@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,261 @@
1
+ import { z } from "zod";
2
+ import { jcs, sha256Hex } from "../core/canonical.js";
3
+ import { OperationAmbiguous, OperationReverted } from "../kernel/tx.js";
4
+ import { budgetGate, firstRefusal } from "../node/policy.js";
5
+ import { ACTION_RANK, VISIBILITY_RANK } from "./types.js";
6
+ /** Gas reserved per step against the fee budget until the operation settles and its real cost replaces it. */
7
+ export const FEE_RESERVE_GAS = 300000n;
8
+ /**
9
+ * The capability catalog and the one place authorization happens (design memo, Phase 3, section 3). `invoke` runs
10
+ * the three checks for every act — call permission (who may use what, at which level), business authorization (the
11
+ * policy engine over the plan the capability made), wallet permission (the account's own scope, simulated before
12
+ * the send and enforced again on chain) — and writes a decision record for every attempt. Reads pass the
13
+ * permission check alone and leave no record. The entrances (CLI, MCP, HTTP, A2A) call this and nothing else.
14
+ */
15
+ export class Catalog {
16
+ caps = new Map();
17
+ register(cap) {
18
+ if (this.caps.has(cap.name))
19
+ throw new Error(`capability ${cap.name} registered twice`);
20
+ this.caps.set(cap.name, cap);
21
+ return this;
22
+ }
23
+ get(name) {
24
+ return this.caps.get(name);
25
+ }
26
+ names() {
27
+ return [...this.caps.keys()];
28
+ }
29
+ /** The capabilities a caller may use, with the modes its level allows. */
30
+ list(caller) {
31
+ const out = [];
32
+ for (const cap of this.caps.values()) {
33
+ if (VISIBILITY_RANK[caller.visibility] < VISIBILITY_RANK[cap.visibility])
34
+ continue;
35
+ const modes = modesFor(caller, cap);
36
+ if (cap.action !== "read" && modes.length === 0)
37
+ continue;
38
+ out.push({
39
+ name: cap.name,
40
+ title: cap.title,
41
+ description: cap.description,
42
+ action: cap.action,
43
+ visibility: cap.visibility,
44
+ principal: cap.principal,
45
+ ercs: cap.ercs,
46
+ modes,
47
+ inputSchema: z.toJSONSchema(cap.input, { target: "draft-2020-12", io: "input" }),
48
+ outputSchema: z.toJSONSchema(cap.output, { target: "draft-2020-12", io: "output" }),
49
+ });
50
+ }
51
+ return out;
52
+ }
53
+ async invoke(node, name, rawInput, caller, opts = {}) {
54
+ const cap = this.caps.get(name);
55
+ if (!cap)
56
+ return { ok: false, gate: "input", reason: `unknown capability ${name}` };
57
+ const mode = opts.mode ?? "act";
58
+ const inputHash = hashInput(rawInput);
59
+ // the data scope first: a capability the caller may not see is as if it did not exist (a read leaves no record)
60
+ if (VISIBILITY_RANK[caller.visibility] < VISIBILITY_RANK[cap.visibility]) {
61
+ const reason = `${cap.name} is ${cap.visibility}-visible; this caller sees ${caller.visibility} data`;
62
+ return cap.action === "read" ? { ok: false, gate: "permission", reason } : this.refuse(node, cap, caller, mode, inputHash, "permission", reason);
63
+ }
64
+ const parsed = cap.input.safeParse(rawInput ?? {});
65
+ if (!parsed.success) {
66
+ const issues = parsed.error.issues.map((i) => `${i.path.join(".") || "input"}: ${i.message}`).join("; ");
67
+ return this.refuse(node, cap, caller, mode, inputHash, "input", issues);
68
+ }
69
+ const input = parsed.data;
70
+ if (cap.action === "read") {
71
+ if (ACTION_RANK[caller.level] < ACTION_RANK.read)
72
+ return { ok: false, gate: "permission", reason: "no read permission" };
73
+ const output = await cap.handler({ node, caller }, input);
74
+ return { ok: true, output: cap.output.parse(output) };
75
+ }
76
+ // call permission: the mode decides the level needed (a build or a dry run needs build; the act itself needs act or admin)
77
+ const needed = mode === "act" ? cap.action : "build";
78
+ if (ACTION_RANK[caller.level] < ACTION_RANK[needed]) {
79
+ return this.refuse(node, cap, caller, mode, inputHash, "permission", `${cap.name} in mode ${mode} needs level ${needed}; this caller has ${caller.level}`);
80
+ }
81
+ if (mode === "act" && cap.signer && !node.hasKey(cap.signer)) {
82
+ return this.refuse(node, cap, caller, mode, inputHash, "permission", `${cap.name} is the ${cap.signer}'s act and this process holds no ${cap.signer} key`);
83
+ }
84
+ const asInput = input.as;
85
+ const as = mode === "build" && asInput ? asInput : node.cell?.address ?? asInput ?? "0x0000000000000000000000000000000000000000";
86
+ let plan;
87
+ try {
88
+ plan = await cap.plan({ node, caller, mode, as }, input);
89
+ }
90
+ catch (e) {
91
+ return this.refuse(node, cap, caller, mode, inputHash, "input", explain(e));
92
+ }
93
+ const jobId = plan.job.id;
94
+ if (mode === "build") {
95
+ const calls = plan.calls ?? (await buildAll(plan.steps));
96
+ const decision = node.store.decide({ entrance: caller.entrance, caller: caller.id, capability: cap.name, mode, inputHash, verdict: "allowed", gate: "build", reason: `${calls.length} call(s) built for ${as}`, jobId, operationId: null, model: opts.model ?? null });
97
+ return { ok: true, output: { as, calls: calls.map((c) => ({ to: c.target, value: c.value.toString(), data: c.data, label: c.label })) }, decisionId: decision.id, jobId };
98
+ }
99
+ // an act under a known job: the same job with the same input is answered from the store, never done twice;
100
+ // a job is created only once every gate has passed, so a refusal leaves a decision record and nothing else
101
+ let job = null;
102
+ if (mode === "act" && !plan.job.ephemeral) {
103
+ const existing = node.store.getJob(jobId);
104
+ if (existing) {
105
+ if (existing.inputHash !== inputHash)
106
+ return this.refuse(node, cap, caller, mode, inputHash, "conflict", `job ${jobId} exists for another request (${existing.inputHash}); the same job id must carry the same request`, jobId);
107
+ if (existing.status === "done") {
108
+ const decision = node.store.decide({ entrance: caller.entrance, caller: caller.id, capability: cap.name, mode, inputHash, verdict: "allowed", gate: "idempotent", reason: `job ${jobId} is done; its result is returned, nothing is sent again`, jobId, operationId: existing.operations[0] ?? null, model: opts.model ?? null });
109
+ return { ok: true, output: existing.result, decisionId: decision.id, jobId, operations: existing.operations };
110
+ }
111
+ if (existing.status === "failed" || existing.status === "cancelled") {
112
+ return this.refuse(node, cap, caller, mode, inputHash, "execution", `job ${jobId} ${existing.status} before (${existing.error ?? "no reason recorded"}); the same job answers with that outcome. A new attempt is a new job (another idempotency key).`, jobId);
113
+ }
114
+ job = existing; // accepted or running: a run that stopped before finishing is continued (the kernel sends nothing twice)
115
+ }
116
+ }
117
+ // the decision section runs one act at a time per node: the budget gate reads what earlier decisions reserved (a job's
118
+ // planned value counts from its creation, before its operations exist), so concurrent callers cannot each pass a cap
119
+ // that only one of them fits under
120
+ const decided = await node.serialize(async () => {
121
+ // business authorization: the capability's gates, then the budget (a cell's act only; an admin step is the owner's own transaction)
122
+ const gates = [...plan.gates];
123
+ if (cap.action === "act" && (plan.steps.length || plan.valueWei > 0n || plan.job.commitment)) {
124
+ const fee = await feeReserve(node, plan.steps.length);
125
+ gates.push(budgetGate(node.config, node.budgetUse(), { valueWei: plan.valueWei, feeReserveWei: fee, commitment: Boolean(plan.job.commitment) && !job }));
126
+ }
127
+ const refusal = firstRefusal(gates);
128
+ if (refusal)
129
+ return this.refuse(node, cap, caller, mode, inputHash, refusal.gate, refusal.reason, jobId);
130
+ // wallet permission: the first step is simulated from its signer (the account's allow-list, gate and daily cap decide on chain as well)
131
+ if (plan.steps.length) {
132
+ const first = plan.steps[0];
133
+ const intent = await first.build([]);
134
+ const route = routeOf(first);
135
+ const from = node.config.identity[route.signer];
136
+ const to = route.route === "execute" ? node.body.address : intent.target;
137
+ const data = route.route === "execute" ? node.body.encodeExecute(intent.target, intent.value, intent.data) : intent.data;
138
+ const value = route.route === "execute" ? 0n : intent.value;
139
+ const revert = await node.wouldRevert(from, to, data, value);
140
+ if (revert !== null)
141
+ return this.refuse(node, cap, caller, mode, inputHash, "wallet", `${first.name} would revert: ${revert}`, jobId);
142
+ }
143
+ if (mode === "simulate") {
144
+ const decision = node.store.decide({ entrance: caller.entrance, caller: caller.id, capability: cap.name, mode, inputHash, verdict: "allowed", gate: "all", reason: summarize(gates, plan.steps.length), jobId, operationId: null, model: opts.model ?? null });
145
+ const output = await plan.output([], undefined);
146
+ return { ok: true, output: cap.output.parse(output), decisionId: decision.id, jobId };
147
+ }
148
+ // the act: recorded as allowed before anything is sent; the job (with the value it will move) exists from here on
149
+ const allowed = node.store.decide({ entrance: caller.entrance, caller: caller.id, capability: cap.name, mode, inputHash, verdict: "allowed", gate: "all", reason: summarize(gates, plan.steps.length), jobId, operationId: plan.steps.length ? `job:${jobId}:${plan.steps[0].name}` : null, model: opts.model ?? null });
150
+ if (plan.job.ephemeral)
151
+ return { allowed, gates, job: null };
152
+ const created = job ?? node.store.createJob({ id: jobId, kind: plan.job.kind, caller: caller.id, cell: node.cell?.address, inputHash, input: plan.job.input, valueWei: plan.valueWei });
153
+ node.store.updateJob(jobId, { status: "running" });
154
+ return { allowed, gates, job: created };
155
+ });
156
+ if ("ok" in decided)
157
+ return decided;
158
+ const { allowed } = decided;
159
+ if (plan.job.ephemeral) {
160
+ const output = cap.output.parse(await plan.output([], plan.work ? await plan.work() : undefined));
161
+ return { ok: true, output, decisionId: allowed.id, jobId };
162
+ }
163
+ job = decided.job;
164
+ const confirmed = [];
165
+ const operations = [...job.operations];
166
+ try {
167
+ const work = plan.work ? await plan.work() : undefined;
168
+ const fee = await feeReserve(node, 1);
169
+ for (const step of plan.steps) {
170
+ const intent = await step.build(confirmed);
171
+ const key = `job:${jobId}:${step.name}`;
172
+ const binding = { requestHash: inputHash, capability: cap.name, caller: caller.id, cell: node.cell?.address, valueWei: intent.value, feeCapWei: fee };
173
+ const route = routeOf(step);
174
+ let c;
175
+ if (route.route === "execute") {
176
+ c = await node.body.execute(key, intent.target, intent.data, { value: intent.value, by: route.signer === "owner" ? "owner" : "operator", binding });
177
+ }
178
+ else {
179
+ c = await node.kernel.tx.send({ key, signer: node.signerOf(route.signer), kind: "direct", binding, build: () => ({ to: intent.target, data: intent.data, value: intent.value }) });
180
+ }
181
+ confirmed.push(c);
182
+ if (!operations.includes(key))
183
+ operations.push(key);
184
+ node.store.updateJob(jobId, { addOperation: key });
185
+ }
186
+ const output = cap.output.parse(await plan.output(confirmed, work));
187
+ node.store.updateJob(jobId, { status: "done", result: output });
188
+ return { ok: true, output, decisionId: allowed.id, jobId, operations };
189
+ }
190
+ catch (e) {
191
+ const reason = explain(e);
192
+ const operationId = e instanceof OperationReverted || e instanceof OperationAmbiguous ? e.key ?? e.op?.key ?? null : operations.at(-1) ?? null;
193
+ node.store.updateJob(jobId, { status: "failed", error: reason });
194
+ const decision = node.store.decide({ entrance: caller.entrance, caller: caller.id, capability: cap.name, mode, inputHash, verdict: "refused", gate: "execution", reason, jobId, operationId, model: opts.model ?? null });
195
+ return { ok: false, gate: "execution", reason, decisionId: decision.id, jobId };
196
+ }
197
+ }
198
+ refuse(node, cap, caller, mode, inputHash, gate, reason, jobId) {
199
+ const decision = node.store.decide({ entrance: caller.entrance, caller: caller.id, capability: cap.name, mode, inputHash, verdict: "refused", gate, reason, jobId: jobId ?? null, operationId: null, model: null });
200
+ return { ok: false, gate, reason, decisionId: decision.id, jobId };
201
+ }
202
+ }
203
+ /**
204
+ * The canonical hash of a request: the input without its idempotency key. `jobId` names the job and is not part of
205
+ * what the job does, so a request first made under a derived id and then repeated naming that id is the same request
206
+ * (idempotent), while the same id with another input is a conflict.
207
+ */
208
+ export function hashInput(input) {
209
+ if (input && typeof input === "object" && !Array.isArray(input) && "jobId" in input) {
210
+ const { jobId: _jobId, ...rest } = input;
211
+ return sha256Hex(jcs(rest));
212
+ }
213
+ return sha256Hex(jcs(input ?? {}));
214
+ }
215
+ function modesFor(caller, cap) {
216
+ if (cap.action === "read")
217
+ return [];
218
+ const out = [];
219
+ if (ACTION_RANK[caller.level] >= ACTION_RANK.build)
220
+ out.push("simulate", "build");
221
+ if (ACTION_RANK[caller.level] >= ACTION_RANK[cap.action])
222
+ out.push("act");
223
+ return out;
224
+ }
225
+ function routeOf(step) {
226
+ return { route: step.route ?? "execute", signer: step.by ?? "operator" };
227
+ }
228
+ async function buildAll(steps) {
229
+ const out = [];
230
+ for (const s of steps) {
231
+ try {
232
+ out.push(await s.build([]));
233
+ }
234
+ catch {
235
+ break; // a later step that needs an earlier receipt cannot be built ahead
236
+ }
237
+ }
238
+ return out;
239
+ }
240
+ async function feeReserve(node, steps) {
241
+ if (!steps)
242
+ return 0n;
243
+ const fee = await node.world.provider.getFeeData();
244
+ const price = fee.maxFeePerGas ?? fee.gasPrice ?? 0n;
245
+ return BigInt(steps) * FEE_RESERVE_GAS * price;
246
+ }
247
+ function summarize(gates, steps) {
248
+ const names = [...new Set(gates.map((g) => g.gate))];
249
+ return `all gates passed (${names.join(", ")}); ${steps} step(s)`;
250
+ }
251
+ function explain(e) {
252
+ if (e instanceof OperationReverted || e instanceof OperationAmbiguous)
253
+ return e.message;
254
+ if (e instanceof Error)
255
+ return e.message;
256
+ return String(e);
257
+ }
258
+ /** True when a result is a success (for callers that narrow). */
259
+ export function succeeded(r) {
260
+ return r.ok;
261
+ }
@@ -0,0 +1,143 @@
1
+ import { getAddress } from "ethers";
2
+ import { z } from "zod";
3
+ import { accountSubject } from "../core/kya.js";
4
+ import { parseDataUri, sha256Hex, jcs } from "../core/canonical.js";
5
+ /** Shared schema fragments. */
6
+ export const zAddress = z.string().regex(/^0x[0-9a-fA-F]{40}$/, "an address (0x + 40 hex digits)");
7
+ export const zBytes32 = z.string().regex(/^0x[0-9a-fA-F]{64}$/, "a 32-byte hex value");
8
+ export const zUint = z.string().regex(/^\d+$/, "a decimal integer");
9
+ export const zHex = z.string().regex(/^0x([0-9a-fA-F]{2})*$/, "hex bytes");
10
+ /** A boolean that also reads "true" / "false" (CLI flags). */
11
+ export const zBool = z.preprocess((v) => (v === "true" ? true : v === "false" ? false : v), z.boolean());
12
+ /** The caller's idempotency key for a job; derived from the semantic input when absent. */
13
+ export const zJobId = z.string().min(1).max(128).regex(/^[A-Za-z0-9:_.-]+$/, "a job id: letters, digits, : _ . -").optional();
14
+ /** The account a build is for (`build` mode): a third party gets calldata it can execute itself. */
15
+ export const zAs = zAddress.optional().describe("build mode: the account the calldata is for (default: the cell)");
16
+ /** A task document a tender points at (foga.task/1), as the genesis publishes it. */
17
+ export const TaskDocumentSchema = z.object({
18
+ schema: z.literal("foga.task/1"),
19
+ skill: z.string(),
20
+ packageHash: zBytes32,
21
+ input: z.unknown(),
22
+ acceptance: z.object({ mode: z.string(), profile: z.string().optional(), verifier: zAddress.optional(), answerHash: zBytes32.optional() }).passthrough(),
23
+ deliverable: z.string().optional(),
24
+ });
25
+ /** Everything the policy and the capabilities need to know about one tender, read from the chain. */
26
+ export async function readTender(node, tokenId, opts = {}) {
27
+ const k = node.kernel;
28
+ const token = k.tenders.contract();
29
+ const [terms, authority, escrow, cancelled, completions, count, demander, taskURI, binding] = await Promise.all([
30
+ k.tenders.terms(tokenId),
31
+ k.tenders.acceptanceAuthorityOf(tokenId),
32
+ k.tenders.escrowBalanceOf(tokenId),
33
+ k.tenders.isCancelled(tokenId),
34
+ token.completionsOf(tokenId).then((v) => BigInt(v)),
35
+ k.tenders.submissionCount(tokenId),
36
+ token.ownerOf(tokenId).then((a) => String(a)),
37
+ token.taskURI(tokenId).then((u) => String(u)),
38
+ token.taskOf(tokenId),
39
+ ]);
40
+ let task = null;
41
+ if (taskURI.startsWith("data:")) {
42
+ try {
43
+ const parsed = TaskDocumentSchema.safeParse(parseDataUri(taskURI));
44
+ task = parsed.success ? parsed.data : null;
45
+ }
46
+ catch {
47
+ task = null;
48
+ }
49
+ }
50
+ const submissions = [];
51
+ // submission ids start at 1 (TaskToken: the count is the last id)
52
+ for (let i = 1n; i <= count; i++) {
53
+ const s = await k.tenders.submissionOf(tokenId, i);
54
+ submissions.push({ id: i.toString(), fulfiller: s.fulfiller, status: s.status, machineSettled: s.machineSettled, resultHash: s.resultHash });
55
+ }
56
+ let counterpartyLevel = null;
57
+ if (opts.counterparty && node.config.identity.standing) {
58
+ const st = node.config.identity.standing;
59
+ try {
60
+ const r = await k.kya.resolve(accountSubject(node.chainId, demander), st.schemeId, st.issuers);
61
+ counterpartyLevel = Number(r.level);
62
+ }
63
+ catch {
64
+ counterpartyLevel = null;
65
+ }
66
+ }
67
+ return {
68
+ facts: {
69
+ tokenId,
70
+ rewardWei: terms.rewardPerCompletion,
71
+ asset: terms.asset,
72
+ submitBy: terms.submitBy,
73
+ settleBy: terms.settleBy,
74
+ maxCompletions: terms.maxCompletions,
75
+ completions,
76
+ acceptanceAuthority: getAddress(authority),
77
+ cancelled,
78
+ escrowWei: escrow,
79
+ skill: task?.skill ?? null,
80
+ packageHash: task?.packageHash ?? null,
81
+ acceptanceProfile: task?.acceptance.profile ?? null,
82
+ counterpartyLevel,
83
+ },
84
+ demander: getAddress(demander),
85
+ taskURI,
86
+ task,
87
+ taskHash: String(binding.taskHash),
88
+ tdHash: String(binding.tdHash),
89
+ version: Number(binding.version),
90
+ submissions,
91
+ };
92
+ }
93
+ /** A tender's JSON view for outputs (bigints as strings). */
94
+ export function tenderOutput(v) {
95
+ const f = v.facts;
96
+ return {
97
+ tokenId: f.tokenId.toString(),
98
+ demander: v.demander,
99
+ rewardWei: f.rewardWei.toString(),
100
+ asset: f.asset,
101
+ submitBy: Number(f.submitBy),
102
+ settleBy: Number(f.settleBy),
103
+ maxCompletions: Number(f.maxCompletions),
104
+ completions: Number(f.completions),
105
+ acceptanceAuthority: f.acceptanceAuthority,
106
+ cancelled: f.cancelled,
107
+ escrowWei: f.escrowWei.toString(),
108
+ taskURI: v.taskURI.length > 200 ? `${v.taskURI.slice(0, 200)}… (${v.taskURI.length} chars)` : v.taskURI,
109
+ task: v.task,
110
+ taskHash: v.taskHash,
111
+ tdHash: v.tdHash,
112
+ version: v.version,
113
+ submissions: v.submissions,
114
+ };
115
+ }
116
+ export const TenderOutputSchema = z.object({
117
+ tokenId: zUint,
118
+ demander: zAddress,
119
+ rewardWei: zUint,
120
+ asset: zAddress,
121
+ submitBy: z.number(),
122
+ settleBy: z.number(),
123
+ maxCompletions: z.number(),
124
+ completions: z.number(),
125
+ acceptanceAuthority: zAddress,
126
+ cancelled: z.boolean(),
127
+ escrowWei: zUint,
128
+ taskURI: z.string(),
129
+ task: TaskDocumentSchema.nullable(),
130
+ taskHash: zBytes32,
131
+ tdHash: zBytes32,
132
+ version: z.number(),
133
+ submissions: z.array(z.object({ id: zUint, fulfiller: zAddress, status: z.number(), machineSettled: z.boolean(), resultHash: zBytes32 })),
134
+ });
135
+ /** A short, stable id from semantic input (the default job id of an act). */
136
+ export function derivedJobId(prefix, parts) {
137
+ return `${prefix}:${sha256Hex(jcs(parts)).slice(2, 18)}`;
138
+ }
139
+ /** The job of a tender's n-th completion by this cell: the second legitimate completion of a tender is another job. */
140
+ export function tenderJobId(tokenId, completions) {
141
+ return `tender:${tokenId}:c${completions}`;
142
+ }
143
+ export const GateSchema = z.object({ gate: z.string(), ok: z.boolean(), reason: z.string() });
@@ -0,0 +1,107 @@
1
+ import { join } from "node:path";
2
+ import { z } from "zod";
3
+ import { kindId, skillDescriptor } from "../core/ard.js";
4
+ import { dataUriJcs } from "../core/canonical.js";
5
+ import { buildSkillPackage, readSkillDir } from "../core/pack/skillpack.js";
6
+ import { skillUnitDefinition } from "../core/units.js";
7
+ import { ok, refused, safetyGate } from "../node/policy.js";
8
+ import { derivedJobId, zBool, zBytes32, zJobId, zUint } from "./common.js";
9
+ /**
10
+ * The cell's own identity and capability acts: minting a skill it will offer (ERC-8338 token, ERC-8444 record,
11
+ * ERC-8454 unit — what the genesis does in T2) and keeping its anchor alive (ERC-8434 heartbeat in every registry
12
+ * it is bound in). Both are the cell's acts through its account.
13
+ */
14
+ export const skill_mint = {
15
+ name: "skill_mint",
16
+ title: "Mint a skill the cell holds",
17
+ description: "Package a skill directory, mint its ERC-8338 token to the cell, register its ERC-8444 skill record and its ERC-8454 unit, and record it in the cell's configuration as held (and offered, when asked).",
18
+ action: "act",
19
+ visibility: "operator",
20
+ principal: "cell",
21
+ ercs: [8338, 8444, 8454],
22
+ input: z.object({ skill: z.string().regex(/^[a-z0-9][a-z0-9-]*$/, "a skill directory name"), offer: zBool.default(true), priceEth: z.string().regex(/^\d+(\.\d+)?$/).default("0"), jobId: zJobId }),
23
+ output: z.object({ skill: z.string(), tokenId: zUint, recordId: zBytes32, unitId: zUint, mdHash: zBytes32, packageHash: zBytes32, offered: z.boolean(), operations: z.array(z.string()), jobId: z.string() }),
24
+ async plan({ node }, input) {
25
+ const k = node.kernel;
26
+ const cell = node.body;
27
+ const chainId = node.chainId;
28
+ const dir = join(node.skillsDir, input.skill);
29
+ const pkg = buildSkillPackage(readSkillDir(dir));
30
+ const gates = [];
31
+ const [aidState, status] = await Promise.all([k.aid.stateName(cell.address), cell.status()]);
32
+ gates.push(safetyGate(aidState, status, "plain"));
33
+ const already = node.config.skills.held[input.skill];
34
+ if (already)
35
+ gates.push(refused("policy", `the cell already holds ${input.skill} as token ${already.tokenId}; update the skill instead`));
36
+ else
37
+ gates.push(ok("policy", `${input.skill} v${pkg.version}: mdHash ${pkg.mdHash.slice(0, 10)}…, packageHash ${pkg.packageHash.slice(0, 10)}…`));
38
+ const packageURI = dataUriJcs({ skill: input.skill, version: pkg.version, mdHash: pkg.mdHash, packageHash: pkg.packageHash, rootCid: pkg.rootCid, source: `${node.config.skills.dir}/${input.skill}` });
39
+ const jobId = input.jobId ?? derivedJobId(`skill-mint:${input.skill}`, { packageHash: pkg.packageHash, cell: cell.address });
40
+ const tokenOf = (prior) => k.skills.tokenIdFromReceipt(prior[0].receipt);
41
+ const steps = [
42
+ { name: "mint", kind: "plain", build: () => ({ target: k.skills.address, value: 0n, data: k.skills.encodeMint(cell.address, cell.address, pkg, packageURI), label: `mintSkill(${input.skill})` }) },
43
+ {
44
+ name: "record",
45
+ kind: "plain",
46
+ build: (prior) => {
47
+ const tokenId = tokenOf(prior);
48
+ const descriptor = skillDescriptor({ chainId, controller: cell.address, slug: input.skill, name: input.skill, description: String(pkg.manifest.summary ?? input.skill), packageHash: pkg.packageHash, packageURI, price: { amount: input.priceEth, currency: "ETH", asset: `eip155:${chainId}/slip44:60` } });
49
+ return { target: k.ard.address, value: 0n, data: k.ard.encodeRegister(kindId("skill"), k.skills.address, tokenId, descriptor), label: `ARD register(skill ${tokenId})` };
50
+ },
51
+ },
52
+ { name: "unit", kind: "plain", build: (prior) => ({ target: k.units.registryAddress, value: 0n, data: k.units.encodeRegisterUnit(cell.address, skillUnitDefinition({ chainId, skillToken: k.skills.address, tokenId: tokenOf(prior), holder: cell.address, name: input.skill }), []), label: `AUF registerUnit(skill ${input.skill})` }) },
53
+ ];
54
+ return {
55
+ job: { id: jobId, kind: "skill-mint", input: { skill: input.skill, packageHash: pkg.packageHash } },
56
+ gates,
57
+ valueWei: 0n,
58
+ steps,
59
+ output: (confirmed) => {
60
+ const tokenId = tokenOf(confirmed);
61
+ const recordId = k.ard.recordIdFromReceipt(confirmed[1].receipt);
62
+ const unitId = k.units.unitIdFromReceipt(confirmed[2].receipt);
63
+ // the configuration follows the chain: the skill is now held (and offered, when asked)
64
+ node.config.skills.held[input.skill] = { tokenId: tokenId.toString(), recordId, unitId: unitId.toString(), packageHash: pkg.packageHash };
65
+ if (input.offer && !node.config.skills.offered.includes(input.skill))
66
+ node.config.skills.offered.push(input.skill);
67
+ node.saveConfig();
68
+ return { skill: input.skill, tokenId: tokenId.toString(), recordId, unitId: unitId.toString(), mdHash: pkg.mdHash, packageHash: pkg.packageHash, offered: node.config.skills.offered.includes(input.skill), operations: steps.map((s) => `job:${jobId}:${s.name}`), jobId };
69
+ },
70
+ };
71
+ },
72
+ };
73
+ export const aid_heartbeat = {
74
+ name: "aid_heartbeat",
75
+ title: "Heartbeat",
76
+ description: "Keep the cell's anchor ACTIVE: an ERC-8434 heartbeat in the reference registry and in the backing registry where the world has one. The watcher sends it on schedule; a caller may ask for one.",
77
+ action: "act",
78
+ visibility: "caller",
79
+ principal: "cell",
80
+ ercs: [8434],
81
+ input: z.object({ jobId: zJobId }),
82
+ output: z.object({ registries: z.array(z.object({ registry: z.string(), state: z.string(), txHash: z.string() })), jobId: z.string() }),
83
+ async plan({ node }, input) {
84
+ const k = node.kernel;
85
+ const cell = node.body;
86
+ const gates = [];
87
+ const [aidState, status] = await Promise.all([k.aid.stateName(cell.address), cell.status()]);
88
+ if (aidState === "RETIRED")
89
+ gates.push(refused("policy", "anchor RETIRED: nothing to keep alive"));
90
+ else if (aidState === "DORMANT" && (await k.aid.bindingOf(cell.address)).registry === "0x0000000000000000000000000000000000000000")
91
+ gates.push(refused("policy", "the anchor is not bound to an agent: nothing to keep alive"));
92
+ else
93
+ gates.push(ok("policy", `anchor ${aidState}`));
94
+ gates.push(safetyGate(aidState, status, "plain"));
95
+ const registries = [k.aid.address, ...(k.aid.backing ? [k.aid.backing.address] : [])];
96
+ const jobId = input.jobId ?? derivedJobId("heartbeat", { at: Math.floor(Date.now() / 60_000) });
97
+ const steps = registries.map((registry, i) => ({ name: i === 0 ? "heartbeat" : "heartbeat.backing", kind: "plain", build: () => ({ target: registry, value: 0n, data: k.aid.encodeHeartbeat(), label: `heartbeat() in ${registry}` }) }));
98
+ return {
99
+ job: { id: jobId, kind: "heartbeat", input: { registries } },
100
+ gates,
101
+ valueWei: 0n,
102
+ steps,
103
+ output: async (confirmed) => ({ registries: await Promise.all(registries.map(async (registry, i) => ({ registry, state: await k.aid.stateName(cell.address, registry), txHash: confirmed[i]?.txHash ?? "" }))), jobId }),
104
+ };
105
+ },
106
+ };
107
+ export const IDENTITY_CAPABILITIES = [skill_mint, aid_heartbeat];
@@ -0,0 +1,14 @@
1
+ import { ADMIN_CAPABILITIES } from "./admin.js";
2
+ import { Catalog } from "./catalog.js";
3
+ import { IDENTITY_CAPABILITIES } from "./identity.js";
4
+ import { READ_CAPABILITIES } from "./read.js";
5
+ import { TENDER_CAPABILITIES } from "./tenders.js";
6
+ export { Catalog, hashInput, succeeded } from "./catalog.js";
7
+ export * from "./types.js";
8
+ /** The catalog every entrance exposes: the Loop D path of the design memo's 43 capabilities, in Phase 3a. */
9
+ export function defaultCatalog() {
10
+ const c = new Catalog();
11
+ for (const cap of [...READ_CAPABILITIES, ...TENDER_CAPABILITIES, ...IDENTITY_CAPABILITIES, ...ADMIN_CAPABILITIES])
12
+ c.register(cap);
13
+ return c;
14
+ }