@finchip.ai/foga 0.6.0-alpha.5 → 0.6.0-alpha.7
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 +41 -15
- package/dist/a2a/card.js +54 -0
- package/dist/a2a/server.js +276 -0
- package/dist/a2a/types.js +20 -0
- package/dist/build/bundle.js +2 -1
- package/dist/cli-cell.js +147 -16
- package/dist/cli.js +192 -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 +365 -0
- package/dist/doctor/loop-d.js +29 -7
- package/dist/genesis/operator-scope.js +1 -0
- package/dist/http/access.js +105 -0
- package/dist/http/server.js +65 -43
- package/dist/http/signed.js +136 -0
- package/dist/mcp/server.js +27 -1
- package/dist/node/config.js +34 -1
- package/dist/node/documents.js +298 -0
- package/dist/node/preflight.js +208 -0
- package/dist/node/wallet.js +118 -0
- package/dist/tools/documents.js +74 -0
- package/dist/tools/index.js +2 -1
- package/dist/world/deploy.js +13 -12
- package/dist/world/sourcify.js +165 -0
- package/docs/a2a.md +31 -0
- package/docs/design-memo.md +18 -5
- package/docs/documents.md +25 -0
- package/docs/examples/README.md +5 -1
- package/docs/loop-d.md +22 -4
- package/docs/mcp.md +18 -13
- package/docs/node.md +28 -10
- package/docs/quickstart.md +87 -0
- package/docs/wallet.md +60 -0
- package/docs/world.md +12 -0
- package/package.json +3 -1
- package/scripts/prepare.mjs +21 -0
- package/server.json +2 -2
package/dist/cli-cell.js
CHANGED
|
@@ -6,6 +6,7 @@ import { cellInit } from "./node/init.js";
|
|
|
6
6
|
import { KeyIsolationError, forbiddenVariables, loadOperatorKey, loadRoleKey } from "./node/keys.js";
|
|
7
7
|
import { cellMigrate } from "./node/migrate.js";
|
|
8
8
|
import { Node } from "./node/node.js";
|
|
9
|
+
import { loadWallet, walletKey } from "./node/wallet.js";
|
|
9
10
|
import { Watchers } from "./node/watchers.js";
|
|
10
11
|
import { defaultCatalog, OPERATOR_CALLER, PUBLIC_CALLER } from "./tools/index.js";
|
|
11
12
|
import { exists, repoRoot } from "./util/fs.js";
|
|
@@ -20,23 +21,37 @@ import { fail } from "./util/log.js";
|
|
|
20
21
|
export const CELL_USAGE = `
|
|
21
22
|
foga cell list [--public] [--as-json] the capabilities (action, visibility, principal, modes) as the operator or as the public sees them
|
|
22
23
|
foga cell schema <capability> the input and output JSON Schema of a capability
|
|
23
|
-
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --
|
|
24
|
-
foga cell init --config <cell.config.json> [--rpc <url>] [--capital-wei <n>]
|
|
25
|
-
the cell's birth (the owner's key: FOGA_OWNER_KEY[_FILE]); writes the account, agent id, binding, record and unit into the config
|
|
24
|
+
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --out <cell.config.json> (--wallet <dir> | --owner <addr> --operator <addr> --guardian <addr>) [--rpc <url>] [--skills <dir>] [--input-dir <dir>]
|
|
25
|
+
foga cell init --config <cell.config.json> [--wallet <dir>] [--rpc <url>] [--capital-wei <n>]
|
|
26
|
+
the cell's birth (the owner's key: the wallet's, or FOGA_OWNER_KEY[_FILE]); writes the account, agent id, binding, record and unit into the config
|
|
26
27
|
foga cell migrate --config <cell.config.json> --to <new.config.json> [--store <new ops.sqlite>] [--operator <addr>] [--capital-wei <n>] [--no-sweep]
|
|
27
28
|
move the cell to a new revision-2 account (the owner's key); the new config and store are written beside
|
|
28
29
|
foga cell status --config <cell.config.json> cell_status as the operator
|
|
29
|
-
foga cell
|
|
30
|
-
|
|
30
|
+
foga cell docs build --config <cell.config.json> [--rpc <url>] [--out <dir>]
|
|
31
|
+
the cell's documents from its configuration and the chain (registration, AID document, ARD descriptor, AUF unit,
|
|
32
|
+
agent card, server.json, manifest) under runtime.http.documentsDir — what the node serves at /documents and /.well-known
|
|
33
|
+
foga cell docs publish --config <cell.config.json> [--wallet <dir>] [--rpc <url>]
|
|
34
|
+
build, then commit on chain what differs (setAgentURI, setDocumentURI, updateDescriptor, publishVersion; the operator
|
|
35
|
+
key, one keyed job), then read every commitment back
|
|
36
|
+
foga cell docs status --config <cell.config.json> [--rpc <url>] what is built, what the chain holds, what a publish would commit
|
|
37
|
+
foga cell <capability> --config <cell.config.json> [--wallet <dir>] [--mode simulate|build|act] [--json '<input json>' | --json-file <f>] [--<field> <value> ...]
|
|
38
|
+
any capability; an act loads the operator key, an admin capability the owner's or the guardian's — from --wallet, or from
|
|
39
|
+
FOGA_OPERATOR_KEY[_FILE], FOGA_OWNER_KEY[_FILE], FOGA_GUARDIAN_KEY[_FILE]
|
|
31
40
|
[--caller <id> --level read|build|act] call as another caller (what the public or an API-key holder would get)
|
|
32
41
|
foga cell owner <capability> … | foga cell guardian <capability> … the same, said explicitly
|
|
42
|
+
foga node <command> … [--wallet <dir>] every node command takes the operator key from the wallet's operator.key (= FOGA_OPERATOR_KEY_FILE)
|
|
43
|
+
foga node preflight --config <cell.config.json> [--wallet <dir>] [--rpc <url>] [--no-executors] [--no-network] [--out <report.json>] [--strict]
|
|
44
|
+
the readiness reconciliation before the public is served: the account and its allow-list, the operator key and its
|
|
45
|
+
gas, the standing, the skills' packages and executors, the documents against the chain, the public URL, the model;
|
|
46
|
+
advisory (exit 5 on a Fail only with --strict)
|
|
33
47
|
foga node tick --config <cell.config.json> run the watchers once (heartbeat, discovery, settlement, safety) in a node environment (operator key alone)
|
|
34
48
|
foga node watch --config <cell.config.json> [--interval <s>] run the watchers on the poll interval until stopped
|
|
35
49
|
foga node mcp --config <cell.config.json> MCP over stdio for a local client (Claude Desktop, Claude Code, Cursor): the operator's full level with
|
|
36
50
|
FOGA_OPERATOR_KEY[_FILE] in the environment, read and build without it; logs go to stderr
|
|
37
51
|
foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch]
|
|
38
|
-
|
|
39
|
-
a Bearer key acts as its grant says;
|
|
52
|
+
host mode: POST /api/<capability>, /mcp (Streamable HTTP), /a2a (A2A 1.0), /.well-known/agent-card.json, /documents/*,
|
|
53
|
+
/healthz, / — the public reads and builds, a Bearer key or a wallet signature (FOGA-EIP712) acts as its grant says;
|
|
54
|
+
the watchers run beside it; no model
|
|
40
55
|
foga node key new --config <cell.config.json> --id <id> [--level read|build|act] [--visibility public|caller|operator] [--note <text>]
|
|
41
56
|
mint an API key for the HTTP entrance: printed once, kept only as its hash in the config
|
|
42
57
|
foga node run --config <cell.config.json> [--model stub|anthropic] [--listen <host:port> | --no-http] [--interval <s>] [--once | --ticks <n>] [--transcripts <dir>]
|
|
@@ -44,7 +59,7 @@ export const CELL_USAGE = `
|
|
|
44
59
|
judges) on the poll interval; the operator key alone; anthropic needs ANTHROPIC_API_KEY and runtime.model.model
|
|
45
60
|
`;
|
|
46
61
|
const str = (flags, key) => (typeof flags[key] === "string" ? flags[key] : undefined);
|
|
47
|
-
const RESERVED = new Set(["config", "mode", "json", "json-file", "caller", "level", "rpc", "public", "help"]);
|
|
62
|
+
const RESERVED = new Set(["config", "mode", "json", "json-file", "caller", "level", "rpc", "public", "help", "wallet"]);
|
|
48
63
|
function inputFromFlags(flags) {
|
|
49
64
|
let input = {};
|
|
50
65
|
const inline = str(flags, "json");
|
|
@@ -81,7 +96,26 @@ function callerFromFlags(flags) {
|
|
|
81
96
|
fail(`--level must be read, build, act or admin`);
|
|
82
97
|
return { id: id ?? "caller", entrance: "cli", level: lvl, visibility: lvl === "admin" ? "operator" : id ? "caller" : "public" };
|
|
83
98
|
}
|
|
84
|
-
|
|
99
|
+
/** The wallet `--wallet <dir>` names, or null without the flag. */
|
|
100
|
+
function walletFromFlags(flags) {
|
|
101
|
+
const dir = str(flags, "wallet");
|
|
102
|
+
if (!dir)
|
|
103
|
+
return null;
|
|
104
|
+
const w = loadWallet(resolve(dir));
|
|
105
|
+
if (!w)
|
|
106
|
+
fail(`${resolve(dir)} holds no wallet (no wallet.json); foga wallet new --dir ${resolve(dir)} makes one`);
|
|
107
|
+
return w;
|
|
108
|
+
}
|
|
109
|
+
/** A role's key: from the wallet when --wallet is given, from the environment otherwise. */
|
|
110
|
+
function keysFor(role, strict = false, wallet = null) {
|
|
111
|
+
if (wallet) {
|
|
112
|
+
try {
|
|
113
|
+
return walletKey(wallet, role);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
fail(e.message);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
85
119
|
try {
|
|
86
120
|
return (role === "operator" && strict ? loadOperatorKey() : loadRoleKey(role)).privateKey;
|
|
87
121
|
}
|
|
@@ -120,7 +154,8 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
120
154
|
return print({ name: cap.name, title: cap.title, description: cap.description, action: cap.action, visibility: cap.visibility, principal: cap.principal, ercs: cap.ercs, input: z.toJSONSchema(cap.input, { target: "draft-2020-12", io: "input" }), output: z.toJSONSchema(cap.output, { target: "draft-2020-12", io: "output" }) });
|
|
121
155
|
}
|
|
122
156
|
if (cmd === "new") {
|
|
123
|
-
const
|
|
157
|
+
const wallet = walletFromFlags(flags);
|
|
158
|
+
const need = (k) => str(flags, k) ?? (wallet && wallet.addresses[k] ? wallet.addresses[k] : fail(`--${k} is required (or --wallet <dir> with a ${k} key)`));
|
|
124
159
|
const out = resolve(need("out"));
|
|
125
160
|
if (exists(out))
|
|
126
161
|
fail(`${out} exists; choose another --out (a config is never overwritten)`);
|
|
@@ -137,8 +172,9 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
137
172
|
const configPath = str(flags, "config") ?? fail("--config <cell.config.json> is required");
|
|
138
173
|
const rpc = str(flags, "rpc");
|
|
139
174
|
const log = (line) => console.error(line);
|
|
175
|
+
const wallet = walletFromFlags(flags);
|
|
140
176
|
if (cmd === "init") {
|
|
141
|
-
const owner = keysFor("owner");
|
|
177
|
+
const owner = keysFor("owner", false, wallet);
|
|
142
178
|
const node = await Node.open({ configPath, rpc, keys: { owner }, allowUnborn: true, log });
|
|
143
179
|
try {
|
|
144
180
|
const capital = str(flags, "capital-wei");
|
|
@@ -151,7 +187,7 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
151
187
|
return;
|
|
152
188
|
}
|
|
153
189
|
if (cmd === "migrate") {
|
|
154
|
-
const owner = keysFor("owner");
|
|
190
|
+
const owner = keysFor("owner", false, wallet);
|
|
155
191
|
const to = resolve(str(flags, "to") ?? fail("--to <new.config.json> is required"));
|
|
156
192
|
if (exists(to))
|
|
157
193
|
fail(`${to} exists; choose another --to`);
|
|
@@ -166,6 +202,72 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
166
202
|
}
|
|
167
203
|
return;
|
|
168
204
|
}
|
|
205
|
+
if (cmd === "docs") {
|
|
206
|
+
const sub = rest[0];
|
|
207
|
+
const { buildDocuments, readBack, documentsDir, onChainState, readBuilt, publishSteps } = await import("./node/documents.js");
|
|
208
|
+
const { version } = await import("./util/version.js");
|
|
209
|
+
if (sub === "build") {
|
|
210
|
+
const node = await Node.open({ configPath, rpc, keys: {}, log });
|
|
211
|
+
try {
|
|
212
|
+
const built = await buildDocuments(node, { version, dir: str(flags, "out") });
|
|
213
|
+
console.error(`documents v${built.manifest.version} written to ${built.dir}:`);
|
|
214
|
+
for (const [file, d] of Object.entries(built.manifest.documents))
|
|
215
|
+
console.error(` ${file.padEnd(20)} ${d.standard.padEnd(40)} ${d.contentHash}`);
|
|
216
|
+
const { steps, unchanged } = await publishSteps(node, built);
|
|
217
|
+
console.error(steps.length ? `a publish would commit: ${steps.map((x) => x.label).join("; ")}` : "every commitment on chain matches what was built");
|
|
218
|
+
if (unchanged.length)
|
|
219
|
+
console.error(`already as built: ${unchanged.join(", ")}`);
|
|
220
|
+
print(built.manifest);
|
|
221
|
+
}
|
|
222
|
+
finally {
|
|
223
|
+
node.close();
|
|
224
|
+
}
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (sub === "status") {
|
|
228
|
+
const node = await Node.open({ configPath, rpc, keys: {}, log });
|
|
229
|
+
try {
|
|
230
|
+
const dir = documentsDir(node);
|
|
231
|
+
const built = readBuilt(dir);
|
|
232
|
+
const rows = built ? await readBack(node, built) : [];
|
|
233
|
+
print({ dir, manifest: built?.manifest ?? null, onChain: await onChainState(node), readBack: rows, pending: built ? (await publishSteps(node, built)).steps.map((x) => x.label) : [] });
|
|
234
|
+
if (rows.some((r) => r.ok === false))
|
|
235
|
+
process.exitCode = 2;
|
|
236
|
+
}
|
|
237
|
+
finally {
|
|
238
|
+
node.close();
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (sub === "publish") {
|
|
243
|
+
const node = await Node.open({ configPath, rpc, keys: { operator: keysFor("operator", false, wallet) }, log });
|
|
244
|
+
try {
|
|
245
|
+
const result = await catalog.invoke(node, "docs_publish", {}, OPERATOR_CALLER, { mode: "act" });
|
|
246
|
+
if (!result.ok) {
|
|
247
|
+
print(result);
|
|
248
|
+
process.exitCode = 2;
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
const out = result.output;
|
|
252
|
+
console.error(`documents v${out.version} in ${out.dir}: ${out.committed.length ? `committed ${out.committed.join(", ")}` : "nothing to commit"}${out.unchanged.length ? `; already as built: ${out.unchanged.join(", ")}` : ""} (job ${out.jobId}, ${out.operations.length} operation(s))`);
|
|
253
|
+
// the read-back from here fetches the URLs too, when the node serves them
|
|
254
|
+
const built = readBuilt(out.dir);
|
|
255
|
+
const rows = await readBack(node, built, { fetchUrls: true });
|
|
256
|
+
for (const r of rows)
|
|
257
|
+
console.error(` ${r.ok === true ? "ok " : r.ok === false ? "FAIL " : "unread "} ${r.commitment.padEnd(26)} ${r.detail}`);
|
|
258
|
+
print({ ...out, readBack: rows });
|
|
259
|
+
if (rows.some((r) => r.ok === false)) {
|
|
260
|
+
console.error("a commitment read back from the chain does not match what was built: the publish is not complete");
|
|
261
|
+
process.exitCode = 2;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
finally {
|
|
265
|
+
node.close();
|
|
266
|
+
}
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
fail("foga cell docs build | publish | status");
|
|
270
|
+
}
|
|
169
271
|
// a capability, possibly said as "owner <capability>" / "guardian <capability>"
|
|
170
272
|
let name = cmd;
|
|
171
273
|
let explicitRole = null;
|
|
@@ -186,10 +288,10 @@ export async function cellCommand(cmd, rest, flags) {
|
|
|
186
288
|
if (cap.action !== "read" && mode === "act") {
|
|
187
289
|
if (cap.action === "admin") {
|
|
188
290
|
const role = cap.signer ?? explicitRole ?? "owner";
|
|
189
|
-
keys[role] = keysFor(role);
|
|
291
|
+
keys[role] = keysFor(role, false, wallet);
|
|
190
292
|
}
|
|
191
293
|
else
|
|
192
|
-
keys.operator = keysFor("operator");
|
|
294
|
+
keys.operator = keysFor("operator", false, wallet);
|
|
193
295
|
}
|
|
194
296
|
const node = await Node.open({ configPath, rpc, keys, log });
|
|
195
297
|
try {
|
|
@@ -230,6 +332,32 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
230
332
|
}
|
|
231
333
|
const configPath = str(flags, "config") ?? fail("--config <cell.config.json> is required");
|
|
232
334
|
const log = (line) => console.error(line);
|
|
335
|
+
// --wallet <dir>: the operator key file of the wallet, as FOGA_OPERATOR_KEY_FILE would name it; never beside another key source
|
|
336
|
+
const wallet = walletFromFlags(flags);
|
|
337
|
+
if (wallet) {
|
|
338
|
+
if (process.env.FOGA_OPERATOR_KEY || process.env.FOGA_OPERATOR_KEY_FILE)
|
|
339
|
+
fail("--wallet and FOGA_OPERATOR_KEY[_FILE] are both set; a node takes its operator key from one place");
|
|
340
|
+
if (!wallet.files.operator)
|
|
341
|
+
fail(`wallet ${wallet.dir} has no operator key`);
|
|
342
|
+
process.env.FOGA_OPERATOR_KEY_FILE = wallet.files.operator;
|
|
343
|
+
}
|
|
344
|
+
if (cmd === "preflight") {
|
|
345
|
+
const { preflight, printPreflight } = await import("./node/preflight.js");
|
|
346
|
+
const { writeJson } = await import("./util/fs.js");
|
|
347
|
+
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys: {}, log });
|
|
348
|
+
try {
|
|
349
|
+
const report = await preflight(node, { executors: !flags["no-executors"], network: !flags["no-network"] });
|
|
350
|
+
printPreflight(report, (line) => console.log(line));
|
|
351
|
+
if (str(flags, "out"))
|
|
352
|
+
writeJson(resolve(str(flags, "out")), report);
|
|
353
|
+
if (flags.strict && report.summary.Fail > 0)
|
|
354
|
+
process.exitCode = 5;
|
|
355
|
+
}
|
|
356
|
+
finally {
|
|
357
|
+
node.close();
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
233
361
|
if (cmd === "key") {
|
|
234
362
|
if (rest[0] !== "new")
|
|
235
363
|
fail("foga node key new --config <cfg> --id <id> [--level act] [--visibility caller]");
|
|
@@ -281,7 +409,10 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
281
409
|
const { version } = await import("./util/version.js");
|
|
282
410
|
const listen = str(flags, "listen") ?? node.config.runtime.http.listen;
|
|
283
411
|
const running = await createHttpServer({ node, catalog, version, log }).listen(listen);
|
|
284
|
-
|
|
412
|
+
const { documentsDir, readBuilt } = await import("./node/documents.js");
|
|
413
|
+
const built = readBuilt(documentsDir(node));
|
|
414
|
+
const signers = node.config.access.signers;
|
|
415
|
+
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)"}`);
|
|
285
416
|
const stopWatch = !flags["no-watch"] && keys.operator && node.cell ? startWatchLoop(new Watchers(node, catalog), node.config.runtime.pollSeconds * 1000, log) : () => { };
|
|
286
417
|
const stop = () => {
|
|
287
418
|
stopWatch();
|
|
@@ -363,7 +494,7 @@ export async function nodeCommand(cmd, rest, flags) {
|
|
|
363
494
|
return;
|
|
364
495
|
}
|
|
365
496
|
if (cmd !== "tick" && cmd !== "watch")
|
|
366
|
-
fail(`unknown node command ${cmd}; tick, watch, mcp, serve, run or key`);
|
|
497
|
+
fail(`unknown node command ${cmd}; tick, watch, mcp, serve, run, preflight or key`);
|
|
367
498
|
// a node environment: the operator key alone, nothing that derives another role
|
|
368
499
|
const operator = keysFor("operator", true);
|
|
369
500
|
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys: { operator }, log });
|
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, readFileSync, 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";
|
|
@@ -21,6 +21,16 @@ usage:
|
|
|
21
21
|
deploy-mode config: the whole stack; attach-mode config: attach what it
|
|
22
22
|
lists and deploy the reference contracts it does not (deployer = W)
|
|
23
23
|
foga world attach --world <config.json> [--out <file>] address book only, no transactions (every contract listed)
|
|
24
|
+
foga world verify --world <world.json> --sourcify [--dry-run] [--only <id,id>] [--server <url>] [--out <dir>] [--build-info <dir>]
|
|
25
|
+
submit every contract the world deployed from the bundle to Sourcify, with the standard JSON input of its
|
|
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>] [--single] the first step: a wallet of your own — owner, operator and guardian keys in 0600 files
|
|
28
|
+
(default ~/.foga/wallet; --name under ~/.foga/wallets/<n>; --single: one signer key). Never overwrites; prints the addresses
|
|
29
|
+
foga wallet show [--name <n> | --dir <dir>] [--rpc <url>] the wallet's addresses (and balances with an RPC)
|
|
30
|
+
foga wallet sign (--config <cell.config.json> | --url <node url> | --cell <account> --chain-id <n>) [--name <n> | --dir <dir>]
|
|
31
|
+
[--role signer|operator|owner|guardian] [--ttl <s>] [--agent-id <id>] [--method <METHOD> --path <path> [--body '<json>' | --body-file <f>]]
|
|
32
|
+
an Authorization header (FOGA-EIP712) proving the wallet to that cell — named by its configuration, by its node's
|
|
33
|
+
URL (/healthz says which account and chain) or by hand: a session envelope, or one bound to a request
|
|
24
34
|
foga keys new [--out <file>] write a fresh 12-word phrase (default ~/.foga/genesis.mnemonic) and print the role addresses
|
|
25
35
|
foga keys show print the role addresses the environment derives (FOGA_MNEMONIC[_FILE], FOGA_KEY_*)
|
|
26
36
|
foga keys export --role <role> [--out <file>] write one role's key to a 0600 file for a node that holds that key alone
|
|
@@ -38,12 +48,18 @@ usage:
|
|
|
38
48
|
[--zk auto|require|skip] [--crash-after <opKey>] [--crash-before <opKey>]
|
|
39
49
|
run the genesis scenario (idempotent; re-run to resume)
|
|
40
50
|
foga genesis status --world <world.json> [--store <file.sqlite>] list the operations the store holds
|
|
51
|
+
foga quickstart [--dir <dir>] [--wallet <dir>] [--port <n>] [--listen <host:port> | --no-serve] [--name <cell>] [--no-tenders] [--exit]
|
|
52
|
+
one command to a live cell: the wallet (made on the first run, reused after), anvil, the local world, a cell born with
|
|
53
|
+
the wallet's keys and its two skills, two tenders, the node serving HTTP and MCP, and the MCP client configurations
|
|
54
|
+
printed with every path filled in (~/.foga/quickstart)
|
|
41
55
|
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
|
-
|
|
56
|
+
foga demo --loop-d [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery] [--port <n>] [--keep]
|
|
57
|
+
the above, then Loop D: a new cell born, a demander minting cases a-g, the node with a model (or served for an
|
|
58
|
+
outside agent), the recovery drill, the guardian's suspension; evidence to deployments/loop-d.local.json, judged by the doctor
|
|
59
|
+
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]
|
|
60
|
+
[--listen <host:port>] [--wait <s>] [--idle <s>]
|
|
61
|
+
Loop D on a world that exists (FOGA_FUNDER_KEY funds the cell's roles on a non-local chain); with --model external the
|
|
62
|
+
node is served and an outside agent — any model, any runtime — works the tenders through /mcp or /api with the key printed
|
|
47
63
|
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
64
|
the independent demander of Loop D (FOGA_DEMANDER_KEY; anvil's first account by default)
|
|
49
65
|
foga bundle info summarize the committed bundle
|
|
@@ -144,6 +160,21 @@ async function main() {
|
|
|
144
160
|
}
|
|
145
161
|
return;
|
|
146
162
|
}
|
|
163
|
+
if (group === "world" && cmd === "verify") {
|
|
164
|
+
if (!flags.sourcify)
|
|
165
|
+
fail("foga world verify --sourcify --world <world.json> (Sourcify is the one verifier wired so far)");
|
|
166
|
+
const worldPath = str(flags, "world") ?? fail("--world <world.json> is required (a deployed world)");
|
|
167
|
+
const { verifyWorld } = await import("./world/sourcify.js");
|
|
168
|
+
const { report, outDir } = await verifyWorld({ worldPath: resolve(worldPath), server: str(flags, "server"), dryRun: flags["dry-run"] ? true : undefined, only: str(flags, "only")?.split(","), outDir: str(flags, "out") ? resolve(str(flags, "out")) : undefined, buildInfoRoot: str(flags, "build-info") ? resolve(str(flags, "build-info")) : undefined, log: (line) => console.error(line) });
|
|
169
|
+
for (const r of report.results)
|
|
170
|
+
console.log(` ${r.outcome.padEnd(12)} ${r.id.padEnd(32)} ${r.address} ${r.contractIdentifier} (${r.compilerVersion})${r.outcome === "dry-run" ? "" : ` ${r.detail}`}`);
|
|
171
|
+
for (const m of report.missing)
|
|
172
|
+
console.log(` missing ${m.id.padEnd(32)} ${m.reason}`);
|
|
173
|
+
console.log(`sourcify: ${Object.entries(report.summary).map(([k, v]) => `${v} ${k}`).join(", ") || "nothing to do"}${report.missing.length ? `, ${report.missing.length} missing` : ""}; report ${relative(root, join(outDir, "report.json"))}`);
|
|
174
|
+
if (report.missing.length || report.results.some((r) => r.outcome === "failed" || r.outcome === "error"))
|
|
175
|
+
process.exitCode = 3;
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
147
178
|
if (group === "world" && (cmd === "deploy" || cmd === "attach")) {
|
|
148
179
|
const worldConfigPath = str(flags, "world");
|
|
149
180
|
if (!worldConfigPath)
|
|
@@ -212,6 +243,113 @@ async function main() {
|
|
|
212
243
|
process.stdout.write(RELAY_SCRIPT);
|
|
213
244
|
return;
|
|
214
245
|
}
|
|
246
|
+
if (group === "wallet") {
|
|
247
|
+
const { createWallet, loadWallet, walletDir, walletFileProblems } = await import("./node/wallet.js");
|
|
248
|
+
const dir = resolve(str(flags, "dir") ?? walletDir(str(flags, "name")));
|
|
249
|
+
const printWallet = async (w, made) => {
|
|
250
|
+
console.log(`${made ? "wallet made at" : "wallet"} ${w.dir}${w.name !== "default" ? ` (${w.name})` : ""}, ${w.createdAt}`);
|
|
251
|
+
const rpc = str(flags, "rpc") ?? process.env.FOGA_RPC_URL;
|
|
252
|
+
const provider = rpc ? new (await import("ethers")).JsonRpcProvider(rpc) : null;
|
|
253
|
+
for (const role of w.roles) {
|
|
254
|
+
let balance = "";
|
|
255
|
+
if (provider) {
|
|
256
|
+
try {
|
|
257
|
+
balance = ` ${(await import("ethers")).formatEther(await provider.getBalance(w.addresses[role]))} ETH`;
|
|
258
|
+
}
|
|
259
|
+
catch {
|
|
260
|
+
balance = " (balance: RPC did not answer)";
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
console.log(` ${role.padEnd(9)} ${w.addresses[role]}${balance} ${w.files[role]}`);
|
|
264
|
+
}
|
|
265
|
+
provider?.destroy();
|
|
266
|
+
for (const p of walletFileProblems(w))
|
|
267
|
+
log.warn(p);
|
|
268
|
+
if (w.roles.includes("owner"))
|
|
269
|
+
console.log(`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
|
+
};
|
|
271
|
+
if (cmd === "new") {
|
|
272
|
+
const existing = loadWallet(dir);
|
|
273
|
+
if (existing) {
|
|
274
|
+
await printWallet(existing, false);
|
|
275
|
+
console.log("(already there; a wallet is never overwritten — foga wallet new --name <n> makes another)");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
await printWallet(createWallet(dir, { name: str(flags, "name") ?? "default", roles: flags.single ? ["signer"] : undefined }), true);
|
|
280
|
+
}
|
|
281
|
+
catch (e) {
|
|
282
|
+
fail(e.message);
|
|
283
|
+
}
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (cmd === "show") {
|
|
287
|
+
const w = loadWallet(dir);
|
|
288
|
+
if (!w)
|
|
289
|
+
fail(`no wallet at ${dir}; foga wallet new makes one`);
|
|
290
|
+
await printWallet(w, false);
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (cmd === "sign") {
|
|
294
|
+
const w = loadWallet(dir);
|
|
295
|
+
if (!w)
|
|
296
|
+
fail(`no wallet at ${dir}; foga wallet new makes one`);
|
|
297
|
+
const { walletKey } = await import("./node/wallet.js");
|
|
298
|
+
const { loadCellConfig, configPath } = await import("./node/config.js");
|
|
299
|
+
const { signAuthorization } = await import("./http/signed.js");
|
|
300
|
+
const { Wallet } = await import("ethers");
|
|
301
|
+
// the cell the signature is for: its own configuration (the operator's shell), its node's URL (anyone's: /healthz names the account and the chain), or both values by hand
|
|
302
|
+
let cellAddress;
|
|
303
|
+
let chainId;
|
|
304
|
+
let cellName;
|
|
305
|
+
if (str(flags, "config")) {
|
|
306
|
+
const loaded = loadCellConfig(str(flags, "config"));
|
|
307
|
+
cellAddress = loaded.config.identity.account ?? fail("the config names no account yet (the cell is not born)");
|
|
308
|
+
chainId = readJson(configPath(loaded, loaded.config.world)).chainId;
|
|
309
|
+
cellName = loaded.config.name;
|
|
310
|
+
}
|
|
311
|
+
else if (str(flags, "url")) {
|
|
312
|
+
const base = str(flags, "url").replace(/\/+$/, "");
|
|
313
|
+
let health;
|
|
314
|
+
try {
|
|
315
|
+
health = (await (await fetch(`${base}/healthz`, { signal: AbortSignal.timeout(10_000) })).json());
|
|
316
|
+
}
|
|
317
|
+
catch (e) {
|
|
318
|
+
fail(`${base}/healthz did not answer: ${e.message}`);
|
|
319
|
+
}
|
|
320
|
+
if (!health.account || !health.chainId)
|
|
321
|
+
fail(`${base}/healthz names no account or chain (${JSON.stringify(health).slice(0, 200)}); the cell is not born or this is not a FOGA node`);
|
|
322
|
+
cellAddress = health.account;
|
|
323
|
+
chainId = health.chainId;
|
|
324
|
+
cellName = health.cell ?? base;
|
|
325
|
+
}
|
|
326
|
+
else if (str(flags, "cell") && str(flags, "chain-id")) {
|
|
327
|
+
cellAddress = str(flags, "cell");
|
|
328
|
+
chainId = Number(str(flags, "chain-id"));
|
|
329
|
+
cellName = cellAddress;
|
|
330
|
+
}
|
|
331
|
+
else
|
|
332
|
+
fail("the signature is made for one cell: --config <cell.config.json>, or --url <its node's URL>, or --cell <account> --chain-id <n>");
|
|
333
|
+
const role = (str(flags, "role") ?? (w.roles.includes("signer") ? "signer" : "operator"));
|
|
334
|
+
let key;
|
|
335
|
+
try {
|
|
336
|
+
key = walletKey(w, role);
|
|
337
|
+
}
|
|
338
|
+
catch (e) {
|
|
339
|
+
fail(e.message);
|
|
340
|
+
}
|
|
341
|
+
const method = str(flags, "method");
|
|
342
|
+
const path = str(flags, "path");
|
|
343
|
+
if ((method && !path) || (!method && path))
|
|
344
|
+
fail("--method and --path go together");
|
|
345
|
+
const bodyText = str(flags, "body") ?? (str(flags, "body-file") ? readFileSync(resolve(str(flags, "body-file")), "utf8") : undefined);
|
|
346
|
+
const header = await signAuthorization(new Wallet(key), { chainId, cell: cellAddress, ttlSeconds: str(flags, "ttl") ? Number(str(flags, "ttl")) : undefined, agentId: str(flags, "agent-id"), request: method ? { method, path: path, body: bodyText ?? null } : undefined });
|
|
347
|
+
console.error(`${role} ${new Wallet(key).address} signed ${method ? `a ${method} ${path} envelope` : "a session envelope"} for cell ${cellName} (${cellAddress}, chain ${chainId}); the header value follows (one line):`);
|
|
348
|
+
console.log(header);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
fail("foga wallet new | show | sign");
|
|
352
|
+
}
|
|
215
353
|
if (group === "keys" && cmd === "export") {
|
|
216
354
|
const { ROLE_ORDER, envKeys, roleAddresses } = await import("./genesis/roles.js");
|
|
217
355
|
const role = str(flags, "role");
|
|
@@ -350,33 +488,54 @@ async function main() {
|
|
|
350
488
|
const { runLoopD, defaultLoopDDir } = await import("./demo/loop-d.js");
|
|
351
489
|
const worldPath = str(flags, "world") ?? fail("--world <world.json> is required");
|
|
352
490
|
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 });
|
|
491
|
+
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
492
|
return;
|
|
358
493
|
}
|
|
494
|
+
if (group === "quickstart") {
|
|
495
|
+
const { runQuickstart } = await import("./demo/quickstart.js");
|
|
496
|
+
let result;
|
|
497
|
+
try {
|
|
498
|
+
result = await runQuickstart({ dir: str(flags, "dir"), wallet: str(flags, "wallet"), 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) });
|
|
499
|
+
}
|
|
500
|
+
catch (e) {
|
|
501
|
+
fail(e.message);
|
|
502
|
+
}
|
|
503
|
+
if (flags.exit) {
|
|
504
|
+
await result.stop();
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const stopping = () => {
|
|
508
|
+
console.error("quickstart: stopping the node and the chain");
|
|
509
|
+
result.stop().finally(() => process.exit(0));
|
|
510
|
+
};
|
|
511
|
+
process.on("SIGINT", stopping);
|
|
512
|
+
process.on("SIGTERM", stopping);
|
|
513
|
+
const why = await result.ended;
|
|
514
|
+
fail(`${why}; the quickstart is over`);
|
|
515
|
+
}
|
|
359
516
|
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 });
|
|
517
|
+
await demo({ port: Number(str(flags, "port") ?? 8545), keep: Boolean(flags.keep), genesis: str(flags, "genesis"), loopD: flags["loop-d"] ? loopDFlags(flags) : undefined });
|
|
364
518
|
return;
|
|
365
519
|
}
|
|
366
520
|
console.log(USAGE);
|
|
367
521
|
fail(`unknown command: ${positional.join(" ")}`);
|
|
368
522
|
}
|
|
523
|
+
/** The Loop D options the flags name (shared by `foga demo --loop-d` and `foga loop-d run`). */
|
|
524
|
+
function loopDFlags(flags) {
|
|
525
|
+
const model = (str(flags, "model") ?? "stub");
|
|
526
|
+
if (!LOOP_D_MODELS.includes(model))
|
|
527
|
+
fail(`--model must be one of ${LOOP_D_MODELS.join(", ")}`);
|
|
528
|
+
const seconds = (key) => (str(flags, key) ? Number(str(flags, key)) * 1000 : undefined);
|
|
529
|
+
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") };
|
|
530
|
+
}
|
|
369
531
|
async function demo(opts) {
|
|
370
532
|
const root = repoRoot();
|
|
371
533
|
const rpc = `http://127.0.0.1:${opts.port}`;
|
|
372
534
|
// Say what is wrong before starting anything: a port that is already taken makes anvil exit at once, and a
|
|
373
535
|
// 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
|
-
}
|
|
536
|
+
const problem = await portProblem(opts.port);
|
|
537
|
+
if (problem)
|
|
538
|
+
fail(problem);
|
|
380
539
|
// A machine that is already saturated (a proving-key build, for instance) starves anvil: blocks come late and the
|
|
381
540
|
// kernel gives up on a receipt after its timeout. Say so before starting rather than after a failure.
|
|
382
541
|
const cores = cpus().length || 1;
|
|
@@ -385,51 +544,27 @@ async function demo(opts) {
|
|
|
385
544
|
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
545
|
}
|
|
387
546
|
// 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
547
|
// anvil's output goes to a log file rather than a pipe: with --keep the node survives this process, and a
|
|
391
548
|
// Rust program whose stderr pipe has gone away dies on its next write.
|
|
549
|
+
const { path: anvilBin, fromDir } = anvilBinary();
|
|
392
550
|
const logPath = join(root, ".foga-build", "anvil.log");
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
});
|
|
551
|
+
log.info(`starting anvil on ${rpc}${fromDir ? ` (${anvilBin})` : ""}; log ${relative(root, logPath)}`);
|
|
552
|
+
let anvil;
|
|
553
|
+
try {
|
|
554
|
+
anvil = await launchAnvil({ port: opts.port, logPath, detached: opts.keep });
|
|
555
|
+
}
|
|
556
|
+
catch (e) {
|
|
557
|
+
fail(e.message);
|
|
558
|
+
}
|
|
402
559
|
const stop = () => {
|
|
403
560
|
if (!opts.keep && !anvil.killed)
|
|
404
561
|
anvil.kill("SIGTERM");
|
|
405
562
|
};
|
|
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
563
|
process.on("exit", stop);
|
|
412
564
|
process.on("SIGINT", () => {
|
|
413
565
|
stop();
|
|
414
566
|
process.exit(130);
|
|
415
567
|
});
|
|
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
568
|
const worldConfig = join(root, "worlds", "local.json");
|
|
434
569
|
const out = join(root, "deployments", "local.json");
|
|
435
570
|
await deployWorld({ worldConfigPath: worldConfig, rpc, outPath: out });
|
|
@@ -454,7 +589,7 @@ async function demo(opts) {
|
|
|
454
589
|
if (opts.loopD) {
|
|
455
590
|
const { runLoopD, defaultLoopDDir } = await import("./demo/loop-d.js");
|
|
456
591
|
const evidencePath = join(root, "deployments", "loop-d.local.json");
|
|
457
|
-
await runLoopD({ rpc, worldPath: out, dir: defaultLoopDDir(), evidencePath,
|
|
592
|
+
await runLoopD({ rpc, worldPath: out, dir: defaultLoopDDir(), evidencePath, ...opts.loopD });
|
|
458
593
|
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
594
|
if (judged.report && judged.report.summary.Fail > 0)
|
|
460
595
|
process.exitCode = 3;
|
|
@@ -467,39 +602,6 @@ async function demo(opts) {
|
|
|
467
602
|
else
|
|
468
603
|
stop();
|
|
469
604
|
}
|
|
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
605
|
main().catch((e) => {
|
|
504
606
|
fail(e?.stack ?? String(e));
|
|
505
607
|
});
|