diff --git a/doc/execution-semantics.md b/doc/execution-semantics.md index 58248674d5..fdc71cad49 100644 --- a/doc/execution-semantics.md +++ b/doc/execution-semantics.md @@ -1207,7 +1207,9 @@ that the task is complete. For an explicit file output in the current request, an empty report, a verification-only reference, or an unregistered URL cannot satisfy delivery. The report must cite a task-scoped attachment or a registered accessible work -product. Reading or reviewing an existing file for an inline answer does not +product with a published URL. A `workspace_file` locator alone is not delivery +evidence: it neither verifies the file nor preserves its bytes after cleanup. +Reading or reviewing an existing file for an inline answer does not require uploading that input. Ambiguous prose remains subject to the runner's completion contract; the server's explicit-output check is deliberately narrow. diff --git a/server/src/services/native-runtime/native-deliverable-feedback.ts b/server/src/services/native-runtime/native-deliverable-feedback.ts index 998bdb605e..64f8a516c1 100644 --- a/server/src/services/native-runtime/native-deliverable-feedback.ts +++ b/server/src/services/native-runtime/native-deliverable-feedback.ts @@ -84,9 +84,10 @@ export async function validateNativeDeliverableEvidence( )) : []; const accessibleProduct = products.some(product => { if (["failed", "cancelled", "archived"].includes(product.status)) return false; - const resource = product.metadata?.resourceRef as { kind?: unknown; path?: unknown } | undefined; - const accessible = (typeof product.url === "string" && /^https?:\/\//iu.test(product.url)) || - (resource?.kind === "workspace_file" && typeof resource.path === "string" && resource.path.length > 0); + // A workspace_file resource is only a locator: registration neither checks + // its current bytes nor keeps them alive after workspace cleanup. Requested + // files need a published URL or the verified attachment receipt above. + const accessible = typeof product.url === "string" && /^https?:\/\//iu.test(product.url); return accessible && [product.url, `work_product:${product.id}`, `work-product:${product.id}`, `artifact:${product.id}`] .some(ref => typeof ref === "string" && refs.has(ref)); }); diff --git a/server/src/services/native-runtime/native-runner-file-handoff.test.ts b/server/src/services/native-runtime/native-runner-file-handoff.test.ts index 8c2d7c02f6..212756c819 100644 --- a/server/src/services/native-runtime/native-runner-file-handoff.test.ts +++ b/server/src/services/native-runtime/native-runner-file-handoff.test.ts @@ -226,9 +226,11 @@ describe("native runner file handoff", () => { await expect(nativeCompletionFeedback(db, runId, doneReport([ref]))) .resolves.toContain("Completion report accepted"); } - await db.update(issueWorkProducts).set({ url: null, metadata: { resourceRef: { kind: "workspace_file", path: "report.pdf" } } }).where(eq(issueWorkProducts.id, product.id)); + // Metadata can name a nonexistent or cleaned-up workspace file; it is not a download. + await db.update(issueWorkProducts).set({ url: null, metadata: { resourceRef: { kind: "workspace_file", path: "missing-report.pdf" } } }).where(eq(issueWorkProducts.id, product.id)); await expect(nativeCompletionFeedback(db, runId, doneReport([`work_product:${product.id}`]))) - .resolves.toContain("Completion report accepted"); + .rejects.toThrow("requested file has no accessible delivery evidence"); + await db.update(issueWorkProducts).set({ url: product.url }).where(eq(issueWorkProducts.id, product.id)); await db.update(issueWorkProducts).set({ status: "failed" }).where(eq(issueWorkProducts.id, product.id)); await expect(nativeCompletionFeedback(db, runId, doneReport([`work_product:${product.id}`]))) .rejects.toThrow("requested file has no accessible delivery evidence");