Share the external Runner lifecycle and add strict Muse contracts (#16444)

Add the external Runner foundation with distinct Dot/Grok V6, canonical home-mode V7 and Muse V8 contracts.

Keep generated schemas and profiles synchronized. Bound default question-answer command IDs in a disjoint namespace while preserving existing IDs and durable replay. Preserve canonical migration history and add normally generated, replay-safe Muse schema milestones with literal preview-history fixtures.

Refs #16444.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-10-16 13:46:50 -05:00
1 parent 4de6335c3b
commit 2cc7992369
95 files changed
+12594 -1600

No files matched your search

@@ -60,6 +60,7 @@ describe("instance settings service", () => {
enableAgentChat: false,
enablePublicMcp: false,
enableOpenAiDot: false,
enableMuse: false,
enableGrokBot: false,
enableCombinedInboxTasks: false,
enableChatConnectors: false,
+4
View File
@@ -311,6 +311,10 @@ describe("redaction", () => {
providerValue: input.nested.providerValue,
},
});
expect(sanitizeRecord({ schema: "paperclip.native-execution-input.v7", runtimeSchema: "paperclip.native-execution-input.v8" })).toEqual({
schema: "paperclip.native-execution-input.v7",
runtimeSchema: "paperclip.native-execution-input.v8",
});
expect(sanitizeRecord({ schema: "paperclip.native-execution-input.v4", runtimeSchema: "paperclip.native-model-envelope.v2" })).toEqual({
schema: "paperclip.native-execution-input.v4",
runtimeSchema: "paperclip.native-model-envelope.v2",
@@ -0,0 +1,17 @@
import type { Db } from "@paperclipai/db";
import { agents, heartbeatRuns, dotRunnerAssignments, externalAgentHolds, museRunnerAssignments } from "@paperclipai/db";
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
import { conflict } from "../../errors.js";
export type ExternalAdmissionTransaction = Parameters<Parameters<Db["transaction"]>[0]>[0];
/** Acquire only after the queue's issue/wake/run locks. Generation/stop operations acquire this guard before binding locks. */
export async function withExternalAdmissionGuard<T>(tx: ExternalAdmissionTransaction, companyId: string, agentId: string, action: () => Promise<T>): Promise<T> {
await tx.execute(sql`select pg_advisory_xact_lock(hashtextextended(${`paperclip:external-admission:${companyId}:${agentId}`},0))`);
return action();
}
export async function assertNoExternalOverlap(tx: ExternalAdmissionTransaction, companyId: string, agentId: string): Promise<void> {
const [hold] = await tx.select({ id: externalAgentHolds.id }).from(externalAgentHolds).where(and(eq(externalAgentHolds.companyId,companyId),eq(externalAgentHolds.agentId,agentId),isNull(externalAgentHolds.releasedAt),or(eq(externalAgentHolds.workerUnknown,true),eq(externalAgentHolds.nativeEffectsUnknown,true)))).limit(1);
const [muse] = await tx.select({ id:museRunnerAssignments.id }).from(museRunnerAssignments).where(and(eq(museRunnerAssignments.companyId,companyId),eq(museRunnerAssignments.agentId,agentId),inArray(museRunnerAssignments.status,["offered","claimed","accepted"]))).limit(1);
const [dot] = await tx.select({ id:dotRunnerAssignments.id }).from(dotRunnerAssignments).where(and(eq(dotRunnerAssignments.companyId,companyId),eq(dotRunnerAssignments.agentId,agentId),inArray(dotRunnerAssignments.status,["offered","accepted"]))).limit(1);
const [claimed] = await tx.select({ id: heartbeatRuns.id }).from(heartbeatRuns).where(and(eq(heartbeatRuns.companyId,companyId),eq(heartbeatRuns.agentId,agentId),eq(heartbeatRuns.status,"running"),or(inArray(heartbeatRuns.driverKind,["openai_dot_mcp","grok_bot_mcp","muse_external"]),sql`exists (select 1 from ${agents} where ${agents.id} = ${agentId}::uuid and ${agents.companyId} = ${companyId}::uuid and ${agents.adapterConfig}->>'provider' in ('openai_dot','grok_bot','muse'))`))).limit(1);
if (hold || muse || dot || claimed) throw conflict("External work is live or unresolved for this agent. Reconcile the exact assignment before starting another run.", { code: "external_agent_overlap" });
}
+1
View File
@@ -91,6 +91,7 @@ export const PAPERCLIP_PUBLIC_SCHEMA_IDS = new Set([
"paperclip.native-execution-input.v4",
"paperclip.native-execution-input.v5",
"paperclip.native-execution-input.v7",
"paperclip.native-execution-input.v8",
"paperclip.native-finalization.v1",
"paperclip.native-finalization.v2",
"paperclip.native-harness-backup-stamp.v1",
@@ -130,6 +130,34 @@ describe.skipIf(!support.supported)("heartbeat environment selection boundary",
expect(result.requestedReusableExecutionWorkspaceConfig).toBeNull();
});
it.each(["muse", "openai_dot", "grok_bot"] as const)("keeps %s managed placement without adopting the task's selected workspace", async provider => {
const { input, issue, workspace } = await fixture();
const source = await workspace();
const [sandbox] = await db.insert(environments).values({ name: `External controller ${provider}`, driver: "sandbox", status: "active", config: { provider: "fake" } }).returning();
input.agent.adapterType = "paperclip_runner";
input.agent.adapterConfig = { provider };
input.agent.defaultEnvironmentId = sandbox.id;
input.task.issueRef!.executionWorkspaceId = source.id;
input.task.issueRef!.projectWorkspaceId = randomUUID();
input.task.boundSourceWorkspace = source;
input.task.workspaceProjectContext = { hasWorkspace: true };
input.task.hasSelectedWorkspaceSource = true;
input.task.taskWorkspaceIntent = { version: 1, source: "explicit", selection: { kind: "task_directory" } };
await db.update(issues).set({ originKind: "chat_channel" }).where(eq(issues.id, issue.id));
const read = vi.spyOn(input.services.executionWorkspacesSvc, "getStoredById").mockRejectedValue(new Error("Task source must remain inaccessible"));
const select = vi.spyOn(input.services.executionWorkspacesSvc, "validateSelection").mockRejectedValue(new Error("Task selection must remain inaccessible"));
try {
const result = await selectHeartbeatEnvironment(db, input);
expect(result).toMatchObject({ isDotRun: true, selectedEnvironmentId: sandbox.id,
existingExecutionWorkspace: null, reusableExistingExecutionWorkspace: null,
nativeChatWorkspaceScope: null, useIsolatedTaskDirectory: false, explicitTaskDirectory: false });
expect(read).not.toHaveBeenCalled();
expect(select).not.toHaveBeenCalled();
expect(input.services.findSharedWorkspaceHolder).not.toHaveBeenCalled();
expect(input.task.issueRef!.executionWorkspaceId).toBe(source.id);
} finally { read.mockRestore(); select.mockRestore(); }
});
it("uses the low-trust task sandbox even when workspace feature gates hide issue settings", async () => {
const { input, issue } = await fixture();
const [sandbox] = await db.insert(environments).values({ name: "Task sandbox", driver: "sandbox", status: "active", config: { provider: "fake" } }).returning();
@@ -1,3 +1,4 @@
import { heartbeatExternalProvider, nativeExecutionUsesTaskWorkspace } from "./external-provider.js";
import { heartbeatRuns, issues, type Db, type agents } from "@paperclipai/db";
import type { ExecutionWorkspace, IssueExecutionWorkspaceSettings, ProjectExecutionWorkspacePolicy } from "@paperclipai/shared";
import { parseObject } from "../../adapters/utils.js";
@@ -97,11 +98,11 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
persistedRunnerProfile.nativeExecutionInput,
)
: null;
const isDotRun = persistedNativeExecutionInput?.provider.kind === "openai_dot"
|| (!persistedNativeExecutionInput && agent.adapterType === "paperclip_runner" && parseObject(agent.adapterConfig).provider === "openai_dot");
// This retained name covers both external bridges; neither adopts task files.
const isDotRun = heartbeatExternalProvider({ run, agent }) !== null;
const persistedNativeExecutionWorkspaceId =
persistedNativeExecutionInput?.binding.executionWorkspaceId ?? null;
const requestedExecutionWorkspaceId =
const requestedExecutionWorkspaceId = isDotRun ? null :
persistedNativeExecutionWorkspaceId ??
readNonEmptyString(issueRef?.executionWorkspaceId);
const existingExecutionWorkspace = requestedExecutionWorkspaceId
@@ -115,7 +116,7 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
const workspaceReuseRequest =
resolveExecutionWorkspaceReuseRequestForIssue({
issueExecutionWorkspaceId: requestedExecutionWorkspaceId,
issueExecutionWorkspacePreference: nativeRecoveryExecutionWorkspaceId
issueExecutionWorkspacePreference: isDotRun ? null : nativeRecoveryExecutionWorkspaceId
? "reuse_existing"
: (issueRef?.executionWorkspacePreference ?? null),
existingExecutionWorkspaceStatus:
@@ -129,7 +130,7 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
: null;
const workspaceAuthorizationActor = { type: "agent" as const, agentId: agent.id, companyId: agent.companyId,
source: "agent_jwt" as const, runId: run.id, onBehalfOfUserId: responsibleUserId === "local-board" ? null : responsibleUserId };
if (!persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId) {
if (!isDotRun && !persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId) {
const workspaceRequiringAccess = reusableExistingExecutionWorkspace ?? boundSourceWorkspace;
if (workspaceRequiringAccess) {
await assertTaskWorkspaceAccess(db, workspaceAuthorizationActor, agent.companyId, workspaceRequiringAccess.id, { write: true, issueId });
@@ -253,13 +254,13 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
: selectedEnvironmentId
? await environmentsSvc.getById(selectedEnvironmentId)
: null;
const nativeChatWorkspaceScope = await findNativeChatWorkspaceScope(db, {
const nativeChatWorkspaceScope = isDotRun ? null : await findNativeChatWorkspaceScope(db, {
adapterType: agent.adapterType,
environmentDriver: selectedEnvironmentForConfig?.driver ?? null,
companyId: agent.companyId,
agentId: agent.id,
issueId,
admittedCwd: persistedNativeExecutionInput && persistedNativeExecutionInput.schema !== "paperclip.native-execution-input.v6"
admittedCwd: nativeExecutionUsesTaskWorkspace(persistedNativeExecutionInput)
? persistedNativeExecutionInput.workspace.cwd : null,
});
let nativeChatExpectedCwd = nativeChatWorkspaceScope
@@ -271,8 +272,7 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
: null;
if (
nativeChatWorkspaceScope &&
persistedNativeExecutionInput &&
persistedNativeExecutionInput.schema !== "paperclip.native-execution-input.v6" &&
nativeExecutionUsesTaskWorkspace(persistedNativeExecutionInput) &&
!nativeChatWorkspaceMatches({
scope: nativeChatWorkspaceScope,
expectedCwd: nativeChatExpectedCwd,
@@ -352,7 +352,7 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
// project or issue settings request serialization. Sandbox protection still uses
// the existing holder staleness and workspace_busy retry ladder.
if (
issueRef?.projectWorkspaceId &&
!isDotRun && issueRef?.projectWorkspaceId &&
effectiveExecutionWorkspaceMode === "shared_workspace"
) {
const workspaceHolder = await findSharedWorkspaceHolder({
@@ -433,14 +433,14 @@ export async function selectHeartbeatEnvironment(db: Db, input: HeartbeatEnviron
);
}
}
if (taskWorkspaceIntent && !boundSourceWorkspace && !nativeRecoveryExecutionWorkspaceId) {
if (!isDotRun && taskWorkspaceIntent && !boundSourceWorkspace && !nativeRecoveryExecutionWorkspaceId) {
await executionWorkspacesSvc.validateSelection({ companyId: agent.companyId,
actor: { type: "agent", agentId: agent.id, companyId: agent.companyId, runId: run.id, source: "agent_jwt", onBehalfOfUserId: responsibleUserId === "local-board" ? null : responsibleUserId },
selection: taskWorkspaceIntent.selection, issueId,
preserveImplicitDefaultSettings: ["project", "task_default"].includes(taskWorkspaceIntent.source) });
}
const explicitTaskDirectory = taskWorkspaceIntent?.selection.kind === "task_directory";
const useIsolatedTaskDirectory = issueRef !== null && shouldUseIsolatedTaskDirectory({
const explicitTaskDirectory = !isDotRun && taskWorkspaceIntent?.selection.kind === "task_directory";
const useIsolatedTaskDirectory = !isDotRun && issueRef !== null && shouldUseIsolatedTaskDirectory({
trustPreset: trustPreset.kind,
environmentDriver: selectedEnvironmentForConfig?.driver ?? null,
mode: requestedExecutionWorkspaceMode,
@@ -0,0 +1,44 @@
import { randomUUID } from "node:crypto";
import { describe, expect, it } from "vitest";
import { buildNativeExecutionInput } from "../native-runtime/native-execution-input.js";
import { nativeRuntimeContextFixture } from "../native-runtime/runtime-context.test-fixture.js";
import { heartbeatExternalProvider, nativeRuntimeHasInstructionFilesystem, nativeExecutionUsesTaskWorkspace } from "./external-provider.js";
function execution(provider: "muse" | "openai_dot" | "grok_bot" | "codex") {
const companyId = randomUUID(), agentId = randomUUID();
const binding = { companyId, agentId, bindingId: randomUUID(), bindingGeneration: 1,
acceptByUnixMs: Date.now() + 60_000, expiresAtUnixMs: Date.now() + 600_000 };
return buildNativeExecutionInput({ companyId, agentId, runId: randomUUID(), provider,
...(provider === "muse" ? { museBinding: binding } : provider === "openai_dot" || provider === "grok_bot" ? { dotBinding: binding } : {}),
issue: { id: randomUUID(), identifier: "EXT-1", title: "Bound task", description: null, workMode: "standard" },
taskPrompt: "Complete the task", normalizedSessionId: randomUUID(),
workspace: { id: randomUUID(), cwd: "/controller", repoUrl: null, repoRef: null, branchName: null },
completionContract: { id: randomUUID(), sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.run-result.v1",
contract: { revision: "1", objective: "Complete the task", criteria: [{ id: "objective", requirement: "Complete the task" }] } },
runtimeContext: nativeRuntimeContextFixture(),
});
}
describe("shared external heartbeat classification", () => {
it.each(["muse", "openai_dot", "grok_bot"] as const)("keeps %s external on fresh admission and after a configured provider change", provider => {
const agent = { adapterType: "paperclip_runner", adapterConfig: { provider } };
expect(heartbeatExternalProvider({ agent, run: { runtimeMode: "legacy", runnerProfileJson: null } })).toBe(provider);
const admitted = execution(provider);
expect(admitted.schema).toBe(provider === "muse" ? "paperclip.native-execution-input.v8" : "paperclip.native-execution-input.v6");
expect(admitted.provider.kind).toBe(provider);
expect(admitted.session.driverKind).toBe(provider === "muse" ? "muse_external" : provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp");
expect(heartbeatExternalProvider({ agent: { ...agent, adapterConfig: { provider: "codex" } },
run: { runtimeMode: "native", runnerProfileJson: { nativeExecutionInput: admitted } } })).toBe(provider);
expect(nativeRuntimeHasInstructionFilesystem(admitted.session.driverKind)).toBe(false);
expect(nativeExecutionUsesTaskWorkspace(admitted)).toBe(false);
});
it("does not turn an admitted local provider into an external bridge after configuration changes", () => {
expect(heartbeatExternalProvider({ agent: { adapterType: "paperclip_runner", adapterConfig: { provider: "muse" } },
run: { runtimeMode: "native", runnerProfileJson: { nativeExecutionInput: execution("codex") } } })).toBeNull();
expect(nativeRuntimeHasInstructionFilesystem("codex_app_server")).toBe(true);
expect(nativeExecutionUsesTaskWorkspace(execution("codex"))).toBe(true);
expect(heartbeatExternalProvider({ agent: { adapterType: "process", adapterConfig: { provider: "muse" } },
run: { runtimeMode: "legacy", runnerProfileJson: null } })).toBeNull();
});
});
@@ -0,0 +1,28 @@
import { parseObject } from "../../adapters/utils.js";
import { parseNativeExecutionInput, type NativeExecutionInput } from "../../vendor/paperclip-runner/index.js";
/** Provider placement is distinct from access to a task's workspace. */
export function heartbeatExternalProvider(input: {
run: { runtimeMode: string | null; runnerProfileJson: unknown };
agent: { adapterType: string; adapterConfig: unknown };
}): "openai_dot" | "grok_bot" | "muse" | null {
const persisted = input.run.runtimeMode === "native"
? parseObject(input.run.runnerProfileJson).nativeExecutionInput : undefined;
const provider = persisted !== undefined
? parseNativeExecutionInput(persisted).provider.kind
: input.agent.adapterType === "paperclip_runner" ? parseObject(input.agent.adapterConfig).provider : null;
return provider === "openai_dot" || provider === "grok_bot" || provider === "muse" ? provider : null;
}
export function nativeRuntimeHasInstructionFilesystem(backend: string): boolean {
return !["claude_managed_agents_api", "aws_agentcore_harness_api", "openai_dot_mcp", "grok_bot_mcp", "muse_external"].includes(backend);
}
type TaskWorkspaceExecution = Exclude<NativeExecutionInput, {
schema: "paperclip.native-execution-input.v6" | "paperclip.native-execution-input.v8";
}>;
export function nativeExecutionUsesTaskWorkspace(execution: NativeExecutionInput | null): execution is TaskWorkspaceExecution {
return execution !== null && execution.schema !== "paperclip.native-execution-input.v6"
&& execution.schema !== "paperclip.native-execution-input.v8";
}
@@ -19,6 +19,8 @@ import { workspaceOperationService } from "../workspace-operations.js";
import { nativeWorkspaceSyncInternals, prepareNativeWorkspaceSync } from "../native-runtime/native-workspace-sync.js";
import { createHeartbeatWorkspaceResolver, prepareProjectRepositoryWorkspaces, resolveExecutionWorkspaceReuseRequestForIssue } from "./workspaces.js";
import { prepareHeartbeatWorkspace, type HeartbeatWorkspacePreparationInput } from "./workspace-preparation.js";
import { buildNativeExecutionInput } from "../native-runtime/native-execution-input.js";
import { nativeRuntimeContextFixture } from "../native-runtime/runtime-context.test-fixture.js";
import * as managedRepositoryCheckout from "../managed-repository-checkout.js";
import { isolatedRemoteSourcePath } from "../isolated-remote-workspace-source.js";
import { cleanupExecutionWorkspaceArtifacts } from "../workspace-runtime.js";
@@ -105,6 +107,47 @@ describe.skipIf(!support.supported)("heartbeat workspace preparation boundary",
input.run = run;
}
it.each(["muse", "openai_dot", "grok_bot"] as const)("uses private %s controller storage without rebinding task files or preparing repositories", async provider => {
const { input, issue } = await fixture();
input.agent.adapterType = "paperclip_runner";
input.config.mergedConfig = { provider, cwd: "/task/source/must-not-be-read", workspaceStrategy: { type: "git_worktree" } };
input.reuse.isDotRun = true;
const resolve = vi.spyOn(input.services, "resolveWorkspaceForRun").mockRejectedValue(new Error("Task files must not be adopted"));
const bind = vi.spyOn(input.services.executionWorkspacesSvc, "bindTaskWorkspace").mockRejectedValue(new Error("Task binding must not change"));
const repos = vi.spyOn(input.services.executionWorkspacesSvc, "prepareTaskRepositoriesForAdmission").mockRejectedValue(new Error("Repositories must not be prepared"));
try {
const result = await prepareHeartbeatWorkspace(db, input);
expect(result.executionWorkspace.cwd).toContain(provider === "muse" ? "muse-controllers" : "dot-controllers");
expect(result.persistedExecutionWorkspace).toBeNull();
expect(result.projectRepositoryPaths).toEqual([]);
expect((await readIssue(issue.id)).executionWorkspaceId).toBeNull();
expect(resolve).not.toHaveBeenCalled();
expect(bind).not.toHaveBeenCalled();
expect(repos).not.toHaveBeenCalled();
const binding = { companyId: input.agent.companyId, agentId: input.agent.id, bindingId: input.run.id,
bindingGeneration: 1, acceptByUnixMs: Date.now() + 60_000, expiresAtUnixMs: Date.now() + 600_000 };
const admitted = buildNativeExecutionInput({ companyId: input.agent.companyId, agentId: input.agent.id,
runId: input.run.id, provider, ...(provider === "muse" ? { museBinding: binding } : { dotBinding: binding }),
issue: { id: issue.id, identifier: null, title: issue.title, description: null, workMode: "standard" },
taskPrompt: "Preserve the admitted controller", normalizedSessionId: input.run.id,
workspace: { id: input.run.id, cwd: result.executionWorkspace.cwd, repoUrl: null, repoRef: null, branchName: null },
completionContract: { id: input.run.id, sha256: `sha256:${"a".repeat(64)}`, schemaVersion: "paperclip.run-result.v1",
contract: { revision: "1", objective: "Preserve the controller", criteria: [{ id: "objective", requirement: "Preserve the original checkpoint path" }] } },
runtimeContext: nativeRuntimeContextFixture(),
});
input.reuse.persistedNativeExecutionInput = admitted;
input.run.runtimeMode = "native";
input.run.runnerProfileJson = { nativeExecutionInput: admitted };
input.config.mergedConfig = { provider: "codex" };
const recovered = await prepareHeartbeatWorkspace(db, input);
expect(recovered.executionWorkspace.cwd).toBe(result.executionWorkspace.cwd);
expect(recovered.persistedExecutionWorkspace).toBeNull();
expect(bind).not.toHaveBeenCalled();
expect(repos).not.toHaveBeenCalled();
} finally { resolve.mockRestore(); bind.mockRestore(); repos.mockRestore(); }
});
it("persists the workspace and issue binding before acquiring its environment", async () => {
const { input, source, issue, envOrchestrator } = await fixture();
const acquire = envOrchestrator.acquireForRun;
@@ -1,3 +1,4 @@
import { nativeExecutionUsesTaskWorkspace } from "./external-provider.js";
import { createHeartbeatPreparationTiming } from "./preparation-timing.js";
import {
resolveWorkspaceAfterLowTrustPreflight,
@@ -247,8 +248,8 @@ async function prepareHeartbeatWorkspaceLocked(db: Db, input: HeartbeatWorkspace
resolveWorkspace: async () => {
if (isDotRun) {
// This is private controller storage, never a provider filesystem.
// v6 projects workspace.access=none and cwd=null to Dot.
const cwd = path.resolve(resolvePaperclipInstanceRoot(), "runtime", "paperclip-runner", "dot-controllers", agent.companyId, run.id);
// External providers receive workspace.access=none and cwd=null.
const cwd = path.resolve(resolvePaperclipInstanceRoot(), "runtime", "paperclip-runner", (persistedNativeExecutionInput?.provider.kind ?? mergedConfig.provider) === "muse" ? "muse-controllers" : "dot-controllers", agent.companyId, run.id);
await fs.mkdir(cwd, { recursive: true, mode: 0o700 });
return { cwd, source: "agent_home" as const, projectId: null, workspaceId: null, repoUrl: null, repoRef: null,
workspaceHints: [], warnings: [], baseCwdFallback: false, materializationFailures: [], additionalWorkspaces: [], referencedProjectFailures: [] };
@@ -682,7 +683,7 @@ async function prepareHeartbeatWorkspaceLocked(db: Db, input: HeartbeatWorkspace
const resolvedProjectWorkspaceId =
resolvedWorkspaceReusePolicy.shouldRestoreExistingWorkspace && reusableExistingExecutionWorkspace?.strategyType === "git_worktree"
? reusableExistingExecutionWorkspace.projectWorkspaceId
: issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
: isDotRun ? null : issueRef?.projectWorkspaceId ?? resolvedWorkspace.workspaceId ?? null;
let persistedExecutionWorkspace: ExecutionWorkspace | null = null;
let issueExecutionWorkspaceIdForRun =
issueRef?.executionWorkspaceId ?? null;
@@ -703,7 +704,7 @@ async function prepareHeartbeatWorkspaceLocked(db: Db, input: HeartbeatWorkspace
const bindIssueToPersistedExecutionWorkspace = async (
workspace: ExecutionWorkspace | null,
) => {
if (!issueId || !workspace || nativeRecoveryExecutionWorkspaceId) {
if (isDotRun || !issueId || !workspace || nativeRecoveryExecutionWorkspaceId) {
return;
}
const nextIssueWorkspaceMode =
@@ -968,7 +969,7 @@ async function prepareHeartbeatWorkspaceLocked(db: Db, input: HeartbeatWorkspace
}
await bindIssueToPersistedExecutionWorkspace(persistedExecutionWorkspace);
const projectRepositoryPaths: string[] = [];
if (persistedExecutionWorkspace && issueId && !persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId) {
if (!isDotRun && persistedExecutionWorkspace && issueId && !persistedNativeExecutionInput && !nativeRecoveryExecutionWorkspaceId) {
const taskRepositories = await timeWorkspace("workspace_repository_admission", () => executionWorkspacesSvc.prepareTaskRepositoriesForAdmission({ companyId: agent.companyId,
issueId, workspaceId: persistedExecutionWorkspace!.id, cwd: executionWorkspace.cwd,
agentId: agent.id, runId: run.id, responsibleUserId }));
@@ -1178,7 +1179,7 @@ async function prepareHeartbeatWorkspaceLocked(db: Db, input: HeartbeatWorkspace
}
const cwd = reference
? await readNativeWorkspaceSyncLocalCwd({ companyId: agent.companyId, runId: run.id, workspaceId, reference })
: persistedNativeExecutionInput && persistedNativeExecutionInput.schema !== "paperclip.native-execution-input.v6"
: nativeExecutionUsesTaskWorkspace(persistedNativeExecutionInput)
&& executionTarget?.kind !== "remote"
? persistedNativeExecutionInput.workspace.cwd
: executionWorkspace.cwd;
+2
View File
@@ -235,6 +235,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableMcpAggregators: true,
enablePublicMcp: parsed.data.enablePublicMcp ?? false,
enableOpenAiDot: parsed.data.enableOpenAiDot ?? false,
enableMuse: parsed.data.enableMuse ?? false,
enableGrokBot: parsed.data.enableGrokBot ?? false,
enableChatConnectors: parsed.data.enableChatConnectors ?? false,
enableGitHubReviewBots: parsed.data.enableGitHubReviewBots ?? false,
@@ -285,6 +286,7 @@ export function normalizeExperimentalSettings(raw: unknown): InstanceExperimenta
enableMcpAggregators: true,
enablePublicMcp: false,
enableOpenAiDot: false,
enableMuse: false,
enableGrokBot: false,
enableChatConnectors: false,
enableGitHubReviewBots: false,
@@ -8,6 +8,8 @@ import type {
NativeCodexApprovalPolicy,
NativeExecutionInputV5,
NativeExecutionInputV6,
NativeExecutionInputV8,
MuseBindingSnapshot,
DotBindingSnapshot,
NativeExecutionInputV7,
NativeCompletionSource,
@@ -72,7 +74,8 @@ export interface BuildNativeExecutionInput {
branchName: string | null;
};
normalizedSessionId: string | null;
provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx" | "openai_dot" | "grok_bot";
provider?: "codex" | "opencode" | "claude_managed" | "aws_agentcore" | "acpx" | "openai_dot" | "grok_bot" | "muse";
museBinding?: MuseBindingSnapshot;
dotBinding?: DotBindingSnapshot;
acpxAgent?: NativeAcpxAgent;
homeMode?: "isolated" | "preconfigured";
@@ -110,10 +113,11 @@ export interface BuildNativeExecutionInput {
};
runtimeContext: NativeRuntimeContextSnapshot;
}
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider: "muse"; museBinding: MuseBindingSnapshot }): NativeExecutionInputV8;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider: "openai_dot" | "grok_bot"; dotBinding: DotBindingSnapshot }): NativeExecutionInputV6;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider?: Exclude<BuildNativeExecutionInput["provider"], "openai_dot" | "grok_bot"> }): NativeExecutionInputV5 | NativeExecutionInputV7;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 {
export function buildNativeExecutionInput(input: BuildNativeExecutionInput & { provider?: Exclude<BuildNativeExecutionInput["provider"], "openai_dot" | "grok_bot" | "muse"> }): NativeExecutionInputV5 | NativeExecutionInputV7;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 | NativeExecutionInputV8;
export function buildNativeExecutionInput(input: BuildNativeExecutionInput): NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 | NativeExecutionInputV8 {
if (input.issue.workMode !== "standard" && input.issue.workMode !== "planning" && input.issue.workMode !== "ask") {
throw new Error("native_execution_input_invalid: issue work mode must be standard, planning, or ask");
}
@@ -333,11 +337,15 @@ export function buildNativeExecutionInput(input: BuildNativeExecutionInput): Nat
credentialBindings: [],
runtimeContext: input.runtimeContext,
};
return parseNativeExecutionInput(["openai_dot", "grok_bot"].includes(input.provider ?? "") ? { ...prepared,
return parseNativeExecutionInput(input.provider === "muse" ? { ...prepared,
schema: "paperclip.native-execution-input.v8", provider: { kind: "muse", model: null, binding: input.museBinding, bridgeRevision: "muse-v1" },
workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
session: { ...prepared.session, driverKind: "muse_external", lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }
} : ["openai_dot", "grok_bot"].includes(input.provider ?? "") ? { ...prepared,
schema: "paperclip.native-execution-input.v6", provider: { kind: input.provider, model: null, binding: input.dotBinding },
workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null },
session: { ...prepared.session, driverKind: input.provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp", lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }
} : prepared) as NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7;
} : prepared) as NativeExecutionInputV5 | NativeExecutionInputV6 | NativeExecutionInputV7 | NativeExecutionInputV8;
}
@@ -1,7 +1,7 @@
import { randomUUID } from "node:crypto";
import { and, eq } from "drizzle-orm";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { activityLog, agents, companies, createDb, environmentLeases, environments, heartbeatRuns, issueRecoveryActions, issues, nativeRunFinalizations } from "@paperclipai/db";
import { activityLog, agents, companies, createDb, environmentLeases, environments, externalAgentHolds, heartbeatRuns, issueRecoveryActions, issues, nativeRunFinalizations } from "@paperclipai/db";
import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "../../__tests__/helpers/embedded-postgres.js";
import { getExecutionBlocker } from "../execution-blocker.js";
import { markExecutionReconciliation, validateExecutionReconciliation } from "../execution-recovery-resolution.js";
@@ -76,23 +76,27 @@ const support = externalUrl ? { supported: true } : await getEmbeddedPostgresTes
eq(activityLog.action, "issue.workspace_recovery_required")))).toHaveLength(1);
});
async function bindExternalInput(f: Awaited<ReturnType<typeof seed>>, provider: "grok_bot" | "openai_dot", change?: (input: { schema: string; binding: { runId: string; companyId: string; agentId: string }; workspace: { access: string } }) => void) {
async function bindExternalInput(f: Awaited<ReturnType<typeof seed>>, provider: "grok_bot" | "openai_dot" | "muse", change?: (input: { schema: string; binding: { runId: string; companyId: string; agentId: string; issueId: string }; provider: { kind: string; bridgeRevision?: string }; workspace: { access: string; cwd: string | null } }) => void) {
const run = await readRun(f.runId);
const input = { schema: "paperclip.native-execution-input.v6",
const input = { schema: provider === "muse" ? "paperclip.native-execution-input.v8" : "paperclip.native-execution-input.v6",
binding: { runId: f.runId, companyId: f.companyId, agentId: f.agentId, issueId: f.issueId },
provider: { kind: provider }, workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null } };
provider: { kind: provider, ...(provider === "muse" ? { bridgeRevision: "muse-v1" } : {}) }, workspace: { access: "none", cwd: null, repoUrl: null, repoRef: null, branchName: null } };
change?.(input);
const [updated] = await db.update(heartbeatRuns).set({ driverKind: provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp",
const [updated] = await db.update(heartbeatRuns).set({ driverKind: provider === "muse" ? "muse_external" : provider === "grok_bot" ? "grok_bot_mcp" : "openai_dot_mcp",
runnerProfileJson: { ...run.runnerProfileJson, nativeExecutionInput: input } }).where(eq(heartbeatRuns.id, f.runId)).returning();
return updated;
}
it.each(["grok_bot", "openai_dot"] as const)("does not invent file recovery for %s admitted without workspace access", async provider => {
it.each(["grok_bot", "openai_dot", "muse"] as const)("does not invent file recovery for %s admitted without workspace access", async provider => {
const f = await seed(), run = await bindExternalInput(f, provider);
const [hold] = provider === "muse" ? await db.insert(externalAgentHolds).values({ companyId: f.companyId, agentId: f.agentId,
provider: "muse", assignmentId: randomUUID(), bindingId: randomUUID(), bindingGeneration: 1, runId: f.runId,
workerUnknown: true, nativeEffectsUnknown: true }).returning() : [];
await preserveNativeFailedWorkspace(db, run);
expect((await readRun(f.runId)).resultJson).not.toHaveProperty("workspaceRestoreFailure");
expect(await readLease(f.lease.id)).toMatchObject({ status: "active", leasePolicy: "ephemeral" });
expect(await actions(f.issueId)).toHaveLength(0);
if (hold) expect((await db.select().from(externalAgentHolds).where(eq(externalAgentHolds.id, hold.id)))[0]).toEqual(hold);
});
it.each(["run", "company", "agent", "schema", "write_access"])("preserves file safeguards when no-workspace evidence has an invalid %s", async mismatch => {
@@ -109,6 +113,27 @@ const support = externalUrl ? { supported: true } : await getEmbeddedPostgresTes
expect(await readLease(f.lease.id)).toMatchObject({ leasePolicy: "retain_on_failure" });
});
it.each(["run", "company", "agent", "issue", "schema", "provider", "bridge", "missing_bridge", "driver", "write_access", "cwd"] as const)("retains file safeguards for a mismatched Muse %s contract", async mismatch => {
const f = await seed();
await bindExternalInput(f, "muse", input => {
if (mismatch === "run") input.binding.runId = randomUUID();
if (mismatch === "company") input.binding.companyId = randomUUID();
if (mismatch === "agent") input.binding.agentId = randomUUID();
if (mismatch === "issue") input.binding.issueId = randomUUID();
if (mismatch === "schema") input.schema = "paperclip.native-execution-input.v6";
if (mismatch === "provider") input.provider.kind = "grok_bot";
if (mismatch === "bridge") input.provider.bridgeRevision = "unknown";
if (mismatch === "missing_bridge") delete input.provider.bridgeRevision;
if (mismatch === "write_access") input.workspace.access = "read_write";
if (mismatch === "cwd") input.workspace.cwd = "/untrusted-task-workspace";
});
if (mismatch === "driver") await db.update(heartbeatRuns).set({ driverKind: "openai_dot_mcp" }).where(eq(heartbeatRuns.id, f.runId));
await preserveNativeFailedWorkspace(db, await readRun(f.runId));
expect((await readRun(f.runId)).resultJson).toHaveProperty("workspaceRestoreFailure", "restore_failed");
expect(await readLease(f.lease.id)).toMatchObject({ leasePolicy: "retain_on_failure" });
expect(await actions(f.issueId)).toHaveLength(1);
});
it.each(["grok_bot", "openai_dot"] as const)("keeps historical %s inspection holds with an available recovery path", async provider => {
const f = await seed();
// Reproduce a hold persisted by an older cancellation path, then attach its admitted contract.
@@ -1689,6 +1689,7 @@ export function nativeSessionWorkspaceScope(execution: { binding: Pick<NativeExe
function nativeProviderSessionScope(execution: NativeExecutionInput) {
switch (execution.provider.kind) {
case "muse":
case "grok_bot":
case "openai_dot": return { kind: execution.provider.kind, bindingId: execution.provider.binding.bindingId, generation: execution.provider.binding.bindingGeneration };
case "claude_managed":
@@ -5549,6 +5550,7 @@ function canonicalJson(value: unknown): string {
function runnerProviderStateFilename(execution: NativeExecutionInput): string {
switch (execution.provider.kind) {
case "muse": return "muse-provider-state.json";
case "grok_bot":
case "openai_dot":
return "dot-provider-state.json";
@@ -5584,9 +5586,10 @@ export function providerSessionIdentityFromDurableProviderState(input: {
providerSessionIdentity: null,
});
switch (input.execution.provider.kind) {
case "muse":
case "grok_bot":
case "openai_dot":
// Dot checkpoints recover the bridge only; there is no native provider thread.
// External checkpoints recover the bridge only; there is no native provider thread.
return emptyIdentity();
case "acpx": {
const descriptor = record(state.descriptor);
@@ -8658,7 +8661,7 @@ async function executePaperclipNativeSessionWithinScope(
: null;
}
const runnerExecution: NativeExecutionInput =
input.execution.schema !== "paperclip.native-execution-input.v6" && input.useRunnerd && input.runnerExecutionTarget?.kind === "remote"
input.execution.schema !== "paperclip.native-execution-input.v6" && input.execution.schema !== "paperclip.native-execution-input.v8" && input.useRunnerd && input.runnerExecutionTarget?.kind === "remote"
? {
...input.execution,
workspace: {
@@ -11431,6 +11434,9 @@ async function createRunnerdBackendWithinSessionClaim(
retainedTransition?: VerifiedWarmTransitionBinding,
): Promise<NativeSessionBackend & { bindManagedSession(session: NativeSession): NativeSession }> {
const timeController = createRunPreparationTiming(input.execution.binding.runId, "native.controller");
if (input.execution.schema === "paperclip.native-execution-input.v8") {
throw new Error("paperclip_runner_provider_unsupported");
}
let recoveryPending = retainedTransition !== undefined;
const target = input.runnerExecutionTarget ?? { kind: "local" as const };
const remoteTarget = target.kind === "remote" ? target : null;
@@ -2,7 +2,7 @@ import { connectorToolDefinitionsForSkills } from "../connector-runtime.js";
import * as cloudIdentity from "../cloud-runtime-identity.js";
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { randomUUID } from "node:crypto";
import { upsertIssueDocumentSchema } from "@paperclipai/shared";
import { LOW_TRUST_REVIEW_PRESET, upsertIssueDocumentSchema } from "@paperclipai/shared";
import { and, eq, inArray } from "drizzle-orm";
import {
activityLog,
@@ -24,9 +24,11 @@ import { initializeRunIdentity, reserveSteeredIdentity, reconcileSteeredIdentity
import { documentService } from "../documents.js";
import { issueService } from "../issues.js";
import { PaperclipRunnerToolAuthority } from "./paperclip-runner-tool-authority.js";
import { PaperclipRunnerSemanticAuthority } from "./runner-semantic-authority.js";
import { createAssignedMcpTools } from "./assigned-mcp-tools.js";
import type { ToolGatewayService } from "../tool-gateway.js";
import { READ_CURRENT_WAKE_COMMENTS_TOOL_NAME } from "./current-wake-comments.js";
import { buildLowTrustSourceTrust, buildPromotedSourceTrust, LOW_TRUST_QUARANTINED_BODY } from "../source-trust.js";
import { CAPABILITY_SEMANTIC_TOOL_CATALOG, runnerCodexDynamicToolsFit } from "../../vendor/paperclip-runner/index.js";
describe("PaperclipRunnerToolAuthority", () => {
@@ -196,6 +198,109 @@ describe("PaperclipRunnerToolAuthority", () => {
).rejects.toThrow("paperclip_runner_tool_not_advertised");
});
it("preserves platform receipt provenance in both mixed task history readers", async () => {
const at = new Date("2026-10-10T12:00:00Z");
const commentIds = Array.from({ length: 4 }, () => randomUUID());
const requestId = randomUUID();
await db.insert(issueComments).values([
{ id: commentIds[0], companyId, issueId, authorUserId: "board-user", authorType: "user",
body: "Check the configuration.", createdAt: at },
{ id: commentIds[1], companyId, issueId, authorAgentId: agentId, authorType: "agent",
origin: "fast_response", fastResponseRequestId: requestId, body: "I will check the configuration.",
createdAt: new Date(at.getTime() + 1000) },
{ id: commentIds[2], companyId, issueId, authorAgentId: agentId, authorType: "agent",
body: "Verified the configuration: two settings need updates.", createdAt: new Date(at.getTime() + 2000) },
{ id: commentIds[3], companyId, issueId, authorAgentId: agentId, authorType: "agent",
origin: "fast_response", body: "Deleted acknowledgement", createdAt: new Date(at.getTime() + 3000),
deletedAt: new Date(at.getTime() + 4000) },
]);
try {
const tool = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
const semantic = new PaperclipRunnerSemanticAuthority(db, { companyId, agentId, issueId, runId });
const histories = [
async (limit: number) => tool.execute({ tool: "get_task_history", callId: `mixed-tool-history-${limit}`, arguments: { limit } }),
async (limit: number) => {
const result = await semantic.dispatch({ callId: `mixed-semantic-history-${limit}`, operationId: "get_task_history", input: { limit },
correlation: { runId, normalizedSessionId: randomUUID(), turnId: "history", itemId: "history" } });
expect(result).toMatchObject({ ok: true });
return result.ok ? result.value : null;
},
];
const expected = [
{ id: commentIds[0], authorAgentId: null, authorUserId: "board-user", origin: "comment", body: "Check the configuration." },
{ id: commentIds[1], authorAgentId: agentId, authorUserId: null, origin: "fast_response",
body: "[Paperclip acknowledgement; not agent work] I will check the configuration." },
{ id: commentIds[2], authorAgentId: agentId, authorUserId: null, origin: "comment",
body: "Verified the configuration: two settings need updates." },
];
for (const history of histories) {
expect(await history(3)).toMatchObject({ comments: expected });
expect(await history(2)).toMatchObject({ comments: expected.slice(1) });
}
expect((await db.select().from(issueComments).where(eq(issueComments.id, commentIds[1])))[0])
.toMatchObject({ id: commentIds[1], origin: "fast_response", fastResponseRequestId: requestId,
authorAgentId: agentId, body: "I will check the configuration." });
} finally {
await db.delete(issueComments).where(inArray(issueComments.id, commentIds));
}
});
it.each([false, true])("legacy history preserves source trust for a current reader run (low trust: %s)", async (lowTrustReader) => {
const commentIds = Array.from({ length: 4 }, () => randomUUID());
const [originalRun] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.id, runId));
const [originalIssue] = await db.select().from(issues).where(eq(issues.id, issueId));
const quarantine = buildLowTrustSourceTrust({ issueId, runId, agentId });
const promotion = buildPromotedSourceTrust({
sourceIssueId: issueId, sourceArtifactKind: "comment", sourceArtifactId: commentIds[2],
promotedByActorType: "user", promotedByActorId: "board-user",
});
const at = new Date("2026-10-13T12:00:00Z");
await db.insert(issueComments).values([
{ id: commentIds[0], companyId, issueId, authorAgentId: agentId, authorType: "agent",
body: "QUARANTINED legacy history canary", sourceTrust: quarantine, createdAt: at },
{ id: commentIds[1], companyId, issueId, authorAgentId: agentId, authorType: "agent",
origin: "fast_response", body: "QUARANTINED acknowledgement canary", sourceTrust: quarantine,
createdAt: new Date(at.getTime() + 1000) },
{ id: commentIds[2], companyId, issueId, authorAgentId: agentId, authorType: "agent",
body: "Reviewed and promoted summary", sourceTrust: promotion, createdAt: new Date(at.getTime() + 2000) },
{ id: commentIds[3], companyId, issueId, authorUserId: "board-user", authorType: "user",
body: "Trusted coordination message", createdAt: new Date(at.getTime() + 3000) },
]);
try {
if (lowTrustReader) {
const executionPolicy = { authorizationPolicy: { trustBoundary: {
mode: LOW_TRUST_REVIEW_PRESET, companyId, rootIssueId: issueId,
} } };
await db.update(issues).set({ executionPolicy }).where(eq(issues.id, issueId));
await db.update(heartbeatRuns).set({ contextSnapshot: { issueId, executionPolicy } }).where(eq(heartbeatRuns.id, runId));
// Relaxing the live task must not relabel the already-admitted reader run.
await db.update(issues).set({ executionPolicy: null }).where(eq(issues.id, issueId));
}
const semantic = new PaperclipRunnerSemanticAuthority(db, { companyId, agentId, issueId, runId });
const result = await semantic.dispatch({
callId: `legacy-trust-history-${lowTrustReader}`, operationId: "get_task_history", input: { limit: 4 },
correlation: { runId, normalizedSessionId: randomUUID(), turnId: "history", itemId: "history" },
});
expect(result).toMatchObject({ ok: true });
expect(result.ok ? result.value : null).toMatchObject({ comments: [
{ id: commentIds[0], sourceTrust: quarantine,
body: lowTrustReader ? "QUARANTINED legacy history canary" : LOW_TRUST_QUARANTINED_BODY },
{ id: commentIds[1], origin: "fast_response", sourceTrust: quarantine,
body: `[Paperclip acknowledgement; not agent work] ${lowTrustReader ? "QUARANTINED acknowledgement canary" : LOW_TRUST_QUARANTINED_BODY}` },
{ id: commentIds[2], sourceTrust: promotion, body: "Reviewed and promoted summary" },
{ id: commentIds[3], sourceTrust: null, body: "Trusted coordination message" },
] });
if (!lowTrustReader) expect(JSON.stringify(result)).not.toContain("QUARANTINED legacy history canary");
if (!lowTrustReader) expect(JSON.stringify(result)).not.toContain("QUARANTINED acknowledgement canary");
expect((await db.select().from(issueComments).where(eq(issueComments.id, commentIds[0])))[0])
.toMatchObject({ body: "QUARANTINED legacy history canary", sourceTrust: quarantine });
} finally {
await db.update(heartbeatRuns).set({ contextSnapshot: originalRun!.contextSnapshot }).where(eq(heartbeatRuns.id, runId));
await db.update(issues).set({ executionPolicy: originalIssue!.executionPolicy }).where(eq(issues.id, issueId));
await db.delete(issueComments).where(inArray(issueComments.id, commentIds));
}
});
it("returns public task URLs from the current claimed origin in context and search results", async () => {
const origin = vi.spyOn(cloudIdentity, "runtimeCanonicalOrigin").mockReturnValue("https://board.example");
const authority = new PaperclipRunnerToolAuthority(db, { companyId, agentId, issueId, runId });
@@ -10,7 +10,7 @@ import { isConversation } from "../agent-conversations.js";
import { setIssueTitle } from "../issue-title.js";
import { externalObjectService } from "../external-objects.js";
import { instanceSettingsService } from "../instance-settings.js";
import { setIssueTitleSchema } from "@paperclipai/shared";
import { fastResponseHistoryBody, setIssueTitleSchema } from "@paperclipai/shared";
import { authorizeInstructionCommit } from "../agent-instruction-authorization.js";
import { executeAgentInstructionTool } from "./agent-instruction-tools.js";
import { createReadStream } from "node:fs";
@@ -625,6 +625,7 @@ export class PaperclipRunnerToolAuthority {
const comments = await this.db.select({
id: issueComments.id,
body: issueComments.body,
origin: issueComments.origin,
authorAgentId: issueComments.authorAgentId,
authorUserId: issueComments.authorUserId,
createdAt: issueComments.createdAt,
@@ -636,7 +637,7 @@ export class PaperclipRunnerToolAuthority {
))
.orderBy(desc(issueComments.createdAt))
.limit(limit);
return { comments: comments.reverse() };
return { comments: comments.reverse().map(comment => ({ ...comment, body: fastResponseHistoryBody(comment) })) };
}
case "search_tasks": {
const tasks = await issueService(this.db).list(this.binding.companyId, { readCondition: await issueReadSqlCondition(this.db, this.#privacyActor(context.run)) });
@@ -1,5 +1,11 @@
import { fastResponseHistoryBody } from "@paperclipai/shared";
import { readTaskQuestionContext } from "../issue-question-context.js";
import { isConversation } from "../agent-conversations.js";
import {
isLowTrustQuarantined,
resolveActorSourceTrustForIssue,
sanitizeQuarantinedCommentForHigherTrust,
} from "../source-trust.js";
import { and, desc, eq, isNull } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
@@ -155,6 +161,8 @@ export class PaperclipRunnerSemanticAuthority {
.select({
id: issueComments.id,
body: issueComments.body,
origin: issueComments.origin,
sourceTrust: issueComments.sourceTrust,
authorAgentId: issueComments.authorAgentId,
authorUserId: issueComments.authorUserId,
createdAt: issueComments.createdAt,
@@ -169,8 +177,19 @@ export class PaperclipRunnerSemanticAuthority {
)
.orderBy(desc(issueComments.createdAt))
.limit(boundedLimit(input.limit));
const readerSourceTrust = await resolveActorSourceTrustForIssue({
db: this.#db,
issue: context.issue,
actor: { actorType: "agent", actorId: context.agent.id, agentId: context.agent.id, runId: context.run.id },
});
const comments = rows.reverse().map(row => {
const comment = isLowTrustQuarantined(readerSourceTrust)
? row
: sanitizeQuarantinedCommentForHigherTrust(row);
return { ...comment, body: fastResponseHistoryBody(comment) };
});
return {
value: jsonValue({ comments: rows.reverse() }),
value: jsonValue({ comments }),
references: [{ kind: "task", id: context.issue.id }],
};
}
@@ -10,12 +10,14 @@ export const LEGACY_WORKSPACE_RECOVERY_SCHEMA = "paperclip.workspace-restore-rec
export function nativeRunHasNoWorkspaceAccess(run: Pick<typeof heartbeatRuns.$inferSelect,
"id" | "companyId" | "agentId" | "issueId" | "nativeIssueId" | "runtimeMode" | "driverKind" | "runnerProfileJson">): boolean {
const input = run.runnerProfileJson?.nativeExecutionInput as Record<string, unknown> | undefined;
if (run.runtimeMode !== "native" || input?.schema !== "paperclip.native-execution-input.v6") return false;
if (run.runtimeMode !== "native" || !input || (input.schema !== "paperclip.native-execution-input.v6" && input.schema !== "paperclip.native-execution-input.v8")) return false;
const binding = input.binding as Record<string, unknown> | undefined;
const workspace = input.workspace as Record<string, unknown> | undefined;
const provider = input.provider as Record<string, unknown> | undefined;
const driver = provider?.kind === "grok_bot" ? "grok_bot_mcp"
: provider?.kind === "openai_dot" ? "openai_dot_mcp" : null;
const driver = input?.schema === "paperclip.native-execution-input.v8"
? provider?.kind === "muse" && provider.bridgeRevision === "muse-v1" ? "muse_external" : null
: provider?.kind === "grok_bot" ? "grok_bot_mcp"
: provider?.kind === "openai_dot" ? "openai_dot_mcp" : null;
return driver !== null && run.driverKind === driver && workspace?.access === "none" && workspace.cwd === null
&& binding?.runId === run.id && binding.companyId === run.companyId && binding.agentId === run.agentId
&& binding.issueId === (run.nativeIssueId ?? run.issueId);
+3
View File
@@ -36,6 +36,8 @@ export type {
NativeExecutionInputV4,
NativeExecutionInputV5,
NativeExecutionInputV6,
NativeExecutionInputV8,
MuseBindingSnapshot,
DotBindingSnapshot,
ExternalProviderPort,
ExternalProviderOperation,
@@ -158,6 +160,7 @@ export const bundledRemoteProviderPackManifestPath = runner.bundledRemoteProvide
export const bundledRemoteRunnerBinary = runner.bundledRemoteRunnerBinary;
export const externalOperationDigest = runner.externalOperationDigest;
export const digestPaperclipSemanticContent = runner.digestPaperclipSemanticContent;
export const CONFIGURED_ENVIRONMENT_KEYS = runner.CONFIGURED_ENVIRONMENT_KEYS;
export const GENERATED_RUNTIME_ENVIRONMENT_KEYS = runner.GENERATED_RUNTIME_ENVIRONMENT_KEYS;
export const configuredEnvironmentProjection = runner.configuredEnvironmentProjection;