mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-07 16:35:48 +01:00
merge: preserve Claude behavior with canonical task-workspace source
Retain both scoped Storybook reviews and source lists, regenerate the index, preserve Claude acceptance docs, and refresh only ignored shared/SDK JavaScript declarations. UI/server types and189 creation/form controls pass. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
cf1fa2e982
240 files changed
+192172
-1485
No files matched your search
@@ -266,6 +266,8 @@ describe("native external chat workspace boundary", () => {
|
||||
path.join(instanceRoot, "workspaces", agentId),
|
||||
]) {
|
||||
const oldInput = { ...execution, workspace: { cwd } };
|
||||
const admittedScope = { ...scopeA, taskRoot: cwd };
|
||||
expect(nativeChatWorkspaceCwd(admittedScope, null, false)).toBeNull();
|
||||
expect(
|
||||
nativeChatWorkspaceMatches({
|
||||
scope: scopeA,
|
||||
@@ -275,6 +277,8 @@ describe("native external chat workspace boundary", () => {
|
||||
).toBe(false);
|
||||
expect(oldInput.workspace.cwd).toBe(cwd);
|
||||
}
|
||||
const legacyTaskRoot = path.join(instanceRoot, "chat-workspaces", companyId, agentId, issueA);
|
||||
expect(nativeChatWorkspaceCwd({ ...scopeA, taskRoot: legacyTaskRoot }, null, false)).toBe(legacyTaskRoot);
|
||||
});
|
||||
|
||||
it("fails closed if an existing task directory is a symlink to another task", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { lstat, mkdir, realpath } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { and, eq, sql } from "drizzle-orm";
|
||||
import { chatConversations, issues, type Db } from "@paperclipai/db";
|
||||
import { chatConversations, issues, executionWorkspaces, projectWorkspaces, type Db } from "@paperclipai/db";
|
||||
import { resolvePaperclipInstanceRoot } from "../../home-paths.js";
|
||||
|
||||
export type NativeChatWorkspaceScope = {
|
||||
@@ -30,6 +30,7 @@ export async function findNativeChatWorkspaceScope(
|
||||
agentId: string;
|
||||
issueId: string | null;
|
||||
instanceRoot?: string;
|
||||
admittedCwd?: string | null;
|
||||
},
|
||||
): Promise<NativeChatWorkspaceScope | null> {
|
||||
if (
|
||||
@@ -43,6 +44,19 @@ export async function findNativeChatWorkspaceScope(
|
||||
id: issues.id,
|
||||
projectId: issues.projectId,
|
||||
originKind: issues.originKind,
|
||||
selection: issues.workspaceSelection,
|
||||
configuredProjectId: projectWorkspaces.projectId,
|
||||
hasConfiguredDefault: sql<boolean>`exists (
|
||||
select 1 from ${projectWorkspaces}
|
||||
where ${projectWorkspaces.projectId} = ${issues.projectId}
|
||||
and ${projectWorkspaces.companyId} = ${input.companyId}
|
||||
and (nullif(btrim(${projectWorkspaces.cwd}), '') is not null
|
||||
or nullif(btrim(${projectWorkspaces.repoUrl}), '') is not null)
|
||||
)`,
|
||||
boundWorkspaceId: executionWorkspaces.id,
|
||||
boundProjectId: executionWorkspaces.projectId,
|
||||
boundSourceIssueId: executionWorkspaces.sourceIssueId,
|
||||
boundCwd: executionWorkspaces.cwd,
|
||||
hasConversation: sql<boolean>`exists (
|
||||
select 1 from ${chatConversations}
|
||||
where ${chatConversations.issueId} = ${issues.id}
|
||||
@@ -50,6 +64,8 @@ export async function findNativeChatWorkspaceScope(
|
||||
)`,
|
||||
})
|
||||
.from(issues)
|
||||
.leftJoin(projectWorkspaces, and(eq(projectWorkspaces.id, issues.projectWorkspaceId), eq(projectWorkspaces.companyId, input.companyId)))
|
||||
.leftJoin(executionWorkspaces, and(eq(executionWorkspaces.id, issues.executionWorkspaceId), eq(executionWorkspaces.companyId, input.companyId)))
|
||||
.where(
|
||||
and(eq(issues.id, input.issueId), eq(issues.companyId, input.companyId)),
|
||||
)
|
||||
@@ -64,23 +80,18 @@ export async function findNativeChatWorkspaceScope(
|
||||
companyId: input.companyId,
|
||||
agentId: input.agentId,
|
||||
issueId: issue.id,
|
||||
projectId: issue.projectId,
|
||||
projectId: issue.boundWorkspaceId ? issue.boundProjectId : issue.selection?.selection.kind === "task_directory" ? null : issue.configuredProjectId ?? (issue.hasConfiguredDefault ? issue.projectId : null),
|
||||
instanceRoot,
|
||||
// This must be a sibling of agent homes, not a child readable by an old
|
||||
// provider thread whose immutable permission root is the entire agent home.
|
||||
taskRoot: path.join(
|
||||
instanceRoot,
|
||||
"chat-workspaces",
|
||||
segment(input.companyId),
|
||||
segment(input.agentId),
|
||||
segment(issue.id),
|
||||
),
|
||||
taskRoot: issue.boundSourceIssueId === issue.id && issue.boundCwd
|
||||
? issue.boundCwd : input.admittedCwd ?? path.join(instanceRoot, "isolated-workspaces", segment(input.companyId), segment(issue.id)),
|
||||
};
|
||||
}
|
||||
|
||||
export type NativeChatProjectWorkspace = {
|
||||
companyId: string;
|
||||
projectId: string;
|
||||
projectId: string | null;
|
||||
sourceIssueId: string | null;
|
||||
mode: string;
|
||||
strategyType: string;
|
||||
@@ -89,13 +100,26 @@ export type NativeChatProjectWorkspace = {
|
||||
providerRef: string | null;
|
||||
};
|
||||
|
||||
function isNativeChatTaskRoot(scope: NativeChatWorkspaceScope): boolean {
|
||||
const relative = path.relative(scope.instanceRoot, scope.taskRoot);
|
||||
const allowedTask = path.join("isolated-workspaces", segment(scope.companyId), segment(scope.issueId));
|
||||
const legacyParts = relative.split(path.sep);
|
||||
return relative === allowedTask || (
|
||||
legacyParts.length === 4 && legacyParts[0] === "chat-workspaces" &&
|
||||
legacyParts[1] === scope.companyId && /^[a-zA-Z0-9_-]+$/.test(legacyParts[2]!) && legacyParts[3] === scope.issueId
|
||||
);
|
||||
}
|
||||
|
||||
/** No implicit move from an intentionally configured repository to an empty cwd. */
|
||||
export function nativeChatWorkspaceCwd(
|
||||
scope: NativeChatWorkspaceScope,
|
||||
workspace: NativeChatProjectWorkspace | null,
|
||||
reuseExisting: boolean,
|
||||
): string | null {
|
||||
if (!scope.projectId) return workspace === null ? scope.taskRoot : null;
|
||||
if (!scope.projectId) return isNativeChatTaskRoot(scope) && (workspace === null || (
|
||||
workspace.companyId === scope.companyId && workspace.sourceIssueId === scope.issueId &&
|
||||
workspace.projectId === null && workspace.cwd === scope.taskRoot && ["active", "idle"].includes(workspace.status)
|
||||
)) ? scope.taskRoot : null;
|
||||
if (
|
||||
!reuseExisting ||
|
||||
!workspace ||
|
||||
@@ -120,12 +144,9 @@ export async function materializeNativeChatTaskRoot(
|
||||
if (scope.projectId)
|
||||
throw new Error("native_chat_workspace_project_requires_isolation");
|
||||
let cursor = scope.instanceRoot;
|
||||
for (const part of [
|
||||
"chat-workspaces",
|
||||
segment(scope.companyId),
|
||||
segment(scope.agentId),
|
||||
segment(scope.issueId),
|
||||
]) {
|
||||
const relative = path.relative(scope.instanceRoot, scope.taskRoot);
|
||||
if (!isNativeChatTaskRoot(scope)) throw new Error("native_chat_workspace_path_not_isolated");
|
||||
for (const part of relative.split(path.sep)) {
|
||||
cursor = path.join(cursor, part);
|
||||
await mkdir(cursor, { mode: 0o700 }).catch(
|
||||
(error: NodeJS.ErrnoException) => {
|
||||
|
||||
@@ -80,7 +80,7 @@ describe("native runner performance trace", () => {
|
||||
});
|
||||
expect(ingress).toEqual({
|
||||
name: "question_response.to_run_created",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: answeredAtMs,
|
||||
endedAtMs: answeredAtMs + 10,
|
||||
});
|
||||
@@ -120,7 +120,7 @@ describe("native runner performance trace", () => {
|
||||
clock.mockRestore();
|
||||
}
|
||||
expect(
|
||||
events.find((event) => event.payload?.span === "task.run.measured")
|
||||
events.find((event) => event.payload?.span === "task.provider_session.measured")
|
||||
?.payload,
|
||||
).toMatchObject({ durationMs: 19_000 });
|
||||
expect(original.createdAt).toBe(new Date(1_000).toISOString());
|
||||
@@ -144,7 +144,7 @@ describe("native runner performance trace", () => {
|
||||
}),
|
||||
).toEqual({
|
||||
name: "comment.to_run_created",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: 1_000,
|
||||
endedAtMs: 3_000,
|
||||
});
|
||||
@@ -210,7 +210,7 @@ describe("native runner performance trace", () => {
|
||||
},
|
||||
});
|
||||
const prepare = trace.start("task.prepare", {
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: starts.preparationStartedAtMs,
|
||||
});
|
||||
await trace.end(prepare, { endedAtMs: attemptStartedAtMs + 50 });
|
||||
@@ -263,7 +263,7 @@ describe("native runner performance trace", () => {
|
||||
schema: "paperclip.run-performance-span.v1",
|
||||
traceSchemaVersion: NATIVE_RUN_TRACE_SCHEMA_VERSION,
|
||||
span: "runner.transport.selected",
|
||||
parentSpan: "task.run",
|
||||
parentSpan: "task.provider_session",
|
||||
startOffsetMs: 125,
|
||||
durationMs: 0,
|
||||
outcome: "ok",
|
||||
@@ -304,7 +304,7 @@ describe("native runner performance trace", () => {
|
||||
const { traceContext, spans } = createRecordingTraceContext();
|
||||
const trace = createNativeRunTrace({ runId: "skills-run", startedAtMs: 100,
|
||||
traceContext, onEvent: async () => { throw new Error("run log unavailable"); } });
|
||||
const preparation = trace.start("task.prepare", { parentName: "task.run", startedAtMs: 100 });
|
||||
const preparation = trace.start("task.prepare", { parentName: "task.provider_session", startedAtMs: 100 });
|
||||
await expect(trace.record({ name: "skills.prepare", parentName: "task.prepare", startedAtMs: 120, endedAtMs: 170 })).resolves.toBeUndefined();
|
||||
await trace.end(preparation, { endedAtMs: 200 });
|
||||
expect(spans.find((span) => span.name === "skills.prepare")).toMatchObject({ name: "skills.prepare", parentName: "task.prepare", endedAtMs: 170 });
|
||||
@@ -350,7 +350,7 @@ describe("native runner performance trace", () => {
|
||||
});
|
||||
|
||||
const prepare = trace.start("task.prepare", {
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: 1_010,
|
||||
});
|
||||
const environment = trace.start("environment.startup", {
|
||||
@@ -367,7 +367,7 @@ describe("native runner performance trace", () => {
|
||||
await trace.end(prepare, { endedAtMs: 1_040 });
|
||||
|
||||
const execute = trace.start("native.session.execute", {
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: 1_050,
|
||||
});
|
||||
const startup = trace.start("runner.session.startup", {
|
||||
@@ -417,11 +417,11 @@ describe("native runner performance trace", () => {
|
||||
|
||||
const parentOf = (name: string) =>
|
||||
spans.find((span) => span.name === name)?.parentName;
|
||||
expect(parentOf("task.run")).toBeNull();
|
||||
expect(parentOf("task.prepare")).toBe("task.run");
|
||||
expect(parentOf("task.provider_session")).toBeNull();
|
||||
expect(parentOf("task.prepare")).toBe("task.provider_session");
|
||||
expect(parentOf("environment.startup")).toBe("task.prepare");
|
||||
expect(parentOf("environment.acquire")).toBe("environment.startup");
|
||||
expect(parentOf("native.session.execute")).toBe("task.run");
|
||||
expect(parentOf("native.session.execute")).toBe("task.provider_session");
|
||||
expect(parentOf("runner.session.startup")).toBe("native.session.execute");
|
||||
expect(parentOf("runner.artifact.prepare")).toBe("runner.session.startup");
|
||||
expect(parentOf("runner.artifact.discover")).toBe(
|
||||
@@ -437,10 +437,10 @@ describe("native runner performance trace", () => {
|
||||
expect(parentOf("provider.dynamic")).toBe("agent.turn");
|
||||
expect(parentOf("provider.turn.queue")).toBe("agent.turn");
|
||||
|
||||
expect(spans.filter((span) => span.name === "task.run")).toHaveLength(1);
|
||||
expect(spans.some((span) => span.name === "task.run.measured")).toBe(false);
|
||||
expect(spans.filter((span) => span.name === "task.provider_session")).toHaveLength(1);
|
||||
expect(spans.some((span) => span.name === "task.provider_session.measured")).toBe(false);
|
||||
expect(
|
||||
events.some((event) => event.payload?.span === "task.run.measured"),
|
||||
events.some((event) => event.payload?.span === "task.provider_session.measured"),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from "../../instrumentation.js";
|
||||
|
||||
export const NATIVE_RUN_SPAN_EVENT_TYPE = "run.performance.span";
|
||||
export const NATIVE_RUN_TRACE_SCHEMA_VERSION = 2;
|
||||
export const NATIVE_RUN_TRACE_SCHEMA_VERSION = 3;
|
||||
|
||||
export type NativeRunSpanOutcome = "ok" | "failed";
|
||||
|
||||
@@ -41,7 +41,7 @@ export function buildNativeWakeIngressSpan(input: {
|
||||
if (answeredAt !== null && Number.isFinite(answeredAt) && answeredAt >= 0) {
|
||||
return {
|
||||
name: "question_response.to_run_created",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: answeredAt,
|
||||
endedAtMs: Math.max(answeredAt, input.runCreatedAtMs),
|
||||
};
|
||||
@@ -60,7 +60,7 @@ export function buildNativeWakeIngressSpan(input: {
|
||||
? null
|
||||
: {
|
||||
name: "comment.to_run_created",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: createdAt,
|
||||
endedAtMs: Math.max(createdAt, input.runCreatedAtMs),
|
||||
};
|
||||
@@ -77,13 +77,13 @@ export function buildNativeHeartbeatPreparationSpans(input: {
|
||||
return [
|
||||
{
|
||||
name: "heartbeat.queue",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: input.runCreatedAtMs,
|
||||
endedAtMs: Math.max(input.runCreatedAtMs, input.runStartedAtMs),
|
||||
},
|
||||
{
|
||||
name: "heartbeat.prepare_before_environment",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
// A same-run resume retains startedAt for wall-time accounting; it is
|
||||
// not the beginning of this dispatch attempt's preparation.
|
||||
startedAtMs: input.attemptStartedAtMs,
|
||||
@@ -94,7 +94,7 @@ export function buildNativeHeartbeatPreparationSpans(input: {
|
||||
},
|
||||
{
|
||||
name: "heartbeat.prepare_after_environment",
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: Math.min(
|
||||
input.nativeDispatchAtMs,
|
||||
input.environmentRealizeEndedAtMs,
|
||||
@@ -210,7 +210,7 @@ export function createNativeRunTrace(input: {
|
||||
const runIdHash = hashedId(input.runId);
|
||||
let rootSpan: SpanHandle;
|
||||
try {
|
||||
rootSpan = tracing.tracer.startSpan("task.run", {
|
||||
rootSpan = tracing.tracer.startSpan("task.provider_session", {
|
||||
startTime: traceStartedAtMs,
|
||||
attributes: {
|
||||
"paperclip.task.run.run_id": runIdHash,
|
||||
@@ -249,19 +249,19 @@ export function createNativeRunTrace(input: {
|
||||
? { name: active.name, context: active.context }
|
||||
: runtimeParentScope && !runtimeParentScope.ended
|
||||
? { name: runtimeParentScope.name, context: runtimeParentScope.context }
|
||||
: { name: "task.run", context: rootContext };
|
||||
: { name: "task.provider_session", context: rootContext };
|
||||
};
|
||||
|
||||
const parentFor = (
|
||||
parentName?: string,
|
||||
): { name: string; context: unknown } => {
|
||||
if (parentName && parentName !== "task.run") {
|
||||
if (parentName && parentName !== "task.provider_session") {
|
||||
const named = namedScopes.get(parentName);
|
||||
if (named) return { name: named.name, context: named.context };
|
||||
return currentParent();
|
||||
}
|
||||
if (parentName === "task.run")
|
||||
return { name: "task.run", context: rootContext };
|
||||
if (parentName === "task.provider_session")
|
||||
return { name: "task.provider_session", context: rootContext };
|
||||
return currentParent();
|
||||
};
|
||||
|
||||
@@ -298,7 +298,7 @@ export function createNativeRunTrace(input: {
|
||||
schema: "paperclip.run-performance-span.v1",
|
||||
traceSchemaVersion: NATIVE_RUN_TRACE_SCHEMA_VERSION,
|
||||
span: span.name,
|
||||
parentSpan: span.parentName ?? "task.run",
|
||||
parentSpan: span.parentName ?? "task.provider_session",
|
||||
startOffsetMs: startedAtMs - traceStartedAtMs,
|
||||
durationMs,
|
||||
outcome,
|
||||
@@ -508,8 +508,8 @@ export function createNativeRunTrace(input: {
|
||||
// Preserve the historical run-log record without emitting a duplicate
|
||||
// full-width child span into the OpenTelemetry waterfall.
|
||||
await emitCompletedSpanEvent({
|
||||
name: "task.run.measured",
|
||||
parentName: "task.run",
|
||||
name: "task.provider_session.measured",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: traceStartedAtMs,
|
||||
endedAtMs,
|
||||
outcome,
|
||||
@@ -541,7 +541,7 @@ export async function recordFailedSkillPreparation(input: {
|
||||
}): Promise<void> {
|
||||
try {
|
||||
const trace = createNativeRunTrace(input);
|
||||
const preparation = trace.start("task.prepare", { parentName: "task.run", startedAtMs: input.startedAtMs });
|
||||
const preparation = trace.start("task.prepare", { parentName: "task.provider_session", startedAtMs: input.startedAtMs });
|
||||
const endedAtMs = Date.now();
|
||||
await trace.record({ name: "skills.prepare", parentName: "task.prepare", startedAtMs: input.startedAtMs, endedAtMs, outcome: "failed" });
|
||||
await trace.end(preparation, { endedAtMs, outcome: "failed" });
|
||||
|
||||
@@ -5851,7 +5851,7 @@ describe("native resumed preparation timing", () => {
|
||||
const payloadFor = (span: string) =>
|
||||
events.find((event) => event.payload?.span === span)?.payload;
|
||||
expect(payloadFor("question_response.to_run_created")).toMatchObject({
|
||||
parentSpan: "task.run",
|
||||
parentSpan: "task.provider_session",
|
||||
durationMs: 50,
|
||||
startOffsetMs: 0,
|
||||
});
|
||||
@@ -5859,7 +5859,7 @@ describe("native resumed preparation timing", () => {
|
||||
durationMs: 30,
|
||||
startOffsetMs: 70,
|
||||
});
|
||||
expect(payloadFor("task.run.measured")).toMatchObject({ durationMs: 100 });
|
||||
expect(payloadFor("task.provider_session.measured")).toMatchObject({ durationMs: 100 });
|
||||
});
|
||||
|
||||
it("uses attempt-local preparation in the executor without truncating run elapsed time", async () => {
|
||||
@@ -5913,7 +5913,7 @@ describe("native resumed preparation timing", () => {
|
||||
durationMs: 1_000,
|
||||
startOffsetMs: 0,
|
||||
});
|
||||
expect(payloadFor("task.run.measured")).toMatchObject({
|
||||
expect(payloadFor("task.provider_session.measured")).toMatchObject({
|
||||
durationMs: 984_050,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7696,7 +7696,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
});
|
||||
const toolTrace = createNativeToolTrace(trace);
|
||||
const taskPrepareScope = trace.start("task.prepare", {
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: preparationStarts.preparationStartedAtMs,
|
||||
});
|
||||
const environmentSpans = preparationSpans.filter(
|
||||
@@ -7724,7 +7724,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
await trace.record({
|
||||
...span,
|
||||
parentName: rootMilestone
|
||||
? "task.run"
|
||||
? "task.provider_session"
|
||||
: environmentSpans.includes(span)
|
||||
? "environment.startup"
|
||||
: "task.prepare",
|
||||
@@ -8283,7 +8283,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
outcome,
|
||||
});
|
||||
taskSettleScope = trace.start("task.settle", {
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: milestoneAtMs,
|
||||
});
|
||||
trace.activate(taskSettleScope);
|
||||
@@ -8953,7 +8953,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
});
|
||||
return result;
|
||||
},
|
||||
{ parentName: "task.run" },
|
||||
{ parentName: "task.provider_session" },
|
||||
);
|
||||
// A detached consumer can resolve successfully after its stream closes.
|
||||
// Only the replacement controller may settle the run or certify accounting.
|
||||
@@ -9057,7 +9057,7 @@ async function executePaperclipNativeSessionWithinScope(
|
||||
).slice(-4_096);
|
||||
if (!taskSettleScope) {
|
||||
taskSettleScope = trace.start("task.settle", {
|
||||
parentName: "task.run",
|
||||
parentName: "task.provider_session",
|
||||
startedAtMs: failedAtMs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { WorkspaceCheckpointMetrics } from "@paperclipai/adapter-utils/workspace-checkpoint";
|
||||
import type { RuntimeSpanRunner } from "@paperclipai/adapter-utils/acpx-engine/startup-timing";
|
||||
import fs from "node:fs/promises";
|
||||
import { createReadStream } from "node:fs";
|
||||
import path from "node:path";
|
||||
@@ -31,8 +33,9 @@ import { parseObject } from "../../adapters/utils.js";
|
||||
import type { NativeRestartRecoveryClaim } from "./native-restart-recovery.js";
|
||||
|
||||
const LEGACY_DESCRIPTOR_SCHEMA = "paperclip.native-workspace-sync/v1";
|
||||
const DESCRIPTOR_SCHEMA = "paperclip.native-workspace-sync/v2";
|
||||
type DescriptorSchema = typeof DESCRIPTOR_SCHEMA | typeof LEGACY_DESCRIPTOR_SCHEMA;
|
||||
const PREVIOUS_DESCRIPTOR_SCHEMA = "paperclip.native-workspace-sync/v2";
|
||||
const DESCRIPTOR_SCHEMA = "paperclip.native-workspace-sync/v3";
|
||||
type DescriptorSchema = typeof DESCRIPTOR_SCHEMA | typeof PREVIOUS_DESCRIPTOR_SCHEMA | typeof LEGACY_DESCRIPTOR_SCHEMA;
|
||||
const STAMP_SCHEMA = "paperclip.native-workspace-stamp/v1";
|
||||
const STATE_ROOT_NAME = "native-workspace-sync";
|
||||
const DESCRIPTOR_NAME = "descriptor";
|
||||
@@ -61,6 +64,7 @@ interface NativeWorkspaceSyncDescriptor {
|
||||
baselineSha256: string;
|
||||
baseline: SerializedDirectorySnapshot;
|
||||
gitSnapshot: GitWorkspaceSnapshot | null;
|
||||
repositories?: NonNullable<GitWorkspaceSnapshot["repositories"]>;
|
||||
seed: {
|
||||
workspaceArchiveSha256: string;
|
||||
gitArchiveSha256: string | null;
|
||||
@@ -190,6 +194,7 @@ async function verifiedDurableSeed(input: {
|
||||
runId: string;
|
||||
seed: NonNullable<NativeWorkspaceSyncDescriptor["seed"]>;
|
||||
gitSnapshot: GitWorkspaceSnapshot | null;
|
||||
repositories?: NonNullable<GitWorkspaceSnapshot["repositories"]>;
|
||||
}): Promise<WorkspaceDurableSeedPaths> {
|
||||
try {
|
||||
const paths = durableSeedPaths(input.runId);
|
||||
@@ -203,7 +208,7 @@ async function verifiedDurableSeed(input: {
|
||||
) {
|
||||
throw new Error("workspace_sync_out_unrecoverable");
|
||||
}
|
||||
if (input.gitSnapshot) {
|
||||
if (input.gitSnapshot || input.repositories?.length) {
|
||||
const gitStat = await fs.lstat(paths.gitArchivePath);
|
||||
if (
|
||||
gitStat.isSymbolicLink() ||
|
||||
@@ -219,7 +224,7 @@ async function verifiedDurableSeed(input: {
|
||||
return {
|
||||
workspaceArchivePath: paths.workspaceArchivePath,
|
||||
workspaceArchiveSha256: input.seed.workspaceArchiveSha256,
|
||||
gitArchivePath: input.gitSnapshot ? paths.gitArchivePath : null,
|
||||
gitArchivePath: input.gitSnapshot || input.repositories?.length ? paths.gitArchivePath : null,
|
||||
gitArchiveSha256: input.seed.gitArchiveSha256,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -281,7 +286,7 @@ export function readNativeWorkspaceSyncReference(
|
||||
): NativeWorkspaceSyncReference | null {
|
||||
const candidate = parseObject(value);
|
||||
if (
|
||||
(candidate.schema !== DESCRIPTOR_SCHEMA && candidate.schema !== LEGACY_DESCRIPTOR_SCHEMA) ||
|
||||
(candidate.schema !== DESCRIPTOR_SCHEMA && candidate.schema !== PREVIOUS_DESCRIPTOR_SCHEMA && candidate.schema !== LEGACY_DESCRIPTOR_SCHEMA) ||
|
||||
(candidate.state !== "prepared" && candidate.state !== "finalized") ||
|
||||
typeof candidate.descriptorSha256 !== "string" ||
|
||||
!/^[0-9a-f]{64}$/.test(candidate.descriptorSha256) ||
|
||||
@@ -360,6 +365,7 @@ async function persistSnapshotManifests(runId: string, snapshot: NonNullable<Pre
|
||||
if (baseline.ignoredPaths && isPathManifest(baseline.ignoredPaths)) yield baseline.ignoredPaths;
|
||||
}
|
||||
yield* gitManifests(snapshot.gitSnapshot);
|
||||
for (const repo of snapshot.repositories ?? []) yield* gitManifests(repo.snapshot);
|
||||
};
|
||||
const copies = new Map<string, string>();
|
||||
for (const manifest of manifests()) {
|
||||
@@ -385,6 +391,7 @@ async function persistSnapshotManifests(runId: string, snapshot: NonNullable<Pre
|
||||
// Ownership tracks original scratch directories, never the new durable path.
|
||||
await disposeDirectorySnapshot(snapshot.baseline);
|
||||
await disposeGitWorkspaceSnapshot(snapshot.gitSnapshot);
|
||||
for (const repo of snapshot.repositories ?? []) await disposeGitWorkspaceSnapshot(repo.snapshot);
|
||||
}
|
||||
|
||||
function parseGitSnapshot(
|
||||
@@ -394,7 +401,7 @@ function parseGitSnapshot(
|
||||
if (value === null) return null;
|
||||
const candidate = parseObject(value);
|
||||
const paths = [candidate.overlayPaths, candidate.deletedPaths, candidate.ignoredPaths];
|
||||
if (typeof candidate.headCommit !== "string" || (candidate.branchName !== null && typeof candidate.branchName !== "string")
|
||||
if ((candidate.originUrl !== undefined && candidate.originUrl !== null && typeof candidate.originUrl !== "string") || typeof candidate.headCommit !== "string" || (candidate.branchName !== null && typeof candidate.branchName !== "string")
|
||||
|| !paths.every((entries) => validManifestShape(entries) || (Array.isArray(entries) && entries.every((entry) =>
|
||||
typeof entry === "string" && entry.length > 0 && !entry.includes("\0") && !path.posix.isAbsolute(entry) && !path.win32.isAbsolute(entry)
|
||||
&& !entry.split(/[\\/]/).some((segment) => segment === ".." || segment === "."))))) return undefined;
|
||||
@@ -414,6 +421,7 @@ function parseGitSnapshot(
|
||||
return {
|
||||
headCommit: candidate.headCommit,
|
||||
branchName: candidate.branchName as string | null,
|
||||
...(candidate.originUrl !== undefined ? { originUrl: candidate.originUrl as string | null } : {}),
|
||||
overlayPaths: candidate.overlayPaths as WorkspacePaths,
|
||||
deletedPaths: candidate.deletedPaths as WorkspacePaths,
|
||||
ignoredPaths: candidate.ignoredPaths as WorkspacePaths,
|
||||
@@ -464,7 +472,7 @@ async function readDescriptor(input: {
|
||||
const binding = parseObject(candidate.binding);
|
||||
const rawBaseline = parseObject(candidate.baseline);
|
||||
let baseline: DirectorySnapshot | null;
|
||||
if (candidate.schema === DESCRIPTOR_SCHEMA && rawBaseline.version === 2
|
||||
if ((candidate.schema === DESCRIPTOR_SCHEMA || candidate.schema === PREVIOUS_DESCRIPTOR_SCHEMA) && rawBaseline.version === 2
|
||||
&& Array.isArray(rawBaseline.exclude) && rawBaseline.exclude.every((entry) => typeof entry === "string")
|
||||
&& validManifestShape(rawBaseline.entries) && rawBaseline.entries.category === "baseline"
|
||||
&& (rawBaseline.ignoredPaths === undefined || validManifestShape(rawBaseline.ignoredPaths))) {
|
||||
@@ -475,6 +483,12 @@ async function readDescriptor(input: {
|
||||
baseline = parseDirectorySnapshot(candidate.baseline);
|
||||
}
|
||||
const gitSnapshot = parseGitSnapshot(candidate.gitSnapshot);
|
||||
const repositoryContainer = candidate.repositories === undefined ? null : parseGitSnapshot({
|
||||
headCommit: "", branchName: null, overlayPaths: [], deletedPaths: [], ignoredPaths: [], repositories: candidate.repositories,
|
||||
});
|
||||
if (candidate.repositories !== undefined && (!repositoryContainer || candidate.schema !== DESCRIPTOR_SCHEMA || gitSnapshot)) throw new Error("native_workspace_sync_repositories_invalid");
|
||||
const repositories = repositoryContainer?.repositories;
|
||||
for (const repo of repositories ?? []) for (const manifest of gitManifests(repo.snapshot)) await verifyManifest(input.runId, manifest);
|
||||
for (const manifest of gitManifests(gitSnapshot ?? null)) await verifyManifest(input.runId, manifest);
|
||||
const rawSeed =
|
||||
candidate.seed === null || candidate.seed === undefined
|
||||
@@ -509,7 +523,7 @@ async function readDescriptor(input: {
|
||||
? null
|
||||
: undefined;
|
||||
if (
|
||||
(candidate.schema !== DESCRIPTOR_SCHEMA && candidate.schema !== LEGACY_DESCRIPTOR_SCHEMA) ||
|
||||
(candidate.schema !== DESCRIPTOR_SCHEMA && candidate.schema !== PREVIOUS_DESCRIPTOR_SCHEMA && candidate.schema !== LEGACY_DESCRIPTOR_SCHEMA) ||
|
||||
(candidate.state !== "prepared" && candidate.state !== "finalized") ||
|
||||
!baseline ||
|
||||
gitSnapshot === undefined ||
|
||||
@@ -560,6 +574,7 @@ async function readDescriptor(input: {
|
||||
baselineSha256: candidate.baselineSha256,
|
||||
baseline: candidate.baseline as SerializedDirectorySnapshot,
|
||||
gitSnapshot,
|
||||
...(repositories?.length ? { repositories } : {}),
|
||||
seed,
|
||||
createdAt: candidate.createdAt,
|
||||
finalizedAt:
|
||||
@@ -681,6 +696,7 @@ async function remoteStampMatches(input: {
|
||||
target: Extract<AdapterExecutionTarget, { transport: "sandbox" }>;
|
||||
expected: Record<string, unknown>;
|
||||
gitSnapshot?: GitWorkspaceSnapshot | null;
|
||||
repositories?: NonNullable<GitWorkspaceSnapshot["repositories"]>;
|
||||
}): Promise<boolean> {
|
||||
if (!input.target.runner) return false;
|
||||
const stampPath = path.posix.join(
|
||||
@@ -699,11 +715,15 @@ async function remoteStampMatches(input: {
|
||||
gitChecks.push(snapshot.branchName === null
|
||||
? `(${git} symbolic-ref --quiet HEAD >/dev/null 2>&1; test $? -eq 1)`
|
||||
: `test "$(${git} symbolic-ref --quiet --short HEAD)" = ${shellQuote(snapshot.branchName)}`);
|
||||
if (snapshot.originUrl !== undefined) gitChecks.push(snapshot.originUrl === null
|
||||
? `! ${git} config --get remote.origin.url >/dev/null 2>&1`
|
||||
: `test "$(${git} config --get remote.origin.url)" = ${shellQuote(snapshot.originUrl)}`);
|
||||
for (const repository of snapshot.repositories ?? []) {
|
||||
checkGit(path.posix.join(remoteDir, repository.path), repository.snapshot);
|
||||
}
|
||||
};
|
||||
if (input.gitSnapshot) checkGit(input.target.remoteCwd, input.gitSnapshot);
|
||||
for (const repo of input.repositories ?? []) checkGit(path.posix.join(input.target.remoteCwd, repo.path), repo.snapshot);
|
||||
const result = await input.target.runner.execute({
|
||||
command: input.target.shellCommand ?? "sh",
|
||||
args: [
|
||||
@@ -767,7 +787,12 @@ async function prepareRuntime(input: {
|
||||
mode: WorkspaceInboundMode;
|
||||
baseline?: DirectorySnapshot;
|
||||
gitSnapshot?: GitWorkspaceSnapshot | null;
|
||||
repositories?: NonNullable<GitWorkspaceSnapshot["repositories"]>;
|
||||
durableSeed?: WorkspaceDurableSeedPaths;
|
||||
seedCacheDirectory?: string;
|
||||
seedCacheCompanyDirectory?: string;
|
||||
onPhase?: RuntimeSpanRunner;
|
||||
onCheckpoint?: (metrics: WorkspaceCheckpointMetrics) => Promise<void>;
|
||||
}): Promise<PreparedAdapterExecutionTargetRuntime> {
|
||||
return prepareAdapterExecutionTargetRuntime({
|
||||
runId: input.runId,
|
||||
@@ -779,6 +804,12 @@ async function prepareRuntime(input: {
|
||||
workspaceDurableSeed: input.durableSeed,
|
||||
workspaceBaseline: input.baseline,
|
||||
workspaceGitSnapshot: input.gitSnapshot,
|
||||
workspaceRepositories: input.repositories ?? (input.baseline ? [] : undefined),
|
||||
workspaceCheckpoint: true,
|
||||
onWorkspaceCheckpoint: input.onCheckpoint,
|
||||
workspaceSeedCacheDirectory: input.seedCacheDirectory,
|
||||
workspaceSeedCacheCompanyDirectory: input.seedCacheCompanyDirectory,
|
||||
runtimeSpan: input.onPhase,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -789,8 +820,11 @@ async function finalizePreparedRuntime(input: {
|
||||
runtime: PreparedAdapterExecutionTargetRuntime;
|
||||
descriptor: NativeWorkspaceSyncDescriptor;
|
||||
assertOwnership?: () => Promise<void>;
|
||||
onPhase?: RuntimeSpanRunner;
|
||||
onCheckpoint?: (metrics: WorkspaceCheckpointMetrics) => Promise<void>;
|
||||
}): Promise<NativeWorkspaceSyncReference> {
|
||||
await input.runtime.restoreWorkspace();
|
||||
if (input.onPhase) await input.onPhase("workspace.restore", () => input.runtime.restoreWorkspace());
|
||||
else await input.runtime.restoreWorkspace();
|
||||
await input.assertOwnership?.();
|
||||
const finalSnapshot =
|
||||
await import("@paperclipai/adapter-utils/workspace-restore-merge").then(
|
||||
@@ -836,6 +870,8 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
restartRecovery?: NativeRestartRecoveryClaim;
|
||||
sameRunRecovery?: boolean;
|
||||
resourceDisposition?: NativeWorkspaceResourceDisposition;
|
||||
onPhase?: RuntimeSpanRunner;
|
||||
onCheckpoint?: (metrics: WorkspaceCheckpointMetrics) => Promise<void>;
|
||||
}): Promise<PreparedNativeWorkspaceSync | null> {
|
||||
if (input.target?.kind !== "remote" || input.target.transport !== "sandbox") {
|
||||
return null;
|
||||
@@ -896,6 +932,7 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
runId: input.runId,
|
||||
seed: existing.descriptor.seed,
|
||||
gitSnapshot: existing.descriptor.gitSnapshot,
|
||||
repositories: existing.descriptor.repositories,
|
||||
})
|
||||
: (() => {
|
||||
throw new Error("workspace_sync_out_unrecoverable");
|
||||
@@ -904,10 +941,13 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
runtime = await prepareRuntime({
|
||||
runId: input.runId,
|
||||
target,
|
||||
onPhase: input.onPhase,
|
||||
onCheckpoint: input.onCheckpoint,
|
||||
workspaceLocalDir: input.workspaceLocalDir,
|
||||
mode,
|
||||
baseline: existing.baseline,
|
||||
gitSnapshot: existing.descriptor.gitSnapshot,
|
||||
repositories: existing.descriptor.repositories,
|
||||
durableSeed,
|
||||
});
|
||||
descriptor = {
|
||||
@@ -934,8 +974,12 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
runtime = await prepareRuntime({
|
||||
runId: input.runId,
|
||||
target,
|
||||
onPhase: input.onPhase,
|
||||
onCheckpoint: input.onCheckpoint,
|
||||
workspaceLocalDir: input.workspaceLocalDir,
|
||||
mode,
|
||||
seedCacheCompanyDirectory: path.join(resolvePaperclipInstanceRoot(), STATE_ROOT_NAME, "generations", requireSafeSegment(input.companyId, "company_id")),
|
||||
seedCacheDirectory: path.join(resolvePaperclipInstanceRoot(), STATE_ROOT_NAME, "generations", requireSafeSegment(input.companyId, "company_id"), requireSafeSegment(input.workspaceId, "workspace_id")),
|
||||
durableSeed: {
|
||||
workspaceArchivePath: seedPaths.workspaceArchivePath,
|
||||
gitArchivePath: seedPaths.gitArchivePath,
|
||||
@@ -951,7 +995,7 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
);
|
||||
const verifiedWarmAdoption =
|
||||
priorStamp.hostSha256 === currentHostSha256 &&
|
||||
(await remoteStampMatches({ target, expected: priorStamp, gitSnapshot: currentSnapshot.gitSnapshot }));
|
||||
(await remoteStampMatches({ target, expected: priorStamp, gitSnapshot: currentSnapshot.gitSnapshot, repositories: currentSnapshot.repositories }));
|
||||
if (verifiedWarmAdoption) {
|
||||
mode = "adopt_remote";
|
||||
} else {
|
||||
@@ -963,6 +1007,9 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
mode,
|
||||
baseline: currentSnapshot.baseline,
|
||||
gitSnapshot: currentSnapshot.gitSnapshot,
|
||||
repositories: currentSnapshot.repositories,
|
||||
onPhase: input.onPhase,
|
||||
onCheckpoint: input.onCheckpoint,
|
||||
durableSeed: {
|
||||
workspaceArchivePath: seedPaths.workspaceArchivePath,
|
||||
gitArchivePath: seedPaths.gitArchivePath,
|
||||
@@ -977,7 +1024,7 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
const workspaceArchiveSha256 = await sha256File(
|
||||
seedPaths.workspaceArchivePath,
|
||||
);
|
||||
const gitArchiveSha256 = snapshot.gitSnapshot
|
||||
const gitArchiveSha256 = snapshot.gitSnapshot || snapshot.repositories?.length
|
||||
? await sha256File(seedPaths.gitArchivePath)
|
||||
: null;
|
||||
descriptor = {
|
||||
@@ -995,6 +1042,7 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
baselineSha256: directorySnapshotSha256(snapshot.baseline),
|
||||
baseline: serializeDirectorySnapshot(snapshot.baseline),
|
||||
gitSnapshot: snapshot.gitSnapshot,
|
||||
...(snapshot.repositories?.length ? { repositories: snapshot.repositories } : {}),
|
||||
seed: { workspaceArchiveSha256, gitArchiveSha256 },
|
||||
createdAt: now,
|
||||
finalizedAt: null,
|
||||
@@ -1021,6 +1069,8 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
runtime: preparedRuntime,
|
||||
descriptor,
|
||||
assertOwnership,
|
||||
onPhase: input.onPhase,
|
||||
onCheckpoint: input.onCheckpoint,
|
||||
})
|
||||
.then((finalizedReference) => {
|
||||
reference = finalizedReference;
|
||||
@@ -1041,11 +1091,35 @@ export async function prepareNativeWorkspaceSync(input: {
|
||||
}
|
||||
}
|
||||
|
||||
export async function readNativeWorkspaceSyncLocalCwd(input: {
|
||||
companyId: string;
|
||||
runId: string;
|
||||
workspaceId: string;
|
||||
reference: NativeWorkspaceSyncReference;
|
||||
}): Promise<string> {
|
||||
const existing = await readDescriptor(input);
|
||||
try {
|
||||
if (existing.descriptor.binding.companyId !== input.companyId ||
|
||||
existing.descriptor.binding.workspaceId !== input.workspaceId) {
|
||||
throw new Error("native_workspace_sync_descriptor_binding_mismatch");
|
||||
}
|
||||
return existing.descriptor.binding.localCwd;
|
||||
} finally {
|
||||
await disposeDirectorySnapshot(existing.baseline);
|
||||
await disposeGitWorkspaceSnapshot(existing.descriptor.gitSnapshot);
|
||||
for (const repository of existing.descriptor.repositories ?? []) {
|
||||
await disposeGitWorkspaceSnapshot(repository.snapshot);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function resumeNativeWorkspaceSync(input: {
|
||||
db: Db;
|
||||
runId: string;
|
||||
target: AdapterExecutionTarget;
|
||||
assertOwnership?: () => Promise<void>;
|
||||
onPhase?: RuntimeSpanRunner;
|
||||
onCheckpoint?: (metrics: WorkspaceCheckpointMetrics) => Promise<void>;
|
||||
}): Promise<boolean> {
|
||||
if (input.target.kind !== "remote" || input.target.transport !== "sandbox") {
|
||||
throw new Error("workspace_sync_out_unrecoverable");
|
||||
@@ -1094,10 +1168,13 @@ export async function resumeNativeWorkspaceSync(input: {
|
||||
const runtime = await prepareRuntime({
|
||||
runId: input.runId,
|
||||
target: input.target,
|
||||
onPhase: input.onPhase,
|
||||
onCheckpoint: input.onCheckpoint,
|
||||
workspaceLocalDir: existing.descriptor.binding.localCwd,
|
||||
mode: "adopt_remote",
|
||||
baseline: existing.baseline,
|
||||
gitSnapshot: existing.descriptor.gitSnapshot,
|
||||
repositories: existing.descriptor.repositories,
|
||||
});
|
||||
await finalizePreparedRuntime({
|
||||
db: input.db,
|
||||
@@ -1106,6 +1183,8 @@ export async function resumeNativeWorkspaceSync(input: {
|
||||
runtime,
|
||||
descriptor: existing.descriptor,
|
||||
assertOwnership: input.assertOwnership,
|
||||
onPhase: input.onPhase,
|
||||
onCheckpoint: input.onCheckpoint,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -103,7 +103,10 @@ describe("PaperclipRunnerToolAuthority", () => {
|
||||
issueId,
|
||||
runId,
|
||||
});
|
||||
expect(authority.definitions()).toHaveLength(44);
|
||||
expect(authority.definitions()).toHaveLength(48);
|
||||
expect(authority.definitions().map(tool => tool.name)).toEqual(expect.arrayContaining([
|
||||
"get_workspace", "list_workspaces", "select_workspace", "prepare_repository",
|
||||
]));
|
||||
expect(authority.definitions().map(tool => tool.name)).not.toContain("read_chat_attachment");
|
||||
expect(authority.definitions().map(tool => tool.name)).not.toContain("read_current_wake_comments");
|
||||
const questions = authority.definitions().find(tool => tool.name === "request_human_input")!;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { callTaskWorkspaceTool } from "../task-workspace-tools.js";
|
||||
import { TaskAttachmentReadCache, TASK_ATTACHMENT_DEFINITIONS, TASK_ATTACHMENT_SCHEMAS, listTaskAttachments, readTaskAttachment } from "./runner-task-attachments.js";
|
||||
import { RUNNER_BRIDGE_SCHEMAS, runnerBridgeDefinitions, readAssignedSkill, executeWorkspaceTool, readWorkspaceUploadFile, settleRunnerBridgeRead, assertRunnerWorkspacePathAllowed } from "./runner-bridge-tools.js";
|
||||
import type { NativeRuntimeContextSnapshot } from "../../vendor/paperclip-runner/index.js";
|
||||
@@ -110,7 +111,7 @@ const IMPLEMENTED_OPERATIONS = new Set([
|
||||
"search_api", "call_api", "hire_agent",
|
||||
"get_task_context", "get_task_history", "search_tasks", "report_progress", "set_task_title", "set_task_monitor",
|
||||
"request_human_input",
|
||||
"create_skill", "update_skill", "create_task", "reassign_task", "set_dependencies", "create_project", "list_project_repositories", "list_projects", "register_deliverable",
|
||||
"get_workspace", "list_workspaces", "select_workspace", "prepare_repository", "create_skill", "update_skill", "create_task", "reassign_task", "set_dependencies", "create_project", "list_project_repositories", "list_projects", "register_deliverable",
|
||||
"list_documents", "read_document", "list_document_revisions", "write_document",
|
||||
"list_agents", "get_agent", "list_approvals", "get_approval", "get_approval_context",
|
||||
]);
|
||||
@@ -507,6 +508,15 @@ export class PaperclipRunnerToolAuthority {
|
||||
if (!apiUrl || !token) throw new Error("Skill tool authentication is unavailable");
|
||||
return callUpdateSkillTool({ arguments: input, apiUrl, token, companyId: this.binding.companyId });
|
||||
}
|
||||
case "get_workspace":
|
||||
case "list_workspaces":
|
||||
case "select_workspace":
|
||||
case "prepare_repository": {
|
||||
const apiUrl = this.binding.apiUrl ?? process.env.PAPERCLIP_API_URL;
|
||||
const token = createLocalAgentJwt(this.binding.agentId, this.binding.companyId, context.actor.adapterType, this.binding.runId, context.run.responsibleUserId);
|
||||
if (!apiUrl || !token) throw new Error("Workspace tool authentication is unavailable");
|
||||
return callTaskWorkspaceTool({ name: call.tool, arguments: input, apiUrl, token, companyId: this.binding.companyId, issueId: this.binding.issueId, workspaceRoot: this.binding.workspaceRoot });
|
||||
}
|
||||
case "create_project":
|
||||
case "list_project_repositories":
|
||||
case "list_projects": {
|
||||
@@ -1219,6 +1229,7 @@ export class PaperclipRunnerToolAuthority {
|
||||
const createInput = {
|
||||
id: issueId,
|
||||
projectId,
|
||||
workspaceSelectionActor: this.#privacyActor(context.run),
|
||||
...(trust.kind === "low_trust_review" ? {
|
||||
executionPolicy: { ...normalizeIssueExecutionPolicy({ authorizationPolicy: { trustPreset: trust.preset, trustBoundary: trust.boundary } }) },
|
||||
sourceTrust: buildLowTrustSourceTrust({ issueId, runId: this.binding.runId, agentId: this.binding.agentId }),
|
||||
|
||||
Reference in new issue
Block a user