LearnNewsExamplesServices
Frontmatter
id12032
titleGitMirror credentialRef: support file: scheme for tenant secrets
stateClosed
labels
enhancementaiarchitecture
assigneesneo-gpt
createdAtMay 26, 2026, 8:37 PM
updatedAtMay 26, 2026, 10:32 PM
githubUrlhttps://github.com/neomjs/neo/issues/12032
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
blockedBy[]
blocking[]
closedAtMay 26, 2026, 10:32 PM

GitMirror credentialRef: support file: scheme for tenant secrets

Closed v13.0.0/archive-v13-0-0-chunk-14 enhancementaiarchitecture
neo-opus-ada
neo-opus-ada commented on May 26, 2026, 8:37 PM

Context

The current GitMirror.normalizeCredentialRef (ai/services/knowledge-base/helpers/GitMirror.mjs:96-125) supports only three credentialRef shapes — env:VAR, ssh:keyPath, and bare-string (treated as env:). For single-tenant deployments this is sufficient. For multi-tenant deployments — even modest ones with 2–5 tenant repos — the env: indirection forces operators to:

  1. Declare one NEO_TENANT_<SLUG>_TOKEN env var per tenant in the deployment's environment-source file.
  2. Either list each var explicitly in the orchestrator service's environment: block (compose grows linearly with tenant count), OR adopt env_file: (which over-broadly pulls all env-source entries into the container, including unrelated secrets like OAuth client secrets and session signing keys).

Surfaced during a first-real-world cloud deployment exercise — friction-to-gold signal post-#12030 RawRepoSource.

The Problem

env: indirection bottoms out at the OS-level env-var surface, which has two limitations:

  • Container-runtime granularity: env vars are passed to the entire container process, not per-secret. Docker's native secrets: directive mounts secrets as files at /run/secrets/<name> with per-file UID/GID + mode control — a finer-grained primitive that env: cannot consume.
  • Production secret stores: HashiCorp Vault, AWS Secrets Manager, GCP Secret Manager, and Kubernetes Secrets all expose the canonical "secret bytes available at a filesystem path" pattern. Today's GitMirror credentialRef cannot consume any of them without an intermediate "read file, set env var, restart process" shim.

The narrower friction — "operator wants to add 2nd tenant without bloating the orchestrator's environment: block" — is the entry point. The broader gap — "credentialRef cannot consume the dominant secret-distribution primitive (filesystem mount)" — is what justifies substrate work.

The Architectural Reality

The Fix

Extend normalizeCredentialRef to accept a third string scheme:

  • file:/absolute/path/to/secret{type: 'file', filePath: '/absolute/path/to/secret'}

Add a matching branch in resolveCredentialEnvironment:

  • Read filePath via fs.readFile(..., 'utf-8'), trim whitespace.
  • Error on empty resolved contents with the same KB_GITMIRROR_CREDENTIAL_REF_INVALID code path as env-not-set.
  • Reuse the existing askpass-temp-dir + NEO_GITMIRROR_PASSWORD + NEO_GITMIRROR_USERNAME pattern identically.
  • Default NEO_GITMIRROR_USERNAME to 'x-access-token' (matching the env branch's GitLab/GitHub deploy-token convention).
  • Object shape {type: 'file', filePath} passes through normalizeCredentialRef unchanged for callers that need to set a non-default username.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
credentialRef: 'file:/path' (new string scheme) ai/services/knowledge-base/helpers/GitMirror.mjs normalizeCredentialRef Resolved to {type: 'file', filePath} shape, secret bytes read at git subprocess boundary, never persisted env: and ssh: schemes unchanged New scheme added to the cloud-deployment credential-boundary guide Unit test for file-resolution branch; integration test using /tmp/test-secret fixture
credentialRef: {type: 'file', filePath} (object shape) Same as above Same resolution path Same Same Object-shape pass-through test

Decision Record impact

aligned-with ADR 0014 — extends the credential-boundary surface within the existing tenant-ingestion subsystem without redrawing service boundaries. ADR 0014's tenant-repo amendment names #11787 as the credential-boundary prerequisite; this ticket extends #11787's substrate forward without superseding it.

Acceptance Criteria

  • normalizeCredentialRef recognizes file:/absolute/path string form → {type: 'file', filePath} shape.
  • Object shape {type: 'file', filePath} passes through normalizeCredentialRef unchanged.
  • resolveCredentialEnvironment adds file branch: reads filePath, trims, errors on empty/missing with KB_GITMIRROR_CREDENTIAL_REF_INVALID, sets NEO_GITMIRROR_PASSWORD + GIT_ASKPASS identical to env branch.
  • Unit tests for the new branch: (a) string form file:/tmp/<fixture>, (b) object form, (c) empty-file rejection, (d) missing-file rejection.
  • Secret-redaction backstop test extended: secretHints includes the file-resolved token; redaction sweeps git stdout/stderr the same way as env-resolved tokens.
  • Docs note in the cloud-deployment credential-boundary guide: Docker secrets: directive pattern + Kubernetes Secret mount pattern both unlock with file:/run/secrets/<name>.

Out of Scope

  • vault: / secrets-manager: schemes — separate tickets once a deployment actually needs them. Premature abstraction is a worse tax than the missing scheme.
  • Pluggable resolver plugin architecture — string-based dispatch with a small fixed set of schemes is sufficient until we have 4+ schemes to discriminate.
  • Deployment-side migration — downstream deployments that switch from env:file: are operator decisions; this ticket only widens the surface.

Avoided Traps

  • Treating this as "just add another scheme": filesystem-mounted secrets is the canonical primitive for Docker secrets + every major cloud secret store. It's a substrate gap, not a feature request.
  • Adding vault: speculatively: no deployment is currently using Vault. Each new scheme adds an integration surface; one-scheme-at-a-time keeps the substrate lean.

Related

  • Substrate foundation: #11787 (config + credentialed contract), #11788 (GitMirror primitive)
  • Parent epic: #11731 (Server-side tenant-repo ingestion)
  • Empirical anchor: #12030 (RawRepoSource opt-in, merged)
tobiu closed this issue on May 26, 2026, 10:32 PM
tobiu referenced in commit 6d3d0cc - "feat(kb): support file credential refs (#12032) (#12046) on May 26, 2026, 10:32 PM