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
80 changes: 28 additions & 52 deletions .github/workflows/smoke-enclave-build-test.lock.yml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions .github/workflows/smoke-enclave-build-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,27 @@ safe-outputs:
run-success: "🔐🏗️ [{workflow_name}]({run_url}) completed. Enclave build test passed. ✅"
run-failure: "🔐🏗️ [{workflow_name}]({run_url}) reports {status}. Enclave build compatibility issue detected."
timeout-minutes: 20
runtimes:
node:
version: "24"
sandbox:
agent:
id: awf
version: v0.28.1
command: "sudo --preserve-env awf"
strict: false
steps:
- name: Install AWF dependencies
run: npm ci
- name: Build AWF
run: npm run build
- name: Install local AWF binary
run: |
sudo tee /usr/local/bin/awf > /dev/null <<EOF
#!/bin/bash
exec "$(command -v node)" "${GITHUB_WORKSPACE}/dist/cli.js" "\$@"
EOF
sudo chmod +x /usr/local/bin/awf
concurrency:
group: smoke-enclave-build-test
cancel-in-progress: false
Expand Down
78 changes: 28 additions & 50 deletions .github/workflows/smoke-enclave-dynamic-issues-read.lock.yml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions .github/workflows/smoke-enclave-dynamic-issues-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,29 @@ safe-outputs:
run-success: "🔐📖 [{workflow_name}]({run_url}) completed. Dynamic Enclave Issues read test passed. ✅"
run-failure: "🔐📖 [{workflow_name}]({run_url}) reports {status}. Dynamic Enclave Issues read compatibility issue detected."
timeout-minutes: 20
runtimes:
node:
version: "24"
sandbox:
agent:
id: awf
version: v0.28.14
command: "sudo --preserve-env awf"
mcp:
version: v0.4.19
strict: false
steps:
- name: Install AWF dependencies
run: npm ci
- name: Build AWF
run: npm run build
- name: Install local AWF binary
run: |
sudo tee /usr/local/bin/awf > /dev/null <<EOF
#!/bin/bash
exec "$(command -v node)" "${GITHUB_WORKSPACE}/dist/cli.js" "\$@"
EOF
sudo chmod +x /usr/local/bin/awf
concurrency:
group: smoke-enclave-dynamic-issues-read
cancel-in-progress: false
Expand Down
78 changes: 28 additions & 50 deletions .github/workflows/smoke-enclave-issues-read.lock.yml

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions .github/workflows/smoke-enclave-issues-read.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,29 @@ safe-outputs:
run-success: "🔐📖 [{workflow_name}]({run_url}) completed. Enclave Issues read test passed. ✅"
run-failure: "🔐📖 [{workflow_name}]({run_url}) reports {status}. Enclave Issues read compatibility issue detected."
timeout-minutes: 20
runtimes:
node:
version: "24"
sandbox:
agent:
id: awf
version: v0.28.13
command: "sudo --preserve-env awf"
mcp:
version: v0.4.15
strict: false
steps:
- name: Install AWF dependencies
run: npm ci
- name: Build AWF
run: npm run build
- name: Install local AWF binary
run: |
sudo tee /usr/local/bin/awf > /dev/null <<EOF
#!/bin/bash
exec "$(command -v node)" "${GITHUB_WORKSPACE}/dist/cli.js" "\$@"
EOF
sudo chmod +x /usr/local/bin/awf
concurrency:
group: smoke-enclave-issues-read
cancel-in-progress: false
Expand Down
25 changes: 18 additions & 7 deletions containers/enclave/mcp-server/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ function dockerSize(name, fallback) {

function loadConfig(files = fs) {
const executorBackend = requireEnv('AWF_ENCLAVE_BACKEND');
if (executorBackend === 'cloud-hypervisor') {
if (executorBackend === 'cloud-hypervisor' || executorBackend === 'docker') {
return loadHostConfig('script', loadServerConfig(files));
}
if (executorBackend !== 'docker' && executorBackend !== 'gvisor') {
Expand Down Expand Up @@ -234,7 +234,7 @@ function resolveRunId() {
*/
function loadAgentConfig(server, files = fs) {
const backend = requireEnv('AWF_ENCLAVE_AGENT_BACKEND');
if (backend === 'cloud-hypervisor') return loadHostConfig('agent', server);
if (backend === 'cloud-hypervisor' || backend === 'docker') return loadHostConfig('agent', server);
if (!AGENT_SUPPORTED_BACKENDS.has(backend)) {
throw new Error(`Unsupported AWF_ENCLAVE_AGENT_BACKEND: ${backend}`);
}
Expand Down Expand Up @@ -370,10 +370,19 @@ function loadAgentConfig(server, files = fs) {
}

function loadHostConfig(kind, server) {
if (kind === 'agent' && isDynamicAdmissionEnabled()) {
const prefix = kind === 'script' ? 'AWF_ENCLAVE' : 'AWF_ENCLAVE_AGENT';
const backend = requireEnv(`${prefix}_BACKEND`);
const dynamicEnabled = kind === 'agent' && isDynamicAdmissionEnabled();
if (dynamicEnabled && backend !== 'docker') {
throw new Error('Dynamic host executor admission is not yet supported');
}
const prefix = kind === 'script' ? 'AWF_ENCLAVE' : 'AWF_ENCLAVE_AGENT';
if (dynamicEnabled && process.env.AWF_ENCLAVE_AGENT_GITHUB_ENABLED === 'true') {
throw new Error('An enclave entry declares a static GitHub profile and a dynamic policy');
}
const dynamicSensitivity = dynamicEnabled ? process.env.AWF_ENCLAVE_AGENT_DYNAMIC_SENSITIVITY : undefined;
if (dynamicEnabled && !DYNAMIC_SENSITIVITIES.has(dynamicSensitivity)) {
throw new Error('AWF_ENCLAVE_AGENT_DYNAMIC_SENSITIVITY is not a supported sensitivity');
}
const entryId = requireEnv(`${prefix}_ENTRY_ID`);
if (!/^[a-z0-9](?:[a-z0-9-]{0,62})$/.test(entryId)) {
throw new Error('Invalid trusted host executor entry identity');
Expand All @@ -383,9 +392,9 @@ function loadHostConfig(kind, server) {
return {
entryId,
runId,
executorBackend: 'cloud-hypervisor',
executorBackend: backend,
executorTransport: 'host',
backend: 'cloud-hypervisor',
backend,
primaryBackend: server.primaryBackend,
hostExecutorSocketPath: path.join(HOST_EXECUTOR_DIR, 'executor.sock'),
hostExecutorCapabilityPath: path.join(HOST_EXECUTOR_DIR, 'capability'),
Expand All @@ -394,7 +403,9 @@ function loadHostConfig(kind, server) {
maxOutputBytes: positiveInt(`${prefix}_MAX_OUTPUT_BYTES`, MAX_RESULT_BYTES, MAX_RESULT_BYTES),
maxScriptBytes: positiveInt('AWF_ENCLAVE_MAX_SCRIPT_BYTES', MAX_SCRIPT_BYTES, MAX_SCRIPT_BYTES),
maxPromptBytes: positiveInt('AWF_ENCLAVE_AGENT_MAX_PROMPT_BYTES', 4096, MAX_TASK_BYTES),
dynamicEnabled: false,
githubEnabled: kind === 'agent' && process.env.AWF_ENCLAVE_AGENT_GITHUB_ENABLED === 'true',
dynamicEnabled,
dynamicSensitivity,
};
}

Expand Down
13 changes: 6 additions & 7 deletions containers/enclave/mcp-server/environment-probes.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,21 +100,20 @@ function selectedProbeIds(environment) {
const scriptBackend = scriptEnabled ? environment.AWF_ENCLAVE_BACKEND : undefined;
const agentBackend = agentEnabled ? environment.AWF_ENCLAVE_AGENT_BACKEND : undefined;
const usesContainerExecutor = [scriptBackend, agentBackend].some(
backend => backend !== undefined && backend !== 'cloud-hypervisor',
backend => backend === 'gvisor',
);
if (environment.AWF_ENCLAVE_SEED_MAP_ENABLED !== 'false') ids.push('seed-map');
if (usesContainerExecutor) {
ids.push('docker-socket', 'docker-executable');
if (environment.AWF_ENCLAVE_SEED_MAP_ENABLED !== 'false') ids.push('seed-directory');
if (scriptBackend && scriptBackend !== 'cloud-hypervisor') ids.push('script-seccomp');
if (agentBackend && agentBackend !== 'cloud-hypervisor') ids.push('agent-seccomp');
if (scriptBackend === 'gvisor') ids.push('script-seccomp');
if (agentBackend === 'gvisor') ids.push('agent-seccomp');
}
if (environment.AWF_ENCLAVE_AGENT_GITHUB_ENABLED === 'true') ids.push('agent-id');
if (environment.AWF_ENCLAVE_BACKEND === 'cloud-hypervisor'
|| environment.AWF_ENCLAVE_AGENT_BACKEND === 'cloud-hypervisor') {
if (agentBackend === 'gvisor' && environment.AWF_ENCLAVE_AGENT_GITHUB_ENABLED === 'true') ids.push('agent-id');
if ([scriptBackend, agentBackend].some(backend => backend === 'docker' || backend === 'cloud-hypervisor')) {
ids.push('host-executor-socket', 'host-executor-capability');
}
if (environment.AWF_ENCLAVE_AGENT_DYNAMIC_ENABLED === 'true') ids.push('delegation-channel');
if (agentBackend === 'gvisor' && environment.AWF_ENCLAVE_AGENT_DYNAMIC_ENABLED === 'true') ids.push('delegation-channel');
return ids;
}

Expand Down
21 changes: 12 additions & 9 deletions containers/enclave/mcp-server/host-executor-runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ const FAILURE_REASONS = new Map([
* prerequisite checks. Repository staging and result files belong to AWF.
*/
function usesHostExecutor(config) {
return config.executorTransport === 'host' || config.executorBackend === 'cloud-hypervisor';
return config.executorTransport === 'host'
|| config.executorBackend === 'docker' || config.executorBackend === 'cloud-hypervisor';
Comment on lines +35 to +36
}

function createHostExecutorRunner(config, deps = {}) {
Expand Down Expand Up @@ -90,7 +91,7 @@ function createHostExecutorRunner(config, deps = {}) {
} catch {
diagnose({ phase: 'availability', status: 'failed' });
closed = true;
throw new Error('Cloud Hypervisor enclave host executor is unavailable; no runtime fallback');
throw new Error('Enclave host executor is unavailable; no runtime fallback');
}
},
async reconcileRun() {
Expand Down Expand Up @@ -128,7 +129,7 @@ function createHostExecutorRunner(config, deps = {}) {
};
const timer = setTimeout(() => {
timedOut = true;
cancel();
if (!registered) cancel();
}, Math.max(0, deadlineMs - nowMs()));
signal?.addEventListener('abort', cancel);
try {
Expand All @@ -149,18 +150,16 @@ function createHostExecutorRunner(config, deps = {}) {
admissionId,
}));
registered = true;
if (signal?.aborted || timedOut) cancel();
if (signal?.aborted && response.state !== 'terminal') cancel();
let previousState = response.state;
let previousGeneration = response.cancelGeneration;
while (response.state === 'running' || response.state === 'cancelling') {
if (signal?.aborted || nowMs() >= deadlineMs) {
timedOut ||= nowMs() >= deadlineMs;
cancel();
}
if (signal?.aborted) cancel();
if (cancelledAt !== undefined && nowMs() - cancelledAt >= drainMs) {
throw new Error('Host executor cancellation did not terminate');
}
await new Promise((resolve) => setTimeout(resolve, pollMs));
const remainingMs = Math.max(0, deadlineMs - nowMs());
await new Promise((resolve) => setTimeout(resolve, Math.min(pollMs, remainingMs)));
progress('status');
response = checked(await client.status(identity));
if (response.cancelGeneration < previousGeneration
Expand All @@ -169,6 +168,10 @@ function createHostExecutorRunner(config, deps = {}) {
}
previousState = response.state;
previousGeneration = response.cancelGeneration;
if (response.state !== 'terminal' && nowMs() >= deadlineMs) {
timedOut = true;
cancel();
}
}
if (cancelPromise) {
progress('cancel-confirmation');
Expand Down
2 changes: 1 addition & 1 deletion containers/enclave/mcp-server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ async function main() {
// admission before any repository content is exposed. `enclave_run_script`
// is never registered for a dynamic entry, so it is neither advertised
// nor dispatchable.
...(config.dynamicEnabled
...(config.dynamicEnabled && !runner.hostOwned
? { admission: createDynamicDelegationClient(config) }
: {}),
}),
Expand Down
10 changes: 10 additions & 0 deletions containers/enclave/script-entrypoint.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import json
import runpy
import shutil
import stat
import sys
import time
from pathlib import Path
Expand All @@ -15,6 +16,14 @@
SCRIPT = "/awf/query-script.py"
OUTPUT = Path("/awf/out")

def make_repository_writable():
for root, directories, files in os.walk(REPO, followlinks=False):
for name in [".", *directories, *files]:
target = Path(root) if name == "." else Path(root) / name
mode = target.lstat().st_mode
if stat.S_ISREG(mode) or stat.S_ISDIR(mode):
target.chmod(stat.S_IMODE(mode) | stat.S_IWUSR)

def main():
started = time.monotonic()
phase = "startup"
Expand All @@ -35,6 +44,7 @@ def diagnostic(status, **fields):
phase = "seed-copy"
diagnostic("started")
shutil.copytree(SEED, REPO, symlinks=True)
make_repository_writable()
diagnostic("passed")
phase = "output-link"
diagnostic("started")
Expand Down
6 changes: 6 additions & 0 deletions containers/enclave/script-executor/executor-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,12 @@ function createExecutorHandler(params) {
return;
}
seed = { seedId: undefined, sensitivity: admitted.sensitivity };
} else if (runner.hostOwned && config.dynamicEnabled) {
// The host alone admits the selector and owns its delegated identity.
// Reserve the worst-case disclosure locally before asking it to launch;
// denied selectors never refund or reset the shared repository ledger.
ledger.registerRepository(repoKey, config.dynamicSensitivity);
seed = { seedId: undefined, sensitivity: config.dynamicSensitivity };
} else {
seed = seedMap.get(repoKey);
if (!seed) {
Expand Down
Loading
Loading