Move secrets between users and computers without the model seeing them

A new personal env scope (paradigm user: one user's secret for one bot)
beats every other scope, reaches only that user's runs of that bot, and
is redacted like any secret. Bots take secrets in with request_secret,
which puts a [secret-request:ID] card in the chat: the user types the
value into a masked input there, never into a message, and the bot is
told only the key. Bots show secrets with capture_secret (a task's
computer tool that saves a file or a command's output straight to a
personal env, then redacts it from the rest of the run) and show_secret
(an env by key): both give a [secret:ID] chip whose Reveal and Copy fetch
the value on click, for its own user (personal) or a super admin
(anything else), logged by key. Values never go into messages, page
props, history, Copy chat or logs. Users manage their own secrets on
Settings > Secrets; the Environment page lists everyone's, remove only.
Deleting a bot or user removes its personal secrets, and SecretsNote
tells bots never to ask for or print secrets in chat.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-25 16:51:32 +01:00
co-authored by Claude Opus 5.5
parent 83ff79b9c3
commit 69988e08fc
78 changed files with 2472 additions and 52 deletions
+20 -1
View File
@@ -51,12 +51,26 @@ export const ProposalRefHrefPrefix = "#proposal-";
*/
export const HookProposalRefHrefPrefix = "#hook-proposal-";
/**
* The link target a secret ref (`secretRef`) becomes, e.g. `#secret-ref-12`.
* The chat renders it as a masked chip with Reveal and Copy.
*/
export const SecretRefHrefPrefix = "#secret-ref-";
/**
* The link target a secret request ref (`secretRequestRef`) becomes, e.g.
* `#secret-request-4`. The chat renders it as a chip that opens the card
* the user types the secret into.
*/
export const SecretRequestRefHrefPrefix = "#secret-request-";
/**
* Turns every computer ref (`[computer:3]`) and take-over ref
* (`[take-over:3:browser]`), bot ref (`[bot:3]`), workspace ref
* (`[workspace:2]`), file ref (`[file:12]`), folder ref
* (`[folder:computer:3:seo/raw]`), proposal ref (`[proposal:7]`) and hook
* proposal ref (`[hook-proposal:4]`) in a message into a markdown link, so it
* proposal ref (`[hook-proposal:4]`), secret ref (`[secret:12]`) and secret
* request ref (`[secret-request:4]`) in a message into a markdown link, so it
* survives markdown rendering as a link the chat can swap for a button,
* chip or download.
*
@@ -87,6 +101,11 @@ export default function linkComputerRefs({ text }: { text: string }) {
/\[hook-proposal:(\d+)\](?!\()/g,
(_, id: string) => `[hook proposal ${id}](${HookProposalRefHrefPrefix}${id})`,
)
.replace(/\[secret:(\d+)\](?!\()/g, (_, id: string) => `[secret ${id}](${SecretRefHrefPrefix}${id})`)
.replace(
/\[secret-request:(\d+)\](?!\()/g,
(_, id: string) => `[secret request ${id}](${SecretRequestRefHrefPrefix}${id})`,
)
.replace(/\[file:(\d+)\](?!\()/g, (_, id: string) => `[file ${id}](${FileRefHrefPrefix}${id})`)
.replace(/\[folder:([a-z]+):(\d+):([^\]\n]+)\](?!\()/g, (_, volume: string, owner: string, path: string) =>
`[${path}](${FolderRefHrefPrefix}${encodeURIComponent(`${volume}:${owner}:${path}`)})`,