@gajae-code/coding-agent 0.18.10 → 0.18.12

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 (270) hide show
  1. package/CHANGELOG.md +295 -0
  2. package/dist/types/async/job-manager.d.ts +7 -3
  3. package/dist/types/async/result-formatting.d.ts +2 -0
  4. package/dist/types/autoresearch/git.d.ts +1 -1
  5. package/dist/types/capability/fs.d.ts +2 -2
  6. package/dist/types/cli/accounts-cli.d.ts +2 -2
  7. package/dist/types/cli/doctor/install-repairs.d.ts +4 -1
  8. package/dist/types/cli/install-activation.d.ts +10 -4
  9. package/dist/types/cli/public-command-errors.d.ts +1 -0
  10. package/dist/types/cli/update-cli.d.ts +14 -1
  11. package/dist/types/cli-ordinary.d.ts +2 -2
  12. package/dist/types/commands/accounts.d.ts +2 -1
  13. package/dist/types/commands/migrate.d.ts +1 -1
  14. package/dist/types/commands/sdk.d.ts +5 -0
  15. package/dist/types/commit/changelog/index.d.ts +1 -0
  16. package/dist/types/config/autorouting-tier-map.d.ts +30 -0
  17. package/dist/types/config/config-hot-reload-trust.d.ts +12 -0
  18. package/dist/types/config/config-hot-reload.d.ts +39 -0
  19. package/dist/types/config/file-lock.d.ts +8 -1
  20. package/dist/types/config/model-discovery-manager.d.ts +2 -0
  21. package/dist/types/config/model-profile-activation.d.ts +62 -6
  22. package/dist/types/config/model-registry.d.ts +56 -5
  23. package/dist/types/config/model-resolver.d.ts +2 -0
  24. package/dist/types/config/models-config-schema.d.ts +10 -0
  25. package/dist/types/config/settings-schema.d.ts +46 -3
  26. package/dist/types/config/settings.d.ts +33 -1
  27. package/dist/types/dap/config.d.ts +8 -4
  28. package/dist/types/debug/system-info.d.ts +6 -1
  29. package/dist/types/discovery/mcp-compat.d.ts +3 -1
  30. package/dist/types/edit/modes/patch.d.ts +5 -0
  31. package/dist/types/edit/modes/replace.d.ts +3 -0
  32. package/dist/types/exec/bash-executor.d.ts +12 -0
  33. package/dist/types/gjc-runtime/managed-owner-admission.d.ts +3 -0
  34. package/dist/types/gjc-runtime/session-state-lock.d.ts +2 -0
  35. package/dist/types/gjc-runtime/state-runtime.d.ts +0 -2
  36. package/dist/types/migrate/adapters/codex.d.ts +2 -1
  37. package/dist/types/migrate/adapters/index.d.ts +10 -1
  38. package/dist/types/migrate/executor.d.ts +3 -1
  39. package/dist/types/migrate/types.d.ts +15 -3
  40. package/dist/types/modes/components/settings-selector.d.ts +6 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +2 -0
  42. package/dist/types/modes/theme/theme.d.ts +1 -0
  43. package/dist/types/runtime-mcp/content-limits.d.ts +4 -0
  44. package/dist/types/runtime-mcp/manager.d.ts +7 -0
  45. package/dist/types/runtime-mcp/plugin-mcp-name-filter.d.ts +22 -0
  46. package/dist/types/runtime-mcp/tool-bridge.d.ts +4 -0
  47. package/dist/types/sdk/broker/broker-exit.d.ts +2 -2
  48. package/dist/types/sdk/broker/broker.d.ts +7 -1
  49. package/dist/types/sdk/broker/daemon-entry.d.ts +11 -1
  50. package/dist/types/sdk/broker/doctor-restart.d.ts +1 -1
  51. package/dist/types/sdk/broker/ensure.d.ts +136 -7
  52. package/dist/types/sdk/broker/hop.d.ts +36 -0
  53. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +18 -10
  54. package/dist/types/sdk/broker/lock-host-identity.d.ts +5 -0
  55. package/dist/types/sdk/broker/session-index.d.ts +4 -1
  56. package/dist/types/sdk/bus/chat-command-policy.d.ts +2 -1
  57. package/dist/types/sdk/bus/index.d.ts +23 -0
  58. package/dist/types/sdk/bus/reconciliation-store.d.ts +10 -0
  59. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -1
  60. package/dist/types/sdk/cli/session-cli.d.ts +0 -1
  61. package/dist/types/sdk/host/session-runtime.d.ts +32 -0
  62. package/dist/types/sdk/prompt-deadline-flush.d.ts +42 -4
  63. package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -0
  64. package/dist/types/sdk/protocol/adapter-validation.d.ts +10 -4
  65. package/dist/types/sdk/reconciliation-extensions.d.ts +1 -1
  66. package/dist/types/sdk/session-list.d.ts +1 -1
  67. package/dist/types/session/agent-session.d.ts +58 -2
  68. package/dist/types/session/internal/managed-session-scope.d.ts +2 -2
  69. package/dist/types/session/internal/managed-session-storage.d.ts +1 -1
  70. package/dist/types/session/internal/native-publish-outcome.d.ts +1 -1
  71. package/dist/types/session/messages.d.ts +5 -1
  72. package/dist/types/session/session-manager.d.ts +13 -3
  73. package/dist/types/session/startup-auth-config.d.ts +24 -2
  74. package/dist/types/session/streaming-output.d.ts +11 -0
  75. package/dist/types/session/terminal-abort.d.ts +3 -1
  76. package/dist/types/session-import/service.d.ts +1 -1
  77. package/dist/types/skill-state/workflow-mutation-guard.d.ts +7 -3
  78. package/dist/types/slash-commands/helpers/provider-auth-status.d.ts +8 -0
  79. package/dist/types/task/executor.d.ts +19 -2
  80. package/dist/types/tools/ast-edit-write-path.d.ts +10 -0
  81. package/dist/types/tools/atomic-file-write.d.ts +9 -7
  82. package/dist/types/tools/bash-allowed-prefixes.d.ts +11 -0
  83. package/dist/types/tools/descriptors.d.ts +2 -0
  84. package/dist/types/tools/fetch.d.ts +1 -1
  85. package/dist/types/tools/index.d.ts +2 -0
  86. package/dist/types/tools/python.d.ts +1 -1
  87. package/dist/types/tools/report-issue.d.ts +41 -0
  88. package/dist/types/tools/subagent.d.ts +23 -0
  89. package/dist/types/web/scrapers/types.d.ts +8 -1
  90. package/dist/types/web/search/providers/insane.d.ts +7 -1
  91. package/dist/types/web/search/providers/searxng.d.ts +5 -0
  92. package/package.json +8 -8
  93. package/scripts/build-binary.ts +2 -1
  94. package/scripts/capture-subagent-compact-panel-showcase.ts +105 -0
  95. package/scripts/generate-docs-index.ts +8 -1
  96. package/scripts/generate-sdk-operation-inventory.ts +22 -4
  97. package/src/async/job-manager.ts +356 -56
  98. package/src/async/result-formatting.ts +40 -0
  99. package/src/autoresearch/git.ts +9 -2
  100. package/src/autoresearch/prompts/prompt-setup.md +1 -1
  101. package/src/capability/fs.ts +27 -19
  102. package/src/capability/index.ts +2 -2
  103. package/src/cli/accounts-cli.ts +176 -8
  104. package/src/cli/auth-broker-cli.ts +21 -11
  105. package/src/cli/doctor/install-repairs.ts +35 -5
  106. package/src/cli/fast-help.ts +1 -1
  107. package/src/cli/install-activation.ts +47 -4
  108. package/src/cli/migrate-cli.ts +7 -5
  109. package/src/cli/public-command-errors.ts +4 -0
  110. package/src/cli/root-flags.ts +2 -2
  111. package/src/cli/stats-cli.ts +9 -5
  112. package/src/cli/update-cli.ts +224 -7
  113. package/src/cli-ordinary.ts +7 -2
  114. package/src/commands/accounts.ts +10 -5
  115. package/src/commands/migrate.ts +3 -3
  116. package/src/commands/sdk.ts +199 -42
  117. package/src/commands/ultragoal.ts +16 -8
  118. package/src/commit/changelog/index.ts +39 -1
  119. package/src/commit/message.ts +10 -3
  120. package/src/config/autorouting-tier-map.ts +30 -0
  121. package/src/config/config-hot-reload-trust.ts +148 -0
  122. package/src/config/config-hot-reload.ts +734 -0
  123. package/src/config/file-lock.ts +216 -56
  124. package/src/config/model-bindings-applier.ts +85 -47
  125. package/src/config/model-discovery-manager.ts +10 -0
  126. package/src/config/model-profile-activation.ts +716 -113
  127. package/src/config/model-registry.ts +1334 -158
  128. package/src/config/model-resolver.ts +3 -1
  129. package/src/config/models-config-schema.ts +6 -0
  130. package/src/config/settings-schema.ts +60 -3
  131. package/src/config/settings.ts +458 -24
  132. package/src/coordinator-mcp/policy.ts +26 -1
  133. package/src/crash/sanitize.ts +4 -0
  134. package/src/customization/import.ts +3 -1
  135. package/src/dap/config.ts +31 -11
  136. package/src/dap/defaults.json +1 -1
  137. package/src/debug/crash-diagnostics.ts +2 -1
  138. package/src/debug/report-bundle.ts +19 -0
  139. package/src/debug/system-info.ts +8 -3
  140. package/src/defaults/gjc/extensions/grok-cli-vendor/src/shared/base-url.ts +21 -7
  141. package/src/discovery/agents-md.ts +1 -1
  142. package/src/discovery/helpers.ts +1 -2
  143. package/src/discovery/mcp-compat.ts +120 -17
  144. package/src/edit/apply-patch/index.ts +12 -1
  145. package/src/edit/index.ts +13 -17
  146. package/src/edit/modes/patch.ts +187 -8
  147. package/src/edit/modes/replace.ts +136 -0
  148. package/src/eval/js/context-manager.ts +29 -3
  149. package/src/eval/js/worker-entry.ts +51 -18
  150. package/src/eval/py/executor.ts +14 -4
  151. package/src/eval/py/runner.py +144 -5
  152. package/src/exec/bash-executor.ts +81 -4
  153. package/src/exec/bash-shell-guardian.ts +4 -1
  154. package/src/exec/bash-shell-supervisor.ts +8 -4
  155. package/src/exec/isolated-shell.ts +5 -1
  156. package/src/export/html/index.ts +46 -6
  157. package/src/extensibility/custom-tools/wrapper.ts +3 -1
  158. package/src/extensibility/extensions/loader.ts +8 -1
  159. package/src/extensibility/plugins/marketplace/fetcher.ts +2 -2
  160. package/src/gjc-runtime/gc-runtime.ts +33 -6
  161. package/src/gjc-runtime/launch-tmux.ts +7 -2
  162. package/src/gjc-runtime/launch-worktree.test.ts +148 -0
  163. package/src/gjc-runtime/launch-worktree.ts +101 -14
  164. package/src/gjc-runtime/managed-owner-admission.ts +46 -2
  165. package/src/gjc-runtime/session-resolution.ts +38 -6
  166. package/src/gjc-runtime/session-state-lock.ts +59 -14
  167. package/src/gjc-runtime/state-runtime.ts +23 -19
  168. package/src/hooks/native-skill-hook.ts +14 -11
  169. package/src/internal-urls/docs-index.generated.ts +14 -13
  170. package/src/internal-urls/local-protocol.ts +142 -8
  171. package/src/lsp/index.ts +29 -0
  172. package/src/lsp/workspace-path.ts +23 -0
  173. package/src/migrate/action-planner.ts +57 -0
  174. package/src/migrate/adapters/claude-code.ts +23 -4
  175. package/src/migrate/adapters/codex.ts +22 -4
  176. package/src/migrate/adapters/index.ts +22 -4
  177. package/src/migrate/adapters/opencode.ts +6 -1
  178. package/src/migrate/executor.ts +84 -7
  179. package/src/migrate/report.ts +19 -2
  180. package/src/migrate/types.ts +14 -4
  181. package/src/modes/DESIGN.md +51 -0
  182. package/src/modes/acp/permission-mode.ts +18 -1
  183. package/src/modes/components/assistant-message.ts +55 -1
  184. package/src/modes/components/settings-selector.ts +103 -37
  185. package/src/modes/controllers/command-controller.ts +38 -2
  186. package/src/modes/controllers/plan-mode-controller.ts +17 -7
  187. package/src/modes/controllers/selector-controller.ts +62 -6
  188. package/src/modes/interactive-mode.ts +68 -0
  189. package/src/modes/theme/theme.ts +15 -0
  190. package/src/prompts/tools/debug.md +1 -1
  191. package/src/prompts/tools/read.md +1 -1
  192. package/src/prompts/tools/report-issue.md +13 -0
  193. package/src/prompts/tools/task.md +1 -1
  194. package/src/runtime-credential-selector.ts +2 -2
  195. package/src/runtime-mcp/content-limits.ts +7 -0
  196. package/src/runtime-mcp/manager-pool.test.ts +5 -4
  197. package/src/runtime-mcp/manager.ts +32 -0
  198. package/src/runtime-mcp/oauth-flow.ts +32 -6
  199. package/src/runtime-mcp/plugin-mcp-name-filter.ts +62 -0
  200. package/src/runtime-mcp/render.ts +16 -1
  201. package/src/runtime-mcp/tool-bridge.ts +126 -17
  202. package/src/sdk/broker/broker-exit.ts +8 -5
  203. package/src/sdk/broker/broker.ts +342 -82
  204. package/src/sdk/broker/daemon-entry.ts +338 -116
  205. package/src/sdk/broker/doctor-restart.ts +2 -1
  206. package/src/sdk/broker/ensure.ts +1143 -265
  207. package/src/sdk/broker/hop.ts +274 -0
  208. package/src/sdk/broker/lifecycle-ledger.ts +35 -11
  209. package/src/sdk/broker/lock-host-identity.ts +29 -0
  210. package/src/sdk/broker/session-index.ts +62 -12
  211. package/src/sdk/bus/chat-command-policy.ts +16 -3
  212. package/src/sdk/bus/index.ts +52 -21
  213. package/src/sdk/bus/reconciliation-store.ts +201 -8
  214. package/src/sdk/bus/telegram-daemon-contract.ts +3 -1
  215. package/src/sdk/cli/session-cli.ts +21 -6
  216. package/src/sdk/host/session-runtime.test.ts +118 -23
  217. package/src/sdk/host/session-runtime.ts +352 -47
  218. package/src/sdk/lifecycle/service.ts +1 -0
  219. package/src/sdk/prompt-deadline-flush.ts +78 -5
  220. package/src/sdk/prompt-deadline-manager.ts +68 -14
  221. package/src/sdk/protocol/adapter-validation.ts +53 -11
  222. package/src/sdk/protocol/operation-inventory.generated.json +101 -2
  223. package/src/sdk/reconciliation-extensions.ts +1 -0
  224. package/src/sdk/session-list.ts +1 -0
  225. package/src/sdk/session.ts +114 -61
  226. package/src/session/agent-session.ts +1043 -81
  227. package/src/session/internal/managed-session-scope.ts +126 -39
  228. package/src/session/internal/managed-session-storage.ts +14 -11
  229. package/src/session/internal/native-publish-outcome.ts +5 -1
  230. package/src/session/messages.ts +47 -6
  231. package/src/session/session-manager.ts +236 -14
  232. package/src/session/startup-auth-config.ts +116 -8
  233. package/src/session/streaming-output.ts +262 -4
  234. package/src/session/terminal-abort.ts +14 -2
  235. package/src/session-import/service.ts +1 -0
  236. package/src/skill-state/workflow-mutation-guard.ts +491 -32
  237. package/src/slash-commands/builtin-registry.ts +15 -3
  238. package/src/slash-commands/helpers/credential-switch.ts +10 -5
  239. package/src/slash-commands/helpers/provider-auth-status.ts +456 -0
  240. package/src/task/executor.ts +103 -12
  241. package/src/task/index.ts +389 -175
  242. package/src/tools/ast-edit-write-path.ts +64 -0
  243. package/src/tools/ast-edit.ts +18 -1
  244. package/src/tools/atomic-file-write.ts +46 -15
  245. package/src/tools/auto-generated-guard.ts +3 -0
  246. package/src/tools/bash-allowed-prefixes.ts +153 -5
  247. package/src/tools/bash-interactive.ts +14 -2
  248. package/src/tools/bash.ts +14 -0
  249. package/src/tools/debug.ts +7 -6
  250. package/src/tools/descriptors.test.ts +44 -0
  251. package/src/tools/descriptors.ts +34 -2
  252. package/src/tools/fetch.ts +3 -2
  253. package/src/tools/index.ts +2 -0
  254. package/src/tools/output-meta.ts +23 -2
  255. package/src/tools/python.ts +1 -1
  256. package/src/tools/read.ts +17 -35
  257. package/src/tools/report-issue.ts +184 -0
  258. package/src/tools/subagent-render.ts +114 -12
  259. package/src/tools/subagent.ts +108 -1
  260. package/src/tools/todo-write.ts +41 -3
  261. package/src/tools/tool-catalog.generated.ts +31 -3
  262. package/src/tools/write.ts +35 -3
  263. package/src/utils/commit-message-generator.ts +9 -2
  264. package/src/utils/file-mentions.ts +29 -1
  265. package/src/utils/title-generator.ts +7 -2
  266. package/src/web/scrapers/index.ts +3 -1
  267. package/src/web/scrapers/types.ts +8 -0
  268. package/src/web/scrapers/youtube.ts +3 -2
  269. package/src/web/search/providers/insane.ts +91 -28
  270. package/src/web/search/providers/searxng.ts +62 -10
@@ -5,7 +5,7 @@ import type { CompactionOutcome } from "@gajae-code/agent-core/compaction";
5
5
  import { type Model, modelsAreEqual } from "@gajae-code/ai/core";
6
6
  import { Container, type KeyId, Markdown, Spacer, Text } from "@gajae-code/tui";
7
7
  import { isEnoent, prompt } from "@gajae-code/utils";
8
- import { resolveLocalUrlToPath } from "../../internal-urls";
8
+ import { resolveLocalRoot, resolveLocalUrlToPath } from "../../internal-urls";
9
9
  import { humanizePlanTitle, type PlanApprovalDetails, resolvePlanTitle } from "../../plan-mode/approved-plan";
10
10
  import planModeApprovedPrompt from "../../prompts/system/plan-mode-approved.md" with { type: "text" };
11
11
  import planModeCompactInstructionsPrompt from "../../prompts/system/plan-mode-compact-instructions.md" with {
@@ -13,6 +13,7 @@ import planModeCompactInstructionsPrompt from "../../prompts/system/plan-mode-co
13
13
  };
14
14
  import type { AgentSession, TemporaryProviderSessionScope } from "../../session/agent-session";
15
15
  import type { SessionContext, SessionManager } from "../../session/session-manager";
16
+ import { writeFileAtomically } from "../../tools/atomic-file-write";
16
17
  import { normalizeLocalScheme } from "../../tools/path-utils";
17
18
  import { type ResolveToolDetails, runResolveInvocation } from "../../tools/resolve";
18
19
  import { ToolError } from "../../tools/tool-errors";
@@ -338,13 +339,16 @@ export class PlanModeController {
338
339
  throw error;
339
340
  }
340
341
  }
342
+ #localProtocolOptions() {
343
+ return {
344
+ getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
345
+ isManagedDestination: () => this.ctx.sessionManager.isManagedDestination(),
346
+ getSessionId: () => this.ctx.sessionManager.getSessionId(),
347
+ };
348
+ }
341
349
  #resolvePath(planFilePath: string): string {
342
350
  return planFilePath.startsWith("local:")
343
- ? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), {
344
- getArtifactsDir: () => this.ctx.sessionManager.getArtifactsDir(),
345
- isManagedDestination: () => this.ctx.sessionManager.isManagedDestination(),
346
- getSessionId: () => this.ctx.sessionManager.getSessionId(),
347
- })
351
+ ? resolveLocalUrlToPath(normalizeLocalScheme(planFilePath), this.#localProtocolOptions())
348
352
  : path.resolve(this.ctx.sessionManager.getCwd(), planFilePath);
349
353
  }
350
354
  async #finalizeApprovedPlan(planContent: string, planFilePath: string, finalPlanFilePath: string): Promise<void> {
@@ -420,7 +424,13 @@ export class PlanModeController {
420
424
  trimTrailingNewline: false,
421
425
  });
422
426
  if (result !== null) {
423
- await Bun.write(resolved, result);
427
+ // Lexical local:// checks do not see a symlink. Bind this save to the
428
+ // same local root so a link inside it cannot publish outside.
429
+ if (planFilePath.startsWith("local:")) {
430
+ await writeFileAtomically(resolved, result, {
431
+ trustBoundary: path.resolve(resolveLocalRoot(this.#localProtocolOptions())),
432
+ });
433
+ } else await Bun.write(resolved, result);
424
434
  this.ctx.showStatus("Plan updated in external editor.");
425
435
  }
426
436
  return result;
@@ -107,7 +107,10 @@ import { ensureTelegramDaemonRunningDetailed, resolveTelegramSetupPreflight } fr
107
107
  import { TelegramDaemonController } from "../../sdk/bus/telegram-daemon-control";
108
108
  import { runTelegramSetup, type TelegramSetupPreflight } from "../../sdk/bus/telegram-setup";
109
109
  import { clearPersistentPinForRemovedRows } from "../../session/account-inventory";
110
- import type { DefaultFallbackRuntimeState } from "../../session/agent-session";
110
+ import type {
111
+ AgentSessionProfileInstalledOverrideState,
112
+ DefaultFallbackRuntimeState,
113
+ } from "../../session/agent-session";
111
114
  import { CREDENTIAL_STORE_UNREADABLE_MESSAGE } from "../../session/credential-store-errors";
112
115
  import { type SessionInfo, SessionManager } from "../../session/session-manager";
113
116
  import { getTreeForInternalRead } from "../../session/session-manager-internal";
@@ -1214,6 +1217,7 @@ interface DefaultAssignmentRollbackSnapshot {
1214
1217
  profileOverride: SettingValue<"modelProfile.default"> | undefined;
1215
1218
  chain: { entries: readonly string[]; origin: string; identity?: string; explicitHead?: boolean } | undefined;
1216
1219
  activeProfile: string | undefined;
1220
+ profileInstalledOverrideState: AgentSessionProfileInstalledOverrideState | undefined;
1217
1221
  canonicalVariant: string | undefined;
1218
1222
  resumeDefaultSelector: string | undefined;
1219
1223
  fallbackRuntimeState: DefaultFallbackRuntimeState;
@@ -1248,6 +1252,10 @@ function rawAutoroutingState(current: RawSettings): {
1248
1252
 
1249
1253
  export class SelectorController {
1250
1254
  #smartRoutingInFlight?: Promise<unknown>;
1255
+ #modelSelector?: ModelSelectorComponent;
1256
+ #settingsSelector?: SettingsSelectorComponent;
1257
+ #statusLinePreview?: Partial<StatusLineSettings>;
1258
+ #statusLinePreviewActive = false;
1251
1259
  #transcriptViewerOpen = false;
1252
1260
  #transcriptViewer?: TranscriptViewerOverlay;
1253
1261
  #sessionsDashboardOpen = false;
@@ -1277,6 +1285,7 @@ export class SelectorController {
1277
1285
  profileOverride: this.ctx.settings.getOverride("modelProfile.default"),
1278
1286
  chain: this.ctx.session.getConfiguredModelChainState("default"),
1279
1287
  activeProfile: this.ctx.session.getActiveModelProfile?.(),
1288
+ profileInstalledOverrideState: this.ctx.session.getProfileInstalledOverrideState?.(),
1280
1289
  canonicalVariant: this.ctx.session.modelRegistry.getSessionCanonicalVariant?.(this.ctx.session.sessionId),
1281
1290
  resumeDefaultSelector: this.ctx.session.sessionManager.buildSessionContext().models.default,
1282
1291
  fallbackRuntimeState: this.ctx.session.getDefaultFallbackRuntimeState(),
@@ -1354,6 +1363,11 @@ export class SelectorController {
1354
1363
  restore(() => this.ctx.session.recordResumeDefaultModel(snapshot.resumeDefaultSelector));
1355
1364
  }
1356
1365
  restore(() => this.ctx.session.setActiveModelProfile?.(snapshot.activeProfile));
1366
+ if (snapshot.profileInstalledOverrideState) {
1367
+ restore(() =>
1368
+ this.ctx.session.restoreProfileInstalledOverrideState?.(snapshot.profileInstalledOverrideState!),
1369
+ );
1370
+ }
1357
1371
  try {
1358
1372
  await this.ctx.settings.flushOrThrow();
1359
1373
  } catch (rollbackError) {
@@ -1401,7 +1415,15 @@ export class SelectorController {
1401
1415
  */
1402
1416
  showSelector(create: (done: () => void) => { component: Component; focus: Component }): void {
1403
1417
  if (this.ctx.isStopped?.()) return;
1418
+ this.#modelSelector = undefined;
1419
+ this.#settingsSelector = undefined;
1420
+ this.#statusLinePreview = undefined;
1421
+ this.#statusLinePreviewActive = false;
1404
1422
  const done = () => {
1423
+ this.#modelSelector = undefined;
1424
+ this.#settingsSelector = undefined;
1425
+ this.#statusLinePreview = undefined;
1426
+ this.#statusLinePreviewActive = false;
1405
1427
  if (this.ctx.isStopped?.()) return;
1406
1428
  // Prefer the pet-aware composer restore (InteractiveMode.restoreComposer); fall back
1407
1429
  // to a plain editor swap for contexts that predate it (e.g. lightweight test doubles).
@@ -1424,6 +1446,28 @@ export class SelectorController {
1424
1446
  this.ctx.ui.requestRender();
1425
1447
  }
1426
1448
 
1449
+ /** Refresh accepted disk configuration without discarding an open preview. */
1450
+ refreshConfiguration(): void {
1451
+ if (this.ctx.isStopped?.()) return;
1452
+ this.#applyStatusLineSettings();
1453
+ this.#modelSelector?.refreshRoleAssignments({
1454
+ currentModel: this.ctx.session.model,
1455
+ currentThinkingLevel: this.ctx.session.thinkingLevel,
1456
+ activeModelProfile: this.ctx.session.getActiveModelProfile(),
1457
+ });
1458
+ this.#modelSelector?.refreshPresetProfiles();
1459
+ this.#settingsSelector?.refreshFromConfiguration([...this.ctx.session.modelRegistry.getModelProfiles().keys()]);
1460
+ this.ctx.updateEditorTopBorder();
1461
+ this.ctx.ui.requestRender();
1462
+ }
1463
+
1464
+ #applyStatusLineSettings(): void {
1465
+ this.ctx.statusLine.updateSettings({
1466
+ ...buildStatusLineSettings(this.ctx.settings),
1467
+ ...(this.#statusLinePreviewActive ? this.#statusLinePreview : undefined),
1468
+ });
1469
+ }
1470
+
1427
1471
  showCommandPalette(
1428
1472
  commands: SlashCommand[],
1429
1473
  actions: CommandPaletteAction[],
@@ -1990,11 +2034,19 @@ export class SelectorController {
1990
2034
  },
1991
2035
  onPetCommit: mode => this.ctx.commitPetPreviewMode(mode as PetMode),
1992
2036
  onStatusLinePreview: previewSettings => {
1993
- // Update status line with preview settings
1994
- this.ctx.statusLine.updateSettings({
1995
- ...buildStatusLineSettings(settings),
1996
- ...previewSettings,
1997
- });
2037
+ if (this.#statusLinePreviewActive) this.#statusLinePreview = previewSettings;
2038
+ this.#applyStatusLineSettings();
2039
+ this.ctx.updateEditorTopBorder();
2040
+ this.ctx.ui.requestRender();
2041
+ },
2042
+ onStatusLinePreviewStart: () => {
2043
+ this.#statusLinePreviewActive = true;
2044
+ this.#statusLinePreview = undefined;
2045
+ },
2046
+ onStatusLinePreviewEnd: () => {
2047
+ this.#statusLinePreviewActive = false;
2048
+ this.#statusLinePreview = undefined;
2049
+ this.#applyStatusLineSettings();
1998
2050
  this.ctx.updateEditorTopBorder();
1999
2051
  this.ctx.ui.requestRender();
2000
2052
  },
@@ -2068,6 +2120,7 @@ export class SelectorController {
2068
2120
  },
2069
2121
  notificationsOperations,
2070
2122
  );
2123
+ this.#settingsSelector = selector;
2071
2124
  return { component: selector, focus: selector };
2072
2125
  });
2073
2126
  });
@@ -2849,6 +2902,7 @@ export class SelectorController {
2849
2902
  if (!materializedProfile) {
2850
2903
  for (const targetRole of targetRoles) {
2851
2904
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[targetRole];
2905
+ this.ctx.session.markProfileRoleOverrideManual?.(target.settingsPath, targetRole);
2852
2906
  if (target.settingsPath === "modelRoles") {
2853
2907
  this.ctx.settings.setModelRole(targetRole, value);
2854
2908
  } else {
@@ -2952,6 +3006,7 @@ export class SelectorController {
2952
3006
  });
2953
3007
  if (!materializedProfile) {
2954
3008
  const target = GJC_MODEL_ASSIGNMENT_TARGETS[role];
3009
+ this.ctx.session.markProfileRoleOverrideManual?.(target.settingsPath, role);
2955
3010
  if (target.settingsPath === "modelRoles") {
2956
3011
  this.ctx.settings.setModelRole(role, value);
2957
3012
  } else {
@@ -2996,6 +3051,7 @@ export class SelectorController {
2996
3051
  smartRoutingOnly: options?.smartRoutingOnly,
2997
3052
  },
2998
3053
  );
3054
+ this.#modelSelector = modelSelector;
2999
3055
  return { component: modelSelector, focus: modelSelector };
3000
3056
  });
3001
3057
  }
@@ -16,6 +16,8 @@ import {
16
16
  import { APP_NAME, adjustHsv, getAgentDir, getProjectDir, logger, postmortem, sanitizeText } from "@gajae-code/utils";
17
17
  import chalk from "chalk";
18
18
  import { AsyncJobManager } from "../async";
19
+ import { ConfigHotReloadWatcher } from "../config/config-hot-reload";
20
+ import { isConfigHotReloadSupported, isConfigHotReloadTrusted } from "../config/config-hot-reload-trust";
19
21
  import {
20
22
  type AppKeybinding,
21
23
  defaultMessageQueueKeysForPlatform,
@@ -573,6 +575,7 @@ export class InteractiveMode implements InteractiveModeContext {
573
575
  // paint, which renders the full session anyway.
574
576
  #initialTranscriptPainted = false;
575
577
  #stopListeners = new Set<() => void>();
578
+ #configHotReload?: ConfigHotReloadWatcher;
576
579
  #eventBus?: EventBus;
577
580
  #eventBusUnsubscribers: Array<() => void> = [];
578
581
  #welcomeComponent?: WelcomeComponent;
@@ -999,6 +1002,8 @@ export class InteractiveMode implements InteractiveModeContext {
999
1002
  this.updateEditorChrome();
1000
1003
  this.#syncEditorMaxHeight();
1001
1004
  this.isInitialized = true;
1005
+ await this.#startConfigHotReload();
1006
+ if (this.#stopped) return;
1002
1007
  this.syncActivityIndicator();
1003
1008
  if (this.settings.get("tasksPane.defaultVisible")) this.showTasksPane();
1004
1009
  this.#syncIrcSidebarAvailabilityFromSettings();
@@ -1691,6 +1696,65 @@ export class InteractiveMode implements InteractiveModeContext {
1691
1696
  return this.#stopped;
1692
1697
  }
1693
1698
 
1699
+ async #startConfigHotReload(): Promise<void> {
1700
+ const paths = this.session.getConfigurationPaths();
1701
+ if (!paths) return;
1702
+ if (!isConfigHotReloadSupported()) {
1703
+ logger.debug("Configuration hot reload is unsupported on this platform", { platform: process.platform });
1704
+ return;
1705
+ }
1706
+ const trusted = await isConfigHotReloadTrusted(paths);
1707
+ if (this.#stopped || this.#isShuttingDown) return;
1708
+ if (!trusted) {
1709
+ this.showWarning(
1710
+ "Configuration watching could not start because directory ownership and privacy could not be verified.",
1711
+ );
1712
+ return;
1713
+ }
1714
+ const assertTrustedPaths = async (): Promise<void> => {
1715
+ if (await isConfigHotReloadTrusted(paths)) return;
1716
+ const error = new Error("Configuration directory ownership and privacy could not be verified");
1717
+ Object.assign(error, { code: "EACCES" });
1718
+ throw error;
1719
+ };
1720
+ const watcher = new ConfigHotReloadWatcher({
1721
+ onValidate: async candidate => {
1722
+ await assertTrustedPaths();
1723
+ await this.session.validateConfiguration(candidate);
1724
+ },
1725
+ onCandidate: async (candidate, signal) => {
1726
+ if (signal.aborted || this.#stopped || this.#isShuttingDown) return;
1727
+ await assertTrustedPaths();
1728
+ if (signal.aborted || this.#stopped || this.#isShuttingDown) return;
1729
+ const result = await this.session.reloadConfiguration(candidate, signal);
1730
+ if (signal.aborted || this.#stopped || this.#isShuttingDown || !result.applied) return;
1731
+ this.#selectorController.refreshConfiguration();
1732
+ this.#syncEditorMaxHeight();
1733
+ this.#syncIrcSidebarAvailabilityFromSettings();
1734
+ this.updateEditorChrome();
1735
+ this.showStatus(
1736
+ result.settingsChanged
1737
+ ? "Configuration reloaded. Startup-only services require a restart."
1738
+ : "Model configuration reloaded.",
1739
+ );
1740
+ this.ui.requestRender();
1741
+ },
1742
+ onError: error => {
1743
+ if (!this.#stopped && !this.#isShuttingDown) {
1744
+ this.showWarning(`${error.message}. Repair the configuration file and save again.`);
1745
+ }
1746
+ },
1747
+ });
1748
+ this.#configHotReload = watcher;
1749
+ try {
1750
+ await watcher.start(paths);
1751
+ } catch {
1752
+ watcher.dispose();
1753
+ this.#configHotReload = undefined;
1754
+ this.showWarning("Configuration watching could not start. Restart after repairing file access.");
1755
+ }
1756
+ }
1757
+
1694
1758
  onStop(callback: () => void): () => void {
1695
1759
  if (this.#stopped) {
1696
1760
  callback();
@@ -1703,6 +1767,8 @@ export class InteractiveMode implements InteractiveModeContext {
1703
1767
  stop(): void {
1704
1768
  const wasInitialized = this.isInitialized;
1705
1769
  this.#stopped = true;
1770
+ this.#configHotReload?.dispose();
1771
+ this.#configHotReload = undefined;
1706
1772
  this.ui.setRenderPreparationLifecycleCallbacks(undefined);
1707
1773
  this.#inputController.discardDeferredSubmission();
1708
1774
  for (const listener of this.#stopListeners) {
@@ -1776,6 +1842,8 @@ export class InteractiveMode implements InteractiveModeContext {
1776
1842
  return;
1777
1843
  }
1778
1844
  this.#isShuttingDown = true;
1845
+ this.#configHotReload?.dispose();
1846
+ this.#configHotReload = undefined;
1779
1847
 
1780
1848
  // `/btw` owns the shared composer while its panel is open. Never persist a
1781
1849
  // side-chat draft or pending side-chat images into the main-session draft.
@@ -1260,9 +1260,11 @@ const langMap: Record<string, SymbolKey> = {
1260
1260
  };
1261
1261
 
1262
1262
  export class Theme {
1263
+ #svgColorValues: Record<string, string | number>;
1263
1264
  #fgColors: Record<ThemeColor, string>;
1264
1265
  #bgColors: Record<ThemeBg, string>;
1265
1266
  #symbols: SymbolMap;
1267
+ #svgPalette: Readonly<Record<string, string>>;
1266
1268
 
1267
1269
  constructor(
1268
1270
  fgColors: Record<ThemeColor, string | number>,
@@ -1271,10 +1273,19 @@ export class Theme {
1271
1273
  private readonly symbolPreset: SymbolPreset,
1272
1274
  symbolOverrides: Partial<Record<SymbolKey, string>>,
1273
1275
  ) {
1276
+ this.#svgColorValues = { ...fgColors, ...bgColors };
1274
1277
  this.#fgColors = {} as Record<ThemeColor, string>;
1275
1278
  for (const [key, value] of Object.entries(fgColors) as [ThemeColor, string | number][]) {
1276
1279
  this.#fgColors[key] = fgAnsi(value, mode);
1277
1280
  }
1281
+ const textColor = this.#svgColorValues.text;
1282
+ const foreground = typeof textColor === "number" ? ansi256ToHex(textColor) : textColor || "#e5e5e7";
1283
+ const svgPalette: Record<string, string> = { fg: foreground };
1284
+ for (const color of THEME_COLOR_KEYS) {
1285
+ const value = this.#svgColorValues[color];
1286
+ svgPalette[color] = typeof value === "number" ? ansi256ToHex(value) : value || foreground;
1287
+ }
1288
+ this.#svgPalette = Object.freeze(svgPalette);
1278
1289
  this.#bgColors = {} as Record<ThemeBg, string>;
1279
1290
  for (const [key, value] of Object.entries(bgColors) as [ThemeBg, string | number][]) {
1280
1291
  this.#bgColors[key] = bgAnsi(value, mode);
@@ -1329,6 +1340,10 @@ export class Theme {
1329
1340
  return ansi;
1330
1341
  }
1331
1342
 
1343
+ getSvgPalette(): Readonly<Record<string, string>> {
1344
+ return this.#svgPalette;
1345
+ }
1346
+
1332
1347
  getBgAnsi(color: ThemeBg): string {
1333
1348
  const ansi = this.#bgColors[color];
1334
1349
  if (!ansi) throw new Error(`Unknown theme background color: ${color}`);
@@ -15,7 +15,7 @@ Use for launching or attaching debuggers, setting breakpoints, stepping through
15
15
  <caution>
16
16
  - Only one active debug session is supported at a time.
17
17
  - Some adapters require a launched session to receive `configurationDone` before the target actually runs; if the tool says configuration is pending, set breakpoints and then call `continue`.
18
- - Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -m debugpy.adapter`, `dlv dap`.
18
+ - Adapter availability depends on local binaries. Common built-ins: `gdb`, `lldb-dap`, `python -I -m debugpy.adapter`, `dlv dap`.
19
19
  - `program` must be an executable file or debug target, not a directory or interpreter name that resolves to a workspace directory.
20
20
  </caution>
21
21
 
@@ -73,7 +73,7 @@ For `.sqlite`, `.sqlite3`, `.db`, `.db3`:
73
73
 
74
74
  `agent://<id>`, `artifact://<id>`, `rule://<name>`, and `local://<name>.md` resolve transparently and accept the same line selectors as filesystem paths. Use `artifact://<id>` to recover full output that a previous bash/eval/tool result spilled or truncated.
75
75
 
76
- Bundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/...` identifiers.
76
+ Bundled skills have no filesystem home, so the skill tool and skill discovery report them as `embedded:gjc/skills/<name>/SKILL.md`; read that identifier verbatim. Bundled skill fragments, when surfaced, likewise use `embedded:gjc/skill-fragments/…` identifiers.
77
77
 
78
78
  <critical>
79
79
  - Always include `path`; never call `read` with `{}`.
@@ -0,0 +1,13 @@
1
+ Records a tool that behaved inconsistently with its own description, for local quality tracking.
2
+
3
+ <instruction>
4
+ - Call this when a tool's output contradicts what its description or parameters promise: ignored or misapplied parameters, wrong or truncated results, misleading errors, silent no-ops.
5
+ - `tool` is the misbehaving tool's name; `report` states what you called, what you expected, and what actually happened, in one or two sentences.
6
+ - Do not report your own mistakes, user-caused failures, or expected errors (missing files, failing tests, network outages).
7
+ - Do not paste secrets, tokens, credentials, or raw tool output into report; summarize it instead.
8
+ - False positives are acceptable. Report once per distinct problem, then continue the task; this never fixes or retries anything.
9
+ </instruction>
10
+
11
+ <output>
12
+ Confirms whether the report was recorded. Reports stay on this machine.
13
+ </output>
@@ -23,7 +23,7 @@ Use `irc` for live coordination when it is available.
23
23
  {{#if customSchemaEnabled}}- `schema`: JTD schema for expected structured output (do not put format rules in assignments){{/if}}
24
24
  - `spawnPlan` (optional): required before any batch with more than 4 tasks; include whyParallel, whyNotLocal, independence, expectedReceiptShape, and maxInlineTokens.
25
25
  {{#if isolationEnabled}}- `isolated`: run in an isolated environment; REQUIRED when the user explicitly requests a worktree (for example, "use worktree"), and use when tasks edit overlapping files{{/if}}
26
- - `repositoryBinding`: optional authoritative repository/worktree identity. It must match the active session and cannot authorize a foreign repository from this session. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`); do not use `/move` because materialized tasks retain their original binding.
26
+ - `repositoryBinding`: optional authoritative repository/worktree identity. It must match the session's committed cwd and cannot itself authorize relocation. After an authorized session move, new work uses the committed target; already-admitted work retains its source execution inputs. Verified storage relocation of that same parent does not change child execution cwd; a logical session switch does not substitute successor storage. For another approved repository, start a fresh session rooted there (for example, `gjc --cwd <approved-worktree>`) or use the existing user-authorized relocation surface; `move_session` still only narrows to a descendant directory.
27
27
  </parameters>
28
28
 
29
29
  <rules>
@@ -6,7 +6,7 @@ export interface CliCredentialSelector {
6
6
  raw: string;
7
7
  }
8
8
 
9
- const SELECTOR_KINDS = new Set<AuthCredentialSelectorKind>(["id", "email", "account", "project"]);
9
+ const SELECTOR_KINDS = new Set<AuthCredentialSelectorKind>(["id", "email", "account", "project", "org"]);
10
10
 
11
11
  function parseSelectorBody(body: string): AuthCredentialSelector | undefined {
12
12
  const separator = body.indexOf(":");
@@ -28,7 +28,7 @@ export function parseCliCredentialSelector(raw: string): CliCredentialSelector {
28
28
  const selector = parseSelectorBody(body);
29
29
  if (!selector || provider === "") {
30
30
  throw new Error(
31
- `Invalid --credential selector "${raw}". Use email:name@example.com, id:123, account:<id>, project:<id>, or provider/email:name@example.com.`,
31
+ `Invalid --credential selector "${raw}". Use email:name@example.com, id:123, account:<id>, project:<id>, org:<organization id or name>, or provider/email:name@example.com.`,
32
32
  );
33
33
  }
34
34
  return { ...(provider ? { provider } : {}), selector, raw };
@@ -1,4 +1,11 @@
1
1
  export const MCP_MAX_CONTENT_BYTES = 16 * 1024 * 1024;
2
+ // Match the pasted-image count limit; bound decode attempts as well as output count.
3
+ export const MCP_MAX_IMAGES = 16;
4
+ // Bound decoded source complexity before native image decoding.
5
+ export const MCP_MAX_IMAGE_SOURCE_DIMENSION = 8_192;
6
+ export const MCP_MAX_IMAGE_SOURCE_PIXELS = 16_000_000;
7
+ // Decoded image-file bytes after resizing, before base64 encoding (per result).
8
+ export const MCP_MAX_IMAGE_OUTPUT_BYTES = 4 * 1024 * 1024;
2
9
  export const MCP_MAX_ERROR_BYTES = 16 * 1024;
3
10
  export const MCP_MAX_SSE_REQUEST_MESSAGES = 1024;
4
11
  export const MCP_MAX_SSE_BATCH_MESSAGES = 1024;
@@ -2,6 +2,7 @@ import { expect, test, vi } from "bun:test";
2
2
  import { rm } from "node:fs/promises";
3
3
  import { legacyMcpMethodNotFound } from "../../test/mcp-test-utils";
4
4
  import * as configValue from "../config/resolve-config-value";
5
+ import { wrapUntrustedContent } from "../tools/fetch";
5
6
  import { MCPManager } from "./manager";
6
7
  import { MCPConnectionPool } from "./pool";
7
8
  import { computeMCPPoolKey } from "./pool-key";
@@ -1386,8 +1387,8 @@ test("shared noReplay request failure coordinates one replacement without resend
1386
1387
  expect(second.getTools()[0]?.mcpToolName).toBe("shared-2");
1387
1388
  const firstResult = await first.getTools()[0]!.execute("first-later", {}, undefined, {} as never);
1388
1389
  const secondResult = await second.getTools()[0]!.execute("second-later", {}, undefined, {} as never);
1389
- expect(firstResult.content).toEqual([{ type: "text", text: "ok-2" }]);
1390
- expect(secondResult.content).toEqual([{ type: "text", text: "ok-2" }]);
1390
+ expect(firstResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
1391
+ expect(secondResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
1391
1392
  } finally {
1392
1393
  await first.disconnectAll().catch(() => {});
1393
1394
  await second.disconnectAll().catch(() => {});
@@ -1474,8 +1475,8 @@ test("shared restart rebinds every surviving manager lease before subsequent cal
1474
1475
  expect(second.getTools()[0]?.mcpToolName).toBe("shared-2");
1475
1476
  const firstResult = await first.getTools()[0]!.execute("first", {}, undefined, {} as never);
1476
1477
  const secondResult = await second.getTools()[0]!.execute("second", {}, undefined, {} as never);
1477
- expect(firstResult.content).toEqual([{ type: "text", text: "ok-2" }]);
1478
- expect(secondResult.content).toEqual([{ type: "text", text: "ok-2" }]);
1478
+ expect(firstResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
1479
+ expect(secondResult.content).toEqual([{ type: "text", text: wrapUntrustedContent("ok-2") }]);
1479
1480
  } finally {
1480
1481
  await first.disconnectAll().catch(() => {});
1481
1482
  await second.disconnectAll().catch(() => {});
@@ -303,6 +303,30 @@ export interface MCPToolCatalogSnapshot {
303
303
  generation: number;
304
304
  }
305
305
 
306
+ /** Drop project stdio servers unless the user/global opt-in is set. */
307
+ export function omitUnapprovedProjectStdio(
308
+ configs: Record<string, MCPServerConfig>,
309
+ sources: Record<string, SourceMeta>,
310
+ allowProjectStdio: boolean,
311
+ toolsOnly = false,
312
+ ): void {
313
+ if (allowProjectStdio) return;
314
+ for (const name of Object.keys(configs)) {
315
+ const config = configs[name];
316
+ if (!config || config.type === "http" || config.type === "sse") continue;
317
+ if (!isUntrustedProjectMcpSource(sources[name], toolsOnly)) continue;
318
+ delete configs[name];
319
+ delete sources[name];
320
+ if (!toolsOnly) {
321
+ logger.warn("Skipping MCP autoload registration", {
322
+ path: `mcp:${name}`,
323
+ serverName: name,
324
+ reason: "project stdio server is not user-approved",
325
+ });
326
+ }
327
+ }
328
+ }
329
+
306
330
  /** Options for discovering and connecting to MCP servers */
307
331
  export interface MCPDiscoverOptions {
308
332
  /** Whether to load project-level config (default: true) */
@@ -330,6 +354,11 @@ export interface MCPDiscoverOptions {
330
354
  export interface MCPManagerOptions {
331
355
  /** Restrict this instance to tools from an explicit MCP config. */
332
356
  toolsOnly?: boolean;
357
+ /**
358
+ * Spawn stdio servers loaded from a project MCP file. Default false.
359
+ * User-scope servers and an explicit `--mcp-config` file are unaffected.
360
+ */
361
+ allowUntrustedProjectStdio?: boolean;
333
362
  /**
334
363
  * Ceiling for the startup wait, in milliseconds. ACP lifecycle launches and
335
364
  * plugin MCP startup use bounded caller-owned budgets; other consumers keep
@@ -460,6 +489,7 @@ export class MCPManager {
460
489
  #scopedLifecycle: "open" | "disconnecting" | "reconnecting" = "open";
461
490
  #scopedLifecycleEpoch = 0;
462
491
  readonly #toolsOnly: boolean;
492
+ readonly #allowUntrustedProjectStdio: boolean;
463
493
  #toolCacheServerNames: Set<string> | undefined;
464
494
  #toolsOnlyConfigLoaded = false;
465
495
  #connectionSetMutationBlocked = false;
@@ -995,6 +1025,7 @@ export class MCPManager {
995
1025
  options: MCPManagerOptions = {},
996
1026
  ) {
997
1027
  this.#toolsOnly = options.toolsOnly === true;
1028
+ this.#allowUntrustedProjectStdio = options.allowUntrustedProjectStdio === true;
998
1029
  this.#toolCacheServerNames = options.toolCacheServerNames ? new Set(options.toolCacheServerNames) : undefined;
999
1030
  this.#maxStartupTimeoutMs = options.maxStartupTimeoutMs;
1000
1031
  this.#sleep = options.sleep ?? delay;
@@ -1185,6 +1216,7 @@ export class MCPManager {
1185
1216
  nativeOnly: options?.nativeOnly,
1186
1217
  configPath: options?.configPath,
1187
1218
  });
1219
+ omitUnapprovedProjectStdio(configs, sources, this.#allowUntrustedProjectStdio, this.#toolsOnly);
1188
1220
  if (this.#toolCacheServerNames !== undefined) {
1189
1221
  this.#toolCacheServerNames = new Set(
1190
1222
  Object.keys(configs).filter(name => sources[name]?.provider !== "gjc-plugins"),
@@ -9,6 +9,7 @@ import type { OAuthCallbackFlowOptions } from "@gajae-code/ai/utils/oauth/callba
9
9
  import { OAuthCallbackFlow } from "@gajae-code/ai/utils/oauth/callback-server";
10
10
  import type { OAuthController, OAuthCredentials } from "@gajae-code/ai/utils/oauth/types";
11
11
  import { guardedPublicFetch } from "../web/insane/url-guard";
12
+ import { cancelMCPStream } from "./content-limits";
12
13
  import { assertPublicOAuthUrl } from "./oauth-public-url";
13
14
 
14
15
  const DEFAULT_PORT = 3000;
@@ -104,6 +105,34 @@ function resolveCallbackOptions(config: MCPOAuthConfig): OAuthCallbackFlowOption
104
105
  };
105
106
  }
106
107
 
108
+ function isOAuthRedirect(response: Response): boolean {
109
+ return response.type === "opaqueredirect" || (response.status >= 300 && response.status < 400);
110
+ }
111
+
112
+ /**
113
+ * Dial the URL on an address `guardedPublicFetch` just accepted. That helper
114
+ * forces `redirect: "manual"`, so a 3xx is a refusal rather than a followed hop.
115
+ */
116
+ async function fetchPinnedOAuthEndpoint(
117
+ url: string,
118
+ init: { method: string; headers: Record<string, string>; body?: string; signal?: AbortSignal },
119
+ ): Promise<Response> {
120
+ const dialed = await guardedPublicFetch(url, {
121
+ method: init.method,
122
+ headers: init.headers,
123
+ body: init.body,
124
+ signal: init.signal,
125
+ });
126
+ if (!dialed.ok) {
127
+ throw new Error(`Refusing non-public OAuth endpoint: ${dialed.reason}`);
128
+ }
129
+ if (isOAuthRedirect(dialed.response)) {
130
+ cancelMCPStream(dialed.response.body);
131
+ throw new Error("Refusing non-public OAuth endpoint: redirect");
132
+ }
133
+ return dialed.response;
134
+ }
135
+
107
136
  /**
108
137
  * Canonical MCP server resource URI per RFC 8707 §2: absolute URI, lowercase
109
138
  * scheme/host, no fragment, no trailing slash on an empty path.
@@ -264,9 +293,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
264
293
  }
265
294
 
266
295
  await assertPublicOAuthUrl(this.config.tokenUrl, { signal: this.ctrl.signal });
267
- const response = await fetch(this.config.tokenUrl, {
296
+ const response = await fetchPinnedOAuthEndpoint(this.config.tokenUrl, {
268
297
  method: "POST",
269
- redirect: "error",
270
298
  headers: {
271
299
  "Content-Type": "application/x-www-form-urlencoded",
272
300
  },
@@ -345,9 +373,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
345
373
 
346
374
  try {
347
375
  await assertPublicOAuthUrl(registrationEndpoint, { signal });
348
- const response = await fetch(registrationEndpoint, {
376
+ const response = await fetchPinnedOAuthEndpoint(registrationEndpoint, {
349
377
  method: "POST",
350
- redirect: "error",
351
378
  headers: {
352
379
  "Content-Type": "application/json",
353
380
  Accept: "application/json",
@@ -387,9 +414,8 @@ export class MCPOAuthFlow extends OAuthCallbackFlow {
387
414
  const authorizationEndpoint = new URL(this.config.authorizationUrl);
388
415
  const metadataUrl = new URL("/.well-known/oauth-authorization-server", authorizationEndpoint.origin);
389
416
  await assertPublicOAuthUrl(metadataUrl.toString(), { signal });
390
- const response = await fetch(metadataUrl.toString(), {
417
+ const response = await fetchPinnedOAuthEndpoint(metadataUrl.toString(), {
391
418
  method: "GET",
392
- redirect: "error",
393
419
  headers: { Accept: "application/json" },
394
420
  signal,
395
421
  });