mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 20:05:57 +02:00
## Thinking Path > - Paperclip is the open source control plane people use to manage AI-agent companies. > - Agents already receive selected company secrets through `env.*` bindings at run launch, but environment injection is ambient, long-lived, and not suitable for every secret consumer. > - The existing binding and secret-access-event models already provide company-scoped authorization and per-resolution audit seams. > - Agents need an explicit way to discover only the secrets granted to them and fetch a value on demand without exposing the wider company catalog. > - That capability must remain run-bound, preserve low-trust token carve-outs, and make every value read visible in both security and operator audit trails. > - This pull request adds an `access.*` delivery namespace, two run-bound agent routes, dual audit logging, documentation, and an operator grants editor. > - The benefit is least-privilege, revocable, auditable secret access while preserving existing env injection behavior. ## Linked Issues or Issue Description No pre-existing public issue. Related work: - Refs #9797 — existing in-sheet agent access UI that this PR extends to distinguish env and API delivery. - Refs #9918 — complementary searchable-agent picker improvement for the same secrets sheet. - Refs #9530 — related company-wide metadata catalog proposal; this PR intentionally exposes only the authenticated run's granted aliases and values. **Problem / motivation:** Agents can currently consume secrets only through process environment injection. This keeps values resident for the run, does not support on-demand consumers, and cannot provide a discrete operator-visible activity event for each agent-initiated read. **Proposed solution:** Treat `company_secret_bindings` as the source of truth for agent secret grants. Keep `env.KEY` as env delivery and add `access.ALIAS` for API-only delivery; an env binding also implies read access because the value is already present in the agent process. Add run-bound list/fetch endpoints that derive scope from the authenticated heartbeat run and never accept caller-selected overlays. **Alternatives considered:** A company-wide agent-readable catalog was rejected for this value path because it increases reconnaissance and does not prove a per-secret grant. Reusing the ephemeral environment-probe resolver was rejected because it lacks binding enforcement. Approval-gated reads and user-scoped secrets remain deferred beyond v1. **Roadmap alignment:** This extends the completed **Secrets Manager with per-agent access** roadmap capability from launch-time env injection to explicit run-bound API delivery without duplicating a separate planned initiative. ## What Changed - Added `access.*` agent binding validation and a dedicated run-bound resolver that combines `secrets:read` authorization with binding-context enforcement. - Added `GET /api/agents/me/secrets` for minimal granted metadata and `POST /api/agents/me/secrets/:key/value` for on-demand value fetches with `Cache-Control: no-store`. - Preserved the existing denials for low-trust review agents, task-bridge credentials, and skill-test tokens; standard long-lived agent API keys cannot call the run-bound routes. - Added dual audit behavior: value attempts write `secret_access_events` and `activity_log` (`secret.value.read`), while metadata listing writes the lighter `secret.access.listed` activity event. - Kept env compatibility: `env.*` remains injected at launch and also implies API read for the same bound agent; `access.*` never becomes an environment variable. - Added the agent-settings **Secret access** editor plus delivery-mode/alias surfacing on the Secrets page, with focused UI tests and tokenized layout styles. - Updated OpenAPI, shared types, agent-facing skill documentation, and API reference documentation. ### UI Screenshots P3 produced and reviewed three screenshots using mock data; images are intentionally not committed to the repository: - `secret-access-editor.png` — agent settings grant editor. - `secret-access-light.png` — Secrets-page delivery surfacing in light mode. - `secret-access-dark.png` — Secrets-page delivery surfacing in dark mode. The source attachments are retained with the implementation task and linked in the internal handoff; the public page publisher was unavailable in the PR-prep runtime. ## Verification - `pnpm exec vitest run server/src/__tests__/agent-secrets-routes.test.ts server/src/__tests__/secrets-service.test.ts server/src/__tests__/secrets-routes.test.ts ui/src/lib/secret-delivery.test.ts ui/src/components/AgentSecretAccessEditor.test.tsx` — 5 files, 122 tests passed. - Security follow-up: `pnpm exec vitest run server/src/__tests__/agent-secrets-routes.test.ts server/src/__tests__/secrets-service.test.ts` — 2 files, 73 tests passed after active-run and version-consistency fixes. - Final-head CI: all feature, typecheck, build, e2e, security, and review gates pass; `General tests (server (1/3))` remains red after one rerun because unrelated `heartbeat-retry-scheduling.test.ts` cleanup deletes `heartbeat_runs` before referenced `activity_log` rows. - `pnpm --filter @paperclipai/server typecheck` — passed. - `pnpm --filter @paperclipai/shared typecheck` — passed. - `pnpm --filter @paperclipai/ui typecheck` — passed. - `pnpm check:token-gates` — feature-local arbitrary-value violations fixed; command still reports five unchanged `#9627` literals outside this PR. - End-to-end QA passed all eight acceptance criteria: grant/list, fetch, dual audit, env-implies-read, denial matrix, revocation, UI rendering, and env-injection regression. Evidence: https://github.com/paperclipai/paperclip/pull/9921#issuecomment-5027455492 - Security review returned PASS-with-required-changes; the implementation uses the required dedicated binding-enforcing resolver, run-bound JWT restriction, run-derived overlays, minimal metadata, and a resolver redaction-registration hook. Evidence: https://github.com/paperclipai/paperclip/pull/9921#issuecomment-5027455382 ## Risks - A compromised agent can exfiltrate any secret explicitly granted to it; explicit company-scoped/run-scoped grants, revocation, and audit reduce but cannot remove that inherent capability risk. - The resolver invokes a redaction-registration hook before returning values, but the current route has no persistent cross-request per-run redaction registry. Paperclip-owned later comments/events therefore cannot yet guarantee automatic scrubbing of a deliberately copied fetched value; QA classified this as non-blocking residual hardening. - Audit-event insertion currently fails open if the security-event insert itself fails; the operator activity event provides partial redundancy, but a future hardening change should define fail-closed behavior for value delivery. - This PR overlaps `ui/src/pages/Secrets.tsx` with #9918 and may require a straightforward rebase after that PR moves. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI Codex, `gpt-5.3-codex`, with reasoning, repository tool use, terminal execution, Paperclip API access, and GitHub CLI capabilities. Context-window size is not exposed by the runtime. - Anthropic Claude Opus 4.8 with 1M context and tool use assisted with the UI implementation commit. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
492 lines
15 KiB
Markdown
492 lines
15 KiB
Markdown
---
|
|
title: Secrets
|
|
summary: Secrets CRUD
|
|
---
|
|
|
|
Manage encrypted secrets that agents receive through environment bindings or fetch on demand.
|
|
|
|
## Agent List and Fetch
|
|
|
|
These routes require the current run-bound agent JWT. They are not available to
|
|
long-lived agent keys, low-trust review agents, task-bridge keys, or skill-test
|
|
tokens.
|
|
|
|
List the secrets accessible to the current run without materializing values:
|
|
|
|
```
|
|
GET /api/agents/me/secrets
|
|
```
|
|
|
|
```json
|
|
{
|
|
"secrets": [
|
|
{
|
|
"key": "github_token",
|
|
"name": "GitHub token",
|
|
"description": null,
|
|
"delivery": "env",
|
|
"projectionClass": "unclassified",
|
|
"latestVersion": 2,
|
|
"versionSelector": "latest",
|
|
"resolvedVersion": 2
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`delivery` is `env`, `api`, or `both`. The list never returns values, secret
|
|
IDs, binding IDs, or config paths. An `env.*` binding implies read access through
|
|
this API; an `access.*` binding grants API access without environment injection.
|
|
|
|
Fetch a value only when it is needed. The request has no body and the response
|
|
uses `Cache-Control: no-store`:
|
|
|
|
```
|
|
POST /api/agents/me/secrets/github_token/value
|
|
```
|
|
|
|
```json
|
|
{
|
|
"key": "github_token",
|
|
"value": "decrypted-secret-value",
|
|
"version": 2
|
|
}
|
|
```
|
|
|
|
Prefer env injection when the adapter or its child processes need the value on
|
|
every run. Prefer on-demand fetch for values used only on some runs, large or
|
|
structured values, or skills and tools that do not inherit adapter env. Every
|
|
successful or failed value fetch is audited in both `secret_access_events` and
|
|
`activity_log`; agents must not log or paste fetched values into issues,
|
|
comments, or documents.
|
|
|
|
## List Secrets
|
|
|
|
```
|
|
GET /api/companies/{companyId}/secrets
|
|
```
|
|
|
|
Returns secret metadata (not decrypted values).
|
|
|
|
## Create Secret
|
|
|
|
```
|
|
POST /api/companies/{companyId}/secrets
|
|
{
|
|
"name": "anthropic-api-key",
|
|
"value": "sk-ant-..."
|
|
}
|
|
```
|
|
|
|
The value is encrypted at rest. Only the secret ID and metadata are returned.
|
|
|
|
To link a provider-owned secret without copying the value into Paperclip, create
|
|
an external-reference secret:
|
|
|
|
```json
|
|
{
|
|
"name": "prod-stripe-key",
|
|
"provider": "aws_secrets_manager",
|
|
"managedMode": "external_reference",
|
|
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/stripe",
|
|
"providerVersionRef": "version-id-or-label"
|
|
}
|
|
```
|
|
|
|
Paperclip stores the provider reference and a non-sensitive fingerprint only.
|
|
The value is resolved, when the provider is configured, through the server
|
|
runtime path that enforces binding context and records access events.
|
|
|
|
## Provider Health
|
|
|
|
```
|
|
GET /api/companies/{companyId}/secret-providers/health
|
|
```
|
|
|
|
Returns provider setup diagnostics, warnings, and local backup guidance. Health
|
|
responses must not include secret values or provider credentials.
|
|
|
|
For `aws_secrets_manager`, an unready health response names the missing
|
|
non-secret provider environment variables, the AWS SDK default credential source
|
|
expected by the server runtime, and the custody rule that AWS bootstrap
|
|
credentials must not be stored in Paperclip `company_secrets`.
|
|
|
|
The equivalent CLI check is:
|
|
|
|
```sh
|
|
pnpm paperclipai secrets doctor --company-id {companyId}
|
|
```
|
|
|
|
## Provider Vaults
|
|
|
|
Provider vaults are named, company-scoped configurations that route secret
|
|
material to one of the supported provider backends. See the
|
|
[secrets deploy guide](/deploy/secrets#provider-vaults) for the operator model
|
|
and custody rules.
|
|
|
|
All routes below require board auth and company access. Mutating routes emit
|
|
`secret_provider_config.*` activity-log entries. No route in this surface
|
|
returns provider credential values; submitting credential-shaped fields in
|
|
`config` is rejected at validation time.
|
|
|
|
### List Vaults
|
|
|
|
```
|
|
GET /api/companies/{companyId}/secret-provider-configs
|
|
```
|
|
|
|
Returns every vault for the company (including disabled rows for audit), each
|
|
with id, provider, displayName, status, isDefault, non-sensitive `config`,
|
|
latest health snapshot (`healthStatus`, `healthCheckedAt`, `healthMessage`,
|
|
`healthDetails`), `disabledAt`, and audit columns.
|
|
|
|
### Create Vault
|
|
|
|
```
|
|
POST /api/companies/{companyId}/secret-provider-configs
|
|
{
|
|
"provider": "aws_secrets_manager",
|
|
"displayName": "Prod US-East",
|
|
"isDefault": true,
|
|
"config": {
|
|
"region": "us-east-1",
|
|
"namespace": "paperclip",
|
|
"secretNamePrefix": "paperclip",
|
|
"kmsKeyId": "arn:aws:kms:us-east-1:123456789012:key/abcd-...",
|
|
"environmentTag": "production"
|
|
}
|
|
}
|
|
```
|
|
|
|
Per-provider `config` shapes:
|
|
|
|
- `local_encrypted`: optional `backupReminderAcknowledged: boolean`.
|
|
- `aws_secrets_manager`: required `region`; optional `namespace`,
|
|
`secretNamePrefix`, `kmsKeyId`, `ownerTag`, `environmentTag`.
|
|
- `gcp_secret_manager` (coming soon): optional `projectId`, `location`,
|
|
`namespace`, `secretNamePrefix`.
|
|
- `vault` (coming soon): optional origin-only HTTPS `address`, `namespace`,
|
|
`mountPath`, `secretPathPrefix`. `address` values with embedded credentials,
|
|
paths, query strings, or fragments are rejected.
|
|
|
|
`status` defaults to `ready` for `local_encrypted` and `aws_secrets_manager`,
|
|
and to `coming_soon` for `gcp_secret_manager` and `vault`. Coming-soon and
|
|
disabled vaults cannot be marked `isDefault`. Setting `isDefault: true` clears
|
|
the previous default for the same provider in the same transaction.
|
|
|
|
### Get Vault
|
|
|
|
```
|
|
GET /api/secret-provider-configs/{id}
|
|
```
|
|
|
|
### Update Vault
|
|
|
|
```
|
|
PATCH /api/secret-provider-configs/{id}
|
|
{
|
|
"displayName": "Prod US-East-2",
|
|
"config": {
|
|
"region": "us-east-2",
|
|
"kmsKeyId": "arn:aws:kms:us-east-2:123456789012:key/abcd-..."
|
|
}
|
|
}
|
|
```
|
|
|
|
`config` is replaced wholesale on update — pass the full provider config
|
|
payload, not a partial diff. Status transitions for `gcp_secret_manager` and
|
|
`vault` are constrained to `coming_soon` and `disabled` until their runtime
|
|
modules ship.
|
|
|
|
### Disable Vault
|
|
|
|
```
|
|
DELETE /api/secret-provider-configs/{id}
|
|
```
|
|
|
|
Soft-deletes the vault: status flips to `disabled`, `isDefault` clears, and
|
|
`disabledAt` is stamped. Disabled vaults remain in `GET` results for audit
|
|
purposes but are no longer offered in the secret create/rotate flow.
|
|
|
|
### Set Default
|
|
|
|
```
|
|
POST /api/secret-provider-configs/{id}/default
|
|
```
|
|
|
|
Marks the target vault as the default for its provider family and clears the
|
|
previous default. Returns 422 when the target is `coming_soon` or `disabled`.
|
|
|
|
### Run Health Check
|
|
|
|
```
|
|
POST /api/secret-provider-configs/{id}/health
|
|
```
|
|
|
|
Runs a provider-specific health probe and persists the result on the vault.
|
|
Response shape:
|
|
|
|
```json
|
|
{
|
|
"configId": "<uuid>",
|
|
"provider": "aws_secrets_manager",
|
|
"status": "ready" | "warning" | "error" | "coming_soon" | "disabled",
|
|
"message": "Provider vault is ready to handle managed writes",
|
|
"details": {
|
|
"code": "provider_ready",
|
|
"message": "...",
|
|
"guidance": ["..."]
|
|
},
|
|
"checkedAt": "2026-05-06T14:00:00.000Z"
|
|
}
|
|
```
|
|
|
|
Health responses never include provider credentials or secret values. For AWS
|
|
vaults, `details.guidance` may include missing non-secret env names and the
|
|
expected AWS SDK credential source; coming-soon vaults always return
|
|
`status: "coming_soon"` with `code: "runtime_locked"` and never call into
|
|
provider modules.
|
|
|
|
### Selecting A Vault When Creating Or Rotating Secrets
|
|
|
|
`POST /api/companies/{companyId}/secrets` and
|
|
`POST /api/secrets/{secretId}/rotate` both accept an optional
|
|
`providerConfigId` field that pins the secret to a specific vault. When
|
|
omitted (or null), the operation runs through the deployment-level provider
|
|
configuration — the same path existing installs already use. The board UI
|
|
preselects the company's default vault for the chosen provider before
|
|
submitting, so callers should usually send an explicit `providerConfigId`.
|
|
Coming-soon and disabled vaults are rejected with a 422; a vault that does not
|
|
match the secret's provider is rejected the same way.
|
|
|
|
```json
|
|
POST /api/companies/{companyId}/secrets
|
|
{
|
|
"name": "prod-stripe-key",
|
|
"provider": "aws_secrets_manager",
|
|
"providerConfigId": "<vault-uuid>",
|
|
"managedMode": "external_reference",
|
|
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:paperclip/prod/stripe"
|
|
}
|
|
```
|
|
|
|
### Response Redaction Rules
|
|
|
|
Every route in this surface enforces the same redaction contract:
|
|
|
|
- Secret values are never returned. The board UI never has a "reveal value"
|
|
affordance; resolution happens server-side at runtime under a binding.
|
|
- Provider credential values are never accepted, stored, returned, logged, or
|
|
echoed in error messages. Submitting credential-shaped fields fails
|
|
validation with a non-leaking error.
|
|
- Activity log entries record vault id, provider, displayName, status, and
|
|
isDefault transitions — never `config` payloads or health detail bodies.
|
|
|
|
## Remote Import From AWS Secrets Manager
|
|
|
|
Remote import links existing AWS Secrets Manager entries into Paperclip as
|
|
`external_reference` secrets. Import stores provider reference metadata only; it
|
|
does not copy the remote secret plaintext into Paperclip.
|
|
|
|
The routes are board-only and company-scoped. `providerConfigId` must point to
|
|
a same-company AWS provider vault with status `ready` or `warning`. Disabled,
|
|
coming-soon, non-AWS, and cross-company vaults are rejected. Imported secrets
|
|
resolve later through the selected vault, so runtime reads still need
|
|
`secretsmanager:GetSecretValue` and any required KMS decrypt permission on the
|
|
selected external secret.
|
|
|
|
### Preview Remote Import Candidates
|
|
|
|
```
|
|
POST /api/companies/{companyId}/secrets/remote-import/preview
|
|
{
|
|
"providerConfigId": "<aws-vault-uuid>",
|
|
"query": "stripe",
|
|
"nextToken": "opaque-provider-token",
|
|
"pageSize": 50
|
|
}
|
|
```
|
|
|
|
`query` is optional and is passed to AWS Secrets Manager inventory filtering.
|
|
Treat it as non-secret metadata because AWS may record list request parameters
|
|
in CloudTrail. `nextToken` is an opaque AWS cursor; callers must pass it back
|
|
unchanged and must not synthesize offsets. `pageSize` is optional, defaults to
|
|
50 in the UI, and is capped at 100.
|
|
|
|
Preview uses AWS `ListSecrets` only. It must not call `GetSecretValue` or
|
|
`BatchGetSecretValue`, must not request `SecretString`, and must not require KMS
|
|
decrypt. The response contains sanitized metadata for display and conflict
|
|
decisions:
|
|
|
|
```json
|
|
{
|
|
"providerConfigId": "<aws-vault-uuid>",
|
|
"provider": "aws_secrets_manager",
|
|
"nextToken": null,
|
|
"candidates": [
|
|
{
|
|
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
|
"remoteName": "prod/stripe",
|
|
"name": "prod/stripe",
|
|
"key": "prod-stripe",
|
|
"providerVersionRef": null,
|
|
"providerMetadata": {
|
|
"createdDate": "2026-05-06T00:00:00.000Z",
|
|
"lastChangedDate": "2026-05-06T00:00:00.000Z",
|
|
"hasDescription": true,
|
|
"hasKmsKey": true,
|
|
"tagCount": 3
|
|
},
|
|
"status": "ready",
|
|
"importable": true,
|
|
"conflicts": []
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Candidate statuses:
|
|
|
|
- `ready`: the row can be selected for import.
|
|
- `duplicate`: a Paperclip secret already links the same canonical provider
|
|
reference for the same provider vault.
|
|
- `conflict`: the row has a name/key collision or provider guardrail failure.
|
|
|
|
Conflict types are `exact_reference`, `name`, `key`, and
|
|
`provider_guardrail`. AWS refs under Paperclip's own managed namespace are
|
|
blocked as external references; use the Paperclip-managed secret flow for those
|
|
resources instead.
|
|
|
|
### Import Selected Remote References
|
|
|
|
```
|
|
POST /api/companies/{companyId}/secrets/remote-import
|
|
{
|
|
"providerConfigId": "<aws-vault-uuid>",
|
|
"secrets": [
|
|
{
|
|
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
|
"name": "Stripe production key",
|
|
"key": "stripe-production-key",
|
|
"description": "Stripe key used by production checkout",
|
|
"providerVersionRef": null,
|
|
"providerMetadata": {
|
|
"createdDate": "2026-05-06T00:00:00.000Z"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
The `secrets` array accepts 1-100 rows. Each row may override the suggested
|
|
Paperclip `name`, `key`, optional Paperclip `description`,
|
|
`providerVersionRef`, and sanitized `providerMetadata`. Blank descriptions are
|
|
stored as `null`; AWS provider descriptions are not copied into Paperclip
|
|
descriptions. The backend re-checks duplicate refs and name/key conflicts at
|
|
submit time; a stale preview does not bypass those checks.
|
|
|
|
The import response is row-level:
|
|
|
|
```json
|
|
{
|
|
"providerConfigId": "<aws-vault-uuid>",
|
|
"provider": "aws_secrets_manager",
|
|
"importedCount": 1,
|
|
"skippedCount": 1,
|
|
"errorCount": 0,
|
|
"results": [
|
|
{
|
|
"externalRef": "arn:aws:secretsmanager:us-east-1:123456789012:secret:prod/stripe",
|
|
"name": "Stripe production key",
|
|
"key": "stripe-production-key",
|
|
"status": "imported",
|
|
"reason": null,
|
|
"secretId": "<paperclip-secret-id>",
|
|
"conflicts": []
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
Row statuses:
|
|
|
|
- `imported`: Paperclip created an active `external_reference` secret and one
|
|
metadata-only version row.
|
|
- `skipped`: the row had an exact-reference duplicate or name/key conflict.
|
|
- `error`: the provider rejected the reference or the row failed validation.
|
|
|
|
Activity logs for preview/import store aggregate counts, provider id, and vault
|
|
id only. They must not store remote secret names, ARNs, descriptions, tags,
|
|
plaintext values, provider credentials, or raw AWS error blobs.
|
|
|
|
## Rotate Secret
|
|
|
|
```
|
|
POST /api/secrets/{secretId}/rotate
|
|
{
|
|
"value": "sk-ant-new-value..."
|
|
}
|
|
```
|
|
|
|
Creates a new version of the secret. Agents referencing `"version": "latest"`
|
|
automatically get the new value on next heartbeat. Pin to a specific version
|
|
when a bad `latest` rollout would affect many agents at once.
|
|
|
|
## Using Secrets in Agent Config
|
|
|
|
Reference secrets in agent adapter config instead of inline values:
|
|
|
|
```json
|
|
{
|
|
"env": {
|
|
"ANTHROPIC_API_KEY": {
|
|
"type": "secret_ref",
|
|
"secretId": "{secretId}",
|
|
"version": "latest"
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
The server resolves and decrypts secret references at runtime, injecting the
|
|
real value into the agent process environment. Paperclip's custody guarantees
|
|
end at injection: the agent process can read, log, or forward the value, so
|
|
treat any secret bound to an agent as exposed to that agent. See the custody
|
|
boundaries note in the [secrets deploy guide](/deploy/secrets#custody-boundaries).
|
|
|
|
User-specific env bindings use a definition key instead of a concrete
|
|
`secretId`. The concrete value is resolved for the run's responsible user:
|
|
|
|
```json
|
|
{
|
|
"env": {
|
|
"GITHUB_TOKEN": {
|
|
"type": "user_secret_ref",
|
|
"key": "github_api_token",
|
|
"version": "latest",
|
|
"required": true,
|
|
"allowMissingOverride": false
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
`required` defaults to `true` and `allowMissingOverride` defaults to `false`.
|
|
Missing required user-secret values must fail closed before adapter dispatch.
|
|
Optional missing values omit the environment variable; they must not inject an
|
|
empty string or another user's value. Paperclip records value-free access
|
|
events with `secretScope`, `responsibleUserId`, `credentialOwnerUserId`, and
|
|
`userSecretDefinitionId`.
|
|
|
|
## Portability
|
|
|
|
Company export/import APIs represent agent and project environment requirements
|
|
as declarations in the package manifest. Exports omit secret values, secret IDs,
|
|
provider references, and encrypted provider material. Use:
|
|
|
|
```sh
|
|
pnpm paperclipai secrets declarations --company-id {companyId}
|
|
```
|
|
|
|
to inspect the declarations that an export would emit before moving a package.
|