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
10 changes: 5 additions & 5 deletions service/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# File Server Dockerfile
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /app

# Install dependencies
Expand Down Expand Up @@ -27,15 +27,15 @@ RUN bun build ./src/worker-server.ts --minify --outdir .build-worker --target bu
RUN bun build ./src/egress-gateway.ts --minify --outdir .build-egress-gateway --target bun --external '@opentelemetry/*'

# File server production
FROM oven/bun:1.3.14 AS production
FROM oven/bun:1.4.2 AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=install /temp/prod/node_modules ./node_modules
COPY --from=builder /app/.build ./.build
CMD ["bun", "run", ".build/file-server.js"]

# API server (HTTP on port 3112)
FROM oven/bun:1.3.14 AS api
FROM oven/bun:1.4.2 AS api
ENV NODE_ENV=production
WORKDIR /app
COPY --from=install /temp/prod/node_modules ./node_modules
Expand All @@ -45,7 +45,7 @@ COPY --from=builder /app/src/*.py ./src/
CMD ["bun", "run", ".build-api/api-server.js"]

# Worker server (job processor, health on port 3113)
FROM oven/bun:1.3.14 AS worker
FROM oven/bun:1.4.2 AS worker
ENV NODE_ENV=production
WORKDIR /app
COPY --from=install /temp/prod/node_modules ./node_modules
Expand All @@ -54,7 +54,7 @@ COPY --from=builder /app/src/*.py ./src/
CMD ["bun", "run", ".build-worker/worker-server.js"]

# Egress gateway (sandbox outbound delegation)
FROM oven/bun:1.3.14 AS egress-gateway
FROM oven/bun:1.4.2 AS egress-gateway
ENV NODE_ENV=production
WORKDIR /app
COPY --from=install /temp/prod/node_modules ./node_modules
Expand Down
4 changes: 2 additions & 2 deletions service/Dockerfile.api
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# API-Only Server Dockerfile
# This builds the HTTP API server without workers
# Scale this based on HTTP traffic
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /app

# Install dependencies
Expand All @@ -27,7 +27,7 @@ RUN bun build ./src/api-server.ts --minify --outdir .build --target bun --extern
RUN bun build ./scripts/rehydrate-session-cache.ts --minify --outdir .build-migrations --target bun --external '@opentelemetry/*'

# Production stage
FROM oven/bun:1.3.14 AS production
FROM oven/bun:1.4.2 AS production
ENV NODE_ENV=production
WORKDIR /app
# Install curl for healthcheck (not included in bun base image)
Expand Down
2 changes: 1 addition & 1 deletion service/Dockerfile.bun
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Base stage
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /usr/src/app

# Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions service/Dockerfile.egress-gateway
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Egress Gateway Dockerfile
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /app

FROM base AS install
Expand All @@ -11,7 +11,7 @@ RUN mkdir -p /temp/prod
COPY service/package.json service/bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production

FROM oven/bun:1.3.14 AS production
FROM oven/bun:1.4.2 AS production
ENV NODE_ENV=production
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
Expand Down
4 changes: 2 additions & 2 deletions service/Dockerfile.local
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Local development Dockerfile - no authentication required
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /app

# Install dependencies
Expand All @@ -22,7 +22,7 @@ COPY service/tsconfig.json ./
RUN bun build ./src/local-api.ts --minify --outdir .build --target bun --external '@opentelemetry/*'

# Production stage
FROM oven/bun:1.3.14 AS production
FROM oven/bun:1.4.2 AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=install /temp/prod/node_modules ./node_modules
Expand Down
2 changes: 1 addition & 1 deletion service/Dockerfile.service
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Service API Dockerfile
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /usr/src/app

# Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions service/Dockerfile.tool-call-server
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Tool Call Server Dockerfile
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /app

# Install dependencies
Expand All @@ -13,7 +13,7 @@ COPY service/package.json service/bun.lock /temp/prod/
RUN cd /temp/prod && bun install --frozen-lockfile --production

# Production stage
FROM oven/bun:1.3.14 AS production
FROM oven/bun:1.4.2 AS production
ENV NODE_ENV=production
WORKDIR /app
COPY --from=install /temp/prod/node_modules ./node_modules
Expand Down
4 changes: 2 additions & 2 deletions service/Dockerfile.worker
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# This builds the job processing worker without HTTP server
# Deploy alongside a sandbox sidecar for execution
# Scale this based on queue depth
FROM oven/bun:1.3.14 AS base
FROM oven/bun:1.4.2 AS base
WORKDIR /app

# Install dependencies
Expand All @@ -25,7 +25,7 @@ COPY service/tsconfig.json ./
RUN bun build ./src/worker-server.ts --minify --outdir .build --target bun --external '@opentelemetry/*'

# Production stage
FROM oven/bun:1.3.14 AS production
FROM oven/bun:1.4.2 AS production
ENV NODE_ENV=production
WORKDIR /app
# Install curl for healthcheck
Expand Down
73 changes: 51 additions & 22 deletions service/src/service/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import busboy from 'busboy';
import { nanoid } from 'nanoid';
import { Router } from 'express';
import type { Response } from 'express';
import type { Readable } from 'stream';
import { Readable } from 'stream';
import type * as t from '../types';
import { checkServiceStartUp, checkServiceShutDown } from '../lifecycle';
import { sessionAuth } from '../middleware/auth';
Expand Down Expand Up @@ -42,6 +42,43 @@ const JOB_COMPLETION_WAIT_TIMEOUT_MS = jobCompletionWaitTimeoutMs(
);

const UPLOAD_TIMEOUT_MS = 30_000;

/**
* Streams one busboy file part to the file-server.
*
* Uses the global `fetch` rather than axios on purpose. axios routes a
* stream body through `node:http`'s `ClientRequest`, and on Bun (the
* runtime in `Dockerfile.api`) that client can drop the tail of a
* chunked request body: every byte is accepted by `write()`, `end()`
* is called after the last write, yet the peer receives 32 KiB-800 KiB
* less and the file-server stores a short object while reporting
* success (reproduced on Bun 1.3.10-1.3.14 with a 20 MiB upload; a
* 1 MiB upload is unaffected). Bun's native `fetch` and Node's undici
* stream the same body intact. busboy's `limits.fileSize` already caps
* the part, so no separate body-length guard is needed here.
*/
async function putFileToFileServer(
url: string,
file: Readable,
headers: Record<string, string>,
signal: AbortSignal,
): Promise<t.UploadResult> {
const response = await fetch(url, {
method: 'PUT',
headers,
body: Readable.toWeb(file) as unknown as ReadableStream,
signal,
/* Required by the WHATWG fetch spec for streamed request bodies. */
duplex: 'half',
} as RequestInit);
if (!response.ok) {
const detail = await response.text().catch(() => '');
throw new Error(
`file-server responded ${response.status}${detail ? `: ${detail.slice(0, 200)}` : ''}`,
);
}
return (await response.json()) as t.UploadResult;
}
/* Batch cap sized for skill-priming uploads: a single skill (e.g. pptx)
* can carry 60+ resource files including .xsd schemas, helper scripts,
* docs, and Python __init__.py markers. The previous cap of 20 silently
Expand Down Expand Up @@ -500,20 +537,16 @@ router.post('/upload', uploadLimiter, async (req: t.AuthenticatedRequest, res: R
recordSessionOwnership(connection, session_id, sessionKey)
.then(() => {
logger.info(`[${INSTANCE_ID}] Upload: Session ID: ${session_id} | User ID: ${userId} | Session key: ${sessionKey}`);
return axios.put<t.UploadResult>(
`${env.FILE_SERVER_URL}/sessions/${session_id}/objects/${fileId}`,
file,
{
headers: internalServiceHeaders(putHeaders),
maxBodyLength: planFileSize,
maxContentLength: planFileSize,
signal: abortController.signal,
},
);
return putFileToFileServer(
`${env.FILE_SERVER_URL}/sessions/${session_id}/objects/${fileId}`,
file,
internalServiceHeaders(putHeaders),
abortController.signal,
);
})
.then(response => {
.then(result => {
clearTimeout(uploadTimeout);
resolve(response.data);
resolve(result);
})
.catch(error => {
clearTimeout(uploadTimeout);
Expand Down Expand Up @@ -742,18 +775,14 @@ router.post('/upload/batch', uploadLimiter, async (req: t.AuthenticatedRequest,
logger.error(`[${INSTANCE_ID}] Batch upload file failed: ${filename} | Session: ${session_id}`, { error: message });
resolve({ status: 'error', filename, error: message });
};
const forwardFile = (): Promise<void> => axios.put<t.UploadResult>(
const forwardFile = (): Promise<void> => putFileToFileServer(
`${env.FILE_SERVER_URL}/sessions/${session_id}/objects/${fileId}`,
file,
{
headers: internalServiceHeaders(putHeaders),
maxBodyLength: planFileSize,
maxContentLength: planFileSize,
signal: abortController.signal,
},
).then(response => {
internalServiceHeaders(putHeaders),
abortController.signal,
).then(result => {
clearTimeout(uploadTimeout);
resolve({ status: 'success', filename: response.data.filename, fileId: response.data.fileId });
resolve({ status: 'success', filename: result.filename, fileId: result.fileId });
}, resolveUploadFailure);

void ensureSessionRegistered(sessionKey)
Expand Down