HOMELAB-1669/1747 (LD1): seat provisioning contracts + runtime/MCP server images + MCP install module #523
No reviewers
Labels
No labels
operator-review
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/infra-core!523
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "epic/LD1"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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).
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.Independent landing review — PR #523 (epic/LD1 → live), head
30e14a1Verdict: 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)
core/manifests/agent-sandbox-mcprenderskubectl kustomize→ exactly 5 objects, allnamespace: agent-sandbox-system; Servicetype: ClusterIPextensions.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 renderkubernetes-sigs/agent-sandboxtagv1.0.4(commit810726d89c71…);sha256sum -c vendor/SHA256SUMS→ 21/21 OK;diff -rofclients/integrations/mcp-servervsvendor/→ no delta (onlytests/+skill/excluded, documented;DockerfilerenamedDockerfile.upstream, identical bytes);vendor/LICENSE== upstream root LICENSEmain.pysha2561fb9c4aa…andrequirements.txtsha256c7641846…byte-identical toexamples/python-runtime-sandbox/; workflow's hash pins match both the files and the README table (3/3 places)crane digest python:3.12-slim=sha256:f77ac9e44ae96ef2c90b8053ea08c31f8be030f824196b0ae4db6d462c84e51f— identical to both Dockerfiles;crane configconfirmsPYTHON_VERSION=3.12.1430e14a1ci.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 runsrun:block clean underbash -n; both heredoc Python smoke bodies compilek8s-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_IPinertness claim (module README §Finding)SandboxInClusterConnectionConfigat 0.5.6 declares onlyserver_port; aSettings()-shaped parse withK8S_SANDBOX_CONNECTION__USE_POD_IP=trueaccepts and drops the key (no validation error) → the Deployment will not crash-loop on that env var. Claim is honestprodhas no Application referencingagent-sandbox*, andprod/apps-root/root.yamlpoints atapps/as explicit Application manifests (no directory generator / auto-discovery) → a new directory incore/manifests/cannot be picked up implicitlyFindings
secrets.HARBOR_USERNAME/HARBOR_PASSWORDinlined intorun:. HOMELAB-1735 moved this repo's publishes to the project-scoped push robot (HARBOR_PUSH_*, scoped to platform + lab-director) via a step-levelenv: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-runtimeandsandbox-mcpare bothlab-directorrepos, 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-158vsrunner-image-build.yaml:40-41,57-58; commit51ee8bfpip freezethat the workflow never prints. The README asserts the resolved closure is "part of the build record" (twice), but the smoke prints siximportlib.metadataversions and nopip 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).py+pyproject.toml+README.md+Dockerfile.upstream+LICENSE), which is exactly whatSHA256SUMScovers (21 lines).mcp-server-image/README.md:24;wc -l vendor/SHA256SUMS= 21podSelector: app.kubernetes.io/name: agent-sandbox-mcp; the module labels its podsapp: agent-sandbox-mcp. LD1.2 drafting §6 verbatim produces a policy that silently matches nothing (MCP server → sidecar:8888broken). One side must change; this PR introduces the labels, so it is the natural place.seat-provisioning-contracts.md:764vsdeployment.yaml:35-37runtime-codeat/app, readOnly: truebut itsvolumes:block declares onlytmp— 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-192sandbox-mcp:v1.0.4is created and repointed by everylivepush, 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-190pip 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 printspip freeze. Documented, so this is a "know it", not a defect.mcp-server-image/Dockerfile:54-60;README.md:54-56,188-189lint.yamlrunsyamllint -d relaxed core/manifests/only; nothing renders the module or asserts the verb set, so the README's "the Role must never gainpods/exec" is enforced by prose alone. The README already contains a runnable kustomize+RBAC assertion — port it intolint.yaml. The new.forgejo/workflows/*.yamlfiles are also outside every lint path.lint.yaml:22-27;agent-sandbox-mcp/README.md:87-100,226-237status: proposed, outside the standard enum (`draftNo blockers found. Not a finding: the mcp image deliberately leaves
readOnlyRootFilesystemunset (documented, tracked to the LD1 hardening pass), andfsGroup: 65532with no volumes is harmless (documented).Exposure bound
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 publiclab-directorHarbor project (no credentials embedded).agent-sandbox-mcp.agent-sandbox-system:8000— or any operator credential that canport-forwardit — gets an MCP session that can create/deleteSandboxClaims inagent-sandbox-system, run commands inside those seats (via the unauthenticated sidecar on:8888), and read/write everything under the seat's/workspaceincluding the secondagent-statemount at/workspace/.seat-state/agent(§2). Hard bound: the namespaced Role — no secrets, no configmaps, nopods/exec, no cluster scope, no other namespace.:8888is 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
live— additive files only (docs, 2 workflows, 2 image dirs, 1 manifest module). No migration, no state.livepush creates<short-sha>,v1.0.4,latestfor both repos; the<short-sha>tags are immutable build evidence, butv1.0.4/latestare aliases that a revert leaves pointing at the reverted content. If retraction is wanted, delete the tags in Harborlab-director/sandbox-runtimeandlab-director/sandbox-mcp(public project, no consumers today).agent-sandbox-systemcarriesPrune=falseon the Namespace, so the namespace itself survives, as intended.OPERATOR-DECISION
v1.0.4/latestaliases 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.:8888, gated by ClusterIP + ingress rule. Only partially encoded: the ingress half is not implemented (§6 netpol deferred).proposeduntil #1 and the rest are decided.lab-director-scoped push robot).