build(runtime): require Node 24 toolchain

This commit is contained in:
777genius 2026-05-26 19:44:23 +03:00
parent 5355570f2c
commit 58a0eb603d
23 changed files with 336 additions and 418 deletions

52
.dockerignore Normal file
View file

@ -0,0 +1,52 @@
# Dependencies installed inside the image
node_modules/
landing/node_modules/
# Local build output
dist/
dist-electron/
dist-standalone/
out/
release/
coverage/
landing/.nuxt/
landing/.output/
electron.vite.config.*.mjs
# Runtime and local caches
.git/
.pnpm-store/
.runtime-download/
resources/runtime/*
!resources/runtime/.gitkeep
.eslintcache
.eslintcache-fast
*.tsbuildinfo
# Local-only data
.claude/
.home/
.serena/
.playwright-mcp/
logs/
*.log
.env
.env.*
# OS and editor noise
.DS_Store
Thumbs.db
.vscode/
.idea/
*.swp
*.swo
*~
# Local scratch artifacts
notification_example/
temp/
eslint-fix/
remotion/*
.tmp-*
agent-teams-reference-fix-*.png
ORCHESTRATOR_RELEASE_RUNBOOK.local.md

View file

@ -64,7 +64,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Restore pnpm node-gyp executable bit
@ -102,7 +102,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Restore pnpm node-gyp executable bit
@ -136,7 +136,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Install dependencies

View file

@ -58,7 +58,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Install dependencies

View file

@ -23,7 +23,7 @@ jobs:
- uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: npm
cache-dependency-path: landing/package-lock.json

View file

@ -42,7 +42,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Restore pnpm node-gyp executable bit
@ -334,7 +334,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Setup Python for node-gyp
@ -455,7 +455,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Setup Python for node-gyp
@ -577,7 +577,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 22
node-version-file: .node-version
cache: pnpm
- name: Setup Python for node-gyp

1
.node-version Normal file
View file

@ -0,0 +1 @@
24.16.0

1
.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

1
.nvmrc Normal file
View file

@ -0,0 +1 @@
24.16.0

View file

@ -14,6 +14,6 @@
"test:watch": "vitest --config vitest.config.js"
},
"engines": {
"node": ">=20"
"node": ">=24.16.0 <25"
}
}

View file

@ -8,35 +8,55 @@
# Run: docker run -p 3456:3456 -v ~/.claude:/data/.claude:ro agent-teams-ai
# =============================================================================
FROM node:20-slim AS builder
ARG NODE_VERSION=24.16.0
FROM node:${NODE_VERSION}-slim AS base
WORKDIR /app
# Enable corepack for pnpm
RUN corepack enable
FROM base AS builder
# Native dependencies such as node-pty may need source builds on slim images.
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
# Install dependencies first (better layer caching)
COPY package.json pnpm-lock.yaml ./
COPY patches ./patches
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm standalone:build
RUN AGENT_TEAMS_DISABLE_SOURCEMAPS=1 pnpm standalone:build
# =============================================================================
# Production stage — minimal image with only the built output
# Production dependencies stage
# =============================================================================
FROM node:20-slim
FROM base AS prod-deps
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
# Enable corepack for pnpm
RUN corepack enable
# Copy package files and install production-only dependencies
# Install production-only dependencies
# (fastify, @fastify/cors, @fastify/static are externalized from the bundle)
COPY --from=builder /app/package.json /app/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile --prod
COPY package.json pnpm-lock.yaml ./
COPY patches ./patches
RUN pnpm install --frozen-lockfile --prod --ignore-scripts \
&& pnpm rebuild node-pty cpu-features ssh2
# =============================================================================
# Production stage - minimal image with only runtime dependencies and built output
# =============================================================================
FROM base
COPY --from=prod-deps /app/package.json /app/pnpm-lock.yaml ./
COPY --from=prod-deps /app/node_modules ./node_modules
COPY --from=builder /app/agent-teams-controller ./agent-teams-controller
# Copy built standalone server and renderer output
COPY --from=builder /app/dist-standalone ./dist-standalone

View file

@ -14,6 +14,7 @@ import type { Plugin } from 'vite'
// `vite build --config docker/vite.standalone.config.ts`, so __dirname
// is docker/. All paths must resolve relative to the repo root.
const ROOT = resolve(__dirname, '..')
const sourceMapsEnabled = process.env.AGENT_TEAMS_DISABLE_SOURCEMAPS !== '1'
// Node.js built-in modules that should be externalized
const nodeBuiltins = new Set([
@ -35,11 +36,13 @@ function nativeModuleStub(): Plugin {
const STUB_ID = '\0native-stub'
return {
name: 'native-module-stub',
enforce: 'pre',
resolveId(source) {
if (source.endsWith('.node')) return STUB_ID
return null
},
load(id) {
if (id.endsWith('.node')) return 'export default {}'
if (id === STUB_ID) return 'export default {}'
return null
}
@ -63,6 +66,8 @@ export const ipcMain = { handle: noop, on: noop, removeHandler: noop };
export const shell = { openPath: noop, openExternal: noop };
export const dialog = { showOpenDialog: async () => ({ canceled: true, filePaths: [] }) };
export const Notification = class { show() {} };
export const nativeImage = { createFromPath: () => proxyObj, createEmpty: () => proxyObj };
export const net = { fetch: globalThis.fetch };
export const safeStorage = { isEncryptionAvailable: () => false, encryptString: noop, decryptString: () => '' };
export const screen = proxyObj;
export default proxyObj;
@ -87,6 +92,7 @@ export default defineConfig({
plugins: [nativeModuleStub(), electronStub()],
resolve: {
alias: {
'@features': resolve(ROOT, 'src/features'),
'@main': resolve(ROOT, 'src/main'),
'@shared': resolve(ROOT, 'src/shared'),
'@preload': resolve(ROOT, 'src/preload')
@ -99,7 +105,7 @@ export default defineConfig({
},
build: {
outDir: 'dist-standalone',
target: 'node20',
target: 'node24',
ssr: true,
rollupOptions: {
input: {
@ -119,6 +125,6 @@ export default defineConfig({
}
},
minify: false,
sourcemap: true
sourcemap: sourceMapsEnabled
}
})

View file

@ -55,6 +55,8 @@ const sentrySourceMapTargets = {
},
} as const
const sourceMapSetting = process.env.AGENT_TEAMS_DISABLE_SOURCEMAPS === '1' ? false : 'hidden'
// Sentry source map upload - only active in CI when SENTRY_AUTH_TOKEN is set.
function createSentryPlugins(target: keyof typeof sentrySourceMapTargets): Plugin[] {
if (!process.env.SENTRY_AUTH_TOKEN) return []
@ -98,7 +100,7 @@ export default defineConfig({
commonjsOptions: {
strictRequires: [/node_modules\/.*ssh2\//],
},
sourcemap: 'hidden',
sourcemap: sourceMapSetting,
outDir: 'dist-electron/main',
rollupOptions: {
input: {
@ -169,7 +171,7 @@ export default defineConfig({
},
plugins: [react(), ...createSentryPlugins('renderer')],
build: {
sourcemap: 'hidden',
sourcemap: sourceMapSetting,
rollupOptions: {
input: {
index: resolve(__dirname, 'src/renderer/index.html')

1
landing/.npmrc Normal file
View file

@ -0,0 +1 @@
engine-strict=true

View file

@ -35,6 +35,9 @@
"vitepress": "2.0.0-alpha.17",
"vitepress-codeblock-collapse": "^1.0.0",
"vitepress-plugin-llms": "^1.12.2"
},
"engines": {
"node": ">=24.16.0 <25"
}
},
"node_modules/@alloc/quick-lru": {

View file

@ -2,6 +2,9 @@
"name": "agent-teams-landing",
"private": true,
"type": "module",
"engines": {
"node": ">=24.16.0 <25"
},
"scripts": {
"dev": "nuxt dev",
"build": "nuxt build",

View file

@ -41,13 +41,13 @@
"zod": "^4.3.6"
},
"devDependencies": {
"@types/node": "^24.12.4",
"tsup": "^8.5.1",
"tsx": "^4.21.0",
"typescript": "^5.8.2",
"vitest": "^3.1.4",
"@types/node": "^22.15.18"
"vitest": "^3.1.4"
},
"engines": {
"node": ">=20"
"node": ">=24.16.0 <25"
}
}

View file

@ -3,7 +3,7 @@ import { defineConfig } from 'tsup';
export default defineConfig({
entry: ['src/index.ts'],
format: ['esm'],
target: 'node20',
target: 'node24',
platform: 'node',
outDir: 'dist',
clean: true,

View file

@ -16,6 +16,9 @@
"bugs": {
"url": "https://github.com/777genius/agent-teams-ai/issues"
},
"engines": {
"node": ">=24.16.0 <25"
},
"main": "dist-electron/main/index.cjs",
"scripts": {
"dev": "node ./scripts/dev-with-runtime.mjs",
@ -83,7 +86,7 @@
"test:coverage": "vitest run --coverage",
"test:coverage:critical": "vitest run --coverage --config vitest.critical.config.ts",
"standalone": "tsx src/main/standalone.ts",
"standalone:build": "electron-vite build && vite build --config docker/vite.standalone.config.ts",
"standalone:build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build && node --max-old-space-size=8192 ./node_modules/vite/bin/vite.js build --config docker/vite.standalone.config.ts",
"standalone:start": "node dist-standalone/index.cjs",
"prepare": "husky",
"postinstall": "electron-rebuild -f -o node-pty,ssh2,cpu-features || echo 'native Electron rebuild failed (terminal/ssh features may be degraded)'"
@ -212,7 +215,7 @@
"@tailwindcss/typography": "^0.5.19",
"@types/hast": "^3.0.4",
"@types/mdast": "^4.0.4",
"@types/node": "^25.0.7",
"@types/node": "^24.12.4",
"@types/pidusage": "2.0.5",
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
@ -388,7 +391,7 @@
}
]
},
"packageManager": "pnpm@10.33.0+sha512.10568bb4a6afb58c9eb3630da90cc9516417abebd3fabbe6739f0ae795728da1491e9db5a544c76ad8eb7570f5c4bb3d6c637b2cb41bfdcdb47fa823c8649319",
"packageManager": "pnpm@10.33.4+sha512.1c67b3b359b2d408119ba1ed289f34b8fc3c6873412bec6fd264fbdc82489e510fcbecb9ce9d22dae7f3b76269d8441046014bdca53b9979cd7a561ad631b800",
"pnpm": {
"overrides": {
"@hono/node-server@1": "1.19.13",

View file

@ -3,6 +3,9 @@
"version": "0.1.0",
"private": true,
"type": "module",
"engines": {
"node": ">=24.16.0 <25"
},
"main": "src/index.ts",
"types": "src/index.ts",
"exports": {

File diff suppressed because it is too large Load diff

View file

@ -47,7 +47,10 @@ const MCP_CONFIG_PREFIX = 'agent-teams-mcp-';
const MCP_CONFIG_REMOVE_RETRY_DELAYS_MS = [25, 75, 150] as const;
const NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
const ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS = 5_000;
const MIN_MCP_NODE_MAJOR_VERSION = 20;
// The packaged Electron runtime can lag the source toolchain patch version,
// so MCP launch validation pins the Node 24 runtime line, not .node-version.
const MIN_MCP_NODE_MAJOR_VERSION = 24;
const MAX_MCP_NODE_MAJOR_VERSION = 25;
const NODE_RUNTIME_PROBE_SCRIPT =
'process.stdout.write(JSON.stringify({execPath:process.execPath,version:process.versions.node}))';
/**
@ -335,9 +338,9 @@ function parseNodeRuntimeProbeMetadata(stdout: string, command: string): NodeRun
function assertSupportedMcpNodeRuntime(command: string, metadata: NodeRuntimeProbeMetadata): void {
const major = parseNodeMajorVersion(metadata.version);
if (major === null || major < MIN_MCP_NODE_MAJOR_VERSION) {
if (major === null || major < MIN_MCP_NODE_MAJOR_VERSION || major >= MAX_MCP_NODE_MAJOR_VERSION) {
throw new Error(
`${command} resolved ${metadata.path} with Node.js ${metadata.version}; Agent Teams MCP requires Node.js ${MIN_MCP_NODE_MAJOR_VERSION}+`
`${command} resolved ${metadata.path} with Node.js ${metadata.version}; Agent Teams MCP requires Node.js 24.x`
);
}
}
@ -392,21 +395,16 @@ async function probePackagedElectronNodeRuntime(
emitProgress(options, 'electron-node-runtime', 'Checking bundled Electron Node runtime...');
try {
const { stdout } = await execCli(
process.execPath.trim(),
['-e', 'process.stdout.write("agent-teams-electron-node-ok")'],
{
const { stdout } = await execCli(process.execPath.trim(), ['-e', NODE_RUNTIME_PROBE_SCRIPT], {
encoding: 'utf-8',
timeout: ELECTRON_NODE_RUNTIME_PROBE_TIMEOUT_MS,
env: {
...process.env,
...getPackagedElectronNodeEnv(),
},
}
);
if (stdout.trim() !== 'agent-teams-electron-node-ok') {
throw new Error('Electron Node runtime probe did not return the expected marker');
}
});
const metadata = parseNodeRuntimeProbeMetadata(stdout, process.execPath.trim());
assertSupportedMcpNodeRuntime(process.execPath.trim(), metadata);
_packagedElectronNodeRuntimeProbe = { ok: true };
} catch (error) {
_packagedElectronNodeRuntimeProbe = { ok: false, error };

View file

@ -55,6 +55,7 @@ vi.mock('@features/codex-runtime-installer/main', () => ({
import { resolveVerifiedOpenCodeRuntimeBinaryPath } from '../../../../src/main/services/infrastructure/OpenCodeRuntimeInstallerService';
import { ensureOpenCodeBridgeRuntimeBinaryEnv } from '../../../../src/main/services/runtime/openCodeBridgeRuntimeEnv';
import { buildProviderAwareCliEnv } from '../../../../src/main/services/runtime/providerAwareCliEnv';
import { clearResolvedNodePathForTests } from '../../../../src/main/services/team/TeamMcpConfigBuilder';
import { execCli } from '../../../../src/main/utils/childProcess';
import { setAppDataBasePath } from '../../../../src/main/utils/pathDecoder';
import { clearShellEnvCache } from '../../../../src/main/utils/shellEnv';
@ -72,6 +73,7 @@ describePosix('OpenCode packaged-runtime preflight integration', () => {
tempDir = await mkdtemp(path.join(os.tmpdir(), 'opencode-prod-preflight-'));
setAppDataBasePath(path.join(tempDir, 'app-data'));
clearShellEnvCache();
clearResolvedNodePathForTests();
originalPath = process.env.PATH;
originalShell = process.env.SHELL;
@ -142,7 +144,7 @@ describePosix('OpenCode packaged-runtime preflight integration', () => {
[
'#!/bin/sh',
'if [ "$1" = "-e" ]; then',
' printf "{\\"execPath\\":\\"%s\\",\\"version\\":\\"%s\\"}" "$FAKE_NODE_PATH" "22.0.0"',
' printf "{\\"execPath\\":\\"%s\\",\\"version\\":\\"%s\\"}" "$FAKE_NODE_PATH" "24.16.0"',
' exit 0',
'fi',
'echo "unexpected node args: $*" >&2',

View file

@ -22,7 +22,7 @@ const hoisted = vi.hoisted(() => ({
version: '9.9.9-test',
},
execCliMock: vi.fn<ExecCliMock>(async () => ({
stdout: JSON.stringify({ execPath: '/mock/node', version: '20.11.0' }),
stdout: JSON.stringify({ execPath: '/mock/node', version: '24.16.0' }),
stderr: '',
})),
cachedShellEnv: null as NodeJS.ProcessEnv | null,
@ -68,7 +68,7 @@ import {
} from '@main/services/team/TeamMcpConfigBuilder';
import { setAppDataBasePath, setClaudeBasePathOverride } from '@main/utils/pathDecoder';
function nodeRuntimeProbeStdout(execPath: string, version = '20.11.0'): string {
function nodeRuntimeProbeStdout(execPath: string, version = '24.16.0'): string {
return JSON.stringify({ execPath, version });
}
@ -385,7 +385,7 @@ describe('TeamMcpConfigBuilder', () => {
createPackagedServerBundle(resourcesDir, '// packaged server');
setResourcesPath(resourcesDir);
hoisted.execCliMock.mockResolvedValue({
stdout: 'agent-teams-electron-node-ok',
stdout: nodeRuntimeProbeStdout(electronBinary, '24.15.0'),
stderr: '',
});
@ -418,7 +418,7 @@ describe('TeamMcpConfigBuilder', () => {
expect(hoisted.execCliMock).toHaveBeenCalledTimes(1);
expect(hoisted.execCliMock).toHaveBeenCalledWith(
electronBinary,
['-e', 'process.stdout.write("agent-teams-electron-node-ok")'],
['-e', expect.stringContaining('process.versions.node')],
expect.objectContaining({
env: expect.objectContaining({ ELECTRON_RUN_AS_NODE: '1' }),
})
@ -528,11 +528,11 @@ describe('TeamMcpConfigBuilder', () => {
if (env?.PATH?.split(path.delimiter)[0] === '/strict-shell-node-bin') {
expect(command).toBe('node');
return {
stdout: nodeRuntimeProbeStdout('/strict-shell-node-bin/node', '20.11.0'),
stdout: nodeRuntimeProbeStdout('/strict-shell-node-bin/node', '24.16.0'),
stderr: '',
};
}
return { stdout: nodeRuntimeProbeStdout('/usr/bin/node', '18.19.0'), stderr: '' };
return { stdout: nodeRuntimeProbeStdout('/usr/bin/node', '22.21.1'), stderr: '' };
});
try {