diff --git a/BREAKTHROUGHS.md b/BREAKTHROUGHS.md index 9f271a15..4b3b5900 100644 --- a/BREAKTHROUGHS.md +++ b/BREAKTHROUGHS.md @@ -163,6 +163,12 @@ This separates long-lived root trust from short-lived operational signing author It does not establish legal identity, employment, organizational approval, regulatory authority, certification, independent validation, or official ĀRU authorization. Those remain separate external claims. +## 18. A host-owned approval can be claimed once before an agent tool runs + +The proposed-action pilot has an optional `consumeApproval` callback. After exact digest approval and policy recheck, a host can atomically claim a short-lived grant before invoking the tool. A reused or expired grant blocks, and a policy revocation during an asynchronous claim is checked again before dispatch. + +Run `node pilots/action-boundary/one-shot-grant-demo.mjs`: the first call invokes one simulated callback; a second call reusing the same grant is blocked with no additional callback. The [visual replay lab](https://aruintelligence.github.io/aml-core/action-lab.html?scenario=replay) shows the same project-authored scenario. The local store works in one Node process only; it is not a durable cross-worker replay defense, authenticated human approval, proof of delivery, or independent validation. + ## The frontier The next technical frontiers are: diff --git a/README.md b/README.md index 390fbe02..45d005a5 100644 --- a/README.md +++ b/README.md @@ -24,12 +24,14 @@ **Explore the interface:** [Live lab](https://aruintelligence.github.io/aml-core/) · [View Meaning™ inspector](https://aruintelligence.github.io/aml-core/view-meaning.html) · [API contract](protocol/aml-http.openapi.yaml) -**Explore the next boundary:** [Try the Action Boundary Lab](https://aruintelligence.github.io/aml-core/action-lab.html?view=action-path-1) — change an agent's intended tool call and see an exact policy decision, digest, stale simulated approval, and uncertain outcome. [Explore the visual 13-case challenge](https://aruintelligence.github.io/aml-core/action-challenge.html), [read its adapter contract](publications/ACTION_BOUNDARY_CHALLENGE.md), or [read the pilot](pilots/action-boundary/README.md) and its [six published vectors](pilots/action-boundary/vectors.json). The browser rehearsal and repository self-test are project-authored, with no live tool access. +**Explore the next boundary:** [Try the Action Boundary Lab](https://aruintelligence.github.io/aml-core/action-lab.html?view=action-path-1) — change an agent's intended tool call and see an exact policy decision, digest, stale simulated approval, and uncertain outcome. [Try a one-use grant replay](https://aruintelligence.github.io/aml-core/action-lab.html?scenario=replay): a second attempt with the same local grant blocks before a callback. [Explore the visual 13-case challenge](https://aruintelligence.github.io/aml-core/action-challenge.html), [read its adapter contract](publications/ACTION_BOUNDARY_CHALLENGE.md), or [read the pilot](pilots/action-boundary/README.md) and its [six published vectors](pilots/action-boundary/vectors.json). The browser rehearsal and repository self-test are project-authored, with no live tool access. [](https://aruintelligence.github.io/aml-core/action-lab.html?view=action-path-1) **Wire one agent tool:** [Follow the function-tool quickstart](docs/AGENT_TOOL_QUICKSTART.md) to wrap a host-owned callback with exact policy matching, approval binding, and a dispatch receipt. Its runnable local demo simulates execution; the host must supply real authorization and tool mapping. +[](https://aruintelligence.github.io/aml-core/action-lab.html?scenario=replay) + ## Prove the core claim in 60 seconds Open this exact state: [**attention 5 / restoration 1 → SUPPRESS**](https://aruintelligence.github.io/aml-core/proof.html?attention=5&restoration=1&lang=en). diff --git a/docs/AGENT_TOOL_QUICKSTART.md b/docs/AGENT_TOOL_QUICKSTART.md index aebe1faa..e5c1579d 100644 --- a/docs/AGENT_TOOL_QUICKSTART.md +++ b/docs/AGENT_TOOL_QUICKSTART.md @@ -45,6 +45,32 @@ const execute = createGuardedToolExecute({ `requestApproval` must return `{ approved: true, proposal_sha256 }` for the exact frozen proposal, or refuse. The callback may be omitted for a policy rule that allows an action without approval. If an approval-required rule has no callback, dispatch blocks. The adapter passes the framework's second `execute` argument as `context` to the host callbacks, and calls the tool with the frozen canonical arguments. A host mapping error throws before any tool invocation. +## Rehearse a one-use grant + +Run `node pilots/action-boundary/one-shot-grant-demo.mjs` or open the [replay scenario](https://aruintelligence.github.io/aml-core/action-lab.html?scenario=replay). The first invocation uses a local grant; the second attempt with the same grant blocks before the callback. The example sends no message and authenticates no person. + +To opt in for a host function tool, keep the grant store and approval issuer **inside the host**: + +```js +import { createLocalOneShotGrants } from "./pilots/action-boundary/one-shot-grants.mjs"; + +const grants = createLocalOneShotGrants(); // One Node process, demonstration only. +const guarded = createGuardedToolExecute({ + tool: "send_message", effect: "send", purpose: "Send a team update", + resourceFor: args => args.to, policy, + requestApproval: async (input, context) => { + if (!await hostAuthenticateAndApprove(input.proposal, input.proposal_sha256, context)) { + return { approved: false }; + } + return grants.issue(input.proposal_sha256); // New ID, short expiry. + }, + consumeApproval: grants.consume, + execute: async (args, context) => sendThroughMyService(args.to, args.body, context) +}); +``` + +`hostAuthenticateAndApprove` and `sendThroughMyService` are host functions you must implement; the browser and CLI examples only simulate their outcome. The `consumeApproval` hook runs after the proposal digest and rule match, before the callback. It must make an atomic one-use claim bound to the digest and return `true` only once. A false claim blocks with `approval_reused_or_expired`; an error blocks with `approval_consume_error`. A policy change during an asynchronous claim still blocks. The local Map store does not survive restarts or coordinate multiple workers. Use a durable atomic store and your own authenticated approval, time policy, audit, and idempotency controls in a real deployment. An uncertain callback result still consumes the grant and must not be retried automatically. + For the [OpenAI Agents SDK JavaScript function-tool API](https://openai.github.io/openai-agents-js/guides/tools/), install `@openai/agents` and `zod` in your agent app, then attach the `execute` function above: ```js 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" }; } 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" ? 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 @@ - +