@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/README.md +31 -14
- 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 +138 -5
- package/dist/demo/birth.js +4 -4
- package/dist/demo/quickstart.js +60 -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/sourcify.js +165 -0
- package/docs/a2a.md +31 -0
- package/docs/design-memo.md +17 -6
- package/docs/documents.md +25 -0
- package/docs/examples/README.md +2 -2
- package/docs/mcp.md +16 -11
- package/docs/node.md +28 -10
- package/docs/quickstart.md +30 -21
- package/docs/wallet.md +60 -0
- package/docs/world.md +12 -0
- package/package.json +1 -1
- 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,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
|
|
43
|
-
the node serving HTTP and MCP, and the MCP client configurations
|
|
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);
|
package/dist/demo/birth.js
CHANGED
|
@@ -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;
|
package/dist/demo/quickstart.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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
|
-
|
|
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})` : ""}
|
|
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
|
|
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,
|