From 9a0b898410f10744a249a0e5ef09bf2a2049d065 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Thu, 25 Jun 2026 23:27:20 +0000 Subject: [PATCH] Add dynamic secrets operator guide Document the shipped dynamic secret host-command contract, generator safety guidance, trust posture, TTL guidance, and a GitHub App installation-token example. Link the guide from existing secrets docs and surface the guidance requirement in the experimental settings copy. Co-Authored-By: Paperclip --- doc/DATABASE.md | 5 + doc/DEVELOPING.md | 5 + doc/DYNAMIC-SECRETS.md | 139 ++++++++++++++++++ doc/SPEC-implementation.md | 3 + .../InstanceExperimentalSettings.test.tsx | 2 +- ui/src/pages/InstanceExperimentalSettings.tsx | 2 +- 6 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 doc/DYNAMIC-SECRETS.md diff --git a/doc/DATABASE.md b/doc/DATABASE.md index 43dfc05a8af..b0a25354a32 100644 --- a/doc/DATABASE.md +++ b/doc/DATABASE.md @@ -189,6 +189,11 @@ For local/default installs, the active provider is `local_encrypted`: - Backup/restore requires both the database metadata and the local master key file; either artifact alone is insufficient. - The server best-effort enforces `0600` key file permissions and provider health reports permission warnings. +Dynamic command-backed secrets are experimental and use host-side generator +commands instead of stored secret material. For the generator invocation +contract, trust posture, TTL guidance, and a GitHub App token example, see +[DYNAMIC-SECRETS.md](./DYNAMIC-SECRETS.md). + Optional overrides: - `PAPERCLIP_SECRETS_MASTER_KEY` (32-byte key as base64, hex, or raw 32-char string) diff --git a/doc/DEVELOPING.md b/doc/DEVELOPING.md index f15c244098e..3050319f56a 100644 --- a/doc/DEVELOPING.md +++ b/doc/DEVELOPING.md @@ -645,6 +645,11 @@ CLI configuration support: - `pnpm paperclipai doctor` validates secrets adapter configuration, can create a missing local key file with `--repair`, and reports missing AWS Secrets Manager bootstrap env when that provider is selected. - Provider health is available at `GET /api/companies/:companyId/secret-providers/health` and reports local key permission warnings plus backup guidance. +Dynamic secrets are experimental host-command generators. Before enabling +`enableDynamicSecrets` or attaching a generator, read +`doc/DYNAMIC-SECRETS.md` for the shipped invocation contract, fail-closed +behavior, trust posture, and TTL guidance. + Per-company provider vaults are configured in the board UI under `Company Settings → Secrets → Provider vaults`, backed by `/api/companies/{companyId}/secret-provider-configs`. The CLI does not own diff --git a/doc/DYNAMIC-SECRETS.md b/doc/DYNAMIC-SECRETS.md new file mode 100644 index 00000000000..1b3620b857f --- /dev/null +++ b/doc/DYNAMIC-SECRETS.md @@ -0,0 +1,139 @@ +# Dynamic Secrets Operator Guide + +Dynamic secrets are experimental command-backed secrets. They let an operator +store a generator definition instead of storing secret material. At runtime, +Paperclip runs the generator on the host and injects the generated value into the +target environment. + +This guide is about safe generator authoring. Paperclip can enforce the runtime +contract described here, but it does not certify that a generator is safe or that +the token it mints has the right privileges. The operator owns that trust +decision. + +## Runtime Contract + +A dynamic secret uses `managedMode: "dynamic_command"` and provider +`host_command`. It is available only when the `enableDynamicSecrets` +experimental setting is enabled. + +The shipped runtime behavior is: + +- The generator command is stored on the secret as a single executable path or + command string. +- Static positional arguments are stored on each secret binding. Agents cannot + provide or alter these arguments at runtime. +- Paperclip invokes the generator with `spawn(command, staticArgv, { shell: + false })`. There is no shell interpolation, and stdin is ignored. +- The generator inherits the Paperclip server process environment. +- `stdout` is the generated secret value. Paperclip trims surrounding whitespace + from stdout before injection. +- `stderr` is not returned to the agent or board UI. It only affects the failure + message by indicating that host logs may contain details. +- Exit code `0` with non-empty stdout succeeds. +- Missing command, non-zero exit, signal termination, timeout, or empty stdout + fails closed with no value injected. +- The default timeout is 30 seconds. `PAPERCLIP_DYNAMIC_SECRET_COMMAND_TIMEOUT_MS` + can lower it or set another positive value up to that 30 second ceiling. +- Successful generated values are cached by binding and static argv for the + secret's configured TTL. During that TTL, Paperclip injects the cached value + instead of re-running the generator. + +Dry runs from the board UI use the same command contract, but return only a +pass/fail result plus non-sensitive metadata such as byte length. They never +return the generated value. + +## Safety Practices + +Treat the generator as privileged host-side code. + +- Keep the root credential on the host side, outside Paperclip agent workspaces + and outside agent-readable configuration. +- Prefer a dedicated helper binary or script with a narrow interface over a + general-purpose shell wrapper. +- Mint the narrowest token possible for the specific binding. Use static argv + for operator-controlled selectors such as organization, repository, account, + role, or installation id. +- Make the minted token short-lived. Avoid long-lived bearer tokens when the + upstream service supports scoped temporary credentials. +- Never print the root secret, refresh token, private key, or credential source. + Print only the derived runtime token to stdout. +- Keep stderr non-sensitive. Stderr may be referenced in host logs during + debugging. +- Make generators idempotent and side-effect-light. A generator may be retried + after a timeout or process failure, and a failed injection should not leave + durable external state behind. +- Fail with a non-zero exit when the generated credential would be missing, + over-broad, expired, or otherwise unsafe. +- Avoid reading mutable input from the agent workspace. Runtime arguments are + already fixed by the operator on the binding. +- Run the board UI dry-run after authoring or changing a generator, then verify + the target workflow with the least-privilege binding you expect agents to use. + +## Trust Posture + +Dynamic secrets reduce storage of long-lived secret material in Paperclip, but +they do not automatically create a hard isolation boundary. + +Under the current local/default posture, dynamic generators are a soft boundary. +The generator runs as the same operating-system user as the Paperclip server, +similar to the `local_encrypted` provider posture. A same-UID agent or process +with enough local access can potentially observe process state, host files, or +runtime values. Use this posture for trusted local operation, not as containment +against a hostile same-UID agent. + +A hard boundary requires an approved sandbox or environment driver that isolates +the generator and injected value from the host and from untrusted agent code. +Do not describe a deployment as hard-isolated just because it uses dynamic +secrets. + +## TTL Guidance + +TTL controls Paperclip's successful-value cache. It should be no longer than the +usable lifetime of the minted token and usually shorter. + +Choose a TTL by balancing freshness and upstream rate limits: + +- For tokens valid for minutes, use a TTL of seconds to a few minutes. +- For tokens valid for about one hour, a TTL around 5 to 15 minutes is a common + starting point. +- Lower the TTL when revocation speed or least privilege matters more than API + rate limits. +- Raise the TTL only when the generator is expensive or the upstream service has + tight token-minting limits. +- Do not set the TTL equal to or beyond the upstream token expiry. Clock skew, + queued runs, and retry timing can otherwise inject a nearly expired token. + +## Worked Example: GitHub App Installation Token + +Goal: let an agent push to one repository without giving it the GitHub App +private key. + +Host setup: + +- Store the GitHub App private key outside the repo and outside agent + workspaces, for example under a root- or operator-owned secrets directory. +- Install a small generator at an absolute path such as + `/usr/local/bin/paperclip-mint-github-installation-token`. +- Make the generator accept only operator-fixed static argv such as: + `["--installation-id", "123456", "--repository", "owner/repo"]`. +- The generator authenticates as the GitHub App, requests an installation access + token scoped to that repository and only the permissions needed for the task, + prints the token to stdout, and exits `0`. +- On any API, key, permission, or repository mismatch, the generator prints no + token and exits non-zero. + +Paperclip setup: + +- Create a dynamic secret with command + `/usr/local/bin/paperclip-mint-github-installation-token`. +- Set the binding static argv for the target agent, project, or routine to the + installation id and repository it may access. +- Set TTL lower than the GitHub installation token lifetime. For a one-hour + GitHub App installation token, start around 600 seconds and adjust for rate + limits and run frequency. +- Run the board UI generator test before saving, then run a task that needs only + that repository permission. + +The agent receives only the installation token selected for that binding. It +does not receive the GitHub App private key and cannot choose a different +installation or repository unless the operator changes the binding argv. diff --git a/doc/SPEC-implementation.md b/doc/SPEC-implementation.md index e3246ed6da9..91cf96a55dd 100644 --- a/doc/SPEC-implementation.md +++ b/doc/SPEC-implementation.md @@ -358,6 +358,9 @@ Operational policy: - Config read APIs redact sensitive plain values. - Activity and approval payloads must not persist raw sensitive values. - Config revisions may include redacted placeholders; such revisions are non-restorable for redacted fields. +- Dynamic command-backed secrets are experimental host-command generators. The + operator-facing runtime contract and safe authoring guidance live in + `doc/DYNAMIC-SECRETS.md`. ## 7.14 Required Indexes diff --git a/ui/src/pages/InstanceExperimentalSettings.test.tsx b/ui/src/pages/InstanceExperimentalSettings.test.tsx index 5d2fcf296cf..3900686c507 100644 --- a/ui/src/pages/InstanceExperimentalSettings.test.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.test.tsx @@ -201,7 +201,7 @@ describe("InstanceExperimentalSettings — Conference Room Chat card (PAP-11233) expect(container.textContent).toContain("Dynamic Secrets"); expect(container.textContent).toContain( - "Enables experimental dynamic secret resolution for later-stage secret bindings and command-backed values.", + "Enables experimental command-backed secret resolution. Review the dynamic secrets operator guide before attaching generator commands.", ); const toggle = container.querySelector(DYNAMIC_SECRETS_TOGGLE_SELECTOR); diff --git a/ui/src/pages/InstanceExperimentalSettings.tsx b/ui/src/pages/InstanceExperimentalSettings.tsx index 3c502112fbd..b98445f0e3a 100644 --- a/ui/src/pages/InstanceExperimentalSettings.tsx +++ b/ui/src/pages/InstanceExperimentalSettings.tsx @@ -341,7 +341,7 @@ export function InstanceExperimentalSettings() {

Dynamic Secrets

- Enables experimental dynamic secret resolution for later-stage secret bindings and command-backed values. + Enables experimental command-backed secret resolution. Review the dynamic secrets operator guide before attaching generator commands.