diff --git a/agents/darkwing/work/s6-review/mut/Ma-late-signals-cli.txt b/agents/darkwing/work/s6-review/mut/Ma-late-signals-cli.txt new file mode 100644 index 00000000..5ce9d54c --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Ma-late-signals-cli.txt @@ -0,0 +1,117 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (102.91821ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (121.245477ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (104.328016ms) +✔ decide prints a declining choice as declining (98.264991ms) +✔ an unknown outcome is reported once and never resent (87.04748ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (81.339494ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (92.410729ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (85.608463ms) +✔ every human command refuses inside an agent run before it touches the bus (69.207672ms) +✔ usage errors exit 4; no business and no host is a usage error (83.827733ms) +✔ agents and tasks print through the broker (66.23017ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.136984ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (53.128351ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.918861ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.322342ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (29.633405ms) +✔ a business without tracker.baseUrl gets no trackers entry (28.530151ms) +✔ an unknown business and a broken system config refuse with exit 3 (49.933711ms) +✔ empty views say so (0.95895ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.176214ms) +✔ tasks print the tracker fields the snapshot carries (0.206492ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (897.060907ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (218.96706ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (166.998187ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (240.380587ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (176.050616ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (99.085127ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (157.614738ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (106.266039ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (172.254634ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (24.320455ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.668627ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (209.251282ms) +✔ bus start refuses with exit 3 without a notifier config (90.631351ms) +✔ bus start runs until bus stop; status reports it while it runs (651.145497ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.252918ms) +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (30261.274586ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (18484.12709ms) +✔ a runner that stops at once ends its launch with the runner's reason (192.822298ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (30672.073519ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (33689.424955ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (170.672928ms) +✔ zoned uses the IANA zone across DST (24.830415ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (116.216116ms) +✔ two blocking decisions get two DMs with different nonces (115.607596ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.182767ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (108.508712ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (94.134315ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (100.255285ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (94.210215ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (164.847367ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (129.956437ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (114.751541ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (46.672456ms) +✔ an inbox read failure is logged and the next poll retries (0.651746ms) +✔ no Discord id reaches the journal or the log (74.269566ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (13.045248ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (31.448374ms) +✔ the journal: a whole file that is one torn line truncates to empty (25.364473ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.986317ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.773563ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (5.098288ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.437801ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.763012ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.638287ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.733887ms) +✔ digest content stays within Discord's 2000 characters (0.483725ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.972535ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (365.138347ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (41.26363ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2139.875576ms) +✔ busExit and refuseInsideAgent (0.410677ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (201.635337ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1135.61941ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (70.768797ms) +✔ launches off and on go to the broker and change the business's launch state (75.752156ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (56.90734ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 75 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 113545.957339 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:105:1 +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (30261.274586ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + 'killed' + - 'stopped' + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:171:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 'killed', + expected: 'stopped', + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:175:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (18484.12709ms) + Error: timed out waiting + at until (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:43:9) + at async TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:215:3) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) diff --git a/agents/darkwing/work/s6-review/mut/Ma-late-signals-harness.txt b/agents/darkwing/work/s6-review/mut/Ma-late-signals-harness.txt new file mode 100644 index 00000000..6a765a9c --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Ma-late-signals-harness.txt @@ -0,0 +1,116 @@ +✔ sessionModel: agent vars win, then the system's execution settings (14.060701ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.264076ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (2.839664ms) +✔ a bundle is written once: an existing file refuses (3.015861ms) +✔ a path with a single quote can't go into the hook command (2.544498ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (132.672334ms) +✔ a missing or wrong policy, or a bad event, exits 2 (87.579364ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1096.85555ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (822.993933ms) +✔ claude: the hook alone blocks a path outside the workspace (474.272951ms) +✔ claude: a second turn resumes the first turn's session (795.497289ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.958534ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.905317ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.824151ms) +✔ file tool paths must resolve inside the workspace (0.924751ms) +✔ pi's own path normalisation can't be used to step out (0.989216ms) +✔ a symlink inside the workspace that points out is outside (0.928767ms) +✔ claude path fields per tool (1.135022ms) +✔ glob patterns stay inside the workspace (0.905393ms) +✔ a path that can't be checked is blocked (0.641327ms) +✔ initialize, ping and tools/list (52.803314ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (36.588106ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (33.504222ms) +✔ a missing argument is a usage error (31.403397ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (415.995746ms) +✔ pi: a missing extension refuses before any model call (8.458682ms) +✔ pi: an extension without its configuration fails pi's start (279.315528ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.858056ms) +✔ turnRequest names the sender, class, reply and decision (0.265717ms) +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (193.748977ms) +✖ a SIGTERM before the claim stops the runner with exit 0 and no claim (92.328678ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (443.407365ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1296.372554ms) +✖ SIGTERM during a turn kills the turn's process group and still exits 0 (144.59476ms) +✔ founder credentials stop before the claim (20) (171.480323ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (197.025228ms) +✔ the launch ending under a running session exits 22 (142.65891ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (248.859517ms) +✔ a broker that is down at the claim exits 23, not 21 (106.087394ms) +✔ no capability, or a malformed one, on stdin exits 2 (164.168036ms) +✔ the PM gets launch, its task verbs and the reads (7.809666ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.544128ms) +✔ launch only when the business's launch block names the instance as launcher (2.508715ms) +✔ an action outside the instance's authority has no tool (3.166666ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (11.382644ms) +ℹ tests 45 +ℹ suites 0 +ℹ pass 42 +ℹ fail 3 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10318.14779 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:141:1 +✖ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (193.748977ms) + AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run + + + null !== 0 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:160:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: null, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:166:1 +✖ a SIGTERM before the claim stops the runner with exit 0 and no claim (92.328678ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly deep-equal: + + actual - expected + + [ + - 0, + null, + + 'SIGTERM' + ] + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:185:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: [ null, 'SIGTERM' ], + expected: [ 0, null ], + operator: 'deepStrictEqual', + diff: 'simple' + } + +test at packages/harness/tests/runner.test.mjs:232:1 +✖ SIGTERM during a turn kills the turn's process group and still exits 0 (144.59476ms) + AssertionError [ERR_ASSERTION]: runner: demo/coder claimed by run coder-run + + + null !== 0 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:242:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: null, + expected: 0, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mb-runs-set-early-bus.txt b/agents/darkwing/work/s6-review/mut/Mb-runs-set-early-bus.txt new file mode 100644 index 00000000..49dca98d --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mb-runs-set-early-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (184.047781ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (257.734218ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (278.500051ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (165.068907ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (161.335881ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (159.243496ms) +✔ task action subjects and linked decision trail are complete and ordered (176.904294ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (109.155948ms) +✔ business isolation includes inherited object names and cross-business message references (174.305239ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (270.690431ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (127.365414ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (183.630887ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (137.706215ms) +✔ both arbiters require human resolution when their cross-role route is themselves (271.610917ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.748448ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.508833ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (2.886979ms) +✔ expiry refuses use and env references never become client data (0.832119ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.473483ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.359737ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (133.186192ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (129.549343ms) +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (152.425639ms) +✔ endLaunch leaves a claim another run took alone (181.719438ms) +✔ refuse records action.refused against the caller with the code only (145.867162ms) +✔ launches off refuses role.launch with launch-revoked until launches on (176.571624ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (214.227705ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (2.122501ms) +✔ process reader gets own kernel identity without exposing environment values (0.56463ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.712195ms) +✔ real pid 1 remains inspectable when its environment is protected (0.361585ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (199.323709ms) +✔ missing and foreign-business citations refuse and roll back message and grant (191.07183ms) +✔ cross-role sends still need a matching resolved decision and consume it once (246.333185ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (188.169376ms) +✔ startup token refusal returns safe code without value or partial listening broker (41.838933ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (166.55587ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (176.255856ms) +✔ trusted host registers later launches; socket clients never have a registration verb (178.685487ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (36.737248ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (165.960804ms) +✔ v3b prototype refusals, views and append-only mutations (1032.22821ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (249.596775ms) +✔ another run cannot consume an approval; a failed check leaves it usable (224.168828ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (170.880883ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (308.703039ms) +✔ class drift gated to cross-role refuses before consumption (198.719962ms) +✔ class drift cross-role to gated refuses before consumption (226.403993ms) +✔ class drift gated to within-role refuses before consumption (207.238596ms) +✔ class drift cross-role to within-role refuses before consumption (227.695738ms) +✔ class drift within-role to gated refuses before consumption (194.611101ms) +✔ class drift within-role to cross-role refuses before consumption (170.531727ms) +✔ message.send consumes approval and prevents a later send or authorize (209.047544ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (321.964213ms) +✔ creates private WAL store and excludes a second writer until explicit close (137.579044ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.969185ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (191.788232ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (171.439266ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (105.58886ms) +✔ async transactions refuse before invoking their function (85.925133ms) +✔ recordTask keeps sync reads and a role write apart (184.805478ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (108.899917ms) +✔ a bad entry refuses the whole record (103.269817ms) +✔ taskView reads the projection for one business (126.877852ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (239.547908ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (395.224422ms) +✔ without an adapter the server refuses every task verb (198.533632ms) +✔ the runtime refuses an invalid adapter and closes a valid one (212.245707ms) +✔ the process loads the S3 adapter from plain-data trackers (258.328732ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (160.333179ms) +✔ two wire claims serialize; a lost reply never automatically retries (180.845064ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (99.411062ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.674905ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (135.16461ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2789.931252 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:73:1 +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (152.425639ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/bus/tests/end-launch.test.mjs:80:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /unknown-run/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mb-runs-set-early-cli.txt b/agents/darkwing/work/s6-review/mut/Mb-runs-set-early-cli.txt new file mode 100644 index 00000000..55d65f11 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mb-runs-set-early-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (175.975906ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (184.05482ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (135.975643ms) +✔ decide prints a declining choice as declining (115.597908ms) +✔ an unknown outcome is reported once and never resent (107.994328ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (114.399879ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (118.753309ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (98.356969ms) +✔ every human command refuses inside an agent run before it touches the bus (96.122047ms) +✔ usage errors exit 4; no business and no host is a usage error (104.825903ms) +✔ agents and tasks print through the broker (109.999782ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.3094ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (38.79278ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (37.77004ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.111855ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (32.831006ms) +✔ a business without tracker.baseUrl gets no trackers entry (28.202059ms) +✔ an unknown business and a broken system config refuse with exit 3 (56.936951ms) +✔ empty views say so (0.745144ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.076708ms) +✔ tasks print the tracker fields the snapshot carries (0.205381ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (950.611218ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (227.116354ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (176.539657ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (211.488732ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (225.739981ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (176.746236ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (269.362482ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (165.128453ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (259.705176ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (24.783932ms) +✔ bus stop refuses to signal a live pid that is not a bus host (205.447122ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (219.571849ms) +✔ bus start refuses with exit 3 without a notifier config (107.523707ms) +✔ bus start runs until bus stop; status reports it while it runs (731.219938ms) +✔ bus-service.sh renders the unit and installs it into a given directory (38.872725ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (331.308668ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (667.342883ms) +✔ a runner that stops at once ends its launch with the runner's reason (255.258443ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (843.785719ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3808.232229ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (167.609852ms) +✔ zoned uses the IANA zone across DST (13.817706ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (148.840172ms) +✔ two blocking decisions get two DMs with different nonces (156.482282ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.184902ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (155.373324ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (119.234353ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (114.466929ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (120.568701ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (173.689096ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (139.718846ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (137.675725ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (114.287003ms) +✔ an inbox read failure is logged and the next poll retries (0.671341ms) +✔ no Discord id reaches the journal or the log (113.963173ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (22.026662ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (42.879376ms) +✔ the journal: a whole file that is one torn line truncates to empty (19.168903ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.911022ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.753617ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (4.426854ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.578878ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.669505ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.410289ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.471842ms) +✔ digest content stays within Discord's 2000 characters (0.330777ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.304879ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (411.263109ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (32.559089ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2156.033595ms) +✔ busExit and refuseInsideAgent (0.434997ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (295.088871ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1177.149209ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (89.322782ms) +✔ launches off and on go to the broker and change the business's launch state (112.778973ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (164.101361ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6161.026277 diff --git a/agents/darkwing/work/s6-review/mut/Mc-no-run-ended-bus.txt b/agents/darkwing/work/s6-review/mut/Mc-no-run-ended-bus.txt new file mode 100644 index 00000000..774233f6 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mc-no-run-ended-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (292.561005ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (277.551361ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (497.078545ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (226.410623ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (200.406094ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (167.322227ms) +✔ task action subjects and linked decision trail are complete and ordered (167.929413ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (115.499176ms) +✔ business isolation includes inherited object names and cross-business message references (191.409782ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (274.594256ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (108.292348ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (189.599503ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (122.287696ms) +✔ both arbiters require human resolution when their cross-role route is themselves (209.820218ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.954793ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (6.947483ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.716844ms) +✔ expiry refuses use and env references never become client data (1.082525ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.931891ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.446136ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (247.796173ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (130.726344ms) +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (157.253366ms) +✔ endLaunch leaves a claim another run took alone (289.241075ms) +✔ refuse records action.refused against the caller with the code only (251.654803ms) +✔ launches off refuses role.launch with launch-revoked until launches on (250.720391ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (262.370088ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.987867ms) +✔ process reader gets own kernel identity without exposing environment values (0.776973ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.162267ms) +✔ real pid 1 remains inspectable when its environment is protected (0.450215ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (318.86415ms) +✔ missing and foreign-business citations refuse and roll back message and grant (196.815379ms) +✔ cross-role sends still need a matching resolved decision and consume it once (373.987519ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (309.978292ms) +✔ startup token refusal returns safe code without value or partial listening broker (40.260658ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (212.318126ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (284.283419ms) +✔ trusted host registers later launches; socket clients never have a registration verb (331.796722ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (50.863608ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (199.936442ms) +✔ v3b prototype refusals, views and append-only mutations (1474.221154ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (370.765558ms) +✔ another run cannot consume an approval; a failed check leaves it usable (254.968901ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (269.788235ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (497.173983ms) +✔ class drift gated to cross-role refuses before consumption (223.510713ms) +✔ class drift cross-role to gated refuses before consumption (231.466212ms) +✔ class drift gated to within-role refuses before consumption (212.371643ms) +✔ class drift cross-role to within-role refuses before consumption (210.13879ms) +✔ class drift within-role to gated refuses before consumption (186.617229ms) +✔ class drift within-role to cross-role refuses before consumption (166.777678ms) +✔ message.send consumes approval and prevents a later send or authorize (201.323203ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (232.747756ms) +✔ creates private WAL store and excludes a second writer until explicit close (245.691122ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (172.009427ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (256.716305ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (351.248691ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (149.511218ms) +✔ async transactions refuse before invoking their function (119.740418ms) +✔ recordTask keeps sync reads and a role write apart (277.097949ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (114.028215ms) +✔ a bad entry refuses the whole record (116.960536ms) +✔ taskView reads the projection for one business (210.868484ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (438.207826ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (438.027227ms) +✔ without an adapter the server refuses every task verb (198.146102ms) +✔ the runtime refuses an invalid adapter and closes a valid one (216.186052ms) +✔ the process loads the S3 adapter from plain-data trackers (273.619163ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (260.103868ms) +✔ two wire claims serialize; a lost reply never automatically retries (186.295991ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (122.398394ms) +✔ client preserves UTF-8 when a response divides a multibyte character (12.28368ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (244.901528ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3166.363958 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:73:1 +✖ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (157.253366ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/bus/tests/end-launch.test.mjs:79:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: /run-ended/, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mc-no-run-ended-cli.txt b/agents/darkwing/work/s6-review/mut/Mc-no-run-ended-cli.txt new file mode 100644 index 00000000..6321423b --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mc-no-run-ended-cli.txt @@ -0,0 +1,119 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (126.592557ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (130.171901ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (125.162483ms) +✔ decide prints a declining choice as declining (104.33608ms) +✔ an unknown outcome is reported once and never resent (89.318318ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (105.532354ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (105.868605ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (65.576879ms) +✔ every human command refuses inside an agent run before it touches the bus (111.297337ms) +✔ usage errors exit 4; no business and no host is a usage error (98.771393ms) +✔ agents and tasks print through the broker (255.048787ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.546141ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (45.672646ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (32.240318ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (29.603951ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.350544ms) +✔ a business without tracker.baseUrl gets no trackers entry (26.705721ms) +✔ an unknown business and a broken system config refuse with exit 3 (54.028045ms) +✔ empty views say so (0.997992ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.124414ms) +✔ tasks print the tracker fields the snapshot carries (0.15378ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (895.526588ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (212.725435ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (273.362879ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (245.235107ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (198.483689ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (167.789165ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (197.473695ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (124.052455ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (201.202702ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (23.250612ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.761707ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (213.465669ms) +✔ bus start refuses with exit 3 without a notifier config (86.816406ms) +✔ bus start runs until bus stop; status reports it while it runs (658.567869ms) +✔ bus-service.sh renders the unit and installs it into a given directory (28.128558ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1360.589465ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (776.641584ms) +✔ a runner that stops at once ends its launch with the runner's reason (199.870832ms) +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (711.911393ms) +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3581.287492ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (123.48589ms) +✔ zoned uses the IANA zone across DST (17.451622ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (132.26037ms) +✔ two blocking decisions get two DMs with different nonces (118.678545ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.218622ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (129.429198ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (109.642392ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (90.790543ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (101.894504ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (159.050941ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (147.116141ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (142.57825ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (249.598656ms) +✔ an inbox read failure is logged and the next poll retries (0.635093ms) +✔ no Discord id reaches the journal or the log (112.597633ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (24.68035ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (66.147162ms) +✔ the journal: a whole file that is one torn line truncates to empty (50.965235ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.213804ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.663316ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.799538ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.308098ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.665486ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.365494ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.426602ms) +✔ digest content stays within Discord's 2000 characters (0.260771ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.010563ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (365.325466ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (42.334679ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2132.640014ms) +✔ busExit and refuseInsideAgent (0.431379ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (226.693198ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1151.320994ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (103.217521ms) +✔ launches off and on go to the broker and change the business's launch state (173.616665ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (134.088458ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 75 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6835.150141 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:252:3 +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (711.911393ms) + AssertionError [ERR_ASSERTION]: run r3e4b2104c041 (pm) from an earlier host ended: host-lost + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:297:12) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:252:3 +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3581.287492ms) + AssertionError [ERR_ASSERTION]: run r86d98d19d98a (pm) from an earlier host ended: host-lost + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:297:12) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: false, + expected: true, + operator: '==', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Md-no-instance-running-cli.txt b/agents/darkwing/work/s6-review/mut/Md-no-instance-running-cli.txt new file mode 100644 index 00000000..e8c343e0 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Md-no-instance-running-cli.txt @@ -0,0 +1,132 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (134.382091ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (127.538676ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (116.860604ms) +✔ decide prints a declining choice as declining (125.22075ms) +✔ an unknown outcome is reported once and never resent (103.496002ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (125.776748ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (117.149997ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (115.624806ms) +✔ every human command refuses inside an agent run before it touches the bus (223.103098ms) +✔ usage errors exit 4; no business and no host is a usage error (150.021496ms) +✔ agents and tasks print through the broker (124.340719ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.57056ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (41.155739ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (35.962477ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (34.977988ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (45.484448ms) +✔ a business without tracker.baseUrl gets no trackers entry (35.743739ms) +✔ an unknown business and a broken system config refuse with exit 3 (70.823037ms) +✔ empty views say so (1.141349ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.432144ms) +✔ tasks print the tracker fields the snapshot carries (0.228737ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (911.580879ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (276.115157ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (200.288603ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (202.357798ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (207.000651ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (112.164695ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (185.066354ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (134.881776ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (189.591959ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.719956ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.112654ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (213.10587ms) +✔ bus start refuses with exit 3 without a notifier config (86.928226ms) +✔ bus start runs until bus stop; status reports it while it runs (656.036208ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.867061ms) +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (282.417559ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (575.753358ms) +✔ a runner that stops at once ends its launch with the runner's reason (272.021572ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (770.912983ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3659.382952ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (135.231912ms) +✔ zoned uses the IANA zone across DST (17.476571ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (132.004501ms) +✔ two blocking decisions get two DMs with different nonces (122.871191ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.292362ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (105.352783ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (109.51429ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (121.525089ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (118.943528ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (173.203968ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (138.607623ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (259.309314ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (157.557174ms) +✔ an inbox read failure is logged and the next poll retries (0.717398ms) +✔ no Discord id reaches the journal or the log (113.42138ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (20.122071ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (38.603439ms) +✔ the journal: a whole file that is one torn line truncates to empty (25.084268ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.048053ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.595689ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.751411ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.383996ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.480832ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.369812ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.429485ms) +✔ digest content stays within Discord's 2000 characters (0.264703ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.764422ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (394.904188ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (41.671676ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2160.080078ms) +✔ busExit and refuseInsideAgent (0.4448ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (219.683221ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1121.161112ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (108.204627ms) +✔ launches off and on go to the broker and change the business's launch state (117.554155ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (112.64814ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 75 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 5778.01493 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:105:1 +✖ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (282.417559ms) + AssertionError [ERR_ASSERTION]: The input did not match the regular expression /PM launch refused: instance-running/. Input: + + 'CliError: PM launch refused: capacity-full (opus sessions: 1 of 1)' + + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:137:3) + at async Test.run (node:internal/test_runner/test:1409:7) + at async startSubtestAfterBootstrap (node:internal/test_runner/harness:387:3) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: CliError: PM launch refused: capacity-full (opus sessions: 1 of 1) + at file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/src/launcher.mjs:295:15 + at process.processTicksAndRejections (node:internal/process/task_queues:104:5), + expected: /PM launch refused: instance-running/, + operator: 'rejects', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:175:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (575.753358ms) + AssertionError [ERR_ASSERTION]: coder + + actual - expected + + { + + error: 'capacity-full', + - error: 'instance-running', + ok: false + } + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:209:12) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: { ok: false, error: 'capacity-full' }, + expected: { ok: false, error: 'instance-running' }, + operator: 'deepStrictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Me-no-authorize-cli.txt b/agents/darkwing/work/s6-review/mut/Me-no-authorize-cli.txt new file mode 100644 index 00000000..3bdff40c --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Me-no-authorize-cli.txt @@ -0,0 +1,107 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (138.757292ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (181.365968ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (135.995008ms) +✔ decide prints a declining choice as declining (131.321963ms) +✔ an unknown outcome is reported once and never resent (121.033541ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (143.568257ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (133.382442ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (103.046809ms) +✔ every human command refuses inside an agent run before it touches the bus (101.665231ms) +✔ usage errors exit 4; no business and no host is a usage error (137.587049ms) +✔ agents and tasks print through the broker (166.273722ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (36.002104ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (46.154581ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (45.097471ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (45.020635ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (41.702747ms) +✔ a business without tracker.baseUrl gets no trackers entry (33.239399ms) +✔ an unknown business and a broken system config refuse with exit 3 (72.015913ms) +✔ empty views say so (1.166082ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.362349ms) +✔ tasks print the tracker fields the snapshot carries (0.2314ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (952.770524ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (235.027778ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (216.642435ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (263.246996ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (215.728182ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (226.22554ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (166.772705ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (166.509966ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (257.036469ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.899723ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.205762ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.565389ms) +✔ bus start refuses with exit 3 without a notifier config (87.058049ms) +✔ bus start runs until bus stop; status reports it while it runs (657.293355ms) +✔ bus-service.sh renders the unit and installs it into a given directory (25.693957ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1333.711945ms) +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (861.939206ms) +✔ a runner that stops at once ends its launch with the runner's reason (231.210908ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (770.734104ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3630.367619ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (122.084335ms) +✔ zoned uses the IANA zone across DST (18.085725ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (143.101836ms) +✔ two blocking decisions get two DMs with different nonces (178.859924ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.256723ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (138.729402ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (128.495778ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (123.094867ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (138.652432ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (183.760709ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (148.067127ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (156.1858ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (164.926157ms) +✔ an inbox read failure is logged and the next poll retries (0.636575ms) +✔ no Discord id reaches the journal or the log (194.997457ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (25.301543ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (84.875465ms) +✔ the journal: a whole file that is one torn line truncates to empty (36.937581ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (3.6726ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.589677ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.820138ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.315697ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.459806ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.363338ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.442958ms) +✔ digest content stays within Discord's 2000 characters (0.260548ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.734543ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (442.592712ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (36.742582ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2185.380382ms) +✔ busExit and refuseInsideAgent (0.412571ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (260.22186ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1168.161409ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (158.427277ms) +✔ launches off and on go to the broker and change the business's launch state (175.506781ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (137.45557ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 76 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 7039.056371 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:175:1 +✖ the PM launches a coder through its launch tool; the coder answers; refusals name their code (861.939206ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:217:10) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mf-no-exit2-wrapper-harness.txt b/agents/darkwing/work/s6-review/mut/Mf-no-exit2-wrapper-harness.txt new file mode 100644 index 00000000..3abdb0dd --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mf-no-exit2-wrapper-harness.txt @@ -0,0 +1,78 @@ +✔ sessionModel: agent vars win, then the system's execution settings (11.121928ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.728183ms) +✖ a claude-code bundle adds the wrapped gate hook and the MCP config (4.014424ms) +✔ a bundle is written once: an existing file refuses (1.996239ms) +✔ a path with a single quote can't go into the hook command (1.855058ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (130.812334ms) +✔ a missing or wrong policy, or a bad event, exits 2 (79.031735ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1094.514702ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (764.062618ms) +✔ claude: the hook alone blocks a path outside the workspace (589.82994ms) +✔ claude: a second turn resumes the first turn's session (736.915159ms) +✔ claude: a missing hook or MCP file refuses before claude starts (8.629216ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.876463ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.752765ms) +✔ file tool paths must resolve inside the workspace (0.962332ms) +✔ pi's own path normalisation can't be used to step out (0.907476ms) +✔ a symlink inside the workspace that points out is outside (0.808361ms) +✔ claude path fields per tool (0.860736ms) +✔ glob patterns stay inside the workspace (0.904636ms) +✔ a path that can't be checked is blocked (0.617929ms) +✔ initialize, ping and tools/list (42.391174ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (37.12388ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (31.935515ms) +✔ a missing argument is a usage error (33.805981ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (359.224518ms) +✔ pi: a missing extension refuses before any model call (8.186443ms) +✔ pi: an extension without its configuration fails pi's start (270.982753ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.711642ms) +✔ turnRequest names the sender, class, reply and decision (0.259584ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (269.324752ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (131.430937ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (464.429598ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1520.134523ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (187.836217ms) +✔ founder credentials stop before the claim (20) (208.81483ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (244.227937ms) +✔ the launch ending under a running session exits 22 (172.416748ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (253.75994ms) +✔ a broker that is down at the claim exits 23, not 21 (92.919627ms) +✔ no capability, or a malformed one, on stdin exits 2 (191.524811ms) +✔ the PM gets launch, its task verbs and the reads (7.204459ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.916872ms) +✔ launch only when the business's launch block names the instance as launcher (2.364338ms) +✔ an action outside the instance's authority has no tool (2.35004ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.053898ms) +ℹ tests 45 +ℹ suites 0 +ℹ pass 44 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10694.510151 + +✖ failing tests: + +test at packages/harness/tests/bundle.test.mjs:70:1 +✖ a claude-code bundle adds the wrapped gate hook and the MCP config (4.014424ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + actual - expected + + + "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json'" + - "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json' || exit 2" + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/bundle.test.mjs:78:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json'", + expected: "timeout -k 2 10 '/usr/bin/node' '/home/jwoltje/darkwing-scratch/r41/wt/packages/harness/src/claude-gate.mjs' '/home/jwoltje/darkwing-scratch/tmp/mosaic-harness-F6vGQM/bundle/policy.json' || exit 2", + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mg-no-founder-check-cli.txt b/agents/darkwing/work/s6-review/mut/Mg-no-founder-check-cli.txt new file mode 100644 index 00000000..2bccf75e --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mg-no-founder-check-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (130.720681ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (144.44443ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (123.061895ms) +✔ decide prints a declining choice as declining (244.593115ms) +✔ an unknown outcome is reported once and never resent (208.332046ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (147.08676ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (117.042615ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (119.363758ms) +✔ every human command refuses inside an agent run before it touches the bus (292.507856ms) +✔ usage errors exit 4; no business and no host is a usage error (138.115749ms) +✔ agents and tasks print through the broker (140.189507ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.09492ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (49.445915ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (39.563603ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.21625ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (30.793644ms) +✔ a business without tracker.baseUrl gets no trackers entry (36.275401ms) +✔ an unknown business and a broken system config refuse with exit 3 (56.275821ms) +✔ empty views say so (1.306748ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.509261ms) +✔ tasks print the tracker fields the snapshot carries (0.246762ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (911.121735ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (235.366643ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (369.841177ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (262.305547ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (236.663948ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (143.842312ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (190.154398ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (108.391088ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (206.604045ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.186881ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.80689ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (229.010884ms) +✔ bus start refuses with exit 3 without a notifier config (93.774875ms) +✔ bus start runs until bus stop; status reports it while it runs (668.258443ms) +✔ bus-service.sh renders the unit and installs it into a given directory (26.758168ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1323.665538ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (848.147276ms) +✔ a runner that stops at once ends its launch with the runner's reason (245.519218ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (647.385463ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3619.234805ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (104.246508ms) +✔ zoned uses the IANA zone across DST (24.971532ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (124.019745ms) +✔ two blocking decisions get two DMs with different nonces (127.853015ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.200872ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (116.476651ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (183.577337ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (261.864425ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (161.040869ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (179.477962ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (244.681013ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (245.72771ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (140.118308ms) +✔ an inbox read failure is logged and the next poll retries (0.686691ms) +✔ no Discord id reaches the journal or the log (138.080337ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (32.66773ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (49.31037ms) +✔ the journal: a whole file that is one torn line truncates to empty (32.685563ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.076912ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.597882ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (2.010512ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.388819ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.471387ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.381919ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.426331ms) +✔ digest content stays within Discord's 2000 characters (0.247464ms) +✔ runLoop never overlaps ticks and stops after the one in flight (111.169144ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (395.541301ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (38.675564ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2159.439268ms) +✔ busExit and refuseInsideAgent (0.44803ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (219.100073ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1125.936788ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (247.706439ms) +✔ launches off and on go to the broker and change the business's launch state (174.665863ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (131.345212ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6883.778277 diff --git a/agents/darkwing/work/s6-review/mut/Mg-no-founder-check-harness.txt b/agents/darkwing/work/s6-review/mut/Mg-no-founder-check-harness.txt new file mode 100644 index 00000000..ee1f2136 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mg-no-founder-check-harness.txt @@ -0,0 +1,65 @@ +✔ sessionModel: agent vars win, then the system's execution settings (14.067777ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.83597ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.203511ms) +✔ a bundle is written once: an existing file refuses (2.324752ms) +✔ a path with a single quote can't go into the hook command (2.432801ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (125.69164ms) +✔ a missing or wrong policy, or a bad event, exits 2 (87.248095ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1096.130839ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (822.887941ms) +✔ claude: the hook alone blocks a path outside the workspace (619.788359ms) +✔ claude: a second turn resumes the first turn's session (720.544276ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.807494ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (2.760889ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.858294ms) +✔ file tool paths must resolve inside the workspace (1.329669ms) +✔ pi's own path normalisation can't be used to step out (1.208492ms) +✔ a symlink inside the workspace that points out is outside (1.109705ms) +✔ claude path fields per tool (0.783786ms) +✔ glob patterns stay inside the workspace (1.009446ms) +✔ a path that can't be checked is blocked (0.632633ms) +✔ initialize, ping and tools/list (44.810011ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (34.700842ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (35.585978ms) +✔ a missing argument is a usage error (39.424495ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (377.597538ms) +✔ pi: a missing extension refuses before any model call (8.384513ms) +✔ pi: an extension without its configuration fails pi's start (294.902223ms) +✖ founderCheck: founder variables, then a needed service without a usable token (2.011371ms) +✔ turnRequest names the sender, class, reply and decision (0.351869ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (239.104388ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (134.155238ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (534.9943ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1584.330846ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (150.826229ms) +✔ the PM gets launch, its task verbs and the reads (7.17446ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.29576ms) +✔ launch only when the business's launch block names the instance as launcher (1.646169ms) +✔ an action outside the instance's authority has no tool (1.594448ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.633147ms) +ℹ tests 39 +ℹ suites 0 +ℹ pass 38 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 263663.299994 + +✖ failing tests: + +test at packages/harness/tests/runner.test.mjs:34:1 +✖ founderCheck: founder variables, then a needed service without a usable token (2.011371ms) + AssertionError [ERR_ASSERTION]: The "string" argument must be of type string. Received type object (null) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/runner.test.mjs:38:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.start (node:internal/test_runner/test:1262:17) + at startSubtestAfterBootstrap (node:internal/test_runner/harness:387:17) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: null, + expected: /GITEA_TOKEN, SSH_AUTH_SOCK/, + operator: 'match', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mh-no-close-sigkill-cli.txt b/agents/darkwing/work/s6-review/mut/Mh-no-close-sigkill-cli.txt new file mode 100644 index 00000000..47a8ee78 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mh-no-close-sigkill-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (125.138875ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (132.971762ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (117.374088ms) +✔ decide prints a declining choice as declining (108.459149ms) +✔ an unknown outcome is reported once and never resent (70.01985ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (94.875686ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (94.31331ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (50.391423ms) +✔ every human command refuses inside an agent run before it touches the bus (72.351222ms) +✔ usage errors exit 4; no business and no host is a usage error (83.26574ms) +✔ agents and tasks print through the broker (91.938302ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.1808ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (45.455893ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (34.445063ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.484263ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (31.217508ms) +✔ a business without tracker.baseUrl gets no trackers entry (32.020055ms) +✔ an unknown business and a broken system config refuse with exit 3 (59.190349ms) +✔ empty views say so (1.181996ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.453134ms) +✔ tasks print the tracker fields the snapshot carries (0.228609ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (911.109415ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (229.643058ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (307.116557ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (203.545959ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (165.693521ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (138.295568ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (180.904134ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (136.928153ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (167.809839ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (26.616224ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.697476ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (216.070056ms) +✔ bus start refuses with exit 3 without a notifier config (91.812758ms) +✔ bus start runs until bus stop; status reports it while it runs (661.600894ms) +✔ bus-service.sh renders the unit and installs it into a given directory (25.834051ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1429.993014ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (639.118083ms) +✔ a runner that stops at once ends its launch with the runner's reason (266.413468ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (635.92366ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3671.785556ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (133.951668ms) +✔ zoned uses the IANA zone across DST (21.770408ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (124.613973ms) +✔ two blocking decisions get two DMs with different nonces (127.527978ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.311431ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (112.494043ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (111.51636ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (80.954353ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (81.322222ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (153.159571ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (110.188277ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (115.163887ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (75.162747ms) +✔ an inbox read failure is logged and the next poll retries (0.6174ms) +✔ no Discord id reaches the journal or the log (68.352032ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (16.452539ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (16.506609ms) +✔ the journal: a whole file that is one torn line truncates to empty (10.938296ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (0.635215ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.620336ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.874754ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.330028ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.49249ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.414306ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.448777ms) +✔ digest content stays within Discord's 2000 characters (0.259028ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.716598ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (378.128826ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (41.478941ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2151.535751ms) +✔ busExit and refuseInsideAgent (0.435856ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (215.850821ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1133.958331ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (108.172473ms) +✔ launches off and on go to the broker and change the business's launch state (120.665848ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (88.354039ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6865.453741 diff --git a/agents/darkwing/work/s6-review/mut/Mi-recover-no-kill-cli.txt b/agents/darkwing/work/s6-review/mut/Mi-recover-no-kill-cli.txt new file mode 100644 index 00000000..184622c9 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mi-recover-no-kill-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (137.376158ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (121.751693ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (116.798959ms) +✔ decide prints a declining choice as declining (146.977151ms) +✔ an unknown outcome is reported once and never resent (119.334436ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (115.298648ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (125.351114ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (94.326095ms) +✔ every human command refuses inside an agent run before it touches the bus (119.08327ms) +✔ usage errors exit 4; no business and no host is a usage error (101.827088ms) +✔ agents and tasks print through the broker (95.910812ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.360098ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (48.404329ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.696218ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (33.178759ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (33.332481ms) +✔ a business without tracker.baseUrl gets no trackers entry (29.07756ms) +✔ an unknown business and a broken system config refuse with exit 3 (59.228455ms) +✔ empty views say so (0.801616ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.937632ms) +✔ tasks print the tracker fields the snapshot carries (0.142201ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (908.509723ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (259.291592ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (155.72522ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (216.453668ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (215.732438ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (133.198746ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (165.465891ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (98.613063ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (240.0965ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (24.289345ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.292309ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (225.9222ms) +✔ bus start refuses with exit 3 without a notifier config (88.973332ms) +✔ bus start runs until bus stop; status reports it while it runs (751.784886ms) +✔ bus-service.sh renders the unit and installs it into a given directory (49.720154ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1296.224536ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (656.132557ms) +✔ a runner that stops at once ends its launch with the runner's reason (217.485134ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (728.931825ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3650.060252ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (146.530135ms) +✔ zoned uses the IANA zone across DST (18.184875ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (145.69019ms) +✔ two blocking decisions get two DMs with different nonces (118.99032ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.176236ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (118.676676ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (133.002797ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (118.116242ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (110.099936ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (184.568054ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (144.754853ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (138.783123ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (89.036985ms) +✔ an inbox read failure is logged and the next poll retries (0.62497ms) +✔ no Discord id reaches the journal or the log (111.308913ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (31.479031ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (46.260958ms) +✔ the journal: a whole file that is one torn line truncates to empty (25.374439ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.988813ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.581731ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.786572ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.340661ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.466709ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.374083ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.410585ms) +✔ digest content stays within Discord's 2000 characters (0.25301ms) +✔ runLoop never overlaps ticks and stops after the one in flight (125.526296ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (399.433333ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (46.256649ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2150.449936ms) +✔ busExit and refuseInsideAgent (0.391979ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (236.876993ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1149.345411ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (98.170237ms) +✔ launches off and on go to the broker and change the business's launch state (98.79469ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (103.804047ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6770.811975 diff --git a/agents/darkwing/work/s6-review/mut/Mj-no-stdin-cap-harness.txt b/agents/darkwing/work/s6-review/mut/Mj-no-stdin-cap-harness.txt new file mode 100644 index 00000000..9e2796a7 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mj-no-stdin-cap-harness.txt @@ -0,0 +1,53 @@ +✔ sessionModel: agent vars win, then the system's execution settings (14.050443ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.120541ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (4.081354ms) +✔ a bundle is written once: an existing file refuses (2.996025ms) +✔ a path with a single quote can't go into the hook command (2.951571ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (150.595498ms) +✔ a missing or wrong policy, or a bad event, exits 2 (124.742454ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1126.482389ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (909.741969ms) +✔ claude: the hook alone blocks a path outside the workspace (516.900249ms) +✔ claude: a second turn resumes the first turn's session (763.872943ms) +✔ claude: a missing hook or MCP file refuses before claude starts (8.211736ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.953963ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.997202ms) +✔ file tool paths must resolve inside the workspace (1.423588ms) +✔ pi's own path normalisation can't be used to step out (1.401389ms) +✔ a symlink inside the workspace that points out is outside (1.139946ms) +✔ claude path fields per tool (0.791831ms) +✔ glob patterns stay inside the workspace (0.945422ms) +✔ a path that can't be checked is blocked (0.646247ms) +✔ initialize, ping and tools/list (51.210531ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (41.206446ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (38.168479ms) +✔ a missing argument is a usage error (39.579956ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (496.281275ms) +✔ pi: a missing extension refuses before any model call (9.103805ms) +✔ pi: an extension without its configuration fails pi's start (301.173776ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.727546ms) +✔ turnRequest names the sender, class, reply and decision (0.275096ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (264.146564ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (143.576835ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (536.40783ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1382.766716ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (214.519665ms) +✔ founder credentials stop before the claim (20) (304.717878ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (320.859843ms) +✔ the launch ending under a running session exits 22 (207.759761ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (302.795855ms) +✔ a broker that is down at the claim exits 23, not 21 (148.704368ms) +✔ no capability, or a malformed one, on stdin exits 2 (210.336697ms) +✔ the PM gets launch, its task verbs and the reads (9.712494ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (3.23337ms) +✔ launch only when the business's launch block names the instance as launcher (2.566048ms) +✔ an action outside the instance's authority has no tool (3.156249ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (9.65517ms) +ℹ tests 45 +ℹ suites 0 +ℹ pass 45 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10678.399832 diff --git a/agents/darkwing/work/s6-review/mut/Mk-launch-line-max-cli.txt b/agents/darkwing/work/s6-review/mut/Mk-launch-line-max-cli.txt new file mode 100644 index 00000000..8fa61f27 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mk-launch-line-max-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (132.317557ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (134.090645ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (132.53407ms) +✔ decide prints a declining choice as declining (127.229793ms) +✔ an unknown outcome is reported once and never resent (97.531419ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (98.822586ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (113.290637ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (87.563392ms) +✔ every human command refuses inside an agent run before it touches the bus (89.267983ms) +✔ usage errors exit 4; no business and no host is a usage error (90.872874ms) +✔ agents and tasks print through the broker (99.389871ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.229007ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (65.988943ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (40.683718ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (45.804031ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (35.221951ms) +✔ a business without tracker.baseUrl gets no trackers entry (38.611374ms) +✔ an unknown business and a broken system config refuse with exit 3 (64.054106ms) +✔ empty views say so (1.179352ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.468164ms) +✔ tasks print the tracker fields the snapshot carries (0.245647ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (956.305599ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (235.464033ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (175.440963ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (221.774919ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (183.250261ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (146.195016ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (195.557326ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (228.386699ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (193.898946ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (28.035345ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.365135ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (215.714196ms) +✔ bus start refuses with exit 3 without a notifier config (97.320284ms) +✔ bus start runs until bus stop; status reports it while it runs (733.744653ms) +✔ bus-service.sh renders the unit and installs it into a given directory (30.592503ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1316.305561ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (735.280709ms) +✔ a runner that stops at once ends its launch with the runner's reason (282.231928ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (707.643793ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3693.537166ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (139.911386ms) +✔ zoned uses the IANA zone across DST (29.966188ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (134.629552ms) +✔ two blocking decisions get two DMs with different nonces (130.426366ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.212076ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (143.19933ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (118.110025ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (95.961197ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (107.204382ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (178.413565ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (126.22777ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (124.406448ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (92.254829ms) +✔ an inbox read failure is logged and the next poll retries (0.653785ms) +✔ no Discord id reaches the journal or the log (111.290157ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (27.715695ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (93.436044ms) +✔ the journal: a whole file that is one torn line truncates to empty (35.121818ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.199765ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.630155ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.853005ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.494157ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.50078ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.37415ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.425391ms) +✔ digest content stays within Discord's 2000 characters (0.250758ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.832081ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (419.495349ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (50.573663ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2173.090633ms) +✔ busExit and refuseInsideAgent (0.401847ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (224.32061ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1170.213512ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (116.532473ms) +✔ launches off and on go to the broker and change the business's launch state (97.462875ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (83.439542ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6998.23078 diff --git a/agents/darkwing/work/s6-review/mut/Ml-gate-pattern-harness.txt b/agents/darkwing/work/s6-review/mut/Ml-gate-pattern-harness.txt new file mode 100644 index 00000000..386c0908 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Ml-gate-pattern-harness.txt @@ -0,0 +1,77 @@ +✔ sessionModel: agent vars win, then the system's execution settings (11.615827ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (4.608262ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (3.093679ms) +✔ a bundle is written once: an existing file refuses (2.743247ms) +✔ a path with a single quote can't go into the hook command (1.960427ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (129.070397ms) +✔ a missing or wrong policy, or a bad event, exits 2 (95.751067ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1098.628901ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (833.557723ms) +✔ claude: the hook alone blocks a path outside the workspace (501.161697ms) +✔ claude: a second turn resumes the first turn's session (799.047884ms) +✔ claude: a missing hook or MCP file refuses before claude starts (7.620084ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.560331ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (1.195475ms) +✔ file tool paths must resolve inside the workspace (1.373421ms) +✔ pi's own path normalisation can't be used to step out (1.395868ms) +✔ a symlink inside the workspace that points out is outside (1.15816ms) +✔ claude path fields per tool (1.025541ms) +✖ glob patterns stay inside the workspace (2.186725ms) +✔ a path that can't be checked is blocked (0.852835ms) +✔ initialize, ping and tools/list (48.515972ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (31.715873ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (30.592543ms) +✔ a missing argument is a usage error (32.791709ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (403.4094ms) +✔ pi: a missing extension refuses before any model call (8.601035ms) +✔ pi: an extension without its configuration fails pi's start (303.980444ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.638328ms) +✔ turnRequest names the sender, class, reply and decision (0.265375ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (353.187333ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (165.33647ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (717.750899ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1432.11811ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (276.246822ms) +✔ founder credentials stop before the claim (20) (215.437887ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (382.043681ms) +✔ the launch ending under a running session exits 22 (173.218595ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (348.026841ms) +✔ a broker that is down at the claim exits 23, not 21 (243.327611ms) +✔ no capability, or a malformed one, on stdin exits 2 (273.537344ms) +✔ the PM gets launch, its task verbs and the reads (9.380584ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (2.545292ms) +✔ launch only when the business's launch block names the instance as launcher (1.844344ms) +✔ an action outside the instance's authority has no tool (2.256037ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (7.218466ms) +ℹ tests 45 +ℹ suites 0 +ℹ pass 44 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 11069.564681 + +✖ failing tests: + +test at packages/harness/tests/gate.test.mjs:92:1 +✖ glob patterns stay inside the workspace (2.186725ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + true !== false + + at blocked (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/gate.test.mjs:19:10) + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/harness/tests/gate.test.mjs:98:5) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: true, + expected: false, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mm-no-revoke-bus.txt b/agents/darkwing/work/s6-review/mut/Mm-no-revoke-bus.txt new file mode 100644 index 00000000..c2c3e778 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mm-no-revoke-bus.txt @@ -0,0 +1,102 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (163.89862ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (275.331186ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (282.996644ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (195.063187ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (208.388066ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (157.831971ms) +✔ task action subjects and linked decision trail are complete and ordered (156.524887ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (97.415614ms) +✔ business isolation includes inherited object names and cross-business message references (159.041055ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (227.530616ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (94.890129ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (180.592353ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (106.579275ms) +✔ both arbiters require human resolution when their cross-role route is themselves (189.732423ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (1.899132ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (5.641078ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (3.241221ms) +✔ expiry refuses use and env references never become client data (0.737712ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.517077ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.297143ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (122.779602ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (127.653631ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (155.030205ms) +✔ endLaunch leaves a claim another run took alone (193.274277ms) +✔ refuse records action.refused against the caller with the code only (145.282316ms) +✖ launches off refuses role.launch with launch-revoked until launches on (183.043385ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (266.177254ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (3.887495ms) +✔ process reader gets own kernel identity without exposing environment values (0.762853ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (1.082879ms) +✔ real pid 1 remains inspectable when its environment is protected (0.430481ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (196.161745ms) +✔ missing and foreign-business citations refuse and roll back message and grant (183.836168ms) +✔ cross-role sends still need a matching resolved decision and consume it once (274.522528ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (190.003233ms) +✔ startup token refusal returns safe code without value or partial listening broker (40.621827ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (184.028601ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (188.935171ms) +✔ trusted host registers later launches; socket clients never have a registration verb (175.835554ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (39.471345ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (178.989186ms) +✔ v3b prototype refusals, views and append-only mutations (1120.730725ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (251.703532ms) +✔ another run cannot consume an approval; a failed check leaves it usable (238.891458ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (180.906441ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (351.290843ms) +✔ class drift gated to cross-role refuses before consumption (218.014846ms) +✔ class drift cross-role to gated refuses before consumption (203.570384ms) +✔ class drift gated to within-role refuses before consumption (183.6351ms) +✔ class drift cross-role to within-role refuses before consumption (196.755743ms) +✔ class drift within-role to gated refuses before consumption (134.864109ms) +✔ class drift within-role to cross-role refuses before consumption (160.50952ms) +✔ message.send consumes approval and prevents a later send or authorize (173.816965ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (195.598037ms) +✔ creates private WAL store and excludes a second writer until explicit close (123.44453ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (181.784127ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (215.291682ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (169.453598ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (119.878794ms) +✔ async transactions refuse before invoking their function (107.045531ms) +✔ recordTask keeps sync reads and a role write apart (179.866702ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (109.539813ms) +✔ a bad entry refuses the whole record (118.580361ms) +✔ taskView reads the projection for one business (143.124952ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (262.005489ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (413.469594ms) +✔ without an adapter the server refuses every task verb (173.691791ms) +✔ the runtime refuses an invalid adapter and closes a valid one (180.508213ms) +✔ the process loads the S3 adapter from plain-data trackers (232.75675ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (146.93786ms) +✔ two wire claims serialize; a lost reply never automatically retries (182.474377ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (113.923911ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.855361ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (162.548925ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 73 +ℹ fail 1 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 2579.991609 + +✖ failing tests: + +test at packages/bus/tests/end-launch.test.mjs:121:1 +✖ launches off refuses role.launch with launch-revoked until launches on (183.043385ms) + AssertionError [ERR_ASSERTION]: Missing expected exception. + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/bus/tests/end-launch.test.mjs:128:10) + at Test.runInAsyncScope (node:async_hooks:226:14) + at Test.run (node:internal/test_runner/test:1402:25) + at Test.processPendingSubtests (node:internal/test_runner/test:974:18) + at Test.postRun (node:internal/test_runner/test:1542:19) + at Test.run (node:internal/test_runner/test:1467:12) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: false, + code: 'ERR_ASSERTION', + actual: undefined, + expected: { code: 'launch-revoked' }, + operator: 'throws', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/Mm-no-revoke-cli.txt b/agents/darkwing/work/s6-review/mut/Mm-no-revoke-cli.txt new file mode 100644 index 00000000..892de479 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mm-no-revoke-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (109.862086ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (129.587767ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (92.553872ms) +✔ decide prints a declining choice as declining (105.185722ms) +✔ an unknown outcome is reported once and never resent (74.403125ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (75.320493ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (97.310508ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (80.781684ms) +✔ every human command refuses inside an agent run before it touches the bus (84.435133ms) +✔ usage errors exit 4; no business and no host is a usage error (80.337038ms) +✔ agents and tasks print through the broker (75.861924ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (1.835921ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (54.390762ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (37.014457ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (34.755279ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (35.304414ms) +✔ a business without tracker.baseUrl gets no trackers entry (27.453313ms) +✔ an unknown business and a broken system config refuse with exit 3 (73.51488ms) +✔ empty views say so (1.16644ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (1.434549ms) +✔ tasks print the tracker fields the snapshot carries (0.234705ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (886.74793ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (207.220461ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (125.163127ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (193.933367ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (204.121221ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (121.372003ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (175.827333ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (143.726724ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (167.137229ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (22.294447ms) +✔ bus stop refuses to signal a live pid that is not a bus host (202.775467ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (210.492927ms) +✔ bus start refuses with exit 3 without a notifier config (90.517024ms) +✔ bus start runs until bus stop; status reports it while it runs (670.225504ms) +✔ bus-service.sh renders the unit and installs it into a given directory (30.367746ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1264.763565ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (659.045675ms) +✔ a runner that stops at once ends its launch with the runner's reason (207.163503ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (617.947072ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3574.705922ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (110.311682ms) +✔ zoned uses the IANA zone across DST (16.358337ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (116.467328ms) +✔ two blocking decisions get two DMs with different nonces (124.106016ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.199104ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (95.468473ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (99.017605ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (75.330303ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (81.013867ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (159.689637ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (116.245282ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (96.001449ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (75.69486ms) +✔ an inbox read failure is logged and the next poll retries (0.661409ms) +✔ no Discord id reaches the journal or the log (59.545832ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (16.205132ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (20.246556ms) +✔ the journal: a whole file that is one torn line truncates to empty (21.610334ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.917923ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.705192ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.917939ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.529388ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.533089ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.461178ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.564232ms) +✔ digest content stays within Discord's 2000 characters (0.288698ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.579344ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (358.584995ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (41.579301ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2145.123853ms) +✔ busExit and refuseInsideAgent (0.399961ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (198.808191ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1102.511621ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (118.315699ms) +✔ launches off and on go to the broker and change the business's launch state (127.484544ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (83.416693ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6523.228172 diff --git a/agents/darkwing/work/s6-review/mut/Mn-recover-no-end-cli.txt b/agents/darkwing/work/s6-review/mut/Mn-recover-no-end-cli.txt new file mode 100644 index 00000000..100d30fc --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/Mn-recover-no-end-cli.txt @@ -0,0 +1,125 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (122.875541ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (130.94748ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (111.565521ms) +✔ decide prints a declining choice as declining (100.598618ms) +✔ an unknown outcome is reported once and never resent (81.113719ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (83.224029ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (84.988204ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (70.293143ms) +✔ every human command refuses inside an agent run before it touches the bus (81.875595ms) +✔ usage errors exit 4; no business and no host is a usage error (95.688893ms) +✔ agents and tasks print through the broker (87.007356ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.678919ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (42.09848ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (31.644567ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (32.461392ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (39.422053ms) +✔ a business without tracker.baseUrl gets no trackers entry (31.112022ms) +✔ an unknown business and a broken system config refuse with exit 3 (56.891472ms) +✔ empty views say so (0.786498ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.964626ms) +✔ tasks print the tracker fields the snapshot carries (0.156842ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (924.290753ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (224.387012ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (131.886124ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (200.781729ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (202.118243ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (133.013545ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (183.058103ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (111.514305ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (204.993453ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (28.041817ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.159141ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (229.44384ms) +✔ bus start refuses with exit 3 without a notifier config (105.05832ms) +✔ bus start runs until bus stop; status reports it while it runs (658.38108ms) +✔ bus-service.sh renders the unit and installs it into a given directory (27.340512ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (1284.454067ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (660.213353ms) +✔ a runner that stops at once ends its launch with the runner's reason (228.790342ms) +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (471.399509ms) +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3406.092202ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (137.539713ms) +✔ zoned uses the IANA zone across DST (17.45739ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (131.994345ms) +✔ two blocking decisions get two DMs with different nonces (124.836571ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.179365ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (97.474367ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (99.938485ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (91.272173ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (85.503667ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (145.863192ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (119.177833ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (124.013706ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (84.553923ms) +✔ an inbox read failure is logged and the next poll retries (0.616339ms) +✔ no Discord id reaches the journal or the log (72.213113ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (12.627127ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (15.576535ms) +✔ the journal: a whole file that is one torn line truncates to empty (17.63237ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (1.965226ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.641952ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.809363ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.310274ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.45827ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.364092ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.445383ms) +✔ digest content stays within Discord's 2000 characters (0.25583ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.775491ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (378.369694ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (35.260579ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2151.84806ms) +✔ busExit and refuseInsideAgent (0.392495ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (229.052611ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1132.31844ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (120.754589ms) +✔ launches off and on go to the broker and change the business's launch state (115.404453ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (77.278529ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 75 +ℹ fail 2 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6277.504144 + +✖ failing tests: + +test at packages/cli/tests/launcher.test.mjs:252:3 +✖ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (471.399509ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 0 !== 1 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:281:12) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 0, + expected: 1, + operator: 'strictEqual', + diff: 'simple' + } + +test at packages/cli/tests/launcher.test.mjs:252:3 +✖ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3406.092202ms) + AssertionError [ERR_ASSERTION]: Expected values to be strictly equal: + + 0 !== 1 + + at TestContext. (file:///home/jwoltje/darkwing-scratch/r41/wt/packages/cli/tests/launcher.test.mjs:281:12) + at process.processTicksAndRejections (node:internal/process/task_queues:104:5) + at async Test.run (node:internal/test_runner/test:1409:7) + at async Test.processPendingSubtests (node:internal/test_runner/test:974:7) { + generatedMessage: true, + code: 'ERR_ASSERTION', + actual: 0, + expected: 1, + operator: 'strictEqual', + diff: 'simple' + } diff --git a/agents/darkwing/work/s6-review/mut/all.sh b/agents/darkwing/work/s6-review/mut/all.sh new file mode 100755 index 00000000..682d1836 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/all.sh @@ -0,0 +1,18 @@ +#!/bin/bash +R=~/darkwing-scratch/r41/mut +rm -f $R/summary.txt +$R/run.sh Ma-late-signals harness cli +$R/run.sh Mb-runs-set-early bus cli +$R/run.sh Mc-no-run-ended bus cli +$R/run.sh Md-no-instance-running cli +$R/run.sh Me-no-authorize cli +$R/run.sh Mf-no-exit2-wrapper harness +$R/run.sh Mg-no-founder-check harness cli +$R/run.sh Mh-no-close-sigkill cli +$R/run.sh Mi-recover-no-kill cli +$R/run.sh Mj-no-stdin-cap harness +$R/run.sh Mk-launch-line-max cli +$R/run.sh Ml-gate-pattern harness +$R/run.sh Mm-no-revoke bus cli +$R/run.sh Mn-recover-no-end cli +echo DONE >> $R/summary.txt diff --git a/agents/darkwing/work/s6-review/mut/mutate.py b/agents/darkwing/work/s6-review/mut/mutate.py new file mode 100644 index 00000000..9114df15 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/mutate.py @@ -0,0 +1,40 @@ +# mutate.py : apply one named mutant (exact text, must match once). +import sys +M = { + "Ma-late-signals": ("packages/harness/src/runner.mjs", + ' process.on("SIGTERM", stop);\n process.on("SIGINT", stop);\n\n let session, cap;', + ' let session, cap;', ), + "Mb-runs-set-early": ("packages/bus/src/broker.mjs", + " const session = { ...record, address: record.address ?? null, human: false };\n", + " const session = { ...record, address: record.address ?? null, human: false };\n this.#runs.set(key, session);\n"), + "Mc-no-run-ended": ("packages/bus/src/broker.mjs", + " fail('run-ended');\n } else", " void 0;\n } else"), + "Md-no-instance-running": ("packages/cli/src/launcher.mjs", + ' if (registry.running(b.id, instance)) throw new Refusal("instance-running");\n', ""), + "Me-no-authorize": ("packages/cli/src/launcher.mjs", + ' await host.op({ op: "authorizeLaunch", cap, instance });\n', ""), + "Mf-no-exit2-wrapper": ("packages/harness/src/bundle.mjs", + "${quote(files.policy)} || exit 2`", "${quote(files.policy)}`"), + "Mg-no-founder-check": ("packages/harness/src/runner.mjs", + " const found = FOUNDER_ENV.filter((k) => env[k] !== undefined);", " const found = [];"), + "Mh-no-close-sigkill": ("packages/cli/src/launcher.mjs", + ' if (startTimeOf(s.pid) === s.startTime) process.kill(s.pid, "SIGKILL");', " void 0;"), + "Mi-recover-no-kill": ("packages/cli/src/launcher.mjs", + ' process.kill(s.pid, "SIGKILL");', " void 0;"), + "Mj-no-stdin-cap": ("packages/harness/src/runner.mjs", + " if (input.length > 4096) reject", " if (false) reject"), + "Mk-launch-line-max": ("packages/cli/src/launcher.mjs", + " if (buf.length > LINE_MAX) return reply", " if (false) return reply"), + "Ml-gate-pattern": ("packages/harness/src/gate.mjs", + "/(^|[/\\\\])\\.\\.([/\\\\]|$)/.test(pattern)", "false"), + "Mm-no-revoke": ("packages/bus/src/broker.mjs", + " fail('launch-revoked');", " void 0;"), + "Mn-recover-no-end": ("packages/cli/src/launcher.mjs", + ' await endRun(s.run, "host-lost", null);\n', ""), +} +f, old, new = M[sys.argv[1]] +s = open(f).read() +n = s.count(old) +if n != 1: sys.exit(f"{sys.argv[1]}: {n} matches in {f}") +open(f, "w").write(s.replace(old, new)) +print(f) diff --git a/agents/darkwing/work/s6-review/mut/parse.sh b/agents/darkwing/work/s6-review/mut/parse.sh new file mode 100755 index 00000000..18a2e000 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/parse.sh @@ -0,0 +1,8 @@ +#!/bin/bash +# parse.sh: pass/fail and failing test names per mutant output (spec reporter). +cd ~/darkwing-scratch/r41/mut +for f in M*-*.txt; do + p=$(grep -E '^ℹ pass' "$f" | awk '{print $3}'); x=$(grep -E '^ℹ fail' "$f" | awk '{print $3}') + echo "$f: pass $p fail $x" + [ "$x" != 0 ] && awk '/^✖ failing tests:/{on=1;next} on && /^✖ /{sub(/^✖ /," "); sub(/ \([0-9.]+ms\)$/,""); print}' "$f" | sort -u +done diff --git a/agents/darkwing/work/s6-review/mut/run.sh b/agents/darkwing/work/s6-review/mut/run.sh new file mode 100755 index 00000000..0469efbc --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/run.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# run.sh ...: mutate, run each package's node suite, restore. +set -u +cd ~/darkwing-scratch/r41/wt +export TMPDIR=~/darkwing-scratch/tmp DOCKER_HOST=unix:///nonexistent.sock +m=$1; shift +f=$(python3 ../mut/mutate.py "$m") || { echo "$m: no match" | tee -a ../mut/summary.txt; exit 1; } +line="$m ($f):" +for p in "$@"; do + out=../mut/$m-$p.txt + node --test "packages/$p/tests/*.test.mjs" > "$out" 2>&1 + pass=$(grep -E '^# pass' "$out" | awk '{print $3}'); fail=$(grep -E '^# fail' "$out" | awk '{print $3}') + line="$line $p pass $pass fail $fail;" + [ "$fail" != 0 ] && line="$line [$(grep -E '^not ok' "$out" | sed 's/^not ok [0-9]* - //' | head -3 | paste -sd'|')]" +done +git checkout -- "$f" +echo "$line" | tee -a ../mut/summary.txt diff --git a/agents/darkwing/work/s6-review/mut/summary.txt b/agents/darkwing/work/s6-review/mut/summary.txt new file mode 100644 index 00000000..96539a90 --- /dev/null +++ b/agents/darkwing/work/s6-review/mut/summary.txt @@ -0,0 +1,15 @@ +Ma-late-signals (packages/harness/src/runner.mjs): harness pass fail ; [] cli pass fail ; [] +Mb-runs-set-early (packages/bus/src/broker.mjs): bus pass fail ; [] cli pass fail ; [] +Mc-no-run-ended (packages/bus/src/broker.mjs): bus pass fail ; [] cli pass fail ; [] +Md-no-instance-running (packages/cli/src/launcher.mjs): cli pass fail ; [] +Me-no-authorize (packages/cli/src/launcher.mjs): cli pass fail ; [] +Mf-no-exit2-wrapper (packages/harness/src/bundle.mjs): harness pass fail ; [] +Mg-no-founder-check (packages/harness/src/runner.mjs): harness pass fail ; [] cli pass fail ; [] +Mh-no-close-sigkill (packages/cli/src/launcher.mjs): cli pass fail ; [] +Mi-recover-no-kill (packages/cli/src/launcher.mjs): cli pass fail ; [] +Mj-no-stdin-cap (packages/harness/src/runner.mjs): harness pass fail ; [] +Mk-launch-line-max (packages/cli/src/launcher.mjs): cli pass fail ; [] +Ml-gate-pattern (packages/harness/src/gate.mjs): harness pass fail ; [] +Mm-no-revoke (packages/bus/src/broker.mjs): bus pass fail ; [] cli pass fail ; [] +Mn-recover-no-end (packages/cli/src/launcher.mjs): cli pass fail ; [] +DONE diff --git a/agents/darkwing/work/s6-review/out/node-bus.txt b/agents/darkwing/work/s6-review/out/node-bus.txt new file mode 100644 index 00000000..9f79dab4 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/node-bus.txt @@ -0,0 +1,82 @@ +✔ launch identity is stamped, payload identity is refused and stale holder cannot send (230.298902ms) +✔ decision classes route from policy; gated resolution is human-only, choice and target must match (495.596263ms) +✔ claim exclusion, holder release, gated revoke and rerouting to a new holder are atomic (295.350922ms) +✔ launch events require a human CLI capability; generic emit cannot forge authority events (209.247055ms) +✔ within-role decisions close atomically and invalid options or blocking omissions refuse (201.016991ms) +✔ observer capabilities read human inbox but cannot mutate or forge launch identity (146.820046ms) +✔ task action subjects and linked decision trail are complete and ordered (186.279171ms) +✔ launch binding is durable and reconnecting requires the identical trusted record (124.058295ms) +✔ business isolation includes inherited object names and cross-business message references (201.009327ms) +✔ authority never transfers between action, run, target, unresolved or replaced role holder (325.185099ms) +✔ task projection uses schema current view, skipping earlier and equal-start polls (123.998135ms) +✔ revocation permanently bars the old run from reclaiming first, including after broker restart (220.538954ms) +✔ empty message references refuse before storage; refusal-evidence failure stays a typed error (170.419881ms) +✔ both arbiters require human resolution when their cross-role route is themselves (217.901165ms) +✔ S1 adapter takes resolved limits and refs, rejects mismatched instance, never mutates input (2.544455ms) +✔ only validated broker references load; returned data and exceptions cannot expose a known token (4.455749ms) +✔ bad file modes, symlinks, repository/data paths, malformed tokens and missing dates refuse (6.670204ms) +✔ expiry refuses use and env references never become client data (0.783979ms) +✔ S1 parsed service refs work, service mismatch refuses, Gitea rotation due is a warning state (1.944049ms) +✔ opaque tokens shorter than 16 characters refuse before use (0.278578ms) +✔ endLaunch writes session.ended, releases the run claim and kills its capabilities (212.9763ms) +✔ endLaunch refuses an unknown run, a second end and a rebind of the ended run (283.135446ms) +✔ a restarted broker refuses to rebind an ended run; a refused rebind leaves the run unbound (239.880561ms) +✔ endLaunch leaves a claim another run took alone (189.377591ms) +✔ refuse records action.refused against the caller with the code only (148.68694ms) +✔ launches off refuses role.launch with launch-revoked until launches on (221.873558ms) +✔ broker process: launch ops authorize role.launch, record refusals and end runs (234.098929ms) +✔ human proof binds CLI entry, process start and nonce; agents and incomplete ancestry refuse (1.844295ms) +✔ process reader gets own kernel identity without exposing environment values (1.229094ms) +✔ EACCES ancestor environments skip only markers; commands and registered launches still refuse (0.718827ms) +✔ real pid 1 remains inspectable when its environment is protected (0.280884ms) +✔ within-role sends cite an open gated launch decision without spending it or naming it in grants (336.734778ms) +✔ missing and foreign-business citations refuse and roll back message and grant (356.763341ms) +✔ cross-role sends still need a matching resolved decision and consume it once (272.056987ms) +✔ broker process binds trusted launches, offers reader capabilities, refuses human mutation, closes cleanly (271.449396ms) +✔ startup token refusal returns safe code without value or partial listening broker (38.528737ms) +✔ loaded fixture token is absent from socket replies and SQLite, including refusal evidence (319.293579ms) +✔ killed broker leaves an explicit stale lock; another process cannot silently reclaim it (181.982551ms) +✔ trusted host registers later launches; socket clients never have a registration verb (178.757199ms) +✔ runtime excludes declared project roots even when host supplies no repoRoots (35.654181ms) +✔ a refused launch binding leaves the broker and existing capabilities alive; bad protocol stops it (188.093502ms) +✔ v3b prototype refusals, views and append-only mutations (1445.847272ms) +✔ gated approval authorizes once, survives store reopen, and fresh approval works (478.15649ms) +✔ another run cannot consume an approval; a failed check leaves it usable (310.533789ms) +✔ two scheduled callers have exactly one grant and one consumed refusal (193.236882ms) +✔ failed commit rolls consumption back; cross-role consumes and within-role stays reusable (384.018937ms) +✔ class drift gated to cross-role refuses before consumption (200.035157ms) +✔ class drift cross-role to gated refuses before consumption (257.75596ms) +✔ class drift gated to within-role refuses before consumption (192.144254ms) +✔ class drift cross-role to within-role refuses before consumption (267.10218ms) +✔ class drift within-role to gated refuses before consumption (197.394414ms) +✔ class drift within-role to cross-role refuses before consumption (210.140443ms) +✔ message.send consumes approval and prevents a later send or authorize (243.201049ms) +✔ role.revoke consumes approval and prevents a later revoke or authorize (221.001388ms) +✔ creates private WAL store and excludes a second writer until explicit close (190.861398ms) +✔ rollback is atomic and schema metadata is checked against trusted DDL, not just itself (370.583805ms) +✔ existing empty database and symlink runtime directory refuse, never initialize over damage (240.634985ms) +✔ crash during a transaction recovers no partial event after explicit fixture-only lock removal (172.61397ms) +✔ writer refuses mixed at/read_at forms atomically, even through trusted SQL helpers (114.410444ms) +✔ async transactions refuse before invoking their function (124.463303ms) +✔ recordTask keeps sync reads and a role write apart (290.376296ms) +✔ read_at must be one canonical UTC format, so the projection compares strings safely (254.023515ms) +✔ a bad entry refuses the whole record (152.38741ms) +✔ taskView reads the projection for one business (132.084924ms) +✔ requestTask hands only a holder and a task verb to the handler, and records refusals (283.971235ms) +✔ the server sends task verbs to the adapter with its own timeout; other verbs stay synchronous (431.972902ms) +✔ without an adapter the server refuses every task verb (229.494873ms) +✔ the runtime refuses an invalid adapter and closes a valid one (221.815058ms) +✔ the process loads the S3 adapter from plain-data trackers (297.187861ms) +✔ socket capability stamps launch identity; shared views use wire, no SQL client (247.764449ms) +✔ two wire claims serialize; a lost reply never automatically retries (371.117662ms) +✔ malformed, oversized and identity-forging envelopes refuse without echoing input (125.675324ms) +✔ client preserves UTF-8 when a response divides a multibyte character (11.780579ms) +✔ committed mutation followed by dropped reply reports unknown and is never retried (155.539343ms) +ℹ tests 74 +ℹ suites 0 +ℹ pass 74 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 3244.829626 diff --git a/agents/darkwing/work/s6-review/out/node-business.txt b/agents/darkwing/work/s6-review/out/node-business.txt new file mode 100644 index 00000000..77d507e9 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/node-business.txt @@ -0,0 +1,68 @@ +✔ config directory and file path follow MOSAIC_CONFIG (1.46993ms) +✔ the fixture business validates and comes back frozen (3.65864ms) +✔ two instances may share a definition (1.05404ms) +✔ top-level refusals (3.346619ms) +✔ arbiters and projects (5.222003ms) +✔ role instances (2.840985ms) +✔ Vikunja bots (5.529147ms) +✔ a role without Vikunja takes no tracker block (1.73776ms) +✔ credential references match the definition's services (2.08883ms) +✔ launch (6.709177ms) +✔ loadBusiness: file checks (1.526452ms) +✔ loadBusiness: not a regular file (37.652055ms) +✔ loading writes nothing (1.309657ms) +✔ names that are Object.prototype properties don't count as declared (2.476127ms) +✔ the shipped example refuses as written and validates once filled in (0.52605ms) +✔ usage errors exit 4 (283.72903ms) +✔ validate: a good business exits 0 and prints instance digests (67.029589ms) +✔ validate: project files (305.887244ms) +✔ validate: missing files and a broken system config (248.794647ms) +✔ validate: credential reference problems exit 2 and name each one (62.924086ms) +✔ validate: a token file inside the repository is refused (65.917526ms) +✔ validate: role definitions come from MOSAIC_ROLES_DIR (186.048198ms) +✔ resolve: prints one instance's record (193.663903ms) +✔ resolve: refusals (386.422626ms) +✔ parse: exactly one of file or env, plus the service's date (1.918165ms) +✔ check: a good file has no problems (0.606329ms) +✔ check never opens the file: a write-only token passes (0.253188ms) +✔ check: file problems (0.698464ms) +✔ check: token files can't live in the repository or dataRoot, even through a linked directory (0.647238ms) +✔ check: dates and environment references (0.304771ms) +✔ path and load (1.699579ms) +✔ refusals (0.864022ms) +✔ systemVars flattens the validated config (1.491298ms) +✔ precedence: system, business, project, project role, agent (3.709113ms) +✔ limits narrow the definition and never widen it (1.852486ms) +✔ role.launch stays within-role only for the instance the launch block names (3.761048ms) +✔ limits.authority without role.launch leaves the launcher with no launch block (1.43637ms) +✔ limits.authority narrows cross-role actions too (0.814065ms) +✔ classify (0.916153ms) +✔ the record carries what the broker and launcher need (0.797372ms) +✔ digest: key order doesn't matter, any value change does (4.852509ms) +✔ refusals (1.887646ms) +✔ the four shipped version 2 roles load (2.349244ms) +✔ shipped role scopes match addendum B section 2 and the SR runbook (0.971249ms) +✔ shipped authority follows the note's table (0.495674ms) +✔ version 1 files keep loading with no authority (0.755756ms) +✔ the conductor policy isn't a role (0.203739ms) +✔ a missing role file is exit 4, a symbolic link too (0.312205ms) +✔ version 2 refusals (0.981826ms) +✔ authority: closed vocabulary, no gated-only action, no overlap (2.054732ms) +✔ credentials: Gitea scopes (0.783621ms) +✔ credentials: Vikunja scopes are a group-to-verbs map from the grantable list (0.892118ms) +✔ credentials: services (0.458892ms) +✔ contract: a non-empty regular Markdown file beside the role file (0.624834ms) +✔ every key names known layers and a merge rule (0.76137ms) +✔ unknown keys and wrong layers refuse (0.780944ms) +✔ types (1.595196ms) +✔ merge: defaults, then the most specific layer wins (0.253108ms) +✔ merge: limits only narrow, and provenance lists each source (0.323743ms) +✔ merge doesn't change its inputs (0.108269ms) +ℹ tests 60 +ℹ suites 0 +ℹ pass 60 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 1859.606824 diff --git a/agents/darkwing/work/s6-review/out/node-cli.txt b/agents/darkwing/work/s6-review/out/node-cli.txt new file mode 100644 index 00000000..cfc5001c --- /dev/null +++ b/agents/darkwing/work/s6-review/out/node-cli.txt @@ -0,0 +1,87 @@ +✔ inbox lists only decisions routed to the human, with what approving authorizes and how to decide (127.407326ms) +✔ decide resolves by id prefix with --yes, and the trail shows it in broker order with the task to follow (141.689489ms) +✔ decide refuses without a terminal or --yes, on an unknown option and on a short reference (142.605184ms) +✔ decide prints a declining choice as declining (121.229846ms) +✔ an unknown outcome is reported once and never resent (112.51702ms) +✔ a decision closed before the answer arrives exits 2 and points at its trail (98.802976ms) +✔ a prefix that matches two open decisions exits 2 and resolves neither (99.474469ms) +✔ without --business a command uses the live host's business, and a stale host.json is not a host (96.405106ms) +✔ every human command refuses inside an agent run before it touches the bus (135.991574ms) +✔ usage errors exit 4; no business and no host is a usage error (113.200529ms) +✔ agents and tasks print through the broker (164.909648ms) +✔ notify.json: missing, loose, malformed and extra keys refuse; a binding or null passes (2.055998ms) +✔ bootConfig builds the broker's boot message for one business, with no trackers key when no project names one (44.45945ms) +✔ trackers come from the tracker.* variables of the one project that names a tracker project (33.551109ms) +✔ with two projects, the one that sets tracker.project supplies the tracker and the other is no conflict (31.918045ms) +✔ two projects that each name a tracker project refuse, since the boot shape holds one (28.986033ms) +✔ a business without tracker.baseUrl gets no trackers entry (27.034417ms) +✔ an unknown business and a broken system config refuse with exit 3 (51.63733ms) +✔ empty views say so (0.744068ms) +✔ the trail keeps the broker's order and names a decision's task without its rows (0.991007ms) +✔ tasks print the tracker fields the snapshot carries (0.136071ms) +✔ the host boots the broker, binds a launch in process, and the notifier DMs a blocking decision exactly once (896.785596ms) +✔ a notifier that dies takes the host down with exit 1, so the unit restarts the pair (256.455544ms) +✔ a second host for the same data root refuses with exit 3 while the first runs (239.608612ms) +✔ a notifier that refuses stops the broker and the host refuses with exit 3 (291.720324ms) +✔ a notifier that refuses after the broker died still refuses with exit 3, without a send to the dead broker (265.305714ms) +✔ a notifier that dies before it replies, after the broker died, still refuses, without a send to the dead broker (122.588192ms) +✔ a close send that fails with EPIPE after the notifier refuses still gives the notifier's refusal, exit 3 (178.527251ms) +✔ a close send that fails with EPIPE after the notifier dies unanswered still gives the notifier's error (197.615861ms) +✔ close() whose stop and close sends fail with EPIPE still finishes, with exit 1 (180.946075ms) +✔ watchChildren reports a child that died before it was called, and one that dies later (21.7181ms) +✔ bus stop refuses to signal a live pid that is not a bus host (203.058669ms) +✔ bus start refuses with exit 3 and the code when the broker refuses to boot; bus status names the lock (212.928287ms) +✔ bus start refuses with exit 3 without a notifier config (89.140811ms) +✔ bus start runs until bus stop; status reports it while it runs (656.935804ms) +✔ bus-service.sh renders the unit and installs it into a given directory (28.348121ms) +✔ bus start --pm: the PM runs under its own PID namespace, registered, with a manifest (270.794964ms) +✔ the PM launches a coder through its launch tool; the coder answers; refusals name their code (815.154892ms) +✔ a runner that stops at once ends its launch with the runner's reason (263.406741ms) +✔ a host that died hard leaves its sessions to the next host, which kills them and ends their runs so the role can be launched again (815.211823ms) +✔ a host that died hard leaves its sessions to the next host, which finds one already exited (23) and ends their runs so the role can be launched again (3657.484394ms) +✔ a malformed sessions.json refuses the host start with exit 3 and stays as it was (113.489374ms) +✔ zoned uses the IANA zone across DST (15.474129ms) +✔ each open blocking decision is DM'd once, across polls and a restart; non-blocking ones are not (129.254695ms) +✔ two blocking decisions get two DMs with different nonces (128.049389ms) +✔ the digest nonce differs per business and per day and fits Discord's 25 characters (0.188199ms) +✔ a failed DM is journaled, backs off, and is retried until it lands (133.43126ms) +✔ five definite refusals stop a DM: one gave-up line, one log line, and a restart keeps the count (124.968283ms) +✔ 429s, 5xx-style unknowns and refusals without a status never count toward the limit (116.127597ms) +✔ a crash between the fifth refusal and its gave-up line: the next poll appends it and sends nothing (93.4847ms) +✔ polled every POLL_MS against a permanent 403, a DM is sent at 0, 30, 60, 90 and 120 min and gives up only then (165.887479ms) +✔ a restart after the second refusal does not send before that refusal's 30 min are up (141.037869ms) +✔ the digest goes at 08:00 Chicago once a day, with blocking ones marked as DM'd (153.200525ms) +✔ a late start with no digest for the day sends one at once; an empty inbox gets one line (137.038182ms) +✔ an inbox read failure is logged and the next poll retries (0.650764ms) +✔ no Discord id reaches the journal or the log (170.603977ms) +✔ the journal: a torn tail is copied out and truncated, so an append after it reopens cleanly (49.136453ms) +✔ the journal: a crash between the copy and the truncate leaves a tail the next open repairs (81.494654ms) +✔ the journal: a whole file that is one torn line truncates to empty (22.271123ms) +✔ the journal: a malformed complete line refuses and leaves the file and any torn tail alone (2.728536ms) +✔ the journal: a loose file mode, a loose directory or a symlinked journal refuses (0.584967ms) +✔ the journal: a line with a wrong type refuses with exit 3 and names the field (1.950866ms) +✔ the journal: a symlinked directory refuses and says it is a link (0.292651ms) +✔ the journal: a dangling directory link, a parent that is a file and a journal that is a directory each refuse with exit 3 (0.45137ms) +✔ the journal: an append after the file was swapped for a symlink refuses and writes nothing through it (0.395917ms) +✔ the journal: a directory it cannot write or create refuses with exit 3 and names the path (0.410196ms) +✔ digest content stays within Discord's 2000 characters (0.266032ms) +✔ runLoop never overlaps ticks and stops after the one in flight (110.668485ms) +task.close {} answered: invalid-request; fake saw 18 requests, first GET /info 200, GET /projects/1 200, GET /projects/1/views 200 +task.close on a missing task answered: task-not-found; it made GET /tasks/999 404 +✔ bootConfig trackers reach the S3 adapter in the real broker child, which goes ready against a fake Vikunja (375.657886ms) +✔ the transport writes {business, verb, args} to the child and reads its JSON (34.268882ms) +✔ a bus code on stderr becomes the exit code; garbage and timeouts are outcome problems (2137.373473ms) +✔ busExit and refuseInsideAgent (0.46046ms) +✔ talk sends a REQUEST, prints and reads everything that arrives, and stops at the reply (229.138287ms) +✔ talk --wait 0 only sends; no reply within --wait exits 1 and says where it will show (1162.869992ms) +✔ talk, stop and launches off/on refuse inside an agent run; bad arguments are usage errors (170.664262ms) +✔ launches off and on go to the broker and change the business's launch state (117.53746ms) +✔ launches list reads sessions.json and marks a stale entry; stop reports it and refuses a non-runner (82.164709ms) +ℹ tests 77 +ℹ suites 0 +ℹ pass 77 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 6022.109024 diff --git a/agents/darkwing/work/s6-review/out/node-harness.txt b/agents/darkwing/work/s6-review/out/node-harness.txt new file mode 100644 index 00000000..f9fefff2 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/node-harness.txt @@ -0,0 +1,53 @@ +✔ sessionModel: agent vars win, then the system's execution settings (17.278444ms) +✔ a pi bundle: prompt, policy, tools and manifest, 0600 in a 0700 directory (6.894082ms) +✔ a claude-code bundle adds the wrapped gate hook and the MCP config (5.689389ms) +✔ a bundle is written once: an existing file refuses (2.33278ms) +✔ a path with a single quote can't go into the hook command (3.881591ms) +✔ allow exits 0, a deny exits 2 with the reason on stderr (130.760929ms) +✔ a missing or wrong policy, or a bad event, exits 2 (76.614744ms) +✔ the bundle's wrapped command: a missing gate or node still blocks (1087.842947ms) +✔ claude: typed tools through MCP, the hook blocks, builtins outside --tools don't exist (731.662814ms) +✔ claude: the hook alone blocks a path outside the workspace (467.79837ms) +✔ claude: a second turn resumes the first turn's session (708.774651ms) +✔ claude: a missing hook or MCP file refuses before claude starts (16.048526ms) +✔ pi: policy tools and typed tools pass, anything else is blocked (3.616285ms) +✔ claude: builtins map from pi names, typed tools need the mcp prefix (0.753166ms) +✔ file tool paths must resolve inside the workspace (1.154562ms) +✔ pi's own path normalisation can't be used to step out (1.161134ms) +✔ a symlink inside the workspace that points out is outside (1.095973ms) +✔ claude path fields per tool (0.776238ms) +✔ glob patterns stay inside the workspace (1.201128ms) +✔ a path that can't be checked is blocked (0.581822ms) +✔ initialize, ping and tools/list (44.402251ms) +✔ tools/call goes through the tool socket; a refusal is an isError result (31.674476ms) +✔ unknown tools and methods are JSON-RPC errors and never reach the socket (30.522533ms) +✔ a missing argument is a usage error (28.622595ms) +✔ pi: typed tools reach the socket, the gate blocks, agent_end writes the marker (350.095579ms) +✔ pi: a missing extension refuses before any model call (8.3472ms) +✔ pi: an extension without its configuration fails pi's start (275.717684ms) +✔ founderCheck: founder variables, then a needed service without a usable token (1.095529ms) +✔ turnRequest names the sender, class, reply and decision (0.177223ms) +✔ a message becomes a turn, the answer goes back as a RESULT, SIGTERM releases and exits 0 (238.692774ms) +✔ a SIGTERM before the claim stops the runner with exit 0 and no claim (99.758588ms) +✔ typed tools carry the runner's capability; launch goes to the host's launch socket (439.551997ms) +✔ a RESULT gets no automatic reply; failed turns reply with the reason (1312.043711ms) +✔ SIGTERM during a turn kills the turn's process group and still exits 0 (151.833299ms) +✔ founder credentials stop before the claim (20) (166.088906ms) +✔ a refused claim exits 21; an ended run's capability exits 22 (212.018616ms) +✔ the launch ending under a running session exits 22 (155.468877ms) +✔ a broker that stays unreachable exits 23 after brokerRetries polls (248.508739ms) +✔ a broker that is down at the claim exits 23, not 21 (105.883143ms) +✔ no capability, or a malformed one, on stdin exits 2 (153.856802ms) +✔ the PM gets launch, its task verbs and the reads (8.254629ms) +✔ a coder gets no launch, no resolve_decision, and no task tools without a tracker (4.257605ms) +✔ launch only when the business's launch block names the instance as launcher (1.639872ms) +✔ an action outside the instance's authority has no tool (1.401759ms) +✔ callTool: one JSON line out, the result back, a refusal rejects (8.517699ms) +ℹ tests 45 +ℹ suites 0 +ℹ pass 45 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 10363.182249 diff --git a/agents/darkwing/work/s6-review/out/node-seat.txt b/agents/darkwing/work/s6-review/out/node-seat.txt new file mode 100644 index 00000000..d6486f43 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/node-seat.txt @@ -0,0 +1,35 @@ +✔ resolveSeat: by name under --repo resolves the repo layout (1.254502ms) +✔ resolveSeat: by path resolves the fleet layout (0.516747ms) +✔ resolveSeat: refusals for missing dir, missing launch.sh, non-executable launch.sh, invalid name, and unknown layout (0.847962ms) +✔ tmuxContext: outside tmux, default socket, custom socket, and exec failure (0.635368ms) +✔ makeRegistration produces a record that validates; each shape violation throws SeatError (0.803431ms) +✔ writeRegistration/readRegistration: round trip, permissions, absence, and malformed records (1.24324ms) +✔ updateTask: changes task, taskSetBy and updatedAt only, and refuses appropriately (1.239321ms) +✔ CLI launch: registers, execs the fake launch script, and passes args through (30.059497ms) +✔ CLI launch: --harness lands in the record (31.801058ms) +✔ CLI launch: the launch script's own exit code passes through (28.546759ms) +✔ CLI launch: relaunching a seat rewrites the one registration record (58.693016ms) +✔ CLI launch: omitting --task records an empty string, not null (28.874274ms) +✔ CLI seat task: updates only the task after a launch, and refuses on an unlaunched seat (80.75812ms) +✔ CLI refusals: no args, unknown flag, missing config, already-registered env, and exec failure (135.711596ms) +✔ samePath: equal paths, symlinked dirs, distinct dirs, and non-strings (0.40395ms) +✔ resolveSetBy: explicit --by wins over the environment; absent or empty environment gives unknown; invalid explicit or environment values refuse with exit 4 (0.555598ms) +✔ validateRegistration/readRegistration: taskSetBy is optional; a record without it (written before #1511) still loads unchanged; an invalid one is refused; the version does not change (0.899447ms) +✔ updateTask: records setBy, preserves startedAt and every unrelated field, upgrades an old record in place only when the task is set, and replaces a previous attribution (0.861976ms) +✔ CLI seat task: --by beats MOSAIC_AGENT_NAME, the environment beats nothing, empty environment is unknown, invalid --by or environment refuses with exit 4 and leaves the record byte for byte (271.018867ms) +✔ family: exactly one launch.max key in the model name, else null (0.617808ms) +✔ sessionEnv passes only the allowlist, the repo's bin on PATH, and the run id (0.665325ms) +✔ newRun: short ids, 0700 directories, and a refusal when the socket path won't fit (1.463053ms) +✔ session file and launch log: 0600, the session file written once (1.346008ms) +✔ endReason maps the runner's exit codes; a signal is killed (0.107394ms) +✔ Registry mirrors to sessions.json; readSessions marks live entries; bad files refuse (1.244129ms) +✔ stopSession refuses an unknown run, reports a stale one, and won't signal a pid that isn't a runner (1.325051ms) +✔ a session runs under unshare as pid 1 of its namespace, claims, answers, and stops on mosaic stop (260.809593ms) +ℹ tests 27 +ℹ suites 0 +ℹ pass 27 +ℹ fail 0 +ℹ cancelled 0 +ℹ skipped 0 +ℹ todo 0 +ℹ duration_ms 726.22422 diff --git a/agents/darkwing/work/s6-review/out/summary.txt b/agents/darkwing/work/s6-review/out/summary.txt new file mode 100644 index 00000000..6176eb45 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/summary.txt @@ -0,0 +1,15 @@ +node-harness exit=0 ℹ pass 45 ℹ fail 0 +node-seat exit=0 ℹ pass 27 ℹ fail 0 +node-cli exit=0 ℹ pass 77 ℹ fail 0 +node-bus exit=0 ℹ pass 74 ℹ fail 0 +node-business exit=0 ℹ pass 60 ℹ fail 0 +test-auth exit=0 selftest: 15 passed, 0 failed +test-config exit=0 selftest: 24 passed, 0 failed +test-conductor exit=0 selftest: 17 passed, 0 failed +test-queue exit=0 queue suite: 27 passed, 0 failed +test-foundation exit=0 selftest: 44 passed, 0 failed +test-extension-package exit=0 extension package selftest: 18 passed, 0 failed +test-release exit=0 selftest: 4 passed, 0 failed +test-discord exit=0 discord suite: 66 passed, 0 failed +test-task exit=1 selftest: 26 passed, 2 failed +GATE-DONE diff --git a/agents/darkwing/work/s6-review/out/test-auth.txt b/agents/darkwing/work/s6-review/out/test-auth.txt new file mode 100644 index 00000000..ac228760 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-auth.txt @@ -0,0 +1,17 @@ +OK status with missing harness credential exits 3 and still lists accounts +OK status reports harness credential (read-only) + mosaic accounts +OK api key material never reaches output +OK oauth token material never reaches output +OK unparseable credential file exits 2 +OK symlinked credential file exits 4 +OK env-side credential names reported +OK env var values never reach output +OK accounts without an accounts dir reports none and creates nothing +OK accounts lists files and marks the active one +OK loose account perms flagged in listing +OK agent --auth with missing account file refuses (exit 4) +OK agent --auth with non-0600 account file refuses +OK agent --auth with invalid account name refuses +OK auth.sh without valid config refuses + +selftest: 15 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-conductor.txt b/agents/darkwing/work/s6-review/out/test-conductor.txt new file mode 100644 index 00000000..30b12230 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-conductor.txt @@ -0,0 +1,55 @@ +Note: switching to '2855e628ee91d046f4ea365b45dba5b0ccf4fd2a'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +Not currently on any branch. +nothing to commit, working tree clean +Note: switching to '2855e628ee91d046f4ea365b45dba5b0ccf4fd2a'. + +You are in 'detached HEAD' state. You can look around, make experimental +changes and commit them, and you can discard any commits you make in this +state without impacting any branches by switching back to a branch. + +If you want to create a new branch to retain commits you create, you may +do so (now or later) by using -c with the switch command. Example: + + git switch -c + +Or undo this operation with: + + git switch - + +Turn off this advice by setting config variable advice.detachedHead to false + +OK dry-run: allowed change, exit 0, nothing committed (exit 0) +OK dry-run committed nothing +OK apply: allowed change exits 0 (exit 0) +OK apply: attribution in commit subject +OK apply: target tree clean after commit +OK disallowed path refused (exit 1) +OK disallowed path: target untouched +OK syntax gate refused broken .mjs (exit 1) +OK syntax gate: target untouched +OK suite failure refused (exit 1) +OK suite failure: target reverted to clean +OK disabled policy refused (exit 2) +OK disabled policy: target untouched +OK failed run refused (exit 1) +OK failed run: target untouched +OK missing run exits 4 (exit 4) +OK invalid policy exits 2 (exit 2) + +selftest: 17 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-config.txt b/agents/darkwing/work/s6-review/out/test-config.txt new file mode 100644 index 00000000..76c05ef7 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-config.txt @@ -0,0 +1,26 @@ +OK absent adapter defaults to pi +OK adapter mock validates (exit 0) +OK unsupported adapter exits 2 (exit 2) +OK env exports adapter +OK bootstrap creates default when absent (exit 0) +OK bootstrap wrote config file +OK bootstrap is idempotent on existing config (exit 0) +OK bootstrap did not rewrite existing config +OK validate missing config exits 3 (exit 3) +OK malformed JSON exits 2 (exit 2) +OK unsupported configVersion exits 2 (exit 2) +OK unknown top-level key exits 2 (exit 2) +OK unknown execution key exits 2 (exit 2) +OK unsupported backend exits 2 (exit 2) +OK unsupported environment exits 2 (exit 2) +OK relative dataRoot exits 2 (exit 2) +OK non-canonical dataRoot exits 2 (exit 2) +OK filesystem root dataRoot exits 2 (exit 2) +OK home directory dataRoot exits 2 (exit 2) +OK dataRoot containing config dir exits 2 (exit 2) +OK control character in provider exits 2 (exit 2) +OK symlinked config file exits 2 (exit 2) +OK env exports resolve correctly +OK failed validation modified nothing + +selftest: 24 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-discord.txt b/agents/darkwing/work/s6-review/out/test-discord.txt new file mode 100644 index 00000000..b013f9ff --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-discord.txt @@ -0,0 +1,70 @@ +toolchain: node v26.8.1 + +OK syntax: packages/discord/src/approvals.mjs +OK syntax: packages/discord/src/authorize.mjs +OK syntax: packages/discord/src/binding.mjs +OK syntax: packages/discord/src/cli.mjs +OK syntax: packages/discord/src/connector.mjs +OK syntax: packages/discord/src/context.mjs +OK syntax: packages/discord/src/engine-pi.mjs +OK syntax: packages/discord/src/errors.mjs +OK syntax: packages/discord/src/gateway.mjs +OK syntax: packages/discord/src/git.mjs +OK syntax: packages/discord/src/journal.mjs +OK syntax: packages/discord/src/notify.mjs +OK syntax: packages/discord/src/rest.mjs +OK syntax: packages/discord/src/setspark.mjs +OK syntax: packages/discord/src/tools.mjs +OK syntax: packages/discord/src/web.mjs +OK syntax: packages/discord/bin/git-credential.mjs +OK syntax: packages/discord/extension/tools.mjs +OK syntax: packages/discord/tests/approvals.test.mjs +OK syntax: packages/discord/tests/authorize.test.mjs +OK syntax: packages/discord/tests/binding.test.mjs +OK syntax: packages/discord/tests/connector.test.mjs +OK syntax: packages/discord/tests/context.test.mjs +OK syntax: packages/discord/tests/engine.test.mjs +OK syntax: packages/discord/tests/fake-pi.mjs +OK syntax: packages/discord/tests/gateway.test.mjs +OK syntax: packages/discord/tests/git.test.mjs +OK syntax: packages/discord/tests/helpers.mjs +OK syntax: packages/discord/tests/journal.test.mjs +OK syntax: packages/discord/tests/notify.test.mjs +OK syntax: packages/discord/tests/recover.test.mjs +OK syntax: packages/discord/tests/rest.test.mjs +OK syntax: packages/discord/tests/setspark.test.mjs +OK syntax: packages/discord/tests/tools.test.mjs +OK syntax: packages/discord/tests/web.test.mjs +OK syntax: packages/discord/fixtures/claim-worker.mjs +OK syntax: packages/discord/fixtures/legacy-owner-worker.mjs +OK syntax: scripts/discord.sh +OK syntax: scripts/discord-service.sh +OK packages/discord declares no dependencies +OK no bot-token-shaped string in packages/discord +OK fixture binding uses placeholder ids only +OK fixture binding validates +OK real pi with the extension exposes exactly list_dir, read_file, search and no built-in tool +OK real pi with a writable root exposes exactly the three reads plus write_file and edit_file, and writes nothing at start +OK real pi with a web key exposes the three reads plus web_fetch and web_search, and no write tool without a writable root +OK real pi with a git root exposes the reads, writes and the four git verbs, commits nothing at start, and never shows the token +OK real pi with protocol vault adds reserve_id to the git verbs +OK real pi with a setspark key exposes the reads and the eight record verbs, no counters, and never shows the key +OK real pi refuses a git key on a read-only root (fail closed) +OK real pi with the pilot flags (--no-tools) exposes no tool at all +OK real pi exits non-zero without MOSAIC_DISCORD_TOOLS: no session, no tools (fail closed) +OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT +OK node --test packages/discord/tests/ (ℹ pass 178) +OK scripts/discord.sh --help exits 0 +OK scripts/discord.sh check without a binding exits 4 +OK scripts/discord.sh recover without a binding exits 4 +OK scripts/discord.sh reload without a binding exits 4 +OK scripts/discord-service.sh without a command exits 4 +OK service unit renders with the repository path, a supervised run as the main process, exit 3 never retried, and reload as SIGHUP +OK service install writes the rendered unit (0644) and leaves no temp file +OK service install a second time reports unchanged +OK systemd-analyze verify accepts the rendered unit +OK service uninstall removes the unit file +OK service install with an unknown flag exits 4 +OK service install with USER unset finishes and names the account for lingering + +discord suite: 66 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-extension-package.txt b/agents/darkwing/work/s6-review/out/test-extension-package.txt new file mode 100644 index 00000000..97281cce --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-extension-package.txt @@ -0,0 +1,21 @@ +OK initial ordinary-file install +OK installed tree matches canonical source +OK installed tree has no symlinks +OK check detects installation drift +OK sync refuses to overwrite installation drift +OK check detects an extra destination file +OK check detects an extra destination directory +OK check rejects a destination symlink +OK sync accepts a canonical source update +OK updated installation matches canonical source +scripts/test-extension-package.sh: line 14: 42746 Killed "$@" > /dev/null 2>&1 +OK forced interruption kills the replacing process +OK next invocation recovers old consistent installation +OK interrupted replacement rolled back +OK sync succeeds after interruption recovery +OK unlocked stale lock file does not block +OK active lock refuses a concurrent sync +OK source symlink fails closed +OK nested second entrypoint fails closed + +extension package selftest: 18 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-foundation.txt b/agents/darkwing/work/s6-review/out/test-foundation.txt new file mode 100644 index 00000000..6338e1e6 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-foundation.txt @@ -0,0 +1,53 @@ +toolchain: node v26.8.1, python 3.12.8, jsonschema 4.26.0 + +OK syntax: scripts/foundation-inspect.mjs +OK syntax: scripts/foundation/strict-json.mjs +OK syntax: scripts/foundation/canonical.mjs +OK syntax: scripts/foundation/resolve.mjs +OK syntax: scripts/foundation/validate-record.mjs +OK syntax: scripts/foundation/fixtures/build-fixtures.mjs +OK syntax: scripts/foundation/canonical.test.mjs +OK syntax: scripts/foundation/cli.test.mjs +OK syntax: scripts/foundation/fixtures.test.mjs +OK syntax: scripts/foundation/resolve.test.mjs +OK syntax: scripts/foundation/strict-json.test.mjs +OK syntax: scripts/foundation/verify-schema.py (ast only; no bytecode written) +OK fixture generator runs +OK checked-in fixtures/bundles equal a fresh generation +OK checked-in fixtures/raw equal a fresh generation +OK checked-in fixtures/index.json equal a fresh generation +OK checked-in demo bundles equal a fresh generation +OK a failing nested test fails the run under a parent runner's NODE_TEST_CONTEXT +OK node --test scripts/foundation/ (ℹ pass 80) +OK differential schema oracle: PASS: differential schema oracle (finite corpus; compatibility evidence, not equivalence proof) + platform witness: strftime('%Y') for year 999 -> '999' (pinned checker refuses years 0001..0999) + node v26.8.1; corpus 1568 records (38 pinned fixtures, 478 unique bundle records, 1052 typeCase/mutation/lexical cases) + schema column: agree-valid 540, agree-invalid 991, DISAGREEMENTS 0; strict-only (parser-bound) cases: 27; unsupported-kind records not schema-assessed by the inspector: 10 + profile column (schema-valid records only): profile-valid 510, profile-invalid 30 + profile refusals asserted: 30 schema-agreed-valid records refused only by the strict typed-string profile (rule profile-pattern-mismatch), 12 declared by name; 73 named probes verified against declared schema/profile columns +OK oracle: zero schema-column disagreements with the pinned checker +OK oracle: strict-only profile refusals are counted and asserted +OK demo: permitted read preview exits 0 (exit 0) +OK demo: permitted file.change preview exits 0 (exit 0) +OK demo: assignment.change proposal is unresolved (exit 3) (exit 3) +OK demo: revoked registration is refused (exit 3) (exit 3) +OK demo: message is not authority (exit 3) (exit 3) +OK usage: no arguments exits 2 (exit 2) +OK io: missing file exits 4 (exit 4) +OK io: directory exits 4 (exit 4) +OK io: symlink exits 4 (O_NOFOLLOW) (exit 4) +OK bound: oversize fixture exits 2 (exit 2) +OK profile: one final LF in a typed selection id is refused before admission (exit 2) (exit 2) +OK profile: two final LFs fail the schema pattern itself (exit 2) (exit 2) +OK profile: escaped newlines in free-form text stay allowed (exit 0) (exit 0) +OK profile refusal is invalid-request/profile-pattern-mismatch with selection and operation withheld, value not echoed +OK text output starts with the disclaimer +OK json output is valid JSON with result allowed and exactly the charter §7 fields +OK json golden matches byte-for-byte +OK sandboxed bundle run (env -i, PATH=/nonexistent) produced the unresolved proposal +OK sandbox inventory (path/type/size/mode/uid/gid/inode/mtime/sha256) unchanged by runs +OK canary never printed (bundle run and credential-file run) +OK a non-bundle JSON file is refused at the shape gate, not read into output +OK no field of the non-bundle file is echoed + +selftest: 44 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-queue.txt b/agents/darkwing/work/s6-review/out/test-queue.txt new file mode 100644 index 00000000..e8dc2a04 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-queue.txt @@ -0,0 +1,35 @@ +toolchain: node v26.8.1, git version 2.55.0 + +OK syntax: packages/queue/src/cli.mjs +OK syntax: packages/queue/src/errors.mjs +OK syntax: packages/queue/src/io.mjs +OK syntax: packages/queue/src/lock.mjs +OK syntax: packages/queue/src/queue.mjs +OK syntax: packages/queue/src/review.mjs +OK syntax: packages/queue/src/store.mjs +OK syntax: packages/queue/tests/commit.test.mjs +OK syntax: packages/queue/tests/data.test.mjs +OK syntax: packages/queue/tests/dispatch.test.mjs +OK syntax: packages/queue/tests/helpers.mjs +OK syntax: packages/queue/tests/lock.test.mjs +OK syntax: packages/queue/tests/migration.test.mjs +OK syntax: packages/queue/tests/review.test.mjs +OK syntax: packages/queue/tests/store.test.mjs +OK syntax: packages/queue/tests/write.test.mjs +OK syntax: packages/queue/tests/fixtures/fake-gitea.mjs +OK syntax: packages/queue/tests/fixtures/kill-at.mjs +OK syntax: packages/queue/tests/fixtures/lock-child.mjs +OK syntax: packages/queue/tests/fixtures/mosaic-pre-a2.sh +OK syntax: scripts/queue-commit.sh +OK syntax: scripts/git-hooks/pre-commit +OK syntax: scripts/mosaic +OK queue-commit.sh, the guard and scripts/mosaic are executable +OK packages/queue declares no dependencies +ℹ tests 148 +ℹ pass 148 +ℹ fail 0 +OK node --test packages/queue/tests/ +OK scripts/mosaic queue help +skip queue verify and render --check: this checkout (/home/jwoltje/darkwing-scratch/r41/wt) is not the queue's canonical root (/mnt/storage/src/mosaic-stack) + +queue suite: 27 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-release.txt b/agents/darkwing/work/s6-review/out/test-release.txt new file mode 100644 index 00000000..1d6ed050 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-release.txt @@ -0,0 +1,7 @@ +OK valid RELEASE resolves (exit 0) +OK invalid RELEASE exits 1 (exit 1) +OK missing RELEASE exits 1 (exit 1) +OK valid RELEASE leaves image tag consistent with version +skip state-machine cases (docker daemon unavailable) + +selftest: 4 passed, 0 failed diff --git a/agents/darkwing/work/s6-review/out/test-task.txt b/agents/darkwing/work/s6-review/out/test-task.txt new file mode 100644 index 00000000..85aa46f0 --- /dev/null +++ b/agents/darkwing/work/s6-review/out/test-task.txt @@ -0,0 +1,33 @@ +OK valid task validates (exit 0) +OK unknown task key exits 2 (exit 2) +OK unsupported taskVersion exits 2 (exit 2) +OK invalid task id exits 2 (exit 2) +OK empty prompt exits 2 (exit 2) +OK NUL in expectExact exits 2 (exit 2) +OK out-of-range timeout exits 2 (exit 2) +OK missing mission file exits 4 (exit 4) +OK task with valid mission validates (exit 0) +OK invalid mission exits 2 (exit 2) +OK validate missing task exits 4 (exit 4) +OK validation does not modify the task file +OK prune dry-run exits 0 (exit 0) +OK dry-run deleted nothing +OK prune --keep=2 --yes removes oldest (exit 0) +OK kept exactly 2 newest runs +OK newest run kept, oldest pruned +OK append-only receipt written (3 entries) +OK sessions/workspaces untouched by prune +OK prune with invalid keep exits 4 (exit 4) +skip adapter seam cases (docker daemon unavailable) +skip workspace/capability cases (docker daemon unavailable) +skip live task cases (docker unavailable) +OK onboard without name exits 4 (non-interactive) (exit 4) +OK onboard --name renders profile (exit 0) +OK profile written +OK canon structure: required filled, optional placeholdered +OK canon sections present +FAIL user recall run succeeds (exit 1) +FAIL recalled user name (response: ) +OK no agent identity on headless run + +selftest: 26 passed, 2 failed diff --git a/agents/darkwing/work/s6-review/probe/claude-dangling.sh b/agents/darkwing/work/s6-review/probe/claude-dangling.sh new file mode 100755 index 00000000..444f6919 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/claude-dangling.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# The dangling-symlink write through a real Claude Code session: the +# candidate adapter, a settings.json in the bundle's form running the real +# claude-gate.mjs, --tools Write, and a mock API whose first answer calls +# Write on the link. Dummy key, 127.0.0.1 only. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-cdangle-XXXX") +mkdir -p "$R/home" "$R/ws" "$R/sess" "$R/bundle" "$R/outside" +ln -s "$R/outside/planted.txt" "$R/ws/notes.md" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"claude-code","workspace":"%s","tools":["write"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json" +printf '{"hooks":{"PreToolUse":[{"matcher":"*","hooks":[{"type":"command","command":"timeout -k 2 10 %s %s %s || exit 2","timeout":20}]}]}}\n' \ + "$(command -v node)" "$WT/packages/harness/src/claude-gate.mjs" "$R/bundle/policy.json" > "$R/bundle/settings.json" +echo '{"mcpServers":{}}' > "$R/bundle/mcp.json" +TOOL_USE=$(printf '{"name":"Write","input":{"file_path":"%s","content":"written through the gate\\n"}}' "$R/ws/notes.md") \ + node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +set +e +env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \ + DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 MOSAIC_TOOLS=Write \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: write notes" \ + MOSAIC_WORKSPACE="$R/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \ + MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \ + timeout 90 /bin/sh "$WT/adapters/claude/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + echo "tool_result sent back: $(node -e ' + const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages")); + const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result"); + console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")" + echo "outside file: $( [ -e "$R/outside/planted.txt" ] && cat "$R/outside/planted.txt" || echo absent)" +} | tee -a "$OUT" diff --git a/agents/darkwing/work/s6-review/probe/claude-dangling.txt b/agents/darkwing/work/s6-review/probe/claude-dangling.txt new file mode 100644 index 00000000..a89ef484 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/claude-dangling.txt @@ -0,0 +1,4 @@ +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"/home/jwoltje/darkwing-scratch/tmp/r41-cdangle-0sjd/ws/notes.md resolves through a symlink to /home/jwoltje/darkwing-scratch/tmp/r41-cdangle-0sjd/outside/planted.txt, which is outside /home/jwoltje/darkwing-scratch/tmp/r41-cdangle-0sjd/ws; --restricted confines the file tools to the working directory."}] +outside file: absent diff --git a/agents/darkwing/work/s6-review/probe/claude-memory.sh b/agents/darkwing/work/s6-review/probe/claude-memory.sh new file mode 100755 index 00000000..bb3d2743 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/claude-memory.sh @@ -0,0 +1,39 @@ +#!/bin/sh +# Does a Claude Code session started by adapters/claude/adapter.sh load +# CLAUDE.md files (fake HOME, parent and workspace) and auto-memory? +# Runs the host's claude against the mock API with a dummy key. +set -eu +WT=$1; OUT=$2; VARIANT=$3 # VARIANT: candidate | no-restricted +R=$(mktemp -d "$TMPDIR/r41-claude-XXXX") +mkdir -p "$R/home/.claude" "$R/work/ws" "$R/sess" "$R/bundle" +echo "MARKER-USER-CLAUDE-MD" > "$R/home/.claude/CLAUDE.md" +echo "MARKER-PARENT-CLAUDE-MD" > "$R/work/CLAUDE.md" +echo "MARKER-WS-CLAUDE-MD" > "$R/work/ws/CLAUDE.md" +SLUG=$(printf '%s' "$R/work/ws" | sed 's|[/.]|-|g') +mkdir -p "$R/home/.claude/projects/$SLUG/memory" +echo "MARKER-AUTOMEMORY" > "$R/home/.claude/projects/$SLUG/memory/MEMORY.md" +echo "the generated prompt" > "$R/bundle/prompt.md" +echo '{}' > "$R/bundle/settings.json" +echo '{"mcpServers":{}}' > "$R/bundle/mcp.json" +cp "$WT/adapters/claude/adapter.sh" "$R/adapter.sh" +[ "$VARIANT" = candidate ] || sed -i '/--restricted \\/d' "$R/adapter.sh" +node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +set +e +env -i PATH="$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + ANTHROPIC_BASE_URL="http://127.0.0.1:$(cat "$R/port")" ANTHROPIC_API_KEY=sk-ant-dummy-not-a-key \ + DISABLE_TELEMETRY=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: hi" \ + MOSAIC_WORKSPACE="$R/work/ws" MOSAIC_SESSION_DIR="$R/sess" MOSAIC_MODEL=claude-sonnet-5-5 \ + MOSAIC_CLAUDE_SETTINGS="$R/bundle/settings.json" MOSAIC_CLAUDE_MCP_CONFIG="$R/bundle/mcp.json" \ + timeout 60 /bin/sh "$R/adapter.sh" > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "variant $VARIANT: $(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "requests: $(wc -l < "$R/api.log"); POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + for m in MARKER-USER-CLAUDE-MD MARKER-PARENT-CLAUDE-MD MARKER-WS-CLAUDE-MD MARKER-AUTOMEMORY "the generated prompt"; do + printf ' %-26s %s\n' "$m" "$(grep -c "$m" "$R/api.log" || true) request(s)" + done +} | tee -a "$OUT" +echo "$R" >> "$OUT.dirs" diff --git a/agents/darkwing/work/s6-review/probe/claude-memory.txt b/agents/darkwing/work/s6-review/probe/claude-memory.txt new file mode 100644 index 00000000..3c2bf6cc --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/claude-memory.txt @@ -0,0 +1,14 @@ +variant candidate: exit 0; stdout: mock answer; stderr: Warning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer. +requests: 1; POST /v1/messages: 1 + MARKER-USER-CLAUDE-MD 0 request(s) + MARKER-PARENT-CLAUDE-MD 0 request(s) + MARKER-WS-CLAUDE-MD 0 request(s) + MARKER-AUTOMEMORY 0 request(s) + the generated prompt 1 request(s) +variant no-restricted: exit 0; stdout: mock answer; stderr: Warning: no stdin data received in 3s, proceeding without it. If piping from a slow command, redirect stdin explicitly: < /dev/null to skip, or wait longer. +requests: 1; POST /v1/messages: 1 + MARKER-USER-CLAUDE-MD 1 request(s) + MARKER-PARENT-CLAUDE-MD 1 request(s) + MARKER-WS-CLAUDE-MD 1 request(s) + MARKER-AUTOMEMORY 1 request(s) + the generated prompt 1 request(s) diff --git a/agents/darkwing/work/s6-review/probe/close-hang.mjs b/agents/darkwing/work/s6-review/probe/close-hang.mjs new file mode 100644 index 00000000..2da4a8f5 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/close-hang.mjs @@ -0,0 +1,37 @@ +// Does launcher.close() (a beforeClose hook) wait on a launch.sock client +// that never closes its side? MODE=silent sends nothing; MODE=line sends a +// request line and keeps the socket open after the reply. +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { connect } from "node:net"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const WT = process.env.WT, MODE = process.env.MODE ?? "silent"; +const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`); +const { startHost } = await import(`${WT}/packages/cli/src/host.mjs`); +const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`); +const { launchSocketPath } = await import(`${WT}/packages/seat/src/session.mjs`); +const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-close-")); +const f = fixture(root, "acme", (doc) => ((doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } }), doc)); +mkdirSync(f.dataRoot, { mode: 0o700 }); +const adapter = join(root, "adapter.sh"); +writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`); +const system = loadSystem({ env: f.env }); +const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => console.log("host:", l) }); +const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => console.log("launcher:", l) }); +await launcher.listen(); +const s = connect(launchSocketPath(f.dataRoot)); +s.allowHalfOpen = true; +await new Promise((r) => s.on("connect", r)); +s.on("data", (b) => console.log(`client got ${b.toString().trim()} at ${Date.now() - t0} ms`)); +s.on("end", () => console.log(`client got FIN at ${Date.now() - t0} ms (not ending its side)`)); +const t0 = Date.now(); +if (MODE === "line") s.write(`{"cap":"${"0".repeat(64)}","instance":"coder"}\n`); +await new Promise((r) => setTimeout(r, 200)); +console.log(`host.close(0) at ${Date.now() - t0} ms`); +const closing = host.close(0).then((c) => `closed ${c}`); +const limit = new Promise((r) => setTimeout(() => r("still not closed"), Number(process.env.LIMIT ?? 25000))); +console.log(`${await Promise.race([closing, limit])} at ${Date.now() - t0} ms`); +s.destroy(); +console.log(`client destroyed; ${await closing} at ${Date.now() - t0} ms`); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/probe/close-hang.txt b/agents/darkwing/work/s6-review/probe/close-hang.txt new file mode 100644 index 00000000..59818683 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/close-hang.txt @@ -0,0 +1,12 @@ +== MODE=silent +host.close(0) at 201 ms +client got {"ok":false,"error":"invalid-request"} at 10050 ms +client got FIN at 10050 ms (not ending its side) +still not closed at 25227 ms +client destroyed; closed 0 at 25240 ms +== MODE=line +client got {"ok":false,"error":"unauthenticated"} at 2 ms +client got FIN at 2 ms (not ending its side) +host.close(0) at 201 ms +still not closed at 25227 ms +client destroyed; closed 0 at 25246 ms diff --git a/agents/darkwing/work/s6-review/probe/close-kill.mjs b/agents/darkwing/work/s6-review/probe/close-kill.mjs new file mode 100644 index 00000000..b806e8e4 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/close-kill.mjs @@ -0,0 +1,43 @@ +// Does launcher.close() SIGKILL a session whose runner never answers its +// SIGTERM? Launch the PM, SIGSTOP its runner from outside the namespace (a +// stopped process handles no signal but SIGKILL), close the host, and time +// it. Imports from the review worktree by absolute path. +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const WT = process.env.WT; +const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`); +const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`); +const { createLauncher } = await import(`${WT}/packages/cli/src/launcher.mjs`); +const { readSessions, launchLogFile } = await import(`${WT}/packages/seat/src/session.mjs`); +const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-kill-")); +const f = fixture(root, "acme", (doc) => { + doc.roles.coder.vars.model = "claude-sonnet-5-5"; + doc.launch = { by: "pm", instances: ["coder"], max: { opus: 1, sonnet: 1 } }; + return doc; +}); +mkdirSync(f.dataRoot, { mode: 0o700 }); +const adapter = join(root, "adapter.sh"); +writeFileSync(adapter, `exec '${process.execPath}' '${WT}/packages/harness/tests/fixtures/fake-adapter.mjs'\n`); +const system = loadSystem({ env: f.env }); +const logs = []; +const host = await startHost({ boot: bootConfig({ system, businessId: "acme", env: f.env }), business: "acme", log: (l) => logs.push(`host: ${l}`) }); +const launcher = createLauncher({ host, system, env: f.env, adapters: { pi: adapter, "claude-code": adapter }, sessionDefaults: { pollInterval: 100 }, versions: { pi: "0.85.1", claude: null }, log: (l) => logs.push(`launcher: ${l}`) }); +await launcher.listen(); +const pm = await launcher.launchPm(); +const [s] = readSessions(f.dataRoot); +const runnerLog = join(f.dataRoot, "launches", "acme", pm.run, "runner.log"); +for (let i = 0; i < 300 && !readFileSync(runnerLog, "utf8").includes("claimed by run"); i++) await new Promise((r) => setTimeout(r, 50)); +console.log(`pm run ${pm.run}: session pid ${s.pid}, runner pid ${s.runnerPid}, claimed`); +process.kill(s.runnerPid, "SIGSTOP"); +console.log("runner SIGSTOPped"); +const t0 = Date.now(); +const code = await host.close(0); +console.log(`host.close -> ${code} after ${Date.now() - t0} ms`); +console.log(`session pid alive: ${startTimeOf(s.pid) === s.startTime}; runner pid alive: ${startTimeOf(s.runnerPid) === s.runnerStartTime}`); +const ends = readFileSync(launchLogFile(f.dataRoot, "acme"), "utf8").trim().split("\n").map((l) => JSON.parse(l)).filter((e) => e.event === "end"); +console.log("launch-log end lines:", JSON.stringify(ends.map((e) => ({ run: e.run, reason: e.reason, exitCode: e.exitCode, signal: e.signal })))); +console.log("sessions.json after:", JSON.stringify(readSessions(f.dataRoot))); +console.log(logs.filter((l) => /ended|run /.test(l)).join("\n")); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/probe/close-kill.txt b/agents/darkwing/work/s6-review/probe/close-kill.txt new file mode 100644 index 00000000..bfb700a4 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/close-kill.txt @@ -0,0 +1,7 @@ +pm run r497e2e1c6477: session pid 316699, runner pid 316700, claimed +runner SIGSTOPped +host.close -> 0 after 30033 ms +session pid alive: false; runner pid alive: false +launch-log end lines: [{"run":"r497e2e1c6477","reason":"killed","exitCode":null,"signal":"SIGKILL"}] +sessions.json after: [] +launcher: run r497e2e1c6477 (pm) ended: killed diff --git a/agents/darkwing/work/s6-review/probe/dangling-write.mjs b/agents/darkwing/work/s6-review/probe/dangling-write.mjs new file mode 100644 index 00000000..766d12d0 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/dangling-write.mjs @@ -0,0 +1,27 @@ +// A dangling symlink in the workspace that points outside it. decide() walks +// up to the nearest existing ancestor with existsSync, which follows links, +// so the link itself counts as "missing" and the path passes. Then the same +// two calls pi's write tool makes (core/tools/write.js:45-48: mkdir of the +// dirname, recursive, then writeFile) create the file outside. +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, symlinkSync } from "node:fs"; +import { mkdir, writeFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { tmpdir } from "node:os"; +const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-dangle-")); +const ws = join(root, "ws"); +const outside = join(root, "outside", "planted.txt"); +mkdirSync(dirname(outside), { recursive: true }); +mkdirSync(ws); +symlinkSync(outside, join(ws, "notes.md")); // as a checked-out repo could carry it +for (const harness of ["pi", "claude-code"]) { + const policy = { harness, workspace: ws, tools: ["write", "edit"], typed: [] }; + const [tool, field] = harness === "pi" ? ["write", "path"] : ["Write", "file_path"]; + console.log(`${harness} ${tool} {${field}: "notes.md"}:`, JSON.stringify(decide(policy, tool, { [field]: harness === "pi" ? "notes.md" : join(ws, "notes.md") }))); +} +console.log("outside file exists before:", existsSync(outside)); +const target = join(ws, "notes.md"); +await mkdir(dirname(target), { recursive: true }); +await writeFile(target, "written through the gate\n", "utf-8"); +console.log("outside file after pi's write calls:", existsSync(outside) ? JSON.stringify(readFileSync(outside, "utf8")) : "absent"); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/probe/dangling-write.txt b/agents/darkwing/work/s6-review/probe/dangling-write.txt new file mode 100644 index 00000000..d01d33a7 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/dangling-write.txt @@ -0,0 +1,4 @@ +pi write {path: "notes.md"}: {"allow":true} +claude-code Write {file_path: "notes.md"}: {"allow":true} +outside file exists before: false +outside file after pi's write calls: "written through the gate\n" diff --git a/agents/darkwing/work/s6-review/probe/desync.mjs b/agents/darkwing/work/s6-review/probe/desync.mjs new file mode 100644 index 00000000..98d3d4d7 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/desync.mjs @@ -0,0 +1,69 @@ +// R1: host->broker IPC replies carry no id. Stall the broker past the 10 s +// firstReply wait with two requests queued, resume it, and see which reply +// each request gets. Imports from the review worktree by absolute path. +import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const WT = process.env.WT; +const { Client } = await import(`${WT}/packages/bus/src/client.mjs`); +const { bootConfig, loadSystem } = await import(`${WT}/packages/cli/src/config.mjs`); +const { startHost, startTimeOf } = await import(`${WT}/packages/cli/src/host.mjs`); +const { fixture } = await import(`${WT}/packages/cli/tests/helpers.mjs`); + +const root = mkdtempSync(join(tmpdir(), "r41-desync-")); +const f = fixture(root, "acme", (doc) => { + doc.launch = { by: "pm", instances: ["coder", "reviewer", "cto"], max: { opus: 1, sonnet: 1 } }; + return doc; +}); +mkdirSync(f.dataRoot, { mode: 0o700 }); +const system = loadSystem({ env: f.env }); +const boot = bootConfig({ system, businessId: "acme", env: f.env }); +const host = await startHost({ boot, business: "acme", log: (l) => console.log("host:", l) }); +const bind = async (role, run) => { + const b = await host.bindLaunch({ business: "acme", role, run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); + await new Client({ path: host.path, cap: b.cap }).call("role.claim"); + return b.cap; +}; +const pm = await bind("pm", "pm-run"); +const cto = await bind("cto", "cto-run"); +const show = (p) => p.then((v) => ({ ok: v }), (e) => ({ err: e.code ?? e.message })); + +console.log("baseline, no stall:"); +console.log(" authorizeLaunch(pm, coder):", JSON.stringify(await show(host.op({ op: "authorizeLaunch", cap: pm, instance: "coder" })))); +console.log(" authorizeLaunch(cto, coder):", JSON.stringify(await show(host.op({ op: "authorizeLaunch", cap: cto, instance: "coder" })))); +console.log(" identity(bogus):", JSON.stringify(await show(host.op({ op: "identity", cap: "bogus" })))); + +console.log(`SIGSTOP broker ${host.pids.broker}`); +process.kill(host.pids.broker, "SIGSTOP"); +const t0 = Date.now(); +const p1 = show(host.op({ op: "authorizeLaunch", cap: pm, instance: "coder" })); +const p2 = show(host.op({ op: "authorizeLaunch", cap: cto, instance: "coder" })); +const p3 = show(host.op({ op: "identity", cap: "bogus" })); +const r1 = await p1; +console.log(`p1 authorizeLaunch(pm) after ${Date.now() - t0} ms:`, JSON.stringify(r1)); +process.kill(host.pids.broker, "SIGCONT"); +console.log("SIGCONT"); +console.log("p2 authorizeLaunch(cto), should refuse:", JSON.stringify(await p2)); +console.log("p3 identity(bogus), should refuse:", JSON.stringify(await p3)); +const p4 = await show(host.op({ op: "identity", cap: cto })); +console.log("p4 identity(cto), should be cto:", JSON.stringify(p4)); + +await new Promise((r) => setTimeout(r, 500)); +console.log("--- bind case: two binds queued during a stall"); +process.kill(host.pids.broker, "SIGSTOP"); +const rec = (run) => ({ business: "acme", role: "coder", run, harness: "pi", pid: process.pid, startTime: startTimeOf(process.pid) }); +const b1 = show(host.bindLaunch(rec("run-x"))); +const b2 = show(host.bindLaunch({ ...rec("run-y"), role: "reviewer" })); +console.log("b1 bind run-x:", JSON.stringify(await b1)); +process.kill(host.pids.broker, "SIGCONT"); +const r2 = await b2; +console.log("b2 bind run-y (reviewer) got:", r2.ok ? JSON.stringify({ run: r2.ok.run, role: r2.ok.role }) : JSON.stringify(r2)); +if (r2.ok?.cap) { + const who = await show(new Client({ path: host.path, cap: r2.ok.cap }).call("role.claim")); + console.log("claim with b2's cap:", JSON.stringify(who)); +} +await new Promise((r) => setTimeout(r, 3000)); +console.log("after 3 s idle, identity(pm), should be pm:", JSON.stringify(await show(host.op({ op: "identity", cap: pm })))); +console.log("then identity(pm) again:", JSON.stringify(await show(host.op({ op: "identity", cap: pm })))); +await host.close(0); +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/probe/desync.txt b/agents/darkwing/work/s6-review/probe/desync.txt new file mode 100644 index 00000000..6fb65885 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/desync.txt @@ -0,0 +1,16 @@ +baseline, no stall: + authorizeLaunch(pm, coder): {"ok":{"class":"within-role"}} + authorizeLaunch(cto, coder): {"err":"decision-required"} + identity(bogus): {"err":"unauthenticated"} +SIGSTOP broker 86383 +p1 authorizeLaunch(pm) after 10024 ms: {"err":"broker did not reply within 10 s"} +SIGCONT +p2 authorizeLaunch(cto), should refuse: {"ok":{"class":"within-role"}} +p3 identity(bogus), should refuse: {"err":"decision-required"} +p4 identity(cto), should be cto: {"err":"unauthenticated"} +--- bind case: two binds queued during a stall +b1 bind run-x: {} +b2 bind run-y (reviewer) got: {"run":"run-x"} +claim with b2's cap: {"ok":{"role":"coder","run":"run-x"}} +after 3 s idle, identity(pm), should be pm: {} +then identity(pm) again: {"ok":{"business":"acme","role":"pm","run":"pm-run","human":null}} diff --git a/agents/darkwing/work/s6-review/probe/gate-edges.mjs b/agents/darkwing/work/s6-review/probe/gate-edges.mjs new file mode 100644 index 00000000..0807ab87 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/gate-edges.mjs @@ -0,0 +1,28 @@ +// decide() on path spellings that might leave the workspace. +import { mkdirSync, mkdtempSync, symlinkSync, rmSync } from "node:fs"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +const { decide } = await import(`${process.env.WT}/packages/harness/src/gate.mjs`); +const root = mkdtempSync(join(tmpdir(), "r41-gate-")); +const ws = join(root, "ws"); +mkdirSync(join(ws, "sub"), { recursive: true }); +symlinkSync("/etc", join(ws, "etc-link")); +symlinkSync(join(root, "not-yet"), join(ws, "dangling")); +const pi = { harness: "pi", workspace: ws, tools: ["read", "write", "grep", "find", "ls"], typed: ["message.send"] }; +const cc = { ...pi, harness: "claude-code" }; +const cases = [ + [pi, "read", { path: "sub/../../x" }], [pi, "read", { path: "@/etc/passwd" }], [pi, "read", { path: "~/x" }], + [pi, "read", { path: "file:///etc/passwd" }], [pi, "read", { path: " /etc/passwd" }], [pi, "read", { path: "etc-link/passwd" }], + [pi, "write", { path: "dangling/x" }], [pi, "read", { path: "sub/./x" }], [pi, "grep", { path: "..", pattern: "x" }], + [pi, "find", { pattern: "{..,x}/*" }], [pi, "find", { pattern: "*", path: "/" }], [pi, "ls", {}], + [pi, "bash", { command: "cat /etc/passwd" }], [pi, "message.send", {}], + [cc, "Read", { file_path: "/etc/passwd" }], [cc, "Read", { file_path: `${ws}/sub/x` }], [cc, "Glob", { pattern: "/etc/*" }], + [cc, "Glob", { pattern: "**/x", path: "/" }], [cc, "Grep", { pattern: "x", path: "/etc" }], [cc, "Grep", { pattern: "x", glob: "../*" }], + [cc, "Bash", { command: "id" }], [cc, "WebFetch", { url: "http://x" }], [cc, "mcp__mosaic__message.send", {}], + [cc, "mcp__other__message.send", {}], [cc, "message.send", {}], [cc, "Read", { file_path: 7 }], +]; +for (const [p, tool, input] of cases) { + const d = decide(p, tool, input); + console.log(`${p.harness.padEnd(11)} ${tool.padEnd(28)} ${JSON.stringify(input).padEnd(40)} ${d.allow ? "ALLOW" : "deny: " + d.reason.replace("mosaic gate: ", "")}`); +} +rmSync(root, { recursive: true, force: true }); diff --git a/agents/darkwing/work/s6-review/probe/gate-edges.txt b/agents/darkwing/work/s6-review/probe/gate-edges.txt new file mode 100644 index 00000000..e3459764 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/gate-edges.txt @@ -0,0 +1,26 @@ +pi read {"path":"sub/../../x"} deny: read path is outside the workspace: sub/../../x +pi read {"path":"@/etc/passwd"} deny: read path is outside the workspace: @/etc/passwd +pi read {"path":"~/x"} deny: read path is outside the workspace: ~/x +pi read {"path":"file:///etc/passwd"} deny: read path is outside the workspace: file:///etc/passwd +pi read {"path":" /etc/passwd"} ALLOW +pi read {"path":"etc-link/passwd"} deny: read path is outside the workspace: etc-link/passwd +pi write {"path":"dangling/x"} ALLOW +pi read {"path":"sub/./x"} ALLOW +pi grep {"path":"..","pattern":"x"} deny: grep path is outside the workspace: .. +pi find {"pattern":"{..,x}/*"} ALLOW +pi find {"pattern":"*","path":"/"} deny: find path is outside the workspace: / +pi ls {} ALLOW +pi bash {"command":"cat /etc/passwd"} deny: bash isn't in this session's policy +pi message.send {} ALLOW +claude-code Read {"file_path":"/etc/passwd"} deny: Read path is outside the workspace: /etc/passwd +claude-code Read {"file_path":"/home/jwoltje/darkwing-scratch/tmp/r41-gate-1f41z7/ws/sub/x"} ALLOW +claude-code Glob {"pattern":"/etc/*"} deny: Glob pattern must stay inside the workspace: /etc/* +claude-code Glob {"pattern":"**/x","path":"/"} deny: Glob path is outside the workspace: / +claude-code Grep {"pattern":"x","path":"/etc"} deny: Grep path is outside the workspace: /etc +claude-code Grep {"pattern":"x","glob":"../*"} ALLOW +claude-code Bash {"command":"id"} deny: Bash isn't in this session's policy +claude-code WebFetch {"url":"http://x"} deny: WebFetch isn't in this session's policy +claude-code mcp__mosaic__message.send {} ALLOW +claude-code mcp__other__message.send {} deny: mcp__other__message.send isn't in this session's policy +claude-code message.send {} deny: message.send isn't in this session's policy +claude-code Read {"file_path":7} deny: Read.file_path isn't a string diff --git a/agents/darkwing/work/s6-review/probe/mock-api.mjs b/agents/darkwing/work/s6-review/probe/mock-api.mjs new file mode 100644 index 00000000..4ae893af --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/mock-api.mjs @@ -0,0 +1,45 @@ +// A mock Messages API on 127.0.0.1: logs each request body to LOG and +// streams back one short text answer. No real model is reached. +import { createServer } from "node:http"; +import { appendFileSync } from "node:fs"; +const LOG = process.argv[2]; +// TOOL_USE: optional JSON {name, input}; the first streamed answer calls it. +let toolUse = process.env.TOOL_USE ? JSON.parse(process.env.TOOL_USE) : null; +const sse = (res, ev, data) => res.write(`event: ${ev}\ndata: ${JSON.stringify(data)}\n\n`); +const server = createServer((req, res) => { + let body = ""; + req.on("data", (b) => (body += b)); + req.on("end", () => { + appendFileSync(LOG, `${JSON.stringify({ method: req.method, url: req.url, body })}\n`); + if (req.method !== "POST" || !req.url.startsWith("/v1/messages") || req.url.includes("count_tokens")) { + res.writeHead(200, { "content-type": "application/json" }); + return res.end(req.url.includes("count_tokens") ? '{"input_tokens":1}' : "{}"); + } + let stream = false; + try { stream = JSON.parse(body).stream === true; } catch {} + const msg = { id: "msg_mock", type: "message", role: "assistant", model: "claude-sonnet-5-5", content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 1 } }; + if (!stream) { + res.writeHead(200, { "content-type": "application/json" }); + return res.end(JSON.stringify({ ...msg, content: [{ type: "text", text: "mock answer" }], stop_reason: "end_turn" })); + } + res.writeHead(200, { "content-type": "text/event-stream" }); + sse(res, "message_start", { type: "message_start", message: msg }); + if (toolUse) { + const t = toolUse; + toolUse = null; + sse(res, "content_block_start", { type: "content_block_start", index: 0, content_block: { type: "tool_use", id: "toolu_mock1", name: t.name, input: {} } }); + sse(res, "content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "input_json_delta", partial_json: JSON.stringify(t.input) } }); + sse(res, "content_block_stop", { type: "content_block_stop", index: 0 }); + sse(res, "message_delta", { type: "message_delta", delta: { stop_reason: "tool_use", stop_sequence: null }, usage: { output_tokens: 2 } }); + sse(res, "message_stop", { type: "message_stop" }); + return res.end(); + } + sse(res, "content_block_start", { type: "content_block_start", index: 0, content_block: { type: "text", text: "" } }); + sse(res, "content_block_delta", { type: "content_block_delta", index: 0, delta: { type: "text_delta", text: "mock answer" } }); + sse(res, "content_block_stop", { type: "content_block_stop", index: 0 }); + sse(res, "message_delta", { type: "message_delta", delta: { stop_reason: "end_turn", stop_sequence: null }, usage: { output_tokens: 2 } }); + sse(res, "message_stop", { type: "message_stop" }); + res.end(); + }); +}); +server.listen(0, "127.0.0.1", () => console.log(server.address().port)); diff --git a/agents/darkwing/work/s6-review/probe/pi-dangling.sh b/agents/darkwing/work/s6-review/probe/pi-dangling.sh new file mode 100755 index 00000000..68237276 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/pi-dangling.sh @@ -0,0 +1,41 @@ +#!/bin/sh +# R3 through a real Pi 0.85.1 session: the candidate adapters/pi/adapter.sh, +# the candidate pi-extension.mjs (the gate) loaded with -e, --tools write, +# and a mock Messages API on 127.0.0.1 whose first answer calls write on a +# dangling symlink in the workspace. Dummy key; no real model. +set -eu +WT=$1; OUT=$2 +R=$(mktemp -d "$TMPDIR/r41-pdangle-XXXX") +mkdir -p "$R/home" "$R/agent" "$R/ws" "$R/bundle" "$R/outside" +ln -s "$R/outside/planted.txt" "$R/ws/notes.md" +# MODE=existing: the control, the link target exists, so the link isn't dangling. +if [ "${MODE:-dangling}" = existing ]; then echo "was here before" > "$R/outside/planted.txt"; fi +echo "== MODE=${MODE:-dangling}" | tee -a "$OUT" +echo "the generated prompt" > "$R/bundle/prompt.md" +printf '{"harness":"pi","workspace":"%s","tools":["write"],"typed":[]}\n' "$R/ws" > "$R/bundle/policy.json" +echo '[]' > "$R/bundle/tools.json" +TOOL_USE='{"name":"write","input":{"path":"notes.md","content":"written through the gate\n"}}' \ + node "$(dirname "$0")/mock-api.mjs" "$R/api.log" > "$R/port" & MOCK=$! +while [ ! -s "$R/port" ]; do sleep 0.1; done +printf '{"providers":{"mock":{"baseUrl":"http://127.0.0.1:%s","api":"anthropic-messages","apiKey":"dummy-not-a-key","models":[{"id":"claude-sonnet-5-5"}]}}}\n' "$(cat "$R/port")" > "$R/agent/models.json" +set +e +env -i PATH="$WT/node_modules/.bin:$PATH" HOME="$R/home" USER="$USER" LANG=C.UTF-8 \ + PI_CODING_AGENT_DIR="$R/agent" PI_PROVIDER=mock PI_MODEL=claude-sonnet-5-5 \ + MOSAIC_SYSTEM_PROMPT_FILE="$R/bundle/prompt.md" MOSAIC_REQUEST="Message m1 from jason: write notes" \ + MOSAIC_WORKSPACE="$R/ws" MOSAIC_TOOLS=write \ + MOSAIC_EXTENSIONS="$WT/packages/harness/src/pi-extension.mjs" \ + MOSAIC_POLICY_FILE="$R/bundle/policy.json" MOSAIC_TOOLS_FILE="$R/bundle/tools.json" \ + MOSAIC_TOOL_SOCKET="$R/no-socket" MOSAIC_TURN_MARKER="$R/marker" \ + timeout 90 /bin/sh "$WT/adapters/pi/adapter.sh" < /dev/null > "$R/stdout" 2> "$R/stderr" +echo "exit $?" > "$R/exit" +kill $MOCK +{ + echo "$(cat "$R/exit"); stdout: $(head -c 200 "$R/stdout"); stderr: $(head -c 300 "$R/stderr")" + echo "POST /v1/messages: $(grep -c '"url":"/v1/messages' "$R/api.log")" + echo "tool_result sent back: $(node -e ' + const ls=require("fs").readFileSync(process.argv[1],"utf8").trim().split("\n").map(l=>JSON.parse(l)).filter(l=>l.url.startsWith("/v1/messages")); + const b=JSON.parse(ls.at(-1).body); const r=b.messages.flatMap(m=>Array.isArray(m.content)?m.content:[]).filter(c=>c.type==="tool_result"); + console.log(JSON.stringify(r.map(c=>({is_error:c.is_error??false,content:c.content}))));' "$R/api.log")" + echo "turn marker: $( [ -e "$R/marker" ] && echo written || echo absent)" + echo "outside file: $( [ -e "$R/outside/planted.txt" ] && cat "$R/outside/planted.txt" || echo absent)" +} | tee -a "$OUT" diff --git a/agents/darkwing/work/s6-review/probe/pi-dangling.txt b/agents/darkwing/work/s6-review/probe/pi-dangling.txt new file mode 100644 index 00000000..942fde73 --- /dev/null +++ b/agents/darkwing/work/s6-review/probe/pi-dangling.txt @@ -0,0 +1,12 @@ +== MODE=dangling +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":false,"content":"Successfully wrote to notes.md"}] +turn marker: written +outside file: written through the gate +== MODE=existing +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path is outside the workspace: notes.md"}] +turn marker: written +outside file: was here before diff --git a/agents/darkwing/work/s6-review/review-r1.md b/agents/darkwing/work/s6-review/review-r1.md new file mode 100644 index 00000000..f814a29e --- /dev/null +++ b/agents/darkwing/work/s6-review/review-r1.md @@ -0,0 +1,351 @@ +# Row 41, slice 1 S6 (meta-harness and launching), round 1 review (Darkwing) + +Issue #1523, request comment 26991, queue revs 244 and 245 (`c7a2c703`). +Packet: `agents/filbert/work/s6/` at `9b670e27`, base `915e00e5`, gate at +`2855e628`, 41 files, +4302/−55. Candidate manifest sha256 +`5b3a934d01eb518e23b842623aeb3cbfc287f1b88110edc1e044d4131b2927f3`, +`build.patch` sha256 +`dc346027db9b650c70b57ff390b57a8d76d3e2ccd434538cb083fe9a97ea4454`. +Rulings: lead decisions 77 and 78. + +Verdict: **changes**, comment 26995. Two findings block. R1: a broker +reply that misses the host's 10 s wait shifts every later reply by one, so a +refused `authorizeLaunch` comes back allowed and one run's capability goes +to another. R3: a dangling symlink in the workspace passes the gate, and a +real Pi session writes through it to a file outside. R2 should be fixed in +the same round: `launcher.close()` waits on any `launch.sock` client that +keeps its side open, so the runners never get their SIGTERM. The rest of +the brief holds under my probes and mutants. + +## Method + +- A detached worktree at `2855e628`, then `git apply --index build.patch` + and `sha256sum -c candidate-manifest.sha256`: 41 OK. After the mutants + and probes I checked again: 41 OK, and `git diff --name-only` empty. +- I read the diff for the five places Filbert named, plus `gate.mjs`, + `claude-gate.mjs`, `pi-extension.mjs`, `session.mjs` and the READMEs, + against the S6 brief, the S0 lines and decisions 77 and 78. +- Probes in `probe/`. They import from my scratch worktree by absolute + path (`WT`), so they won't run as committed without setting it. The + model calls go to `probe/mock-api.mjs`, a mock Messages API on + 127.0.0.1 with a dummy key; no real model was reached and nothing was + spent. Claude Code is 2.1.296, Pi is the pinned 0.85.1. +- Fourteen mutants of my own (`mut/mutate.py`, `mut/run.sh`), each run + against the node suites of the packages it touches, then restored. + +Node is v26.8.1, `TMPDIR=~/darkwing-scratch/tmp`, +`DOCKER_HOST=unix:///nonexistent.sock`. + +## Suites + +Run sequentially in the worktree, each output in `out/`. + +| Suite | Result | File | +|---|---|---| +| `node --test 'packages/harness/tests/*.test.mjs'` | 45/45 | `out/node-harness.txt` | +| `node --test 'packages/seat/tests/*.test.mjs'` | 27/27 | `out/node-seat.txt` | +| `node --test 'packages/cli/tests/*.test.mjs'` | 77/77 | `out/node-cli.txt` | +| `node --test 'packages/bus/tests/*.test.mjs'` | 74/74 | `out/node-bus.txt` | +| `node --test 'packages/business/tests/*.test.mjs'` | 60/60 | `out/node-business.txt` | +| `test-auth`, `test-config`, `test-conductor` | 15, 24, 17 passed, 0 failed | `out/test-*.txt` | +| `test-queue`, `test-foundation`, `test-extension-package` | 27, 44, 18 passed, 0 failed | | +| `test-release`, `test-discord` | 4, 66 passed, 0 failed | | +| `test-task` | 26 passed, 2 failed | `out/test-task.txt` | + +The two `test-task` failures are the live user-recall pair, which needs +Docker. They match Filbert's base run. Filbert's gate covers the other +node suites. + +## R1: broker replies carry no id, and a late one shifts every later reply (blocking) + +`host.mjs:157-171`. `request()` sends one message to the broker child and +takes the first IPC message that arrives within 10 s (`firstReply`) as the +answer. The comment at `:155` says the replies carry no request id, so +requests go one at a time. That holds only while every reply arrives +inside the wait. When one doesn't, `firstReply` rejects and the queue +moves on, but the broker still answers. That late answer is the first +message the next request sees. From then on every request gets the reply +to the one before it. + +`probe/desync.mjs` starts a real host, binds a `pm` and a `cto` run, and +stalls the broker with SIGSTOP while three requests wait +(`probe/desync.txt`): + +``` +baseline, no stall: + authorizeLaunch(pm, coder): {"ok":{"class":"within-role"}} + authorizeLaunch(cto, coder): {"err":"decision-required"} + identity(bogus): {"err":"unauthenticated"} +p1 authorizeLaunch(pm) after 10024 ms: {"err":"broker did not reply within 10 s"} +p2 authorizeLaunch(cto), should refuse: {"ok":{"class":"within-role"}} +p3 identity(bogus), should refuse: {"err":"decision-required"} +p4 identity(cto), should be cto: {"err":"unauthenticated"} +--- bind case: two binds queued during a stall +b1 bind run-x: {} +b2 bind run-y (reviewer) got: {"run":"run-x"} +claim with b2's cap: {"ok":{"role":"coder","run":"run-x"}} +after 3 s idle, identity(pm), should be pm: {} +then identity(pm) again: {"ok":{"business":"acme","role":"pm","run":"pm-run","human":null}} +``` + +The `cto` instance needs a decision to launch a coder, and after the stall +it is allowed. In the bind case the reviewer launch `run-y` receives +`run-x`'s capability, and a `role.claim` with it acts as `coder`, run +`run-x`. In the launcher that capability goes on the new runner's stdin +(`launcher.mjs:217`), so the reviewer process would run as the coder. The +shift doesn't heal with idle time: 3 s later `identity(pm)` still gets the +bind reply (`{}`, no `result`). The last line looks right only because it +asks the same question as the one before it. + +This fails open at the boundary decision 77 names. The trigger in my probe +is SIGSTOP. Without it, anything that holds the broker past 10 s does the +same: a slow disk under a checkpoint, swap, or a same-UID process that asks +the systemd user manager to stop it (the README's own limit). I didn't find +a path inside the broker alone that blocks for 10 s; sqlite's busy timeout +is 5 s per statement. The pattern itself came with `bindLaunch` in S4 +(`2f5303c1`), but nothing called it until this row, which now routes all +six launch ops through it. + +Either fix works: + +1. Put an id on each request, echo it in `process.mjs`, and have + `firstReply` ignore (and log) a reply whose id doesn't match. That + needs a test that stalls the broker past the wait and checks the next + request's answer. +2. Treat a `firstReply` timeout on the broker as fatal to the channel: + close the host with exit 1, and the unit restarts it. Simpler, and the + launches in flight fail closed. + +## R2: `launcher.close()` waits on any `launch.sock` client (should fix) + +`launcher.mjs:377-381`. `close()` awaits `server.close(r)`, which waits +until every connection on `launch.sock` is gone. The reply path +(`:343`) calls `socket.end()`, which sends FIN but doesn't destroy the +socket; a client that keeps its write side open keeps the connection, and +`close()` waits. The SIGTERM to the runners comes after that line, so they +get none. + +`probe/close-hang.mjs` connects with `allowHalfOpen` and never ends its +side (`probe/close-hang.txt`): + +``` +== MODE=silent +host.close(0) at 201 ms +client got {"ok":false,"error":"invalid-request"} at 10050 ms +client got FIN at 10050 ms (not ending its side) +still not closed at 25227 ms +client destroyed; closed 0 at 25240 ms +== MODE=line +client got {"ok":false,"error":"unauthenticated"} at 2 ms +client got FIN at 2 ms (not ending its side) +host.close(0) at 201 ms +still not closed at 25227 ms +client destroyed; closed 0 at 25246 ms +``` + +The host closes only when the client goes away. A default Node client +would end its side on FIN, so this takes a client that doesn't: a stopped +or hung process, or a session that does it on purpose. The socket is 0600 +and same UID, and a session with `bash` can reach it. Then `bus stop` +waits until systemd's stop timeout kills the host, and the runners die by +SIGKILL with no `role.release` and no end record from this host. Filbert +asked about the 30 s close. The 30 s SIGKILL timer starts only after this +wait, so it doesn't bound it. + +Fix: keep the accepted sockets in a set and destroy them in `close()` +before awaiting `server.close`, or `destroySoon()` after the reply. A test +with a half-open client would cover it. + +## R3: a dangling symlink in the workspace passes the gate (blocking for Pi) + +`gate.mjs:45-55`. `real()` walks up from the target until `existsSync` +returns true, then realpaths that ancestor and keeps the missing tail. +`existsSync` follows links, so a symlink whose target doesn't exist counts +as missing. Its name stays in the tail, under the workspace root, and +`insideWorkspace` says yes. The harness README says every path "must +resolve inside the workspace after symlinks"; for a dangling link it +doesn't. + +The gate on its own (`probe/dangling-write.mjs`, `dangling-write.txt`), +with `ws/notes.md -> ../outside/planted.txt` and the target absent: + +``` +pi write {path: "notes.md"}: {"allow":true} +claude-code Write {file_path: "notes.md"}: {"allow":true} +outside file exists before: false +outside file after pi's write calls: "written through the gate\n" +``` + +The last line repeats the two calls Pi's write tool makes +(`core/tools/write.js:45-48`: a recursive `mkdir` of the dirname, then +`writeFile`). + +Then a real Pi 0.85.1 session (`probe/pi-dangling.sh`, +`pi-dangling.txt`): the candidate `adapters/pi/adapter.sh` with the +candidate `pi-extension.mjs`, `--tools write`, and a mock API whose first +answer calls `write` on `notes.md`. The control run gives the link a target +that exists. + +``` +== MODE=dangling +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":false,"content":"Successfully wrote to notes.md"}] +turn marker: written +outside file: written through the gate +== MODE=existing +exit 0; stdout: mock answer; stderr: +POST /v1/messages: 2 +tool_result sent back: [{"is_error":true,"content":"mosaic gate: write path is outside the workspace: notes.md"}] +turn marker: written +outside file: was here before +``` + +The gate refuses the link when its target exists. Remove the target and +the same write lands outside the workspace. + +Claude Code through the candidate adapter and the real `claude-gate.mjs` +(`probe/claude-dangling.sh`, `claude-dangling.txt`): the gate allowed the +`Write`, and Claude Code refused it itself. + +``` +tool_result sent back: [{"is_error":true,"content":".../ws/notes.md resolves through a symlink to .../outside/planted.txt, which is outside .../ws; --restricted confines the file tools to the working directory."}] +outside file: absent +``` + +So Claude Code is held by `--restricted`, which the README calls an extra +layer the S0 lines don't depend on. Pi has no second layer. + +The link doesn't have to come from the session. Workspaces persist per +instance (`workspaceDir(dataRoot, business, instance)`), so a checked-out +repository can carry one, and so can an earlier session of the same +instance whose role had `bash`. A link under a missing directory +(`dangling/x` in `probe/gate-edges.txt`) passes the same way. + +Fix: in `real()`, `lstat` each component that doesn't exist. If it is a +symlink, either refuse the path or `readlink` it, resolve the target +against its directory and keep walking. Add a gate test for a dangling +link and for a link under a missing directory, for both harnesses. + +## What holds + +- **Early signals.** `runner.mjs:256-257` installs SIGTERM and SIGINT + before anything reads stdin or the session file. Mutant Ma moves them + after setup and fails three harness tests and two cli tests. +- **Stdin cap.** `runner.mjs:78` refuses more than 4096 bytes. Mutant Mj (no cap) survives; see note 5. +- **Exit codes 20-23.** The runner tests cover each. Mutant Mg (no + founder variable check) fails the `founderCheck` unit test, and the runner test that expects + exit 20 hangs instead; I killed it after four minutes. +- **Launcher check order.** identity, not-a-role, instance-not-listed, + instance-running, harness and role contract, model family, capacity, + then the broker's `authorizeLaunch`, all before `start`. Mutant Md + (no instance-running check) fails two cli tests, Me (no + `authorizeLaunch`) fails one. +- **`recover()`.** It SIGKILLs a live leftover whose start time matches and + whose cmdline is `unshare`, rebinds the run with its recorded launch and + ends it `host-lost`; a rebind refused with `run-ended` is logged and + skipped. Mutant Mn (no `endRun`) fails both host-died-hard tests, and so + does Mc. Mutant Mi (no SIGKILL) survives; see note 5. +- **The 30 s close.** `probe/close-kill.mjs` SIGSTOPs a claimed PM's + runner from outside the namespace and closes the host + (`probe/close-kill.txt`): `host.close -> 0 after 30033 ms`, the session + and runner are both gone, the launch log has one end line + (`killed`, `SIGKILL`) and `sessions.json` is empty. Mutant Mh (no + SIGKILL) survives, so only the probe shows it. +- **Broker.** `bindLaunch` records the run only after its checks, and a + rebind of an ended run refuses with `run-ended` + (`broker.mjs:160-193`, `:203`). Mutant Mb (record first) and Mc (no + `run-ended`) each fail "a restarted broker refuses to rebind an ended + run; a refused rebind leaves the run unbound". Mutant Mm (no `launch-revoked`) fails "launches off refuses + role.launch with launch-revoked until launches on". No new socket verb, + event kind or schema change: the new ops are in `process.mjs`'s + `LAUNCH_OPS` only. +- **Claude Code hook.** The bundle writes + `timeout -k 2 10 || exit 2` with hook timeout 20, and + the adapter never passes `--bare`. That matches S0 lines 1-4. Mutant Mf + (no `|| exit 2`) fails the bundle test. +- **Gate spellings.** Every escape I tried is denied except the dangling + link (`probe/gate-edges.txt`): `sub/../../x`, `@/etc/passwd`, `~/x`, + `file:///etc/passwd`, a symlink to `/etc`, absolute paths, `Glob /etc/*`, + `Glob` with path `/`, `Grep /etc`, `Bash`, `WebFetch`, a foreign `mcp__` + prefix, the bare typed name for Claude Code, and a non-string path. Pi + `read " /etc/passwd"` is allowed, and Pi resolves it the same way, as a + relative path under the workspace. `find "{..,x}/*"` and `Grep` glob + `../*` are allowed, and they're harmless: fd and rg filter files under + the search root and can't leave it. Mutant Ml (no `..` check on glob patterns) fails "glob + patterns stay inside the workspace". +- **`--no-approve`.** Both `adapters/pi/adapter.sh` and + `scripts/agent-host-dev.sh` pass it, which matches Filbert's table. +- **Adapter stdin** is `/dev/null` (the runner spawns it with + `["ignore", "pipe", errFd]`), and the session environment is the + `ENV_ALLOW` list. + +## Mutants + +`mut/summary.txt` has the raw lines; `mut/parse.sh` reads the outputs. + +| Mutant | Change | Result | +|---|---|---| +| Ma | signal handlers installed after setup | harness 42/3, cli 75/2 | +| Mb | broker records the run before its checks | bus 73/1, cli 77/0 | +| Mc | no `run-ended` refusal on rebind | bus 73/1, cli 75/2 | +| Md | no instance-running check | cli 75/2 | +| Me | no `authorizeLaunch` before start | cli 76/1 | +| Mf | hook command without `\|\| exit 2` | harness 44/1 | +| Mg | `founderCheck` finds no founder variables | harness: unit test fails, runner test hangs (killed); cli 77/0 | +| Mh | no SIGKILL after 30 s in `close()` | cli 77/0, **survives** | +| Mi | no SIGKILL in `recover()` | cli 77/0, **survives** | +| Mj | no 4096-byte stdin cap in the runner | harness 45/0, **survives** | +| Mk | no `LINE_MAX` on `launch.sock` | cli 77/0, **survives** | +| Ml | no `..` check on glob patterns | harness 44/1 | +| Mm | no `launch-revoked` refusal | bus 73/1, cli 77/0 | +| Mn | `recover()` doesn't end the run | cli 75/2 | + +## Notes (not blocking) + +1. **`--restricted` does more than the README says.** `probe/claude-memory.sh` + plants marker lines in `~/.claude/CLAUDE.md`, a parent `CLAUDE.md`, the + workspace `CLAUDE.md` and the auto-memory file, and runs the candidate + adapter against the mock API (`probe/claude-memory.txt`). With + `--restricted`, none of the four markers reaches the request and the + generated prompt does. Without it, all four reach it. So `--restricted` + is what keeps founder and repository memory out of the session's + prompt, and with R3 it is the layer that holds for Claude Code. The + README ("no test treats it as the layer that holds") and the adapter + comment should say so, and a test should fail if the adapter drops the + flag. +2. **`launchPm` skips `authorizeLaunch`.** `mosaic launches off` doesn't + stop `bus start --pm`. That looks intended (the human launches the PM), + but one README sentence would say so. +3. **A bad policy file is an uncaught rejection.** `runner.mjs:268` reads + and parses the policy outside the `try` at `:260`, so a missing or + malformed one exits 1 with a stack trace instead of the usage exit. +4. **The system prompt is in argv** for both adapters + (`--system-prompt "$PROMPT_CONTENT"`), readable in `/proc//cmdline` + by the same UID. The PID namespace hides it from other sessions. It + holds no secret today; noting it for when it might. +5. **Test gaps.** Four mutants survive the suites: Mh, Mi, Mj and Mk + (table above). The code does the right thing in each as far as I + checked (`close-kill.txt` for Mh), but no test would notice it going. + For Mi, the host-died-hard test checks that the old session is gone once + the next host is up, and it is gone even without the kill; I didn't + trace why. A leftover kept alive (a SIGSTOPped runner, as in + `close-kill.mjs`) would separate the two. Mg is caught by the unit test, + but its runner-level test hangs rather than fails; a timeout on it would + turn that into a failure. With R1, R2 and R3 fixed, a test for each + belongs in the same round. + +## Files + +- `review-r1.md`, this file. +- `out/`: the suite outputs and `summary.txt`. +- `probe/desync.mjs`, `desync.txt`: R1. +- `probe/close-hang.mjs`, `close-hang.txt`: R2. +- `probe/close-kill.mjs`, `close-kill.txt`: the 30 s SIGKILL in `close()`. +- `probe/dangling-write.mjs`, `dangling-write.txt`, `pi-dangling.sh`, + `pi-dangling.txt`, `claude-dangling.sh`, `claude-dangling.txt`: R3. +- `probe/gate-edges.mjs`, `gate-edges.txt`: gate spellings. +- `probe/claude-memory.sh`, `claude-memory.txt`: note 1. +- `probe/mock-api.mjs`: the mock Messages API. +- `mut/`: `mutate.py`, `run.sh`, `all.sh`, `parse.sh`, each mutant's + output and `summary.txt`.