@finchip.ai/foga 0.0.0-stage → 0.6.0-alpha.4

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 (131) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +216 -2
  4. package/artifacts/bundle.json +74250 -0
  5. package/artifacts/bundle.sha256 +1 -0
  6. package/bin/foga.js +14 -0
  7. package/build/known-interface-ids.json +18 -0
  8. package/build/profiles.json +120 -0
  9. package/dist/build/bundle.js +417 -0
  10. package/dist/build/vendor.js +74 -0
  11. package/dist/bundle/load.js +24 -0
  12. package/dist/bundle/types.js +1 -0
  13. package/dist/cli-cell.js +395 -0
  14. package/dist/cli.js +505 -0
  15. package/dist/core/acdf.js +162 -0
  16. package/dist/core/adrf.js +273 -0
  17. package/dist/core/aid.js +83 -0
  18. package/dist/core/ard.js +146 -0
  19. package/dist/core/asf.js +157 -0
  20. package/dist/core/canonical.js +79 -0
  21. package/dist/core/erc8004.js +52 -0
  22. package/dist/core/kya.js +225 -0
  23. package/dist/core/measure.js +96 -0
  24. package/dist/core/pack/cbor.js +130 -0
  25. package/dist/core/pack/skillpack.js +142 -0
  26. package/dist/core/skills.js +40 -0
  27. package/dist/core/swarm.js +206 -0
  28. package/dist/core/tenders.js +121 -0
  29. package/dist/core/units.js +122 -0
  30. package/dist/core/world.js +104 -0
  31. package/dist/demo/demander.js +76 -0
  32. package/dist/demo/evidence.js +1 -0
  33. package/dist/demo/loop-d.js +449 -0
  34. package/dist/doctor/checks.js +51 -0
  35. package/dist/doctor/doctor.js +385 -0
  36. package/dist/doctor/loop-d.js +103 -0
  37. package/dist/doctor/probe.js +97 -0
  38. package/dist/doctor/semantic.js +83 -0
  39. package/dist/genesis/extract.js +64 -0
  40. package/dist/genesis/funding.js +126 -0
  41. package/dist/genesis/operator-scope.js +90 -0
  42. package/dist/genesis/preflight.js +54 -0
  43. package/dist/genesis/probe.js +164 -0
  44. package/dist/genesis/roles.js +96 -0
  45. package/dist/genesis/run.js +71 -0
  46. package/dist/genesis/scenario.js +765 -0
  47. package/dist/genesis/skill-runner.js +103 -0
  48. package/dist/genesis/skill-worker.js +41 -0
  49. package/dist/genesis/t1-zk.js +62 -0
  50. package/dist/genesis/t3-zk.js +105 -0
  51. package/dist/genesis/t4.js +259 -0
  52. package/dist/genesis/t5.js +168 -0
  53. package/dist/genesis/t6.js +222 -0
  54. package/dist/genesis/t7.js +198 -0
  55. package/dist/genesis/t8.js +189 -0
  56. package/dist/genesis/t9.js +188 -0
  57. package/dist/genesis/zk.js +128 -0
  58. package/dist/http/server.js +276 -0
  59. package/dist/kernel/cell.js +104 -0
  60. package/dist/kernel/errors.js +78 -0
  61. package/dist/kernel/kernel.js +251 -0
  62. package/dist/kernel/provider.js +125 -0
  63. package/dist/kernel/store.js +470 -0
  64. package/dist/kernel/tx.js +385 -0
  65. package/dist/mcp/server.js +170 -0
  66. package/dist/node/config.js +195 -0
  67. package/dist/node/init.js +200 -0
  68. package/dist/node/keys.js +95 -0
  69. package/dist/node/migrate.js +173 -0
  70. package/dist/node/model/anthropic.js +97 -0
  71. package/dist/node/model/stub.js +129 -0
  72. package/dist/node/model/types.js +9 -0
  73. package/dist/node/node.js +198 -0
  74. package/dist/node/policy.js +95 -0
  75. package/dist/node/runner.js +256 -0
  76. package/dist/node/watchers.js +88 -0
  77. package/dist/tools/admin.js +184 -0
  78. package/dist/tools/catalog.js +261 -0
  79. package/dist/tools/common.js +143 -0
  80. package/dist/tools/identity.js +107 -0
  81. package/dist/tools/index.js +14 -0
  82. package/dist/tools/read.js +471 -0
  83. package/dist/tools/tenders.js +400 -0
  84. package/dist/tools/types.js +4 -0
  85. package/dist/util/bytecode.js +123 -0
  86. package/dist/util/fs.js +58 -0
  87. package/dist/util/hash.js +4 -0
  88. package/dist/util/log.js +11 -0
  89. package/dist/util/version.js +12 -0
  90. package/dist/world/deploy.js +277 -0
  91. package/dist/world/plan.js +93 -0
  92. package/dist/world/types.js +1 -0
  93. package/docs/build.md +45 -0
  94. package/docs/cell-account.md +58 -0
  95. package/docs/compatibility-matrix.md +38 -0
  96. package/docs/design-memo.md +113 -0
  97. package/docs/doctor.md +53 -0
  98. package/docs/examples/README.md +11 -0
  99. package/docs/examples/claude-code-mcp.json +13 -0
  100. package/docs/examples/claude_desktop_config.json +12 -0
  101. package/docs/examples/cursor-mcp.json +11 -0
  102. package/docs/genesis.md +312 -0
  103. package/docs/kernel.md +101 -0
  104. package/docs/loop-d.md +90 -0
  105. package/docs/mcp.md +105 -0
  106. package/docs/node.md +66 -0
  107. package/docs/version-lock.md +67 -0
  108. package/docs/world.md +50 -0
  109. package/docs/zk.md +48 -0
  110. package/package.json +82 -4
  111. package/prover/README.md +26 -0
  112. package/prover/build-predicate.mjs +77 -0
  113. package/prover/package-lock.json +1837 -0
  114. package/prover/package.json +15 -0
  115. package/prover/run.cjs +199 -0
  116. package/scenarios/fixtures/sample-filing.txt +16 -0
  117. package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
  118. package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
  119. package/scenarios/skills/finch-extract-verify/fields.json +56 -0
  120. package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
  121. package/scenarios/skills/foga-probe/LICENSE +2 -0
  122. package/scenarios/skills/foga-probe/SKILL.md +35 -0
  123. package/scenarios/skills/foga-probe/manifest.json +16 -0
  124. package/scenarios/skills/foga-probe/probe.schema.json +40 -0
  125. package/server.json +79 -0
  126. package/vendor/LOCK.json +107 -0
  127. package/vendor/README.md +13 -0
  128. package/worlds/local.json +34 -0
  129. package/worlds/sepolia.json +314 -0
  130. package/zk/build/verification_key.json +174 -0
  131. package/zk/circuits/kya_predicate_count_v1.circom +231 -0
package/dist/cli.js ADDED
@@ -0,0 +1,505 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import { closeSync, mkdirSync, openSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { createConnection } from "node:net";
5
+ import { cpus, homedir, loadavg } from "node:os";
6
+ import { dirname, join, relative, resolve } from "node:path";
7
+ import { buildBundle } from "./build/bundle.js";
8
+ import { syncVendor, verifyVendor } from "./build/vendor.js";
9
+ import { runDoctor } from "./doctor/doctor.js";
10
+ import { ensureDir, exists, readJson, repoRoot, writeJson } from "./util/fs.js";
11
+ import { fail, log } from "./util/log.js";
12
+ import { attachWorld, deployWorld, loadWorldConfig, missingPlanSteps } from "./world/deploy.js";
13
+ import { loadBundle } from "./bundle/load.js";
14
+ const USAGE = `foga — Finch OG Agent toolchain (Phase 1 world layer; Phase 2 cells and the genesis T0-T9, local and public chains; Phase 3 a cell as a node)
15
+
16
+ usage:
17
+ foga vendor sync [--only <repo,repo>] fetch the locked upstream sources into vendor/
18
+ foga vendor verify check vendor/ against vendor/LOCK.json
19
+ foga build [--verify] [--solc-dir <dir>] compile the ten reference implementations into artifacts/bundle.json
20
+ foga world deploy --world <config.json> [--rpc <url>] [--out <file>] [--resume] [--gas-multiplier <x>] [--allow-chain-mismatch]
21
+ deploy-mode config: the whole stack; attach-mode config: attach what it
22
+ lists and deploy the reference contracts it does not (deployer = W)
23
+ foga world attach --world <config.json> [--out <file>] address book only, no transactions (every contract listed)
24
+ foga keys new [--out <file>] write a fresh 12-word phrase (default ~/.foga/genesis.mnemonic) and print the role addresses
25
+ foga keys show print the role addresses the environment derives (FOGA_MNEMONIC[_FILE], FOGA_KEY_*)
26
+ foga keys export --role <role> [--out <file>] write one role's key to a 0600 file for a node that holds that key alone
27
+ (default ~/.foga/keys/<role>.key; the node reads it from FOGA_OPERATOR_KEY_FILE)
28
+ foga keys check check the current environment as a node would: its operator key alone, nothing that derives other roles
29
+ foga doctor --world <world.json> [--rpc <url>] [--out <report.json>] [--genesis <evidence.json>]
30
+ foga doctor --world <config.json> --attach [--rpc <url>] [--out <report.json>]
31
+ foga doctor --world <...> --export-probe <probe.json> write the JSON-RPC probe plan instead of calling an RPC
32
+ foga doctor --world <...> --import-results <results.json> evaluate results collected by a relay (offline replay)
33
+ foga doctor [--world <world.json>] --loop-d <evidence.json> [--compare <evidence2.json>] [--out <report.json>]
34
+ the Loop D verdicts from a run's evidence (beside layer 5 when --world is given; alone otherwise)
35
+ foga probe relay-script print a dependency-free node script that runs a probe plan
36
+ foga genesis preflight --world <world.json> [--rpc <url>] check RPC, keys, balances against the floors, escrow admin, proving material; sends nothing
37
+ foga genesis run --world <world.json> [--rpc <url>] [--until T0..T9] [--store <file.sqlite>] [--out <evidence.json>]
38
+ [--zk auto|require|skip] [--crash-after <opKey>] [--crash-before <opKey>]
39
+ run the genesis scenario (idempotent; re-run to resume)
40
+ foga genesis status --world <world.json> [--store <file.sqlite>] list the operations the store holds
41
+ 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 embedded node with a model,
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
+ Loop D on a world that exists (FOGA_FUNDER_KEY funds the cell's roles on a non-local chain)
47
+ 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
+ the independent demander of Loop D (FOGA_DEMANDER_KEY; anvil's first account by default)
49
+ foga bundle info summarize the committed bundle
50
+ foga cell … a cell as a node: birth, migration, and every capability of the catalog as a subcommand (foga cell help)
51
+ foga node tick | watch | mcp | serve | run | key new --config <cell.config.json> a running node: the watchers, MCP over stdio, the HTTP server, the embedded runner, API keys
52
+ foga mcp --config <cell.config.json> = foga node mcp (what an MCP client's command line runs)
53
+
54
+ environment:
55
+ FOGA_SOLC_DIR directory with solc-<version> static binaries (offline builds; otherwise forge downloads solc)
56
+ FOGA_FOUNDRY_DIR directory holding anvil (scripts/install-foundry.sh: the static Foundry build for older Linux)
57
+ FOGA_RPC_URL default RPC for doctor and genesis (world configs may name their own variable, e.g. SEPOLIA_RPC_URL)
58
+ FOGA_MNEMONIC the 12-word phrase the twelve genesis roles are derived from on non-anvil chains (role i at m/44'/60'/0'/0/i;
59
+ W = account 0 is also the world deployer); FOGA_MNEMONIC_FILE names a file holding it instead
60
+ FOGA_KEY_<ROLE> overrides one role's key (W, G, OWNER_A, OPERATOR_A, ..., E, J1, J2, J3); anvil derives all of them
61
+ FOGA_DEPLOYER_PK deployer key for world deploy when it is not W of the phrase (worlds/local.json falls back to anvil account #0)
62
+ FOGA_OPERATOR_KEY_FILE a node's one key (its cell's operator), written by 'foga keys export'; FOGA_OPERATOR_KEY holds the key itself.
63
+ A node refuses to start when its environment also carries FOGA_MNEMONIC[_FILE], FOGA_DEPLOYER_PK or any FOGA_KEY_<ROLE>
64
+ `;
65
+ function parseArgs(argv) {
66
+ const positional = [];
67
+ const flags = {};
68
+ for (let i = 0; i < argv.length; i++) {
69
+ const a = argv[i];
70
+ if (a.startsWith("--")) {
71
+ const key = a.slice(2);
72
+ const next = argv[i + 1];
73
+ if (next !== undefined && !next.startsWith("--")) {
74
+ flags[key] = next;
75
+ i++;
76
+ }
77
+ else
78
+ flags[key] = true;
79
+ }
80
+ else
81
+ positional.push(a);
82
+ }
83
+ return { positional, flags };
84
+ }
85
+ function str(flags, key) {
86
+ const v = flags[key];
87
+ return typeof v === "string" ? v : undefined;
88
+ }
89
+ async function main() {
90
+ const { positional, flags } = parseArgs(process.argv.slice(2));
91
+ const [group, cmd] = positional;
92
+ const root = repoRoot();
93
+ if (!group || flags.help || group === "help") {
94
+ console.log(USAGE);
95
+ return;
96
+ }
97
+ if (group === "cell") {
98
+ const { cellCommand } = await import("./cli-cell.js");
99
+ await cellCommand(cmd, positional.slice(2), flags);
100
+ return;
101
+ }
102
+ if (group === "node") {
103
+ const { nodeCommand } = await import("./cli-cell.js");
104
+ await nodeCommand(cmd, positional.slice(2), flags);
105
+ return;
106
+ }
107
+ if (group === "mcp") {
108
+ // `npx -y @finchip.ai/foga mcp --config <cell.config.json>`: the stdio MCP server
109
+ const { nodeCommand } = await import("./cli-cell.js");
110
+ await nodeCommand("mcp", [], flags);
111
+ return;
112
+ }
113
+ if (group === "vendor" && cmd === "sync") {
114
+ const only = str(flags, "only")?.split(",");
115
+ syncVendor({ only });
116
+ log.info("vendor tree matches vendor/LOCK.json");
117
+ return;
118
+ }
119
+ if (group === "vendor" && cmd === "verify") {
120
+ const problems = verifyVendor();
121
+ if (problems.length)
122
+ fail("vendor tree problems:\n " + problems.join("\n "));
123
+ log.info("vendor tree matches vendor/LOCK.json");
124
+ return;
125
+ }
126
+ if (group === "build") {
127
+ const res = await buildBundle({ verify: Boolean(flags.verify), solcDir: str(flags, "solc-dir"), only: str(flags, "only")?.split(",") });
128
+ if (flags.verify && !res.verified)
129
+ process.exit(2);
130
+ return;
131
+ }
132
+ if (group === "bundle" && cmd === "info") {
133
+ const b = loadBundle(str(flags, "bundle"));
134
+ console.log(`bundle ${b.hash}\nbuilt ${b.builtAt} with forge ${b.builtWith.forge}; solc ${Object.values(b.builtWith.solc).join(", ")}`);
135
+ for (const s of b.sources) {
136
+ const n = Object.values(b.contracts).filter((c) => c.repo === s.repo);
137
+ const dep = n.filter((c) => c.deployable).length;
138
+ console.log(` ${String(s.erc).padEnd(5)} ${s.repo.padEnd(24)} ${(s.commit ?? "local").slice(0, 7)} ${String(n.length).padStart(3)} targets, ${String(dep).padStart(3)} deployable (${s.mode}, solc ${s.solc})`);
139
+ }
140
+ for (const [repo, ids] of Object.entries(b.interfaceIds)) {
141
+ const mismatched = Object.entries(ids).filter(([, r]) => r.match === false);
142
+ if (mismatched.length)
143
+ console.log(` warning: ${repo} interface id mismatches: ${mismatched.map(([n, r]) => `${n} computed ${r.computed} upstream ${r.upstream}`).join("; ")}`);
144
+ }
145
+ return;
146
+ }
147
+ if (group === "world" && (cmd === "deploy" || cmd === "attach")) {
148
+ const worldConfigPath = str(flags, "world");
149
+ if (!worldConfigPath)
150
+ fail("--world <config.json> is required");
151
+ const cfg = loadWorldConfig(resolve(worldConfigPath));
152
+ const outPath = resolve(str(flags, "out") ?? join(root, "deployments", `${cfg.name}.json`));
153
+ if (cmd === "attach") {
154
+ if (cfg.mode !== "attach")
155
+ fail(`${worldConfigPath} is a deploy-mode config; use foga world deploy`);
156
+ const bundle = exists(join(root, "artifacts", "bundle.json")) ? loadBundle() : null;
157
+ const missing = missingPlanSteps(cfg);
158
+ if (missing.length)
159
+ log.warn(`the config does not list ${missing.length} reference contracts (${missing.join(", ")}); this address book is incomplete for a genesis — foga world deploy adds them from the bundle`);
160
+ const world = attachWorld(cfg, bundle);
161
+ writeJson(outPath, world);
162
+ log.info(`attached world ${world.worldId}: ${Object.keys(world.contracts).length} contracts -> ${outPath}`);
163
+ return;
164
+ }
165
+ const gm = str(flags, "gas-multiplier");
166
+ await deployWorld({
167
+ worldConfigPath: resolve(worldConfigPath),
168
+ rpc: str(flags, "rpc"),
169
+ outPath,
170
+ resume: Boolean(flags.resume),
171
+ gasMultiplier: gm ? Number(gm) : undefined,
172
+ allowChainMismatch: Boolean(flags["allow-chain-mismatch"]),
173
+ });
174
+ return;
175
+ }
176
+ if (group === "doctor") {
177
+ let worldPath = str(flags, "world");
178
+ if (!worldPath && str(flags, "loop-d")) {
179
+ const { runLoopDDoctor } = await import("./doctor/doctor.js");
180
+ const report = runLoopDDoctor({ evidencePath: resolve(str(flags, "loop-d")), comparePath: str(flags, "compare") ? resolve(str(flags, "compare")) : undefined, outPath: str(flags, "out") ? resolve(str(flags, "out")) : undefined });
181
+ if (report.summary.Fail > 0)
182
+ process.exitCode = 3;
183
+ return;
184
+ }
185
+ if (!worldPath)
186
+ fail("--world <world.json> is required (or --loop-d <evidence.json> alone)");
187
+ worldPath = resolve(worldPath);
188
+ if (flags.attach) {
189
+ const cfg = loadWorldConfig(worldPath);
190
+ const bundle = loadBundle();
191
+ const world = attachWorld(cfg, bundle);
192
+ const tmp = join(root, ".foga-build", `${cfg.name}.attached.json`);
193
+ writeJson(tmp, world);
194
+ worldPath = tmp;
195
+ }
196
+ const { report } = await runDoctor({
197
+ worldPath,
198
+ rpc: str(flags, "rpc"),
199
+ outPath: str(flags, "out") ? resolve(str(flags, "out")) : undefined,
200
+ exportProbePath: str(flags, "export-probe") ? resolve(str(flags, "export-probe")) : undefined,
201
+ importResultsPath: str(flags, "import-results") ? resolve(str(flags, "import-results")) : undefined,
202
+ genesisPath: str(flags, "genesis") ? resolve(str(flags, "genesis")) : undefined,
203
+ loopDPath: str(flags, "loop-d") ? resolve(str(flags, "loop-d")) : undefined,
204
+ loopDComparePath: str(flags, "compare") ? resolve(str(flags, "compare")) : undefined,
205
+ });
206
+ if (report && report.summary.Fail > 0)
207
+ process.exitCode = 3;
208
+ return;
209
+ }
210
+ if (group === "probe" && cmd === "relay-script") {
211
+ const { RELAY_SCRIPT } = await import("./doctor/probe.js");
212
+ process.stdout.write(RELAY_SCRIPT);
213
+ return;
214
+ }
215
+ if (group === "keys" && cmd === "export") {
216
+ const { ROLE_ORDER, envKeys, roleAddresses } = await import("./genesis/roles.js");
217
+ const role = str(flags, "role");
218
+ if (!role || !ROLE_ORDER.includes(role))
219
+ fail(`--role <role> is required: one of ${ROLE_ORDER.join(", ")}`);
220
+ const out = resolve(str(flags, "out") ?? join(homedir(), ".foga", "keys", `${role}.key`));
221
+ if (exists(out))
222
+ fail(`${out} exists; pass --out <file> for another file (an existing key file is never overwritten)`);
223
+ const keys = envKeys([role]);
224
+ mkdirSync(dirname(out), { recursive: true, mode: 0o700 });
225
+ writeFileSync(out, keys[role] + "\n", { mode: 0o600 });
226
+ const address = roleAddresses(keys)[role];
227
+ console.log(`${role} ${address}`);
228
+ console.log(`key written to ${out} (mode 0600). Move it to the node's user (chown) and keep the phrase out of that user's environment:`);
229
+ console.log(` export FOGA_OPERATOR_KEY_FILE=${out}`);
230
+ return;
231
+ }
232
+ if (group === "keys" && cmd === "check") {
233
+ const { loadOperatorKey, KeyIsolationError } = await import("./node/keys.js");
234
+ try {
235
+ const key = loadOperatorKey();
236
+ console.log(`operator key: ${key.address} (from ${key.source.kind === "env" ? key.source.name : key.source.path})`);
237
+ console.log("the environment holds nothing that derives another role");
238
+ }
239
+ catch (e) {
240
+ if (e instanceof KeyIsolationError) {
241
+ console.log(`not a node environment: ${e.message}`);
242
+ process.exitCode = 6;
243
+ return;
244
+ }
245
+ throw e;
246
+ }
247
+ return;
248
+ }
249
+ if (group === "keys" && (cmd === "new" || cmd === "show")) {
250
+ const { ROLE_ORDER, envKeys, newMnemonic, roleAddresses, mnemonicKeys } = await import("./genesis/roles.js");
251
+ let keys;
252
+ if (cmd === "new") {
253
+ const out = resolve(str(flags, "out") ?? join(homedir(), ".foga", "genesis.mnemonic"));
254
+ if (exists(out))
255
+ fail(`${out} exists; pass --out <file> for another phrase (an existing phrase is never overwritten)`);
256
+ const phrase = newMnemonic();
257
+ mkdirSync(dirname(out), { recursive: true, mode: 0o700 });
258
+ writeFileSync(out, phrase + "\n", { mode: 0o600 });
259
+ keys = mnemonicKeys(phrase);
260
+ console.log(`new phrase written to ${out} (mode 0600). Keep it out of the repository; anyone holding it holds every role.`);
261
+ console.log(` export FOGA_MNEMONIC_FILE=${out}`);
262
+ }
263
+ else {
264
+ keys = envKeys(ROLE_ORDER);
265
+ }
266
+ const addresses = roleAddresses(keys);
267
+ console.log("roles (W is the world deployer and the treasury: fund W, the genesis funds the rest):");
268
+ for (const role of ROLE_ORDER)
269
+ console.log(` ${role.padEnd(11)} ${addresses[role]}${role === "W" ? " <- fund this address" : ""}`);
270
+ return;
271
+ }
272
+ if (group === "genesis" && cmd === "preflight") {
273
+ const worldPath = str(flags, "world");
274
+ if (!worldPath)
275
+ fail("--world <world.json> is required (a deployed world)");
276
+ const world = readJson(resolve(worldPath));
277
+ const rpc = str(flags, "rpc") ?? process.env.FOGA_RPC_URL ?? (world.network === "anvil" ? "http://127.0.0.1:8545" : undefined);
278
+ if (!rpc)
279
+ fail("no RPC: pass --rpc or set FOGA_RPC_URL");
280
+ const { preflight } = await import("./genesis/preflight.js");
281
+ const problems = await preflight({ worldPath: resolve(worldPath), rpc, log: (line) => console.log(line) });
282
+ if (problems.length) {
283
+ console.log("\nproblems:");
284
+ for (const p of problems)
285
+ console.log(` - ${p}`);
286
+ process.exitCode = 5;
287
+ }
288
+ else
289
+ console.log("\npreflight: ready");
290
+ return;
291
+ }
292
+ if (group === "genesis" && cmd === "run") {
293
+ const worldPath = str(flags, "world");
294
+ if (!worldPath)
295
+ fail("--world <world.json> is required (a deployed world)");
296
+ const { runGenesis } = await import("./genesis/run.js");
297
+ const { failed } = await runGenesis({
298
+ worldPath,
299
+ rpc: str(flags, "rpc"),
300
+ storePath: str(flags, "store"),
301
+ evidencePath: str(flags, "out"),
302
+ skillsDir: str(flags, "skills"),
303
+ until: str(flags, "until"),
304
+ zk: str(flags, "zk"),
305
+ crashAfterBroadcast: str(flags, "crash-after"),
306
+ crashBeforeBroadcast: str(flags, "crash-before"),
307
+ });
308
+ if (failed > 0)
309
+ process.exitCode = 4;
310
+ return;
311
+ }
312
+ if (group === "genesis" && cmd === "status") {
313
+ const worldPath = str(flags, "world");
314
+ if (!worldPath)
315
+ fail("--world <world.json> is required");
316
+ const { OperationStore } = await import("./kernel/store.js");
317
+ const name = worldPath.replace(/^.*\//, "").replace(/\.json$/, "");
318
+ const storePath = resolve(str(flags, "store") ?? join(root, "deployments", `${name}.genesis.sqlite`));
319
+ if (!exists(storePath))
320
+ fail(`no operation store at ${storePath}`);
321
+ const store = OperationStore.open(storePath);
322
+ const ops = store.all();
323
+ console.log(`store ${storePath}: ${ops.length} operations (chain ${store.getMeta("chainId")}, world ${store.getMeta("worldId")})`);
324
+ for (const op of ops)
325
+ console.log(` ${op.status.padEnd(9)} ${op.key.padEnd(36)} ${op.signer.padEnd(11)} nonce ${String(op.nonce ?? "-").padStart(3)} ${op.txHash ?? ""}${op.error ? " error: " + op.error : ""}`);
326
+ const s = store.summary();
327
+ console.log(` facts ${s.facts}, journal entries ${s.journal}`);
328
+ store.close();
329
+ return;
330
+ }
331
+ if (group === "demo" && cmd === "demander") {
332
+ const { mintCases } = await import("./demo/demander.js");
333
+ const { anvilKeys } = await import("./genesis/roles.js");
334
+ const need = (k) => str(flags, k) ?? fail(`--${k} is required`);
335
+ const worldPath = resolve(need("world"));
336
+ const worldFile = readJson(worldPath);
337
+ const key = process.env.FOGA_DEMANDER_KEY ?? (worldFile.network === "anvil" ? anvilKeys().W : undefined);
338
+ if (!key)
339
+ fail("FOGA_DEMANDER_KEY is required on a non-local world");
340
+ const cases = (str(flags, "cases") ?? "a,b,c,d,d2,e").split(",").map((c) => c.trim());
341
+ const result = await mintCases({ worldPath, rpc: need("rpc"), storePath: resolve(need("store")), key, cell: need("cell"), schemeId: need("scheme"), issuers: need("issuer").split(","), packages: { "foga-probe": need("probe-hash"), "finch-extract-verify": need("extract-hash") }, cases, seed: Number(str(flags, "seed") ?? 7), log: (line) => console.error(line) });
342
+ const out = str(flags, "out");
343
+ if (out)
344
+ writeJson(resolve(out), result);
345
+ else
346
+ console.log(JSON.stringify(result, null, 2));
347
+ return;
348
+ }
349
+ if (group === "loop-d" && cmd === "run") {
350
+ const { runLoopD, defaultLoopDDir } = await import("./demo/loop-d.js");
351
+ const worldPath = str(flags, "world") ?? fail("--world <world.json> is required");
352
+ const rpc = str(flags, "rpc") ?? process.env.FOGA_RPC_URL ?? fail("--rpc <url> is required");
353
+ const model = (str(flags, "model") ?? "stub");
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 });
357
+ return;
358
+ }
359
+ if (group === "demo") {
360
+ const model = (str(flags, "model") ?? "stub");
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 });
364
+ return;
365
+ }
366
+ console.log(USAGE);
367
+ fail(`unknown command: ${positional.join(" ")}`);
368
+ }
369
+ async function demo(opts) {
370
+ const root = repoRoot();
371
+ const rpc = `http://127.0.0.1:${opts.port}`;
372
+ // Say what is wrong before starting anything: a port that is already taken makes anvil exit at once, and a
373
+ // silent exit would otherwise surface only as "the RPC did not come up".
374
+ if (await portInUse(opts.port)) {
375
+ const answers = await rpcAnswers(rpc);
376
+ fail(answers
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
+ }
380
+ // A machine that is already saturated (a proving-key build, for instance) starves anvil: blocks come late and the
381
+ // kernel gives up on a receipt after its timeout. Say so before starting rather than after a failure.
382
+ const cores = cpus().length || 1;
383
+ const load = loadavg()[0];
384
+ if (load > cores * 0.8) {
385
+ 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
+ }
387
+ // 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
+ // anvil's output goes to a log file rather than a pipe: with --keep the node survives this process, and a
391
+ // Rust program whose stderr pipe has gone away dies on its next write.
392
+ const logPath = join(root, ".foga-build", "anvil.log");
393
+ ensureDir(join(root, ".foga-build"));
394
+ const logFd = openSync(logPath, "w");
395
+ log.info(`starting anvil on ${rpc}${foundryDir ? ` (${anvilBin})` : ""}; log ${relative(root, logPath)}`);
396
+ const anvil = spawn(anvilBin, ["--port", String(opts.port), "--silent"], { stdio: ["ignore", logFd, logFd], detached: opts.keep });
397
+ closeSync(logFd);
398
+ let exited = null;
399
+ anvil.on("exit", (code, signal) => {
400
+ exited = code !== null ? `exit code ${code}` : `signal ${signal}`;
401
+ });
402
+ const stop = () => {
403
+ if (!opts.keep && !anvil.killed)
404
+ anvil.kill("SIGTERM");
405
+ };
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
+ process.on("exit", stop);
412
+ process.on("SIGINT", () => {
413
+ stop();
414
+ process.exit(130);
415
+ });
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
+ const worldConfig = join(root, "worlds", "local.json");
434
+ const out = join(root, "deployments", "local.json");
435
+ await deployWorld({ worldConfigPath: worldConfig, rpc, outPath: out });
436
+ const { report } = await runDoctor({ worldPath: out, rpc, outPath: join(root, "deployments", "local.doctor.json") });
437
+ if (report && report.summary.Fail > 0)
438
+ process.exitCode = 3;
439
+ if (opts.genesis) {
440
+ const { runGenesis } = await import("./genesis/run.js");
441
+ const { rmSync } = await import("node:fs");
442
+ const storePath = join(root, "deployments", "local.genesis.sqlite");
443
+ for (const suffix of ["", "-wal", "-shm"])
444
+ rmSync(storePath + suffix, { force: true }); // a fresh chain means a fresh store
445
+ const evidencePath = join(root, "deployments", "local.genesis.json");
446
+ const { failed } = await runGenesis({ worldPath: out, rpc, storePath, evidencePath, until: opts.genesis });
447
+ if (failed > 0)
448
+ process.exitCode = 4;
449
+ // the doctor's fifth layer: what the genesis established, per standard
450
+ const second = await runDoctor({ worldPath: out, rpc, outPath: join(root, "deployments", "local.doctor.json"), genesisPath: evidencePath, quiet: true });
451
+ if (second.report && second.report.summary.Fail > 0)
452
+ process.exitCode = 3;
453
+ }
454
+ if (opts.loopD) {
455
+ const { runLoopD, defaultLoopDDir } = await import("./demo/loop-d.js");
456
+ const evidencePath = join(root, "deployments", "loop-d.local.json");
457
+ await runLoopD({ rpc, worldPath: out, dir: defaultLoopDDir(), evidencePath, model: opts.loopD.model, modelName: opts.loopD.modelName, seed: opts.loopD.seed, recovery: opts.loopD.recovery });
458
+ 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
+ if (judged.report && judged.report.summary.Fail > 0)
460
+ process.exitCode = 3;
461
+ }
462
+ if (opts.keep) {
463
+ // Let this process return while anvil, started detached, keeps serving.
464
+ anvil.unref();
465
+ log.info(`anvil left running on ${rpc} (pid ${anvil.pid}, log ${relative(root, logPath)}); world at deployments/local.json. Stop it with: kill ${anvil.pid}`);
466
+ }
467
+ else
468
+ stop();
469
+ }
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
+ main().catch((e) => {
504
+ fail(e?.stack ?? String(e));
505
+ });