@oh-my-pi/pi-coding-agent 17.2.10 → 17.2.11
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 +40 -0
- package/dist/{CHANGELOG-rhwzpexa.md → CHANGELOG-d9xenpn9.md} +40 -0
- package/dist/cli.js +3025 -3016
- package/dist/types/capability/mcp.d.ts +11 -0
- package/dist/types/capability/skill.d.ts +6 -0
- package/dist/types/cli/command-help.d.ts +3 -0
- package/dist/types/commands/share.d.ts +25 -0
- package/dist/types/commit/agentic/index.d.ts +3 -1
- package/dist/types/commit/execute.d.ts +32 -0
- package/dist/types/commit/index.d.ts +2 -1
- package/dist/types/commit/pipeline.d.ts +7 -1
- package/dist/types/config/model-registry.d.ts +4 -0
- package/dist/types/config/settings-schema.d.ts +1 -34
- package/dist/types/discovery/agent-plugin-format.d.ts +143 -0
- package/dist/types/discovery/agent-plugins.d.ts +1 -0
- package/dist/types/discovery/contained-path.d.ts +34 -0
- package/dist/types/discovery/index.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
- package/dist/types/extensibility/extensions/loader.d.ts +7 -0
- package/dist/types/extensibility/extensions/types.d.ts +11 -0
- package/dist/types/extensibility/shared-events.d.ts +3 -2
- package/dist/types/extensibility/skills.d.ts +5 -0
- package/dist/types/mcp/transports/header-policy.d.ts +45 -0
- package/dist/types/mcp/types.d.ts +15 -0
- package/dist/types/modes/components/agent-hub.d.ts +2 -0
- package/dist/types/modes/components/agent-transcript-viewer.d.ts +2 -0
- package/dist/types/modes/components/chat-transcript-builder.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +1 -2
- package/dist/types/modes/components/status-line/types.d.ts +2 -0
- package/dist/types/modes/components/tool-execution.d.ts +2 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +3 -2
- package/dist/types/secrets/index.d.ts +23 -1
- package/dist/types/session/agent-session-events.d.ts +2 -2
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/messages.d.ts +1 -0
- package/dist/types/session/prewalk.d.ts +3 -1
- package/dist/types/session/session-tools.d.ts +8 -0
- package/dist/types/session/turn-recovery.d.ts +7 -2
- package/dist/types/task/index.d.ts +2 -0
- package/dist/types/tools/shell-tokenize.d.ts +16 -0
- package/package.json +13 -13
- package/scripts/legacy-pi-virtual-module.ts +4 -1
- package/src/capability/mcp.ts +11 -0
- package/src/capability/skill.ts +6 -0
- package/src/cli/command-help.ts +4 -0
- package/src/cli/gallery-cli.ts +1 -1
- package/src/cli-commands.ts +5 -0
- package/src/commands/commit.ts +16 -3
- package/src/commands/share.ts +71 -0
- package/src/commit/agentic/index.ts +39 -21
- package/src/commit/execute.ts +56 -0
- package/src/commit/index.ts +2 -1
- package/src/commit/pipeline.ts +20 -7
- package/src/config/model-discovery.ts +1 -1
- package/src/config/model-registry.ts +21 -1
- package/src/config/settings-schema.ts +2 -33
- package/src/config/settings.ts +46 -0
- package/src/discovery/agent-plugin-format.ts +551 -0
- package/src/discovery/agent-plugins.ts +341 -0
- package/src/discovery/claude-plugins.ts +21 -5
- package/src/discovery/contained-path.ts +78 -0
- package/src/discovery/helpers.ts +23 -9
- package/src/discovery/index.ts +1 -0
- package/src/discovery/omp-plugins.ts +20 -7
- package/src/exec/non-interactive-env.ts +1 -0
- package/src/extensibility/custom-tools/types.ts +2 -2
- package/src/extensibility/extensions/loader.ts +78 -14
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +12 -0
- package/src/extensibility/plugins/marketplace/manager.ts +2 -1
- package/src/extensibility/shared-events.ts +3 -2
- package/src/extensibility/skills.ts +9 -0
- package/src/internal-urls/skill-protocol.ts +14 -0
- package/src/internal-urls/vault-protocol.ts +2 -2
- package/src/launch/client.ts +11 -1
- package/src/launch/protocol.ts +7 -2
- package/src/main.ts +3 -2
- package/src/mcp/config.ts +3 -0
- package/src/mcp/manager.ts +12 -9
- package/src/mcp/transports/header-policy.ts +95 -0
- package/src/mcp/transports/http.ts +35 -52
- package/src/mcp/transports/sse.ts +20 -27
- package/src/mcp/types.ts +15 -0
- package/src/modes/acp/acp-agent.ts +15 -4
- package/src/modes/components/agent-dashboard.ts +2 -0
- package/src/modes/components/agent-hub.ts +5 -0
- package/src/modes/components/agent-transcript-viewer.ts +3 -0
- package/src/modes/components/chat-transcript-builder.ts +3 -0
- package/src/modes/components/custom-editor.ts +0 -9
- package/src/modes/components/status-line/component.ts +1 -0
- package/src/modes/components/status-line/segments.ts +6 -4
- package/src/modes/components/status-line/types.ts +2 -0
- package/src/modes/components/tool-execution.ts +13 -16
- package/src/modes/components/tree-selector.ts +62 -3
- package/src/modes/controllers/command-controller.ts +4 -1
- package/src/modes/controllers/event-controller.ts +15 -15
- package/src/modes/controllers/input-controller.ts +20 -2
- package/src/modes/controllers/selector-controller.ts +1 -0
- package/src/modes/utils/transcript-render-helpers.ts +4 -2
- package/src/modes/utils/ui-helpers.ts +1 -0
- package/src/sdk.ts +6 -49
- package/src/secrets/index.ts +52 -1
- package/src/session/agent-session-events.ts +2 -2
- package/src/session/agent-session.ts +33 -14
- package/src/session/messages.ts +1 -0
- package/src/session/prewalk.ts +57 -17
- package/src/session/session-context.ts +3 -5
- package/src/session/session-handoff.ts +5 -1
- package/src/session/session-tools.ts +37 -3
- package/src/session/todo-tracker.ts +11 -1
- package/src/session/turn-recovery.ts +94 -72
- package/src/slash-commands/builtin-registry.ts +12 -8
- package/src/task/index.ts +21 -6
- package/src/tools/bash-skill-urls.ts +15 -0
- package/src/tools/bash.ts +10 -11
- package/src/tools/debug.ts +1 -1
- package/src/tools/jtd-to-json-schema.ts +123 -21
- package/src/tools/read.ts +5 -4
- package/src/tools/shell-tokenize.ts +98 -0
- package/src/web/search/providers/exa.ts +1 -1
|
@@ -73,6 +73,15 @@ export class ExtensionRuntime implements IExtensionRuntime {
|
|
|
73
73
|
flagValues = new Map<string, boolean | string>();
|
|
74
74
|
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; sourceId: string }> = [];
|
|
75
75
|
|
|
76
|
+
registerProvider(name: string, config: ProviderConfig, sourceId: string): void {
|
|
77
|
+
this.pendingProviderRegistrations.push({ name, config, sourceId });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
unregisterProvider(name: string): void {
|
|
81
|
+
const remaining = this.pendingProviderRegistrations.filter(registration => registration.name !== name);
|
|
82
|
+
this.pendingProviderRegistrations.splice(0, this.pendingProviderRegistrations.length, ...remaining);
|
|
83
|
+
}
|
|
84
|
+
|
|
76
85
|
sendMessage(): void {
|
|
77
86
|
throw new ExtensionRuntimeNotInitializedError();
|
|
78
87
|
}
|
|
@@ -290,7 +299,11 @@ class ConcreteExtensionAPI implements ExtensionAPI, IExtensionRuntime {
|
|
|
290
299
|
}
|
|
291
300
|
|
|
292
301
|
registerProvider(name: string, config: ProviderConfig): void {
|
|
293
|
-
this.runtime.
|
|
302
|
+
this.runtime.registerProvider(name, config, this.extension.path);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
unregisterProvider(name: string): void {
|
|
306
|
+
this.runtime.unregisterProvider(name, this.extension.path);
|
|
294
307
|
}
|
|
295
308
|
}
|
|
296
309
|
|
|
@@ -311,12 +324,37 @@ function createExtension(extensionPath: string, resolvedPath: string): Extension
|
|
|
311
324
|
};
|
|
312
325
|
}
|
|
313
326
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
327
|
+
/**
|
|
328
|
+
* Runs an extension factory with provider registration rollback on failure.
|
|
329
|
+
* Restores the complete registration queue when the factory throws because an
|
|
330
|
+
* extension may unregister entries queued by an earlier extension.
|
|
331
|
+
*/
|
|
332
|
+
async function runExtensionFactory(
|
|
333
|
+
factory: ExtensionFactory,
|
|
334
|
+
api: ExtensionAPI,
|
|
318
335
|
runtime: IExtensionRuntime,
|
|
319
|
-
): Promise<
|
|
336
|
+
): Promise<void> {
|
|
337
|
+
const providerRegistrationCheckpoint = [...runtime.pendingProviderRegistrations];
|
|
338
|
+
|
|
339
|
+
try {
|
|
340
|
+
await factory(api);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
runtime.pendingProviderRegistrations.splice(
|
|
343
|
+
0,
|
|
344
|
+
runtime.pendingProviderRegistrations.length,
|
|
345
|
+
...providerRegistrationCheckpoint,
|
|
346
|
+
);
|
|
347
|
+
throw error;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
interface ImportedExtensionModule {
|
|
352
|
+
factory: ExtensionFactory | null;
|
|
353
|
+
resolvedPath: string;
|
|
354
|
+
error: string | null;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
async function importExtensionModule(extensionPath: string, cwd: string): Promise<ImportedExtensionModule> {
|
|
320
358
|
const resolvedPath = resolvePath(extensionPath, cwd);
|
|
321
359
|
try {
|
|
322
360
|
const module = (await withHostGuard(() => loadLegacyPiModule(resolvedPath))) as LoadedExtensionModule;
|
|
@@ -324,16 +362,34 @@ async function loadExtension(
|
|
|
324
362
|
|
|
325
363
|
if (typeof factory !== "function") {
|
|
326
364
|
return {
|
|
327
|
-
|
|
365
|
+
factory: null,
|
|
366
|
+
resolvedPath,
|
|
328
367
|
error: `Extension does not export a valid factory function: ${extensionPath}`,
|
|
329
368
|
};
|
|
330
369
|
}
|
|
331
370
|
|
|
332
|
-
|
|
371
|
+
return { factory, resolvedPath, error: null };
|
|
372
|
+
} catch (err) {
|
|
373
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
374
|
+
return { factory: null, resolvedPath, error: `Failed to load extension: ${message}` };
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function bindExtension(
|
|
379
|
+
extensionPath: string,
|
|
380
|
+
imported: ImportedExtensionModule,
|
|
381
|
+
cwd: string,
|
|
382
|
+
eventBus: EventBus,
|
|
383
|
+
runtime: IExtensionRuntime,
|
|
384
|
+
): Promise<{ extension: Extension | null; error: string | null }> {
|
|
385
|
+
const factory = imported.factory;
|
|
386
|
+
if (imported.error !== null || factory === null) {
|
|
387
|
+
return { extension: null, error: imported.error };
|
|
388
|
+
}
|
|
389
|
+
try {
|
|
390
|
+
const extension = createExtension(extensionPath, imported.resolvedPath);
|
|
333
391
|
const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
|
|
334
|
-
await withHostGuard(
|
|
335
|
-
await factory(api);
|
|
336
|
-
});
|
|
392
|
+
await withHostGuard(() => runExtensionFactory(factory, api, runtime));
|
|
337
393
|
|
|
338
394
|
return { extension, error: null };
|
|
339
395
|
} catch (err) {
|
|
@@ -354,12 +410,17 @@ export async function loadExtensionFromFactory(
|
|
|
354
410
|
): Promise<Extension> {
|
|
355
411
|
const extension = createExtension(name, name);
|
|
356
412
|
const api = new ConcreteExtensionAPI(PiCodingAgent, extension, runtime, cwd, eventBus);
|
|
357
|
-
await factory
|
|
413
|
+
await runExtensionFactory(factory, api, runtime);
|
|
358
414
|
return extension;
|
|
359
415
|
}
|
|
360
416
|
|
|
361
417
|
/**
|
|
362
418
|
* Load extensions from paths.
|
|
419
|
+
*
|
|
420
|
+
* Module import (the dominant cold-start cost — file I/O plus module
|
|
421
|
+
* evaluation) runs concurrently across extensions; factory binding then runs
|
|
422
|
+
* sequentially in the original path order, so registration semantics
|
|
423
|
+
* (last-wins collisions, shared runtime flag defaults) stay deterministic.
|
|
363
424
|
*/
|
|
364
425
|
export async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {
|
|
365
426
|
const extensions: Extension[] = [];
|
|
@@ -367,8 +428,11 @@ export async function loadExtensions(paths: string[], cwd: string, eventBus?: Ev
|
|
|
367
428
|
const resolvedEventBus = eventBus ?? new EventBus();
|
|
368
429
|
const runtime = new ExtensionRuntime();
|
|
369
430
|
|
|
370
|
-
|
|
371
|
-
|
|
431
|
+
const imported = await Promise.all(paths.map(extPath => importExtensionModule(extPath, cwd)));
|
|
432
|
+
|
|
433
|
+
for (let i = 0; i < paths.length; i++) {
|
|
434
|
+
const extPath = paths[i]!;
|
|
435
|
+
const { extension, error } = await bindExtension(extPath, imported[i]!, cwd, resolvedEventBus, runtime);
|
|
372
436
|
|
|
373
437
|
if (error) {
|
|
374
438
|
errors.push({ path: extPath, error });
|
|
@@ -530,6 +530,12 @@ export class ExtensionRunner {
|
|
|
530
530
|
this.runtime.setServiceTier = actions.setServiceTier ?? throwUnsupportedServiceTierAction;
|
|
531
531
|
this.runtime.getSessionName = actions.getSessionName;
|
|
532
532
|
this.runtime.setSessionName = actions.setSessionName;
|
|
533
|
+
this.runtime.registerProvider = (name, config, sourceId) => {
|
|
534
|
+
this.modelRegistry.registerProvider(name, config, sourceId);
|
|
535
|
+
};
|
|
536
|
+
this.runtime.unregisterProvider = name => {
|
|
537
|
+
this.modelRegistry.unregisterProvider(name);
|
|
538
|
+
};
|
|
533
539
|
|
|
534
540
|
// Context actions (required)
|
|
535
541
|
this.#getModel = contextActions.getModel;
|
|
@@ -1373,6 +1373,14 @@ export interface ExtensionAPI {
|
|
|
1373
1373
|
*/
|
|
1374
1374
|
registerProvider(name: string, config: ProviderConfig): void;
|
|
1375
1375
|
|
|
1376
|
+
/**
|
|
1377
|
+
* Unregister a provider previously registered by an extension.
|
|
1378
|
+
*
|
|
1379
|
+
* Removes extension-provided models and restores overridden built-in models.
|
|
1380
|
+
* Has no effect when the provider is not registered.
|
|
1381
|
+
*/
|
|
1382
|
+
unregisterProvider(name: string): void;
|
|
1383
|
+
|
|
1376
1384
|
/** Shared event bus for extension communication. */
|
|
1377
1385
|
events: EventBus;
|
|
1378
1386
|
}
|
|
@@ -1516,6 +1524,10 @@ export interface ExtensionRuntimeState {
|
|
|
1516
1524
|
flagValues: Map<string, boolean | string>;
|
|
1517
1525
|
/** Provider registrations queued during extension loading, processed during session initialization */
|
|
1518
1526
|
pendingProviderRegistrations: Array<{ name: string; config: ProviderConfig; sourceId: string }>;
|
|
1527
|
+
/** Queue a provider registration until initialization, then apply it immediately. */
|
|
1528
|
+
registerProvider(name: string, config: ProviderConfig, sourceId: string): void;
|
|
1529
|
+
/** Remove a queued or initialized provider registration. */
|
|
1530
|
+
unregisterProvider(name: string, sourceId: string): void;
|
|
1519
1531
|
}
|
|
1520
1532
|
|
|
1521
1533
|
/** Action implementations for ExtensionAPI methods. */
|
|
@@ -408,7 +408,7 @@ export class MarketplaceManager {
|
|
|
408
408
|
/**
|
|
409
409
|
* Resolve plugin version from multiple sources:
|
|
410
410
|
* 1. Catalog entry version (if set)
|
|
411
|
-
* 2. Plugin manifest (.claude-plugin/plugin.json or package.json)
|
|
411
|
+
* 2. Plugin manifest (.claude-plugin/plugin.json, Agent Plugins root plugin.json, or package.json)
|
|
412
412
|
* 3. Git SHA from source (truncated to 7 chars)
|
|
413
413
|
* 4. Fallback "0.0.0"
|
|
414
414
|
*/
|
|
@@ -419,6 +419,7 @@ export class MarketplaceManager {
|
|
|
419
419
|
// 2. Plugin manifest
|
|
420
420
|
for (const manifestPath of [
|
|
421
421
|
path.join(sourcePath, ".claude-plugin", "plugin.json"),
|
|
422
|
+
path.join(sourcePath, "plugin.json"),
|
|
422
423
|
path.join(sourcePath, "package.json"),
|
|
423
424
|
]) {
|
|
424
425
|
try {
|
|
@@ -249,7 +249,8 @@ export interface AutoRetryStartEvent {
|
|
|
249
249
|
errorId?: number;
|
|
250
250
|
}
|
|
251
251
|
|
|
252
|
-
|
|
252
|
+
/** Persisted retry error whose transcript presentation changed when the retry saga settled. */
|
|
253
|
+
export interface RetryErrorUpdate {
|
|
253
254
|
entryId: string;
|
|
254
255
|
persistenceKey?: string;
|
|
255
256
|
note: string;
|
|
@@ -262,7 +263,7 @@ export interface AutoRetryEndEvent {
|
|
|
262
263
|
success: boolean;
|
|
263
264
|
attempt: number;
|
|
264
265
|
finalError?: string;
|
|
265
|
-
|
|
266
|
+
retryErrors?: RetryErrorUpdate[];
|
|
266
267
|
}
|
|
267
268
|
|
|
268
269
|
// ============================================================================
|
|
@@ -27,6 +27,11 @@ export interface Skill {
|
|
|
27
27
|
* prompt's `<skills>` listing.
|
|
28
28
|
*/
|
|
29
29
|
hide?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Filesystem-resolved plugin root for Agent Plugin skills (spec §4.1):
|
|
32
|
+
* every `skill://` resource access must realpath-resolve within it.
|
|
33
|
+
*/
|
|
34
|
+
containRoot?: string;
|
|
30
35
|
/** Source metadata for display */
|
|
31
36
|
_source?: SourceMeta;
|
|
32
37
|
}
|
|
@@ -104,6 +109,7 @@ export async function loadSkillsFromDir(options: LoadSkillsFromDirOptions): Prom
|
|
|
104
109
|
filePath: capSkill.path,
|
|
105
110
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
106
111
|
source: options.source,
|
|
112
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
107
113
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
108
114
|
_source: capSkill._source,
|
|
109
115
|
})),
|
|
@@ -239,6 +245,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
239
245
|
filePath: capSkill.path,
|
|
240
246
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
241
247
|
source: `${capSkill._source.provider}:${capSkill.level}`,
|
|
248
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
242
249
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
243
250
|
_source: capSkill._source,
|
|
244
251
|
});
|
|
@@ -276,6 +283,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
276
283
|
filePath: capSkill.path,
|
|
277
284
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
278
285
|
source: "custom:user",
|
|
286
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
279
287
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
280
288
|
_source: { ...capSkill._source, providerName: "Custom" },
|
|
281
289
|
},
|
|
@@ -376,6 +384,7 @@ export async function loadSkills(options: LoadSkillsOptions = {}): Promise<LoadS
|
|
|
376
384
|
filePath: capSkill.path,
|
|
377
385
|
baseDir: capSkill.path.replace(/[\\/]SKILL\.md$/, ""),
|
|
378
386
|
source: `${capSkill._source.provider}:${capSkill.level}`,
|
|
387
|
+
...(capSkill.containRoot !== undefined && { containRoot: capSkill.containRoot }),
|
|
379
388
|
hide: capSkill.frontmatter?.hide === true || capSkill.frontmatter?.disableModelInvocation === true,
|
|
380
389
|
_source: capSkill._source,
|
|
381
390
|
});
|
|
@@ -11,6 +11,7 @@ import type * as fsTypes from "node:fs";
|
|
|
11
11
|
import * as fs from "node:fs/promises";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { isEnoent } from "@oh-my-pi/pi-utils";
|
|
14
|
+
import { resolveContainedPath } from "../discovery/contained-path";
|
|
14
15
|
import { getActiveSkills } from "../extensibility/skills";
|
|
15
16
|
import { isMarkdownPath } from "../utils/lang-from-path";
|
|
16
17
|
import { buildDirectoryResource } from "./filesystem-resource";
|
|
@@ -76,6 +77,19 @@ export class SkillProtocolHandler implements ProtocolHandler {
|
|
|
76
77
|
if (!resolvedPath.startsWith(resolvedBaseDir + path.sep) && resolvedPath !== resolvedBaseDir) {
|
|
77
78
|
throw new Error("Path traversal is not allowed");
|
|
78
79
|
}
|
|
80
|
+
// Agent Plugin skills (§4.1): the resource must canonically resolve
|
|
81
|
+
// within the plugin root; a dangling or unresolvable path fails closed.
|
|
82
|
+
// Symlinks may target other files inside the same package.
|
|
83
|
+
if (skill.containRoot) {
|
|
84
|
+
const contained = await resolveContainedPath(skill.containRoot, resolvedPath);
|
|
85
|
+
if (contained.status === "outside") {
|
|
86
|
+
throw new Error(`skill:// path resolves outside the plugin root: ${url.href}`);
|
|
87
|
+
}
|
|
88
|
+
if (contained.status === "missing") {
|
|
89
|
+
throw new Error(`File not found: ${resolvedPath}`);
|
|
90
|
+
}
|
|
91
|
+
targetPath = contained.realPath;
|
|
92
|
+
}
|
|
79
93
|
} else {
|
|
80
94
|
targetPath = context?.pathOnly === true ? skill.baseDir : skill.filePath;
|
|
81
95
|
}
|
|
@@ -787,7 +787,7 @@ export class VaultProtocolHandler implements ProtocolHandler {
|
|
|
787
787
|
const cacheKey = parsed.ref.active ? "_" : (parsed.ref.vault ?? "_");
|
|
788
788
|
let cliInfo = cachedVaultInfo.get(cacheKey);
|
|
789
789
|
if (cliInfo === undefined) {
|
|
790
|
-
const result = await this.#spawn([
|
|
790
|
+
const result = await this.#spawn([...this.#vaultCliArg(parsed.ref), "vault", "info"], context);
|
|
791
791
|
assertCliSuccess("vault info", result);
|
|
792
792
|
cliInfo = result.stdout.trim();
|
|
793
793
|
cachedVaultInfo.set(cacheKey, cliInfo);
|
|
@@ -926,7 +926,7 @@ export class VaultProtocolHandler implements ProtocolHandler {
|
|
|
926
926
|
context?: ResolveContext,
|
|
927
927
|
): Promise<InternalResource> {
|
|
928
928
|
const invocation = buildObsidianCliInvocation(parsed);
|
|
929
|
-
const args = [...
|
|
929
|
+
const args = [...this.#vaultCliArg(parsed.ref), ...invocation.args];
|
|
930
930
|
const result = await this.#spawn(args, context);
|
|
931
931
|
assertCliSuccess(invocation.opLabel, result);
|
|
932
932
|
return {
|
package/src/launch/client.ts
CHANGED
|
@@ -367,7 +367,17 @@ class SocketDaemonClient implements DaemonBrokerClient {
|
|
|
367
367
|
try {
|
|
368
368
|
message = parseDaemonWireMessage(decoded);
|
|
369
369
|
} catch (error) {
|
|
370
|
-
|
|
370
|
+
const parseError = error instanceof Error ? error : new Error(String(error));
|
|
371
|
+
if (
|
|
372
|
+
typeof decoded === "object" &&
|
|
373
|
+
decoded !== null &&
|
|
374
|
+
"event" in decoded &&
|
|
375
|
+
decoded.event === "daemon-completed"
|
|
376
|
+
) {
|
|
377
|
+
logger.warn("Ignoring malformed daemon completion", { error: parseError.message });
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
this.#rejectPending(parseError);
|
|
371
381
|
continue;
|
|
372
382
|
}
|
|
373
383
|
if ("event" in message) {
|
package/src/launch/protocol.ts
CHANGED
|
@@ -168,6 +168,11 @@ function optionalString(value: unknown, label: string): string | undefined {
|
|
|
168
168
|
return stringValue(value, label);
|
|
169
169
|
}
|
|
170
170
|
|
|
171
|
+
function optionalRawString(value: unknown, label: string): string | undefined {
|
|
172
|
+
if (value === undefined) return undefined;
|
|
173
|
+
return rawString(value, label);
|
|
174
|
+
}
|
|
175
|
+
|
|
171
176
|
function booleanValue(value: unknown, label: string): boolean {
|
|
172
177
|
if (typeof value !== "boolean") throw new Error(`${label} must be a boolean`);
|
|
173
178
|
return value;
|
|
@@ -272,7 +277,7 @@ export function parseDaemonSnapshot(value: unknown): DaemonSnapshot {
|
|
|
272
277
|
restartCount: numberValue(source.restartCount, "daemon.restartCount"),
|
|
273
278
|
outputBytes: numberValue(source.outputBytes, "daemon.outputBytes"),
|
|
274
279
|
owner: optionalString(source.owner, "daemon.owner"),
|
|
275
|
-
readyMatch:
|
|
280
|
+
readyMatch: optionalRawString(source.readyMatch, "daemon.readyMatch"),
|
|
276
281
|
readyPending: source.readyPending === undefined ? undefined : readyPendingList(source.readyPending),
|
|
277
282
|
persist: booleanValue(source.persist, "daemon.persist"),
|
|
278
283
|
detached: source.detached === undefined ? false : booleanValue(source.detached, "daemon.detached"),
|
|
@@ -427,7 +432,7 @@ export function parseDaemonRpcResult(operation: DaemonOperation, value: unknown)
|
|
|
427
432
|
return {
|
|
428
433
|
op: "wait",
|
|
429
434
|
daemon: parseDaemonSnapshot(source.daemon),
|
|
430
|
-
matched:
|
|
435
|
+
matched: optionalRawString(source.matched, "result.matched"),
|
|
431
436
|
timedOut: booleanValue(source.timedOut, "result.timedOut"),
|
|
432
437
|
};
|
|
433
438
|
case "send":
|
package/src/main.ts
CHANGED
|
@@ -1033,11 +1033,12 @@ export async function buildSessionOptions(
|
|
|
1033
1033
|
if (parsed.noPrewalk && (parsed.prewalk || parsed.prewalkInto !== undefined)) {
|
|
1034
1034
|
throw new Error("--no-prewalk cannot be combined with --prewalk or --prewalk-into");
|
|
1035
1035
|
}
|
|
1036
|
+
const explicitPrewalk = parsed.prewalk === true || parsed.prewalkInto !== undefined;
|
|
1036
1037
|
const prewalkEnabled = parsed.noPrewalk
|
|
1037
1038
|
? false
|
|
1038
|
-
:
|
|
1039
|
+
: explicitPrewalk
|
|
1039
1040
|
? true
|
|
1040
|
-
: activeSettings.get("prewalk.enabled");
|
|
1041
|
+
: !restoringSession && activeSettings.get("prewalk.enabled");
|
|
1041
1042
|
if (prewalkEnabled) {
|
|
1042
1043
|
const rolePattern = expandRoleAlias(parsed.prewalkInto ?? DEFAULT_PREWALK_TARGET, activeSettings);
|
|
1043
1044
|
const resolved = resolveCliModel({ cliModel: rolePattern, modelRegistry, preferences: modelMatchPreferences });
|
package/src/mcp/config.ts
CHANGED
|
@@ -54,6 +54,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
|
|
|
54
54
|
};
|
|
55
55
|
if (server.args) config.args = server.args;
|
|
56
56
|
if (server.env) config.env = server.env;
|
|
57
|
+
if (server.envPolicy) config.envPolicy = server.envPolicy;
|
|
57
58
|
if (server.cwd) config.cwd = server.cwd;
|
|
58
59
|
return config;
|
|
59
60
|
}
|
|
@@ -65,6 +66,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
|
|
|
65
66
|
url: server.url ?? "",
|
|
66
67
|
};
|
|
67
68
|
if (server.headers) config.headers = server.headers;
|
|
69
|
+
if (server.headerPolicy) config.headerPolicy = server.headerPolicy;
|
|
68
70
|
return config;
|
|
69
71
|
}
|
|
70
72
|
|
|
@@ -75,6 +77,7 @@ function convertToLegacyConfig(server: MCPServer): MCPServerConfig {
|
|
|
75
77
|
url: server.url ?? "",
|
|
76
78
|
};
|
|
77
79
|
if (server.headers) config.headers = server.headers;
|
|
80
|
+
if (server.headerPolicy) config.headerPolicy = server.headerPolicy;
|
|
78
81
|
return config;
|
|
79
82
|
}
|
|
80
83
|
|
package/src/mcp/manager.ts
CHANGED
|
@@ -37,6 +37,7 @@ import type { McpConnectionStatusEvent } from "./startup-events";
|
|
|
37
37
|
import type { MCPToolDetails } from "./tool-bridge";
|
|
38
38
|
import { DeferredMCPTool, MCPTool } from "./tool-bridge";
|
|
39
39
|
import type { MCPToolCache } from "./tool-cache";
|
|
40
|
+
import { setGeneratedHeader } from "./transports/header-policy";
|
|
40
41
|
import type {
|
|
41
42
|
MCPAuthChallenge,
|
|
42
43
|
MCPGetPromptResult,
|
|
@@ -1458,13 +1459,11 @@ export class MCPManager {
|
|
|
1458
1459
|
|
|
1459
1460
|
if (credential) {
|
|
1460
1461
|
if (resolved.type === "http" || resolved.type === "sse") {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
},
|
|
1467
|
-
};
|
|
1462
|
+
// Client-generated authorization wins over any configured header
|
|
1463
|
+
// with the same case-insensitive name (Agent Plugins §7.2.1).
|
|
1464
|
+
const headers = { ...resolved.headers };
|
|
1465
|
+
setGeneratedHeader(headers, "Authorization", `Bearer ${credential.access}`);
|
|
1466
|
+
resolved = { ...resolved, headers };
|
|
1468
1467
|
} else {
|
|
1469
1468
|
resolved = {
|
|
1470
1469
|
...resolved,
|
|
@@ -1481,7 +1480,9 @@ export class MCPManager {
|
|
|
1481
1480
|
}
|
|
1482
1481
|
|
|
1483
1482
|
if (resolved.type !== "http" && resolved.type !== "sse") {
|
|
1484
|
-
|
|
1483
|
+
// Literal env values (Agent Plugins §§4.1/9.2) are opaque package data:
|
|
1484
|
+
// no env-name lookup, no `!command` execution, no dropping empty values.
|
|
1485
|
+
if (resolved.env && resolved.envPolicy !== "literal") {
|
|
1485
1486
|
const nextEnv: Record<string, string> = {};
|
|
1486
1487
|
for (const [key, value] of Object.entries(resolved.env)) {
|
|
1487
1488
|
const resolvedValue = await resolveConfigValue(value);
|
|
@@ -1490,7 +1491,9 @@ export class MCPManager {
|
|
|
1490
1491
|
resolved = { ...resolved, env: nextEnv };
|
|
1491
1492
|
}
|
|
1492
1493
|
} else {
|
|
1493
|
-
|
|
1494
|
+
// Origin-locked servers (Agent Plugins §9.2) carry literal header
|
|
1495
|
+
// values: no placeholder or environment-variable expansion.
|
|
1496
|
+
if (resolved.headers && resolved.headerPolicy !== "origin-locked") {
|
|
1494
1497
|
const nextHeaders: Record<string, string> = {};
|
|
1495
1498
|
for (const [key, value] of Object.entries(resolved.headers)) {
|
|
1496
1499
|
const resolvedValue = await resolveConfigValue(value);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP header precedence and redirect-origin policy for remote MCP transports.
|
|
3
|
+
*
|
|
4
|
+
* Two invariants, applied at every transport fetch:
|
|
5
|
+
*
|
|
6
|
+
* 1. Client-generated headers (protocol headers like `Content-Type`, `Accept`,
|
|
7
|
+
* `Mcp-Session-Id`, and authorization) take precedence over configured
|
|
8
|
+
* headers with the same case-insensitive name. Configured headers can never
|
|
9
|
+
* corrupt the MCP wire protocol via casing tricks.
|
|
10
|
+
* 2. Origin-locked servers (Agent Plugins §7.2.1) never forward configured
|
|
11
|
+
* headers to a different origin: redirects are followed manually and
|
|
12
|
+
* configured headers are attached only when the hop targets the configured
|
|
13
|
+
* origin. Method-changing redirects of non-GET requests are refused.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** Header buckets for one MCP HTTP request. */
|
|
17
|
+
export interface MCPHeaderSources {
|
|
18
|
+
/** Client-generated HTTP/MCP/authorization headers; win case-insensitively. */
|
|
19
|
+
generated: Record<string, string>;
|
|
20
|
+
/** Configured headers from the server entry (package or user config). */
|
|
21
|
+
configured?: Record<string, string>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Merge configured headers under client-generated ones: a configured entry is
|
|
26
|
+
* dropped when a generated header with the same case-insensitive name exists.
|
|
27
|
+
*/
|
|
28
|
+
export function mergeMCPHeaders({ generated, configured }: MCPHeaderSources): Record<string, string> {
|
|
29
|
+
if (!configured) return { ...generated };
|
|
30
|
+
const generatedNames = new Set<string>();
|
|
31
|
+
for (const name in generated) generatedNames.add(name.toLowerCase());
|
|
32
|
+
const merged: Record<string, string> = {};
|
|
33
|
+
for (const name in configured) {
|
|
34
|
+
if (!generatedNames.has(name.toLowerCase())) merged[name] = configured[name];
|
|
35
|
+
}
|
|
36
|
+
return { ...merged, ...generated };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Set a client-generated header, removing any existing entry with the same
|
|
41
|
+
* case-insensitive name so the generated value is the only one sent.
|
|
42
|
+
*/
|
|
43
|
+
export function setGeneratedHeader(headers: Record<string, string>, name: string, value: string): void {
|
|
44
|
+
const lower = name.toLowerCase();
|
|
45
|
+
for (const existing in headers) {
|
|
46
|
+
if (existing.toLowerCase() === lower) delete headers[existing];
|
|
47
|
+
}
|
|
48
|
+
headers[name] = value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const REDIRECT_STATUSES: Record<number, true> = { 301: true, 302: true, 303: true, 307: true, 308: true };
|
|
52
|
+
const MAX_REDIRECT_HOPS = 5;
|
|
53
|
+
|
|
54
|
+
export interface MCPFetchInit {
|
|
55
|
+
method: "GET" | "POST" | "DELETE";
|
|
56
|
+
body?: string;
|
|
57
|
+
signal?: AbortSignal;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Fetch an MCP endpoint with header precedence and, for origin-locked servers,
|
|
62
|
+
* manual redirect handling that strips configured headers on cross-origin hops.
|
|
63
|
+
*
|
|
64
|
+
* Non-locked servers keep the platform default redirect behavior. Locked
|
|
65
|
+
* non-GET requests only follow 307/308 (method-preserving); a 301/302/303
|
|
66
|
+
* redirect of a JSON-RPC POST is a connection error, never a silent GET.
|
|
67
|
+
*/
|
|
68
|
+
export async function mcpFetch(
|
|
69
|
+
url: string,
|
|
70
|
+
init: MCPFetchInit,
|
|
71
|
+
sources: MCPHeaderSources,
|
|
72
|
+
originLocked: boolean,
|
|
73
|
+
): Promise<Response> {
|
|
74
|
+
if (!originLocked) {
|
|
75
|
+
return fetch(url, { ...init, headers: mergeMCPHeaders(sources) });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const configuredOrigin = new URL(url).origin;
|
|
79
|
+
let currentUrl = url;
|
|
80
|
+
for (let hop = 0; hop <= MAX_REDIRECT_HOPS; hop++) {
|
|
81
|
+
const attachConfigured = new URL(currentUrl).origin === configuredOrigin;
|
|
82
|
+
const headers = mergeMCPHeaders(attachConfigured ? sources : { generated: sources.generated });
|
|
83
|
+
const response = await fetch(currentUrl, { ...init, headers, redirect: "manual" });
|
|
84
|
+
if (!REDIRECT_STATUSES[response.status]) return response;
|
|
85
|
+
|
|
86
|
+
const location = response.headers.get("Location");
|
|
87
|
+
if (!location) return response;
|
|
88
|
+
await response.body?.cancel();
|
|
89
|
+
if (init.method !== "GET" && response.status !== 307 && response.status !== 308) {
|
|
90
|
+
throw new Error(`HTTP ${response.status}: server redirected a ${init.method} request; refusing to follow`);
|
|
91
|
+
}
|
|
92
|
+
currentUrl = new URL(location, currentUrl).href;
|
|
93
|
+
}
|
|
94
|
+
throw new Error(`Too many redirects (> ${MAX_REDIRECT_HOPS}) fetching ${url}`);
|
|
95
|
+
}
|