Skip to content

Commit 1b5fc85

Browse files
committed
h
1 parent 308fbc2 commit 1b5fc85

2 files changed

Lines changed: 171 additions & 20 deletions

File tree

worker/src/index.js

Lines changed: 145 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ import { catalogTick, catalogReport, catalogFull, observatoryPage, observatoryJs
3030
// su loro invito. Zero costo: 1 lettura + 1 firma per tick; niente chain.
3131
// Secret WITNESS_KEY assente → disattivato in silenzio (fail-safe).
3232
import { witnessTick, witnessIndex, witnessLatestNote, witnessAddCheckpoint, witnessHistory, WITNESSED_LOGS } from "./witness.mjs";
33-
import { sealAction, getReceipt, rlogStatus, demoAllowed, treeRoot, signedCheckpoint, proofFor, verifyReceipt, RLOG_ORIGIN } from "./rlog.mjs";
33+
import { sealAction, getReceipt, rlogStatus, demoAllowed, treeRoot, signedCheckpoint, proofFor, verifyReceipt, anchorConsistency, PROVENANCE_LEVELS, RLOG_ORIGIN } from "./rlog.mjs";
3434

3535
const GBLIN = "0x36C81d7E1966310F305eA637e761Cf77F90852f0";
3636
const BASKET_SELECTOR = "0x8c7e0875"; // basket(uint256)
@@ -44,7 +44,7 @@ const FALLBACK_RPCS = [
4444
];
4545
const SITE = "https://gblin.digital";
4646
const SUPPORTED_PROTOCOLS = ["2025-06-18", "2025-03-26", "2024-11-05"];
47-
const SERVER_INFO = { name: "gblin-mcp-http", version: "0.2.0" };
47+
const SERVER_INFO = { name: "gblin-mcp-http", version: "0.3.0" };
4848

4949
// ── Tools ───────────────────────────────────────────────────────────────────
5050

@@ -84,15 +84,21 @@ const RECEIPT_SCHEMA = {
8484
properties: {
8585
chain: { type: "string" }, method: { type: "string" }, eas_schema_uid: { type: "string" }, promise_id: { type: "string" },
8686
last_anchor: { type: ["object", "null"], properties: { day: { type: "string" }, tree_size: { type: "integer" }, root: { type: "string" }, tx: { type: "string" } } },
87-
covers_this_receipt: { type: "boolean" }, explorer: { type: "string" },
87+
root_covers_this_receipt: { type: "boolean", description: "true iff index < anchored_tree_size: the anchored ROOT commits to this leaf via the inclusion proof" },
88+
covers_this_receipt: { type: "boolean", description: "deprecated alias of root_covers_this_receipt" },
89+
anchored_tree_size: { type: ["integer", "null"] },
90+
what_is_anchored: { type: "string" }, explorer: { type: "string" },
8891
},
89-
required: ["covers_this_receipt"],
92+
required: ["root_covers_this_receipt", "anchored_tree_size", "what_is_anchored"],
9093
},
9194
provenance: {
9295
type: "object",
9396
description: "What the receipt does and does not prove",
94-
properties: { level: { type: "string", enum: ["self-reported"] }, meaning: { type: "string" } },
95-
required: ["level"],
97+
properties: {
98+
level: { type: "string", enum: ["self-reported", "server-observed", "externally-verified"] },
99+
levels: { type: "array", items: { type: "string" } }, levels_meaning: { type: "object" }, meaning: { type: "string" },
100+
},
101+
required: ["level", "levels"],
96102
},
97103
},
98104
required: ["format", "payload", "leaf", "index", "tree_size", "root", "signature", "verifier_key", "inclusion_proof", "checkpoint", "anchor", "provenance"],
@@ -105,7 +111,9 @@ const RESOURCES = [
105111
{ uri: "gblin://howto/seal", name: "How to seal AI actions without limits (x402)", mimeType: "application/json",
106112
description: "Paid seal endpoint ($0.01 USDC on Base via x402), fields, free reading routes and the offline verifier." },
107113
{ uri: "gblin://limits", name: "Rate limits and costs of this server", mimeType: "application/json",
108-
description: "60 requests/min/IP on /mcp; seal_action demo 5/day/IP; all tools free; what is paid lives on x402 HTTP endpoints." },
114+
description: "Machine-readable numbers: 60 requests/min/IP on /mcp, seal_action demo 5/day/IP, all tools free; paid prices of the x402 HTTP endpoints." },
115+
{ uri: "gblin://keys", name: "Signing keys and rotation policy", mimeType: "application/json",
116+
description: "Current verifier keys (receipts log, witness), the EAS attester wallet, and the pre-registered key-rotation procedure (how old receipts stay verifiable)." },
109117
];
110118

111119
const TOOLS = [
@@ -225,7 +233,7 @@ const TOOLS = [
225233
tool: { type: "string", description: "Tool/model used (optional, <=128)" },
226234
meta: { type: "string", description: "Extra JSON, <=512 chars (optional)" },
227235
},
228-
required: ["action", "input_hash"],
236+
required: ["mode", "action", "input_hash"],
229237
},
230238
annotations: { title: "Seal an AI action (demo receipt)", readOnlyHint: false, idempotentHint: false, openWorldHint: false },
231239
outputSchema: RECEIPT_SCHEMA,
@@ -239,7 +247,7 @@ const TOOLS = [
239247
},
240248
{
241249
name: "verify_receipt",
242-
description: "Verify a gblin-receipt/v1 JSON with pure math (no log lookup, no trust in this server): leaf hash, Ed25519 signature, RFC 6962 inclusion proof, C2SP checkpoint signature, verifier-key hash. Same checks as the zero-dependency verify-receipt.mjs you can run offline.",
250+
description: "Verify a gblin-receipt/v1 JSON with pure math (no log lookup, no trust in this server): leaf hash, Ed25519 signature, RFC 6962 inclusion proof, C2SP checkpoint signature, verifier-key hash. Same checks as the zero-dependency verify-receipt.mjs you can run offline. For the extra on-chain-anchor consistency check use GET /v1/verify/:index.",
243251
inputSchema: {
244252
type: "object",
245253
properties: { receipt: { type: "object", description: "The receipt JSON as returned by seal_action / get_receipt / GET /v1/receipt/:i (bare or wrapped in {receipt})" } },
@@ -530,6 +538,9 @@ async function coherenceAttestClosedDay(env) {
530538
// Mark sealed only after a tx hash exists, so a failure retries next run.
531539
await env.COHERENCE.put(sealKey, hash, { expirationTtl: 120 * 86400 });
532540
await env.COHERENCE.put(`txlast:${p.id}`, JSON.stringify({ day, hash }));
541+
// Dogfooding: the observer seals ITS OWN real action (this EAS tx) into the
542+
// receipts log as a non-demo, operator-labelled receipt. Never blocks the seal.
543+
await sealOperatorAction(env, { action: "coherence.eas-seal", subject: `${p.id}:${day}`, tx: hash, meta: { promise: p.id, day, tx: hash } });
533544
} catch {
534545
// RPC/gas hiccup: leave this day unsealed AND mark the run incomplete, so
535546
// the outer daily gate keeps retrying every 10 min instead of waiting a
@@ -584,6 +595,23 @@ async function rlogAnchorDaily(env) {
584595
} catch { return false; }
585596
}
586597

598+
// Operator receipts: real actions performed by this Worker, sealed as non-demo
599+
// entries with an explicit operator label in meta (never confused with customers).
600+
async function sha256hex(str) {
601+
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(str));
602+
return [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");
603+
}
604+
async function sealOperatorAction(env, { action, subject, tx, meta }) {
605+
try {
606+
if (!env.RLOG_KEY) return;
607+
await sealAction(env, {
608+
action, input_hash: await sha256hex(subject), output_hash: tx ? await sha256hex(tx) : undefined,
609+
agent_id: "gblin.digital/coherence-observer", tool: "cloudflare-worker-cron",
610+
meta: JSON.stringify({ operator: "gblin.digital", self_sealed: true, ...meta }).slice(0, 512),
611+
}, { demo: false });
612+
} catch (e) { console.error("operator seal:", e && e.message); }
613+
}
614+
587615
// One-shot "genesis" seal: attest the real cumulative window observed so far
588616
// (from first observation to now), honestly labelled as a partial genesis
589617
// window — never a full closed day. Used to write the very first on-chain proof
@@ -917,17 +945,94 @@ function howtoAttestation() {
917945
};
918946
}
919947

920-
function readResource(uri) {
948+
// Exposed in tools/list._meta so an agent can tell this surface from the stdio package without reading docs.
949+
const SURFACE_META = {
950+
server: SERVER_INFO,
951+
transport: "streamable-http (stateless, no auth)",
952+
tool_count: 8,
953+
paid_over_mcp: false,
954+
sibling_package: { name: "@gblin-protocol/mcp-server", transport: "stdio (npm)", tool_count: 10, note: "different tool set: treasury/swap/governance tools; only get_market_risk_regime is shared" },
955+
resources: ["gblin://howto/attestation", "gblin://howto/seal", "gblin://limits", "gblin://keys"],
956+
get_audit_urls: { meta: "/meta", tools: "/tools.json", resources: "/resources.json", conformance: "/conformance", verify: "/v1/verify/:index" },
957+
};
958+
// Canonical manifest hash (tools + resources), so docs and Smithery can be checked against the live surface.
959+
let MANIFEST_HASH = null;
960+
async function manifestHash() {
961+
if (MANIFEST_HASH) return MANIFEST_HASH;
962+
const canon = JSON.stringify({ tools: TOOLS, resources: RESOURCES });
963+
const d = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canon));
964+
MANIFEST_HASH = "sha256:" + [...new Uint8Array(d)].map((b) => b.toString(16).padStart(2, "0")).join("");
965+
return MANIFEST_HASH;
966+
}
967+
async function metaDoc(env) {
968+
return {
969+
server: SERVER_INFO, transport: SURFACE_META.transport, protocol_versions: SUPPORTED_PROTOCOLS,
970+
endpoint: "https://gblin-mcp.gblin-mcp-worker.workers.dev/mcp",
971+
tool_count: TOOLS.length, tool_names: TOOLS.map((t) => t.name),
972+
resource_count: RESOURCES.length, resource_uris: RESOURCES.map((r) => r.uri),
973+
manifest_hash: await manifestHash(),
974+
paid_over_mcp: false, auth_required: false, rate_limit_rpm_per_ip: 60,
975+
sibling_package: SURFACE_META.sibling_package,
976+
docs: { llms_txt: `${SITE}/llms.txt`, smithery: "https://smithery.ai/servers/gblin-protocol/mcp", repo: "https://github.com/gblinproject/gblin-treasury-risk-regime" },
977+
audit: SURFACE_META.get_audit_urls,
978+
};
979+
}
980+
async function conformanceDoc(env) {
981+
const meta = await metaDoc(env);
982+
return {
983+
about: "GET-able conformance fixture of the hosted MCP surface: identical data to tools/list and resources/list, plus error shapes and representative outputs. Lets an auditor without POST capability check the live surface.",
984+
meta, tools: TOOLS, resources: RESOURCES,
985+
jsonrpc_errors: [
986+
{ code: -32700, when: "body is not JSON" }, { code: -32600, when: "not a JSON-RPC 2.0 request" },
987+
{ code: -32601, when: "unknown method" }, { code: -32602, when: "unknown tool, bad arguments, or seal_action mode != demo" },
988+
{ code: -32002, when: "unknown resource uri" }, { code: -32603, when: "tool threw (e.g. log unavailable)" },
989+
],
990+
http: { get_mcp: "405 (stateless: POST only)", rate_limited: "429 after 60 req/min/IP", disabled: "503 when MCP_DISABLED" },
991+
examples: {
992+
get_market_risk_regime: await cachedRegime(env).catch(() => null),
993+
get_receipt_0: await getReceipt(env, 0).then((r) => r.receipt).catch(() => null),
994+
resource_limits: await readResource("gblin://limits", env),
995+
},
996+
};
997+
}
998+
999+
async function readResource(uri, env) {
9211000
switch (uri) {
9221001
case "gblin://howto/attestation": return howtoAttestation();
9231002
case "gblin://howto/seal": return howtoSeal();
9241003
case "gblin://limits": return {
925-
mcp_rate_limit: "60 requests per minute per IP (HTTP 429 beyond)",
926-
seal_action_demo: "5 seals per day per IP, receipts marked demo:true",
927-
tools: "all free, no auth, no session",
928-
paid: { attestation: `${SITE}/api/x402/attestation ($0.003 USDC)`, seal: `${SITE}/api/x402/seal ($0.01 USDC)` },
929-
kill_switch: "env MCP_DISABLED returns 503 for every request",
1004+
mcp_rpm_per_ip: 60,
1005+
mcp_rpm_exceeded_status: 429,
1006+
seal_demo_per_day_per_ip: 5,
1007+
seal_demo_marked: "payload.demo = true",
1008+
tools_free: true, auth_required: false, session_required: false,
1009+
paid: {
1010+
attestation: { url: `${SITE}/api/x402/attestation`, price_usdc: 0.003, network: "eip155:8453", protocol: "x402" },
1011+
seal: { url: `${SITE}/api/x402/seal`, price_usdc: 0.01, network: "eip155:8453", protocol: "x402" },
1012+
},
1013+
kill_switch: "env MCP_DISABLED => HTTP 503 on every request",
9301014
};
1015+
case "gblin://keys": {
1016+
let rlog = null, witness = null;
1017+
try { rlog = (await rlogStatus(env)).verifier_key; } catch {}
1018+
try { if (env.WITNESS_KEY) { const { parseWitnessSecret, witnessVerifierKey } = await import("./witness.mjs"); witness = await witnessVerifierKey(parseWitnessSecret(env.WITNESS_KEY).pub); } } catch {}
1019+
return {
1020+
receipts_log: { origin: RLOG_ORIGIN, verifier_key: rlog, alg: "Ed25519 (C2SP signed note, key id 0x01)", signs: ["gblin-receipt/v1 receipts", "checkpoints"] },
1021+
witness: { name: "gblin.digital/witness", verifier_key: witness, alg: "Ed25519 (C2SP tlog-cosignature v1, key id 0x04)", cosigns: "markovianprotocol.com/log" },
1022+
eas_attester_wallet: { address: "0x14d4d81233EAa95F071f514510661a2a873D83a1", role: "pays the daily EAS attestations on Base (Coherence days + receipts-log root)", note: "dedicated hot wallet, not the protocol owner" },
1023+
rotation_policy: {
1024+
trigger: "suspected compromise or scheduled rotation; never silent",
1025+
procedure: [
1026+
"1. New keypair generated; new verifier_key published here, in /log/checkpoint and in llms.txt with the rotation date.",
1027+
"2. The last checkpoint signed by the OLD key is sealed on Base via EAS (same schema) so the hand-over point is on-chain.",
1028+
"3. The old verifier_key stays listed under retired_keys with its valid_until and last_tree_size; receipts issued before that size verify with the old key.",
1029+
"4. The new key signs a checkpoint over the SAME tree (no new log, no re-indexing): inclusion proofs of old receipts remain valid against new checkpoints.",
1030+
],
1031+
retired_keys: [],
1032+
last_rotation: null,
1033+
},
1034+
};
1035+
}
9311036
default: return null;
9321037
}
9331038
}
@@ -966,12 +1071,12 @@ async function handleMessage(msg, env) {
9661071
case "ping":
9671072
return rpcResult(id, {});
9681073
case "tools/list":
969-
return rpcResult(id, { tools: TOOLS });
1074+
return rpcResult(id, { tools: TOOLS, _meta: SURFACE_META });
9701075
case "resources/list":
9711076
return rpcResult(id, { resources: RESOURCES });
9721077
case "resources/read": {
9731078
const uri = params && params.uri;
974-
const body = readResource(uri);
1079+
const body = await readResource(uri, env);
9751080
if (!body) return rpcError(id, -32002, `Resource not found: ${uri}`);
9761081
return rpcResult(id, { contents: [{ uri, mimeType: "application/json", text: JSON.stringify(body, null, 2) }] });
9771082
}
@@ -1055,11 +1160,34 @@ export default {
10551160
stdio_twin: "npx @gblin-protocol/mcp-server (full toolset, free)",
10561161
site: SITE,
10571162
witness: "/witness (we cosign third-party transparency-log checkpoints; C2SP tlog-cosignature v1)",
1163+
audit: "/meta · /tools.json · /resources.json · /conformance · /v1/verify/:index (GET-only audit of the MCP surface)",
10581164
receipts: "/log (AI Action Receipts: seal what your agent did — $0.01 via x402 at gblin.digital/api/x402/seal, demo via MCP tool seal_action)",
10591165
});
10601166
}
10611167

10621168
// Public coherence report for humans, dashboards and crawlers. Free forever.
1169+
if (url.pathname === "/meta" && request.method === "GET") return json(await metaDoc(env), 200, { "cache-control": "public, max-age=300" });
1170+
if (url.pathname === "/tools.json" && request.method === "GET") return json({ manifest_hash: await manifestHash(), tools: TOOLS }, 200, { "cache-control": "public, max-age=300" });
1171+
if (url.pathname === "/resources.json" && request.method === "GET") return json({ manifest_hash: await manifestHash(), resources: RESOURCES }, 200, { "cache-control": "public, max-age=300" });
1172+
if (url.pathname === "/conformance" && request.method === "GET") return json(await conformanceDoc(env), 200, { "cache-control": "public, max-age=120" });
1173+
if (url.pathname.startsWith("/v1/verify/") && request.method === "GET") {
1174+
const idx = Number(url.pathname.slice("/v1/verify/".length));
1175+
const r = await getReceipt(env, idx);
1176+
if (r.status !== 200) return json({ error: r.error }, r.status);
1177+
const v = await verifyReceipt(r.receipt);
1178+
const byName = Object.fromEntries(v.checks.map((c) => [c.name, c.ok]));
1179+
const a = await anchorConsistency(env);
1180+
return json({
1181+
index: idx, tree_size: r.receipt.tree_size,
1182+
signature_valid: !!byName.signature, leaf_valid: !!byName.leaf, inclusion_valid: !!byName.inclusion_proof,
1183+
checkpoint_valid: !!byName.checkpoint, verifier_key_valid: !!byName.verifier_key,
1184+
anchor_found: a.anchor_found, anchor_root_matches: a.anchor_root_matches, anchored_tree_size: a.anchored_tree_size, anchor_tx: a.anchor_tx,
1185+
root_covers_this_receipt: a.anchor_found && idx < a.anchored_tree_size,
1186+
provenance_level: r.receipt.provenance.level, demo: !!r.receipt.payload.demo,
1187+
valid: v.valid, errors: v.errors,
1188+
note: "Cryptographic checks are recomputed server-side from the receipt alone (same math as verify_receipt / verify-receipt.mjs); anchor_root_matches recomputes the root at anchored_tree_size from the log and compares it with the root written on Base. Trust model: re-run verify-receipt.mjs offline if you do not trust this server.",
1189+
}, 200, { "cache-control": "public, max-age=60" });
1190+
}
10631191
if (url.pathname === "/coherence" && request.method === "GET") {
10641192
return json(await coherenceReport(env));
10651193
}

0 commit comments

Comments
 (0)