Skip to content

Commit f35d0ea

Browse files
committed
fix(permission): clear denials only on operator inbound
Mailbox, fleet-dry, and background-shell wakes are also message.received and must not forget a deny the operator just made.
1 parent 5d67a01 commit f35d0ea

3 files changed

Lines changed: 51 additions & 3 deletions

File tree

‎src/agent/director.ts‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
type CompactionGovernor,
2323
} from "./compaction.js";
2424
import { onTurnBoundary } from "./reactor-events.js";
25+
import { isOperatorOriginated } from "./message-provenance.js";
2526
import { type } from "arktype";
2627
import {
2728
applyManageTasks,
@@ -1019,7 +1020,15 @@ class ChatDirectorImpl extends DefaultDirector {
10191020
this.toolOnlyStreak = 0;
10201021
this.toolOnlyNudgeFired = false;
10211022
this.pendingToolOnlyNudge = false;
1022-
this.clearDenials?.();
1023+
// Occupancy mailbox / fleet-dry / bg-shell inbounds are also
1024+
// message.received; only a human prompt may forget cached denies.
1025+
const inboundFlags =
1026+
"flags" in event.message && Array.isArray(event.message.flags)
1027+
? event.message.flags.filter(
1028+
(flag): flag is string => typeof flag === "string",
1029+
)
1030+
: undefined;
1031+
if (isOperatorOriginated(inboundFlags)) this.clearDenials?.();
10231032
}
10241033
if (onTurnBoundary(event)) this.inferenceRecoveries = 0;
10251034

‎src/director.test.ts‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -740,7 +740,11 @@ describe("open-task termination guard", () => {
740740
await director.decide(
741741
{
742742
type: "message.received",
743-
message: { role: "user", content: "just talk" },
743+
message: {
744+
role: "user",
745+
content: "just talk",
746+
flags: ["operator-originated"],
747+
},
744748
} as unknown as ReactorInboundEvent,
745749
mockState,
746750
mockCapabilities,
@@ -757,6 +761,28 @@ describe("open-task termination guard", () => {
757761
expect(next.some((a) => a.type === "infer")).toBe(true);
758762
expect(cleared).toBe(1);
759763
});
764+
765+
test("mailbox inbound does not clear cached denies", async () => {
766+
const director = createChatDirector("base", [], {});
767+
let cleared = 0;
768+
director.setClearDenials(() => {
769+
cleared++;
770+
});
771+
await director.decide(
772+
makeToolErrorEvent("c", declined),
773+
mockState,
774+
mockCapabilities,
775+
);
776+
await director.decide(
777+
{
778+
type: "message.received",
779+
message: { role: "user", content: "worker report" },
780+
} as unknown as ReactorInboundEvent,
781+
mockState,
782+
mockCapabilities,
783+
);
784+
expect(cleared).toBe(0);
785+
});
760786
});
761787

762788
describe("chatDirector compaction", () => {

‎tests/integration/harness.ts‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { type } from "arktype";
3232

3333
import { createAgentWithLiveToolDispatch } from "../../src/agent/live-tool-dispatch.js";
3434
import { createChatDirector } from "../../src/agent/director.js";
35+
import { OPERATOR_ORIGINATED_FLAG } from "../../src/agent/message-provenance.js";
3536
import {
3637
readSourceCredentialMaterial,
3738
registerSourceCredential,
@@ -304,7 +305,19 @@ export async function runUntilDone(
304305

305306
const collectTask = collect;
306307
const sendResult = await Promise.all([
307-
session.agent.send(message).then((result) => {
308+
session.agent.send({
309+
ref: { uid: 1, mailbox: "INBOX" },
310+
headers: {
311+
from: "user@local",
312+
to: ["agent@local"],
313+
date: new Date().toISOString(),
314+
messageId: `<${crypto.randomUUID()}@local>`,
315+
interchangeType: "conversation.message",
316+
},
317+
flags: [OPERATOR_ORIGINATED_FLAG],
318+
content: message,
319+
signatureStatus: "missing",
320+
}).then((result) => {
308321
turnComplete = true;
309322
return result;
310323
}),

0 commit comments

Comments
 (0)