diff --git a/agent-network/bin/cli.ts b/agent-network/bin/cli.ts index 48eb27b48..ab92423f0 100644 --- a/agent-network/bin/cli.ts +++ b/agent-network/bin/cli.ts @@ -6956,6 +6956,14 @@ anet node rename [--force] // dst at 0700 even when src is 0755. Structural fix, no post-cpSync chmod // (would be TOCTOU vs the预检's own identity-bound fchmod branch). mkdirSync(newDir, { mode: 0o700, recursive: false }); + // cpSync also creates nested directories through the process umask. For + // OpenCode profiles that would turn the private .config/.local/cache tree + // into 0755 and make PHASE 1 reject its own copy. Establish and validate + // every private root before copying; cpSync preserves an existing target + // directory, so no post-copy chmod or check-then-repair window is needed. + if (normalizeRuntime(stored) === "opencode-cli") { + prepareOpencodeNodeForProfileWrite(newDir); + } cpSync(oldDir, newDir, { recursive: true }); const newLock = join(newDir, "rename.lock"); if (existsSync(newLock)) rmSync(newLock, { force: true }); // lock belongs to oldDir only diff --git a/agent-network/src/opencode-agent-node-pair.ts b/agent-network/src/opencode-agent-node-pair.ts index 1a146a115..0c9f3477e 100644 --- a/agent-network/src/opencode-agent-node-pair.ts +++ b/agent-network/src/opencode-agent-node-pair.ts @@ -17,8 +17,8 @@ import type { Stats } from "fs"; import { basename, delimiter, dirname, isAbsolute, join, relative, resolve } from "path"; import { opencodeOwnedPathModeIsSafe } from "./opencode-owner-mode"; -export const OPENCODE_AGENT_NETWORK_VERSION = "2.3.0-preview.34"; -export const OPENCODE_AGENT_NODE_VERSION = "2.5.0-preview.28"; +export const OPENCODE_AGENT_NETWORK_VERSION = "2.3.0-preview.39"; +export const OPENCODE_AGENT_NODE_VERSION = "2.5.0-preview.31"; export const OPENCODE_AGENT_NODE_SPEC = `@sleep2agi/agent-node@${OPENCODE_AGENT_NODE_VERSION}`; diff --git a/docs/RELEASE-SOP.md b/docs/RELEASE-SOP.md index 6adccd142..45a28e0e6 100644 --- a/docs/RELEASE-SOP.md +++ b/docs/RELEASE-SOP.md @@ -194,7 +194,10 @@ for p in agent-network agent-node server; do done # ② PINNED 链(见 §3 第 2 条) git show origin/main:agent-network/bin/cli.ts | grep 'PINNED_SERVER_VERSION *=' -# ③ 与 npm 上的 preview tag 逐一比对,必须相等 +# ③ OpenCode 精确配对 pin(两个常量必须分别等于对应包的 preview tag) +git show origin/main:agent-network/src/opencode-agent-node-pair.ts | \ + grep -E 'OPENCODE_AGENT_(NETWORK|NODE)_VERSION *=' +# ④ 与 npm 上的 preview tag 逐一比对,必须相等 npm view @sleep2agi/@preview version ``` diff --git a/docs/tests/report-test745-opencode-pair-sync.txt b/docs/tests/report-test745-opencode-pair-sync.txt new file mode 100644 index 000000000..97119c031 --- /dev/null +++ b/docs/tests/report-test745-opencode-pair-sync.txt @@ -0,0 +1,120 @@ +# Test 745 — OpenCode exact-pair release gate and rename recovery + +Date: 2026-08-13 (Asia/Shanghai) +Base commit: e0c27341ea004a46da3ff7abfaba2fd2cc165bc5 +Source commit: 03fa958a3261d2ae59a34883670a69e083af4e14 +Scope: OpenCode agent-network/agent-node exact-pair pins, release synchronization, +test384/test386 fixtures, and the private-root rename lifecycle required by test384. + +## Result + +PASS. No production deployment, npm publication, registry tag mutation, database +write, or running-node change was performed. + +The exact preview pair observed in the public registry and pinned by this source is: + +- @sleep2agi/agent-network@2.3.0-preview.39 +- @sleep2agi/agent-node@2.5.0-preview.31 +- opencode-ai@1.18.1 (the test384 OpenCode dependency) + +## Why the lifecycle fix is in the same source + +Updating only the pair constants did not make the committed release gate pass. +The diagnostic progression was: + +1. The base test384 Dockerfile failed before the product test because run.sh + sourced tests/lib/safe-rm.sh but the image did not contain that file. +2. After supplying that dependency, the wizard fixture still modeled six runtime + choices and selected the wrong entry after grok-build-cli had become a seventh + production choice. +3. With those fixture defects repaired, the real rename lifecycle reached L8 and + failed because the copied OpenCode tree contained a nested .config directory + with mode 0755. The existing rename code pre-created only the destination root + at 0700; cpSync could create nested private roots with a wider mode. +4. The source now calls prepareOpencodeNodeForProfileWrite(newDir) before cpSync, + so .config, .local/share, and .local/state are created or validated as private + roots before any recursive copy. The normal and deliberately slowed lifecycle + both pass. + +This is the same reproducible failure family tracked by issue #457. It is not a +post-copy chmod repair and does not create a permissions-widening interval. + +## Registry artifact capability smoke + +A clean node:22-bookworm-slim environment installed the exact public-registry +versions above. The observed versions were exact, agent-node was executable, +agent-node help exposed opencode-cli, anet --version reported network preview.39 +with node preview.31, and the OpenCode ACP help path succeeded. + +Marker: REGISTRY_PAIR_CAPABILITY_SMOKE_PASS + +This smoke verifies installability and command/help capabilities only. It did not +run a real CommHub task through the npm registry artifacts. + +## Docker evidence + +All commands were run from the clean source worktree with Docker via sg docker. + +### test384 — local-package OpenCode E2E + +- Image: sha256:90b67da6f0d5cb51109db38c9194ced6e2af36d35880e452d363a317d70d20ed +- Runner log SHA256: 5b843feda25c7752dc859b9176b058449964f24a5611bedad6413acc608bb448 +- Result: OVERALL PASS +- Exact local tarballs: network preview.39 / node preview.31 +- Real picker: seven runtime choices, OpenCode selected +- Security: private roots and keys checked; hostile configurations rejected +- Runtime: real OpenCode 1.18.1 task reached replied; zero orphan task +- Lifecycle: rename preserved node_id and binding, then delete completed + +The same image was run with bash -x to perturb timing: + +- Slow runner log SHA256: 0423ca20c2aea5fe48447f33829778e4a1e531044bb95180d759d8a2dff9d15c +- Result: OVERALL PASS, including L8 rename/delete lifecycle + +### test386 — exact agent-node selection gate + +- Image: sha256:aa446982ccd0682e879d00d1c515aa6edff5431911175949354786a405f79c18 +- Runner log SHA256: 76df79d692c173221bd5480601be71f44b21018838574271e62674872d6bf2f2 +- Result: OVERALL PASS +- Exact expectations and fixtures: network preview.39 / node preview.31 + +### test621 — test384 hold-open layer isolation + +- Image: sha256:6d95199da9d04f7031a5b7bba6035781dd4d76678f99e465040f29d5a5e565a6 +- Image env TEST621_SOURCE_COMMIT: 03fa958a3261d2ae59a34883670a69e083af4e14 +- Runner log SHA256: e555485489e6306c56b7313ecc7dccba78eb4a3a6680edf39b395c21b2669979 +- Result: PASS +- Witnessed red: hold-open-layer-cancel, rc=1 + +## Release synchronization behavior + +scripts/sync-pinned-versions.sh now treats the two exported OpenCode pair +constants as release pins. A diagnostic apply run first reverted both constants, +then updated each to the exact package version. It also verified that the source +file still ended in byte 0a. The implementation preserves a trailing newline +through command substitution by adding and removing an explicit sentinel. + +docs/RELEASE-SOP.md now requires reading both OpenCode constants back from +origin/main and matching them to the preview tags before release. + +## Provenance boundary + +The source commit contains two implementation commits. The report is added only +after that frozen source; no source or test file is changed by the report commit. +The local image IDs and runner digests above are execution evidence, not a claim +that logs containing generated IDs or model output are reproducible byte-for-byte. + +## Honest limits + +- No Windows execution was performed. +- No npm publish, GitHub release, deployment, production DB access, or fleet + restart was performed. +- The registry smoke was non-interactive capability verification only. +- The real replied-task E2E used source-built local tarballs, not the npm registry + artifacts. +- test384 uses a live free OpenCode model; response content and generated IDs are + nondeterministic. Its runner digest is an integrity record for this run, not a + reproducible provenance anchor. +- A mandatory CI gate for the complete agent-network unit-test domain remains + NOT COVERED. That governance/CI tail remains tracked by issue #745 and is not + represented as solved by this source. diff --git a/scripts/sync-pinned-versions.sh b/scripts/sync-pinned-versions.sh index dfe0aace7..b4fab77f4 100755 --- a/scripts/sync-pinned-versions.sh +++ b/scripts/sync-pinned-versions.sh @@ -68,6 +68,7 @@ register() { # @sleep2agi/agent-network — 用户安装入口 register "@sleep2agi/agent-network" "docs-site/docs/guide/runtimes.md" register "@sleep2agi/agent-network" "docs-site/docs/en/guide/runtimes.md" +register "@sleep2agi/agent-network" "agent-network/src/opencode-agent-node-pair.ts:OPENCODE_AGENT_NETWORK_VERSION" # @sleep2agi/agent-node — runtime + SDK 行号锚点 register "@sleep2agi/agent-node" "docs-site/docs/guide/runtimes.md" @@ -76,6 +77,7 @@ register "@sleep2agi/agent-node" "docs-site/docs/guide/agent-node.md" register "@sleep2agi/agent-node" "docs-site/docs/en/guide/agent-node.md" register "@sleep2agi/agent-node" "docs-site/docs/guide/sdk-deep-dive.md" register "@sleep2agi/agent-node" "docs-site/docs/en/guide/sdk-deep-dive.md" +register "@sleep2agi/agent-node" "agent-network/src/opencode-agent-node-pair.ts:OPENCODE_AGENT_NODE_VERSION" # @sleep2agi/commhub-server — agent-network CLI 内 PINNED_SERVER_VERSION 常量 register "@sleep2agi/commhub-server" "agent-network/bin/cli.ts:PINNED_SERVER_VERSION" @@ -121,12 +123,13 @@ md_pattern() { "$pkg_no_scope" "$ESCAPED_VERSION" } -# cli.ts PINNED 常量模板:仅替换 `const NAME = "..."` 字串字面值 +# PINNED 常量模板:仅替换 `const NAME = "..."` 或 +# `export const NAME = "..."` 的字串字面值。 # 不动 declaration 周围 logic、不动 NAME 之外的同字串引用、不改类型/作用域 ts_pinned_pattern() { local const_name="$1" - # 严格锚定 `const = "x.y.z..."`,保留引号 + 行内其它内容 - printf 's#\\(const %s = \\)"[^"]*"#\\1"%s"#g' \ + # 严格锚定可选 export + `const = "x.y.z..."`,保留其它内容。 + printf 's#\\(\\(export \\)\\?const %s = \\)"[^"]*"#\\1"%s"#g' \ "$const_name" "$ESCAPED_VERSION" } @@ -139,19 +142,20 @@ apply_or_preview() { return fi local before - before="$(cat "$file")" + # Command substitution strips trailing newlines. Append a sentinel before + # capture, then remove only that sentinel so a release sync never changes + # the target file's EOF shape as a side effect. + before="$(cat "$file"; printf '\036')" + before="${before%$'\036'}" local after - after="$(sed "$sed_expr" "$file")" + after="$(sed "$sed_expr" "$file"; printf '\036')" + after="${after%$'\036'}" if [[ "$before" == "$after" ]]; then echo " unchanged: $file" return fi if [[ "$MODE" == "apply" ]]; then printf '%s' "$after" > "$file" - # 原文件如果以换行结尾,保留 - if [[ "${before: -1}" == $'\n' && "${after: -1}" != $'\n' ]]; then - printf '\n' >> "$file" - fi echo " WROTE: $file" CHANGED_FILES+=("$file") else diff --git a/tests/test384-opencode-local-package-e2e/Dockerfile b/tests/test384-opencode-local-package-e2e/Dockerfile index 6d66f13d2..f71996af6 100644 --- a/tests/test384-opencode-local-package-e2e/Dockerfile +++ b/tests/test384-opencode-local-package-e2e/Dockerfile @@ -1,8 +1,8 @@ FROM node:22-bookworm-slim ARG OPENCODE_VERSION=1.18.1 -ARG AGENT_NETWORK_VERSION=2.3.0-preview.34 -ARG AGENT_NODE_VERSION=2.5.0-preview.28 +ARG AGENT_NETWORK_VERSION=2.3.0-preview.39 +ARG AGENT_NODE_VERSION=2.5.0-preview.31 RUN apt-get update && apt-get install -y --no-install-recommends \ bash ca-certificates curl jq procps python3 python3-pexpect ripgrep unzip \ @@ -49,6 +49,7 @@ RUN npm install -g --omit=optional "opencode-ai@${OPENCODE_VERSION}" --silent \ && timeout 20 opencode acp --help >/dev/null WORKDIR /test384 +COPY tests/lib/safe-rm.sh /lib/safe-rm.sh COPY tests/test384-opencode-local-package-e2e/run.sh /test384/run.sh COPY tests/test384-opencode-local-package-e2e/wizard_probe.py /test384/wizard_probe.py COPY tests/test384-opencode-local-package-e2e/auth_login_probe.py /test384/auth_login_probe.py diff --git a/tests/test384-opencode-local-package-e2e/run.sh b/tests/test384-opencode-local-package-e2e/run.sh index 21786f102..12da21abc 100644 --- a/tests/test384-opencode-local-package-e2e/run.sh +++ b/tests/test384-opencode-local-package-e2e/run.sh @@ -14,8 +14,8 @@ ADMIN_PASSWORD='Test384-Strong-Password!' LIVE_ALIAS=wizard-openai FREE_MODEL="${OPENCODE_FREE_MODEL:-opencode/deepseek-v4-flash-free}" EXPECTED_OPENCODE="${OPENCODE_VERSION_UNDER_TEST:-1.18.1}" -EXPECTED_NETWORK="${AGENT_NETWORK_VERSION_UNDER_TEST:-2.3.0-preview.34}" -EXPECTED_NODE="${AGENT_NODE_VERSION_UNDER_TEST:-2.5.0-preview.28}" +EXPECTED_NETWORK="${AGENT_NETWORK_VERSION_UNDER_TEST:-2.3.0-preview.39}" +EXPECTED_NODE="${AGENT_NODE_VERSION_UNDER_TEST:-2.5.0-preview.31}" REAL_PATH="$PATH" FAKE_BIN_DIR=/test384/fake-bin FAKE_CANONICAL_BIN=/test384/fake-global/node_modules/opencode-ai/bin/opencode.exe @@ -556,7 +556,7 @@ CURRENT_LAYER="L2 real PTY picker, both create entry points" echo echo "## L2 — real pexpect picker: unnamed/Anthropic + named/OpenAI" python3 /test384/wizard_probe.py -echo "PASS: both installed-bundle picker paths rendered the exact 6-choice canonical-main set/order, selected opencode-cli, and exited 0" +echo "PASS: both installed-bundle picker paths rendered the exact 7-choice canonical-main set/order, selected opencode-cli, and exited 0" CURRENT_LAYER="L3 preset materialization" echo diff --git a/tests/test384-opencode-local-package-e2e/wizard_probe.py b/tests/test384-opencode-local-package-e2e/wizard_probe.py index 069086cf0..c03e209b2 100644 --- a/tests/test384-opencode-local-package-e2e/wizard_probe.py +++ b/tests/test384-opencode-local-package-e2e/wizard_probe.py @@ -21,6 +21,7 @@ "codex-sdk", "codex-app-server", "grok-build-acp", + "grok-build-cli", "opencode-cli", ] PRESETS = ["Anthropic 原生 API", "OpenAI"] @@ -123,7 +124,7 @@ def main() -> None: finish(named, "wizard-openai") print( - "PEXPECT_PASS runtime_choices=6 exact_order=yes " + "PEXPECT_PASS runtime_choices=7 exact_order=yes " "unnamed=opencode-cli/anthropic named=opencode-cli/openai" ) diff --git a/tests/test386-opencode-agent-node-gate/bin/npx b/tests/test386-opencode-agent-node-gate/bin/npx index f41544d20..a9d16293b 100644 --- a/tests/test386-opencode-agent-node-gate/bin/npx +++ b/tests/test386-opencode-agent-node-gate/bin/npx @@ -4,7 +4,7 @@ set -eu printf '%s\n' "$*" > /tmp/test386-npx-args if [ "$#" -eq 3 ] \ && [ "$1" = "-y" ] \ - && [ "$2" = "@sleep2agi/agent-node@2.5.0-preview.28" ] \ + && [ "$2" = "@sleep2agi/agent-node@2.5.0-preview.31" ] \ && [ "$3" = "--print-entrypoint" ]; then printf '%s\n' '/test/exact-global/node_modules/@sleep2agi/agent-node/dist/cli.js' exit 0 diff --git a/tests/test386-opencode-agent-node-gate/exact-node/package.json b/tests/test386-opencode-agent-node-gate/exact-node/package.json index 19c878cbc..68694e7f2 100644 --- a/tests/test386-opencode-agent-node-gate/exact-node/package.json +++ b/tests/test386-opencode-agent-node-gate/exact-node/package.json @@ -1,6 +1,6 @@ { "name": "@sleep2agi/agent-node", - "version": "2.5.0-preview.28", + "version": "2.5.0-preview.31", "type": "module", "bin": { "agent-node": "dist/cli.js" diff --git a/tests/test386-opencode-agent-node-gate/project-agent-node/package.json b/tests/test386-opencode-agent-node-gate/project-agent-node/package.json index 19c878cbc..68694e7f2 100644 --- a/tests/test386-opencode-agent-node-gate/project-agent-node/package.json +++ b/tests/test386-opencode-agent-node-gate/project-agent-node/package.json @@ -1,6 +1,6 @@ { "name": "@sleep2agi/agent-node", - "version": "2.5.0-preview.28", + "version": "2.5.0-preview.31", "type": "module", "bin": { "agent-node": "dist/cli.js" diff --git a/tests/test386-opencode-agent-node-gate/run.sh b/tests/test386-opencode-agent-node-gate/run.sh index e5fc2b1f7..145622c4f 100644 --- a/tests/test386-opencode-agent-node-gate/run.sh +++ b/tests/test386-opencode-agent-node-gate/run.sh @@ -217,7 +217,7 @@ jq -e ' [ ! -e /tmp/test386-profile-coverage ] \ || fail "profile NODE_V8_COVERAGE wrote outside the node state boundary" [ ! -e /tmp/test386-npx-args ] || fail "exact global resolution unexpectedly executed npx" -grep -Fq 'using installed exact @sleep2agi/agent-node@2.5.0-preview.28' \ +grep -Fq 'using installed exact @sleep2agi/agent-node@2.5.0-preview.31' \ /tmp/test386-success.log || fail "exact installed agent-node diagnostic is missing" pass "stale global bypassed; later exact global received protected PATH/binary/version/base; npx was not executed" @@ -294,7 +294,7 @@ mask_log < /tmp/test386-project-explicit.log >> "$REPORT" || fail "explicit project-local rejection unexpectedly launched another agent-node" [ ! -e /tmp/test386-npx-args ] \ || fail "explicit project-local rejection unexpectedly executed npx" -grep -Fq 'ANET_AGENT_NODE_BIN is not the exact trusted @sleep2agi/agent-node@2.5.0-preview.28' \ +grep -Fq 'ANET_AGENT_NODE_BIN is not the exact trusted @sleep2agi/agent-node@2.5.0-preview.31' \ /tmp/test386-project-explicit.log \ || fail "explicit project-local rejection omitted the exact-pair diagnostic" grep -Fq 'project/node-local agent-node package payload is not trusted' \ @@ -336,7 +336,7 @@ jq -e '.executable == "/test/exact-global/node_modules/@sleep2agi/agent-node/dis /tmp/test386-exact-preview-launch.json >/dev/null \ || fail "capable-looking preview.21 was not bypassed for the later exact global" [ ! -e /tmp/test386-npx-args ] || fail "preview.21 bypass unexpectedly executed npx" -pass "capable-looking global preview.21 rejected; later exact global preview.26 launched without npx" +pass "capable-looking global preview.21 rejected; later exact global preview.31 launched without npx" # An explicit override is not permission to bypass the exact release pair. rm -rf /tmp/test386-work-explicit /tmp/test386-home-explicit \ @@ -362,7 +362,7 @@ mask_log < /tmp/test386-explicit.log >> "$REPORT" [ "$explicit_rc" -ne 0 ] || fail "stale explicit agent-node override unexpectedly started" [ ! -e /tmp/test386-stale-capable-global-was-launched ] \ || fail "stale explicit preview.21 was launched" -grep -Fq 'ANET_AGENT_NODE_BIN is not the exact trusted @sleep2agi/agent-node@2.5.0-preview.28' \ +grep -Fq 'ANET_AGENT_NODE_BIN is not the exact trusted @sleep2agi/agent-node@2.5.0-preview.31' \ /tmp/test386-explicit.log \ || fail "explicit override exact-version diagnostic is missing" pass "ANET_AGENT_NODE_BIN cannot bypass the exact hardened pair" @@ -395,7 +395,7 @@ mask_log < /tmp/test386-fail.log >> "$REPORT" grep -Fq 'automatic npx execution is disabled for opencode-cli' \ /tmp/test386-fail.log \ || fail "hard-fail omitted the disabled-npx diagnostic" -grep -Fq 'npm install -g @sleep2agi/agent-network@2.3.0-preview.34 @sleep2agi/agent-node@2.5.0-preview.28' \ +grep -Fq 'npm install -g @sleep2agi/agent-network@2.3.0-preview.39 @sleep2agi/agent-node@2.5.0-preview.31' \ /tmp/test386-fail.log \ || fail "hard-fail omitted the exact dual-package install command" grep -Fq 'Refusing to start: an unsupported agent-node could silently select another runtime.' \