Model credentials

How a model API key reaches the agent without the agent (or the wire, or the worker host) ever holding it in clear, and how the worker keys that make that possible are pinned. Threat model: ../run-trust.md.

Code: app/credentials.py (resolution), app/crypto/sealed.py + swarm/src/security.py (sealed box + keypairs), app/routers/orch.py (/orch/seal, enrollment consume), swarm/src/sandbox.py (the broker).

Two layers: sealing + brokering

A credential is protected twice:

  1. Sealed to the worker (in transit + at rest on the worker). At claim time pond resolves the credential (project scope org fallback) and seals its env to the claiming worker’s X25519 pubkey via /orch/seal. The cleartext lives in pond memory only long enough to seal; the worker unseals with its private key (unseal_credential). Fail-closed: no pinned pubkey no seal (never a cleartext fallback). The poll response itself carries only a cred_request intent (name + project) — see worker-pool-protocol.

  2. Brokered (in use, away from the agent). Even unsealed, the key isn’t put in the agent’s env. A broker sidecar holds the real key and injects it on the way upstream; the agent env has only a dummy key + BROKER_URL pointing at the broker (the sole egress on broker-only/allowlist networks — see sandbox-isolation). A prompt-injected agent can’t read a key it never had. The broker also never honors a client-supplied upstream host, so it can’t be turned into an open proxy.

broker_plan is fail-closed: a broker-only stage whose credential isn’t a recognized base-URL-honoring provider is refused rather than run with the raw key in the agent env.

Worker keys

A worker holds two persisted keypairs (each graceful-degrade — absent cryptography simply disables that feature):

KeyTypePurposeCache
sealingX25519unseal credentials + bundle keys (key agreement)*.x25519
signingEd25519sign result attestations*.ed25519

They are different operations and intentionally different keys — a key-agreement key can’t sign.

Pubkey pinning at enrollment

The whole scheme rests on pond pinning the right worker pubkey. The defense against a day-1 compromised orchestrator (which relays the worker’s keys):

  • An operator mints an enrollment code and may paste the worker’s expected fingerprint (read out-of-band via swarm fingerprint). At /orch/enrollment/{code}/consumed the forwarded X25519 pubkey’s fingerprint must match or the consume is refused (the enrollment stays unconsumed).
  • Both the X25519 sealing key (pinned_pubkey) and the Ed25519 signing key (pinned_sig_pubkey) are pinned at that same gated consume; the worker snapshot then prefers the enrollment pin over first-sight TOFU.
  • A later snapshot presenting a different pubkey is rejected (loud warning) — rotation requires evict + re-enroll.

So even a malicious orchestrator can’t swap in keys it controls when an operator fingerprint is set; without one, the pins are TOFU (still closes drive-by forgery by non-keyholders).

Result attestation (uses the signing key)

The worker signs (job_id, input_sha, output_sha, rc) with its pinned Ed25519 key at done; pond verifies against pinned_sig_pubkey and that input_sha matches the issued source bundle. Design + flow: ../run-trust.md and worker-source-delivery.

Invariants

  • The cleartext credential exists only transiently in pond memory at seal time; it is never persisted unencrypted, never on the poll wire, never in the agent env.
  • Sealing/bundle keys are sealed to the specific claiming worker.
  • With an operator fingerprint set, an orchestrator can’t forge the pinned keys.
  • Sealing (X25519) and signing (Ed25519) keys are distinct.