Skip to content

Credential Source Pattern (cross-cutting)

Status: Open — design pattern (Phase 5, Agent Orchestrator Research Program)

jackin’ already has several credential-resolution paths, documented in Environment Variables and Authentication. They do not yet share one typed credential-source abstraction.

Several upcoming items need more sources:

If each item adds its own source resolution code, drift is guaranteed. The right move is a single cross-cutting pattern that any feature can plug into.

This isn’t a feature that ships on its own — it’s a refactor that unblocks several other features. The leaf exists so the design call is made once, in one place, instead of being rediscovered five times.

  • Without a unified pattern, adding a new credential source (Apple Keychain, Linux secret-tool, AWS Secrets Manager, Vault) means touching every consumer.
  • multicode has already tried three sources (env, command, keychain) for one credential (their GitHub PAT) and converged on a small consistent shape. Borrowing the shape is much cheaper than designing one from scratch.
  • It pairs with jackin-remote and the future Kubernetes platform — both of which need to know how to resolve credentials locally and forward them remotely.

Sources:

[github]
# One of three forms:
token = { env = "GITHUB_TOKEN" }
token = { command = "gh auth token" }
token = { keychain-service = "multicode.github", keychain-account = "github-mcp-token" }

multicode also exposes populate-git-credentials = true for auto-injecting the token as a git credential helper inside the container.

The shape: a tagged-union TOML value with multiple resolver backends, each backend implementing a small trait (fn resolve() -> Result<SecretString>). Resolution happens at jackin-process startup (or per-invocation, depending on the consumer).

A CredentialSource type that’s accepted anywhere a token/secret is needed in TOML. The resolver vocabulary is small and explicit; no implicit chains.

# Direct literal — convenience, NOT recommended for real secrets
some_token = "sk-actual-value"
# Tagged forms
some_token = { env = "MY_VAR" }
some_token = { command = "gh auth token" }
some_token = { op = "op://Vault/Item/Field" }
some_token = { os_store = "jackin.token", account = "default" }
some_token = { file = "~/.secrets/token" }

Six backends:

  1. Literal (string) — for non-secret values; emits a warning if a string field tagged #[credential] is supplied as a literal in any operator-config file outside ~/.config/jackin/local.toml.
  2. env — read host-operator env var; error if unset. This is never broad environment inheritance into the agent container.
  3. command — run a command without a shell by default, with a minimal environment; the command must exit 0 and may only return the secret on stdout.
  4. op — route to 1Password CLI using the current operator-env behavior.
  5. OS secret stores — Apple Keychain via security on macOS, libsecret/secret-tool on Linux, and Windows Credential Manager when supported. New backend.
  6. file — read file contents. Useful for K8s secret mounts and local development.
  • Resolution happens at the consumer’s first need (lazy), not at config load. This means a misconfigured backend errors only when the feature using it actually runs.
  • Resolved values implement SecretString (zeroize-on-drop, no Display impl, redacted in debug logs).
  • Failures are explicit and contextual: GitHub PAT (configured via [github].token = { command = ... }) failed: <stderr from command>.
  • Resolved values are not cached across runs of jackin. Within one run, they may be cached per-source-instance.

Each consumer (GitHub link tracker, task source, Claude auth, future ones) accepts Option<CredentialSource> in its config and resolves when it needs the value. No consumer reaches around the abstraction.

populate_git_credentials (multicode’s name) generalizes to a per-credential forward_to_container = true flag on the consumer’s config block. Implementation: jackin’ writes the resolved secret into a container env var or file mount per the consumer’s spec, never to the agent’s persisted state directory.

  • CredentialSource enum + TOML deserialization.
  • Six backends: literal, env, command, op, OS secret store, file.
  • SecretString newtype with zeroize-on-drop.
  • Replace existing ad-hoc credential lookups with the unified type while preserving the behavior documented under Environment Variables and Authentication.
  • New consumers (GitHub link tracking, task sources) accept CredentialSource from day one.
  • Documentation page: “Credential sources” — explains all six backends, security tradeoffs, recommended pattern per use case.
  • Vault / AWS Secrets Manager / GCP Secret Manager backends. Defer until a real user asks; the file-mount pattern + K8s secrets covers most of the space.
  • Per-source resolution caching with TTL. Lazy-once-per-run is enough for V1.
  • A jackin secrets test CLI that resolves every configured credential and reports status. Useful but small; defer.
  • Literal-as-secret warning. Should jackin’ refuse to load a literal where a CredentialSource is expected (with override flag), or just warn loudly? Recommended: warn loudly; the convenience for non-secret tokens (e.g. a Slack webhook URL) outweighs strictness.
  • OS secret-store implementation. macOS Keychain and Linux secret-tool are the first practical targets; Windows Credential Manager should share the same resolver shape once a Windows host story matters.
  • Forwarding interaction with Claude auth strategy. That item is already considering token plumbing; this pattern should be the abstraction it uses, not a parallel design. Recommended: make this leaf land first so the Claude auth work has the type to use.
  • New module (e.g. src/credential.rs) — CredentialSource enum + resolver
  • src/config/mod.rs — TOML deserialization
  • Existing op:// resolution path — refactored to use the unified type
  • Future consumers — link to this leaf as their credential plumbing
  • New docs page: docs/src/content/docs/guides/credentials.mdx