mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 22:58:42 -10:00
feat(security): P21 I3 CLI reconcile — queue-spine verbs stop sending body identity
The I3 fold made the daemon queue routes derive the actor/sender from the X-OpenRig-Session transport header (requireSenderIdentity). This is the CLI half: the queue verbs stop sending forgeable body-identity claims and stop advertising the identity flags as authoritative — matching the resolve/update/block adopt-drops and the P21 charter (body identity REMOVED, not just tolerated). Verbs reconciled (deprecate+ignore the flag, drop the body claim, add an env pre-check that fails-loud locally when the seat env is unset — the header source): - create --source → sourceSession dropped - claim --destination → destinationSession dropped - unclaim --destination → destinationSession dropped - update --actor → actorSession dropped - block --actor → actorSession dropped (writes via the same update route) - handoff --from → fromSession dropped - handoff-and-complete --from → fromSession dropped - outbox-record --sender → senderSession dropped (--sender was still required+sent) TARGET fields stay body fields (destinationSession/toSession on create/handoff — the recipient, not the caller). The daemon tolerates an equal body claim in the adopt-drop window, so no live breakage: the CLI flags defaulted from the seat env == the header, header==claim==tolerated; the 409 only ever fired on a genuine flag!=env mismatch (the forgery this closes). Tests: 5 CLI unit tests revised from the pre-P21 "sends identity from env in the body" contract to "sends NO body identity claim (daemon derives from the header); an explicit/forged flag is dropped; the TARGET stays". cli tsc0; CLI queue suite 100/100; daemon queue cluster 130/130; daemon tsc0. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
be4790265a
commit
081c1c9308
2 files changed
+111
-85
No files matched your search
@@ -306,7 +306,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
cmd
|
||||
.command("create")
|
||||
.description("Create a new qitem")
|
||||
.requiredOption("--source <session>", "Source session")
|
||||
.option("--source <session>", "(deprecated, ignored) the source is derived from the seat env (X-OpenRig-Session); P21 I3 made the create route derive it from the transport header")
|
||||
.requiredOption("--destination <session>", "Destination session (the seat that owns the work)")
|
||||
.option("--body <text>", "Qitem body inline (use - to read from stdin; mutually exclusive with --body-file)")
|
||||
.option("--body-file <path>", "Read qitem body from a file path (use - for stdin; mutually exclusive with --body). Kills the backtick-shell-corruption class for multiline bodies.")
|
||||
@@ -326,7 +326,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
.option("--no-nudge", "Suppress the default destination nudge (cold-queue)")
|
||||
.option("--json", "JSON output for agents")
|
||||
.action(async (opts: {
|
||||
source: string;
|
||||
source?: string;
|
||||
destination: string;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
@@ -382,6 +382,9 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
"warning: rig queue create called without --summary. New qitems should carry a short human-readable summary; the Story node degrades to a body truncation without it. A good summary is 1-2 plain sentences a human skims in the needs-you view — what the work is and why it needs this seat, not the agent-speak --body. Proceeding (pre-18 callers exempt).\n"
|
||||
);
|
||||
}
|
||||
// P21 I3 reconcile: the source is DERIVED from the seat env (X-OpenRig-Session) — --source
|
||||
// deprecated + ignored, no body sourceSession. Verify the env or the daemon refuses-loud 401.
|
||||
if (!resolveCurrentSession(undefined, "source")) return;
|
||||
const deps = getDeps();
|
||||
// OPR.0.3.2.21.FR-4(b) — first-class --mission / --slice flags
|
||||
// translate to canonical mission:<id> / slice:<id> tags. Composes
|
||||
@@ -419,7 +422,6 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
}
|
||||
const res = await client.post<Record<string, unknown>>("/api/queue/create", {
|
||||
qitemId: opts.id,
|
||||
sourceSession: opts.source,
|
||||
destinationSession: hostResolved.destination,
|
||||
body: resolvedBody,
|
||||
summary: opts.summary,
|
||||
@@ -441,16 +443,15 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
cmd
|
||||
.command("claim <qitemId>")
|
||||
.description("Claim a qitem (pending → in-progress); computes closure_required_at from tier")
|
||||
.option("--destination <session>", "Destination session claiming the qitem (defaults to OPENRIG_SESSION_NAME)")
|
||||
.option("--destination <session>", "(deprecated, ignored) the claimant is derived from the seat env (X-OpenRig-Session); P21 I3 made the claim route derive it from the transport header")
|
||||
.option("--json", "JSON output for agents")
|
||||
.action(async (qitemId: string, opts: { destination?: string; json?: boolean }) => {
|
||||
const destination = resolveCurrentSession(opts.destination, "destination");
|
||||
if (!destination) return;
|
||||
// P21 I3 reconcile: the claimant is DERIVED from the seat env — --destination deprecated + ignored,
|
||||
// no body claim. Verify the env (the header source) or the daemon refuses-loud 401.
|
||||
if (!resolveCurrentSession(undefined, "destination")) return;
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/claim`, {
|
||||
destinationSession: destination,
|
||||
});
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/claim`, {});
|
||||
printResult(opts.json ?? false, res.data, res.status);
|
||||
});
|
||||
});
|
||||
@@ -458,16 +459,16 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
cmd
|
||||
.command("unclaim <qitemId>")
|
||||
.description("Release a claimed qitem (in-progress → pending)")
|
||||
.option("--destination <session>", "Destination session releasing the qitem (defaults to OPENRIG_SESSION_NAME)")
|
||||
.option("--destination <session>", "(deprecated, ignored) the releaser is derived from the seat env (X-OpenRig-Session); the unclaim route derives it from the transport header")
|
||||
.option("--reason <text>", "Reason for unclaim", "manual")
|
||||
.option("--json", "JSON output for agents")
|
||||
.action(async (qitemId: string, opts: { destination?: string; reason: string; json?: boolean }) => {
|
||||
const destination = resolveCurrentSession(opts.destination, "destination");
|
||||
if (!destination) return;
|
||||
// P21 I3 reconcile: the releaser is DERIVED from the seat env — --destination deprecated + ignored,
|
||||
// no body claim. Verify the env or the daemon refuses-loud 401.
|
||||
if (!resolveCurrentSession(undefined, "destination")) return;
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/unclaim`, {
|
||||
destinationSession: destination,
|
||||
reason: opts.reason,
|
||||
});
|
||||
printResult(opts.json ?? false, res.data, res.status);
|
||||
@@ -477,7 +478,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
cmd
|
||||
.command("update <qitemId>")
|
||||
.description("Mutate qitem state. state=done REQUIRES --closure-reason (one of: handed_off_to, blocked_on, denied, canceled, no-follow-on, escalation). Closure ≠ acceptance: handed_off_to records delivery to the next stage; acceptance is the next stage's verdict on its own qitem, not this closure.")
|
||||
.option("--actor <session>", "Actor session performing the transition (defaults to OPENRIG_SESSION_NAME)")
|
||||
.option("--actor <session>", "(deprecated, ignored) the actor is derived from the seat env (X-OpenRig-Session); P21 I3 made the update route derive it from the transport header")
|
||||
.requiredOption("--state <state>", "New state: pending | in-progress | done | blocked | failed | denied | canceled | handed-off")
|
||||
.option("--closure-reason <reason>", "Required for state=done")
|
||||
.option("--closure-target <target>", "Required for handed_off_to, blocked_on, escalation")
|
||||
@@ -497,12 +498,13 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
note?: string;
|
||||
json?: boolean;
|
||||
}) => {
|
||||
const actor = resolveCurrentSession(opts.actor, "actor");
|
||||
if (!actor) return;
|
||||
// P21 I3 reconcile: the actor is DERIVED from the seat env (X-OpenRig-Session, stamped by
|
||||
// DaemonClient) — --actor is deprecated + ignored, no body actorSession. Verify the env (the
|
||||
// header source) or the daemon refuses-loud 401 unattributable_sender. Matches `resolve`.
|
||||
if (!resolveCurrentSession(undefined, "actor")) return;
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/update`, {
|
||||
actorSession: actor,
|
||||
state: opts.state,
|
||||
closureReason: opts.closureReason,
|
||||
closureTarget: opts.closureTarget,
|
||||
@@ -526,7 +528,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
.command("block <qitemId>")
|
||||
.description("Park a qitem as state=blocked on a blocker (qitem id, or a human seat for a needs-human park). Non-terminal: the owner keeps the item; resolution returns to it (rig queue resolve).")
|
||||
.requiredOption("--on <blocker>", "The blocker: a qitem id, or a human-seat session (e.g. human-review@kernel) for the needs-human park")
|
||||
.option("--actor <session>", "Acting session (defaults to OPENRIG_SESSION_NAME)")
|
||||
.option("--actor <session>", "(deprecated, ignored) the actor is derived from the seat env (X-OpenRig-Session); the park writes via the same P21 I3 header-deriving update route")
|
||||
.option("--summary <text>", "Plain-language summary of the decision owed (required for human-seat parks unless already on the item)")
|
||||
.option("--evidence-ref <path>", "Durable artifact the human judges (required for human-seat parks unless already on the item)")
|
||||
.option("--note <text>", "Transition note for the audit log")
|
||||
@@ -539,12 +541,12 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
note?: string;
|
||||
json?: boolean;
|
||||
}) => {
|
||||
const actor = resolveCurrentSession(opts.actor, "actor");
|
||||
if (!actor) return;
|
||||
// P21 I3 reconcile: actor DERIVED from the seat env (X-OpenRig-Session) — --actor deprecated +
|
||||
// ignored, no body actorSession. Verify the env or the daemon refuses-loud. Matches `resolve`.
|
||||
if (!resolveCurrentSession(undefined, "actor")) return;
|
||||
const deps = getDeps();
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/update`, {
|
||||
actorSession: actor,
|
||||
state: "blocked",
|
||||
blockedOn: opts.on,
|
||||
summary: opts.summary,
|
||||
@@ -601,7 +603,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
cmd
|
||||
.command("handoff <qitemId>")
|
||||
.description("Transactional handoff: closes source as handed-off + creates new qitem owned by --to")
|
||||
.option("--from <session>", "Source seat handing off (defaults to OPENRIG_SESSION_NAME)")
|
||||
.option("--from <session>", "(deprecated, ignored) the handing-off seat is derived from the seat env (X-OpenRig-Session); P21 I3 made the handoff route derive it from the transport header")
|
||||
.requiredOption("--to <session>", "Destination seat receiving the new qitem")
|
||||
.option("--body <text>", "New qitem body inline (use - to read from stdin; mutually exclusive with --body-file). Omit both to keep the source body.")
|
||||
.option("--body-file <path>", "Read the new qitem body from a file path (use - for stdin; mutually exclusive with --body). Kills the backtick-shell-corruption class.")
|
||||
@@ -633,8 +635,9 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
nudge?: boolean;
|
||||
json?: boolean;
|
||||
}) => {
|
||||
const from = resolveCurrentSession(opts.from, "from");
|
||||
if (!from) return;
|
||||
// P21 I3 reconcile: the handing-off seat is DERIVED from the seat env (X-OpenRig-Session) —
|
||||
// --from deprecated + ignored, no body fromSession. Verify the env or the daemon refuses-loud 401.
|
||||
if (!resolveCurrentSession(undefined, "from")) return;
|
||||
// slice-08 OPR.0.4.7.8 — body-input parity. Resolve through the shipped
|
||||
// resolveQueueBody ONLY when a body source is supplied; neither preserves
|
||||
// today's source-body default (POST body undefined). Both/invalid reject
|
||||
@@ -674,7 +677,6 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
const tags = dedupedTags.length > 0 ? dedupedTags : undefined;
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/handoff`, {
|
||||
fromSession: from,
|
||||
toSession: hostResolved.destination,
|
||||
body: resolvedBody,
|
||||
summary: opts.summary,
|
||||
@@ -696,7 +698,7 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
.description(
|
||||
"Atomic close (state=done, closure_reason=handed_off_to) + create new qitem owned by --to. Variant of handoff that fully terminates the source qitem."
|
||||
)
|
||||
.option("--from <session>", "Source seat handing off (defaults to OPENRIG_SESSION_NAME)")
|
||||
.option("--from <session>", "(deprecated, ignored) the handing-off seat is derived from the seat env (X-OpenRig-Session); P21 I3 made the handoff route derive it from the transport header")
|
||||
.requiredOption("--to <session>", "Destination seat receiving the new qitem")
|
||||
.option("--body <text>", "New qitem body inline (use - to read from stdin; mutually exclusive with --body-file). Omit both to keep the source body.")
|
||||
.option("--body-file <path>", "Read the new qitem body from a file path (use - for stdin; mutually exclusive with --body). Kills the backtick-shell-corruption class.")
|
||||
@@ -728,8 +730,9 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
nudge?: boolean;
|
||||
json?: boolean;
|
||||
}) => {
|
||||
const from = resolveCurrentSession(opts.from, "from");
|
||||
if (!from) return;
|
||||
// P21 I3 reconcile: the handing-off seat is DERIVED from the seat env (X-OpenRig-Session) —
|
||||
// --from deprecated + ignored, no body fromSession. Verify the env or the daemon refuses-loud 401.
|
||||
if (!resolveCurrentSession(undefined, "from")) return;
|
||||
// slice-08 OPR.0.4.7.8 — body-input parity (same contract as handoff):
|
||||
// resolve only when a body source is supplied; neither keeps the
|
||||
// source-body default; both/invalid reject before daemon contact.
|
||||
@@ -767,7 +770,6 @@ export function queueCommand(depsOverride?: QueueDeps): Command {
|
||||
const tags = dedupedTags.length > 0 ? dedupedTags : undefined;
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>(`/api/queue/${encodeURIComponent(qitemId)}/handoff-and-complete`, {
|
||||
fromSession: from,
|
||||
toSession: hostResolved.destination,
|
||||
body: resolvedBody,
|
||||
summary: opts.summary,
|
||||
@@ -1080,7 +1082,7 @@ Examples:
|
||||
cmd
|
||||
.command("outbox-record")
|
||||
.description("Record an outbound dispatch in the sender's outbox")
|
||||
.requiredOption("--sender <session>", "Sender session")
|
||||
.option("--sender <session>", "(deprecated, ignored) the sender is derived from the seat env (X-OpenRig-Session); P21 I3 made the outbox-record route derive it from the transport header")
|
||||
.requiredOption("--destination <session>", "Destination session")
|
||||
.option("--body <text>", "Outbox body inline (use - to read from stdin; mutually exclusive with --body-file).")
|
||||
.option("--body-file <path>", "Read the outbox body from a file path (use - for stdin; mutually exclusive with --body). Kills the backtick-shell-corruption class.")
|
||||
@@ -1090,7 +1092,7 @@ Examples:
|
||||
.option("--id <outboxId>", "Idempotent outbox_id")
|
||||
.option("--json", "JSON output for agents")
|
||||
.action(async (opts: {
|
||||
sender: string;
|
||||
sender?: string;
|
||||
destination: string;
|
||||
body?: string;
|
||||
bodyFile?: string;
|
||||
@@ -1110,12 +1112,14 @@ Examples:
|
||||
emitBodyResolveError(err as Error & { fact?: string; consequence?: string; action?: string }, opts.json ?? false);
|
||||
return;
|
||||
}
|
||||
// P21 I3 reconcile: the sender is DERIVED from the seat env (X-OpenRig-Session) — --sender
|
||||
// deprecated + ignored, no body senderSession. Verify the env or the daemon refuses-loud 401.
|
||||
if (!resolveCurrentSession(undefined, "sender")) return;
|
||||
const deps = getDeps();
|
||||
const tags = opts.tags ? opts.tags.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
||||
await withClient(deps, async (client) => {
|
||||
const res = await client.post<unknown>("/api/queue/outbox/record", {
|
||||
outboxId: opts.id,
|
||||
senderSession: opts.sender,
|
||||
destinationSession: opts.destination,
|
||||
body: resolvedBody,
|
||||
tags,
|
||||
|
||||
@@ -101,28 +101,37 @@ describe("rig queue CLI", () => {
|
||||
expect(subs).toContain("show");
|
||||
});
|
||||
|
||||
it("create POSTs to /api/queue/create with sourceSession + destinationSession + body", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "qitem-x", state: "pending" } } },
|
||||
});
|
||||
const program = createProgram({ queueDeps: deps });
|
||||
program.exitOverride();
|
||||
await program.parseAsync([
|
||||
"node", "rig", "queue", "create",
|
||||
"--source", "alice@rig",
|
||||
"--destination", "bob@rig",
|
||||
"--body", "do thing",
|
||||
"--json",
|
||||
]);
|
||||
const create = calls.find((c) => c.path === "/api/queue/create");
|
||||
expect(create).toBeDefined();
|
||||
const body = create!.body as Record<string, unknown>;
|
||||
expect(body.sourceSession).toBe("alice@rig");
|
||||
expect(body.destinationSession).toBe("bob@rig");
|
||||
expect(body.body).toBe("do thing");
|
||||
// R1: commander's --no-nudge sets opts.nudge to true by default.
|
||||
// The CLI sends nudge: true, and the daemon treats nudge !== false as nudging.
|
||||
expect(body.nudge).toBe(true);
|
||||
it("create sends NO body sourceSession — the source derives from the transport header (X-OpenRig-Session); an explicit --source is dropped, never forwarded as a body claim", async () => {
|
||||
const saved = process.env["OPENRIG_SESSION_NAME"];
|
||||
process.env["OPENRIG_SESSION_NAME"] = "alice@rig"; // the seat env == the X-OpenRig-Session the DaemonClient stamps
|
||||
try {
|
||||
const { deps, calls } = makeDeps({
|
||||
routes: { "POST /api/queue/create": { status: 201, data: { qitemId: "qitem-x", state: "pending" } } },
|
||||
});
|
||||
const program = createProgram({ queueDeps: deps });
|
||||
program.exitOverride();
|
||||
await program.parseAsync([
|
||||
"node", "rig", "queue", "create",
|
||||
"--source", "forged@evil", // P21: IGNORED — must not ride the body
|
||||
"--destination", "bob@rig",
|
||||
"--body", "do thing",
|
||||
"--json",
|
||||
]);
|
||||
const create = calls.find((c) => c.path === "/api/queue/create");
|
||||
expect(create).toBeDefined();
|
||||
const body = create!.body as Record<string, unknown>;
|
||||
// P21 I3 reconcile: no body identity claim — the daemon derives the source from the header, and
|
||||
// the forged --source is DROPPED (not forwarded). The destination is the TARGET, a legit body field.
|
||||
expect(body.sourceSession).toBeUndefined();
|
||||
expect(body.destinationSession).toBe("bob@rig");
|
||||
expect(body.body).toBe("do thing");
|
||||
// R1: commander's --no-nudge sets opts.nudge to true by default.
|
||||
// The CLI sends nudge: true, and the daemon treats nudge !== false as nudging.
|
||||
expect(body.nudge).toBe(true);
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["OPENRIG_SESSION_NAME"];
|
||||
else process.env["OPENRIG_SESSION_NAME"] = saved;
|
||||
}
|
||||
});
|
||||
|
||||
// Slice-03 Atom 6b — --body-context snapshot + provenance rule.
|
||||
@@ -557,33 +566,42 @@ describe("rig queue CLI", () => {
|
||||
expect(out).toContain("validReasons");
|
||||
});
|
||||
|
||||
it("handoff-and-complete POSTs to /api/queue/:id/handoff-and-complete with from + to", async () => {
|
||||
const { deps, calls } = makeDeps({
|
||||
routes: {
|
||||
"POST /api/queue/qitem-src/handoff-and-complete": {
|
||||
status: 201,
|
||||
data: {
|
||||
closed: { state: "done", closureReason: "handed_off_to" },
|
||||
created: { state: "pending", qitemId: "qitem-new" },
|
||||
it("handoff-and-complete sends NO body fromSession — the handing-off seat derives from the transport header (X-OpenRig-Session); --from is dropped, --to stays the target", async () => {
|
||||
const saved = process.env["OPENRIG_SESSION_NAME"];
|
||||
process.env["OPENRIG_SESSION_NAME"] = "bob@rig"; // the seat env == the X-OpenRig-Session the DaemonClient stamps
|
||||
try {
|
||||
const { deps, calls } = makeDeps({
|
||||
routes: {
|
||||
"POST /api/queue/qitem-src/handoff-and-complete": {
|
||||
status: 201,
|
||||
data: {
|
||||
closed: { state: "done", closureReason: "handed_off_to" },
|
||||
created: { state: "pending", qitemId: "qitem-new" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const program = createProgram({ queueDeps: deps });
|
||||
program.exitOverride();
|
||||
await program.parseAsync([
|
||||
"node", "rig", "queue", "handoff-and-complete", "qitem-src",
|
||||
"--from", "bob@rig",
|
||||
"--to", "carol@rig",
|
||||
"--body", "carol's piece",
|
||||
"--json",
|
||||
]);
|
||||
const call = calls.find((c) => c.path === "/api/queue/qitem-src/handoff-and-complete");
|
||||
expect(call).toBeDefined();
|
||||
const body = call!.body as Record<string, unknown>;
|
||||
expect(body.fromSession).toBe("bob@rig");
|
||||
expect(body.toSession).toBe("carol@rig");
|
||||
expect(body.body).toBe("carol's piece");
|
||||
});
|
||||
const program = createProgram({ queueDeps: deps });
|
||||
program.exitOverride();
|
||||
await program.parseAsync([
|
||||
"node", "rig", "queue", "handoff-and-complete", "qitem-src",
|
||||
"--from", "forged@evil", // P21: IGNORED — must not ride the body
|
||||
"--to", "carol@rig",
|
||||
"--body", "carol's piece",
|
||||
"--json",
|
||||
]);
|
||||
const call = calls.find((c) => c.path === "/api/queue/qitem-src/handoff-and-complete");
|
||||
expect(call).toBeDefined();
|
||||
const body = call!.body as Record<string, unknown>;
|
||||
// P21 I3 reconcile: no body identity claim — the daemon derives fromSession from the header, and
|
||||
// the forged --from is DROPPED. toSession is the TARGET, a legit body field.
|
||||
expect(body.fromSession).toBeUndefined();
|
||||
expect(body.toSession).toBe("carol@rig");
|
||||
expect(body.body).toBe("carol's piece");
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["OPENRIG_SESSION_NAME"];
|
||||
else process.env["OPENRIG_SESSION_NAME"] = saved;
|
||||
}
|
||||
});
|
||||
|
||||
it("whoami GETs /api/queue/whoami with session + recentLimit query params", async () => {
|
||||
@@ -643,7 +661,7 @@ describe("rig queue CLI", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("claim defaults the destination from OPENRIG_SESSION_NAME when --destination is omitted", async () => {
|
||||
it("claim sends NO body destinationSession — the claimant derives from the transport header (X-OpenRig-Session); env present ⇒ POST fires", async () => {
|
||||
const saved = process.env["OPENRIG_SESSION_NAME"];
|
||||
process.env["OPENRIG_SESSION_NAME"] = "bob@rig";
|
||||
try {
|
||||
@@ -661,14 +679,16 @@ describe("rig queue CLI", () => {
|
||||
|
||||
const call = calls.find((c) => c.path === "/api/queue/qitem-x/claim");
|
||||
expect(call).toBeDefined();
|
||||
expect((call!.body as { destinationSession: string }).destinationSession).toBe("bob@rig");
|
||||
// P21 I3 reconcile: the claimant is NOT a body claim — the daemon derives it from the
|
||||
// X-OpenRig-Session header (env present ⇒ pre-check passes ⇒ POST fires). No forgeable body field.
|
||||
expect((call!.body as Record<string, unknown>).destinationSession).toBeUndefined();
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["OPENRIG_SESSION_NAME"];
|
||||
else process.env["OPENRIG_SESSION_NAME"] = saved;
|
||||
}
|
||||
});
|
||||
|
||||
it("update defaults the actor from OPENRIG_SESSION_NAME when --actor is omitted", async () => {
|
||||
it("update sends NO body actorSession — the actor derives from the transport header (X-OpenRig-Session); env present ⇒ POST fires", async () => {
|
||||
const saved = process.env["OPENRIG_SESSION_NAME"];
|
||||
process.env["OPENRIG_SESSION_NAME"] = "bob@rig";
|
||||
try {
|
||||
@@ -691,14 +711,15 @@ describe("rig queue CLI", () => {
|
||||
|
||||
const call = calls.find((c) => c.path === "/api/queue/qitem-x/update");
|
||||
expect(call).toBeDefined();
|
||||
expect((call!.body as { actorSession: string }).actorSession).toBe("bob@rig");
|
||||
// P21 I3 reconcile: the actor is NOT a body claim — the daemon derives it from the header.
|
||||
expect((call!.body as Record<string, unknown>).actorSession).toBeUndefined();
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["OPENRIG_SESSION_NAME"];
|
||||
else process.env["OPENRIG_SESSION_NAME"] = saved;
|
||||
}
|
||||
});
|
||||
|
||||
it("handoff defaults the source from OPENRIG_SESSION_NAME when --from is omitted", async () => {
|
||||
it("handoff sends NO body fromSession — the handing-off seat derives from the transport header (X-OpenRig-Session); env present ⇒ POST fires", async () => {
|
||||
const saved = process.env["OPENRIG_SESSION_NAME"];
|
||||
process.env["OPENRIG_SESSION_NAME"] = "bob@rig";
|
||||
try {
|
||||
@@ -720,7 +741,8 @@ describe("rig queue CLI", () => {
|
||||
|
||||
const call = calls.find((c) => c.path === "/api/queue/qitem-x/handoff");
|
||||
expect(call).toBeDefined();
|
||||
expect((call!.body as { fromSession: string }).fromSession).toBe("bob@rig");
|
||||
// P21 I3 reconcile: the handing-off seat is NOT a body claim — the daemon derives it from the header.
|
||||
expect((call!.body as Record<string, unknown>).fromSession).toBeUndefined();
|
||||
} finally {
|
||||
if (saved === undefined) delete process.env["OPENRIG_SESSION_NAME"];
|
||||
else process.env["OPENRIG_SESSION_NAME"] = saved;
|
||||
|
||||
Reference in new issue
Block a user