kestrel.markets
Version:
A typed, token-efficient language + runtime for agentic trading: agents author bounded plans, the runtime fires them at the tick. CLI + typed library + MCP server.
399 lines (398 loc) • 21.6 kB
JavaScript
/**
* # cli/backend/mock — the TRUTHFUL in-process contract server (kestrel-5rb).
*
* A `fetch(url, init) → Response` that serves the EXACT platform contract
* (kestrel.markets): the M1 routes, **snake_case** wire bodies, the structured
* 402/OfferResponse for the paid-dataset boundary, a canonical resumable SSE stream
* whose events are the `operation.*` {@link ./wire.ts OperationEvent.type} vocabulary
* with **opaque-STRING** `id:` cursors, and `Idempotency-Key` effect-once replay.
* Inject into RemoteBackend (`{ baseUrl, fetch: mockFetch }`) → the whole CLI runs
* in CI with no real API.
*
* It shares its wire shapes and SSE event names with {@link ./remote.ts} through
* `wire.ts` — one definition of the dialect for client AND server — so this mock can
* never again hand-roll a fabricated dialect that hides a divergence (the failure
* mode kestrel-5rb was filed to kill). The route/shape/vocabulary conformance is
* independently pinned to the platform OpenAPI fixture by
* `tests/cli.remote-conformance.test.ts`.
*/
import { PROTOCOL_VERSION } from "../../protocol/index.js";
// kestrel-djm.5: the BAKED catalog-conformant records this server ALSO serves so the SDK's HTTP transport
// returns the SAME protocol Blotter / transcript / grade / root the LOCAL projection yields (light: pure
// data, no engine — the server stays on the remote graph, tests/package.boundary.test.ts).
import { CATALOG_ENTRIES, CATALOG_RECORDS, SESSION_TO_ENTRY } from "./catalog-records.js";
// kestrel-dnxq: the light contract server serves `GET /catalog` in the DEPLOYED snake_case listing dialect
// (`{ entries: HostedCatalogEntry[], version }`) — NOT the old bare camelCase CatalogEntry[] that hid the
// remote-vs-local divergence (kestrel-y9v1/uasi). Built through the SAME shared listing constructor the LOCAL
// transport uses, so `remoteTransport`'s decode reconstructs the byte-identical canonical CatalogListing.
import { buildCatalogListing, toCatalogListingWire } from "../../catalog/catalog-listing.js";
const TRIAL = {
// `capability` IS the bearer token (openapi TrialCapability). Scopes exclude
// paper/broker/live — the trial is a subset of the wallet-signable set (ADR-0002).
capability: "cap_trial_mock_0001",
subject_commitment: "sc_ephemeral_mock",
expiry: "2099-01-01T00:00:00Z",
scopes: ["data", "sim", "grade"],
rate_limit: { requests_per_minute: 100, compute_quota: "trial" },
};
/** The `GET /catalog` body the light server serves — the deployed `{ entries, version }` snake_case listing
* wire, one row per baked catalog entry, built once for byte-stability through the SAME shared listing
* constructor the LOCAL transport uses (kestrel-dnxq). The `version` is a fixed content tag (the fixture is
* static); a self-hosted mirror may serve any opaque version — the client never interprets it. */
const CATALOG_LISTING_BODY = {
entries: buildCatalogListing(CATALOG_ENTRIES.map((e) => e.id)).map(toCatalogListingWire),
version: "cat_mock_1",
};
const SIM_OP_ID = "op_sim_1";
const GRADE_OP_ID = "op_grade_1";
function simOperation() {
return { operation_id: SIM_OP_ID, intent_hash: "ih_" + SIM_OP_ID, status: "running", cursor: "c0", artifacts: [], receipts: [] };
}
function gradeOperation() {
return { operation_id: GRADE_OP_ID, intent_hash: "ih_" + GRADE_OP_ID, status: "running", cursor: "c0", artifacts: [], receipts: [] };
}
/** A running Operation whose id ENCODES a catalog entry (kestrel-djm.5) — its events serve the baked record. */
function catalogOperation(opId) {
return { operation_id: opId, intent_hash: "ih_" + opId, status: "running", cursor: "c0", artifacts: [], receipts: [] };
}
/** The paid boundary is a property of the DATASET (§4 Sim): a free-catalog artifact
* runs free; anything outside it triggers the 402. (The client sends no `scope`.) */
function isPaidDataset(artifactId) {
return typeof artifactId === "string" && /premium|paid|out-of-catalog/.test(artifactId);
}
// kestrel-djm.5 — catalog-conformant serving. Operation ids ENCODE the catalog entry so the events endpoint
// serves the right baked record on resume (the op id is the durable handle, ADR-0004).
const SIM_CAT_PREFIX = "op_sim_cat_";
const GRADE_CAT_PREFIX = "op_grade_cat_";
/** The baked catalog entry id a `POST /sim` dataset addresses, or `null` when the dataset is not a
* catalog-Session subject (a non-baked id / a raw dataset — those fall through to the generic mock behavior,
* unchanged). Accepts the BARE `<id>` handle — the dialect the deployed server speaks and the one the SDK's
* `remoteTransport` now sends after kestrel-p6hz dropped the client-invented `catalog:` prefix — AND the
* legacy `catalog:<id>` form, so both the new bare path and any prefix-passing caller resolve identically. */
function catalogSimEntry(artifactId) {
if (typeof artifactId !== "string")
return null;
const id = artifactId.startsWith("catalog:") ? artifactId.slice("catalog:".length) : artifactId;
return Object.prototype.hasOwnProperty.call(CATALOG_RECORDS, id) ? id : null;
}
/** Resolve a baked Session record by its graded-Session id — the SDK's durable Operation/artifact handle
* (src/sdk/local.ts models an Operation id as the finalized Session id). `undefined` when unknown. */
function recordBySession(sessionId) {
const entryId = SESSION_TO_ENTRY[sessionId];
return entryId !== undefined ? CATALOG_RECORDS[entryId] : undefined;
}
/** Resolve a baked Session record by its conformance root (the graded-bus sha256). `undefined` when unknown. */
function recordByRoot(root) {
return Object.values(CATALOG_RECORDS).find((r) => r.conformanceRoot === root);
}
function offerResponse() {
return {
operation_id: SIM_OP_ID,
offer: {
offer_id: "of_mock_paper_01",
operation_id: SIM_OP_ID,
intent_hash: "ih_" + SIM_OP_ID, // the canonical intent this Offer prices
scope: ["paper"], // out-of-trial scope → must convert
ceiling: 500,
amount: { value: "25.00", asset: "USDC" }, // Money = { value, asset }
terms_digest: "sha256:termsdigest",
expiry: "2099-01-01T00:00:00Z",
nonce: "nonce_off_01",
requires_human: false,
},
proof: {
proof_id: "pf_" + SIM_OP_ID,
operation_id: SIM_OP_ID,
intent_hash: "ih_" + SIM_OP_ID,
artifacts: [],
url: "https://kestrel.markets/proof/" + SIM_OP_ID,
},
settlement_methods: [
{ method: "stripe-mpp", network: "tempo", accepts: ["USDC"] }, // MPP first (DESIGN §5)
{ method: "x402", network: "base", accepts: ["USDC"] },
],
human_action: {
kind: "claim-and-fund",
url: "https://kestrel.markets/claim/" + SIM_OP_ID,
required: false,
label: "Send to your human to claim & fund",
},
};
}
const REPORT = {
session: {
date: "2024-05-07",
instruments: ["SPY"],
mode: "sim",
fill_model: "maker-fair-v1",
bus_events: 42,
determinism_hash: "dh_mock",
bus_sha256: "bs_mock",
reprice_count: 0,
settle_ts: 1715112000000,
},
plans: [],
orders: [],
totals: { realized_floor_usd: 27, expected_usd: 30, premium_spent: 12 },
emitted: { PLAN: 1, ORDER: 2 },
};
const SAMPLE_BLOTTER = {
sessionId: "bl_mock_1",
orders: [],
fills: [],
positions: [],
settlement: { realized: 0, asset: "usd", settledAt: "2024-05-07T20:00:00Z" },
};
const GRADE = { subjectSessionId: "dh_mock", metrics: { bankable_ev: 27.3, fill_support: 0.61 } };
/** client-facing idempotency ledger: Idempotency-Key → whether it has been seen. */
const seenIdempotencyKeys = new Set();
/** The contract fetch handler. Pass to RemoteBackend as `fetch`. */
export function mockFetch(url, init) {
const u = new URL(url);
const p = u.pathname;
const method = (init?.method ?? "GET").toUpperCase();
const body = init?.body ? JSON.parse(String(init.body)) : {};
const headers = lowerHeaders(init?.headers);
const json = (o, status = 200, extra = {}) => new Response(JSON.stringify(o), { status, headers: { "content-type": "application/json", ...extra } });
// effect-once: a replayed Idempotency-Key returns the prior result marked replayed.
const replay = () => {
const key = headers["idempotency-key"];
if (key === undefined)
return {};
if (seenIdempotencyKeys.has(key))
return { "idempotent-replay": "true" };
seenIdempotencyKeys.add(key);
return {};
};
// ── capability-discovery: POST /capabilities/trial ──
if (p === "/capabilities/trial" && method === "POST")
return Promise.resolve(json(TRIAL, 201, replay()));
// ── validate: POST /validate (pure; no Operation, no idempotency) ──
if (p === "/validate" && method === "POST") {
const r = { valid: true, artifact_id: "art_plan_1", diagnostics: [] };
return Promise.resolve(json(r, 200));
}
// ── catalog: GET /catalog → the deployed snake_case listing wire (kestrel-dnxq; public, no auth) ──
if (p === "/catalog" && method === "GET")
return Promise.resolve(json(CATALOG_LISTING_BODY, 200));
// ── sim: POST /sim → 201 Operation (direct) | 402 OfferResponse (paid dataset) ──
if (p === "/sim" && method === "POST") {
const dataset = body["dataset"];
// kestrel-djm.5: a real catalog-Session subject → an Operation whose id encodes the entry (its events
// serve the baked catalog record). Non-catalog datasets are unchanged (paid → 402; else generic sim).
const catEntry = catalogSimEntry(dataset?.artifact_id);
if (catEntry !== null)
return Promise.resolve(json(catalogOperation(SIM_CAT_PREFIX + catEntry), 201, replay()));
if (isPaidDataset(dataset?.artifact_id))
return Promise.resolve(json(offerResponse(), 402));
return Promise.resolve(json(simOperation(), 201, replay()));
}
// ── grade: POST /grade → 201 Operation ──
if (p === "/grade" && method === "POST") {
// kestrel-djm.5: grading a baked catalog Blotter (its sessionId) → an Operation whose id encodes the
// entry (its events serve the baked grade). A generic Blotter id → the generic grade, unchanged.
const blotters = body["blotters"];
const b0 = blotters?.[0];
const catId = typeof b0 === "string" ? SESSION_TO_ENTRY[b0] : undefined;
if (catId !== undefined)
return Promise.resolve(json(catalogOperation(GRADE_CAT_PREFIX + catId), 201, replay()));
return Promise.resolve(json(gradeOperation(), 201, replay()));
}
// ── artifacts: GET /artifacts/{ref} (kestrel-djm.5) ──
// Resolve a content-addressed artifact ref to the SAME { ref, kind, value } the LOCAL projection returns
// (src/sdk/local.ts artifact()): `blotter:<sessionId>` → the protocol Blotter; `sha256:<root>` → the
// conformance root. So the SDK's HTTP transport returns a byte-identical ArtifactResult to LOCAL. An
// unresolvable ref falls through to the hard 404 below (fail-closed, never a fabricated payload).
const artMatch = p.match(/^\/artifacts\/([^/]+)$/);
if (artMatch && method === "GET") {
const ref = decodeURIComponent(artMatch[1]);
if (ref.startsWith("blotter:")) {
const rec = recordBySession(ref.slice("blotter:".length));
if (rec !== undefined)
return Promise.resolve(json({ ref, kind: "blotter", value: rec.blotter }, 200));
}
else if (ref.startsWith("sha256:")) {
const rec = recordByRoot(ref.slice("sha256:".length));
if (rec !== undefined)
return Promise.resolve(json({ ref, kind: "conformance-root", value: rec.conformanceRoot }, 200));
}
// unresolvable artifact ref → fall through to the hard 404 below
}
// ── operations: GET /operations/{id} — the STATUS envelope ONLY ──
// kestrel-o3bi: the plain operation resource carries the operation's control-plane STATUS, NOT its domain
// OUTCOME. A COMPLETED operation returns { operation_id, status, cursor } with NO `payload` — the outcome
// (blotter / report / receipt + artifact refs) is carried on the RESUMABLE EVENT STREAM below
// (`GET /operations/{id}/events`), the SAME stream sim/grade consume. This reflects production: the earlier
// mock FABRICATED a `payload:{ blotter, grade, artifacts }` here that the real endpoint never serves, which
// masked the empty-payload resume defect a wire-first integrator hit. `resumeOperation` now reads the event
// stream, so this endpoint is deliberately payload-free — a client that reads it for the outcome gets {}
// (the decisive delete-the-call-site probe: revert the transport to this endpoint → empty payload → RED).
const opMatch = p.match(/^\/operations\/([^/]+)$/);
if (opMatch && method === "GET") {
const id = decodeURIComponent(opMatch[1]);
const rec = recordBySession(id);
if (rec !== undefined) {
// A finalized durable operation: COMPLETED, but the outcome lives on /events (no payload here).
return Promise.resolve(json({ operation_id: id, status: "completed", cursor: null }, 200));
}
const op = id === GRADE_OP_ID ? gradeOperation() : simOperation();
return Promise.resolve(json({ ...op, status: "completed", cursor: "c3" }, 200));
}
// ── events: GET /operations/{id}/events (resumable SSE) ──
const evMatch = p.match(/^\/operations\/([^/]+)\/events$/);
if (evMatch && method === "GET") {
const id = decodeURIComponent(evMatch[1]);
const cursor = u.searchParams.get("cursor") ?? headers["last-event-id"] ?? null;
// kestrel-o3bi: RESUME a finalized durable operation by its Session-id handle — replay the OUTCOME
// ({ blotter, grade, artifacts }) on the resumable stream, byte-identical to the LOCAL projection. This is
// the stream `resumeOperation` reads; a COMPLETED operation returns its full outcome here (the plain
// status endpoint above is payload-free), closing the wire read-back the batch redirect points at.
const finalizedRec = recordBySession(id);
if (finalizedRec !== undefined)
return Promise.resolve(resumptionSse(id, finalizedRec, cursor));
// kestrel-djm.5: a catalog Operation streams the baked record (sim) or the baked grade (grade).
if (id.startsWith(SIM_CAT_PREFIX)) {
const rec = CATALOG_RECORDS[id.slice(SIM_CAT_PREFIX.length)];
if (rec !== undefined)
return Promise.resolve(catalogSse(id, rec, "sim", cursor));
}
if (id.startsWith(GRADE_CAT_PREFIX)) {
const rec = CATALOG_RECORDS[id.slice(GRADE_CAT_PREFIX.length)];
if (rec !== undefined)
return Promise.resolve(catalogSse(id, rec, "grade", cursor));
}
return Promise.resolve(sseResponse(id, cursor));
}
// fail closed: an off-contract route is a hard 404 (problem+json), never a lie.
return Promise.resolve(new Response(JSON.stringify({ type: "about:blank", title: "not_found", status: 404, code: "not_found" }), {
status: 404,
headers: { "content-type": "application/problem+json" },
}));
}
/** A canonical SSE stream (§9 Events + §"Cursor-based SSE resume"): `id:` is an
* OPAQUE STRING; each `event:` is an OperationEvent.type; the domain payload rides
* opaquely in the OperationEvent `data`; terminal is `operation.completed`. A client
* re-opening at cursor `c2` gets only the tail (resume). */
function sseResponse(opId, resumeFrom) {
const isGrade = opId === GRADE_OP_ID;
const artifactType = isGrade ? "operation.receipt" : "operation.artifact";
const artifactData = isGrade
? { artifact: { artifact_id: "gr_mock_1", kind: "grade" }, grade: GRADE }
: { artifact: { artifact_id: SAMPLE_BLOTTER.sessionId, kind: "blotter" }, report: REPORT, blotter: SAMPLE_BLOTTER, wakes: [] };
const all = [
{ id: "c1", type: "operation.started", data: {} },
{ id: "c2", type: artifactType, data: artifactData },
{ id: "c3", type: "operation.completed", data: {} },
];
// resume: replay only events strictly AFTER the given opaque cursor.
const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0;
const emit = all.slice(Math.max(0, start));
const frames = emit.map((e) => {
const env = { cursor: e.id, type: e.type, operation_id: opId, data: e.data };
return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`;
});
const stream = new ReadableStream({
start(c) {
const enc = new TextEncoder();
for (const f of frames)
c.enqueue(enc.encode(f));
c.close();
},
});
return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}
/**
* The catalog-conformant SSE stream (kestrel-djm.5): the SAME resumable framing as {@link sseResponse}, but
* the artifact/receipt frame carries the BAKED record — for `sim`, the protocol Blotter PLUS the full
* server-computed Session record (`session`: id + pentad-chained entries + delivered frames + tip + root +
* artifacts) the HTTP transport replays; for `grade`, the baked portable GradeResult. So the HTTP transport
* returns byte-identical protocol objects to the LOCAL projection (the djm.5 headline).
*/
function catalogSse(opId, record, mode, resumeFrom) {
const artifactType = mode === "grade" ? "operation.receipt" : "operation.artifact";
const artifactData = mode === "grade"
? { artifact: { artifact_id: `gr_cat_${record.entryId}`, kind: "grade" }, grade: record.grade }
: {
artifact: { artifact_id: record.blotter.sessionId, kind: "blotter" },
blotter: record.blotter,
session: {
sessionId: record.sessionId,
blotter: record.blotter,
entries: record.entries,
frames: record.frames,
tipHash: record.tipHash,
conformanceRoot: record.conformanceRoot,
artifacts: record.artifacts,
},
};
const all = [
{ id: "c1", type: "operation.started", data: {} },
{ id: "c2", type: artifactType, data: artifactData },
{ id: "c3", type: "operation.completed", data: {} },
];
const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0;
const emit = all.slice(Math.max(0, start));
const frames = emit.map((e) => {
const env = { cursor: e.id, type: e.type, operation_id: opId, data: e.data };
return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`;
});
const stream = new ReadableStream({
start(c) {
const enc = new TextEncoder();
for (const f of frames)
c.enqueue(enc.encode(f));
c.close();
},
});
return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}
/**
* The RESUME stream for a FINALIZED durable operation (kestrel-o3bi): the SAME resumable framing as
* {@link sseResponse}, whose single `operation.artifact` frame carries the operation's OUTCOME —
* `{ blotter, grade, artifacts }` — so a client that replays this stream (`resumeOperation`) accumulates
* exactly the payload the LOCAL projection returns (src/sdk/local.ts resumeOperation). This is the stream the
* outcome is served on; the plain `GET /operations/{id}` status endpoint is payload-free.
*/
function resumptionSse(opId, record, resumeFrom) {
const all = [
{ id: "c1", type: "operation.started", data: {} },
{ id: "c2", type: "operation.artifact", data: { blotter: record.blotter, grade: record.grade, artifacts: record.artifacts } },
{ id: "c3", type: "operation.completed", data: {} },
];
const start = resumeFrom ? all.findIndex((e) => e.id === resumeFrom) + 1 : 0;
const emit = all.slice(Math.max(0, start));
const frames = emit.map((e) => {
const env = { cursor: e.id, type: e.type, operation_id: opId, data: e.data };
return `id: ${e.id}\nevent: ${e.type}\ndata: ${JSON.stringify(env)}\n\n`;
});
const stream = new ReadableStream({
start(c) {
const enc = new TextEncoder();
for (const f of frames)
c.enqueue(enc.encode(f));
c.close();
},
});
return new Response(stream, { status: 200, headers: { "content-type": "text/event-stream" } });
}
function lowerHeaders(h) {
const out = {};
if (!h)
return out;
if (h instanceof Headers) {
h.forEach((v, k) => (out[k.toLowerCase()] = v));
}
else if (Array.isArray(h)) {
for (const pair of h) {
const k = pair[0];
if (k !== undefined)
out[k.toLowerCase()] = String(pair[1] ?? "");
}
}
else {
for (const [k, v] of Object.entries(h))
out[k.toLowerCase()] = String(v);
}
return out;
}
export const CONTRACT_PROTOCOL_VERSION = PROTOCOL_VERSION;