cowork-harness 0.7.1 → 0.8.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 (61) hide show
  1. package/.claude/skills/cowork-harness/scripts/assertion-keys.json +1 -0
  2. package/.claude/skills/cowork-harness/scripts/scenario.py +5 -1
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +20 -17
  5. package/baselines/desktop-1.14271.0.json +220 -0
  6. package/baselines/provisioning/rootfs-provisioning.json +179 -0
  7. package/dist/agent/session.js +70 -16
  8. package/dist/assert.js +62 -22
  9. package/dist/baseline.js +32 -7
  10. package/dist/boundary-paths.js +73 -0
  11. package/dist/boundary.js +1 -1
  12. package/dist/cli-args.js +5 -0
  13. package/dist/cli.js +317 -124
  14. package/dist/decide/decider.js +117 -9
  15. package/dist/decide/external-channel.js +3 -1
  16. package/dist/egress/proxy.js +11 -10
  17. package/dist/hostloop/workspace-handler.js +134 -22
  18. package/dist/prompt.js +10 -7
  19. package/dist/run/cassette.js +236 -63
  20. package/dist/run/chat.js +104 -35
  21. package/dist/run/doctor.js +5 -3
  22. package/dist/run/execute.js +173 -15
  23. package/dist/run/run.js +49 -19
  24. package/dist/run/runs-gc.js +16 -7
  25. package/dist/run/scaffold.js +21 -2
  26. package/dist/run/skill-hash.js +6 -6
  27. package/dist/run/trace-view.js +34 -21
  28. package/dist/run/verdict.js +12 -0
  29. package/dist/runtime/container.js +1 -1
  30. package/dist/runtime/hostloop-prompt.js +47 -0
  31. package/dist/runtime/hostloop.js +31 -6
  32. package/dist/runtime/image-capabilities.js +271 -0
  33. package/dist/runtime/lima.js +24 -4
  34. package/dist/runtime/protocol.js +1 -1
  35. package/dist/runtime/stage.js +17 -4
  36. package/dist/session.js +82 -25
  37. package/dist/staging/mount-naming.js +124 -0
  38. package/dist/sync/cowork-sync.js +33 -8
  39. package/dist/types.js +35 -2
  40. package/docker/Dockerfile.agent +98 -6
  41. package/docker/Dockerfile.proxy +4 -1
  42. package/docs/README.md +3 -1
  43. package/docs/boundary.md +2 -2
  44. package/docs/cassette.md +12 -7
  45. package/docs/chat.md +15 -13
  46. package/docs/cowork-spawn-contract-1.12603.1.md +5 -4
  47. package/docs/decider-dir.md +10 -0
  48. package/docs/discovery.md +1 -1
  49. package/docs/fidelity-gaps.md +3 -3
  50. package/docs/scenario.md +20 -6
  51. package/docs/session.md +9 -7
  52. package/package.json +3 -1
  53. package/python/README.md +7 -1
  54. package/python/cowork_harness.py +91 -7
  55. package/python/test_cowork_lane.py +102 -1
  56. package/schema/run-result.json +6 -1
  57. package/schema/scenario.schema.json +4 -0
  58. package/schema/session.schema.json +0 -3
  59. package/scripts/boot-rootfs-vz.ts +99 -0
  60. package/scripts/build-rootfs-image.ts +152 -0
  61. package/scripts/capture-rootfs-manifest.ts +290 -0
@@ -2,6 +2,7 @@
2
2
  "$comment": "GENERATED from the Zod Assertion schema (src/types.ts) by scripts/gen-schema.ts — do not edit; run `npm run schema`.",
3
3
  "keys": [
4
4
  "allow_l0_plugin_divergence",
5
+ "allow_missing_capability",
5
6
  "allow_permissive_auto_allow",
6
7
  "artifact_json",
7
8
  "dispatch_count_max",
@@ -70,7 +70,11 @@ LIVE_ONLY_KEYS = {
70
70
  }
71
71
  EGRESS_KEYS = {"egress_denied", "egress_allowed"}
72
72
  # verdict modifiers — don't verify anything themselves (e.g. suppress a default-fail)
73
- VERDICT_MODIFIER_KEYS = {"allow_permissive_auto_allow", "allow_l0_plugin_divergence"}
73
+ VERDICT_MODIFIER_KEYS = {
74
+ "allow_permissive_auto_allow",
75
+ "allow_l0_plugin_divergence",
76
+ "allow_missing_capability",
77
+ }
74
78
 
75
79
  # Every key the replay-class logic knows how to handle. `replay_protocol_fidelity` is valid-but-not-authorable
76
80
  # (errored separately below). This is also the embedded fallback for ASSERT_KEYS — kept EQUAL to the generated
package/CHANGELOG.md CHANGED
@@ -6,6 +6,171 @@ All notable changes to this project are documented here. The format is based on
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.8.0] — 2026-06-21
10
+
11
+ ### Breaking changes
12
+
13
+ - **Work folders now mount at `mnt/<folder-name>`, not `mnt/.projects/<id>`; the folder `to:` field is
14
+ removed.** Binary-verified (asar 1.14271.0): real Cowork mounts each connected work folder at a
15
+ collision-resolved **basename** of its canonical path (e.g. `mnt/project`) with no author-chosen name —
16
+ so the session-schema `folders[].to` override is GONE (it had no Cowork analog; names are always derived).
17
+ Same-basename folders are disambiguated tier-accurately (host-loop keeps the first bare, the VM/container
18
+ tier escalates both with a `--parent` prefix). Plugins likewise move from the synthetic
19
+ `mnt/.local-plugins/cache/<…>` to the real `mnt/.local-plugins/marketplaces/<marketplace>/<plugin>` (no
20
+ `cache/`, no version segment). **Version-gated:** this applies to Desktop **≥ 1.14271.0** (current
21
+ baselines); older baselines keep the legacy `.projects/<id>` + `cache/` paths. `user_visible_artifact`
22
+ and the artifact manifest now derive their visible roots from the actual mount set (persisted as
23
+ `RunResult.userVisibleRoots`), and the cassette format bumps **v3 → v4** to store them.
24
+ - *Upgrade note:* remove `to:` from `folders[]` in session files (the name derives from the folder
25
+ basename). Reference connected-folder artifacts as `<folder-name>/…` (e.g. `project/summary.md`) instead
26
+ of `.projects/<id>/…`. A folder-artifact cassette recorded before v4 must be **re-recorded** (`rehash`
27
+ cannot migrate it — it only re-hashes skill fingerprints). A connected folder whose basename collides
28
+ with a reserved Cowork mount name (`outputs`, `uploads`, `.projects`, …) on the VM/container tier is now
29
+ rejected loudly instead of silently shadowing the fixed dir — rename the folder.
30
+
31
+ - **Run output now defaults to `~/.cowork-harness/runs`, not `<cwd>/runs`.** A `run` / `skill` / `chat` /
32
+ `record` launched from a repo no longer drops run artifacts (often sensitive skill inputs/outputs) into the
33
+ working tree — the root moved out of any working tree, matching the `~/.cowork-harness/` convention already
34
+ used for VM work dirs. The root is **flat and machine-global** (shared across every project on the machine),
35
+ not per-project. The readers (`trace` / `scaffold` / `verify-run`) resolve the same default, so a bare
36
+ `trace <run-id>` now works from any directory; the previous cwd-relative / repo-root resolution tiers were
37
+ removed. A one-time `runs → <dir>` line prints on stderr when the default is used (suppressed under
38
+ `--quiet` / `--output-format json`, or when an override is set).
39
+ - *Upgrade note (CI / scripts):* anything that reads `./runs` after a run — a CI `upload-artifact path: runs/`,
40
+ a glob over `runs/**`, a `.gitignore` entry — must now set **`COWORK_HARNESS_RUNS_DIR`** (or pass
41
+ `--run-dir`) to a workspace path so output lands where it's expected. Otherwise the step finds an empty
42
+ `./runs` (and, if it doesn't fail on empty, passes silently). The bundled CI recipe sets
43
+ `COWORK_HARNESS_RUNS_DIR: runs` on the live-scenario job for exactly this reason.
44
+
45
+ - **Agent image bumped `cowork-agent-base:1` → `:2` — REBUILD REQUIRED.** The image now mirrors the real
46
+ Cowork rootfs's preinstalled toolchain (binary-verified by mounting the rootfs): **Node 22.22.3** (was
47
+ ubuntu's node 12), the full **Python document/data stack** (openpyxl/pandas/numpy/pdfplumber/python-docx/
48
+ python-pptx/matplotlib/…), node doc-gen globals (pdf-lib/pptxgenjs/sharp/tsx/…), `ruby`/`ffmpeg`/`qpdf`,
49
+ `C.UTF-8` locale, and it now runs as **uid 1000 (`ubuntu`)** like the real VM. Rebuild with the command
50
+ `cowork-harness doctor --tier container` prints, or set `COWORK_AGENT_IMAGE` to your own tag. *(Why: the
51
+ real rootfs ships openpyxl etc. preinstalled — omitting them made skills that read `.xlsx`/PDFs falsely
52
+ appear degraded under the harness. "pypi blocked at runtime" ≠ "not preinstalled.")*
53
+ - **A green run that uses a capability the agent image OMITS now FAILS** (verdict signal `missing_capability`),
54
+ mirroring `permissive_auto_allow`/`l0_plugin_divergence`. The default "core" image omits the heavy lane
55
+ (OCR/LibreOffice/markitdown/opencv/PDF-tables); a run that uses one is a likely false negative (real Cowork
56
+ ships it). Suppress per-scenario with **`allow_missing_capability: true`** (when the fallback is equivalent),
57
+ rebuild full parity (`--build-arg COWORK_FULL_PARITY=1`), use the rootfs `max` tier, or skip the check with
58
+ `COWORK_SKIP_CAPABILITY_PROBE=1`. Live tiers only (container/hostloop/microvm).
59
+ - **Stricter, fail-loud guardrails from the codebase bug-review sweep — a few previously-silent paths now
60
+ error.** These close false-greens / false-accepts and can surface on an existing setup:
61
+ - **Negative `verify-run` assertions fail on *missing* evidence instead of passing vacuously.**
62
+ `tool_result_not_contains`, `tool_not_called`, `subagent_tool_absent`, `dispatch_count_max`,
63
+ `subagent_declared_but_unused`, `no_delete_in_outputs`, `transcript_no_host_path`, and `self_heal_ran`
64
+ now fail with an "evidence unavailable" reason when the underlying field is absent from a partial/older
65
+ `result.json` (verify-run lane only — the live and replay lanes, where the evidence is always present,
66
+ are unchanged). *Upgrade note:* re-run rather than re-assert against a fresh `result.json` if a verify-run
67
+ flips to this failure.
68
+ - **Non-strict cassette replay now fails on corrupt `controlOut`** — a malformed line, or a duplicate
69
+ `request_id` with differing bodies — instead of warning and proceeding. Corruption is a protocol-fidelity
70
+ failure, not advisory; `--strict` still additionally catches staleness/extra-data.
71
+ - **`chat --raw` now rejects `--upload` / `--folder` / `--plugin` / `--fidelity`** (it can't honor them in
72
+ native mode) instead of silently ignoring them, and an invalid `COWORK_HARNESS_FIDELITY` value is rejected
73
+ loudly instead of silently falling back to `container`. An invalid `COWORK_HARNESS_MAX_ARTIFACT_BYTES` now
74
+ errors (parity with the `--max-artifact-bytes` flag) instead of silently using the default.
75
+ - **A `web_fetch` `approved_domains` / seed entry that isn't a bare host** (a URL, scheme, path, port, empty
76
+ string, or `*` wildcard) is now rejected loudly instead of being added as an inert, never-matching entry.
77
+ - **An over-cap (> 256 KiB) control-out frame now fails the live recording immediately** with a clear error,
78
+ instead of writing an unreplayable truncation marker that only surfaced later as a replay failure.
79
+
80
+ ### Added
81
+
82
+ - **Capability fidelity detection.** The harness probes the agent runtime (Docker image via `--network none`
83
+ run, or the L2 microVM via `limactl shell`) for the document/OCR/Office capabilities the real Cowork rootfs
84
+ ships, caches the result by `(tier, identity)`, and detects (from `events.jsonl`) when a skill used an
85
+ omitted one — surfaced as the new `RunResult.missingCapabilityUse` field, a `::notice::`/`::warning::`, and
86
+ the `missing_capability` verdict above. New assertion `allow_missing_capability`.
87
+ - **Opt-in full-parity image** (`docker build --build-arg COWORK_FULL_PARITY=1 -t cowork-agent-full:2`) adds
88
+ tesseract / LibreOffice / opencv / onnxruntime+markitdown / camelot+tabula for OCR/Office/extraction skills.
89
+ - **Rootfs `max` tier (`npm run build:rootfs-image`)** — builds a Docker image from the user's OWN
90
+ `rootfs.img` (local, byte-for-byte parity; cached by rootfs mtime+size); point `COWORK_AGENT_IMAGE` at it.
91
+ - **Provisioning drift gate (`npm run capture:rootfs`)** — captures the rootfs's toolchain to
92
+ `baselines/provisioning/rootfs-provisioning.json`; `--check <image>` diffs a built image against it.
93
+ - **L2 (microVM) toolchain parity** — Lima provisioning now installs the same document/data stack
94
+ (best-effort, with 24.04 version drift accepted).
95
+ - **`--run-dir <path>` global flag** to relocate the runs root (a thin shim over `COWORK_HARNESS_RUNS_DIR`).
96
+ Precedence: `--run-dir` > `COWORK_HARNESS_RUNS_DIR` > `~/.cowork-harness/runs`. Keeps sensitive artifacts out
97
+ of a working tree without the prior `cd`-into-a-scratch-dir workaround. Both spellings (`--run-dir <path>` and
98
+ `--run-dir=<path>`) are accepted; unlike `--dotenv` it does not require the path to exist (it's an output dir).
99
+ - **Cross-project overwrite guard for pinned (`--session-id`) sessions.** On the flat shared root a pinned
100
+ `sess-<id>` run dir is deterministic, so two projects can resolve to the same path. The writer now identifies
101
+ a run by its **mounted-source content** (recorded in an `outDir/.origin` marker) and:
102
+ - **errors instead of silently `rm -rf`-ing** a dir that belongs to a different project (the old behavior
103
+ blind-deleted a colliding peer's persisted, resumable session);
104
+ - **fails closed** on a missing/partial marker (a crashed prior run) — it throws with a "delete `<dir>` to
105
+ reset" hint rather than deleting an unconfirmable dir;
106
+ - treats a **sourceless inline scenario** (which has no content to identify it, only cwd) as unconfirmable —
107
+ it throws rather than risk a cwd-collision delete (`skill <dir>` runs always mount the skill, so the common
108
+ pinned workflow is unaffected);
109
+ - blocks **`--resume`** onto a different project's session in place (override with
110
+ `COWORK_HARNESS_ALLOW_FOREIGN_RESUME=1`).
111
+ - **`runs gc` never prunes pinned `sess-*` sessions** (and they don't consume a `--keep-last` slot — partitioned
112
+ out before counting, so a retained pinned dir can't evict a newer ephemeral run). Only ephemeral `local_*`
113
+ runs are pruned. Because the default root is now shared, a bare `runs gc` prunes ephemeral runs across **all**
114
+ projects; pass an explicit `<runs-dir>` to scope it.
115
+ - **multiSelect `AskUserQuestion` gates are now answerable on every answer channel.** Scripted
116
+ `choose: [list]` already worked; the in-band `--decider-dir` channel now accepts a repeated
117
+ `--choose` (`answer <dir> --gate 1 --choose Auth --choose Audit`), and `--decider-cmd` helpers /
118
+ hand-written `resp-N.json` accept a JSON-array reply (`{"answers":{"<q>":["Auth","Audit"]}}`). All
119
+ channels deliver the binary-verified `", "`-joined wire shape; a member matching no option, an array
120
+ on a single-select gate, or an empty array each fails loud. `cowork-harness answer`'s `--choose` is
121
+ now repeatable for multiSelect gates (still single-only on single-select). Verified end-to-end
122
+ against a live model (the real agent re-reads the joined answer as multiple selections).
123
+ - **`scaffold --from-run`** flags a delivered answer that looks like a multiSelect set (contains
124
+ `", "`) with a loud comment telling the author to split `choose: "A, B"` into `choose: [A, B]`
125
+ before replay (a scaffolded multiSelect answer would otherwise not match on replay).
126
+
127
+ ### Fixed
128
+
129
+ - `doctor`'s staged-agent remedy now hints to put `COWORK_AGENT_BINARY` in `.env` so `--dotenv` covers it
130
+ (like the auth token) — avoiding a misleading "red" when `doctor` is run without the same env/flags the real
131
+ run uses.
132
+ - **`--decider-dir` / `--decider-cmd` no longer crash on a multiSelect array reply.** `coerceLabel`
133
+ previously called `.trim()` on a non-string answer and threw a bare `TypeError`, aborting the run;
134
+ it now throws a clear `UnansweredError` instead, and a multiSelect array is validated per-member and
135
+ delivered as the joined wire shape.
136
+ - **Codebase bug-review sweep — 49 validated fixes across the harness** (beyond the behavior changes noted
137
+ under Breaking):
138
+ - **CLI parsing:** `vm status --output-format json` works and emits a JSON envelope, instead of misreading
139
+ the flag as a baseline name; `skill` and common flags accept every documented `--flag=value` form;
140
+ `boundary-check` reports a missing/malformed session as a clean (JSON-aware) usage error rather than an
141
+ internal error; `chat` rejects extra positionals and empty/flag-looking value-flag arguments; `parseArgs`
142
+ rejects empty `--flag ""` values.
143
+ - **Path / boundary hardening:** named baselines can no longer escape `baselines/` via `../`; marketplace and
144
+ staged-mount symlinks that resolve out of tree are rejected (realpath, not lexical); collected artifacts
145
+ skip hardlinks (`nlink > 1`) that could inline out-of-root content into a cassette; a new
146
+ `src/boundary-paths.ts` centralizes `safeNamedBaseline` / `containedRealPath` / `normalizeHost` /
147
+ `validateBareDomain` so the egress allowlist and seed-domain paths share one policy.
148
+ - **Protocol / replay integrity:** every control-request validates its `request_id` before replying; the
149
+ AskUserQuestion body is validated at ingress (optionless / header-only gates still pass); a malformed
150
+ cassette no longer aborts the whole replay batch; a `web_fetch` decision of the wrong kind is recorded as
151
+ `mismatch→deny` with a warning; egress host matching is case- and trailing-dot-normalized.
152
+ - **host-loop fidelity:** the `web_fetch` SSRF backstop pins the vetted address through connect (closing a
153
+ DNS-rebind TOCTOU) and re-vets each redirect; a Docker infrastructure failure (daemon down, missing
154
+ container, exit 125) is reported to the model as a generic harness error and logged raw, instead of
155
+ leaking daemon text framed as a normal command exit.
156
+ - **Fidelity drift checks:** the rootfs `--check` diffs the whole Layer-A pip set (generated from the
157
+ Dockerfile) plus Node, the apt doc stack, and global npm; rootfs image tags are content-addressed; the
158
+ capability cache keys on image content rather than a mutable tag; `sync` cleans its temp extraction dir and
159
+ records a drift signal on a corrupt `config.json`.
160
+ - **decider / schema / Python:** `allow_if` predicates accept non-identifier input keys (`input["file-path"]`);
161
+ `choose`+`answer` and inert `grant` are rejected at schema time; optionless prompt/LLM gates are answerable;
162
+ the Python wrapper returns a `BatchResult` for directory/replay runs so a later failure can't hide behind a
163
+ passing first result.
164
+ - **Documentation audit sweep — stale references corrected, gaps filled.** Bumped the "current baseline" pins
165
+ across README / DESIGN / SPEC / spawn-contract / skill docs to `desktop-1.14271.0` (agent ELF 2.1.181),
166
+ re-verified end-to-end against the live staged agent at this baseline; added the missing
167
+ `tool_result_contains` / `tool_result_not_contains` and `allow_missing_capability` assertion rows plus a
168
+ verdict-signals (`prompt_asset_missing`) section to the docs; documented `doctor` / `rehash` / `runs gc` and
169
+ exit code `3` in `llms.txt` and the README command table; corrected the SPEC §11 exit-code table
170
+ (`boundary` → `3`), the `chat --folder` / `folders[].to` notes, the `trace --view tools` flag, and the
171
+ skill-bootstrap version floor (`@>=0.7.1`). **CI fix:** the agent-base image build in
172
+ `.github/workflows/ci.yml` was tagged `:1` while every code path defaults to `:2` — now `:2`.
173
+
9
174
  ## [0.7.1] — 2026-06-20
10
175
 
11
176
  ### Fixed
package/README.md CHANGED
@@ -42,7 +42,7 @@ A skill's behavior under Cowork is determined by four things, all reproducible o
42
42
  | Dimension | What Cowork does | How we reproduce it | Fidelity |
43
43
  |---|---|---|---|
44
44
  | **Agent** | Spawns the staged in-VM agent `claude-code-vm/<ver>/claude` in cowork mode (`CLAUDE_CODE_IS_COWORK=1` env — there is no `--cowork` flag) | Run the **same pinned agent**, **bind-mounted** from your Claude Desktop install's staged Linux/arm64 ELF (no npm path; override with `COWORK_AGENT_BINARY`) | **High** — same binary contract |
45
- | **Mounts** | `/sessions/<id>/mnt/{uploads,.projects/<id>,.local-plugins,.remote-plugins}` | Recreate the same paths as bind mounts; skill-under-test discovered at the plugin mount, same as Cowork | **High** — same discovery path |
45
+ | **Mounts** | `/sessions/<id>/mnt/{uploads,<folder-name>,.local-plugins,.remote-plugins}` (work folders mount at the collision-resolved folder basename; ≥1.14271.0, older baselines use `.projects/<id>`) | Recreate the same paths as bind mounts; skill-under-test discovered at the plugin mount, same as Cowork | **High** — same discovery path |
46
46
  | **Egress** | gVisor netstack with a compiled domain allowlist (`vmAllowedDomains()` + `coworkEgressAllowedHosts`) | Default-deny egress proxy enforcing the **synced** allowlist | **Med-High** — allowlist-exact, transport-approximate |
47
47
  | **Permissions / questions** | `onToolPermissionRequest` → `respondToToolPermission`; AskUserQuestion answered by the UI | The **Agent SDK `can_use_tool` control protocol** — the exact same channel — answered by your scenario script | **High** — same protocol Desktop uses |
48
48
 
@@ -101,6 +101,8 @@ Skill testing is the headline use, but the tool is a general harness over the Co
101
101
  | `boundary-check [baseline] [--session <file>]` | Prove the sandbox enforces Cowork's limitations; `--session` folds a session's `egress.extra_allow` into the probe allowlist | verifying the harness's own fidelity |
102
102
  | `sync` / `list` | Derive/refresh & list platform baselines from the Desktop install | after Claude Desktop updates (baselines ship, so it's optional otherwise) |
103
103
  | `doctor [--tier <t>]` | Read-only prerequisite check (Docker, staged agent, token, baseline); prints the exact `docker build` line if the agent image is missing | "can I run the live tiers — what's missing?" before a first live run |
104
+ | `runs gc [--keep-last <n>]` | Prune accumulated run dirs (keeps the N most recent per scenario; pinned `--session-id` runs are never pruned); `--dry-run` | the machine-global runs root has grown and you want space back |
105
+ | `rehash <dir/>` | Migrate cassette fingerprints to the current format version when the content is provably unchanged (`--dry-run`); no re-record needed | a cassette-format bump flagged committed fixtures as stale |
104
106
  | `vm <init\|status\|delete\|prune>` | Manage the L2 Apple-VZ / Lima microVM (`prune` removes orphaned VMs left by config/agent-version changes) | running `--fidelity microvm` |
105
107
 
106
108
  There's also a **Python `cowork` pytest lane** (`python/`) for driving any of this from `pytest` beside your normal tests — see [`python/README.md`](./python/README.md).
@@ -134,7 +136,7 @@ cowork-harness skill --help # full
134
136
 
135
137
  cowork-harness chat ~/my-plugin # interactive multi-turn REPL (full harness: egress sandbox + control protocol)
136
138
  # chat --raw → native interactive cowork mode via `docker run -it` (needs Docker + the arm64
137
- # cowork-agent-base:1 image; the egress sandbox is NOT applied in --raw)
139
+ # cowork-agent-base:2 image; the egress sandbox is NOT applied in --raw)
138
140
  ```
139
141
 
140
142
  **Input policy — no silent false-greens.** When an AskUserQuestion arrives with no scripted
@@ -186,7 +188,7 @@ npm install && npm run build && npm link # puts the `cowork-harness` command
186
188
  **Try it in 10 seconds — no token, no Docker.** A committed synthetic cassette replays on a fresh clone, so you can see a green run before setting anything up:
187
189
 
188
190
  ```bash
189
- cowork-harness replay --cassette examples/replays/example-pdf-skill.cassette.json
191
+ cowork-harness replay examples/replays/example-pdf-skill.cassette.json
190
192
  ```
191
193
 
192
194
  Only the committed-cassette `replay` above is fully self-contained. Live `run`/`skill` need the prerequisites in the next section — and note the `protocol` tier skips Docker and the staged agent but **still calls a real model** (via the host `claude`), so it needs the auth token.
@@ -200,7 +202,7 @@ This repo ships a **companion skill** (`.claude/skills/cowork-harness/`) that te
200
202
  /plugin install cowork-harness@cowork-harness
201
203
  ```
202
204
 
203
- The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.5.0` (a version floor that fails loud rather than silently fetching a too-old CLI; Node ≥ 20). Tiers above `protocol` still need Docker/Lima and a Claude Desktop agent binary — see the prerequisites below.
205
+ The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.8.0` (a version floor that fails loud rather than silently fetching a too-old CLI; Node ≥ 20). Tiers above `protocol` still need Docker/Lima and a Claude Desktop agent binary — see the prerequisites below.
204
206
 
205
207
  It also follows the open [Agent Skills](https://github.com/vercel-labs/skills) spec, so it installs cross-editor (Cursor, Codex, OpenCode, …) by pointing the `npx skills` CLI at `.claude/skills/cowork-harness` in this repo. (Working *inside* this repo, the skill auto-loads as a project skill — no install needed.)
206
208
 
@@ -208,7 +210,7 @@ It also follows the open [Agent Skills](https://github.com/vercel-labs/skills) s
208
210
 
209
211
  **Prerequisites for anything above `protocol` fidelity** (the `protocol` tier skips items 1–2 — no Docker, no staged agent — but still calls a real model via the host `claude`, so it needs item 3, the auth token; only a committed-cassette `replay` needs nothing at all):
210
212
  1. **Claude Desktop, opened once.** The Cowork agent binary is **bind-mounted from your own install** at run time — nothing Anthropic-owned is bundled. Open Cowork once so the agent ELF is staged (`…/claude-code-vm/<ver>/claude`); the harness auto-detects it, or set `COWORK_AGENT_BINARY=<path>` to point at it. Without a staged agent, container/cowork runs fail with "Open Cowork once to stage it…".
211
- 2. **Docker (arm64)** + the agent image: `docker build --platform linux/arm64 -t cowork-agent-base:1 -f docker/Dockerfile.agent .` (override the tag with `COWORK_AGENT_IMAGE`). The `-f docker/Dockerfile.agent .` paths are **repo-relative** — run it from a source checkout, not a global `npm install -g` (where the Dockerfile lives under the package dir). (Global install? Run `cowork-harness doctor --tier container` — it prints the exact `docker build` command with the correct package-local Dockerfile path.)
213
+ 2. **Docker (arm64)** + the agent image: `docker build --platform linux/arm64 -t cowork-agent-base:2 -f docker/Dockerfile.agent .` (override the tag with `COWORK_AGENT_IMAGE`). This **core** image mirrors the real Cowork rootfs's document/data toolchain (Node 22, openpyxl/pandas/pdf/docx/pptx, …). For OCR / LibreOffice / markitdown / opencv / PDF-table skills, build the **full-parity** image instead: `docker build --platform linux/arm64 --build-arg COWORK_FULL_PARITY=1 -t cowork-agent-full:2 -f docker/Dockerfile.agent .` and point `COWORK_AGENT_IMAGE` at it. (A run on the core image that uses an omitted capability is flagged — see capability detection.) The `-f docker/Dockerfile.agent .` paths are **repo-relative** — run it from a source checkout, not a global `npm install -g`. (Global install? Run `cowork-harness doctor --tier container` — it prints the exact `docker build` command with the correct package-local Dockerfile path.)
212
214
  3. **An auth token** — either `export CLAUDE_CODE_OAUTH_TOKEN=$(claude setup-token)` or a **`.env`** file (copy `.env.example` → `.env`; gitignored). The token resolves in priority order: exported env > `--dotenv <path>` > `./.env` (cwd) > `<install>/.env` (the package root), so a `npm link`ed install works from any directory. Keep `.env` at a working-dir or install root, never inside a mounted skill/project folder. (Use `--dotenv`, not `--env-file` — Node reserves the latter.)
213
215
 
214
216
  > `sync` (below) is **optional for a first run** — the repo ships `baselines/desktop-*.json`, so `baseline: latest` already resolves. Run `sync` only to refresh the platform baseline after Claude Desktop updates. (`sync` is **macOS-only** today; on Linux/Windows use the committed baselines — they work cross-platform.)
@@ -231,7 +233,7 @@ cowork-harness run examples/scenarios/ --output-format json
231
233
  # (without --out, the cassette is named after the scenario — its `name:`, or the filename)
232
234
  # Commit cassettes under examples/replays/ (this repo) or cassettes/ (conventional skill-repo name).
233
235
  cowork-harness record examples/scenarios/example-pdf-skill.yaml --out examples/replays/example-pdf-skill.cassette.json
234
- cowork-harness replay --cassette examples/replays/example-pdf-skill.cassette.json
236
+ cowork-harness replay examples/replays/example-pdf-skill.cassette.json
235
237
 
236
238
  # Cassettes are COMMITTED fixtures — record against synthetic data, and gate them in CI:
237
239
  cowork-harness verify-cassettes examples/replays/ # privacy scan (email/currency/domain) + staleness; exit 1 on a finding
@@ -317,13 +319,13 @@ max_thinking_tokens: 8000
317
319
  permission_mode: default
318
320
  permission_parity: cowork # cowork (allow unscripted tool calls, the default) | strict (deny unscripted)
319
321
  folders:
320
- - { from: ~/code/myproject, to: proj1 } # a work folder / Space -> mnt/.projects/proj1
322
+ - { from: ~/code/myproject } # a work folder / Space -> mnt/myproject (collision-resolved basename)
321
323
  uploads:
322
324
  - ~/Downloads/report.pdf # -> mnt/uploads
323
325
  plugins:
324
326
  marketplaces: ["https://github.com/anthropics/claude-code.git"]
325
327
  # local_marketplaces: ["../my-marketplace"] # LOCAL marketplace dirs (each with a marketplace.json)
326
- local_plugins: ["../skills/my-pdf-skill"] # mounted at mnt/.local-plugins/cache under the synthetic "local" marketplace
328
+ local_plugins: ["../skills/my-pdf-skill"] # mounted at mnt/.local-plugins/marketplaces/local-desktop-app-uploads/<name>
327
329
  enabled: ["my-pdf-skill@local"] # name@marketplace: a local_plugins entry is referenced as <plugin>@local
328
330
  mcp:
329
331
  config: ../data/mcp.json # standard mcpServers map (--mcp-config) — the way to attach an MCP server
@@ -345,7 +347,7 @@ The agent we run **is** `claude-code` (the same binary Cowork stages in `claude-
345
347
 
346
348
  | Kind | Real root | How the harness populates it | Override |
347
349
  |---|---|---|---|
348
- | Plugins / marketplaces | `CLAUDE_CONFIG_DIR/plugins`, `plugin_marketplaces` + Cowork mounts `.local-plugins/cache`, `.remote-plugins` | session `plugins.local_plugins`/`remote_plugins` → mounted at those paths; `marketplaces`/`local_marketplaces`/`enabled` → `settings.json` | point `plugins.config_dir` at a test dir |
350
+ | Plugins / marketplaces | `CLAUDE_CONFIG_DIR/plugins`, `plugin_marketplaces` + Cowork mounts `.local-plugins/marketplaces/<marketplace>/<plugin>` (≥1.14271.0; older baselines use `.local-plugins/cache`), `.remote-plugins` | session `plugins.local_plugins`/`remote_plugins` → mounted at those paths; `marketplaces`/`local_marketplaces`/`enabled` → `settings.json` | point `plugins.config_dir` at a test dir |
349
351
  | Skills | `CLAUDE_CONFIG_DIR/skills` + skills inside plugins | session `skills.local` staged into the config dir; plugin skills discovered at the mount | swap the config dir or local dirs |
350
352
  | MCP servers | `.mcp.json` / `--mcp-config`, `enabledMcpjsonServers` | session `mcp.config` → `--mcp-config`; `mcp.enabled` → `settings.json` | use a test `mcp.json` |
351
353
 
@@ -357,7 +359,7 @@ The harness builds a **clean managed `CLAUDE_CONFIG_DIR` per run** (with a gener
357
359
 
358
360
  ## What you get out (inspectable output)
359
361
 
360
- Every run writes `runs/<scenario>/<sessionId>/`:
362
+ Every run writes to `~/.cowork-harness/runs/<scenario>/<sessionId>/` (out of any working tree; relocate with `--run-dir <path>` or `COWORK_HARNESS_RUNS_DIR`):
361
363
 
362
364
  ```
363
365
  events.jsonl full stream-json event log (child→driver; the cassette source)
@@ -388,7 +390,7 @@ Secrets (the injected OAuth token / API key) are scrubbed from every persisted l
388
390
  │ Agent: claude -p (CLAUDE_CODE_IS_COWORK=1) │
389
391
  │ --input-format / --output-format stream-json│
390
392
  │ cwd = /sessions/<id>/mnt │
391
- │ mnt/uploads · mnt/.projects/* · plugins │
393
+ │ mnt/uploads · mnt/<folder-name> · plugins │
392
394
  └───────────────────────┬────────────────────────┘
393
395
  decision control request │ outbound network (egress)
394
396
  (tool · question · dialog) │ default-deny → allowlist
@@ -452,9 +454,10 @@ Unit tests cover the scripted-answer logic, the egress allowlist matcher, the se
452
454
 
453
455
  - `COWORK_LOCKDOWN=off` — relax container hardening for debugging (default `on`). With it `on`, an L2 microVM whose guest egress firewall fails to apply **aborts loudly** rather than running un-isolated.
454
456
  - `COWORK_CONTAINER_RUNTIME=podman` — use Podman instead of Docker.
455
- - `COWORK_AGENT_IMAGE=<tag>` — override the agent image name (default `cowork-agent-base:1`); `COWORK_AGENT_BINARY=<path>` — override the auto-detected staged agent ELF.
457
+ - `COWORK_AGENT_IMAGE=<tag>` — override the agent image name (default `cowork-agent-base:2`); `COWORK_AGENT_BINARY=<path>` — override the auto-detected staged agent ELF.
458
+ - `COWORK_SKIP_CAPABILITY_PROBE=1` — skip the per-run capability probe (the harness otherwise probes the agent image/VM for the document/OCR/Office capabilities the real Cowork rootfs ships and **fails a run that uses one the image omits** — a likely false negative; suppress per-scenario with `allow_missing_capability: true`, or rebuild full parity).
456
459
  - `COWORK_HARNESS_DECIDER_DIR_POLL_MS` / `_TIMEOUT_MS` — tune the `--decider-dir` rendezvous poll/backstop; `COWORK_HARNESS_DECIDER_CMD_TIMEOUT_MS` / `COWORK_HARNESS_LLM_TIMEOUT_MS` — backstop a hung `--decider-cmd` helper / `--decider-llm` model call (default 600 s, fail loud); `COWORK_HARNESS_DIALOG_TIMEOUT_MS` — override the 6 s dialog auto-cancel.
457
- - `COWORK_HARNESS_RUNS_DIR` — relocate the `runs/` output root (so `trace` resolves runs from any directory).
460
+ - `COWORK_HARNESS_RUNS_DIR` (or the `--run-dir <path>` flag) — override the default run-output root `~/.cowork-harness/runs` (kept out of any working tree so sensitive skill inputs/outputs don't land in a repo). Precedence: `--run-dir` > env > default. The root is flat and machine-global (shared across projects); pinned `--session-id` runs are guarded against cross-project overwrite, and `runs gc` never prunes them. In CI, set it to a workspace path (e.g. `runs`) so artifact upload can collect the runs. `COWORK_HARNESS_ALLOW_FOREIGN_RESUME=1` overrides the guard that blocks `--resume` onto another project's pinned session.
458
461
  - **Networking / loop:** `COWORK_EGRESS_PROXY` overrides the egress-proxy URL injected into the sandbox; `COWORK_PROXY_IMAGE` overrides the egress proxy Docker image name (default `cowork-egress-proxy:1`); `COWORK_DOCKER_NETWORK` pins the Docker network the agent container joins; `CLAUDE_FORCE_HOST_LOOP=1` forces the host-loop path regardless of the baseline's loop decision (the `cowork` tier's auto-pick). `COWORK_LIMACTL` overrides the `limactl` binary path (default `/opt/homebrew/bin/limactl`).
459
462
  - **Strictness escape hatches** (the harness fails loud by default): `COWORK_HARNESS_SOFT_MISSING=1` downgrades a missing mount source from a hard error to warn-and-exclude; `COWORK_HARNESS_ALLOW_CONFIG_DIR_WRITE=1` permits writing into an existing pinned `plugins.config_dir` (otherwise refused, to avoid clobbering a real Claude config).
460
463
  - **Secret scrubbing:** `COWORK_HARNESS_SCRUB_KEYS=<KEY1,KEY2>` adds extra env-var names whose values are redacted from logs (beyond the known auth tokens + `ANTHROPIC_CUSTOM_HEADERS`); `COWORK_HARNESS_SCRUB_VALUES=<v1,v2>` redacts literal values regardless of env. **Committed-cassette redaction:** `COWORK_HARNESS_REDACT_PATTERNS=<rx1,rx2>` / `COWORK_HARNESS_REDACT_KEYS=<k1,k2>` extend the privacy layer that scrubs recorded `controlOut` before a cassette is written for commit.
@@ -505,7 +508,7 @@ This repo is built to be driven by agents, not just read by humans:
505
508
 
506
509
  - **[AGENTS.md](./AGENTS.md)** — the canonical agent-instructions file (architecture seams, the build gate, invariants, ethos). Read it before changing code. Also indexed in **[llms.txt](./llms.txt)**.
507
510
  - **Companion skill** — [`.claude/skills/cowork-harness/`](./.claude/skills/cowork-harness/SKILL.md) teaches an agent to drive the harness; install it via the marketplace (see [above](#drive-it-from-claude-code-companion-skill)).
508
- - **Machine-readable interfaces** — stable `--output-format json` envelope on stdout, deterministic exit codes (`0`/`1`/`2`), and `--help` on every command.
511
+ - **Machine-readable interfaces** — stable `--output-format json` envelope on stdout, deterministic exit codes (`0`/`1`/`2`/`3`), and `--help` on every command.
509
512
  - **JSON Schemas** — [`schema/scenario.schema.json`](./schema/scenario.schema.json) and [`schema/session.schema.json`](./schema/session.schema.json) describe every field of the YAML you author (generated from the source schemas; `npm run schema`).
510
513
 
511
514
  ---
@@ -530,16 +533,16 @@ This repo is built to be driven by agents, not just read by humans:
530
533
 
531
534
  ## Status
532
535
 
533
- **Verified end-to-end against the live staged agent (2.1.177 / asar 1.13576.1).**
536
+ **Verified end-to-end against the live staged agent (2.1.181 / asar 1.14271.0).**
534
537
 
535
538
  - ✅ **Three isolation tiers (L0/L1/L2) + two loop-mode overlays** — `protocol` (L0 control loop), `container` (L1 sandboxed arm64 + per-run default-deny egress sidecar), `microvm` (L2 real Apple-VZ Linux microVM + guest firewall); plus the loop-mode overlays `hostloop` (production split-execution: agent loop on host, shell/web via the workspace SDK-MCP server) and `cowork` (auto-picks host-loop vs container the way Cowork does — gate `1143815894`). Egress enforced at container/microvm/hostloop; `boundary-check` reports **ALL CONSTRAINTS ENFORCED**.
536
539
  - ✅ **Three-seam driver** — `AgentSession` (control protocol) → `Decider` (scripted + `on_unanswered` policy, no silent false-greens) → `Run` (turn loop, sub-agent dispatch tree, `RunRecord`). Multi-turn `chat`, deterministic cassette `record`/`replay` (no token), `run.jsonl`/`trace.json` logging with secret-scrub.
537
- - ✅ **Answering live questions, every way you'd need** — `--decider-llm --intent "<one line>"` (a small model picks per question, steered by your test intent — the ergonomic default; non-determinism is flagged so a green isn't mistaken for a scripted pass), `--answer-policy <yaml>`/`--answer "rx=c"` (declarative regex→label, deterministic CI), `--decider-cmd '<helper>'` (custom logic — the Python `serve_decider(fn)` adapter pre-builds the wire loop so the helper writes only the decision), and **`--decider-dir <dir>`** (the *driving agent* answers each gate **in-band** with full context — it arms a Monitor on `cowork-harness gates <dir> --follow` and replies with `cowork-harness answer <dir> --gate <N> --choose <label>`; the session-under-test stays live, no resume/re-worded question — binary-verified). Every channel keeps stdout free, so all compose with `--output-format json`. A question is **never silently answered with option 1** — unhandled fails loud. Validate any decider in ~2s with `cowork-harness decide`.
540
+ - ✅ **Answering live questions, every way you'd need** — `--decider-llm --intent "<one line>"` (a small model picks per question, steered by your test intent — the ergonomic default; non-determinism is flagged so a green isn't mistaken for a scripted pass), `--answer-policy <yaml>`/`--answer "rx=c"` (declarative regex→label, deterministic CI), `--decider-cmd '<helper>'` (custom logic — the Python `serve_decider(fn)` adapter pre-builds the wire loop so the helper writes only the decision), and **`--decider-dir <dir>`** (the *driving agent* answers each gate **in-band** with full context — it arms a Monitor on `cowork-harness gates <dir> --follow` and replies with `cowork-harness answer <dir> --gate <N> --choose <label>`; the session-under-test stays live, no resume/re-worded question — binary-verified). **multiSelect gates** are answerable on every channel — repeat `--choose` (or send a JSON-array reply from a `--decider-cmd` helper / hand-written `resp-N.json`); all deliver the binary-verified `", "`-joined wire shape. Every channel keeps stdout free, so all compose with `--output-format json`. A question is **never silently answered with option 1** — unhandled fails loud. Validate any decider in ~2s with `cowork-harness decide`.
538
541
  - ✅ **Sub-agent aggregation + `trace`** — recognizes the real `Agent` dispatch tool (binary-verified; `Task` is its alias) so `subagent_dispatched`/`dispatch_count_max` fire under `--fidelity cowork`, excluding the `TaskCreate` todo list; `cowork-harness trace <id> --view tools` digests `events.jsonl` (each tool row now shows its **result status** `ok`/`error`), and `trace <id> --view questions` shows the gate lifecycle (**question → injected answer → delivered result**); `result.json`/`--keep` surface the deep `mnt/outputs` deliverable path.
539
542
  - ✅ **Answer delivery is verified, not assumed** — an AskUserQuestion answer is injected as the binary's full tool input (`{questions, answers}`, ELF-verified), so the answer actually reaches the model; `RunResult.gateDeliveries[]` + the `gate_answers_delivered` assertion catch any delivery failure, and `RunResult.toolCounts` gives the **truthful** per-tool call count (host-routed `WebSearch` shows here, not the always-0 `usage.server_tool_use`).
540
543
  - ✅ **Binary-grounded fidelity** — cwd `/sessions/<id>`, the three-channel MCP model (`--mcp-config` honored in plain cowork mode; host/API-routed `web_fetch`; SDK-server delivery), host-loop tool partition, auth-env token-only drop, and production GrowthBook gates pinned per release.
541
544
  - ✅ The agent binary is **bind-mounted from your own install** at run time — nothing Anthropic-owned is in any image or distributed.
542
- - ✅ **File provision & session resume** — `--upload <file>` / `--folder <dir>` attach files & connect folders (`mnt/uploads`, `mnt/.projects`); `--session-id <id>` + `--resume` persist and continue a session via the agent's *native* resume (binary-verified), so checkpoint-and-resume gated skills are testable. Demonstrated end-to-end in the live-contract suite (`test/live-contract.test.ts`: codeword established → resumed → recalled).
545
+ - ✅ **File provision & session resume** — `--upload <file>` / `--folder <dir>` attach files & connect folders (`mnt/uploads`, `mnt/<folder-name>`); `--session-id <id>` + `--resume` persist and continue a session via the agent's *native* resume (binary-verified), so checkpoint-and-resume gated skills are testable. Demonstrated end-to-end in the live-contract suite (`test/live-contract.test.ts`: codeword established → resumed → recalled).
543
546
  - ✅ **The full unit suite + the live-contract suite green**; a `cowork` pytest lane (`python/`) for skill authors.
544
547
  - ℹ️ **Auth:** a `claude setup-token` OAuth token (or `ANTHROPIC_API_KEY`), provided via the env, `--dotenv <path>`, `./.env`, or the install's own `.env` (gitignored; keep it out of mounted folders). It's passed into the sandbox **off the process argv** (Docker: `-e KEY` inherit-by-name; microVM: a stdin prologue) so the token isn't visible via `ps`/`/proc`, scrubbed from logs, never persisted in a runtime path; the token-only path mirrors the desktop. A fresh `CLAUDE_CONFIG_DIR` alone breaks local OAuth.
545
548
 
@@ -0,0 +1,220 @@
1
+ {
2
+ "baselineVersion": 1,
3
+ "appVersion": "1.14271.0",
4
+ "agentVersion": "2.1.181",
5
+ "agentBinary": {
6
+ "stagedPath": "~/Library/Application Support/Claude/claude-code-vm/2.1.181/claude",
7
+ "format": "elf-aarch64"
8
+ },
9
+ "guest": {
10
+ "os": "linux",
11
+ "arch": "arm64",
12
+ "baseImage": "ubuntu:22.04"
13
+ },
14
+ "spawn": {
15
+ "configDirInGuest": "mnt/.claude",
16
+ "settingSources": [
17
+ "user"
18
+ ],
19
+ "permissionMode": "default",
20
+ "maxThinkingTokens": 31999,
21
+ "effortDefault": "medium",
22
+ "tools": [
23
+ "Task",
24
+ "Bash",
25
+ "Glob",
26
+ "Grep",
27
+ "Read",
28
+ "Edit",
29
+ "Write",
30
+ "NotebookEdit",
31
+ "WebFetch",
32
+ "TaskCreate",
33
+ "TaskUpdate",
34
+ "TaskGet",
35
+ "TaskList",
36
+ "TaskStop",
37
+ "WebSearch",
38
+ "Skill",
39
+ "REPL",
40
+ "JavaScript",
41
+ "AskUserQuestion",
42
+ "ToolSearch"
43
+ ],
44
+ "allowedTools": [
45
+ "Task",
46
+ "Bash",
47
+ "Glob",
48
+ "Grep",
49
+ "Read",
50
+ "Edit",
51
+ "Write",
52
+ "NotebookEdit",
53
+ "WebFetch",
54
+ "TaskCreate",
55
+ "TaskUpdate",
56
+ "TaskGet",
57
+ "TaskList",
58
+ "TaskStop",
59
+ "WebSearch",
60
+ "Skill",
61
+ "REPL",
62
+ "JavaScript",
63
+ "ToolSearch"
64
+ ],
65
+ "env": {
66
+ "CLAUDE_CODE_IS_COWORK": "1",
67
+ "CLAUDE_CODE_ENTRYPOINT": "local-agent",
68
+ "CLAUDE_CODE_TAGS": "lam_session_type:chat",
69
+ "CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST": "1",
70
+ "CLAUDE_CODE_ENABLE_ASK_USER_QUESTION_TOOL": "true",
71
+ "CLAUDE_CODE_DISABLE_CRON": "1",
72
+ "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS": "1",
73
+ "CLAUDE_CODE_DISABLE_AGENTS_FLEET": "1",
74
+ "CLAUDE_CODE_ENABLE_APPEND_SUBAGENT_PROMPT": "1",
75
+ "CLAUDE_CODE_ENABLE_TASKS": "true",
76
+ "CLAUDE_CODE_DISABLE_TERMINAL_TITLE": "1",
77
+ "ENABLE_PROMPT_CACHING_1H": "1",
78
+ "DISABLE_MICROCOMPACT": "1",
79
+ "MCP_CONNECTION_NONBLOCKING": "true"
80
+ },
81
+ "promptTemplate": "prompts/desktop-1.12603.1/system-prompt-append.md",
82
+ "subagentAppend": "prompts/desktop-1.12603.1/subagent-append-vm.md",
83
+ "$comment": "Binary-verified Desktop->agent spawn contract (asar 1.12603.1). See docs/cowork-spawn-contract-1.12603.1.md.",
84
+ "$comment_notSet": "Deliberately NOT set: CLAUDE_CODE_USE_COWORK_PLUGINS (Desktop never sets it; would flip the agent to cowork_settings.json/cowork_plugins). Host-derived (TZ, account UUIDs, WORKSPACE_HOST_PATHS, OTEL) injected at runtime, not pinned.",
85
+ "$comment_prompts": "Reconstructed cowork-specific sections (not the full base prompt — not cleanly extractable). The main append is delivered via the --append-system-prompt CLI flag (layered on the agent's built-in base prompt), NOT the initialize handshake; only the subagent append goes over initialize (appendSubagentSystemPrompt), gated on CLAUDE_CODE_ENABLE_APPEND_SUBAGENT_PROMPT."
86
+ },
87
+ "mountLayout": {
88
+ "sessionRoot": "/sessions/{sessionId}",
89
+ "cwd": "/sessions/{sessionId}",
90
+ "mntRoot": "/sessions/{sessionId}/mnt",
91
+ "mounts": [
92
+ {
93
+ "name": "uploads",
94
+ "mountPath": "uploads",
95
+ "mode": "r",
96
+ "purpose": "user-uploaded files (read-only — asar 'ro')"
97
+ },
98
+ {
99
+ "name": "projects",
100
+ "mountPath": ".projects/{projectId}",
101
+ "mode": "rw",
102
+ "purpose": "RESERVED namespace (and the separate UUID project-sync feature) — NOT the work-folder path. From Desktop 1.14271.0 selected work folders mount at mnt/<collision-resolved-basename> (dynamic, derived per session by buildLaunchPlan; see MOUNT_BARE_NAME_MIN_VERSION). This decorative row is not consumed for binding (staged paths come from plan.mounts)."
103
+ },
104
+ {
105
+ "name": "local-plugins",
106
+ "mountPath": ".local-plugins/marketplaces",
107
+ "mode": "r",
108
+ "purpose": "marketplace skills/plugins, runtime-discovered"
109
+ },
110
+ {
111
+ "name": "remote-plugins",
112
+ "mountPath": ".remote-plugins",
113
+ "mode": "r",
114
+ "purpose": "org-remote plugins, runtime-discovered"
115
+ },
116
+ {
117
+ "name": "outputs",
118
+ "mountPath": "outputs",
119
+ "mode": "rw",
120
+ "purpose": "session outputs/artifacts — delete denied by default (asar IX); rwd only when approved"
121
+ }
122
+ ]
123
+ },
124
+ "network": {
125
+ "mode": "gvisor",
126
+ "allowKind": "allowlist",
127
+ "allowDomains": [
128
+ "sentry.io",
129
+ "preview.claude.ai",
130
+ "downloads.claude.ai",
131
+ "api.anthropic.com",
132
+ "a-cdn.anthropic.com",
133
+ "a-api.anthropic.com",
134
+ "console.anthropic.com",
135
+ "api-staging.anthropic.com",
136
+ "www.anthropic.com",
137
+ "docs.anthropic.com",
138
+ "mcp-proxy.anthropic.com",
139
+ "pivot.claude.ai",
140
+ "support.anthropic.com",
141
+ "assets.claude.ai"
142
+ ]
143
+ },
144
+ "bgEnvStrip": {
145
+ "knownVars": [
146
+ "CLAUDE_CODE_OAUTH_TOKEN",
147
+ "CLAUDE_CODE_SESSION_KIND",
148
+ "CLAUDE_CODE_SESSION_ID",
149
+ "CLAUDE_CODE_SESSION_NAME",
150
+ "CLAUDE_CODE_SESSION_LOG"
151
+ ]
152
+ },
153
+ "$comment": "Platform baseline auto-derived by `cowork-harness sync` from a live Claude Desktop install + app.asar. VOLATILE per-release facts only. Regenerate per release; review the diff. Captured 2026-06-12 on macOS arm64.",
154
+ "capturedAt": "2026-06-20",
155
+ "platform": "darwin-arm64",
156
+ "settings": {
157
+ "autoMountFolders": {
158
+ "key": "autoMountFolders",
159
+ "default": false
160
+ },
161
+ "localAgentModeTrustedFolders": {
162
+ "key": "localAgentModeTrustedFolders",
163
+ "default": []
164
+ }
165
+ },
166
+ "provenance": {
167
+ "asarPath": "/Applications/Claude.app/Contents/Resources/app.asar",
168
+ "asarFingerprint": "bd96569a339874cb",
169
+ "gates": {
170
+ "$comment": "Production GrowthBook gate states decoded from ~/Library/Application Support/Claude/fcache (standard interactive Anthropic account, 2026-06-13; binary-verified app.asar 1.12603.1). Pin per release. Behavior-affecting gates the harness models: 1143815894 (loop), 1648655587 (dispatch cap), 1978029737 (web_fetch routing). Telemetry/auth-internal gates omitted.",
171
+ "bridgeSdkTransport:583857784": {
172
+ "on": true,
173
+ "source": "force",
174
+ "value": true,
175
+ "note": "— Cowork uses the SDK-based transport (control protocol), confirming the harness's sdkMcpServers/mcp_message path is the production transport."
176
+ },
177
+ "hostLoop:1143815894": {
178
+ "on": true,
179
+ "source": "force",
180
+ "value": true
181
+ },
182
+ "taskDispatchLimiter:1648655587": {
183
+ "on": true,
184
+ "source": "force",
185
+ "value": {
186
+ "perTask": 1,
187
+ "global": 3
188
+ },
189
+ "note": "perTask=1 global=3 (host-side SKIP: recordSkipAndEmit/GCA.PerTaskLimit — NOT queue/deny). A dispatch session launches <=1 sub-task; <=3 concurrent globally."
190
+ },
191
+ "coworkRuntimeConfig:1978029737": {
192
+ "on": true,
193
+ "source": "force",
194
+ "value": {
195
+ "sessionsBridgePollBlockMs": 30,
196
+ "coworkNativeFilePreview": true,
197
+ "coworkWebFetchViaApi": true,
198
+ "coworkWebFetchPrompt": true,
199
+ "workspaceBashWaitLonger": true
200
+ },
201
+ "note": "coworkWebFetchViaApi=true coworkWebFetchPrompt=true workspaceBashWaitLonger=true sessionsBridgePollBlockMs=30 — web_fetch is host/API-routed (POST /api/organizations/<org>/cowork/web_fetch), NOT container egress; gated by a separate web-fetch hostname allowlist + URL provenance."
202
+ },
203
+ "cliPlugin:2307090146": {
204
+ "on": false,
205
+ "source": "defaultValue",
206
+ "value": false,
207
+ "note": "— the CLI-plugin credential broker is dark-launched off for standard interactive accounts (Ch23/L106)."
208
+ },
209
+ "pluginSyncSparkplug:2340532315": {
210
+ "on": true,
211
+ "source": "force",
212
+ "value": true,
213
+ "note": "— startup syncPlugins(); plugins load via --plugin-dir (registry inert in-VM)."
214
+ }
215
+ },
216
+ "eipcChannelUuid": "4f426349-8d6f-45f3-ae22-280fef323564",
217
+ "$comment": "eipcChannelUuid is per-build; recorded for provenance only — the harness does not use Desktop IPC."
218
+ },
219
+ "requireFullVmSandbox": null
220
+ }