@provos/ironcurtain 0.3.1 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -1
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -1
- package/dist/config/index.js +12 -1
- package/dist/config/index.js.map +1 -1
- package/dist/config/mcp-servers.json +3 -0
- package/dist/config/types.d.ts +2 -0
- package/dist/config/user-config.d.ts +11 -0
- package/dist/config/user-config.js +56 -2
- package/dist/config/user-config.js.map +1 -1
- package/dist/docker/adapters/claude-code.d.ts +4 -0
- package/dist/docker/adapters/claude-code.js +35 -21
- package/dist/docker/adapters/claude-code.js.map +1 -1
- package/dist/docker/agent-adapter.d.ts +6 -12
- package/dist/docker/audit-log-tailer.d.ts +4 -3
- package/dist/docker/audit-log-tailer.js +9 -7
- package/dist/docker/audit-log-tailer.js.map +1 -1
- package/dist/docker/code-mode-proxy.d.ts +34 -0
- package/dist/docker/code-mode-proxy.js +137 -0
- package/dist/docker/code-mode-proxy.js.map +1 -0
- package/dist/docker/docker-agent-session.d.ts +17 -5
- package/dist/docker/docker-agent-session.js +171 -36
- package/dist/docker/docker-agent-session.js.map +1 -1
- package/dist/docker/docker-manager.js +106 -15
- package/dist/docker/docker-manager.js.map +1 -1
- package/dist/docker/mitm-proxy.d.ts +7 -4
- package/dist/docker/mitm-proxy.js +43 -13
- package/dist/docker/mitm-proxy.js.map +1 -1
- package/dist/docker/orientation.d.ts +3 -2
- package/dist/docker/orientation.js +6 -3
- package/dist/docker/orientation.js.map +1 -1
- package/dist/docker/platform.d.ts +20 -0
- package/dist/docker/platform.js +24 -0
- package/dist/docker/platform.js.map +1 -0
- package/dist/docker/types.d.ts +59 -7
- package/dist/sandbox/index.d.ts +25 -1
- package/dist/sandbox/index.js +78 -9
- package/dist/sandbox/index.js.map +1 -1
- package/dist/servers/fetch-server.d.ts +6 -1
- package/dist/servers/fetch-server.js +24 -12
- package/dist/servers/fetch-server.js.map +1 -1
- package/dist/session/agent-session.js +9 -5
- package/dist/session/agent-session.js.map +1 -1
- package/dist/session/index.js +18 -36
- package/dist/session/index.js.map +1 -1
- package/dist/session/preflight.js +6 -0
- package/dist/session/preflight.js.map +1 -1
- package/dist/session/prompts.d.ts +9 -3
- package/dist/session/prompts.js +28 -10
- package/dist/session/prompts.js.map +1 -1
- package/dist/signal/bot-command.d.ts +13 -0
- package/dist/signal/bot-command.js +54 -0
- package/dist/signal/bot-command.js.map +1 -0
- package/dist/signal/format.d.ts +31 -0
- package/dist/signal/format.js +101 -0
- package/dist/signal/format.js.map +1 -0
- package/dist/signal/index.d.ts +16 -0
- package/dist/signal/index.js +13 -0
- package/dist/signal/index.js.map +1 -0
- package/dist/signal/markdown-to-signal.d.ts +29 -0
- package/dist/signal/markdown-to-signal.js +154 -0
- package/dist/signal/markdown-to-signal.js.map +1 -0
- package/dist/signal/setup-signal.d.ts +48 -0
- package/dist/signal/setup-signal.js +443 -0
- package/dist/signal/setup-signal.js.map +1 -0
- package/dist/signal/signal-bot-daemon.d.ts +142 -0
- package/dist/signal/signal-bot-daemon.js +504 -0
- package/dist/signal/signal-bot-daemon.js.map +1 -0
- package/dist/signal/signal-config.d.ts +64 -0
- package/dist/signal/signal-config.js +53 -0
- package/dist/signal/signal-config.js.map +1 -0
- package/dist/signal/signal-container.d.ts +37 -0
- package/dist/signal/signal-container.js +112 -0
- package/dist/signal/signal-container.js.map +1 -0
- package/dist/signal/signal-transport.d.ts +34 -0
- package/dist/signal/signal-transport.js +70 -0
- package/dist/signal/signal-transport.js.map +1 -0
- package/dist/trusted-process/mcp-proxy-server.d.ts +111 -1
- package/dist/trusted-process/mcp-proxy-server.js +319 -247
- package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
- package/dist/trusted-process/path-utils.d.ts +5 -1
- package/dist/trusted-process/path-utils.js +47 -4
- package/dist/trusted-process/path-utils.js.map +1 -1
- package/dist/trusted-process/policy-engine.d.ts +3 -1
- package/dist/trusted-process/policy-engine.js +17 -6
- package/dist/trusted-process/policy-engine.js.map +1 -1
- package/dist/trusted-process/sandbox-integration.d.ts +11 -4
- package/dist/trusted-process/sandbox-integration.js +13 -7
- package/dist/trusted-process/sandbox-integration.js.map +1 -1
- package/dist/trusted-process/tcp-server-transport.d.ts +43 -0
- package/dist/trusted-process/tcp-server-transport.js +119 -0
- package/dist/trusted-process/tcp-server-transport.js.map +1 -0
- package/dist/types/argument-roles.d.ts +7 -0
- package/dist/types/argument-roles.js +30 -5
- package/dist/types/argument-roles.js.map +1 -1
- package/docker/Dockerfile.base.arm64 +23 -0
- package/package.json +9 -4
- package/src/config/mcp-servers.json +3 -0
- package/dist/docker/managed-proxy.d.ts +0 -27
- package/dist/docker/managed-proxy.js +0 -175
- package/dist/docker/managed-proxy.js.map +0 -1
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tails a JSONL audit log file and emits DiagnosticEvent callbacks
|
|
3
|
-
* for each new entry. Uses fs.
|
|
3
|
+
* for each new entry. Uses fs.watchFile() for change notification and
|
|
4
4
|
* tracks the file read offset to parse only new lines.
|
|
5
5
|
*/
|
|
6
|
-
import { openSync, readSync, fstatSync, closeSync,
|
|
6
|
+
import { openSync, readSync, fstatSync, closeSync, watchFile, unwatchFile } from 'node:fs';
|
|
7
7
|
export class AuditLogTailer {
|
|
8
8
|
offset = 0;
|
|
9
|
-
|
|
9
|
+
watching = false;
|
|
10
10
|
auditLogPath;
|
|
11
11
|
onDiagnostic;
|
|
12
12
|
constructor(auditLogPath, onDiagnostic) {
|
|
@@ -14,14 +14,16 @@ export class AuditLogTailer {
|
|
|
14
14
|
this.onDiagnostic = onDiagnostic;
|
|
15
15
|
}
|
|
16
16
|
start() {
|
|
17
|
-
this.
|
|
17
|
+
this.watching = true;
|
|
18
|
+
watchFile(this.auditLogPath, { interval: 100 }, () => this.readNewEntries());
|
|
18
19
|
}
|
|
19
20
|
stop() {
|
|
20
|
-
if (this.
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
21
|
+
if (this.watching) {
|
|
22
|
+
unwatchFile(this.auditLogPath);
|
|
23
|
+
this.watching = false;
|
|
23
24
|
}
|
|
24
25
|
}
|
|
26
|
+
/** Read and process any new entries appended since the last read. */
|
|
25
27
|
readNewEntries() {
|
|
26
28
|
let fd;
|
|
27
29
|
try {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"audit-log-tailer.js","sourceRoot":"","sources":["../../src/docker/audit-log-tailer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,
|
|
1
|
+
{"version":3,"file":"audit-log-tailer.js","sourceRoot":"","sources":["../../src/docker/audit-log-tailer.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3F,MAAM,OAAO,cAAc;IACjB,MAAM,GAAG,CAAC,CAAC;IACX,QAAQ,GAAG,KAAK,CAAC;IACR,YAAY,CAAS;IACrB,YAAY,CAAmC;IAEhE,YAAY,YAAoB,EAAE,YAA8C;QAC9E,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACnC,CAAC;IAED,KAAK;QACH,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,SAAS,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,GAAG,EAAE,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,WAAW,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;YAC/B,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACxB,CAAC;IACH,CAAC;IAED,qEAAqE;IACrE,cAAc;QACZ,IAAI,EAAU,CAAC;QACf,IAAI,CAAC;YACH,EAAE,GAAG,QAAQ,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QACxC,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,CAAC,yBAAyB;QACnC,CAAC;QAED,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;YAC3B,IAAI,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,MAAM;gBAAE,OAAO;YAErC,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC;YAClD,QAAQ,CAAC,EAAE,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;YAC9C,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC;YAExB,MAAM,KAAK,GAAG,GAAG,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;YACzD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBACzB,IAAI,CAAC;oBACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAe,CAAC;oBAC7C,IAAI,CAAC,YAAY,CAAC;wBAChB,IAAI,EAAE,WAAW;wBACjB,QAAQ,EAAE,GAAG,KAAK,CAAC,UAAU,IAAI,KAAK,CAAC,QAAQ,EAAE;wBACjD,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC;qBAClC,CAAC,CAAC;gBACL,CAAC;gBAAC,MAAM,CAAC;oBACP,0CAA0C;gBAC5C,CAAC;YACH,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,EAAE,CAAC,CAAC;QAChB,CAAC;IACH,CAAC;IAEO,YAAY,CAAC,KAAiB;QACpC,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC;QACnC,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC7C,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC;QACxE,OAAO,IAAI,MAAM,KAAK,OAAO,EAAE,CAAC;IAClC,CAAC;CACF"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeModeProxy -- MCP server that exposes a single `execute_code` tool
|
|
3
|
+
* backed by the UTCP Code Mode sandbox.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the old ManagedProxy (which spawned mcp-proxy-server.ts as a
|
|
6
|
+
* child process). Docker agents now send TypeScript via `execute_code`,
|
|
7
|
+
* sharing the same execution engine as builtin Code Mode sessions.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* Container (Claude Code) → socat → UDS/TCP socket
|
|
11
|
+
* → CodeModeProxy (MCP Server with execute_code)
|
|
12
|
+
* → Sandbox.executeCode(code)
|
|
13
|
+
* → V8 isolate → UTCP → per-server mcp-proxy-server.ts (stdio)
|
|
14
|
+
* → PolicyEngine → real MCP servers
|
|
15
|
+
*/
|
|
16
|
+
import { type HelpData } from '../sandbox/index.js';
|
|
17
|
+
import type { IronCurtainConfig } from '../config/types.js';
|
|
18
|
+
/** Public interface for the Docker session's proxy. */
|
|
19
|
+
export interface DockerProxy {
|
|
20
|
+
start(): Promise<void>;
|
|
21
|
+
getHelpData(): HelpData;
|
|
22
|
+
stop(): Promise<void>;
|
|
23
|
+
readonly socketPath: string;
|
|
24
|
+
readonly port: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
export interface CodeModeProxyOptions {
|
|
27
|
+
/** Absolute path for the UDS socket (used in UDS mode, also stored for reference in TCP mode). */
|
|
28
|
+
readonly socketPath: string;
|
|
29
|
+
/** Session configuration passed to the Sandbox. */
|
|
30
|
+
readonly config: IronCurtainConfig;
|
|
31
|
+
/** Listen mode: 'uds' (default) or 'tcp'. TCP mode uses OS-assigned port. */
|
|
32
|
+
readonly listenMode?: 'uds' | 'tcp';
|
|
33
|
+
}
|
|
34
|
+
export declare function createCodeModeProxy(options: CodeModeProxyOptions): DockerProxy;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CodeModeProxy -- MCP server that exposes a single `execute_code` tool
|
|
3
|
+
* backed by the UTCP Code Mode sandbox.
|
|
4
|
+
*
|
|
5
|
+
* Replaces the old ManagedProxy (which spawned mcp-proxy-server.ts as a
|
|
6
|
+
* child process). Docker agents now send TypeScript via `execute_code`,
|
|
7
|
+
* sharing the same execution engine as builtin Code Mode sessions.
|
|
8
|
+
*
|
|
9
|
+
* Architecture:
|
|
10
|
+
* Container (Claude Code) → socat → UDS/TCP socket
|
|
11
|
+
* → CodeModeProxy (MCP Server with execute_code)
|
|
12
|
+
* → Sandbox.executeCode(code)
|
|
13
|
+
* → V8 isolate → UTCP → per-server mcp-proxy-server.ts (stdio)
|
|
14
|
+
* → PolicyEngine → real MCP servers
|
|
15
|
+
*/
|
|
16
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
17
|
+
import { ListToolsRequestSchema, CallToolRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
18
|
+
import { UdsServerTransport } from '../trusted-process/uds-server-transport.js';
|
|
19
|
+
import { TcpServerTransport } from '../trusted-process/tcp-server-transport.js';
|
|
20
|
+
import { Sandbox } from '../sandbox/index.js';
|
|
21
|
+
import { VERSION } from '../version.js';
|
|
22
|
+
export function createCodeModeProxy(options) {
|
|
23
|
+
const sandbox = new Sandbox();
|
|
24
|
+
const useTcp = options.listenMode === 'tcp';
|
|
25
|
+
let mcpServer = null; // eslint-disable-line @typescript-eslint/no-deprecated
|
|
26
|
+
let transport = null;
|
|
27
|
+
return {
|
|
28
|
+
socketPath: options.socketPath,
|
|
29
|
+
get port() {
|
|
30
|
+
if (transport instanceof TcpServerTransport) {
|
|
31
|
+
try {
|
|
32
|
+
return transport.port;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
},
|
|
40
|
+
async start() {
|
|
41
|
+
// 1. Initialize the sandbox (registers MCP proxy servers per backend)
|
|
42
|
+
await sandbox.initialize(options.config);
|
|
43
|
+
// 2. Create the MCP server exposing a single execute_code tool
|
|
44
|
+
mcpServer = new Server(// eslint-disable-line @typescript-eslint/no-deprecated
|
|
45
|
+
{ name: 'ironcurtain-code-mode', version: VERSION }, { capabilities: { tools: {} } });
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/require-await -- MCP SDK handler must return Promise
|
|
47
|
+
mcpServer.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
48
|
+
tools: [
|
|
49
|
+
{
|
|
50
|
+
name: 'execute_code',
|
|
51
|
+
description: 'Execute TypeScript code in a secure sandbox. ' +
|
|
52
|
+
'Tool calls are synchronous (no await). ' +
|
|
53
|
+
'Use `return` to send a value back. ' +
|
|
54
|
+
'Call help.help() to discover available tools.',
|
|
55
|
+
inputSchema: {
|
|
56
|
+
type: 'object',
|
|
57
|
+
properties: {
|
|
58
|
+
code: {
|
|
59
|
+
type: 'string',
|
|
60
|
+
description: 'TypeScript code to execute',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: ['code'],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
}));
|
|
68
|
+
mcpServer.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
69
|
+
if (request.params.name !== 'execute_code') {
|
|
70
|
+
return {
|
|
71
|
+
content: [{ type: 'text', text: `Unknown tool: ${request.params.name}` }],
|
|
72
|
+
isError: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const args = request.params.arguments;
|
|
76
|
+
const code = args?.code;
|
|
77
|
+
if (!code || typeof code !== 'string') {
|
|
78
|
+
return {
|
|
79
|
+
content: [{ type: 'text', text: 'Missing required parameter: code' }],
|
|
80
|
+
isError: true,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
const { result, logs } = await sandbox.executeCode(code);
|
|
85
|
+
const parts = [];
|
|
86
|
+
if (logs.length > 0) {
|
|
87
|
+
parts.push(`[logs]\n${logs.join('\n')}`);
|
|
88
|
+
}
|
|
89
|
+
if (result === undefined) {
|
|
90
|
+
parts.push('(no return value)');
|
|
91
|
+
}
|
|
92
|
+
else if (typeof result === 'string') {
|
|
93
|
+
parts.push(result);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
parts.push(JSON.stringify(result, null, 2));
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text', text: parts.join('\n\n') }],
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
return {
|
|
104
|
+
content: [
|
|
105
|
+
{
|
|
106
|
+
type: 'text',
|
|
107
|
+
text: `Execution error: ${err instanceof Error ? err.message : String(err)}`,
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
isError: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
// 3. Start the transport
|
|
115
|
+
if (useTcp) {
|
|
116
|
+
transport = new TcpServerTransport('127.0.0.1', 0);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
transport = new UdsServerTransport(options.socketPath);
|
|
120
|
+
}
|
|
121
|
+
await mcpServer.connect(transport);
|
|
122
|
+
},
|
|
123
|
+
getHelpData() {
|
|
124
|
+
return sandbox.getHelpData();
|
|
125
|
+
},
|
|
126
|
+
async stop() {
|
|
127
|
+
if (mcpServer) {
|
|
128
|
+
await mcpServer.close();
|
|
129
|
+
mcpServer = null;
|
|
130
|
+
}
|
|
131
|
+
// mcpServer.close() closes the transport, but shutdown sandbox separately
|
|
132
|
+
transport = null;
|
|
133
|
+
await sandbox.shutdown();
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=code-mode-proxy.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-mode-proxy.js","sourceRoot":"","sources":["../../src/docker/code-mode-proxy.ts"],"names":[],"mappings":"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"}
|
|
@@ -15,7 +15,7 @@ import type { Session, SessionId, SessionInfo, ConversationTurn, DiagnosticEvent
|
|
|
15
15
|
import type { IronCurtainConfig } from '../config/types.js';
|
|
16
16
|
import type { AgentAdapter } from './agent-adapter.js';
|
|
17
17
|
import type { DockerManager } from './types.js';
|
|
18
|
-
import type {
|
|
18
|
+
import type { DockerProxy } from './code-mode-proxy.js';
|
|
19
19
|
import type { MitmProxy } from './mitm-proxy.js';
|
|
20
20
|
import type { CertificateAuthority } from './ca.js';
|
|
21
21
|
export interface DockerAgentSessionDeps {
|
|
@@ -23,7 +23,7 @@ export interface DockerAgentSessionDeps {
|
|
|
23
23
|
readonly sessionId: SessionId;
|
|
24
24
|
readonly adapter: AgentAdapter;
|
|
25
25
|
readonly docker: DockerManager;
|
|
26
|
-
readonly proxy:
|
|
26
|
+
readonly proxy: DockerProxy;
|
|
27
27
|
readonly mitmProxy: MitmProxy;
|
|
28
28
|
readonly ca: CertificateAuthority;
|
|
29
29
|
readonly fakeKeys: ReadonlyMap<string, string>;
|
|
@@ -31,6 +31,8 @@ export interface DockerAgentSessionDeps {
|
|
|
31
31
|
readonly sandboxDir: string;
|
|
32
32
|
readonly escalationDir: string;
|
|
33
33
|
readonly auditLogPath: string;
|
|
34
|
+
/** Use TCP transport instead of UDS (macOS Docker Desktop). */
|
|
35
|
+
readonly useTcp?: boolean;
|
|
34
36
|
readonly onEscalation?: (request: EscalationRequest) => void;
|
|
35
37
|
readonly onEscalationExpired?: () => void;
|
|
36
38
|
readonly onDiagnostic?: (event: DiagnosticEvent) => void;
|
|
@@ -48,9 +50,12 @@ export declare class DockerAgentSession implements Session {
|
|
|
48
50
|
private readonly sandboxDir;
|
|
49
51
|
private readonly escalationDir;
|
|
50
52
|
private readonly auditLogPath;
|
|
53
|
+
private readonly useTcp;
|
|
51
54
|
private status;
|
|
52
55
|
private readonly createdAt;
|
|
53
56
|
private containerId;
|
|
57
|
+
private sidecarContainerId;
|
|
58
|
+
private networkName;
|
|
54
59
|
private systemPrompt;
|
|
55
60
|
private turns;
|
|
56
61
|
private diagnosticLog;
|
|
@@ -66,9 +71,9 @@ export declare class DockerAgentSession implements Session {
|
|
|
66
71
|
constructor(deps: DockerAgentSessionDeps);
|
|
67
72
|
/**
|
|
68
73
|
* Initialize the Docker agent session:
|
|
69
|
-
* 1. Start
|
|
70
|
-
* 2. Start MITM proxy (UDS)
|
|
71
|
-
* 3.
|
|
74
|
+
* 1. Start Code Mode proxy (UDS/TCP)
|
|
75
|
+
* 2. Start MITM proxy (UDS/TCP)
|
|
76
|
+
* 3. Build server listings from sandbox help data
|
|
72
77
|
* 4. Generate orientation files
|
|
73
78
|
* 5. Ensure Docker image exists (with CA cert baked in)
|
|
74
79
|
* 6. Create and start container (--network=none)
|
|
@@ -79,11 +84,18 @@ export declare class DockerAgentSession implements Session {
|
|
|
79
84
|
sendMessage(userMessage: string): Promise<string>;
|
|
80
85
|
getHistory(): readonly ConversationTurn[];
|
|
81
86
|
getDiagnosticLog(): readonly DiagnosticEvent[];
|
|
87
|
+
/** Process any new audit log entries immediately (useful for tests). */
|
|
88
|
+
flushAuditLog(): void;
|
|
82
89
|
getPendingEscalation(): EscalationRequest | undefined;
|
|
83
90
|
getBudgetStatus(): BudgetStatus;
|
|
84
91
|
resolveEscalation(escalationId: string, decision: 'approved' | 'denied'): Promise<void>;
|
|
85
92
|
close(): Promise<void>;
|
|
86
93
|
private emitDiagnostic;
|
|
94
|
+
/**
|
|
95
|
+
* Checks whether the container can reach host-side proxies via the
|
|
96
|
+
* socat sidecar on the internal Docker network. Throws if connectivity fails.
|
|
97
|
+
*/
|
|
98
|
+
private checkInternalNetworkConnectivity;
|
|
87
99
|
private startEscalationWatcher;
|
|
88
100
|
private stopEscalationWatcher;
|
|
89
101
|
private pollEscalationDirectory;
|
|
@@ -14,11 +14,13 @@
|
|
|
14
14
|
import { mkdirSync, readdirSync, readFileSync, writeFileSync, existsSync, copyFileSync, rmSync } from 'node:fs';
|
|
15
15
|
import { resolve, dirname } from 'node:path';
|
|
16
16
|
import { mkdtempSync } from 'node:fs';
|
|
17
|
-
import { tmpdir } from 'node:os';
|
|
17
|
+
import { tmpdir, arch } from 'node:os';
|
|
18
|
+
import { quote } from 'shell-quote';
|
|
18
19
|
import { createHash } from 'node:crypto';
|
|
19
20
|
import { fileURLToPath } from 'node:url';
|
|
20
21
|
import { AuditLogTailer } from './audit-log-tailer.js';
|
|
21
22
|
import { prepareSession } from './orientation.js';
|
|
23
|
+
import { INTERNAL_NETWORK_NAME, INTERNAL_NETWORK_SUBNET, INTERNAL_NETWORK_GATEWAY } from './platform.js';
|
|
22
24
|
import { SessionNotReadyError, SessionClosedError } from '../session/errors.js';
|
|
23
25
|
import * as logger from '../logger.js';
|
|
24
26
|
const ESCALATION_POLL_INTERVAL_MS = 300;
|
|
@@ -35,9 +37,12 @@ export class DockerAgentSession {
|
|
|
35
37
|
sandboxDir;
|
|
36
38
|
escalationDir;
|
|
37
39
|
auditLogPath;
|
|
40
|
+
useTcp;
|
|
38
41
|
status = 'initializing';
|
|
39
42
|
createdAt;
|
|
40
43
|
containerId = null;
|
|
44
|
+
sidecarContainerId = null;
|
|
45
|
+
networkName = null;
|
|
41
46
|
systemPrompt = '';
|
|
42
47
|
turns = [];
|
|
43
48
|
diagnosticLog = [];
|
|
@@ -63,6 +68,7 @@ export class DockerAgentSession {
|
|
|
63
68
|
this.sandboxDir = deps.sandboxDir;
|
|
64
69
|
this.escalationDir = deps.escalationDir;
|
|
65
70
|
this.auditLogPath = deps.auditLogPath;
|
|
71
|
+
this.useTcp = deps.useTcp ?? false;
|
|
66
72
|
this.onEscalation = deps.onEscalation;
|
|
67
73
|
this.onEscalationExpired = deps.onEscalationExpired;
|
|
68
74
|
this.onDiagnostic = deps.onDiagnostic;
|
|
@@ -70,9 +76,9 @@ export class DockerAgentSession {
|
|
|
70
76
|
}
|
|
71
77
|
/**
|
|
72
78
|
* Initialize the Docker agent session:
|
|
73
|
-
* 1. Start
|
|
74
|
-
* 2. Start MITM proxy (UDS)
|
|
75
|
-
* 3.
|
|
79
|
+
* 1. Start Code Mode proxy (UDS/TCP)
|
|
80
|
+
* 2. Start MITM proxy (UDS/TCP)
|
|
81
|
+
* 3. Build server listings from sandbox help data
|
|
76
82
|
* 4. Generate orientation files
|
|
77
83
|
* 5. Ensure Docker image exists (with CA cert baked in)
|
|
78
84
|
* 6. Create and start container (--network=none)
|
|
@@ -82,46 +88,143 @@ export class DockerAgentSession {
|
|
|
82
88
|
// Ensure directories exist
|
|
83
89
|
mkdirSync(this.sandboxDir, { recursive: true });
|
|
84
90
|
mkdirSync(this.escalationDir, { recursive: true });
|
|
85
|
-
// 1. Start
|
|
91
|
+
// 1. Start Code Mode proxy
|
|
86
92
|
await this.proxy.start();
|
|
87
|
-
|
|
88
|
-
|
|
93
|
+
if (this.useTcp && this.proxy.port !== undefined) {
|
|
94
|
+
logger.info(`Code Mode proxy listening on 127.0.0.1:${this.proxy.port}`);
|
|
95
|
+
}
|
|
96
|
+
else {
|
|
97
|
+
logger.info(`Code Mode proxy listening on ${this.proxy.socketPath}`);
|
|
98
|
+
}
|
|
99
|
+
// 2. Start MITM proxy
|
|
89
100
|
const mitmAddr = await this.mitmProxy.start();
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
if (mitmAddr.port !== undefined) {
|
|
102
|
+
logger.info(`MITM proxy listening on 127.0.0.1:${mitmAddr.port}`);
|
|
103
|
+
}
|
|
104
|
+
else {
|
|
105
|
+
logger.info(`MITM proxy listening on ${mitmAddr.socketPath}`);
|
|
106
|
+
}
|
|
107
|
+
// 3. Build server listings from sandbox help data
|
|
108
|
+
const helpData = this.proxy.getHelpData();
|
|
109
|
+
const serverListings = Object.entries(helpData.serverDescriptions).map(([name, description]) => ({
|
|
110
|
+
name,
|
|
111
|
+
description,
|
|
112
|
+
}));
|
|
113
|
+
logger.info(`Available servers: ${serverListings.map((s) => s.name).join(', ')}`);
|
|
94
114
|
// 4. Generate orientation
|
|
95
|
-
|
|
115
|
+
// In TCP mode, the container reaches the MCP proxy via host.docker.internal
|
|
116
|
+
const proxyAddress = this.useTcp && this.proxy.port !== undefined ? `host.docker.internal:${this.proxy.port}` : undefined;
|
|
117
|
+
const { systemPrompt } = prepareSession(this.adapter, serverListings, this.sessionDir, this.config, this.sandboxDir, proxyAddress);
|
|
96
118
|
this.systemPrompt = systemPrompt;
|
|
97
119
|
// 5. Ensure Docker image exists (build if needed, with CA cert)
|
|
98
120
|
const image = await this.adapter.getImage();
|
|
99
121
|
await this.ensureImage(image);
|
|
100
|
-
// 6. Create and start container
|
|
122
|
+
// 6. Create and start container
|
|
101
123
|
const shortId = this.sessionId.substring(0, 12);
|
|
102
|
-
const env = {
|
|
103
|
-
...this.adapter.buildEnv(this.config, this.fakeKeys),
|
|
104
|
-
HTTPS_PROXY: 'http://127.0.0.1:18080',
|
|
105
|
-
HTTP_PROXY: 'http://127.0.0.1:18080',
|
|
106
|
-
};
|
|
107
124
|
const orientationDir = resolve(this.sessionDir, 'orientation');
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
125
|
+
let env;
|
|
126
|
+
let network;
|
|
127
|
+
let mounts;
|
|
128
|
+
let extraHosts;
|
|
129
|
+
if (this.useTcp && mitmAddr.port !== undefined && this.proxy.port !== undefined) {
|
|
130
|
+
// macOS TCP mode: internal bridge network blocks egress.
|
|
131
|
+
// A socat sidecar bridges the internal network to the host
|
|
132
|
+
// because Docker Desktop VMs don't forward gateway traffic.
|
|
133
|
+
const mcpPort = this.proxy.port;
|
|
134
|
+
const mitmPort = mitmAddr.port;
|
|
135
|
+
env = {
|
|
136
|
+
...this.adapter.buildEnv(this.config, this.fakeKeys),
|
|
137
|
+
HTTPS_PROXY: `http://host.docker.internal:${mitmPort}`,
|
|
138
|
+
HTTP_PROXY: `http://host.docker.internal:${mitmPort}`,
|
|
139
|
+
};
|
|
140
|
+
// Create an --internal Docker network that blocks internet egress
|
|
141
|
+
await this.docker.createNetwork(INTERNAL_NETWORK_NAME, {
|
|
142
|
+
internal: true,
|
|
143
|
+
subnet: INTERNAL_NETWORK_SUBNET,
|
|
144
|
+
gateway: INTERNAL_NETWORK_GATEWAY,
|
|
145
|
+
});
|
|
146
|
+
network = INTERNAL_NETWORK_NAME;
|
|
147
|
+
// Ensure the socat image is available
|
|
148
|
+
const socatImage = 'alpine/socat';
|
|
149
|
+
if (!(await this.docker.imageExists(socatImage))) {
|
|
150
|
+
logger.info(`Pulling ${socatImage}...`);
|
|
151
|
+
await this.docker.pullImage(socatImage);
|
|
152
|
+
}
|
|
153
|
+
// Create socat sidecar on the default bridge (can reach host.docker.internal)
|
|
154
|
+
const sidecarName = `ironcurtain-sidecar-${shortId}`;
|
|
155
|
+
this.sidecarContainerId = await this.docker.create({
|
|
156
|
+
image: socatImage,
|
|
157
|
+
name: sidecarName,
|
|
158
|
+
network: 'bridge',
|
|
159
|
+
mounts: [],
|
|
160
|
+
env: {},
|
|
161
|
+
entrypoint: '/bin/sh',
|
|
162
|
+
command: [
|
|
163
|
+
'-c',
|
|
164
|
+
quote(['socat', `TCP-LISTEN:${mcpPort},fork,reuseaddr`, `TCP:host.docker.internal:${mcpPort}`]) +
|
|
165
|
+
' & ' +
|
|
166
|
+
quote(['socat', `TCP-LISTEN:${mitmPort},fork,reuseaddr`, `TCP:host.docker.internal:${mitmPort}`]) +
|
|
167
|
+
' & wait',
|
|
168
|
+
],
|
|
169
|
+
});
|
|
170
|
+
await this.docker.start(this.sidecarContainerId);
|
|
171
|
+
// Connect sidecar to the internal network so the app container can reach it
|
|
172
|
+
await this.docker.connectNetwork(INTERNAL_NETWORK_NAME, this.sidecarContainerId);
|
|
173
|
+
const sidecarIp = await this.docker.getContainerIp(this.sidecarContainerId, INTERNAL_NETWORK_NAME);
|
|
174
|
+
extraHosts = [`host.docker.internal:${sidecarIp}`];
|
|
175
|
+
logger.info(`Sidecar ${sidecarName} bridging ports ${mcpPort},${mitmPort} at ${sidecarIp}`);
|
|
176
|
+
mounts = [
|
|
177
|
+
{ source: this.sandboxDir, target: '/workspace', readonly: false },
|
|
178
|
+
// No session dir mount needed for sockets (TCP mode) -- only orientation subdir is mounted
|
|
179
|
+
{ source: orientationDir, target: '/etc/ironcurtain', readonly: true },
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
// Linux UDS mode: --network=none, session dir with sockets mounted
|
|
184
|
+
env = {
|
|
185
|
+
...this.adapter.buildEnv(this.config, this.fakeKeys),
|
|
186
|
+
HTTPS_PROXY: 'http://127.0.0.1:18080',
|
|
187
|
+
HTTP_PROXY: 'http://127.0.0.1:18080',
|
|
188
|
+
};
|
|
189
|
+
network = 'none';
|
|
190
|
+
mounts = [
|
|
113
191
|
{ source: this.sandboxDir, target: '/workspace', readonly: false },
|
|
192
|
+
// Session dir contains proxy.sock and mitm-proxy.sock -- directory mount
|
|
193
|
+
// exposes both to the container (file mounts for UDS don't work on macOS Docker Desktop).
|
|
114
194
|
{ source: this.sessionDir, target: '/run/ironcurtain', readonly: false },
|
|
115
195
|
{ source: orientationDir, target: '/etc/ironcurtain', readonly: true },
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
try {
|
|
199
|
+
this.containerId = await this.docker.create({
|
|
200
|
+
image,
|
|
201
|
+
name: `ironcurtain-${shortId}`,
|
|
202
|
+
network,
|
|
203
|
+
mounts,
|
|
204
|
+
env,
|
|
205
|
+
command: ['sleep', 'infinity'],
|
|
206
|
+
sessionLabel: this.sessionId,
|
|
207
|
+
resources: { memoryMb: 4096, cpus: 2 },
|
|
208
|
+
extraHosts,
|
|
209
|
+
});
|
|
210
|
+
await this.docker.start(this.containerId);
|
|
211
|
+
this.networkName = network;
|
|
212
|
+
logger.info(`Container started: ${this.containerId.substring(0, 12)}`);
|
|
213
|
+
// Connectivity check: verify the container can reach host proxies
|
|
214
|
+
// through the internal network. Abort if unreachable.
|
|
215
|
+
if (this.useTcp && network === INTERNAL_NETWORK_NAME && this.proxy.port !== undefined) {
|
|
216
|
+
await this.checkInternalNetworkConnectivity(this.containerId, this.proxy.port);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch (err) {
|
|
220
|
+
// Clean up sidecar if app container setup fails
|
|
221
|
+
if (this.sidecarContainerId) {
|
|
222
|
+
await this.docker.stop(this.sidecarContainerId);
|
|
223
|
+
await this.docker.remove(this.sidecarContainerId);
|
|
224
|
+
this.sidecarContainerId = null;
|
|
225
|
+
}
|
|
226
|
+
throw err;
|
|
227
|
+
}
|
|
125
228
|
// 7. Start watchers
|
|
126
229
|
this.startEscalationWatcher();
|
|
127
230
|
// Create the audit log file so fs.watch() can attach to it.
|
|
@@ -194,6 +297,10 @@ export class DockerAgentSession {
|
|
|
194
297
|
getDiagnosticLog() {
|
|
195
298
|
return this.diagnosticLog;
|
|
196
299
|
}
|
|
300
|
+
/** Process any new audit log entries immediately (useful for tests). */
|
|
301
|
+
flushAuditLog() {
|
|
302
|
+
this.auditTailer?.readNewEntries();
|
|
303
|
+
}
|
|
197
304
|
getPendingEscalation() {
|
|
198
305
|
return this.pendingEscalation;
|
|
199
306
|
}
|
|
@@ -238,6 +345,15 @@ export class DockerAgentSession {
|
|
|
238
345
|
await this.docker.stop(this.containerId);
|
|
239
346
|
await this.docker.remove(this.containerId);
|
|
240
347
|
}
|
|
348
|
+
// Stop and remove sidecar container
|
|
349
|
+
if (this.sidecarContainerId) {
|
|
350
|
+
await this.docker.stop(this.sidecarContainerId);
|
|
351
|
+
await this.docker.remove(this.sidecarContainerId);
|
|
352
|
+
}
|
|
353
|
+
// Remove internal network (ignore errors -- other sessions may use it)
|
|
354
|
+
if (this.networkName === INTERNAL_NETWORK_NAME) {
|
|
355
|
+
await this.docker.removeNetwork(INTERNAL_NETWORK_NAME);
|
|
356
|
+
}
|
|
241
357
|
// Stop proxies
|
|
242
358
|
await this.mitmProxy.stop();
|
|
243
359
|
await this.proxy.stop();
|
|
@@ -247,6 +363,20 @@ export class DockerAgentSession {
|
|
|
247
363
|
this.diagnosticLog.push(event);
|
|
248
364
|
this.onDiagnostic?.(event);
|
|
249
365
|
}
|
|
366
|
+
/**
|
|
367
|
+
* Checks whether the container can reach host-side proxies via the
|
|
368
|
+
* socat sidecar on the internal Docker network. Throws if connectivity fails.
|
|
369
|
+
*/
|
|
370
|
+
async checkInternalNetworkConnectivity(containerId, mcpPort) {
|
|
371
|
+
const result = await this.docker.exec(containerId, ['socat', '-u', '/dev/null', `TCP:host.docker.internal:${mcpPort},connect-timeout=5`],
|
|
372
|
+
// Allow a small buffer above socat's 5s connect-timeout for docker exec/process startup overhead.
|
|
373
|
+
6_000);
|
|
374
|
+
if (result.exitCode !== 0) {
|
|
375
|
+
throw new Error(`Internal network connectivity check failed (exit=${result.exitCode}). ` +
|
|
376
|
+
`The container cannot reach host-side proxies via the socat sidecar on the --internal Docker network. ` +
|
|
377
|
+
`Check that the sidecar container is running and connected to the internal network.`);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
250
380
|
startEscalationWatcher() {
|
|
251
381
|
this.escalationPollInterval = setInterval(() => {
|
|
252
382
|
this.pollEscalationDirectory();
|
|
@@ -312,10 +442,15 @@ export class DockerAgentSession {
|
|
|
312
442
|
async ensureImage(image) {
|
|
313
443
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
314
444
|
const dockerDir = resolve(packageRoot, 'docker');
|
|
445
|
+
// On arm64 hosts (Apple Silicon), use the lightweight arm64-native Dockerfile
|
|
446
|
+
// instead of the amd64-only devcontainers/universal image.
|
|
447
|
+
const baseDockerfile = arch() === 'arm64' && existsSync(resolve(dockerDir, 'Dockerfile.base.arm64'))
|
|
448
|
+
? 'Dockerfile.base.arm64'
|
|
449
|
+
: 'Dockerfile.base';
|
|
315
450
|
// Build base image with CA cert baked in (if stale or missing)
|
|
316
451
|
const baseImage = 'ironcurtain-base:latest';
|
|
317
|
-
const baseBuildHash = this.computeBuildHash(dockerDir, [
|
|
318
|
-
const baseRebuilt = await this.ensureBaseImage(baseImage, dockerDir, baseBuildHash);
|
|
452
|
+
const baseBuildHash = this.computeBuildHash(dockerDir, [baseDockerfile]);
|
|
453
|
+
const baseRebuilt = await this.ensureBaseImage(baseImage, dockerDir, baseDockerfile, baseBuildHash);
|
|
319
454
|
// Build the agent-specific image (if stale, missing, or base was rebuilt)
|
|
320
455
|
const agentName = image.replace('ironcurtain-', '').replace(':latest', '');
|
|
321
456
|
const dockerfile = `Dockerfile.${agentName}`;
|
|
@@ -337,7 +472,7 @@ export class DockerAgentSession {
|
|
|
337
472
|
* Ensures the base image exists and is up-to-date.
|
|
338
473
|
* Returns true if the base image was (re)built.
|
|
339
474
|
*/
|
|
340
|
-
async ensureBaseImage(baseImage, dockerDir, buildHash) {
|
|
475
|
+
async ensureBaseImage(baseImage, dockerDir, dockerfile, buildHash) {
|
|
341
476
|
if (!(await this.isImageStale(baseImage, buildHash)))
|
|
342
477
|
return false;
|
|
343
478
|
logger.info('Building base Docker image (this may take a while on first run)...');
|
|
@@ -350,7 +485,7 @@ export class DockerAgentSession {
|
|
|
350
485
|
}
|
|
351
486
|
// Copy CA cert into build context
|
|
352
487
|
copyFileSync(this.ca.certPath, resolve(tmpContext, 'ironcurtain-ca-cert.pem'));
|
|
353
|
-
await this.docker.buildImage(baseImage, resolve(tmpContext,
|
|
488
|
+
await this.docker.buildImage(baseImage, resolve(tmpContext, dockerfile), tmpContext, {
|
|
354
489
|
'ironcurtain.build-hash': buildHash,
|
|
355
490
|
});
|
|
356
491
|
}
|