Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
6487ee4
feat(agents): vendor the five NEAR Agent API domain packages (ticket 06)
elliotBraem Oct 1, 2026
77a886d
feat(agents): vendor the framework-free server core as agents-core (t…
elliotBraem Oct 1, 2026
b1494b9
feat(agents): scaffold the agents plugin with dedicated-database wiri…
elliotBraem Oct 1, 2026
2850764
feat(agents): vendor the full module tree into agents-core (ticket 09…
elliotBraem Oct 1, 2026
f6597a9
feat(agents): owner intent flow tracer bullet — generate/submit/statu…
elliotBraem Oct 1, 2026
f1086ae
feat(agents): sponsor-relay submit leg, idempotency keys, test harnes…
elliotBraem Oct 1, 2026
4a021c9
feat(agents): agent reads and lifecycle surface (ticket 10)
elliotBraem Oct 1, 2026
2bb30fa
feat(agents): grant issue/revoke surface (ticket 11, partial)
elliotBraem Oct 1, 2026
9f776ac
refactor(agents): apply code-review fixes
elliotBraem Oct 1, 2026
db315a9
feat(agents): delegated execution under grants (ticket 11)
elliotBraem Oct 1, 2026
91d5143
test(agents): port the core intent invariants to the plugin harness (…
elliotBraem Oct 1, 2026
2f558c2
refactor(agents): renew session key expiry and drop the narrowing ass…
elliotBraem Oct 1, 2026
5b572e4
feat(agents): sign-message, revocation reporting, and the race invari…
elliotBraem Oct 1, 2026
9a31d2f
test(agents): order assertion across grant and budget layers; gate th…
elliotBraem Oct 1, 2026
4c1c084
test(agents): port the spend-budget, policy-finalization, and mainten…
elliotBraem Oct 1, 2026
c0d0e08
refactor(agents): extract the shared race-database support module
elliotBraem Oct 1, 2026
0e7748a
feat(agents): plugin UI panel + folder-form ui dev wiring
elliotBraem Oct 1, 2026
b89e575
feat(agents): signature delivery routes + variablized runtime origins
elliotBraem Oct 1, 2026
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
5 changes: 5 additions & 0 deletions .changeset/agents-plugin-ui-dev-wiring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"everything-dev": patch
---

Fix folder-form plugin UI dev wiring: 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 plugin's UI rsbuild then auto-ported into the plugin API's port and MF chunk loads returned HTML. A local plugin's associated ui now keeps its local development whenever the plugin entry itself resolves locally. Also declares the agents plugin (with its folder-form ui) in the authored citynode descriptor.
5 changes: 5 additions & 0 deletions .changeset/agents-signature-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@everything-dev/agents-plugin": minor
---

Expose the upstream signature-delivery loop — `getSignature` (read a completed signature once, under the grant that authorized it) and `acknowledgeSignature` (erase it server-side, one-time-consume with idempotent acks) — grant-token-gated like `signMessage`, backed by the encrypted operation-artifacts store. Also variablize the runtime wiring: `agentsTrustedOrigins` (required; feeds the runtime's trusted origins) and `agentsServiceUrl` (optional; its hostname is the NEP-413 recipient owners sign into every owner message) replace the hardcoded citynode.app/agents.local placeholders.
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ TWILIO_AUTH_TOKEN=
TWILIO_PHONE_NUMBER=
RESEND_API_KEY=

# plugins.agents
AGENTS_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5432/api_db
AGENTS_SPONSOR_KEYS=
AGENTS_SECRET_ENCRYPTION_KEYS=
AGENTS_SECRET_ENCRYPTION_ACTIVE_KEY_ID=

# plugins.template
TEMPLATE_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5432/api_db

Expand Down
3 changes: 3 additions & 0 deletions .env.test
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ API_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5434/api_test_
AUTH_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5435/auth_test_db
BETTER_AUTH_SECRET=regression-test-secret-do-not-use-in-production

# plugins.agents
AGENTS_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5434/api_test_db

# plugins.template
TEMPLATE_DATABASE_URL=postgres://everythingdev:everythingdev@localhost:5434/api_test_db

Expand Down
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@
"!!**/virtual-modules.d.ts",
"!!bos.config.json",
"!!plans",
"!!.agents"
"!!.agents",
"!!packages/database/drizzle"
],
"ignoreUnknown": true
},
Expand Down
18 changes: 18 additions & 0 deletions bos.citynode.app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,24 @@ export default App({
},
}),
plugins: {
agents: Plugin("agents").path("plugins/agents", {
name: "@everything-dev/agents-plugin",
ui: { path: "plugins/agents/ui" },
variables: {
agentsNearRpcUrls: "https://near.drpc.org,https://free.rpc.fastnear.com",
agentsTrustedOrigins: "https://citynode.app",
agentsServiceUrl: "https://agents.local",
agentsSponsorDailyGlobalLimit: 100,
agentsSponsorDailyTenantLimit: 20,
agentsSponsorDailyAgentLimit: 10,
},
secrets: [
"AGENTS_DATABASE_URL",
"AGENTS_SPONSOR_KEYS",
"AGENTS_SECRET_ENCRYPTION_KEYS",
"AGENTS_SECRET_ENCRYPTION_ACTIVE_KEY_ID",
],
}),
template: Plugin("template").path("plugins/_template", {
secrets: ["TEMPLATE_DATABASE_URL"],
}),
Expand Down
21 changes: 21 additions & 0 deletions bos.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,27 @@
}
},
"plugins": {
"agents": {
"development": "local:plugins/agents",
"variables": {
"agentsNearRpcUrls": "https://near.drpc.org,https://free.rpc.fastnear.com",
"agentsTrustedOrigins": "https://citynode.app",
"agentsServiceUrl": "https://agents.local",
"agentsSponsorDailyGlobalLimit": 100,
"agentsSponsorDailyTenantLimit": 20,
"agentsSponsorDailyAgentLimit": 10
},
"secrets": [
"AGENTS_DATABASE_URL",
"AGENTS_SPONSOR_KEYS",
"AGENTS_SECRET_ENCRYPTION_KEYS",
"AGENTS_SECRET_ENCRYPTION_ACTIVE_KEY_ID"
],
"ui": {
"name": "_everything_dev_agents_plugin",
"development": "local:plugins/agents/ui"
}
},
"template": {
"development": "local:plugins/_template",
"variables": {},
Expand Down
286 changes: 280 additions & 6 deletions bun.lock

Large diffs are not rendered by default.

81 changes: 81 additions & 0 deletions docs/agents/agents-plugin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Agents plugin — usage guide

The `agents` plugin re-hosts the NEAR Intents Agent control plane as an
everything.dev plugin: intents custody wallets operated by the platform's
sponsor under the owner's signed policy. The wire contract is generated from
the vendored `@near-intents-agent-api/contracts` schemas, so the public API
surface matches the upstream control plane.

## The three actors

| Actor | Authenticates with | Can do |
|-------|--------------------|--------|
| Owner | NEAR SIWN session | Create agents, sign governance intents, read everything about their agents |
| Grant holder | `x-grant-token` header | Execute within the grant's scope: swap, withdraw, transfer, sign messages |
| MCP client | `x-api-key` (Settings → API Keys) | Everything the API exposes, as generated tools |

## Owner flow

1. Sign in with your NEAR wallet (SIWN). The session binds you as an owner.
2. Open **Agents** in the sidebar (or `POST /api/rpc/agents/generateIntent`
with `type: "agent_create"`). The first intent creates the agent and its
custody wallet.
3. The server builds every byte the owner signs and returns it as
`intent: { standard, payload }`:
- **Policy intents** (`agent_create`, `policy_update`, freeze/unfreeze)
sign as **NEP-366 delegate actions** — the UI calls
`authClient.near.buildSignedDelegateAction(...)`.
- **Consent intents** (`grant_issue`, `grant_revoke`, `budget_set`,
`timelock_set`, …) sign as **NEP-413 messages** — the UI calls
`Near.signMessage({ message, recipient, nonce })` with the base64
nonce decoded to bytes.
4. Submit the wallet's output unchanged as
`submitIntent({ type, correlationId, signedData })` and poll
`intentStatus` for settlement. Submitting the same signature twice is
idempotent.

The sponsor relays the wallet's delegate on-chain (gasless for the owner);
sponsor keys come from the `AGENTS_SPONSOR_KEYS` plugin secret. Without
them, sponsor features are disabled and the log explains what is missing.

## Delegated flow (grants)

A grant is the owner's delegation of bounded execution authority to one
token. The token is created **client-side**; only its SHA-256 commitment is
ever sent:

1. `createGrantCredential()` → `{ token: "ngt_…", commitment: <sha256 hex> }`
2. `generateIntent({ type: "grant_issue", label, credential, actions,
recipients, signingAudiences, expiresAt })` → sign (NEP-413) → submit.
3. Keep the token in your secret manager. Execute with the header:
`x-grant-token: ngt_…` against the execution routes (`/agents/swap`,
`/agents/withdraw`, `/agents/transfer`, `/agents/sign-message`, …).

Every execution is checked against three layers at dispatch: the grant's
actions and destinations, the owner's USD spend budget, and the provider
policy. `dry: true` previews a swap or withdraw (provider quote) without
executing. Revoking a grant (`grant_revoke` intent) retracts future work;
work that already reached its dispatch commitment is reported in
`committedCorrelationIds`.

## MCP usage

Every route is generated as an MCP tool from the OpenAPI spec
(`POST /api/mcp`, discovery at `/.well-known/mcp.json`). Owner routes
require a session cookie or `x-api-key`; delegated routes additionally
require the `x-grant-token` header — pass it per call.

## Local development

```bash
bun run dev # local bos CLI — boots the citynode runtime incl. agents on :3010
```

- `.env` needs `BETTER_AUTH_SECRET` set (auth validate-secrets) and, for the
submit leg, `AGENTS_SPONSOR_KEYS` (testnet sponsor full-access key).
- The plugin's UI is folder-form (`plugins/agents/ui/`) — the dev harness
builds it automatically and composes its routes under the authenticated
mount; the sidebar entry comes from the route's `staticData.nav`.
- Secrets: `AGENTS_DATABASE_URL` (auto-generated from resolved ports in
dev), `AGENTS_SPONSOR_KEYS`, `AGENTS_SECRET_ENCRYPTION_KEYS`,
`AGENTS_SECRET_ENCRYPTION_ACTIVE_KEY_ID`.
37 changes: 37 additions & 0 deletions packages/agents-core/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "@near-intents-agent-api/agents-core",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"typecheck": "tsc --noEmit",
"test": "vitest run"
},
"exports": {
".": {
"types": "./src/index.ts",
"import": "./src/index.ts"
},
"./config": {
"types": "./src/config/env.ts",
"import": "./src/config/env.ts"
},
"./views": {
"types": "./src/api/views.ts",
"import": "./src/api/views.ts"
}
},
"dependencies": {
"@near-intents-agent-api/contracts": "workspace:*",
"@near-intents-agent-api/database": "workspace:*",
"@near-intents-agent-api/outlayer": "workspace:*",
"@near-intents-agent-api/owner-auth": "workspace:*",
"@near-intents-agent-api/relayer": "workspace:*",
"drizzle-orm": "catalog:",
"near-api-js": "7.2.0",
"zod": "catalog:"
},
"devDependencies": {
"@types/node": "catalog:"
}
}
30 changes: 30 additions & 0 deletions packages/agents-core/src/api/camel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* Provider evidence and pass-through records reach the wire camelCased. Signed protocol objects
* (policies, owner messages, wallet requests) never pass through here: their field names are
* part of what the owner signed.
*/

export function camelKey(key: string): string {
return key.replace(/_([a-z0-9])/g, (_, letter: string) => letter.toUpperCase());
}

const maxDepth = 8;

export function camelize(value: unknown, depth = 0): unknown {
if (depth > maxDepth) return value;
if (Array.isArray(value)) return value.map((entry) => camelize(entry, depth + 1));
if (value === null || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value as Record<string, unknown>).map(([key, entry]) => [
camelKey(key),
camelize(entry, depth + 1),
]),
);
}

export function camelRecord(value: unknown): Record<string, unknown> {
const result = camelize(value);
return result && typeof result === "object" && !Array.isArray(result)
? (result as Record<string, unknown>)
: {};
}
Loading