@finchip.ai/foga 0.6.0-alpha.6 → 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/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> --owner <addr> --operator <addr> --guardian <addr> --out <cell.config.json> [--rpc <url>] [--skills <dir>] [--input-dir <dir>]
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 <capability> --config <cell.config.json> [--mode simulate|build|act] [--json '<input json>' | --json-file <f>] [--<field> <value> ...]
30
- any capability; an act loads FOGA_OPERATOR_KEY[_FILE], an admin capability FOGA_OWNER_KEY[_FILE] or FOGA_GUARDIAN_KEY[_FILE]
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
- the HTTP server: POST /api/<capability>, /mcp (Streamable HTTP), /healthz, / — the public reads and builds,
39
- a Bearer key acts as its grant says; the watchers run beside it
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
- function keysFor(role, strict = false) {
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 need = (k) => str(flags, k) ?? fail(`--${k} is required`);
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
- log(`foga ${version}: serving cell ${node.config.name}${node.cell ? ` (${node.cell.address})` : ""} at ${running.url} — /mcp, /api/<capability>, /healthz; ${keys.operator ? "acting with the operator key" : "no operator key: reads and builds only"}; ${node.config.access.keys.length} API key(s) configured`);
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,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import { cpus, homedir, loadavg } from "node:os";
4
4
  import { dirname, join, relative, resolve } from "node:path";
5
5
  import { buildBundle } from "./build/bundle.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,9 +48,10 @@ 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
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)
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)
44
55
  foga demo [--port <n>] [--keep] [--genesis <T0..T9>] start anvil, deploy the local world, run the doctor (and the genesis)
45
56
  foga demo --loop-d [--model stub|anthropic|external] [--model-name <name>] [--seed <n>] [--no-recovery] [--port <n>] [--keep]
46
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
@@ -149,6 +160,21 @@ async function main() {
149
160
  }
150
161
  return;
151
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
+ }
152
178
  if (group === "world" && (cmd === "deploy" || cmd === "attach")) {
153
179
  const worldConfigPath = str(flags, "world");
154
180
  if (!worldConfigPath)
@@ -217,6 +243,113 @@ async function main() {
217
243
  process.stdout.write(RELAY_SCRIPT);
218
244
  return;
219
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
+ }
220
353
  if (group === "keys" && cmd === "export") {
221
354
  const { ROLE_ORDER, envKeys, roleAddresses } = await import("./genesis/roles.js");
222
355
  const role = str(flags, "role");
@@ -362,7 +495,7 @@ async function main() {
362
495
  const { runQuickstart } = await import("./demo/quickstart.js");
363
496
  let result;
364
497
  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) });
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) });
366
499
  }
367
500
  catch (e) {
368
501
  fail(e.message);
@@ -12,10 +12,10 @@ export async function birthCell(o) {
12
12
  const log = o.log ?? (() => { });
13
13
  const root = repoRoot();
14
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}`);
15
+ const owner = o.keys ? new Wallet(o.keys.owner) : Wallet.createRandom();
16
+ const operator = o.keys ? new Wallet(o.keys.operator) : Wallet.createRandom();
17
+ const guardian = o.keys ? new Wallet(o.keys.guardian) : Wallet.createRandom();
18
+ log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}${o.keys ? " (the wallet's keys)" : " (fresh keys)"}`);
19
19
  // --- funding by W, the standing scheme registered by W
20
20
  const price = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
21
21
  const gas = (units) => units * price * 2n;
@@ -1,12 +1,15 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
- import { join, resolve } from "node:path";
4
+ import { join, resolve, sep } from "node:path";
5
5
  import { formatEther, parseEther } from "ethers";
6
6
  import { anvilKeys } from "../genesis/roles.js";
7
7
  import { Kernel } from "../kernel/kernel.js";
8
8
  import { newApiKey } from "../http/server.js";
9
9
  import { loadCellConfig, saveCellConfig } from "../node/config.js";
10
+ import { Node } from "../node/node.js";
11
+ import { defaultCatalog, OPERATOR_CALLER } from "../tools/index.js";
12
+ import { ensureWallet, ensuredKey, walletDir } from "../node/wallet.js";
10
13
  import { repoRoot } from "../util/fs.js";
11
14
  import { deployWorld } from "../world/deploy.js";
12
15
  import { ANVIL_INSTALL_HINT, anvilBinary, anvilVersion, launchAnvil, portInUse, portProblem, stopChild } from "./anvil.js";
@@ -49,6 +52,12 @@ export async function runQuickstart(o = {}) {
49
52
  const listen = o.listen === undefined ? "127.0.0.1:8787" : o.listen;
50
53
  const name = o.name ?? "quick";
51
54
  const rpc = `http://127.0.0.1:${port}`;
55
+ const walletPath = resolve(o.wallet ?? walletDir());
56
+ const nodeUrlPlanned = listen ? (() => {
57
+ const i = listen.lastIndexOf(":");
58
+ const host = listen.slice(0, i) || "127.0.0.1";
59
+ return `http://${host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host}:${listen.slice(i + 1)}`;
60
+ })() : null;
52
61
  // --- say what is wrong before touching anything
53
62
  const anvilBin = anvilBinary();
54
63
  const version = anvilVersion(anvilBin.path);
@@ -62,6 +71,8 @@ export async function runQuickstart(o = {}) {
62
71
  if (await portInUse(lp))
63
72
  throw new Error(`port ${lp} is in use; pass --listen <host:port> for the node (or --no-serve to run no node)`);
64
73
  }
74
+ if (walletPath === dir || walletPath.startsWith(dir + sep))
75
+ throw new Error(`the wallet (${walletPath}) must live outside the quickstart directory (${dir}), which is cleared on every run`);
65
76
  if (existsSync(dir)) {
66
77
  const entries = readdirSync(dir);
67
78
  if (entries.length && !entries.includes(MARKER))
@@ -74,6 +85,12 @@ export async function runQuickstart(o = {}) {
74
85
  mkdirSync(dir, { recursive: true });
75
86
  const keysDir = join(dir, "keys");
76
87
  mkdirSync(keysDir, { recursive: true, mode: 0o700 });
88
+ // --- the wallet: the first step; made now when there is none, reused when there is; never a reason to stop
89
+ const wallet = ensureWallet(walletPath, { name: "default" });
90
+ if (wallet.ephemeral)
91
+ log(`quickstart: wallet: could not write ${walletPath} (${wallet.reason}); using keys made in memory for this run — they are not kept`);
92
+ else
93
+ log(`quickstart: wallet: ${wallet.created ? "made" : "using"} ${wallet.wallet.dir} — owner ${wallet.wallet.addresses.owner}, operator ${wallet.wallet.addresses.operator}, guardian ${wallet.wallet.addresses.guardian}`);
77
94
  // --- the chain and the world
78
95
  log(`quickstart: starting ${version} on ${rpc} (log ${join(dir, "anvil.log")})`);
79
96
  const anvil = await launchAnvil({ port, logPath: join(dir, "anvil.log") });
@@ -106,6 +123,7 @@ export async function runQuickstart(o = {}) {
106
123
  configPath: join(dir, "cell.config.json"),
107
124
  storePath: join(dir, "cell.ops.sqlite"),
108
125
  description: `Cell ${name}: born by foga quickstart on a local anvil chain`,
126
+ keys: { owner: ensuredKey(wallet, "owner"), operator: ensuredKey(wallet, "operator"), guardian: ensuredKey(wallet, "guardian") },
109
127
  log: (line) => log(`quickstart: ${line}`),
110
128
  configure: (cfg) => {
111
129
  cfg.policy.budget = { dailyValueWei: parseEther("0.002").toString(), perOperationValueWei: parseEther("0.001").toString(), dailyGasWei: parseEther("0.05").toString(), maxConcurrentCommitments: 2 };
@@ -113,6 +131,8 @@ export async function runQuickstart(o = {}) {
113
131
  cfg.runtime.pollSeconds = 2;
114
132
  cfg.runtime.http.listen = listen ?? "127.0.0.1:8787";
115
133
  cfg.runtime.http.rateLimitPerMinute = 600;
134
+ if (nodeUrlPlanned)
135
+ cfg.runtime.http.publicUrl = nodeUrlPlanned;
116
136
  },
117
137
  });
118
138
  }
@@ -121,13 +141,16 @@ export async function runQuickstart(o = {}) {
121
141
  k.world.provider.destroy();
122
142
  }
123
143
  const configPath = cell.configPath;
124
- // --- the keys, each in its own 0600 file (anvil money; nothing of value), and an act-level API key for the HTTP entrance
144
+ // --- the key files the configurations name: the wallet's own; an in-memory wallet is written beside the cell so the run still works
125
145
  const keyFile = (file, value) => {
126
146
  const path = join(keysDir, file);
127
147
  writeFileSync(path, value + "\n", { mode: 0o600 });
128
148
  return path;
129
149
  };
130
- const keyPaths = { owner: keyFile("owner.key", cell.owner.privateKey), operator: keyFile("operator.key", cell.operator.privateKey), guardian: keyFile("guardian.key", cell.guardian.privateKey) };
150
+ const keyPaths = wallet.ephemeral
151
+ ? { owner: keyFile("owner.key", cell.owner.privateKey), operator: keyFile("operator.key", cell.operator.privateKey), guardian: keyFile("guardian.key", cell.guardian.privateKey) }
152
+ : { owner: wallet.wallet.files.owner, operator: wallet.wallet.files.operator, guardian: wallet.wallet.files.guardian };
153
+ // an act-level API key for the HTTP entrance
131
154
  const api = newApiKey("agent");
132
155
  {
133
156
  const loaded = loadCellConfig(configPath);
@@ -135,6 +158,23 @@ export async function runQuickstart(o = {}) {
135
158
  saveCellConfig(configPath, loaded.config);
136
159
  }
137
160
  const agentKeyPath = keyFile("agent.api-key", api.key);
161
+ // --- the cell says what it is: both skills offered, the documents built and committed on chain (the operator's act, in this process)
162
+ {
163
+ const loaded = loadCellConfig(configPath);
164
+ loaded.config.skills.offered = Object.keys(loaded.config.skills.held);
165
+ saveCellConfig(configPath, loaded.config);
166
+ const publisher = await Node.open({ configPath, rpc, keys: { operator: ensuredKey(wallet, "operator") }, log: () => { } });
167
+ try {
168
+ const r = await defaultCatalog().invoke(publisher, "docs_publish", {}, OPERATOR_CALLER, { mode: "act" });
169
+ if (!r.ok)
170
+ throw new Error(`docs_publish: ${r.gate}: ${r.reason}`);
171
+ const out = r.output;
172
+ log(`quickstart: documents v${out.version} built in ${out.dir} and committed on chain (${out.committed.join(", ")})`);
173
+ }
174
+ finally {
175
+ publisher.close();
176
+ }
177
+ }
138
178
  // --- two tenders for the cell to find: a probe of the cell itself and an extraction over the fixture
139
179
  let minted = [];
140
180
  if (o.tenders ?? true) {
@@ -177,7 +217,15 @@ export async function runQuickstart(o = {}) {
177
217
  const toolCount = (toolsText.match(/"name":"/g) ?? []).length;
178
218
  if (toolCount < 20)
179
219
  throw new Error(`the MCP entrance listed ${toolCount} tool(s) for the agent key; expected the act-level set`);
180
- log(`quickstart: the entrance answers — /healthz ok, cell_status names ${cell.account}, tender ${minted[0]?.tokenId ?? "-"} eligible for the agent key, MCP initialize ok, ${toolCount} tools`);
220
+ const cardRes = await fetch(`${nodeUrl}/.well-known/agent-card.json`);
221
+ const cardDoc = (await cardRes.json());
222
+ if (!cardRes.ok || cardDoc.supportedInterfaces?.[0]?.url !== `${nodeUrl}/a2a`)
223
+ throw new Error(`the agent card at /.well-known/agent-card.json does not name ${nodeUrl}/a2a`);
224
+ const quoteRes = await fetch(`${nodeUrl}/a2a`, { method: "POST", headers: { "content-type": "application/json", "a2a-version": "1.0" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "SendMessage", params: { message: { messageId: "quickstart-quote", role: "ROLE_USER", parts: [{ text: "What can you do?" }] } } }) });
225
+ const quoteBody = (await quoteRes.json());
226
+ if (quoteBody.result?.task?.status?.state !== "TASK_STATE_COMPLETED")
227
+ throw new Error(`the A2A entrance did not answer a quote: ${JSON.stringify(quoteBody).slice(0, 200)}`);
228
+ log(`quickstart: the entrance answers — /healthz ok, cell_status names ${cell.account}, tender ${minted[0]?.tokenId ?? "-"} eligible for the agent key, MCP initialize ok, ${toolCount} tools; the agent card names ${nodeUrl}/a2a and a quote came back`);
181
229
  }
182
230
  // --- the stdio server the client configurations name, started as a client would start it: the operator's tool set over stdio
183
231
  const stdioTools = await stdioToolCount(cli, configPath, rpc, cleanEnv({ FOGA_OPERATOR_KEY_FILE: keyPaths.operator }));
@@ -197,12 +245,14 @@ export async function runQuickstart(o = {}) {
197
245
  lines.push(`foga quickstart: a FOGA cell is live on a local chain`);
198
246
  lines.push("");
199
247
  lines.push(` chain anvil on ${rpc} (chain ${world.chainId}), world ${world.worldId} — the chain lives as long as this process`);
200
- lines.push(` cell ${name} = ${cell.account}${cell.agentId ? ` (ERC-8004 agent ${cell.agentId})` : ""}; owner, operator and guardian keys in ${keysDir} (anvil money, no value)`);
248
+ lines.push(` cell ${name} = ${cell.account}${cell.agentId ? ` (ERC-8004 agent ${cell.agentId})` : ""}`);
249
+ lines.push(` wallet ${wallet.ephemeral ? `keys made in memory for this run, written to ${keysDir} (${walletPath} was not writable)` : `${wallet.wallet.dir} (${wallet.created ? "made now" : "reused"}) — owner ${cell.owner.address}, operator ${cell.operator.address}, guardian ${cell.guardian.address}; the same wallet on every run`}`);
201
250
  lines.push(` skills ${Object.keys(cell.held).join(", ")} (held and offered)`);
202
251
  if (minted.length)
203
252
  lines.push(` tenders ${minted.map((c) => `#${c.tokenId} ${c.skill} (${formatEther(c.rewardWei)} ETH)`).join(", ")} — open, waiting for the cell to take them`);
204
253
  if (nodeUrl)
205
- lines.push(` node ${nodeUrl} — /mcp (Streamable HTTP), /api/<capability>, /healthz; its watchers settle and withdraw what the cell commits to`);
254
+ lines.push(` node ${nodeUrl} — /mcp (Streamable HTTP), /api/<capability>, /a2a (A2A 1.0, JSON-RPC), /.well-known/agent-card.json, /documents/*, /healthz; its watchers settle and withdraw`);
255
+ lines.push(` documents ${join(dir, "documents")} — registration, AID document, ARD descriptor, AUF unit, agent card, server.json; committed on chain${nodeUrl ? ` as URLs under ${nodeUrl}` : ""}`);
206
256
  lines.push(` files ${configPath} (the cell), ${join(dir, "connect.md")} (this text), ${mcpDir}${process.platform === "win32" ? "\\" : "/"}*.json (the client configurations)`);
207
257
  lines.push("");
208
258
  lines.push("Connect an MCP client over stdio (the client starts the server on this machine; with the operator key it acts for the cell):");
@@ -217,6 +267,8 @@ export async function runQuickstart(o = {}) {
217
267
  lines.push("Or over HTTP (a client on another machine, or any agent runtime that speaks HTTP):");
218
268
  lines.push(` MCP ${nodeUrl}/mcp with the header Authorization: Bearer ${api.key} (${join(mcpDir, "http-mcp.json")})`);
219
269
  lines.push(` API ${nodeUrl}/api/<capability>, POST a JSON body (reads also GET); the same header for acts. GET ${nodeUrl}/api/capabilities lists them.`);
270
+ lines.push(` A2A POST ${nodeUrl}/a2a with A2A-Version: 1.0 — SendMessage for a quote (anyone) or a tender ({"foga":{"mode":"tender","tokenId":"…"}}, the header above); the card is at ${nodeUrl}/.well-known/agent-card.json`);
271
+ lines.push(` wallet a credential of your own instead of the key: Authorization: FOGA-EIP712 <envelope> signed by any wallet (foga wallet sign --url ${nodeUrl} prints one; docs/wallet.md)`);
220
272
  lines.push(` curl -s ${nodeUrl}/api/cell_status`);
221
273
  if (minted.length)
222
274
  lines.push(` curl -s -X POST ${nodeUrl}/api/tender_precheck -H "content-type: application/json" -H "authorization: Bearer ${api.key}" -d "{\\"tokenId\\":\\"${minted[0].tokenId}\\"}"`);
@@ -231,7 +283,7 @@ export async function runQuickstart(o = {}) {
231
283
  lines.push(o.exit ? "Started with --exit: stopping the node and the chain." : "Ctrl-C stops the node and the chain; the next quickstart starts over in the same directory.");
232
284
  const summary = lines.join("\n");
233
285
  writeFileSync(join(dir, "connect.md"), `# foga quickstart\n\n\`\`\`\n${summary}\n\`\`\`\n`);
234
- writeFileSync(join(dir, MARKER), JSON.stringify({ schema: "foga.quickstart/1", createdAt: new Date().toISOString(), rpc, chainId: world.chainId, worldId: world.worldId, cell: name, account: cell.account, agentId: cell.agentId, nodeUrl, tenders: minted.map((c) => c.tokenId), anvil: version }, null, 2) + "\n");
286
+ writeFileSync(join(dir, MARKER), JSON.stringify({ schema: "foga.quickstart/1", createdAt: new Date().toISOString(), rpc, chainId: world.chainId, worldId: world.worldId, cell: name, account: cell.account, agentId: cell.agentId, wallet: wallet.ephemeral ? null : wallet.wallet.dir, nodeUrl, tenders: minted.map((c) => c.tokenId), anvil: version }, null, 2) + "\n");
235
287
  out(summary);
236
288
  return {
237
289
  dir,
@@ -242,6 +294,7 @@ export async function runQuickstart(o = {}) {
242
294
  configPath,
243
295
  account: cell.account,
244
296
  agentId: cell.agentId,
297
+ wallet: { dir: wallet.wallet.dir, created: wallet.created, ephemeral: wallet.ephemeral, addresses: wallet.wallet.addresses },
245
298
  keys: { ...keyPaths, agentApiKey: agentKeyPath },
246
299
  apiKey: api.key,
247
300
  nodeUrl,