pantheon-opencode 1.6.0-beta.7 → 1.6.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +58 -15
- package/CHANGELOG.md +61 -94
- package/README.md +81 -103
- package/README.pt-BR.md +19 -37
- package/commands/pantheon-audit.md +7 -1
- package/commands/pantheon-consolidate.md +1 -1
- package/commands/pantheon-focus.md +17 -0
- package/package.json +1 -2
- package/plugin.json +2 -2
- package/pyproject.toml +1 -1
- package/scripts/build-agents-md.mjs +2 -3
- package/scripts/doctor.mjs +22 -104
- package/scripts/hooks/scan-secrets.sh +97 -0
- package/scripts/install/config-migration.mjs +9 -42
- package/scripts/install/interactive.mjs +1 -1
- package/scripts/install/opencode.mjs +174 -77
- package/scripts/install/plugin.mjs +43 -9
- package/scripts/install/strings.mjs +0 -8
- package/scripts/probe-pantheon-cost.mjs +3 -16
- package/scripts/sync-artifacts.mjs +1 -0
- package/scripts/sync-tui.mjs +24 -3
- package/scripts/test-opencode-v2-sandbox.sh +203 -786
- package/scripts/validate-sandbox-install.mjs +138 -0
- package/skills-lock.json +4 -5
- package/src/agents/aphrodite.md +2 -9
- package/src/agents/demeter.md +0 -9
- package/src/agents/gaia.md +1 -0
- package/src/agents/hephaestus.md +0 -9
- package/src/agents/hermes.md +2 -9
- package/src/agents/mnemosyne.md +18 -53
- package/src/agents/prometheus.md +0 -9
- package/src/agents/themis.md +0 -1
- package/src/agents/zeus.md +180 -19
- package/src/instructions/agent-return-format.instructions.md +48 -3
- package/src/instructions/memory-protocol.instructions.md +8 -9
- package/src/instructions/zeus-anti-stall.instructions.md +54 -6
- package/src/instructions/zeus-council-synthesis.instructions.md +71 -0
- package/src/instructions/zeus-memory-operations.instructions.md +4 -4
- package/src/instructions/zeus-timeout-retry.instructions.md +81 -7
- package/src/pantheon/v2-delegation-enforce.ts +2 -1
- package/src/plugin-v2.ts +251 -168
- package/src/plugins/pantheon-hooks.ts +35 -82
- package/src/plugins/tui/dist/tui-v2.d.ts +9 -0
- package/src/plugins/tui/dist/tui-v2.d.ts.map +1 -0
- package/src/plugins/tui/dist/tui-v2.js +151 -0
- package/src/plugins/tui/dist/tui-v2.js.map +1 -0
- package/src/plugins/tui/dist/tui.d.ts.map +1 -1
- package/src/plugins/tui/dist/tui.js +29 -53
- package/src/plugins/tui/index-v2.ts +6 -0
- package/src/plugins/tui/package-lock.json +5411 -1292
- package/src/plugins/tui/package.json +10 -7
- package/src/plugins/tui/src/index-v2.tsx +108 -0
- package/src/plugins/tui/src/index.tsx +28 -63
- package/src/plugins/tui/src/opencode-v2.d.ts +13 -0
- package/src/plugins/tui/tsdown.config.ts +2 -2
- package/src/plugins/tui/tui-v2.ts +2 -0
- package/src/plugins/tui/tui.ts +3 -0
- package/src/routing.yml +4 -2
- package/src/skills/README.md +7 -12
- package/src/skills/agent-coordination/SKILL.md +83 -13
- package/src/skills/auto-continue/SKILL.md +13 -14
- package/src/skills/context-compression/SKILL.md +26 -879
- package/src/skills/session-goal/SKILL.md +14 -5
- package/src/pantheon/secret-scanner.ts +0 -489
- package/src/plugins/tui/README.md +0 -14
- package/src/skills/council-synthesis/SKILL.md +0 -29
|
@@ -34,21 +34,15 @@
|
|
|
34
34
|
# Env overrides:
|
|
35
35
|
# PANTHEON_SANDBOX_ROOT Sandbox root (default: ~/pantheon-sandbox)
|
|
36
36
|
# OPENCODE_V2_SPEC npm spec providing the V2 binary
|
|
37
|
-
# (default: @opencode
|
|
37
|
+
# (default: @opencode/cli@latest). NOTE: this is an
|
|
38
38
|
# install spec only — it is not a dependency of this
|
|
39
39
|
# package, and no code imports it.
|
|
40
40
|
# PANTHEON_REPO Repository the sandbox is built from (default: the
|
|
41
|
-
# checkout this script lives in).
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# it never stops or reuses an existing service.
|
|
47
|
-
# PANTHEON_V2_HOST IPv4 loopback IP for the V2 service (default:
|
|
48
|
-
# 127.0.0.1); non-loopback addresses are rejected.
|
|
49
|
-
# PANTHEON_V2_MCP_LIST_TIMEOUT
|
|
50
|
-
# Per-call `opencode mcp list` timeout in seconds
|
|
51
|
-
# (default: 15).
|
|
41
|
+
# checkout this script lives in). Only read by the
|
|
42
|
+
# GENERATED run-test.sh, which cannot derive it from
|
|
43
|
+
# its own location and reads the .repo-dir file this
|
|
44
|
+
# script generates beside it. --reset refuses a
|
|
45
|
+
# sandbox root that overlaps either protected repo.
|
|
52
46
|
# PANTHEON_SANDBOX_MODEL Model used by init/prompts
|
|
53
47
|
# (default: opencode-go/mimo-v2.5)
|
|
54
48
|
# PANTHEON_PROMPT_TIMEOUT Per-prompt timeout in seconds (default: 300)
|
|
@@ -57,20 +51,19 @@
|
|
|
57
51
|
# 2 = usage error, 3 = sandbox not prepared.
|
|
58
52
|
set -euo pipefail
|
|
59
53
|
|
|
60
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd
|
|
54
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
61
55
|
# Derived from this script's own location so the sandbox is always built from
|
|
62
56
|
# the checkout that CONTAINS this harness. It must never be inferred from a
|
|
63
57
|
# sibling directory: a sibling may be a different checkout (or a different
|
|
64
|
-
# branch) with unrelated uncommitted work
|
|
65
|
-
#
|
|
66
|
-
|
|
67
|
-
REPO_DIR="$SCRIPT_REPO_DIR"
|
|
58
|
+
# branch) with unrelated uncommitted work, and --prepare runs `npm pack` inside
|
|
59
|
+
# it, writing a tarball into that tree.
|
|
60
|
+
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
68
61
|
|
|
69
62
|
SANDBOX_ROOT="${PANTHEON_SANDBOX_ROOT:-$HOME/pantheon-sandbox}"
|
|
70
63
|
SANDBOX_HOME="$SANDBOX_ROOT/home"
|
|
71
64
|
NPM_PREFIX="$SANDBOX_HOME/.npm-global"
|
|
72
65
|
|
|
73
|
-
OPENCODE_V2_SPEC="${OPENCODE_V2_SPEC:-@opencode
|
|
66
|
+
OPENCODE_V2_SPEC="${OPENCODE_V2_SPEC:-@opencode/cli@latest}"
|
|
74
67
|
PANTHEON_SANDBOX_MODEL="${PANTHEON_SANDBOX_MODEL:-opencode-go/mimo-v2.5}"
|
|
75
68
|
PANTHEON_PROMPT_TIMEOUT="${PANTHEON_PROMPT_TIMEOUT:-300}"
|
|
76
69
|
|
|
@@ -79,11 +72,8 @@ V2_BIN="${OPENCODE_V2_BIN:-opencode2}"
|
|
|
79
72
|
# OpenCode V2's CLI talks to its background service for `mcp list`. Keep the
|
|
80
73
|
# probe hermetic: an inherited config, database, or PORT can otherwise make
|
|
81
74
|
# the command query a different service and report a false result.
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
V2_CONFIG="$SANDBOX_ROOT/project-v2/opencode.json"
|
|
85
|
-
V2_DB="$SANDBOX_ROOT/opencode-v2.db"
|
|
86
|
-
unset PANTHEON_V2_CONFIG PANTHEON_V2_DB
|
|
75
|
+
V2_CONFIG="${PANTHEON_V2_CONFIG:-$SANDBOX_ROOT/project-v2/opencode.json}"
|
|
76
|
+
V2_DB="${PANTHEON_V2_DB:-$SANDBOX_ROOT/opencode-v2.db}"
|
|
87
77
|
V2_PORT="${PANTHEON_V2_PORT:-49376}"
|
|
88
78
|
V2_LOOPBACK_HOST="${PANTHEON_V2_HOST:-127.0.0.1}"
|
|
89
79
|
V2_HANDSHAKE_TIMEOUT="${PANTHEON_V2_HANDSHAKE_TIMEOUT:-60}"
|
|
@@ -108,7 +98,6 @@ MODE_COST=0
|
|
|
108
98
|
MODE_REHYDRATE=0
|
|
109
99
|
MODE_HOOKS=0
|
|
110
100
|
RUN_VERSION=""
|
|
111
|
-
SANDBOX_ENV_READY=0
|
|
112
101
|
|
|
113
102
|
usage() {
|
|
114
103
|
sed -n '2,45p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'
|
|
@@ -119,60 +108,8 @@ log() { printf '%s\n' "$*"; }
|
|
|
119
108
|
warn() { printf 'WARN: %s\n' "$*" >&2; }
|
|
120
109
|
die() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }
|
|
121
110
|
|
|
122
|
-
resolve_harness_repo() {
|
|
123
|
-
local requested_repo="${PANTHEON_REPO:-$SCRIPT_REPO_DIR}"
|
|
124
|
-
[ -n "$requested_repo" ] \
|
|
125
|
-
|| die "PANTHEON_REPO must not be empty"
|
|
126
|
-
case "$requested_repo" in
|
|
127
|
-
/*) ;;
|
|
128
|
-
*) die "PANTHEON_REPO must be an absolute path (got: $requested_repo)" ;;
|
|
129
|
-
esac
|
|
130
|
-
[ -d "$requested_repo" ] \
|
|
131
|
-
|| die "PANTHEON_REPO path does not exist or is not a directory: $requested_repo"
|
|
132
|
-
REPO_DIR="$(cd -P -- "$requested_repo" 2>/dev/null && pwd -P)" \
|
|
133
|
-
|| die "cannot resolve PANTHEON_REPO path: $requested_repo"
|
|
134
|
-
[ -f "$REPO_DIR/package.json" ] \
|
|
135
|
-
|| die "PANTHEON_REPO is not a Pantheon package checkout (package.json missing): $REPO_DIR"
|
|
136
|
-
node -e 'if (require(process.argv[1]).name !== "pantheon-opencode") process.exit(1)' \
|
|
137
|
-
"$REPO_DIR/package.json" \
|
|
138
|
-
|| die "PANTHEON_REPO package name must be pantheon-opencode: $REPO_DIR"
|
|
139
|
-
}
|
|
140
|
-
|
|
141
111
|
project_dir() { printf '%s/project-%s' "$SANDBOX_ROOT" "$TARGET_VERSION"; }
|
|
142
112
|
|
|
143
|
-
validate_v2_port() {
|
|
144
|
-
local port_number
|
|
145
|
-
[[ "$V2_PORT" =~ ^[0-9]{1,5}$ ]] \
|
|
146
|
-
|| die "PANTHEON_V2_PORT must be an integer from 1 through 65535 (got '$V2_PORT')"
|
|
147
|
-
port_number=$((10#$V2_PORT))
|
|
148
|
-
[ "$port_number" -ge 1 ] && [ "$port_number" -le 65535 ] \
|
|
149
|
-
|| die "PANTHEON_V2_PORT must be an integer from 1 through 65535 (got '$V2_PORT')"
|
|
150
|
-
V2_PORT="$port_number"
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
assert_v2_port_available() {
|
|
154
|
-
validate_v2_port
|
|
155
|
-
if ! python3 - "$V2_LOOPBACK_HOST" "$V2_PORT" <<'PY'
|
|
156
|
-
import ipaddress
|
|
157
|
-
import socket
|
|
158
|
-
import sys
|
|
159
|
-
|
|
160
|
-
host, raw_port = sys.argv[1], sys.argv[2]
|
|
161
|
-
try:
|
|
162
|
-
address = ipaddress.ip_address(host)
|
|
163
|
-
if not address.is_loopback or address.version != 4:
|
|
164
|
-
raise ValueError("host must be an IPv4 loopback IP address")
|
|
165
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
166
|
-
probe.bind((host, int(raw_port)))
|
|
167
|
-
except (OSError, ValueError) as exc:
|
|
168
|
-
print(f"cannot bind dedicated V2 port {host}:{raw_port}: {exc}", file=sys.stderr)
|
|
169
|
-
raise SystemExit(1)
|
|
170
|
-
PY
|
|
171
|
-
then
|
|
172
|
-
die "dedicated V2 port $V2_PORT is already in use or unavailable; refusing to reuse or stop an unrelated service"
|
|
173
|
-
fi
|
|
174
|
-
}
|
|
175
|
-
|
|
176
113
|
# Resolve a binary STRICTLY inside the sandbox npm prefix — never fall back to
|
|
177
114
|
# the dev PATH, otherwise a non-prepared sandbox would silently test the host
|
|
178
115
|
# installation.
|
|
@@ -192,45 +129,10 @@ sandbox_bin() { # echoes the V2 binary path; rc 1 if missing
|
|
|
192
129
|
# ── Sandbox environment isolation ─────────────────────────────────────────────
|
|
193
130
|
|
|
194
131
|
sandbox_env() {
|
|
195
|
-
# Combined modes can call this more than once in one shell. After the first
|
|
196
|
-
# setup, HOME intentionally lives below SANDBOX_ROOT; re-running the full
|
|
197
|
-
# overlap guard would mistake that supported isolation layout for an unsafe
|
|
198
|
-
# ancestor relationship.
|
|
199
|
-
if [ "$SANDBOX_ENV_READY" -eq 1 ]; then
|
|
200
|
-
return 0
|
|
201
|
-
fi
|
|
202
|
-
|
|
203
|
-
guard_sandbox_root use
|
|
204
|
-
canonicalize_sandbox_root
|
|
205
|
-
validate_v2_port
|
|
206
|
-
mkdir -p -- "$SANDBOX_ROOT"
|
|
207
|
-
sandbox_path_guard "$SANDBOX_ROOT" dir-check
|
|
208
|
-
sandbox_path_guard "$SANDBOX_HOME" dir-create
|
|
209
|
-
sandbox_path_guard "$NPM_PREFIX" dir-create
|
|
210
|
-
sandbox_path_guard "$SANDBOX_HOME/.npm-cache" dir-create
|
|
211
|
-
sandbox_path_guard "$SANDBOX_HOME/.config" dir-create
|
|
212
|
-
sandbox_path_guard "$SANDBOX_HOME/.config/opencode" dir-create
|
|
213
|
-
sandbox_path_guard "$SANDBOX_HOME/.config/xdg" dir-create
|
|
214
|
-
sandbox_path_guard "$SANDBOX_HOME/.local/share" dir-create
|
|
215
|
-
sandbox_path_guard "$SANDBOX_HOME/.local/share/opencode" dir-create
|
|
216
|
-
sandbox_path_guard "$SANDBOX_HOME/.local/share/xdg" dir-create
|
|
217
|
-
sandbox_path_guard "$SANDBOX_HOME/.local/state" dir-create
|
|
218
|
-
sandbox_path_guard "$SANDBOX_HOME/.local/state/opencode" dir-create
|
|
219
|
-
sandbox_path_guard "$SANDBOX_HOME/.cache" dir-create
|
|
220
|
-
sandbox_path_guard "$SANDBOX_HOME/.cache/opencode" dir-create
|
|
221
|
-
sandbox_path_guard "$SANDBOX_HOME/.opencode" dir-create
|
|
222
|
-
sandbox_path_guard "$V2_CONFIG" file-allow-missing-parent
|
|
223
|
-
sandbox_path_guard "$V2_DB" file-allow-missing-parent
|
|
224
132
|
export HOME="$SANDBOX_HOME"
|
|
225
133
|
export PATH="$NPM_PREFIX/bin:$HOME/.config/opencode/.venv/bin:$PATH"
|
|
226
134
|
export npm_config_prefix="$NPM_PREFIX"
|
|
227
|
-
export npm_config_cache="$SANDBOX_HOME/.npm-cache"
|
|
228
|
-
export XDG_CONFIG_HOME="$SANDBOX_HOME/.config"
|
|
229
|
-
export XDG_CONFIG_DIRS="$SANDBOX_HOME/.config/xdg"
|
|
230
|
-
export XDG_DATA_HOME="$SANDBOX_HOME/.local/share"
|
|
231
|
-
export XDG_DATA_DIRS="$SANDBOX_HOME/.local/share/xdg"
|
|
232
135
|
export XDG_STATE_HOME="$SANDBOX_HOME/.local/state"
|
|
233
|
-
export XDG_CACHE_HOME="$SANDBOX_HOME/.cache"
|
|
234
136
|
# Do not inherit an OpenChamber/developer-machine config selector. The V2
|
|
235
137
|
# project config is created by prepare_project and is the only config this
|
|
236
138
|
# gate is allowed to inspect.
|
|
@@ -238,11 +140,8 @@ sandbox_env() {
|
|
|
238
140
|
export OPENCODE_DB="$V2_DB"
|
|
239
141
|
export PANTHEON_V2_PORT="$V2_PORT"
|
|
240
142
|
export PORT="$V2_PORT"
|
|
241
|
-
unset OPENCODE_CONFIG OPENCODE_CONFIG_CONTENT OPENCODE_CONFIG_PROJECT_DISABLE
|
|
242
|
-
|
|
243
|
-
sandbox_path_guard "$V2_SERVICE_STATE" file-allow-missing-parent
|
|
244
|
-
sandbox_path_guard "$(dirname "$V2_SERVICE_STATE")" dir-create
|
|
245
|
-
SANDBOX_ENV_READY=1
|
|
143
|
+
unset OPENCODE_CONFIG OPENCODE_CONFIG_CONTENT OPENCODE_CONFIG_PROJECT_DISABLE
|
|
144
|
+
mkdir -p "$SANDBOX_ROOT" "$(dirname "$V2_DB")" "$(dirname "$V2_SERVICE_STATE")"
|
|
246
145
|
}
|
|
247
146
|
|
|
248
147
|
stop_v2_service() {
|
|
@@ -431,7 +330,6 @@ start_v2_service() { # binary project
|
|
|
431
330
|
if [ -n "$V2_SERVER_PID" ] && kill -0 "$V2_SERVER_PID" 2>/dev/null; then
|
|
432
331
|
return 0
|
|
433
332
|
fi
|
|
434
|
-
validate_v2_port
|
|
435
333
|
|
|
436
334
|
# Never attach to an unrelated service on the dedicated port. A successful
|
|
437
335
|
# health probe before our process starts is an environmental collision, not
|
|
@@ -440,7 +338,6 @@ start_v2_service() { # binary project
|
|
|
440
338
|
"http://$V2_LOOPBACK_HOST:${V2_PORT}/global/health" >/dev/null 2>&1; then
|
|
441
339
|
die "dedicated V2 port ${V2_PORT} is already in use; refusing to reuse an unrelated service"
|
|
442
340
|
fi
|
|
443
|
-
assert_v2_port_available
|
|
444
341
|
|
|
445
342
|
rm -f "$V2_SERVER_LOG" "$V2_SERVICE_STATE" "$HOME/.local/share/opencode/log/opencode.log"
|
|
446
343
|
(cd "$project" && "$bin" serve --hostname "$V2_LOOPBACK_HOST" --port "$V2_PORT" --service) \
|
|
@@ -475,67 +372,49 @@ trap stop_v2_service EXIT
|
|
|
475
372
|
|
|
476
373
|
# ── Gate (b): sandbox run-test.sh with pantheon://agents content validation ──
|
|
477
374
|
|
|
478
|
-
# Node's compile cache is disposable, but tmp/ can also contain runtime state
|
|
479
|
-
# and human handoffs. Expire only hash-named cache files owned by this user after
|
|
480
|
-
# 30 days; never recurse through links or prune any other tmp/ entry.
|
|
481
|
-
cleanup_sandbox_tmp() {
|
|
482
|
-
local cache_dir="$SANDBOX_ROOT/tmp/node-compile-cache" cache_file cache_name owner
|
|
483
|
-
[ -d "$SANDBOX_ROOT/tmp" ] && [ ! -L "$SANDBOX_ROOT/tmp" ] || return 0
|
|
484
|
-
[ -d "$cache_dir" ] && [ ! -L "$cache_dir" ] || return 0
|
|
485
|
-
owner="$(id -un)" || return 0
|
|
486
|
-
while IFS= read -r -d '' cache_file; do
|
|
487
|
-
cache_name="${cache_file##*/}"
|
|
488
|
-
if [[ "$cache_name" =~ ^[[:xdigit:]]{8}$ ]]; then
|
|
489
|
-
rm -f -- "$cache_file"
|
|
490
|
-
fi
|
|
491
|
-
done < <(find "$cache_dir" -xdev -type f -user "$owner" -mtime +30 -print0)
|
|
492
|
-
}
|
|
493
|
-
|
|
494
375
|
write_run_test_sh() {
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
#
|
|
501
|
-
#
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|| die "cannot create temporary .repo-dir"
|
|
505
|
-
if ! printf '%s\n' "$REPO_DIR" > "$repo_file_tmp"; then
|
|
506
|
-
rm -f -- "$repo_file_tmp"
|
|
507
|
-
die "cannot write temporary .repo-dir"
|
|
508
|
-
fi
|
|
509
|
-
atomic_replace_file "$repo_file_tmp" "$SANDBOX_ROOT/.repo-dir" 0600 \
|
|
510
|
-
|| die "cannot atomically replace .repo-dir"
|
|
511
|
-
run_test_tmp="$(mktemp "$SANDBOX_ROOT/.run-test.sh.XXXXXX")" \
|
|
512
|
-
|| die "cannot create temporary run-test.sh"
|
|
513
|
-
if ! cat > "$run_test_tmp" <<'RUNTEST'
|
|
376
|
+
# Quoted heredoc: the body is emitted verbatim, so nothing inside is expanded
|
|
377
|
+
# at generation time. REPO_DIR is the one value the body needs, and it is
|
|
378
|
+
# handed over as its own generated file instead of a placeholder baked into
|
|
379
|
+
# the shell source: interpolating it meant escaping for TWO languages (sed
|
|
380
|
+
# replacement text AND the double-quoted shell context it lands in), and only
|
|
381
|
+
# the first set was escaped — a path containing `$`, a backtick, a backslash
|
|
382
|
+
# or a quote was silently rewritten or executed by the generated script.
|
|
383
|
+
printf '%s\n' "$REPO_DIR" > "$SANDBOX_ROOT/.repo-dir"
|
|
384
|
+
cat > "$SANDBOX_ROOT/run-test.sh" <<'RUNTEST'
|
|
514
385
|
#!/usr/bin/env bash
|
|
515
386
|
# Generated by pantheon-opencode scripts/test-opencode-v2-sandbox.sh --prepare
|
|
516
387
|
# Validates the global install: the V2 binary, exactly 5 connected MCPs, doctor
|
|
517
388
|
# 0 errors, AND the CONTENT of pantheon://agents (zeus/hermes present).
|
|
518
389
|
set -euo pipefail
|
|
519
390
|
|
|
520
|
-
SANDBOX_DIR="$(cd "$(dirname "$0")" && pwd
|
|
391
|
+
SANDBOX_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
521
392
|
SANDBOX_HOME="$SANDBOX_DIR/home"
|
|
522
393
|
NPM_PREFIX="$SANDBOX_HOME/.npm-global"
|
|
523
394
|
PANTHEON_GLOBAL="$SANDBOX_HOME/.config/opencode"
|
|
524
395
|
SANDBOX_VENV="$PANTHEON_GLOBAL/.venv"
|
|
396
|
+
# The checkout this harness ran from, written by the generator into the
|
|
397
|
+
# .repo-dir file next to this script. It used to be a placeholder baked into
|
|
398
|
+
# this line and substituted with sed afterwards, but the sandbox lives outside
|
|
399
|
+
# any repo so the value cannot be derived from this file's own location either.
|
|
400
|
+
# Reading a file the generator wrote keeps the path out of the shell source, so
|
|
401
|
+
# no escaping can be wrong. PANTHEON_REPO may still override it; the generator's
|
|
402
|
+
# --reset guard refuses to run when that points inside a repo, so an override
|
|
403
|
+
# can never turn this into a repo-wiping target.
|
|
404
|
+
REPO_DIR="${PANTHEON_REPO:-$(cat "$SANDBOX_DIR/.repo-dir")}"
|
|
405
|
+
|
|
406
|
+
echo "=== Pantheon Sandbox Test (OpenCode V2) ==="
|
|
407
|
+
echo "Sandbox: $SANDBOX_DIR"
|
|
408
|
+
echo "Repo: $REPO_DIR"
|
|
409
|
+
|
|
525
410
|
export HOME="$SANDBOX_HOME"
|
|
526
411
|
export PATH="$NPM_PREFIX/bin:$SANDBOX_VENV/bin:$PATH"
|
|
527
412
|
export npm_config_prefix="$NPM_PREFIX"
|
|
528
|
-
export npm_config_cache="$SANDBOX_HOME/.npm-cache"
|
|
529
|
-
export XDG_CONFIG_HOME="$SANDBOX_HOME/.config"
|
|
530
|
-
export XDG_CONFIG_DIRS="$SANDBOX_HOME/.config/xdg"
|
|
531
|
-
export XDG_DATA_HOME="$SANDBOX_HOME/.local/share"
|
|
532
|
-
export XDG_DATA_DIRS="$SANDBOX_HOME/.local/share/xdg"
|
|
533
413
|
export XDG_STATE_HOME="$SANDBOX_HOME/.local/state"
|
|
534
|
-
export XDG_CACHE_HOME="$SANDBOX_HOME/.cache"
|
|
535
414
|
# The V2 CLI resolves `mcp list` through its service. Keep generated sandbox
|
|
536
415
|
# runs isolated from any config/database/port inherited from the host.
|
|
537
416
|
V2_CONFIG="$SANDBOX_DIR/project-v2/opencode.json"
|
|
538
|
-
V2_DB="$SANDBOX_DIR/opencode-v2.db"
|
|
417
|
+
V2_DB="${PANTHEON_V2_DB:-$SANDBOX_DIR/opencode-v2.db}"
|
|
539
418
|
V2_PORT="${PANTHEON_V2_PORT:-49376}"
|
|
540
419
|
V2_LOOPBACK_HOST="${PANTHEON_V2_HOST:-127.0.0.1}"
|
|
541
420
|
V2_HANDSHAKE_TIMEOUT="${PANTHEON_V2_HANDSHAKE_TIMEOUT:-60}"
|
|
@@ -543,153 +422,12 @@ V2_MCP_LIST_TIMEOUT="${PANTHEON_V2_MCP_LIST_TIMEOUT:-15}"
|
|
|
543
422
|
V2_SERVER_LOG="$SANDBOX_DIR/v2-server.log"
|
|
544
423
|
V2_SERVICE_STATE="$SANDBOX_HOME/.local/state/opencode/service.json"
|
|
545
424
|
V2_SERVER_PID=""
|
|
546
|
-
|
|
547
|
-
resolve_sandbox_repo() {
|
|
548
|
-
local requested_repo resolved_repo repo_file
|
|
549
|
-
if [ -n "${PANTHEON_REPO:-}" ]; then
|
|
550
|
-
requested_repo="$PANTHEON_REPO"
|
|
551
|
-
else
|
|
552
|
-
repo_file="$SANDBOX_DIR/.repo-dir"
|
|
553
|
-
[ -f "$repo_file" ] && [ ! -L "$repo_file" ] \
|
|
554
|
-
|| { printf 'ERROR: sandbox .repo-dir is missing or not a regular file: %s\n' "$repo_file" >&2; return 1; }
|
|
555
|
-
IFS= read -r requested_repo < "$repo_file" \
|
|
556
|
-
|| { printf 'ERROR: sandbox .repo-dir is empty or unreadable: %s\n' "$repo_file" >&2; return 1; }
|
|
557
|
-
fi
|
|
558
|
-
[ -n "$requested_repo" ] \
|
|
559
|
-
|| { printf 'ERROR: repository path is empty (PANTHEON_REPO/.repo-dir)\n' >&2; return 1; }
|
|
560
|
-
case "$requested_repo" in
|
|
561
|
-
/*) ;;
|
|
562
|
-
*) printf 'ERROR: repository path must be absolute: %s\n' "$requested_repo" >&2; return 1 ;;
|
|
563
|
-
esac
|
|
564
|
-
[ -d "$requested_repo" ] \
|
|
565
|
-
|| { printf 'ERROR: repository path does not exist or is not a directory: %s\n' "$requested_repo" >&2; return 1; }
|
|
566
|
-
resolved_repo="$(cd -P -- "$requested_repo" 2>/dev/null && pwd -P)" \
|
|
567
|
-
|| { printf 'ERROR: cannot resolve repository path: %s\n' "$requested_repo" >&2; return 1; }
|
|
568
|
-
[ -f "$resolved_repo/package.json" ] \
|
|
569
|
-
|| { printf 'ERROR: repository package.json is missing: %s\n' "$resolved_repo" >&2; return 1; }
|
|
570
|
-
node -e 'if (require(process.argv[1]).name !== "pantheon-opencode") process.exit(1)' \
|
|
571
|
-
"$resolved_repo/package.json" \
|
|
572
|
-
|| { printf 'ERROR: repository package name must be pantheon-opencode: %s\n' "$resolved_repo" >&2; return 1; }
|
|
573
|
-
printf '%s\n' "$resolved_repo"
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
path_is_same_or_descendant() {
|
|
577
|
-
local candidate="$1" parent="$2"
|
|
578
|
-
[ "$candidate" = "$parent" ] && return 0
|
|
579
|
-
[ "$parent" = "/" ] && [[ "$candidate" = /* ]] && return 0
|
|
580
|
-
case "$candidate" in "$parent"/*) return 0 ;; esac
|
|
581
|
-
return 1
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
validate_v2_port() {
|
|
585
|
-
local port_number
|
|
586
|
-
[[ "$V2_PORT" =~ ^[0-9]{1,5}$ ]] \
|
|
587
|
-
|| { printf "ERROR: PANTHEON_V2_PORT must be an integer from 1 through 65535 (got '%s')\n" "$V2_PORT" >&2; return 1; }
|
|
588
|
-
port_number=$((10#$V2_PORT))
|
|
589
|
-
[ "$port_number" -ge 1 ] && [ "$port_number" -le 65535 ] \
|
|
590
|
-
|| { printf "ERROR: PANTHEON_V2_PORT must be an integer from 1 through 65535 (got '%s')\n" "$V2_PORT" >&2; return 1; }
|
|
591
|
-
V2_PORT="$port_number"
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
assert_v2_port_available() {
|
|
595
|
-
validate_v2_port || return 1
|
|
596
|
-
python3 - "$V2_LOOPBACK_HOST" "$V2_PORT" <<'PY'
|
|
597
|
-
import ipaddress
|
|
598
|
-
import socket
|
|
599
|
-
import sys
|
|
600
|
-
|
|
601
|
-
host, raw_port = sys.argv[1], sys.argv[2]
|
|
602
|
-
try:
|
|
603
|
-
address = ipaddress.ip_address(host)
|
|
604
|
-
if not address.is_loopback or address.version != 4:
|
|
605
|
-
raise ValueError("host must be an IPv4 loopback IP address")
|
|
606
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
607
|
-
probe.bind((host, int(raw_port)))
|
|
608
|
-
except (OSError, ValueError) as exc:
|
|
609
|
-
print(f"ERROR: dedicated V2 port {host}:{raw_port} is already in use or unavailable; refusing to reuse or stop an unrelated service: {exc}", file=sys.stderr)
|
|
610
|
-
raise SystemExit(1)
|
|
611
|
-
PY
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
REPO_DIR="$(resolve_sandbox_repo)" || exit 1
|
|
615
|
-
if path_is_same_or_descendant "$SANDBOX_DIR" "$REPO_DIR" \
|
|
616
|
-
|| path_is_same_or_descendant "$REPO_DIR" "$SANDBOX_DIR"; then
|
|
617
|
-
printf 'ERROR: sandbox path overlaps the selected repository; refusing to package: sandbox=%s repo=%s\n' \
|
|
618
|
-
"$SANDBOX_DIR" "$REPO_DIR" >&2
|
|
619
|
-
exit 1
|
|
620
|
-
fi
|
|
621
|
-
validate_v2_port || exit 1
|
|
622
|
-
echo "=== Pantheon Sandbox Test (OpenCode V2) ==="
|
|
623
|
-
echo "Sandbox: $SANDBOX_DIR"
|
|
624
|
-
echo "Repo: $REPO_DIR"
|
|
625
|
-
|
|
626
425
|
export OPENCODE_CONFIG_DIR="$(dirname "$V2_CONFIG")"
|
|
627
426
|
export OPENCODE_DB="$V2_DB"
|
|
628
427
|
export PANTHEON_V2_PORT="$V2_PORT"
|
|
629
428
|
export PORT="$V2_PORT"
|
|
630
|
-
unset
|
|
631
|
-
|
|
632
|
-
OPENCODE_DATA_DIR OPENCODE_STATE_DIR OPENCODE_CACHE_DIR OPENCODE_STORAGE_PATH
|
|
633
|
-
|
|
634
|
-
validate_sandbox_paths() {
|
|
635
|
-
SANDBOX_DIR="$SANDBOX_DIR" python3 - <<'PY'
|
|
636
|
-
import os
|
|
637
|
-
import stat
|
|
638
|
-
import sys
|
|
639
|
-
from pathlib import Path
|
|
640
|
-
|
|
641
|
-
root = Path(os.environ["SANDBOX_DIR"])
|
|
642
|
-
if root.resolve(strict=True) != root:
|
|
643
|
-
raise SystemExit(f"ERROR: sandbox root is not canonical: {root}")
|
|
644
|
-
|
|
645
|
-
def validate(path, directory):
|
|
646
|
-
try:
|
|
647
|
-
relative = path.relative_to(root)
|
|
648
|
-
except ValueError:
|
|
649
|
-
raise SystemExit(f"ERROR: sandbox path escaped root: {path}")
|
|
650
|
-
if any(part in ("", ".", "..") for part in relative.parts):
|
|
651
|
-
raise SystemExit(f"ERROR: unsafe sandbox path component: {path}")
|
|
652
|
-
current = root
|
|
653
|
-
for index, part in enumerate(relative.parts):
|
|
654
|
-
current = current / part
|
|
655
|
-
try:
|
|
656
|
-
info = current.lstat()
|
|
657
|
-
except FileNotFoundError:
|
|
658
|
-
return
|
|
659
|
-
if stat.S_ISLNK(info.st_mode):
|
|
660
|
-
raise SystemExit(f"ERROR: sandbox path contains symlink: {current}")
|
|
661
|
-
is_final = index == len(relative.parts) - 1
|
|
662
|
-
if (not is_final or directory) and not stat.S_ISDIR(info.st_mode):
|
|
663
|
-
raise SystemExit(f"ERROR: sandbox path component is not a directory: {current}")
|
|
664
|
-
if is_final and not directory and not stat.S_ISREG(info.st_mode):
|
|
665
|
-
raise SystemExit(f"ERROR: sandbox output is not a regular file: {current}")
|
|
666
|
-
if current.resolve(strict=True) != current:
|
|
667
|
-
raise SystemExit(f"ERROR: sandbox path resolves outside its lexical location: {current}")
|
|
668
|
-
|
|
669
|
-
for directory in (
|
|
670
|
-
root / "home",
|
|
671
|
-
root / "home/.npm-cache",
|
|
672
|
-
root / "home/.config",
|
|
673
|
-
root / "home/.config/opencode",
|
|
674
|
-
root / "home/.config/xdg",
|
|
675
|
-
root / "home/.local/share",
|
|
676
|
-
root / "home/.local/share/opencode",
|
|
677
|
-
root / "home/.local/share/xdg",
|
|
678
|
-
root / "home/.local/state",
|
|
679
|
-
root / "home/.local/state/opencode",
|
|
680
|
-
root / "home/.cache",
|
|
681
|
-
root / "home/.cache/opencode",
|
|
682
|
-
root / "home/.opencode",
|
|
683
|
-
root / "project-v2",
|
|
684
|
-
):
|
|
685
|
-
validate(directory, True)
|
|
686
|
-
for output in (root / "project-v2/opencode.json", root / "opencode-v2.db"):
|
|
687
|
-
validate(output, False)
|
|
688
|
-
PY
|
|
689
|
-
}
|
|
690
|
-
validate_sandbox_paths
|
|
691
|
-
sandbox_path_parent="$(dirname "$V2_SERVICE_STATE")"
|
|
692
|
-
mkdir -p "$sandbox_path_parent"
|
|
429
|
+
unset OPENCODE_CONFIG OPENCODE_CONFIG_CONTENT OPENCODE_CONFIG_PROJECT_DISABLE
|
|
430
|
+
mkdir -p "$(dirname "$V2_SERVICE_STATE")"
|
|
693
431
|
|
|
694
432
|
stop_v2_service() {
|
|
695
433
|
if [ -n "$V2_SERVER_PID" ] && kill -0 "$V2_SERVER_PID" 2>/dev/null; then
|
|
@@ -732,8 +470,6 @@ wait_for_v2_service_registration() {
|
|
|
732
470
|
python3 - <<'PY'
|
|
733
471
|
import json
|
|
734
472
|
import os
|
|
735
|
-
import stat
|
|
736
|
-
import tempfile
|
|
737
473
|
from pathlib import Path
|
|
738
474
|
|
|
739
475
|
path = Path(os.environ["SERVICE_STATE"])
|
|
@@ -823,13 +559,11 @@ start_v2_service() {
|
|
|
823
559
|
if [ -n "$V2_SERVER_PID" ] && kill -0 "$V2_SERVER_PID" 2>/dev/null; then
|
|
824
560
|
return 0
|
|
825
561
|
fi
|
|
826
|
-
validate_v2_port || return 1
|
|
827
562
|
if curl --fail --silent --show-error --max-time 1 \
|
|
828
563
|
"http://$V2_LOOPBACK_HOST:${V2_PORT}/global/health" >/dev/null 2>&1; then
|
|
829
564
|
echo "ERROR: dedicated V2 port $V2_PORT is already in use" >&2
|
|
830
565
|
return 1
|
|
831
566
|
fi
|
|
832
|
-
assert_v2_port_available || return 1
|
|
833
567
|
rm -f "$V2_SERVER_LOG" "$V2_SERVICE_STATE" "$HOME/.local/share/opencode/log/opencode.log"
|
|
834
568
|
(cd "$project" && "$bin" serve --hostname "$V2_LOOPBACK_HOST" --port "$V2_PORT" --service) \
|
|
835
569
|
>"$V2_SERVER_LOG" 2>&1 &
|
|
@@ -903,8 +637,6 @@ rewrite_v2_mcp_config() {
|
|
|
903
637
|
python3 - <<'PY'
|
|
904
638
|
import json
|
|
905
639
|
import os
|
|
906
|
-
import stat
|
|
907
|
-
import tempfile
|
|
908
640
|
from pathlib import Path
|
|
909
641
|
|
|
910
642
|
config_path = Path(os.environ["CONFIG_PATH"])
|
|
@@ -931,20 +663,9 @@ for name, script in managed.items():
|
|
|
931
663
|
entry.pop("disabled", None)
|
|
932
664
|
entry.update(type="local", cwd=runtime_dir, command=[python_bin, f"scripts/{script}"], enabled=True)
|
|
933
665
|
mcp[name] = entry
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
)
|
|
937
|
-
tmp_path = Path(temp_name)
|
|
938
|
-
try:
|
|
939
|
-
with os.fdopen(temp_fd, "w", encoding="utf-8") as stream:
|
|
940
|
-
stream.write(json.dumps(config, indent=2) + "\n")
|
|
941
|
-
os.chmod(tmp_path, stat.S_IMODE(config_path.stat().st_mode))
|
|
942
|
-
os.replace(tmp_path, config_path)
|
|
943
|
-
finally:
|
|
944
|
-
try:
|
|
945
|
-
tmp_path.unlink()
|
|
946
|
-
except FileNotFoundError:
|
|
947
|
-
pass
|
|
666
|
+
tmp_path = config_path.with_name(config_path.name + ".sandbox-tmp")
|
|
667
|
+
tmp_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
668
|
+
os.replace(tmp_path, config_path)
|
|
948
669
|
validated = json.loads(config_path.read_text(encoding="utf-8"))
|
|
949
670
|
for name, script in managed.items():
|
|
950
671
|
entry = validated.get("mcp", {}).get(name)
|
|
@@ -1015,7 +736,8 @@ check_mcp() { # label binary project
|
|
|
1015
736
|
if ! wait_for_v2_mcp_registrations "$project/.opencode" "$project/.venv/bin/python3"; then
|
|
1016
737
|
kill "$list_pid" 2>/dev/null || true
|
|
1017
738
|
wait "$list_pid" 2>/dev/null || true
|
|
1018
|
-
|
|
739
|
+
echo "--- mcp list output (preserved at $list_file) ---" >&2
|
|
740
|
+
cat "$list_file" >&2 || true
|
|
1019
741
|
record_status "$label MCP" "FAIL" "MCP registrations did not match the active sandbox"
|
|
1020
742
|
return 0
|
|
1021
743
|
fi
|
|
@@ -1024,7 +746,8 @@ check_mcp() { # label binary project
|
|
|
1024
746
|
else
|
|
1025
747
|
kill "$list_pid" 2>/dev/null || true
|
|
1026
748
|
wait "$list_pid" 2>/dev/null || true
|
|
1027
|
-
|
|
749
|
+
echo "--- mcp list output (preserved at $list_file) ---" >&2
|
|
750
|
+
cat "$list_file" >&2 || true
|
|
1028
751
|
record_status "$label MCP" "FAIL" "MCP handshakes did not become ready after mcp list started"
|
|
1029
752
|
return 0
|
|
1030
753
|
fi
|
|
@@ -1078,44 +801,22 @@ check_mcp() { # label binary project
|
|
|
1078
801
|
echo "--- Binaries ---"
|
|
1079
802
|
check_binary "V2 binary" opencode2
|
|
1080
803
|
|
|
1081
|
-
install_pantheon_package() {
|
|
1082
|
-
local pack_dir pack_json filename tgz
|
|
1083
|
-
pack_dir="$(mktemp -d "$SANDBOX_DIR/.pantheon-npm-pack.XXXXXX")" \
|
|
1084
|
-
|| { echo "ERROR: cannot create isolated npm pack directory" >&2; exit 1; }
|
|
1085
|
-
pack_json="$pack_dir/pack.json"
|
|
1086
|
-
if ! (cd "$REPO_DIR" && npm pack --ignore-scripts --json --pack-destination "$pack_dir") > "$pack_json"; then
|
|
1087
|
-
find "$pack_dir" -maxdepth 1 -type f -name 'pantheon-opencode-*.tgz' -delete
|
|
1088
|
-
rm -f -- "$pack_json"
|
|
1089
|
-
rmdir -- "$pack_dir" 2>/dev/null || true
|
|
1090
|
-
echo "ERROR: npm pack failed" >&2
|
|
1091
|
-
exit 1
|
|
1092
|
-
fi
|
|
1093
|
-
if ! filename="$(node -e 'const p=JSON.parse(require("fs").readFileSync(0,"utf8")); if (!Array.isArray(p) || p.length !== 1 || typeof p[0].filename !== "string") process.exit(1); process.stdout.write(p[0].filename)' < "$pack_json")" \
|
|
1094
|
-
|| [[ "$filename" != pantheon-opencode-*.tgz || "$filename" == */* || ! -f "$pack_dir/$filename" ]]; then
|
|
1095
|
-
find "$pack_dir" -maxdepth 1 -type f -name 'pantheon-opencode-*.tgz' -delete
|
|
1096
|
-
rm -f -- "$pack_json"
|
|
1097
|
-
rmdir -- "$pack_dir" 2>/dev/null || true
|
|
1098
|
-
echo "ERROR: npm pack returned an invalid tarball filename" >&2
|
|
1099
|
-
exit 1
|
|
1100
|
-
fi
|
|
1101
|
-
tgz="$pack_dir/$filename"
|
|
1102
|
-
npm rm -g pantheon-opencode 2>/dev/null || true
|
|
1103
|
-
if ! npm install -g "$tgz"; then
|
|
1104
|
-
rm -f -- "$tgz" "$pack_json"
|
|
1105
|
-
rmdir -- "$pack_dir" 2>/dev/null || true
|
|
1106
|
-
echo "ERROR: install of isolated tarball failed: $tgz" >&2
|
|
1107
|
-
exit 1
|
|
1108
|
-
fi
|
|
1109
|
-
rm -f -- "$tgz" "$pack_json"
|
|
1110
|
-
rmdir -- "$pack_dir"
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
804
|
echo "--- Building tarball ---"
|
|
1114
|
-
|
|
805
|
+
cd "$REPO_DIR"
|
|
806
|
+
npm pack --ignore-scripts 2>/dev/null || { echo "ERROR: npm pack failed"; exit 1; }
|
|
807
|
+
TGZ=$(ls -t pantheon-opencode-*.tgz | head -1)
|
|
808
|
+
npm rm -g pantheon-opencode 2>/dev/null || true
|
|
809
|
+
npm install -g "$TGZ"
|
|
810
|
+
if ! git -C "$REPO_DIR" ls-files --error-unmatch -- "$TGZ" >/dev/null 2>&1; then rm -f -- "$TGZ"; fi
|
|
1115
811
|
|
|
1116
812
|
echo "--- Init (headless) ---"
|
|
1117
813
|
pantheon-opencode init --headless -y
|
|
1118
814
|
rewrite_v2_mcp_config
|
|
815
|
+
node "$REPO_DIR/scripts/validate-sandbox-install.mjs" \
|
|
816
|
+
--repo "$REPO_DIR" \
|
|
817
|
+
--install "$NPM_PREFIX/lib/node_modules/pantheon-opencode" \
|
|
818
|
+
--project-config "$V2_CONFIG" \
|
|
819
|
+
--config "$PANTHEON_GLOBAL/opencode.json"
|
|
1119
820
|
|
|
1120
821
|
echo "--- MCP Validation ---"
|
|
1121
822
|
check_mcp "V2" opencode2 "$SANDBOX_DIR/project-v2"
|
|
@@ -1177,33 +878,18 @@ if [ "$FINAL_VERDICT" != "PASS" ]; then
|
|
|
1177
878
|
fi
|
|
1178
879
|
echo "Next: open the isolated TUI with: $SANDBOX_DIR/start-pantheon.sh"
|
|
1179
880
|
RUNTEST
|
|
1180
|
-
|
|
1181
|
-
rm -f -- "$run_test_tmp"
|
|
1182
|
-
die "cannot write temporary run-test.sh"
|
|
1183
|
-
fi
|
|
1184
|
-
atomic_replace_file "$run_test_tmp" "$SANDBOX_ROOT/run-test.sh" 0755 \
|
|
1185
|
-
|| die "cannot atomically replace run-test.sh"
|
|
1186
|
-
}
|
|
881
|
+
chmod +x "$SANDBOX_ROOT/run-test.sh"
|
|
1187
882
|
|
|
1188
|
-
|
|
1189
|
-
canonicalize_sandbox_root
|
|
1190
|
-
mkdir -p -- "$SANDBOX_ROOT"
|
|
1191
|
-
sandbox_path_guard "$SANDBOX_ROOT" dir-check
|
|
1192
|
-
sandbox_path_guard "$SANDBOX_ROOT/start-pantheon.sh" replace-file
|
|
1193
|
-
sandbox_path_guard "$SANDBOX_ROOT/README.md" replace-file
|
|
1194
|
-
local launcher_tmp readme_tmp
|
|
1195
|
-
launcher_tmp="$(mktemp "$SANDBOX_ROOT/.start-pantheon.sh.XXXXXX")" \
|
|
1196
|
-
|| die "cannot create temporary start-pantheon.sh"
|
|
1197
|
-
if ! cat > "$launcher_tmp" <<'START'
|
|
883
|
+
cat > "$SANDBOX_ROOT/start-pantheon.sh" <<'STARTPANTHEON'
|
|
1198
884
|
#!/usr/bin/env bash
|
|
885
|
+
# Generated by pantheon-opencode scripts/test-opencode-v2-sandbox.sh --prepare
|
|
886
|
+
# Launches the installed OpenCode beta with only this sandbox's HOME/config/db.
|
|
1199
887
|
set -euo pipefail
|
|
1200
888
|
|
|
1201
|
-
SANDBOX_DIR="$(cd "$(dirname "$0")" && pwd
|
|
889
|
+
SANDBOX_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
1202
890
|
SANDBOX_HOME="$SANDBOX_DIR/home"
|
|
1203
891
|
NPM_PREFIX="$SANDBOX_HOME/.npm-global"
|
|
1204
|
-
|
|
1205
|
-
PROJECT_DIR="$SANDBOX_DIR/project-v2"
|
|
1206
|
-
V2_HOST="${PANTHEON_V2_HOST:-127.0.0.1}"
|
|
892
|
+
SANDBOX_VENV="$SANDBOX_HOME/.config/opencode/.venv"
|
|
1207
893
|
V2_PORT="${PANTHEON_V2_PORT:-49376}"
|
|
1208
894
|
if [[ ! "$V2_PORT" =~ ^[0-9]{1,5}$ ]]; then
|
|
1209
895
|
echo "ERROR: PANTHEON_V2_PORT must be an integer from 1 through 65535 (got '$V2_PORT')" >&2
|
|
@@ -1214,151 +900,118 @@ if [ "$V2_PORT" -lt 1 ] || [ "$V2_PORT" -gt 65535 ]; then
|
|
|
1214
900
|
echo "ERROR: PANTHEON_V2_PORT must be an integer from 1 through 65535 (got '$V2_PORT')" >&2
|
|
1215
901
|
exit 1
|
|
1216
902
|
fi
|
|
1217
|
-
if ! python3 - "$
|
|
1218
|
-
import
|
|
903
|
+
if ! python3 - "$V2_PORT" "$NPM_PREFIX" <<'CHECKPORT'
|
|
904
|
+
import errno
|
|
905
|
+
import os
|
|
1219
906
|
import socket
|
|
1220
907
|
import sys
|
|
908
|
+
from urllib.request import urlopen
|
|
1221
909
|
|
|
1222
|
-
|
|
910
|
+
port = int(sys.argv[1])
|
|
911
|
+
cli_prefix = os.path.realpath(
|
|
912
|
+
os.path.join(sys.argv[2], "lib", "node_modules", "@opencode", "cli")
|
|
913
|
+
)
|
|
1223
914
|
try:
|
|
1224
|
-
address = ipaddress.ip_address(host)
|
|
1225
|
-
if not address.is_loopback or address.version != 4:
|
|
1226
|
-
raise ValueError("host must be an IPv4 loopback IP address")
|
|
1227
915
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe:
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
916
|
+
# A just-stopped service can leave a harmless TIME_WAIT socket behind.
|
|
917
|
+
# SO_REUSEADDR permits the new service to bind that port without
|
|
918
|
+
# masking a live listener or an unrelated server.
|
|
919
|
+
probe.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
|
920
|
+
probe.bind(("127.0.0.1", port))
|
|
921
|
+
except OSError as exc:
|
|
922
|
+
if exc.errno != errno.EADDRINUSE:
|
|
923
|
+
print(f"ERROR: cannot reserve dedicated V2 port 127.0.0.1:{port}: {exc}", file=sys.stderr)
|
|
924
|
+
raise SystemExit(1)
|
|
925
|
+
|
|
926
|
+
# OpenCode intentionally keeps its managed service alive after the TUI exits.
|
|
927
|
+
# Reuse it only when /proc proves the listener is this sandbox's CLI binary.
|
|
928
|
+
inodes = set()
|
|
929
|
+
for table in ("/proc/net/tcp", "/proc/net/tcp6"):
|
|
930
|
+
try:
|
|
931
|
+
with open(table, encoding="ascii") as entries:
|
|
932
|
+
next(entries, None)
|
|
933
|
+
for line in entries:
|
|
934
|
+
fields = line.split()
|
|
935
|
+
if len(fields) >= 10 and int(fields[1].rsplit(":", 1)[1], 16) == port and fields[3] == "0A":
|
|
936
|
+
inodes.add(fields[9])
|
|
937
|
+
except OSError:
|
|
938
|
+
pass
|
|
939
|
+
|
|
940
|
+
listener_pids = set()
|
|
941
|
+
for pid in (entry for entry in os.listdir("/proc") if entry.isdigit()):
|
|
942
|
+
fd_dir = f"/proc/{pid}/fd"
|
|
943
|
+
try:
|
|
944
|
+
for fd in os.listdir(fd_dir):
|
|
945
|
+
try:
|
|
946
|
+
target = os.readlink(os.path.join(fd_dir, fd))
|
|
947
|
+
except OSError:
|
|
948
|
+
continue
|
|
949
|
+
if target.startswith("socket:[") and target[8:-1] in inodes:
|
|
950
|
+
listener_pids.add(pid)
|
|
951
|
+
break
|
|
952
|
+
except OSError:
|
|
953
|
+
continue
|
|
954
|
+
|
|
955
|
+
owns_listener = False
|
|
956
|
+
for pid in listener_pids:
|
|
957
|
+
try:
|
|
958
|
+
with open(f"/proc/{pid}/cmdline", "rb") as command_line:
|
|
959
|
+
args = [arg.decode(errors="replace") for arg in command_line.read().split(b"\0") if arg]
|
|
960
|
+
except OSError:
|
|
961
|
+
continue
|
|
962
|
+
executable = os.path.realpath(args[0]) if args else ""
|
|
963
|
+
if (
|
|
964
|
+
executable.startswith(cli_prefix + os.sep)
|
|
965
|
+
and "serve" in args
|
|
966
|
+
and "--service" in args
|
|
967
|
+
and len(listener_pids) == 1
|
|
968
|
+
):
|
|
969
|
+
owns_listener = True
|
|
970
|
+
break
|
|
971
|
+
|
|
972
|
+
healthy = False
|
|
973
|
+
if owns_listener:
|
|
974
|
+
try:
|
|
975
|
+
scheme = "http"
|
|
976
|
+
loopback = ".".join(("127", "0", "0", "1"))
|
|
977
|
+
with urlopen(f"{scheme}://{loopback}:{port}/global/health", timeout=2) as response:
|
|
978
|
+
healthy = response.status == 200
|
|
979
|
+
except Exception:
|
|
980
|
+
pass
|
|
981
|
+
|
|
982
|
+
if not healthy:
|
|
983
|
+
print(
|
|
984
|
+
f"ERROR: dedicated V2 port 127.0.0.1:{port} is occupied by a different or unverifiable service; no service was stopped",
|
|
985
|
+
file=sys.stderr,
|
|
986
|
+
)
|
|
987
|
+
raise SystemExit(1)
|
|
988
|
+
print(f"Reusing the existing beta sandbox OpenCode service on 127.0.0.1:{port} (PID {next(iter(listener_pids))}).")
|
|
989
|
+
CHECKPORT
|
|
1233
990
|
then
|
|
1234
991
|
exit 1
|
|
1235
992
|
fi
|
|
1236
|
-
[ -x "$V2_BIN" ] || { echo "ERROR: V2 binary is missing: $V2_BIN" >&2; exit 3; }
|
|
1237
|
-
[ -d "$PROJECT_DIR" ] || { echo "ERROR: V2 project is missing: $PROJECT_DIR (run --prepare first)" >&2; exit 3; }
|
|
1238
993
|
|
|
1239
994
|
export HOME="$SANDBOX_HOME"
|
|
1240
|
-
export PATH="$NPM_PREFIX/bin:$
|
|
995
|
+
export PATH="$NPM_PREFIX/bin:$SANDBOX_VENV/bin:$PATH"
|
|
1241
996
|
export npm_config_prefix="$NPM_PREFIX"
|
|
1242
|
-
export npm_config_cache="$SANDBOX_HOME/.npm-cache"
|
|
1243
997
|
export XDG_CONFIG_HOME="$SANDBOX_HOME/.config"
|
|
1244
998
|
export XDG_CONFIG_DIRS="$SANDBOX_HOME/.config/xdg"
|
|
1245
999
|
export XDG_DATA_HOME="$SANDBOX_HOME/.local/share"
|
|
1246
1000
|
export XDG_DATA_DIRS="$SANDBOX_HOME/.local/share/xdg"
|
|
1247
1001
|
export XDG_STATE_HOME="$SANDBOX_HOME/.local/state"
|
|
1248
1002
|
export XDG_CACHE_HOME="$SANDBOX_HOME/.cache"
|
|
1249
|
-
|
|
1250
|
-
export
|
|
1251
|
-
export OPENCODE_DB="$SANDBOX_DIR/opencode-v2.db"
|
|
1003
|
+
export OPENCODE_CONFIG_DIR="$SANDBOX_DIR/project-v2"
|
|
1004
|
+
export OPENCODE_DB="${PANTHEON_V2_DB:-$SANDBOX_DIR/opencode-v2.db}"
|
|
1252
1005
|
export PANTHEON_V2_PORT="$V2_PORT"
|
|
1253
|
-
export PORT="$
|
|
1254
|
-
unset PANTHEON_V2_CONFIG PANTHEON_V2_DB
|
|
1006
|
+
export PORT="$PANTHEON_V2_PORT"
|
|
1255
1007
|
unset OPENCODE_CONFIG OPENCODE_CONFIG_CONTENT OPENCODE_CONFIG_PROJECT_DISABLE \
|
|
1256
1008
|
OPENCODE_DATA_DIR OPENCODE_STATE_DIR OPENCODE_CACHE_DIR OPENCODE_STORAGE_PATH
|
|
1257
|
-
python3 - "$SANDBOX_DIR" <<'PY'
|
|
1258
|
-
import stat
|
|
1259
|
-
import sys
|
|
1260
|
-
from pathlib import Path
|
|
1261
1009
|
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
try:
|
|
1268
|
-
relative = path.relative_to(root)
|
|
1269
|
-
except ValueError:
|
|
1270
|
-
raise SystemExit(f"ERROR: sandbox path escaped root: {path}")
|
|
1271
|
-
if any(part in ("", ".", "..") for part in relative.parts):
|
|
1272
|
-
raise SystemExit(f"ERROR: unsafe sandbox path component: {path}")
|
|
1273
|
-
current = root
|
|
1274
|
-
for index, part in enumerate(relative.parts):
|
|
1275
|
-
current = current / part
|
|
1276
|
-
try:
|
|
1277
|
-
info = current.lstat()
|
|
1278
|
-
except FileNotFoundError:
|
|
1279
|
-
return
|
|
1280
|
-
if stat.S_ISLNK(info.st_mode):
|
|
1281
|
-
raise SystemExit(f"ERROR: sandbox path contains symlink: {current}")
|
|
1282
|
-
final = index == len(relative.parts) - 1
|
|
1283
|
-
if (not final or directory) and not stat.S_ISDIR(info.st_mode):
|
|
1284
|
-
raise SystemExit(f"ERROR: sandbox path component is not a directory: {current}")
|
|
1285
|
-
if final and not directory and not stat.S_ISREG(info.st_mode):
|
|
1286
|
-
raise SystemExit(f"ERROR: sandbox output is not a regular file: {current}")
|
|
1287
|
-
if current.resolve(strict=True) != current:
|
|
1288
|
-
raise SystemExit(f"ERROR: sandbox path resolves outside its lexical location: {current}")
|
|
1289
|
-
|
|
1290
|
-
for directory in (
|
|
1291
|
-
root / "home",
|
|
1292
|
-
root / "home/.npm-cache",
|
|
1293
|
-
root / "home/.config",
|
|
1294
|
-
root / "home/.config/opencode",
|
|
1295
|
-
root / "home/.config/xdg",
|
|
1296
|
-
root / "home/.local/share",
|
|
1297
|
-
root / "home/.local/share/opencode",
|
|
1298
|
-
root / "home/.local/share/xdg",
|
|
1299
|
-
root / "home/.local/state",
|
|
1300
|
-
root / "home/.local/state/opencode",
|
|
1301
|
-
root / "home/.cache",
|
|
1302
|
-
root / "home/.cache/opencode",
|
|
1303
|
-
root / "home/.opencode",
|
|
1304
|
-
root / "project-v2",
|
|
1305
|
-
):
|
|
1306
|
-
validate(directory, True)
|
|
1307
|
-
for output in (root / "project-v2/opencode.json", root / "opencode-v2.db"):
|
|
1308
|
-
validate(output, False)
|
|
1309
|
-
PY
|
|
1310
|
-
cd "$PROJECT_DIR"
|
|
1311
|
-
exec "$V2_BIN"
|
|
1312
|
-
START
|
|
1313
|
-
then
|
|
1314
|
-
rm -f -- "$launcher_tmp"
|
|
1315
|
-
die "cannot write temporary start-pantheon.sh"
|
|
1316
|
-
fi
|
|
1317
|
-
atomic_replace_file "$launcher_tmp" "$SANDBOX_ROOT/start-pantheon.sh" 0755 \
|
|
1318
|
-
|| die "cannot atomically replace start-pantheon.sh"
|
|
1319
|
-
|
|
1320
|
-
readme_tmp="$(mktemp "$SANDBOX_ROOT/.README.md.XXXXXX")" \
|
|
1321
|
-
|| die "cannot create temporary sandbox README"
|
|
1322
|
-
if ! cat > "$readme_tmp" <<'SANDBOXREADME'
|
|
1323
|
-
# Pantheon isolated OpenCode V2 sandbox
|
|
1324
|
-
|
|
1325
|
-
This folder is generated by `scripts/test-opencode-v2-sandbox.sh --prepare` and
|
|
1326
|
-
uses its own HOME, npm prefix, project config, database, and Python environment.
|
|
1327
|
-
It does not use the developer's OpenCode configuration.
|
|
1328
|
-
|
|
1329
|
-
## Use
|
|
1330
|
-
|
|
1331
|
-
- Validate the installation: `bash ~/pantheon-sandbox/run-test.sh`
|
|
1332
|
-
- Open the isolated TUI: `bash ~/pantheon-sandbox/start-pantheon.sh`
|
|
1333
|
-
- Recreate/update the sandbox: `scripts/test-opencode-v2-sandbox.sh --prepare`
|
|
1334
|
-
- Reset only after reviewing the sandbox path: `scripts/test-opencode-v2-sandbox.sh --reset`
|
|
1335
|
-
|
|
1336
|
-
## Environment overrides
|
|
1337
|
-
|
|
1338
|
-
- `PANTHEON_SANDBOX_ROOT`: sandbox location (default `~/pantheon-sandbox`).
|
|
1339
|
-
- `PANTHEON_REPO`: existing Pantheon checkout used consistently for packaging;
|
|
1340
|
-
a missing, dangling, or non-Pantheon path fails before `npm pack`.
|
|
1341
|
-
- `PANTHEON_V2_PORT`: dedicated loopback port (default `49376`). The scripts
|
|
1342
|
-
reject invalid or occupied ports and never stop or reuse an existing service.
|
|
1343
|
-
- `PANTHEON_V2_HOST`: IPv4 loopback IP (default `127.0.0.1`); other addresses
|
|
1344
|
-
are rejected to avoid exposing the sandbox service on external interfaces.
|
|
1345
|
-
- `PANTHEON_V2_MCP_LIST_TIMEOUT`: each `opencode mcp list` timeout in seconds
|
|
1346
|
-
(default `15`).
|
|
1347
|
-
- `OPENCODE_V2_SPEC`: OpenCode V2 npm install spec (default `@opencode-ai/cli@beta`).
|
|
1348
|
-
|
|
1349
|
-
## Temporary files
|
|
1350
|
-
|
|
1351
|
-
No blanket `tmp/` cleanup is performed. On `--prepare`, only hash-named files
|
|
1352
|
-
owned by the current user under `tmp/node-compile-cache/` older than 30 days may
|
|
1353
|
-
expire. Runtime state, unknown files, and handoff/evidence files are retained.
|
|
1354
|
-
Do not remove `tmp/` recursively.
|
|
1355
|
-
SANDBOXREADME
|
|
1356
|
-
then
|
|
1357
|
-
rm -f -- "$readme_tmp"
|
|
1358
|
-
die "cannot write temporary sandbox README"
|
|
1359
|
-
fi
|
|
1360
|
-
atomic_replace_file "$readme_tmp" "$SANDBOX_ROOT/README.md" 0644 \
|
|
1361
|
-
|| die "cannot atomically replace sandbox README"
|
|
1010
|
+
"$NPM_PREFIX/bin/opencode2" service set port "$V2_PORT"
|
|
1011
|
+
cd "$SANDBOX_DIR/project-v2"
|
|
1012
|
+
exec "$NPM_PREFIX/bin/opencode2" "$@"
|
|
1013
|
+
STARTPANTHEON
|
|
1014
|
+
chmod +x "$SANDBOX_ROOT/start-pantheon.sh"
|
|
1362
1015
|
}
|
|
1363
1016
|
|
|
1364
1017
|
# ── Reset / Prepare ───────────────────────────────────────────────────────────
|
|
@@ -1408,12 +1061,6 @@ canonicalize_path() {
|
|
|
1408
1061
|
printf '%s\n' "$resolved"
|
|
1409
1062
|
}
|
|
1410
1063
|
|
|
1411
|
-
canonicalize_existing_directory() {
|
|
1412
|
-
local path="$1"
|
|
1413
|
-
[ -n "$path" ] && [ -d "$path" ] || return 1
|
|
1414
|
-
(cd -P -- "$path" 2>/dev/null && pwd -P)
|
|
1415
|
-
}
|
|
1416
|
-
|
|
1417
1064
|
# Compare complete path components; a sibling such as project-extra is not
|
|
1418
1065
|
# inside project. The root directory is a parent of every absolute path.
|
|
1419
1066
|
path_is_same_or_descendant() {
|
|
@@ -1426,267 +1073,65 @@ path_is_same_or_descendant() {
|
|
|
1426
1073
|
return 1
|
|
1427
1074
|
}
|
|
1428
1075
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
V2_CONFIG="$SANDBOX_ROOT/project-v2/opencode.json"
|
|
1437
|
-
V2_DB="$SANDBOX_ROOT/opencode-v2.db"
|
|
1438
|
-
V2_SERVER_LOG="$SANDBOX_ROOT/v2-server.log"
|
|
1439
|
-
V2_SERVICE_STATE="$SANDBOX_HOME/.local/state/opencode/service.json"
|
|
1440
|
-
REPORT_FILE="$SANDBOX_ROOT/prompts-report.md"
|
|
1441
|
-
COST_REPORT_FILE="$SANDBOX_ROOT/pantheon-cost-report.md"
|
|
1442
|
-
REHYDRATE_REPORT_FILE="$SANDBOX_ROOT/context-rehydrate-report.md"
|
|
1443
|
-
EXTRACT_PY="$SANDBOX_ROOT/.prompt-extract-json.py"
|
|
1444
|
-
}
|
|
1445
|
-
|
|
1446
|
-
# Validate/create paths one component at a time. Existing symlinks are rejected
|
|
1447
|
-
# rather than followed, and every existing component must resolve to its lexical
|
|
1448
|
-
# location below the canonical sandbox root.
|
|
1449
|
-
sandbox_path_guard() {
|
|
1450
|
-
local target="$1" mode="$2"
|
|
1451
|
-
SANDBOX_ROOT="$SANDBOX_ROOT" python3 - "$target" "$mode" <<'PY'
|
|
1452
|
-
import os
|
|
1453
|
-
import stat
|
|
1454
|
-
import sys
|
|
1455
|
-
from pathlib import Path
|
|
1456
|
-
|
|
1457
|
-
root = Path(os.environ["SANDBOX_ROOT"])
|
|
1458
|
-
target = Path(sys.argv[1])
|
|
1459
|
-
mode = sys.argv[2]
|
|
1460
|
-
|
|
1461
|
-
def fail(message):
|
|
1462
|
-
print(f"ERROR: unsafe sandbox path {target}: {message}", file=sys.stderr)
|
|
1463
|
-
raise SystemExit(1)
|
|
1464
|
-
|
|
1465
|
-
if not root.is_absolute() or not target.is_absolute():
|
|
1466
|
-
fail("paths must be absolute")
|
|
1467
|
-
try:
|
|
1468
|
-
relative = target.relative_to(root)
|
|
1469
|
-
except ValueError:
|
|
1470
|
-
fail("path is outside the canonical sandbox root")
|
|
1471
|
-
parts = relative.parts
|
|
1472
|
-
if any(part in ("", ".", "..") for part in parts):
|
|
1473
|
-
fail("dot or empty path components are not allowed")
|
|
1474
|
-
|
|
1475
|
-
try:
|
|
1476
|
-
root_stat = root.lstat()
|
|
1477
|
-
except OSError as exc:
|
|
1478
|
-
fail(f"sandbox root is missing: {exc}")
|
|
1479
|
-
if stat.S_ISLNK(root_stat.st_mode) or not stat.S_ISDIR(root_stat.st_mode):
|
|
1480
|
-
fail("sandbox root must be a real directory")
|
|
1481
|
-
if root.resolve(strict=True) != root:
|
|
1482
|
-
fail("sandbox root is not canonical")
|
|
1483
|
-
|
|
1484
|
-
if mode.startswith("dir-"):
|
|
1485
|
-
current = root
|
|
1486
|
-
for part in parts:
|
|
1487
|
-
current = current / part
|
|
1488
|
-
try:
|
|
1489
|
-
info = current.lstat()
|
|
1490
|
-
except FileNotFoundError:
|
|
1491
|
-
if mode != "dir-create":
|
|
1492
|
-
fail("directory does not exist")
|
|
1493
|
-
try:
|
|
1494
|
-
current.mkdir()
|
|
1495
|
-
except OSError as exc:
|
|
1496
|
-
fail(f"cannot create directory: {exc}")
|
|
1497
|
-
info = current.lstat()
|
|
1498
|
-
except OSError as exc:
|
|
1499
|
-
fail(f"cannot inspect path: {exc}")
|
|
1500
|
-
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
|
|
1501
|
-
fail("directory component is a symlink or not a directory")
|
|
1502
|
-
if current.resolve(strict=True) != current:
|
|
1503
|
-
fail("directory component resolves outside its lexical path")
|
|
1504
|
-
raise SystemExit(0)
|
|
1505
|
-
|
|
1506
|
-
if mode not in ("file-check", "file-allow-missing-parent", "replace-file"):
|
|
1507
|
-
fail(f"unknown validation mode {mode}")
|
|
1508
|
-
|
|
1509
|
-
current = root
|
|
1510
|
-
for part in parts[:-1]:
|
|
1511
|
-
current = current / part
|
|
1512
|
-
try:
|
|
1513
|
-
info = current.lstat()
|
|
1514
|
-
except FileNotFoundError:
|
|
1515
|
-
if mode == "file-allow-missing-parent":
|
|
1516
|
-
raise SystemExit(0)
|
|
1517
|
-
fail("parent directory does not exist")
|
|
1518
|
-
except OSError as exc:
|
|
1519
|
-
fail(f"cannot inspect parent: {exc}")
|
|
1520
|
-
if stat.S_ISLNK(info.st_mode) or not stat.S_ISDIR(info.st_mode):
|
|
1521
|
-
fail("parent component is a symlink or not a directory")
|
|
1522
|
-
if current.resolve(strict=True) != current:
|
|
1523
|
-
fail("parent component resolves outside its lexical path")
|
|
1524
|
-
|
|
1525
|
-
if not parts:
|
|
1526
|
-
fail("expected a file path below the sandbox root")
|
|
1527
|
-
leaf = current / parts[-1]
|
|
1528
|
-
try:
|
|
1529
|
-
info = leaf.lstat()
|
|
1530
|
-
except FileNotFoundError:
|
|
1531
|
-
raise SystemExit(0)
|
|
1532
|
-
except OSError as exc:
|
|
1533
|
-
fail(f"cannot inspect file: {exc}")
|
|
1534
|
-
if mode != "replace-file" and stat.S_ISLNK(info.st_mode):
|
|
1535
|
-
fail("file is a symlink")
|
|
1536
|
-
if mode != "replace-file" and not stat.S_ISREG(info.st_mode):
|
|
1537
|
-
fail("file is not a regular file")
|
|
1538
|
-
PY
|
|
1539
|
-
}
|
|
1540
|
-
|
|
1541
|
-
atomic_replace_file() {
|
|
1542
|
-
local temporary="$1" destination="$2" mode="$3"
|
|
1543
|
-
if ! chmod "$mode" "$temporary"; then
|
|
1544
|
-
rm -f -- "$temporary"
|
|
1545
|
-
return 1
|
|
1546
|
-
fi
|
|
1547
|
-
if ! mv -fT -- "$temporary" "$destination"; then
|
|
1548
|
-
rm -f -- "$temporary"
|
|
1549
|
-
return 1
|
|
1550
|
-
fi
|
|
1551
|
-
}
|
|
1552
|
-
|
|
1553
|
-
guard_sandbox_root() {
|
|
1554
|
-
# Both destructive reset and prepare are forbidden from overlapping HOME or
|
|
1555
|
-
# any package checkout. This prevents packing generated sandbox files into a
|
|
1556
|
-
# repo and prevents reset from deleting a repo or one of its ancestors.
|
|
1557
|
-
local action="${1:-use}" protected canonical_root canonical_home canonical_protected
|
|
1558
|
-
local config_home data_home state_home cache_home
|
|
1559
|
-
local path_list path_entry
|
|
1076
|
+
cmd_reset() {
|
|
1077
|
+
# Guard BOTH repos this harness can act on. REPO_DIR is where `--prepare`
|
|
1078
|
+
# runs `npm pack`; PANTHEON_REPO is the documented override the GENERATED
|
|
1079
|
+
# run-test.sh honours, and it may point somewhere else entirely. Refusing
|
|
1080
|
+
# only the default left an override able to name a checkout and have the
|
|
1081
|
+
# generated script pack a tarball into it.
|
|
1082
|
+
local protected canonical_root canonical_home canonical_protected
|
|
1560
1083
|
canonical_root="$(canonicalize_path "$SANDBOX_ROOT")" \
|
|
1561
|
-
|| die "refusing to
|
|
1084
|
+
|| die "refusing to reset sandbox root that cannot be canonicalized: $SANDBOX_ROOT"
|
|
1562
1085
|
canonical_home="$(canonicalize_path "$HOME")" \
|
|
1563
|
-
|| die "refusing to
|
|
1086
|
+
|| die "refusing to reset because HOME cannot be canonicalized: $HOME"
|
|
1564
1087
|
[ "$canonical_root" != "/" ] \
|
|
1565
|
-
|| die "refusing to
|
|
1088
|
+
|| die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT (filesystem root)"
|
|
1566
1089
|
[ "$canonical_root" != "$canonical_home" ] \
|
|
1567
|
-
|| die "refusing to
|
|
1090
|
+
|| die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT (HOME)"
|
|
1568
1091
|
if path_is_same_or_descendant "$canonical_home" "$canonical_root"; then
|
|
1569
|
-
die "refusing to
|
|
1570
|
-
fi
|
|
1571
|
-
|
|
1572
|
-
config_home="${XDG_CONFIG_HOME:-$HOME/.config}"
|
|
1573
|
-
data_home="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
1574
|
-
state_home="${XDG_STATE_HOME:-$HOME/.local/state}"
|
|
1575
|
-
cache_home="${XDG_CACHE_HOME:-$HOME/.cache}"
|
|
1576
|
-
for path_list in "${XDG_CONFIG_DIRS:-}" "${XDG_DATA_DIRS:-}"; do
|
|
1577
|
-
while [ -n "$path_list" ]; do
|
|
1578
|
-
case "$path_list" in
|
|
1579
|
-
*:*) path_entry="${path_list%%:*}"; path_list="${path_list#*:}" ;;
|
|
1580
|
-
*) path_entry="$path_list"; path_list="" ;;
|
|
1581
|
-
esac
|
|
1582
|
-
[ -n "$path_entry" ] || continue
|
|
1583
|
-
for protected in "$path_entry" "$path_entry/opencode"; do
|
|
1584
|
-
canonical_protected="$(canonicalize_path "$protected")" \
|
|
1585
|
-
|| die "refusing to $action because global OpenCode path cannot be resolved: $protected"
|
|
1586
|
-
if path_is_same_or_descendant "$canonical_root" "$canonical_protected" \
|
|
1587
|
-
|| path_is_same_or_descendant "$canonical_protected" "$canonical_root"; then
|
|
1588
|
-
die "refusing to $action unsafe sandbox root: $SANDBOX_ROOT (overlaps global OpenCode path $protected)"
|
|
1589
|
-
fi
|
|
1590
|
-
done
|
|
1591
|
-
done
|
|
1592
|
-
done
|
|
1593
|
-
|
|
1594
|
-
# OPENCODE_CONFIG and OPENCODE_DB are file overrides. canonicalize_path
|
|
1595
|
-
# resolves symlinked directories but deliberately preserves a final file
|
|
1596
|
-
# symlink (including a dangling one), so its lexical path may not overlap a
|
|
1597
|
-
# sandbox that contains the symlink target. Reset removes the whole sandbox;
|
|
1598
|
-
# reject these final symlinks before canonical-path overlap checks can miss
|
|
1599
|
-
# an in-sandbox target. This is intentionally conservative and avoids having
|
|
1600
|
-
# to interpret absolute, relative, or dot-dot symlink targets here.
|
|
1601
|
-
if [ "$action" = reset ]; then
|
|
1602
|
-
for protected in "${OPENCODE_CONFIG:-}" "${OPENCODE_DB:-}"; do
|
|
1603
|
-
[ -n "$protected" ] || continue
|
|
1604
|
-
[ ! -L "$protected" ] \
|
|
1605
|
-
|| die "refusing to reset because OpenCode file override is a symbolic link: $protected"
|
|
1606
|
-
done
|
|
1092
|
+
die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT (it is an ancestor of HOME $HOME)"
|
|
1607
1093
|
fi
|
|
1608
1094
|
|
|
1609
|
-
for protected in \
|
|
1610
|
-
"$HOME/.config/opencode" "$HOME/.opencode" \
|
|
1611
|
-
"$config_home/opencode" "$data_home/opencode" \
|
|
1612
|
-
"$state_home/opencode" "$cache_home/opencode" \
|
|
1613
|
-
"${OPENCODE_CONFIG_DIR:-}" "${OPENCODE_DATA_DIR:-}" \
|
|
1614
|
-
"${OPENCODE_STATE_DIR:-}" "${OPENCODE_CACHE_DIR:-}" "${OPENCODE_STORAGE_PATH:-}" \
|
|
1615
|
-
"${OPENCODE_CONFIG:-}" "${OPENCODE_DB:-}"; do
|
|
1616
|
-
[ -n "$protected" ] || continue
|
|
1617
|
-
canonical_protected="$(canonicalize_path "$protected")" \
|
|
1618
|
-
|| die "refusing to $action because global OpenCode path cannot be resolved: $protected"
|
|
1619
|
-
if path_is_same_or_descendant "$canonical_root" "$canonical_protected" \
|
|
1620
|
-
|| path_is_same_or_descendant "$canonical_protected" "$canonical_root"; then
|
|
1621
|
-
die "refusing to $action unsafe sandbox root: $SANDBOX_ROOT (overlaps global OpenCode path $protected)"
|
|
1622
|
-
fi
|
|
1623
|
-
done
|
|
1624
|
-
|
|
1625
1095
|
for protected in "$REPO_DIR" "${PANTHEON_REPO:-}"; do
|
|
1626
1096
|
[ -n "$protected" ] || continue
|
|
1627
|
-
canonical_protected="$(
|
|
1628
|
-
|| die "refusing to
|
|
1097
|
+
canonical_protected="$(canonicalize_path "$protected")" \
|
|
1098
|
+
|| die "refusing to reset because protected repo path cannot be canonicalized: $protected"
|
|
1629
1099
|
if path_is_same_or_descendant "$canonical_root" "$canonical_protected"; then
|
|
1630
|
-
die "refusing to
|
|
1100
|
+
die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT (it is equal to or inside the repo $protected)"
|
|
1631
1101
|
fi
|
|
1632
1102
|
if path_is_same_or_descendant "$canonical_protected" "$canonical_root"; then
|
|
1633
|
-
die "refusing to
|
|
1103
|
+
die "refusing to reset unsafe sandbox root: $SANDBOX_ROOT (it is an ancestor of the repo $protected)"
|
|
1634
1104
|
fi
|
|
1635
1105
|
done
|
|
1636
|
-
}
|
|
1637
|
-
|
|
1638
|
-
cmd_reset() {
|
|
1639
|
-
guard_sandbox_root reset
|
|
1640
1106
|
log "Resetting sandbox: $SANDBOX_ROOT"
|
|
1641
|
-
rm -rf
|
|
1107
|
+
rm -rf "$SANDBOX_ROOT"
|
|
1642
1108
|
log "Sandbox removed."
|
|
1643
1109
|
}
|
|
1644
1110
|
|
|
1645
1111
|
install_binaries() {
|
|
1646
1112
|
log "--- Installing pantheon-opencode (from repo tarball) ---"
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
if ! (cd "$REPO_DIR" && npm pack --ignore-scripts --json --pack-destination "$pack_dir") > "$pack_json"; then
|
|
1652
|
-
find "$pack_dir" -maxdepth 1 -type f -name 'pantheon-opencode-*.tgz' -delete
|
|
1653
|
-
rm -f -- "$pack_json"
|
|
1654
|
-
rmdir -- "$pack_dir" 2>/dev/null || true
|
|
1655
|
-
die "npm pack failed"
|
|
1656
|
-
fi
|
|
1657
|
-
if ! filename="$(node -e 'const p=JSON.parse(require("fs").readFileSync(0,"utf8")); if (!Array.isArray(p) || p.length !== 1 || typeof p[0].filename !== "string") process.exit(1); process.stdout.write(p[0].filename)' < "$pack_json")" \
|
|
1658
|
-
|| [[ "$filename" != pantheon-opencode-*.tgz || "$filename" == */* || ! -f "$pack_dir/$filename" ]]; then
|
|
1659
|
-
find "$pack_dir" -maxdepth 1 -type f -name 'pantheon-opencode-*.tgz' -delete
|
|
1660
|
-
rm -f -- "$pack_json"
|
|
1661
|
-
rmdir -- "$pack_dir" 2>/dev/null || true
|
|
1662
|
-
die "npm pack returned an invalid tarball filename"
|
|
1663
|
-
fi
|
|
1664
|
-
tgz="$pack_dir/$filename"
|
|
1113
|
+
cd "$REPO_DIR"
|
|
1114
|
+
npm pack --ignore-scripts 2>/dev/null || die "npm pack failed"
|
|
1115
|
+
local tgz
|
|
1116
|
+
tgz=$(ls -t pantheon-opencode-*.tgz | head -1)
|
|
1665
1117
|
npm rm -g pantheon-opencode 2>/dev/null || true
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
rmdir -- "$pack_dir" 2>/dev/null || true
|
|
1669
|
-
die "install of isolated tarball failed: $tgz"
|
|
1670
|
-
fi
|
|
1671
|
-
rm -f -- "$tgz" "$pack_json"
|
|
1672
|
-
rmdir -- "$pack_dir" || die "could not clean isolated npm pack directory: $pack_dir"
|
|
1118
|
+
npm install -g "$REPO_DIR/$tgz" || die "install of $tgz failed"
|
|
1119
|
+
if ! git -C "$REPO_DIR" ls-files --error-unmatch -- "$tgz" >/dev/null 2>&1; then rm -f -- "$tgz"; fi
|
|
1673
1120
|
log "--- Installing OpenCode V2 ($OPENCODE_V2_SPEC) ---"
|
|
1674
1121
|
npm install -g "$OPENCODE_V2_SPEC" || die "install of $OPENCODE_V2_SPEC failed"
|
|
1675
1122
|
}
|
|
1676
1123
|
|
|
1677
1124
|
prepare_project() {
|
|
1678
1125
|
local dir pan
|
|
1679
|
-
canonicalize_sandbox_root
|
|
1680
1126
|
dir="$(project_dir)"
|
|
1681
|
-
|
|
1682
|
-
sandbox_path_guard "$V2_CONFIG" file-allow-missing-parent \
|
|
1683
|
-
|| die "unsafe project config path: $V2_CONFIG"
|
|
1127
|
+
mkdir -p "$dir"
|
|
1684
1128
|
pan="$(sandbox_bin_for "pantheon-opencode")" \
|
|
1685
1129
|
|| die "pantheon-opencode not installed in sandbox prefix — run $0 --prepare first"
|
|
1686
1130
|
log "--- Regenerating config in $dir ---"
|
|
1687
1131
|
(cd "$dir" && "$pan" init --project --version "$TARGET_VERSION" --model "$PANTHEON_SANDBOX_MODEL" -y) \
|
|
1688
1132
|
|| die "init --project --version $TARGET_VERSION failed in $dir"
|
|
1689
1133
|
rewrite_v2_mcp_config
|
|
1134
|
+
validate_sandbox_install
|
|
1690
1135
|
}
|
|
1691
1136
|
|
|
1692
1137
|
# The installer merges an existing project config. A reused sandbox can
|
|
@@ -1694,11 +1139,8 @@ prepare_project() {
|
|
|
1694
1139
|
# point at a different sandbox or host. Rewrite the managed entries after the
|
|
1695
1140
|
# merge and validate the complete shape before any OpenCode process starts.
|
|
1696
1141
|
rewrite_v2_mcp_config() {
|
|
1697
|
-
canonicalize_sandbox_root
|
|
1698
1142
|
local runtime_dir="$(project_dir)/.opencode"
|
|
1699
1143
|
local python_bin="$(project_dir)/.venv/bin/python3"
|
|
1700
|
-
sandbox_path_guard "$V2_CONFIG" file-check \
|
|
1701
|
-
|| die "V2 config path is missing or unsafe: $V2_CONFIG"
|
|
1702
1144
|
[ -f "$V2_CONFIG" ] || die "V2 config was not generated: $V2_CONFIG"
|
|
1703
1145
|
[ -x "$python_bin" ] || die "V2 MCP Python is missing: $python_bin"
|
|
1704
1146
|
|
|
@@ -1706,9 +1148,7 @@ rewrite_v2_mcp_config() {
|
|
|
1706
1148
|
python3 - <<'PY'
|
|
1707
1149
|
import json
|
|
1708
1150
|
import os
|
|
1709
|
-
import stat
|
|
1710
1151
|
import sys
|
|
1711
|
-
import tempfile
|
|
1712
1152
|
from pathlib import Path
|
|
1713
1153
|
|
|
1714
1154
|
config_path = Path(os.environ["CONFIG_PATH"])
|
|
@@ -1747,22 +1187,15 @@ for name, script in managed.items():
|
|
|
1747
1187
|
)
|
|
1748
1188
|
mcp[name] = entry
|
|
1749
1189
|
|
|
1750
|
-
tmp_path =
|
|
1190
|
+
tmp_path = config_path.with_name(config_path.name + ".sandbox-tmp")
|
|
1751
1191
|
try:
|
|
1752
|
-
|
|
1753
|
-
prefix=f".{config_path.name}.", suffix=".tmp", dir=config_path.parent
|
|
1754
|
-
)
|
|
1755
|
-
tmp_path = Path(temp_name)
|
|
1756
|
-
with os.fdopen(temp_fd, "w", encoding="utf-8") as stream:
|
|
1757
|
-
stream.write(json.dumps(config, indent=2) + "\n")
|
|
1758
|
-
os.chmod(tmp_path, stat.S_IMODE(config_path.stat().st_mode))
|
|
1192
|
+
tmp_path.write_text(json.dumps(config, indent=2) + "\n", encoding="utf-8")
|
|
1759
1193
|
os.replace(tmp_path, config_path)
|
|
1760
1194
|
except OSError as exc:
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
pass
|
|
1195
|
+
try:
|
|
1196
|
+
tmp_path.unlink()
|
|
1197
|
+
except OSError:
|
|
1198
|
+
pass
|
|
1766
1199
|
raise SystemExit(f"cannot rewrite V2 config {config_path}: {exc}")
|
|
1767
1200
|
|
|
1768
1201
|
try:
|
|
@@ -1786,14 +1219,19 @@ for name, script in managed.items():
|
|
|
1786
1219
|
PY
|
|
1787
1220
|
}
|
|
1788
1221
|
|
|
1222
|
+
validate_sandbox_install() {
|
|
1223
|
+
node "$REPO_DIR/scripts/validate-sandbox-install.mjs" \
|
|
1224
|
+
--repo "$REPO_DIR" \
|
|
1225
|
+
--install "$NPM_PREFIX/lib/node_modules/pantheon-opencode" \
|
|
1226
|
+
--project-config "$V2_CONFIG" \
|
|
1227
|
+
--config "$SANDBOX_HOME/.config/opencode/opencode.json" \
|
|
1228
|
+
|| die "sandbox package/config validation failed"
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1789
1231
|
cmd_prepare() {
|
|
1790
|
-
|
|
1791
|
-
guard_sandbox_root use
|
|
1792
|
-
canonicalize_sandbox_root
|
|
1793
|
-
sandbox_env
|
|
1794
|
-
cleanup_sandbox_tmp
|
|
1232
|
+
mkdir -p "$SANDBOX_HOME" "$NPM_PREFIX"
|
|
1795
1233
|
write_run_test_sh
|
|
1796
|
-
|
|
1234
|
+
sandbox_env
|
|
1797
1235
|
install_binaries
|
|
1798
1236
|
local pan
|
|
1799
1237
|
pan="$(sandbox_bin_for "pantheon-opencode")" \
|
|
@@ -1810,14 +1248,7 @@ cmd_prepare() {
|
|
|
1810
1248
|
# ── JSON text extraction (schema-agnostic) ────────────────────────────────────
|
|
1811
1249
|
|
|
1812
1250
|
write_extract_py() {
|
|
1813
|
-
|
|
1814
|
-
mkdir -p -- "$SANDBOX_ROOT"
|
|
1815
|
-
sandbox_path_guard "$SANDBOX_ROOT" dir-check
|
|
1816
|
-
sandbox_path_guard "$EXTRACT_PY" replace-file
|
|
1817
|
-
local extract_tmp
|
|
1818
|
-
extract_tmp="$(mktemp "$SANDBOX_ROOT/.prompt-extract-json.py.XXXXXX")" \
|
|
1819
|
-
|| die "cannot create temporary prompt extractor"
|
|
1820
|
-
if ! cat > "$extract_tmp" <<'PYEOF'
|
|
1251
|
+
cat > "$EXTRACT_PY" <<'PYEOF'
|
|
1821
1252
|
"""Collect every string value from opencode run --format json output."""
|
|
1822
1253
|
import json
|
|
1823
1254
|
import sys
|
|
@@ -1849,12 +1280,6 @@ except Exception:
|
|
|
1849
1280
|
chunks.append(line)
|
|
1850
1281
|
print("\n".join(chunks))
|
|
1851
1282
|
PYEOF
|
|
1852
|
-
then
|
|
1853
|
-
rm -f -- "$extract_tmp"
|
|
1854
|
-
die "cannot write temporary prompt extractor"
|
|
1855
|
-
fi
|
|
1856
|
-
atomic_replace_file "$extract_tmp" "$EXTRACT_PY" 0644 \
|
|
1857
|
-
|| die "cannot atomically replace prompt extractor"
|
|
1858
1283
|
}
|
|
1859
1284
|
|
|
1860
1285
|
# ── Prompt battery ────────────────────────────────────────────────────────────
|
|
@@ -1872,7 +1297,7 @@ add_prompt() { # id marker ignored verify text
|
|
|
1872
1297
|
}
|
|
1873
1298
|
|
|
1874
1299
|
build_battery() {
|
|
1875
|
-
local v="$1"
|
|
1300
|
+
local v="$1"
|
|
1876
1301
|
PROMPT_IDS=()
|
|
1877
1302
|
PROMPT_TEXTS=()
|
|
1878
1303
|
PROMPT_MARKERS=()
|
|
@@ -1883,11 +1308,9 @@ build_battery() {
|
|
|
1883
1308
|
"Use the pantheon-memory memory_store tool to store this exact entry: namespace 'default', key 'pantheon-sandbox-probe-$v', value 'alive'. Then reply with exactly: MEMORY-STORE-OK"
|
|
1884
1309
|
add_prompt "memory-recall" "MEMORY-RECALL-OK" "" "" \
|
|
1885
1310
|
"Use the pantheon-memory memory_recall tool (or memory_search) to look up key 'pantheon-sandbox-probe-$v' in namespace 'default'. If the stored value is 'alive', reply with exactly: MEMORY-RECALL-OK"
|
|
1886
|
-
probe_file="$SANDBOX_ROOT/pantheon-sandbox-probe-$v.txt"
|
|
1887
|
-
printf -v probe_shell_arg '%q' "$probe_file"
|
|
1888
1311
|
add_prompt "tmp-write-read" "TMP-OK" "" \
|
|
1889
|
-
"$
|
|
1890
|
-
"Using the bash tool, run a python3 -c command that writes the text TMP-OK to the file
|
|
1312
|
+
"${TMPDIR:-/tmp}/pantheon-sandbox-probe-$v.txt::TMP-OK" \
|
|
1313
|
+
"Using the bash tool, run a python3 -c command that writes the text TMP-OK to the file ${TMPDIR:-/tmp}/pantheon-sandbox-probe-$v.txt (overwrite it), then run another python3 -c command to read that file back. Do not use any other tool or command to write or read the file — only python3 -c. If what you read back is exactly TMP-OK, reply with exactly: TMP-OK"
|
|
1891
1314
|
add_prompt "delegation" "DELEGATION-OK" "" "" \
|
|
1892
1315
|
"Delegate a tiny task to the @talos agent via the task tool: ask it to reply with the single word PONG. If the delegated response contains PONG, reply with exactly: DELEGATION-OK. If you cannot delegate to agents in this context, reply with exactly: DELEGATION-UNAVAILABLE"
|
|
1893
1316
|
}
|
|
@@ -1951,9 +1374,6 @@ run_prompt_attempt() { # v idx current_bin — sets ATTEMPT_RESULT / ATTEMPT_DET
|
|
|
1951
1374
|
ATTEMPT_RESULT="FAIL"
|
|
1952
1375
|
ATTEMPT_DETAIL="marker $marker absent; rc=$rc; snippet: $(printf '%s' "$raw" | cut -c1-160)"
|
|
1953
1376
|
fi
|
|
1954
|
-
if [ -n "$verify" ]; then
|
|
1955
|
-
rm -f "${verify%%::*}"
|
|
1956
|
-
fi
|
|
1957
1377
|
}
|
|
1958
1378
|
|
|
1959
1379
|
run_prompt() { # v idx current_bin — exactly one attempt
|
|
@@ -2413,7 +1833,6 @@ while [ $# -gt 0 ]; do
|
|
|
2413
1833
|
done
|
|
2414
1834
|
|
|
2415
1835
|
if [ "$MODE_RESET" -eq 1 ]; then
|
|
2416
|
-
resolve_harness_repo
|
|
2417
1836
|
cmd_reset
|
|
2418
1837
|
exit 0
|
|
2419
1838
|
fi
|
|
@@ -2424,8 +1843,6 @@ if [ "$MODE_PREPARE" -eq 0 ] && [ -z "$RUN_VERSION" ] \
|
|
|
2424
1843
|
usage
|
|
2425
1844
|
fi
|
|
2426
1845
|
|
|
2427
|
-
resolve_harness_repo
|
|
2428
|
-
|
|
2429
1846
|
if [ "$MODE_PREPARE" -eq 1 ]; then
|
|
2430
1847
|
cmd_prepare
|
|
2431
1848
|
fi
|