@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,95 @@
|
|
|
1
|
+
export const ok = (gate, reason) => ({ gate, ok: true, reason });
|
|
2
|
+
export const refused = (gate, reason) => ({ gate, ok: false, reason });
|
|
3
|
+
/** The first refusal, or null when every gate passed. */
|
|
4
|
+
export function firstRefusal(gates) {
|
|
5
|
+
return gates.find((g) => !g.ok) ?? null;
|
|
6
|
+
}
|
|
7
|
+
/** The policy block of a cell applied to one tender: reward, deadlines, state, verifier, counterparty. */
|
|
8
|
+
export function tenderGates(cfg, t, clock, dir) {
|
|
9
|
+
const p = cfg.policy.tenders;
|
|
10
|
+
const gates = [];
|
|
11
|
+
const minReward = BigInt(p.minRewardWei);
|
|
12
|
+
// reward and state
|
|
13
|
+
if (t.asset !== "0x0000000000000000000000000000000000000000")
|
|
14
|
+
gates.push(refused("policy", `the reward is paid in ${t.asset}; the cell takes native-asset tenders only`));
|
|
15
|
+
else if (t.rewardWei < minReward)
|
|
16
|
+
gates.push(refused("policy", `reward ${t.rewardWei} wei is below the policy floor ${minReward} wei`));
|
|
17
|
+
else
|
|
18
|
+
gates.push(ok("policy", `reward ${t.rewardWei} wei meets the floor ${minReward} wei`));
|
|
19
|
+
if (t.cancelled)
|
|
20
|
+
gates.push(refused("policy", "the tender is cancelled"));
|
|
21
|
+
if (t.maxCompletions !== 0n && t.completions >= t.maxCompletions)
|
|
22
|
+
gates.push(refused("policy", `the tender has its ${t.maxCompletions} completion(s) already`));
|
|
23
|
+
if (t.escrowWei < t.rewardWei)
|
|
24
|
+
gates.push(refused("policy", `escrow ${t.escrowWei} wei does not cover the reward ${t.rewardWei} wei`));
|
|
25
|
+
// deadlines: enough time left to submit and to settle
|
|
26
|
+
const submitLeft = t.submitBy === 0n ? null : Number(t.submitBy) - clock.now;
|
|
27
|
+
const settleLeft = t.settleBy === 0n ? null : Number(t.settleBy) - clock.now;
|
|
28
|
+
const deadlines = [];
|
|
29
|
+
if (submitLeft !== null && submitLeft < p.minSubmitWindowSeconds)
|
|
30
|
+
deadlines.push(refused("policy", submitLeft <= 0 ? "past submitBy" : `only ${submitLeft} s left to submit; the policy wants ${p.minSubmitWindowSeconds} s`));
|
|
31
|
+
if (settleLeft !== null && settleLeft < p.minSettleWindowSeconds)
|
|
32
|
+
deadlines.push(refused("policy", settleLeft <= 0 ? "past settleBy" : `only ${settleLeft} s left to settle; the policy wants ${p.minSettleWindowSeconds} s`));
|
|
33
|
+
gates.push(...(deadlines.length ? deadlines : [ok("policy", `deadlines open (submitBy ${submitLeft === null ? "none" : `in ${submitLeft} s`}, settleBy ${settleLeft === null ? "none" : `in ${settleLeft} s`})`)]));
|
|
34
|
+
// the acceptance authority the cell works under
|
|
35
|
+
const resolveName = (a) => (a === "hashlock" ? dir.hashlock : a);
|
|
36
|
+
const match = p.acceptance.find((a) => resolveName(a).toLowerCase() === t.acceptanceAuthority.toLowerCase());
|
|
37
|
+
if (!match)
|
|
38
|
+
gates.push(refused("verifier", `acceptance authority ${t.acceptanceAuthority} is not one the cell works under (${p.acceptance.join(", ")})`));
|
|
39
|
+
else if (match === "hashlock" && t.acceptanceProfile && t.acceptanceProfile !== "x-hashlock-v1")
|
|
40
|
+
gates.push(refused("verifier", `the task document names acceptance profile ${t.acceptanceProfile}; the hashlock verifier settles x-hashlock-v1 only`));
|
|
41
|
+
else
|
|
42
|
+
gates.push(ok("verifier", `acceptance authority ${t.acceptanceAuthority} (${match})`));
|
|
43
|
+
// the counterparty's standing, when the policy asks for it
|
|
44
|
+
if (p.counterpartyMinLevel > 0) {
|
|
45
|
+
if (t.counterpartyLevel === null)
|
|
46
|
+
gates.push(refused("policy", `the policy requires the demander at KYA level ${p.counterpartyMinLevel} and no level could be read`));
|
|
47
|
+
else if (t.counterpartyLevel < p.counterpartyMinLevel)
|
|
48
|
+
gates.push(refused("policy", `the demander's KYA level ${t.counterpartyLevel} is below the required ${p.counterpartyMinLevel}`));
|
|
49
|
+
else
|
|
50
|
+
gates.push(ok("policy", `demander at KYA level ${t.counterpartyLevel} (required ${p.counterpartyMinLevel})`));
|
|
51
|
+
}
|
|
52
|
+
return gates;
|
|
53
|
+
}
|
|
54
|
+
/** Whether the cell holds and offers the skill a tender asks for, with the package the chain commits to. */
|
|
55
|
+
export function skillGate(cfg, t, held) {
|
|
56
|
+
if (!t.skill)
|
|
57
|
+
return refused("skill", "the task document names no skill (foga.task/1 expected)");
|
|
58
|
+
const have = held.find((h) => h.name === t.skill);
|
|
59
|
+
if (!have)
|
|
60
|
+
return refused("skill", `the cell holds no skill named ${t.skill} (held: ${held.map((h) => h.name).join(", ") || "none"})`);
|
|
61
|
+
if (!cfg.skills.offered.includes(t.skill))
|
|
62
|
+
return refused("skill", `the cell holds ${t.skill} but does not offer it (policy: offered = ${cfg.skills.offered.join(", ") || "none"})`);
|
|
63
|
+
if (t.packageHash && t.packageHash.toLowerCase() !== have.packageHash.toLowerCase())
|
|
64
|
+
return refused("skill", `the task wants package ${t.packageHash.slice(0, 10)}… of ${t.skill}; the cell's token commits to ${have.packageHash.slice(0, 10)}…`);
|
|
65
|
+
return ok("skill", `${t.skill} held and offered; package ${have.packageHash.slice(0, 10)}… matches the token's commitment`);
|
|
66
|
+
}
|
|
67
|
+
/** Liveness and safety: the anchor must be ACTIVE and the binding Satisfied for a commitment; de-risk actions pass under a derisk-only suspension. */
|
|
68
|
+
export function safetyGate(aidState, status, kind) {
|
|
69
|
+
if (kind === "commitment" && aidState !== "ACTIVE")
|
|
70
|
+
return refused("safety", `anchor ${aidState}: no commitment-class call while not ACTIVE`);
|
|
71
|
+
if (status.verdictName === "Satisfied")
|
|
72
|
+
return ok("safety", `anchor ${aidState}, binding Satisfied`);
|
|
73
|
+
const stopped = status.reasonNames.filter((r) => r === "Suspended" || r === "StoppedByIntervener" || r === "Terminated");
|
|
74
|
+
if (kind === "derisk" && status.verdictName === "NotSatisfied" && stopped.length && !status.restrictions.includes(16))
|
|
75
|
+
return ok("safety", `binding ${status.verdictName} [${status.reasonNames.join(",")}]: a de-risk action, allowed under the suspension`);
|
|
76
|
+
return refused("safety", `binding ${status.verdictName} [${status.reasonNames.join(",")}]${status.restrictions.length ? `, restrictions ${status.restrictions.join(",")}` : ""}`);
|
|
77
|
+
}
|
|
78
|
+
/** The four books of the budget (value, gas, model, resources; the first two here) against a request. */
|
|
79
|
+
export function budgetGate(cfg, use, req) {
|
|
80
|
+
const b = cfg.policy.budget;
|
|
81
|
+
const perOp = BigInt(b.perOperationValueWei);
|
|
82
|
+
const dailyValue = BigInt(b.dailyValueWei);
|
|
83
|
+
const dailyGas = BigInt(b.dailyGasWei);
|
|
84
|
+
if (req.valueWei > perOp)
|
|
85
|
+
return refused("budget", `the operation would move ${req.valueWei} wei; the per-operation cap is ${perOp} wei`);
|
|
86
|
+
const valueAfter = use.valueWei + use.reservedValueWei + req.valueWei;
|
|
87
|
+
if (valueAfter > dailyValue)
|
|
88
|
+
return refused("budget", `value moved and reserved in the last 24 h would reach ${valueAfter} wei; the daily cap is ${dailyValue} wei`);
|
|
89
|
+
const gasAfter = use.gasWei + use.reservedFeeWei + req.feeReserveWei;
|
|
90
|
+
if (gasAfter > dailyGas)
|
|
91
|
+
return refused("budget", `fees paid and reserved in the last 24 h would reach ${gasAfter} wei; the daily cap is ${dailyGas} wei`);
|
|
92
|
+
if (req.commitment && use.runningCommitments >= b.maxConcurrentCommitments)
|
|
93
|
+
return refused("budget", `${use.runningCommitments} commitment(s) open; the policy allows ${b.maxConcurrentCommitments} at a time`);
|
|
94
|
+
return ok("budget", `value ${valueAfter}/${dailyValue} wei, fees ${gasAfter}/${dailyGas} wei in the window${req.commitment ? `, commitments ${use.runningCommitments + 1}/${b.maxConcurrentCommitments}` : ""}`);
|
|
95
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { jcs, sha256Hex } from "../core/canonical.js";
|
|
4
|
+
import { chooseTender } from "./model/stub.js";
|
|
5
|
+
import { ModelError } from "./model/types.js";
|
|
6
|
+
/** The model's proposals arrive through this caller: act level, the operator's view, entrance `runner`. */
|
|
7
|
+
export const RUNNER_CALLER = { id: "runner", entrance: "runner", level: "act", visibility: "operator" };
|
|
8
|
+
/** The key under which the runner counts the model calls of one UTC day. */
|
|
9
|
+
export const modelCallsKey = (day = new Date().toISOString().slice(0, 10)) => `runner.model.calls.${day}`;
|
|
10
|
+
const JOURNAL_SEQ_FACT = "runner.journal.seq";
|
|
11
|
+
/**
|
|
12
|
+
* The runner (design memo, Phase 3, section 8): the node's loop with a model in it. Each tick perceives through the
|
|
13
|
+
* read capabilities, hands the situation and the capability catalog to the model as tools, carries out what it
|
|
14
|
+
* proposes — reads at once, acts through `Catalog.invoke` where the three checks decide — gives the result or the
|
|
15
|
+
* refusal back so the model can revise, and records the tick: the decision records the catalog wrote, the hash of
|
|
16
|
+
* the whole transcript, and the counts. Limits bound a tick (tool calls, acts, the day's model calls); nothing a
|
|
17
|
+
* model says reaches the chain except through the catalog, and nothing it says is kept except by hash. The watchers
|
|
18
|
+
* (heartbeat, settlement, withdrawal, discovery) do not pass through the model; they run beside the runner.
|
|
19
|
+
*/
|
|
20
|
+
export class Runner {
|
|
21
|
+
node;
|
|
22
|
+
catalog;
|
|
23
|
+
adapter;
|
|
24
|
+
opts;
|
|
25
|
+
maxToolCalls;
|
|
26
|
+
maxActs;
|
|
27
|
+
log;
|
|
28
|
+
ticks = 0;
|
|
29
|
+
constructor(node, catalog, adapter, opts = {}) {
|
|
30
|
+
this.node = node;
|
|
31
|
+
this.catalog = catalog;
|
|
32
|
+
this.adapter = adapter;
|
|
33
|
+
this.opts = opts;
|
|
34
|
+
this.maxToolCalls = opts.maxToolCallsPerTick ?? node.config.runtime.model.maxToolCallsPerTick;
|
|
35
|
+
this.maxActs = opts.maxActsPerTick ?? node.config.runtime.model.maxActsPerTick;
|
|
36
|
+
this.log = opts.log ?? node.log;
|
|
37
|
+
}
|
|
38
|
+
/** The tools the model gets: the reads and the acts of the catalog as this caller sees them (no admin, no build or dry-run variants). */
|
|
39
|
+
tools() {
|
|
40
|
+
return this.catalog
|
|
41
|
+
.list(RUNNER_CALLER)
|
|
42
|
+
.filter((c) => c.action === "read" || (c.action === "act" && c.modes.includes("act")))
|
|
43
|
+
.map((c) => ({ name: c.name, description: c.description, inputSchema: c.inputSchema }));
|
|
44
|
+
}
|
|
45
|
+
/** What the model is told once per tick, before the situation. */
|
|
46
|
+
system() {
|
|
47
|
+
const cfg = this.node.config;
|
|
48
|
+
return [
|
|
49
|
+
`You are the runner of FOGA cell ${cfg.name} (account ${this.node.body.address}), an agent on the ten-ERC stack. You decide which open ERC-8414 tenders the cell takes and carry them through: tender_inspect, tender_precheck (eligible: false names the gate that refuses), skill_run with the task's input, tender_submit with the run's resultHash and runReceipt, tender_settle with the run's answer, credit_withdraw when a settlement credited rather than paid.`,
|
|
50
|
+
`Every act you propose is judged by the cell's catalog: permission, the policy below, the budget, the account's own gate. A refusal (ok: false, gate, reason, decisionId) is the cell's verdict; do not look for a way around it and do not retry the same act with cosmetic changes. A tender whose status is failed (a run or a submission of the cell's failed) answers with that failure; leave it. Reads are free; acts are limited per tick (the situation says how many). Say briefly what you are doing and why; stop when there is nothing left to decide.`,
|
|
51
|
+
`The cell's policy (what it takes and how much it may move): ${JSON.stringify(cfg.policy)}.`,
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|
|
54
|
+
/** The situation of this tick, composed from the read capabilities and the store. */
|
|
55
|
+
async situation() {
|
|
56
|
+
const node = this.node;
|
|
57
|
+
const k = node.kernel;
|
|
58
|
+
const cell = node.body.address;
|
|
59
|
+
const invoke = (name, input = {}) => this.catalog.invoke(node, name, input, RUNNER_CALLER);
|
|
60
|
+
const [block, statusR, listR, anchor] = await Promise.all([k.blockNumber(), invoke("cell_status"), invoke("tender_list"), k.aid.stateName(cell)]);
|
|
61
|
+
const status = statusR.ok ? statusR.output : null;
|
|
62
|
+
const decisions = node.store.decisions({ limit: 5000 });
|
|
63
|
+
const runnerPrechecks = decisions.filter((d) => d.caller === RUNNER_CALLER.id && d.capability === "tender_precheck");
|
|
64
|
+
const jobs = node.store.jobs(null, 5000);
|
|
65
|
+
const runs = jobs.filter((j) => j.kind === "skill-run" && j.status === "done");
|
|
66
|
+
const held = await node.heldSkills().catch(() => []);
|
|
67
|
+
const tenders = [];
|
|
68
|
+
if (listR.ok) {
|
|
69
|
+
const open = listR.output.tenders.filter((t) => t.demander.toLowerCase() !== cell.toLowerCase());
|
|
70
|
+
for (const t of open.slice(0, 20)) {
|
|
71
|
+
const r = await invoke("tender_inspect", { tokenId: t.tokenId });
|
|
72
|
+
if (!r.ok)
|
|
73
|
+
continue;
|
|
74
|
+
const v = r.output;
|
|
75
|
+
const mine = v.submissions.filter((s) => s.fulfiller.toLowerCase() === cell.toLowerCase()).at(-1) ?? null;
|
|
76
|
+
const credit = await k.tenders.creditOf(BigInt(t.tokenId), cell).catch(() => 0n);
|
|
77
|
+
let st = "open";
|
|
78
|
+
let gate;
|
|
79
|
+
let reason;
|
|
80
|
+
const failedJob = jobs.find((j) => j.status === "failed" && ((j.kind === "skill-run" && String(j.input?.tokenId) === t.tokenId) || (j.kind === "tender" && j.id.startsWith(`tender:${t.tokenId}:`))));
|
|
81
|
+
if (mine && mine.status === 1)
|
|
82
|
+
st = "settled";
|
|
83
|
+
else if (mine && mine.status === 0)
|
|
84
|
+
st = "submitted";
|
|
85
|
+
else if (failedJob) {
|
|
86
|
+
st = "failed";
|
|
87
|
+
gate = "execution";
|
|
88
|
+
reason = `${failedJob.kind} ${failedJob.id} failed: ${(failedJob.error ?? "no reason recorded").slice(0, 200)}`;
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const last = runnerPrechecks.filter((d) => d.jobId === `precheck:${t.tokenId}`).at(-1);
|
|
92
|
+
if (last?.verdict === "allowed")
|
|
93
|
+
st = "eligible";
|
|
94
|
+
else if (last) {
|
|
95
|
+
st = "refused";
|
|
96
|
+
gate = last.gate;
|
|
97
|
+
reason = last.reason;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
const answer = mine ? (runs.find((j) => j.result?.resultHash?.toLowerCase() === mine.resultHash.toLowerCase())?.result?.answer ?? null) : null;
|
|
101
|
+
tenders.push({
|
|
102
|
+
tokenId: v.tokenId,
|
|
103
|
+
demander: v.demander,
|
|
104
|
+
rewardWei: v.rewardWei,
|
|
105
|
+
escrowWei: v.escrowWei,
|
|
106
|
+
submitBy: v.submitBy,
|
|
107
|
+
settleBy: v.settleBy,
|
|
108
|
+
completions: v.completions,
|
|
109
|
+
maxCompletions: v.maxCompletions,
|
|
110
|
+
acceptanceAuthority: v.acceptanceAuthority,
|
|
111
|
+
task: v.task ? { skill: v.task.skill, packageHash: v.task.packageHash, input: v.task.input, acceptance: v.task.acceptance } : null,
|
|
112
|
+
status: st,
|
|
113
|
+
...(gate ? { gate, reason } : {}),
|
|
114
|
+
submission: mine ? { submissionId: mine.id, status: mine.status, resultHash: mine.resultHash, answer } : null,
|
|
115
|
+
creditWei: credit.toString(),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const since = node.store.getFact(JOURNAL_SEQ_FACT) ?? 0;
|
|
120
|
+
const events = node.store
|
|
121
|
+
.journalEntries()
|
|
122
|
+
.filter((e) => e.seq > since && (e.event === "tender-discovered" || e.event === "safety-changed"))
|
|
123
|
+
.map((e) => ({ seq: e.seq, at: e.at, event: e.event, detail: e.detail }));
|
|
124
|
+
const use = node.budgetUse();
|
|
125
|
+
const cfg = node.config;
|
|
126
|
+
return {
|
|
127
|
+
at: new Date().toISOString(),
|
|
128
|
+
block,
|
|
129
|
+
chainId: node.chainId,
|
|
130
|
+
cell: { name: cfg.name, account: cell, operator: cfg.identity.operator, anchor, safety: status?.safety ?? { verdict: "unknown", reasons: [] } },
|
|
131
|
+
budget: {
|
|
132
|
+
dailyValueWei: cfg.policy.budget.dailyValueWei,
|
|
133
|
+
perOperationValueWei: cfg.policy.budget.perOperationValueWei,
|
|
134
|
+
dailyGasWei: cfg.policy.budget.dailyGasWei,
|
|
135
|
+
maxConcurrentCommitments: cfg.policy.budget.maxConcurrentCommitments,
|
|
136
|
+
valueWei: use.valueWei.toString(),
|
|
137
|
+
gasWei: use.gasWei.toString(),
|
|
138
|
+
reservedValueWei: use.reservedValueWei.toString(),
|
|
139
|
+
reservedFeeWei: use.reservedFeeWei.toString(),
|
|
140
|
+
runningCommitments: use.runningCommitments,
|
|
141
|
+
model: { callsToday: this.modelCallsToday(), dailyCalls: cfg.runtime.model.dailyCalls },
|
|
142
|
+
},
|
|
143
|
+
policy: cfg.policy.tenders,
|
|
144
|
+
skills: { held: held.map((h) => ({ name: h.name, packageHash: h.packageHash })), offered: cfg.skills.offered },
|
|
145
|
+
tenders,
|
|
146
|
+
events,
|
|
147
|
+
// the last forty, plus the runner's own mints however old (the stub's one-time over-budget proposal is recognized by its record)
|
|
148
|
+
recentDecisions: [...decisions.filter((d) => d.caller === RUNNER_CALLER.id && d.capability === "tender_mint" && !decisions.slice(-40).includes(d)), ...decisions.slice(-40)].map((d) => ({ id: d.id, at: d.at, caller: d.caller, capability: d.capability, mode: d.mode, verdict: d.verdict, gate: d.gate, reason: d.reason, jobId: d.jobId })),
|
|
149
|
+
limits: { toolCalls: this.maxToolCalls, acts: this.maxActs },
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
modelCallsToday() {
|
|
153
|
+
return this.node.store.getFact(modelCallsKey()) ?? 0;
|
|
154
|
+
}
|
|
155
|
+
countModelCall() {
|
|
156
|
+
const key = modelCallsKey();
|
|
157
|
+
this.node.store.setFact(key, (this.node.store.getFact(key) ?? 0) + 1);
|
|
158
|
+
}
|
|
159
|
+
/** One tick: perceive, propose, decide, execute, record. */
|
|
160
|
+
async tick() {
|
|
161
|
+
const node = this.node;
|
|
162
|
+
this.ticks++;
|
|
163
|
+
const situation = await this.situation();
|
|
164
|
+
const lastSeq = Math.max(node.store.getFact(JOURNAL_SEQ_FACT) ?? 0, ...situation.events.map((e) => e.seq));
|
|
165
|
+
const report = { at: situation.at, block: situation.block, skipped: null, modelCalls: 0, toolCalls: 0, acts: 0, decisions: [], refusals: [], trajectoryHash: null, error: null, tenders: situation.tenders.map((t) => ({ tokenId: t.tokenId, status: t.status, ...(t.gate ? { gate: t.gate } : {}) })) };
|
|
166
|
+
// event-driven: the model is called when something asks for a decision — an actionable tender or a new event
|
|
167
|
+
if (!chooseTender(situation) && situation.events.length === 0) {
|
|
168
|
+
report.skipped = "nothing to propose";
|
|
169
|
+
node.store.setFact(JOURNAL_SEQ_FACT, lastSeq);
|
|
170
|
+
return report;
|
|
171
|
+
}
|
|
172
|
+
if (situation.budget.model.callsToday >= situation.budget.model.dailyCalls) {
|
|
173
|
+
report.skipped = `the day's ${situation.budget.model.dailyCalls} model calls are spent`;
|
|
174
|
+
node.store.journal("runner-skipped", { at: situation.at, block: situation.block, reason: report.skipped });
|
|
175
|
+
node.store.setFact(JOURNAL_SEQ_FACT, lastSeq);
|
|
176
|
+
return report;
|
|
177
|
+
}
|
|
178
|
+
const system = this.system();
|
|
179
|
+
const tools = this.tools();
|
|
180
|
+
const offered = new Set(tools.map((t) => t.name));
|
|
181
|
+
const messages = [{ role: "user", content: [{ type: "text", text: JSON.stringify(situation) }] }];
|
|
182
|
+
const maxRounds = this.maxToolCalls + 2;
|
|
183
|
+
try {
|
|
184
|
+
for (let round = 0; round < maxRounds; round++) {
|
|
185
|
+
const request = { system, messages, tools };
|
|
186
|
+
const promptHash = sha256Hex(jcs(request));
|
|
187
|
+
this.countModelCall();
|
|
188
|
+
report.modelCalls++;
|
|
189
|
+
const res = await this.adapter.propose(request);
|
|
190
|
+
const responseHash = sha256Hex(jcs(res.content));
|
|
191
|
+
messages.push({ role: "assistant", content: res.content });
|
|
192
|
+
const uses = res.content.filter((b) => b.type === "tool_use");
|
|
193
|
+
if (uses.length === 0)
|
|
194
|
+
break;
|
|
195
|
+
const results = [];
|
|
196
|
+
for (const use of uses) {
|
|
197
|
+
const answer = (content, isError = false) => {
|
|
198
|
+
results.push({ type: "tool_result", tool_use_id: use.id, content: typeof content === "string" ? content : JSON.stringify(content, (_k, v) => (typeof v === "bigint" ? v.toString() : v)), ...(isError ? { is_error: true } : {}) });
|
|
199
|
+
};
|
|
200
|
+
if (!offered.has(use.name)) {
|
|
201
|
+
answer({ ok: false, gate: "permission", reason: `${use.name} is not a tool of this runner` }, true);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (report.toolCalls >= this.maxToolCalls) {
|
|
205
|
+
answer({ ok: false, gate: "limit", reason: `this tick's ${this.maxToolCalls} tool calls are used; the rest waits for the next tick` }, true);
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
report.toolCalls++;
|
|
209
|
+
const cap = this.catalog.get(use.name);
|
|
210
|
+
if (cap.action === "read") {
|
|
211
|
+
const r = await this.catalog.invoke(node, use.name, use.input, RUNNER_CALLER);
|
|
212
|
+
answer(r.ok ? r.output : { ok: false, gate: r.gate, reason: r.reason }, !r.ok);
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
const counted = use.name !== "tender_precheck";
|
|
216
|
+
if (counted && report.acts >= this.maxActs) {
|
|
217
|
+
answer({ ok: false, gate: "limit", reason: `this tick's ${this.maxActs} acts are used; the rest waits for the next tick` }, true);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (counted)
|
|
221
|
+
report.acts++;
|
|
222
|
+
const r = await this.catalog.invoke(node, use.name, use.input, RUNNER_CALLER, { mode: "act", model: { provider: this.adapter.provider, model: this.adapter.model, promptHash, responseHash } });
|
|
223
|
+
if (r.decisionId !== undefined)
|
|
224
|
+
report.decisions.push(r.decisionId);
|
|
225
|
+
if (use.name === "tender_precheck") {
|
|
226
|
+
answer(r.ok ? { eligible: true, decisionId: r.decisionId ?? null, result: r.output } : { eligible: false, gate: r.gate, reason: r.reason, decisionId: r.decisionId ?? null });
|
|
227
|
+
if (!r.ok)
|
|
228
|
+
report.refusals.push({ capability: use.name, gate: r.gate, reason: r.reason });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (r.ok)
|
|
232
|
+
answer({ ok: true, decisionId: r.decisionId ?? null, jobId: r.jobId ?? null, operations: r.operations ?? [], output: r.output });
|
|
233
|
+
else {
|
|
234
|
+
report.refusals.push({ capability: use.name, gate: r.gate, reason: r.reason });
|
|
235
|
+
answer({ ok: false, gate: r.gate, reason: r.reason, decisionId: r.decisionId ?? null, jobId: r.jobId ?? null }, true);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
messages.push({ role: "user", content: results });
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch (e) {
|
|
242
|
+
report.error = e instanceof ModelError ? `model: ${e.message}` : e.message;
|
|
243
|
+
this.log(`runner: ${report.error}`);
|
|
244
|
+
node.store.journal("runner-model-error", { at: situation.at, block: situation.block, provider: this.adapter.provider, model: this.adapter.model, message: report.error });
|
|
245
|
+
}
|
|
246
|
+
report.trajectoryHash = sha256Hex(jcs({ system, tools: tools.map((t) => t.name), messages }));
|
|
247
|
+
node.store.journal("runner-tick", { at: situation.at, block: situation.block, provider: this.adapter.provider, model: this.adapter.model, modelCalls: report.modelCalls, toolCalls: report.toolCalls, acts: report.acts, decisions: report.decisions, refusals: report.refusals, trajectoryHash: report.trajectoryHash, tenders: report.tenders, error: report.error });
|
|
248
|
+
node.store.setFact(JOURNAL_SEQ_FACT, lastSeq);
|
|
249
|
+
if (this.opts.transcriptsDir) {
|
|
250
|
+
mkdirSync(this.opts.transcriptsDir, { recursive: true });
|
|
251
|
+
writeFileSync(join(this.opts.transcriptsDir, `${situation.at.replace(/[:.]/g, "-")}-tick-${String(this.ticks).padStart(4, "0")}.json`), JSON.stringify({ at: situation.at, system, tools: tools.map((t) => t.name), messages, report }, null, 2));
|
|
252
|
+
}
|
|
253
|
+
this.log(`runner tick ${this.ticks} at block ${report.block}: ${report.modelCalls} model call(s), ${report.toolCalls} tool call(s), ${report.acts} act(s), decisions [${report.decisions.join(", ")}]${report.refusals.length ? `, refused at ${report.refusals.map((r) => `${r.capability}:${r.gate}`).join(", ")}` : ""}; trajectory ${report.trajectoryHash.slice(0, 18)}…`);
|
|
254
|
+
return report;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/** The node acting for itself: the watchers propose nothing to a model and need no key beyond the operator's. */
|
|
2
|
+
export const WATCHER_CALLER = { id: "watcher", entrance: "runner", level: "act", visibility: "operator" };
|
|
3
|
+
/**
|
|
4
|
+
* The watchers (design memo, Phase 3, section 2): what a node does without being asked and without a model —
|
|
5
|
+
* keep the anchor alive, notice new tenders, finish what it committed to once the chain lets it (settle a pending
|
|
6
|
+
* submission whose answer it holds, withdraw credited reward), and record a change of its safety state. Each duty
|
|
7
|
+
* goes through the catalog like any caller, so it passes the same gates and leaves the same records. `tick` runs
|
|
8
|
+
* every duty once; the serve loop (3b) calls it on the poll interval.
|
|
9
|
+
*/
|
|
10
|
+
export class Watchers {
|
|
11
|
+
node;
|
|
12
|
+
catalog;
|
|
13
|
+
constructor(node, catalog) {
|
|
14
|
+
this.node = node;
|
|
15
|
+
this.catalog = catalog;
|
|
16
|
+
}
|
|
17
|
+
async tick() {
|
|
18
|
+
const node = this.node;
|
|
19
|
+
const k = node.kernel;
|
|
20
|
+
const cell = node.body;
|
|
21
|
+
const block = await k.blockNumber();
|
|
22
|
+
const now = await node.now();
|
|
23
|
+
// 1. liveness: heartbeat when less than half the window is left
|
|
24
|
+
const anchor = await k.aid.stateName(cell.address);
|
|
25
|
+
let heartbeat = { sent: false, reason: `anchor ${anchor}` };
|
|
26
|
+
if (anchor === "ACTIVE" || anchor === "STALE") {
|
|
27
|
+
const [window, lastSeen] = await Promise.all([k.aid.livenessWindow(cell.address), k.aid.lastSeen(cell.address)]);
|
|
28
|
+
const left = Number(lastSeen) + window - now;
|
|
29
|
+
if (anchor === "STALE" || left < window / 2) {
|
|
30
|
+
const r = await this.catalog.invoke(node, "aid_heartbeat", { jobId: `heartbeat:${Math.floor(now / Math.max(60, Math.floor(window / 2)))}` }, WATCHER_CALLER);
|
|
31
|
+
heartbeat = r.ok ? { sent: true, reason: `${left} s of ${window} s left` } : { sent: false, reason: `${r.gate}: ${r.reason}` };
|
|
32
|
+
}
|
|
33
|
+
else
|
|
34
|
+
heartbeat = { sent: false, reason: `${left} s of ${window} s left` };
|
|
35
|
+
}
|
|
36
|
+
// 2. discovery: open tenders not seen before are recorded as facts for the runner (3c) and reported
|
|
37
|
+
const discovered = [];
|
|
38
|
+
const list = await this.catalog.invoke(node, "tender_list", {}, WATCHER_CALLER);
|
|
39
|
+
if (list.ok) {
|
|
40
|
+
for (const t of list.output.tenders) {
|
|
41
|
+
if (t.demander.toLowerCase() === cell.address.toLowerCase())
|
|
42
|
+
continue;
|
|
43
|
+
const key = `watch.tender.${t.tokenId}`;
|
|
44
|
+
if (k.fact(key) === undefined) {
|
|
45
|
+
k.setFact(key, { seenAt: now, block, rewardWei: t.rewardWei, demander: t.demander });
|
|
46
|
+
k.store.journal("tender-discovered", { tokenId: t.tokenId, demander: t.demander, rewardWei: t.rewardWei, block });
|
|
47
|
+
discovered.push(t.tokenId);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 3. completion: a pending submission of the cell whose answer a skill run holds is settled; credited reward is withdrawn
|
|
52
|
+
const settled = [];
|
|
53
|
+
const withdrawn = [];
|
|
54
|
+
const jobs = k.store.jobs(null, 1000);
|
|
55
|
+
const runs = jobs.filter((j) => j.kind === "skill-run" && j.status === "done");
|
|
56
|
+
for (const job of jobs.filter((j) => j.kind === "tender" && j.status === "done")) {
|
|
57
|
+
const result = job.result;
|
|
58
|
+
if (!result?.tokenId || result.submissionId === undefined)
|
|
59
|
+
continue;
|
|
60
|
+
const tokenId = BigInt(result.tokenId);
|
|
61
|
+
const sub = await k.tenders.submissionOf(tokenId, BigInt(result.submissionId)).catch(() => null);
|
|
62
|
+
if (!sub)
|
|
63
|
+
continue;
|
|
64
|
+
if (sub.status === 0) {
|
|
65
|
+
const run = runs.find((r) => r.result?.resultHash?.toLowerCase() === sub.resultHash.toLowerCase());
|
|
66
|
+
const answer = run?.result?.answer;
|
|
67
|
+
if (!answer)
|
|
68
|
+
continue;
|
|
69
|
+
const r = await this.catalog.invoke(node, "tender_settle", { tokenId: result.tokenId, submissionId: result.submissionId, answer, jobId: `${job.id}:settle` }, WATCHER_CALLER);
|
|
70
|
+
settled.push({ tokenId: result.tokenId, submissionId: result.submissionId, outcome: r.ok ? "settled" : `${r.gate}: ${r.reason}` });
|
|
71
|
+
}
|
|
72
|
+
const credit = await k.tenders.creditOf(tokenId, cell.address);
|
|
73
|
+
if (credit > 0n) {
|
|
74
|
+
const r = await this.catalog.invoke(node, "credit_withdraw", { tokenId: result.tokenId, jobId: `${job.id}:withdraw:${credit}` }, WATCHER_CALLER);
|
|
75
|
+
withdrawn.push({ tokenId: result.tokenId, outcome: r.ok ? `withdrew ${credit} wei` : `${r.gate}: ${r.reason}` });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// 4. safety: a change of verdict is journaled (the decision records show what it stopped)
|
|
79
|
+
const status = await cell.status();
|
|
80
|
+
const last = k.fact("watch.safety");
|
|
81
|
+
const changed = !last || last.verdict !== status.verdictName;
|
|
82
|
+
if (changed) {
|
|
83
|
+
k.setFact("watch.safety", { verdict: status.verdictName, reasons: status.reasonNames, at: now });
|
|
84
|
+
k.store.journal("safety-changed", { from: last?.verdict ?? null, to: status.verdictName, reasons: status.reasonNames, restrictions: status.restrictions, block });
|
|
85
|
+
}
|
|
86
|
+
return { at: new Date().toISOString(), block, heartbeat, discovered, settled, withdrawn, safety: { verdict: status.verdictName, reasons: status.reasonNames, changed }, anchor };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { getAddress } from "ethers";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { BindingStateName, Restriction } from "../core/asf.js";
|
|
4
|
+
import { keccakJcs } from "../core/canonical.js";
|
|
5
|
+
import { ok, refused } from "../node/policy.js";
|
|
6
|
+
import { derivedJobId, zAddress, zBytes32, zJobId, zUint } from "./common.js";
|
|
7
|
+
/**
|
|
8
|
+
* Admin capabilities: the owner's and the guardian's acts (design memo, Phase 3, section 3 — admin is CLI-only in
|
|
9
|
+
* Phase 3). They need the owner's or the guardian's key loaded for the one run (`foga cell owner …`,
|
|
10
|
+
* `foga cell guardian …`); a node process holding the operator key alone refuses them at the permission gate. Their
|
|
11
|
+
* transactions are the signer's own (`route: direct`), not the cell's, except `identity_transfer`, which moves an
|
|
12
|
+
* asset through the account and is therefore the owner's `execute`.
|
|
13
|
+
*/
|
|
14
|
+
export const operator_rotate = {
|
|
15
|
+
name: "operator_rotate",
|
|
16
|
+
title: "Rotate the operator",
|
|
17
|
+
description: "The owner sets a new operator key (address, optional expiry and daily native-value cap) on the account; the allow-list stays, and the configuration records the new operator.",
|
|
18
|
+
action: "admin",
|
|
19
|
+
visibility: "operator",
|
|
20
|
+
principal: "cell",
|
|
21
|
+
signer: "owner",
|
|
22
|
+
ercs: [8468],
|
|
23
|
+
input: z.object({ operator: zAddress, validUntil: z.coerce.number().int().min(0).default(0), dailyValueCapWei: zUint.default("10000000000000000"), jobId: zJobId }),
|
|
24
|
+
output: z.object({ operator: zAddress, previous: zAddress, txHash: z.string(), jobId: z.string() }),
|
|
25
|
+
async plan({ node }, input) {
|
|
26
|
+
const cell = node.body;
|
|
27
|
+
const previous = await cell.operator();
|
|
28
|
+
const gates = [];
|
|
29
|
+
if (getAddress(previous) === getAddress(input.operator))
|
|
30
|
+
gates.push(refused("input", `${input.operator} is the operator already`));
|
|
31
|
+
else
|
|
32
|
+
gates.push(ok("policy", `operator ${previous} -> ${input.operator}`));
|
|
33
|
+
const jobId = input.jobId ?? derivedJobId("operator-rotate", { operator: input.operator, previous });
|
|
34
|
+
return {
|
|
35
|
+
job: { id: jobId, kind: "admin", input: { operator: input.operator, validUntil: input.validUntil, dailyValueCapWei: input.dailyValueCapWei } },
|
|
36
|
+
gates,
|
|
37
|
+
valueWei: 0n,
|
|
38
|
+
steps: [{ name: "setOperator", by: "owner", route: "direct", build: () => ({ target: cell.address, value: 0n, data: cell.iface.encodeFunctionData("setOperator", [input.operator, input.validUntil, BigInt(input.dailyValueCapWei)]), label: `setOperator(${input.operator})` }) }],
|
|
39
|
+
output: (confirmed) => {
|
|
40
|
+
node.config.identity.operator = getAddress(input.operator);
|
|
41
|
+
node.saveConfig();
|
|
42
|
+
return { operator: getAddress(input.operator), previous: getAddress(previous), txHash: confirmed[0].txHash, jobId };
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
const RESTRICTIONS = Object.keys(Restriction);
|
|
48
|
+
export const guardian_suspend = {
|
|
49
|
+
name: "guardian_suspend",
|
|
50
|
+
title: "Suspend the cell's binding",
|
|
51
|
+
description: "The guardian (the binding's intervener) suspends the cell's presented binding in the ERC-8468 Binding Registry with the given restrictions; commitment-class calls stop at the account's gate, de-risk actions stay open under deriskOnly.",
|
|
52
|
+
action: "admin",
|
|
53
|
+
visibility: "operator",
|
|
54
|
+
principal: "cell",
|
|
55
|
+
signer: "guardian",
|
|
56
|
+
ercs: [8468],
|
|
57
|
+
input: z.object({ restrictions: z.array(z.enum(RESTRICTIONS)).default(["noNewRisk", "deriskOnly"]), reason: z.string().min(1).max(500), response: z.string().max(500).default("no new commitments; de-risk actions continue; wait for resume"), jobId: zJobId }),
|
|
58
|
+
output: z.object({ bindingId: zBytes32, restrictions: z.number(), state: z.string(), txHash: z.string(), jobId: z.string() }),
|
|
59
|
+
async plan({ node }, input) {
|
|
60
|
+
const k = node.kernel;
|
|
61
|
+
const bindingId = node.config.identity.bindingId;
|
|
62
|
+
const gates = [];
|
|
63
|
+
if (!bindingId)
|
|
64
|
+
gates.push(refused("input", "the configuration names no binding (identity.bindingId)"));
|
|
65
|
+
const mask = input.restrictions.reduce((m, r) => m | Restriction[r], 0);
|
|
66
|
+
let state = "None";
|
|
67
|
+
if (bindingId) {
|
|
68
|
+
const b = await k.asf.bindingOf(bindingId);
|
|
69
|
+
state = BindingStateName[b.state] ?? String(b.state);
|
|
70
|
+
if (state !== "Active")
|
|
71
|
+
gates.push(refused("policy", `binding ${bindingId.slice(0, 10)}… is ${state}, not Active`));
|
|
72
|
+
else
|
|
73
|
+
gates.push(ok("policy", `binding ${bindingId.slice(0, 10)}… Active; restrictions ${input.restrictions.join("|")} (${mask})`));
|
|
74
|
+
}
|
|
75
|
+
const jobId = input.jobId ?? derivedJobId("guardian-suspend", { bindingId, mask, reason: input.reason, at: Math.floor(Date.now() / 1000) });
|
|
76
|
+
const reasonRef = keccakJcs({ kind: "reason", summary: input.reason });
|
|
77
|
+
const responseRef = keccakJcs({ kind: "response", summary: input.response });
|
|
78
|
+
return {
|
|
79
|
+
job: { id: jobId, kind: "admin", input: { bindingId, restrictions: input.restrictions, reason: input.reason } },
|
|
80
|
+
gates,
|
|
81
|
+
valueWei: 0n,
|
|
82
|
+
steps: [{ name: "suspend", by: "guardian", route: "direct", build: () => ({ target: k.asf.bindingRegistryAddress, value: 0n, data: k.asf.encodeSuspend(bindingId, mask, responseRef, reasonRef), label: `suspend(${bindingId.slice(0, 10)}…, ${mask})` }) }],
|
|
83
|
+
output: async (confirmed) => ({ bindingId: bindingId, restrictions: mask, state: BindingStateName[(await k.asf.bindingOf(bindingId)).state] ?? "?", txHash: confirmed[0].txHash, jobId }),
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
export const guardian_resume = {
|
|
88
|
+
name: "guardian_resume",
|
|
89
|
+
title: "Resume the cell's binding",
|
|
90
|
+
description: "The guardian lifts the suspension of the cell's presented binding.",
|
|
91
|
+
action: "admin",
|
|
92
|
+
visibility: "operator",
|
|
93
|
+
principal: "cell",
|
|
94
|
+
signer: "guardian",
|
|
95
|
+
ercs: [8468],
|
|
96
|
+
input: z.object({ reason: z.string().min(1).max(500), jobId: zJobId }),
|
|
97
|
+
output: z.object({ bindingId: zBytes32, state: z.string(), txHash: z.string(), jobId: z.string() }),
|
|
98
|
+
async plan({ node }, input) {
|
|
99
|
+
const k = node.kernel;
|
|
100
|
+
const bindingId = node.config.identity.bindingId;
|
|
101
|
+
const gates = [];
|
|
102
|
+
if (!bindingId)
|
|
103
|
+
gates.push(refused("input", "the configuration names no binding (identity.bindingId)"));
|
|
104
|
+
else {
|
|
105
|
+
const b = await k.asf.bindingOf(bindingId);
|
|
106
|
+
const state = BindingStateName[b.state] ?? String(b.state);
|
|
107
|
+
if (state !== "Suspended")
|
|
108
|
+
gates.push(refused("policy", `binding ${bindingId.slice(0, 10)}… is ${state}, not Suspended`));
|
|
109
|
+
else
|
|
110
|
+
gates.push(ok("policy", `binding ${bindingId.slice(0, 10)}… Suspended`));
|
|
111
|
+
}
|
|
112
|
+
const jobId = input.jobId ?? derivedJobId("guardian-resume", { bindingId, reason: input.reason, at: Math.floor(Date.now() / 1000) });
|
|
113
|
+
return {
|
|
114
|
+
job: { id: jobId, kind: "admin", input: { bindingId, reason: input.reason } },
|
|
115
|
+
gates,
|
|
116
|
+
valueWei: 0n,
|
|
117
|
+
steps: [{ name: "resume", by: "guardian", route: "direct", build: () => ({ target: k.asf.bindingRegistryAddress, value: 0n, data: k.asf.encodeResume(bindingId, keccakJcs({ kind: "reason", summary: input.reason })), label: `resume(${bindingId.slice(0, 10)}…)` }) }],
|
|
118
|
+
output: async (confirmed) => ({ bindingId: bindingId, state: BindingStateName[(await k.asf.bindingOf(bindingId)).state] ?? "?", txHash: confirmed[0].txHash, jobId }),
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
export const binding_present = {
|
|
123
|
+
name: "binding_present",
|
|
124
|
+
title: "Present bindings",
|
|
125
|
+
description: "The owner re-points the account's presented bindings (presentBindings is onlyOwner); the account executes nothing until the presented set satisfies its requirement.",
|
|
126
|
+
action: "admin",
|
|
127
|
+
visibility: "operator",
|
|
128
|
+
principal: "cell",
|
|
129
|
+
signer: "owner",
|
|
130
|
+
ercs: [8468],
|
|
131
|
+
input: z.object({ bindingIds: z.array(zBytes32).min(1), jobId: zJobId }),
|
|
132
|
+
output: z.object({ bindingIds: z.array(zBytes32), verdict: z.string(), txHash: z.string(), jobId: z.string() }),
|
|
133
|
+
async plan({ node }, input) {
|
|
134
|
+
const cell = node.body;
|
|
135
|
+
const jobId = input.jobId ?? derivedJobId("binding-present", { bindingIds: input.bindingIds });
|
|
136
|
+
return {
|
|
137
|
+
job: { id: jobId, kind: "admin", input: { bindingIds: input.bindingIds } },
|
|
138
|
+
gates: [ok("policy", `${input.bindingIds.length} binding(s) to present`)],
|
|
139
|
+
valueWei: 0n,
|
|
140
|
+
steps: [{ name: "present", by: "owner", route: "direct", build: () => ({ target: cell.address, value: 0n, data: cell.iface.encodeFunctionData("presentBindings", [input.bindingIds]), label: "presentBindings" }) }],
|
|
141
|
+
output: async (confirmed) => {
|
|
142
|
+
node.config.identity.bindingId = input.bindingIds[0];
|
|
143
|
+
node.saveConfig();
|
|
144
|
+
return { bindingIds: input.bindingIds, verdict: (await cell.status()).verdictName, txHash: confirmed[0].txHash, jobId };
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
export const identity_transfer = {
|
|
150
|
+
name: "identity_transfer",
|
|
151
|
+
title: "Transfer the ERC-8004 identity",
|
|
152
|
+
description: "The owner moves the cell's ERC-8004 agent to another account through the cell's `execute` (an asset leaves the cell: the account refuses this to the operator). The anchor turns STALE until the successor binds; `foga cell migrate` does the whole move.",
|
|
153
|
+
action: "admin",
|
|
154
|
+
visibility: "operator",
|
|
155
|
+
principal: "cell",
|
|
156
|
+
signer: "owner",
|
|
157
|
+
ercs: [8004, 8434],
|
|
158
|
+
input: z.object({ to: zAddress, jobId: zJobId }),
|
|
159
|
+
output: z.object({ agentId: zUint, from: zAddress, to: zAddress, txHash: z.string(), jobId: z.string() }),
|
|
160
|
+
async plan({ node }, input) {
|
|
161
|
+
const k = node.kernel;
|
|
162
|
+
const cell = node.body;
|
|
163
|
+
const agentId = node.config.identity.agentId;
|
|
164
|
+
const gates = [];
|
|
165
|
+
if (!agentId)
|
|
166
|
+
gates.push(refused("input", "the configuration names no agentId"));
|
|
167
|
+
else {
|
|
168
|
+
const owner = await k.identity.ownerOf(BigInt(agentId));
|
|
169
|
+
if (getAddress(owner) !== getAddress(cell.address))
|
|
170
|
+
gates.push(refused("policy", `agent ${agentId} is held by ${owner}, not by the cell`));
|
|
171
|
+
else
|
|
172
|
+
gates.push(ok("policy", `agent ${agentId} held by the cell; moving to ${input.to}`));
|
|
173
|
+
}
|
|
174
|
+
const jobId = input.jobId ?? derivedJobId("identity-transfer", { agentId, to: input.to });
|
|
175
|
+
return {
|
|
176
|
+
job: { id: jobId, kind: "admin", input: { agentId, to: input.to } },
|
|
177
|
+
gates,
|
|
178
|
+
valueWei: 0n,
|
|
179
|
+
steps: [{ name: "transfer", by: "owner", route: "execute", build: () => ({ target: k.identity.address, value: 0n, data: k.identity.encodeTransfer(cell.address, input.to, BigInt(agentId)), label: `transferFrom(cell, ${input.to}, ${agentId})` }) }],
|
|
180
|
+
output: (confirmed) => ({ agentId: agentId, from: getAddress(cell.address), to: getAddress(input.to), txHash: confirmed[0].txHash, jobId }),
|
|
181
|
+
};
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
export const ADMIN_CAPABILITIES = [operator_rotate, guardian_suspend, guardian_resume, binding_present, identity_transfer];
|