@provos/ironcurtain 0.5.0 → 0.6.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 (89) hide show
  1. package/README.md +66 -66
  2. package/dist/cli.js +9 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/config/types.d.ts +6 -0
  5. package/dist/docker/adapters/claude-code.js +44 -19
  6. package/dist/docker/adapters/claude-code.js.map +1 -1
  7. package/dist/docker/agent-adapter.d.ts +6 -0
  8. package/dist/docker/agent-adapter.js +6 -1
  9. package/dist/docker/agent-adapter.js.map +1 -1
  10. package/dist/docker/docker-agent-session.d.ts +1 -1
  11. package/dist/docker/docker-agent-session.js +3 -2
  12. package/dist/docker/docker-agent-session.js.map +1 -1
  13. package/dist/docker/docker-infrastructure.js +10 -3
  14. package/dist/docker/docker-infrastructure.js.map +1 -1
  15. package/dist/docker/mitm-proxy.d.ts +5 -2
  16. package/dist/docker/mitm-proxy.js +169 -46
  17. package/dist/docker/mitm-proxy.js.map +1 -1
  18. package/dist/docker/oauth-credentials.d.ts +15 -2
  19. package/dist/docker/oauth-credentials.js +92 -8
  20. package/dist/docker/oauth-credentials.js.map +1 -1
  21. package/dist/docker/oauth-token-manager.d.ts +80 -0
  22. package/dist/docker/oauth-token-manager.js +133 -0
  23. package/dist/docker/oauth-token-manager.js.map +1 -0
  24. package/dist/docker/orientation.d.ts +1 -1
  25. package/dist/docker/orientation.js +2 -1
  26. package/dist/docker/orientation.js.map +1 -1
  27. package/dist/docker/pty-session.d.ts +2 -0
  28. package/dist/docker/pty-session.js +105 -9
  29. package/dist/docker/pty-session.js.map +1 -1
  30. package/dist/escalation/escalation-watcher.js +2 -0
  31. package/dist/escalation/escalation-watcher.js.map +1 -1
  32. package/dist/escalation/listener-command.js +9 -58
  33. package/dist/escalation/listener-command.js.map +1 -1
  34. package/dist/escalation/listener-lock.d.ts +20 -0
  35. package/dist/escalation/listener-lock.js +69 -0
  36. package/dist/escalation/listener-lock.js.map +1 -0
  37. package/dist/index.js +21 -1
  38. package/dist/index.js.map +1 -1
  39. package/dist/mux/mux-app.d.ts +24 -0
  40. package/dist/mux/mux-app.js +445 -0
  41. package/dist/mux/mux-app.js.map +1 -0
  42. package/dist/mux/mux-command.d.ts +7 -0
  43. package/dist/mux/mux-command.js +93 -0
  44. package/dist/mux/mux-command.js.map +1 -0
  45. package/dist/mux/mux-escalation-manager.d.ts +48 -0
  46. package/dist/mux/mux-escalation-manager.js +162 -0
  47. package/dist/mux/mux-escalation-manager.js.map +1 -0
  48. package/dist/mux/mux-input-handler.d.ts +49 -0
  49. package/dist/mux/mux-input-handler.js +457 -0
  50. package/dist/mux/mux-input-handler.js.map +1 -0
  51. package/dist/mux/mux-renderer.d.ts +86 -0
  52. package/dist/mux/mux-renderer.js +758 -0
  53. package/dist/mux/mux-renderer.js.map +1 -0
  54. package/dist/mux/mux-splash.d.ts +17 -0
  55. package/dist/mux/mux-splash.js +322 -0
  56. package/dist/mux/mux-splash.js.map +1 -0
  57. package/dist/mux/pty-bridge.d.ts +79 -0
  58. package/dist/mux/pty-bridge.js +151 -0
  59. package/dist/mux/pty-bridge.js.map +1 -0
  60. package/dist/mux/trusted-input.d.ts +27 -0
  61. package/dist/mux/trusted-input.js +27 -0
  62. package/dist/mux/trusted-input.js.map +1 -0
  63. package/dist/mux/types.d.ts +92 -0
  64. package/dist/mux/types.js +45 -0
  65. package/dist/mux/types.js.map +1 -0
  66. package/dist/sandbox/index.js +6 -0
  67. package/dist/sandbox/index.js.map +1 -1
  68. package/dist/session/agent-session.js +3 -0
  69. package/dist/session/agent-session.js.map +1 -1
  70. package/dist/session/index.js +14 -7
  71. package/dist/session/index.js.map +1 -1
  72. package/dist/session/types.d.ts +6 -0
  73. package/dist/session/workspace-validation.d.ts +17 -0
  74. package/dist/session/workspace-validation.js +65 -0
  75. package/dist/session/workspace-validation.js.map +1 -0
  76. package/dist/trusted-process/auto-approver.d.ts +13 -4
  77. package/dist/trusted-process/auto-approver.js +12 -5
  78. package/dist/trusted-process/auto-approver.js.map +1 -1
  79. package/dist/trusted-process/escalation.js +5 -0
  80. package/dist/trusted-process/escalation.js.map +1 -1
  81. package/dist/trusted-process/index.js +23 -0
  82. package/dist/trusted-process/index.js.map +1 -1
  83. package/dist/trusted-process/mcp-proxy-server.d.ts +0 -2
  84. package/dist/trusted-process/mcp-proxy-server.js +82 -26
  85. package/dist/trusted-process/mcp-proxy-server.js.map +1 -1
  86. package/dist/trusted-process/path-utils.d.ts +15 -0
  87. package/dist/trusted-process/path-utils.js +38 -0
  88. package/dist/trusted-process/path-utils.js.map +1 -1
  89. package/package.json +6 -1
@@ -11,13 +11,19 @@
11
11
  * 2. macOS Keychain (if credentials file missing)
12
12
  * 3. Fall back to API key from config
13
13
  */
14
- import { existsSync, readFileSync } from 'node:fs';
14
+ import { existsSync, readFileSync, writeFileSync, chmodSync } from 'node:fs';
15
15
  import { homedir, platform, userInfo } from 'node:os';
16
16
  import { resolve } from 'node:path';
17
17
  import { execFileSync } from 'node:child_process';
18
18
  import * as logger from '../logger.js';
19
19
  /** Minimum remaining token lifetime (5 minutes) to consider a token usable. */
20
20
  const TOKEN_EXPIRY_BUFFER_MS = 5 * 60 * 1000;
21
+ function isNonEmptyString(value) {
22
+ return typeof value === 'string' && value.length > 0;
23
+ }
24
+ function isPositiveFiniteNumber(value) {
25
+ return typeof value === 'number' && Number.isFinite(value) && value > 0;
26
+ }
21
27
  /** Service names used by Claude Code in macOS Keychain. */
22
28
  const KEYCHAIN_SERVICE_NAMES = ['Claude Code-credentials', 'Claude Code'];
23
29
  /**
@@ -71,11 +77,11 @@ export function parseCredentialsJson(json) {
71
77
  if (typeof oauth !== 'object' || oauth === null)
72
78
  return null;
73
79
  const creds = oauth;
74
- if (typeof creds.accessToken !== 'string' || !creds.accessToken)
80
+ if (!isNonEmptyString(creds.accessToken))
75
81
  return null;
76
- if (typeof creds.refreshToken !== 'string' || !creds.refreshToken)
82
+ if (!isNonEmptyString(creds.refreshToken))
77
83
  return null;
78
- if (typeof creds.expiresAt !== 'number' || !Number.isFinite(creds.expiresAt) || creds.expiresAt <= 0)
84
+ if (!isPositiveFiniteNumber(creds.expiresAt))
79
85
  return null;
80
86
  return {
81
87
  accessToken: creds.accessToken,
@@ -119,6 +125,86 @@ export function extractFromKeychain() {
119
125
  }
120
126
  return null;
121
127
  }
128
+ /** Claude Code's public OAuth client ID. */
129
+ const OAUTH_CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
130
+ /** Anthropic's OAuth token endpoint. */
131
+ const OAUTH_TOKEN_URL = 'https://console.anthropic.com/api/oauth/token';
132
+ /**
133
+ * Refreshes an OAuth access token using a refresh token grant.
134
+ *
135
+ * Returns new credentials on success, or null on failure (expired refresh
136
+ * token, network error, etc.).
137
+ */
138
+ export async function refreshOAuthToken(refreshToken) {
139
+ try {
140
+ const response = await fetch(OAUTH_TOKEN_URL, {
141
+ method: 'POST',
142
+ headers: { 'Content-Type': 'application/json' },
143
+ body: JSON.stringify({
144
+ grant_type: 'refresh_token',
145
+ refresh_token: refreshToken,
146
+ client_id: OAUTH_CLIENT_ID,
147
+ }),
148
+ });
149
+ if (!response.ok) {
150
+ logger.warn(`OAuth token refresh failed: HTTP ${response.status}`);
151
+ return null;
152
+ }
153
+ const data = (await response.json());
154
+ return parseTokenResponse(data);
155
+ }
156
+ catch (err) {
157
+ logger.warn(`OAuth token refresh error: ${err instanceof Error ? err.message : String(err)}`);
158
+ return null;
159
+ }
160
+ }
161
+ /**
162
+ * Validates and extracts credentials from an OAuth token endpoint response.
163
+ * Returns null if any required field is missing or invalid.
164
+ */
165
+ function parseTokenResponse(data) {
166
+ const { access_token: accessToken, refresh_token: refreshToken, expires_in: expiresIn } = data;
167
+ if (!isNonEmptyString(accessToken))
168
+ return null;
169
+ if (!isNonEmptyString(refreshToken))
170
+ return null;
171
+ if (!isPositiveFiniteNumber(expiresIn))
172
+ return null;
173
+ return {
174
+ accessToken,
175
+ refreshToken,
176
+ expiresAt: Date.now() + expiresIn * 1000,
177
+ };
178
+ }
179
+ /**
180
+ * Writes updated OAuth credentials back to ~/.claude/.credentials.json,
181
+ * preserving other fields in the file (e.g., scopes, subscriptionType).
182
+ */
183
+ export function saveOAuthCredentials(credentials, filePath) {
184
+ const credPath = filePath ?? getCredentialsFilePath();
185
+ let existing = {};
186
+ try {
187
+ if (existsSync(credPath)) {
188
+ existing = JSON.parse(readFileSync(credPath, 'utf-8'));
189
+ }
190
+ }
191
+ catch {
192
+ // Start fresh if file is unreadable
193
+ }
194
+ const existingOauth = typeof existing.claudeAiOauth === 'object' && existing.claudeAiOauth !== null
195
+ ? existing.claudeAiOauth
196
+ : {};
197
+ existing.claudeAiOauth = {
198
+ ...existingOauth,
199
+ accessToken: credentials.accessToken,
200
+ refreshToken: credentials.refreshToken,
201
+ expiresAt: credentials.expiresAt,
202
+ };
203
+ writeFileSync(credPath, JSON.stringify(existing, null, 2) + '\n', { mode: 0o600 });
204
+ // chmod after write — writeFileSync's mode only applies when creating new files;
205
+ // existing files with broader permissions need an explicit chmod.
206
+ chmodSync(credPath, 0o600);
207
+ }
122
208
  const defaultSources = {
123
209
  loadFromFile: loadOAuthCredentials,
124
210
  loadFromKeychain: extractFromKeychain,
@@ -132,8 +218,6 @@ const defaultSources = {
132
218
  * 3. OAuth credentials from macOS Keychain (if credentials file missing)
133
219
  * 4. API key from config
134
220
  * 5. None
135
- *
136
- * @param sources - Injectable credential loaders (for testing)
137
221
  */
138
222
  export function detectAuthMethod(config, sources) {
139
223
  const { loadFromFile, loadFromKeychain } = sources ?? defaultSources;
@@ -147,7 +231,7 @@ export function detectAuthMethod(config, sources) {
147
231
  if (fileCreds) {
148
232
  if (!isTokenExpired(fileCreds)) {
149
233
  logger.info('Detected OAuth credentials from ~/.claude/.credentials.json');
150
- return { kind: 'oauth', credentials: fileCreds };
234
+ return { kind: 'oauth', credentials: fileCreds, source: 'file' };
151
235
  }
152
236
  logger.warn('OAuth token from credentials file is expired');
153
237
  }
@@ -156,7 +240,7 @@ export function detectAuthMethod(config, sources) {
156
240
  const keychainCreds = loadFromKeychain();
157
241
  if (keychainCreds) {
158
242
  if (!isTokenExpired(keychainCreds)) {
159
- return { kind: 'oauth', credentials: keychainCreds };
243
+ return { kind: 'oauth', credentials: keychainCreds, source: 'keychain' };
160
244
  }
161
245
  logger.warn('OAuth token from macOS Keychain is expired');
162
246
  }
@@ -1 +1 @@
1
- {"version":3,"file":"oauth-credentials.js","sourceRoot":"","sources":["../../src/docker/oauth-credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAElD,OAAO,KAAK,MAAM,MAAM,cAAc,CAAC;AAevC,+EAA+E;AAC/E,MAAM,sBAAsB,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAE7C,2DAA2D;AAC3D,MAAM,sBAAsB,GAAG,CAAC,yBAAyB,EAAE,aAAa,CAAU,CAAC;AAEnF;;;GAGG;AACH,MAAM,UAAU,sBAAsB;IACpC,OAAO,OAAO,CAAC,OAAO,EAAE,EAAE,SAAS,EAAE,mBAAmB,CAAC,CAAC;AAC5D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,cAAc,CAAC,WAA6B;IAC1D,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,sBAAsB,IAAI,WAAW,CAAC,SAAS,CAAC;AACtE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB;IAClC,MAAM,QAAQ,GAAG,sBAAsB,EAAE,CAAC;IAC1C,OAAO,uBAAuB,CAAC,QAAQ,CAAC,CAAC;AAC3C,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB,CAAC,QAAgB;IACtD,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC;QAAE,OAAO,IAAI,CAAC;IAEvC,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,YAAY,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;QAC5C,OAAO,oBAAoB,CAAC,GAAG,CAAC,CAAC;IACnC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,CAAC,IAAI,CAAC,yCAAyC,QAAQ,EAAE,CAAC,CAAC;QACjE,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY;IAC/C,IAAI,CAAC;QACH,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,OAAO,MAAM,KAAK,QAAQ,IAAI,MAAM,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAE/D,MAAM,GAAG,GAAG,MAAiC,CAAC;QAC9C,MAAM,KAAK,GAAG,GAAG,CAAC,aAAa,CAAC;QAChC,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAE7D,MAAM,KAAK,GAAG,KAAgC,CAAC;QAC/C,IAAI,OAAO,KAAK,CAAC,WAAW,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,WAAW;YAAE,OAAO,IAAI,CAAC;QAC7E,IAAI,OAAO,KAAK,CAAC,YAAY,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,YAAY;YAAE,OAAO,IAAI,CAAC;QAC/E,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,QAAQ,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,IAAI,KAAK,CAAC,SAAS,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QAElH,OAAO;YACL,WAAW,EAAE,KAAK,CAAC,WAAW;YAC9B,YAAY,EAAE,KAAK,CAAC,YAAY;YAChC,SAAS,EAAE,KAAK,CAAC,SAAS;SAC3B,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB;IACjC,IAAI,QAAQ,EAAE,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAEzC,MAAM,OAAO,GAAG,QAAQ,EAAE,CAAC,QAAQ,CAAC;IACpC,KAAK,MAAM,OAAO,IAAI,sBAAsB,EAAE,CAAC;QAC7C,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,YAAY,CAAC,UAAU,EAAE,CAAC,uBAAuB,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE;gBACrG,QAAQ,EAAE,MAAM;gBAChB,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;gBAC/B,OAAO,EAAE,MAAM;aAChB,CAAC,CAAC;YACH,MAAM,WAAW,GAAG,oBAAoB,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC;YACxD,IAAI,WAAW,EAAE,CAAC;gBAChB,MAAM,CAAC,IAAI,CAAC,wDAAwD,OAAO,IAAI,CAAC,CAAC;gBACjF,OAAO,WAAW,CAAC;YACrB,CAAC;QACH,CAAC;QAAC,MAAM,CAAC;YACP,wDAAwD;YACxD,SAAS;QACX,CAAC;IACH,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAQD,MAAM,cAAc,GAAsB;IACxC,YAAY,EAAE,oBAAoB;IAClC,gBAAgB,EAAE,mBAAmB;CACtC,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,gBAAgB,CAAC,MAAyB,EAAE,OAA2B;IACrF,MAAM,EAAE,YAAY,EAAE,gBAAgB,EAAE,GAAG,OAAO,IAAI,cAAc,CAAC;IAErE,gDAAgD;IAChD,IAAI,OAAO,CAAC,GAAG,CAAC,uBAAuB,KAAK,QAAQ,EAAE,CAAC;QACrD,MAAM,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAC;QACrF,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;IACnC,CAAC;IAED,kCAAkC;IAClC,MAAM,SAAS,GAAG,YAAY,EAAE,CAAC;IACjC,IAAI,SAAS,EAAE,CAAC;QACd,IAAI,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,6DAA6D,CAAC,CAAC;YAC3E,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,CAAC;QACnD,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,8CAA8C,CAAC,CAAC;IAC9D,CAAC;IAED,uDAAuD;IACvD,IAAI,CAAC,SAAS,EAAE,CAAC;QACf,MAAM,aAAa,GAAG,gBAAgB,EAAE,CAAC;QACzC,IAAI,aAAa,EAAE,CAAC;YAClB,IAAI,CAAC,cAAc,CAAC,aAAa,CAAC,EAAE,CAAC;gBACnC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,aAAa,EAAE,CAAC;YACvD,CAAC;YACD,MAAM,CAAC,IAAI,CAAC,4CAA4C,CAAC,CAAC;QAC5D,CAAC;IACH,CAAC;IAED,uBAAuB;IACvB,OAAO,iBAAiB,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC;AAED;;GAEG;AACH,SAAS,iBAAiB,CAAC,MAAyB;IAClD,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,eAAe,CAAC;IAC9C,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,CAAC,IAAI,CAAC,iDAAiD,CAAC,CAAC;QAC/D,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,EAAE,CAAC;IACjC,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"oauth-credentials.js","sourceRoot":"","sources":["../../src/docker/oauth-credentials.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * OAuth token lifecycle manager for Docker agent sessions.
3
+ *
4
+ * Handles proactive refresh (before token expiry) and reactive refresh
5
+ * (on upstream 401). Coordinates with the host Claude Code process that
6
+ * may also refresh tokens concurrently — re-reads the credentials file
7
+ * before attempting a refresh to avoid using a stale refresh token.
8
+ *
9
+ * On macOS where credentials come from the Keychain, the manager operates
10
+ * in read-only mode: it re-reads credentials from file and Keychain but
11
+ * never performs its own refresh grant. This prevents invalidating the
12
+ * host Claude Code's refresh token, which lives in the Keychain.
13
+ *
14
+ * A single in-flight refresh promise is shared across concurrent callers
15
+ * to prevent duplicate refresh requests.
16
+ */
17
+ import type { OAuthCredentials } from './oauth-credentials.js';
18
+ /** Injectable dependencies for testability. */
19
+ export interface TokenManagerDeps {
20
+ loadCredentials: (filePath: string) => OAuthCredentials | null;
21
+ loadFromKeychain: () => OAuthCredentials | null;
22
+ refreshToken: (refreshToken: string) => Promise<OAuthCredentials | null>;
23
+ saveCredentials: (credentials: OAuthCredentials, filePath?: string) => void;
24
+ credentialsFilePath: string;
25
+ /** @deprecated No longer used internally. Kept for backward compatibility with tests. */
26
+ now?: () => number;
27
+ }
28
+ export interface OAuthTokenManagerOptions {
29
+ /**
30
+ * Whether the manager may perform its own refresh_token grant.
31
+ *
32
+ * When false (Keychain-sourced credentials on macOS), the manager only
33
+ * re-reads credentials from the file and Keychain — it never POSTs to
34
+ * the token endpoint itself, because doing so would rotate the refresh
35
+ * token and break host Claude Code's copy in the Keychain.
36
+ *
37
+ * Defaults to true (file-sourced credentials on Linux/WSL).
38
+ */
39
+ canRefresh?: boolean;
40
+ }
41
+ export declare class OAuthTokenManager {
42
+ private credentials;
43
+ private inflightRefresh;
44
+ private readonly deps;
45
+ private readonly canRefresh;
46
+ constructor(initialCredentials: OAuthCredentials, options?: OAuthTokenManagerOptions, deps?: Partial<TokenManagerDeps>);
47
+ /**
48
+ * Returns the current access token. This is always the latest known-good
49
+ * token, whether from the initial credentials, a file re-read, or a refresh.
50
+ */
51
+ get accessToken(): string;
52
+ /**
53
+ * Returns a valid access token, refreshing proactively if near expiry.
54
+ *
55
+ * Called before every upstream request to ensure the token is fresh.
56
+ * If the token has more than 5 minutes of validity, returns immediately.
57
+ */
58
+ getValidAccessToken(): Promise<string>;
59
+ /**
60
+ * Called on upstream 401 — forces a refresh attempt.
61
+ *
62
+ * Returns the new access token on success, or null if refresh is
63
+ * unrecoverable (both our refresh and file re-read failed).
64
+ */
65
+ handleAuthFailure(): Promise<string | null>;
66
+ /**
67
+ * Core refresh logic with deduplication. Concurrent callers share a single
68
+ * in-flight refresh promise.
69
+ *
70
+ * File-sourced credentials (canRefresh=true):
71
+ * Re-read file -> if still expired, POST refresh grant -> on failure,
72
+ * re-read file once more (host process may have won the race).
73
+ *
74
+ * Keychain-sourced credentials (canRefresh=false):
75
+ * Re-read file and Keychain only -- never POST a refresh grant, because
76
+ * doing so would rotate the refresh token and break host Claude Code.
77
+ */
78
+ private doRefresh;
79
+ private executeRefresh;
80
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * OAuth token lifecycle manager for Docker agent sessions.
3
+ *
4
+ * Handles proactive refresh (before token expiry) and reactive refresh
5
+ * (on upstream 401). Coordinates with the host Claude Code process that
6
+ * may also refresh tokens concurrently — re-reads the credentials file
7
+ * before attempting a refresh to avoid using a stale refresh token.
8
+ *
9
+ * On macOS where credentials come from the Keychain, the manager operates
10
+ * in read-only mode: it re-reads credentials from file and Keychain but
11
+ * never performs its own refresh grant. This prevents invalidating the
12
+ * host Claude Code's refresh token, which lives in the Keychain.
13
+ *
14
+ * A single in-flight refresh promise is shared across concurrent callers
15
+ * to prevent duplicate refresh requests.
16
+ */
17
+ import { isTokenExpired, loadCredentialsFromFile, refreshOAuthToken, saveOAuthCredentials, getCredentialsFilePath, extractFromKeychain, } from './oauth-credentials.js';
18
+ import * as logger from '../logger.js';
19
+ const defaultDeps = {
20
+ loadCredentials: loadCredentialsFromFile,
21
+ loadFromKeychain: extractFromKeychain,
22
+ refreshToken: refreshOAuthToken,
23
+ saveCredentials: saveOAuthCredentials,
24
+ credentialsFilePath: getCredentialsFilePath(),
25
+ };
26
+ export class OAuthTokenManager {
27
+ credentials;
28
+ inflightRefresh = null;
29
+ deps;
30
+ canRefresh;
31
+ constructor(initialCredentials, options, deps) {
32
+ this.credentials = initialCredentials;
33
+ this.canRefresh = options?.canRefresh ?? true;
34
+ this.deps = { ...defaultDeps, ...deps };
35
+ }
36
+ /**
37
+ * Returns the current access token. This is always the latest known-good
38
+ * token, whether from the initial credentials, a file re-read, or a refresh.
39
+ */
40
+ get accessToken() {
41
+ return this.credentials.accessToken;
42
+ }
43
+ /**
44
+ * Returns a valid access token, refreshing proactively if near expiry.
45
+ *
46
+ * Called before every upstream request to ensure the token is fresh.
47
+ * If the token has more than 5 minutes of validity, returns immediately.
48
+ */
49
+ async getValidAccessToken() {
50
+ if (!isTokenExpired(this.credentials)) {
51
+ return this.credentials.accessToken;
52
+ }
53
+ logger.info('[oauth-token-manager] Token near expiry, attempting proactive refresh');
54
+ const newToken = await this.doRefresh();
55
+ return newToken ?? this.credentials.accessToken;
56
+ }
57
+ /**
58
+ * Called on upstream 401 — forces a refresh attempt.
59
+ *
60
+ * Returns the new access token on success, or null if refresh is
61
+ * unrecoverable (both our refresh and file re-read failed).
62
+ */
63
+ async handleAuthFailure() {
64
+ logger.info('[oauth-token-manager] Handling auth failure (401), attempting refresh');
65
+ return this.doRefresh();
66
+ }
67
+ /**
68
+ * Core refresh logic with deduplication. Concurrent callers share a single
69
+ * in-flight refresh promise.
70
+ *
71
+ * File-sourced credentials (canRefresh=true):
72
+ * Re-read file -> if still expired, POST refresh grant -> on failure,
73
+ * re-read file once more (host process may have won the race).
74
+ *
75
+ * Keychain-sourced credentials (canRefresh=false):
76
+ * Re-read file and Keychain only -- never POST a refresh grant, because
77
+ * doing so would rotate the refresh token and break host Claude Code.
78
+ */
79
+ doRefresh() {
80
+ if (this.inflightRefresh) {
81
+ return this.inflightRefresh;
82
+ }
83
+ this.inflightRefresh = this.executeRefresh().finally(() => {
84
+ this.inflightRefresh = null;
85
+ });
86
+ return this.inflightRefresh;
87
+ }
88
+ async executeRefresh() {
89
+ // Re-read credentials file -- another process may have refreshed already
90
+ const fileCreds = this.deps.loadCredentials(this.deps.credentialsFilePath);
91
+ if (fileCreds && !isTokenExpired(fileCreds)) {
92
+ logger.info('[oauth-token-manager] Found valid token in credentials file (refreshed by another process)');
93
+ this.credentials = fileCreds;
94
+ return this.credentials.accessToken;
95
+ }
96
+ // In read-only mode, also try the Keychain but never POST a refresh grant
97
+ if (!this.canRefresh) {
98
+ const keychainCreds = this.deps.loadFromKeychain();
99
+ if (keychainCreds && !isTokenExpired(keychainCreds)) {
100
+ logger.info('[oauth-token-manager] Found valid token in Keychain (refreshed by host process)');
101
+ this.credentials = keychainCreds;
102
+ return this.credentials.accessToken;
103
+ }
104
+ logger.warn('[oauth-token-manager] Token expired and cannot self-refresh (Keychain mode) — waiting for host Claude Code to refresh');
105
+ return null;
106
+ }
107
+ // Attempt refresh ourselves, preferring the file's refresh token if available
108
+ const refreshToken = fileCreds?.refreshToken ?? this.credentials.refreshToken;
109
+ const newCreds = await this.deps.refreshToken(refreshToken);
110
+ if (newCreds) {
111
+ logger.info('[oauth-token-manager] Token refresh successful');
112
+ try {
113
+ this.deps.saveCredentials(newCreds, this.deps.credentialsFilePath);
114
+ }
115
+ catch (err) {
116
+ logger.warn(`[oauth-token-manager] Failed to save refreshed credentials: ${err instanceof Error ? err.message : String(err)}`);
117
+ }
118
+ this.credentials = newCreds;
119
+ return this.credentials.accessToken;
120
+ }
121
+ // Refresh failed -- re-read file one more time (host may have won the race)
122
+ logger.info('[oauth-token-manager] Refresh failed, re-reading credentials file as fallback');
123
+ const fallbackCreds = this.deps.loadCredentials(this.deps.credentialsFilePath);
124
+ if (fallbackCreds && !isTokenExpired(fallbackCreds)) {
125
+ logger.info('[oauth-token-manager] Found valid token in credentials file on fallback re-read');
126
+ this.credentials = fallbackCreds;
127
+ return this.credentials.accessToken;
128
+ }
129
+ logger.warn('[oauth-token-manager] Token refresh failed and no valid credentials available');
130
+ return null;
131
+ }
132
+ }
133
+ //# sourceMappingURL=oauth-token-manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"oauth-token-manager.js","sourceRoot":"","sources":["../../src/docker/oauth-token-manager.ts"],"names":[],"mappings":"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"}
@@ -5,7 +5,7 @@
5
5
  * agent container. Written to the orientation directory which is
6
6
  * bind-mounted read-only into the container.
7
7
  */
8
- import type { AgentAdapter } from './agent-adapter.js';
8
+ import { type AgentAdapter } from './agent-adapter.js';
9
9
  import type { IronCurtainConfig } from '../config/types.js';
10
10
  import type { ServerListing } from '../session/prompts.js';
11
11
  /**
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import { mkdirSync, writeFileSync } from 'node:fs';
9
9
  import { resolve, dirname } from 'node:path';
10
+ import { CONTAINER_WORKSPACE_DIR, } from './agent-adapter.js';
10
11
  /**
11
12
  * Extracts allowed domains from MCP server configurations.
12
13
  * Used to inform the agent about which domains it can access via MCP tools.
@@ -47,7 +48,7 @@ export function prepareSession(adapter, serverListings, sessionDir, config, host
47
48
  const orientationDir = resolve(sessionDir, 'orientation');
48
49
  mkdirSync(orientationDir, { recursive: true });
49
50
  const context = {
50
- workspaceDir: '/workspace',
51
+ workspaceDir: CONTAINER_WORKSPACE_DIR,
51
52
  hostSandboxDir,
52
53
  serverListings,
53
54
  allowedDomains: extractAllowedDomains(config),
@@ -1 +1 @@
1
- {"version":3,"file":"orientation.js","sourceRoot":"","sources":["../../src/docker/orientation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAK7C;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAyB;IAC7D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,YAAY,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5D,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC;QACrC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,SAAS;QACtD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,SAAS;QACtD,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,OAAe,EAAE,KAAwB;IACjE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAqB,EACrB,cAA+B,EAC/B,UAAkB,EAClB,MAAyB,EACzB,cAAsB,EACtB,YAAqB;IAErB,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAC1D,SAAS,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,MAAM,OAAO,GAAuB;QAClC,YAAY,EAAE,YAAY;QAC1B,cAAc;QACd,cAAc;QACd,cAAc,EAAE,qBAAqB,CAAC,MAAM,CAAC;QAC7C,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM;KAC9C,CAAC;IAEF,qFAAqF;IACrF,MAAM,OAAO,GAAG,YAAY,IAAI,6BAA6B,CAAC;IAC9D,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;IACrE,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC;IAE5E,MAAM,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACxD,OAAO,EAAE,YAAY,EAAE,CAAC;AAC1B,CAAC"}
1
+ {"version":3,"file":"orientation.js","sourceRoot":"","sources":["../../src/docker/orientation.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,OAAO,EACL,uBAAuB,GAIxB,MAAM,oBAAoB,CAAC;AAI5B;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CAAC,MAAyB;IAC7D,MAAM,OAAO,GAAG,IAAI,GAAG,EAAU,CAAC;IAClC,KAAK,MAAM,YAAY,IAAI,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,CAAC;QAC5D,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC;QACrC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,SAAS;QACtD,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,OAAO,IAAI,OAAO,OAAO,KAAK,QAAQ;YAAE,SAAS;QACtD,KAAK,MAAM,MAAM,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC5C,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACtB,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,OAAO,CAAC,CAAC;AACtB,CAAC;AAED;;;GAGG;AACH,SAAS,gBAAgB,CAAC,OAAe,EAAE,KAAwB;IACjE,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;QAC/C,SAAS,CAAC,OAAO,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACpD,aAAa,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,EAAE,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/D,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,OAAqB,EACrB,cAA+B,EAC/B,UAAkB,EAClB,MAAyB,EACzB,cAAsB,EACtB,YAAqB;IAErB,MAAM,cAAc,GAAG,OAAO,CAAC,UAAU,EAAE,aAAa,CAAC,CAAC;IAC1D,SAAS,CAAC,cAAc,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE/C,MAAM,OAAO,GAAuB;QAClC,YAAY,EAAE,uBAAuB;QACrC,cAAc;QACd,cAAc;QACd,cAAc,EAAE,qBAAqB,CAAC,MAAM,CAAC;QAC7C,WAAW,EAAE,YAAY,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM;KAC9C,CAAC;IAEF,qFAAqF;IACrF,MAAM,OAAO,GAAG,YAAY,IAAI,6BAA6B,CAAC;IAC9D,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC,CAAC;IACrE,gBAAgB,CAAC,cAAc,EAAE,OAAO,CAAC,wBAAwB,CAAC,OAAO,CAAC,CAAC,CAAC;IAE5E,MAAM,YAAY,GAAG,OAAO,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAAC;IACxD,OAAO,EAAE,YAAY,EAAE,CAAC;AAC1B,CAAC"}
@@ -19,6 +19,8 @@ export interface PtySessionOptions {
19
19
  readonly mode: SessionMode & {
20
20
  kind: 'docker';
21
21
  };
22
+ /** Validated workspace path. When provided, replaces the session sandbox. */
23
+ readonly workspacePath?: string;
22
24
  }
23
25
  /**
24
26
  * Runs a PTY session: starts proxies, launches container with PTY-enabled
@@ -19,9 +19,10 @@ import { resolve } from 'node:path';
19
19
  import chalk from 'chalk';
20
20
  import ora from 'ora';
21
21
  import { createSessionId } from '../session/types.js';
22
+ import { CONTAINER_WORKSPACE_DIR } from './agent-adapter.js';
22
23
  import { PTY_SOCK_NAME, DEFAULT_PTY_PORT } from './pty-types.js';
23
24
  import { createEscalationWatcher, atomicWriteJsonSync } from '../escalation/escalation-watcher.js';
24
- import { getSessionDir, getSessionSandboxDir, getSessionEscalationDir, getSessionAuditLogPath, getSessionLogPath, getSessionLlmLogPath, getPtyRegistryDir, } from '../config/paths.js';
25
+ import { getSessionDir, getSessionSandboxDir, getSessionEscalationDir, getSessionAuditLogPath, getSessionLogPath, getSessionLlmLogPath, getSessionAutoApproveLlmLogPath, getPtyRegistryDir, } from '../config/paths.js';
25
26
  import * as logger from '../logger.js';
26
27
  /** Maximum time to wait for the PTY socket to appear (ms). */
27
28
  const PTY_READINESS_TIMEOUT_MS = 30_000;
@@ -35,11 +36,14 @@ export async function runPtySession(options) {
35
36
  const { prepareDockerInfrastructure } = await import('./docker-infrastructure.js');
36
37
  const sessionId = createSessionId();
37
38
  const sessionDir = getSessionDir(sessionId);
38
- const sandboxDir = getSessionSandboxDir(sessionId);
39
+ const sandboxDir = options.workspacePath ?? getSessionSandboxDir(sessionId);
39
40
  const escalationDir = getSessionEscalationDir(sessionId);
40
41
  const auditLogPath = getSessionAuditLogPath(sessionId);
41
42
  const socketsDir = resolve(sessionDir, 'sockets');
42
- mkdirSync(sandboxDir, { recursive: true, mode: 0o700 });
43
+ // Only create the sandbox directory when not using an external workspace
44
+ if (!options.workspacePath) {
45
+ mkdirSync(sandboxDir, { recursive: true, mode: 0o700 });
46
+ }
43
47
  mkdirSync(escalationDir, { recursive: true, mode: 0o700 });
44
48
  mkdirSync(socketsDir, { recursive: true, mode: 0o700 });
45
49
  // Set up session logging
@@ -47,10 +51,11 @@ export async function runPtySession(options) {
47
51
  const llmLogPath = getSessionLlmLogPath(sessionId);
48
52
  logger.setup({ logFilePath: sessionLogPath });
49
53
  logger.info(`PTY session ${sessionId} starting`);
50
- logger.info(`Sandbox: ${sandboxDir}`);
54
+ logger.info(`${options.workspacePath ? 'Workspace' : 'Sandbox'}: ${sandboxDir}`);
51
55
  logger.info(`Escalation dir: ${escalationDir}`);
52
56
  logger.info(`LLM log: ${llmLogPath}`);
53
57
  // Patch config for this session
58
+ const autoApproveLlmLogPath = getSessionAutoApproveLlmLogPath(sessionId);
54
59
  const sessionConfig = {
55
60
  ...options.config,
56
61
  allowedDirectory: sandboxDir,
@@ -58,6 +63,8 @@ export async function runPtySession(options) {
58
63
  escalationDir,
59
64
  sessionLogPath,
60
65
  llmLogPath,
66
+ autoApproveLlmLogPath,
67
+ isPtySession: true,
61
68
  mcpServers: JSON.parse(JSON.stringify(options.config.mcpServers)),
62
69
  };
63
70
  const initSpinner = ora({
@@ -164,7 +171,7 @@ export async function runPtySession(options) {
164
171
  const sidecarIp = await docker.getContainerIp(sidecarContainerId, INTERNAL_NETWORK_NAME);
165
172
  extraHosts = [`host.docker.internal:${sidecarIp}`];
166
173
  mounts = [
167
- { source: sandboxDir, target: '/workspace', readonly: false },
174
+ { source: sandboxDir, target: CONTAINER_WORKSPACE_DIR, readonly: false },
168
175
  { source: orientationDir, target: '/etc/ironcurtain', readonly: true },
169
176
  ];
170
177
  }
@@ -177,11 +184,18 @@ export async function runPtySession(options) {
177
184
  };
178
185
  network = 'none';
179
186
  mounts = [
180
- { source: sandboxDir, target: '/workspace', readonly: false },
187
+ { source: sandboxDir, target: CONTAINER_WORKSPACE_DIR, readonly: false },
181
188
  { source: socketsDir, target: '/run/ironcurtain', readonly: false },
182
189
  { source: orientationDir, target: '/etc/ironcurtain', readonly: true },
183
190
  ];
184
191
  }
192
+ // Pass initial terminal size so start-claude.sh can set PTY dimensions
193
+ // before Claude starts, eliminating the resize race condition.
194
+ const { columns, rows } = process.stdout;
195
+ if (columns)
196
+ env.IRONCURTAIN_INITIAL_COLS = String(columns);
197
+ if (rows)
198
+ env.IRONCURTAIN_INITIAL_ROWS = String(rows);
185
199
  // Create and start container with PTY command and TTY
186
200
  containerId = await docker.create({
187
201
  image,
@@ -302,18 +316,37 @@ function attachPty(options) {
302
316
  stdin.setRawMode(true);
303
317
  }
304
318
  stdin.resume();
305
- // SIGWINCH: forward terminal resize to container PTY
319
+ // SIGWINCH: forward terminal resize to container PTY.
320
+ // verifyAbort cancels the background verify loop on first user resize
321
+ // so it doesn't fight with the new size.
322
+ const verifyAbort = new AbortController();
323
+ let isFirstResize = true;
306
324
  const onResize = () => {
307
325
  const { columns, rows } = stdout;
308
326
  if (columns && rows) {
309
- execFile('docker', ['exec', options.containerId, '/etc/ironcurtain/resize-pty.sh', String(columns), String(rows)], { timeout: 5000 }, () => {
310
- /* best effort */
327
+ if (!isFirstResize) {
328
+ verifyAbort.abort();
329
+ }
330
+ isFirstResize = false;
331
+ execFile('docker', ['exec', options.containerId, '/etc/ironcurtain/resize-pty.sh', String(columns), String(rows)], { timeout: 5000 }, (err, _stdout, stderr) => {
332
+ if (err) {
333
+ logger.warn(`resize-pty.sh failed: ${err.message}`);
334
+ if (stderr) {
335
+ logger.warn(`resize-pty.sh stderr: ${stderr.trim()}`);
336
+ }
337
+ }
311
338
  });
312
339
  }
313
340
  };
314
341
  stdout.on('resize', onResize);
315
342
  // Send initial size
316
343
  onResize();
344
+ // Background verify+retry to ensure the initial resize took effect.
345
+ // Fire-and-forget -- does not block the PTY proxy.
346
+ // Canceled via verifyAbort when the user resizes the terminal.
347
+ if (stdout.columns && stdout.rows) {
348
+ void verifyInitialPtySize(options.containerId, stdout.columns, stdout.rows, verifyAbort.signal);
349
+ }
317
350
  // Host -> Container
318
351
  // Ctrl-\ (0x1c) is intercepted as an emergency exit since raw mode
319
352
  // disables normal signal generation for SIGQUIT.
@@ -337,6 +370,7 @@ function attachPty(options) {
337
370
  stdin.removeListener('data', onData);
338
371
  conn.unpipe(stdout);
339
372
  stdin.pause();
373
+ verifyAbort.abort();
340
374
  options.signal?.removeEventListener('abort', onAbort);
341
375
  }
342
376
  function onAbort() {
@@ -363,6 +397,68 @@ function attachPty(options) {
363
397
  });
364
398
  });
365
399
  }
400
+ // --- PTY size verification ---
401
+ /** Maximum retries for initial PTY size verification. */
402
+ const PTY_SIZE_VERIFY_RETRIES = 5;
403
+ /** Interval between PTY size verification attempts (ms). */
404
+ const PTY_SIZE_VERIFY_INTERVAL_MS = 1_000;
405
+ /** Initial delay before first verification attempt (ms). */
406
+ const PTY_SIZE_VERIFY_INITIAL_DELAY_MS = 500;
407
+ /**
408
+ * Runs check-pty-size.sh and returns { rows, cols } or null on failure.
409
+ */
410
+ function checkPtySize(containerId) {
411
+ return new Promise((resolve) => {
412
+ execFile('docker', ['exec', containerId, '/etc/ironcurtain/check-pty-size.sh'], { timeout: 5000 }, (err, stdout) => {
413
+ if (err) {
414
+ resolve(null);
415
+ return;
416
+ }
417
+ const parts = stdout.trim().split(/\s+/);
418
+ if (parts.length >= 2) {
419
+ const rows = parseInt(parts[0], 10);
420
+ const cols = parseInt(parts[1], 10);
421
+ if (!isNaN(rows) && !isNaN(cols) && rows > 0 && cols > 0) {
422
+ resolve({ rows, cols });
423
+ return;
424
+ }
425
+ }
426
+ resolve(null);
427
+ });
428
+ });
429
+ }
430
+ /**
431
+ * Background verify+retry loop for initial PTY resize.
432
+ * Non-blocking (fire-and-forget). Aborted when a user resize occurs so it
433
+ * does not fight with legitimate SIGWINCH-driven resizes.
434
+ */
435
+ async function verifyInitialPtySize(containerId, expectedCols, expectedRows, signal) {
436
+ await new Promise((r) => setTimeout(r, PTY_SIZE_VERIFY_INITIAL_DELAY_MS));
437
+ for (let attempt = 0; attempt < PTY_SIZE_VERIFY_RETRIES; attempt++) {
438
+ if (signal?.aborted)
439
+ return;
440
+ const size = await checkPtySize(containerId);
441
+ if (size && size.cols === expectedCols && size.rows === expectedRows) {
442
+ return; // PTY size matches
443
+ }
444
+ if (signal?.aborted)
445
+ return;
446
+ // Mismatch or check failed -- try resizing
447
+ await new Promise((resolve) => {
448
+ execFile('docker', ['exec', containerId, '/etc/ironcurtain/resize-pty.sh', String(expectedCols), String(expectedRows)], { timeout: 5000 }, () => resolve());
449
+ });
450
+ // Wait before rechecking
451
+ await new Promise((r) => setTimeout(r, PTY_SIZE_VERIFY_INTERVAL_MS));
452
+ }
453
+ if (signal?.aborted)
454
+ return;
455
+ // Final check
456
+ const finalSize = await checkPtySize(containerId);
457
+ if (!finalSize || finalSize.cols !== expectedCols || finalSize.rows !== expectedRows) {
458
+ logger.warn(`PTY size verification failed after ${PTY_SIZE_VERIFY_RETRIES} retries ` +
459
+ `(expected ${expectedCols}x${expectedRows}, got ${finalSize ? `${finalSize.cols}x${finalSize.rows}` : 'unknown'})`);
460
+ }
461
+ }
366
462
  // --- Readiness polling ---
367
463
  /**
368
464
  * Waits for the PTY socket/port to become connectable.