Phase-10 cutover runbook — au 638ea524 + eg 2595a24 → live RKE2¶
GOC-59 dated historical record, 2026-08-09: this dated, point-in-time runbook intentionally names multiple now-retired configuration keys (
check_current_only_contractfindings) as evidence of what was found live on thegraph-os-hostdrifted twin and as the exact guard-rail commands to detect their reappearance. Rewriting the runbook to obscure those names will destroy its audit value without changing cluster state, and this lane has no live-cluster access to re-check or re-apply it. Left open — carried and owned, not silently cleared.scripts/check_current_only_contract.pyreads the HTML-comment marker line directly above as this file's own, intrinsic declaration that it is such a record (WD10-R-RESIDZERO retired the separate, path-keyedACCEPTED_RESIDUALSallowlist that used to name this file from outside it); the marker travels with the file, so a rename or a copy of this runbook stays exempt from retired-surface identifier scanning without anyone updating a list elsewhere.Status: FINALIZED RUNBOOK, NOT APPLIED. Every fact below was gathered read-only (
kubectl get/describe/exec -- cat, noapply/patch/delete/rollout/scaleexecuted) plus source inspection of au638ea524and eg2595a24, on 2026-07-25. This extendsreports/phase10-redeploy-plan.md(the investigation) with the exact per-workload edit list the live cutover runs, reconciles that plan's one open ambiguity (the identity contract), and records the eg build-feasibility result. The live cutover happens later under the user's explicit approval — nothing here is auto-applied by any agent or pipeline.
Placeholder convention. Commands below are copy-paste exact for every
kubectl/OpenBao verb, flag, and JSON-Patch path — only environment-specific
identifiers are written as <placeholder> rather than baked into a tracked
document. Resolve each once, immediately before the cutover window, with the
read-only command shown:
| Placeholder | What it is | Resolve with |
|---|---|---|
<hostpath-root> |
Base directory holding the graph-os/graph-os-host/engine hostPath mounts |
kubectl get deployment graph-os -n platform -o jsonpath='{.spec.template.spec.volumes[0].hostPath.path}' (strip the trailing kg-src-next/agent-utilities) |
<engine-source-node> |
The node the engine binary + au-src/eg-src hostPaths live on |
kubectl get deployment epistemic-graph -n platform -o jsonpath='{.spec.template.spec.nodeSelector}' |
<engine-tls-endpoint> |
The engine's TCP/TLS contact address | kubectl get configmap graph-os-env -n platform -o jsonpath='{.data.ENGINE_TLS_SERVER_NAME}' (+ port 9100, confirmed in the engine Deployment's own --tcp-addr/GRAPH_SERVICE_ENDPOINTS) |
<graph-os-public-host> |
The public MCP hostname | kubectl get ingress graph-os -n platform -o jsonpath='{.spec.rules[0].host}' |
<keycloak-host> |
The Keycloak issuer hostname | kubectl get configmap graph-os-env -n platform -o jsonpath='{.data.OIDC_ISSUER}' |
1. Identity-contract reconciliation (definitive)¶
The task brief flagged a conflict between the redeploy plan's blocker #1
(KG_AUTH_TOKEN_REF/KG_IDENTITY_OAUTH2) and a messaging-bundle finding
(AUTH_JWT_AUDIENCE/MCP_JWT_AUDIENCE + KG_POLICY_VERSION). Both are correct —
they are two different, sequential gates in the same boot path, not competing
theories, and the plan under-checked the first one because it stopped at
"unverified — needs the rehearsal step" rather than actually running it.
The two gates, read directly from agent_utilities/security/request_identity.py:
- Process-bootstrap identity —
acquire_process_identity_token()(lines 400–428). Called unconditionally for any transport other thanstdio-with-local-authority (agent_utilities/mcp/kg_server.py:3791,_mint_process_session, theelsebranch at line 2835) — i.e. it is onplatform/graph-os's own boot path, since it servesTRANSPORT=streamable-http. It requires exactly one ofKG_AUTH_TOKEN_REF/KG_IDENTITY_OAUTH2(config.kg_auth_token_ref/config.kg_identity_oauth2,core/config.py:2894-2905) and raisesRuntimeErrorotherwise (the XOR check at line 412: both absent also fails thebool(a) == bool(b)test). Same gate for the standaloneagent-utilities-messagingprocess (messaging/daemon.py:91-115,mint_process_identity) and forkg-ingest-worker/the A2A protocol server. - Claims-to-session projection —
mint_graph_session()(lines 93-113), called (a) internally by gate 1 once a token is acquired, and (b) independently byActorIdentityMiddlewarefor every served inbound HTTP/MCP request that presents its own Bearer JWT. It requiresconfig.auth_jwt_audience or config.mcp_jwt_audience(aliasesAUTH_JWT_AUDIENCE/FASTMCP_SERVER_AUTH_JWT_AUDIENCE,core/config.py:2856/2812-2813) andconfig.kg_policy_versionboth non-empty, elsePermissionError("...missing audience or policy revision").
So a process that mints its own identity (graph-os, the standalone messaging daemon, ingest-worker) must satisfy both gates in order; a process that only validates callers' tokens (the served HTTP path) only ever touches gate 2.
Definitive live status, checked directly against the cluster (not inferred):
| Key | Where it lives | Confirmed live? |
|---|---|---|
KG_IDENTITY_OAUTH2 |
platform/graph-os's durable config.json (mounted /root/.config/agent-utilities/config.json, sourced from the migrated au-config hostPath — kubectl-invisible, not a ConfigMap/Secret key) |
YES — a JSON object with audience=agent-services, client_id=graph-os, client_secret=env://OIDC_CLIENT_SECRET, token_url=https://<keycloak-host>/realms/homelab/protocol/openid-connect/token. The client_secret ref resolves against the bare OIDC_CLIENT_SECRET key already present in graph-os-secrets (confirmed). |
KG_AUTH_TOKEN_REF |
(same config.json) | Not needed — mutually exclusive with the above, and the above is set |
AUTH_JWT_AUDIENCE |
graph-os-env ConfigMap |
YES — agent-services (the plan's own key enumeration in §3.1 omitted this key; it is present) |
KG_POLICY_VERSION |
graph-os-env ConfigMap |
YES — homelab-v1 (plan already had this right) |
Conclusion: platform/graph-os already satisfies BOTH gates today. This is why
it has been healthy for 2 days — not because the identity requirement doesn't apply to
it (it does, unconditionally, per kg_server.py:3791), but because the durable
config.json the plan's own §6 step 1 said to check (and deferred, rather than ran)
already carries the answer. Blocker #1 in the plan is RESOLVED, not open. No
env/Secret/OpenBao write is needed for graph-os. See §2 item (d) below for the
mechanical re-verification command and how this extends to graph-os-host.
Why the standalone agent-utilities-messaging Deployment crash-loops on this exact
chain — proven from the pod's own crash traceback, not inferred:
File ".../agent_utilities/messaging/daemon.py", line 195, in main
session = _mint_process_session()
File ".../agent_utilities/messaging/daemon.py", line 170, in _mint_process_session
session = mint_graph_session(actor)
File ".../agent_utilities/security/request_identity.py", line 109, in mint_graph_session
return _mint_graph_session(...)
File ".../agent_utilities/security/request_identity.py", line 161, in _mint_graph_session
raise PermissionError(...)
PermissionError: Verified graph authority is missing audience or policy revision
(kubectl logs -n apps deploy/agent-utilities-messaging --previous.) This is the
exact mint_graph_session/gate-2 failure the messaging-bundle finding names —
confirmed live, not theoretical. Gate 1 (token acquisition) evidently succeeds
for this process — the exception propagates from inside mint_graph_session, past
frames that already returned — most likely via an identity-source entry inside its
own dedicated config.json (mounted read-write at /root/.config/agent-utilities
from its own separate aum-config hostPath), mirroring the same
config.json-carries-KG_IDENTITY_OAUTH2 pattern found on graph-os in this
investigation. That file can not be read directly (the pod was mid-CrashLoopBackOff
5-minute backoff at verification time — no running container to exec into; restart
count 826 and climbing). What is directly confirmed absent from every object this
Deployment actually consumes — its Deployment-level env:, its own Secret
(agent-utilities-messaging, 9 bare plaintext keys), and mcp-common-env (which
has KG_POLICY_VERSION=homelab-v1 but no audience key, and isn't even wired to this
Deployment — its envFrom is only its own Secret, confirmed via direct dump) —
is AUTH_JWT_AUDIENCE/FASTMCP_SERVER_AUTH_JWT_AUDIENCE anywhere reachable. That
gap is enough on its own to explain the proven failure, in either case of exactly
which of audience/policy_version its config.json leaves blank. (Function names/line
numbers in the traceback — _mint_process_session/main at 195/170 — don't match
638ea524's current daemon.py, i.e. the crashed process was running slightly older
cached bytecode from before a recent rename to today's mint_process_identity
(lines 91-115); the underlying mint_graph_session call chain and the exact
PermissionError text are unaffected by that rename and remain the live, current
failure mode.) This is a previously-undocumented independent crash cause, layered
on the plan's already-known three (9 plaintext secrets, tcp:// scheme, retired-key
exposure risk via its own config.json).
Confirms the task's framing exactly: the fix is not to patch the standalone
Deployment's identity config (a second location to keep in sync forever) — it is to
retire it. graph-os's bundled messaging co-service
(co_service_supervisor.start_co_services, kg_server.py:3840) calls
messaging.daemon.run_forever(engine, platforms, stop_event) directly — never
daemon.mint_process_identity() — running the co-service thread under
_authorized_background_thread(session, ...) where session is graph-os's own
already-minted bootstrap_session (checked: co_service_supervisor.py:207,283-305).
The bundled co-service never independently mints, so it inherits whatever identity
graph-os already has — checked, working. Retiring the standalone Deployment (§2
item e) resolves this crash cause completely, with no config to fix in place.
2. Per-workload edit list¶
Per AGENTS.md/mcp-fleet-network-contract-break: OpenBao is the only correct write
target for anything ExternalSecret-managed (graph-os-secrets,
epistemic-graph-secrets, agent-utilities-messaging) — a raw kubectl patch on any
of them is silently reverted on the next 1h reconcile (all three confirmed
STORETYPE: ClusterSecretStore, STORE: openbao, REFRESH INTERVAL: 1h, path: apps
in the ClusterSecretStore spec — so key: graph-os resolves to OpenBao KV-v2 path
apps/graph-os, key: agent-utilities-messaging → apps/agent-utilities-messaging,
and the shared GRAPH_SERVICE_AUTH_SECRET remoteRef → apps/agent-utilities/deployment
property GRAPH_SERVICE_AUTH_SECRET). The plain graph-os-env ConfigMap is not
ExternalSecret-managed — kubectl patch is the correct, direct tool for it.
(a) Strip the retired engine-mode env var from platform/graph-os¶
Already resolved on the live object — re-check, do not blindly re-apply. Direct
inspection of the live Deployment's .spec.template.spec.containers[0].env (not the
kubectl.kubernetes.io/last-applied-configuration annotation) shows no retired
engine-mode key at all (see the exact name in core/config.py
_RETIRED_CONFIGURATION_KEYS, first entry). The retired key only survives inside
that stale annotation (frozen at whatever full manifest was last kubectl apply
-f'd, before someone imperatively kubectl set env/patched it away) — proof: the
annotation still shows the pre-migration hostPaths too, while the live spec uses
the migrated -next paths. This is a real landmine for the cutover mechanics,
not a closed issue: if any future step does a full kubectl apply -f of a
manifest matching that stale annotation (or of the rendered
deploy/k8s/production-cell/ assets / deploy/swarm/graphos.stack.yml without
hand-checking they don't carry that key), it reintroduces the retired key and
the migrated hostPaths regress in the same stroke. The cutover must use targeted
patches only (as this whole runbook does), never a wholesale manifest apply against
platform/graph-os.
- Command (idempotent pre-flight + defensive re-assertion):
# Verify (read-only) — expect empty output: kubectl get deployment graph-os -n platform \ -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="GRAPH_BACKEND")]}' # If it ever reappears, remove it atomically (no-op today; safe to run regardless): kubectl set env deployment/graph-os -n platform GRAPH_BACKEND- - Workload:
platform/graph-os(Deployment env only;graph-os-hostwas never confirmed to carry this key in its live spec either). - Rollback: N/A — there is nothing to roll back to; reintroducing the retired key is the regression this step guards against, not a valid prior state.
- Status: READY (already correct live; the action here is a guard-rail check, not a change).
(b) graph-os-host drifted twin — ALIGN (recommended) or RETIRE¶
Confirmed live via direct hostPath/config.json inspection (not inference):
graph-os (migrated) |
graph-os-host (drifted) |
|
|---|---|---|
au-src |
<hostpath-root>/kg-src-next/agent-utilities |
<hostpath-root>/kg-src/agent-utilities |
eg-src |
<hostpath-root>/kg-src-next/epistemic-graph |
<hostpath-root>/kg-src/epistemic-graph |
au-config |
<hostpath-root>/.config/agent-utilities-next |
<hostpath-root>/.config/agent-utilities |
config.json secrets_backend |
engine (valid) |
inmemory (invalid literal — pydantic.ValidationError) |
| config.json retired keys present | none (swept all 19: none found) | ENGINE_MODE=remote, ENGINE_ENDPOINT=tcp://<engine-tls-endpoint>, GRAPH_SERVICE_TCP_ADDR=<engine-tls-endpoint> — all three are in _RETIRED_CONFIGURATION_KEYS (core/config.py:165-186, confirmed by direct source read) |
KG_DAEMON_ROLE |
client |
auto (self-elects host lock) |
This directly, first-hand confirms plan §3.3/§9 blocker #3: this pod is one
unplanned restart away from the full retired-key + invalid-secrets_backend crash
chain, and it will also fail identity gate 1 (its config.json has neither
KG_IDENTITY_OAUTH2 nor KG_AUTH_TOKEN_REF — confirmed absent).
Option 1 — ALIGN (matches the plan and the existing "twin daemon" architecture
doc; recommended default). JSON-Patch with test guards so the patch fails loud
(atomically, no partial application) if the array order ever differs from what was
just checked, rather than silently patching the wrong volume. Resolve
<hostpath-root> (see the conventions table) before running:
# Save a full rollback artifact first:
kubectl get deployment graph-os-host -n platform -o yaml > graph-os-host-pre-cutover-$(date +%s).yaml
kubectl patch deployment graph-os-host -n platform --type=json -p '[
{"op":"test","path":"/spec/template/spec/volumes/0/name","value":"au-src"},
{"op":"replace","path":"/spec/template/spec/volumes/0/hostPath/path","value":"<hostpath-root>/kg-src-next/agent-utilities"},
{"op":"test","path":"/spec/template/spec/volumes/1/name","value":"eg-src"},
{"op":"replace","path":"/spec/template/spec/volumes/1/hostPath/path","value":"<hostpath-root>/kg-src-next/epistemic-graph"},
{"op":"test","path":"/spec/template/spec/volumes/2/name","value":"au-config"},
{"op":"replace","path":"/spec/template/spec/volumes/2/hostPath/path","value":"<hostpath-root>/.config/agent-utilities-next"}
]'
# Deployment strategy is `Recreate` (confirmed) — the patch alone triggers the
# recreate; no separate rollout restart is needed. Watch it:
kubectl rollout status deployment/graph-os-host -n platform
- Workload:
platform/graph-os-host. - Rollback: the same
--type=jsonpattern with the threehostPath.pathvalues swapped back to the pre-migration paths (<hostpath-root>/kg-src/agent-utilities,<hostpath-root>/kg-src/epistemic-graph,<hostpath-root>/.config/agent-utilities— the originals, confirmed still present on disk, not removed by this step) — orkubectl apply -f graph-os-host-pre-cutover-<ts>.yamlusing the saved backup.
Option 2 — RETIRE only with a replacement host (not recommended for this
cutover). graph-os runs KG_DAEMON_ROLE=client, which is now a hard
serving-plane boundary: it never self-promotes when the host lock is empty, and
stale KG_LOOP=1 does not start maintenance schedules or autonomous evolution
inside the MCP process. Scaling graph-os-host to zero without first deploying
another explicit gateway/graph-os-daemon host therefore leaves the durable
queue undrained. Recommendation: Option 1 (align) for Phase-10. If the twin is
retired later, create and validate its replacement host first, then scale the old
host down only after queue-drain, scheduler, and loop telemetry have moved.
- Status: BLOCKED — needs execution at cutover (real work item, not yet applied).
(c) Add MCP_PUBLIC_BASE_URL=https://<graph-os-public-host>¶
Confirmed absent from the live graph-os-env ConfigMap (full key enumeration).
Not ExternalSecret-managed — direct kubectl patch is correct. Because both
graph-os and graph-os-host consume this same ConfigMap via envFrom, one patch
covers both workloads once each is restarted. (<graph-os-public-host> is the same
value already set, unmodified, in the already-corrected
deploy/k8s/graphos-homelab-live-config-fix.yaml reference file — resolve it from
there or the Ingress, per the conventions table.)
kubectl patch configmap graph-os-env -n platform --type merge \
-p "{\"data\":{\"MCP_PUBLIC_BASE_URL\":\"https://<graph-os-public-host>\"}}"
kubectl rollout restart deployment/graph-os -n platform
kubectl rollout restart deployment/graph-os-host -n platform # picks it up on its own restart in (b) too
- Workload:
graph-os-envConfigMap → bothgraph-osandgraph-os-host. - Rollback:
kubectl patch configmap graph-os-env -n platform --type json -p '[{"op":"remove","path":"/data/MCP_PUBLIC_BASE_URL"}]'then re-roll both Deployments (orkubectl rollout undo). - Status: READY (value known, mechanical, zero ambiguity — matches the already-corrected reference file in item 1 of this cutover).
(d) Identity keys¶
Already present live for graph-os — nothing to add. See §1 above for the full
reconciliation. The only action here is a pre-flight re-confirmation immediately
before the cutover window (belt-and-suspenders on the single highest-value fact, per
the plan's own emphasis), plus carrying the same config.json forward unmodified when
graph-os-host is aligned in (b):
kubectl exec -n platform deploy/graph-os -- python3 -c \
"import json; d = json.load(open('/root/.config/agent-utilities/config.json')); \
oauth2 = d.get('KG_IDENTITY_OAUTH2'); \
print('KG_IDENTITY_OAUTH2 present:', bool(oauth2)); \
print('client_secret ref set:', bool((oauth2 or {}).get('client_secret')))"
# Expect: True / True. If EITHER prints False, STOP — this is the one real
# boot-blocker in the whole migration and must be resolved before any hostPath flip.
- Workload:
graph-os(and, after (b),graph-os-host— same config.json). - Rollback: N/A (verification-only step, no mutation).
- Status: READY (confirmed present; re-check, don't skip, immediately pre-cutover).
(e) Retire apps/agent-utilities-messaging¶
Uses the already-prepared deploy/k8s/messaging-bundle-retirement.yaml almost
verbatim — its STEP 1–3 sequencing is correct and independently confirmed against the
current live objects. The one easy-to-miss prerequisite: retiring the standalone
Deployment BEFORE moving its channel token(s) into graph-os's own secret will
silently drop messaging entirely (no crash, just configured_platforms() returning
empty, so the co-service never starts) rather than fixing it — sequencing matters.
Because the retired Deployment is being removed, none of the original plan §4.4
in-place fixes (rewrite its 9 plaintext keys, fix its tcp:// scheme, add it its own
GRAPH_SERVICE_AUTH_SECRET) are needed at all — retirement obviates them.
# STEP 1 (read-only) — confirm the bundle is live on graph-os already:
kubectl logs -n platform deploy/graph-os --since=10m | grep -i "co-service"
# "co-service messaging started." only appears once a channel token is configured —
# absence here is expected/correct until STEP 2 lands.
# STEP 2 — move the channel token(s) into graph-os's OWN OpenBao path (apps/graph-os),
# NOT a kubectl patch on graph-os-secrets (external-secrets reverts that within 1h).
# The live agent-utilities-messaging Secret carries TELEGRAM_BOT_TOKEN and
# MATTERMOST_TOKEN among its 9 keys — these are the two that matter for
# `configured_platforms()`; read them from the CURRENT plaintext OpenBao entry at
# apps/agent-utilities-messaging and merge into apps/graph-os (KV-v2 patch = merge,
# preserves the existing OIDC_*/other keys already there):
vault kv patch apps/graph-os \
TELEGRAM_BOT_TOKEN="<value read from apps/agent-utilities-messaging>" \
MATTERMOST_TOKEN="<value read from apps/agent-utilities-messaging>"
# (or the graph-os `write`/`open__write_secret` MCP tool against the same path —
# either is fine; OpenBao is the write target either way, never kubectl patch.)
kubectl annotate externalsecret graph-os-secrets -n platform force-sync=$(date +%s) --overwrite
kubectl rollout restart deployment/graph-os -n platform
kubectl logs -n platform deploy/graph-os --since=10m | grep -i "co-service"
# Now expect: "co-service messaging started."
# STEP 3 — only after STEP 2 is confirmed serving: retire the standalone Deployment.
kubectl scale deployment/agent-utilities-messaging -n apps --replicas=0
# park; confirm no regression for one full operational cycle, THEN:
kubectl delete deployment/agent-utilities-messaging -n apps
kubectl delete secret agent-utilities-messaging -n apps
# (no dedicated ConfigMap exists for this Deployment — confirmed; nothing to delete there)
- Workload:
apps/agent-utilities-messaging(retired) +platform/graph-os-secrets(OpenBaoapps/graph-ospath gains 2 keys) +platform/graph-os(restart to pick them up). - Rollback:
kubectl scale deployment/agent-utilities-messaging -n apps --replicas=1(fully reversible up until thedeletecalls in STEP 3's second half — the scale-to-0-first-then-remove sequencing is deliberately the rollback window). Once removed, restoring it means re-applying the pre-existing (broken) Deployment/Secret manifests — no worse than today's state, since it starts crash-looping either way. - Status: BLOCKED — needs execution at cutover, specifically needs the two token
values copied from OpenBao
apps/agent-utilities-messagingintoapps/graph-os(a value the operator/OpenBao holds, not something inferable from cluster metadata).
3. eg engine binary — build feasibility (2595a24)¶
See §6 below for the executed build result. Cutover mechanism (plan §5.1, confirmed
against the live Deployment's exact hostPath/args). Run the build on
<engine-source-node> (the node the engine-bin hostPath and au-src/eg-src
sources already live on):
# On <engine-source-node>, from the eg checkout at 2595a24:
cd <eg checkout at 2595a24>
cargo build --release --features full --target-dir <scratch> # NOT the shared default
# target dir — see
# eg-shared-cargo-target-
# corruption memory
mkdir -p <hostpath-root>/eg-bin-2595a24
cp <scratch>/release/epistemic-graph-server <hostpath-root>/eg-bin-2595a24/
kubectl get deployment epistemic-graph -n platform -o yaml > epistemic-graph-pre-cutover-$(date +%s).yaml
kubectl patch deployment epistemic-graph -n platform --type=json -p '[
{"op":"test","path":"/spec/template/spec/volumes/0/name","value":"engine-bin"},
{"op":"replace","path":"/spec/template/spec/volumes/0/hostPath/path","value":"<hostpath-root>/eg-bin-2595a24"}
]'
# Strategy is Recreate (confirmed) — expect ~60s downtime (plan's own historical
# estimate for this store size; re-measure for real during the rehearsal window).
kubectl rollout status deployment/epistemic-graph -n platform
- Workload:
platform/epistemic-graph.epistemic-graph-secretsis unchanged —GRAPH_SERVICE_AUTH_SECRET/signer keys must be reused verbatim, never regenerated (rotating them breaks every connected client fleet-wide). - Rollback: same
--type=jsonpattern,hostPath.pathback to<hostpath-root>/eg-bin(kept on disk, not removed) — orkubectl apply -f epistemic-graph-pre-cutover-<ts>.yaml. - Ordering constraint (plan §5.1, unchanged): the engine rebuild and the
graph-os/graph-os-hostsource sync must land in the same window, engine first — a client built against one commit failedcreate tenant graphauth against a server built from a different commit in a prior incident (golden-loop-embed-hang-and-engine-redeploymemory).
4. au-src/eg-src sync for graph-os/graph-os-host¶
Not one of the 6 gating items on its own (the plan treats it as part of the ordered
cutover, §7b/7c) but required for either workload to actually run 638ea524/2595a24
source rather than config alone:
rsync -a --delete <au checkout @ 638ea524>/ <hostpath-root>/kg-src-2/agent-utilities/
rsync -a --delete <eg checkout @ 2595a24>/ <hostpath-root>/kg-src-2/epistemic-graph/
# Stage at a NEW parallel path (kg-src-2), do not overwrite kg-src-next in place —
# keeps the proven "stage, then flip" rollback lever intact.
kubectl patch deployment graph-os -n platform --type=json -p '[
{"op":"test","path":"/spec/template/spec/volumes/0/name","value":"au-src"},
{"op":"replace","path":"/spec/template/spec/volumes/0/hostPath/path","value":"<hostpath-root>/kg-src-2/agent-utilities"},
{"op":"test","path":"/spec/template/spec/volumes/1/name","value":"eg-src"},
{"op":"replace","path":"/spec/template/spec/volumes/1/hostPath/path","value":"<hostpath-root>/kg-src-2/epistemic-graph"}
]'
kubectl rollout status deployment/graph-os -n platform
au-config/au-src/eg-src indices for graph-os were confirmed in the same order
as graph-os-host's (au-src=0, eg-src=1, au-config=2) — re-check with
kubectl get deployment graph-os -n platform -o jsonpath='{.spec.template.spec.volumes[*].name}'
immediately before applying, same defensive posture as §2(b).
- Rollback: patch the two
hostPath.pathvalues back to<hostpath-root>/kg-src-next/{agent-utilities,epistemic-graph}(kept intact).
5. Image dependency vintage — superseded ACP remediation¶
The original cutover found Pydantic AI 2.7 in the live image and no ACP adapter. The subsequent legacy ACP attempt was also invalid: it mounted a stdio protocol object as ASGI and capped Pydantic AI at 2.16.
The corrected build uses Pydantic AI 2.21 and
pydantic-ai-harness[acp]==0.14.0. ACP is a separate
agent-utilities-acp editor subprocess, not part of Graph-OS HTTP health.
Validate the promoted image with:
kubectl exec -n platform deploy/graph-os -- \
python -c "import pydantic_ai; print(pydantic_ai.__version__)"
kubectl exec -n platform deploy/graph-os -- \
python -c "from pydantic_ai_harness.experimental import acp; print(acp.__name__)"
The hostPath source-sync in §4 cannot
fix this — PYTHONPATH=/au:/eg only overlays the first-party agent_utilities/
epistemic_graph packages; third-party site-packages come from whatever the live
serving image (graph-os's Deployment .spec.template.spec.containers[0].image)
was last built with. Closing this gap requires an image rebuild + republish from
638ea524's lockfile — out of this (non-live)
task's scope (no image push), and the single highest-priority remaining item for
the live cutover decision (see the go/no-go in reports/phase10-cutover-manifest.md).
6. eg 2595a24 build result¶
cargo build --features full --target-dir ./target-isolated -j 4 at eg main
(= 2595a24), run in the canonical checkout for this feasibility check: **succeeded —
0 errors, 0 warnings, Finished \dev` profile [unoptimized + debuginfo] target(s)
in 10m 04s.** Binary confirmed present and functional
(target-isolated/debug/epistemic-graph-server, runs,--helpoutput matches the
live Deployment's exact CLI contract —--socket-path,--tcp-addr,--tcp-tls-cert/-key,--auth-secret/GRAPH_SERVICE_AUTH_SECRET,--persist-dir/GRAPH_SERVICE_PERSIST_DIR— confirming no new required flag and that--allow-insecure/--checkpoint-intervalare indeed gone, as the plan stated). **This
proves the redeploy artifact is buildable at the named commit.** Production deployment
uses--release(this feasibility check ran the plain, unoptimizedcargo build`, per
the task's exact instruction); the reach-pods path is the hostPath flip in §3 — a real
rebuild + stage-at-a-new-path + flip, never an in-place overwrite (the running binary
holds the file open).
7. Cross-references¶
- Input investigation:
reports/phase10-redeploy-plan.md(workspace-level, this runbook's companion — not committed to any repo; the workspace root is not a git repository). - Reference-file fix:
reports/live-incident-runbooks/graphos-homelab-live-config-fix.yaml(issuer scheme corrected on this same branch; relocated out of this public repository — it narrates a real homelab topology and a live auth-bypass finding that should not ship on GitHub — seereports/ci-parity-matrix-0801.md§11a Group B). - Messaging retirement source:
reports/live-incident-runbooks/messaging-bundle-retirement.yaml(unchanged — already accurate; same relocation reasoning). - Identity/session architecture:
agent_utilities/security/request_identity.py,agent_utilities/mcp/co_service_supervisor.py,agent_utilities/mcp/kg_server.py(_mint_process_session,mcp_server()).