@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.
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +216 -2
- package/artifacts/bundle.json +74250 -0
- package/artifacts/bundle.sha256 +1 -0
- package/bin/foga.js +14 -0
- package/build/known-interface-ids.json +18 -0
- package/build/profiles.json +120 -0
- package/dist/build/bundle.js +417 -0
- package/dist/build/vendor.js +74 -0
- package/dist/bundle/load.js +24 -0
- package/dist/bundle/types.js +1 -0
- package/dist/cli-cell.js +395 -0
- package/dist/cli.js +505 -0
- package/dist/core/acdf.js +162 -0
- package/dist/core/adrf.js +273 -0
- package/dist/core/aid.js +83 -0
- package/dist/core/ard.js +146 -0
- package/dist/core/asf.js +157 -0
- package/dist/core/canonical.js +79 -0
- package/dist/core/erc8004.js +52 -0
- package/dist/core/kya.js +225 -0
- package/dist/core/measure.js +96 -0
- package/dist/core/pack/cbor.js +130 -0
- package/dist/core/pack/skillpack.js +142 -0
- package/dist/core/skills.js +40 -0
- package/dist/core/swarm.js +206 -0
- package/dist/core/tenders.js +121 -0
- package/dist/core/units.js +122 -0
- package/dist/core/world.js +104 -0
- package/dist/demo/demander.js +76 -0
- package/dist/demo/evidence.js +1 -0
- package/dist/demo/loop-d.js +449 -0
- package/dist/doctor/checks.js +51 -0
- package/dist/doctor/doctor.js +385 -0
- package/dist/doctor/loop-d.js +103 -0
- package/dist/doctor/probe.js +97 -0
- package/dist/doctor/semantic.js +83 -0
- package/dist/genesis/extract.js +64 -0
- package/dist/genesis/funding.js +126 -0
- package/dist/genesis/operator-scope.js +90 -0
- package/dist/genesis/preflight.js +54 -0
- package/dist/genesis/probe.js +164 -0
- package/dist/genesis/roles.js +96 -0
- package/dist/genesis/run.js +71 -0
- package/dist/genesis/scenario.js +765 -0
- package/dist/genesis/skill-runner.js +103 -0
- package/dist/genesis/skill-worker.js +41 -0
- package/dist/genesis/t1-zk.js +62 -0
- package/dist/genesis/t3-zk.js +105 -0
- package/dist/genesis/t4.js +259 -0
- package/dist/genesis/t5.js +168 -0
- package/dist/genesis/t6.js +222 -0
- package/dist/genesis/t7.js +198 -0
- package/dist/genesis/t8.js +189 -0
- package/dist/genesis/t9.js +188 -0
- package/dist/genesis/zk.js +128 -0
- package/dist/http/server.js +276 -0
- package/dist/kernel/cell.js +104 -0
- package/dist/kernel/errors.js +78 -0
- package/dist/kernel/kernel.js +251 -0
- package/dist/kernel/provider.js +125 -0
- package/dist/kernel/store.js +470 -0
- package/dist/kernel/tx.js +385 -0
- package/dist/mcp/server.js +170 -0
- package/dist/node/config.js +195 -0
- package/dist/node/init.js +200 -0
- package/dist/node/keys.js +95 -0
- package/dist/node/migrate.js +173 -0
- package/dist/node/model/anthropic.js +97 -0
- package/dist/node/model/stub.js +129 -0
- package/dist/node/model/types.js +9 -0
- package/dist/node/node.js +198 -0
- package/dist/node/policy.js +95 -0
- package/dist/node/runner.js +256 -0
- package/dist/node/watchers.js +88 -0
- package/dist/tools/admin.js +184 -0
- package/dist/tools/catalog.js +261 -0
- package/dist/tools/common.js +143 -0
- package/dist/tools/identity.js +107 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/read.js +471 -0
- package/dist/tools/tenders.js +400 -0
- package/dist/tools/types.js +4 -0
- package/dist/util/bytecode.js +123 -0
- package/dist/util/fs.js +58 -0
- package/dist/util/hash.js +4 -0
- package/dist/util/log.js +11 -0
- package/dist/util/version.js +12 -0
- package/dist/world/deploy.js +277 -0
- package/dist/world/plan.js +93 -0
- package/dist/world/types.js +1 -0
- package/docs/build.md +45 -0
- package/docs/cell-account.md +58 -0
- package/docs/compatibility-matrix.md +38 -0
- package/docs/design-memo.md +113 -0
- package/docs/doctor.md +53 -0
- package/docs/examples/README.md +11 -0
- package/docs/examples/claude-code-mcp.json +13 -0
- package/docs/examples/claude_desktop_config.json +12 -0
- package/docs/examples/cursor-mcp.json +11 -0
- package/docs/genesis.md +312 -0
- package/docs/kernel.md +101 -0
- package/docs/loop-d.md +90 -0
- package/docs/mcp.md +105 -0
- package/docs/node.md +66 -0
- package/docs/version-lock.md +67 -0
- package/docs/world.md +50 -0
- package/docs/zk.md +48 -0
- package/package.json +82 -4
- package/prover/README.md +26 -0
- package/prover/build-predicate.mjs +77 -0
- package/prover/package-lock.json +1837 -0
- package/prover/package.json +15 -0
- package/prover/run.cjs +199 -0
- package/scenarios/fixtures/sample-filing.txt +16 -0
- package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
- package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
- package/scenarios/skills/finch-extract-verify/fields.json +56 -0
- package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
- package/scenarios/skills/foga-probe/LICENSE +2 -0
- package/scenarios/skills/foga-probe/SKILL.md +35 -0
- package/scenarios/skills/foga-probe/manifest.json +16 -0
- package/scenarios/skills/foga-probe/probe.schema.json +40 -0
- package/server.json +79 -0
- package/vendor/LOCK.json +107 -0
- package/vendor/README.md +13 -0
- package/worlds/local.json +34 -0
- package/worlds/sepolia.json +314 -0
- package/zk/build/verification_key.json +174 -0
- package/zk/circuits/kya_predicate_count_v1.circom +231 -0
|
@@ -0,0 +1,449 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { Contract, Wallet, getAddress, parseEther } from "ethers";
|
|
7
|
+
import { dataUriJcs, keccakJcs } from "../core/canonical.js";
|
|
8
|
+
import { accountSubject, fogaAttestedSchemeDescriptor } from "../core/kya.js";
|
|
9
|
+
import { anvilKeys } from "../genesis/roles.js";
|
|
10
|
+
import { Kernel } from "../kernel/kernel.js";
|
|
11
|
+
import { OperationStore } from "../kernel/store.js";
|
|
12
|
+
import { newApiKey } from "../http/server.js";
|
|
13
|
+
import { loadCellConfig, newCellConfig, saveCellConfig } from "../node/config.js";
|
|
14
|
+
import { cellInit } from "../node/init.js";
|
|
15
|
+
import { Node } from "../node/node.js";
|
|
16
|
+
import { defaultCatalog, OPERATOR_CALLER } from "../tools/index.js";
|
|
17
|
+
import { repoRoot } from "../util/fs.js";
|
|
18
|
+
import { ALL_CASES } from "./demander.js";
|
|
19
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
20
|
+
const bigJson = (v) => JSON.stringify(v, (_k, x) => (typeof x === "bigint" ? x.toString() : x), 2);
|
|
21
|
+
/** The CLI of this very package: compiled when this module runs from dist/, the TypeScript source under tsx otherwise. */
|
|
22
|
+
export function ownCli() {
|
|
23
|
+
const here = fileURLToPath(import.meta.url);
|
|
24
|
+
const root = repoRoot();
|
|
25
|
+
if (/[\\/]dist[\\/]/.test(here))
|
|
26
|
+
return { command: process.execPath, args: [join(root, "dist", "cli.js")] };
|
|
27
|
+
return { command: process.execPath, args: ["--import", "tsx", join(root, "src", "cli.ts")] };
|
|
28
|
+
}
|
|
29
|
+
function freePort() {
|
|
30
|
+
return new Promise((resolvePort, reject) => {
|
|
31
|
+
const s = createServer();
|
|
32
|
+
s.once("error", reject);
|
|
33
|
+
s.listen(0, "127.0.0.1", () => {
|
|
34
|
+
const addr = s.address();
|
|
35
|
+
const port = typeof addr === "object" && addr ? addr.port : 0;
|
|
36
|
+
s.close(() => resolvePort(port));
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
function cleanEnv(extra) {
|
|
41
|
+
const env = {};
|
|
42
|
+
for (const [k, v] of Object.entries(process.env))
|
|
43
|
+
if (v !== undefined && !k.startsWith("FOGA_"))
|
|
44
|
+
env[k] = v;
|
|
45
|
+
return { ...env, ...extra };
|
|
46
|
+
}
|
|
47
|
+
function opOutput(op) {
|
|
48
|
+
return { key: op.key, kind: op.kind, signer: op.signer, status: op.status, txHash: op.txHash, block: op.block, nonce: op.nonce, attempts: op.attempts, replacements: op.replacements, error: op.error, capability: op.capability, caller: op.caller, valueWei: op.valueWei.toString(), gasCostWei: op.gasCostWei === null ? null : op.gasCostWei.toString(), createdAt: op.createdAt, updatedAt: op.updatedAt };
|
|
49
|
+
}
|
|
50
|
+
/** A snapshot of the cell's store, read beside the running node. */
|
|
51
|
+
function snapshot(storePath) {
|
|
52
|
+
const store = OperationStore.open(storePath);
|
|
53
|
+
try {
|
|
54
|
+
return { ops: store.all(), jobs: store.jobs(null, 10_000), decisions: store.decisions({ limit: 100_000 }) };
|
|
55
|
+
}
|
|
56
|
+
finally {
|
|
57
|
+
store.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export async function runLoopD(opts) {
|
|
61
|
+
const log = opts.log ?? ((line) => console.error(line));
|
|
62
|
+
const root = repoRoot();
|
|
63
|
+
const dir = resolve(opts.dir);
|
|
64
|
+
mkdirSync(dir, { recursive: true });
|
|
65
|
+
const worldPath = resolve(opts.worldPath);
|
|
66
|
+
const seed = opts.seed ?? 7;
|
|
67
|
+
const cases = opts.cases ?? ALL_CASES;
|
|
68
|
+
const cli = opts.cli ?? ownCli();
|
|
69
|
+
const deadline = Date.now() + (opts.timeoutMs ?? 6 * 60_000);
|
|
70
|
+
const ranAt = new Date().toISOString();
|
|
71
|
+
const worldFile = JSON.parse(readFileSync(worldPath, "utf8"));
|
|
72
|
+
const funderKey = opts.funderKey ?? (worldFile.network === "anvil" ? anvilKeys().W : undefined);
|
|
73
|
+
if (!funderKey)
|
|
74
|
+
throw new Error("loop D on a non-local world needs --funder-key (the key that funds the cell's roles and mints the tenders)");
|
|
75
|
+
const timeLeft = () => {
|
|
76
|
+
if (Date.now() > deadline)
|
|
77
|
+
throw new Error("loop D ran out of time");
|
|
78
|
+
};
|
|
79
|
+
// --- the world operator's kernel: funds the roles, registers the standing scheme, attests
|
|
80
|
+
const k = await Kernel.open({ worldPath, rpc: opts.rpc, storePath: join(dir, "orchestrator.sqlite"), keys: { W: funderKey } });
|
|
81
|
+
const W = new Wallet(funderKey).address;
|
|
82
|
+
let child = null;
|
|
83
|
+
try {
|
|
84
|
+
const owner = Wallet.createRandom();
|
|
85
|
+
const operator = Wallet.createRandom();
|
|
86
|
+
const guardian = Wallet.createRandom();
|
|
87
|
+
log(`loop D: a new cell — owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}; seed ${seed}; model ${opts.model}${opts.modelName ? `/${opts.modelName}` : ""}`);
|
|
88
|
+
const price = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
|
|
89
|
+
const gas = (units) => units * price * 2n;
|
|
90
|
+
await k.transfer("fund.owner", "W", owner.address, parseEther("0.05") + gas(12000000n));
|
|
91
|
+
await k.transfer("fund.operator", "W", operator.address, gas(20000000n) + parseEther("0.003"));
|
|
92
|
+
await k.transfer("fund.guardian", "W", guardian.address, gas(1000000n));
|
|
93
|
+
const att = await k.call("scheme", "W", k.kya.schemeRegistryAddress, k.kya.schemes().interface, "registerScheme", [k.kya.attestedSchemeInput(fogaAttestedSchemeDescriptor())]);
|
|
94
|
+
const schemeId = k.kya.schemeIdFromReceipt(att.receipt);
|
|
95
|
+
const bornAtBlock = await k.blockNumber();
|
|
96
|
+
// --- the cell's configuration and birth (the owner's act, in this process, as `foga cell init` would)
|
|
97
|
+
const configPath = join(dir, "D.json");
|
|
98
|
+
const storePath = join(dir, "D.ops.sqlite");
|
|
99
|
+
const cfg = newCellConfig({ name: "D", world: worldPath, store: storePath, owner: owner.address, operator: operator.address, guardian: guardian.address, rpc: opts.rpc, skillsDir: join(root, "scenarios", "skills"), inputDir: join(root, "scenarios", "fixtures"), description: "Loop D: a cell born for one unscripted run" });
|
|
100
|
+
cfg.identity.standing = { schemeId, issuers: [W] };
|
|
101
|
+
// the fee book is sized for the run (two skill mints, a few mints, the tenders); the value caps stay tight so cases g and the concurrent mints meet them
|
|
102
|
+
cfg.policy.budget = { dailyValueWei: parseEther("0.002").toString(), perOperationValueWei: parseEther("0.001").toString(), dailyGasWei: parseEther("0.05").toString(), maxConcurrentCommitments: 2 };
|
|
103
|
+
cfg.policy.tenders = { minRewardWei: parseEther("0.0005").toString(), minSubmitWindowSeconds: 600, minSettleWindowSeconds: 600, acceptance: ["hashlock"], counterpartyMinLevel: 0, lookbackBlocks: 9000 };
|
|
104
|
+
cfg.runtime.pollSeconds = opts.intervalSeconds ?? 1;
|
|
105
|
+
cfg.runtime.model = { provider: opts.model, model: opts.modelName, dailyCalls: opts.model === "stub" ? 400 : 120, maxToolCallsPerTick: 12, maxActsPerTick: 2 };
|
|
106
|
+
cfg.runtime.http.rateLimitPerMinute = 100_000;
|
|
107
|
+
saveCellConfig(configPath, cfg);
|
|
108
|
+
const birthNode = await Node.open({ configPath, rpc: opts.rpc, keys: { owner: owner.privateKey }, allowUnborn: true, log: () => { } });
|
|
109
|
+
const born = await cellInit(birthNode, { log: () => { } });
|
|
110
|
+
birthNode.close();
|
|
111
|
+
const account = getAddress(born.account);
|
|
112
|
+
log(`loop D: cell D born as ${account} (agent ${born.agentId}) in ${born.operations.length} operations`);
|
|
113
|
+
const accountBalanceStart = await k.balance(account);
|
|
114
|
+
// --- standing from the world operator, the two skills minted by the operator
|
|
115
|
+
const claim = { scheme: "S_att", level: 2, subject: `eip155:${k.chainId}:${account.toLowerCase()}` };
|
|
116
|
+
const expiresAt = (await k.blockTimestamp()) + 365 * 86400;
|
|
117
|
+
await k.call("attest", "W", k.kya.registryAddress, k.kya.registry().interface, "attest", [accountSubject(k.chainId, account), schemeId, 2, keccakJcs(claim), expiresAt, dataUriJcs(claim), keccakJcs(claim)]);
|
|
118
|
+
const catalog = defaultCatalog();
|
|
119
|
+
const skillNode = await Node.open({ configPath, rpc: opts.rpc, keys: { operator: operator.privateKey }, log: () => { } });
|
|
120
|
+
for (const skill of ["foga-probe", "finch-extract-verify"]) {
|
|
121
|
+
const r = await catalog.invoke(skillNode, "skill_mint", { skill }, OPERATOR_CALLER, { mode: "act" });
|
|
122
|
+
if (!r.ok)
|
|
123
|
+
throw new Error(`skill_mint ${skill}: ${r.gate}: ${r.reason}`);
|
|
124
|
+
}
|
|
125
|
+
skillNode.close();
|
|
126
|
+
const held = loadCellConfig(configPath).config.skills.held;
|
|
127
|
+
const packages = { "foga-probe": held["foga-probe"].packageHash, "finch-extract-verify": held["finch-extract-verify"].packageHash };
|
|
128
|
+
log(`loop D: standing level 2 attested by ${W}; skills minted (${Object.keys(held).join(", ")})`);
|
|
129
|
+
// --- an API key for the orchestrator's own calls through the HTTP entrance, and the node's port
|
|
130
|
+
const key = newApiKey("orchestrator");
|
|
131
|
+
{
|
|
132
|
+
const loaded = loadCellConfig(configPath);
|
|
133
|
+
loaded.config.access.keys.push({ id: "orchestrator", sha256: key.sha256, level: "act", visibility: "caller", note: "the loop D orchestrator" });
|
|
134
|
+
saveCellConfig(configPath, loaded.config);
|
|
135
|
+
}
|
|
136
|
+
const port = await freePort();
|
|
137
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
138
|
+
// --- the independent demander: cases a–e (f comes later, during the suspension)
|
|
139
|
+
const demanderStore = join(dir, "demander.sqlite");
|
|
140
|
+
const demand = async (ids, batch) => {
|
|
141
|
+
const out = join(dir, `cases-${batch}.json`);
|
|
142
|
+
const r = await runChild(cli, ["demo", "demander", "--world", worldPath, "--rpc", opts.rpc, "--store", demanderStore, "--cell", account, "--scheme", schemeId, "--issuer", W, "--probe-hash", packages["foga-probe"], "--extract-hash", packages["finch-extract-verify"], "--cases", ids.join(","), "--seed", String(seed), "--out", out], cleanEnv({ FOGA_DEMANDER_KEY: funderKey }), (line) => log(` demander| ${line}`));
|
|
143
|
+
if (r.code !== 0)
|
|
144
|
+
throw new Error(`the demander failed (exit ${r.code})`);
|
|
145
|
+
return JSON.parse(readFileSync(out, "utf8")).cases;
|
|
146
|
+
};
|
|
147
|
+
const firstBatch = cases.filter((c) => c !== "f");
|
|
148
|
+
const minted = firstBatch.length ? await demand(firstBatch, "1") : [];
|
|
149
|
+
// --- the node: `foga node run`, the operator key alone, a model proposing
|
|
150
|
+
const startNode = () => {
|
|
151
|
+
const env = cleanEnv({ FOGA_OPERATOR_KEY: operator.privateKey });
|
|
152
|
+
const args = [...cli.args, "node", "run", "--config", configPath, "--rpc", opts.rpc, "--model", opts.model, "--listen", `127.0.0.1:${port}`, "--interval", String(opts.intervalSeconds ?? 1), "--transcripts", join(dir, "transcripts")];
|
|
153
|
+
const c = spawn(cli.command, args, { env, stdio: ["ignore", "ignore", "pipe"] });
|
|
154
|
+
c.stderr.on("data", (d) => {
|
|
155
|
+
for (const line of String(d).split("\n"))
|
|
156
|
+
if (line.trim())
|
|
157
|
+
log(` node| ${line}`);
|
|
158
|
+
});
|
|
159
|
+
return c;
|
|
160
|
+
};
|
|
161
|
+
const exited = (c) => new Promise((r) => (c.exitCode !== null || c.signalCode !== null ? r() : c.once("exit", () => r())));
|
|
162
|
+
child = startNode();
|
|
163
|
+
log(`loop D: node started (pid ${child.pid}) at ${baseUrl}`);
|
|
164
|
+
const api = async (path, body) => {
|
|
165
|
+
const r = await fetch(`${baseUrl}${path}`, { method: "POST", headers: { "content-type": "application/json", authorization: `Bearer ${key.key}` }, body: JSON.stringify(body) });
|
|
166
|
+
return (await r.json());
|
|
167
|
+
};
|
|
168
|
+
const takenIds = minted.filter((c) => c.expect.outcome === "taken").map((c) => c.tokenId);
|
|
169
|
+
const refusedIds = minted.filter((c) => c.expect.outcome === "refused").map((c) => c.tokenId);
|
|
170
|
+
const recovery = { performed: false, killedAfterOperation: null, killedAt: null, restartedAt: null, tokenId: null };
|
|
171
|
+
const entrances = { idempotent: null, conflict: null, concurrent: null };
|
|
172
|
+
const submitKey = (tokenId) => new RegExp(`^job:tender:${tokenId}:c\\d+:submit$`);
|
|
173
|
+
const settledOnChain = async (tokenId) => {
|
|
174
|
+
const count = Number(await k.tenders.submissionCount(BigInt(tokenId)).catch(() => 0n));
|
|
175
|
+
for (let i = 1; i <= count; i++) {
|
|
176
|
+
const s = await k.tenders.submissionOf(BigInt(tokenId), BigInt(i));
|
|
177
|
+
if (getAddress(s.fulfiller) === account && s.status === 1)
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
};
|
|
182
|
+
// --- phase 1: the first batch is worked; the node is killed after its first submission and restarted; the other entrances join in
|
|
183
|
+
// the orchestrator's reads are pinned to the last block its kernel saw; the pin moves with each look at the head
|
|
184
|
+
const head = () => k.world.provider.getBlockNumber();
|
|
185
|
+
let concurrentDone = false;
|
|
186
|
+
for (;;) {
|
|
187
|
+
timeLeft();
|
|
188
|
+
await sleep(1000);
|
|
189
|
+
await head();
|
|
190
|
+
if (child.exitCode !== null && !recovery.performed)
|
|
191
|
+
throw new Error(`the node exited with ${child.exitCode} before the run was done`);
|
|
192
|
+
const snap = snapshot(storePath);
|
|
193
|
+
if ((opts.recovery ?? true) && !recovery.performed) {
|
|
194
|
+
const submit = snap.ops.find((o) => o.status === "confirmed" && takenIds.some((id) => submitKey(id).test(o.key)));
|
|
195
|
+
if (submit) {
|
|
196
|
+
const tokenId = /^job:tender:(\d+):/.exec(submit.key)[1];
|
|
197
|
+
const settle = snap.ops.find((o) => o.key.startsWith(`job:tender:${tokenId}:`) && o.key.endsWith(":settle:settle") && o.status === "confirmed");
|
|
198
|
+
if (!settle) {
|
|
199
|
+
child.kill("SIGKILL");
|
|
200
|
+
await exited(child);
|
|
201
|
+
recovery.performed = true;
|
|
202
|
+
recovery.killedAfterOperation = submit.key;
|
|
203
|
+
recovery.killedAt = new Date().toISOString();
|
|
204
|
+
recovery.tokenId = tokenId;
|
|
205
|
+
log(`loop D: node killed (SIGKILL) after ${submit.key} and before its settlement; restarting`);
|
|
206
|
+
await sleep(500);
|
|
207
|
+
child = startNode();
|
|
208
|
+
recovery.restartedAt = new Date().toISOString();
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// beside the runner: the same submission again through HTTP (one business operation), the same job under another request (a conflict), three mints at once
|
|
214
|
+
if (!concurrentDone && recovery.performed && child.exitCode === null) {
|
|
215
|
+
const submitted = snap.jobs.find((j) => j.kind === "tender" && j.status === "done" && takenIds.includes(String(j.input?.tokenId ?? "")));
|
|
216
|
+
if (submitted) {
|
|
217
|
+
const input = submitted.input;
|
|
218
|
+
const run = snap.jobs.find((j) => j.kind === "skill-run" && j.status === "done" && j.result?.resultHash?.toLowerCase() === input.resultHash.toLowerCase());
|
|
219
|
+
const runReceipt = run?.result?.runReceipt;
|
|
220
|
+
await waitForHttp(`${baseUrl}/healthz`);
|
|
221
|
+
const again = await api("/api/tender_submit", { tokenId: input.tokenId, resultHash: input.resultHash, ...(runReceipt ? { runReceipt } : {}), jobId: submitted.id });
|
|
222
|
+
const txHash = submitted.result?.txHash ?? null;
|
|
223
|
+
entrances.idempotent = { decisionId: again.decisionId ?? null, gate: again.ok ? "idempotent" : (again.gate ?? null), sameTxHash: again.ok ? again.output?.txHash === txHash : false };
|
|
224
|
+
const conflict = await api("/api/tender_submit", { tokenId: input.tokenId, resultHash: "0x" + "cd".repeat(32), jobId: submitted.id });
|
|
225
|
+
entrances.conflict = { decisionId: conflict.decisionId ?? null, gate: conflict.ok ? null : (conflict.gate ?? null) };
|
|
226
|
+
const reward = parseEther("0.0008").toString();
|
|
227
|
+
const mints = await Promise.all([1, 2, 3].map((n) => api("/api/tender_mint", { skill: "foga-probe", packageHash: packages["foga-probe"], input: { concurrent: n }, answerHash: "0x" + "22".repeat(32), rewardWei: reward, submitIn: 86400, settleIn: 7 * 86400 })));
|
|
228
|
+
entrances.concurrent = { requested: mints.length, allowed: mints.filter((m) => m.ok).length, refused: mints.filter((m) => !m.ok).map((m) => ({ gate: m.gate ?? "?", reason: m.reason ?? "" })) };
|
|
229
|
+
concurrentDone = true;
|
|
230
|
+
log(`loop D: through HTTP beside the runner — same submission: ${entrances.idempotent.gate}${entrances.idempotent.sameTxHash ? " (same transaction)" : ""}; same job, another request: ${entrances.conflict.gate}; three mints at once: ${entrances.concurrent.allowed} allowed, ${entrances.concurrent.refused.map((r) => r.gate).join("/") || "none refused"}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// done when every taken case is settled with no credit left and every refused case has its refusal
|
|
234
|
+
const refusalsSeen = refusedIds.every((id) => snap.decisions.some((d) => d.caller === "runner" && d.capability === "tender_precheck" && d.jobId === `precheck:${id}` && d.verdict === "refused"));
|
|
235
|
+
let takenDone = true;
|
|
236
|
+
for (const id of takenIds) {
|
|
237
|
+
if (!(await settledOnChain(id)) || (await k.tenders.creditOf(BigInt(id), account)) > 0n) {
|
|
238
|
+
takenDone = false;
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
if (refusalsSeen && takenDone && (concurrentDone || !(opts.recovery ?? true) || !takenIds.length))
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
log("loop D: phase 1 done (the first batch is settled or refused)");
|
|
246
|
+
// --- phase 2, case f: the guardian suspends, the demander mints f, the runner refuses at safety, the guardian resumes, the runner takes f
|
|
247
|
+
let fCase = null;
|
|
248
|
+
if (cases.includes("f")) {
|
|
249
|
+
const guardianAct = async (name, input) => {
|
|
250
|
+
const g = await Node.open({ configPath, rpc: opts.rpc, keys: { guardian: guardian.privateKey }, log: () => { } });
|
|
251
|
+
try {
|
|
252
|
+
const r = await catalog.invoke(g, name, input, OPERATOR_CALLER, { mode: "act" });
|
|
253
|
+
if (!r.ok)
|
|
254
|
+
throw new Error(`${name}: ${r.gate}: ${r.reason}`);
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
g.close();
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
await guardianAct("guardian_suspend", { reason: "loop D case f: a review while a tender arrives", restrictions: ["noNewRisk", "deriskOnly"] });
|
|
261
|
+
log("loop D: the guardian suspended the binding (noNewRisk, deriskOnly)");
|
|
262
|
+
fCase = (await demand(["f"], "2"))[0];
|
|
263
|
+
for (;;) {
|
|
264
|
+
timeLeft();
|
|
265
|
+
await sleep(1000);
|
|
266
|
+
await head();
|
|
267
|
+
const snap = snapshot(storePath);
|
|
268
|
+
if (snap.decisions.some((d) => d.caller === "runner" && d.capability === "tender_precheck" && d.jobId === `precheck:${fCase.tokenId}` && d.verdict === "refused" && d.gate === "safety"))
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
log(`loop D: tender ${fCase.tokenId} refused at safety during the suspension`);
|
|
272
|
+
await guardianAct("guardian_resume", { reason: "loop D case f: review complete" });
|
|
273
|
+
log("loop D: the guardian resumed the binding");
|
|
274
|
+
for (;;) {
|
|
275
|
+
timeLeft();
|
|
276
|
+
await sleep(1000);
|
|
277
|
+
await head();
|
|
278
|
+
if ((await settledOnChain(fCase.tokenId)) && (await k.tenders.creditOf(BigInt(fCase.tokenId), account)) === 0n)
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
log(`loop D: tender ${fCase.tokenId} taken after the resume`);
|
|
282
|
+
}
|
|
283
|
+
// --- stop the node, then read everything it left
|
|
284
|
+
child.kill("SIGTERM");
|
|
285
|
+
await Promise.race([exited(child), sleep(15_000)]);
|
|
286
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
287
|
+
child.kill("SIGKILL");
|
|
288
|
+
child = null;
|
|
289
|
+
const toBlock = await head();
|
|
290
|
+
const store = OperationStore.open(storePath);
|
|
291
|
+
const decisions = store.decisions({ limit: 100_000 });
|
|
292
|
+
const jobs = store.jobs(null, 100_000);
|
|
293
|
+
const ops = store.all();
|
|
294
|
+
const journal = store.journalEntries().filter((e) => ["runner-tick", "runner-skipped", "runner-model-error", "tender-discovered", "safety-changed"].includes(e.event));
|
|
295
|
+
const modelCalls = Object.values(store.facts("runner.model.calls.")).reduce((a, b) => a + Number(b), 0);
|
|
296
|
+
store.close();
|
|
297
|
+
const allCases = fCase ? [...minted, fCase] : minted;
|
|
298
|
+
const reader = await Node.open({ configPath, rpc: opts.rpc, keys: {}, log: () => { } });
|
|
299
|
+
const outcomes = [];
|
|
300
|
+
try {
|
|
301
|
+
for (const c of allCases) {
|
|
302
|
+
const r = await catalog.invoke(reader, "tender_inspect", { tokenId: c.tokenId }, OPERATOR_CALLER);
|
|
303
|
+
const view = r.ok ? r.output : { submissions: [] };
|
|
304
|
+
const mine = decisions.filter((d) => d.caller === "runner" && (d.jobId === `precheck:${c.tokenId}` || d.jobId?.startsWith(`tender:${c.tokenId}:`)));
|
|
305
|
+
const gates = decisions.filter((d) => d.caller === "runner" && d.capability === "tender_precheck" && d.jobId === `precheck:${c.tokenId}`).map((d) => (d.verdict === "refused" ? d.gate : "allowed"));
|
|
306
|
+
const subs = view.submissions.map((s) => ({ id: s.id, fulfiller: s.fulfiller, status: s.status, byCell: getAddress(s.fulfiller) === account }));
|
|
307
|
+
const settled = subs.some((s) => s.byCell && s.status === 1);
|
|
308
|
+
const settleJob = jobs.find((j) => j.kind === "tender-settle" && String(j.input?.tokenId) === c.tokenId && j.status === "done");
|
|
309
|
+
const paid = settleJob?.result ?? {};
|
|
310
|
+
const withdrawn = jobs.filter((j) => j.kind === "withdraw" && String(j.input?.tokenId) === c.tokenId && j.status === "done").reduce((a, j) => a + BigInt(j.result?.withdrawnWei ?? "0"), 0n);
|
|
311
|
+
const refusedGates = gates.filter((g) => g !== "allowed");
|
|
312
|
+
let status = "other";
|
|
313
|
+
if (settled && refusedGates.length)
|
|
314
|
+
status = "refused-then-taken";
|
|
315
|
+
else if (settled)
|
|
316
|
+
status = "taken";
|
|
317
|
+
else if (refusedGates.length && !subs.some((s) => s.byCell))
|
|
318
|
+
status = "refused";
|
|
319
|
+
else if (!gates.length && !subs.some((s) => s.byCell))
|
|
320
|
+
status = "pending";
|
|
321
|
+
outcomes.push({ ...c, outcome: { status, gates, decisions: mine.map((d) => d.id), submissions: subs, settled, paidWei: paid.paidWei ?? "0", creditWei: paid.creditWei ?? "0", withdrawnWei: withdrawn.toString() } });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
finally {
|
|
325
|
+
reader.close();
|
|
326
|
+
}
|
|
327
|
+
// --- every transaction the operator key signed, from the birth to now
|
|
328
|
+
const accountIface = k.world.ifaceOf("foga:FogaCellAccount");
|
|
329
|
+
const accountContract = new Contract(account, accountIface, k.world.provider);
|
|
330
|
+
const operatorTxs = [];
|
|
331
|
+
let ownerTxs = 0;
|
|
332
|
+
let guardianTxs = 0;
|
|
333
|
+
for (let n = bornAtBlock; n <= toBlock; n++) {
|
|
334
|
+
const block = await k.world.provider.getBlock(n, true);
|
|
335
|
+
if (!block)
|
|
336
|
+
continue;
|
|
337
|
+
for (const tx of block.prefetchedTransactions) {
|
|
338
|
+
const from = getAddress(tx.from);
|
|
339
|
+
if (from === owner.address)
|
|
340
|
+
ownerTxs++;
|
|
341
|
+
if (from === guardian.address)
|
|
342
|
+
guardianTxs++;
|
|
343
|
+
if (from !== operator.address)
|
|
344
|
+
continue;
|
|
345
|
+
const receipt = await k.world.provider.getTransactionReceipt(tx.hash);
|
|
346
|
+
let inner = null;
|
|
347
|
+
if (tx.to && getAddress(tx.to) === account) {
|
|
348
|
+
try {
|
|
349
|
+
const parsed = accountIface.parseTransaction({ data: tx.data, value: tx.value });
|
|
350
|
+
if (parsed && parsed.name === "execute") {
|
|
351
|
+
const target = getAddress(parsed.args[0]);
|
|
352
|
+
const data = parsed.args[2];
|
|
353
|
+
const selector = data.slice(0, 10);
|
|
354
|
+
const allowed = Boolean(await accountContract.operatorAllowed(target, selector).catch(() => null));
|
|
355
|
+
inner = { target, selector, valueWei: parsed.args[1].toString(), allowed };
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
inner = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
operatorTxs.push({ hash: tx.hash, block: n, nonce: tx.nonce, to: tx.to ? getAddress(tx.to) : null, toIsAccount: Boolean(tx.to && getAddress(tx.to) === account), valueWei: tx.value.toString(), inner, status: receipt?.status ?? null });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
const ticks = journal.filter((e) => e.event === "runner-tick");
|
|
366
|
+
const detail = (e) => e.detail;
|
|
367
|
+
const probeDecision = decisions.find((d) => d.caller === "runner" && d.capability === "tender_mint");
|
|
368
|
+
const used = { valueWei: ops.filter((o) => o.signer === "operator" && o.status === "confirmed").reduce((a, o) => a + o.valueWei, 0n), gasWei: ops.filter((o) => o.signer === "operator").reduce((a, o) => a + (o.gasCostWei ?? 0n), 0n) };
|
|
369
|
+
const config = loadCellConfig(configPath).config;
|
|
370
|
+
const finishedAt = new Date().toISOString();
|
|
371
|
+
const evidence = {
|
|
372
|
+
schema: "foga.loop-d-evidence/1",
|
|
373
|
+
worldId: worldFile.worldId,
|
|
374
|
+
chainId: worldFile.chainId,
|
|
375
|
+
network: worldFile.network,
|
|
376
|
+
ranAt,
|
|
377
|
+
finishedAt,
|
|
378
|
+
seed,
|
|
379
|
+
model: { provider: opts.model, model: opts.model === "stub" ? "foga-stub/1" : (opts.modelName ?? "") },
|
|
380
|
+
cell: { name: "D", account, agentId: born.agentId, owner: owner.address, operator: operator.address, guardian: guardian.address, bornAtBlock, specId: born.specId ?? null },
|
|
381
|
+
demander: W,
|
|
382
|
+
cases: outcomes,
|
|
383
|
+
budgetProbe: { attempted: Boolean(probeDecision), decisionId: probeDecision?.id ?? null, gate: probeDecision ? (probeDecision.verdict === "refused" ? probeDecision.gate : "allowed") : null, capability: probeDecision?.capability ?? null },
|
|
384
|
+
runner: {
|
|
385
|
+
ticks: ticks.length,
|
|
386
|
+
skipped: journal.filter((e) => e.event === "runner-skipped").length,
|
|
387
|
+
errors: journal.filter((e) => e.event === "runner-model-error").length + ticks.filter((e) => detail(e).error).length,
|
|
388
|
+
modelCalls,
|
|
389
|
+
toolCalls: ticks.reduce((a, e) => a + (detail(e).toolCalls ?? 0), 0),
|
|
390
|
+
acts: ticks.reduce((a, e) => a + (detail(e).acts ?? 0), 0),
|
|
391
|
+
trajectoryHashes: ticks.map((e) => detail(e).trajectoryHash ?? "").filter(Boolean),
|
|
392
|
+
},
|
|
393
|
+
recovery,
|
|
394
|
+
entrances,
|
|
395
|
+
budget: { dailyValueWei: config.policy.budget.dailyValueWei, perOperationValueWei: config.policy.budget.perOperationValueWei, dailyGasWei: config.policy.budget.dailyGasWei, maxConcurrentCommitments: config.policy.budget.maxConcurrentCommitments, dailyModelCalls: config.runtime.model.dailyCalls, used: { valueWei: used.valueWei.toString(), gasWei: used.gasWei.toString(), modelCalls, skillRuns: jobs.filter((j) => j.kind === "skill-run").length, commitmentsMade: jobs.filter((j) => j.kind === "tender").length } },
|
|
396
|
+
chain: { fromBlock: bornAtBlock, toBlock, account, operator: operator.address, operatorTxs, ownerTxs, guardianTxs, accountBalanceWei: { start: accountBalanceStart.toString(), end: (await k.balance(account)).toString() } },
|
|
397
|
+
decisions,
|
|
398
|
+
jobs,
|
|
399
|
+
operations: ops.map(opOutput),
|
|
400
|
+
journal,
|
|
401
|
+
summary: { cases: outcomes.length, taken: outcomes.filter((c) => c.outcome.status === "taken" || c.outcome.status === "refused-then-taken").length, refused: outcomes.filter((c) => c.outcome.status === "refused").length, pending: outcomes.filter((c) => c.outcome.status === "pending" || c.outcome.status === "other").length, decisions: { allowed: decisions.filter((d) => d.verdict === "allowed").length, refused: decisions.filter((d) => d.verdict === "refused").length }, operations: ops.length },
|
|
402
|
+
};
|
|
403
|
+
mkdirSync(dirname(resolve(opts.evidencePath)), { recursive: true });
|
|
404
|
+
writeFileSync(resolve(opts.evidencePath), bigJson(evidence) + "\n");
|
|
405
|
+
log(`loop D: evidence written to ${opts.evidencePath} — ${evidence.summary.taken} taken, ${evidence.summary.refused} refused, ${evidence.summary.pending} pending; ${decisions.length} decision records, ${ops.length} operations, ${ticks.length} runner ticks, ${modelCalls} model calls`);
|
|
406
|
+
return evidence;
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
if (child && child.exitCode === null)
|
|
410
|
+
child.kill("SIGKILL");
|
|
411
|
+
k.close();
|
|
412
|
+
k.world.provider.destroy();
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
async function waitForHttp(url, tries = 60) {
|
|
416
|
+
for (let i = 0; i < tries; i++) {
|
|
417
|
+
try {
|
|
418
|
+
const r = await fetch(url);
|
|
419
|
+
if (r.status < 500)
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
catch {
|
|
423
|
+
/* not yet */
|
|
424
|
+
}
|
|
425
|
+
await sleep(250);
|
|
426
|
+
}
|
|
427
|
+
throw new Error(`${url} did not come up`);
|
|
428
|
+
}
|
|
429
|
+
function runChild(cli, args, env, onLine) {
|
|
430
|
+
return new Promise((resolveRun, reject) => {
|
|
431
|
+
const c = spawn(cli.command, [...cli.args, ...args], { env, stdio: ["ignore", "pipe", "pipe"] });
|
|
432
|
+
let stdout = "";
|
|
433
|
+
c.stdout.on("data", (d) => (stdout += d));
|
|
434
|
+
c.stderr.on("data", (d) => {
|
|
435
|
+
for (const line of String(d).split("\n"))
|
|
436
|
+
if (line.trim())
|
|
437
|
+
onLine(line);
|
|
438
|
+
});
|
|
439
|
+
c.once("error", reject);
|
|
440
|
+
c.once("exit", (code) => resolveRun({ code, stdout }));
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
/** Where `foga demo --loop-d` keeps its working files. */
|
|
444
|
+
export function defaultLoopDDir() {
|
|
445
|
+
const dir = join(repoRoot(), "deployments", "loop-d");
|
|
446
|
+
if (!existsSync(dir))
|
|
447
|
+
mkdirSync(dir, { recursive: true });
|
|
448
|
+
return dir;
|
|
449
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
function kind(world, name) {
|
|
2
|
+
const kinds = (world.wiring?.["8444/kindIds"] ?? null);
|
|
3
|
+
return kinds?.[name] ? [kinds[name]] : null;
|
|
4
|
+
}
|
|
5
|
+
export const WIRING_CHECKS = [
|
|
6
|
+
{ subject: "8419/KYARegistry", fn: "getSchemeRegistry", expectRef: "8419/KYASchemeRegistry" },
|
|
7
|
+
{ subject: "8419/KYAPolicyRegistry", fn: "kyaRegistry", expectRef: "8419/KYARegistry" },
|
|
8
|
+
{ subject: "8419/ValidationRegistry8004", fn: "getIdentityRegistry", expectRef: "identity8004" },
|
|
9
|
+
{ subject: "8419/KYABridge8004", fn: "kyaRegistry", expectRef: "8419/KYARegistry" },
|
|
10
|
+
{ subject: "8419/KYABridge8004", fn: "validationRegistry", expectRef: "8419/ValidationRegistry8004" },
|
|
11
|
+
{ subject: "8419/KYABridge8004", fn: "identityRegistry", expectRef: "identity8004" },
|
|
12
|
+
{ subject: "8419/Groth16KYAVerifierAdapter", fn: "groth16", expectRef: "8419/Groth16Verifier" },
|
|
13
|
+
{ subject: "8419/Groth16KYAVerifierAdapter", fn: "requiredIssuerSetRoot", expectParam: (w) => w.params.kya.issuerSetRoot },
|
|
14
|
+
{ subject: "8419/Groth16KYAVerifierAdapter", fn: "epochLength", expectParam: (w) => w.params.kya.epochLength },
|
|
15
|
+
{ subject: "8434/AIDRegistry", fn: "defaultLivenessWindow", expectParam: (w) => w.params.aid.defaultLivenessWindow },
|
|
16
|
+
{ subject: "8434/AIDRegistry", fn: "maxLivenessWindow", expectParam: (w) => w.params.aid.maxLivenessWindow },
|
|
17
|
+
{ subject: "8436/ACDFRegistry", fn: "policies", expectRef: "8436/ACDFPolicyRegistry" },
|
|
18
|
+
{ subject: "8444/ARDOnchainRegistry", fn: "kindAdapter", args: (w) => kind(w, "skill"), expectRef: "8444/SkillAdapter", label: "kindAdapter(skill)" },
|
|
19
|
+
{ subject: "8444/ARDOnchainRegistry", fn: "kindAdapter", args: (w) => kind(w, "task"), expectRef: "8444/TaskAdapter", label: "kindAdapter(task)" },
|
|
20
|
+
{ subject: "8444/ARDOnchainRegistry", fn: "kindAdapter", args: (w) => kind(w, "agent"), expectRef: "8444/AgentAdapter", label: "kindAdapter(agent)" },
|
|
21
|
+
{ subject: "8444/ARDOnchainRegistry", fn: "kindAdapter", args: (w) => kind(w, "registry"), expectRef: "8444/RegistryAdapter", label: "kindAdapter(registry)" },
|
|
22
|
+
{ subject: "8444/AgentAdapter", fn: "identityRegistry", expectRef: "8434/AIDRegistry" },
|
|
23
|
+
{ subject: "8447/SwarmTenderAdapter", fn: "swarms", expectRef: "8447/SwarmRegistry" },
|
|
24
|
+
{ subject: "8447/SwarmTenderAdapter", fn: "task", expectRef: "8414/TaskToken" },
|
|
25
|
+
{ subject: "8447/ERC8004Eligibility", fn: "identity", expectRef: "identity8004" },
|
|
26
|
+
{ subject: "8447/IdentityStateEligibility", fn: "identities", expectRef: "8434/AIDRegistry" },
|
|
27
|
+
{ subject: "8447/SwarmDecisionAdapter", fn: "swarms", expectRef: "8447/SwarmRegistry" },
|
|
28
|
+
{ subject: "8447/SwarmDecisionAdapter", fn: "decisions", expectRef: "8436/ACDFRegistry" },
|
|
29
|
+
{ subject: "8454/AUFAssertionRegistry", fn: "unitRegistry", expectRef: "8454/AUFUnitRegistry" },
|
|
30
|
+
{ subject: "8468/ASFTaskTenderAdapter", fn: "registry", expectRef: "8468/ASFBindingRegistry" },
|
|
31
|
+
{ subject: "8468/ASFTaskTenderAdapter", fn: "task", expectRef: "8414/TaskToken" },
|
|
32
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "policies", expectRef: "8471/ADRFPolicyRegistry" },
|
|
33
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "bindingModule", expectRef: "8471/ADRFBindingModule" },
|
|
34
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "filingModule", expectRef: "8471/ADRFFilingModule" },
|
|
35
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "resolutionModule", expectRef: "8471/ADRFResolutionModule" },
|
|
36
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "decisionModule", expectRef: "8471/ADRFDecisionModule" },
|
|
37
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "recordModule", expectRef: "8471/ADRFRecordModule" },
|
|
38
|
+
{ subject: "8471/ADRFDisputeRegistry", fn: "outcomeModule", expectRef: "8471/ADRFOutcomeModule" },
|
|
39
|
+
{ subject: "8471/ADRFACDFBridge", fn: "adrf", expectRef: "8471/ADRFDisputeRegistry" },
|
|
40
|
+
{ subject: "8471/ADRFACDFBridge", fn: "acdf", expectRef: "8436/ACDFRegistry" },
|
|
41
|
+
{ subject: "8471/ADRFServiceEscrow", fn: "adrf", expectRef: "8471/ADRFDisputeRegistry" },
|
|
42
|
+
];
|
|
43
|
+
/** Layer-4 readouts: authority slots worth seeing even when nothing is expected of them. */
|
|
44
|
+
export const READOUTS = [
|
|
45
|
+
{ subject: "8444/ARDOnchainRegistry", fn: "owner" },
|
|
46
|
+
{ subject: "8419/KYABridge8004", fn: "owner" },
|
|
47
|
+
{ subject: "8447/AllowlistEligibility", fn: "owner" },
|
|
48
|
+
{ subject: "8471/ADRFServiceEscrow", fn: "admin" },
|
|
49
|
+
{ subject: "8471/ADRFServiceEscrow", fn: "bindingId" },
|
|
50
|
+
{ subject: "8454/AUFUnitRegistry", fn: "unitCount" },
|
|
51
|
+
];
|