Files
ten31-database/start9/0.4/startos/actions/setAnthropicApiKey.ts
T
Keysat 3d9caac178 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>
2026-06-05 13:52:26 -05:00

130 lines
4.7 KiB
TypeScript

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