cowork-harness 0.10.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +162 -0
- package/README.md +18 -12
- package/baselines/desktop-1.15200.0.json +220 -0
- package/baselines/prompts/desktop-1.15200.0/subagent-append-vm.md +18 -0
- package/baselines/prompts/desktop-1.15200.0/system-prompt-append.md +58 -0
- package/dist/async-pool.js +27 -0
- package/dist/cli.js +74 -17
- package/dist/decide/decider.js +57 -6
- package/dist/prompt.js +12 -2
- package/dist/run/cassette.js +124 -25
- package/dist/run/doctor.js +31 -4
- package/dist/run/execute.js +125 -5
- package/dist/run/inspect-view.js +95 -0
- package/dist/run/run.js +8 -0
- package/dist/run/runs-gc.js +8 -8
- package/dist/run/scaffold.js +22 -6
- package/dist/run/skill-hash.js +61 -18
- package/dist/run/trace-view.js +1 -1
- package/dist/runtime/hostloop-prompt.js +5 -1
- package/dist/runtime/hostloop.js +2 -5
- package/dist/runtime/image-capabilities.js +23 -0
- package/dist/session.js +1 -1
- package/docs/cassette.md +42 -2
- package/docs/cowork-spawn-contract-1.12603.1.md +1 -1
- package/docs/scenario.md +47 -11
- package/docs/session.md +3 -1
- package/examples/replays/example-pdf-skill.cassette.json +102 -22
- package/package.json +1 -1
- package/schema/cassette.v6.json +5 -0
- package/schema/run-result.json +2 -0
- package/schema/session.schema.json +1 -0
- package/scripts/check-versions.ts +27 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,168 @@ All notable changes to this project are documented here. The format is based on
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/). The project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/); pre-1.0 minor versions may include breaking changes.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.13.0] — 2026-06-25
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **`inspect <run-id | run-dir>` — see what a run produced.** Lists the run's artifacts and prints a shallow
|
|
14
|
+
field preview of each JSON artifact (scalars inline, arrays as a count, nested objects collapsed), so
|
|
15
|
+
"did it do the job?" is a first-class check instead of hand-parsing `…/mnt/outputs/...`. `--output-format
|
|
16
|
+
json` emits a structured digest. When the work dir was torn down (a non-`--keep` container/microvm run),
|
|
17
|
+
the artifact manifest still prints from `result.json` and the preview notes it's unavailable.
|
|
18
|
+
- **A whiffed gate no longer discards the paid run.** When a run exits on an unanswered gate
|
|
19
|
+
(`on_unanswered: fail`), the harness now salvages a **PARTIAL** `result.json` (+ `run.jsonl`/`trace.json`)
|
|
20
|
+
with the artifacts the agent wrote before the whiff, then still exits 2. The run dir is printed so you can
|
|
21
|
+
`inspect` it. `partial` and `unansweredGate` are new `RunResult` fields; `verify-run` and `scaffold`
|
|
22
|
+
refuse to treat a partial run's half-finished output as a passing result (scaffold still emits the gates,
|
|
23
|
+
drops the artifact/result asserts, and warns loudly).
|
|
24
|
+
- **Capability pre-flight — fail fast.** A skill that declares `requires_capabilities` against an image that
|
|
25
|
+
provably omits them now **aborts before the (paid) run** (`exit 3`) instead of running ~12 min to a verdict
|
|
26
|
+
that's already known — unless the scenario asserts `allow_missing_capability: true`, which downgrades it to
|
|
27
|
+
a notice and proceeds. `doctor` also surfaces the full-parity remedy on its agent-image line.
|
|
28
|
+
|
|
29
|
+
### Changed
|
|
30
|
+
|
|
31
|
+
- **LLM decider tolerates a near-miss label.** `--decider-llm` now binds a reply like `Confirmed.` or
|
|
32
|
+
`"Confirmed"` to the label `Confirmed` (trailing sentence punctuation / surrounding quotes trimmed before
|
|
33
|
+
matching) instead of failing loud — common on binary confirm gates. The `:` of the `OTHER:` free-text
|
|
34
|
+
sentinel is never stripped, and fuzzy substring matching stays off, so the change can't mis-bind. The
|
|
35
|
+
tolerance lives in `matchLabel` itself, so the web_fetch-approval path gets it too (a `Deny.` reply now
|
|
36
|
+
binds `Deny`).
|
|
37
|
+
- **`runs gc` → top-level `prune`; the `runs` namespace is dropped.** Pruning accumulated run dirs is now
|
|
38
|
+
`cowork-harness prune [--keep-last <n>] [--dry-run] [<runs-dir>]` (same flags). `runs` was a namespace with
|
|
39
|
+
a single member and collided confusingly with `run` (execute); removing it leaves `run` = execute,
|
|
40
|
+
inspection verbs (`trace`/`inspect`/`verify-run`/`scaffold`) top-level, and `prune` for cleanup.
|
|
41
|
+
|
|
42
|
+
### Fixed
|
|
43
|
+
|
|
44
|
+
- **Session-protocol over-cap test no longer flakes.** The over-cap control-out test read
|
|
45
|
+
`control-out.jsonl` synchronously, but the stream opens it asynchronously (`createWriteStream`, flags
|
|
46
|
+
`"a"`), so under load the file may not exist yet at the read — a flaky ENOENT. The over-cap frame is
|
|
47
|
+
rejected before any write, so a missing file means nothing was written; the test now treats absence as
|
|
48
|
+
empty.
|
|
49
|
+
|
|
50
|
+
### Docs
|
|
51
|
+
|
|
52
|
+
- **Documented the LLM-decider free-text path.** `--decider-llm` supplies free text via `OTHER: <value>`
|
|
53
|
+
on an options-bearing gate; a bare out-of-set answer (no matching label, no `OTHER:`) fails loud
|
|
54
|
+
(`UnansweredError` → exit 2), never stalling or guessing, and open-ended (no-option) gates need no
|
|
55
|
+
prefix. Noted in the decider section of `fidelity-and-answers.md` and cross-referenced from the lone
|
|
56
|
+
`OTHER:` mention in `SKILL.md` so the two don't drift.
|
|
57
|
+
- **Documented the live real-document validation workflow.** A new SKILL.md section covers driving a skill
|
|
58
|
+
against real input documents (not recording a cassette): explore with `--decider-llm --intent`, script the
|
|
59
|
+
load-bearing and binary-confirm gates, budget ~1 re-run per file (a whiffed gate now salvages a partial
|
|
60
|
+
run), and `inspect` the outputs to judge correctness.
|
|
61
|
+
|
|
62
|
+
## [0.12.0] — 2026-06-24
|
|
63
|
+
|
|
64
|
+
### Added
|
|
65
|
+
|
|
66
|
+
- **`record` now shows the offered options when a scripted answer matches none.** A `choose:` that names no
|
|
67
|
+
offered option previously failed with just "matched no offered option" — you had to dig through
|
|
68
|
+
`events.jsonl` for the real labels. The error now lists the **valid labels** and suggests the **closest
|
|
69
|
+
match**, so you can fix the anchor from the error alone. (The labels were already on the error object; the
|
|
70
|
+
record path just wasn't printing them — `run`/`skill` already did.)
|
|
71
|
+
- **`doctor` detects the git-worktree `.env` trap.** Running from a git worktree where `./.env` is gitignored
|
|
72
|
+
(so absent) yields "no token"; when the **main checkout** has a `.env`, `doctor` now points you at
|
|
73
|
+
`--dotenv <main>/.env` instead of the generic remedy. (Keychain hint still takes precedence on macOS.)
|
|
74
|
+
- **`scaffold` stamps a provenance header on generated scenarios.** Output now carries a
|
|
75
|
+
`# generated by cowork-harness v<ver> (scaffold)` comment (a comment, since the scenario schema is
|
|
76
|
+
`additionalProperties: false`), mirroring the cassette's `generator`/`$schema`/`cassetteVersion` provenance.
|
|
77
|
+
The shipped `example-pdf-skill` replay cassette — previously a hand-authored legacy fixture with no
|
|
78
|
+
signature and a stale embedded scenario — was re-recorded live so it now carries a genuine v6 signature +
|
|
79
|
+
fingerprint/artifacts/userVisibleRoots and matches the current scenario YAML (replays green;
|
|
80
|
+
`verify-cassettes` clean).
|
|
81
|
+
|
|
82
|
+
### Changed
|
|
83
|
+
|
|
84
|
+
- **`verify-run` answer-coverage now refuses against a *stale* kept run.** Every run persists a skill
|
|
85
|
+
fingerprint in `result.json`; on the answer-coverage path (`answers:` declared), `verify-run` recomputes it
|
|
86
|
+
live and, if the skill source changed since the run was kept, **exits 2** ("the kept run predates the current
|
|
87
|
+
skill") instead of vouching for answers against a stale gate snapshot. ⚠️ This closes a false-green: a
|
|
88
|
+
reworded/moved gate after `--keep` would previously green against the old labels. The plain `assert:`-only
|
|
89
|
+
re-eval (no `answers:`) is unaffected; a kept run recorded by an older harness (no fingerprint) → a warning,
|
|
90
|
+
not a refusal. `RunResult.fingerprint` is the new persisted field.
|
|
91
|
+
|
|
92
|
+
### Parity
|
|
93
|
+
|
|
94
|
+
- **Synced the platform baseline to Claude Desktop 1.15200.0** (`baselines/desktop-1.15200.0.json`). The
|
|
95
|
+
staged agent ELF is **unchanged (2.1.181)** — the bump is host-side: `sync` re-derived egress / gates /
|
|
96
|
+
mount / web_fetch facts (no `unknown delta`; only the `asarFingerprint` moved). Re-verified end-to-end —
|
|
97
|
+
the live scenario suite (`protocol` + `container` tiers) passes against the new baseline.
|
|
98
|
+
- **Re-paraphrased the reconstructed system-prompt + subagent appends for 1.15200.0**
|
|
99
|
+
(`baselines/prompts/desktop-1.15200.0/`). Cowork's identity constant was rewritten **first→third person**
|
|
100
|
+
(it no longer says "powering Cowork mode"; the load-bearing "is NOT Claude Code" correction is kept);
|
|
101
|
+
`file_handling` became a scratchpad-vs-workspace split, and `working_with_user_files` / `product_information`
|
|
102
|
+
blocks were added; the subagent-append VM clause changed to "not on the user's real computer." Host-only
|
|
103
|
+
affordances (`request_cowork_directory`, `computer://` links) are described as behavior, **not** injected as
|
|
104
|
+
instructions, so they don't induce dead tool calls on the container / microvm / protocol tiers. The host-loop
|
|
105
|
+
`## Shell access` generator was re-verified **byte-identical** (one new conditional sentence stays omitted —
|
|
106
|
+
our single-container topology never triggers it; comment + verification stamp refreshed).
|
|
107
|
+
|
|
108
|
+
### Fixed
|
|
109
|
+
|
|
110
|
+
- **Baseline-bump-stable staleness tests.** `staleness-roundtrip`, `manifest`, and `agent-scope` round-trip
|
|
111
|
+
tests hardcoded the baseline version (`1.14271.0`), so adding a new latest baseline re-staled them; they now
|
|
112
|
+
source it from `loadBaseline("latest")`, so a parity bump no longer breaks the green round-trips.
|
|
113
|
+
- **`example-pdf-skill` asserts the workspace deliverable correctly.** The scenario connects a folder, so
|
|
114
|
+
`{{workspaceFolder}} = mnt/project` and the model writes the deliverable into the folder
|
|
115
|
+
(`mnt/project/outputs/actions.md`), not `mnt/outputs` — but the assert was `file_exists: outputs/actions.md`
|
|
116
|
+
(anchored at `mnt/`), so it failed. Switched to `user_visible_artifact: project/outputs/actions.md` (spans
|
|
117
|
+
the user-visible roots). A pre-existing scenario bug (fails identically on the old prompt), not a parity
|
|
118
|
+
regression; the `{{workspaceFolder}}` resolution is faithful to the Desktop builder `y8r`. Docs + skill now
|
|
119
|
+
teach `user_visible_artifact` vs `file_exists` for folder-connected deliverables.
|
|
120
|
+
|
|
121
|
+
### Docs
|
|
122
|
+
|
|
123
|
+
- **Baseline-pin sweep to `desktop-1.15200.0`** across README / DESIGN / SPEC / spawn-contract / skill docs
|
|
124
|
+
(agent ELF still 2.1.181). The `≥1.14271.0` mount/bare-name gate-boundary references are intentionally left
|
|
125
|
+
as-is — that boundary did not move.
|
|
126
|
+
- Skill + scenario docs: the gate-centric answer-coverage currency rule; the cheap `--keep` → `trace
|
|
127
|
+
--view questions`/`verify-run` authoring loop (no token-free gate probe exists — gates are model-decided);
|
|
128
|
+
the mismatch-vs-unanswered hard-fail gotcha; "anchor only assert-relevant gates, prefer `on_unanswered:
|
|
129
|
+
first` elsewhere"; the keep-going batch semantics (`record <dir>`/`--rerecord-stale` record all and report
|
|
130
|
+
at the end — no one-at-a-time wrapper needed); and the git-worktree `.env` gotcha.
|
|
131
|
+
|
|
132
|
+
### Security
|
|
133
|
+
|
|
134
|
+
- **Hardened HTML-comment stripping in prompt assets (CodeQL `js/incomplete-multi-character-sanitization`).**
|
|
135
|
+
A one-shot `.replace(/<!--[\s\S]*?-->/g, "")` is incomplete multi-character sanitization: removing an inner
|
|
136
|
+
comment can recombine surrounding fragments into a fresh `<!--` the single pass leaves behind (e.g.
|
|
137
|
+
`<!<!-- -->-- x -->` → `<!-- x -->`). `stripComments` now loops until the string stabilizes and is shared
|
|
138
|
+
from `prompt.ts`; `hostloop.ts` reuses it instead of a duplicated inline regex. Resolves the finding at both
|
|
139
|
+
call sites; a regression test covers the recombination case.
|
|
140
|
+
|
|
141
|
+
## [0.11.0] — 2026-06-24
|
|
142
|
+
|
|
143
|
+
### Added
|
|
144
|
+
|
|
145
|
+
- **`record --concurrency <N>` — parallel fleet re-records.** A directory batch (or `--rerecord-stale`) can
|
|
146
|
+
now record N cassettes at a time (`record cassettes/ --rerecord-stale --concurrency 3`) instead of one ~7–8
|
|
147
|
+
min run after another. Every run is already fully isolated (its own per-run Docker networks + egress proxy,
|
|
148
|
+
its own session dir), so parallelism is safe; `--concurrency` is purely a **bound** against Docker's address
|
|
149
|
+
pool and model API rate limits. Default `1` (unchanged behavior + ordered output), max `8`. A dir batch
|
|
150
|
+
where two scenarios' `name:` slugify to the same cassette path is now rejected up front (they would clobber
|
|
151
|
+
each other — a pre-existing footgun parallelism would have surfaced).
|
|
152
|
+
- **Opt-in per-skill agent scoping for cassette staleness (`COWORK_HARNESS_AGENT_SCOPE=skill`).** By default a
|
|
153
|
+
plugin's `agents/` directory is a fleet-wide staleness root, so editing one skill's sub-agent contract
|
|
154
|
+
(`agents/cap-table.md`) re-stales *every* cassette. With this env set, a **skill-named** `agents/<name>.md`
|
|
155
|
+
is treated as skill `<name>`'s private input (refining a scenario's `skills:` scope), so it re-stales only
|
|
156
|
+
that skill's cassettes; generic (non-skill-named) agents stay shared. The setting is stamped into the
|
|
157
|
+
cassette fingerprint (`agentScope`), so flipping it is an honest one-time "re-record under the same setting"
|
|
158
|
+
(like `COWORK_HARNESS_GITSET`); cassettes recorded without it are unaffected. Caveat: assumes an agent named
|
|
159
|
+
after a skill belongs to that skill — keep it off if you share a skill-named agent across skills.
|
|
160
|
+
|
|
161
|
+
### Fixed
|
|
162
|
+
|
|
163
|
+
- **Clarified `verify-run` answer-coverage docs (gate-centric, not rule-centric).** `verify-run` checks that
|
|
164
|
+
every gate the run *actually fired* is covered by a matching `answer`; it does **not** penalize answer rules
|
|
165
|
+
that no fired gate matched (e.g. rules for conditional gates that didn't fire). The behavior was always
|
|
166
|
+
correct; `docs/scenario.md` now states it precisely (a scenario with 5 rules whose run fired 2 gates passes
|
|
167
|
+
at "2/2 matched").
|
|
168
|
+
|
|
7
169
|
## [0.10.0] — 2026-06-23
|
|
8
170
|
|
|
9
171
|
### Added
|
package/README.md
CHANGED
|
@@ -24,13 +24,18 @@ Scriptable, CI-friendly test harness that reproduces **Claude Cowork's observabl
|
|
|
24
24
|
|
|
25
25
|
> **What this is and isn't.** This is an *emulator of the contract*, not the Desktop runtime. Cowork's real session control plane lives behind the Desktop renderer's IPC (per-build UUID + `senderFrame` origin checks) and the app ships with remote debugging disabled (verified: `--remote-debugging-port` opens no listener; Electron `EnableNodeCliInspectArguments` fuse is OFF). So you **cannot** drive the real Apple Virtualization.framework microVM from a script. What you *can* faithfully reproduce is everything that actually changes how a **skill** behaves: the same agent binary in **cowork mode** (`CLAUDE_CODE_IS_COWORK=1` — there is no `--cowork` flag), the same mount layout, the same egress allowlist, and the same permission/question protocol. That's what this project does.
|
|
26
26
|
|
|
27
|
-
**Zero-friction preview — no token, no Docker.** A committed cassette replays
|
|
27
|
+
**Zero-friction preview — no token, no Docker.** A committed cassette replays from a fresh clone (the example
|
|
28
|
+
cassette ships in the repo; just Node ≥ 20):
|
|
28
29
|
|
|
29
30
|
```bash
|
|
30
|
-
|
|
31
|
-
|
|
31
|
+
git clone https://github.com/yaniv-golan/cowork-harness && cd cowork-harness
|
|
32
|
+
npm ci && npm run build
|
|
33
|
+
node dist/cli.js replay examples/replays/example-pdf-skill.cassette.json
|
|
32
34
|
```
|
|
33
35
|
|
|
36
|
+
(Installing globally — `npm install -g cowork-harness` — gives you the `cowork-harness` CLI for your own
|
|
37
|
+
scenarios and cassettes; the bundled example above is replayed from a checkout.)
|
|
38
|
+
|
|
34
39
|
Full setup → [Quick start](#quick-start).
|
|
35
40
|
|
|
36
41
|
---
|
|
@@ -88,20 +93,21 @@ Skill testing is the headline use, but the tool is a general harness over the Co
|
|
|
88
93
|
| `skill <folder> "<prompt>"` | Run a local skill/plugin folder once against the staged agent | ad-hoc "is the skill alive / does it do X?" — the fast inner loop |
|
|
89
94
|
| `run <scenario.yaml \| dir/>` | Run authored scenarios with `assert:` + a CI-ready exit code | you want a repeatable, **asserted regression test** |
|
|
90
95
|
| `chat <folder> [prompt]` | Interactive multi-turn REPL against a skill (TTY); optional seed prompt is sent as the first turn. `--upload <file>` / `--folder <dir>` (repeatable) attach files/project folders; `--verbose` / `-V` shows thinking blocks + tool inputs; `--fidelity protocol\|container\|hostloop` | debugging a multi-turn flow by hand |
|
|
91
|
-
| `record` / `replay` | `record` saves a control-protocol cassette (one scenario, or batch a `dir/`; `--rerecord-stale` refreshes only drifted ones; `--max-artifact-bytes` raises the 64 KiB inline-body cap; `--dry-run` previews the scenarios + token/binary checks without recording; answer gates **live during the recording** with `--decider-dir`/`--decider-llm`/`--on-unanswered first` instead of pre-scripting them — a live decider flags the cassette non-deterministic but it still replays deterministically); `replay <file.cassette.json \| dir/>` runs a cassette **file or a `dir/` of `*.cassette.json`** deterministically (`--strict` fails on a stale one; exits on the worst verdict) | **token-free, Docker-free CI** from a once-recorded run |
|
|
96
|
+
| `record` / `replay` | `record` saves a control-protocol cassette (one scenario, or batch a `dir/`; `--rerecord-stale` refreshes only drifted ones; `--concurrency <N>` records a batch N at a time — runs are fully isolated, so it just bounds Docker/API pressure; `--max-artifact-bytes` raises the 64 KiB inline-body cap; `--dry-run` previews the scenarios + token/binary checks without recording; answer gates **live during the recording** with `--decider-dir`/`--decider-llm`/`--on-unanswered first` instead of pre-scripting them — a live decider flags the cassette non-deterministic but it still replays deterministically); `replay <file.cassette.json \| dir/>` runs a cassette **file or a `dir/` of `*.cassette.json`** deterministically (`--strict` fails on a stale one; exits on the worst verdict) | **token-free, Docker-free CI** from a once-recorded run |
|
|
92
97
|
| `verify-cassettes <file\|dir>` | Token-free CI gate over committed cassettes: a privacy scan (email/currency/domain → exit 1; whole-token allows via `--allow` / class-scoped `--allow-domain` / `--allow-email` / `--allow-file`) + a staleness check (`--skip-privacy` or `--skip-staleness` to run only one half). A dir argument scans `*.cassette.json` in that dir only (**non-recursive**) | gating **committed cassettes** against PII leaks + "edited the skill, forgot to re-record" |
|
|
93
98
|
| `verify-run <run-dir> <scenario.yaml>` | Re-evaluate a scenario's `assert:` (and, when the scenario declares `answers:`, whether they still match the run's actual gates) against an already-kept run dir — **no live agent, no tokens, no Docker** (~1s) | iterating on a wrong assertion or a drifted `answer` without a full live re-record |
|
|
94
99
|
| `trace <run-id>` | Digest a run's `events.jsonl` (`--view tools\|questions\|dispatches`; legacy `--tools` / `--gates` / `--dispatches` aliases still accepted) | "how many sub-agents *actually* dispatched, and which?" |
|
|
100
|
+
| `inspect <run-id>` | Show what a run **produced**: the artifacts + a shallow field preview of each JSON artifact (`--output-format json` for a digest). Works on a salvaged partial run too | "did it do the job?" — without hand-parsing `…/mnt/outputs/…` |
|
|
95
101
|
| `scaffold <run-id>` | Turn a kept run into a starter scenario YAML (gates→answers, artifacts→`file_exists`); `--from-run <id>` is a deprecated alias for the positional | authoring a scenario from a real run instead of guessing |
|
|
96
102
|
| `python3 …/scenario.py scaffold --name <name> --skill <dir>` | Generate a starter scenario skeleton from scratch (the `…` is `.claude/skills/cowork-harness/scripts/`) | starting a new scenario when you have no prior run |
|
|
97
103
|
| `lint <scenario.yaml \| dir/>…` | Check scenarios for silent false-greens — assertions placed on the wrong CI lane, mixed content/live keys, missing `controlOut`-required keys (files or a directory of `*.yaml`/`*.yml`; bundled `scenario.py`; needs python3 — PyYAML is bundled) | before committing a new scenario or after changing assertions |
|
|
98
|
-
| `assertions --list` | List the available scenario assertions (generated from the schema)
|
|
104
|
+
| `assertions --list` | List the available scenario assertions (generated from the schema) | "what can I assert?" without grepping the source |
|
|
99
105
|
| `decide` | Validate a decider against a sample question in ~2 s (no run) | sanity-check a `--decider-*` / `--answer` wiring before a long run |
|
|
100
106
|
| `gates` / `answer` | Stream / answer in-band gates for `--decider-dir` | a **driving agent** answers live questions via a Monitor |
|
|
101
107
|
| `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
108
|
| `sync` / `list` | Derive/refresh & list platform baselines from the Desktop install | after Claude Desktop updates (baselines ship, so it's optional otherwise) |
|
|
103
109
|
| `doctor [--tier <t>]` | Read-only prerequisite check, per tier (Docker + agent image for `container`/`hostloop`; **Lima** for `microvm`; plus 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
|
-
| `
|
|
110
|
+
| `prune [--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
111
|
| `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 |
|
|
106
112
|
| `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` |
|
|
107
113
|
|
|
@@ -202,7 +208,7 @@ This repo ships a **companion skill** (`.claude/skills/cowork-harness/`) that te
|
|
|
202
208
|
/plugin install cowork-harness@cowork-harness
|
|
203
209
|
```
|
|
204
210
|
|
|
205
|
-
The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.
|
|
211
|
+
The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.13.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.
|
|
206
212
|
|
|
207
213
|
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.)
|
|
208
214
|
|
|
@@ -261,7 +267,7 @@ cowork-harness lint examples/scenarios/*.yaml
|
|
|
261
267
|
> `serializeDecision` as a token-free O7 guard (the AskUserQuestion `{questions,answers}` answer-shape
|
|
262
268
|
> invariant). Evaluated on replay: `transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`,
|
|
263
269
|
> `result`, and the verdict modifiers `allow_permissive_auto_allow` / `allow_missing_capability` /
|
|
264
|
-
`allow_l0_plugin_divergence` / `allow_stall` (no-op passes, kept on replay). **`question_asked`,
|
|
270
|
+
> `allow_l0_plugin_divergence` / `allow_stall` (no-op passes, kept on replay). **`question_asked`,
|
|
265
271
|
> `questions_count_max`, and `gate_answers_delivered` are also evaluated —
|
|
266
272
|
> but only when the cassette carries `controlOut` (full-fidelity)**; old cassettes without it get a
|
|
267
273
|
> loud warning and those three keys are excluded (not vacuously passed). **Filesystem assertions
|
|
@@ -293,7 +299,7 @@ Configuration splits the way Cowork itself splits — *what you set up before th
|
|
|
293
299
|
# scenarios/pdf.yaml ← the filename is the test's identity (name: is an optional override)
|
|
294
300
|
baseline: latest # platform baseline (auto-synced from Desktop)
|
|
295
301
|
session: ../sessions/default.yaml # pre-prompt setup, resolved relative to THIS file
|
|
296
|
-
fidelity: container # protocol | container | microvm
|
|
302
|
+
fidelity: container # protocol | container | microvm | hostloop | cowork
|
|
297
303
|
|
|
298
304
|
prompt: |
|
|
299
305
|
Summarize report.pdf and write the action items to outputs/actions.md
|
|
@@ -468,10 +474,10 @@ Unit tests cover the scripted-answer logic, the egress allowlist matcher, the se
|
|
|
468
474
|
- `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.
|
|
469
475
|
- `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).
|
|
470
476
|
- `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.
|
|
471
|
-
- `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 `
|
|
477
|
+
- `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 `prune` 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.
|
|
472
478
|
- **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`).
|
|
473
479
|
- **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).
|
|
474
|
-
- **Staleness boundary:** the cassette-staleness skill hash uses the **git-tracked** file set of each skill/plugin source dir by default (a non-repo dir falls back to a raw walk; OS-junk like `.DS_Store` is always excluded). `COWORK_HARNESS_GITSET=0` opts out to the legacy raw walk for every dir. `COWORK_HARNESS_DEBUG_SKILLHASH=1` dumps the exact file set feeding the hash on a staleness mismatch (and flags OS-junk) so a drift source is one line. Declare per-plugin non-runtime paths in a `.cowork-hashignore` file (or the session `staleness.hash_ignore`).
|
|
480
|
+
- **Staleness boundary:** the cassette-staleness skill hash uses the **git-tracked** file set of each skill/plugin source dir by default (a non-repo dir falls back to a raw walk; OS-junk like `.DS_Store` is always excluded). `COWORK_HARNESS_GITSET=0` opts out to the legacy raw walk for every dir. `COWORK_HARNESS_DEBUG_SKILLHASH=1` dumps the exact file set feeding the hash on a staleness mismatch (and flags OS-junk) so a drift source is one line. Declare per-plugin non-runtime paths in a `.cowork-hashignore` file (or the session `staleness.hash_ignore`). `COWORK_HARNESS_AGENT_SCOPE=skill` (opt-in) refines a scenario's `skills:` scope so a **skill-named** `agents/<name>.md` re-stales only that skill's cassettes instead of the whole fleet (generic agents stay shared; stamped into the fingerprint, so flipping it is a one-time re-record like `GITSET`).
|
|
475
481
|
- **`skill` / `chat` defaults:** `COWORK_HARNESS_FIDELITY` sets the default fidelity tier for ad-hoc `skill`/`chat` runs (a `--fidelity` flag or a scenario's `fidelity:` still wins); `COWORK_HARNESS_MODEL` sets the default model; `COWORK_HARNESS_OUTPUT_FORMAT` (`text`|`json`) sets the default output format. Each is overridden by the matching explicit flag.
|
|
476
482
|
- **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.
|
|
477
483
|
- L2 microVM: `COWORK_VM_GATEWAY` overrides the Lima host-proxy gateway IP (default `192.168.5.2`); `COWORK_VM_PROXY_PORT` the proxy port. The Lima instance is named `cowork-vm-<config-hash>` (a config change → a fresh VM); `COWORK_LIMA_INSTANCE` pins a fixed name, and `vm prune` removes orphaned ones.
|
|
@@ -549,7 +555,7 @@ This repo is built to be driven by agents, not just read by humans:
|
|
|
549
555
|
|
|
550
556
|
## Status
|
|
551
557
|
|
|
552
|
-
**Verified end-to-end against the live staged agent (2.1.181 / asar 1.
|
|
558
|
+
**Verified end-to-end against the live staged agent (2.1.181 / asar 1.15200.0).** Host-side parity (egress/gates/mount/web_fetch facts + the reconstructed prompt appends) re-derived for 1.15200.0 via `sync` + asar inspection; the live scenario suite passes against this baseline (the staged agent ELF is unchanged at 2.1.181).
|
|
553
559
|
|
|
554
560
|
- ✅ **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**.
|
|
555
561
|
- ✅ **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.
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
{
|
|
2
|
+
"baselineVersion": 1,
|
|
3
|
+
"appVersion": "1.15200.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.15200.0/system-prompt-append.md",
|
|
82
|
+
"subagentAppend": "prompts/desktop-1.15200.0/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, re-paraphrased from asar 1.15200.0 const MZi (system prompt) + generator CVr (subagent) — not the full base prompt (not cleanly extractable), and excluding the generic refusal/safety tail. 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
|
+
"assets.claude.ai",
|
|
135
|
+
"console.anthropic.com",
|
|
136
|
+
"api-staging.anthropic.com",
|
|
137
|
+
"www.anthropic.com",
|
|
138
|
+
"docs.anthropic.com",
|
|
139
|
+
"mcp-proxy.anthropic.com",
|
|
140
|
+
"pivot.claude.ai",
|
|
141
|
+
"support.anthropic.com"
|
|
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-24",
|
|
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": "636f468d72f314f7",
|
|
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
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<!-- Reconstructed (PARAPHRASED, not verbatim) from the subagent system-prompt append in
|
|
2
|
+
Claude.app app.asar 1.15200.0 — generator `CVr({vmProcessName, hostLoopMode, ...})` at
|
|
3
|
+
byte offset ~9,305,235, VM branch (the text below corresponds to hostLoopMode=false).
|
|
4
|
+
Delivered to spawned sub-agents over the `initialize` control_request field
|
|
5
|
+
`appendSubagentSystemPrompt` (NOT --append-system-prompt). In 1.15200.0 this text is the
|
|
6
|
+
HARDCODED FALLBACK behind GrowthBook flag 124685897 / key `subagent_env_vm` — used when the
|
|
7
|
+
flag is off or no server override is supplied (the shipped default). A separate host-loop
|
|
8
|
+
branch (key `subagent_env_hl`, different wording about reaching the user's real filesystem)
|
|
9
|
+
exists in the asar but is NOT modeled here — the harness wires a single subagentAppend.
|
|
10
|
+
|
|
11
|
+
1.12603.1 → 1.15200.0 drift folded in: the middle clause changed from "are not visible to
|
|
12
|
+
the user unless written to the outputs directory" to "not on the user's real computer".
|
|
13
|
+
The real cwd renders as /sessions/<vmProcessName>; {{cwd}} is the harness reconstruction
|
|
14
|
+
token (substituted by src/prompt.ts). Paraphrased per the repo's no-bundling rule. -->
|
|
15
|
+
|
|
16
|
+
## Cowork environment
|
|
17
|
+
|
|
18
|
+
You are running as a subagent inside a Cowork session. Shell commands execute in an isolated Linux sandbox rooted at {{cwd}} — files created there (or under /tmp) exist only in the sandbox, not on the user's real computer. User-attached folders are mounted under {{cwd}}/mnt/.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<!-- Reconstructed (PARAPHRASED, not verbatim) from the real Cowork system prompt in
|
|
2
|
+
Claude.app app.asar 1.15200.0. The cowork body is module constant `MZi` at byte offset
|
|
3
|
+
~6,583,523, emitted locally by the desktop app's 3P bootstrap builder (consumed at
|
|
4
|
+
~6,632,043) as `cowork_system_prompt:{value:{prompt:MZi}, on:true, source:"defaultValue",
|
|
5
|
+
ruleId:null}` — the shipped DEFAULT, layered through a server-overridable envelope (not
|
|
6
|
+
itself a GrowthBook/spVariant lookup, but the server COULD replace it). Cross-checked
|
|
7
|
+
against the 1.13576.1 reconstruction this supersedes.
|
|
8
|
+
|
|
9
|
+
Scope: only the cowork-specific, behavior-driving sections (identity, product info,
|
|
10
|
+
skills, computer use, file handling, user files, uploads). MZi continues into ~28KB of
|
|
11
|
+
GENERIC refusal/safety policy — deliberately EXCLUDED here: it is not Cowork-specific and
|
|
12
|
+
the agent's built-in base prompt already carries safety. Delivered via
|
|
13
|
+
`--append-system-prompt` (layered on the agent's base prompt), NOT the initialize
|
|
14
|
+
handshake. Tokens substituted by src/prompt.ts mirroring the Desktop builder `y8r`. -->
|
|
15
|
+
|
|
16
|
+
<!-- 1.14271.0 → 1.15200.0 drift folded in: the identity constant was rewritten FIRST→THIRD
|
|
17
|
+
person and no longer says "powering Cowork mode" / "made by Anthropic" / "refer to
|
|
18
|
+
yourself as Claude"; it KEEPS the load-bearing "is NOT Claude Code / should not refer to
|
|
19
|
+
itself as such" correction (without it the base claude_code preset makes the agent say
|
|
20
|
+
"I'm Claude Code"). high_level_computer_use_explanation expanded; file_handling_rules
|
|
21
|
+
became a scratchpad-vs-workspace split; a working_with_user_files block was added.
|
|
22
|
+
The real asar's `{{workspaceContext}}` token (Desktop host workspace state the harness
|
|
23
|
+
does not have) is resolved INLINE below to the single-container model rather than wired as
|
|
24
|
+
a renderer token. The asar's `request_cowork_directory` tool and `computer://` link scheme
|
|
25
|
+
are Desktop/host-loop-only affordances — they are described as behavior, NOT emitted as
|
|
26
|
+
instructions, so the model is never told to call a tool / emit a URI that does not resolve
|
|
27
|
+
on the container / microvm / protocol tiers. Paraphrased per the repo's no-bundling rule;
|
|
28
|
+
model name comes from the <env> block, not hardcoded here. -->
|
|
29
|
+
<application_details>
|
|
30
|
+
Claude is operating as an agent inside the Claude desktop app; this agent capability is currently a research preview. Claude is implemented on top of Claude Code and the Claude Agent SDK, but Claude is NOT Claude Code and should not refer to itself as such. Claude runs in a lightweight Linux VM on the user's computer — a secure sandbox for executing code with controlled access to a workspace folder. Claude should not mention implementation details like this (including Claude Code or the Claude Agent SDK) unless it is relevant to the user's request.
|
|
31
|
+
</application_details>
|
|
32
|
+
|
|
33
|
+
<product_information>
|
|
34
|
+
Claude runs inside the Claude desktop app to help automate file and task management. The desktop app supports plugins — installable bundles of MCP servers, skills, and tools. The model powering this session is given in the <env> section at the end of the prompt; public documentation for configuration, policy keys, and troubleshooting is published by Anthropic for the Claude desktop app. When it helps, Claude can offer guidance on effective prompting — being clear and detailed, giving positive and negative examples, encouraging step-by-step reasoning, requesting specific XML tags, and stating the desired length or format — with concrete examples where possible.
|
|
35
|
+
</product_information>
|
|
36
|
+
|
|
37
|
+
<skills>
|
|
38
|
+
Skills are folders of condensed best practices for producing high-quality outputs (for example a docx skill for Word documents, a PDF skill for creating and filling PDFs). They live under {{skillsDir}}/skills/. Before writing any code, creating files, or using computer tools, Claude should first check which available skills are relevant and Read the relevant SKILL.md — the documentation is worth reading up front, and more than one skill may apply to a task.
|
|
39
|
+
</skills>
|
|
40
|
+
|
|
41
|
+
<high_level_computer_use_explanation>
|
|
42
|
+
Claude runs in a lightweight Linux VM (Ubuntu 22) on the user's computer — a secure sandbox for executing code with controlled access to user files. Available tools: Bash (run commands), Edit (edit existing files), Write (create files), and Read (read files — not directories; use `ls` via Bash for those). The working directory {{cwd}} is for all temporary work. The VM's internal file system resets between tasks, but the workspace folder ({{workspaceFolder}}) persists on the user's actual computer, so files saved there remain accessible after the session ends. Claude can create files such as docx, pptx, and xlsx for the user to open from their selected folder.
|
|
43
|
+
</high_level_computer_use_explanation>
|
|
44
|
+
|
|
45
|
+
<file_handling_rules>
|
|
46
|
+
FILE LOCATIONS AND ACCESS:
|
|
47
|
+
1. Claude's work — {{cwd}}: create new files here first; this is a temporary scratchpad and the user cannot see files in it.
|
|
48
|
+
2. Workspace folder — {{workspaceFolder}}: where final outputs and deliverables belong. Files written here are surfaced to the user; files written elsewhere in the sandbox are not — so saving final work here is essential, or the user won't see it. For simple tasks (a single short file) write directly here. If the user mounted a folder from their computer, this IS that folder and Claude can both read from and write to it.
|
|
49
|
+
- Never expose absolute /sessions/ sandbox paths to the user; refer to files by name or workspace-relative location.
|
|
50
|
+
</file_handling_rules>
|
|
51
|
+
|
|
52
|
+
<working_with_user_files>
|
|
53
|
+
The user connects folders to the session; a connected work folder is mounted under {{workspaceFolder}} and is where user-visible deliverables belong. Files written to the workspace folder are surfaced to the user; files written elsewhere in the sandbox (the scratchpad at {{cwd}}, or /tmp) are not. When no folder is connected, work in the scratchpad and tell the user a folder is needed to persist results. When referring to file locations, Claude should say "the folder you selected" if it has access to the user's files, or "my working folder" if it only has a temporary folder — and never expose internal /sessions/ paths, which look like backend infrastructure and cause confusion.
|
|
54
|
+
</working_with_user_files>
|
|
55
|
+
|
|
56
|
+
<notes_on_user_uploaded_files>
|
|
57
|
+
Files the user uploads are placed under {{cwd}}/mnt/uploads and can be accessed programmatically at that path.
|
|
58
|
+
</notes_on_user_uploaded_files>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Bounded-concurrency async map. Runs `fn` over `items` with at most `concurrency` in flight at once,
|
|
2
|
+
* preserving result order (results[i] corresponds to items[i]). A worker that throws rejects the whole
|
|
3
|
+
* pool — callers that want per-item error capture (the batch-record paths do) should make `fn` return a
|
|
4
|
+
* discriminated result and never throw. `concurrency` is clamped to [1, items.length].
|
|
5
|
+
*
|
|
6
|
+
* Note on in-process safety: the record setup (egress sidecar, image build) is `spawnSync` — it blocks the
|
|
7
|
+
* event loop, so the synchronous setup of one item completes before another item's worker can run. Only the
|
|
8
|
+
* long agent run is async and therefore actually overlaps. That's where the wall-clock win comes from, and
|
|
9
|
+
* it's also why a cold-start proxy-image build can't race in-process (the first worker builds it synchronously
|
|
10
|
+
* before yielding). Separate OS processes (`xargs -P`) do NOT get this serialization — see docs/cassette.md. */
|
|
11
|
+
export async function pMapBounded(items, concurrency, fn) {
|
|
12
|
+
const results = new Array(items.length);
|
|
13
|
+
if (items.length === 0)
|
|
14
|
+
return results;
|
|
15
|
+
const limit = Math.max(1, Math.min(concurrency, items.length));
|
|
16
|
+
let next = 0;
|
|
17
|
+
const worker = async () => {
|
|
18
|
+
for (;;) {
|
|
19
|
+
const i = next++;
|
|
20
|
+
if (i >= items.length)
|
|
21
|
+
return;
|
|
22
|
+
results[i] = await fn(items[i], i);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
await Promise.all(Array.from({ length: limit }, () => worker()));
|
|
26
|
+
return results;
|
|
27
|
+
}
|