Files
PaperClipAI/server/src/services/chat-interaction-arbitration.ts
T
DottaandPaperclip 5bad074baa feat: add individual agent interaction permissions (#20411)
Add eleven sparse incoming and outgoing restrictions, human CLI/API administration, shared mutation enforcement, and pending-delivery checks. Preserve existing defaults, agent settings, and existing authorization boundaries.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
2026-11-09 20:34:04 -06:00

80 lines
3.0 KiB
TypeScript

import { and, eq, exists, inArray, or, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
chatPublications,
heartbeatRuns,
issueThreadInteractions,
} from "@paperclipai/db";
type ChatInteractionArbitrationDb = Pick<Db, "select">;
/**
* Returns whether a run has yielded its provider-visible response slot to a
* native question or confirmation. A pending interaction authored by this
* run's agent is authoritative even before its publication row is inserted.
* System-authored completion reviews cannot be projected to a provider and
* must not strand its working placeholder. Once resolved, the durable provider
* prompt proves that the source run's prose remains internal.
*/
export async function hasChatRunOwnedProviderInteraction(
db: ChatInteractionArbitrationDb,
input: { companyId: string; issueId: string; runId: string },
): Promise<boolean> {
const promptPublication = db
.select({ id: chatPublications.id })
.from(chatPublications)
.where(
and(
eq(chatPublications.companyId, issueThreadInteractions.companyId),
eq(chatPublications.issueId, issueThreadInteractions.issueId),
sql`${chatPublications.payload} ->> 'interactionId' = ${issueThreadInteractions.id}::text`,
sql`${chatPublications.idempotencyKey} = 'interaction:' || ${issueThreadInteractions.id}::text || ':' || ${chatPublications.endpointId}::text`,
inArray(chatPublications.state, [
"pending",
"streaming",
"published",
"retry",
"delivery_unknown",
]),
),
);
const rows = await db
.select({ id: issueThreadInteractions.id })
.from(issueThreadInteractions)
.where(
and(
eq(issueThreadInteractions.companyId, input.companyId),
eq(issueThreadInteractions.issueId, input.issueId),
eq(issueThreadInteractions.sourceRunId, input.runId),
or(
and(
inArray(issueThreadInteractions.kind, ["ask_user_questions", "request_confirmation"]),
eq(issueThreadInteractions.status, "pending"),
sql`NOT (${issueThreadInteractions.kind} = 'request_confirmation'
AND coalesce(${issueThreadInteractions.payload}->'target'->>'type', '') = 'custom'
AND coalesce(${issueThreadInteractions.payload}->'target'->>'key', '') = 'native_completion_review')`,
exists(
db
.select({ id: heartbeatRuns.id })
.from(heartbeatRuns)
.where(
and(
eq(heartbeatRuns.id, input.runId),
eq(heartbeatRuns.companyId, input.companyId),
eq(
heartbeatRuns.agentId,
issueThreadInteractions.createdByAgentId,
),
),
),
),
),
exists(promptPublication),
),
),
)
.limit(1);
return rows.length > 0;
}