@cluesmith/codev 3.3.2 → 3.3.4

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 (257) hide show
  1. package/dashboard-dist/assets/{index-D3XaKpZW.js → index-DZf6pNPE.js} +41 -41
  2. package/dashboard-dist/assets/index-DZf6pNPE.js.map +1 -0
  3. package/dashboard-dist/assets/index-DnYLy5C_.css +32 -0
  4. package/dashboard-dist/index.html +2 -2
  5. package/dist/agent-farm/cli.d.ts.map +1 -1
  6. package/dist/agent-farm/cli.js +48 -0
  7. package/dist/agent-farm/cli.js.map +1 -1
  8. package/dist/agent-farm/commands/ide.d.ts +20 -0
  9. package/dist/agent-farm/commands/ide.d.ts.map +1 -0
  10. package/dist/agent-farm/commands/ide.js +138 -0
  11. package/dist/agent-farm/commands/ide.js.map +1 -0
  12. package/dist/agent-farm/commands/inbox.d.ts.map +1 -1
  13. package/dist/agent-farm/commands/inbox.js +79 -10
  14. package/dist/agent-farm/commands/inbox.js.map +1 -1
  15. package/dist/agent-farm/commands/interrupt.d.ts +4 -1
  16. package/dist/agent-farm/commands/interrupt.d.ts.map +1 -1
  17. package/dist/agent-farm/commands/interrupt.js +6 -3
  18. package/dist/agent-farm/commands/interrupt.js.map +1 -1
  19. package/dist/agent-farm/commands/reset.d.ts +4 -1
  20. package/dist/agent-farm/commands/reset.d.ts.map +1 -1
  21. package/dist/agent-farm/commands/reset.js +6 -3
  22. package/dist/agent-farm/commands/reset.js.map +1 -1
  23. package/dist/agent-farm/commands/send.d.ts +28 -0
  24. package/dist/agent-farm/commands/send.d.ts.map +1 -1
  25. package/dist/agent-farm/commands/send.js +79 -6
  26. package/dist/agent-farm/commands/send.js.map +1 -1
  27. package/dist/agent-farm/commands/spawn-worktree.d.ts.map +1 -1
  28. package/dist/agent-farm/commands/spawn-worktree.js +16 -7
  29. package/dist/agent-farm/commands/spawn-worktree.js.map +1 -1
  30. package/dist/agent-farm/commands/tower.d.ts +21 -9
  31. package/dist/agent-farm/commands/tower.d.ts.map +1 -1
  32. package/dist/agent-farm/commands/tower.js +90 -30
  33. package/dist/agent-farm/commands/tower.js.map +1 -1
  34. package/dist/agent-farm/db/index.d.ts +3 -1
  35. package/dist/agent-farm/db/index.d.ts.map +1 -1
  36. package/dist/agent-farm/db/index.js +8 -443
  37. package/dist/agent-farm/db/index.js.map +1 -1
  38. package/dist/agent-farm/db/mailbox.d.ts +47 -7
  39. package/dist/agent-farm/db/mailbox.d.ts.map +1 -1
  40. package/dist/agent-farm/db/mailbox.js +56 -12
  41. package/dist/agent-farm/db/mailbox.js.map +1 -1
  42. package/dist/agent-farm/db/migrations.d.ts +57 -0
  43. package/dist/agent-farm/db/migrations.d.ts.map +1 -0
  44. package/dist/agent-farm/db/migrations.js +530 -0
  45. package/dist/agent-farm/db/migrations.js.map +1 -0
  46. package/dist/agent-farm/db/schema.d.ts +1 -1
  47. package/dist/agent-farm/db/schema.d.ts.map +1 -1
  48. package/dist/agent-farm/db/schema.js +1 -0
  49. package/dist/agent-farm/db/schema.js.map +1 -1
  50. package/dist/agent-farm/db/tower-owner.d.ts +168 -0
  51. package/dist/agent-farm/db/tower-owner.d.ts.map +1 -0
  52. package/dist/agent-farm/db/tower-owner.js +369 -0
  53. package/dist/agent-farm/db/tower-owner.js.map +1 -0
  54. package/dist/agent-farm/db/types.d.ts +47 -0
  55. package/dist/agent-farm/db/types.d.ts.map +1 -1
  56. package/dist/agent-farm/db/types.js.map +1 -1
  57. package/dist/agent-farm/lib/ide-record.d.ts +65 -0
  58. package/dist/agent-farm/lib/ide-record.d.ts.map +1 -0
  59. package/dist/agent-farm/lib/ide-record.js +118 -0
  60. package/dist/agent-farm/lib/ide-record.js.map +1 -0
  61. package/dist/agent-farm/lib/tunnel-client.d.ts +22 -0
  62. package/dist/agent-farm/lib/tunnel-client.d.ts.map +1 -1
  63. package/dist/agent-farm/lib/tunnel-client.js +96 -19
  64. package/dist/agent-farm/lib/tunnel-client.js.map +1 -1
  65. package/dist/agent-farm/servers/cron-delivery.d.ts +7 -1
  66. package/dist/agent-farm/servers/cron-delivery.d.ts.map +1 -1
  67. package/dist/agent-farm/servers/cron-delivery.js +2 -1
  68. package/dist/agent-farm/servers/cron-delivery.js.map +1 -1
  69. package/dist/agent-farm/servers/gate-profiles.d.ts +44 -2
  70. package/dist/agent-farm/servers/gate-profiles.d.ts.map +1 -1
  71. package/dist/agent-farm/servers/gate-profiles.js +79 -6
  72. package/dist/agent-farm/servers/gate-profiles.js.map +1 -1
  73. package/dist/agent-farm/servers/ide-forward.d.ts +62 -0
  74. package/dist/agent-farm/servers/ide-forward.d.ts.map +1 -0
  75. package/dist/agent-farm/servers/ide-forward.js +274 -0
  76. package/dist/agent-farm/servers/ide-forward.js.map +1 -0
  77. package/dist/agent-farm/servers/ide-server.d.ts +68 -0
  78. package/dist/agent-farm/servers/ide-server.d.ts.map +1 -0
  79. package/dist/agent-farm/servers/ide-server.js +281 -0
  80. package/dist/agent-farm/servers/ide-server.js.map +1 -0
  81. package/dist/agent-farm/servers/mailbox-delivery.d.ts +296 -12
  82. package/dist/agent-farm/servers/mailbox-delivery.d.ts.map +1 -1
  83. package/dist/agent-farm/servers/mailbox-delivery.js +566 -69
  84. package/dist/agent-farm/servers/mailbox-delivery.js.map +1 -1
  85. package/dist/agent-farm/servers/mailbox-wiring.d.ts +66 -19
  86. package/dist/agent-farm/servers/mailbox-wiring.d.ts.map +1 -1
  87. package/dist/agent-farm/servers/mailbox-wiring.js +204 -37
  88. package/dist/agent-farm/servers/mailbox-wiring.js.map +1 -1
  89. package/dist/agent-farm/servers/message-write.d.ts +138 -11
  90. package/dist/agent-farm/servers/message-write.d.ts.map +1 -1
  91. package/dist/agent-farm/servers/message-write.js +144 -35
  92. package/dist/agent-farm/servers/message-write.js.map +1 -1
  93. package/dist/agent-farm/servers/overview.d.ts +28 -12
  94. package/dist/agent-farm/servers/overview.d.ts.map +1 -1
  95. package/dist/agent-farm/servers/overview.js +130 -79
  96. package/dist/agent-farm/servers/overview.js.map +1 -1
  97. package/dist/agent-farm/servers/render-gate.d.ts +77 -1
  98. package/dist/agent-farm/servers/render-gate.d.ts.map +1 -1
  99. package/dist/agent-farm/servers/render-gate.js +174 -25
  100. package/dist/agent-farm/servers/render-gate.js.map +1 -1
  101. package/dist/agent-farm/servers/session-submit.d.ts +7 -4
  102. package/dist/agent-farm/servers/session-submit.d.ts.map +1 -1
  103. package/dist/agent-farm/servers/session-submit.js +7 -4
  104. package/dist/agent-farm/servers/session-submit.js.map +1 -1
  105. package/dist/agent-farm/servers/tower-cron.d.ts.map +1 -1
  106. package/dist/agent-farm/servers/tower-cron.js +7 -3
  107. package/dist/agent-farm/servers/tower-cron.js.map +1 -1
  108. package/dist/agent-farm/servers/tower-instances.d.ts.map +1 -1
  109. package/dist/agent-farm/servers/tower-instances.js +17 -10
  110. package/dist/agent-farm/servers/tower-instances.js.map +1 -1
  111. package/dist/agent-farm/servers/tower-routes.d.ts +32 -0
  112. package/dist/agent-farm/servers/tower-routes.d.ts.map +1 -1
  113. package/dist/agent-farm/servers/tower-routes.js +186 -26
  114. package/dist/agent-farm/servers/tower-routes.js.map +1 -1
  115. package/dist/agent-farm/servers/tower-server.js +79 -9
  116. package/dist/agent-farm/servers/tower-server.js.map +1 -1
  117. package/dist/agent-farm/servers/tower-terminals.d.ts +21 -0
  118. package/dist/agent-farm/servers/tower-terminals.d.ts.map +1 -1
  119. package/dist/agent-farm/servers/tower-terminals.js +106 -13
  120. package/dist/agent-farm/servers/tower-terminals.js.map +1 -1
  121. package/dist/agent-farm/servers/tower-utils.d.ts +35 -0
  122. package/dist/agent-farm/servers/tower-utils.d.ts.map +1 -1
  123. package/dist/agent-farm/servers/tower-utils.js +39 -0
  124. package/dist/agent-farm/servers/tower-utils.js.map +1 -1
  125. package/dist/agent-farm/servers/tower-websocket.d.ts.map +1 -1
  126. package/dist/agent-farm/servers/tower-websocket.js +26 -2
  127. package/dist/agent-farm/servers/tower-websocket.js.map +1 -1
  128. package/dist/agent-farm/utils/message-format.d.ts +78 -2
  129. package/dist/agent-farm/utils/message-format.d.ts.map +1 -1
  130. package/dist/agent-farm/utils/message-format.js +107 -11
  131. package/dist/agent-farm/utils/message-format.js.map +1 -1
  132. package/dist/agent-farm/utils/port.d.ts +22 -0
  133. package/dist/agent-farm/utils/port.d.ts.map +1 -0
  134. package/dist/agent-farm/utils/port.js +35 -0
  135. package/dist/agent-farm/utils/port.js.map +1 -0
  136. package/dist/agent-farm/utils/server-utils.d.ts +26 -0
  137. package/dist/agent-farm/utils/server-utils.d.ts.map +1 -1
  138. package/dist/agent-farm/utils/server-utils.js +36 -0
  139. package/dist/agent-farm/utils/server-utils.js.map +1 -1
  140. package/dist/cli.d.ts.map +1 -1
  141. package/dist/cli.js +3 -1
  142. package/dist/cli.js.map +1 -1
  143. package/dist/commands/consult/index.d.ts.map +1 -1
  144. package/dist/commands/consult/index.js +61 -1
  145. package/dist/commands/consult/index.js.map +1 -1
  146. package/dist/commands/consult/tree-tripwire.d.ts +177 -0
  147. package/dist/commands/consult/tree-tripwire.d.ts.map +1 -0
  148. package/dist/commands/consult/tree-tripwire.js +356 -0
  149. package/dist/commands/consult/tree-tripwire.js.map +1 -0
  150. package/dist/commands/doctor.d.ts +69 -0
  151. package/dist/commands/doctor.d.ts.map +1 -1
  152. package/dist/commands/doctor.js +165 -2
  153. package/dist/commands/doctor.js.map +1 -1
  154. package/dist/commands/generate-image.d.ts +25 -1
  155. package/dist/commands/generate-image.d.ts.map +1 -1
  156. package/dist/commands/generate-image.js +410 -2
  157. package/dist/commands/generate-image.js.map +1 -1
  158. package/dist/commands/porch/prompts.d.ts.map +1 -1
  159. package/dist/commands/porch/prompts.js +18 -2
  160. package/dist/commands/porch/prompts.js.map +1 -1
  161. package/dist/lib/agent-env.d.ts +86 -0
  162. package/dist/lib/agent-env.d.ts.map +1 -0
  163. package/dist/lib/agent-env.js +126 -0
  164. package/dist/lib/agent-env.js.map +1 -0
  165. package/dist/lib/forge-contracts.d.ts +76 -5
  166. package/dist/lib/forge-contracts.d.ts.map +1 -1
  167. package/dist/lib/forge-contracts.js.map +1 -1
  168. package/dist/lib/forge-rate-limit.d.ts +45 -0
  169. package/dist/lib/forge-rate-limit.d.ts.map +1 -0
  170. package/dist/lib/forge-rate-limit.js +89 -0
  171. package/dist/lib/forge-rate-limit.js.map +1 -0
  172. package/dist/lib/forge.d.ts +29 -1
  173. package/dist/lib/forge.d.ts.map +1 -1
  174. package/dist/lib/forge.js +96 -21
  175. package/dist/lib/forge.js.map +1 -1
  176. package/dist/lib/github.d.ts +13 -1
  177. package/dist/lib/github.d.ts.map +1 -1
  178. package/dist/lib/github.js +58 -15
  179. package/dist/lib/github.js.map +1 -1
  180. package/dist/lib/test-env.d.ts +17 -0
  181. package/dist/lib/test-env.d.ts.map +1 -1
  182. package/dist/lib/test-env.js +20 -0
  183. package/dist/lib/test-env.js.map +1 -1
  184. package/dist/terminal/pty-manager.d.ts +10 -1
  185. package/dist/terminal/pty-manager.d.ts.map +1 -1
  186. package/dist/terminal/pty-manager.js +38 -9
  187. package/dist/terminal/pty-manager.js.map +1 -1
  188. package/dist/terminal/pty-session.d.ts +174 -8
  189. package/dist/terminal/pty-session.d.ts.map +1 -1
  190. package/dist/terminal/pty-session.js +299 -25
  191. package/dist/terminal/pty-session.js.map +1 -1
  192. package/dist/terminal/session-manager.d.ts +23 -2
  193. package/dist/terminal/session-manager.d.ts.map +1 -1
  194. package/dist/terminal/session-manager.js +115 -36
  195. package/dist/terminal/session-manager.js.map +1 -1
  196. package/dist/terminal/session-screen.d.ts +34 -5
  197. package/dist/terminal/session-screen.d.ts.map +1 -1
  198. package/dist/terminal/session-screen.js +52 -6
  199. package/dist/terminal/session-screen.js.map +1 -1
  200. package/dist/terminal/shellper-client.d.ts +73 -0
  201. package/dist/terminal/shellper-client.d.ts.map +1 -1
  202. package/dist/terminal/shellper-client.js +151 -0
  203. package/dist/terminal/shellper-client.js.map +1 -1
  204. package/dist/terminal/shellper-main.js +20 -3
  205. package/dist/terminal/shellper-main.js.map +1 -1
  206. package/dist/terminal/shellper-process.d.ts +2 -0
  207. package/dist/terminal/shellper-process.d.ts.map +1 -1
  208. package/dist/terminal/shellper-process.js +17 -1
  209. package/dist/terminal/shellper-process.js.map +1 -1
  210. package/dist/terminal/shellper-protocol.d.ts +28 -0
  211. package/dist/terminal/shellper-protocol.d.ts.map +1 -1
  212. package/dist/terminal/shellper-protocol.js.map +1 -1
  213. package/dist/terminal/terminal-replies.d.ts +102 -0
  214. package/dist/terminal/terminal-replies.d.ts.map +1 -0
  215. package/dist/terminal/terminal-replies.js +181 -0
  216. package/dist/terminal/terminal-replies.js.map +1 -0
  217. package/package.json +4 -4
  218. package/scripts/forge/gitea/_lib.sh +214 -0
  219. package/scripts/forge/gitea/issue-comment.sh +8 -1
  220. package/scripts/forge/gitea/issue-view.sh +95 -4
  221. package/scripts/forge/gitea/pr-create.sh +155 -0
  222. package/scripts/forge/gitea/pr-exists.sh +32 -4
  223. package/scripts/forge/gitea/pr-list.sh +40 -31
  224. package/scripts/forge/gitea/pr-view.sh +65 -4
  225. package/scripts/forge/gitea/recently-merged.sh +91 -21
  226. package/scripts/forge/gitea/user-identity.sh +30 -1
  227. package/scripts/forge/github/issue-view.sh +5 -2
  228. package/scripts/forge/github/pr-create.sh +47 -0
  229. package/scripts/forge/github/pr-list.sh +6 -2
  230. package/scripts/forge/github/pr-search.sh +6 -2
  231. package/scripts/forge/gitlab/issue-search.sh +1 -0
  232. package/scripts/forge/gitlab/issue-view.sh +24 -4
  233. package/scripts/forge/gitlab/pr-create.sh +53 -0
  234. package/scripts/forge/gitlab/pr-search.sh +37 -2
  235. package/scripts/forge/linear/auth-status.sh +1 -0
  236. package/scripts/forge/linear/issue-comment.sh +1 -0
  237. package/scripts/forge/linear/issue-list.sh +1 -0
  238. package/scripts/forge/linear/issue-search.sh +1 -0
  239. package/scripts/forge/linear/issue-view.sh +14 -2
  240. package/scripts/forge/linear/recently-closed.sh +1 -0
  241. package/scripts/forge/linear/user-identity.sh +1 -0
  242. package/skeleton/.claude/skills/generate-image/SKILL.md +39 -1
  243. package/skeleton/.codex/skills/generate-image/SKILL.md +39 -1
  244. package/skeleton/protocols/air/prompts/pr.md +6 -1
  245. package/skeleton/protocols/aspir/prompts/review.md +6 -1
  246. package/skeleton/protocols/bugfix/prompts/pr.md +6 -1
  247. package/skeleton/protocols/bugfix/protocol.md +2 -1
  248. package/skeleton/protocols/maintain/prompts/review.md +6 -1
  249. package/skeleton/protocols/pir/prompts/review.md +10 -7
  250. package/skeleton/protocols/spir/prompts/review.md +6 -1
  251. package/skeleton/resources/commands/agent-farm.md +28 -3
  252. package/skeleton/roles/consultant.md +32 -0
  253. package/skeleton/templates/AGENTS.md +1 -1
  254. package/skeleton/templates/CLAUDE.md +1 -1
  255. package/templates/tower.html +16 -1
  256. package/dashboard-dist/assets/index-BZRTEMtH.css +0 -32
  257. package/dashboard-dist/assets/index-D3XaKpZW.js.map +0 -1
@@ -21,13 +21,16 @@ import { decodeWorkspacePath } from '../lib/tower-client.js';
21
21
  import { readCloudConfig } from '../lib/cloud-config.js';
22
22
  import { fileURLToPath } from 'node:url';
23
23
  import { version } from '../../version.js';
24
+ import { sanitizeAgentEnv } from '../../lib/agent-env.js';
24
25
  const execAsync = promisify(exec);
25
26
  import { getBuilders, setArchitectByName } from '../state.js';
26
27
  import { DEFAULT_COLS, defaultSessionOptions } from '../../terminal/index.js';
27
28
  import { parseJsonBody, isRequestAllowed, isAllowedOrigin, isAllowedHost, getExpectedKey, escapeHtml } from '../utils/server-utils.js';
28
29
  import { TOWER_KEY_HEADER, LEGACY_WEB_KEY_HEADER, VSCODE_USER_SENDER } from '@cluesmith/codev-types';
29
- import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, } from './tower-utils.js';
30
+ import { isRateLimited, normalizeWorkspacePath, getLanguageForExt, getMimeTypeForFile, serveStaticFile, persistableCommand, logSessionIdentity, } from './tower-utils.js';
30
31
  import { handleTunnelEndpoint } from './tower-tunnel.js';
32
+ import { handleIdeApi } from './ide-server.js';
33
+ import { matchIdePrefix, forwardIdeHttp } from './ide-forward.js';
31
34
  import { getWorktreeConfig, getActivityHooks } from '../utils/config.js';
32
35
  import { ensureCodevConfigWatcher } from './codev-config-watcher.js';
33
36
  import { hasTeam, loadTeamMembers, loadMessages } from '../../lib/team.js';
@@ -35,12 +38,13 @@ import { fetchTeamGitHubData } from '../../lib/team-github.js';
35
38
  import { resolveTarget, resolveAgentInRegistry, broadcastMessage, isResolveError } from './tower-messages.js';
36
39
  import { handleCommandRoute, COMMAND_ROUTE } from './command-relay.js';
37
40
  import { handleCanvasRoute, CANVAS_ROUTE_PREFIX } from './canvas-relay.js';
38
- import { formatArchitectMessage, formatArchitectToBuilderMessage, formatBuilderMessage, formatUserViaVsCodeMessage, messageTooLargeError, MAX_MESSAGE_BYTES, } from '../utils/message-format.js';
39
- import { writeMessageToSession, writeEscapeToSession } from './message-write.js';
40
- import { makeDeliveryPorts, getMailboxDrainer } from './mailbox-wiring.js';
41
+ import { formatArchitectMessage, formatArchitectToBuilderMessage, formatBuilderMessage, formatUserViaVsCodeMessage, isSafeSenderId, messageTooLargeError, MAX_MESSAGE_BYTES, } from '../utils/message-format.js';
42
+ import { writeMessageToSession, writeEscapeToSession, writeStrategyForApp } from './message-write.js';
43
+ import { makeDeliveryPorts, getMailboxDrainer, resolveProfileForSession } from './mailbox-wiring.js';
41
44
  import { deliverAgentMailSerialized } from './mailbox-delivery.js';
42
45
  import { deliverCronMail, CRON_SENDER } from './cron-delivery.js';
43
46
  import { enqueue as enqueueMailbox, getById as getMailboxById, markDelivered as markMailboxDelivered, listHeld as listHeldMailbox, dismiss as dismissMailbox, } from '../db/mailbox.js';
47
+ import { formatVerdict } from '@cluesmith/codev-sdk/hold-verdict';
44
48
  // Spec 1273 per-terminal submission lock. Originally the anti-fusion lock for the two
45
49
  // explicit human-bypass paths (escape + interrupt); since Issue #1365 the gated mailbox
46
50
  // delivery path takes it too, so it now serializes the whole write edge of a terminal.
@@ -54,7 +58,7 @@ import { submitToSession, OPERATOR_SUBMIT_WAIT_CEILING_MS } from './session-subm
54
58
  import { scheduleDelayedSend, validateDelaySeconds } from './delayed-send.js';
55
59
  import { getKnownWorkspacePaths, getInstances, getDirectorySuggestions, launchInstance, killTerminalWithShellper, instancesReady, stopInstance, addArchitect, removeArchitect, } from './tower-instances.js';
56
60
  import { OverviewCache } from './overview.js';
57
- import { fetchIssue, fetchPR, searchIssues, fetchPRList, fetchCurrentUser, parseLinkedIssue, parseArea, } from '../../lib/github.js';
61
+ import { fetchIssue, fetchPR, searchIssues, fetchPRList, fetchCurrentUser, probeGhGraphqlRateLimit, parseLinkedIssue, parseArea, } from '../../lib/github.js';
58
62
  import { computeAnalytics } from './analytics.js';
59
63
  import { getAllTasks, executeTask, getTaskId } from './tower-cron.js';
60
64
  import { getGlobalDb } from '../db/index.js';
@@ -65,7 +69,8 @@ import { getWorkspaceTerminals, getTerminalManager, getWorkspaceTerminalsEntry,
65
69
  const __filename = fileURLToPath(import.meta.url);
66
70
  const __dirname = path.dirname(__filename);
67
71
  // Singleton cache for overview endpoint (Spec 0126 Phase 4)
68
- const overviewCache = new OverviewCache();
72
+ // #1645: the gh reset probe is enabled here, in Tower, and nowhere else.
73
+ const overviewCache = new OverviewCache({ probe: probeGhGraphqlRateLimit });
69
74
  const ROUTES = {
70
75
  'GET /health': (_req, res, _url, ctx) => handleHealthCheck(res, ctx),
71
76
  'GET /api/shellpers/husks': (_req, res, _url, ctx) => handleHuskPreview(res, ctx),
@@ -158,6 +163,18 @@ export async function handleRequest(req, res, ctx) {
158
163
  return await handler(req, res, url, ctx);
159
164
  }
160
165
  // Pattern-based routes (require regex or prefix matching)
166
+ // IDE management endpoint: /api/ide (Issue #1668). Local-only — blocked from
167
+ // the tunnel in tunnel-client (like /api/tunnel/*) — and reached only after
168
+ // the isRequestAllowed key check above. Drives the IDE server lifecycle.
169
+ if (url.pathname === '/api/ide') {
170
+ return await handleIdeApi(req, res, ctx.log);
171
+ }
172
+ // IDE prefix forward: /ide/* → local IDE server (Issue #1668). A post-auth
173
+ // handler: isRequestAllowed above is the whole trust boundary (the forward
174
+ // adds no per-folder authorization). Path + query forwarded unchanged.
175
+ if (matchIdePrefix(url.pathname)) {
176
+ return await forwardIdeHttp(req, res, ctx.log);
177
+ }
161
178
  // Tunnel endpoints: /api/tunnel/* (Spec 0097 Phase 4)
162
179
  if (url.pathname.startsWith('/api/tunnel/')) {
163
180
  const tunnelSub = url.pathname.slice('/api/tunnel/'.length);
@@ -609,9 +626,11 @@ async function handleTerminalCreate(req, res, ctx) {
609
626
  if (requestPersistence && shellperManager && command && cwd) {
610
627
  try {
611
628
  const sessionId = crypto.randomUUID();
612
- // Strip CLAUDECODE so spawned Claude processes don't detect nesting
613
- const sessionEnv = { ...(env || process.env) };
614
- delete sessionEnv['CLAUDECODE'];
629
+ // Strip Claude Code session markers so spawned Claude processes don't
630
+ // detect nesting and disable transcript saving (#1219). `env` is normally
631
+ // absent — `createPtySession` sends no env — so this is the path by which
632
+ // Tower's own environment becomes every builder's environment.
633
+ const sessionEnv = sanitizeAgentEnv(env || process.env);
615
634
  const client = await shellperManager.createSession({
616
635
  sessionId,
617
636
  command,
@@ -650,7 +669,12 @@ async function handleTerminalCreate(req, res, ctx) {
650
669
  else {
651
670
  entry.shells.set(roleId, session.id);
652
671
  }
653
- saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, label ?? null, cwd ?? null, command ?? null);
672
+ // PIR #1475: prefer the identity the shellper reports. A fresh spawn is
673
+ // the no-op case (it reports back what we just asked for), but routing
674
+ // it through the same accessor keeps one rule everywhere: the row
675
+ // records what is RUNNING, not what was requested.
676
+ logSessionIdentity(ctx.log, 'terminal-create', session.id, ptySession, command ?? null);
677
+ saveTerminalSession(session.id, workspacePath, termType, roleId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, label ?? null, cwd ?? null, persistableCommand(ptySession) ?? command ?? null);
654
678
  ctx.log('INFO', `Registered shellper terminal ${session.id} as ${termType} "${roleId}" for workspace ${workspacePath}`);
655
679
  }
656
680
  }
@@ -661,7 +685,7 @@ async function handleTerminalCreate(req, res, ctx) {
661
685
  // Fallback: non-persistent session (graceful degradation per plan)
662
686
  // Shellper is the only persistence backend for new sessions.
663
687
  if (!info) {
664
- info = await manager.createSession({ command, args, cols, rows, cwd, env, label });
688
+ info = await manager.createSession({ command, args, cols, rows, cwd, env: env && sanitizeAgentEnv(env), label });
665
689
  persistent = false;
666
690
  if (workspacePath && termType && roleId) {
667
691
  const entry = getWorkspaceTerminalsEntry(normalizeWorkspacePath(workspacePath));
@@ -744,6 +768,9 @@ async function handleTerminalRoutes(req, res, url, match) {
744
768
  res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
745
769
  return;
746
770
  }
771
+ // Deliberately the default `'external'` origin (Issue #1473): this is a foreign writer, so
772
+ // its bytes count as input at the delivery gate and are reply-filtered on the way — both
773
+ // for free, because the recording lives inside `write()` rather than at each call site.
747
774
  session.write(body.data);
748
775
  res.writeHead(200, { 'Content-Type': 'application/json' });
749
776
  res.end(JSON.stringify({ ok: true }));
@@ -765,8 +792,20 @@ async function handleTerminalRoutes(req, res, url, match) {
765
792
  }
766
793
  const info = manager.resizeSession(terminalId, body.cols, body.rows);
767
794
  if (!info) {
768
- res.writeHead(404, { 'Content-Type': 'application/json' });
769
- res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
795
+ // Issue #1482: a resize that did not reach the process is NOT a missing session, and it
796
+ // is not a success either — echoing back the requested dimensions (what a 200 here used
797
+ // to do) is how Tower came to believe a geometry the TUI never adopted.
798
+ if (!manager.getSession(terminalId)) {
799
+ res.writeHead(404, { 'Content-Type': 'application/json' });
800
+ res.end(JSON.stringify({ error: 'NOT_FOUND', message: `Session ${terminalId} not found` }));
801
+ }
802
+ else {
803
+ res.writeHead(409, { 'Content-Type': 'application/json' });
804
+ res.end(JSON.stringify({
805
+ error: 'RESIZE_DROPPED',
806
+ message: `Session ${terminalId} did not accept the resize (no live process or dropped shellper write); dimensions unchanged`,
807
+ }));
808
+ }
770
809
  return;
771
810
  }
772
811
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -1282,6 +1321,11 @@ function liveTargetIdentity(result) {
1282
1321
  * architect reverse-map for architect targets, the resolved agent id otherwise),
1283
1322
  * so the header a recipient reads names the same identity the row was addressed
1284
1323
  * to. Passing anything else here would make the frame attest to a fiction.
1324
+ *
1325
+ * #1478: `from` is the SENDER, and it is threaded on the any → builder path too —
1326
+ * that branch used to discard it, so an `architect:<name>` origin reached the
1327
+ * builder's composer as a bare `ARCHITECT INSTRUCTION`. The two are complementary:
1328
+ * #1574 answers "was this addressed to me?", #1478 answers "who sent it?".
1285
1329
  */
1286
1330
  function formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw) {
1287
1331
  // #1494: a human's approval relayed by the VS Code extension gets its own
@@ -1290,8 +1334,12 @@ function formatMessageForTarget(isArchitectTarget, from, toAgent, message, raw)
1290
1334
  return formatUserViaVsCodeMessage(toAgent, message, undefined, raw);
1291
1335
  if (isArchitectTarget && from)
1292
1336
  return formatBuilderMessage(from, toAgent, message, undefined, raw); // builder → architect
1337
+ // any → builder: thread the sender so an `architect:<name>` origin is named in the
1338
+ // header (issue #1478) instead of collapsing to a bare `ARCHITECT INSTRUCTION`.
1293
1339
  if (!isArchitectTarget)
1294
- return formatArchitectToBuilderMessage(toAgent, message, undefined, raw); // any → builder
1340
+ return formatArchitectToBuilderMessage(toAgent, message, undefined, raw, from); // any → builder
1341
+ // unknown → architect: `from` is absent by construction on this branch, so there is
1342
+ // no sender to name.
1295
1343
  return raw ? message : formatArchitectMessage(toAgent, message, undefined, false); // unknown → architect
1296
1344
  }
1297
1345
  /**
@@ -1347,7 +1395,7 @@ export async function deliverCronMessage(task, message, log) {
1347
1395
  }
1348
1396
  }
1349
1397
  log('WARN', `Cron '${task.name}': target '${task.target}' not found — message not delivered`);
1350
- return { outcome: 'unresolved', reason: null, mailboxId: null };
1398
+ return { outcome: 'unresolved', reason: null, detail: null, mailboxId: null };
1351
1399
  }
1352
1400
  /**
1353
1401
  * Persist a `held` mailbox row and write the Spec 1313 `held` send response. Used
@@ -1368,6 +1416,10 @@ function holdAndRespond(res, ctx, input, reason) {
1368
1416
  delivered: false,
1369
1417
  held: true,
1370
1418
  reason,
1419
+ // A hold from THIS path is always a non-gate one (no live PTY), so it carries no gate
1420
+ // detail (Issue #1482). Reported explicitly rather than omitted so the held response has
1421
+ // one shape whichever path produced it.
1422
+ detail: null,
1371
1423
  mailboxId: row.id,
1372
1424
  bodyLength: Buffer.byteLength(input.body, 'utf8'),
1373
1425
  });
@@ -1561,6 +1613,25 @@ async function handleSend(req, res, ctx) {
1561
1613
  }
1562
1614
  // Optional fields
1563
1615
  const from = typeof body.from === 'string' ? body.from : undefined;
1616
+ // Maintainer review (PR #1486): `from` was the one unguarded field on this route — the
1617
+ // body above is bounded by MAX_MESSAGE_BYTES, but the sender was taken verbatim, and
1618
+ // `/api/send` is a public local route whose only well-behaved caller is the CLI. It is
1619
+ // stored on the mailbox row and rendered by every attribution surface, and `afx inbox`
1620
+ // now sizes its `FROM → TO` column to the widest value it holds — so an unbounded
1621
+ // identity is a rendering cost paid by every row, not just its own. Enforced with the
1622
+ // SAME predicate the header formatter uses, so the boundary and the renderer cannot
1623
+ // disagree about what an identity is. Refused rather than silently dropped or
1624
+ // truncated: a message delivered under a quietly-discarded sender is exactly the
1625
+ // unattributed frame this PR exists to fix.
1626
+ if (from !== undefined && !isSafeSenderId(from)) {
1627
+ res.writeHead(400, { 'Content-Type': 'application/json' });
1628
+ res.end(JSON.stringify({
1629
+ error: 'INVALID_PARAMS',
1630
+ message: 'Invalid "from" field: a sender identity must be 1-128 characters of ' +
1631
+ '[A-Za-z0-9._:-] (no whitespace, brackets or newlines).',
1632
+ }));
1633
+ return;
1634
+ }
1564
1635
  const workspace = typeof body.workspace === 'string' ? body.workspace : undefined;
1565
1636
  const fromWorkspace = typeof body.fromWorkspace === 'string' ? body.fromWorkspace : undefined;
1566
1637
  const options = typeof body.options === 'object' && body.options !== null
@@ -1787,7 +1858,10 @@ async function handleSend(req, res, ctx) {
1787
1858
  let degraded = false;
1788
1859
  await submitToSession(result.terminalId, () => {
1789
1860
  session.write('\x03'); // Ctrl+C
1790
- return writeMessageToSession(session, formattedMessage, noEnter, 100);
1861
+ // Issue #1567: the same per-harness write strategy the gated path uses — an
1862
+ // interrupt to an opted-out harness must not be bracketed just because it bypassed
1863
+ // the gate.
1864
+ return writeMessageToSession(session, formattedMessage, noEnter, 100, writeStrategyForApp(resolveProfileForSession(session)?.app));
1791
1865
  }, undefined, {
1792
1866
  waitCeilingMs: OPERATOR_SUBMIT_WAIT_CEILING_MS,
1793
1867
  onCeilingExpired: (waitedMs) => {
@@ -1813,6 +1887,7 @@ async function handleSend(req, res, ctx) {
1813
1887
  held: false,
1814
1888
  mailboxId: row.id,
1815
1889
  reason: null,
1890
+ detail: null,
1816
1891
  bodyLength,
1817
1892
  // Additive and present only when it happened, so older clients are unaffected.
1818
1893
  ...(degraded ? { degraded: true, degradedReason: DEGRADED_SUBMIT_REASON } : {}),
@@ -1842,14 +1917,25 @@ async function handleSend(req, res, ctx) {
1842
1917
  ...basePorts,
1843
1918
  getSessionForAgent: (ws, agent) => ws === result.workspacePath && agent === toAgent ? session : basePorts.getSessionForAgent(ws, agent),
1844
1919
  };
1920
+ // Issue #1584: keep the outcome — it carries whether the delivered message's header was
1921
+ // actually seen on the receiving terminal, which is the only thing the sender can act on now
1922
+ // that an unconfirmed delivery is recorded rather than re-written.
1923
+ let outcome = null;
1845
1924
  try {
1846
- await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
1925
+ outcome = await deliverAgentMailSerialized(ports, db, result.workspacePath, toAgent);
1847
1926
  }
1848
1927
  catch (err) {
1849
1928
  // A gate/write error leaves the row HELD (markDelivered only runs on a
1850
1929
  // completed write); the backstop drainer will retry. Report held, not a 500.
1851
1930
  ctx.log('ERROR', `Delivery attempt errored for ${toAgent} (row ${row.id.slice(0, 8)}... stays held): ${err.message}`);
1852
1931
  }
1932
+ // Issue #1473: this pass ran OUTSIDE the drainer, so hand it the outcome — otherwise a hold
1933
+ // on recent terminal input arms no re-drain here, and the send an operator is watching falls
1934
+ // through to the quiescence trigger or the backstop instead of clearing after one settle.
1935
+ // That is exactly the latency the re-drain exists to remove, in the case where somebody is
1936
+ // waiting for it. A no-op for every other outcome.
1937
+ if (outcome)
1938
+ getMailboxDrainer().noteOutcome(result.workspacePath, toAgent, outcome);
1853
1939
  const stored = getMailboxById(db, row.id);
1854
1940
  if (stored?.status === 'delivered') {
1855
1941
  ctx.log('INFO', `Message delivered: ${from ?? 'unknown'} → ${toAgent} (terminal ${result.terminalId.slice(0, 8)}...)`);
@@ -1863,11 +1949,33 @@ async function handleSend(req, res, ctx) {
1863
1949
  mailboxId: row.id,
1864
1950
  reason: null,
1865
1951
  bodyLength,
1952
+ // Additive (Issue #1584). `false` means the bytes were written and accepted but the header
1953
+ // never appeared on the terminal — delivered, flagged, and NOT re-written. Reported only
1954
+ // when THIS row is the one our own pass delivered: a pass picks the agent's OLDEST held
1955
+ // row, so without the id check a concurrent drainer delivery of this row could be
1956
+ // reported with another message's verification. Absent (verification skipped, or someone
1957
+ // else's pass delivered it) reads exactly as it did before this field existed.
1958
+ ...(outcome?.delivered.includes(row.id) && outcome.verified !== undefined
1959
+ ? { verified: outcome.verified }
1960
+ : {}),
1961
+ // Issue #1473. `verified` alone was not enough for the SENDER — the case that motivated
1962
+ // it is a human's Enter submitting our half-written body, where the header landed, so
1963
+ // `verified` is `true` (or absent) and this human read a plain "Message delivered". The
1964
+ // cause says which flag it is: `input-raced` (may be truncated or submitted early) or
1965
+ // `no-echo` (header never appeared). Same id guard as `verified` — reported only when
1966
+ // THIS pass is the one that delivered THIS row.
1967
+ ...(outcome?.delivered.includes(row.id) && outcome.unverifiedCause !== undefined
1968
+ ? { unverifiedCause: outcome.unverifiedCause }
1969
+ : {}),
1866
1970
  });
1867
1971
  return;
1868
1972
  }
1869
1973
  const reason = stored?.reason ?? 'busy';
1870
- ctx.log('INFO', `Message held (${reason}): ${from ?? 'unknown'} → ${toAgent} (mailbox ${row.id.slice(0, 8)}...)`);
1974
+ // Issue #1482: read the detail back off the row the delivery pass just refreshed, rather
1975
+ // than re-deriving it — the row is the record every other surface reads, so the sender's
1976
+ // response and `afx inbox` can never disagree about the same hold.
1977
+ const detail = stored?.detail ?? null;
1978
+ ctx.log('INFO', `Message held (${formatVerdict(reason, detail)}): ${from ?? 'unknown'} → ${toAgent} (mailbox ${row.id.slice(0, 8)}...)`);
1871
1979
  // The message stayed held → a new held row is in the set; refresh the indicator
1872
1980
  // count (Spec 1313, Phase 7). The delivered branch above needs no fire — the
1873
1981
  // delivery path's onHeldStateChange already broadcast when the row left the set.
@@ -1880,6 +1988,7 @@ async function handleSend(req, res, ctx) {
1880
1988
  delivered: false,
1881
1989
  held: true,
1882
1990
  reason,
1991
+ detail,
1883
1992
  mailboxId: row.id,
1884
1993
  bodyLength,
1885
1994
  });
@@ -1894,20 +2003,55 @@ async function handleSend(req, res, ctx) {
1894
2003
  * redaction rule): id, addresses, why-held reason, escalation flag, and enqueue time —
1895
2004
  * the message BODY is deliberately never surfaced here (it travels only over the live
1896
2005
  * terminal stream on delivery). `escalated` is normalized from SQLite's 0/1 to a bool.
2006
+ *
2007
+ * Issue 1450: `workspaceOverride` lets the workspace-scoped route
2008
+ * (`/workspace/<base64>/api/inbox`, which backs the dashboard's held-mail popover) pass the
2009
+ * workspace resolved from the URL prefix, exactly as `handleOverview` / `handleAnalytics` do.
2010
+ * The override arrives already normalized by the prefix decoder, so `normalizeWorkspacePath`
2011
+ * re-running on it is a safe no-op.
2012
+ *
2013
+ * Two separate widening hazards are closed here, because a scoped caller must never receive
2014
+ * another workspace's held mail:
2015
+ * - `??` (not `||`) means `?workspace=` can never take effect once an override was passed.
2016
+ * - An EMPTY override does not fall through to the unscoped all-workspaces listing; it
2017
+ * scopes to a path that matches nothing. Today the dispatcher 400s a missing prefix so
2018
+ * this is unreachable, but "unreachable" is a property of the caller, not of this
2019
+ * function, and the safe failure for a scoped call is zero rows, never every row.
2020
+ * The all-workspaces listing therefore remains reachable only when NO override was passed —
2021
+ * the Tower-level route without `?workspace=`, the direct-caller convenience the CLI never uses.
2022
+ *
2023
+ * NOTE: this lists ALL held rows, including pre-due `--delay` rows, while the badge count
2024
+ * (`heldSummaryForWorkspace`) excludes them — see the `HeldMessage` type for why the two
2025
+ * legitimately disagree.
1897
2026
  */
1898
- function handleInboxList(res, url) {
1899
- const rawWorkspace = url.searchParams.get('workspace');
2027
+ // Exported as a narrow test seam (Issue 1450): the empty-override branch below cannot be
2028
+ // reached through `handleRequest` — the workspace dispatcher rejects a missing or non-absolute
2029
+ // prefix with a 400 before this runs — so the only way to pin that guard's behaviour is to call
2030
+ // the handler directly. Production callers go through the two route tables, not this export.
2031
+ export function handleInboxList(res, url, workspaceOverride) {
2032
+ const scoped = workspaceOverride !== undefined;
2033
+ const rawWorkspace = workspaceOverride ?? url.searchParams.get('workspace');
1900
2034
  // Normalize to the stored realpath key (mailbox workspace_path is normalized at
1901
2035
  // enqueue — tower-routes handleSend / holdAndRespond — matching overview.ts). Without
1902
2036
  // this a symlinked workspace root would miss its own held rows.
1903
- const workspace = rawWorkspace ? normalizeWorkspacePath(rawWorkspace) : undefined;
2037
+ // `scoped ? '' : undefined` on the empty branch: '' matches no workspace_path, so a scoped
2038
+ // call with a blank override returns nothing rather than widening to every workspace.
2039
+ const workspace = rawWorkspace
2040
+ ? normalizeWorkspacePath(rawWorkspace)
2041
+ : (scoped ? '' : undefined);
1904
2042
  const rows = listHeldMailbox(getGlobalDb(), workspace);
2043
+ // Annotated, not inferred: `HeldMessage` is the shared contract this route owes its
2044
+ // clients, so a projection that drifts from it (a dropped field, or a `body` slipping in)
2045
+ // fails the server build rather than only surprising the dashboard at runtime.
1905
2046
  const projected = rows.map((r) => ({
1906
2047
  id: r.id,
1907
2048
  workspacePath: r.workspace_path,
1908
2049
  toAgent: r.to_agent,
1909
2050
  fromAgent: r.from_agent,
1910
2051
  reason: r.reason,
2052
+ // Issue #1482: the gate verdict behind a `busy` hold, so `afx inbox` and the dashboard
2053
+ // popover can distinguish an occupied composer from one the classifier could not read.
2054
+ detail: r.detail,
1911
2055
  escalated: r.escalated === 1,
1912
2056
  createdAt: r.created_at,
1913
2057
  // Spec 1313 round 3: due time of a pre-due delayed (`--delay`) row; null = deliver-ASAP.
@@ -1973,6 +2117,7 @@ function handleInboxShow(req, res, match) {
1973
2117
  fromWorkspace: row.from_workspace,
1974
2118
  status: row.status,
1975
2119
  reason: row.reason,
2120
+ detail: row.detail, // Issue #1482: the gate verdict behind a `busy` hold
1976
2121
  escalated: row.escalated === 1,
1977
2122
  body: row.body,
1978
2123
  createdAt: row.created_at,
@@ -2392,6 +2537,19 @@ async function handleWorkspaceRoutes(req, res, ctx, url) {
2392
2537
  if (req.method === 'GET' && apiPath === 'overview') {
2393
2538
  return handleOverview(res, url, workspacePath, ctx);
2394
2539
  }
2540
+ // GET /api/inbox - held mailbox rows for THIS workspace (Issue 1450). Backs the
2541
+ // dashboard's clickable held-mail counter. Reuses the Tower-level handler with the
2542
+ // workspace resolved from the /workspace/<base64>/ prefix, the same way `overview`
2543
+ // and `analytics` above do — the dashboard calls relative `./api/...` and has no
2544
+ // absolute workspace path of its own to pass as `?workspace=`.
2545
+ //
2546
+ // Deliberately an EXACT match on 'inbox', not a prefix: `inbox/:id` (which returns the
2547
+ // message BODY) and `inbox/:id/dismiss` (which mutates) do not match, fall through to
2548
+ // the 404 below, and so stay off the dashboard surface. That is what keeps this read
2549
+ // path inside Spec 1313's redaction rule and decision 8 (dismissal is CLI-only).
2550
+ if (req.method === 'GET' && apiPath === 'inbox') {
2551
+ return handleInboxList(res, url, workspacePath);
2552
+ }
2395
2553
  // POST /api/overview/refresh - Invalidate overview cache (Spec 0126 Phase 4)
2396
2554
  if (req.method === 'POST' && apiPath === 'overview/refresh') {
2397
2555
  return handleOverviewRefresh(res, ctx);
@@ -2576,9 +2734,9 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
2576
2734
  if (shellperManager) {
2577
2735
  try {
2578
2736
  const sessionId = crypto.randomUUID();
2579
- // Strip CLAUDECODE so spawned Claude processes don't detect nesting
2580
- const shellEnv = { ...process.env };
2581
- delete shellEnv['CLAUDECODE'];
2737
+ // Strip Claude Code session markers so spawned Claude processes don't
2738
+ // detect nesting and disable transcript saving (#1219).
2739
+ const shellEnv = sanitizeAgentEnv(process.env);
2582
2740
  // Inject session identity for afx rename (Spec 468)
2583
2741
  shellEnv['SHELLPER_SESSION_ID'] = sessionId;
2584
2742
  shellEnv['TOWER_PORT'] = String(ctx.port);
@@ -2612,7 +2770,9 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
2612
2770
  }
2613
2771
  const entry = getWorkspaceTerminalsEntry(workspacePath);
2614
2772
  entry.shells.set(shellId, session.id);
2615
- saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, session.label, workspacePath, shellCmd);
2773
+ // PIR #1475: hydrated identity wins; a fresh spawn reports back `shellCmd`.
2774
+ logSessionIdentity(ctx.log, 'shell-create', session.id, ptySession, shellCmd);
2775
+ saveTerminalSession(session.id, workspacePath, 'shell', shellId, shellperInfo.pid, shellperInfo.socketPath, shellperInfo.pid, shellperInfo.startTime, session.label, workspacePath, persistableCommand(ptySession) ?? shellCmd);
2616
2776
  shellCreated = true;
2617
2777
  res.writeHead(200, { 'Content-Type': 'application/json' });
2618
2778
  res.end(JSON.stringify({
@@ -2637,7 +2797,7 @@ async function handleWorkspaceShellCreate(res, ctx, workspacePath) {
2637
2797
  args: shellArgs,
2638
2798
  cwd: workspacePath,
2639
2799
  label: `Shell ${shellId.replace('shell-', '')}`,
2640
- env: process.env,
2800
+ env: sanitizeAgentEnv(process.env),
2641
2801
  });
2642
2802
  const entry = getWorkspaceTerminalsEntry(workspacePath);
2643
2803
  entry.shells.set(shellId, session.id);