@provos/ironcurtain 0.4.0 → 0.5.0

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 (161) hide show
  1. package/README.md +219 -248
  2. package/dist/cli.js +10 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/config-command.js +144 -6
  5. package/dist/config/config-command.js.map +1 -1
  6. package/dist/config/constitution-user-base.md +2 -0
  7. package/dist/config/first-start.js +77 -12
  8. package/dist/config/first-start.js.map +1 -1
  9. package/dist/config/generated/compiled-policy.json +199 -72
  10. package/dist/config/generated/tool-annotations.json +1018 -36
  11. package/dist/config/index.js +6 -0
  12. package/dist/config/index.js.map +1 -1
  13. package/dist/config/mcp-servers.json +13 -0
  14. package/dist/config/paths.d.ts +31 -0
  15. package/dist/config/paths.js +52 -2
  16. package/dist/config/paths.js.map +1 -1
  17. package/dist/config/types.d.ts +10 -0
  18. package/dist/config/user-config.d.ts +21 -3
  19. package/dist/config/user-config.js +34 -3
  20. package/dist/config/user-config.js.map +1 -1
  21. package/dist/docker/adapters/claude-code.d.ts +4 -0
  22. package/dist/docker/adapters/claude-code.js +96 -37
  23. package/dist/docker/adapters/claude-code.js.map +1 -1
  24. package/dist/docker/agent-adapter.d.ts +22 -13
  25. package/dist/docker/code-mode-proxy.d.ts +34 -0
  26. package/dist/docker/code-mode-proxy.js +137 -0
  27. package/dist/docker/code-mode-proxy.js.map +1 -0
  28. package/dist/docker/docker-agent-session.d.ts +22 -14
  29. package/dist/docker/docker-agent-session.js +144 -117
  30. package/dist/docker/docker-agent-session.js.map +1 -1
  31. package/dist/docker/docker-infrastructure.d.ts +51 -0
  32. package/dist/docker/docker-infrastructure.js +261 -0
  33. package/dist/docker/docker-infrastructure.js.map +1 -0
  34. package/dist/docker/docker-manager.js +27 -0
  35. package/dist/docker/docker-manager.js.map +1 -1
  36. package/dist/docker/mitm-proxy.js +4 -0
  37. package/dist/docker/mitm-proxy.js.map +1 -1
  38. package/dist/docker/oauth-credentials.d.ts +81 -0
  39. package/dist/docker/oauth-credentials.js +178 -0
  40. package/dist/docker/oauth-credentials.js.map +1 -0
  41. package/dist/docker/orientation.d.ts +3 -2
  42. package/dist/docker/orientation.js +4 -4
  43. package/dist/docker/orientation.js.map +1 -1
  44. package/dist/docker/provider-config.d.ts +3 -0
  45. package/dist/docker/provider-config.js +28 -0
  46. package/dist/docker/provider-config.js.map +1 -1
  47. package/dist/docker/pty-session.d.ts +27 -0
  48. package/dist/docker/pty-session.js +420 -0
  49. package/dist/docker/pty-session.js.map +1 -0
  50. package/dist/docker/pty-types.d.ts +24 -0
  51. package/dist/docker/pty-types.js +12 -0
  52. package/dist/docker/pty-types.js.map +1 -0
  53. package/dist/docker/types.d.ts +18 -2
  54. package/dist/escalation/escalation-watcher.d.ts +44 -0
  55. package/dist/escalation/escalation-watcher.js +92 -0
  56. package/dist/escalation/escalation-watcher.js.map +1 -0
  57. package/dist/escalation/listener-command.d.ts +10 -0
  58. package/dist/escalation/listener-command.js +407 -0
  59. package/dist/escalation/listener-command.js.map +1 -0
  60. package/dist/escalation/listener-state.d.ts +49 -0
  61. package/dist/escalation/listener-state.js +104 -0
  62. package/dist/escalation/listener-state.js.map +1 -0
  63. package/dist/escalation/session-registry.d.ts +17 -0
  64. package/dist/escalation/session-registry.js +98 -0
  65. package/dist/escalation/session-registry.js.map +1 -0
  66. package/dist/index.js +15 -0
  67. package/dist/index.js.map +1 -1
  68. package/dist/logger.js +2 -3
  69. package/dist/logger.js.map +1 -1
  70. package/dist/pipeline/annotate.js +35 -15
  71. package/dist/pipeline/annotate.js.map +1 -1
  72. package/dist/pipeline/constitution-customizer.d.ts +2 -1
  73. package/dist/pipeline/constitution-customizer.js +36 -3
  74. package/dist/pipeline/constitution-customizer.js.map +1 -1
  75. package/dist/pipeline/github-identity.d.ts +38 -0
  76. package/dist/pipeline/github-identity.js +64 -0
  77. package/dist/pipeline/github-identity.js.map +1 -0
  78. package/dist/pipeline/handwritten-scenarios.js +34 -0
  79. package/dist/pipeline/handwritten-scenarios.js.map +1 -1
  80. package/dist/sandbox/index.d.ts +25 -1
  81. package/dist/sandbox/index.js +90 -12
  82. package/dist/sandbox/index.js.map +1 -1
  83. package/dist/servers/fetch-server.d.ts +6 -1
  84. package/dist/servers/fetch-server.js +26 -14
  85. package/dist/servers/fetch-server.js.map +1 -1
  86. package/dist/session/agent-session.js +9 -5
  87. package/dist/session/agent-session.js.map +1 -1
  88. package/dist/session/base-transport.d.ts +19 -0
  89. package/dist/session/base-transport.js +36 -0
  90. package/dist/session/base-transport.js.map +1 -0
  91. package/dist/session/cli-transport.d.ts +3 -3
  92. package/dist/session/cli-transport.js +7 -4
  93. package/dist/session/cli-transport.js.map +1 -1
  94. package/dist/session/index.js +24 -106
  95. package/dist/session/index.js.map +1 -1
  96. package/dist/session/interaction-log.d.ts +13 -0
  97. package/dist/session/interaction-log.js +28 -0
  98. package/dist/session/interaction-log.js.map +1 -0
  99. package/dist/session/preflight.d.ts +3 -0
  100. package/dist/session/preflight.js +45 -18
  101. package/dist/session/preflight.js.map +1 -1
  102. package/dist/session/prompts.d.ts +9 -3
  103. package/dist/session/prompts.js +28 -10
  104. package/dist/session/prompts.js.map +1 -1
  105. package/dist/session/types.d.ts +2 -0
  106. package/dist/signal/bot-command.js +5 -0
  107. package/dist/signal/bot-command.js.map +1 -1
  108. package/dist/signal/format.d.ts +21 -1
  109. package/dist/signal/format.js +41 -3
  110. package/dist/signal/format.js.map +1 -1
  111. package/dist/signal/signal-bot-daemon.d.ts +53 -18
  112. package/dist/signal/signal-bot-daemon.js +344 -106
  113. package/dist/signal/signal-bot-daemon.js.map +1 -1
  114. package/dist/signal/signal-config.d.ts +4 -0
  115. package/dist/signal/signal-config.js +2 -0
  116. package/dist/signal/signal-config.js.map +1 -1
  117. package/dist/signal/signal-transport.d.ts +13 -7
  118. package/dist/signal/signal-transport.js +36 -12
  119. package/dist/signal/signal-transport.js.map +1 -1
  120. package/dist/trusted-process/audit-log.d.ts +6 -1
  121. package/dist/trusted-process/audit-log.js +22 -2
  122. package/dist/trusted-process/audit-log.js.map +1 -1
  123. package/dist/trusted-process/audit-redactor.d.ts +25 -0
  124. package/dist/trusted-process/audit-redactor.js +121 -0
  125. package/dist/trusted-process/audit-redactor.js.map +1 -0
  126. package/dist/trusted-process/escalation.d.ts +1 -1
  127. package/dist/trusted-process/escalation.js +6 -1
  128. package/dist/trusted-process/escalation.js.map +1 -1
  129. package/dist/trusted-process/index.d.ts +2 -1
  130. package/dist/trusted-process/index.js +24 -6
  131. package/dist/trusted-process/index.js.map +1 -1
  132. package/dist/trusted-process/mcp-client-manager.js +14 -2
  133. package/dist/trusted-process/mcp-client-manager.js.map +1 -1
  134. package/dist/trusted-process/mcp-error-utils.d.ts +11 -0
  135. package/dist/trusted-process/mcp-error-utils.js +50 -0
  136. package/dist/trusted-process/mcp-error-utils.js.map +1 -0
  137. package/dist/trusted-process/mcp-proxy-server.d.ts +115 -1
  138. package/dist/trusted-process/mcp-proxy-server.js +318 -267
  139. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  140. package/dist/trusted-process/policy-engine.d.ts +3 -1
  141. package/dist/trusted-process/policy-engine.js +17 -6
  142. package/dist/trusted-process/policy-engine.js.map +1 -1
  143. package/dist/trusted-process/sandbox-integration.d.ts +16 -4
  144. package/dist/trusted-process/sandbox-integration.js +14 -8
  145. package/dist/trusted-process/sandbox-integration.js.map +1 -1
  146. package/dist/trusted-process/server-context.d.ts +21 -0
  147. package/dist/trusted-process/server-context.js +42 -0
  148. package/dist/trusted-process/server-context.js.map +1 -0
  149. package/dist/types/argument-roles.d.ts +12 -2
  150. package/dist/types/argument-roles.js +37 -4
  151. package/dist/types/argument-roles.js.map +1 -1
  152. package/docker/Dockerfile.claude-code +8 -3
  153. package/docker/entrypoint-claude-code.sh +64 -4
  154. package/package.json +19 -9
  155. package/src/config/constitution-user-base.md +2 -0
  156. package/src/config/generated/compiled-policy.json +199 -72
  157. package/src/config/generated/tool-annotations.json +1018 -36
  158. package/src/config/mcp-servers.json +13 -0
  159. package/dist/docker/managed-proxy.d.ts +0 -31
  160. package/dist/docker/managed-proxy.js +0 -265
  161. package/dist/docker/managed-proxy.js.map +0 -1
@@ -0,0 +1,420 @@
1
+ /**
2
+ * PTY session module -- orchestrates a Docker session where the user's
3
+ * terminal is attached directly to Claude Code's PTY inside the container.
4
+ *
5
+ * Instead of the batch-oriented DockerAgentSession (docker exec per turn),
6
+ * this module provides a native interactive experience by bridging the
7
+ * terminal via a Node.js PTY proxy to a socat-managed PTY inside the container.
8
+ *
9
+ * Architecture:
10
+ * User terminal -> Node.js PTY proxy -> UDS/TCP -> socat (container)
11
+ * -> Claude Code (interactive, with PTY)
12
+ * -> Code Mode Proxy (MCP)
13
+ * -> mcp-proxy-server (PolicyEngine + Audit)
14
+ */
15
+ import { createConnection } from 'node:net';
16
+ import { execFile } from 'node:child_process';
17
+ import { mkdirSync, writeFileSync, unlinkSync } from 'node:fs';
18
+ import { resolve } from 'node:path';
19
+ import chalk from 'chalk';
20
+ import ora from 'ora';
21
+ import { createSessionId } from '../session/types.js';
22
+ import { PTY_SOCK_NAME, DEFAULT_PTY_PORT } from './pty-types.js';
23
+ import { createEscalationWatcher, atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
24
+ import { getSessionDir, getSessionSandboxDir, getSessionEscalationDir, getSessionAuditLogPath, getSessionLogPath, getSessionLlmLogPath, getPtyRegistryDir, } from '../config/paths.js';
25
+ import * as logger from '../logger.js';
26
+ /** Maximum time to wait for the PTY socket to appear (ms). */
27
+ const PTY_READINESS_TIMEOUT_MS = 30_000;
28
+ /** Poll interval when waiting for PTY socket (ms). */
29
+ const PTY_READINESS_POLL_MS = 200;
30
+ /**
31
+ * Runs a PTY session: starts proxies, launches container with PTY-enabled
32
+ * Claude Code, attaches the terminal, and blocks until the session ends.
33
+ */
34
+ export async function runPtySession(options) {
35
+ const { prepareDockerInfrastructure } = await import('./docker-infrastructure.js');
36
+ const sessionId = createSessionId();
37
+ const sessionDir = getSessionDir(sessionId);
38
+ const sandboxDir = getSessionSandboxDir(sessionId);
39
+ const escalationDir = getSessionEscalationDir(sessionId);
40
+ const auditLogPath = getSessionAuditLogPath(sessionId);
41
+ const socketsDir = resolve(sessionDir, 'sockets');
42
+ mkdirSync(sandboxDir, { recursive: true, mode: 0o700 });
43
+ mkdirSync(escalationDir, { recursive: true, mode: 0o700 });
44
+ mkdirSync(socketsDir, { recursive: true, mode: 0o700 });
45
+ // Set up session logging
46
+ const sessionLogPath = getSessionLogPath(sessionId);
47
+ const llmLogPath = getSessionLlmLogPath(sessionId);
48
+ logger.setup({ logFilePath: sessionLogPath });
49
+ logger.info(`PTY session ${sessionId} starting`);
50
+ logger.info(`Sandbox: ${sandboxDir}`);
51
+ logger.info(`Escalation dir: ${escalationDir}`);
52
+ logger.info(`LLM log: ${llmLogPath}`);
53
+ // Patch config for this session
54
+ const sessionConfig = {
55
+ ...options.config,
56
+ allowedDirectory: sandboxDir,
57
+ auditLogPath,
58
+ escalationDir,
59
+ sessionLogPath,
60
+ llmLogPath,
61
+ mcpServers: JSON.parse(JSON.stringify(options.config.mcpServers)),
62
+ };
63
+ const initSpinner = ora({
64
+ text: `Initializing PTY session (${options.mode.agent})...`,
65
+ stream: process.stderr,
66
+ discardStdin: false,
67
+ }).start();
68
+ let containerId = null;
69
+ let sidecarContainerId = null;
70
+ let escalationFileWatcher = null;
71
+ let registrationPath = null;
72
+ let shutdownSpinner = null;
73
+ // Terminal safety: ensure raw mode is restored on any exit
74
+ const restoreTerminal = () => {
75
+ try {
76
+ if (process.stdin.isTTY && process.stdin.isRaw) {
77
+ process.stdin.setRawMode(false);
78
+ process.stdin.pause();
79
+ }
80
+ }
81
+ catch {
82
+ /* best effort */
83
+ }
84
+ };
85
+ process.on('exit', restoreTerminal);
86
+ // SIGTERM triggers graceful shutdown by aborting the PTY connection.
87
+ // This causes attachPty() to resolve, which then falls through to the
88
+ // finally block for full async cleanup (containers, proxies, files).
89
+ const shutdownController = new AbortController();
90
+ const handleSigterm = () => {
91
+ shutdownController.abort();
92
+ };
93
+ process.on('SIGTERM', handleSigterm);
94
+ // Infra variables set inside try, used in finally for cleanup
95
+ let proxy = null;
96
+ let mitmProxy = null;
97
+ let docker = null;
98
+ let useTcp = false;
99
+ try {
100
+ const infra = await prepareDockerInfrastructure(sessionConfig, options.mode, sessionDir, sandboxDir, escalationDir, auditLogPath, sessionId);
101
+ ({ docker, proxy, mitmProxy, useTcp } = infra);
102
+ const { adapter, fakeKeys, orientationDir, systemPrompt, image, mitmAddr } = infra;
103
+ // Validate adapter supports PTY mode
104
+ if (!adapter.buildPtyCommand) {
105
+ throw new Error(`Agent ${adapter.id} does not support PTY mode.`);
106
+ }
107
+ // Write system prompt to file for shell-injection-safe PTY command
108
+ writeFileSync(resolve(orientationDir, 'system-prompt.txt'), systemPrompt);
109
+ // Determine PTY connection target
110
+ const ptySockPath = useTcp ? undefined : `/run/ironcurtain/${PTY_SOCK_NAME}`;
111
+ const ptyPort = useTcp ? DEFAULT_PTY_PORT : undefined;
112
+ // Build the PTY command
113
+ const ptyCommand = adapter.buildPtyCommand(systemPrompt, ptySockPath, ptyPort);
114
+ // Build container configuration
115
+ const shortId = sessionId.substring(0, 12);
116
+ const { INTERNAL_NETWORK_NAME, INTERNAL_NETWORK_SUBNET, INTERNAL_NETWORK_GATEWAY } = await import('./platform.js');
117
+ const { quote } = await import('shell-quote');
118
+ let env;
119
+ let network;
120
+ let mounts;
121
+ let extraHosts;
122
+ if (useTcp && proxy.port !== undefined && mitmAddr.port !== undefined) {
123
+ // macOS TCP mode
124
+ const mcpPort = proxy.port;
125
+ const mitmPort = mitmAddr.port;
126
+ env = {
127
+ ...adapter.buildEnv(sessionConfig, fakeKeys),
128
+ HTTPS_PROXY: `http://host.docker.internal:${mitmPort}`,
129
+ HTTP_PROXY: `http://host.docker.internal:${mitmPort}`,
130
+ };
131
+ await docker.createNetwork(INTERNAL_NETWORK_NAME, {
132
+ internal: true,
133
+ subnet: INTERNAL_NETWORK_SUBNET,
134
+ gateway: INTERNAL_NETWORK_GATEWAY,
135
+ });
136
+ network = INTERNAL_NETWORK_NAME;
137
+ const socatImage = 'alpine/socat';
138
+ if (!(await docker.imageExists(socatImage))) {
139
+ logger.info(`Pulling ${socatImage}...`);
140
+ await docker.pullImage(socatImage);
141
+ }
142
+ // Create socat sidecar with PTY port forwarding
143
+ const sidecarName = `ironcurtain-sidecar-${shortId}`;
144
+ const ptyPortNum = ptyPort ?? DEFAULT_PTY_PORT;
145
+ sidecarContainerId = await docker.create({
146
+ image: socatImage,
147
+ name: sidecarName,
148
+ network: 'bridge',
149
+ mounts: [],
150
+ env: {},
151
+ entrypoint: '/bin/sh',
152
+ command: [
153
+ '-c',
154
+ quote(['socat', `TCP-LISTEN:${mcpPort},fork,reuseaddr`, `TCP:host.docker.internal:${mcpPort}`]) +
155
+ ' & ' +
156
+ quote(['socat', `TCP-LISTEN:${mitmPort},fork,reuseaddr`, `TCP:host.docker.internal:${mitmPort}`]) +
157
+ ' & ' +
158
+ quote(['socat', `TCP-LISTEN:${ptyPortNum},fork,reuseaddr`, `TCP:host.docker.internal:${ptyPortNum}`]) +
159
+ ' & wait',
160
+ ],
161
+ });
162
+ await docker.start(sidecarContainerId);
163
+ await docker.connectNetwork(INTERNAL_NETWORK_NAME, sidecarContainerId);
164
+ const sidecarIp = await docker.getContainerIp(sidecarContainerId, INTERNAL_NETWORK_NAME);
165
+ extraHosts = [`host.docker.internal:${sidecarIp}`];
166
+ mounts = [
167
+ { source: sandboxDir, target: '/workspace', readonly: false },
168
+ { source: orientationDir, target: '/etc/ironcurtain', readonly: true },
169
+ ];
170
+ }
171
+ else {
172
+ // Linux UDS mode
173
+ env = {
174
+ ...adapter.buildEnv(sessionConfig, fakeKeys),
175
+ HTTPS_PROXY: 'http://127.0.0.1:18080',
176
+ HTTP_PROXY: 'http://127.0.0.1:18080',
177
+ };
178
+ network = 'none';
179
+ mounts = [
180
+ { source: sandboxDir, target: '/workspace', readonly: false },
181
+ { source: socketsDir, target: '/run/ironcurtain', readonly: false },
182
+ { source: orientationDir, target: '/etc/ironcurtain', readonly: true },
183
+ ];
184
+ }
185
+ // Create and start container with PTY command and TTY
186
+ containerId = await docker.create({
187
+ image,
188
+ name: `ironcurtain-pty-${shortId}`,
189
+ network,
190
+ mounts,
191
+ env,
192
+ command: ptyCommand,
193
+ sessionLabel: sessionId,
194
+ resources: { memoryMb: 4096, cpus: 2 },
195
+ extraHosts,
196
+ tty: true,
197
+ });
198
+ await docker.start(containerId);
199
+ logger.info(`PTY container started: ${containerId.substring(0, 12)}`);
200
+ // Write session registration for the escalation listener
201
+ registrationPath = writeRegistration(sessionId, escalationDir, adapter.displayName);
202
+ // Start escalation file watcher (emits BEL to alert user)
203
+ escalationFileWatcher = createEscalationWatcher(escalationDir, {
204
+ onEscalation: () => {
205
+ process.stderr.write('\x07'); // BEL character
206
+ },
207
+ onEscalationExpired: () => { },
208
+ });
209
+ escalationFileWatcher.start();
210
+ // Wait for PTY socket readiness
211
+ const ptyTarget = useTcp
212
+ ? { host: 'localhost', port: ptyPort ?? DEFAULT_PTY_PORT }
213
+ : resolve(socketsDir, PTY_SOCK_NAME);
214
+ await waitForPtyReady(ptyTarget);
215
+ initSpinner.succeed(chalk.dim('PTY session ready'));
216
+ process.stderr.write('\n');
217
+ // Attach terminal via Node.js PTY proxy
218
+ const exitCode = await attachPty({
219
+ target: ptyTarget,
220
+ containerId,
221
+ signal: shutdownController.signal,
222
+ });
223
+ // PTY disconnected -- restore terminal and show shutdown progress
224
+ restoreTerminal();
225
+ process.stderr.write('\n');
226
+ shutdownSpinner = ora({
227
+ text: 'Shutting down PTY session...',
228
+ stream: process.stderr,
229
+ discardStdin: false,
230
+ }).start();
231
+ if (exitCode !== 0) {
232
+ process.stderr.write(chalk.yellow(`PTY session exited with code ${exitCode}\n`));
233
+ }
234
+ }
235
+ finally {
236
+ // Stop spinner if still running (e.g. error during setup)
237
+ if (initSpinner.isSpinning) {
238
+ initSpinner.fail(chalk.red('PTY session failed'));
239
+ }
240
+ restoreTerminal();
241
+ process.off('exit', restoreTerminal);
242
+ process.off('SIGTERM', handleSigterm);
243
+ // Stop escalation watcher
244
+ escalationFileWatcher?.stop();
245
+ // Delete registration file
246
+ if (registrationPath) {
247
+ try {
248
+ unlinkSync(registrationPath);
249
+ }
250
+ catch {
251
+ /* best effort */
252
+ }
253
+ }
254
+ // Stop and remove containers
255
+ if (docker && containerId) {
256
+ await docker.stop(containerId).catch(() => { });
257
+ await docker.remove(containerId).catch(() => { });
258
+ }
259
+ if (docker && sidecarContainerId) {
260
+ await docker.stop(sidecarContainerId).catch(() => { });
261
+ await docker.remove(sidecarContainerId).catch(() => { });
262
+ }
263
+ // Remove internal network if used
264
+ if (docker && useTcp) {
265
+ const { INTERNAL_NETWORK_NAME } = await import('./platform.js');
266
+ await docker.removeNetwork(INTERNAL_NETWORK_NAME).catch(() => { });
267
+ }
268
+ // Stop proxies
269
+ await mitmProxy?.stop().catch(() => { });
270
+ await proxy?.stop().catch(() => { });
271
+ logger.info(`PTY session ${sessionId} ended`);
272
+ logger.teardown();
273
+ // shutdownSpinner is declared inside try but accessible here via closure
274
+ shutdownSpinner?.succeed(chalk.dim('PTY session ended'));
275
+ }
276
+ }
277
+ /** Creates a net.Socket connection to the PTY target (UDS or TCP). */
278
+ function connectToTarget(target) {
279
+ if (typeof target === 'string') {
280
+ return createConnection({ path: target });
281
+ }
282
+ return createConnection({ host: target.host, port: target.port });
283
+ }
284
+ /**
285
+ * Attaches the user's terminal to the container PTY via a Node.js socket.
286
+ * Returns a promise that resolves with 0 on normal close, 1 on error.
287
+ */
288
+ function attachPty(options) {
289
+ const conn = connectToTarget(options.target);
290
+ const { stdin, stdout } = process;
291
+ return new Promise((resolvePromise) => {
292
+ let resolved = false;
293
+ const settle = (code) => {
294
+ if (resolved)
295
+ return;
296
+ resolved = true;
297
+ resolvePromise(code);
298
+ };
299
+ conn.once('connect', () => {
300
+ // Put terminal in raw mode
301
+ if (stdin.isTTY) {
302
+ stdin.setRawMode(true);
303
+ }
304
+ stdin.resume();
305
+ // SIGWINCH: forward terminal resize to container PTY
306
+ const onResize = () => {
307
+ const { columns, rows } = stdout;
308
+ if (columns && rows) {
309
+ execFile('docker', ['exec', options.containerId, '/etc/ironcurtain/resize-pty.sh', String(columns), String(rows)], { timeout: 5000 }, () => {
310
+ /* best effort */
311
+ });
312
+ }
313
+ };
314
+ stdout.on('resize', onResize);
315
+ // Send initial size
316
+ onResize();
317
+ // Host -> Container
318
+ // Ctrl-\ (0x1c) is intercepted as an emergency exit since raw mode
319
+ // disables normal signal generation for SIGQUIT.
320
+ const CTRL_BACKSLASH = 0x1c;
321
+ const onData = (data) => {
322
+ if (data.length === 1 && data[0] === CTRL_BACKSLASH) {
323
+ cleanup();
324
+ conn.destroy();
325
+ settle(0);
326
+ return;
327
+ }
328
+ conn.write(data);
329
+ };
330
+ stdin.on('data', onData);
331
+ // Container -> Host (untrusted output, displayed directly)
332
+ conn.pipe(stdout);
333
+ // Use function declarations (hoisted) so cleanup and onAbort can
334
+ // reference each other without temporal dead zone issues.
335
+ function cleanup() {
336
+ stdout.removeListener('resize', onResize);
337
+ stdin.removeListener('data', onData);
338
+ conn.unpipe(stdout);
339
+ stdin.pause();
340
+ options.signal?.removeEventListener('abort', onAbort);
341
+ }
342
+ function onAbort() {
343
+ cleanup();
344
+ conn.destroy();
345
+ settle(0);
346
+ }
347
+ if (options.signal?.aborted) {
348
+ onAbort();
349
+ return;
350
+ }
351
+ options.signal?.addEventListener('abort', onAbort, { once: true });
352
+ conn.once('close', () => {
353
+ cleanup();
354
+ settle(0);
355
+ });
356
+ conn.once('error', () => {
357
+ cleanup();
358
+ settle(1);
359
+ });
360
+ });
361
+ conn.once('error', () => {
362
+ settle(1);
363
+ });
364
+ });
365
+ }
366
+ // --- Readiness polling ---
367
+ /**
368
+ * Waits for the PTY socket/port to become connectable.
369
+ */
370
+ async function waitForPtyReady(target) {
371
+ const deadline = Date.now() + PTY_READINESS_TIMEOUT_MS;
372
+ while (Date.now() < deadline) {
373
+ const connected = await tryConnect(target);
374
+ if (connected)
375
+ return;
376
+ await new Promise((r) => setTimeout(r, PTY_READINESS_POLL_MS));
377
+ }
378
+ throw new Error(`PTY socket did not become ready within ${PTY_READINESS_TIMEOUT_MS / 1000}s`);
379
+ }
380
+ /**
381
+ * Tries to connect to a target. Returns true if the connection succeeds.
382
+ */
383
+ function tryConnect(target) {
384
+ return new Promise((resolve) => {
385
+ const conn = connectToTarget(target);
386
+ const timer = setTimeout(() => {
387
+ conn.destroy();
388
+ resolve(false);
389
+ }, 1000);
390
+ conn.on('connect', () => {
391
+ clearTimeout(timer);
392
+ conn.destroy();
393
+ resolve(true);
394
+ });
395
+ conn.on('error', () => {
396
+ clearTimeout(timer);
397
+ resolve(false);
398
+ });
399
+ });
400
+ }
401
+ // --- Registration ---
402
+ /**
403
+ * Writes a PTY session registration file to the registry directory.
404
+ * Returns the absolute path of the registration file.
405
+ */
406
+ function writeRegistration(sessionId, escalationDir, adapterDisplayName) {
407
+ const registryDir = getPtyRegistryDir();
408
+ mkdirSync(registryDir, { recursive: true, mode: 0o700 });
409
+ const registration = {
410
+ sessionId,
411
+ escalationDir,
412
+ label: `${adapterDisplayName} (interactive)`,
413
+ startedAt: new Date().toISOString(),
414
+ pid: process.pid,
415
+ };
416
+ const registrationPath = resolve(registryDir, `session-${sessionId}.json`);
417
+ atomicWriteJsonSync(registrationPath, registration);
418
+ return registrationPath;
419
+ }
420
+ //# sourceMappingURL=pty-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty-session.js","sourceRoot":"","sources":["../../src/docker/pty-session.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Types for PTY session management and the escalation listener.
3
+ */
4
+ /** Registration file written by PTY sessions for the escalation listener. */
5
+ export interface PtySessionRegistration {
6
+ /** Unique session identifier (SessionId). */
7
+ readonly sessionId: string;
8
+ /** Absolute path to the session's escalation directory. */
9
+ readonly escalationDir: string;
10
+ /** Human-readable label for TUI display. */
11
+ readonly label: string;
12
+ /** ISO 8601 timestamp when the session started. */
13
+ readonly startedAt: string;
14
+ /** PID of the ironcurtain process managing this session. */
15
+ readonly pid: number;
16
+ }
17
+ /** Well-known directory for PTY session registration files. */
18
+ export declare const PTY_REGISTRY_DIR_NAME = "pty-registry";
19
+ /** Lock file name for single-instance enforcement. */
20
+ export declare const LISTENER_LOCK_FILE = "escalation-listener.lock";
21
+ /** PTY socket filename (Linux UDS mode). */
22
+ export declare const PTY_SOCK_NAME = "pty.sock";
23
+ /** Default PTY port inside the container (macOS TCP mode only). */
24
+ export declare const DEFAULT_PTY_PORT = 19000;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Types for PTY session management and the escalation listener.
3
+ */
4
+ /** Well-known directory for PTY session registration files. */
5
+ export const PTY_REGISTRY_DIR_NAME = 'pty-registry';
6
+ /** Lock file name for single-instance enforcement. */
7
+ export const LISTENER_LOCK_FILE = 'escalation-listener.lock';
8
+ /** PTY socket filename (Linux UDS mode). */
9
+ export const PTY_SOCK_NAME = 'pty.sock';
10
+ /** Default PTY port inside the container (macOS TCP mode only). */
11
+ export const DEFAULT_PTY_PORT = 19000;
12
+ //# sourceMappingURL=pty-types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pty-types.js","sourceRoot":"","sources":["../../src/docker/pty-types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAgBH,+DAA+D;AAC/D,MAAM,CAAC,MAAM,qBAAqB,GAAG,cAAc,CAAC;AAEpD,sDAAsD;AACtD,MAAM,CAAC,MAAM,kBAAkB,GAAG,0BAA0B,CAAC;AAE7D,4CAA4C;AAC5C,MAAM,CAAC,MAAM,aAAa,GAAG,UAAU,CAAC;AAExC,mEAAmE;AACnE,MAAM,CAAC,MAAM,gBAAgB,GAAG,KAAK,CAAC"}
@@ -13,11 +13,17 @@ export interface DockerContainerConfig {
13
13
  readonly network: string;
14
14
  /** Environment variables passed to the container. */
15
15
  readonly env: Readonly<Record<string, string>>;
16
- /** Command to execute as the container entrypoint. */
16
+ /** Command to execute (CMD). */
17
17
  readonly command: readonly string[];
18
+ /**
19
+ * Override the image's ENTRYPOINT.
20
+ * Optional. When set, uses `--entrypoint` flag on `docker create`.
21
+ * Needed for images like `alpine/socat` that set ENTRYPOINT to a binary.
22
+ */
23
+ readonly entrypoint?: string;
18
24
  /** IronCurtain session label for stale container detection. */
19
25
  readonly sessionLabel?: string;
20
- /** Extra --add-host entries (e.g. ["host.docker.internal:172.30.0.1"]). When set, suppresses the default host-gateway mapping. */
26
+ /** Extra --add-host entries (e.g. ["host.docker.internal:172.30.0.3"]). When set, suppresses the default host-gateway mapping. */
21
27
  readonly extraHosts?: readonly string[];
22
28
  /** Optional resource limits. */
23
29
  readonly resources?: {
@@ -51,6 +57,12 @@ export interface DockerContainerConfig {
51
57
  * must NEVER set this field.
52
58
  */
53
59
  readonly capAdd?: readonly string[];
60
+ /**
61
+ * Allocate a pseudo-TTY for the container (-t flag).
62
+ * Required for PTY mode where the container runs an interactive process.
63
+ * Optional. Defaults to false (no TTY allocated).
64
+ */
65
+ readonly tty?: boolean;
54
66
  }
55
67
  /** A volume mount for a Docker container. */
56
68
  export interface DockerMount {
@@ -117,4 +129,8 @@ export interface DockerManager {
117
129
  containerExists(nameOrId: string): Promise<boolean>;
118
130
  /** Returns the image ID (sha256 digest) for a container or image. undefined if not found. */
119
131
  getImageId(nameOrId: string): Promise<string | undefined>;
132
+ /** Connect an existing container to a Docker network. */
133
+ connectNetwork(networkName: string, containerId: string): Promise<void>;
134
+ /** Get a container's IP address on a specific network. */
135
+ getContainerIp(containerId: string, network: string): Promise<string>;
120
136
  }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared escalation watcher for polling escalation directories.
3
+ *
4
+ * Pure data module -- no I/O presentation. Consumers (DockerAgentSession,
5
+ * EscalationListener) handle display. Polls a session's escalation
6
+ * directory for new request files and handles resolution via response files.
7
+ */
8
+ import type { EscalationRequest } from '../session/types.js';
9
+ /** Events emitted by the escalation watcher. */
10
+ export interface EscalationWatcherEvents {
11
+ /** A new escalation request was detected. */
12
+ onEscalation: (request: EscalationRequest) => void;
13
+ /** A pending escalation expired (proxy timed out and cleaned up files). */
14
+ onEscalationExpired: (escalationId: string) => void;
15
+ }
16
+ export interface EscalationWatcher {
17
+ /** Start polling the escalation directory. */
18
+ start(): void;
19
+ /** Stop polling. */
20
+ stop(): void;
21
+ /** Returns the currently pending escalation, if any. */
22
+ getPending(): EscalationRequest | undefined;
23
+ /**
24
+ * Resolves a pending escalation by writing the response file.
25
+ * Returns true if the resolution was accepted (request file still exists),
26
+ * or false if the escalation had already expired.
27
+ *
28
+ * @throws {Error} if no escalation with this ID is pending
29
+ */
30
+ resolve(escalationId: string, decision: 'approved' | 'denied'): boolean;
31
+ }
32
+ export interface EscalationWatcherOptions {
33
+ /** Poll interval in ms. Default: 300. */
34
+ pollIntervalMs?: number;
35
+ }
36
+ /**
37
+ * Atomically writes a JSON file using write-to-temp-then-rename.
38
+ * Prevents partial reads by other processes polling the same directory.
39
+ */
40
+ export declare function atomicWriteJsonSync(filePath: string, data: unknown): void;
41
+ /**
42
+ * Creates an escalation watcher for a single session's escalation directory.
43
+ */
44
+ export declare function createEscalationWatcher(escalationDir: string, events: EscalationWatcherEvents, options?: EscalationWatcherOptions): EscalationWatcher;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Shared escalation watcher for polling escalation directories.
3
+ *
4
+ * Pure data module -- no I/O presentation. Consumers (DockerAgentSession,
5
+ * EscalationListener) handle display. Polls a session's escalation
6
+ * directory for new request files and handles resolution via response files.
7
+ */
8
+ import { readdirSync, readFileSync, existsSync, writeFileSync, renameSync } from 'node:fs';
9
+ import { resolve } from 'node:path';
10
+ const DEFAULT_POLL_INTERVAL_MS = 300;
11
+ /**
12
+ * Atomically writes a JSON file using write-to-temp-then-rename.
13
+ * Prevents partial reads by other processes polling the same directory.
14
+ */
15
+ export function atomicWriteJsonSync(filePath, data) {
16
+ const tmpPath = `${filePath}.tmp`;
17
+ writeFileSync(tmpPath, JSON.stringify(data, null, 2));
18
+ renameSync(tmpPath, filePath);
19
+ }
20
+ /**
21
+ * Creates an escalation watcher for a single session's escalation directory.
22
+ */
23
+ export function createEscalationWatcher(escalationDir, events, options) {
24
+ const pollIntervalMs = options?.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
25
+ let pendingEscalation;
26
+ const seenEscalationIds = new Set();
27
+ let pollInterval = null;
28
+ function extractEscalationId(filename) {
29
+ return filename.replace(/^request-/, '').replace(/\.json$/, '');
30
+ }
31
+ function checkEscalationExpiry() {
32
+ if (!pendingEscalation)
33
+ return;
34
+ const escalationId = pendingEscalation.escalationId;
35
+ const requestExists = existsSync(resolve(escalationDir, `request-${escalationId}.json`));
36
+ const responseExists = existsSync(resolve(escalationDir, `response-${escalationId}.json`));
37
+ if (!requestExists && !responseExists) {
38
+ const expiredId = escalationId;
39
+ pendingEscalation = undefined;
40
+ events.onEscalationExpired(expiredId);
41
+ }
42
+ }
43
+ function pollEscalationDirectory() {
44
+ if (pendingEscalation) {
45
+ checkEscalationExpiry();
46
+ return;
47
+ }
48
+ try {
49
+ const files = readdirSync(escalationDir);
50
+ const requestFile = files.find((f) => f.startsWith('request-') && f.endsWith('.json') && !seenEscalationIds.has(extractEscalationId(f)));
51
+ if (!requestFile)
52
+ return;
53
+ const requestPath = resolve(escalationDir, requestFile);
54
+ const request = JSON.parse(readFileSync(requestPath, 'utf-8'));
55
+ seenEscalationIds.add(request.escalationId);
56
+ pendingEscalation = request;
57
+ events.onEscalation(request);
58
+ }
59
+ catch {
60
+ // Directory may not exist yet or be empty
61
+ }
62
+ }
63
+ return {
64
+ start() {
65
+ if (pollInterval)
66
+ return;
67
+ pollInterval = setInterval(pollEscalationDirectory, pollIntervalMs);
68
+ },
69
+ stop() {
70
+ if (pollInterval) {
71
+ clearInterval(pollInterval);
72
+ pollInterval = null;
73
+ }
74
+ },
75
+ getPending() {
76
+ return pendingEscalation;
77
+ },
78
+ resolve(escalationId, decision) {
79
+ if (!pendingEscalation || pendingEscalation.escalationId !== escalationId) {
80
+ throw new Error(`No pending escalation with ID: ${escalationId}`);
81
+ }
82
+ const responsePath = resolve(escalationDir, `response-${escalationId}.json`);
83
+ atomicWriteJsonSync(responsePath, { decision });
84
+ pendingEscalation = undefined;
85
+ // Stale detection: verify the request file still exists after writing the response.
86
+ // If the proxy already timed out and cleaned up, the response was too late.
87
+ const requestStillExists = existsSync(resolve(escalationDir, `request-${escalationId}.json`));
88
+ return requestStillExists;
89
+ },
90
+ };
91
+ }
92
+ //# sourceMappingURL=escalation-watcher.js.map