@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
package/dist/cli-cell.js
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { newCellConfig, saveCellConfig } from "./node/config.js";
|
|
5
|
+
import { cellInit } from "./node/init.js";
|
|
6
|
+
import { KeyIsolationError, forbiddenVariables, loadOperatorKey, loadRoleKey } from "./node/keys.js";
|
|
7
|
+
import { cellMigrate } from "./node/migrate.js";
|
|
8
|
+
import { Node } from "./node/node.js";
|
|
9
|
+
import { Watchers } from "./node/watchers.js";
|
|
10
|
+
import { defaultCatalog, OPERATOR_CALLER, PUBLIC_CALLER } from "./tools/index.js";
|
|
11
|
+
import { exists, repoRoot } from "./util/fs.js";
|
|
12
|
+
import { fail } from "./util/log.js";
|
|
13
|
+
/**
|
|
14
|
+
* `foga cell …` and `foga node …`: the CLI entrance to the capability catalog (design memo, Phase 3, section 5).
|
|
15
|
+
* Every capability is a subcommand generated from the catalog; the differences from the other entrances are who
|
|
16
|
+
* is calling (the operator, at full level, unless --caller and --level say otherwise) and where the keys are (this
|
|
17
|
+
* shell's environment, loaded for the one command: the operator's for an act, the owner's or guardian's for an
|
|
18
|
+
* admin capability, none for reads, builds and dry runs).
|
|
19
|
+
*/
|
|
20
|
+
export const CELL_USAGE = `
|
|
21
|
+
foga cell list [--public] [--as-json] the capabilities (action, visibility, principal, modes) as the operator or as the public sees them
|
|
22
|
+
foga cell schema <capability> the input and output JSON Schema of a capability
|
|
23
|
+
foga cell new --name <cell> --world <world.json> --store <ops.sqlite> --owner <addr> --operator <addr> --guardian <addr> --out <cell.config.json> [--rpc <url>] [--skills <dir>] [--input-dir <dir>]
|
|
24
|
+
foga cell init --config <cell.config.json> [--rpc <url>] [--capital-wei <n>]
|
|
25
|
+
the cell's birth (the owner's key: FOGA_OWNER_KEY[_FILE]); writes the account, agent id, binding, record and unit into the config
|
|
26
|
+
foga cell migrate --config <cell.config.json> --to <new.config.json> [--store <new ops.sqlite>] [--operator <addr>] [--capital-wei <n>] [--no-sweep]
|
|
27
|
+
move the cell to a new revision-2 account (the owner's key); the new config and store are written beside
|
|
28
|
+
foga cell status --config <cell.config.json> cell_status as the operator
|
|
29
|
+
foga cell <capability> --config <cell.config.json> [--mode simulate|build|act] [--json '<input json>' | --json-file <f>] [--<field> <value> ...]
|
|
30
|
+
any capability; an act loads FOGA_OPERATOR_KEY[_FILE], an admin capability FOGA_OWNER_KEY[_FILE] or FOGA_GUARDIAN_KEY[_FILE]
|
|
31
|
+
[--caller <id> --level read|build|act] call as another caller (what the public or an API-key holder would get)
|
|
32
|
+
foga cell owner <capability> … | foga cell guardian <capability> … the same, said explicitly
|
|
33
|
+
foga node tick --config <cell.config.json> run the watchers once (heartbeat, discovery, settlement, safety) in a node environment (operator key alone)
|
|
34
|
+
foga node watch --config <cell.config.json> [--interval <s>] run the watchers on the poll interval until stopped
|
|
35
|
+
foga node mcp --config <cell.config.json> MCP over stdio for a local client (Claude Desktop, Claude Code, Cursor): the operator's full level with
|
|
36
|
+
FOGA_OPERATOR_KEY[_FILE] in the environment, read and build without it; logs go to stderr
|
|
37
|
+
foga node serve --config <cell.config.json> [--listen <host:port>] [--no-watch]
|
|
38
|
+
the HTTP server: POST /api/<capability>, /mcp (Streamable HTTP), /healthz, / — the public reads and builds,
|
|
39
|
+
a Bearer key acts as its grant says; the watchers run beside it
|
|
40
|
+
foga node key new --config <cell.config.json> --id <id> [--level read|build|act] [--visibility public|caller|operator] [--note <text>]
|
|
41
|
+
mint an API key for the HTTP entrance: printed once, kept only as its hash in the config
|
|
42
|
+
foga node run --config <cell.config.json> [--model stub|anthropic] [--listen <host:port> | --no-http] [--interval <s>] [--once | --ticks <n>] [--transcripts <dir>]
|
|
43
|
+
the embedded node: the HTTP server, the watchers and the runner (a model proposing acts the catalog
|
|
44
|
+
judges) on the poll interval; the operator key alone; anthropic needs ANTHROPIC_API_KEY and runtime.model.model
|
|
45
|
+
`;
|
|
46
|
+
const str = (flags, key) => (typeof flags[key] === "string" ? flags[key] : undefined);
|
|
47
|
+
const RESERVED = new Set(["config", "mode", "json", "json-file", "caller", "level", "rpc", "public", "help"]);
|
|
48
|
+
function inputFromFlags(flags) {
|
|
49
|
+
let input = {};
|
|
50
|
+
const inline = str(flags, "json");
|
|
51
|
+
const file = str(flags, "json-file");
|
|
52
|
+
if (inline)
|
|
53
|
+
input = JSON.parse(inline);
|
|
54
|
+
if (file)
|
|
55
|
+
input = { ...input, ...JSON.parse(readFileSync(resolve(file), "utf8")) };
|
|
56
|
+
for (const [k, v] of Object.entries(flags)) {
|
|
57
|
+
if (RESERVED.has(k))
|
|
58
|
+
continue;
|
|
59
|
+
if (typeof v === "boolean")
|
|
60
|
+
input[k] = v;
|
|
61
|
+
else if (/^[\[{]/.test(v)) {
|
|
62
|
+
try {
|
|
63
|
+
input[k] = JSON.parse(v);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
input[k] = v;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
input[k] = v;
|
|
71
|
+
}
|
|
72
|
+
return input;
|
|
73
|
+
}
|
|
74
|
+
function callerFromFlags(flags) {
|
|
75
|
+
const id = str(flags, "caller");
|
|
76
|
+
const level = str(flags, "level");
|
|
77
|
+
if (!id && !level)
|
|
78
|
+
return OPERATOR_CALLER;
|
|
79
|
+
const lvl = level ?? "build";
|
|
80
|
+
if (!["read", "build", "act", "admin"].includes(lvl))
|
|
81
|
+
fail(`--level must be read, build, act or admin`);
|
|
82
|
+
return { id: id ?? "caller", entrance: "cli", level: lvl, visibility: lvl === "admin" ? "operator" : id ? "caller" : "public" };
|
|
83
|
+
}
|
|
84
|
+
function keysFor(role, strict = false) {
|
|
85
|
+
try {
|
|
86
|
+
return (role === "operator" && strict ? loadOperatorKey() : loadRoleKey(role)).privateKey;
|
|
87
|
+
}
|
|
88
|
+
catch (e) {
|
|
89
|
+
if (e instanceof KeyIsolationError)
|
|
90
|
+
fail(e.message);
|
|
91
|
+
throw e;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function print(value) {
|
|
95
|
+
console.log(JSON.stringify(value, (_k, v) => (typeof v === "bigint" ? v.toString() : v), 2));
|
|
96
|
+
}
|
|
97
|
+
export async function cellCommand(cmd, rest, flags) {
|
|
98
|
+
const catalog = defaultCatalog();
|
|
99
|
+
if (!cmd || cmd === "help") {
|
|
100
|
+
console.log(CELL_USAGE);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (cmd === "list") {
|
|
104
|
+
const caller = flags.public ? PUBLIC_CALLER : OPERATOR_CALLER;
|
|
105
|
+
const rows = catalog.list(caller).map((c) => ({ name: c.name, action: c.action, visibility: c.visibility, principal: c.principal, modes: c.modes.join("/") || "-", ercs: c.ercs.join(" "), title: c.title }));
|
|
106
|
+
if (flags["as-json"])
|
|
107
|
+
return print(rows);
|
|
108
|
+
const w = Math.max(...rows.map((r) => r.name.length));
|
|
109
|
+
console.log(`${"capability".padEnd(w)} action visibility principal modes ercs`);
|
|
110
|
+
for (const r of rows)
|
|
111
|
+
console.log(`${r.name.padEnd(w)} ${r.action.padEnd(6)} ${r.visibility.padEnd(10)} ${r.principal.padEnd(9)} ${r.modes.padEnd(18)} ${r.ercs}`);
|
|
112
|
+
console.log(`\n${rows.length} capabilities${flags.public ? " visible to the public" : ""}; foga cell schema <capability> for the input and output schemas`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (cmd === "schema") {
|
|
116
|
+
const name = rest[0];
|
|
117
|
+
const cap = name ? catalog.get(name) : undefined;
|
|
118
|
+
if (!cap)
|
|
119
|
+
fail(`unknown capability ${name ?? "(none)"}; foga cell list shows them`);
|
|
120
|
+
return print({ name: cap.name, title: cap.title, description: cap.description, action: cap.action, visibility: cap.visibility, principal: cap.principal, ercs: cap.ercs, input: z.toJSONSchema(cap.input, { target: "draft-2020-12", io: "input" }), output: z.toJSONSchema(cap.output, { target: "draft-2020-12", io: "output" }) });
|
|
121
|
+
}
|
|
122
|
+
if (cmd === "new") {
|
|
123
|
+
const need = (k) => str(flags, k) ?? fail(`--${k} is required`);
|
|
124
|
+
const out = resolve(need("out"));
|
|
125
|
+
if (exists(out))
|
|
126
|
+
fail(`${out} exists; choose another --out (a config is never overwritten)`);
|
|
127
|
+
// paths in a config resolve against the config's directory; the defaults point at the repository's skills and
|
|
128
|
+
// fixtures, relative when the config lives inside the repository and absolute when it does not
|
|
129
|
+
const outDir = dirname(out);
|
|
130
|
+
const root = repoRoot();
|
|
131
|
+
const defaultDir = (sub) => (outDir.startsWith(root) ? relative(outDir, join(root, sub)) || "." : join(root, sub));
|
|
132
|
+
const cfg = newCellConfig({ name: need("name"), world: relative(outDir, resolve(need("world"))), store: relative(outDir, resolve(need("store"))), owner: need("owner"), operator: need("operator"), guardian: need("guardian"), rpc: str(flags, "rpc"), skillsDir: str(flags, "skills") ?? defaultDir("scenarios/skills"), inputDir: str(flags, "input-dir") ?? defaultDir("scenarios/fixtures") });
|
|
133
|
+
saveCellConfig(out, cfg);
|
|
134
|
+
console.log(`cell config written to ${out}; fill presence and policy as you like, then: foga cell init --config ${out}`);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const configPath = str(flags, "config") ?? fail("--config <cell.config.json> is required");
|
|
138
|
+
const rpc = str(flags, "rpc");
|
|
139
|
+
const log = (line) => console.error(line);
|
|
140
|
+
if (cmd === "init") {
|
|
141
|
+
const owner = keysFor("owner");
|
|
142
|
+
const node = await Node.open({ configPath, rpc, keys: { owner }, allowUnborn: true, log });
|
|
143
|
+
try {
|
|
144
|
+
const capital = str(flags, "capital-wei");
|
|
145
|
+
const result = await cellInit(node, { capitalWei: capital ? BigInt(capital) : undefined, log });
|
|
146
|
+
print(result);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
node.close();
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (cmd === "migrate") {
|
|
154
|
+
const owner = keysFor("owner");
|
|
155
|
+
const to = resolve(str(flags, "to") ?? fail("--to <new.config.json> is required"));
|
|
156
|
+
if (exists(to))
|
|
157
|
+
fail(`${to} exists; choose another --to`);
|
|
158
|
+
const node = await Node.open({ configPath, rpc, keys: { owner }, log });
|
|
159
|
+
try {
|
|
160
|
+
const capital = str(flags, "capital-wei");
|
|
161
|
+
const result = await cellMigrate(node, { toConfigPath: to, toStorePath: str(flags, "store") ?? to.replace(/\.json$/, "") + ".ops.sqlite", operator: str(flags, "operator"), capitalWei: capital ? BigInt(capital) : undefined, sweep: !flags["no-sweep"], log });
|
|
162
|
+
print(result);
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
node.close();
|
|
166
|
+
}
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
// a capability, possibly said as "owner <capability>" / "guardian <capability>"
|
|
170
|
+
let name = cmd;
|
|
171
|
+
let explicitRole = null;
|
|
172
|
+
if (cmd === "owner" || cmd === "guardian") {
|
|
173
|
+
explicitRole = cmd;
|
|
174
|
+
name = rest.shift() ?? fail(`foga cell ${cmd} <capability> …`);
|
|
175
|
+
}
|
|
176
|
+
if (name === "status")
|
|
177
|
+
name = "cell_status";
|
|
178
|
+
const cap = catalog.get(name);
|
|
179
|
+
if (!cap)
|
|
180
|
+
fail(`unknown capability ${name}; foga cell list shows them`);
|
|
181
|
+
const mode = (str(flags, "mode") ?? "act");
|
|
182
|
+
if (!["simulate", "build", "act"].includes(mode))
|
|
183
|
+
fail("--mode must be simulate, build or act");
|
|
184
|
+
const caller = callerFromFlags(flags);
|
|
185
|
+
const keys = {};
|
|
186
|
+
if (cap.action !== "read" && mode === "act") {
|
|
187
|
+
if (cap.action === "admin") {
|
|
188
|
+
const role = cap.signer ?? explicitRole ?? "owner";
|
|
189
|
+
keys[role] = keysFor(role);
|
|
190
|
+
}
|
|
191
|
+
else
|
|
192
|
+
keys.operator = keysFor("operator");
|
|
193
|
+
}
|
|
194
|
+
const node = await Node.open({ configPath, rpc, keys, log });
|
|
195
|
+
try {
|
|
196
|
+
const input = inputFromFlags(flags);
|
|
197
|
+
const result = await catalog.invoke(node, name, input, caller, { mode });
|
|
198
|
+
print(result);
|
|
199
|
+
if (!result.ok)
|
|
200
|
+
process.exitCode = 2;
|
|
201
|
+
}
|
|
202
|
+
finally {
|
|
203
|
+
node.close();
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
/** The node's own key situation: the operator key alone (acting), or none at all (reads and builds); anything else is refused. */
|
|
207
|
+
function nodeKeys() {
|
|
208
|
+
const forbidden = forbiddenVariables(process.env);
|
|
209
|
+
if (forbidden.length)
|
|
210
|
+
fail(`the node's environment holds ${forbidden.join(", ")}: a node runs with its operator key alone (FOGA_OPERATOR_KEY or FOGA_OPERATOR_KEY_FILE), or with no key for reads and builds`);
|
|
211
|
+
try {
|
|
212
|
+
return { operator: loadOperatorKey().privateKey };
|
|
213
|
+
}
|
|
214
|
+
catch (e) {
|
|
215
|
+
if (e instanceof KeyIsolationError && /no operator key/.test(e.message))
|
|
216
|
+
return {};
|
|
217
|
+
if (e instanceof KeyIsolationError)
|
|
218
|
+
fail(e.message);
|
|
219
|
+
throw e;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Who a local stdio client is: the operator when the key is here, otherwise the machine's user — operator data, build level. */
|
|
223
|
+
function localCaller(hasKey) {
|
|
224
|
+
return hasKey ? { ...OPERATOR_CALLER, entrance: "mcp" } : { id: "local", entrance: "mcp", level: "build", visibility: "operator" };
|
|
225
|
+
}
|
|
226
|
+
export async function nodeCommand(cmd, rest, flags) {
|
|
227
|
+
if (!cmd || cmd === "help") {
|
|
228
|
+
console.log(CELL_USAGE);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const configPath = str(flags, "config") ?? fail("--config <cell.config.json> is required");
|
|
232
|
+
const log = (line) => console.error(line);
|
|
233
|
+
if (cmd === "key") {
|
|
234
|
+
if (rest[0] !== "new")
|
|
235
|
+
fail("foga node key new --config <cfg> --id <id> [--level act] [--visibility caller]");
|
|
236
|
+
const id = str(flags, "id") ?? fail("--id <id> is required");
|
|
237
|
+
const { loadCellConfig } = await import("./node/config.js");
|
|
238
|
+
const loaded = loadCellConfig(configPath);
|
|
239
|
+
if (loaded.config.access.keys.some((k) => k.id === id))
|
|
240
|
+
fail(`a key with id ${id} exists; choose another id (or remove the old entry from the config first)`);
|
|
241
|
+
const level = (str(flags, "level") ?? "act");
|
|
242
|
+
const visibility = (str(flags, "visibility") ?? "caller");
|
|
243
|
+
if (!["read", "build", "act"].includes(level))
|
|
244
|
+
fail("--level must be read, build or act");
|
|
245
|
+
if (!["public", "caller", "operator"].includes(visibility))
|
|
246
|
+
fail("--visibility must be public, caller or operator");
|
|
247
|
+
const { newApiKey } = await import("./http/server.js");
|
|
248
|
+
const made = newApiKey(id);
|
|
249
|
+
loaded.config.access.keys.push({ id, sha256: made.sha256, level, visibility, note: str(flags, "note") });
|
|
250
|
+
saveCellConfig(loaded.path, loaded.config);
|
|
251
|
+
console.error(`key ${id} (${level}, ${visibility}) recorded in ${loaded.path} as its hash; the key itself is printed once below and never again:`);
|
|
252
|
+
console.log(made.key);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (cmd === "mcp") {
|
|
256
|
+
const keys = nodeKeys();
|
|
257
|
+
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys, log });
|
|
258
|
+
const catalog = defaultCatalog();
|
|
259
|
+
const caller = localCaller(Boolean(keys.operator));
|
|
260
|
+
const { buildMcpServer } = await import("./mcp/server.js");
|
|
261
|
+
const { serveStdio } = await import("@modelcontextprotocol/server/stdio");
|
|
262
|
+
const { version } = await import("./util/version.js");
|
|
263
|
+
log(`foga ${version}: MCP over stdio for cell ${node.config.name}${node.cell ? ` (${node.cell.address})` : ""}; caller ${caller.id} at level ${caller.level}`);
|
|
264
|
+
const handle = await serveStdio(() => buildMcpServer({ node, catalog, caller, version }), { onerror: (e) => log(`mcp: ${e.message}`) });
|
|
265
|
+
const stop = () => {
|
|
266
|
+
handle.close().finally(() => {
|
|
267
|
+
node.close();
|
|
268
|
+
process.exit(0);
|
|
269
|
+
});
|
|
270
|
+
};
|
|
271
|
+
process.on("SIGINT", stop);
|
|
272
|
+
process.on("SIGTERM", stop);
|
|
273
|
+
process.stdin.on("end", stop);
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
if (cmd === "serve") {
|
|
277
|
+
const keys = nodeKeys();
|
|
278
|
+
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys, log });
|
|
279
|
+
const catalog = defaultCatalog();
|
|
280
|
+
const { createHttpServer, startWatchLoop } = await import("./http/server.js");
|
|
281
|
+
const { version } = await import("./util/version.js");
|
|
282
|
+
const listen = str(flags, "listen") ?? node.config.runtime.http.listen;
|
|
283
|
+
const running = await createHttpServer({ node, catalog, version, log }).listen(listen);
|
|
284
|
+
log(`foga ${version}: serving cell ${node.config.name}${node.cell ? ` (${node.cell.address})` : ""} at ${running.url} — /mcp, /api/<capability>, /healthz; ${keys.operator ? "acting with the operator key" : "no operator key: reads and builds only"}; ${node.config.access.keys.length} API key(s) configured`);
|
|
285
|
+
const stopWatch = !flags["no-watch"] && keys.operator && node.cell ? startWatchLoop(new Watchers(node, catalog), node.config.runtime.pollSeconds * 1000, log) : () => { };
|
|
286
|
+
const stop = () => {
|
|
287
|
+
stopWatch();
|
|
288
|
+
running.close().finally(() => {
|
|
289
|
+
node.close();
|
|
290
|
+
process.exit(0);
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
process.on("SIGINT", stop);
|
|
294
|
+
process.on("SIGTERM", stop);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
if (cmd === "run") {
|
|
298
|
+
const keys = nodeKeys();
|
|
299
|
+
if (!keys.operator)
|
|
300
|
+
fail("foga node run acts for the cell and needs its operator key (FOGA_OPERATOR_KEY or FOGA_OPERATOR_KEY_FILE); without a key, foga node serve offers reads and builds");
|
|
301
|
+
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys, log });
|
|
302
|
+
const catalog = defaultCatalog();
|
|
303
|
+
const { Runner } = await import("./node/runner.js");
|
|
304
|
+
const { StubAdapter } = await import("./node/model/stub.js");
|
|
305
|
+
const { AnthropicAdapter } = await import("./node/model/anthropic.js");
|
|
306
|
+
const { version } = await import("./util/version.js");
|
|
307
|
+
const providerName = str(flags, "model") ?? node.config.runtime.model.provider;
|
|
308
|
+
if (providerName !== "stub" && providerName !== "anthropic")
|
|
309
|
+
fail("--model must be stub or anthropic");
|
|
310
|
+
let adapter;
|
|
311
|
+
try {
|
|
312
|
+
adapter = providerName === "stub" ? new StubAdapter() : new AnthropicAdapter(node.config.runtime.model.model ?? "");
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
node.close();
|
|
316
|
+
fail(e.message);
|
|
317
|
+
}
|
|
318
|
+
const runner = new Runner(node, catalog, adapter, { transcriptsDir: str(flags, "transcripts"), log });
|
|
319
|
+
const watchers = new Watchers(node, catalog);
|
|
320
|
+
let running = null;
|
|
321
|
+
if (!flags["no-http"]) {
|
|
322
|
+
const { createHttpServer } = await import("./http/server.js");
|
|
323
|
+
running = await createHttpServer({ node, catalog, version, log }).listen(str(flags, "listen") ?? node.config.runtime.http.listen);
|
|
324
|
+
}
|
|
325
|
+
log(`foga ${version}: running cell ${node.config.name} (${node.body.address}) with model ${adapter.provider}/${adapter.model}${running ? ` at ${running.url}` : ", no HTTP"}; ${node.config.runtime.model.dailyCalls} model calls a day, ${node.config.runtime.model.maxActsPerTick} act(s) and ${node.config.runtime.model.maxToolCallsPerTick} tool call(s) a tick`);
|
|
326
|
+
const interval = Number(str(flags, "interval") ?? node.config.runtime.pollSeconds) * 1000;
|
|
327
|
+
const ticks = flags.once ? 1 : str(flags, "ticks") ? Number(str(flags, "ticks")) : Infinity;
|
|
328
|
+
let stop = false;
|
|
329
|
+
const shutdown = () => {
|
|
330
|
+
stop = true;
|
|
331
|
+
};
|
|
332
|
+
process.on("SIGINT", shutdown);
|
|
333
|
+
process.on("SIGTERM", shutdown);
|
|
334
|
+
let done = 0;
|
|
335
|
+
try {
|
|
336
|
+
while (!stop && done < ticks) {
|
|
337
|
+
try {
|
|
338
|
+
const w = await watchers.tick();
|
|
339
|
+
log(`${w.at} block ${w.block}: anchor ${w.anchor}, heartbeat ${w.heartbeat.sent ? "sent" : "not sent"} (${w.heartbeat.reason}); ${w.discovered.length} new tender(s); ${w.settled.length} settled, ${w.withdrawn.length} withdrawn; safety ${w.safety.verdict}${w.safety.changed ? " (changed)" : ""}`);
|
|
340
|
+
}
|
|
341
|
+
catch (e) {
|
|
342
|
+
log(`watch: ${e.message}`);
|
|
343
|
+
}
|
|
344
|
+
try {
|
|
345
|
+
const r = await runner.tick();
|
|
346
|
+
if (r.skipped)
|
|
347
|
+
log(`runner: ${r.skipped}`);
|
|
348
|
+
}
|
|
349
|
+
catch (e) {
|
|
350
|
+
log(`runner: ${e.message}`);
|
|
351
|
+
}
|
|
352
|
+
done++;
|
|
353
|
+
if (stop || done >= ticks)
|
|
354
|
+
break;
|
|
355
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
if (running)
|
|
360
|
+
await running.close();
|
|
361
|
+
node.close();
|
|
362
|
+
}
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (cmd !== "tick" && cmd !== "watch")
|
|
366
|
+
fail(`unknown node command ${cmd}; tick, watch, mcp, serve, run or key`);
|
|
367
|
+
// a node environment: the operator key alone, nothing that derives another role
|
|
368
|
+
const operator = keysFor("operator", true);
|
|
369
|
+
const node = await Node.open({ configPath, rpc: str(flags, "rpc"), keys: { operator }, log });
|
|
370
|
+
const watchers = new Watchers(node, defaultCatalog());
|
|
371
|
+
try {
|
|
372
|
+
if (cmd === "tick") {
|
|
373
|
+
print(await watchers.tick());
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
const interval = Number(str(flags, "interval") ?? node.config.runtime.pollSeconds) * 1000;
|
|
377
|
+
let stop = false;
|
|
378
|
+
process.on("SIGINT", () => (stop = true));
|
|
379
|
+
process.on("SIGTERM", () => (stop = true));
|
|
380
|
+
console.error(`watching cell ${node.config.name} (${node.body.address}) every ${interval / 1000} s; Ctrl-C to stop`);
|
|
381
|
+
while (!stop) {
|
|
382
|
+
try {
|
|
383
|
+
const r = await watchers.tick();
|
|
384
|
+
console.error(`${r.at} block ${r.block}: anchor ${r.anchor}, heartbeat ${r.heartbeat.sent ? "sent" : "not sent"} (${r.heartbeat.reason}); ${r.discovered.length} new tender(s); ${r.settled.length} settled, ${r.withdrawn.length} withdrawn; safety ${r.safety.verdict}${r.safety.changed ? " (changed)" : ""}`);
|
|
385
|
+
}
|
|
386
|
+
catch (e) {
|
|
387
|
+
console.error(`tick failed: ${e.message}`);
|
|
388
|
+
}
|
|
389
|
+
await new Promise((r) => setTimeout(r, interval));
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
finally {
|
|
393
|
+
node.close();
|
|
394
|
+
}
|
|
395
|
+
}
|