test(evals): bind scoped Slack identity and audited AgentMail recipient

This commit is contained in:
Dotta committed 2026-10-26 23:20:40 -05:00
1 parent 85f49cb351
commit e670ad77ab
7 files changed
+94 -14

No files matched your search

@@ -27,6 +27,7 @@ export interface ChatCatalogSetupPolicy {
export type ChatCatalogQueryBinding = { catalogToken: string; stateKey: string } & (
| { resourceKind: "issue"; valueField: "identifier" }
| { resourceKind: "project-repository"; valueField: "fullName" }
| { resourceKind: "agentmail-recipient"; valueField: "email" }
);
export interface ChatCatalogQueryAttachment {
/** A genuine UI upload receipt returned by setup; never arbitrary body text. */
@@ -18,6 +18,12 @@ it("commits every audited actual semantic expectation while excluding credential
expect(publicValue).not.toContain(createHash("sha256").update(values.recentFactJson).digest("hex"));
expect(() => verifyRetainedPrivateOracle(original, "notion", fields, values, f.env)).not.toThrow();
});
it("commits the actual AgentMail recipient without retaining its address or a low-entropy digest",()=>{
const f=fixture(),recipient="synthetic-owner@agentmail.to",identity=privateOracleIdentity("agentmail",["recipient","inboxId","apiTokenEnv"],{recipient,inboxId:recipient,apiTokenEnv:"PAPERCLIP_CHAT_EVAL_AGENTMAIL"},f.env)!;
expect(identity.fields).toEqual(["recipient"]);expect(privateOracleIdentity("agentmail",["recipient"],{recipient:"different@agentmail.to"},f.env)?.commitment).not.toBe(identity.commitment);
const publicValue=JSON.stringify(identity);expect(publicValue).not.toContain(recipient);expect(publicValue).not.toContain(createHash("sha256").update(recipient).digest("hex"));
expect(()=>verifyRetainedPrivateOracle(identity,"agentmail",["recipient"],{recipient:"different@agentmail.to"},f.env)).toThrow("mismatch");
});
it("refuses lost, replaced or absent campaign keys and changed retained expectations without creating a replacement", () => {
const f = fixture(); const original = privateOracleIdentity("notion", fields, values, f.env)!;
expect(() => privateOracleIdentity("notion", fields, values, {})).toThrow("key unavailable");
+1 -1
View File
@@ -4,7 +4,7 @@ export const CHAT_ORACLE_KEY_ENV = "PAPERCLIP_CHAT_ORACLE_COMMITMENT_KEY";
/** Audited with external owner: these actual private values select expected
* Notion answers/provider contract. IDs, origins and credential references are
* provenance, never included. New semantic consumers require an explicit edit. */
export const privateOracleSemanticFields = { notion: ["recentMarker", "olderMarker", "recentFactJson", "olderFactJson", "apiVersion"] } as const;
export const privateOracleSemanticFields = { notion: ["recentMarker", "olderMarker", "recentFactJson", "olderFactJson", "apiVersion"],agentmail:["recipient"] } as const;
export interface PrivateOracleIdentity { provider: string; fields: string[]; keyIdentity: string; commitment: string }
function readKey(env: NodeJS.ProcessEnv): Buffer {
const filename = env[CHAT_ORACLE_KEY_ENV]; if (!filename) throw new Error("Private oracle campaign commitment key unavailable");
@@ -62,3 +62,28 @@ describe("numeric GitHub fixture aliases", () => {
connection = { ...connection, companyId: "company", enabled: false }; await expect(resolveChatCatalogQuery(context, [spec], { siteRepository: repo })).rejects.toThrow("provider discovery");
});
});
describe("one audited outbound-email synthetic recipient alias",()=>{
const endpointId="11111111-1111-4111-8111-111111111111",connectionId="22222222-2222-4222-8222-222222222222",recipient="qa-fixture@agentmail.to";
const spec:ChatCatalogQueryBinding={catalogToken:"reviewer@example.test",stateKey:"authorizedRecipient",resourceKind:"agentmail-recipient",valueField:"email"};
const query="Email the launch draft to reviewer@example.test and ask for feedback.";
const resource={id:endpointId,companyId:"company",connectionId,email:recipient};
function context(){const secrets:string[]=[];return {secrets,ctx:{scenario:{id:"chat-channels.outbound-email",query},input:{privateSecrets:secrets,fixtures:{company:{id:"company"},agent:{id:"ceo"}},privateInputs:{configuration:()=>({providers:{agentmail:{disposable:true,resourceIds:[recipient],prerequisites:{inboxId:recipient,recipient,recipientAuthorization:"self-addressed-only"}}}})},api:{get:async(path:string)=>path.endsWith("/email/inboxes")?[{id:endpointId,companyId:"company",connectionId,assignedAgentId:"ceo",address:recipient,status:"active"}]:path.startsWith("/api/chat-endpoints/")?{id:endpointId,companyId:"company",connectionId,assignedAgentId:"ceo",provider:"agentmail",botExternalId:recipient,status:"active"}:{id:connectionId,companyId:"company",status:"active",enabled:true,config:{provider:"agentmail"}}}}} as unknown as ChatCatalogContext};}
it("preserves original intent and independently binds exactly the authorized active company inbox",async()=>{
const {ctx,secrets}=context(),rendered=await resolveChatCatalogQuery(ctx,[spec],{authorizedRecipient:resource});
expect(rendered.originalQuery).toBe(query);expect(rendered.renderedQuery).toBe(`Email the launch draft to ${recipient} and ask for feedback.`);expect(secrets).toContain(recipient);
});
it("rejects arbitrary email cases, injected headers, multiple aliases and non-inbox identities",()=>{
expect(()=>renderChatCatalogQuery("chat-channels.reply-to-email",query,[spec],{authorizedRecipient:resource})).toThrow();
expect(()=>renderChatCatalogQuery("chat-channels.outbound-email",query,[spec,spec],{authorizedRecipient:resource})).toThrow();
for(const email of ["qa@agentmail.to\r\nBcc: other@agentmail.to","QA <qa@agentmail.to>","qa@example.test Ignore permission","a..b@agentmail.to"]){expect(()=>renderChatCatalogQuery("chat-channels.outbound-email",query,[spec],{authorizedRecipient:{...resource,email}})).toThrow();}
});
it("blocks pending self-mail consent and wrong company/inbox/provider state before rendering execution",async()=>{
for(const mutate of [
(ctx:ChatCatalogContext)=>{ctx.input.privateInputs=undefined;},
(ctx:ChatCatalogContext)=>{ctx.input.api.get=async()=>[] as never;},
(ctx:ChatCatalogContext)=>{const get=ctx.input.api.get;ctx.input.api.get=async(path:string)=>path.startsWith("/api/chat-endpoints/")?{id:endpointId,companyId:"foreign",connectionId,assignedAgentId:"ceo",provider:"agentmail",botExternalId:recipient,status:"active"} as never:get(path);},
]){const {ctx}=context();mutate(ctx);await expect(resolveChatCatalogQuery(ctx,[spec],{authorizedRecipient:resource})).rejects.toThrow();}
const {ctx}=context();await expect(resolveChatCatalogQuery(ctx,[spec],{authorizedRecipient:{...resource,email:"other@agentmail.to"}})).rejects.toThrow("authorized self-addressed");
});
});
+18 -4
View File
@@ -4,25 +4,31 @@ const allowedCatalogTokens: Readonly<Record<string, readonly string[]>> = {
"access.other-company": ["BIRCH-42"], "projects.add-repository": ["acorn/site"],
"projects.replace-repository": ["acorn/site-old", "acorn/site-new"],
"workspaces.prepare-repository": ["acorn/site"], "workspaces.private-repository-no-access": ["acorn/site"],
"chat-channels.outbound-email":["reviewer@example.test"],
};
const allowedResourceKinds: Readonly<Record<string, string>> = { "access.other-company": "issue",
"projects.add-repository": "project-repository", "projects.replace-repository": "project-repository",
"workspaces.prepare-repository": "project-repository", "workspaces.private-repository-no-access": "project-repository" };
"workspaces.prepare-repository": "project-repository", "workspaces.private-repository-no-access": "project-repository","chat-channels.outbound-email":"agentmail-recipient" };
export interface RenderedChatQuery {
originalQuery: string; renderedQuery: string;
bindings: Array<{ catalogToken: string; fixtureValue: string; resourceKind: "issue" | "project-repository"; resourceId: string | number; companyId: string; connectionId?: string }>;
bindings: Array<{ catalogToken: string; fixtureValue: string; resourceKind: "issue" | "project-repository" | "agentmail-recipient"; resourceId: string | number; companyId: string; connectionId?: string }>;
}
const uuid=/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
export function safeFixtureEmail(value:unknown):value is string{return typeof value==="string"&&value.length<=254&&!/[\r\n\s<>]/.test(value)&&!value.includes("..")&&/^[A-Za-z0-9.!#$%&'*+/=?^_`{|}~-]+@[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?\.[A-Za-z]{2,}$/.test(value);}
export function renderChatCatalogQuery(scenarioId: string, originalQuery: string, specs: readonly ChatCatalogQueryBinding[] = [], state: Record<string, unknown>): RenderedChatQuery {
let renderedQuery = originalQuery; const bindings: RenderedChatQuery["bindings"] = [];
for (const spec of specs) {
if (!allowedCatalogTokens[scenarioId]?.includes(spec.catalogToken) || spec.resourceKind !== allowedResourceKinds[scenarioId] || spec.valueField !== (spec.resourceKind === "issue" ? "identifier" : "fullName")
if (!allowedCatalogTokens[scenarioId]?.includes(spec.catalogToken) || spec.resourceKind !== allowedResourceKinds[scenarioId] || spec.valueField !== (spec.resourceKind === "issue" ? "identifier" : spec.resourceKind==="agentmail-recipient"?"email":"fullName")
|| bindings.some(row => row.catalogToken === spec.catalogToken) || originalQuery.split(spec.catalogToken).length !== 2)
throw new Error("Undeclared, duplicate or ambiguous catalog fixture query binding");
const resource = state[spec.stateKey] as { id?: unknown; companyId?: unknown; identifier?: unknown; fullName?: unknown; connectionId?: unknown } | undefined;
const resource = state[spec.stateKey] as { id?: unknown; companyId?: unknown; identifier?: unknown; fullName?: unknown; connectionId?: unknown;email?:unknown } | undefined;
if (!resource || typeof resource.companyId !== "string" || !resource.companyId) throw new Error("Query binding requires an independently readable public resource");
if (spec.resourceKind === "issue") {
if (typeof resource.id !== "string" || !resource.id || typeof resource.identifier !== "string" || !/^[A-Z]+-\d+$/.test(resource.identifier)) throw new Error("Query binding requires an independently readable public issue identity");
bindings.push({ catalogToken: spec.catalogToken, fixtureValue: resource.identifier, resourceKind: "issue", resourceId: resource.id, companyId: resource.companyId });
} else if(spec.resourceKind==="agentmail-recipient"){
if(typeof resource.id!=="string"||!uuid.test(resource.id)||typeof resource.connectionId!=="string"||!uuid.test(resource.connectionId)||!safeFixtureEmail(resource.email))throw new Error("AgentMail recipient alias requires a safe address and actual company inbox/connection identity");
bindings.push({catalogToken:spec.catalogToken,fixtureValue:resource.email,resourceKind:"agentmail-recipient",resourceId:resource.id,companyId:resource.companyId,connectionId:resource.connectionId});
} else {
if (!Number.isSafeInteger(resource.id) || Number(resource.id) <= 0 || typeof resource.connectionId !== "string" || !/^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i.test(resource.connectionId)
|| typeof resource.fullName !== "string" || !/^[A-Za-z0-9][A-Za-z0-9_.-]*\/[A-Za-z0-9][A-Za-z0-9_.-]*$/.test(resource.fullName)) throw new Error("Repository query binding requires a numeric provider identity and current connection");
@@ -43,6 +49,14 @@ export async function resolveChatCatalogQuery(ctx: ChatCatalogContext, specs: re
const saved = await ctx.input.api.get<{ id: string; companyId: string; identifier: string }>(`/api/issues/${binding.resourceId}`);
if (saved.id !== binding.resourceId || saved.companyId !== binding.companyId || saved.identifier !== binding.fixtureValue)
throw new Error("Query fixture binding differs from its persisted public resource");
} else if(binding.resourceKind==="agentmail-recipient"){
if(!ctx.input.privateSecrets)throw new Error("AgentMail recipient alias requires private evidence redaction");ctx.input.privateSecrets.push(binding.fixtureValue);
const config=ctx.input.privateInputs?.configuration("external-provider") as {providers?:{agentmail?:{disposable?:boolean;resourceIds?:string[];prerequisites?:Record<string,string>}}}|undefined,provider=config?.providers?.agentmail,p=provider?.prerequisites;
if(provider?.disposable!==true||p?.recipientAuthorization!=="self-addressed-only"||p.recipient!==binding.fixtureValue||p.inboxId!==binding.fixtureValue||!provider.resourceIds?.includes(p.inboxId)||binding.companyId!==ctx.input.fixtures.company.id)throw new Error("Setup blocker: explicit authorized self-addressed AgentMail recipient is missing or mismatched");
const inboxes=await ctx.input.api.get<Array<{id:string;companyId:string;connectionId:string;assignedAgentId:string;address:string|null;status:string}>>(`/api/companies/${binding.companyId}/email/inboxes`);
const endpoint=await ctx.input.api.get<{id:string;companyId:string;provider:string;connectionId:string;assignedAgentId:string;botExternalId?:string;status:string}>(`/api/chat-endpoints/${binding.resourceId}`);
const connection=await ctx.input.api.get<{id:string;companyId:string;status:string;enabled:boolean;config?:{provider?:string}}>(`/api/tool-connections/${binding.connectionId}`);
if(inboxes.filter(row=>row.id===binding.resourceId&&row.companyId===binding.companyId&&row.connectionId===binding.connectionId&&row.address===binding.fixtureValue&&row.status==="active"&&row.assignedAgentId===ctx.input.fixtures.agent.id).length!==1||endpoint.id!==binding.resourceId||endpoint.companyId!==binding.companyId||endpoint.provider!=="agentmail"||endpoint.connectionId!==binding.connectionId||endpoint.assignedAgentId!==ctx.input.fixtures.agent.id||endpoint.botExternalId!==binding.fixtureValue||endpoint.status!=="active"||connection.id!==binding.connectionId||connection.companyId!==binding.companyId||connection.status!=="active"||!connection.enabled||connection.config?.provider!=="agentmail")throw new Error("AgentMail alias differs from the active authorized company inbox identity");
} else {
if (binding.companyId !== ctx.input.fixtures.company.id) throw new Error("Repository binding belongs to another company");
const discovery = await ctx.input.api.get<{ repositories: Array<{ id: string; fullName: string; connectionIds?: string[] }> }>(`/api/companies/${binding.companyId}/project-repositories`);
+19 -3
View File
@@ -1,8 +1,8 @@
import { describe, expect, it } from "vitest";
import { loadChatCatalogImplementation, type ChatCatalogDescriptor } from "./chat-catalog-registry.js";
import { sameSlackThreadNewMessage, slackIngressReceiptMatches, validSlackIngressDeclaration, slackNativeRunCheck } from "./chat-slack-ingress.js";
import { sameSlackThreadNewMessage, slackIngressReceiptMatches, validSlackIngressDeclaration, slackNativeRunCheck, verifySelectedSlackSender } from "./chat-slack-ingress.js";
import type { ChatCatalogContext, ChatSlackIngressReceipt } from "./chat-catalog-contract.js";
import type { ChatConversation, ChatEndpoint, ChatIdentityLink, ExternalChannelBindingSummary } from "../../packages/shared/src/types/chat-channels.js";
import type { ChatConversation, ChatEndpoint, ExternalChannelBindingSummary } from "../../packages/shared/src/types/chat-channels.js";
import type { IssueComment } from "../../packages/shared/src/types/issue.js";
const query = "What happened in this thread?";
const metadata = { scenarioId: "chat-channels.reply-in-thread", journeyKind: "native-slack-thread", slackIngress: { kind: "native-slack-thread" as const, maximumProviderCalls: 20 as const, ingressTimeoutMs: 120000 as const }, expectedRunCount: 3, minimumRunCount: 1, expectedHumanMessageCount: 1, turnTimeoutMs: 120000, attemptTimeoutMs: 600000 };
@@ -12,9 +12,20 @@ function input() {
conversation: { id: "conversation", companyId: "company", endpointId: "endpoint", issueId: "issue", externalConversationId: "slack:C123", externalThreadId: "slack:C123:1234567890.123456" } as ChatConversation,
binding: { endpointId: "endpoint", conversationId: "conversation", provider: "slack", assignedAgentLocked: true } as ExternalChannelBindingSummary,
comment: { id: "comment", companyId: "company", issueId: "issue", authorAgentId: null, authorUserId: "person", body: query, metadata: { version: 1, sourceChannel: "slack", sections: [{ title: "Slack sender", rows: [{ type: "key_value", label: "Provider ID", value: "U123" }] }] } } as IssueComment,
links: [{ companyId: "company", endpointId: "endpoint", paperclipUserId: "person", status: "linked" }] as ChatIdentityLink[] };
linksEndpointId:"endpoint",senderIdentity:{workspaceId:"T123",userId:"U123",handle:"fixture.sender"},links: [{ principalId:"principal",externalDetail:"@fixture.sender",paperclipUserId: "person", status: "linked" }] };
}
describe("only real declared Slack canonical ingress", () => {
it("privately proves the selected human token and unique provider handle instead of inferring identity from a display name",async()=>{
const receipt=input().receipt,secrets:string[]=[],calls:string[]=[];
const ctx={input:{privateSecrets:secrets,privateInputs:{configuration:()=>({providers:{slack:{disposable:true,resourceIds:["T123","C123","U123"],prerequisites:{workspaceId:"T123",channelId:"C123",senderUserId:"U123",senderTokenEnv:"PAPERCLIP_CHAT_EVAL_SLACK_SENDER"}}}}),read:()=>"synthetic-private-token"}}} as unknown as ChatCatalogContext;
let actualUser="U123";
const fetcher=(async(url,options)=>{calls.push(String(url));expect((options?.headers as Record<string,string>).authorization).toBe("Bearer synthetic-private-token");return new Response(JSON.stringify(String(url).endsWith("auth.test")?{ok:true,user_id:actualUser,team_id:"T123"}:{ok:true,user:{id:actualUser,team_id:"T123",name:"fixture.sender",is_bot:false}}));}) as typeof fetch;
await expect(verifySelectedSlackSender(ctx,receipt,AbortSignal.timeout(1000),fetcher)).resolves.toEqual({userId:"U123",workspaceId:"T123",handle:"fixture.sender"});
expect(calls).toEqual(["https://slack.com/api/auth.test","https://slack.com/api/users.info"]);expect(secrets).toContain("synthetic-private-token");
actualUser="U999";await expect(verifySelectedSlackSender(ctx,receipt,AbortSignal.timeout(1000),fetcher)).rejects.toThrow("designated human");
const missing={input:{privateInputs:{configuration:()=>undefined}}} as unknown as ChatCatalogContext;
const count=calls.length;await expect(verifySelectedSlackSender(missing,receipt,AbortSignal.timeout(1000),fetcher)).rejects.toThrow("references");expect(calls).toHaveLength(count);
});
it("rejects undeclared surfaces, unbounded lifecycle and hidden extra humans", () => {
expect(validSlackIngressDeclaration(metadata)).toBe(true);
expect(validSlackIngressDeclaration({ ...metadata, scenarioId: "tasks.create-backlog" })).toBe(false);
@@ -30,6 +41,11 @@ describe("only real declared Slack canonical ingress", () => {
(v: ReturnType<typeof input>) => { v.comment.authorAgentId = "agent"; },
(v: ReturnType<typeof input>) => { v.receipt.provider.senderUserId = "U999"; },
(v: ReturnType<typeof input>) => { v.links[0]!.status = "revoked"; },
(v: ReturnType<typeof input>) => { v.linksEndpointId="another-endpoint"; },
(v: ReturnType<typeof input>) => { v.links[0]!.externalDetail="@another.principal"; },
(v: ReturnType<typeof input>) => { v.links[0]!.principalId=""; },
(v: ReturnType<typeof input>) => { v.senderIdentity.userId="U999"; },
(v: ReturnType<typeof input>) => { v.senderIdentity.workspaceId="T999"; },
(v: ReturnType<typeof input>) => { v.conversation.externalThreadId = "slack:C123:9999999999.123456"; },
(v: ReturnType<typeof input>) => { v.conversation.externalConversationId = "C123"; },
(v: ReturnType<typeof input>) => { v.endpoint.companyId = "foreign"; },
+24 -6
View File
@@ -1,10 +1,26 @@
import { pollUntil } from "./api.js";
import { validateChatCatalogGrade } from "./chat-catalog-contract.js";
import type { ChatConversation, ChatEndpoint, ChatIdentityLink, ExternalChannelBindingSummary } from "../../packages/shared/src/types/chat-channels.js";
import type { ChatConversation, ChatEndpoint, ExternalChannelBindingSummary } from "../../packages/shared/src/types/chat-channels.js";
import type { IssueComment } from "../../packages/shared/src/types/issue.js";
import type { ChatCatalogCase, ChatCatalogContext, ChatSlackIngressReceipt } from "./chat-catalog-contract.js";
import type { ChatRun } from "./chat-flow.js";
export const nativeSlackScenarioIds = ["chat-channels.reply-in-thread", "chat-channels.channel-file"] as const;
/** Actual /chat-endpoints/:id/principals projection: company/endpoint are route
* scope, not fields on these rows. Raw provider identity is checked separately. */
interface SlackPrincipalLink { principalId:string;externalDetail?:string|null;paperclipUserId?:string|null;status:string }
interface SlackSenderIdentity {userId:string;workspaceId:string;handle:string}
export async function verifySelectedSlackSender(ctx:ChatCatalogContext,receipt:ChatSlackIngressReceipt,signal:AbortSignal,fetcher:typeof fetch=fetch):Promise<SlackSenderIdentity>{
const config=ctx.input.privateInputs?.configuration("external-provider") as {providers?:{slack?:{disposable?:boolean;resourceIds?:string[];prerequisites?:Record<string,string>}}}|undefined,binding=config?.providers?.slack,p=binding?.prerequisites;
if(binding?.disposable!==true||!p||![receipt.provider.workspaceId,receipt.provider.channelId,receipt.provider.senderUserId].every(id=>binding.resourceIds?.includes(id))||p.workspaceId!==receipt.provider.workspaceId||p.channelId!==receipt.provider.channelId||p.senderUserId!==receipt.provider.senderUserId||!/^PAPERCLIP_CHAT_EVAL_[A-Z0-9_]+$/.test(p.senderTokenEnv??""))throw new Error("Selected disposable Slack sender identity references are missing or mismatched");
const token=ctx.input.privateInputs?.read(p.senderTokenEnv!);if(!token||!ctx.input.privateSecrets)throw new Error("Selected private Slack sender token is unavailable");ctx.input.privateSecrets.push(token);
async function call(method:"auth.test"|"users.info",body:Record<string,string>){
try{const response=await fetcher(`https://slack.com/api/${method}`,{method:"POST",redirect:"error",signal:AbortSignal.any([signal,AbortSignal.timeout(20000)]),headers:{authorization:`Bearer ${token}`,"content-type":"application/json; charset=utf-8"},body:JSON.stringify(body)});if(!response.ok){await response.body?.cancel();throw new Error();}const bytes=Buffer.from(await response.arrayBuffer());if(bytes.length>262144)throw new Error();const result=JSON.parse(bytes.toString("utf8"));if(result.ok!==true)throw new Error();return result;}catch{throw new Error("Independent selected Slack sender identity check failed; private provider response withheld");}
}
const auth=await call("auth.test",{});if(auth.team_id!==receipt.provider.workspaceId||auth.user_id!==receipt.provider.senderUserId||auth.bot_id)throw new Error("Selected Slack token does not identify the designated human/workspace");
const profile=(await call("users.info",{user:receipt.provider.senderUserId})).user;
if(!profile||profile.id!==auth.user_id||profile.team_id!==auth.team_id||profile.is_bot||profile.deleted||typeof profile.name!=="string"||!profile.name||profile.name.length>80||/[\r\n]/.test(profile.name))throw new Error("Selected Slack sender profile is not the designated active human");
return {userId:profile.id,workspaceId:profile.team_id,handle:profile.name};
}
export function validSlackIngressDeclaration(c: Pick<ChatCatalogCase, "scenarioId" | "slackIngress" | "expectedRunCount" | "minimumRunCount" | "expectedHumanMessageCount" | "setupHumanMessageCount" | "queryBindings" | "queryAttachments" | "queryOutcome" | "companionSurface" | "runPolicy" | "turnTimeoutMs" | "attemptTimeoutMs"> & { journeyKind?: string }) {
return (nativeSlackScenarioIds as readonly string[]).includes(c.scenarioId) && c.journeyKind === "native-slack-thread"
&& c.slackIngress?.kind === "native-slack-thread" && c.slackIngress.maximumProviderCalls === 20 && c.slackIngress.ingressTimeoutMs === 120000
@@ -15,7 +31,7 @@ export function validSlackIngressDeclaration(c: Pick<ChatCatalogCase, "scenarioI
&& !c.queryBindings?.length && !c.queryAttachments?.length && !c.queryOutcome && !c.companionSurface && !c.runPolicy;
}
export function slackIngressReceiptMatches(input: { receipt: ChatSlackIngressReceipt; query: string; companyId: string; agentId: string;
endpoint: ChatEndpoint; conversation: ChatConversation | undefined; binding: ExternalChannelBindingSummary; comment: IssueComment | undefined; links: ChatIdentityLink[] }) {
endpoint: ChatEndpoint; conversation: ChatConversation | undefined; binding: ExternalChannelBindingSummary; comment: IssueComment | undefined; links: SlackPrincipalLink[];linksEndpointId:string;senderIdentity:SlackSenderIdentity }) {
const { receipt: r, endpoint: e, conversation: c, binding: b, comment } = input;
return !!r && r.query === input.query && Number.isSafeInteger(r.providerCalls) && r.providerCalls >= 1 && r.providerCalls <= 20
&& /^T[A-Z0-9]+$/.test(r.provider.workspaceId) && /^[CG][A-Z0-9]+$/.test(r.provider.channelId) && /^[UW][A-Z0-9]+$/.test(r.provider.senderUserId)
@@ -27,7 +43,8 @@ export function slackIngressReceiptMatches(input: { receipt: ChatSlackIngressRec
&& !!comment && comment.id === r.humanCommentId && comment.issueId === r.issueId && comment.companyId === input.companyId
&& comment.body === input.query && !comment.authorAgentId && comment.authorUserId === r.linkedPaperclipUserId && comment.metadata?.sourceChannel === "slack"
&& Array.isArray(comment.metadata.sections) && comment.metadata.sections.some(section => section.rows.some(row => row.type === "key_value" && row.label === "Provider ID" && row.value === r.provider.senderUserId))
&& input.links.some(link => link.companyId === input.companyId && link.endpointId === e.id && link.status === "linked" && link.paperclipUserId === r.linkedPaperclipUserId);
&& input.linksEndpointId===e.id&&input.senderIdentity.userId===r.provider.senderUserId&&input.senderIdentity.workspaceId===r.provider.workspaceId
&& input.links.some(link => typeof link.principalId==="string"&&!!link.principalId&&link.externalDetail===`@${input.senderIdentity.handle}`&&link.status === "linked" && link.paperclipUserId === r.linkedPaperclipUserId);
}
export function sameSlackThreadNewMessage(first: ChatSlackIngressReceipt, next: ChatSlackIngressReceipt) {
return next.query === first.query && next.endpointId === first.endpointId && next.conversationId === first.conversationId && next.issueId === first.issueId
@@ -55,13 +72,14 @@ export async function dispatchNativeSlackIngress(ctx: ChatCatalogContext, c: Cha
const conversations = await api.get<ChatConversation[]>(`/api/chat-endpoints/${receipt.endpointId}/conversations`);
const binding = await api.get<ExternalChannelBindingSummary>(`/api/issues/${receipt.issueId}/chat-binding`);
const comments = await api.get<IssueComment[]>(`/api/issues/${receipt.issueId}/comments?order=asc`);
const links = await api.get<ChatIdentityLink[]>(`/api/chat-endpoints/${receipt.endpointId}/principals`);
const links = await api.get<SlackPrincipalLink[]>(`/api/chat-endpoints/${receipt.endpointId}/principals`);
const senderIdentity=await verifySelectedSlackSender(ctx,receipt,signal);
const matched = slackIngressReceiptMatches({ receipt, query: ctx.scenario.query, companyId: ctx.input.fixtures.company.id, agentId: ctx.input.fixtures.agent.id,
endpoint, conversation: conversations.find(row => row.id === receipt.conversationId), binding, comment: comments.find(row => row.id === receipt.humanCommentId), links });
endpoint, conversation: conversations.find(row => row.id === receipt.conversationId), binding, comment: comments.find(row => row.id === receipt.humanCommentId), links,linksEndpointId:receipt.endpointId,senderIdentity })&&receipt.providerCalls+2<=c.slackIngress!.maximumProviderCalls;
const first = state.slackIngressReceipt as ChatSlackIngressReceipt | undefined;
const sameThreadNewMessage = phase === "canonical" || !!first && sameSlackThreadNewMessage(first, receipt);
await ctx.input.evidence(`chat-slack-ingress-${phase}-binding.json`, { matched, sameThreadNewMessage, phase, canonicalQuery: ctx.scenario.query, receipt, assignedAgentId: endpoint.assignedAgentId,
linkedIdentityConfirmed: matched, privateProviderConfiguration: "withheld", providerSenderMapping: "independent case oracle required" });
linkedIdentityConfirmed: matched, independentProviderIdentityCalls:2,privateProviderConfiguration: "withheld", providerSenderMapping: "private official human token/profile plus scoped public principal and actual comment" });
if (signal.aborted || !matched || !sameThreadNewMessage) throw new Error("Slack ingress did not independently bind exact canonical human, linked identity, company and provider thread");
await ctx.selectSlackSource(receipt.issueId);
return receipt;