cowork-harness 0.7.0 → 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.
- package/.claude/skills/cowork-harness/scripts/assertion-keys.json +1 -0
- package/.claude/skills/cowork-harness/scripts/scenario.py +5 -1
- package/CHANGELOG.md +178 -0
- package/README.md +20 -17
- package/baselines/desktop-1.14271.0.json +220 -0
- package/baselines/provisioning/rootfs-provisioning.json +179 -0
- package/dist/agent/session.js +70 -16
- package/dist/assert.js +71 -24
- package/dist/baseline.js +32 -7
- package/dist/boundary-paths.js +73 -0
- package/dist/boundary.js +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/cli.js +317 -124
- package/dist/decide/decider.js +117 -9
- package/dist/decide/external-channel.js +3 -1
- package/dist/egress/proxy.js +11 -10
- package/dist/hostloop/workspace-handler.js +134 -22
- package/dist/prompt.js +10 -7
- package/dist/run/cassette.js +241 -67
- package/dist/run/chat.js +104 -35
- package/dist/run/doctor.js +5 -3
- package/dist/run/execute.js +173 -15
- package/dist/run/run.js +49 -19
- package/dist/run/runs-gc.js +16 -7
- package/dist/run/scaffold.js +21 -2
- package/dist/run/skill-hash.js +6 -6
- package/dist/run/trace-view.js +34 -21
- package/dist/run/verdict.js +12 -0
- package/dist/runtime/container.js +1 -1
- package/dist/runtime/hostloop-prompt.js +47 -0
- package/dist/runtime/hostloop.js +31 -6
- package/dist/runtime/image-capabilities.js +271 -0
- package/dist/runtime/lima.js +24 -4
- package/dist/runtime/protocol.js +1 -1
- package/dist/runtime/stage.js +17 -4
- package/dist/session.js +82 -25
- package/dist/staging/mount-naming.js +124 -0
- package/dist/sync/cowork-sync.js +33 -8
- package/dist/types.js +35 -2
- package/docker/Dockerfile.agent +98 -6
- package/docker/Dockerfile.proxy +4 -1
- package/docs/README.md +3 -1
- package/docs/boundary.md +2 -2
- package/docs/cassette.md +12 -7
- package/docs/chat.md +15 -13
- package/docs/cowork-spawn-contract-1.12603.1.md +5 -4
- package/docs/decider-dir.md +10 -0
- package/docs/discovery.md +1 -1
- package/docs/fidelity-gaps.md +3 -3
- package/docs/scenario.md +20 -6
- package/docs/session.md +9 -7
- package/package.json +3 -1
- package/python/README.md +7 -1
- package/python/cowork_harness.py +91 -7
- package/python/test_cowork_lane.py +102 -1
- package/schema/run-result.json +6 -1
- package/schema/scenario.schema.json +4 -0
- package/schema/session.schema.json +0 -3
- package/scripts/boot-rootfs-vz.ts +99 -0
- package/scripts/build-rootfs-image.ts +152 -0
- 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 = {
|
|
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,184 @@ 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
|
+
|
|
174
|
+
## [0.7.1] — 2026-06-20
|
|
175
|
+
|
|
176
|
+
### Fixed
|
|
177
|
+
|
|
178
|
+
- **`file_exists` and `user_visible_artifact` now pass for truncated (large) cassette artifacts.**
|
|
179
|
+
A truncated manifest entry (`truncated: true`) carries `path`, `bytes`, and `sha256` — positive
|
|
180
|
+
proof the file existed at record time. 0.7.0 incorrectly failed these existence/promotion
|
|
181
|
+
assertions with `"was truncated in the cassette — content was not committed; assertion cannot
|
|
182
|
+
pass"`, producing false-REDs for any cassette whose artifacts exceeded the 64 KiB inline cap.
|
|
183
|
+
Only **content** assertions (`artifact_json`) require the inlined body; existence assertions
|
|
184
|
+
now correctly pass from the manifest. `artifact_json` on a truncated artifact continues to fail
|
|
185
|
+
(the 0-byte placeholder is not valid JSON). Regression test added.
|
|
186
|
+
|
|
9
187
|
## [0.7.0] — 2026-06-19
|
|
10
188
|
|
|
11
189
|
### Added
|
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
|
|
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:
|
|
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
|
|
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.
|
|
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:
|
|
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
|
|
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
|
|
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/
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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` —
|
|
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.
|
|
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
|
|
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
|
+
}
|