@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.
Files changed (101) hide show
  1. package/README.md +20 -1
  2. package/dist/cli.js +15 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/index.js +12 -1
  5. package/dist/config/index.js.map +1 -1
  6. package/dist/config/mcp-servers.json +3 -0
  7. package/dist/config/types.d.ts +2 -0
  8. package/dist/config/user-config.d.ts +11 -0
  9. package/dist/config/user-config.js +56 -2
  10. package/dist/config/user-config.js.map +1 -1
  11. package/dist/docker/adapters/claude-code.d.ts +4 -0
  12. package/dist/docker/adapters/claude-code.js +35 -21
  13. package/dist/docker/adapters/claude-code.js.map +1 -1
  14. package/dist/docker/agent-adapter.d.ts +6 -12
  15. package/dist/docker/audit-log-tailer.d.ts +4 -3
  16. package/dist/docker/audit-log-tailer.js +9 -7
  17. package/dist/docker/audit-log-tailer.js.map +1 -1
  18. package/dist/docker/code-mode-proxy.d.ts +34 -0
  19. package/dist/docker/code-mode-proxy.js +137 -0
  20. package/dist/docker/code-mode-proxy.js.map +1 -0
  21. package/dist/docker/docker-agent-session.d.ts +17 -5
  22. package/dist/docker/docker-agent-session.js +171 -36
  23. package/dist/docker/docker-agent-session.js.map +1 -1
  24. package/dist/docker/docker-manager.js +106 -15
  25. package/dist/docker/docker-manager.js.map +1 -1
  26. package/dist/docker/mitm-proxy.d.ts +7 -4
  27. package/dist/docker/mitm-proxy.js +43 -13
  28. package/dist/docker/mitm-proxy.js.map +1 -1
  29. package/dist/docker/orientation.d.ts +3 -2
  30. package/dist/docker/orientation.js +6 -3
  31. package/dist/docker/orientation.js.map +1 -1
  32. package/dist/docker/platform.d.ts +20 -0
  33. package/dist/docker/platform.js +24 -0
  34. package/dist/docker/platform.js.map +1 -0
  35. package/dist/docker/types.d.ts +59 -7
  36. package/dist/sandbox/index.d.ts +25 -1
  37. package/dist/sandbox/index.js +78 -9
  38. package/dist/sandbox/index.js.map +1 -1
  39. package/dist/servers/fetch-server.d.ts +6 -1
  40. package/dist/servers/fetch-server.js +24 -12
  41. package/dist/servers/fetch-server.js.map +1 -1
  42. package/dist/session/agent-session.js +9 -5
  43. package/dist/session/agent-session.js.map +1 -1
  44. package/dist/session/index.js +18 -36
  45. package/dist/session/index.js.map +1 -1
  46. package/dist/session/preflight.js +6 -0
  47. package/dist/session/preflight.js.map +1 -1
  48. package/dist/session/prompts.d.ts +9 -3
  49. package/dist/session/prompts.js +28 -10
  50. package/dist/session/prompts.js.map +1 -1
  51. package/dist/signal/bot-command.d.ts +13 -0
  52. package/dist/signal/bot-command.js +54 -0
  53. package/dist/signal/bot-command.js.map +1 -0
  54. package/dist/signal/format.d.ts +31 -0
  55. package/dist/signal/format.js +101 -0
  56. package/dist/signal/format.js.map +1 -0
  57. package/dist/signal/index.d.ts +16 -0
  58. package/dist/signal/index.js +13 -0
  59. package/dist/signal/index.js.map +1 -0
  60. package/dist/signal/markdown-to-signal.d.ts +29 -0
  61. package/dist/signal/markdown-to-signal.js +154 -0
  62. package/dist/signal/markdown-to-signal.js.map +1 -0
  63. package/dist/signal/setup-signal.d.ts +48 -0
  64. package/dist/signal/setup-signal.js +443 -0
  65. package/dist/signal/setup-signal.js.map +1 -0
  66. package/dist/signal/signal-bot-daemon.d.ts +142 -0
  67. package/dist/signal/signal-bot-daemon.js +504 -0
  68. package/dist/signal/signal-bot-daemon.js.map +1 -0
  69. package/dist/signal/signal-config.d.ts +64 -0
  70. package/dist/signal/signal-config.js +53 -0
  71. package/dist/signal/signal-config.js.map +1 -0
  72. package/dist/signal/signal-container.d.ts +37 -0
  73. package/dist/signal/signal-container.js +112 -0
  74. package/dist/signal/signal-container.js.map +1 -0
  75. package/dist/signal/signal-transport.d.ts +34 -0
  76. package/dist/signal/signal-transport.js +70 -0
  77. package/dist/signal/signal-transport.js.map +1 -0
  78. package/dist/trusted-process/mcp-proxy-server.d.ts +111 -1
  79. package/dist/trusted-process/mcp-proxy-server.js +319 -247
  80. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  81. package/dist/trusted-process/path-utils.d.ts +5 -1
  82. package/dist/trusted-process/path-utils.js +47 -4
  83. package/dist/trusted-process/path-utils.js.map +1 -1
  84. package/dist/trusted-process/policy-engine.d.ts +3 -1
  85. package/dist/trusted-process/policy-engine.js +17 -6
  86. package/dist/trusted-process/policy-engine.js.map +1 -1
  87. package/dist/trusted-process/sandbox-integration.d.ts +11 -4
  88. package/dist/trusted-process/sandbox-integration.js +13 -7
  89. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  90. package/dist/trusted-process/tcp-server-transport.d.ts +43 -0
  91. package/dist/trusted-process/tcp-server-transport.js +119 -0
  92. package/dist/trusted-process/tcp-server-transport.js.map +1 -0
  93. package/dist/types/argument-roles.d.ts +7 -0
  94. package/dist/types/argument-roles.js +30 -5
  95. package/dist/types/argument-roles.js.map +1 -1
  96. package/docker/Dockerfile.base.arm64 +23 -0
  97. package/package.json +9 -4
  98. package/src/config/mcp-servers.json +3 -0
  99. package/dist/docker/managed-proxy.d.ts +0 -27
  100. package/dist/docker/managed-proxy.js +0 -175
  101. 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.watch() for change notification and
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, watch } from 'node:fs';
6
+ import { openSync, readSync, fstatSync, closeSync, watchFile, unwatchFile } from 'node:fs';
7
7
  export class AuditLogTailer {
8
8
  offset = 0;
9
- watcher = null;
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.watcher = watch(this.auditLogPath, () => this.readNewEntries());
17
+ this.watching = true;
18
+ watchFile(this.auditLogPath, { interval: 100 }, () => this.readNewEntries());
18
19
  }
19
20
  stop() {
20
- if (this.watcher) {
21
- this.watcher.close();
22
- this.watcher = null;
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,KAAK,EAAkB,MAAM,SAAS,CAAC;AAI1F,MAAM,OAAO,cAAc;IACjB,MAAM,GAAG,CAAC,CAAC;IACX,OAAO,GAAqB,IAAI,CAAC;IACxB,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,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC,CAAC;IACvE,CAAC;IAED,IAAI;QACF,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;IAEO,cAAc;QACpB,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"}
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 { ManagedProxy } from './managed-proxy.js';
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: ManagedProxy;
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 MCP proxy (UDS)
70
- * 2. Start MITM proxy (UDS)
71
- * 3. Query proxy for available tools
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 MCP proxy (UDS)
74
- * 2. Start MITM proxy (UDS)
75
- * 3. Query proxy for available tools
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 MCP proxy
91
+ // 1. Start Code Mode proxy
86
92
  await this.proxy.start();
87
- logger.info(`MCP proxy listening on ${this.proxy.socketPath}`);
88
- // 2. Start MITM proxy (UDS)
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
- logger.info(`MITM proxy listening on ${mitmAddr.socketPath}`);
91
- // 3. Query proxy for available tools
92
- const tools = await this.proxy.listTools();
93
- logger.info(`Available tools: ${tools.map((t) => t.name).join(', ')}`);
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
- const { systemPrompt } = prepareSession(this.adapter, tools, this.sessionDir, this.config, this.sandboxDir);
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 (--network=none + UDS for MITM proxy)
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
- this.containerId = await this.docker.create({
109
- image,
110
- name: `ironcurtain-${shortId}`,
111
- network: 'none',
112
- mounts: [
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
- { source: mitmAddr.socketPath, target: '/run/ironcurtain/mitm-proxy.sock', readonly: true },
117
- ],
118
- env,
119
- command: ['sleep', 'infinity'],
120
- sessionLabel: this.sessionId,
121
- resources: { memoryMb: 4096, cpus: 2 },
122
- });
123
- await this.docker.start(this.containerId);
124
- logger.info(`Container started: ${this.containerId.substring(0, 12)}`);
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, ['Dockerfile.base']);
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, 'Dockerfile.base'), tmpContext, {
488
+ await this.docker.buildImage(baseImage, resolve(tmpContext, dockerfile), tmpContext, {
354
489
  'ironcurtain.build-hash': buildHash,
355
490
  });
356
491
  }