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, } }, )