fix(daemon): OpenRig's own rows wake from OpenRig; transport identity not called authenticated (#1905)

## What and why

An outside audit raised two questions about labels (credited in #1918, from Heretek-AI/heretek-rigs#81 part b).
**1. The stuck sweep's recovery rows look like they came from a person or seat that never sent them.**
- **The cause:** a recovery row's source is the stuck row's creator (`queue-stuck-sweep.ts`, "the obligation's own creator is the finding's source"). `create()` staged the wake with that source as the envelope sender, so the seat saw "From: <creator>" and "↩ Reply: rig send <creator>". That creator never sent it.
- **The change:** a row OpenRig creates itself (`wakeTrigger: "openrig"`) now wakes from `daemon@kernel`, the scheduler's existing sender name. The envelope offers no reply for OpenRig's own senders; it says "↩ Sent by OpenRig itself, not by a seat. Act on the row it names."
- **Unchanged:** the row's `source_session`, its admission, and its urgency marking. A source that is already OpenRig's (`daemon@kernel`, `system:health`) keeps its own name.

**2. `identityProvenance: transport:v1` reads as stronger than it is.**
- **What it records:** the label is accurate. It means the name came from the `X-OpenRig-Session` header, which the CLI sets from the seat's `OPENRIG_SESSION_NAME`, rather than from a body claim. The reference docs already say so.
- **What overclaimed:** two user-visible `actor_required` messages said "no authenticated transport identity", and several comments said the header "proved" or "certified" the actor.
- **The change:** those are reworded, and `coordination-primitive.md` gains one sentence saying it isn't authentication.
- **Unchanged:** the label, the stored values, and every refusal.

## Rows that change wake sender

| Creator (`wakeTrigger: "openrig"`) | Row source | Wake sender before | After |
|---|---|---|---|
| stuck sweep finding | stuck row's creator | that creator | `daemon@kernel` |
| workflow exception escalation | instance creator | that creator | `daemon@kernel` |
| health diagnosis, periodic | `system:health` | `system:health` (with a reply line) | `system:health`, no reply line |
| health diagnosis via `rig health diagnosis evaluate --apply` | the seat that ran it | that seat | `daemon@kernel` |
| continuity cutover baton (watchdog) | the registrant | the registrant | `daemon@kernel` |
| mode scheduled end | `daemon@kernel` | `daemon@kernel` (with a reply line) | `daemon@kernel`, no reply line |

**Why the relabel sits in `create()`:** at wake time, trigger `openrig` also covers boot recovery and ladder retries of rows that agents created. Relabeling there would hide real senders; only `create()` knows that OpenRig itself made the row.

**What counts as OpenRig's own sender:** `daemon@kernel` and `system:` actors. Neither can be a seat's name (`{pod}-{member}@{rig}`). `…@system` isn't matched, because a rig may be named `system`.

## Cost

| | Lines |
|---|---|
| Source, net vs main | +42/-29 |
| of which behaviour: `queue-repository.ts` | +6/-3 |
| of which behaviour: `pane-envelope.ts` | +9/-1 |
| of which wording only (7 files) | the rest |
| Tests | +42/-1 |
| Docs | +3/-1 |

- About 1 test line per source line.
- **New refusals:** none. **New error codes:** none. **New settings:** none.

## Proof

- **RED:** the new test files were run against main's three source files (`pane-envelope.ts`, `queue-repository.ts`, `require-sender-identity.ts`), with everything else at this head. 5 cases fail. The recovery-row case received `From: peer@test`; the envelope case received `↩ Reply: rig send daemon@kernel "..."`; both message cases received "no authenticated transport identity"; the held copy was `senderSession: peer@test`. The fixed files were restored and confirmed byte-identical with `cmp`.
- **GREEN, daemon:** 23 files, 911 tests at `--maxWorkers=2`. In the first run, 910 passed and one new case failed on the test's own mistake: a peer's routine wake is held, not typed, at that level. I fixed the test, and its file then passed alone, 21 of 21. The set:
  - the four changed test files;
  - stuck sweep (both), workflow exception (four), health diagnosis, continuity materializer, mode scheduled end;
  - wake ladder, held-wake escalation, parked-owner consumer;
  - local-bare-identity, self-host envelope (triple and sweep), session-transport, queue nudge summary, transactional closure, s02-p3-sender.
- **Typecheck:** daemon `tsc` is clean.
- **CLI:** send, send-header, client-headers, self-host-triple: 129 tests.
- **Repo:** `npm run test:repo` 309 pass, 0 fail.

## Base

Built on `26d629e3`. Main has since gained only #1903, which touches `queue-stuck-sweep.ts` and its test; there's no overlap with this PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Mike Schwarz authored and GitHub committed 2026-10-24 07:45:45 -07:00
1 parent 7923e13e35
commit 92a052d55d
14 files changed
+87 -31

No files matched your search

@@ -429,7 +429,9 @@ over a body actor. On a direct request from a known origin it is recorded as
`transport:v1`; `resolveRecordedProvenance` (`:199`) instead records
`origin-unknown:v1` when the origin-unknown marker is set or a relayed request
carried `origin-unknown:v1` (`:207`), `relay:v1` for a relayed request that
carried `transport:v1`, and `claimed:v1` for other relayed requests. Without the
carried `transport:v1`, and `claimed:v1` for other relayed requests. `transport:v1`
says the name came from that header, which the CLI sets from the seat's
`OPENRIG_SESSION_NAME`; it isn't an authentication. Without the
header, the
body actor is recorded as `claimed:v1`; with neither (or, for inbox drop,
without the header) the route answers 400 `actor_required`. The stamp lands in
@@ -12,7 +12,7 @@ import { WAKE_INTENT_PREFIX, type OutboxHandler, type OutboxEntry } from "./outb
import type { QueueWakeFacts } from "./draft-aware-delivery.js";
import { derivePickup, type PickupReceipt } from "./queue-pickup.js";
import { lastMeaningfulTransition, readWaitingView, type WaitingView, type WaitingActivityReader } from "./queue-waiting.js";
import { wrapPaneEnvelope } from "../lib/pane-envelope.js";
import { isOpenRigSender, OPENRIG_SENDER, wrapPaneEnvelope } from "../lib/pane-envelope.js";
import { isBoundTerminalSeat } from "./terminal-seat.js";
import { renderQueueHandoffNudge } from "./queue-nudge-text.js";
import { getSelfHostId } from "./hosts/fanout-contract.js";
@@ -1700,13 +1700,16 @@ export class QueueRepository {
}
let wakeStaged = false;
// A row OpenRig creates itself (a sweep finding, an escalation, a health or mode notice) keeps its source for the
// record, but its wake comes from OpenRig: the seat named as source didn't send it.
const wakeFrom = input.wakeTrigger === "openrig" && !isOpenRigSender(input.sourceSession) ? OPENRIG_SENDER : input.sourceSession;
const txn = this.db.transaction(() => {
const created = this.createInTransactionalContext(input);
if (this.outbox && input.nudge !== false) {
try {
// A savepoint removes any partial intent when staging fails, while
// preserving ordinary create's existing acceptance of the task.
this.db.transaction(() => this.stageWakeIntent(created.qitemId, input.sourceSession, input.destinationSession, input.identityProvenance ?? null, input.nudge, input.wakeTrigger))();
this.db.transaction(() => this.stageWakeIntent(created.qitemId, wakeFrom, input.destinationSession, input.identityProvenance ?? null, input.nudge, input.wakeTrigger))();
wakeStaged = true;
} catch (err) {
this.recordNudgeAttempt(created.qitemId, `failed:wake not retained: ${err instanceof Error ? err.message : String(err)}`);
@@ -1769,7 +1772,7 @@ export class QueueRepository {
if (this.outbox) {
if (wakeStaged) this.deliverWakeIntentAfterCommit(`${WAKE_INTENT_PREFIX}${id}`);
} else {
await this.maybeNudge(id, input.destinationSession, input.nudge, input.sourceSession, undefined, input.wakeTrigger);
await this.maybeNudge(id, input.destinationSession, input.nudge, wakeFrom, undefined, input.wakeTrigger);
}
return this.getByIdOrThrow(id);
}
@@ -40,7 +40,7 @@ export interface ScopeApproveInput {
* delegation). */
actorSession: string;
/** P21 era-stamp: how actorSession was established. The route passes `transport:v1` when it derived
* the actor from the authenticated transport chokepoint; omitted (null) ⇒ claimed-era (a direct
* the actor from the transport chokepoint; omitted (null) ⇒ claimed-era (a direct
* caller / pre-P21 row), rendered "recorded (pre-verification era)", never re-labeled. */
identityProvenance?: string | null;
/** DELEGATED APPROVAL: whose decision this stamp records when an agent
@@ -1567,7 +1567,7 @@ export function readInputBox(pane: string | null, runtime: "claude-code" | "code
}
export interface SendOpts {
/** Internal provenance derived by the authenticated transport route. */
/** Internal provenance derived by the transport route. */
identityProvenance?: string;
/** Internal wake provenance, never accepted from the public transport route. */
queueWake?: boolean;
+9 -1
View File
@@ -21,6 +21,12 @@
const SENDER_FALLBACK = "<unknown sender>";
/** OpenRig's own sender: the daemon (`daemon@kernel`, the scheduler's existing name) or a subsystem actor (`system:health`).
* No seat answers at these addresses, so their envelope doesn't offer a reply. (`…@system` isn't matched: a rig may be named
* `system`, and its seats are real senders.) */
export const OPENRIG_SENDER = "daemon@kernel";
export const isOpenRigSender = (sender: string): boolean => sender === OPENRIG_SENDER || sender.startsWith("system:");
/** Send/broadcast header envelope metadata (ruling 03c35295). The ENVELOPE carries the machine truth;
* the rendered header is a PROJECTION of it. Both twins (this + cli wrapSendBody) share this shape. */
export interface EnvelopeScope {
@@ -116,6 +122,8 @@ export function wrapPaneEnvelope(
}
const reply = senderLabel.endsWith("@external")
? `↩ Reply if needed: rig queue create --destination ${senderLabel} --summary "<one line>" --body "..." --verify`
: `↩ Reply: rig send ${senderLabel} "..."`;
: isOpenRigSender(senderLabel)
? "↩ Sent by OpenRig itself, not by a seat. Act on the row it names."
: `↩ Reply: rig send ${senderLabel} "..."`;
return [...header, "---", body, "---", reply].join("\n");
}
@@ -350,7 +350,7 @@ export function missionControlRoutes(opts?: MissionControlRoutesOpts): Hono {
// never refused, never laundered. Runs BEFORE the local write AND the cross-host forward.
const identity = resolveActorWithDeferral(c, { verb: `mission-control ${body.verb}`, bodyClaim: body.actorSession });
if (!identity.ok) return identity.response;
// The SOLE provenance decider (rail 2): transport:v1 ONLY when transport proved it at THIS hop; relay:v1
// The SOLE provenance decider (rail 2): transport:v1 ONLY when the header named the actor at THIS hop; relay:v1
// one hop away; claimed:v1 for the UI deferral OR an unpropagatable/legacy marker (degrade-down default).
const recordedProvenance = resolveRecordedProvenance(c, identity);
if (typeof body.hostId === "string" && body.hostId !== "" && body.hostId !== LOCAL_HOST_ID) {
@@ -385,7 +385,7 @@ export function missionControlRoutes(opts?: MissionControlRoutesOpts): Hono {
"X-OpenRig-Session": identity.session,
"X-OpenRig-Relay": getSelfHostId() ?? "unknown",
// Carry the provenance THIS hop resolved so the origin NEVER launders a claimed-era actor into a
// verified one: identity.provenance is transport:v1 (CLI) or claimed:v1 (UI deferral). The origin's
// transport-derived one: identity.provenance is transport:v1 (CLI) or claimed:v1 (UI deferral). The origin's
// resolveRecordedProvenance degrades — a transport:v1 marker ⇒ relay:v1, everything else ⇒ claimed:v1.
// (Literal capitalized key, matching the sibling X-OpenRig-Session/Relay send convention; the
// receive side reads it case-insensitively via IDENTITY_PROVENANCE_HEADER.)
+6 -6
View File
@@ -601,7 +601,7 @@ export function queueRoutes(): Hono {
summary: body.summary,
evidenceRef: body.evidenceRef,
nudge: (body as { nudge?: boolean }).nudge,
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
});
const advisory = destinationAdvisory(c, item.destinationSession);
return c.json({ ...item, ...(advisory ? { advisories: [advisory] } : {}) }, 201);
@@ -712,7 +712,7 @@ export function queueRoutes(): Hono {
wakeAfterSeconds: body.wakeAfterSeconds,
summary: body.summary,
evidenceRef: body.evidenceRef,
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
});
return c.json(item);
} catch (err) {
@@ -797,7 +797,7 @@ export function queueRoutes(): Hono {
evidenceRef: body.evidenceRef,
nudge: (body as { nudge?: boolean }).nudge,
humanAlert: body.humanAlert,
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
});
const advisory = destinationAdvisory(c, result.created.destinationSession);
return c.json({ ...result, ...(advisory ? { advisories: [advisory] } : {}) }, 201);
@@ -881,7 +881,7 @@ export function queueRoutes(): Hono {
evidenceRef: body.evidenceRef,
nudge: body.nudge,
humanAlert: body.humanAlert,
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
});
const advisory = destinationAdvisory(c, result.created.destinationSession);
return c.json({ ...result, ...(advisory ? { advisories: [advisory] } : {}) }, 201);
@@ -1269,7 +1269,7 @@ export function queueRoutes(): Hono {
// ---- Inbox routes (mailbox) ----
app.post("/inbox/drop", async (c) => {
// P18 sender-provenance: the sender is DERIVED from the authenticated transport header (stamped
// P18 sender-provenance: the sender is DERIVED from the transport header (stamped
// once by the CLI from the seat env), NEVER from the request body. senderSession/authenticatedSender
// are intentionally NOT read from the body — a body-supplied identity is forgeable false history in
// the channel of record (inbox_entries.sender_session → absorb → the receiver's queue).
@@ -1378,7 +1378,7 @@ export function queueRoutes(): Hono {
tags: body.tags,
urgency: body.urgency,
auditPointer: body.auditPointer,
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
});
return c.json(entry, 201);
});
@@ -4,8 +4,10 @@ import { getSelfHostId } from "../domain/hosts/fanout-contract.js";
/**
* P21 sender-provenance chokepoint — the ONE shared route helper that generalizes P18's inline
* `/inbox/drop` form (b2437104) across every caller-identity site. The acting seat's identity is
* DERIVED from the authenticated transport header the CLI's DaemonClient stamps once from the seat
* env (X-OpenRig-Session), and the row records WHICH ERA that derivation came from.
* DERIVED from the transport header (X-OpenRig-Session) that the CLI's DaemonClient stamps from the seat's
* environment (OPENRIG_SESSION_NAME), and the row records WHICH ERA that derivation came from. The header
* isn't authentication: a process that can reach the daemon can set it. `transport:v1` records how the
* name was established (from the header, not a body claim), which is what a trusted network needs.
*
* P18 DELIVER-AND-LABEL (founder ruling, over-engineering audit — the refusals are deleted). The two
* refusals this helper used to raise both asserted "this sender is ILLEGITIMATE" when the only thing
@@ -16,7 +18,7 @@ import { getSelfHostId } from "../domain/hosts/fanout-contract.js";
* `claimed:v1`.
* - 409 `identity_mismatch` (body claim ≠ transport identity) — DELETED (PM, 2026-08-11, ruling (A):
* both refusals die, folded into this ONE sweep — the earlier one-atom-each split was retired as
* unnecessary ceremony). A certified wire identity is exactly why refusing was wrong: the wire decides
* unnecessary ceremony). The wire identity is exactly why refusing was wrong: the wire decides
* the actor and the body never does, so a disagreeing body claim is NOISE TO BE SUPERSEDED, not a
* reason to refuse. Deliver under the wire identity, labelled transport:v1; the discrepancy is NOT
* persisted (no new field/schema). The byte-identical 409 in resolveActorWithDeferral (below) is
@@ -68,7 +70,7 @@ function transportProvenance(c: Context): Exclude<IdentityProvenance, "relay:v1"
export type SenderIdentity =
// `provenance` is the NON-RELAY subset — this helper only ever derives locally: transport:v1 when the
// header proved it here, claimed:v1 when it did not. relay:v1 belongs to the cross-host forward and is
// header named the actor here, claimed:v1 when it did not. relay:v1 belongs to the cross-host forward and is
// decided by resolveRecordedProvenance, never here.
| { ok: true; session: string; provenance: Exclude<IdentityProvenance, "relay:v1"> }
| { ok: false; response: Response };
@@ -81,7 +83,7 @@ export function requireSenderIdentity(
const session = transportSenderSession(c);
const claim = opts?.bodyClaim?.trim();
if (session) {
// Transport path — the header PROVED the actor at this hop. P18 SWEEP: the wire SUPERSEDES any body
// Transport path — the header named the actor at this hop. P18 SWEEP: the wire SUPERSEDES any body
// claim (the 409 identity_mismatch refusal is retired). A disagreeing body actor is noise to be
// superseded, not an attack to refuse — the wire decides the actor, the body never does. Deliver
// under the transport seat identity, preserving any unknown-origin marker; the discrepancy is not
@@ -97,7 +99,7 @@ export function requireSenderIdentity(
response: c.json({
error: "actor_required",
message:
`Cannot record ${verb}: no authenticated transport identity (X-OpenRig-Session absent) and no ` +
`Cannot record ${verb}: no transport identity (no X-OpenRig-Session header, which the CLI sets from the seat's OPENRIG_SESSION_NAME) and no ` +
"actor named in the request body. The channel of record needs an actor to attribute the row to — " +
"name one, or run from a managed seat so the identity is derived for you.",
}, 400),
@@ -162,7 +164,7 @@ export function resolveActorWithDeferral(
response: c.json({
error: "actor_required",
message:
`Refusing ${verb}: no authenticated transport identity (X-OpenRig-Session absent) and no body ` +
`Refusing ${verb}: no transport identity (no X-OpenRig-Session header, which the CLI sets from the seat's OPENRIG_SESSION_NAME) and no body ` +
"actor to record. The channel of record needs at least a claimed-era actor.",
}, 400),
};
@@ -172,7 +174,7 @@ export function resolveActorWithDeferral(
/**
* The wire header a FORWARDING daemon stamps to carry the provenance it resolved for the actor it is
* re-stamping, so the origin hop records the TRUTH about that actor's verification instead of inferring
* re-stamping, so the origin hop records how that actor was established instead of inferring
* it from the mere presence of a re-stamped X-OpenRig-Session. SAME token alphabet as the store
* (IdentityProvenance) — never an ad-hoc string. Its absence is meaningful (see resolveRecordedProvenance).
*/
@@ -182,19 +184,19 @@ export const RELAY_HEADER = "x-openrig-relay";
/**
* P21 §4 — the SOLE decider of the provenance RECORDED on a row (rail 2: exactly one place can ever say
* `transport:v1`, and only when the transport actually proved it at THIS hop). `identity` is the local
* `transport:v1`, and only when the header named the actor at THIS hop). `identity` is the local
* derivation from resolveActorWithDeferral (transport:v1 = header present; claimed:v1 = the deferral).
*
* Direct request (no relay hop): record exactly what this hop proved — identity.provenance verbatim.
* Direct request (no relay hop): record exactly what this hop saw — identity.provenance verbatim.
*
* Relayed request (a forwarding daemon re-stamped the header): NEVER `transport:v1` — the actor was, at
* best, verified ONE HOP AWAY, so the strongest honest claim is `relay:v1`, and ONLY when the forwarder
* best, named by a header ONE HOP AWAY, so the strongest honest claim is `relay:v1`, and ONLY when the forwarder
* explicitly carried a `transport:v1` marker. Rail 1 — DEGRADE DOWN AS THE DEFAULT BRANCH: any other
* carried value, AND a MISSING marker (an old forwarder that predates this plumbing), records `claimed:v1`.
* NB the honesty nuance: a missing marker and a forwarder-declared `claimed:v1` are INDISTINGUISHABLE here
* and that is correct — both are "not verified at this boundary". `claimed:v1` on a relayed row therefore
* means "unverified", NEVER proof the origin action was a UI tap. Uncertainty weakens the claim; it can
* never strengthen it — a claimed-era actor can never be laundered into a verified one across a hop.
* never strengthen it — a claimed-era actor can never be laundered into a transport-derived one across a hop.
*/
export function resolveRecordedProvenance(
c: Context,
+2 -2
View File
@@ -29,7 +29,7 @@ export function scopeApproveRoutes(): Hono {
return c.json({ error: "scope_tier_invalid", message: "scopeTier must be 'slice' or 'mission'" }, 400);
}
if (!body.scopePath) return c.json({ error: "scope_path_required", message: "scopePath is required" }, 400);
// P21 I1 (the signing surface): the approver identity is DERIVED from the authenticated transport
// P21 I1 (the signing surface): the approver identity is DERIVED from the transport
// header, never the request body. body.actorSession is the transitional adopt-and-drop claim —
// recorded beside the wire identity; P18 deliver-and-label: a different claim is SUPERSEDED by the
// header (transport:v1), not refused; absent header + no body actor → 400 actor_required (parameter
@@ -61,7 +61,7 @@ export function scopeApproveRoutes(): Hono {
missionsRoot: typeof body.missionsRoot === "string" ? body.missionsRoot : null,
approvalScope,
actorSession: identity.session, // transport-derived, authoritative
identityProvenance: resolveRecordedProvenance(c, identity), // P21 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
onBehalfOf: body.onBehalfOf ?? null,
reApprove: body.reApprove === true,
reason: typeof body.reason === "string" ? body.reason : null,
+1 -1
View File
@@ -84,7 +84,7 @@ export function streamRoutes(): Hono {
hintDestination: body.hintDestination ?? null,
hintTags: body.hintTags ?? null,
interrupt: body.interrupt,
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header proved it here, else claimed:v1 (resolveRecordedProvenance degrades)
identityProvenance: resolveRecordedProvenance(c, identity), // P21 §4 era-stamp: transport:v1 if the header named the actor here, else claimed:v1 (resolveRecordedProvenance degrades)
});
return c.json(item, 201);
});
+4 -1
View File
@@ -274,7 +274,10 @@ describe("UI Enhancement Pack v0 — /api/files routes", () => {
body: JSON.stringify({ root: "workspace", path: "STEERING.md", content: "x", expectedMtime, expectedContentHash }),
});
expect(res.status).toBe(400);
expect(((await res.json()) as { error: string }).error).toBe("actor_required");
const err = (await res.json()) as { error: string; message: string };
expect(err.error).toBe("actor_required");
expect(err.message).toContain("no transport identity (no X-OpenRig-Session header, which the CLI sets from the seat's OPENRIG_SESSION_NAME)");
expect(err.message).not.toMatch(/authenticated/i);
});
it("rejects missing required fields with 400", async () => {
@@ -30,6 +30,18 @@ describe("wrapPaneEnvelope — slice 23 envelope renderer (daemon-side)", () =>
expect(wrapPaneEnvelope("driver@external-tools", "guard@rig", "Status.")).toContain('↩ Reply: rig send driver@external-tools "..."');
});
// Daemon-only: OpenRig's own senders never send through the CLI twin. A seat's name is `{pod}-{member}@{rig}`, so it can't be
// `daemon@kernel` or a `system:` actor.
it("offers no reply when OpenRig itself sent it (daemon@kernel, a system: actor); a seat on a rig named system still gets one", () => {
for (const sender of ["daemon@kernel", "system:health"]) {
const out = wrapPaneEnvelope(sender, "driver@rig", "Queue handoff: qitem-X - check your queue.");
expect(out).toContain(`From: ${sender}`);
expect(out.split("\n").at(-1)).toBe("↩ Sent by OpenRig itself, not by a seat. Act on the row it names.");
expect(out).not.toContain("rig send");
}
expect(wrapPaneEnvelope("ops-lead@system", "driver@rig", "Status.")).toContain('↩ Reply: rig send ops-lead@system "..."');
});
it("preserves the original body verbatim between the dash separators", () => {
const body = "Multi-line\nbody with\nthree lines.";
const out = wrapPaneEnvelope("a@r", "b@r", body);
@@ -895,6 +895,9 @@ describe("queue routes", () => {
const err = await res.json() as { error: string; message: string };
expect(err.error).toBe("actor_required");
expect(err.message).toMatch(/X-OpenRig-Session/);
// The header is set by the CLI from the seat's environment; it isn't called authenticated.
expect(err.message).toContain("no transport identity (no X-OpenRig-Session header, which the CLI sets from the seat's OPENRIG_SESSION_NAME)");
expect(err.message).not.toMatch(/authenticated/i);
});
});
@@ -148,6 +148,28 @@ describe("queue wakes: who triggered the wake decides its source", () => {
await vi.waitFor(() => expect(f.typed.join("\n")).toContain(peerAsk.qitemId));
});
it("a row OpenRig creates itself is typed as from OpenRig, not from the seat it names as source; the row keeps its source", async () => {
const f = fixture();
await f.level("openrig-urgent");
const finding = await f.queue.create({ sourceSession: "peer@test", destinationSession: seat, body: "Stuck sweep: unclaimed", wakeTrigger: "openrig" });
const health = await f.queue.create({ sourceSession: "system:health", destinationSession: seat, body: "Health finding", wakeTrigger: "openrig" });
const peerAsk = await f.queue.create({ sourceSession: "peer@test", destinationSession: seat, body: "A deliberate ask" });
const wakeFor = async (id: string) => { await vi.waitFor(() => expect(f.typed.join("\n")).toContain(id)); return f.typed.find((t) => t.includes(id))!; };
const findingWake = await wakeFor(finding.qitemId);
expect(findingWake).toContain("From: daemon@kernel");
expect(findingWake).not.toContain("peer@test");
expect(findingWake).toContain("↩ Sent by OpenRig itself, not by a seat. Act on the row it names.");
expect(f.queue.getById(finding.qitemId)!.sourceSession).toBe("peer@test");
// A source that's already OpenRig's keeps its more specific name.
expect(await wakeFor(health.qitemId)).toContain("From: system:health");
// A seat's own row still wakes from that seat, with a reply (held at this level).
await vi.waitFor(() => expect(f.queue.getById(peerAsk.qitemId)!.lastNudgeResult).toBe("retained:typing_guard"));
const askWake = f.held().find((h) => h.body.includes(peerAsk.qitemId))!;
expect(askWake).toMatchObject({ senderSession: "peer@test" });
expect(askWake.body).toContain("From: peer@test");
expect(askWake.body).toContain('↩ Reply: rig send peer@test "..."');
});
it("OpenRig's own urgent row is marked urgent by OpenRig, not by the person it's about", async () => {
const f = fixture();
await f.level("urgent-only");
@@ -170,6 +192,7 @@ describe("an urgent queue wake shows who marked it, typed or held, exactly once"
expect(heldFor(agentRow.qitemId)).toMatchObject({ urgency: "urgent", senderSession: "peer@test" });
expect(heldFor(agentRow.qitemId).body).toContain("Urgent (marked by peer@test):");
expect(heldFor(openrigRow.qitemId).body).toContain("Urgent (marked by OpenRig):");
expect(heldFor(openrigRow.qitemId)).toMatchObject({ senderSession: "daemon@kernel" });
expect(lines(heldFor(openrigRow.qitemId).body)).toBe(1);
});