HOMELAB-1669/1747 (LD1): seat provisioning contracts + runtime/MCP server images + MCP install module #523

Merged
aaron merged 6 commits from epic/LD1 into live 2026-09-30 01:22:19 +00:00
Owner

Epic LD1 substrate slice for the MCP spike (HOMELAB-1669) and its precursor contracts (HOMELAB-1747). Three commits:

  • 626f4c9 — agent-sandbox/seat-provisioning-contracts.md: five ratification-ready seat contracts (model endpoint, context injection, PI_OFFLINE/hardening, readOnlyRootFilesystem, ports/router/MCP) + NetworkPolicy posture + 10-item operator-ratification register.
  • 83222f7 — agent-sandbox/runtime-image/ + CI workflow: the :8888 legacy-python runtime sidecar image (lab-director/sandbox-runtime), vendored upstream example source with sha256-pinned requirements.
  • 30e14a1 — agent-sandbox/mcp-server-image/ + CI workflow + core/manifests/agent-sandbox-mcp/ install module (Deployment/Service/namespaced Role+RoleBinding, ClusterIP-only). Nothing syncs the module yet — inert until prod wiring, same pattern as LD0.3's #520.

Class: E-adjacent (RBAC manifests + new cluster namespace objects once wired). Review package + operator decision required before merge; see HOMELAB-1669 for the full landing package. Merge before the prod PR (image builds must exist before the seat template's warm pool pulls).

Epic LD1 substrate slice for the MCP spike (HOMELAB-1669) and its precursor contracts (HOMELAB-1747). Three commits: - `626f4c9` — `agent-sandbox/seat-provisioning-contracts.md`: five ratification-ready seat contracts (model endpoint, context injection, PI_OFFLINE/hardening, readOnlyRootFilesystem, ports/router/MCP) + NetworkPolicy posture + 10-item operator-ratification register. - `83222f7` — `agent-sandbox/runtime-image/` + CI workflow: the :8888 legacy-python runtime sidecar image (`lab-director/sandbox-runtime`), vendored upstream example source with sha256-pinned requirements. - `30e14a1` — `agent-sandbox/mcp-server-image/` + CI workflow + `core/manifests/agent-sandbox-mcp/` install module (Deployment/Service/namespaced Role+RoleBinding, ClusterIP-only). Nothing syncs the module yet — inert until prod wiring, same pattern as LD0.3's #520. **Class: E-adjacent (RBAC manifests + new cluster namespace objects once wired). Review package + operator decision required before merge; see HOMELAB-1669 for the full landing package. Merge before the prod PR (image builds must exist before the seat template's warm pool pulls).**
Five contracts for LD1 seats, re-derived from primary sources:
model endpoint (in-cluster OmniRoute :20128, per-seat TTL sk- keys),
context injection via MCP file API (files in, JSONL out; LD0.2 image
unchanged), PI_SKIP_VERSION_CHECK=1 + PI_OFFLINE=1, readOnlyRootFilesystem
+ /tmp emptyDir, :8888 runtime sidecar + in-cluster MCP server (router
uninstalled). Includes NetworkPolicy posture (Managed + explicit allowlist)
and the operator-ratification register.
Contract §5 of agent-sandbox/seat-provisioning-contracts.md fixes the in-pod
runtime API: the upstream python-runtime-sandbox example as a sidecar on
8888/TCP with SANDBOX_BASE_DIR=/workspace, serving the readiness probe GET /.

- agent-sandbox/runtime-image/Dockerfile: python:3.12-slim pinned by digest
  (f77ac9e4…), upstream main.py + requirements.txt vendored byte-for-byte from
  kubernetes-sigs/agent-sandbox v1.0.4 (hashes recorded in the README and
  re-asserted in CI), deps baked with --require-hashes, non-root uid/gid 1000
  matching the pi image's node user, contract §5 env defaults, EXPOSE 8888.
  No build stage: all 24 pins resolve to prebuilt cp312 wheels (checked before
  pinning the base), so no toolchain is needed.
- agent-sandbox/runtime-image/README.md: contract mapping, pins table (base
  digest, upstream file hashes, python deps), volume/user contract (shared
  /workspace + the second agent-state mount that makes "files in" work),
  tags/promotion, verification, recorded deviations, open questions.
- .forgejo/workflows/runtime-image-build.yaml: mirrors pi-image-build.yaml —
  push on live + PRs touching the dir + workflow_dispatch, <short-sha> on every
  build, v1.0.4/latest alias from live only, vendored-hash assert, GET / and
  POST /execute smoke, 1 GB size gate, digest read-back via crane.

Deviations from upstream/contract are listed in the README (base interpreter
3.12 vs upstream 3.14, log level info vs trace, ML extras baked, no /tmp or
PVC mounts in the image — and contract §4's runtime-code emptyDir at /app would
shadow the baked code, flagged there).
HOMELAB-1669: feat(agent-sandbox): in-cluster MCP server image + install module
Some checks are pending
Plan in progress...
CI Review / receiver-test (pull_request) Successful in 7s
CI Review / helm-validate (pull_request) Successful in 7s
Lint & Validate / shellcheck (pull_request) Successful in 5s
Lint & Validate / yaml-lint (pull_request) Successful in 12s
Build Sandbox Runtime Image / build-and-push (pull_request) Successful in 1m45s
Build agent-sandbox MCP Server Image / build-and-push (pull_request) Successful in 2m5s
Lint & Validate / terraform-validate (pull_request) Successful in 3m21s
30e14a1755
LD1.1 MCP spike, per the ratified contract in
agent-sandbox/seat-provisioning-contracts.md §5.

Image (new file only, LD0.3 module untouched):
- agent-sandbox/mcp-server-image/Dockerfile — python:3.12-slim by index
  digest, deterministic non-root uid/gid 65532, vendored upstream source +
  pinned pip install, upstream's uvicorn entrypoint (:8000).
- vendor/ — clients/integrations/mcp-server @ v1.0.4 (commit 810726d8),
  byte-for-byte (upstream Dockerfile kept as Dockerfile.upstream) + LICENSE +
  SHA256SUMS over all 20 files.
- .forgejo/workflows/mcp-server-image-build.yaml — pi-image conventions:
  SHA256SUMS check, Dockerfile-ARG vs workflow pin assert, build, container
  smoke (uid/gid, dependency closure at the vendored pins, ASGI route table,
  /healthz 200 + /readyz shape over real HTTP), 1 GB size gate, branch
  <short-sha> tag, alias promoted from live only, digest printed.

Install module (manifests), all in agent-sandbox-system:
- core/manifests/agent-sandbox-mcp/{kustomization,deployment,service,rbac}.yaml
  + README.md. 1 replica; env per contract §5
  (CONNECTION__TYPE=in-cluster, __USE_POD_IP=true, __SERVER_PORT=8888,
  PROBE_NAMESPACE=agent-sandbox-system); liveness /healthz, readiness /readyz;
  requests 100m/128Mi; hardening (runAsNonRoot 65532, seccomp RuntimeDefault,
  drop ALL, no privilege escalation). ClusterIP-only Service, never routed.
  Namespaced Role only: sandboxclaims create/delete/get/list/watch,
  sandboxes get/list/watch — no ClusterRole, no secret/configmap verbs.
  Image tag is a placeholder; digest is pinned at prod wiring time.
- No ServiceMonitor: the app has no /metrics (routes are /mcp, /healthz,
  /readyz); documented rather than scraped as a 404.

Verification: kubectl kustomize renders 5 objects cleanly; yamllint -d relaxed
0 errors; Role verb audit shows no write verb outside sandboxclaims and no
secrets/configmaps/exec/ClusterRole in the render; vendored tree verifies
against SHA256SUMS and diffs clean vs the v1.0.4 clone; smoke scripts dry-run
against the same dependency closure (k8s-agent-sandbox 0.5.6, fastmcp 3.4.7).
Finding recorded on the ticket: at that SDK version
SandboxInClusterConnectionConfig has no use_pod_ip field, so the contract's
USE_POD_IP var is accepted-but-inert (pod IP is preferred unconditionally).

No push, no PR — orchestrator owns landing.
Author
Owner

Independent landing review — PR #523 (epic/LD1 → live), head 30e14a1

Verdict: APPROVE-WITH-CONCERNS — 0 blockers, 5 concerns, 4 suggestions.
Merge-ready PENDING operator ratification (dangerous-adjacent class: new CI publishers + an RBAC Role + image build sources). Nothing was merged by this review; the operator owns the merge.

Reviewer was not the implementer and received no implementer summary. Every claim below was re-derived from the diff and primary sources (upstream tag clone, krane/kubectl kustomize, PyPI, Forgejo run pages, the prod repo).

What was independently verified (not taken on trust)

Check Result
core/manifests/agent-sandbox-mcp renders kubectl kustomize → exactly 5 objects, all namespace: agent-sandbox-system; Service type: ClusterIP
Role is minimal, no cluster scope rules are exactly extensions.agents.x-k8s.io/sandboxclaims [create delete get list watch] + agents.x-k8s.io/sandboxes [get list watch]; zero secret/configmap/pod/exec verbs; no ClusterRole/CRD/Namespace in the render
Vendored upstream == v1.0.4 clone kubernetes-sigs/agent-sandbox tag v1.0.4 (commit 810726d89c71…); sha256sum -c vendor/SHA256SUMS → 21/21 OK; diff -r of clients/integrations/mcp-server vs vendor/ → no delta (only tests/+skill/ excluded, documented; Dockerfile renamed Dockerfile.upstream, identical bytes); vendor/LICENSE == upstream root LICENSE
Runtime image vendored files main.py sha256 1fb9c4aa… and requirements.txt sha256 c7641846… byte-identical to examples/python-runtime-sandbox/; workflow's hash pins match both the files and the README table (3/3 places)
Base image pin crane digest python:3.12-slim = sha256:f77ac9e44ae96ef2c90b8053ea08c31f8be030f824196b0ae4db6d462c84e51f — identical to both Dockerfiles; crane config confirms PYTHON_VERSION=3.12.14
CI on head 30e14a1 4/4 success — ci.yaml #1500, lint.yaml #1501, mcp-server-image-build.yaml #1502 (Build 1m50s; both smokes pass; size gate pass; Push skipped on PR), runtime-image-build.yaml #1503 (Build 1m32s; both smokes pass; size gate pass; Push skipped on PR). No secrets reachable from PR runs
Workflow YAML + scripts both files parse (PyYAML); every run: block clean under bash -n; both heredoc Python smoke bodies compile
Dependency pins resolvable all six direct pins exist on PyPI: k8s-agent-sandbox 0.5.6, fastmcp 3.4.7, fastapi 0.139.2, uvicorn 0.51.0, pydantic-settings 2.14.2 (build already proved this)
USE_POD_IP inertness claim (module README §Finding) reproduced: SandboxInClusterConnectionConfig at 0.5.6 declares only server_port; a Settings()-shaped parse with K8S_SANDBOX_CONNECTION__USE_POD_IP=true accepts and drops the key (no validation error) → the Deployment will not crash-loop on that env var. Claim is honest
"Nothing syncs this module yet" true: prod has no Application referencing agent-sandbox*, and prod/apps-root/root.yaml points at apps/ as explicit Application manifests (no directory generator / auto-discovery) → a new directory in core/manifests/ cannot be picked up implicitly
Secret hygiene no secret material in the diff (grep of all 4 626 added lines); Harbor secrets are referenced only inside the push step, which is skipped on PRs
Contract §5 ↔ module the four env values, port 8888/8000 split, probe semantics, readiness namespace, and RBAC table all match the shipped artifacts

Findings

# Class Finding Evidence
C1 concern Publisher credential deviates from the repo's just-established convention. Both new workflows log in with admin-grade secrets.HARBOR_USERNAME/HARBOR_PASSWORD inlined into run:. HOMELAB-1735 moved this repo's publishes to the project-scoped push robot (HARBOR_PUSH_*, scoped to platform + lab-director) via a step-level env: block, precisely because Forgejo/act substitutes ${{ secrets.X }} textually and bash then re-expands $ in the value (robot$forgejo-runner-push → unbound variable / silent 401, run 1491). sandbox-runtime and sandbox-mcp are both lab-director repos, so the scoped robot already covers them. Latent today (the admin username has no $), but it re-imports the exact defect class #522 fixed. mcp-server-image-build.yaml:180-182, runtime-image-build.yaml:156-158 vs runner-image-build.yaml:40-41,57-58; commit 51ee8bf
C2 concern Doc claims a pip freeze that the workflow never prints. The README asserts the resolved closure is "part of the build record" (twice), but the smoke prints six importlib.metadata versions and no pip freeze. With ~= constraints and no lockfile, the full transitive closure is therefore not recorded anywhere — only the direct six. agent-sandbox/mcp-server-image/README.md:54-56,162; workflow steps list (no freeze step)
C3 concern Vendor file count wrong. "byte-for-byte upstream, 20 files"; the vendored set is 21 files (17 .py + pyproject.toml + README.md + Dockerfile.upstream + LICENSE), which is exactly what SHA256SUMS covers (21 lines). mcp-server-image/README.md:24; wc -l vendor/SHA256SUMS = 21
C4 concern Contract §6 netpol selector would not match the implemented pods. §6's ingress rule allows podSelector: app.kubernetes.io/name: agent-sandbox-mcp; the module labels its pods app: agent-sandbox-mcp. LD1.2 drafting §6 verbatim produces a policy that silently matches nothing (MCP server → sidecar :8888 broken). One side must change; this PR introduces the labels, so it is the natural place. seat-provisioning-contracts.md:764 vs deployment.yaml:35-37
C5 concern Contract §4 sketch is internally inconsistent and conflicts with the image now built. §4's "Exact contract" YAML mounts runtime-code at /app, readOnly: true but its volumes: block declares only tmp — an undeclared volume, so the sketch cannot be applied literally. Taken at face value the mount also shadows the app code the new runtime image bakes at /app (COPY main.py /app/main.py), i.e. the sidecar would start against an empty /app. The image README flags the shadowing as an open question but not the missing volume declaration. Resolve before LD1.2 implements §4. seat-provisioning-contracts.md:464-511; runtime-image/Dockerfile:64; runtime-image/README.md:187-192
S1 suggestion Placeholder image tag is a live tag. sandbox-mcp:v1.0.4 is created and repointed by every live push, so a prod Application wired before the digest pin would silently run whatever the alias last pointed at. The README states the alias is not a pin; a deliberately unresolvable placeholder tag (:digest-pending) would fail loudly (ImagePullBackOff) instead. deployment.yaml:58-64; workflow promotion block :185-190
S2 suggestion MCP image is not reproducible. pip install . resolves ~= ranges with no hashes and no lock; a rebuild of the same commit can ship different transitive code, and the digest (the only pin) changes silently. The runtime image does better (--require-hashes). Mitigates C2 if the workflow prints pip freeze. Documented, so this is a "know it", not a defect. mcp-server-image/Dockerfile:54-60; README.md:54-56,188-189
S3 suggestion No CI guard on the module's RBAC. lint.yaml runs yamllint -d relaxed core/manifests/ only; nothing renders the module or asserts the verb set, so the README's "the Role must never gain pods/exec" is enforced by prose alone. The README already contains a runnable kustomize+RBAC assertion — port it into lint.yaml. The new .forgejo/workflows/*.yaml files are also outside every lint path. lint.yaml:22-27; agent-sandbox-mcp/README.md:87-100,226-237
S4 suggestion Context Block uses status: proposed, outside the standard enum (`draft review

No blockers found. Not a finding: the mcp image deliberately leaves readOnlyRootFilesystem unset (documented, tracked to the LD1 hardening pass), and fsGroup: 65532 with no volumes is harmless (documented).

Exposure bound

  • At merge: zero cluster exposure. Nothing syncs core/manifests/agent-sandbox-mcp (verified above), the Service is ClusterIP-only with no Gateway/Ingress/LoadBalancer, and the module pins a tag that no ArgoCD Application consumes. The only externally visible effect is two new images in the public lab-director Harbor project (no credentials embedded).
  • Once a prod Application lands: the MCP server ships no authentication by upstream design, so reachability is the whole boundary. Anything in the cluster that can reach agent-sandbox-mcp.agent-sandbox-system:8000 — or any operator credential that can port-forward it — gets an MCP session that can create/delete SandboxClaims in agent-sandbox-system, run commands inside those seats (via the unauthenticated sidecar on :8888), and read/write everything under the seat's /workspace including the second agent-state mount at /workspace/.seat-state/agent (§2). Hard bound: the namespaced Role — no secrets, no configmaps, no pods/exec, no cluster scope, no other namespace.
  • Adjacent, not introduced here: the seat sidecar :8888 is unauthenticated and §6's NetworkPolicy has no implementation yet (deferred by the register, item #1). Until it lands, any pod that can reach a seat pod IP gets file/exec as uid 1000.

Rollback notes

  • Code: plain revert of the merge commit on live — additive files only (docs, 2 workflows, 2 image dirs, 1 manifest module). No migration, no state.
  • Images: reverting git does not unpublish. A live push creates <short-sha>, v1.0.4, latest for both repos; the <short-sha> tags are immutable build evidence, but v1.0.4/latest are aliases that a revert leaves pointing at the reverted content. If retraction is wanted, delete the tags in Harbor lab-director/sandbox-runtime and lab-director/sandbox-mcp (public project, no consumers today).
  • Cluster: nothing to undo. If the module is ever wired and must be withdrawn, delete the prod Application (ArgoCD prunes Deployment/Service/Role/RoleBinding/ServiceAccount) — no PVC, no cluster-scoped residue, no CRDs. Note agent-sandbox-system carries Prune=false on the Namespace, so the namespace itself survives, as intended.
  • Blast radius of a bad merge is bounded by the above two facts: no sync path, and no consumer pins either image.

OPERATOR-DECISION

  1. Merge order (must be this order): merge #523 → live first. The live push is the only trigger that creates the v1.0.4/latest aliases and prints the digests (crane digest, printed to the run log); the prod wiring PR can only pin a digest that exists, so opening prod first would pin nothing. #523 itself does not depend on anything in prod.
  2. Merge method: ordinary merge commit (repo convention); do not squash — the three commits are independently reviewable (contracts / runtime image / MCP image + module) and the promotion rule reasons per-commit.
  3. Ratification items this PR touches (contracts register, §"Decisions pending operator ratification"):
    • #2 — MCP server Deployment + scoped ServiceAccount / namespaced Role / ClusterIP-only. Implemented here, inert. This is the one item that maps 1:1 onto shipped YAML; ratifying it ratifies the Role verb set as the standing blast radius.
    • §5 (port/router/MCP-server contract) — encoded by the image pair + module; the "router stays uninstalled" decision is now backed by real artifacts.
    • #10 — unauthenticated seat :8888, gated by ClusterIP + ingress rule. Only partially encoded: the ingress half is not implemented (§6 netpol deferred).
    • Not touched, still open: #1 (NetworkPolicy posture — no netpol in this PR), #3, #4, #5, #6, #7, #8, #9. Contract status stays proposed until #1 and the rest are decided.
  4. Two decisions to make before the prod wiring PR, not before this merge: (a) accept an unauthenticated MCP server for the spike on the strength of ClusterIP + kubelet port-forward until §6 lands; (b) the publisher credential scope in C1 (admin-grade vs the lab-director-scoped push robot).
  5. Class: dangerous-adjacent (RBAC manifests, new publishers, image build sources). CI is green and there are no blockers, so this is merge-ready — but the merge itself is the operator's call, and no auto-merge was performed by this review.
## Independent landing review — PR #523 (epic/LD1 → live), head `30e14a1` **Verdict: APPROVE-WITH-CONCERNS** — 0 blockers, 5 concerns, 4 suggestions. **Merge-ready PENDING operator ratification** (dangerous-adjacent class: new CI publishers + an RBAC Role + image build sources). Nothing was merged by this review; the operator owns the merge. Reviewer was not the implementer and received no implementer summary. Every claim below was re-derived from the diff and primary sources (upstream tag clone, `krane`/`kubectl kustomize`, PyPI, Forgejo run pages, the prod repo). ### What was independently verified (not taken on trust) | Check | Result | |---|---| | `core/manifests/agent-sandbox-mcp` renders | `kubectl kustomize` → exactly 5 objects, all `namespace: agent-sandbox-system`; Service `type: ClusterIP` | | Role is minimal, no cluster scope | rules are exactly `extensions.agents.x-k8s.io/sandboxclaims [create delete get list watch]` + `agents.x-k8s.io/sandboxes [get list watch]`; **zero** secret/configmap/pod/exec verbs; no ClusterRole/CRD/Namespace in the render | | Vendored upstream == v1.0.4 | clone `kubernetes-sigs/agent-sandbox` tag `v1.0.4` (commit `810726d89c71…`); `sha256sum -c vendor/SHA256SUMS` → 21/21 OK; `diff -r` of `clients/integrations/mcp-server` vs `vendor/` → no delta (only `tests/`+`skill/` excluded, documented; `Dockerfile` renamed `Dockerfile.upstream`, identical bytes); `vendor/LICENSE` == upstream root LICENSE | | Runtime image vendored files | `main.py` sha256 `1fb9c4aa…` and `requirements.txt` sha256 `c7641846…` byte-identical to `examples/python-runtime-sandbox/`; workflow's hash pins match both the files and the README table (3/3 places) | | Base image pin | `crane digest python:3.12-slim` = `sha256:f77ac9e44ae96ef2c90b8053ea08c31f8be030f824196b0ae4db6d462c84e51f` — identical to both Dockerfiles; `crane config` confirms `PYTHON_VERSION=3.12.14` | | CI on head `30e14a1` | 4/4 success — `ci.yaml` #1500, `lint.yaml` #1501, `mcp-server-image-build.yaml` #1502 (Build 1m50s; both smokes pass; size gate pass; **Push skipped on PR**), `runtime-image-build.yaml` #1503 (Build 1m32s; both smokes pass; size gate pass; **Push skipped on PR**). No secrets reachable from PR runs | | Workflow YAML + scripts | both files parse (PyYAML); every `run:` block clean under `bash -n`; both heredoc Python smoke bodies compile | | Dependency pins resolvable | all six direct pins exist on PyPI: `k8s-agent-sandbox 0.5.6`, `fastmcp 3.4.7`, `fastapi 0.139.2`, `uvicorn 0.51.0`, `pydantic-settings 2.14.2` (build already proved this) | | `USE_POD_IP` inertness claim (module README §Finding) | reproduced: `SandboxInClusterConnectionConfig` at 0.5.6 declares only `server_port`; a `Settings()`-shaped parse with `K8S_SANDBOX_CONNECTION__USE_POD_IP=true` accepts and drops the key (no validation error) → the Deployment will not crash-loop on that env var. Claim is honest | | "Nothing syncs this module yet" | true: `prod` has no Application referencing `agent-sandbox*`, and `prod/apps-root/root.yaml` points at `apps/` as **explicit** Application manifests (no directory generator / auto-discovery) → a new directory in `core/manifests/` cannot be picked up implicitly | | Secret hygiene | no secret material in the diff (grep of all 4 626 added lines); Harbor secrets are referenced only inside the push step, which is skipped on PRs | | Contract §5 ↔ module | the four env values, port 8888/8000 split, probe semantics, readiness namespace, and RBAC table all match the shipped artifacts | ### Findings | # | Class | Finding | Evidence | |---|---|---|---| | C1 | concern | **Publisher credential deviates from the repo's just-established convention.** Both new workflows log in with admin-grade `secrets.HARBOR_USERNAME/HARBOR_PASSWORD` **inlined into `run:`**. HOMELAB-1735 moved this repo's publishes to the project-scoped push robot (`HARBOR_PUSH_*`, scoped to platform + lab-director) via a step-level `env:` block, precisely because Forgejo/act substitutes `${{ secrets.X }}` textually and bash then re-expands `$` in the value (`robot$forgejo-runner-push` → unbound variable / silent 401, run 1491). `sandbox-runtime` and `sandbox-mcp` are both `lab-director` repos, so the scoped robot already covers them. Latent today (the admin username has no `$`), but it re-imports the exact defect class #522 fixed. | `mcp-server-image-build.yaml:180-182`, `runtime-image-build.yaml:156-158` vs `runner-image-build.yaml:40-41,57-58`; commit `51ee8bf` | | C2 | concern | **Doc claims a `pip freeze` that the workflow never prints.** The README asserts the resolved closure is "part of the build record" (twice), but the smoke prints six `importlib.metadata` versions and no `pip freeze`. With `~=` constraints and no lockfile, the full transitive closure is therefore *not* recorded anywhere — only the direct six. | `agent-sandbox/mcp-server-image/README.md:54-56,162`; workflow steps list (no freeze step) | | C3 | concern | **Vendor file count wrong.** "byte-for-byte upstream, 20 files"; the vendored set is 21 files (17 `.py` + `pyproject.toml` + `README.md` + `Dockerfile.upstream` + `LICENSE`), which is exactly what `SHA256SUMS` covers (21 lines). | `mcp-server-image/README.md:24`; `wc -l vendor/SHA256SUMS` = 21 | | C4 | concern | **Contract §6 netpol selector would not match the implemented pods.** §6's ingress rule allows `podSelector: app.kubernetes.io/name: agent-sandbox-mcp`; the module labels its pods `app: agent-sandbox-mcp`. LD1.2 drafting §6 verbatim produces a policy that silently matches nothing (MCP server → sidecar `:8888` broken). One side must change; this PR introduces the labels, so it is the natural place. | `seat-provisioning-contracts.md:764` vs `deployment.yaml:35-37` | | C5 | concern | **Contract §4 sketch is internally inconsistent and conflicts with the image now built.** §4's "Exact contract" YAML mounts `runtime-code` at `/app, readOnly: true` but its `volumes:` block declares only `tmp` — an undeclared volume, so the sketch cannot be applied literally. Taken at face value the mount also shadows the app code the new runtime image bakes at `/app` (`COPY main.py /app/main.py`), i.e. the sidecar would start against an empty `/app`. The image README flags the shadowing as an open question but not the missing volume declaration. Resolve before LD1.2 implements §4. | `seat-provisioning-contracts.md:464-511`; `runtime-image/Dockerfile:64`; `runtime-image/README.md:187-192` | | S1 | suggestion | **Placeholder image tag is a *live* tag.** `sandbox-mcp:v1.0.4` is created and repointed by every `live` push, so a prod Application wired before the digest pin would silently run whatever the alias last pointed at. The README states the alias is not a pin; a deliberately unresolvable placeholder tag (`:digest-pending`) would fail loudly (ImagePullBackOff) instead. | `deployment.yaml:58-64`; workflow promotion block `:185-190` | | S2 | suggestion | **MCP image is not reproducible.** `pip install .` resolves `~=` ranges with no hashes and no lock; a rebuild of the same commit can ship different transitive code, and the digest (the only pin) changes silently. The runtime image does better (`--require-hashes`). Mitigates C2 if the workflow prints `pip freeze`. Documented, so this is a "know it", not a defect. | `mcp-server-image/Dockerfile:54-60`; `README.md:54-56,188-189` | | S3 | suggestion | **No CI guard on the module's RBAC.** `lint.yaml` runs `yamllint -d relaxed core/manifests/` only; nothing renders the module or asserts the verb set, so the README's "the Role must never gain `pods/exec`" is enforced by prose alone. The README already contains a runnable kustomize+RBAC assertion — port it into `lint.yaml`. The new `.forgejo/workflows/*.yaml` files are also outside every lint path. | `lint.yaml:22-27`; `agent-sandbox-mcp/README.md:87-100,226-237` | | S4 | suggestion | **Context Block uses `status: proposed`,** outside the standard enum (`draft|review|current|deprecated`). The doc says so explicitly, so this is a conscious deviation — but if parsers enforce the enum, either amend the standard or use `draft` + a ratified marker. | `seat-provisioning-contracts.md:22,38-40` | No blockers found. Not a finding: the mcp image deliberately leaves `readOnlyRootFilesystem` unset (documented, tracked to the LD1 hardening pass), and `fsGroup: 65532` with no volumes is harmless (documented). ### Exposure bound - **At merge: zero cluster exposure.** Nothing syncs `core/manifests/agent-sandbox-mcp` (verified above), the Service is ClusterIP-only with no Gateway/Ingress/LoadBalancer, and the module pins a tag that no ArgoCD Application consumes. The only externally visible effect is two new images in the **public** `lab-director` Harbor project (no credentials embedded). - **Once a prod Application lands:** the MCP server ships *no authentication* by upstream design, so reachability is the whole boundary. Anything in the cluster that can reach `agent-sandbox-mcp.agent-sandbox-system:8000` — or any operator credential that can `port-forward` it — gets an MCP session that can create/delete `SandboxClaim`s in `agent-sandbox-system`, run commands inside those seats (via the unauthenticated sidecar on `:8888`), and read/write everything under the seat's `/workspace` including the second `agent-state` mount at `/workspace/.seat-state/agent` (§2). Hard bound: the namespaced Role — no secrets, no configmaps, no `pods/exec`, no cluster scope, no other namespace. - **Adjacent, not introduced here:** the seat sidecar `:8888` is unauthenticated and §6's NetworkPolicy has no implementation yet (deferred by the register, item #1). Until it lands, any pod that can reach a seat pod IP gets file/exec as uid 1000. ### Rollback notes - **Code:** plain revert of the merge commit on `live` — additive files only (docs, 2 workflows, 2 image dirs, 1 manifest module). No migration, no state. - **Images:** reverting git does **not** unpublish. A `live` push creates `<short-sha>`, `v1.0.4`, `latest` for both repos; the `<short-sha>` tags are immutable build evidence, but `v1.0.4`/`latest` are aliases that a revert leaves pointing at the reverted content. If retraction is wanted, delete the tags in Harbor `lab-director/sandbox-runtime` and `lab-director/sandbox-mcp` (public project, no consumers today). - **Cluster:** nothing to undo. If the module is ever wired and must be withdrawn, delete the prod Application (ArgoCD prunes Deployment/Service/Role/RoleBinding/ServiceAccount) — no PVC, no cluster-scoped residue, no CRDs. Note `agent-sandbox-system` carries `Prune=false` on the Namespace, so the namespace itself survives, as intended. - **Blast radius of a bad merge is bounded by the above two facts:** no sync path, and no consumer pins either image. ### OPERATOR-DECISION 1. **Merge order (must be this order):** merge **#523 → live first**. The live push is the only trigger that creates the `v1.0.4`/`latest` aliases and prints the digests (`crane digest`, printed to the run log); the prod wiring PR can only pin a digest that exists, so opening prod first would pin nothing. #523 itself does not depend on anything in prod. 2. **Merge method:** ordinary merge commit (repo convention); do **not** squash — the three commits are independently reviewable (contracts / runtime image / MCP image + module) and the promotion rule reasons per-commit. 3. **Ratification items this PR touches** (contracts register, §"Decisions pending operator ratification"): - **#2 — MCP server Deployment + scoped ServiceAccount / namespaced Role / ClusterIP-only.** *Implemented here, inert.* This is the one item that maps 1:1 onto shipped YAML; ratifying it ratifies the Role verb set as the standing blast radius. - **§5 (port/router/MCP-server contract)** — encoded by the image pair + module; the "router stays uninstalled" decision is now backed by real artifacts. - **#10 — unauthenticated seat `:8888`, gated by ClusterIP + ingress rule.** Only *partially* encoded: the ingress half is not implemented (§6 netpol deferred). - **Not touched, still open:** #1 (NetworkPolicy posture — no netpol in this PR), #3, #4, #5, #6, #7, #8, #9. Contract status stays `proposed` until #1 and the rest are decided. 4. **Two decisions to make before the prod wiring PR, not before this merge:** (a) accept an unauthenticated MCP server for the spike on the strength of ClusterIP + kubelet port-forward until §6 lands; (b) the publisher credential scope in C1 (admin-grade vs the `lab-director`-scoped push robot). 5. **Class:** dangerous-adjacent (RBAC manifests, new publishers, image build sources). CI is green and there are no blockers, so this is merge-ready — but the merge itself is the operator's call, and no auto-merge was performed by this review.
Operator ruling 2026-09-30 (epic LD1 session): ratified as proposed.
Status proposed -> review; landing-review amendments recorded against
items 1 (netpol selector label fix deferred to LD1.2 wiring) and 7
(runtime image baked deps vs runtime-code emptyDir wording).
Review concern on PR #523: both new workflows inlined the admin-grade Harbor
credentials into `run:`, the exact pattern HOMELAB-1735 removed.

- runtime-image-build.yaml, mcp-server-image-build.yaml: the `Push to Harbor
  and report the digest` step now takes HARBOR_PUSH_USERNAME /
  HARBOR_PUSH_PASSWORD through a step-level `env:` block and logs in with
  `$HARBOR_PUSH_USERNAME` / `$HARBOR_PUSH_PASSWORD`, byte-identical to the
  env-block and login lines in runner-image-build.yaml / docs-build.yaml
  (asserted programmatically, not by eye).

Why this is not cosmetic: Forgejo/act substitutes `${{ secrets.X }}`
textually into the generated step script, and the scoped robot username is
`robot$forgejo-runner-push` (HOMELAB-1735 defect B: run 1491 aborted with
`forgejo: unbound variable` under `set -u`; without `set -u` it degrades
silently to a 401). `env:` values arrive as process environment variables, so
the shell never re-parses them. The scoped robot covers `platform` +
`lab-director` — which is where both images publish (lab-director/sandbox-
runtime, lab-director/sandbox-mcp) — so the admin-grade credential is no
longer needed here.

Also corrected the now-false "Secrets scope" headers: both claimed the
admin-grade HARBOR_* pair was still the push credential (and pointed at
pi-image-build.yaml, which HOMELAB-1735 did not migrate). Nothing else
touched: no version bumps, no push/promotion-rule changes. pi-image-build.yaml
is left alone deliberately.

Verification: yaml.safe_load parses both files; every `run:` block passes
`bash -n` after act-style `${{ }}` substitution (heredoc bodies included);
yamllint -d relaxed reports zero errors and no new warnings (the 97-char
inlined login line is gone in both files); the credential projection of both
push steps is byte-identical to the runner-image-build.yaml reference.
HOMELAB-1669: docs(agent-sandbox): correct mcp-server-image README verification claim
All checks were successful
CI Review / receiver-test (pull_request) Successful in 5s
CI Review / helm-validate (pull_request) Successful in 5s
Lint & Validate / shellcheck (pull_request) Successful in 4s
Lint & Validate / yaml-lint (pull_request) Successful in 11s
Build Sandbox Runtime Image / build-and-push (pull_request) Successful in 1m39s
Build agent-sandbox MCP Server Image / build-and-push (pull_request) Successful in 2m0s
Lint & Validate / terraform-validate (pull_request) Successful in 2m53s
a177602789
Review concern on PR #523: the README claimed the workflow prints `pip freeze`
in three places. It never does — there is no `pip freeze` anywhere in
mcp-server-image-build.yaml or the Dockerfile. What the workflow actually does
is print `importlib.metadata.version` for six named distributions in the
dependency-closure smoke step (k8s-agent-sandbox, k8s-agent-sandbox-mcp-server,
fastmcp, fastapi, uvicorn, pydantic-settings) and the pushed image digest in
the push step. Corrected all three to that, worded to match the actual
coverage: the six named distributions, not a full closure dump.

Also folded in the one credential claim the CI commit above invalidated: the
README said push credentials are "scoped exactly as `pi-image-build.yaml`
documents (repo-level Harbor secrets …)", which is both stale (pi-image-build
was never migrated by HOMELAB-1735) and now wrong for this workflow — it
follows the scoped `HARBOR_PUSH_*` env-block convention in
runner-image-build.yaml.

README-only change; no code, no workflow, no pin touched.
aaron merged commit 01b3df0750 into live 2026-09-30 01:22:19 +00:00
Sign in to join this conversation.
No reviewers
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Homelab/infra-core!523
No description provided.