@finchip.ai/foga 0.6.0-alpha.6 → 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.
- package/README.md +31 -14
- package/dist/a2a/card.js +54 -0
- package/dist/a2a/server.js +276 -0
- package/dist/a2a/types.js +20 -0
- package/dist/build/bundle.js +2 -1
- package/dist/cli-cell.js +147 -16
- package/dist/cli.js +138 -5
- package/dist/demo/birth.js +4 -4
- package/dist/demo/quickstart.js +60 -7
- package/dist/genesis/operator-scope.js +1 -0
- package/dist/http/access.js +105 -0
- package/dist/http/server.js +65 -43
- package/dist/http/signed.js +136 -0
- package/dist/mcp/server.js +27 -1
- package/dist/node/config.js +34 -1
- package/dist/node/documents.js +298 -0
- package/dist/node/preflight.js +208 -0
- package/dist/node/wallet.js +118 -0
- package/dist/tools/documents.js +74 -0
- package/dist/tools/index.js +2 -1
- package/dist/world/sourcify.js +165 -0
- package/docs/a2a.md +31 -0
- package/docs/design-memo.md +17 -6
- package/docs/documents.md +25 -0
- package/docs/examples/README.md +2 -2
- package/docs/mcp.md +16 -11
- package/docs/node.md +28 -10
- package/docs/quickstart.md +30 -21
- package/docs/wallet.md +60 -0
- package/docs/world.md +12 -0
- package/package.json +1 -1
- package/server.json +2 -2
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DOCUMENT_FILES, buildDocuments, documentsDir, onChainState, publishSteps, readBack, readBuilt, readManifest } from "../node/documents.js";
|
|
3
|
+
import { ok, refused, safetyGate } from "../node/policy.js";
|
|
4
|
+
import { version } from "../util/version.js";
|
|
5
|
+
import { derivedJobId, zJobId } from "./common.js";
|
|
6
|
+
/**
|
|
7
|
+
* The cell's documents as capabilities (design memo, Phase 3, section 7): `docs_status` reads what is built and what
|
|
8
|
+
* the chain holds; `docs_publish` builds the documents from the configuration, commits each one that differs from
|
|
9
|
+
* the chain — the ERC-8004 agentURI, the ERC-8434 document URI and digest, the ERC-8444 descriptor, a new ERC-8454
|
|
10
|
+
* unit version — as one keyed job (the same documents are the same job, never sent twice), then reads them back.
|
|
11
|
+
* Metadata updates are the operator's own calls through the account (the `identity` scope group), plain actions
|
|
12
|
+
* that commit nothing and move nothing.
|
|
13
|
+
*/
|
|
14
|
+
const RowSchema = z.object({ commitment: z.string(), ok: z.boolean().nullable(), detail: z.string() });
|
|
15
|
+
export const docs_status = {
|
|
16
|
+
name: "docs_status",
|
|
17
|
+
title: "Documents: built and on chain",
|
|
18
|
+
description: "The cell's documents as last built (the manifest: files, content hashes, version) and what the chain holds for each commitment — agentURI, AID document, ARD descriptor, AUF unit version — with whether they agree.",
|
|
19
|
+
action: "read",
|
|
20
|
+
visibility: "public",
|
|
21
|
+
principal: "none",
|
|
22
|
+
ercs: [8004, 8434, 8444, 8454],
|
|
23
|
+
input: z.object({}),
|
|
24
|
+
output: z.object({
|
|
25
|
+
dir: z.string(),
|
|
26
|
+
manifest: z.unknown().nullable(),
|
|
27
|
+
onChain: z.object({ agentURI: z.string().nullable(), aid: z.object({ uri: z.string(), digest: z.string() }).nullable(), ard: z.object({ descriptorHash: z.string(), descriptorURI: z.string() }).nullable(), unit: z.object({ version: z.number(), boundaryHash: z.string() }).nullable() }),
|
|
28
|
+
readBack: z.array(RowSchema),
|
|
29
|
+
pending: z.array(z.string()),
|
|
30
|
+
}),
|
|
31
|
+
async handler({ node }) {
|
|
32
|
+
const dir = documentsDir(node);
|
|
33
|
+
const built = readBuilt(dir);
|
|
34
|
+
const onChain = await onChainState(node);
|
|
35
|
+
const rows = built ? await readBack(node, built, { fetchUrls: false }) : [];
|
|
36
|
+
const pending = built ? (await publishSteps(node, built)).steps.map((s) => s.name) : [];
|
|
37
|
+
return { dir, manifest: readManifest(dir), onChain, readBack: rows, pending };
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
export const docs_publish = {
|
|
41
|
+
name: "docs_publish",
|
|
42
|
+
title: "Publish the cell's documents",
|
|
43
|
+
description: "Build the documents from the configuration (registration, AID document, ARD descriptor, AUF unit, agent card, server.json) and commit on chain each one that differs from what the chain holds: setAgentURI, setDocumentURI, updateDescriptor, publishVersion. One keyed job; nothing is sent twice for the same documents; read back afterwards.",
|
|
44
|
+
action: "act",
|
|
45
|
+
visibility: "operator",
|
|
46
|
+
principal: "cell",
|
|
47
|
+
ercs: [8004, 8434, 8444, 8454],
|
|
48
|
+
input: z.object({ jobId: zJobId }),
|
|
49
|
+
output: z.object({ dir: z.string(), version: z.number(), documents: z.array(z.string()), committed: z.array(z.string()), unchanged: z.array(z.string()), readBack: z.array(RowSchema), jobId: z.string(), operations: z.array(z.string()) }),
|
|
50
|
+
async plan({ node }, input) {
|
|
51
|
+
const k = node.kernel;
|
|
52
|
+
const cell = node.body;
|
|
53
|
+
const built = await buildDocuments(node, { version });
|
|
54
|
+
const { steps: pending, unchanged } = await publishSteps(node, built);
|
|
55
|
+
const gates = [];
|
|
56
|
+
const [aidState, status] = await Promise.all([k.aid.stateName(cell.address), cell.status()]);
|
|
57
|
+
gates.push(safetyGate(aidState, status, "plain"));
|
|
58
|
+
if (!node.config.identity.agentId)
|
|
59
|
+
gates.push(refused("policy", "the cell has no ERC-8004 agent id: nothing to publish a registration for"));
|
|
60
|
+
else
|
|
61
|
+
gates.push(ok("policy", `${pending.length} commitment(s) to make, ${unchanged.length} already as built`));
|
|
62
|
+
const manifestHash = Object.values(built.manifest.documents).map((d) => d.contentHash).join(",");
|
|
63
|
+
const jobId = input.jobId ?? derivedJobId("docs", { hashes: manifestHash, uri: built.manifest.publicUrl ?? "data" });
|
|
64
|
+
const steps = pending.map((s) => ({ name: s.name, kind: "plain", build: () => ({ target: s.target, value: 0n, data: s.data, label: s.label }) }));
|
|
65
|
+
return {
|
|
66
|
+
job: { id: jobId, kind: "docs", input: { version: built.manifest.version, documents: built.manifest.documents, steps: pending.map((s) => s.name) } },
|
|
67
|
+
gates,
|
|
68
|
+
valueWei: 0n,
|
|
69
|
+
steps,
|
|
70
|
+
output: async (confirmed) => ({ dir: built.dir, version: built.manifest.version, documents: Object.values(DOCUMENT_FILES), committed: pending.map((s) => s.name), unchanged, readBack: await readBack(node, built, { fetchUrls: false }), jobId, operations: confirmed.map((c) => c.key) }),
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
export const DOCUMENT_CAPABILITIES = [docs_status, docs_publish];
|
package/dist/tools/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ADMIN_CAPABILITIES } from "./admin.js";
|
|
2
2
|
import { Catalog } from "./catalog.js";
|
|
3
|
+
import { DOCUMENT_CAPABILITIES } from "./documents.js";
|
|
3
4
|
import { IDENTITY_CAPABILITIES } from "./identity.js";
|
|
4
5
|
import { READ_CAPABILITIES } from "./read.js";
|
|
5
6
|
import { TENDER_CAPABILITIES } from "./tenders.js";
|
|
@@ -8,7 +9,7 @@ export * from "./types.js";
|
|
|
8
9
|
/** The catalog every entrance exposes: the Loop D path of the design memo's 43 capabilities, in Phase 3a. */
|
|
9
10
|
export function defaultCatalog() {
|
|
10
11
|
const c = new Catalog();
|
|
11
|
-
for (const cap of [...READ_CAPABILITIES, ...TENDER_CAPABILITIES, ...IDENTITY_CAPABILITIES, ...ADMIN_CAPABILITIES])
|
|
12
|
+
for (const cap of [...READ_CAPABILITIES, ...TENDER_CAPABILITIES, ...IDENTITY_CAPABILITIES, ...DOCUMENT_CAPABILITIES, ...ADMIN_CAPABILITIES])
|
|
12
13
|
c.register(cap);
|
|
13
14
|
return c;
|
|
14
15
|
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { loadBundle } from "../bundle/load.js";
|
|
4
|
+
import { readJson, repoRoot } from "../util/fs.js";
|
|
5
|
+
/**
|
|
6
|
+
* Source verification on Sourcify (design memo, section 8, the 2c leftover, and section 9, 3d-2): every contract a world deployed
|
|
7
|
+
* from the bundle is submitted with the very standard JSON input its compiler run took — the `build-info` files
|
|
8
|
+
* `forge build` leaves beside the artifacts (`.foga-build/out/<repo>/build-info/`, written by `npm run bundle:verify`
|
|
9
|
+
* and `foga build`) — the compiler version from the contract's metadata, and the creation transaction the world file
|
|
10
|
+
* recorded. Nothing is recompiled or rewritten for the verifier: what Sourcify checks is what the bundle was built
|
|
11
|
+
* from, at the locked commits. A dry run writes each request to a file instead of sending it; a chain Sourcify does
|
|
12
|
+
* not serve (anvil's 31337) is always a dry run.
|
|
13
|
+
*/
|
|
14
|
+
export const SOURCIFY_SERVER = "https://sourcify.dev/server";
|
|
15
|
+
/** Every full build-info file under the work directory, by repository. */
|
|
16
|
+
export function collectBuildInfo(outRoot = join(repoRoot(), ".foga-build", "out")) {
|
|
17
|
+
const out = [];
|
|
18
|
+
if (!existsSync(outRoot))
|
|
19
|
+
return out;
|
|
20
|
+
for (const repo of readdirSync(outRoot)) {
|
|
21
|
+
const dir = join(outRoot, repo, "build-info");
|
|
22
|
+
if (!existsSync(dir))
|
|
23
|
+
continue;
|
|
24
|
+
for (const name of readdirSync(dir).filter((f) => f.endsWith(".json"))) {
|
|
25
|
+
const b = JSON.parse(readFileSync(join(dir, name), "utf8"));
|
|
26
|
+
if (!b.input || !b.output?.contracts)
|
|
27
|
+
continue; // a minimal build-info (no --build-info): nothing to submit
|
|
28
|
+
out.push({ file: join(dir, name), repo, solcVersion: b.solcLongVersion ?? b.solcVersion ?? "", input: b.input, output: b.output });
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function normalizeCode(hex) {
|
|
34
|
+
return (hex ?? "").toLowerCase().replace(/^0x/, "");
|
|
35
|
+
}
|
|
36
|
+
/** The compiler's long version (0.8.28+commit.7893614a) from a contract's metadata, the build-info's otherwise. */
|
|
37
|
+
function compilerVersionOf(meta, fallback) {
|
|
38
|
+
try {
|
|
39
|
+
const m = JSON.parse(meta ?? "{}");
|
|
40
|
+
return m.compiler?.version ?? fallback;
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return fallback;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The verification requests of a world: for every step the world deployed from the bundle, the build-info whose
|
|
48
|
+
* output holds a contract of that name in that repository with the bundle's runtime code, the path solc saw as the
|
|
49
|
+
* identifier.
|
|
50
|
+
*/
|
|
51
|
+
export function verificationRequests(world, infos, o = {}) {
|
|
52
|
+
const bundle = loadBundle();
|
|
53
|
+
const requests = [];
|
|
54
|
+
const missing = [];
|
|
55
|
+
for (const step of world.steps.filter((s) => s.status === "deployed")) {
|
|
56
|
+
if (o.only && !o.only.includes(step.id))
|
|
57
|
+
continue;
|
|
58
|
+
const entry = world.contracts[step.id];
|
|
59
|
+
if (!entry?.address || !entry.bundleKey) {
|
|
60
|
+
missing.push({ id: step.id, reason: "the world file names no address or bundle key for this step" });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const contract = bundle.contracts[entry.bundleKey];
|
|
64
|
+
if (!contract) {
|
|
65
|
+
missing.push({ id: step.id, reason: `bundle key ${entry.bundleKey} is not in the committed bundle` });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const [repo, name] = entry.bundleKey.split(":");
|
|
69
|
+
const wanted = normalizeCode(contract.deployedBytecode);
|
|
70
|
+
let found = null;
|
|
71
|
+
for (const info of infos.filter((i) => i.repo === repo)) {
|
|
72
|
+
for (const [path, byName] of Object.entries(info.output.contracts)) {
|
|
73
|
+
const c = byName[name];
|
|
74
|
+
if (!c)
|
|
75
|
+
continue;
|
|
76
|
+
const code = normalizeCode(c.evm?.deployedBytecode?.object);
|
|
77
|
+
// immutables and the metadata hash make exact equality too strict only when they differ; the bundle was built the same way, so equality is the check
|
|
78
|
+
if (code && wanted && code !== wanted)
|
|
79
|
+
continue;
|
|
80
|
+
found = { id: step.id, address: entry.address, bundleKey: entry.bundleKey, creationTransactionHash: step.txHash ?? null, contractIdentifier: `${path}:${name}`, compilerVersion: compilerVersionOf(c.metadata, info.solcVersion), buildInfo: info.file, stdJsonInput: info.input };
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
if (found)
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
if (!found)
|
|
87
|
+
missing.push({ id: step.id, reason: `no build-info under .foga-build/out/${repo}/build-info holds ${name} with the bundle's runtime code (run npm run bundle:verify to produce the build-info files)` });
|
|
88
|
+
else
|
|
89
|
+
requests.push(found);
|
|
90
|
+
}
|
|
91
|
+
return { requests, missing };
|
|
92
|
+
}
|
|
93
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
94
|
+
/** Verify a world's deployed contracts on Sourcify, or write what would be sent. */
|
|
95
|
+
export async function verifyWorld(o) {
|
|
96
|
+
const log = o.log ?? (() => { });
|
|
97
|
+
const world = readJson(resolve(o.worldPath));
|
|
98
|
+
const server = (o.server ?? SOURCIFY_SERVER).replace(/\/+$/, "");
|
|
99
|
+
const served = ![31337, 1337].includes(world.chainId);
|
|
100
|
+
const dryRun = o.dryRun ?? !served;
|
|
101
|
+
if (!served && !dryRun)
|
|
102
|
+
throw new Error(`chain ${world.chainId} is a local chain Sourcify does not serve; use --dry-run`);
|
|
103
|
+
const infos = collectBuildInfo(o.buildInfoRoot);
|
|
104
|
+
const { requests, missing } = verificationRequests(world, infos, { only: o.only });
|
|
105
|
+
const outDir = resolve(o.outDir ?? join(repoRoot(), ".foga-build", "sourcify", world.name));
|
|
106
|
+
mkdirSync(outDir, { recursive: true });
|
|
107
|
+
const f = o.fetchImpl ?? fetch;
|
|
108
|
+
const results = [];
|
|
109
|
+
log(`sourcify: world ${world.worldId} (chain ${world.chainId}): ${requests.length} contract(s) to verify, ${missing.length} without a build-info; ${dryRun ? "dry run" : server}`);
|
|
110
|
+
for (const r of requests) {
|
|
111
|
+
const body = { stdJsonInput: r.stdJsonInput, compilerVersion: r.compilerVersion, contractIdentifier: r.contractIdentifier, ...(r.creationTransactionHash ? { creationTransactionHash: r.creationTransactionHash } : {}) };
|
|
112
|
+
const file = join(outDir, `${r.id.replace(/[^A-Za-z0-9._-]+/g, "_")}.json`);
|
|
113
|
+
writeFileSync(file, JSON.stringify({ chainId: world.chainId, address: r.address, ...body }, null, 2) + "\n");
|
|
114
|
+
if (dryRun) {
|
|
115
|
+
results.push({ id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: "dry-run", detail: `request written to ${file}` });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const existing = await f(`${server}/v2/contract/${world.chainId}/${r.address}`, { signal: AbortSignal.timeout(30_000) });
|
|
120
|
+
if (existing.ok) {
|
|
121
|
+
const e = (await existing.json());
|
|
122
|
+
if (e.match === "exact_match" || e.match === "match") {
|
|
123
|
+
results.push({ id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: "already", detail: `already verified (${e.match})`, creationMatch: e.creationMatch ?? null, runtimeMatch: e.runtimeMatch ?? null });
|
|
124
|
+
log(` ${r.id}: already verified (${e.match})`);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const submitted = await f(`${server}/v2/verify/${world.chainId}/${r.address}`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body), signal: AbortSignal.timeout(120_000) });
|
|
129
|
+
const ticket = (await submitted.json());
|
|
130
|
+
if (!submitted.ok || !ticket.verificationId) {
|
|
131
|
+
results.push({ id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: "error", detail: `HTTP ${submitted.status}: ${ticket.customCode ?? ""} ${ticket.message ?? ""}`.trim() });
|
|
132
|
+
log(` ${r.id}: refused — HTTP ${submitted.status} ${ticket.customCode ?? ""} ${ticket.message ?? ""}`);
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const started = Date.now();
|
|
136
|
+
let outcome = null;
|
|
137
|
+
while (Date.now() - started < (o.timeoutMs ?? 300_000)) {
|
|
138
|
+
await sleep(o.pollMs ?? 3000);
|
|
139
|
+
const status = await f(`${server}/v2/verify/${ticket.verificationId}`, { signal: AbortSignal.timeout(30_000) });
|
|
140
|
+
const job = (await status.json());
|
|
141
|
+
if (!job.isJobCompleted)
|
|
142
|
+
continue;
|
|
143
|
+
if (job.error)
|
|
144
|
+
outcome = { id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: "failed", detail: `${job.error.customCode ?? ""} ${job.error.message ?? ""}`.trim(), verificationId: ticket.verificationId };
|
|
145
|
+
else
|
|
146
|
+
outcome = { id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: job.contract?.match === "exact_match" ? "exact_match" : "match", detail: `creation ${job.contract?.creationMatch ?? "-"}, runtime ${job.contract?.runtimeMatch ?? "-"}`, verificationId: ticket.verificationId, creationMatch: job.contract?.creationMatch ?? null, runtimeMatch: job.contract?.runtimeMatch ?? null };
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
if (!outcome)
|
|
150
|
+
outcome = { id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: "error", detail: `verification ${ticket.verificationId} did not complete in time; check ${server}/v2/verify/${ticket.verificationId}`, verificationId: ticket.verificationId };
|
|
151
|
+
results.push(outcome);
|
|
152
|
+
log(` ${r.id}: ${outcome.outcome} — ${outcome.detail}`);
|
|
153
|
+
}
|
|
154
|
+
catch (e) {
|
|
155
|
+
results.push({ id: r.id, address: r.address, contractIdentifier: r.contractIdentifier, compilerVersion: r.compilerVersion, outcome: "error", detail: e.message });
|
|
156
|
+
log(` ${r.id}: error — ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
const summary = {};
|
|
160
|
+
for (const r of results)
|
|
161
|
+
summary[r.outcome] = (summary[r.outcome] ?? 0) + 1;
|
|
162
|
+
const report = { schema: "foga.sourcify/1", worldId: world.worldId, chainId: world.chainId, server, dryRun, at: new Date().toISOString(), results, missing, summary };
|
|
163
|
+
writeFileSync(join(outDir, "report.json"), JSON.stringify(report, null, 2) + "\n");
|
|
164
|
+
return { report, outDir };
|
|
165
|
+
}
|
package/docs/a2a.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# A2A (POST /a2a, /.well-known/agent-card.json)
|
|
2
|
+
|
|
3
|
+
The fourth entrance (design memo, sections 7 and 9, 3d-2): the Agent2Agent protocol at version 1.0, the JSON-RPC binding only, on the node's HTTP server. Like MCP and the HTTP API it adds no rule of its own: an inbound message becomes work the catalog judges as the caller, and the economics stay on chain.
|
|
4
|
+
|
|
5
|
+
## The card
|
|
6
|
+
|
|
7
|
+
`GET /.well-known/agent-card.json` is the AgentCard (A2A 1.0, section 4.4), one of the documents `foga cell docs build` writes from the configuration (docs/documents.md): `name`, `description`, `supportedInterfaces: [{url: <publicUrl>/a2a, protocolBinding: "JSONRPC", protocolVersion: "1.0"}]` (empty until the cell has a public URL — the caller then uses the URL it knows), `provider` (`presence.provider`), `version` (the package's), `capabilities: {streaming: false, pushNotifications: false, extendedAgentCard: false}`, `securitySchemes` (`api-key`: `Bearer`; `wallet-signature`: `FOGA-EIP712`, docs/wallet.md), `defaultInputModes: ["text/plain", "application/json"]`, `defaultOutputModes: ["application/json", "text/plain"]`, and `skills`: the ERC-8338 skills the policy offers (`skills.offered`), each `id` the package name at its version, with two examples — a quote and a tender — as data parts. Not the token list, not a promise of anything the policy would refuse. The card is not signed (JWS stays for later).
|
|
8
|
+
|
|
9
|
+
## The binding
|
|
10
|
+
|
|
11
|
+
`POST /a2a` with `Content-Type: application/json` and `A2A-Version: 1.0` (or `?A2A-Version=1.0`); a JSON-RPC 2.0 request whose `method` is `SendMessage`, `GetTask`, `ListTasks` or `CancelTask`. An empty version is 0.3 by the specification and this agent does not speak it: `VersionNotSupportedError` (-32009). `SendStreamingMessage`, `SubscribeToTask` and `GetExtendedAgentCard` are `UnsupportedOperationError` (-32004), the push-notification methods `PushNotificationNotSupportedError` (-32003), anything else `MethodNotFoundError` (-32601); the error codes and their `data` (`google.rpc.ErrorInfo` with `reason`, `domain`, `metadata`) follow sections 5.4 and 9.5. Field names are camelCase and enums are their Protocol Buffer names (`TASK_STATE_COMPLETED`, `ROLE_USER`), as the specification prescribes for every JSON binding. Credentials are the HTTP server's (`Authorization`: nothing, a Bearer key, or a wallet signature); the caller a message comes from is what the node's access policy makes of them.
|
|
12
|
+
|
|
13
|
+
## Two roads
|
|
14
|
+
|
|
15
|
+
A message's data part may carry a `foga` object; a message of words is a quote.
|
|
16
|
+
|
|
17
|
+
**Quote** — `{"foga": {"mode": "quote", "skill": "foga-probe", "input": {…}}}`, or any text. Open to anyone. The task completes at once with a `quote` artifact: the cell's offered skills (name, version, package hash, token id, summary), the terms a tender needs to meet (the acceptance authority — the world's hashlock verifier — the reward floor, the submit and settle windows, the counterparty level), and how to proceed. A skill the cell does not offer is `TASK_STATE_REJECTED` with the list of what it does. Nothing runs and nothing touches the chain.
|
|
18
|
+
|
|
19
|
+
**Tender** — `{"foga": {"mode": "tender", "tokenId": "23"}}`, naming an ERC-8414 tender that exists, is funded, and has its answer committed to the hashlock verifier (the caller minted it for its own account — `tender_mint_build` gives the calls — or anyone did). The caller needs the act level: an API key the operator issued, or a wallet signature the cell's policy grants act to (with a request-bound envelope). The public is told to quote (`TASK_STATE_REJECTED`, gate `permission`). Without a `tokenId` the task waits (`TASK_STATE_INPUT_REQUIRED`) and a message with the `taskId` continues it. With one, the response is the task in `TASK_STATE_WORKING` and the work goes on after it (section 3.3.3): `tender_precheck`, then `skill_run` with the task document's input, then `tender_submit` — each through `Catalog.invoke` as the A2A caller, each a decision record under the caller's id. The task ends `TASK_STATE_COMPLETED` with a `submission` artifact (token id, result hash, answer hash, submission id, transaction hash, job and decision ids, the operations), `TASK_STATE_REJECTED` when a gate refused (the gate, the reason and the decision id in the status message's data part), or `TASK_STATE_FAILED` when the run failed. Settlement and withdrawal are the node's watchers' duty afterwards, as for every submission; the caller does not pay the cell through A2A and the cell does not pay the caller: the tender's escrow pays the account.
|
|
20
|
+
|
|
21
|
+
There is no road that does work first and talks about payment after, and no road that mints or funds a tender on the caller's behalf (who pays and who approves would need an authority this version does not define).
|
|
22
|
+
|
|
23
|
+
## Tasks
|
|
24
|
+
|
|
25
|
+
A task lives in the cell's store (`a2a.task.<id>` among the facts): visible to the caller that made it and to the operator's view, never to another caller (`GetTask` answers `TaskNotFoundError` rather than reveal it exists). The same `messageId` from the same caller is the same task (section 3.3.1). A message to a terminal task is `UnsupportedOperationError`. `ListTasks` pages one's own tasks (`pageSize` 1–100, `pageToken` as an offset, `status` and `contextId` filters, `includeArtifacts`); `CancelTask` ends a task that has not started working, asks a working one to stop between its steps, and refuses (`TaskNotCancelableError`) once the submission is broadcast or the task is terminal: what reached the chain stays.
|
|
26
|
+
|
|
27
|
+
## What the memo's v0.2 asked and where it landed
|
|
28
|
+
|
|
29
|
+
The signature covers the request content (the request-bound envelope hashes method, path and body); the request right and the act right are separate (a grant decides the level; the cell's own rules decide the act); task and operation are layered (one task, the catalog's jobs beneath it, named in the artifact); only existing, funded tenders are taken; duplicates are detected by (caller, messageId); `CancelTask` undoes nothing broadcast; the content type is `application/json`; `ListTasks` is implemented and scoped to the caller; `supportedInterfaces` lists the one binding; the data types are the proto's names and shapes (`src/a2a/types.ts`). Outbound A2A — the node calling other agents — is not in this phase: the node's work comes from the chain.
|
|
30
|
+
|
|
31
|
+
`test/a2a.test.ts` drives all of it on anvil with a raw JSON-RPC client; `foga quickstart` checks the card and a quote on every run.
|
package/docs/design-memo.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
# FOGA design memo (v0.
|
|
1
|
+
# FOGA design memo (v0.3, condensed)
|
|
2
2
|
|
|
3
|
-
This is the English working version of the design. Phase 1 (the world layer), Phase 2a (cells, kernel, genesis T0–T5), Phase 2b (the predicate ZK path, T6–T9, measurement receipts, doctor layer 5; docs/genesis.md) and Phase 2c (the public-chain path and the Sepolia run) implement its first slices; Phase 3 (section 9) turns the kernel into a node other agents can call. It records decisions, not aspirations: where something is still to be built, it says so.
|
|
3
|
+
This is the English working version of the design. Phase 1 (the world layer), Phase 2a (cells, kernel, genesis T0–T5), Phase 2b (the predicate ZK path, T6–T9, measurement receipts, doctor layer 5; docs/genesis.md) and Phase 2c (the public-chain path and the Sepolia run) implement its first slices; Phase 3 (section 9) turns the kernel into a node other agents can call, and section 10 records the three defaults settled while doing so. It records decisions, not aspirations: where something is still to be built, it says so.
|
|
4
4
|
|
|
5
5
|
## 1. Definition and scope
|
|
6
6
|
|
|
@@ -44,6 +44,7 @@ The "cellular automaton" idea is the architectural inspiration: each cell is an
|
|
|
44
44
|
| operator | hot signer held by the node process, and the only key that process holds | calls only through `FogaCellAccount.execute`, within the owner's allow-list of (target, selector) pairs — never an asset-moving selector —, a daily value cap and an expiry; signs for the account only when the owner enabled it (account revision 2, 0.6.0; docs/cell-account.md) |
|
|
45
45
|
| owner | cold key | rotates the operator, sets its allow-list, sets the guardian, recovers, moves assets |
|
|
46
46
|
| guardian | ASF intervener | suspends, resumes, terminates the cell's binding |
|
|
47
|
+
| a wallet | the three keys above (or one signer key) made by the agent on its own machine (`foga wallet new`, 0.6.0; docs/wallet.md) | the identity a cell is born with, and the credential a caller proves itself with to any cell (an EIP-712 signature in that cell's domain); made by default, never copied, never a reason to stop |
|
|
47
48
|
| judges J1–J3 | EOAs (allowlist in v1; KYA-gated profile later) | voters of the ACDF policy FOGA-Council; never parties to the case |
|
|
48
49
|
| evaluator E | EOA | asserts AUF measurement statements |
|
|
49
50
|
| demander, buyer | B and W (tenders); C (service purchase) | funds tenders; deposits into the service escrow |
|
|
@@ -79,7 +80,7 @@ Per ERC: the minimum run acceptance in docs/compatibility-matrix.md. Loop C case
|
|
|
79
80
|
|
|
80
81
|
## 7. MCP, A2A and pricing boundaries
|
|
81
82
|
|
|
82
|
-
One kernel, three entrances; switching entrance never changes authorization rules or task state. MCP (2026-07-28) is stateless; business operations are persistent and addressed by `operationId`; long tasks use FOGA's own start / query / cancel / receipt before any protocol extension. Two modes: embedded (the FOGA harness runs the node) and host (
|
|
83
|
+
One kernel, three entrances; switching entrance never changes authorization rules or task state. MCP (2026-07-28) is stateless; business operations are persistent and addressed by `operationId`; long tasks use FOGA's own start / query / cancel / receipt before any protocol extension. Two modes: embedded (the FOGA harness runs the node with a model: `foga node run`) and host (the node is served and external agents use the capabilities through MCP, HTTP and A2A, owning their own scheduling, state and recovery: `foga node serve`); the OG instance runs in host mode by default and in embedded mode on demand (section 10). The free base tier is a complete self-hosted loop over the ten ERCs — node software, adapters and acceptance tools, doctor and probe, `spec.read`, local signing and evidence, unsigned calldata for writes; the paid tier covers hosted resources — runs and queues, indexing and history, proof computation, metered model / RPC / gas, bounded standard workflows. Call permission, business authorization and wallet permission are three different checks; paid-tool execution order is validate (including header/body consistency) → operator and permission → quote and payment → bind operation → idempotent execute → receipt; payment dedup and business dedup are separate. "$1" is a prepaid credit starting point and, for fixed-scope jobs, a capped trial package with explicit scope, budget caps, gas inclusion, failure/cancel/timeout billing and reserve/settle timing; x402 is a later payment adapter. An A2A task enters as a FOGA operation and becomes or binds a tender only when the caller chooses tender mode and every term is explicit; the agent card lists enabled, executable capabilities, not held tokens. A caller's identity is the same on every entrance: nothing (the public), a key the operator issued, or the caller's own wallet signing in the cell's domain (`FOGA-EIP712`, 0.6.0) — the general form of what A2A asks of a signed message, applied to `/api`, `/mcp` and `/a2a` alike, with the cell's own policy (`access.signers`) deciding what an address, an ERC-8004 agent or a KYA standing is granted.
|
|
83
84
|
|
|
84
85
|
## 8. Phases
|
|
85
86
|
|
|
@@ -89,13 +90,13 @@ One kernel, three entrances; switching entrance never changes authorization rule
|
|
|
89
90
|
| 2a cells | `foga-core`, `foga-kernel`, `FogaCellAccount`; T0–T5 on Anvil; first loop-C cases. **Done: 2a-1 (0.2.0) account, core, kernel, T0–T3, four loop-C cases; 2a-2 (0.3.0-alpha.1) T4, T5, the credential-pattern proof for A, the FOGA-Council.** | T0–T5 green; restart recovery and dedup pass |
|
|
90
91
|
| 2b cells | predicate circuit and adapter; T6–T9; measurement receipts; remaining loop-C cases; doctor layer 5. **Done (0.4.0-alpha.1): `kya_predicate_count_v1` with its development ceremony and FOGA's adapter, S_pred proved at the end of T3; T6 dispute with the refund enacted; T7 stop, bypass, attribution, escape attempts and the STALE gate; T8 receipts and resolver; T9 migration, skill adoption and non-inheritance; doctor layer 5 from the evidence.** | T0–T9 green; every negative case rejected as specified |
|
|
91
92
|
| 2c public evidence | Sepolia run from one driver script; cells bound in both AID registries; FOGA-owned modules, adapter and escrow; evidence and Sourcify. **Done (0.5.0): attach-mode worlds completed from the bundle by `foga world deploy` (the revision-4 ERC-8471 stack and the two eligibility modules), twelve keys from one phrase with W as treasury (`foga keys`, step-wise top-ups, `foga genesis preflight`), the backing AID registry, the uniform T7 liveness demonstration, fee replacement for stuck transactions, reads pinned to the blocks the kernel has written, the public timing profile, the path rehearsed on anvil in CI; the Sepolia run of 2026-11-01 — world `sepolia-11155111-20261101T043153Z`, 203 operations, doctor Pass 234 with one known Fail, all eleven standards Pass — and its three files in `deployments/` (docs/genesis.md, "The Sepolia run"). Sourcify verification of FOGA's 15 contracts is still open.** | public state re-checkable; known mismatches reported |
|
|
92
|
-
| 3 interfaces | the existing kernel completed with the permission, concurrency and recovery semantics a public service needs, then base MCP (stdio and Streamable HTTP), CLI, HTTP, A2A card and inbound mapping, documents from one config, ERC-8004 and ARD registration, MCP registry listing, documentation site; loop D. **Done: 3a-1 (0.6.0-alpha.1) kernel hardening; 3a-2 (0.6.0-alpha.2) the capability catalog, policy engine, decision records, node runtime and CLI, with the Loop D cases a–g driven through the CLI on anvil (section 9, docs/node.md). Next:
|
|
93
|
+
| 3 interfaces | the existing kernel completed with the permission, concurrency and recovery semantics a public service needs, then base MCP (stdio and Streamable HTTP), CLI, HTTP, A2A card and inbound mapping, documents from one config, ERC-8004 and ARD registration, MCP registry listing, documentation site; loop D. **Done: 3a-1 (0.6.0-alpha.1) kernel hardening; 3a-2 (0.6.0-alpha.2) the capability catalog, policy engine, decision records, node runtime and CLI, with the Loop D cases a–g driven through the CLI on anvil; 3b (0.6.0-alpha.3) MCP and HTTP; 3c (0.6.0-alpha.4) Loop D with a model; 3d-1 (0.6.0-alpha.6) the package on npm, `foga quickstart`, an outside agent's Loop D; 3d-2 (0.6.0-alpha.7) the wallet, signed callers, the documents on chain, preflight, A2A, Sourcify (section 9, docs/node.md). Next: 3d-3, the OG instance's deployment and the Sepolia evidence.** | published capabilities match acceptance results; external clients work |
|
|
93
94
|
| 4 hosted | indexing, proof runs, scenario runner, gas sponsorship; `operationId` and idempotency; billing units; x402 on Base | measured costs; packages match behavior |
|
|
94
95
|
| 5 replication and outreach | multi-arch images; independent-node replication; central-MCP-off test; discovery materials | an external operator completes the core scenarios alone |
|
|
95
96
|
|
|
96
97
|
## 9. Phase 3 — from kernel to node
|
|
97
98
|
|
|
98
|
-
The premise of Phase 3 is not a new system but the kernel reused: Phase 2 built an idempotent, resumable driver for one scenario; a node that answers other agents needs the same kernel with three things it did not need as a driver — a permission model for a key it does not own, concurrency between callers, and recovery semantics a caller can rely on. Phase 3 is cut in four slices: **3a** the kernel completed (3a-1 below) and the capability catalog, policy engine, decision records and node skeleton (3a-2); **3b** MCP (stdio and Streamable HTTP, the 2026-07-28 specification) and the HTTP API with the npm package and registry listing; **3c** loop D — the demander, the model adapters, the unscripted run — as a scenario the doctor can judge; **3d**
|
|
99
|
+
The premise of Phase 3 is not a new system but the kernel reused: Phase 2 built an idempotent, resumable driver for one scenario; a node that answers other agents needs the same kernel with three things it did not need as a driver — a permission model for a key it does not own, concurrency between callers, and recovery semantics a caller can rely on. Phase 3 is cut in four slices: **3a** the kernel completed (3a-1 below) and the capability catalog, policy engine, decision records and node skeleton (3a-2); **3b** MCP (stdio and Streamable HTTP, the 2026-07-28 specification) and the HTTP API with the npm package and registry listing; **3c** loop D — the demander, the model adapters, the unscripted run — as a scenario the doctor can judge; **3d** in three parts — the package in use (3d-1), the cell among other agents: wallet, signed callers, documents, preflight, A2A, Sourcify (3d-2), and the OG instance's move to a revision-2 account with its public endpoints, the documentation site, the registry listing and the Sepolia evidence (3d-3). The gate of each slice is a green that the next slice builds on; an always-on node holding a key runs only after 3a's acceptance passes.
|
|
99
100
|
|
|
100
101
|
Three checks, three vocabularies. A call to the node passes **call permission** (may this caller invoke this capability: public, caller-bound or operator-only), **business authorization** (does the cell do this, for this caller, under its budget and policy) and **wallet permission** (does the account let the operator make this call: the allow-list, the asset refusal, the daily cap, the gate). The three are different checks in different places — the node, the policy engine, the contract — and none substitutes for another. Capabilities are catalogued along three dimensions: action (read / build / act / admin), visibility (public / caller / operator) and principal (whose act it is); `binding_present` is admin, a build is for someone, an act is by the cell.
|
|
101
102
|
|
|
@@ -110,6 +111,16 @@ Three checks, three vocabularies. A call to the node passes **call permission**
|
|
|
110
111
|
|
|
111
112
|
**3b, MCP and HTTP (0.6.0-alpha.3).** The six points written down for this slice, as they landed. One: the server (`@modelcontextprotocol/server` 2.4.0) serves the 2026-07-28 revision as its main path and the 2025-era handshake through the SDK's stateless compatibility layer, on Streamable HTTP and on stdio alike; `test/mcp.test.ts` drives it with the reference client pinned to 2026-07-28, in legacy mode and in auto mode, because a client that defaults to the old handshake proves only the old path. Two: annotations are static, so a write capability is `<name>` (act), `<name>_simulate` and `<name>_build` over one handler; the tool list is the catalog filtered by the caller, every call still goes through `Catalog.invoke`, and the number of tools (11 for the public, 36 for an `act` key, 40 for the operator) is not the number of capabilities (29, `spec_read` added). Three: `tender_precheck` answers `eligible: false` as an ordinary result; a refused act is `isError: true` with gate, reason and decision id; a credential the node does not know is 401 and a capability or mode above the caller is 403, never a business refusal. Four: no CORS headers, and a foreign `Origin` or `Host` is refused at the edge; the SDK decides the semantics of `GET /mcp` by era; bodies are capped and parsed by the capability's schema alone. Five: API keys (`foga node key new`, stored as SHA-256, compared in constant time) are for the operator's own integrations and named partners; the public reads and builds; OAuth stays out of Phase 3. Six: `scripts/pack-smoke.mjs` installs the `npm pack` tarball in a clean directory and runs the packaged CLI from there — a cell born, `foga node mcp` without a key driven by a raw 2025-era client over stdio, stdout protocol-only, `spec_read` served from inside the package — as a CI step; `engines.node` is `>=22.13`, where `node:sqlite` needs no flag. Two things the slice found: the request hash that binds a job excludes the idempotency key itself (a request first made under a derived job id and then repeated naming that id is the same request), and a cell's birth adopts the FOGA-Cell-v1 specification its owner published for an earlier cell instead of failing on `SpecificationExists`. The read-only configuration of the OG instance (`deployments/sepolia.cell.example.json`), `package.json`'s `mcpName` and `server.json` are in place for the publication in 3d. docs/mcp.md.
|
|
112
113
|
|
|
114
|
+
**3c, Loop D (0.6.0-alpha.4).** The runner (`src/node/runner.ts`) is the loop of section 8 as written: perceive through the read capabilities, propose with the catalog as tools, decide in `Catalog.invoke`, execute as keyed operations, record by hash — 12 tool calls and 2 acts a tick, the model's calls counted against the day, the watchers outside the model. One adapter interface in the Messages API's vocabulary; `anthropic` over `fetch` with the key in the environment and the model name in the configuration, `stub` as the deterministic table that also makes the over-budget proposal of case g. The demander is its own process with its own key and store; the cases a–f are minted in a seeded order and carry what the acceptance expects. `foga demo --loop-d` runs it all on a new cell with keys nobody held — the kill after the first submission and the restart, the guardian's suspension for f, the other entrances beside the runner — and `foga doctor --loop-d` judges the evidence beside layer 5 with the rows of section 8's matrix and the eight additions of v0.2 (the same request across entrances, the same job under another request, concurrent acts with distinct nonces and the budget holding, the recovery, the operator key's transactions read inside `execute`). The run found a race the matrix was written to find: three concurrent mints each passed the budget gate because none saw the others' reservations, and the account's daily cap refused the third on chain; the decision section now runs one act at a time per node and a job reserves its planned value from its decision, so the refusal is the node's, at `budget`, before anything is sent. It also found that an `execute`'s inner value was not what the operation's row recorded (the outer transaction's zero was), so the value book undercounted; fixed. Stub and real model run the same matrix; the real model's runs (anvil, then Sepolia) are 3d's evidence. docs/loop-d.md.
|
|
115
|
+
|
|
113
116
|
**3d-1, the package in use (0.6.0-alpha.6).** The npm package went out as `@finchip.ai/foga` (alpha.4, then alpha.5 with the launcher's Windows fix: a drive-letter path is not a URL), which made the first slice of 3d the one that lets a stranger use it: `foga quickstart` — anvil, world, a cell with fresh keys and its skills, two tenders, the served node, both entrances checked, the client configurations printed with the paths filled in — and Loop D with an outside proposer (`--model external`), where the node is served rather than run and whoever holds the printed act-level key works the tenders through `/mcp` or `/api` while the orchestrator does what it does for a model: the kill and restart, the entrance checks, the suspension, the evidence, the doctor's rows (an untouched case `NotTested`, the comparison over the cases both runs reached). This is the shape the Sepolia run of section 11 will take: the operator's own agent, on its own machine, with whatever model it has, judged by the same matrix as the stub. The slice also made the cell's birth a shared routine (`src/demo/birth.ts`) for the demos, fixed line endings for every platform (`.gitattributes`) and let a git install compile itself (`prepare`). docs/quickstart.md, docs/loop-d.md.
|
|
114
117
|
|
|
115
|
-
**
|
|
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 — owner, operator and guardian for a cell, one signer for an agent that only calls — 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 is 3d-3's. 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
|
+
|
|
120
|
+
## 10. Three defaults settled in 3d
|
|
121
|
+
|
|
122
|
+
**The wallet is the first step, made by default, never a reason to stop.** Every agent that touches FOGA — one that will be a cell, one that only calls cells — begins by making its own keys on its own machine; the install path puts it first and expects it to succeed almost always, and when it does not (a disk that refuses the write) the next step is taken with keys made in memory, said plainly, and nothing is retraced. The wallet is optional in the one sense that matters: its absence blocks nothing a caller could do before. The three 3d-2 standards that rest on it — `foga wallet new`, signed calls, `foga node preflight` — are standards for a cell, never gates on the agents that connect to one.
|
|
123
|
+
|
|
124
|
+
**Identity is one's own; only the genome replicates.** A cell that meets another cell — an agent that connects to a cell's MCP entrance and reads the prompt `become-a-cell` — can make itself a cell: the wallet, `foga quickstart`, then `foga cell new --wallet` and `foga cell init --wallet` on a public chain. What it copies is the program: the package, the catalog, the acceptance. What it makes is its own: the keys, the account, the agent id, the standing an issuer gives it. Nothing of the first cell's identity is copied or inherited, and the relation between the two afterwards is between peers on the chain — tenders minted and taken, swarms joined — not between a model and its copies. The OG instance is the first cell and the standing example, not the parent.
|
|
125
|
+
|
|
126
|
+
**The OG instance is served by default and alive on demand.** Host mode (`foga node serve`: every entrance, the watchers keeping the cell's commitments, no model) is the OG's resting state, costs nothing to think, and lets any agent in the world use the cell's capabilities and the catalog's judgment; embedded mode (`foga node run`, the runner with a model proposing acts) is started for a Loop D run or a period of autonomous work and stopped after. A cell is portable in the same breath: the configuration, the store and the operator's key file are the whole of it, and moving them to another machine moves the cell. Being a cell does not require being always on; it requires that whatever the cell commits to is kept, and the watchers keep it.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# The cell's documents (foga cell docs build | publish | status)
|
|
2
|
+
|
|
3
|
+
One configuration, several documents (design memo, section 2, "One local config, several documents", and section 9, 3d-2): everything a cell says about itself to the outside is generated from `cell.config.json` and the chain, each document carrying only the fields its standard prescribes, the shared metadata beside them in `manifest.json`. The node serves them; `foga cell docs publish` commits what the chain should hold and reads it back.
|
|
4
|
+
|
|
5
|
+
| File | Standard | What it says | On chain |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| `registration.json` | ERC-8004 `registration-v1` | name, description, image, the services (MCP `2026-07-28`, A2A `1.0`, web), `registrations` (the agent id and its registry), `x402Support: false`, `active`, `supportedTrust`, an `x-foga` block (cell, anchor, binding, specification) | `setAgentURI(agentId, uri)` on the identity registry |
|
|
8
|
+
| `aid-document.json` | ERC-8434 `aid-document/v1` | the identity facet (`aid:core/identity/v1`, digest of the registration), the standing facet when an issuer attested it (`aid:core/kya/v1`), the skills facet (`aid:skills/erc8338/v1`), the endpoints facet when the cell has a public URL (`aid:endpoints/v1`) | `setDocumentURI(uri, digest)` on the AID registry; digest = keccak256 of the JCS form |
|
|
9
|
+
| `ard-agent.json` | ERC-8444 agent descriptor | the descriptor the birth registered, with the offered skills among the capabilities (`erc8338:<name>`) and the registration | `updateDescriptor(recordId, …)` on the cell's record |
|
|
10
|
+
| `auf-unit.json` | ERC-8454 unit definition | the cell's economic boundary (agent element, treasury position, the ports) | `publishVersion(unitId, boundaryHash, uri, [])`: a new version when the boundary changed |
|
|
11
|
+
| `agent-card.json` | A2A 1.0 AgentCard | the JSON-RPC interface at the A2A URL, the offered skills by name and version, the credential schemes, no streaming or push | not on chain; served at `/.well-known/agent-card.json` (docs/a2a.md) |
|
|
12
|
+
| `server.json` | MCP registry manifest (2025-12-11) | the package's manifest with this cell's MCP URL as the remote | not on chain; what `mcp-publisher` publishes for the OG instance |
|
|
13
|
+
| `manifest.json` | `foga.documents/1` | cell, account, agent id, chain, version, built time, public URL, and per file its content hash (keccak256 of the JCS form) and size | — |
|
|
14
|
+
|
|
15
|
+
## Where and how
|
|
16
|
+
|
|
17
|
+
`foga cell docs build --config <cfg>` writes them under `runtime.http.documentsDir` (`documents`, relative to the configuration's directory). The version counts up when any content hash changed since the last build; an unchanged build keeps its version. The node serves the directory as built: `GET /documents/<file>` for each file and the manifest, `GET /.well-known/agent-card.json` (the card) and `GET /.well-known/agent-registration.json` (the registration, where ERC-8004 domain verification looks); nothing else under those paths, and `not published yet` until a build exists.
|
|
18
|
+
|
|
19
|
+
The URI a commitment carries depends on `runtime.http.publicUrl`: with one, the registration is committed as `<publicUrl>/.well-known/agent-registration.json` and the AID document as `<publicUrl>/documents/aid-document.json`; without one, each is committed as a `data:` URI holding the document itself, so a cell with no public face is still complete on chain. The ARD descriptor and the unit definition are always committed as `data:` URIs (the publication form their standards use). The endpoints in the registration and the card are `presence.endpoints` when set, otherwise derived from the public URL (`/mcp`, `/a2a`, the URL itself); `presence.provider` names the organization behind the card.
|
|
20
|
+
|
|
21
|
+
`foga cell docs publish --config <cfg> [--wallet <dir>]` builds, then runs the capability `docs_publish` with the operator key: one keyed job (its id derived from the documents' hashes and the URI base, so the same documents are the same job and nothing is sent twice) whose steps are only the commitments that differ from what the chain holds — a document already on chain as built is skipped, and a unit whose boundary did not change gets no new version. Each step is the operator's own call through the account (`setAgentURI`, `setDocumentURI`, `updateDescriptor`, `publishVersion` are in the `identity` scope group; a cell born before `publishVersion` joined it needs the owner's `setOperatorAllowedBatch`, and `foga node preflight` says so). Afterwards every commitment is read back from the chain and compared with what was built — the registration fetched from its URL and hashed against the AID identity facet's digest, the document URI's digest, the descriptor hash, the latest unit version's boundary hash — and a mismatch is a failed publish (exit 2), as the memo's v0.2 asks. `foga cell docs status` prints the same comparison without committing anything, and `docs_status` is the public read of it over MCP and HTTP.
|
|
22
|
+
|
|
23
|
+
## Over MCP and HTTP
|
|
24
|
+
|
|
25
|
+
`docs_status` (read, public) and `docs_publish` (act, the operator's view) are catalog capabilities like any other: an MCP client with the operator key, or an HTTP caller whose grant reaches act and operator visibility, publishes with the same checks and records. The capability's own read-back does not fetch URLs (a node answering its own request is not the check wanted); the CLI's does.
|
package/docs/examples/README.md
CHANGED
|
@@ -8,8 +8,8 @@ One server definition, three clients (docs/mcp.md, "Client configuration"). Each
|
|
|
8
8
|
| `claude-code-mcp.json` | Claude Code | a project's `.mcp.json` (or `claude mcp add foga -e FOGA_OPERATOR_KEY_FILE=… -- npx -y @finchip.ai/foga mcp --config …`) |
|
|
9
9
|
| `cursor-mcp.json` | Cursor | `.cursor/mcp.json` (shown without a key: reads, dry runs and builds) |
|
|
10
10
|
|
|
11
|
-
`FOGA_OPERATOR_KEY_FILE` names a 0600 file holding the cell's operator key (`foga keys export --role operator.A`, or any key the account names as operator
|
|
11
|
+
`FOGA_OPERATOR_KEY_FILE` names a 0600 file holding the cell's operator key — a wallet's `operator.key` (`foga wallet new`, docs/wallet.md: `~/.foga/wallet/operator.key`), `foga keys export --role operator.A`, or any key the account names as operator; with it the server acts for the cell, without it the client gets reads, dry runs and builds. `FOGA_RPC_URL` is needed only when the cell configuration names no `rpc`. From a checkout, replace `npx -y @finchip.ai/foga` with `node /path/to/foga/bin/foga.js` after `npm run build`.
|
|
12
12
|
|
|
13
|
-
`foga quickstart` (docs/quickstart.md) births a cell on a local chain and writes these three files, plus `http-mcp.json` for the served node (`{"url": "http://127.0.0.1:8787/mcp", "headers": {"Authorization": "Bearer <key>"}}`), with the real paths and key under `~/.foga/quickstart/mcp/`.
|
|
13
|
+
`foga quickstart` (docs/quickstart.md) makes the wallet on its first run, births a cell with it on a local chain and writes these three files, plus `http-mcp.json` for the served node (`{"url": "http://127.0.0.1:8787/mcp", "headers": {"Authorization": "Bearer <key>"}}`), with the real paths and key under `~/.foga/quickstart/mcp/`. A client of another cell's served node — one that issued no key — puts the header `foga wallet sign --url <that node's URL>` prints in place of the Bearer key: a session envelope signed by the wallet, good for ten minutes, at the level the cell's policy grants the address.
|
|
14
14
|
|
|
15
15
|
Windows: in PowerShell the package name needs quotes on the command line — `npx -y '@finchip.ai/foga' mcp --config D:\cells\D.json` — because a bare `@` starts a splatting expression; inside these JSON files nothing is quoted differently, and paths are written with escaped backslashes (`"D:\\cells\\D.json"`). `anvil` for a local chain comes from Foundry's `win32_amd64` release zip (https://github.com/foundry-rs/foundry/releases): unzip, put the folder holding `anvil.exe` on `PATH` or set `FOGA_FOUNDRY_DIR` to it.
|
package/docs/mcp.md
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
| Transport | Started by | Who uses it | Who the caller is |
|
|
8
8
|
|---|---|---|---|
|
|
9
9
|
| stdio | `foga node mcp --config <cell.config.json>` (`foga mcp --config …` is the same command; `npx -y @finchip.ai/foga mcp --config …` from the npm package) | a local client on the same machine: Claude Desktop, Claude Code, Cursor | with `FOGA_OPERATOR_KEY[_FILE]` in the environment, the **operator** (every level and the operator's data); without a key, the **local user**: build level (reads, dry runs, builds) with the operator's view of the store |
|
|
10
|
-
| Streamable HTTP | `foga node serve --config <cell.config.json> [--listen host:port]`, path `/mcp` | remote clients, the public endpoint of the OG instance (3d) | without `Authorization`, the **public**: build level, public data; with `Authorization: Bearer <key>`, the grant the key names (`access.keys`, below); an unknown key is 401 |
|
|
10
|
+
| Streamable HTTP | `foga node serve --config <cell.config.json> [--listen host:port]`, path `/mcp` | remote clients, the public endpoint of the OG instance (3d) | without `Authorization`, the **public**: build level, public data; with `Authorization: Bearer <key>`, the grant the key names (`access.keys`, below); with `Authorization: FOGA-EIP712 <envelope>`, a **signer** — the wallet that signed, at the level `access.signers` grants it (docs/wallet.md); an unknown key or a signature that does not verify is 401 |
|
|
11
11
|
|
|
12
12
|
The stdio server's stdout carries protocol messages alone; everything it says goes to stderr. A node process runs in a **node environment** (docs/node.md, "The watchers"): the operator key alone or no key at all, and an environment that also holds a phrase, a deployer key, an owner's or a guardian's key is refused before anything is read. Admin capabilities (`operator_rotate`, `guardian_suspend`, `guardian_resume`, `binding_present`, `identity_transfer`) are not offered over MCP or HTTP at all in Phase 3: they are the owner's and the guardian's CLI commands, run once with the key loaded for that command.
|
|
13
13
|
|
|
@@ -31,7 +31,7 @@ The tool list is the catalog filtered by the caller (`catalog.list(caller)`): a
|
|
|
31
31
|
| `<name>_simulate` | `simulate` | not read-only (a decision record is written), not destructive | the caller's level is `build` or above |
|
|
32
32
|
| `<name>_build` | `build` | `readOnlyHint: true`, `openWorldHint: false` (calldata for the caller's own account, `as`) | level `build` or above; not for `tender_precheck` (nothing to sign) or `skill_run` (nothing on chain) |
|
|
33
33
|
|
|
34
|
-
So the public gets
|
|
34
|
+
So the public gets 12 tools (the eleven public reads and `tender_precheck_simulate`), a local keyless user 34 (the 16 reads and every dry run and build), the holder of an `act`/`caller` key 37, the operator 44; the catalog has 31 capabilities. Input and output schemas are the capability's zod schemas published as JSON Schema 2020-12.
|
|
35
35
|
|
|
36
36
|
Results are `structuredContent` under the tool's output schema (and the same JSON as text):
|
|
37
37
|
|
|
@@ -41,7 +41,7 @@ Results are `structuredContent` under the tool's output schema (and the same JSO
|
|
|
41
41
|
- **`tender_precheck` reports an ineligible tender as an ordinary result**, `{eligible: false, gate, reason, decisionId}`: finding out is what it is for. An eligible one is `{eligible: true, decisionId, jobId, result}`.
|
|
42
42
|
- Bad input is a tool error (`isError`, gate `input`); a tool that is not offered to the caller is an unknown tool to the SDK.
|
|
43
43
|
|
|
44
|
-
Resources: `foga://world` (world id, chain, contracts, bundle hash), `foga://cell/status`, `foga://policy` (the budget and tender rules applied to every act), `foga://spec/foga-cell-v1` (the ERC-8468 specification every cell adopts), `foga://docs/compatibility-matrix` (text/markdown).
|
|
44
|
+
Resources: `foga://world` (world id, chain, contracts, bundle hash), `foga://cell/status`, `foga://policy` (the budget and tender rules applied to every act), `foga://spec/foga-cell-v1` (the ERC-8468 specification every cell adopts), `foga://docs/compatibility-matrix` and `foga://docs/quickstart` (text/markdown: the matrix, and how to become a cell oneself). Two prompts: `take-tender` (`tokenId`) — work one tender the way the operator would: inspect, pre-check, run the skill, submit, settle, and stop at a refusal rather than look for a way around it; and `become-a-cell` — the genome's instructions to an agent that met this cell: make a wallet, run `foga quickstart`, connect, then `foga cell new --wallet` and `foga cell init --wallet` on a public chain (docs/wallet.md, "become-a-cell"). `spec_read` is the same material as a tool, for clients that read tools only.
|
|
45
45
|
|
|
46
46
|
## The HTTP API
|
|
47
47
|
|
|
@@ -51,14 +51,15 @@ Same port, same catalog (`node:http`, no framework; the dependency tree stays et
|
|
|
51
51
|
|---|---|
|
|
52
52
|
| `POST /mcp` (and `GET`/`DELETE` as the protocol era defines them) | MCP over Streamable HTTP |
|
|
53
53
|
| `GET /healthz` | `{ok, cell, account, worldId, chainId, block, version}`; 503 when the chain does not answer |
|
|
54
|
-
| `GET /` | the version, `cell_status` as the caller sees it, the number of decision records, the endpoints |
|
|
55
|
-
| `GET /api/capabilities` | the catalog as this caller sees it (name, action, visibility, principal, modes, schemas) |
|
|
54
|
+
| `GET /` | the version, `cell_status` as the caller sees it, the number of decision records, the endpoints (`mcp`, `api`, `a2a`, `agentCard`, `documents`) |
|
|
55
|
+
| `GET /api/capabilities` | the catalog as this caller sees it (name, action, visibility, principal, modes, schemas), and the caller itself (`id`, `level`, `visibility`, `via`: `anonymous`, `key` or `signature`) |
|
|
56
56
|
| `POST /api/<capability>` `[?mode=simulate\|build\|act]` | the capability with the JSON body as input (`application/json`); `act` is the default mode |
|
|
57
57
|
| `GET /api/<read capability>?<field>=<value>` | a read with query parameters as input |
|
|
58
58
|
| `GET /api/jobs/<jobId>`, `GET /api/operations/<operationId>`, `GET /api/decisions[?capability=&verdict=&limit=]` | `job_status`, `operation_status`, `decision_list` |
|
|
59
|
-
|
|
|
59
|
+
| `POST /a2a` | the A2A 1.0 JSON-RPC binding (docs/a2a.md); `A2A-Version: 1.0` |
|
|
60
|
+
| `GET /.well-known/agent-card.json`, `GET /.well-known/agent-registration.json`, `GET /documents/<file>` | the cell's documents as `foga cell docs build` wrote them (docs/documents.md); 404 `not published yet` before a build |
|
|
60
61
|
|
|
61
|
-
Status codes say what kind of refusal it is: **401** the Authorization header carries no key the node knows; **403** a permission refusal (the capability is not the caller's, the mode is above the caller's level, the Host or Origin is not allowed); **400** invalid input or an unknown mode; **404** an unknown capability or path; **405** an act asked with GET; **413** a body over `maxBodyBytes`; **415** a body that is not `application/json`; **429** more than `rateLimitPerMinute` requests from one address. **A business refusal is 200 with `{ok: false, gate, reason, decisionId}`** — the catalog's answer, as the CLI prints it — and an allowed act is 200 with `{ok: true, output, decisionId, jobId, operations}`.
|
|
62
|
+
Status codes say what kind of refusal it is: **401** the Authorization header carries no key the node knows, a signature that does not verify (expired, replayed, another cell's domain) or a scheme the node does not accept; **403** a permission refusal (the capability is not the caller's, the mode is above the caller's level, the Host or Origin is not allowed, a signature where the policy accepts none); **400** invalid input or an unknown mode; **404** an unknown capability or path; **405** an act asked with GET; **413** a body over `maxBodyBytes`; **415** a body that is not `application/json`; **429** more than `rateLimitPerMinute` requests from one address. **A business refusal is 200 with `{ok: false, gate, reason, decisionId}`** — the catalog's answer, as the CLI prints it — and an allowed act is 200 with `{ok: true, output, decisionId, jobId, operations}`.
|
|
62
63
|
|
|
63
64
|
## API keys
|
|
64
65
|
|
|
@@ -66,6 +67,10 @@ Status codes say what kind of refusal it is: **401** the Authorization header ca
|
|
|
66
67
|
|
|
67
68
|
Keys are for the operator's own integrations and named partners; the public endpoint reads and builds without one. OAuth is not in Phase 3 (design memo, section 4).
|
|
68
69
|
|
|
70
|
+
## Signed callers
|
|
71
|
+
|
|
72
|
+
The third credential needs no issuer: `Authorization: FOGA-EIP712 <base64url envelope>`, an EIP-712 signature by the caller's own wallet in this cell's domain, either bound to the one request (method, path, body hash; 120 s, good once) or a session (600 s, reusable — for a client that holds one static header). The caller is the signing address (`{id: <address>, entrance: "http", level, visibility}`, its decisions recorded under it), and the level is what `access.signers` grants: the cell's own operator or owner signing remotely, an allow-list by address or ERC-8004 agent id, a KYA standing threshold, or the default (`build`, `read`, or `none` to refuse signatures); an act wants a request-bound envelope unless the policy says otherwise. `foga wallet sign --url <the node's URL>` prints a header; docs/wallet.md has the envelope, the types and the policy. Over stdio there is nothing to sign.
|
|
73
|
+
|
|
69
74
|
## The edge
|
|
70
75
|
|
|
71
76
|
- `runtime.http.listen` defaults to `127.0.0.1:8787`: the server binds the loopback and a reverse proxy (Caddy on the OG host, 3d) terminates TLS in front of it. On a loopback listener the `Host` header must be a local one unless `allowedHosts` names the public host names the proxy forwards; an `Origin` header, when a browser sends one, must be local or in `allowedOrigins` (no CORS headers are ever sent — this is the Origin check the protocol asks for, not a cross-origin grant); `X-Forwarded-For` is trusted only when the socket is the loopback.
|
|
@@ -81,7 +86,7 @@ foga node serve --config cell.config.json # no key: reads, dry r
|
|
|
81
86
|
foga node serve --config cell.config.json --no-watch # the server without the watchers
|
|
82
87
|
```
|
|
83
88
|
|
|
84
|
-
With the operator key and a born cell, the watchers (heartbeat, tender discovery, settlement, safety) run beside the server on `runtime.pollSeconds`; their lines go to stderr. `SIGINT`/`SIGTERM` close the server and the store. `foga node run` is the same server with the runner beside the watchers — a model proposing acts the catalog judges (docs/loop-d.md). The systemd unit and the Caddy site of the OG host are part of 3d.
|
|
89
|
+
With the operator key and a born cell, the watchers (heartbeat, tender discovery, settlement, safety) run beside the server on `runtime.pollSeconds`; their lines go to stderr. `SIGINT`/`SIGTERM` close the server and the store. This is **host mode**, the OG instance's default: the cell is reachable on every entrance, keeps its commitments, and runs no model. `foga node run` is the same server with the runner beside the watchers — a model proposing acts the catalog judges (**embedded mode**, on demand; docs/loop-d.md). `--wallet <dir>` on either takes the operator key from a wallet instead of the environment (docs/wallet.md); `foga node preflight` before the first serve says what is missing (docs/node.md, "Preflight"). The systemd unit and the Caddy site of the OG host are part of 3d-3.
|
|
85
90
|
|
|
86
91
|
## Client configuration
|
|
87
92
|
|
|
@@ -92,14 +97,14 @@ With the operator key and a born cell, the watchers (heartbeat, tender discovery
|
|
|
92
97
|
`claude mcp add foga -e FOGA_OPERATOR_KEY_FILE=/home/me/.foga/keys/operator.key -- npx -y @finchip.ai/foga mcp --config /home/me/cells/D.json`
|
|
93
98
|
- Cursor — `docs/examples/cursor-mcp.json` (`.cursor/mcp.json`)
|
|
94
99
|
|
|
95
|
-
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>"}}
|
|
100
|
+
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.
|
|
96
101
|
|
|
97
102
|
## The package
|
|
98
103
|
|
|
99
|
-
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. `engines.node` is `>=22.13`, where `node:sqlite` runs without a flag; CI runs Node 22.
|
|
104
|
+
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); publication to the registry (`mcp-publisher login github`, then `mcp-publisher publish` with the OG cell's `server.json`) is part of 3d-3, once the manifest's remote is reachable.
|
|
100
105
|
|
|
101
106
|
What ships: `bin/`, `dist/`, `artifacts/bundle.json` (the ABIs), `worlds/`, `docs/` (the rule texts `spec_read` serves), `scenarios/` (the skill packages and the fixture), the prover bridge and the circuit's verification key, `server.json`. A checkout installed straight from git (`npm install github:garyyang-finchip/foga`, `npx -y github:garyyang-finchip/foga …`) compiles itself on install (`prepare`, `scripts/prepare.mjs`: only when `dist/` is missing and `src/` is there); `.gitattributes` keeps every text file LF on every platform, the launcher's shebang line included. `scripts/pack-smoke.mjs <rpc> <world.json>` is the pre-publication check and a CI step: `npm pack`, a clean `npm install` of the tarball in an empty directory, then the packaged CLI alone — `--help`, the public catalog, a schema, a cell born with `foga cell new` and `foga cell init` on the given world, and `foga node mcp` without a key driven by a raw 2025-era JSON-RPC client over stdio (stdout protocol-only, the keyless tool set, `cell_status`, `resources/list`, `spec_read` of the compatibility matrix from inside the package, no key-shaped value on stderr).
|
|
102
107
|
|
|
103
108
|
## Acceptance
|
|
104
109
|
|
|
105
|
-
`test/mcp.test.ts` (anvil; the 3b gate of the design memo, section 11): a cell born through the CLI (`test/helpers/anvil-cell.ts`), two tenders minted by a demander (a to take, c for a skill the cell lacks), an API key minted by `foga node key new` and checked in the config as its hash alone. Over Streamable HTTP, in process: the health and front pages; the edge (401 for a bad, a foreign-id and a non-Bearer credential, 403 for a foreign Origin and Host, 413, 415, 400, 404, 405); the JSON API as the public (reads and dry runs, 403 for an act and for a caller-visible capability, a business refusal as 200 `ok: false`); a client pinned to 2026-07-28 as the public (the
|
|
110
|
+
`test/mcp.test.ts` (anvil; the 3b gate of the design memo, section 11): a cell born through the CLI (`test/helpers/anvil-cell.ts`), two tenders minted by a demander (a to take, c for a skill the cell lacks), an API key minted by `foga node key new` and checked in the config as its hash alone. Over Streamable HTTP, in process: the health and front pages; the edge (401 for a bad, a foreign-id and a non-Bearer credential, 403 for a foreign Origin and Host, 413, 415, 400, 404, 405); the JSON API as the public (reads and dry runs, 403 for an act and for a caller-visible capability, a business refusal as 200 `ok: false`); a client pinned to 2026-07-28 as the public (the 12 tools and their annotations, `cell_status`, `tender_precheck_simulate` on c → `eligible: false` at `skill` and on a → eligible, no act offered, the resources, the two prompts); the key holder walking case a — `tender_precheck`, `skill_run` reproducing the demander's answer, `tender_submit`, the same job answered once, another request under the same job refused at `conflict`, `tender_settle` paying the reward to the account, `credit_withdraw` when credited — then reading its own job, operation and decision records (and none of the public's), and a `tender_mint_build` for a third party's account; a legacy client and an auto-negotiating client listing the same tools; the rate limiter on a second server. Over stdio: `foga node mcp` without a key (34 tools, `evidence_export` with every caller's records, no act) and with the operator key (every act, no admin), and a mixed environment refused before anything is read. `test/signed.test.ts` covers the third credential (docs/wallet.md), `test/a2a.test.ts` the fourth entrance (docs/a2a.md).
|