diff --git a/.github/workflows/action-boundary-vectors.yml b/.github/workflows/action-boundary-vectors.yml index 4ec45081..07a99cee 100644 --- a/.github/workflows/action-boundary-vectors.yml +++ b/.github/workflows/action-boundary-vectors.yml @@ -6,9 +6,11 @@ on: - "pilots/action-boundary/**" - "protocol/canonicalJson.js" - "docs/action-dispatch-matrix.json" + - "docs/action-dispatch-mutants.json" - "docs/dispatch-matrix.html" - "docs/dispatch-matrix.js" - "test/action-boundary-dispatch-matrix.test.js" + - "test/action-boundary-mutation-proof.test.js" - "test/dispatch-matrix-view.test.js" - ".github/workflows/action-boundary-vectors.yml" pull_request: @@ -16,9 +18,11 @@ on: - "pilots/action-boundary/**" - "protocol/canonicalJson.js" - "docs/action-dispatch-matrix.json" + - "docs/action-dispatch-mutants.json" - "docs/dispatch-matrix.html" - "docs/dispatch-matrix.js" - "test/action-boundary-dispatch-matrix.test.js" + - "test/action-boundary-mutation-proof.test.js" - "test/dispatch-matrix-view.test.js" - ".github/workflows/action-boundary-vectors.yml" @@ -41,5 +45,7 @@ jobs: - run: python3 pilots/action-boundary/verify_vectors.py - run: node pilots/action-boundary/dispatch-matrix.mjs --check - run: python3 pilots/action-boundary/verify_dispatch_matrix.py docs/action-dispatch-matrix.json + - run: node pilots/action-boundary/dispatch-mutants.mjs --check - run: node --test test/action-boundary-dispatch-matrix.test.js + - run: node --test test/action-boundary-mutation-proof.test.js - run: node --test test/dispatch-matrix-view.test.js diff --git a/docs/action-dispatch-mutants.json b/docs/action-dispatch-mutants.json new file mode 100644 index 00000000..0db43632 --- /dev/null +++ b/docs/action-dispatch-mutants.json @@ -0,0 +1,365 @@ +{ + "protocol": "aml-action-dispatch-mutants/1", + "provenance": "project-authored exact source substitutions and local callbacks", + "limits": "Selected mutations and finite fixtures only. No external side effect, arbitrary bug coverage, outside witness, or formal proof of production safety.", + "boundary_source_sha256": "4a7d72c4df073c669312f614cad109ea487772c852e4cb00880e566fc9ad329f", + "matrix_report_sha256": "3622583d7f5184e43824c3976a589425f9264b8d3e25ae2c9a784841b8d70fa9", + "matrix_vectors": 192, + "resource_probe_reference": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + }, + "summary": { + "selected_mutants": 7, + "matrix_detected": 6, + "additional_probe_detected": 1, + "undetected": 0 + }, + "mutations": [ + { + "id": "approval_digest_bypass", + "change": "Ignore an approval digest mismatch", + "matrix_changed_rows": 8, + "first_changed_vector": { + "index": 112, + "axes": { + "policy": "requires_approval", + "approval": "mismatch", + "claim": "accept", + "callback": "return" + }, + "reference": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "blocked", + "reason": "approval_missing_or_mismatched" + }, + "approval_requests": 1, + "claim_attempts": 0, + "claims_consumed": 0, + "callback_count": 0, + "fixture_writes": 0, + "callback_target": null, + "caller_target_after": "team@example.test", + "result_target": null + }, + "mutant": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "dispatched", + "reason": "host_dispatch_returned" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 1, + "fixture_writes": 0, + "callback_target": "team@example.test", + "caller_target_after": "team@example.test", + "result_target": "team@example.test" + } + }, + "resource_probe_changed": false, + "resource_probe_result": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + } + }, + { + "id": "one_use_claim_bypass", + "change": "Skip the host's one-use approval claim", + "matrix_changed_rows": 16, + "first_changed_vector": { + "index": 96, + "axes": { + "policy": "requires_approval", + "approval": "match", + "claim": "accept", + "callback": "return" + }, + "reference": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "dispatched", + "reason": "host_dispatch_returned" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 1, + "fixture_writes": 0, + "callback_target": "team@example.test", + "caller_target_after": "team@example.test", + "result_target": "team@example.test" + }, + "mutant": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "dispatched", + "reason": "host_dispatch_returned" + }, + "approval_requests": 1, + "claim_attempts": 0, + "claims_consumed": 0, + "callback_count": 1, + "fixture_writes": 0, + "callback_target": "team@example.test", + "caller_target_after": "team@example.test", + "result_target": "team@example.test" + } + }, + "resource_probe_changed": false, + "resource_probe_result": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + } + }, + { + "id": "post_approval_policy_bypass", + "change": "Skip policy recheck after the approval callback", + "matrix_changed_rows": 8, + "first_changed_vector": { + "index": 128, + "axes": { + "policy": "requires_approval", + "approval": "revoke", + "claim": "accept", + "callback": "return" + }, + "reference": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "blocked", + "reason": "policy_changed" + }, + "approval_requests": 1, + "claim_attempts": 0, + "claims_consumed": 0, + "callback_count": 0, + "fixture_writes": 0, + "callback_target": null, + "caller_target_after": "team@example.test", + "result_target": null + }, + "mutant": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "blocked", + "reason": "policy_changed" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 0, + "fixture_writes": 0, + "callback_target": null, + "caller_target_after": "team@example.test", + "result_target": null + } + }, + "resource_probe_changed": false, + "resource_probe_result": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + } + }, + { + "id": "post_claim_policy_bypass", + "change": "Skip policy recheck after the claim callback", + "matrix_changed_rows": 4, + "first_changed_vector": { + "index": 102, + "axes": { + "policy": "requires_approval", + "approval": "match", + "claim": "revoke_after_consume", + "callback": "return" + }, + "reference": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "blocked", + "reason": "policy_changed" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 0, + "fixture_writes": 0, + "callback_target": null, + "caller_target_after": "team@example.test", + "result_target": null + }, + "mutant": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "dispatched", + "reason": "host_dispatch_returned" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 1, + "fixture_writes": 0, + "callback_target": "team@example.test", + "caller_target_after": "team@example.test", + "result_target": "team@example.test" + } + }, + "resource_probe_changed": false, + "resource_probe_result": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + } + }, + { + "id": "mutable_dispatch_target", + "change": "Pass the caller's mutable object to the host callback", + "matrix_changed_rows": 2, + "first_changed_vector": { + "index": 136, + "axes": { + "policy": "requires_approval", + "approval": "mutate_caller", + "claim": "accept", + "callback": "return" + }, + "reference": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "dispatched", + "reason": "host_dispatch_returned" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 1, + "fixture_writes": 0, + "callback_target": "team@example.test", + "caller_target_after": "other@example.test", + "result_target": "team@example.test" + }, + "mutant": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "requires_approval", + "execution_status": "dispatched", + "reason": "host_dispatch_returned" + }, + "approval_requests": 1, + "claim_attempts": 1, + "claims_consumed": 1, + "callback_count": 1, + "fixture_writes": 0, + "callback_target": "other@example.test", + "caller_target_after": "other@example.test", + "result_target": "other@example.test" + } + }, + "resource_probe_changed": false, + "resource_probe_result": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + } + }, + { + "id": "false_blocked_receipt", + "change": "Call an uncertain callback failure blocked", + "matrix_changed_rows": 26, + "first_changed_vector": { + "index": 49, + "axes": { + "policy": "allow", + "approval": "match", + "claim": "accept", + "callback": "throw_after_write" + }, + "reference": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "allow", + "execution_status": "unknown", + "reason": "host_dispatch_error" + }, + "approval_requests": 0, + "claim_attempts": 0, + "claims_consumed": 0, + "callback_count": 1, + "fixture_writes": 1, + "callback_target": "team@example.test", + "caller_target_after": "team@example.test", + "result_target": null + }, + "mutant": { + "receipt": { + "protocol": "aml-action-dispatch/1", + "proposal_sha256": "2b22778cd386ab9c452f9ddf4f9f4eee57ea41a89721ac24efdf761946178813", + "policy_decision": "allow", + "execution_status": "blocked", + "reason": "host_dispatch_error" + }, + "approval_requests": 0, + "claim_attempts": 0, + "claims_consumed": 0, + "callback_count": 1, + "fixture_writes": 1, + "callback_target": "team@example.test", + "caller_target_after": "team@example.test", + "result_target": null + } + }, + "resource_probe_changed": false, + "resource_probe_result": { + "execution_status": "blocked", + "reason": "no_exact_rule", + "policy_decision": "deny", + "callback_count": 0 + } + }, + { + "id": "resource_wildcard", + "change": "Accept a policy rule for another resource", + "matrix_changed_rows": 0, + "first_changed_vector": null, + "resource_probe_changed": true, + "resource_probe_result": { + "execution_status": "dispatched", + "reason": "host_dispatch_returned", + "policy_decision": "allow", + "callback_count": 1 + } + } + ] +} diff --git a/docs/dispatch-matrix.html b/docs/dispatch-matrix.html index 4917a53e..af27dc20 100644 --- a/docs/dispatch-matrix.html +++ b/docs/dispatch-matrix.html @@ -15,8 +15,8 @@ .section-heading{display:flex;justify-content:space-between;align-items:end;gap:28px;margin-bottom:23px}.section-heading h2{font-size:clamp(2.3rem,4vw,4.1rem);letter-spacing:-.06em;line-height:1;margin:8px 0 0}.section-heading p{max-width:460px;color:var(--muted);margin:0;font-size:.94rem}.lenses{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin-bottom:72px}.lens{text-align:left;border:1px solid var(--line);border-radius:17px;background:linear-gradient(145deg,#173746,#102434);color:var(--ink);padding:23px;min-height:196px;transition:transform .18s,border-color .18s}.lens:hover{transform:translateY(-3px);border-color:var(--sky)}.lens small{color:var(--sky);font:800 .7rem ui-monospace,monospace;letter-spacing:.13em}.lens strong{display:block;font-size:1.36rem;letter-spacing:-.03em;line-height:1.17;margin:16px 0 8px}.lens span{color:var(--muted);font-size:.87rem} .board{background:#0b1d2b;border:1px solid var(--line);border-radius:24px;padding:28px;margin-bottom:28px}.board-head{display:flex;justify-content:space-between;gap:20px;align-items:end;flex-wrap:wrap}.board-head h2{font-size:clamp(2rem,3.5vw,3rem);letter-spacing:-.05em;margin:7px 0 0}.board-head p{color:var(--muted);margin:0;max-width:630px;font-size:.88rem}.filters{display:flex;gap:15px;flex-wrap:wrap;align-items:end;border-block:1px solid var(--line);padding:19px 0;margin:23px 0}.filters label{display:grid;gap:6px;color:var(--sky);font-size:.75rem;letter-spacing:.08em;font-weight:800;text-transform:uppercase;min-width:178px}.filters select{background:#0a1723;border:1px solid #6a8793;color:var(--ink);padding:10px 12px;border-radius:9px;font-size:.88rem;min-width:178px}.filters output{margin-left:auto;color:var(--muted);font-size:.83rem}.board-key{display:flex;gap:10px 19px;flex-wrap:wrap;align-items:center;margin:0 0 18px;color:var(--muted);font-size:.77rem}.key{display:inline-flex;align-items:center;gap:6px}.key i{display:inline-block;width:12px;height:12px;border-radius:4px}.key[data-status=dispatched] i,.vector[data-status=dispatched]{background:#5dd1a5}.key[data-status=unknown] i,.vector[data-status=unknown]{background:#f0bf73}.key[data-status=blocked] i,.vector[data-status=blocked]{background:#c88b95}.matrix-scroll{overflow-x:auto;padding-bottom:10px;scrollbar-color:#547786 transparent}.matrix-grid{display:grid;grid-template-columns:repeat(4,minmax(255px,1fr));gap:13px;min-width:1080px}.policy-column{border:1px solid var(--line);border-radius:14px;background:#112b3a;padding:15px}.policy-column h3{text-transform:capitalize;font-size:1.25rem;margin:0 0 12px;letter-spacing:-.025em}.approval-group{border-top:1px solid #3e6170;padding:10px 0 11px}.approval-group h4{margin:0 0 8px;text-transform:capitalize;color:#c4d9dd;font:700 .72rem ui-monospace,monospace}.cells{display:grid;grid-template-columns:repeat(8,minmax(0,1fr));gap:4px;min-height:26px}.vector{border:0;border-radius:6px;min-width:0;height:26px;color:#091622;font-weight:900;font-size:.81rem;line-height:1}.vector:hover,.vector[aria-pressed=true]{box-shadow:0 0 0 3px var(--ink),0 0 0 5px #183a48;z-index:1}.empty{grid-column:1/-1;color:#9fb2bb;font-size:.72rem}.board-foot{color:var(--muted);font-size:.78rem;margin:16px 0 0} .case{border:1px solid #5f888b;border-radius:24px;background:linear-gradient(135deg,#153944,#0d2332 55%,#172c38);padding:clamp(22px,4vw,42px);margin-bottom:75px;scroll-margin-top:24px}.case[hidden]{display:none}.case-top{display:flex;align-items:start;justify-content:space-between;gap:20px;flex-wrap:wrap}.case h2{font-size:clamp(1.75rem,3.1vw,3rem);line-height:1.08;letter-spacing:-.045em;margin:12px 0 10px;text-transform:capitalize}.badge{display:inline-block;font:900 .83rem ui-monospace,monospace;letter-spacing:.06em;padding:9px 13px;border-radius:7px;background:#c88b95;color:#091622}.badge[data-status=dispatched]{background:#5dd1a5}.badge[data-status=unknown]{background:#f0bf73}.reason{font:800 .78rem ui-monospace,monospace;color:var(--sky);overflow-wrap:anywhere}.case-summary{max-width:800px;color:#dce8e8;font-size:1.04rem;margin:16px 0 30px}.path{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));list-style:none;padding:0;margin:0;gap:9px}.path-item{border:1px solid var(--line);background:#091c2a;border-radius:13px;padding:16px;min-height:118px}.path-label{display:block;color:var(--sky);font:800 .68rem ui-monospace,monospace;letter-spacing:.09em}.path strong{display:block;margin-top:14px;font-size:.92rem;line-height:1.35;overflow-wrap:anywhere}.bindings{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:20px 0}.binding{border:1px solid var(--line);border-radius:12px;padding:15px 17px}.binding span{display:block;font:800 .67rem ui-monospace,monospace;letter-spacing:.1em;color:var(--muted)}.binding strong{display:block;margin-top:7px;overflow-wrap:anywhere;font-size:.93rem}.digest{color:var(--muted);font-size:.77rem;overflow-wrap:anywhere}.digest code{color:var(--sky)}.case-actions{display:flex;gap:15px;align-items:center;flex-wrap:wrap;margin-top:23px}.copy{border:1px solid var(--sky);background:#0e3540;color:var(--ink);border-radius:9px;padding:10px 15px;font-weight:800}.case-actions a{font-size:.82rem;font-weight:750}.case-actions output{color:var(--mint);font-size:.8rem}details{margin-top:17px}summary{cursor:pointer;color:var(--sky);font-size:.83rem;font-weight:750}pre{white-space:pre-wrap;overflow-wrap:anywhere;padding:18px;border-radius:11px;background:#071721;border:1px solid var(--line);font:12px/1.55 ui-monospace,monospace;max-height:440px;overflow:auto} - .method{border-top:1px solid var(--line);padding:35px 0 60px;display:grid;grid-template-columns:.8fr 1.2fr;gap:30px}.method h2{margin:0;font-size:clamp(1.9rem,3vw,2.7rem);letter-spacing:-.04em}.method p{color:var(--muted);margin:0 0 14px}.method code{color:var(--mint);overflow-wrap:anywhere}.method a{font-weight:700}.foot{border-top:1px solid var(--line);padding:25px 0 55px;color:var(--muted);font-size:.78rem} - @media(max-width:850px){.hero{grid-template-columns:1fr;gap:25px;padding:50px 0}.totals{grid-template-columns:repeat(2,1fr)}.lenses{grid-template-columns:1fr}.lens{min-height:0}.path{grid-template-columns:repeat(2,1fr)}.method{grid-template-columns:1fr}} + .challenge{border:1px solid #8c9c7d;border-radius:24px;background:radial-gradient(circle at 94% 4%,#bd905e31,transparent 42%),linear-gradient(135deg,#203b3a,#172c35 64%,#293138);padding:clamp(25px,4vw,48px);margin:0 0 75px}.challenge-grid{display:grid;grid-template-columns:1fr 1fr;gap:clamp(25px,5vw,70px);align-items:center}.challenge h2{font-size:clamp(2.25rem,4vw,4.5rem);letter-spacing:-.06em;line-height:1.02;margin:12px 0 17px}.challenge p{color:#dce6e3;margin:0 0 17px;max-width:590px}.challenge a{font-weight:800}.challenge-signal{border-left:2px solid var(--amber);padding-left:24px}.challenge-numbers{display:flex;gap:16px;margin-bottom:22px}.challenge-numbers div{flex:1;border:1px solid #71827e;background:#102936b8;border-radius:15px;padding:16px}.challenge-numbers strong{display:block;font-size:clamp(2.4rem,5vw,4.5rem);letter-spacing:-.08em;line-height:1;color:var(--mint)}.challenge-numbers div:last-child strong{color:var(--amber)}.challenge-numbers span{display:block;margin-top:7px;color:#d0dddd;font:.77rem/1.35 ui-monospace,monospace}.challenge-signal p{font-size:.91rem;margin-bottom:0}.method{border-top:1px solid var(--line);padding:35px 0 60px;display:grid;grid-template-columns:.8fr 1.2fr;gap:30px}.method h2{margin:0;font-size:clamp(1.9rem,3vw,2.7rem);letter-spacing:-.04em}.method p{color:var(--muted);margin:0 0 14px}.method code{color:var(--mint);overflow-wrap:anywhere}.method a{font-weight:700}.foot{border-top:1px solid var(--line);padding:25px 0 55px;color:var(--muted);font-size:.78rem} + @media(max-width:850px){.hero{grid-template-columns:1fr;gap:25px;padding:50px 0}.totals{grid-template-columns:repeat(2,1fr)}.lenses{grid-template-columns:1fr}.lens{min-height:0}.path{grid-template-columns:repeat(2,1fr)}.challenge-grid,.method{grid-template-columns:1fr}} @media(max-width:520px){.wrap{width:min(100% - 28px,1300px)}.top{align-items:flex-start;flex-direction:column}.top nav{gap:11px}.hero{padding-top:43px}.hero h1{font-size:clamp(3.55rem,15vw,5.8rem)}.hero-aside{padding-left:16px}.totals{gap:7px;margin-bottom:55px}.total{padding:16px;min-height:127px}.total strong{font-size:2.7rem}.section-heading{display:block}.section-heading p{margin-top:13px}.board{padding:17px}.filters label,.filters select{flex:1;min-width:135px}.filters output{width:100%;margin-left:0}.path,.bindings{grid-template-columns:1fr}.path-item{min-height:0}.case{margin-bottom:55px}} .cell-placeholder{height:26px;border:1px dashed #426170;border-radius:6px;opacity:.55} @media(prefers-reduced-motion:reduce){html{scroll-behavior:auto}.lens{transition:none}} @@ -31,7 +31,8 @@
THREE IMPORTANT EDGES

What a receipt hides.

These counts describe the finite matrix, not a production rate. Each card opens one exact example below.

THE COMPLETE CROSS-PRODUCT

Every local path.

Each square is one vector. Columns are policy states; rows are approval-hook behaviors. In each row, squares follow the four claim behaviors, with a returning callback then a throwing callback. Select one to inspect its path.

Loading vectors…
DispatchedUnknownBlocked↗ callback returns · ! callback throws

Colors are supported by text labels and each square has an accessible name. Horizontal scrolling reveals all four policies on narrow screens.

-

What was actually checked?

The Node runner calls the reference dispatchAction for all 192 local fixtures. A separate project-authored Python oracle checks each row, including receipt, digest, claim consumption, callback target, and marker write. CI rejects four deliberately falsified reports.

Reproduce with node pilots/action-boundary/dispatch-matrix.mjs --check and python3 pilots/action-boundary/verify_dispatch_matrix.py docs/action-dispatch-matrix.json. Read the method and limits ↗

Scope: Finite project-authored callbacks, no human approval, provider call, arbitrary interleavings, real delivery, outside witness, or formal proof of production safety. A browser view of the report does not independently verify its provenance. Never silently retry an unknown result.

+
SOURCE MUTATION CHALLENGE

The case the matrix missed.

We weakened seven guards in the actual reference source, one at a time. The 192-case matrix detected six. The seventh accepted a rule for the wrong resource, yet all 192 rows looked unchanged.

Inspect the source-bound results ↗
6CHANGES CAUGHT
BY THE MATRIX
1GAP EXPOSED
BY A NEW PROBE

A proposal for outside@example.test meets a rule for team@example.test. The real boundary blocks before the host callback; the weakened version enters it once. The new probe keeps that distinction under test.

+

What was actually checked?

The Node runner calls the reference dispatchAction for all 192 local fixtures. A separate project-authored Python oracle checks each row, including receipt, digest, claim consumption, callback target, and marker write. CI rejects four deliberately falsified reports.

Reproduce with node pilots/action-boundary/dispatch-matrix.mjs --check, python3 pilots/action-boundary/verify_dispatch_matrix.py docs/action-dispatch-matrix.json, and node pilots/action-boundary/dispatch-mutants.mjs --check. Read the method and limits ↗

Scope: Finite project-authored callbacks and selected mutations, no human approval, provider call, arbitrary interleavings, real delivery, outside witness, or formal proof of production safety. A browser view of the report does not independently verify its provenance. Never silently retry an unknown result.

diff --git a/pilots/action-boundary/DISPATCH_MATRIX.md b/pilots/action-boundary/DISPATCH_MATRIX.md index 6c8c5016..495a72b5 100644 --- a/pilots/action-boundary/DISPATCH_MATRIX.md +++ b/pilots/action-boundary/DISPATCH_MATRIX.md @@ -6,6 +6,8 @@ This project-authored check enumerates **192 combinations** of four host policy node pilots/action-boundary/dispatch-matrix.mjs --check python3 pilots/action-boundary/verify_dispatch_matrix.py docs/action-dispatch-matrix.json node --test test/action-boundary-dispatch-matrix.test.js +node pilots/action-boundary/dispatch-mutants.mjs --check +node --test test/action-boundary-mutation-proof.test.js ``` The [complete checked-in matrix](../../docs/action-dispatch-matrix.json) is deterministic. To regenerate it after an intentional behavior change, run `node pilots/action-boundary/dispatch-matrix.mjs --write`, review the changed rows, and run the Python oracle. The test deliberately changes a blocked status, callback count, digest, and scope claim in separate copies; all four must fail verification. CI runs both languages. @@ -16,4 +18,12 @@ The checked matrix has 26 `dispatched`, 26 `unknown`, and 140 `blocked` receipts The [interactive atlas](../../docs/dispatch-matrix.html) derives three additional counts from these rows: **8 blocked vectors with consumed claims**, **26 unknown vectors after a local marker write**, and **2 callback vectors with a changed caller object but the original frozen target**. Filters and deep links reveal each exact receipt and counters. These are proportions of a deliberately selected finite cross-product, not estimates of real-world frequency. -**Limits:** This is exhaustive only over the declared finite axes and one local callback attempt per vector. It does not explore arbitrary interleavings, crashes, malicious host hooks, providers, network effects, human authorization, or delivery. The Python oracle is independently coded but project-authored; it is not an outside witness or formal proof of production safety. Real hosts must own policy, authentication, one-use claims, operation idempotency, and reconciliation. Never silently retry an unknown result. +## Source mutation challenge + +The [checked-in mutation report](../../docs/action-dispatch-mutants.json) loads seven exact, one-at-a-time substitutions of `boundary.mjs` and reruns the actual dispatch matrix against each changed module. A replacement must match exactly once, so a changed source guard makes the check fail until the substitutions are reviewed. The report records the source and matrix SHA-256 digests, changed-row counts, and the first differing vector with both outcomes. The independent Python oracle checks the unchanged matrix; the mutation comparisons use the checked-in baseline verified by that oracle. + +Six substitutions change matrix rows: bypassing the approval digest check (8 rows), skipping the one-use claim (16), skipping the policy recheck after approval (8), skipping the recheck after claim (4), passing the caller's mutable object to the callback (2), and calling an uncertain callback failure blocked (26). These counts are differences from the baseline, not real-world rates or independent defect counts. + +The seventh substitution changes resource matching to accept a rule for any resource. It changes **zero** of the 192 matrix rows because the matrix has no mismatched-resource fixture. The added local resource-substitution probe presents a proposal for `outside@example.test` with a rule only for `team@example.test`: the reference boundary blocks with `no_exact_rule` and zero callback entries, while the mutant dispatches once. CI runs the probe against the unchanged boundary as a direct regression test and regenerates the mutation report. Reproduce the report with `node pilots/action-boundary/dispatch-mutants.mjs --check`; after an intentional source or fixture change, review `--write` output and rerun both checks. + +**Limits:** This is exhaustive only over the declared finite axes and one local callback attempt per vector. The mutation challenge samples seven chosen guard changes, not arbitrary bugs or equivalent mutants. It does not explore arbitrary interleavings, crashes, malicious host hooks, providers, network effects, human authorization, or delivery. The Python oracle is independently coded but project-authored; it is not an outside witness or formal proof of production safety. Real hosts must own policy, authentication, one-use claims, operation idempotency, and reconciliation. Never silently retry an unknown result. diff --git a/pilots/action-boundary/README.md b/pilots/action-boundary/README.md index 2a6c4d2e..84c5ae2d 100644 --- a/pilots/action-boundary/README.md +++ b/pilots/action-boundary/README.md @@ -16,6 +16,8 @@ Agents can propose a tool call; the host must decide whether to dispatch it. Thi [**Explore the proof atlas**](../../docs/dispatch-matrix.html) to filter all 192 results, inspect the local effects behind one receipt, and share an exact vector. The page presents the checked-in report; it does not itself provide an outside verification. +[**Challenge seven source guards**](DISPATCH_MATRIX.md#source-mutation-challenge) by applying exact one-at-a-time substitutions to the reference boundary. Six changes alter matrix rows; weakening exact resource matching leaves all 192 rows unchanged. A new local substitution probe exposes that gap and keeps it under a direct regression check. The [mutation report](../../docs/action-dispatch-mutants.json) records source digests and first differing outcomes; it does not measure general defect coverage. + [**Bind all recipients in a mail function tool**](MAIL_GUARDED_TOOL.md) with `createMailGuardedToolExecute` when the host callback accepts To, CC, or BCC. The [seven-case visual comparison](https://aruintelligence.github.io/aml-core/mail-tool-integration.html?case=added_bcc#cases) executes both adapters with in-memory callbacks. It is a narrow host-side integration example, not a live provider or authenticated approval. [**Check the provider request before the send callback**](PREPARED_MAIL_TOOL.md) with `createPreparedMailToolExecute` when the host builds or expands a request from the tool arguments. The [eight-case visual lab](https://aruintelligence.github.io/aml-core/prepared-mail-lab.html?case=route_bcc#cases) executes both timing paths; a BCC added after the earlier gate reaches one simulated provider callback, while the prepared-request gate blocks it under the same intended rule. The sender must use only the prepared fields. No actual message or authenticated approval is involved. diff --git a/pilots/action-boundary/dispatch-matrix.mjs b/pilots/action-boundary/dispatch-matrix.mjs index 36f148c2..ba8d544f 100644 --- a/pilots/action-boundary/dispatch-matrix.mjs +++ b/pilots/action-boundary/dispatch-matrix.mjs @@ -15,7 +15,7 @@ export const axes = Object.freeze({ callback: ["return", "throw_after_write"] }); -async function runVector(policyMode, approvalMode, claimMode, callbackMode) { +async function runVector(dispatch, policyMode, approvalMode, claimMode, callbackMode) { const proposal = { protocol: "aml-proposed-action/1", tool: "send_message", effect: "send", resource: "team@example.test", purpose: "Matrix local callback", arguments: { to: "team@example.test", body: "Build passed." } }; @@ -29,7 +29,7 @@ async function runVector(policyMode, approvalMode, claimMode, callbackMode) { let callbackCount = 0; let fixtureWrites = 0; let callbackTarget = null; - const outcome = await dispatchAction(proposal, { policy, + const outcome = await dispatch(proposal, { policy, requestApproval: async ({ proposal: offered, proposal_sha256 }) => { approvalRequests++; if (approvalMode === "throw") throw new Error("local_approval_error"); @@ -67,11 +67,11 @@ async function runVector(policyMode, approvalMode, claimMode, callbackMode) { result_target: outcome.result?.would_send_to ?? null }; } -export async function buildDispatchMatrix() { +export async function buildDispatchMatrix(dispatch = dispatchAction) { const rows = []; for (const policy of axes.policy) for (const approval of axes.approval) for (const claim of axes.claim) for (const callback of axes.callback) - rows.push(await runVector(policy, approval, claim, callback)); + rows.push(await runVector(dispatch, policy, approval, claim, callback)); return { protocol: "aml-action-dispatch-matrix/1", provenance: "project-authored bounded cross-product of local callbacks", limits: "192 finite combinations. No human approval, provider call, delivery, arbitrary interleaving, or production safety proof.", diff --git a/pilots/action-boundary/dispatch-mutants.mjs b/pilots/action-boundary/dispatch-mutants.mjs new file mode 100644 index 00000000..c7ed4a86 --- /dev/null +++ b/pilots/action-boundary/dispatch-mutants.mjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +// Project-authored fault injection against the actual reference source. These +// selected substitutions measure a finite suite's sensitivity, not safety. +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import { readFile, writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { dispatchAction } from "./boundary.mjs"; +import { buildDispatchMatrix } from "./dispatch-matrix.mjs"; + +const sourceUrl = new URL("./boundary.mjs", import.meta.url); +const matrixUrl = new URL("../../docs/action-dispatch-matrix.json", import.meta.url); +const reportUrl = new URL("../../docs/action-dispatch-mutants.json", import.meta.url); +const digest = bytes => crypto.createHash("sha256").update(bytes).digest("hex"); + +export const mutations = Object.freeze([ + { + id: "approval_digest_bypass", + change: "Ignore an approval digest mismatch", + before: "approval?.approved !== true || approval?.proposal_sha256 !== frozen.digest", + after: "approval?.approved !== true || false" + }, + { + id: "one_use_claim_bypass", + change: "Skip the host's one-use approval claim", + before: 'plan.decision === "requires_approval" && consumeApproval !== undefined', + after: "false && consumeApproval !== undefined" + }, + { + id: "post_approval_policy_bypass", + change: "Skip policy recheck after the approval callback", + before: "rechecked.decision !== plan.decision || rechecked.proposal_sha256 !== frozen.digest", + after: "false" + }, + { + id: "post_claim_policy_bypass", + change: "Skip policy recheck after the claim callback", + before: "finalPlan.decision !== plan.decision || finalPlan.proposal_sha256 !== frozen.digest", + after: "false" + }, + { + id: "mutable_dispatch_target", + change: "Pass the caller's mutable object to the host callback", + before: "executeTool(JSON.parse(frozen.bytes))", + after: "executeTool(proposal)" + }, + { + id: "false_blocked_receipt", + change: "Call an uncertain callback failure blocked", + before: 'receipt("unknown", "host_dispatch_error")', + after: 'receipt("blocked", "host_dispatch_error")' + }, + { + id: "resource_wildcard", + change: "Accept a policy rule for another resource", + before: "rule.resource === proposal.resource", + after: "true" + } +]); + +function replaceExactlyOnce(source, before, after) { + assert.equal(source.split(before).length, 2, `Expected exactly one source occurrence: ${before}`); + return source.replace(before, after); +} + +async function loadMutant(source, mutation) { + const changed = replaceExactlyOnce(source, mutation.before, mutation.after); + const canonicalUrl = new URL("../../protocol/canonicalJson.js", import.meta.url); + const selfContained = replaceExactlyOnce(changed, + 'from "../../protocol/canonicalJson.js"', `from "${canonicalUrl.href}"`); + // Each data URL is a distinct ESM module, with no temporary file or host call. + const moduleUrl = `data:text/javascript;base64,${Buffer.from(selfContained).toString("base64")}`; + return (await import(moduleUrl)).dispatchAction; +} + +function outcome(row) { + return { + receipt: row.receipt, approval_requests: row.approval_requests, + claim_attempts: row.claim_attempts, claims_consumed: row.claims_consumed, + callback_count: row.callback_count, fixture_writes: row.fixture_writes, + callback_target: row.callback_target, caller_target_after: row.caller_target_after, + result_target: row.result_target + }; +} + +export async function resourceSubstitutionProbe(dispatch = dispatchAction) { + const proposal = { protocol: "aml-proposed-action/1", tool: "send_message", effect: "send", + resource: "outside@example.test", purpose: "Local resource substitution probe", + arguments: { to: "outside@example.test", body: "Local marker only." } }; + const policy = { protocol: "aml-action-policy/1", rules: [ + { tool: "send_message", effect: "send", resource: "team@example.test", requires_approval: false } + ] }; + let callbackCount = 0; + const { receipt } = await dispatch(proposal, { policy, executeTool: async () => { + callbackCount++; + return { local_marker: true }; + } }); + return { execution_status: receipt.execution_status, reason: receipt.reason, + policy_decision: receipt.policy_decision, callback_count: callbackCount }; +} + +export async function buildDispatchMutants() { + const [source, matrixBytes] = await Promise.all([ + readFile(sourceUrl, "utf8"), readFile(matrixUrl, "utf8") + ]); + const reference = JSON.parse(matrixBytes); + assert.deepEqual(await buildDispatchMatrix(), reference, + "Matrix report is stale; regenerate and verify it before mutation testing"); + const baselineProbe = await resourceSubstitutionProbe(); + assert.deepEqual(baselineProbe, { execution_status: "blocked", reason: "no_exact_rule", + policy_decision: "deny", callback_count: 0 }); + const results = []; + for (const mutation of mutations) { + const dispatch = await loadMutant(source, mutation); + const rows = (await buildDispatchMatrix(dispatch)).rows; + const differences = rows.flatMap((row, index) => + JSON.stringify(row) === JSON.stringify(reference.rows[index]) ? [] : [index]); + const first = differences[0]; + const probe = await resourceSubstitutionProbe(dispatch); + results.push({ id: mutation.id, change: mutation.change, + matrix_changed_rows: differences.length, + first_changed_vector: first === undefined ? null : { + index: first, axes: Object.fromEntries( + ["policy", "approval", "claim", "callback"].map(axis => [axis, rows[first][axis]])), + reference: outcome(reference.rows[first]), mutant: outcome(rows[first]) + }, + resource_probe_changed: JSON.stringify(probe) !== JSON.stringify(baselineProbe), + resource_probe_result: probe }); + } + const summary = { selected_mutants: results.length, + matrix_detected: results.filter(row => row.matrix_changed_rows > 0).length, + additional_probe_detected: results.filter(row => row.matrix_changed_rows === 0 && row.resource_probe_changed).length, + undetected: results.filter(row => row.matrix_changed_rows === 0 && !row.resource_probe_changed).length }; + return { protocol: "aml-action-dispatch-mutants/1", + provenance: "project-authored exact source substitutions and local callbacks", + limits: "Selected mutations and finite fixtures only. No external side effect, arbitrary bug coverage, outside witness, or formal proof of production safety.", + boundary_source_sha256: digest(source), matrix_report_sha256: digest(matrixBytes), + matrix_vectors: reference.rows.length, resource_probe_reference: baselineProbe, + summary, mutations: results }; +} + +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { + const args = process.argv.slice(2); + if (!(args.length === 0 || (args.length === 1 && ["--write", "--check"].includes(args[0])))) { + throw new Error("Use no argument, --write, or --check"); + } + const report = await buildDispatchMutants(); + const json = JSON.stringify(report, null, 2) + "\n"; + if (args[0] === "--write") { + await writeFile(reportUrl, json, "utf8"); + console.log(`Wrote ${report.summary.selected_mutants} selected source mutations.`); + } else if (args[0] === "--check") { + assert.equal(await readFile(reportUrl, "utf8"), json, + "Dispatch mutation report is stale; run --write"); + console.log(`${report.summary.selected_mutants} selected source mutations match the checked-in report.`); + } else process.stdout.write(json); +} diff --git a/test/action-boundary-mutation-proof.test.js b/test/action-boundary-mutation-proof.test.js new file mode 100644 index 00000000..f624fb0e --- /dev/null +++ b/test/action-boundary-mutation-proof.test.js @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { buildDispatchMutants, resourceSubstitutionProbe } from "../pilots/action-boundary/dispatch-mutants.mjs"; + +const checkedIn = JSON.parse(readFileSync( + new URL("../docs/action-dispatch-mutants.json", import.meta.url), "utf8")); + +test("selected source mutations match the checked-in, source-bound report", async () => { + assert.deepEqual(await buildDispatchMutants(), checkedIn); + assert.deepEqual(checkedIn.summary, { selected_mutants: 7, + matrix_detected: 6, additional_probe_detected: 1, undetected: 0 }); + assert.equal(checkedIn.matrix_vectors, 192); + assert.deepEqual(checkedIn.mutations.filter(row => row.matrix_changed_rows === 0) + .map(row => row.id), ["resource_wildcard"]); + assert.equal(checkedIn.mutations.at(-1).resource_probe_changed, true); + assert.equal(checkedIn.mutations.at(-1).resource_probe_result.callback_count, 1); +}); + +test("a rule for another resource never reaches the host callback", async () => { + assert.deepEqual(await resourceSubstitutionProbe(), { + execution_status: "blocked", reason: "no_exact_rule", + policy_decision: "deny", callback_count: 0 + }); +});