Community threads

`Secret is not bound to agent` with intact `secret_ref`: bindings-table desync

Justin Simpson · 2026-05-24

Posting this in case it saves someone else half a day. We hit a credential-binding failure on our production Paperclip install (v2026.517.0, embedded Postgres). The symptom: Error: Secret is not bound to agent:<UUID> at env.ANTHROPICAPIKEY with status 422 on adapter init, while the public API surface showed every binding intact. The actual cause turned out to be a two-table desync, not a fingerprint issue (which is what I'd previously assumed, incorrectly), so the diagnostic and recovery look different from what the symptom suggests.

What it looks like from the outside.

Agents fail at heartbeat with the error above. GET /api/companies/<id>/agents returns each affected agent with adapterConfig.env.ANTHROPICAPIKEY still showing secretref:<id>...v=latest. Restart does not clear it. The agent JSON reads like a healthy binding; the resolver insists nothing is bound.

Source trace.

Paperclip stores secret binding metadata in two places: each agent's adapterConfig.env.<KEY> JSON column, and a separate central companysecretbindings table. The runtime resolver reads from the central table, not the agent JSON.

assertBindingContext at services/secrets.js:178 queries companySecretBindings for a row keyed by (companyId, secretId, targetType, targetId, configPath). If no row is present, it throws the literal Secret is not bound to agent:<UUID> at env.<KEY> with status 422. It does not check fingerprints; that's a separate code path on the write side.

The sync point between the two locations is syncEnvBindingsForTarget in the same file, around line 1531. Its body opens a transaction, deletes all env. bindings for the target, then runs if (refs.length === 0) return; await tx.insert(...). If the caller passes empty refs, the delete fires, the insert is skipped, and nothing is logged.

In our case, some caller path between 19 and 20 May fired this with empty refs for nine bound agents at once. The trigger isn't captured in any log we still have. The agents were silently unbound for three days; only one kept producing because it was already on the inline config.llm.apiKey fallback rather than reading from the secret store.

Diagnostic probe.

GET /api/secrets/<id>/usage returns the actual rows in companysecretbindings for that secret. If the response shows fewer rows than the agent JSON would suggest, that is the desync. This was the cleanest read for us, because the agent-side surface is misleading by design here.

Answers

Aron Prins · 2026-05-24

Appreciate you @justinssimpson - thanks for sharing this!