@cairnkeep/cli 2.3.1 → 2.5.0
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/CHANGELOG.md +85 -0
- package/README.md +232 -6
- package/bin/cairn +43 -2
- package/claude/capability-contract/hooks/capability-command-finish.sh +81 -0
- package/claude/capability-contract/hooks/capability-command-start.sh +76 -0
- package/claude/hooks/compaction-capture.sh +34 -0
- package/claude/hooks/memory-capture.sh +47 -5
- package/claude/hooks/memory-wakeup.sh +55 -2
- package/docs/building-an-overlay.md +4 -3
- package/docs/design/meta-agent.md +439 -0
- package/docs/harness-compatibility.md +117 -0
- package/docs/learning/CURRICULUM-MAP.md +91 -0
- package/docs/learning/FEATURE-GUIDE.md +86 -0
- package/docs/learning/PRODUCTION-PLAN.md +45 -14
- package/docs/learning/README.md +20 -1
- package/docs/learning/instructor-guide.md +6 -0
- package/docs/learning/lessons/L00-why-cairnkeep.md +20 -8
- package/docs/learning/lessons/L01-safe-trial.md +3 -3
- package/docs/learning/lessons/L02-installation.md +23 -5
- package/docs/learning/lessons/L03-first-project.md +7 -2
- package/docs/learning/lessons/L04-memory-fundamentals.md +4 -0
- package/docs/learning/lessons/L05-daily-workflow.md +4 -0
- package/docs/learning/lessons/L07-privacy-and-storage.md +3 -0
- package/docs/learning/lessons/L10-context-exploration.md +4 -2
- package/docs/learning/lessons/L12-managed-overlays.md +2 -0
- package/docs/learning/lessons/L13-session-evidence.md +71 -0
- package/docs/learning/lessons/L14-typed-memory.md +51 -0
- package/docs/learning/lessons/L15-capability-governance.md +56 -0
- package/docs/learning/lessons/L16-evaluation.md +67 -0
- package/docs/learning/lessons/L17-agent-boundary.md +55 -0
- package/docs/learning/templates/lesson-template.md +4 -1
- package/docs/learning/tracks/evidence-and-evaluation.md +20 -0
- package/docs/learning/tracks/operator.md +8 -2
- package/docs/learning/tracks/practitioner.md +6 -1
- package/docs/learning/tracks/quickstart.md +3 -0
- package/docs/learning/video-scripts/L00-why-cairnkeep.md +21 -17
- package/docs/learning/video-scripts/L02-installation.md +9 -2
- package/docs/learning/video-scripts/L03-first-project.md +3 -0
- package/docs/operating.md +567 -7
- package/docs/privacy-and-data-flow.md +417 -0
- package/docs/storage.md +252 -0
- package/examples/eval/README.md +42 -0
- package/examples/eval/adapter.json +12 -0
- package/examples/eval/bundled-fake.json +1 -0
- package/examples/eval/task-set.json +1 -0
- package/mcp-memory-server/dist/artifact-cli.js +184 -0
- package/mcp-memory-server/dist/artifact-schema.js +218 -0
- package/mcp-memory-server/dist/artifact-store.js +812 -0
- package/mcp-memory-server/dist/capability-adapter.js +286 -0
- package/mcp-memory-server/dist/capability-cli.js +361 -0
- package/mcp-memory-server/dist/capability-config.js +310 -0
- package/mcp-memory-server/dist/capability-harness.js +496 -0
- package/mcp-memory-server/dist/capability-registry.js +123 -0
- package/mcp-memory-server/dist/capability-schema.js +65 -0
- package/mcp-memory-server/dist/capability-store.js +431 -0
- package/mcp-memory-server/dist/compaction-normalize.js +267 -0
- package/mcp-memory-server/dist/eval-cli.js +523 -0
- package/mcp-memory-server/dist/eval-plan.js +346 -0
- package/mcp-memory-server/dist/eval-process.js +273 -0
- package/mcp-memory-server/dist/eval-report.js +609 -0
- package/mcp-memory-server/dist/eval-runner.js +827 -0
- package/mcp-memory-server/dist/eval-schema.js +368 -0
- package/mcp-memory-server/dist/eval-statistics.js +161 -0
- package/mcp-memory-server/dist/eval-workspace.js +313 -0
- package/mcp-memory-server/dist/failure-signature.js +141 -0
- package/mcp-memory-server/dist/index.js +600 -55
- package/mcp-memory-server/dist/node-cli.js +84 -0
- package/mcp-memory-server/dist/node-schema.js +170 -0
- package/mcp-memory-server/dist/node-store.js +455 -0
- package/mcp-memory-server/dist/note-cli.js +235 -0
- package/mcp-memory-server/dist/note-distiller.js +177 -0
- package/mcp-memory-server/dist/note-enrichment.js +93 -0
- package/mcp-memory-server/dist/note-schema.js +4 -0
- package/mcp-memory-server/dist/note-store.js +985 -0
- package/mcp-memory-server/dist/trajectory-cli.js +141 -0
- package/mcp-memory-server/dist/trajectory-normalize.js +448 -0
- package/mcp-memory-server/dist/trajectory-redaction.js +155 -0
- package/mcp-memory-server/dist/trajectory-schema.js +73 -0
- package/mcp-memory-server/dist/trajectory-store.js +339 -0
- package/mcp-memory-server/package.json +24 -2
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.219.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-claude-code-2.1.220.json +9 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-event.json +6 -0
- package/mcp-memory-server/scripts/fixtures/compaction-opencode-1.17.20-messages.json +119 -0
- package/mcp-memory-server/scripts/fixtures/compaction-unknown-version.json +17 -0
- package/opencode/capability-contract/plugins/capability-command.ts +319 -0
- package/opencode/command/graphify.md +25 -14
- package/opencode/plugins/memory-capture.ts +76 -10
- package/opencode/plugins/memory-wakeup.ts +58 -6
- package/package.json +8 -4
- package/pi/extensions/cairnkeep-trajectory.ts +55 -0
- package/schemas/artifact.schema.json +133 -0
- package/schemas/capability-callback.schema.json +68 -0
- package/schemas/capability-contract.schema.json +126 -0
- package/schemas/eval-adapter.schema.json +43 -0
- package/schemas/eval-protocol.schema.json +114 -0
- package/schemas/eval-report.schema.json +423 -0
- package/schemas/eval-task-set.schema.json +114 -0
- package/schemas/memory-node.schema.json +90 -0
- package/schemas/note.schema.json +112 -0
- package/schemas/trajectory-redaction.schema.json +25 -0
- package/schemas/trajectory.schema.json +82 -0
- package/scripts/bootstrap.sh +6 -0
- package/scripts/completion.sh +119 -5
- package/scripts/doctor.sh +284 -0
- package/scripts/fake-eval-adapter.mjs +120 -0
- package/scripts/fixtures/capability-harness-contracts.json +408 -0
- package/scripts/install-audit-timer.sh +3 -1
- package/scripts/lib/capability-opencode-plugin-harness.mjs +291 -0
- package/scripts/lib/trajectory-opencode-plugin-harness.mjs +25 -0
- package/scripts/lib/trajectory-pi-extension-harness.mjs +42 -0
- package/scripts/memory-wiki-audit.sh +49 -0
- package/scripts/smoke-bash32.sh +9 -0
- package/scripts/sync-claude-assets.sh +99 -6
- package/scripts/sync-opencode-graphify-assets.sh +24 -3
- package/scripts/sync-opencode-plugin-assets.sh +31 -5
- package/scripts/sync-opencode-security-assets.sh +24 -3
- package/scripts/sync-opencode-wiki-assets.sh +24 -3
- package/scripts/sync-pi-assets.sh +72 -0
- package/scripts/test-audit-timer.sh +98 -0
- package/scripts/test-bootstrap-untracked.sh +49 -0
- package/scripts/test-cli-dispatch.sh +41 -2
- package/scripts/test-compaction-hooks.sh +495 -0
- package/scripts/test-completion.sh +77 -0
- package/scripts/test-doctor.sh +348 -1
- package/scripts/test-eval-cli.sh +175 -0
- package/scripts/test-launcher-hooks.sh +630 -3
- package/scripts/test-learning-docs.sh +20 -4
- package/scripts/test-notes-cli.sh +114 -0
- package/scripts/test-package-install.sh +254 -0
- package/scripts/test-phase16-node-lifecycle.sh +50 -0
- package/scripts/test-phase17-artifact-lifecycle.sh +370 -0
- package/scripts/test-phase18-capability-lifecycle.sh +504 -0
- package/scripts/test-phase18-harness-boundary.sh +1060 -0
- package/scripts/test-phase19-eval-lifecycle.sh +1015 -0
- package/scripts/test-trajectory-cli.sh +87 -0
- package/scripts/test-trajectory-hooks.sh +142 -0
- package/scripts/test-uninstall.sh +231 -5
- package/scripts/uninstall.sh +38 -5
- package/scripts/verify-dependency-security.mjs +121 -0
- package/scripts/verify-docs-parity.sh +527 -1
- package/scripts/verify-phase18-runtime-evidence.mjs +895 -0
- package/scripts/verify-phase19-runtime-evidence.mjs +947 -0
- package/templates/capabilities.json.template +7 -0
- package/templates/env.example.template +51 -0
- package/templates/memory-wiki-audit.service.template +1 -1
- package/templates/start-claude.sh.template +36 -0
- package/templates/start-kimi.sh.template +39 -0
- package/templates/start-opencode.sh.template +47 -5
- package/templates/start-pi.sh.template +30 -0
- package/templates/trajectory-redaction.json.template +4 -0
|
@@ -0,0 +1,1060 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Native Claude/OpenCode capability-boundary contract tests. The no-argument
|
|
3
|
+
# path stays inert so callers must select an explicit, honestly scoped mode.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
|
|
7
|
+
FIXTURE="$ROOT/scripts/fixtures/capability-harness-contracts.json"
|
|
8
|
+
CLAUDE_START="$ROOT/claude/capability-contract/hooks/capability-command-start.sh"
|
|
9
|
+
CLAUDE_FINISH="$ROOT/claude/capability-contract/hooks/capability-command-finish.sh"
|
|
10
|
+
OPENCODE_PLUGIN="$ROOT/opencode/capability-contract/plugins/capability-command.ts"
|
|
11
|
+
OPENCODE_HARNESS="$ROOT/scripts/lib/capability-opencode-plugin-harness.mjs"
|
|
12
|
+
|
|
13
|
+
fail() {
|
|
14
|
+
echo "FAIL: $1" >&2
|
|
15
|
+
exit 1
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
usage() {
|
|
19
|
+
printf '%s\n' \
|
|
20
|
+
"Usage: test-phase18-harness-boundary.sh MODE" \
|
|
21
|
+
"" \
|
|
22
|
+
"Deterministic boundary modes:" \
|
|
23
|
+
" claude-hooks | opencode-plugin | opencode-sync-modes" \
|
|
24
|
+
" opencode-disposal" \
|
|
25
|
+
" claude-owner-only | opencode-command-owner-only | opencode-owner-only" \
|
|
26
|
+
" claude-delegate-calls | opencode-delegate-calls | evidence-scope" \
|
|
27
|
+
"" \
|
|
28
|
+
"Live negative controls:" \
|
|
29
|
+
" --live-claude | --live-opencode"
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
validate_fixture() {
|
|
33
|
+
node - "$FIXTURE" <<'NODE'
|
|
34
|
+
const fs = require("node:fs");
|
|
35
|
+
const fixture = JSON.parse(fs.readFileSync(process.argv[2], "utf8"));
|
|
36
|
+
if (fixture?.claude?.version !== "2.1.220") process.exit(1);
|
|
37
|
+
if (fixture?.opencode?.version !== "1.17.20") process.exit(1);
|
|
38
|
+
if (fixture?.opencode?.source?.commit !== "4473fc3c9055046183990a965d68df3db7ea6f62") process.exit(1);
|
|
39
|
+
const claudeEvents = Object.keys(fixture.claude.events).sort().join(",");
|
|
40
|
+
if (claudeEvents !== "CwdChanged,SessionEnd,Stop,StopFailure,UserPromptExpansion,UserPromptSubmit") process.exit(1);
|
|
41
|
+
const openCodeEvents = Object.keys(fixture.opencode.events).sort().join(",");
|
|
42
|
+
if (openCodeEvents !== "session.deleted,session.error,session.idle,session.status") process.exit(1);
|
|
43
|
+
if (fixture.opencode.hook.name !== "command.execute.before") process.exit(1);
|
|
44
|
+
if (fixture.opencode.dispose?.name !== "dispose") process.exit(1);
|
|
45
|
+
if (fixture.opencode.dispose?.source_commit !== "4473fc3c9055046183990a965d68df3db7ea6f62") process.exit(1);
|
|
46
|
+
NODE
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
assert_no_native_claude_assets() {
|
|
50
|
+
local live_root="$1"
|
|
51
|
+
[[ ! -e "$live_root/hooks/capability-command-start.sh" ]] || fail "normal Claude sync installed the capability start hook"
|
|
52
|
+
[[ ! -e "$live_root/hooks/capability-command-finish.sh" ]] || fail "normal Claude sync installed the capability finish hook"
|
|
53
|
+
if [[ -f "$live_root/settings.json" ]] && grep -q 'capability-command-' "$live_root/settings.json"; then
|
|
54
|
+
fail "normal Claude sync registered a capability hook"
|
|
55
|
+
fi
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
assert_no_native_opencode_assets() {
|
|
59
|
+
local live_root="$1"
|
|
60
|
+
[[ ! -e "$live_root/plugins/capability-command.ts" ]] || fail "normal OpenCode sync installed the capability plugin"
|
|
61
|
+
if [[ -d "$live_root/plugins" ]] && grep -R -q 'CapabilityCommandPlugin' "$live_root/plugins"; then
|
|
62
|
+
fail "normal OpenCode sync registered the capability plugin"
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
normal_sync_trace() {
|
|
67
|
+
local temp_root="$1"
|
|
68
|
+
local claude_root="$temp_root/claude"
|
|
69
|
+
local opencode_root="$temp_root/opencode"
|
|
70
|
+
local trace="$temp_root/process.trace"
|
|
71
|
+
|
|
72
|
+
: > "$trace"
|
|
73
|
+
printf 'sync:claude:normal\n' >> "$trace"
|
|
74
|
+
"$ROOT/scripts/sync-claude-assets.sh" --apply --live-root "$claude_root" >/dev/null
|
|
75
|
+
printf 'sync:opencode:normal\n' >> "$trace"
|
|
76
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$opencode_root" >/dev/null
|
|
77
|
+
|
|
78
|
+
assert_no_native_claude_assets "$claude_root"
|
|
79
|
+
assert_no_native_opencode_assets "$opencode_root"
|
|
80
|
+
[[ $(wc -l < "$trace" | tr -d ' ') -eq 2 ]] || fail "unexpected normal-sync process trace"
|
|
81
|
+
[[ ! -e "$temp_root/project/.agentfs/trajectory.db" ]] || fail "normal sync created measurement state"
|
|
82
|
+
[[ ! -e "$temp_root/project/.agentfs/project.db" ]] || fail "normal sync created project state"
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
install_claude_hook_fixture() {
|
|
86
|
+
local source="$1"
|
|
87
|
+
local target="$2"
|
|
88
|
+
sed "s|@@INFRA_ROOT@@|$ROOT|g" "$source" > "$target"
|
|
89
|
+
chmod 700 "$target"
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
write_capability_config() {
|
|
93
|
+
local project="$1"
|
|
94
|
+
local wiki="$2"
|
|
95
|
+
local logging="$3"
|
|
96
|
+
mkdir -p "$project/.ai"
|
|
97
|
+
cat > "$project/.ai/capabilities.json" <<JSON
|
|
98
|
+
{"schema_version":1,"capabilities":{"wiki":$wiki},"logging":{"callbacks":$logging}}
|
|
99
|
+
JSON
|
|
100
|
+
chmod 600 "$project/.ai/capabilities.json"
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
run_claude_hook() {
|
|
104
|
+
local hook="$1"
|
|
105
|
+
local project="$2"
|
|
106
|
+
local state_root="$3"
|
|
107
|
+
local payload="$4"
|
|
108
|
+
local stdout_file="$5"
|
|
109
|
+
local stderr_file="$6"
|
|
110
|
+
local trajectory="${7:-1}"
|
|
111
|
+
local status=0
|
|
112
|
+
(
|
|
113
|
+
cd "$project"
|
|
114
|
+
printf '%s' "$payload" | env \
|
|
115
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
116
|
+
CAIRN_HARNESS_STATE_DIR="$state_root" \
|
|
117
|
+
CAIRN_TRAJECTORY_CAPTURE="$trajectory" \
|
|
118
|
+
"$hook" >"$stdout_file" 2>"$stderr_file"
|
|
119
|
+
) || status=$?
|
|
120
|
+
return "$status"
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
assert_fixed_block() {
|
|
124
|
+
local stdout_file="$1"
|
|
125
|
+
local stderr_file="$2"
|
|
126
|
+
local status="$3"
|
|
127
|
+
[[ "$status" -eq 2 ]] || fail "Claude admission did not fail closed with exit 2"
|
|
128
|
+
[[ "$(cat "$stdout_file")" == '{"decision":"block","reason":"capability disabled"}' ]] || \
|
|
129
|
+
fail "Claude admission did not emit the fixed disabled block"
|
|
130
|
+
[[ ! -s "$stderr_file" ]] || fail "Claude admission disclosed rejected input on stderr"
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
callback_rows() {
|
|
134
|
+
local project="$1"
|
|
135
|
+
local database="$project/.agentfs/trajectory.db"
|
|
136
|
+
[[ -f "$database" ]] || { printf '[]'; return; }
|
|
137
|
+
(
|
|
138
|
+
cd "$ROOT/mcp-memory-server"
|
|
139
|
+
NODE_NO_WARNINGS=1 node --input-type=module - "$database" <<'NODE'
|
|
140
|
+
import { AgentFS } from "agentfs-sdk";
|
|
141
|
+
const agent = await AgentFS.open({ id: "trajectory", path: process.argv[2] });
|
|
142
|
+
try {
|
|
143
|
+
const rows = await agent.kv.list("capability-callback/v1/record/");
|
|
144
|
+
process.stdout.write(JSON.stringify(rows.map((row) => row.value)));
|
|
145
|
+
} finally {
|
|
146
|
+
await agent.close();
|
|
147
|
+
}
|
|
148
|
+
NODE
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
pending_rows() {
|
|
153
|
+
local project="$1"
|
|
154
|
+
local database="$project/.agentfs/trajectory.db"
|
|
155
|
+
[[ -f "$database" ]] || { printf '[]'; return; }
|
|
156
|
+
(
|
|
157
|
+
cd "$ROOT/mcp-memory-server"
|
|
158
|
+
NODE_NO_WARNINGS=1 node --input-type=module - "$database" <<'NODE'
|
|
159
|
+
import { AgentFS } from "agentfs-sdk";
|
|
160
|
+
const agent = await AgentFS.open({ id: "trajectory", path: process.argv[2] });
|
|
161
|
+
try {
|
|
162
|
+
const rows = await agent.kv.list("capability-callback/v1/pending/");
|
|
163
|
+
process.stdout.write(JSON.stringify(rows.map((row) => row.value)));
|
|
164
|
+
} finally {
|
|
165
|
+
await agent.close();
|
|
166
|
+
}
|
|
167
|
+
NODE
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
assert_value_free_rows() {
|
|
172
|
+
local rows="$1"
|
|
173
|
+
local expected_outcome="$2"
|
|
174
|
+
node - "$rows" "$expected_outcome" <<'NODE'
|
|
175
|
+
const rows = JSON.parse(process.argv[2]);
|
|
176
|
+
const expected = process.argv[3];
|
|
177
|
+
if (rows.length !== 1 || rows[0].outcome !== expected) process.exit(1);
|
|
178
|
+
const forbidden = ["argument", "result", "prompt", "query", "path", "stack", "error_message", "secret"];
|
|
179
|
+
const raw = JSON.stringify(rows[0]).toLowerCase();
|
|
180
|
+
if (forbidden.some((name) => raw.includes(name))) process.exit(1);
|
|
181
|
+
NODE
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
assert_opencode_result() {
|
|
185
|
+
local result="$1"
|
|
186
|
+
local expected="$2"
|
|
187
|
+
node - "$result" "$expected" <<'NODE'
|
|
188
|
+
const value = JSON.parse(process.argv[2]);
|
|
189
|
+
const expected = process.argv[3];
|
|
190
|
+
const admission = value.calls.find((call) => call.boundary === "command.execute.before");
|
|
191
|
+
if (!admission || admission.result !== expected) process.exit(1);
|
|
192
|
+
if (expected === "blocked" && admission.error !== "Cairn capability disabled.") process.exit(1);
|
|
193
|
+
if (expected === "allowed" && admission.parts !== 0) process.exit(1);
|
|
194
|
+
NODE
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
run_opencode_scenario() {
|
|
198
|
+
local plugin="$1"
|
|
199
|
+
local project="$2"
|
|
200
|
+
local state_root="$3"
|
|
201
|
+
local scenario="$4"
|
|
202
|
+
local session="$5"
|
|
203
|
+
local trajectory="${6:-1}"
|
|
204
|
+
local non_awaited="${7:-0}"
|
|
205
|
+
(
|
|
206
|
+
cd "$project"
|
|
207
|
+
env \
|
|
208
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
209
|
+
CAIRN_HARNESS_STATE_DIR="$state_root" \
|
|
210
|
+
CAIRN_TRAJECTORY_CAPTURE="$trajectory" \
|
|
211
|
+
CAIRN_TEST_OPENCODE_NON_AWAITED="$non_awaited" \
|
|
212
|
+
CAIRN_TEST_SESSION="$session" \
|
|
213
|
+
node --experimental-strip-types "$OPENCODE_HARNESS" \
|
|
214
|
+
"$plugin" "$project" "$FIXTURE" "$scenario"
|
|
215
|
+
)
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
opencode_plugin() {
|
|
219
|
+
local temp_root plugin result rows case_root project decoy state_root
|
|
220
|
+
temp_root=$(mktemp -d)
|
|
221
|
+
trap "rm -rf '$temp_root'" EXIT
|
|
222
|
+
plugin="$temp_root/capability-command.ts"
|
|
223
|
+
|
|
224
|
+
validate_fixture
|
|
225
|
+
[[ -f "$OPENCODE_PLUGIN" ]] || fail "OpenCode capability plugin is absent"
|
|
226
|
+
sed "s|@@INFRA_ROOT@@|$ROOT|g" "$OPENCODE_PLUGIN" > "$plugin"
|
|
227
|
+
|
|
228
|
+
result=$(node --experimental-strip-types "$OPENCODE_HARNESS" \
|
|
229
|
+
"$plugin" "$temp_root/contract-project" "$FIXTURE" contract)
|
|
230
|
+
node - "$result" <<'NODE' || fail "OpenCode implementation disagrees with the pinned contract"
|
|
231
|
+
const value = JSON.parse(process.argv[2]);
|
|
232
|
+
if (value.fixture.version !== "1.17.20") process.exit(1);
|
|
233
|
+
if (value.fixture.commit !== "4473fc3c9055046183990a965d68df3db7ea6f62") process.exit(1);
|
|
234
|
+
if (value.calls.length !== 0) process.exit(1);
|
|
235
|
+
NODE
|
|
236
|
+
|
|
237
|
+
case_root="$temp_root/malformed"
|
|
238
|
+
project="$case_root/project"
|
|
239
|
+
decoy="$project-decoy"
|
|
240
|
+
state_root="$case_root/state"
|
|
241
|
+
mkdir -p "$project" "$decoy"
|
|
242
|
+
write_capability_config "$project" true true
|
|
243
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" malformed-admission malformed 1)
|
|
244
|
+
assert_opencode_result "$result" blocked || fail "malformed OpenCode admission did not fail closed"
|
|
245
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "malformed OpenCode admission created callback state"
|
|
246
|
+
[[ ! -e "$state_root/capability-leases-v1" ]] || fail "malformed OpenCode admission created lease state"
|
|
247
|
+
|
|
248
|
+
case_root="$temp_root/identity"
|
|
249
|
+
project="$case_root/project"
|
|
250
|
+
decoy="$project-decoy"
|
|
251
|
+
state_root="$case_root/state"
|
|
252
|
+
mkdir -p "$project" "$decoy"
|
|
253
|
+
write_capability_config "$project" true true
|
|
254
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" identity-mismatch identity 1)
|
|
255
|
+
assert_opencode_result "$result" blocked || fail "ambiguous OpenCode project identity did not fail closed"
|
|
256
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "ambiguous OpenCode identity created callback state"
|
|
257
|
+
|
|
258
|
+
for consent in false:1 true:0; do
|
|
259
|
+
local logging=${consent%%:*}
|
|
260
|
+
local trajectory=${consent#*:}
|
|
261
|
+
case_root="$temp_root/enabled-$logging-$trajectory"
|
|
262
|
+
project="$case_root/project"
|
|
263
|
+
decoy="$project-decoy"
|
|
264
|
+
state_root="$case_root/state"
|
|
265
|
+
mkdir -p "$project" "$decoy"
|
|
266
|
+
write_capability_config "$project" true "$logging"
|
|
267
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" admission "enabled-$logging-$trajectory" "$trajectory")
|
|
268
|
+
assert_opencode_result "$result" allowed || fail "enabled unmeasured OpenCode command changed owner execution"
|
|
269
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "enabled unmeasured OpenCode command created callback state"
|
|
270
|
+
[[ ! -e "$state_root/capability-leases-v1" ]] || fail "enabled unmeasured OpenCode command created lease state"
|
|
271
|
+
done
|
|
272
|
+
|
|
273
|
+
for consent in false:1 true:0; do
|
|
274
|
+
local logging=${consent%%:*}
|
|
275
|
+
local trajectory=${consent#*:}
|
|
276
|
+
case_root="$temp_root/disabled-$logging-$trajectory"
|
|
277
|
+
project="$case_root/project"
|
|
278
|
+
decoy="$project-decoy"
|
|
279
|
+
state_root="$case_root/state"
|
|
280
|
+
mkdir -p "$project" "$decoy"
|
|
281
|
+
write_capability_config "$project" false "$logging"
|
|
282
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" admission "disabled-$logging-$trajectory" "$trajectory")
|
|
283
|
+
assert_opencode_result "$result" blocked || fail "disabled unmeasured OpenCode command did not return the fixed block"
|
|
284
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "disabled unmeasured OpenCode command created callback state"
|
|
285
|
+
find "$state_root/capability-leases-v1" -type f -print -quit 2>/dev/null | grep -q . && \
|
|
286
|
+
fail "disabled unmeasured OpenCode command left lease state"
|
|
287
|
+
done
|
|
288
|
+
|
|
289
|
+
case_root="$temp_root/disabled-measured"
|
|
290
|
+
project="$case_root/project"
|
|
291
|
+
decoy="$project-decoy"
|
|
292
|
+
state_root="$case_root/state"
|
|
293
|
+
mkdir -p "$project" "$decoy"
|
|
294
|
+
write_capability_config "$project" false true
|
|
295
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" admission disabled-measured 1)
|
|
296
|
+
assert_opencode_result "$result" blocked || fail "disabled measured OpenCode command did not return the fixed block"
|
|
297
|
+
rows=$(callback_rows "$project")
|
|
298
|
+
assert_value_free_rows "$rows" disabled || fail "disabled OpenCode command did not settle one value-free final"
|
|
299
|
+
|
|
300
|
+
for terminal in success error; do
|
|
301
|
+
case_root="$temp_root/$terminal"
|
|
302
|
+
project="$case_root/project"
|
|
303
|
+
decoy="$project-decoy"
|
|
304
|
+
state_root="$case_root/state"
|
|
305
|
+
mkdir -p "$project" "$decoy"
|
|
306
|
+
write_capability_config "$project" true true
|
|
307
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" "$terminal" "terminal-$terminal" 1)
|
|
308
|
+
assert_opencode_result "$result" allowed || fail "OpenCode $terminal terminal changed owner execution"
|
|
309
|
+
rows=$(callback_rows "$project")
|
|
310
|
+
assert_value_free_rows "$rows" "$terminal" || fail "OpenCode $terminal terminal did not settle exactly once"
|
|
311
|
+
done
|
|
312
|
+
|
|
313
|
+
case_root="$temp_root/non-awaited-success"
|
|
314
|
+
project="$case_root/project"
|
|
315
|
+
decoy="$project-decoy"
|
|
316
|
+
state_root="$case_root/state"
|
|
317
|
+
mkdir -p "$project" "$decoy"
|
|
318
|
+
write_capability_config "$project" true true
|
|
319
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" success non-awaited-success 1 1)
|
|
320
|
+
assert_opencode_result "$result" allowed || fail "non-awaited OpenCode terminal changed owner execution"
|
|
321
|
+
rows=$(callback_rows "$project")
|
|
322
|
+
assert_value_free_rows "$rows" success || fail "non-awaited OpenCode terminal did not settle before return"
|
|
323
|
+
|
|
324
|
+
for terminal in success error; do
|
|
325
|
+
case_root="$temp_root/duplicate-$terminal"
|
|
326
|
+
project="$case_root/project"
|
|
327
|
+
decoy="$project-decoy"
|
|
328
|
+
state_root="$case_root/state"
|
|
329
|
+
mkdir -p "$project" "$decoy"
|
|
330
|
+
write_capability_config "$project" true true
|
|
331
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" "duplicate-$terminal" "duplicate-$terminal" 1)
|
|
332
|
+
assert_opencode_result "$result" allowed || fail "duplicate OpenCode $terminal changed owner execution"
|
|
333
|
+
rows=$(callback_rows "$project")
|
|
334
|
+
assert_value_free_rows "$rows" "$terminal" || fail "duplicate OpenCode $terminal delivery was not idempotent"
|
|
335
|
+
done
|
|
336
|
+
|
|
337
|
+
case_root="$temp_root/settled-delete"
|
|
338
|
+
project="$case_root/project"
|
|
339
|
+
decoy="$project-decoy"
|
|
340
|
+
state_root="$case_root/state"
|
|
341
|
+
mkdir -p "$project" "$decoy"
|
|
342
|
+
write_capability_config "$project" true true
|
|
343
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" settled-then-delete settled-delete 1)
|
|
344
|
+
assert_opencode_result "$result" allowed || fail "settled OpenCode command changed owner execution"
|
|
345
|
+
rows=$(callback_rows "$project")
|
|
346
|
+
assert_value_free_rows "$rows" success || fail "session deletion replaced a settled OpenCode terminal"
|
|
347
|
+
|
|
348
|
+
case_root="$temp_root/abandonment"
|
|
349
|
+
project="$case_root/project"
|
|
350
|
+
decoy="$project-decoy"
|
|
351
|
+
state_root="$case_root/state"
|
|
352
|
+
mkdir -p "$project" "$decoy"
|
|
353
|
+
write_capability_config "$project" true true
|
|
354
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" abandonment abandonment 1)
|
|
355
|
+
assert_opencode_result "$result" allowed || fail "OpenCode abandonment changed owner execution"
|
|
356
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "OpenCode abandonment created a final callback"
|
|
357
|
+
find "$state_root/capability-leases-v1" -type f -print -quit 2>/dev/null | grep -q . && \
|
|
358
|
+
fail "OpenCode abandonment left an unfinished recoverable lease"
|
|
359
|
+
|
|
360
|
+
case_root="$temp_root/cwd-drift"
|
|
361
|
+
project="$case_root/project"
|
|
362
|
+
decoy="$project-decoy"
|
|
363
|
+
state_root="$case_root/state"
|
|
364
|
+
mkdir -p "$project" "$decoy"
|
|
365
|
+
write_capability_config "$project" true true
|
|
366
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" cwd-drift-success cwd-drift 1)
|
|
367
|
+
assert_opencode_result "$result" allowed || fail "OpenCode cwd drift changed owner execution"
|
|
368
|
+
rows=$(callback_rows "$project")
|
|
369
|
+
assert_value_free_rows "$rows" success || fail "OpenCode cwd drift rebound immutable project identity"
|
|
370
|
+
[[ "$(callback_rows "$decoy")" == '[]' ]] || fail "OpenCode cwd drift wrote callback state to the decoy project"
|
|
371
|
+
|
|
372
|
+
echo "PASS: OpenCode native capability plugin"
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
assert_dispose_completed() {
|
|
376
|
+
local result="$1"
|
|
377
|
+
node - "$result" <<'NODE'
|
|
378
|
+
const value = JSON.parse(process.argv[2]);
|
|
379
|
+
if (!value.dispose_present) process.exit(1);
|
|
380
|
+
const disposal = value.calls.filter((call) => call.boundary === "dispose");
|
|
381
|
+
if (disposal.length !== 1 || disposal[0].result !== "completed") process.exit(1);
|
|
382
|
+
NODE
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
write_disposal_coordinator_fixture() {
|
|
386
|
+
local fixture_root="$1"
|
|
387
|
+
mkdir -p "$fixture_root/mcp-memory-server/dist"
|
|
388
|
+
cat > "$fixture_root/mcp-memory-server/dist/capability-cli.js" <<'NODE'
|
|
389
|
+
#!/usr/bin/env node
|
|
390
|
+
const fs = require("node:fs");
|
|
391
|
+
const operation = process.argv[2];
|
|
392
|
+
const chunks = [];
|
|
393
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
394
|
+
process.stdin.on("end", () => {
|
|
395
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
396
|
+
fs.appendFileSync(process.env.CAIRN_BOUNDARY_TRACE, `${JSON.stringify({ operation, payload })}\n`);
|
|
397
|
+
const respond = () => {
|
|
398
|
+
if (operation === "harness-before") process.stdout.write('{"schema_version":1,"decision":"allow"}');
|
|
399
|
+
else process.stdout.write('{"schema_version":1,"finalized":true}');
|
|
400
|
+
};
|
|
401
|
+
if (operation === "harness-terminal" && process.env.CAIRN_TEST_HOLD_TERMINAL === "1") {
|
|
402
|
+
const timer = setInterval(() => {
|
|
403
|
+
if (!fs.existsSync(process.env.CAIRN_TEST_RELEASE_FILE)) return;
|
|
404
|
+
clearInterval(timer);
|
|
405
|
+
respond();
|
|
406
|
+
}, 10);
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
respond();
|
|
410
|
+
});
|
|
411
|
+
NODE
|
|
412
|
+
chmod 700 "$fixture_root/mcp-memory-server/dist/capability-cli.js"
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
assert_disposal_trace() {
|
|
416
|
+
local trace="$1"
|
|
417
|
+
local scenario="$2"
|
|
418
|
+
node - "$trace" "$scenario" <<'NODE'
|
|
419
|
+
const fs = require("node:fs");
|
|
420
|
+
const [tracePath, scenario] = process.argv.slice(2);
|
|
421
|
+
const raw = fs.readFileSync(tracePath, "utf8").trim();
|
|
422
|
+
const rows = raw ? raw.split(/\n/).map(JSON.parse) : [];
|
|
423
|
+
const expected = scenario === "dispose-terminal-race"
|
|
424
|
+
? ["harness-before", "harness-terminal"]
|
|
425
|
+
: ["harness-before", "harness-terminal", "harness-before"];
|
|
426
|
+
if (rows.map(({ operation }) => operation).join(",") !== expected.join(",")) process.exit(1);
|
|
427
|
+
for (const row of rows) {
|
|
428
|
+
const keys = Object.keys(row.payload).sort().join(",");
|
|
429
|
+
const allowed = row.operation === "harness-before"
|
|
430
|
+
? "command,harness,project_root,schema_version,session_id"
|
|
431
|
+
: "harness,outcome,schema_version,session_id";
|
|
432
|
+
if (keys !== allowed) process.exit(1);
|
|
433
|
+
if (row.payload.harness !== "opencode" || row.payload.schema_version !== 1) process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
const terminals = rows.filter(({ operation }) => operation === "harness-terminal");
|
|
436
|
+
if (terminals.length !== 1 || terminals[0].payload.outcome === "abandoned") process.exit(1);
|
|
437
|
+
if (scenario === "dispose-terminal-race" && terminals[0].payload.outcome !== "success") process.exit(1);
|
|
438
|
+
if (scenario === "terminal-next-admission" && terminals[0].payload.outcome !== "error") process.exit(1);
|
|
439
|
+
NODE
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
opencode_disposal() {
|
|
443
|
+
local temp_root plugin result case_root project state_root rows fixture_root trace release scenario expected
|
|
444
|
+
temp_root=$(mktemp -d)
|
|
445
|
+
trap "rm -rf '$temp_root'" EXIT
|
|
446
|
+
plugin="$temp_root/capability-command.ts"
|
|
447
|
+
|
|
448
|
+
validate_fixture
|
|
449
|
+
sed "s|@@INFRA_ROOT@@|$ROOT|g" "$OPENCODE_PLUGIN" > "$plugin"
|
|
450
|
+
mkdir -p "$temp_root/contract-project"
|
|
451
|
+
result=$(node --experimental-strip-types "$OPENCODE_HARNESS" \
|
|
452
|
+
"$plugin" "$temp_root/contract-project" "$FIXTURE" dispose-contract)
|
|
453
|
+
if ! node - "$result" <<'NODE'
|
|
454
|
+
const value = JSON.parse(process.argv[2]);
|
|
455
|
+
process.exit(value.dispose_present ? 0 : 1);
|
|
456
|
+
NODE
|
|
457
|
+
then
|
|
458
|
+
echo "PHASE18_RED:OPENCODE_DISPOSAL"
|
|
459
|
+
return 86
|
|
460
|
+
fi
|
|
461
|
+
assert_dispose_completed "$result" || fail "OpenCode dispose contract did not complete"
|
|
462
|
+
|
|
463
|
+
for scenario in dispose-unfinished dispose-multiple; do
|
|
464
|
+
case_root="$temp_root/$scenario"
|
|
465
|
+
project="$case_root/project"
|
|
466
|
+
state_root="$case_root/state"
|
|
467
|
+
mkdir -p "$project"
|
|
468
|
+
write_capability_config "$project" true true
|
|
469
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" "$scenario" "$scenario" 1)
|
|
470
|
+
assert_dispose_completed "$result" || fail "$scenario did not invoke native disposal"
|
|
471
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "$scenario appended an abandonment final"
|
|
472
|
+
[[ "$(pending_rows "$project")" == '[]' ]] || fail "$scenario left pending issuance"
|
|
473
|
+
find "$state_root/capability-leases-v1" -type f -print -quit 2>/dev/null | grep -q . && \
|
|
474
|
+
fail "$scenario left a recoverable lease"
|
|
475
|
+
done
|
|
476
|
+
|
|
477
|
+
for scenario in dispose-settled-success dispose-settled-error; do
|
|
478
|
+
case_root="$temp_root/$scenario"
|
|
479
|
+
project="$case_root/project"
|
|
480
|
+
state_root="$case_root/state"
|
|
481
|
+
mkdir -p "$project"
|
|
482
|
+
write_capability_config "$project" true true
|
|
483
|
+
result=$(run_opencode_scenario "$plugin" "$project" "$state_root" "$scenario" "$scenario" 1)
|
|
484
|
+
assert_dispose_completed "$result" || fail "$scenario did not invoke native disposal"
|
|
485
|
+
rows=$(callback_rows "$project")
|
|
486
|
+
[[ "$scenario" == dispose-settled-success ]] && expected=success || expected=error
|
|
487
|
+
assert_value_free_rows "$rows" "$expected" || fail "$scenario rewrote or duplicated its settled final"
|
|
488
|
+
[[ "$(pending_rows "$project")" == '[]' ]] || fail "$scenario left pending issuance"
|
|
489
|
+
find "$state_root/capability-leases-v1" -type f -print -quit 2>/dev/null | grep -q . && \
|
|
490
|
+
fail "$scenario left a recoverable lease"
|
|
491
|
+
done
|
|
492
|
+
|
|
493
|
+
fixture_root="$temp_root/fixture-infra"
|
|
494
|
+
write_disposal_coordinator_fixture "$fixture_root"
|
|
495
|
+
sed "s|@@INFRA_ROOT@@|$fixture_root|g" "$OPENCODE_PLUGIN" > "$plugin"
|
|
496
|
+
project="$temp_root/mock-project"
|
|
497
|
+
mkdir -p "$project"
|
|
498
|
+
|
|
499
|
+
trace="$temp_root/master-off.trace"
|
|
500
|
+
: > "$trace"
|
|
501
|
+
result=$(CAIRN_CAPABILITY_CONTRACT=0 CAIRN_BOUNDARY_TRACE="$trace" \
|
|
502
|
+
CAIRN_TEST_SESSION=dispose-master-off \
|
|
503
|
+
node --experimental-strip-types "$OPENCODE_HARNESS" \
|
|
504
|
+
"$plugin" "$project" "$FIXTURE" dispose-master-off)
|
|
505
|
+
assert_dispose_completed "$result" || fail "master-off disposal did not return cleanly"
|
|
506
|
+
[[ ! -s "$trace" ]] || fail "master-off disposal invoked the coordinator"
|
|
507
|
+
|
|
508
|
+
for scenario in dispose-terminal-race terminal-next-admission; do
|
|
509
|
+
trace="$temp_root/$scenario.trace"
|
|
510
|
+
release="$temp_root/$scenario.release"
|
|
511
|
+
: > "$trace"
|
|
512
|
+
rm -f "$release"
|
|
513
|
+
result=$(CAIRN_CAPABILITY_CONTRACT=1 CAIRN_BOUNDARY_TRACE="$trace" \
|
|
514
|
+
CAIRN_TEST_HOLD_TERMINAL=1 CAIRN_TEST_RELEASE_FILE="$release" \
|
|
515
|
+
CAIRN_TEST_SESSION="$scenario" \
|
|
516
|
+
node --experimental-strip-types "$OPENCODE_HARNESS" \
|
|
517
|
+
"$plugin" "$project" "$FIXTURE" "$scenario")
|
|
518
|
+
if [[ "$scenario" == dispose-terminal-race ]]; then
|
|
519
|
+
assert_dispose_completed "$result" || fail "terminal/dispose race did not complete disposal"
|
|
520
|
+
fi
|
|
521
|
+
assert_disposal_trace "$trace" "$scenario" || fail "$scenario violated per-session ordering"
|
|
522
|
+
done
|
|
523
|
+
|
|
524
|
+
echo "PASS: OpenCode disposal and per-session ordering contract"
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
plugin_tree_digest() {
|
|
528
|
+
local live_root="$1"
|
|
529
|
+
(
|
|
530
|
+
cd "$live_root"
|
|
531
|
+
find plugins -type f -print | LC_ALL=C sort | while IFS= read -r asset; do
|
|
532
|
+
sha256sum "$asset"
|
|
533
|
+
done
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
assert_legacy_opencode_plugins() {
|
|
538
|
+
local live_root="$1"
|
|
539
|
+
local asset
|
|
540
|
+
for asset in memory-wakeup.ts memory-capture.ts memory-recall.ts; do
|
|
541
|
+
[[ -f "$live_root/plugins/$asset" ]] || fail "legacy OpenCode plugin is missing: $asset"
|
|
542
|
+
cmp -s <(sed "s|@@INFRA_ROOT@@|$ROOT|g" "$ROOT/opencode/plugins/$asset") \
|
|
543
|
+
"$live_root/plugins/$asset" || fail "legacy OpenCode plugin changed: $asset"
|
|
544
|
+
done
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
opencode_sync_modes() {
|
|
548
|
+
local temp_root normal_root enabled_root before after plugin_count
|
|
549
|
+
temp_root=$(mktemp -d)
|
|
550
|
+
trap "rm -rf '$temp_root'" EXIT
|
|
551
|
+
normal_root="$temp_root/normal"
|
|
552
|
+
enabled_root="$temp_root/enabled"
|
|
553
|
+
|
|
554
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
555
|
+
CAIRN_HARNESS_STATE_DIR="$temp_root/normal-state" \
|
|
556
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --live-root "$normal_root" >/dev/null
|
|
557
|
+
assert_legacy_opencode_plugins "$normal_root"
|
|
558
|
+
assert_no_native_opencode_assets "$normal_root"
|
|
559
|
+
before=$(plugin_tree_digest "$normal_root")
|
|
560
|
+
[[ ! -e "$temp_root/normal-state" ]] || fail "normal OpenCode sync invoked capability state"
|
|
561
|
+
|
|
562
|
+
env -u CAIRN_CAPABILITY_CONTRACT \
|
|
563
|
+
CAIRN_HARNESS_STATE_DIR="$temp_root/master-off-state" \
|
|
564
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --capability-overlay --live-root "$normal_root" >/dev/null
|
|
565
|
+
after=$(plugin_tree_digest "$normal_root")
|
|
566
|
+
[[ "$after" == "$before" ]] || fail "master-off OpenCode overlay changed legacy plugin bytes"
|
|
567
|
+
assert_no_native_opencode_assets "$normal_root"
|
|
568
|
+
[[ ! -e "$temp_root/master-off-state" ]] || fail "master-off OpenCode overlay invoked capability state"
|
|
569
|
+
|
|
570
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
571
|
+
CAIRN_HARNESS_STATE_DIR="$temp_root/enabled-state" \
|
|
572
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --apply --capability-overlay --live-root "$enabled_root" >/dev/null
|
|
573
|
+
assert_legacy_opencode_plugins "$enabled_root"
|
|
574
|
+
plugin_count=$(find "$enabled_root/plugins" -maxdepth 1 -type f -name '*.ts' | wc -l | tr -d ' ')
|
|
575
|
+
[[ "$plugin_count" -eq 4 ]] || fail "enabled OpenCode overlay did not install exactly one additional plugin"
|
|
576
|
+
[[ -f "$enabled_root/plugins/capability-command.ts" ]] || fail "enabled OpenCode overlay did not register the capability plugin"
|
|
577
|
+
cmp -s <(sed "s|@@INFRA_ROOT@@|$ROOT|g" "$OPENCODE_PLUGIN") \
|
|
578
|
+
"$enabled_root/plugins/capability-command.ts" || fail "enabled OpenCode capability plugin bytes do not match source"
|
|
579
|
+
grep -qF 'export const CapabilityCommandPlugin' "$enabled_root/plugins/capability-command.ts" || \
|
|
580
|
+
fail "enabled OpenCode capability plugin registration is absent"
|
|
581
|
+
[[ ! -e "$temp_root/enabled-state" ]] || fail "enabled OpenCode sync invoked the capability coordinator"
|
|
582
|
+
|
|
583
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
584
|
+
"$ROOT/scripts/sync-opencode-plugin-assets.sh" --check --capability-overlay --live-root "$enabled_root" >/dev/null
|
|
585
|
+
|
|
586
|
+
echo "PASS: OpenCode capability plugin sync modes"
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
claude_hooks() {
|
|
590
|
+
local temp_root project decoy state_root start_hook finish_hook stdout_file stderr_file status payload rows
|
|
591
|
+
temp_root=$(mktemp -d)
|
|
592
|
+
trap "rm -rf '$temp_root'" EXIT
|
|
593
|
+
project="$temp_root/project"
|
|
594
|
+
decoy="$temp_root/decoy"
|
|
595
|
+
state_root="$temp_root/state"
|
|
596
|
+
start_hook="$temp_root/capability-command-start.sh"
|
|
597
|
+
finish_hook="$temp_root/capability-command-finish.sh"
|
|
598
|
+
stdout_file="$temp_root/stdout"
|
|
599
|
+
stderr_file="$temp_root/stderr"
|
|
600
|
+
mkdir -p "$project" "$decoy"
|
|
601
|
+
|
|
602
|
+
validate_fixture
|
|
603
|
+
[[ -f "$CLAUDE_START" ]] || fail "Claude capability start hook is absent"
|
|
604
|
+
[[ -f "$CLAUDE_FINISH" ]] || fail "Claude capability finish hook is absent"
|
|
605
|
+
install_claude_hook_fixture "$CLAUDE_START" "$start_hook"
|
|
606
|
+
install_claude_hook_fixture "$CLAUDE_FINISH" "$finish_hook"
|
|
607
|
+
|
|
608
|
+
status=0
|
|
609
|
+
run_claude_hook "$start_hook" "$project" "$state_root" '{bad-json' "$stdout_file" "$stderr_file" || status=$?
|
|
610
|
+
assert_fixed_block "$stdout_file" "$stderr_file" "$status"
|
|
611
|
+
|
|
612
|
+
payload=$(node - "$FIXTURE" "$project" <<'NODE'
|
|
613
|
+
const fixture = require(process.argv[2]);
|
|
614
|
+
const value = structuredClone(fixture.claude.events.UserPromptExpansion.sample);
|
|
615
|
+
value.cwd = process.argv[3];
|
|
616
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
617
|
+
value.command_name = "wiki-query";
|
|
618
|
+
value.session_id = "claude-enabled-no-measurement";
|
|
619
|
+
process.stdout.write(JSON.stringify(value));
|
|
620
|
+
NODE
|
|
621
|
+
)
|
|
622
|
+
status=0
|
|
623
|
+
run_claude_hook "$start_hook" "$decoy" "$state_root" "$payload" "$stdout_file" "$stderr_file" || status=$?
|
|
624
|
+
assert_fixed_block "$stdout_file" "$stderr_file" "$status"
|
|
625
|
+
|
|
626
|
+
write_capability_config "$project" true false
|
|
627
|
+
run_claude_hook "$start_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || \
|
|
628
|
+
fail "enabled unmeasured Claude command was changed"
|
|
629
|
+
[[ "$(cat "$stdout_file")" == '{}' ]] || fail "enabled Claude command was not passed through unchanged"
|
|
630
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "enabled unmeasured Claude command created callback state"
|
|
631
|
+
[[ ! -e "$state_root/capability-leases-v1" ]] || fail "enabled unmeasured Claude command created lease state"
|
|
632
|
+
|
|
633
|
+
for consent in false:1 true:0; do
|
|
634
|
+
local logging=${consent%%:*}
|
|
635
|
+
local trajectory=${consent#*:}
|
|
636
|
+
write_capability_config "$project" false "$logging"
|
|
637
|
+
payload=$(node - "$FIXTURE" "$project" "$logging" <<'NODE'
|
|
638
|
+
const fixture = require(process.argv[2]);
|
|
639
|
+
const value = structuredClone(fixture.claude.events.UserPromptExpansion.sample);
|
|
640
|
+
value.cwd = process.argv[3];
|
|
641
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
642
|
+
value.session_id = `claude-disabled-unmeasured-${process.argv[4]}`;
|
|
643
|
+
process.stdout.write(JSON.stringify(value));
|
|
644
|
+
NODE
|
|
645
|
+
)
|
|
646
|
+
status=0
|
|
647
|
+
run_claude_hook "$start_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" "$trajectory" || status=$?
|
|
648
|
+
assert_fixed_block "$stdout_file" "$stderr_file" "$status"
|
|
649
|
+
[[ "$(callback_rows "$project")" == '[]' ]] || fail "disabled unmeasured Claude command created callback state"
|
|
650
|
+
done
|
|
651
|
+
|
|
652
|
+
write_capability_config "$project" false true
|
|
653
|
+
payload=$(node - "$FIXTURE" "$project" <<'NODE'
|
|
654
|
+
const fixture = require(process.argv[2]);
|
|
655
|
+
const value = structuredClone(fixture.claude.events.UserPromptExpansion.sample);
|
|
656
|
+
value.cwd = process.argv[3];
|
|
657
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
658
|
+
value.session_id = "claude-disabled-measured";
|
|
659
|
+
process.stdout.write(JSON.stringify(value));
|
|
660
|
+
NODE
|
|
661
|
+
)
|
|
662
|
+
status=0
|
|
663
|
+
run_claude_hook "$start_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || status=$?
|
|
664
|
+
assert_fixed_block "$stdout_file" "$stderr_file" "$status"
|
|
665
|
+
rows=$(callback_rows "$project")
|
|
666
|
+
assert_value_free_rows "$rows" disabled || fail "disabled Claude command did not settle one value-free final"
|
|
667
|
+
|
|
668
|
+
write_capability_config "$project" true true
|
|
669
|
+
for terminal in Stop StopFailure; do
|
|
670
|
+
local session
|
|
671
|
+
case "$terminal" in
|
|
672
|
+
Stop) session="claude-stop" ;;
|
|
673
|
+
StopFailure) session="claude-stopfailure" ;;
|
|
674
|
+
esac
|
|
675
|
+
payload=$(node - "$FIXTURE" "$project" "$session" <<'NODE'
|
|
676
|
+
const fixture = require(process.argv[2]);
|
|
677
|
+
const value = structuredClone(fixture.claude.events.UserPromptExpansion.sample);
|
|
678
|
+
value.cwd = process.argv[3];
|
|
679
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
680
|
+
value.session_id = process.argv[4];
|
|
681
|
+
process.stdout.write(JSON.stringify(value));
|
|
682
|
+
NODE
|
|
683
|
+
)
|
|
684
|
+
run_claude_hook "$start_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || \
|
|
685
|
+
fail "Claude start failed for $terminal"
|
|
686
|
+
payload=$(node - "$FIXTURE" "$project" "$session" "$terminal" <<'NODE'
|
|
687
|
+
const fixture = require(process.argv[2]);
|
|
688
|
+
const value = structuredClone(fixture.claude.events[process.argv[5]].sample);
|
|
689
|
+
value.cwd = process.argv[3];
|
|
690
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
691
|
+
value.session_id = process.argv[4];
|
|
692
|
+
process.stdout.write(JSON.stringify(value));
|
|
693
|
+
NODE
|
|
694
|
+
)
|
|
695
|
+
run_claude_hook "$finish_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || \
|
|
696
|
+
fail "Claude $terminal hook changed the owner terminal"
|
|
697
|
+
run_claude_hook "$finish_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || \
|
|
698
|
+
fail "repeated Claude $terminal hook changed the owner terminal"
|
|
699
|
+
payload=$(node - "$FIXTURE" "$project" "$session" <<'NODE'
|
|
700
|
+
const fixture = require(process.argv[2]);
|
|
701
|
+
const value = structuredClone(fixture.claude.events.SessionEnd.sample);
|
|
702
|
+
value.cwd = process.argv[3];
|
|
703
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
704
|
+
value.session_id = process.argv[4];
|
|
705
|
+
process.stdout.write(JSON.stringify(value));
|
|
706
|
+
NODE
|
|
707
|
+
)
|
|
708
|
+
run_claude_hook "$finish_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || \
|
|
709
|
+
fail "SessionEnd changed a settled $terminal owner terminal"
|
|
710
|
+
done
|
|
711
|
+
rows=$(callback_rows "$project")
|
|
712
|
+
node - "$rows" <<'NODE' || fail "Claude terminal hooks did not settle exact success/error outcomes"
|
|
713
|
+
const rows = JSON.parse(process.argv[2]);
|
|
714
|
+
const outcomes = rows.map((row) => row.outcome).sort();
|
|
715
|
+
if (outcomes.join(",") !== "disabled,error,success") process.exit(1);
|
|
716
|
+
NODE
|
|
717
|
+
|
|
718
|
+
payload=$(node - "$FIXTURE" "$project" <<'NODE'
|
|
719
|
+
const fixture = require(process.argv[2]);
|
|
720
|
+
const value = structuredClone(fixture.claude.events.UserPromptExpansion.sample);
|
|
721
|
+
value.cwd = process.argv[3];
|
|
722
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
723
|
+
value.session_id = "claude-abandon";
|
|
724
|
+
process.stdout.write(JSON.stringify(value));
|
|
725
|
+
NODE
|
|
726
|
+
)
|
|
727
|
+
run_claude_hook "$start_hook" "$project" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || fail "Claude abandonment start failed"
|
|
728
|
+
payload=$(node - "$FIXTURE" "$project" "$decoy" <<'NODE'
|
|
729
|
+
const fixture = require(process.argv[2]);
|
|
730
|
+
const value = structuredClone(fixture.claude.events.CwdChanged.sample);
|
|
731
|
+
value.cwd = process.argv[3];
|
|
732
|
+
value.old_cwd = process.argv[3];
|
|
733
|
+
value.new_cwd = process.argv[4];
|
|
734
|
+
value.session_id = "claude-abandon";
|
|
735
|
+
process.stdout.write(JSON.stringify(value));
|
|
736
|
+
NODE
|
|
737
|
+
)
|
|
738
|
+
run_claude_hook "$finish_hook" "$decoy" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || fail "Claude CwdChanged observation failed"
|
|
739
|
+
payload=$(node - "$FIXTURE" "$decoy" <<'NODE'
|
|
740
|
+
const fixture = require(process.argv[2]);
|
|
741
|
+
const value = structuredClone(fixture.claude.events.SessionEnd.sample);
|
|
742
|
+
value.cwd = process.argv[3];
|
|
743
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
744
|
+
value.session_id = "claude-abandon";
|
|
745
|
+
process.stdout.write(JSON.stringify(value));
|
|
746
|
+
NODE
|
|
747
|
+
)
|
|
748
|
+
run_claude_hook "$finish_hook" "$decoy" "$state_root" "$payload" "$stdout_file" "$stderr_file" 1 || fail "Claude SessionEnd cleanup failed"
|
|
749
|
+
[[ "$(callback_rows "$project")" == "$rows" ]] || fail "SessionEnd rewrote a settled terminal or emitted an abandonment final"
|
|
750
|
+
find "$state_root/capability-leases-v1" -type f -print -quit 2>/dev/null | grep -q . && fail "SessionEnd left an unfinished recoverable lease"
|
|
751
|
+
|
|
752
|
+
echo "PASS: Claude native capability hooks"
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
claude_owner_only() {
|
|
756
|
+
[[ ! -e "$ROOT/claude/hooks/capability-command-start.sh" ]] || fail "capability start hook entered the normal hook tree"
|
|
757
|
+
[[ ! -e "$ROOT/claude/hooks/capability-command-finish.sh" ]] || fail "capability finish hook entered the normal hook tree"
|
|
758
|
+
grep -qF 'harness-before' "$CLAUDE_START" || fail "Claude admission is not owned by the native coordinator"
|
|
759
|
+
grep -qF 'harness-terminal' "$CLAUDE_FINISH" || fail "Claude terminal settlement is not owned by the native coordinator"
|
|
760
|
+
if grep -q -E 'cairn capabilities (guard|start|finish)' "$CLAUDE_START" "$CLAUDE_FINISH"; then
|
|
761
|
+
fail "Claude native hooks delegate through obsolete model-authored operations"
|
|
762
|
+
fi
|
|
763
|
+
claude_hooks
|
|
764
|
+
echo "PASS: Claude native hook owner is isolated from the normal hook tree"
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
opencode_command_owner_only() {
|
|
768
|
+
local temp_root command
|
|
769
|
+
local -a commands=(wiki-ingest wiki-lint wiki-query graphify security-audit)
|
|
770
|
+
|
|
771
|
+
[[ -f "$OPENCODE_PLUGIN" ]] || fail "OpenCode native capability plugin is absent"
|
|
772
|
+
grep -qF '"command.execute.before"' "$OPENCODE_PLUGIN" || \
|
|
773
|
+
fail "OpenCode native plugin does not own pre-expansion admission"
|
|
774
|
+
grep -qF 'harness-before' "$OPENCODE_PLUGIN" || \
|
|
775
|
+
fail "OpenCode native admission does not delegate through the coordinator"
|
|
776
|
+
|
|
777
|
+
for command in "${commands[@]}"; do
|
|
778
|
+
[[ ! -e "$ROOT/opencode/capability-contract/command/$command.md" ]] || \
|
|
779
|
+
fail "OpenCode capability command overlay remains: $command"
|
|
780
|
+
[[ -f "$ROOT/opencode/command/$command.md" ]] || \
|
|
781
|
+
fail "legacy OpenCode command is missing: $command"
|
|
782
|
+
done
|
|
783
|
+
if grep -q -E 'cairn capabilities (guard|start|finish)|capability_handle|capability-overlay' \
|
|
784
|
+
"$ROOT/opencode/command/wiki-ingest.md" \
|
|
785
|
+
"$ROOT/opencode/command/wiki-lint.md" \
|
|
786
|
+
"$ROOT/opencode/command/wiki-query.md" \
|
|
787
|
+
"$ROOT/opencode/command/graphify.md" \
|
|
788
|
+
"$ROOT/opencode/command/security-audit.md"; then
|
|
789
|
+
fail "legacy OpenCode command contains lifecycle enforcement"
|
|
790
|
+
fi
|
|
791
|
+
|
|
792
|
+
temp_root=$(mktemp -d)
|
|
793
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
794
|
+
"$ROOT/scripts/sync-opencode-wiki-assets.sh" --apply --capability-overlay --live-root "$temp_root" >/dev/null
|
|
795
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
796
|
+
"$ROOT/scripts/sync-opencode-graphify-assets.sh" --apply --capability-overlay --live-root "$temp_root" >/dev/null
|
|
797
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
798
|
+
"$ROOT/scripts/sync-opencode-security-assets.sh" --apply --capability-overlay --live-root "$temp_root" >/dev/null
|
|
799
|
+
for command in "${commands[@]}"; do
|
|
800
|
+
cmp -s "$ROOT/opencode/command/$command.md" "$temp_root/command/$command.md" || {
|
|
801
|
+
rm -rf "$temp_root"
|
|
802
|
+
fail "enabled OpenCode sync changed the legacy command: $command"
|
|
803
|
+
}
|
|
804
|
+
done
|
|
805
|
+
rm -rf "$temp_root"
|
|
806
|
+
|
|
807
|
+
opencode_plugin
|
|
808
|
+
echo "PASS: OpenCode native plugin is the sole command lifecycle owner"
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
opencode_owner_only() {
|
|
812
|
+
local temp_root workflow
|
|
813
|
+
local -a workflows=(wiki-ingest-workflow wiki-lint-workflow wiki-query-workflow security-audit-workflow)
|
|
814
|
+
|
|
815
|
+
opencode_command_owner_only
|
|
816
|
+
for workflow in "${workflows[@]}"; do
|
|
817
|
+
[[ ! -e "$ROOT/opencode/capability-contract/workflows/$workflow.md" ]] || \
|
|
818
|
+
fail "OpenCode capability workflow overlay remains: $workflow"
|
|
819
|
+
[[ -f "$ROOT/opencode/workflows/$workflow.md" ]] || \
|
|
820
|
+
fail "legacy OpenCode workflow is missing: $workflow"
|
|
821
|
+
done
|
|
822
|
+
if grep -q -E 'cairn capabilities (guard|start|finish)|capability_handle|capability-overlay' \
|
|
823
|
+
"$ROOT/opencode/workflows/wiki-ingest-workflow.md" \
|
|
824
|
+
"$ROOT/opencode/workflows/wiki-lint-workflow.md" \
|
|
825
|
+
"$ROOT/opencode/workflows/wiki-query-workflow.md" \
|
|
826
|
+
"$ROOT/opencode/workflows/security-audit-workflow.md"; then
|
|
827
|
+
fail "legacy OpenCode workflow contains lifecycle enforcement"
|
|
828
|
+
fi
|
|
829
|
+
|
|
830
|
+
temp_root=$(mktemp -d)
|
|
831
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
832
|
+
"$ROOT/scripts/sync-opencode-wiki-assets.sh" --apply --capability-overlay --live-root "$temp_root" >/dev/null
|
|
833
|
+
CAIRN_CAPABILITY_CONTRACT=1 \
|
|
834
|
+
"$ROOT/scripts/sync-opencode-security-assets.sh" --apply --capability-overlay --live-root "$temp_root" >/dev/null
|
|
835
|
+
for workflow in "${workflows[@]}"; do
|
|
836
|
+
cmp -s "$ROOT/opencode/workflows/$workflow.md" "$temp_root/workflows/$workflow.md" || {
|
|
837
|
+
rm -rf "$temp_root"
|
|
838
|
+
fail "enabled OpenCode sync changed the legacy workflow: $workflow"
|
|
839
|
+
}
|
|
840
|
+
done
|
|
841
|
+
rm -rf "$temp_root"
|
|
842
|
+
|
|
843
|
+
echo "PASS: OpenCode native plugin is the sole lifecycle owner"
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
write_boundary_coordinator_fixture() {
|
|
847
|
+
local fixture_root="$1"
|
|
848
|
+
mkdir -p "$fixture_root/mcp-memory-server/dist"
|
|
849
|
+
cat > "$fixture_root/mcp-memory-server/dist/capability-cli.js" <<'NODE'
|
|
850
|
+
#!/usr/bin/env node
|
|
851
|
+
const fs = require("node:fs");
|
|
852
|
+
const operation = process.argv[2];
|
|
853
|
+
const chunks = [];
|
|
854
|
+
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
855
|
+
process.stdin.on("end", () => {
|
|
856
|
+
const payload = JSON.parse(Buffer.concat(chunks).toString("utf8"));
|
|
857
|
+
fs.appendFileSync(process.env.CAIRN_BOUNDARY_TRACE, `${JSON.stringify({ operation, payload })}\n`);
|
|
858
|
+
if (operation === "harness-before") {
|
|
859
|
+
process.stdout.write('{"schema_version":1,"decision":"allow"}');
|
|
860
|
+
} else {
|
|
861
|
+
process.stdout.write('{"schema_version":1,"finalized":true}');
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
NODE
|
|
865
|
+
chmod 700 "$fixture_root/mcp-memory-server/dist/capability-cli.js"
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
append_owner_expansion() {
|
|
869
|
+
local trace="$1"
|
|
870
|
+
local harness="$2"
|
|
871
|
+
local outcome="$3"
|
|
872
|
+
printf '{"operation":"owner-expanded","payload":{"harness":"%s","outcome":"%s"}}\n' \
|
|
873
|
+
"$harness" "$outcome" >> "$trace"
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
assert_boundary_trace() {
|
|
877
|
+
local trace="$1"
|
|
878
|
+
local harness="$2"
|
|
879
|
+
local outcome="$3"
|
|
880
|
+
node - "$trace" "$harness" "$outcome" <<'NODE' || fail "$harness $outcome delegate order is not observable"
|
|
881
|
+
const fs = require("node:fs");
|
|
882
|
+
const [tracePath, harness, outcome] = process.argv.slice(2);
|
|
883
|
+
const rows = fs.readFileSync(tracePath, "utf8").trim().split(/\n/).map(JSON.parse);
|
|
884
|
+
if (rows.length !== 3) process.exit(1);
|
|
885
|
+
if (rows[0].operation !== "harness-before" || rows[0].payload?.harness !== harness) process.exit(1);
|
|
886
|
+
if (rows[1].operation !== "owner-expanded" || rows[1].payload?.harness !== harness) process.exit(1);
|
|
887
|
+
if (rows[2].operation !== "harness-terminal" || rows[2].payload?.harness !== harness) process.exit(1);
|
|
888
|
+
if (rows[1].payload?.outcome !== outcome || rows[2].payload?.outcome !== outcome) process.exit(1);
|
|
889
|
+
if (rows[0].payload?.session_id !== rows[2].payload?.session_id) process.exit(1);
|
|
890
|
+
if (typeof rows[0].payload?.command !== "string" || "command" in rows[2].payload) process.exit(1);
|
|
891
|
+
NODE
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
claude_delegate_calls() {
|
|
895
|
+
local temp_root fixture_root start_hook finish_hook project trace payload stdout_file stderr_file outcome event status
|
|
896
|
+
temp_root=$(mktemp -d)
|
|
897
|
+
fixture_root="$temp_root/fixture-infra"
|
|
898
|
+
start_hook="$temp_root/capability-command-start.sh"
|
|
899
|
+
finish_hook="$temp_root/capability-command-finish.sh"
|
|
900
|
+
stdout_file="$temp_root/stdout"
|
|
901
|
+
stderr_file="$temp_root/stderr"
|
|
902
|
+
write_boundary_coordinator_fixture "$fixture_root"
|
|
903
|
+
sed "s|@@INFRA_ROOT@@|$fixture_root|g" "$CLAUDE_START" > "$start_hook"
|
|
904
|
+
sed "s|@@INFRA_ROOT@@|$fixture_root|g" "$CLAUDE_FINISH" > "$finish_hook"
|
|
905
|
+
chmod 700 "$start_hook" "$finish_hook"
|
|
906
|
+
|
|
907
|
+
for outcome in success error; do
|
|
908
|
+
project="$temp_root/project-$outcome"
|
|
909
|
+
trace="$temp_root/claude-$outcome.jsonl"
|
|
910
|
+
mkdir -p "$project"
|
|
911
|
+
: > "$trace"
|
|
912
|
+
payload=$(node - "$FIXTURE" "$project" "$outcome" <<'NODE'
|
|
913
|
+
const fixture = require(process.argv[2]);
|
|
914
|
+
const value = structuredClone(fixture.claude.events.UserPromptExpansion.sample);
|
|
915
|
+
value.cwd = process.argv[3];
|
|
916
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
917
|
+
value.command_name = "wiki-query";
|
|
918
|
+
value.session_id = `claude-boundary-${process.argv[4]}`;
|
|
919
|
+
process.stdout.write(JSON.stringify(value));
|
|
920
|
+
NODE
|
|
921
|
+
)
|
|
922
|
+
status=0
|
|
923
|
+
CAIRN_BOUNDARY_TRACE="$trace" run_claude_hook \
|
|
924
|
+
"$start_hook" "$project" "$temp_root/state-$outcome" "$payload" "$stdout_file" "$stderr_file" 1 || status=$?
|
|
925
|
+
[[ "$status" -eq 0 && "$(cat "$stdout_file")" == '{}' && ! -s "$stderr_file" ]] || \
|
|
926
|
+
fail "Claude $outcome admission did not reach the coordinator"
|
|
927
|
+
append_owner_expansion "$trace" claude-code "$outcome"
|
|
928
|
+
[[ "$outcome" == success ]] && event=Stop || event=StopFailure
|
|
929
|
+
payload=$(node - "$FIXTURE" "$project" "$outcome" "$event" <<'NODE'
|
|
930
|
+
const fixture = require(process.argv[2]);
|
|
931
|
+
const value = structuredClone(fixture.claude.events[process.argv[5]].sample);
|
|
932
|
+
value.cwd = process.argv[3];
|
|
933
|
+
value.transcript_path = `${process.argv[3]}/transcript.jsonl`;
|
|
934
|
+
value.session_id = `claude-boundary-${process.argv[4]}`;
|
|
935
|
+
process.stdout.write(JSON.stringify(value));
|
|
936
|
+
NODE
|
|
937
|
+
)
|
|
938
|
+
CAIRN_BOUNDARY_TRACE="$trace" run_claude_hook \
|
|
939
|
+
"$finish_hook" "$project" "$temp_root/state-$outcome" "$payload" "$stdout_file" "$stderr_file" 1 || \
|
|
940
|
+
fail "Claude $event changed the terminal path"
|
|
941
|
+
assert_boundary_trace "$trace" claude-code "$outcome"
|
|
942
|
+
done
|
|
943
|
+
rm -rf "$temp_root"
|
|
944
|
+
echo "PASS: Claude hooks delegate admission before owner expansion and terminal settlement after it"
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
opencode_delegate_calls() {
|
|
948
|
+
local temp_root fixture_root plugin project trace outcome
|
|
949
|
+
temp_root=$(mktemp -d)
|
|
950
|
+
fixture_root="$temp_root/fixture-infra"
|
|
951
|
+
plugin="$temp_root/capability-command.ts"
|
|
952
|
+
write_boundary_coordinator_fixture "$fixture_root"
|
|
953
|
+
sed "s|@@INFRA_ROOT@@|$fixture_root|g" "$OPENCODE_PLUGIN" > "$plugin"
|
|
954
|
+
|
|
955
|
+
for outcome in success error; do
|
|
956
|
+
project="$temp_root/project-$outcome"
|
|
957
|
+
trace="$temp_root/opencode-$outcome.jsonl"
|
|
958
|
+
mkdir -p "$project"
|
|
959
|
+
: > "$trace"
|
|
960
|
+
CAIRN_CAPABILITY_CONTRACT=1 CAIRN_BOUNDARY_TRACE="$trace" \
|
|
961
|
+
node --experimental-strip-types --input-type=module - \
|
|
962
|
+
"$plugin" "$project" "$FIXTURE" "$outcome" "$trace" <<'NODE' || \
|
|
963
|
+
fail "OpenCode $outcome fixture did not execute the native plugin"
|
|
964
|
+
import { appendFileSync, readFileSync } from "node:fs";
|
|
965
|
+
import { pathToFileURL } from "node:url";
|
|
966
|
+
const [pluginPath, project, fixturePath, outcome, trace] = process.argv.slice(2);
|
|
967
|
+
const fixture = JSON.parse(readFileSync(fixturePath, "utf8")).opencode;
|
|
968
|
+
if (fixture.version !== "1.17.20" || fixture.source.commit !== "4473fc3c9055046183990a965d68df3db7ea6f62") process.exit(1);
|
|
969
|
+
const module = await import(`${pathToFileURL(pluginPath).href}?boundary=${outcome}`);
|
|
970
|
+
const plugin = await module.CapabilityCommandPlugin({
|
|
971
|
+
client: {},
|
|
972
|
+
project: { id: "project-fixture", worktree: project },
|
|
973
|
+
directory: project,
|
|
974
|
+
worktree: project,
|
|
975
|
+
serverUrl: new URL("http://127.0.0.1:4096"),
|
|
976
|
+
$: undefined,
|
|
977
|
+
experimental_workspace: undefined,
|
|
978
|
+
});
|
|
979
|
+
const input = structuredClone(fixture.hook.input.sample);
|
|
980
|
+
const output = structuredClone(fixture.hook.output.sample);
|
|
981
|
+
input.command = "wiki-query";
|
|
982
|
+
input.sessionID = `opencode-boundary-${outcome}`;
|
|
983
|
+
await plugin[fixture.hook.name](input, output);
|
|
984
|
+
appendFileSync(trace, `${JSON.stringify({ operation: "owner-expanded", payload: { harness: "opencode", outcome } })}\n`);
|
|
985
|
+
const eventName = outcome === "success" ? "session.idle" : "session.error";
|
|
986
|
+
const event = structuredClone(fixture.events[eventName].sample);
|
|
987
|
+
event.properties.sessionID = input.sessionID;
|
|
988
|
+
await plugin.event({ event });
|
|
989
|
+
NODE
|
|
990
|
+
assert_boundary_trace "$trace" opencode "$outcome"
|
|
991
|
+
done
|
|
992
|
+
rm -rf "$temp_root"
|
|
993
|
+
echo "PASS: OpenCode 1.17.20 plugin delegates admission before owner expansion and terminal settlement after it"
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
evidence_scope() {
|
|
997
|
+
validate_fixture
|
|
998
|
+
claude_delegate_calls
|
|
999
|
+
opencode_delegate_calls
|
|
1000
|
+
printf '%s\n' 'DETERMINISTIC_BOUNDARY_EVIDENCE:{"schema_version":1,"harnesses":["claude-code","opencode"],"outcomes":["success","error"],"owner_execution":"simulated-after-admission","scope":"native-delegate-order","status":"pass"}'
|
|
1001
|
+
printf '%s\n' 'PHASE18_REQUIRED_LIVE_MATRIX:{"schema_version":1,"required_cells":56,"passing_cells":0,"status":"blocking","acceptance":false,"owner_execution":"not-claimed","replacement_plan":"18-27"}'
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
live_claude() {
|
|
1005
|
+
command -v claude >/dev/null 2>&1 || fail "Claude live control unavailable: claude is not on PATH"
|
|
1006
|
+
[[ -n "${CAIRN_PHASE18_CLAUDE_MODEL:-}" ]] || fail "Claude live control unavailable: CAIRN_PHASE18_CLAUDE_MODEL is unset"
|
|
1007
|
+
[[ -n "${CAIRN_PHASE18_LIVE_PROJECT:-}" ]] || fail "Claude live control unavailable: CAIRN_PHASE18_LIVE_PROJECT is unset"
|
|
1008
|
+
[[ -d "$CAIRN_PHASE18_LIVE_PROJECT" ]] || fail "Claude live control project does not exist"
|
|
1009
|
+
[[ -f "$CLAUDE_START" && -f "$CLAUDE_FINISH" ]] || fail "Claude native hooks are not installed in the source tree"
|
|
1010
|
+
echo "LIVE_REQUIRED:CLAUDE_HAIKU_NEGATIVE_CONTROL"
|
|
1011
|
+
return 1
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
live_opencode() {
|
|
1015
|
+
command -v opencode >/dev/null 2>&1 || fail "OpenCode live control unavailable: opencode is not on PATH"
|
|
1016
|
+
[[ -n "${CAIRN_PHASE18_OPENCODE_MODEL:-}" ]] || fail "OpenCode live control unavailable: CAIRN_PHASE18_OPENCODE_MODEL is unset"
|
|
1017
|
+
[[ -n "${CAIRN_PHASE18_LIVE_PROJECT:-}" ]] || fail "OpenCode live control unavailable: CAIRN_PHASE18_LIVE_PROJECT is unset"
|
|
1018
|
+
[[ -d "$CAIRN_PHASE18_LIVE_PROJECT" ]] || fail "OpenCode live control project does not exist"
|
|
1019
|
+
[[ -f "$OPENCODE_PLUGIN" ]] || fail "OpenCode native plugin is not installed in the source tree"
|
|
1020
|
+
echo "LIVE_REQUIRED:OPENCODE_QWEN_NEGATIVE_CONTROL"
|
|
1021
|
+
return 1
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
mode="${1:-}"
|
|
1025
|
+
case "$mode" in
|
|
1026
|
+
"")
|
|
1027
|
+
echo "SKIP: select an explicit native-boundary evidence mode"
|
|
1028
|
+
;;
|
|
1029
|
+
-h|--help)
|
|
1030
|
+
usage
|
|
1031
|
+
;;
|
|
1032
|
+
--live-claude)
|
|
1033
|
+
live_claude
|
|
1034
|
+
;;
|
|
1035
|
+
--live-opencode)
|
|
1036
|
+
live_opencode
|
|
1037
|
+
;;
|
|
1038
|
+
claude-hooks)
|
|
1039
|
+
claude_hooks
|
|
1040
|
+
;;
|
|
1041
|
+
claude-owner-only)
|
|
1042
|
+
claude_owner_only
|
|
1043
|
+
;;
|
|
1044
|
+
opencode-plugin|opencode-disposal|opencode-sync-modes|opencode-command-owner-only|opencode-owner-only|claude-delegate-calls|opencode-delegate-calls|evidence-scope)
|
|
1045
|
+
case "$mode" in
|
|
1046
|
+
opencode-plugin) opencode_plugin ;;
|
|
1047
|
+
opencode-disposal) opencode_disposal ;;
|
|
1048
|
+
opencode-sync-modes) opencode_sync_modes ;;
|
|
1049
|
+
opencode-command-owner-only) opencode_command_owner_only ;;
|
|
1050
|
+
opencode-owner-only) opencode_owner_only ;;
|
|
1051
|
+
claude-delegate-calls) claude_delegate_calls ;;
|
|
1052
|
+
opencode-delegate-calls) opencode_delegate_calls ;;
|
|
1053
|
+
evidence-scope) evidence_scope ;;
|
|
1054
|
+
esac
|
|
1055
|
+
;;
|
|
1056
|
+
*)
|
|
1057
|
+
usage >&2
|
|
1058
|
+
exit 2
|
|
1059
|
+
;;
|
|
1060
|
+
esac
|