Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions agent-network/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6956,6 +6956,14 @@ anet node rename <node-id|node-name> <new-node-name> [--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
Expand Down
4 changes: 2 additions & 2 deletions agent-network/src/opencode-agent-node-pair.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;

Expand Down
5 changes: 4 additions & 1 deletion docs/RELEASE-SOP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<pkg>@preview version
```

Expand Down
120 changes: 120 additions & 0 deletions docs/tests/report-test745-opencode-pair-sync.txt
Original file line number Diff line number Diff line change
@@ -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.
22 changes: 13 additions & 9 deletions scripts/sync-pinned-versions.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep stable rollbacks out of the preview pair pins

Registering this preview-only constant in the generic package release synchronizer means the documented rollback command in docs/RELEASE-SOP.md §5 (sync-pinned-versions.sh ... <old-stable> --apply) rewrites the OpenCode gate to an old stable agent-network version, even though Step 10 requires these constants to match the preview tags. The same applies to the agent-node registration below; rolling agent-node back to a pre-OpenCode stable such as 2.4.x can therefore make a subsequent CLI build trust that unsupported runtime instead of the vetted preview pair. Restrict these registrations to preview releases or give the exact-pair pins a separate synchronized workflow.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.


# @sleep2agi/agent-node — runtime + SDK 行号锚点
register "@sleep2agi/agent-node" "docs-site/docs/guide/runtimes.md"
Expand All @@ -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"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fail when a registered constant is not replaced

The new registration relies on apply_or_preview, but that helper reports unchanged and exits successfully whenever the file exists and the generated sed expression matches nothing. If this constant is renamed, reformatted, or removed during a future release, the sync command will therefore claim success while leaving the exact agent-node pin stale; the documented release build step does not run the pairing unit test, so this can reach publication unnoticed. Require every constant target to match exactly once and fail otherwise.

AGENTS.md reference: AGENTS.md:L25-L25

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Let the mandatory dry-run succeed when it finds a change

Adding this target makes every real agent-node version bump reach the diff -u ... | sed ... | head pipeline in apply_or_preview; because the script enables set -o pipefail, the expected diff status 1 propagates and terminates the dry-run before its completion message. I reproduced this with sync-pinned-versions.sh @sleep2agi/agent-node 2.5.0-preview.99, which printed the intended pin diff but exited 1. Since docs/RELEASE-SOP.md requires a successful dry-run before --apply, mask only diff's expected status 1 while preserving genuine errors.

AGENTS.md reference: AGENTS.md:L24-L25

Useful? React with 👍 / 👎.


# @sleep2agi/commhub-server — agent-network CLI 内 PINNED_SERVER_VERSION 常量
register "@sleep2agi/commhub-server" "agent-network/bin/cli.ts:PINNED_SERVER_VERSION"
Expand Down Expand Up @@ -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 <NAME> = "x.y.z..."`,保留引号 + 行内其它内容
printf 's#\\(const %s = \\)"[^"]*"#\\1"%s"#g' \
# 严格锚定可选 export + `const <NAME> = "x.y.z..."`,保留其它内容。
printf 's#\\(\\(export \\)\\?const %s = \\)"[^"]*"#\\1"%s"#g' \
"$const_name" "$ESCAPED_VERSION"
}

Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions tests/test384-opencode-local-package-e2e/Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions tests/test384-opencode-local-package-e2e/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion tests/test384-opencode-local-package-e2e/wizard_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"codex-sdk",
"codex-app-server",
"grok-build-acp",
"grok-build-cli",
"opencode-cli",
]
PRESETS = ["Anthropic 原生 API", "OpenAI"]
Expand Down Expand Up @@ -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"
)

Expand Down
2 changes: 1 addition & 1 deletion tests/test386-opencode-agent-node-gate/bin/npx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
10 changes: 5 additions & 5 deletions tests/test386-opencode-agent-node-gate/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down Expand Up @@ -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' \
Expand Down Expand Up @@ -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 \
Expand All @@ -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"
Expand Down Expand Up @@ -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.' \
Expand Down
Loading