@humain/terminal 0.1.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/dist/bundle/chunks/{anthropic-messages-XULXUBIG.js → anthropic-messages-QFCEKGI3.js} +1 -1
- package/dist/bundle/chunks/{azure-openai-responses-JEG2XOYC.js → azure-openai-responses-G4ZMHV37.js} +1 -1
- package/dist/bundle/chunks/chunk-7T6XCLJF.js +22 -0
- package/dist/bundle/chunks/{chunk-LYAMSPR2.js → chunk-ADQYVZ6A.js} +483 -506
- package/dist/bundle/chunks/chunk-D6HHRSB5.js +103 -0
- package/dist/bundle/chunks/{chunk-JUPSJTFQ.js → chunk-K4JAZYQH.js} +1 -1
- package/dist/bundle/chunks/{chunk-HPPQHK3J.js → chunk-KKIB2TVR.js} +1 -1
- package/dist/bundle/chunks/chunk-LI2YVPWP.js +2 -0
- package/dist/bundle/chunks/chunk-NMPIM7MN.js +2 -0
- package/dist/bundle/chunks/{chunk-BZQ53VGE.js → chunk-XSR434CO.js} +1 -1
- package/dist/bundle/chunks/compat-CN7WN6DA.js +2 -0
- package/dist/bundle/chunks/dist-PRPIUY3U.js +2 -0
- package/dist/bundle/chunks/esm-E7UVEYTO.js +4 -0
- package/dist/bundle/chunks/{extract-TRW5FBY6.js → extract-CTIFQZDJ.js} +15 -14
- package/dist/bundle/chunks/{google-generative-ai-OX6VSCA6.js → google-generative-ai-RSUU4MWO.js} +1 -1
- package/dist/bundle/chunks/{google-vertex-GW76GSS2.js → google-vertex-CKQPOITO.js} +1 -1
- package/dist/bundle/chunks/host-WP3MVKS2.js +2 -0
- package/dist/bundle/chunks/{node-KOIP55YZ.js → node-NF43OPII.js} +1 -1
- package/dist/bundle/chunks/{node-XY3CBY3C.js → node-UMFM7CDZ.js} +1 -1
- package/dist/bundle/chunks/{openai-codex-responses-NPLWP6ZC.js → openai-codex-responses-S34V5ZSD.js} +1 -1
- package/dist/bundle/chunks/{openai-completions-OT2GTJKC.js → openai-completions-T2IGVUEI.js} +1 -1
- package/dist/bundle/chunks/{openai-responses-5A6CR7GI.js → openai-responses-GYZODCT3.js} +1 -1
- package/dist/bundle/chunks/readability-LAGF34IB.js +3 -0
- package/dist/bundle/chunks/turndown.cjs-HRMZVRUL.js +2 -0
- package/dist/bundle/cli.js +1 -1
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/core/model-runtime.d.ts +21 -0
- package/dist/core/model-runtime.d.ts.map +1 -1
- package/dist/core/model-runtime.js +34 -4
- package/dist/core/model-runtime.js.map +1 -1
- package/dist/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/core/tools/file-mutation-queue.js +21 -8
- package/dist/core/tools/file-mutation-queue.js.map +1 -1
- package/dist/core/tools/subagent-env.d.ts +20 -0
- package/dist/core/tools/subagent-env.d.ts.map +1 -0
- package/dist/core/tools/subagent-env.js +47 -0
- package/dist/core/tools/subagent-env.js.map +1 -0
- package/dist/core/tools/subagent-scheduler.d.ts +36 -0
- package/dist/core/tools/subagent-scheduler.d.ts.map +1 -0
- package/dist/core/tools/subagent-scheduler.js +213 -0
- package/dist/core/tools/subagent-scheduler.js.map +1 -0
- package/dist/core/tools/subagent.d.ts +83 -3
- package/dist/core/tools/subagent.d.ts.map +1 -1
- package/dist/core/tools/subagent.js +215 -76
- package/dist/core/tools/subagent.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/dist/humain/mcp-adapter/vendor/README.md +125 -19
- package/dist/humain/mcp-adapter/vendor/agent-plugin-loader.ts +69 -30
- package/dist/humain/mcp-adapter/vendor/agent-plugin-provenance.ts +36 -0
- package/dist/humain/mcp-adapter/vendor/claude-plugin-loader.ts +365 -0
- package/dist/humain/mcp-adapter/vendor/commands.ts +13 -15
- package/dist/humain/mcp-adapter/vendor/config.ts +198 -30
- package/dist/humain/mcp-adapter/vendor/consent-manager.ts +40 -9
- package/dist/humain/mcp-adapter/vendor/direct-tool-surface.ts +250 -0
- package/dist/humain/mcp-adapter/vendor/direct-tools.ts +9 -228
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js +74 -32
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.d.ts +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js +31 -0
- package/dist/humain/mcp-adapter/vendor/dist/agent-plugin-provenance.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.d.ts +9 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js +362 -0
- package/dist/humain/mcp-adapter/vendor/dist/claude-plugin-loader.js.map +1 -0
- package/dist/humain/mcp-adapter/vendor/dist/config.d.ts +3 -1
- package/dist/humain/mcp-adapter/vendor/dist/config.js +189 -27
- package/dist/humain/mcp-adapter/vendor/dist/config.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js +7 -16
- package/dist/humain/mcp-adapter/vendor/dist/metadata-cache.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js +5 -17
- package/dist/humain/mcp-adapter/vendor/dist/package-mcp-loader.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/types.d.ts +30 -4
- package/dist/humain/mcp-adapter/vendor/dist/types.js +26 -0
- package/dist/humain/mcp-adapter/vendor/dist/types.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/dist/utils.d.ts +6 -0
- package/dist/humain/mcp-adapter/vendor/dist/utils.js +65 -6
- package/dist/humain/mcp-adapter/vendor/dist/utils.js.map +1 -1
- package/dist/humain/mcp-adapter/vendor/host-html-template.ts +20 -19
- package/dist/humain/mcp-adapter/vendor/http-ca.ts +84 -0
- package/dist/humain/mcp-adapter/vendor/index.bundle.mjs +37102 -15103
- package/dist/humain/mcp-adapter/vendor/index.ts +828 -164
- package/dist/humain/mcp-adapter/vendor/init.ts +48 -10
- package/dist/humain/mcp-adapter/vendor/lazy-loader.ts +7 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-fetch.ts +113 -0
- package/dist/humain/mcp-adapter/vendor/mcp-auth-flow.ts +256 -77
- package/dist/humain/mcp-adapter/vendor/mcp-auth.ts +316 -54
- package/dist/humain/mcp-adapter/vendor/mcp-code.ts +45 -18
- package/dist/humain/mcp-adapter/vendor/mcp-install.ts +60 -0
- package/dist/humain/mcp-adapter/vendor/mcp-oauth-provider.ts +70 -6
- package/dist/humain/mcp-adapter/vendor/mcp-output-guard.ts +53 -42
- package/dist/humain/mcp-adapter/vendor/mcp-panel-theme.ts +105 -0
- package/dist/humain/mcp-adapter/vendor/mcp-panel.ts +366 -331
- package/dist/humain/mcp-adapter/vendor/mcp-references.ts +9 -14
- package/dist/humain/mcp-adapter/vendor/mcp-script-worker.mjs +3 -1
- package/dist/humain/mcp-adapter/vendor/mcp-setup-panel.ts +383 -341
- package/dist/humain/mcp-adapter/vendor/mcp-status.ts +5 -0
- package/dist/humain/mcp-adapter/vendor/metadata-cache.ts +7 -16
- package/dist/humain/mcp-adapter/vendor/namespace-tools.ts +61 -3
- package/dist/humain/mcp-adapter/vendor/oauth.ts +6 -2
- package/dist/humain/mcp-adapter/vendor/package-mcp-loader.ts +5 -17
- package/dist/humain/mcp-adapter/vendor/package.json +13 -3
- package/dist/humain/mcp-adapter/vendor/prompts.ts +35 -3
- package/dist/humain/mcp-adapter/vendor/proxy-modes.ts +108 -7
- package/dist/humain/mcp-adapter/vendor/runtime-owner.ts +16 -1
- package/dist/humain/mcp-adapter/vendor/sampling-handler.ts +9 -32
- package/dist/humain/mcp-adapter/vendor/sandbox-proxy-template.ts +20 -93
- package/dist/humain/mcp-adapter/vendor/server-manager.ts +227 -40
- package/dist/humain/mcp-adapter/vendor/session-approvals.ts +187 -0
- package/dist/humain/mcp-adapter/vendor/skills/mcp-scripting/SKILL.md +9 -1
- package/dist/humain/mcp-adapter/vendor/state.ts +10 -1
- package/dist/humain/mcp-adapter/vendor/tool-approval.ts +10 -22
- package/dist/humain/mcp-adapter/vendor/tool-metadata.ts +14 -0
- package/dist/humain/mcp-adapter/vendor/types.ts +57 -4
- package/dist/humain/mcp-adapter/vendor/ui-server.ts +36 -28
- package/dist/humain/mcp-adapter/vendor/utils.ts +64 -6
- package/dist/humain/web-access/vendor/abortable.d.ts +3 -0
- package/dist/humain/web-access/vendor/abortable.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/abortable.js +22 -0
- package/dist/humain/web-access/vendor/abortable.js.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts +9 -0
- package/dist/humain/web-access/vendor/crawl4ai.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/crawl4ai.js +187 -0
- package/dist/humain/web-access/vendor/crawl4ai.js.map +1 -0
- package/dist/humain/web-access/vendor/credential-source.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/credential-source.js +1 -0
- package/dist/humain/web-access/vendor/credential-source.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-page.js +20 -2
- package/dist/humain/web-access/vendor/curator-page.js.map +1 -1
- package/dist/humain/web-access/vendor/curator-run.d.ts +11 -0
- package/dist/humain/web-access/vendor/curator-run.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/curator-run.js +38 -0
- package/dist/humain/web-access/vendor/curator-run.js.map +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts +1 -0
- package/dist/humain/web-access/vendor/curator-server.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/curator-server.js +2 -0
- package/dist/humain/web-access/vendor/curator-server.js.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js +1 -1
- package/dist/humain/web-access/vendor/duckduckgo.js.map +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts +1 -1
- package/dist/humain/web-access/vendor/extract.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/extract.js +58 -11
- package/dist/humain/web-access/vendor/extract.js.map +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js +1 -1
- package/dist/humain/web-access/vendor/fetch-params.js.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.d.ts +2 -2
- package/dist/humain/web-access/vendor/gemini-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/gemini-search.js +12 -5
- package/dist/humain/web-access/vendor/gemini-search.js.map +1 -1
- package/dist/humain/web-access/vendor/index.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/index.js +23 -19
- package/dist/humain/web-access/vendor/index.js.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/openai-search.js +40 -14
- package/dist/humain/web-access/vendor/openai-search.js.map +1 -1
- package/dist/humain/web-access/vendor/page-query.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/page-query.js +6 -2
- package/dist/humain/web-access/vendor/page-query.js.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/query-rewrite.js +6 -2
- package/dist/humain/web-access/vendor/query-rewrite.js.map +1 -1
- package/dist/humain/web-access/vendor/serpapi.d.ts +4 -0
- package/dist/humain/web-access/vendor/serpapi.d.ts.map +1 -0
- package/dist/humain/web-access/vendor/serpapi.js +215 -0
- package/dist/humain/web-access/vendor/serpapi.js.map +1 -0
- package/dist/humain/web-access/vendor/ssrf-protection.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js +5 -1
- package/dist/humain/web-access/vendor/ssrf-protection.js.map +1 -1
- package/dist/humain/web-access/vendor/storage.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/storage.js +9 -0
- package/dist/humain/web-access/vendor/storage.js.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts +1 -1
- package/dist/humain/web-access/vendor/summary-review.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/summary-review.js +35 -24
- package/dist/humain/web-access/vendor/summary-review.js.map +1 -1
- package/dist/humain/web-access/vendor/utils.d.ts.map +1 -1
- package/dist/humain/web-access/vendor/utils.js +5 -3
- package/dist/humain/web-access/vendor/utils.js.map +1 -1
- package/dist/modes/json-event.d.ts +8 -0
- package/dist/modes/json-event.d.ts.map +1 -1
- package/dist/modes/json-event.js +17 -0
- package/dist/modes/json-event.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +3 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/docs/environment-variables.md +9 -0
- package/docs/json.md +16 -0
- package/npm-shrinkwrap.json +6 -6
- package/package.json +2 -2
- package/src/humain/mcp-adapter/UPSTREAM.md +56 -7
- package/src/humain/mcp-adapter/upstream.json +6 -4
- package/src/humain/mcp-adapter/vendor/CHANGELOG.md +78 -0
- package/src/humain/web-access/UPSTREAM.md +18 -8
- package/src/humain/web-access/upstream.json +6 -5
- package/dist/bundle/chunks/chunk-2LC3S3VV.js +0 -105
|
@@ -311,9 +311,9 @@ export async function authenticateServer(
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
ui.setStatus("mcp-auth", `Authenticating ${serverName}...`);
|
|
314
|
-
const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd);
|
|
314
|
+
const authStorageOptions = getAuthStorageOptions(config.settings?.oauthDir, cwd, config.settings?.oauthCredentialStore);
|
|
315
315
|
const status = await authenticate(serverName, serverUrl, definition, {
|
|
316
|
-
...(authStorageOptions.
|
|
316
|
+
...(Object.keys(authStorageOptions).length > 0 ? { authStorageOptions } : {}),
|
|
317
317
|
onAuthorizationUrl: () => {},
|
|
318
318
|
onAuthorizationInput: async (authorizationUrl, inputSignal) => {
|
|
319
319
|
if (inputSignal.aborted) return undefined;
|
|
@@ -364,27 +364,24 @@ export async function logoutServer(
|
|
|
364
364
|
|
|
365
365
|
const signal = state.owner?.signal;
|
|
366
366
|
try {
|
|
367
|
-
await
|
|
367
|
+
await state.manager.close(serverName);
|
|
368
368
|
} catch (error) {
|
|
369
369
|
if (isAbortError(error, signal)) throw error;
|
|
370
370
|
const message = error instanceof Error ? error.message : String(error);
|
|
371
371
|
if (ui) {
|
|
372
|
-
ui.notify(`Failed to
|
|
372
|
+
ui.notify(`Failed to close OAuth server "${serverName}"; credentials were not cleared: ${sanitizeTerminalText(message)}`, "error");
|
|
373
373
|
}
|
|
374
374
|
return { ok: false, message };
|
|
375
375
|
}
|
|
376
376
|
|
|
377
377
|
state.owner?.throwIfInactive();
|
|
378
378
|
try {
|
|
379
|
-
await state.
|
|
379
|
+
await removeAuth(serverName, { authStorageOptions: state.authStorageOptions, signal, runtime: state.oauthRuntime });
|
|
380
380
|
} catch (error) {
|
|
381
381
|
if (isAbortError(error, signal)) throw error;
|
|
382
382
|
const message = error instanceof Error ? error.message : String(error);
|
|
383
383
|
if (ui) {
|
|
384
|
-
ui.notify(
|
|
385
|
-
`OAuth credentials were cleared for "${serverName}", but its connection could not be closed: ${sanitizeTerminalText(message)}`,
|
|
386
|
-
"error",
|
|
387
|
-
);
|
|
384
|
+
ui.notify(`Failed to clear OAuth credentials for "${serverName}": ${sanitizeTerminalText(message)}`, "error");
|
|
388
385
|
}
|
|
389
386
|
return { ok: false, message };
|
|
390
387
|
}
|
|
@@ -474,7 +471,7 @@ function buildSharedConfigNoticeLines(configOverridePath: string | undefined, cw
|
|
|
474
471
|
const discovery = getMcpStandardConfigSummary(configOverridePath, cwd);
|
|
475
472
|
const onboardingState = loadOnboardingState();
|
|
476
473
|
const sharedSources = discovery.sources.filter((source) =>
|
|
477
|
-
(source.id === "shared-project" || source.id === "shared-global") && source.serverCount > 0,
|
|
474
|
+
(source.id === "shared-project" || source.id === "shared-project-ancestor" || source.id === "shared-global") && source.serverCount > 0,
|
|
478
475
|
);
|
|
479
476
|
if (sharedSources.length === 0 || onboardingState.sharedConfigHintShown) {
|
|
480
477
|
return { lines: [], fingerprint: null };
|
|
@@ -556,8 +553,8 @@ export async function openMcpSetup(
|
|
|
556
553
|
|
|
557
554
|
return new Promise<PanelFlowResult>((resolve) => {
|
|
558
555
|
ctx.ui.custom(
|
|
559
|
-
(tui,
|
|
560
|
-
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings }, tui, () => {
|
|
556
|
+
(tui, theme, keybindings, done) => {
|
|
557
|
+
return createMcpSetupPanel(discovery, callbacks, { mode, onboardingState, keybindings, theme }, tui, () => {
|
|
561
558
|
done(undefined);
|
|
562
559
|
resolve({ configChanged });
|
|
563
560
|
});
|
|
@@ -670,7 +667,7 @@ export async function openMcpPanel(
|
|
|
670
667
|
|
|
671
668
|
await new Promise<void>((resolve) => {
|
|
672
669
|
ctx.ui.custom(
|
|
673
|
-
(tui,
|
|
670
|
+
(tui, theme, keybindings, done) => {
|
|
674
671
|
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, (result: McpPanelResult) => {
|
|
675
672
|
void (async () => {
|
|
676
673
|
if (!result.cancelled && result.disabledChanges.size > 0) {
|
|
@@ -699,7 +696,7 @@ export async function openMcpPanel(
|
|
|
699
696
|
done(undefined);
|
|
700
697
|
resolve();
|
|
701
698
|
});
|
|
702
|
-
}, { noticeLines, keybindings });
|
|
699
|
+
}, { noticeLines, keybindings, theme });
|
|
703
700
|
},
|
|
704
701
|
{
|
|
705
702
|
overlay: true,
|
|
@@ -750,13 +747,14 @@ export async function openMcpAuthPanel(
|
|
|
750
747
|
|
|
751
748
|
await new Promise<void>((resolve) => {
|
|
752
749
|
ctx.ui.custom(
|
|
753
|
-
(tui,
|
|
750
|
+
(tui, theme, keybindings, done) => {
|
|
754
751
|
return createMcpPanel(config, cache, provenanceMap, callbacks, tui, () => {
|
|
755
752
|
done(undefined);
|
|
756
753
|
resolve();
|
|
757
754
|
}, {
|
|
758
755
|
authOnly: true,
|
|
759
756
|
keybindings,
|
|
757
|
+
theme,
|
|
760
758
|
noticeLines: ["Select an OAuth MCP server and press Enter or ctrl+a to authenticate."],
|
|
761
759
|
});
|
|
762
760
|
},
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
// config.ts - Config loading with import support
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, realpathSync, statSync, writeFileSync, mkdirSync, renameSync } from "node:fs";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { dirname, join, resolve } from "node:path";
|
|
4
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
5
5
|
import { parse as parseToml } from "smol-toml";
|
|
6
6
|
import stripJsonComments from "strip-json-comments";
|
|
7
7
|
import { getAgentPath, getConfigDirName } from "./agent-dir.ts";
|
|
8
8
|
import { getAgentPluginSummaries, loadAgentPluginConfigs, type AgentPluginSummary } from "./agent-plugin-loader.ts";
|
|
9
|
+
import { cloneBuiltInAgentPluginEntry, isBuiltInAgentPlugin, mergeBuiltInAgentPluginEntries } from "./agent-plugin-provenance.ts";
|
|
10
|
+
import { loadClaudePluginBundles } from "./claude-plugin-loader.ts";
|
|
9
11
|
import { loadPackageMcpConfigs } from "./package-mcp-loader.ts";
|
|
10
|
-
import { isServerDisabled, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
11
|
-
import { toStringRecord } from "./utils.ts";
|
|
12
|
+
import { formatServerNamespace, isServerDisabled, type ClaudePluginConfig, type HostConfigDiscovery, type McpConfig, type ServerEntry, type McpSettings, type ImportKind, type ServerProvenance } from "./types.ts";
|
|
13
|
+
import { parseJsonWithComments, toStringRecord } from "./utils.ts";
|
|
12
14
|
|
|
13
15
|
const GENERIC_GLOBAL_CONFIG_PATH = join(homedir(), ".config", "mcp", "mcp.json");
|
|
14
16
|
const AGENTS_GLOBAL_CONFIG_PATHS = [
|
|
@@ -93,7 +95,7 @@ const IMPORT_PATHS: Record<ImportKind, string[]> = {
|
|
|
93
95
|
};
|
|
94
96
|
|
|
95
97
|
interface ConfigSourceSpec {
|
|
96
|
-
id: "shared-global" | "agents-global" | "agents-nested-global" | "pi-global" | "shared-project" | "pi-project";
|
|
98
|
+
id: "shared-global" | "agents-global" | "agents-nested-global" | "pi-global" | "shared-project-ancestor" | "pi-project-ancestor" | "shared-project" | "pi-project";
|
|
97
99
|
label: string;
|
|
98
100
|
readPath: string;
|
|
99
101
|
writePath: string;
|
|
@@ -253,7 +255,7 @@ export function getMcpDiscoverySummary(
|
|
|
253
255
|
const includeHostConfigs = options.includeHostConfigs !== false;
|
|
254
256
|
|
|
255
257
|
const importKinds = isExclusiveConfigMode()
|
|
256
|
-
? (readValidatedConfig(
|
|
258
|
+
? (readValidatedConfig(getPiGlobalConfigPath(overridePath), "MCP exclusive config")?.imports ?? [])
|
|
257
259
|
: (Object.keys(IMPORT_PATHS) as ImportKind[]);
|
|
258
260
|
const imports = includeHostConfigs
|
|
259
261
|
? importKinds
|
|
@@ -312,7 +314,12 @@ export function getMcpDiscoverySummary(
|
|
|
312
314
|
}
|
|
313
315
|
|
|
314
316
|
export function cloneMcpConfig(config: McpConfig): McpConfig {
|
|
315
|
-
|
|
317
|
+
const cloned = structuredClone(config);
|
|
318
|
+
for (const [name, source] of Object.entries(config.mcpServers)) {
|
|
319
|
+
const builtInClone = cloneBuiltInAgentPluginEntry(source);
|
|
320
|
+
if (builtInClone) cloned.mcpServers[name] = builtInClone;
|
|
321
|
+
}
|
|
322
|
+
return cloned;
|
|
316
323
|
}
|
|
317
324
|
|
|
318
325
|
export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpConfig {
|
|
@@ -331,14 +338,44 @@ export function loadMcpConfig(overridePath?: string, cwd = process.cwd()): McpCo
|
|
|
331
338
|
config = mergeConfigs(config, expandImports(loaded, cwd));
|
|
332
339
|
}
|
|
333
340
|
|
|
334
|
-
if (isExclusiveConfigMode()) return config;
|
|
341
|
+
if (isExclusiveConfigMode()) return resolveConfiguredClaudePluginMcp(config, cwd);
|
|
335
342
|
|
|
336
343
|
const packageConfig = loadPackageMcpConfigs(cwd);
|
|
337
344
|
const pluginConfig = loadAgentPluginConfigs(config.settings?.agentPluginPaths, cwd);
|
|
338
345
|
const packageServers = Object.fromEntries(
|
|
339
346
|
Object.entries(packageConfig.mcpServers).filter(([name]) => !Object.hasOwn(pluginConfig.mcpServers, name)),
|
|
340
347
|
);
|
|
341
|
-
|
|
348
|
+
const higherPrecedenceConfig = mergeConfigs(
|
|
349
|
+
{ mcpServers: packageServers },
|
|
350
|
+
mergeConfigs(pluginConfig, config),
|
|
351
|
+
);
|
|
352
|
+
return mergeClaudePluginMcpDefaults(config.claudePlugins, higherPrecedenceConfig, cwd);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function resolveConfiguredClaudePluginMcp(config: McpConfig, cwd = process.cwd()): McpConfig {
|
|
356
|
+
return mergeClaudePluginMcpDefaults(config.claudePlugins, config, cwd);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function discoverConfiguredClaudePluginSkills(config: McpConfig, cwd = process.cwd()): string[] {
|
|
360
|
+
return loadClaudePluginBundles(config.claudePlugins, cwd, validateConfig, { mcp: false, skills: true }).skillPaths;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function mergeClaudePluginMcpDefaults(
|
|
364
|
+
plugins: ClaudePluginConfig[] | undefined,
|
|
365
|
+
higherPrecedenceConfig: McpConfig,
|
|
366
|
+
cwd: string,
|
|
367
|
+
): McpConfig {
|
|
368
|
+
const pluginServers = loadClaudePluginBundles(plugins, cwd, validateConfig, { mcp: true, skills: false }).mcpServers;
|
|
369
|
+
const higherNamesByNamespace = new Map(
|
|
370
|
+
Object.keys(higherPrecedenceConfig.mcpServers).map(name => [formatServerNamespace(name), name]),
|
|
371
|
+
);
|
|
372
|
+
const defaults = Object.fromEntries(Object.entries(pluginServers).filter(([name]) => {
|
|
373
|
+
const higherName = higherNamesByNamespace.get(formatServerNamespace(name));
|
|
374
|
+
if (!higherName || higherName === name) return true;
|
|
375
|
+
console.warn(`Claude plugin MCP server "${name}" is shadowed by higher-precedence server "${higherName}" because both normalize to the same namespace`);
|
|
376
|
+
return false;
|
|
377
|
+
}));
|
|
378
|
+
return mergeConfigs({ mcpServers: defaults }, higherPrecedenceConfig);
|
|
342
379
|
}
|
|
343
380
|
|
|
344
381
|
function getMergedSettings(overridePath?: string, cwd = process.cwd()): McpSettings | undefined {
|
|
@@ -418,7 +455,7 @@ function getConfigConflicts(
|
|
|
418
455
|
}
|
|
419
456
|
|
|
420
457
|
function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSourceSpec[] {
|
|
421
|
-
const userPath =
|
|
458
|
+
const userPath = getPiGlobalConfigPath(overridePath);
|
|
422
459
|
const projectPath = getProjectConfigPath(cwd);
|
|
423
460
|
const projectPiPath = getProjectPiConfigPath(cwd);
|
|
424
461
|
const sources: ConfigSourceSpec[] = [];
|
|
@@ -472,6 +509,43 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
472
509
|
scope: "global",
|
|
473
510
|
});
|
|
474
511
|
|
|
512
|
+
// Compare file identities so symlink aliases cannot reload a global source
|
|
513
|
+
// at ancestor precedence. Keep original paths for display and writes.
|
|
514
|
+
const reservedPaths = new Set([
|
|
515
|
+
...sources.map((source) => getConfigPathIdentity(source.readPath)),
|
|
516
|
+
getConfigPathIdentity(projectPath),
|
|
517
|
+
getConfigPathIdentity(projectPiPath),
|
|
518
|
+
]);
|
|
519
|
+
// Only user-global files (including an explicit override) may opt in to
|
|
520
|
+
// ancestor discovery. Project files cannot extend this trust boundary.
|
|
521
|
+
const ancestorSources = new Map<string, ConfigSourceSpec>();
|
|
522
|
+
const descriptors = [
|
|
523
|
+
{ id: "shared-project-ancestor", label: "ancestor standard MCP", path: getProjectConfigPath, shared: true },
|
|
524
|
+
{ id: "pi-project-ancestor", label: "ancestor Pi override", path: getProjectPiConfigPath, shared: false },
|
|
525
|
+
] as const;
|
|
526
|
+
const ancestorRoot = getConfiguredAncestorRoot(sources, cwd);
|
|
527
|
+
if (ancestorRoot) {
|
|
528
|
+
for (const dir of getAncestorProjectDirs(cwd, ancestorRoot)) {
|
|
529
|
+
for (const descriptor of descriptors) {
|
|
530
|
+
const path = descriptor.path(dir);
|
|
531
|
+
const identity = getConfigPathIdentity(path);
|
|
532
|
+
if (reservedPaths.has(identity) || !existsSync(path)) continue;
|
|
533
|
+
// Reinsert aliases at their nearest precedence position.
|
|
534
|
+
ancestorSources.delete(identity);
|
|
535
|
+
ancestorSources.set(identity, {
|
|
536
|
+
id: descriptor.id,
|
|
537
|
+
label: descriptor.label,
|
|
538
|
+
readPath: path,
|
|
539
|
+
writePath: path,
|
|
540
|
+
kind: "project",
|
|
541
|
+
shared: descriptor.shared,
|
|
542
|
+
scope: "project",
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
sources.push(...ancestorSources.values());
|
|
548
|
+
|
|
475
549
|
if (projectPath !== userPath) {
|
|
476
550
|
sources.push({
|
|
477
551
|
id: "shared-project",
|
|
@@ -499,21 +573,81 @@ function getConfigSources(overridePath?: string, cwd = process.cwd()): ConfigSou
|
|
|
499
573
|
return sources;
|
|
500
574
|
}
|
|
501
575
|
|
|
502
|
-
function
|
|
503
|
-
|
|
576
|
+
function getConfigPathIdentity(path: string): string {
|
|
577
|
+
try {
|
|
578
|
+
return realpathSync(path);
|
|
579
|
+
} catch {
|
|
580
|
+
// Missing or inaccessible paths still participate in lexical deduplication.
|
|
581
|
+
return resolve(path);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function isWithin(base: string, target: string): boolean {
|
|
586
|
+
const path = relative(base, target);
|
|
587
|
+
return path !== ".." && !path.startsWith(`..${sep}`) && !isAbsolute(path);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function getConfiguredAncestorRoot(globalSources: ConfigSourceSpec[], cwd: string): string | undefined {
|
|
591
|
+
let configured: unknown;
|
|
592
|
+
for (const source of globalSources) {
|
|
593
|
+
const roots = readValidatedConfig(source.readPath, `MCP config from ${source.readPath}`)?.settings?.ancestorConfigRoots;
|
|
594
|
+
if (roots !== undefined) configured = roots;
|
|
595
|
+
}
|
|
596
|
+
if (configured === undefined || (Array.isArray(configured) && configured.length === 0)) return undefined;
|
|
597
|
+
if (!Array.isArray(configured)) {
|
|
598
|
+
console.warn("Invalid settings.ancestorConfigRoots: expected an array of paths");
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const home = getConfigPathIdentity(resolve(homedir()));
|
|
603
|
+
const canonicalCwd = getConfigPathIdentity(resolve(cwd));
|
|
604
|
+
const valid: string[] = [];
|
|
605
|
+
for (const entry of configured) {
|
|
606
|
+
const expanded = typeof entry === "string" && entry.startsWith("~/")
|
|
607
|
+
? join(homedir(), entry.slice(2))
|
|
608
|
+
: entry;
|
|
609
|
+
if (typeof expanded !== "string" || !isAbsolute(expanded)) {
|
|
610
|
+
console.warn(`Invalid settings.ancestorConfigRoots entry ${JSON.stringify(entry)}: expected an absolute path or ~/...`);
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const root = realpathSync(expanded);
|
|
615
|
+
if (!statSync(root).isDirectory() || !isWithin(home, root) || !isWithin(root, canonicalCwd)) throw new Error();
|
|
616
|
+
valid.push(root);
|
|
617
|
+
} catch {
|
|
618
|
+
console.warn(`Invalid settings.ancestorConfigRoots entry ${JSON.stringify(entry)}: expected an existing directory under HOME containing cwd`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
return valid.sort((left, right) => right.length - left.length)[0];
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
function getAncestorProjectDirs(cwd: string, root: string): string[] {
|
|
625
|
+
const start = getConfigPathIdentity(resolve(cwd));
|
|
626
|
+
const dirs: string[] = [];
|
|
627
|
+
let current = dirname(start);
|
|
628
|
+
while (isWithin(root, current)) {
|
|
629
|
+
dirs.unshift(current);
|
|
630
|
+
if (current === root) break;
|
|
631
|
+
const parent = dirname(current);
|
|
632
|
+
if (parent === current) break;
|
|
633
|
+
current = parent;
|
|
634
|
+
}
|
|
635
|
+
return dirs;
|
|
504
636
|
}
|
|
505
637
|
|
|
506
|
-
function
|
|
507
|
-
return
|
|
638
|
+
function isExclusiveConfigMode(): boolean {
|
|
639
|
+
return process.env.PI_MCP_CONFIG_MODE?.trim().toLowerCase() === "exclusive";
|
|
508
640
|
}
|
|
509
641
|
|
|
510
642
|
function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
511
643
|
const imports = mergeImports(base.imports, next.imports);
|
|
512
644
|
const settings = next.settings ? { ...base.settings, ...next.settings } : base.settings;
|
|
645
|
+
const claudePlugins = next.claudePlugins ?? base.claudePlugins;
|
|
513
646
|
return {
|
|
514
647
|
mcpServers: mergeServerMaps(base.mcpServers, next.mcpServers),
|
|
515
648
|
...(imports !== undefined ? { imports } : {}),
|
|
516
649
|
...(settings !== undefined ? { settings } : {}),
|
|
650
|
+
...(claudePlugins !== undefined ? { claudePlugins } : {}),
|
|
517
651
|
};
|
|
518
652
|
}
|
|
519
653
|
|
|
@@ -522,7 +656,7 @@ function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
|
522
656
|
// different url, these MUST NOT be inherited from the lower-precedence entry —
|
|
523
657
|
// otherwise the original endpoint's credentials would be shipped to the new
|
|
524
658
|
// url. See the SECURITY note in mergeServerMaps.
|
|
525
|
-
const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "bearerTokenStore", "requestHeadersCommand"] as const;
|
|
659
|
+
const URL_BOUND_AUTH_FIELDS = ["headers", "bearerToken", "bearerTokenEnv", "bearerTokenStore", "requestHeadersCommand", "caFile"] as const;
|
|
526
660
|
|
|
527
661
|
function mergeServerMaps(
|
|
528
662
|
base: Record<string, ServerEntry>,
|
|
@@ -545,24 +679,24 @@ function mergeServerMaps(
|
|
|
545
679
|
if (existing && typeof definition.command === "string") {
|
|
546
680
|
baseEntry = { ...existing };
|
|
547
681
|
for (const field of [
|
|
548
|
-
"url", "headers", "requestHeadersCommand", "auth", "bearerToken",
|
|
549
|
-
"bearerTokenEnv", "oauth", "httpTransport", "socket",
|
|
682
|
+
"url", "headers", "requestHeadersCommand", "caFile", "auth", "bearerToken",
|
|
683
|
+
"bearerTokenEnv", "bearerTokenStore", "oauth", "httpTransport", "socket",
|
|
550
684
|
] as const) {
|
|
551
685
|
delete baseEntry[field];
|
|
552
686
|
}
|
|
553
687
|
} else if (existing && typeof definition.url === "string") {
|
|
554
688
|
baseEntry = { ...existing };
|
|
555
689
|
for (const field of [
|
|
556
|
-
"command", "args", "env", "cwd", "pluginDataDir", "literalEnv", "socket",
|
|
690
|
+
"command", "args", "env", "cwd", "pluginDataDir", "literalEnv", "inheritEnv", "socket",
|
|
557
691
|
] as const) {
|
|
558
692
|
delete baseEntry[field];
|
|
559
693
|
}
|
|
560
694
|
} else if (existing && typeof definition.socket === "string") {
|
|
561
695
|
baseEntry = { ...existing };
|
|
562
696
|
for (const field of [
|
|
563
|
-
"command", "args", "env", "cwd", "pluginDataDir", "literalEnv", "url",
|
|
564
|
-
"headers", "requestHeadersCommand", "auth", "bearerToken", "bearerTokenEnv",
|
|
565
|
-
"oauth", "httpTransport",
|
|
697
|
+
"command", "args", "env", "cwd", "pluginDataDir", "literalEnv", "inheritEnv", "url",
|
|
698
|
+
"headers", "requestHeadersCommand", "caFile", "auth", "bearerToken", "bearerTokenEnv",
|
|
699
|
+
"bearerTokenStore", "oauth", "httpTransport",
|
|
566
700
|
] as const) {
|
|
567
701
|
delete baseEntry[field];
|
|
568
702
|
}
|
|
@@ -576,7 +710,11 @@ function mergeServerMaps(
|
|
|
576
710
|
delete baseEntry.oauth;
|
|
577
711
|
}
|
|
578
712
|
}
|
|
579
|
-
|
|
713
|
+
if (existing && Object.hasOwn(definition, "env") && isBuiltInAgentPlugin(existing, "env") && !Object.hasOwn(definition, "literalEnv")) {
|
|
714
|
+
if (baseEntry === existing) baseEntry = { ...existing };
|
|
715
|
+
delete baseEntry.literalEnv;
|
|
716
|
+
}
|
|
717
|
+
merged[name] = mergeBuiltInAgentPluginEntries(baseEntry, definition);
|
|
580
718
|
}
|
|
581
719
|
return merged;
|
|
582
720
|
}
|
|
@@ -606,6 +744,7 @@ function expandImports(config: McpConfig, cwd = process.cwd()): McpConfig {
|
|
|
606
744
|
return {
|
|
607
745
|
imports: config.imports,
|
|
608
746
|
...(config.settings !== undefined ? { settings: config.settings } : {}),
|
|
747
|
+
...(config.claudePlugins !== undefined ? { claudePlugins: config.claudePlugins } : {}),
|
|
609
748
|
mcpServers: mergeServerMaps(importedServers, config.mcpServers),
|
|
610
749
|
};
|
|
611
750
|
}
|
|
@@ -638,13 +777,9 @@ function resolveImportCandidates(importKind: ImportKind, cwd: string): string[]
|
|
|
638
777
|
});
|
|
639
778
|
}
|
|
640
779
|
|
|
641
|
-
function parseJsonConfig(raw: string): unknown {
|
|
642
|
-
return JSON.parse(stripJsonComments(raw, { trailingCommas: true }));
|
|
643
|
-
}
|
|
644
|
-
|
|
645
780
|
function readImportedConfig(path: string): unknown {
|
|
646
781
|
const raw = readFileSync(path, "utf-8");
|
|
647
|
-
return path.endsWith(".toml") ? parseToml(raw) :
|
|
782
|
+
return path.endsWith(".toml") ? parseToml(raw) : parseJsonWithComments(raw);
|
|
648
783
|
}
|
|
649
784
|
|
|
650
785
|
function loadImportedConfig(
|
|
@@ -694,7 +829,9 @@ function readValidatedConfig(path: string, label: string): McpConfig | null {
|
|
|
694
829
|
if (!existsSync(path)) return null;
|
|
695
830
|
|
|
696
831
|
try {
|
|
697
|
-
|
|
832
|
+
const text = readFileSync(path, "utf-8");
|
|
833
|
+
if (stripJsonComments(text, { trailingCommas: true }).trim() === "") return null;
|
|
834
|
+
return validateConfig(parseJsonWithComments(text));
|
|
698
835
|
} catch (error) {
|
|
699
836
|
console.warn(`Failed to load ${label}:`, error);
|
|
700
837
|
return null;
|
|
@@ -710,9 +847,39 @@ function validateConfig(raw: unknown): McpConfig {
|
|
|
710
847
|
mcpServers: toServerEntries(raw.mcpServers ?? raw["mcp-servers"]),
|
|
711
848
|
...(Array.isArray(raw.imports) ? { imports: raw.imports as ImportKind[] } : {}),
|
|
712
849
|
...(raw.settings !== undefined ? { settings: raw.settings as McpSettings } : {}),
|
|
850
|
+
...(raw.claudePlugins !== undefined ? { claudePlugins: parseClaudePlugins(raw.claudePlugins) } : {}),
|
|
713
851
|
};
|
|
714
852
|
}
|
|
715
853
|
|
|
854
|
+
function parseClaudePlugins(value: unknown): ClaudePluginConfig[] {
|
|
855
|
+
if (!Array.isArray(value)) {
|
|
856
|
+
console.warn("Invalid claudePlugins config: expected an array");
|
|
857
|
+
return [];
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const plugins: ClaudePluginConfig[] = [];
|
|
861
|
+
for (const [index, entry] of value.entries()) {
|
|
862
|
+
if (!isRecord(entry) || typeof entry.path !== "string" || entry.path.trim().length === 0) {
|
|
863
|
+
console.warn(`Invalid claudePlugins[${index}]: expected an object with a non-empty path`);
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
if ((entry.mcp !== undefined && typeof entry.mcp !== "boolean") || (entry.skills !== undefined && typeof entry.skills !== "boolean")) {
|
|
867
|
+
console.warn(`Invalid claudePlugins[${index}] for ${entry.path}: mcp and skills must be booleans`);
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
870
|
+
if (entry.mcp !== true && entry.skills !== true) {
|
|
871
|
+
console.warn(`Invalid claudePlugins[${index}] for ${entry.path}: enable mcp, skills, or both`);
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
plugins.push({
|
|
875
|
+
path: entry.path,
|
|
876
|
+
...(entry.mcp !== undefined ? { mcp: entry.mcp } : {}),
|
|
877
|
+
...(entry.skills !== undefined ? { skills: entry.skills } : {}),
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
return plugins;
|
|
881
|
+
}
|
|
882
|
+
|
|
716
883
|
function toServerEntries(servers: unknown): Record<string, ServerEntry> {
|
|
717
884
|
if (!isRecord(servers)) return {};
|
|
718
885
|
const entries: Record<string, ServerEntry> = {};
|
|
@@ -850,6 +1017,7 @@ function extractServers(config: unknown, kind: ImportKind): Record<string, Serve
|
|
|
850
1017
|
mapped.oauth = {
|
|
851
1018
|
...(typeof oauth.clientId === "string" ? { clientId: oauth.clientId } : {}),
|
|
852
1019
|
...(typeof oauth.clientSecret === "string" ? { clientSecret: oauth.clientSecret } : {}),
|
|
1020
|
+
...(typeof oauth.clientMetadataUrl === "string" ? { clientMetadataUrl: oauth.clientMetadataUrl } : {}),
|
|
853
1021
|
...(typeof oauth.scope === "string" ? { scope: oauth.scope } : {}),
|
|
854
1022
|
...(typeof oauth.authServerMetadataUrl === "string" ? { authServerMetadataUrl: oauth.authServerMetadataUrl } : {}),
|
|
855
1023
|
...(typeof oauth.skipIssuerMetadataValidation === "boolean"
|
|
@@ -964,7 +1132,7 @@ function readRawConfigObject(filePath: string): Record<string, unknown> {
|
|
|
964
1132
|
if (!existsSync(filePath)) return {};
|
|
965
1133
|
|
|
966
1134
|
try {
|
|
967
|
-
const raw =
|
|
1135
|
+
const raw = parseJsonWithComments(readFileSync(filePath, "utf-8"));
|
|
968
1136
|
return raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
969
1137
|
} catch {
|
|
970
1138
|
return {};
|
|
@@ -1011,7 +1179,7 @@ export function writeProjectServerDisabledOverride(
|
|
|
1011
1179
|
let raw: Record<string, unknown> = {};
|
|
1012
1180
|
if (existsSync(filePath)) {
|
|
1013
1181
|
try {
|
|
1014
|
-
const parsed =
|
|
1182
|
+
const parsed = parseJsonWithComments(readFileSync(filePath, "utf-8"));
|
|
1015
1183
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1016
1184
|
throw new Error("root value must be an object");
|
|
1017
1185
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConsentError } from "./errors.ts";
|
|
2
2
|
import { logger } from "./logger.ts";
|
|
3
|
+
import type { SessionApprovalWriter } from "./session-approvals.ts";
|
|
3
4
|
|
|
4
5
|
export type ToolConsentMode = "never" | "once-per-server" | "always";
|
|
5
6
|
|
|
@@ -8,7 +9,10 @@ export class ConsentManager {
|
|
|
8
9
|
private deniedServers = new Set<string>();
|
|
9
10
|
private log = logger.child({ component: "ConsentManager" });
|
|
10
11
|
|
|
11
|
-
constructor(
|
|
12
|
+
constructor(
|
|
13
|
+
private mode: ToolConsentMode = "once-per-server",
|
|
14
|
+
private persistDecision?: SessionApprovalWriter,
|
|
15
|
+
) {
|
|
12
16
|
this.log.debug("Initialized", { mode });
|
|
13
17
|
}
|
|
14
18
|
|
|
@@ -24,17 +28,30 @@ export class ConsentManager {
|
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
registerDecision(serverName: string, approved: boolean): void {
|
|
27
|
-
this.
|
|
28
|
-
|
|
31
|
+
this.applyDecision(serverName, approved);
|
|
32
|
+
try {
|
|
33
|
+
this.persistDecision?.({
|
|
34
|
+
version: 1,
|
|
35
|
+
kind: "iframe",
|
|
36
|
+
decision: approved ? "allow" : "deny",
|
|
37
|
+
serverName,
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
41
|
+
this.log.debug("Failed to persist consent decision", { server: serverName, error: detail });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
29
44
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
/** Apply a persisted decision without writing it back to the session. */
|
|
46
|
+
restoreDecision(serverName: string, approved: boolean): void {
|
|
47
|
+
if (this.mode === "always" && approved) {
|
|
48
|
+
// Restored "always" grants cannot be reused. A later grant still
|
|
49
|
+
// overrides an earlier denial.
|
|
50
|
+
this.approvedServers.delete(serverName);
|
|
51
|
+
this.deniedServers.delete(serverName);
|
|
33
52
|
return;
|
|
34
53
|
}
|
|
35
|
-
|
|
36
|
-
this.deniedServers.add(serverName);
|
|
37
|
-
this.log.debug("Consent denied", { server: serverName });
|
|
54
|
+
this.applyDecision(serverName, approved);
|
|
38
55
|
}
|
|
39
56
|
|
|
40
57
|
ensureApproved(serverName: string): void {
|
|
@@ -61,4 +78,18 @@ export class ConsentManager {
|
|
|
61
78
|
this.deniedServers.clear();
|
|
62
79
|
this.log.debug("Cleared all consent records");
|
|
63
80
|
}
|
|
81
|
+
|
|
82
|
+
private applyDecision(serverName: string, approved: boolean): void {
|
|
83
|
+
this.deniedServers.delete(serverName);
|
|
84
|
+
this.approvedServers.delete(serverName);
|
|
85
|
+
|
|
86
|
+
if (approved) {
|
|
87
|
+
this.approvedServers.add(serverName);
|
|
88
|
+
this.log.debug("Consent granted", { server: serverName });
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.deniedServers.add(serverName);
|
|
93
|
+
this.log.debug("Consent denied", { server: serverName });
|
|
94
|
+
}
|
|
64
95
|
}
|