From d26d128b5eb6431a146f53ea830865dbbc5e5e57 Mon Sep 17 00:00:00 2001 From: Imran Hassanali <2126188+imran31415@users.noreply.github.com> Date: Thu, 23 Jul 2026 11:26:05 -0700 Subject: [PATCH] fix(workspace): ship the Python backend in the image, not the ConfigMap (#430) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The workspace `browser-config` ConfigMap embedded the entire Python backend (server.py + memory/skills/adapters/gateway/mcp_* — ~992 KiB across 32 files) via `{{ .Files.Get }}`. As the code grew it crossed BOTH 1 MiB limits: - the rendered ConfigMap object hit ~1086 KiB (> 1 MiB ConfigMap cap), and - the Helm release Secret (rendered manifest + bundled chart files) hit ~1 MiB, so `helm upgrade` started failing for every workspace: Secret "sh.helm.release.v1..vN" ... Too long: may not be more than 1048576 bytes This silently froze every workspace's ConfigMap-delivered server.py — self-serve updates only patched the *image*, so e.g. a v1.41.1 image still ran a v1.40.1 server.py (no Mission Control -> /api/missioncontrol/queue 404). Fix — bake the backend into the image and drop it from the chart: - devlaptop/Dockerfile: COPY server.py + memory/ skills/ adapters/ (+ the top-level mcp_*/gateway/seed_*/harness/etc.) into /opt/browser-src. - start.sh: seed /tmp/browser and /home/dev/.claude-memory from /opt/browser-src (a `cp -a` tree copy) instead of unpacking flat /browser-config ConfigMap keys. - Delete templates/browser-configmap.yaml; remove its mount/volume/checksum from the Deployment. Code changes now roll the pod via the image tag. - .helmignore the backend .py so Helm no longer bundles it into the chart — this is what actually shrinks the release Secret. Packaged chart: ~1 MB -> 64 KiB. Consequence: backend changes ship via an image rebuild (a release), not a config-only `make ship-config`. Every release already rebuilds the image, so this also fixes the long-standing "image updated but backend stale" class of bug — an image update now delivers backend code. Verified: helm lint clean; 106 helm unittests green (2 updated for the new path); `helm template` renders with no browser-config ConfigMap/mount; a local COPY+seed simulation imports server/hypervisor_session/gateway/mcp_*/memory/ skills/adapters cleanly and confirms the missioncontrol route is present. --- charts/workspace/.helmignore | 21 +++++ charts/workspace/start.sh | 87 +++++-------------- .../templates/browser-configmap.yaml | 74 ---------------- charts/workspace/templates/deployment.yaml | 9 +- charts/workspace/tests/deployment_test.yaml | 1 - .../tests/mcp_registry_boot_test.yaml | 14 +-- devlaptop/Dockerfile | 15 ++++ 7 files changed, 61 insertions(+), 160 deletions(-) delete mode 100644 charts/workspace/templates/browser-configmap.yaml diff --git a/charts/workspace/.helmignore b/charts/workspace/.helmignore index ea1d45da..74c0cb79 100644 --- a/charts/workspace/.helmignore +++ b/charts/workspace/.helmignore @@ -25,6 +25,27 @@ web/vitest.config.ts web/index.html web/README.md +# Workspace Python backend — baked into the image (devlaptop/Dockerfile -> +# /opt/browser-src) and seeded by start.sh; NOT rendered into a ConfigMap +# anymore (#430). The chart doesn't reference these at render time, and bundling +# them into the chart tarball blew the 1 MiB Helm-release-Secret limit. +server.py +hypervisor_session.py +gateway.py +harness.py +harness_test.py +mcp_agent_orchestrator.py +mcp_dashboard.py +mcp_memory.py +mcp_registry.py +memory_inject_hook.py +playwright_reaper.py +seed_claude_config.py +seed_demo.py +memory/ +skills/ +adapters/ + # Python bytecode caches __pycache__/ *.pyc diff --git a/charts/workspace/start.sh b/charts/workspace/start.sh index e7fe3b31..9649ef33 100644 --- a/charts/workspace/start.sh +++ b/charts/workspace/start.sh @@ -739,62 +739,25 @@ if [ -x "$LIBREFANG_TARGET/bin/librefang" ]; then || HOME=/home/dev "$LIBREFANG_TARGET/bin/librefang" start >/dev/null 2>&1 & fi -# The memory subsystem ships its Python package as flat configmap keys -# (configmap keys cannot contain "/"). Unpack them into a real `memory/` -# tree next to server.py so `from memory.manager import ...` resolves. -mkdir -p /tmp/browser/memory /home/dev/.claude-memory/backups +# ── Workspace Python backend: seed from the image (issue #430) ───────────── +# server.py + the memory/skills/adapters/gateway subsystems are baked into the +# image at /opt/browser-src (they outgrew the 1 MiB ConfigMap). Copy the whole +# tree into /tmp/browser so `import server`, `from memory.manager import ...`, +# `from skills.providers... import ...`, `import gateway`, and +# `from adapters.internal import ...` all resolve. +mkdir -p /tmp/browser /home/dev/.claude-memory/backups chmod 700 /home/dev/.claude-memory -# Materialize package files (rename flat keys → real layout). -install -m 0644 /browser-config/memory__init__.py /tmp/browser/memory/__init__.py -install -m 0644 /browser-config/memory_store.py /tmp/browser/memory/store.py -install -m 0644 /browser-config/memory_manager.py /tmp/browser/memory/manager.py -install -m 0644 /browser-config/memory_sync.py /tmp/browser/memory/sync.py -install -m 0644 /browser-config/memory_embeddings.py /tmp/browser/memory/embeddings.py -install -m 0644 /browser-config/memory_embeddings_worker.py /tmp/browser/memory/embeddings_worker.py - -# The skills subsystem (multi-harness SKILL.md surface, issue #187) ships -# the same way: flat configmap keys reassembled into a real package tree. -mkdir -p /tmp/browser/skills/providers -install -m 0644 /browser-config/skills__init__.py /tmp/browser/skills/__init__.py -install -m 0644 /browser-config/skills_model.py /tmp/browser/skills/model.py -install -m 0644 /browser-config/skills_parser.py /tmp/browser/skills/parser.py -install -m 0644 /browser-config/skills_sync.py /tmp/browser/skills/sync.py -install -m 0644 /browser-config/skills_commands.py /tmp/browser/skills/commands.py -install -m 0644 /browser-config/skills_providers__init__.py /tmp/browser/skills/providers/__init__.py -install -m 0644 /browser-config/skills_providers_claude.py /tmp/browser/skills/providers/claude.py -install -m 0644 /browser-config/skills_providers_opencode.py /tmp/browser/skills/providers/opencode.py -install -m 0644 /browser-config/skills_providers_ante.py /tmp/browser/skills/providers/ante.py -install -m 0644 /browser-config/skills_providers_antigravity.py /tmp/browser/skills/providers/antigravity.py - -# The Conversation Gateway (issue #306) — chat with the Hypervisor over a -# channel (WhatsApp) plus the in-app Walkie-Talkie loopback preview — ships -# gateway.py flat and the adapters/ package as flat keys (configmap keys -# cannot contain "/"). Reassemble next to server.py so `import gateway` and -# `from adapters.internal import ...` resolve; without this the gateway -# subsystem self-disables (_GATEWAY_AVAILABLE=False) and every -# /api/gateway/* call 503s. -mkdir -p /tmp/browser/adapters -install -m 0644 /browser-config/gateway.py /tmp/browser/gateway.py -install -m 0644 /browser-config/adapters__init__.py /tmp/browser/adapters/__init__.py -install -m 0644 /browser-config/adapters_whatsapp.py /tmp/browser/adapters/whatsapp.py -install -m 0644 /browser-config/adapters_internal.py /tmp/browser/adapters/internal.py - -# Seed the per-user MCP server + user-prompt-submit hook next to the -# SQLite file so claude config points at PVC-backed paths that survive -# configmap rotations. -install -m 0755 /browser-config/mcp_memory.py /home/dev/.claude-memory/mcp_memory.py -install -m 0755 /browser-config/memory_inject_hook.py /home/dev/.claude-memory/memory_inject_hook.py -# The MCP server imports the same memory.* package; expose it alongside. +cp -a /opt/browser-src/. /tmp/browser/ + +# The memory MCP server (mcp_memory.py) imports the same memory.* package; expose +# it + the user-prompt-submit hook next to the PVC-backed SQLite db so claude +# config points at stable paths across restarts. +install -m 0755 /opt/browser-src/mcp_memory.py /home/dev/.claude-memory/mcp_memory.py +install -m 0755 /opt/browser-src/memory_inject_hook.py /home/dev/.claude-memory/memory_inject_hook.py rm -rf /home/dev/.claude-memory/memory -mkdir -p /home/dev/.claude-memory/memory -install -m 0644 /tmp/browser/memory/__init__.py /home/dev/.claude-memory/memory/__init__.py -install -m 0644 /tmp/browser/memory/store.py /home/dev/.claude-memory/memory/store.py -install -m 0644 /tmp/browser/memory/manager.py /home/dev/.claude-memory/memory/manager.py -install -m 0644 /tmp/browser/memory/sync.py /home/dev/.claude-memory/memory/sync.py -install -m 0644 /tmp/browser/memory/embeddings.py /home/dev/.claude-memory/memory/embeddings.py -install -m 0644 /tmp/browser/memory/embeddings_worker.py /home/dev/.claude-memory/memory/embeddings_worker.py -# Register the MCP server in the user's claude config (idempotent merge). -python3 /browser-config/seed_claude_config.py || \ +cp -a /opt/browser-src/memory /home/dev/.claude-memory/memory +# Register the memory MCP server in the user's claude config (idempotent merge). +python3 /opt/browser-src/seed_claude_config.py || \ log_stage "WARNING: seed_claude_config.py failed (memory MCP not registered)" # Seed Ante's config so a spawned/selected Ante agent gets the SAME stdio @@ -886,24 +849,14 @@ fi # (/home/dev/.claude-tasks/mcp-servers.json) but most provider configs are # under the ephemeral $HOME (~/.claude.json, opencode.json) or regenerated # each boot, so the fan-out must re-run AFTER all the seeding blocks. Runs -# from /browser-config (same delivery as seed_claude_config.py); best-effort — +# from /opt/browser-src (same delivery as seed_claude_config.py); best-effort — # a failed provider never stalls boot. log_stage "syncing user MCP servers to provider configs" -python3 /browser-config/mcp_registry.py --sync || \ +python3 /opt/browser-src/mcp_registry.py --sync || \ log_stage "WARNING: mcp_registry.py sync failed (user MCP servers not re-applied)" log_stage "starting browser/Claude API server on :6080" -mkdir -p /tmp/browser -# Copy Python sources (these need a pod restart to take effect). -# Skip the flat memory_* / memory__init__.py keys — reassembled above. -for f in /browser-config/*; do - base=$(basename "$f") - case "$base" in - memory_*|memory__init__.py) continue ;; - skills_*|skills__init__.py) continue ;; - esac - cp "$f" /tmp/browser/ -done +mkdir -p /tmp/browser # server.py + subsystems already seeded from /opt/browser-src above # Symlink HTML/CSS/JS from the separate (non-checksummed) browser-html # ConfigMap so dashboard edits + helm-upgrade refresh without a pod # restart. The kubelet syncs the new ConfigMap to /browser-html diff --git a/charts/workspace/templates/browser-configmap.yaml b/charts/workspace/templates/browser-configmap.yaml deleted file mode 100644 index 717df551..00000000 --- a/charts/workspace/templates/browser-configmap.yaml +++ /dev/null @@ -1,74 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: browser-config-{{ .Values.user.name }} - namespace: {{ .Values.namespace }} -# HTML/CSS/JS for the dashboard lives in browser-html-{{ .Values.user.name }} -# (separate ConfigMap, not checksummed in the deployment) so dashboard-only -# changes don't require a pod restart. Keep this ConfigMap to Python source -# + supporting files — those DO need a restart when changed. -data: - server.py: | -{{ .Files.Get "server.py" | indent 4 }} - harness.py: | -{{ .Files.Get "harness.py" | indent 4 }} - harness_test.py: | -{{ .Files.Get "harness_test.py" | indent 4 }} - mcp_memory.py: | -{{ .Files.Get "mcp_memory.py" | indent 4 }} - memory_inject_hook.py: | -{{ .Files.Get "memory_inject_hook.py" | indent 4 }} - seed_claude_config.py: | -{{ .Files.Get "seed_claude_config.py" | indent 4 }} - playwright_reaper.py: | -{{ .Files.Get "playwright_reaper.py" | indent 4 }} - seed_demo.py: | -{{ .Files.Get "seed_demo.py" | indent 4 }} - memory__init__.py: | -{{ .Files.Get "memory/__init__.py" | indent 4 }} - memory_store.py: | -{{ .Files.Get "memory/store.py" | indent 4 }} - memory_manager.py: | -{{ .Files.Get "memory/manager.py" | indent 4 }} - memory_sync.py: | -{{ .Files.Get "memory/sync.py" | indent 4 }} - memory_embeddings.py: | -{{ .Files.Get "memory/embeddings.py" | indent 4 }} - memory_embeddings_worker.py: | -{{ .Files.Get "memory/embeddings_worker.py" | indent 4 }} - skills__init__.py: | -{{ .Files.Get "skills/__init__.py" | indent 4 }} - skills_model.py: | -{{ .Files.Get "skills/model.py" | indent 4 }} - skills_parser.py: | -{{ .Files.Get "skills/parser.py" | indent 4 }} - skills_sync.py: | -{{ .Files.Get "skills/sync.py" | indent 4 }} - skills_commands.py: | -{{ .Files.Get "skills/commands.py" | indent 4 }} - skills_providers__init__.py: | -{{ .Files.Get "skills/providers/__init__.py" | indent 4 }} - skills_providers_claude.py: | -{{ .Files.Get "skills/providers/claude.py" | indent 4 }} - skills_providers_opencode.py: | -{{ .Files.Get "skills/providers/opencode.py" | indent 4 }} - skills_providers_ante.py: | -{{ .Files.Get "skills/providers/ante.py" | indent 4 }} - skills_providers_antigravity.py: | -{{ .Files.Get "skills/providers/antigravity.py" | indent 4 }} - mcp_agent_orchestrator.py: | -{{ .Files.Get "mcp_agent_orchestrator.py" | indent 4 }} - mcp_dashboard.py: | -{{ .Files.Get "mcp_dashboard.py" | indent 4 }} - hypervisor_session.py: | -{{ .Files.Get "hypervisor_session.py" | indent 4 }} - mcp_registry.py: | -{{ .Files.Get "mcp_registry.py" | indent 4 }} - gateway.py: | -{{ .Files.Get "gateway.py" | indent 4 }} - adapters__init__.py: | -{{ .Files.Get "adapters/__init__.py" | indent 4 }} - adapters_whatsapp.py: | -{{ .Files.Get "adapters/whatsapp.py" | indent 4 }} - adapters_internal.py: | -{{ .Files.Get "adapters/internal.py" | indent 4 }} \ No newline at end of file diff --git a/charts/workspace/templates/deployment.yaml b/charts/workspace/templates/deployment.yaml index 309ba8eb..9d43a94b 100644 --- a/charts/workspace/templates/deployment.yaml +++ b/charts/workspace/templates/deployment.yaml @@ -23,7 +23,8 @@ spec: kube-coder.dev/component: workspace {{- end }} annotations: - checksum/browser-config: {{ include (print $.Template.BasePath "/browser-configmap.yaml") . | sha256sum }} + # server.py + the Python backend now ship in the image (#430), so an + # image-tag change rolls the pod — no browser-config checksum needed. checksum/claude-config: {{ include (print $.Template.BasePath "/claude-configmap.yaml") . | sha256sum }} checksum/terminal-entry: {{ include (print $.Template.BasePath "/terminal-entry-configmap.yaml") . | sha256sum }} checksum/workspace-entrypoint: {{ include (print $.Template.BasePath "/workspace-entrypoint-configmap.yaml") . | sha256sum }} @@ -278,8 +279,6 @@ spec: volumeMounts: - name: home mountPath: /home/dev - - name: browser-config - mountPath: /browser-config - name: browser-html mountPath: /browser-html - name: claude-config @@ -402,10 +401,6 @@ spec: - name: home persistentVolumeClaim: claimName: ws-{{ .Values.user.name }}-home - - name: browser-config - configMap: - name: browser-config-{{ .Values.user.name }} - defaultMode: 0755 - name: browser-html configMap: name: browser-html-{{ .Values.user.name }} diff --git a/charts/workspace/tests/deployment_test.yaml b/charts/workspace/tests/deployment_test.yaml index fb65012f..12af95f1 100644 --- a/charts/workspace/tests/deployment_test.yaml +++ b/charts/workspace/tests/deployment_test.yaml @@ -1,7 +1,6 @@ suite: deployment tests templates: - templates/deployment.yaml - - templates/browser-configmap.yaml - templates/claude-configmap.yaml - templates/terminal-entry-configmap.yaml - templates/workspace-entrypoint-configmap.yaml diff --git a/charts/workspace/tests/mcp_registry_boot_test.yaml b/charts/workspace/tests/mcp_registry_boot_test.yaml index 969fff0b..e6b340c3 100644 --- a/charts/workspace/tests/mcp_registry_boot_test.yaml +++ b/charts/workspace/tests/mcp_registry_boot_test.yaml @@ -2,27 +2,19 @@ suite: user MCP server registry boot wiring (#353) # User-added MCP servers live in one PVC registry and are fanned out to each # assistant's native config. Most of those configs sit under the ephemeral # $HOME or are regenerated at boot, so start.sh MUST re-run the fan-out AFTER -# every default-seeding block — and the module itself must ship in the -# browser ConfigMap for both the boot invocation and server.py's import. +# every default-seeding block. The module itself now ships in the image at +# /opt/browser-src (#430), not the browser ConfigMap. templates: - templates/workspace-entrypoint-configmap.yaml - - templates/browser-configmap.yaml values: - test-values.yaml tests: - - it: ships mcp_registry.py in the browser ConfigMap - template: templates/browser-configmap.yaml - asserts: - - matchRegex: - path: data["mcp_registry.py"] - pattern: "REGISTRY_FILE = '/home/dev/.claude-tasks/mcp-servers.json'" - - it: re-applies user MCP servers in start.sh after the default seeding template: templates/workspace-entrypoint-configmap.yaml asserts: - matchRegex: path: data["start.sh"] - pattern: "python3 /browser-config/mcp_registry.py --sync" + pattern: "python3 /opt/browser-src/mcp_registry.py --sync" # The sync must run AFTER the Codex default seeding (the last of the # per-provider seeding blocks) so it layers on top of the defaults. - matchRegex: diff --git a/devlaptop/Dockerfile b/devlaptop/Dockerfile index 065ef15b..414ebbaa 100644 --- a/devlaptop/Dockerfile +++ b/devlaptop/Dockerfile @@ -330,6 +330,21 @@ OUTER # migration and at / once Phase 6 cuts over. COPY --from=dashboard-builder /web/dist /opt/dashboard-dist +# ── Workspace Python backend (server.py + memory/skills/adapters/gateway) ── +# Baked into the image at /opt/browser-src. This used to be embedded in the +# browser-config ConfigMap via {{ .Files.Get }}, but the sources (~1 MB) outgrew +# BOTH the 1 MiB ConfigMap object limit AND the 1 MiB Helm-release-Secret limit, +# which silently blocked every workspace `helm upgrade` (issue #430). start.sh +# seeds /tmp/browser + /home/dev/.claude-memory from here at boot. Consequence: +# backend changes now ship via an image rebuild (a release), not a config-only +# helm upgrade — every release already rebuilds the image, so this is fine. +COPY charts/workspace/*.py /opt/browser-src/ +COPY charts/workspace/memory /opt/browser-src/memory +COPY charts/workspace/skills /opt/browser-src/skills +COPY charts/workspace/adapters /opt/browser-src/adapters +RUN find /opt/browser-src -name __pycache__ -type d -prune -exec rm -rf {} + 2>/dev/null; \ + find /opt/browser-src -name '*.pyc' -delete 2>/dev/null; true + # Non-root dev user RUN useradd -m -s /bin/bash dev && echo "dev ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers # Also give ubuntu user (created by code-server) passwordless sudo