@fangorn-network/westmarch 2026.10.26-dev → 2026.10.27
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/package.json +2 -2
- package/src/agent/agent-card.js +12 -1
- package/src/agent/mcp.js +11 -5
- package/src/discover/apps.js +7 -1
- package/src/publish/ship.js +13 -1
- package/template/package.json +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fangorn-network/westmarch",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.27",
|
|
4
4
|
"description": "all-in-one builder for fangorn apps",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"@fangorn-network/markov": "^0.1.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@fangorn-network/sdk": "^2026.10.
|
|
48
|
+
"@fangorn-network/sdk": "^2026.10.26-dev",
|
|
49
49
|
"@huggingface/transformers": "^4.2.0",
|
|
50
50
|
"@noble/curves": "^1.9.1",
|
|
51
51
|
"viem": "^2.55.1"
|
package/src/agent/agent-card.js
CHANGED
|
@@ -48,6 +48,10 @@ const HEX32 = /^0x[0-9a-fA-F]{64}$/;
|
|
|
48
48
|
* tools, as the free tier's Worker does at /mcp.
|
|
49
49
|
* config the deployment; defaults to the SDK's, so the addresses in the
|
|
50
50
|
* card cannot drift from the ones `discoverApp` checks.
|
|
51
|
+
* skill `{url, sha256}`: the app's own agent skill (a SKILL.md), for agents
|
|
52
|
+
* that adopt it, so its behaviour travels with the app and not with a
|
|
53
|
+
* marketplace. The hash pins what the reader saw: an agent adopts it
|
|
54
|
+
* only on its user's yes, and asks again when the hash changes.
|
|
51
55
|
*/
|
|
52
56
|
export function agentCard({ name, description, url, version = "0.0.0", tools = [], tags = [], skills = [], fangorn, ...rest }) {
|
|
53
57
|
if (!name || !url) throw new Error("agentCard: name and url are required");
|
|
@@ -75,7 +79,7 @@ export function agentCard({ name, description, url, version = "0.0.0", tools = [
|
|
|
75
79
|
|
|
76
80
|
/** The params `discoverApp` reads, validated the same way it validates them.
|
|
77
81
|
* A malformed card should fail the build, not the first reader. */
|
|
78
|
-
export function fangornExtension({ app, fromBlock, repos = [], views = [], mcp, paid, access, config = BaseSepoliaConfig }) {
|
|
82
|
+
export function fangornExtension({ app, fromBlock, repos = [], views = [], mcp, paid, access, skill, config = BaseSepoliaConfig }) {
|
|
79
83
|
if (!app) throw new Error("fangorn.app is required");
|
|
80
84
|
const id = HEX32.test(app) ? app : hashAppId(app);
|
|
81
85
|
const from = String(fromBlock ?? "");
|
|
@@ -96,6 +100,8 @@ export function fangornExtension({ app, fromBlock, repos = [], views = [], mcp,
|
|
|
96
100
|
}
|
|
97
101
|
// `access`: a private app. Its view and tools need a pass (`pass_url`, `pass` in base units, for
|
|
98
102
|
// `days`) or one x402 payment of `call` per tool call; describe and the catalog are free.
|
|
103
|
+
if (skill && !(/^https?:\/\//.test(skill.url ?? "") && /^[0-9a-f]{64}$/.test(skill.sha256 ?? "")))
|
|
104
|
+
throw new Error(`fangorn.skill needs an http(s) url and the sha256 of the file, in hex: ${JSON.stringify(skill)}`);
|
|
99
105
|
if (access && !(/^https?:\/\//.test(access.pass_url ?? "") && /^\d+$/.test(String(access.pass)) && /^\d+$/.test(String(access.call))))
|
|
100
106
|
throw new Error(`fangorn.access needs pass_url, and pass and call in base units: ${JSON.stringify(access)}`);
|
|
101
107
|
return {
|
|
@@ -110,6 +116,7 @@ export function fangornExtension({ app, fromBlock, repos = [], views = [], mcp,
|
|
|
110
116
|
...(paid ? { paid: { url: paid.url, price: String(paid.price), asset: paid.asset, network: paid.network,
|
|
111
117
|
symbol: paid.symbol ?? "USDC", decimals: paid.decimals ?? 6, description: paid.description ?? "" } } : {}),
|
|
112
118
|
...(access ? { access } : {}),
|
|
119
|
+
...(skill ? { skill: { url: skill.url, sha256: skill.sha256 } } : {}),
|
|
113
120
|
},
|
|
114
121
|
};
|
|
115
122
|
}
|
|
@@ -148,6 +155,10 @@ if (typeof process !== "undefined" && import.meta.url === `file://${process.argv
|
|
|
148
155
|
throws(() => fangornExtension({ app: "a" }), /fromBlock/, "missing fromBlock");
|
|
149
156
|
throws(() => fangornExtension({ app: "a", fromBlock: 0, views: ["javascript:alert(1)"] }), /views/, "non-http view");
|
|
150
157
|
throws(() => agentCard({ name: "X" }), /url/, "missing url");
|
|
158
|
+
const sha = "a".repeat(64);
|
|
159
|
+
eq(fangornExtension({ app: "a", fromBlock: 0, skill: { url: "https://x.test/skill/a/SKILL.md", sha256: sha } }).params.skill, { url: "https://x.test/skill/a/SKILL.md", sha256: sha }, "a skill rides in the extension");
|
|
160
|
+
throws(() => fangornExtension({ app: "a", fromBlock: 0, skill: { url: "https://x.test/s.md" } }), /skill/, "a skill without its hash");
|
|
161
|
+
throws(() => fangornExtension({ app: "a", fromBlock: 0, skill: { url: "file:///etc/passwd", sha256: sha } }), /skill/, "a non-http skill");
|
|
151
162
|
|
|
152
163
|
console.log("agent-card.js self-check ok — tools captured without executing, extension matches the SDK's, bad params fail the build");
|
|
153
164
|
}
|
package/src/agent/mcp.js
CHANGED
|
@@ -24,7 +24,7 @@ import net from "node:net";
|
|
|
24
24
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { createInterface } from "node:readline";
|
|
27
|
-
import { APP_EXTENSION, listApps, toApp } from "../discover/apps.js";
|
|
27
|
+
import { APP_EXTENSION, listApps, skillOf, toApp } from "../discover/apps.js";
|
|
28
28
|
|
|
29
29
|
const log = (...a) => console.error("[fangorn-mcp]", ...a); // stdout is the protocol
|
|
30
30
|
// FANGORN_MCP_TRACE=1: step timings on stderr, for finding where an answer waits.
|
|
@@ -214,8 +214,9 @@ export function handlers(deps) {
|
|
|
214
214
|
if (!mcp) throw new Error(`${card.name ?? cardUrl} has no MCP server (its card has no \`mcp\`)`);
|
|
215
215
|
let s = slug(card.name ?? app);
|
|
216
216
|
while (open.has(s)) s += "-";
|
|
217
|
-
const
|
|
218
|
-
const
|
|
217
|
+
const ext = card.capabilities?.extensions?.find((e) => e.uri === APP_EXTENSION);
|
|
218
|
+
const access = ext?.params?.access ?? null;
|
|
219
|
+
const o = { slug: s, name: card.name, card: cardUrl, appId, skill: skillOf(ext), ...await deps.remoteTools(mcp, { access }) };
|
|
219
220
|
trace(`open ${app}: ready`);
|
|
220
221
|
open.set(o.slug, o);
|
|
221
222
|
deps.notify?.("notifications/tools/list_changed");
|
|
@@ -249,7 +250,10 @@ export function handlers(deps) {
|
|
|
249
250
|
"open-app": async ({ app }) => {
|
|
250
251
|
const o = await openApp(app);
|
|
251
252
|
return text({ app: o.slug, name: o.name, card: o.card,
|
|
252
|
-
call_as: `${o.slug}__<tool>, or call-app-tool`, tools: o.tools
|
|
253
|
+
call_as: `${o.slug}__<tool>, or call-app-tool`, tools: o.tools,
|
|
254
|
+
// An app's own skill: standing instructions for the reader's agent, so never loaded
|
|
255
|
+
// silently. The fangorn-index skill offers it to the user and pins the hash on a yes.
|
|
256
|
+
...(o.skill ? { skill: { ...o.skill, adopt: "The app ships an agent skill. Offer it to the user (what it would make you do, from its description); adopt it only on their yes, checking sha256, and ask again if the hash changes." } } : {}) });
|
|
253
257
|
},
|
|
254
258
|
"call-app-tool": async ({ app, tool, arguments: args }) => call(app, tool, args),
|
|
255
259
|
};
|
|
@@ -298,7 +302,8 @@ if (process.argv[2] === "--selfcheck") {
|
|
|
298
302
|
findApps: async () => ({ apps: [{ name: "Kings Foil", desc: "trials", card: "https://k.test/card", appId: "0xk" },
|
|
299
303
|
{ name: "Bare", desc: "", card: "https://b.test/card", appId: "0xb" }], rejected: [{}] }),
|
|
300
304
|
exploreApps: async (q) => ({ ranked_by: q ? "meaning" : null, apps: [{ app: "kings-foil", name: "Kings Foil", relevant: true }, { app: "b", name: "B", relevant: false }] }),
|
|
301
|
-
resolveCard: async (url) => (url.includes("b.test") ? { card: { name: "Bare" }, mcp: null } : { card: { name: "Kings Foil"
|
|
305
|
+
resolveCard: async (url) => (url.includes("b.test") ? { card: { name: "Bare" }, mcp: null } : { card: { name: "Kings Foil", capabilities: { extensions: [{ uri: APP_EXTENSION,
|
|
306
|
+
params: { skill: { url: "https://k.test/skill/kings-foil/SKILL.md", sha256: "b".repeat(64) } } }] } }, mcp: "https://k.test/mcp" }),
|
|
302
307
|
remoteTools: async () => (opened++, remote),
|
|
303
308
|
};
|
|
304
309
|
deps.notify = serve(handlers(deps), { input, output });
|
|
@@ -321,6 +326,7 @@ if (process.argv[2] === "--selfcheck") {
|
|
|
321
326
|
call(5, "open-app", { app: "Kings Foil" }); await wait();
|
|
322
327
|
call(6, "open-app", { app: "https://k.test/card" }); await wait();
|
|
323
328
|
assert(body(5).tools[0].name === "search" && opened === 1, "open once, by name or by card URL");
|
|
329
|
+
assert(body(5).skill?.url === "https://k.test/skill/kings-foil/SKILL.md" && /user/.test(body(5).skill.adopt), "open-app offers the app's skill, for the user to adopt");
|
|
324
330
|
assert(replies.some((r) => r.method === "notifications/tools/list_changed"), "opening notifies");
|
|
325
331
|
rpc(7, "tools/list"); call(8, "kings-foil__search", { query: "x" }); call(9, "call-app-tool", { app: "kings-foil", tool: "search" });
|
|
326
332
|
call(10, "call-app-tool", { app: "kings-foil", tool: "boom" }); call(11, "open-app", { app: "nope" }); call(13, "open-app", { app: "Bare" }); rpc(12, "nope"); await wait();
|
package/src/discover/apps.js
CHANGED
|
@@ -33,6 +33,12 @@ function safeView(url) {
|
|
|
33
33
|
* src/agent/agent-card.js's self-check fails if the two drift. */
|
|
34
34
|
export const APP_EXTENSION = "https://fangorn.network/a2a/app/v1";
|
|
35
35
|
|
|
36
|
+
/** The app's agent skill, `{url, sha256}`, when its card names one well-formed; else null. */
|
|
37
|
+
export const skillOf = (ext) => {
|
|
38
|
+
const k = ext?.params?.skill;
|
|
39
|
+
return k && safeView(k.url) && /^[0-9a-f]{64}$/.test(k.sha256 ?? "") ? { url: k.url, sha256: k.sha256 } : null;
|
|
40
|
+
};
|
|
41
|
+
|
|
36
42
|
/** A verified card → an app entry. `views` is westmarch's param; the SDK does not read it. */
|
|
37
43
|
export function toApp({ card, appId, fromBlock, repos }, cardUrl) {
|
|
38
44
|
const ext = card.capabilities.extensions.find((e) => e.uri === APP_EXTENSION);
|
|
@@ -40,7 +46,7 @@ export function toApp({ card, appId, fromBlock, repos }, cardUrl) {
|
|
|
40
46
|
const p = ext?.params?.paid;
|
|
41
47
|
const paid = p && /^https?:\/\//.test(p.url ?? "") && p.url.includes("{id}") && /^\d+$/.test(String(p.price)) ? p : null;
|
|
42
48
|
return {
|
|
43
|
-
appId, card: cardUrl, fromBlock, repos, views, paid,
|
|
49
|
+
appId, card: cardUrl, fromBlock, repos, views, paid, skill: skillOf(ext),
|
|
44
50
|
mcp: safeView(ext?.params?.mcp),
|
|
45
51
|
name: card.name ?? appId,
|
|
46
52
|
desc: card.description ?? "",
|
package/src/publish/ship.js
CHANGED
|
@@ -83,6 +83,8 @@ const log = (s) => console.error(`[ship] ${s}`);
|
|
|
83
83
|
// // (pages/intro.html, a fragment, replaces the description on the front page)
|
|
84
84
|
// "nav": [{ "href": "coverage.html", "label": "Coverage" }], // optional: header links to them
|
|
85
85
|
// "examples": ["…"], // optional: questions to start from, on the front page (and a private app's gate)
|
|
86
|
+
// "skill": "skill/SKILL.md", // optional: the app's agent skill, served at skill/<app>/SKILL.md and named
|
|
87
|
+
// // in the card with its sha256; fangorn-index offers it to whoever opens the app
|
|
86
88
|
// "agent": "app/agent.js" }, // optional: the module whose registerAgent the card lists, when
|
|
87
89
|
// // `pages` replaces the stock index.html with a page of its own
|
|
88
90
|
// "types": { "<tag>": { "description": "…", "role_map": {…}, "presentation": {…} } },
|
|
@@ -549,6 +551,15 @@ export async function buildSite(cfg, base) {
|
|
|
549
551
|
if (cfg.site.pages) cpSync(resolve(base, cfg.site.pages), site, { recursive: true });
|
|
550
552
|
writeFileSync(join(site, "nav.json"), JSON.stringify(cfg.site.nav ?? []));
|
|
551
553
|
writeFileSync(join(site, "examples.json"), JSON.stringify(cfg.site.examples ?? []));
|
|
554
|
+
// site.skill: the app's agent skill (a SKILL.md), served at skill/<app>/SKILL.md and named in the
|
|
555
|
+
// card with its hash, so fangorn-index can offer it to whoever opens the app.
|
|
556
|
+
let skill = null;
|
|
557
|
+
if (cfg.site.skill) {
|
|
558
|
+
const bytes = readFileSync(resolve(base, cfg.site.skill));
|
|
559
|
+
mkdirSync(join(site, "skill", cfg.app), { recursive: true });
|
|
560
|
+
writeFileSync(join(site, "skill", cfg.app, "SKILL.md"), bytes);
|
|
561
|
+
skill = { path: `skill/${cfg.app}/SKILL.md`, sha256: createHash("sha256").update(bytes).digest("hex") };
|
|
562
|
+
}
|
|
552
563
|
// The card lists the tools the page at `url` registers: the stock page's, or the app's own.
|
|
553
564
|
const card = await import("../agent/agent-card.js");
|
|
554
565
|
const { registerAgent } = await import(cfg.site.agent ? pathToFileURL(resolve(base, cfg.site.agent)).href : "../../site/agent.js");
|
|
@@ -564,7 +575,8 @@ export async function buildSite(cfg, base) {
|
|
|
564
575
|
...(paid ? [{ id: "keys", name: "Keys", tags: [...(cfg.tags ?? []), "x402"],
|
|
565
576
|
description: `The keys to sealed records (POST ${u}/paid), per record.` }] : []),
|
|
566
577
|
],
|
|
567
|
-
fangorn: { app: cfg.app, fromBlock, repos, views: [`${u}/view`], mcp: `${u}/mcp`, config, ...(paid ? { paid } : {}), ...(access ? { access } : {})
|
|
578
|
+
fangorn: { app: cfg.app, fromBlock, repos, views: [`${u}/view`], mcp: `${u}/mcp`, config, ...(paid ? { paid } : {}), ...(access ? { access } : {}),
|
|
579
|
+
...(skill ? { skill: { url: `${u}/${skill.path}`, sha256: skill.sha256 } } : {}) },
|
|
568
580
|
});
|
|
569
581
|
mkdirSync(join(site, ".well-known"), { recursive: true });
|
|
570
582
|
writeFileSync(join(site, ".well-known/agent-card.json"), JSON.stringify(agentCard, null, 1));
|