Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions GLOSSARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
102 changes: 102 additions & 0 deletions docs/adr/0029-agent-surface-split.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 4 additions & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
8 changes: 7 additions & 1 deletion docs/plans/deploy-agents-rebuild/SPRINT.md
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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

Expand All @@ -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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading