@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,173 @@
|
|
|
1
|
+
import { getAddress, parseEther } from "ethers";
|
|
2
|
+
import { AID } from "../core/aid.js";
|
|
3
|
+
import { agentDescriptor, kindId } from "../core/ard.js";
|
|
4
|
+
import { Enforcement, Rights, accountSubjectKey, oneClause, scopeKeyOf } from "../core/asf.js";
|
|
5
|
+
import { accountRef, keccakJcs } from "../core/canonical.js";
|
|
6
|
+
import { cellUnitDefinition } from "../core/units.js";
|
|
7
|
+
import { operatorScope } from "../genesis/operator-scope.js";
|
|
8
|
+
import { Cell } from "../kernel/cell.js";
|
|
9
|
+
import { saveCellConfig } from "./config.js";
|
|
10
|
+
import { NODE_SCOPE_GROUPS } from "./init.js";
|
|
11
|
+
import { ROLE } from "./node.js";
|
|
12
|
+
/**
|
|
13
|
+
* Move a cell to a new account, the way T9 moves A to A-prime (design memo, Phase 3: the OG instance leaves its
|
|
14
|
+
* revision-1 body for a revision-2 one and keeps its identity line). The owner deploys the new account, configures
|
|
15
|
+
* it, adopts a binding for it, moves the ERC-8004 agent through the old account (an asset: the owner's act), binds
|
|
16
|
+
* the anchor in the new account (the takeover), retires the old anchor naming its successor, publishes the new
|
|
17
|
+
* record, document and unit, and moves the skill tokens, recommitting their records. Every step is a keyed
|
|
18
|
+
* operation in the old cell's store; the new cell starts with a config and a fresh store of its own.
|
|
19
|
+
*/
|
|
20
|
+
export async function cellMigrate(node, opts) {
|
|
21
|
+
const k = node.kernel;
|
|
22
|
+
const cfg = node.config;
|
|
23
|
+
const log = opts.log ?? node.log;
|
|
24
|
+
if (!node.hasKey("owner"))
|
|
25
|
+
throw new Error("cell migrate is the owner's act: load the owner key (FOGA_OWNER_KEY or FOGA_OWNER_KEY_FILE)");
|
|
26
|
+
const old = node.body;
|
|
27
|
+
const chainId = node.chainId;
|
|
28
|
+
const owner = cfg.identity.owner;
|
|
29
|
+
const guardian = cfg.identity.guardian;
|
|
30
|
+
const operator = getAddress(opts.operator ?? cfg.identity.operator);
|
|
31
|
+
const agentId = BigInt(cfg.identity.agentId ?? (() => { throw new Error("the config names no agentId"); })());
|
|
32
|
+
const spec = cfg.identity.safety;
|
|
33
|
+
if (!spec.specId || !spec.contentHash)
|
|
34
|
+
throw new Error("the config names no specification (identity.safety.specId/contentHash)");
|
|
35
|
+
const P = `migrate.${agentId}`;
|
|
36
|
+
const operations = [];
|
|
37
|
+
const op = (key) => operations.push(key);
|
|
38
|
+
// 1. the new body
|
|
39
|
+
const requirement = (predicted) => oneClause({ specRegistry: k.asf.specRegistryAddress, specId: spec.specId, versions: [spec.version], adopter: predicted, subjectKey: accountSubjectKey(chainId, predicted), scopeKey: scopeKeyOf({ ref: accountRef(chainId, predicted) }), enforcement: Enforcement.account | Enforcement.contract, interveners: [{ account: guardian, rights: Rights.suspend | Rights.resume }] });
|
|
40
|
+
let predictedFor = null;
|
|
41
|
+
const { address } = await k.deploy(`${P}.deploy`, ROLE.owner, "foga:FogaCellAccount", [], (predicted) => {
|
|
42
|
+
predictedFor = predicted;
|
|
43
|
+
return [k.asf.bindingRegistryAddress, owner, operator, guardian, cfg.identity.recovery ?? owner, requirement(predicted)];
|
|
44
|
+
});
|
|
45
|
+
op(`${P}.deploy`);
|
|
46
|
+
if (predictedFor && predictedFor.toLowerCase() !== address.toLowerCase())
|
|
47
|
+
throw new Error(`deployed at ${address} but the requirement was built for ${predictedFor}`);
|
|
48
|
+
const next = new Cell(k, `${cfg.name}-next`, address, { owner: ROLE.owner, operator: ROLE.operator, guardian, recovery: cfg.identity.recovery });
|
|
49
|
+
log(` successor: account ${address} (operator ${operator})`);
|
|
50
|
+
// 2. funds: sweep the old body to the owner, give the new one its capital
|
|
51
|
+
if (opts.sweep ?? true) {
|
|
52
|
+
const balance = await old.balance();
|
|
53
|
+
if (balance > 0n || k.store.get(`${P}.sweep`)) {
|
|
54
|
+
await old.admin(`${P}.sweep`, "sweep", ["0x0000000000000000000000000000000000000000", await k.memo(`${P}.sweep.amount`, () => balance.toString()).then(BigInt)]);
|
|
55
|
+
op(`${P}.sweep`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const capital = opts.capitalWei ?? (k.isLocalChain ? parseEther("0.01") : parseEther("0.002"));
|
|
59
|
+
if (capital > 0n) {
|
|
60
|
+
await k.transfer(`${P}.fund`, ROLE.owner, address, capital);
|
|
61
|
+
op(`${P}.fund`);
|
|
62
|
+
}
|
|
63
|
+
// 3. configuration of the new account (as init does)
|
|
64
|
+
await next.admin(`${P}.setOperator`, "setOperator", [operator, 0, BigInt(cfg.policy.budget.dailyValueWei)]);
|
|
65
|
+
op(`${P}.setOperator`);
|
|
66
|
+
const scope = operatorScope(k, NODE_SCOPE_GROUPS);
|
|
67
|
+
await next.admin(`${P}.scope`, "setOperatorAllowedBatch", [scope.targets, scope.selectors, true]);
|
|
68
|
+
op(`${P}.scope`);
|
|
69
|
+
await next.admin(`${P}.setAIDRegistry`, "setAIDRegistry", [k.aid.address]);
|
|
70
|
+
op(`${P}.setAIDRegistry`);
|
|
71
|
+
const sel = (id, fn) => node.world.iface(id).getFunction(fn).selector;
|
|
72
|
+
const classes = [
|
|
73
|
+
["class.8414.submitFulfillment", k.tenders.address, sel("8414/TaskToken", "submitFulfillment"), "setCommitmentClass"],
|
|
74
|
+
["class.8447.join", node.world.address("8447/SwarmRegistry"), sel("8447/SwarmRegistry", "join"), "setCommitmentClass"],
|
|
75
|
+
["class.8447.applyToJoin", node.world.address("8447/SwarmRegistry"), sel("8447/SwarmRegistry", "applyToJoin"), "setCommitmentClass"],
|
|
76
|
+
["class.8454.assertStatement", node.world.address("8454/AUFAssertionRegistry"), sel("8454/AUFAssertionRegistry", "assertStatement"), "setCommitmentClass"],
|
|
77
|
+
["class.8436.castBallot", k.acdf.registryAddress, sel("8436/ACDFRegistry", "castBallot"), "setCommitmentClass"],
|
|
78
|
+
["derisk.8414.withdrawCredit", k.tenders.address, sel("8414/TaskToken", "withdrawCredit"), "setDeriskAllowed"],
|
|
79
|
+
["derisk.8414.reclaimEscrow", k.tenders.address, sel("8414/TaskToken", "reclaimEscrow"), "setDeriskAllowed"],
|
|
80
|
+
["derisk.8447.leave", node.world.address("8447/SwarmRegistry"), sel("8447/SwarmRegistry", "leave"), "setDeriskAllowed"],
|
|
81
|
+
["derisk.8471.withdrawBalance", k.adrf.escrowAddress, sel("8471/ADRFServiceEscrow", "withdrawBalance"), "setDeriskAllowed"],
|
|
82
|
+
];
|
|
83
|
+
for (const [name, target, selector, fn] of classes) {
|
|
84
|
+
await next.admin(`${P}.${name}`, fn, [target, selector, true]);
|
|
85
|
+
op(`${P}.${name}`);
|
|
86
|
+
}
|
|
87
|
+
// 4. the successor's own binding
|
|
88
|
+
const params = { profile: "foga-cell/1", cell: cfg.name, successorOf: accountRef(chainId, old.address), operatorScope: NODE_SCOPE_GROUPS };
|
|
89
|
+
const authority = { kind: "owner", owner: accountRef(chainId, owner), guardian: accountRef(chainId, guardian) };
|
|
90
|
+
const input = k.asf.cellBindingInput({ chainId, cell: address, specId: spec.specId, version: spec.version, contentHash: spec.contentHash, guardian, params, authority });
|
|
91
|
+
const { bindingId } = await next.adoptBinding(`${P}.adopt`, input, 0);
|
|
92
|
+
op(`${P}.adopt`);
|
|
93
|
+
// 5. the identity moves through the old account: the owner's act (an asset leaves the cell)
|
|
94
|
+
await old.execute(`${P}.8004.transfer`, k.identity.address, k.identity.encodeTransfer(old.address, address, agentId), { by: "owner" });
|
|
95
|
+
op(`${P}.8004.transfer`);
|
|
96
|
+
// 6. the takeover: the new anchor binds the agent in every registry; the old one retires naming its successor
|
|
97
|
+
const bindCall = k.aid.encodeBind(k.identity.address, agentId);
|
|
98
|
+
await next.execute(`${P}.aid.bind`, k.aid.address, bindCall, { by: "owner" });
|
|
99
|
+
op(`${P}.aid.bind`);
|
|
100
|
+
await next.execute(`${P}.aid.heartbeat`, k.aid.address, k.aid.encodeHeartbeat(), { by: "owner" });
|
|
101
|
+
op(`${P}.aid.heartbeat`);
|
|
102
|
+
if (k.aid.backing) {
|
|
103
|
+
await next.execute(`${P}.aid.backing.bind`, k.aid.backing.address, bindCall, { by: "owner" });
|
|
104
|
+
op(`${P}.aid.backing.bind`);
|
|
105
|
+
}
|
|
106
|
+
await old.execute(`${P}.aid.retire`, k.aid.address, k.aid.encodeRetire(address), { by: "owner" });
|
|
107
|
+
op(`${P}.aid.retire`);
|
|
108
|
+
if (k.aid.backing) {
|
|
109
|
+
await old.execute(`${P}.aid.backing.retire`, k.aid.backing.address, k.aid.encodeRetire(address), { by: "owner" });
|
|
110
|
+
op(`${P}.aid.backing.retire`);
|
|
111
|
+
}
|
|
112
|
+
// 7. the new record and document
|
|
113
|
+
const registration = {
|
|
114
|
+
type: "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
|
|
115
|
+
name: cfg.presence.name,
|
|
116
|
+
description: cfg.presence.description,
|
|
117
|
+
services: [],
|
|
118
|
+
x402Support: false,
|
|
119
|
+
active: true,
|
|
120
|
+
supportedTrust: ["reputation"],
|
|
121
|
+
"x-foga": { cell: cfg.name, anchor: accountRef(chainId, address), predecessor: accountRef(chainId, old.address), binding: bindingId },
|
|
122
|
+
};
|
|
123
|
+
const descriptor = agentDescriptor({ chainId, anchor: address, slug: `cell-${cfg.name.toLowerCase()}`, name: cfg.presence.name, description: cfg.presence.description, url: cfg.presence.homepage, capabilities: ["erc8414:fulfiller", "erc8338:holder"], registration: { registry: `eip155:${chainId}:${k.identity.address.toLowerCase()}`, agentId: agentId.toString() } });
|
|
124
|
+
const ard = await next.execute(`${P}.ard.register`, k.ard.address, k.ard.encodeRegister(kindId("agent"), address, 0n, descriptor), { by: "owner" });
|
|
125
|
+
op(`${P}.ard.register`);
|
|
126
|
+
const recordId = k.ard.recordIdFromReceipt(ard.receipt);
|
|
127
|
+
const doc = {
|
|
128
|
+
version: "aid-document/v1",
|
|
129
|
+
aid: accountRef(chainId, address),
|
|
130
|
+
did: `did:aid:${accountRef(chainId, address)}`,
|
|
131
|
+
binding: { registry: k.identity.address, agentId: Number(agentId) },
|
|
132
|
+
livenessWindow: cfg.identity.livenessWindow ?? Number(node.world.file.params.aid.defaultLivenessWindow),
|
|
133
|
+
facets: [{ facetType: "aid:core/identity/v1", provenance: "SELF", issuer: accountRef(chainId, address), validUntil: 0, digest: keccakJcs(registration), access: { mode: "PUBLIC" }, resolver: { kind: "erc8004-identity", chainId, registry: k.identity.address, agentId: Number(agentId) } }],
|
|
134
|
+
};
|
|
135
|
+
const commitment = AID.documentCommitment(doc);
|
|
136
|
+
await next.execute(`${P}.aid.document`, k.aid.address, k.aid.encodeSetDocumentURI(commitment.uri, commitment.digest), { by: "owner" });
|
|
137
|
+
op(`${P}.aid.document`);
|
|
138
|
+
// the registration file stays the one the agent carries; setAgentURI to a served URL is the documents step (3d)
|
|
139
|
+
// 8. the skills follow: each token moves through the old account (owner), each record is recommitted by the new controller
|
|
140
|
+
const skills = [];
|
|
141
|
+
for (const [name, h] of Object.entries(cfg.skills.held)) {
|
|
142
|
+
const tokenId = BigInt(h.tokenId);
|
|
143
|
+
const holder = await k.skills.ownerOf(tokenId);
|
|
144
|
+
if (getAddress(holder) === getAddress(old.address)) {
|
|
145
|
+
await old.execute(`${P}.skill.${name}.transfer`, k.skills.address, k.skills.encodeSafeTransfer(old.address, address, tokenId), { by: "owner" });
|
|
146
|
+
op(`${P}.skill.${name}.transfer`);
|
|
147
|
+
}
|
|
148
|
+
const recordId = h.recordId ?? k.ard.recordIdOf(k.skills.address, tokenId);
|
|
149
|
+
const rec = await k.ard.recordOf(recordId);
|
|
150
|
+
if (rec.kind !== "0x" + "00".repeat(32)) {
|
|
151
|
+
await next.execute(`${P}.skill.${name}.adopt`, k.ard.address, k.ard.encodeRecommit(recordId, { descriptorHash: rec.descriptorHash, descriptorURI: rec.descriptorURI, urn: rec.urn }), { by: "owner" });
|
|
152
|
+
op(`${P}.skill.${name}.adopt`);
|
|
153
|
+
}
|
|
154
|
+
skills.push(name);
|
|
155
|
+
}
|
|
156
|
+
// 9. the successor's own unit (the old unit stays the old body's)
|
|
157
|
+
const unit = await next.execute(`${P}.auf.unit`, k.units.registryAddress, k.units.encodeRegisterUnit(address, cellUnitDefinition({ chainId, cell: address, name: cfg.presence.name }), []), { by: "owner" });
|
|
158
|
+
op(`${P}.auf.unit`);
|
|
159
|
+
const unitId = k.units.unitIdFromReceipt(unit.receipt);
|
|
160
|
+
// the new config: the same cell, a new body, a fresh store
|
|
161
|
+
const nextCfg = JSON.parse(JSON.stringify(cfg));
|
|
162
|
+
nextCfg.store = opts.toStorePath;
|
|
163
|
+
nextCfg.identity = { ...cfg.identity, account: getAddress(address), operator, bindingId, recordId, unitId: unitId.toString(), documentDigest: commitment.digest };
|
|
164
|
+
nextCfg.skills.held = Object.fromEntries(Object.entries(cfg.skills.held).map(([n, h]) => [n, { tokenId: h.tokenId, recordId: h.recordId, packageHash: h.packageHash }]));
|
|
165
|
+
saveCellConfig(opts.toConfigPath, nextCfg);
|
|
166
|
+
k.setFact(`${P}.successor`, { account: address, config: opts.toConfigPath });
|
|
167
|
+
k.evidence("migrate", cfg.name, { from: old.address, to: address, agentId: agentId.toString(), operator, bindingId, recordId, unitId: unitId.toString(), skills, operations });
|
|
168
|
+
const [oldState, newState, holder] = await Promise.all([k.aid.stateName(old.address), k.aid.stateName(address), k.identity.ownerOf(agentId)]);
|
|
169
|
+
log(` moved: agent ${agentId} held by ${holder}; old anchor ${oldState}, new anchor ${newState}; config ${opts.toConfigPath}`);
|
|
170
|
+
if (oldState !== "RETIRED" || newState !== "ACTIVE" || getAddress(holder) !== getAddress(address))
|
|
171
|
+
throw new Error(`the move is incomplete: old ${oldState}, new ${newState}, agent held by ${holder}`);
|
|
172
|
+
return { from: old.address, to: address, agentId: agentId.toString(), bindingId, recordId, unitId: unitId.toString(), skills, operations, configPath: opts.toConfigPath };
|
|
173
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ModelError } from "./types.js";
|
|
2
|
+
export const ANTHROPIC_VERSION = "2023-06-01";
|
|
3
|
+
export class AnthropicAdapter {
|
|
4
|
+
model;
|
|
5
|
+
provider = "anthropic";
|
|
6
|
+
key;
|
|
7
|
+
baseUrl;
|
|
8
|
+
maxTokens;
|
|
9
|
+
timeoutMs;
|
|
10
|
+
retries;
|
|
11
|
+
fetchImpl;
|
|
12
|
+
constructor(model, opts = {}) {
|
|
13
|
+
this.model = model;
|
|
14
|
+
if (!model)
|
|
15
|
+
throw new ModelError("runtime.model.model names no model; set it in the cell configuration (provider anthropic)");
|
|
16
|
+
const key = opts.apiKey ?? process.env.ANTHROPIC_API_KEY;
|
|
17
|
+
if (!key)
|
|
18
|
+
throw new ModelError("ANTHROPIC_API_KEY is not set; the anthropic provider needs it in the node's environment");
|
|
19
|
+
this.key = key;
|
|
20
|
+
this.baseUrl = (opts.baseUrl ?? process.env.ANTHROPIC_BASE_URL ?? "https://api.anthropic.com").replace(/\/+$/, "");
|
|
21
|
+
this.maxTokens = opts.maxTokens ?? 2048;
|
|
22
|
+
this.timeoutMs = opts.timeoutMs ?? 120_000;
|
|
23
|
+
this.retries = opts.retries ?? 2;
|
|
24
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
25
|
+
}
|
|
26
|
+
async propose(req) {
|
|
27
|
+
const body = {
|
|
28
|
+
model: this.model,
|
|
29
|
+
max_tokens: this.maxTokens,
|
|
30
|
+
system: req.system,
|
|
31
|
+
tools: req.tools.map((t) => ({ name: t.name, description: t.description, input_schema: inputSchema(t.inputSchema) })),
|
|
32
|
+
messages: req.messages.map((m) => ({ role: m.role, content: m.content.map(toWire) })),
|
|
33
|
+
};
|
|
34
|
+
let attempt = 0;
|
|
35
|
+
for (;;) {
|
|
36
|
+
let res;
|
|
37
|
+
try {
|
|
38
|
+
res = await this.fetchImpl(`${this.baseUrl}/v1/messages`, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "content-type": "application/json", "x-api-key": this.key, "anthropic-version": ANTHROPIC_VERSION },
|
|
41
|
+
body: JSON.stringify(body),
|
|
42
|
+
signal: AbortSignal.timeout(this.timeoutMs),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
if (attempt++ < this.retries) {
|
|
47
|
+
await pause(attempt);
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
throw new ModelError(`the Messages API could not be reached: ${e.message}`);
|
|
51
|
+
}
|
|
52
|
+
if (res.status === 429 || res.status >= 500) {
|
|
53
|
+
if (attempt++ < this.retries) {
|
|
54
|
+
const after = Number(res.headers.get("retry-after"));
|
|
55
|
+
await pause(attempt, Number.isFinite(after) && after > 0 ? after * 1000 : undefined);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
throw new ModelError(`the Messages API answered ${res.status} after ${attempt} attempts`, res.status);
|
|
59
|
+
}
|
|
60
|
+
if (!res.ok) {
|
|
61
|
+
let detail = "";
|
|
62
|
+
try {
|
|
63
|
+
const err = (await res.json());
|
|
64
|
+
detail = err.error ? `${err.error.type ?? "error"}: ${err.error.message ?? ""}` : "";
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
detail = "";
|
|
68
|
+
}
|
|
69
|
+
throw new ModelError(`the Messages API answered ${res.status}${detail ? ` (${detail})` : ""}`, res.status);
|
|
70
|
+
}
|
|
71
|
+
const data = (await res.json());
|
|
72
|
+
if (!Array.isArray(data.content))
|
|
73
|
+
throw new ModelError("the Messages API answered without a content array");
|
|
74
|
+
const content = [];
|
|
75
|
+
for (const raw of data.content) {
|
|
76
|
+
if (raw.type === "text" && typeof raw.text === "string")
|
|
77
|
+
content.push({ type: "text", text: raw.text });
|
|
78
|
+
else if (raw.type === "tool_use" && typeof raw.id === "string" && typeof raw.name === "string")
|
|
79
|
+
content.push({ type: "tool_use", id: raw.id, name: raw.name, input: (raw.input ?? {}) });
|
|
80
|
+
}
|
|
81
|
+
return { content, stopReason: data.stop_reason ?? "end_turn", usage: { inputTokens: data.usage?.input_tokens ?? 0, outputTokens: data.usage?.output_tokens ?? 0 } };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
/** The API wants a plain object schema; the catalog's `$schema` marker is dropped. */
|
|
86
|
+
function inputSchema(schema) {
|
|
87
|
+
const { $schema: _s, ...rest } = schema;
|
|
88
|
+
return rest.type ? rest : { type: "object", ...rest };
|
|
89
|
+
}
|
|
90
|
+
function toWire(b) {
|
|
91
|
+
if (b.type === "tool_result")
|
|
92
|
+
return { type: "tool_result", tool_use_id: b.tool_use_id, content: b.content, ...(b.is_error ? { is_error: true } : {}) };
|
|
93
|
+
return b;
|
|
94
|
+
}
|
|
95
|
+
function pause(attempt, ms) {
|
|
96
|
+
return new Promise((r) => setTimeout(r, ms ?? 500 * 2 ** (attempt - 1)));
|
|
97
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The deterministic stand-in for a model (design memo, Phase 3, section 8): a fixed decision table over the
|
|
3
|
+
* situation and the transcript of the tick so far, so the same situation always yields the same proposals and a
|
|
4
|
+
* CI run needs no key and no network. It does what a careful operator's model would — inspect a tender, pre-check
|
|
5
|
+
* it, run the skill, submit, settle, withdraw — and one thing a careful model would not: once per cell it proposes a
|
|
6
|
+
* tender whose reward is five times the per-operation cap, so the evidence shows the budget gate closing on a
|
|
7
|
+
* proposal (case g). It never looks for a way around a refusal: a refused pre-check ends its interest in that
|
|
8
|
+
* tender until the situation changes (a safety refusal is retried once the binding is Satisfied again).
|
|
9
|
+
*
|
|
10
|
+
* The stub is a pure function of the request: it keeps no state between calls. Everything it needs to know about
|
|
11
|
+
* what happened earlier in the tick is in the tool_use and tool_result blocks the runner put in the transcript.
|
|
12
|
+
*/
|
|
13
|
+
export const STUB_MODEL = "foga-stub/1";
|
|
14
|
+
function firstUserText(req) {
|
|
15
|
+
for (const m of req.messages)
|
|
16
|
+
if (m.role === "user")
|
|
17
|
+
for (const b of m.content)
|
|
18
|
+
if (b.type === "text")
|
|
19
|
+
return b.text;
|
|
20
|
+
throw new Error("the stub needs the situation as the first user message");
|
|
21
|
+
}
|
|
22
|
+
/** The tool calls of this tick so far, each with its result (parsed when it is JSON). */
|
|
23
|
+
function transcript(req) {
|
|
24
|
+
const uses = new Map();
|
|
25
|
+
const out = [];
|
|
26
|
+
for (const m of req.messages) {
|
|
27
|
+
for (const b of m.content) {
|
|
28
|
+
if (b.type === "tool_use")
|
|
29
|
+
uses.set(b.id, { name: b.name, input: b.input });
|
|
30
|
+
if (b.type === "tool_result") {
|
|
31
|
+
const use = uses.get(b.tool_use_id);
|
|
32
|
+
if (!use)
|
|
33
|
+
continue;
|
|
34
|
+
let result = b.content;
|
|
35
|
+
try {
|
|
36
|
+
result = JSON.parse(b.content);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
/* text */
|
|
40
|
+
}
|
|
41
|
+
out.push({ name: use.name, input: use.input, result, isError: Boolean(b.is_error) });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
const tokenOf = (d) => (typeof d.input.tokenId === "string" ? d.input.tokenId : undefined);
|
|
48
|
+
/** Which tender to work on: the lowest id that still asks for something. */
|
|
49
|
+
export function chooseTender(s) {
|
|
50
|
+
const satisfied = s.cell.safety.verdict === "Satisfied";
|
|
51
|
+
const actionable = s.tenders.filter((t) => {
|
|
52
|
+
if (t.status === "open" || t.status === "eligible")
|
|
53
|
+
return true;
|
|
54
|
+
if (t.status === "submitted")
|
|
55
|
+
return t.submission?.status === 0;
|
|
56
|
+
if (t.status === "settled")
|
|
57
|
+
return BigInt(t.creditWei) > 0n;
|
|
58
|
+
if (t.status === "refused")
|
|
59
|
+
return t.gate === "safety" && satisfied;
|
|
60
|
+
return false;
|
|
61
|
+
});
|
|
62
|
+
actionable.sort((a, b) => (BigInt(a.tokenId) < BigInt(b.tokenId) ? -1 : 1));
|
|
63
|
+
return actionable[0] ?? null;
|
|
64
|
+
}
|
|
65
|
+
export class StubAdapter {
|
|
66
|
+
provider = "stub";
|
|
67
|
+
model = STUB_MODEL;
|
|
68
|
+
async propose(req) {
|
|
69
|
+
const s = JSON.parse(firstUserText(req));
|
|
70
|
+
const done = transcript(req);
|
|
71
|
+
let n = done.length;
|
|
72
|
+
const call = (name, input) => ({ type: "tool_use", id: `stub-${++n}`, name, input });
|
|
73
|
+
const turn = (text, ...uses) => ({ content: [{ type: "text", text }, ...uses], stopReason: uses.length ? "tool_use" : "end_turn", usage: { inputTokens: 0, outputTokens: 0 } });
|
|
74
|
+
const has = (name) => req.tools.some((t) => t.name === name);
|
|
75
|
+
// g. once per cell: a tender above the per-operation cap, on purpose — the budget gate must refuse it
|
|
76
|
+
const triedBefore = s.recentDecisions.some((d) => d.capability === "tender_mint" && d.caller === "runner") || done.some((d) => d.name === "tender_mint");
|
|
77
|
+
if (!triedBefore && has("tender_mint") && s.skills.held.length) {
|
|
78
|
+
const probe = s.skills.held.find((h) => h.name === "foga-probe") ?? s.skills.held[0];
|
|
79
|
+
const reward = (BigInt(s.budget.perOperationValueWei) * 5n).toString();
|
|
80
|
+
return turn(`Proposing a tender for ${probe.name} with a reward of ${reward} wei, five times the per-operation cap of ${s.budget.perOperationValueWei} wei. The budget gate should refuse this; the refusal is the point.`, call("tender_mint", { skill: probe.name, packageHash: probe.packageHash, input: { purpose: "stub case g" }, answerHash: "0x" + "11".repeat(32), rewardWei: reward, submitIn: 86400, settleIn: 7 * 86400 }));
|
|
81
|
+
}
|
|
82
|
+
const t = chooseTender(s);
|
|
83
|
+
if (!t)
|
|
84
|
+
return turn("Nothing to do: no open tender asks for a decision and nothing of mine is pending.");
|
|
85
|
+
const mine = done.filter((d) => tokenOf(d) === t.tokenId);
|
|
86
|
+
const last = mine.at(-1);
|
|
87
|
+
if (last?.isError)
|
|
88
|
+
return turn(`${last.name} on tender ${t.tokenId} answered an error; stopping here for this tick.`);
|
|
89
|
+
const by = (name) => mine.find((d) => d.name === name);
|
|
90
|
+
// a pending submission of mine: settle, then withdraw what was credited
|
|
91
|
+
if (t.status === "submitted" && t.submission?.status === 0) {
|
|
92
|
+
const settled = by("tender_settle");
|
|
93
|
+
if (!settled) {
|
|
94
|
+
if (!t.submission.answer)
|
|
95
|
+
return turn(`Tender ${t.tokenId}: my submission is pending but no run of mine holds its answer; leaving it to the operator.`);
|
|
96
|
+
return turn(`Tender ${t.tokenId}: settling my pending submission ${t.submission.submissionId}.`, call("tender_settle", { tokenId: t.tokenId, submissionId: t.submission.submissionId, answer: t.submission.answer }));
|
|
97
|
+
}
|
|
98
|
+
const out = settled.result.output;
|
|
99
|
+
if (out?.creditWei && BigInt(out.creditWei) > 0n && !by("credit_withdraw"))
|
|
100
|
+
return turn(`Tender ${t.tokenId}: ${out.creditWei} wei were credited rather than paid; withdrawing.`, call("credit_withdraw", { tokenId: t.tokenId }));
|
|
101
|
+
return turn(`Tender ${t.tokenId}: settled.`);
|
|
102
|
+
}
|
|
103
|
+
if (t.status === "settled") {
|
|
104
|
+
if (!by("credit_withdraw"))
|
|
105
|
+
return turn(`Tender ${t.tokenId}: withdrawing the credited ${t.creditWei} wei.`, call("credit_withdraw", { tokenId: t.tokenId }));
|
|
106
|
+
return turn(`Tender ${t.tokenId}: credit withdrawn.`);
|
|
107
|
+
}
|
|
108
|
+
// a tender to judge: inspect, pre-check, run, submit — in that order, one step per turn
|
|
109
|
+
if (!by("tender_inspect"))
|
|
110
|
+
return turn(`Looking at tender ${t.tokenId} (${t.task?.skill ?? "no task document"}, reward ${t.rewardWei} wei).`, call("tender_inspect", { tokenId: t.tokenId }));
|
|
111
|
+
const pre = by("tender_precheck");
|
|
112
|
+
if (!pre)
|
|
113
|
+
return turn(`Pre-checking tender ${t.tokenId} against the cell's gates.`, call("tender_precheck", { tokenId: t.tokenId }));
|
|
114
|
+
const verdict = pre.result;
|
|
115
|
+
if (!verdict.eligible)
|
|
116
|
+
return turn(`Tender ${t.tokenId} is not for this cell: refused at ${verdict.gate ?? "?"} (${verdict.reason ?? "no reason given"}). Not looking for a way around it.`);
|
|
117
|
+
if (!t.task)
|
|
118
|
+
return turn(`Tender ${t.tokenId} passed the pre-check but has no readable task document; stopping.`);
|
|
119
|
+
const run = by("skill_run");
|
|
120
|
+
if (!run)
|
|
121
|
+
return turn(`Running ${t.task.skill} for tender ${t.tokenId}.`, call("skill_run", { skill: t.task.skill, input: t.task.input, tokenId: t.tokenId }));
|
|
122
|
+
const runOut = run.result.output;
|
|
123
|
+
if (!runOut?.resultHash)
|
|
124
|
+
return turn(`The run for tender ${t.tokenId} returned no result hash; stopping.`);
|
|
125
|
+
if (!by("tender_submit"))
|
|
126
|
+
return turn(`Submitting the result of tender ${t.tokenId}.`, call("tender_submit", { tokenId: t.tokenId, resultHash: runOut.resultHash, runReceipt: runOut.runReceipt }));
|
|
127
|
+
return turn(`Tender ${t.tokenId}: submitted; the settlement is the next tick's (or the watchers').`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** A failure of the model layer (transport, quota, a malformed answer); never carries a credential. */
|
|
2
|
+
export class ModelError extends Error {
|
|
3
|
+
status;
|
|
4
|
+
constructor(message, status) {
|
|
5
|
+
super(message);
|
|
6
|
+
this.status = status;
|
|
7
|
+
this.name = "ModelError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAddress, Wallet } from "ethers";
|
|
4
|
+
import { buildSkillPackage, readSkillDir } from "../core/pack/skillpack.js";
|
|
5
|
+
import { Cell } from "../kernel/cell.js";
|
|
6
|
+
import { Kernel } from "../kernel/kernel.js";
|
|
7
|
+
import { configPath, loadCellConfig, saveCellConfig } from "./config.js";
|
|
8
|
+
/** Signer names the node uses in the kernel. */
|
|
9
|
+
export const ROLE = { operator: "operator", owner: "owner", guardian: "guardian" };
|
|
10
|
+
/**
|
|
11
|
+
* The node: one cell's configuration, the kernel opened on the cell's world and store, the cell handle, and what
|
|
12
|
+
* the capability catalog needs to decide and act (design memo, Phase 3, section 2). It knows which keys this
|
|
13
|
+
* process holds and refuses an act whose signer is not here rather than looking for a key anywhere else.
|
|
14
|
+
*/
|
|
15
|
+
export class Node {
|
|
16
|
+
loaded;
|
|
17
|
+
kernel;
|
|
18
|
+
cell;
|
|
19
|
+
held;
|
|
20
|
+
rpc;
|
|
21
|
+
log;
|
|
22
|
+
constructor(loaded, kernel, cell, held, rpc, log) {
|
|
23
|
+
this.loaded = loaded;
|
|
24
|
+
this.kernel = kernel;
|
|
25
|
+
this.cell = cell;
|
|
26
|
+
this.held = held;
|
|
27
|
+
this.rpc = rpc;
|
|
28
|
+
this.log = log;
|
|
29
|
+
}
|
|
30
|
+
static async open(opts) {
|
|
31
|
+
const loaded = loadCellConfig(opts.configPath);
|
|
32
|
+
const cfg = loaded.config;
|
|
33
|
+
const keys = {};
|
|
34
|
+
for (const role of ["operator", "owner", "guardian"]) {
|
|
35
|
+
const pk = opts.keys[role];
|
|
36
|
+
if (!pk)
|
|
37
|
+
continue;
|
|
38
|
+
const address = new Wallet(pk).address;
|
|
39
|
+
const expected = cfg.identity[role];
|
|
40
|
+
if (getAddress(address) !== getAddress(expected))
|
|
41
|
+
throw new Error(`the ${role} key loaded here is ${address}; the config names ${expected} as ${role}. The key and the config disagree; nothing was done.`);
|
|
42
|
+
keys[ROLE[role]] = pk;
|
|
43
|
+
}
|
|
44
|
+
const worldPath = configPath(loaded, cfg.world);
|
|
45
|
+
const storePath = configPath(loaded, cfg.store);
|
|
46
|
+
const worldNetwork = JSON.parse(readFileSync(worldPath, "utf8")).network;
|
|
47
|
+
const rpc = opts.rpc ?? cfg.rpc ?? process.env.FOGA_RPC_URL ?? (worldNetwork === "anvil" ? "http://127.0.0.1:8545" : undefined);
|
|
48
|
+
if (!rpc)
|
|
49
|
+
throw new Error(`no RPC for cell ${cfg.name}: set "rpc" in the config, pass --rpc or set FOGA_RPC_URL`);
|
|
50
|
+
const kernel = await Kernel.open({ worldPath, rpc, bundlePath: cfg.bundle ? configPath(loaded, cfg.bundle) : undefined, storePath, keys, log: opts.log });
|
|
51
|
+
const log = opts.log ?? (() => { });
|
|
52
|
+
const known = kernel.store.getMeta("cellAccount");
|
|
53
|
+
if (known && cfg.identity.account && known.toLowerCase() !== cfg.identity.account.toLowerCase()) {
|
|
54
|
+
kernel.close();
|
|
55
|
+
throw new Error(`operation store ${storePath} belongs to cell account ${known}, not to ${cfg.identity.account} (${opts.configPath}). Point the config at its own store or start a fresh one.`);
|
|
56
|
+
}
|
|
57
|
+
let cell = null;
|
|
58
|
+
if (cfg.identity.account) {
|
|
59
|
+
cell = new Cell(kernel, cfg.name, cfg.identity.account, { owner: ROLE.owner, operator: ROLE.operator, guardian: cfg.identity.guardian, recovery: cfg.identity.recovery });
|
|
60
|
+
// the account must be there, be revision 2 or later, and name the roles the config names
|
|
61
|
+
const code = await kernel.world.provider.getCode(cfg.identity.account);
|
|
62
|
+
if (code === "0x") {
|
|
63
|
+
kernel.close();
|
|
64
|
+
throw new Error(`no contract at the cell account ${cfg.identity.account} on this chain (${opts.configPath}); is the config for this world?`);
|
|
65
|
+
}
|
|
66
|
+
let version = 1;
|
|
67
|
+
try {
|
|
68
|
+
version = Number(await cell.contract().ACCOUNT_VERSION());
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
version = 1;
|
|
72
|
+
}
|
|
73
|
+
// a revision-1 account has no operator allow-list: nothing holding the operator key may run on it. Reads, builds
|
|
74
|
+
// and the owner's own commands (the migration away from it, above all) are not what the refusal is about.
|
|
75
|
+
if (version < 2 && opts.keys.operator) {
|
|
76
|
+
kernel.close();
|
|
77
|
+
throw new Error(`the cell account ${cfg.identity.account} is a revision-${version} FogaCellAccount: it has no operator allow-list, so a node must not act on it. Move the cell to a revision-2 account first (foga cell migrate); reads and builds work without the operator key.`);
|
|
78
|
+
}
|
|
79
|
+
const [onchainOperator, onchainOwner] = await Promise.all([cell.operator(), cell.owner()]);
|
|
80
|
+
if (getAddress(onchainOperator) !== getAddress(cfg.identity.operator)) {
|
|
81
|
+
kernel.close();
|
|
82
|
+
throw new Error(`the account ${cfg.identity.account} names ${onchainOperator} as operator; the config names ${cfg.identity.operator}. Update the config (or rotate the operator) before running.`);
|
|
83
|
+
}
|
|
84
|
+
if (getAddress(onchainOwner) !== getAddress(cfg.identity.owner)) {
|
|
85
|
+
kernel.close();
|
|
86
|
+
throw new Error(`the account ${cfg.identity.account} is owned by ${onchainOwner}; the config names ${cfg.identity.owner} as owner.`);
|
|
87
|
+
}
|
|
88
|
+
if (!known)
|
|
89
|
+
kernel.store.setMeta("cellAccount", cfg.identity.account);
|
|
90
|
+
}
|
|
91
|
+
else if (!opts.allowUnborn) {
|
|
92
|
+
kernel.close();
|
|
93
|
+
throw new Error(`${opts.configPath} names no account: the cell has not been born. Run foga cell init first.`);
|
|
94
|
+
}
|
|
95
|
+
return new Node(loaded, kernel, cell, opts.keys, rpc, log);
|
|
96
|
+
}
|
|
97
|
+
close() {
|
|
98
|
+
this.kernel.close();
|
|
99
|
+
this.kernel.world.provider.destroy();
|
|
100
|
+
}
|
|
101
|
+
decisions = Promise.resolve();
|
|
102
|
+
/** Run `fn` after every earlier decision section of this node has finished: the catalog judges one act at a time, so a budget cap is read with what the act before it reserved. */
|
|
103
|
+
serialize(fn) {
|
|
104
|
+
const turn = this.decisions.catch(() => undefined).then(fn);
|
|
105
|
+
this.decisions = turn;
|
|
106
|
+
return turn;
|
|
107
|
+
}
|
|
108
|
+
get config() {
|
|
109
|
+
return this.loaded.config;
|
|
110
|
+
}
|
|
111
|
+
get store() {
|
|
112
|
+
return this.kernel.store;
|
|
113
|
+
}
|
|
114
|
+
get world() {
|
|
115
|
+
return this.kernel.world;
|
|
116
|
+
}
|
|
117
|
+
get chainId() {
|
|
118
|
+
return this.kernel.chainId;
|
|
119
|
+
}
|
|
120
|
+
/** The cell handle; an error when the cell has not been born. */
|
|
121
|
+
get body() {
|
|
122
|
+
if (!this.cell)
|
|
123
|
+
throw new Error(`cell ${this.config.name} has not been born (foga cell init)`);
|
|
124
|
+
return this.cell;
|
|
125
|
+
}
|
|
126
|
+
hasKey(role) {
|
|
127
|
+
return Boolean(this.held[role]);
|
|
128
|
+
}
|
|
129
|
+
/** The signer name of a role for the kernel, checked to be loaded. */
|
|
130
|
+
signerOf(role) {
|
|
131
|
+
if (!this.hasKey(role))
|
|
132
|
+
throw new Error(`this process holds no ${role} key for cell ${this.config.name}`);
|
|
133
|
+
return ROLE[role];
|
|
134
|
+
}
|
|
135
|
+
saveConfig() {
|
|
136
|
+
saveCellConfig(this.loaded.path, this.loaded.config);
|
|
137
|
+
}
|
|
138
|
+
/** Paths the probe worker needs. */
|
|
139
|
+
get worldPath() {
|
|
140
|
+
return configPath(this.loaded, this.config.world);
|
|
141
|
+
}
|
|
142
|
+
get bundlePath() {
|
|
143
|
+
return this.config.bundle ? configPath(this.loaded, this.config.bundle) : undefined;
|
|
144
|
+
}
|
|
145
|
+
/** FogaCellAccount revision of the cell's account (1 for accounts deployed before 0.6.0). */
|
|
146
|
+
async accountVersion() {
|
|
147
|
+
try {
|
|
148
|
+
return Number(await this.body.contract().ACCOUNT_VERSION());
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return 1;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
get skillsDir() {
|
|
155
|
+
return configPath(this.loaded, this.config.skills.dir);
|
|
156
|
+
}
|
|
157
|
+
get inputDir() {
|
|
158
|
+
return configPath(this.loaded, this.config.skills.inputDir);
|
|
159
|
+
}
|
|
160
|
+
get storePath() {
|
|
161
|
+
return configPath(this.loaded, this.config.store);
|
|
162
|
+
}
|
|
163
|
+
get acceptance() {
|
|
164
|
+
return { hashlock: this.kernel.tenders.hashlockAddress };
|
|
165
|
+
}
|
|
166
|
+
/** The skills the config says the cell holds, each rebuilt from its directory and checked against the chain's commitment. */
|
|
167
|
+
async heldSkills() {
|
|
168
|
+
const out = [];
|
|
169
|
+
for (const [name, h] of Object.entries(this.config.skills.held)) {
|
|
170
|
+
const pkg = buildSkillPackage(readSkillDir(join(this.skillsDir, name)));
|
|
171
|
+
const tokenId = BigInt(h.tokenId);
|
|
172
|
+
const onchain = await this.kernel.skills.skillOf(tokenId);
|
|
173
|
+
if (onchain.packageHash.toLowerCase() !== pkg.packageHash.toLowerCase())
|
|
174
|
+
throw new Error(`skill ${name}: the local package ${pkg.packageHash} differs from token ${tokenId}'s commitment ${onchain.packageHash}; the directory was changed since the mint`);
|
|
175
|
+
out.push({ name, tokenId, packageHash: pkg.packageHash, mdHash: pkg.mdHash, pkg });
|
|
176
|
+
}
|
|
177
|
+
return out;
|
|
178
|
+
}
|
|
179
|
+
/** What the cell spent and reserved in the last 24 hours, and how many commitments are open. */
|
|
180
|
+
budgetUse() {
|
|
181
|
+
const since = new Date(Date.now() - 24 * 3600 * 1000).toISOString();
|
|
182
|
+
const books = this.store.budgetBooks(ROLE.operator, since);
|
|
183
|
+
return { ...books, runningCommitments: this.store.countJobs("tender", ["accepted", "running"]) };
|
|
184
|
+
}
|
|
185
|
+
/** eth_estimateGas of a call: the decoded revert reason, or null when it would succeed. */
|
|
186
|
+
async wouldRevert(from, to, data, value = 0n) {
|
|
187
|
+
try {
|
|
188
|
+
await this.world.provider.estimateGas({ from, to, data, value });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
return this.kernel.tx.decoder.explain(e);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async now() {
|
|
196
|
+
return this.kernel.blockTimestamp();
|
|
197
|
+
}
|
|
198
|
+
}
|