comisai 1.0.36 → 1.0.37

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 (239) hide show
  1. package/node_modules/@comis/agent/dist/background/auto-background-middleware.js +9 -0
  2. package/node_modules/@comis/agent/dist/background/background-task-manager.d.ts +22 -2
  3. package/node_modules/@comis/agent/dist/background/background-task-manager.js +48 -41
  4. package/node_modules/@comis/agent/dist/background/background-task-persistence.js +28 -5
  5. package/node_modules/@comis/agent/dist/background/background-task-types.d.ts +49 -0
  6. package/node_modules/@comis/agent/dist/background/completion-dispatcher.d.ts +130 -0
  7. package/node_modules/@comis/agent/dist/background/completion-dispatcher.js +215 -0
  8. package/node_modules/@comis/agent/dist/background/completion-runner.d.ts +10 -1
  9. package/node_modules/@comis/agent/dist/background/completion-runner.js +98 -15
  10. package/node_modules/@comis/agent/dist/background/index.d.ts +6 -1
  11. package/node_modules/@comis/agent/dist/background/index.js +2 -0
  12. package/node_modules/@comis/agent/dist/background/session-resolver.d.ts +85 -0
  13. package/node_modules/@comis/agent/dist/background/session-resolver.js +78 -0
  14. package/node_modules/@comis/agent/dist/bootstrap/sections/messaging-sections.js +1 -0
  15. package/node_modules/@comis/agent/dist/bootstrap/sections/tool-descriptions.js +3 -3
  16. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.d.ts +30 -2
  17. package/node_modules/@comis/agent/dist/bootstrap/sections/tooling-sections.js +51 -2
  18. package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.d.ts +22 -0
  19. package/node_modules/@comis/agent/dist/bootstrap/system-prompt-assembler.js +2 -2
  20. package/node_modules/@comis/agent/dist/bridge/bridge-event-handlers.d.ts +1 -5
  21. package/node_modules/@comis/agent/dist/bridge/bridge-event-handlers.js +2 -14
  22. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.d.ts +26 -0
  23. package/node_modules/@comis/agent/dist/bridge/bridge-metrics.js +3 -0
  24. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.d.ts +9 -0
  25. package/node_modules/@comis/agent/dist/bridge/pi-event-bridge.js +73 -2
  26. package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.d.ts +10 -10
  27. package/node_modules/@comis/agent/dist/context-engine/signature-surrogate-guard.js +14 -14
  28. package/node_modules/@comis/agent/dist/context-engine/thinking-block-cleaner.d.ts +11 -13
  29. package/node_modules/@comis/agent/dist/context-engine/thinking-block-cleaner.js +14 -15
  30. package/node_modules/@comis/agent/dist/executor/capability-index-context.d.ts +72 -0
  31. package/node_modules/@comis/agent/dist/executor/capability-index-context.js +329 -0
  32. package/node_modules/@comis/agent/dist/executor/drain-helper.d.ts +122 -0
  33. package/node_modules/@comis/agent/dist/executor/drain-helper.js +173 -0
  34. package/node_modules/@comis/agent/dist/executor/error-classifier.js +2 -2
  35. package/node_modules/@comis/agent/dist/executor/executor-post-execution.d.ts +48 -4
  36. package/node_modules/@comis/agent/dist/executor/executor-post-execution.js +134 -31
  37. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.d.ts +7 -0
  38. package/node_modules/@comis/agent/dist/executor/executor-prompt-runner.js +25 -4
  39. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.d.ts +18 -1
  40. package/node_modules/@comis/agent/dist/executor/executor-tool-assembly.js +19 -16
  41. package/node_modules/@comis/agent/dist/executor/jit-guide-injector.d.ts +11 -2
  42. package/node_modules/@comis/agent/dist/executor/jit-guide-injector.js +16 -2
  43. package/node_modules/@comis/agent/dist/executor/pi-executor.d.ts +8 -2
  44. package/node_modules/@comis/agent/dist/executor/pi-executor.js +25 -12
  45. package/node_modules/@comis/agent/dist/executor/prompt-assembly.d.ts +9 -1
  46. package/node_modules/@comis/agent/dist/executor/prompt-assembly.js +15 -1
  47. package/node_modules/@comis/agent/dist/executor/tool-deferral.d.ts +18 -27
  48. package/node_modules/@comis/agent/dist/executor/tool-deferral.js +29 -38
  49. package/node_modules/@comis/agent/dist/model/model-registry-adapter.js +1 -1
  50. package/node_modules/@comis/agent/dist/model/model-scanner.js +1 -1
  51. package/node_modules/@comis/agent/dist/safety/tool-retry-breaker.d.ts +11 -1
  52. package/node_modules/@comis/agent/dist/safety/tool-retry-breaker.js +19 -22
  53. package/node_modules/@comis/agent/dist/session/comis-session-manager.d.ts +16 -2
  54. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.d.ts +1 -1
  55. package/node_modules/@comis/agent/dist/spawn/pi-mono-adapters.js +5 -5
  56. package/node_modules/@comis/agent/dist/workspace/data-env.d.ts +38 -0
  57. package/node_modules/@comis/agent/dist/workspace/data-env.js +56 -0
  58. package/node_modules/@comis/agent/dist/workspace/index.d.ts +1 -0
  59. package/node_modules/@comis/agent/dist/workspace/index.js +1 -0
  60. package/node_modules/@comis/agent/dist/workspace/templates.js +5 -1
  61. package/node_modules/@comis/agent/package.json +1 -1
  62. package/node_modules/@comis/channels/dist/index.d.ts +1 -1
  63. package/node_modules/@comis/channels/dist/index.js +1 -1
  64. package/node_modules/@comis/channels/dist/shared/channel-manager.d.ts +9 -3
  65. package/node_modules/@comis/channels/dist/shared/inbound-gate.d.ts +1 -1
  66. package/node_modules/@comis/channels/dist/shared/inbound-gate.js +22 -7
  67. package/node_modules/@comis/channels/dist/shared/inbound-pipeline.d.ts +10 -3
  68. package/node_modules/@comis/channels/dist/shared/inbound-route.d.ts +1 -1
  69. package/node_modules/@comis/channels/dist/shared/inbound-route.js +13 -2
  70. package/node_modules/@comis/channels/dist/shared/response-filter.d.ts +11 -24
  71. package/node_modules/@comis/channels/dist/shared/response-filter.js +25 -53
  72. package/node_modules/@comis/channels/package.json +1 -1
  73. package/node_modules/@comis/cli/dist/commands/providers.d.ts +1 -2
  74. package/node_modules/@comis/cli/dist/commands/providers.js +5 -6
  75. package/node_modules/@comis/cli/package.json +1 -1
  76. package/node_modules/@comis/core/dist/config/field-metadata.js +2 -0
  77. package/node_modules/@comis/core/dist/config/immutable-keys.js +4 -1
  78. package/node_modules/@comis/core/dist/config/index.d.ts +4 -0
  79. package/node_modules/@comis/core/dist/config/index.js +2 -0
  80. package/node_modules/@comis/core/dist/config/schema-agent.d.ts +0 -792
  81. package/node_modules/@comis/core/dist/config/schema-approvals.d.ts +0 -14
  82. package/node_modules/@comis/core/dist/config/schema-auto-reply-engine.d.ts +0 -6
  83. package/node_modules/@comis/core/dist/config/schema-background-tasks.d.ts +0 -12
  84. package/node_modules/@comis/core/dist/config/schema-browser.d.ts +0 -18
  85. package/node_modules/@comis/core/dist/config/schema-channel.d.ts +0 -158
  86. package/node_modules/@comis/core/dist/config/schema-coalescer.d.ts +0 -5
  87. package/node_modules/@comis/core/dist/config/schema-daemon.d.ts +0 -32
  88. package/node_modules/@comis/core/dist/config/schema-delivery.d.ts +0 -18
  89. package/node_modules/@comis/core/dist/config/schema-documentation.d.ts +0 -12
  90. package/node_modules/@comis/core/dist/config/schema-embedding.d.ts +0 -20
  91. package/node_modules/@comis/core/dist/config/schema-envelope.d.ts +0 -15
  92. package/node_modules/@comis/core/dist/config/schema-gateway.d.ts +0 -37
  93. package/node_modules/@comis/core/dist/config/schema-gemini-cache.d.ts +0 -2
  94. package/node_modules/@comis/core/dist/config/schema-integrations.d.ts +0 -318
  95. package/node_modules/@comis/core/dist/config/schema-lifecycle-reactions.d.ts +0 -18
  96. package/node_modules/@comis/core/dist/config/schema-memory-review.d.ts +0 -7
  97. package/node_modules/@comis/core/dist/config/schema-memory.d.ts +0 -16
  98. package/node_modules/@comis/core/dist/config/schema-messages.d.ts +0 -8
  99. package/node_modules/@comis/core/dist/config/schema-models.d.ts +0 -15
  100. package/node_modules/@comis/core/dist/config/schema-notification.d.ts +0 -5
  101. package/node_modules/@comis/core/dist/config/schema-oauth.d.ts +0 -5
  102. package/node_modules/@comis/core/dist/config/schema-observability.d.ts +0 -38
  103. package/node_modules/@comis/core/dist/config/schema-output-retention.d.ts +34 -0
  104. package/node_modules/@comis/core/dist/config/schema-output-retention.js +48 -0
  105. package/node_modules/@comis/core/dist/config/schema-plugins.d.ts +0 -8
  106. package/node_modules/@comis/core/dist/config/schema-providers.d.ts +0 -64
  107. package/node_modules/@comis/core/dist/config/schema-queue.d.ts +0 -58
  108. package/node_modules/@comis/core/dist/config/schema-response-prefix.d.ts +0 -2
  109. package/node_modules/@comis/core/dist/config/schema-retry.d.ts +0 -6
  110. package/node_modules/@comis/core/dist/config/schema-scheduler.d.ts +0 -39
  111. package/node_modules/@comis/core/dist/config/schema-secrets.d.ts +0 -3
  112. package/node_modules/@comis/core/dist/config/schema-security.d.ts +0 -18
  113. package/node_modules/@comis/core/dist/config/schema-send-policy.d.ts +0 -13
  114. package/node_modules/@comis/core/dist/config/schema-sender-trust-display.d.ts +0 -5
  115. package/node_modules/@comis/core/dist/config/schema-serializer.js +2 -0
  116. package/node_modules/@comis/core/dist/config/schema-skills.d.ts +0 -61
  117. package/node_modules/@comis/core/dist/config/schema-streaming.d.ts +0 -38
  118. package/node_modules/@comis/core/dist/config/schema-telegram-file-guard.d.ts +0 -3
  119. package/node_modules/@comis/core/dist/config/schema-tooling.d.ts +87 -0
  120. package/node_modules/@comis/core/dist/config/schema-tooling.js +152 -0
  121. package/node_modules/@comis/core/dist/config/schema-verbosity.d.ts +0 -12
  122. package/node_modules/@comis/core/dist/config/schema-webhooks.d.ts +0 -40
  123. package/node_modules/@comis/core/dist/config/schema.d.ts +41 -38
  124. package/node_modules/@comis/core/dist/config/schema.js +6 -0
  125. package/node_modules/@comis/core/dist/context/context.d.ts +0 -4
  126. package/node_modules/@comis/core/dist/domain/approval-request.d.ts +0 -17
  127. package/node_modules/@comis/core/dist/domain/background-task-origin.d.ts +0 -10
  128. package/node_modules/@comis/core/dist/domain/delivery-origin.d.ts +0 -5
  129. package/node_modules/@comis/core/dist/domain/execution-graph.d.ts +0 -48
  130. package/node_modules/@comis/core/dist/domain/memory-entry.d.ts +0 -3
  131. package/node_modules/@comis/core/dist/domain/model-compat.d.ts +0 -4
  132. package/node_modules/@comis/core/dist/domain/normalized-message.d.ts +0 -15
  133. package/node_modules/@comis/core/dist/domain/provider-capabilities.d.ts +0 -6
  134. package/node_modules/@comis/core/dist/domain/rich-message.d.ts +0 -14
  135. package/node_modules/@comis/core/dist/domain/subagent-context-config.d.ts +0 -22
  136. package/node_modules/@comis/core/dist/domain/subagent-context-types.d.ts +0 -8
  137. package/node_modules/@comis/core/dist/event-bus/events-agent.d.ts +31 -0
  138. package/node_modules/@comis/core/dist/event-bus/events-infra.d.ts +5 -0
  139. package/node_modules/@comis/core/dist/exports/config.d.ts +2 -2
  140. package/node_modules/@comis/core/dist/exports/config.js +3 -1
  141. package/node_modules/@comis/core/dist/exports/hooks.d.ts +1 -1
  142. package/node_modules/@comis/core/dist/exports/ports.d.ts +2 -2
  143. package/node_modules/@comis/core/dist/exports/ports.js +1 -1
  144. package/node_modules/@comis/core/dist/ports/channel-plugin.d.ts +0 -13
  145. package/node_modules/@comis/core/dist/ports/index.d.ts +2 -0
  146. package/node_modules/@comis/core/dist/ports/index.js +4 -0
  147. package/node_modules/@comis/core/dist/ports/no-op-tool-capability.d.ts +30 -0
  148. package/node_modules/@comis/core/dist/ports/no-op-tool-capability.js +47 -0
  149. package/node_modules/@comis/core/dist/ports/tool-capability.d.ts +165 -0
  150. package/node_modules/@comis/core/dist/ports/tool-capability.js +15 -0
  151. package/node_modules/@comis/core/dist/security/audit.d.ts +0 -11
  152. package/node_modules/@comis/core/dist/tool-metadata.d.ts +21 -1
  153. package/node_modules/@comis/core/dist/tool-metadata.js +1 -1
  154. package/node_modules/@comis/core/package.json +1 -1
  155. package/node_modules/@comis/daemon/bundled-skills/skill-creator/scripts/validate-skill.py +1 -1
  156. package/node_modules/@comis/daemon/dist/daemon.js +89 -14
  157. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.d.ts +1 -1
  158. package/node_modules/@comis/daemon/dist/rpc/agent-inline-workspace.js +1 -1
  159. package/node_modules/@comis/daemon/dist/rpc/builtin-provider-guard.js +2 -2
  160. package/node_modules/@comis/daemon/dist/rpc/credential-resolver.js +1 -1
  161. package/node_modules/@comis/daemon/dist/rpc/model-handlers.d.ts +1 -1
  162. package/node_modules/@comis/daemon/dist/rpc/model-handlers.js +2 -2
  163. package/node_modules/@comis/daemon/dist/sub-agent-runner.d.ts +18 -0
  164. package/node_modules/@comis/daemon/dist/sub-agent-runner.js +41 -9
  165. package/node_modules/@comis/daemon/dist/wiring/index.d.ts +2 -0
  166. package/node_modules/@comis/daemon/dist/wiring/index.js +1 -0
  167. package/node_modules/@comis/daemon/dist/wiring/setup-agents.d.ts +36 -2
  168. package/node_modules/@comis/daemon/dist/wiring/setup-agents.js +45 -8
  169. package/node_modules/@comis/daemon/dist/wiring/setup-background-completion-runner.d.ts +28 -9
  170. package/node_modules/@comis/daemon/dist/wiring/setup-background-completion-runner.js +36 -9
  171. package/node_modules/@comis/daemon/dist/wiring/setup-background-tasks.js +2 -2
  172. package/node_modules/@comis/daemon/dist/wiring/setup-channels.d.ts +9 -2
  173. package/node_modules/@comis/daemon/dist/wiring/setup-channels.js +15 -9
  174. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.d.ts +20 -5
  175. package/node_modules/@comis/daemon/dist/wiring/setup-cross-session.js +20 -15
  176. package/node_modules/@comis/daemon/dist/wiring/setup-delivery.js +14 -2
  177. package/node_modules/@comis/daemon/dist/wiring/setup-gateway.d.ts +4 -6
  178. package/node_modules/@comis/daemon/dist/wiring/setup-gateway.js +3 -5
  179. package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.d.ts +20 -5
  180. package/node_modules/@comis/daemon/dist/wiring/setup-heartbeat.js +11 -2
  181. package/node_modules/@comis/daemon/dist/wiring/setup-output-retention.d.ts +89 -0
  182. package/node_modules/@comis/daemon/dist/wiring/setup-output-retention.js +212 -0
  183. package/node_modules/@comis/daemon/dist/wiring/setup-tools.d.ts +18 -4
  184. package/node_modules/@comis/daemon/dist/wiring/setup-tools.js +29 -10
  185. package/node_modules/@comis/daemon/dist/wiring/tool-capability-adapter.d.ts +75 -0
  186. package/node_modules/@comis/daemon/dist/wiring/tool-capability-adapter.js +253 -0
  187. package/node_modules/@comis/daemon/package.json +1 -1
  188. package/node_modules/@comis/gateway/dist/webhook/webhook-endpoint.d.ts +0 -4
  189. package/node_modules/@comis/gateway/package.json +1 -1
  190. package/node_modules/@comis/infra/package.json +1 -1
  191. package/node_modules/@comis/memory/package.json +1 -1
  192. package/node_modules/@comis/scheduler/dist/cron/cron-types.d.ts +0 -42
  193. package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.d.ts +29 -8
  194. package/node_modules/@comis/scheduler/dist/heartbeat/agent-heartbeat-source.js +19 -7
  195. package/node_modules/@comis/scheduler/dist/system-events/system-event-types.d.ts +0 -3
  196. package/node_modules/@comis/scheduler/dist/tasks/task-types.d.ts +0 -17
  197. package/node_modules/@comis/scheduler/package.json +1 -1
  198. package/node_modules/@comis/shared/dist/index.d.ts +3 -0
  199. package/node_modules/@comis/shared/dist/index.js +4 -0
  200. package/node_modules/@comis/shared/dist/mcp-tool-name.d.ts +78 -0
  201. package/node_modules/@comis/shared/dist/mcp-tool-name.js +92 -0
  202. package/node_modules/@comis/shared/dist/silent-tokens.d.ts +38 -0
  203. package/node_modules/@comis/shared/dist/silent-tokens.js +51 -0
  204. package/node_modules/@comis/shared/dist/visible-delivery.d.ts +28 -0
  205. package/node_modules/@comis/shared/dist/visible-delivery.js +16 -0
  206. package/node_modules/@comis/shared/package.json +1 -1
  207. package/node_modules/@comis/skills/dist/bridge/mcp-tool-bridge.d.ts +2 -13
  208. package/node_modules/@comis/skills/dist/bridge/mcp-tool-bridge.js +3 -21
  209. package/node_modules/@comis/skills/dist/bridge/tool-metadata-enforcement.js +1 -1
  210. package/node_modules/@comis/skills/dist/bridge/tool-metadata-registry.js +4 -4
  211. package/node_modules/@comis/skills/dist/builtin/exec-tool.d.ts +55 -9
  212. package/node_modules/@comis/skills/dist/builtin/exec-tool.js +383 -19
  213. package/node_modules/@comis/skills/dist/builtin/install-detour.d.ts +67 -0
  214. package/node_modules/@comis/skills/dist/builtin/install-detour.js +342 -0
  215. package/node_modules/@comis/skills/dist/builtin/platform/admin-manage-factory.js +5 -5
  216. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.d.ts +2 -2
  217. package/node_modules/@comis/skills/dist/builtin/platform/agents-manage-tool.js +2 -2
  218. package/node_modules/@comis/skills/dist/builtin/platform/message-tool.js +18 -0
  219. package/node_modules/@comis/skills/dist/builtin/platform/messaging-factory.d.ts +18 -1
  220. package/node_modules/@comis/skills/dist/builtin/platform/messaging-factory.js +18 -2
  221. package/node_modules/@comis/skills/dist/builtin/platform/models-manage-tool.js +3 -3
  222. package/node_modules/@comis/skills/dist/builtin/process-registry.d.ts +14 -0
  223. package/node_modules/@comis/skills/dist/builtin/process-tool.d.ts +24 -4
  224. package/node_modules/@comis/skills/dist/builtin/process-tool.js +25 -7
  225. package/node_modules/@comis/skills/dist/builtin/sandbox/bwrap-provider.d.ts +1 -1
  226. package/node_modules/@comis/skills/dist/builtin/sandbox/bwrap-provider.js +9 -0
  227. package/node_modules/@comis/skills/dist/index.d.ts +4 -1
  228. package/node_modules/@comis/skills/dist/index.js +3 -1
  229. package/node_modules/@comis/skills/dist/manifest/capability-parser.d.ts +44 -0
  230. package/node_modules/@comis/skills/dist/manifest/capability-parser.js +68 -0
  231. package/node_modules/@comis/skills/dist/manifest/schema.d.ts +44 -37
  232. package/node_modules/@comis/skills/dist/manifest/schema.js +35 -0
  233. package/node_modules/@comis/skills/dist/registry/discovery.d.ts +8 -0
  234. package/node_modules/@comis/skills/dist/registry/discovery.js +10 -3
  235. package/node_modules/@comis/skills/dist/registry/skill-registry.d.ts +45 -1
  236. package/node_modules/@comis/skills/dist/registry/skill-registry.js +70 -7
  237. package/node_modules/@comis/skills/package.json +1 -1
  238. package/node_modules/@comis/web/package.json +1 -1
  239. package/package.json +21 -21
@@ -26,6 +26,8 @@ import type { SecretManager } from "@comis/core";
26
26
  import type { ExecSandboxConfig } from "./sandbox/types.js";
27
27
  import type { ProcessRegistry } from "./process-registry.js";
28
28
  import type { TypedEventBus } from "@comis/core";
29
+ import type { ToolCapabilityPort, ApprovalGate } from "@comis/core";
30
+ import { type InstallDetourDecision } from "./install-detour.js";
29
31
  declare const ExecParams: Type.TObject<{
30
32
  command: Type.TString;
31
33
  cwd: Type.TOptional<Type.TString>;
@@ -37,6 +39,7 @@ declare const ExecParams: Type.TObject<{
37
39
  description: Type.TOptional<Type.TString>;
38
40
  pty: Type.TOptional<Type.TBoolean>;
39
41
  secretRefs: Type.TOptional<Type.TArray<Type.TString>>;
42
+ allowInstallDetour: Type.TOptional<Type.TBoolean>;
40
43
  }>;
41
44
  /** Minimal pino-compatible logger for structured tool logging. */
42
45
  interface ToolLogger {
@@ -68,17 +71,60 @@ export declare function buildSpawnCommand(command: string, cwd: string, sandboxC
68
71
  args: string[];
69
72
  cwd: string | undefined;
70
73
  };
74
+ /**
75
+ * Build the structured install-detour hint augmentation for advise mode.
76
+ * Returns BOTH a string (for `details.installDetourHint`) AND a sibling
77
+ * `[hint]` content block (for the `result.content` array). Primary content,
78
+ * stdout, stderr, exitCode, and status are NEVER mutated by callers.
79
+ *
80
+ * Hint is informational; primary signal stays on the
81
+ * exec result envelope.
82
+ */
83
+ export declare function buildInstallDetourHint(decision: InstallDetourDecision): {
84
+ installDetourHint: string;
85
+ hintContentBlock: {
86
+ type: "text";
87
+ text: string;
88
+ };
89
+ };
90
+ /**
91
+ * Dependencies for the exec tool factory. Backward compatibility is NOT
92
+ * preserved (see CLAUDE.md user-memory `feedback_no_backward_compat`).
93
+ *
94
+ * `toolCapabilityPort` is REQUIRED — wires the install-detour policy gate
95
+ * inside `execute(...)` consuming `port.getInstallDetourMode()`. Daemon
96
+ * wiring may inject `createNoOpCapabilityPort()` at the construction site
97
+ * (`packages/daemon/src/wiring/setup-tools.ts`); the no-op returns empty
98
+ * connected-server and skill arrays so the parser sees no overlaps.
99
+ *
100
+ * `approvalGate` is OPTIONAL — only required by the `soft-stop` mode
101
+ * override path. Missing gate → `soft-stop` denies override
102
+ * (fail-closed per AGENTS.md §2.1).
103
+ */
104
+ export interface ExecToolDeps {
105
+ readonly workspacePath: string;
106
+ readonly registry: ProcessRegistry;
107
+ readonly secretManager: SecretManager;
108
+ readonly platformSecretNames: ReadonlySet<string>;
109
+ readonly logger?: ToolLogger;
110
+ readonly subprocessEnv?: Record<string, string>;
111
+ readonly sandboxConfig?: ExecSandboxConfig;
112
+ readonly eventBus?: TypedEventBus;
113
+ readonly getToolResultsDir?: () => string | undefined;
114
+ /** REQUIRED for v1.1 capability layer. */
115
+ readonly toolCapabilityPort: ToolCapabilityPort;
116
+ /** Optional. Required only for soft-stop override path. */
117
+ readonly approvalGate?: ApprovalGate;
118
+ }
71
119
  /**
72
120
  * Create an exec tool for shell command execution.
73
121
  *
74
- * @param workspacePath - Default working directory for commands
75
- * @param registry - ProcessRegistry for background process tracking
76
- * @param logger - Optional structured logger for DEBUG-level operation logging
77
- * @param subprocessEnv - Optional filtered env for subprocesses (defense-in-depth)
78
- * @param sandboxConfig - Optional sandbox configuration for OS-level isolation
79
- * @param eventBus - Optional TypedEventBus for emitting command:blocked audit events
80
- * @param getToolResultsDir - Optional getter for session tool-results directory
81
- * @returns AgentTool implementing the exec interface
122
+ * Backward compat NOT preserved (memory `feedback_no_backward_compat`).
123
+ *
124
+ * @param deps - Dependencies bundle. See `ExecToolDeps` for field semantics.
125
+ * `toolCapabilityPort` is REQUIRED; `approvalGate` is optional but
126
+ * required for the soft-stop override path.
127
+ * @returns AgentTool implementing the exec interface.
82
128
  */
83
- export declare function createExecTool(workspacePath: string, registry: ProcessRegistry, secretManager: SecretManager, platformSecretNames: ReadonlySet<string>, logger?: ToolLogger, subprocessEnv?: Record<string, string>, sandboxConfig?: ExecSandboxConfig, eventBus?: TypedEventBus, getToolResultsDir?: () => string | undefined): AgentTool<typeof ExecParams>;
129
+ export declare function createExecTool(deps: ExecToolDeps): AgentTool<typeof ExecParams>;
84
130
  export {};
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { Type } from "typebox";
25
25
  import { spawn } from "node:child_process";
26
- import { createWriteStream, mkdirSync, writeFileSync, copyFileSync, statSync } from "node:fs";
26
+ import { createWriteStream, mkdirSync, writeFileSync, copyFileSync, statSync, existsSync } from "node:fs";
27
27
  import { tmpdir } from "node:os";
28
28
  import { randomBytes } from "node:crypto";
29
29
  import { safePath, PathTraversalError } from "@comis/core";
@@ -35,6 +35,7 @@ import { createOutputCleaner } from "./output-cleaner.js";
35
35
  import { extractHeredoc, validateExecCommand, interpretExitCode } from "./exec-security.js";
36
36
  import { matchExecRecoveryHint } from "./exec-diagnostics.js";
37
37
  import { tryGetContext } from "@comis/core";
38
+ import { parseInstallDetour, } from "./install-detour.js";
38
39
  // ---------------------------------------------------------------------------
39
40
  // Constants
40
41
  // ---------------------------------------------------------------------------
@@ -75,6 +76,12 @@ const ExecParams = Type.Object({
75
76
  maxItems: 8,
76
77
  description: "Secret/credential NAMES (not values) to inject as env vars into the subprocess. Use this to pass API tokens to CLI tools like wrangler/gh/gcloud/kubectl without tripping the env-var allowlist. Names are resolved server-side via SecretManager; values never flow through agent context. Call env_list first to discover available names. Platform-managed secrets (referenced by the daemon config, e.g. ANTHROPIC_API_KEY) are rejected. Raw-interpreter commands (python -c, node -e, bash -c, etc.) are rejected with secretRefs to prevent trivial echo-to-stdout leaks. Example: {command: 'npx wrangler pages deploy ./dist', secretRefs: ['CLOUDFLARE_API_TOKEN', 'CLOUDFLARE_ACCOUNT_ID']}",
77
78
  })),
79
+ allowInstallDetour: Type.Optional(Type.Boolean({
80
+ description: "Request an operator-approved override when the user explicitly needs " +
81
+ "package installation despite an equivalent connected MCP server or " +
82
+ "available skill. Submits an approval request — does NOT self-authorize. " +
83
+ "Per-commandDigest scoped (one approval does not cover a different install).",
84
+ })),
78
85
  });
79
86
  // ---------------------------------------------------------------------------
80
87
  // cwd resolution
@@ -201,6 +208,39 @@ export function buildSpawnCommand(command, cwd, sandboxConfig, workspacePath, te
201
208
  // ---------------------------------------------------------------------------
202
209
  /** Valid secret env var name (same rule as env.set). */
203
210
  const SECRET_REF_NAME_PATTERN = /^[A-Z][A-Z0-9_]*$/;
211
+ /**
212
+ * Workspace-internal data env resolver.
213
+ *
214
+ * Returns env vars (PATH, MPLCONFIGDIR, XDG_CACHE_HOME, MPLBACKEND,
215
+ * PIP_DISABLE_PIP_VERSION_CHECK) derived from `workspaceDir` so python /
216
+ * matplotlib subprocesses do NOT inherit the daemon's host PATH or
217
+ * cache-dir defaults. (15s pip-install hot path; matplotlib Fontconfig
218
+ * error from a non-writable default cache dir).
219
+ *
220
+ * Defined inline to avoid a cross-package import from `@comis/agent`
221
+ * (`@comis/skills` does not depend on `@comis/agent` -- they are siblings
222
+ * wired together at the daemon composition root, mirroring the
223
+ * `bridge-event-handlers.ts` "Defined inline to avoid cross-package
224
+ * import" precedent). The shape mirrors
225
+ * `packages/agent/src/workspace/data-env.ts` verbatim; if the contract
226
+ * diverges, lift this into `@comis/shared`. The agent-side `data-env.ts`
227
+ * remains the canonical owner of the no-host-env contract.
228
+ *
229
+ * This helper does NOT read host env. The values are pure derivations
230
+ * from workspaceDir + safePath (no path.join).
231
+ */
232
+ function resolveDataEnv(opts) {
233
+ const venvBin = safePath(opts.workspaceDir, "venv", "bin");
234
+ const cacheDir = safePath(opts.workspaceDir, ".cache");
235
+ const mplDir = safePath(cacheDir, "matplotlib");
236
+ return {
237
+ PATH: venvBin,
238
+ MPLCONFIGDIR: mplDir,
239
+ XDG_CACHE_HOME: cacheDir,
240
+ MPLBACKEND: "Agg",
241
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
242
+ };
243
+ }
204
244
  /**
205
245
  * Detect raw-interpreter command shapes. When `secretRefs` is present,
206
246
  * these are refused because they make `echo $TOKEN` / `print(os.environ)`
@@ -271,21 +311,116 @@ function resolveSecretRefs(refs, secretManager, platformSecretNames) {
271
311
  return { ok: true, env };
272
312
  }
273
313
  // ---------------------------------------------------------------------------
274
- // Factory
314
+ // Install-detour helpers
275
315
  // ---------------------------------------------------------------------------
316
+ /**
317
+ * Map package manager to the structured ecosystem field used by the
318
+ * `tool:install_detour_detected` event payload (closed shape).
319
+ */
320
+ function ecosystemFor(pm) {
321
+ return pm === "pip" ? "python" : "node";
322
+ }
323
+ /**
324
+ * Build the structured install-detour hint augmentation for advise mode.
325
+ * Returns BOTH a string (for `details.installDetourHint`) AND a sibling
326
+ * `[hint]` content block (for the `result.content` array). Primary content,
327
+ * stdout, stderr, exitCode, and status are NEVER mutated by callers.
328
+ *
329
+ * Hint is informational; primary signal stays on the
330
+ * exec result envelope.
331
+ */
332
+ export function buildInstallDetourHint(decision) {
333
+ const lines = decision.overlaps.map((o) => {
334
+ const cluster = o.cluster ? ` (cluster: ${o.cluster})` : "";
335
+ return o.sourceType === "mcp"
336
+ ? `- ${o.packageName} -> connected MCP server "${o.sourceName}"${cluster}`
337
+ : `- ${o.packageName} -> available skill "${o.sourceName}"${cluster}`;
338
+ });
339
+ const text = `[hint] Installed packages overlap available capabilities:\n${lines.join("\n")}\n` +
340
+ `If you can use these capabilities directly, do so before relying on the install.`;
341
+ return {
342
+ installDetourHint: text,
343
+ hintContentBlock: { type: "text", text },
344
+ };
345
+ }
346
+ /**
347
+ * Build the verbatim soft-stop error template. Returns `null` when
348
+ * no overlap source remains connected/visible at error-build time
349
+ * (overlaps disappeared mid-call; refusal no longer justified).
350
+ * Caller falls through to spawn in that case.
351
+ *
352
+ * Bullet count == filtered-overlap count. Cluster shown in parens when defined.
353
+ * MCP overlaps say `connected MCP server`; skill overlaps say `available skill`.
354
+ * NO provider-specific tool names; NO self-authorizing override wording.
355
+ */
356
+ function buildSoftStopErrorTemplate(decision, port) {
357
+ // Re-check connection/visibility at error time, not parser time.
358
+ // The connected-server set may have drifted since parse. Filter to overlaps
359
+ // whose source is currently reachable.
360
+ const connectedServers = new Set(port.getConnectedMcpServers());
361
+ const visibleSkills = new Set(port.getPromptSkillCapabilities().map((s) => s.name));
362
+ const filtered = decision.overlaps.filter((o) => o.sourceType === "mcp" ? connectedServers.has(o.sourceName) : visibleSkills.has(o.sourceName));
363
+ if (filtered.length === 0)
364
+ return null; // overlaps disappeared — fall through
365
+ const bullets = filtered.map((o) => {
366
+ const cluster = o.cluster ? ` (cluster: ${o.cluster})` : "";
367
+ return o.sourceType === "mcp"
368
+ ? `- ${o.packageName} -> connected MCP server "${o.sourceName}"${cluster}`
369
+ : `- ${o.packageName} -> available skill "${o.sourceName}"${cluster}`;
370
+ });
371
+ return [
372
+ "Refused: install overlaps with available capability source(s).",
373
+ "",
374
+ "Overlapping packages:",
375
+ ...bullets,
376
+ "",
377
+ "To proceed, choose one:",
378
+ "1. Use the connected tool(s) or available skill(s) listed above for the overlapping work.",
379
+ "2. If you only need the non-overlapping packages, rerun exec with the overlapping ones removed.",
380
+ "3. If you genuinely need the install despite the overlap, ask the user/operator to approve the install-detour override, then rerun this exact command with `allowInstallDetour: true`.",
381
+ ].join("\n");
382
+ }
383
+ /**
384
+ * Build the closed `tool:install_detour_detected` event payload from a
385
+ * decision + the action/mode tags. Sanitized only:
386
+ * NEVER includes `command`, `rawCommand`, `stdout`, `stderr`, raw shell
387
+ * fragments, URLs, paths, or credentials.
388
+ */
389
+ function buildInstallDetourEventPayload(decision, mode, action) {
390
+ const ctx = tryGetContext();
391
+ return {
392
+ agentId: ctx?.sessionKey ?? "unknown", // sessionKey-as-agentId convention
393
+ sessionKey: ctx?.sessionKey ?? "unknown",
394
+ traceId: ctx?.traceId,
395
+ packageManager: decision.packageManager,
396
+ commandDigest: decision.commandDigest,
397
+ packages: decision.packages.map((p) => ({
398
+ normalizedName: p,
399
+ ecosystem: ecosystemFor(decision.packageManager),
400
+ })),
401
+ overlaps: decision.overlaps.map((o) => ({
402
+ packageName: o.packageName,
403
+ sourceType: o.sourceType,
404
+ sourceName: o.sourceName,
405
+ reason: o.reason,
406
+ })),
407
+ mode,
408
+ action,
409
+ timestamp: Date.now(),
410
+ };
411
+ }
276
412
  /**
277
413
  * Create an exec tool for shell command execution.
278
414
  *
279
- * @param workspacePath - Default working directory for commands
280
- * @param registry - ProcessRegistry for background process tracking
281
- * @param logger - Optional structured logger for DEBUG-level operation logging
282
- * @param subprocessEnv - Optional filtered env for subprocesses (defense-in-depth)
283
- * @param sandboxConfig - Optional sandbox configuration for OS-level isolation
284
- * @param eventBus - Optional TypedEventBus for emitting command:blocked audit events
285
- * @param getToolResultsDir - Optional getter for session tool-results directory
286
- * @returns AgentTool implementing the exec interface
415
+ * Backward compat NOT preserved (memory `feedback_no_backward_compat`).
416
+ *
417
+ * @param deps - Dependencies bundle. See `ExecToolDeps` for field semantics.
418
+ * `toolCapabilityPort` is REQUIRED; `approvalGate` is optional but
419
+ * required for the soft-stop override path.
420
+ * @returns AgentTool implementing the exec interface.
287
421
  */
288
- export function createExecTool(workspacePath, registry, secretManager, platformSecretNames, logger, subprocessEnv, sandboxConfig, eventBus, getToolResultsDir) {
422
+ export function createExecTool(deps) {
423
+ const { workspacePath, registry, secretManager, platformSecretNames, logger, subprocessEnv, sandboxConfig, eventBus, getToolResultsDir, } = deps;
289
424
  // Comis extension: promptGuidelines is not part of AgentTool type, use object
290
425
  // spread to avoid excess property checks in the return statement.
291
426
  const guidelines = {
@@ -365,9 +500,132 @@ export function createExecTool(workspacePath, registry, secretManager, platformS
365
500
  });
366
501
  throwToolError("permission_denied", validationError.message);
367
502
  }
503
+ // ===================================================================
504
+ // Install-detour mode policy gate
505
+ //
506
+ // Runs synchronously BEFORE any subprocess spawn. soft-stop refuses
507
+ // pre-spawn; observe and advise fall through to spawn after emitting
508
+ // events.
509
+ //
510
+ // The decision is computed ONCE here and propagated to:
511
+ // - Event emission (this block)
512
+ // - ProcessSession population at all 3 spawn sites
513
+ // - Foreground / auto-bg / explicit-bg envelope augmentation (advise)
514
+ // - process.status retroactive augmentation (read back from session)
515
+ // ===================================================================
516
+ const installDetourMode = deps.toolCapabilityPort.getInstallDetourMode();
517
+ const installDetourDecision = parseInstallDetour(command, deps.toolCapabilityPort);
518
+ const allowInstallDetourOverride = readBooleanParam(p, "allowInstallDetour", false) ?? false;
519
+ // Track the terminal action emitted by the mode cascade for the per-call INFO log.
520
+ // Initialized to "no-decision" because the parser may return null (no install form /
521
+ // no overlap); when null, the mode cascade and the log block both short-circuit (the
522
+ // log lives inside `if (decision !== null)`).
523
+ let terminalAction = "no-decision";
524
+ if (installDetourDecision !== null) {
525
+ // Decision is non-null only when overlaps.length > 0 (parser contract).
526
+ if (installDetourMode === "observe") {
527
+ for (const overlap of installDetourDecision.overlaps) {
528
+ // Scope each emit to the single overlap so downstream
529
+ // consumers receive distinct per-overlap payloads.
530
+ // The spread keeps packageManager / commandDigest /
531
+ // packages identical across the N events for a single call —
532
+ // only `overlaps[]` differs.
533
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload({ ...installDetourDecision, overlaps: [overlap] }, "observe", "observed"));
534
+ terminalAction = "observed";
535
+ }
536
+ // Fall through to spawn (observe runs unchanged)
537
+ }
538
+ else if (installDetourMode === "advise") {
539
+ for (const overlap of installDetourDecision.overlaps) {
540
+ // Scope each emit to the single overlap.
541
+ // See observe-loop comment above for the rationale.
542
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload({ ...installDetourDecision, overlaps: [overlap] }, "advise", "hinted"));
543
+ terminalAction = "hinted";
544
+ }
545
+ // Fall through to spawn; envelope augmentation applied at completion sites
546
+ }
547
+ else if (installDetourMode === "soft-stop") {
548
+ if (!allowInstallDetourOverride) {
549
+ // Refuse pre-spawn, single soft_stopped event
550
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload(installDetourDecision, "soft-stop", "soft_stopped"));
551
+ terminalAction = "soft_stopped";
552
+ const errorMessage = buildSoftStopErrorTemplate(installDetourDecision, deps.toolCapabilityPort);
553
+ if (errorMessage === null) {
554
+ // Overlaps disappeared mid-call. Fall through to spawn.
555
+ logger?.debug({
556
+ toolName: "exec",
557
+ commandDigest: installDetourDecision.commandDigest,
558
+ mode: "soft-stop",
559
+ outcome: "overlaps-disappeared",
560
+ }, "install-detour overlap sources disappeared mid-call; falling through to spawn");
561
+ }
562
+ else {
563
+ throwToolError("permission_denied", errorMessage);
564
+ }
565
+ }
566
+ else {
567
+ // allowInstallDetour: true → submit approval request
568
+ const ctx = tryGetContext();
569
+ if (!deps.approvalGate || !ctx) {
570
+ // Fail-closed: missing gate or missing context → emit override_denied + throw
571
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload(installDetourDecision, "soft-stop", "override_denied"));
572
+ terminalAction = "override_denied";
573
+ const errorMessage = buildSoftStopErrorTemplate(installDetourDecision, deps.toolCapabilityPort);
574
+ throwToolError("permission_denied", errorMessage ?? "Install-detour override denied: missing approval gate or request context.");
575
+ }
576
+ else {
577
+ // Emit override_requested BEFORE awaiting (event-pair contract)
578
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload(installDetourDecision, "soft-stop", "override_requested"));
579
+ terminalAction = "override_requested";
580
+ const resolution = await deps.approvalGate.requestApproval({
581
+ toolName: "exec",
582
+ action: `exec.install_detour.override:${installDetourDecision.commandDigest}`,
583
+ params: {
584
+ // sanitized only
585
+ packageManager: installDetourDecision.packageManager,
586
+ packages: installDetourDecision.packages,
587
+ overlaps: installDetourDecision.overlaps,
588
+ mode: "soft-stop",
589
+ commandDigest: installDetourDecision.commandDigest,
590
+ },
591
+ agentId: ctx.userId ?? "unknown",
592
+ sessionKey: ctx.sessionKey,
593
+ trustLevel: (ctx.trustLevel ?? "admin"),
594
+ channelType: ctx.channelType,
595
+ });
596
+ if (!resolution.approved) {
597
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload(installDetourDecision, "soft-stop", "override_denied"));
598
+ terminalAction = "override_denied";
599
+ throwToolError("permission_denied", `Install-detour override denied: ${resolution.reason ?? "no reason given"}`);
600
+ }
601
+ // Approved — emit overridden, fall through to spawn UNAUGMENTED.
602
+ eventBus?.emit("tool:install_detour_detected", buildInstallDetourEventPayload(installDetourDecision, "soft-stop", "overridden"));
603
+ terminalAction = "overridden";
604
+ }
605
+ }
606
+ }
607
+ // Per-call INFO log summarizing the policy-gate evaluation
608
+ // (AGENTS.md §2.7 object-first convention).
609
+ // Fires for every command where a decision was produced AND a mode branch ran.
610
+ // The `terminalAction` field reflects the LAST emitted action for this call.
611
+ // For multi-event paths (observe/advise: 1-per-overlap; soft-stop with override:
612
+ // 2-event pair), `terminalAction` is the last action in the cascade for the call.
613
+ logger?.info({
614
+ toolName: "exec",
615
+ commandDigest: installDetourDecision.commandDigest,
616
+ packageManager: installDetourDecision.packageManager,
617
+ packageCount: installDetourDecision.packages.length,
618
+ overlapCount: installDetourDecision.overlaps.length,
619
+ mode: installDetourMode,
620
+ action: terminalAction,
621
+ }, "install-detour policy gate evaluated");
622
+ }
623
+ // ===================================================================
624
+ // End install-detour mode block
625
+ // ===================================================================
368
626
  // Detect --break-system-packages for post-execution warning
369
627
  const breakSystemWarning = command.includes("--break-system-packages")
370
- ? "\u26a0\ufe0f WARNING: --break-system-packages modifies the system Python. Use a virtualenv instead: python3 -m venv .venv && .venv/bin/pip install ...\n\n"
628
+ ? "\u26a0\ufe0f WARNING: --break-system-packages modifies the system Python. Use a virtualenv: the workspace's pre-warmed venv (venv/bin/pip install ...) or a per-project one (python3 -m venv projects/<name>/.venv).\n\n"
371
629
  : "";
372
630
  // Log command start (truncate command to 200 chars for security)
373
631
  logger?.debug({ toolName: "exec", command: command.slice(0, 200), background, pty, ...(description && { description }) }, "Exec command start");
@@ -415,10 +673,48 @@ export function createExecTool(workspacePath, registry, secretManager, platformS
415
673
  }, "Exec resolved secretRefs for subprocess");
416
674
  }
417
675
  }
418
- // Build environment (use filtered subprocess env instead of raw process.env)
676
+ // Build environment (use filtered subprocess env instead of raw process.env).
419
677
  const baseEnv = subprocessEnv ?? process.env;
678
+ // Workspace-internal env split into two halves:
679
+ //
680
+ // 1. Always-on: MPLCONFIGDIR / XDG_CACHE_HOME / MPLBACKEND /
681
+ // PIP_DISABLE_PIP_VERSION_CHECK. Eliminates the matplotlib
682
+ // `Fontconfig error` regardless of which venv layout (workspace-
683
+ // root `venv/`, per-project `.venv/`, none) the agent uses --
684
+ // the host `/root/.cache/matplotlib` may be read-only under
685
+ // `ProtectSystem=strict`; the workspace cache dir is always
686
+ // writable. Also prevents host XDG_CACHE_HOME leakage.
687
+ //
688
+ // 2. Conditional: PATH gets `${workspacePath}/venv/bin` prepended
689
+ // only when that directory actually exists (i.e. the Dockerfile
690
+ // pre-warm landed and wasn't shadowed by a stale volume). This
691
+ // closes the 15s pip-install hot path for prewarm users without
692
+ // shadowing per-project `projects/<name>/.venv/bin/python`
693
+ // invocations the agent makes directly.
694
+ //
695
+ // Order: spread baseEnv -> override with dataEnv -> userEnv (so
696
+ // the agent can still pin a specific value via `env: {...}`) ->
697
+ // server-resolved secrets (always last; agent cannot override).
698
+ const dataEnv = resolveDataEnv({ workspaceDir: workspacePath });
699
+ const venvBin = safePath(workspacePath, "venv", "bin");
700
+ if (existsSync(venvBin)) {
701
+ logger?.debug({ toolName: "exec", workspaceDir: workspacePath, hint: "Prepending workspace-prewarmed venv/bin to PATH" }, "Exec workspace venv detected");
702
+ // Prepend venv bin so `python` / `pip` resolve to the prewarm
703
+ // while system binaries (bash, sh, node, git, curl) remain
704
+ // reachable via baseEnv.PATH.
705
+ if (dataEnv.PATH && baseEnv.PATH) {
706
+ dataEnv.PATH = `${dataEnv.PATH}:${baseEnv.PATH}`;
707
+ }
708
+ }
709
+ else {
710
+ // No prewarm on this workspace -- drop the venv-only PATH so
711
+ // baseEnv.PATH wins. The matplotlib + cache dir env vars survive
712
+ // and still kill the Fontconfig warning universally.
713
+ delete dataEnv.PATH;
714
+ }
420
715
  const env = {
421
716
  ...baseEnv,
717
+ ...dataEnv,
422
718
  ...(userEnv ?? {}),
423
719
  ...(resolvedSecretEnv ?? {}),
424
720
  };
@@ -436,9 +732,9 @@ export function createExecTool(workspacePath, registry, secretManager, platformS
436
732
  logger?.debug({ toolName: "exec", stdinLength: input.length }, "Exec stdin write");
437
733
  }
438
734
  if (background) {
439
- return executeBackground(command, cwd, finalEnv, input, registry, logger, sandboxConfig, workspacePath, tempDir, description, pty);
735
+ return executeBackground(command, cwd, finalEnv, input, registry, logger, sandboxConfig, workspacePath, tempDir, description, pty, installDetourDecision ?? undefined, installDetourMode);
440
736
  }
441
- const result = await executeForeground(command, cwd, finalEnv, timeoutMs, input, signal, onUpdate, logger, sandboxConfig, workspacePath, tempDir, registry, autoBackgroundMs, pty, description, toolCallId, getToolResultsDir);
737
+ const result = await executeForeground(command, cwd, finalEnv, timeoutMs, input, signal, onUpdate, logger, sandboxConfig, workspacePath, tempDir, registry, autoBackgroundMs, pty, description, toolCallId, getToolResultsDir, installDetourDecision ?? undefined, installDetourMode);
442
738
  // Prepend --break-system-packages warning to stdout so the LLM sees it
443
739
  if (breakSystemWarning && result.details) {
444
740
  const details = result.details;
@@ -470,7 +766,7 @@ function escalateToBackground(ctx) {
470
766
  id: generateSessionId(),
471
767
  command: ctx.command,
472
768
  pid: ctx.child.pid,
473
- startedAt: Math.round(ctx.startTime),
769
+ startedAt: ctx.startTimeMs,
474
770
  status: "running",
475
771
  exitCode: undefined,
476
772
  stdout: ctx.stdoutBuf,
@@ -480,6 +776,14 @@ function escalateToBackground(ctx) {
480
776
  sandboxed: !!ctx.sandboxConfig,
481
777
  autoBackgrounded: true,
482
778
  ...(ctx.description && { description: ctx.description }),
779
+ // Spawn-time decision capture (advise+overlap only).
780
+ // observe-mode runs unchanged with no retroactive hint; soft-stop refused calls
781
+ // never reach a session-creation site (refused pre-spawn).
782
+ ...(ctx.installDetourDecision !== null
783
+ && ctx.installDetourDecision !== undefined
784
+ && ctx.installDetourDecision.overlaps.length > 0
785
+ && ctx.installDetourMode === "advise"
786
+ && { installDetourDecision: ctx.installDetourDecision }),
483
787
  };
484
788
  // Re-wire stdout/stderr from rolling buffer to session append
485
789
  const bgStdoutCleaner = createOutputCleaner();
@@ -507,6 +811,27 @@ function escalateToBackground(ctx) {
507
811
  ctx.logger?.info({ toolName: "exec", sessionId: session.id, pid: ctx.child.pid, durationMs: Math.round(performance.now() - ctx.startTime), ...(ctx.description && { description: ctx.description }) }, "Exec auto-backgrounded after threshold");
508
812
  if (ctx.spillStream)
509
813
  ctx.spillStream.end();
814
+ // Auto-bg envelope augmentation in advise mode
815
+ if (ctx.installDetourMode === "advise" &&
816
+ ctx.installDetourDecision !== null &&
817
+ ctx.installDetourDecision !== undefined &&
818
+ ctx.installDetourDecision.overlaps.length > 0) {
819
+ const hint = buildInstallDetourHint(ctx.installDetourDecision);
820
+ const augmented = jsonResult({
821
+ status: "backgrounded",
822
+ sessionId: session.id,
823
+ pid: ctx.child.pid,
824
+ stdoutSoFar: truncateTail(ctx.stdoutBuf).content,
825
+ stderrSoFar: truncateTail(ctx.stderrBuf).content,
826
+ ...(ctx.description && { description: ctx.description }),
827
+ installDetourHint: hint.installDetourHint,
828
+ });
829
+ ctx.resolve({
830
+ content: [...augmented.content, hint.hintContentBlock],
831
+ details: augmented.details,
832
+ });
833
+ return;
834
+ }
510
835
  ctx.resolve(jsonResult({
511
836
  status: "backgrounded",
512
837
  sessionId: session.id,
@@ -519,8 +844,9 @@ function escalateToBackground(ctx) {
519
844
  // ---------------------------------------------------------------------------
520
845
  // Foreground execution
521
846
  // ---------------------------------------------------------------------------
522
- function executeForeground(command, cwd, env, timeoutMs, input, signal, onUpdate, logger, sandboxConfig, workspacePath, tempDir, registry, autoBackgroundMs, pty, description, toolCallId, getToolResultsDir) {
847
+ function executeForeground(command, cwd, env, timeoutMs, input, signal, onUpdate, logger, sandboxConfig, workspacePath, tempDir, registry, autoBackgroundMs, pty, description, toolCallId, getToolResultsDir, installDetourDecision, installDetourMode) {
523
848
  const startTime = performance.now();
849
+ const startTimeMs = Date.now();
524
850
  return new Promise((resolve) => {
525
851
  const { bin, args, cwd: spawnCwd } = buildSpawnCommand(command, cwd, sandboxConfig, workspacePath ?? cwd, tempDir ?? tmpdir(), pty);
526
852
  const child = spawn(bin, args, {
@@ -654,11 +980,14 @@ function executeForeground(command, cwd, env, timeoutMs, input, signal, onUpdate
654
980
  if (resolved)
655
981
  return;
656
982
  escalateToBackground({
657
- command, child, startTime, stdoutBuf, stderrBuf,
983
+ command, child, startTime, startTimeMs, stdoutBuf, stderrBuf,
658
984
  registry, sandboxConfig, logger, spillStream,
659
985
  signal, onAbort, timeoutTimer, resolve,
660
986
  setResolved: () => { resolved = true; },
661
987
  description,
988
+ // Forward install-detour spawn-time decision + mode
989
+ ...(installDetourDecision !== undefined && { installDetourDecision }),
990
+ ...(installDetourMode !== undefined && { installDetourMode }),
662
991
  });
663
992
  }, effectiveAutoMs)
664
993
  : null;
@@ -791,6 +1120,19 @@ function executeForeground(command, cwd, env, timeoutMs, input, signal, onUpdate
791
1120
  }
792
1121
  }
793
1122
  }
1123
+ // Foreground completion envelope augmentation
1124
+ if (installDetourMode === "advise" &&
1125
+ installDetourDecision !== undefined &&
1126
+ installDetourDecision.overlaps.length > 0) {
1127
+ const hint = buildInstallDetourHint(installDetourDecision);
1128
+ result.installDetourHint = hint.installDetourHint;
1129
+ const augmented = jsonResult(result);
1130
+ resolve({
1131
+ content: [...augmented.content, hint.hintContentBlock],
1132
+ details: augmented.details,
1133
+ });
1134
+ return;
1135
+ }
794
1136
  resolve(jsonResult(result));
795
1137
  });
796
1138
  // Handle spawn errors
@@ -818,7 +1160,7 @@ function executeForeground(command, cwd, env, timeoutMs, input, signal, onUpdate
818
1160
  // ---------------------------------------------------------------------------
819
1161
  // Background execution
820
1162
  // ---------------------------------------------------------------------------
821
- function executeBackground(command, cwd, env, input, registry, logger, sandboxConfig, workspacePath, tempDir, description, pty) {
1163
+ function executeBackground(command, cwd, env, input, registry, logger, sandboxConfig, workspacePath, tempDir, description, pty, installDetourDecision, installDetourMode) {
822
1164
  const sessionId = generateSessionId();
823
1165
  const { bin, args, cwd: spawnCwd } = buildSpawnCommand(command, cwd, sandboxConfig, workspacePath ?? cwd, tempDir ?? tmpdir(), pty);
824
1166
  const child = spawn(bin, args, {
@@ -840,6 +1182,11 @@ function executeBackground(command, cwd, env, input, registry, logger, sandboxCo
840
1182
  maxOutputChars: BACKGROUND_MAX_OUTPUT_CHARS,
841
1183
  sandboxed: !!sandboxConfig,
842
1184
  ...(description && { description }),
1185
+ // Spawn-time decision capture (advise+overlap only).
1186
+ ...(installDetourDecision !== undefined
1187
+ && installDetourDecision.overlaps.length > 0
1188
+ && installDetourMode === "advise"
1189
+ && { installDetourDecision }),
843
1190
  };
844
1191
  // Output cleaners for stateful UTF-8 decode + ANSI strip + CR normalize + binary sanitize
845
1192
  const stdoutCleaner = createOutputCleaner();
@@ -886,6 +1233,23 @@ function executeBackground(command, cwd, env, input, registry, logger, sandboxCo
886
1233
  // Register in ProcessRegistry
887
1234
  registry.add(session);
888
1235
  logger?.debug({ toolName: "exec", sessionId, pid: child.pid }, "Background process spawned");
1236
+ // Explicit-bg envelope augmentation in advise mode
1237
+ if (installDetourMode === "advise" &&
1238
+ installDetourDecision !== undefined &&
1239
+ installDetourDecision.overlaps.length > 0) {
1240
+ const hint = buildInstallDetourHint(installDetourDecision);
1241
+ const augmented = jsonResult({
1242
+ status: "started",
1243
+ sessionId,
1244
+ pid: child.pid,
1245
+ ...(description && { description }),
1246
+ installDetourHint: hint.installDetourHint,
1247
+ });
1248
+ return {
1249
+ content: [...augmented.content, hint.hintContentBlock],
1250
+ details: augmented.details,
1251
+ };
1252
+ }
889
1253
  return jsonResult({
890
1254
  status: "started",
891
1255
  sessionId,