3d9caac178
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>
130 lines
4.7 KiB
TypeScript
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,
|
|
}
|
|
},
|
|
)
|