diff --git a/.github/workflows/harness-canary.yml b/.github/workflows/harness-canary.yml index 1b2e76d..90d7567 100644 --- a/.github/workflows/harness-canary.yml +++ b/.github/workflows/harness-canary.yml @@ -77,11 +77,13 @@ permissions: contents: read env: - # Scheduled set. prime-agent, grok and muse are dispatch-only until their - # harness install path is verifiable (run 37692378153: `prime-agent`, - # `@xai-org/grok-build` and `muse-code` are all 404 on npm; Prime Agent is - # now a Rust binary (gsc#1378), Muse Code ships as a binary, Grok Build's - # package is unnamed in the plugin repo). sdk-proxy is out until + # Scheduled set. prime-agent, grok and muse are dispatch-only. Prime Agent + # now installs from its GitHub release tarballs (npm `prime-agent` is 404), + # but the Rust beta has no extension loader, so the ACP extension cannot + # load and the leg stays red until the plugin is ported (gsc#1378, #1406); + # it joins the scheduled set only once it can go green. `@xai-org/grok-build` + # and `muse-code` are 404 on npm (run 37692378153); Muse Code ships as a + # binary, Grok Build's package is unnamed in the plugin repo. sdk-proxy is out until # @agenticcontrolplane/proxy is actually published (404 on npm; not in the # SDK monorepo tree either). Details: docs/harness-canary.md. ALL_HARNESSES: '["claude-code","codex","opencode","qwen-code","pi","dsh","hermes","openclaw"]' @@ -144,9 +146,12 @@ jobs: # 2026-10-07). pi and prime-agent: the installer detects them by # ~/.pi/agent and ~/.prime/agent, which a fresh npm install does not # create, so the leg creates the dir (same as muse's ~/.config/muse). - # prime-agent, grok, muse names are still unverified (see env above). + # prime-agent installs from GitHub release tarballs (see its case arm); + # grok and muse names are still unverified (see env above). - name: Install current released harness id: harness + env: + GH_TOKEN: ${{ github.token }} run: | set -eo pipefail case "$HARNESS" in @@ -155,7 +160,24 @@ jobs: opencode) npm i -g opencode-ai@latest; v="$(opencode --version | head -1)" ;; qwen-code) npm i -g @qwen-code/qwen-code@latest; v="$(qwen --version | head -1)" ;; pi) npm i -g @earendil-works/pi-coding-agent@latest; mkdir -p "$HOME/.pi/agent"; v="$(pi --version | head -1)" ;; - prime-agent) npm i -g prime-agent@latest; mkdir -p "$HOME/.prime/agent"; v="$(prime-agent --version 2>/dev/null | head -1 || prime --version | head -1)" ;; + prime-agent) + # Not on npm (gsc#1390). Real install path: versioned GitHub + # release tarballs (PrimeIntellect-ai/prime-agent), verified + # against the release SHA256SUMS. "Latest" = newest non-draft + # release INCLUDING prereleases, because the Rust beta is what + # `prime-agent update` moves people onto; /releases/latest + # would pin the old TypeScript stable (v0.9.8). + tag="$(gh api repos/PrimeIntellect-ai/prime-agent/releases --jq '[.[] | select(.draft == false)][0].tag_name')" + test -n "$tag" || { echo "no prime-agent release resolved"; exit 1; } + ver="${tag#v}"; asset="prime-agent-${ver}-linux-x64.tar.gz" + pdir="$(mktemp -d)" + gh release download "$tag" -R PrimeIntellect-ai/prime-agent -p "$asset" -p SHA256SUMS -D "$pdir" + (cd "$pdir" && grep -F " $asset" SHA256SUMS | sha256sum -c -) + mkdir -p "$HOME/.local/share/prime-agent" "$HOME/.local/bin" "$HOME/.prime/agent" + tar -xzf "$pdir/$asset" -C "$HOME/.local/share/prime-agent" + ln -sf "$HOME/.local/share/prime-agent/prime-agent" "$HOME/.local/bin/prime-agent" + echo "$HOME/.local/bin" >> "$GITHUB_PATH"; export PATH="$HOME/.local/bin:$PATH" + v="$(prime-agent --version | head -1)" ;; grok) npm i -g @xai-org/grok-build@latest; v="$(grok --version | head -1)" ;; dsh) npm i -g @deepseek-ai/dsh@latest; v="$(dsh --version | head -1)" ;; hermes) pipx install hermes-agent; v="$(hermes --version | head -1)" ;; @@ -216,8 +238,24 @@ jobs: claude-code) test -n "$CLAUDE_CODE_OAUTH_TOKEN" || { echo "CLAUDE_CODE_OAUTH_TOKEN secret is not set"; exit 2; } timeout 300 "$HOME/.acp/bin/claude-acp" -p "$prompt" --allowedTools "Bash(echo:*)" 2>&1 | tee canary-run.log ;; - opencode) timeout 300 "$HOME/.acp/bin/opencode-acp" run "$prompt" 2>&1 | tee canary-run.log ;; - qwen-code) timeout 300 "$HOME/.acp/bin/qwen-acp" -p "$prompt" 2>&1 | tee canary-run.log ;; + # Headless `opencode run` has no one to answer a permission "ask" + # and auto-rejects it (opencode 1.18.35: "permission requested: + # bash (...); auto-rejecting"), because the installer writes + # permission.bash = "ask" for real, attended users. Pre-approve + # ONLY `echo *` for this one process via OPENCODE_CONFIG_CONTENT + # (highest-precedence config layer); ~/.config/opencode/opencode.json + # stays exactly as the installer wrote it, and the acp-opencode + # plugin's tool.execute.before still governs + audits the call. + opencode) OPENCODE_CONFIG_CONTENT='{"permission":{"bash":{"echo *":"allow"}}}' \ + timeout 300 "$HOME/.acp/bin/opencode-acp" run "$prompt" 2>&1 | tee canary-run.log ;; + qwen-code) + # Headless qwen cannot prompt, so without --yolo it declines + # run_shell_command ("permission ... was declined (non-interactive + # mode cannot prompt)"), the model hunts with tool_search/Read/glob + # and never emits the echo. --yolo skips only qwen's own prompt; + # the ACP PreToolUse hook still gates every call. &1 | tee canary-run.log ;; pi) timeout 300 "$HOME/.acp/bin/pi-acp" -p "$prompt" 2>&1 | tee canary-run.log ;; prime-agent) timeout 300 "$HOME/.acp/bin/prime-acp" -p "$prompt" 2>&1 | tee canary-run.log ;; *) echo "no live call defined for $HARNESS"; exit 2 ;; @@ -228,9 +266,9 @@ jobs: # in the window with its decision so the failure can be classified # (decision=allow -> plugin/harness permission contract drift; # decision=ask -> the gateway is asking in audit mode). - if grep -Eiq 'auto-rejecting|rejected permission|Denied at approval|permission denied|requires approval' canary-run.log; then + if grep -Eiq 'auto-rejecting|rejected permission|Denied at approval|permission denied|requires approval|permission was declined' canary-run.log; then echo "FAIL $HARNESS rejected the governed call (gsc#1380):" - grep -Ei 'auto-rejecting|rejected permission|Denied at approval|permission denied|requires approval' canary-run.log + grep -Ei 'auto-rejecting|rejected permission|Denied at approval|permission denied|requires approval|permission was declined' canary-run.log echo "audit rows seen since $CANARY_SINCE (decision per row):" node scripts/canary-assert.mjs rows --harness "$HARNESS" --since "$CANARY_SINCE" || true exit 1 diff --git a/docs/harness-canary.md b/docs/harness-canary.md index 96869f6..0d04c56 100644 --- a/docs/harness-canary.md +++ b/docs/harness-canary.md @@ -73,7 +73,7 @@ Each matrix leg, on a fresh `ubuntu-latest` runner with Node 22 (and Python `acp-canary--` through the launcher (commands in the table). The step **fails** if the transcript contains a rejection (`auto-rejecting`, `rejected permission`, `Denied at approval`, - `permission denied`, `requires approval`): that is gsc#1380 (a headless + `permission denied`, `requires approval`, `permission was declined`): that is gsc#1380 (a headless harness rejecting the governed call), and the leg goes red with the transcript attached rather than passing silently. On that failure the step prints every audit row in the window with its `decision` @@ -216,8 +216,11 @@ tenant doc before the first scheduled run. installer only checks `opencode --help` for `--model`. If the released opencode rejects a global `--model`, that leg fails at `governed-call` and the issue says so. The installer also sets `permission.bash = "ask"` in - `opencode.json`; headless `opencode run` relies on the `acp-opencode` - plugin to answer that ask. + `opencode.json`; headless `opencode run` auto-rejects an unanswered ask, so + the canary pre-approves `echo *` via `OPENCODE_CONFIG_CONTENT` for its own + process (see the opencode row under the failure table). That means the leg + proves the plugin's audit row, not the `permission.ask` allow path. +- qwen-code headless needs `--yolo` (shell calls are declined, not prompted, in `-p` mode); the leg passes it and closes stdin. - Headless `-p` on qwen-code, pi and prime-agent runs the shell call only if the hook's `allow` is honoured as the permission decision; if the harness still asks, the transcript shows the rejection and the leg goes red with @@ -261,8 +264,8 @@ sdk-pydantic-ai) were green. | `openclaw` | `openclaw@2026.9.8` refuses Node 22 (engines `>=24.16.0 <25 \|\| >=26.1.0`). | canary env | Node 24 for this leg only. | | `hermes` | Installer exit 23. `install.sh` greps `pipx list` for `package hermes`, which matches `hermes-agent`, then runs `pipx inject hermes ...` against a venv that is named `hermes-agent`; the shebang fallback has no pip (pipx venvs ship without it). | **installer bug** (`install.sh`, the hermes block around `pipx inject hermes acp-hermes`) | None: the leg is right to be red until the installer injects into `hermes-agent`. | | `claude-code` | Anthropic answered `401 authentication_error: Invalid bearer token` through the BYO path. The proxy forwards `Authorization: Bearer` and `anthropic-beta` verbatim (`apps/tenant-gateway/src/proxy/anthropicNative.ts`), so this is the `CLAUDE_CODE_OAUTH_TOKEN` secret itself. | secret | None: re-mint with `claude setup-token` and update the secret. | -| `qwen-code` | `qwen-acp -p` ran for the full 300 s (exit 124) printing `ACP write error: write EPIPE` from qwen-code's own `packages/cli/src/acp-integration` (Zed Agent Client Protocol transport); audit showed Read/Grep/agent/tool_search rows but never the `echo`. | upstream / product (reproduce in a sandbox) | None yet; gsc#1399 holds the log. | -| `opencode` | "The user rejected permission to use this specific tool call" in headless `run`. | product bug gsc#1380 | None. | +| `qwen-code` | `qwen-acp -p` ran for the full 300 s (exit 124) printing `ACP write error: write EPIPE` from qwen-code's own `packages/cli/src/acp-integration` (Zed Agent Client Protocol transport); audit showed Read/Grep/agent/tool_search rows but never the `echo`. | canary invocation (run 37784245015 later showed qwen finishing in ~60 s with 33 tool rows and no shell row: headless qwen declines `run_shell_command` without YOLO) | `qwen-acp --yolo -p ... =1.3.3'`. | | `sdk-governance-anthropic` | `getConfig` is not exported by `@agenticcontrolplane/governance-anthropic@0.2.1` (it re-exports `governed`, `withContext`, `configure`, `getContext`). | canary script | Version from the package manifest, base URL from env. | | `sdk-proxy` | `@agenticcontrolplane/proxy` is 404 on npm and not in the SDK monorepo tree. | unpublished | Out of the scheduled set until it is published. | diff --git a/scripts/canary-sdk-governance-anthropic.mjs b/scripts/canary-sdk-governance-anthropic.mjs index 90c95ec..2070945 100644 --- a/scripts/canary-sdk-governance-anthropic.mjs +++ b/scripts/canary-sdk-governance-anthropic.mjs @@ -10,14 +10,30 @@ // governance-anthropic re-exports governed/withContext/configure/getContext // from @agenticcontrolplane/governance but NOT getConfig (0.2.1), so the // version comes from the package manifest and the base URL from env. -import { createRequire } from "node:module"; +import { readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join } from "node:path"; import { configure, governHandlers, withContext } from "@agenticcontrolplane/governance-anthropic"; const key = process.env.ACP_CANARY_KEY; if (!key) { console.error("ACP_CANARY_KEY is not set"); process.exit(2); } const marker = process.env.MARKER || `acp-canary-local-${Date.now()}`; -const require = createRequire(import.meta.url); -const version = require("@agenticcontrolplane/governance-anthropic/package.json").version || "unknown"; +// The package "exports" map has no "./package.json" subpath (0.2.x), so +// require(".../package.json") throws ERR_PACKAGE_PATH_NOT_EXPORTED. Resolve +// the allowed main entry, then walk up to the manifest on disk. +function pkgVersion() { + try { + let dir = dirname(fileURLToPath(import.meta.resolve("@agenticcontrolplane/governance-anthropic"))); + for (let i = 0; i < 6; i++, dir = dirname(dir)) { + try { + const m = JSON.parse(readFileSync(join(dir, "package.json"), "utf8")); + if (m.name === "@agenticcontrolplane/governance-anthropic") return m.version || "unknown"; + } catch {} + } + } catch {} + return "unknown"; +} +const version = pkgVersion(); configure({ baseUrl: process.env.ACP_BASE_URL || "https://api.agenticcontrolplane.com", clientHeader: `acp-canary-sdk-governance-anthropic/${version}` }); let lapse = "";