fix(runner): publish verified files from remote task workspaces

Bind remote deliverable reads to the executing environment and accept verified attachment download refs in completion evidence.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-12 21:47:58 -05:00
parent 184648c966
commit 4242598d8b
3 changed files with 29 additions and 18 deletions

View File

@ -1198,3 +1198,8 @@ references and fabricated or cross-task delivery receipts. Text answers and
accessible repository work products do not require an attachment. Publication
failure calls for continued work or a concrete blocker, not a human confirmation
that the task is complete.
Local and remote runners use the same attachment publication contract. Remote
files are read through the bound environment runner, with workspace confinement,
no symlinks or hardlinks, stable file identity, a 10 MiB bound, and exact size and
SHA-256 checks before storage. Remote paths are never opened on the controller.

View File

@ -15,9 +15,11 @@ export async function validateNativeDeliverableEvidence(
...result.completionClaim.criteria.flatMap(({ evidenceRefs }) => evidenceRefs),
]);
for (const value of refs) {
if (typeof value !== "string") continue;
const ref = value.trim();
if (ref.startsWith("deliverable:")) {
const id = ref.slice("deliverable:".length);
const attachmentPath = /^\/api\/attachments\/([^/?#]+)\/content(?:[?#].*)?$/u.exec(ref);
if (ref.startsWith("deliverable:") || attachmentPath) {
const id = attachmentPath?.[1] ?? ref.slice("deliverable:".length);
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
const [attachment] = uuid.test(id)
? await db.select({ id: issueAttachments.id }).from(issueAttachments)
@ -33,7 +35,7 @@ export async function validateNativeDeliverableEvidence(
// URLs and typed durable refs are not workspace paths. Verification commands
// belong in verification; do not scan prose or upload files named by a model.
const localFile = /^(?:file:|\.{0,2}\/|[a-z]:[\\/])/iu.test(ref)
|| (!/^[a-z][a-z0-9+.-]*:/iu.test(ref) && /^[^\s]+\.[a-z0-9]{1,16}(?::\d+(?::\d+)?)?$/iu.test(ref));
|| (!/^[a-z][a-z0-9+.-]*:/iu.test(ref) && /^[^\r\n]+\.[a-z0-9]{1,16}(?::\d+(?::\d+)?)?$/iu.test(ref));
if (localFile) {
throw new Error("Completion cites a workspace-only file that the user cannot download. Before finishing, use register_deliverable for requested file outputs and cite deliverable:<attachmentId> from the receipt, with /api/attachments/<attachmentId>/content as the download link. For repository changes, cite an accessible PR or registered work product instead. No human completion approval was created.");
}

View File

@ -1,3 +1,4 @@
import { readVerifiedRemoteWorkspaceFile } from "./remote-deliverable-file.js";
import { copyBackCodexAuth } from "@paperclipai/adapter-codex-local/server";
import { nativeCompletionFeedback } from "./native-completion-feedback.js";
import { hasAcknowledgedNativeStopIntent } from "../acknowledged-native-stop.js";
@ -9915,6 +9916,20 @@ async function createRunnerdBackendWithinSessionClaim(
): Promise<NativeSessionBackend> {
let recoveryPending = retainedTransition !== undefined;
const target = input.runnerExecutionTarget ?? { kind: "local" as const };
const remoteTarget = target.kind === "remote" ? target : null;
const remoteCommandRunner = remoteTarget
? remoteTarget.transport === "ssh"
? createSshCommandManagedRuntimeRunner({
spec: remoteTarget.spec,
defaultCwd: remoteTarget.remoteCwd,
})
: remoteTarget.runner
: null;
if (remoteTarget && !remoteCommandRunner) {
throw new Error(
"runner_transport_ineligible: remote process runner is unavailable",
);
}
const currentWakeComments = await resolveCurrentWakeCommentsBinding(
input.db,
input.execution.binding,
@ -9934,8 +9949,11 @@ async function createRunnerdBackendWithinSessionClaim(
? input.execution.runtimeContext.mcp.digest
: undefined,
workMode: input.execution.task.workMode,
workspaceRoot: input.execution.workspace.cwd,
workspaceRoot: remoteTarget?.remoteCwd ?? input.execution.workspace.cwd,
executionTargetKind: target.kind,
readRemoteWorkspaceFile: remoteTarget && remoteCommandRunner
? (file) => readVerifiedRemoteWorkspaceFile({ runner: remoteCommandRunner, workspaceRoot: remoteTarget.remoteCwd, ...file })
: undefined,
currentWakeComments: currentWakeComments ?? undefined,
chatAttachmentReadScope: input.chatAttachmentReadScope,
enqueueWakeup: input.enqueueWakeup,
@ -9967,20 +9985,6 @@ async function createRunnerdBackendWithinSessionClaim(
input.durableEnvironmentLeaseId ??
input.execution.binding.executionWorkspaceId;
mkdirSync(root, { recursive: true, mode: 0o700 });
const remoteTarget = target.kind === "remote" ? target : null;
const remoteCommandRunner = remoteTarget
? remoteTarget.transport === "ssh"
? createSshCommandManagedRuntimeRunner({
spec: remoteTarget.spec,
defaultCwd: remoteTarget.remoteCwd,
})
: remoteTarget.runner
: null;
if (remoteTarget && !remoteCommandRunner) {
throw new Error(
"runner_transport_ineligible: remote process runner is unavailable",
);
}
const remoteRuntimeRoot = remoteTarget
? posix.join(
remoteTarget.remoteCwd,