@oh-my-pi/pi-coding-agent 17.3.5 → 17.3.8
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/CHANGELOG.md +80 -0
- package/dist/{CHANGELOG-tt9k4jpr.md → CHANGELOG-vr9cckb4.md} +80 -0
- package/dist/cli.js +2993 -3001
- package/dist/docs-index.generated.txt +1 -1
- package/dist/{tool-views.generated-jdfmzwmn.js → tool-views.generated-dd2km5r2.js} +19 -19
- package/dist/types/advisor/advise-tool.d.ts +4 -2
- package/dist/types/cli/auth-broker-cli.d.ts +15 -0
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/cli/update-cli.d.ts +8 -0
- package/dist/types/cli-commands.d.ts +10 -2
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +28 -0
- package/dist/types/config/settings.d.ts +9 -0
- package/dist/types/extensibility/extensions/runner.d.ts +23 -4
- package/dist/types/extensibility/extensions/types.d.ts +58 -0
- package/dist/types/launch/presence.d.ts +4 -1
- package/dist/types/mcp/oauth-credentials.d.ts +23 -0
- package/dist/types/mcp/oauth-flow.d.ts +11 -0
- package/dist/types/mnemopi/backend.d.ts +12 -0
- package/dist/types/modes/components/tool-execution.d.ts +12 -0
- package/dist/types/modes/controllers/input-controller.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +25 -3
- package/dist/types/modes/types.d.ts +10 -0
- package/dist/types/session/agent-session.d.ts +3 -0
- package/dist/types/session/prewalk.d.ts +4 -0
- package/dist/types/session/session-entries.d.ts +0 -1
- package/dist/types/session/session-manager.d.ts +12 -0
- package/dist/types/session/session-stats.d.ts +13 -1
- package/dist/types/session/skill-title-input.d.ts +13 -0
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/subprocess/worker-client.d.ts +7 -4
- package/dist/types/task/label.d.ts +2 -0
- package/dist/types/task/render.d.ts +2 -0
- package/dist/types/tiny/completion-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +6 -4
- package/dist/types/tiny/title-protocol.d.ts +1 -0
- package/dist/types/tiny/worker.d.ts +27 -0
- package/dist/types/tools/bash.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/tools/read-format.d.ts +6 -0
- package/dist/types/tools/read-summary.d.ts +7 -1
- package/dist/types/utils/block-context.d.ts +14 -0
- package/dist/types/utils/fetch-timeout.d.ts +15 -0
- package/dist/types/utils/git.d.ts +25 -1
- package/dist/types/web/search/providers/tinyfish.d.ts +4 -0
- package/package.json +13 -13
- package/src/advisor/advise-tool.ts +5 -3
- package/src/cli/auth-broker-cli.ts +36 -1
- package/src/cli/profile-bootstrap.ts +2 -6
- package/src/cli/stats-cli.ts +6 -72
- package/src/cli/update-cli.ts +63 -11
- package/src/cli-commands.ts +61 -7
- package/src/commands/completions.ts +2 -1
- package/src/commands/stats.ts +7 -4
- package/src/commit/agentic/index.ts +15 -2
- package/src/commit/git/diff.ts +6 -2
- package/src/config/model-resolver.ts +52 -6
- package/src/config/models-config.ts +2 -2
- package/src/config/settings-schema.ts +33 -0
- package/src/config/settings.ts +159 -30
- package/src/discovery/helpers.ts +45 -2
- package/src/discovery/omp-plugins.ts +2 -1
- package/src/discovery/opencode.ts +56 -3
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/eval/js/process-entry.ts +4 -4
- package/src/export/html/tool-views.generated.js +19 -19
- package/src/extensibility/extensions/loader.ts +11 -0
- package/src/extensibility/extensions/runner.ts +118 -5
- package/src/extensibility/extensions/types.ts +60 -0
- package/src/extensibility/extensions/wrapper.ts +10 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +47 -0
- package/src/launch/client.ts +9 -4
- package/src/launch/presence.ts +19 -4
- package/src/lsp/defaults.json +1 -1
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/manager.ts +41 -20
- package/src/mcp/oauth-credentials.ts +38 -0
- package/src/mcp/oauth-flow.ts +21 -0
- package/src/mcp/tool-bridge.ts +32 -16
- package/src/mnemopi/backend.ts +35 -3
- package/src/modes/components/model-hub.ts +37 -4
- package/src/modes/components/settings-selector.ts +17 -11
- package/src/modes/components/tool-execution.ts +97 -29
- package/src/modes/components/tree-selector.ts +7 -2
- package/src/modes/controllers/event-controller.ts +12 -2
- package/src/modes/controllers/input-controller.ts +64 -27
- package/src/modes/controllers/mcp-command-controller.ts +13 -4
- package/src/modes/interactive-mode.ts +79 -11
- package/src/modes/types.ts +11 -0
- package/src/prompts/system/memory-extraction-system.md +5 -22
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/session/agent-session.ts +52 -6
- package/src/session/messages.ts +6 -0
- package/src/session/prewalk.ts +25 -7
- package/src/session/session-entries.ts +0 -1
- package/src/session/session-maintenance.ts +10 -1
- package/src/session/session-manager.ts +15 -0
- package/src/session/session-stats.ts +24 -3
- package/src/session/settings-stream-fn.ts +7 -0
- package/src/session/skill-title-input.ts +32 -0
- package/src/session/turn-recovery.ts +23 -18
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/subprocess/worker-client.ts +8 -5
- package/src/task/executor.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/task/label.ts +14 -1
- package/src/task/persisted-revive.ts +13 -0
- package/src/task/render.ts +1 -1
- package/src/task/structured-subagent.ts +5 -2
- package/src/tiny/completion-prompt.ts +16 -0
- package/src/tiny/title-client.ts +15 -6
- package/src/tiny/title-protocol.ts +8 -1
- package/src/tiny/worker.ts +21 -19
- package/src/tools/bash.ts +7 -1
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/read-format.ts +16 -2
- package/src/tools/read-summary.ts +9 -4
- package/src/tools/read.ts +306 -72
- package/src/utils/block-context.ts +15 -1
- package/src/utils/fetch-timeout.ts +33 -0
- package/src/utils/git.ts +54 -11
- package/src/web/search/providers/browser-page.ts +21 -3
- package/src/web/search/providers/tinyfish.ts +26 -0
|
@@ -28,6 +28,7 @@ import { execCommand } from "../../exec/exec";
|
|
|
28
28
|
// Runtime self-reference: dereference this namespace only inside loader functions to keep the index.ts cycle safe.
|
|
29
29
|
import * as PiCodingAgent from "../../index";
|
|
30
30
|
import type { CustomMessagePayload } from "../../session/messages";
|
|
31
|
+
import type { FileDeleteFallbackHandler, FileWriteFallbackHandler } from "../../tools/file-write-fallback";
|
|
31
32
|
import { EventBus } from "../../utils/event-bus";
|
|
32
33
|
import * as TypeBox from "../legacy-typebox";
|
|
33
34
|
import { installLegacyPiSpecifierShim, loadLegacyPiModule } from "../plugins/legacy-pi-compat";
|
|
@@ -183,6 +184,14 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
183
184
|
for (const listener of this.extension.toolRegistrationListeners ?? []) listener(tool.name);
|
|
184
185
|
}
|
|
185
186
|
|
|
187
|
+
registerFileWriteFallback(handler: FileWriteFallbackHandler): void {
|
|
188
|
+
this.extension.fileWriteFallbackHandlers.push(handler);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
registerFileDeleteFallback(handler: FileDeleteFallbackHandler): void {
|
|
192
|
+
this.extension.fileDeleteFallbackHandlers.push(handler);
|
|
193
|
+
}
|
|
194
|
+
|
|
186
195
|
registerCommand(
|
|
187
196
|
name: string,
|
|
188
197
|
options: {
|
|
@@ -320,6 +329,8 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
|
|
|
320
329
|
tools: new Map(),
|
|
321
330
|
toolRegistrationListeners: new Set(),
|
|
322
331
|
assistantThinkingRenderers: [],
|
|
332
|
+
fileWriteFallbackHandlers: [],
|
|
333
|
+
fileDeleteFallbackHandlers: [],
|
|
323
334
|
messageRenderers: new Map(),
|
|
324
335
|
commands: new Map(),
|
|
325
336
|
flags: new Map(),
|
|
@@ -19,6 +19,7 @@ import type { MemoryRuntimeContext } from "../../memory-backend";
|
|
|
19
19
|
import { type Theme, theme } from "../../modes/theme/theme";
|
|
20
20
|
import type { AsyncJobSnapshot } from "../../session/agent-session";
|
|
21
21
|
import type { SessionManager } from "../../session/session-manager";
|
|
22
|
+
import { addFileDeleteFallback, addFileWriteFallback } from "../../tools/file-write-fallback";
|
|
22
23
|
import type { BranchHandler, NavigateTreeHandler, NewSessionHandler } from "../session-handler-types";
|
|
23
24
|
import { ManagedTimers } from "./managed-timers";
|
|
24
25
|
import { createExtensionModelQuery } from "./model-api";
|
|
@@ -375,10 +376,12 @@ export type SwitchSessionHandler = (sessionPath: string) => Promise<{ cancelled:
|
|
|
375
376
|
export type ShutdownHandler = () => void;
|
|
376
377
|
|
|
377
378
|
/**
|
|
378
|
-
* Emit `session_shutdown
|
|
379
|
+
* Emit `session_shutdown`, dispose file-write-fallback registrations, and clear
|
|
380
|
+
* timers owned by an extension runner.
|
|
379
381
|
*
|
|
380
|
-
* Returns whether any shutdown handlers were present.
|
|
381
|
-
* when a handler fails so extension background work
|
|
382
|
+
* Returns whether any shutdown handlers were present. Fallback disposal and timer
|
|
383
|
+
* cleanup run even when a handler fails so extension background work — and a
|
|
384
|
+
* fallback bound to this session's context — cannot outlive its host.
|
|
382
385
|
*/
|
|
383
386
|
export async function emitSessionShutdownEvent(extensionRunner: ExtensionRunner | undefined): Promise<boolean> {
|
|
384
387
|
if (!extensionRunner) return false;
|
|
@@ -389,6 +392,7 @@ export async function emitSessionShutdownEvent(extensionRunner: ExtensionRunner
|
|
|
389
392
|
});
|
|
390
393
|
return true;
|
|
391
394
|
} finally {
|
|
395
|
+
extensionRunner.disposeFileFallbacks();
|
|
392
396
|
extensionRunner.clearManagedTimers();
|
|
393
397
|
}
|
|
394
398
|
}
|
|
@@ -486,6 +490,23 @@ export class ExtensionRunner {
|
|
|
486
490
|
#managedTimers = new ManagedTimers((event, error, stack) =>
|
|
487
491
|
this.emitError({ extensionPath: "<timer>", event, error, stack }),
|
|
488
492
|
);
|
|
493
|
+
/**
|
|
494
|
+
* Disposers for the trampolines installed via {@link addFileWriteFallback} and
|
|
495
|
+
* {@link addFileDeleteFallback} — one per extension per seam it registered for.
|
|
496
|
+
* Installed during {@link initialize} (after the UI/runtime context is live, so
|
|
497
|
+
* the bound handler sees a working `ctx.ui`) and drained by
|
|
498
|
+
* {@link disposeFileFallbacks} on session shutdown so a handler from a
|
|
499
|
+
* torn-down session can never fire for a later one sharing the same process.
|
|
500
|
+
*
|
|
501
|
+
* Each trampoline re-reads its extension's handler list at call time rather than
|
|
502
|
+
* closing over a snapshot, matching how `ext.handlers` is re-read on every emit,
|
|
503
|
+
* so an extension that already had a handler for that seam at `initialize` picks
|
|
504
|
+
* up later additions to it. A seam the extension registered NOTHING for gets no
|
|
505
|
+
* trampoline at all, which keeps the registry empty for a host with no fallbacks;
|
|
506
|
+
* the cost is that a first registration for that seam after `initialize` never
|
|
507
|
+
* takes effect, which is why the API documents load-time registration.
|
|
508
|
+
*/
|
|
509
|
+
#fileFallbackDisposers: Array<() => void> = [];
|
|
489
510
|
/**
|
|
490
511
|
* Dedup markers for `tool_call` emission, keyed `${toolCallId}:${toolName}`.
|
|
491
512
|
* The agent loop emits `tool_call` at arg-prep time (before scheduling and
|
|
@@ -609,6 +630,18 @@ export class ExtensionRunner {
|
|
|
609
630
|
return this.sessionManager.getCwd();
|
|
610
631
|
}
|
|
611
632
|
|
|
633
|
+
/**
|
|
634
|
+
* Stable id of the session this runner serves. Read through `sessionManager`
|
|
635
|
+
* for the same reason as {@link cwd}: it is this session's own, never a
|
|
636
|
+
* process-global, so a subagent runner reports itself and not its parent.
|
|
637
|
+
*
|
|
638
|
+
* Used to attribute a denied file write or delete to the session that issued
|
|
639
|
+
* it, since the fallback registry those handlers live in is process-wide.
|
|
640
|
+
*/
|
|
641
|
+
get sessionId(): string {
|
|
642
|
+
return this.sessionManager.getSessionId();
|
|
643
|
+
}
|
|
644
|
+
|
|
612
645
|
initialize(
|
|
613
646
|
actions: ExtensionActions,
|
|
614
647
|
contextActions: ExtensionContextActions,
|
|
@@ -666,6 +699,75 @@ export class ExtensionRunner {
|
|
|
666
699
|
this.#mode = mode;
|
|
667
700
|
this.#initialized = true;
|
|
668
701
|
|
|
702
|
+
// Re-initialize (e.g. a mode switch rewiring UI/runtime actions) must not
|
|
703
|
+
// accumulate duplicate global registrations — drop the prior generation before
|
|
704
|
+
// installing this one's trampolines.
|
|
705
|
+
this.disposeFileFallbacks();
|
|
706
|
+
for (const ext of this.extensions) {
|
|
707
|
+
// Nothing registered by this extension means no trampoline, so a host with
|
|
708
|
+
// no fallback-registering extension leaves the seam genuinely empty and
|
|
709
|
+
// `hasFileWriteFallback()`/`hasFileDeleteFallback()` false — the invariant
|
|
710
|
+
// the whole feature rests on. Each seam is checked separately, so an
|
|
711
|
+
// extension that only brokers writes never appears in the delete registry.
|
|
712
|
+
if (ext.fileWriteFallbackHandlers.length === 0 && ext.fileDeleteFallbackHandlers.length === 0) continue;
|
|
713
|
+
// One trampoline per extension per seam, not per handler: the list is walked
|
|
714
|
+
// at mutation time so a handler this extension adds later still takes effect,
|
|
715
|
+
// and `createContext()` takes no extension argument, so within one invocation
|
|
716
|
+
// a single context is all any of this extension's handlers would have
|
|
717
|
+
// received anyway.
|
|
718
|
+
//
|
|
719
|
+
// The context is built PER INVOCATION rather than captured here, matching
|
|
720
|
+
// every other dispatch site. `createContext()` materializes `cwd` and
|
|
721
|
+
// `hasUI` as values, so a trampoline holding one context for the life of the
|
|
722
|
+
// session would keep handing handlers the workspace this runner initialized
|
|
723
|
+
// in — wrong the moment `SessionManager.moveTo()` relocates the session
|
|
724
|
+
// (`/move`), and a handler that scopes or prompts against `ctx.cwd` would
|
|
725
|
+
// then allow the old workspace and deny the new one. A denied mutation is a
|
|
726
|
+
// rare path, so the extra object costs nothing that matters.
|
|
727
|
+
//
|
|
728
|
+
// Isolation is per HANDLER, not per extension. The registry only sees one
|
|
729
|
+
// trampoline per extension, so a throw escaping this loop would advance the
|
|
730
|
+
// registry to the NEXT extension and skip every later handler this one
|
|
731
|
+
// registered — breaking both the documented "a throwing handler is skipped"
|
|
732
|
+
// contract and registration order for a backup-handler setup.
|
|
733
|
+
if (ext.fileWriteFallbackHandlers.length > 0) {
|
|
734
|
+
this.#fileFallbackDisposers.push(
|
|
735
|
+
addFileWriteFallback(async req => {
|
|
736
|
+
const ctx = this.createContext();
|
|
737
|
+
for (const handler of ext.fileWriteFallbackHandlers) {
|
|
738
|
+
try {
|
|
739
|
+
if (await handler(req, ctx)) return true;
|
|
740
|
+
} catch (error) {
|
|
741
|
+
logger.warn("Extension file write fallback handler threw; trying next handler", {
|
|
742
|
+
extension: ext.path,
|
|
743
|
+
error: error instanceof Error ? error.message : String(error),
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
return false;
|
|
748
|
+
}),
|
|
749
|
+
);
|
|
750
|
+
}
|
|
751
|
+
if (ext.fileDeleteFallbackHandlers.length > 0) {
|
|
752
|
+
this.#fileFallbackDisposers.push(
|
|
753
|
+
addFileDeleteFallback(async req => {
|
|
754
|
+
const ctx = this.createContext();
|
|
755
|
+
for (const handler of ext.fileDeleteFallbackHandlers) {
|
|
756
|
+
try {
|
|
757
|
+
if (await handler(req, ctx)) return true;
|
|
758
|
+
} catch (error) {
|
|
759
|
+
logger.warn("Extension file delete fallback handler threw; trying next handler", {
|
|
760
|
+
extension: ext.path,
|
|
761
|
+
error: error instanceof Error ? error.message : String(error),
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return false;
|
|
766
|
+
}),
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
669
771
|
// Drain events buffered by emitCredentialDisabled() before initialize ran. The
|
|
670
772
|
// spread adds the `type` discriminator — `event` is the pi-ai shape (no `type`).
|
|
671
773
|
// Deferred by one microtask so callers that register an onError listener
|
|
@@ -1098,6 +1200,16 @@ export class ExtensionRunner {
|
|
|
1098
1200
|
this.#managedTimers.clearAll();
|
|
1099
1201
|
}
|
|
1100
1202
|
|
|
1203
|
+
/**
|
|
1204
|
+
* Remove every file write and delete fallback this runner installed into the
|
|
1205
|
+
* process-wide registries. Called on session shutdown (and before reinstalling
|
|
1206
|
+
* on a re-{@link initialize}) so a handler bound to a torn-down session's
|
|
1207
|
+
* context can never fire for another session sharing this process.
|
|
1208
|
+
*/
|
|
1209
|
+
disposeFileFallbacks(): void {
|
|
1210
|
+
for (const dispose of this.#fileFallbackDisposers.splice(0)) dispose();
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1101
1213
|
createCommandContext(): ExtensionCommandContext {
|
|
1102
1214
|
return {
|
|
1103
1215
|
...this.createContext(),
|
|
@@ -1553,8 +1665,9 @@ export class ExtensionRunner {
|
|
|
1553
1665
|
return currentPayload;
|
|
1554
1666
|
}
|
|
1555
1667
|
|
|
1556
|
-
|
|
1557
|
-
|
|
1668
|
+
/** Runs response hooks with the model that produced that provider response. */
|
|
1669
|
+
async emitAfterProviderResponse(response: ProviderResponseMetadata, model?: Model): Promise<void> {
|
|
1670
|
+
const ctx = this.createContext(model);
|
|
1558
1671
|
|
|
1559
1672
|
for (const ext of this.extensions) {
|
|
1560
1673
|
const handlers = ext.handlers.get("after_provider_response");
|
|
@@ -76,6 +76,7 @@ import type {
|
|
|
76
76
|
WriteToolInput,
|
|
77
77
|
} from "../../tools";
|
|
78
78
|
import type { ApprovalMode } from "../../tools/approval";
|
|
79
|
+
import type { FileDeleteFallbackHandler, FileWriteFallbackHandler } from "../../tools/file-write-fallback";
|
|
79
80
|
import type { EventBus } from "../../utils/event-bus";
|
|
80
81
|
import type {
|
|
81
82
|
AgentEndEvent,
|
|
@@ -1256,6 +1257,63 @@ export interface ExtensionAPI {
|
|
|
1256
1257
|
/** Register a tool that the LLM can call. */
|
|
1257
1258
|
registerTool<TParams extends TSchema = TSchema, TDetails = unknown>(tool: ToolDefinition<TParams, TDetails>): void;
|
|
1258
1259
|
|
|
1260
|
+
/**
|
|
1261
|
+
* Register a fallback writer consulted when a native `write`/`edit` byte-write is
|
|
1262
|
+
* denied with a permission error (`EPERM`/`EACCES`/`EROFS`). Every other write
|
|
1263
|
+
* error is unaffected. Handlers run in registration order; the first one to
|
|
1264
|
+
* resolve `true` counts as the bytes being durably on disk, and the native tool
|
|
1265
|
+
* continues as if its own write had succeeded — including recording its file
|
|
1266
|
+
* snapshot under the real destination path, so a later hashline `edit` on that
|
|
1267
|
+
* path keeps working. Intended for a host embedding the agent inside a sandbox
|
|
1268
|
+
* that denies direct filesystem writes but exposes a privileged write channel.
|
|
1269
|
+
*
|
|
1270
|
+
* A denial that `Bun.write` masks as `ENOENT` — a write into a directory the host
|
|
1271
|
+
* may not create — also diverts here, with `req.dst`'s parent absent and the
|
|
1272
|
+
* handler responsible for creating it.
|
|
1273
|
+
*
|
|
1274
|
+
* `req.dst` is symlink-RESOLVED: the path the failed write itself acted on, not
|
|
1275
|
+
* the one the tool was given. A link anywhere in a lexical path redirects the
|
|
1276
|
+
* bytes while still passing a prefix allowlist, so treat `req.dst` as
|
|
1277
|
+
* authoritative. A destination that cannot be resolved is never brokered.
|
|
1278
|
+
*
|
|
1279
|
+
* Call this during extension load, like the other `register*` methods: handlers
|
|
1280
|
+
* are installed when the runner initializes, so an extension that has registered
|
|
1281
|
+
* none by then is skipped and a first registration made later never takes effect.
|
|
1282
|
+
*
|
|
1283
|
+
* The underlying registry is process-wide, so a handler may be consulted for a
|
|
1284
|
+
* denied write from any session in the process, not only its own.
|
|
1285
|
+
* `req.sessionId` names the session that issued the write and
|
|
1286
|
+
* `ctx.sessionManager.getSessionId()` names the handler's own; compare them
|
|
1287
|
+
* before prompting, because `ctx.ui` belongs to the latter. See
|
|
1288
|
+
* `docs/extensions.md`.
|
|
1289
|
+
*/
|
|
1290
|
+
registerFileWriteFallback(handler: FileWriteFallbackHandler): void;
|
|
1291
|
+
|
|
1292
|
+
/**
|
|
1293
|
+
* Register a fallback deleter consulted when a native `edit`/`apply_patch` unlink is
|
|
1294
|
+
* denied with a permission error (`EPERM`/`EACCES`/`EROFS`). Covers `edit`'s `REM`,
|
|
1295
|
+
* the source side of a hashline `MV`, and `apply_patch`'s delete op. Return `true`
|
|
1296
|
+
* once `dst` is gone from disk.
|
|
1297
|
+
*
|
|
1298
|
+
* A handler MUST remove `dst` with a plain unlink and MUST NOT fall back to a
|
|
1299
|
+
* recursive removal. `unlink` on a directory reports `EPERM` on Darwin, so the seam
|
|
1300
|
+
* checks the target before diverting — but when the target's own metadata is behind
|
|
1301
|
+
* the same boundary that denied the unlink, which is the common sandbox case, that
|
|
1302
|
+
* check cannot be resolved and `dst` may be a directory. `req.confirmedFile` says
|
|
1303
|
+
* which situation the handler is in.
|
|
1304
|
+
*
|
|
1305
|
+
* `req.dst` resolves every component ABOVE the last, for the same reason the
|
|
1306
|
+
* write seam resolves all of them; the last is left alone because `unlink`
|
|
1307
|
+
* removes a link rather than its target, so `req.dst` may name a link.
|
|
1308
|
+
*
|
|
1309
|
+
* Separate from {@link registerFileWriteFallback} on purpose. A write handler
|
|
1310
|
+
* brokers `req.content` to `req.dst`, so a delete request reaching it with no
|
|
1311
|
+
* content invites brokering an empty write and truncating the file instead of
|
|
1312
|
+
* removing it. Registering for deletes is therefore an explicit opt-in, and the
|
|
1313
|
+
* same load-time and process-wide notes above apply.
|
|
1314
|
+
*/
|
|
1315
|
+
registerFileDeleteFallback(handler: FileDeleteFallbackHandler): void;
|
|
1316
|
+
|
|
1259
1317
|
// =========================================================================
|
|
1260
1318
|
// Command, Shortcut, Flag Registration
|
|
1261
1319
|
// =========================================================================
|
|
@@ -1632,6 +1690,8 @@ export interface Extension {
|
|
|
1632
1690
|
tools: Map<string, RegisteredTool<any, any>>;
|
|
1633
1691
|
toolRegistrationListeners?: Set<ToolRegistrationListener>;
|
|
1634
1692
|
assistantThinkingRenderers: AssistantThinkingRenderer[];
|
|
1693
|
+
fileWriteFallbackHandlers: FileWriteFallbackHandler[];
|
|
1694
|
+
fileDeleteFallbackHandlers: FileDeleteFallbackHandler[];
|
|
1635
1695
|
messageRenderers: Map<string, MessageRenderer>;
|
|
1636
1696
|
commands: Map<string, RegisteredCommand>;
|
|
1637
1697
|
flags: Map<string, ExtensionFlag>;
|
|
@@ -14,6 +14,7 @@ import type { Settings } from "../../config/settings";
|
|
|
14
14
|
import type { Theme } from "../../modes/theme/theme";
|
|
15
15
|
import { type ApprovalMode, formatApprovalPrompt, resolveApproval, truncateForPrompt } from "../../tools/approval";
|
|
16
16
|
import { defaultLoadModeForToolName } from "../../tools/essential-tools";
|
|
17
|
+
import { withFileMutationSession } from "../../tools/file-write-fallback";
|
|
17
18
|
import { normalizeToolEventInput, resolveToolEventInput } from "../tool-event-input";
|
|
18
19
|
import { applyToolProxy } from "../tool-proxy";
|
|
19
20
|
import type { ExtensionRunner } from "./runner";
|
|
@@ -349,7 +350,15 @@ export class ExtensionToolWrapper<TParameters extends TSchema = TSchema, TDetail
|
|
|
349
350
|
let executionError: Error | undefined;
|
|
350
351
|
|
|
351
352
|
try {
|
|
352
|
-
|
|
353
|
+
// A denied file write or delete inside this tool can be brokered to an
|
|
354
|
+
// extension handler, and that registry is PROCESS-WIDE — so the session is
|
|
355
|
+
// named here, the one place where every tool's execution and the runner
|
|
356
|
+
// that owns the handlers are both in scope (`sdk.ts` wraps the whole tool
|
|
357
|
+
// registry with this class whenever a runner exists). Inert with no
|
|
358
|
+
// fallback registered: no scope is entered.
|
|
359
|
+
result = await withFileMutationSession(this.runner.sessionId, () =>
|
|
360
|
+
this.tool.execute(toolCallId, effectiveParams, signal, onUpdate, context),
|
|
361
|
+
);
|
|
353
362
|
} catch (err) {
|
|
354
363
|
executionError = err instanceof Error ? err : new Error(String(err));
|
|
355
364
|
result = {
|
|
@@ -1351,6 +1351,10 @@ async function findNodePackageRootUncached(packageName: string, importerPath: st
|
|
|
1351
1351
|
if (await pathExists(path.join(candidate, "package.json"))) {
|
|
1352
1352
|
return candidate;
|
|
1353
1353
|
}
|
|
1354
|
+
const workspaceMember = await findWorkspaceMemberPackageRoot(dir, packageName);
|
|
1355
|
+
if (workspaceMember) {
|
|
1356
|
+
return workspaceMember;
|
|
1357
|
+
}
|
|
1354
1358
|
const parent = path.dirname(dir);
|
|
1355
1359
|
if (parent === dir) {
|
|
1356
1360
|
return null;
|
|
@@ -1359,6 +1363,49 @@ async function findNodePackageRootUncached(packageName: string, importerPath: st
|
|
|
1359
1363
|
}
|
|
1360
1364
|
}
|
|
1361
1365
|
|
|
1366
|
+
/**
|
|
1367
|
+
* Resolve `packageName` as a workspace member when `dir` is a workspace root.
|
|
1368
|
+
*
|
|
1369
|
+
* An installed git dependency of a monorepo plugin contains the full
|
|
1370
|
+
* workspace tree but no node_modules links: `bun install` materializes a git
|
|
1371
|
+
* dependency's regular npm dependencies into the host tree and skips its
|
|
1372
|
+
* `workspace:*` / `file:` edges. Bare imports between workspace siblings
|
|
1373
|
+
* therefore never resolve through the node_modules walk above. When a
|
|
1374
|
+
* directory on that walk declares `workspaces` (array form or the yarn-style
|
|
1375
|
+
* `{ packages: [...] }` object), scan the member manifests for the requested
|
|
1376
|
+
* package name. node_modules candidates at the same level win, so an
|
|
1377
|
+
* explicitly installed copy still shadows the workspace member.
|
|
1378
|
+
*/
|
|
1379
|
+
async function findWorkspaceMemberPackageRoot(dir: string, packageName: string): Promise<string | null> {
|
|
1380
|
+
if (!(await pathExists(path.join(dir, "package.json")))) {
|
|
1381
|
+
return null;
|
|
1382
|
+
}
|
|
1383
|
+
const manifest = await readPackageManifest(dir);
|
|
1384
|
+
const rawWorkspaces = manifest?.workspaces;
|
|
1385
|
+
const patterns = Array.isArray(rawWorkspaces)
|
|
1386
|
+
? rawWorkspaces
|
|
1387
|
+
: isRecord(rawWorkspaces) && Array.isArray(rawWorkspaces.packages)
|
|
1388
|
+
? rawWorkspaces.packages
|
|
1389
|
+
: null;
|
|
1390
|
+
if (!patterns) {
|
|
1391
|
+
return null;
|
|
1392
|
+
}
|
|
1393
|
+
for (const pattern of patterns) {
|
|
1394
|
+
if (typeof pattern !== "string" || pattern.startsWith("!")) {
|
|
1395
|
+
continue;
|
|
1396
|
+
}
|
|
1397
|
+
const glob = new Bun.Glob(path.join(pattern, "package.json"));
|
|
1398
|
+
for await (const match of glob.scan({ cwd: dir, onlyFiles: true })) {
|
|
1399
|
+
const memberRoot = path.dirname(path.join(dir, match));
|
|
1400
|
+
const memberManifest = await readPackageManifest(memberRoot);
|
|
1401
|
+
if (memberManifest?.name === packageName) {
|
|
1402
|
+
return memberRoot;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return null;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1362
1409
|
async function readPackageManifest(packageRoot: string): Promise<Record<string, unknown> | null> {
|
|
1363
1410
|
const cached = packageManifestCache.get(packageRoot);
|
|
1364
1411
|
if (cached) return cached;
|
package/src/launch/client.ts
CHANGED
|
@@ -516,8 +516,14 @@ export async function closeDaemonClients(): Promise<void> {
|
|
|
516
516
|
|
|
517
517
|
/** Exercise worker-host broker startup and authenticated RPC for distribution smoke tests. */
|
|
518
518
|
export async function smokeTestDaemonBroker(): Promise<void> {
|
|
519
|
-
|
|
520
|
-
|
|
519
|
+
// Keep the broker's runtime dir under a private parent this process owns, so
|
|
520
|
+
// the broker's dead-scope sweep (pruneDeadDaemonRuntimeDirs, fired on startup)
|
|
521
|
+
// can only ever reclaim siblings inside it — never unrelated neighbours in
|
|
522
|
+
// os.tmpdir() such as tmux/ssh sockets or build trees (issue #8721).
|
|
523
|
+
const smokeRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-daemon-smoke-"));
|
|
524
|
+
const projectDir = path.join(smokeRoot, "project");
|
|
525
|
+
const runtimeDir = path.join(smokeRoot, "run");
|
|
526
|
+
await fs.mkdir(projectDir, { recursive: true });
|
|
521
527
|
const client = await createDaemonBrokerClient(projectDir, { runtimeDir, idleGraceMs: 5_000 });
|
|
522
528
|
try {
|
|
523
529
|
const ping = await client.request({ op: "ping" });
|
|
@@ -525,7 +531,6 @@ export async function smokeTestDaemonBroker(): Promise<void> {
|
|
|
525
531
|
await client.request({ op: "shutdown" });
|
|
526
532
|
} finally {
|
|
527
533
|
client.close();
|
|
528
|
-
await fs.rm(
|
|
529
|
-
await fs.rm(runtimeDir, { recursive: true, force: true });
|
|
534
|
+
await fs.rm(smokeRoot, { recursive: true, force: true });
|
|
530
535
|
}
|
|
531
536
|
}
|
package/src/launch/presence.ts
CHANGED
|
@@ -6,7 +6,19 @@ import { daemonRuntimeDir } from "./paths";
|
|
|
6
6
|
|
|
7
7
|
const CLIENTS_DIR = "clients";
|
|
8
8
|
const BROKER_PID_FILE = "broker.pid";
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Basename of the container holding per-project daemon scopes
|
|
11
|
+
* (`<state>/run/daemons`). {@link pruneDeadDaemonRuntimeDirs} refuses to sweep
|
|
12
|
+
* any other root so a runtime dir passed from outside the state tree cannot
|
|
13
|
+
* turn the reclaim into an rm -rf of unrelated neighbours (issue #8721).
|
|
14
|
+
*/
|
|
15
|
+
const DAEMONS_DIR = "daemons";
|
|
16
|
+
/**
|
|
17
|
+
* Name shape of a project daemon scope: the 16-hex wyhash of the project dir
|
|
18
|
+
* produced by `getDaemonRuntimeDir`. Only entries matching this are pruned,
|
|
19
|
+
* which excludes the machine-global `global` container and any foreign dir.
|
|
20
|
+
*/
|
|
21
|
+
const DAEMON_SCOPE_KEY = /^[0-9a-f]{16}$/;
|
|
10
22
|
/**
|
|
11
23
|
* Grace before a dead daemon runtime dir becomes prune-eligible. Guards against
|
|
12
24
|
* deleting a scope whose owning omp process is mid-startup (token written, broker
|
|
@@ -118,11 +130,14 @@ async function hasLiveDaemonBroker(runtimeDir: string): Promise<boolean> {
|
|
|
118
130
|
* Best-effort and non-throwing: a scope is deleted only when its `broker.pid`
|
|
119
131
|
* is absent/dead, no live client presence remains, and it has been untouched
|
|
120
132
|
* for {@link DAEMON_RUNTIME_STALE_GRACE_MS}. The caller's own `currentRuntimeDir`
|
|
121
|
-
* and the
|
|
133
|
+
* is always skipped, and the sweep runs only inside the {@link DAEMONS_DIR}
|
|
134
|
+
* container over entries named like a {@link DAEMON_SCOPE_KEY} — so a runtime
|
|
135
|
+
* dir relocated elsewhere (e.g. the smoke test under `os.tmpdir()`) never
|
|
136
|
+
* reclaims unrelated neighbours (issue #8721).
|
|
122
137
|
*/
|
|
123
138
|
export async function pruneDeadDaemonRuntimeDirs(currentRuntimeDir: string): Promise<void> {
|
|
124
139
|
const root = path.dirname(currentRuntimeDir);
|
|
125
|
-
if (path.basename(root)
|
|
140
|
+
if (path.basename(root) !== DAEMONS_DIR) return;
|
|
126
141
|
const current = path.resolve(currentRuntimeDir);
|
|
127
142
|
let entries: Dirent[];
|
|
128
143
|
try {
|
|
@@ -138,7 +153,7 @@ export async function pruneDeadDaemonRuntimeDirs(currentRuntimeDir: string): Pro
|
|
|
138
153
|
}
|
|
139
154
|
const now = Date.now();
|
|
140
155
|
for (const entry of entries) {
|
|
141
|
-
if (!entry.isDirectory() || entry.name
|
|
156
|
+
if (!entry.isDirectory() || !DAEMON_SCOPE_KEY.test(entry.name)) continue;
|
|
142
157
|
const dir = path.join(root, entry.name);
|
|
143
158
|
if (path.resolve(dir) === current) continue;
|
|
144
159
|
try {
|
package/src/lsp/defaults.json
CHANGED
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"biome": {
|
|
70
70
|
"command": "biome",
|
|
71
71
|
"args": ["lsp-proxy"],
|
|
72
|
-
"fileTypes": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc"],
|
|
72
|
+
"fileTypes": [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".json", ".jsonc", ".css"],
|
|
73
73
|
"rootMarkers": ["biome.json", "biome.jsonc"],
|
|
74
74
|
"isLinter": true
|
|
75
75
|
},
|
package/src/lsp/writethrough.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import { isEnoent, logger, once, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import type { BunFile } from "bun";
|
|
4
|
+
import { isPermissionDeniedError, writeFileWithFallback } from "../tools/file-write-fallback";
|
|
4
5
|
import { FileChangeType, notifyWorkspaceWatchedFiles } from "./client";
|
|
5
6
|
import { getServersForFile } from "./config";
|
|
6
7
|
import {
|
|
@@ -66,11 +67,7 @@ export async function writethroughNoop(
|
|
|
66
67
|
_batch?: LspWritethroughBatchRequest,
|
|
67
68
|
_getDeferred?: (dst: string) => WritethroughDeferredHandle | undefined,
|
|
68
69
|
): Promise<FileDiagnosticsResult | undefined> {
|
|
69
|
-
|
|
70
|
-
await file.write(content);
|
|
71
|
-
} else {
|
|
72
|
-
await Bun.write(dst, content);
|
|
73
|
-
}
|
|
70
|
+
await writeFileWithFallback(dst, content, file);
|
|
74
71
|
return undefined;
|
|
75
72
|
}
|
|
76
73
|
|
|
@@ -78,6 +75,12 @@ interface PendingWritethrough {
|
|
|
78
75
|
dst: string;
|
|
79
76
|
file?: BunFile;
|
|
80
77
|
changeType: FileChangeType;
|
|
78
|
+
/**
|
|
79
|
+
* The bytes this entry committed. The flush prefers a fresh read of `dst` so
|
|
80
|
+
* post-processing sees whatever else in the batch touched the file, and falls
|
|
81
|
+
* back to these when that read is denied.
|
|
82
|
+
*/
|
|
83
|
+
content: string;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
interface RunLspWritethroughOptions {
|
|
@@ -288,7 +291,7 @@ async function runLspWritethrough(
|
|
|
288
291
|
const contentAlreadyWritten = runOptions?.contentAlreadyWritten ?? false;
|
|
289
292
|
|
|
290
293
|
let finalContent = content;
|
|
291
|
-
const writeContent = async (value: string) => (
|
|
294
|
+
const writeContent = async (value: string) => writeFileWithFallback(dst, value, file);
|
|
292
295
|
const getWritePromise = once(() =>
|
|
293
296
|
contentAlreadyWritten && finalContent === content ? Promise.resolve() : writeContent(finalContent),
|
|
294
297
|
);
|
|
@@ -458,9 +461,16 @@ async function flushWritethroughBatch(
|
|
|
458
461
|
try {
|
|
459
462
|
content = await fs.promises.readFile(entry.dst, "utf8");
|
|
460
463
|
} catch (error) {
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
+
if (isEnoent(error)) {
|
|
465
|
+
bundle?.finalize(undefined);
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
// A brokered write lands bytes this process may not be able to read
|
|
469
|
+
// back: a sandbox that denies the write commonly denies the read too.
|
|
470
|
+
// Failing here would fail a flush whose every write succeeded, so the
|
|
471
|
+
// content this entry committed stands in for the unreadable file.
|
|
472
|
+
if (!isPermissionDeniedError(error)) throw error;
|
|
473
|
+
content = entry.content;
|
|
464
474
|
}
|
|
465
475
|
const deferredInner =
|
|
466
476
|
bundle &&
|
|
@@ -552,7 +562,7 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
|
|
552
562
|
}
|
|
553
563
|
|
|
554
564
|
const state = getOrCreateWritethroughBatch(batch.id, resolvedOptions);
|
|
555
|
-
state.entries.set(dst, { dst, file, changeType });
|
|
565
|
+
state.entries.set(dst, { dst, file, changeType, content });
|
|
556
566
|
if (!batch.flush) return undefined;
|
|
557
567
|
|
|
558
568
|
writethroughBatches.delete(batch.id);
|
package/src/mcp/manager.ts
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import * as path from "node:path";
|
|
8
8
|
import * as url from "node:url";
|
|
9
9
|
import { isDefinitiveOAuthFailure, type TSchema } from "@oh-my-pi/pi-ai";
|
|
10
|
+
import type { OAuthCredentials } from "@oh-my-pi/pi-ai/oauth/types";
|
|
10
11
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
11
12
|
import type { SourceMeta } from "../capability/types";
|
|
12
13
|
import { resolveConfigValue } from "../config/resolve-config-value";
|
|
@@ -30,9 +31,10 @@ import { type LoadMCPConfigsResult, loadAllMCPConfigs, validateServerConfig } fr
|
|
|
30
31
|
import {
|
|
31
32
|
lookupMcpOAuthCredential,
|
|
32
33
|
type MCPOAuthCredentialLookup,
|
|
34
|
+
refreshManagedMcpOAuthCredential,
|
|
33
35
|
selectMcpOAuthRefreshMaterial,
|
|
34
36
|
} from "./oauth-credentials";
|
|
35
|
-
import {
|
|
37
|
+
import type { MCPStoredOAuthCredential } from "./oauth-flow";
|
|
36
38
|
import type { McpConnectionStatusEvent } from "./startup-events";
|
|
37
39
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
38
40
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
@@ -1404,6 +1406,36 @@ export class MCPManager {
|
|
|
1404
1406
|
};
|
|
1405
1407
|
}
|
|
1406
1408
|
|
|
1409
|
+
/**
|
|
1410
|
+
* Refresh a broker-redacted MCP OAuth credential through the auth-broker.
|
|
1411
|
+
*
|
|
1412
|
+
* When running in broker mode the client only ever holds the redacted
|
|
1413
|
+
* refresh sentinel; the real refresh token lives on the broker. Delegating
|
|
1414
|
+
* to {@link AuthStorage.forceRefreshCredentialById} makes the broker run the
|
|
1415
|
+
* `refresh_token` grant and return a fresh access token, which the client
|
|
1416
|
+
* uses while keeping {@link REMOTE_REFRESH_SENTINEL} in the refresh slot.
|
|
1417
|
+
*/
|
|
1418
|
+
async #refreshBrokeredMcpCredential(credentialId: string, signal?: AbortSignal): Promise<OAuthCredentials> {
|
|
1419
|
+
const storage = this.#authStorage;
|
|
1420
|
+
if (!storage) throw new Error("MCP OAuth broker refresh requires an auth storage");
|
|
1421
|
+
const row = storage.listStoredCredentials(credentialId).find(entry => entry.credential.type === "oauth");
|
|
1422
|
+
if (!row) throw new Error(`No broker credential row for ${credentialId}`);
|
|
1423
|
+
const entry = await storage.forceRefreshCredentialById(row.id, signal);
|
|
1424
|
+
if (entry.credential.type !== "oauth") {
|
|
1425
|
+
throw new Error(`Broker returned non-OAuth credential for ${credentialId}`);
|
|
1426
|
+
}
|
|
1427
|
+
const refreshed = entry.credential;
|
|
1428
|
+
return {
|
|
1429
|
+
access: refreshed.access,
|
|
1430
|
+
refresh: REMOTE_REFRESH_SENTINEL,
|
|
1431
|
+
expires: refreshed.expires,
|
|
1432
|
+
accountId: refreshed.accountId,
|
|
1433
|
+
email: refreshed.email,
|
|
1434
|
+
projectId: refreshed.projectId,
|
|
1435
|
+
enterpriseUrl: refreshed.enterpriseUrl,
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1407
1439
|
/**
|
|
1408
1440
|
* Resolve OAuth credentials and shell commands in config.
|
|
1409
1441
|
* `oauth: false` skips credential injection (reauth's unauthenticated probe);
|
|
@@ -1435,24 +1467,15 @@ export class MCPManager {
|
|
|
1435
1467
|
return Boolean(current.refresh && material?.tokenUrl);
|
|
1436
1468
|
},
|
|
1437
1469
|
refresh: (current, signal) => {
|
|
1470
|
+
// Broker-backed credentials redact the refresh token
|
|
1471
|
+
// (REMOTE_REFRESH_SENTINEL); the broker holds the real one, so
|
|
1472
|
+
// route the refresh through it instead of failing locally.
|
|
1438
1473
|
if (current.refresh === REMOTE_REFRESH_SENTINEL) {
|
|
1439
|
-
|
|
1474
|
+
return this.#refreshBrokeredMcpCredential(credentialId, signal);
|
|
1440
1475
|
}
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
throw new Error("MCP OAuth credential is missing refresh material");
|
|
1445
|
-
}
|
|
1446
|
-
const clientId = material?.clientId;
|
|
1447
|
-
const clientSecret = material?.clientSecret;
|
|
1448
|
-
const authorizationUrl =
|
|
1449
|
-
material && "authorizationUrl" in material ? material.authorizationUrl : undefined;
|
|
1450
|
-
const resourceIsFallback =
|
|
1451
|
-
!material?.resource && (config.type === "http" || config.type === "sse") && Boolean(config.url);
|
|
1452
|
-
const resource = material?.resource ?? (resourceIsFallback ? config.url : undefined);
|
|
1453
|
-
return refreshMCPOAuthToken(tokenUrl, current.refresh, clientId, clientSecret, resource, {
|
|
1454
|
-
authorizationUrl,
|
|
1455
|
-
stripSameOriginResource: resourceIsFallback,
|
|
1476
|
+
return refreshManagedMcpOAuthCredential(current, {
|
|
1477
|
+
serverUrl: config.type === "http" || config.type === "sse" ? config.url : undefined,
|
|
1478
|
+
auth,
|
|
1456
1479
|
signal,
|
|
1457
1480
|
});
|
|
1458
1481
|
},
|
|
@@ -1480,10 +1503,8 @@ export class MCPManager {
|
|
|
1480
1503
|
isDefinitiveOAuthFailure(error instanceof Error ? error.message : String(error)),
|
|
1481
1504
|
disabledCause: error =>
|
|
1482
1505
|
`oauth refresh failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
1483
|
-
keepCredentialOnRefreshFailure:
|
|
1484
|
-
!(error instanceof Error && error.message.includes("broker-redacted")),
|
|
1506
|
+
keepCredentialOnRefreshFailure: true,
|
|
1485
1507
|
onRefreshFailure: refreshError => {
|
|
1486
|
-
if (refreshError instanceof Error && refreshError.message.includes("broker-redacted")) return;
|
|
1487
1508
|
logger.warn("MCP OAuth refresh failed, using existing token", {
|
|
1488
1509
|
credentialId,
|
|
1489
1510
|
error: refreshError,
|