Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion docs/AGENT_TOOL_QUICKSTART.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ node pilots/action-boundary/guarded-tool-demo.mjs

The first fixture has an exact policy rule and a **simulated** approval; its callback returns a `would_send_to` object. The second changes the destination and is blocked before the callback. There is no network request, message delivery, or human authentication.

For the thirteen-case host rehearsal, list the case IDs and ordered receipt statuses, then inspect one case:
For the fourteen-case host rehearsal, list the case IDs and ordered receipt statuses, then inspect one case:

```bash
node pilots/action-boundary/guarded-tool-rehearsal.mjs --list
Expand All @@ -26,6 +26,8 @@ The list prints one tab-separated ID and status sequence per fixture; a two-atte

The [host allow case](https://aruintelligence.github.io/aml-core/agent-start.html?case=host_allow#rehearsal) shows an exact rule with `requires_approval: false`: one local callback returns with zero approval requests and zero claim attempts. Only the host can choose that policy; the fixture is not a recommendation to waive approval for a real send.

The [approval hook error case](https://aruintelligence.github.io/aml-core/agent-start.html?case=approval_hook_error#rehearsal) writes one in-memory approval fixture marker just before the hook throws. The tool receipt is `blocked`, with no claim or tool callback. This is not a human approval or a durable grant; a real host must reconcile its authorization store rather than infer that no approval-state change occurred.

On the visual builder, **Copy fixture JSON** copies the selected case in the same shape as `--case`, including its project-authored provenance and scope. Include your checkout commit when sharing a reproduction; the copied fixture is not evidence of a real approval or delivery.

The [declined approval case](https://aruintelligence.github.io/aml-core/agent-start.html?case=approval_declined#rehearsal) requests a fixture decision and receives a refusal. The receipt blocks before any claim or tool callback. A refusal is not a grant; a later attempt needs its own authenticated decision.
Expand Down
4 changes: 2 additions & 2 deletions docs/agent-start.html
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
git rev-parse HEAD
node pilots/action-boundary/guarded-tool-demo.mjs
node pilots/action-boundary/guarded-tool-rehearsal.mjs --check</code><p id="command-status" class="status" role="status" aria-live="polite"></p>
<div class="rehearsal" id="rehearsal" aria-labelledby="rehearsal-title"><div class="rehearsal-head"><div><span id="case-count-label">HOST REHEARSAL / CONTROLLED CASES</span><h3 id="rehearsal-title">Where does the call stop?</h3><p>Choose a condition. Use arrow keys, Home, or End after focusing a case. These are recorded results from the project adapter running local fixtures, with approval and callback counts visible.</p></div><a href="#case-title">Jump to selected result ↓</a><a href="guarded-tool-rehearsal.json">Inspect the complete report ↗</a></div><div id="case-picker" class="case-picker" role="radiogroup" aria-label="Choose a host rehearsal case"></div><div class="case-detail" id="case-panel" role="region" aria-labelledby="case-title" tabindex="0"><span class="eyebrow">OBSERVED IN THE LOCAL REHEARSAL</span><h4 id="case-title" aria-live="polite">Loading the report…</h4><p id="case-change">Checking the project-authored snapshot.</p><div id="case-attempts" class="attempts"></div><div id="case-metrics" class="case-metrics"></div><div class="host-move"><span>HOST DESIGN MOVE</span><p id="case-host-move"></p></div><div class="case-command-row"><span>REPRODUCE OR SHARE THIS CASE</span><div class="case-actions"><button type="button" id="copy-case-command">Copy command</button><button type="button" id="copy-case-link">Copy case link</button><button type="button" id="copy-case-json">Copy fixture JSON</button></div></div><code class="code case-command" id="case-command">node pilots/action-boundary/guarded-tool-rehearsal.mjs --case changed_destination</code><p id="case-copy-status" class="case-copy-status" role="status" aria-live="polite"></p><p class="limit">The hook trace records which fixture hooks ran; it does not expose every internal policy check. A <code>dispatched</code> receipt means the fixture callback returned. An <code>unknown</code> receipt does not mean no side effect occurred. A claim-hook error can leave a grant spent without entering the tool callback. In-memory fixture writes are local markers, not provider delivery. Neither proves real approval, service delivery, or production safety.</p></div></div>
<div class="rehearsal" id="rehearsal" aria-labelledby="rehearsal-title"><div class="rehearsal-head"><div><span id="case-count-label">HOST REHEARSAL / CONTROLLED CASES</span><h3 id="rehearsal-title">Where does the call stop?</h3><p>Choose a condition. Use arrow keys, Home, or End after focusing a case. These are recorded results from the project adapter running local fixtures, with approval and callback counts visible.</p></div><a href="#case-title">Jump to selected result ↓</a><a href="guarded-tool-rehearsal.json">Inspect the complete report ↗</a></div><div id="case-picker" class="case-picker" role="radiogroup" aria-label="Choose a host rehearsal case"></div><div class="case-detail" id="case-panel" role="region" aria-labelledby="case-title" tabindex="0"><span class="eyebrow">OBSERVED IN THE LOCAL REHEARSAL</span><h4 id="case-title" aria-live="polite">Loading the report…</h4><p id="case-change">Checking the project-authored snapshot.</p><div id="case-attempts" class="attempts"></div><div id="case-metrics" class="case-metrics"></div><div class="host-move"><span>HOST DESIGN MOVE</span><p id="case-host-move"></p></div><div class="case-command-row"><span>REPRODUCE OR SHARE THIS CASE</span><div class="case-actions"><button type="button" id="copy-case-command">Copy command</button><button type="button" id="copy-case-link">Copy case link</button><button type="button" id="copy-case-json">Copy fixture JSON</button></div></div><code class="code case-command" id="case-command">node pilots/action-boundary/guarded-tool-rehearsal.mjs --case changed_destination</code><p id="case-copy-status" class="case-copy-status" role="status" aria-live="polite"></p><p class="limit">The hook trace records which fixture hooks ran; it does not expose every internal policy check. A <code>dispatched</code> receipt means the fixture callback returned. An <code>unknown</code> receipt does not mean no side effect occurred. A claim-hook error can leave a grant spent without entering the tool callback. An approval-hook error can leave host authorization state changed. In-memory fixture writes are local markers, not provider delivery. Neither proves real approval, service delivery, or production safety.</p></div></div>
</div></div>
<div class="stage"><div class="number">02</div><div class="content"><div class="code-row"><h2>Move the boundary into your host.</h2><button type="button" class="copy" data-copy="sketch">Copy host sketch</button></div><p>The real host owns the tool identity, effect, resource mapping, policy, authenticated approval, one-use claim, and service call. This sketch deliberately throws until you implement the three host functions. It imports the project preview from your checkout.</p><code id="sketch" class="code sketch">import { createGuardedToolExecute } from "./pilots/action-boundary/guarded-tool.mjs";

Expand Down Expand Up @@ -65,6 +65,6 @@
<div class="next"><a href="action-boundary-studio.html"><span>DESIGN</span><b>Edit proposal and policy ↗</b><small>Export a calculation bundle and recheck it in Node.</small></a><a href="connected-mcp-lab.html"><span>TRACE</span><b>See a connected MCP call ↗</b><small>Compare a misleading tool hint with a host-owned rule.</small></a><a href="action-challenge.html?case=changed-destination#build"><span>CHALLENGE</span><b>Build your own adapter ↗</b><small>Run the public cases outside the reference implementation.</small></a></div>
</main><footer class="foot"><strong>Scope:</strong> This is a project-authored, simulated function-tool path from a preview checkout. It does not intercept every agent tool, authenticate a human, send mail, prove delivery, or certify a production integration. ĀML™ / ĀRU Meaning Language™ is stewarded by ĀRU Intelligence Inc. · <a href="https://github.com/aruintelligence/aml-core/blob/main/CLAIMS.md">Claims and evidence</a>.</footer>
</div>
<script type="module" src="agent-start.js?v=741f7a5face7"></script>
<script type="module" src="agent-start.js?v=7d9c5c2d6fa3"></script>
</body>
</html>
2 changes: 1 addition & 1 deletion docs/agent-start.js
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ if (typeof document !== "undefined" && document.getElementById("rehearsal")) {
}
});

fetch("guarded-tool-rehearsal.json?v=5cad3661cb4c").then(response => {
fetch("guarded-tool-rehearsal.json?v=a2c836a065eb").then(response => {
if (!response.ok) throw new Error("Report request failed");
return response.json();
}).then(report => {
Expand Down
25 changes: 25 additions & 0 deletions docs/guarded-tool-rehearsal.json
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,31 @@
"callback_count": 0,
"fixture_state_changes": 0
},
{
"id": "approval_hook_error",
"title": "Approval hook throws after a local write",
"change": "The approval fixture writes an in-memory marker, then throws before returning a decision. No one-use claim or tool callback runs.",
"host_move": "Reconcile host authorization records after an approval hook error. A blocked tool receipt does not prove that the approval system recorded nothing; require a fresh verified decision for another attempt.",
"destination": "team@example.test",
"attempts": [
{
"policy_decision": "requires_approval",
"proposal_sha256": "f416748534da5982a029d03d6db48adf00b4138e0785d38857f0b1734923776c",
"execution_status": "blocked",
"reason": "approval_error",
"host_hook_events": [
"Approval requested",
"In-memory approval fixture marker written",
"Approval hook threw"
]
}
],
"approval_requests": 1,
"claim_attempts": 0,
"claims_consumed": 0,
"callback_count": 0,
"fixture_state_changes": 1
},
{
"id": "approval_mismatch",
"title": "Approval for another proposal",
Expand Down
18 changes: 18 additions & 0 deletions pilots/action-boundary/guarded-tool-rehearsal.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ const fixtures = [
host_move: "Treat a refusal as final for this attempt. A later attempt needs a new authenticated approval decision; never turn a refusal into an implicit grant.",
options: { approval: "decline" }
},
{
id: "approval_hook_error", title: "Approval hook throws after a local write",
change: "The approval fixture writes an in-memory marker, then throws before returning a decision. No one-use claim or tool callback runs.",
host_move: "Reconcile host authorization records after an approval hook error. A blocked tool receipt does not prove that the approval system recorded nothing; require a fresh verified decision for another attempt.",
options: { approval: "throw_after_write" }
},
{
id: "approval_mismatch", title: "Approval for another proposal",
change: "The approval fixture returns a different proposal digest.",
Expand Down Expand Up @@ -116,6 +122,12 @@ async function runFixture({ id, title, change, host_move, options }) {
hookEvents.push("Fixture approval declined");
return { approved: false };
}
if (options.approval === "throw_after_write") {
fixtureStateChanges++;
hookEvents.push("In-memory approval fixture marker written");
hookEvents.push("Approval hook threw");
throw new Error("simulated_approval_response_error");
}
hookEvents.push(options.approval === "mismatch" ? "Different digest returned" : "Matching digest returned");
return { approved: true, proposal_sha256: options.approval === "mismatch" ? "0".repeat(64) : proposal_sha256 };
};
Expand Down Expand Up @@ -191,6 +203,7 @@ export function assertGuardedToolRehearsal(report) {
changed_destination: [["deny", "blocked", "no_exact_rule"], 0],
approval_unavailable: [["requires_approval", "blocked", "approval_unavailable"], 0],
approval_declined: [["requires_approval", "blocked", "approval_missing_or_mismatched"], 0],
approval_hook_error: [["requires_approval", "blocked", "approval_error"], 0],
approval_mismatch: [["requires_approval", "blocked", "approval_missing_or_mismatched"], 0],
approval_reused: [
["requires_approval", "dispatched", "host_dispatch_returned"],
Expand Down Expand Up @@ -232,6 +245,11 @@ export function assertGuardedToolRehearsal(report) {
assert.equal(byId.approval_declined.claim_attempts, 0);
assert.deepEqual(byId.approval_declined.attempts[0].host_hook_events,
["Approval requested", "Fixture approval declined"]);
assert.equal(byId.approval_hook_error.approval_requests, 1);
assert.equal(byId.approval_hook_error.claim_attempts, 0);
assert.equal(byId.approval_hook_error.fixture_state_changes, 1);
assert.deepEqual(byId.approval_hook_error.attempts[0].host_hook_events,
["Approval requested", "In-memory approval fixture marker written", "Approval hook threw"]);
assert.equal(byId.approval_mismatch.claim_attempts, 0);
assert.equal(byId.policy_revoked.claim_attempts, 0);
assert.equal(byId.approval_reused.claim_attempts, 2);
Expand Down
1 change: 1 addition & 0 deletions test/agent-start.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ test("the page labels the report as a fixture and offers the exact recheck comma
assert.match(page, /consumeApproval: \(input, context\) => hostConsumeGrantOnce/);
assert.match(page, /An <code>unknown<\/code> receipt does not mean no side effect occurred/);
assert.match(page, /A claim-hook error can leave a grant spent/);
assert.match(page, /An approval-hook error can leave host authorization state changed/);
});

test("case radios expose one checked choice and explicit receipt meanings", () => {
Expand Down
10 changes: 9 additions & 1 deletion test/guarded-tool-rehearsal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,13 @@ test("checked-in browser examples come from the live host adapter rehearsal", as
assert.equal(declined.callback_count, 0);
assert.deepEqual(declined.attempts[0].host_hook_events,
["Approval requested", "Fixture approval declined"]);
const approvalError = report.cases.find(item => item.id === "approval_hook_error");
assert.equal(approvalError.attempts[0].reason, "approval_error");
assert.equal(approvalError.approval_requests, 1);
assert.equal(approvalError.claim_attempts, 0);
assert.equal(approvalError.callback_count, 0);
assert.equal(approvalError.fixture_state_changes, 1);
assert.ok(approvalError.attempts[0].host_hook_events.includes("In-memory approval fixture marker written"));
const revoked = report.cases.find(item => item.id === "claim_revoked");
assert.equal(revoked.claims_consumed, 1);
assert.equal(revoked.callback_count, 0);
Expand Down Expand Up @@ -62,8 +69,9 @@ test("one selected case keeps the same observed receipt and callback count", asy
test("case list prints each fixture ID and its ordered receipt statuses", async () => {
const report = await buildGuardedToolRehearsal();
const listed = listGuardedToolCases(report);
assert.equal(listed.split("\n").filter(Boolean).length, 13);
assert.equal(listed.split("\n").filter(Boolean).length, 14);
assert.match(listed, /^host_allow\tdispatched$/m);
assert.match(listed, /^approval_hook_error\tblocked$/m);
assert.match(listed, /^approval_declined\tblocked$/m);
assert.match(listed, /^approval_reused\tdispatched -> blocked$/m);
assert.match(listed, /^claim_hook_unknown\tblocked$/m);
Expand Down
Loading