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:
@@ -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,
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user