From 3a43ca5d49ca778dc4de8e85d05a6caf7b44c426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Sat, 10 Oct 2026 22:14:39 -0700 Subject: [PATCH 01/15] Add one-use grant path: pilots/action-boundary/boundary.mjs --- pilots/action-boundary/boundary.mjs | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/pilots/action-boundary/boundary.mjs b/pilots/action-boundary/boundary.mjs index 7dc48945..91bdd92f 100644 --- a/pilots/action-boundary/boundary.mjs +++ b/pilots/action-boundary/boundary.mjs @@ -51,9 +51,9 @@ export function planAction(proposal, policy) { } } -// requestApproval and executeTool must be supplied by the trusted host. A model -// must never supply either callback or the policy. No approval callback => deny. -export async function dispatchAction(proposal, { policy, requestApproval, executeTool } = {}) { +// All callbacks and policy must be supplied by the trusted host. A model must +// never supply them. consumeApproval is an optional atomic, one-use host claim. +export async function dispatchAction(proposal, { policy, requestApproval, consumeApproval, executeTool } = {}) { const plan = planAction(proposal, policy); const receipt = (status, reason) => ({ protocol: "aml-action-dispatch/1", proposal_sha256: plan.proposal_sha256, policy_decision: plan.decision, @@ -61,9 +61,9 @@ export async function dispatchAction(proposal, { policy, requestApproval, execut if (plan.decision === "deny") return { receipt: receipt("blocked", plan.reason) }; if (typeof executeTool !== "function") return { receipt: receipt("blocked", "missing_host_dispatch") }; const frozen = snapshot(proposal); + let approval; if (plan.decision === "requires_approval") { if (typeof requestApproval !== "function") return { receipt: receipt("blocked", "approval_unavailable") }; - let approval; try { approval = await requestApproval({ proposal: JSON.parse(frozen.bytes), proposal_sha256: frozen.digest }); } catch { return { receipt: receipt("blocked", "approval_error") }; } if (approval?.approved !== true || approval?.proposal_sha256 !== frozen.digest) { @@ -76,6 +76,23 @@ export async function dispatchAction(proposal, { policy, requestApproval, execut if (rechecked.decision !== plan.decision || rechecked.proposal_sha256 !== frozen.digest) { return { receipt: receipt("blocked", "policy_changed") }; } + if (plan.decision === "requires_approval" && consumeApproval !== undefined) { + if (typeof consumeApproval !== "function") return { receipt: receipt("blocked", "invalid_host_consumer") }; + let consumed; + try { + consumed = await consumeApproval({ approval, proposal: JSON.parse(frozen.bytes), + proposal_sha256: frozen.digest }); + } catch { + return { receipt: receipt("blocked", "approval_consume_error") }; + } + if (consumed !== true) return { receipt: receipt("blocked", "approval_reused_or_expired") }; + // An asynchronous host store can outlive the policy snapshot above. A + // consumed grant stays spent if the host revokes the rule in the meantime. + const finalPlan = planAction(frozen.value, policy); + if (finalPlan.decision !== plan.decision || finalPlan.proposal_sha256 !== frozen.digest) { + return { receipt: receipt("blocked", "policy_changed") }; + } + } try { const result = await executeTool(JSON.parse(frozen.bytes)); return { receipt: receipt("dispatched", "host_dispatch_returned"), result }; @@ -85,3 +102,4 @@ export async function dispatchAction(proposal, { policy, requestApproval, execut return { receipt: receipt("unknown", "host_dispatch_error") }; } } + From 51184cec8eece94069698339074a95fa4a7f56c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Sat, 10 Oct 2026 22:14:40 -0700 Subject: [PATCH 02/15] Add one-use grant path: pilots/action-boundary/guarded-tool.mjs --- pilots/action-boundary/guarded-tool.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pilots/action-boundary/guarded-tool.mjs b/pilots/action-boundary/guarded-tool.mjs index f6195dd2..5cd30811 100644 --- a/pilots/action-boundary/guarded-tool.mjs +++ b/pilots/action-boundary/guarded-tool.mjs @@ -4,13 +4,14 @@ import { planAction, dispatchAction } from "./boundary.mjs"; export function createGuardedToolExecute({ - tool, effect, purpose, resourceFor, policy, requestApproval, execute + tool, effect, purpose, resourceFor, policy, requestApproval, consumeApproval, execute } = {}) { if (typeof tool !== "string" || !tool.trim() || typeof effect !== "string" || !effect.trim() || typeof purpose !== "string" || !purpose.trim() || typeof resourceFor !== "function" || typeof execute !== "function" || (requestApproval !== undefined && typeof requestApproval !== "function") || + (consumeApproval !== undefined && typeof consumeApproval !== "function") || !policy || typeof policy !== "object") { throw new TypeError("A host-owned tool, effect, purpose, resourceFor, policy, and execute are required"); } @@ -28,6 +29,7 @@ export function createGuardedToolExecute({ const outcome = await dispatchAction(proposal, { policy, requestApproval: requestApproval && (input => requestApproval(input, context)), + consumeApproval: consumeApproval && (input => consumeApproval(input, context)), executeTool: frozen => execute(frozen.arguments, context, frozen) }); return { From d6c41bcdc58a57bf4df23ca9db7554efa76f8bc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Sat, 10 Oct 2026 22:14:41 -0700 Subject: [PATCH 03/15] Add one-use grant path: docs/action-lab-core.js --- docs/action-lab-core.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/action-lab-core.js b/docs/action-lab-core.js index 81ab7a5a..6d447210 100644 --- a/docs/action-lab-core.js +++ b/docs/action-lab-core.js @@ -54,11 +54,14 @@ export async function planPreview(proposal, policy, subtle) { } } -export function rehearse(plan, approvedDigest, uncertain = false) { +export function rehearse(plan, approvedDigest, uncertain = false, grantConsumed = false) { if (plan.decision === "deny") return { outcome: "blocked", reason: plan.reason }; if (plan.decision === "requires_approval" && approvedDigest !== plan.proposal_sha256) { return { outcome: "blocked", reason: "approval_missing_or_mismatched" }; } + if (plan.decision === "requires_approval" && grantConsumed) { + return { outcome: "blocked", reason: "approval_reused_or_expired" }; + } return uncertain ? { outcome: "unknown", reason: "simulated_dispatch_exception" } : { outcome: "would_dispatch", reason: "simulation_only" }; } From 69cca1b5425543a1a77830038b3cef40f6dfbd71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Sat, 10 Oct 2026 22:14:43 -0700 Subject: [PATCH 04/15] Add one-use grant path: docs/action-lab-story.js --- docs/action-lab-story.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/action-lab-story.js b/docs/action-lab-story.js index 70cdcc66..d88fb9e1 100644 --- a/docs/action-lab-story.js +++ b/docs/action-lab-story.js @@ -1,6 +1,6 @@ // Presentation mapping for the local Action Boundary Lab. It uses the plan // and rehearsal result; it does not make a second policy or dispatch decision. -export function actionPathStory(proposal, plan, approvedDigest, outcome) { +export function actionPathStory(proposal, plan, approvedDigest, outcome, grantConsumed = false) { const digest = plan?.proposal_sha256; const match = Boolean(digest && approvedDigest && digest === approvedDigest); const stale = Boolean(approvedDigest && !match); @@ -17,6 +17,8 @@ export function actionPathStory(proposal, plan, approvedDigest, outcome) { { value: "DENY", detail: `Host rule: ${readable(plan?.reason)}`, tone: "blocked" }; const approvalCard = stale ? { value: "STALE", detail: "Prior simulated digest no longer matches", tone: "blocked" } : + match && grantConsumed ? + { value: "SPENT", detail: "The one-use grant was already claimed", tone: "blocked" } : match ? { value: "DIGEST MATCH", detail: `Simulated approval · ${digest.slice(0, 12)}…`, tone: "allowed" } : plan?.decision === "allow" ? From 4eef4ff259fa8aef7e0ba82d681825c9ac3534e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Sat, 10 Oct 2026 22:14:44 -0700 Subject: [PATCH 05/15] Add one-use grant path: docs/action-lab.js --- docs/action-lab.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/action-lab.js b/docs/action-lab.js index 9b579472..6e6c6c20 100644 --- a/docs/action-lab.js +++ b/docs/action-lab.js @@ -16,12 +16,15 @@ const cases = { read: { tool: "fetch_record", effect: "read", resource: "record:42", purpose: "Inspect record", arguments: { id: 42 } }, timeout: { tool: "send_message", effect: "send", resource: "team@example.test", + purpose: "Send a status update", arguments: { subject: "Update", body: "The build passed." } }, + replay: { tool: "send_message", effect: "send", resource: "team@example.test", purpose: "Send a status update", arguments: { subject: "Update", body: "The build passed." } } }; const $ = id => document.getElementById(id); const form = $("proposal"); let scenario = "send"; let approvedDigest = null; +let grantConsumed = false; let outcome = null; let latest = null; let generation = 0; @@ -59,12 +62,14 @@ async function render() { "Reason: " + plan.reason.replaceAll("_", " "); $("digest").textContent = plan.proposal_sha256 || "No valid proposal digest"; $("approve").disabled = plan.decision !== "requires_approval"; - $("approval").textContent = approved ? "Simulated approval matches this exact digest." : + $("approval").textContent = approved && grantConsumed ? + "Simulated one-use grant spent. Re-approve to try again." : + approved ? "Simulated approval matches this exact digest." : approvedDigest ? "Previous simulated approval is stale for this proposal." : "No simulated approval recorded."; $("outcome").textContent = outcome ? outcome.outcome.toUpperCase().replaceAll("_", " ") : "NOT ATTEMPTED"; $("outcome").className = "badge " + (outcome?.outcome || ""); - const story = actionPathStory(proposal, plan, approvedDigest, outcome); + const story = actionPathStory(proposal, plan, approvedDigest, outcome, grantConsumed); for (const key of ["proposal", "policy", "approval", "receipt"]) { const card = $("path-" + key); card.dataset.tone = story[key].tone; @@ -73,6 +78,7 @@ async function render() { } const report = { protocol: "aml-action-lab-report/1", provenance: "project-authored browser simulation", proposal: proposal || null, plan, simulated_approval_matches: approved, + simulated_one_use_grant_spent: scenario === "replay" ? grantConsumed : null, simulation: outcome, canonical_proposal_json: plan.canonical_json, limits: "No live tool, authenticated approver, external delivery, or execution proof." }; $("report").textContent = JSON.stringify(report, null, 2); @@ -81,6 +87,7 @@ async function render() { function selectCase(name) { scenario = name; approvedDigest = null; + grantConsumed = false; outcome = null; const selected = cases[name]; for (const key of ["tool", "effect", "resource", "purpose"]) input(key).value = selected[key]; @@ -100,13 +107,17 @@ $("approve").addEventListener("click", async () => { await render(); if (latest.plan.decision === "requires_approval") { approvedDigest = latest.plan.proposal_sha256; + grantConsumed = false; outcome = null; await render(); } }); $("dispatch").addEventListener("click", async () => { await render(); - outcome = rehearse(latest.plan, approvedDigest, scenario === "timeout"); + const oneUse = scenario === "replay"; + const wasConsumed = oneUse && grantConsumed; + outcome = rehearse(latest.plan, approvedDigest, scenario === "timeout", wasConsumed); + if (oneUse && !wasConsumed && outcome.outcome === "would_dispatch") grantConsumed = true; await render(); }); $("copy").addEventListener("click", async () => { From 5eda3d08a9b1882acc9091c1e3534145efba471a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Jacob=20Read=20IV=20=E2=80=94=20Creator=20of=20?= =?UTF-8?q?=C4=80ML=E2=84=A2?= Date: Sat, 10 Oct 2026 22:14:45 -0700 Subject: [PATCH 06/15] Add one-use grant path: docs/action-lab.html --- docs/action-lab.html | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/action-lab.html b/docs/action-lab.html index 207a3793..980b90c0 100644 --- a/docs/action-lab.html +++ b/docs/action-lab.html @@ -4,7 +4,7 @@ - + Action Boundary Lab · ĀML™