@khalilgharbaoui/opencode-claude-code-plugin 0.39.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4887,6 +4887,9 @@ function noteInteractiveProcessExit(sessionKey2, ap, code) {
4887
4887
  if (activeProcesses.get(sessionKey2) !== ap) return;
4888
4888
  cancelIdleProcessEviction(sessionKey2);
4889
4889
  activeProcesses.delete(sessionKey2);
4890
+ if (getPendingProxyCalls(sessionKey2).length > 0) {
4891
+ rejectAllPendingProxyCallsForSession(sessionKey2, new Error(CHILD_EXITED_MESSAGE));
4892
+ }
4890
4893
  if (code !== 0 && code !== null) {
4891
4894
  claudeSessions.delete(sessionKey2);
4892
4895
  forgetResumePoint(sessionKey2);
@@ -8772,6 +8775,8 @@ var ClaudeSession = class {
8772
8775
  trustMoves = 0;
8773
8776
  trustMovedAt = 0;
8774
8777
  turn = null;
8778
+ /** The running turn's read state, shared with `flushTranscript`. */
8779
+ scan = null;
8775
8780
  turnDenied = [];
8776
8781
  abandonTurn = false;
8777
8782
  o;
@@ -9084,67 +9089,103 @@ var ClaudeSession = class {
9084
9089
  this.proc.terminal.write(prompt);
9085
9090
  }
9086
9091
  await this.submitTurn();
9087
- let stopReason = null;
9088
- let stopSeenAt = 0;
9089
- let end = null;
9090
- let sawUserRecord = false;
9091
- let lastBeat = Date.now();
9092
+ const scan = {
9093
+ onRecord,
9094
+ stopReason: null,
9095
+ stopSeenAt: 0,
9096
+ end: null,
9097
+ sawUserRecord: false,
9098
+ lastBeat: Date.now()
9099
+ };
9100
+ this.scan = scan;
9092
9101
  const deadline = Date.now() + timeout;
9093
9102
  while (Date.now() < deadline) {
9094
9103
  await delay(this.o.pollMs);
9095
9104
  if (this.aborted) throw new Error("aborted mid-turn");
9096
9105
  const fatalNow = this.fatalError();
9097
9106
  if (fatalNow) throw fatalNow;
9098
- const lines = this.readRawLines();
9099
- const lastComplete = lines.length - 1;
9100
- if (lastComplete > this.cursor) {
9101
- for (let i = this.cursor; i < lastComplete && !end; i++) {
9102
- const s = lines[i];
9103
- if (!s || !s.trim()) continue;
9104
- let rec = null;
9105
- try {
9106
- rec = JSON.parse(s);
9107
- } catch {
9108
- }
9109
- onRecord(s, rec);
9110
- if (!rec) continue;
9111
- if (rec.type === "assistant" && rec.message) {
9112
- const reason = rec.message.stop_reason;
9113
- if (typeof reason === "string") {
9114
- stopReason = TERMINAL_STOP.has(reason) ? reason : null;
9115
- }
9116
- } else if (rec.type === "user") {
9117
- if (sawUserRecord && isInterruptRecord(rec)) end = "interrupted";
9118
- sawUserRecord = true;
9119
- } else if ((sawUserRecord || stopReason) && isTurnDurationRecord(rec)) {
9120
- end = stopReason ? "stop" : "ended";
9121
- }
9122
- this.cursor = i + 1;
9107
+ if (this.scanTranscript() || scan.end) {
9108
+ if (scan.end) {
9109
+ return {
9110
+ stopReason: scan.end === "stop" ? scan.stopReason : null,
9111
+ end: scan.end,
9112
+ denied: this.turnDenied
9113
+ };
9123
9114
  }
9124
- if (!end) this.cursor = lastComplete;
9125
- lastBeat = Date.now();
9126
- stopSeenAt = Date.now();
9127
- if (end) return { stopReason: end === "stop" ? stopReason : null, end, denied: this.turnDenied };
9128
9115
  continue;
9129
9116
  }
9130
- if (stopReason && Date.now() - stopSeenAt >= this.o.stopSettleMs) {
9131
- return { stopReason, end: "stop", denied: this.turnDenied };
9117
+ if (scan.stopReason && Date.now() - scan.stopSeenAt >= this.o.stopSettleMs) {
9118
+ return { stopReason: scan.stopReason, end: "stop", denied: this.turnDenied };
9132
9119
  }
9133
9120
  if (this.exited) throw new Error(this.failureMessage("claude exited mid-turn", true));
9134
9121
  if (this.abandonTurn) return { stopReason: null, end: "interrupted", denied: this.turnDenied };
9135
9122
  const now = Date.now();
9136
- if (onHeartbeat && now - lastBeat >= this.o.heartbeatMs) {
9137
- lastBeat = now;
9123
+ if (onHeartbeat && now - scan.lastBeat >= this.o.heartbeatMs) {
9124
+ scan.lastBeat = now;
9138
9125
  if (now - this.lastDataAt < this.o.heartbeatMs) onHeartbeat();
9139
9126
  }
9140
9127
  }
9141
9128
  return { stopReason: null, end: null, denied: this.turnDenied };
9142
9129
  } finally {
9143
9130
  this.turn = null;
9131
+ this.scan = null;
9144
9132
  this.abandonTurn = false;
9145
9133
  settle();
9146
9134
  }
9147
9135
  }
9136
+ /**
9137
+ * Read every complete transcript record past the cursor into the running
9138
+ * turn. Returns whether anything new was read. The one place a record is
9139
+ * interpreted, shared by the poll loop and `flushTranscript`.
9140
+ */
9141
+ scanTranscript() {
9142
+ const scan = this.scan;
9143
+ if (!scan || scan.end) return false;
9144
+ const lines = this.readRawLines();
9145
+ const lastComplete = lines.length - 1;
9146
+ if (lastComplete <= this.cursor) return false;
9147
+ for (let i = this.cursor; i < lastComplete && !scan.end; i++) {
9148
+ const s = lines[i];
9149
+ if (!s || !s.trim()) {
9150
+ this.cursor = i + 1;
9151
+ continue;
9152
+ }
9153
+ let rec = null;
9154
+ try {
9155
+ rec = JSON.parse(s);
9156
+ } catch {
9157
+ }
9158
+ scan.onRecord(s, rec);
9159
+ if (rec) {
9160
+ if (rec.type === "assistant" && rec.message) {
9161
+ const reason = rec.message.stop_reason;
9162
+ if (typeof reason === "string") {
9163
+ scan.stopReason = TERMINAL_STOP.has(reason) ? reason : null;
9164
+ }
9165
+ } else if (rec.type === "user") {
9166
+ if (scan.sawUserRecord && isInterruptRecord(rec)) scan.end = "interrupted";
9167
+ scan.sawUserRecord = true;
9168
+ } else if ((scan.sawUserRecord || scan.stopReason) && isTurnDurationRecord(rec)) {
9169
+ scan.end = scan.stopReason ? "stop" : "ended";
9170
+ }
9171
+ }
9172
+ this.cursor = i + 1;
9173
+ }
9174
+ if (!scan.end) this.cursor = lastComplete;
9175
+ scan.lastBeat = Date.now();
9176
+ scan.stopSeenAt = Date.now();
9177
+ return true;
9178
+ }
9179
+ /**
9180
+ * Hand the running turn every record the TUI has already written, now
9181
+ * rather than at the next poll. The TUI writes a reply's records before it
9182
+ * runs the reply's tools, but a proxied tool's MCP call can reach the
9183
+ * plugin inside one poll interval, and the step it ends must not close
9184
+ * before the text that preceded the call was read. No-op between turns.
9185
+ */
9186
+ flushTranscript() {
9187
+ this.scanTranscript();
9188
+ }
9148
9189
  /**
9149
9190
  * Stop the running turn the way the TUI's own Esc does, and keep the
9150
9191
  * session alive for the next one. Resolves true once the turn ended, false
@@ -9316,6 +9357,9 @@ function interactiveExtraArgs(opts) {
9316
9357
  "--strict-mcp-config"
9317
9358
  );
9318
9359
  }
9360
+ if (opts.disallowedTools && opts.disallowedTools.length > 0) {
9361
+ extraArgs.push("--disallowedTools", ...opts.disallowedTools);
9362
+ }
9319
9363
  for (const dir of opts.pluginDirs ?? []) {
9320
9364
  extraArgs.push("--plugin-dir", dir);
9321
9365
  }
@@ -9415,12 +9459,30 @@ function interactiveResultFrame(opts) {
9415
9459
  duration_ms: 0
9416
9460
  });
9417
9461
  }
9462
+ function carriesReplyText(line) {
9463
+ try {
9464
+ const rec = JSON.parse(line);
9465
+ if (rec?.type !== "assistant" || !Array.isArray(rec.message?.content)) return false;
9466
+ return rec.message.content.some(
9467
+ (block) => block?.type === "text" && typeof block.text === "string" && block.text.trim()
9468
+ );
9469
+ } catch {
9470
+ return false;
9471
+ }
9472
+ }
9418
9473
  function spawnInteractiveProcess(opts) {
9419
9474
  const extraArgs = interactiveExtraArgs(opts);
9420
9475
  const openCalls = new OpenToolCalls();
9421
9476
  let denials = [];
9422
9477
  const lineEmitter = new EventEmitter4();
9423
9478
  const proc = new EventEmitter4();
9479
+ let ap;
9480
+ let proxyClosed = false;
9481
+ const closeProxyServer = () => {
9482
+ if (proxyClosed || !opts.proxyServer) return;
9483
+ proxyClosed = true;
9484
+ void opts.proxyServer.close();
9485
+ };
9424
9486
  const session = new ClaudeSession({
9425
9487
  ...opts.tuning,
9426
9488
  cwd: opts.cwd,
@@ -9450,6 +9512,7 @@ function spawnInteractiveProcess(opts) {
9450
9512
  onExit: (code) => {
9451
9513
  proc.exitCode = code;
9452
9514
  proc.signalCode = code === null ? "SIGTERM" : null;
9515
+ closeProxyServer();
9453
9516
  log.info("interactive claude exited", { sessionId: session.sessionId, code });
9454
9517
  proc.emit("exit", proc.exitCode, proc.signalCode);
9455
9518
  proc.emit("close", proc.exitCode, proc.signalCode);
@@ -9477,7 +9540,12 @@ function spawnInteractiveProcess(opts) {
9477
9540
  const runTurn = async (userMsg, turnNumber) => {
9478
9541
  const current = () => turnNumber === writes;
9479
9542
  const emit2 = (line) => {
9480
- if (current()) lineEmitter.emit("line", line);
9543
+ if (!current()) return;
9544
+ if (lineEmitter.listenerCount("line") === 0) {
9545
+ if (carriesReplyText(line)) bufferUnattendedLine(ap, line);
9546
+ return;
9547
+ }
9548
+ lineEmitter.emit("line", line);
9481
9549
  };
9482
9550
  try {
9483
9551
  await ensureStarted();
@@ -9497,7 +9565,13 @@ function spawnInteractiveProcess(opts) {
9497
9565
  emit2(raw);
9498
9566
  },
9499
9567
  void 0,
9500
- () => emit2(heartbeatFrame(session.sessionId, openCalls.newest()))
9568
+ // Only to a listening turn: it feeds that turn's watchdog, and the
9569
+ // TUI redraws its spinner the whole time it waits on a proxied call.
9570
+ () => {
9571
+ if (lineEmitter.listenerCount("line") > 0) {
9572
+ emit2(heartbeatFrame(session.sessionId, openCalls.newest()));
9573
+ }
9574
+ }
9501
9575
  );
9502
9576
  log.info("interactive turn ended", {
9503
9577
  end,
@@ -9570,6 +9644,7 @@ function spawnInteractiveProcess(opts) {
9570
9644
  });
9571
9645
  }
9572
9646
  }
9647
+ closeProxyServer();
9573
9648
  const started = startPromise !== null;
9574
9649
  try {
9575
9650
  session.dispose();
@@ -9583,11 +9658,13 @@ function spawnInteractiveProcess(opts) {
9583
9658
  return true;
9584
9659
  }
9585
9660
  });
9586
- return {
9661
+ ap = {
9587
9662
  proc,
9588
9663
  lineEmitter,
9589
- proxyServer: null,
9664
+ proxyServer: opts.proxyServer ?? null,
9590
9665
  mcpHash: void 0,
9666
+ unattendedLines: [],
9667
+ unattendedDropped: 0,
9591
9668
  systemPromptFile: opts.systemPromptFile,
9592
9669
  cliPath: opts.cliPath,
9593
9670
  interactiveControl: {
@@ -9597,9 +9674,11 @@ function spawnInteractiveProcess(opts) {
9597
9674
  interrupt: (timeoutMs) => {
9598
9675
  cancelledThrough = writes;
9599
9676
  return session.interrupt(timeoutMs);
9600
- }
9677
+ },
9678
+ flushTranscript: () => session.flushTranscript()
9601
9679
  }
9602
9680
  };
9681
+ return ap;
9603
9682
  }
9604
9683
 
9605
9684
  // src/prompts.ts
@@ -11162,6 +11241,8 @@ function drainNow(state) {
11162
11241
  }
11163
11242
  if (state.drainBuffer.length === 0) return;
11164
11243
  if (state.controllerClosed) return;
11244
+ state.activeProcess?.interactiveControl?.flushTranscript();
11245
+ if (state.controllerClosed) return;
11165
11246
  const batch = state.drainBuffer.splice(0, state.drainBuffer.length);
11166
11247
  log.info("draining pending proxy calls into stream finish", {
11167
11248
  sessionKey: state.sessionKey,
@@ -13015,13 +13096,145 @@ var ClaudeCodeLanguageModel = class {
13015
13096
  state.proxyServer = null;
13016
13097
  }
13017
13098
  }
13099
+ const resolveProxyWiring = async () => {
13100
+ const discovery = self.effectiveMcpConfig(
13101
+ cwd,
13102
+ void 0,
13103
+ runtimeStatus
13104
+ );
13105
+ const mcpResolution = self.resolvedProxyMcpTools(
13106
+ discovery.allEnabledServerNames,
13107
+ options.tools,
13108
+ new Set((resolvedProxy ?? []).map((def) => def.name))
13109
+ );
13110
+ const proxyMcpTools = mcpResolution?.defs ?? null;
13111
+ const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
13112
+ const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
13113
+ const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
13114
+ const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
13115
+ log.debug("opencode tool forwarding gate", {
13116
+ requested: self.config.proxyOpencodeTools ?? null,
13117
+ opencodeToolsRequested
13118
+ });
13119
+ const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
13120
+ resolved: false,
13121
+ taskDescription: void 0,
13122
+ taskParameters: void 0,
13123
+ questionDescription: void 0,
13124
+ hasQuestion: false
13125
+ };
13126
+ let enrichedProxy = resolvedProxy;
13127
+ let backgroundSubagentsSupported = false;
13128
+ if (enrichedProxy && taskProxyEnabled) {
13129
+ enrichedProxy = overlayTaskProxyDescription(
13130
+ enrichedProxy,
13131
+ liveToolInfo.taskDescription
13132
+ );
13133
+ log.info("task proxy description overlay", {
13134
+ applied: Boolean(liveToolInfo.taskDescription),
13135
+ liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
13136
+ listsAgentTypes: Boolean(
13137
+ liveToolInfo.taskDescription?.includes(
13138
+ "Available agent types"
13139
+ )
13140
+ )
13141
+ });
13142
+ const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
13143
+ backgroundSubagentsSupported = liveTaskSupportsBackground(
13144
+ liveToolInfo.taskParameters,
13145
+ hostDialect
13146
+ );
13147
+ enrichedProxy = applyBackgroundSubagentSupport(
13148
+ enrichedProxy,
13149
+ backgroundSubagentsSupported,
13150
+ hostDialect
13151
+ );
13152
+ recordBackgroundSubagentGate({
13153
+ supported: backgroundSubagentsSupported,
13154
+ hostApi: hostDialect,
13155
+ registryResolved: liveToolInfo.resolved
13156
+ });
13157
+ log.info("background subagent gate", {
13158
+ supported: backgroundSubagentsSupported,
13159
+ registryResolved: liveToolInfo.resolved,
13160
+ hostApi: hostDialect,
13161
+ note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
13162
+ });
13163
+ }
13164
+ if (enrichedProxy && questionProxyEnabled) {
13165
+ enrichedProxy = overlayQuestionProxyDescription(
13166
+ enrichedProxy,
13167
+ liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
13168
+ );
13169
+ enrichedProxy = filterQuestionProxyByOpencodeSupport(
13170
+ enrichedProxy,
13171
+ liveToolInfo.hasQuestion
13172
+ );
13173
+ log.info("question proxy version gate", {
13174
+ opencodeHasQuestion: liveToolInfo.hasQuestion,
13175
+ kept: liveToolInfo.hasQuestion
13176
+ });
13177
+ }
13178
+ const opencodeToolDefs = resolveProxyOpencodeToolDefs({
13179
+ requested: self.config.proxyOpencodeTools,
13180
+ items: liveToolInfo.items,
13181
+ modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
13182
+ allowCodeExecution: self.config.permissionPreset !== "read-only",
13183
+ taken: new Set(
13184
+ [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
13185
+ (t) => t.name
13186
+ )
13187
+ )
13188
+ });
13189
+ if (opencodeToolDefs.length > 0) {
13190
+ log.info("forwarding opencode tools through the proxy", {
13191
+ tools: opencodeToolDefs.map((t) => t.name)
13192
+ });
13193
+ }
13194
+ const combinedList = [
13195
+ ...enrichedProxy ?? [],
13196
+ ...proxyMcpTools ?? [],
13197
+ ...opencodeToolDefs
13198
+ ];
13199
+ const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
13200
+ const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
13201
+ if (!state.proxyServer && combinedProxyTools) {
13202
+ state.proxyServer = await self.ensureProxyServer(
13203
+ combinedProxyTools,
13204
+ sk,
13205
+ pluginCompressEnabled,
13206
+ affinity
13207
+ );
13208
+ }
13209
+ const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
13210
+ const allDisallowed = resolveDisallowedTools({
13211
+ proxyTools: enrichedProxy,
13212
+ extraDisallowedTools: self.config.extraDisallowedTools,
13213
+ disableWebSearch: self.config.webSearch === "disabled"
13214
+ });
13215
+ return {
13216
+ excludeServers,
13217
+ taskProxyEnabled,
13218
+ backgroundSubagentsSupported,
13219
+ questionProxyActive,
13220
+ opencodeToolDefs,
13221
+ pluginCompressEnabled,
13222
+ allDisallowed
13223
+ };
13224
+ };
13018
13225
  if (useInteractive && !compactionMode) {
13019
- const mcp = self.effectiveMcpConfig(cwd, void 0, runtimeStatus);
13020
13226
  if (state.activeProcess) {
13021
13227
  state.proc = state.activeProcess.proc;
13022
13228
  state.lineEmitter = state.activeProcess.lineEmitter;
13023
13229
  log.debug("reusing active interactive session", { sk });
13024
13230
  } else {
13231
+ const wiring = await resolveProxyWiring();
13232
+ const mcp = self.effectiveMcpConfig(
13233
+ cwd,
13234
+ state.proxyServer?.configPath(),
13235
+ runtimeStatus,
13236
+ wiring.excludeServers
13237
+ );
13025
13238
  const allow = [
13026
13239
  ...mcp.allEnabledServerNames.map((n) => `mcp__${n}__*`),
13027
13240
  "mcp__opencode_proxy__*",
@@ -13035,12 +13248,27 @@ var ClaudeCodeLanguageModel = class {
13035
13248
  ];
13036
13249
  const systemPromptFile = self.config.interactiveSystemPrompt === false ? void 0 : buildAppendedSystemPrompt(
13037
13250
  cwd,
13038
- self.config.multiStepContinuation !== false
13251
+ self.config.multiStepContinuation !== false,
13039
13252
  // Do not forward opencode's own system prompt into the
13040
13253
  // interactive TUI. Live subscription-account testing
13041
13254
  // showed that large forwarded payload can trigger Claude
13042
13255
  // Code's third-party-app usage gate, while our static
13043
- // CLI/AGENTS/continuation prompt remains safe.
13256
+ // CLI/AGENTS/continuation prompt remains safe. The
13257
+ // plugin's own short proxy hints are part of that static
13258
+ // prompt: without them the model narrates a subagent
13259
+ // dispatch instead of calling the proxy (h #g74).
13260
+ [
13261
+ ...wiring.taskProxyEnabled ? [SUBAGENT_DISPATCH_HINT] : [],
13262
+ ...wiring.backgroundSubagentsSupported ? [BACKGROUND_SUBAGENT_HINT] : [],
13263
+ ...wiring.questionProxyActive ? [QUESTION_PROXY_HINT] : []
13264
+ ],
13265
+ {
13266
+ compressEnabled: wiring.pluginCompressEnabled,
13267
+ opencodeCompressEnabled: wiring.opencodeToolDefs.some(
13268
+ (t) => t.name === "compress"
13269
+ ),
13270
+ compressionSummary: getCompressionSummary(sk)
13271
+ }
13044
13272
  );
13045
13273
  if (self.config.interactiveSystemPrompt === false) {
13046
13274
  log.warn(
@@ -13069,6 +13297,8 @@ var ClaudeCodeLanguageModel = class {
13069
13297
  mcpConfigPaths: mcp.paths,
13070
13298
  pluginDirs: skillPluginDirs,
13071
13299
  permissionsAllow: allow,
13300
+ disallowedTools: wiring.allDisallowed,
13301
+ proxyServer: state.proxyServer,
13072
13302
  systemPromptFile,
13073
13303
  ignoreAnthropicApiKey: self.config.ignoreAnthropicApiKey,
13074
13304
  effort: reasoningEffort,
@@ -13115,121 +13345,15 @@ var ClaudeCodeLanguageModel = class {
13115
13345
  cliVersion
13116
13346
  });
13117
13347
  } else {
13118
- const discovery = self.effectiveMcpConfig(
13119
- cwd,
13120
- void 0,
13121
- runtimeStatus
13122
- );
13123
- const mcpResolution = self.resolvedProxyMcpTools(
13124
- discovery.allEnabledServerNames,
13125
- options.tools,
13126
- new Set((resolvedProxy ?? []).map((def) => def.name))
13127
- );
13128
- const proxyMcpTools = mcpResolution?.defs ?? null;
13129
- const excludeServers = mcpResolution ? mcpResolution.coveredServers : void 0;
13130
- const taskProxyEnabled = resolvedProxy?.some((t) => t.name === "task") ?? false;
13131
- const questionProxyEnabled = resolvedProxy?.some((t) => t.name === "question") ?? false;
13132
- const opencodeToolsRequested = (self.config.proxyOpencodeTools?.length ?? 0) > 0;
13133
- log.debug("opencode tool forwarding gate", {
13134
- requested: self.config.proxyOpencodeTools ?? null,
13135
- opencodeToolsRequested
13136
- });
13137
- const liveToolInfo = taskProxyEnabled || questionProxyEnabled || opencodeToolsRequested ? await loadLiveToolInfo() : {
13138
- resolved: false,
13139
- taskDescription: void 0,
13140
- taskParameters: void 0,
13141
- questionDescription: void 0,
13142
- hasQuestion: false
13143
- };
13144
- let enrichedProxy = resolvedProxy;
13145
- let backgroundSubagentsSupported = false;
13146
- if (enrichedProxy && taskProxyEnabled) {
13147
- enrichedProxy = overlayTaskProxyDescription(
13148
- enrichedProxy,
13149
- liveToolInfo.taskDescription
13150
- );
13151
- log.info("task proxy description overlay", {
13152
- applied: Boolean(liveToolInfo.taskDescription),
13153
- liveDescriptionLength: liveToolInfo.taskDescription?.length ?? 0,
13154
- listsAgentTypes: Boolean(
13155
- liveToolInfo.taskDescription?.includes(
13156
- "Available agent types"
13157
- )
13158
- )
13159
- });
13160
- const hostDialect = self.config.hostApi === "v2" ? "v2" : "v1";
13161
- backgroundSubagentsSupported = liveTaskSupportsBackground(
13162
- liveToolInfo.taskParameters,
13163
- hostDialect
13164
- );
13165
- enrichedProxy = applyBackgroundSubagentSupport(
13166
- enrichedProxy,
13167
- backgroundSubagentsSupported,
13168
- hostDialect
13169
- );
13170
- recordBackgroundSubagentGate({
13171
- supported: backgroundSubagentsSupported,
13172
- hostApi: hostDialect,
13173
- registryResolved: liveToolInfo.resolved
13174
- });
13175
- log.info("background subagent gate", {
13176
- supported: backgroundSubagentsSupported,
13177
- registryResolved: liveToolInfo.resolved,
13178
- hostApi: hostDialect,
13179
- note: backgroundSubagentsSupported ? "task accepts `background`; task_status and task_cancel are registered" : "`background` stripped from the task schema; set OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true in opencode's environment to enable it"
13180
- });
13181
- }
13182
- if (enrichedProxy && questionProxyEnabled) {
13183
- enrichedProxy = overlayQuestionProxyDescription(
13184
- enrichedProxy,
13185
- liveToolInfo.hasQuestion ? liveToolInfo.questionDescription : void 0
13186
- );
13187
- enrichedProxy = filterQuestionProxyByOpencodeSupport(
13188
- enrichedProxy,
13189
- liveToolInfo.hasQuestion
13190
- );
13191
- log.info("question proxy version gate", {
13192
- opencodeHasQuestion: liveToolInfo.hasQuestion,
13193
- kept: liveToolInfo.hasQuestion
13194
- });
13195
- }
13196
- const opencodeToolDefs = resolveProxyOpencodeToolDefs({
13197
- requested: self.config.proxyOpencodeTools,
13198
- items: liveToolInfo.items,
13199
- modelTools: self.config.hostApi === "v2" ? options.tools ?? [] : void 0,
13200
- allowCodeExecution: self.config.permissionPreset !== "read-only",
13201
- taken: new Set(
13202
- [...enrichedProxy ?? [], ...proxyMcpTools ?? []].map(
13203
- (t) => t.name
13204
- )
13205
- )
13206
- });
13207
- if (opencodeToolDefs.length > 0) {
13208
- log.info("forwarding opencode tools through the proxy", {
13209
- tools: opencodeToolDefs.map((t) => t.name)
13210
- });
13211
- }
13212
- const combinedList = [
13213
- ...enrichedProxy ?? [],
13214
- ...proxyMcpTools ?? [],
13215
- ...opencodeToolDefs
13216
- ];
13217
- const combinedProxyTools = combinedList.length > 0 ? combinedList : null;
13218
- const pluginCompressEnabled = enrichedProxy?.some((t) => t.name === "compress") ?? false;
13219
- if (!state.proxyServer && combinedProxyTools) {
13220
- state.proxyServer = await self.ensureProxyServer(
13221
- combinedProxyTools,
13222
- sk,
13223
- pluginCompressEnabled,
13224
- affinity
13225
- );
13226
- }
13227
- const questionProxyActive = enrichedProxy?.some((t) => t.name === "question") ?? false;
13228
- const allDisallowed = resolveDisallowedTools({
13229
- proxyTools: enrichedProxy,
13230
- extraDisallowedTools: self.config.extraDisallowedTools,
13231
- disableWebSearch: self.config.webSearch === "disabled"
13232
- });
13348
+ const {
13349
+ excludeServers,
13350
+ taskProxyEnabled,
13351
+ backgroundSubagentsSupported,
13352
+ questionProxyActive,
13353
+ opencodeToolDefs,
13354
+ pluginCompressEnabled,
13355
+ allDisallowed
13356
+ } = await resolveProxyWiring();
13233
13357
  const mcp = self.effectiveMcpConfig(
13234
13358
  cwd,
13235
13359
  state.proxyServer?.configPath(),