@finchip.ai/foga 0.6.0-alpha.7 → 0.6.1
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.
- package/README.md +16 -6
- package/dist/cli-cell.js +29 -0
- package/dist/cli.js +18 -6
- package/dist/core/world.js +17 -0
- package/dist/demo/birth.js +7 -1
- package/dist/demo/loop-d.js +39 -1
- package/dist/demo/quickstart.js +39 -6
- package/dist/genesis/probe.js +23 -7
- package/dist/genesis/skill-runner.js +31 -2
- package/dist/http/server.js +20 -1
- package/dist/mcp/server.js +23 -2
- package/dist/node/advise.js +134 -0
- package/dist/node/config.js +21 -0
- package/dist/node/propagation.js +95 -0
- package/dist/node/wallet.js +24 -10
- package/dist/tools/index.js +2 -1
- package/dist/tools/propagation.js +35 -0
- package/dist/tools/tenders.js +22 -6
- package/dist/util/version.js +2 -0
- package/docs/design-memo.md +12 -6
- package/docs/examples/README.md +1 -1
- package/docs/extending.md +49 -0
- package/docs/loop-d.md +11 -0
- package/docs/mcp.md +7 -5
- package/docs/node.md +8 -2
- package/docs/og-deployment.md +131 -0
- package/docs/propagation.md +41 -0
- package/docs/quickstart.md +15 -7
- package/docs/wallet.md +8 -6
- package/package.json +3 -2
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -19,9 +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
|
|
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
23
|
|
|
24
|
-
**3d-
|
|
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.
|
|
25
27
|
|
|
26
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.
|
|
27
29
|
|
|
@@ -84,10 +86,12 @@ With Node 22.13+ and Foundry's `anvil` installed, one command makes your wallet
|
|
|
84
86
|
npx -y @finchip.ai/foga quickstart
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
`@finchtech.ai/foga` is the same package under a second scope (`alias/finchtech.ai`: a launcher that runs `@finchip.ai/foga` at the same version), so `npx -y @finchtech.ai/foga quickstart` is the same command.
|
|
90
|
+
|
|
87
91
|
The wallet alone, for an agent that will only call cells (docs/wallet.md), and a cell on a public chain born with it:
|
|
88
92
|
|
|
89
93
|
```bash
|
|
90
|
-
npx -y @finchip.ai/foga wallet new # ~/.foga/wallet:
|
|
94
|
+
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
|
|
91
95
|
foga cell new --wallet ~/.foga/wallet --name mine --world deployments/sepolia.json --store mine.sqlite --out mine.json
|
|
92
96
|
foga cell init --config mine.json --wallet ~/.foga/wallet
|
|
93
97
|
foga node preflight --config mine.json --wallet ~/.foga/wallet
|
|
@@ -109,6 +113,10 @@ npm run demo:genesis # anvil + world deploy + doctor + genesis T0-T9 + d
|
|
|
109
113
|
# 2. World and genesis only (Node >= 22 and Foundry's anvil, no Solidity build; the ZK paths are left out without their material):
|
|
110
114
|
npm ci && npm run demo:genesis
|
|
111
115
|
|
|
116
|
+
# 2b. The same show without a checkout, from the published package (Node >= 22.13 and anvil): a fresh chain, the 47 contracts,
|
|
117
|
+
# the genesis T0-T9 operation by operation, then the doctor's per-standard verdicts — about half a minute
|
|
118
|
+
npx -y @finchip.ai/foga demo --genesis T9 # add --keep to leave the chain running afterwards
|
|
119
|
+
|
|
112
120
|
# 3. Containers (Docker with Compose):
|
|
113
121
|
docker compose up --build # anvil + world + doctor + genesis; outputs land in ./deployments
|
|
114
122
|
```
|
|
@@ -176,7 +184,9 @@ deployments/ world files, doctor reports, genesis evidence (local output
|
|
|
176
184
|
prover/ the ZK proving sub-package: run.cjs bridge (credential and predicate proofs), build-predicate.mjs (the development ceremony), its own dependencies
|
|
177
185
|
zk/ circuits/ (kya_predicate_count_v1.circom) and build/ (verification_key.json committed; the proving key is rebuilt locally)
|
|
178
186
|
docker/, docker-compose.yml
|
|
179
|
-
|
|
187
|
+
deploy/ the OG host assets: foga-og.service (systemd, host mode), Caddyfile, og.env.example (docs/og-deployment.md)
|
|
188
|
+
site/ the documentation site for GitHub Pages (docs.foga.finchip.ai): index.html, llms.txt, CNAME
|
|
189
|
+
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)
|
|
180
190
|
scenarios/skills/ the two ERC-8338 skill packages of the genesis: foga-probe, finch-extract-verify
|
|
181
191
|
scenarios/fixtures/ the pinned document finch-extract-verify works on in T5 and T7
|
|
182
192
|
scripts/ install-solc.sh, install-foundry.sh, relay.mjs, pack-smoke.mjs (the npm tarball run from a clean install)
|
|
@@ -206,8 +216,8 @@ foga probe relay-script
|
|
|
206
216
|
foga genesis run --world <world> [--rpc <url>] [--until T0..T9] [--store <file>] [--out <evidence>] [--zk auto|require|skip] [--crash-after <key>] [--crash-before <key>]
|
|
207
217
|
foga genesis status --world <world> [--store <file>]
|
|
208
218
|
foga keys new [--out <file>] | show | export --role <role> [--out <file>] | check
|
|
209
|
-
foga wallet new [--name <n> | --dir <dir>] [--
|
|
210
|
-
an agent's own
|
|
219
|
+
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]]
|
|
220
|
+
an agent's own key (the first step; --cell for the separated owner/operator/guardian set), and a signed Authorization header for a cell
|
|
211
221
|
foga demo [--port <n>] [--keep] [--genesis <T0..T9>]
|
|
212
222
|
foga cell list [--public] | schema <capability>
|
|
213
223
|
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --out <cell.config.json> (--wallet <dir> | --owner <addr> --operator <addr> --guardian <addr>)
|
package/dist/cli-cell.js
CHANGED
|
@@ -27,6 +27,9 @@ export const CELL_USAGE = `
|
|
|
27
27
|
foga cell migrate --config <cell.config.json> --to <new.config.json> [--store <new ops.sqlite>] [--operator <addr>] [--capital-wei <n>] [--no-sweep]
|
|
28
28
|
move the cell to a new revision-2 account (the owner's key); the new config and store are written beside
|
|
29
29
|
foga cell status --config <cell.config.json> cell_status as the operator
|
|
30
|
+
foga cell advise --config <cell.config.json> [--brief] [--as-json]
|
|
31
|
+
what a running cell should do next — separate the roles, tune the policy, standing, a public face, a supervisor,
|
|
32
|
+
backups, a dedicated RPC — only what applies to this cell; advice, never required; no chain read, no key
|
|
30
33
|
foga cell docs build --config <cell.config.json> [--rpc <url>] [--out <dir>]
|
|
31
34
|
the cell's documents from its configuration and the chain (registration, AID document, ARD descriptor, AUF unit,
|
|
32
35
|
agent card, server.json, manifest) under runtime.http.documentsDir — what the node serves at /documents and /.well-known
|
|
@@ -173,6 +176,20 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
173
176
|
const rpc = str(flags, "rpc");
|
|
174
177
|
const log = (line) => console.error(line);
|
|
175
178
|
const wallet = walletFromFlags(flags);
|
|
179
|
+
if (cmd === "advise") {
|
|
180
|
+
const { adviseCell, formatAdvice } = await import("./node/advise.js");
|
|
181
|
+
const node = await Node.open({ configPath, rpc, keys: {}, allowUnborn: true, log: () => { } });
|
|
182
|
+
try {
|
|
183
|
+
const advice = adviseCell(node);
|
|
184
|
+
if (flags["as-json"])
|
|
185
|
+
return print(advice);
|
|
186
|
+
console.log(formatAdvice(advice, { brief: Boolean(flags.brief), config: configPath }));
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
node.close();
|
|
190
|
+
}
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
176
193
|
if (cmd === "init") {
|
|
177
194
|
const owner = keysFor("owner", false, wallet);
|
|
178
195
|
const node = await Node.open({ configPath, rpc, keys: { owner }, allowUnborn: true, log });
|
|
@@ -180,6 +197,11 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
180
197
|
const capital = str(flags, "capital-wei");
|
|
181
198
|
const result = await cellInit(node, { capitalWei: capital ? BigInt(capital) : undefined, log });
|
|
182
199
|
print(result);
|
|
200
|
+
// Born: say what a cell should do next, once, briefly — the first run stays minimal, the hardening is advice.
|
|
201
|
+
const { adviseCell, formatAdvice } = await import("./node/advise.js");
|
|
202
|
+
const next = formatAdvice(adviseCell(node), { brief: true, config: configPath });
|
|
203
|
+
if (next)
|
|
204
|
+
console.error(`\n${next}`);
|
|
183
205
|
}
|
|
184
206
|
finally {
|
|
185
207
|
node.close();
|
|
@@ -413,6 +435,13 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
413
435
|
const built = readBuilt(documentsDir(node));
|
|
414
436
|
const signers = node.config.access.signers;
|
|
415
437
|
log(`foga ${version}: serving cell ${node.config.name}${node.cell ? ` (${node.cell.address})` : ""} at ${running.url} — /mcp, /api/<capability>, /a2a, /.well-known/agent-card.json, /documents/*, /healthz; ${keys.operator ? "acting with the operator key" : "no operator key: reads and builds only"}; ${node.config.access.keys.length} API key(s) configured; signatures: ${signers.default} by default, ${signers.allow.length} allowed${signers.standing ? `, standing level ${signers.standing.minLevel}+ granted ${signers.standing.level}` : ""}; documents ${built ? `v${built.manifest.version}` : "not built (foga cell docs build)"}`);
|
|
438
|
+
{
|
|
439
|
+
// One line of advice at start-up (the full list is `foga cell advise`); nothing here is a condition of serving.
|
|
440
|
+
const { adviseCell } = await import("./node/advise.js");
|
|
441
|
+
const recommended = adviseCell(node).filter((a) => a.level === "recommended");
|
|
442
|
+
if (recommended.length)
|
|
443
|
+
log(`advice: ${recommended.map((a) => a.id).join(", ")} — foga cell advise --config ${configPath} explains; nothing required`);
|
|
444
|
+
}
|
|
416
445
|
const stopWatch = !flags["no-watch"] && keys.operator && node.cell ? startWatchLoop(new Watchers(node, catalog), node.config.runtime.pollSeconds * 1000, log) : () => { };
|
|
417
446
|
const stop = () => {
|
|
418
447
|
stopWatch();
|
package/dist/cli.js
CHANGED
|
@@ -24,8 +24,9 @@ usage:
|
|
|
24
24
|
foga world verify --world <world.json> --sourcify [--dry-run] [--only <id,id>] [--server <url>] [--out <dir>] [--build-info <dir>]
|
|
25
25
|
submit every contract the world deployed from the bundle to Sourcify, with the standard JSON input of its
|
|
26
26
|
compiler run (.foga-build/out/<repo>/build-info, written by npm run bundle:verify); a local chain is a dry run
|
|
27
|
-
foga wallet new [--name <n> | --dir <dir>] [--
|
|
28
|
-
(default ~/.foga/wallet; --name under ~/.foga/wallets/<n>; --
|
|
27
|
+
foga wallet new [--name <n> | --dir <dir>] [--cell] the first step: a key of your own — one signer in a 0600 file, your identity, which fills
|
|
28
|
+
every role a cell needs (default ~/.foga/wallet; --name under ~/.foga/wallets/<n>; --cell: the
|
|
29
|
+
separated owner/operator/guardian set for a guarded cell account). Never overwrites; prints the addresses
|
|
29
30
|
foga wallet show [--name <n> | --dir <dir>] [--rpc <url>] the wallet's addresses (and balances with an RPC)
|
|
30
31
|
foga wallet sign (--config <cell.config.json> | --url <node url> | --cell <account> --chain-id <n>) [--name <n> | --dir <dir>]
|
|
31
32
|
[--role signer|operator|owner|guardian] [--ttl <s>] [--agent-id <id>] [--method <METHOD> --path <path> [--body '<json>' | --body-file <f>]]
|
|
@@ -244,7 +245,7 @@ async function main() {
|
|
|
244
245
|
return;
|
|
245
246
|
}
|
|
246
247
|
if (group === "wallet") {
|
|
247
|
-
const { createWallet, loadWallet, walletDir, walletFileProblems } = await import("./node/wallet.js");
|
|
248
|
+
const { CELL_ROLES, createWallet, loadWallet, walletDir, walletFileProblems } = await import("./node/wallet.js");
|
|
248
249
|
const dir = resolve(str(flags, "dir") ?? walletDir(str(flags, "name")));
|
|
249
250
|
const printWallet = async (w, made) => {
|
|
250
251
|
console.log(`${made ? "wallet made at" : "wallet"} ${w.dir}${w.name !== "default" ? ` (${w.name})` : ""}, ${w.createdAt}`);
|
|
@@ -265,8 +266,11 @@ async function main() {
|
|
|
265
266
|
provider?.destroy();
|
|
266
267
|
for (const p of walletFileProblems(w))
|
|
267
268
|
log.warn(p);
|
|
268
|
-
|
|
269
|
-
|
|
269
|
+
const single = w.roles.length === 1 && w.roles[0] === "signer";
|
|
270
|
+
if (single)
|
|
271
|
+
console.log(`one key, your identity; it fills every role a cell needs. For a cell's birth, fund this address (capital and gas), then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
|
|
272
|
+
else if (w.roles.includes("owner"))
|
|
273
|
+
console.log(`a separated cell wallet; fund the owner for a cell's birth (capital and gas) and the operator for its gas, then: foga cell new --wallet ${dir} … and foga cell init --config <cfg> --wallet ${dir}`);
|
|
270
274
|
};
|
|
271
275
|
if (cmd === "new") {
|
|
272
276
|
const existing = loadWallet(dir);
|
|
@@ -275,8 +279,10 @@ async function main() {
|
|
|
275
279
|
console.log("(already there; a wallet is never overwritten — foga wallet new --name <n> makes another)");
|
|
276
280
|
return;
|
|
277
281
|
}
|
|
282
|
+
// the base is one key (--single, now the default); --cell makes the separated owner/operator/guardian set (the guarded FogaCellAccount profile)
|
|
283
|
+
const roles = flags.cell ? [...CELL_ROLES] : undefined;
|
|
278
284
|
try {
|
|
279
|
-
await printWallet(createWallet(dir, { name: str(flags, "name") ?? "default", roles
|
|
285
|
+
await printWallet(createWallet(dir, { name: str(flags, "name") ?? "default", roles }), true);
|
|
280
286
|
}
|
|
281
287
|
catch (e) {
|
|
282
288
|
fail(e.message);
|
|
@@ -504,13 +510,19 @@ async function main() {
|
|
|
504
510
|
await result.stop();
|
|
505
511
|
return;
|
|
506
512
|
}
|
|
513
|
+
let asked = false;
|
|
507
514
|
const stopping = () => {
|
|
515
|
+
if (asked)
|
|
516
|
+
return;
|
|
517
|
+
asked = true;
|
|
508
518
|
console.error("quickstart: stopping the node and the chain");
|
|
509
519
|
result.stop().finally(() => process.exit(0));
|
|
510
520
|
};
|
|
511
521
|
process.on("SIGINT", stopping);
|
|
512
522
|
process.on("SIGTERM", stopping);
|
|
513
523
|
const why = await result.ended;
|
|
524
|
+
if (why === "stopped")
|
|
525
|
+
return; // a stop that was asked for: both children are gone
|
|
514
526
|
fail(`${why}; the quickstart is over`);
|
|
515
527
|
}
|
|
516
528
|
if (group === "demo") {
|
package/dist/core/world.js
CHANGED
|
@@ -39,6 +39,23 @@ export class World {
|
|
|
39
39
|
get chainId() {
|
|
40
40
|
return this.file.chainId;
|
|
41
41
|
}
|
|
42
|
+
get network() {
|
|
43
|
+
return this.file.network;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The covenants this world has contracts for, as ERC ids ("ERC-8414"), read off the contract keys ("8414/TaskToken";
|
|
47
|
+
* "identity8004/…" is the ERC-8004 registry). Data, not a constant: a world that attaches an eleventh covenant under
|
|
48
|
+
* a new key advertises it here with no code change (docs/extending.md), and a world that lacks one does not claim it.
|
|
49
|
+
*/
|
|
50
|
+
covenants() {
|
|
51
|
+
const ids = new Set();
|
|
52
|
+
for (const key of [...Object.keys(this.file.contracts), ...this.dynamic.keys()]) {
|
|
53
|
+
const m = /^(?:identity)?(\d{4,5})$/.exec(key.split("/")[0]);
|
|
54
|
+
if (m)
|
|
55
|
+
ids.add(`ERC-${m[1]}`);
|
|
56
|
+
}
|
|
57
|
+
return [...ids].sort((a, b) => Number(a.slice(4)) - Number(b.slice(4)));
|
|
58
|
+
}
|
|
42
59
|
has(id) {
|
|
43
60
|
return Boolean(this.file.contracts[id] || this.dynamic.get(id));
|
|
44
61
|
}
|
package/dist/demo/birth.js
CHANGED
|
@@ -17,7 +17,13 @@ export async function birthCell(o) {
|
|
|
17
17
|
const guardian = o.keys ? new Wallet(o.keys.guardian) : Wallet.createRandom();
|
|
18
18
|
log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}${o.keys ? " (the wallet's keys)" : " (fresh keys)"}`);
|
|
19
19
|
// --- funding by W, the standing scheme registered by W
|
|
20
|
-
|
|
20
|
+
// The reserve is priced at the chain's gas price, floored at 1 gwei. A public testnet can quote a price near zero
|
|
21
|
+
// (Sepolia has sat at a few wei), which would fund the roles with the flat amounts alone: the operator below the
|
|
22
|
+
// cell policy's gas floor (a quarter of the daily gas budget — the budget gate then refuses every tender) and the
|
|
23
|
+
// guardian with next to nothing for its suspend and resume. The floor keeps the funding meaningful on any chain;
|
|
24
|
+
// on a local chain, whose price is above it, nothing changes.
|
|
25
|
+
const quoted = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
|
|
26
|
+
const price = quoted > 1000000000n ? quoted : 1000000000n;
|
|
21
27
|
const gas = (units) => units * price * 2n;
|
|
22
28
|
await k.transfer("fund.owner", "W", owner.address, parseEther("0.05") + gas(12000000n));
|
|
23
29
|
await k.transfer("fund.operator", "W", operator.address, gas(20000000n) + parseEther("0.003"));
|
package/dist/demo/loop-d.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { createServer } from "node:net";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -106,6 +106,14 @@ export async function runLoopD(opts) {
|
|
|
106
106
|
const root = repoRoot();
|
|
107
107
|
const dir = resolve(opts.dir);
|
|
108
108
|
mkdirSync(dir, { recursive: true });
|
|
109
|
+
// One working directory holds one run: the cell's configuration and store, the orchestrator's and the demander's
|
|
110
|
+
// stores, the cases, the brief, the transcripts. A second run in the same directory would find the first run's
|
|
111
|
+
// keyed operations and replay them (a resumable kernel resumes) against a cell with fresh keys — the birth then
|
|
112
|
+
// fails on the first replayed answer. So a previous run's files are cleared first; the evidence (`--out`) lives
|
|
113
|
+
// elsewhere and is kept.
|
|
114
|
+
const cleared = cleanLoopDDir(dir);
|
|
115
|
+
if (cleared.length)
|
|
116
|
+
log(`loop D: cleared the previous run's files in ${dir} (${cleared.join(", ")})`);
|
|
109
117
|
const worldPath = resolve(opts.worldPath);
|
|
110
118
|
const seed = opts.seed ?? 7;
|
|
111
119
|
const cases = opts.cases ?? ALL_CASES;
|
|
@@ -563,3 +571,33 @@ export function defaultLoopDDir() {
|
|
|
563
571
|
mkdirSync(dir, { recursive: true });
|
|
564
572
|
return dir;
|
|
565
573
|
}
|
|
574
|
+
/** The files a Loop D run leaves in its working directory — its own and nothing else. */
|
|
575
|
+
const LOOP_D_FILES = ["D.json", "D.ops.sqlite", "D.ops.sqlite-wal", "D.ops.sqlite-shm", "orchestrator.sqlite", "orchestrator.sqlite-wal", "orchestrator.sqlite-shm", "demander.sqlite", "demander.sqlite-wal", "demander.sqlite-shm", "agent-brief.md"];
|
|
576
|
+
const LOOP_D_DIRS = ["transcripts", "documents"];
|
|
577
|
+
/** Remove what a previous Loop D run left in `dir` (only the run's own files); returns the names removed. */
|
|
578
|
+
export function cleanLoopDDir(dir) {
|
|
579
|
+
if (!existsSync(dir))
|
|
580
|
+
return [];
|
|
581
|
+
const removed = [];
|
|
582
|
+
for (const name of LOOP_D_FILES) {
|
|
583
|
+
const p = join(dir, name);
|
|
584
|
+
if (existsSync(p)) {
|
|
585
|
+
rmSync(p, { force: true });
|
|
586
|
+
removed.push(name);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
for (const name of readdirSync(dir)) {
|
|
590
|
+
if (/^cases-.*\.json$/.test(name)) {
|
|
591
|
+
rmSync(join(dir, name), { force: true });
|
|
592
|
+
removed.push(name);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
for (const name of LOOP_D_DIRS) {
|
|
596
|
+
const p = join(dir, name);
|
|
597
|
+
if (existsSync(p)) {
|
|
598
|
+
rmSync(p, { recursive: true, force: true });
|
|
599
|
+
removed.push(`${name}/`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
return removed;
|
|
603
|
+
}
|
package/dist/demo/quickstart.js
CHANGED
|
@@ -17,7 +17,8 @@ import { birthCell } from "./birth.js";
|
|
|
17
17
|
import { mintCases } from "./demander.js";
|
|
18
18
|
import { cleanEnv, ownCli, waitForHttp } from "./loop-d.js";
|
|
19
19
|
export const MARKER = "quickstart.json";
|
|
20
|
-
export
|
|
20
|
+
export { PACKAGE_NAME } from "../util/version.js";
|
|
21
|
+
import { PACKAGE_NAME } from "../util/version.js";
|
|
21
22
|
/** How a client should start this very CLI: the published package through npx, or this checkout's compiled launcher. */
|
|
22
23
|
export function clientLauncher() {
|
|
23
24
|
const root = repoRoot();
|
|
@@ -87,22 +88,32 @@ export async function runQuickstart(o = {}) {
|
|
|
87
88
|
mkdirSync(keysDir, { recursive: true, mode: 0o700 });
|
|
88
89
|
// --- the wallet: the first step; made now when there is none, reused when there is; never a reason to stop
|
|
89
90
|
const wallet = ensureWallet(walletPath, { name: "default" });
|
|
91
|
+
const singleKey = wallet.wallet.roles.length === 1 && wallet.wallet.roles[0] === "signer";
|
|
92
|
+
const walletAddrs = singleKey ? `identity ${wallet.wallet.addresses.signer}` : `owner ${wallet.wallet.addresses.owner}, operator ${wallet.wallet.addresses.operator}, guardian ${wallet.wallet.addresses.guardian}`;
|
|
90
93
|
if (wallet.ephemeral)
|
|
91
94
|
log(`quickstart: wallet: could not write ${walletPath} (${wallet.reason}); using keys made in memory for this run — they are not kept`);
|
|
92
95
|
else
|
|
93
|
-
log(`quickstart: wallet: ${wallet.created ? "made" : "using"} ${wallet.wallet.dir} —
|
|
96
|
+
log(`quickstart: wallet: ${wallet.created ? "made" : "using"} ${wallet.wallet.dir} — ${walletAddrs}`);
|
|
94
97
|
// --- the chain and the world
|
|
95
98
|
log(`quickstart: starting ${version} on ${rpc} (log ${join(dir, "anvil.log")})`);
|
|
96
99
|
const anvil = await launchAnvil({ port, logPath: join(dir, "anvil.log") });
|
|
97
100
|
let node = null;
|
|
98
101
|
let endedWith = null;
|
|
99
102
|
const ended = new Promise((r) => (endedWith = r));
|
|
100
|
-
|
|
103
|
+
// a stop asked for (Ctrl-C, --exit, SIGTERM) ends the children in order; their exits are then expected and do not end the run
|
|
104
|
+
// early — otherwise the node's exit would resolve `ended` while anvil is still being stopped and leave the chain running
|
|
105
|
+
let stopping = false;
|
|
106
|
+
anvil.on("exit", (code, signal) => {
|
|
107
|
+
if (!stopping)
|
|
108
|
+
endedWith?.(`anvil stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`);
|
|
109
|
+
});
|
|
101
110
|
const stop = async () => {
|
|
111
|
+
stopping = true;
|
|
102
112
|
if (node)
|
|
103
113
|
await stopChild(node);
|
|
104
114
|
node = null;
|
|
105
115
|
await stopChild(anvil);
|
|
116
|
+
endedWith?.("stopped");
|
|
106
117
|
};
|
|
107
118
|
try {
|
|
108
119
|
const worldPath = join(dir, "world.json");
|
|
@@ -189,12 +200,17 @@ export async function runQuickstart(o = {}) {
|
|
|
189
200
|
const host = listen.slice(0, i) || "127.0.0.1";
|
|
190
201
|
nodeUrl = `http://${host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host}:${listen.slice(i + 1)}`;
|
|
191
202
|
node = spawn(cli.command, [...cli.args, "node", "serve", "--config", configPath, "--rpc", rpc, "--listen", listen], { env: cleanEnv({ FOGA_OPERATOR_KEY_FILE: keyPaths.operator }), stdio: ["ignore", "ignore", "pipe"] });
|
|
203
|
+
// the node's lines are relayed, except a watcher tick in which nothing happened (every two seconds, forever)
|
|
204
|
+
const idleTick = /: anchor ACTIVE, heartbeat not sent \([^)]*\); 0 new tender\(s\); 0 settled, 0 withdrawn; safety Satisfied$/;
|
|
192
205
|
node.stderr.on("data", (d) => {
|
|
193
206
|
for (const line of String(d).split("\n"))
|
|
194
|
-
if (line.trim())
|
|
207
|
+
if (line.trim() && !idleTick.test(line.trim()))
|
|
195
208
|
log(` node| ${line}`);
|
|
196
209
|
});
|
|
197
|
-
node.on("exit", (code, signal) =>
|
|
210
|
+
node.on("exit", (code, signal) => {
|
|
211
|
+
if (!stopping)
|
|
212
|
+
endedWith?.(`the node stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`);
|
|
213
|
+
});
|
|
198
214
|
await waitForHttp(`${nodeUrl}/healthz`);
|
|
199
215
|
// --- the entrance answers: health, a public read, the agent key's pre-check of the first tender, the MCP handshake and tool list
|
|
200
216
|
const health = (await (await fetch(`${nodeUrl}/healthz`)).json());
|
|
@@ -246,7 +262,8 @@ export async function runQuickstart(o = {}) {
|
|
|
246
262
|
lines.push("");
|
|
247
263
|
lines.push(` chain anvil on ${rpc} (chain ${world.chainId}), world ${world.worldId} — the chain lives as long as this process`);
|
|
248
264
|
lines.push(` cell ${name} = ${cell.account}${cell.agentId ? ` (ERC-8004 agent ${cell.agentId})` : ""}`);
|
|
249
|
-
|
|
265
|
+
const walletRoles = singleKey ? `identity ${cell.owner.address} (one key, every role)` : `owner ${cell.owner.address}, operator ${cell.operator.address}, guardian ${cell.guardian.address}`;
|
|
266
|
+
lines.push(` wallet ${wallet.ephemeral ? `keys made in memory for this run, written to ${keysDir} (${walletPath} was not writable)` : `${wallet.wallet.dir} (${wallet.created ? "made now" : "reused"}) — ${walletRoles}; the same wallet on every run`}`);
|
|
250
267
|
lines.push(` skills ${Object.keys(cell.held).join(", ")} (held and offered)`);
|
|
251
268
|
if (minted.length)
|
|
252
269
|
lines.push(` tenders ${minted.map((c) => `#${c.tokenId} ${c.skill} (${formatEther(c.rewardWei)} ETH)`).join(", ")} — open, waiting for the cell to take them`);
|
|
@@ -279,6 +296,22 @@ export async function runQuickstart(o = {}) {
|
|
|
279
296
|
lines.push(` FOGA_DEMANDER_KEY=<a funded key, anvil's first account by default> ${demanderCmd}`);
|
|
280
297
|
if (launcher.fromCheckout)
|
|
281
298
|
lines.push(`(This is a checkout: the configurations start ${launcher.command} ${launcher.args.join(" ")}; a published install would start npx -y ${PACKAGE_NAME}.)`);
|
|
299
|
+
// Next: what a cell should do once it runs — advice, kept out of the first run on purpose (docs/node.md, "Advice").
|
|
300
|
+
try {
|
|
301
|
+
const { adviseCell, formatAdvice } = await import("../node/advise.js");
|
|
302
|
+
const reader = await Node.open({ configPath, rpc, keys: {}, allowUnborn: true, log: () => { } });
|
|
303
|
+
try {
|
|
304
|
+
const next = formatAdvice(adviseCell(reader), { brief: true, config: configPath });
|
|
305
|
+
if (next)
|
|
306
|
+
lines.push("", next);
|
|
307
|
+
}
|
|
308
|
+
finally {
|
|
309
|
+
reader.close();
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch {
|
|
313
|
+
// advice is never a reason for a quickstart to fail
|
|
314
|
+
}
|
|
282
315
|
lines.push("");
|
|
283
316
|
lines.push(o.exit ? "Started with --exit: stopping the node and the chain." : "Ctrl-C stops the node and the chain; the next quickstart starts over in the same directory.");
|
|
284
317
|
const summary = lines.join("\n");
|
package/dist/genesis/probe.js
CHANGED
|
@@ -18,6 +18,22 @@ export async function runProbe(world, input) {
|
|
|
18
18
|
throw new Error(`block ${input.block} not found`);
|
|
19
19
|
const at = { blockTag: tag };
|
|
20
20
|
const fromBlock = input.lookback === undefined ? 0 : Math.max(0, input.block - input.lookback);
|
|
21
|
+
// A bounded scan is asked in windows of 2000 blocks, the windows in parallel and their logs concatenated in block
|
|
22
|
+
// order — the same logs a single query returns, in the same order (the answer hash is over them, so this must not
|
|
23
|
+
// change), but each request small enough for a public endpoint, which caps the range of `eth_getLogs` and answers
|
|
24
|
+
// a 9000-block scan slowly or not at all: a skill run on Sepolia overran its worker's budget on exactly this. An
|
|
25
|
+
// unbounded scan (no lookback: a local chain) stays one query.
|
|
26
|
+
const WINDOW = 2000;
|
|
27
|
+
const logsOf = async (c, filter) => {
|
|
28
|
+
const to = input.block;
|
|
29
|
+
if (input.lookback === undefined || to - fromBlock < WINDOW)
|
|
30
|
+
return c.queryFilter(filter, fromBlock, to);
|
|
31
|
+
const windows = [];
|
|
32
|
+
for (let a = fromBlock; a <= to; a += WINDOW)
|
|
33
|
+
windows.push([a, Math.min(a + WINDOW - 1, to)]);
|
|
34
|
+
const parts = await Promise.all(windows.map(([a, b]) => c.queryFilter(filter, a, b)));
|
|
35
|
+
return parts.flat();
|
|
36
|
+
};
|
|
21
37
|
const section = async (fn) => {
|
|
22
38
|
try {
|
|
23
39
|
return await fn();
|
|
@@ -95,13 +111,13 @@ export async function runProbe(world, input) {
|
|
|
95
111
|
});
|
|
96
112
|
checks.erc8454 = await section(async () => {
|
|
97
113
|
const units = world.contract("8454/AUFUnitRegistry");
|
|
98
|
-
const logs = await
|
|
114
|
+
const logs = await logsOf(units, units.filters.UnitRegistered(null, subject));
|
|
99
115
|
return { unitsControlled: logs.map((l) => String(l.args.unitId)) };
|
|
100
116
|
});
|
|
101
117
|
checks.erc8338 = await section(async () => {
|
|
102
118
|
const skills = world.contract("8338/SkillToken");
|
|
103
119
|
const held = String(await skills.balanceOf(subject, at));
|
|
104
|
-
const received = await
|
|
120
|
+
const received = await logsOf(skills, skills.filters.Transfer(null, subject));
|
|
105
121
|
const ids = new Set();
|
|
106
122
|
for (const l of received)
|
|
107
123
|
ids.add(String(l.args.tokenId));
|
|
@@ -122,13 +138,13 @@ export async function runProbe(world, input) {
|
|
|
122
138
|
checks.erc8414 = await section(async () => {
|
|
123
139
|
const tasks = world.contract("8414/TaskToken");
|
|
124
140
|
const held = String(await tasks.balanceOf(subject, at));
|
|
125
|
-
const submitted = await
|
|
126
|
-
const accepted = await
|
|
141
|
+
const submitted = await logsOf(tasks, tasks.filters.FulfillmentSubmitted(null, null, subject));
|
|
142
|
+
const accepted = await logsOf(tasks, tasks.filters.FulfillmentAccepted(null, null, subject));
|
|
127
143
|
return { held, submissions: String(submitted.length), accepted: String(accepted.length) };
|
|
128
144
|
});
|
|
129
145
|
checks.erc8447 = await section(async () => {
|
|
130
146
|
const swarms = world.contract("8447/SwarmRegistry");
|
|
131
|
-
const activated = await
|
|
147
|
+
const activated = await logsOf(swarms, swarms.filters.MemberActivated(null, subject));
|
|
132
148
|
const ids = [...new Set(activated.map((l) => String(l.args.swarmId)))].sort();
|
|
133
149
|
const memberships = [];
|
|
134
150
|
for (const swarmId of ids)
|
|
@@ -137,12 +153,12 @@ export async function runProbe(world, input) {
|
|
|
137
153
|
});
|
|
138
154
|
checks.erc8436 = await section(async () => {
|
|
139
155
|
const acdf = world.contract("8436/ACDFRegistry");
|
|
140
|
-
const filed = await
|
|
156
|
+
const filed = await logsOf(acdf, acdf.filters.IssueFiled(null, null, null, subject));
|
|
141
157
|
return { issuesFiled: String(filed.length) };
|
|
142
158
|
});
|
|
143
159
|
checks.erc8471 = await section(async () => {
|
|
144
160
|
const adrf = world.contract("8471/ADRFDisputeRegistry");
|
|
145
|
-
const filed = await
|
|
161
|
+
const filed = await logsOf(adrf, adrf.filters.CaseFiled());
|
|
146
162
|
let asParty = 0;
|
|
147
163
|
for (const l of filed) {
|
|
148
164
|
if (String(l.args.claimant).toLowerCase() === low || String(l.args.respondent).toLowerCase() === low)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import {
|
|
2
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
4
|
import { repoRoot } from "../util/fs.js";
|
|
4
5
|
import { buildSkillPackage, readSkillDir } from "../core/pack/skillpack.js";
|
|
5
6
|
/**
|
|
@@ -26,6 +27,34 @@ function workerPath() {
|
|
|
26
27
|
const ext = here.endsWith(".ts") ? ".ts" : ".js";
|
|
27
28
|
return new URL(`./skill-worker${ext}`, here).pathname;
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* The `node_modules` directories the worker's imports resolve through. In a checkout they are under the package root;
|
|
32
|
+
* in an npm or npx install the dependencies are hoisted beside the package (`<prefix>/node_modules/ethers` next to
|
|
33
|
+
* `<prefix>/node_modules/@finchip.ai/foga`), outside it, and a worker allowed to read the package root alone died on
|
|
34
|
+
* its first import (`ERR_ACCESS_DENIED` on `ethers/package.json`). Every ancestor's `node_modules` is granted, by its
|
|
35
|
+
* real path as well when a link is involved; the grant is read-only and the worker still never sees a key.
|
|
36
|
+
*/
|
|
37
|
+
export function dependencyDirs(root = repoRoot()) {
|
|
38
|
+
const out = new Set();
|
|
39
|
+
let dir = resolve(root);
|
|
40
|
+
for (;;) {
|
|
41
|
+
const nm = join(dir, "node_modules");
|
|
42
|
+
if (existsSync(nm)) {
|
|
43
|
+
out.add(nm);
|
|
44
|
+
try {
|
|
45
|
+
out.add(realpathSync(nm));
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
/* a dangling link: the literal path is enough */
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const parent = dirname(dir);
|
|
52
|
+
if (parent === dir)
|
|
53
|
+
break;
|
|
54
|
+
dir = parent;
|
|
55
|
+
}
|
|
56
|
+
return [...out];
|
|
57
|
+
}
|
|
29
58
|
/** True under the development loader (tsx): Node's permission model cannot be combined with a custom loader. */
|
|
30
59
|
function underLoader() {
|
|
31
60
|
return process.execArgv.some((a) => /--import|--loader|--require|tsx/.test(a)) || /\.ts$/.test(workerPath());
|
|
@@ -43,7 +72,7 @@ export async function runIsolated(job, opts = {}) {
|
|
|
43
72
|
const args = [...process.execArgv, "--max-old-space-size=512"];
|
|
44
73
|
if (!underLoader() && opts.readDirs?.length) {
|
|
45
74
|
args.push("--permission");
|
|
46
|
-
for (const d of new Set([...opts.readDirs, dirname(workerPath())]))
|
|
75
|
+
for (const d of new Set([...opts.readDirs, dirname(workerPath()), ...dependencyDirs()]))
|
|
47
76
|
args.push(`--allow-fs-read=${resolve(d)}`);
|
|
48
77
|
}
|
|
49
78
|
args.push(workerPath());
|
package/dist/http/server.js
CHANGED
|
@@ -6,6 +6,7 @@ import { A2AServer } from "../a2a/server.js";
|
|
|
6
6
|
import { A2A_ERRORS, A2A_WELL_KNOWN } from "../a2a/types.js";
|
|
7
7
|
import { buildMcpServer } from "../mcp/server.js";
|
|
8
8
|
import { DOCUMENT_FILES, documentsDir, readManifest } from "../node/documents.js";
|
|
9
|
+
import { buildPropagationManifest, servicesCatalogOf } from "../node/propagation.js";
|
|
9
10
|
import { PUBLIC_CALLER } from "../tools/index.js";
|
|
10
11
|
import { resolveCaller } from "./access.js";
|
|
11
12
|
import { NonceCache } from "./signed.js";
|
|
@@ -162,7 +163,7 @@ export function createHttpServer(o) {
|
|
|
162
163
|
if (method === "GET" && url.pathname === "/") {
|
|
163
164
|
const r = await catalog.invoke(node, "cell_status", {}, caller);
|
|
164
165
|
const decisions = node.store.summary().decisions;
|
|
165
|
-
return json(res, 200, { foga: o.version, cell: cfg.name, status: r.ok ? r.output : null, decisions, endpoints: { mcp: "/mcp", api: "/api/<capability>", a2a: "/a2a", agentCard: A2A_WELL_KNOWN, documents: "/documents/<file>", health: "/healthz" } });
|
|
166
|
+
return json(res, 200, { foga: o.version, cell: cfg.name, status: r.ok ? r.output : null, decisions, endpoints: { mcp: "/mcp", api: "/api/<capability>", a2a: "/a2a", agentCard: A2A_WELL_KNOWN, manifest: "/.well-known/foga-cell.json", documents: "/documents/<file>", health: "/healthz" } });
|
|
166
167
|
}
|
|
167
168
|
if (method === "GET" && url.pathname === "/api/capabilities")
|
|
168
169
|
return json(res, 200, { caller: { id: caller.id, level: caller.level, visibility: caller.visibility, via: resolved.via }, capabilities: catalog.list(caller) });
|
|
@@ -200,6 +201,24 @@ export function createHttpServer(o) {
|
|
|
200
201
|
return json(res, 400, { ok: false, gate: "input", reason: "mode must be simulate, build or act" });
|
|
201
202
|
return answer(res, await catalog.invoke(node, name, input, caller, { mode }));
|
|
202
203
|
}
|
|
204
|
+
// the propagation manifest: free, built live from the configuration, answered even before any document is published
|
|
205
|
+
if (method === "GET" && url.pathname === "/.well-known/foga-cell.json") {
|
|
206
|
+
const text = Buffer.from(JSON.stringify(buildPropagationManifest(node, { version: o.version }), null, 2) + "\n");
|
|
207
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "public, max-age=300", "content-length": text.length });
|
|
208
|
+
res.end(text);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
// the services catalog, when the one this cell's manifest points at is its own (the reference cell: the maintainer's list);
|
|
212
|
+
// a static file at this path on the host, answered by the proxy before the node, is how the list changes without a release
|
|
213
|
+
if (method === "GET" && url.pathname === "/services.json") {
|
|
214
|
+
const doc = servicesCatalogOf(node, { version: o.version });
|
|
215
|
+
if (!doc)
|
|
216
|
+
return json(res, 404, { ok: false, reason: "this cell's services pointer names another catalog; read it from the manifest at /.well-known/foga-cell.json" });
|
|
217
|
+
const text = Buffer.from(JSON.stringify(doc, null, 2) + "\n");
|
|
218
|
+
res.writeHead(200, { "content-type": "application/json; charset=utf-8", "cache-control": "public, max-age=300", "content-length": text.length });
|
|
219
|
+
res.end(text);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
203
222
|
// the published documents (foga cell docs build): served as built, by their file names, and at the well-known paths
|
|
204
223
|
if (method === "GET" && (url.pathname.startsWith("/.well-known/") || url.pathname.startsWith("/documents/"))) {
|
|
205
224
|
const dir = documentsDir(node);
|