diff --git a/app/brand.css b/app/brand.css index 7de0265..5c119bc 100644 --- a/app/brand.css +++ b/app/brand.css @@ -959,3 +959,7 @@ .sanction .sn-story-strip > p { margin-top: 18px; } @media (max-width: 900px) { .sanction .sn-story-strip ol { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 540px) { .sanction .sn-story-strip { padding: 20px; } .sanction .sn-story-strip ol { grid-template-columns: 1fr; } } + +/* Four oversight questions on the marketing homepage. */ +.sn-home .sn-oversight-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } +@media (max-width: 700px) { .sn-home .sn-oversight-grid { grid-template-columns: 1fr; } } diff --git a/app/page.tsx b/app/page.tsx index 4963d13..5abcf90 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -74,7 +74,7 @@ export default function Landing() {
Sanction
- For your teamHow it worksPlatformDocs + Use casesHow it worksPlatformDocs
Sign inTry one approval
@@ -95,11 +95,22 @@ export default function Landing() {
Your AI toolsYour approval rulesYour decision
+
+

Decide what it can do.
Know when you need a say.

Your team may work across different AI providers. Give each agent its own limits and bring the decisions into one shared history.

+
+

Access: what does it need?

Give each agent its own identity and scoped access. Use governed integrations to add credentials without handing the agent your provider keys.

Scope access to the work →
+

Actions: what may it change?

Define which tools, capabilities, and expenses are allowed. Route actions through an enforcing integration when the limit must be applied before execution.

Set boundaries for new capabilities →
+

Approval: what needs your review?

Review the exact recipient, message, deployment, or expense. Approve that request once, without changing the standing rules.

Review a message before it leaves →
+

Oversight: how do you stay in control?

Inspect requests and decisions, track budgets, and revoke access. A decision record shows what was authorized; it does not prove an external action ran.

Keep a shared decision history →
+
+
+

Keep the work moving.
Keep a say in what happens.

Choose the moments that need a person. Connect the relevant workflow so the agent asks before taking that step.

- {humanUses.map(([number, title, body, category]) =>
{category}{number}

{title}

{body}

)} + {humanUses.map(([number, title, body, category]) =>
{category}{number}

{title}

{body}

Explore this use case →
)}
+

Explore all ten use cases →

These workflows require a connected agent or integration. Sanction does not automatically intercept your AI host’s other tools.

@@ -142,7 +153,7 @@ export default function Landing() {

If you’re not sure, Sanction it.

One safe request. A human decision. A clear next step.

Try one approvalCreate a free account
- + ) } diff --git a/app/use-cases/page.tsx b/app/use-cases/page.tsx new file mode 100644 index 0000000..ea3054a --- /dev/null +++ b/app/use-cases/page.tsx @@ -0,0 +1,74 @@ +import type { Metadata } from "next" +import Link from "next/link" +import { brandFontVars } from "../brand-fonts" +import "../brand.css" +import "./use-cases.css" +import { stories } from "./stories" +import { StoryFilter } from "./story-filter" + +export const metadata: Metadata = { + title: "Ten use cases for human oversight | Sanction", + description: "Production changes, expenses, messages, scoped access, and agent collaboration: ten ways to review an exact action before it proceeds.", + alternates: { canonical: "https://getsanction.com/use-cases" }, +} + +const loop = [ + ["Propose", "An exact action, its arguments, and a reason."], + ["Decide", "Policy allows, escalates for review, or denies."], + ["Review", "An authorized person decides an escalated request."], + ["Redeem", "After human approval, the agent redeems one expiring grant for the same action."], +] + +export default function UseCasesPage() { + return ( +
+ Skip to the stories + +
+

SANCTION / USE CASES

+

Ten moments when
someone should ask first.

+

A production change. An unexpected expense. A message with your name on it. Give the decision a place to happen before the agent proceeds.

+
Explore the stories Understand the control boundary
+

Five stories for people and engineers. Five for autonomous agents.

+
+
+
+

THE COMMON THREAD

One action. One decision.

Start with a one-off approval. Add shared policies, budgets, and scoped access as the work grows.

+
    {loop.map(([title, detail], index) =>
  1. 0{index + 1}

    {title}

    {detail}

  2. )}
+

Changed arguments require a new decision. Hard limits still deny. Approval does not execute the action; the caller or governed integration takes the next step.

+
+
+
+

KNOW THE BOUNDARY

The decision is shared.
Control depends on the path.

+
+
COOPERATIVE

The agent honors the answer.

Over the approval connector, an agent asks Sanction, waits when needed, and proceeds only when the response says to proceed. Sanction cannot stop a separate action that bypasses that decision.

Approvals connector: /mcp/approvals. An approval gives authority for the matching request, not proof of execution.

+
ENFORCED PATH

The execution path checks the answer.

The MCP broker checks governed tool calls before forwarding. The model gateway applies its budget checks before supported provider calls. An SDK integration enforces a decision only when the calling code gates execution on it.

The full wallet MCP profile adds tools; connecting it alone does not put downstream actions behind an enforcement boundary.

+
+
+
+

THE STORIES

Where the pause matters.

Illustrative scenarios, with the review scope and integration boundary made explicit.

+ + +
{stories.map(story => ( +
+
{story.n}{story.track === "people" ? "People & engineers" : "Autonomous agents"}Link
+

{story.title}

+

{story.story}

+
+
    {story.strip.map((step, index) =>
  1. {index + 1}{step.t}{step.s.split("|").join(" · ")}
  2. )}
+
{"note" in story ? story.note : "Illustrative flow. Policy and the integration path determine the decision."}
+
+
What is reviewed
{story.reviewed}
+
Technical detail & boundaries
Cooperative path
{story.coop}
Enforced path
{story.enf}
Boundary
{story.boundary}
+
+ ))}
+
+
+

START WITH ONE DECISION

If you’re not sure, Sanction it.

Connect your agent and walk through a safe approval request. Review the proposal, decide, and see the grant redeemed without running a real action.

Try one approval
+ +
+ ) +} diff --git a/app/use-cases/stories.ts b/app/use-cases/stories.ts new file mode 100644 index 0000000..f5efed4 --- /dev/null +++ b/app/use-cases/stories.ts @@ -0,0 +1,352 @@ +export const stories = [ + { + "n": "01", + "track": "people", + "title": "Approve a production change", + "story": "An engineer asks a coding agent to run a migration on the billing database. The agent doesn't just run it. It sends Sanction the exact command, the target, and why, and waits. The designated owner reads that exact proposal and approves it. The agent gets a one-use grant for that command and nothing else.", + "strip": [ + { + "i": "agent", + "t": "Agent", + "s": "migrate billing|--target prod" + }, + { + "i": "shield", + "t": "Escalated", + "s": "require_approval|reason given", + "tone": "esc" + }, + { + "i": "person", + "t": "Owner reviews", + "s": "exact command|approves once" + }, + { + "i": "ticket", + "t": "Grant", + "s": "1 use · expires|args bound", + "tone": "ok" + } + ], + "note": "A retry with a different --target is refused and needs a new decision.", + "reviewed": "The tool, the server, and the full arguments: the actual command, not a summary of it.", + "coop": "sanction_authorize_tool with require_approval, then sanction_check_authorization, then redeem with the grant_id. The agent honors proceed.", + "enf": "Put the deploy or infrastructure MCP server behind Sanction's broker. The call is forwarded only with an allowed decision.", + "boundary": "Sanction governs calls routed through it. Approval authorizes the proposed operation; it does not establish that a deployment is safe or execute the deployment." + }, + { + "n": "02", + "track": "people", + "title": "Approve an unexpected expense", + "story": "Someone needs a paid API plan or extra compute that crosses their normal review threshold but remains within the hard budget. They don't edit the standing budget and they don't need an exception that lasts all quarter. They ask for that one purchase. A person approves that amount for that purpose, and the budget policy stays as it was.", + "strip": [ + { + "i": "card", + "t": "Request", + "s": "$240 · compute|for this job" + }, + { + "i": "shield", + "t": "Over threshold", + "s": "escalate_over_usd", + "tone": "esc" + }, + { + "i": "person", + "t": "Approver", + "s": "this amount|this purpose" + }, + { + "i": "check", + "t": "Policy intact", + "s": "standing budget|unchanged", + "tone": "ok" + } + ], + "note": "Illustrative amounts. Hard limits still deny, even with a person available.", + "reviewed": "The amount, what it pays for, and the reason. The standing daily budget isn't changed.", + "coop": "sanction_authorize with the amount. Policy escalates above the review threshold, and the agent waits for the decision.", + "enf": "The LLM gateway meters supported calls and returns 402 on new calls once recorded daily usage reaches the budget. In-flight usage can cross the budget. External purchases need a caller that honors the decision.", + "boundary": "Approval does not raise a hard budget limit or make a payment. An external purchase still needs a caller that honors the decision." + }, + { + "n": "03", + "track": "people", + "title": "Review something before it leaves the company", + "story": "An assistant drafts a customer email, a contract redline, or a public post. Before it sends, it submits the real recipient and the real content. A human approves that exact message to that exact recipient. If the agent edits the content or changes the recipient afterward, that's a new decision.", + "strip": [ + { + "i": "mail", + "t": "Draft ready", + "s": "to: renewal@…|body + attachment" + }, + { + "i": "shield", + "t": "Escalated", + "s": "outbound send", + "tone": "esc" + }, + { + "i": "slack", + "t": "Human reviews", + "s": "dashboard|or Slack" + }, + { + "i": "ticket", + "t": "Send once", + "s": "same recipient|same content", + "tone": "ok" + } + ], + "note": "Slack is optional. The dashboard approval inbox is the baseline.", + "reviewed": "Recipient, subject, body, and attachments, exactly as the send tool will receive them.", + "coop": "The send tool's arguments go into sanction_authorize_tool. The agent sends only after proceed, using the matching grant_id.", + "enf": "Route the email or social MCP server through the broker so the send is forwarded only with an allowed decision.", + "boundary": "Only a person with authority over the wallet can approve. A channel membership alone does not grant approval authority." + }, + { + "n": "04", + "track": "people", + "title": "Give a contractor narrowly scoped access", + "story": "A contractor's agent needs to work in your repository for two weeks. You don't share your keys. You give it its own agent seat with a tool allow-list, a budget, and an expiry date. Sensitive credentials stay in the vault and are used only on the governed execution path. When the seat expires, the key stops working.", + "strip": [ + { + "i": "agent", + "t": "Contractor seat", + "s": "own key|own identity" + }, + { + "i": "stack", + "t": "Scoped policy", + "s": "tool allow-list|daily budget" + }, + { + "i": "vault", + "t": "Vault", + "s": "credentials stay|server-side", + "tone": "ok" + }, + { + "i": "clock", + "t": "Expires", + "s": "key fails closed|after end date", + "tone": "dark" + } + ], + "reviewed": "Anything outside the allow-list or above budget escalates or is denied. Expiry is automatic.", + "coop": "Over the approvals profile, the contractor's agent asks Sanction and honors the answer.", + "enf": "Give the contractor the broker URL, not the upstream. Calls are policy-checked, tools/list shows only allowed tools, and the upstream credential is injected server-side.", + "boundary": "Enforcement covers the governed execution path. Separate access to an upstream service remains outside that boundary." + }, + { + "n": "05", + "track": "people", + "title": "Coordinate work across AI providers", + "story": "A team researches in one assistant, builds in another, and reviews in Slack. Each agent connects to the same Sanction wallet under its own identity. Whichever tool asks, decisions land in one shared history, and the team can see what was requested, who decided, and what was redeemed.", + "strip": [ + { + "i": "agent", + "t": "Research", + "s": "assistant A" + }, + { + "i": "agent", + "t": "Build", + "s": "assistant B" + }, + { + "i": "shield", + "t": "One service", + "s": "same policy|same wallet", + "tone": "dark" + }, + { + "i": "history", + "t": "One history", + "s": "every decision|every host", + "tone": "ok" + } + ], + "note": "Each host needs its own configured connection and agent identity.", + "reviewed": "Each request on its own merits, attributed to the agent and host that sent it.", + "coop": "Each host connects to the approvals profile. Every agent asks the same service before acting.", + "enf": "Tool traffic routed through the broker is checked before forwarding. An SDK integration enforces the decision only when its calling code gates execution on that decision. Other paths remain cooperative.", + "boundary": "Connection and enforcement depend on each host’s integration. Sharing a decision history does not make every host an enforced execution path." + }, + { + "n": "06", + "track": "agents", + "title": "Stop at the edge of a budget", + "story": "An agent does routine paid work inside its allowance without interrupting anyone. When one expense crosses the review threshold, it escalates that expense and waits. When an action would break a hard limit, it gets a denial, and asking a human doesn't change that.", + "strip": [ + { + "i": "agent", + "t": "Routine", + "s": "within allowance", + "tone": "ok" + }, + { + "i": "card", + "t": "Large expense", + "s": "over threshold" + }, + { + "i": "shield", + "t": "Escalate", + "s": "one review", + "tone": "esc" + }, + { + "i": "stop", + "t": "Hard limit", + "s": "deny · no|override", + "tone": "no" + } + ], + "reviewed": "Only the expense that crossed the threshold. Routine spend stays quiet.", + "coop": "sanction_authorize before each spend returns allow, escalate, or deny. sanction_wallet_status shows remaining headroom.", + "enf": "The LLM gateway meters supported calls and returns 402 on new calls once recorded daily usage reaches the budget. In-flight usage can cross the budget; this budget check is separate from the approval-and-grant loop.", + "boundary": "An escalation timeout does not override hard limits. Gateway checks happen before provider calls; in-flight usage can affect the final cost." + }, + { + "n": "07", + "track": "agents", + "title": "Ask before acquiring new capabilities", + "story": "Before it installs a skill, enables a plugin, or calls a new API, the agent asks. The organization's capability policy allows it, escalates it, or blocks it. Gaining a new capability is governed the same way spending money is.", + "strip": [ + { + "i": "plug", + "t": "Wants", + "s": "skill:install:|web-reader" + }, + { + "i": "shield", + "t": "Capability rules", + "s": "block → allow →|escalate", + "tone": "dark" + }, + { + "i": "person", + "t": "Review", + "s": "if escalated", + "tone": "esc" + }, + { + "i": "check", + "t": "Decision", + "s": "recorded", + "tone": "ok" + } + ], + "note": "Rules use namespaced IDs with prefix matching, e.g. api:github.com/*", + "reviewed": "The namespaced capability the agent wants, e.g. skill:install:… or api:….", + "coop": "sanction_authorize_capability before the install or the first call to the new API.", + "enf": "Enforced where the install or API path goes through Sanction. A host's own plugin installer stays cooperative.", + "boundary": "Authorization records a decision; it does not install a skill or plugin. A host’s own installer must honor that decision or gate execution itself." + }, + { + "n": "08", + "track": "agents", + "title": "Escalate an exceptional action and resume once", + "story": "The agent reaches a step that needs human judgment. It submits the exact action, pauses, and checks back. Once someone approves, it redeems the grant for that identical action, one time. If the arguments change, it has to ask again. If redemption fails, it stops and reports instead of asking again on its own.", + "strip": [ + { + "i": "agent", + "t": "Submit", + "s": "exact action" + }, + { + "i": "clock", + "t": "Wait", + "s": "next_action: wait", + "tone": "esc" + }, + { + "i": "ticket", + "t": "Redeem once", + "s": "identical input|+ grant_id", + "tone": "ok" + }, + { + "i": "stop", + "t": "Changed args", + "s": "new decision|required", + "tone": "no" + } + ], + "note": "If redemption fails, the agent stops and reports. It doesn't re-request automatically.", + "reviewed": "The paused action, exactly as it will run.", + "coop": "sanction_authorize_tool → sanction_check_authorization → retry_with_grant → proceed. This is the synthetic first request in the connection guide.", + "enf": "The same loop through the broker: the forward happens only with the redeemed grant.", + "boundary": "A grant authorizes one matching request. An outcome log records what the caller reports; it is not independent proof that the action ran." + }, + { + "n": "09", + "track": "agents", + "title": "Use credentials without possessing them", + "story": "An agent needs to call an API that requires a secret. It holds its Sanction key. A governed integration—the MCP broker or LLM gateway—adds the stored upstream credential on the server side. The agent can complete the supported call without receiving the credential as a tool result.", + "strip": [ + { + "i": "agent", + "t": "Agent", + "s": "holds only|its Sanction key" + }, + { + "i": "shield", + "t": "Authorize", + "s": "scoped operation", + "tone": "dark" + }, + { + "i": "vault", + "t": "Inject", + "s": "server-side|from vault", + "tone": "ok" + }, + { + "i": "plug", + "t": "Upstream API", + "s": "credential added|server-side" + } + ], + "note": "Broker and gateway inject server-side. sanction_inject_credential hands the value to the agent, so it isn't this story.", + "reviewed": "The operation and its scope. Calls to the vaulted provider key are limited to metered endpoints.", + "coop": "Not applicable. This story is about the enforced path by definition.", + "enf": "The broker decrypts and injects the upstream credential server-side. The gateway injects a stored provider key for supported metered calls. Both keep credential handling on the forwarding path.", + "boundary": "Sanction stores encrypted credentials. Server-side injection avoids returning the credential as a tool result; upstream responses must not echo secrets. The direct credential-injection tool returns a credential value and is a different path." + }, + { + "n": "10", + "track": "agents", + "title": "Collaborate without passing around blanket authority", + "story": "A research agent hands work to a purchasing or deployment agent. Each one acts under its own identity and its own limits. The handoff passes the work, not permission. The receiving agent asks Sanction for what it intends to do. If a parent agent deliberately delegates, it mints a short-lived, scoped mandate instead of sharing its key.", + "strip": [ + { + "i": "agent", + "t": "Research", + "s": "own identity" + }, + { + "i": "handoff", + "t": "Handoff", + "s": "work, not|permission" + }, + { + "i": "agent", + "t": "Purchasing", + "s": "own identity|own limits" + }, + { + "i": "shield", + "t": "Asks again", + "s": "its own decision", + "tone": "esc" + } + ], + "note": "A handoff does not transfer an approval. Each agent remains accountable for its own request.", + "reviewed": "The receiving agent's proposed action, under the receiving agent's own policy.", + "coop": "Each agent connects with its own identity and asks before acting. An approval granted to one agent can't be redeemed by another.", + "enf": "A parent can issue a short-lived mandate with a credential scope and a spend cap. The mandate is verified by Sanction on the consuming path; enforcement depends on that path checking it. Agents keep separate keys.", + "boundary": "Approvals are bound to an agent and cannot be transferred to another. A scoped mandate is a separate delegation mechanism, not a transferable approval." + } +] as const diff --git a/app/use-cases/story-filter.tsx b/app/use-cases/story-filter.tsx new file mode 100644 index 0000000..0e1b127 --- /dev/null +++ b/app/use-cases/story-filter.tsx @@ -0,0 +1,39 @@ +"use client" + +import { useEffect, useState, type ReactNode } from "react" + +const audiences = [ + ["all", "All ten stories"], + ["people", "People & engineers"], + ["agents", "Autonomous agents"], +] as const + +export function StoryFilter({ children }: { children: ReactNode }) { + const [audience, setAudience] = useState("all") + useEffect(() => { + const revealLinkedStory = () => { + if (window.location.hash.startsWith("#case-")) { + setAudience("all") + requestAnimationFrame(() => { + document.getElementById(window.location.hash.slice(1))?.scrollIntoView() + }) + } + } + window.addEventListener("hashchange", revealLinkedStory) + return () => window.removeEventListener("hashchange", revealLinkedStory) + }, []) + + return ( +
{ + if (event.target instanceof Element && event.target.closest('a[href^="#case-"]')) setAudience("all") + }}> +
+
+ {audiences.map(([value, label]) => )} +
+ {audience === "all" ? 10 : 5} stories shown +
+ {children} +
+ ) +} diff --git a/app/use-cases/use-cases.css b/app/use-cases/use-cases.css new file mode 100644 index 0000000..7ec6a9a --- /dev/null +++ b/app/use-cases/use-cases.css @@ -0,0 +1,103 @@ +.sanction.uc-page { background: var(--paper-0); color: var(--ink-1); } +.uc-page .uc-wrap { width: min(1120px, calc(100% - 64px)); margin-inline: auto; } +.uc-page .uc-nav { min-height: 96px; display: flex; align-items: center; justify-content: space-between; gap: 24px; border-bottom: 1px solid var(--line-1); } +.uc-page .uc-nav > div { display: flex; align-items: center; gap: 28px; font-size: 14px; } +.uc-page a { text-underline-offset: 5px; } +.uc-page a:focus-visible, .uc-page button:focus-visible, .uc-page summary:focus-visible { outline: 3px solid var(--pine-7); outline-offset: 5px; } +.uc-page .uc-skip { position: absolute; top: -100px; left: 20px; padding: 12px; background: white; z-index: 100; } +.uc-page .uc-skip:focus { top: 12px; } +.uc-page .uc-hero { padding-block: 88px 72px; } +.uc-page .uc-eyebrow { font-family: var(--font-mono); font-size: 11px; letter-spacing: .13em; color: var(--ink-2); margin: 0 0 22px; } +.uc-page h1 { max-width: 950px; font-size: clamp(42px, 6.1vw, 74px); line-height: 1.06; letter-spacing: -.045em; font-weight: 500; margin: 0 0 28px; } +.uc-page .uc-lede { max-width: 710px; font-size: 21px; line-height: 1.6; color: var(--ink-2); } +.uc-page .uc-hero-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 28px; margin-top: 32px; } +.uc-page .uc-text-link { font-size: 14px; text-decoration: underline; } +.uc-page .uc-caption { font-size: 12px; color: var(--ink-2); margin-top: 24px; } +.uc-page .uc-loop-section { padding-block: 54px; border-block: 1px solid var(--line-1); background: var(--paper-1); } +.uc-page .uc-section-intro h2 { font-size: clamp(28px, 3.5vw, 42px); line-height: 1.18; letter-spacing: -.035em; font-weight: 500; margin: 0 0 16px; } +.uc-page .uc-section-intro > p:last-child { max-width: 650px; color: var(--ink-2); line-height: 1.6; } +.uc-page .uc-loop { display: grid; grid-template-columns: repeat(4, 1fr); margin: 32px 0 20px; padding: 0; list-style: none; border: 1px solid var(--line-1); background: var(--paper-0); } +.uc-page .uc-loop li { padding: 24px; border-right: 1px solid var(--line-1); } +.uc-page .uc-loop li:last-child { border: 0; } +.uc-page .uc-step-number { font-family: var(--font-mono); font-size: 12px; color: var(--ochre-7); } +.uc-page .uc-loop h3 { margin: 18px 0 10px; font-size: 19px; font-weight: 500; } +.uc-page .uc-loop p, .uc-page .uc-loop-note { font-size: 14px; line-height: 1.6; color: var(--ink-2); } +.uc-page .uc-loop-note { max-width: 860px; } +.uc-page .uc-modes { padding-block: 72px; scroll-margin-top: 24px; } +.uc-page .uc-mode-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; margin-top: 32px; } +.uc-page .uc-mode-grid article { border: 1px solid var(--line-1); padding: 30px; } +.uc-page .uc-mode-tag { display: inline-block; padding: 6px 9px; font-size: 10px; letter-spacing: .08em; font-family: var(--font-mono); background: var(--ochre-tint); color: #785512; } +.uc-page .uc-enforced { color: var(--pine-9); background: var(--pine-tint); } +.uc-page .uc-mode-grid h3 { font-size: 23px; font-weight: 500; margin: 20px 0 14px; letter-spacing: -.025em; } +.uc-page .uc-mode-grid p { font-size: 15px; line-height: 1.7; color: var(--ink-2); } +.uc-page .uc-mode-grid .uc-mode-detail { border-top: 1px solid var(--line-2); margin-top: 22px; padding-top: 18px; font-size: 13px; } +.uc-page code { overflow-wrap: anywhere; } +.uc-page .uc-stories { padding-block: 20px 80px; scroll-margin-top: 24px; } +.uc-page .uc-filter-row { display: flex; justify-content: space-between; gap: 20px; align-items: center; margin-top: 30px; } +.uc-page .uc-filters { display: flex; flex-wrap: wrap; gap: 8px; } +.uc-page .uc-filters button { cursor: pointer; border: 1px solid var(--line-1); border-radius: 3px; background: transparent; padding: 10px 15px; color: var(--ink-2); font: inherit; font-size: 13px; } +.uc-page .uc-filters button[aria-pressed="true"] { background: var(--pine-9); color: var(--paper-0); border-color: var(--pine-9); } +.uc-page .uc-count { color: var(--ink-2); font-family: var(--font-mono); font-size: 11px; white-space: nowrap; } +.uc-page .uc-story-index { display: grid; grid-template-columns: 1fr 1fr; gap: 0 32px; margin: 24px 0 48px; } +.uc-page .uc-story-index a { display: flex; gap: 14px; padding: 12px 0; border-bottom: 1px solid var(--line-2); font-size: 13px; color: var(--ink-2); } +.uc-page .uc-story-index a:hover { color: var(--pine-7); } +.uc-page .uc-story-index a span { font-family: var(--font-mono); color: var(--ochre-7); font-size: 11px; } +.uc-page .uc-filtered[data-audience="people"] [data-track="agents"], .uc-page .uc-filtered[data-audience="agents"] [data-track="people"] { display: none; } +.uc-page .uc-case-list { display: grid; gap: 32px; } +.uc-page .uc-case { border: 1px solid var(--line-1); background: #fffefa; padding: 38px 40px 0; scroll-margin-top: 24px; } +.uc-page .uc-case:target { border-color: var(--ochre-7); } +.uc-page .uc-case-meta { display: flex; align-items: center; gap: 16px; font-size: 11px; color: var(--ink-2); font-family: var(--font-mono); } +.uc-page .uc-case-number { color: var(--ochre-7); font-size: 18px; } +.uc-page .uc-case-meta a { margin-left: auto; font-size: 11px; } +.uc-page .uc-case h3 { font-size: clamp(25px, 3vw, 35px); line-height: 1.2; font-weight: 500; letter-spacing: -.03em; margin: 22px 0 18px; } +.uc-page .uc-story { max-width: 880px; color: var(--ink-2); font-size: 17px; line-height: 1.8; } +.uc-page .uc-diagram { margin: 28px 0; } +.uc-page .uc-diagram ol { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; padding: 0; list-style: none; } +.uc-page .uc-diagram li { min-width: 0; position: relative; border: 1px solid var(--line-1); padding: 17px; background: var(--paper-1); } +.uc-page .uc-diagram li + li::before { content: "→"; position: absolute; left: -16px; top: 46%; font-size: 12px; color: var(--ink-2); } +.uc-page .uc-diagram li[data-tone="esc"] { background: var(--ochre-tint); } +.uc-page .uc-diagram li[data-tone="ok"] { background: var(--pine-tint); } +.uc-page .uc-diagram li[data-tone="no"] { background: var(--brick-tint); } +.uc-page .uc-diagram li > span { display: block; font-size: 11px; line-height: 1.6; color: var(--ink-2); font-family: var(--font-mono); overflow-wrap: anywhere; } +.uc-page .uc-diagram li strong { display: block; font-size: 14px; font-weight: 500; margin: 10px 0 7px; } +.uc-page .uc-diagram figcaption { margin-top: 12px; font-size: 12px; line-height: 1.6; color: var(--ink-2); } +.uc-page .uc-reviewed { display: grid; grid-template-columns: 150px 1fr; gap: 24px; border-top: 1px solid var(--line-2); padding-top: 24px; margin: 24px 0; } +.uc-page dt { font-size: 12px; font-weight: 600; } +.uc-page dd { margin: 0; color: var(--ink-2); font-size: 14px; line-height: 1.7; } +.uc-page .uc-technical { border-top: 1px solid var(--line-1); } +.uc-page .uc-technical summary { cursor: pointer; padding: 21px 0; font-size: 13px; font-weight: 500; } +.uc-page .uc-technical dl { margin: 0 0 28px; } +.uc-page .uc-technical dl > div { display: grid; grid-template-columns: 150px 1fr; gap: 24px; padding: 16px 0; border-top: 1px solid var(--line-2); } +.uc-page .uc-technical .uc-boundary { background: var(--paper-1); padding: 18px; margin-top: 8px; } +.uc-page .uc-closing { background: var(--paper-1); border-block: 1px solid var(--line-1); padding: 70px 0; } +.uc-page .uc-closing h2 { font-size: clamp(32px, 4vw, 48px); font-weight: 500; letter-spacing: -.04em; } +.uc-page .uc-closing p:not(.uc-eyebrow) { color: var(--ink-2); line-height: 1.8; max-width: 620px; margin: 22px 0 28px; } +.uc-page .uc-footer { display: flex; gap: 24px; align-items: center; padding-block: 32px; font-size: 12px; color: var(--ink-2); } +.uc-page .uc-footer > a:last-child { margin-left: auto; } +@media (max-width: 700px) { + .uc-page .uc-wrap { width: calc(100% - 36px); } + .uc-page .uc-nav { min-height: 80px; gap: 14px; } + .uc-page .uc-nav > div { gap: 14px; } + .uc-page .uc-nav > div > a:first-child { display: none; } + .uc-page .uc-nav img { width: 108px; height: auto; } + .uc-page .uc-nav .sn-btn { font-size: 11px; padding: 9px; } + .uc-page .uc-hero { padding-block: 56px; } + .uc-page .uc-lede { font-size: 18px; } + .uc-page .uc-loop { grid-template-columns: 1fr 1fr; } + .uc-page .uc-loop li { padding: 20px; border-bottom: 1px solid var(--line-1); } + .uc-page .uc-loop li:nth-child(2) { border-right: 0; } + .uc-page .uc-loop li:nth-child(3) { border-bottom: 0; } + .uc-page .uc-mode-grid, .uc-page .uc-story-index { grid-template-columns: 1fr; } + .uc-page .uc-mode-grid article { padding: 24px; } + .uc-page .uc-modes { padding-block: 50px; } + .uc-page .uc-filter-row { align-items: flex-start; flex-direction: column; gap: 12px; } + .uc-page .uc-filters button { padding: 9px 11px; font-size: 12px; } + .uc-page .uc-case { padding: 26px 20px 0; } + .uc-page .uc-case-meta { gap: 10px; font-size: 9px; } + .uc-page .uc-story { font-size: 16px; } + .uc-page .uc-diagram ol { grid-template-columns: 1fr 1fr; gap: 14px; } + .uc-page .uc-diagram li { padding: 12px; } + .uc-page .uc-diagram li + li::before { content: none; } + .uc-page .uc-reviewed, .uc-page .uc-technical dl > div { grid-template-columns: 1fr; gap: 8px; } + .uc-page .uc-footer { flex-wrap: wrap; gap: 14px; } +} diff --git a/docs/DEMO-RUNBOOK.md b/docs/DEMO-RUNBOOK.md index b029658..7b55f67 100644 --- a/docs/DEMO-RUNBOOK.md +++ b/docs/DEMO-RUNBOOK.md @@ -119,3 +119,51 @@ demo fleet, read-only by design. This is the "help them from my side" view. budget-denial expectations (they're real budgets). - Pending escalations from pulse self-clean: every persona's policy times out escalations to deny, so yesterday's staging never piles up. + + +## Exact-recipient approval demo — candidate, not recorded evidence + +Prepared 2026-10-08. This demonstration authorizes synthetic inputs only. It +never sends mail. Use a dedicated test wallet and the same authenticated agent +throughout. Configure policy to permit review of `sanction.demo.email` without +other hard denials. Do not connect a real email-sending tool for this demo. +Authorization requests create records and may send configured approval alerts. + +1. Ask the host to call `sanction_authorize_tool` once with this exact input, + report the request ID, then wait without polling or executing anything: + + ```json + { + "tool": "sanction.demo.email", + "arguments": { + "execute": false, + "to": "reviewer@example.invalid", + "subject": "Synthetic approval demonstration", + "body": "This is a test proposal. No email will be sent." + }, + "require_approval": true, + "approval_reason": "Review the exact recipient and message; test only, no email will be sent." + } + ``` + +2. Show the pending request and expand its exact arguments. The authorized + wallet owner approves it. Keep credentials and grant values off the recording. +3. Ask the host to call `sanction_check_authorization` once for that request ID. + Expected: approval with an active grant, but no authorization to execute from + the check alone. If it is pending, expired, or denied, stop this take. +4. With that unconsumed grant, call `sanction_authorize_tool` once using the + original fields plus `grant_id`, changing **only** `arguments.to` to + `different-recipient@example.invalid`. Expected: `authorized: false`, + `next_action: stop`, and a binding-mismatch denial. Do not retry, execute, + or automatically create a new request. A successful redemption here is a + failure of the test: stop and preserve the evidence. +5. Show the denial and its audit linkage where returned. Record the actual + status, code, request references, host, date, and build; redact grant values. + End with: permission covered one proposed recipient and message. Connecting + the approvals MCP is cooperative; enforced sending requires the real send + path to pass through the broker or an equivalent controlled executor. + +Use a separate fresh request if demonstrating successful exact-input redemption. +Never redeem first and then use that consumed grant for the changed-recipient +case: that would test replay rejection, not argument binding. The two flows +must be recorded separately, and neither invokes a target action.