Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ deletion RPCs terminate the run's Workflow before removing its durable state.
Artifact messages persist only the output UUID and presentation metadata. The authenticated
`POST /v1/outputs/:outputId/download-url` path rechecks tenant ownership, retention, and R2
existence before minting a one-hour HMAC capability; the public signed download route is only the
streaming second hop. Expiring capabilities and internal R2 keys are never stored in transcripts or
returned by artifact tools.
streaming second hop. That route forwards single HTTP byte ranges to R2 and returns `206` metadata,
so browser media previews seek and start without downloading the entire artifact. Expiring
capabilities and internal R2 keys are never stored in transcripts or returned by artifact tools.

Browser screenshots use the same crash-consistent R2 persistence but are classified as internal
tool evidence. Their reserved filenames keep them out of project-file and slash-command catalogs,
Expand Down
51 changes: 43 additions & 8 deletions apps/agent-worker/src/agent-api-system-routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,21 +162,56 @@ async function downloadOutput(c: AgentContext): Promise<Response> {
});
}
const output = await findDownloadableOutput(c.env, outputId, query.userId);
const object = await c.env.R2_OUTPUTS.get(output.r2Key);
const object = await c.env.R2_OUTPUTS.get(output.r2Key, {
range: c.req.raw.headers,
});
if (!object?.body) {
throw new APIError(404, "resource_output_not_found", "Output object not found", {
retriable: false,
});
}
const headers = outputDownloadHeaders(output, object);
return new Response(object.body, {
headers: {
"Cache-Control": "private, max-age=0, no-store",
"Content-Disposition": downloadContentDisposition(output.filename),
"Content-Type": output.mimeType,
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
},
headers,
status: object.range ? 206 : 200,
});
}

function outputDownloadHeaders(
output: { filename: string; mimeType: string },
object: R2ObjectBody,
): Headers {
const headers = new Headers({
"Accept-Ranges": "bytes",
"Cache-Control": "private, max-age=0, no-store",
"Content-Disposition": downloadContentDisposition(output.filename),
"Content-Type": output.mimeType,
"Cross-Origin-Resource-Policy": "cross-origin",
ETag: object.httpEtag,
"Referrer-Policy": "no-referrer",
"X-Content-Type-Options": "nosniff",
});
const range = resolveOutputRange(object.range, object.size);
headers.set("Content-Length", String(range?.length ?? object.size));
if (range) {
headers.set("Content-Range", `bytes ${range.offset}-${range.end}/${object.size}`);
}
return headers;
}

function resolveOutputRange(
range: R2Range | undefined,
objectSize: number,
): { end: number; length: number; offset: number } | undefined {
if (!range) return undefined;
if ("suffix" in range) {
const length = Math.min(range.suffix, objectSize);
const offset = objectSize - length;
return { end: objectSize - 1, length, offset };
}
const offset = range.offset ?? 0;
const boundedLength = Math.min(range.length ?? objectSize - offset, objectSize - offset);
return { end: offset + boundedLength - 1, length: boundedLength, offset };
}

function parseOutputId(value: string | undefined): string {
Expand Down
4 changes: 3 additions & 1 deletion apps/gateway-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@ Public Clerk credentials, cookies, proxy credentials, plaintext idempotency keys
caller-supplied `X-Cheatcode-*` headers terminate at the gateway. Normal service-binding
requests receive only gateway-minted internal identity/idempotency headers. Artifact downloads
use that boundary to mint an owner-checked short-lived URL; only the resulting HMAC-bound
streaming URL is public. Local preview traffic has a separate, explicit capability/cookie bridge.
streaming URL is public. The gateway preserves the download response's explicit cross-origin
resource policy for browser media while defaulting every other response to `same-origin`.
Local preview traffic has a separate, explicit capability/cookie bridge.

Composio account sync follows provider cursors instead of treating the first
page as complete, and fails closed if a user exceeds the 1,000-account safety
Expand Down
7 changes: 7 additions & 0 deletions apps/gateway-worker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ const GATEWAY_SECURITY_HEADERS = {
styleSrc: ["'self'", "'unsafe-inline'"],
workerSrc: ["'none'"],
},
crossOriginResourcePolicy: false,
referrerPolicy: "strict-origin-when-cross-origin",
strictTransportSecurity: "max-age=31536000; includeSubDomains; preload",
xFrameOptions: "DENY",
Expand All @@ -88,6 +89,12 @@ gatewayApp.onError((error, context) => {
});

gatewayApp.use("*", secureHeaders(GATEWAY_SECURITY_HEADERS));
gatewayApp.use("*", async (c, next) => {
await next();
if (!c.res.headers.has("Cross-Origin-Resource-Policy")) {
c.res.headers.set("Cross-Origin-Resource-Policy", "same-origin");
}
});
gatewayApp.use("/v1/*", async (c, next) => {
let handle: DatabaseHandle | undefined;
c.set("database", () => {
Expand Down