@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,157 @@
1
+ import { keccak256 } from "ethers";
2
+ import { abi, accountRef, dataUriJcs, jcs, keccakUtf8 } from "./canonical.js";
3
+ import { eventArgs } from "./world.js";
4
+ /** ERC-8468 Agent Safety Framework: specifications a cell adopts and interventions it must honor. */
5
+ export const Enforcement = { self: 1, account: 2, runtime: 4, custodian: 8, contract: 16, collective: 32 };
6
+ export const Rights = { suspend: 1, resume: 2, terminate: 4 };
7
+ export const Restriction = { noNewRisk: 1, shrink: 2, essentialOnly: 4, deriskOnly: 8, fullStop: 16 };
8
+ export const Combinator = { All: 0, Any: 1, KOf: 2 };
9
+ export const Verdict = { Unknown: 0, Satisfied: 1, NotSatisfied: 2 };
10
+ export const VerdictName = ["Unknown", "Satisfied", "NotSatisfied"];
11
+ export const ReasonName = ["None", "NoBinding", "Terminated", "Suspended", "NotYetValid", "Expired", "SpecMismatch", "VersionMismatch", "AdopterMismatch", "SubjectMismatch", "ScopeMismatch", "EnforcementMismatch", "ParentNotSatisfied", "SpecRegistryUnavailable", "IntervenerMismatch", "StoppedByIntervener"];
12
+ export const BindingStateName = ["None", "Active", "Suspended", "Terminated"];
13
+ const ZERO32 = "0x" + "00".repeat(32);
14
+ export function subjectKeyOf(ref) {
15
+ return keccakUtf8(ref);
16
+ }
17
+ export function accountSubjectKey(chainId, account) {
18
+ return keccakUtf8(accountRef(chainId, account));
19
+ }
20
+ export function scopeKeyOf(scope) {
21
+ return keccakUtf8(jcs(scope));
22
+ }
23
+ export function specIdOf(chainId, registry, publisher, salt) {
24
+ return keccak256(abi.encode(["uint256", "address", "address", "bytes32"], [chainId, registry, publisher, salt]));
25
+ }
26
+ export const REQUIREMENT_TUPLE = "tuple(uint8 combinator,uint16 k,tuple(address specRegistry,bytes32 specId,uint32[] versions,address adopter,bytes32 subjectKey,bytes32 scopeKey,uint16 enforcement,tuple(address account,uint8 rights)[] interveners)[] clauses)";
27
+ export function requirementIdOf(req) {
28
+ return keccak256(abi.encode([REQUIREMENT_TUPLE], [req]));
29
+ }
30
+ export function oneClause(p) {
31
+ return {
32
+ combinator: Combinator.All,
33
+ k: 0,
34
+ clauses: [
35
+ {
36
+ specRegistry: p.specRegistry,
37
+ specId: p.specId,
38
+ versions: p.versions ?? [1],
39
+ adopter: p.adopter ?? "0x0000000000000000000000000000000000000000",
40
+ subjectKey: p.subjectKey ?? ZERO32,
41
+ scopeKey: p.scopeKey ?? ZERO32,
42
+ enforcement: p.enforcement ?? Enforcement.account,
43
+ interveners: p.interveners ?? [],
44
+ },
45
+ ],
46
+ };
47
+ }
48
+ export class ASF {
49
+ world;
50
+ constructor(world) {
51
+ this.world = world;
52
+ }
53
+ specs(runner) {
54
+ return this.world.contract("8468/ASFSpecificationRegistry", runner);
55
+ }
56
+ bindings(runner) {
57
+ return this.world.contract("8468/ASFBindingRegistry", runner);
58
+ }
59
+ get specRegistryAddress() {
60
+ return this.world.address("8468/ASFSpecificationRegistry");
61
+ }
62
+ get bindingRegistryAddress() {
63
+ return this.world.address("8468/ASFBindingRegistry");
64
+ }
65
+ /** The commitment of a specification document: contentHash = keccak256(JCS(document)); locator = data: URI. */
66
+ specificationCommitment(document) {
67
+ return { contentHash: keccakUtf8(jcs(document)), locator: dataUriJcs(document) };
68
+ }
69
+ encodeRegisterSpecification(salt, document, requiredProfiles = []) {
70
+ const c = this.specificationCommitment(document);
71
+ return this.specs().interface.encodeFunctionData("registerSpecification", [salt, c.contentHash, c.locator, requiredProfiles]);
72
+ }
73
+ specIdFromReceipt(receipt) {
74
+ return eventArgs(this.specs().interface, receipt, "SpecificationRegistered", this.specRegistryAddress).specId;
75
+ }
76
+ encodeSuspend(bindingId, restrictions, responseRef, reasonRef) {
77
+ return this.bindings().interface.encodeFunctionData("suspend", [bindingId, restrictions, responseRef, reasonRef]);
78
+ }
79
+ encodeResume(bindingId, reasonRef) {
80
+ return this.bindings().interface.encodeFunctionData("resume", [bindingId, reasonRef]);
81
+ }
82
+ /** Publish a safety specification document (version 1). contentHash = keccak256(JCS(document)); the locator is a data: URI. */
83
+ async registerSpecification(publisher, salt, document, requiredProfiles = []) {
84
+ const contentHash = keccakUtf8(jcs(document));
85
+ const locator = dataUriJcs(document);
86
+ const tx = await this.specs(publisher).registerSpecification(salt, contentHash, locator, requiredProfiles);
87
+ const receipt = await tx.wait();
88
+ return { specId: eventArgs(this.specs().interface, receipt, "SpecificationRegistered").specId, contentHash, locator, receipt };
89
+ }
90
+ async versionOf(specId, version) {
91
+ const v = await this.specs().versionOf(specId, version);
92
+ return { contentHash: v.contentHash, locator: v.locator, publishedAt: BigInt(v.publishedAt), deprecated: Boolean(v.deprecated) };
93
+ }
94
+ encodeAdopt(input) {
95
+ return this.bindings().interface.encodeFunctionData("adopt", [input]);
96
+ }
97
+ async computeBindingId(adopter, input) {
98
+ return this.bindings().computeBindingId(adopter, input);
99
+ }
100
+ bindingIdFromReceipt(receipt) {
101
+ return eventArgs(this.bindings().interface, receipt, "BindingCreated", this.bindingRegistryAddress).bindingId;
102
+ }
103
+ async suspend(intervener, bindingId, restrictions, responseRef, reasonRef) {
104
+ const tx = await this.bindings(intervener).suspend(bindingId, restrictions, responseRef, reasonRef);
105
+ return tx.wait();
106
+ }
107
+ async resume(intervener, bindingId, reasonRef) {
108
+ const tx = await this.bindings(intervener).resume(bindingId, reasonRef);
109
+ return tx.wait();
110
+ }
111
+ async check(requirement, bindingIds) {
112
+ const r = await this.bindings().check(requirement, bindingIds);
113
+ return { verdict: Number(r[0]), reasons: Array.from(r[1], (x) => Number(x)), restrictions: Array.from(r[2], (x) => Number(x)) };
114
+ }
115
+ async bindingOf(bindingId) {
116
+ const b = await this.bindings().bindingOf(bindingId);
117
+ return { adopter: b.adopter, state: Number(b.state), restrictions: Number(b.restrictions), validUntil: BigInt(b.validUntil) };
118
+ }
119
+ /** The binding a cell adopts for the FOGA cell specification. */
120
+ cellBindingInput(p) {
121
+ return {
122
+ subjectKey: accountSubjectKey(p.chainId, p.cell),
123
+ scopeKey: scopeKeyOf({ ref: accountRef(p.chainId, p.cell) }),
124
+ specRegistry: this.specRegistryAddress,
125
+ specId: p.specId,
126
+ version: p.version,
127
+ contentHash: p.contentHash,
128
+ paramsHash: keccakUtf8(jcs(p.params)),
129
+ paramsLocator: dataUriJcs(p.params),
130
+ authorityRef: keccakUtf8(jcs(p.authority)),
131
+ enforcement: Enforcement.account | Enforcement.contract,
132
+ validFrom: 0,
133
+ validUntil: p.validUntil ?? 0,
134
+ parentBindingId: ZERO32,
135
+ salt: p.salt ?? "0x" + "00".repeat(31) + "01",
136
+ interveners: [{ account: p.guardian, rights: Rights.suspend | Rights.resume }],
137
+ };
138
+ }
139
+ }
140
+ /** The FOGA cell safety specification, version 1. */
141
+ export function fogaCellSpecification() {
142
+ return {
143
+ asf: "1.0",
144
+ kind: "safety-specification",
145
+ name: "FOGA Cell Safety Specification",
146
+ version: 1,
147
+ summary: "The baseline every FOGA cell adopts: commitment-class actions only while the anchor is ACTIVE, interventions honored at the execution gate, de-risking always open.",
148
+ clauses: [
149
+ { id: "aid-active", text: "A cell MUST NOT make a commitment-class call (submit, join, adopt, assert) while its ERC-8434 anchor is not ACTIVE. Enforced by the cell account's AID-state gate." },
150
+ { id: "honor-stop", text: "A cell MUST stop commitment-class calls while its binding is suspended and MUST NOT continue under a full stop. Enforced by the cell account's execution gate against the Binding Registry." },
151
+ { id: "derisk-open", text: "Withdrawals of earned credit, refunds and leaving a swarm remain allowed under a derisk-only suspension and MUST be listed as de-risk actions by the owner." },
152
+ { id: "attribution", text: "Only calls routed through the cell account are the cell's acts; a direct call by the operator key is the operator's own." },
153
+ ],
154
+ enforcement: ["account", "contract"],
155
+ profiles: [],
156
+ };
157
+ }
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import { AbiCoder, getAddress, keccak256, toUtf8Bytes } from "ethers";
3
+ /**
4
+ * RFC 8785 JSON Canonicalization Scheme (the subset the ten ERCs use): object keys sorted by UTF-16 code
5
+ * units, no whitespace, strings and finite numbers serialized as JSON.stringify does, `undefined`
6
+ * properties skipped. Throws on non-finite numbers, bigints and functions.
7
+ */
8
+ export function jcs(value) {
9
+ if (value === null)
10
+ return "null";
11
+ switch (typeof value) {
12
+ case "string":
13
+ case "boolean":
14
+ return JSON.stringify(value);
15
+ case "number":
16
+ if (!Number.isFinite(value))
17
+ throw new Error("JCS: non-finite number");
18
+ return JSON.stringify(value);
19
+ case "bigint":
20
+ throw new Error("JCS: bigint is not JSON; stringify it first");
21
+ case "undefined":
22
+ throw new Error("JCS: undefined is not JSON");
23
+ case "object": {
24
+ if (Array.isArray(value))
25
+ return "[" + value.map((v) => (v === undefined ? "null" : jcs(v))).join(",") + "]";
26
+ const obj = value;
27
+ const keys = Object.keys(obj)
28
+ .filter((k) => obj[k] !== undefined)
29
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
30
+ return "{" + keys.map((k) => JSON.stringify(k) + ":" + jcs(obj[k])).join(",") + "}";
31
+ }
32
+ default:
33
+ throw new Error(`JCS: unsupported type ${typeof value}`);
34
+ }
35
+ }
36
+ export function sha256Hex(data) {
37
+ const buf = typeof data === "string" ? Buffer.from(data, "utf8") : Buffer.from(data);
38
+ return "0x" + createHash("sha256").update(buf).digest("hex");
39
+ }
40
+ export function sha256Bytes(data) {
41
+ return new Uint8Array(createHash("sha256").update(Buffer.from(data)).digest());
42
+ }
43
+ /** keccak256 over the UTF-8 bytes of a string (identifier ids, AID / AUF / ASF content hashes). */
44
+ export function keccakUtf8(s) {
45
+ return keccak256(toUtf8Bytes(s));
46
+ }
47
+ /** keccak256(UTF-8(JCS(doc))) — AID documents, ASF documents, AUF definitions and companions. */
48
+ export function keccakJcs(doc) {
49
+ return keccakUtf8(jcs(doc));
50
+ }
51
+ /** SHA-256(UTF-8(JCS(doc))) — ARD descriptors (content members) and SAI pack hashes. */
52
+ export function sha256Jcs(doc) {
53
+ return sha256Hex(jcs(doc));
54
+ }
55
+ export function dataUri(doc) {
56
+ return "data:application/json;base64," + Buffer.from(JSON.stringify(doc)).toString("base64");
57
+ }
58
+ export function dataUriJcs(doc) {
59
+ return "data:application/json;base64," + Buffer.from(jcs(doc)).toString("base64");
60
+ }
61
+ export function parseDataUri(uri) {
62
+ const m = /^data:application\/json(;base64)?,(.*)$/s.exec(uri);
63
+ if (!m)
64
+ throw new Error("not a JSON data: URI");
65
+ const body = m[1] ? Buffer.from(m[2], "base64").toString("utf8") : decodeURIComponent(m[2]);
66
+ return JSON.parse(body);
67
+ }
68
+ /** "eip155:<chainId>:<0x + lowercase hex>" — CAIP-10 account reference used by AID, ASF and AUF. */
69
+ export function accountRef(chainId, address) {
70
+ return `eip155:${chainId}:${getAddress(address).toLowerCase()}`;
71
+ }
72
+ /** "eip155:<chainId>/erc721:<0x lowercase>/<id>" — CAIP-19 token reference used by ARD and AUF. */
73
+ export function erc721Ref(chainId, collection, id) {
74
+ return `eip155:${chainId}/erc721:${getAddress(collection).toLowerCase()}/${id}`;
75
+ }
76
+ export const abi = AbiCoder.defaultAbiCoder();
77
+ export function nowSeconds() {
78
+ return Math.floor(Date.now() / 1000);
79
+ }
@@ -0,0 +1,52 @@
1
+ import { Contract } from "ethers";
2
+ import { eventArgs } from "./world.js";
3
+ /**
4
+ * ERC-8004 Identity Registry surface: the official registry on public chains, the local stub on Anvil.
5
+ * Both expose register(string) / ownerOf / getAgentWallet / setMetadata / transferFrom.
6
+ */
7
+ export const ERC8004_ABI = [
8
+ "function register() returns (uint256)",
9
+ "function register(string agentURI) returns (uint256)",
10
+ "function ownerOf(uint256 agentId) view returns (address)",
11
+ "function getAgentWallet(uint256 agentId) view returns (address)",
12
+ "function setAgentURI(uint256 agentId, string newURI)",
13
+ "function tokenURI(uint256 agentId) view returns (string)",
14
+ "function getMetadata(uint256 agentId, string metadataKey) view returns (bytes)",
15
+ "function setMetadata(uint256 agentId, string metadataKey, bytes metadataValue)",
16
+ "function transferFrom(address from, address to, uint256 agentId)",
17
+ "function safeTransferFrom(address from, address to, uint256 agentId)",
18
+ "function balanceOf(address owner) view returns (uint256)",
19
+ "function supportsInterface(bytes4) view returns (bool)",
20
+ "event Registered(uint256 indexed agentId, string agentURI, address indexed owner)",
21
+ "event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)",
22
+ "event MetadataSet(uint256 indexed agentId, string indexed indexedMetadataKey, string metadataKey, bytes metadataValue)",
23
+ ];
24
+ export class Identity8004 {
25
+ world;
26
+ address;
27
+ constructor(world) {
28
+ this.world = world;
29
+ this.address = world.address("identity8004");
30
+ }
31
+ contract(runner) {
32
+ return new Contract(this.address, ERC8004_ABI, runner ?? this.world.provider);
33
+ }
34
+ /** Calldata for register(agentURI) — used when a contract account registers through `execute`. */
35
+ encodeRegister(agentURI) {
36
+ return this.contract().interface.encodeFunctionData("register(string)", [agentURI]);
37
+ }
38
+ encodeTransfer(from, to, agentId) {
39
+ return this.contract().interface.encodeFunctionData("transferFrom", [from, to, agentId]);
40
+ }
41
+ /** The agent id minted by a register() call, from the Transfer(0, owner, id) event in the receipt. */
42
+ agentIdFromReceipt(receipt) {
43
+ const args = eventArgs(this.contract().interface, receipt, "Transfer", this.address);
44
+ return BigInt(args.tokenId);
45
+ }
46
+ async ownerOf(agentId) {
47
+ return this.contract().ownerOf(agentId);
48
+ }
49
+ async agentWallet(agentId) {
50
+ return this.contract().getAgentWallet(agentId);
51
+ }
52
+ }
@@ -0,0 +1,225 @@
1
+ import { keccak256, toUtf8Bytes } from "ethers";
2
+ import { abi, dataUri } from "./canonical.js";
3
+ import { eventArgs } from "./world.js";
4
+ /** ERC-8419 Know-Your-Agent: schemes, attested assertions, checks. */
5
+ export const SubjectType = {
6
+ erc8004: keccak256(toUtf8Bytes("erc8004")),
7
+ account: keccak256(toUtf8Bytes("account")),
8
+ erc721: keccak256(toUtf8Bytes("erc721")),
9
+ did: keccak256(toUtf8Bytes("did")),
10
+ };
11
+ export const SchemeMode = { ATTESTED: 0, PROVED: 1 };
12
+ export const BindingKind = { IDENTITY: 0, CONTROLLER: 1, INSTANCE: 2 };
13
+ export const ResultKind = { ORDERED_LEVEL: 0, CATEGORICAL: 1, OPAQUE: 2 };
14
+ /** An `account` subject: abi.encode(uint256 chainId, address account). Schemes for it must use IDENTITY binding. */
15
+ export function accountSubject(chainId, account) {
16
+ return { subjectType: SubjectType.account, subjectData: abi.encode(["uint256", "address"], [chainId, account]) };
17
+ }
18
+ export function erc8004Subject(chainId, identityRegistry, agentId) {
19
+ return { subjectType: SubjectType.erc8004, subjectData: abi.encode(["uint256", "address", "uint256"], [chainId, identityRegistry, agentId]) };
20
+ }
21
+ export function subjectKey(s) {
22
+ return keccak256(abi.encode(["bytes32", "bytes"], [s.subjectType, s.subjectData]));
23
+ }
24
+ /** levelMask: bit n set <=> level n is a declared result. */
25
+ export function levelMaskOf(levels) {
26
+ return levels.reduce((m, l) => m | (1n << BigInt(l)), 0n);
27
+ }
28
+ export class KYA {
29
+ world;
30
+ constructor(world) {
31
+ this.world = world;
32
+ }
33
+ schemes(runner) {
34
+ return this.world.contract("8419/KYASchemeRegistry", runner);
35
+ }
36
+ registry(runner) {
37
+ return this.world.contract("8419/KYARegistry", runner);
38
+ }
39
+ get registryAddress() {
40
+ return this.world.address("8419/KYARegistry");
41
+ }
42
+ /** SchemeInput for an ATTESTED, ORDERED_LEVEL scheme (IDENTITY binding: the only kind a plain address satisfies). */
43
+ attestedSchemeInput(descriptor) {
44
+ const bytes = Buffer.from(JSON.stringify(descriptor));
45
+ return {
46
+ schemeURI: dataUri(descriptor),
47
+ schemeHash: keccak256(bytes),
48
+ mode: SchemeMode.ATTESTED,
49
+ binding: BindingKind.IDENTITY,
50
+ resultKind: ResultKind.ORDERED_LEVEL,
51
+ levelMask: levelMaskOf(Object.keys(descriptor.levels).map(Number)),
52
+ verifier: "0x0000000000000000000000000000000000000000",
53
+ predecessor: "0x0000000000000000000000000000000000000000000000000000000000000000",
54
+ };
55
+ }
56
+ /** SchemeInput for a PROVED scheme admitted by an IKYAVerifier. */
57
+ provedSchemeInput(descriptor, verifier) {
58
+ return { ...this.attestedSchemeInput(descriptor), mode: SchemeMode.PROVED, verifier };
59
+ }
60
+ encodeRegisterScheme(input) {
61
+ return this.schemes().interface.encodeFunctionData("registerScheme", [input]);
62
+ }
63
+ encodeAttest(p) {
64
+ return this.registry().interface.encodeFunctionData("attest", [p.subject, p.schemeId, p.level, p.claimDigest, p.expiresAt, p.evidenceURI ?? "", p.evidenceHash ?? "0x" + "00".repeat(32)]);
65
+ }
66
+ /** attestWithProof(subject, schemeId, publicInputs, proof, evidenceURI): the proved path; the verifier adapter becomes the issuer. */
67
+ encodeAttestWithProof(subject, schemeId, publicInputs, proof, evidenceURI = "") {
68
+ return this.registry().interface.encodeFunctionData("attestWithProof", [subject, schemeId, publicInputs, proof, evidenceURI]);
69
+ }
70
+ async isNullifierUsed(schemeId, nullifier) {
71
+ return this.registry().isNullifierUsed(schemeId, nullifier);
72
+ }
73
+ encodeRevoke(assertionId, reasonCode) {
74
+ return this.registry().interface.encodeFunctionData("revoke", [assertionId, reasonCode]);
75
+ }
76
+ schemeIdFromReceipt(receipt) {
77
+ return eventArgs(this.schemes().interface, receipt, "SchemeRegistered", this.world.address("8419/KYASchemeRegistry")).schemeId;
78
+ }
79
+ assertionIdFromReceipt(receipt) {
80
+ return eventArgs(this.registry().interface, receipt, "Asserted", this.registryAddress).assertionId;
81
+ }
82
+ get schemeRegistryAddress() {
83
+ return this.world.address("8419/KYASchemeRegistry");
84
+ }
85
+ /**
86
+ * Register an ATTESTED, ORDERED_LEVEL scheme for `account` subjects (IDENTITY binding, the only binding
87
+ * kind a plain address can satisfy). The descriptor is published as a data: URI and committed by keccak.
88
+ */
89
+ async registerAttestedScheme(controller, descriptor) {
90
+ const bytes = Buffer.from(JSON.stringify(descriptor));
91
+ const schemeHash = keccak256(bytes);
92
+ const levels = Object.keys(descriptor.levels).map(Number);
93
+ const input = {
94
+ schemeURI: dataUri(descriptor),
95
+ schemeHash,
96
+ mode: SchemeMode.ATTESTED,
97
+ binding: BindingKind.IDENTITY,
98
+ resultKind: ResultKind.ORDERED_LEVEL,
99
+ levelMask: levelMaskOf(levels),
100
+ verifier: "0x0000000000000000000000000000000000000000",
101
+ predecessor: "0x0000000000000000000000000000000000000000000000000000000000000000",
102
+ };
103
+ const tx = await this.schemes(controller).registerScheme(input);
104
+ const receipt = await tx.wait();
105
+ const schemeId = eventArgs(this.schemes().interface, receipt, "SchemeRegistered").schemeId;
106
+ return { schemeId, receipt, schemeHash };
107
+ }
108
+ /** Register a PROVED scheme whose assertions are admitted by an IKYAVerifier (e.g. the Groth16 adapter). */
109
+ async registerProvedScheme(controller, descriptor, verifier) {
110
+ const bytes = Buffer.from(JSON.stringify(descriptor));
111
+ const schemeHash = keccak256(bytes);
112
+ const levels = Object.keys(descriptor.levels).map(Number);
113
+ const input = {
114
+ schemeURI: dataUri(descriptor),
115
+ schemeHash,
116
+ mode: SchemeMode.PROVED,
117
+ binding: BindingKind.IDENTITY,
118
+ resultKind: ResultKind.ORDERED_LEVEL,
119
+ levelMask: levelMaskOf(levels),
120
+ verifier,
121
+ predecessor: "0x0000000000000000000000000000000000000000000000000000000000000000",
122
+ };
123
+ const tx = await this.schemes(controller).registerScheme(input);
124
+ const receipt = await tx.wait();
125
+ return { schemeId: eventArgs(this.schemes().interface, receipt, "SchemeRegistered").schemeId, receipt, schemeHash };
126
+ }
127
+ /** The issuer records an assertion about a subject (ATTESTED mode). */
128
+ async attest(issuer, p) {
129
+ const tx = await this.registry(issuer).attest(p.subject, p.schemeId, p.level, p.claimDigest, p.expiresAt, p.evidenceURI ?? "", p.evidenceHash ?? "0x" + "00".repeat(32));
130
+ const receipt = await tx.wait();
131
+ return { assertionId: eventArgs(this.registry().interface, receipt, "Asserted").assertionId, receipt };
132
+ }
133
+ async revoke(issuer, assertionId, reasonCode) {
134
+ const tx = await this.registry(issuer).revoke(assertionId, reasonCode);
135
+ return tx.wait();
136
+ }
137
+ async check(subject, schemeId, minLevel, issuers) {
138
+ return this.registry().check(subject, schemeId, minLevel, issuers);
139
+ }
140
+ async resolve(subject, schemeId, issuers) {
141
+ const r = await this.registry().resolve(subject, schemeId, issuers);
142
+ return { level: Number(r.level), expiresAt: BigInt(r.expiresAt), assertionId: r.assertionId };
143
+ }
144
+ async admissionDomain() {
145
+ return this.registry().admissionDomain();
146
+ }
147
+ async getAssertion(assertionId) {
148
+ const a = await this.registry().getAssertion(assertionId);
149
+ return { subjectKey: a.subjectKey, schemeId: a.schemeId, issuer: a.issuer, level: Number(a.level), claimDigest: a.claimDigest, issuedAt: Number(a.issuedAt), expiresAt: Number(a.expiresAt), anchor: a.anchor, status: Number(a.status) };
150
+ }
151
+ }
152
+ /** The descriptor FOGA registers for its attested scheme: a plain ordered ladder of operational levels. */
153
+ export function fogaAttestedSchemeDescriptor() {
154
+ return {
155
+ type: "https://eips.ethereum.org/EIPS/eip-8419#kya-scheme-v1",
156
+ name: "FOGA cell standing (attested) v1",
157
+ description: "An issuer's ordered judgment of a cell's operational standing: 1 = known, 2 = operating under a safety binding, 3 = audited.",
158
+ mode: "attested",
159
+ binding: "identity",
160
+ result: { kind: "ordered-level" },
161
+ dimensions: ["cell-standing"],
162
+ levels: {
163
+ "0": { label: "none", meaning: "no standing asserted" },
164
+ "1": { label: "known", meaning: "the issuer has identified the cell" },
165
+ "2": { label: "bound", meaning: "the cell operates under an adopted safety specification" },
166
+ "3": { label: "audited", meaning: "the issuer reviewed the cell's conduct" },
167
+ },
168
+ evidenceKinds: ["foga:cell-document", "erc8468:binding"],
169
+ issuerPolicy: { kind: "open" },
170
+ };
171
+ }
172
+ /** The descriptor for the credential-pattern proved scheme admitted by the Groth16 adapter (levels 0..4 as the reference circuit). */
173
+ export function fogaProvedSchemeDescriptor() {
174
+ return {
175
+ type: "https://eips.ethereum.org/EIPS/eip-8419#kya-scheme-v1",
176
+ name: "FOGA cell standing (proved, credential pattern) v1",
177
+ description: "A hidden issuer from the admitted issuer set signed a standing level; the cell proves it in zero knowledge (kya-public-v1 layout).",
178
+ mode: "proved",
179
+ binding: "identity",
180
+ result: { kind: "ordered-level" },
181
+ dimensions: ["cell-standing"],
182
+ levels: {
183
+ "0": { label: "none", meaning: "no standing" },
184
+ "1": { label: "known", meaning: "identified" },
185
+ "2": { label: "bound", meaning: "safety-bound" },
186
+ "3": { label: "audited", meaning: "conduct reviewed" },
187
+ "4": { label: "verified", meaning: "credential verified by an admitted issuer" },
188
+ },
189
+ evidenceKinds: ["zk:groth16:kya-public-v1"],
190
+ issuerPolicy: { kind: "issuer-set" },
191
+ };
192
+ }
193
+ /**
194
+ * The descriptor for the predicate-pattern proved scheme (ERC-8419 section 5) admitted by FOGA's own adapter:
195
+ * the scheme is the rule "at least K task completions about the subject, signed by a source of the committed
196
+ * source set, are Accepted inside [t0, t1]"; the proof reveals the subject, the rule's parameters and the result
197
+ * bit. Level 1 = the rule holds for the disclosed parameters (committed in claimDigest); level 0 is declared and
198
+ * never issued (a false predicate is rejected, not recorded).
199
+ */
200
+ export function fogaPredicateSchemeDescriptor() {
201
+ return {
202
+ type: "https://eips.ethereum.org/EIPS/eip-8419#kya-scheme-v1",
203
+ name: "FOGA accepted completions (proved, predicate pattern) v1",
204
+ description: "At least K task-completion records about the cell, signed by a source of the committed source set, carry status Accepted inside the window [t0, t1]; proved in zero knowledge over the records (kya-predicate-count-v1 layout). A fact about a range of records, not a judgment of trustworthiness.",
205
+ mode: "proved",
206
+ binding: "identity",
207
+ result: { kind: "ordered-level" },
208
+ dimensions: ["accepted-completions"],
209
+ levels: {
210
+ "0": { label: "not shown", meaning: "no proof recorded" },
211
+ "1": { label: "shown", meaning: "the rule holds for the parameters committed in claimDigest (challenge, t0, t1, K, source set root)" },
212
+ },
213
+ circuit: {
214
+ name: "kya_predicate_count_v1",
215
+ slots: 8,
216
+ sourceSetDepth: 16,
217
+ publicInputLayout: "abi.encode(bytes32 subjectKey, bytes32 nullifier, bytes32 challenge, uint64 t0, uint64 t1, uint16 k, bytes32 sourceSetRoot, uint64 epoch, uint8 result)",
218
+ record: "Poseidon(subjectKeyHi, subjectKeyLo, taskRef128, status, timestamp) signed EdDSA-Poseidon (Baby Jubjub) by the source",
219
+ nullifier: "Poseidon(secret, schemeIdHi, schemeIdLo, admissionDomainHi, admissionDomainLo, epoch, challengeHi, challengeLo)",
220
+ setup: "development ceremony with a public beacon (prover/build-predicate.mjs); not for production use",
221
+ },
222
+ evidenceKinds: ["zk:groth16:kya-predicate-count-v1"],
223
+ issuerPolicy: { kind: "source-set" },
224
+ };
225
+ }
@@ -0,0 +1,96 @@
1
+ import { getAddress, keccak256, toUtf8Bytes } from "ethers";
2
+ import { abi, dataUriJcs, keccakJcs, parseDataUri } from "./canonical.js";
3
+ /**
4
+ * Realized net cash flow, measured (ERC-8454 observation profile). A receipt states what was observed inside an
5
+ * economic boundary over a window — inflows with their on-chain references, costs, the items that could not be
6
+ * observed — and never a value. The resolver recomputes the receipt from the chain and checks the boundary, the
7
+ * method registration and double counting across receipts.
8
+ */
9
+ export const METHOD_KIND = keccak256(toUtf8Bytes("auf:method-descriptor"));
10
+ export const RECEIPT_SCHEMA = "foga.measurement-receipt/1";
11
+ export const CHECK_SCHEMA = "foga.measurement-check/1";
12
+ export const AssertionKind = { None: 0, Characterization: 1, Observation: 2, Valuation: 3, Extension: 4 };
13
+ /** The method FOGA registers once and every receipt cites by its descriptor hash. */
14
+ export function realizedNetCashFlowMethod() {
15
+ return {
16
+ auf: "1.0",
17
+ kind: "method-descriptor",
18
+ name: "foga.realized-net-cash-flow/1",
19
+ profile: "observation",
20
+ summary: "Realized net cash flow of a unit over a window: inflows that settled on chain to accounts inside the unit's economic boundary, minus the gas the unit's own operations paid, in the asset's smallest denomination. Items that cannot be observed on chain are listed as missing, never written as zero. The result is a measurement, not a valuation: no field of a receipt states a value.",
21
+ rules: [
22
+ { id: "boundary", text: "An inflow counts only when its on-chain recipient is an account of the unit's economic boundary." },
23
+ { id: "settled", text: "An inflow counts only once it settled on chain (a tender settlement, a released escrow settlement); pending, held and refunded amounts are shown and not counted." },
24
+ { id: "attribution", text: "A payment to a key of the unit's operator that did not pass through the unit's account is not the unit's inflow." },
25
+ { id: "once", text: "An inflow reference appears in at most one receipt of a set; a settlement counted for two units is a double count." },
26
+ { id: "costs", text: "Costs are the gas of the unit's own operations in the window, as paid; other costs are listed as missing when not observable." },
27
+ { id: "no-value", text: "A receipt carries no value, price, worth or score." },
28
+ ],
29
+ receiptSchema: RECEIPT_SCHEMA,
30
+ };
31
+ }
32
+ export function receiptHash(receipt) {
33
+ return keccakJcs(receipt);
34
+ }
35
+ export function receiptUri(receipt) {
36
+ return dataUriJcs(receipt);
37
+ }
38
+ export function totals(inflows, gasWei) {
39
+ const inflowsWei = inflows.filter((i) => i.status === "counted").reduce((s, i) => s + BigInt(i.amount), 0n);
40
+ return { inflowsWei: inflowsWei.toString(), costsWei: gasWei.toString(), netWei: (inflowsWei - gasWei).toString() };
41
+ }
42
+ /** ObservationCore payload: (snapshotHash, windowStart, windowEnd). */
43
+ export function observationPayload(snapshotHash, windowStart, windowEnd) {
44
+ return abi.encode(["bytes32", "uint64", "uint64"], [snapshotHash, windowStart, windowEnd]);
45
+ }
46
+ const FORBIDDEN = /"(value|price|worth|valuation|score)"\s*:/i;
47
+ /**
48
+ * Checks one receipt against the chain and against the other receipts of the set. Deterministic: the same
49
+ * receipts and the same chain state give the same report.
50
+ */
51
+ export async function checkReceipt(receipt, others, chain, assertionId) {
52
+ const lines = [];
53
+ const hash = receiptHash(receipt);
54
+ const d = await chain.descriptor(receipt.methodHash);
55
+ lines.push({ rule: "method.registered", pass: d !== null && d.kind === METHOD_KIND, detail: d ? `descriptor ${receipt.methodHash.slice(0, 10)}… registered by ${d.publisher}` : `descriptor ${receipt.methodHash.slice(0, 10)}… is not registered: the receipt is not resolvable` });
56
+ lines.push({ rule: "method.named", pass: receipt.method === "foga.realized-net-cash-flow/1" && receipt.methodHash === keccakJcs(realizedNetCashFlowMethod()), detail: `method ${receipt.method}, hash ${receipt.methodHash === keccakJcs(realizedNetCashFlowMethod()) ? "matches" : "does not match"} the committed descriptor` });
57
+ const latest = await chain.latestVersion(BigInt(receipt.unit.unitId));
58
+ lines.push({ rule: "unit.version", pass: receipt.unit.version >= 1 && receipt.unit.version <= latest, detail: `unit ${receipt.unit.unitId} version ${receipt.unit.version} (latest ${latest})` });
59
+ lines.push({ rule: "window", pass: receipt.window.end >= receipt.window.start, detail: `window [${receipt.window.start}, ${receipt.window.end}]` });
60
+ lines.push({ rule: "no-value", pass: !FORBIDDEN.test(JSON.stringify(receipt)), detail: "no value, price, worth, valuation or score field" });
61
+ const boundary = new Set(receipt.boundary.accounts.map((a) => getAddress(a)));
62
+ let counted = 0n;
63
+ for (const inflow of receipt.inflows) {
64
+ const s = await chain.settled(inflow);
65
+ if (inflow.status === "counted") {
66
+ const ok = s !== null && s.settled && boundary.has(getAddress(s.recipient)) && s.amount === BigInt(inflow.amount);
67
+ if (ok)
68
+ counted += s.amount;
69
+ lines.push({ rule: `inflow.${inflow.ref}`, pass: ok, detail: s ? `${inflow.amount} wei to ${s.recipient}${boundary.has(getAddress(s.recipient)) ? " (inside the boundary)" : " (OUTSIDE the boundary)"}, settled ${s.settled}, chain amount ${s.amount}` : `could not be re-derived from the chain` });
70
+ }
71
+ else {
72
+ const outside = s === null || !boundary.has(getAddress(s.recipient)) || !s.settled;
73
+ lines.push({ rule: `excluded.${inflow.ref}`, pass: outside || Boolean(inflow.reason), detail: `${inflow.amount} wei excluded: ${inflow.reason ?? "no reason given"}` });
74
+ }
75
+ }
76
+ lines.push({ rule: "totals.inflows", pass: counted.toString() === receipt.totals.inflowsWei, detail: `counted ${counted} wei; receipt says ${receipt.totals.inflowsWei}` });
77
+ const net = BigInt(receipt.totals.inflowsWei) - BigInt(receipt.totals.costsWei);
78
+ lines.push({ rule: "totals.net", pass: net.toString() === receipt.totals.netWei && receipt.totals.costsWei === receipt.costs.gasWei, detail: `net ${net} wei = inflows - gas ${receipt.costs.gasWei}` });
79
+ // double counting across the set: the same settlement reference counted in two receipts
80
+ const mine = new Set(receipt.inflows.filter((i) => i.status === "counted").map((i) => i.ref));
81
+ const dup = [];
82
+ for (const o of others)
83
+ for (const i of o.inflows)
84
+ if (i.status === "counted" && mine.has(i.ref))
85
+ dup.push(`${i.ref} (also counted for unit ${o.unit.unitId})`);
86
+ lines.push({ rule: "once", pass: dup.length === 0, detail: dup.length ? `double count: ${dup.join("; ")}` : `no inflow of this receipt is counted by another receipt of the set (${others.length} other${others.length === 1 ? "" : "s"})` });
87
+ if (assertionId && chain.assertion) {
88
+ const a = await chain.assertion(assertionId);
89
+ const ok = a !== null && a.kind === AssertionKind.Observation && a.profileId === receipt.methodHash && a.contentHash === hash && a.unitId === BigInt(receipt.unit.unitId) && a.version === receipt.unit.version && a.windowStart === receipt.window.start && a.windowEnd === receipt.window.end;
90
+ lines.push({ rule: "assertion", pass: ok, detail: a ? `assertion ${assertionId.slice(0, 10)}…: kind ${a.kind}, profile ${a.profileId === receipt.methodHash ? "= method" : "differs"}, contentHash ${a.contentHash === hash ? "= receipt" : "differs"}, window ${a.windowStart}-${a.windowEnd}` : `assertion ${assertionId.slice(0, 10)}… not found` });
91
+ }
92
+ return { schema: CHECK_SCHEMA, receiptHash: hash, unit: receipt.unit, lines, pass: lines.every((l) => l.pass) };
93
+ }
94
+ export function receiptFromUri(uri) {
95
+ return parseDataUri(uri);
96
+ }