cowork-harness 0.4.3 → 0.6.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 +2 -1
- package/CHANGELOG.md +136 -0
- package/README.md +34 -24
- package/baselines/prompts/desktop-1.12603.1/system-prompt-append.md +12 -0
- package/baselines/prompts/desktop-1.13576.1/host-loop-append.md +11 -0
- package/dist/agent/session.js +22 -4
- package/dist/assert.js +12 -2
- package/dist/baseline.js +10 -3
- package/dist/cli.js +459 -106
- package/dist/decide/decider.js +48 -7
- package/dist/decide/external-channel.js +2 -2
- package/dist/hostloop/workspace-handler.js +6 -1
- package/dist/prompt.js +12 -4
- package/dist/redact.js +18 -1
- package/dist/run/cassette.js +323 -57
- package/dist/run/chat.js +129 -39
- package/dist/run/doctor.js +210 -0
- package/dist/run/execute.js +70 -10
- package/dist/run/renderer.js +5 -0
- package/dist/run/skill-hash.js +197 -14
- package/dist/run/verdict.js +10 -0
- package/dist/runtime/hostloop.js +9 -4
- package/dist/runtime/microvm.js +11 -1
- package/dist/runtime/protocol.js +11 -3
- package/dist/scan.js +21 -6
- package/dist/secrets.js +3 -0
- package/dist/session.js +22 -1
- package/dist/types.js +11 -0
- package/docs/README.md +3 -2
- package/docs/boundary.md +3 -3
- package/docs/cassette.md +50 -12
- package/docs/chat.md +126 -0
- package/docs/cowork-spawn-contract-1.12603.1.md +7 -1
- package/docs/fidelity-gaps.md +81 -0
- package/docs/scenario.md +56 -4
- package/docs/session.md +21 -2
- package/examples/README.md +72 -0
- package/examples/replays/README.md +38 -0
- package/examples/replays/example-pdf-skill.cassette.json +33 -0
- package/package.json +2 -1
- package/python/README.md +2 -1
- package/schema/scenario.schema.json +11 -0
- package/schema/session.schema.json +16 -0
|
@@ -68,7 +68,7 @@ LIVE_ONLY_KEYS = {
|
|
|
68
68
|
}
|
|
69
69
|
EGRESS_KEYS = {"egress_denied", "egress_allowed"}
|
|
70
70
|
# verdict modifiers — don't verify anything themselves (e.g. suppress a default-fail)
|
|
71
|
-
VERDICT_MODIFIER_KEYS = {"allow_permissive_auto_allow"}
|
|
71
|
+
VERDICT_MODIFIER_KEYS = {"allow_permissive_auto_allow", "allow_l0_plugin_divergence"}
|
|
72
72
|
|
|
73
73
|
# Every key the replay-class logic knows how to handle. `replay_protocol_fidelity` is valid-but-not-authorable
|
|
74
74
|
# (errored separately below). This is also the embedded fallback for ASSERT_KEYS — kept EQUAL to the generated
|
|
@@ -115,6 +115,7 @@ TOP_LEVEL_KEYS = {
|
|
|
115
115
|
"answers",
|
|
116
116
|
"expect_denied",
|
|
117
117
|
"assert",
|
|
118
|
+
"skills", # F-6: opt-in skill-staleness hash scope
|
|
118
119
|
}
|
|
119
120
|
REGEX_KEYS = {
|
|
120
121
|
"transcript_matches",
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,142 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.6.0] — 2026-06-19
|
|
10
|
+
|
|
11
|
+
### Breaking changes
|
|
12
|
+
|
|
13
|
+
- **Exit code 3 for boundary/integrity violations.** Commands that previously exited `2` for a
|
|
14
|
+
boundary constraint (e.g. `skill` hitting the egress sandbox, `run` with a `boundary`-category
|
|
15
|
+
failure) now exit `3`. Exit `2` is narrowed to usage errors, unknown flags, and runtime errors.
|
|
16
|
+
Scripts that check `$? -eq 2` to detect boundary failures must be updated to `$? -eq 3`.
|
|
17
|
+
- **`verify-cassettes --staleness-only` / `--privacy-only` removed.** Replaced by
|
|
18
|
+
`--skip-staleness` (run privacy scan only) and `--skip-privacy` (run staleness scan only). The
|
|
19
|
+
old flags are not aliased — they now exit `2` as unknown flags.
|
|
20
|
+
- **`decide` with no configuration exits `2` instead of `1`.** Previously, calling `decide` with no
|
|
21
|
+
`--decider-*`, `--answer`, or `--answer-policy` would fall through to a `ScriptedDecider([])` and
|
|
22
|
+
exit `1` ("no rule matched"). It now fails early with exit `2` ("no decider configured") and a
|
|
23
|
+
clear message.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`chat` — full flag parity for interactive debugging:**
|
|
28
|
+
- `[prompt]` — optional seed prompt sent as the first turn before the REPL opens.
|
|
29
|
+
- `--upload <file>` (repeatable) — attach a file at `mnt/uploads/<basename>`; live at session start.
|
|
30
|
+
- `--folder <dir>` (repeatable) — connect a project folder at `mnt/.projects/<basename>` as a live bind mount.
|
|
31
|
+
- `--verbose` / `-V` — show thinking blocks, tool inputs, and the sub-agent tree (previously hardcoded off).
|
|
32
|
+
- `--fidelity protocol` — no-Docker fastest tier; accepted alongside `container` and `hostloop`.
|
|
33
|
+
- `--model` in `--raw` mode — previously silently dropped; now passed as `--model <id>` to the docker argv.
|
|
34
|
+
- Idle heartbeat wired in all three fidelity branches (protocol / hostloop / container).
|
|
35
|
+
- Run ID printed at session start (before first turn) so a mid-session crash still tells you where the transcript is.
|
|
36
|
+
- **`assertions` command** — canonical rename of `assert`; `assert` is kept as a deprecated alias
|
|
37
|
+
that prints a migration notice. `assertions --list` is the new canonical form.
|
|
38
|
+
- **`scaffold <run-id>` positional** — canonical form; `--from-run <id>` is kept as a deprecated
|
|
39
|
+
alias that prints a migration notice.
|
|
40
|
+
- **`trace --view tools|questions|dispatches`** — replaces the three separate `--tools` / `--gates`
|
|
41
|
+
/ `--dispatches` flags with a single `--view` enum. Legacy flags are kept as backward-compat
|
|
42
|
+
aliases (`--gates` maps to `--view questions`).
|
|
43
|
+
- **Env-var defaults for all live commands:**
|
|
44
|
+
- `COWORK_HARNESS_FIDELITY` — default fidelity tier for `skill` and `chat` (validated; exits 2 on an invalid value).
|
|
45
|
+
- `COWORK_HARNESS_MODEL` — default model override for `skill` and `chat`.
|
|
46
|
+
- `COWORK_HARNESS_OUTPUT_FORMAT` — default `--output-format` for all commands (`text` or `json`).
|
|
47
|
+
- **`decide` no-decider guard** — calling `decide` with no configuration fails immediately with a
|
|
48
|
+
clear message and exit `2` instead of falling through to a vacuous "no rule matched" exit `1`.
|
|
49
|
+
- **`vm` per-subcommand `--help`** — `vm <sub> --help` prints the subcommand usage and exits `0`.
|
|
50
|
+
- **`--quiet` / `-q` accepted in `decide`** — no-op flag for flag-surface consistency with `skill` / `run`.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **`--rerecord-stale` now prefers on-disk scenario over embedded snapshot (G-1).** When a
|
|
55
|
+
`scenarios/<name>.yaml` exists alongside the cassette dir, `--rerecord-stale` re-records from it
|
|
56
|
+
instead of the embedded copy. Edits to the scenario (e.g. adding `skills:` for staleness scoping)
|
|
57
|
+
now take effect. Falls back to the embedded snapshot when no on-disk file is found, with a clear
|
|
58
|
+
warning.
|
|
59
|
+
- **Staleness message distinguishes format-version bump from real content change (G-2).** After a
|
|
60
|
+
harness upgrade that changes the hash algorithm, `verify-cassettes` now reports
|
|
61
|
+
`recorded under an older hash format (vN → vM)` instead of the misleading
|
|
62
|
+
`local skill/plugin dir contents changed`.
|
|
63
|
+
- **`.cowork-hashignore` leading-slash patterns now correctly anchor to the mount root (G-3).**
|
|
64
|
+
`/tests` previously compiled to a regex that never matched (no leading slash in relative paths);
|
|
65
|
+
it now matches only the top-level `tests/` dir, as expected.
|
|
66
|
+
- **Scoped staleness findings now name the changed bucket (G-4).** When a cassette was recorded with
|
|
67
|
+
`skills: [<name>]` scoping, `verify-cassettes` now reports `skills/<name> changed` or
|
|
68
|
+
`shared root changed` rather than the generic `local skill/plugin dir contents changed`.
|
|
69
|
+
- **Unknown flags in `chat` now exit `2`.** Previously ignored silently; any unrecognised flag now
|
|
70
|
+
exits `2` with a clear message.
|
|
71
|
+
- **`chat --model` bounds-checked.** If `--model` is the last argument (no value following), the
|
|
72
|
+
command now exits `2` with a clear message instead of silently using `undefined`.
|
|
73
|
+
- **`cmdSync` platform guard fires before argument parsing.** On non-macOS platforms the guard now
|
|
74
|
+
exits before `parseArgs`, so `sync --help` on Linux no longer crashes on a missing flag.
|
|
75
|
+
- **`record` conflict check.** Passing both a positional and `--cassette` now exits `2` immediately
|
|
76
|
+
instead of silently preferring one.
|
|
77
|
+
- **L0 plugin divergence signal.** `spawnProtocol` now reports whether the skill resolved at L0
|
|
78
|
+
(no container) vs. L1, so callers can surface the divergence accurately.
|
|
79
|
+
- **System-prompt threading.** `systemPromptAppend` is now passed through all runtime paths that
|
|
80
|
+
previously dropped it.
|
|
81
|
+
- **Strict agent binary resolution.** When the exact staged binary path is missing, the harness now
|
|
82
|
+
fails with a clear message pointing to `COWORK_HARNESS_ALLOW_AGENT_FALLBACK=1` instead of
|
|
83
|
+
silently falling back to an arbitrary sibling version.
|
|
84
|
+
- **Path traversal guard on plugin sources.** Plugin source paths are now validated to be
|
|
85
|
+
directories (not files or traversal strings) before mounting.
|
|
86
|
+
- **Cassette correctness (batch):** privacy/staleness coverage tracking, redaction verdict
|
|
87
|
+
preservation across re-records, base64-encoded artifact body scanning, and exhaustiveness
|
|
88
|
+
checks on the replay assertion set.
|
|
89
|
+
|
|
90
|
+
## [0.5.0] — 2026-06-18
|
|
91
|
+
|
|
92
|
+
### Added
|
|
93
|
+
|
|
94
|
+
- **`verify-run <run-dir> <scenario.yaml>`** — re-evaluate a scenario's `assert:` block against an
|
|
95
|
+
already-kept run dir with **no live agent** (no tokens, no Docker). Fixing a wrong assertion was a full live
|
|
96
|
+
re-record (~17 min); this turns it into ~1s. Reconstructs the assert context from the run's `result.json` +
|
|
97
|
+
the `run.jsonl`/`trace.json` sidecars and routes the verdict through the same path as a real record. Refuses
|
|
98
|
+
(rather than false-passing) when a filesystem assertion needs a work dir that's already torn down.
|
|
99
|
+
- **`record --max-artifact-bytes <n>` / `COWORK_HARNESS_MAX_ARTIFACT_BYTES`** — override the 64 KiB
|
|
100
|
+
inline-body cap so a large structured deliverable can be inlined instead of stored hash-only. Paired with a
|
|
101
|
+
**record-time guard**: if an `artifact_json` asserts an artifact that had to be truncated, record now fails
|
|
102
|
+
(or warns under `--allow-failing`) at the cause, instead of producing a green record that goes red at replay
|
|
103
|
+
(no committed body to parse).
|
|
104
|
+
- **`verify-cassettes --allow-domain` / `--allow-email` / `--allow-file`** — class-scoped privacy allows, plus
|
|
105
|
+
a version-controlled allow file (one regex per line, `#` comments).
|
|
106
|
+
- **Scoped cassette-staleness fingerprint** — scenario **`skills: [<name>]`** narrows the staleness hash to the
|
|
107
|
+
named skills' dirs plus the plugin's shared roots (fail-closed to whole-tree on an unknown name); session
|
|
108
|
+
**`staleness.hash_ignore`** globs and a plugin-local **`.cowork-hashignore`** file (composed) drop
|
|
109
|
+
non-runtime paths (`tests/`, `docs/`). Default behavior is unchanged (whole-tree, byte-identical). Cassette
|
|
110
|
+
format bumped to **v2** (an older reader warns rather than mis-flagging a scoped cassette as stale).
|
|
111
|
+
- **Per-subcommand `--help`** — subcommands now print a usage line and exit 0 instead of answering `--help`
|
|
112
|
+
with `unknown flag` (exit 2).
|
|
113
|
+
- **Cowork identity in the system-prompt append** — the emulated agent now self-identifies as "Claude, the
|
|
114
|
+
Cowork assistant" and is told it is **not** Claude Code (verified against the installed Claude Desktop app;
|
|
115
|
+
reconstructed, not bundled).
|
|
116
|
+
|
|
117
|
+
### Fixed
|
|
118
|
+
|
|
119
|
+
- **Privacy allows are whole-token + class-scoped.** A bare `--allow <regex>` previously substring-matched, so a
|
|
120
|
+
domain allow (`example\.com`) silently cleared an email finding (`alice@example.com`) whose domain it matched.
|
|
121
|
+
Allows are now anchored to the whole finding token, and `--allow-domain`/`--allow-email` can't bleed across
|
|
122
|
+
classes — the email tripwire stays live.
|
|
123
|
+
- **Staleness hash no longer over-fires.** A pure `plugin.json` `version` bump (and, with the new scoping knobs,
|
|
124
|
+
unrelated skills/tests/docs) no longer re-stales every cassette in a multi-skill plugin. *Upgrade note:*
|
|
125
|
+
because the hash now ignores the `plugin.json` `version` field, cassettes recorded before this release
|
|
126
|
+
recompute to a new digest and are flagged **stale once** after upgrading — re-record them
|
|
127
|
+
(`record --rerecord-stale`). The cassette format is also bumped to **v2**.
|
|
128
|
+
- **`chat` is pipe/script-safe.** A piped/non-interactive stdin reaching EOF mid-turn crashed the REPL with
|
|
129
|
+
`ERR_USE_AFTER_CLOSE`; it now exits cleanly.
|
|
130
|
+
- **Outputs-delete findings show the `rm` itself.** A long `VAR=…` assignment prefix used to push the operative
|
|
131
|
+
delete past the truncation; the finding now isolates and variable-resolves the delete target.
|
|
132
|
+
- **Clearer record/run messaging.** The record freeze-refusal separates the run *result* from the *verdict* and
|
|
133
|
+
names the failing signal; the run log states the unscripted-question *policy* instead of reading as a failure
|
|
134
|
+
on clean runs.
|
|
135
|
+
- **`sync` warns when a synced baseline lacks its host-loop prompt asset** — previously host-loop records
|
|
136
|
+
silently ran with an empty shell-access section.
|
|
137
|
+
|
|
138
|
+
### Internal
|
|
139
|
+
|
|
140
|
+
- Corrected the system-prompt fidelity note in `docs/boundary.md` (Cowork appends onto the `claude_code` preset
|
|
141
|
+
by default rather than replacing it).
|
|
142
|
+
- Assertion docs steer content checks to `artifact_json` / stable lexical markers (not paraphrasable prose).
|
|
143
|
+
- `vitest` excludes `runs/` from test discovery (ephemeral live output could crash the walk with EACCES).
|
|
144
|
+
|
|
9
145
|
## [0.4.3] — 2026-06-18
|
|
10
146
|
|
|
11
147
|
### Fixed
|
package/README.md
CHANGED
|
@@ -15,6 +15,11 @@ Scriptable, CI-friendly test harness that reproduces **Claude Cowork's observabl
|
|
|
15
15
|
|
|
16
16
|
**Contents:** [Why it works](#why-this-works-for-skill-testing) · [Fidelity tiers](#fidelity-tiers-pick-per-scenario--per-ci-job) · [Quick start](#quick-start) · [Session + scenario](#two-files-session--scenario) · [Boundary](#sandboxing-container-vs-the-real-vm) · [Discovery](#discovery-marketplaces-plugins-skills-mcp) · [Testing & CI/CD](#testing--cicd) · [Maintenance](#maintenance-parity-between-releases) · [Docs](#documentation)
|
|
17
17
|
|
|
18
|
+
> **Requirements at a glance**
|
|
19
|
+
> - **Free demo (`replay`):** Node ≥ 20 — nothing else (no Docker, token, or Claude Desktop).
|
|
20
|
+
> - **Live tiers** (`container` default / `microvm` / `hostloop`): **Docker (arm64)** · **Claude Desktop opened once** (stages the agent — nothing is bundled) · a **Claude token** (real per-run cost; runs take minutes). The `protocol` tier skips Docker + the staged agent but still calls a real model, so it needs the token.
|
|
21
|
+
> - **Platform:** best on **macOS Apple Silicon**; **Windows is not supported** for the live tiers (use the token-free `replay`); `sync` and `microvm` are **macOS-arm64 only**. Full detail in [Quick start → prerequisites](#quick-start).
|
|
22
|
+
|
|
18
23
|
> **New here?** Read [docs/boundary.md](./docs/boundary.md) (the limitations model) and [docs/session.md](./docs/session.md) (the file you'll author).
|
|
19
24
|
|
|
20
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.
|
|
@@ -73,16 +78,20 @@ Skill testing is the headline use, but the tool is a general harness over the Co
|
|
|
73
78
|
|---|---|---|
|
|
74
79
|
| `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 |
|
|
75
80
|
| `run <scenario.yaml \| dir/>` | Run authored scenarios with `assert:` + a CI-ready exit code | you want a repeatable, **asserted regression test** |
|
|
76
|
-
| `chat <folder
|
|
77
|
-
| `record` / `replay` | `record` saves a control-protocol cassette (one scenario, or batch a `dir/`; `--rerecord-stale` refreshes only drifted ones); `replay` runs a cassette **file or a `dir/` of `*.cassette.json`** deterministically (`<file\|dir>`, `--cassette <file>` for the explicit single form; `--strict` fails on a stale one; exits on the worst verdict) | **token-free, Docker-free CI** from a once-recorded run |
|
|
78
|
-
| `verify-cassettes <file\|dir>` | Token-free CI gate over committed cassettes: a privacy scan (email/currency/domain → exit 1) + a staleness check (`--staleness
|
|
79
|
-
| `
|
|
80
|
-
| `
|
|
81
|
-
| `
|
|
81
|
+
| `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 |
|
|
82
|
+
| `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); `replay` runs a cassette **file or a `dir/` of `*.cassette.json`** deterministically (`<file\|dir>`, `--cassette <file>` for the explicit single form; `--strict` fails on a stale one; exits on the worst verdict) | **token-free, Docker-free CI** from a once-recorded run |
|
|
83
|
+
| `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" |
|
|
84
|
+
| `verify-run <run-dir> <scenario.yaml>` | Re-evaluate a scenario's `assert:` against an already-kept run dir — **no live agent, no tokens, no Docker** (~1s) | iterating on a wrong assertion without a full live re-record |
|
|
85
|
+
| `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?" |
|
|
86
|
+
| `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 |
|
|
87
|
+
| `python3 …/scenario.py scaffold --name <name> --skill <dir>` | Generate a starter scenario skeleton from scratch (the `…` is `.claude/skills/cowork-harness/scripts/`; `cowork-harness lint --help` shows the resolved path) | starting a new scenario when you have no prior run |
|
|
88
|
+
| `lint <scenario.yaml>…` | Check scenarios for silent false-greens — assertions placed on the wrong CI lane, mixed content/live keys, missing `controlOut`-required keys (bundled `scenario.py`; needs python3 + PyYAML) | before committing a new scenario or after changing assertions |
|
|
89
|
+
| `assertions --list` | List the available scenario assertions (generated from the schema); `assert` is a deprecated alias | "what can I assert?" without grepping the source |
|
|
82
90
|
| `decide` | Validate a decider against a sample question in ~2 s (no run) | sanity-check a `--decider-*` / `--answer` wiring before a long run |
|
|
83
91
|
| `gates` / `answer` | Stream / answer in-band gates for `--decider-dir` | a **driving agent** answers live questions via a Monitor |
|
|
84
|
-
| `boundary-check [baseline]` | Prove the sandbox enforces Cowork's limitations | verifying the harness's own fidelity |
|
|
92
|
+
| `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 |
|
|
85
93
|
| `sync` / `list` | Derive/refresh & list platform baselines from the Desktop install | after Claude Desktop updates (baselines ship, so it's optional otherwise) |
|
|
94
|
+
| `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 |
|
|
86
95
|
| `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` |
|
|
87
96
|
|
|
88
97
|
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).
|
|
@@ -111,7 +120,7 @@ cowork-harness skill ~/my-plugin 'review this deck' --upload deck.pdf # att
|
|
|
111
120
|
cowork-harness skill ~/my-plugin "..." --session-id s1 # pin a session…
|
|
112
121
|
cowork-harness skill ~/my-plugin '<next turn>' --session-id s1 --resume # …then resume it (gated/checkpoint skills)
|
|
113
122
|
cowork-harness skill ~/my-plugin "..." --keep # then: trace the run
|
|
114
|
-
cowork-harness trace <run-id> --tools
|
|
123
|
+
cowork-harness trace <run-id> --view tools # tool calls + sub-agent dispatches from events.jsonl
|
|
115
124
|
cowork-harness skill --help # full per-command flag reference
|
|
116
125
|
|
|
117
126
|
cowork-harness chat ~/my-plugin # interactive multi-turn REPL (full harness: egress sandbox + control protocol)
|
|
@@ -124,8 +133,8 @@ cowork-harness chat ~/my-plugin # interactive multi-turn REPL (
|
|
|
124
133
|
`run`/CI), `prompt` (ask at the TTY — the default for `skill` when interactive), or `first` (pick
|
|
125
134
|
option 1, loudly warn). Pick with `--on-unanswered`; left unset, `skill` is **adaptive** (`prompt` on
|
|
126
135
|
a TTY, `fail` when piped/CI) and `run` is always `fail`. Exit codes: `0`
|
|
127
|
-
pass · `1` assertion/agent failure · `2` usage /
|
|
128
|
-
|
|
136
|
+
pass · `1` assertion/agent failure · `2` usage / unanswered-under-`fail` / runtime · `3` boundary/integrity.
|
|
137
|
+
After a run, the footer **echoes every auto-answered
|
|
129
138
|
question as a copy-pasteable `--answer "<q>=<choice>"` line** — run once exploratorily, then paste them
|
|
130
139
|
back to lock in a deterministic re-run.
|
|
131
140
|
|
|
@@ -182,13 +191,15 @@ This repo ships a **companion skill** (`.claude/skills/cowork-harness/`) that te
|
|
|
182
191
|
/plugin install cowork-harness@cowork-harness
|
|
183
192
|
```
|
|
184
193
|
|
|
185
|
-
The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.
|
|
194
|
+
The skill **self-bootstraps the CLI**: if `cowork-harness` isn't on your PATH it falls back to `npx cowork-harness@>=0.5.0` (a version floor that fails loud rather than silently fetching a too-old CLI; Node ≥ 20). Tiers above `protocol` still need Docker/Lima and a Claude Desktop agent binary — see the prerequisites below.
|
|
186
195
|
|
|
187
196
|
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.)
|
|
188
197
|
|
|
198
|
+
> Note: `npm install -g cowork-harness` ships `scenario.py` and assertion keys (enough for `lint` in CI) but not the full SKILL.md teaching document; install via the marketplace command above to get the teaching skill.
|
|
199
|
+
|
|
189
200
|
**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):
|
|
190
201
|
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…".
|
|
191
|
-
2. **Docker (arm64)** + the agent image: `docker build --platform linux/arm64 -t cowork-agent-base:1 -f docker/Dockerfile.agent .` (override the tag with `COWORK_AGENT_IMAGE`). The `-f docker/Dockerfile.agent .` paths are **repo-relative** — run it from a source checkout, not a global `npm install -g` (where the Dockerfile lives under the package dir).
|
|
202
|
+
2. **Docker (arm64)** + the agent image: `docker build --platform linux/arm64 -t cowork-agent-base:1 -f docker/Dockerfile.agent .` (override the tag with `COWORK_AGENT_IMAGE`). The `-f docker/Dockerfile.agent .` paths are **repo-relative** — run it from a source checkout, not a global `npm install -g` (where the Dockerfile lives under the package dir). (Global install? Run `cowork-harness doctor --tier container` — it prints the exact `docker build` command with the correct package-local Dockerfile path.)
|
|
192
203
|
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.)
|
|
193
204
|
|
|
194
205
|
> `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.)
|
|
@@ -209,14 +220,12 @@ cowork-harness run examples/scenarios/ --output-format json
|
|
|
209
220
|
|
|
210
221
|
# 4. Record a cassette once, then replay it deterministically (no token, no Docker)
|
|
211
222
|
# (without --out, the cassette is named after the scenario — its `name:`, or the filename)
|
|
212
|
-
|
|
213
|
-
cowork-harness
|
|
214
|
-
|
|
215
|
-
# A committed synthetic fixture is ready to replay on a fresh clone (no record step needed):
|
|
223
|
+
# Commit cassettes under examples/replays/ (this repo) or cassettes/ (conventional skill-repo name).
|
|
224
|
+
cowork-harness record examples/scenarios/example-pdf-skill.yaml --out examples/replays/example-pdf-skill.cassette.json
|
|
216
225
|
cowork-harness replay --cassette examples/replays/example-pdf-skill.cassette.json
|
|
217
226
|
|
|
218
227
|
# Cassettes are COMMITTED fixtures — record against synthetic data, and gate them in CI:
|
|
219
|
-
cowork-harness verify-cassettes
|
|
228
|
+
cowork-harness verify-cassettes examples/replays/ # privacy scan (email/currency/domain) + staleness; exit 1 on a finding
|
|
220
229
|
```
|
|
221
230
|
|
|
222
231
|
> **Privacy:** a cassette snapshots the transcript and the `outputs/` JSON bodies, so it's committed PII
|
|
@@ -230,7 +239,8 @@ cowork-harness verify-cassettes cassettes/ # privacy scan (email/currency/doma
|
|
|
230
239
|
> orchestration from both, re-evaluates the **content** assertions, and re-exercises
|
|
231
240
|
> `serializeDecision` as a token-free O7 guard (the AskUserQuestion `{questions,answers}` answer-shape
|
|
232
241
|
> invariant). Evaluated on replay: `transcript_*`, `tool_*`, `subagent_*`, `dispatch_count_max`,
|
|
233
|
-
> `result
|
|
242
|
+
> `result`, `allow_permissive_auto_allow` (always-on Cowork-parity scan). **`question_asked`,
|
|
243
|
+
> `questions_count_max`, and `gate_answers_delivered` are also evaluated —
|
|
234
244
|
> but only when the cassette carries `controlOut` (full-fidelity)**; old cassettes without it get a
|
|
235
245
|
> loud warning and those three keys are excluded (not vacuously passed). **Filesystem assertions
|
|
236
246
|
> (`file_exists`, `user_visible_artifact`, `artifact_json`) are evaluated when the cassette carries an
|
|
@@ -287,7 +297,7 @@ assert:
|
|
|
287
297
|
```
|
|
288
298
|
|
|
289
299
|
```yaml
|
|
290
|
-
# sessions/default.yaml (abridged — see the file for every field)
|
|
300
|
+
# examples/sessions/default.yaml (abridged — see the file for every field)
|
|
291
301
|
# Relative paths below resolve from THIS file's dir (absolute and ~ are used as-is).
|
|
292
302
|
model: claude-opus-4-8
|
|
293
303
|
effort: high
|
|
@@ -403,11 +413,11 @@ Author scenarios in your own `scenarios/` dir, run the lot, get a non-zero exit
|
|
|
403
413
|
cowork-harness run scenarios/ # your repo's scenarios; runs every *.yaml, CI-ready exit code
|
|
404
414
|
```
|
|
405
415
|
|
|
406
|
-
The provided [GitHub Actions workflow](.github/workflows/ci.yml) runs a **four-stage pipeline** you can copy into your skill repo:
|
|
416
|
+
The provided [GitHub Actions workflow](.github/workflows/ci.yml) runs a **four-stage pipeline**. The **unit** stage is the token-free gate you can copy into your skill repo; the `boundary`, `scenarios`, and `parity-drift` stages are this repo's own fidelity self-tests and are not directly portable (they build the harness's Docker image and run harness-specific e2e scenarios — see [`ci-recipe.md`](./.claude/skills/cowork-harness/references/ci-recipe.md) for the skill-repo template):
|
|
407
417
|
|
|
408
418
|
| Stage | Runs | Needs | Gates |
|
|
409
419
|
|---|---|---|---|
|
|
410
|
-
| **unit** | format check · typecheck · unit tests · build · CLI smoke · token-free `replay`
|
|
420
|
+
| **unit** | format check · typecheck · unit tests · build · CLI smoke · token-free `replay` · `verify-cassettes` · `lint` | nothing | every push/PR |
|
|
411
421
|
| **boundary** | builds the pinned agent image, brings up the default-deny network, runs `boundary-check` | Docker, arm64 runner | proves the sandbox enforces Cowork's limits — **no API key** |
|
|
412
422
|
| **scenarios** | the live scenario suite at `container` fidelity, uploads transcripts/egress logs as artifacts | `ANTHROPIC_API_KEY` (or `CLAUDE_CODE_OAUTH_TOKEN`) | fork PRs: the whole job is skipped (`if:` guard); same-repo without a key: warns and exits 0 |
|
|
413
423
|
| **parity-drift** | reminder to re-`sync` when Desktop updates | nothing | informational, never blocks |
|
|
@@ -433,7 +443,7 @@ Unit tests cover the scripted-answer logic, the egress allowlist matcher, the se
|
|
|
433
443
|
- `COWORK_AGENT_IMAGE=<tag>` — override the agent image name (default `cowork-agent-base:1`); `COWORK_AGENT_BINARY=<path>` — override the auto-detected staged agent ELF.
|
|
434
444
|
- `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.
|
|
435
445
|
- `COWORK_HARNESS_RUNS_DIR` — relocate the `runs/` output root (so `trace` resolves runs from any directory).
|
|
436
|
-
- **Networking / loop:** `COWORK_EGRESS_PROXY` overrides the egress-proxy URL injected into the sandbox; `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`).
|
|
446
|
+
- **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`).
|
|
437
447
|
- **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).
|
|
438
448
|
- **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.
|
|
439
449
|
- 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.
|
|
@@ -508,12 +518,12 @@ This repo is built to be driven by agents, not just read by humans:
|
|
|
508
518
|
|
|
509
519
|
## Status
|
|
510
520
|
|
|
511
|
-
**Verified end-to-end against the live staged agent (2.1.
|
|
521
|
+
**Verified end-to-end against the live staged agent (2.1.177 / asar 1.13576.1).**
|
|
512
522
|
|
|
513
523
|
- ✅ **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**.
|
|
514
524
|
- ✅ **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.
|
|
515
525
|
- ✅ **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`.
|
|
516
|
-
- ✅ **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> --tools` digests `events.jsonl` (each tool row now shows its **result status** `ok`/`error`), and `trace <id> --
|
|
526
|
+
- ✅ **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.
|
|
517
527
|
- ✅ **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`).
|
|
518
528
|
- ✅ **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.
|
|
519
529
|
- ✅ The agent binary is **bind-mounted from your own install** at run time — nothing Anthropic-owned is in any image or distributed.
|
|
@@ -6,6 +6,18 @@
|
|
|
6
6
|
`initialize` (appendSubagentSystemPrompt). Tokens substituted by src/prompt.ts
|
|
7
7
|
mirroring the Desktop builder `y8r`. -->
|
|
8
8
|
|
|
9
|
+
<!-- <application_details> reconstructed from the real Cowork prompt. Binary-confirmed in
|
|
10
|
+
Claude.app app.asar 1.13576.1 at byte offset ~6,971,088 as a STATIC module constant —
|
|
11
|
+
the default cowork prompt body, NOT gated behind a server-pushed spVariant — and
|
|
12
|
+
cross-checked against behavioral capture (2026-06-18). Real Cowork = claude_code preset
|
|
13
|
+
+ this append; the base preset alone makes the agent say "I'm Claude Code", so the
|
|
14
|
+
identity correction must live in the append (layered after the preset via
|
|
15
|
+
--append-system-prompt). Paraphrased, NOT copied verbatim, per the repo's no-bundling
|
|
16
|
+
rule; model name comes from the <env> block, not hardcoded here. -->
|
|
17
|
+
<application_details>
|
|
18
|
+
You are Claude, an AI assistant made by Anthropic. You are powering Cowork mode, a feature of the Claude desktop app that is currently a research preview. You are implemented on top of Claude Code and the Claude Agent SDK, but you are not Claude Code and should not refer to yourself as such — refer to yourself as Claude. Do not mention implementation details like this (including Claude Code or the Claude Agent SDK) unless it is relevant to the user's request.
|
|
19
|
+
</application_details>
|
|
20
|
+
|
|
9
21
|
<high_level_computer_use_explanation>
|
|
10
22
|
Claude runs in a lightweight Linux VM (Ubuntu 22) on the user's computer. This VM provides a secure sandbox for executing code while allowing controlled access to user files. The working directory is {{cwd}}; user-attached folders are mounted under {{cwd}}/mnt/.
|
|
11
23
|
</high_level_computer_use_explanation>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<!-- host-loop "Shell access" section. Carried forward from desktop-1.12603.1 (the shell-access
|
|
2
|
+
semantics — independent bash calls, {{vmMnt}} path translation, unmounted ${CLAUDE_PLUGIN_ROOT}
|
|
3
|
+
self-heal — are version-stable across this Desktop range). Re-verify against the desktop-1.13576.1
|
|
4
|
+
asar (D8r host branch) when an asar copy is available. Appended to the system prompt only in
|
|
5
|
+
host-loop. {{vmMnt}} substituted. -->
|
|
6
|
+
|
|
7
|
+
## Shell access
|
|
8
|
+
|
|
9
|
+
Shell commands use `mcp__workspace__bash` and run in an isolated Linux environment. Each call is independent — no cwd or env carryover between calls. Use absolute paths.
|
|
10
|
+
|
|
11
|
+
Paths in bash differ from what file tools (Read/Write/Edit) see. Your connected folders are mounted in bash under {{vmMnt}}/. A file you Read at a host path is reached in bash under {{vmMnt}}/ — translate host paths to their {{vmMnt}} equivalent before using them in bash. In particular, `${CLAUDE_PLUGIN_ROOT}` is a host path; to run a plugin's scripts in bash, locate them under {{vmMnt}} (e.g. `find {{vmMnt}} -path '*/skills/*/scripts'`) rather than using the host path directly.
|
package/dist/agent/session.js
CHANGED
|
@@ -361,8 +361,19 @@ export class LiveAgentSession {
|
|
|
361
361
|
// (If large control frames ever become legitimate, switch to a drain-aware queue, making writes async.)
|
|
362
362
|
if (line.length > 256 * 1024)
|
|
363
363
|
throw new Error(`control frame is ${line.length} bytes (> 256 KiB safe limit) — refusing to write to avoid partial stdin buffering; this indicates an unexpectedly large control payload`);
|
|
364
|
+
// Bug 34: check stream writability before writing — a closed/destroyed stdin after a child crash
|
|
365
|
+
// loses decision frames silently. Throw immediately so callers surface the failure rather than
|
|
366
|
+
// hanging or silently dropping frames.
|
|
367
|
+
if (this.proc.stdin.destroyed || !this.proc.stdin.writable)
|
|
368
|
+
throw new Error("control-protocol write failed: child stdin is no longer writable (process may have crashed)");
|
|
364
369
|
this.controlOut.write(line + "\n");
|
|
365
|
-
|
|
370
|
+
// Attach a one-shot error handler on the write so stream errors surface via rejectError rather
|
|
371
|
+
// than as unhandled 'error' events (the permanent 'error' handler on constructor covers stdin
|
|
372
|
+
// errors that fire outside a write, e.g. EPIPE after a crash).
|
|
373
|
+
this.proc.stdin.write(line + "\n", (err) => {
|
|
374
|
+
if (err && this.rejectError)
|
|
375
|
+
this.rejectError(err instanceof Error ? err : new Error(String(err)));
|
|
376
|
+
});
|
|
366
377
|
}
|
|
367
378
|
}
|
|
368
379
|
/** Pure translation of one parsed stream-json message → AgentEvents (no side-effects). Shared by
|
|
@@ -447,15 +458,22 @@ export function parseMessage(msg) {
|
|
|
447
458
|
}
|
|
448
459
|
return ev;
|
|
449
460
|
}
|
|
450
|
-
/** Flatten a tool_result `content` (a string, or an array of
|
|
461
|
+
/** Flatten a tool_result `content` (a string, or an array of content blocks), capped at
|
|
451
462
|
* `max` chars. The 500-char DISPLAY value (toolResultText) keeps the recorder/trace compact; the larger
|
|
452
|
-
* PROVENANCE value (toolResultRaw) is what seeds web_fetch provenance, so a URL past char 500 isn't lost.
|
|
463
|
+
* PROVENANCE value (toolResultRaw) is what seeds web_fetch provenance, so a URL past char 500 isn't lost.
|
|
464
|
+
* Bug 35: preserve all content block types — text blocks use their text value; all other block types
|
|
465
|
+
* (json, resource, link, unknown shapes) are JSON-stringified so no content is silently dropped. */
|
|
453
466
|
function flattenToolResult(content, max) {
|
|
454
467
|
if (typeof content === "string")
|
|
455
468
|
return content.slice(0, max);
|
|
456
469
|
if (Array.isArray(content))
|
|
457
470
|
return content
|
|
458
|
-
.map((b) =>
|
|
471
|
+
.map((b) => {
|
|
472
|
+
if (b && typeof b === "object" && "text" in b)
|
|
473
|
+
return String(b.text);
|
|
474
|
+
// Non-text blocks (json, resource, link, unknown): JSON-stringify to preserve all content.
|
|
475
|
+
return JSON.stringify(b);
|
|
476
|
+
})
|
|
459
477
|
.join(" ")
|
|
460
478
|
.slice(0, max);
|
|
461
479
|
return "";
|
package/dist/assert.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { resolve, relative, isAbsolute, sep } from "node:path";
|
|
3
3
|
import { compileUserRegex } from "./regex.js";
|
|
4
4
|
/** Resolve a user-authored assertion path under `workRoot`, rejecting absolute paths and any `..` that
|
|
@@ -209,6 +209,9 @@ function check(a, ctx) {
|
|
|
209
209
|
// evidence the assertion requires, so it fails loud ("no silent false-greens"). `delivered:
|
|
210
210
|
// false` is a real errored tool_result; `null` is "no tool_result observed for this gate".
|
|
211
211
|
if (a.gate_answers_delivered) {
|
|
212
|
+
if (ctx.gateDeliveries.length === 0) {
|
|
213
|
+
results.push(fail("gate_answers_delivered: no gates were recorded during this run — either no gate fired, or gate delivery tracking is not wired. Expected at least one delivered gate."));
|
|
214
|
+
}
|
|
212
215
|
const bad = ctx.gateDeliveries.filter((g) => g.delivered !== true);
|
|
213
216
|
results.push(bad.length === 0
|
|
214
217
|
? ok()
|
|
@@ -236,8 +239,15 @@ function check(a, ctx) {
|
|
|
236
239
|
else {
|
|
237
240
|
let doc;
|
|
238
241
|
let parsed = true;
|
|
242
|
+
const fileSizeLimit = 10 * 1024 * 1024;
|
|
243
|
+
const fileSize = statSync(file).size;
|
|
244
|
+
if (fileSize > fileSizeLimit) {
|
|
245
|
+
results.push(fail(`artifact_json: file too large to parse as JSON (${fileSize} bytes, limit 10 MiB)`));
|
|
246
|
+
parsed = false;
|
|
247
|
+
}
|
|
239
248
|
try {
|
|
240
|
-
|
|
249
|
+
if (parsed)
|
|
250
|
+
doc = JSON.parse(readFileSync(file, "utf8"));
|
|
241
251
|
}
|
|
242
252
|
catch (e) {
|
|
243
253
|
parsed = false;
|
package/dist/baseline.js
CHANGED
|
@@ -15,13 +15,20 @@ export function resolveAgentBinary(baseline) {
|
|
|
15
15
|
const staged = (baseline.agentBinary?.stagedPath ?? "").replace(/^~(?=$|\/)/, homedir());
|
|
16
16
|
if (staged && existsSync(staged))
|
|
17
17
|
return resolve(staged);
|
|
18
|
-
// The baseline's exact version dir is gone (e.g. Claude Desktop updated).
|
|
19
|
-
//
|
|
18
|
+
// The baseline's exact version dir is gone (e.g. Claude Desktop updated).
|
|
19
|
+
// By default this is a hard failure — a different agent version can silently change behavior.
|
|
20
|
+
// Set COWORK_HARNESS_ALLOW_AGENT_FALLBACK=1 to opt in to using the newest sibling binary.
|
|
21
|
+
const exactPath = staged || "(unknown)";
|
|
20
22
|
const fallback = staged ? newestStagedBinary(staged) : undefined;
|
|
21
|
-
if (fallback) {
|
|
23
|
+
if (fallback && process.env.COWORK_HARNESS_ALLOW_AGENT_FALLBACK === "1") {
|
|
22
24
|
process.stderr.write(`cowork-harness: staged agent binary "${staged}" not found; ` + `falling back to newest sibling "${fallback}".\n`);
|
|
23
25
|
return fallback;
|
|
24
26
|
}
|
|
27
|
+
if (fallback) {
|
|
28
|
+
// A fallback exists but the opt-in env is not set — fail explicitly rather than silently using a
|
|
29
|
+
// different agent version that could make the run appear green while running the wrong binary.
|
|
30
|
+
throw new Error(`cowork-harness: baseline agent binary not found: ${exactPath}. Set COWORK_HARNESS_ALLOW_AGENT_FALLBACK=1 to use the newest available.`);
|
|
31
|
+
}
|
|
25
32
|
throw new Error(`Staged agent binary not found at "${staged}". It is extracted from your Claude Desktop install ` +
|
|
26
33
|
`(claude-code-vm/<ver>/claude). Open Cowork once to stage it, or set COWORK_AGENT_BINARY to its path.`);
|
|
27
34
|
}
|