From c0310d5fa4fafaef23be1b4ac806a1026c76846b Mon Sep 17 00:00:00 2001
From: Egor Merkushev
Date: Fri, 10 Jul 2026 23:12:41 +0300
Subject: [PATCH 1/2] Add reproducible RFC review dashboard workflow
---
.github/workflows/docs.yml | 17 +
.gitignore | 1 +
Makefile | 10 +
README.md | 22 +-
review/build_review.py | 160 ++++
review/check_review.mjs | 14 +
review/requirements.txt | 1 +
review/review-data.json | 1597 +++++++++++++++++++++++++++++++++++
review/review-template.html | 720 ++++++++++++++++
review/standalone.css | 129 +++
10 files changed, 2670 insertions(+), 1 deletion(-)
create mode 100644 Makefile
create mode 100644 review/build_review.py
create mode 100644 review/check_review.mjs
create mode 100644 review/requirements.txt
create mode 100644 review/review-data.json
create mode 100644 review/review-template.html
create mode 100644 review/standalone.css
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index f0db136..f43035d 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -18,6 +18,16 @@ jobs:
- name: Checkout
uses: actions/checkout@v4
+ - name: Set up Python
+ uses: actions/setup-python@v5
+ with:
+ python-version: '3.12'
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: '22'
+
- name: Check required RFC draft
shell: bash
run: |
@@ -26,6 +36,13 @@ jobs:
test -s drafts/agent-surface.md
grep -q '^# Agent Surface Protocol Specification' drafts/agent-surface.md
+ - name: Check RFC review dashboard
+ shell: bash
+ run: |
+ set -euo pipefail
+ python -m pip install --requirement review/requirements.txt
+ make review-check
+
- name: Check Markdown hygiene
shell: bash
run: |
diff --git a/.gitignore b/.gitignore
index 31bfd70..e8bdab5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
/target/
/generated/
.DS_Store
+review/__pycache__/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8be7b9a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,10 @@
+PYTHON ?= python3
+
+.PHONY: review-build review-check
+
+review-build:
+ $(PYTHON) review/build_review.py
+
+review-check:
+ $(PYTHON) review/build_review.py --check
+ node review/check_review.mjs
diff --git a/README.md b/README.md
index 228874d..2436aa4 100644
--- a/README.md
+++ b/README.md
@@ -61,13 +61,33 @@ Agent Surface + Agent Grant bind those pieces into safe app-specific delegation.
agent-surface/
.github/
PULL_REQUEST_TEMPLATE.md
- workflows/docs.yml Markdown and RFC checks.
+ workflows/docs.yml Markdown, RFC, and dashboard checks.
drafts/ Source RFCs written in Markdown.
+ review/ Source data, template, and generated RFC review dashboard.
LICENSE MIT license for repository source code.
LICENSE-CC-BY-4.0 CC BY 4.0 summary for specifications and documents.
CONTRIBUTING.md Contribution guidelines.
```
+## Interactive RFC Review Dashboard
+
+The standalone [RFC review dashboard](review/agent-surface-rfc-review.html) is
+generated from the RFC, card data, and UI template. Do not edit the generated
+HTML directly.
+
+```sh
+python3 -m pip install -r review/requirements.txt
+make review-build
+make review-check
+```
+
+When the RFC changes, update the relevant cards in
+[`review/review-data.json`](review/review-data.json), rebuild the dashboard,
+and commit the RFC, review data, and generated HTML together. `review-check`
+validates card fields and priorities, verifies every linked heading still
+exists in the RFC, checks that the generated artifact is current, and parses
+the dashboard's inline JavaScript.
+
## Status
The specification is experimental and subject to change. The current draft is
diff --git a/review/build_review.py b/review/build_review.py
new file mode 100644
index 0000000..6994b0f
--- /dev/null
+++ b/review/build_review.py
@@ -0,0 +1,160 @@
+#!/usr/bin/env python3
+"""Build the standalone Agent Surface RFC review dashboard."""
+
+from __future__ import annotations
+
+import argparse
+import json
+import re
+import sys
+import unicodedata
+from collections import Counter, defaultdict
+from pathlib import Path
+
+from markdown_it import MarkdownIt
+
+
+ROOT = Path(__file__).resolve().parents[1]
+REVIEW_DIR = Path(__file__).resolve().parent
+RFC_PATH = ROOT / "drafts" / "agent-surface.md"
+DATA_PATH = REVIEW_DIR / "review-data.json"
+TEMPLATE_PATH = REVIEW_DIR / "review-template.html"
+STYLESHEET_PATH = REVIEW_DIR / "standalone.css"
+OUTPUT_PATH = REVIEW_DIR / "agent-surface-rfc-review.html"
+VALID_PRIORITIES = {"P0", "P1", "P2", "P3"}
+VALID_SIDES = {"left", "right"}
+VALID_STATUSES = {"present", "partial", "missing"}
+
+
+def slugify(value: str) -> str:
+ normalized = unicodedata.normalize("NFKD", value).encode("ascii", "ignore").decode()
+ slug = re.sub(r"[^a-z0-9]+", "-", normalized.lower()).strip("-")
+ return slug or "section"
+
+
+def render_rfc() -> tuple[str, dict[str, str]]:
+ markdown = MarkdownIt("commonmark", {"html": False, "linkify": True})
+ tokens = markdown.parse(RFC_PATH.read_text(encoding="utf-8"))
+ occurrences: Counter[str] = Counter()
+ headings: defaultdict[str, list[tuple[int, str]]] = defaultdict(list)
+
+ for index, token in enumerate(tokens):
+ if token.type != "heading_open":
+ continue
+ title = tokens[index + 1].content.strip()
+ level = int(token.tag[1:])
+ occurrences[title] += 1
+ anchor_id = slugify(title)
+ if occurrences[title] > 1:
+ anchor_id = f"{anchor_id}-{occurrences[title]}"
+ token.attrSet("id", anchor_id)
+ token.attrSet("data-asp-heading", title)
+ headings[title].append((level, anchor_id))
+
+ heading_ids = {
+ title: sorted(candidates, key=lambda candidate: candidate[0])[0][1]
+ for title, candidates in headings.items()
+ }
+ return markdown.renderer.render(tokens, markdown.options, {}), heading_ids
+
+
+def load_reviews(heading_ids: dict[str, str]) -> list[dict[str, object]]:
+ payload = json.loads(DATA_PATH.read_text(encoding="utf-8"))
+ reviews = payload.get("reviews")
+ if not isinstance(reviews, list) or not reviews:
+ raise ValueError("review-data.json must contain a non-empty reviews array")
+
+ ids = [review.get("id") for review in reviews]
+ if not all(isinstance(review_id, int) for review_id in ids):
+ raise ValueError("Every review id must be an integer")
+ if sorted(ids) != list(range(1, len(reviews) + 1)):
+ raise ValueError("Review ids must be unique and sequential, starting at 1")
+
+ missing_headings: list[str] = []
+ normalized_reviews: list[dict[str, object]] = []
+ required_fields = {"id", "title", "description", "category", "side", "status", "rationale", "priority", "anchors"}
+ for review in sorted(reviews, key=lambda item: item["id"]):
+ absent = required_fields - set(review)
+ if absent:
+ raise ValueError(f"Review #{review.get('id', '?')} is missing fields: {', '.join(sorted(absent))}")
+ if review["side"] not in VALID_SIDES:
+ raise ValueError(f"Review #{review['id']} has invalid side: {review['side']}")
+ if review["status"] not in VALID_STATUSES:
+ raise ValueError(f"Review #{review['id']} has invalid status: {review['status']}")
+ if review["priority"] not in VALID_PRIORITIES:
+ raise ValueError(f"Review #{review['id']} has invalid priority: {review['priority']}")
+ if not all(isinstance(review[field], str) and review[field].strip() for field in required_fields - {"id", "anchors"}):
+ raise ValueError(f"Review #{review['id']} has an empty text field")
+ anchors = review["anchors"]
+ if not isinstance(anchors, list) or not anchors:
+ raise ValueError(f"Review #{review['id']} must have anchors")
+ anchor_headings = []
+ for anchor in anchors:
+ heading = anchor if isinstance(anchor, str) else anchor.get("heading") if isinstance(anchor, dict) else None
+ if not isinstance(heading, str) or not heading.strip():
+ raise ValueError(f"Review #{review['id']} has an invalid anchor")
+ anchor_headings.append(heading)
+ if len(anchor_headings) != len(set(anchor_headings)):
+ raise ValueError(f"Review #{review['id']} must have unique anchors")
+
+ resolved_anchors = []
+ for heading in anchor_headings:
+ anchor_id = heading_ids.get(heading)
+ if anchor_id is None:
+ missing_headings.append(f"#{review['id']}: {heading}")
+ continue
+ resolved_anchors.append({"heading": heading, "anchorId": anchor_id})
+ normalized_reviews.append({**review, "anchors": resolved_anchors})
+
+ if missing_headings:
+ raise ValueError("Unresolved RFC headings:\n" + "\n".join(missing_headings))
+ return normalized_reviews
+
+
+def build_document() -> str:
+ rfc_html, heading_ids = render_rfc()
+ reviews = load_reviews(heading_ids)
+ template = TEMPLATE_PATH.read_text(encoding="utf-8")
+ fragment = template.replace("", rfc_html).replace(
+ "/*__REVIEW_DATA__*/", json.dumps(reviews, ensure_ascii=False, separators=(",", ":"))
+ )
+ if "" in fragment or "/*__REVIEW_DATA__*/" in fragment:
+ raise ValueError("review template placeholders were not fully replaced")
+ stylesheet = STYLESHEET_PATH.read_text(encoding="utf-8")
+ return f"""
+
+
+
+
+
+
+Agent Surface Protocol — Interactive RFC Review
+
+
+
+
+{fragment}
+
+
+
+"""
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--check", action="store_true", help="fail when the committed dashboard is stale")
+ args = parser.parse_args()
+ document = build_document()
+ if args.check:
+ if not OUTPUT_PATH.exists() or OUTPUT_PATH.read_text(encoding="utf-8") != document:
+ print("Dashboard is stale; run: make review-build", file=sys.stderr)
+ return 1
+ print(f"Dashboard is current: {OUTPUT_PATH.relative_to(ROOT)}")
+ return 0
+ OUTPUT_PATH.write_text(document, encoding="utf-8")
+ print(f"Built {OUTPUT_PATH.relative_to(ROOT)}")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/review/check_review.mjs b/review/check_review.mjs
new file mode 100644
index 0000000..fe96dca
--- /dev/null
+++ b/review/check_review.mjs
@@ -0,0 +1,14 @@
+#!/usr/bin/env node
+
+import { readFileSync } from "node:fs";
+
+const dashboardPath = new URL("./agent-surface-rfc-review.html", import.meta.url);
+const dashboard = readFileSync(dashboardPath, "utf8");
+const scripts = [...dashboard.matchAll(/
diff --git a/review/standalone.css b/review/standalone.css
new file mode 100644
index 0000000..c1b5fa8
--- /dev/null
+++ b/review/standalone.css
@@ -0,0 +1,129 @@
+:root {
+ color-scheme: light dark;
+ --background: light-dark(rgb(250 250 248), rgb(22 23 24));
+ --foreground: light-dark(rgb(28 30 32), rgb(244 245 246));
+ --card: light-dark(rgb(237 238 235), rgb(36 37 38));
+ --card-foreground: var(--foreground);
+ --muted: light-dark(rgb(232 233 230), rgb(48 49 50));
+ --muted-foreground: light-dark(rgb(91 94 97), rgb(167 170 173));
+ --border: light-dark(rgb(28 30 32 / 14%), rgb(244 245 246 / 14%));
+ --primary: light-dark(rgb(23 105 176), rgb(126 191 248));
+ --destructive: light-dark(rgb(190 51 42), rgb(248 111 101));
+ --viz-series-1: var(--primary);
+ --viz-series-2: light-dark(rgb(183 91 29), rgb(239 155 91));
+ --viz-series-3: light-dark(rgb(46 137 77), rgb(112 204 140));
+ --font-size-base: 14px;
+ --font-size-small: 12px;
+ --radius-sm: 5px;
+ --radius-xl: 10px;
+ --radius-2xl: 14px;
+ background: var(--background);
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ margin: 0;
+ min-width: 0;
+ color: var(--foreground);
+ background: var(--background);
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ font-size: var(--font-size-base);
+}
+
+body {
+ padding: 16px;
+}
+
+#widget {
+ width: 100%;
+}
+
+.card {
+ min-width: 0;
+ padding: 12px;
+ overflow-wrap: break-word;
+ border-radius: var(--radius-2xl);
+ color: var(--card-foreground);
+ background: var(--card);
+}
+
+h1,
+h2,
+h3,
+h4,
+p {
+ margin-block: 0;
+}
+
+h1 {
+ font-size: calc(var(--font-size-base) * 1.72);
+ font-weight: 600;
+ line-height: 1.25;
+}
+
+h2 {
+ font-size: calc(var(--font-size-base) * 1.43);
+ font-weight: 600;
+ line-height: 1.25;
+}
+
+h3,
+h4 {
+ font-size: calc(var(--font-size-base) * 1.28);
+ font-weight: 600;
+ line-height: 1.3;
+}
+
+strong,
+th {
+ font-weight: 600;
+}
+
+a {
+ color: var(--primary);
+ text-underline-offset: 3px;
+}
+
+a:focus-visible {
+ outline: 2px solid var(--primary);
+ outline-offset: 3px;
+ border-radius: var(--radius-sm);
+}
+
+.btn {
+ display: inline-flex;
+ min-height: 28px;
+ align-items: center;
+ justify-content: center;
+ padding: 0 8px;
+ border: 1px solid var(--border);
+ border-radius: var(--radius-sm);
+ color: var(--foreground);
+ background: var(--muted);
+ font: inherit;
+ cursor: pointer;
+}
+
+.btn:focus-visible {
+ outline: 2px solid var(--primary);
+ outline-offset: 2px;
+}
+
+code {
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
+}
+
+ul,
+ol {
+ padding-inline-start: 26px;
+}
+
+@media (max-width: 560px) {
+ body {
+ padding: 8px;
+ }
+}
From 72660ce2500b70cf66c612e530bb5629b45a740a Mon Sep 17 00:00:00 2001
From: Egor Merkushev
Date: Sat, 11 Jul 2026 12:01:39 +0300
Subject: [PATCH 2/2] Fix review layout after viewport changes
---
review/agent-surface-rfc-review.html | 133 +++++++++++++++++++++++++--
review/review-template.html | 5 +-
2 files changed, 127 insertions(+), 11 deletions(-)
diff --git a/review/agent-surface-rfc-review.html b/review/agent-surface-rfc-review.html
index 0f83831..fd0aeaf 100644
--- a/review/agent-surface-rfc-review.html
+++ b/review/agent-surface-rfc-review.html
@@ -210,6 +210,7 @@ Abstract
Unless otherwise stated, the following sections are normative:
+- Conventions
- Terminology
- Design Principles
- Agent Surface Manifest
@@ -230,6 +231,8 @@
- Do not require signed grants or signed receipts in the MVP profile.
Conventions
-The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", and "MAY" in this
-document are to be interpreted in the RFC 2119 and RFC 8174 sense when, and only
-when, they appear in all capitals.
+The key words "MUST", "MUST NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", and
+"MAY" in this document are to be interpreted in the RFC 2119 and RFC 8174 sense
+when, and only when, they appear in all capitals.
This is not an IETF document. The keywords are used to make interoperability and
security expectations explicit for future implementers.
Terminology
@@ -743,7 +746,7 @@ Discovery
If the manifest contains sensitive tenant-specific affordances, it MUST require
ordinary authenticated app access.
-The manifest SHOULD be served with:
+The manifest MUST be served over HTTPS and SHOULD be served with:
Content-Type: application/json
Cache-Control: max-age=300
@@ -932,6 +935,7 @@ Actions
output_schema
side_effect
execution
+optional capability_hint
idempotency for side-effecting actions
receipt for side-effecting actions
@@ -957,6 +961,12 @@ Proposal
proposal-only actions.
A proposal-only action is a typed action whose output is a draft, suggestion,
patch, review body, or other non-committed artifact.
+A proposal-only action declares side_effect: false because it does not
+commit domain-visible changes. However, when the application persists
+proposals as drafts — as the proposal flow in this draft assumes — repeated
+proposal requests can still accumulate duplicate drafts under retries and
+agent loops. Applications that persist proposals SHOULD accept idempotency
+keys for proposal actions and deduplicate stored drafts accordingly.
Example:
{
"id": "pull_request.review.propose",
@@ -993,6 +1003,11 @@ Events
"schema": "https://example.com/schemas/ci-failed.event.schema.json"
}
+Grant constraints filter events the same way they filter actions: a grant
+constrained to one repository SHOULD NOT receive events about other
+repositories, even when the event scope matches.
+Event delivery semantics — transport, ordering, acknowledgement, and replay —
+are not defined in this draft; see Open Questions.
Risk Taxonomy
Every action SHOULD have a standard risk label. Runtimes can map risk labels to
local policy defaults.
@@ -1006,6 +1021,15 @@ Risk Taxonomy
| financial_side_effect | Charges, refunds, purchases, invoices, payroll. | Always require explicit approval. |
| destructive | Deletes, closes, revokes, disables, or irreversibly changes state. | Deny by default or require step-up approval. |
| privileged | Changes permissions, secrets, tokens, admin settings, or access policy. | Deny by default. |
+Risk labels are ordered by increasing severity from read to privileged.
+The labels are not mutually exclusive properties of an action: a single action
+can plausibly be described by several of them. When more than one label
+applies, the action MUST carry the most severe applicable label. For example,
+invoice.refund.request is both a mutation and a financial operation; it MUST
+be labeled financial_side_effect, not write.
+The risk label and the side_effect flag MUST be consistent: an action
+labeled write or a more severe label MUST declare side_effect: true, and
+an action labeled read MUST declare side_effect: false.
Applications MAY define extension risk labels, but they SHOULD map them to the
standard labels for runtime interoperability.
Approval Semantics
@@ -1018,6 +1042,17 @@ Approval Seman
| user_or_app | Either a runtime approval or app-side approval MAY satisfy the requirement, depending on grant caveats. |
| runtime_and_app | Both runtime-side and app-side approval are required. |
Approval records SHOULD be linked into receipts.
+The runtime and user_or_app modes allow a runtime-side approval to satisfy
+the requirement. In those modes the application is accepting the runtime's
+assertion that a local user approval occurred. To keep this compatible with
+the rule that an application MUST NOT accept a runtime's self-assertion of
+authority, that acceptance MUST be an explicit grant caveat presented to the
+user at consent time, not a silent default. Action requests that rely on a
+runtime-side approval SHOULD carry an approval reference (for example an
+approval_ref identifier, or in future profiles a signed approval object) so
+the approval can be linked into receipts and audited. Applications that do not
+want to accept runtime approval assertions MUST declare app or
+runtime_and_app for the affected actions.
Idempotency
Every side-effecting action MUST support idempotency.
Action requests SHOULD include:
@@ -1030,6 +1065,13 @@ Idempotency
same normalized input do not repeat the side effect.
If the same key is reused with different normalized input, the application SHOULD
return an idempotency conflict error.
+Idempotency keys are scoped to the grant and action: the application MUST
+treat a request as a duplicate only when the same key is presented under the
+same grant_id and action_id. On a duplicate request, the application
+SHOULD return the original result and receipt reference rather than an error,
+so a retrying runtime can converge on the outcome of the first attempt.
+Applications SHOULD retain idempotency state at least for the remaining
+lifetime of the grant and SHOULD document their retention window.
Applications SHOULD define the input normalization procedure per action, or use
the declared input schema with a canonical JSON profile. A future draft is
expected to define canonicalization requirements for signed receipts and signed
@@ -1052,7 +1094,7 @@
Grant Object
"resource_server": {
"app_id": "code.example.com",
"issuer": "https://code.example.com",
- "surface_version": "code-review-agent-surface/0.1"
+ "surface_version": "2026-06-25"
},
"scopes": [
"pull_request.read",
@@ -1072,6 +1114,16 @@ Grant Object
}
}
+Numeric caveats need defined accounting. In this draft, max_actions counts
+side-effecting action requests accepted by the application under the grant.
+Reads and denied requests do not consume the budget, and neither do idempotent
+replays: a retry deduplicated under a previously accepted idempotency key
+MUST NOT consume the budget again, or lost responses and transport retries
+could exhaust a grant without producing new side effects. max_cost_usd is
+advisory in the MVP profile: the runtime SHOULD meter agent-side cost against
+it, and applications MAY additionally meter app-side cost where actions carry
+a price. When a budget caveat is exhausted, further matching requests MUST be
+rejected with limit_exceeded.
Grant Lifecycle
discover surface
-> verify manifest
@@ -1097,6 +1149,11 @@ M
This is the RECOMMENDED MVP model because it fits existing OAuth/resource-server
deployments.
+Because this draft does not require browser-to-localhost communication, the
+consent flow SHOULD support a completion mode that does not depend on a
+loopback redirect — for example an OAuth device-authorization-style exchange
+or an app-mediated pairing code that the runtime polls or receives over its
+outbound channel.
Pros:
- Easy for applications to enforce.
@@ -1221,7 +1278,7 @@ Session Start
"agent_id": "local_agent_789",
"surface": {
"app_id": "code.example.com",
- "surface_version": "code-review-agent-surface/0.1"
+ "surface_version": "2026-06-25"
},
"task": {
"kind": "pull_request.review",
@@ -1240,6 +1297,15 @@ Action Request
The action request MUST be authorized by the HTTP authorization layer or an
equivalent proof. The grant_id inside the body is a correlation identifier, not
a credential.
+The application MUST also verify that the supplied session_id belongs to the
+presented grant. Otherwise a valid grant credential could be replayed against
+sessions created under other grants, corrupting session accounting and receipt
+linkage.
+If both the Idempotency-Key header and the body idempotency_key field are
+present, they MUST match, and the application MUST reject a mismatch as
+schema_invalid. Accepting a mismatched request and picking either value
+would let app-side deduplication and runtime receipts refer to different
+idempotency identifiers.
Example:
POST /agent-actions HTTP/1.1
Host: example.com
@@ -1366,7 +1432,7 @@ App Receipt
"session_id": "sess_456",
"action_id": "comment.create",
"app_id": "code.example.com",
- "surface_version": "code-review-agent-surface/0.1",
+ "surface_version": "2026-06-25",
"runtime": {
"runtime_id": "application_runtime_456"
},
@@ -1397,8 +1463,16 @@ App Receipt
Receipts MAY be signed by the app, runtime, or both. A future draft is expected
to define canonicalization and signature profiles.
+How the application obtains the runtime_receipt_hash value for the links
+field — for example, in the action request itself or through a later
+submission to the receipt endpoint — is not defined in this draft; see Open
+Questions.
Revocation Semantics
The protocol MUST define what happens when authority changes.
+Revocation MUST be possible from both sides. Applications SHOULD give users an
+in-app view of active agent grants — comparable to OAuth application
+management pages — where a grant can be inspected and revoked without going
+through the runtime.
Grant Revoked
If a grant is revoked:
@@ -1407,6 +1481,10 @@ Grant Revoked
- active sessions SHOULD be cancelled or downgraded to read-only according to
app policy
- receipt generation SHOULD record the revocation event
+- the event channel SHOULD deliver
grant.revoked as a final event before the
+app closes the subscription; delivery of this one event MUST NOT itself
+require an active grant, or the runtime could never learn about the
+revocation through the event channel
Runtime Disconnected
If the runtime disconnects:
@@ -1451,7 +1529,14 @@ Error Model
| passport_invalid | Agent Passport is missing, expired, revoked, or invalid. |
| runtime_untrusted | Runtime binding or attestation is not accepted. |
| surface_incompatible | Runtime does not support the surface version. |
-| proposal_required | The app only supports proposal mode for this action or grant. |
+| proposal_required | The app only supports proposal mode for this action or grant. |
+| session_invalid | Session is unknown, ended, or not associated with the presented grant. |
+| action_unknown | Action id is not part of the surface version the grant was issued against. |
+| limit_exceeded | A grant budget caveat such as max_actions or max_cost_usd is exhausted. |
+| rate_limited | The request was throttled independently of grant caveats. |
+Errors SHOULD be returned in a structured envelope containing at least the
+error code, a human-readable description, and a retryability indication.
+Mapping error codes to HTTP status codes is left to a future draft.
Errors SHOULD be safe to show to users and precise enough for runtime policy
debugging.
Versioning and Compatibility
@@ -1465,9 +1550,13 @@ App-Embedded Runtime
+
The Terminology section allows a runtime to be embedded in an application.
+That deployment collapses the two trust domains this protocol otherwise
+separates: the component that is supposed to protect the user is operated by
+the party the user is being protected from. An app-embedded runtime can
+satisfy the wire protocol while voiding the "runtime protects the user"
+guarantee — its policy checks, approvals, and runtime receipts are all
+app-controlled.
+When the runtime is app-operated, the user's protection reduces to app-side
+consent and app receipts. Runtimes SHOULD disclose their operator during
+consent, and enterprise policy MAY require user-controlled or third-party
+runtimes for high-risk scopes.
Malicious or Compromised Agent
Agents can hallucinate, loop, ignore instructions, leak data, or attempt
unauthorized actions.
@@ -1566,6 +1667,11 @@ Prompt Injection
application content as authority to escalate scopes, reveal secrets, or bypass
policy.
Runtime and app policies SHOULD treat model output as untrusted until validated.
+Session task descriptions, resource payloads, and event payloads are
+app-authored input to the agent and can carry injected instructions. The
+runtime SHOULD present the session task to the user at session start or
+consent time, and MUST NOT allow app-delivered content to widen grant scope,
+weaken approval requirements, or alter local policy.
Replay and Duplicate Actions
Idempotency keys, timestamps, nonce binding, and grant expiration reduce replay
risk. Side-effecting actions MUST be idempotent.
@@ -1709,6 +1815,15 @@ Open Questions
How do users compare two agents with overlapping Agent Passport
capabilities during grant consent?
What happens to active sessions when an app changes surface versions?
+What transport, ordering, acknowledgement, and replay semantics do event
+subscriptions use?
+How does the application obtain the runtime receipt hash that an app receipt
+links to?
+How is max_cost_usd metered when runtime-side inference cost and app-side
+action cost diverge, and which side is authoritative?
+How are runtime-side approvals proven to the application beyond an
+approval_ref identifier — signed approval objects, step-up verification,
+or app-rendered approval UI?
References
diff --git a/review/review-template.html b/review/review-template.html
index 525de32..9abedf2 100644
--- a/review/review-template.html
+++ b/review/review-template.html
@@ -349,7 +349,7 @@
margin-block: 12px 18px;
}
- @media (max-width: 1179px) {
+ @media (max-width: 1231px) {
#asp-review-019f48fb .asp-review-navigation {
position: static;
width: 100%;
@@ -545,6 +545,7 @@
let insertionPoint = anchor;
for (const review of anchorReviews) {
const card = cards.get(String(review.id));
+ card.style.removeProperty("top");
card.classList.add("is-inline");
insertionPoint.insertAdjacentElement("afterend", card);
insertionPoint = card;
@@ -678,7 +679,7 @@
function layout() {
scheduled = 0;
- if (root.clientWidth < 1180) enterNarrowMode();
+ if (root.clientWidth < 1200) enterNarrowMode();
else enterWideMode();
}