Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
09f347d
Add bounded production agent adapters
mchwang Sep 24, 2026
8e9f77a
Harden adapter capture settlement
mchwang Sep 24, 2026
44d1304
Pin adapter output to dedicated tmpfs
mchwang Sep 25, 2026
cb6f3fb
Close remaining adapter lifecycle races
mchwang Sep 25, 2026
0453c5d
Validate pinned output identities
mchwang Sep 25, 2026
485521d
Make cleanup and decode settlement retryable
mchwang Sep 25, 2026
a4322e8
Abort and await bounded adapter capture
mchwang Sep 25, 2026
963ca16
Retain adapter cleanup ownership
mchwang Sep 25, 2026
ef40f26
Allow loaded Docker cleanup observation
mchwang Sep 25, 2026
97ca736
Secure deferred output acknowledgement
mchwang Sep 25, 2026
1fed991
Retain colliding cleanup recovery
mchwang Sep 25, 2026
ae99db9
Preserve adapter setup ownership
mchwang Sep 25, 2026
ee57917
Bound cancellation with monotonic deadlines
mchwang Sep 25, 2026
fe8c11f
Carry invocation ownership through settlement
mchwang Sep 25, 2026
82dbc83
Guard active profile and close deadline
mchwang Sep 25, 2026
bee7c64
Apply adapter timeout across setup
mchwang Sep 25, 2026
0988f12
Bound adapter cleanup and final stderr
mchwang Sep 25, 2026
1f7cf36
Keep decoder settlement timers alive
mchwang Sep 25, 2026
4c1dc2e
Retain recovery profile ownership
mchwang Sep 25, 2026
8b963a4
Revalidate container at launch boundary
mchwang Sep 25, 2026
bf65dec
Validate profile capability before cleanup
mchwang Sep 25, 2026
27059e3
Preserve cleanup cancellation reasons
mchwang Sep 25, 2026
a5dfaba
Update cleanup cancellation regression
mchwang Sep 25, 2026
10c8d52
Authenticate profiles at disposal boundary
mchwang Sep 25, 2026
d9539fb
Skip cleanup for unauthenticated profiles and stop on late close
mchwang Sep 25, 2026
cd1fc31
Reject duplicate attempts without touching the active container
mchwang Sep 25, 2026
691df93
Keep D4 Docker suites out of the parallel CI run
mchwang Sep 25, 2026
dd0af27
Retain captured output in fixed-size blocks
mchwang Sep 25, 2026
8bc00da
Keep killed creates unsettled across cleanups and publish strict UTF-8
mchwang Sep 25, 2026
6c757e1
Treat an absent container as settled once the create window passes
mchwang Sep 25, 2026
a7a6eb9
Fail closed on a trailing incomplete UTF-8 character unless truncated
mchwang Sep 25, 2026
d309dd7
Release only the profile when rejecting an invocation before creation
mchwang Sep 25, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/agent-isolation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,4 @@ jobs:
- run: npm ci --ignore-scripts
- run: npm run typecheck
# The Docker suites share one image tag and daemon, so run test files one at a time.
- run: npx vitest run --no-file-parallelism test/agent-contract.test.ts test/agent-clone.test.ts test/agent-container.test.ts test/agent-network.test.ts test/agent-policy.test.ts test/agent-proxy.test.ts
- run: npx vitest run --no-file-parallelism test/agent-contract.test.ts test/agent-clone.test.ts test/agent-container.test.ts test/agent-network.test.ts test/agent-policy.test.ts test/agent-proxy.test.ts test/agent-adapter.test.ts test/agent-supervisor.test.ts test/agent-output.test.ts
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ jobs:
- run: npm run typecheck
# The Docker agent suites run one file at a time in the Agent isolation workflow; running them here
# would put them in parallel against the same image tag and daemon.
- run: npm test -- --exclude test/agent-container.test.ts --exclude test/agent-network.test.ts
- run: npm test -- --exclude test/agent-container.test.ts --exclude test/agent-network.test.ts --exclude test/agent-adapter.test.ts --exclude test/agent-supervisor.test.ts
- run: npx playwright install --with-deps chromium
- run: npm run test:browser
54 changes: 54 additions & 0 deletions agents/adapters/claude.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import type { InvocationHandle } from '../contract.ts';
import { createContainerProfile, ProfileCreationCleanupError } from '../container/profile.ts';
import { createVendorNetwork, removeVendorNetwork, VendorNetworkCreationCleanupError,
type VendorNetwork } from '../network/network.ts';
import { createClaudeCommand, createPhasePolicy } from '../policy.ts';
import { retainNetworkCleanup, retainSetupCleanup, startProfileInvocation } from './supervisor.ts';
import { createAdapterInvocationBudget, type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts';

export function parseClaudeOutput(raw: Buffer): { text: string; providerFailed: boolean } {
const envelope = JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(raw)) as
{ result?: unknown; is_error?: unknown };
if (typeof envelope.result !== 'string' || typeof envelope.is_error !== 'boolean')
throw new Error('Claude returned a malformed output envelope.');
return Object.freeze({ text: envelope.result, providerFailed: envelope.is_error });
}

export function startClaudeInvocation(request: AgentAdapterRequest,
oauthToken: string, options: AgentAdapterOptions = {}): InvocationHandle {
if (!oauthToken || oauthToken.includes('\0')) throw new Error('Claude OAuth token is malformed.');
const policy = createPhasePolicy(request.invocation);
const remaining = createAdapterInvocationBudget(request.invocation, options.timeoutMs);
let network: VendorNetwork;
try { network = createVendorNetwork(request.invocation, request.imageId, Math.min(60_000, remaining())); }
catch (error) {
if (error instanceof VendorNetworkCreationCleanupError)
return retainSetupCleanup(request.invocation, error.retryCleanup, error.startupError, error,
'network creation cleanup');
throw error;
}
try {
const profile = createContainerProfile({ ...request, policy, network,
command: createClaudeCommand(policy, request.prompt), claudeToken: oauthToken,
timeoutMs: Math.min(60_000, remaining()) });
return startProfileInvocation(profile, { ...options, secrets: { CLAUDE_CODE_OAUTH_TOKEN: oauthToken },
invocationBudget: remaining,
decode: (_profile, raw) => parseClaudeOutput(raw) });
} catch (error) {
if (error instanceof ProfileCreationCleanupError) {
const retryCleanup = (networkTimeoutMs = 30_000) => {
const failures: unknown[] = [];
try { error.retryCleanup(); } catch (cleanupError) { failures.push(cleanupError); }
try { removeVendorNetwork(network, networkTimeoutMs); } catch (cleanupError) { failures.push(cleanupError); }
if (failures.length) throw new AggregateError(failures, 'Adapter setup cleanup did not settle.');
};
try { retryCleanup(Math.min(30_000, remaining())); }
catch (cleanupError) { return retainSetupCleanup(request.invocation, () => retryCleanup(),
error.startupError, cleanupError, 'profile and network cleanup'); }
throw error.startupError;
}
try { removeVendorNetwork(network, Math.min(30_000, remaining())); }
catch (cleanupError) { return retainNetworkCleanup(request.invocation, network, error, cleanupError); }
throw error;
Comment thread
mchwang marked this conversation as resolved.
}
}
57 changes: 57 additions & 0 deletions agents/adapters/codex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import type { InvocationHandle } from '../contract.ts';
import { createContainerProfile, ProfileCreationCleanupError } from '../container/profile.ts';
import { createVendorNetwork, removeVendorNetwork, VendorNetworkCreationCleanupError,
type VendorNetwork } from '../network/network.ts';
import { createCodexCommand, createPhasePolicy } from '../policy.ts';
import { readBoundedContainerFile, retainNetworkCleanup, retainSetupCleanup,
startProfileInvocation } from './supervisor.ts';
import { createAdapterInvocationBudget, type AgentAdapterOptions, type AgentAdapterRequest } from './types.ts';

export const CODEX_OUTPUT_FILE = '/run/codeboost-output/final.txt';

export async function readCodexOutput(container: string, maximumBytes: number, timeoutMs = 30_000,
signal?: AbortSignal) {
const output = await readBoundedContainerFile(container, CODEX_OUTPUT_FILE, maximumBytes, timeoutMs, signal);
const text = new TextDecoder('utf-8', { fatal: true }).decode(output);
return Object.freeze({ text, additionalBytes: output.length });
}

export function startCodexInvocation(request: AgentAdapterRequest,
authFile: string, options: AgentAdapterOptions = {}): InvocationHandle {
if (!authFile || authFile.includes('\0')) throw new Error('Codex auth path is malformed.');
const policy = createPhasePolicy(request.invocation);
const remaining = createAdapterInvocationBudget(request.invocation, options.timeoutMs);
let network: VendorNetwork;
try { network = createVendorNetwork(request.invocation, request.imageId, Math.min(60_000, remaining())); }
catch (error) {
if (error instanceof VendorNetworkCreationCleanupError)
return retainSetupCleanup(request.invocation, error.retryCleanup, error.startupError, error,
'network creation cleanup');
throw error;
}
try {
const profile = createContainerProfile({ ...request, policy, network,
command: createCodexCommand(policy, request.prompt), codexAuthFile: authFile, deferredOutput: true,
timeoutMs: Math.min(60_000, remaining()) });
return startProfileInvocation(profile, { ...options,
invocationBudget: remaining,
decode: (current, _raw, maximum, timeoutMs, signal) =>
readCodexOutput(current.name, maximum, timeoutMs, signal) });
} catch (error) {
if (error instanceof ProfileCreationCleanupError) {
const retryCleanup = (networkTimeoutMs = 30_000) => {
const failures: unknown[] = [];
try { error.retryCleanup(); } catch (cleanupError) { failures.push(cleanupError); }
try { removeVendorNetwork(network, networkTimeoutMs); } catch (cleanupError) { failures.push(cleanupError); }
if (failures.length) throw new AggregateError(failures, 'Adapter setup cleanup did not settle.');
};
try { retryCleanup(Math.min(30_000, remaining())); }
catch (cleanupError) { return retainSetupCleanup(request.invocation, () => retryCleanup(),
error.startupError, cleanupError, 'profile and network cleanup'); }
throw error.startupError;
}
try { removeVendorNetwork(network, Math.min(30_000, remaining())); }
catch (cleanupError) { return retainNetworkCleanup(request.invocation, network, error, cleanupError); }
throw error;
Comment thread
mchwang marked this conversation as resolved.
}
}
Loading
Loading