e34d797336
合并 master 后 isolated AppData 配置改为私有副本,避免 Windows 安全校验拒绝硬链接。 只读 runner 状态命令显式配置 config dir,避免误读为 runner 未启用。 为 debug 构建增加仅 E2E 显式开启的确定性 Provider 路由开关,并补充回归测试。 兼容 Windows 对私有副本权限位的校验差异。
2093 lines
71 KiB
JavaScript
2093 lines
71 KiB
JavaScript
import { assert, codedError, hashValue, sleep } from '../assertions/core.mjs';
|
|
import {
|
|
absolutePathVariants,
|
|
isNonEmptyString,
|
|
} from '../assertions/runtime.mjs';
|
|
import {
|
|
createHash,
|
|
fs,
|
|
fsConstants,
|
|
path,
|
|
randomUUID,
|
|
spawn,
|
|
watchFileSystem,
|
|
} from '../dependencies.mjs';
|
|
import {
|
|
activeCommandChildren,
|
|
appRoot,
|
|
BlockedError,
|
|
configFileName,
|
|
contextCompactionAppDataSentinelFileName,
|
|
contextCompactionAppDataSentinelSchema,
|
|
goalAppDataSentinelFileName,
|
|
goalAppDataSentinelSchema,
|
|
linuxPidfdHelperSource,
|
|
localConfigFileName,
|
|
mainAgentId,
|
|
parallelReadAppDataSentinelFileName,
|
|
parallelReadAppDataSentinelSchema,
|
|
projectSkillAppDataSentinelFileName,
|
|
projectSkillAppDataSentinelSchema,
|
|
projectSupervisorAgentId,
|
|
responseStreamAppDataSentinelFileName,
|
|
responseStreamAppDataSentinelSchema,
|
|
runnerEndpointFileName,
|
|
scopedAgentsAppDataSentinelFileName,
|
|
scopedAgentsAppDataSentinelSchema,
|
|
state,
|
|
steerRunnerKillAppDataSentinelFileName,
|
|
steerRunnerKillAppDataSentinelSchema,
|
|
supervisorAutonomousPlayableAppDataSentinelFileName,
|
|
supervisorAutonomousPlayableAppDataSentinelSchema,
|
|
supervisorSwarmAppDataSentinelFileName,
|
|
supervisorSwarmAppDataSentinelSchema,
|
|
supervisorSwarmAutonomousChatAppDataSentinelFileName,
|
|
supervisorSwarmAutonomousChatAppDataSentinelSchema,
|
|
supervisorSwarmCollaborationPolicyAppDataSentinelFileName,
|
|
supervisorSwarmCollaborationPolicyAppDataSentinelSchema,
|
|
supervisorSwarmFinalReplyTransientRetryAppDataSentinelFileName,
|
|
supervisorSwarmFinalReplyTransientRetryAppDataSentinelSchema,
|
|
supervisorSwarmRequiredAgentIds,
|
|
supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName,
|
|
supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema,
|
|
supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelFileName,
|
|
supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelSchema,
|
|
supervisorSwarmTransientRetryAppDataSentinelFileName,
|
|
supervisorSwarmTransientRetryAppDataSentinelSchema,
|
|
supervisorSwarmTransientRetryTargetAgentId,
|
|
userInputAppDataSentinelFileName,
|
|
userInputAppDataSentinelSchema,
|
|
webSearchAppDataSentinelFileName,
|
|
webSearchAppDataSentinelSchema,
|
|
windowsProcessHandleHelperSource,
|
|
} from '../runtime-state.mjs';
|
|
import { isContextCompactionSuite } from '../suites/context-compaction.mjs';
|
|
import { isGoalRuntimeSuite } from '../suites/goal.mjs';
|
|
import { isParallelReadSuite } from '../suites/parallel-read.mjs';
|
|
import { isProjectSkillSuite } from '../suites/project-skill.mjs';
|
|
import { isResponseStreamSuite } from '../suites/response-stream.mjs';
|
|
import { isScopedAgentsSuite } from '../suites/scoped-agents.mjs';
|
|
import { isSteerRunnerKillSuite } from '../suites/steer-runner-kill.mjs';
|
|
import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs';
|
|
import {
|
|
isSupervisorSwarmAutonomousChatSuite,
|
|
isSupervisorSwarmCollaborationPolicyMixedRecoverySuite,
|
|
isSupervisorSwarmFinalReplyTransientRetrySuite,
|
|
isSupervisorSwarmInitialTransientRetrySuite,
|
|
isSupervisorSwarmInteractiveChatSuite,
|
|
isSupervisorSwarmMixedHarnessSuite,
|
|
isSupervisorSwarmStaticIsolatedAutonomousChatSuite,
|
|
isSupervisorSwarmSuite,
|
|
isSupervisorSwarmToolPlanHandoffRunnerKillSuite,
|
|
isSupervisorSwarmTransientRetrySuite,
|
|
rebuildSupervisorSwarmTranscriptScanner,
|
|
} from '../suites/supervisor-swarm.mjs';
|
|
import { isUserInputRuntimeSuite } from '../suites/user-input.mjs';
|
|
import {
|
|
isWebSearchSuite,
|
|
sameEffectiveAgentLlmWithoutWebSearch,
|
|
} from '../suites/web-search.mjs';
|
|
import {
|
|
effectiveAgentLlmConfig,
|
|
isPlainObject,
|
|
loadConfig,
|
|
mergeConfigPatch,
|
|
safeEffectiveAgentLlmPolicy,
|
|
sameEffectiveAgentLlmWithoutStream,
|
|
} from './config.mjs';
|
|
import { collectApiKeys, isPathInside, readJson } from './io.mjs';
|
|
import { appendBounded, runProcess } from './process.mjs';
|
|
import { decodeUtf8Fatal, isIsolatedRunnerSuite } from './reporting.mjs';
|
|
import { killRunnerOnce, readRunnerStatus, runnerBootId } from './runtime.mjs';
|
|
|
|
const platformSessionFixtureEnv = 'GENARRATIVE_AGC_PLATFORM_SESSION_FIXTURE';
|
|
const platformSessionFixtureMaxBytes = 16 * 1024;
|
|
const isolatedPlatformSessionFixtureName =
|
|
'.deterministic-platform-session.json';
|
|
const platformSessionFixtureSchema =
|
|
'genarrative-agc-platform-session-fixture.v1';
|
|
|
|
export function isolatedSuiteProtectsSourceAppData() {
|
|
return (
|
|
isSupervisorSwarmTransientRetrySuite() ||
|
|
isSupervisorSwarmToolPlanHandoffRunnerKillSuite() ||
|
|
isSupervisorAutonomousPlayableLaneDefenseSuite() ||
|
|
isSupervisorSwarmInteractiveChatSuite()
|
|
);
|
|
}
|
|
|
|
export function isolatedSuiteUsesSiblingAppData() {
|
|
return isWebSearchSuite() || isolatedSuiteProtectsSourceAppData();
|
|
}
|
|
|
|
export function sameSupervisorPlayableProviderBinding(left, right) {
|
|
const scalarFields = [
|
|
'providerAgentMode',
|
|
'providerModel',
|
|
'providerApiKind',
|
|
'providerReasoningEffort',
|
|
'providerBaseUrlSha256',
|
|
];
|
|
return (
|
|
left != null &&
|
|
right != null &&
|
|
scalarFields.every((field) => left[field] === right[field]) &&
|
|
Array.isArray(left.boundAgentIds) &&
|
|
Array.isArray(right.boundAgentIds) &&
|
|
left.boundAgentIds.length === right.boundAgentIds.length &&
|
|
left.boundAgentIds.every(
|
|
(agentId, index) => agentId === right.boundAgentIds[index],
|
|
)
|
|
);
|
|
}
|
|
|
|
export function isolatedSuiteAppDataProfile() {
|
|
if (isSteerRunnerKillSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-steer-runner-kill-',
|
|
sentinelName: steerRunnerKillAppDataSentinelFileName,
|
|
sentinelSchema: steerRunnerKillAppDataSentinelSchema,
|
|
codePrefix: 'steer-runner-kill-appdata',
|
|
};
|
|
}
|
|
if (isGoalRuntimeSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-goal-',
|
|
sentinelName: goalAppDataSentinelFileName,
|
|
sentinelSchema: goalAppDataSentinelSchema,
|
|
codePrefix: 'goal-appdata',
|
|
};
|
|
}
|
|
if (isWebSearchSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-web-search-',
|
|
sentinelName: webSearchAppDataSentinelFileName,
|
|
sentinelSchema: webSearchAppDataSentinelSchema,
|
|
codePrefix: 'web-search-appdata',
|
|
};
|
|
}
|
|
if (isContextCompactionSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-context-compaction-',
|
|
sentinelName: contextCompactionAppDataSentinelFileName,
|
|
sentinelSchema: contextCompactionAppDataSentinelSchema,
|
|
codePrefix: 'context-compaction-appdata',
|
|
};
|
|
}
|
|
if (isUserInputRuntimeSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-user-input-',
|
|
sentinelName: userInputAppDataSentinelFileName,
|
|
sentinelSchema: userInputAppDataSentinelSchema,
|
|
codePrefix: 'user-input-appdata',
|
|
};
|
|
}
|
|
if (isScopedAgentsSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-scoped-agents-',
|
|
sentinelName: scopedAgentsAppDataSentinelFileName,
|
|
sentinelSchema: scopedAgentsAppDataSentinelSchema,
|
|
codePrefix: 'scoped-agents-appdata',
|
|
};
|
|
}
|
|
if (isProjectSkillSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-project-skill-',
|
|
sentinelName: projectSkillAppDataSentinelFileName,
|
|
sentinelSchema: projectSkillAppDataSentinelSchema,
|
|
codePrefix: 'project-skill-appdata',
|
|
};
|
|
}
|
|
if (isParallelReadSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-parallel-read-',
|
|
sentinelName: parallelReadAppDataSentinelFileName,
|
|
sentinelSchema: parallelReadAppDataSentinelSchema,
|
|
codePrefix: 'parallel-read-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmFinalReplyTransientRetrySuite()) {
|
|
return {
|
|
prefix:
|
|
'.agent-runtime-real-e2e-supervisor-swarm-final-reply-transient-retry-',
|
|
sentinelName:
|
|
supervisorSwarmFinalReplyTransientRetryAppDataSentinelFileName,
|
|
sentinelSchema:
|
|
supervisorSwarmFinalReplyTransientRetryAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-final-reply-transient-retry-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmToolPlanHandoffRunnerKillSuite()) {
|
|
return {
|
|
prefix:
|
|
'.agent-runtime-real-e2e-supervisor-swarm-tool-plan-handoff-runner-kill-',
|
|
sentinelName:
|
|
supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelFileName,
|
|
sentinelSchema:
|
|
supervisorSwarmToolPlanHandoffRunnerKillAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-tool-plan-handoff-runner-kill-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmInitialTransientRetrySuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-supervisor-swarm-transient-retry-',
|
|
sentinelName: supervisorSwarmTransientRetryAppDataSentinelFileName,
|
|
sentinelSchema: supervisorSwarmTransientRetryAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-transient-retry-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorAutonomousPlayableLaneDefenseSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-supervisor-autonomous-playable-',
|
|
sentinelName: supervisorAutonomousPlayableAppDataSentinelFileName,
|
|
sentinelSchema: supervisorAutonomousPlayableAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-autonomous-playable-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmAutonomousChatSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-supervisor-swarm-autonomous-chat-',
|
|
sentinelName: supervisorSwarmAutonomousChatAppDataSentinelFileName,
|
|
sentinelSchema: supervisorSwarmAutonomousChatAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-autonomous-chat-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmStaticIsolatedAutonomousChatSuite()) {
|
|
return {
|
|
prefix:
|
|
'.agent-runtime-real-e2e-supervisor-swarm-static-isolated-autonomous-chat-',
|
|
sentinelName:
|
|
supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelFileName,
|
|
sentinelSchema:
|
|
supervisorSwarmStaticIsolatedAutonomousChatAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-static-isolated-autonomous-chat-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmCollaborationPolicyMixedRecoverySuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-supervisor-swarm-collaboration-policy-',
|
|
sentinelName: supervisorSwarmCollaborationPolicyAppDataSentinelFileName,
|
|
sentinelSchema: supervisorSwarmCollaborationPolicyAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-collaboration-policy-appdata',
|
|
};
|
|
}
|
|
if (isSupervisorSwarmSuite()) {
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-supervisor-swarm-',
|
|
sentinelName: supervisorSwarmAppDataSentinelFileName,
|
|
sentinelSchema: supervisorSwarmAppDataSentinelSchema,
|
|
codePrefix: 'supervisor-swarm-appdata',
|
|
};
|
|
}
|
|
assert(
|
|
isResponseStreamSuite(),
|
|
'isolated-appdata-used-outside-isolated-suite',
|
|
);
|
|
return {
|
|
prefix: '.agent-runtime-real-e2e-response-stream-',
|
|
sentinelName: responseStreamAppDataSentinelFileName,
|
|
sentinelSchema: responseStreamAppDataSentinelSchema,
|
|
codePrefix: 'response-stream-appdata',
|
|
};
|
|
}
|
|
|
|
export async function createSentinelOwnedTempDirectory({
|
|
prefix,
|
|
sentinelName,
|
|
sentinel,
|
|
codePrefix,
|
|
}) {
|
|
const directory = await fs.mkdtemp(prefix);
|
|
try {
|
|
if (process.platform === 'win32') {
|
|
await secureWindowsOwnedTempPath(directory, true);
|
|
} else {
|
|
await fs.chmod(directory, 0o700);
|
|
}
|
|
const sentinelPath = path.join(directory, sentinelName);
|
|
await fs.writeFile(sentinelPath, `${JSON.stringify(sentinel)}\n`, {
|
|
flag: 'wx',
|
|
mode: 0o600,
|
|
});
|
|
if (process.platform === 'win32') {
|
|
await secureWindowsOwnedTempPath(sentinelPath, false);
|
|
}
|
|
return directory;
|
|
} catch (error) {
|
|
try {
|
|
await fs.rm(directory, { recursive: true, force: true });
|
|
} catch (cleanupError) {
|
|
throw codedError(
|
|
`${codePrefix}-sentinel-create-cleanup-failed`,
|
|
cleanupError,
|
|
);
|
|
}
|
|
throw codedError(`${codePrefix}-sentinel-create-failed`, error);
|
|
}
|
|
}
|
|
|
|
export async function secureWindowsOwnedTempPath(targetPath, isDirectory) {
|
|
assert(
|
|
process.platform === 'win32' && path.isAbsolute(targetPath),
|
|
'isolated-windows-owned-path-precondition-invalid',
|
|
);
|
|
const powershell = await findControlledWindowsPowerShell();
|
|
const script = String.raw`
|
|
$ErrorActionPreference = 'Stop'
|
|
$targetPath = [Environment]::GetEnvironmentVariable(
|
|
'AGC_OWNED_TEMP_PATH',
|
|
[EnvironmentVariableTarget]::Process)
|
|
$isDirectory = [Environment]::GetEnvironmentVariable(
|
|
'AGC_OWNED_TEMP_PATH_IS_DIRECTORY',
|
|
[EnvironmentVariableTarget]::Process) -eq '1'
|
|
if ([String]::IsNullOrWhiteSpace($targetPath)) { exit 70 }
|
|
$sid = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
|
|
$inheritance = [System.Security.AccessControl.InheritanceFlags]::None
|
|
if ($isDirectory) {
|
|
if (-not (Test-Path -LiteralPath $targetPath -PathType Container)) { exit 71 }
|
|
$acl = [System.Security.AccessControl.DirectorySecurity]::new()
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.SetOwner($sid)
|
|
$inheritance = [System.Security.AccessControl.InheritanceFlags]::ContainerInherit -bor [System.Security.AccessControl.InheritanceFlags]::ObjectInherit
|
|
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
$sid,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
$inheritance,
|
|
[System.Security.AccessControl.PropagationFlags]::None,
|
|
[System.Security.AccessControl.AccessControlType]::Allow)
|
|
} else {
|
|
if (-not (Test-Path -LiteralPath $targetPath -PathType Leaf)) { exit 72 }
|
|
$acl = [System.Security.AccessControl.FileSecurity]::new()
|
|
$acl.SetAccessRuleProtection($true, $false)
|
|
$acl.SetOwner($sid)
|
|
$rule = [System.Security.AccessControl.FileSystemAccessRule]::new(
|
|
$sid,
|
|
[System.Security.AccessControl.FileSystemRights]::FullControl,
|
|
[System.Security.AccessControl.AccessControlType]::Allow)
|
|
}
|
|
[void]$acl.AddAccessRule($rule)
|
|
Set-Acl -LiteralPath $targetPath -AclObject $acl
|
|
$targetItem = Get-Item -LiteralPath $targetPath -Force
|
|
$verified = $targetItem.GetAccessControl()
|
|
$verifiedOwner = $verified.GetOwner(
|
|
[System.Security.Principal.SecurityIdentifier])
|
|
$verifiedRules = @($verified.GetAccessRules(
|
|
$true,
|
|
$true,
|
|
[System.Security.Principal.SecurityIdentifier]))
|
|
if (
|
|
-not $verifiedOwner.Equals($sid) -or
|
|
-not $verified.AreAccessRulesProtected -or
|
|
$verifiedRules.Count -ne 1
|
|
) { exit 73 }
|
|
$verifiedRule = $verifiedRules[0]
|
|
if (
|
|
-not $verifiedRule.IdentityReference.Equals($sid) -or
|
|
$verifiedRule.AccessControlType -ne
|
|
[System.Security.AccessControl.AccessControlType]::Allow -or
|
|
[int]$verifiedRule.FileSystemRights -ne
|
|
[int][System.Security.AccessControl.FileSystemRights]::FullControl -or
|
|
$verifiedRule.InheritanceFlags -ne $inheritance -or
|
|
$verifiedRule.PropagationFlags -ne
|
|
[System.Security.AccessControl.PropagationFlags]::None -or
|
|
$verifiedRule.IsInherited
|
|
) { exit 74 }
|
|
`;
|
|
try {
|
|
await runProcess(
|
|
powershell,
|
|
['-NoLogo', '-NoProfile', '-NonInteractive', '-Command', script],
|
|
{
|
|
cwd: appRoot,
|
|
timeoutMs: 30_000,
|
|
env: controlledWindowsPowerShellEnvironment({
|
|
AGC_OWNED_TEMP_PATH: targetPath,
|
|
AGC_OWNED_TEMP_PATH_IS_DIRECTORY: isDirectory ? '1' : '0',
|
|
}),
|
|
},
|
|
);
|
|
} catch (error) {
|
|
throw codedError('isolated-windows-owned-path-secure-failed', error);
|
|
}
|
|
}
|
|
|
|
export async function captureSourceRunnerEndpointSnapshot(sourceConfigDir) {
|
|
const endpointPath = path.join(sourceConfigDir, runnerEndpointFileName);
|
|
const metadata = await fs.lstat(endpointPath).catch((error) => {
|
|
if (error?.code === 'ENOENT') return null;
|
|
throw error;
|
|
});
|
|
if (!metadata) return { exists: false, fingerprint: null };
|
|
assert(
|
|
metadata.isFile() && !metadata.isSymbolicLink(),
|
|
'source-runner-endpoint-not-regular-file',
|
|
);
|
|
const endpoint = await readJson(endpointPath);
|
|
const stableEndpoint = { ...endpoint };
|
|
delete stableEndpoint.heartbeatAt;
|
|
return {
|
|
exists: true,
|
|
fingerprint: hashValue(JSON.stringify(stableEndpoint)),
|
|
};
|
|
}
|
|
|
|
export async function verifySourceRunnerEndpointUnchanged() {
|
|
const sourceConfigDir = await fs.realpath(state.options.configDir);
|
|
const current = await captureSourceRunnerEndpointSnapshot(sourceConfigDir);
|
|
assert(
|
|
JSON.stringify(current) ===
|
|
JSON.stringify(state.isolatedRunner.sourceEndpointSnapshot),
|
|
'source-runner-endpoint-changed-during-isolated-suite',
|
|
);
|
|
state.isolatedRunner.sourceRunnerEndpointUnchanged = true;
|
|
}
|
|
|
|
export function closeSourceAppDataDirectoryGuard() {
|
|
const watcher = state.isolatedRunner.sourceAppDataDirectoryWatcher;
|
|
if (!watcher) return;
|
|
watcher.close();
|
|
state.isolatedRunner.sourceAppDataDirectoryWatcher = null;
|
|
}
|
|
|
|
export function sourceAppDataDirectoryEventIsViolation(
|
|
fileName,
|
|
profilePrefix,
|
|
sourceEndpointSnapshot,
|
|
) {
|
|
const name = Buffer.isBuffer(fileName)
|
|
? fileName.toString('utf8')
|
|
: String(fileName ?? '');
|
|
return (
|
|
name.startsWith(profilePrefix) ||
|
|
(sourceEndpointSnapshot?.exists === false &&
|
|
name === runnerEndpointFileName)
|
|
);
|
|
}
|
|
|
|
export function startSourceAppDataDirectoryGuard(sourceConfigDir, profile) {
|
|
if (!isolatedSuiteProtectsSourceAppData()) return;
|
|
assert(
|
|
!state.isolatedRunner.sourceAppDataDirectoryWatcher,
|
|
'source-appdata-directory-guard-already-started',
|
|
);
|
|
const watcher = watchFileSystem(
|
|
sourceConfigDir,
|
|
{ persistent: false },
|
|
(_eventType, fileName) => {
|
|
if (
|
|
sourceAppDataDirectoryEventIsViolation(
|
|
fileName,
|
|
profile.prefix,
|
|
state.isolatedRunner.sourceEndpointSnapshot,
|
|
)
|
|
) {
|
|
state.isolatedRunner.sourceAppDataDirectoryViolationCount += 1;
|
|
}
|
|
},
|
|
);
|
|
watcher.on('error', () => {
|
|
state.isolatedRunner.sourceAppDataDirectoryViolationCount += 1;
|
|
});
|
|
state.isolatedRunner.sourceAppDataDirectoryWatcher = watcher;
|
|
}
|
|
|
|
export async function verifySourceAppDataDirectoryUntouched() {
|
|
if (!isolatedSuiteProtectsSourceAppData()) return;
|
|
closeSourceAppDataDirectoryGuard();
|
|
const sourceConfigDir = await fs.realpath(state.options.configDir);
|
|
const profile = isolatedSuiteAppDataProfile();
|
|
const entries = await fs.readdir(sourceConfigDir);
|
|
assert(
|
|
state.isolatedRunner.sourceAppDataDirectoryViolationCount === 0 &&
|
|
!entries.some((name) => name.startsWith(profile.prefix)),
|
|
'source-appdata-directory-touched-by-suite',
|
|
);
|
|
state.isolatedRunner.sourceAppDataDirectoryUntouched = true;
|
|
}
|
|
|
|
async function readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir) {
|
|
const rawPath = process.env[platformSessionFixtureEnv];
|
|
assert(
|
|
isNonEmptyString(rawPath) && path.isAbsolute(rawPath),
|
|
'supervisor-autonomous-playable-platform-session-fixture-missing',
|
|
);
|
|
const sourceRealPath = await fs.realpath(sourceConfigDir);
|
|
const requestedPath = path.resolve(rawPath);
|
|
const requestedMetadata = await fs.lstat(requestedPath).catch((error) => {
|
|
if (error?.code === 'ENOENT') return null;
|
|
throw error;
|
|
});
|
|
assert(
|
|
requestedMetadata?.isFile() && !requestedMetadata.isSymbolicLink(),
|
|
'supervisor-autonomous-playable-platform-session-fixture-not-regular',
|
|
);
|
|
assert(
|
|
requestedMetadata.size <= platformSessionFixtureMaxBytes,
|
|
'supervisor-autonomous-playable-platform-session-fixture-too-large',
|
|
);
|
|
const realPath = await fs.realpath(requestedPath);
|
|
assert(
|
|
isPathInside(sourceRealPath, realPath),
|
|
'supervisor-autonomous-playable-platform-session-fixture-outside-config',
|
|
);
|
|
const bytes = await fs.readFile(realPath);
|
|
assert(
|
|
bytes.length <= platformSessionFixtureMaxBytes,
|
|
'supervisor-autonomous-playable-platform-session-fixture-too-large',
|
|
);
|
|
let fixture;
|
|
try {
|
|
fixture = JSON.parse(
|
|
decodeUtf8Fatal(bytes, 'platform-session-fixture-invalid-utf8'),
|
|
);
|
|
} catch (error) {
|
|
throw codedError(
|
|
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
|
error,
|
|
);
|
|
}
|
|
const expectedKeys = [
|
|
'schemaVersion',
|
|
'userId',
|
|
'accessToken',
|
|
'apiBaseUrl',
|
|
'generation',
|
|
];
|
|
assert(
|
|
isPlainObject(fixture) &&
|
|
JSON.stringify(Object.keys(fixture).sort()) ===
|
|
JSON.stringify([...expectedKeys].sort()) &&
|
|
fixture.schemaVersion === platformSessionFixtureSchema &&
|
|
isNonEmptyString(fixture.userId) &&
|
|
isNonEmptyString(fixture.accessToken) &&
|
|
isNonEmptyString(fixture.apiBaseUrl) &&
|
|
Number.isSafeInteger(fixture.generation) &&
|
|
fixture.generation > 0,
|
|
'supervisor-autonomous-playable-platform-session-fixture-invalid',
|
|
);
|
|
return {
|
|
sourcePath: realPath,
|
|
bytes,
|
|
fixture,
|
|
sha256: createHash('sha256').update(bytes).digest('hex'),
|
|
};
|
|
}
|
|
|
|
async function installPlatformSessionFixtureIntoIsolatedAppData(
|
|
sourceConfigDir,
|
|
appDataDir,
|
|
) {
|
|
if (!isSupervisorAutonomousPlayableLaneDefenseSuite()) return;
|
|
const source =
|
|
await readPlatformSessionFixtureForIsolatedSuite(sourceConfigDir);
|
|
const isolatedPath = path.join(
|
|
appDataDir,
|
|
isolatedPlatformSessionFixtureName,
|
|
);
|
|
await fs.copyFile(
|
|
source.sourcePath,
|
|
isolatedPath,
|
|
fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE,
|
|
);
|
|
await fs.chmod(isolatedPath, 0o600).catch(() => {});
|
|
const isolatedMetadata = await fs.lstat(isolatedPath);
|
|
assert(
|
|
isolatedMetadata.isFile() &&
|
|
!isolatedMetadata.isSymbolicLink() &&
|
|
isolatedMetadata.size === source.bytes.length,
|
|
'supervisor-autonomous-playable-platform-session-fixture-copy-invalid',
|
|
);
|
|
const isolatedBytes = await fs.readFile(isolatedPath);
|
|
assert(
|
|
createHash('sha256').update(isolatedBytes).digest('hex') === source.sha256,
|
|
'supervisor-autonomous-playable-platform-session-fixture-copy-mismatch',
|
|
);
|
|
state.isolatedRunner.platformSessionFixtureSourcePath = source.sourcePath;
|
|
state.isolatedRunner.platformSessionFixturePath = isolatedPath;
|
|
state.isolatedRunner.platformSessionFixtureSha256 = source.sha256;
|
|
state.isolatedRunner.platformSessionFixturePreviousEnv =
|
|
Object.prototype.hasOwnProperty.call(process.env, platformSessionFixtureEnv)
|
|
? process.env[platformSessionFixtureEnv]
|
|
: undefined;
|
|
process.env[platformSessionFixtureEnv] = isolatedPath;
|
|
state.formalConfigPathTranscriptScanner?.addSecrets(
|
|
absolutePathVariants(source.sourcePath, isolatedPath),
|
|
);
|
|
const previousLeakCount = state.transcriptScanner?.count ?? 0;
|
|
state.secrets = [...new Set([...state.secrets, source.fixture.accessToken])];
|
|
rebuildSupervisorSwarmTranscriptScanner();
|
|
state.transcriptScanner.count = previousLeakCount;
|
|
}
|
|
|
|
export async function prepareIsolatedSuiteAppData({
|
|
streamAgentId = null,
|
|
webSearchAgentId = null,
|
|
configOverlay = null,
|
|
} = {}) {
|
|
assert(
|
|
isIsolatedRunnerSuite(),
|
|
'isolated-appdata-used-outside-isolated-suite',
|
|
);
|
|
assert(
|
|
[streamAgentId, webSearchAgentId, configOverlay].filter(Boolean).length <=
|
|
1,
|
|
'isolated-appdata-multiple-overlays-forbidden',
|
|
);
|
|
if (configOverlay) {
|
|
assert(
|
|
isPlainObject(configOverlay) &&
|
|
collectApiKeys(configOverlay).length === 0,
|
|
'isolated-appdata-config-overlay-invalid',
|
|
);
|
|
}
|
|
const profile = isolatedSuiteAppDataProfile();
|
|
const suiteSecrets = new Set(state.secrets);
|
|
const sourceConfigDir = await fs.realpath(state.options.configDir);
|
|
state.isolatedRunner.sourceEndpointSnapshot =
|
|
await captureSourceRunnerEndpointSnapshot(sourceConfigDir);
|
|
const ownerToken = randomUUID();
|
|
const createdAt = Date.now();
|
|
const appDataParent = isolatedSuiteUsesSiblingAppData()
|
|
? path.dirname(sourceConfigDir)
|
|
: sourceConfigDir;
|
|
const appDataDir = await createSentinelOwnedTempDirectory({
|
|
prefix: path.join(appDataParent, profile.prefix),
|
|
sentinelName: profile.sentinelName,
|
|
sentinel: {
|
|
schemaVersion: profile.sentinelSchema,
|
|
token: ownerToken,
|
|
ownerPid: process.pid,
|
|
createdAt,
|
|
},
|
|
codePrefix: profile.codePrefix,
|
|
});
|
|
state.isolatedRunner.appDataDir = appDataDir;
|
|
state.isolatedRunner.ownerToken = ownerToken;
|
|
state.isolatedRunner.createdAt = createdAt;
|
|
const realAppDataDir = await fs.realpath(appDataDir);
|
|
state.formalConfigPathTranscriptScanner?.addSecrets(
|
|
absolutePathVariants(appDataDir, realAppDataDir),
|
|
);
|
|
if (isolatedSuiteProtectsSourceAppData()) {
|
|
assert(
|
|
!isPathInside(sourceConfigDir, realAppDataDir) &&
|
|
path.dirname(realAppDataDir) === path.dirname(sourceConfigDir),
|
|
`${profile.codePrefix}-source-appdata-write-boundary-invalid`,
|
|
);
|
|
startSourceAppDataDirectoryGuard(sourceConfigDir, profile);
|
|
}
|
|
const sourceConfigs = [];
|
|
for (const name of [configFileName, localConfigFileName]) {
|
|
const sourcePath = path.join(sourceConfigDir, name);
|
|
const metadata = await fs.lstat(sourcePath).catch((error) => {
|
|
if (error?.code === 'ENOENT') return null;
|
|
throw error;
|
|
});
|
|
if (!metadata) {
|
|
assert(
|
|
name !== configFileName,
|
|
`${profile.codePrefix}-source-config-missing`,
|
|
);
|
|
continue;
|
|
}
|
|
assert(
|
|
metadata.isFile() && !metadata.isSymbolicLink(),
|
|
`${profile.codePrefix}-source-config-not-regular-file`,
|
|
);
|
|
const sourceContent = await fs.readFile(sourcePath);
|
|
let sourceConfig;
|
|
try {
|
|
sourceConfig = JSON.parse(
|
|
decodeUtf8Fatal(
|
|
sourceContent,
|
|
`${profile.codePrefix}-linked-config-invalid-utf8`,
|
|
),
|
|
);
|
|
} catch (error) {
|
|
throw codedError(
|
|
`${profile.codePrefix}-linked-config-json-invalid`,
|
|
error,
|
|
);
|
|
}
|
|
assert(
|
|
isPlainObject(sourceConfig),
|
|
`${profile.codePrefix}-linked-config-root-invalid`,
|
|
);
|
|
for (const secret of collectApiKeys(sourceConfig)) suiteSecrets.add(secret);
|
|
sourceConfigs.push({
|
|
name,
|
|
sourcePath,
|
|
metadata,
|
|
sourceContent,
|
|
config: sourceConfig,
|
|
});
|
|
}
|
|
|
|
const mergedSourceConfig = {};
|
|
for (const source of sourceConfigs) {
|
|
mergeConfigPatch(mergedSourceConfig, source.config);
|
|
}
|
|
const overlayAgentId = streamAgentId ?? webSearchAgentId;
|
|
const sourceEffective = overlayAgentId
|
|
? effectiveAgentLlmConfig(mergedSourceConfig, overlayAgentId)
|
|
: null;
|
|
let activeConfigSource = null;
|
|
if (overlayAgentId && (webSearchAgentId || sourceEffective.stream !== true)) {
|
|
const sameEffective = webSearchAgentId
|
|
? sameEffectiveAgentLlmWithoutWebSearch
|
|
: sameEffectiveAgentLlmWithoutStream;
|
|
activeConfigSource =
|
|
sourceConfigs.find(
|
|
(source) =>
|
|
source.name === configFileName &&
|
|
sameEffective(
|
|
effectiveAgentLlmConfig(source.config, overlayAgentId),
|
|
sourceEffective,
|
|
),
|
|
) ??
|
|
sourceConfigs.find((source) =>
|
|
sameEffective(
|
|
effectiveAgentLlmConfig(source.config, overlayAgentId),
|
|
sourceEffective,
|
|
),
|
|
);
|
|
assert(
|
|
Boolean(activeConfigSource),
|
|
webSearchAgentId
|
|
? 'web-search-source-config-cannot-accept-search-only-overlay'
|
|
: 'response-stream-source-config-cannot-accept-stream-only-overlay',
|
|
);
|
|
}
|
|
|
|
for (const source of sourceConfigs) {
|
|
const linkedName = configOverlay
|
|
? `.source-${source.name}`
|
|
: activeConfigSource
|
|
? source === activeConfigSource
|
|
? configFileName
|
|
: `.source-${source.name}`
|
|
: source.name;
|
|
const linkedPath = path.join(appDataDir, linkedName);
|
|
const storageMode =
|
|
isWebSearchSuite() ||
|
|
isScopedAgentsSuite() ||
|
|
isProjectSkillSuite() ||
|
|
isParallelReadSuite() ||
|
|
isSupervisorSwarmSuite() ||
|
|
isSupervisorAutonomousPlayableLaneDefenseSuite()
|
|
? 'private-copy'
|
|
: 'hardlink';
|
|
try {
|
|
if (storageMode === 'private-copy') {
|
|
await fs.copyFile(
|
|
source.sourcePath,
|
|
linkedPath,
|
|
fsConstants.COPYFILE_EXCL | fsConstants.COPYFILE_FICLONE,
|
|
);
|
|
await fs.chmod(linkedPath, 0o600);
|
|
} else {
|
|
// Existing isolated suites share credential-bearing config inodes.
|
|
await fs.link(source.sourcePath, linkedPath);
|
|
}
|
|
} catch (error) {
|
|
throw codedError(`${profile.codePrefix}-config-replica-failed`, error);
|
|
}
|
|
const linkedMetadata = await fs.lstat(linkedPath);
|
|
const privateCopyValid =
|
|
storageMode === 'private-copy' &&
|
|
(linkedMetadata.dev !== source.metadata.dev ||
|
|
linkedMetadata.ino !== source.metadata.ino) &&
|
|
(process.platform === 'win32' || (linkedMetadata.mode & 0o077) === 0);
|
|
const hardlinkValid =
|
|
storageMode === 'hardlink' &&
|
|
linkedMetadata.dev === source.metadata.dev &&
|
|
linkedMetadata.ino === source.metadata.ino;
|
|
assert(
|
|
linkedMetadata.isFile() &&
|
|
!linkedMetadata.isSymbolicLink() &&
|
|
(privateCopyValid || hardlinkValid),
|
|
`${profile.codePrefix}-config-replica-identity-invalid`,
|
|
);
|
|
state.isolatedRunner.configLinks.push({
|
|
storageMode,
|
|
sourceName: source.name,
|
|
linkedName,
|
|
sourcePath: source.sourcePath,
|
|
linkedPath,
|
|
dev: source.metadata.dev,
|
|
ino: source.metadata.ino,
|
|
linkedDev: linkedMetadata.dev,
|
|
linkedIno: linkedMetadata.ino,
|
|
nlink: source.metadata.nlink,
|
|
sourceMode: source.metadata.mode,
|
|
sourceSize: source.metadata.size,
|
|
sourceMtimeMs: source.metadata.mtimeMs,
|
|
sourceCtimeMs: source.metadata.ctimeMs,
|
|
sha256: createHash('sha256').update(source.sourceContent).digest('hex'),
|
|
});
|
|
}
|
|
if (configOverlay) {
|
|
const primaryConfigPath = path.join(appDataDir, configFileName);
|
|
const localOverlayPath = path.join(appDataDir, localConfigFileName);
|
|
await fs.writeFile(
|
|
primaryConfigPath,
|
|
`${JSON.stringify(mergedSourceConfig)}\n`,
|
|
{ flag: 'wx', mode: 0o600 },
|
|
);
|
|
await fs.writeFile(localOverlayPath, `${JSON.stringify(configOverlay)}\n`, {
|
|
flag: 'wx',
|
|
mode: 0o600,
|
|
});
|
|
const [primaryMetadata, overlayMetadata] = await Promise.all([
|
|
fs.lstat(primaryConfigPath),
|
|
fs.lstat(localOverlayPath),
|
|
]);
|
|
assert(
|
|
primaryMetadata.isFile() &&
|
|
!primaryMetadata.isSymbolicLink() &&
|
|
overlayMetadata.isFile() &&
|
|
!overlayMetadata.isSymbolicLink() &&
|
|
(primaryMetadata.mode & 0o077) === 0 &&
|
|
(overlayMetadata.mode & 0o077) === 0,
|
|
`${profile.codePrefix}-materialized-config-invalid`,
|
|
);
|
|
state.isolatedRunner.configOverlayCreated = true;
|
|
} else {
|
|
assert(
|
|
state.isolatedRunner.configLinks.some(
|
|
(link) => link.linkedName === configFileName,
|
|
),
|
|
`${profile.codePrefix}-primary-config-link-missing`,
|
|
);
|
|
}
|
|
|
|
if (activeConfigSource) {
|
|
const overrideKey = webSearchAgentId ? 'webSearchEnabled' : 'stream';
|
|
const overlay = { agentLlm: { [overlayAgentId]: { [overrideKey]: true } } };
|
|
assert(
|
|
collectApiKeys(overlay).length === 0 &&
|
|
JSON.stringify(Object.keys(overlay)) === JSON.stringify(['agentLlm']) &&
|
|
JSON.stringify(Object.keys(overlay.agentLlm)) ===
|
|
JSON.stringify([overlayAgentId]) &&
|
|
JSON.stringify(Object.keys(overlay.agentLlm[overlayAgentId])) ===
|
|
JSON.stringify([overrideKey]),
|
|
webSearchAgentId
|
|
? 'web-search-overlay-shape-invalid'
|
|
: 'response-stream-overlay-shape-invalid',
|
|
);
|
|
await fs.writeFile(
|
|
path.join(appDataDir, localConfigFileName),
|
|
`${JSON.stringify(overlay)}\n`,
|
|
{ flag: 'wx', mode: 0o600 },
|
|
);
|
|
if (webSearchAgentId) {
|
|
state.isolatedRunner.webSearchOverrideCreated = true;
|
|
} else {
|
|
state.isolatedRunner.streamOverrideCreated = true;
|
|
}
|
|
}
|
|
|
|
const previousLeakCount = state.transcriptScanner?.count ?? 0;
|
|
state.secrets = [...suiteSecrets];
|
|
rebuildSupervisorSwarmTranscriptScanner();
|
|
state.transcriptScanner.count = previousLeakCount;
|
|
await installPlatformSessionFixtureIntoIsolatedAppData(
|
|
sourceConfigDir,
|
|
appDataDir,
|
|
);
|
|
const unexpectedEndpoint = await fs
|
|
.lstat(path.join(appDataDir, runnerEndpointFileName))
|
|
.catch((error) => {
|
|
if (error?.code === 'ENOENT') return null;
|
|
throw error;
|
|
});
|
|
assert(!unexpectedEndpoint, `${profile.codePrefix}-endpoint-preexisted`);
|
|
state.runtimeConfigDir = appDataDir;
|
|
if (isSupervisorAutonomousPlayableLaneDefenseSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const expectedBinding = state.config.providerBinding;
|
|
const agentIds = [projectSupervisorAgentId];
|
|
const bindings = agentIds.map((agentId) => {
|
|
const effective = effectiveAgentLlmConfig(isolatedConfig.config, agentId);
|
|
assert(
|
|
['apiKey', 'baseUrl', 'model', 'apiKind', 'reasoningEffort'].every(
|
|
(key) => isNonEmptyString(effective[key]),
|
|
),
|
|
'supervisor-autonomous-playable-effective-provider-incomplete',
|
|
);
|
|
return {
|
|
providerModel: effective.model.trim(),
|
|
providerApiKind: effective.apiKind.trim(),
|
|
providerReasoningEffort: effective.reasoningEffort.trim(),
|
|
providerBaseUrlSha256: hashValue(effective.baseUrl.trim()),
|
|
};
|
|
});
|
|
const effectiveBinding = {
|
|
...bindings[0],
|
|
boundAgentIds: [...agentIds].sort(),
|
|
};
|
|
assert(
|
|
expectedBinding &&
|
|
bindings.every(
|
|
(binding) => JSON.stringify(binding) === JSON.stringify(bindings[0]),
|
|
) &&
|
|
sameSupervisorPlayableProviderBinding(
|
|
effectiveBinding,
|
|
expectedBinding,
|
|
),
|
|
'supervisor-autonomous-playable-effective-provider-binding-mismatch',
|
|
);
|
|
state.supervisorAutonomousPlayable.expectedProviderBinding = {
|
|
...expectedBinding,
|
|
};
|
|
state.supervisorAutonomousPlayable.effectiveProviderBinding =
|
|
effectiveBinding;
|
|
}
|
|
if (streamAgentId) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
streamAgentId,
|
|
);
|
|
assert(
|
|
isolatedEffective.stream === true &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'response-stream-effective-llm-config-invalid',
|
|
);
|
|
state.responseStream.effectiveStreamEnabled = true;
|
|
}
|
|
if (webSearchAgentId) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
webSearchAgentId,
|
|
);
|
|
if (isolatedEffective.apiKind === 'anthropic') {
|
|
state.webSearch.gatewayDiagnosis = 'anthropic-web-search-unsupported';
|
|
throw new BlockedError(['web-search-anthropic-unsupported']);
|
|
}
|
|
assert(
|
|
isolatedEffective.webSearchEnabled === true &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'web-search-effective-llm-config-invalid',
|
|
);
|
|
state.webSearch.effectiveEnabled = true;
|
|
}
|
|
if (isUserInputRuntimeSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
projectSupervisorAgentId,
|
|
);
|
|
assert(
|
|
isolatedEffective.model === 'gpt-5.5' &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'user-input-effective-gpt-5-5-config-invalid',
|
|
);
|
|
}
|
|
if (isScopedAgentsSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
mainAgentId,
|
|
);
|
|
assert(
|
|
isolatedEffective.model === 'gpt-5.5' &&
|
|
isolatedEffective.apiKind === 'openai_chat' &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'scoped-agents-effective-openai-chat-gpt-5-5-config-invalid',
|
|
);
|
|
state.scopedAgents.effectiveModel = isolatedEffective.model;
|
|
state.scopedAgents.effectiveApiKind = isolatedEffective.apiKind;
|
|
}
|
|
if (isProjectSkillSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
mainAgentId,
|
|
);
|
|
assert(
|
|
isolatedEffective.model === 'gpt-5.5' &&
|
|
isolatedEffective.apiKind === 'openai_chat' &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'project-skill-effective-openai-chat-gpt-5-5-config-invalid',
|
|
);
|
|
state.projectSkill.effectiveModel = isolatedEffective.model;
|
|
state.projectSkill.effectiveApiKind = isolatedEffective.apiKind;
|
|
}
|
|
if (isParallelReadSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
mainAgentId,
|
|
);
|
|
assert(
|
|
isolatedEffective.model === 'gpt-5.5' &&
|
|
isolatedEffective.apiKind === 'openai_chat' &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'parallel-read-effective-openai-chat-gpt-5-5-config-invalid',
|
|
);
|
|
state.parallelRead.effectiveModel = isolatedEffective.model;
|
|
state.parallelRead.effectiveApiKind = isolatedEffective.apiKind;
|
|
}
|
|
if (isSupervisorSwarmSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const requiredAgentIds = supervisorSwarmRequiredAgentIds;
|
|
const requiredEffectiveConfigs = requiredAgentIds.map((agentId) => [
|
|
agentId,
|
|
effectiveAgentLlmConfig(isolatedConfig.config, agentId),
|
|
]);
|
|
assert(
|
|
requiredEffectiveConfigs.every(
|
|
([agentId, effective]) =>
|
|
(isSupervisorSwarmToolPlanHandoffRunnerKillSuite()
|
|
? isNonEmptyString(effective.model)
|
|
: effective.model === 'gpt-5.5') &&
|
|
effective.apiKind === 'openai_chat' &&
|
|
isNonEmptyString(effective.reasoningEffort) &&
|
|
Number.isSafeInteger(effective.requestTimeoutMs) &&
|
|
effective.requestTimeoutMs > 0 &&
|
|
Number.isSafeInteger(effective.maxRetries) &&
|
|
effective.maxRetries >=
|
|
(isSupervisorSwarmInteractiveChatSuite() ||
|
|
(isSupervisorSwarmTransientRetrySuite() &&
|
|
agentId !== supervisorSwarmTransientRetryTargetAgentId)
|
|
? 0
|
|
: 1) &&
|
|
effective.maxRetries <= 3 &&
|
|
Number.isSafeInteger(effective.retryBackoffMs) &&
|
|
effective.retryBackoffMs > 0 &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof effective[key] === 'string' &&
|
|
effective[key].trim().length > 0,
|
|
),
|
|
),
|
|
isSupervisorSwarmToolPlanHandoffRunnerKillSuite()
|
|
? 'supervisor-swarm-effective-openai-chat-configured-model-invalid'
|
|
: 'supervisor-swarm-effective-openai-chat-gpt-5-5-config-invalid',
|
|
);
|
|
const globalEffective = effectiveAgentLlmConfig(
|
|
{ llm: isolatedConfig.config.llm },
|
|
'',
|
|
);
|
|
if (isSupervisorSwarmMixedHarnessSuite()) {
|
|
assert(
|
|
globalEffective.model === 'gpt-5.5' &&
|
|
globalEffective.apiKind === 'openai_chat' &&
|
|
isNonEmptyString(globalEffective.reasoningEffort) &&
|
|
Number.isSafeInteger(globalEffective.requestTimeoutMs) &&
|
|
globalEffective.requestTimeoutMs > 0 &&
|
|
Number.isSafeInteger(globalEffective.maxRetries) &&
|
|
globalEffective.maxRetries >= 0 &&
|
|
globalEffective.maxRetries <= 3 &&
|
|
Number.isSafeInteger(globalEffective.retryBackoffMs) &&
|
|
globalEffective.retryBackoffMs > 0 &&
|
|
['apiKey', 'baseUrl', 'model'].every((key) =>
|
|
isNonEmptyString(globalEffective[key]),
|
|
),
|
|
'supervisor-swarm-mixed-default-provider-policy-invalid',
|
|
);
|
|
}
|
|
const configuredAgentIds = Object.keys(
|
|
isPlainObject(isolatedConfig.config.agentLlm)
|
|
? isolatedConfig.config.agentLlm
|
|
: {},
|
|
).filter(isNonEmptyString);
|
|
const policyAgentIds = [
|
|
...new Set([...requiredAgentIds, ...configuredAgentIds]),
|
|
].sort();
|
|
const effectivePolicies = policyAgentIds.map((agentId) => [
|
|
agentId,
|
|
effectiveAgentLlmConfig(isolatedConfig.config, agentId),
|
|
]);
|
|
const supervisorEffective = requiredEffectiveConfigs.find(
|
|
([agentId]) => agentId === projectSupervisorAgentId,
|
|
)[1];
|
|
state.supervisorSwarm.effectiveModel = supervisorEffective.model;
|
|
state.supervisorSwarm.effectiveApiKind = supervisorEffective.apiKind;
|
|
state.supervisorSwarm.effectiveReasoningEffort =
|
|
supervisorEffective.reasoningEffort;
|
|
state.supervisorSwarm.effectiveRequestTimeoutMs =
|
|
supervisorEffective.requestTimeoutMs;
|
|
state.supervisorSwarm.effectiveMaxRetries = supervisorEffective.maxRetries;
|
|
state.supervisorSwarm.effectiveRetryBackoffMs =
|
|
supervisorEffective.retryBackoffMs;
|
|
state.supervisorSwarm.effectiveDefaultPolicy =
|
|
safeEffectiveAgentLlmPolicy(globalEffective);
|
|
state.supervisorSwarm.effectiveAgentPolicies = Object.fromEntries(
|
|
effectivePolicies.map(([agentId, effective]) => [
|
|
agentId,
|
|
safeEffectiveAgentLlmPolicy(effective),
|
|
]),
|
|
);
|
|
}
|
|
if (isSteerRunnerKillSuite()) {
|
|
const isolatedConfig = await loadConfig(appDataDir);
|
|
const isolatedEffective = effectiveAgentLlmConfig(
|
|
isolatedConfig.config,
|
|
mainAgentId,
|
|
);
|
|
assert(
|
|
isolatedEffective.model === 'gpt-5.5' &&
|
|
isolatedEffective.apiKind === 'openai_chat' &&
|
|
['apiKey', 'baseUrl', 'model'].every(
|
|
(key) =>
|
|
typeof isolatedEffective[key] === 'string' &&
|
|
isolatedEffective[key].trim().length > 0,
|
|
),
|
|
'steer-runner-kill-effective-openai-chat-gpt-5-5-config-invalid',
|
|
);
|
|
state.steerRunnerKill.effectiveModel = isolatedEffective.model;
|
|
state.steerRunnerKill.effectiveApiKind = isolatedEffective.apiKind;
|
|
}
|
|
}
|
|
|
|
export async function readIsolatedAppDataSentinel() {
|
|
const runner = state.isolatedRunner;
|
|
const profile = isolatedSuiteAppDataProfile();
|
|
assert(
|
|
isNonEmptyString(runner.appDataDir) && isNonEmptyString(runner.ownerToken),
|
|
'isolated-appdata-ownership-missing',
|
|
);
|
|
const sentinelPath = path.join(runner.appDataDir, profile.sentinelName);
|
|
const metadata = await fs.lstat(sentinelPath);
|
|
const sentinel = await readJson(sentinelPath);
|
|
assert(
|
|
metadata.isFile() &&
|
|
!metadata.isSymbolicLink() &&
|
|
sentinel.schemaVersion === profile.sentinelSchema &&
|
|
sentinel.token === runner.ownerToken &&
|
|
sentinel.ownerPid === process.pid &&
|
|
sentinel.createdAt === runner.createdAt,
|
|
'isolated-appdata-ownership-invalid',
|
|
);
|
|
return sentinel;
|
|
}
|
|
|
|
export async function inspectOwnedRunnerIdentity(status) {
|
|
await readIsolatedAppDataSentinel();
|
|
const runner = state.isolatedRunner;
|
|
const pid = Number(status?.pid ?? status?.status?.pid);
|
|
const bootId = runnerBootId(status);
|
|
assert(
|
|
status?.running === true &&
|
|
Number.isSafeInteger(pid) &&
|
|
pid > 1 &&
|
|
pid !== process.pid &&
|
|
isNonEmptyString(bootId),
|
|
'isolated-owned-runner-status-invalid',
|
|
);
|
|
const endpointPath = path.join(runner.appDataDir, runnerEndpointFileName);
|
|
const endpointMetadata = await fs.lstat(endpointPath);
|
|
const endpoint = await readJson(endpointPath);
|
|
assert(
|
|
endpointMetadata.isFile() &&
|
|
!endpointMetadata.isSymbolicLink() &&
|
|
endpoint.pid === pid &&
|
|
endpoint.bootId === bootId &&
|
|
endpoint.protocolVersion === status.protocolVersion &&
|
|
endpoint.port === status.port &&
|
|
Number.isSafeInteger(endpoint.heartbeatAt) &&
|
|
endpoint.heartbeatAt >= runner.createdAt &&
|
|
isNonEmptyString(endpoint.token) &&
|
|
endpoint.token.length >= 32,
|
|
'isolated-owned-runner-endpoint-identity-invalid',
|
|
);
|
|
return {
|
|
pid,
|
|
bootId,
|
|
protocolVersion: endpoint.protocolVersion,
|
|
port: endpoint.port,
|
|
processIdentity: await captureOwnedRunnerProcessIdentity(pid),
|
|
};
|
|
}
|
|
|
|
export async function claimOwnedRunner(status = null) {
|
|
const liveStatus = status ?? (await readRunnerStatus());
|
|
const identity = await inspectOwnedRunnerIdentity(liveStatus);
|
|
const current = state.isolatedRunner.current;
|
|
if (current) {
|
|
assert(
|
|
current.pid === identity.pid &&
|
|
current.bootId === identity.bootId &&
|
|
current.processIdentity.fingerprint ===
|
|
identity.processIdentity.fingerprint &&
|
|
current.killHandle?.closed === false,
|
|
'isolated-owned-runner-identity-changed-after-claim',
|
|
);
|
|
return current;
|
|
}
|
|
|
|
const killHandle = await openOwnedRunnerKillHandle(identity.pid);
|
|
try {
|
|
const rechecked = await inspectOwnedRunnerIdentity(
|
|
await readRunnerStatus(),
|
|
);
|
|
assert(
|
|
rechecked.pid === identity.pid &&
|
|
rechecked.bootId === identity.bootId &&
|
|
rechecked.protocolVersion === identity.protocolVersion &&
|
|
rechecked.port === identity.port &&
|
|
rechecked.processIdentity.fingerprint ===
|
|
identity.processIdentity.fingerprint,
|
|
'isolated-owned-runner-identity-changed-during-pidfd-claim',
|
|
);
|
|
} catch (error) {
|
|
await closeOwnedRunnerKillHandle(killHandle).catch(() => {});
|
|
throw error;
|
|
}
|
|
state.isolatedRunner.current = { ...identity, killHandle };
|
|
state.isolatedRunner.pidfdClaimCount += 1;
|
|
return state.isolatedRunner.current;
|
|
}
|
|
|
|
export async function verifyOwnedRunnerForKill() {
|
|
const claimed = state.isolatedRunner.current;
|
|
assert(claimed, 'isolated-owned-runner-not-claimed');
|
|
const current = await inspectOwnedRunnerIdentity(await readRunnerStatus());
|
|
assert(
|
|
current.pid === claimed.pid &&
|
|
current.bootId === claimed.bootId &&
|
|
current.protocolVersion === claimed.protocolVersion &&
|
|
current.port === claimed.port &&
|
|
current.processIdentity.fingerprint ===
|
|
claimed.processIdentity.fingerprint &&
|
|
claimed.killHandle?.pid === claimed.pid &&
|
|
claimed.killHandle.closed === false,
|
|
'isolated-owned-runner-identity-changed-before-kill',
|
|
);
|
|
return claimed;
|
|
}
|
|
|
|
export async function ensureOwnedRunnerStableKillSupport() {
|
|
if (process.platform === 'linux') {
|
|
const python = await findControlledLinuxPython();
|
|
const probe =
|
|
'import os, signal; assert hasattr(os, "pidfd_open") and hasattr(signal, "pidfd_send_signal"); fd = os.pidfd_open(os.getpid(), 0); os.close(fd)';
|
|
try {
|
|
await runProcess(python, ['-I', '-S', '-c', probe], {
|
|
cwd: appRoot,
|
|
timeoutMs: 30_000,
|
|
env: { LANG: 'C', LC_ALL: 'C', PATH: '/usr/bin:/bin' },
|
|
});
|
|
} catch (error) {
|
|
throw codedError('isolated-runner-pidfd-support-unavailable', error);
|
|
}
|
|
return;
|
|
}
|
|
if (process.platform === 'win32') {
|
|
const handle = await openOwnedRunnerKillHandle(process.pid);
|
|
await closeOwnedRunnerKillHandle(handle);
|
|
return;
|
|
}
|
|
throw codedError('isolated-runner-stable-kill-handle-platform-unsupported');
|
|
}
|
|
|
|
export async function findControlledLinuxPython() {
|
|
if (state.linuxPidfdPythonPath) return state.linuxPidfdPythonPath;
|
|
for (const candidate of ['/usr/bin/python3', '/usr/local/bin/python3']) {
|
|
const resolved = await fs.realpath(candidate).catch(() => null);
|
|
const metadata = resolved
|
|
? await fs.stat(resolved).catch(() => null)
|
|
: null;
|
|
if (metadata?.isFile() && (metadata.mode & 0o111) !== 0) {
|
|
state.linuxPidfdPythonPath = resolved;
|
|
return resolved;
|
|
}
|
|
}
|
|
throw codedError('isolated-runner-controlled-python-unavailable');
|
|
}
|
|
|
|
export async function findControlledWindowsPowerShell() {
|
|
if (state.windowsProcessHandlePowerShellPath)
|
|
return state.windowsProcessHandlePowerShellPath;
|
|
const systemRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
assert(
|
|
isNonEmptyString(systemRoot) && path.isAbsolute(systemRoot),
|
|
'isolated-runner-windows-system-root-invalid',
|
|
);
|
|
const candidate = path.join(
|
|
systemRoot,
|
|
'System32',
|
|
'WindowsPowerShell',
|
|
'v1.0',
|
|
'powershell.exe',
|
|
);
|
|
const resolved = await fs.realpath(candidate).catch(() => null);
|
|
const metadata = resolved ? await fs.stat(resolved).catch(() => null) : null;
|
|
assert(
|
|
metadata?.isFile(),
|
|
'isolated-runner-controlled-powershell-unavailable',
|
|
);
|
|
state.windowsProcessHandlePowerShellPath = resolved;
|
|
return resolved;
|
|
}
|
|
|
|
export function controlledWindowsPowerShellEnvironment(extra = {}) {
|
|
const systemRoot = process.env.SystemRoot ?? process.env.windir;
|
|
assert(
|
|
isNonEmptyString(systemRoot) && path.isAbsolute(systemRoot),
|
|
'isolated-runner-windows-system-root-invalid',
|
|
);
|
|
const powershellRoot = path.join(
|
|
systemRoot,
|
|
'System32',
|
|
'WindowsPowerShell',
|
|
'v1.0',
|
|
);
|
|
const programFiles = process.env.ProgramW6432 ?? process.env.ProgramFiles;
|
|
const moduleRoots = [path.join(powershellRoot, 'Modules')];
|
|
if (isNonEmptyString(programFiles) && path.isAbsolute(programFiles)) {
|
|
moduleRoots.push(path.join(programFiles, 'WindowsPowerShell', 'Modules'));
|
|
}
|
|
const environment = {
|
|
SystemRoot: systemRoot,
|
|
windir: systemRoot,
|
|
ComSpec: path.join(systemRoot, 'System32', 'cmd.exe'),
|
|
PATH: [powershellRoot, path.join(systemRoot, 'System32'), systemRoot].join(
|
|
path.delimiter,
|
|
),
|
|
PATHEXT: '.COM;.EXE;.BAT;.CMD',
|
|
PSModulePath: moduleRoots.join(path.delimiter),
|
|
};
|
|
for (const name of ['TEMP', 'TMP', 'LOCALAPPDATA', 'USERPROFILE']) {
|
|
if (isNonEmptyString(process.env[name]))
|
|
environment[name] = process.env[name];
|
|
}
|
|
if (isNonEmptyString(programFiles)) environment.ProgramFiles = programFiles;
|
|
return { ...environment, ...extra };
|
|
}
|
|
|
|
export async function openOwnedRunnerKillHandle(pid) {
|
|
assert(
|
|
Number.isSafeInteger(pid) && pid > 1,
|
|
'isolated-runner-stable-kill-handle-open-precondition-invalid',
|
|
);
|
|
let child;
|
|
let readyMarker;
|
|
let exitedMarker;
|
|
let kind;
|
|
if (process.platform === 'linux') {
|
|
const python = await findControlledLinuxPython();
|
|
child = spawn(
|
|
python,
|
|
['-I', '-S', '-c', linuxPidfdHelperSource, String(pid)],
|
|
{
|
|
cwd: appRoot,
|
|
env: { LANG: 'C', LC_ALL: 'C', PATH: '/usr/bin:/bin' },
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
},
|
|
);
|
|
readyMarker = Buffer.from('PIDFD_READY\n');
|
|
exitedMarker = Buffer.from('PIDFD_EXITED\n');
|
|
kind = 'linux-pidfd';
|
|
} else if (process.platform === 'win32') {
|
|
const powershell = await findControlledWindowsPowerShell();
|
|
child = spawn(
|
|
powershell,
|
|
[
|
|
'-NoLogo',
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-EncodedCommand',
|
|
Buffer.from(windowsProcessHandleHelperSource, 'utf16le').toString(
|
|
'base64',
|
|
),
|
|
],
|
|
{
|
|
cwd: appRoot,
|
|
env: controlledWindowsPowerShellEnvironment({
|
|
AGC_OWNED_RUNNER_PID: String(pid),
|
|
}),
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
},
|
|
);
|
|
readyMarker = Buffer.from('HANDLE_READY');
|
|
exitedMarker = Buffer.from('HANDLE_EXITED');
|
|
kind = 'windows-process-handle';
|
|
} else {
|
|
throw codedError('isolated-runner-stable-kill-handle-platform-unsupported');
|
|
}
|
|
const handle = {
|
|
kind,
|
|
readyMarker,
|
|
exitedMarker,
|
|
pid,
|
|
child,
|
|
stdout: Buffer.alloc(0),
|
|
stderr: Buffer.alloc(0),
|
|
closed: false,
|
|
command: null,
|
|
stdinError: null,
|
|
};
|
|
activeCommandChildren.add(child);
|
|
child.once('error', () => {
|
|
if (!Number.isSafeInteger(child.pid)) activeCommandChildren.delete(child);
|
|
});
|
|
child.once('close', () => {
|
|
activeCommandChildren.delete(child);
|
|
handle.closed = true;
|
|
});
|
|
child.stdin.on('error', (error) => {
|
|
handle.stdinError ??= error;
|
|
});
|
|
child.stdout.on('data', (chunk) => {
|
|
handle.stdout = appendBounded(handle.stdout, chunk, 4_096);
|
|
});
|
|
child.stderr.on('data', (chunk) => {
|
|
handle.stderr = appendBounded(handle.stderr, chunk, 4_096);
|
|
});
|
|
await waitForOwnedRunnerKillHandleReady(handle);
|
|
return handle;
|
|
}
|
|
|
|
export async function waitForOwnedRunnerKillHandleReady(handle) {
|
|
await new Promise((resolve, reject) => {
|
|
const timer = setTimeout(() => {
|
|
settle(() => {
|
|
void terminateOwnedRunnerKillHandleHelper(handle).then(
|
|
() =>
|
|
reject(
|
|
codedError('isolated-runner-stable-kill-handle-open-timeout'),
|
|
),
|
|
reject,
|
|
);
|
|
});
|
|
}, 10_000);
|
|
const settle = (callback) => {
|
|
clearTimeout(timer);
|
|
handle.child.stdout.off('data', onData);
|
|
handle.child.off('error', onError);
|
|
handle.child.off('close', onClose);
|
|
callback();
|
|
};
|
|
const onData = () => {
|
|
if (handle.stdout.includes(handle.readyMarker)) {
|
|
settle(resolve);
|
|
}
|
|
};
|
|
const onError = (error) =>
|
|
settle(() =>
|
|
reject(
|
|
codedError(
|
|
'isolated-runner-stable-kill-handle-helper-spawn-failed',
|
|
error,
|
|
),
|
|
),
|
|
);
|
|
const onClose = () =>
|
|
settle(() =>
|
|
reject(codedError('isolated-runner-stable-kill-handle-open-failed')),
|
|
);
|
|
handle.child.stdout.on('data', onData);
|
|
handle.child.on('error', onError);
|
|
handle.child.on('close', onClose);
|
|
onData();
|
|
});
|
|
}
|
|
|
|
export async function closeOwnedRunnerKillHandle(handle) {
|
|
if (!handle || handle.closed) return;
|
|
if (handle.child.exitCode !== null || handle.child.signalCode !== null) {
|
|
await waitForChildClose(handle.child, 2_000, 5_000);
|
|
handle.closed = true;
|
|
return;
|
|
}
|
|
if (handle.command !== null) {
|
|
await terminateOwnedRunnerKillHandleHelper(handle);
|
|
return;
|
|
}
|
|
handle.command = 'CLOSE';
|
|
try {
|
|
await writeOwnedRunnerKillHandleCommand(handle, 'CLOSE');
|
|
const result = await waitForChildClose(handle.child, 10_000);
|
|
assert(
|
|
result.code === 0,
|
|
'isolated-runner-stable-kill-handle-close-failed',
|
|
);
|
|
handle.closed = true;
|
|
} catch (error) {
|
|
await rethrowOwnedRunnerKillHandleFailureAfterCleanup(handle, error);
|
|
}
|
|
}
|
|
|
|
export async function signalOwnedRunnerKillHandle(handle) {
|
|
assert(
|
|
handle &&
|
|
handle.closed === false &&
|
|
handle.command === null &&
|
|
handle.child.exitCode === null &&
|
|
handle.child.signalCode === null,
|
|
'isolated-runner-stable-kill-handle-not-live',
|
|
);
|
|
handle.command = 'KILL';
|
|
try {
|
|
await writeOwnedRunnerKillHandleCommand(handle, 'KILL');
|
|
const result = await waitForChildClose(handle.child, 15_000);
|
|
assert(
|
|
result.code === 0 && handle.stdout.includes(handle.exitedMarker),
|
|
'isolated-runner-stable-kill-handle-signal-failed',
|
|
);
|
|
handle.closed = true;
|
|
} catch (error) {
|
|
await rethrowOwnedRunnerKillHandleFailureAfterCleanup(handle, error);
|
|
}
|
|
}
|
|
|
|
export async function writeOwnedRunnerKillHandleCommand(
|
|
handle,
|
|
command,
|
|
timeoutMs = 5_000,
|
|
) {
|
|
assert(
|
|
handle?.child?.stdin &&
|
|
!handle.child.stdin.destroyed &&
|
|
!handle.child.stdin.writableEnded &&
|
|
['CLOSE', 'KILL'].includes(command) &&
|
|
Number.isSafeInteger(timeoutMs) &&
|
|
timeoutMs > 0,
|
|
'isolated-runner-stable-kill-handle-stdin-unavailable',
|
|
);
|
|
await new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
let timer = null;
|
|
const finish = (error = null) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
handle.child.stdin.off('error', onError);
|
|
if (error) reject(error);
|
|
else resolve();
|
|
};
|
|
const onError = (error) => {
|
|
finish(
|
|
codedError('isolated-runner-stable-kill-handle-stdin-failed', error),
|
|
);
|
|
};
|
|
timer = setTimeout(() => {
|
|
finish(codedError('isolated-runner-stable-kill-handle-stdin-timeout'));
|
|
}, timeoutMs);
|
|
handle.child.stdin.once('error', onError);
|
|
try {
|
|
handle.child.stdin.end(`${command}\n`, () => finish());
|
|
} catch (error) {
|
|
onError(error);
|
|
}
|
|
});
|
|
}
|
|
|
|
async function rethrowOwnedRunnerKillHandleFailureAfterCleanup(handle, error) {
|
|
try {
|
|
await terminateOwnedRunnerKillHandleHelper(handle);
|
|
} catch (cleanupError) {
|
|
throw codedError(
|
|
isNonEmptyString(error?.code)
|
|
? error.code
|
|
: 'isolated-runner-stable-kill-handle-operation-failed',
|
|
new AggregateError(
|
|
[error, cleanupError],
|
|
'stable kill handle operation and helper cleanup both failed',
|
|
),
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
async function terminateOwnedRunnerKillHandleHelper(handle) {
|
|
if (!handle || handle.closed) return;
|
|
const child = handle.child;
|
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
await waitForChildClose(child, 2_000, 5_000);
|
|
handle.closed = true;
|
|
return;
|
|
}
|
|
let initialKillError = null;
|
|
try {
|
|
if (!child.kill('SIGKILL')) {
|
|
initialKillError = codedError(
|
|
'isolated-runner-stable-kill-handle-helper-kill-rejected',
|
|
);
|
|
}
|
|
} catch (error) {
|
|
initialKillError = codedError(
|
|
'isolated-runner-stable-kill-handle-helper-kill-failed',
|
|
error,
|
|
);
|
|
}
|
|
try {
|
|
await waitForChildClose(child, 2_000, 5_000);
|
|
handle.closed = true;
|
|
} catch (error) {
|
|
if (child.exitCode !== null || child.signalCode !== null) {
|
|
handle.closed = true;
|
|
return;
|
|
}
|
|
if (initialKillError) {
|
|
throw codedError(
|
|
'isolated-runner-stable-kill-handle-helper-cleanup-failed',
|
|
new AggregateError(
|
|
[initialKillError, error],
|
|
'stable kill handle helper could not be terminated and reaped',
|
|
),
|
|
);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function waitForChildClose(
|
|
child,
|
|
timeoutMs,
|
|
reapTimeoutMs = 5_000,
|
|
) {
|
|
assert(
|
|
child &&
|
|
Number.isSafeInteger(timeoutMs) &&
|
|
timeoutMs > 0 &&
|
|
Number.isSafeInteger(reapTimeoutMs) &&
|
|
reapTimeoutMs > 0,
|
|
'isolated-runner-stable-kill-handle-wait-precondition-invalid',
|
|
);
|
|
if (
|
|
(child.exitCode !== null || child.signalCode !== null) &&
|
|
childProcessStdioClosed(child)
|
|
) {
|
|
return { code: child.exitCode, signal: child.signalCode };
|
|
}
|
|
return new Promise((resolve, reject) => {
|
|
let settled = false;
|
|
let timedOut = false;
|
|
let killError = null;
|
|
let reapTimer = null;
|
|
const finish = (error = null, result = null) => {
|
|
if (settled) return;
|
|
settled = true;
|
|
clearTimeout(timer);
|
|
clearTimeout(reapTimer);
|
|
child.off('error', onError);
|
|
child.off('close', onClose);
|
|
if (error) reject(error);
|
|
else resolve(result);
|
|
};
|
|
const timer = setTimeout(() => {
|
|
timedOut = true;
|
|
try {
|
|
if (!child.kill('SIGKILL')) {
|
|
killError = codedError(
|
|
'isolated-runner-stable-kill-handle-helper-kill-rejected',
|
|
);
|
|
}
|
|
} catch (error) {
|
|
killError = codedError(
|
|
'isolated-runner-stable-kill-handle-helper-kill-failed',
|
|
error,
|
|
);
|
|
}
|
|
reapTimer = setTimeout(() => {
|
|
finish(
|
|
codedError(
|
|
'isolated-runner-stable-kill-handle-helper-reap-timeout',
|
|
killError,
|
|
),
|
|
);
|
|
}, reapTimeoutMs);
|
|
if (
|
|
(child.exitCode !== null || child.signalCode !== null) &&
|
|
childProcessStdioClosed(child)
|
|
) {
|
|
onClose(child.exitCode, child.signalCode);
|
|
}
|
|
}, timeoutMs);
|
|
const onError = (error) => {
|
|
finish(
|
|
codedError('isolated-runner-stable-kill-handle-helper-failed', error),
|
|
);
|
|
};
|
|
const onClose = (code, signal) => {
|
|
if (timedOut) {
|
|
finish(
|
|
codedError(
|
|
'isolated-runner-stable-kill-handle-helper-timeout',
|
|
killError,
|
|
),
|
|
);
|
|
} else {
|
|
finish(null, { code, signal });
|
|
}
|
|
};
|
|
child.once('error', onError);
|
|
child.once('close', onClose);
|
|
if (
|
|
(child.exitCode !== null || child.signalCode !== null) &&
|
|
childProcessStdioClosed(child)
|
|
) {
|
|
onClose(child.exitCode, child.signalCode);
|
|
}
|
|
});
|
|
}
|
|
|
|
function childProcessStdioClosed(child) {
|
|
return [child.stdin, child.stdout, child.stderr]
|
|
.filter(Boolean)
|
|
.every((stream) => stream.destroyed || stream.closed === true);
|
|
}
|
|
|
|
export async function captureOwnedRunnerProcessIdentity(pid) {
|
|
const expectedAppData = state.isolatedRunner.appDataDir;
|
|
assert(
|
|
isNonEmptyString(expectedAppData) && Boolean(state.cliBinary),
|
|
'isolated-runner-process-identity-context-missing',
|
|
);
|
|
if (process.platform === 'linux') {
|
|
const [executable, expectedExecutable, stat, commandLine] =
|
|
await Promise.all([
|
|
fs.realpath(`/proc/${pid}/exe`),
|
|
fs.realpath(state.cliBinary),
|
|
fs.readFile(`/proc/${pid}/stat`, 'utf8'),
|
|
fs.readFile(`/proc/${pid}/cmdline`),
|
|
]);
|
|
const closeParenthesis = stat.lastIndexOf(')');
|
|
const fields = stat
|
|
.slice(closeParenthesis + 1)
|
|
.trim()
|
|
.split(/\s+/u);
|
|
const startTime = fields[19];
|
|
const argv = commandLine.toString('utf8').split('\0').filter(Boolean);
|
|
const configIndex = argv.indexOf('--config-dir');
|
|
assert(
|
|
closeParenthesis > 0 &&
|
|
isNonEmptyString(startTime) &&
|
|
executable === expectedExecutable &&
|
|
argv.includes('--agent-runner') &&
|
|
configIndex >= 0 &&
|
|
argv[configIndex + 1] === expectedAppData,
|
|
'isolated-runner-linux-process-identity-invalid',
|
|
);
|
|
return {
|
|
kind: 'linux-proc',
|
|
fingerprint: hashValue(JSON.stringify({ executable, startTime, argv })),
|
|
};
|
|
}
|
|
|
|
if (process.platform === 'darwin') {
|
|
const result = await runProcess(
|
|
'/bin/ps',
|
|
['-p', String(pid), '-o', 'lstart=', '-o', 'command='],
|
|
{ cwd: appRoot, timeoutMs: 30_000 },
|
|
);
|
|
assert(
|
|
result.stdout.includes(path.basename(state.cliBinary)) &&
|
|
result.stdout.includes('--agent-runner') &&
|
|
result.stdout.includes(expectedAppData),
|
|
'isolated-runner-darwin-process-identity-invalid',
|
|
);
|
|
return {
|
|
kind: 'darwin-ps',
|
|
fingerprint: hashValue(result.stdout.trim()),
|
|
};
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
const powershell = await findControlledWindowsPowerShell();
|
|
const script = `$process = Get-CimInstance Win32_Process -Filter "ProcessId = ${pid}"; if ($null -eq $process) { exit 3 }; $process | Select-Object ProcessId,CreationDate,ExecutablePath,CommandLine | ConvertTo-Json -Compress`;
|
|
const result = await runProcess(
|
|
powershell,
|
|
['-NoProfile', '-NonInteractive', '-Command', script],
|
|
{
|
|
cwd: appRoot,
|
|
timeoutMs: 30_000,
|
|
env: controlledWindowsPowerShellEnvironment(),
|
|
},
|
|
);
|
|
const value = JSON.parse(result.stdout);
|
|
const executable = path.resolve(String(value.ExecutablePath ?? ''));
|
|
const expectedExecutable = path.resolve(state.cliBinary);
|
|
const commandLine = String(value.CommandLine ?? '');
|
|
assert(
|
|
Number(value.ProcessId) === pid &&
|
|
executable.toLowerCase() === expectedExecutable.toLowerCase() &&
|
|
isNonEmptyString(value.CreationDate) &&
|
|
commandLine.includes('--agent-runner') &&
|
|
commandLine.includes(expectedAppData),
|
|
'isolated-runner-windows-process-identity-invalid',
|
|
);
|
|
return {
|
|
kind: 'windows-cim',
|
|
fingerprint: hashValue(
|
|
JSON.stringify({
|
|
pid,
|
|
creationDate: value.CreationDate,
|
|
executable: executable.toLowerCase(),
|
|
commandLine,
|
|
}),
|
|
),
|
|
};
|
|
}
|
|
|
|
throw codedError('isolated-runner-process-identity-platform-unsupported');
|
|
}
|
|
|
|
export function isProcessAlive(pid) {
|
|
if (!Number.isSafeInteger(pid) || pid <= 1 || pid === process.pid)
|
|
return false;
|
|
try {
|
|
process.kill(pid, 0);
|
|
return true;
|
|
} catch (error) {
|
|
return error?.code === 'EPERM';
|
|
}
|
|
}
|
|
|
|
export async function killRunnerPidOnce(pid, ownedRunner) {
|
|
if (isIsolatedRunnerSuite()) {
|
|
assert(ownedRunner, 'isolated-runner-pid-kill-fallback-forbidden');
|
|
}
|
|
if (ownedRunner) {
|
|
const claimed = state.isolatedRunner.current;
|
|
assert(
|
|
claimed?.pid === pid && claimed.killHandle?.pid === pid,
|
|
'isolated-runner-stable-kill-handle-identity-missing',
|
|
);
|
|
await signalOwnedRunnerKillHandle(claimed.killHandle);
|
|
state.isolatedRunner.pidfdSignalCount += 1;
|
|
state.runnerKilled = true;
|
|
state.isolatedRunner.current = null;
|
|
return;
|
|
}
|
|
try {
|
|
process.kill(pid, 'SIGKILL');
|
|
} catch (error) {
|
|
throw codedError('runner-sigkill-failed', error);
|
|
}
|
|
state.runnerKilled = true;
|
|
const deadline = Date.now() + 10_000;
|
|
while (Date.now() < deadline) {
|
|
if (!isProcessAlive(pid)) {
|
|
if (ownedRunner) state.isolatedRunner.current = null;
|
|
return;
|
|
}
|
|
await sleep(50);
|
|
}
|
|
throw codedError('runner-still-alive-after-sigkill');
|
|
}
|
|
|
|
export async function stopClaimedOwnedRunnerWithoutEndpoint() {
|
|
const claimed = state.isolatedRunner.current;
|
|
if (!claimed) return;
|
|
if (!isProcessAlive(claimed.pid)) {
|
|
await closeOwnedRunnerKillHandle(claimed.killHandle);
|
|
state.isolatedRunner.current = null;
|
|
return;
|
|
}
|
|
let currentIdentity;
|
|
try {
|
|
currentIdentity = await captureOwnedRunnerProcessIdentity(claimed.pid);
|
|
} catch (error) {
|
|
if (!isProcessAlive(claimed.pid)) {
|
|
await closeOwnedRunnerKillHandle(claimed.killHandle);
|
|
state.isolatedRunner.current = null;
|
|
return;
|
|
}
|
|
throw error;
|
|
}
|
|
assert(
|
|
currentIdentity.fingerprint === claimed.processIdentity.fingerprint,
|
|
'isolated-owned-runner-identity-changed-without-endpoint',
|
|
);
|
|
await killRunnerPidOnce(claimed.pid, true);
|
|
}
|
|
|
|
export async function stopOwnedIsolatedRunner() {
|
|
await readIsolatedAppDataSentinel();
|
|
const endpointPath = path.join(
|
|
state.isolatedRunner.appDataDir,
|
|
runnerEndpointFileName,
|
|
);
|
|
const endpointMetadata = await fs.lstat(endpointPath).catch((error) => {
|
|
if (error?.code === 'ENOENT') return null;
|
|
throw error;
|
|
});
|
|
if (!endpointMetadata) {
|
|
assert(
|
|
state.isolatedRunner.current || !state.isolatedRunner.launchAttempted,
|
|
'isolated-owned-runner-endpoint-missing-before-stable-claim',
|
|
);
|
|
await stopClaimedOwnedRunnerWithoutEndpoint();
|
|
return;
|
|
}
|
|
assert(
|
|
endpointMetadata.isFile() && !endpointMetadata.isSymbolicLink(),
|
|
'isolated-owned-runner-endpoint-not-regular-file',
|
|
);
|
|
const endpoint = await readJson(endpointPath);
|
|
const status = await readRunnerStatus();
|
|
if (status?.running !== true) {
|
|
assert(
|
|
!isProcessAlive(Number(endpoint.pid)),
|
|
'isolated-owned-runner-live-pid-without-identity',
|
|
);
|
|
await closeOwnedRunnerKillHandle(state.isolatedRunner.current?.killHandle);
|
|
state.isolatedRunner.current = null;
|
|
return;
|
|
}
|
|
await claimOwnedRunner(status);
|
|
await killRunnerOnce();
|
|
}
|
|
|
|
export async function verifyIsolatedSuiteConfigLinksUnchanged() {
|
|
for (const link of state.isolatedRunner.configLinks) {
|
|
const [sourceMetadata, linkedMetadata, sourceContent, linkedContent] =
|
|
await Promise.all([
|
|
fs.lstat(link.sourcePath),
|
|
fs.lstat(link.linkedPath),
|
|
fs.readFile(link.sourcePath),
|
|
fs.readFile(link.linkedPath),
|
|
]);
|
|
const sourceHash = createHash('sha256').update(sourceContent).digest('hex');
|
|
const linkedHash = createHash('sha256').update(linkedContent).digest('hex');
|
|
const replicaIdentityValid =
|
|
link.storageMode === 'private-copy'
|
|
? linkedMetadata.dev === link.linkedDev &&
|
|
linkedMetadata.ino === link.linkedIno &&
|
|
(linkedMetadata.dev !== sourceMetadata.dev ||
|
|
linkedMetadata.ino !== sourceMetadata.ino) &&
|
|
(process.platform === 'win32' || (linkedMetadata.mode & 0o077) === 0)
|
|
: linkedMetadata.dev === link.dev && linkedMetadata.ino === link.ino;
|
|
const sourceMetadataStable =
|
|
sourceMetadata.mode === link.sourceMode &&
|
|
sourceMetadata.size === link.sourceSize &&
|
|
sourceMetadata.mtimeMs === link.sourceMtimeMs &&
|
|
(link.storageMode !== 'private-copy' ||
|
|
(sourceMetadata.ctimeMs === link.sourceCtimeMs &&
|
|
sourceMetadata.nlink === link.nlink));
|
|
assert(
|
|
sourceMetadata.isFile() &&
|
|
!sourceMetadata.isSymbolicLink() &&
|
|
linkedMetadata.isFile() &&
|
|
!linkedMetadata.isSymbolicLink() &&
|
|
sourceMetadata.dev === link.dev &&
|
|
sourceMetadata.ino === link.ino &&
|
|
sourceMetadataStable &&
|
|
replicaIdentityValid &&
|
|
sourceHash === link.sha256 &&
|
|
linkedHash === link.sha256,
|
|
'isolated-source-config-changed-during-suite',
|
|
);
|
|
}
|
|
const fixture = state.isolatedRunner;
|
|
if (
|
|
fixture.platformSessionFixtureSourcePath &&
|
|
fixture.platformSessionFixturePath &&
|
|
fixture.platformSessionFixtureSha256
|
|
) {
|
|
const [sourceMetadata, isolatedMetadata, sourceBytes, isolatedBytes] =
|
|
await Promise.all([
|
|
fs.lstat(fixture.platformSessionFixtureSourcePath),
|
|
fs.lstat(fixture.platformSessionFixturePath),
|
|
fs.readFile(fixture.platformSessionFixtureSourcePath),
|
|
fs.readFile(fixture.platformSessionFixturePath),
|
|
]);
|
|
assert(
|
|
sourceMetadata.isFile() &&
|
|
!sourceMetadata.isSymbolicLink() &&
|
|
isolatedMetadata.isFile() &&
|
|
!isolatedMetadata.isSymbolicLink() &&
|
|
createHash('sha256').update(sourceBytes).digest('hex') ===
|
|
fixture.platformSessionFixtureSha256 &&
|
|
createHash('sha256').update(isolatedBytes).digest('hex') ===
|
|
fixture.platformSessionFixtureSha256,
|
|
'supervisor-autonomous-playable-platform-session-fixture-changed',
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function verifySourceConfigLinkCountsRestored() {
|
|
for (const link of state.isolatedRunner.configLinks) {
|
|
const metadata = await fs.lstat(link.sourcePath);
|
|
assert(
|
|
metadata.isFile() &&
|
|
!metadata.isSymbolicLink() &&
|
|
metadata.dev === link.dev &&
|
|
metadata.ino === link.ino &&
|
|
metadata.nlink === link.nlink,
|
|
'isolated-source-config-link-count-not-restored',
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function removeIsolatedSuiteAppData() {
|
|
await readIsolatedAppDataSentinel();
|
|
const profile = isolatedSuiteAppDataProfile();
|
|
const [sourceConfigDir, appDataDir] = await Promise.all([
|
|
fs.realpath(state.options.configDir),
|
|
fs.realpath(state.isolatedRunner.appDataDir),
|
|
]);
|
|
const cleanupPathValid = isolatedSuiteUsesSiblingAppData()
|
|
? !isPathInside(sourceConfigDir, appDataDir) &&
|
|
path.dirname(appDataDir) === path.dirname(sourceConfigDir)
|
|
: isPathInside(sourceConfigDir, appDataDir);
|
|
assert(
|
|
cleanupPathValid && path.basename(appDataDir).startsWith(profile.prefix),
|
|
'isolated-appdata-cleanup-path-invalid',
|
|
);
|
|
let ownershipError = null;
|
|
try {
|
|
await verifySourceAppDataDirectoryUntouched();
|
|
await verifyIsolatedSuiteConfigLinksUnchanged();
|
|
await verifySourceRunnerEndpointUnchanged();
|
|
} catch (error) {
|
|
ownershipError = error;
|
|
}
|
|
try {
|
|
await fs.rm(appDataDir, { recursive: true, force: false });
|
|
} finally {
|
|
if (state.isolatedRunner.platformSessionFixtureSourcePath) {
|
|
const previous = state.isolatedRunner.platformSessionFixturePreviousEnv;
|
|
if (previous === undefined) {
|
|
delete process.env[platformSessionFixtureEnv];
|
|
} else {
|
|
process.env[platformSessionFixtureEnv] = previous;
|
|
}
|
|
}
|
|
state.isolatedRunner.platformSessionFixturePath = null;
|
|
state.isolatedRunner.platformSessionFixtureSourcePath = null;
|
|
state.isolatedRunner.platformSessionFixtureSha256 = null;
|
|
state.isolatedRunner.platformSessionFixturePreviousEnv = undefined;
|
|
}
|
|
state.runtimeConfigDir = state.options.configDir;
|
|
try {
|
|
await verifySourceConfigLinkCountsRestored();
|
|
state.isolatedRunner.sourceConfigLinksVerified = true;
|
|
} catch (error) {
|
|
ownershipError ??= error;
|
|
}
|
|
if (ownershipError) throw ownershipError;
|
|
return true;
|
|
}
|