Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
db94a08712
11 changed files with 199 additions and 32 deletions
|
|
@ -35,6 +35,7 @@ import {
|
|||
registerRecentProjectsIpc,
|
||||
removeRecentProjectsIpc,
|
||||
} from '@features/recent-projects/main';
|
||||
import { applyOpenCodeAutoUpdatePolicy } from '@main/services/runtime/openCodeAutoUpdatePolicy';
|
||||
import { providerConnectionService } from '@main/services/runtime/ProviderConnectionService';
|
||||
import { JsonScheduleRepository } from '@main/services/schedule/JsonScheduleRepository';
|
||||
import { ScheduledTaskExecutor } from '@main/services/schedule/ScheduledTaskExecutor';
|
||||
|
|
@ -113,6 +114,7 @@ import {
|
|||
OpenCodeBridgeCommandHandshakePort,
|
||||
} from './services/team/opencode/bridge/OpenCodeBridgeHandshakeClient';
|
||||
import { OpenCodeStateChangingBridgeCommandService } from './services/team/opencode/bridge/OpenCodeStateChangingBridgeCommandService';
|
||||
import { resolveOpenCodeTeamLaunchModeFromEnv } from './services/team/opencode/config/OpenCodeLaunchModeEnv';
|
||||
import { resolveOpenCodeProductionE2EEvidencePath } from './services/team/opencode/e2e/OpenCodeProductionE2EEvidencePath';
|
||||
import { OpenCodeProductionE2EEvidenceStore } from './services/team/opencode/e2e/OpenCodeProductionE2EEvidenceStore';
|
||||
import { OpenCodeRuntimeManifestEvidenceReader } from './services/team/opencode/store/OpenCodeRuntimeManifestEvidenceReader';
|
||||
|
|
@ -173,7 +175,6 @@ import {
|
|||
UpdaterService,
|
||||
} from './services';
|
||||
|
||||
import type { OpenCodeTeamLaunchMode } from './services/team';
|
||||
import type { FileChangeEvent } from '@main/types';
|
||||
import type { TeamChangeEvent } from '@shared/types';
|
||||
|
||||
|
|
@ -200,17 +201,6 @@ const INBOX_NOTIFY_DEBOUNCE_MS = 500;
|
|||
/** Messages sent from our UI (user_sent) — suppress notifications for these. */
|
||||
const suppressedSources = new Set(['user_sent']);
|
||||
|
||||
function resolveOpenCodeTeamLaunchModeFromEnv(): OpenCodeTeamLaunchMode {
|
||||
const raw = process.env.CLAUDE_TEAM_OPENCODE_LAUNCH_MODE?.trim().toLowerCase();
|
||||
if (raw === 'dogfood' || raw === 'production' || raw === 'disabled') {
|
||||
return raw;
|
||||
}
|
||||
if (process.env.CLAUDE_TEAM_OPENCODE_DOGFOOD === '1') {
|
||||
return 'dogfood';
|
||||
}
|
||||
return 'disabled';
|
||||
}
|
||||
|
||||
async function createOpenCodeRuntimeAdapterRegistry(): Promise<TeamRuntimeAdapterRegistry> {
|
||||
const binaryPath = await ClaudeBinaryResolver.resolve();
|
||||
if (!binaryPath) {
|
||||
|
|
@ -218,7 +208,7 @@ async function createOpenCodeRuntimeAdapterRegistry(): Promise<TeamRuntimeAdapte
|
|||
return new TeamRuntimeAdapterRegistry();
|
||||
}
|
||||
|
||||
const bridgeEnv = { ...process.env };
|
||||
const bridgeEnv = applyOpenCodeAutoUpdatePolicy({ ...process.env });
|
||||
try {
|
||||
const mcpLaunchSpec = await resolveAgentTeamsMcpLaunchSpec();
|
||||
const mcpEntry = mcpLaunchSpec.args[0];
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import { getShellPreferredHome } from '@main/utils/shellEnv';
|
|||
|
||||
import { configManager } from '../infrastructure/ConfigManager';
|
||||
|
||||
import { applyOpenCodeAutoUpdatePolicy } from './openCodeAutoUpdatePolicy';
|
||||
import {
|
||||
applyConfiguredRuntimeBackendsEnv,
|
||||
applyProviderRuntimeEnv,
|
||||
|
|
@ -42,6 +43,11 @@ export function buildRuntimeBaseEnv(options: BuildRuntimeBaseEnvOptions = {}): {
|
|||
|
||||
applyConfiguredRuntimeBackendsEnv(env, configManager.getConfig().runtime);
|
||||
Object.assign(env, options.env ?? {});
|
||||
const policyAppliedEnv = applyOpenCodeAutoUpdatePolicy(env);
|
||||
if (policyAppliedEnv.OPENCODE_DISABLE_AUTOUPDATE === undefined) {
|
||||
delete env.OPENCODE_DISABLE_AUTOUPDATE;
|
||||
}
|
||||
Object.assign(env, policyAppliedEnv);
|
||||
|
||||
const explicitHome = getFirstNonEmptyEnvValue(options.env?.HOME, options.env?.USERPROFILE);
|
||||
const fallbackHome = getFirstNonEmptyEnvValue(
|
||||
|
|
|
|||
22
src/main/services/runtime/openCodeAutoUpdatePolicy.ts
Normal file
22
src/main/services/runtime/openCodeAutoUpdatePolicy.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
export const OPENCODE_DISABLE_AUTOUPDATE_ENV = 'OPENCODE_DISABLE_AUTOUPDATE';
|
||||
export const CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE_ENV = 'CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE';
|
||||
|
||||
const ENABLED_VALUES = new Set(['1', 'true', 'yes', 'on']);
|
||||
|
||||
export function isOpenCodeAutoUpdateAllowed(env: NodeJS.ProcessEnv = process.env): boolean {
|
||||
const raw = env[CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE_ENV]?.trim().toLowerCase();
|
||||
return raw ? ENABLED_VALUES.has(raw) : false;
|
||||
}
|
||||
|
||||
export function applyOpenCodeAutoUpdatePolicy<T extends Record<string, string | undefined>>(
|
||||
env: T,
|
||||
policyEnv: NodeJS.ProcessEnv = env as NodeJS.ProcessEnv
|
||||
): T & NodeJS.ProcessEnv {
|
||||
const next: Record<string, string | undefined> = { ...env };
|
||||
if (isOpenCodeAutoUpdateAllowed(policyEnv)) {
|
||||
delete next[OPENCODE_DISABLE_AUTOUPDATE_ENV];
|
||||
return next as T & NodeJS.ProcessEnv;
|
||||
}
|
||||
next[OPENCODE_DISABLE_AUTOUPDATE_ENV] = '1';
|
||||
return next as T & NodeJS.ProcessEnv;
|
||||
}
|
||||
|
|
@ -1,20 +1,20 @@
|
|||
import { applyOpenCodeAutoUpdatePolicy } from '@main/services/runtime/openCodeAutoUpdatePolicy';
|
||||
import { execCli } from '@main/utils/childProcess';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
import { execCli } from '@main/utils/childProcess';
|
||||
|
||||
import {
|
||||
extractRunId,
|
||||
OPEN_CODE_BRIDGE_SCHEMA_VERSION,
|
||||
parseSingleBridgeJsonResult,
|
||||
validateBridgeResultEnvelope,
|
||||
type OpenCodeBridgeCommandEnvelope,
|
||||
type OpenCodeBridgeCommandName,
|
||||
type OpenCodeBridgeDiagnosticEvent,
|
||||
type OpenCodeBridgeFailure,
|
||||
type OpenCodeBridgeFailureKind,
|
||||
type OpenCodeBridgeResult,
|
||||
parseSingleBridgeJsonResult,
|
||||
validateBridgeResultEnvelope,
|
||||
} from './OpenCodeBridgeCommandContract';
|
||||
|
||||
export interface OpenCodeBridgeProcessRunInput {
|
||||
|
|
@ -113,7 +113,7 @@ export class OpenCodeBridgeCommandClient {
|
|||
this.diagnosticIdFactory =
|
||||
options.diagnosticIdFactory ?? (() => `opencode-bridge-diagnostic-${randomUUID()}`);
|
||||
this.clock = options.clock ?? (() => new Date());
|
||||
this.env = options.env ?? process.env;
|
||||
this.env = applyOpenCodeAutoUpdatePolicy(options.env ?? process.env);
|
||||
this.keepInputFile = options.keepInputFile ?? false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
import type { OpenCodeTeamLaunchMode } from '../bridge/OpenCodeBridgeCommandContract';
|
||||
|
||||
export const CLAUDE_TEAM_OPENCODE_LAUNCH_MODE_ENV = 'CLAUDE_TEAM_OPENCODE_LAUNCH_MODE';
|
||||
export const CLAUDE_TEAM_OPENCODE_DOGFOOD_ENV = 'CLAUDE_TEAM_OPENCODE_DOGFOOD';
|
||||
|
||||
export function resolveOpenCodeTeamLaunchModeFromEnv(
|
||||
env: NodeJS.ProcessEnv = process.env
|
||||
): OpenCodeTeamLaunchMode {
|
||||
const raw = env[CLAUDE_TEAM_OPENCODE_LAUNCH_MODE_ENV]?.trim().toLowerCase();
|
||||
if (raw === 'dogfood' || raw === 'production' || raw === 'disabled') {
|
||||
return raw;
|
||||
}
|
||||
if (env[CLAUDE_TEAM_OPENCODE_DOGFOOD_ENV] === '1') {
|
||||
return 'dogfood';
|
||||
}
|
||||
return 'production';
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { applyOpenCodeAutoUpdatePolicy } from '@main/services/runtime/openCodeAutoUpdatePolicy';
|
||||
import { createHash } from 'crypto';
|
||||
import { promises as fs } from 'fs';
|
||||
import * as os from 'os';
|
||||
|
|
@ -33,7 +34,7 @@ export interface OpenCodeManagedOverlay {
|
|||
projectPath: string;
|
||||
env: {
|
||||
OPENCODE_CONFIG_CONTENT: string;
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1';
|
||||
OPENCODE_DISABLE_AUTOUPDATE?: '1';
|
||||
};
|
||||
appMcpServerName: string;
|
||||
appMcpConfig: OpenCodeMcpServerConfig;
|
||||
|
|
@ -48,6 +49,7 @@ export interface OpenCodeManagedOverlayBuilderInput {
|
|||
appMcpArgs: string[];
|
||||
appMcpEnv: Record<string, string>;
|
||||
mcpTimeoutMs?: number;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
}
|
||||
|
||||
export interface OpenCodeBehaviorSourceScannerOptions {
|
||||
|
|
@ -97,10 +99,12 @@ export class OpenCodeManagedOverlayBuilder {
|
|||
return {
|
||||
launchMode: 'project_root_with_inline_overlay',
|
||||
projectPath: input.projectPath,
|
||||
env: {
|
||||
OPENCODE_CONFIG_CONTENT: JSON.stringify(overlayConfig),
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
},
|
||||
env: applyOpenCodeAutoUpdatePolicy(
|
||||
{
|
||||
OPENCODE_CONFIG_CONTENT: JSON.stringify(overlayConfig),
|
||||
},
|
||||
input.env ?? process.env
|
||||
),
|
||||
appMcpServerName,
|
||||
appMcpConfig: overlayConfig.mcp[appMcpServerName],
|
||||
preservedSources,
|
||||
|
|
@ -125,7 +129,7 @@ export class OpenCodeBehaviorSourceScanner {
|
|||
}
|
||||
|
||||
async scan(projectPath: string): Promise<OpenCodeBehaviorSource[]> {
|
||||
const sourceSpecs: Array<{ kind: OpenCodeBehaviorSourceKind; targetPath: string }> = [
|
||||
const sourceSpecs: { kind: OpenCodeBehaviorSourceKind; targetPath: string }[] = [
|
||||
{
|
||||
kind: 'global_config',
|
||||
targetPath: path.join(this.homePath, '.config/opencode/opencode.json'),
|
||||
|
|
@ -224,19 +228,19 @@ export class OpenCodeBehaviorSourceScanner {
|
|||
}
|
||||
|
||||
private async listDirectoryFiles(rootPath: string): Promise<
|
||||
Array<{
|
||||
{
|
||||
relativePath: string;
|
||||
size: number;
|
||||
mtimeMs: number;
|
||||
contentHash: string;
|
||||
}>
|
||||
}[]
|
||||
> {
|
||||
const results: Array<{
|
||||
const results: {
|
||||
relativePath: string;
|
||||
size: number;
|
||||
mtimeMs: number;
|
||||
contentHash: string;
|
||||
}> = [];
|
||||
}[] = [];
|
||||
|
||||
const visit = async (directoryPath: string): Promise<void> => {
|
||||
if (results.length >= this.maxDirectoryFiles) {
|
||||
|
|
|
|||
40
test/main/services/runtime/openCodeAutoUpdatePolicy.test.ts
Normal file
40
test/main/services/runtime/openCodeAutoUpdatePolicy.test.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
applyOpenCodeAutoUpdatePolicy,
|
||||
isOpenCodeAutoUpdateAllowed,
|
||||
} from '../../../../src/main/services/runtime/openCodeAutoUpdatePolicy';
|
||||
|
||||
describe('openCodeAutoUpdatePolicy', () => {
|
||||
it('disables OpenCode auto-update by default for app-managed envs', () => {
|
||||
const input = { PATH: '/usr/bin' };
|
||||
|
||||
const result = applyOpenCodeAutoUpdatePolicy(input);
|
||||
|
||||
expect(result).toEqual({
|
||||
PATH: '/usr/bin',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
});
|
||||
expect(input).toEqual({ PATH: '/usr/bin' });
|
||||
});
|
||||
|
||||
it('allows an explicit app override to remove inherited disable-auto-update', () => {
|
||||
const result = applyOpenCodeAutoUpdatePolicy({
|
||||
CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE: '1',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
});
|
||||
|
||||
expect(result.CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE).toBe('1');
|
||||
expect(result.OPENCODE_DISABLE_AUTOUPDATE).toBeUndefined();
|
||||
expect(isOpenCodeAutoUpdateAllowed(result)).toBe(true);
|
||||
});
|
||||
|
||||
it('treats non-enabled override values as fail-closed', () => {
|
||||
const result = applyOpenCodeAutoUpdatePolicy({
|
||||
CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE: '0',
|
||||
});
|
||||
|
||||
expect(result.OPENCODE_DISABLE_AUTOUPDATE).toBe('1');
|
||||
expect(isOpenCodeAutoUpdateAllowed(result)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
|
@ -132,6 +132,26 @@ describe('buildProviderAwareCliEnv', () => {
|
|||
);
|
||||
expect(result.connectionIssues).toEqual({});
|
||||
expect(result.providerArgs).toEqual([]);
|
||||
expect(result.env.OPENCODE_DISABLE_AUTOUPDATE).toBe('1');
|
||||
});
|
||||
|
||||
it('allows OpenCode auto-update only behind an explicit app override', async () => {
|
||||
buildEnrichedEnvMock.mockReturnValue({
|
||||
PATH: '/usr/bin',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
});
|
||||
const { buildProviderAwareCliEnv } = await import(
|
||||
'../../../../src/main/services/runtime/providerAwareCliEnv'
|
||||
);
|
||||
|
||||
const result = await buildProviderAwareCliEnv({
|
||||
env: {
|
||||
CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE: '1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.env.CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE).toBe('1');
|
||||
expect(result.env.OPENCODE_DISABLE_AUTOUPDATE).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses non-destructive credential augmentation for PTY-style envs', async () => {
|
||||
|
|
|
|||
|
|
@ -57,6 +57,9 @@ describe('OpenCodeBridgeCommandClient', () => {
|
|||
args: ['runtime', 'opencode-command', '--json', '--input', expect.any(String)],
|
||||
cwd: '/tmp/project',
|
||||
timeoutMs: 10_000,
|
||||
env: expect.objectContaining({
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
}),
|
||||
});
|
||||
|
||||
const inputPath = runner.calls[0].args[4];
|
||||
|
|
|
|||
33
test/main/services/team/OpenCodeLaunchModeEnv.test.ts
Normal file
33
test/main/services/team/OpenCodeLaunchModeEnv.test.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import { resolveOpenCodeTeamLaunchModeFromEnv } from '../../../../src/main/services/team/opencode/config/OpenCodeLaunchModeEnv';
|
||||
|
||||
describe('resolveOpenCodeTeamLaunchModeFromEnv', () => {
|
||||
it('defaults to production so OpenCode is visible while strict readiness remains authoritative', () => {
|
||||
expect(resolveOpenCodeTeamLaunchModeFromEnv({})).toBe('production');
|
||||
});
|
||||
|
||||
it('preserves explicit launch mode overrides', () => {
|
||||
expect(
|
||||
resolveOpenCodeTeamLaunchModeFromEnv({ CLAUDE_TEAM_OPENCODE_LAUNCH_MODE: 'disabled' })
|
||||
).toBe('disabled');
|
||||
expect(
|
||||
resolveOpenCodeTeamLaunchModeFromEnv({ CLAUDE_TEAM_OPENCODE_LAUNCH_MODE: 'dogfood' })
|
||||
).toBe('dogfood');
|
||||
expect(
|
||||
resolveOpenCodeTeamLaunchModeFromEnv({ CLAUDE_TEAM_OPENCODE_LAUNCH_MODE: 'production' })
|
||||
).toBe('production');
|
||||
});
|
||||
|
||||
it('keeps the legacy dogfood flag as an explicit opt-in', () => {
|
||||
expect(resolveOpenCodeTeamLaunchModeFromEnv({ CLAUDE_TEAM_OPENCODE_DOGFOOD: '1' })).toBe(
|
||||
'dogfood'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to production for invalid launch mode values', () => {
|
||||
expect(
|
||||
resolveOpenCodeTeamLaunchModeFromEnv({ CLAUDE_TEAM_OPENCODE_LAUNCH_MODE: 'enabled' })
|
||||
).toBe('production');
|
||||
});
|
||||
});
|
||||
|
|
@ -61,9 +61,33 @@ describe('OpenCodeManagedOverlay', () => {
|
|||
});
|
||||
expect(overlay.env).not.toHaveProperty('OPENCODE_PURE');
|
||||
expect(overlay.env).not.toHaveProperty('OPENCODE_DISABLE_PROJECT_CONFIG');
|
||||
expect(overlay.env.OPENCODE_DISABLE_AUTOUPDATE).toBe('1');
|
||||
expect(config).not.toHaveProperty('plugin');
|
||||
expect(config).not.toHaveProperty('model');
|
||||
expect(overlay.diagnostics).toContain('OpenCode managed overlay checked at 2026-04-21T12:00:00.000Z');
|
||||
expect(overlay.diagnostics).toContain(
|
||||
'OpenCode managed overlay checked at 2026-04-21T12:00:00.000Z'
|
||||
);
|
||||
});
|
||||
|
||||
it('allows app-managed OpenCode auto-update only behind an explicit override', async () => {
|
||||
const builder = new OpenCodeManagedOverlayBuilder(
|
||||
new OpenCodeBehaviorSourceScanner({ homePath })
|
||||
);
|
||||
|
||||
const overlay = await builder.build({
|
||||
projectPath,
|
||||
preferredMcpName: 'agent-teams',
|
||||
appMcpCommand: 'node',
|
||||
appMcpArgs: ['server.js'],
|
||||
appMcpEnv: {},
|
||||
env: {
|
||||
CLAUDE_TEAM_OPENCODE_ALLOW_AUTOUPDATE: '1',
|
||||
OPENCODE_DISABLE_AUTOUPDATE: '1',
|
||||
},
|
||||
});
|
||||
|
||||
expect(overlay.env.OPENCODE_CONFIG_CONTENT).toBeTruthy();
|
||||
expect(overlay.env.OPENCODE_DISABLE_AUTOUPDATE).toBeUndefined();
|
||||
});
|
||||
|
||||
it('renames the app-owned MCP server when user config already declares the preferred name', async () => {
|
||||
|
|
@ -73,7 +97,9 @@ describe('OpenCodeManagedOverlay', () => {
|
|||
'agent-teams-runtime-1': { type: 'local', command: 'custom-2', enabled: true },
|
||||
},
|
||||
});
|
||||
const builder = new OpenCodeManagedOverlayBuilder(new OpenCodeBehaviorSourceScanner({ homePath }));
|
||||
const builder = new OpenCodeManagedOverlayBuilder(
|
||||
new OpenCodeBehaviorSourceScanner({ homePath })
|
||||
);
|
||||
|
||||
const overlay = await builder.build({
|
||||
projectPath,
|
||||
|
|
@ -99,10 +125,16 @@ describe('OpenCodeManagedOverlay', () => {
|
|||
}`,
|
||||
'utf8'
|
||||
);
|
||||
await fs.writeFile(path.join(projectPath, '.opencode/plugins/example.ts'), 'export default {}', 'utf8');
|
||||
await fs.writeFile(
|
||||
path.join(projectPath, '.opencode/plugins/example.ts'),
|
||||
'export default {}',
|
||||
'utf8'
|
||||
);
|
||||
const scanner = new OpenCodeBehaviorSourceScanner({ homePath });
|
||||
|
||||
await expect(scanner.readDeclaredMcpNames(projectPath)).resolves.toEqual(new Set(['agent-teams']));
|
||||
await expect(scanner.readDeclaredMcpNames(projectPath)).resolves.toEqual(
|
||||
new Set(['agent-teams'])
|
||||
);
|
||||
const sources = await scanner.scan(projectPath);
|
||||
|
||||
expect(sources).toContainEqual(
|
||||
|
|
|
|||
Loading…
Reference in a new issue