Q118: read_messages reads restricted channels and direct threads only for their members
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cb1a4be167
commit
754f9ba44b
2 files changed
+29
-1
No files matched your search
@@ -15,6 +15,13 @@ interface MessageFilter {
|
|||||||
limit: number;
|
limit: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Workspace-wide channels are open to every agent; restricted channels and direct conversations only to their members. */
|
||||||
|
function mayRead(deps: AgentToolDeps, ctx: AgentToolContext, spaceId: string): boolean {
|
||||||
|
const space = deps.db.prepare("select visibility, space_type from chat_spaces where id = ?").get(spaceId) as { visibility: string; space_type: string };
|
||||||
|
if (space.visibility === "workspace" && space.space_type !== "direct") return true;
|
||||||
|
return Boolean(deps.db.prepare("select 1 from chat_memberships where space_id = ? and principal_type = 'agent' and principal_id = ?").get(spaceId, ctx.agent.id));
|
||||||
|
}
|
||||||
|
|
||||||
export function readMessagesTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
|
export function readMessagesTool(deps: AgentToolDeps, ctx: AgentToolContext, args: JsonRecord): string {
|
||||||
const channel = optionalString(args.channel);
|
const channel = optionalString(args.channel);
|
||||||
const withTeammate = optionalString(args.with);
|
const withTeammate = optionalString(args.with);
|
||||||
@@ -47,11 +54,13 @@ export function readMessagesTool(deps: AgentToolDeps, ctx: AgentToolContext, arg
|
|||||||
"select id from chat_spaces where org_id = ? and lower(name) = lower(?) and space_type <> 'direct' limit 1",
|
"select id from chat_spaces where org_id = ? and lower(name) = lower(?) and space_type <> 'direct' limit 1",
|
||||||
).get(ctx.orgId, channel) as { id: string } | undefined;
|
).get(ctx.orgId, channel) as { id: string } | undefined;
|
||||||
if (!space) throw new Error(`No channel named "${channel}".`);
|
if (!space) throw new Error(`No channel named "${channel}".`);
|
||||||
|
if (!mayRead(deps, ctx, space.id)) throw new Error(`You are not a member of #${channel}, so you cannot read it.`);
|
||||||
return messagePage(deps, ctx.orgId, { column: "space_id", id: space.id }, filter, `#${channel}`);
|
return messagePage(deps, ctx.orgId, { column: "space_id", id: space.id }, filter, `#${channel}`);
|
||||||
}
|
}
|
||||||
if (threadId) {
|
if (threadId) {
|
||||||
const thread = deps.db.prepare("select id from chat_threads where id = ? and org_id = ?").get(threadId, ctx.orgId);
|
const thread = deps.db.prepare("select space_id from chat_threads where id = ? and org_id = ?").get(threadId, ctx.orgId) as { space_id: string } | undefined;
|
||||||
if (!thread) throw new Error(`No thread with thread_id "${threadId}".`);
|
if (!thread) throw new Error(`No thread with thread_id "${threadId}".`);
|
||||||
|
if (!mayRead(deps, ctx, thread.space_id)) throw new Error(`You are not a member of the conversation thread ${threadId} belongs to, so you cannot read it.`);
|
||||||
return messagePage(deps, ctx.orgId, { column: "thread_id", id: threadId }, filter, `thread ${threadId}`);
|
return messagePage(deps, ctx.orgId, { column: "thread_id", id: threadId }, filter, `thread ${threadId}`);
|
||||||
}
|
}
|
||||||
return unansweredMentions(deps, ctx, filter.limit);
|
return unansweredMentions(deps, ctx, filter.limit);
|
||||||
|
|||||||
@@ -155,4 +155,23 @@ describe("agent direct messages", () => {
|
|||||||
expect(readBack).toContain("priorities for today");
|
expect(readBack).toContain("priorities for today");
|
||||||
expect(readBack).toMatch(/\] CEO \(agent, [^)]+\) \(thread=/);
|
expect(readBack).toMatch(/\] CEO \(agent, [^)]+\) \(thread=/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("read_messages reads only where the agent may: restricted channels and others' direct messages need membership", () => {
|
||||||
|
const h = harness({ enableProvider: false });
|
||||||
|
const deps = { db: h.connection.db, organization: h.organization, chat: h.chat };
|
||||||
|
const dana = { ...h.ctx, agent: { id: h.danaId, modelProviderId: null, modelId: null } };
|
||||||
|
h.connection.transaction(() => h.chat.createSpace({ orgId: h.orgId, spaceType: "project", name: "board-room", topic: "t", visibility: "restricted", memberAgentIds: [h.ceoId] }));
|
||||||
|
dispatchAgentTool(deps, h.ctx, call("send_message", { channel: "board-room", body: "Acquisition terms are 4x revenue." }));
|
||||||
|
expect(dispatchAgentTool(deps, h.ctx, call("read_messages", { channel: "board-room" }))).toContain("Acquisition terms");
|
||||||
|
expect(dispatchAgentTool(deps, dana, call("read_messages", { channel: "board-room" }))).toBe("ERROR: You are not a member of #board-room, so you cannot read it.");
|
||||||
|
|
||||||
|
const eveId = h.connection.transaction(() => h.organization.createAgent({
|
||||||
|
orgId: h.orgId, name: "Eve", role: "Engineer", department: "Engineering", modelProviderId: null, modelId: null, budgetCents: 1000, managerAgentId: h.ceoId,
|
||||||
|
}));
|
||||||
|
const sent = dispatchAgentTool(deps, h.ctx, call("send_message", { to: "Dana", body: "Your raise is approved." }));
|
||||||
|
const threadId = /thread_id="([^"]+)"/.exec(sent)![1];
|
||||||
|
expect(dispatchAgentTool(deps, dana, call("read_messages", { thread_id: threadId }))).toContain("Your raise is approved.");
|
||||||
|
const eve = { ...h.ctx, agent: { id: eveId, modelProviderId: null, modelId: null } };
|
||||||
|
expect(dispatchAgentTool(deps, eve, call("read_messages", { thread_id: threadId }))).toBe(`ERROR: You are not a member of the conversation thread ${threadId} belongs to, so you cannot read it.`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
Reference in new issue
Block a user