Skip to content

feat(agents): agent-connect plugin — owner-controlled agent accounts on the NEAR Intents Agent API - #403

Open
elliotBraem wants to merge 22 commits into
mainfrom
feat/agent-connect
Open

elliotBraem wants to merge 22 commits into
mainfrom
feat/agent-connect

Conversation

@elliotBraem

@elliotBraem elliotBraem commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds the Agent Connect control plane as a dedicated plugin (plugins/agent-connect): every signed-in user gets an owner-controlled agent account on the NEAR Intents Agent API — the owner signs the rules once with their wallet, and agents hold revocable ngt_ grant tokens that only act inside the owner-signed policy.

Also lands:

  • Target-state vision doc (docs/plans/target-state/README.md) — platform primitives inventory, the custody-first default path (decisions recorded 2026-10-16), the economy layer as open design space
  • Vendored agent skill (.agents/skills/near-intents-agent-api, lock-tracked) — the API's rules source for agent sessions
  • Client signing surface in better-near-auth: signAgentIntent (nep413 / nep366 / webauthn)
  • Merge with main (custody publish, deploy-key mint, bos:// import + source archives, consumer CI)

Architecture

Separation of concerns — the domain moved out of the auth stack:

Concern Home
Session identity / ownership principal auth plugin (via context), unchanged
Wallet signing of owner intents better-near-auth client (signAgentIntent)
Agent accounts, policies, intents, grants plugins/agent-connect (this PR)
Domain shapes @near-intents-agent-api/sdk — the only protocol dependency (catalog-pinned); route outputs are z.custom<SDKType> so nothing can drift

BFF split (per the API's canonical pattern): agentCreate/prepareIntent return intent.payload + preview → the browser signs → intentSubmit carries the wallet's output. The naa_ partner key and ngt_ grant tokens never reach a browser.

Invariants (tested):

  1. Session-scoped ownership on every route (agent/grant/correlation id checked against the caller's rows)
  2. Grant tokens sealed at rest (HKDF-SHA256 → AES-256-GCM); only their sha256 commitment enters the intent
  3. Intents persisted before the send (pre-send ledger, request + response jsonb); link failure is fatal and retry-safe
  4. Idempotent prepare: optional client idempotencyKey — same key replays the stored intent or resends the stored request; cross-type reuse conflicts (upstream still enforces body equality)
  5. Submit and status polling share one reconciliation path — a later-observed SUCCESS applies the same local effects as an immediate one

Surfaces: the oRPC contract exposes the domain to REST and MCP via the host's OpenAPI mounts — agent operations are now agent-accessible, which Better Auth endpoints were not.

Migrations

  • plugins/auth: the branch-only 0015_agent-connect was dropped before merge (never published); the auth chain is identical to main (…0014_deep_toad)
  • plugins/agent-connect: fresh single-migration chain (0000_init), own plugin_agent_connect schema

Drift kit

  • SDK catalog pin + types never re-declared
  • Env-guarded live drift-gate test against the hosted /openapi.json (AGENT_CONNECT_LIVE_CHECK=1)
  • Type-level test: generated oRPC client outputs are the SDK's response types (no casts for the UI/helper)
  • Upstream-first policy for protocol needs (NEAR-Intents-Agent-API: sdk-typescript, skills; private api, agent-connect)

Configuration

Dormant until NEAR_INTENTS_AGENT_API_KEY (naa_…) is configured. Secrets: AGENT_CONNECT_ENCRYPTION_KEY (vault key), AGENT_CONNECT_DATABASE_URL (pglite default for dev). Variable apiBaseUrl for non-mainnet deployments.

Testing

  • 18 plugin tests + 1 skipped live drift gate (unit: vault crypto, type fidelity; integration: all route invariants, later-success reconciliation, idempotent reuse/conflict, dormant posture)
  • Root suite green (191), better-near-auth green (129), typecheck 13/13 workspaces, build train green, lint 0 errors

Follow-ups (deliberately not here)

  • Agent-account UI (accounts/policy/budget/grants pages) — next PR
  • Passkey-only owner derivation (near-linked owners only for now)
  • Custody-first bos login default path (vision doc graduation items)

🚀 Squash-merge intended — the branch history carries pre-refactor lineages that the net diff supersedes.

@railway-app

railway-app Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the everything-dev-pr-403 environment in heartfelt-ambition

Service Status Web Updated
upload.everything.dev ✅ Success (View Logs) Oct 16, 2026 at 8:46 pm UTC
8 services not affected by this PR
  • projects
  • auth-db-ISTt
  • api-db-6AR0
  • api-db
  • dev-app
  • projects-db
  • app
  • auth-db

@railway-app
railway-app Bot temporarily deployed to heartfelt-ambition / everything-dev-pr-403 October 15, 2026 06:55 Destroyed
@elliotBraem
elliotBraem added this pull request to stack #404 October 15, 2026 07:03
… repo path (#392)

bos import <workspace> --from <owner/repo>[@ref]|url|dir imports a workspace
subtree (e.g. plugins/auth) from an upstream GitHub repo tarball or a local
directory, without cloning. Shared files are classified against a per-workspace
baseline (.bos/import-snapshot.json, falling back to .bos/sync-snapshot.json)
into added / unchanged / updated / preserved-local / preserved-local-edit /
merged / conflict buckets; local-only files are never deleted and a diverged
package.json is structurally merged instead of overwritten. Unresolvable
conflicts — and a migration chain that cannot be applied verbatim (index
reuse, content drift, local-only journal entries), which is held back rather
than dropped — gate the run: nothing is written without --yes or an
interactive confirm, exiting non-zero with a full .bos/import-report.md
divergence report (conflict checklist, migration collisions and chain delta,
contract route-set delta across both monolithic and split src/contract/**
shapes). Successful imports back up overwritten files to .bos/import-backup/,
record fresh baselines, and support --dry-run, --no-install, and --json.

Closes #392
Refs #393
…0.0-rc.1

The changeset release bumped packages/better-near-auth to 2.0.0-rc.1
(better-auth 1.6.25 → 1.7.7 internal upgrade — no documented API changes),
so the six skills' library_version markers drift and the Check Skills
workflow fails with needsReview on every PR merge commit. Reviewed via
'intent stale packages/better-near-auth': version bump only, no content
regeneration needed.
Applied local journal entries are kept verbatim; colliding upstream
migrations are renumbered and appended after the local tail (.sql +
meta/*_snapshot.json renamed, journal rebuilt with monotonic when).
Repeat imports converge via SQL content-hash matching. ImportResult
gains migrationReconciliation (kept/appended/contentMatched); the
chain is held back only for unresolvable content drift or an
unreadable journal. Appends gate the run behind --yes/confirm like
conflicts.
Give every signed-in user an owner-controlled agent account on NEAR
Intents: the owner signs the rules once (nep413/nep366 via the connected
wallet, webauthn via passkey), agents hold revocable ngt_ grant tokens
that only act inside the owner-signed policy.

- better-near-auth: server plugin agentConnect (dormant without a naa_
  key) wrapping @near-intents-agent-api/sdk behind /agent/* endpoints
  with session-agent-intent ownership checks, persisted idempotency
  keys, and encrypted-at-rest grant tokens; client plugin exposes
  client.agent.* with byte-for-byte intent signing and a one-call
  signAndSubmitIntent; SDK wire types re-exported, not re-declared
- plugins/auth: register agentConnect; drizzle tables agent_account,
  owner_intent, agent_grant (+ generated migration)
- ui/auth: register agentConnectClient; bos.app.ts declares the
  NEAR_INTENTS_AGENT_API_KEY secret
- persist the owner-intent ledger row (keyed by idempotency key) before
  the generate call, then link the real correlation id after — the
  send/persist ordering rule holds across crashes; ledger writes fail
  loud instead of being swallowed
- ownerIntent.grantId held two meanings (local row id for issues, API
  grant id for revokes); rename to grantRowId and resolve the local row
  at revoke-creation time so submit updates one uniform key
- cross-user correlation ids answer 404 (not 403) — no enumeration leak
- type the error-status map as a literal union; drop the lying cast and
  the dead null parameter in the wallet signer bridge
- test the grant-token round trip (encrypt → refuse wrong secret →
  decrypt) and add GLOSSARY entries for the agent-connect vocabulary
meta/_journal.json is per-workspace and resolves nowhere from the skill
file — reword so the docs link check passes on the stacked branch.
@railway-app
railway-app Bot temporarily deployed to heartfelt-ambition / everything-dev-pr-403 October 15, 2026 07:15 Destroyed
The rebase's snapshot/journal resolution clobbered the upstream
0011-0013 entries; restore them verbatim from the base and regenerate
the agent-connect migration as 0014 from the merged schema.
elliotBraem added a commit that referenced this pull request Oct 15, 2026
…410)

ADR 0029 records the three-surface model (connect UX / operating surface /
model tools), the two-ledger rule, the permission layering, and the
default-deny exposure principle; the durable role of #403's endpoint layer
stays open for ticket 16 (ready-for-human). Ticket 17 makes MCP per-tool
permission filtering a prerequisite for ticket 13. Sprint graph, ADR index
(backfills 0027-0028), and GLOSSARY surface vocabulary updated.
Base automatically changed from feat/bos-import to main October 15, 2026 16:24
@railway-app
railway-app Bot temporarily deployed to heartfelt-ambition / everything-dev-pr-403 October 15, 2026 16:25 Destroyed
# Conflicts:
#	.changeset/bos-import-workspace.md
#	GLOSSARY.md
#	packages/everything-dev/skills/cli-reference/SKILL.md
#	packages/everything-dev/skills/init-upgrade/SKILL.md
#	packages/everything-dev/src/cli.ts
#	packages/everything-dev/src/cli/import.ts
#	packages/everything-dev/src/commands/import.ts
#	packages/everything-dev/src/contract.meta.ts
#	packages/everything-dev/src/contract.ts
#	packages/everything-dev/tests/unit/import-from-spec.test.ts
#	plugins/auth/src/db/migrations/meta/0014_snapshot.json
#	plugins/auth/src/db/migrations/meta/_journal.json
#	plugins/auth/src/db/schema.ts
@railway-app
railway-app Bot temporarily deployed to heartfelt-ambition / everything-dev-pr-403 October 16, 2026 18:49 Destroyed
Move the agent-account control plane out of better-near-auth/plugins/auth
into plugins/agent-connect: oRPC contract (agent CRUD, prepare/submit/status
intents, grants), Effect services over @near-intents-agent-api/sdk (the only
protocol dependency — types re-exported, never re-declared), sealed grant
vault, pre-send intent ledger, and an own plugin_agent_connect schema with a
fresh migration chain (the never-published auth 0015 is dropped).

better-near-auth keeps only the wallet side: signAgentIntent signs a
generated intent.payload with the connected wallet (nep413/nep366/webauthn);
the BFF split means the browser never sees the naa_ key or ngt_ tokens.

Drift kit: SDK catalog-pinned, an env-guarded live drift-gate test against
the hosted /openapi.json, and the vendored near-intents-agent-api skill as
the rules source. UI (agent accounts, budget view, grants) is a planned
follow-up.
@railway-app
railway-app Bot temporarily deployed to heartfelt-ambition / everything-dev-pr-403 October 16, 2026 19:38 Destroyed
…t_id, honest idempotency, typed contract

Review findings addressed:
- submit and status polling now share one reconciliation path: a SUCCESS
  observed later applies the same local effects as an immediate one (grant
  id linked, revoke marked, agent row activated)
- the intent ledger records the reserved agent_id at link time, so
  agent_create rows can activate on later success
- idempotency made real: prepare routes accept an optional client
  idempotency key (8-128 chars); the stored response replays on reuse, an
  interrupted prepare resends the exact stored request body under the same
  key, and key reuse across intent types conflicts
- contract outputs typed where the UI/helper need it (agent_id, signer,
  idempotencyKey, grant views) with passthrough elsewhere - the SDK stays
  the only shape authority

No migration changes needed: the new plugin's unpublished 0000 chain was
regenerated in place; the auth chain is untouched vs main.
…tion, idempotent prepare, type fidelity

- stateful SDK mock (submit/poll status controls, generate-calls counter)
- reconciliation tests: a SUCCESS observed by polling links the grant id
  and activates the agent row exactly like an immediate success (asserted
  against the store via the runtime's effect context)
- idempotency tests: the same key replays the stored response without a
  second upstream generate; cross-type key reuse conflicts
- type-level drift gate: the generated oRPC client returns the SDK's own
  response types, so signAgentIntent and the UI consume them without casts
- contract outputs switched to z.custom<SDKType> (the SDK is the shape
  authority — zero re-declaration), README invariants + changeset updated
@elliotBraem elliotBraem changed the title feat(auth): agentConnect plugin for the NEAR Intents Agent API feat(agents): agent-connect plugin — owner-controlled agent accounts on the NEAR Intents Agent API Oct 16, 2026
# Conflicts:
#	packages/better-near-auth/src/client.ts
The regression env guard refused to run: the new plugin's database secret
was absent from the committed .env.test, so it resolved to the dev database
(localhost:5432) and the stack preflight probed a port with nothing
listening. Map it to the shared test api database, matching every other
plugin *_DATABASE_URL and byte-for-byte what bos dev regenerates.

Verified locally: regression HTTP dev (20/20) and start (20/20) green with
the plugin booting in the stack.

This branch was successfully deployed

1 active (outdated) deployment
heartfelt-ambition / everything-dev-pr-403 — fe0f2d08 Deployed Oct 16, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant