@finchip.ai/foga 0.6.0-alpha.5 → 0.6.0-alpha.7

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.
@@ -0,0 +1,129 @@
1
+ import { execFileSync, spawn } from "node:child_process";
2
+ import { closeSync, mkdirSync, openSync, readFileSync } from "node:fs";
3
+ import { createConnection } from "node:net";
4
+ import { dirname, join } from "node:path";
5
+ /**
6
+ * A local chain for the demos (`foga demo`, `foga quickstart`): finding anvil, starting it with its output in a log
7
+ * file, waiting for its RPC, and saying what is wrong in the words a first run needs — the binary missing (with the
8
+ * install path per platform), the port taken, the glibc too old for the dynamic build.
9
+ */
10
+ /** The anvil binary this machine offers: FOGA_FOUNDRY_DIR/anvil (scripts/install-foundry.sh) or anvil on PATH. */
11
+ export function anvilBinary() {
12
+ const dir = process.env.FOGA_FOUNDRY_DIR;
13
+ return { path: dir ? join(dir, "anvil") : "anvil", fromDir: dir ?? null };
14
+ }
15
+ /** anvil's version line, or null when the binary cannot be run from here. */
16
+ export function anvilVersion(path) {
17
+ try {
18
+ return execFileSync(path, ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n")[0];
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ export const ANVIL_INSTALL_HINT = [
25
+ "anvil (Foundry) is not installed, or not where FOGA_FOUNDRY_DIR says.",
26
+ " Linux, macOS: curl -L https://foundry.sh | bash && foundryup (an older glibc: scripts/install-foundry.sh, then export FOGA_FOUNDRY_DIR=~/.foga/foundry)",
27
+ " Windows: download the win32_amd64 zip of a release from https://github.com/foundry-rs/foundry/releases, unzip it, and put the folder",
28
+ " holding anvil.exe on PATH (or set FOGA_FOUNDRY_DIR to that folder); then open a new terminal",
29
+ " any platform: docker compose (docker/) runs the toolchain with anvil inside",
30
+ ].join("\n");
31
+ /** True when something accepts TCP connections on 127.0.0.1:port. */
32
+ export function portInUse(port) {
33
+ return new Promise((resolve) => {
34
+ const socket = createConnection({ host: "127.0.0.1", port });
35
+ const done = (v) => {
36
+ socket.destroy();
37
+ resolve(v);
38
+ };
39
+ socket.setTimeout(1000, () => done(false));
40
+ socket.once("connect", () => done(true));
41
+ socket.once("error", () => done(false));
42
+ });
43
+ }
44
+ export async function rpcAnswers(rpc) {
45
+ try {
46
+ const res = await fetch(rpc, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "eth_chainId", params: [] }), signal: AbortSignal.timeout(2000) });
47
+ return res.ok;
48
+ }
49
+ catch {
50
+ return false;
51
+ }
52
+ }
53
+ export async function waitForRpc(rpc, timeoutMs, giveUp) {
54
+ const start = Date.now();
55
+ while (Date.now() - start < timeoutMs) {
56
+ if (await rpcAnswers(rpc))
57
+ return;
58
+ if (giveUp?.())
59
+ break;
60
+ await new Promise((r) => setTimeout(r, 250));
61
+ }
62
+ throw new Error(`RPC at ${rpc} did not come up within ${timeoutMs} ms`);
63
+ }
64
+ /** Why a port cannot be used for a new anvil: another RPC already answers there, or something else holds it. */
65
+ export async function portProblem(port) {
66
+ if (!(await portInUse(port)))
67
+ return null;
68
+ const rpc = `http://127.0.0.1:${port}`;
69
+ return (await rpcAnswers(rpc))
70
+ ? `${rpc} already answers JSON-RPC (another anvil or node is running). Stop it, or pass --port <n> to use another port.`
71
+ : `port ${port} is in use by something that does not speak JSON-RPC. Free it, or pass --port <n>.`;
72
+ }
73
+ /**
74
+ * Start anvil and wait until its RPC answers. Rejects with a message fit to print when the binary is missing, when
75
+ * anvil stops before serving (its output quoted, the glibc case named) or when it does not come up in time.
76
+ */
77
+ export async function launchAnvil(o) {
78
+ const { path: bin, fromDir } = anvilBinary();
79
+ const rpc = `http://127.0.0.1:${o.port}`;
80
+ mkdirSync(dirname(o.logPath), { recursive: true });
81
+ const logFd = openSync(o.logPath, "w");
82
+ const child = spawn(bin, ["--port", String(o.port), "--silent", ...(o.args ?? [])], { stdio: ["ignore", logFd, logFd], detached: Boolean(o.detached) });
83
+ closeSync(logFd);
84
+ let exited = null;
85
+ child.on("exit", (code, signal) => {
86
+ exited = code !== null ? `exit code ${code}` : `signal ${signal}`;
87
+ });
88
+ const spawnError = new Promise((_, reject) => {
89
+ child.once("error", (e) => {
90
+ reject(new Error(e.code === "ENOENT" ? (fromDir ? `${bin} not found (FOGA_FOUNDRY_DIR=${fromDir}). Run scripts/install-foundry.sh ${fromDir} first, or unset FOGA_FOUNDRY_DIR to use anvil on PATH.\n${ANVIL_INSTALL_HINT}` : `anvil not found on PATH.\n${ANVIL_INSTALL_HINT}`) : `could not start anvil: ${e.message}`));
91
+ });
92
+ });
93
+ const timeoutMs = o.timeoutMs ?? 20_000;
94
+ try {
95
+ await Promise.race([waitForRpc(rpc, timeoutMs, () => exited !== null), spawnError]);
96
+ }
97
+ catch (e) {
98
+ if (!child.killed && child.exitCode === null)
99
+ child.kill("SIGTERM");
100
+ if (e.message?.startsWith("RPC at ")) {
101
+ let output = "";
102
+ try {
103
+ output = readFileSync(o.logPath, "utf8").slice(-8000);
104
+ }
105
+ catch {
106
+ /* no log */
107
+ }
108
+ const why = exited ? `anvil stopped (${exited}) before serving ${rpc}` : `anvil did not serve ${rpc} within ${timeoutMs / 1000} s`;
109
+ const glibc = /GLIBC_[0-9.]+' not found/.test(output)
110
+ ? "\nThis anvil is the glibc build and this machine's glibc is older than it needs (GLIBC >= 2.35: Ubuntu 22.04 or newer). Install the static build next to it: `scripts/install-foundry.sh` then `export FOGA_FOUNDRY_DIR=~/.foga/foundry` (or put that directory first on PATH)."
111
+ : "";
112
+ throw new Error(`${why}.${output.trim() ? `\n--- anvil output ---\n${output.trim()}\n--------------------` : ""}${glibc}\nRun \`${bin} --port ${o.port}\` by hand to see its output; \`${bin} --version\` shows the installed version (CI uses Foundry v1.7.1).`);
113
+ }
114
+ throw e;
115
+ }
116
+ return child;
117
+ }
118
+ /** Stop a child and wait for it to be gone (SIGTERM, then SIGKILL after a grace period). */
119
+ export async function stopChild(child, graceMs = 5000) {
120
+ if (child.exitCode !== null || child.signalCode !== null)
121
+ return;
122
+ const gone = new Promise((r) => child.once("exit", () => r()));
123
+ child.kill("SIGTERM");
124
+ await Promise.race([gone, new Promise((r) => setTimeout(r, graceMs))]);
125
+ if (child.exitCode === null && child.signalCode === null) {
126
+ child.kill("SIGKILL");
127
+ await Promise.race([gone, new Promise((r) => setTimeout(r, 2000))]);
128
+ }
129
+ }
@@ -0,0 +1,71 @@
1
+ import { join } from "node:path";
2
+ import { Wallet, getAddress, parseEther } from "ethers";
3
+ import { dataUriJcs, keccakJcs } from "../core/canonical.js";
4
+ import { accountSubject, fogaAttestedSchemeDescriptor } from "../core/kya.js";
5
+ import { loadCellConfig, newCellConfig, saveCellConfig } from "../node/config.js";
6
+ import { cellInit } from "../node/init.js";
7
+ import { Node } from "../node/node.js";
8
+ import { defaultCatalog, OPERATOR_CALLER } from "../tools/index.js";
9
+ import { repoRoot } from "../util/fs.js";
10
+ export async function birthCell(o) {
11
+ const k = o.kernel;
12
+ const log = o.log ?? (() => { });
13
+ const root = repoRoot();
14
+ const W = k.address("W");
15
+ const owner = o.keys ? new Wallet(o.keys.owner) : Wallet.createRandom();
16
+ const operator = o.keys ? new Wallet(o.keys.operator) : Wallet.createRandom();
17
+ const guardian = o.keys ? new Wallet(o.keys.guardian) : Wallet.createRandom();
18
+ log(`cell ${o.name}: owner ${owner.address}, operator ${operator.address}, guardian ${guardian.address}${o.keys ? " (the wallet's keys)" : " (fresh keys)"}`);
19
+ // --- funding by W, the standing scheme registered by W
20
+ const price = (await k.world.provider.getFeeData()).gasPrice ?? 1000000000n;
21
+ const gas = (units) => units * price * 2n;
22
+ await k.transfer("fund.owner", "W", owner.address, parseEther("0.05") + gas(12000000n));
23
+ await k.transfer("fund.operator", "W", operator.address, gas(20000000n) + parseEther("0.003"));
24
+ await k.transfer("fund.guardian", "W", guardian.address, gas(1000000n));
25
+ const att = await k.call("scheme", "W", k.kya.schemeRegistryAddress, k.kya.schemes().interface, "registerScheme", [k.kya.attestedSchemeInput(fogaAttestedSchemeDescriptor())]);
26
+ const schemeId = k.kya.schemeIdFromReceipt(att.receipt);
27
+ const bornAtBlock = await k.blockNumber();
28
+ // --- the configuration and the birth (the owner's act, in this process, as `foga cell init` would)
29
+ const cfg = newCellConfig({ name: o.name, world: o.worldPath, store: o.storePath, owner: owner.address, operator: operator.address, guardian: guardian.address, rpc: o.rpc, skillsDir: join(root, "scenarios", "skills"), inputDir: join(root, "scenarios", "fixtures"), description: o.description });
30
+ cfg.identity.standing = { schemeId, issuers: [W] };
31
+ o.configure?.(cfg);
32
+ saveCellConfig(o.configPath, cfg);
33
+ const birthNode = await Node.open({ configPath: o.configPath, rpc: o.rpc, keys: { owner: owner.privateKey }, allowUnborn: true, log: () => { } });
34
+ const born = await cellInit(birthNode, { log: () => { } });
35
+ birthNode.close();
36
+ const account = getAddress(born.account);
37
+ log(`cell ${o.name}: born as ${account} (agent ${born.agentId}) in ${born.operations.length} operations`);
38
+ // --- standing from the world operator, the two skills minted by the operator
39
+ const claim = { scheme: "S_att", level: 2, subject: `eip155:${k.chainId}:${account.toLowerCase()}` };
40
+ const expiresAt = (await k.blockTimestamp()) + 365 * 86400;
41
+ await k.call("attest", "W", k.kya.registryAddress, k.kya.registry().interface, "attest", [accountSubject(k.chainId, account), schemeId, 2, keccakJcs(claim), expiresAt, dataUriJcs(claim), keccakJcs(claim)]);
42
+ const catalog = defaultCatalog();
43
+ const skillNode = await Node.open({ configPath: o.configPath, rpc: o.rpc, keys: { operator: operator.privateKey }, log: () => { } });
44
+ try {
45
+ for (const skill of ["foga-probe", "finch-extract-verify"]) {
46
+ const r = await catalog.invoke(skillNode, "skill_mint", { skill }, OPERATOR_CALLER, { mode: "act" });
47
+ if (!r.ok)
48
+ throw new Error(`skill_mint ${skill}: ${r.gate}: ${r.reason}`);
49
+ }
50
+ }
51
+ finally {
52
+ skillNode.close();
53
+ }
54
+ const held = loadCellConfig(o.configPath).config.skills.held;
55
+ log(`cell ${o.name}: standing level 2 attested by ${W}; skills minted (${Object.keys(held).join(", ")})`);
56
+ return {
57
+ owner,
58
+ operator,
59
+ guardian,
60
+ account,
61
+ agentId: born.agentId,
62
+ specId: born.specId ?? null,
63
+ schemeId,
64
+ bornAtBlock,
65
+ birthOperations: born.operations.length,
66
+ held,
67
+ packages: { "foga-probe": held["foga-probe"].packageHash, "finch-extract-verify": held["finch-extract-verify"].packageHash },
68
+ configPath: o.configPath,
69
+ storePath: o.storePath,
70
+ };
71
+ }