@finchip.ai/foga 0.6.0-alpha.6 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +35 -14
  2. package/dist/a2a/card.js +54 -0
  3. package/dist/a2a/server.js +276 -0
  4. package/dist/a2a/types.js +20 -0
  5. package/dist/build/bundle.js +2 -1
  6. package/dist/cli-cell.js +176 -16
  7. package/dist/cli.js +150 -5
  8. package/dist/core/world.js +17 -0
  9. package/dist/demo/birth.js +11 -5
  10. package/dist/demo/loop-d.js +39 -1
  11. package/dist/demo/quickstart.js +97 -11
  12. package/dist/genesis/operator-scope.js +1 -0
  13. package/dist/genesis/probe.js +23 -7
  14. package/dist/http/access.js +105 -0
  15. package/dist/http/server.js +84 -43
  16. package/dist/http/signed.js +136 -0
  17. package/dist/mcp/server.js +48 -1
  18. package/dist/node/advise.js +134 -0
  19. package/dist/node/config.js +55 -1
  20. package/dist/node/documents.js +298 -0
  21. package/dist/node/preflight.js +208 -0
  22. package/dist/node/propagation.js +95 -0
  23. package/dist/node/wallet.js +132 -0
  24. package/dist/tools/documents.js +74 -0
  25. package/dist/tools/index.js +3 -1
  26. package/dist/tools/propagation.js +35 -0
  27. package/dist/tools/tenders.js +22 -6
  28. package/dist/util/version.js +2 -0
  29. package/dist/world/sourcify.js +165 -0
  30. package/docs/a2a.md +31 -0
  31. package/docs/design-memo.md +23 -6
  32. package/docs/documents.md +25 -0
  33. package/docs/examples/README.md +2 -2
  34. package/docs/extending.md +49 -0
  35. package/docs/loop-d.md +11 -0
  36. package/docs/mcp.md +18 -11
  37. package/docs/node.md +35 -11
  38. package/docs/og-deployment.md +131 -0
  39. package/docs/propagation.md +41 -0
  40. package/docs/quickstart.md +36 -21
  41. package/docs/wallet.md +62 -0
  42. package/docs/world.md +12 -0
  43. package/package.json +1 -1
  44. package/server.json +2 -2
package/README.md CHANGED
@@ -19,7 +19,11 @@ ERC-8004 (Trustless Agents) is a required dependency: ERC-8419, ERC-8434 and ERC
19
19
 
20
20
  The project delivers three things from one kernel: the **Finch OG Agent** (the first running instance), the **FOGA node software** anyone can run with their own identity, model and wallet, and a **public access layer** (MCP, A2A, HTTP/CLI). It is built in phases; this repository has completed **Phase 2c: the genesis on a public chain**, on top of the Phase 2 genesis (T0–T9) and the Phase 1 world layer.
21
21
 
22
- ## Status: Phase 3d in progress — 3d-1 done (quickstart, an outside agent's Loop D); Phase 3c done — Loop D, the unscripted run; Phase 3b done — the node served over MCP and HTTP; Phase 3a done — the kernel hardened and turned into a node with a capability catalog; Phase 2c done
22
+ ## Status: Phase 3 done (0.6.0, 2026-11-05) — the OG live at foga.finchip.ai, the MCP registry listing, the Sepolia Loop D with an outside agent; 3d-2 (the wallet, signed callers, the documents on chain, preflight, A2A, Sourcify), 3d-1 (quickstart, an outside agent's Loop D), 3c (Loop D with a model), 3b (MCP and HTTP), 3a (the kernel as a node) all done
23
+
24
+ **3d-3 (0.6.0, 2026-11-05).** Phase 3 closes with the reference cell served and the last acceptance run on the public chain. The OG was born fresh on Sepolia by the path any cell takes (`wallet new`, `cell new`, `cell init`, `docs publish`, `node serve` — one key, no migration; docs/og-deployment.md): agent 11261, its skill offered, its documents on chain, its fifteen contracts Sourcify-verified; it is served in host mode under systemd behind Caddy at `foga.finchip.ai` and listed in the MCP registry as `io.github.garyyang-finchip/foga`. Loop D with an outside agent ran on Sepolia and every invariant row passed (docs/loop-d.md, "On a public chain"); what the public chain taught is fixed in this release — a working directory is one run, role funding is priced with a floor, the settlement figure is the tender's own, a log scan is windowed. `foga cell advise` (`src/node/advise.ts`) tells a running cell what to do next — separate the one key into owner, operator and guardian first — and the recommended items print after a quickstart, a birth and at serve; the first run stays minimal on purpose. The covenant list is read off the world (an eleventh covenant appears with no code change) and the manifest carries a `services` slot — the maintainer's catalog, declinable, served by the reference cell at `/services.json` — docs/extending.md. 107 tests.
25
+
26
+ **3d-2 (0.6.0-alpha.7, 2026-11-03).** The second slice of 3d is what a cell needs to stand on its own among other agents, under one principle (docs/design-memo.md, section 10): the wallet is the first step, made by default and never a reason to stop; identity is one's own; only the genome replicates. `foga wallet new` (`src/node/wallet.ts`, docs/wallet.md) makes an agent's keys on its own machine — by default one key in a 0600 file under `~/.foga/wallet` — the identity that fills every role a cell needs; `--cell` for the separated owner/operator/guardian set — never overwritten, no network — and `foga quickstart` makes the default wallet on its first run and births every later cell with the same keys; `--wallet <dir>` on every `foga cell` and `foga node` command takes a role's key from there. A wallet is also a credential: `Authorization: FOGA-EIP712 <envelope>` (`src/http/signed.ts`, `src/http/access.ts`) is an EIP-712 signature in the cell's own domain — bound to one request and good once, or a session for a client with static headers — and the caller is the signing address at the level `access.signers` grants it (the cell's own operator or owner, an allow-list by address or ERC-8004 agent id, a KYA standing threshold, a default); `/api`, `/mcp` and `/a2a` take it, `foga wallet sign` prints it, and every cell's MCP server carries the prompt `become-a-cell` and the resource `foga://docs/quickstart` for an agent that meets it. `foga cell docs build | publish | status` (`src/node/documents.ts`, docs/documents.md) generates the cell's documents from one configuration — the ERC-8004 registration, the ERC-8434 AID document, the ERC-8444 descriptor, the ERC-8454 unit, the A2A agent card, `server.json`, a manifest with content hashes — serves them at `/documents/*` and `/.well-known/*`, and commits them on chain as the operator's keyed job (`setAgentURI`, `setDocumentURI`, `updateDescriptor`, `publishVersion`; only what differs is sent), read back and cross-checked; `docs_status` and `docs_publish` are the catalog's 30th and 31st capabilities. `foga node preflight` (`src/node/preflight.ts`) is the readiness reconciliation — the world, the account and its allow-list, the budget, the keys, the standing, the skills run in the worker, the documents against the chain, the public URL, the model — advisory, `--strict` for CI. `POST /a2a` (`src/a2a/`, docs/a2a.md) is the A2A 1.0 JSON-RPC binding with the card at `/.well-known/agent-card.json`: a quote for anyone, a tender taken for an act-level caller as one task over the catalog's jobs, `GetTask`, `ListTasks`, `CancelTask`, errors by the specification. `foga world verify --sourcify` (`src/world/sourcify.ts`, docs/world.md) submits every FOGA-deployed contract of a world with the standard JSON input of its own compiler run (`forge build` now leaves the build-info beside the artifacts), matched by runtime code; a local chain is a dry run, which CI runs over all 47. The OG instance's default is host mode (`foga node serve`: every entrance, the watchers, no model) with embedded mode (`foga node run`, the runner with a model) on demand. Seven new tests on anvil and offline (`wallet`, `signed`, `documents`, `a2a`, `preflight`, `sourcify`, and `quickstart` extended) are the gate; `foga quickstart` now ends with a cell whose documents are on chain, whose card answers, and whose A2A entrance quotes. Next, 3d-3: the OG deployment runbook, `foga.finchip.ai`, the registry listing, the Sepolia Sourcify run, the OG cell's migration to revision 2 and an outside agent's Loop D on Sepolia.
23
27
 
24
28
  **3d-1 (0.6.0-alpha.6, 2026-11-02).** The package is on npm as `@finchip.ai/foga` (0.6.0-alpha.4 and alpha.5, the latter with the launcher fixed for Windows), so the first slice of 3d is the two things that make it usable from nothing. `foga quickstart` (`src/demo/quickstart.ts`, docs/quickstart.md) is one command to a live cell: anvil, the local world, a cell born with keys nobody held and its two skills, two tenders minted for it, the node served (watchers, HTTP API, MCP over Streamable HTTP), both entrances checked (the handshake and tool list over HTTP with an act-level key, and over stdio with the operator key), and the MCP client configurations — Claude Desktop, Claude Code, Cursor, and the HTTP form with URL and Bearer key — printed and written with every path filled in, under `~/.foga/quickstart`. Loop D gains `--model external` (docs/loop-d.md): the node is served instead of run, an act-level key is minted for whoever proposes, the orchestrator prints a brief (the entrance, the five steps of a tender, what the run does around the agent — nothing about the cases) and waits for the agent — any model, any runtime, a person with `curl` — with the same recovery drill, the same entrance checks and the same acceptance; the evidence names the agent and carries no runner ticks, the doctor marks a case the agent left alone `NotTested` rather than `Fail`, and `--compare` skips what one run did not reach. `test/loop-d-external.test.ts` drives it with a scripted agent through the MCP entrance and every verdict passes, the model-agnostic row included against the kept stub run. Smaller: `.gitattributes` keeps every text file LF (the launcher's shebang included), a `prepare` script compiles a checkout installed straight from git, the shared cell birth (`src/demo/birth.ts`) and anvil helpers (`src/demo/anvil.ts`) serve the demos, and the docs carry the PowerShell and Windows notes. Next in 3d: `foga node preflight`, `docs build/publish` with `setAgentURI`, the A2A card and inbound mapping, Sourcify, then the OG deployment runbook, the documentation site and the registry listing.
25
29
 
@@ -72,16 +76,26 @@ From Phase 1, unchanged in substance:
72
76
  - **The doctor.** `foga doctor` reports five layers per contract — chain and code, interfaces, version match, configuration and permissions, semantic validation — with the verdicts Pass / Fail / Unknown / Not tested / Not applicable. Layers 1–4 probe the chain; layer 5 reads the genesis evidence. A doctor run can be exported as a JSON-RPC probe plan and replayed from results collected elsewhere, so a report never depends on which machine can reach which RPC.
73
77
  - **Sepolia address book.** `worlds/sepolia.json` records every upstream reference deployment at its deployed revision, including the three seams the inventory found (docs/version-lock.md); the completed world and the doctor's report on it are the 2c files above.
74
78
 
75
- What is next: the rest of 3d closes Phase 3 — the readiness check, the documents on chain, A2A, Sourcify, the OG instance's move to a revision-2 account with public endpoints, the registry listing and an outside agent's Loop D run on Sepolia; Phase 4 the hosted node and payments. The design is in docs/design-memo.md.
79
+ What is next: 3d-3 closes Phase 3 — the OG instance's move to a revision-2 account with public endpoints (`foga.finchip.ai`), its documents and sources verified on Sepolia, the registry listing and an outside agent's Loop D run on Sepolia; Phase 4 the hosted node and payments. The design is in docs/design-memo.md.
76
80
 
77
81
  ## Quick start
78
82
 
79
- With Node 22.13+ and Foundry's `anvil` installed, one command births a cell on a local chain and prints the MCP client configuration to connect to it (docs/quickstart.md; in PowerShell quote the name: `npx -y '@finchip.ai/foga' quickstart`):
83
+ With Node 22.13+ and Foundry's `anvil` installed, one command makes your wallet (on the first run; `~/.foga/wallet`, reused after), births a cell with it on a local chain, publishes the cell's documents and prints the MCP client configuration to connect to it (docs/quickstart.md; in PowerShell quote the name: `npx -y '@finchip.ai/foga' quickstart`):
80
84
 
81
85
  ```bash
82
86
  npx -y @finchip.ai/foga quickstart
83
87
  ```
84
88
 
89
+ The wallet alone, for an agent that will only call cells (docs/wallet.md), and a cell on a public chain born with it:
90
+
91
+ ```bash
92
+ npx -y @finchip.ai/foga wallet new # ~/.foga/wallet: one key (your identity, every role); prints the address to fund. --cell for the separated owner/operator/guardian set
93
+ foga cell new --wallet ~/.foga/wallet --name mine --world deployments/sepolia.json --store mine.sqlite --out mine.json
94
+ foga cell init --config mine.json --wallet ~/.foga/wallet
95
+ foga node preflight --config mine.json --wallet ~/.foga/wallet
96
+ foga node serve --config mine.json --wallet ~/.foga/wallet
97
+ ```
98
+
85
99
  From a checkout, three ways in, from most to least demanding:
86
100
 
87
101
  ```bash
@@ -164,18 +178,20 @@ deployments/ world files, doctor reports, genesis evidence (local output
164
178
  prover/ the ZK proving sub-package: run.cjs bridge (credential and predicate proofs), build-predicate.mjs (the development ceremony), its own dependencies
165
179
  zk/ circuits/ (kya_predicate_count_v1.circom) and build/ (verification_key.json committed; the proving key is rebuilt locally)
166
180
  docker/, docker-compose.yml
167
- docs/ design memo, compatibility matrix, version lock, build, world, doctor, cell account, kernel, genesis, zk, node, mcp, loop-d; examples/ (client configurations)
181
+ deploy/ the OG host assets: foga-og.service (systemd, host mode), Caddyfile, og.env.example (docs/og-deployment.md)
182
+ site/ the documentation site for GitHub Pages (docs.foga.finchip.ai): index.html, llms.txt, CNAME
183
+ docs/ design memo, compatibility matrix, version lock, build, world, doctor, cell account, kernel, genesis, zk, node, mcp, loop-d, quickstart, wallet, documents, a2a, og-deployment; examples/ (client configurations)
168
184
  scenarios/skills/ the two ERC-8338 skill packages of the genesis: foga-probe, finch-extract-verify
169
185
  scenarios/fixtures/ the pinned document finch-extract-verify works on in T5 and T7
170
186
  scripts/ install-solc.sh, install-foundry.sh, relay.mjs, pack-smoke.mjs (the npm tarball run from a clean install)
171
187
  src/ TypeScript: build/ (vendor + bundle), world/ (plan + deploy), doctor/ (probe + evaluate),
172
188
  core/ (foga-core: one module per ERC incl. acdf, adrf and swarm, measure, canonicalization, DAG-CBOR packager),
173
189
  kernel/ (store, transaction manager, cell, kernel), genesis/ (roles, operator scope, probe, extract, skill worker, zk, scenario T0-T3, t3-zk, t4..t9, runner),
174
- tools/ (the capability catalog: types, catalog, read, tenders, identity, admin), node/ (config, keys, node, policy, init, migrate, watchers),
175
- mcp/ (the MCP server built from the catalog), http/ (the HTTP API, MCP over Streamable HTTP, API keys),
176
- node/runner.ts and node/model/ (the runner, the stub and anthropic adapters), demo/ (the Loop D demander, orchestration and evidence),
190
+ tools/ (the capability catalog: types, catalog, read, tenders, identity, documents, admin), node/ (config, keys, wallet, node, policy, init, migrate, watchers, documents, preflight),
191
+ mcp/ (the MCP server built from the catalog), http/ (the HTTP API, MCP over Streamable HTTP, API keys, signed callers), a2a/ (the A2A 1.0 binding and card),
192
+ node/runner.ts and node/model/ (the runner, the stub and anthropic adapters), demo/ (the Loop D demander, orchestration and evidence, quickstart), world/sourcify.ts,
177
193
  cli.ts, cli-cell.ts (foga cell / foga node)
178
- test/ unit tests: bytecode and interface ids, skill packages, canonicalization, operation store, transaction semantics against an in-process chain (mock-chain.ts), keys, policy gates, catalog, cell config, swarm/ACDF vectors, extractor, measurement resolver, doctor layer 5; node-cli.test.ts (the Loop D cases through the CLI on anvil), mcp.test.ts (the entrances over MCP and HTTP on anvil; helpers/anvil-cell.ts births the cell), model.test.ts (the stub's table, the Anthropic adapter against a stand-in API, the Loop D verdicts), loop-d.test.ts (the unscripted run with the stub on anvil)
194
+ test/ unit tests: bytecode and interface ids, skill packages, canonicalization, operation store, transaction semantics against an in-process chain (mock-chain.ts), keys, policy gates, catalog, cell config, swarm/ACDF vectors, extractor, measurement resolver, doctor layer 5, sourcify; node-cli.test.ts (the Loop D cases through the CLI on anvil), mcp.test.ts (the entrances over MCP and HTTP on anvil; helpers/anvil-cell.ts births the cell), model.test.ts (the stub's table, the Anthropic adapter against a stand-in API, the Loop D verdicts), loop-d.test.ts (the unscripted run with the stub on anvil), loop-d-external.test.ts, quickstart.test.ts, wallet.test.ts, signed.test.ts, documents.test.ts, a2a.test.ts, preflight.test.ts (3d, on anvil)
179
195
  vendor/ LOCK.json (sources are materialized here and gitignored)
180
196
  worlds/ world configs: local.json (deploy), sepolia.json (attach)
181
197
  ```
@@ -188,27 +204,32 @@ foga build [--verify] [--solc-dir <dir>] [--only <repo,repo>]
188
204
  foga bundle info
189
205
  foga world deploy --world <config> [--rpc <url>] [--out <file>] [--resume] [--gas-multiplier <x>] [--allow-chain-mismatch]
190
206
  foga world attach --world <config> [--out <file>]
207
+ foga world verify --world <world.json> --sourcify [--dry-run] [--only <id,id>] [--server <url>] [--out <dir>] [--build-info <dir>] source verification from the bundle's own compiler runs
191
208
  foga doctor --world <world|config> [--attach] [--rpc <url>] [--out <report>] [--export-probe <file>] [--import-results <file>] [--genesis <evidence>]
192
209
  foga probe relay-script
193
210
  foga genesis run --world <world> [--rpc <url>] [--until T0..T9] [--store <file>] [--out <evidence>] [--zk auto|require|skip] [--crash-after <key>] [--crash-before <key>]
194
211
  foga genesis status --world <world> [--store <file>]
195
212
  foga keys new [--out <file>] | show | export --role <role> [--out <file>] | check
213
+ foga wallet new [--name <n> | --dir <dir>] [--cell] | show [--rpc <url>] | sign (--config <cfg> | --url <node> | --cell <acct> --chain-id <n>) [--role] [--ttl <s>] [--agent-id <id>] [--method --path [--body]]
214
+ an agent's own key (the first step; --cell for the separated owner/operator/guardian set), and a signed Authorization header for a cell
196
215
  foga demo [--port <n>] [--keep] [--genesis <T0..T9>]
197
216
  foga cell list [--public] | schema <capability>
198
- foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --owner <addr> --operator <addr> --guardian <addr> --out <cell.config.json>
199
- foga cell init --config <cell.config.json> [--capital-wei <n>] (owner key)
217
+ foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --out <cell.config.json> (--wallet <dir> | --owner <addr> --operator <addr> --guardian <addr>)
218
+ foga cell init --config <cell.config.json> [--wallet <dir>] [--capital-wei <n>] (owner key)
200
219
  foga cell migrate --config <cell.config.json> --to <new.config.json> [--operator <addr>] (owner key)
201
- foga cell <capability> --config <cell.config.json> [--mode simulate|build|act] [--<field> <value> ...] [--json '<input>']
220
+ foga cell docs build | publish | status --config <cell.config.json> [--wallet <dir>] the cell's documents, served and committed on chain (publish: operator key)
221
+ foga cell <capability> --config <cell.config.json> [--wallet <dir>] [--mode simulate|build|act] [--<field> <value> ...] [--json '<input>']
222
+ foga node preflight --config <cell.config.json> [--wallet <dir>] [--no-executors] [--no-network] [--out <report.json>] [--strict] the readiness reconciliation; sends nothing
202
223
  foga node tick | watch --config <cell.config.json> [--interval <s>] (operator key alone)
203
- foga node mcp --config <cell.config.json> MCP over stdio (= foga mcp; the operator with FOGA_OPERATOR_KEY[_FILE], reads and builds without)
204
- foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch] the HTTP API, /mcp over Streamable HTTP, /healthz, the watchers
224
+ foga node mcp --config <cell.config.json> MCP over stdio (= foga mcp; the operator with FOGA_OPERATOR_KEY[_FILE] or --wallet, reads and builds without)
225
+ foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch] host mode: the HTTP API, /mcp, /a2a, the documents, /healthz, the watchers; no model
205
226
  foga node key new --config <cell.config.json> --id <id> [--level read|build|act] [--visibility public|caller|operator] [--note <text>]
206
227
  foga node run --config <cell.config.json> [--model stub|anthropic] [--listen <host:port> | --no-http] [--interval <s>] [--once | --ticks <n>] [--transcripts <dir>]
207
228
  the embedded node: server, watchers and the runner (operator key alone)
208
229
  foga demo --loop-d [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery] anvil, the world, then Loop D judged by the doctor
209
230
  foga loop-d run --world <world.json> --rpc <url> [--dir <dir>] [--out <evidence.json>] [--model] [--model-name] [--seed] [--no-recovery] [--listen] [--wait <s>] [--idle <s>]
210
231
  foga doctor [--world <world.json>] --loop-d <evidence.json> [--compare <evidence2.json>] [--out <report.json>]
211
- foga quickstart [--dir <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit] one command to a live cell and its client configuration
232
+ foga quickstart [--dir <dir>] [--wallet <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit] one command to a live cell (the wallet first) and its client configuration
212
233
  ```
213
234
 
214
235
  During development the CLI runs from source with `npm run foga -- <args>`; `npm run build` compiles it to `dist/` for the `foga` binary.
@@ -0,0 +1,54 @@
1
+ import { SIGNED_SCHEME } from "../http/signed.js";
2
+ import { A2A_PROTOCOL_VERSION } from "./types.js";
3
+ /** The public endpoints of a cell: the configuration's, or derived from runtime.http.publicUrl. */
4
+ export function endpointsOf(node) {
5
+ const cfg = node.config;
6
+ const base = cfg.runtime.http.publicUrl?.replace(/\/+$/, "") ?? null;
7
+ return {
8
+ a2a: cfg.presence.endpoints.a2a ?? (base ? `${base}/a2a` : null),
9
+ mcp: cfg.presence.endpoints.mcp ?? (base ? `${base}/mcp` : null),
10
+ web: cfg.presence.endpoints.web ?? cfg.presence.homepage ?? base,
11
+ };
12
+ }
13
+ export async function buildAgentCard(node, o) {
14
+ const cfg = node.config;
15
+ const endpoints = endpointsOf(node);
16
+ const held = node.cell ? await node.heldSkills() : [];
17
+ const skills = held
18
+ .filter((h) => cfg.skills.offered.includes(h.name))
19
+ .map((h) => {
20
+ const m = h.pkg.manifest;
21
+ const version = m.skillVersion ?? String(h.pkg.version);
22
+ return {
23
+ id: `${h.name}@${version}`,
24
+ name: h.name,
25
+ description: m.summary ?? `The ERC-8338 skill ${h.name} (package ${h.packageHash})`,
26
+ tags: ["erc8338", "foga", h.name, ...(m.skillType ? [m.skillType] : [])],
27
+ examples: [
28
+ `{"foga":{"mode":"quote","skill":"${h.name}","input":{}}}`,
29
+ `{"foga":{"mode":"tender","tokenId":"<an ERC-8414 tender for ${h.name}, funded, with the hashlock committed>"}}`,
30
+ ],
31
+ inputModes: ["application/json", "text/plain"],
32
+ outputModes: ["application/json"],
33
+ };
34
+ });
35
+ const card = {
36
+ name: cfg.presence.name,
37
+ description: cfg.presence.description || `FOGA cell ${cfg.name}`,
38
+ supportedInterfaces: endpoints.a2a ? [{ url: endpoints.a2a, protocolBinding: "JSONRPC", protocolVersion: A2A_PROTOCOL_VERSION }] : [],
39
+ ...(cfg.presence.provider ? { provider: { organization: cfg.presence.provider.organization, url: cfg.presence.provider.url } } : {}),
40
+ version: o.version,
41
+ documentationUrl: "https://github.com/garyyang-finchip/foga#readme",
42
+ capabilities: { streaming: false, pushNotifications: false, extendedAgentCard: false },
43
+ securitySchemes: {
44
+ "api-key": { httpAuthSecurityScheme: { scheme: "Bearer", description: "An API key the cell's operator issued (foga node key new)." } },
45
+ "wallet-signature": { httpAuthSecurityScheme: { scheme: SIGNED_SCHEME, description: "An EIP-712 envelope signed by the caller's own wallet for this cell (docs/mcp.md, signed calls)." } },
46
+ },
47
+ securityRequirements: [],
48
+ defaultInputModes: ["text/plain", "application/json"],
49
+ defaultOutputModes: ["application/json", "text/plain"],
50
+ skills,
51
+ ...(cfg.presence.image ? { iconUrl: cfg.presence.image } : {}),
52
+ };
53
+ return card;
54
+ }
@@ -0,0 +1,276 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { ACTION_RANK } from "../tools/index.js";
3
+ import { A2A_ERRORS, A2A_PROTOCOL_VERSION, TERMINAL_STATES } from "./types.js";
4
+ const TASK_PREFIX = "a2a.task.";
5
+ const MSG_PREFIX = "a2a.msg.";
6
+ const QUOTE_SKILL_LIMIT = 50;
7
+ const now = () => new Date().toISOString();
8
+ function errorInfo(reason, metadata = {}) {
9
+ return { "@type": "type.googleapis.com/google.rpc.ErrorInfo", reason, domain: "a2a-protocol.org", metadata: { ...metadata, timestamp: now() } };
10
+ }
11
+ export class A2AError extends Error {
12
+ code;
13
+ data;
14
+ constructor(code, message, data) {
15
+ super(message);
16
+ this.code = code;
17
+ this.data = data;
18
+ }
19
+ }
20
+ function agentMessage(text, data, taskId, contextId) {
21
+ const parts = [{ text }];
22
+ if (data !== undefined)
23
+ parts.push({ data, mediaType: "application/json" });
24
+ return { messageId: randomUUID(), role: "ROLE_AGENT", parts, ...(taskId ? { taskId } : {}), ...(contextId ? { contextId } : {}) };
25
+ }
26
+ /** The `foga` request in a message: from a data part with a `foga` object, or a text part (a question in words → quote). */
27
+ export function fogaRequestOf(message) {
28
+ for (const p of message.parts) {
29
+ const d = p.data;
30
+ if (d && typeof d === "object" && d.foga && typeof d.foga === "object") {
31
+ const f = d.foga;
32
+ const mode = f.mode === "tender" ? "tender" : "quote";
33
+ return { mode, skill: typeof f.skill === "string" ? f.skill : undefined, input: f.input, tokenId: f.tokenId !== undefined ? String(f.tokenId) : undefined };
34
+ }
35
+ }
36
+ const text = message.parts.map((p) => p.text).filter((t) => typeof t === "string").join("\n").trim();
37
+ return { mode: "quote", text: text || undefined };
38
+ }
39
+ export class A2AServer {
40
+ node;
41
+ catalog;
42
+ log;
43
+ constructor(node, catalog, log = () => { }) {
44
+ this.node = node;
45
+ this.catalog = catalog;
46
+ this.log = log;
47
+ }
48
+ get store() {
49
+ return this.node.store;
50
+ }
51
+ load(id) {
52
+ return this.store.getFact(TASK_PREFIX + id);
53
+ }
54
+ save(t) {
55
+ this.store.setFact(TASK_PREFIX + t.task.id, t);
56
+ }
57
+ visible(t, caller) {
58
+ return caller.visibility === "operator" || t.caller === caller.id;
59
+ }
60
+ setState(t, state, message) {
61
+ t.task.status = { state, timestamp: now(), ...(message ? { message } : {}) };
62
+ if (message)
63
+ t.task.history = [...(t.task.history ?? []), message];
64
+ this.save(t);
65
+ }
66
+ /** The JSON-RPC dispatcher: one request in, one response out; the HTTP layer checks the version header and the media type. */
67
+ async handle(request, caller, version) {
68
+ const id = request?.id ?? null;
69
+ try {
70
+ if (!request || typeof request !== "object" || request.jsonrpc !== "2.0" || typeof request.method !== "string")
71
+ throw new A2AError(A2A_ERRORS.InvalidRequestError, "Request payload validation error");
72
+ const v = (version ?? "").trim() || "0.3";
73
+ if (v !== A2A_PROTOCOL_VERSION)
74
+ throw new A2AError(A2A_ERRORS.VersionNotSupportedError, `A2A version ${v} is not supported; this agent speaks ${A2A_PROTOCOL_VERSION} (send A2A-Version: ${A2A_PROTOCOL_VERSION})`, [errorInfo("VERSION_NOT_SUPPORTED", { requested: v, supported: [A2A_PROTOCOL_VERSION] })]);
75
+ const { method, params } = request;
76
+ let result;
77
+ switch (method) {
78
+ case "SendMessage":
79
+ result = await this.sendMessage(params, caller);
80
+ break;
81
+ case "GetTask":
82
+ result = this.getTask(params, caller);
83
+ break;
84
+ case "ListTasks":
85
+ result = this.listTasks((params ?? {}), caller);
86
+ break;
87
+ case "CancelTask":
88
+ result = await this.cancelTask(params, caller);
89
+ break;
90
+ case "SendStreamingMessage":
91
+ case "SubscribeToTask":
92
+ throw new A2AError(A2A_ERRORS.UnsupportedOperationError, "streaming is not supported by this agent (capabilities.streaming is false)");
93
+ case "GetExtendedAgentCard":
94
+ throw new A2AError(A2A_ERRORS.UnsupportedOperationError, "no extended agent card (capabilities.extendedAgentCard is false)");
95
+ case "CreateTaskPushNotificationConfig":
96
+ case "GetTaskPushNotificationConfig":
97
+ case "ListTaskPushNotificationConfigs":
98
+ case "DeleteTaskPushNotificationConfig":
99
+ throw new A2AError(A2A_ERRORS.PushNotificationNotSupportedError, "push notifications are not supported by this agent (capabilities.pushNotifications is false)");
100
+ default:
101
+ throw new A2AError(A2A_ERRORS.MethodNotFoundError, "Method not found");
102
+ }
103
+ return { jsonrpc: "2.0", id, result };
104
+ }
105
+ catch (e) {
106
+ if (e instanceof A2AError)
107
+ return { jsonrpc: "2.0", id, error: { code: e.code, message: e.message, ...(e.data ? { data: e.data } : {}) } };
108
+ this.log(`a2a: ${e.message}`);
109
+ return { jsonrpc: "2.0", id, error: { code: A2A_ERRORS.InternalError, message: "Internal error" } };
110
+ }
111
+ }
112
+ // ---------------------------------------------------------------------------- SendMessage
113
+ async sendMessage(params, caller) {
114
+ const m = params?.message;
115
+ if (!m || typeof m !== "object" || typeof m.messageId !== "string" || !Array.isArray(m.parts) || m.parts.length === 0)
116
+ throw new A2AError(A2A_ERRORS.InvalidParamsError, "Invalid parameters", [{ "@type": "type.googleapis.com/google.rpc.BadRequest", fieldViolations: [{ field: "message", description: "messageId and at least one part are required" }] }]);
117
+ for (const p of m.parts) {
118
+ if (p.raw !== undefined || p.url !== undefined)
119
+ throw new A2AError(A2A_ERRORS.ContentTypeNotSupportedError, "file parts are not accepted; send text/plain or an application/json data part");
120
+ if (p.mediaType && !/^(text\/plain|application\/json)\b/.test(p.mediaType))
121
+ throw new A2AError(A2A_ERRORS.ContentTypeNotSupportedError, `media type ${p.mediaType} is not accepted; text/plain or application/json`);
122
+ }
123
+ // the same message from the same caller is the same task (3.3.1: messageId detects duplicates)
124
+ const dedupeKey = `${MSG_PREFIX}${caller.id}:${m.messageId}`;
125
+ const known = this.store.getFact(dedupeKey);
126
+ if (known) {
127
+ const t = this.load(known);
128
+ if (t)
129
+ return { task: this.publicTask(t, params.configuration?.historyLength) };
130
+ }
131
+ // a message to an existing task: input for one that asked, nothing for a terminal one
132
+ if (m.taskId) {
133
+ const t = this.load(m.taskId);
134
+ if (!t || !this.visible(t, caller))
135
+ throw new A2AError(A2A_ERRORS.TaskNotFoundError, "Task not found", [errorInfo("TASK_NOT_FOUND", { taskId: m.taskId })]);
136
+ if (TERMINAL_STATES.includes(t.task.status.state))
137
+ throw new A2AError(A2A_ERRORS.UnsupportedOperationError, `task ${m.taskId} is ${t.task.status.state} and accepts no further message`);
138
+ t.messageIds.push(m.messageId);
139
+ t.task.history = [...(t.task.history ?? []), m];
140
+ this.store.setFact(dedupeKey, t.task.id);
141
+ await this.work(t, m, caller);
142
+ return { task: this.publicTask(t, params.configuration?.historyLength) };
143
+ }
144
+ const task = { task: { id: randomUUID(), contextId: m.contextId ?? randomUUID(), status: { state: "TASK_STATE_SUBMITTED", timestamp: now() }, history: [m], artifacts: [], metadata: { caller: caller.id, cell: this.node.config.name } }, caller: caller.id, messageIds: [m.messageId], createdAt: now(), jobs: {} };
145
+ this.save(task);
146
+ this.store.setFact(dedupeKey, task.task.id);
147
+ await this.work(task, m, caller);
148
+ return { task: this.publicTask(task, params.configuration?.historyLength) };
149
+ }
150
+ /** Decide what a message asks for and do it: a quote at once, a tender's work started. */
151
+ async work(t, m, caller) {
152
+ const req = fogaRequestOf(m);
153
+ if (req.mode === "quote") {
154
+ await this.quote(t, req);
155
+ return;
156
+ }
157
+ const tokenId = req.tokenId ?? t.tokenId;
158
+ if (!tokenId) {
159
+ this.setState(t, "TASK_STATE_INPUT_REQUIRED", agentMessage("Which tender? Send a data part {\"foga\":{\"mode\":\"tender\",\"tokenId\":\"<id>\"}} naming an ERC-8414 tender that exists, is funded, and has its answer committed to the hashlock verifier.", { foga: { need: ["tokenId"] } }, t.task.id, t.task.contextId));
160
+ return;
161
+ }
162
+ if (ACTION_RANK[caller.level] < ACTION_RANK.act) {
163
+ this.setState(t, "TASK_STATE_REJECTED", agentMessage("This caller may not ask the cell to act: the tender mode needs an act-level credential (an API key the operator issued, or a wallet signature the cell's policy grants). A quote is open to everyone.", { foga: { gate: "permission", callerLevel: caller.level } }, t.task.id, t.task.contextId));
164
+ return;
165
+ }
166
+ t.tokenId = tokenId;
167
+ this.setState(t, "TASK_STATE_WORKING", agentMessage(`Working tender ${tokenId}: pre-check, run, submit. Settlement is the node's own duty afterwards.`, undefined, t.task.id, t.task.contextId));
168
+ // the work continues after the response (3.3.3); GetTask reads the result
169
+ void this.takeTender(t, tokenId, caller).catch((e) => {
170
+ this.log(`a2a: task ${t.task.id}: ${e.message}`);
171
+ const fresh = this.load(t.task.id) ?? t;
172
+ this.setState(fresh, "TASK_STATE_FAILED", agentMessage(`The work failed: ${e.message}`, undefined, t.task.id, t.task.contextId));
173
+ });
174
+ }
175
+ async quote(t, req) {
176
+ const cfg = this.node.config;
177
+ const held = this.node.cell ? await this.node.heldSkills() : [];
178
+ const offered = held.filter((h) => cfg.skills.offered.includes(h.name)).slice(0, QUOTE_SKILL_LIMIT);
179
+ const terms = { acceptance: "hashlock", verifier: this.node.acceptance.hashlock, asset: "native", minRewardWei: cfg.policy.tenders.minRewardWei, minSubmitWindowSeconds: cfg.policy.tenders.minSubmitWindowSeconds, minSettleWindowSeconds: cfg.policy.tenders.minSettleWindowSeconds, counterpartyMinLevel: cfg.policy.tenders.counterpartyMinLevel };
180
+ const how = "Mint an ERC-8414 tender for the skill with these terms (tender_mint_build gives the calls for your own account), fund it, commit the answer hash to the hashlock verifier, then send {\"foga\":{\"mode\":\"tender\",\"tokenId\":\"<id>\"}} with an act-level credential.";
181
+ if (req.skill && !offered.some((h) => h.name === req.skill)) {
182
+ this.setState(t, "TASK_STATE_REJECTED", agentMessage(`The cell does not offer a skill named ${req.skill}. Offered: ${offered.map((h) => h.name).join(", ") || "none"}.`, { foga: { quote: { skill: req.skill, offered: false, available: offered.map((h) => h.name) } } }, t.task.id, t.task.contextId));
183
+ return;
184
+ }
185
+ const chosen = req.skill ? offered.filter((h) => h.name === req.skill) : offered;
186
+ const quote = { cell: cfg.name, account: this.node.cell?.address ?? null, skills: chosen.map((h) => ({ name: h.name, version: String(h.pkg.manifest.skillVersion ?? h.pkg.version), packageHash: h.packageHash, tokenId: h.tokenId.toString(), summary: h.pkg.manifest.summary ?? null })), terms, ...(req.input !== undefined ? { inputEchoed: req.input } : {}), how };
187
+ const artifact = { artifactId: randomUUID(), name: "quote", description: "What the cell can do and on what terms", parts: [{ data: { foga: { quote } }, mediaType: "application/json" }] };
188
+ t.task.artifacts = [artifact];
189
+ const text = req.text ? `You asked: ${req.text.slice(0, 200)}. ` : "";
190
+ this.setState(t, "TASK_STATE_COMPLETED", agentMessage(`${text}The cell offers ${chosen.length} skill(s)${chosen.length ? `: ${chosen.map((h) => h.name).join(", ")}` : ""}; the terms are in the quote artifact. Nothing ran and nothing touched the chain.`, undefined, t.task.id, t.task.contextId));
191
+ }
192
+ /** The tender road: the catalog decides every step as the caller; the task records the decisions. */
193
+ async takeTender(t, tokenId, caller) {
194
+ const reject = (text, data) => this.setState(this.load(t.task.id) ?? t, "TASK_STATE_REJECTED", agentMessage(text, data, t.task.id, t.task.contextId));
195
+ const cancelled = () => Boolean(this.load(t.task.id)?.cancelRequested);
196
+ const pre = await this.catalog.invoke(this.node, "tender_precheck", { tokenId }, caller, { mode: "act" });
197
+ if (!pre.ok)
198
+ return reject(`Tender ${tokenId} refused at ${pre.gate}: ${pre.reason}`, { foga: { gate: pre.gate, reason: pre.reason, decisionId: pre.decisionId ?? null } });
199
+ const view = pre.output.tender;
200
+ if (!view.task)
201
+ return reject(`Tender ${tokenId} has no readable task document`, { foga: { gate: "input" } });
202
+ if (cancelled())
203
+ return this.setState(this.load(t.task.id), "TASK_STATE_CANCELED", agentMessage("Cancelled before the run.", undefined, t.task.id, t.task.contextId));
204
+ const run = await this.catalog.invoke(this.node, "skill_run", { skill: view.task.skill, input: view.task.input, tokenId }, caller, { mode: "act" });
205
+ if (!run.ok)
206
+ return reject(`The run for tender ${tokenId} was refused at ${run.gate}: ${run.reason}`, { foga: { gate: run.gate, reason: run.reason, decisionId: run.decisionId ?? null } });
207
+ const stored = this.load(t.task.id) ?? t;
208
+ stored.jobs.run = run.jobId;
209
+ this.save(stored);
210
+ if (cancelled())
211
+ return this.setState(this.load(t.task.id), "TASK_STATE_CANCELED", agentMessage("Cancelled before the submission.", undefined, t.task.id, t.task.contextId));
212
+ const out = run.output;
213
+ const sub = await this.catalog.invoke(this.node, "tender_submit", { tokenId, resultHash: out.resultHash, runReceipt: out.runReceipt }, caller, { mode: "act" });
214
+ const latest = this.load(t.task.id) ?? t;
215
+ if (!sub.ok) {
216
+ latest.jobs.submit = sub.jobId;
217
+ this.save(latest);
218
+ return reject(`The submission for tender ${tokenId} was refused at ${sub.gate}: ${sub.reason}`, { foga: { gate: sub.gate, reason: sub.reason, decisionId: sub.decisionId ?? null, jobId: sub.jobId ?? null } });
219
+ }
220
+ latest.jobs.submit = sub.jobId;
221
+ const result = sub.output;
222
+ latest.task.artifacts = [{ artifactId: randomUUID(), name: "submission", description: "The cell's submission on the tender", parts: [{ data: { foga: { tokenId, resultHash: out.resultHash, answerHash: out.answerHash, submissionId: result.submissionId ?? null, txHash: result.txHash ?? null, jobId: sub.jobId, decisionId: sub.decisionId ?? null, operations: sub.operations ?? [] } }, mediaType: "application/json" }] }];
223
+ this.setState(latest, "TASK_STATE_COMPLETED", agentMessage(`Tender ${tokenId} submitted (job ${sub.jobId}); the node settles it once the chain allows.`, undefined, t.task.id, t.task.contextId));
224
+ }
225
+ // ---------------------------------------------------------------------------- GetTask, ListTasks, CancelTask
226
+ getTask(params, caller) {
227
+ if (!params || typeof params.id !== "string")
228
+ throw new A2AError(A2A_ERRORS.InvalidParamsError, "Invalid parameters", [{ "@type": "type.googleapis.com/google.rpc.BadRequest", fieldViolations: [{ field: "id", description: "the task id is required" }] }]);
229
+ const t = this.load(params.id);
230
+ if (!t || !this.visible(t, caller))
231
+ throw new A2AError(A2A_ERRORS.TaskNotFoundError, "Task not found", [errorInfo("TASK_NOT_FOUND", { taskId: params.id })]);
232
+ return this.publicTask(t, params.historyLength);
233
+ }
234
+ listTasks(params, caller) {
235
+ const all = Object.values(this.store.facts(TASK_PREFIX));
236
+ let tasks = all.filter((t) => this.visible(t, caller));
237
+ if (params.contextId)
238
+ tasks = tasks.filter((t) => t.task.contextId === params.contextId);
239
+ if (params.status)
240
+ tasks = tasks.filter((t) => t.task.status.state === params.status);
241
+ if (params.statusTimestampAfter)
242
+ tasks = tasks.filter((t) => (t.task.status.timestamp ?? "") > params.statusTimestampAfter);
243
+ tasks.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1));
244
+ const pageSize = Math.min(100, Math.max(1, params.pageSize ?? 50));
245
+ const offset = params.pageToken ? Number(params.pageToken) || 0 : 0;
246
+ const page = tasks.slice(offset, offset + pageSize);
247
+ return { tasks: page.map((t) => this.publicTask(t, params.historyLength, params.includeArtifacts ?? false)), nextPageToken: offset + pageSize < tasks.length ? String(offset + pageSize) : "", pageSize, totalSize: tasks.length };
248
+ }
249
+ async cancelTask(params, caller) {
250
+ if (!params || typeof params.id !== "string")
251
+ throw new A2AError(A2A_ERRORS.InvalidParamsError, "Invalid parameters");
252
+ const t = this.load(params.id);
253
+ if (!t || !this.visible(t, caller))
254
+ throw new A2AError(A2A_ERRORS.TaskNotFoundError, "Task not found", [errorInfo("TASK_NOT_FOUND", { taskId: params.id })]);
255
+ if (TERMINAL_STATES.includes(t.task.status.state))
256
+ throw new A2AError(A2A_ERRORS.TaskNotCancelableError, `task ${params.id} is ${t.task.status.state}`, [errorInfo("TASK_NOT_CANCELABLE", { taskId: params.id, state: t.task.status.state })]);
257
+ // a submission on its way to the chain is not undone by a cancellation: what was broadcast stays
258
+ if (t.jobs.submit)
259
+ throw new A2AError(A2A_ERRORS.TaskNotCancelableError, `task ${params.id} has submitted on chain (job ${t.jobs.submit}); a broadcast is not undone`, [errorInfo("TASK_NOT_CANCELABLE", { taskId: params.id, jobId: t.jobs.submit })]);
260
+ if (t.task.status.state === "TASK_STATE_WORKING") {
261
+ t.cancelRequested = true;
262
+ this.save(t);
263
+ return this.publicTask(t);
264
+ }
265
+ this.setState(t, "TASK_STATE_CANCELED", agentMessage("Cancelled.", undefined, t.task.id, t.task.contextId));
266
+ return this.publicTask(t);
267
+ }
268
+ publicTask(t, historyLength, artifacts = true) {
269
+ const task = { ...t.task };
270
+ if (historyLength !== undefined && task.history)
271
+ task.history = historyLength <= 0 ? [] : task.history.slice(-historyLength);
272
+ if (!artifacts)
273
+ delete task.artifacts;
274
+ return task;
275
+ }
276
+ }
@@ -0,0 +1,20 @@
1
+ export const TERMINAL_STATES = ["TASK_STATE_COMPLETED", "TASK_STATE_FAILED", "TASK_STATE_CANCELED", "TASK_STATE_REJECTED"];
2
+ /** JSON-RPC error codes of the binding (specification section 5.4 and 9.5). */
3
+ export const A2A_ERRORS = {
4
+ JSONParseError: -32700,
5
+ InvalidRequestError: -32600,
6
+ MethodNotFoundError: -32601,
7
+ InvalidParamsError: -32602,
8
+ InternalError: -32603,
9
+ TaskNotFoundError: -32001,
10
+ TaskNotCancelableError: -32002,
11
+ PushNotificationNotSupportedError: -32003,
12
+ UnsupportedOperationError: -32004,
13
+ ContentTypeNotSupportedError: -32005,
14
+ InvalidAgentResponseError: -32006,
15
+ ExtendedAgentCardNotConfiguredError: -32007,
16
+ ExtensionSupportRequiredError: -32008,
17
+ VersionNotSupportedError: -32009,
18
+ };
19
+ export const A2A_PROTOCOL_VERSION = "1.0";
20
+ export const A2A_WELL_KNOWN = "/.well-known/agent-card.json";
@@ -35,7 +35,8 @@ function solcBinary(profiles, version, solcDir) {
35
35
  function forgeBuild(projectDir, outDir, skip, solcPath, extraArgs = []) {
36
36
  rmrf(outDir);
37
37
  ensureDir(outDir);
38
- const args = ["build", "--ast", "--no-cache", "--out", outDir, ...extraArgs];
38
+ // the build-info files (the standard JSON input and output of each compiler run) stay beside the artifacts: `foga world verify --sourcify` submits them
39
+ const args = ["build", "--ast", "--no-cache", "--out", outDir, "--build-info", "--build-info-path", join(outDir, "build-info"), ...extraArgs];
39
40
  for (const s of skip)
40
41
  args.push("--skip", s);
41
42
  if (solcPath)