@finchip.ai/foga 0.6.1 → 0.7.0
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/README.md +17 -6
- package/deployments/sepolia.example.json +638 -0
- package/dist/cli-cell.js +17 -0
- package/dist/cli.js +50 -0
- package/dist/core/chains.js +82 -0
- package/dist/demo/anvil.js +11 -0
- package/dist/demo/quickstart.js +33 -14
- package/dist/mcp/server.js +14 -8
- package/dist/node/card.js +67 -0
- package/dist/node/join.js +343 -0
- package/docs/design-memo.md +2 -0
- package/docs/join.md +86 -0
- package/docs/mcp.md +15 -0
- package/docs/quickstart.md +3 -1
- package/package.json +3 -2
- package/server.json +2 -2
- package/worlds/arbitrum-sepolia.json +48 -0
- package/worlds/base-sepolia.json +48 -0
- package/worlds/optimism-sepolia.json +48 -0
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { JsonRpcProvider, formatEther, parseEther } from "ethers";
|
|
6
|
+
import { chainById, chainByName, packagedWorld, resolveRpc, CHAINS } from "../core/chains.js";
|
|
7
|
+
import { newApiKey } from "../http/access.js";
|
|
8
|
+
import { adviseCell, formatAdvice } from "./advise.js";
|
|
9
|
+
import { cellCard, formatCard } from "./card.js";
|
|
10
|
+
import { loadCellConfig, newCellConfig, saveCellConfig } from "./config.js";
|
|
11
|
+
import { cellInit } from "./init.js";
|
|
12
|
+
import { Node } from "./node.js";
|
|
13
|
+
import { ensureWallet, ensuredKey, walletDir } from "./wallet.js";
|
|
14
|
+
import { defaultCatalog, OPERATOR_CALLER } from "../tools/index.js";
|
|
15
|
+
import { clientConfigs, clientLauncher } from "../demo/quickstart.js";
|
|
16
|
+
import { cleanEnv, ownCli, waitForHttp } from "../demo/loop-d.js";
|
|
17
|
+
import { nextFreePort, portInUse } from "../demo/anvil.js";
|
|
18
|
+
import { repoRoot } from "../util/fs.js";
|
|
19
|
+
import { PACKAGE_NAME, version } from "../util/version.js";
|
|
20
|
+
export const JOIN_MARKER = "join.json";
|
|
21
|
+
export const DEFAULT_SKILLS = ["foga-probe", "finch-extract-verify"];
|
|
22
|
+
const PUBLIC_MIN = parseEther("0.03");
|
|
23
|
+
const PUBLIC_COMFORTABLE = parseEther("0.05");
|
|
24
|
+
export function fogaHome() {
|
|
25
|
+
return process.env.FOGA_HOME ?? join(homedir(), ".foga");
|
|
26
|
+
}
|
|
27
|
+
/** What a person is told while the identity waits for ether. */
|
|
28
|
+
export function fundingMessage(p) {
|
|
29
|
+
const lines = [];
|
|
30
|
+
const need = p.minWei - p.balanceWei;
|
|
31
|
+
lines.push(`fund ${p.address} on ${p.network} (chain ${p.chainId}) with at least ${formatEther(need)} ETH more (it holds ${formatEther(p.balanceWei)}, needs ${formatEther(p.minWei)}; ${formatEther(PUBLIC_COMFORTABLE)} is comfortable).`);
|
|
32
|
+
if (p.chain?.testnet && p.chain.faucets.length)
|
|
33
|
+
lines.push(` test ether is free: ${p.chain.faucets.join(" · ")}`);
|
|
34
|
+
if (p.chain && !p.chain.testnet)
|
|
35
|
+
lines.push(" this is a mainnet: the ether is real, and so is everything the cell will commit to.");
|
|
36
|
+
if (p.chain?.explorer)
|
|
37
|
+
lines.push(` watch it arrive: ${p.chain.explorer}/address/${p.address}`);
|
|
38
|
+
return lines.join("\n");
|
|
39
|
+
}
|
|
40
|
+
async function balanceOf(rpc, address) {
|
|
41
|
+
const provider = new JsonRpcProvider(rpc, undefined, { staticNetwork: true });
|
|
42
|
+
try {
|
|
43
|
+
return await provider.getBalance(address);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
const why = e.shortMessage ?? e.message;
|
|
47
|
+
throw new Error(`cannot reach the RPC ${rpc} (${why}); pass --rpc <url> for one this machine can reach`);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
provider.destroy();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function runJoin(o = {}) {
|
|
54
|
+
const log = o.log ?? ((line) => console.error(line));
|
|
55
|
+
const out = o.out ?? ((text) => console.log(text));
|
|
56
|
+
const root = repoRoot();
|
|
57
|
+
// --- the chain and its world: by name from the package, or a deployed address book given by path
|
|
58
|
+
let chain;
|
|
59
|
+
let worldSource;
|
|
60
|
+
if (o.world) {
|
|
61
|
+
worldSource = resolve(o.world);
|
|
62
|
+
if (!existsSync(worldSource))
|
|
63
|
+
throw new Error(`world file ${worldSource} not found`);
|
|
64
|
+
const file = JSON.parse(readFileSync(worldSource, "utf8"));
|
|
65
|
+
if (file.schema !== "foga.world/1")
|
|
66
|
+
throw new Error(`${worldSource} is not a world file (foga.world/1)`);
|
|
67
|
+
chain = chainById(Number(file.chainId)) ?? (file.network ? chainByName(file.network) : null);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
const name = (o.chain ?? "sepolia").toLowerCase();
|
|
71
|
+
chain = chainByName(name);
|
|
72
|
+
if (!chain)
|
|
73
|
+
throw new Error(`unknown chain "${name}"; known: ${Object.keys(CHAINS).join(", ")}. A chain not listed: deploy a world on it (foga world deploy --world worlds/<chain>.json --rpc <url>) and pass its address book with --world.`);
|
|
74
|
+
const packaged = packagedWorld(chain);
|
|
75
|
+
if (!packaged)
|
|
76
|
+
throw new Error(`no world ships for ${chain.name}: deploy one (foga world deploy --world worlds/${chain.name}.json --rpc <url>) and pass its address book with --world <file>`);
|
|
77
|
+
worldSource = packaged;
|
|
78
|
+
}
|
|
79
|
+
const worldFile = JSON.parse(readFileSync(worldSource, "utf8"));
|
|
80
|
+
const network = worldFile.network;
|
|
81
|
+
const local = network === "anvil";
|
|
82
|
+
const rpcResolved = resolveRpc(chain, o.rpc);
|
|
83
|
+
const rpc = rpcResolved.url;
|
|
84
|
+
if (rpcResolved.source === "public")
|
|
85
|
+
log(`join: no RPC given; using the public endpoint ${rpc} (shared and rate-limited — a dedicated one is advised: --rpc <url> or ${chain?.rpcEnv ?? "FOGA_RPC_URL"})`);
|
|
86
|
+
// --- the wallet: one key, the identity, made once and reused on every chain
|
|
87
|
+
const walletPath = resolve(o.wallet ?? walletDir());
|
|
88
|
+
const wallet = ensureWallet(walletPath, { name: "default" });
|
|
89
|
+
if (wallet.ephemeral)
|
|
90
|
+
throw new Error(`a cell that lives needs a wallet on disk, and ${walletPath} could not be written (${wallet.reason}); pass --wallet <a writable directory>`);
|
|
91
|
+
const singleKey = wallet.wallet.roles.length === 1 && wallet.wallet.roles[0] === "signer";
|
|
92
|
+
const identity = singleKey ? wallet.wallet.addresses.signer : wallet.wallet.addresses.owner;
|
|
93
|
+
log(`join: wallet ${wallet.created ? "made" : "using"} ${wallet.wallet.dir} — ${singleKey ? `identity ${identity} (one key, every role)` : `owner ${wallet.wallet.addresses.owner}, operator ${wallet.wallet.addresses.operator}, guardian ${wallet.wallet.addresses.guardian}`}; the same address on every EVM chain`);
|
|
94
|
+
// --- the cell's home: never cleared; a second join here resumes
|
|
95
|
+
const name = o.name ?? `cell-${identity.slice(2, 6).toLowerCase()}`;
|
|
96
|
+
const dir = resolve(o.dir ?? join(fogaHome(), "cells", network, name));
|
|
97
|
+
mkdirSync(dir, { recursive: true });
|
|
98
|
+
const configPath = join(dir, "cell.config.json");
|
|
99
|
+
const worldPath = join(dir, "world.json");
|
|
100
|
+
const keysDir = join(dir, "keys");
|
|
101
|
+
mkdirSync(keysDir, { recursive: true, mode: 0o700 });
|
|
102
|
+
if (!existsSync(worldPath))
|
|
103
|
+
writeFileSync(worldPath, readFileSync(worldSource));
|
|
104
|
+
// the skill packages and the fixtures travel with the cell: their hashes are what the chain holds, their files
|
|
105
|
+
// must stay where the cell is, not where this version of the package happens to be installed
|
|
106
|
+
for (const sub of ["skills", "fixtures"]) {
|
|
107
|
+
const dst = join(dir, sub);
|
|
108
|
+
if (!existsSync(dst))
|
|
109
|
+
cpSync(join(root, "scenarios", sub), dst, { recursive: true });
|
|
110
|
+
}
|
|
111
|
+
const resumed = existsSync(configPath);
|
|
112
|
+
if (resumed)
|
|
113
|
+
log(`join: resuming the cell in ${dir}`);
|
|
114
|
+
else {
|
|
115
|
+
const cfg = newCellConfig({
|
|
116
|
+
name,
|
|
117
|
+
world: "world.json",
|
|
118
|
+
store: "cell.ops.sqlite",
|
|
119
|
+
owner: wallet.wallet.addresses.owner ?? identity,
|
|
120
|
+
operator: wallet.wallet.addresses.operator ?? identity,
|
|
121
|
+
guardian: wallet.wallet.addresses.guardian ?? identity,
|
|
122
|
+
rpc: rpcResolved.source === "public" ? undefined : rpc,
|
|
123
|
+
skillsDir: "skills",
|
|
124
|
+
inputDir: "fixtures",
|
|
125
|
+
description: `Cell ${name}: born by foga join on ${network}`,
|
|
126
|
+
});
|
|
127
|
+
// a day with a birth, two mints and a publish in it spends more gas than the default daily cap allows on a chain
|
|
128
|
+
// that prices gas; the cap is raised to what the quickstart uses, and the operator's gas floor (a quarter of it)
|
|
129
|
+
// stays under the funding gate below
|
|
130
|
+
cfg.policy.budget.dailyGasWei = parseEther("0.02").toString();
|
|
131
|
+
cfg.runtime.http.listen = o.listen ?? "127.0.0.1:8787";
|
|
132
|
+
if (o.publicUrl)
|
|
133
|
+
cfg.runtime.http.publicUrl = o.publicUrl.replace(/\/+$/, "");
|
|
134
|
+
saveCellConfig(configPath, cfg);
|
|
135
|
+
log(`join: cell ${name} configured in ${dir} (world ${worldFile.worldId}, ${Object.keys(worldFile.contracts).length} contracts)`);
|
|
136
|
+
}
|
|
137
|
+
if (o.publicUrl) {
|
|
138
|
+
const loaded = loadCellConfig(configPath);
|
|
139
|
+
if (loaded.config.runtime.http.publicUrl !== o.publicUrl.replace(/\/+$/, "")) {
|
|
140
|
+
loaded.config.runtime.http.publicUrl = o.publicUrl.replace(/\/+$/, "");
|
|
141
|
+
saveCellConfig(configPath, loaded.config);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// --- the funding gate: nothing is sent until the identity can pay for its birth
|
|
145
|
+
const minWei = o.minWei ?? (local ? 0n : PUBLIC_MIN);
|
|
146
|
+
let balance = await balanceOf(rpc, identity);
|
|
147
|
+
if (balance < minWei) {
|
|
148
|
+
const msg = fundingMessage({ address: identity, chain, network, chainId: worldFile.chainId, balanceWei: balance, minWei });
|
|
149
|
+
if (o.wait === false)
|
|
150
|
+
throw new Error(`the identity is not funded yet.\n${msg}`);
|
|
151
|
+
log(`join: waiting for ether — ${msg}`);
|
|
152
|
+
const started = Date.now();
|
|
153
|
+
let lastSaid = started;
|
|
154
|
+
while (balance < minWei) {
|
|
155
|
+
if (Date.now() - started > 60 * 60_000)
|
|
156
|
+
throw new Error(`waited an hour for ${identity} to be funded; run the same command again once it is.\n${msg}`);
|
|
157
|
+
await new Promise((r) => setTimeout(r, local ? 1000 : 15_000));
|
|
158
|
+
balance = await balanceOf(rpc, identity);
|
|
159
|
+
if (balance < minWei && Date.now() - lastSaid > 120_000) {
|
|
160
|
+
lastSaid = Date.now();
|
|
161
|
+
log(`join: still waiting — ${identity} holds ${formatEther(balance)} ETH, needs ${formatEther(minWei)}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
log(`join: funded — ${identity} holds ${formatEther(balance)} ETH`);
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
log(`join: ${identity} holds ${formatEther(balance)} ETH`);
|
|
168
|
+
// --- the birth: the owner's act; resumable, never repeated
|
|
169
|
+
let born = loadCellConfig(configPath).config.identity.account ?? null;
|
|
170
|
+
let agentId = loadCellConfig(configPath).config.identity.agentId ?? null;
|
|
171
|
+
if (!born) {
|
|
172
|
+
const node = await Node.open({ configPath, rpc, keys: { owner: ensuredKey(wallet, "owner") }, allowUnborn: true, log: () => { } });
|
|
173
|
+
try {
|
|
174
|
+
const r = await cellInit(node, { capitalWei: o.capitalWei, log: (line) => log(`join: ${line}`) });
|
|
175
|
+
born = r.account;
|
|
176
|
+
agentId = r.agentId;
|
|
177
|
+
log(`join: cell ${name} born as ${born} (ERC-8004 agent ${agentId}) in ${r.operations.length} operations`);
|
|
178
|
+
}
|
|
179
|
+
finally {
|
|
180
|
+
node.close();
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else
|
|
184
|
+
log(`join: cell ${name} is ${born}${agentId ? ` (agent ${agentId})` : ""}, born before`);
|
|
185
|
+
// --- the skills: minted by the operator and offered; the documents committed on chain
|
|
186
|
+
const catalog = defaultCatalog();
|
|
187
|
+
{
|
|
188
|
+
const want = o.skills ?? DEFAULT_SKILLS;
|
|
189
|
+
const held = loadCellConfig(configPath).config.skills.held;
|
|
190
|
+
const missing = want.filter((s) => !held[s]);
|
|
191
|
+
if (missing.length) {
|
|
192
|
+
const node = await Node.open({ configPath, rpc, keys: { operator: ensuredKey(wallet, "operator") }, log: () => { } });
|
|
193
|
+
try {
|
|
194
|
+
for (const skill of missing) {
|
|
195
|
+
const r = await catalog.invoke(node, "skill_mint", { skill }, OPERATOR_CALLER, { mode: "act" });
|
|
196
|
+
if (!r.ok)
|
|
197
|
+
throw new Error(`skill_mint ${skill}: ${r.gate}: ${r.reason}`);
|
|
198
|
+
log(`join: skill ${skill} minted`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
finally {
|
|
202
|
+
node.close();
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const loaded = loadCellConfig(configPath);
|
|
206
|
+
const offered = Object.keys(loaded.config.skills.held).filter((s) => want.includes(s));
|
|
207
|
+
if (offered.some((s) => !loaded.config.skills.offered.includes(s))) {
|
|
208
|
+
loaded.config.skills.offered = [...new Set([...loaded.config.skills.offered, ...offered])];
|
|
209
|
+
saveCellConfig(configPath, loaded.config);
|
|
210
|
+
}
|
|
211
|
+
log(`join: skills ${offered.join(", ")} held and offered`);
|
|
212
|
+
}
|
|
213
|
+
// --- an act-level API key for the HTTP entrance, kept in the cell's directory
|
|
214
|
+
const agentKeyPath = join(keysDir, "agent.api-key");
|
|
215
|
+
let apiKey;
|
|
216
|
+
{
|
|
217
|
+
const loaded = loadCellConfig(configPath);
|
|
218
|
+
const have = loaded.config.access.keys.find((k) => k.id === "agent");
|
|
219
|
+
if (have && existsSync(agentKeyPath))
|
|
220
|
+
apiKey = readFileSync(agentKeyPath, "utf8").trim();
|
|
221
|
+
else {
|
|
222
|
+
const api = newApiKey("agent");
|
|
223
|
+
loaded.config.access.keys = loaded.config.access.keys.filter((k) => k.id !== "agent");
|
|
224
|
+
loaded.config.access.keys.push({ id: "agent", sha256: api.sha256, level: "act", visibility: "caller", note: "the join's act-level key for the HTTP entrance" });
|
|
225
|
+
saveCellConfig(configPath, loaded.config);
|
|
226
|
+
writeFileSync(agentKeyPath, api.key + "\n", { mode: 0o600 });
|
|
227
|
+
apiKey = api.key;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
{
|
|
231
|
+
const node = await Node.open({ configPath, rpc, keys: { operator: ensuredKey(wallet, "operator") }, log: () => { } });
|
|
232
|
+
try {
|
|
233
|
+
const r = await catalog.invoke(node, "docs_publish", {}, OPERATOR_CALLER, { mode: "act" });
|
|
234
|
+
if (!r.ok)
|
|
235
|
+
throw new Error(`docs_publish: ${r.gate}: ${r.reason}`);
|
|
236
|
+
const d = r.output;
|
|
237
|
+
// the same documents make the same job: a resume is answered from the store, and the chain is not asked again
|
|
238
|
+
const replayed = node.store.decisions({ limit: 1 })[0]?.gate === "idempotent";
|
|
239
|
+
log(`join: documents v${d.version} in ${d.dir}; ${replayed ? "the chain holds them (nothing sent)" : d.committed.length ? `committed on chain (${d.committed.join(", ")})` : "the chain already holds them"}`);
|
|
240
|
+
}
|
|
241
|
+
finally {
|
|
242
|
+
node.close();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// --- the node: served (every entrance, the watchers, no model) or run (with a model: it takes its tenders)
|
|
246
|
+
let listen = o.listen === undefined ? "127.0.0.1:8787" : o.listen;
|
|
247
|
+
if (listen) {
|
|
248
|
+
const lp = Number(listen.slice(listen.lastIndexOf(":") + 1));
|
|
249
|
+
if (await portInUse(lp)) {
|
|
250
|
+
if (o.listen !== undefined)
|
|
251
|
+
throw new Error(`port ${lp} is in use; pass --listen <host:port> (or --no-serve)`);
|
|
252
|
+
const next = await nextFreePort(lp + 1);
|
|
253
|
+
if (next === null)
|
|
254
|
+
throw new Error(`ports ${lp}-${lp + 20} are all in use; pass --listen <host:port>`);
|
|
255
|
+
log(`join: port ${lp} is in use; the node listens on ${next} instead`);
|
|
256
|
+
listen = `127.0.0.1:${next}`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const operatorKeyPath = wallet.wallet.files.operator ?? wallet.wallet.files.signer;
|
|
260
|
+
const cli = ownCli();
|
|
261
|
+
let child = null;
|
|
262
|
+
let nodeUrl = null;
|
|
263
|
+
let endedWith = () => { };
|
|
264
|
+
const ended = new Promise((r) => (endedWith = r));
|
|
265
|
+
let stopping = false;
|
|
266
|
+
if (listen) {
|
|
267
|
+
const i = listen.lastIndexOf(":");
|
|
268
|
+
const host = listen.slice(0, i) || "127.0.0.1";
|
|
269
|
+
nodeUrl = `http://${host === "0.0.0.0" || host === "::" ? "127.0.0.1" : host}:${listen.slice(i + 1)}`;
|
|
270
|
+
const args = o.run ? ["node", "run", "--config", configPath, "--rpc", rpc, "--model", o.run, "--listen", listen] : ["node", "serve", "--config", configPath, "--rpc", rpc, "--listen", listen];
|
|
271
|
+
child = spawn(cli.command, [...cli.args, ...args], { env: cleanEnv({ FOGA_OPERATOR_KEY_FILE: operatorKeyPath }), stdio: ["ignore", "ignore", "pipe"] });
|
|
272
|
+
const idleTick = /: anchor ACTIVE, heartbeat not sent \([^)]*\); 0 new tender\(s\); 0 settled, 0 withdrawn; safety Satisfied$/;
|
|
273
|
+
child.stderr.on("data", (d) => {
|
|
274
|
+
for (const line of String(d).split("\n"))
|
|
275
|
+
if (line.trim() && !idleTick.test(line.trim()))
|
|
276
|
+
log(` node| ${line}`);
|
|
277
|
+
});
|
|
278
|
+
child.on("exit", (code, signal) => {
|
|
279
|
+
if (!stopping)
|
|
280
|
+
endedWith(`the node stopped (${code !== null ? `exit code ${code}` : `signal ${signal}`})`);
|
|
281
|
+
});
|
|
282
|
+
await waitForHttp(`${nodeUrl}/healthz`, 120);
|
|
283
|
+
const health = (await (await fetch(`${nodeUrl}/healthz`)).json());
|
|
284
|
+
if (!health.ok)
|
|
285
|
+
throw new Error("the node's /healthz is not ok");
|
|
286
|
+
}
|
|
287
|
+
else
|
|
288
|
+
endedWith("no node");
|
|
289
|
+
const stop = async () => {
|
|
290
|
+
stopping = true;
|
|
291
|
+
if (child) {
|
|
292
|
+
const { stopChild } = await import("../demo/anvil.js");
|
|
293
|
+
await stopChild(child);
|
|
294
|
+
child = null;
|
|
295
|
+
}
|
|
296
|
+
endedWith("stopped");
|
|
297
|
+
};
|
|
298
|
+
// --- the card and the configurations: what the cell is, and how anyone connects
|
|
299
|
+
const reader = await Node.open({ configPath, rpc, keys: {}, allowUnborn: true, log: () => { } });
|
|
300
|
+
let card;
|
|
301
|
+
let next = "";
|
|
302
|
+
try {
|
|
303
|
+
card = cellCard(reader, { version });
|
|
304
|
+
next = formatAdvice(adviseCell(reader), { brief: true, config: configPath });
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
reader.close();
|
|
308
|
+
}
|
|
309
|
+
const launcher = clientLauncher();
|
|
310
|
+
const configs = clientConfigs({ launcher, configPath, operatorKeyPath, nodeUrl, apiKey });
|
|
311
|
+
const mcpDir = join(dir, "mcp");
|
|
312
|
+
mkdirSync(mcpDir, { recursive: true });
|
|
313
|
+
for (const [file, text] of Object.entries(configs))
|
|
314
|
+
writeFileSync(join(mcpDir, file), text);
|
|
315
|
+
const lines = [];
|
|
316
|
+
lines.push(`foga join: cell ${name} lives on ${network}${resumed ? " (resumed)" : ""}`);
|
|
317
|
+
lines.push("");
|
|
318
|
+
lines.push(formatCard(card));
|
|
319
|
+
lines.push("");
|
|
320
|
+
lines.push(` files ${dir} — cell.config.json, cell.ops.sqlite (the store), world.json, skills/, documents/, keys/ (0600), mcp/*.json, connect.md`);
|
|
321
|
+
lines.push(` wallet ${wallet.wallet.dir} — the identity; back it up, it is the cell`);
|
|
322
|
+
if (nodeUrl)
|
|
323
|
+
lines.push(` node ${nodeUrl} (${o.run ? `running with model ${o.run}: it takes the tenders it is eligible for` : "served: every entrance and the watchers, no model"}) — /mcp, /api/<capability>, /a2a, /.well-known/foga-cell.json, /.well-known/agent-card.json, /healthz`);
|
|
324
|
+
lines.push("");
|
|
325
|
+
lines.push("Connect an MCP client over stdio (the client starts the server on this machine; with the operator key it acts for the cell):");
|
|
326
|
+
lines.push(configs["claude_desktop_config.json"].trimEnd().split("\n").map((l) => ` ${l}`).join("\n"));
|
|
327
|
+
if (nodeUrl) {
|
|
328
|
+
lines.push("");
|
|
329
|
+
lines.push(`Or over HTTP: MCP ${nodeUrl}/mcp and ${nodeUrl}/api/<capability> with Authorization: Bearer ${apiKey} (${join(mcpDir, "http-mcp.json")}); A2A at ${nodeUrl}/a2a.`);
|
|
330
|
+
lines.push(`Reachable from other machines only if ${listen.split(":")[0]} is not the loopback (--listen 0.0.0.0:<port>) and the port is open; then --public-url https://<host> puts the address in the documents and the card.`);
|
|
331
|
+
}
|
|
332
|
+
lines.push("");
|
|
333
|
+
lines.push("Work comes over the chain: a demander mints an ERC-8414 tender for an offered skill; the watchers find it; an agent at the entrance (or the model, with --run) pre-checks, runs the skill and submits; the hashlock verifier accepts a right answer; the watchers settle and withdraw.");
|
|
334
|
+
if (next)
|
|
335
|
+
lines.push("", next);
|
|
336
|
+
lines.push("");
|
|
337
|
+
lines.push(o.exit ? "Started with --exit: stopping the node." : `Ctrl-C stops the node; the cell stays. Start it again with the same command, or: ${launcher.command} ${launcher.args.join(" ")} node serve --config ${configPath} --wallet ${wallet.wallet.dir}`);
|
|
338
|
+
const summary = lines.join("\n");
|
|
339
|
+
writeFileSync(join(dir, "connect.md"), `# foga join\n\n\`\`\`\n${summary}\n\`\`\`\n`);
|
|
340
|
+
writeFileSync(join(dir, JOIN_MARKER), JSON.stringify({ schema: "foga.join/1", joinedAt: new Date().toISOString(), package: PACKAGE_NAME, version, network, chainId: worldFile.chainId, worldId: worldFile.worldId, rpc: rpcResolved.source === "public" ? null : rpc, cell: name, account: born, agentId, wallet: wallet.wallet.dir, listen, publicUrl: o.publicUrl ?? null, run: o.run ?? null }, null, 2) + "\n");
|
|
341
|
+
out(summary);
|
|
342
|
+
return { dir, configPath, worldPath, rpc, chainId: worldFile.chainId, network, account: born, agentId, resumed, nodeUrl, card, stop, ended };
|
|
343
|
+
}
|
package/docs/design-memo.md
CHANGED
|
@@ -117,6 +117,8 @@ Three checks, three vocabularies. A call to the node passes **call permission**
|
|
|
117
117
|
|
|
118
118
|
**3d-2, the cell among other agents (0.6.0-alpha.7).** Six pieces, one principle (section 10). *The wallet.* `foga wallet new` makes an agent's keys on its own machine — by default one key, its identity, which fills every role a cell needs (the separated owner/operator/guardian set is the opt-in `--cell` hardening) — in 0600 files, with no network and no chain; it never overwrites, and whatever needs the keys goes on with keys made in memory when the disk refuses (`foga quickstart` makes the default wallet on its first run and births every later cell with it; `--wallet <dir>` on every `foga cell` and `foga node` command takes the role's key from there). *Signed callers.* `Authorization: FOGA-EIP712 <envelope>` is EIP-712 typed data in the cell's own domain (name FOGA, version 1, the chain, the cell's account): a `Request` envelope bound to method, path and body hash, good once within 120 s, or a `Session` envelope, reusable within 600 s for a client with static headers; the caller is the signing address and `access.signers` says what it is granted — the cell's own operator or owner remotely, an allow-list by address or by ERC-8004 agent id (checked against the registry's owner and agent wallet), a KYA standing threshold, a default of build, read or none; an act wants a request-bound envelope unless the policy says otherwise. `/api`, `/mcp` and `/a2a` take the same header; `GET /api/capabilities` tells a caller what it is. *The documents.* `foga cell docs build | publish | status` generates from one configuration the ERC-8004 registration (`registration-v1`, with `registrations`), the ERC-8434 AID document (identity, standing, skills and endpoints facets), the ERC-8444 descriptor, the ERC-8454 unit definition, the A2A agent card, the registry's `server.json` and a manifest of content hashes; the node serves them under `/documents` and `/.well-known`; `docs_publish` (the 31st capability, the operator's act) commits only what differs from the chain — `setAgentURI`, `setDocumentURI`, `updateDescriptor`, `publishVersion`, as one keyed job — and reads every commitment back; a cell without a public URL commits `data:` URIs and is complete on chain all the same. *Preflight.* `foga node preflight` reconciles a cell against the chain and the machine before it is served — the world, the account's revision, roles and allow-list, the budget, the operator key alone and its gas, the standing, the skills' packages and their executors actually run, the documents, the public URL, the model — as rows that are advisory by default and fatal with `--strict`. *A2A.* `POST /a2a` is the 1.0 JSON-RPC binding with the card at `/.well-known/agent-card.json`: a quote for anyone (the offered skills and the terms a tender needs to meet), a tender taken for an act-level caller as one task over the catalog's jobs (pre-check, run, submit; settlement stays the watchers'), `GetTask`, `ListTasks` scoped to the caller, `CancelTask` that undoes nothing broadcast, duplicates by (caller, messageId), the specification's error codes; no road does work first and talks about payment after. *Sourcify.* `foga world verify --sourcify` submits every FOGA-deployed contract of a world with the standard JSON input of its own compiler run — `forge build` leaves the build-info beside the artifacts — matched by runtime code and identified by the path solc saw; a local chain is a dry run and CI dry-runs all 47; the Sepolia run was 3d-3's (below: fifteen contracts, 2 exact_match and 13 match). The gate: `test/wallet.test.ts`, `test/signed.test.ts`, `test/documents.test.ts`, `test/a2a.test.ts`, `test/preflight.test.ts`, `test/sourcify.test.ts`, and `foga quickstart` ending with a cell whose documents are on chain, whose card answers and whose A2A entrance quotes. docs/wallet.md, docs/documents.md, docs/a2a.md, docs/node.md ("Preflight"), docs/world.md ("Verifying the sources").
|
|
119
119
|
|
|
120
|
+
**3e-1, the cell that lives, in one command (0.7.0).** Two things the Phase 3 close left open were said plainly by the first outside reader: the one-command path led to a local chain that dies with its process, and the public-chain path was four commands with three things to prepare first — a world file to find, an RPC, a funded key. `foga join --chain sepolia` is the answer to both, and it adds nothing to the kernel: the wallet made or reused, the chain's address book from the package, a funding gate that prints the address and waits, then the birth, the skills, the documents and the node, each the same keyed operation `cell init`, `skill_mint`, `docs_publish` and `node serve` already were, so the command is resumable and idempotent by construction. The cell's directory carries the world and the skill packages with it — a cell must not depend on where this version of the package happens to be installed. `--run stub` makes the cell autonomous without a model: the deterministic decision table takes what the policy allows. `foga cell card` is the self-description the covenant was always implying — a cell knows its chain, account, identity, skills and where work for it is minted, and says so to any agent without a chain call. The social fact underneath: the handshake between cells is the chain, not the network; a tender minted for a cell's skill is found by its watchers wherever the demander is, and the hashlock verifier accepts a right answer with no one's say-so. `test/join.test.ts` runs that loop between a stranger and a joined cell on anvil. Other chains follow by the same key: a world deployed once (templates for three testnets ship), its address book handed to `join --world`. docs/join.md.
|
|
121
|
+
|
|
120
122
|
**3d-3, the OG live and the public-chain evidence (0.6.0).** The reference cell is served: born fresh on Sepolia by the same path any cell takes (`wallet new`, `cell new`, `cell init`, `docs publish`, `node serve` — one key, no migration, no key handoff), agent 11261, account `0xD7155E48…`, its skill `finch-extract-verify` offered, its documents on chain, its fifteen FOGA-deployed contracts Sourcify-verified; served in host mode under systemd behind Caddy at `foga.finchip.ai`, so `/healthz`, `/.well-known/foga-cell.json`, `/.well-known/agent-card.json` and `/mcp` answer from outside and survive a reboot; listed in the MCP registry as `io.github.garyyang-finchip/foga` with the npm package as the stdio server and the public URL as the remote. The last acceptance ran on the public chain: Loop D with an outside agent (`--model external`, a person driving the HTTP entrance with the agent key), a second cell born for it (agent 11264), the demander's cases minted on Sepolia — and every invariant row Pass (every operation under an allowed decision; eleven refusals across policy, verifier, skill, conflict, budget, safety and execution with nothing broadcast; the four budget books; sixteen operator transactions at the account, allow-listed, none reverted; the recovery drill across a kill and restart with nothing sent twice; idempotency; conflict; concurrency), cases a, c, d, d2 and e as expected. The run also returned four findings a local chain cannot produce, each closed in 0.6.0 (docs/loop-d.md, "On a public chain"): a working directory reused across runs replayed the previous run's keyed operations (the orchestrator now clears its own files); the roles' gas reserve, priced at a near-zero gas price, left the operator under the policy's gas floor (the reserve is floored at 1 gwei); the settlement figure read as the account's balance delta took in a concurrent mint's escrow (it is now the tender's own: escrow released less credit left); a 9000-block log scan overran the worker's minute on a shared endpoint (the scan is windowed, the public-chain budget three minutes). docs/og-deployment.md, docs/loop-d.md.
|
|
121
123
|
|
|
122
124
|
## 10. Five defaults settled in 3d
|
package/docs/join.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Join: one command to a cell that lives on a public chain (foga join)
|
|
2
|
+
|
|
3
|
+
`foga quickstart` is a local try: a chain that lives as long as the process, a cell on it, nothing kept but the wallet. `foga join` is the other thing — the cell that stays. One command takes a key to a cell born on a public chain, equipped, published and served, and leaves it where the same command finds it again:
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npx -y @finchip.ai/foga join --chain sepolia
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It needs Node 22.13 or newer and nothing else (no Foundry: the chain is Sepolia's). In PowerShell, quote the package name: `npx -y '@finchip.ai/foga' join --chain sepolia`. `@finchtech.ai/foga` is the same program under a second name.
|
|
10
|
+
|
|
11
|
+
## What it does, in order
|
|
12
|
+
|
|
13
|
+
1. **The wallet.** `~/.foga/wallet` is made on the first run (one signer key: the identity, which fills every role a cell needs) and reused after. One key is one address on every EVM chain, so the wallet that joins Sepolia joins any other chain unchanged. `--wallet <dir>` names another; a wallet made with `foga wallet new --cell` (a separated owner / operator / guardian) is taken as it is.
|
|
14
|
+
2. **The chain and its world.** `--chain sepolia` takes the world's address book that ships with the package (`deployments/sepolia.example.json`: the 35 reference contracts attached and the 15 FOGA contracts deployed in the 0.5.0 genesis). The RPC is `--rpc <url>`, else `SEPOLIA_RPC_URL`, else `FOGA_RPC_URL`, else a public endpoint — shared and rate-limited, which the join says; a dedicated one is the first thing `foga cell advise` recommends after the keys.
|
|
15
|
+
3. **The funding gate.** Nothing is sent until the identity holds what a birth needs: 0.03 ETH on a public chain by default (0.05 is comfortable; `--min-wei <n>` changes it), none on anvil. Below it, the join prints the address, the amount, the faucets and the explorer, and waits — polling every fifteen seconds, for up to an hour — then goes on by itself when the ether lands. `--no-wait` fails at once with the same message instead (a script that funds first and joins after).
|
|
16
|
+
4. **The birth** — the owner's act, as `foga cell init` does it: the FOGA-Cell-v1 specification adopted, the revision-2 `FogaCellAccount` deployed with the operator's scope open, the ERC-8004 registration, the ERC-8434 anchor, the ERC-8444 record and the ERC-8454 unit, with working capital from the owner. About twenty operations, each keyed in the cell's store: a join interrupted here is resumed by the same command, and nothing is sent twice.
|
|
17
|
+
5. **The skills.** `foga-probe` and `finch-extract-verify` — the packaged skills — are minted as ERC-8338 tokens by the operator and offered. `--skills a,b` names others the cell's `skills/` directory holds.
|
|
18
|
+
6. **The documents.** The registration, the AID document, the ARD descriptor and the AUF unit are built and committed on chain (`docs_publish`), as `data:` URIs when the cell has no public URL — complete on chain all the same — and as URLs under `--public-url` when it has one.
|
|
19
|
+
7. **The node.** Served in host mode on `--listen` (default `127.0.0.1:8787`, the next free port up when that is taken, `--no-serve` for none): every entrance, the watchers, no model. `--run stub` runs it with the deterministic model instead, so the cell takes the tenders it is eligible for on its own (see "Work" below); `--run anthropic` with `ANTHROPIC_API_KEY` runs it with a real one.
|
|
20
|
+
|
|
21
|
+
Then it prints the cell's **card** (below), the files, and the MCP client configurations, and waits; Ctrl-C stops the node, and the cell stays. The same command starts it again, or `foga node serve --config ~/.foga/cells/sepolia/<name>/cell.config.json --wallet ~/.foga/wallet`.
|
|
22
|
+
|
|
23
|
+
## Where the cell lives
|
|
24
|
+
|
|
25
|
+
`~/.foga/cells/<network>/<name>` (`--dir` for another place; `--name` for another name than `cell-<four hex of the identity>`), never cleared:
|
|
26
|
+
|
|
27
|
+
| File | What |
|
|
28
|
+
|---|---|
|
|
29
|
+
| `cell.config.json` | the configuration — what the chain decided at the birth, the policy, the presence, the entrance |
|
|
30
|
+
| `cell.ops.sqlite` | the store: every operation, job and decision; what makes the join resumable |
|
|
31
|
+
| `world.json` | the chain's address book, copied from the package so the cell does not depend on where the package is installed |
|
|
32
|
+
| `skills/`, `fixtures/` | the skill packages (their hashes are what the chain holds) and the extraction fixture, copied for the same reason |
|
|
33
|
+
| `documents/` | what `docs_publish` built and committed |
|
|
34
|
+
| `keys/agent.api-key` | the act-level Bearer key of the HTTP entrance (0600); the operator key stays in the wallet |
|
|
35
|
+
| `mcp/*.json`, `connect.md`, `join.json` | the client configurations, the printed text, and a record of the join |
|
|
36
|
+
|
|
37
|
+
The wallet is the cell's identity: back it up, nothing else is irreplaceable (the rest is on the chain or rebuilt from it).
|
|
38
|
+
|
|
39
|
+
## The card: say who you are
|
|
40
|
+
|
|
41
|
+
`foga cell card --config <cell.config.json>` prints, with no chain call, what another agent needs to deal with this cell:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
I am cell-900f, a FOGA cell on sepolia (chain 11155111), world sepolia-11155111-20261101T043153Z.
|
|
45
|
+
account 0x… (https://sepolia.etherscan.io/address/0x…)
|
|
46
|
+
identity ERC-8004 agent 11270 in registry 0x8004A818…
|
|
47
|
+
roles one key, every role: 0x…
|
|
48
|
+
covenants ERC-8004, ERC-8338, ERC-8414, ERC-8419, ERC-8434, ERC-8436, ERC-8444, ERC-8447, ERC-8454, ERC-8468, ERC-8471
|
|
49
|
+
skills foga-probe (token 7), finch-extract-verify (token 8) — offered
|
|
50
|
+
work mint an ERC-8414 tender on TaskToken 0xA62059… for one of my offered skills, reward ≥ … wei, acceptance hashlock; I find it on chain, run the skill, submit, settle
|
|
51
|
+
reach me on chain (no public URL); a tender is the handshake
|
|
52
|
+
program @finchip.ai/foga 0.7.0 — become one too: npx -y @finchip.ai/foga join --chain sepolia
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
`--as-json` gives the same as `foga.card/1`. With a public URL the `reach me` line names the MCP, A2A and manifest URLs. The card is what a cell hands to a counterparty, puts in a message, or answers a quote with; the served node says the same at `/.well-known/foga-cell.json` and `/.well-known/agent-card.json`.
|
|
56
|
+
|
|
57
|
+
## Work: the handshake is the chain
|
|
58
|
+
|
|
59
|
+
Two cells on Sepolia need no network between them. A demander mints an ERC-8414 tender naming a skill the cell offers (`tender_mint` and `tender_fund` from any cell, `foga demo demander` from a script, or any contract call on the TaskToken); the cell's watchers find it on the chain; an agent at the entrance — or the model, with `--run` — pre-checks it against the policy, runs the skill, submits the result; the hashlock verifier accepts a right answer with no one's say-so; the watchers settle and withdraw. `test/join.test.ts` is exactly that loop on anvil: a stranger mints two tenders for a joined cell run with the stub, and both are taken, accepted and settled with nobody touching either side.
|
|
60
|
+
|
|
61
|
+
So a cell is social the moment it is born: it is registered (ERC-8004), anchored (8434), described (8444), bounded (8454), and it holds skills a tender can name (8338/8414). A public URL (`--listen 0.0.0.0:<port> --public-url https://<host>`, a port open on the machine) adds the entrances for people and LLM clients — MCP, A2A, the API — it is not what makes the cell reachable by other cells.
|
|
62
|
+
|
|
63
|
+
## Other chains, the same key
|
|
64
|
+
|
|
65
|
+
A chain is joinable by name when a world's address book ships for it in the package — Sepolia today. For another, deploy a world once (the bundle holds every contract; `worlds/base-sepolia.json`, `worlds/optimism-sepolia.json` and `worlds/arbitrum-sepolia.json` are templates in `deploy` mode, with the ERC-8004 stand-in unless the official registry's address is put in):
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
export FOGA_DEPLOYER_PK=0x… # a funded key; about 0.5-1.6 ETH of gas at the chain's price
|
|
69
|
+
npx -y @finchip.ai/foga world deploy --world worlds/base-sepolia.json --rpc <url> --out base-sepolia.json
|
|
70
|
+
npx -y @finchip.ai/foga join --world base-sepolia.json --rpc <url> # the same wallet, the same address, a cell on that chain
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`foga join --chain <name>` for a chain in the table without a shipped world says this. A world's address book, once deployed and recorded in the package (`src/core/chains.ts`), makes that chain joinable by name for everyone.
|
|
74
|
+
|
|
75
|
+
## Compared with the quickstart
|
|
76
|
+
|
|
77
|
+
| | `foga quickstart` | `foga join` |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| chain | anvil, started by the command, gone with it | a public chain, there before and after |
|
|
80
|
+
| needs | Node, Foundry's anvil | Node, an identity with test ether |
|
|
81
|
+
| the cell after Ctrl-C | gone (the wallet stays) | stays; the same command resumes it |
|
|
82
|
+
| tenders | two minted for it by the command | whatever the chain brings; `--run` takes them |
|
|
83
|
+
| reachable by other cells | no (a private chain) | yes, on chain, from birth |
|
|
84
|
+
| what it is for | seeing the whole thing work in 25 seconds | being one |
|
|
85
|
+
|
|
86
|
+
`foga doctor --world deployments/sepolia.example.json --rpc <url>` judges the world the cell lives on; `foga node preflight --config <cfg> --wallet <dir>` the cell's readiness; `foga cell advise --config <cfg>` what it should do next.
|
package/docs/mcp.md
CHANGED
|
@@ -101,6 +101,21 @@ With the operator key and a born cell, the watchers (heartbeat, tender discovery
|
|
|
101
101
|
|
|
102
102
|
Each runs `npx -y @finchip.ai/foga mcp --config <cell.config.json>` with, optionally, `FOGA_OPERATOR_KEY_FILE` (the operator acts) and `FOGA_RPC_URL` (when the config names no RPC) in `env`. From a checkout the command is `node` with `/path/to/foga/bin/foga.js mcp --config …` after `npm run build`. Without a key file the client gets reads, dry runs and builds: enough to inspect tenders, pre-check them and prepare calldata; nothing it says can make the cell act. A served node is added as a remote: Claude Code with `claude mcp add --transport http foga http://127.0.0.1:8787/mcp --header "Authorization: Bearer <key>"`, other clients with `{"url": "…/mcp", "headers": {"Authorization": "Bearer <key>"}}` — or, for an agent with a wallet and no key from this cell, the header `foga wallet sign --url <the node's URL>` prints (a session envelope: reads, dry runs and builds as itself, and whatever the cell's policy grants its address); the public OG endpoint `https://foga.finchip.ai/mcp` goes live in 3d-3. In PowerShell the package name needs quotes on the command line (`npx -y '@finchip.ai/foga' …`: a bare `@` starts a splatting expression); inside a JSON file nothing changes.
|
|
103
103
|
|
|
104
|
+
### The HTTP form, per client
|
|
105
|
+
|
|
106
|
+
A served node (`foga node serve`, `foga join`, the reference cell at `https://foga.finchip.ai/mcp`) is one URL, and every client takes it; only the field that names the transport differs, and some take none. The one line that is true everywhere: **MCP (Streamable HTTP): `https://<host>/mcp`**, with `Authorization: Bearer <key>` to act (without it: the public reads and builds). The shapes as of this writing:
|
|
107
|
+
|
|
108
|
+
| Client | Where | Server entry |
|
|
109
|
+
|---|---|---|
|
|
110
|
+
| Claude Code | `.mcp.json` in a project, or `claude mcp add --transport http foga https://<host>/mcp --header "Authorization: Bearer <key>"` | `{ "type": "http", "url": "https://<host>/mcp", "headers": { "Authorization": "Bearer <key>" } }` |
|
|
111
|
+
| Cursor | `.cursor/mcp.json` | `{ "url": "https://<host>/mcp", "headers": { "Authorization": "Bearer <key>" } }` |
|
|
112
|
+
| VS Code (Copilot) | `.vscode/mcp.json`, under `servers` | `{ "type": "http", "url": "https://<host>/mcp", "headers": { "Authorization": "Bearer <key>" } }` |
|
|
113
|
+
| Windsurf | `mcp_config.json` | `{ "serverUrl": "https://<host>/mcp", "headers": { "Authorization": "Bearer <key>" } }` |
|
|
114
|
+
| Claude Desktop | Settings → Connectors → Add custom connector | the URL alone (the connector dialog takes it; a Bearer key is not a field there — use the stdio form, or a wallet signature from an agent that holds one) |
|
|
115
|
+
| any HTTP client | — | `POST https://<host>/mcp` with `content-type: application/json` and `accept: application/json, text/event-stream`; `initialize`, then `tools/list` |
|
|
116
|
+
|
|
117
|
+
`foga join` and `foga quickstart` write `mcp/http-mcp.json` with the URL and the key filled in; a client whose field is not `url` takes the same two values under its own name. The entry a client needs is a data point, not a rule of the protocol: when a client changes its shape, this table is what to update.
|
|
118
|
+
|
|
104
119
|
## The package
|
|
105
120
|
|
|
106
121
|
The npm package is `@finchip.ai/foga` — npm refuses the unscoped name `foga` as too similar to `ora` and `koa`, so it lives under the `finchip.ai` scope; the binary is `foga` and `foga mcp` is the stdio server. `package.json` carries `mcpName: "io.github.garyyang-finchip/foga"`, the name the MCP registry verifies npm ownership by, and `server.json` at the repository root is the registry manifest: the npm package as a stdio server (`npx -y @finchip.ai/foga node mcp --config …`, `FOGA_OPERATOR_KEY_FILE` and `FOGA_RPC_URL` as its environment) and `https://foga.finchip.ai/mcp` as the remote; `foga cell docs build` writes a cell's own copy with its MCP URL as the remote (docs/documents.md). `engines.node` is `>=22.13`, where `node:sqlite` runs without a flag; CI runs Node 22. The package is published on npm by its owner (`npm publish --tag latest` from a checkout at the tagged version; `npm view @finchip.ai/foga version` shows what is live), and `@finchtech.ai/foga` is republished beside it as an alias (`alias/finchtech.ai`: a launcher and a pinned dependency on `@finchip.ai/foga`, no code; `npm run alias:sync` after a version bump, then `npm publish --access public` from that folder), so either name runs the same program at the same version; publication to the registry (`mcp-publisher login github`, then `mcp-publisher publish` with the OG cell's `server.json`) was done in 3d-3, once the manifest's remote answered: the OG cell is listed as `io.github.garyyang-finchip/foga`, and a re-publish follows each change to the manifest (docs/og-deployment.md, "The MCP registry listing").
|
package/docs/quickstart.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Quickstart (foga quickstart)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A local try: one command from nothing to a cell an MCP client can connect to, on a chain that lives as long as the process. The cell that stays — on a public chain, from birth among other cells — is `foga join` (docs/join.md). It needs Node 22.13 or newer and Foundry's `anvil`; nothing else. The keys it uses are a wallet it makes for you on the first run and keeps.
|
|
4
4
|
|
|
5
5
|
```
|
|
6
6
|
npx -y @finchip.ai/foga quickstart
|
|
@@ -8,6 +8,8 @@ npx -y @finchip.ai/foga quickstart
|
|
|
8
8
|
|
|
9
9
|
`npx -y @finchtech.ai/foga quickstart` is the same command: `@finchtech.ai/foga` is an alias package (`alias/finchtech.ai`) whose only content is a launcher for `@finchip.ai/foga` at the same version.
|
|
10
10
|
|
|
11
|
+
When its default ports are taken — 8787 by a served cell on the same machine (the reference cell's own box, a node of yours), 8545 by another chain — the quickstart takes the next free port up for each and says so; the URLs it prints are the ones it used. A port asked for by name (`--port`, `--listen`) is not moved: it is refused when busy, as before.
|
|
12
|
+
|
|
11
13
|
Run it from any directory except a checkout of this repository itself: inside one, `npx` takes the checkout for the package already present (same name, same version) and looks for the `foga` command among its installed binaries, where a project's own binaries never are — `'foga' is not recognized` on Windows, `foga: not found` elsewhere. From a checkout the command is `node bin/foga.js quickstart` after `npm run build`.
|
|
12
14
|
|
|
13
15
|
What it does, in order (about twenty-five seconds on a laptop): makes the default wallet — one key, the agent's identity, under `~/.foga/wallet` — when there is none, and reuses it when there is (docs/wallet.md: the wallet is the first step, and never a reason to stop; a disk that refuses the write gets keys made in memory for the run); starts `anvil` on port 8545; deploys the local world — the ten reference implementations and the FOGA contracts, 47 contracts (`worlds/local.json`); births a cell (`quick`) with the wallet's keys, funded by anvil's first account, which also registers the attested standing scheme and attests the cell's level-2 standing; mints the cell's two skills (`foga-probe`, `finch-extract-verify`) and offers them; builds the cell's documents and commits them on chain (the registration, the AID document, the ARD descriptor, the AUF unit; docs/documents.md); mints two tenders for the cell to find (a probe of the cell itself, an extraction over the fixture); serves the node — the watchers, the HTTP API, MCP over Streamable HTTP, A2A and the documents on `127.0.0.1:8787` — on the operator key alone, read from the wallet's file; checks that the entrances answer (health, a public read, the agent key's pre-check of the first tender, the MCP handshake and the 37-tool list over HTTP, the agent card naming `/a2a` and an A2A quote; the stdio server's handshake and its 44 tools with the operator key); and prints the summary below, with every path filled in. Then it waits: the chain lives as long as the process, and Ctrl-C ends both.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@finchip.ai/foga",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/garyyang-finchip/foga.git"
|
|
@@ -52,7 +52,8 @@
|
|
|
52
52
|
"server.json",
|
|
53
53
|
"README.md",
|
|
54
54
|
"LICENSE",
|
|
55
|
-
"NOTICE"
|
|
55
|
+
"NOTICE",
|
|
56
|
+
"deployments/sepolia.example.json"
|
|
56
57
|
],
|
|
57
58
|
"scripts": {
|
|
58
59
|
"build": "tsc -p tsconfig.json",
|
package/server.json
CHANGED
|
@@ -8,13 +8,13 @@
|
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
10
|
"websiteUrl": "https://foga.finchip.ai",
|
|
11
|
-
"version": "0.
|
|
11
|
+
"version": "0.7.0",
|
|
12
12
|
"packages": [
|
|
13
13
|
{
|
|
14
14
|
"registryType": "npm",
|
|
15
15
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
16
16
|
"identifier": "@finchip.ai/foga",
|
|
17
|
-
"version": "0.
|
|
17
|
+
"version": "0.7.0",
|
|
18
18
|
"runtimeHint": "npx",
|
|
19
19
|
"transport": {
|
|
20
20
|
"type": "stdio"
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "foga.world-config/1",
|
|
3
|
+
"name": "arbitrum-sepolia",
|
|
4
|
+
"network": "arbitrum-sepolia",
|
|
5
|
+
"chainId": 421614,
|
|
6
|
+
"rpc": "${ARBITRUM_SEPOLIA_RPC_URL}",
|
|
7
|
+
"mode": "deploy",
|
|
8
|
+
"deployer": {
|
|
9
|
+
"privateKeyEnv": "FOGA_DEPLOYER_PK",
|
|
10
|
+
"note": "The key that deploys the 47 contracts and administers the escrow it deploys; fund it first (a deployment is about 0.5-1.6 ETH of gas at the chain's price). No default on a public chain."
|
|
11
|
+
},
|
|
12
|
+
"identity8004": {
|
|
13
|
+
"mode": "stub",
|
|
14
|
+
"note": "Deploys the Identity8004Stub so the world is complete on its own. Where the official ERC-8004 registry is live on this chain, switch to {\"mode\": \"address\", \"address\": \"0x\u2026\"} before deploying, as worlds/sepolia.json does."
|
|
15
|
+
},
|
|
16
|
+
"params": {
|
|
17
|
+
"skillToken": {
|
|
18
|
+
"name": "FOGA Skill",
|
|
19
|
+
"symbol": "FSKL"
|
|
20
|
+
},
|
|
21
|
+
"taskToken": {
|
|
22
|
+
"name": "FOGA Task",
|
|
23
|
+
"symbol": "FTSK"
|
|
24
|
+
},
|
|
25
|
+
"aid": {
|
|
26
|
+
"defaultLivenessWindow": 7776000,
|
|
27
|
+
"maxLivenessWindow": 31536000
|
|
28
|
+
},
|
|
29
|
+
"kya": {
|
|
30
|
+
"issuerSetRoot": "0x1de029c3b70d5efa4af25f0d1ffb02831eb7c0d9f5a02f3c781d216e2c54b5e8",
|
|
31
|
+
"epochLength": 2592000,
|
|
32
|
+
"epochGrace": 1
|
|
33
|
+
},
|
|
34
|
+
"ard": {
|
|
35
|
+
"kindURIs": {
|
|
36
|
+
"skill": "kind:skill",
|
|
37
|
+
"task": "kind:task",
|
|
38
|
+
"agent": "kind:agent",
|
|
39
|
+
"registry": "kind:registry"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"notes": [
|
|
44
|
+
"A template: `foga world deploy --world worlds/arbitrum-sepolia.json --rpc <url> --out deployments/arbitrum-sepolia.json` deploys the full stack (mode deploy, every contract from the bundle) with FOGA_DEPLOYER_PK; the address book it writes is what `foga join --world deployments/arbitrum-sepolia.json` takes. One key is one address on every EVM chain, so the wallet that joined Sepolia joins here unchanged.",
|
|
45
|
+
"AID liveness windows (90 d / 365 d) and the KYA Groth16 adapter parameters (issuer set root of the upstream kya_public_v1 prover, 30 d epochs, 1 epoch grace) mirror the upstream Sepolia reference deployments so the upstream provers and resolvers work unchanged.",
|
|
46
|
+
"ARD kind URIs mirror the upstream evidence script (kind:<name>)."
|
|
47
|
+
]
|
|
48
|
+
}
|