cowork-harness 0.7.1 → 0.9.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 +6 -0
- package/.claude/skills/cowork-harness/scripts/scenario.py +38 -5
- package/CHANGELOG.md +247 -0
- package/README.md +46 -30
- package/baselines/desktop-1.14271.0.json +220 -0
- package/baselines/provisioning/rootfs-provisioning.json +179 -0
- package/dist/agent/session.js +79 -17
- package/dist/assert.js +66 -26
- package/dist/baseline.js +32 -7
- package/dist/boundary-paths.js +73 -0
- package/dist/boundary.js +1 -1
- package/dist/cli-args.js +5 -0
- package/dist/cli.js +328 -138
- package/dist/decide/decider.js +142 -14
- package/dist/decide/external-channel.js +3 -1
- package/dist/egress/proxy.js +11 -10
- package/dist/egress/sidecar.js +75 -4
- package/dist/hostloop/workspace-handler.js +134 -22
- package/dist/prompt.js +10 -7
- package/dist/run/cassette.js +389 -91
- package/dist/run/chat.js +122 -36
- package/dist/run/doctor.js +37 -11
- package/dist/run/execute.js +233 -16
- package/dist/run/renderer.js +14 -1
- package/dist/run/run.js +90 -20
- package/dist/run/runs-gc.js +16 -7
- package/dist/run/scaffold.js +21 -2
- package/dist/run/skill-files.js +92 -0
- package/dist/run/skill-hash.js +99 -93
- package/dist/run/trace-view.js +34 -21
- package/dist/run/verdict.js +67 -3
- package/dist/runtime/container.js +1 -1
- package/dist/runtime/hostloop-prompt.js +47 -0
- package/dist/runtime/hostloop.js +31 -6
- package/dist/runtime/image-capabilities.js +271 -0
- package/dist/runtime/lima.js +24 -4
- package/dist/runtime/protocol.js +8 -3
- package/dist/runtime/stage.js +24 -6
- package/dist/session.js +87 -26
- package/dist/staging/mount-naming.js +124 -0
- package/dist/sync/cowork-sync.js +33 -8
- package/dist/types.js +53 -2
- package/docker/Dockerfile.agent +98 -6
- package/docker/Dockerfile.proxy +4 -1
- package/docs/README.md +8 -1
- package/docs/boundary.md +18 -3
- package/docs/cassette.md +52 -23
- package/docs/chat.md +15 -13
- package/docs/cowork-spawn-contract-1.12603.1.md +6 -5
- package/docs/decider-dir.md +14 -4
- package/docs/discovery.md +31 -3
- package/docs/fidelity-gaps.md +6 -4
- package/docs/maintenance.md +15 -10
- package/docs/scenario.md +70 -8
- package/docs/session.md +18 -14
- package/examples/README.md +2 -2
- package/examples/replays/README.md +2 -2
- package/package.json +7 -1
- package/python/README.md +23 -7
- package/python/cowork_harness.py +92 -8
- package/python/test_cowork_lane.py +102 -1
- package/python/test_scenario_lint.py +86 -0
- package/schema/cassette.v5.json +107 -0
- package/schema/cassette.v6.json +107 -0
- package/schema/run-result.json +24 -1
- package/schema/scenario.schema.json +11 -0
- package/schema/session.schema.json +0 -3
- package/scripts/boot-rootfs-vz.ts +99 -0
- package/scripts/build-rootfs-image.ts +152 -0
- package/scripts/capture-rootfs-manifest.ts +290 -0
- package/scripts/gen-schema.ts +4 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"$comment": "GENERATED from the Zod Assertion schema (src/types.ts) by scripts/gen-schema.ts — do not edit; run `npm run schema`.",
|
|
3
3
|
"keys": [
|
|
4
4
|
"allow_l0_plugin_divergence",
|
|
5
|
+
"allow_missing_capability",
|
|
5
6
|
"allow_permissive_auto_allow",
|
|
6
7
|
"artifact_json",
|
|
7
8
|
"dispatch_count_max",
|
|
@@ -29,5 +30,10 @@
|
|
|
29
30
|
"transcript_not_contains",
|
|
30
31
|
"transcript_not_matches",
|
|
31
32
|
"user_visible_artifact"
|
|
33
|
+
],
|
|
34
|
+
"verdictModifierKeys": [
|
|
35
|
+
"allow_l0_plugin_divergence",
|
|
36
|
+
"allow_missing_capability",
|
|
37
|
+
"allow_permissive_auto_allow"
|
|
32
38
|
]
|
|
33
39
|
}
|
|
@@ -38,7 +38,11 @@ import re
|
|
|
38
38
|
import sys
|
|
39
39
|
from pathlib import Path
|
|
40
40
|
|
|
41
|
-
# --- the replay-class taxonomy
|
|
41
|
+
# --- the replay-class taxonomy ---
|
|
42
|
+
# NB: this is NOT a 1:1 mirror of `contentKeys` in src/run/cassette.ts. cassette.ts keeps the verdict
|
|
43
|
+
# modifiers (VERDICT_MODIFIER_KEYS) in its content set so they replay as no-op passes; the linter
|
|
44
|
+
# deliberately keeps them OUT of CONTENT_KEYS so a modifier-only scenario still trips the `replay-noop`
|
|
45
|
+
# warning below (a no-op pass verifies nothing real — exactly what that warning is for).
|
|
42
46
|
CONTENT_KEYS = {
|
|
43
47
|
"result",
|
|
44
48
|
"transcript_contains",
|
|
@@ -70,7 +74,11 @@ LIVE_ONLY_KEYS = {
|
|
|
70
74
|
}
|
|
71
75
|
EGRESS_KEYS = {"egress_denied", "egress_allowed"}
|
|
72
76
|
# verdict modifiers — don't verify anything themselves (e.g. suppress a default-fail)
|
|
73
|
-
VERDICT_MODIFIER_KEYS = {
|
|
77
|
+
VERDICT_MODIFIER_KEYS = {
|
|
78
|
+
"allow_permissive_auto_allow",
|
|
79
|
+
"allow_l0_plugin_divergence",
|
|
80
|
+
"allow_missing_capability",
|
|
81
|
+
}
|
|
74
82
|
|
|
75
83
|
# Every key the replay-class logic knows how to handle. `replay_protocol_fidelity` is valid-but-not-authorable
|
|
76
84
|
# (errored separately below). This is also the embedded fallback for ASSERT_KEYS — kept EQUAL to the generated
|
|
@@ -282,8 +290,9 @@ def lint_doc(doc, path, raw_lines):
|
|
|
282
290
|
"WARN",
|
|
283
291
|
"replay-noop",
|
|
284
292
|
"every assertion is live-only (egress / no_delete_in_outputs / self_heal_ran / "
|
|
285
|
-
"transcript_no_host_path)
|
|
286
|
-
"
|
|
293
|
+
"transcript_no_host_path) or a verdict modifier (allow_*, a no-op pass) — on the "
|
|
294
|
+
"token-free `replay` lane the live-only ones are silently skipped and the verdict "
|
|
295
|
+
"modifiers verify nothing, so a replay PR gate would verify nothing.",
|
|
287
296
|
"Add a content assertion (result / transcript_* / tool_* / subagent_*) or a "
|
|
288
297
|
"manifest-backed one (file_exists / user_visible_artifact / artifact_json), or run this "
|
|
289
298
|
"scenario only on the live (run/record) lane.",
|
|
@@ -419,6 +428,30 @@ def _print_findings(findings, n_files):
|
|
|
419
428
|
|
|
420
429
|
def cmd_lint(args):
|
|
421
430
|
all_findings = []
|
|
431
|
+
# Expand directory args to their scenario files — mirrors src/run/inputs.ts `resolveInputs`: a SINGLE
|
|
432
|
+
# combined-sorted `*.yaml` + `*.yml` listing (non-recursive), a single file kept as-is, and an EMPTY dir
|
|
433
|
+
# as a loud ERROR (never a vacuous "0 files = clean"). Done in place so the lint loop AND the count below
|
|
434
|
+
# both see the expanded list.
|
|
435
|
+
expanded = []
|
|
436
|
+
for arg in args.files:
|
|
437
|
+
p = Path(arg)
|
|
438
|
+
if p.is_dir():
|
|
439
|
+
matches = sorted(str(q) for q in (list(p.glob("*.yaml")) + list(p.glob("*.yml"))))
|
|
440
|
+
if matches:
|
|
441
|
+
expanded.extend(matches)
|
|
442
|
+
else:
|
|
443
|
+
all_findings.append(
|
|
444
|
+
Finding(
|
|
445
|
+
"ERROR",
|
|
446
|
+
"no-scenarios",
|
|
447
|
+
f"no .yaml/.yml files under {arg} — nothing to do (loud non-zero, not a vacuous pass)",
|
|
448
|
+
"Point lint at a scenario file or a directory containing *.yaml / *.yml scenarios.",
|
|
449
|
+
arg,
|
|
450
|
+
)
|
|
451
|
+
)
|
|
452
|
+
else:
|
|
453
|
+
expanded.append(arg)
|
|
454
|
+
args.files = expanded
|
|
422
455
|
# Linter self-check (B3): a valid schema key the replay-class sets don't classify can't be linted
|
|
423
456
|
# correctly — surface it as a hard ERROR so it fails the gate (and --strict) until someone classifies it.
|
|
424
457
|
if UNCLASSIFIED_KEYS:
|
|
@@ -598,7 +631,7 @@ def main(argv=None):
|
|
|
598
631
|
sub = ap.add_subparsers(dest="command", required=True)
|
|
599
632
|
|
|
600
633
|
lp = sub.add_parser("lint", help="lint scenario(s) for silent-false-green invariants")
|
|
601
|
-
lp.add_argument("files", nargs="+", help="scenario YAML file(s) to lint")
|
|
634
|
+
lp.add_argument("files", nargs="+", help="scenario YAML file(s) or director(ies) of *.yaml/*.yml to lint")
|
|
602
635
|
lp.add_argument("--json", action="store_true", help="emit findings as JSON")
|
|
603
636
|
lp.add_argument("--strict", action="store_true", help="exit non-zero on WARN/INFO too, not just ERROR")
|
|
604
637
|
lp.set_defaults(func=cmd_lint)
|
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,253 @@ All notable changes to this project are documented here. The format is based on
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.9.0] — 2026-06-22
|
|
10
|
+
|
|
11
|
+
### Breaking changes
|
|
12
|
+
|
|
13
|
+
- **Cassette staleness fingerprint bumped to format v6 (re-record once).** The skill-hash boundary changed,
|
|
14
|
+
so committed cassettes recorded before this release report `recorded under an older hash format — re-record
|
|
15
|
+
once`. Drivers: (1) **OS-junk** files (`.DS_Store` / `Thumbs.db` / `desktop.ini`) are excluded from
|
|
16
|
+
`skillHash` — an out-of-band OS metadata touch can no longer re-stale a cassette (the "fresh cassette is
|
|
17
|
+
immediately stale" bug); (2) **`contentSig` is unified onto the same walk as `skillHash`** (same file set,
|
|
18
|
+
plugin.json `version` stripped, in-tree symlinks hashed by target) — `rehash` cannot bridge this algorithm
|
|
19
|
+
change, so a pre-v6 cassette gets an honest *"algorithm changed — re-record"* (not "content changed"); (3)
|
|
20
|
+
the **git-tracked file set is the default boundary** (see Added).
|
|
21
|
+
- **Git-tracked staleness/mount boundary is now the DEFAULT.** When a skill/plugin source dir is in a git
|
|
22
|
+
work tree, both the staleness hash and the sandbox mount use only its **git-tracked** files (untracked
|
|
23
|
+
scratch / build output / OS-junk are excluded from both, so they can't drift the hash or leak into the
|
|
24
|
+
sandbox). A dir that isn't a git repo falls back to the raw walk automatically. Opt out with
|
|
25
|
+
`COWORK_HARNESS_GITSET=0`.
|
|
26
|
+
- **Removed the legacy CLI aliases.** `assert` → use `assertions`; `replay --cassette <file>` → pass the
|
|
27
|
+
path positionally (`replay <file | dir/>`); `verify-cassettes --privacy-only` / `--staleness-only` →
|
|
28
|
+
`--skip-privacy` / `--skip-staleness`. (0.8.0 had documented the latter two groups as renamed/removed but
|
|
29
|
+
the code still accepted the old forms; they are now gone. Each removed alias exits `2` if used.)
|
|
30
|
+
- **`assertions --list --output-format json` now reports `command: "assertions"`** (was the stale
|
|
31
|
+
`"assert"`) — a JSON-envelope contract fix for anything keying on the `command` field.
|
|
32
|
+
- **`decide` exits `2` (not `1`) on a runtime error**, matching the documented "usage / runtime → `2`"
|
|
33
|
+
contract. No-match / abstain still exits `1`.
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- **`cowork-harness/secrets` package export** — `scrubField` and `collectSecrets` are now importable as a
|
|
38
|
+
declared subpath (`import { scrubField, collectSecrets } from "cowork-harness/secrets"`) for custom
|
|
39
|
+
redaction pipelines, with the documented usage corrected to `scrubField(value, collectSecrets())` (a bare
|
|
40
|
+
`[token]` array misses secrets embedded in encoded fields). Adding the `exports` map also **bounds the
|
|
41
|
+
package's public surface to this one subpath** — deep imports into `dist/` (`cowork-harness/dist/...`),
|
|
42
|
+
previously resolvable by accident, are now private. The CLI (`bin`) is unaffected.
|
|
43
|
+
- **`lint` accepts a directory** — it expands to the directory's `*.yaml` / `*.yml` scenarios
|
|
44
|
+
(non-recursive, sorted), the same file-or-dir ergonomics as `replay` / `verify-cassettes`. An empty
|
|
45
|
+
directory is a loud error, never a vacuous "0 files = clean" pass.
|
|
46
|
+
- **Staleness now names the EXACT changed file.** A per-file manifest (`fileSigs`) in the cassette fingerprint
|
|
47
|
+
lets `verify-cassettes` report e.g. `skill files changed since record — 1 changed (skills/x/SKILL.md)`
|
|
48
|
+
instead of a coarse bucket message (appended to the existing shared-vs-scoped diagnosis). Manifest paths are
|
|
49
|
+
root-relative and are scanned + redacted with the same privacy layer as `skillSources`. Omitted (with a
|
|
50
|
+
loud `fileSigsOmitted`) above an internal size cap.
|
|
51
|
+
- **`COWORK_HARNESS_DEBUG_SKILLHASH=1`** — on a staleness mismatch, dumps the exact file set feeding the hash
|
|
52
|
+
to stderr and flags OS-junk, so a drift source is one line instead of a black-box hunt (a one-line hint
|
|
53
|
+
points to it when the flag is off).
|
|
54
|
+
- **`COWORK_HARNESS_GITSET=0`** — opt out of the new default git-tracked boundary (see Breaking) back to the
|
|
55
|
+
legacy raw filesystem walk for every dir.
|
|
56
|
+
- **`requires_capabilities` scenario assertion** — fail a scenario unless the running tier provides *and can
|
|
57
|
+
verify* the declared capability families (e.g. `office_convert`, `pdf_tables`). The unmet set is persisted
|
|
58
|
+
in the run result (`requiresCapabilityUnmet`), so `verify-run` can't false-fail; opt out with the
|
|
59
|
+
`allow_missing_capability` verdict modifier when the skill's fallback is genuinely equivalent.
|
|
60
|
+
- **LLM decider `OTHER:` free-text directive** — on an options-bearing gate, a decider answer of
|
|
61
|
+
`OTHER: <text>` is matched to a label first, else passed through as free text; a bare out-of-set value
|
|
62
|
+
still fails loud.
|
|
63
|
+
|
|
64
|
+
### Fixed
|
|
65
|
+
|
|
66
|
+
- **`doctor --tier microvm` now checks the right prerequisites.** It previously probed the Docker daemon +
|
|
67
|
+
agent image + egress-proxy image for every live tier, but the `microvm` (L2) tier runs on **Lima / Apple
|
|
68
|
+
Virtualization.framework**, not Docker — so it could report "not ready" on a Lima-only host, or "ready"
|
|
69
|
+
with no Lima installed. `microvm` now checks `limactl` (honoring `COWORK_LIMACTL`) + the staged agent
|
|
70
|
+
binary, and skips the Docker checks; `container`/`hostloop`/`cowork` are unchanged.
|
|
71
|
+
- **A freshly recorded cassette no longer reports `[stale]` immediately** because the OS rewrote a `.DS_Store`
|
|
72
|
+
(or other OS-junk) in the skill tree — OS-junk is excluded from the skill hash. A chronic false-positive
|
|
73
|
+
that pushed consumers to WARN-only (which then masked real drift).
|
|
74
|
+
- A standalone verdict-modifier assertion (e.g. `allow_l0_plugin_divergence: true`) no longer false-fails
|
|
75
|
+
as "empty assertion", and verdict modifiers no longer trigger a misleading "filesystem/egress skipped"
|
|
76
|
+
warning on the replay lane. The verdict modifiers are now single-sourced from one list (`assert.ts`,
|
|
77
|
+
`cassette.ts`, and the Python linter all derive from / are checked against it), guarded by a convention
|
|
78
|
+
test against drift.
|
|
79
|
+
- **A tail-end transport drop is no longer conflated with an agent failure.** A connection closed *after* a
|
|
80
|
+
clean result is classified as `resultErrorKind: "transport"` (vs `"agent"`) and surfaced as a
|
|
81
|
+
lane/assertion-aware `transport_error` verdict — still a failure (no false-green), but distinguishable from
|
|
82
|
+
a genuine skill error; a non-matching envelope falls back to the agent classification.
|
|
83
|
+
- **Clearer guard / capability legibility.** The run footer lists only guards that actually ran this lane
|
|
84
|
+
(`capabilityProbe: definitive | unverified | skipped`) — never a false check-mark for a guard that didn't
|
|
85
|
+
run; capability notices state their own safety net + all-clear with verdict-impact tags; the unbuilt `max`
|
|
86
|
+
tier is dropped from capability hints; and Docker pool-exhaustion is reframed as a concurrency limit, not a
|
|
87
|
+
leak.
|
|
88
|
+
- **Ordered interrupt cleanup.** A `SIGINT` / `SIGTERM` during a live run reaps in-flight egress resources in
|
|
89
|
+
order (container thunks before network thunks) and announces itself, instead of leaving them dangling.
|
|
90
|
+
|
|
91
|
+
## [0.8.0] — 2026-06-21
|
|
92
|
+
|
|
93
|
+
### Breaking changes
|
|
94
|
+
|
|
95
|
+
- **Work folders now mount at `mnt/<folder-name>`, not `mnt/.projects/<id>`; the folder `to:` field is
|
|
96
|
+
removed.** Binary-verified (asar 1.14271.0): real Cowork mounts each connected work folder at a
|
|
97
|
+
collision-resolved **basename** of its canonical path (e.g. `mnt/project`) with no author-chosen name —
|
|
98
|
+
so the session-schema `folders[].to` override is GONE (it had no Cowork analog; names are always derived).
|
|
99
|
+
Same-basename folders are disambiguated tier-accurately (host-loop keeps the first bare, the VM/container
|
|
100
|
+
tier escalates both with a `--parent` prefix). Plugins likewise move from the synthetic
|
|
101
|
+
`mnt/.local-plugins/cache/<…>` to the real `mnt/.local-plugins/marketplaces/<marketplace>/<plugin>` (no
|
|
102
|
+
`cache/`, no version segment). **Version-gated:** this applies to Desktop **≥ 1.14271.0** (current
|
|
103
|
+
baselines); older baselines keep the legacy `.projects/<id>` + `cache/` paths. `user_visible_artifact`
|
|
104
|
+
and the artifact manifest now derive their visible roots from the actual mount set (persisted as
|
|
105
|
+
`RunResult.userVisibleRoots`), and the cassette format bumps **v3 → v4** to store them.
|
|
106
|
+
- *Upgrade note:* remove `to:` from `folders[]` in session files (the name derives from the folder
|
|
107
|
+
basename). Reference connected-folder artifacts as `<folder-name>/…` (e.g. `project/summary.md`) instead
|
|
108
|
+
of `.projects/<id>/…`. A folder-artifact cassette recorded before v4 must be **re-recorded** (`rehash`
|
|
109
|
+
cannot migrate it — it only re-hashes skill fingerprints). A connected folder whose basename collides
|
|
110
|
+
with a reserved Cowork mount name (`outputs`, `uploads`, `.projects`, …) on the VM/container tier is now
|
|
111
|
+
rejected loudly instead of silently shadowing the fixed dir — rename the folder.
|
|
112
|
+
|
|
113
|
+
- **Run output now defaults to `~/.cowork-harness/runs`, not `<cwd>/runs`.** A `run` / `skill` / `chat` /
|
|
114
|
+
`record` launched from a repo no longer drops run artifacts (often sensitive skill inputs/outputs) into the
|
|
115
|
+
working tree — the root moved out of any working tree, matching the `~/.cowork-harness/` convention already
|
|
116
|
+
used for VM work dirs. The root is **flat and machine-global** (shared across every project on the machine),
|
|
117
|
+
not per-project. The readers (`trace` / `scaffold` / `verify-run`) resolve the same default, so a bare
|
|
118
|
+
`trace <run-id>` now works from any directory; the previous cwd-relative / repo-root resolution tiers were
|
|
119
|
+
removed. A one-time `runs → <dir>` line prints on stderr when the default is used (suppressed under
|
|
120
|
+
`--quiet` / `--output-format json`, or when an override is set).
|
|
121
|
+
- *Upgrade note (CI / scripts):* anything that reads `./runs` after a run — a CI `upload-artifact path: runs/`,
|
|
122
|
+
a glob over `runs/**`, a `.gitignore` entry — must now set **`COWORK_HARNESS_RUNS_DIR`** (or pass
|
|
123
|
+
`--run-dir`) to a workspace path so output lands where it's expected. Otherwise the step finds an empty
|
|
124
|
+
`./runs` (and, if it doesn't fail on empty, passes silently). The bundled CI recipe sets
|
|
125
|
+
`COWORK_HARNESS_RUNS_DIR: runs` on the live-scenario job for exactly this reason.
|
|
126
|
+
|
|
127
|
+
- **Agent image bumped `cowork-agent-base:1` → `:2` — REBUILD REQUIRED.** The image now mirrors the real
|
|
128
|
+
Cowork rootfs's preinstalled toolchain (binary-verified by mounting the rootfs): **Node 22.22.3** (was
|
|
129
|
+
ubuntu's node 12), the full **Python document/data stack** (openpyxl/pandas/numpy/pdfplumber/python-docx/
|
|
130
|
+
python-pptx/matplotlib/…), node doc-gen globals (pdf-lib/pptxgenjs/sharp/tsx/…), `ruby`/`ffmpeg`/`qpdf`,
|
|
131
|
+
`C.UTF-8` locale, and it now runs as **uid 1000 (`ubuntu`)** like the real VM. Rebuild with the command
|
|
132
|
+
`cowork-harness doctor --tier container` prints, or set `COWORK_AGENT_IMAGE` to your own tag. *(Why: the
|
|
133
|
+
real rootfs ships openpyxl etc. preinstalled — omitting them made skills that read `.xlsx`/PDFs falsely
|
|
134
|
+
appear degraded under the harness. "pypi blocked at runtime" ≠ "not preinstalled.")*
|
|
135
|
+
- **A green run that uses a capability the agent image OMITS now FAILS** (verdict signal `missing_capability`),
|
|
136
|
+
mirroring `permissive_auto_allow`/`l0_plugin_divergence`. The default "core" image omits the heavy lane
|
|
137
|
+
(OCR/LibreOffice/markitdown/opencv/PDF-tables); a run that uses one is a likely false negative (real Cowork
|
|
138
|
+
ships it). Suppress per-scenario with **`allow_missing_capability: true`** (when the fallback is equivalent),
|
|
139
|
+
rebuild full parity (`--build-arg COWORK_FULL_PARITY=1`), use the rootfs `max` tier, or skip the check with
|
|
140
|
+
`COWORK_SKIP_CAPABILITY_PROBE=1`. Live tiers only (container/hostloop/microvm).
|
|
141
|
+
- **Stricter, fail-loud guardrails from the codebase bug-review sweep — a few previously-silent paths now
|
|
142
|
+
error.** These close false-greens / false-accepts and can surface on an existing setup:
|
|
143
|
+
- **Negative `verify-run` assertions fail on *missing* evidence instead of passing vacuously.**
|
|
144
|
+
`tool_result_not_contains`, `tool_not_called`, `subagent_tool_absent`, `dispatch_count_max`,
|
|
145
|
+
`subagent_declared_but_unused`, `no_delete_in_outputs`, `transcript_no_host_path`, and `self_heal_ran`
|
|
146
|
+
now fail with an "evidence unavailable" reason when the underlying field is absent from a partial/older
|
|
147
|
+
`result.json` (verify-run lane only — the live and replay lanes, where the evidence is always present,
|
|
148
|
+
are unchanged). *Upgrade note:* re-run rather than re-assert against a fresh `result.json` if a verify-run
|
|
149
|
+
flips to this failure.
|
|
150
|
+
- **Non-strict cassette replay now fails on corrupt `controlOut`** — a malformed line, or a duplicate
|
|
151
|
+
`request_id` with differing bodies — instead of warning and proceeding. Corruption is a protocol-fidelity
|
|
152
|
+
failure, not advisory; `--strict` still additionally catches staleness/extra-data.
|
|
153
|
+
- **`chat --raw` now rejects `--upload` / `--folder` / `--plugin` / `--fidelity`** (it can't honor them in
|
|
154
|
+
native mode) instead of silently ignoring them, and an invalid `COWORK_HARNESS_FIDELITY` value is rejected
|
|
155
|
+
loudly instead of silently falling back to `container`. An invalid `COWORK_HARNESS_MAX_ARTIFACT_BYTES` now
|
|
156
|
+
errors (parity with the `--max-artifact-bytes` flag) instead of silently using the default.
|
|
157
|
+
- **A `web_fetch` `approved_domains` / seed entry that isn't a bare host** (a URL, scheme, path, port, empty
|
|
158
|
+
string, or `*` wildcard) is now rejected loudly instead of being added as an inert, never-matching entry.
|
|
159
|
+
- **An over-cap (> 256 KiB) control-out frame now fails the live recording immediately** with a clear error,
|
|
160
|
+
instead of writing an unreplayable truncation marker that only surfaced later as a replay failure.
|
|
161
|
+
|
|
162
|
+
### Added
|
|
163
|
+
|
|
164
|
+
- **Capability fidelity detection.** The harness probes the agent runtime (Docker image via `--network none`
|
|
165
|
+
run, or the L2 microVM via `limactl shell`) for the document/OCR/Office capabilities the real Cowork rootfs
|
|
166
|
+
ships, caches the result by `(tier, identity)`, and detects (from `events.jsonl`) when a skill used an
|
|
167
|
+
omitted one — surfaced as the new `RunResult.missingCapabilityUse` field, a `::notice::`/`::warning::`, and
|
|
168
|
+
the `missing_capability` verdict above. New assertion `allow_missing_capability`.
|
|
169
|
+
- **Opt-in full-parity image** (`docker build --build-arg COWORK_FULL_PARITY=1 -t cowork-agent-full:2`) adds
|
|
170
|
+
tesseract / LibreOffice / opencv / onnxruntime+markitdown / camelot+tabula for OCR/Office/extraction skills.
|
|
171
|
+
- **Rootfs `max` tier (`npm run build:rootfs-image`)** — builds a Docker image from the user's OWN
|
|
172
|
+
`rootfs.img` (local, byte-for-byte parity; cached by rootfs mtime+size); point `COWORK_AGENT_IMAGE` at it.
|
|
173
|
+
- **Provisioning drift gate (`npm run capture:rootfs`)** — captures the rootfs's toolchain to
|
|
174
|
+
`baselines/provisioning/rootfs-provisioning.json`; `--check <image>` diffs a built image against it.
|
|
175
|
+
- **L2 (microVM) toolchain parity** — Lima provisioning now installs the same document/data stack
|
|
176
|
+
(best-effort, with 24.04 version drift accepted).
|
|
177
|
+
- **`--run-dir <path>` global flag** to relocate the runs root (a thin shim over `COWORK_HARNESS_RUNS_DIR`).
|
|
178
|
+
Precedence: `--run-dir` > `COWORK_HARNESS_RUNS_DIR` > `~/.cowork-harness/runs`. Keeps sensitive artifacts out
|
|
179
|
+
of a working tree without the prior `cd`-into-a-scratch-dir workaround. Both spellings (`--run-dir <path>` and
|
|
180
|
+
`--run-dir=<path>`) are accepted; unlike `--dotenv` it does not require the path to exist (it's an output dir).
|
|
181
|
+
- **Cross-project overwrite guard for pinned (`--session-id`) sessions.** On the flat shared root a pinned
|
|
182
|
+
`sess-<id>` run dir is deterministic, so two projects can resolve to the same path. The writer now identifies
|
|
183
|
+
a run by its **mounted-source content** (recorded in an `outDir/.origin` marker) and:
|
|
184
|
+
- **errors instead of silently `rm -rf`-ing** a dir that belongs to a different project (the old behavior
|
|
185
|
+
blind-deleted a colliding peer's persisted, resumable session);
|
|
186
|
+
- **fails closed** on a missing/partial marker (a crashed prior run) — it throws with a "delete `<dir>` to
|
|
187
|
+
reset" hint rather than deleting an unconfirmable dir;
|
|
188
|
+
- treats a **sourceless inline scenario** (which has no content to identify it, only cwd) as unconfirmable —
|
|
189
|
+
it throws rather than risk a cwd-collision delete (`skill <dir>` runs always mount the skill, so the common
|
|
190
|
+
pinned workflow is unaffected);
|
|
191
|
+
- blocks **`--resume`** onto a different project's session in place (override with
|
|
192
|
+
`COWORK_HARNESS_ALLOW_FOREIGN_RESUME=1`).
|
|
193
|
+
- **`runs gc` never prunes pinned `sess-*` sessions** (and they don't consume a `--keep-last` slot — partitioned
|
|
194
|
+
out before counting, so a retained pinned dir can't evict a newer ephemeral run). Only ephemeral `local_*`
|
|
195
|
+
runs are pruned. Because the default root is now shared, a bare `runs gc` prunes ephemeral runs across **all**
|
|
196
|
+
projects; pass an explicit `<runs-dir>` to scope it.
|
|
197
|
+
- **multiSelect `AskUserQuestion` gates are now answerable on every answer channel.** Scripted
|
|
198
|
+
`choose: [list]` already worked; the in-band `--decider-dir` channel now accepts a repeated
|
|
199
|
+
`--choose` (`answer <dir> --gate 1 --choose Auth --choose Audit`), and `--decider-cmd` helpers /
|
|
200
|
+
hand-written `resp-N.json` accept a JSON-array reply (`{"answers":{"<q>":["Auth","Audit"]}}`). All
|
|
201
|
+
channels deliver the binary-verified `", "`-joined wire shape; a member matching no option, an array
|
|
202
|
+
on a single-select gate, or an empty array each fails loud. `cowork-harness answer`'s `--choose` is
|
|
203
|
+
now repeatable for multiSelect gates (still single-only on single-select). Verified end-to-end
|
|
204
|
+
against a live model (the real agent re-reads the joined answer as multiple selections).
|
|
205
|
+
- **`scaffold --from-run`** flags a delivered answer that looks like a multiSelect set (contains
|
|
206
|
+
`", "`) with a loud comment telling the author to split `choose: "A, B"` into `choose: [A, B]`
|
|
207
|
+
before replay (a scaffolded multiSelect answer would otherwise not match on replay).
|
|
208
|
+
|
|
209
|
+
### Fixed
|
|
210
|
+
|
|
211
|
+
- `doctor`'s staged-agent remedy now hints to put `COWORK_AGENT_BINARY` in `.env` so `--dotenv` covers it
|
|
212
|
+
(like the auth token) — avoiding a misleading "red" when `doctor` is run without the same env/flags the real
|
|
213
|
+
run uses.
|
|
214
|
+
- **`--decider-dir` / `--decider-cmd` no longer crash on a multiSelect array reply.** `coerceLabel`
|
|
215
|
+
previously called `.trim()` on a non-string answer and threw a bare `TypeError`, aborting the run;
|
|
216
|
+
it now throws a clear `UnansweredError` instead, and a multiSelect array is validated per-member and
|
|
217
|
+
delivered as the joined wire shape.
|
|
218
|
+
- **Codebase bug-review sweep — 49 validated fixes across the harness** (beyond the behavior changes noted
|
|
219
|
+
under Breaking):
|
|
220
|
+
- **CLI parsing:** `vm status --output-format json` works and emits a JSON envelope, instead of misreading
|
|
221
|
+
the flag as a baseline name; `skill` and common flags accept every documented `--flag=value` form;
|
|
222
|
+
`boundary-check` reports a missing/malformed session as a clean (JSON-aware) usage error rather than an
|
|
223
|
+
internal error; `chat` rejects extra positionals and empty/flag-looking value-flag arguments; `parseArgs`
|
|
224
|
+
rejects empty `--flag ""` values.
|
|
225
|
+
- **Path / boundary hardening:** named baselines can no longer escape `baselines/` via `../`; marketplace and
|
|
226
|
+
staged-mount symlinks that resolve out of tree are rejected (realpath, not lexical); collected artifacts
|
|
227
|
+
skip hardlinks (`nlink > 1`) that could inline out-of-root content into a cassette; a new
|
|
228
|
+
`src/boundary-paths.ts` centralizes `safeNamedBaseline` / `containedRealPath` / `normalizeHost` /
|
|
229
|
+
`validateBareDomain` so the egress allowlist and seed-domain paths share one policy.
|
|
230
|
+
- **Protocol / replay integrity:** every control-request validates its `request_id` before replying; the
|
|
231
|
+
AskUserQuestion body is validated at ingress (optionless / header-only gates still pass); a malformed
|
|
232
|
+
cassette no longer aborts the whole replay batch; a `web_fetch` decision of the wrong kind is recorded as
|
|
233
|
+
`mismatch→deny` with a warning; egress host matching is case- and trailing-dot-normalized.
|
|
234
|
+
- **host-loop fidelity:** the `web_fetch` SSRF backstop pins the vetted address through connect (closing a
|
|
235
|
+
DNS-rebind TOCTOU) and re-vets each redirect; a Docker infrastructure failure (daemon down, missing
|
|
236
|
+
container, exit 125) is reported to the model as a generic harness error and logged raw, instead of
|
|
237
|
+
leaking daemon text framed as a normal command exit.
|
|
238
|
+
- **Fidelity drift checks:** the rootfs `--check` diffs the whole Layer-A pip set (generated from the
|
|
239
|
+
Dockerfile) plus Node, the apt doc stack, and global npm; rootfs image tags are content-addressed; the
|
|
240
|
+
capability cache keys on image content rather than a mutable tag; `sync` cleans its temp extraction dir and
|
|
241
|
+
records a drift signal on a corrupt `config.json`.
|
|
242
|
+
- **decider / schema / Python:** `allow_if` predicates accept non-identifier input keys (`input["file-path"]`);
|
|
243
|
+
`choose`+`answer` and inert `grant` are rejected at schema time; optionless prompt/LLM gates are answerable;
|
|
244
|
+
the Python wrapper returns a `BatchResult` for directory/replay runs so a later failure can't hide behind a
|
|
245
|
+
passing first result.
|
|
246
|
+
- **Documentation audit sweep — stale references corrected, gaps filled.** Bumped the "current baseline" pins
|
|
247
|
+
across README / DESIGN / SPEC / spawn-contract / skill docs to `desktop-1.14271.0` (agent ELF 2.1.181),
|
|
248
|
+
re-verified end-to-end against the live staged agent at this baseline; added the missing
|
|
249
|
+
`tool_result_contains` / `tool_result_not_contains` and `allow_missing_capability` assertion rows plus a
|
|
250
|
+
verdict-signals (`prompt_asset_missing`) section to the docs; documented `doctor` / `rehash` / `runs gc` and
|
|
251
|
+
exit code `3` in `llms.txt` and the README command table; corrected the SPEC §11 exit-code table
|
|
252
|
+
(`boundary` → `3`), the `chat --folder` / `folders[].to` notes, the `trace --view tools` flag, and the
|
|
253
|
+
skill-bootstrap version floor (`@>=0.7.1`). **CI fix:** the agent-base image build in
|
|
254
|
+
`.github/workflows/ci.yml` was tagged `:1` while every code path defaults to `:2` — now `:2`.
|
|
255
|
+
|
|
9
256
|
## [0.7.1] — 2026-06-20
|
|
10
257
|
|
|
11
258
|
### Fixed
|