pi-subagents 0.57.0 → 0.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +104 -0
- package/docs/agents.md +18 -9
- package/docs/configuration.md +4 -4
- package/docs/extension-api.md +40 -1
- package/docs/models.md +27 -4
- package/docs/observability.md +1 -1
- package/docs/tool-reference.md +92 -6
- package/docs/workflows.md +115 -0
- package/package.json +3 -1
- package/prompts/review-loop.md +2 -2
- package/skills/council-mode/SKILL.md +1 -1
- package/skills/pi-subagents/SKILL.md +3 -1
- package/skills/pi-subagents/references/execution-controls.md +6 -2
- package/skills/pi-subagents/references/management-authoring-rpc.md +2 -2
- package/skills/pi-subagents/references/prompting-and-roles.md +26 -5
- package/src/agents/agent-management.ts +28 -17
- package/src/agents/agent-serializer.ts +9 -2
- package/src/agents/agents.ts +103 -27
- package/src/agents/runtime-agent-events.ts +70 -0
- package/src/agents/runtime-agent-registry.ts +19 -18
- package/src/api/agents.ts +10 -5
- package/src/api/background-work.ts +5 -1
- package/src/api/delegation.ts +0 -7
- package/src/api/preflight.ts +32 -10
- package/src/extension/fanout-child.ts +5 -3
- package/src/extension/index.ts +66 -27
- package/src/extension/public-execution.ts +15 -2
- package/src/extension/rpc.ts +35 -14
- package/src/extension/schemas.ts +37 -12
- package/src/extension/tool-description.ts +25 -6
- package/src/integrations/herdr-status.ts +5 -0
- package/src/intercom/result-intercom.ts +2 -0
- package/src/profiles/profiles.ts +5 -6
- package/src/runs/background/active-async-capacity.ts +2 -2
- package/src/runs/background/async-execution.ts +78 -46
- package/src/runs/background/async-job-tracker.ts +14 -12
- package/src/runs/background/async-resume.ts +29 -27
- package/src/runs/background/async-status-snapshot.ts +23 -261
- package/src/runs/background/async-status.ts +68 -7
- package/src/runs/background/chain-append.ts +8 -3
- package/src/runs/background/chain-root-attachment.ts +60 -8
- package/src/runs/background/fleet-view.ts +21 -11
- package/src/runs/background/notify.ts +184 -9
- package/src/runs/background/result-delivery-ownership.ts +45 -0
- package/src/runs/background/result-files.ts +2 -1
- package/src/runs/background/result-watcher.ts +29 -10
- package/src/runs/background/resume-guidance.ts +1 -1
- package/src/runs/background/retained-children.ts +1 -1
- package/src/runs/background/run-status.ts +18 -8
- package/src/runs/background/scheduled-runs.ts +86 -7
- package/src/runs/background/stale-run-reconciler.ts +10 -4
- package/src/runs/background/steering.ts +4 -14
- package/src/runs/background/subagent-runner.ts +389 -350
- package/src/runs/background/subagent-wait.ts +58 -10
- package/src/runs/background/terminal-run-index.ts +1 -1
- package/src/runs/background/wait-completions.ts +22 -1
- package/src/runs/background/wait-config.ts +23 -9
- package/src/runs/background/wait-tool.ts +9 -2
- package/src/runs/foreground/async-steering-action.ts +2 -2
- package/src/runs/foreground/execution.ts +134 -121
- package/src/runs/foreground/foreground-control.ts +3 -0
- package/src/runs/foreground/foreground-history.ts +1 -0
- package/src/runs/foreground/subagent-executor.ts +527 -257
- package/src/runs/foreground/workflow-detach-reconcile.ts +130 -120
- package/src/runs/shared/abort-recovery.ts +119 -0
- package/src/runs/shared/async-status-projection.ts +463 -0
- package/src/runs/shared/child-identity.ts +19 -4
- package/src/runs/shared/child-launch-plan.ts +151 -0
- package/src/runs/shared/completion-evidence.ts +89 -0
- package/src/runs/shared/completion-guard.ts +5 -4
- package/src/runs/shared/dynamic-fanout.ts +3 -3
- package/src/runs/shared/fast-mode-extension.ts +5 -5
- package/src/runs/shared/host-step-status.ts +230 -0
- package/src/runs/shared/lane-metadata.ts +105 -0
- package/src/runs/shared/launch-cwd.ts +16 -0
- package/src/runs/shared/long-running-guard.ts +2 -1
- package/src/runs/shared/mcp-config-sources.ts +422 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +221 -158
- package/src/runs/shared/mcp-direct-tool-grant.ts +197 -0
- package/src/runs/shared/model-exclusions.ts +12 -0
- package/src/runs/shared/model-fallback.ts +25 -6
- package/src/runs/shared/nested-events.ts +6 -2
- package/src/runs/shared/nested-render.ts +7 -3
- package/src/runs/shared/parallel-handoff.ts +419 -7
- package/src/runs/shared/parallel-utils.ts +15 -1
- package/src/runs/shared/pi-args.ts +81 -8
- package/src/runs/shared/single-output.ts +44 -4
- package/src/runs/shared/subagent-prompt-runtime.ts +98 -13
- package/src/runs/shared/worktree-cleanup-plan.ts +847 -0
- package/src/runs/shared/worktree.ts +18 -0
- package/src/shared/child-session-name.ts +46 -0
- package/src/shared/extension-context.ts +24 -0
- package/src/shared/formatters.ts +11 -2
- package/src/shared/launch-contract.ts +5 -1
- package/src/shared/settings.ts +9 -103
- package/src/shared/types.ts +240 -30
- package/src/shared/utils.ts +41 -84
- package/src/slash/delegation-adapters.ts +1 -8
- package/src/slash/delegation-request.ts +0 -4
- package/src/slash/slash-bridge.ts +1 -2
- package/src/slash/slash-commands.ts +369 -90
- package/src/slash/slash-live-state.ts +22 -11
- package/src/slash/subagents-admin.ts +3 -0
- package/src/tui/fleet-status.ts +125 -74
- package/src/tui/fleet.ts +11 -5
- package/src/tui/render.ts +330 -33
- package/src/watchdog/turn-delta.ts +1 -1
- package/src/workflows/chat-progress.ts +6 -3
- package/src/workflows/host-command.ts +230 -0
- package/src/workflows/scripted-workflow.ts +496 -35
- package/src/workflows/workflow-child-summary.ts +9 -5
- package/src/workflows/workflow-preflight.ts +270 -0
- package/src/workflows/workflow-receipt.ts +79 -5
- package/src/workflows/workflow-settlement.ts +246 -0
- package/src/runs/shared/turn-budget.ts +0 -98
|
@@ -2,11 +2,21 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as os from "node:os";
|
|
4
4
|
import * as path from "node:path";
|
|
5
|
+
import { findConfiguredProjectRoot } from "../../agents/agents.ts";
|
|
5
6
|
import { getAgentDir, getProjectConfigDir } from "../../shared/utils.ts";
|
|
7
|
+
import { isMcpServerDefinition, loadAgentPluginMcpServers, loadPackageMcpServers, type McpServerDefinition } from "./mcp-config-sources.ts";
|
|
8
|
+
import {
|
|
9
|
+
normalizeMcpDirectToolSelectors,
|
|
10
|
+
parseMcpDirectToolSelectors,
|
|
11
|
+
planMcpDirectToolGrant,
|
|
12
|
+
} from "./mcp-direct-tool-grant.ts";
|
|
13
|
+
import type { McpToolPrefix, ResolvedMcpDirectToolSelection } from "./mcp-direct-tool-grant.ts";
|
|
14
|
+
|
|
15
|
+
export { formatUnresolvedMcpDirectToolSelectors } from "./mcp-direct-tool-grant.ts";
|
|
16
|
+
export type { ResolvedMcpDirectToolSelection } from "./mcp-direct-tool-grant.ts";
|
|
6
17
|
|
|
7
18
|
const CACHE_VERSION = 1;
|
|
8
19
|
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
9
|
-
const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
10
20
|
const GENERIC_GLOBAL_CONFIG_PATH = path.join(os.homedir(), ".config", "mcp", "mcp.json");
|
|
11
21
|
const IMPORT_PATHS = {
|
|
12
22
|
cursor: [path.join(os.homedir(), ".cursor", "mcp.json")],
|
|
@@ -21,39 +31,41 @@ const IMPORT_PATHS = {
|
|
|
21
31
|
vscode: [".vscode/mcp.json"],
|
|
22
32
|
} as const;
|
|
23
33
|
|
|
24
|
-
type ToolPrefix = "server" | "none" | "short";
|
|
25
34
|
type ImportKind = keyof typeof IMPORT_PATHS;
|
|
26
35
|
|
|
27
|
-
|
|
28
|
-
command?: string;
|
|
29
|
-
args?: string[];
|
|
30
|
-
socket?: string;
|
|
31
|
-
env?: Record<string, string>;
|
|
32
|
-
cwd?: string;
|
|
33
|
-
url?: string;
|
|
34
|
-
headers?: Record<string, string>;
|
|
35
|
-
requestHeadersCommand?: {
|
|
36
|
-
command: string;
|
|
37
|
-
args?: string[];
|
|
38
|
-
env?: Record<string, string>;
|
|
39
|
-
timeoutMs?: number;
|
|
40
|
-
};
|
|
41
|
-
auth?: "oauth" | "bearer" | false;
|
|
42
|
-
bearerToken?: string;
|
|
43
|
-
bearerTokenEnv?: string;
|
|
44
|
-
exposeResources?: boolean;
|
|
45
|
-
includeTools?: string[];
|
|
46
|
-
excludeTools?: string[];
|
|
47
|
-
protocolVersion?: string;
|
|
48
|
-
directTools?: boolean | string[];
|
|
49
|
-
}
|
|
36
|
+
type ServerEntry = McpServerDefinition;
|
|
50
37
|
|
|
51
|
-
interface McpConfig {
|
|
38
|
+
export interface McpConfig {
|
|
52
39
|
mcpServers: Record<string, ServerEntry>;
|
|
53
40
|
imports?: ImportKind[];
|
|
54
41
|
settings?: {
|
|
55
|
-
toolPrefix?:
|
|
42
|
+
toolPrefix?: McpToolPrefix;
|
|
56
43
|
directTools?: boolean;
|
|
44
|
+
agentPluginPaths?: unknown;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const MCP_RUNTIME_SNAPSHOT_EVENT = "pi-mcp-adapter:runtime-snapshot:v1" as const;
|
|
49
|
+
export const MCP_RUNTIME_SNAPSHOT_VERSION = 1 as const;
|
|
50
|
+
|
|
51
|
+
export interface McpRuntimeServerSnapshot {
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly definition: ServerEntry;
|
|
54
|
+
readonly runtime: true;
|
|
55
|
+
readonly persisted: false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
interface McpRuntimeSnapshotRequest {
|
|
59
|
+
version: typeof MCP_RUNTIME_SNAPSHOT_VERSION;
|
|
60
|
+
name: string;
|
|
61
|
+
result?:
|
|
62
|
+
| { ok: true; snapshot: McpRuntimeServerSnapshot }
|
|
63
|
+
| { ok: false; error: Error };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface McpRuntimeSnapshotHost {
|
|
67
|
+
events: {
|
|
68
|
+
emit(event: string, request: McpRuntimeSnapshotRequest): void;
|
|
57
69
|
};
|
|
58
70
|
}
|
|
59
71
|
|
|
@@ -78,19 +90,59 @@ interface MetadataCache {
|
|
|
78
90
|
servers: Record<string, ServerCacheEntry>;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
export interface
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
export interface McpDirectToolResolution {
|
|
94
|
+
selections: ResolvedMcpDirectToolSelection[];
|
|
95
|
+
unresolvedSelectors: string[];
|
|
96
|
+
/** Normal loaded config plus the selected runtime server definitions. */
|
|
97
|
+
mcpConfig?: McpConfig;
|
|
98
|
+
runtimeServerNames?: string[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function resolveMcpDirectToolResolution(
|
|
102
|
+
mcpDirectTools: string[] | undefined,
|
|
103
|
+
cwd = process.cwd(),
|
|
104
|
+
runtimeSnapshotHost?: McpRuntimeSnapshotHost,
|
|
105
|
+
configOverride?: McpConfig,
|
|
106
|
+
): McpDirectToolResolution {
|
|
107
|
+
const selectors = normalizeMcpDirectToolSelectors(mcpDirectTools);
|
|
108
|
+
if (selectors.length === 0) return { selections: [], unresolvedSelectors: [] };
|
|
109
|
+
|
|
110
|
+
const config = configOverride ?? loadMcpConfig(cwd);
|
|
111
|
+
const { servers: selectedServers, tools: selectedTools } = parseMcpDirectToolSelectors(selectors);
|
|
112
|
+
const runtimeSelectionServers = new Set([...selectedServers, ...selectedTools.keys()]);
|
|
113
|
+
const runtimeServers = resolveRuntimeMcpServers(config, runtimeSelectionServers, runtimeSnapshotHost);
|
|
114
|
+
const resolvedConfig = Object.keys(runtimeServers).length > 0
|
|
115
|
+
? mergeConfigs(config, { mcpServers: runtimeServers })
|
|
116
|
+
: config;
|
|
117
|
+
validateSelectedServerDefinitions(resolvedConfig, selectors);
|
|
118
|
+
const cache = loadMetadataCache();
|
|
119
|
+
if (!cache) return { selections: [], unresolvedSelectors: selectors };
|
|
120
|
+
const validMetadata: Record<string, ServerCacheEntry> = {};
|
|
121
|
+
for (const [serverName, definition] of Object.entries(resolvedConfig.mcpServers)) {
|
|
122
|
+
const serverCache = cache.servers[serverName];
|
|
123
|
+
if (isServerCacheValid(serverCache, definition)) validMetadata[serverName] = serverCache;
|
|
93
124
|
}
|
|
125
|
+
const grant = planMcpDirectToolGrant({
|
|
126
|
+
selectors,
|
|
127
|
+
servers: resolvedConfig.mcpServers,
|
|
128
|
+
metadata: validMetadata,
|
|
129
|
+
toolPrefix: resolvedConfig.settings?.toolPrefix,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
selections: grant.selections,
|
|
133
|
+
unresolvedSelectors: grant.unresolvedSelectors,
|
|
134
|
+
...(Object.keys(runtimeServers).length > 0
|
|
135
|
+
? { mcpConfig: resolvedConfig, runtimeServerNames: Object.keys(runtimeServers) }
|
|
136
|
+
: {}),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function resolveMcpDirectToolSelections(
|
|
141
|
+
mcpDirectTools: string[] | undefined,
|
|
142
|
+
cwd = process.cwd(),
|
|
143
|
+
runtimeSnapshotHost?: McpRuntimeSnapshotHost,
|
|
144
|
+
): ResolvedMcpDirectToolSelection[] {
|
|
145
|
+
return resolveMcpDirectToolResolution(mcpDirectTools, cwd, runtimeSnapshotHost).selections;
|
|
94
146
|
}
|
|
95
147
|
|
|
96
148
|
function loadMetadataCache(): MetadataCache | null {
|
|
@@ -102,28 +154,116 @@ function loadMetadataCache(): MetadataCache | null {
|
|
|
102
154
|
return null;
|
|
103
155
|
}
|
|
104
156
|
|
|
105
|
-
if (!parsed ||
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
if (!isRecord(parsed) || parsed.version !== CACHE_VERSION || !isRecord(parsed.servers)) return null;
|
|
158
|
+
const servers: Record<string, ServerCacheEntry> = {};
|
|
159
|
+
for (const [name, value] of Object.entries(parsed.servers)) {
|
|
160
|
+
const entry = parseServerCacheEntry(value);
|
|
161
|
+
if (entry) servers[name] = entry;
|
|
162
|
+
}
|
|
163
|
+
return { version: CACHE_VERSION, servers };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function parseServerCacheEntry(value: unknown): ServerCacheEntry | undefined {
|
|
167
|
+
if (!isRecord(value)) return undefined;
|
|
168
|
+
if (value.configHash !== undefined && typeof value.configHash !== "string") return undefined;
|
|
169
|
+
if (value.cachedAt !== undefined && (typeof value.cachedAt !== "number" || !Number.isFinite(value.cachedAt))) return undefined;
|
|
170
|
+
if (value.tools !== undefined && !Array.isArray(value.tools)) return undefined;
|
|
171
|
+
if (value.resources !== undefined && !Array.isArray(value.resources)) return undefined;
|
|
172
|
+
const tools = value.tools?.map(parseCachedTool);
|
|
173
|
+
const resources = value.resources?.map(parseCachedResource);
|
|
174
|
+
if (tools && !tools.every((entry): entry is CachedTool => entry !== undefined)) return undefined;
|
|
175
|
+
if (resources && !resources.every((entry): entry is CachedResource => entry !== undefined)) return undefined;
|
|
176
|
+
return {
|
|
177
|
+
...(value.configHash !== undefined ? { configHash: value.configHash } : {}),
|
|
178
|
+
...(value.cachedAt !== undefined ? { cachedAt: value.cachedAt } : {}),
|
|
179
|
+
...(tools ? { tools } : {}),
|
|
180
|
+
...(resources ? { resources } : {}),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function parseCachedTool(value: unknown): CachedTool | undefined {
|
|
185
|
+
if (!isRecord(value) || (value.name !== undefined && typeof value.name !== "string")) return undefined;
|
|
186
|
+
return value.name === undefined ? {} : { name: value.name };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function parseCachedResource(value: unknown): CachedResource | undefined {
|
|
190
|
+
if (!isRecord(value)) return undefined;
|
|
191
|
+
if (value.uri !== undefined && typeof value.uri !== "string") return undefined;
|
|
192
|
+
if (value.name !== undefined && typeof value.name !== "string") return undefined;
|
|
193
|
+
return {
|
|
194
|
+
...(value.uri !== undefined ? { uri: value.uri } : {}),
|
|
195
|
+
...(value.name !== undefined ? { name: value.name } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function resolveRuntimeMcpServers(
|
|
200
|
+
config: McpConfig,
|
|
201
|
+
selectedServers: ReadonlySet<string>,
|
|
202
|
+
runtimeSnapshotHost: McpRuntimeSnapshotHost | undefined,
|
|
203
|
+
): Record<string, ServerEntry> {
|
|
204
|
+
if (!runtimeSnapshotHost) return {};
|
|
205
|
+
const runtimeServers: Record<string, ServerEntry> = {};
|
|
206
|
+
for (const serverName of selectedServers) {
|
|
207
|
+
if (Object.hasOwn(config.mcpServers, serverName)) continue;
|
|
208
|
+
try {
|
|
209
|
+
const snapshot = getRuntimeMcpServerSnapshot(runtimeSnapshotHost, serverName);
|
|
210
|
+
runtimeServers[serverName] = snapshot.definition;
|
|
211
|
+
} catch {
|
|
212
|
+
// Missing, disposed, or shadowed runtime servers remain unresolved below.
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return runtimeServers;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getRuntimeMcpServerSnapshot(
|
|
219
|
+
host: McpRuntimeSnapshotHost,
|
|
220
|
+
name: string,
|
|
221
|
+
): McpRuntimeServerSnapshot {
|
|
222
|
+
const request: McpRuntimeSnapshotRequest = {
|
|
223
|
+
version: MCP_RUNTIME_SNAPSHOT_VERSION,
|
|
224
|
+
name,
|
|
225
|
+
};
|
|
226
|
+
host.events.emit(MCP_RUNTIME_SNAPSHOT_EVENT, request);
|
|
227
|
+
if (!request.result) throw new Error("pi-mcp-adapter is not installed for this Pi instance");
|
|
228
|
+
if (!request.result.ok) throw request.result.error;
|
|
229
|
+
const snapshot = request.result.snapshot;
|
|
230
|
+
if (
|
|
231
|
+
!snapshot ||
|
|
232
|
+
snapshot.name !== name ||
|
|
233
|
+
snapshot.runtime !== true ||
|
|
234
|
+
snapshot.persisted !== false ||
|
|
235
|
+
!isMcpServerDefinition(snapshot.definition)
|
|
236
|
+
) {
|
|
237
|
+
throw new Error(`Invalid MCP runtime snapshot for server "${name}"`);
|
|
109
238
|
}
|
|
110
|
-
return
|
|
239
|
+
return snapshot;
|
|
111
240
|
}
|
|
112
241
|
|
|
113
242
|
function loadMcpConfig(cwd: string): McpConfig {
|
|
243
|
+
const resolvedCwd = path.resolve(cwd);
|
|
244
|
+
const projectRoot = findConfiguredProjectRoot(resolvedCwd) ?? resolvedCwd;
|
|
114
245
|
let config: McpConfig = { mcpServers: {} };
|
|
115
|
-
for (const sourcePath of getConfigPaths(
|
|
246
|
+
for (const sourcePath of getConfigPaths(projectRoot)) {
|
|
116
247
|
const loaded = readConfig(sourcePath);
|
|
117
248
|
if (!loaded) continue;
|
|
118
|
-
config = mergeConfigs(config, expandImports(loaded,
|
|
249
|
+
config = mergeConfigs(config, expandImports(loaded, projectRoot));
|
|
119
250
|
}
|
|
120
|
-
|
|
251
|
+
|
|
252
|
+
const packageServers = loadPackageMcpServers(projectRoot);
|
|
253
|
+
const pluginServers = loadAgentPluginMcpServers(config.settings?.agentPluginPaths, projectRoot);
|
|
254
|
+
const packageOnlyServers = Object.fromEntries(
|
|
255
|
+
Object.entries(packageServers).filter(([name]) => !Object.hasOwn(pluginServers, name)),
|
|
256
|
+
);
|
|
257
|
+
return mergeConfigs(
|
|
258
|
+
{ mcpServers: packageOnlyServers },
|
|
259
|
+
mergeConfigs({ mcpServers: pluginServers }, config),
|
|
260
|
+
);
|
|
121
261
|
}
|
|
122
262
|
|
|
123
|
-
function getConfigPaths(
|
|
263
|
+
function getConfigPaths(projectRoot: string): string[] {
|
|
124
264
|
const piGlobalPath = path.join(getAgentDir(), "mcp.json");
|
|
125
|
-
const projectPath = path.resolve(
|
|
126
|
-
const projectPiPath = path.resolve(getProjectConfigDir(
|
|
265
|
+
const projectPath = path.resolve(projectRoot, ".mcp.json");
|
|
266
|
+
const projectPiPath = path.resolve(getProjectConfigDir(projectRoot), "mcp.json");
|
|
127
267
|
const sources: string[] = [];
|
|
128
268
|
if (GENERIC_GLOBAL_CONFIG_PATH !== piGlobalPath) sources.push(GENERIC_GLOBAL_CONFIG_PATH);
|
|
129
269
|
sources.push(piGlobalPath);
|
|
@@ -143,18 +283,33 @@ function readConfig(configPath: string): McpConfig | null {
|
|
|
143
283
|
}
|
|
144
284
|
|
|
145
285
|
function validateConfig(raw: unknown): McpConfig {
|
|
146
|
-
if (!
|
|
147
|
-
const obj = raw
|
|
286
|
+
if (!isRecord(raw)) return { mcpServers: {} };
|
|
287
|
+
const obj = raw;
|
|
148
288
|
const servers = obj.mcpServers ?? obj["mcp-servers"] ?? {};
|
|
149
289
|
return {
|
|
150
|
-
mcpServers:
|
|
290
|
+
mcpServers: parseServerEntries(servers),
|
|
151
291
|
imports: Array.isArray(obj.imports) ? obj.imports.filter((value): value is ImportKind => isImportKind(value)) : undefined,
|
|
152
|
-
settings:
|
|
153
|
-
? obj.settings as McpConfig["settings"]
|
|
154
|
-
: undefined,
|
|
292
|
+
settings: parseSettings(obj.settings),
|
|
155
293
|
};
|
|
156
294
|
}
|
|
157
295
|
|
|
296
|
+
function parseServerEntries(value: unknown): Record<string, ServerEntry> {
|
|
297
|
+
if (!isRecord(value)) return {};
|
|
298
|
+
return Object.fromEntries(Object.entries(value).filter((entry): entry is [string, ServerEntry] => isMcpServerDefinition(entry[1])));
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function parseSettings(value: unknown): McpConfig["settings"] | undefined {
|
|
302
|
+
if (!isRecord(value)) return undefined;
|
|
303
|
+
const toolPrefix = value.toolPrefix === "server" || value.toolPrefix === "short" || value.toolPrefix === "none" ? value.toolPrefix : undefined;
|
|
304
|
+
const directTools = typeof value.directTools === "boolean" ? value.directTools : undefined;
|
|
305
|
+
const settings: NonNullable<McpConfig["settings"]> = {
|
|
306
|
+
...(toolPrefix ? { toolPrefix } : {}),
|
|
307
|
+
...(directTools !== undefined ? { directTools } : {}),
|
|
308
|
+
...(value.agentPluginPaths !== undefined ? { agentPluginPaths: value.agentPluginPaths } : {}),
|
|
309
|
+
};
|
|
310
|
+
return Object.keys(settings).length ? settings : undefined;
|
|
311
|
+
}
|
|
312
|
+
|
|
158
313
|
function mergeConfigs(base: McpConfig, next: McpConfig): McpConfig {
|
|
159
314
|
const imports = [...(base.imports ?? []), ...(next.imports ?? [])];
|
|
160
315
|
return {
|
|
@@ -198,76 +353,24 @@ function resolveImportPath(importKind: ImportKind, cwd: string): string | null {
|
|
|
198
353
|
}
|
|
199
354
|
|
|
200
355
|
function extractServers(config: unknown, kind: ImportKind): Record<string, ServerEntry> {
|
|
201
|
-
if (!
|
|
202
|
-
const obj = config
|
|
356
|
+
if (!isRecord(config)) return {};
|
|
357
|
+
const obj = config;
|
|
203
358
|
const servers = kind === "cursor" || kind === "windsurf" || kind === "vscode"
|
|
204
359
|
? obj.mcpServers ?? obj["mcp-servers"]
|
|
205
360
|
: obj.mcpServers;
|
|
206
|
-
return
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function resolveDirectToolSelections(config: McpConfig, cache: MetadataCache, prefix: ToolPrefix, envOverride: string[]): ResolvedMcpDirectToolSelection[] {
|
|
210
|
-
const names: ResolvedMcpDirectToolSelection[] = [];
|
|
211
|
-
const seenNames = new Set<string>();
|
|
212
|
-
const { servers: selectedServers, tools: selectedTools } = parseSelections(envOverride);
|
|
213
|
-
|
|
214
|
-
for (const [serverName, definition] of Object.entries(config.mcpServers)) {
|
|
215
|
-
const serverCache = cache.servers[serverName];
|
|
216
|
-
if (!isServerCacheValid(serverCache, definition)) continue;
|
|
217
|
-
|
|
218
|
-
const toolFilter = selectedServers.has(serverName)
|
|
219
|
-
? true
|
|
220
|
-
: selectedTools.get(serverName);
|
|
221
|
-
if (!toolFilter) continue;
|
|
222
|
-
|
|
223
|
-
for (const tool of Array.isArray(serverCache.tools) ? serverCache.tools : []) {
|
|
224
|
-
if (typeof tool?.name !== "string" || !tool.name) continue;
|
|
225
|
-
if (toolFilter !== true && !toolFilter.has(tool.name)) continue;
|
|
226
|
-
if (isToolExcluded(tool.name, serverName, prefix, definition.excludeTools)) continue;
|
|
227
|
-
const prefixedName = formatToolName(tool.name, serverName, prefix);
|
|
228
|
-
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
229
|
-
seenNames.add(prefixedName);
|
|
230
|
-
names.push({ name: prefixedName, selector: `${serverName}/${tool.name}` });
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
if (definition.exposeResources === false) continue;
|
|
234
|
-
for (const resource of Array.isArray(serverCache.resources) ? serverCache.resources : []) {
|
|
235
|
-
if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri) continue;
|
|
236
|
-
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
237
|
-
if (toolFilter !== true && !toolFilter.has(baseName)) continue;
|
|
238
|
-
if (isToolExcluded(baseName, serverName, prefix, definition.excludeTools)) continue;
|
|
239
|
-
const prefixedName = formatToolName(baseName, serverName, prefix);
|
|
240
|
-
if (BUILTIN_TOOL_NAMES.has(prefixedName) || seenNames.has(prefixedName)) continue;
|
|
241
|
-
seenNames.add(prefixedName);
|
|
242
|
-
names.push({ name: prefixedName, selector: `${serverName}/${baseName}` });
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
return names;
|
|
361
|
+
return parseServerEntries(servers);
|
|
247
362
|
}
|
|
248
363
|
|
|
249
364
|
export function resolveMcpDirectToolNames(mcpDirectTools: string[] | undefined, cwd = process.cwd()): string[] {
|
|
250
365
|
return resolveMcpDirectToolSelections(mcpDirectTools, cwd).map((selection) => selection.name);
|
|
251
366
|
}
|
|
252
367
|
|
|
253
|
-
function
|
|
254
|
-
const servers =
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
if (item.includes("/")) {
|
|
259
|
-
const [server, tool] = item.split("/", 2);
|
|
260
|
-
if (server && tool) {
|
|
261
|
-
if (!tools.has(server)) tools.set(server, new Set());
|
|
262
|
-
tools.get(server)!.add(tool);
|
|
263
|
-
} else if (server) {
|
|
264
|
-
servers.add(server);
|
|
265
|
-
}
|
|
266
|
-
} else if (item) {
|
|
267
|
-
servers.add(item);
|
|
268
|
-
}
|
|
368
|
+
function validateSelectedServerDefinitions(config: McpConfig, selectors: string[]): void {
|
|
369
|
+
const { servers, tools } = parseMcpDirectToolSelectors(selectors);
|
|
370
|
+
for (const serverName of new Set([...servers, ...tools.keys()])) {
|
|
371
|
+
const definition = config.mcpServers[serverName];
|
|
372
|
+
if (definition) computeMcpServerHash(definition);
|
|
269
373
|
}
|
|
270
|
-
return { servers, tools };
|
|
271
374
|
}
|
|
272
375
|
|
|
273
376
|
function isServerCacheValid(entry: ServerCacheEntry | undefined, definition: ServerEntry): entry is ServerCacheEntry {
|
|
@@ -304,52 +407,12 @@ export function computeMcpServerHash(definition: ServerEntry): string {
|
|
|
304
407
|
return createHash("sha256").update(stableStringify(identity)).digest("hex");
|
|
305
408
|
}
|
|
306
409
|
|
|
307
|
-
function getToolPrefix(value: unknown): ToolPrefix {
|
|
308
|
-
return value === "none" || value === "short" || value === "server" ? value : "server";
|
|
309
|
-
}
|
|
310
|
-
|
|
311
410
|
function isImportKind(value: unknown): value is ImportKind {
|
|
312
411
|
return typeof value === "string" && Object.hasOwn(IMPORT_PATHS, value);
|
|
313
412
|
}
|
|
314
413
|
|
|
315
|
-
function
|
|
316
|
-
|
|
317
|
-
if (mode === "short") {
|
|
318
|
-
const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
|
|
319
|
-
return short || "mcp";
|
|
320
|
-
}
|
|
321
|
-
return serverName.replace(/-/g, "_");
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
function formatToolName(toolName: string, serverName: string, prefix: ToolPrefix): string {
|
|
325
|
-
const serverPrefix = getServerPrefix(serverName, prefix);
|
|
326
|
-
return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
function isToolExcluded(toolName: string, serverName: string, prefix: ToolPrefix, excludeTools: unknown): boolean {
|
|
330
|
-
if (!Array.isArray(excludeTools) || excludeTools.length === 0) return false;
|
|
331
|
-
const candidates = new Set([
|
|
332
|
-
normalizeToolName(toolName),
|
|
333
|
-
normalizeToolName(formatToolName(toolName, serverName, prefix)),
|
|
334
|
-
normalizeToolName(formatToolName(toolName, serverName, "server")),
|
|
335
|
-
normalizeToolName(formatToolName(toolName, serverName, "short")),
|
|
336
|
-
]);
|
|
337
|
-
return excludeTools.some((excluded) => typeof excluded === "string" && candidates.has(normalizeToolName(excluded)));
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
function normalizeToolName(value: string): string {
|
|
341
|
-
return value.replace(/-/g, "_");
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function resourceNameToToolName(name: string): string {
|
|
345
|
-
let result = name
|
|
346
|
-
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
347
|
-
.replace(/_+/g, "_")
|
|
348
|
-
.replace(/^_+/, "")
|
|
349
|
-
.replace(/_+$/, "")
|
|
350
|
-
.toLowerCase();
|
|
351
|
-
if (!result || /^\d/.test(result)) result = `resource${result ? `_${result}` : ""}`;
|
|
352
|
-
return result;
|
|
414
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
415
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
353
416
|
}
|
|
354
417
|
|
|
355
418
|
function interpolateEnvRecord(values: Record<string, string> | undefined): Record<string, string> | undefined {
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
const BUILTIN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls", "mcp"]);
|
|
2
|
+
|
|
3
|
+
export type McpToolPrefix = "server" | "none" | "short";
|
|
4
|
+
|
|
5
|
+
export interface McpGrantServerFacts {
|
|
6
|
+
readonly exposeResources?: boolean;
|
|
7
|
+
readonly includeTools?: readonly unknown[];
|
|
8
|
+
readonly excludeTools?: readonly unknown[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface McpGrantToolMetadata {
|
|
12
|
+
readonly name?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface McpGrantResourceMetadata {
|
|
16
|
+
readonly name?: string;
|
|
17
|
+
readonly uri?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface McpGrantServerMetadata {
|
|
21
|
+
readonly tools?: readonly McpGrantToolMetadata[];
|
|
22
|
+
readonly resources?: readonly McpGrantResourceMetadata[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface McpDirectToolGrantInput {
|
|
26
|
+
readonly selectors?: readonly string[];
|
|
27
|
+
readonly servers: Readonly<Record<string, McpGrantServerFacts>>;
|
|
28
|
+
/** Cache entries already validated by the source-loading adapter. */
|
|
29
|
+
readonly metadata: Readonly<Record<string, McpGrantServerMetadata>>;
|
|
30
|
+
readonly toolPrefix?: unknown;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ResolvedMcpDirectToolSelection {
|
|
34
|
+
name: string;
|
|
35
|
+
selector: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface McpDirectToolGrant {
|
|
39
|
+
selections: ResolvedMcpDirectToolSelection[];
|
|
40
|
+
unresolvedSelectors: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function normalizeMcpDirectToolSelectors(selectors: readonly string[] | undefined): string[] {
|
|
44
|
+
return [...new Set((selectors ?? []).map((selector) => selector.replace(/\/+$/, "")).filter(Boolean))];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function parseMcpDirectToolSelectors(selectors: readonly string[]): {
|
|
48
|
+
servers: Set<string>;
|
|
49
|
+
tools: Map<string, Set<string>>;
|
|
50
|
+
} {
|
|
51
|
+
const servers = new Set<string>();
|
|
52
|
+
const tools = new Map<string, Set<string>>();
|
|
53
|
+
for (const item of selectors) {
|
|
54
|
+
if (item.includes("/")) {
|
|
55
|
+
const [server, tool] = item.split("/", 2);
|
|
56
|
+
if (server && tool) {
|
|
57
|
+
if (!tools.has(server)) tools.set(server, new Set());
|
|
58
|
+
tools.get(server)!.add(tool);
|
|
59
|
+
} else if (server) {
|
|
60
|
+
servers.add(server);
|
|
61
|
+
}
|
|
62
|
+
} else if (item) {
|
|
63
|
+
servers.add(item);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return { servers, tools };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function planMcpDirectToolGrant(input: McpDirectToolGrantInput): McpDirectToolGrant {
|
|
70
|
+
const selectors = normalizeMcpDirectToolSelectors(input.selectors);
|
|
71
|
+
if (selectors.length === 0) return { selections: [], unresolvedSelectors: [] };
|
|
72
|
+
|
|
73
|
+
const { servers: selectedServers, tools: selectedTools } = parseMcpDirectToolSelectors(selectors);
|
|
74
|
+
const selections: ResolvedMcpDirectToolSelection[] = [];
|
|
75
|
+
const seenNames = new Set<string>();
|
|
76
|
+
const prefix = normalizeMcpToolPrefix(input.toolPrefix);
|
|
77
|
+
|
|
78
|
+
for (const [serverName, server] of Object.entries(input.servers)) {
|
|
79
|
+
const metadata = input.metadata[serverName];
|
|
80
|
+
if (!metadata) continue;
|
|
81
|
+
|
|
82
|
+
const toolFilter: true | ReadonlySet<string> | undefined = selectedServers.has(serverName)
|
|
83
|
+
? true
|
|
84
|
+
: selectedTools.get(serverName);
|
|
85
|
+
if (!toolFilter) continue;
|
|
86
|
+
|
|
87
|
+
for (const tool of Array.isArray(metadata.tools) ? metadata.tools : []) {
|
|
88
|
+
if (typeof tool?.name !== "string" || !tool.name) continue;
|
|
89
|
+
if (toolFilter !== true && !toolFilter.has(tool.name)) continue;
|
|
90
|
+
if (!isToolAllowed(tool.name, serverName, prefix, server.includeTools, server.excludeTools)) continue;
|
|
91
|
+
const name = formatToolName(tool.name, serverName, prefix);
|
|
92
|
+
if (BUILTIN_TOOL_NAMES.has(name) || seenNames.has(name)) continue;
|
|
93
|
+
seenNames.add(name);
|
|
94
|
+
selections.push({ name, selector: `${serverName}/${tool.name}` });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (server.exposeResources === false) continue;
|
|
98
|
+
for (const resource of Array.isArray(metadata.resources) ? metadata.resources : []) {
|
|
99
|
+
if (typeof resource?.name !== "string" || !resource.name || typeof resource.uri !== "string" || !resource.uri) continue;
|
|
100
|
+
const baseName = `get_${resourceNameToToolName(resource.name)}`;
|
|
101
|
+
if (toolFilter !== true && !toolFilter.has(baseName)) continue;
|
|
102
|
+
if (!isToolAllowed(baseName, serverName, prefix, server.includeTools, server.excludeTools)) continue;
|
|
103
|
+
const name = formatToolName(baseName, serverName, prefix);
|
|
104
|
+
if (BUILTIN_TOOL_NAMES.has(name) || seenNames.has(name)) continue;
|
|
105
|
+
seenNames.add(name);
|
|
106
|
+
selections.push({ name, selector: `${serverName}/${baseName}` });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const unresolvedSelectors = selectors.filter((selector) =>
|
|
111
|
+
selector.includes("/")
|
|
112
|
+
? !selections.some((selection) => selection.selector === selector)
|
|
113
|
+
: !selections.some((selection) => selection.selector.startsWith(`${selector}/`)),
|
|
114
|
+
);
|
|
115
|
+
return { selections, unresolvedSelectors };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function normalizeMcpToolPrefix(value: unknown): McpToolPrefix {
|
|
119
|
+
return value === "none" || value === "short" || value === "server" ? value : "server";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function formatUnresolvedMcpDirectToolSelectors(selectors: readonly string[]): string {
|
|
123
|
+
return `Unresolved MCP direct-tool selectors: ${selectors.join(", ")}. Direct MCP tools require a matching configured server and fresh metadata cache; runtime-registered servers require a host/pi-mcp-adapter handoff before child launch.`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function getServerPrefix(serverName: string, mode: McpToolPrefix): string {
|
|
127
|
+
if (mode === "none") return "";
|
|
128
|
+
if (mode === "short") {
|
|
129
|
+
const short = serverName.replace(/-?mcp$/i, "").replace(/-/g, "_");
|
|
130
|
+
return short || "mcp";
|
|
131
|
+
}
|
|
132
|
+
return serverName.replace(/-/g, "_");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function formatToolName(toolName: string, serverName: string, prefix: McpToolPrefix): string {
|
|
136
|
+
const serverPrefix = getServerPrefix(serverName, prefix);
|
|
137
|
+
return serverPrefix ? `${serverPrefix}_${toolName}` : toolName;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isToolAllowed(
|
|
141
|
+
toolName: string,
|
|
142
|
+
serverName: string,
|
|
143
|
+
prefix: McpToolPrefix,
|
|
144
|
+
includeTools: readonly unknown[] | undefined,
|
|
145
|
+
excludeTools: readonly unknown[] | undefined,
|
|
146
|
+
): boolean {
|
|
147
|
+
const candidates = toolNameCandidates(toolName, serverName, prefix);
|
|
148
|
+
return (!Array.isArray(includeTools) || includeTools.length === 0 || matchesToolPatterns(candidates, includeTools))
|
|
149
|
+
&& !matchesToolPatterns(candidates, excludeTools);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function toolNameCandidates(toolName: string, serverName: string, prefix: McpToolPrefix): Set<string> {
|
|
153
|
+
return new Set([
|
|
154
|
+
toolName,
|
|
155
|
+
`mcp_${toolName}`,
|
|
156
|
+
formatToolName(toolName, serverName, prefix),
|
|
157
|
+
formatToolName(toolName, serverName, "server"),
|
|
158
|
+
formatToolName(toolName, serverName, "short"),
|
|
159
|
+
formatToolName(toolName, serverName, "none"),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function matchesToolPatterns(candidates: ReadonlySet<string>, patterns: readonly unknown[] | undefined): boolean {
|
|
164
|
+
if (!Array.isArray(patterns) || patterns.length === 0) return false;
|
|
165
|
+
for (const pattern of patterns) {
|
|
166
|
+
if (typeof pattern !== "string") continue;
|
|
167
|
+
const normalizedPattern = normalizeToolName(pattern);
|
|
168
|
+
const matcher = normalizedPattern.includes("*") || normalizedPattern.includes("?")
|
|
169
|
+
? globToRegExp(normalizedPattern)
|
|
170
|
+
: undefined;
|
|
171
|
+
for (const candidate of candidates) {
|
|
172
|
+
const normalizedCandidate = normalizeToolName(candidate);
|
|
173
|
+
if (matcher ? matcher.test(normalizedCandidate) : normalizedCandidate === normalizedPattern) return true;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function globToRegExp(pattern: string): RegExp {
|
|
180
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
181
|
+
return new RegExp(`^${escaped}$`);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function normalizeToolName(value: string): string {
|
|
185
|
+
return value.replace(/-/g, "_");
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function resourceNameToToolName(name: string): string {
|
|
189
|
+
let result = name
|
|
190
|
+
.replace(/[^a-zA-Z0-9]/g, "_")
|
|
191
|
+
.replace(/_+/g, "_")
|
|
192
|
+
.replace(/^_+/, "")
|
|
193
|
+
.replace(/_+$/, "")
|
|
194
|
+
.toLowerCase();
|
|
195
|
+
if (!result || /^\d/.test(result)) result = `resource${result ? `_${result}` : ""}`;
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
@@ -205,6 +205,18 @@ export function isExcluded(modelId: string, provider: string): boolean {
|
|
|
205
205
|
return exclusions.some((entry) => entryMatches(entry, modelId, provider, Date.now()));
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
/**
|
|
209
|
+
* Return the active exclusion matching a full model id, if any.
|
|
210
|
+
*
|
|
211
|
+
* The caller uses this for hard-fail diagnostics; fallback filtering should
|
|
212
|
+
* continue to use {@link filterFallbackCandidates}.
|
|
213
|
+
*/
|
|
214
|
+
export function findModelExclusion(fullId: string, now = Date.now()): Readonly<ModelExclusion> | undefined {
|
|
215
|
+
ensureLoaded();
|
|
216
|
+
const { provider, modelId } = parseModelKey(fullId);
|
|
217
|
+
return exclusions.find((entry) => entryMatches(entry, modelId, provider, now));
|
|
218
|
+
}
|
|
219
|
+
|
|
208
220
|
/**
|
|
209
221
|
* Number of live (non-expired) exclusions.
|
|
210
222
|
*/
|