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 <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip committed 2026-06-25 23:27:20 +00:00
1 parent 2649f5945d
commit 9a0b898410
6 files changed
+154 -2

No files matched your search

+5
View File
@@ -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)
+5
View File
@@ -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
+139
View File
@@ -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.
+3
View File
@@ -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
@@ -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<HTMLButtonElement>(DYNAMIC_SECRETS_TOGGLE_SELECTOR);
@@ -341,7 +341,7 @@ export function InstanceExperimentalSettings() {
<div className="space-y-1.5">
<h2 className="text-sm font-semibold">Dynamic Secrets</h2>
<p className="max-w-2xl text-sm text-muted-foreground">
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.
</p>
</div>
<ToggleSwitch