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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +21 -0
  3. package/README.md +216 -2
  4. package/artifacts/bundle.json +74250 -0
  5. package/artifacts/bundle.sha256 +1 -0
  6. package/bin/foga.js +14 -0
  7. package/build/known-interface-ids.json +18 -0
  8. package/build/profiles.json +120 -0
  9. package/dist/build/bundle.js +417 -0
  10. package/dist/build/vendor.js +74 -0
  11. package/dist/bundle/load.js +24 -0
  12. package/dist/bundle/types.js +1 -0
  13. package/dist/cli-cell.js +395 -0
  14. package/dist/cli.js +505 -0
  15. package/dist/core/acdf.js +162 -0
  16. package/dist/core/adrf.js +273 -0
  17. package/dist/core/aid.js +83 -0
  18. package/dist/core/ard.js +146 -0
  19. package/dist/core/asf.js +157 -0
  20. package/dist/core/canonical.js +79 -0
  21. package/dist/core/erc8004.js +52 -0
  22. package/dist/core/kya.js +225 -0
  23. package/dist/core/measure.js +96 -0
  24. package/dist/core/pack/cbor.js +130 -0
  25. package/dist/core/pack/skillpack.js +142 -0
  26. package/dist/core/skills.js +40 -0
  27. package/dist/core/swarm.js +206 -0
  28. package/dist/core/tenders.js +121 -0
  29. package/dist/core/units.js +122 -0
  30. package/dist/core/world.js +104 -0
  31. package/dist/demo/demander.js +76 -0
  32. package/dist/demo/evidence.js +1 -0
  33. package/dist/demo/loop-d.js +449 -0
  34. package/dist/doctor/checks.js +51 -0
  35. package/dist/doctor/doctor.js +385 -0
  36. package/dist/doctor/loop-d.js +103 -0
  37. package/dist/doctor/probe.js +97 -0
  38. package/dist/doctor/semantic.js +83 -0
  39. package/dist/genesis/extract.js +64 -0
  40. package/dist/genesis/funding.js +126 -0
  41. package/dist/genesis/operator-scope.js +90 -0
  42. package/dist/genesis/preflight.js +54 -0
  43. package/dist/genesis/probe.js +164 -0
  44. package/dist/genesis/roles.js +96 -0
  45. package/dist/genesis/run.js +71 -0
  46. package/dist/genesis/scenario.js +765 -0
  47. package/dist/genesis/skill-runner.js +103 -0
  48. package/dist/genesis/skill-worker.js +41 -0
  49. package/dist/genesis/t1-zk.js +62 -0
  50. package/dist/genesis/t3-zk.js +105 -0
  51. package/dist/genesis/t4.js +259 -0
  52. package/dist/genesis/t5.js +168 -0
  53. package/dist/genesis/t6.js +222 -0
  54. package/dist/genesis/t7.js +198 -0
  55. package/dist/genesis/t8.js +189 -0
  56. package/dist/genesis/t9.js +188 -0
  57. package/dist/genesis/zk.js +128 -0
  58. package/dist/http/server.js +276 -0
  59. package/dist/kernel/cell.js +104 -0
  60. package/dist/kernel/errors.js +78 -0
  61. package/dist/kernel/kernel.js +251 -0
  62. package/dist/kernel/provider.js +125 -0
  63. package/dist/kernel/store.js +470 -0
  64. package/dist/kernel/tx.js +385 -0
  65. package/dist/mcp/server.js +170 -0
  66. package/dist/node/config.js +195 -0
  67. package/dist/node/init.js +200 -0
  68. package/dist/node/keys.js +95 -0
  69. package/dist/node/migrate.js +173 -0
  70. package/dist/node/model/anthropic.js +97 -0
  71. package/dist/node/model/stub.js +129 -0
  72. package/dist/node/model/types.js +9 -0
  73. package/dist/node/node.js +198 -0
  74. package/dist/node/policy.js +95 -0
  75. package/dist/node/runner.js +256 -0
  76. package/dist/node/watchers.js +88 -0
  77. package/dist/tools/admin.js +184 -0
  78. package/dist/tools/catalog.js +261 -0
  79. package/dist/tools/common.js +143 -0
  80. package/dist/tools/identity.js +107 -0
  81. package/dist/tools/index.js +14 -0
  82. package/dist/tools/read.js +471 -0
  83. package/dist/tools/tenders.js +400 -0
  84. package/dist/tools/types.js +4 -0
  85. package/dist/util/bytecode.js +123 -0
  86. package/dist/util/fs.js +58 -0
  87. package/dist/util/hash.js +4 -0
  88. package/dist/util/log.js +11 -0
  89. package/dist/util/version.js +12 -0
  90. package/dist/world/deploy.js +277 -0
  91. package/dist/world/plan.js +93 -0
  92. package/dist/world/types.js +1 -0
  93. package/docs/build.md +45 -0
  94. package/docs/cell-account.md +58 -0
  95. package/docs/compatibility-matrix.md +38 -0
  96. package/docs/design-memo.md +113 -0
  97. package/docs/doctor.md +53 -0
  98. package/docs/examples/README.md +11 -0
  99. package/docs/examples/claude-code-mcp.json +13 -0
  100. package/docs/examples/claude_desktop_config.json +12 -0
  101. package/docs/examples/cursor-mcp.json +11 -0
  102. package/docs/genesis.md +312 -0
  103. package/docs/kernel.md +101 -0
  104. package/docs/loop-d.md +90 -0
  105. package/docs/mcp.md +105 -0
  106. package/docs/node.md +66 -0
  107. package/docs/version-lock.md +67 -0
  108. package/docs/world.md +50 -0
  109. package/docs/zk.md +48 -0
  110. package/package.json +82 -4
  111. package/prover/README.md +26 -0
  112. package/prover/build-predicate.mjs +77 -0
  113. package/prover/package-lock.json +1837 -0
  114. package/prover/package.json +15 -0
  115. package/prover/run.cjs +199 -0
  116. package/scenarios/fixtures/sample-filing.txt +16 -0
  117. package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
  118. package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
  119. package/scenarios/skills/finch-extract-verify/fields.json +56 -0
  120. package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
  121. package/scenarios/skills/foga-probe/LICENSE +2 -0
  122. package/scenarios/skills/foga-probe/SKILL.md +35 -0
  123. package/scenarios/skills/foga-probe/manifest.json +16 -0
  124. package/scenarios/skills/foga-probe/probe.schema.json +40 -0
  125. package/server.json +79 -0
  126. package/vendor/LOCK.json +107 -0
  127. package/vendor/README.md +13 -0
  128. package/worlds/local.json +34 -0
  129. package/worlds/sepolia.json +314 -0
  130. package/zk/build/verification_key.json +174 -0
  131. package/zk/circuits/kya_predicate_count_v1.circom +231 -0
@@ -0,0 +1,83 @@
1
+ import { readJson } from "../util/fs.js";
2
+ /**
3
+ * Which standards a genesis check exercises, by the dotted tokens of its name; several may apply. A token matches
4
+ * whole (`aid` does not match `unpaid`), so a name is attributed by what it names.
5
+ */
6
+ const tok = (...words) => new RegExp(`(^|\\.)(${words.join("|")})(\\.|$)`);
7
+ const RULES = [
8
+ { test: tok("kya", "scheme", "zk", "S_att", "S_cred", "S_pred", "nullifier", "epoch", "predicate", "attest"), ercs: ["8419"] },
9
+ { test: tok("aid", "anchor", "heartbeat", "document", "retire", "retired", "stale", "bind"), ercs: ["8434"] },
10
+ { test: /^A\.active|^Aprime\.active/, ercs: ["8434"] },
11
+ { test: tok("asf", "spec", "binding", "status", "suspend", "resume", "escape", "bypass", "derisk", "newrisk", "safety", "requirementId", "bindingId"), ercs: ["8468"] },
12
+ { test: tok("skill", "tamper", "commitment", "finch-extract-verify", "foga-probe"), ercs: ["8338"] },
13
+ { test: tok("tender", "submission", "submissions", "settle", "paid", "escrow", "completion", "copy", "answer", "fulfilment", "attribution", "withdraw", "accepted"), ercs: ["8414"] },
14
+ { test: tok("council", "ballot", "round", "nd", "gov", "judgment", "decide", "finalize", "appeal", "judges", "execute"), ercs: ["8436"] },
15
+ { test: tok("swarm", "roster", "join", "represent", "eligibility", "challenge", "charter", "members", "activate", "seat"), ercs: ["8447"] },
16
+ { test: tok("unit", "resolver", "receipt", "method", "measurement", "value"), ercs: ["8454"] },
17
+ { test: /^assertion\./, ercs: ["8454"] },
18
+ { test: tok("ard", "record", "backed", "adopted", "discovery"), ercs: ["8444"] },
19
+ { test: tok("dispute", "case", "ruling", "refund", "call1", "call2", "file", "finding", "service", "provider", "balances", "conforms", "released", "deposited", "charged", "held"), ercs: ["8471"] },
20
+ { test: tok("8004", "identity", "histories", "inherit"), ercs: ["8004"] },
21
+ ];
22
+ /** Steps and the standards they are about, used when a check name says nothing. */
23
+ const STEP_ERCS = {
24
+ T0: ["8419", "8468", "8436", "8471"],
25
+ T1: ["8004", "8434", "8468", "8419", "8444", "8454"],
26
+ T2: ["8338", "8444", "8454", "8434"],
27
+ T3: ["8414", "8444", "8338"],
28
+ T4: ["8447", "8436", "8414", "8454"],
29
+ T5: ["8436", "8447", "8414"],
30
+ T6: ["8471", "8436", "8338"],
31
+ T7: ["8468", "8414", "8434"],
32
+ T8: ["8454"],
33
+ T9: ["8434", "8004", "8444", "8338", "8419", "8447"],
34
+ };
35
+ export function ercsOf(check) {
36
+ const out = new Set();
37
+ for (const r of RULES)
38
+ if (r.test.test(check.name))
39
+ for (const e of r.ercs)
40
+ out.add(e);
41
+ if (out.size === 0)
42
+ for (const e of STEP_ERCS[check.step] ?? [])
43
+ out.add(e);
44
+ return [...out];
45
+ }
46
+ export function loadGenesisEvidence(path) {
47
+ const e = readJson(path);
48
+ if (e.schema !== "foga.genesis-evidence/1")
49
+ throw new Error(`${path} is not a genesis evidence file (schema ${e.schema})`);
50
+ return e;
51
+ }
52
+ /** Layer-5 verdicts per ERC from one evidence file. `ercs` are the standards present in the world. */
53
+ export function semanticVerdicts(evidence, ercs) {
54
+ const per = new Map();
55
+ for (const e of ercs)
56
+ per.set(e, { checks: [] });
57
+ for (const c of evidence.checks)
58
+ for (const e of ercsOf(c))
59
+ per.get(e)?.checks.push(c);
60
+ const out = [];
61
+ for (const erc of ercs) {
62
+ const checks = per.get(erc).checks;
63
+ const skipped = checks.filter((c) => /^skipped:/.test(c.detail));
64
+ const effective = checks.filter((c) => !/^skipped:/.test(c.detail));
65
+ const failed = effective.filter((c) => !c.pass);
66
+ const steps = [...new Set(checks.map((c) => c.step))].sort();
67
+ let verdict = "NotTested";
68
+ if (effective.length > 0)
69
+ verdict = failed.length ? "Fail" : "Pass";
70
+ out.push({
71
+ erc,
72
+ verdict,
73
+ checks: checks.length,
74
+ passed: effective.length - failed.length,
75
+ failed: failed.length,
76
+ skipped: skipped.length,
77
+ steps,
78
+ failing: failed.map((c) => `${c.step} ${c.name}`),
79
+ detail: effective.length === 0 ? `no genesis check exercises ERC-${erc} in this evidence (until ${evidence.until})` : `${effective.length - failed.length}/${effective.length} checks pass over ${steps.join(", ")}${skipped.length ? `; ${skipped.length} skipped` : ""}${failed.length ? `; failing: ${failed.map((c) => `${c.step} ${c.name}`).join(", ")}` : ""}`,
80
+ });
81
+ }
82
+ return out;
83
+ }
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { jcs, sha256Hex } from "../core/canonical.js";
3
+ function normalize(kind, raw) {
4
+ const v = raw.trim();
5
+ if (!v)
6
+ return null;
7
+ if (kind === "amount") {
8
+ const n = v.replace(/,/g, "").match(/^-?\d+(\.\d+)?/);
9
+ return n ? n[0] : null;
10
+ }
11
+ if (kind === "date") {
12
+ const d = v.match(/^\d{4}-\d{2}-\d{2}/);
13
+ return d ? d[0] : null;
14
+ }
15
+ return v;
16
+ }
17
+ export function extractVerify(text, spec, packageHash) {
18
+ const lines = text.split(/\r?\n/);
19
+ const fields = {};
20
+ for (const f of spec.fields) {
21
+ const prefix = `${f.label}:`;
22
+ const idx = lines.findIndex((l) => l.startsWith(prefix));
23
+ if (idx < 0) {
24
+ fields[f.name] = { value: null, evidence: [] };
25
+ continue;
26
+ }
27
+ fields[f.name] = { value: normalize(f.kind, lines[idx].slice(prefix.length)), evidence: [{ line: idx + 1, quote: lines[idx].trim() }] };
28
+ }
29
+ const num = (name) => (fields[name]?.value === null || fields[name]?.value === undefined ? null : Number(fields[name].value));
30
+ const rules = [];
31
+ for (const r of spec.rules) {
32
+ if (r.id === "balance") {
33
+ const a = num("totalAssets");
34
+ const l = num("totalLiabilities");
35
+ const e = num("totalEquity");
36
+ if (a === null || l === null || e === null)
37
+ rules.push({ id: r.id, verdict: "unknown", note: "a term is missing" });
38
+ else {
39
+ const diff = Math.abs(a - (l + e));
40
+ const tol = Number(r.tolerance ?? "0");
41
+ rules.push({ id: r.id, verdict: diff <= tol ? "pass" : "fail", note: `${a} vs ${l} + ${e} (difference ${diff}, tolerance ${tol})` });
42
+ }
43
+ }
44
+ else if (r.id === "period") {
45
+ const p = fields.periodEnd?.value ?? null;
46
+ const d = fields.documentDate?.value ?? null;
47
+ if (!p || !d)
48
+ rules.push({ id: r.id, verdict: "unknown", note: "a date is missing" });
49
+ else
50
+ rules.push({ id: r.id, verdict: p <= d ? "pass" : "fail", note: `period end ${p}, document date ${d}` });
51
+ }
52
+ else {
53
+ rules.push({ id: r.id, verdict: "unknown", note: "rule not implemented by the deterministic path" });
54
+ }
55
+ }
56
+ return { schema: "finch.extract-verify/1", skill: { name: "finch-extract-verify", packageHash }, documentDigest: sha256Hex(text), fields, rules };
57
+ }
58
+ export function extractVerifyFile(documentPath, fieldsPath, packageHash) {
59
+ const text = readFileSync(documentPath, "utf8");
60
+ const spec = JSON.parse(readFileSync(fieldsPath, "utf8"));
61
+ const report = extractVerify(text, spec, packageHash);
62
+ const canonical = jcs(report);
63
+ return { report, canonical, resultHash: sha256Hex(canonical) };
64
+ }
@@ -0,0 +1,126 @@
1
+ import { formatEther, parseEther, parseUnits } from "ethers";
2
+ import { CELLS, ROLE_ORDER } from "./roles.js";
3
+ /**
4
+ * Who pays for what. Every role pays its own gas; the cells hold working capital (rewards they fund, deposits
5
+ * they make) and receive what they earn. W is the treasury of the genesis: it tops the other roles up to a floor
6
+ * before every step and gives each cell its capital at birth.
7
+ *
8
+ * On anvil the first ten accounts are pre-funded, so only the judges beyond them (J1-J3) and the cells need
9
+ * anything. On a public chain the floors come from the gas each role spent in the Sepolia run of 2026-11-01
10
+ * (docs/genesis.md, "Running on a public chain"), with a margin, at the fee the chain quotes when the step starts:
11
+ * a floor is a budget in gas, priced when it is checked, so a rising base fee raises the top-up rather than
12
+ * starving a role. A role is refilled to its floor once it has fallen below half of it. The quote is never taken
13
+ * below a floor of its own (FOGA_FEE_FLOOR_GWEI, default 0.1 gwei): a testnet quotes fractions of a wei when it is
14
+ * quiet, and a role funded at that price would be stranded by the first busy block.
15
+ */
16
+ /**
17
+ * Gas a role spends over T0-T9 on a public chain, rounded up with a margin of about one half. Measured on Sepolia
18
+ * (deployments/sepolia.genesis.example.json, 203 operations, 475 M gas in all): W 131 M, G 0.8 M, owner.A 44.7 M,
19
+ * owner.B and owner.C 21.9 M each, operator.A 85.7 M, operator.B 103.5 M, operator.C 32.2 M, E 31.8 M, J1 0.8 M,
20
+ * J2 0.4 M, J3 nothing (the council decides with two of three). Sepolia's gas schedule prices the same operations
21
+ * 4.4 times higher than anvil's (a plain transfer 204,600 gas, code deposit about 1,500 gas per byte: deployments
22
+ * cost 6.5-7 times, calls 2-5 times), which is what a public chain costs from now on. The judges share one budget
23
+ * so that whichever two of them decide are funded.
24
+ */
25
+ export const ROLE_GAS_BUDGET = {
26
+ W: 200000000n, // the genesis only; what `foga world deploy` spends before it is budgeted in src/world/deploy.ts (DEPLOY_GAS_PER_STEP)
27
+ G: 1250000n,
28
+ "owner.A": 67000000n,
29
+ "owner.B": 33000000n,
30
+ "owner.C": 33000000n,
31
+ "operator.A": 130000000n,
32
+ "operator.B": 156000000n,
33
+ "operator.C": 49000000n,
34
+ E: 48000000n,
35
+ J1: 1250000n,
36
+ J2: 1250000n,
37
+ J3: 1250000n,
38
+ };
39
+ /** Working capital of the cells: what they fund, deposit and keep (B funds 0.003 ETH of rewards over T3 and T7, C deposits 0.002). */
40
+ export const CELL_CAPITAL = { local: parseEther("0.05"), public: parseEther("0.005") };
41
+ export const SUCCESSOR_CAPITAL = { local: parseEther("0.01"), public: parseEther("0.001") };
42
+ /** What W itself pays into tenders (the T4 reward and the T5 no-decision reward), on top of its gas. */
43
+ export const W_VALUE_OUTLAY = parseEther("0.003");
44
+ /** The least fee per gas a budget is priced at: FOGA_FEE_FLOOR_GWEI, default 0.1 gwei. */
45
+ export function feeFloor() {
46
+ const v = process.env.FOGA_FEE_FLOOR_GWEI?.trim();
47
+ return parseUnits(v && /^\d+(\.\d+)?$/.test(v) ? v : "0.1", "gwei");
48
+ }
49
+ /** anvil pre-funds its first ten accounts with 10 000 ETH; the judges are beyond them. */
50
+ const LOCAL_FLOORS = { J1: parseEther("1"), J2: parseEther("1"), J3: parseEther("1") };
51
+ /** The fee per gas used to price a budget: the chain's current maxFeePerGas quote, at least the fee floor. */
52
+ export async function quotePriceOf(provider) {
53
+ const fee = await provider.getFeeData();
54
+ const quoted = fee.maxFeePerGas ?? fee.gasPrice ?? 0n;
55
+ const floor = feeFloor();
56
+ return quoted > floor ? quoted : floor;
57
+ }
58
+ export function quotePrice(k) {
59
+ return quotePriceOf(k.world.provider);
60
+ }
61
+ export function floorOf(role, local, price) {
62
+ if (local)
63
+ return LOCAL_FLOORS[role] ?? 0n;
64
+ return ROLE_GAS_BUDGET[role] * price;
65
+ }
66
+ /**
67
+ * The gas an operator must still hold for its cell to take on work (the kernel's pre-commitment check): a quarter of
68
+ * the role's budget at the current price on a public chain; 0.01 ETH on anvil, where the operators are pre-funded.
69
+ */
70
+ export async function operatorGasFloor(k, role) {
71
+ if (k.isLocalChain)
72
+ return parseEther("0.01");
73
+ return (ROLE_GAS_BUDGET[role] * (await quotePrice(k))) / 4n;
74
+ }
75
+ /** Balances against floors for every role but W, and W's own requirement. */
76
+ export async function fundingPlan(k, price) {
77
+ const local = k.isLocalChain;
78
+ const p = price ?? (await quotePrice(k));
79
+ const lines = [];
80
+ for (const role of ROLE_ORDER) {
81
+ if (role === "W")
82
+ continue;
83
+ const address = k.address(role);
84
+ const balance = await k.balance(address);
85
+ const floor = floorOf(role, local, p);
86
+ // refilled to the floor once below half of it: a role that merely spent a little is not topped up at every step
87
+ lines.push({ role, address, balance, floor, topUp: balance < floor / 2n ? floor - balance : 0n });
88
+ }
89
+ const capital = local ? CELL_CAPITAL.local * BigInt(CELLS.length) + SUCCESSOR_CAPITAL.local : CELL_CAPITAL.public * BigInt(CELLS.length) + SUCCESSOR_CAPITAL.public;
90
+ const own = local ? 0n : ROLE_GAS_BUDGET.W * p + W_VALUE_OUTLAY;
91
+ const needW = lines.reduce((a, l) => a + l.topUp, 0n) + capital + own;
92
+ return { price: p, lines, needW, balanceW: await k.balance(k.address("W")) };
93
+ }
94
+ /**
95
+ * Before a step: W tops up every role that has fallen below its floor. Keyed per step and role, so a run that
96
+ * resumes finds the transfers it made and a role that stayed funded costs nothing. Fails before any transfer when W
97
+ * itself cannot cover the step, naming the shortfall.
98
+ */
99
+ export async function fundRoles(k, step, log) {
100
+ // facts hold bigints as decimal strings: a resumed run reads the price back as a string
101
+ const price = BigInt(await k.memo(`${step}.fund.price`, async () => (await quotePrice(k)).toString()));
102
+ const plan = await fundingPlan(k, price);
103
+ const due = plan.lines.filter((l) => l.topUp > 0n || k.store.get(`${step}.fund.${l.role}`)?.status === "confirmed");
104
+ if (!due.length)
105
+ return [];
106
+ const total = due.reduce((a, l) => a + l.topUp, 0n);
107
+ if (plan.balanceW < total) {
108
+ throw new Error(`W ${k.address("W")} holds ${formatEther(plan.balanceW)} ETH but ${step} needs ${formatEther(total)} ETH of top-ups at ${Number(price) / 1e9} gwei (${due.map((l) => `${l.role} ${formatEther(l.topUp)}`).join(", ")}). Fund W and run again; \`foga genesis preflight\` prints the full plan.`);
109
+ }
110
+ for (const l of due) {
111
+ // the amount is fixed the first time (a resumed run must rebuild the same transfer), so a replayed row costs nothing new
112
+ const amount = await k.memo(`${step}.fund.${l.role}.amount`, () => l.topUp.toString());
113
+ if (BigInt(amount) === 0n)
114
+ continue;
115
+ await k.transfer(`${step}.fund.${l.role}`, "W", l.address, BigInt(amount));
116
+ log(` fund: W -> ${l.role} ${formatEther(BigInt(amount))} ETH (floor ${formatEther(l.floor)} at ${Number(price) / 1e9} gwei)`);
117
+ }
118
+ return due;
119
+ }
120
+ export function formatPlan(plan, local) {
121
+ const out = [`fee per gas used for the floors: ${Number(plan.price) / 1e9} gwei${local ? " (local chain: fixed floors)" : plan.price === feeFloor() ? ` (the floor, FOGA_FEE_FLOOR_GWEI; the chain quotes less)` : " (the chain's quote)"}`];
122
+ for (const l of plan.lines)
123
+ out.push(` ${l.role.padEnd(11)} ${l.address} balance ${formatEther(l.balance).padStart(10)} floor ${formatEther(l.floor).padStart(10)} ${l.topUp > 0n ? `top-up ${formatEther(l.topUp)}` : "ok"}`);
124
+ out.push(` W needs ${formatEther(plan.needW)} ETH for the top-ups, the cells' capital (${formatEther(local ? CELL_CAPITAL.local : CELL_CAPITAL.public)} each) and its own budget; it holds ${formatEther(plan.balanceW)} ETH${plan.balanceW < plan.needW ? ` — short by ${formatEther(plan.needW - plan.balanceW)} ETH` : ""}`);
125
+ return out;
126
+ }
@@ -0,0 +1,90 @@
1
+ import { Interface } from "ethers";
2
+ import { ERC8004_ABI } from "../core/erc8004.js";
3
+ export const SCOPE_GROUPS = {
4
+ identity: [
5
+ { contract: "identity8004", fn: "register(string)", abi: ERC8004_ABI },
6
+ { contract: "identity8004", fn: "setAgentURI(uint256,string)", abi: ERC8004_ABI },
7
+ { contract: "8434/AIDRegistry", fn: "bind(address,uint256)" },
8
+ { contract: "8434/AIDRegistry", fn: "heartbeat()" },
9
+ { contract: "8434/AIDRegistry", fn: "setDocumentURI(string,bytes32)" },
10
+ { contract: "8434/AIDRegistry", fn: "setLivenessWindow(uint64)" },
11
+ { contract: "8434/AIDRegistry", fn: "retire(address)" },
12
+ { contract: "8444/ARDOnchainRegistry", fn: "register(bytes32,address,uint256,bytes32,string,string)" },
13
+ { contract: "8444/ARDOnchainRegistry", fn: "updateDescriptor(bytes32,bytes32,string,string)" },
14
+ { contract: "8454/AUFUnitRegistry", fn: "registerUnit(address,bytes32,string,(uint256,uint32)[])" },
15
+ { contract: "8447/ERC8004Eligibility", fn: "declareAgent(uint256)" },
16
+ ],
17
+ standing: [{ contract: "8419/KYARegistry", fn: "attestWithProof((bytes32,bytes),bytes32,bytes,bytes,string)" }],
18
+ "skills-provider": [
19
+ { contract: "8338/SkillToken", fn: "mintSkill(address,address,bytes32,bytes32,string)" },
20
+ { contract: "8338/SkillToken", fn: "updateSkill(uint256,bytes32,bytes32)" },
21
+ ],
22
+ labour: [
23
+ { contract: "8414/TaskToken", fn: "submitFulfillment(uint256,bytes32,string)" },
24
+ { contract: "8414/TaskToken", fn: "settleFulfillment(uint256,uint256,bytes)" },
25
+ { contract: "8414/TaskToken", fn: "withdrawCredit" },
26
+ ],
27
+ demander: [
28
+ { contract: "8414/TaskToken", fn: "mintTask" },
29
+ { contract: "8414/TaskToken", fn: "fundTask(uint256,uint256)" },
30
+ { contract: "8414/TaskToken", fn: "reclaimEscrow" },
31
+ { contract: "8414/HashlockVerifier", fn: "commitAnswer(address,uint256,bytes32)" },
32
+ ],
33
+ "organization-member": [
34
+ { contract: "8447/SwarmRegistry", fn: "join(bytes32,bytes32,bytes32)" },
35
+ { contract: "8447/SwarmRegistry", fn: "applyToJoin" },
36
+ { contract: "8447/SwarmRegistry", fn: "leave" },
37
+ { contract: "8436/ACDFRegistry", fn: "castBallot(bytes32,uint32,uint32,bool)" },
38
+ ],
39
+ "organization-coordinator": [
40
+ { contract: "8447/SwarmRegistry", fn: "form" },
41
+ { contract: "8447/SwarmRegistry", fn: "setAccount(bytes32,address)" },
42
+ // the swarm's account and the decision adapter are deployed in T4 and opened to the coordinator then (dynamic entries)
43
+ ],
44
+ "service-provider": [
45
+ { contract: "8471/ADRFServiceEscrow", fn: "deliver(uint256,bytes32)" },
46
+ { contract: "8471/ADRFServiceEscrow", fn: "settle(uint256)" },
47
+ { contract: "8471/ADRFServiceEscrow", fn: "withdrawBalance(uint256)" },
48
+ ],
49
+ "service-client": [
50
+ { contract: "8471/ADRFServiceEscrow", fn: "deposit()" },
51
+ { contract: "8471/ADRFServiceEscrow", fn: "openCall(address,uint256,bytes32,bytes32,uint64)" },
52
+ { contract: "8471/ADRFServiceEscrow", fn: "withdrawBalance(uint256)" },
53
+ { contract: "8471/ADRFServiceEscrow", fn: "enact(bytes32,bytes32)" },
54
+ { contract: "8471/ADRFDisputeRegistry", fn: "file", iface: "ai-dispute-resolution:IADRFDisputeRegistry" },
55
+ ],
56
+ };
57
+ /** The groups each genesis cell plays. A-prime (T9) takes the successor's groups: identity and what it inherits by adoption. */
58
+ export const CELL_GROUPS = {
59
+ A: ["identity", "standing", "skills-provider", "labour", "organization-member", "service-provider"],
60
+ B: ["identity", "demander", "organization-coordinator", "organization-member"],
61
+ C: ["identity", "organization-member", "service-client"],
62
+ "A-prime": ["identity", "skills-provider", "labour", "service-provider"],
63
+ };
64
+ /** Resolve groups to (target, selector) pairs on this world; the backing AID registry mirrors the primary one's entries. */
65
+ export function operatorScope(k, groups) {
66
+ const out = { targets: [], selectors: [], names: [] };
67
+ const seen = new Set();
68
+ const add = (target, selector, name) => {
69
+ const id = `${target.toLowerCase()}:${selector}`;
70
+ if (seen.has(id))
71
+ return;
72
+ seen.add(id);
73
+ out.targets.push(target);
74
+ out.selectors.push(selector);
75
+ out.names.push(name);
76
+ };
77
+ for (const group of groups) {
78
+ for (const e of SCOPE_GROUPS[group]) {
79
+ const target = k.world.address(e.contract);
80
+ const iface = e.abi ? new Interface(e.abi) : e.iface ? k.world.ifaceOf(e.iface) : k.world.iface(e.contract);
81
+ const f = iface.getFunction(e.fn);
82
+ if (!f)
83
+ throw new Error(`operator scope: ${e.contract} has no function ${e.fn}`);
84
+ add(target, f.selector, `${e.contract}.${e.fn}`);
85
+ if (e.contract === "8434/AIDRegistry" && k.aid.backing)
86
+ add(k.aid.backing.address, f.selector, `${k.aid.backing.id}.${e.fn}`);
87
+ }
88
+ }
89
+ return out;
90
+ }
@@ -0,0 +1,54 @@
1
+ import { formatEther, getAddress } from "ethers";
2
+ import { readJson } from "../util/fs.js";
3
+ import { Kernel } from "../kernel/kernel.js";
4
+ import { formatPlan, fundingPlan } from "./funding.js";
5
+ import { ROLE_ORDER, roleKeys } from "./roles.js";
6
+ import { LOCAL_TIMING, PUBLIC_TIMING, LIVENESS_DEMO_WINDOW } from "./scenario.js";
7
+ import { predicateAvailability, zkAvailability } from "./zk.js";
8
+ /**
9
+ * `foga genesis preflight`: everything a run on a public chain needs, checked before the first transaction — the
10
+ * RPC and its chain, the twelve keys, every role's balance against its floor and what W must hold, the escrow's
11
+ * admin, the proving material, and how long the waits will take. Nothing is sent. Returns the problems found.
12
+ */
13
+ export async function preflight(opts) {
14
+ const problems = [];
15
+ const say = opts.log;
16
+ const world = readJson(opts.worldPath);
17
+ if (world.schema !== "foga.world/1")
18
+ throw new Error(`${opts.worldPath} is not a deployed world file (run foga world deploy first)`);
19
+ const keys = roleKeys(world);
20
+ const k = await Kernel.open({ worldPath: opts.worldPath, rpc: opts.rpc, bundlePath: opts.bundlePath, storePath: ":memory:", keys });
21
+ try {
22
+ const net = await k.world.provider.getNetwork();
23
+ const block = await k.world.provider.getBlock("latest");
24
+ say(`world ${world.worldId} (${world.network}, chain ${world.chainId}, ${world.mode}) via ${opts.rpc}: chain ${net.chainId}, block ${block?.number} at ${new Date(Number(block?.timestamp) * 1000).toISOString()}`);
25
+ say("roles:");
26
+ for (const role of ROLE_ORDER)
27
+ say(` ${role.padEnd(11)} ${k.address(role)}`);
28
+ const plan = await fundingPlan(k);
29
+ say("funding:");
30
+ for (const line of formatPlan(plan, k.isLocalChain))
31
+ say(line);
32
+ if (plan.balanceW < plan.needW)
33
+ problems.push(`W ${k.address("W")} is short by ${formatEther(plan.needW - plan.balanceW)} ETH (holds ${formatEther(plan.balanceW)}, needs ${formatEther(plan.needW)} at ${Number(plan.price) / 1e9} gwei)`);
34
+ const admin = await k.adrf.escrow().admin();
35
+ const adminOk = getAddress(admin) === getAddress(k.address("W"));
36
+ say(`escrow: ${k.adrf.escrowAddress} administered by ${admin}${adminOk ? " (W)" : " (NOT W)"}`);
37
+ if (!adminOk)
38
+ problems.push(`the world's ADRFServiceEscrow is administered by ${admin}, not by W: deploy the world with W's key`);
39
+ say(`AID: primary registry ${k.aid.address}${k.aid.backing ? `; backing registry ${k.aid.backing.address} (${k.aid.backing.id}): cells bind, heartbeat and retire in both` : " (no backing registry)"}`);
40
+ const defaultWindow = await k.aid.defaultLivenessWindow();
41
+ say(` default liveness window ${defaultWindow} s; T7 shortens A's window to ${LIVENESS_DEMO_WINDOW} s and waits for it`);
42
+ const zk = zkAvailability();
43
+ const pred = predicateAvailability();
44
+ say(`zk: credential path ${zk.ok ? "available" : "not available (" + zk.missing.join("; ") + ")"}; predicate path ${pred.ok ? "available" : "not available (" + pred.missing.join("; ") + ")"}`);
45
+ const t = k.isLocalChain ? LOCAL_TIMING : PUBLIC_TIMING;
46
+ const waits = t.appealWindow + t.councilWindow + t.noDecisionJudgmentWindow + t.appealWindow + t.service.reviewWindow + t.service.disputePeriod + LIVENESS_DEMO_WINDOW;
47
+ say(`timing: ${k.isLocalChain ? "local profile (the clock is moved)" : `public profile: about ${Math.round(waits / 60)} minutes of waiting over T5-T7 (council ${t.councilWindow} s, appeal ${t.appealWindow} s, judgment ${t.noDecisionJudgmentWindow} s, review ${t.service.reviewWindow} s, dispute period ${t.service.disputePeriod} s), plus one block per operation (about 190 operations)`}`);
48
+ }
49
+ finally {
50
+ k.close();
51
+ k.world.provider.destroy();
52
+ }
53
+ return problems;
54
+ }
@@ -0,0 +1,164 @@
1
+ import { Contract, getAddress } from "ethers";
2
+ import { AIDStateName } from "../core/aid.js";
3
+ import { ReasonName, VerdictName, accountSubjectKey } from "../core/asf.js";
4
+ import { accountRef, jcs } from "../core/canonical.js";
5
+ import { ERC8004_ABI } from "../core/erc8004.js";
6
+ import { accountSubject } from "../core/kya.js";
7
+ const ZERO = "0x0000000000000000000000000000000000000000";
8
+ /**
9
+ * The `foga-probe` skill: eleven sections (ERC-8004 plus the ten), every read pinned to `input.block`, every
10
+ * value rendered so that JCS of the report is byte-stable (numbers as decimal strings, addresses lower-case).
11
+ */
12
+ export async function runProbe(world, input) {
13
+ const subject = getAddress(input.subject);
14
+ const low = subject.toLowerCase();
15
+ const tag = input.block;
16
+ const block = await world.provider.getBlock(input.block);
17
+ if (!block)
18
+ throw new Error(`block ${input.block} not found`);
19
+ const at = { blockTag: tag };
20
+ const fromBlock = input.lookback === undefined ? 0 : Math.max(0, input.block - input.lookback);
21
+ const section = async (fn) => {
22
+ try {
23
+ return await fn();
24
+ }
25
+ catch {
26
+ return null;
27
+ }
28
+ };
29
+ const aid = world.contract("8434/AIDRegistry");
30
+ const checks = {};
31
+ checks.erc8434 = await section(async () => {
32
+ const state = Number(await aid.state(subject, at));
33
+ const b = await aid.bindingOf(subject, at);
34
+ const doc = await aid.documentURI(subject, at);
35
+ return {
36
+ state: AIDStateName[state],
37
+ registry: String(b.registry).toLowerCase(),
38
+ agentId: String(b.agentId),
39
+ boundAt: String(b.boundAt),
40
+ lastSeen: String(await aid.lastSeen(subject, at)),
41
+ documentDigest: String(doc.digest ?? doc[1]),
42
+ };
43
+ });
44
+ checks.erc8004 = await section(async () => {
45
+ const b = await aid.bindingOf(subject, at);
46
+ const registry = String(b.registry);
47
+ if (registry === ZERO)
48
+ return { bound: false };
49
+ const identity = new Contract(registry, ERC8004_ABI, world.provider);
50
+ const agentId = BigInt(b.agentId);
51
+ return {
52
+ bound: true,
53
+ registry: registry.toLowerCase(),
54
+ agentId: agentId.toString(),
55
+ owner: String(await identity.ownerOf(agentId, at)).toLowerCase(),
56
+ agentURI: String(await identity.tokenURI(agentId, at)),
57
+ };
58
+ });
59
+ checks.erc8419 = await section(async () => {
60
+ const kya = world.contract("8419/KYARegistry");
61
+ const issuers = input.kya.issuers.map((a) => getAddress(a));
62
+ const r = await kya.resolve(accountSubject(world.chainId, subject), input.kya.schemeId, issuers, at);
63
+ return {
64
+ schemeId: input.kya.schemeId,
65
+ issuers: issuers.map((a) => a.toLowerCase()),
66
+ level: String(r.level ?? r[0]),
67
+ expiresAt: String(r.expiresAt ?? r[1]),
68
+ assertionId: String(r.assertionId ?? r[2]),
69
+ };
70
+ });
71
+ checks.erc8468 = await section(async () => {
72
+ const bindings = world.contract("8468/ASFBindingRegistry");
73
+ const count = Number(await bindings.countBySubject(accountSubjectKey(world.chainId, subject), subject, at));
74
+ const out = { bindingsAsSubject: String(count) };
75
+ try {
76
+ const cell = world.at("foga:FogaCellAccount", subject);
77
+ const s = await cell.status(at);
78
+ out.status = { verdict: VerdictName[Number(s[0])], reasons: Array.from(s[1], (x) => ReasonName[Number(x)]), restrictions: Array.from(s[2], (x) => String(x)) };
79
+ }
80
+ catch {
81
+ out.status = null;
82
+ }
83
+ return out;
84
+ });
85
+ checks.erc8444 = await section(async () => {
86
+ const ard = world.contract("8444/ARDOnchainRegistry");
87
+ const recordId = String(await ard.recordIdOf(subject, 0n, at));
88
+ const rec = await ard.recordOf(recordId, at);
89
+ const status = Number(rec.status);
90
+ if (status === 0)
91
+ return { agentRecord: null };
92
+ return {
93
+ agentRecord: { recordId, status: String(status), descriptorHash: String(rec.descriptorHash), urn: String(rec.urn), backed: Boolean(await ard.backed(recordId, at)) },
94
+ };
95
+ });
96
+ checks.erc8454 = await section(async () => {
97
+ const units = world.contract("8454/AUFUnitRegistry");
98
+ const logs = await units.queryFilter(units.filters.UnitRegistered(null, subject), fromBlock, input.block);
99
+ return { unitsControlled: logs.map((l) => String(l.args.unitId)) };
100
+ });
101
+ checks.erc8338 = await section(async () => {
102
+ const skills = world.contract("8338/SkillToken");
103
+ const held = String(await skills.balanceOf(subject, at));
104
+ const received = await skills.queryFilter(skills.filters.Transfer(null, subject), fromBlock, input.block);
105
+ const ids = new Set();
106
+ for (const l of received)
107
+ ids.add(String(l.args.tokenId));
108
+ const tokens = [];
109
+ for (const id of [...ids].sort((a, b) => (BigInt(a) < BigInt(b) ? -1 : 1))) {
110
+ try {
111
+ if (String(await skills.ownerOf(BigInt(id), at)).toLowerCase() !== low)
112
+ continue;
113
+ const b = await skills.skillOf(BigInt(id), at);
114
+ tokens.push({ tokenId: id, packageHash: String(b.packageHash), version: String(b.version) });
115
+ }
116
+ catch {
117
+ /* burned or absent at that block */
118
+ }
119
+ }
120
+ return { held, tokens };
121
+ });
122
+ checks.erc8414 = await section(async () => {
123
+ const tasks = world.contract("8414/TaskToken");
124
+ const held = String(await tasks.balanceOf(subject, at));
125
+ const submitted = await tasks.queryFilter(tasks.filters.FulfillmentSubmitted(null, null, subject), fromBlock, input.block);
126
+ const accepted = await tasks.queryFilter(tasks.filters.FulfillmentAccepted(null, null, subject), fromBlock, input.block);
127
+ return { held, submissions: String(submitted.length), accepted: String(accepted.length) };
128
+ });
129
+ checks.erc8447 = await section(async () => {
130
+ const swarms = world.contract("8447/SwarmRegistry");
131
+ const activated = await swarms.queryFilter(swarms.filters.MemberActivated(null, subject), fromBlock, input.block);
132
+ const ids = [...new Set(activated.map((l) => String(l.args.swarmId)))].sort();
133
+ const memberships = [];
134
+ for (const swarmId of ids)
135
+ memberships.push({ swarmId, status: String(await swarms.memberStatus(swarmId, subject, at)) });
136
+ return { memberships };
137
+ });
138
+ checks.erc8436 = await section(async () => {
139
+ const acdf = world.contract("8436/ACDFRegistry");
140
+ const filed = await acdf.queryFilter(acdf.filters.IssueFiled(null, null, null, subject), fromBlock, input.block);
141
+ return { issuesFiled: String(filed.length) };
142
+ });
143
+ checks.erc8471 = await section(async () => {
144
+ const adrf = world.contract("8471/ADRFDisputeRegistry");
145
+ const filed = await adrf.queryFilter(adrf.filters.CaseFiled(), fromBlock, input.block);
146
+ let asParty = 0;
147
+ for (const l of filed) {
148
+ if (String(l.args.claimant).toLowerCase() === low || String(l.args.respondent).toLowerCase() === low)
149
+ asParty++;
150
+ }
151
+ return { casesAsParty: String(asParty) };
152
+ });
153
+ const ordered = {};
154
+ for (const k of ["erc8004", "erc8434", "erc8419", "erc8468", "erc8444", "erc8454", "erc8338", "erc8414", "erc8447", "erc8436", "erc8471"])
155
+ ordered[k] = checks[k] ?? null;
156
+ const report = {
157
+ schema: "foga.probe/1",
158
+ skill: "foga-probe",
159
+ world: { worldId: world.file.worldId, chainId: world.chainId, block: input.block, blockHash: block.hash },
160
+ subject: accountRef(world.chainId, subject),
161
+ checks: ordered,
162
+ };
163
+ return { report, answer: jcs(report) };
164
+ }