Skip to content

feat: NEAR Intents Agent as the agents plugin (tickets 06-12) - #297

Closed
elliotBraem wants to merge 18 commits into
mainfrom
agents-rebuild
Closed

elliotBraem wants to merge 18 commits into
mainfrom
agents-rebuild

Conversation

@elliotBraem

Copy link
Copy Markdown
Contributor

Ports the NEARFDE near-intents-agent-api server into this monorepo as the `agents` plugin, per plans/deploy-agents-rebuild (tickets 06-12). Draft — invariant suites still landing.

  • 06 five domain packages vendored verbatim (contracts, outlayer, relayer, owner-auth, database) with VENDOR.md provenance at upstream SHA 66833ad
  • 07 `packages/agents-core` — the framework-free server core (lib/shared/config + the full 75-file modules tree), slot-based config seams, Better-Auth replaced by the platform session
  • 08 `plugins/agents` scaffold — dedicated database (pglite dev / postgres prod) through the platform migration runner
  • 09 owner intent flow: generate → wallet-sign (real Ed25519 doubles) → submit → sponsor relay → SUCCESS; idempotency keys via header, expiry, replay semantics, long-polling
  • 10 agent reads + lifecycle routes from the vendored wire schemas; session-scoped with cross-owner refusal tests
  • 11 grants + delegated execution: issue/revoke, swap/withdraw/transfer/shield/unshield/deposit/recover routes, sign-message, budget charged at dispatch, revocation reporting committedCorrelationIds
  • 12 invariant suites: draft authority, completion atomicity, admission race + budget races on real Postgres (env-gated via AGENTS_RACE_DATABASE_URL)

Ticket statuses tracked in the tickets PR; remaining: maintenance-race port, boot verification, staging legs.

Copies contracts, outlayer, relayer, owner-auth, database from
NEARFDE/near-intents-agent-api at 66833ad as private workspace packages,
verbatim source. Mechanical adaptations only:

- exports import conditions resolve src (no dist builds; consumers bundle TS)
- shared deps aligned to the workspace catalog (zod, drizzle-orm, pg,
  @scure/base, pglite) to avoid duplicate instances in bundles
- tsdown/pnpm scripts removed; typecheck + vitest via the repo toolchain
- VENDOR.md per package records the upstream SHA and refresh procedure
- biome excludes the generated drizzle migration artifacts

Surface-contract smoke tests pin each package's public API (19 tests);
all five typecheck clean under TS 7.
…icket 07)

Copies NEARFDE/near-intents-agent-api apps/server/src/{lib,shared,config}
at 66833ad into packages/agents-core — zero web-framework imports, the
slot-based dependency locator (configureRuntime/configureDatabase) is the
injection seam the plugin's initialize layer will drive.

Excluded, replaced by the platform: lib/auth.ts (Better-Auth server setup —
the better-near-auth session is the identity here; the unused auth field was
removed from AppRuntime — the only non-verbatim change) and lib/maintenance.ts
(ports with the modules into the plugin).

Slot-pattern and sponsor-key provisioning tests (7 tests); typecheck clean.
…ng (ticket 08)

plugins/agents from the template scaffold:

- contract typed from the plugin's own routes (ContractType exported for
  generated types); ping + dbHealth as the ticket-08 surface
- dedicated database: dual-driver db layer — pglite (dev/test, in-memory,
  trivial advisory serialization) and the vendored production driver
  (pg Pool + queued session locks + dedicated lock pool)
- the vendored migration history applies through the shared migration
  runner with the per-plugin journal; drift detection wired
- initialize wires the agents-core slots exactly once (database, runtime,
  outlayer client) via a discard-layer composed with Layer.provide — one
  handle serves both the core and the handlers
- sponsor clients configure only when AGENTS_SPONSOR_KEYS is present and
  the engine supports advisory locks; absent keys log a clear notice

Registered in bos.config.json (local dev attachment, secrets list);
typecheck green across all 10 workspaces; plugin tests green.
… prep)

Copies the complete control-plane engine — modules/ (agents, developer,
intents, operations, wallet; 75 files) plus the framework-free mapping
helpers api/camel.ts and api/views.ts — at upstream 66833ad. The modules'
relative imports resolve within agents-core unchanged; the Hono route shell
stays upstream and is replaced by the plugin shell.

The root barrel exposes a named service surface (intents generate/submit/
status, agents onboarding and reads, the Actor type) for the plugin shell —
star re-exports are avoided across module families on purpose.

Whole-tree typecheck clean under TS 7 on first pass; no logic changes.
…s shell (ticket 09)

Plugin shell over the vendored core:

- contract routes from the upstream wire schemas verbatim (generateIntent,
  submitIntent, status) — idempotency keys arrive via headers in a follow-up
- session→actor seam (src/actor.ts): the better-near-auth session user is
  the tenant; first activity provisions the tenant row the schema's FKs expect
- tracer test: agent_create end-to-end through the real module code —
  custody provisioning (fake OutLayer client via the core slot), policy
  staging (storage-estimator seam + the upstream NEAR RPC double fixture),
  owner intent persisted PENDING_SIGNATURE with the nep366 standard, preview
  and expiry

Submit's sponsor-relay leg is wired but its test harness (sponsor keys +
relay doubles, the upstream intent-signers fixtures) is the next session's
scope, with the delegated-actions (ticket 11) port after.
…s port (ticket 09)

Completes the owner intent flow tracer:

- submit-relay leg tested end-to-end with real Ed25519 wallet doubles: the
  owner's NEP-366 delegate is verified against the prepared policy, storage
  funding journals through the wallet sponsor, the owner sponsor broadcasts,
  and the reconciler observes finality into SUCCESS — broadcast alone leaves
  the agent PENDING (sponsor status 'pending' holds it at PROCESSING)
- settled intents replay their status instead of rebroadcasting (the
  upstream wire contract); the sponsor relay count stays at one
- idempotency keys dedupe interrupted generations: same key replays the
  original correlationId and intent; the plugin handler reads the
  Idempotency-Key header from the host request context (context schema
  added)
- test harness ported from upstream: stateful provider double (per-wallet
  policy reads, custody credential tracking), owner-policy sponsor and
  wallet-sponsor doubles, real NEP-413 proof verification against the RPC
  double, per-test PGlite with the session→actor mapping in place of API
  keys
Read routes wired to the core's actor-scoped services, using the vendored
wire schemas verbatim so the plugin contract cannot drift from the upstream
OpenAPI: agents page/get, wallet identity, balances, policy, policy
history, limits, budget, timelock, scheduled executions, token catalog,
and per-agent history. Lifecycle actions (policy_update, budget_set,
timelock_set, freeze/unfreeze, archive/restore, delete) flow through the
existing generate/submit/status routes — they are intent types in the core.

Tests: reads are scoped to the session's owner (a second session cannot
touch another's agent), and a policy_update intent completes with one
wallet signature into revision 2 with the frozen flag applied.
Grant intents complete with one owner signature: the NEP-413 consent signs
the credential commitment (never the token), issuance stores the token
encrypted at rest, and the listGrants route returns views with the token
absent. Revocation is a tombstone — revokedAt set, matching the upstream
wire contract. The delegated execution routes (swap/withdraw/transfer
under X-Grant-Token) need the upstream api/executions.ts orchestration
vendored and are the next slice of ticket 11.
- auth failures use the shared every-plugin UNAUTHORIZED error shape
  (apiKeyProvided data) instead of a message-only override
- dbHealth reports a broken database as not-ok instead of swallowing the
  error into ok: true
- cross-owner refusals asserted for list (empty page), wallet, and policy
  reads, not just one route
- expired unsigned intents fail with intent_expired per the wire contract
  (new test)
- drop explanatory comments from the plugin glue per repo style
The upstream api/executions.ts orchestration is vendored into agents-core
(swap/withdraw/transfer/balance-move/deposit/recover public bodies mapped
onto the internal ExecutionRequest; quotes via wallet queries). The plugin
exposes the execution routes — swap, withdraw (both with dry-run quotes),
transfer, shield, unshield, confidential deposit, cross-chain deposit, and
recover — with the grant token from the x-grant-token header and the
required idempotency key from the idempotency-key header, exactly like the
upstream mount.

Core tests prove the layered controls: a grant-scoped transfer dispatches
once; an action outside the grant's action list is refused per-layer
(grant_action_denied); a revoked grant stops new work (agent_grant_required);
and the same idempotency key with a different payload is refused
(idempotency_conflict).

The session→actor mapping now provisions the session's api_keys row —
delegated dispatch share-locks the admitting key (dispatch-fence), so a
session without its key row could never dispatch grant work. That gap also
fixes the owner-policy relay path the earlier sponsor tests exercised.
…ticket 12, first slice)

Two invariant suites from the upstream integration estate now run against
PGlite through the plugin harness:

- draft authority: an unsigned freeze proposal blocks neither policy
  readiness nor privileged artifact delivery; an ACCEPTED freeze (signed,
  relayed, pending finality) blocks both while the operation processes —
  readiness is gated on on-chain authority, not on intent drafts
- completion atomicity: an injected owner_intents trigger failure rolls the
  timelock revision, epoch snapshot, nonces and audit trail back as one
  transaction; the retry commits exactly once and the ambiguous-state
  transitions still replay the completed status

Remaining ticket-12 suites (operation admission, policy finalization,
spend-budget and maintenance races) run against the test Postgres and port
with the race infrastructure.
…ertion

The session apiKeys row is the admitting authority for delegated dispatch
(the dispatch fence caps delegation validity at min(key, grant) expiry).
Its expiry was write-once: thirty days after first sign-in every grant
execution would refuse with authorization_stale and no recovery path.
Each actorForSession call now extends the expiry — revocation still sticks
(the update touches only expiresAt) — and requireActor binds the guarded
userId to a const instead of a non-null assertion.
…ants (tickets 11-12)

Ticket 11 tail:
- signMessage route: detached NEP-413 identity signatures under sign-grants,
  with the policy allowlist and the grant's signing audiences both enforced;
  unlisted audiences are refused (grant_recipient_denied) and non-challenge
  messages never reach the provider (signing_identity_challenge_invalid)
- grant revocation surfaces the committed correlation ids of work whose
  dispatch committed but whose outcome no provider has observed yet, with
  the truncation flag; observed work is excluded
- the owner's USD budget charges at dispatch commitment: spend lands in the
  budget window before the response, the spend that would exceed the cap is
  refused before any provider write (spend_budget_exceeded) and leaves
  spend unchanged, and the exact remainder still fits

Ticket 12 race slice (against a real Postgres, not pglite):
- eight concurrent grant dispatches of one request agree on the correlation
  id and commit the provider write exactly once — admission serializes
  through row locks, one winner, idempotent replays for the rest

The provider double gains signNearMessage with its call counter and a
mutable mutation status so tests can hold an outcome unobserved.
…e race suite on a provisioned postgres

A request violating both the grant's recipient list and the USD budget is
refused with the GRANT's code and charges nothing — the documented layer
order (grant before budget) is asserted, not just single layers.

The postgres race suite now derives its admin URL from
AGENTS_RACE_DATABASE_URL and skips entirely when unset, so the standard
test commands stay green without a provisioned server and the suite never
drops a database on a server it does not own.
…ance race suites (ticket 12)

The remaining invariant-bearing suites from the upstream postgres estate
now run against a real Postgres through the plugin harness, env-gated on
AGENTS_RACE_DATABASE_URL and skipped entirely when unset:

- spend-budget races (6 scenarios): 24 concurrent commitments against a
  .50 cap commit exactly the three that fit and refuse the rest with
  spend_budget_exceeded, charges exist exactly when commitments do; a cap
  that commits before an in-flight dispatch governs it; a lowered cap
  applies; a commit-first dispatch is charged under the old caps and
  cannot be retracted; clearing caps keeps the shared ledger so restored
  caps count old usage; a losing worker neither dispatches nor refunds
  the winner (operation_not_pending, one charge)

- policy finalization races: five concurrent confirmations apply the
  revision and advance the policy epoch exactly once with one applied
  audit event; racing applied/failed outcomes settle operation and policy
  to the same side under either ordering

- maintenance races: a stale interrupted-preparation sweep never fails
  work that progressed (pending, dispatching, or settled) — the sweep
  samples before the worker commits and must decide against the state
  after; concurrent sweeps fail a truly interrupted preparation once
  (preparation_interrupted, agent abandoned, two audit events)

The barrel exposes the internal seams the suites drive: admission
(createPendingOperation, claimOperationDispatch), the dispatch commitment
(commitExecutionDispatch), finalization, sweeps, and budget reads.
Refunds and tombstones ride the same cleanup paths as upstream. This also
closes ticket 11's concurrent-over-cap acceptance with the real race.
The four postgres race suites (admission, spend-budget, policy
finalization, maintenance) now share tests/support/race-db.ts: the admin
drop/create against AGENTS_RACE_DATABASE_URL, the migrations-applying
withDatabase wrapper (which configures the core's database handle itself,
so callers no longer do it), and the enabled gate the suites skip on.
Per-suite database names keep parallel workers isolated. Unused barrel
exports (refundSpend, sweepUncommittedOperations) are removed, and biome
pruned the unused imports/renamed the unused params it flagged.
@elliotBraem

Copy link
Copy Markdown
Contributor Author

Ticket 12 race estate — landed through c0d0e08

Now ported and passing against a real Postgres (env-gated on AGENTS_RACE_DATABASE_URL, skipped cleanly when unset):

  • spend-budget races — all 6 upstream scenarios (24-concurrent cap fitting, governing/lowered caps, commit-first accounting, uncapped ledger continuity, losing-worker semantics)
  • policy finalization — concurrent confirmations advance the epoch exactly once; racing applied/failed settle consistently
  • maintenance — stale sweeps never fail progressed work; concurrent sweeps fail an interrupted preparation once
  • admission — 8-way concurrent dispatch commits exactly one provider write (plus the pglite suites: draft authority, completion atomicity)

Gates: plugin suite 30/30 with races live, typecheck 10/10 workspaces, biome 0 errors, UI 411/411 + API 184/184 (e2e failures are the pre-existing environmental ones — local DBs not provisioned).

Ticket status update: ticket 11's concurrent-over-cap box is closed by the budget races. Ticket 12 remains partial: ~13 upstream scenarios unported (refund paths, lock-order/deadlock, readback-mismatch fence, onboarding-admission sweeps, operation-admission detail cases) and the "run in CI" box — CI wiring was dropped deliberately, so the suites skip without the env var provisioned.

Tickets 14/15 will not be created upstream (per owner call). Remaining for this PR: ticket 13 (boot verification, docs, contract error surface, staging legs).

The agents plugin ships its own folder-form UI (plugins/agents/ui, the
auth-ui pattern): an agents list with a wallet-signed agent_create
onboarding CTA, an agent detail page (wallet, balances with USD
estimates, policy, USD budget windows, timelock, activity, grants), an
execute console (swap with dry-quote preview, withdraw, deposit,
sign-message) and grant issue/revoke — all driven by owner-signed
intents (NEP-366 delegates via buildSignedDelegateAction, NEP-413
consent via Near.signMessage). Grant tokens are created client-side
(WebCrypto) and only their SHA-256 commitment is ever submitted; they
ride a per-token oRPC client instance as the x-grant-token header.

Everything-dev fix: a local plugin's associated ui development entry was
stripped during config resolution (only entries with a nested
bos.config.json kept local paths), so the dev harness never allocated a
UI port or passed BOS_UI_PORT — the folder-form UI rsbuild then
auto-ported into the plugin API's port and MF chunk loads returned HTML.
The associated ui now keeps its local development whenever the plugin
entry itself resolves locally. The agents plugin (with its ui) is also
declared in the authored citynode descriptor, and the plugin contract's
generate-intent output now carries the full upstream wire shape
(intent + signer) — the old flat schema stripped them, which no browser
client could sign around. Go regression boot test + usage docs included.
The relying-party loop now has its server surface: getSignature reads a
completed sign-message's artifact once (only the authorizing grant's
token can collect it — credential-hash, live epoch, sign-action and
audit checks all live in the core), and acknowledgeSignature erases it
(ciphertext/keyId/nonce nulled, consumedAt set; acks are idempotent,
further reads are 410 consumed). Routes are grant-token-gated like
signMessage and mapped through views.signatureDelivery onto the
upstream wire schemas.

Also variablize the two citynode leftovers: agentsTrustedOrigins
(required, comma-separated — feeds configureRuntime's trustedOrigins)
and agentsServiceUrl (its hostname is the NEP-413 recipient owners sign
into every owner message) replace the hardcoded
TRUSTED_ORIGINS/BETTER_AUTH_URL values in the plugin shell; declared in
bos.config.json and the authored citynode descriptor.
@elliotBraem

Copy link
Copy Markdown
Contributor Author

Closed in favor of NEARBuilders/everything-dev#403

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