feat: harden orchestrator execution runtime
This commit is contained in:
+101
-2
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user