550 lines
18 KiB
Markdown
550 lines
18 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",
|
|
"secretRef": "11111111-1111-4111-8111-111111111111",
|
|
"name": "GitHub token",
|
|
"description": null,
|
|
"delivery": "env",
|
|
"projectionClass": "unclassified",
|
|
"latestVersion": 2,
|
|
"versionSelector": "latest",
|
|
"resolvedVersion": 2
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
`delivery` is `env`, `api`, or `both`. `secretRef` is a stable opaque handle,
|
|
not secret material or a capability; every consuming route re-authorizes it.
|
|
The list never returns values, the internal `secretId` field, 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.
|
|
|
|
## Agent Secret Proposals
|
|
|
|
These routes use the same current run-bound agent JWT as the list and fetch
|
|
routes:
|
|
|
|
```
|
|
POST /api/agents/me/secret-proposals
|
|
GET /api/agents/me/secret-proposals
|
|
DELETE /api/agents/me/secret-proposals/{proposalId}
|
|
```
|
|
|
|
An agent can ask Paperclip to bind an existing secret under a new path without
|
|
knowing a secret ID. Set `kind` to `binding` and identify the source by the
|
|
agent's own existing `env.*` or `access.*` config path:
|
|
|
|
```json
|
|
POST /api/agents/me/secret-proposals
|
|
{
|
|
"kind": "binding",
|
|
"sourceConfigPath": "access.openai_api_key",
|
|
"configPath": "access.evals_openai_api_key",
|
|
"justification": "Use the existing OpenAI credential under the eval-specific alias"
|
|
}
|
|
```
|
|
|
|
`sourceConfigPath` must resolve from the proposing agent's own binding. An
|
|
unknown path or another agent's path returns `404`. A binding request must
|
|
provide exactly one of `sourceConfigPath`, `secretId`, or `secretProposalId`.
|
|
Omit `targetAgentId` to target the proposing agent; under the default policy a
|
|
manager may instead target one of its reports. `configPath` accepts
|
|
`env.<KEY>` for environment injection or `access.<ALIAS>` for API-only access.
|
|
|
|
For a run with a checked-out origin issue, a successful proposal automatically
|
|
creates a human-only **Confirm secret binding** card in that issue. API clients
|
|
must not create a second interaction. The card contains only non-secret
|
|
metadata: the source label, target agent, new config path, justification, and
|
|
expiry.
|
|
|
|
Selecting **Create binding** accepts the card and then triggers a separate,
|
|
freshly authorized binding write. Card acceptance is not execution. Read
|
|
`result.secretProposal.status` for the actual outcome:
|
|
|
|
- `executed` means the binding write completed.
|
|
- `failed` means the card was accepted but execution failed. The card renders
|
|
**FAILED**, exposes a non-secret `errorCode`, and Paperclip posts a **Secret
|
|
binding execution failed** comment with `Binding created: no`.
|
|
- `rejected`, `withdrawn`, or `expired` means no binding was created.
|
|
|
|
The card wakes the issue assignee after resolution. The wake payload includes
|
|
`secretProposal.configPath`, `decision`, `executionStatus`, and instructions.
|
|
After any secret card, call `GET /api/agents/me/secrets` again and confirm the
|
|
expected secret metadata and delivery before using the new binding. Acceptance
|
|
is not execution; a failed wake or missing metadata means the alias must be
|
|
treated as unavailable until a fresh proposal executes successfully.
|
|
|
|
## 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
|
|
npx 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
|
|
npx paperclipai secrets declarations --company-id {companyId}
|
|
```
|
|
|
|
to inspect the declarations that an export would emit before moving a package.
|