Merge master b189f586d into Codex runner default candidate

Preserve master transport, lifecycle, and release verification controls alongside macOS qualification.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 07:55:09 -05:00
commit f4abdc2cfc
749 files changed
+124181 -68418

No files matched your search

@@ -28,6 +28,9 @@ import {
} from "../../attachment-types.js";
import { getStorageService } from "../../storage/index.js";
import type { StorageService } from "../../storage/types.js";
import { loadConfig } from "../../config.js";
import { HttpError } from "../../errors.js";
import { voiceSessionStore, type VoiceTransaction } from "../voice/voice-session-store.js";
import { issueService } from "../issues.js";
import { boundExternalChatProvider } from "./external-chat-provider.js";
import { resolveExternalChatQuestionResponse } from "./external-chat-question-response.js";
@@ -340,7 +343,7 @@ async function principalAuthorized(
}
}
const principalQuery = tx
.select({ id: chatExternalPrincipals.id })
.select({ id: chatExternalPrincipals.id, externalId: chatExternalPrincipals.externalId })
.from(chatExternalPrincipals)
.where(
and(
@@ -359,6 +362,16 @@ async function principalAuthorized(
? principalQuery.for("update", { noWait: true }).limit(1)
: principalQuery.for("update").limit(1));
if (!principal) return false;
if (endpoint.provider === "speko") {
try {
await voiceSessionStore(tx, { allowLocalBoard: loadConfig().deploymentMode === "local_trusted", nonblockingAuthority: lockMode === "nonblocking" })
.authorizePrincipal(tx as unknown as VoiceTransaction, endpoint.companyId, endpoint.id, principal.externalId);
return true;
} catch (error) {
if (error instanceof HttpError && [403, 404].includes(error.status)) return false;
throw error;
}
}
const linkQuery = tx
.select({
status: chatIdentityLinks.status,
@@ -34,6 +34,7 @@ const EXTERNAL_CHAT_PROVIDERS = new Set([
"microsoft-teams",
"telegram",
"imessage-photon",
"speko",
]);
const ATTACHMENT_OMISSION_REASONS = new Set([
"attachment_limit",
@@ -7,6 +7,7 @@ const BOUND_EXTERNAL_CHAT_PROVIDERS = [
"microsoft-teams",
"telegram",
"imessage-photon",
"speko",
] as const;
export function boundExternalChatProvider(source: unknown) {
@@ -1,5 +1,5 @@
import { photonAnswersMatch } from "../photon/interactions.js";
import { and, or, eq, inArray, notExists, sql } from "drizzle-orm";
import { and, or, eq, lte, inArray, notExists, sql } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
import {
agentWakeupRequests,
@@ -7,7 +7,12 @@ import {
chatConversations,
chatDeliveries,
chatEndpoints,
chatExternalPrincipals,
chatIdentityLinks,
chatVoiceSessions,
chatVoiceToolCalls,
chatVoiceReplies,
toolConnections,
chatMessageLinks,
chatPublications,
heartbeatRuns,
@@ -21,6 +26,7 @@ import {
validateChatQuestionFormSubmission,
} from "../chat-question-forms.js";
import { questionResponseDeliveryValues } from "../question-response-delivery.js";
import { voiceSessionStore, type VoiceTransaction, currentVoiceCredentialFingerprint } from "../voice/voice-session-store.js";
import { nativeSha256 } from "./canonical.js";
export const EXTERNAL_CHAT_QUESTION_RESPONSE_KEY =
@@ -339,7 +345,7 @@ async function resolveQuestionResponseChain(
const provider =
parent?.provider ??
(
["slack", "github", "discord", "microsoft-teams", "telegram", "imessage-photon"] as const
["slack", "github", "discord", "microsoft-teams", "telegram", "imessage-photon", "speko"] as const
).find(
(candidate) =>
sourceContext.source === `chat:${candidate}` ||
@@ -358,7 +364,7 @@ async function resolveQuestionResponseChain(
if (
!provider ||
source.agentId !== binding.agentId ||
!sourceTaskMatches ||
(!sourceTaskMatches && !(provider === "speko" && source.runtimeMode === "legacy" && source.nativeIssueId === null)) ||
sourceContext.issueId !== binding.issueId ||
(source.status !== "succeeded" &&
!(
@@ -418,6 +424,121 @@ async function resolveQuestionResponseChain(
)
return null;
if (provider === "speko") {
// A model-selected interaction ID is insufficient. Require the exact
// canonical answer, a presented publication, and the durable signed-tool
// receipt committed atomically with that answer. No synthetic identity link.
const proofs = await tx.select({ receipt: chatVoiceToolCalls, session: chatVoiceSessions,
reply: chatVoiceReplies, publication: chatPublications, endpoint: chatEndpoints, connection: toolConnections,
conversation: chatConversations }).from(chatVoiceToolCalls)
.innerJoin(chatVoiceSessions, and(eq(chatVoiceSessions.id, chatVoiceToolCalls.sessionId), eq(chatVoiceSessions.companyId, binding.companyId)))
.innerJoin(chatVoiceReplies, and(eq(chatVoiceReplies.sessionId, chatVoiceSessions.id), eq(chatVoiceReplies.companyId, binding.companyId)))
.innerJoin(chatPublications, and(eq(chatPublications.id, chatVoiceReplies.publicationId), eq(chatPublications.companyId, binding.companyId)))
.innerJoin(chatEndpoints, and(eq(chatEndpoints.id, chatVoiceSessions.endpointId), eq(chatEndpoints.companyId, binding.companyId)))
.innerJoin(toolConnections, and(eq(toolConnections.id, chatEndpoints.connectionId), eq(toolConnections.companyId, binding.companyId)))
.innerJoin(chatConversations, and(eq(chatConversations.id, chatVoiceSessions.conversationId), eq(chatConversations.companyId, binding.companyId)))
.where(and(eq(chatVoiceToolCalls.companyId, binding.companyId), eq(chatVoiceToolCalls.tool, "answer_question"),
sql`${chatVoiceToolCalls.response}->>'interactionId' = ${interaction.id}`,
sql`${chatPublications.payload}->>'interactionId' = ${interaction.id}`,
eq(chatVoiceSessions.issueId, binding.issueId),
// Terminal acknowledgment publications use the same interaction ID.
// Only a question actually delivered before its answer can be proof.
lte(chatVoiceReplies.deliveredAt, interaction.resolvedAt)));
if (proofs.length === 0 && interaction.resolvedByRunId === null) {
// The authenticated task-page answer commits the canonical interaction
// and response delivery too. Bind it to the original voice requester;
// do not require hearing the question or fabricate a signed-tool receipt.
const origins = await tx.select({ session: chatVoiceSessions, endpoint: chatEndpoints,
connection: toolConnections, conversation: chatConversations, comment: issueComments,
inbound: chatDeliveries, link: chatMessageLinks }).from(chatMessageLinks)
.innerJoin(issueComments, and(eq(issueComments.id, chatMessageLinks.commentId), eq(issueComments.companyId, binding.companyId), eq(issueComments.issueId, binding.issueId)))
.innerJoin(chatDeliveries, and(eq(chatDeliveries.id, chatMessageLinks.deliveryId), eq(chatDeliveries.companyId, binding.companyId), eq(chatDeliveries.endpointId, chatMessageLinks.endpointId), eq(chatDeliveries.conversationId, chatMessageLinks.conversationId)))
.innerJoin(chatExternalPrincipals, and(eq(chatExternalPrincipals.id, chatDeliveries.principalId), eq(chatExternalPrincipals.companyId, binding.companyId), eq(chatExternalPrincipals.provider, "speko")))
.innerJoin(chatVoiceSessions, and(sql`${chatExternalPrincipals.externalId} = 'voice:' || ${chatVoiceSessions.id}::text`, eq(chatVoiceSessions.companyId, binding.companyId), eq(chatVoiceSessions.endpointId, chatMessageLinks.endpointId), eq(chatVoiceSessions.conversationId, chatMessageLinks.conversationId), eq(chatVoiceSessions.issueId, binding.issueId)))
.innerJoin(chatEndpoints, and(eq(chatEndpoints.id, chatVoiceSessions.endpointId), eq(chatEndpoints.companyId, binding.companyId)))
.innerJoin(toolConnections, and(eq(toolConnections.id, chatEndpoints.connectionId), eq(toolConnections.companyId, binding.companyId)))
.innerJoin(chatConversations, and(eq(chatConversations.id, chatVoiceSessions.conversationId), eq(chatConversations.companyId, binding.companyId), eq(chatConversations.endpointId, chatEndpoints.id)))
.where(and(eq(chatMessageLinks.companyId, binding.companyId), eq(chatMessageLinks.commentId, context.sourceCommentId), eq(chatMessageLinks.direction, "inbound")));
if (origins.length !== 1) return null;
const { session, endpoint, connection, conversation, comment, inbound, link } = origins[0]!;
if (comment.deletedAt || inbound.state !== "processed" || comment.authorUserId !== interaction.resolvedByUserId
|| session.callerId !== interaction.resolvedByUserId || !["member", "instance_admin", "local_board"].includes(session.callerAuthority)
|| session.assignedAgentId !== binding.agentId || endpoint.assignedAgentId !== binding.agentId
|| endpoint.provider !== "speko" || !["active", "verifying"].includes(endpoint.status)
|| !connection.enabled || connection.status !== "active" || connection.transport !== "voice"
|| session.generation !== record(endpoint.setup).runtimeGeneration
|| session.credentialFingerprint !== await currentVoiceCredentialFingerprint(tx, binding.companyId, connection.credentialSecretRefs)
|| !["active", "waiting"].includes(conversation.state) || conversation.issueId !== binding.issueId
|| (parent && (parent.marker.endpointId !== endpoint.id || parent.marker.conversationId !== conversation.id))) return null;
try {
const store = voiceSessionStore(tx, { allowLocalBoard: session.callerAuthority === "local_board" });
await store.authorizePrincipal(tx as unknown as VoiceTransaction, binding.companyId, endpoint.id, `voice:${session.id}`);
} catch (error) {
if (error && typeof error === "object" && "code" in error && error.code === "55P03") throw error;
return null;
}
const marker: Marker = {
schema: "paperclip.external_chat_question_response.v1", interactionId: interaction.id,
responseDeliveryId: delivery.id, sourceRunId: source.id, sourceCommentId: context.sourceCommentId,
endpointId: endpoint.id, conversationId: conversation.id,
bindingSha256: nativeSha256({ binding: {companyId: binding.companyId, issueId: binding.issueId, runId: binding.runId, agentId: binding.agentId}, responseDeliveryId: delivery.id, payloadSha256: delivery.payloadSha256,
sourceRunId: source.id, sourceCommentId: context.sourceCommentId, sourceOrigin: sourceContext.source,
...(parent ? { sourceQuestionResponseSha256: parent.marker.bindingSha256 } : {}),
interaction: { id: interaction.id, payload: interaction.payload, result: interaction.result,
resolvedAt: interaction.resolvedAt.toISOString(), resolvedByUserId: interaction.resolvedByUserId },
boardResponse: { sessionId: session.id, callerId: session.callerId, generation: session.generation,
credentialFingerprint: session.credentialFingerprint, inboundId: inbound.id, linkId: link.id },
conversationGeneration: conversation.sessionGeneration, wakeId: wake.id }),
};
if (!mint && nativeSha256(record(context[EXTERNAL_CHAT_QUESTION_RESPONSE_KEY])) !== nativeSha256(marker)) return null;
return { marker, provider, answeredAtMs: interaction.resolvedAt.getTime(),
interactionIds: [...(parent?.interactionIds ?? []), interaction.id],
authorizationContext: { ...context, source: "chat:speko", paperclipHarnessCheckedOut: false, paperclipExternalChatExecutionBound: true } };
}
if (proofs.length !== 1) return null;
const proof = proofs[0]!;
const { receipt, session, reply, publication, endpoint, connection, conversation } = proof;
const [original] = await tx.select({ comment: issueComments, inbound: chatDeliveries, link: chatMessageLinks })
.from(chatMessageLinks).innerJoin(issueComments, eq(issueComments.id, chatMessageLinks.commentId))
.innerJoin(chatDeliveries, eq(chatDeliveries.id, chatMessageLinks.deliveryId))
.where(and(eq(chatMessageLinks.companyId, binding.companyId), eq(chatMessageLinks.endpointId, endpoint.id),
eq(chatMessageLinks.conversationId, conversation.id), eq(chatMessageLinks.direction, "inbound"),
eq(chatMessageLinks.commentId, context.sourceCommentId), eq(issueComments.companyId, binding.companyId),
eq(issueComments.issueId, binding.issueId), eq(chatDeliveries.companyId, binding.companyId)));
if (!original || original.comment.deletedAt || original.inbound.state !== "processed"
|| original.comment.authorUserId !== interaction.resolvedByUserId
|| session.callerId !== interaction.resolvedByUserId || !["member", "instance_admin", "local_board"].includes(session.callerAuthority)
|| session.assignedAgentId !== binding.agentId || endpoint.assignedAgentId !== binding.agentId
|| endpoint.provider !== "speko" || !["active", "verifying"].includes(endpoint.status)
|| !connection.enabled || connection.status !== "active" || connection.transport !== "voice"
|| session.generation !== record(endpoint.setup).runtimeGeneration
|| session.credentialFingerprint !== await currentVoiceCredentialFingerprint(tx, binding.companyId, connection.credentialSecretRefs)
|| !["active", "waiting"].includes(conversation.state) || conversation.issueId !== binding.issueId
|| publication.conversationId !== conversation.id || publication.endpointId !== endpoint.id || publication.issueId !== binding.issueId
|| publication.state !== "published" || !publication.providerMessageId || !reply.deliveredAt
|| reply.deliveredAt > interaction.resolvedAt || receipt.response.status !== "answered"
|| receipt.response.resultSha256 !== nativeSha256(interaction.result)
|| (parent && (parent.marker.endpointId !== endpoint.id || parent.marker.conversationId !== conversation.id))) return null;
const marker: Marker = {
schema: "paperclip.external_chat_question_response.v1", interactionId: interaction.id,
responseDeliveryId: delivery.id, sourceRunId: source.id, sourceCommentId: context.sourceCommentId,
endpointId: endpoint.id, conversationId: conversation.id,
bindingSha256: nativeSha256({ binding: { companyId: binding.companyId, issueId: binding.issueId, runId: binding.runId, agentId: binding.agentId }, responseDeliveryId: delivery.id, payloadSha256: delivery.payloadSha256,
sourceRunId: source.id, sourceCommentId: context.sourceCommentId, sourceOrigin: sourceContext.source,
...(parent ? { sourceQuestionResponseSha256: parent.marker.bindingSha256 } : {}),
interaction: { id: interaction.id, payload: interaction.payload, result: interaction.result,
resolvedAt: interaction.resolvedAt.toISOString(), resolvedByUserId: interaction.resolvedByUserId },
voiceResponse: { receiptId: receipt.id, fingerprint: receipt.fingerprint, response: receipt.response,
sessionId: session.id, callerId: session.callerId, generation: session.generation, credentialFingerprint: session.credentialFingerprint },
publicationId: publication.id, replyId: reply.id, inboundId: original.inbound.id, linkId: original.link.id,
conversationGeneration: conversation.sessionGeneration, wakeId: wake.id }),
};
if (!mint && nativeSha256(record(context[EXTERNAL_CHAT_QUESTION_RESPONSE_KEY])) !== nativeSha256(marker)) return null;
return { marker, provider, answeredAtMs: interaction.resolvedAt.getTime(),
interactionIds: [...(parent?.interactionIds ?? []), interaction.id],
authorizationContext: { ...context, source: "chat:speko", paperclipHarnessCheckedOut: false, paperclipExternalChatExecutionBound: true } };
}
const actionQuery = tx
.select({
action: chatActions,
@@ -15,6 +15,7 @@ import {
chatEndpointResources,
chatEndpoints,
chatExternalPrincipals,
chatGitHubConfigurations,
chatIdentityLinks,
chatMessageLinks,
chatPublications,
@@ -35,6 +36,7 @@ import {
statusDecisions,
toolApplications,
toolConnections,
chatVoiceSessions, chatVoiceReplies, chatVoiceToolCalls,
workspaceOperations,
} from "@paperclipai/db";
import type {
@@ -61,11 +63,13 @@ import {
resolveExternalChatResponseWaitAuthorization,
} from "./chat-attachment-reuse.js";
import { attestReviewedExternalChatRun, buildPaperclipWakePayload } from "../heartbeat.js";
import { voiceCredentialFingerprint } from "../voice/voice-session-store.js";
import { nativeSha256 } from "./canonical.js";
import { questionResponseDeliveryValues } from "../question-response-delivery.js";
import { resolveExternalChatQuestionResponse } from "./external-chat-question-response.js";
import { materializeExternalChatQuestionResponseInput } from "./external-chat-question-response-input.js";
import * as nativeInteractionBridge from "./native-interaction-bridge.js";
import type { AskUserQuestionsInteraction } from "@paperclipai/shared";
import { defaultGitHubReviewPolicy, type AskUserQuestionsInteraction } from "@paperclipai/shared";
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
import { createLocalDiskStorageProvider } from "../../storage/local-disk-provider.js";
import { createStorageService } from "../../storage/service.js";
@@ -1114,6 +1118,52 @@ describe("native external-chat response wait", () => {
},
);
it.each((["native", "legacy"] as const).flatMap(sourceRuntime => (["voice", "board"] as const).map(answerRoute => ({sourceRuntime, answerRoute}))))("authorizes $sourceRuntime Speko question continuation from its exact $answerRoute answer", async ({sourceRuntime, answerRoute}) => {
const f = await seedAnsweredChatTurn();
const [endpoint] = await db.select().from(chatEndpoints).where(eq(chatEndpoints.id, f.endpointId));
const [connection] = await db.update(toolConnections).set({ transport: "voice", status: "active", enabled: true }).where(eq(toolConnections.id, endpoint.connectionId)).returning();
await db.update(chatEndpoints).set({ provider: "speko", setup: { step: "complete", runtimeGeneration: 1 } as typeof endpoint.setup }).where(eq(chatEndpoints.id, f.endpointId));
const [source] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, f.sourceRunId));
const sourceContext = JSON.parse(JSON.stringify(source.contextSnapshot).replaceAll("chat:telegram", "chat:speko").replaceAll('"telegram"', '"speko"'));
await db.update(heartbeatRuns).set({ contextSnapshot: sourceContext, runtimeMode: sourceRuntime, nativeIssueId: sourceRuntime === "native" ? f.issueId : null }).where(eq(heartbeatRuns.id, f.sourceRunId));
const resolve = () => resolveExternalChatQuestionResponse(db, f, f.context, "read", true);
expect(await resolve()).toBeNull();
const [interaction] = await db.select().from(issueThreadInteractions).where(eq(issueThreadInteractions.id, f.interactionId));
const sessionId = randomUUID();
await db.insert(chatVoiceSessions).values({ id: sessionId, companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, issueId: f.issueId, assignedAgentId: f.agentId, callerId: f.userId, callerAuthority: "member",
mode: "browser", state: "ended", providerSessionId: randomUUID(), generation: 1, credentialFingerprint: voiceCredentialFingerprint(connection.credentialSecretRefs),
toolTokenHash: "a".repeat(64), idempotencyKey: randomUUID(), requestFingerprint: "fixture", expiresAt: new Date(Date.now() + 60000) });
await db.insert(chatVoiceReplies).values({ companyId: f.companyId, sessionId, publicationId: f.publicationId, cursor: 1,
deliveredAt: new Date(interaction.resolvedAt!.getTime() - 1) });
const [receipt] = answerRoute === "voice" ? await db.insert(chatVoiceToolCalls).values({ companyId: f.companyId, sessionId, providerToolCallId: randomUUID(),
webhookId: randomUUID(), fingerprint: "signed-tool-fingerprint", tool: "answer_question",
response: { status: "answered", interactionId: f.interactionId, resultSha256: nativeSha256(interaction.result) } }).returning() : [];
if (answerRoute === "board") {
await db.update(chatExternalPrincipals).set({ provider: "speko", externalId: `voice:${sessionId}` }).where(eq(chatExternalPrincipals.id, f.principalId));
await db.insert(chatVoiceToolCalls).values({companyId: f.companyId, sessionId, providerToolCallId: randomUUID(),
webhookId: randomUUID(), fingerprint: "accepted-source-request", tool: "submit_request", response: {status: "accepted"}});
}
// The answer also publishes a terminal acknowledgment. It cannot compete
// with the pre-answer presentation as authority for the continuation.
const [terminal] = await db.insert(chatPublications).values({ companyId: f.companyId, endpointId: f.endpointId,
conversationId: f.conversationId, issueId: f.issueId, idempotencyKey: randomUUID(), state: "published",
payload: { text: "Answered: High.", interactionId: f.interactionId } }).returning();
await db.insert(chatVoiceReplies).values({ companyId: f.companyId, sessionId, publicationId: terminal.id, cursor: 2 });
expect(await resolve()).toMatchObject({ provider: "speko", marker: { interactionId: f.interactionId } });
// Voice authority is the authenticated session, not a synthetic provider
// identity link. Exercise the real native run attestation and recheck path.
await db.update(chatExternalPrincipals).set({ provider: "speko", externalId: `voice:${sessionId}` }).where(eq(chatExternalPrincipals.id, f.principalId));
await db.delete(chatIdentityLinks).where(eq(chatIdentityLinks.principalId, f.principalId));
await attestAnswer(f);
expect(await authorizeChatConversationForBoundRun(db, f, f.context, "read")).toMatchObject({ conversationId: f.conversationId });
await db.update(companyMemberships).set({ status: "removed" }).where(eq(companyMemberships.principalId, f.userId));
await expect(authorizeChatConversationForBoundRun(db, f, f.context, "read")).rejects.toThrow(answerRoute === "board" ? "paperclip_runner_chat_attachment_binding_denied" : "paperclip_runner_chat_attachment_principal_denied");
await db.update(companyMemberships).set({ status: "active" }).where(eq(companyMemberships.principalId, f.userId));
if (receipt) await db.update(chatVoiceToolCalls).set({ response: { ...receipt.response, resultSha256: "different-answer" } }).where(eq(chatVoiceToolCalls.id, receipt.id));
else await db.update(issueComments).set({authorUserId: randomUUID()}).where(eq(issueComments.id, f.commentId));
expect(await resolve()).toBeNull();
});
it.each(["valid", "wrong_task", "revoked_identity", "wrong_answer_actor"] as const)(
"attests legacy Slack question continuations only with the durable source binding: %s",
async (condition) => {
@@ -3837,6 +3887,24 @@ describe("native external-chat response wait", () => {
const fixture = await seedWaitTurn(
provider as Parameters<typeof seedWaitTurn>[0],
);
if (provider === "github") {
// Only upgraded GitHub bots keep the selected final internal. Legacy
// connection fixtures retain their automatic presentation behavior.
await db.insert(chatGitHubConfigurations).values({
companyId: fixture.companyId,
endpointId: fixture.endpointId,
configuration: {
version: 1,
toolsEnabled: true,
responsibleUserId: fixture.userId,
memberAccess: "all_linked",
people: [],
defaults: defaultGitHubReviewPolicy(),
repositories: {},
},
updatedByUserId: fixture.userId,
});
}
await placeWaitTurnInSetupTest(fixture, { generation });
await expect(
@@ -3866,9 +3934,11 @@ describe("native external-chat response wait", () => {
.select({ resultJson: heartbeatRuns.resultJson })
.from(heartbeatRuns)
.where(eq(heartbeatRuns.id, fixture.runId));
const authorizationReason = provider === "github"
? "internal_agent_write" : "allow_chat_run_presentation";
await expect(
resolveChatRunPresentationAuthorizationReason(db, fixture),
).resolves.toBe("allow_chat_run_presentation");
).resolves.toBe(authorizationReason);
const response = resolveHeartbeatRunResponse({
resultJson: finalizedRun!.resultJson,
preferFinalResponseOverExistingComment: true,
@@ -3884,14 +3954,14 @@ describe("native external-chat response wait", () => {
fixture.issueId,
response.text!,
{ agentId: fixture.agentId, runId: fixture.runId },
{ authorizationReason: "allow_chat_run_presentation" },
{ authorizationReason },
);
await expect(
db
.select()
.from(chatPublications)
.where(eq(chatPublications.commentId, comment.id)),
).resolves.toEqual([
).resolves.toEqual(provider === "github" ? [] : [
expect.objectContaining({
conversationId: fixture.conversationId,
endpointId: fixture.endpointId,
@@ -6,6 +6,7 @@ import { nativeCompletionSource, buildNativeCompletionContract } from "./complet
import { renderPaperclipWakePrompt } from "@paperclipai/adapter-utils/server-utils";
import { buildNativeExecutionInput } from "./native-execution-input.js";
import { nativeRuntimeContextFixture } from "./runtime-context.test-fixture.js";
import { buildNativeModelEnvelope, canonicalNativeRuntimeContextDigest } from "../../vendor/paperclip-runner/index.js";
describe("LCA-05 explicit native work mode", () => {
it.each(["standard", "planning", "ask"])("title and description cannot override %s mode", (workMode) => {
@@ -455,6 +456,28 @@ describe("native execution input external-chat framing", () => {
"Never infer the file's contents or substitute an older file",
);
const runtimeContext = nativeRuntimeContextFixture();
runtimeContext.skills = ["configured", "provider-mentioned"].map((name) => ({
key: `company/test/${name}`,
runtimeName: name,
versionId: null,
bundle: { ...runtimeContext.instructions.bundle },
}));
runtimeContext.aggregateDigest = canonicalNativeRuntimeContextDigest(runtimeContext);
const skillArgs = {
...args,
issue: { ...args.issue, description: "Stale provider message: /provider-mentioned" },
taskPrompt: `${args.taskPrompt}\nGitHub message: /provider-mentioned`,
runtimeContext,
githubInstructionSkillKeys: ["company/test/configured", "company/other/unknown"],
};
const skillInput = buildNativeExecutionInput(skillArgs);
expect(skillInput.task.description).toBe("Configured GitHub instruction skills:\n/configured");
expect(buildNativeModelEnvelope(skillInput)).toMatchObject({ requestedSkills: ["configured"] });
expect(buildNativeModelEnvelope(buildNativeExecutionInput({
...skillArgs, githubInstructionSkillKeys: [],
}))).toMatchObject({ requestedSkills: [] });
const wake = args.wakePayload as Record<string, unknown>;
for (const patch of [
{ externalChatProvider: "slack" },
@@ -472,6 +495,13 @@ describe("native execution input external-chat framing", () => {
expect(unrelated.task.prompt).not.toContain(
"Paperclip owns recovery navigation for unavailable GitHub attachments",
);
if (["slack", "discord", "telegram", "microsoft-teams"].includes(patch.externalChatProvider ?? "")) {
const unrelatedSkills = buildNativeExecutionInput({
...skillArgs, wakePayload: { ...wake, ...patch },
});
expect(unrelatedSkills.task.description).toBeNull();
expect(buildNativeModelEnvelope(unrelatedSkills)).toMatchObject({ requestedSkills: [] });
}
}
},
);
@@ -56,6 +56,8 @@ export interface BuildNativeExecutionInput {
wakePayload?: unknown;
/** Additive source ownership emitted by the server task/wake builders. */
turnContext?: unknown;
/** Skill keys selected from the admitted, manager-authored GitHub configuration. */
githubInstructionSkillKeys?: readonly string[];
resumedSession?: boolean;
previousTurn?: { runId: string; task: { title: string; description: string | null } } | null;
conversationMode?: boolean;
@@ -184,6 +186,15 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
const externalChatTurn =
isPaperclipExternalChatContractTurn(wakePayload) ||
isPaperclipExternalChatQuestionResponseTurn(wakePayload);
const configuredGitHubSkillKeys = new Set(input.githubInstructionSkillKeys ?? []);
const configuredGitHubSkills = externalChatTurn && wake?.externalChatProvider === "github"
? input.runtimeContext.skills.filter((skill) => configuredGitHubSkillKeys.has(skill.key))
: [];
// Native skill invocation reads task.description. Project only verified skill
// names here; never restore the first provider message or scan untrusted text.
const externalChatSkillDescription = configuredGitHubSkills.length > 0
? `Configured GitHub instruction skills:\n${configuredGitHubSkills.map((skill) => `/${skill.runtimeName}`).join("\n")}`
: null;
const taskPrompt = [
wakePrompt,
// Durable task questions must survive the current provider turn.
@@ -232,7 +243,7 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
// explicitly labeled background, but give authenticated external-chat
// turns neutral structured fields.
title: externalChatTurn ? "External chat follow-up" : input.issue.title,
description: externalChatTurn ? null : input.issue.description,
description: externalChatTurn ? externalChatSkillDescription : input.issue.description,
prompt: taskPrompt,
workMode: input.issue.workMode,
},
@@ -19,6 +19,7 @@ import { commitNativeStatusDecision } from "./status-decision-committer.js";
import { nativeSha256 } from "./canonical.js";
import { recordNativeAttentionAssessment } from "./work-assessments.js";
import { formatDurableQuestionResponseSummary } from "../question-response-delivery.js";
import { findSatisfiedToolConnection } from "../satisfied-connection-intents.js";
import {
NATIVE_STATUS_ARBITER_POLICY_VERSION,
type NativeAuthoritativeIssueStatus,
@@ -163,7 +164,7 @@ export async function materializeNativeInteractionResponses(input: {
if (requestedIds.size === 0) return [];
const [interactions, issue] = await Promise.all([
issueThreadInteractionService(input.db).listForIssue(input.issueId),
input.db.select({ status: issues.status }).from(issues).where(and(
input.db.select().from(issues).where(and(
eq(issues.id, input.issueId),
eq(issues.companyId, input.companyId),
)).limit(1).then((rows) => rows[0] ?? null),
@@ -201,6 +202,19 @@ export async function materializeNativeInteractionResponses(input: {
if (invocation.status !== expectedInvocationStatus || (request.status !== "rejected" && interaction.result?.toolAction?.status !== request.status)) throw new NativeInteractionBridgeError("native_interaction_governed_result_mismatch", "Tool review outcome does not match its invocation");
}
const interactionResult = record(interaction.result);
if (interaction.kind === "connection_intent" && interaction.status === "expired"
&& interactionResult.outcome === "expired" && typeof interactionResult.connectionId === "string") {
const [run] = await input.db.select({ responsibleUserId: heartbeatRuns.responsibleUserId }).from(heartbeatRuns)
.where(and(eq(heartbeatRuns.companyId, input.companyId), eq(heartbeatRuns.id, input.runId), eq(heartbeatRuns.agentId, input.agentId)));
const ready = issue.assigneeAgentId === input.agentId && run?.responsibleUserId === interaction.addresseeUserId
? await findSatisfiedToolConnection(input.db, issue, { ...interaction, addresseeUserId: interaction.addresseeUserId ?? null, status: "pending" }, input.runId) : null;
if (!ready || ready.id !== interactionResult.connectionId) {
throw new NativeInteractionBridgeError("native_interaction_unresolved", "The existing connection is no longer available");
}
responses.push({ interactionId: interaction.id, kind: interaction.kind,
response: { status: "expired", result: structuredClone(interaction.result) as unknown as Record<string, unknown> } });
continue;
}
const supersessionOutcome = interaction.status === "expired"
&& ["superseded_by_newer_request", "superseded_by_comment", "stale_target"].includes(String(interactionResult.outcome));
if (supersessionOutcome) {
@@ -2,7 +2,7 @@ import { eligibleIssueMonitorWait } from "../issue-monitors.js";
import { isNativePlanWaitResult, readNativePlanWait } from "./native-plan-wait.js";
import { activeIssueInteractionCondition } from "../issue-question-context.js";
import { hasPendingNativeChildCompletion } from "./native-child-completion-delivery.js";
import { settleSlackConversation } from "../slack-conversation-lifecycle.js";
import { settleExternalConversation } from "../slack-conversation-lifecycle.js";
import { executionFailureRetryCount } from "../execution-recovery-attempt.js";
import { readPersistedNativeProviderFailure } from "./native-provider-failure-evidence.js";
import { dismissAutomaticCompletionReviews } from "./automatic-completion-reviews.js";
@@ -607,7 +607,10 @@ async function resolveCommittedFinalizationRecovery(db: Db, run: typeof heartbea
eq(issueRecoveryActions.companyId, run.companyId), eq(issueRecoveryActions.sourceIssueId, issueId),
eq(issueRecoveryActions.kind, "active_run_watchdog"), inArray(issueRecoveryActions.status, ["active", "escalated"]),
sql`${issueRecoveryActions.evidence}->>'runId' = ${run.id}`,
sql`${issueRecoveryActions.wakePolicy}->>'kind' = 'resume_native_run'`,
or(
sql`${issueRecoveryActions.wakePolicy}->>'kind' = 'resume_native_run'`,
eq(issueRecoveryActions.cause, "native_workspace_finalization_owner_unverified"),
),
));
for (const action of actions) await issueRecoveryActionService(db).resolveActiveForIssue({
companyId: run.companyId, sourceIssueId: issueId, actionId: action.id,
@@ -1176,7 +1179,7 @@ export async function finalizeNativeRun(input: {
runId: run.id,
});
if (input.projectRunStatus) {
await settleSlackConversation(input.db, run.companyId, coordinator.issueId).catch((err) => {
await settleExternalConversation(input.db, run.companyId, coordinator.issueId).catch((err) => {
logger.warn({ err, runId: run.id }, "Slack conversation settlement deferred to reconciliation");
});
}
@@ -1581,7 +1584,7 @@ export async function finalizeNativeRun(input: {
runId: run.id,
});
if (input.projectRunStatus && finalizationPhase === "committed") {
await settleSlackConversation(input.db, run.companyId, coordinator.issueId).catch((err) => {
await settleExternalConversation(input.db, run.companyId, coordinator.issueId).catch((err) => {
logger.warn({ err, runId: run.id }, "Slack conversation settlement deferred to reconciliation");
});
}
@@ -47,7 +47,7 @@ import {
type NativeExecutionInput,
type PrpEvent,
} from "@paperclipai/paperclip-runner";
import { createHash, generateKeyPairSync } from "node:crypto";
import { createHash, generateKeyPairSync, randomUUID } from "node:crypto";
import { DatabaseSync } from "node:sqlite";
import { nativeSha256 } from "./canonical.js";
import { agentDirectoryWorkingCopyService } from "../agent-directory-working-copies.js";
@@ -203,6 +203,8 @@ const state = vi.hoisted(() => ({
async (): Promise<Record<string, unknown> | null> => null,
),
assertCurrentWakeCommentsRead: vi.fn(async () => undefined),
bundledPackManifestPath: null as string | null,
bundledRunnerBinary: null as string | null,
resolveRunnerBinary: vi.fn(() => "/tmp/paperclip-runnerd"),
release: null as null | (() => void),
}));
@@ -231,6 +233,8 @@ vi.mock("../../vendor/paperclip-runner/index.js", async (importOriginal) => {
>();
return {
...original,
bundledRemoteProviderPackManifestPath: () => state.bundledPackManifestPath ?? original.bundledRemoteProviderPackManifestPath(),
bundledRemoteRunnerBinary: () => state.bundledRunnerBinary ?? original.bundledRemoteRunnerBinary(),
createNativeSessionBackend: state.createBackend,
createRunnerdCodexTransport: state.createTransport,
executeNativeSession: state.execute,
@@ -365,6 +369,8 @@ import {
} from "./native-session-executor.js";
beforeEach(() => {
state.bundledPackManifestPath = null;
state.bundledRunnerBinary = null;
state.createAssignedMcpTools.mockReset();
state.resolveRunnerBinary.mockReset().mockReturnValue("/tmp/paperclip-runnerd");
state.resolveCurrentWakeCommentsBinding.mockReset().mockResolvedValue(null);
@@ -828,7 +834,7 @@ describe("remote runner process supervision", () => {
expect(handle.child.exitCode).toBeNull();
});
it("terminates a detached runner when its process identity cannot be adopted", async () => {
it.each(["missing marker", "missing fingerprint"])("terminates a detached runner with %s before admitting managed work", async kind => {
vi.useFakeTimers();
try {
let launchNonce = "";
@@ -847,10 +853,10 @@ describe("remote runner process supervision", () => {
}
if (label === "paperclip-runner-process-identity") {
return {
exitCode: 3,
exitCode: kind === "missing fingerprint" ? 0 : 3,
signal: null,
timedOut: false,
stdout: "",
stdout: kind === "missing fingerprint" ? `${launchNonce}\n4321\n2026-09-06T00:00:00.000Z\nrunner-remote\n` : "",
stderr: "",
};
}
@@ -881,6 +887,7 @@ describe("remote runner process supervision", () => {
stateDirectory: "/runtime",
diagnosticsDirectory: "/runtime/diagnostics",
runnerInstanceId: "runner-remote",
requireProcessStartFingerprint: true,
});
const handle = launcher({
@@ -1042,6 +1049,10 @@ describe("native provider usage normalization", () => {
it("reports the actual runtime billing identity", () => {
expect(resolveNativeBilling({ kind: "opencode", model: "openrouter/deepseek/test", permissionMode: "allow" }))
.toEqual({ provider: "deepseek", biller: "openrouter", billingType: "unknown" });
expect(resolveNativeBilling({ kind: "opencode", model: "openrouter/deepseek/test", permissionMode: "allow" }, { OPENROUTER_API_KEY: "fixture" }))
.toEqual({ provider: "deepseek", biller: "openrouter", billingType: "metered_api" });
expect(resolveNativeBilling({ kind: "codex", model: "gpt-6.1-sol" } as NativeExecutionInput["provider"], { CODEX_API_KEY: "fixture" }))
.toEqual({ provider: "openai", biller: "openai", billingType: "metered_api" });
expect(resolveNativeBilling({ kind: "acpx", agent: "claude", model: "claude-test", permissionMode: "approve-all",
profile: { driverKind: "acpx_runtime", protocolVersion: 1, acpxVersion: "0.13.1", agent: "claude", agentProfileVersion: 1,
agentServerPackage: "@zed-industries/claude-agent-acp", agentServerVersion: "1", agentRuntimePackage: null,
@@ -1056,6 +1067,8 @@ describe("native provider usage normalization", () => {
it.each(["OPENAI_BASE_URL", "OPENAI_API_BASE", "OPENAI_API_BASE_URL"])("does not price native or managed proxy traffic as direct OpenAI using %s", key => {
const provider = { kind: "codex", model: "gpt-6-astra" } as Parameters<typeof resolveNativeBilling>[0];
expect(resolveNativeBilling(provider, { OPENAI_API_KEY: "fixture", [key]: "https://proxy.example/v1" }).biller).toBe("unknown");
expect(resolveNativeBilling({ kind: "opencode", model: "openai/gpt-6-astra", permissionMode: "allow" }, { OPENAI_API_KEY: "fixture", [key]: "https://proxy.example/v1" }))
.toMatchObject({ provider: "openai", biller: "unknown", billingType: "metered_api" });
expect(resolveNativeBilling(provider, { [key]: "https://proxy.example/v1" }, { provider: "openai", biller: "openai", billingType: "metered_api" }).biller).toBe("unknown");
expect(resolveNativeBilling(provider, {}, { provider: "openai", biller: "openai", billingType: "metered_api" })).toMatchObject({ biller: "openai", billingType: "metered_api" });
});
@@ -1120,6 +1133,63 @@ describe("remote provider pack manifest", () => {
return JSON.stringify(value);
};
it.each(["opencode", "cursor"] as const)("uses the bundled Linux image authority for remote %s without controller overrides", async provider => {
const root = await mkdtemp(join(tmpdir(), "paperclip-bundled-image-"));
const digest = "sha256:" + "a".repeat(64);
const artifacts = Object.fromEntries(Object.entries({
grokLauncher: "dist/providers/grok/launcher.cjs", nodeCommand: "node_modules/node/bin/node",
productionLock: "pnpm-lock.yaml", opencodeCommand: "node_modules/.bin/opencode",
opencodeExecutable: "node_modules/opencode-ai/bin/opencode.exe",
opencodeProxy: "dist/cli/opencode-app-server-proxy.cjs", acpxSidecar: "dist/cli/acpx-runtime-sidecar.cjs",
}).map(([key, path]) => [key, { path, sha256: digest }]));
const payload = {
target: { platform: "linux", architecture: "x64" }, runnerSourceRevision: "a".repeat(40),
pins: { nodeMinimum: "24.11.0", codex: QUALIFIED_ACPX_PROFILES.codex.agentRuntimeVersion,
opencode: "1.18.34", acpx: "0.13.1", grok: QUALIFIED_ACPX_PROFILES.grok.agentRuntimeVersion,
claudeAcp: QUALIFIED_ACPX_PROFILES.claude.agentServerVersion, codexAcp: QUALIFIED_ACPX_PROFILES.codex.agentServerVersion },
acpxProfileDigests: Object.fromEntries(["grok", "claude", "codex"].map(agent =>
[agent, QUALIFIED_ACPX_PROFILES[agent as "grok" | "claude" | "codex"].commandDigest])),
distDigest: digest, artifacts,
bridgeDigest: "sha256:" + createHash("sha256").update(digest + "\n" + digest + "\n" + digest).digest("hex"),
};
const manifest = { schema: "paperclip-runner/remote-provider-pack/v1", payload,
digest: "sha256:" + createHash("sha256").update(canonical(payload)).digest("hex") };
state.bundledPackManifestPath = join(root, "provider-pack.json");
state.bundledRunnerBinary = join(root, "linux-paperclip-runnerd");
await writeFile(state.bundledPackManifestPath, JSON.stringify(manifest));
await writeFile(state.bundledRunnerBinary, "verified Linux daemon fixture");
const remoteExecution = { ...execution, provider: provider === "opencode"
? { kind: "opencode", model: null } : { kind: "acpx", agent: "cursor", model: null },
session: { ...execution.session, driverKind: provider === "opencode" ? "opencode_server" : "acpx_runtime" },
} as unknown as NativeExecutionInputV1;
const syncIn = vi.fn(async () => undefined);
const execute = vi.fn(async (command: { command: string; args?: string[] }) => {
if (command.args?.[0] === "--build-metadata") throw new Error("reached-staged-Linux-daemon-verification");
return { exitCode: 0, signal: null, timedOut: false, stdout: "", stderr: "" };
});
try {
await createRunnerdBackend({ db: leaseDb(remoteExecution), execution: remoteExecution,
runnerInstanceId: "bundled-image-runner", runnerIngressAuthorized: true,
runnerExecutionTarget: { kind: "remote", transport: "sandbox", remoteCwd: "/workspace",
environmentId: "environment", leaseId: "lease", providerKey: "daytona",
effectiveCapabilities: { runnerWebSocketIngress: true }, runner: { execute, syncIn } } as never });
state.createTransport.mockClear();
state.createBackend.mock.calls.at(-1)![1].codexTransportFactory!();
const transport = state.createTransport.mock.calls.at(-1)![0] as RunnerTransportOptions & {
controlPlaneRegistration: (authority: unknown) => Promise<unknown>;
};
expect(transport.runnerBinary).toBe(state.bundledRunnerBinary);
await expect(transport.controlPlaneRegistration({})).rejects.toThrow("reached-staged-Linux-daemon-verification");
expect(syncIn).toHaveBeenCalledWith([expect.objectContaining({ files: [expect.objectContaining({
sourcePath: state.bundledRunnerBinary, targetPath: "/workspace/.paperclip-runtime/paperclip-runner/bin/paperclip-runnerd",
})] })]);
expect(execute.mock.calls.some(([call]) => call.args?.[1] === "uname -s; uname -m")).toBe(false);
} finally {
state.bundledPackManifestPath = null; state.bundledRunnerBinary = null;
await rm(root, { recursive: true, force: true });
}
});
it("accepts a fully digested pack and rejects artifact tampering", async () => {
const root = await mkdtemp(join(tmpdir(), "paperclip-provider-pack-"));
await mkdir(join(root, "dist", "cli"), { recursive: true });
@@ -2521,6 +2591,14 @@ describe("remote runner build metadata", () => {
).not.toThrow();
});
it("requires an explicit Dot bridge capability only for remote Dot execution", () => {
expect(() => assertRemoteRunnerBuildMetadata(current, "listen_ws", "openai_dot_mcp"))
.toThrow("runner_remote_dot_capability_missing");
expect(() => assertRemoteRunnerBuildMetadata({ ...current, externalProviderCapabilities: ["openai_dot_mcp"] },
"listen_ws", "openai_dot_mcp")).not.toThrow();
expect(() => assertRemoteRunnerBuildMetadata(current, "listen_ws")).not.toThrow();
});
it("fails before dispatch when a preinstalled runner uses the stale contract", () => {
expect(() =>
assertRemoteRunnerBuildMetadata(
@@ -12890,8 +12968,9 @@ describe("runnerd provider runtime wiring", () => {
["opencode", { kind: "opencode", model: null }, "opencode_server"],
["acpx", { kind: "acpx", agent: "codex", model: null }, "acpx_runtime"],
])(
"requires the build-owned provider pack before launching remote %s",
"requires a build-owned provider pack identity before launching remote %s",
async (providerKind, provider, driverKind) => {
state.bundledPackManifestPath = join(tmpdir(), randomUUID(), "provider-pack.json");
const remoteCwd = "/home/daytona/paperclip-workspace";
const remoteProviderExecution = {
...execution,
@@ -12929,8 +13008,9 @@ describe("runnerd provider runtime wiring", () => {
},
},
}),
).rejects.toThrow(
"runner_remote_provider_artifact_incompatible: configure PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH",
).rejects.toThrow(providerKind === "opencode"
? "runner_remote_provider_artifact_incompatible: provider-pack.json is unreadable"
: "runner_remote_provider_artifact_incompatible: configure PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH",
);
expect(state.createBackend).not.toHaveBeenCalled();
},
@@ -13061,3 +13141,10 @@ it.each([
),
).toBe("native_event_replay_conflict");
});
it("keeps an ACPX missing runtime package failure actionable and stops automatic retries", () => {
const failure = new Error('PRP command session.open failed: {"result":{"code":"command_execution_failed","message":"failed to start ACPX provider: ACPX sidecar command session.open was rejected (retryable=false, classification=runtime_package_missing)"},"status":"failed"}');
const code = nativeSessionFailureSourceCode(failure);
expect(code).toBe("native_provider_runtime_missing");
expect(nativeSessionFailureDisposition(1, new Date(), code)).toEqual({ phase: "terminal_failure", failureCode: code, nextAttemptAt: null });
});
@@ -1,5 +1,7 @@
import { configuredEnvironment } from "../../vendor/paperclip-runner/index.js";
import { apiKeyBillingType } from "@paperclipai/adapter-utils/api-billing-type";
import { agents } from "@paperclipai/db";
import { isCloudManagedInstance } from "../cloud-instance.js";
import { dotRunnerBroker } from "../dot-runner-broker.js";
import { CURSOR_DISTRIBUTION_PINS, QUALIFIED_ACPX_PROFILES, QUALIFIED_ACPX_VERSION } from "../../vendor/paperclip-runner/index.js";
import { isProviderMode } from "../../vendor/paperclip-runner/index.js";
@@ -6187,6 +6189,7 @@ export function nativeSessionFailureDisposition(
sourceFailureCode?: ReturnType<typeof nativeSessionFailureSourceCode>,
) {
const permanentFailure =
sourceFailureCode === "native_provider_runtime_missing" ||
sourceFailureCode === "native_provider_model_rejected" ||
sourceFailureCode === "native_provider_approval_required" ||
sourceFailureCode === "native_event_replay_conflict" ||
@@ -6260,6 +6263,7 @@ export function nativeSessionFailureSourceCode(
| "native_runner_process_exited"
| "planning_mode_unsupported"
| "native_event_replay_conflict"
| "native_provider_runtime_missing"
| "native_provider_model_rejected"
| "native_current_wake_comments_unread"
| "native_current_wake_comments_changed_after_read"
@@ -6282,6 +6286,8 @@ export function nativeSessionFailureSourceCode(
if (error instanceof NativeSessionCleanupQuarantinedError)
return "native_session_cleanup_quarantined";
const message = error instanceof Error ? error.message : String(error);
if (/classification=runtime_package_missing\)/.test(message))
return "native_provider_runtime_missing";
if (/native_provider_model_rejected/i.test(message))
return "native_provider_model_rejected";
if (/native_adopted_runner_authentication_timeout/i.test(message)) {
@@ -7525,6 +7531,11 @@ export async function executePaperclipNativeSession(input: {
await retireSupersededWarmNativeSessions(
ownerScope, nativeSessionScopeKey(input.execution),
);
if (input.execution.provider.kind === "openai_dot" && isCloudManagedInstance()
&& (!input.useRunnerd || input.runnerExecutionTarget?.kind !== "remote"
|| input.runnerExecutionTarget.transport !== "sandbox")) {
throw new Error("dot_cloud_requires_managed_runner");
}
if (!input.useRunnerd) {
return await executePaperclipNativeSessionWithinScope(input);
}
@@ -7546,7 +7557,6 @@ export async function executePaperclipNativeSession(input: {
await closingWarmNativeSessions.get(sessionScopeId);
if (input.execution.provider.kind === "openai_dot") {
if (input.runnerExecutionTarget?.kind === "remote") throw new Error("dot_requires_local_runner_controller");
return await executePaperclipNativeSessionWithinScope(input);
}
const workspaceRoot = input.execution.workspace.cwd;
@@ -8532,7 +8542,7 @@ async function executePaperclipNativeSessionWithinScope(
: []),
),
eq(issueThreadInteractions.status, "pending"),
activeIssueInteractionCondition(),
activeIssueInteractionCondition({ runId: input.execution.binding.runId }),
// Live provider questions resume their current turn; only durable
// wake-based cards park it. A timeout creates a separate fallback.
sql`not (${issueThreadInteractions.kind} = 'ask_user_questions' and ${issueThreadInteractions.continuationPolicy} = 'none' and coalesce(${issueThreadInteractions.idempotencyKey}, '') like 'paperclip-runner-question:%')`,
@@ -9784,11 +9794,15 @@ export function resolveNativeBilling(provider: NativeExecutionInput["provider"],
const parts = (provider.model ?? "").split("/");
const biller = parts.length > 1 ? parts[0] : provider.kind === "acpx" ? nativeUsageBiller(provider) : "unknown";
const modelProvider = biller === "openrouter" && parts.length > 2 ? parts[1] : biller;
return { provider: modelProvider, biller, billingType: "unknown" };
const billingType = apiKeyBillingType(biller, env);
const effectiveBiller = biller === "openai" && billingType === "metered_api"
? inferOpenAiCompatibleBiller(env, "openai") ?? "unknown" : biller;
return { provider: modelProvider, biller: effectiveBiller, billingType };
}
if (provider.kind !== "codex" && !(provider.kind === "acpx" && provider.agent === "codex"))
return { provider: "unknown", biller: "unknown", billingType: "unknown" };
return { provider: "openai", biller: env.OPENAI_API_KEY ? inferOpenAiCompatibleBiller(env, "openai") : "unknown", billingType: env.OPENAI_API_KEY ? "metered_api" : "unknown" };
const billingType = apiKeyBillingType("openai", env, "codex");
return { provider: "openai", biller: billingType === "metered_api" ? inferOpenAiCompatibleBiller(env, "openai") : "unknown", billingType };
}
export function normalizeNativeUsage(usage: Record<string, unknown> | null, options: { inputIncludesCacheReads?: boolean } = {}) {
@@ -10131,6 +10145,7 @@ function readRemoteProviderPackIdentity(packRoot: string, verifyControllerFiles:
export function assertRemoteRunnerBuildMetadata(
value: unknown,
requiredMode: "dial_wss" | "listen_ws",
requiredExternalProvider?: "openai_dot_mcp",
): void {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("runner_remote_artifact_metadata_invalid");
@@ -10147,6 +10162,10 @@ export function assertRemoteRunnerBuildMetadata(
// Older sandbox images share binary contract v2, but reject the zero
// lifetime now used by the controller and cannot renew connection leases.
// Reject them before launch so preparation stages the bundled runner instead.
if (requiredExternalProvider && (!Array.isArray(metadata.externalProviderCapabilities)
|| !metadata.externalProviderCapabilities.includes(requiredExternalProvider))) {
throw new Error("runner_remote_dot_capability_missing: install the current Paperclip Runner artifact");
}
const sessionCapabilities = Array.isArray(metadata.durableSessionCapabilities)
? metadata.durableSessionCapabilities
: [];
@@ -10762,7 +10781,8 @@ async function waitForRemoteRunnerProcessIdentity(input: {
identityPath: string;
nonce: string;
runnerInstanceId: string;
}): Promise<{ pid: number; startedAt: string }> {
requireProcessStartFingerprint?: boolean;
}): Promise<{ pid: number; startedAt: string; processStartFingerprint?: string }> {
const deadline = Date.now() + REMOTE_RUNNER_PROCESS_IDENTITY_WAIT_MS;
while (Date.now() < deadline) {
const result = await input.runner
@@ -10782,7 +10802,7 @@ async function waitForRemoteRunnerProcessIdentity(input: {
result && result.exitCode === 0 && !result.timedOut
? parseRemoteRunnerProcessIdentity(result.stdout, input)
: null;
if (identity) return identity;
if (identity && (!input.requireProcessStartFingerprint || identity.processStartFingerprint)) return identity;
await new Promise<void>((resolve) => setTimeout(resolve, 100));
}
throw new Error("runner_remote_process_identity_unavailable");
@@ -10821,6 +10841,7 @@ export function createRemoteRunnerProcessLauncher(input: {
diagnosticsDirectory: string;
runnerInstanceId: string;
ensureArtifact?: () => Promise<void>;
requireProcessStartFingerprint?: boolean;
onSpawn?: (meta: {
pid: number;
processGroupId: number | null;
@@ -10956,6 +10977,7 @@ export function createRemoteRunnerProcessLauncher(input: {
identityPath: input.processIdentityPath,
nonce: identityNonce,
runnerInstanceId: input.runnerInstanceId,
requireProcessStartFingerprint: input.requireProcessStartFingerprint,
});
} catch {
const cleanupStartedAtMs = Date.now();
@@ -11196,6 +11218,129 @@ export async function createRunnerdBackend(input: {
}
}
/** Dot uses the same managed process launcher and authenticated PRP transports.
* Its execution contract still exposes no workspace, shell, or provider secrets. */
async function prepareRemoteDotRunner({ input, target, runner, root, identity }: {
input: Parameters<typeof createRunnerdBackend>[0];
target: Extract<AdapterExecutionTarget, { kind: "remote" }>;
runner: CommandManagedRuntimeRunner;
root: string;
identity: NonNullable<NonNullable<Parameters<typeof createNativeSessionBackend>[1]>["dotRunnerOptions"]>["identity"];
}) {
const runnerBinary = input.runnerRemoteBinaryPath?.trim() || resolvePaperclipRunnerBinary();
const remoteRoot = posix.join(target.remoteCwd, ".paperclip-runtime", "paperclip-runner");
const remoteBinary = posix.join(remoteRoot, "bin", "paperclip-runnerd");
const runnerStateDirectory = posix.join(remoteRoot, "sessions",
createHash("sha256").update(identity.normalizedSessionId).digest("hex"), "runner");
const requiredMode = resolveRemoteRunnerTransportMode({ target, runnerIngressAuthorized: input.runnerIngressAuthorized === true });
const artifact = readRunnerdArtifactBinding(runnerBinary);
const prepare = async () => {
if (input.restartRecovery?.kind !== "reattach_remote_runner") {
if (!input.runnerRemoteBinaryPath?.trim()) {
const platform = await runner.execute({ command: "sh", args: ["-c", "uname -s; uname -m"], bypassSession: true, timeoutMs: 10000 });
const [os, arch] = platform.stdout.trim().split(/\r?\n/);
if (platform.exitCode !== 0 || platform.timedOut || os !== (process.platform === "darwin" ? "Darwin" : "Linux")
|| ![process.arch === "x64" ? "x86_64" : "aarch64", process.arch].includes(arch)) {
throw new Error("runner_remote_artifact_platform_mismatch: configure PAPERCLIP_RUNNER_REMOTE_BINARY_PATH for the sandbox platform");
}
}
await stageRemoteRunnerFile({ target, runner, sourcePath: runnerBinary, targetPath: remoteBinary, mode: 0o700 });
}
const metadata = await runner.execute({ command: remoteBinary, args: ["--build-metadata"], bypassSession: true, timeoutMs: 30000 });
if (metadata.exitCode !== 0 || metadata.timedOut) throw new Error("runner_remote_artifact_verification_failed");
assertRemoteRunnerBuildMetadata(JSON.parse(metadata.stdout), requiredMode, "openai_dot_mcp");
const checksum = await runner.execute({ command: "sh", args: ["-c",
'if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1"; else shasum -a 256 "$1"; fi', "paperclip-dot-artifact", remoteBinary],
bypassSession: true, timeoutMs: 10000 });
if (checksum.exitCode !== 0 || checksum.timedOut || `sha256:${checksum.stdout.trim().split(/\s+/)[0]}` !== artifact.digest) {
throw new Error("runner_remote_artifact_verification_failed");
}
};
const readProviderState = async () => {
const result = await runner.execute({ command: "sh", args: ["-c",
'set -eu; if test ! -e "$1" && test ! -L "$1"; then exit 3; fi; test -f "$1" && test ! -L "$1"; test "$(wc -c < "$1")" -le 33554432; base64 < "$1"',
"paperclip-dot-checkpoint", posix.join(runnerStateDirectory, "dot-provider-state.json")], bypassSession: true, timeoutMs: 10000 });
if (result.exitCode === 3 && !result.timedOut) return null;
if (result.exitCode !== 0 || result.timedOut) throw new Error("dot_provider_checkpoint_unavailable");
const state = record(JSON.parse(Buffer.from(result.stdout.replace(/\s+/g, ""), "base64").toString("utf8")));
if (state.schema !== "paperclip.runner.dot-provider-state.v1" || state.runId !== identity.runId
|| state.sessionId !== identity.normalizedSessionId || state.turnId !== identity.turnId) throw new Error("dot_runner_checkpoint_authority_mismatch");
// Retain inspectable recovery evidence on the controller without changing
// which execution target is authoritative for replay.
mkdirSync(join(root, "runner"), { recursive: true, mode: 0o700 });
const saved = join(root, "runner", "dot-provider-state.json");
const temporary = `${saved}.${randomUUID()}.tmp`;
writeFileSync(temporary, JSON.stringify(state), { mode: 0o600 });
renameSync(temporary, saved);
return state;
};
let spawned: () => void = () => {};
const processStarted = new Promise<void>(resolve => { spawned = resolve; });
// A transport handshake alone does not prove a recoverable process owner.
// Do not attach the Dot broker until the exact sandbox marker is validated.
let identityReady: () => void = () => {};
let identityFailed: (error: unknown) => void = () => {};
const processIdentityReady = new Promise<void>((resolve, reject) => { identityReady = resolve; identityFailed = reject; });
void processIdentityReady.catch(() => {});
const adoptExistingRunner = input.restartRecovery?.kind === "reattach_remote_runner"
? await verifyRemoteRunnerReattachment({ claim: input.restartRecovery, target, identity: { ...identity },
runId: input.execution.binding.runId, normalizedSessionId: nativeSessionKey(input.execution) }) : undefined;
if (adoptExistingRunner) { spawned(); identityReady(); }
const launch = createRemoteRunnerProcessLauncher({ target, runner, remoteBinary,
processIdentityPath: posix.join(runnerStateDirectory, "runner-process.identity"), stateDirectory: runnerStateDirectory,
diagnosticsDirectory: posix.join(remoteRoot, "diagnostics"), runnerInstanceId: input.runnerInstanceId,
requireProcessStartFingerprint: true,
onSpawn: async meta => { await input.onSpawn?.(meta); identityReady(); },
onLog: input.onLog, trace: input.trace, onRunnerProcessSpawned: () => spawned() });
const runnerProcessLauncher: typeof launch = spec => {
const handle = launch(spec);
void handle.completion.then(() => identityFailed(new Error("runner_remote_process_identity_unavailable")), identityFailed);
return handle;
};
const controlPlaneRegistration: NonNullable<NonNullable<NonNullable<Parameters<typeof createNativeSessionBackend>[1]>["dotRunnerOptions"]>["controlPlaneRegistration"]> = async authority => {
let transport: PaperclipRunnerTransport;
// Validate outbound URL before staging; provider ingress needs the staged
// listener binary before requesting the provider's private endpoint.
if (requiredMode === "dial_wss") {
transport = await resolvePaperclipRunnerTransport({ target, runId: input.execution.binding.runId,
localConnectUrl: "ws://127.0.0.1/unused", runnerPublicUrl: input.runnerPublicUrl,
runnerCaBundlePath: input.runnerCaBundlePath, runnerIngressAuthorized: input.runnerIngressAuthorized === true });
await prepare();
} else {
await prepare();
transport = await resolvePaperclipRunnerTransport({ target, runId: input.execution.binding.runId,
localConnectUrl: "ws://127.0.0.1/unused", runnerIngressAuthorized: input.runnerIngressAuthorized === true });
}
await input.onLog?.("stderr", `[paperclip-runner] Dot controller target=remote transport=${transport.mode}\n`);
if (transport.mode !== "provider_ingress") {
const registration = await registerRunnerPrpAuthority({ companyId: input.execution.binding.companyId,
issueId: input.execution.binding.issueId, agentId: input.execution.binding.agentId,
runId: input.execution.binding.runId, authority });
let caBundlePath = transport.mode === "direct_outbound" ? transport.caBundlePath : undefined;
if (caBundlePath) {
const remoteCaBundlePath = posix.join(remoteRoot, "bin", "runner-ca-bundle.pem");
await stageRemoteRunnerFile({ target, runner, sourcePath: caBundlePath, targetPath: remoteCaBundlePath, mode: 0o600 });
caBundlePath = remoteCaBundlePath;
}
return { ...registration, ready: async () => { await processIdentityReady; }, connection: { mode: "connect" as const, connectUrl: transport.connectUrl,
...(caBundlePath ? { caBundlePath } : {}) } };
}
let outbound: ReturnType<typeof connectRunnerPrpIngress> | null = null;
let activation: Promise<void> | null = null;
return { connection: { mode: "listen" as const, listenAddress: transport.listenAddress,
listenPort: transport.listenPort, listenPath: transport.listenPath },
activate: () => { activation = (async () => { await processStarted;
outbound = connectRunnerPrpIngress({ authority, endpoint: transport.ingress }); })();
void activation.catch(() => {}); },
ready: async () => { await activation; if (!outbound) throw new Error("runner_ingress_unavailable"); await outbound.ready; await processIdentityReady; },
get failure() { return outbound?.failure; },
startupFailureCode: "runner_ingress_unavailable" as const,
release: async () => { if (outbound) await outbound.close(); else await transport.ingress.close(); },
};
};
return { runnerBinary, runnerStateDirectory, runnerProcessLauncher, readProviderState, controlPlaneRegistration, adoptExistingRunner };
}
async function createRunnerdBackendWithinSessionClaim(
input: Parameters<typeof createRunnerdBackend>[0],
sessionScopeId: string,
@@ -11321,24 +11466,31 @@ async function createRunnerdBackendWithinSessionClaim(
input.execution.binding.executionWorkspaceId;
mkdirSync(root, { recursive: true, mode: 0o700 });
if (input.execution.schema === "paperclip.native-execution-input.v6") {
if (target.kind !== "local") throw new Error("dot_runner_requires_local_controller: Dot has no mounted workspace or sandbox process");
if (target.kind === "remote" && input.dotWorkspaceRoot) throw new Error("dot_remote_workspace_access_unavailable");
const remoteDot = target.kind === "remote"
? await prepareRemoteDotRunner({ input, target, runner: remoteCommandRunner!, root,
identity: { runnerInstanceId: effectiveRunnerInstanceId, environmentLeaseId: effectiveEnvironmentLeaseId,
runId: input.execution.binding.runId, normalizedSessionId: nativeSessionKey(input.execution),
turnId: `turn-${input.execution.binding.runId}`, itemId: `item-${input.execution.binding.runId}` } })
: null;
const recoveredProcess = input.restartRecovery?.kind === "reattach_existing_runner" ? input.restartRecovery.process : null;
const backend = createNativeSessionBackend(input.execution, {
onSpawn: input.onSpawn, dynamicTools,
dynamicToolHandler: async call => { await dotRunnerBroker(input.db).assertRunAuthority(input.execution as import("../../vendor/paperclip-runner/index.js").NativeExecutionInputV6); return authorityEpoch.execute(call); },
completionFeedback: async result => { await dotRunnerBroker(input.db).assertRunAuthority(input.execution as import("../../vendor/paperclip-runner/index.js").NativeExecutionInputV6); await authorityEpoch.definitions(); return nativeCompletionFeedback(input.db, input.execution.binding.runId, result); },
dotRunnerOptions: {
stateDirectory: root, runnerBinary: resolvePaperclipRunnerBinary(),
stateDirectory: root, runnerBinary: remoteDot?.runnerBinary ?? resolvePaperclipRunnerBinary(),
...(remoteDot ?? {}),
identity: { runnerInstanceId: effectiveRunnerInstanceId, environmentLeaseId: effectiveEnvironmentLeaseId,
runId: input.execution.binding.runId, normalizedSessionId: nativeSessionKey(input.execution),
turnId: `turn-${input.execution.binding.runId}`, itemId: `item-${input.execution.binding.runId}` },
port: dotRunnerBroker(input.db).port(input.execution),
controlPlaneRegistration: async authority => registerRunnerPrpAuthority({ companyId: input.execution.binding.companyId,
controlPlaneRegistration: remoteDot?.controlPlaneRegistration ?? (async authority => registerRunnerPrpAuthority({ companyId: input.execution.binding.companyId,
issueId: input.execution.binding.issueId, agentId: input.execution.binding.agentId,
runId: input.execution.binding.runId, authority }),
adoptExistingRunner: recoveredProcess ? { ...recoveredProcess,
runId: input.execution.binding.runId, authority })),
adoptExistingRunner: remoteDot?.adoptExistingRunner ?? (recoveredProcess ? { ...recoveredProcess,
isAlive: () => verifiedRecoveryProcessIsAlive(recoveredProcess),
signal: signal => signalVerifiedRecoveryProcess(recoveredProcess, signal) } : undefined,
signal: signal => signalVerifiedRecoveryProcess(recoveredProcess, signal) } : undefined),
},
});
const prior = sessionToolAuthorityEpochs.get(sessionScopeId); if (prior) prior.revoke();
@@ -11368,9 +11520,10 @@ async function createRunnerdBackendWithinSessionClaim(
const configuredProviderPackRoot =
input.runnerRemoteProviderPackPath?.trim() || remotePiCompanion?.providerPack || null;
let expectedProviderPackManifest: RemoteProviderPackManifest | null = null;
const useBundledCursorImageAssets = requiresRemoteProviderPack && !configuredProviderPackRoot &&
input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor";
if (useBundledCursorImageAssets) {
const useBundledRemoteImageAssets = requiresRemoteProviderPack && !configuredProviderPackRoot &&
(input.execution.provider.kind === "opencode" ||
(input.execution.provider.kind === "acpx" && input.execution.provider.agent === "cursor"));
if (useBundledRemoteImageAssets) {
expectedProviderPackManifest = readBundledRemoteProviderPackManifest();
} else if (requiresRemoteProviderPack) {
if (
@@ -11399,7 +11552,7 @@ async function createRunnerdBackendWithinSessionClaim(
// When an explicit remote artifact is configured, prepareRemoteRunner stages
// these exact bytes at remoteBinary before launch.
const controllerRunnerBinary = remoteTarget
? input.runnerRemoteBinaryPath?.trim() || remotePiCompanion?.runnerBinary || (useBundledCursorImageAssets ? bundledRemoteRunnerBinary() : resolvePaperclipRunnerBinary())
? input.runnerRemoteBinaryPath?.trim() || remotePiCompanion?.runnerBinary || (useBundledRemoteImageAssets ? bundledRemoteRunnerBinary() : resolvePaperclipRunnerBinary())
: resolvePaperclipRunnerBinary();
const explicitRemoteCodex = input.runnerRemoteCodexPath?.trim() || null;
const remoteCodexNpmSpec = input.runnerRemoteCodexNpmSpec?.trim() || null;
@@ -11579,7 +11732,7 @@ async function createRunnerdBackendWithinSessionClaim(
remoteRuntimeRoot, remoteCodexBinary, runnerRemoteBinaryPath: input.runnerRemoteBinaryPath,
runnerRemoteCodexPath: input.runnerRemoteCodexPath, runnerRemoteCodexNpmSpec: input.runnerRemoteCodexNpmSpec,
model: input.execution.provider.kind === "codex" ? input.execution.provider.model : null,
reuseRetainedRunner: sandboxLeaseAcquisition?.outcome === "resumed", targetPlatformArtifactVerified: remotePiCompanion !== null,
reuseRetainedRunner: sandboxLeaseAcquisition?.outcome === "resumed", targetPlatformArtifactVerified: remotePiCompanion !== null || useBundledRemoteImageAssets,
trace: input.trace, onLog: input.onLog,
})
: null;
@@ -13246,7 +13399,7 @@ export function createRemoteNativeArtifactPreparation(input: {
runnerRemoteCodexNpmSpec?: string | null;
model?: string | null;
reuseRetainedRunner?: boolean;
/** Existing operator-imported Pi companion already verifies its target-platform artifact. */
/** The selected Pi companion or bundled provider image verifies its target-platform artifact. */
targetPlatformArtifactVerified?: boolean;
trace?: NativeRunTrace;
onLog?: (stream: "stdout" | "stderr", chunk: string) => Promise<void>;
@@ -57,6 +57,7 @@ const support = await getEmbeddedPostgresTestSupport();
{ companyId, issueId, body: "UNTRUSTED BODY", authorAgentId: agentId, createdAt: new Date(5_000),
sourceTrust: { preset: "low_trust_review", disposition: "quarantined", sourceIssueId: issueId } },
{ companyId, issueId, body: "FUTURE MESSAGE", authorUserId: "user", createdAt: new Date(30_000) },
{ companyId, issueId, body: "I’ll check the review request.", authorAgentId: agentId, origin: "fast_response", createdAt: new Date(4_500) },
]);
const { eq } = await import("drizzle-orm");
await db.update(issues).set({ conversationBoundaryCommentId: boundaryId }).where(eq(issues.id, issueId));
@@ -95,6 +96,8 @@ const support = await getEmbeddedPostgresTestSupport();
it("preserves the original goal and prior answer while excluding reset, deleted and future history", async () => {
const result = await buildNativeSessionHandoff({ db, companyId, issueId, agentId, before: new Date(20_000) });
expect(result).toContain("Build a GitHub PR review bot");
expect(result).toContain('"author":"paperclip_receipt"');
expect(result).toContain("[Paperclip acknowledgement; not agent work] I’ll check the review request.");
expect(result).toContain("Final constraint: require a team review");
expect(result).toContain('"truncated":true');
expect(Buffer.byteLength(result!)).toBeLessThanOrEqual(NATIVE_HANDOFF_MAX_BYTES);
@@ -3,6 +3,7 @@ import { documents, heartbeatRunEvents, heartbeatRuns, issueComments, issueDocum
import { createRunSecretRedactionRegistry } from "../run-secret-redaction.js";
import { buildLowTrustSourceTrust, redactQuarantinedBodyForHigherTrust, sanitizeQuarantinedCommentForHigherTrust } from "../source-trust.js";
import { resolveCoreTrustPreset } from "../trust-preset-resolver.js";
import { fastResponseHistoryBody } from "@paperclipai/shared";
export const NATIVE_HANDOFF_MAX_BYTES = 24_000;
const ENTRY_MAX_CHARS = 4_000;
@@ -72,7 +73,7 @@ export async function buildNativeSessionHandoff(input: {
id: issueComments.id, body: excerpt(sql`${issueComments.body}`),
truncated: sql<boolean>`length(${issueComments.body}) > ${ENTRY_MAX_CHARS}`,
authorAgentId: issueComments.authorAgentId, sourceTrust: issueComments.sourceTrust,
createdAt: issueComments.createdAt,
createdAt: issueComments.createdAt, origin: issueComments.origin,
};
const commentScope = and(eq(issueComments.companyId, companyId), eq(issueComments.issueId, issueId), isNull(issueComments.deletedAt),
lte(issueComments.createdAt, input.before),
@@ -140,7 +141,9 @@ export async function buildNativeSessionHandoff(input: {
};
const addComment = (row: typeof recent[number], kind: string) => {
if (entries.some(entry => entry.id === row.id)) return;
entries.push({ ...sanitizeQuarantinedCommentForHigherTrust(row), kind, author: row.authorAgentId ? "agent" : "user" });
const safe = sanitizeQuarantinedCommentForHigherTrust(row);
entries.push({ ...safe, body: fastResponseHistoryBody(safe), kind,
author: row.origin === "fast_response" ? "paperclip_receipt" : row.authorAgentId ? "agent" : "user" });
};
if (origin[0]) addComment(origin[0], "original_request");
recent.slice(0, LIMIT).forEach(row => addComment(row, "message"));
@@ -461,7 +461,7 @@ describe("capability-gated connection tool refresh", () => {
it("wires exact-session recovery and guarded selected identity into heartbeat persistence", () => {
const source = readFileSync(
new URL("../heartbeat.ts", import.meta.url),
new URL("../heartbeat/runtime-selection.ts", import.meta.url),
"utf8",
);
expect(source).toContain("await findNativeSessionResumeRun(db,");
@@ -1922,6 +1922,18 @@ describe("rebindNativeSessionCheckpoint", () => {
retainedFingerprint:
"sha256:68a51d34e091c55ee5d0d2b563153454dd727d72db16e6a27c358d342ae489c9",
},
{
contract: "GitHub working-comment tools",
// Deployed v14 threads retain the catalog without update_comment.
retainedFingerprint:
"sha256:134a7dbd526179aff57f91c261bb653e83db51c20492efab5c26a5ce618792c8",
},
{
contract: "GitHub instruction skill selection",
// Deployed v15 remote threads omitted configured skills from native turns.
retainedFingerprint:
"sha256:f12fe3b1bf5d63b7954d8ab16f57764874806ab49acff7c6582c4de007f4ed4d",
},
{
contract: "task-bound human-input description",
// Deployed v9 still advertises the generic mock-task question description.
@@ -33,7 +33,7 @@ export function nativeToolContractFingerprintForTarget(
return `sha256:${createHash("sha256")
.update(
JSON.stringify({
schema: "paperclip.native-tool-contract.v14",
schema: "paperclip.native-tool-contract.v16",
executionTargetKind,
advertisementPolicy: {
// Direct provider threads retain declarations from thread/start.
@@ -48,6 +48,8 @@ export function nativeToolContractFingerprintForTarget(
"always_advertised_run_issue_agent_binding_gated_current_task_description.v2",
semanticCompletion: "finish_accessible_deliverable_evidence.v4",
connectorTools: "assigned_resources_and_pinned_skill_bundle.v1",
githubResponses: "bound_working_comment_progress_and_final.v1",
githubInstructionSkills: "trusted_configuration_native_selection.v1",
...(executionTargetKind === "remote"
? { assignedMcpTools: "codex_server_gateway_prp_relay.v1" }
: {}),
@@ -0,0 +1,29 @@
import { heartbeatRuns, nativeRunFinalizations, nativeRunResults, type Db } from "@paperclipai/db";
import { and, eq, exists } from "drizzle-orm";
/**
* An accepted result transfers recovery to the workspace finalizer. A dead
* provider PID cannot authorize discarding its unfinished workspace suffix,
* including the interval before physical copyback acquires its owner receipt.
* The heartbeat's display phase and arbitrary profile JSON are not evidence.
*/
export function pendingNativeWorkspaceFinalizationCondition(db: Db) {
return and(
eq(heartbeatRuns.runtimeMode, "native"),
exists(db.select({ runId: nativeRunFinalizations.runId })
.from(nativeRunFinalizations)
.innerJoin(nativeRunResults, and(
eq(nativeRunResults.id, nativeRunFinalizations.resultId),
eq(nativeRunResults.runId, nativeRunFinalizations.runId),
eq(nativeRunResults.companyId, nativeRunFinalizations.companyId),
eq(nativeRunResults.issueId, nativeRunFinalizations.issueId),
eq(nativeRunResults.schemaStatus, "accepted"),
))
.where(and(
eq(nativeRunFinalizations.runId, heartbeatRuns.id),
eq(nativeRunFinalizations.companyId, heartbeatRuns.companyId),
eq(nativeRunFinalizations.issueId, heartbeatRuns.nativeIssueId),
eq(nativeRunFinalizations.phase, "workspace_finalizing"),
))),
)!;
}
@@ -13,8 +13,10 @@ const serviceMocks = vi.hoisted(() => ({
readCommittedForRuntime: vi.fn(),
getEffectiveProfilesForAgent: vi.fn(),
githubBotConnectionIdsForRun: vi.fn(),
spekoToolsForSession: vi.fn(),
}));
vi.mock("../voice/speko-agent-tools.js", () => ({ spekoToolsForSession: serviceMocks.spekoToolsForSession }));
vi.mock("../chat-github-tools.js", () => ({
githubBotConnectionIdsForRun: serviceMocks.githubBotConnectionIdsForRun,
}));
@@ -58,6 +60,7 @@ beforeEach(async () => {
vi.clearAllMocks();
serviceMocks.readCommittedForRuntime.mockResolvedValue(null);
serviceMocks.githubBotConnectionIdsForRun.mockResolvedValue(new Set());
serviceMocks.spekoToolsForSession.mockResolvedValue([]);
previousPaperclipHome = process.env.PAPERCLIP_HOME;
previousInstanceId = process.env.PAPERCLIP_INSTANCE_ID;
const root = await mkdtemp(path.join(tmpdir(), "paperclip-native-context-"));
@@ -406,3 +409,14 @@ it("pins channel tools only when the current task run is bound to that bot", asy
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBe("native-mcp:run-1");
expect(serviceMocks.githubBotConnectionIdsForRun).toHaveBeenLastCalledWith(input.db, "company-1", "agent-1", "run-1");
});
it("pins a voice tool only when the current requester can call through that assigned endpoint", async () => {
serviceMocks.getEffectiveProfilesForAgent.mockResolvedValue({ entries: [{ effect: "include", connectionId: "voice-connection" }], installedConnections: [{ id: "voice-connection", status: "active", enabled: true, healthStatus: "healthy", transport: "voice", config: { provider: "speko" }, transportConfig: {} }], allowedTools: [{ id: "phone-tool", connectionId: "voice-connection" }] });
const limit = vi.fn().mockResolvedValue([{ contextSnapshot: { issueId: "task-1" }, activeIdentityContextId: "identity-1" }]);
const db = { select: () => ({ from: () => ({ where: () => ({ limit }) }) }) } as unknown as Db;
const input = { db, agent: { id: "agent-1", companyId: "company-1" }, runId: "run-1" };
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBeNull();
serviceMocks.spekoToolsForSession.mockResolvedValue([{ connectionId: "voice-connection" }]);
expect((await resolveNativeRuntimeMcpSnapshot(input)).bindingId).toBe("native-mcp:run-1");
expect(serviceMocks.spekoToolsForSession).toHaveBeenLastCalledWith(db, { companyId: "company-1", agentId: "agent-1", runId: "run-1", issueId: "task-1", identityContextId: "identity-1" });
});
@@ -1,3 +1,4 @@
import { spekoToolsForSession } from "../voice/speko-agent-tools.js";
import { githubBotConnectionIdsForRun } from "../chat-github-tools.js";
import { isBrowserUseConnection } from "../browser-use-client.js";
import { createHash, randomUUID } from "node:crypto";
@@ -196,8 +197,9 @@ export async function resolveNativeRuntimeMcpSnapshot(input: { db: Db; agent: Pi
: {};
return config.sourceTemplateKey === "github" || transportConfig.sourceTemplateKey === "github";
});
const [runIdentity] = hasGitHubConnection
? await input.db.select({ responsibleUserId: heartbeatRuns.responsibleUserId, activeIdentityContextId: heartbeatRuns.activeIdentityContextId })
const hasVoiceConnection = effective.installedConnections.some(connection => connection.transport === "voice");
const [runIdentity] = hasGitHubConnection || hasVoiceConnection
? await input.db.select({ contextSnapshot: heartbeatRuns.contextSnapshot, responsibleUserId: heartbeatRuns.responsibleUserId, activeIdentityContextId: heartbeatRuns.activeIdentityContextId })
.from(heartbeatRuns)
.where(and(
eq(heartbeatRuns.id, input.runId),
@@ -219,6 +221,7 @@ export async function resolveNativeRuntimeMcpSnapshot(input: { db: Db; agent: Pi
// App access is optional runtime context. Keep usable assignments pinned, but
// do not stop unrelated work because an assigned app needs attention.
const githubBotConnectionIds = await githubBotConnectionIdsForRun(input.db, input.agent.companyId, input.agent.id, input.runId);
const spekoConnectionIds = hasVoiceConnection ? new Set((await spekoToolsForSession(input.db, { companyId: input.agent.companyId, agentId: input.agent.id, runId: input.runId, issueId: typeof runIdentity?.contextSnapshot?.issueId === "string" ? runIdentity.contextSnapshot.issueId : typeof runIdentity?.contextSnapshot?.taskId === "string" ? runIdentity.contextSnapshot.taskId : null, identityContextId: runIdentity?.activeIdentityContextId })).map(tool => tool.connectionId)) : new Set<string>();
const availableConnectionIds = new Set(resolvedInstalledConnections.filter((connection) =>
permitted.has(connection.id)
&& connection.status === "active"
@@ -226,7 +229,7 @@ export async function resolveNativeRuntimeMcpSnapshot(input: { db: Db; agent: Pi
&& (Boolean(runIdentity?.activeIdentityContextId) && (connection.config?.sourceTemplateKey === "github" || connection.transportConfig?.sourceTemplateKey === "github")
|| connection.credentialPolicy === "per_user"
|| !isToolConnectionAttentionHealth(connection.healthStatus))
&& (["mcp_remote", "local_stdio"].includes(connection.transport) || isBrowserUseConnection(connection) || githubBotConnectionIds.has(connection.id))
&& (["mcp_remote", "local_stdio"].includes(connection.transport) || isBrowserUseConnection(connection) || githubBotConnectionIds.has(connection.id) || spekoConnectionIds.has(connection.id))
).map((connection) => connection.id));
const assignment = {
version: 1,