@cjhyy/code-shell-core 0.5.0-rc.0 → 0.5.0-rc.2

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 (283) hide show
  1. package/dist/agent/agent-definition-registry.d.ts +29 -0
  2. package/dist/agent/agent-definition-registry.js +74 -0
  3. package/dist/agent/agent-definition.d.ts +37 -0
  4. package/dist/agent/agent-definition.js +59 -0
  5. package/dist/arena/arena.js +11 -12
  6. package/dist/arena/context/context-tools.js +2 -4
  7. package/dist/arena/context/within-root.d.ts +7 -0
  8. package/dist/arena/context/within-root.js +15 -0
  9. package/dist/arena/iterate/phases/argue.js +8 -2
  10. package/dist/arena/iterate/phases/revise.js +1 -1
  11. package/dist/arena/iterate/phases/tournament.js +3 -3
  12. package/dist/arena/phases/adjudication.js +1 -4
  13. package/dist/arena/phases/build-consensus.js +1 -4
  14. package/dist/arena/phases/cross-review.js +2 -8
  15. package/dist/arena/phases/debate-rounds.js +1 -4
  16. package/dist/arena/phases/participant-research.js +1 -4
  17. package/dist/arena/phases/planning-detail-expansion.js +8 -10
  18. package/dist/arena/planner.js +0 -1
  19. package/dist/arena/providers/docs.d.ts +1 -0
  20. package/dist/arena/providers/docs.js +5 -2
  21. package/dist/arena/providers/repo.d.ts +1 -0
  22. package/dist/arena/providers/repo.js +5 -2
  23. package/dist/arena/strategies/utils.js +36 -3
  24. package/dist/arena/transitions.d.ts +7 -2
  25. package/dist/arena/transitions.js +9 -4
  26. package/dist/arena/types.d.ts +8 -2
  27. package/dist/automation/cron-expr.d.ts +31 -0
  28. package/dist/automation/cron-expr.js +151 -0
  29. package/dist/automation/index.d.ts +41 -0
  30. package/dist/automation/index.js +39 -0
  31. package/dist/automation/runner.d.ts +67 -0
  32. package/dist/automation/runner.js +56 -0
  33. package/dist/automation/scheduler.d.ts +156 -0
  34. package/dist/automation/scheduler.js +546 -0
  35. package/dist/automation/store.d.ts +39 -0
  36. package/dist/automation/store.js +119 -0
  37. package/dist/automation/write-policy.d.ts +34 -0
  38. package/dist/automation/write-policy.js +113 -0
  39. package/dist/automation/write-run.d.ts +51 -0
  40. package/dist/automation/write-run.js +38 -0
  41. package/dist/capability-control/index.d.ts +5 -0
  42. package/dist/capability-control/index.js +3 -0
  43. package/dist/capability-control/overlay.d.ts +49 -0
  44. package/dist/capability-control/overlay.js +89 -0
  45. package/dist/capability-control/project.d.ts +65 -0
  46. package/dist/capability-control/project.js +147 -0
  47. package/dist/capability-control/service.d.ts +70 -0
  48. package/dist/capability-control/service.js +147 -0
  49. package/dist/capability-control/types.d.ts +62 -0
  50. package/dist/capability-control/types.js +17 -0
  51. package/dist/cli/agent-server-stdio.d.ts +12 -1
  52. package/dist/cli/agent-server-stdio.js +118 -18
  53. package/dist/cli/agent-server-tcp.d.ts +20 -0
  54. package/dist/cli/agent-server-tcp.js +120 -0
  55. package/dist/cli/graceful-shutdown.d.ts +27 -0
  56. package/dist/cli/graceful-shutdown.js +30 -0
  57. package/dist/context/compaction.js +23 -15
  58. package/dist/context/manager.d.ts +0 -20
  59. package/dist/context/manager.js +0 -37
  60. package/dist/cron/cron-runtime.d.ts +2 -0
  61. package/dist/cron/cron-runtime.js +2 -0
  62. package/dist/cron/cron-store.d.ts +2 -0
  63. package/dist/cron/cron-store.js +2 -0
  64. package/dist/cron/scheduler.d.ts +5 -32
  65. package/dist/cron/scheduler.js +5 -113
  66. package/dist/data/openrouter-sync.js +1 -1
  67. package/dist/engine/engine.d.ts +256 -16
  68. package/dist/engine/engine.js +856 -200
  69. package/dist/engine/goal.d.ts +42 -0
  70. package/dist/engine/goal.js +52 -0
  71. package/dist/engine/image-compression.d.ts +81 -0
  72. package/dist/engine/image-compression.js +186 -0
  73. package/dist/engine/image-policy.d.ts +152 -0
  74. package/dist/engine/image-policy.js +165 -0
  75. package/dist/engine/reactive-threshold.d.ts +13 -0
  76. package/dist/engine/reactive-threshold.js +17 -0
  77. package/dist/engine/runtime.js +7 -0
  78. package/dist/engine/session-title.d.ts +13 -0
  79. package/dist/engine/session-title.js +34 -0
  80. package/dist/engine/turn-loop.d.ts +29 -8
  81. package/dist/engine/turn-loop.js +498 -316
  82. package/dist/git/parse-log.d.ts +13 -0
  83. package/dist/git/parse-log.js +21 -0
  84. package/dist/git/utils.d.ts +8 -6
  85. package/dist/git/utils.js +57 -26
  86. package/dist/git/worktree.js +33 -18
  87. package/dist/hooks/events.d.ts +23 -1
  88. package/dist/hooks/goal-stop-hook.d.ts +30 -0
  89. package/dist/hooks/goal-stop-hook.js +102 -0
  90. package/dist/hooks/hook-output.d.ts +33 -0
  91. package/dist/hooks/hook-output.js +79 -0
  92. package/dist/hooks/registry.d.ts +7 -0
  93. package/dist/hooks/registry.js +38 -1
  94. package/dist/hooks/shell-runner.d.ts +2 -0
  95. package/dist/hooks/shell-runner.js +43 -2
  96. package/dist/index.d.ts +32 -7
  97. package/dist/index.js +30 -3
  98. package/dist/llm/capabilities/reasoning-control.d.ts +24 -0
  99. package/dist/llm/capabilities/reasoning-control.js +38 -0
  100. package/dist/llm/capabilities/rules.js +33 -3
  101. package/dist/llm/capabilities/types.d.ts +22 -2
  102. package/dist/llm/clamp-max-tokens.d.ts +11 -0
  103. package/dist/llm/clamp-max-tokens.js +17 -0
  104. package/dist/llm/client-base.d.ts +41 -4
  105. package/dist/llm/client-base.js +67 -13
  106. package/dist/llm/client-factory.d.ts +3 -3
  107. package/dist/llm/client-factory.js +2 -2
  108. package/dist/llm/model-pool.d.ts +15 -11
  109. package/dist/llm/model-pool.js +23 -20
  110. package/dist/llm/provider-catalog.d.ts +2 -2
  111. package/dist/llm/providers/anthropic.d.ts +32 -2
  112. package/dist/llm/providers/anthropic.js +147 -8
  113. package/dist/llm/providers/openai.d.ts +11 -2
  114. package/dist/llm/providers/openai.js +253 -52
  115. package/dist/llm/reasoning-setting.d.ts +51 -0
  116. package/dist/llm/reasoning-setting.js +32 -0
  117. package/dist/llm/stop-reason.d.ts +13 -0
  118. package/dist/llm/stop-reason.js +21 -0
  119. package/dist/llm/strip-vision.d.ts +32 -0
  120. package/dist/llm/strip-vision.js +94 -0
  121. package/dist/llm/types.d.ts +5 -6
  122. package/dist/logging/logger.js +18 -4
  123. package/dist/logging/sanitize-messages.d.ts +10 -0
  124. package/dist/logging/sanitize-messages.js +90 -0
  125. package/dist/lsp/client.js +28 -8
  126. package/dist/lsp/manager.js +2 -1
  127. package/dist/lsp/root-path.d.ts +9 -0
  128. package/dist/lsp/root-path.js +12 -0
  129. package/dist/onboarding.js +18 -11
  130. package/dist/plugins/gitOps.d.ts +34 -0
  131. package/dist/plugins/gitOps.js +59 -3
  132. package/dist/plugins/installer/codex/convertAgents.d.ts +7 -0
  133. package/dist/plugins/installer/codex/convertAgents.js +55 -0
  134. package/dist/plugins/installer/codex/convertMcp.d.ts +8 -0
  135. package/dist/plugins/installer/codex/convertMcp.js +45 -0
  136. package/dist/plugins/installer/codex/convertSkills.d.ts +12 -0
  137. package/dist/plugins/installer/codex/convertSkills.js +33 -0
  138. package/dist/plugins/installer/detectFormat.d.ts +2 -0
  139. package/dist/plugins/installer/detectFormat.js +6 -0
  140. package/dist/plugins/installer/install.d.ts +7 -0
  141. package/dist/plugins/installer/install.js +99 -0
  142. package/dist/plugins/installer/installFromSource.d.ts +13 -0
  143. package/dist/plugins/installer/installFromSource.js +45 -0
  144. package/dist/plugins/installer/list.d.ts +9 -0
  145. package/dist/plugins/installer/list.js +30 -0
  146. package/dist/plugins/installer/loadPluginAgents.d.ts +3 -0
  147. package/dist/plugins/installer/loadPluginAgents.js +23 -0
  148. package/dist/plugins/installer/loadPluginMcp.d.ts +7 -0
  149. package/dist/plugins/installer/loadPluginMcp.js +74 -0
  150. package/dist/plugins/installer/parseSource.d.ts +20 -0
  151. package/dist/plugins/installer/parseSource.js +74 -0
  152. package/dist/plugins/installer/paths.d.ts +5 -0
  153. package/dist/plugins/installer/paths.js +25 -0
  154. package/dist/plugins/installer/types.d.ts +49 -0
  155. package/dist/plugins/installer/types.js +27 -0
  156. package/dist/plugins/installer/uninstall.d.ts +2 -0
  157. package/dist/plugins/installer/uninstall.js +14 -0
  158. package/dist/plugins/installer/update.d.ts +14 -0
  159. package/dist/plugins/installer/update.js +65 -0
  160. package/dist/plugins/loadPluginHooks.d.ts +8 -1
  161. package/dist/plugins/loadPluginHooks.js +11 -1
  162. package/dist/plugins/marketplaceManager.d.ts +8 -1
  163. package/dist/plugins/marketplaceManager.js +38 -4
  164. package/dist/plugins/pluginCommandHook.js +34 -0
  165. package/dist/plugins/pluginInstaller.d.ts +21 -0
  166. package/dist/plugins/pluginInstaller.js +128 -29
  167. package/dist/plugins/schemas.js +40 -6
  168. package/dist/plugins/types.d.ts +4 -0
  169. package/dist/preset/index.js +16 -0
  170. package/dist/prompt/composer.d.ts +10 -0
  171. package/dist/prompt/composer.js +29 -4
  172. package/dist/prompt/sections/orchestration.md +8 -0
  173. package/dist/protocol/chat-session-manager.d.ts +8 -1
  174. package/dist/protocol/chat-session-manager.js +10 -0
  175. package/dist/protocol/chat-session.d.ts +17 -0
  176. package/dist/protocol/chat-session.js +28 -0
  177. package/dist/protocol/redact.d.ts +50 -0
  178. package/dist/protocol/redact.js +71 -0
  179. package/dist/protocol/server.d.ts +41 -2
  180. package/dist/protocol/server.js +231 -33
  181. package/dist/protocol/tcp-transport.d.ts +45 -0
  182. package/dist/protocol/tcp-transport.js +74 -0
  183. package/dist/protocol/types.d.ts +23 -0
  184. package/dist/protocol/types.js +6 -0
  185. package/dist/remote/bridge.d.ts +11 -2
  186. package/dist/remote/bridge.js +61 -33
  187. package/dist/run/ArtifactTracker.js +5 -4
  188. package/dist/run/EngineRunner.d.ts +27 -1
  189. package/dist/run/EngineRunner.js +46 -8
  190. package/dist/run/FileRunStore.js +32 -8
  191. package/dist/run/RunLock.d.ts +28 -2
  192. package/dist/run/RunLock.js +49 -7
  193. package/dist/run/RunManager.js +51 -3
  194. package/dist/run/RunQueue.d.ts +1 -0
  195. package/dist/run/RunQueue.js +8 -2
  196. package/dist/run/factory.d.ts +7 -0
  197. package/dist/run/factory.js +1 -0
  198. package/dist/run/index.d.ts +2 -2
  199. package/dist/run/index.js +1 -1
  200. package/dist/run/redirect-target.d.ts +7 -0
  201. package/dist/run/redirect-target.js +13 -0
  202. package/dist/run/types.js +1 -1
  203. package/dist/services/browser-open.d.ts +13 -0
  204. package/dist/services/browser-open.js +16 -0
  205. package/dist/services/dream-consolidation.d.ts +57 -0
  206. package/dist/services/dream-consolidation.js +151 -0
  207. package/dist/services/memory-orchestrator.js +18 -0
  208. package/dist/services/notifier.d.ts +8 -0
  209. package/dist/services/notifier.js +39 -25
  210. package/dist/services/oauth.js +5 -7
  211. package/dist/services/session-memory-sort.d.ts +8 -0
  212. package/dist/services/session-memory-sort.js +9 -0
  213. package/dist/services/session-memory.js +8 -9
  214. package/dist/session/file-history.js +10 -3
  215. package/dist/session/session-manager.d.ts +23 -1
  216. package/dist/session/session-manager.js +73 -2
  217. package/dist/settings/disk-defaults.d.ts +35 -0
  218. package/dist/settings/disk-defaults.js +24 -0
  219. package/dist/settings/manager.d.ts +45 -1
  220. package/dist/settings/manager.js +121 -17
  221. package/dist/settings/manager.test.d.ts +1 -0
  222. package/dist/settings/manager.test.js +73 -0
  223. package/dist/settings/personalization.d.ts +19 -0
  224. package/dist/settings/personalization.js +7 -0
  225. package/dist/settings/schema.d.ts +1163 -126
  226. package/dist/settings/schema.js +126 -10
  227. package/dist/skills/scanner.d.ts +13 -6
  228. package/dist/skills/scanner.js +23 -4
  229. package/dist/tool-system/builtin/add-marketplace.d.ts +12 -0
  230. package/dist/tool-system/builtin/add-marketplace.js +76 -0
  231. package/dist/tool-system/builtin/agent-registry.d.ts +15 -0
  232. package/dist/tool-system/builtin/agent-registry.js +14 -4
  233. package/dist/tool-system/builtin/agent.d.ts +55 -0
  234. package/dist/tool-system/builtin/agent.js +165 -17
  235. package/dist/tool-system/builtin/apply-patch/index.js +22 -3
  236. package/dist/tool-system/builtin/arena.js +6 -7
  237. package/dist/tool-system/builtin/complete-goal.d.ts +25 -0
  238. package/dist/tool-system/builtin/complete-goal.js +45 -0
  239. package/dist/tool-system/builtin/cron.js +53 -9
  240. package/dist/tool-system/builtin/edit.d.ts +2 -1
  241. package/dist/tool-system/builtin/edit.js +6 -1
  242. package/dist/tool-system/builtin/generate-image.d.ts +25 -0
  243. package/dist/tool-system/builtin/generate-image.js +146 -0
  244. package/dist/tool-system/builtin/glob.js +7 -0
  245. package/dist/tool-system/builtin/grep.js +6 -0
  246. package/dist/tool-system/builtin/index.d.ts +18 -1
  247. package/dist/tool-system/builtin/index.js +62 -1
  248. package/dist/tool-system/builtin/notebook-edit.d.ts +2 -1
  249. package/dist/tool-system/builtin/notebook-edit.js +8 -1
  250. package/dist/tool-system/builtin/read.d.ts +2 -1
  251. package/dist/tool-system/builtin/read.js +9 -1
  252. package/dist/tool-system/builtin/skill.js +23 -5
  253. package/dist/tool-system/builtin/sleep.js +9 -3
  254. package/dist/tool-system/builtin/task.js +4 -1
  255. package/dist/tool-system/builtin/update-automation-memory.d.ts +24 -0
  256. package/dist/tool-system/builtin/update-automation-memory.js +60 -0
  257. package/dist/tool-system/builtin/view-image.d.ts +18 -0
  258. package/dist/tool-system/builtin/view-image.js +96 -0
  259. package/dist/tool-system/builtin/web-search.d.ts +4 -1
  260. package/dist/tool-system/builtin/web-search.js +36 -3
  261. package/dist/tool-system/builtin/write.d.ts +2 -1
  262. package/dist/tool-system/builtin/write.js +7 -1
  263. package/dist/tool-system/context.d.ts +43 -0
  264. package/dist/tool-system/executor.d.ts +0 -1
  265. package/dist/tool-system/executor.js +34 -40
  266. package/dist/tool-system/investigation-guard.d.ts +4 -1
  267. package/dist/tool-system/investigation-guard.js +19 -3
  268. package/dist/tool-system/mcp-manager.d.ts +36 -12
  269. package/dist/tool-system/mcp-manager.js +173 -10
  270. package/dist/tool-system/path-policy.d.ts +94 -0
  271. package/dist/tool-system/path-policy.js +279 -0
  272. package/dist/tool-system/permission.js +43 -50
  273. package/dist/tool-system/plan-mode-allowlist.d.ts +21 -0
  274. package/dist/tool-system/plan-mode-allowlist.js +43 -0
  275. package/dist/tool-system/registry.js +12 -1
  276. package/dist/tool-system/validation.d.ts +10 -0
  277. package/dist/tool-system/validation.js +16 -3
  278. package/dist/types.d.ts +90 -15
  279. package/dist/utils/format.js +4 -2
  280. package/dist/utils/lockfile.js +9 -2
  281. package/dist/utils/theme.d.ts +6 -0
  282. package/dist/utils/theme.js +4 -2
  283. package/package.json +2 -1
@@ -0,0 +1,71 @@
1
+ import { redactSecrets } from "../logging/sanitize-messages.js";
2
+ /**
3
+ * Redaction helpers for protocol query responses.
4
+ *
5
+ * The protocol exposes config snapshots to any connected client. Raw API keys,
6
+ * provider credentials, and authorization headers must never leave this
7
+ * boundary verbatim — the client can use derived fields (`hasApiKey`,
8
+ * `apiKeyPreview`) to render provider/model status without holding the secret.
9
+ *
10
+ * Two entry points:
11
+ * - redactConfigForResponse: rewrites the LLM section of the config-query
12
+ * payload, removing `apiKey` and adding derived presence/preview fields.
13
+ * - maskSecretValue: scrubs a single (key, value) pair returned by
14
+ * config_get, where the client picks the key — including `llm.apiKey`.
15
+ */
16
+ /** Match dotted/camel/snake variants of obviously secret keys. */
17
+ const SECRET_KEY_RE = /(^|[._-])(api[_-]?key|authorization|x[_-]api[_-]key|bearer[_-]?token|access[_-]?token|refresh[_-]?token|secret|password|client[_-]?secret)($|[._-])/i;
18
+ /** Produce a short preview (`sk-…abcd`) so UIs can render presence without revealing the secret. */
19
+ export function makeApiKeyPreview(apiKey) {
20
+ if (typeof apiKey !== "string" || apiKey.length === 0)
21
+ return undefined;
22
+ if (apiKey.length <= 8)
23
+ return "…";
24
+ return `${apiKey.slice(0, 3)}…${apiKey.slice(-4)}`;
25
+ }
26
+ /**
27
+ * Return true when a dotted setting key path points at a secret field.
28
+ * Used to decide whether `config_get`'s response should redact the value.
29
+ */
30
+ export function isSecretKeyPath(key) {
31
+ return SECRET_KEY_RE.test(key);
32
+ }
33
+ /**
34
+ * Redact a single value returned by config_get.
35
+ *
36
+ * Two-stage rule:
37
+ * 1. If the caller-supplied key path itself looks secret
38
+ * ("llm.apiKey", "headers.authorization", …) → collapse to "[redacted]".
39
+ * 2. Otherwise, if the value is an object/array (e.g. config_get("llm") or
40
+ * config_get("providers")), walk it with the shared secret redactor
41
+ * from logging/sanitize-messages so nested apiKey / token / Bearer
42
+ * fields don't slip through verbatim. Primitives pass unchanged.
43
+ *
44
+ * Pre-fix this only inspected the top-level key string, so
45
+ * config_get("providers") returned an array of provider objects with raw
46
+ * apiKey values intact. That defeated the purpose of the boundary.
47
+ */
48
+ export function maskSecretValue(key, value) {
49
+ if (isSecretKeyPath(key))
50
+ return "[redacted]";
51
+ if (value && typeof value === "object") {
52
+ return redactSecrets(value);
53
+ }
54
+ return value;
55
+ }
56
+ /**
57
+ * Rewrite the LLM section of a config snapshot for transport. We only forward
58
+ * fields the client legitimately needs to render status; secrets become
59
+ * derived flags.
60
+ */
61
+ export function redactLlmConfig(llm) {
62
+ const preview = makeApiKeyPreview(llm.apiKey);
63
+ return {
64
+ provider: llm.provider,
65
+ model: llm.model,
66
+ baseUrl: llm.baseUrl,
67
+ maxTokens: llm.maxTokens,
68
+ hasApiKey: typeof llm.apiKey === "string" && llm.apiKey.length > 0,
69
+ ...(preview ? { apiKeyPreview: preview } : {}),
70
+ };
71
+ }
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { Transport } from "./transport.js";
17
17
  import type { Engine } from "../engine/engine.js";
18
+ import type { ValidatedSettings } from "../settings/schema.js";
18
19
  import type { ChatSessionManager } from "./chat-session-manager.js";
19
20
  export interface AgentServerOptions {
20
21
  /**
@@ -29,11 +30,38 @@ export interface AgentServerOptions {
29
30
  */
30
31
  engine?: Engine;
31
32
  transport: Transport;
33
+ /**
34
+ * Reads fresh settings from disk for config hot-reload (layer 2). MUST be
35
+ * the SAME closure the engineFactory uses for new sessions (e.g.
36
+ * agent-server-stdio's `freshSettings`) so a reloaded running session and a
37
+ * newly-created session converge on identical disk config — no divergence.
38
+ * When absent, `configure({ reloadSettings })` returns an explicit error.
39
+ */
40
+ settingsReader?: () => ValidatedSettings;
32
41
  }
33
42
  export declare class AgentServer {
34
43
  private readonly chatManager;
35
44
  private readonly legacyEngine;
45
+ private globalQueryEngine;
36
46
  private transport;
47
+ /** Fresh-disk settings reader for config hot-reload; null when not wired. */
48
+ private readonly settingsReader;
49
+ /**
50
+ * Monotonic config-reload version, bumped per reloadSettings request so each
51
+ * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
52
+ */
53
+ private configVersion;
54
+ /**
55
+ * JSON of the last disk-default patch broadcast to ALL live sessions (#6).
56
+ * When a new reloadSettings request produces a byte-identical patch we SKIP
57
+ * the entire forEachSession broadcast — every session's refreshRuntimeConfig
58
+ * would otherwise unconditionally reloadHooks() (invalidate + full disk
59
+ * re-read + hook teardown/re-register) per call, and the personalization UI
60
+ * auto-saves on a 600ms debounce, so identical re-saves caused K× hook churn
61
+ * per keystroke-pause. A genuine change still differs in JSON → propagates.
62
+ * null until the first broadcast.
63
+ */
64
+ private lastBroadcastPatch;
37
65
  private running;
38
66
  private abortController;
39
67
  /** Pending approval requests: requestId → resolve function (legacy path) */
@@ -67,11 +95,22 @@ export declare class AgentServer {
67
95
  /**
68
96
  * Ask the client to answer a question from the agent (legacy single-engine path).
69
97
  */
98
+ /**
99
+ * Per-session AskUserQuestion for the chatManager path. Mirrors
100
+ * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
101
+ * (the chatManager approve handler looks there, keyed by sessionId+requestId)
102
+ * and tags the notify with sessionId so the renderer routes the question to
103
+ * the right chat tab.
104
+ */
105
+ private requestAskUserForSession;
70
106
  private requestAskUserFromClient;
71
107
  private notify;
72
108
  /**
73
- * Get any available engine — used for global query ops when chatManager is
74
- * present but no legacyEngine was supplied. Borrows the first live session.
109
+ * Get any available engine — used for global query/configure ops when
110
+ * chatManager is present but no legacyEngine was supplied. Prefer a live chat
111
+ * session; before the first user turn, lazily build a detached engine through
112
+ * the manager's factory so global UI surfaces (/login, model manager, model
113
+ * selector) can query models/providers without requiring a chat session first.
75
114
  */
76
115
  private anyEngine;
77
116
  close(): void;
@@ -14,14 +14,36 @@
14
14
  * falls back to borrowing any live session's engine for those reads.
15
15
  */
16
16
  import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
17
+ import { diskDefaultsFrom } from "../engine/engine.js";
17
18
  import { setInteractiveApprovalFn } from "../tool-system/permission.js";
18
19
  import { getArenaStatus } from "../tool-system/builtin/arena.js";
19
20
  import { agentNotificationBus } from "../tool-system/builtin/agent-notifications.js";
20
21
  import { nanoid } from "nanoid";
22
+ import { redactLlmConfig, maskSecretValue } from "./redact.js";
23
+ import { redactSecrets } from "../logging/sanitize-messages.js";
21
24
  export class AgentServer {
22
25
  chatManager;
23
26
  legacyEngine;
27
+ globalQueryEngine = null;
24
28
  transport;
29
+ /** Fresh-disk settings reader for config hot-reload; null when not wired. */
30
+ settingsReader;
31
+ /**
32
+ * Monotonic config-reload version, bumped per reloadSettings request so each
33
+ * Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
34
+ */
35
+ configVersion = 0;
36
+ /**
37
+ * JSON of the last disk-default patch broadcast to ALL live sessions (#6).
38
+ * When a new reloadSettings request produces a byte-identical patch we SKIP
39
+ * the entire forEachSession broadcast — every session's refreshRuntimeConfig
40
+ * would otherwise unconditionally reloadHooks() (invalidate + full disk
41
+ * re-read + hook teardown/re-register) per call, and the personalization UI
42
+ * auto-saves on a 600ms debounce, so identical re-saves caused K× hook churn
43
+ * per keystroke-pause. A genuine change still differs in JSON → propagates.
44
+ * null until the first broadcast.
45
+ */
46
+ lastBroadcastPatch = null;
25
47
  // ── Legacy single-engine state (used when chatManager is null) ──
26
48
  running = false;
27
49
  abortController = null;
@@ -41,6 +63,7 @@ export class AgentServer {
41
63
  constructor(options) {
42
64
  this.chatManager = options.chatManager ?? null;
43
65
  this.legacyEngine = options.engine ?? null;
66
+ this.settingsReader = options.settingsReader ?? null;
44
67
  if (!this.chatManager && !this.legacyEngine) {
45
68
  throw new Error("AgentServer: either chatManager or engine must be supplied");
46
69
  }
@@ -53,16 +76,19 @@ export class AgentServer {
53
76
  });
54
77
  }
55
78
  });
56
- // Wire approval flow for the legacy single-engine path.
57
- // In the chatManager path approval flows are per-session and registered
58
- // when the session's engine raises an approval request via onStream.
79
+ setInteractiveApprovalFn((request) => {
80
+ return this.requestApprovalFromClient(request);
81
+ });
59
82
  if (this.legacyEngine) {
60
- setInteractiveApprovalFn((request) => {
61
- return this.requestApprovalFromClient(request);
62
- });
63
- this.legacyEngine.setAskUser((question, opts) => {
64
- return this.requestAskUserFromClient(question, opts);
65
- });
83
+ // Only wire an interactive askUser when a human is present. For
84
+ // unattended (headless) runs we leave askUser undefined so
85
+ // AskUserQuestion hits its headless-error branch and returns immediately
86
+ // instead of suspending until the tool-exec timeout (~300s).
87
+ if (!this.legacyEngine.isHeadless()) {
88
+ this.legacyEngine.setAskUser((question, opts) => {
89
+ return this.requestAskUserFromClient(question, opts);
90
+ });
91
+ }
66
92
  }
67
93
  // B2.2 — forward background sub-agent completion events through the
68
94
  // protocol so Desktop / SDK / remote AgentClients see them too. The
@@ -140,9 +166,24 @@ export class AgentServer {
140
166
  session.engine.setPlanMode(params.planMode);
141
167
  }
142
168
  const sid = params.sessionId;
169
+ // Wire AskUserQuestion for this interactive session. The chatManager path
170
+ // builds a fresh per-session Engine via engineFactory, which (unlike the
171
+ // legacy single-engine path) never had askUser wired — so AskUserQuestion
172
+ // always fell into its "not available in headless mode" branch in normal
173
+ // chat (and in a resumed automation session). Route it to the client with
174
+ // this session's id so the renderer attributes the question to the right
175
+ // tab, resolving via the session's own pendingApprovals. Skip genuinely
176
+ // headless engines (no human to answer).
177
+ if (!session.engine.isHeadless()) {
178
+ session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
179
+ }
143
180
  try {
144
181
  const result = await session.enqueueTurn(params.task, {
145
182
  cwd: params.cwd,
183
+ goal: typeof params.goal === "string" ||
184
+ (params.goal != null && typeof params.goal === "object")
185
+ ? params.goal
186
+ : undefined,
146
187
  onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
147
188
  });
148
189
  const runResult = {
@@ -191,12 +232,20 @@ export class AgentServer {
191
232
  };
192
233
  // TodoWrite emits task_update directly through ToolContext.streamCallback;
193
234
  // no module singleton wiring needed anymore.
235
+ // Snapshot the controller for the catch block — `this.abortController`
236
+ // is nulled in the `finally`, so by the time we'd want to check
237
+ // `signal.aborted` below it's already gone.
238
+ const runController = this.abortController;
194
239
  try {
195
240
  const result = await this.legacyEngine.run(params.task, {
196
241
  cwd: params.cwd,
197
242
  sessionId: params.sessionId,
198
- signal: this.abortController.signal,
243
+ signal: runController.signal,
199
244
  onStream: streamToClient,
245
+ goal: typeof params.goal === "string" ||
246
+ (params.goal != null && typeof params.goal === "object")
247
+ ? params.goal
248
+ : undefined,
200
249
  });
201
250
  const runResult = {
202
251
  text: result.text,
@@ -208,7 +257,28 @@ export class AgentServer {
208
257
  this.transport.send(createResponse(req.id, runResult));
209
258
  }
210
259
  catch (err) {
211
- this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
260
+ // ESC / Stop path. If the user cancelled, the engine's
261
+ // in-flight LLM call or Bash spawn rejects with an AbortError
262
+ // that bubbles up here. Don't surface it as InternalError —
263
+ // the user already knows they pressed stop; clients should
264
+ // just clear busy.
265
+ const errName = err.name;
266
+ const aborted = runController.signal.aborted ||
267
+ errName === "AbortError" ||
268
+ errName === "APIUserAbortError";
269
+ if (aborted) {
270
+ const cancelledResult = {
271
+ text: "",
272
+ reason: "aborted_streaming",
273
+ sessionId: params.sessionId ?? "",
274
+ turnCount: 0,
275
+ usage: { promptTokens: 0, completionTokens: 0, totalTokens: 0 },
276
+ };
277
+ this.transport.send(createResponse(req.id, cancelledResult));
278
+ }
279
+ else {
280
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
281
+ }
212
282
  }
213
283
  finally {
214
284
  this.running = false;
@@ -306,6 +376,29 @@ export class AgentServer {
306
376
  if (typeof params.permissionMode === "string") {
307
377
  s.engine.setPermissionMode(params.permissionMode);
308
378
  }
379
+ // Per-session model switch — the missing piece that made model changes
380
+ // worker-global (session-isolation research §3). requestModelSwitch
381
+ // applies immediately when idle, defers to the run boundary when busy
382
+ // so it never swaps the model under a running LLM client.
383
+ if (typeof params.model === "string") {
384
+ try {
385
+ s.requestModelSwitch(params.model);
386
+ }
387
+ catch (err) {
388
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
389
+ return;
390
+ }
391
+ }
392
+ // Hot-reload disk-default config onto this one session (layer 2).
393
+ if (params.reloadSettings === true) {
394
+ if (!this.settingsReader) {
395
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "reloadSettings is not supported by this AgentServer: no settingsReader is wired"));
396
+ return;
397
+ }
398
+ const settings = this.settingsReader();
399
+ const version = ++this.configVersion;
400
+ s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings), version);
401
+ }
309
402
  this.transport.send(createResponse(req.id, { ok: true }));
310
403
  return;
311
404
  }
@@ -335,6 +428,42 @@ export class AgentServer {
335
428
  if (params.planMode !== undefined && engine) {
336
429
  engine.setPlanMode(params.planMode);
337
430
  }
431
+ // Config hot-reload layer 2: re-read disk settings once and push the
432
+ // disk-default config patch (+ settings-hook reload + incremental MCP
433
+ // connect) onto every live session. Parallel to reloadModels; no new
434
+ // protocol method. In-flight turns are untouched — refreshRuntimeConfig
435
+ // only mutates this.config, picked up at the next turn boundary.
436
+ if (params.reloadSettings === true) {
437
+ if (!this.settingsReader) {
438
+ this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "reloadSettings is not supported by this AgentServer: no settingsReader is wired"));
439
+ return;
440
+ }
441
+ if (!this.chatManager) {
442
+ // Single-engine host (legacyEngine / anyEngine, no chatManager): there
443
+ // are no sessions to fan out to, but the one engine must still pick up
444
+ // the reload — otherwise we'd report ok:true while silently dropping it.
445
+ // Mirrors how reloadModels/model/planMode above act on `engine`.
446
+ if (engine) {
447
+ const settings = this.settingsReader();
448
+ engine.refreshRuntimeConfig(diskDefaultsFrom(settings), ++this.configVersion);
449
+ }
450
+ this.transport.send(createResponse(req.id, { ok: true }));
451
+ return;
452
+ }
453
+ const settings = this.settingsReader();
454
+ const patch = diskDefaultsFrom(settings);
455
+ // #6: content short-circuit. If the disk-default patch is byte-identical
456
+ // to the last one we broadcast, the disk config that matters to running
457
+ // sessions hasn't changed — skip the whole forEachSession (and the per-
458
+ // session reloadHooks churn it triggers). Still return ok; a genuine
459
+ // change differs in JSON and propagates normally.
460
+ const patchJson = JSON.stringify(patch);
461
+ if (patchJson !== this.lastBroadcastPatch) {
462
+ this.lastBroadcastPatch = patchJson;
463
+ const version = ++this.configVersion;
464
+ this.chatManager.forEachSession((s) => s.engine.refreshRuntimeConfig(patch, version));
465
+ }
466
+ }
338
467
  this.transport.send(createResponse(req.id, { ok: true }));
339
468
  }
340
469
  // ─── Query ──────────────────────────────────────────────────────
@@ -361,12 +490,15 @@ export class AgentServer {
361
490
  case "sessions": {
362
491
  if (this.chatManager) {
363
492
  // Return the ChatSessionManager's live sessions
364
- const sessions = [...this.chatManager.sessions.entries()].map(([id, s]) => ({
365
- sessionId: id,
366
- busy: s.isBusy(),
367
- queueDepth: s.queueDepth(),
368
- lastActivityAt: s.lastActivityAt,
369
- }));
493
+ const sessions = [];
494
+ this.chatManager.forEachSession((s) => {
495
+ sessions.push({
496
+ sessionId: s.id,
497
+ busy: s.isBusy(),
498
+ queueDepth: s.queueDepth(),
499
+ lastActivityAt: s.lastActivityAt,
500
+ });
501
+ });
370
502
  this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
371
503
  }
372
504
  else if (engine) {
@@ -393,15 +525,9 @@ export class AgentServer {
393
525
  model: config.llm.model,
394
526
  cwd: config.cwd,
395
527
  maxContextTokens: config.maxContextTokens,
396
- llm: {
397
- provider: config.llm.provider,
398
- model: config.llm.model,
399
- apiKey: config.llm.apiKey,
400
- baseUrl: config.llm.baseUrl,
401
- temperature: config.llm.temperature,
402
- maxTokens: config.llm.maxTokens,
403
- enableStreaming: config.llm.enableStreaming,
404
- },
528
+ // apiKey is redacted at this boundary: clients only get
529
+ // hasApiKey + optional apiKeyPreview. See protocol/redact.ts.
530
+ llm: redactLlmConfig(config.llm),
405
531
  },
406
532
  }));
407
533
  break;
@@ -499,7 +625,15 @@ export class AgentServer {
499
625
  cachedAt: cache ? cache.fetchedAt : undefined,
500
626
  };
501
627
  });
502
- this.transport.send(createResponse(req.id, { type: "providers", data: enriched }));
628
+ // Strip apiKey / other secret-shaped fields from each provider
629
+ // before sending. `...p` above spreads the whole provider record,
630
+ // which includes apiKey verbatim — without this redact pass, any
631
+ // protocol client could call query("providers") and harvest
632
+ // credentials. The shared redactor preserves
633
+ // presence-without-value (hasApiKey-style consumers still see the
634
+ // field shape, just with [redacted] in place of the secret).
635
+ const safe = redactSecrets(enriched);
636
+ this.transport.send(createResponse(req.id, { type: "providers", data: safe }));
503
637
  }
504
638
  catch (err) {
505
639
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -521,7 +655,11 @@ export class AgentServer {
521
655
  if (!key)
522
656
  throw new Error("key is required for config_set");
523
657
  engine.updateConfig(key, value);
524
- this.transport.send(createResponse(req.id, { type: "config_set", data: { key, value } }));
658
+ // Echo back through the same secret-aware masker as config_get so
659
+ // a `config_set("llm.apiKey", "...")` confirmation doesn't ship
660
+ // the new secret through the response/log path.
661
+ const safeValue = maskSecretValue(key, value);
662
+ this.transport.send(createResponse(req.id, { type: "config_set", data: { key, value: safeValue } }));
525
663
  }
526
664
  catch (err) {
527
665
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -538,7 +676,9 @@ export class AgentServer {
538
676
  if (!key)
539
677
  throw new Error("key is required for config_get");
540
678
  const value = engine.readSetting(key);
541
- this.transport.send(createResponse(req.id, { type: "config_get", data: { key, value } }));
679
+ // Mask secret-looking keys (apiKey/token/secret/…) at the boundary.
680
+ const safeValue = maskSecretValue(key, value);
681
+ this.transport.send(createResponse(req.id, { type: "config_get", data: { key, value: safeValue } }));
542
682
  }
543
683
  catch (err) {
544
684
  this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
@@ -743,6 +883,54 @@ export class AgentServer {
743
883
  /**
744
884
  * Ask the client to answer a question from the agent (legacy single-engine path).
745
885
  */
886
+ /**
887
+ * Per-session AskUserQuestion for the chatManager path. Mirrors
888
+ * requestAskUserFromClient but resolves via the SESSION's pendingApprovals
889
+ * (the chatManager approve handler looks there, keyed by sessionId+requestId)
890
+ * and tags the notify with sessionId so the renderer routes the question to
891
+ * the right chat tab.
892
+ */
893
+ requestAskUserForSession(session, sessionId, question, opts) {
894
+ return new Promise((resolve) => {
895
+ const requestId = nanoid(12);
896
+ session.pendingApprovals.set(requestId, (decision) => {
897
+ this.clearApprovalTimer(requestId);
898
+ const result = decision;
899
+ if (result && typeof result === "object" && "approved" in result) {
900
+ resolve(result.approved ? (result.answer ?? "") : (result.reason ?? "(user declined to answer)"));
901
+ }
902
+ else {
903
+ // chatManager approve handler resolves with the raw decision value.
904
+ resolve(typeof decision === "string" ? decision : "");
905
+ }
906
+ });
907
+ const timer = setTimeout(() => {
908
+ if (session.pendingApprovals.has(requestId)) {
909
+ session.pendingApprovals.delete(requestId);
910
+ this.approvalTimers.delete(requestId);
911
+ resolve("(approval timed out)");
912
+ }
913
+ }, AgentServer.APPROVAL_TIMEOUT_MS);
914
+ this.approvalTimers.set(requestId, timer);
915
+ const args = { question };
916
+ if (opts?.header !== undefined)
917
+ args.header = opts.header;
918
+ if (opts?.options !== undefined)
919
+ args.options = opts.options;
920
+ if (opts?.multiSelect !== undefined)
921
+ args.multiSelect = opts.multiSelect;
922
+ this.notify(Methods.ApprovalRequest, {
923
+ sessionId,
924
+ requestId,
925
+ request: {
926
+ toolName: "__ask_user__",
927
+ args,
928
+ description: question,
929
+ riskLevel: "low",
930
+ },
931
+ });
932
+ });
933
+ }
746
934
  requestAskUserFromClient(question, opts) {
747
935
  return new Promise((resolve) => {
748
936
  const requestId = nanoid(12);
@@ -785,16 +973,26 @@ export class AgentServer {
785
973
  this.transport.send(createNotification(method, params));
786
974
  }
787
975
  /**
788
- * Get any available engine — used for global query ops when chatManager is
789
- * present but no legacyEngine was supplied. Borrows the first live session.
976
+ * Get any available engine — used for global query/configure ops when
977
+ * chatManager is present but no legacyEngine was supplied. Prefer a live chat
978
+ * session; before the first user turn, lazily build a detached engine through
979
+ * the manager's factory so global UI surfaces (/login, model manager, model
980
+ * selector) can query models/providers without requiring a chat session first.
790
981
  */
791
982
  anyEngine() {
792
983
  if (!this.chatManager)
793
984
  return null;
794
- // Access private `sessions` map — acceptable for same-package access.
795
985
  const sessions = this.chatManager.sessions;
796
986
  const first = sessions.values().next().value;
797
- return first ? first.engine : null;
987
+ if (first)
988
+ return first.engine;
989
+ if (this.globalQueryEngine)
990
+ return this.globalQueryEngine;
991
+ const factory = this.chatManager.factory;
992
+ if (typeof factory !== "function")
993
+ return null;
994
+ this.globalQueryEngine = factory({});
995
+ return this.globalQueryEngine;
798
996
  }
799
997
  // ─── Lifecycle ──────────────────────────────────────────────────
800
998
  close() {
@@ -0,0 +1,45 @@
1
+ /**
2
+ * TcpTransport — newline-delimited JSON over a TCP socket (Duplex stream).
3
+ *
4
+ * Phase 6 of the automation plan: lets the same AgentServer that runs over
5
+ * stdio (agent-server-stdio) also serve over a network socket, so a headless
6
+ * host can be reached by a remote/local client. The transport implements the
7
+ * 3-method Transport interface (send/onMessage/close); AgentServer is unchanged
8
+ * (§0 of docs/automation-plan-2026-05-31.md).
9
+ *
10
+ * Framing mirrors StdioTransport: one JSON value per line. The socket core is
11
+ * separated from listen/accept (`listenTcp`) so the framing is unit-testable
12
+ * against any Duplex without opening a real port.
13
+ *
14
+ * v1 security: intended for localhost / SSH-tunnel use only. There is NO
15
+ * authentication here — do not bind to a public interface without adding auth.
16
+ */
17
+ import { type Server, type Socket } from "node:net";
18
+ import type { Duplex } from "node:stream";
19
+ import type { Transport } from "./transport.js";
20
+ import type { RpcMessage } from "./types.js";
21
+ /** Wrap a connected Duplex socket as a Transport (NDJSON framing). */
22
+ export declare class SocketTransport implements Transport {
23
+ private readonly socket;
24
+ private rl;
25
+ private handlers;
26
+ constructor(socket: Duplex);
27
+ send(message: RpcMessage): void;
28
+ onMessage(handler: (message: RpcMessage) => void): void;
29
+ close(): void;
30
+ }
31
+ export interface TcpListenResult {
32
+ server: Server;
33
+ /** The actual port bound (useful when 0 was requested). */
34
+ port: number;
35
+ close(): Promise<void>;
36
+ }
37
+ /**
38
+ * Listen on host:port and invoke `onConnection` with a SocketTransport for each
39
+ * accepted client. Resolves once listening. Defaults to 127.0.0.1 — never bind
40
+ * 0.0.0.0 without auth (v1 has none).
41
+ */
42
+ export declare function listenTcp(opts: {
43
+ port: number;
44
+ host?: string;
45
+ }, onConnection: (transport: SocketTransport, socket: Socket) => void): Promise<TcpListenResult>;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * TcpTransport — newline-delimited JSON over a TCP socket (Duplex stream).
3
+ *
4
+ * Phase 6 of the automation plan: lets the same AgentServer that runs over
5
+ * stdio (agent-server-stdio) also serve over a network socket, so a headless
6
+ * host can be reached by a remote/local client. The transport implements the
7
+ * 3-method Transport interface (send/onMessage/close); AgentServer is unchanged
8
+ * (§0 of docs/automation-plan-2026-05-31.md).
9
+ *
10
+ * Framing mirrors StdioTransport: one JSON value per line. The socket core is
11
+ * separated from listen/accept (`listenTcp`) so the framing is unit-testable
12
+ * against any Duplex without opening a real port.
13
+ *
14
+ * v1 security: intended for localhost / SSH-tunnel use only. There is NO
15
+ * authentication here — do not bind to a public interface without adding auth.
16
+ */
17
+ import { createServer } from "node:net";
18
+ import { createInterface } from "node:readline";
19
+ /** Wrap a connected Duplex socket as a Transport (NDJSON framing). */
20
+ export class SocketTransport {
21
+ socket;
22
+ rl;
23
+ handlers = [];
24
+ constructor(socket) {
25
+ this.socket = socket;
26
+ this.rl = createInterface({ input: socket });
27
+ this.rl.on("line", (line) => {
28
+ if (!line.trim())
29
+ return;
30
+ try {
31
+ const msg = JSON.parse(line);
32
+ for (const handler of this.handlers)
33
+ handler(msg);
34
+ }
35
+ catch {
36
+ // Skip malformed lines (same policy as StdioTransport).
37
+ }
38
+ });
39
+ }
40
+ send(message) {
41
+ this.socket.write(JSON.stringify(message) + "\n");
42
+ }
43
+ onMessage(handler) {
44
+ this.handlers.push(handler);
45
+ }
46
+ close() {
47
+ this.rl.close();
48
+ this.handlers = [];
49
+ this.socket.end();
50
+ }
51
+ }
52
+ /**
53
+ * Listen on host:port and invoke `onConnection` with a SocketTransport for each
54
+ * accepted client. Resolves once listening. Defaults to 127.0.0.1 — never bind
55
+ * 0.0.0.0 without auth (v1 has none).
56
+ */
57
+ export function listenTcp(opts, onConnection) {
58
+ const host = opts.host ?? "127.0.0.1";
59
+ return new Promise((resolve, reject) => {
60
+ const server = createServer((socket) => {
61
+ onConnection(new SocketTransport(socket), socket);
62
+ });
63
+ server.once("error", reject);
64
+ server.listen(opts.port, host, () => {
65
+ const addr = server.address();
66
+ const port = typeof addr === "object" && addr ? addr.port : opts.port;
67
+ resolve({
68
+ server,
69
+ port,
70
+ close: () => new Promise((res) => server.close(() => res())),
71
+ });
72
+ });
73
+ });
74
+ }