@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,277 @@
1
+ import { Contract, ContractFactory, NonceManager, Wallet, formatEther, getAddress, isAddress } from "ethers";
2
+ import { PinnedProvider } from "../kernel/provider.js";
3
+ import { join, relative } from "node:path";
4
+ import { loadBundle, getContract } from "../bundle/load.js";
5
+ import { linkBytecode } from "../util/bytecode.js";
6
+ import { exists, readJson, repoRoot, writeJson } from "../util/fs.js";
7
+ import { log } from "../util/log.js";
8
+ import { DEPLOY_PLAN, WIRE_PLAN } from "./plan.js";
9
+ import { envMnemonic, mnemonicKeys } from "../genesis/roles.js";
10
+ import { feeFloor, quotePriceOf } from "../genesis/funding.js";
11
+ /**
12
+ * Gas the deployer is asked to hold per contract it still has to deploy, as a budget priced at the chain's quote
13
+ * (at least the fee floor, FOGA_FEE_FLOOR_GWEI): the 15 reference contracts `foga world deploy` added on Sepolia on
14
+ * 2026-11-01 took 111 M gas in all, 1.6 M to 14.6 M each (code deposit costs about 1,500 gas per byte there), so a
15
+ * contract is budgeted at the average with a margin of one half. Checked before anything is sent, so an unfunded
16
+ * deployer is told what it needs instead of failing on the first transaction.
17
+ */
18
+ export const DEPLOY_GAS_PER_STEP = 11000000n;
19
+ export function loadWorldConfig(path) {
20
+ const cfg = readJson(path);
21
+ if (cfg.schema !== "foga.world-config/1")
22
+ throw new Error(`unsupported world config schema in ${path}`);
23
+ return cfg;
24
+ }
25
+ function newWorldFile(cfg, bundle, mode, deployer) {
26
+ const batch = new Date().toISOString();
27
+ const compact = batch.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
28
+ return {
29
+ schema: "foga.world/1",
30
+ worldId: `${cfg.network}-${cfg.chainId}-${compact}`,
31
+ name: cfg.name,
32
+ network: cfg.network,
33
+ chainId: cfg.chainId,
34
+ mode,
35
+ bundleHash: bundle?.hash ?? null,
36
+ batch,
37
+ forkSourceBlock: null,
38
+ deployer,
39
+ identity8004: { mode: cfg.identity8004.mode === "stub" ? "stub" : "address", address: "0x0000000000000000000000000000000000000000", note: cfg.identity8004.note },
40
+ params: cfg.params,
41
+ contracts: {},
42
+ wiring: {},
43
+ steps: [],
44
+ notes: [...(cfg.notes ?? [])],
45
+ };
46
+ }
47
+ /** Pure transform for public chains: author the address book from the config, no transactions. */
48
+ export function attachWorld(cfg, bundle) {
49
+ if (cfg.mode !== "attach")
50
+ throw new Error(`world ${cfg.name} is not an attach-mode config`);
51
+ if (cfg.identity8004.mode !== "address")
52
+ throw new Error("attach-mode worlds must name the ERC-8004 Identity Registry address");
53
+ const world = newWorldFile(cfg, bundle, "attached", null);
54
+ world.identity8004 = { mode: "official", address: cfg.identity8004.address, note: cfg.identity8004.note };
55
+ for (const [id, c] of Object.entries(cfg.contracts ?? {})) {
56
+ world.contracts[id] = { ...c, address: normalizeAddress(c.address, id) };
57
+ world.steps.push({ id, status: "attached", note: c.note });
58
+ }
59
+ world.contracts["identity8004"] = { address: normalizeAddress(cfg.identity8004.address, "identity8004"), note: cfg.identity8004.note ?? "official ERC-8004 Identity Registry" };
60
+ world.identity8004.address = world.contracts["identity8004"].address;
61
+ if (cfg.expectations) {
62
+ world.wiring.expectations = Object.fromEntries(Object.entries(cfg.expectations).map(([k, v]) => [k, /^0x[0-9a-fA-F]{40}$/.test(v) ? normalizeAddress(v, k) : v]));
63
+ }
64
+ const kinds = bundle?.extras?.["ard-onchain-framework"]?.kindIds?.kinds;
65
+ if (kinds)
66
+ world.wiring["8444/kindIds"] = kinds;
67
+ return world;
68
+ }
69
+ /** Accept any casing (upstream records are not all EIP-55) and return the checksummed form. */
70
+ export function normalizeAddress(address, label) {
71
+ const lower = address.toLowerCase();
72
+ if (!isAddress(lower))
73
+ throw new Error(`${label}: bad address ${address}`);
74
+ return getAddress(lower);
75
+ }
76
+ /** Expand ${ENV_VAR} references in an RPC URL. */
77
+ export function resolveRpc(rpc) {
78
+ return rpc.replace(/\$\{([A-Z0-9_]+)\}/g, (_m, name) => {
79
+ const v = process.env[name];
80
+ if (!v)
81
+ throw new Error(`RPC URL references ${name}, which is not set`);
82
+ return v;
83
+ });
84
+ }
85
+ export async function deployWorld(opts) {
86
+ const root = repoRoot();
87
+ const cfg = loadWorldConfig(opts.worldConfigPath);
88
+ const bundle = loadBundle(opts.bundlePath);
89
+ const outPath = opts.outPath ?? join(root, "deployments", `${cfg.name}.json`);
90
+ // An attach-mode config lists what exists upstream. Plan steps it does not list are deployed from the bundle by
91
+ // the deployer (W): a reference instance upstream never deployed, or one deployed at another revision than the
92
+ // locked source. A config that lists everything needs no RPC and no key.
93
+ const toDeploy = cfg.mode === "attach" ? missingPlanSteps(cfg) : [];
94
+ if (cfg.mode === "attach" && toDeploy.length === 0) {
95
+ const world = attachWorld(cfg, bundle);
96
+ writeJson(outPath, world);
97
+ log.info(`attached world ${world.worldId}: ${Object.keys(world.contracts).length} contracts -> ${relative(root, outPath)}`);
98
+ return { world, path: outPath };
99
+ }
100
+ const rpc = opts.rpc ?? resolveRpc(cfg.rpc);
101
+ const provider = new PinnedProvider(rpc, undefined, { staticNetwork: true, cacheTimeout: -1 });
102
+ provider.pollingInterval = cfg.network === "anvil" ? 250 : 4_000;
103
+ const net = await provider.getNetwork();
104
+ await provider.getBlockNumber(); // seeds the read pin at the head (kernel/provider.ts)
105
+ if (Number(net.chainId) !== cfg.chainId) {
106
+ const msg = `chain id mismatch: rpc reports ${net.chainId}, world config says ${cfg.chainId}`;
107
+ if (!opts.allowChainMismatch)
108
+ throw new Error(msg + " (pass --allow-chain-mismatch for forks)");
109
+ log.warn(msg);
110
+ }
111
+ const pk = deployerKey(cfg);
112
+ if (!pk)
113
+ throw new Error(`no deployer key: set ${cfg.deployer?.privateKeyEnv ?? "FOGA_DEPLOYER_PK"}, or FOGA_MNEMONIC / FOGA_MNEMONIC_FILE (the deployer is then W, the phrase's first account)`);
114
+ // NonceManager keeps nonces strictly sequential even when a node mines instantly.
115
+ const wallet = new NonceManager(new Wallet(pk, provider));
116
+ const deployer = await wallet.getAddress();
117
+ const balance = await provider.getBalance(deployer);
118
+ log.info(`world ${cfg.name} on chain ${net.chainId} via ${rpc}; deployer ${deployer} (balance ${Number(balance) / 1e18} ETH)`);
119
+ let world;
120
+ if (opts.resume && exists(outPath)) {
121
+ world = readJson(outPath);
122
+ if (world.chainId !== Number(net.chainId) && !opts.allowChainMismatch)
123
+ throw new Error(`cannot resume ${outPath}: it belongs to chain ${world.chainId}`);
124
+ log.info(`resuming world ${world.worldId} from ${relative(root, outPath)}`);
125
+ }
126
+ else if (cfg.mode === "attach") {
127
+ world = attachWorld(cfg, bundle);
128
+ world.notes.push(`${toDeploy.length} reference contracts the upstream records do not provide at the locked revision are deployed from the bundle by ${deployer}: ${toDeploy.join(", ")}`);
129
+ }
130
+ else {
131
+ world = newWorldFile(cfg, bundle, "deployed", deployer);
132
+ }
133
+ world.deployer = deployer;
134
+ world.bundleHash = bundle.hash;
135
+ if (cfg.mode === "attach")
136
+ log.info(`attach-mode world: ${Object.keys(cfg.contracts ?? {}).length} contracts attached; deploying ${toDeploy.length} from the bundle (${toDeploy.join(", ")})`);
137
+ // What the deployer must hold for the contracts not yet in the world file, before the first one is sent.
138
+ const remaining = DEPLOY_PLAN.filter((s) => !world.contracts[s.id] && !(s.id === "identity8004" && cfg.identity8004.mode === "address")).length;
139
+ if (remaining > 0) {
140
+ const price = await quotePriceOf(provider);
141
+ const need = BigInt(remaining) * DEPLOY_GAS_PER_STEP * price;
142
+ const priced = `${remaining} contracts x ${Number(DEPLOY_GAS_PER_STEP) / 1e6} M gas at ${Number(price) / 1e9} gwei (${price === feeFloor() ? "the fee floor FOGA_FEE_FLOOR_GWEI; the chain quotes less" : "the chain's quote"}) = ${formatEther(need)} ETH`;
143
+ if (balance < need)
144
+ throw new Error(`deployer ${deployer} holds ${formatEther(balance)} ETH, the deployment is budgeted at ${priced}. Fund it and run again${world.steps.some((s) => s.status === "deployed") ? " with --resume" : ""}.`);
145
+ log.info(`deployment budget: ${priced}; the deployer holds ${formatEther(balance)} ETH`);
146
+ }
147
+ const stepById = new Map(DEPLOY_PLAN.map((s) => [s.id, s]));
148
+ const ctx = {
149
+ config: cfg,
150
+ bundle,
151
+ world,
152
+ deployer,
153
+ addr: (id) => {
154
+ const c = world.contracts[id];
155
+ if (!c)
156
+ throw new Error(`step ${id} has no address yet (plan order?)`);
157
+ return c.address;
158
+ },
159
+ };
160
+ const gasMultiplier = opts.gasMultiplier ?? 1;
161
+ for (const step of DEPLOY_PLAN) {
162
+ const existing = world.contracts[step.id];
163
+ if (existing && cfg.mode === "attach" && cfg.contracts?.[step.id]) {
164
+ log.step(`${step.id}: attached at ${existing.address}`);
165
+ continue;
166
+ }
167
+ if (existing && (await provider.getCode(existing.address)) !== "0x") {
168
+ log.step(`${step.id}: already at ${existing.address}`);
169
+ continue;
170
+ }
171
+ const skip = step.skip?.(ctx);
172
+ if (skip) {
173
+ if (step.id === "identity8004" && cfg.identity8004.mode === "address") {
174
+ world.contracts[step.id] = { address: cfg.identity8004.address, note: cfg.identity8004.note ?? "existing ERC-8004 Identity Registry" };
175
+ world.identity8004 = { mode: "address", address: cfg.identity8004.address, note: cfg.identity8004.note };
176
+ }
177
+ world.steps.push({ id: step.id, status: "skipped", note: skip });
178
+ writeJson(outPath, world);
179
+ log.step(`${step.id}: skipped (${skip})`);
180
+ continue;
181
+ }
182
+ const art = getContract(bundle, step.bundleKey);
183
+ if (!art.deployable)
184
+ throw new Error(`${step.bundleKey} is not deployable (kind ${art.kind}, abstract ${art.abstract})`);
185
+ const libraries = {};
186
+ const linked = linkBytecode(art.bytecode, art.linkReferences, (_src, lib) => {
187
+ const libStep = DEPLOY_PLAN.find((s) => s.bundleKey === `${art.repo}:${lib}`);
188
+ if (!libStep)
189
+ return undefined;
190
+ const a = world.contracts[libStep.id]?.address;
191
+ if (a)
192
+ libraries[lib] = a;
193
+ return a;
194
+ });
195
+ const args = step.args(ctx);
196
+ const factory = new ContractFactory(art.abi, linked, wallet);
197
+ const txReq = await factory.getDeployTransaction(...args);
198
+ const estimate = await provider.estimateGas({ ...txReq, from: deployer });
199
+ const gasLimit = BigInt(Math.ceil(Number(estimate) * gasMultiplier));
200
+ const contract = await factory.deploy(...args, { gasLimit });
201
+ const deployTx = contract.deploymentTransaction();
202
+ const receipt = await deployTx.wait();
203
+ const address = await contract.getAddress();
204
+ const rec = {
205
+ address,
206
+ bundleKey: step.bundleKey,
207
+ txHash: receipt?.hash,
208
+ block: receipt?.blockNumber,
209
+ args: args.map((a) => (typeof a === "bigint" ? a.toString() : a)),
210
+ libraries: Object.keys(libraries).length ? libraries : undefined,
211
+ note: cfg.mode === "attach" ? `deployed by FOGA from the bundle (not provided upstream at the locked revision)${step.note ? "; " + step.note : ""}` : step.note,
212
+ };
213
+ world.contracts[step.id] = rec;
214
+ if (step.id === "identity8004")
215
+ world.identity8004 = { mode: "stub", address, note: cfg.identity8004.note ?? "local stand-in, not the reference registry" };
216
+ world.steps.push({ id: step.id, status: "deployed", txHash: receipt?.hash });
217
+ writeJson(outPath, world);
218
+ log.step(`${step.id}: ${address} (gas ${receipt?.gasUsed}, block ${receipt?.blockNumber})`);
219
+ }
220
+ for (const wire of WIRE_PLAN) {
221
+ if (world.steps.some((s) => s.id === wire.id && s.status === "wired")) {
222
+ log.step(`${wire.id}: already wired`);
223
+ continue;
224
+ }
225
+ const skip = wire.skip?.(ctx);
226
+ if (skip) {
227
+ world.steps.push({ id: wire.id, status: "skipped", note: skip });
228
+ continue;
229
+ }
230
+ const hashes = [];
231
+ for (const call of wire.calls(ctx)) {
232
+ const target = world.contracts[call.stepId];
233
+ const step = stepById.get(call.stepId);
234
+ if (!target || !step)
235
+ throw new Error(`wiring ${wire.id}: unknown step ${call.stepId}`);
236
+ const art = getContract(bundle, step.bundleKey);
237
+ const c = new Contract(target.address, art.abi, wallet);
238
+ const tx = await c.getFunction(call.fn)(...call.args);
239
+ const receipt = await tx.wait();
240
+ hashes.push(receipt.hash);
241
+ log.step(`${wire.id}: ${call.fn}(${call.note ?? ""}) in ${receipt.hash}`);
242
+ }
243
+ world.wiring[wire.id] = { txHashes: hashes };
244
+ world.steps.push({ id: wire.id, status: "wired", txHash: hashes[hashes.length - 1] });
245
+ writeJson(outPath, world);
246
+ }
247
+ const kinds = bundle.extras["ard-onchain-framework"]?.kindIds?.kinds;
248
+ if (kinds)
249
+ world.wiring["8444/kindIds"] = kinds;
250
+ writeJson(outPath, world);
251
+ log.info(`world ${world.worldId}: ${Object.keys(world.contracts).length} contracts -> ${relative(root, outPath)}`);
252
+ return { world, path: outPath };
253
+ }
254
+ /**
255
+ * The deployer's key: the configured variable (FOGA_DEPLOYER_PK), else W as the genesis will see it (FOGA_KEY_W, or
256
+ * account 0 of the phrase in the environment), else the config's default. The genesis binds the escrow this deploys
257
+ * as W, so the deployer is W unless the variable says otherwise.
258
+ */
259
+ export function deployerKey(cfg) {
260
+ const fromEnv = cfg.deployer?.privateKeyEnv ? process.env[cfg.deployer.privateKeyEnv] : process.env.FOGA_DEPLOYER_PK;
261
+ if (fromEnv)
262
+ return fromEnv;
263
+ if (cfg.network !== "anvil") {
264
+ // on anvil the genesis derives W from anvil's phrase, so a phrase in the environment must not move the deployer
265
+ if (process.env.FOGA_KEY_W)
266
+ return process.env.FOGA_KEY_W;
267
+ const phrase = envMnemonic();
268
+ if (phrase)
269
+ return mnemonicKeys(phrase).W;
270
+ }
271
+ return cfg.deployer?.defaultPrivateKey;
272
+ }
273
+ /** Plan steps an attach-mode config does not list and does not skip: what `foga world deploy` adds from the bundle. */
274
+ export function missingPlanSteps(cfg) {
275
+ const listed = cfg.contracts ?? {};
276
+ return DEPLOY_PLAN.filter((s) => !listed[s.id] && !(s.id === "identity8004" && cfg.identity8004.mode === "address")).map((s) => s.id);
277
+ }
@@ -0,0 +1,93 @@
1
+ export const DEPLOY_PLAN = [
2
+ {
3
+ id: "identity8004",
4
+ bundleKey: "foga:Identity8004Stub",
5
+ args: () => [],
6
+ skip: (ctx) => (ctx.config.identity8004.mode === "address" ? "attached to an existing ERC-8004 Identity Registry" : null),
7
+ note: "LOCAL stand-in for the ERC-8004 Identity Registry; public worlds attach the official registry instead",
8
+ },
9
+ { id: "8338/SkillToken", bundleKey: "skill-token-standard:SkillToken", args: (c) => [c.config.params.skillToken.name, c.config.params.skillToken.symbol] },
10
+ { id: "8414/TaskToken", bundleKey: "task-token-standard:TaskToken", args: (c) => [c.config.params.taskToken.name, c.config.params.taskToken.symbol] },
11
+ { id: "8414/HashlockVerifier", bundleKey: "task-token-standard:HashlockVerifier", args: () => [] },
12
+ { id: "8419/KYASchemeRegistry", bundleKey: "kya-standard:KYASchemeRegistry", args: () => [] },
13
+ { id: "8419/KYARegistry", bundleKey: "kya-standard:KYARegistry", args: (c) => [c.addr("8419/KYASchemeRegistry")] },
14
+ { id: "8419/KYAPolicyRegistry", bundleKey: "kya-standard:KYAPolicyRegistry", args: (c) => [c.addr("8419/KYARegistry")] },
15
+ { id: "8419/ValidationRegistry8004", bundleKey: "kya-standard:ValidationRegistry8004", args: (c) => [c.addr("identity8004")], note: "spec-conforming ERC-8004 Validation Registry companion from kya-standard, bound to the world's identity registry" },
16
+ { id: "8419/KYABridge8004", bundleKey: "kya-standard:KYABridge8004", args: (c) => [c.addr("8419/KYARegistry"), c.addr("8419/ValidationRegistry8004"), c.deployer] },
17
+ { id: "8419/Groth16Verifier", bundleKey: "kya-standard:Groth16Verifier", args: () => [], note: "credential-pattern ZK-KYA verifier (kya_public_v1 circuit)" },
18
+ { id: "8419/Groth16KYAVerifierAdapter", bundleKey: "kya-standard:Groth16KYAVerifierAdapter", args: (c) => [c.addr("8419/Groth16Verifier"), c.config.params.kya.issuerSetRoot, c.config.params.kya.epochLength, c.config.params.kya.epochGrace] },
19
+ { id: "8434/AIDRegistry", bundleKey: "aid-standard:AIDRegistry", args: (c) => [c.config.params.aid.defaultLivenessWindow, c.config.params.aid.maxLivenessWindow] },
20
+ { id: "8436/ACDFPolicyRegistry", bundleKey: "acd-framework:ACDFPolicyRegistry", args: () => [] },
21
+ { id: "8436/ACDFRegistry", bundleKey: "acd-framework:ACDFRegistry", args: (c) => [c.addr("8436/ACDFPolicyRegistry")] },
22
+ { id: "8444/ARDOnchainRegistry", bundleKey: "ard-onchain-framework:ARDOnchainRegistry", args: () => [], note: "owner = deployer; the four kinds are registered by the wiring step" },
23
+ { id: "8444/SkillAdapter", bundleKey: "ard-onchain-framework:SkillAdapter", args: () => [] },
24
+ { id: "8444/TaskAdapter", bundleKey: "ard-onchain-framework:TaskAdapter", args: () => [] },
25
+ { id: "8444/AgentAdapter", bundleKey: "ard-onchain-framework:AgentAdapter", args: (c) => [c.addr("8434/AIDRegistry")], note: "bound to the world's ERC-8434 AIDRegistry (on Sepolia the upstream instance is bound to ARD's own fixture registry — see worlds/sepolia.json)" },
26
+ { id: "8444/RegistryAdapter", bundleKey: "ard-onchain-framework:RegistryAdapter", args: () => [] },
27
+ { id: "8447/SpecValidator", bundleKey: "swarm-ai-framework:SpecValidator", args: () => [], note: "external library" },
28
+ { id: "8447/LifecycleLib", bundleKey: "swarm-ai-framework:LifecycleLib", args: () => [], note: "external library, links SpecValidator" },
29
+ { id: "8447/MembershipLib", bundleKey: "swarm-ai-framework:MembershipLib", args: () => [], note: "external library" },
30
+ { id: "8447/ResourceLib", bundleKey: "swarm-ai-framework:ResourceLib", args: () => [], note: "external library" },
31
+ { id: "8447/SwarmRegistry", bundleKey: "swarm-ai-framework:SwarmRegistry", args: () => [[], []], note: "constructed with no resource kinds, as upstream did on Sepolia" },
32
+ { id: "8447/AllowlistEligibility", bundleKey: "swarm-ai-framework:AllowlistEligibility", args: (c) => [c.deployer] },
33
+ { id: "8447/ERC8004Eligibility", bundleKey: "swarm-ai-framework:ERC8004Eligibility", args: (c) => [c.addr("identity8004")] },
34
+ { id: "8447/IdentityStateEligibility", bundleKey: "swarm-ai-framework:IdentityStateEligibility", args: (c) => [c.addr("8434/AIDRegistry")] },
35
+ { id: "8447/SwarmTenderAdapter", bundleKey: "swarm-ai-framework:SwarmTenderAdapter", args: (c) => [c.addr("8447/SwarmRegistry"), c.addr("8414/TaskToken")] },
36
+ { id: "8454/AUFUnitRegistry", bundleKey: "aifi-unit-framework:AUFUnitRegistry", args: () => [] },
37
+ { id: "8454/AUFAssertionRegistry", bundleKey: "aifi-unit-framework:AUFAssertionRegistry", args: (c) => [c.addr("8454/AUFUnitRegistry")] },
38
+ { id: "8468/ASFEvaluation", bundleKey: "agent-safety-framework:ASFEvaluation", args: () => [], note: "external library" },
39
+ { id: "8468/ASFLifecycle", bundleKey: "agent-safety-framework:ASFLifecycle", args: () => [], note: "external library, links ASFEvaluation" },
40
+ { id: "8468/ASFSpecificationRegistry", bundleKey: "agent-safety-framework:ASFSpecificationRegistry", args: () => [] },
41
+ { id: "8468/ASFBindingRegistry", bundleKey: "agent-safety-framework:ASFBindingRegistry", args: () => [], note: "links ASFEvaluation and ASFLifecycle" },
42
+ { id: "8471/ADRFPolicyRegistry", bundleKey: "ai-dispute-resolution:ADRFPolicyRegistry", args: () => [] },
43
+ { id: "8471/ADRFBindingModule", bundleKey: "ai-dispute-resolution:ADRFBindingModule", args: (c) => [c.addr("8471/ADRFPolicyRegistry")] },
44
+ { id: "8471/ADRFFilingModule", bundleKey: "ai-dispute-resolution:ADRFFilingModule", args: (c) => [c.addr("8471/ADRFPolicyRegistry")] },
45
+ { id: "8471/ADRFResolutionModule", bundleKey: "ai-dispute-resolution:ADRFResolutionModule", args: (c) => [c.addr("8471/ADRFPolicyRegistry")] },
46
+ { id: "8471/ADRFDecisionModule", bundleKey: "ai-dispute-resolution:ADRFDecisionModule", args: (c) => [c.addr("8471/ADRFPolicyRegistry")] },
47
+ { id: "8471/ADRFRecordModule", bundleKey: "ai-dispute-resolution:ADRFRecordModule", args: (c) => [c.addr("8471/ADRFPolicyRegistry")] },
48
+ { id: "8471/ADRFOutcomeModule", bundleKey: "ai-dispute-resolution:ADRFOutcomeModule", args: (c) => [c.addr("8471/ADRFPolicyRegistry")] },
49
+ {
50
+ id: "8471/ADRFDisputeRegistry",
51
+ bundleKey: "ai-dispute-resolution:ADRFDisputeRegistry",
52
+ args: (c) => [
53
+ c.addr("8471/ADRFPolicyRegistry"),
54
+ c.addr("8471/ADRFBindingModule"),
55
+ c.addr("8471/ADRFFilingModule"),
56
+ c.addr("8471/ADRFResolutionModule"),
57
+ c.addr("8471/ADRFDecisionModule"),
58
+ c.addr("8471/ADRFRecordModule"),
59
+ c.addr("8471/ADRFOutcomeModule"),
60
+ ],
61
+ note: "router over six delegatecall modules (each unit fits Sepolia's per-transaction gas cap)",
62
+ },
63
+ { id: "8471/DuplicateSettlementVerifier", bundleKey: "ai-dispute-resolution:DuplicateSettlementVerifier", args: () => [] },
64
+ { id: "8471/DeclaredVersionVerifier", bundleKey: "ai-dispute-resolution:DeclaredVersionVerifier", args: () => [] },
65
+ { id: "8471/AttestorQuorumRule", bundleKey: "ai-dispute-resolution:AttestorQuorumRule", args: () => [] },
66
+ { id: "8471/ADRFACDFBridge", bundleKey: "ai-dispute-resolution:ADRFACDFBridge", args: (c) => [c.addr("8471/ADRFDisputeRegistry"), c.addr("8436/ACDFRegistry")] },
67
+ { id: "8471/ADRFServiceEscrow", bundleKey: "ai-dispute-resolution:ADRFServiceEscrow", args: (c) => [c.addr("8471/ADRFDisputeRegistry")], note: "service escrow example (obligation source + executor); bound to a policy by the genesis runner" },
68
+ ];
69
+ export const WIRE_PLAN = [
70
+ {
71
+ id: "8444/registerKinds",
72
+ // An attached registry was wired upstream (its owner key registered the kinds); only a registry this run deployed is wired here.
73
+ skip: (c) => (c.world.steps.some((s) => s.id === "8444/ARDOnchainRegistry" && s.status === "attached") ? "ARD registry attached: the kinds were registered upstream" : null),
74
+ calls: (c) => {
75
+ const kinds = c.bundle.extras["ard-onchain-framework"]?.kindIds?.kinds;
76
+ if (!kinds)
77
+ throw new Error("bundle extras lack ARD kind ids (ard-onchain-framework/kindIds)");
78
+ const uris = c.config.params.ard.kindURIs;
79
+ const adapters = {
80
+ skill: "8444/SkillAdapter",
81
+ task: "8444/TaskAdapter",
82
+ agent: "8444/AgentAdapter",
83
+ registry: "8444/RegistryAdapter",
84
+ };
85
+ return Object.entries(adapters).map(([kind, step]) => ({
86
+ stepId: "8444/ARDOnchainRegistry",
87
+ fn: "registerKind",
88
+ args: [kinds[kind], c.addr(step), uris[kind] ?? ""],
89
+ note: `kind ${kind} -> ${step}`,
90
+ }));
91
+ },
92
+ },
93
+ ];
@@ -0,0 +1 @@
1
+ export {};
package/docs/build.md ADDED
@@ -0,0 +1,45 @@
1
+ # Build: from ten repositories to one bundle
2
+
3
+ `foga build` turns the ten locked reference implementations (plus FOGA's local contracts) into `artifacts/bundle.json`, a single machine-independent file that every other command reads. Nothing else in FOGA needs the Solidity sources.
4
+
5
+ ## Inputs
6
+
7
+ - `vendor/LOCK.json` — repository URL and commit per ERC.
8
+ - `vendor/<repo>/` — materialized by `foga vendor sync` (a one-commit fetch of exactly the locked commit; the directory is replaced if it is at any other commit).
9
+ - `build/profiles.json` — how each repository is compiled:
10
+ - `native`: `forge build --ast --no-cache --skip test --skip script --out <foga out dir>` runs inside the vendored repository with its own `foundry.toml`, so optimizer, runs, via-IR, EVM version, metadata and remappings are exactly upstream's.
11
+ - `staged` (kya-standard, aid-standard, whose upstream toolchain is solc-js): the contract tree is copied into `.foga-build/stage/<repo>/` with a generated `foundry.toml` that mirrors the upstream compile script (`solc 0.8.28`, optimizer on, 200 runs, EVM `cancun`, `bytecode_hash = ipfs`). Source names inside the staged project equal the upstream script's source names.
12
+ - `local`: `contracts/` (FOGA's own stand-ins), built with `contracts/foundry.toml`.
13
+ - `build/known-interface-ids.json` — the ids 8338 and 8414 publish in their READMEs (those two repositories have no vectors file).
14
+
15
+ ## Compiler binaries
16
+
17
+ The Foundry release binaries are glibc builds that need GLIBC 2.35 or newer; on an older Linux (Amazon Linux 2/2023, CentOS 7, Debian 11) `scripts/install-foundry.sh` installs the statically linked ("alpine", musl) build of the pinned release into a directory of its own, and `FOGA_FOUNDRY_DIR` tells `foga demo` where its anvil is.
18
+
19
+ Foundry downloads solc from the Solidity binary mirror on demand. On a machine that cannot reach the mirror, run `scripts/install-solc.sh` (which fetches the three static Linux binaries from the Solidity GitHub releases) and export `FOGA_SOLC_DIR`; `foga build` then passes `--use <dir>/solc-<version>` to forge. The compiler commits used for the committed bundle are `0.8.24+commit.e11b9ed9`, `0.8.28+commit.7893614a` and `0.8.30+commit.73712a01` — the same commits recorded in the upstream Sepolia deployments.
20
+
21
+ ## What goes into the bundle
22
+
23
+ For every compilation target under a profile's `contractsRoot` (tests, scripts, `lib/` and vendored test fixtures are excluded):
24
+
25
+ | Field | Source |
26
+ |---|---|
27
+ | `abi`, `methodIdentifiers`, `bytecode`, `deployedBytecode` | forge artifact |
28
+ | `linkReferences`, `deployedLinkReferences`, `immutableReferences` | forge artifact; used for linking at deploy time and for masking at verification time |
29
+ | `kind` (contract / library / interface), `abstract`, `baseContracts` | AST |
30
+ | `interfaceId` | XOR of the selectors of the interface's own functions (AST `functionSelector`), the same rule as `type(I).interfaceId` |
31
+ | `compiler`, `settings` (optimizer, runs, viaIR, evmVersion, bytecodeHash, remappings), `sourceKeccak` | artifact metadata |
32
+ | `runtimeIdentity` | keccak256 of the runtime code after stripping the CBOR metadata trailer and zeroing link and immutable slots |
33
+ | `role` | heuristic from path and kind (registry, kernel, adapter, module, library, verifier, bridge, example, companion, mock, interface, local-stub) |
34
+
35
+ `interfaceIds` records, per repository, the computed id, the upstream-published id when there is one, and whether they match. `extras` carries small upstream vectors the world needs (ARD kind ids, SAI term ids, AUF identifiers).
36
+
37
+ ## The bundle hash
38
+
39
+ `hash` is the sha256 of a canonical JSON (sorted keys, no whitespace) of every contract's `abi`, `bytecode`, `deployedBytecode`, link and immutable references, `compiler`, `settings`, `sourceKeccak` and `upstreamPath`, plus the locked commits. It excludes timestamps and machine paths. `foga build --verify` rebuilds everything into `.foga-build/bundle.verify.json` and compares hashes; CI runs this on every push, so the committed bundle is always reproducible from the lock.
40
+
41
+ ## Determinism notes
42
+
43
+ - forge writes repository-relative source paths into the metadata, so builds are independent of the checkout location.
44
+ - The same solc commit produces the same bytecode; a different forge version does not change bytecode but may change the artifact JSON layout, which the extractor tolerates.
45
+ - `--no-cache` is passed so stale caches in a vendored tree cannot leak into the bundle.
@@ -0,0 +1,58 @@
1
+ # FogaCellAccount
2
+
3
+ `contracts/src/FogaCellAccount.sol` is the body of a cell: one contract account that is, at the same time, the cell's ERC-8434 anchor, its ERC-8004 owner, the holder of its assets and tokens, the fulfiller of record on ERC-8414 tenders, the adopter of its ERC-8468 binding, the KYA subject (`account` type), and the controller of its ARD records and AUF units. Everything the cell does on chain is a call made by this address; a call made by the operator key directly is the operator's own act, not the cell's (design memo, section 8.2).
4
+
5
+ The account carries the execution-gate semantics of the ERC-8468 reference's `ASFGuardedAccount` — a requirement fixed at deployment, bindings presented by the owner, a de-risk allow-list that stays open under a derisk-only suspension — and adds what a cell needs on top. It is compiled with via-IR (solc 0.8.30, cancun) and shipped in the bundle as `foga:FogaCellAccount`. `ACCOUNT_VERSION` is 2 since 0.6.0: revision 2 closes the operator's scope to an allow-list (below). The cells of the 0.5.0 Sepolia run are revision-1 accounts; a revision-1 account cannot be upgraded in place, so a cell that is to run as a public node moves to a revision-2 account the way T9 moves A to A-prime (docs/design-memo.md, Phase 3).
6
+
7
+ ## Roles
8
+
9
+ | Role | Held by | Powers |
10
+ |---|---|---|
11
+ | `owner` | the cold principal (on the Finch OG Agent: the author's key; on anvil: a derived account) | set operator, guardian, recovery and AID registry; adopt and present bindings; set selector classes; transfer ownership; `execute`; `sweep` |
12
+ | `operator` | the node's hot signer | `execute`, within scope: only (target, selector) pairs the owner allow-listed (`operatorAllowed`), never an asset-moving selector, optional expiry (`operatorValidUntil`), a per-day cap on native value sent (`operatorDailyValueCap`); ERC-1271 signatures only when the owner enabled them (`operatorMaySign`) |
13
+ | `guardian` | the intervener named in the cell's safety binding | nothing on the account itself: the guardian acts on the ERC-8468 Binding Registry (`suspend`, `resume`), and the gate reads the result |
14
+ | `recovery` | defaults to the owner | `sweep` to the owner, at any time |
15
+
16
+ ## The operator's scope (revision 2)
17
+
18
+ The operator key is the one key a running node holds, so what it can do through the account is what an attacker holding it can do. Revision 1 bounded that by expiry and a daily value cap only: any target, any selector. Revision 2 adds an allow-list and a signing switch, both owner-administered:
19
+
20
+ - `operatorAllowed[target][selector]` — `setOperatorAllowed(target, selector, allowed)` and `setOperatorAllowedBatch(targets, selectors, allowed)` (`OperatorAllowedSet` per entry). A call by the operator whose (target, selector) is not on the list reverts with `OperatorNotAllowed(target, selector)` before the gate is consulted. A fresh account allows nothing: the owner opens the scope after `setOperator`, and the genesis does it in T1 (`T1.<X>.scope`) from the groups in `src/genesis/operator-scope.ts` — identity, standing, skills-provider, labour, demander, organization-member, organization-coordinator, service-provider, service-client — so a cell's allow-list reads as the roles it plays, and opens two more entries for B in T4 when the swarm's decision adapter and account exist (`T4.B.scope.*`). The selector is the first four bytes of `data`; a call with less than four bytes of data has selector `0x00000000` and must be allow-listed as such.
21
+ - Asset-moving selectors are closed to the operator by construction, allow-listed or not: `transfer`, `transferFrom`, both ERC-721 `safeTransferFrom`s, the ERC-1155 `safeTransferFrom` and `safeBatchTransferFrom`, `approve`, `setApprovalForAll`, `increaseAllowance`, `permit`. Allow-listing one reverts with `OperatorAssetSelector`, and so does an operator call carrying one. Moving the cell's assets — the ERC-8004 identity and the skill tokens A hands to A-prime in T9, for instance — is the owner's act (`execute` by the owner, which is never scope-checked, or `sweep`).
22
+ - `operatorMaySign` (`setOperatorMaySign`, `OperatorSigningSet`) — off by default. While off, an operator signature is not the account's signature (ERC-1271 below), so a relayed, signed path (`bindWithSig`, `joinBySig`, `assertStatementBySig`, signed ballots) is not open to a key that only holds `execute` rights.
23
+
24
+ The order of checks for an operator call is: expiry, asset selector, allow-list, daily value cap, then the gate below. The owner's calls pass none of the operator checks and all of the gate.
25
+
26
+ ## The gate
27
+
28
+ `execute(target, value, data)` is the only way to act as the cell. Every call passes `_gate`:
29
+
30
+ 1. The presented bindings must be as many as the requirement has clauses; before the first `adoptBinding` nothing executes (`BindingsNotPresented`). There is no privileged path around this, the owner included.
31
+ 2. `safety.check(requirement, presentedBindings)` must return `Satisfied`. `NotSatisfied` passes only when every failing clause is a suspension (own, inherited, or a standing stop by a named intervener) whose response set keeps de-risking open and is not a full stop, **and** the selector is on the owner's de-risk allow-list for that target. `Unknown` (the specification registry unreachable) never passes.
32
+ 3. A selector the owner marked as commitment-class (`setCommitmentClass`) additionally requires `aid.state(address(this)) == ACTIVE` in the configured ERC-8434 registry — "no delivery while not ACTIVE" is enforced, not promised. De-risk calls skip this clause by construction.
33
+
34
+ The genesis marks `TaskToken.submitFulfillment`, `SwarmRegistry.join`, `SwarmRegistry.applyToJoin`, `AUFAssertionRegistry.assertStatement` and `ACDFRegistry.castBallot` as commitment-class, and `TaskToken.withdrawCredit`, `TaskToken.reclaimEscrow`, `SwarmRegistry.leave` and `ADRFServiceEscrow.withdrawBalance` as de-risk actions. T7 of the genesis exercises the gate on chain: a transaction sent around the kernel fails with `BindingNotSatisfied` under a suspension, the allow-listed withdrawal passes, a fresh binding does not escape the intervener's standing stop, and a STALE anchor stops commitment-class calls until the next heartbeat (docs/genesis.md).
35
+
36
+ ## Adopting the binding
37
+
38
+ Adoption is the bootstrap and it is owner-level: `adoptBinding(input, clauseIndex)` calls `adopt` on the Binding Registry with the account as `msg.sender` (so the registry records the account as adopter) and presents the returned id for the given clause. The operator cannot swap bindings; re-adoption after a termination overwrites the slot. `presentBindings` remains for re-pointing an existing set.
39
+
40
+ Because the requirement names the account as adopter and subject, the deploying kernel predicts the account's address from the deployer's nonce (`getCreateAddress`) and checks the receipt against it.
41
+
42
+ ## Signatures and receivers
43
+
44
+ `isValidSignature` accepts a 65-byte ECDSA signature by the owner always, and one by the operator only when `operatorMaySign` is on, the operator has not expired and the safety gate is satisfied at that moment (bindings presented and the registry's verdict `Satisfied`): a suspension that stops `execute` stops the signed path too, where revision 1 checked the expiry alone. That makes `bindWithSig` (ERC-8434), `joinBySig` (ERC-8447), `assertStatementBySig` (ERC-8454) and signed ballots (ERC-8436) available to a cell whose owner opens them. The account implements the ERC-721 and ERC-1155 receiver hooks and ERC-165 for 1271 and both receivers.
45
+
46
+ ## Sweep
47
+
48
+ `sweep(token, amount)` moves native value or an ERC-20 balance to the owner. It is open to the owner and the recovery address and is not gated: it can only reduce the account's exposure, which is what `RESTRICT_DERISK_ONLY` is meant to keep open.
49
+
50
+ ## Errors
51
+
52
+ `NotOwner`, `NotOperatorOrOwner`, `NotOwnerOrRecovery`, `OperatorExpired(validUntil)`, `OperatorNotAllowed(target, selector)`, `OperatorAssetSelector(target, selector)`, `LengthMismatch` (a batch whose arrays differ in length), `DailyValueCapExceeded(cap, spent, requested)`, `BindingsNotPresented(presented, required)`, `ClauseIndexOutOfRange(index, clauses)`, `BindingNotSatisfied(verdict, reasons)`, `AnchorNotActive(state)`, `CallFailed(data)` (the inner revert data of the target call), `ZeroAddress`. The kernel's revert decoder unwraps `CallFailed` so a refused call reports the real cause (`CallFailed(Error("TaskToken: verification failed"))`).
53
+
54
+ ## Tests
55
+
56
+ `contracts/test/FogaCellAccount.t.sol` (17 tests, `forge test`): constructor fixes requirement and roles; nothing executes before bindings are presented; `adoptBinding` is owner-only, presents the binding and makes the account the adopter; operator executes when satisfied; owner executes, strangers cannot; the gate blocks `NotSatisfied` for operator and owner alike; de-risk allow-list under a derisk-only suspension, full stop blocks even allow-listed calls; `Unknown` blocks; commitment class requires an ACTIVE anchor (DORMANT and STALE refused); operator expiry and the daily cap, reset on the next day; target reverts bubble; the allow-list is the operator's only door (a fresh account refuses with `OperatorNotAllowed` before the gate, the batch opens and closes entries, the owner is not scope-checked); asset selectors are closed to the operator and cannot be allow-listed; ERC-1271 for the owner always and for the operator only when enabled, in scope and satisfied; sweep by owner or recovery only, open under a full stop; receivers and ERC-165; owner-only administration.
57
+
58
+ Timestamps in the tests are absolute: with via-IR the compiler may cache `block.timestamp` within a call, so `block.timestamp + x` after a `vm.warp` can still read the pre-warp value.
@@ -0,0 +1,38 @@
1
+ # Compatibility matrix
2
+
3
+ Compatibility is declared per ERC in three parts: the specification version FOGA follows, the role FOGA takes, and the run evidence that proves it. An interface that merely exists is never counted as a pass. Verdicts use the doctor's vocabulary: Pass / Fail / Unknown / Not tested / Not applicable.
4
+
5
+ Status key for the evidence column: **L1–L4** = doctor layers 1–4 pass on a locally deployed world (`deployments/local.doctor.example.json`); **L5** = semantic validation through the genesis scenario (`deployments/local.genesis.example.json`, T0–T9; `foga doctor --genesis` attributes its checks per standard, all eleven Pass in `deployments/local.doctor.example.json`). The same run on an attach-mode world (`deployments/local-attach.example.json`: the address book attached, the ERC-8471 stack and two eligibility modules added by `foga world deploy`, a second ERC-8434 registry as the ARD backing registry) is the rehearsal of the public-chain path, all eleven Pass in `deployments/local-attach.doctor.example.json`. **Sepolia (2026-11-01)**: the same scenario against the upstream reference deployments, completed from the bundle (`deployments/sepolia.example.json`, world `sepolia-11155111-20261101T043153Z`): 203 operations, the evidence in `deployments/sepolia.genesis.example.json` (the final replay of the completed run: 114 checks remade against the chain, 47 recorded as skipped because the run itself changed that state later, 0 failed) and the doctor in `deployments/sepolia.doctor.example.json` — L1–L4 Pass for the 51 contracts but the one known layer-3 Fail of the ARD fixture registry, L5 Pass for all eleven standards (docs/genesis.md, "The Sepolia run"). One thing the local run exercises was not exercised there: the predicate-pattern proof of ERC-8419 (the proving material was not built on the machine that ran it; recorded as skipped). Everything else was, against the real instances: the ERC-8004 checks against the official Identity Registry, the ERC-8434 checks against two registries (the reference instance and ARD's fixture as backing registry), the ERC-8471 checks against FOGA's revision-4 instance.
6
+
7
+ | ERC | Specification followed | FOGA role(s) | Minimum run acceptance (Phase 2 target) | Evidence today |
8
+ |---|---|---|---|---|
9
+ | 8338 Token-Bound Executable Skills | repository commit `359ad64` (Draft, PR #1879) | holder, executor, publisher of skill tokens | reads the on-chain commitment, verifies the full package before executing; a tampered file is rejected; publishing v2 does not move an existing v1 commitment | L1–L4 Pass; L5: T2 mint ×2 by the account, package verified against `skillOf` before T3's run, tampered package detected; T6 v2 published (`updateSkill`, prev = v1) while a call stays committed to v1; T9 the token transferred to the successor and its ARD record adopted again |
10
+ | 8414 Token-Bound Task Tenders | commit `b3da62e` (Draft, PR #2005) | fulfiller; demander | creates and funds a real tender; submits; one machine settlement; repeated requests do not pay twice; a judged tender and the NoDecision default path | L1–L4 Pass; L5: T3 mint/fund/commit by B, submit/settle by A, paid once, second settlement and further submission refused, crash-and-resume does not resubmit; T4/T5 judged tenders accepted through the council's adapter and, on silence, by `claimUnjudged` — the NoDecision default path; T7 a two-completion tender: the operator key's direct submission is accepted by the chain but attributed to the key, the account's own is accepted and paid |
11
+ | 8419 KYA Framework | commit `f204f2e` (Draft, PR #2012) | subject; prover; issuer (world operator) | attested and two proved paths (credential-pattern, predicate-pattern) run; expiry, revocation, wrong binding and replay are rejected | L1–L4 Pass; L5: T0 schemes S_att and S_cred registered, T1 attested level 2 for three account subjects, `check` true; T1 credential-pattern proof for A through FOGA's Groth16 adapter (`attestWithProof`, `check` level 4 true); replayed, re-targeted and stale-epoch proofs refused; T4 revocation suspends a swarm seat on challenge; T3 predicate-pattern proof over A's first accepted completion through FOGA's circuit and adapter (level 1, anchor = source-set root), a false predicate (K above the count) refused, a flipped result bit refused by the verifier, replay, subject, scheme and stale epoch refused; T9 standing is not inherited by the successor |
12
+ | 8434 Agent Identity | commit `1ff7215` (Draft, PR #2044) | anchor | ERC-8004 binding; ACTIVE / STALE / RETIRED transitions; document keeps provenance, validity and proof class; histories are not merged | L1–L4 Pass; L5: T1 bind/heartbeat/document by three accounts (ACTIVE), T2 document update with a skills facet; T7 the anchor shortens its own liveness window, STALE once it passes stops commitment-class calls at the account's gate, heartbeat restores ACTIVE and the default window is restored; where a world names a backing registry (Sepolia's ARD fixture instance) the cells bind, heartbeat, take over and retire there too; T9 transfer of the agent turns A STALE, the successor's takeover bind, A RETIRED with successor, a retired anchor cannot heartbeat; histories not merged |
13
+ | 8436 ACDF | commit `f8204fb` (Draft, merged 2026-10-14) | consumer (through adapters); judges | a real 2-of-3 Final; enactment only after authorization check; NoDecision as a distinct outcome | L1–L4 Pass; L5: T0 FOGA-Council registered; T5 a real 2-of-3 Final (Provisional, appeal window, finalize) enacted through the tender adapter after its own authorization check; a NoDecision case left to the kernel's clock; the workers' 2-of-2 decision through the swarm decision adapter; wrong-round, non-judge and premature execution refused; T6 the council as judgment tier of an ADRF case through the bridge (Final Decided No imported with provenance) |
14
+ | 8444 ARD Onchain | commit `2641685` (Draft, merged 2026-10-15) | controller; discoverer | an external node finds a resource and checks descriptor and anchor; controller change and stale descriptors are detectable; adoption after transfer | L1–L4 Pass (four kinds registered and verified); L5: T1 agent records, T2 skill records, T3 task record, all `backed`; A discovers T3's task through its record; T9 a new agent record for the successor (the retired anchor's record is no longer backed), the skill record's controller follows the token, adoption by recommitting the same descriptor |
15
+ | 8447 Swarm AI | commit `de5bc29` (Draft, PR #2081) | member; coordinator; SwarmAccount operator | distinct identities accept one charter; roles take effect; a cooperative tender is completed; leave and inactivity handling | L1–L4 Pass; L5: T4 three identities accept one charter under a composite eligibility (8004 agent, AID ACTIVE, KYA level), roles take effect (REPRESENT by the coordinator role only), the swarm account fulfils and is paid, the swarm concludes ACHIEVED from the tender's record; challenge suspends and reinstates; T9 the seat stays with the account that joined; leave and inactivity Not tested |
16
+ | 8454 AUF | commit `1be1da6` (Draft, PR #2095) | definition controller; evaluator | cell and swarm units defined; income, gas and missing items recorded; realized net cash flow measured, never presented as value; no double counting | L1–L4 Pass; L5: T1 cell units ×3 and T2 skill units ×2 registered by the accounts; T4 the swarm unit containing the member units at pinned versions; T8 the realized-net-cash-flow method registered by E, observation receipts for cell A (T3–T7) and the swarm (T4–T5) asserted by E and recomputed by the resolver; a receipt counting the swarm's tender for A fails the boundary and once rules; an unregistered method is refused; no receipt states a value |
17
+ | 8468 ASF | commit `a76071c` (Draft, PR #2113) | adopter; guardian; execution gate | out-of-scope actions rejected at the execution gate; suspension blocks; a second binding cannot bypass an effective stop | L1–L4 Pass; L5: T0 specification, T1 adoption by three accounts with G as intervener, `status()` Satisfied, gate exercised on every act; T7 the guardian's suspension enforced on chain at the gate against a transaction sent around the kernel, the de-risk allow-list open under it, a fresh binding refused by the standing stop, the adopter unable to lift the intervener's suspension, resume; T9 the successor adopts its own binding |
18
+ | 8471 ADRF | commit `cd5a3e0` (Draft, PR #2123) | party; obligation source (escrow); executor | file, evidence, judgment, ruling, enactment with report; repeated enactment rejected; a conforming delivery yields no breach | L1–L4 Pass (on Sepolia the stack is FOGA's revision-4 instance deployed from the bundle, the upstream deployment being revision 3 — docs/version-lock.md seam 3); L5: T0 FOGA-Service-v1 registered and the world's escrow bound to it; T6 a service call committed to v1 delivered as v2, filed by the client (stranger, self-filing and duplicates refused), formal finding by the declared-version verifier, judged finding by FOGA-Council through the bridge, Final Sustained with the refund allowed, enacted once by the escrow and reported, a second enactment refused; a conforming call released to the provider |
19
+ | 8004 Trustless Agents | official registries (Draft) | registrant | register, agentURI, transfer; local stub and public instance always labeled apart | L1–L4 Pass against the local stub; L5: T1 `register(agentURI)` by three accounts against the stub; T9 `transferFrom` through the account moves the agent to the successor (the stub clears the wallet as the official registry does). On Sepolia the same against the official registry `0x8004A818…`: agents 11255–11257 registered by the cells' accounts in T1, agent 11255 transferred to A-prime in T9 (layer 2 Pass for its ERC-165 and ERC-721 declarations; layer 3 Not applicable, not built by FOGA) |
20
+
21
+ ## Interface identifiers
22
+
23
+ Computed from each interface's own function selectors in the compiled AST and compared with the identifiers the upstream repositories publish in their vectors (or README for 8338/8414). At the locked commits every published identifier matches the computed one; `artifacts/bundle.json` → `interfaceIds` holds the full table, including interfaces upstream publishes no id for (shims and helpers).
24
+
25
+ | ERC | Interface | Id |
26
+ |---|---|---|
27
+ | 8338 | ISkillToken / IOnchainSkillDocument | `0x734553a6` / `0x7050dd2c` |
28
+ | 8414 | ITaskToken / ITaskTender / ITaskVerifier / IOnchainTaskDocument | `0xcdaeb26d` / `0xc319d532` / `0x9977db15` / `0xeb078d05` |
29
+ | 8419 | IKYASchemeRegistry / IKYARegistry / IKYAPolicyRegistry / IKYAPolicyEvaluator / IKYAVerifier | `0xe6dbd933` / `0x5d1bec61` / `0x1cf9e558` / `0x173ff2b6` / `0xfc2b4271` |
30
+ | 8434 | IAIDRegistry | `0x72750a54` |
31
+ | 8436 | IACDFPolicyRegistry / IACDFRegistry | `0xb362eb4e` / `0xd31aae30` |
32
+ | 8444 | IARDOnchainRegistry / IARDKindAdapter | `0x881908a5` / `0x67bde273` |
33
+ | 8447 | ISwarmRegistry / IEligibility / ISwarmAccount / IResourceAdapter | `0x085203a4` / `0xd0ede3d8` / `0x25e98353` / `0x8e20b298` |
34
+ | 8454 | IAUFUnitRegistry / IAUFAssertionRegistry | `0x7128ef29` / `0x46ae6e24` |
35
+ | 8468 | IASFSpecificationRegistry / IASFBindingRegistry | `0x9bddde77` / `0xcff7f826` |
36
+ | 8471 | IADRFPolicyRegistry / IADRFDisputeRegistry / IADRFObligationSource / IADRFVerifier / IADRFEvidenceRule / IADRFJudgmentMethod | `0x4413df85` / `0xedc59c17` / `0xa52f2870` / `0xdfed0fe7` / `0x7ff3f359` / `0xc29a559d` |
37
+
38
+ Note that 8454 and 8468 derive ids without `supportsInterface`, while 8447 and 8471 include it; the computed values follow each repository's own declaration, which is what `type(I).interfaceId` yields.