Compare commits

...

3 Commits

Author SHA1 Message Date
Peter Steinberger
6c93143c4a fix: honor cron allowAgents override (#1771) (thanks @Noctivoro) 2026-01-25 13:10:14 +00:00
Peter Steinberger
11a6f6db2e fix: wire requester agent override for cron tools 2026-01-25 13:05:53 +00:00
Nick DiMoro
19e0b701cf fix: cron sessions inherit allowAgents from parent agent config
When a cron job runs in isolated mode, the sessions_spawn tool now correctly
inherits the allowAgents permissions from the parent agent's config.

The fix adds a requesterAgentIdOverride parameter that flows through the
tool creation chain:
- resolveEffectiveToolPolicy() extracts the correct agentId from the session key
- This agentId is passed to sessions_spawn and agents_list tools
- The tools use this override instead of re-parsing the session key

This fixes #1767
2026-01-25 13:05:32 +00:00
9 changed files with 114 additions and 8 deletions

View File

@ -48,6 +48,7 @@ Docs: https://docs.clawd.bot
- Google Chat: normalize space targets without double `spaces/` prefix. - Google Chat: normalize space targets without double `spaces/` prefix.
- Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz. - Agents: auto-compact on context overflow prompt errors before failing. (#1627) Thanks @rodrigouroz.
- Agents: use the active auth profile for auto-compaction recovery. - Agents: use the active auth profile for auto-compaction recovery.
- Agents: let cron isolated runs inherit subagent allowlists from the parent agent. (#1771) Thanks @Noctivoro.
- Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204. - Media understanding: skip image understanding when the primary model already supports vision. (#1747) Thanks @tyler6204.
- Models: default missing custom provider fields so minimal configs are accepted. - Models: default missing custom provider fields so minimal configs are accepted.
- Messaging: keep newline chunking safe for fenced markdown blocks across channels. - Messaging: keep newline chunking safe for fenced markdown blocks across channels.

View File

@ -157,4 +157,44 @@ describe("agents_list", () => {
const research = agents?.find((agent) => agent.id === "research"); const research = agents?.find((agent) => agent.id === "research");
expect(research?.configured).toBe(false); expect(research?.configured).toBe(false);
}); });
it("uses requesterAgentIdOverride when resolving allowlists", async () => {
configOverride = {
session: {
mainKey: "main",
scope: "per-sender",
},
agents: {
list: [
{
id: "cron-owner",
subagents: {
allowAgents: ["research"],
},
},
{
id: "research",
name: "Research",
},
],
},
};
const tool = createClawdbotTools({
agentSessionKey: "cron:job-1",
requesterAgentIdOverride: "cron-owner",
}).find((candidate) => candidate.name === "agents_list");
if (!tool) throw new Error("missing agents_list tool");
const result = await tool.execute("call5", {});
const agents = (
result.details as {
agents?: Array<{ id: string }>;
}
).agents;
expect(agents?.map((agent) => agent.id)).toEqual(["cron-owner", "research"]);
expect(result.details).toMatchObject({
requester: "cron-owner",
});
});
}); });

View File

@ -87,6 +87,52 @@ describe("clawdbot-tools: subagents", () => {
}); });
expect(childSessionKey?.startsWith("agent:research:subagent:")).toBe(true); expect(childSessionKey?.startsWith("agent:research:subagent:")).toBe(true);
}); });
it("sessions_spawn honors requesterAgentIdOverride for cron sessions", async () => {
resetSubagentRegistryForTests();
callGatewayMock.mockReset();
configOverride = {
session: {
mainKey: "main",
scope: "per-sender",
},
agents: {
list: [
{
id: "cron-owner",
subagents: {
allowAgents: ["research"],
},
},
],
},
};
callGatewayMock.mockImplementation(async (opts: unknown) => {
const request = opts as { method?: string; params?: unknown };
if (request.method === "agent") {
return { runId: "run-2", status: "accepted", acceptedAt: 5200 };
}
return {};
});
const tool = createClawdbotTools({
agentSessionKey: "cron:job-1",
requesterAgentIdOverride: "cron-owner",
agentChannel: "whatsapp",
}).find((candidate) => candidate.name === "sessions_spawn");
if (!tool) throw new Error("missing sessions_spawn tool");
const result = await tool.execute("call11", {
task: "do thing",
agentId: "research",
});
expect(result.details).toMatchObject({
status: "accepted",
runId: "run-2",
});
});
it("sessions_spawn forbids cross-agent spawning when not allowed", async () => { it("sessions_spawn forbids cross-agent spawning when not allowed", async () => {
resetSubagentRegistryForTests(); resetSubagentRegistryForTests();
callGatewayMock.mockReset(); callGatewayMock.mockReset();

View File

@ -54,6 +54,8 @@ export function createClawdbotTools(options?: {
hasRepliedRef?: { value: boolean }; hasRepliedRef?: { value: boolean };
/** If true, the model has native vision capability */ /** If true, the model has native vision capability */
modelHasVision?: boolean; modelHasVision?: boolean;
/** Explicit agent ID override for cron/hook sessions. */
requesterAgentIdOverride?: string;
}): AnyAgentTool[] { }): AnyAgentTool[] {
const imageTool = options?.agentDir?.trim() const imageTool = options?.agentDir?.trim()
? createImageTool({ ? createImageTool({
@ -105,7 +107,10 @@ export function createClawdbotTools(options?: {
agentSessionKey: options?.agentSessionKey, agentSessionKey: options?.agentSessionKey,
config: options?.config, config: options?.config,
}), }),
createAgentsListTool({ agentSessionKey: options?.agentSessionKey }), createAgentsListTool({
agentSessionKey: options?.agentSessionKey,
requesterAgentIdOverride: options?.requesterAgentIdOverride,
}),
createSessionsListTool({ createSessionsListTool({
agentSessionKey: options?.agentSessionKey, agentSessionKey: options?.agentSessionKey,
sandboxed: options?.sandboxed, sandboxed: options?.sandboxed,
@ -129,6 +134,7 @@ export function createClawdbotTools(options?: {
agentGroupChannel: options?.agentGroupChannel, agentGroupChannel: options?.agentGroupChannel,
agentGroupSpace: options?.agentGroupSpace, agentGroupSpace: options?.agentGroupSpace,
sandboxed: options?.sandboxed, sandboxed: options?.sandboxed,
requesterAgentIdOverride: options?.requesterAgentIdOverride,
}), }),
createSessionStatusTool({ createSessionStatusTool({
agentSessionKey: options?.agentSessionKey, agentSessionKey: options?.agentSessionKey,
@ -144,10 +150,12 @@ export function createClawdbotTools(options?: {
config: options?.config, config: options?.config,
workspaceDir: options?.workspaceDir, workspaceDir: options?.workspaceDir,
agentDir: options?.agentDir, agentDir: options?.agentDir,
agentId: resolveSessionAgentId({ agentId:
sessionKey: options?.agentSessionKey, options?.requesterAgentIdOverride ??
config: options?.config, resolveSessionAgentId({
}), sessionKey: options?.agentSessionKey,
config: options?.config,
}),
sessionKey: options?.agentSessionKey, sessionKey: options?.agentSessionKey,
messageChannel: options?.agentChannel, messageChannel: options?.agentChannel,
agentAccountId: options?.agentAccountId, agentAccountId: options?.agentAccountId,

View File

@ -293,6 +293,7 @@ export function createClawdbotCodingTools(options?: {
agentGroupChannel: options?.groupChannel ?? null, agentGroupChannel: options?.groupChannel ?? null,
agentGroupSpace: options?.groupSpace ?? null, agentGroupSpace: options?.groupSpace ?? null,
agentDir: options?.agentDir, agentDir: options?.agentDir,
requesterAgentIdOverride: agentId,
sandboxRoot, sandboxRoot,
workspaceDir: options?.workspaceDir, workspaceDir: options?.workspaceDir,
sandboxed: !!sandbox, sandboxed: !!sandbox,

View File

@ -19,7 +19,11 @@ type AgentListEntry = {
configured: boolean; configured: boolean;
}; };
export function createAgentsListTool(opts?: { agentSessionKey?: string }): AnyAgentTool { export function createAgentsListTool(opts?: {
agentSessionKey?: string;
/** Explicit agent ID override for cron/hook sessions. */
requesterAgentIdOverride?: string;
}): AnyAgentTool {
return { return {
label: "Agents", label: "Agents",
name: "agents_list", name: "agents_list",
@ -37,7 +41,9 @@ export function createAgentsListTool(opts?: { agentSessionKey?: string }): AnyAg
}) })
: alias; : alias;
const requesterAgentId = normalizeAgentId( const requesterAgentId = normalizeAgentId(
parseAgentSessionKey(requesterInternalKey)?.agentId ?? DEFAULT_AGENT_ID, opts?.requesterAgentIdOverride ??
parseAgentSessionKey(requesterInternalKey)?.agentId ??
DEFAULT_AGENT_ID,
); );
const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? []; const allowAgents = resolveAgentConfig(cfg, requesterAgentId)?.subagents?.allowAgents ?? [];

View File

@ -67,6 +67,8 @@ export function createSessionsSpawnTool(opts?: {
agentGroupChannel?: string | null; agentGroupChannel?: string | null;
agentGroupSpace?: string | null; agentGroupSpace?: string | null;
sandboxed?: boolean; sandboxed?: boolean;
/** Explicit agent ID override for cron/hook sessions where session key parsing may not work. */
requesterAgentIdOverride?: string;
}): AnyAgentTool { }): AnyAgentTool {
return { return {
label: "Sessions", label: "Sessions",
@ -129,7 +131,7 @@ export function createSessionsSpawnTool(opts?: {
}); });
const requesterAgentId = normalizeAgentId( const requesterAgentId = normalizeAgentId(
parseAgentSessionKey(requesterInternalKey)?.agentId, opts?.requesterAgentIdOverride ?? parseAgentSessionKey(requesterInternalKey)?.agentId,
); );
const targetAgentId = requestedAgentId const targetAgentId = requestedAgentId
? normalizeAgentId(requestedAgentId) ? normalizeAgentId(requestedAgentId)

View File

@ -170,6 +170,7 @@ export async function handleInlineActions(params: {
agentAccountId: (ctx as { AccountId?: string }).AccountId, agentAccountId: (ctx as { AccountId?: string }).AccountId,
agentTo: ctx.OriginatingTo ?? ctx.To, agentTo: ctx.OriginatingTo ?? ctx.To,
agentThreadId: ctx.MessageThreadId ?? undefined, agentThreadId: ctx.MessageThreadId ?? undefined,
requesterAgentIdOverride: agentId,
agentDir, agentDir,
workspaceDir, workspaceDir,
config: cfg, config: cfg,

View File

@ -148,6 +148,7 @@ export async function handleToolsInvokeHttpRequest(
agentSessionKey: sessionKey, agentSessionKey: sessionKey,
agentChannel: messageChannel ?? undefined, agentChannel: messageChannel ?? undefined,
agentAccountId: accountId, agentAccountId: accountId,
requesterAgentIdOverride: agentId,
config: cfg, config: cfg,
pluginToolAllowlist: collectExplicitAllowlist([ pluginToolAllowlist: collectExplicitAllowlist([
profilePolicy, profilePolicy,