@finchip.ai/foga 0.6.0-alpha.4 → 0.6.0-alpha.6
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 +14 -5
- package/bin/foga.js +3 -2
- package/dist/cli.js +59 -90
- package/dist/demo/anvil.js +129 -0
- package/dist/demo/birth.js +71 -0
- package/dist/demo/loop-d.js +227 -111
- package/dist/demo/quickstart.js +312 -0
- package/dist/doctor/loop-d.js +29 -7
- package/dist/world/deploy.js +13 -12
- package/docs/design-memo.md +2 -0
- package/docs/examples/README.md +5 -1
- package/docs/loop-d.md +22 -4
- package/docs/mcp.md +3 -3
- package/docs/quickstart.md +78 -0
- package/package.json +3 -1
- package/scripts/prepare.mjs +21 -0
- package/server.json +2 -2
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ 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 3c done — Loop D, the unscripted run
|
|
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
|
|
23
|
+
|
|
24
|
+
**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.
|
|
23
25
|
|
|
24
26
|
**3c (0.6.0-alpha.4, 2026-11-02).** A model in the loop, and the acceptance that looks at records rather than words. `src/node/runner.ts` is the node's loop with a model: each tick perceives through the read capabilities, hands the situation and the catalog as tools to the model, carries out what it proposes — reads at once, acts through `Catalog.invoke` where the three checks decide and a refusal goes back so the model can revise once — and records the tick by hash (the decision records carry the model and the hashes of its request and answer; the journal the hash of the whole transcript; the text never). Limits bound a tick (12 tool calls, 2 acts) and a day (the model's calls, counted in the store). Two adapters on one interface: `anthropic` (the Messages API over `fetch`, the key in the environment, the model name in the configuration) and `stub` (a deterministic decision table that also proposes, once, a tender above the budget so the evidence shows the gate closing). `foga node run` is the embedded node — server, watchers, runner — on the operator key alone. `foga demo --loop-d` (`src/demo/loop-d.ts`) is the unscripted run: a new cell with fresh keys, an independent demander process minting cases a–f in a seeded order (two tenders to take, four to refuse at `skill`, `policy`, `verifier`, and one met during the guardian's suspension), the node killed after its first submission and restarted, the HTTP entrance used beside the runner (the same request answered once, the same job under another request a conflict, three mints at once of which the third is refused at `budget` before anything is sent), and an evidence file with every record, the cases' outcomes and a scan of every transaction the operator key signed. `foga doctor --loop-d` judges it beside layer 5: every operation under an allowed decision, refusals that broadcast nothing, the four budget books, the operator's transactions all at the account and allow-listed, the cases as expected, the recovery, the entrances, and — given two runs — the same outcomes from the stub and a real model. The slice also closed a race the run exposed: the catalog's decision section now runs one act at a time per node and a job reserves the value it will move from its decision. `test/loop-d.test.ts` (anvil, under a minute) and `test/model.test.ts` are the gate; CI runs Loop D on the world the genesis ran on. docs/loop-d.md.
|
|
25
27
|
|
|
@@ -70,11 +72,17 @@ From Phase 1, unchanged in substance:
|
|
|
70
72
|
- **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.
|
|
71
73
|
- **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.
|
|
72
74
|
|
|
73
|
-
What is next: 3d closes Phase 3 —
|
|
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.
|
|
74
76
|
|
|
75
77
|
## Quick start
|
|
76
78
|
|
|
77
|
-
|
|
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`):
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
npx -y @finchip.ai/foga quickstart
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
From a checkout, three ways in, from most to least demanding:
|
|
78
86
|
|
|
79
87
|
```bash
|
|
80
88
|
# 1. Full reproduction (needs git, Node >= 22 and Foundry):
|
|
@@ -197,9 +205,10 @@ foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch]
|
|
|
197
205
|
foga node key new --config <cell.config.json> --id <id> [--level read|build|act] [--visibility public|caller|operator] [--note <text>]
|
|
198
206
|
foga node run --config <cell.config.json> [--model stub|anthropic] [--listen <host:port> | --no-http] [--interval <s>] [--once | --ticks <n>] [--transcripts <dir>]
|
|
199
207
|
the embedded node: server, watchers and the runner (operator key alone)
|
|
200
|
-
foga demo --loop-d [--model stub|anthropic] [--model-name <name>] [--seed <n>] [--no-recovery] anvil, the world, then Loop D judged by the doctor
|
|
201
|
-
foga loop-d run --world <world.json> --rpc <url> [--dir <dir>] [--out <evidence.json>] [--model] [--model-name] [--seed] [--no-recovery]
|
|
208
|
+
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
|
+
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>]
|
|
202
210
|
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
|
|
203
212
|
```
|
|
204
213
|
|
|
205
214
|
During development the CLI runs from source with `npm run foga -- <args>`; `npm run build` compiles it to `dist/` for the `foga` binary.
|
package/bin/foga.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Launcher for the published package: runs the compiled CLI.
|
|
3
3
|
// During development use `npm run foga -- <args>` (tsx) instead.
|
|
4
4
|
import { existsSync } from "node:fs";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
6
|
import { dirname, join } from "node:path";
|
|
7
7
|
|
|
8
8
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -11,4 +11,5 @@ if (!existsSync(dist)) {
|
|
|
11
11
|
console.error("foga: compiled CLI not found. Run `npm run build` first, or use `npm run foga -- <args>`.");
|
|
12
12
|
process.exit(1);
|
|
13
13
|
}
|
|
14
|
-
|
|
14
|
+
// a dynamic import takes a URL: on Windows an absolute path (C:\...) is not one
|
|
15
|
+
await import(pathToFileURL(dist).href);
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { closeSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
-
import { createConnection } from "node:net";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
5
3
|
import { cpus, homedir, loadavg } from "node:os";
|
|
6
4
|
import { dirname, join, relative, resolve } from "node:path";
|
|
7
5
|
import { buildBundle } from "./build/bundle.js";
|
|
8
6
|
import { syncVendor, verifyVendor } from "./build/vendor.js";
|
|
7
|
+
import { anvilBinary, launchAnvil, portProblem } from "./demo/anvil.js";
|
|
8
|
+
import { LOOP_D_MODELS } from "./demo/loop-d.js";
|
|
9
9
|
import { runDoctor } from "./doctor/doctor.js";
|
|
10
|
-
import {
|
|
10
|
+
import { exists, readJson, repoRoot, writeJson } from "./util/fs.js";
|
|
11
11
|
import { fail, log } from "./util/log.js";
|
|
12
12
|
import { attachWorld, deployWorld, loadWorldConfig, missingPlanSteps } from "./world/deploy.js";
|
|
13
13
|
import { loadBundle } from "./bundle/load.js";
|
|
@@ -38,12 +38,17 @@ usage:
|
|
|
38
38
|
[--zk auto|require|skip] [--crash-after <opKey>] [--crash-before <opKey>]
|
|
39
39
|
run the genesis scenario (idempotent; re-run to resume)
|
|
40
40
|
foga genesis status --world <world.json> [--store <file.sqlite>] list the operations the store holds
|
|
41
|
+
foga quickstart [--dir <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit]
|
|
42
|
+
one command to a live cell: anvil, the local world, a cell born with fresh keys and its two skills, two tenders,
|
|
43
|
+
the node serving HTTP and MCP, and the MCP client configurations printed with every path filled in (~/.foga/quickstart)
|
|
41
44
|
foga demo [--port <n>] [--keep] [--genesis <T0..T9>] start anvil, deploy the local world, run the doctor (and the genesis)
|
|
42
|
-
foga demo --loop-d [--model stub|anthropic] [--model-name <name>] [--seed <n>] [--no-recovery] [--port <n>] [--keep]
|
|
43
|
-
the above, then Loop D: a new cell born, a demander minting cases a-g, the
|
|
44
|
-
the recovery drill, the guardian's suspension; evidence to deployments/loop-d.local.json, judged by the doctor
|
|
45
|
-
foga loop-d run --world <world.json> --rpc <url> [--dir <dir>] [--out <evidence.json>] [--model stub|anthropic] [--model-name <name>] [--seed <n>] [--no-recovery]
|
|
46
|
-
|
|
45
|
+
foga demo --loop-d [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery] [--port <n>] [--keep]
|
|
46
|
+
the above, then Loop D: a new cell born, a demander minting cases a-g, the node with a model (or served for an
|
|
47
|
+
outside agent), the recovery drill, the guardian's suspension; evidence to deployments/loop-d.local.json, judged by the doctor
|
|
48
|
+
foga loop-d run --world <world.json> --rpc <url> [--dir <dir>] [--out <evidence.json>] [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery]
|
|
49
|
+
[--listen <host:port>] [--wait <s>] [--idle <s>]
|
|
50
|
+
Loop D on a world that exists (FOGA_FUNDER_KEY funds the cell's roles on a non-local chain); with --model external the
|
|
51
|
+
node is served and an outside agent — any model, any runtime — works the tenders through /mcp or /api with the key printed
|
|
47
52
|
foga demo demander --world <w> --rpc <url> --store <sqlite> --cell <addr> --scheme <id> --issuer <addr> --probe-hash <h> --extract-hash <h> --cases a,b,c --seed <n> --out <json>
|
|
48
53
|
the independent demander of Loop D (FOGA_DEMANDER_KEY; anvil's first account by default)
|
|
49
54
|
foga bundle info summarize the committed bundle
|
|
@@ -350,33 +355,54 @@ async function main() {
|
|
|
350
355
|
const { runLoopD, defaultLoopDDir } = await import("./demo/loop-d.js");
|
|
351
356
|
const worldPath = str(flags, "world") ?? fail("--world <world.json> is required");
|
|
352
357
|
const rpc = str(flags, "rpc") ?? process.env.FOGA_RPC_URL ?? fail("--rpc <url> is required");
|
|
353
|
-
|
|
354
|
-
if (model !== "stub" && model !== "anthropic")
|
|
355
|
-
fail("--model must be stub or anthropic");
|
|
356
|
-
await runLoopD({ rpc, worldPath: resolve(worldPath), dir: str(flags, "dir") ? resolve(str(flags, "dir")) : defaultLoopDDir(), evidencePath: resolve(str(flags, "out") ?? join(root, "deployments", "loop-d.local.json")), model, modelName: str(flags, "model-name") ?? process.env.FOGA_MODEL, seed: str(flags, "seed") ? Number(str(flags, "seed")) : undefined, recovery: !flags["no-recovery"], funderKey: process.env.FOGA_FUNDER_KEY });
|
|
358
|
+
await runLoopD({ rpc, worldPath: resolve(worldPath), dir: str(flags, "dir") ? resolve(str(flags, "dir")) : defaultLoopDDir(), evidencePath: resolve(str(flags, "out") ?? join(root, "deployments", "loop-d.local.json")), funderKey: process.env.FOGA_FUNDER_KEY, ...loopDFlags(flags) });
|
|
357
359
|
return;
|
|
358
360
|
}
|
|
361
|
+
if (group === "quickstart") {
|
|
362
|
+
const { runQuickstart } = await import("./demo/quickstart.js");
|
|
363
|
+
let result;
|
|
364
|
+
try {
|
|
365
|
+
result = await runQuickstart({ dir: str(flags, "dir"), port: str(flags, "port") ? Number(str(flags, "port")) : undefined, listen: flags["no-serve"] ? null : str(flags, "listen"), name: str(flags, "name"), tenders: !flags["no-tenders"], exit: Boolean(flags.exit) });
|
|
366
|
+
}
|
|
367
|
+
catch (e) {
|
|
368
|
+
fail(e.message);
|
|
369
|
+
}
|
|
370
|
+
if (flags.exit) {
|
|
371
|
+
await result.stop();
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const stopping = () => {
|
|
375
|
+
console.error("quickstart: stopping the node and the chain");
|
|
376
|
+
result.stop().finally(() => process.exit(0));
|
|
377
|
+
};
|
|
378
|
+
process.on("SIGINT", stopping);
|
|
379
|
+
process.on("SIGTERM", stopping);
|
|
380
|
+
const why = await result.ended;
|
|
381
|
+
fail(`${why}; the quickstart is over`);
|
|
382
|
+
}
|
|
359
383
|
if (group === "demo") {
|
|
360
|
-
|
|
361
|
-
if (model !== "stub" && model !== "anthropic")
|
|
362
|
-
fail("--model must be stub or anthropic");
|
|
363
|
-
await demo({ port: Number(str(flags, "port") ?? 8545), keep: Boolean(flags.keep), genesis: str(flags, "genesis"), loopD: flags["loop-d"] ? { model, modelName: str(flags, "model-name") ?? process.env.FOGA_MODEL, seed: str(flags, "seed") ? Number(str(flags, "seed")) : undefined, recovery: !flags["no-recovery"] } : undefined });
|
|
384
|
+
await demo({ port: Number(str(flags, "port") ?? 8545), keep: Boolean(flags.keep), genesis: str(flags, "genesis"), loopD: flags["loop-d"] ? loopDFlags(flags) : undefined });
|
|
364
385
|
return;
|
|
365
386
|
}
|
|
366
387
|
console.log(USAGE);
|
|
367
388
|
fail(`unknown command: ${positional.join(" ")}`);
|
|
368
389
|
}
|
|
390
|
+
/** The Loop D options the flags name (shared by `foga demo --loop-d` and `foga loop-d run`). */
|
|
391
|
+
function loopDFlags(flags) {
|
|
392
|
+
const model = (str(flags, "model") ?? "stub");
|
|
393
|
+
if (!LOOP_D_MODELS.includes(model))
|
|
394
|
+
fail(`--model must be one of ${LOOP_D_MODELS.join(", ")}`);
|
|
395
|
+
const seconds = (key) => (str(flags, key) ? Number(str(flags, key)) * 1000 : undefined);
|
|
396
|
+
return { model, modelName: str(flags, "model-name") ?? process.env.FOGA_MODEL, seed: str(flags, "seed") ? Number(str(flags, "seed")) : undefined, recovery: !flags["no-recovery"], listen: str(flags, "listen"), waitMs: seconds("wait"), idleMs: seconds("idle") };
|
|
397
|
+
}
|
|
369
398
|
async function demo(opts) {
|
|
370
399
|
const root = repoRoot();
|
|
371
400
|
const rpc = `http://127.0.0.1:${opts.port}`;
|
|
372
401
|
// Say what is wrong before starting anything: a port that is already taken makes anvil exit at once, and a
|
|
373
402
|
// silent exit would otherwise surface only as "the RPC did not come up".
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
fail(
|
|
377
|
-
? `${rpc} already answers JSON-RPC (another anvil or node is running). Stop it, or pass --port <n> to use another port.`
|
|
378
|
-
: `port ${opts.port} is in use by something that does not speak JSON-RPC. Free it, or pass --port <n>.`);
|
|
379
|
-
}
|
|
403
|
+
const problem = await portProblem(opts.port);
|
|
404
|
+
if (problem)
|
|
405
|
+
fail(problem);
|
|
380
406
|
// A machine that is already saturated (a proving-key build, for instance) starves anvil: blocks come late and the
|
|
381
407
|
// kernel gives up on a receipt after its timeout. Say so before starting rather than after a failure.
|
|
382
408
|
const cores = cpus().length || 1;
|
|
@@ -385,51 +411,27 @@ async function demo(opts) {
|
|
|
385
411
|
log.warn(`this machine is busy (load ${load.toFixed(1)} on ${cores} core${cores === 1 ? "" : "s"}): another heavy process slows anvil down and can make the genesis time out waiting for receipts. If a proving-key build (npm run zk:predicate) is running, let it finish or stop it first.`);
|
|
386
412
|
}
|
|
387
413
|
// FOGA_FOUNDRY_DIR points at a Foundry install of its own (scripts/install-foundry.sh puts the static build there).
|
|
388
|
-
const foundryDir = process.env.FOGA_FOUNDRY_DIR;
|
|
389
|
-
const anvilBin = foundryDir ? join(foundryDir, "anvil") : "anvil";
|
|
390
414
|
// anvil's output goes to a log file rather than a pipe: with --keep the node survives this process, and a
|
|
391
415
|
// Rust program whose stderr pipe has gone away dies on its next write.
|
|
416
|
+
const { path: anvilBin, fromDir } = anvilBinary();
|
|
392
417
|
const logPath = join(root, ".foga-build", "anvil.log");
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
});
|
|
418
|
+
log.info(`starting anvil on ${rpc}${fromDir ? ` (${anvilBin})` : ""}; log ${relative(root, logPath)}`);
|
|
419
|
+
let anvil;
|
|
420
|
+
try {
|
|
421
|
+
anvil = await launchAnvil({ port: opts.port, logPath, detached: opts.keep });
|
|
422
|
+
}
|
|
423
|
+
catch (e) {
|
|
424
|
+
fail(e.message);
|
|
425
|
+
}
|
|
402
426
|
const stop = () => {
|
|
403
427
|
if (!opts.keep && !anvil.killed)
|
|
404
428
|
anvil.kill("SIGTERM");
|
|
405
429
|
};
|
|
406
|
-
anvil.on("error", (e) => fail(e.code === "ENOENT"
|
|
407
|
-
? foundryDir
|
|
408
|
-
? `${anvilBin} not found (FOGA_FOUNDRY_DIR=${foundryDir}). Run scripts/install-foundry.sh ${foundryDir} first.`
|
|
409
|
-
: "anvil not found on PATH. Install Foundry (https://getfoundry.sh: `curl -L https://foundry.sh | bash && foundryup`), or run scripts/install-foundry.sh and export FOGA_FOUNDRY_DIR, or use docker compose."
|
|
410
|
-
: `could not start anvil: ${e.message}`));
|
|
411
430
|
process.on("exit", stop);
|
|
412
431
|
process.on("SIGINT", () => {
|
|
413
432
|
stop();
|
|
414
433
|
process.exit(130);
|
|
415
434
|
});
|
|
416
|
-
try {
|
|
417
|
-
await waitForRpc(rpc, 20000, () => exited !== null);
|
|
418
|
-
}
|
|
419
|
-
catch (e) {
|
|
420
|
-
let output = "";
|
|
421
|
-
try {
|
|
422
|
-
output = readFileSync(logPath, "utf8").slice(-8000);
|
|
423
|
-
}
|
|
424
|
-
catch {
|
|
425
|
-
/* no log */
|
|
426
|
-
}
|
|
427
|
-
const why = exited ? `anvil stopped (${exited}) before serving ${rpc}` : `anvil did not serve ${rpc} within 20 s`;
|
|
428
|
-
const glibc = /GLIBC_[0-9.]+' not found/.test(output)
|
|
429
|
-
? "\nThis anvil is the glibc build and this machine's glibc is older than it needs (GLIBC >= 2.35: Ubuntu 22.04 or newer). Install the static build next to it: `scripts/install-foundry.sh` then `export FOGA_FOUNDRY_DIR=~/.foga/foundry` (or put that directory first on PATH)."
|
|
430
|
-
: "";
|
|
431
|
-
fail(`${why}.${output.trim() ? `\n--- anvil output ---\n${output.trim()}\n--------------------` : ""}${glibc}\nRun \`${anvilBin} --port ${opts.port}\` by hand to see its output; \`${anvilBin} --version\` shows the installed version (CI uses Foundry v1.7.1).`);
|
|
432
|
-
}
|
|
433
435
|
const worldConfig = join(root, "worlds", "local.json");
|
|
434
436
|
const out = join(root, "deployments", "local.json");
|
|
435
437
|
await deployWorld({ worldConfigPath: worldConfig, rpc, outPath: out });
|
|
@@ -454,7 +456,7 @@ async function demo(opts) {
|
|
|
454
456
|
if (opts.loopD) {
|
|
455
457
|
const { runLoopD, defaultLoopDDir } = await import("./demo/loop-d.js");
|
|
456
458
|
const evidencePath = join(root, "deployments", "loop-d.local.json");
|
|
457
|
-
await runLoopD({ rpc, worldPath: out, dir: defaultLoopDDir(), evidencePath,
|
|
459
|
+
await runLoopD({ rpc, worldPath: out, dir: defaultLoopDDir(), evidencePath, ...opts.loopD });
|
|
458
460
|
const judged = await runDoctor({ worldPath: out, rpc, outPath: join(root, "deployments", "local.doctor.json"), genesisPath: opts.genesis ? join(root, "deployments", "local.genesis.json") : undefined, loopDPath: evidencePath, quiet: true });
|
|
459
461
|
if (judged.report && judged.report.summary.Fail > 0)
|
|
460
462
|
process.exitCode = 3;
|
|
@@ -467,39 +469,6 @@ async function demo(opts) {
|
|
|
467
469
|
else
|
|
468
470
|
stop();
|
|
469
471
|
}
|
|
470
|
-
/** True when something accepts TCP connections on 127.0.0.1:port. */
|
|
471
|
-
function portInUse(port) {
|
|
472
|
-
return new Promise((resolve) => {
|
|
473
|
-
const socket = createConnection({ host: "127.0.0.1", port });
|
|
474
|
-
const done = (v) => {
|
|
475
|
-
socket.destroy();
|
|
476
|
-
resolve(v);
|
|
477
|
-
};
|
|
478
|
-
socket.setTimeout(1000, () => done(false));
|
|
479
|
-
socket.once("connect", () => done(true));
|
|
480
|
-
socket.once("error", () => done(false));
|
|
481
|
-
});
|
|
482
|
-
}
|
|
483
|
-
async function rpcAnswers(rpc) {
|
|
484
|
-
try {
|
|
485
|
-
const res = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }), signal: AbortSignal.timeout(2000) });
|
|
486
|
-
return res.ok;
|
|
487
|
-
}
|
|
488
|
-
catch {
|
|
489
|
-
return false;
|
|
490
|
-
}
|
|
491
|
-
}
|
|
492
|
-
async function waitForRpc(rpc, timeoutMs, giveUp) {
|
|
493
|
-
const start = Date.now();
|
|
494
|
-
while (Date.now() - start < timeoutMs) {
|
|
495
|
-
if (await rpcAnswers(rpc))
|
|
496
|
-
return;
|
|
497
|
-
if (giveUp?.())
|
|
498
|
-
break;
|
|
499
|
-
await new Promise((r) => setTimeout(r, 250));
|
|
500
|
-
}
|
|
501
|
-
throw new Error(`RPC at ${rpc} did not come up within ${timeoutMs} ms`);
|
|
502
|
-
}
|
|
503
472
|
main().catch((e) => {
|
|
504
473
|
fail(e?.stack ?? String(e));
|
|
505
474
|
});
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
2
|
+
import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
/**
|
|
6
|
+
* A local chain for the demos (`foga demo`, `foga quickstart`): finding anvil, starting it with its output in a log
|
|
7
|
+
* file, waiting for its RPC, and saying what is wrong in the words a first run needs — the binary missing (with the
|
|
8
|
+
* install path per platform), the port taken, the glibc too old for the dynamic build.
|
|
9
|
+
*/
|
|
10
|
+
/** The anvil binary this machine offers: FOGA_FOUNDRY_DIR/anvil (scripts/install-foundry.sh) or anvil on PATH. */
|
|
11
|
+
export function anvilBinary() {
|
|
12
|
+
const dir = process.env.FOGA_FOUNDRY_DIR;
|
|
13
|
+
return { path: dir ? join(dir, "anvil") : "anvil", fromDir: dir ?? null };
|
|
14
|
+
}
|
|
15
|
+
/** anvil's version line, or null when the binary cannot be run from here. */
|
|
16
|
+
export function anvilVersion(path) {
|
|
17
|
+
try {
|
|
18
|
+
return execFileSync(path, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n")[0];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export const ANVIL_INSTALL_HINT = [
|
|
25
|
+
"anvil (Foundry) is not installed, or not where FOGA_FOUNDRY_DIR says.",
|
|
26
|
+
" Linux, macOS: curl -L https://foundry.sh | bash && foundryup (an older glibc: scripts/install-foundry.sh, then export FOGA_FOUNDRY_DIR=~/.foga/foundry)",
|
|
27
|
+
" Windows: download the win32_amd64 zip of a release from https://github.com/foundry-rs/foundry/releases, unzip it, and put the folder",
|
|
28
|
+
" holding anvil.exe on PATH (or set FOGA_FOUNDRY_DIR to that folder); then open a new terminal",
|
|
29
|
+
" any platform: docker compose (docker/) runs the toolchain with anvil inside",
|
|
30
|
+
].join("\n");
|
|
31
|
+
/** True when something accepts TCP connections on 127.0.0.1:port. */
|
|
32
|
+
export function portInUse(port) {
|
|
33
|
+
return new Promise((resolve) => {
|
|
34
|
+
const socket = createConnection({ host: "127.0.0.1", port });
|
|
35
|
+
const done = (v) => {
|
|
36
|
+
socket.destroy();
|
|
37
|
+
resolve(v);
|
|
38
|
+
};
|
|
39
|
+
socket.setTimeout(1000, () => done(false));
|
|
40
|
+
socket.once("connect", () => done(true));
|
|
41
|
+
socket.once("error", () => done(false));
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export async function rpcAnswers(rpc) {
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }), signal: AbortSignal.timeout(2000) });
|
|
47
|
+
return res.ok;
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function waitForRpc(rpc, timeoutMs, giveUp) {
|
|
54
|
+
const start = Date.now();
|
|
55
|
+
while (Date.now() - start < timeoutMs) {
|
|
56
|
+
if (await rpcAnswers(rpc))
|
|
57
|
+
return;
|
|
58
|
+
if (giveUp?.())
|
|
59
|
+
break;
|
|
60
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
61
|
+
}
|
|
62
|
+
throw new Error(`RPC at ${rpc} did not come up within ${timeoutMs} ms`);
|
|
63
|
+
}
|
|
64
|
+
/** Why a port cannot be used for a new anvil: another RPC already answers there, or something else holds it. */
|
|
65
|
+
export async function portProblem(port) {
|
|
66
|
+
if (!(await portInUse(port)))
|
|
67
|
+
return null;
|
|
68
|
+
const rpc = `http://127.0.0.1:${port}`;
|
|
69
|
+
return (await rpcAnswers(rpc))
|
|
70
|
+
? `${rpc} already answers JSON-RPC (another anvil or node is running). Stop it, or pass --port <n> to use another port.`
|
|
71
|
+
: `port ${port} is in use by something that does not speak JSON-RPC. Free it, or pass --port <n>.`;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Start anvil and wait until its RPC answers. Rejects with a message fit to print when the binary is missing, when
|
|
75
|
+
* anvil stops before serving (its output quoted, the glibc case named) or when it does not come up in time.
|
|
76
|
+
*/
|
|
77
|
+
export async function launchAnvil(o) {
|
|
78
|
+
const { path: bin, fromDir } = anvilBinary();
|
|
79
|
+
const rpc = `http://127.0.0.1:${o.port}`;
|
|
80
|
+
mkdirSync(dirname(o.logPath), { recursive: true });
|
|
81
|
+
const logFd = openSync(o.logPath, "w");
|
|
82
|
+
const child = spawn(bin, ["--port", String(o.port), "--silent", ...(o.args ?? [])], { stdio: ["ignore", logFd, logFd], detached: Boolean(o.detached) });
|
|
83
|
+
closeSync(logFd);
|
|
84
|
+
let exited = null;
|
|
85
|
+
child.on("exit", (code, signal) => {
|
|
86
|
+
exited = code !== null ? `exit code ${code}` : `signal ${signal}`;
|
|
87
|
+
});
|
|
88
|
+
const spawnError = new Promise((_, reject) => {
|
|
89
|
+
child.once("error", (e) => {
|
|
90
|
+
reject(new Error(e.code === "ENOENT" ? (fromDir ? `${bin} not found (FOGA_FOUNDRY_DIR=${fromDir}). Run scripts/install-foundry.sh ${fromDir} first, or unset FOGA_FOUNDRY_DIR to use anvil on PATH.\n${ANVIL_INSTALL_HINT}` : `anvil not found on PATH.\n${ANVIL_INSTALL_HINT}`) : `could not start anvil: ${e.message}`));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
const timeoutMs = o.timeoutMs ?? 20_000;
|
|
94
|
+
try {
|
|
95
|
+
await Promise.race([waitForRpc(rpc, timeoutMs, () => exited !== null), spawnError]);
|
|
96
|
+
}
|
|
97
|
+
catch (e) {
|
|
98
|
+
if (!child.killed && child.exitCode === null)
|
|
99
|
+
child.kill("SIGTERM");
|
|
100
|
+
if (e.message?.startsWith("RPC at ")) {
|
|
101
|
+
let output = "";
|
|
102
|
+
try {
|
|
103
|
+
output = readFileSync(o.logPath, "utf8").slice(-8000);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
/* no log */
|
|
107
|
+
}
|
|
108
|
+
const why = exited ? `anvil stopped (${exited}) before serving ${rpc}` : `anvil did not serve ${rpc} within ${timeoutMs / 1000} s`;
|
|
109
|
+
const glibc = /GLIBC_[0-9.]+' not found/.test(output)
|
|
110
|
+
? "\nThis anvil is the glibc build and this machine's glibc is older than it needs (GLIBC >= 2.35: Ubuntu 22.04 or newer). Install the static build next to it: `scripts/install-foundry.sh` then `export FOGA_FOUNDRY_DIR=~/.foga/foundry` (or put that directory first on PATH)."
|
|
111
|
+
: "";
|
|
112
|
+
throw new Error(`${why}.${output.trim() ? `\n--- anvil output ---\n${output.trim()}\n--------------------` : ""}${glibc}\nRun \`${bin} --port ${o.port}\` by hand to see its output; \`${bin} --version\` shows the installed version (CI uses Foundry v1.7.1).`);
|
|
113
|
+
}
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
return child;
|
|
117
|
+
}
|
|
118
|
+
/** Stop a child and wait for it to be gone (SIGTERM, then SIGKILL after a grace period). */
|
|
119
|
+
export async function stopChild(child, graceMs = 5000) {
|
|
120
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
121
|
+
return;
|
|
122
|
+
const gone = new Promise((r) => child.once("exit", () => r()));
|
|
123
|
+
child.kill("SIGTERM");
|
|
124
|
+
await Promise.race([gone, new Promise((r) => setTimeout(r, graceMs))]);
|
|
125
|
+
if (child.exitCode === null && child.signalCode === null) {
|
|
126
|
+
child.kill("SIGKILL");
|
|
127
|
+
await Promise.race([gone, new Promise((r) => setTimeout(r, 2000))]);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { Wallet, getAddress, parseEther } from "ethers";
|
|
3
|
+
import { dataUriJcs, keccakJcs } from "../core/canonical.js";
|
|
4
|
+
import { accountSubject, fogaAttestedSchemeDescriptor } from "../core/kya.js";
|
|
5
|
+
import { loadCellConfig, newCellConfig, saveCellConfig } from "../node/config.js";
|
|
6
|
+
import { cellInit } from "../node/init.js";
|
|
7
|
+
import { Node } from "../node/node.js";
|
|
8
|
+
import { defaultCatalog, OPERATOR_CALLER } from "../tools/index.js";
|
|
9
|
+
import { repoRoot } from "../util/fs.js";
|
|
10
|
+
export async function birthCell(o) {
|
|
11
|
+
const k = o.kernel;
|
|
12
|
+
const log = o.log ?? (() => { });
|
|
13
|
+
const root = repoRoot();
|
|
14
|
+
const W = k.address("W");
|
|
15
|
+
const owner = Wallet.createRandom();
|
|
16
|
+
const operator = Wallet.createRandom();
|
|
17
|
+
const guardian = Wallet.createRandom();
|
|
18
|
+
log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}`);
|
|
19
|
+
// --- funding by W, the standing scheme registered by W
|
|
20
|
+
const price = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
|
|
21
|
+
const gas = (units) => units * price * 2n;
|
|
22
|
+
await k.transfer("fund.owner", "W", owner.address, parseEther("0.05") + gas(12000000n));
|
|
23
|
+
await k.transfer("fund.operator", "W", operator.address, gas(20000000n) + parseEther("0.003"));
|
|
24
|
+
await k.transfer("fund.guardian", "W", guardian.address, gas(1000000n));
|
|
25
|
+
const att = await k.call("scheme", "W", k.kya.schemeRegistryAddress, k.kya.schemes().interface, "registerScheme", [k.kya.attestedSchemeInput(fogaAttestedSchemeDescriptor())]);
|
|
26
|
+
const schemeId = k.kya.schemeIdFromReceipt(att.receipt);
|
|
27
|
+
const bornAtBlock = await k.blockNumber();
|
|
28
|
+
// --- the configuration and the birth (the owner's act, in this process, as `foga cell init` would)
|
|
29
|
+
const cfg = newCellConfig({ name: o.name, world: o.worldPath, store: o.storePath, owner: owner.address, operator: operator.address, guardian: guardian.address, rpc: o.rpc, skillsDir: join(root, "scenarios", "skills"), inputDir: join(root, "scenarios", "fixtures"), description: o.description });
|
|
30
|
+
cfg.identity.standing = { schemeId, issuers: [W] };
|
|
31
|
+
o.configure?.(cfg);
|
|
32
|
+
saveCellConfig(o.configPath, cfg);
|
|
33
|
+
const birthNode = await Node.open({ configPath: o.configPath, rpc: o.rpc, keys: { owner: owner.privateKey }, allowUnborn: true, log: () => { } });
|
|
34
|
+
const born = await cellInit(birthNode, { log: () => { } });
|
|
35
|
+
birthNode.close();
|
|
36
|
+
const account = getAddress(born.account);
|
|
37
|
+
log(`cell ${o.name}: born as ${account} (agent ${born.agentId}) in ${born.operations.length} operations`);
|
|
38
|
+
// --- standing from the world operator, the two skills minted by the operator
|
|
39
|
+
const claim = { scheme: "S_att", level: 2, subject: `eip155:${k.chainId}:${account.toLowerCase()}` };
|
|
40
|
+
const expiresAt = (await k.blockTimestamp()) + 365 * 86400;
|
|
41
|
+
await k.call("attest", "W", k.kya.registryAddress, k.kya.registry().interface, "attest", [accountSubject(k.chainId, account), schemeId, 2, keccakJcs(claim), expiresAt, dataUriJcs(claim), keccakJcs(claim)]);
|
|
42
|
+
const catalog = defaultCatalog();
|
|
43
|
+
const skillNode = await Node.open({ configPath: o.configPath, rpc: o.rpc, keys: { operator: operator.privateKey }, log: () => { } });
|
|
44
|
+
try {
|
|
45
|
+
for (const skill of ["foga-probe", "finch-extract-verify"]) {
|
|
46
|
+
const r = await catalog.invoke(skillNode, "skill_mint", { skill }, OPERATOR_CALLER, { mode: "act" });
|
|
47
|
+
if (!r.ok)
|
|
48
|
+
throw new Error(`skill_mint ${skill}: ${r.gate}: ${r.reason}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
finally {
|
|
52
|
+
skillNode.close();
|
|
53
|
+
}
|
|
54
|
+
const held = loadCellConfig(o.configPath).config.skills.held;
|
|
55
|
+
log(`cell ${o.name}: standing level 2 attested by ${W}; skills minted (${Object.keys(held).join(", ")})`);
|
|
56
|
+
return {
|
|
57
|
+
owner,
|
|
58
|
+
operator,
|
|
59
|
+
guardian,
|
|
60
|
+
account,
|
|
61
|
+
agentId: born.agentId,
|
|
62
|
+
specId: born.specId ?? null,
|
|
63
|
+
schemeId,
|
|
64
|
+
bornAtBlock,
|
|
65
|
+
birthOperations: born.operations.length,
|
|
66
|
+
held,
|
|
67
|
+
packages: { "foga-probe": held["foga-probe"].packageHash, "finch-extract-verify": held["finch-extract-verify"].packageHash },
|
|
68
|
+
configPath: o.configPath,
|
|
69
|
+
storePath: o.storePath,
|
|
70
|
+
};
|
|
71
|
+
}
|