feat: harden orchestrator execution runtime

This commit is contained in:
john
2026-07-24 23:53:32 +08:00
parent 396bb78200
commit f6f2cd0933
55 changed files with 5122 additions and 194 deletions
+101 -2
View File
@@ -17,9 +17,38 @@ export function extensionToolsKey(config) {
return [...tools].sort().join('\0');
}
function extensionExecutionConfig(config) {
const type = String(config?.type ?? '');
const executionConfig = {
name: extensionName(config),
type,
availableTools: extensionToolsKey(config),
};
// Platform/builtin extensions are identified by their name and tool grant.
// Stdio extensions also carry the executable, arguments, and environment that
// define their filesystem and service boundaries. Ignoring those fields lets
// a resumed session keep a stale SANDBOX_ROOT after the Portal runtime moves.
if (type === 'stdio') {
executionConfig.cmd = String(config?.cmd ?? '');
executionConfig.args = Array.isArray(config?.args)
? config.args.map((item) => String(item))
: [];
executionConfig.envs = Object.fromEntries(
Object.entries(config?.envs ?? {})
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, value]) => [key, String(value)]),
);
}
return executionConfig;
}
export function extensionConfigsMatch(sessionExt, desiredConfig) {
if (extensionName(sessionExt) !== extensionName(desiredConfig)) return false;
return extensionToolsKey(sessionExt) === extensionToolsKey(desiredConfig);
return (
JSON.stringify(extensionExecutionConfig(sessionExt))
=== JSON.stringify(extensionExecutionConfig(desiredConfig))
);
}
export function extensionsNeedingRefresh(currentExtensions, desiredExtensions) {
@@ -54,6 +83,64 @@ export function extensionsNeedingRefresh(currentExtensions, desiredExtensions) {
return { toRemove, toAdd };
}
export function extensionPolicyViolations(currentExtensions, desiredExtensions) {
const current = currentExtensions ?? [];
const desired = desiredExtensions ?? [];
const desiredByName = new Map();
for (const config of desired) {
const name = extensionName(config);
if (name && !desiredByName.has(name)) desiredByName.set(name, config);
}
const unexpected = [];
const duplicate = [];
const currentByName = new Map();
for (const ext of current) {
const name = extensionName(ext);
if (!name) continue;
if (!desiredByName.has(name)) unexpected.push(name);
const configs = currentByName.get(name) ?? [];
configs.push(ext);
currentByName.set(name, configs);
if (configs.length === 2) duplicate.push(name);
}
const missingOrMismatched = [];
for (const [name, desiredConfig] of desiredByName) {
const configs = currentByName.get(name) ?? [];
if (
configs.length !== 1
|| !extensionConfigsMatch(configs[0], desiredConfig)
) {
missingOrMismatched.push(name);
}
}
return {
unexpected: [...new Set(unexpected)].sort(),
duplicate: [...new Set(duplicate)].sort(),
missingOrMismatched: [...new Set(missingOrMismatched)].sort(),
};
}
function assertExtensionPolicyApplied(currentExtensions, desiredExtensions, sessionId) {
const violations = extensionPolicyViolations(currentExtensions, desiredExtensions);
if (
violations.unexpected.length === 0
&& violations.duplicate.length === 0
&& violations.missingOrMismatched.length === 0
) {
return;
}
const err = new Error(
`session ${sessionId} extension policy mismatch: ${JSON.stringify(violations)}`,
);
err.code = 'SESSION_EXTENSION_POLICY_MISMATCH';
err.retryable = false;
throw err;
}
function samePath(left, right) {
if (!left || !right) return false;
return path.resolve(left) === path.resolve(right);
@@ -175,6 +262,18 @@ export async function reconcileAgentSession(
);
}
// Fail closed: the upstream may acknowledge add/remove/restart calls without
// persisting their effective session configuration. Never let the agent run
// with default or stale tools after a restricted policy was requested.
const effectiveExtensions = needsRestart
? (
(
await readJson(await apiFetch(`/sessions/${sessionId}/extensions`))
)?.extensions ?? []
)
: refreshed;
assertExtensionPolicyApplied(effectiveExtensions, desired, sessionId);
const sandboxText = sandboxConstraints?.trim()
? buildSandboxSessionConstraints({
baseConstraints: sandboxConstraints,