Deferred from: stage 1 (#89). The bucket + role separation (arch.md §17 per-data-class layer) is shipped in this PR; the worker itself is deferred to stage 2.
Background
Per arch.md §15.1 credentials-service:
- Worker takes a cap-token + (service, plaintext_or_ciphertext)
- Derives the per-actor KEK via mTLS to the signer (
derive_cred_kek(operator_omni, k3_epoch))
- AES-256-GCM encrypt/decrypt under that KEK
- PUTs / GETs the blob at
s3://$VAULT_BUCKET/bots/<actor_omni>/credentials/<service>.enc
- Independent re-verify of the cap against the on-chain ScopeContract before any S3 / signer touch
What's already in place (no work needed)
| Piece |
State |
Source |
| Per-actor KEK derivation |
shipped (client-side) |
crates/agentkeys-core/src/s3_backend.rs:222 — derives via signer's /dev/sign-message |
| AES-256-GCM envelope (v1 + v2 shapes) |
shipped (client-side) |
same file, store_credential / read_credential |
OIDC AssumeRoleWithWebIdentity → STS session w/ agentkeys_actor_omni tag |
shipped in this PR |
crates/agentkeys-broker-server/src/handlers/oidc.rs |
$VAULT_BUCKET separate from $EMAIL_BUCKET per arch.md §17 |
shipped in this PR |
scripts/provision-vault-bucket.sh + scripts/provision-vault-role.sh + scripts/apply-vault-bucket-policy.sh |
agentkeys-vault-role with credentials-only inline policy |
shipped in this PR |
scripts/provision-vault-role.sh |
Bucket policy gated on agentkeys_actor_omni PrincipalTag |
shipped in this PR |
scripts/apply-vault-bucket-policy.sh |
Today the CLI does the encrypt + direct S3 PUT (acting as the worker). This is functionally correct and produces the same envelope bytes the worker will produce; the architecture violation is purely topological — the encrypt should happen inside the worker, not on the operator's laptop.
What this issue ships
- Lambda function (
agentkeys-worker-creds per arch.md §28 layout):
- Endpoint
POST /v1/cred/store — takes {cap, service, plaintext}, verifies cap on-chain, derives KEK via signer mTLS, encrypts, PUTs to $VAULT_BUCKET/bots/<actor>/credentials/<service>.enc
- Endpoint
POST /v1/cred/fetch — takes {cap, service}, verifies cap, GETs blob, decrypts via signer-derived KEK, returns plaintext over the TLS-terminated channel
- Endpoint
POST /v1/cred/teardown — takes {cap, target_actor}, verifies cap, wipes bots/<target_actor>/credentials/* prefix
- mTLS plumbing: worker presents its attestation-issued cert; signer accepts only attested worker callers (arch.md §11)
- CLI flag
--credential-backend=sidecar activates the daemon → worker path (today returns "not yet implemented")
- Sidecar daemon hop:
agentkeys-daemon proxies CLI requests to the worker (cap-token cache + SSE drop event push)
Acceptance
agentkeys store openrouter sk-or-... with --credential-backend=sidecar succeeds and the blob lands at s3://$VAULT_BUCKET/bots/<actor>/credentials/openrouter.enc — same path the v2 envelope-v2 CLI writes today
agentkeys read openrouter with --credential-backend=sidecar returns the plaintext
- Cap-mint without the right scope is rejected by the worker (independent re-verify against chain)
- KEK never leaves the signer enclave; the operator's laptop never sees it
Why this is stage 2
The worker is architecturally required but operationally optional for stage-1 smoke testing. The CLI's direct path produces correct envelope bytes; future migration to the worker is invisible to anyone reading them later (same KEK, same AAD, same nonce shape). Deferring lets stage 1 ship the on-chain registry + sidecar daemon + WebAuthn binding first; stage 2 adds the worker hop.
See arch.md §15.1 + §28 (agentkeys-worker-creds/ layout) for the full design.
🤖 Generated with Claude Code
Deferred from: stage 1 (#89). The bucket + role separation (arch.md §17 per-data-class layer) is shipped in this PR; the worker itself is deferred to stage 2.
Background
Per arch.md §15.1 credentials-service:
derive_cred_kek(operator_omni, k3_epoch))s3://$VAULT_BUCKET/bots/<actor_omni>/credentials/<service>.encWhat's already in place (no work needed)
crates/agentkeys-core/src/s3_backend.rs:222— derives via signer's/dev/sign-messagestore_credential/read_credentialagentkeys_actor_omnitagcrates/agentkeys-broker-server/src/handlers/oidc.rs$VAULT_BUCKETseparate from$EMAIL_BUCKETper arch.md §17scripts/provision-vault-bucket.sh+scripts/provision-vault-role.sh+scripts/apply-vault-bucket-policy.shagentkeys-vault-rolewith credentials-only inline policyscripts/provision-vault-role.shagentkeys_actor_omniPrincipalTagscripts/apply-vault-bucket-policy.shToday the CLI does the encrypt + direct S3 PUT (acting as the worker). This is functionally correct and produces the same envelope bytes the worker will produce; the architecture violation is purely topological — the encrypt should happen inside the worker, not on the operator's laptop.
What this issue ships
agentkeys-worker-credsper arch.md §28 layout):POST /v1/cred/store— takes{cap, service, plaintext}, verifies cap on-chain, derives KEK via signer mTLS, encrypts, PUTs to$VAULT_BUCKET/bots/<actor>/credentials/<service>.encPOST /v1/cred/fetch— takes{cap, service}, verifies cap, GETs blob, decrypts via signer-derived KEK, returns plaintext over the TLS-terminated channelPOST /v1/cred/teardown— takes{cap, target_actor}, verifies cap, wipesbots/<target_actor>/credentials/*prefix--credential-backend=sidecaractivates the daemon → worker path (today returns "not yet implemented")agentkeys-daemonproxies CLI requests to the worker (cap-token cache + SSE drop event push)Acceptance
agentkeys store openrouter sk-or-...with--credential-backend=sidecarsucceeds and the blob lands ats3://$VAULT_BUCKET/bots/<actor>/credentials/openrouter.enc— same path the v2 envelope-v2 CLI writes todayagentkeys read openrouterwith--credential-backend=sidecarreturns the plaintextWhy this is stage 2
The worker is architecturally required but operationally optional for stage-1 smoke testing. The CLI's direct path produces correct envelope bytes; future migration to the worker is invisible to anyone reading them later (same KEK, same AAD, same nonce shape). Deferring lets stage 1 ship the on-chain registry + sidecar daemon + WebAuthn binding first; stage 2 adds the worker hop.
See arch.md §15.1 + §28 (
agentkeys-worker-creds/layout) for the full design.🤖 Generated with Claude Code