UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

103 lines • 5.67 kB
"use strict"; /** * hosting/sessionWire — the wire grammar for "show me my own conversations", * owned once. * * A hosted agent stores conversations so a person can come back to one. Until * now the only way back was to already know the session id: the host could * hydrate what you named and nothing else. That is fine for a client that * remembers, and useless for the screen everyone actually builds — a sidebar of * your past chats. * * Two operations answer it, and the shape of the pair is the security design: * * `{ op: 'session-list' }` → the VERIFIED caller's own * sessions, newest first * `{ op: 'session-transcript', sessionId }` → that session's messages, * IF the verified caller * owns it * * ── Why both REQUIRE verified identity ─────────────────────────────────────── * `HostRequest.userId` is caller data — a string anybody can send, as the port * has said since 9.12. Listing "your" sessions from an unverified header is * enumeration with a friendly interface: guess an id, read a stranger's * conversations. So these ops are refused outright at a door that does not * verify ({@link SessionOpNeedsIdentityError}) rather than served under a name * nobody proved. This is the one place in the hosting layer where a feature is * gated on a SECURITY control being present, and it is gated because the * feature without the control is a vulnerability rather than a smaller feature. * * ── Why a foreign transcript is a 404 and not a 403 ────────────────────────── * "That session exists but is not yours" tells an attacker which ids are real. * A session that does not exist, one that exists and belongs to somebody else, * and one whose store lost it are ONE answer here, byte-identical but for the * id the caller already knew. The same law the artifact wire's not-found * follows, for the same reason. * * ── What a transcript actually contains ────────────────────────────────────── * The stored envelope's own messages, read through the envelope readers — the * `LLMMessage[]` an agent conversation IS. Roles and text, projected: no tool * arguments, no tool results, no system prompt. That projection is stated on * {@link TranscriptMessage} rather than implied, because a transcript that * looked complete while silently dropping the tool leg would be a transcript * somebody reconstructs a decision from and gets wrong. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.sessionWireBody = exports.readSessionWireOp = exports.SESSION_PENDING_OP = exports.SESSION_TRANSCRIPT_OP = exports.SESSION_LIST_OP = void 0; const errors_js_1 = require("./errors.js"); const wireOps_js_1 = require("./wireOps.js"); /** The wire spelling of "my sessions". */ exports.SESSION_LIST_OP = wireOps_js_1.WIRE_OPS.sessionList; /** The wire spelling of "one owned session's messages". */ exports.SESSION_TRANSCRIPT_OP = wireOps_js_1.WIRE_OPS.sessionTranscript; exports.SESSION_PENDING_OP = wireOps_js_1.WIRE_OPS.sessionPending; /** * Read a session-history operation out of a request body, if the body names * one. * * Returns `undefined` for a body with no `op`, and for an op that belongs to * another domain's reader (the artifact ops) — declining is not refusing. An * op nobody speaks raises the ONE shared refusal, so the answer is the same * whichever reader a dialect happens to call first. * * Exported for custom {@link import('./httpHost.js').HttpWire} dialects. */ function readSessionWireOp(body) { const op = body.op; if (op === undefined) return undefined; if (op !== exports.SESSION_LIST_OP && op !== exports.SESSION_TRANSCRIPT_OP && op !== exports.SESSION_PENDING_OP) { if ((0, wireOps_js_1.isWireOp)(op)) return undefined; (0, wireOps_js_1.refuseUnknownWireOp)(op); } if (op === exports.SESSION_LIST_OP) return { op: 'list' }; const sessionId = body.sessionId; if (typeof sessionId !== 'string' || sessionId.trim().length === 0) { throw new errors_js_1.InvalidWireOpError(`'${String(op)}' needs 'sessionId' — which conversation to read. The ids a ` + `caller may read are exactly the ones '${exports.SESSION_LIST_OP}' hands back; a session id ` + `from anywhere else answers the same not-found as one that never existed.`); } return { op: op === exports.SESSION_PENDING_OP ? 'pending' : 'transcript', sessionId }; } exports.readSessionWireOp = readSessionWireOp; /** * The standard reply body for a resolved session operation: * `{ sessions: [...] }` for a list, `{ transcript: { sessionId, messages } }` * for a transcript. * * Authored once so every dialect answers byte-compatibly; a dialect that must * add its own envelope fields spreads this and adds them beside (the managed * runtime adds its own `status`) — the `artifactWireBody` precedent, verb for * verb. */ function sessionWireBody(result) { if (result.op === 'list') return { sessions: result.sessions }; if (result.op === 'pending') return { pending: result.pending }; return { transcript: { sessionId: result.sessionId, messages: result.messages } }; } exports.sessionWireBody = sessionWireBody; //# sourceMappingURL=sessionWire.js.map