From 765e38dbdd22048d6a910645c8193155889d7e5b Mon Sep 17 00:00:00 2001 From: itswl Date: Mon, 2 Nov 2026 17:02:46 +0800 Subject: [PATCH] =?UTF-8?q?hookprobe:=20the=20lock=20pins=20the=20agent=20?= =?UTF-8?q?runtime=20=E2=80=94=20the=20image=20stops=20installing=20a=20se?= =?UTF-8?q?cond=20Claude=20CLI=20that=20never=20ran?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The SDK runs the CLI bundled in its wheel before anything on PATH, and nothing here sets cli_path, so the npm CLI this Dockerfile pinned with CLAUDE_CODE_VERSION never ran: the published 0.9.2 image ran the SDK's 2.1.235 while 2.1.259 sat unused on PATH. The build argument and the npm install go; the claude-agent-sdk line in requirements.lock is the pin, and Dependabot proposes it. Node stays for MCP servers started with npx. Read back from a local build: the engine resolves the bundled 2.1.316, nothing named claude is on PATH, node and npx are present, and the image is 1.17 GB where the same lock built 1.69 GB. The real service in that image completed a scripted turn against a recording stand-in model under a third-party model name, with the guard refusing a protected write. Dependabot's comment and the prod compose's memory note are corrected. A new decision note records the choice, and the earlier note from the same day, which left it open, now points at it. --- ...6-11-02-the-lock-pins-the-agent-runtime.md | 46 +++++++++++++++++++ ...-the-message-reaches-the-model-as-words.md | 4 +- .github/dependabot.yml | 6 ++- hookprobe/Dockerfile | 27 +++++------ hookprobe/deploy/docker-compose.prod.yml | 5 +- 5 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 .agents/notes/implemented/2026-11-02-the-lock-pins-the-agent-runtime.md diff --git a/.agents/notes/implemented/2026-11-02-the-lock-pins-the-agent-runtime.md b/.agents/notes/implemented/2026-11-02-the-lock-pins-the-agent-runtime.md new file mode 100644 index 00000000..860d9bdf --- /dev/null +++ b/.agents/notes/implemented/2026-11-02-the-lock-pins-the-agent-runtime.md @@ -0,0 +1,46 @@ +--- +title: The lock pins the agent runtime, and the image stops installing a second CLI +status: implemented +date: 2026-11-02 +scope: hookprobe +--- + +## Decision + +The investigator's image no longer installs the Claude Code CLI from npm, and +the `CLAUDE_CODE_VERSION` build argument is gone. The CLI that runs an +investigation is the one inside the claude-agent-sdk wheel, so the +`claude-agent-sdk==` line in `hookprobe/requirements.lock` is the agent-runtime +pin, and Dependabot proposes it now that its strategy for this service raises +floors. Node stays, for MCP servers started with npx. + +## Why + +The SDK runs its bundled binary before it looks at PATH, and nothing here sets +`cli_path`. Read inside the images: the published 0.9.2 investigator ran CLI +2.1.235, bundled with SDK 0.2.140, while the npm CLI pinned at 2.1.259 sat on +PATH and never ran. After the relock to SDK 0.2.176 the engine runs 2.1.316. +The pin this Dockerfile documented had decided nothing since the SDK began +bundling the CLI, and it was still read as the answer: the 2026-09-03 fix for a +gateway's model ids raised it and changed nothing that ran. + +The other way out was to point the engine at the pinned npm CLI with +`cli_path`. That keeps a version Dependabot cannot see, runs a CLI older than +the one each SDK release was built and tested with, and keeps two pins that +have to agree. The operator chose the lock. + +## Consequences + +- A relock of claude-agent-sdk is a CLI bump. The SDK's changelog names the + CLI each release bundles, and `claude_agent_sdk/_cli_version.py` names it + inside an image. +- `claude` is no longer on PATH in the image, so `claude --version` there + fails; read the version file instead. +- The image lost the npm CLI: 1.69 GB before and 1.17 GB after, in a local + build of the same lock. +- An operator who needs a CLI release the SDK has not shipped yet has no build + argument any more. `ClaudeAgentOptions.cli_path` is where that would go, and + it stays unwired until someone needs it. +- Check every SDK bump against a gateway's own model ids: a CLI release once + refused them with `[claude-code:unrecognized_model]`. 2.1.235 and 2.1.316 + both accept them. diff --git a/.agents/notes/implemented/2026-11-02-the-message-reaches-the-model-as-words.md b/.agents/notes/implemented/2026-11-02-the-message-reaches-the-model-as-words.md index 4c7bb1ee..335da87c 100644 --- a/.agents/notes/implemented/2026-11-02-the-message-reaches-the-model-as-words.md +++ b/.agents/notes/implemented/2026-11-02-the-message-reaches-the-model-as-words.md @@ -58,5 +58,5 @@ commands. `CLAUDE_CODE_VERSION`. The SDK looks for its bundled binary first. The 0.9.2 image ran 2.1.235 with 2.1.259 unused on PATH, and this relock moves the engine to 2.1.316. So the lock, not the build argument, has been pinning the - agent runtime since the SDK began bundling it. Whether the image should drop - the npm CLI or point the engine at it is the operator's call. + agent runtime since the SDK began bundling it. The operator chose the lock + the same day: see 2026-11-02-the-lock-pins-the-agent-runtime. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index f91e5d8c..81d131ea 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -73,8 +73,10 @@ updates: # The base images were the one thing nothing nagged: python:3.14-slim / # python:3.12-slim / node:22-alpine drift silently, and a base with a fixed # CVE never arrives without someone bumping the tag. One entry per Dockerfile, - # same one-PR-per-service reasoning as the pip groups above. The pinned tool - # versions inside (CLAUDE_CODE_VERSION, nodesource) stay manual by design. + # same one-PR-per-service reasoning as the pip groups above. The nodesource + # version inside stays manual by design. The agent's CLI is not installed + # there any more: it ships inside claude-agent-sdk, which the investigator's + # pip entry proposes. - package-ecosystem: docker directory: "/hookrelay" schedule: diff --git a/hookprobe/Dockerfile b/hookprobe/Dockerfile index ba0c1c05..b7396619 100644 --- a/hookprobe/Dockerfile +++ b/hookprobe/Dockerfile @@ -1,21 +1,23 @@ -# hookprobe: Python runtime + Node (the Claude Agent SDK drives the Claude -# Code CLI) + a lean read-only diagnostic core. Domain CLIs (kubectl, db -# clients) stay opt-in below — extend with exactly what your alerts need. +# hookprobe: Python runtime + Node (for MCP servers started with npx, see +# deploy/mcp.example.json) + a lean read-only diagnostic core. Domain CLIs +# (kubectl, db clients) stay opt-in below — extend with exactly what your +# alerts need. FROM python:3.14-slim ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ NPM_CONFIG_UPDATE_NOTIFIER=false -# The CLI is the agent runtime itself. Unpinned, every image rebuild -# silently adopts whatever shipped upstream that day — the one dependency -# where drift changes behaviour most, pinned last. Bump deliberately: -# docker compose build --build-arg CLAUDE_CODE_VERSION=x.y.z -# 2.1.259: 2.1.232 rejected third-party model names its catalog did not know -# (`[claude-code:unrecognized_model]` on e.g. gpt-5.6-luna behind a custom -# ANTHROPIC_BASE_URL), which broke the investigator against gateways that serve -# only their own model ids. 2.1.259 accepts them. -ARG CLAUDE_CODE_VERSION=2.1.259 +# The agent's CLI is not installed here. The Claude Agent SDK wheel carries its +# own build of the Claude Code CLI and runs that one before anything on PATH, +# so the CLI that runs an investigation is the one requirements.lock pins +# through claude-agent-sdk (claude_agent_sdk/_cli_version.py names it), and +# Dependabot proposes it. A second CLI installed here was dead weight that read +# as the pin: the npm build this file pinned at 2.1.259 sat unused on PATH +# while 0.9.2 ran the SDK's 2.1.235. Bump the agent runtime by bumping the SDK, +# and check the new one still takes a gateway's own model ids: a CLI release +# once refused them (`[claude-code:unrecognized_model]` behind a custom +# ANTHROPIC_BASE_URL). 2.1.235 and 2.1.316 both accept them. # The diagnostic core: what any investigation reaches for first, and what # marketplace runbooks assume exists (a patrol skill mis-read this container @@ -26,7 +28,6 @@ RUN apt-get update \ procps jq iproute2 dnsutils netcat-openbsd lsof \ && curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ && apt-get install -y --no-install-recommends nodejs \ - && npm install -g "@anthropic-ai/claude-code@${CLAUDE_CODE_VERSION}" \ && apt-get purge -y gnupg \ && apt-get autoremove -y \ && rm -rf /var/lib/apt/lists/* diff --git a/hookprobe/deploy/docker-compose.prod.yml b/hookprobe/deploy/docker-compose.prod.yml index 8d9f880d..90828516 100644 --- a/hookprobe/deploy/docker-compose.prod.yml +++ b/hookprobe/deploy/docker-compose.prod.yml @@ -92,8 +92,9 @@ services: # analysis must not be able to take the host with it, and a compromised # run must find no privilege ladder. Measured before this existed: no # memory cap, no pid cap, full default capabilities. - # - mem_limit: the SDK CLI is a node process; 2g is roomy for one run - # plus subagents (a real investigation peaks well under 1g). + # - mem_limit: the agent is one native CLI process the SDK ships; 2g is + # roomy for one run plus subagents (a real investigation peaks well + # under 1g). # - pids_limit: a fork bomb hits a wall at 512 instead of the host limit. # - cap_drop ALL: an investigator only opens sockets and reads files; # it never chowns, mounts, raw-sockets or ptraces. Costs `ping`