cowork-harness 0.21.0 → 0.22.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.
Files changed (72) hide show
  1. package/.claude/skills/cowork-harness/SKILL.md +14 -10
  2. package/.claude/skills/cowork-harness/references/ci-recipe.md +61 -7
  3. package/.claude/skills/cowork-harness/references/fidelity-and-answers.md +1 -1
  4. package/.claude/skills/cowork-harness/references/scenario-schema.md +23 -11
  5. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +7 -0
  6. package/.claude/skills/cowork-harness/scripts/scenario.py +9 -1
  7. package/CHANGELOG.md +180 -0
  8. package/DESIGN.md +3 -1
  9. package/README.md +55 -7
  10. package/SPEC.md +28 -4
  11. package/baselines/desktop-1.11847.5.json +2 -0
  12. package/baselines/desktop-1.12603.1.json +2 -0
  13. package/baselines/desktop-1.13576.1.json +3 -1
  14. package/baselines/desktop-1.14271.0.json +3 -1
  15. package/baselines/desktop-1.15200.0.json +3 -1
  16. package/baselines/desktop-1.15962.0.json +3 -1
  17. package/baselines/desktop-1.15962.1.json +3 -1
  18. package/baselines/desktop-1.17377.1.json +4 -1
  19. package/baselines/desktop-1.17377.2.json +5 -1
  20. package/baselines/desktop-1.18286.0.json +250 -0
  21. package/baselines/prompts/desktop-1.18286.0/system-prompt-append.md +214 -0
  22. package/dist/agent/session.js +7 -2
  23. package/dist/assert.js +88 -0
  24. package/dist/baseline.js +45 -3
  25. package/dist/cli.js +917 -38
  26. package/dist/prompt.js +31 -8
  27. package/dist/run/cassette.js +78 -4
  28. package/dist/run/chat.js +30 -3
  29. package/dist/run/computer-links.js +198 -0
  30. package/dist/run/diff.js +178 -0
  31. package/dist/run/display-translate.js +47 -0
  32. package/dist/run/doctor.js +17 -2
  33. package/dist/run/envelope.js +32 -9
  34. package/dist/run/execute.js +61 -4
  35. package/dist/run/matrix.js +131 -0
  36. package/dist/run/renderer.js +22 -7
  37. package/dist/run/repeat.js +89 -0
  38. package/dist/run/run-index.js +253 -0
  39. package/dist/run/run.js +15 -2
  40. package/dist/run/trace-view.js +59 -7
  41. package/dist/session.js +36 -0
  42. package/dist/sync/baseline-diff.js +138 -0
  43. package/dist/sync/cowork-sync.js +24 -2
  44. package/dist/types.js +49 -0
  45. package/dist/vm-paths.js +341 -0
  46. package/docs/README.md +3 -0
  47. package/docs/cassette.md +21 -11
  48. package/docs/debugging.md +19 -0
  49. package/docs/fidelity-gaps.md +15 -0
  50. package/docs/maintenance.md +25 -0
  51. package/docs/protocol.md +136 -0
  52. package/docs/scenario.md +95 -6
  53. package/docs/stats.md +74 -0
  54. package/examples/README.md +1 -0
  55. package/examples/replays/README.md +10 -1
  56. package/examples/replays/example-multiselect-gate.cassette.json +1 -1
  57. package/examples/replays/example-pdf-skill-ci-selftest-failing.yaml +30 -0
  58. package/examples/replays/example-pdf-skill.cassette.json +1 -1
  59. package/fixtures/protocol/v1/allow-response.json +13 -0
  60. package/fixtures/protocol/v1/deny-response.json +11 -0
  61. package/fixtures/protocol/v1/hook-callback.json +27 -0
  62. package/fixtures/protocol/v1/initialize.json +18 -0
  63. package/fixtures/protocol/v1/mcp-message.json +42 -0
  64. package/fixtures/protocol/v1/permission-request.json +13 -0
  65. package/fixtures/protocol/v1/question-answer-response.json +36 -0
  66. package/fixtures/protocol/v1/question-request.json +33 -0
  67. package/llms.txt +3 -0
  68. package/package.json +2 -2
  69. package/schema/protocol.v1.json +261 -0
  70. package/schema/run-result.json +24 -2
  71. package/schema/scenario.schema.json +36 -0
  72. package/schema/session.schema.json +3 -0
@@ -3,8 +3,8 @@ name: cowork-harness
3
3
  description: Test or debug a Claude Code skill/plugin under Claude Cowork's runtime — sandboxed agent, default-deny egress, the can_use_tool permission/question protocol — using the cowork-harness CLI. Use when validating or regression-testing a skill, authoring or debugging a scenario YAML (prompt + scripted answers + assert:), choosing a fidelity tier, scripting AskUserQuestion / tool-permission answers, or asserting artifacts, egress, or sub-agent dispatch. Especially when a harness run no-ops an assertion, fails on an unanswered gate, false-greens, a steered answer never reaches the model, or a web_fetch is unexpectedly denied or gated. NOT for generic unit testing (pytest/vitest of your own scripts) or non-Cowork CI. Covers the skill / run / chat / record / replay / trace / decide / assertions / scaffold commands and the session-vs-scenario split.
4
4
  metadata:
5
5
  author: cowork-harness
6
- version: 0.21.0
7
- tracks-harness: cowork-harness 0.21.0 (baseline desktop-1.17377.2)
6
+ version: 0.22.0
7
+ tracks-harness: cowork-harness 0.22.0 (baseline desktop-1.18286.0)
8
8
  ---
9
9
 
10
10
  # cowork-harness
@@ -22,8 +22,8 @@ flagged with a loud `::warning::`, not silent — auto-answer a gate, observe an
22
22
  allowlist). This skill exists mostly to keep you out of those traps — the Gotchas section below is
23
23
  the highest-value part. Read it.
24
24
 
25
- > **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.21.0` (baseline
26
- > `desktop-1.17377.2`). If your checkout is newer, prefer the live `--help`, `SPEC.md`, and
25
+ > **Version note:** the facts and `file:line` pointers here track `cowork-harness 0.22.0` (baseline
26
+ > `desktop-1.18286.0`). If your checkout is newer, prefer the live `--help`, `SPEC.md`, and
27
27
  > `docs/*.md` over this snapshot, and re-run the bundled linter.
28
28
 
29
29
  ## 0. Preflight — make sure the harness can actually run
@@ -38,7 +38,7 @@ cowork-harness skill ./my-skill "do X" # run the skill once against the sta
38
38
  Before the first command, confirm the CLI is reachable and **fail loud** (never fake a pass) when a tier's dependencies are missing:
39
39
 
40
40
  - **One-shot check.** Run `cowork-harness doctor [--tier <tier>]` first — a read-only prerequisite check that inspects Docker, the staged agent, the token, and the baseline in one pass. The bullets below explain each thing it checks (and how to fix it).
41
- - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.21.0**. If it's missing or older, prefix every command with the version floor `npx cowork-harness@>=0.21.0 <cmd>` (Node ≥ 20), or install once with `npm i -g cowork-harness@>=0.21.0`. **Pin `@>=0.21.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published. (≥ 0.21.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator, `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, and `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field.)
41
+ - **CLI on PATH, recent enough?** Run `cowork-harness --version` — this skill needs **≥ 0.22.0**. If it's missing or older, prefix every command with the version floor `npx cowork-harness@>=0.22.0 <cmd>` (Node ≥ 20), or install once with `npm i -g cowork-harness@>=0.22.0`. **Pin `@>=0.22.0`, never `@latest`** — `@latest` can silently fetch an older CLI and the new commands fail as "unknown command", whereas the floor **fails loud** if no compatible version is published. (≥ 0.22.0 is what gates the commands/assertions this skill teaches: `assertions --list`, `scaffold <run-id>`, `trace --view dispatches`, `artifact_json` incl. the `in:` operator, `verify-cassettes`, batch `record <dir>`/`--rerecord-stale`, `record --concurrency <N>`, record-time redaction, multiSelect/`answer:`, `verify-run` answer-coverage, `record --max-artifact-bytes`, live record-time deciders, `verify-cassettes --allow-domain`/`--allow-email`/`--allow-path`/`--allow-file` (`path` — local absolute filesystem paths — is the scanner's 4th class, new in 0.21.0), scenario `skills:` staleness scoping with `COWORK_HARNESS_AGENT_SCOPE=skill`, `chat --plugin`, `/help` in the REPL, `hostloop`'s native host/VM process split with its `allow_host_writes:` consent field, and `computer_links_resolve` (new in 0.22.0).)
42
42
  - **Agent binary (sandboxed live tiers — `container`/`microvm`/`hostloop`/`cowork`).** The staged Claude Code agent is **bind-mounted** from a local Claude Desktop install, or point `COWORK_AGENT_BINARY` at a `claude-code-vm/<ver>/claude` ELF. Nothing is bundled. `protocol` (L0) and `replay` need no staged agent; for the sandboxed tiers, no agent → no run; report that, don't skip silently.
43
43
  - **Docker / Lima.** Only `--fidelity protocol` (L0) runs without them. `container` / `microvm` / `hostloop` / `cowork` need Docker (Lima for L2). If they're absent, drop to `--fidelity protocol` and **say so** — a green that never exercised the sandbox is not a sandbox pass.
44
44
  - **Auth.** `CLAUDE_CODE_OAUTH_TOKEN` (preferred) or `ANTHROPIC_API_KEY`, via env or `.env`. Minting an OAuth token needs the **`claude` CLI** (`npm i -g @anthropic-ai/claude-code`, then `claude setup-token`).
@@ -54,8 +54,8 @@ Before the first command, confirm the CLI is reachable and **fail loud** (never
54
54
  debugging map).
55
55
 
56
56
  Full command set: `skill · run · chat · record · replay · verify-cassettes · rehash · prune · lint ·
57
- verify-run · trace · inspect · decide · gates · answer · scaffold · assertions --list · sync · list ·
58
- boundary-check · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
57
+ verify-run · trace · inspect · diff · stats · decide · gates · answer · scaffold · assertions --list · sync ·
58
+ list · boundary-check · vm <init|status|delete|prune> · doctor`. Always check `cowork-harness <cmd> --help`.
59
59
 
60
60
  ## 2. Two files: session vs scenario
61
61
 
@@ -288,7 +288,9 @@ keys). Exit code is non-zero on errors (CI-friendly). `scaffold` auto-upgrades t
288
288
  for egress on `protocol`, so it never emits a scenario `lint` would reject.
289
289
 
290
290
  CI placement: a **token-free `replay` PR gate** (content/structure only) + a **nightly live `run`**
291
- (filesystem/egress). See `references/ci-recipe.md` for the four-stage pipeline.
291
+ (filesystem/egress). Fastest setup: `uses: yaniv-golan/cowork-harness@v1` (a packaged GitHub Action with a
292
+ PR job-summary reporter). See `references/ci-recipe.md` for the Action, the manual step-by-step form, and
293
+ the four-stage pipeline.
292
294
 
293
295
  ## 10. Debugging with `chat`
294
296
 
@@ -326,7 +328,8 @@ are the ones that bite hardest.
326
328
  `replay` skips filesystem/egress keys (`file_exists`, `user_visible_artifact`, `artifact_json`,
327
329
  `egress_*`, `no_delete_in_outputs`, `self_heal_ran`, `transcript_no_host_path`); a *mixed* item like
328
330
  `{result, egress_denied}` greens on `result` while its `egress_denied` half is dropped. (`record`
329
- snapshots an `artifacts` manifest, which makes `file_exists`/`user_visible_artifact`/`artifact_json`
331
+ snapshots an `artifacts` manifest, which makes
332
+ `file_exists`/`user_visible_artifact`/`artifact_json`/`computer_links_resolve`
330
333
  replay-checkable — but the live-only egress keys stay skipped.) *Fix:* put egress/live-only checks on
331
334
  a live gate; keep one concern per `assert:` item; run the linter. The harness warns loudly on skip.
332
335
 
@@ -444,7 +447,8 @@ and the CI recipe, read the files in `references/`.
444
447
  - `references/scenario-schema.md` — scenario/session YAML schema, full assertion catalog (with each
445
448
  key's replay class), the web_fetch model, and the complete gotcha list.
446
449
  - `references/fidelity-and-answers.md` — fidelity tiers, answer paths, the determinism contract.
447
- - `references/ci-recipe.md` — replay-vs-live lane split and the four-stage GitHub Actions pipeline.
450
+ - `references/ci-recipe.md` — the packaged GitHub Action, replay-vs-live lane split, and the four-stage
451
+ GitHub Actions pipeline.
448
452
  - `scripts/scenario.py` — `scaffold` a valid scenario skeleton and `lint` scenarios for the
449
453
  no-silent-false-green invariants (both usable as CI steps).
450
454
  - `../../../docs/run-status.md` — checking a background run's status without `ps aux`.
@@ -1,12 +1,59 @@
1
1
  # CI recipe — replay vs live lanes
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.21.0` (baseline `desktop-1.17377.2`).
3
+ Self-contained reference. Tracks `cowork-harness 0.22.0` (baseline `desktop-1.18286.0`).
4
4
 
5
- **Minimal token-free PR gate** — the smallest thing worth committing; runs on stock GitHub-hosted runners,
6
- no token/Docker/agent:
5
+ **Fastest path: the packaged Action.** One step gets you `replay`/`lint`/`verify-cassettes` plus a PR
6
+ job-summary reporter (verdict table, staleness findings, cost/turns when available):
7
7
 
8
8
  ```yaml
9
- - run: npm i -g cowork-harness@>=0.21.0
9
+ - uses: yaniv-golan/cowork-harness@v1
10
+ with:
11
+ command: replay
12
+ path: cassettes/
13
+ ```
14
+
15
+ Reach for the manual multi-step form below only when you need per-step control the Action's inputs don't
16
+ cover (a custom flag combination, a different runner matrix per step, or `lint`/`verify-cassettes` gated
17
+ independently instead of as one action run per command).
18
+
19
+ **Live lane (`command: run`) with the packaged Action — self-hosted runner, you stage the binary.** The
20
+ Action never downloads or provisions the agent ELF itself — by design, not an oversight (see "the
21
+ realistic CI shape" below for why). Stage it yourself as an explicit step in *your own* workflow, then
22
+ point the Action at it:
23
+
24
+ ```yaml
25
+ jobs:
26
+ live:
27
+ runs-on: [self-hosted, linux, arm64] # needs Docker + this staged ELF; not a stock GitHub-hosted runner
28
+ steps:
29
+ - uses: actions/checkout@v4
30
+ - name: Stage the agent binary (official channel, sha256-verified — see docs/maintenance.md)
31
+ run: |
32
+ V=2.1.197 # match your scenario's pinned baseline's agentVersion
33
+ curl -fSL "https://downloads.claude.ai/claude-code-releases/$V/linux-arm64/claude" -o "$RUNNER_TEMP/claude-$V"
34
+ chmod +x "$RUNNER_TEMP/claude-$V"
35
+ # verify against the committed baseline's sha256 (baselines/desktop-*.json → agentBinary.sha256)
36
+ # before trusting it — see docs/maintenance.md's "Agent-binary provenance" section.
37
+ echo "COWORK_AGENT_BINARY=$RUNNER_TEMP/claude-$V" >> "$GITHUB_ENV"
38
+ - uses: yaniv-golan/cowork-harness@v1
39
+ with:
40
+ command: run
41
+ path: scenarios/
42
+ anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
43
+ ```
44
+
45
+ Why this is a step *you* write, not an Action input the harness provides for you: pulling Anthropic's
46
+ binary is a call about your own relationship with their distribution terms — keeping it in your own
47
+ version-controlled workflow keeps that decision and its execution yours, auditable, and outside any
48
+ third-party action's code. See the [agent-binary provenance runbook](../../../../docs/maintenance.md) for
49
+ the full recovery/verification story (including why `COWORK_AGENT_BINARY` substitutions are
50
+ sha256-*checked* but not hard-blocking on mismatch — it's advisory for an intentional substitution).
51
+
52
+ **Minimal token-free PR gate (manual form)** — the smallest thing worth committing; runs on stock
53
+ GitHub-hosted runners, no token/Docker/agent:
54
+
55
+ ```yaml
56
+ - run: npm i -g cowork-harness@>=0.22.0
10
57
  - run: cowork-harness lint scenarios/*.yaml # no silent false-greens
11
58
  - run: cowork-harness verify-cassettes cassettes/ # privacy + staleness
12
59
  - run: cowork-harness replay cassettes/ # token-free content/structure
@@ -22,7 +69,8 @@ The split is not just about tokens — it decides **where each lane can run**:
22
69
  - **`replay` / `verify-cassettes` (token-free, agent-free).** Replays a recorded cassette
23
70
  (`events.jsonl` + `control-out.jsonl`) and lints the committed cassettes. **No model tokens, no
24
71
  Docker, no agent binary** — runs on a stock GitHub Actions runner. Evaluates **content** assertions
25
- only (`transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`, `result`, and the verdict
72
+ only (`transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`, `skill_triggered`,
73
+ `no_skill_triggered`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `result`, and the verdict
26
74
  modifiers `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0_plugin_divergence` /
27
75
  `allow_stall` (no-op passes); plus the gate keys `question_asked` / `questions_count_max` /
28
76
  `gate_answers_delivered` **if** the cassette has `controlOut`). Filesystem/egress assertions are
@@ -110,6 +158,12 @@ A typical skill repo runs four stages, fastest/cheapest first:
110
158
 
111
159
  ## GitHub Actions sketch
112
160
 
161
+ The PR gate below is the manual, step-by-step version of what `uses: yaniv-golan/cowork-harness@v1` does
162
+ in one step (see the top of this doc) — reach for this form when you need independent per-command
163
+ gating/annotations rather than one action run per command. The nightly live job has no packaged-Action
164
+ equivalent yet (the Action's `command: run` mode needs a self-hosted runner with Docker + the agent binary
165
+ already provisioned, same as the manual form below).
166
+
113
167
  PR gate (token-free — runs on every push):
114
168
 
115
169
  ```yaml
@@ -124,7 +178,7 @@ jobs:
124
178
  with: { node-version: '20' }
125
179
  - uses: actions/setup-python@v5
126
180
  with: { python-version: '3.x' } # python3 only — PyYAML is bundled with the linter
127
- - run: npm i -g cowork-harness@>=0.21.0
181
+ - run: npm i -g cowork-harness@>=0.22.0
128
182
  - run: cowork-harness lint scenarios/*.yaml # no-silent-false-green (needs python3; PyYAML bundled)
129
183
  - run: cowork-harness verify-cassettes cassettes/ --output-format json # privacy + staleness gate
130
184
  - run: cowork-harness replay cassettes/ --output-format json # token-free content/structure
@@ -153,7 +207,7 @@ jobs:
153
207
  echo "live=true" >> "$GITHUB_OUTPUT"
154
208
  fi
155
209
  - if: steps.guard.outputs.live == 'true'
156
- run: npm i -g cowork-harness@>=0.21.0
210
+ run: npm i -g cowork-harness@>=0.22.0
157
211
  - if: steps.guard.outputs.live == 'true'
158
212
  run: cowork-harness run scenarios/ --output-format json
159
213
  env:
@@ -1,6 +1,6 @@
1
1
  # Fidelity tiers & answer paths
2
2
 
3
- Self-contained reference. Tracks `cowork-harness 0.21.0` (baseline `desktop-1.17377.2`).
3
+ Self-contained reference. Tracks `cowork-harness 0.22.0` (baseline `desktop-1.18286.0`).
4
4
 
5
5
  ## Fidelity tiers (`fidelity:` in the scenario)
6
6
 
@@ -1,7 +1,7 @@
1
1
  # Scenario & session schema, assertion catalog, web_fetch, full gotchas
2
2
 
3
- Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.21.0`
4
- (baseline `desktop-1.17377.2`). If your checkout is newer, prefer the live `docs/scenario.md`,
3
+ Self-contained reference for authoring `cowork-harness` scenarios. Tracks `cowork-harness 0.22.0`
4
+ (baseline `desktop-1.18286.0`). If your checkout is newer, prefer the live `docs/scenario.md`,
5
5
  `docs/session.md`, and `SPEC.md`.
6
6
 
7
7
  **Minimal scenario** — `prompt` is the only required field:
@@ -220,6 +220,12 @@ passes only if every key passes. Keep one concern per item unless you mean conju
220
220
  | `subagent_dispatched: <regex>` | a sub-agent whose `agentType` **or dispatch description** matches |
221
221
  | `subagent_declared_but_unused: <Tool>` | a sub-agent declared the tool but never used **that** tool (even if it used others) |
222
222
  | `dispatch_count_max: <N>` | at most N sub-agents dispatched (records only — does NOT enforce a cap) |
223
+ | `skill_triggered: <regex>` | a skill matching the regex (invoked id, e.g. `"plugin:skill"`) was invoked via the `Skill` tool — evidence-unavailable (not a normal fail) if the agent's init tools have no `Skill` tool |
224
+ | `no_skill_triggered: <regex>` | no invoked skill id matched — the negative-control / description-collision catcher; evidence-unavailable (never a vacuous pass) if invocation data is absent or the `Skill` tool is unobservable |
225
+ | `max_cost_usd: <N>` | the run's SDK-reported cost is ≤ N USD — evidence-unavailable if cost telemetry is absent. **Replay asserts the frozen recording's cost, not fresh spend** — a real regression needs a live `run` |
226
+ | `max_tokens: <N>` | `usage.input_tokens + usage.output_tokens` ≤ N (cache tokens excluded) — same replay caveat as `max_cost_usd` |
227
+ | `tool_calls_max: <N>` | total top-level tool calls (sum of `toolCounts`) ≤ N — meaningfully replay-checkable (re-drive recomputes `toolCounts` deterministically) |
228
+ | `max_turns: <N>` | the SDK-reported (or fallback-counted) turn count ≤ N — meaningfully replay-checkable (re-drive recounts turns deterministically, same as `tool_calls_max`) |
223
229
  | `question_asked: <regex>` | the agent asked an AskUserQuestion whose text matches |
224
230
  | `questions_count_max: <N>` | the agent asked at most N questions |
225
231
  | `gate_answers_delivered: true` | every answered gate's answer reached the model (observed `tool_result`; unobserved = fail) |
@@ -232,6 +238,7 @@ passes only if every key passes. Keep one concern per item unless you mean conju
232
238
  | `egress_denied: <host>` | the host was blocked by the egress proxy |
233
239
  | `egress_allowed: <host>` | the host was allowed through |
234
240
  | `artifact_json: {artifact, path, …}` | assert a JSON artifact's contents — `equals`/`gt`/`in`/`exists`/`absent`/`is_null` over a dotted `path` (`in` = membership in a list, for a stochastic/LLM value; `absent` ≠ `is_null`; an unresolved intermediate fails loud) |
241
+ | `computer_links_resolve: true` | every `computer://` link in the model-visible transcript resolves to an artifact that exists in the run's collected outputs/mounts — a dangling link fails, naming which target was checked (a live host path, the collected work tree, or the replay manifest). Zero links in the transcript **passes** (presence-gated separately — pair with `transcript_contains` if you also need a link to show up). **Only `true` is valid** (`false` is rejected by the schema) |
235
242
 
236
243
  `expect_denied: [host, …]` adds one `egress_denied` per host. Run `cowork-harness assertions --list` for this
237
244
  table from the live schema. Example: `artifact_json: { artifact: outputs/cap.json, path: me.run_id, equals: "r1" }`.
@@ -270,21 +277,26 @@ sourcing ≠ evaluation (replay warns when you edit one). `verify-run` is the on
270
277
  *run dir*; `--assert-from` is the equivalent for a *cassette*.
271
278
 
272
279
  **Evaluated on replay (content):** `transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`,
273
- `result`. The verdict modifiers `allow_permissive_auto_allow` / `allow_missing_capability` /
274
- `allow_l0_plugin_divergence` / `allow_stall` are also kept on replay, evaluated as no-op passes.
280
+ `skill_triggered`, `no_skill_triggered`, `max_cost_usd`, `max_tokens`, `tool_calls_max`, `max_turns`, `result`
281
+ (`max_cost_usd`/`max_tokens` assert the frozen recording's spend on replay, not fresh spend). The verdict
282
+ modifiers `allow_permissive_auto_allow` / `allow_missing_capability` / `allow_l0_plugin_divergence` /
283
+ `allow_stall` are also kept on replay, evaluated as no-op passes.
275
284
 
276
285
  **Gate keys — replay only with a `controlOut` cassette:** `question_asked`, `questions_count_max`,
277
286
  `gate_answers_delivered`. With `controlOut` present they evaluate; on an old cassette without it, a
278
287
  **loud warning** fires and they are **excluded** (not vacuously passed). Re-record to enable them.
279
288
 
280
289
  **Filesystem — replay-checkable WITH an artifact manifest:** `file_exists`, `user_visible_artifact`,
281
- `artifact_json` run on replay when the cassette carries an `artifacts` snapshot (`record` captures
282
- `outputs/` + connected folders; `replay` materializes it). `artifact_json` needs the small-file JSON `body`
283
- inlined; a hash-only entry still satisfies `file_exists`. Without a manifest (older cassettes) they're
284
- skipped. A green replay re-confirms *record-time* artifacts, not that the current skill still produces them
285
- — `replay --strict` fails when the staleness `fingerprint` shows ANY skill/baseline drift, or
286
- `replay --fail-on-skill-drift` only on skill-source drift; every replay result also reports it class-tagged
287
- in `staleness[]` for a JSON gate.
290
+ `artifact_json`, `computer_links_resolve` run on replay when the cassette carries an `artifacts` snapshot
291
+ (`record` captures `outputs/` + connected folders; `replay` materializes it). `artifact_json` needs the
292
+ small-file JSON `body` inlined; a hash-only entry still satisfies `file_exists`. `computer_links_resolve`
293
+ resolves a `/sessions/…/mnt/…`-shaped link directly against the manifest, and a host-shaped (hostloop) link
294
+ by first normalizing it to a mount-relative path (recorded connected-folder prefixes + the outputs/uploads
295
+ mounts) — replay has no live filesystem to check a host path against directly (that only happens on a live
296
+ `run`/`verify-run`). Without a manifest (older cassettes) all four are skipped. A green replay re-confirms
297
+ *record-time* artifacts, not that the current skill still produces them — `replay --strict` fails when the
298
+ staleness `fingerprint` shows ANY skill/baseline drift, or `replay --fail-on-skill-drift` only on
299
+ skill-source drift; every replay result also reports it class-tagged in `staleness[]` for a JSON gate.
288
300
 
289
301
  **Egress + other filesystem — still skipped on replay (live-only):** `no_delete_in_outputs`,
290
302
  `self_heal_ran`, `transcript_no_host_path`, `egress_*` / `expect_denied`. These run only on a live `run`/`record`.
@@ -6,22 +6,29 @@
6
6
  "allow_permissive_auto_allow",
7
7
  "allow_stall",
8
8
  "artifact_json",
9
+ "computer_links_resolve",
9
10
  "dispatch_count_max",
10
11
  "egress_allowed",
11
12
  "egress_denied",
12
13
  "file_exists",
13
14
  "gate_answers_delivered",
15
+ "max_cost_usd",
16
+ "max_tokens",
17
+ "max_turns",
14
18
  "no_delete_in_outputs",
19
+ "no_skill_triggered",
15
20
  "question_asked",
16
21
  "questions_count_max",
17
22
  "replay_protocol_fidelity",
18
23
  "result",
19
24
  "self_heal_ran",
25
+ "skill_triggered",
20
26
  "subagent_declared_but_unused",
21
27
  "subagent_dispatched",
22
28
  "subagent_tool_absent",
23
29
  "subagent_tool_used",
24
30
  "tool_called",
31
+ "tool_calls_max",
25
32
  "tool_not_called",
26
33
  "tool_result_contains",
27
34
  "tool_result_not_contains",
@@ -59,12 +59,20 @@ CONTENT_KEYS = {
59
59
  "subagent_dispatched",
60
60
  "subagent_declared_but_unused",
61
61
  "dispatch_count_max",
62
+ "skill_triggered",
63
+ "no_skill_triggered",
64
+ "max_cost_usd",
65
+ "max_tokens",
66
+ "tool_calls_max",
67
+ "max_turns",
62
68
  }
63
69
  # content keys, but only evaluated on replay when the cassette carries controlOut
64
70
  GATE_KEYS = {"question_asked", "questions_count_max", "gate_answers_delivered"}
65
71
  # manifest-backed: replay-checkable when the cassette carries an `artifacts` manifest (record snapshots one);
66
72
  # a manifest-less cassette skips them. Since the 0.3.0 artifact-manifest these are NOT always live-only.
67
- MANIFEST_KEYS = {"file_exists", "user_visible_artifact", "artifact_json"}
73
+ # computer_links_resolve joins this bucket (not CONTENT_KEYS): resolving a non-empty link set needs
74
+ # either a live filesystem or the cassette's artifacts manifest — see cassette.ts's manifestKeys comment.
75
+ MANIFEST_KEYS = {"file_exists", "user_visible_artifact", "artifact_json", "computer_links_resolve"}
68
76
  # live-only: ALWAYS skipped on replay, with a loud warning (no filesystem, no network on the token-free lane)
69
77
  LIVE_ONLY_KEYS = {
70
78
  "egress_denied",
package/CHANGELOG.md CHANGED
@@ -6,6 +6,186 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.22.0] — 2026-07-03
10
+
11
+ ### Added
12
+
13
+ - **`computer://` link modeling — the prompt now instructs file links exactly as production does.**
14
+ Four pieces landed together (design + binary research in
15
+ `docs/internal/2026-07-03-computer-link-scheme-research-and-plan.md`):
16
+ - `src/vm-paths.ts` — a faithful port of Desktop's display-side VM→host path transform
17
+ (`deepTranslateVMPaths` / `mapVMPathToHostPath` / `encodeComputerUrlsForHostLoop`): markdown-link,
18
+ backtick, bare-token, and prose rewrite positions; per-segment percent-encoding; traversal
19
+ rejection; dormant `.host-home` / `.auto-memory` mount branches.
20
+ - **Hostloop display translation** (`src/run/display-translate.ts`): at hostloop fidelity the
21
+ `run`/`chat` renderer shows production-identical host paths in assistant text and tool lines.
22
+ Hostloop-only by design (container staging paths would be less faithful than VM paths), identity on
23
+ replay (no live ctx), suppressed in `--compact`/`--demo` (the shareable no-host-paths contract).
24
+ Hostloop prompt tokens now render HOST paths (`{{cwd}}` / `{{workspaceFolder}}` / `{{skillsDir}}` +
25
+ the dedicated `{{cwd}}/mnt/uploads` pre-replacement), matching the Desktop builder's own host-loop
26
+ substitution recipe.
27
+ - **New assertion `computer_links_resolve: true`** — every `computer://` link in the transcript must
28
+ resolve to an artifact that exists (live/verify-run: filesystem; replay: the cassette's artifact
29
+ manifest, with host-shaped links normalized via the recorded session folders). Zero links pass;
30
+ dangling links report which target was checked. Assert links with this key, not literal link text.
31
+ - The `sharing_files` prompt section now instructs `[View your X](computer://{{workspaceFolder}}/x.ext)`
32
+ links faithfully — the prompt-reconstruction divergence for links is retired (docs/fidelity-gaps.md
33
+ updated).
34
+ - **Dark-feature gate sentinels.** Four newly discovered GrowthBook gates pinned in the baseline
35
+ (host-fs skeleton `2614807392`, standard-session auto-memory `123929380`, memory-guidelines env
36
+ `1696890383`, memory extra-guidelines `2860753854`) — all dark or inert-default for standard cowork
37
+ sessions today; pinned so a production flip surfaces as a `sync --diff` delta. Absent-from-fcache
38
+ dark gates record an explicit `source:"absent"` marker (the fcache re-key guard's semantics are
39
+ preserved).
40
+
41
+ - **`stats` command + cross-run result index.** Every `run`/`skill` invocation (and `record`'s live
42
+ execution) now appends one JSON line to `<runsRoot>/index.jsonl` at the same moment it writes
43
+ `result.json` — a durable, queryable history independent of whether the run dir itself survives a later
44
+ `prune`. `cowork-harness stats [<scenario>]` reads it back: run count, pass rate, cost/duration/token/turn
45
+ p50/p95, and the last-green timestamp, filterable by `--since`/`--baseline`/`--branch` and windowable by
46
+ `--last <n>` (per-scenario, not globally). `--reindex` rebuilds the index from the physical run-dir tree
47
+ — the migration path for runs that predate the index. `trace`/`inspect`/`scaffold`/`status`'s existing
48
+ run-id/fragment resolution now checks the index FIRST (faster, and the source of truth going forward),
49
+ falling through to the pre-index filesystem walk automatically and unchanged for any run that predates
50
+ the index or was never indexed — same commands, same output, same ambiguity-handling behavior either
51
+ way. See [docs/stats.md](./docs/stats.md).
52
+ - **`run --matrix` matrix runner.** Runs one scenario across the cross-product of baseline/model/skill_dir
53
+ axes declared in a `matrix.yaml` file (any axis optional; an absent axis contributes one unmodified
54
+ cell) and reports one row per cell instead of a single pass/fail. `--max-cells` caps the cross-product
55
+ (default 16, warns and truncates rather than silently dropping cells); `--concurrency` (default 1, max 8)
56
+ runs cells N at a time via the same bounded pool `record --concurrency` uses. Exit is non-zero if ANY
57
+ cell fails — a real assertion failure or a cell-level infrastructure error (e.g. the pinned baseline's
58
+ agent binary isn't staged), rendered as a distinct `cell error: …` line rather than a fake assertion
59
+ failure. The `skill_dirs` axis substitutes the session's single `plugins.local_plugins` entry; candidates
60
+ must share that entry's directory basename (the mount name derives from it, with no author-chosen
61
+ override anywhere in the harness) — a mismatch is a loud, explicit usage error. `--concurrency > 1`
62
+ cannot combine with `--decider-dir`/`--decider-cmd` (the external decider channel is one shared object
63
+ across every cell, and every channel implementation is strictly serial over shared mutable state — not
64
+ safe for concurrent gate answers; `--concurrency 1`, the default, is genuinely serial and fine). The
65
+ JSON envelope gains an additive `matrix: {cells[]}` field; `ok`/the exit code are `!matrix.anyFail` for
66
+ this mode.
67
+ - **`--matrix` composes with `--repeat`.** Each cell now runs as its own repeat batch (N iterations of that
68
+ cell's axes-overridden scenario) through the same `runRepeatBatch` helper standalone `--repeat` uses —
69
+ same unanswered-gate, error, and budget-cap handling — with `MatrixCellRepeatResult`/`MatrixRepeatRollup`
70
+ carrying each cell's full `RepeatRollup` (pass rate, per-assertion attribution, signal histogram,
71
+ stoppedEarly) rather than a single pass/fail. The matrix verdict judges each cell's rollup against
72
+ `--min-pass-rate`; the JSON envelope gains an additive `matrixRepeat: {cells[]}` field, checked before
73
+ `matrix`/`rollups` when present. Also closes the previously-ungated `--repeat` + `--decider-cmd`
74
+ combination (rejected for the same live-decider reasoning as `--decider-dir`).
75
+ - **Packaged GitHub Action** (`uses: yaniv-golan/cowork-harness@v1`, [`action.yml`](./action.yml)) wrapping
76
+ `replay`/`lint`/`verify-cassettes`/`run` with a PR job-summary reporter (verdict table, staleness
77
+ findings, the skipped-live-only-assertions honesty line, cost/turns when available). Token-free lane runs
78
+ on any `ubuntu-latest` runner; `run` (live lane) needs a self-hosted runner with Docker + the agent binary
79
+ already provisioned — the action does not stage either, by design (staging Anthropic's binary is a call
80
+ about your own distribution-terms relationship, so it stays a step in your own workflow, not something a
81
+ third-party action automates for you). README and the companion skill's `ci-recipe.md` both carry a
82
+ worked self-hosted-runner example for the live lane. Self-tested in CI (`uses: ./` against a packed
83
+ tarball of the current commit, a passing case, a usage-error case, and a genuine assertion-failure case).
84
+ A `publish-image.yml` workflow pushes `ghcr.io/yaniv-golan/cowork-agent-base:2`/`cowork-agent-full:2` on
85
+ release tags for consumers (and this repo's own CI) to `docker pull` instead of building from scratch.
86
+ - **`skill_triggered` / `no_skill_triggered` assertions.** Skill invocation (the top-level `Skill` tool_use)
87
+ is now a first-class assertable event, recorded as `RunResult.skillsInvoked[]` and evaluated as a regex
88
+ match, matching the `subagent_dispatched` convention. Fails as evidence-unavailable (never a vacuous pass)
89
+ when the agent's init tool list has no `Skill` tool (agent-version drift) or, for the negative form, when
90
+ invocation data itself is absent (an old run predating this key). Replay-checkable (content key).
91
+ - **`max_cost_usd` / `max_tokens` / `tool_calls_max` / `max_turns` budget assertions**, built on Wave 0's
92
+ cost/turns seam. Each fails as evidence-unavailable (never a vacuous pass) when the underlying telemetry
93
+ is absent. `max_cost_usd`/`max_tokens` are honest about the replay lane: they assert the *frozen
94
+ recording's* spend, not fresh spend — a live `run` is where a real budget regression is caught.
95
+ `tool_calls_max`/`max_turns` are meaningfully replay-checkable (the re-drive recomputes `toolCounts`/turn
96
+ count deterministically).
97
+ - **`diff <a> <b>` command.** Compares two committed platform baselines (`--changelog` renders known-field
98
+ prose — agent/Desktop version bumps, egress allowlist changes, gate flips — from a proper recursive
99
+ structural differ, replacing the old one-level diff that dumped a whole subtree on any nested change;
100
+ `sync --diff` now uses the same differ), two runs, two cassettes, or a run and a cassette (kind
101
+ auto-detected by CONTENT, not filename — a cassette-shaped file not literally named `*.cassette.json`
102
+ still detects correctly). Run/cassette mode has four views (`tools`/`transcript`/`artifacts`/`meta`, or
103
+ `all`) with normalization masking per-run noise (tool-use ids, UUIDs, session-dir markers, timestamps,
104
+ host paths) so two runs of the *same* scenario diff as identical despite that noise; `--no-normalize`
105
+ compares raw values for forensics. Comparing runs of two *different* scenarios is allowed (useful for
106
+ skill-variant comparison) but warns on stderr — added/removed rows may then reflect scenario
107
+ differences, not drift. Token-free — no live Desktop install or Docker needed either way.
108
+ - **`run --repeat N` variance rollup.** Runs each resolved scenario N times (2-100) and aggregates a
109
+ rollup (pass rate, per-assertion pass/fail attribution, a verdict-signal histogram, cost/token totals,
110
+ non-deterministic-run count) instead of a single pass/fail. `--min-pass-rate` sets the batch threshold
111
+ (default 1.0 — no flakiness tolerance); `--stop-on-diverge` stops the loop as soon as both a pass and a
112
+ fail are observed (that batch always fails — divergence IS the failure being measured for);
113
+ `--max-budget-usd` stops the loop once cumulative cost would exceed it (an incomplete-but-clean stop is a
114
+ warning, not a failure by itself). `--repeat` rejects `--decider-dir`/`--decider-cmd` (an interactive
115
+ driving agent × N runs is not a measurement). The JSON envelope gains an optional `rollups[]` array;
116
+ `ok`/the exit code are
117
+ redefined for this mode from the rollups, not from `results.every(pass)` — `results[]` still holds every
118
+ raw run.
119
+ - **E9: a hand-authored draft-07 JSON Schema for the harness's own control-channel wire protocol**
120
+ (`schema/protocol.v1.json`) — the `initialize` handshake, `can_use_tool` permission/question gates
121
+ (incl. AskUserQuestion's `questions[]`), `hook_callback`/`mcp_message` round-trips, and the nested
122
+ `control_response` envelope + the `answers` wire-shape — formalizing the prose in DESIGN.md §6/SPEC.md
123
+ §4-5. Deliberately does NOT schema the Claude Agent SDK's own event stream (Anthropic's surface).
124
+ Ships with a golden vector pack (`fixtures/protocol/v1/*.json` — real cassette-extracted where
125
+ possible, synthetic-via-the-real-`session.ts`-envelope-builders otherwise) and conformance tests
126
+ (`test/protocol-schema.test.ts`) that validate every committed cassette's control-channel lines plus
127
+ the real envelope-builder functions' actual output, and guard the schema/vector-pack lockstep. See
128
+ [docs/protocol.md](./docs/protocol.md) for the versioning policy and the explicit
129
+ descriptive-not-normative scope statement.
130
+
131
+ ### Parity
132
+
133
+ - **Synced the platform baseline to Claude Desktop 1.18286.0** (`baselines/desktop-1.18286.0.json`).
134
+ The staged agent ELF is unchanged (`2.1.197`; measured sha256 matches the official release manifest).
135
+ `sync` re-derived egress/gates/mount/web_fetch facts — no unknown deltas; the egress allowlist (15
136
+ domains) and all 6 pinned GrowthBook gates held. `asarFingerprint` moved (`0b2f2fb6 → edff6926`);
137
+ the host-loop `## Shell access` generator and the subagent append were re-verified against the new
138
+ asar (unconditional fragments still byte-faithful).
139
+ - **Re-authored the system-prompt append reconstruction for 1.18286.0**
140
+ (`baselines/prompts/desktop-1.18286.0/system-prompt-append.md`). The real append was RESTRUCTURED
141
+ at this release (constant `aui`, 37.9KB): a new `<claude_behavior>` wrapper plus new
142
+ behavior-driving sections — AskUserQuestion-before-work, task-list/verification, citation,
143
+ file-creation/computer-use guidance, web-fetch no-fallback restrictions, sharing/package/examples,
144
+ an `<env>` block — and the skills/file-handling sections moved inside `<computer_use>`. The
145
+ reconstruction (paraphrased per the no-bundling rule) now carries these; generic refusal/safety
146
+ policy stays elided, and the deliberate divergences (artifacts renderer catalog trimmed,
147
+ `computer://` links described-not-instructed) are logged in the asset header. New `<env>` tokens
148
+ `{{currentDateTime}}` / `{{currentTimezone}}` / `{{accountName}}` (session `account_name`,
149
+ default `"User"`) render in `src/prompt.ts`. New tests guard baseline→asset references, token
150
+ hygiene, and the `/sessions/` link-leak trade.
151
+ - **Annotated `desktop-1.17377.2` as append-unverified** (`$comment_prompts_unverified`): its own
152
+ asar was never prompt-spot-checked and is no longer obtainable locally, so whether the 1.18286.0
153
+ restructure landed there or later is unverifiable; the pin carries the last-verified 1.15200.0
154
+ reconstruction.
155
+
156
+ ### Added
157
+
158
+ - **Agent-binary provenance in baselines.** Each baseline's `agentBinary` now records the Linux/arm64 ELF's
159
+ `sha256` plus `shaProvenance` — `measured-local` (hashed from the staged binary at `sync` and cross-checked
160
+ against the official release manifest) or `official-manifest` (copied from the manifest for a version not
161
+ staged on the syncing machine; staging-identity unverified) — and, on measured rows, `manifestChecksumMatch`.
162
+ `sync` computes these and stays offline-capable (an unreachable manifest records `"unknown"` and never fails
163
+ the sync). All committed baselines back-filled. (No `nativeSha256`: the signed native Mach-O never equals a
164
+ manifest hash.)
165
+ - **Default-on agent-ELF integrity check.** The resolved ELF is verified against the recorded `sha256` at run
166
+ time **by default** (opt out with `COWORK_HARNESS_VERIFY_AGENT_SHA=0`; ELF only). Hard-fails only on a
167
+ `measured-local` mismatch at the baseline's own staged path; intentional substitutions (`COWORK_AGENT_BINARY`
168
+ / newest-sibling fallback) and `official-manifest` hashes advisory-warn. `doctor` now shows a
169
+ `[sha256 ✓ vs baseline, …]` provenance line. Old agent versions are re-downloadable and verifiable — recovery
170
+ runbook in `docs/maintenance.md`.
171
+
172
+ ### Changed
173
+
174
+ - **`RunResult.cost`/`.usage` retyped** from opaque `Record<string, unknown>` to structured shapes.
175
+ `cost` is now `{ usd?, raw? }`: `usd` is the SDK result message's `total_cost_usd`, newly extracted (was
176
+ previously dropped on the floor); `raw` is the pre-existing `api_metrics` payload, now nested under `raw`
177
+ instead of being the whole `cost` object. `usage` gains a `turns?: number` field, from the SDK's
178
+ `num_turns` (also newly extracted). Breaking shape change for anything reading `result.json`'s `cost`
179
+ field directly — see SPEC.md's `RunResult` reference for the new shape. No cassette-format bump (derived
180
+ reporting, not a stored format change).
181
+
182
+ ### Fixed
183
+
184
+ - **Replay now surfaces `usage`/`cost` in `result.json`.** `replayCassette` previously omitted both fields
185
+ entirely from every replayed `RunResult`, regardless of what the cassette recorded — a replay-lane blind
186
+ spot, not a live-only limitation. Both are now re-derived from the cassette's re-driven record, same as
187
+ the live/partial-run lanes.
188
+
9
189
  ## [0.21.0] — 2026-07-03
10
190
 
11
191
  ### Added
package/DESIGN.md CHANGED
@@ -38,7 +38,7 @@ flowchart TB
38
38
  A Cowork session is the Desktop app driving an agent that runs **inside an Apple Virtualization.framework microVM**:
39
39
 
40
40
  - VM bundle: `~/Library/Application Support/Claude/vm_bundles/claudevm.bundle/` (`rootfs.img`, `sessiondata.img`, `efivars.fd`, `machineIdentifier`, `gvisorMacAddress`, `vmIP`); a warm pool at `vm_bundles/warm/<sha>/`.
41
- - In-VM agent: `~/Library/Application Support/Claude/claude-code-vm/<ver>/claude` (currently **2.1.197**, an **ELF aarch64** binary), spawned by the host **in cowork mode via the `CLAUDE_CODE_IS_COWORK=1` env var** — *not* a `--cowork` flag (that flag is plugin-scope and the staged agent rejects it; see the control-protocol note below).
41
+ - In-VM agent: `~/Library/Application Support/Claude/claude-code-vm/<ver>/claude` (currently **2.1.197**, an **ELF aarch64** binary), spawned by the host **in cowork mode via the `CLAUDE_CODE_IS_COWORK=1` env var** — *not* a `--cowork` flag (that flag is plugin-scope and the staged agent rejects it; see the control-protocol note below). Each baseline records this ELF's `sha256` (`agentBinary.sha256`/`shaProvenance`), and the resolver integrity-checks the binary it's about to run against that hash by default (opt out `COWORK_HARNESS_VERIFY_AGENT_SHA=0`), so "the same pinned agent" is enforced, not just asserted. Old versions are re-downloadable + verifiable from the official release channel (see `docs/maintenance.md`).
42
42
  - Network: `vm_network_mode: "gvisor"`, egress through a userspace netstack with a **compiled domain allowlist**; off-list partners rejected (`partner rejected: entry not on compiled allowlist`).
43
43
  - Control plane: Electron renderer→main typed IPC on channels named `$eipc_message$_<per-build-UUID>_$_claude.web_$_<Class>_$_<method>`, every handler validating `event.senderFrame.url` against a trusted-origin allowlist. The session manager is `LocalAgentModeSessions` (80 methods: `start`, `sendMessage`, `setDraftSessionFolders`, `onToolPermissionRequest`, `respondToToolPermission`, `getTranscript`, `onEvent`, …), bridged to the renderer as `window.cowork`.
44
44
 
@@ -163,6 +163,8 @@ L1 reproduces this as a **default-deny forward proxy**: the agent's `HTTP(S)_PRO
163
163
 
164
164
  The policy that produces those `allow`/`deny` responses is the **Decider** seam (see the architecture diagram); to smoke-test a decider against a sample question without a full run, use `cowork-harness decide`.
165
165
 
166
+ > **Machine-readable form:** the five shapes below are schema'd as `schema/protocol.v1.json`, with a golden vector pack at `fixtures/protocol/v1/` — see [docs/protocol.md](./docs/protocol.md) for scope, versioning, and how to conformance-test against them.
167
+
166
168
  ### Control protocol — VERIFIED end-to-end against the live host CLI (macOS build 2.1.177+; the staged in-VM agent that L1/L2 run is 2.1.197, baseline `desktop-1.17377.1`)
167
169
 
168
170
  > The staged agent ELF is unchanged (2.1.181) across the 1.14271.0→1.15200.0 asar bump, and 2.1.187 across the 1.15200.0→1.15962.0 bump. The live scenario suite (`protocol` + `container` tiers) was re-run against the 1.15200.0 baseline; the 1.15962.0 bump was verified via asar analysis (content byte-identical: host-loop generator, system prompt, identity string, gates, and egress domains all unchanged) plus a full local test suite pass. The 1.15962.1→1.17377.1 bump moved the staged agent to **2.1.197** and added `api.claude.ai` to the egress allowlist; re-verified via `sync` (no unknown deltas) plus a manual asar spot-check of the reconstructed prompt content (substantively unchanged — see the Parity entry in CHANGELOG.md) and a full live scenario-suite pass (`protocol` + `container` tiers).