agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
98 lines • 5.28 kB
JavaScript
/**
* 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.
*/
import { InvalidWireOpError } from './errors.js';
import { isWireOp, refuseUnknownWireOp, WIRE_OPS } from './wireOps.js';
/** The wire spelling of "my sessions". */
export const SESSION_LIST_OP = WIRE_OPS.sessionList;
/** The wire spelling of "one owned session's messages". */
export const SESSION_TRANSCRIPT_OP = WIRE_OPS.sessionTranscript;
export const SESSION_PENDING_OP = 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.
*/
export function readSessionWireOp(body) {
const op = body.op;
if (op === undefined)
return undefined;
if (op !== SESSION_LIST_OP && op !== SESSION_TRANSCRIPT_OP && op !== SESSION_PENDING_OP) {
if (isWireOp(op))
return undefined;
refuseUnknownWireOp(op);
}
if (op === SESSION_LIST_OP)
return { op: 'list' };
const sessionId = body.sessionId;
if (typeof sessionId !== 'string' || sessionId.trim().length === 0) {
throw new InvalidWireOpError(`'${String(op)}' needs 'sessionId' — which conversation to read. The ids a ` +
`caller may read are exactly the ones '${SESSION_LIST_OP}' hands back; a session id ` +
`from anywhere else answers the same not-found as one that never existed.`);
}
return { op: op === SESSION_PENDING_OP ? 'pending' : 'transcript', sessionId };
}
/**
* 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.
*/
export 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 } };
}
//# sourceMappingURL=sessionWire.js.map