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:
parent
184648c966
commit
4242598d8b
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue