Skip to content

chore(isolation): host-materialized provider facts (split from #436, blocked on quickstart#504) #456

Closed
GSA-TTS/agentic-coding-quickstart
#608

Description

@wz-gsa

Split out of #436, which removed a guest-written provider-facts file rather than ship it at the wrong trust level.

What was removed and why

acq-kits/usai-provider/spec.yaml wrote /var/lib/acq/models/providers/usai.json from inside the sandbox at startup as root. The rationale in the spec was that a root-owned guest path is not agent-writable. That is DAC-true and false as a security property: acq itself grants the agent passwordless sudo —

acq.backends/msb.sh (quickstart, origin/main):
  printf "agent ALL=(ALL) NOPASSWD:ALL\n" > /etc/sudoers.d/90-acq-agent

— and docs/BACKEND_GUIDE.md states the same guarantee as the sbx base-image contract. So sudo tee defeats it.

That matters more than a generic integrity concern because the facts file advertises keyEnv, a credential pointer. An agent able to rewrite it can point a consumer at a different env var. Confused-deputy primitive, shipping with a comment telling consumers to trust it.

What this issue needs

A host-materialized, read-only provider-facts source, per ADR-0004 (#423):

  • acq validates facts host-side before materializing — providerId as a safe whole-string slug, unique and matching its directory; keyEnv ownership (a kit may only name a credential it declares); modelsUrl https:// with host equal to the declared host and on the effective caps.network.allow union.
  • Materialized per-sandbox and mounted :ro at /var/lib/acq/host/models/providers/<providerId>/.
  • Note this path differs from the one feat(usai-provider): normalize and vendor USAi model catalog #436 removed (/var/lib/acq/models/providers/<id>.json). Consumers should be written against the host path only.

Blocked on

The :ro host-config mount does not exist on quickstart main. It lives on the unmerged branch origin/docs/adr-host-authoritative-config (GSA-TTS/agentic-coding-quickstart#504). Measured, not assumed — I had a subagent trace the provisioning path.

Also relevant, measured in the same pass:

  • Kit phases are a closed allowlist (install|initFiles|startup) and all three run in the guest. There is no host phase a kit can declare, so this cannot be solved kit-side.
  • Kit files[] are copied verbatim; no templating step exists.
  • acq does already generate kits/scripts host-side pre-create (twice: the sbx git-identity kit and the msb startup script), so the generate-and-inject shape exists to build on.

Not in scope here

Reconciling acq-neutral-model-catalog/v1 (introduced by ADR-0004) with the already-shipped usai-model-catalog/v1 in integrations/providers/usai/. Two catalog schemas coexist; tracked at #435, and it should land before a consumer depends on either.


AI-assisted (OpenCode), filed on operator instruction. Advisory — the design decision is a maintainer call.

Activity

  1. wz-gsa commented on Oct 1, 2026

    @wz-gsa
    ContributorAuthor

    Prior art worth weighing before this is built — Docker's Sandbox Kit Spec v3 (docker/sandbox-kit-spec, Apache-2.0, experimental, final targeted Q4 2026) solves the exact problem this issue exists for, and solves it differently from how this issue is currently specified.

    Why it is relevant here

    This issue inherits #436's framing: a facts file advertising keyEnv, the NAME of the env var holding the credential, materialized somewhere a consumer can trust. The trust problem was the location of the file.

    Docker's credential@1 capability makes the location question mostly disappear, because the credential never enters the guest at all:

    MUST NOT place the real secret in the container when proxyManaged or OAuth sentinels are in play: where the Kit names a variable or file, the container sees sentinel values there, and the boundary (proxy) substitutes the real credential on outbound requests matching the inject rules or resourceHosts.

    MUST resolve the credential from the host-side store keyed by service. Host environment variables never auto-inject; a Kit cannot name where a secret lives, only what it needs.

    A declaration looks like this:

    - type: com.docker.sandbox/credential@1
      config:
        service: github
        phase: runtime
        apiKey:
          name: GH_TOKEN
          proxyManaged: true
          inject:
            - {domain: api.github.com, header: Authorization, format: "Bearer %s"}

    The container gets GH_TOKEN set to a sentinel, deliberately non-empty "so Kit content can detect wiring without seeing the secret". An inject-only credential (no name) gets no environment presence at all.

    Two further properties we have no equivalent for:

    • Phase-scoped with revocation. "an install-only credential is injectable only while install hooks run and is revoked before the workload's entrypoint starts".
    • Cross-validated against the network policy. inject[].domain "MUST appear in the network policy's allow list for every declared phase" — because "an inject domain outside the allow list would be a credential mapped onto a connection that can never occur."

    What this would change about this issue

    If that model is adopted, keyEnv stops being a trust-sensitive field. Today an agent that rewrites the facts file redirects a consumer to a different env var and harvests whatever is there — which is why #436's version had to be removed. Under the sentinel model there is nothing in the guest to harvest, so the facts file's integrity matters much less and the host-side :ro materialization this issue is blocked on becomes a smaller requirement rather than the whole security control.

    I am not proposing we adopt it. Three honest reasons to be cautious:

    1. It requires a credential-substituting egress proxy. acq has no such component, and building one is far larger than this issue.
    2. The spec is explicitly experimental and still moving.
    3. It is OCI/BuildKit-coupled in ways acq's backend-neutral design (sbx and msb) is not.

    But it is the structurally correct answer to the problem, and this issue should at least record that keyEnv-in-the-guest is a weaker design that was chosen for buildability, not because it is the right shape.

    One correction to my own earlier framing

    I initially read this as "acq is on a deprecated kit line that should migrate to v3." That was wrong, and measuring it inverted the conclusion:

    $ sbx kit validate <kit with schemaVersion "3">
    error: manifest: unsupported schemaVersion "3" (supported: [1 2])
    

    v3 is not a schemaVersion bump — it is a different artifact format (an OCI image carrying declarations in a vnd.docker.sandbox.kit.descriptor manifest annotation). sbx v0.46.0 rejects "3" outright. So there is no migration available today and nothing here is urgent on that basis. The separate, real risk — that nothing checks whether the grammar acq does emit is still accepted — is filed as GSA-TTS/agentic-coding-quickstart#529.

    Smaller borrowable detail

    Docker's lifecycle@1 files[] entries carry no user: field at all, with the rationale: "A path only root can own is an install hook's job — what install[].user is for." That is a cleaner statement of the rule #436 violated, and it is enforced by the grammar's shape rather than by review.


    AI-assisted (OpenCode), on operator instruction. Research summary — treat the quoted spec text as the authority, not my paraphrase.

  2. linked a pull request that will close this issuefeat(acq): project provider inputs read-only #608on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions