diff --git a/GLOSSARY.md b/GLOSSARY.md index 05ae08bd4..376cb7aca 100644 --- a/GLOSSARY.md +++ b/GLOSSARY.md @@ -105,3 +105,17 @@ _Avoid_: provision, register **Lane**: One of a Gas Key's independent nonce sequences; parallel sends pick different Lanes and never serialize on each other. _Avoid_: nonce (the per-lane counter), slot + +## Agent surfaces + +**Connect Surface**: +The owner-facing agent lifecycle in `better-near-auth`'s `agentConnect` — agent creation, grant issue/revoke, and intent signing/submission; gated by session plus wallet or passkey signature. See ADR 0029. +_Avoid_: agent API (that's the upstream hosted service), agent endpoints + +**Operating Surface**: +The `plugins/agents` contract-generated control plane over the vendored engine — reads, lifecycle, and delegated executions; session-gated for owner routes, grant-scoped for delegated ones, with declarative per-operation gates (undeclared means invisible). See ADR 0029. +_Avoid_: agents plugin surface, agent RPC + +**Model Tools**: +The MCP tool subset generated from the Operating Surface's OpenAPI spec — balances, rules, tokens, quotes, executions; one grant per MCP client, and never owner intents (those belong to the owner's UI with a wallet signature). +_Avoid_: agent tools (unqualified), MCP surface (the catalog is larger than this subset) diff --git a/docs/adr/0029-agent-surface-split.md b/docs/adr/0029-agent-surface-split.md new file mode 100644 index 000000000..9cb71ecb3 --- /dev/null +++ b/docs/adr/0029-agent-surface-split.md @@ -0,0 +1,102 @@ +# 0029. Agent surface split — connect UX, operating surface, and model tools + +Date: 2026-10-15 +Status: Proposed + +## Context + +PR #403 ships `agentConnect` in `better-near-auth`: seven hand-curated Better Auth +endpoints (`agent/create`, `agent/intents/submit`, `agent/status`, `agent/list`, +`agent/grants/issue|revoke|list`) behind Better Auth `sessionMiddleware`, with +session↔agent↔intent ownership checks, a persisted idempotency ledger, and grant +tokens encrypted at rest — a thin adapter over the hosted NEAR Intents Agent API, +funneled through one SDK seam (`src/agent-api.ts`). + +The deploy-agents-rebuild sprint (Stream B, tickets 06–13) vendors the engine +in-process as `plugins/agents` — its own dedicated database, its contract generated +from the vendored wire contracts ("identical public API surface to the upstream +control plane… no hand-copied duplicates"). Ticket 13 fixes the auth model: +"session-authenticated for owner routes, grant-scoped for delegated actions." + +Left unexamined, the two efforts collide in three ways: + +1. **Surface conflation.** Owner connect/sign UX, the platform operating surface, + and the model-facing tool surface have different credentials and different + audiences; nothing yet assigns them to distinct layers, so each new capability + (executions, MCP agent tools, EVM owners) risks being bolted onto whichever + surface happens to exist. +2. **Double ledger.** #403's `agent_account` / `owner_intent` / `agent_grant` tables + live in the auth plugin's database, while ticket 08 gives `plugins/agents` its own + dedicated database. Without a stated rule, two stores both claim to be the record. +3. **Unfiltered MCP.** The host MCP server generates one tool per OpenAPI operation + with no per-tool or per-permission filtering — any authenticated session or API + key sees the entire catalog. The moment agents-plugin routes merge into the api + shell router, swap/withdraw-class operations become visible to every broad key. + +The upstream Agent API's own guidance points the same direction: owner intents +(policy, grants, freeze, delete) belong to the owner's UI with a wallet signature and +must never become model tools; the model gets a small read/quote/execute surface +gated by grants; your database is "a cache plus your retry ledger," never the +authority. + +## Decision + +1. **Three surfaces, one engine.** Every agent capability belongs to exactly one of: + + - **Connect Surface** — `better-near-auth` `agentConnect`. Credential: session + + wallet/passkey signature. Scope: `agent_create`, grant issue/revoke, intent + signing and submission — the owner-controlled lifecycle, packaged as a library + any Better Auth consumer can adopt. + - **Operating Surface** — `plugins/agents`. Credential: session for owner routes, + grant for delegated routes. Scope: the contract-generated control plane (reads, + lifecycle, executions) built on the vendored engine. + - **Model Tools** — the MCP subset generated from the Operating Surface's OpenAPI + spec. Credential: grant, one per MCP client. Scope: balances, rules, tokens, + quotes, swap/transfer/withdraw — never owner intents. + +2. **Two-ledger rule.** The engine's dedicated database is the source of truth for + policy, grants, custody, and execution state. The auth plugin's agent tables are + the session↔agent↔intent binding, the idempotency/retry ledger, and encrypted + grant-token custody — a cache plus ledger, never the authority. `runIntent`'s + persist-before-send ordering defines the ledger discipline either way. + +3. **Permission layering.** The app's config-declared permissions (ADR 0028) gate + *reachability* only — who may touch the agent surface at all. The owner-signed + policy bounds *capability* — what any agent can do regardless of app role. Grants + are per-actor credentials the app credential *selects* (the server decrypts and + injects the right grant) but never *carries*. Refusals name the blocking layer: + app permission → grant → policy → budget → timelock. + +4. **Exposure principle.** Operating Surface routes are generated from the vendored + wire contracts with declarative per-operation gates (credential class per + operation, config-declared). An undeclared operation is invisible — default-deny, + ADR 0028's "undeclared means denied" applied to surface generation. Role changes + become catalog edits, not endpoint code. Owner-intent operations are session- and + wallet-gated by construction and are never exposed as model tools. + +## Open + +The durable role of #403's endpoint layer once the vendored engine lands — keep as +the owner UX atop the engine (the `agent-api.ts` seam swaps in place), supersede it +with the `plugins/agents` contract surface, or split by credential — is deliberately +unresolved here. Sprint ticket 16 (decision, `ready-for-human`) settles it before +tickets 08–09 start; this ADR is amended to Accepted (or restructured) with the +outcome. + +## Consequences + +- **MCP filtering becomes a prerequisite.** No agents-plugin route may merge into the + api shell router before the MCP server filters tools by effective permissions — + sprint ticket 17 gates ticket 13. +- **Role changes stop changing code.** Admin vs member vs agent maps to which grants + they can mint/revoke and which declared operations they reach — data in the + permission catalog, not new endpoints. +- **#403's generic machinery survives either fork outcome.** `runIntent`, + `mapAgentApiError`, the ownership guards, and grant encryption are surface-neutral + and are reused wherever the Connect Surface lands. +- **Two databases stay coherent** because the two-ledger rule assigns authority + explicitly; sync is one-directional (engine → auth tables) and best-effort with + fail-loud on ledger writes, matching #403's ordering rule. +- Hand-curated endpoint authoring (~6 touch points per upstream endpoint) is bounded + to the Connect Surface's seven interim endpoints; the Operating Surface's growth is + absorbed by generation plus declarative gates. diff --git a/docs/adr/README.md b/docs/adr/README.md index d3305c49b..e1d716feb 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -32,6 +32,9 @@ Format: context → decision → consequences, one file per decision. | [0024](./0024-composition-stays-manifest-based.md) | Composition stays manifest-based; route chunks load lazily | Accepted | | [0025](./0025-auth-identity-value-classes.md) | Auth identity values derive from the runtime config | Accepted | | [0026](./0026-pnpm-workspace-node-runtime-v2.md) | pnpm workspace, node runtime — one toolchain for parent and children (v2) | Accepted | +| [0027](./0027-apps-decomposition-every-app.md) | The `/apps` decomposition — verifiable apps, a runtime composer, and the `every-app` seam | Accepted | +| [0028](./0028-config-declared-permissions.md) | Config-declared organization permissions | Accepted | +| [0029](./0029-agent-surface-split.md) | Agent surface split — connect UX, operating surface, and model tools | Proposed | Skipped numbers: 0006 exists; there are no gaps otherwise. 0015–0018 were @@ -42,7 +45,7 @@ low-reference file in each pair moved (see git history). ## Number assignment discipline - Take the next free number (`ls docs/adr/ | sort -n | tail -1`, then +1 — - next free is **0026**). Never reuse a number, even for a rejected ADR. + next free is **0030**). Never reuse a number, even for a rejected ADR. - When citing an ADR in code, plans, changesets, or AGENTS.md, cite the number **with the filename** if the context could be ambiguous. - Superseded ADRs stay in place with a `Superseded by` status link to the diff --git a/docs/plans/deploy-agents-rebuild/SPRINT.md b/docs/plans/deploy-agents-rebuild/SPRINT.md index e5bddd521..119c79fa0 100644 --- a/docs/plans/deploy-agents-rebuild/SPRINT.md +++ b/docs/plans/deploy-agents-rebuild/SPRINT.md @@ -1,6 +1,6 @@ # Sprint: deploy-agents-rebuild -One initiative, two streams, fifteen tickets. Shared tag: this folder. +One initiative, two streams, two gates, seventeen tickets. Shared tag: this folder. ## Goal @@ -20,6 +20,8 @@ One initiative, two streams, fifteen tickets. Shared tag: this folder. - Stream A (tickets 01–05): branch `chore/consolidate-deploy` - Stream B (tickets 06–13): its own feature branch - Stream C (tickets 14–15): upstream, gated behind Stream B completion +- Gates: 16 (agent surface split decision, ADR 0029) blocks 08–09; 17 (MCP per-tool + permission filtering) blocks 13 ## Dependency graph @@ -30,9 +32,13 @@ One initiative, two streams, fifteen tickets. Shared tag: this folder. 08 ──┴─ 09 ── 10 ── ┤ │──── 11 ────┤ │──── 12 ────┘ + +16 ── 08 ── 09 (decision gates Stream B's start) +17 ──────────── 13 (MCP filtering gates the join ticket) ``` - 06 and 01 can start immediately in parallel. +- 16 resolves before 08 begins; 09 inherits the outcome through 08. - 13 is the join point: the plugin ships on the consolidated deploy train. - 14 and 15 are the upstream proposals, strictly last. diff --git a/docs/plans/deploy-agents-rebuild/issues/16-decide-agent-surface-split.md b/docs/plans/deploy-agents-rebuild/issues/16-decide-agent-surface-split.md new file mode 100644 index 000000000..49323c443 --- /dev/null +++ b/docs/plans/deploy-agents-rebuild/issues/16-decide-agent-surface-split.md @@ -0,0 +1,33 @@ +# 16: Decide the durable agent surface split + +**What to build:** A decision, recorded by amending ADR 0029 from Proposed to +Accepted: once Stream B's vendored engine lands, what is the durable role of PR +#403's `agentConnect` endpoint layer in `better-near-auth`? The three candidate +outcomes, with their tradeoffs: + +- **(a) Keep as the owner UX on top.** `better-near-auth` keeps the curated + connect/sign/grant endpoints; `agent-api.ts` swaps to the vendored engine (the + seam's stated purpose); `plugins/agents` owns the contract-parity operating + surface. Two surfaces, one engine, ledger roles per ADR 0029's two-ledger rule. + Cost: two surfaces to maintain; benefit: the reusable-library distribution story + (any Better Auth consumer gets the connect UX) and zero churn on a merged PR. +- **(b) Supersede with `plugins/agents`.** Stream B's contract-generated surface + replaces #403's endpoints entirely; `better-near-auth` keeps only the + wallet-signer/passkey client primitives. Cost: churn on a merged PR and the loss + of the library story; benefit: one surface, one shape (upstream wire contract). +- **(c) Split by credential.** Owner-intent routes stay in auth (session + wallet + signature); delegated/execution routes exist only in `plugins/agents` (grant + scope). Cost: capability boundaries straddle two packages; benefit: the smallest + each-package story, matching ticket 13's auth split line for line. + +The decision must also settle the two-ledger rule as written (engine DB = source of +truth; auth tables = binding + retry ledger + grant custody) or replace it. + +**Blocked by:** none — this decision gates Stream B's start (blocks 08 and 09). + +**Status:** ready-for-human + +- [ ] Outcome (a/b/c) chosen and consequences written into ADR 0029; status Proposed → Accepted (or the ADR restructured) +- [ ] Two-ledger rule confirmed or replaced in ADR 0029 +- [ ] SPRINT.md dependency graph finalized (16 removed from the frontier) +- [ ] GLOSSARY entries for the chosen surface vocabulary land diff --git a/docs/plans/deploy-agents-rebuild/issues/17-mcp-per-tool-permission-filtering.md b/docs/plans/deploy-agents-rebuild/issues/17-mcp-per-tool-permission-filtering.md new file mode 100644 index 000000000..872f2f498 --- /dev/null +++ b/docs/plans/deploy-agents-rebuild/issues/17-mcp-per-tool-permission-filtering.md @@ -0,0 +1,35 @@ +# 17: MCP per-tool permission filtering + +**What to build:** Permission-aware tool generation in the host MCP server +(`host/src/services/mcp.ts`). Today the generator emits one tool per OpenAPI +operation (skipping only missing operationIds, SSE responses, and duplicates) and +authorization is all-or-nothing — `isAuthenticated` gates the catalog, so any valid +session or API key, even one with an empty permission map, sees and can invoke every +generated tool. Per-route middleware still refuses at call time, but catalog +visibility leaks the surface and invites confused-deputy calls. + +Scope: + +- Operation→permission metadata flows from oRPC contract metadata into the OpenAPI + spec (an extension attribute) so REST docs and MCP share one annotation source. +- `tools/list` filters by the caller's effective permissions (`context.permissions` + per ADR 0028's `getContext`); tool invocation re-checks before dispatch, so the + route middleware and the catalog never disagree. +- Decide and test the default rule for operations without declared permissions + (default-allow preserves today's behavior for existing routes; default-deny + matches ADR 0028's "undeclared means denied" — the choice must be explicit either + way). +- This is a hard prerequisite before any agents-plugin routes merge into the api + shell router (the `things` merge precedent): swap/withdraw-class operations must + never be visible to every broad key. + +**Blocked by:** none — the machinery exists today. **Blocks:** 13 (ship + MCP +verification). + +**Status:** ready-for-agent + +- [ ] Metadata channel decided and emitted by the OpenAPI generator; at least the agents-relevant operations annotated +- [ ] `tools/list` reflects the caller's slice; invocation re-checks permissions before dispatch +- [ ] Default rule for unannotated operations decided, documented, and tested +- [ ] Regression spec: a key with empty/narrow permissions sees a filtered catalog and cannot invoke a gated tool +- [ ] Ticket 13's checklist gains the filtering verification item