Skip to content

Commit 60e818f

Browse files
committed
fix(agents): forward channel identity to CLI hooks
1 parent 7e88c28 commit 60e818f

18 files changed

Lines changed: 210 additions & 5 deletions

src/agents/cli-runner.reliability.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1700,6 +1700,76 @@ describe("runCliAgent reliability", () => {
17001700
}
17011701
});
17021702

1703+
it("forwards channel identity context to CLI before_agent_run hooks", async () => {
1704+
supervisorSpawnMock.mockClear();
1705+
const hookRunner = {
1706+
hasHooks: vi.fn((hookName: string) => hookName === "before_agent_run"),
1707+
runBeforeAgentRun: vi.fn(async () => ({
1708+
pluginId: "policy-plugin",
1709+
decision: {
1710+
outcome: "block" as const,
1711+
reason: "sender scoped policy",
1712+
message: "The agent cannot read this message.",
1713+
},
1714+
})),
1715+
};
1716+
setHookRunnerForTest(hookRunner);
1717+
const { dir, sessionFile } = createSessionFile();
1718+
1719+
try {
1720+
const context = buildPreparedContext({
1721+
sessionKey: "agent:main:telegram:chat-1",
1722+
runId: "run-cli-channel-before-agent-run",
1723+
});
1724+
const result = await runPreparedCliAgent({
1725+
...context,
1726+
params: {
1727+
...context.params,
1728+
agentId: "main",
1729+
sessionFile,
1730+
workspaceDir: dir,
1731+
prompt: "sender scoped prompt",
1732+
messageChannel: "telegram",
1733+
messageProvider: "telegram",
1734+
currentChannelId: "telegram:chat-1",
1735+
senderId: "user-42",
1736+
senderIsOwner: true,
1737+
userTurnTranscriptRecorder: createCliUserTurnRecorder({
1738+
text: "sender scoped prompt",
1739+
sessionFile,
1740+
sessionKey: "agent:main:telegram:chat-1",
1741+
workspaceDir: dir,
1742+
}),
1743+
},
1744+
});
1745+
1746+
expect(result.payloads).toEqual([
1747+
{
1748+
text: "Your message could not be sent: The agent cannot read this message. (blocked by policy-plugin)",
1749+
isError: true,
1750+
},
1751+
]);
1752+
expect(supervisorSpawnMock).not.toHaveBeenCalled();
1753+
const beforeRunEvent = requireRecord(
1754+
callArg(hookRunner.runBeforeAgentRun, 0, 0, "before_agent_run event"),
1755+
"before_agent_run event",
1756+
);
1757+
expect(beforeRunEvent.channelId).toBe("chat-1");
1758+
expect(beforeRunEvent.senderId).toBe("user-42");
1759+
expect(beforeRunEvent.senderIsOwner).toBe(true);
1760+
const beforeRunContext = requireRecord(
1761+
callArg(hookRunner.runBeforeAgentRun, 0, 1, "before_agent_run context"),
1762+
"before_agent_run context",
1763+
);
1764+
expect(beforeRunContext.channel).toBe("telegram");
1765+
expect(beforeRunContext.chatId).toBe("chat-1");
1766+
expect(beforeRunContext.channelId).toBe("chat-1");
1767+
expect(beforeRunContext.senderId).toBe("user-42");
1768+
} finally {
1769+
fs.rmSync(dir, { recursive: true, force: true });
1770+
}
1771+
});
1772+
17031773
it("does not emit llm_output when the CLI run returns no assistant text", async () => {
17041774
const hookRunner = {
17051775
hasHooks: vi.fn((hookName: string) => hookName === "llm_output"),

src/agents/cli-runner.spawn.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,13 +654,17 @@ describe("runCliAgent spawn path", () => {
654654
currentChannelId: "telegram:-100123:topic:42",
655655
currentThreadTs: "42",
656656
currentMessageId: "reply-message-1",
657+
senderId: "sender-1",
658+
senderIsOwner: true,
657659
});
658660

659661
expect(params.messageChannel).toBe("telegram");
660662
expect(params.messageProvider).toBe("acp");
661663
expect(params.currentChannelId).toBe("telegram:-100123:topic:42");
662664
expect(params.currentThreadTs).toBe("42");
663665
expect(params.currentMessageId).toBe("reply-message-1");
666+
expect(params.senderId).toBe("sender-1");
667+
expect(params.senderIsOwner).toBe(true);
664668
expect(params.cwd).toBe("/tmp/task-repo");
665669
});
666670

src/agents/cli-runner.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,8 @@ export async function runPreparedCliAgent(
750750
}),
751751
channelId: hookContext.channelId,
752752
accountId: params.agentAccountId,
753+
senderId: params.senderId ?? undefined,
754+
senderIsOwner: params.senderIsOwner ?? undefined,
753755
},
754756
buildAgentHookContext(hookContext),
755757
);
@@ -901,6 +903,8 @@ export function buildRunClaudeCliAgentParams(params: RunClaudeCliAgentParams): R
901903
currentThreadTs: params.currentThreadTs,
902904
currentMessageId: params.currentMessageId,
903905
currentInboundAudio: params.currentInboundAudio,
906+
senderId: params.senderId,
907+
senderIsOwner: params.senderIsOwner,
904908
};
905909
}
906910

src/agents/cli-runner/prepare.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,9 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
596596
model: "test-model",
597597
timeoutMs: 1_000,
598598
runId: "run-test-turn-prepare",
599+
messageChannel: "telegram",
600+
currentChannelId: "chat-1",
601+
senderId: "user-456",
599602
config: createCliBackendConfig(),
600603
});
601604

@@ -610,10 +613,19 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
610613
queuedInjections: [],
611614
});
612615
const turnPrepareContext = agentTurnPrepareCalls[0]?.[1] as
613-
| { runId?: string; sessionKey?: string }
616+
| {
617+
channel?: string;
618+
chatId?: string;
619+
runId?: string;
620+
senderId?: string;
621+
sessionKey?: string;
622+
}
614623
| undefined;
615624
expect(turnPrepareContext?.runId).toBe("run-test-turn-prepare");
616625
expect(turnPrepareContext?.sessionKey).toBe("agent:main:test");
626+
expect(turnPrepareContext?.channel).toBe("telegram");
627+
expect(turnPrepareContext?.chatId).toBe("chat-1");
628+
expect(turnPrepareContext?.senderId).toBe("user-456");
617629
expect(hookRunner.runBeforePromptBuild).not.toHaveBeenCalled();
618630
expect(hookRunner.runBeforeAgentStart).not.toHaveBeenCalled();
619631
} finally {
@@ -650,6 +662,9 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
650662
model: "test-model",
651663
timeoutMs: 1_000,
652664
runId: "run-test-legacy-merge",
665+
messageChannel: "discord",
666+
currentChannelId: "channel:room-1",
667+
senderId: "user-789",
653668
config: createCliBackendConfig(),
654669
});
655670

@@ -659,6 +674,24 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
659674
);
660675
expect(hookRunner.runBeforePromptBuild).toHaveBeenCalledOnce();
661676
expect(hookRunner.runBeforeAgentStart).toHaveBeenCalledOnce();
677+
const beforePromptBuildCalls = hookRunner.runBeforePromptBuild.mock.calls as unknown as Array<
678+
[unknown, unknown]
679+
>;
680+
const promptContext = beforePromptBuildCalls[0]?.[1] as
681+
| { channel?: string; chatId?: string; senderId?: string }
682+
| undefined;
683+
expect(promptContext?.channel).toBe("discord");
684+
expect(promptContext?.chatId).toBe("room-1");
685+
expect(promptContext?.senderId).toBe("user-789");
686+
const beforeAgentStartCalls = hookRunner.runBeforeAgentStart.mock.calls as unknown as Array<
687+
[unknown, unknown]
688+
>;
689+
const legacyContext = beforeAgentStartCalls[0]?.[1] as
690+
| { channel?: string; chatId?: string; senderId?: string }
691+
| undefined;
692+
expect(legacyContext?.channel).toBe("discord");
693+
expect(legacyContext?.chatId).toBe("room-1");
694+
expect(legacyContext?.senderId).toBe("user-789");
662695
} finally {
663696
fs.rmSync(dir, { recursive: true, force: true });
664697
}

src/agents/cli-runner/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ export type RunCliAgentParams = {
9191
currentMessageId?: string | number;
9292
currentInboundAudio?: boolean;
9393
agentAccountId?: string;
94+
/** Sender identity for channel-originated runs when available. */
95+
senderId?: string | null;
9496
/** Trusted sender identity bit for channel action auth. */
9597
senderIsOwner?: boolean;
9698
/** Runtime tool allow-list. CLI harnesses fail closed when this is set. */

src/agents/command/attempt-execution.cli.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1126,7 +1126,10 @@ describe("CLI attempt execution", () => {
11261126
opts: {
11271127
messageProvider: "discord-voice",
11281128
} as Parameters<typeof runAgentAttempt>[0]["opts"],
1129-
runContext: {} as Parameters<typeof runAgentAttempt>[0]["runContext"],
1129+
runContext: {
1130+
currentChannelId: "channel:voice-room",
1131+
senderId: "sender-voice",
1132+
} as Parameters<typeof runAgentAttempt>[0]["runContext"],
11301133
spawnedBy: undefined,
11311134
messageChannel: "discord",
11321135
skillsSnapshot: undefined,
@@ -1144,6 +1147,8 @@ describe("CLI attempt execution", () => {
11441147
trigger: "user",
11451148
messageChannel: "discord",
11461149
messageProvider: "discord-voice",
1150+
currentChannelId: "channel:voice-room",
1151+
senderId: "sender-voice",
11471152
});
11481153
});
11491154

src/agents/command/attempt-execution.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ export function runAgentAttempt(params: {
638638
currentThreadTs: params.runContext.currentThreadTs,
639639
currentInboundAudio: params.runContext.currentInboundAudio,
640640
agentAccountId: params.runContext.accountId,
641+
senderId: params.runContext.senderId,
641642
senderIsOwner: params.opts.senderIsOwner,
642643
toolsAllow: params.opts.toolsAllow,
643644
cleanupBundleMcpOnRunEnd: params.opts.cleanupBundleMcpOnRunEnd,
@@ -707,6 +708,7 @@ export function runAgentAttempt(params: {
707708
currentInboundAudio: params.runContext.currentInboundAudio,
708709
replyToMode: params.runContext.replyToMode,
709710
hasRepliedRef: params.runContext.hasRepliedRef,
711+
senderId: params.runContext.senderId,
710712
senderIsOwner: params.opts.senderIsOwner,
711713
sessionFile: params.sessionFile,
712714
workspaceDir: params.workspaceDir,

src/agents/command/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ export type AgentRunContext = {
4141
currentChannelId?: string;
4242
currentThreadTs?: string;
4343
currentInboundAudio?: boolean;
44+
senderId?: string | null;
4445
replyToMode?: "off" | "first" | "all" | "batched";
4546
hasRepliedRef?: { value: boolean };
4647
};

src/agents/harness/hook-context.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ export type AgentHarnessHookContext = {
2525
modelProviderId?: string;
2626
modelId?: string;
2727
messageProvider?: string;
28+
channel?: string;
29+
chatId?: string;
30+
senderId?: string;
2831
trigger?: string;
2932
channelId?: string;
3033
contextTokenBudget?: number;
@@ -46,6 +49,9 @@ export function buildAgentHookContext(params: AgentHarnessHookContext): PluginHo
4649
...(params.modelProviderId ? { modelProviderId: params.modelProviderId } : {}),
4750
...(params.modelId ? { modelId: params.modelId } : {}),
4851
...(params.messageProvider ? { messageProvider: params.messageProvider } : {}),
52+
...(params.channel ? { channel: params.channel } : {}),
53+
...(params.chatId ? { chatId: params.chatId } : {}),
54+
...(params.senderId ? { senderId: params.senderId } : {}),
4955
...(params.trigger ? { trigger: params.trigger } : {}),
5056
...(params.channelId ? { channelId: params.channelId } : {}),
5157
...(params.contextTokenBudget ? { contextTokenBudget: params.contextTokenBudget } : {}),

src/auto-reply/reply/agent-runner-execution.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1838,6 +1838,7 @@ describe("runAgentTurnWithFallback", () => {
18381838
followupRun.run.model = "gpt-5.4";
18391839
followupRun.run.extraSystemPrompt = "dynamic inbound metadata\n\nstable group prompt";
18401840
followupRun.run.extraSystemPromptStatic = "stable group prompt";
1841+
followupRun.run.senderId = "sender-static";
18411842
followupRun.originatingChannel = "telegram";
18421843

18431844
const result = await runAgentTurnWithFallback({
@@ -1870,6 +1871,7 @@ describe("runAgentTurnWithFallback", () => {
18701871
trigger: "user",
18711872
messageChannel: "telegram",
18721873
messageProvider: "telegram",
1874+
senderId: "sender-static",
18731875
});
18741876
});
18751877

0 commit comments

Comments
 (0)