diff --git a/README.md b/README.md index 42857b53c..2c065d8dd 100644 --- a/README.md +++ b/README.md @@ -190,7 +190,6 @@ Key environment variables (server) from packages/platform-server/.env.example an - DOCKER_MIRROR_URL (default http://registry-mirror:5000) - DOCKER_RUNNER_GRPC_HOST (default docker-runner) - DOCKER_RUNNER_GRPC_PORT (default 50051; DOCKER_RUNNER_PORT is accepted as an alias) - - DOCKER_RUNNER_SHARED_SECRET (required HMAC credential) - DOCKER_RUNNER_TIMEOUT_MS (optional request timeout; default 30000) - DOCKER_RUNNER_OPTIONAL (default true; set to false to keep fail-fast bootstrap) - DOCKER_RUNNER_CONNECT_RETRY_BASE_DELAY_MS (default 500) diff --git a/docs/product-spec.md b/docs/product-spec.md index 5d11fe11e..dca9515a0 100644 --- a/docs/product-spec.md +++ b/docs/product-spec.md @@ -117,7 +117,7 @@ Configuration matrix (server env vars) - VAULT_ENABLED: true|false (default false) - VAULT_ADDR, VAULT_TOKEN - DOCKER_MIRROR_URL (default http://registry-mirror:5000) - - DOCKER_RUNNER_GRPC_HOST, DOCKER_RUNNER_GRPC_PORT (or DOCKER_RUNNER_PORT), DOCKER_RUNNER_SHARED_SECRET (required for docker-runner), plus optional DOCKER_RUNNER_TIMEOUT_MS (default 30000), DOCKER_RUNNER_OPTIONAL (default true; set false to fail-fast), and DOCKER_RUNNER_CONNECT_* knobs (RETRY_BASE_DELAY_MS=500, RETRY_MAX_DELAY_MS=30000, RETRY_JITTER_MS=250, PROBE_INTERVAL_MS=30000, MAX_RETRIES=0 for unlimited background retries). + - DOCKER_RUNNER_GRPC_HOST, DOCKER_RUNNER_GRPC_PORT (or DOCKER_RUNNER_PORT), plus optional DOCKER_RUNNER_TIMEOUT_MS (default 30000), DOCKER_RUNNER_OPTIONAL (default true; set false to fail-fast), and DOCKER_RUNNER_CONNECT_* knobs (RETRY_BASE_DELAY_MS=500, RETRY_MAX_DELAY_MS=30000, RETRY_JITTER_MS=250, PROBE_INTERVAL_MS=30000, MAX_RETRIES=0 for unlimited background retries). - MCP_TOOLS_STALE_TIMEOUT_MS - LANGGRAPH_CHECKPOINTER: postgres (default) - POSTGRES_URL (postgres connection string) @@ -133,7 +133,7 @@ HTTP API and sockets (pointers) Runbooks - Local dev - Prereqs: Node 18+, pnpm, Docker, Postgres. -- Set: LITELLM_BASE_URL, LITELLM_MASTER_KEY (LLM_PROVIDER optional; defaults to litellm; only `openai` is also accepted), GITHUB_*, GH_TOKEN, AGENTS_DATABASE_URL, DOCKER_RUNNER_GRPC_HOST, DOCKER_RUNNER_GRPC_PORT (or DOCKER_RUNNER_PORT), DOCKER_RUNNER_SHARED_SECRET. Optional VAULT_* and DOCKER_MIRROR_URL. +- Set: LITELLM_BASE_URL, LITELLM_MASTER_KEY (LLM_PROVIDER optional; defaults to litellm; only `openai` is also accepted), GITHUB_*, GH_TOKEN, AGENTS_DATABASE_URL, DOCKER_RUNNER_GRPC_HOST, DOCKER_RUNNER_GRPC_PORT (or DOCKER_RUNNER_PORT). Optional VAULT_* and DOCKER_MIRROR_URL. - Start deps (compose or local Postgres) - Server: pnpm -w -F @agyn/platform-server dev - UI: pnpm -w -F @agyn/platform-ui dev diff --git a/docs/technical-overview.md b/docs/technical-overview.md index 78298bd9e..a9f1c9614 100644 --- a/docs/technical-overview.md +++ b/docs/technical-overview.md @@ -68,7 +68,7 @@ Per-workspace Docker-in-Docker and registry mirror Remote Docker runner - The platform-server always routes container lifecycle, exec, and log streaming calls through the external docker-runner service (not part of this monorepo). -- The runner exposes authenticated gRPC endpoints; every request includes HMAC metadata derived solely from `DOCKER_RUNNER_SHARED_SECRET`. +- The runner exposes gRPC endpoints; platform-server currently sends no auth metadata. - Only the docker-runner service mounts `/var/run/docker.sock` in default stacks; platform-server and auxiliary services talk to it over the internal network (default `docker-runner:${DOCKER_RUNNER_GRPC_PORT}` with `DOCKER_RUNNER_GRPC_PORT` defaulting to 50051; `DOCKER_RUNNER_PORT` remains an accepted alias). - Container events, logs, and exec streams flow over long-lived gRPC streams so the existing watcher pipeline (ContainerEventProcessor, cleanup jobs, metrics) remains unchanged. - Connectivity is tracked by a background `DockerRunnerConnectivityMonitor` that probes the gRPC `Ready` method with exponential backoff (base-delay, max-delay, jitter, probe interval, and optional retry cap are configurable via DOCKER_RUNNER_CONNECT_* env vars). diff --git a/packages/platform-server/.env.example b/packages/platform-server/.env.example index 7007ca825..255816b57 100644 --- a/packages/platform-server/.env.example +++ b/packages/platform-server/.env.example @@ -39,11 +39,10 @@ VAULT_TOKEN=dev-root # (Slack env removed intentionally; no global Slack config or tokens) -# docker-runner gRPC endpoint and credentials (required) +# docker-runner gRPC endpoint (required) DOCKER_RUNNER_GRPC_HOST=docker-runner DOCKER_RUNNER_GRPC_PORT=7171 # DOCKER_RUNNER_PORT=7171 # legacy alias still accepted -DOCKER_RUNNER_SHARED_SECRET=dev-shared-secret # Optional overrides # DOCKER_RUNNER_TIMEOUT_MS=30000 # DOCKER_RUNNER_CONNECT_MAX_RETRIES=0 diff --git a/packages/platform-server/__e2e__/app.bootstrap.smoke.test.ts b/packages/platform-server/__e2e__/app.bootstrap.smoke.test.ts index 17badd536..a7c34b575 100644 --- a/packages/platform-server/__e2e__/app.bootstrap.smoke.test.ts +++ b/packages/platform-server/__e2e__/app.bootstrap.smoke.test.ts @@ -351,7 +351,6 @@ describe('App bootstrap smoke test', () => { DOCKER_RUNNER_GRPC_HOST: process.env.DOCKER_RUNNER_GRPC_HOST, DOCKER_RUNNER_PORT: process.env.DOCKER_RUNNER_PORT, DOCKER_RUNNER_GRPC_PORT: process.env.DOCKER_RUNNER_GRPC_PORT, - DOCKER_RUNNER_SHARED_SECRET: process.env.DOCKER_RUNNER_SHARED_SECRET, DOCKER_RUNNER_OPTIONAL: process.env.DOCKER_RUNNER_OPTIONAL, VOLUME_GC_ENABLED: process.env.VOLUME_GC_ENABLED, VOLUME_GC_INTERVAL_MS: process.env.VOLUME_GC_INTERVAL_MS, @@ -361,7 +360,6 @@ describe('App bootstrap smoke test', () => { process.env.DOCKER_RUNNER_GRPC_HOST = '127.0.0.1'; process.env.DOCKER_RUNNER_PORT = '59999'; delete process.env.DOCKER_RUNNER_GRPC_PORT; - process.env.DOCKER_RUNNER_SHARED_SECRET = 'shared-secret'; process.env.DOCKER_RUNNER_OPTIONAL = 'true'; process.env.VOLUME_GC_ENABLED = 'true'; process.env.VOLUME_GC_INTERVAL_MS = '25'; diff --git a/packages/platform-server/__e2e__/bootstrap.di.test.ts b/packages/platform-server/__e2e__/bootstrap.di.test.ts index 76dae0e74..618290ec5 100644 --- a/packages/platform-server/__e2e__/bootstrap.di.test.ts +++ b/packages/platform-server/__e2e__/bootstrap.di.test.ts @@ -26,7 +26,6 @@ const REQUIRED_ENV = { process.env.AGENTS_DATABASE_URL ?? 'postgresql://postgres:postgres@127.0.0.1:5432/agents_test?schema=public', DOCKER_RUNNER_GRPC_HOST: '127.0.0.1', DOCKER_RUNNER_PORT: '59999', - DOCKER_RUNNER_SHARED_SECRET: 'dev-shared-secret', DOCKER_RUNNER_OPTIONAL: 'true', CONTAINERS_CLEANUP_ENABLED: 'false', VOLUME_GC_ENABLED: 'false', diff --git a/packages/platform-server/__tests__/containers.delete.docker.integration.test.ts b/packages/platform-server/__tests__/containers.delete.docker.integration.test.ts index e991137d8..0475d7ea4 100644 --- a/packages/platform-server/__tests__/containers.delete.docker.integration.test.ts +++ b/packages/platform-server/__tests__/containers.delete.docker.integration.test.ts @@ -17,10 +17,8 @@ import { PrismaClient as Prisma } from '@prisma/client'; import { DEFAULT_SOCKET, - RUNNER_SECRET, hasTcpDocker, runnerAddressMissing, - runnerSecretMissing, socketMissing, startDockerRunner, startPostgres, @@ -33,7 +31,7 @@ import { Reflect.defineMetadata('design:paramtypes', [PrismaService, ContainerAdminService, ConfigService], ContainersController); Reflect.defineMetadata('design:paramtypes', [Object, ContainerRegistry], ContainerAdminService); -const shouldSkip = process.env.SKIP_DOCKER_DELETE_E2E === '1' || runnerAddressMissing || runnerSecretMissing; +const shouldSkip = process.env.SKIP_DOCKER_DELETE_E2E === '1' || runnerAddressMissing; const describeOrSkip = shouldSkip || (socketMissing && !hasTcpDocker) ? describe.skip : describe; @@ -80,7 +78,7 @@ describeOrSkip('DELETE /api/containers/:id docker runner integration', () => { await registry.ensureIndexes(); runner = await startDockerRunner(); - dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress, sharedSecret: RUNNER_SECRET }); + dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress }); const moduleRef = await Test.createTestingModule({ controllers: [ContainersController], @@ -290,7 +288,7 @@ describeOrSkip('DELETE /api/containers/:id docker runner external process integr await registry.ensureIndexes(); runner = await startDockerRunner(); - dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress, sharedSecret: RUNNER_SECRET }); + dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress }); const moduleRef = await Test.createTestingModule({ controllers: [ContainersController], diff --git a/packages/platform-server/__tests__/containers.delete.integration.test.ts b/packages/platform-server/__tests__/containers.delete.integration.test.ts index 0fd7a3b56..372e398c4 100644 --- a/packages/platform-server/__tests__/containers.delete.integration.test.ts +++ b/packages/platform-server/__tests__/containers.delete.integration.test.ts @@ -278,7 +278,6 @@ describe('ContainersController wiring via InfraModule', () => { beforeAll(async () => { registerTestConfig({ - dockerRunnerSharedSecret: 'runner-secret', dockerRunnerGrpcHost: 'runner-grpc.test', dockerRunnerGrpcPort: 9091, agentsDatabaseUrl: 'postgresql://postgres:postgres@localhost:5432/agents_test', diff --git a/packages/platform-server/__tests__/containers.fullstack.docker.integration.test.ts b/packages/platform-server/__tests__/containers.fullstack.docker.integration.test.ts index 3290ba5b1..0c8549979 100644 --- a/packages/platform-server/__tests__/containers.fullstack.docker.integration.test.ts +++ b/packages/platform-server/__tests__/containers.fullstack.docker.integration.test.ts @@ -19,10 +19,8 @@ import { RunnerGrpcClient, DockerRunnerRequestError } from '../src/infra/contain import { DEFAULT_SOCKET, - RUNNER_SECRET, hasTcpDocker, runnerAddressMissing, - runnerSecretMissing, socketMissing, startDockerRunner, startPostgres, @@ -32,7 +30,7 @@ import { type PostgresHandle, } from './helpers/docker.e2e'; -const shouldSkip = process.env.SKIP_PLATFORM_FULLSTACK_E2E === '1' || runnerAddressMissing || runnerSecretMissing; +const shouldSkip = process.env.SKIP_PLATFORM_FULLSTACK_E2E === '1' || runnerAddressMissing; const describeOrSkip = shouldSkip || (socketMissing && !hasTcpDocker) ? describe.skip : describe; const TEST_IMAGE = 'nginx:1.25-alpine'; @@ -84,12 +82,11 @@ describeOrSkip('workspace create → delete full-stack flow', () => { await runPrismaMigrations(dbHandle.connectionString); runner = await startDockerRunner(); - dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress, sharedSecret: RUNNER_SECRET }); + dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress }); clearTestConfig(); const [grpcHost, grpcPort] = runner.grpcAddress.split(':'); configService = registerTestConfig({ - dockerRunnerSharedSecret: RUNNER_SECRET, dockerRunnerGrpcHost: grpcHost ?? '127.0.0.1', dockerRunnerGrpcPort: grpcPort ? Number(grpcPort) : undefined, agentsDatabaseUrl: dbHandle.connectionString, diff --git a/packages/platform-server/__tests__/helpers/config.ts b/packages/platform-server/__tests__/helpers/config.ts index 291d2dc68..b662e06f8 100644 --- a/packages/platform-server/__tests__/helpers/config.ts +++ b/packages/platform-server/__tests__/helpers/config.ts @@ -1,7 +1,6 @@ import { ConfigService, configSchema } from '../../src/core/services/config.service'; export const runnerConfigDefaults = { - dockerRunnerSharedSecret: 'test-shared-secret', dockerRunnerGrpcHost: '127.0.0.1', dockerRunnerGrpcPort: 50051, litellmKeyAlias: 'agents/test/local', diff --git a/packages/platform-server/__tests__/helpers/docker.e2e.ts b/packages/platform-server/__tests__/helpers/docker.e2e.ts index 570ab2eb3..908f10a0e 100644 --- a/packages/platform-server/__tests__/helpers/docker.e2e.ts +++ b/packages/platform-server/__tests__/helpers/docker.e2e.ts @@ -6,12 +6,9 @@ import { spawn } from 'node:child_process'; import { setTimeout as sleep } from 'node:timers/promises'; import { credentials, Metadata } from '@grpc/grpc-js'; import { create } from '@bufbuild/protobuf'; - -import { NonceCache, buildAuthHeaders } from '../../src/infra/container/auth'; -import { RunnerServiceGrpcClient, RUNNER_SERVICE_READY_PATH } from '../../src/proto/grpc.js'; +import { RunnerServiceGrpcClient } from '../../src/proto/grpc.js'; import { ReadyRequestSchema } from '../../src/proto/gen/agynio/api/runner/v1/runner_pb.js'; -export const RUNNER_SECRET = process.env.DOCKER_RUNNER_SHARED_SECRET ?? ''; export const DEFAULT_SOCKET = process.env.DOCKER_SOCKET ?? '/var/run/docker.sock'; export const hasTcpDocker = Boolean(process.env.DOCKER_HOST); export const socketMissing = !fs.existsSync(DEFAULT_SOCKET); @@ -20,8 +17,6 @@ const runnerPort = process.env.DOCKER_RUNNER_GRPC_PORT ?? process.env.DOCKER_RUN export const runnerAddress = process.env.DOCKER_RUNNER_GRPC_ADDRESS ?? (runnerHost && runnerPort ? `${runnerHost}:${runnerPort}` : undefined); export const runnerAddressMissing = !runnerAddress; -export const runnerSecretMissing = !RUNNER_SECRET; -const readinessNonceCache = new NonceCache(); export type RunnerHandle = { grpcAddress: string; @@ -34,20 +29,20 @@ export type PostgresHandle = { }; export async function startDockerRunner(): Promise { - if (!runnerAddress || !RUNNER_SECRET) { - throw new Error('DOCKER_RUNNER_GRPC_ADDRESS and DOCKER_RUNNER_SHARED_SECRET are required to run docker e2e tests.'); + if (!runnerAddress) { + throw new Error('DOCKER_RUNNER_GRPC_ADDRESS is required to run docker e2e tests.'); } - await waitForRunnerReadyOnAddress(runnerAddress, RUNNER_SECRET); + await waitForRunnerReadyOnAddress(runnerAddress); return { grpcAddress: runnerAddress, close: async () => undefined, }; } -async function waitForRunnerReady(client: RunnerServiceGrpcClient, secret: string): Promise { +async function waitForRunnerReady(client: RunnerServiceGrpcClient): Promise { await waitFor(async () => { try { - await callRunnerReady(client, secret); + await callRunnerReady(client); return true; } catch { return false; @@ -55,18 +50,18 @@ async function waitForRunnerReady(client: RunnerServiceGrpcClient, secret: strin }, { timeoutMs: 30_000, intervalMs: 250 }); } -async function waitForRunnerReadyOnAddress(address: string, secret: string): Promise { +async function waitForRunnerReadyOnAddress(address: string): Promise { const client = new RunnerServiceGrpcClient(address, credentials.createInsecure()); try { - await waitForRunnerReady(client, secret); + await waitForRunnerReady(client); } finally { client.close(); } } -function callRunnerReady(client: RunnerServiceGrpcClient, secret: string): Promise { +function callRunnerReady(client: RunnerServiceGrpcClient): Promise { const request = create(ReadyRequestSchema, {}); - const metadata = authMetadata(secret, RUNNER_SERVICE_READY_PATH); + const metadata = new Metadata(); return new Promise((resolve, reject) => { client.ready(request, metadata, (err) => { if (err) { @@ -78,17 +73,6 @@ function callRunnerReady(client: RunnerServiceGrpcClient, secret: string): Promi }); } -function authMetadata(secret: string, path: string): Metadata { - const nonce = randomUUID(); - readinessNonceCache.add(nonce); - const headers = buildAuthHeaders({ method: 'POST', path, body: '', secret, nonce }); - const metadata = new Metadata(); - for (const [key, value] of Object.entries(headers)) { - metadata.set(key, value); - } - return metadata; -} - export async function startPostgres(): Promise { const containerName = `containers-pg-${randomUUID()}`; const port = await getAvailablePort(); diff --git a/packages/platform-server/__tests__/infra/runnerGrpc.client.test.ts b/packages/platform-server/__tests__/infra/runnerGrpc.client.test.ts index 43c5e843a..d0404bf91 100644 --- a/packages/platform-server/__tests__/infra/runnerGrpc.client.test.ts +++ b/packages/platform-server/__tests__/infra/runnerGrpc.client.test.ts @@ -2,8 +2,6 @@ import { describe, expect, it, vi } from 'vitest'; import { EventEmitter } from 'node:events'; import type { ClientDuplexStream } from '@grpc/grpc-js'; import { Metadata, status } from '@grpc/grpc-js'; -import { NonceCache, verifyAuthHeaders } from '../../src/infra/container/auth'; -import { RUNNER_SERVICE_TOUCH_WORKLOAD_PATH } from '../../src/proto/grpc.js'; import type { RunnerServiceGrpcClientInstance } from '../../src/proto/grpc.js'; import { @@ -21,8 +19,8 @@ class MockClientStream extends EventEmitter { } describe('RunnerGrpcClient', () => { - it('sends signed runner metadata on touchLastUsed calls', async () => { - const client = new RunnerGrpcClient({ address: 'grpc://runner', sharedSecret: 'test-secret' }); + it('sends empty runner metadata on touchLastUsed calls', async () => { + const client = new RunnerGrpcClient({ address: 'grpc://runner' }); const captured: { metadata?: Metadata } = {}; const touchStub = vi.fn((_: unknown, metadata: Metadata, maybeOptions?: unknown, maybeCallback?: (err: Error | null) => void) => { @@ -40,26 +38,11 @@ describe('RunnerGrpcClient', () => { expect(touchStub).toHaveBeenCalledTimes(1); expect(captured.metadata).toBeInstanceOf(Metadata); - - const headers: Record = {}; - const metadataMap = captured.metadata?.getMap() ?? {}; - for (const [key, value] of Object.entries(metadataMap)) { - headers[key] = Buffer.isBuffer(value) ? value.toString('utf8') : String(value); - } - - const verification = verifyAuthHeaders({ - headers, - method: 'POST', - path: RUNNER_SERVICE_TOUCH_WORKLOAD_PATH, - body: '', - secret: 'test-secret', - nonceCache: new NonceCache(), - }); - expect(verification.ok).toBe(true); + expect(Object.keys(captured.metadata?.getMap() ?? {})).toHaveLength(0); }); it('sanitizes infra details from gRPC errors', async () => { - const client = new RunnerGrpcClient({ address: 'grpc://runner', sharedSecret: 'secret' }); + const client = new RunnerGrpcClient({ address: 'grpc://runner' }); const error = Object.assign(new Error('Deadline exceeded after 305.002s,LB pick: 0.001s,remote_addr=172.21.0.3:50051'), { code: status.DEADLINE_EXCEEDED, details: 'Deadline exceeded after 305.002s,LB pick: 0.001s,remote_addr=172.21.0.3:50051', @@ -89,7 +72,6 @@ describe('RunnerGrpcExecClient', () => { ); const execClient = new RunnerGrpcExecClient({ address: 'grpc://runner', - sharedSecret: 'secret', client: { exec: execStub } as unknown as RunnerServiceGrpcClientInstance, }); diff --git a/packages/platform-server/__tests__/runner.exec.cancellation.docker.integration.test.ts b/packages/platform-server/__tests__/runner.exec.cancellation.docker.integration.test.ts index 409fe5150..b25f04ba8 100644 --- a/packages/platform-server/__tests__/runner.exec.cancellation.docker.integration.test.ts +++ b/packages/platform-server/__tests__/runner.exec.cancellation.docker.integration.test.ts @@ -4,16 +4,14 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { RunnerGrpcClient } from '../src/infra/container/runnerGrpc.client'; import { - RUNNER_SECRET, hasTcpDocker, runnerAddressMissing, - runnerSecretMissing, socketMissing, startDockerRunner, type RunnerHandle, } from './helpers/docker.e2e'; -const shouldSkip = process.env.SKIP_RUNNER_EXEC_E2E === '1' || runnerAddressMissing || runnerSecretMissing; +const shouldSkip = process.env.SKIP_RUNNER_EXEC_E2E === '1' || runnerAddressMissing; const describeOrSkip = shouldSkip || (socketMissing && !hasTcpDocker) ? describe.skip : describe; describeOrSkip('runner gRPC exec cancellation integration', () => { @@ -23,7 +21,7 @@ describeOrSkip('runner gRPC exec cancellation integration', () => { beforeAll(async () => { runner = await startDockerRunner(); - dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress, sharedSecret: RUNNER_SECRET }); + dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress }); }, 120_000); afterAll(async () => { diff --git a/packages/platform-server/__tests__/vitest.setup.ts b/packages/platform-server/__tests__/vitest.setup.ts index 5c6ff0a31..d1a0b1e70 100644 --- a/packages/platform-server/__tests__/vitest.setup.ts +++ b/packages/platform-server/__tests__/vitest.setup.ts @@ -3,4 +3,3 @@ import 'reflect-metadata'; process.env.LITELLM_BASE_URL ||= 'http://127.0.0.1:4000'; process.env.LITELLM_MASTER_KEY ||= 'sk-dev-master-1234'; process.env.CONTEXT_ITEM_NULL_GUARD ||= '0'; -process.env.DOCKER_RUNNER_SHARED_SECRET ||= 'test-shared-secret'; diff --git a/packages/platform-server/__tests__/workspace.exec.grpc.test.ts b/packages/platform-server/__tests__/workspace.exec.grpc.test.ts index 5b9ead1e3..5a80c02d8 100644 --- a/packages/platform-server/__tests__/workspace.exec.grpc.test.ts +++ b/packages/platform-server/__tests__/workspace.exec.grpc.test.ts @@ -4,15 +4,13 @@ import { RunnerGrpcClient } from '../src/infra/container/runnerGrpc.client'; import type { ContainerRegistry } from '../src/infra/container/container.registry'; import { DockerWorkspaceRuntimeProvider } from '../src/workspace/providers/docker.workspace.provider'; -const RUNNER_SECRET_OVERRIDE = process.env.DOCKER_RUNNER_SHARED_SECRET_OVERRIDE; -const RUNNER_SECRET = RUNNER_SECRET_OVERRIDE ?? process.env.DOCKER_RUNNER_SHARED_SECRET; const RUNNER_ADDRESS_OVERRIDE = process.env.DOCKER_RUNNER_GRPC_ADDRESS; const RUNNER_HOST = process.env.DOCKER_RUNNER_GRPC_HOST ?? process.env.DOCKER_RUNNER_HOST; const RUNNER_PORT = process.env.DOCKER_RUNNER_GRPC_PORT ?? process.env.DOCKER_RUNNER_PORT; const resolvedRunnerAddress = RUNNER_ADDRESS_OVERRIDE ?? (RUNNER_HOST && RUNNER_PORT ? `${RUNNER_HOST}:${RUNNER_PORT}` : undefined); -const shouldRunTests = Boolean(RUNNER_SECRET && resolvedRunnerAddress); +const shouldRunTests = Boolean(resolvedRunnerAddress); const TEST_IMAGE = 'ghcr.io/agynio/devcontainer:latest'; const THREAD_ID = `grpc-exec-${Date.now()}`; const TEST_TIMEOUT_MS = 30_000; @@ -30,7 +28,7 @@ const describeRunner = shouldRunTests ? describe : describe.skip; describeRunner('DockerWorkspaceRuntimeProvider exec over gRPC runner', () => { beforeAll(async () => { - runnerClient = new RunnerGrpcClient({ address: resolvedRunnerAddress!, sharedSecret: RUNNER_SECRET! }); + runnerClient = new RunnerGrpcClient({ address: resolvedRunnerAddress! }); provider = new DockerWorkspaceRuntimeProvider(runnerClient, registry); const ensure = await provider.ensureWorkspace( diff --git a/packages/platform-server/__tests__/workspace/workspace.reuse.integration.test.ts b/packages/platform-server/__tests__/workspace/workspace.reuse.integration.test.ts index d1e5a404a..5e34ffda3 100644 --- a/packages/platform-server/__tests__/workspace/workspace.reuse.integration.test.ts +++ b/packages/platform-server/__tests__/workspace/workspace.reuse.integration.test.ts @@ -12,10 +12,8 @@ import { ContainerRegistry } from '../../src/infra/container/container.registry' import { PrismaService } from '../../src/core/services/prisma.service'; import { registerTestConfig, clearTestConfig } from '../helpers/config'; import { - RUNNER_SECRET, hasTcpDocker, runnerAddressMissing, - runnerSecretMissing, socketMissing, startDockerRunner, startPostgres, @@ -24,7 +22,7 @@ import { type PostgresHandle, } from '../helpers/docker.e2e'; -const shouldSkip = process.env.SKIP_WORKSPACE_REUSE_E2E === '1' || runnerAddressMissing || runnerSecretMissing; +const shouldSkip = process.env.SKIP_WORKSPACE_REUSE_E2E === '1' || runnerAddressMissing; const describeOrSkip = shouldSkip || (socketMissing && !hasTcpDocker) ? describe.skip : describe.sequential; describeOrSkip('Docker workspace reuse lifecycle', () => { @@ -44,12 +42,11 @@ describeOrSkip('Docker workspace reuse lifecycle', () => { await runPrismaMigrations(dbHandle.connectionString); runner = await startDockerRunner(); - dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress, sharedSecret: RUNNER_SECRET }); + dockerClient = new RunnerGrpcClient({ address: runner.grpcAddress }); clearTestConfig(); const [grpcHost, grpcPort] = runner.grpcAddress.split(':'); const configService = registerTestConfig({ - dockerRunnerSharedSecret: RUNNER_SECRET, dockerRunnerGrpcHost: grpcHost ?? '127.0.0.1', dockerRunnerGrpcPort: grpcPort ? Number(grpcPort) : undefined, agentsDatabaseUrl: dbHandle.connectionString, diff --git a/packages/platform-server/src/core/services/config.service.ts b/packages/platform-server/src/core/services/config.service.ts index 8b33b90bb..44b97ef83 100644 --- a/packages/platform-server/src/core/services/config.service.ts +++ b/packages/platform-server/src/core/services/config.service.ts @@ -63,10 +63,6 @@ export const configSchema = z.object({ vaultToken: z.string().optional(), // Docker registry mirror URL (used by DinD sidecar) dockerMirrorUrl: z.string().min(1).default('http://registry-mirror:5000'), - dockerRunnerSharedSecret: z - .string() - .min(1, 'DOCKER_RUNNER_SHARED_SECRET is required') - .transform((value) => value.trim()), dockerRunnerTimeoutMs: z .union([z.string(), z.number()]) .default('30000') @@ -383,10 +379,6 @@ export class ConfigService implements Config { return this.params.dockerMirrorUrl; } - get dockerRunnerSharedSecret(): string { - return this.params.dockerRunnerSharedSecret; - } - get dockerRunnerTimeoutMs(): number { return this.params.dockerRunnerTimeoutMs; } @@ -431,10 +423,6 @@ export class ConfigService implements Config { return `${this.dockerRunnerGrpcHost}:${this.dockerRunnerGrpcPort}`; } - getDockerRunnerSharedSecret(): string { - return this.dockerRunnerSharedSecret; - } - getDockerRunnerTimeoutMs(): number { return this.dockerRunnerTimeoutMs; } @@ -584,7 +572,6 @@ export class ConfigService implements Config { vaultAddr: process.env.VAULT_ADDR, vaultToken: process.env.VAULT_TOKEN, dockerMirrorUrl: process.env.DOCKER_MIRROR_URL, - dockerRunnerSharedSecret: process.env.DOCKER_RUNNER_SHARED_SECRET, dockerRunnerTimeoutMs: process.env.DOCKER_RUNNER_TIMEOUT_MS, dockerRunnerGrpcHost: process.env.DOCKER_RUNNER_GRPC_HOST, dockerRunnerGrpcPort: dockerRunnerPortEnv, diff --git a/packages/platform-server/src/infra/container/auth.ts b/packages/platform-server/src/infra/container/auth.ts deleted file mode 100644 index 8dbebc5e0..000000000 --- a/packages/platform-server/src/infra/container/auth.ts +++ /dev/null @@ -1,165 +0,0 @@ -import crypto from 'node:crypto'; -import { canonicalJsonStringify } from './json'; - -export type SignatureHeaders = { - timestamp: string; - nonce: string; - signature: string; -}; - -const HEADER_TIMESTAMP = 'x-dr-timestamp'; -const HEADER_NONCE = 'x-dr-nonce'; -const HEADER_SIGNATURE = 'x-dr-signature'; - -export const REQUIRED_HEADERS = [HEADER_TIMESTAMP, HEADER_NONCE, HEADER_SIGNATURE]; - -export function hashBody(body: string | Buffer): string { - const data = typeof body === 'string' ? Buffer.from(body) : body; - return crypto.createHash('sha256').update(data).digest('base64'); -} - -export function buildSignaturePayload(parts: { - method: string; - path: string; - timestamp: string; - nonce: string; - bodyHash: string; -}): string { - return `${parts.method.toUpperCase()}\n${parts.path}\n${parts.timestamp}\n${parts.nonce}\n${parts.bodyHash}`; -} - -export function signPayload(secret: string, payload: string): string { - return crypto.createHmac('sha256', secret).update(payload).digest('base64'); -} - -export function canonicalBodyString(body: unknown): string { - if (body === undefined || body === null || body === '') return ''; - if (typeof body === 'string') return body; - return canonicalJsonStringify(body); -} - -export type NonceCacheOptions = { - ttlMs?: number; - maxEntries?: number; -}; - -export class NonceCache { - private readonly ttlMs: number; - private readonly maxEntries: number; - private readonly store = new Map(); - - constructor(options: NonceCacheOptions = {}) { - this.ttlMs = typeof options.ttlMs === 'number' ? options.ttlMs : 60_000; - this.maxEntries = typeof options.maxEntries === 'number' ? options.maxEntries : 1000; - } - - has(nonce: string): boolean { - this.evictExpired(); - return this.store.has(nonce); - } - - add(nonce: string): void { - this.evictExpired(); - if (this.store.size >= this.maxEntries) { - const [firstKey] = this.store.keys(); - if (firstKey) this.store.delete(firstKey); - } - this.store.set(nonce, Date.now()); - } - - private evictExpired(): void { - const now = Date.now(); - for (const [nonce, ts] of this.store.entries()) { - if (now - ts > this.ttlMs) this.store.delete(nonce); - } - } -} - -export type BuildHeadersInput = { - method: string; - path: string; - body?: unknown; - secret: string; - timestamp?: number; - nonce?: string; -}; - -export function buildAuthHeaders(input: BuildHeadersInput): Record { - const timestamp = (input.timestamp ?? Date.now()).toString(); - const nonce = input.nonce ?? crypto.randomUUID(); - const bodyString = canonicalBodyString(input.body ?? ''); - const bodyHash = hashBody(bodyString); - const payload = buildSignaturePayload({ - method: input.method, - path: input.path, - timestamp, - nonce, - bodyHash, - }); - const signature = signPayload(input.secret, payload); - return { - [HEADER_TIMESTAMP]: timestamp, - [HEADER_NONCE]: nonce, - [HEADER_SIGNATURE]: signature, - }; -} - -export type VerifyHeadersInput = { - headers: Record; - method: string; - path: string; - body?: unknown; - secret: string; - clockSkewMs?: number; - nonceCache: NonceCache; -}; - -export function extractHeader(headers: VerifyHeadersInput['headers'], name: string): string | undefined { - const value = headers[name] ?? headers[name.toLowerCase()]; - if (Array.isArray(value)) return value[0]; - return value as string | undefined; -} - -export function verifyAuthHeaders(input: VerifyHeadersInput): { ok: boolean; code?: string; message?: string } { - const clockSkewMs = typeof input.clockSkewMs === 'number' ? input.clockSkewMs : 60_000; - const timestampStr = extractHeader(input.headers, HEADER_TIMESTAMP); - const nonce = extractHeader(input.headers, HEADER_NONCE); - const signature = extractHeader(input.headers, HEADER_SIGNATURE); - if (!timestampStr || !nonce || !signature) { - return { ok: false, code: 'missing_headers', message: 'Authentication headers missing' }; - } - const timestampNum = Number(timestampStr); - if (!Number.isFinite(timestampNum)) { - return { ok: false, code: 'invalid_timestamp', message: 'Timestamp invalid' }; - } - const now = Date.now(); - if (Math.abs(now - timestampNum) > clockSkewMs) { - return { ok: false, code: 'timestamp_out_of_range', message: 'Timestamp outside allowed skew' }; - } - if (input.nonceCache.has(nonce)) { - return { ok: false, code: 'replayed_nonce', message: 'Nonce already used' }; - } - const bodyString = canonicalBodyString(input.body ?? ''); - const bodyHash = hashBody(bodyString); - const payload = buildSignaturePayload({ - method: input.method, - path: input.path, - timestamp: timestampStr, - nonce, - bodyHash, - }); - const expectedSignature = signPayload(input.secret, payload); - if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) { - return { ok: false, code: 'invalid_signature', message: 'Signature mismatch' }; - } - input.nonceCache.add(nonce); - return { ok: true }; -} - -export function headerNames() { - return { - timestamp: HEADER_TIMESTAMP, - nonce: HEADER_NONCE, - signature: HEADER_SIGNATURE, - }; -} diff --git a/packages/platform-server/src/infra/container/json.ts b/packages/platform-server/src/infra/container/json.ts deleted file mode 100644 index 7f7c22b58..000000000 --- a/packages/platform-server/src/infra/container/json.ts +++ /dev/null @@ -1,23 +0,0 @@ -const isBufferLike = (val: unknown): val is Buffer | Uint8Array => - typeof Buffer !== 'undefined' && (Buffer.isBuffer(val) || val instanceof Uint8Array); - -export function canonicalize(value: unknown): unknown { - if (value instanceof Date) return value.toISOString(); - if (isBufferLike(value)) return Buffer.from(value).toString('base64'); - if (Array.isArray(value)) { - return value.map((item) => canonicalize(item)); - } - if (value && typeof value === 'object') { - const entries = Object.entries(value as Record); - return Object.fromEntries( - entries - .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)) - .map(([key, val]) => [key, canonicalize(val)]), - ); - } - return value; -} - -export function canonicalJsonStringify(value: unknown): string { - return JSON.stringify(canonicalize(value)); -} diff --git a/packages/platform-server/src/infra/container/runnerGrpc.client.ts b/packages/platform-server/src/infra/container/runnerGrpc.client.ts index b2fb0695a..0f2e9b24a 100644 --- a/packages/platform-server/src/infra/container/runnerGrpc.client.ts +++ b/packages/platform-server/src/infra/container/runnerGrpc.client.ts @@ -9,9 +9,7 @@ import { type ClientReadableStream, type ServiceError, } from '@grpc/grpc-js'; -import { create } from '@bufbuild/protobuf'; import { Logger } from '@nestjs/common'; -import { buildAuthHeaders } from './auth'; import { ContainerHandle } from './container.handle'; import type { ContainerInspectInfo, @@ -93,6 +91,7 @@ import { RUNNER_SERVICE_TOUCH_WORKLOAD_PATH, } from '../../proto/grpc.js'; import { containerOptsToStartWorkloadRequest } from './workload.grpc'; +import { createMessage } from '../proto.utils'; import { ExecIdleTimeoutError, ExecTimeoutError } from '../../utils/execTimeout'; import type { DockerClient } from './dockerClient.token'; @@ -148,29 +147,24 @@ export class DockerRunnerRequestError extends Error { type RunnerClientConfig = { address: string; - sharedSecret: string; requestTimeoutMs?: number; }; export class RunnerGrpcClient implements DockerClient { private readonly client: RunnerServiceGrpcClientInstance; private readonly execClient: RunnerGrpcExecClient; - private readonly sharedSecret: string; private readonly requestTimeoutMs: number; private readonly endpoint: string; private readonly logger = new Logger(RunnerGrpcClient.name); constructor(config: RunnerClientConfig) { if (!config.address) throw new Error('RunnerGrpcClient requires address'); - if (!config.sharedSecret) throw new Error('RunnerGrpcClient requires shared secret'); - this.sharedSecret = config.sharedSecret; this.requestTimeoutMs = config.requestTimeoutMs ?? 30_000; this.endpoint = config.address; this.client = new RunnerServiceGrpcClient(config.address, credentials.createInsecure()); this.execClient = new RunnerGrpcExecClient({ client: this.client, address: config.address, - sharedSecret: config.sharedSecret, defaultDeadlineMs: this.requestTimeoutMs, resolveTimeout: (options) => this.resolveExecRequestTimeout(options), logger: this.logger, @@ -182,7 +176,7 @@ export class RunnerGrpcClient implements DockerClient { } async checkConnectivity(): Promise<{ status: string }> { - const request = create(ReadyRequestSchema, {}); + const request = createMessage(ReadyRequestSchema, {}); const response = await this.unary( RUNNER_SERVICE_READY_PATH, request, @@ -195,7 +189,7 @@ export class RunnerGrpcClient implements DockerClient { } async touchLastUsed(containerId: string): Promise { - const request = create(TouchWorkloadRequestSchema, { workloadId: containerId }); + const request = createMessage(TouchWorkloadRequestSchema, { workloadId: containerId }); await this.unary( RUNNER_SERVICE_TOUCH_WORKLOAD_PATH, request, @@ -260,7 +254,7 @@ export class RunnerGrpcClient implements DockerClient { } async streamContainerLogs(containerId: string, options?: LogsStreamOptions): Promise { - const request = create(StreamWorkloadLogsRequestSchema, { + const request = createMessage(StreamWorkloadLogsRequestSchema, { workloadId: containerId, follow: options?.follow ?? true, since: this.normalizeSince(options?.since), @@ -314,7 +308,7 @@ export class RunnerGrpcClient implements DockerClient { } async stopContainer(containerId: string, timeoutSec = 10): Promise { - const request = create(StopWorkloadRequestSchema, { workloadId: containerId, timeoutSec }); + const request = createMessage(StopWorkloadRequestSchema, { workloadId: containerId, timeoutSec }); await this.unary( RUNNER_SERVICE_STOP_WORKLOAD_PATH, request, @@ -332,7 +326,7 @@ export class RunnerGrpcClient implements DockerClient { containerId: string, options?: boolean | { force?: boolean; removeVolumes?: boolean }, ): Promise { - const request = create(RemoveWorkloadRequestSchema, { + const request = createMessage(RemoveWorkloadRequestSchema, { workloadId: containerId, force: typeof options === 'boolean' ? options : options?.force ?? false, removeVolumes: typeof options === 'boolean' ? options : options?.removeVolumes ?? false, @@ -351,7 +345,7 @@ export class RunnerGrpcClient implements DockerClient { } async getContainerLabels(containerId: string): Promise | undefined> { - const request = create(GetWorkloadLabelsRequestSchema, { workloadId: containerId }); + const request = createMessage(GetWorkloadLabelsRequestSchema, { workloadId: containerId }); const response = await this.unary( RUNNER_SERVICE_GET_WORKLOAD_LABELS_PATH, request, @@ -374,7 +368,7 @@ export class RunnerGrpcClient implements DockerClient { labels: Record, options?: { all?: boolean }, ): Promise { - const request = create(FindWorkloadsByLabelsRequestSchema, { + const request = createMessage(FindWorkloadsByLabelsRequestSchema, { labels, all: options?.all ?? false, }); @@ -391,7 +385,7 @@ export class RunnerGrpcClient implements DockerClient { } async listContainersByVolume(volumeName: string): Promise { - const request = create(ListWorkloadsByVolumeRequestSchema, { volumeName }); + const request = createMessage(ListWorkloadsByVolumeRequestSchema, { volumeName }); const response = await this.unary( RUNNER_SERVICE_LIST_WORKLOADS_BY_VOLUME_PATH, request, @@ -404,7 +398,7 @@ export class RunnerGrpcClient implements DockerClient { } async removeVolume(volumeName: string, options?: { force?: boolean }): Promise { - const request = create(RemoveVolumeRequestSchema, { + const request = createMessage(RemoveVolumeRequestSchema, { volumeName, force: options?.force ?? false, }); @@ -435,7 +429,7 @@ export class RunnerGrpcClient implements DockerClient { options: { path: string }, ): Promise { const payload = await this.toBuffer(data); - const request = create(PutArchiveRequestSchema, { + const request = createMessage(PutArchiveRequestSchema, { workloadId: containerId, path: options.path, tarPayload: payload, @@ -455,7 +449,7 @@ export class RunnerGrpcClient implements DockerClient { } async inspectContainer(containerId: string): Promise { - const request = create(InspectWorkloadRequestSchema, { workloadId: containerId }); + const request = createMessage(InspectWorkloadRequestSchema, { workloadId: containerId }); const response = await this.unary( RUNNER_SERVICE_INSPECT_WORKLOAD_PATH, request, @@ -468,7 +462,7 @@ export class RunnerGrpcClient implements DockerClient { } async getEventsStream(options: { since?: number; filters?: DockerEventFilters }): Promise { - const request = create(StreamEventsRequestSchema, { + const request = createMessage(StreamEventsRequestSchema, { since: this.normalizeSince(options?.since), filters: this.toEventFilters(options?.filters), }); @@ -536,7 +530,7 @@ export class RunnerGrpcClient implements DockerClient { .map((value: unknown) => String(value)) .filter((value: string) => value.length > 0); if (!normalized.length) continue; - result.push(create(EventFilterSchema, { key, values: normalized })); + result.push(createMessage(EventFilterSchema, { key, values: normalized })); } return result; } @@ -572,13 +566,8 @@ export class RunnerGrpcClient implements DockerClient { return { deadline: new Date(Date.now() + timeout) }; } - private createMetadata(path: string): Metadata { - const headers = buildAuthHeaders({ method: 'POST', path, body: '', secret: this.sharedSecret }); - const metadata = new Metadata(); - for (const [key, value] of Object.entries(headers)) { - metadata.set(key, value); - } - return metadata; + private createMetadata(_path: string): Metadata { + return new Metadata(); } private translateServiceError(error: ServiceError, context?: { path?: string }): DockerRunnerRequestError { @@ -737,7 +726,6 @@ type ExecTimeoutResolver = (options?: Pick>(); @@ -745,14 +733,12 @@ export class RunnerGrpcExecClient { constructor(options: { address: string; - sharedSecret: string; defaultDeadlineMs?: number; resolveTimeout?: ExecTimeoutResolver; client?: RunnerServiceGrpcClientInstance; logger?: Logger; }) { this.client = options.client ?? new RunnerServiceGrpcClient(options.address, credentials.createInsecure()); - this.sharedSecret = options.sharedSecret; this.defaultDeadlineMs = options.defaultDeadlineMs; this.resolveTimeout = options.resolveTimeout; this.logger = options.logger; @@ -789,8 +775,8 @@ export class RunnerGrpcExecClient { stdinClosed = true; try { call.write( - create(ExecRequestSchema, { - msg: { case: 'stdin', value: create(ExecStdinSchema, { data: new Uint8Array(), eof: true }) }, + createMessage(ExecRequestSchema, { + msg: { case: 'stdin', value: createMessage(ExecStdinSchema, { data: new Uint8Array(), eof: true }) }, }), ); } catch { @@ -1068,8 +1054,8 @@ export class RunnerGrpcExecClient { const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding as BufferEncoding); if (buffer.length > 0) { call.write( - create(ExecRequestSchema, { - msg: { case: 'stdin', value: create(ExecStdinSchema, { data: buffer, eof: false }) }, + createMessage(ExecRequestSchema, { + msg: { case: 'stdin', value: createMessage(ExecStdinSchema, { data: buffer, eof: false }) }, }), ); } @@ -1082,8 +1068,8 @@ export class RunnerGrpcExecClient { final: (callback: (error?: Error | null) => void) => { try { call.write( - create(ExecRequestSchema, { - msg: { case: 'stdin', value: create(ExecStdinSchema, { data: new Uint8Array(), eof: true }) }, + createMessage(ExecRequestSchema, { + msg: { case: 'stdin', value: createMessage(ExecStdinSchema, { data: new Uint8Array(), eof: true }) }, }), ); call.end(); @@ -1187,7 +1173,9 @@ export class RunnerGrpcExecClient { const cols = Math.max(0, Math.floor(size.cols)); const rows = Math.max(0, Math.floor(size.rows)); try { - call.write(create(ExecRequestSchema, { msg: { case: 'resize', value: create(ExecResizeSchema, { cols, rows }) } })); + call.write( + createMessage(ExecRequestSchema, { msg: { case: 'resize', value: createMessage(ExecResizeSchema, { cols, rows }) } }), + ); } catch (error) { throw new DockerRunnerRequestError( 0, @@ -1205,7 +1193,7 @@ export class RunnerGrpcExecClient { typeof deadlineMs === 'number' && deadlineMs > 0 ? { deadline: new Date(Date.now() + deadlineMs) } : undefined; - const request = create(CancelExecutionRequestSchema, { executionId, force }); + const request = createMessage(CancelExecutionRequestSchema, { executionId, force }); return new Promise((resolve, reject) => { const callback = (err: ServiceError | null, response?: CancelExecutionResponse) => { if (err) { @@ -1222,13 +1210,8 @@ export class RunnerGrpcExecClient { }); } - private createMetadata(path: string): Metadata { - const headers = buildAuthHeaders({ method: 'POST', path, body: '', secret: this.sharedSecret }); - const metadata = new Metadata(); - for (const [key, value] of Object.entries(headers)) { - metadata.set(key, value); - } - return metadata; + private createMetadata(_path: string): Metadata { + return new Metadata(); } private createStartRequest(params: { @@ -1244,12 +1227,12 @@ export class RunnerGrpcExecClient { const env = this.normalizeEnv(execOpts.env ?? interactiveOpts.env); const timeoutMs = this.toBigInt(execOpts.timeoutMs); const idleTimeoutMs = this.toBigInt(execOpts.idleTimeoutMs); - const start = create(ExecStartRequestSchema, { + const start = createMessage(ExecStartRequestSchema, { requestId: randomUUID(), targetId: params.containerId, commandArgv, commandShell, - options: create(ExecOptionsSchema, { + options: createMessage(ExecOptionsSchema, { workdir: execOpts.workdir ?? interactiveOpts.workdir ?? undefined, env, timeoutMs: timeoutMs && timeoutMs > 0n ? timeoutMs : undefined, @@ -1260,7 +1243,7 @@ export class RunnerGrpcExecClient { separateStderr: interactiveOpts.demuxStderr ?? true, }), }); - return create(ExecRequestSchema, { msg: { case: 'start', value: start } }); + return createMessage(ExecRequestSchema, { msg: { case: 'start', value: start } }); } private extractRequestedTimeouts(start: ExecRequest): { timeoutMs?: number; idleTimeoutMs?: number } { diff --git a/packages/platform-server/src/infra/container/workload.grpc.ts b/packages/platform-server/src/infra/container/workload.grpc.ts index 4db25d89d..26aab3452 100644 --- a/packages/platform-server/src/infra/container/workload.grpc.ts +++ b/packages/platform-server/src/infra/container/workload.grpc.ts @@ -1,4 +1,3 @@ -import { create } from '@bufbuild/protobuf'; import { ContainerSpec, ContainerSpecSchema, @@ -13,6 +12,7 @@ import { VolumeSpecSchema, } from '../../proto/gen/agynio/api/runner/v1/runner_pb.js'; import type { ContainerOpts, SidecarOpts } from './dockerRunner.types'; +import { createMessage } from '../proto.utils'; const PROP_AUTO_REMOVE = 'auto_remove'; const PROP_NETWORK_MODE = 'network_mode'; @@ -32,13 +32,13 @@ const normalizeEnv = (env?: ContainerOpts['env']): EnvVar[] => { return env .map((entry: string) => { const idx = entry.indexOf('='); - if (idx === -1) return create(EnvVarSchema, { name: entry, value: '' }); - return create(EnvVarSchema, { name: entry.slice(0, idx), value: entry.slice(idx + 1) }); + if (idx === -1) return createMessage(EnvVarSchema, { name: entry, value: '' }); + return createMessage(EnvVarSchema, { name: entry.slice(0, idx), value: entry.slice(idx + 1) }); }) .filter((item) => item.name.length > 0); } return Object.entries(env) - .map(([name, value]: [string, unknown]) => create(EnvVarSchema, { name, value: String(value) })) + .map(([name, value]: [string, unknown]) => createMessage(EnvVarSchema, { name, value: String(value) })) .filter((item: EnvVar) => item.name.length > 0); }; @@ -91,14 +91,14 @@ export const containerOptsToStartWorkloadRequest = (opts: ContainerOpts): StartW if (!parsed) continue; const name = ensureVolumeSpecName('bind', ++volumeIndex); const isReadOnly = parsed.options.includes('ro'); - const spec = create(VolumeSpecSchema, { + const spec = createMessage(VolumeSpecSchema, { name, kind: VolumeKind.NAMED, persistentName: parsed.source, additionalProperties: parsed.options.length > 0 ? { [PROP_BIND_OPTIONS]: parsed.options.join(',') } : {}, }); - const mount = create(VolumeMountSchema, { + const mount = createMessage(VolumeMountSchema, { volume: name, mountPath: parsed.destination, readOnly: isReadOnly, @@ -111,13 +111,13 @@ export const containerOptsToStartWorkloadRequest = (opts: ContainerOpts): StartW for (const path of opts.anonymousVolumes) { if (!isNonEmptyString(path)) continue; const name = ensureVolumeSpecName('ephemeral', ++volumeIndex); - const spec = create(VolumeSpecSchema, { + const spec = createMessage(VolumeSpecSchema, { name, kind: VolumeKind.EPHEMERAL, persistentName: '', additionalProperties: {}, }); - const mount = create(VolumeMountSchema, { + const mount = createMessage(VolumeMountSchema, { volume: name, mountPath: path, readOnly: false, @@ -126,7 +126,7 @@ export const containerOptsToStartWorkloadRequest = (opts: ContainerOpts): StartW } } - const main = create(ContainerSpecSchema, { + const main = createMessage(ContainerSpecSchema, { image: opts.image ?? '', name: opts.name ?? '', cmd: opts.cmd ?? [], @@ -152,13 +152,13 @@ export const containerOptsToStartWorkloadRequest = (opts: ContainerOpts): StartW for (const path of sidecar.anonymousVolumes) { if (!isNonEmptyString(path)) continue; const name = ensureVolumeSpecName('ephemeral', ++volumeIndex); - const spec = create(VolumeSpecSchema, { + const spec = createMessage(VolumeSpecSchema, { name, kind: VolumeKind.EPHEMERAL, persistentName: '', additionalProperties: {}, }); - const mount = create(VolumeMountSchema, { + const mount = createMessage(VolumeMountSchema, { volume: name, mountPath: path, readOnly: false, @@ -167,7 +167,7 @@ export const containerOptsToStartWorkloadRequest = (opts: ContainerOpts): StartW } } - return create(ContainerSpecSchema, { + return createMessage(ContainerSpecSchema, { image: sidecar.image ?? '', name: '', cmd: sidecar.cmd ?? [], @@ -194,7 +194,7 @@ export const containerOptsToStartWorkloadRequest = (opts: ContainerOpts): StartW requestAdditional[PROP_PLATFORM] = opts.platform; } - return create(StartWorkloadRequestSchema, { + return createMessage(StartWorkloadRequestSchema, { main, sidecars, volumes, diff --git a/packages/platform-server/src/infra/infra.module.ts b/packages/platform-server/src/infra/infra.module.ts index bcddff601..38eb10658 100644 --- a/packages/platform-server/src/infra/infra.module.ts +++ b/packages/platform-server/src/infra/infra.module.ts @@ -47,7 +47,6 @@ import { HealthController } from './health/health.controller'; useFactory: (config: ConfigService) => new RunnerGrpcClient({ address: config.getDockerRunnerGrpcAddress(), - sharedSecret: config.getDockerRunnerSharedSecret(), requestTimeoutMs: config.getDockerRunnerTimeoutMs(), }), inject: [ConfigService], diff --git a/packages/platform-server/src/infra/proto.utils.ts b/packages/platform-server/src/infra/proto.utils.ts new file mode 100644 index 000000000..8c317e796 --- /dev/null +++ b/packages/platform-server/src/infra/proto.utils.ts @@ -0,0 +1,6 @@ +import { create, type DescMessage, type MessageInitShape, type MessageShape } from '@bufbuild/protobuf'; + +export const createMessage = ( + schema: Desc, + init?: MessageInitShape, +): MessageShape => create(schema, init) as MessageShape; diff --git a/packages/platform-server/src/notifications/notifications.publisher.ts b/packages/platform-server/src/notifications/notifications.publisher.ts index 3182095ad..4f01918ad 100644 --- a/packages/platform-server/src/notifications/notifications.publisher.ts +++ b/packages/platform-server/src/notifications/notifications.publisher.ts @@ -1,7 +1,8 @@ -import { create, type JsonObject } from '@bufbuild/protobuf'; +import { type JsonObject } from '@bufbuild/protobuf'; import { Inject, Injectable, Logger } from '@nestjs/common'; import { NotificationsGrpcClient } from './notifications.grpc.client'; import { PublishRequestSchema } from '../proto/gen/agynio/api/notifications/v1/notifications_pb.js'; +import { createMessage } from '../infra/proto.utils'; const NOTIFICATIONS_SOURCE = 'platform-server'; @@ -19,7 +20,7 @@ export class NotificationsPublisher { if (!jsonPayload) return; try { - const request = create(PublishRequestSchema, { + const request = createMessage(PublishRequestSchema, { event, rooms, payload: jsonPayload, diff --git a/packages/platform-server/vitest.config.ts b/packages/platform-server/vitest.config.ts index 4924ccf97..2f91701d9 100644 --- a/packages/platform-server/vitest.config.ts +++ b/packages/platform-server/vitest.config.ts @@ -6,9 +6,6 @@ export default defineConfig({ globals: true, include: ["__tests__/**/*.{test,spec}.ts", "__e2e__/**/*.test.ts"], setupFiles: ["./__tests__/vitest.setup.ts"], - env: { - DOCKER_RUNNER_SHARED_SECRET: "test-shared-secret", - }, fileParallelism: false, coverage: { enabled: false,