Add "Set Anthropic API Key" StartOS UI action (v0.1.0:50)
Lets a non-technical operator install the Architect's Claude key from the StartOS UI instead of the terminal: a masked text field whose value is written to /data/secrets/anthropic-api-key (0600) on the box — the same file the entrypoint already loads at boot. Secret is piped over stdin (never argv/env), CR/LF stripped to match the entrypoint's read. allowedStatuses 'any'; a restart is required (and stated in the action's warning + success message) since the entrypoint reads the key only at startup. Verified the Architect's data boundary first: the deployed Thesis Workshop routes send only Ten31's own thesis text (thesis_lines/thesis_nodes) + the partner-typed guidance to Claude — no contacts/lp_profiles/communications/grid. (The MCP CRM-retrieval tools that DO return record substance are not wired into the deployed Architect; the redaction boundary must land before any grounding path uses them — Phase 1 Workstream D.) tsc --noEmit clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -2,8 +2,10 @@ import { sdk } from '../sdk'
|
|||||||
import { buildSearchIndex } from './buildSearchIndex'
|
import { buildSearchIndex } from './buildSearchIndex'
|
||||||
import { refreshSearchIndex } from './refreshSearchIndex'
|
import { refreshSearchIndex } from './refreshSearchIndex'
|
||||||
import { resolveDuplicates } from './resolveDuplicates'
|
import { resolveDuplicates } from './resolveDuplicates'
|
||||||
|
import { setAnthropicApiKey } from './setAnthropicApiKey'
|
||||||
|
|
||||||
export const actions = sdk.Actions.of()
|
export const actions = sdk.Actions.of()
|
||||||
.addAction(buildSearchIndex)
|
.addAction(buildSearchIndex)
|
||||||
.addAction(refreshSearchIndex)
|
.addAction(refreshSearchIndex)
|
||||||
.addAction(resolveDuplicates)
|
.addAction(resolveDuplicates)
|
||||||
|
.addAction(setAnthropicApiKey)
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { i18n } from '../i18n'
|
||||||
|
import { sdk } from '../sdk'
|
||||||
|
import { DATA_MOUNT_PATH, IMAGE_ID } from '../utils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "Set Anthropic API Key (Architect)" action.
|
||||||
|
*
|
||||||
|
* The Architect agent (Phase-1 thesis generation) runs on Claude. The container
|
||||||
|
* entrypoint (docker_entrypoint.sh) loads the key ONCE at boot from
|
||||||
|
* /data/secrets/anthropic-api-key
|
||||||
|
* and exports it as ANTHROPIC_API_KEY into the long-running server process.
|
||||||
|
*
|
||||||
|
* This action lets a non-technical operator paste the key into a masked field
|
||||||
|
* straight from the StartOS UI — no SSH/terminal — and writes it to that file
|
||||||
|
* with owner-only (0600) permissions. The key never leaves the box.
|
||||||
|
*
|
||||||
|
* Two deliberate properties:
|
||||||
|
* - The secret is piped to the writer over STDIN (never argv/env), so it can't
|
||||||
|
* surface in a process listing. We strip stray CR/LF a paste may carry —
|
||||||
|
* exactly what the entrypoint already does when it reads the file back.
|
||||||
|
* - A service RESTART is REQUIRED for the change to take effect: the entrypoint
|
||||||
|
* reads the file only at startup, so the running server won't see a newly
|
||||||
|
* saved key until it is restarted. Both the metadata `warning` and the
|
||||||
|
* success `message` say so.
|
||||||
|
*
|
||||||
|
* Like the ingest actions, this runs in its OWN subcontainer with the main
|
||||||
|
* /data volume mounted read-write; it does NOT go through docker_entrypoint.sh.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const { InputSpec, Value } = sdk
|
||||||
|
|
||||||
|
const KEY_FILE = `${DATA_MOUNT_PATH}/secrets/anthropic-api-key`
|
||||||
|
|
||||||
|
// Single shell command: ensure /data/secrets exists + locked (700), write the
|
||||||
|
// key from stdin stripped of CR/LF, then 0600 the file. umask 077 makes the
|
||||||
|
// created file owner-only even before the explicit chmod. The `tr -d '\n\r'`
|
||||||
|
// mirrors how docker_entrypoint.sh reads the key back.
|
||||||
|
const WRITE_KEY_SCRIPT =
|
||||||
|
'set -eu; umask 077; ' +
|
||||||
|
'mkdir -p "$DATA_DIR/secrets"; chmod 700 "$DATA_DIR/secrets"; ' +
|
||||||
|
"tr -d '\\n\\r' > \"$DATA_DIR/secrets/anthropic-api-key\"; " +
|
||||||
|
'chmod 600 "$DATA_DIR/secrets/anthropic-api-key"'
|
||||||
|
|
||||||
|
export const inputSpec = InputSpec.of({
|
||||||
|
apiKey: Value.text({
|
||||||
|
name: i18n('Anthropic API Key'),
|
||||||
|
description: i18n(
|
||||||
|
'Your Anthropic (Claude) API key. It is written to ' +
|
||||||
|
'/data/secrets/anthropic-api-key on this server (owner-only, 0600) and ' +
|
||||||
|
'never leaves the box. Required for the Architect thesis-generation ' +
|
||||||
|
'features. Leave blank to keep the current key unchanged.',
|
||||||
|
),
|
||||||
|
warning: null,
|
||||||
|
required: true,
|
||||||
|
default: null,
|
||||||
|
// Camouflage the input with dots so the key is not shown on screen.
|
||||||
|
masked: true,
|
||||||
|
placeholder: 'sk-ant-...',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const setAnthropicApiKey = sdk.Action.withInput(
|
||||||
|
// id
|
||||||
|
'set-anthropic-api-key',
|
||||||
|
|
||||||
|
// metadata
|
||||||
|
async ({ effects }) => ({
|
||||||
|
name: i18n('Set Anthropic API Key (Architect)'),
|
||||||
|
description: i18n(
|
||||||
|
'Paste your Anthropic (Claude) API key to enable the Architect ' +
|
||||||
|
'thesis-generation features. The key is stored only on this server at ' +
|
||||||
|
'/data/secrets/anthropic-api-key with owner-only (0600) permissions and ' +
|
||||||
|
'is never sent anywhere except the Anthropic API at generation time. ' +
|
||||||
|
'Restart the service after saving for the key to take effect.',
|
||||||
|
),
|
||||||
|
warning: i18n(
|
||||||
|
'After saving, restart Ten31 Database (Stop, then Start) so the new key is ' +
|
||||||
|
'loaded. The running service does not pick up the key until it restarts.',
|
||||||
|
),
|
||||||
|
allowedStatuses: 'any',
|
||||||
|
group: null,
|
||||||
|
visibility: 'enabled',
|
||||||
|
}),
|
||||||
|
|
||||||
|
// form input specification
|
||||||
|
inputSpec,
|
||||||
|
|
||||||
|
// pre-fill: never read the secret back out — always present a blank field.
|
||||||
|
async ({ effects, prefill }) => null,
|
||||||
|
|
||||||
|
// execution
|
||||||
|
async ({ effects, input }) => {
|
||||||
|
const subcontainer = await sdk.SubContainer.of(
|
||||||
|
effects,
|
||||||
|
{ imageId: IMAGE_ID },
|
||||||
|
sdk.Mounts.of().mountVolume({
|
||||||
|
volumeId: 'main',
|
||||||
|
subpath: null,
|
||||||
|
mountpoint: DATA_MOUNT_PATH,
|
||||||
|
readonly: false,
|
||||||
|
}),
|
||||||
|
'ten31-database-set-anthropic-api-key',
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The key is piped in over stdin (options.input) — never argv/env — so it
|
||||||
|
// cannot appear in a process listing.
|
||||||
|
await subcontainer.execFail(
|
||||||
|
['/bin/sh', '-c', WRITE_KEY_SCRIPT],
|
||||||
|
{ env: { DATA_DIR: DATA_MOUNT_PATH }, input: input.apiKey },
|
||||||
|
60 * 1000,
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
await subcontainer.destroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
version: '1',
|
||||||
|
title: i18n('Anthropic API key saved'),
|
||||||
|
message: i18n(
|
||||||
|
`Your Anthropic API key was written to ${KEY_FILE} (permissions 0600). ` +
|
||||||
|
'IMPORTANT: restart Ten31 Database now (Stop, then Start) so the ' +
|
||||||
|
'Architect picks up the new key, then open the Thesis Workshop to ' +
|
||||||
|
'generate.',
|
||||||
|
),
|
||||||
|
result: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
@@ -14,8 +14,9 @@ export const PACKAGE_TITLE = 'Ten31 Database'
|
|||||||
// * 0.1.0:46 (packaging fix: ship full backend so migrations run + endpoints work)
|
// * 0.1.0:46 (packaging fix: ship full backend so migrations run + endpoints work)
|
||||||
// * 0.1.0:47 (soft-delete instead of hard-delete; source-count diagnostics)
|
// * 0.1.0:47 (soft-delete instead of hard-delete; source-count diagnostics)
|
||||||
// * 0.1.0:48 (entity model: investors vs people; fixes double-count)
|
// * 0.1.0:48 (entity model: investors vs people; fixes double-count)
|
||||||
// * Current: 0.1.0:49 (Architect: Claude thesis generation + Thesis Workshop screen)
|
// * 0.1.0:49 (Architect: Claude thesis generation + Thesis Workshop screen)
|
||||||
export const PACKAGE_VERSION = '0.1.0:49'
|
// * Current: 0.1.0:50 (Set Anthropic API Key UI action — no terminal needed)
|
||||||
|
export const PACKAGE_VERSION = '0.1.0:50'
|
||||||
|
|
||||||
export const DATA_MOUNT_PATH = '/data'
|
export const DATA_MOUNT_PATH = '/data'
|
||||||
export const WEB_PORT = 8080
|
export const WEB_PORT = 8080
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import { v_0_1_0_46 } from './v0.1.0.46'
|
|||||||
import { v_0_1_0_47 } from './v0.1.0.47'
|
import { v_0_1_0_47 } from './v0.1.0.47'
|
||||||
import { v_0_1_0_48 } from './v0.1.0.48'
|
import { v_0_1_0_48 } from './v0.1.0.48'
|
||||||
import { v_0_1_0_49 } from './v0.1.0.49'
|
import { v_0_1_0_49 } from './v0.1.0.49'
|
||||||
|
import { v_0_1_0_50 } from './v0.1.0.50'
|
||||||
|
|
||||||
export const versionGraph = VersionGraph.of({
|
export const versionGraph = VersionGraph.of({
|
||||||
current: v_0_1_0_49,
|
current: v_0_1_0_50,
|
||||||
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48],
|
other: [v_0_1_0_39, v_0_1_0_40, v_0_1_0_41, v_0_1_0_42, v_0_1_0_43, v_0_1_0_44, v_0_1_0_45, v_0_1_0_46, v_0_1_0_47, v_0_1_0_48, v_0_1_0_49],
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { VersionInfo } from '@start9labs/start-sdk'
|
||||||
|
|
||||||
|
// Adds a "Set Anthropic API Key (Architect)" StartOS action so the Claude key
|
||||||
|
// can be installed from the UI — paste it into a masked field and it is written
|
||||||
|
// to /data/secrets/anthropic-api-key (0600) on the box, no terminal/SSH needed.
|
||||||
|
// The Architect only ever sends Ten31's own thesis text + what you type to
|
||||||
|
// Claude; it does not read LP/contact/communication records. Restart the
|
||||||
|
// service after saving so the entrypoint loads the new key. No data migration.
|
||||||
|
export const v_0_1_0_50 = VersionInfo.of({
|
||||||
|
version: '0.1.0:50',
|
||||||
|
releaseNotes: {
|
||||||
|
en_US: [
|
||||||
|
'Adds a "Set Anthropic API Key (Architect)" action: install your Claude API',
|
||||||
|
'key from the StartOS UI (masked field, stored only on the server at',
|
||||||
|
'/data/secrets/anthropic-api-key) — no terminal needed. Restart the service',
|
||||||
|
'after saving so the Architect picks it up.',
|
||||||
|
].join(' '),
|
||||||
|
},
|
||||||
|
migrations: { up: async () => {}, down: async () => {} },
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user