feat(identity): A3 (P22) — auto-record dispatched sends into the outbox at the transport route

Identity/audit family atom 3 (design dc3a0719, A3 section). The outbox was explicit-verb-only
(`rig queue outbox record`), so a derived send was not recorded unless someone remembered —
the specimen-5 window, where a send's attribution survived only via the ghost's provider JSONL.
A3 auto-records at dispatch so a derived-but-unrecorded send cannot accept-and-drop at the audit
layer.

- routes/transport.ts (/send handler): after a SUCCESSFUL dispatch, record one outbox_entries row
  with sender_session = the DERIVED actor (the x-openrig-session header, never a body claim) and
  identity_provenance = "transport:v1". STRICTLY DOWNSTREAM of the certified header-derivation
  (line 65 unchanged) — it only consumes the already-derived actor. Best-effort: the send is
  already committed, so a rare audit-write failure is logged, never a false-negative on delivery.
- Records only a DERIVED send; a null-actor send has no sender to attribute (no fabricated row);
  a REFUSED send returns before dispatch (no row); a cross-host send records the ORIGIN triple
  (the derived header IS the origin after A2/A4), never the relay.

Implemented at the ROUTE level (not session-transport.ts) — derivedActor + outboxHandler are
route-scoped. Pins: derived→1 row(transport:v1), cross-host→origin triple, refused→0, headerless→0.
RED 2-at-base with 2 controls. daemon tsc 0; transport-routes + outbox-handler suites green (38).

DELTA REVIEW: the certified property (identity derives from the header, never the body) is
UNCHANGED and depended upon; A3 adds only a durable write downstream of it. Scope note for review:
/send only — whether /broadcast fan-out should also auto-record is a follow-on question, flagged.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
v-openrig-buildandClaude Opus 4.8 committed 2026-08-08 04:08:06 +00:00
1 parent f48151f5a3
commit 7ac6bc2c0a
2 files changed
+99 -1

No files matched your search

+26
View File
@@ -2,6 +2,7 @@ import { Hono } from "hono";
import type { SessionTransport, TargetSpec } from "../domain/session-transport.js";
import { authBearerTokenMiddleware } from "../middleware/auth-bearer-token.js";
import { requireSenderIdentity } from "./require-sender-identity.js";
import type { OutboxHandler } from "../domain/outbox-handler.js";
export function transportRoutes(opts?: { bearerToken?: string | null }): Hono {
const router = new Hono();
@@ -112,6 +113,31 @@ export function transportRoutes(opts?: { bearerToken?: string | null }): Hono {
return c.json(result, status);
}
// A3 (P22): auto-record the DISPATCHED send into the sender-side outbox, so a derived send cannot
// accept-and-drop at the audit layer (the specimen-5 window: no outbox row, attribution survived only
// via provider JSONL). STRICTLY DOWNSTREAM of the certified header-derivation (line 65): it consumes
// the already-derived `derivedActor`, never re-derives or alters it. Records only a DERIVED send —
// `derivedActor` present — with the era-stamp `transport:v1` (the sole mode this header-derived route
// produces; a cross-host relay's header IS the origin triple, so the recorded sender is the ORIGIN,
// never the relay). A null-actor send has no derived sender to attribute (no fabricated row). The
// send is already committed, so a rare audit-write failure is LOGGED, never a false-negative on a
// delivered send.
if (derivedActor) {
const outbox = c.get("outboxHandler" as never) as OutboxHandler | undefined;
if (outbox) {
try {
outbox.record({
senderSession: derivedActor,
destinationSession: body.session,
body: body.text,
identityProvenance: "transport:v1",
});
} catch (err) {
console.warn(`[transport/send] outbox auto-record failed (send already delivered): ${(err as Error).message}`);
}
}
}
return c.json(result);
});
+73 -1
View File
@@ -19,6 +19,8 @@ import { SessionRegistry } from "../src/domain/session-registry.js";
import { SessionTransport } from "../src/domain/session-transport.js";
import type { TmuxAdapter, TmuxResult } from "../src/adapters/tmux.js";
import { transportRoutes } from "../src/routes/transport.js";
import { OutboxHandler } from "../src/domain/outbox-handler.js";
import { outboxEntriesSchema } from "../src/db/migrations/027_outbox_entries.js";
import { createFullTestDb } from "./helpers/test-app.js";
function setupDb(): Database.Database {
@@ -49,10 +51,11 @@ function mockTmux(overrides?: Partial<{
} as unknown as TmuxAdapter;
}
function createApp(deps: { sessionTransport: SessionTransport }): Hono {
function createApp(deps: { sessionTransport: SessionTransport; outboxHandler?: OutboxHandler }): Hono {
const app = new Hono();
app.use("*", async (c, next) => {
c.set("sessionTransport" as never, deps.sessionTransport);
if (deps.outboxHandler) c.set("outboxHandler" as never, deps.outboxHandler);
await next();
});
app.route("/api/transport", transportRoutes());
@@ -116,6 +119,75 @@ describe("transport routes", () => {
expect(body.sessionName).toBe("dev-impl@my-rig");
});
// ── A3 (P22) — auto-record dispatched sends into the sender-side outbox, so a DERIVED send cannot
// accept-and-drop at the audit layer (the specimen-5 window: no outbox row, attribution survived
// only via provider JSONL). Strictly DOWNSTREAM of the certified header-derivation (uses the
// already-derived actor, never re-derives). ──
function outboxRows(): Array<Record<string, unknown>> {
return db.prepare("SELECT * FROM outbox_entries ORDER BY rowid").all() as Array<Record<string, unknown>>;
}
function sendApp() {
// createFullTestDb excludes 027 (outbox is not on the shared core edge — P24); migrate it inline,
// plus the 067 identity_provenance column (outbox part), so the auto-record can stamp the era.
migrate(db, [outboxEntriesSchema]);
const hasProv = (db.prepare("PRAGMA table_info(outbox_entries)").all() as Array<{ name: string }>)
.some((c) => c.name === "identity_provenance");
if (!hasProv) db.exec("ALTER TABLE outbox_entries ADD COLUMN identity_provenance TEXT");
const transport = new SessionTransport({ db, rigRepo, sessionRegistry, tmuxAdapter: mockTmux() });
return createApp({ sessionTransport: transport, outboxHandler: new OutboxHandler(db) });
}
it("A3 — a DERIVED send auto-records exactly ONE outbox row (sender=derived actor, provenance=transport:v1)", async () => {
seedRig();
const res = await sendApp().request("/api/transport/send", {
method: "POST",
headers: { "Content-Type": "application/json", "X-OpenRig-Session": "orch@my-rig" },
body: JSON.stringify({ session: "dev-impl@my-rig", text: "hello" }),
});
expect(res.status).toBe(200);
const rows = outboxRows();
expect(rows).toHaveLength(1); // exactly one, not zero, not two
expect(rows[0]!["sender_session"]).toBe("orch@my-rig"); // the DERIVED actor, never a body claim
expect(rows[0]!["destination_session"]).toBe("dev-impl@my-rig");
expect(rows[0]!["body"]).toBe("hello");
expect(rows[0]!["identity_provenance"]).toBe("transport:v1"); // era-stamp from the run, not hardcoded-lying
});
it("A3 — a cross-host send records the ORIGIN triple (the derived header), never the relay's seat", async () => {
seedRig();
const res = await sendApp().request("/api/transport/send", {
method: "POST",
headers: { "Content-Type": "application/json", "X-OpenRig-Session": "orch@rig-a@origin-host" }, // 3-part origin (A2/A4 carry)
body: JSON.stringify({ session: "dev-impl@my-rig", text: "coordinate" }),
});
expect(res.status).toBe(200);
const rows = outboxRows();
expect(rows).toHaveLength(1);
expect(rows[0]!["sender_session"]).toBe("orch@rig-a@origin-host"); // ORIGIN triple, not the relay
});
it("A3 — a REFUSED send (body actor != header ⇒ 409 identity_mismatch) writes NO outbox row", async () => {
seedRig();
const res = await sendApp().request("/api/transport/send", {
method: "POST",
headers: { "Content-Type": "application/json", "X-OpenRig-Session": "orch@my-rig" },
body: JSON.stringify({ session: "dev-impl@my-rig", text: "hi", actorSession: "mallory@my-rig" }),
});
expect(res.status).toBe(409);
expect(outboxRows()).toHaveLength(0); // refused before dispatch → nothing recorded
});
it("A3 — a header-LESS send (no derived actor) auto-records NO row (nothing to attribute; no fabricated sender)", async () => {
seedRig();
const res = await sendApp().request("/api/transport/send", {
method: "POST",
headers: { "Content-Type": "application/json" }, // no X-OpenRig-Session
body: JSON.stringify({ session: "dev-impl@my-rig", text: "hi" }),
});
expect(res.status).toBe(200); // delivers (non-interactive send tolerates a null actor)
expect(outboxRows()).toHaveLength(0); // but no derived sender ⇒ no auto-record, never a null-sender row
});
// OPR.99.0.6.3 — the additive outcome field auto-surfaces through the
// c.json(result) passthrough; failure HTTP mapping unchanged.
it("POST /send with verify surfaces the outcome field in the JSON response (passthrough)", async () => {