Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions containers/enclave/mcp-server/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ function loadHostConfig(kind, server) {
entryId,
runId,
executorBackend: 'cloud-hypervisor',
executorTransport: 'host',
backend: 'cloud-hypervisor',
primaryBackend: server.primaryBackend,
hostExecutorSocketPath: path.join(HOST_EXECUTOR_DIR, 'executor.sock'),
Expand Down
8 changes: 6 additions & 2 deletions containers/enclave/mcp-server/host-executor-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,12 @@ const FAILURE_REASONS = new Map([
* Internal broker adapter. AWF constructs the listener only after trusted
* prerequisite checks. Repository staging and result files belong to AWF.
*/
function usesHostExecutor(config) {
return config.executorTransport === 'host' || config.executorBackend === 'cloud-hypervisor';
}

function createHostExecutorRunner(config, deps = {}) {
if (config.executorBackend !== 'cloud-hypervisor'
if (!usesHostExecutor(config)
|| !/^[a-z0-9](?:[a-z0-9-]{0,62})$/.test(config.entryId || '')) {
throw new Error('Invalid trusted host executor configuration');
}
Expand Down Expand Up @@ -222,4 +226,4 @@ function createHostExecutorRunner(config, deps = {}) {
};
}

module.exports = { createHostExecutorRunner };
module.exports = { createHostExecutorRunner, usesHostExecutor };
6 changes: 3 additions & 3 deletions containers/enclave/mcp-server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const {
const { AGENT_TOOL_NAME, TOOL_NAME, dispatchJsonRpc, parseJsonRpcBody } = require('./mcp-protocol');
const { createDynamicDelegationClient } = require('./delegation-channel');
const { createToolCallBudget } = require('./tool-call-budget');
const { createHostExecutorRunner } = require('./host-executor-runner');
const { createHostExecutorRunner, usesHostExecutor } = require('./host-executor-runner');
const { createStartupReporter, runStartupStage, startupDiagnostic } = require('./startup-diagnostics');
const { probeStartupEnvironment } = require('./environment-probes');

Expand Down Expand Up @@ -271,7 +271,7 @@ async function main() {
);
const runner = await runStartupStage(
startupReporter, 'script-selection', 'script-runner-initialization-failed', () =>
config.executorBackend === 'cloud-hypervisor'
usesHostExecutor(config)
? createHostExecutorRunner(config, { nowMs: clock.nowMs, audit: hostExecutorAudit })
: createScriptRunner(config, { nowMs: clock.nowMs }),
{ executorKind: 'script' },
Expand Down Expand Up @@ -317,7 +317,7 @@ async function main() {
);
const runner = await runStartupStage(
startupReporter, 'agent-selection', 'agent-runner-initialization-failed', () =>
config.executorBackend === 'cloud-hypervisor'
usesHostExecutor(config)
? createHostExecutorRunner(config, { nowMs: clock.nowMs, audit: hostExecutorAudit })
: createAgentRunner(config, { nowMs: clock.nowMs }),
{ executorKind: 'agent' },
Expand Down
38 changes: 38 additions & 0 deletions docs/enclaves-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -846,6 +846,44 @@ See [ADR 0001: Agent enclave repository admission](adr/0001-agent-enclaves.md)
for the stable compiler, mcpg, runtime-registry, executor, and integration
contract.

## Shared host execution core

The host-executor protocol and server own authenticated invocation planning,
cancellation, leases, cleanup-before-terminal results, and digest-bound settlement.
The shared modules under `src/enclave/` additionally provide:

- `execution-policy.ts`, `invocation-staging.ts`, and `invocation-result.ts`:
trusted role limits and identities, private input staging, and bounded canonical
schema-valid results.
- `host-executor-run-state.ts`: static seed authority derived from the validated
host catalog and staged run identity, never from broker launch settings.
- `host-service.ts` and `host-executor-lifecycle.ts`: preflight-before-listening,
configuration ownership, readiness, admission closure, startup/shutdown races,
draining, recovery verification ordering, and protected evidence collection.
- `bounded-storage.ts`: aggregate storage requirements and verification hooks.
Adapters must enforce capacity across all writable state, including snapshots,
runtime state, sparse files, and concurrent writers, until cleanup succeeds.
A nominal image size, free-space check, or per-file limit is not enforcement.

Backend adapters retain artifact discovery/provenance, launch and confinement,
mount realization, networking, resource identity checks, journal formats, and
reaping. Failed or uncertain cleanup retains lifecycle and recovery state; it
does not authorize storage release or a runtime fallback. Diagnostic collection
does not bypass existing visibility, redaction, or protected-storage checks.

Host-service readiness does not imply a persistent workload instance. The
`HostEnclaveExecutorBackend.execute(plan, signal)` seam accommodates atomic
one-shot execution and container-owned lifecycles without `startInstance()`.
Primary runtime integration remains separate: Docker Compose and NVX are not
forced into the Cloud Hypervisor primary-manager lifecycle.

The broker routes trusted `executorTransport: 'host'` configurations through the
same protocol, retaining Cloud Hypervisor compatibility. Transport and backend
are not invocation fields. This extraction does not enable NVX enclaves,
migrate Docker execution, change dynamic delegation, or relax any Cloud
Hypervisor host, artifact, storage, or real-backend acceptance gate. The reviewed
cross-backend capability contract in #10339 remains a prerequisite for migration.

## Cloud Hypervisor enclave executor

Cloud Hypervisor supports static enclave runtime selection only after trusted
Expand Down
14 changes: 2 additions & 12 deletions src/cloud-hypervisor/enclave-executor-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import type {
HostExecutorRunState,
} from '../enclave/host-executor-server';
import type { HostExecutorResourceJournal } from '../enclave/host-executor-journal';
import type { EnclaveAgentProfile } from '../types/enclave-options';
import type { HostExecutorAgentPolicy } from '../enclave/execution-policy';
export type { HostExecutorAgentPolicy } from '../enclave/execution-policy';
import type { CloudHypervisorOptions } from '../types/runtime-options';
import type {
CloudHypervisorEnclaveArtifactManifest,
Expand Down Expand Up @@ -44,17 +45,6 @@ export interface CloudHypervisorEnclaveArtifactPreflightOptions {
readonly attestationToolPath: string;
}

export interface HostExecutorAgentPolicy {
readonly model: string;
readonly profile: EnclaveAgentProfile;
readonly maxOutputBytes: number;
readonly maxModelRequests?: number;
readonly maxModelTokens?: number;
readonly modelBudgetSecret?: string;
readonly githubAgentId?: string;
readonly githubBearer?: string;
}

export interface HostEnclaveExecutorManager {
start(): Promise<unknown>;
startInstance(): Promise<void>;
Expand Down
100 changes: 11 additions & 89 deletions src/cloud-hypervisor/enclave-invocation-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ import type {
HostExecutorRunState,
} from '../enclave/host-executor-server';
import { HOST_EXECUTOR_TIMEOUT_GRACE_MS } from '../enclave/host-executor-protocol';
import { verifyBoundedInvocationStorage } from '../enclave/bounded-storage';
import {
invocationFilePath as filePath,
stageInvocationInputs as stageSharedInvocationInputs,
writePrivateInvocationFile as writePrivateFile,
} from '../enclave/invocation-staging';
import {
assertTrustedAncestorChain,
calculateSha256,
Expand All @@ -25,7 +31,6 @@ import type { CloudHypervisorHostToolPaths } from './preflight';
import { CLOUD_HYPERVISOR_ENCLAVE_RESOURCE_PROFILES } from './workload-profile';
import { createEnclaveModelBudgetToken } from '../enclave/model-budget-capability';
import {
assertNoHostGithubTokenValue,
enforceGithubTokenIsolation,
getGithubTokenEnvValues,
} from '../services/agent-environment/github-token-isolation';
Expand All @@ -34,12 +39,6 @@ const GITHUB_PROFILE = 'issues-read-v1';
const GITHUB_ENDPOINT = `http://${ENCLAVE_AGENT_GITHUB_MCP_IP}:${ENCLAVE_GITHUB_MCP_PORT}/mcp/github`;
const OUTPUT_NAME = 'out';

function filePath(directory: string, name: string): string {
const result = path.join(directory, name);
if (path.dirname(result) !== directory) throw new Error('Invalid enclave artifact path');
return result;
}

export async function prepareInvocationFilesystem(
runState: HostExecutorRunState,
plan: HostExecutorInvocationPlan,
Expand Down Expand Up @@ -96,7 +95,9 @@ export async function prepareInvocationFilesystem(
tools,
);
await onMounted();
await dependencies.verifyStorage(plan.invocationHostDir, resourceProfile.writableStorageBytes);
await verifyBoundedInvocationStorage(role, plan.invocationHostDir,
(directory, requirement) =>
dependencies.verifyStorage(directory, requirement.maximumBytes));
for (const name of ['request', 'output', 'runtime']) {
const directory = filePath(plan.invocationHostDir, name);
await dependencies.mkdir(directory, { mode: 0o700 });
Expand Down Expand Up @@ -137,93 +138,14 @@ async function assertTrustedDirectory(
}
}

async function writePrivateFile(
filePathValue: string,
contents: string,
uid: number,
gid: number,
mode: number,
dependencies: Pick<HostEnclaveExecutorDependencies, 'writeFile' | 'chown' | 'chmod'>,
): Promise<void> {
await dependencies.writeFile(filePathValue, contents, { encoding: 'utf8', mode, flag: 'wx' });
await dependencies.chown(filePathValue, uid, gid);
await dependencies.chmod(filePathValue, mode);
}

export async function stageInvocationInputs(
plan: HostExecutorInvocationPlan,
role: CloudHypervisorEnclaveRole,
policy: HostExecutorAgentPolicy | undefined,
dependencies: Pick<HostEnclaveExecutorDependencies, 'writeFile' | 'chown' | 'chmod'>,
): Promise<void> {
if (role === 'agent' && policy?.githubBearer) {
assertNoHostGithubTokenValue(
policy.githubBearer,
getGithubTokenEnvValues(),
'Cloud Hypervisor enclave GitHub bearer file',
);
}
const resourceProfile = role === 'script'
? CLOUD_HYPERVISOR_ENCLAVE_RESOURCE_PROFILES.script
: CLOUD_HYPERVISOR_ENCLAVE_RESOURCE_PROFILES.agent;
const requestDir = filePath(plan.invocationHostDir, 'request');
if (role === 'script') {
await writePrivateFile(
filePath(requestDir, 'query-script.py'),
plan.payload,
resourceProfile.uid,
resourceProfile.gid,
0o400,
dependencies,
);
return;
}
if (!policy) throw new Error('Trusted agent policy is required for an agent enclave');
await writePrivateFile(
filePath(requestDir, 'task.txt'),
plan.payload,
resourceProfile.uid,
resourceProfile.gid,
0o400,
dependencies,
);
await writePrivateFile(
filePath(requestDir, 'schema.json'),
JSON.stringify(plan.schema),
resourceProfile.uid,
resourceProfile.gid,
0o400,
dependencies,
);
const runtimeDir = filePath(plan.invocationHostDir, 'runtime');
const sessionFile = filePath(runtimeDir, 'session.jsonl');
await writePrivateFile(
sessionFile,
'',
resourceProfile.uid,
resourceProfile.gid,
0o600,
dependencies,
);
if (policy.githubAgentId !== undefined && policy.githubBearer !== undefined) {
const handoffDir = filePath(plan.invocationHostDir, 'session-handoff');
await writePrivateFile(
filePath(handoffDir, 'github-agent-id'),
`${policy.githubAgentId}\n`,
resourceProfile.uid,
resourceProfile.gid,
0o400,
dependencies,
);
await writePrivateFile(
filePath(handoffDir, 'github-bearer'),
`${policy.githubBearer}\n`,
resourceProfile.uid,
resourceProfile.gid,
0o400,
dependencies,
);
}
await stageSharedInvocationInputs(plan, role, policy, dependencies,
'Cloud Hypervisor enclave GitHub bearer file');
}

export function agentEnvironment(
Expand Down
87 changes: 4 additions & 83 deletions src/cloud-hypervisor/host-enclave-executor.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import { constants, promises as fs } from 'fs';
import { TextDecoder } from 'util';
import { promises as fs } from 'fs';
import * as path from 'path';
import {
canonicalizeSchemaValue,
validateValueAgainstSchema,
} from '../bounded-execution/finite-schema';
import { normalizeAgentPolicy } from '../enclave/execution-policy';
import { readBoundedEnclaveResult as readBoundedCloudHypervisorEnclaveResult } from '../enclave/invocation-result';
export { readBoundedEnclaveResult as readBoundedCloudHypervisorEnclaveResult } from '../enclave/invocation-result';
import type {
HostEnclaveExecutorBackend,
HostExecutorInvocationPlan,
Expand Down Expand Up @@ -88,19 +86,7 @@ export type {
} from './enclave-executor-types';
export { preflightCloudHypervisorEnclaveArtifacts } from './enclave-artifact-preflight';

const MAX_AGENT_MODEL_BYTES = 256;
const MAX_GITHUB_BEARER_BYTES = 512;
const OUTPUT_NAME = 'out';
const CREDENTIAL_PATTERN = /^[\x21-\x7e]{16,512}$/;
const AGENT_ID_PATTERN = /^[A-Za-z0-9_-]{1,128}$/;

function hasControlCharacters(value: string): boolean {
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code < 0x20 || code === 0x7f) return true;
}
return false;
}

const defaultDependencies: HostEnclaveExecutorDependencies = {
createArtifactSnapshot: (sources, copy, onDirectoryCreated, report) =>
Expand Down Expand Up @@ -213,71 +199,6 @@ export async function createCloudHypervisorHostEnclaveExecutor(
}
}

function normalizeAgentPolicy(
policy: HostExecutorAgentPolicy | undefined,
timeoutMs: number,
): HostExecutorAgentPolicy | undefined {
if (!policy) return undefined;
if (
typeof policy.model !== 'string' ||
policy.model.length === 0 ||
Buffer.byteLength(policy.model, 'utf8') > MAX_AGENT_MODEL_BYTES ||
hasControlCharacters(policy.model) ||
!['openai', 'anthropic'].includes(policy.profile) ||
!Number.isSafeInteger(policy.maxOutputBytes) ||
policy.maxOutputBytes < 1 ||
policy.maxOutputBytes > HOST_EXECUTOR_MAX_RESULT_BYTES ||
(policy.maxModelRequests !== undefined &&
(!Number.isSafeInteger(policy.maxModelRequests) || policy.maxModelRequests < 1)) ||
(policy.maxModelTokens !== undefined &&
(!Number.isSafeInteger(policy.maxModelTokens) || policy.maxModelTokens < 1)) ||
!Number.isSafeInteger(timeoutMs) ||
timeoutMs < 1
) {
throw new Error('Trusted host executor agent policy is invalid');
}
const hasGithubId = policy.githubAgentId !== undefined;
const hasGithubBearer = policy.githubBearer !== undefined;
if (hasGithubId !== hasGithubBearer) {
throw new Error('Trusted GitHub agent identity and bearer must be configured together');
}
if (
(policy.githubAgentId !== undefined && !AGENT_ID_PATTERN.test(policy.githubAgentId)) ||
(policy.githubBearer !== undefined &&
(!CREDENTIAL_PATTERN.test(policy.githubBearer) ||
Buffer.byteLength(policy.githubBearer, 'ascii') > MAX_GITHUB_BEARER_BYTES))
) {
throw new Error('Trusted host executor GitHub credentials are invalid');
}
return policy;
}

export async function readBoundedCloudHypervisorEnclaveResult(
outputPath: string,
schema: HostExecutorInvocationPlan['schema'],
maxBytes: number,
): Promise<string | undefined> {
let handle;
try {
handle = await fs.open(outputPath, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
const stat = await handle.stat();
if (!stat.isFile() || stat.size < 1 || stat.size > maxBytes) return undefined;
const bytes = Buffer.alloc(maxBytes + 1);
const { bytesRead } = await handle.read(bytes, 0, maxBytes + 1, 0);
if (bytesRead > maxBytes) return undefined;
const text = new TextDecoder('utf-8', { fatal: true }).decode(bytes.subarray(0, bytesRead));
const parsed: unknown = JSON.parse(text);
if (!validateValueAgainstSchema(schema, parsed)) return undefined;
const canonical = canonicalizeSchemaValue(schema, parsed);
if (Buffer.byteLength(canonical, 'utf8') > maxBytes) return undefined;
return canonical;
} catch {
return undefined;
} finally {
await handle?.close();
}
}

function createWorkloadProfile(
plan: HostExecutorInvocationPlan,
exportPlan: CloudHypervisorEnclaveExportPlan,
Expand Down
Loading
Loading