@clinebot/core 0.0.33 → 0.0.34

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.
Files changed (69) hide show
  1. package/dist/auth/client.d.ts +19 -0
  2. package/dist/auth/client.d.ts.map +1 -1
  3. package/dist/auth/cline.d.ts.map +1 -1
  4. package/dist/auth/oca.d.ts.map +1 -1
  5. package/dist/auth/server.d.ts +32 -0
  6. package/dist/auth/server.d.ts.map +1 -1
  7. package/dist/auth/types.d.ts +29 -0
  8. package/dist/auth/types.d.ts.map +1 -1
  9. package/dist/extensions/index.d.ts +2 -1
  10. package/dist/extensions/index.d.ts.map +1 -1
  11. package/dist/extensions/plugin/plugin-config-loader.d.ts +2 -1
  12. package/dist/extensions/plugin/plugin-config-loader.d.ts.map +1 -1
  13. package/dist/extensions/plugin/plugin-load-report.d.ts +19 -0
  14. package/dist/extensions/plugin/plugin-load-report.d.ts.map +1 -0
  15. package/dist/extensions/plugin/plugin-loader.d.ts +6 -0
  16. package/dist/extensions/plugin/plugin-loader.d.ts.map +1 -1
  17. package/dist/extensions/plugin/plugin-sandbox.d.ts +2 -1
  18. package/dist/extensions/plugin/plugin-sandbox.d.ts.map +1 -1
  19. package/dist/extensions/plugin-sandbox-bootstrap.js +148 -148
  20. package/dist/index.d.ts +3 -2
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +207 -207
  23. package/dist/runtime/runtime-builder.d.ts +1 -1
  24. package/dist/runtime/runtime-builder.d.ts.map +1 -1
  25. package/dist/runtime/subprocess-sandbox.d.ts +2 -0
  26. package/dist/runtime/subprocess-sandbox.d.ts.map +1 -1
  27. package/dist/runtime/tool-approval.d.ts.map +1 -1
  28. package/dist/session/default-session-manager.d.ts.map +1 -1
  29. package/dist/session/persistence-service.d.ts.map +1 -1
  30. package/dist/session/session-artifacts.d.ts +2 -0
  31. package/dist/session/session-artifacts.d.ts.map +1 -1
  32. package/dist/session/session-config-builder.d.ts.map +1 -1
  33. package/dist/team/team-tools.d.ts.map +1 -1
  34. package/dist/types/config.d.ts +1 -0
  35. package/dist/types/config.d.ts.map +1 -1
  36. package/package.json +4 -4
  37. package/src/auth/client.test.ts +29 -0
  38. package/src/auth/client.ts +21 -0
  39. package/src/auth/cline.ts +2 -0
  40. package/src/auth/oca.ts +2 -0
  41. package/src/auth/server.test.ts +287 -0
  42. package/src/auth/server.ts +50 -1
  43. package/src/auth/types.ts +29 -0
  44. package/src/extensions/index.ts +6 -0
  45. package/src/extensions/plugin/plugin-config-loader.test.ts +37 -0
  46. package/src/extensions/plugin/plugin-config-loader.ts +18 -10
  47. package/src/extensions/plugin/plugin-load-report.ts +20 -0
  48. package/src/extensions/plugin/plugin-loader.test.ts +45 -0
  49. package/src/extensions/plugin/plugin-loader.ts +57 -3
  50. package/src/extensions/plugin/plugin-sandbox-bootstrap.ts +158 -86
  51. package/src/extensions/plugin/plugin-sandbox.test.ts +70 -0
  52. package/src/extensions/plugin/plugin-sandbox.ts +17 -6
  53. package/src/index.ts +11 -0
  54. package/src/runtime/hook-file-hooks.test.ts +42 -7
  55. package/src/runtime/runtime-builder.test.ts +98 -0
  56. package/src/runtime/runtime-builder.ts +112 -65
  57. package/src/runtime/subprocess-sandbox.ts +26 -23
  58. package/src/runtime/tool-approval.ts +13 -15
  59. package/src/session/default-session-manager.ts +1 -3
  60. package/src/session/persistence-service.test.ts +38 -0
  61. package/src/session/persistence-service.ts +16 -1
  62. package/src/session/session-artifacts.ts +16 -0
  63. package/src/session/session-config-builder.ts +46 -0
  64. package/src/team/team-tools.test.ts +104 -0
  65. package/src/team/team-tools.ts +35 -16
  66. package/src/types/config.ts +1 -0
  67. package/dist/runtime/team-runtime-registry.d.ts +0 -13
  68. package/dist/runtime/team-runtime-registry.d.ts.map +0 -1
  69. package/src/runtime/team-runtime-registry.ts +0 -43
@@ -4,6 +4,7 @@ import { dirname, join } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import type { AgentConfig, HookStage, Tool } from "@clinebot/shared";
6
6
  import { SubprocessSandbox } from "../../runtime/subprocess-sandbox";
7
+ import type { PluginLoadDiagnostics } from "./plugin-load-report";
7
8
 
8
9
  export interface PluginSandboxOptions {
9
10
  pluginPaths: string[];
@@ -31,6 +32,7 @@ type SandboxedContributionDescriptor = {
31
32
 
32
33
  type SandboxedPluginDescriptor = {
33
34
  pluginId: string;
35
+ pluginPath: string;
34
36
  name: string;
35
37
  manifest: AgentExtension["manifest"];
36
38
  contributions: {
@@ -43,6 +45,10 @@ type SandboxedPluginDescriptor = {
43
45
  };
44
46
  };
45
47
 
48
+ type SandboxedInitializeResult = {
49
+ plugins: SandboxedPluginDescriptor[];
50
+ } & PluginLoadDiagnostics;
51
+
46
52
  function isUnknownPluginIdError(error: unknown): boolean {
47
53
  const message = error instanceof Error ? error.message : String(error);
48
54
  return message.includes("Unknown sandbox plugin id:");
@@ -153,10 +159,12 @@ function withTimeoutFallback(
153
159
 
154
160
  export async function loadSandboxedPlugins(
155
161
  options: PluginSandboxOptions,
156
- ): Promise<{
157
- extensions: AgentConfig["extensions"];
158
- shutdown: () => Promise<void>;
159
- }> {
162
+ ): Promise<
163
+ {
164
+ extensions: AgentConfig["extensions"];
165
+ shutdown: () => Promise<void>;
166
+ } & PluginLoadDiagnostics
167
+ > {
160
168
  const sandbox = new SubprocessSandbox({
161
169
  name: "plugin-sandbox",
162
170
  ...("file" in BOOTSTRAP
@@ -187,9 +195,9 @@ export async function loadSandboxedPlugins(
187
195
  return reinitPromise;
188
196
  };
189
197
 
190
- let descriptors: SandboxedPluginDescriptor[];
198
+ let initialized: SandboxedInitializeResult;
191
199
  try {
192
- descriptors = await sandbox.call<SandboxedPluginDescriptor[]>(
200
+ initialized = await sandbox.call<SandboxedInitializeResult>(
193
201
  "initialize",
194
202
  initArgs,
195
203
  { timeoutMs: importTimeoutMs },
@@ -200,6 +208,7 @@ export async function loadSandboxedPlugins(
200
208
  });
201
209
  throw error;
202
210
  }
211
+ const descriptors = initialized.plugins;
203
212
 
204
213
  const extensions: NonNullable<AgentConfig["extensions"]> = descriptors.map(
205
214
  (descriptor) => {
@@ -239,9 +248,11 @@ export async function loadSandboxedPlugins(
239
248
 
240
249
  return {
241
250
  extensions,
251
+ failures: initialized.failures,
242
252
  shutdown: async () => {
243
253
  await sandbox.shutdown();
244
254
  },
255
+ warnings: initialized.warnings,
245
256
  };
246
257
  }
247
258
 
package/src/index.ts CHANGED
@@ -128,6 +128,13 @@ export {
128
128
  OCI_HEADER_OPC_REQUEST_ID,
129
129
  refreshOcaToken,
130
130
  } from "./auth/oca";
131
+ export type {
132
+ LocalOAuthServer,
133
+ LocalOAuthServerOptions,
134
+ OAuthCallbackPayload,
135
+ OAuthServerCloseInfo,
136
+ OAuthServerListeningInfo,
137
+ } from "./auth/server";
131
138
  export { startLocalOAuthServer } from "./auth/server";
132
139
  export type {
133
140
  OAuthCredentials,
@@ -163,12 +170,16 @@ export {
163
170
  } from "./chat/chat-schema";
164
171
  export type {
165
172
  LoadAgentPluginFromPathOptions,
173
+ PluginInitializationFailure,
174
+ PluginInitializationWarning,
175
+ PluginLoadDiagnostics,
166
176
  ResolveAgentPluginPathsOptions,
167
177
  } from "./extensions";
168
178
  export {
169
179
  discoverPluginModulePaths,
170
180
  loadAgentPluginFromPath,
171
181
  loadAgentPluginsFromPaths,
182
+ loadAgentPluginsFromPathsWithDiagnostics,
172
183
  resolveAgentPluginPaths,
173
184
  resolveAndLoadAgentPlugins,
174
185
  resolvePluginConfigSearchPaths,
@@ -50,7 +50,12 @@ describe("createHookConfigFileHooks", () => {
50
50
  });
51
51
  expect(hooks).toBeUndefined();
52
52
  } finally {
53
- await rm(workspace, { recursive: true, force: true });
53
+ await rm(workspace, {
54
+ recursive: true,
55
+ force: true,
56
+ maxRetries: 3,
57
+ retryDelay: 250,
58
+ });
54
59
  }
55
60
  });
56
61
 
@@ -78,7 +83,12 @@ describe("createHookConfigFileHooks", () => {
78
83
  });
79
84
  expect(control).toMatchObject({ cancel: true, context: "legacy-ok" });
80
85
  } finally {
81
- await rm(workspace, { recursive: true, force: true });
86
+ await rm(workspace, {
87
+ recursive: true,
88
+ force: true,
89
+ maxRetries: 3,
90
+ retryDelay: 250,
91
+ });
82
92
  }
83
93
  });
84
94
 
@@ -106,7 +116,12 @@ describe("createHookConfigFileHooks", () => {
106
116
  });
107
117
  expect(control).toMatchObject({ cancel: false, context: "shebang-ok" });
108
118
  } finally {
109
- await rm(workspace, { recursive: true, force: true });
119
+ await rm(workspace, {
120
+ recursive: true,
121
+ force: true,
122
+ maxRetries: 3,
123
+ retryDelay: 250,
124
+ });
110
125
  }
111
126
  });
112
127
 
@@ -137,7 +152,12 @@ describe("createHookConfigFileHooks", () => {
137
152
  context: "needs-review",
138
153
  });
139
154
  } finally {
140
- await rm(workspace, { recursive: true, force: true });
155
+ await rm(workspace, {
156
+ recursive: true,
157
+ force: true,
158
+ maxRetries: 3,
159
+ retryDelay: 250,
160
+ });
141
161
  }
142
162
  });
143
163
 
@@ -168,7 +188,12 @@ describe("createHookConfigFileHooks", () => {
168
188
  context: "python-ok",
169
189
  });
170
190
  } finally {
171
- await rm(workspace, { recursive: true, force: true });
191
+ await rm(workspace, {
192
+ recursive: true,
193
+ force: true,
194
+ maxRetries: 3,
195
+ retryDelay: 250,
196
+ });
172
197
  }
173
198
  });
174
199
 
@@ -201,7 +226,12 @@ describe("createHookConfigFileHooks", () => {
201
226
  context: "powershell-ok",
202
227
  });
203
228
  } finally {
204
- await rm(workspace, { recursive: true, force: true });
229
+ await rm(workspace, {
230
+ recursive: true,
231
+ force: true,
232
+ maxRetries: 3,
233
+ retryDelay: 250,
234
+ });
205
235
  }
206
236
  },
207
237
  );
@@ -231,7 +261,12 @@ describe("createHookConfigFileHooks", () => {
231
261
  expect(payload.hookName).toBe("agent_error");
232
262
  expect(payload.error?.message).toBe("401 unauthorized");
233
263
  } finally {
234
- await rm(workspace, { recursive: true, force: true });
264
+ await rm(workspace, {
265
+ recursive: true,
266
+ force: true,
267
+ maxRetries: 3,
268
+ retryDelay: 250,
269
+ });
235
270
  }
236
271
  });
237
272
 
@@ -186,6 +186,22 @@ describe("DefaultRuntimeBuilder", () => {
186
186
  expect(runtime.tools).toEqual([]);
187
187
  });
188
188
 
189
+ it("omits tools disabled by policy from the advertised runtime tool list", async () => {
190
+ const runtime = await new DefaultRuntimeBuilder().build({
191
+ config: makeBaseConfig({
192
+ toolPolicies: {
193
+ run_commands: { enabled: false },
194
+ read_files: { enabled: false },
195
+ },
196
+ }),
197
+ });
198
+
199
+ const names = runtime.tools.map((tool) => tool.name);
200
+ expect(names).not.toContain("run_commands");
201
+ expect(names).not.toContain("read_files");
202
+ expect(names).toContain("search_codebase");
203
+ });
204
+
189
205
  it("adds spawn tool when enabled", async () => {
190
206
  const runtime = await new DefaultRuntimeBuilder().build({
191
207
  config: makeBaseConfig({
@@ -284,6 +300,88 @@ process.stdin.on("data", (chunk) => {
284
300
  }
285
301
  });
286
302
 
303
+ it("skips MCP settings tools when disableMcpSettingsTools is true", async () => {
304
+ const tempRoot = mkdtempSync(
305
+ join(tmpdir(), "runtime-builder-mcp-disabled-"),
306
+ );
307
+ const serverPath = join(tempRoot, "mock-mcp-server.js");
308
+ const settingsPath = join(tempRoot, "cline_mcp_settings.json");
309
+ const previousSettingsPath = process.env.CLINE_MCP_SETTINGS_PATH;
310
+
311
+ writeFileSync(
312
+ serverPath,
313
+ `let buffer = "";
314
+ function write(payload) {
315
+ const body = JSON.stringify(payload);
316
+ process.stdout.write("Content-Length: " + Buffer.byteLength(body, "utf8") + "\\r\\n\\r\\n" + body);
317
+ }
318
+ function handle(message) {
319
+ if (message.method === "initialize") {
320
+ write({ jsonrpc: "2.0", id: message.id, result: { protocolVersion: "2024-11-05", capabilities: { tools: {} }, serverInfo: { name: "mock", version: "1.0.0" } } });
321
+ return;
322
+ }
323
+ if (message.method === "tools/list") {
324
+ write({ jsonrpc: "2.0", id: message.id, result: { tools: [{ name: "echo", description: "Echo tool", inputSchema: { type: "object", properties: { value: { type: "string" } }, required: [] } }] } });
325
+ return;
326
+ }
327
+ if (message.method === "tools/call") {
328
+ write({ jsonrpc: "2.0", id: message.id, result: { echoed: message.params?.arguments?.value ?? null } });
329
+ }
330
+ }
331
+ process.stdin.on("data", (chunk) => {
332
+ buffer += chunk.toString("utf8");
333
+ while (true) {
334
+ const separator = buffer.indexOf("\\r\\n\\r\\n");
335
+ if (separator < 0) break;
336
+ const header = buffer.slice(0, separator);
337
+ const match = header.match(/Content-Length:\\s*(\\d+)/i);
338
+ if (!match) throw new Error("missing content length");
339
+ const length = Number(match[1]);
340
+ const start = separator + 4;
341
+ const end = start + length;
342
+ if (buffer.length < end) break;
343
+ const body = buffer.slice(start, end);
344
+ buffer = buffer.slice(end);
345
+ const message = JSON.parse(body);
346
+ if (message.method === "notifications/initialized") continue;
347
+ handle(message);
348
+ }
349
+ });`,
350
+ "utf8",
351
+ );
352
+ writeFileSync(
353
+ settingsPath,
354
+ JSON.stringify(
355
+ {
356
+ mcpServers: {
357
+ mock: {
358
+ command: process.execPath,
359
+ args: [serverPath],
360
+ },
361
+ },
362
+ },
363
+ null,
364
+ 2,
365
+ ),
366
+ "utf8",
367
+ );
368
+
369
+ process.env.CLINE_MCP_SETTINGS_PATH = settingsPath;
370
+ try {
371
+ const runtime = await new DefaultRuntimeBuilder().build({
372
+ config: makeBaseConfig({
373
+ disableMcpSettingsTools: true,
374
+ }),
375
+ });
376
+ expect(runtime.tools.map((tool) => tool.name)).not.toContain(
377
+ "mock__echo",
378
+ );
379
+ await runtime.shutdown("test");
380
+ } finally {
381
+ process.env.CLINE_MCP_SETTINGS_PATH = previousSettingsPath;
382
+ }
383
+ });
384
+
287
385
  it("skips broken MCP servers without crashing", async () => {
288
386
  const tempRoot = mkdtempSync(join(tmpdir(), "runtime-builder-mcp-bad-"));
289
387
  const serverPath = join(tempRoot, "malformed-mcp-server.js");
@@ -39,7 +39,6 @@ import type {
39
39
  RuntimeBuilderInput,
40
40
  BuiltRuntime as RuntimeEnvironment,
41
41
  } from "./session-runtime";
42
- import { TeamRuntimeRegistry } from "./team-runtime-registry";
43
42
 
44
43
  type SkillsExecutorMetadataItem = {
45
44
  id: string;
@@ -52,6 +51,29 @@ type SkillsExecutorWithMetadata = SkillsExecutor & {
52
51
  configuredSkills?: SkillsExecutorMetadataItem[];
53
52
  };
54
53
 
54
+ function isToolEnabledByPolicies(
55
+ toolName: string,
56
+ toolPolicies: CoreSessionConfig["toolPolicies"],
57
+ ): boolean {
58
+ const globalPolicy = toolPolicies?.["*"] ?? {};
59
+ const toolPolicy = toolPolicies?.[toolName] ?? {};
60
+ return (
61
+ {
62
+ ...globalPolicy,
63
+ ...toolPolicy,
64
+ }.enabled !== false
65
+ );
66
+ }
67
+
68
+ function filterToolsByPolicies(
69
+ tools: Tool[],
70
+ toolPolicies: CoreSessionConfig["toolPolicies"],
71
+ ): Tool[] {
72
+ return tools.filter((tool) =>
73
+ isToolEnabledByPolicies(tool.name, toolPolicies),
74
+ );
75
+ }
76
+
55
77
  export function createTeamName(): string {
56
78
  return `team-${nanoid(5)}`;
57
79
  }
@@ -63,6 +85,7 @@ function createBuiltinToolsList(
63
85
  yolo: boolean | undefined,
64
86
  modelId: string,
65
87
  toolRoutingRules: ToolRoutingRule[] | undefined,
88
+ toolPolicies: CoreSessionConfig["toolPolicies"],
66
89
  skillsExecutor?: SkillsExecutorWithMetadata,
67
90
  executorOverrides?: Partial<ToolExecutors>,
68
91
  ): Tool[] {
@@ -74,20 +97,23 @@ function createBuiltinToolsList(
74
97
  toolRoutingRules ?? DEFAULT_MODEL_TOOL_ROUTING_RULES,
75
98
  );
76
99
 
77
- return createBuiltinTools({
78
- cwd,
79
- ...preset,
80
- enableSkills: !!skillsExecutor,
81
- ...toolRoutingConfig,
82
- executors: {
83
- ...(skillsExecutor
84
- ? {
85
- skills: skillsExecutor,
86
- }
87
- : {}),
88
- ...(executorOverrides ?? {}),
89
- },
90
- });
100
+ return filterToolsByPolicies(
101
+ createBuiltinTools({
102
+ cwd,
103
+ ...preset,
104
+ enableSkills: !!skillsExecutor,
105
+ ...toolRoutingConfig,
106
+ executors: {
107
+ ...(skillsExecutor
108
+ ? {
109
+ skills: skillsExecutor,
110
+ }
111
+ : {}),
112
+ ...(executorOverrides ?? {}),
113
+ },
114
+ }),
115
+ toolPolicies,
116
+ );
91
117
  }
92
118
 
93
119
  const SKILL_FILE_NAME = "SKILL.md";
@@ -96,7 +122,7 @@ function listAvailableSkillNames(
96
122
  watcher: UserInstructionConfigWatcher,
97
123
  allowedSkillNames?: ReadonlyArray<string>,
98
124
  ): string[] {
99
- return listConfiguredSkills(watcher, allowedSkillNames)
125
+ return getConfiguredSkills(watcher, allowedSkillNames)
100
126
  .filter((skill) => !skill.disabled)
101
127
  .map((skill) => skill.name.trim())
102
128
  .filter((name) => name.length > 0)
@@ -143,10 +169,14 @@ function isSkillAllowed(
143
169
  );
144
170
  }
145
171
 
146
- function listConfiguredSkills(
172
+ type ConfiguredSkill = SkillsExecutorMetadataItem & {
173
+ skill: SkillConfig;
174
+ };
175
+
176
+ function getConfiguredSkills(
147
177
  watcher: UserInstructionConfigWatcher,
148
178
  allowedSkillNames?: ReadonlyArray<string>,
149
- ): SkillsExecutorMetadataItem[] {
179
+ ): ConfiguredSkill[] {
150
180
  const allowedSkills = toAllowedSkillSet(allowedSkillNames);
151
181
  const snapshot = watcher.getSnapshot("skill");
152
182
  return [...snapshot.entries()]
@@ -157,6 +187,7 @@ function listConfiguredSkills(
157
187
  name: skill.name.trim(),
158
188
  description: skill.description?.trim(),
159
189
  disabled: skill.disabled === true,
190
+ skill,
160
191
  };
161
192
  })
162
193
  .filter((skill) => isSkillAllowed(skill.id, skill.name, allowedSkills));
@@ -254,28 +285,22 @@ function resolveSkillRecord(
254
285
  requestedSkill: string,
255
286
  allowedSkillNames?: ReadonlyArray<string>,
256
287
  ): { id: string; skill: SkillConfig } | { error: string } {
257
- const allowedSkills = toAllowedSkillSet(allowedSkillNames);
258
288
  const normalized = requestedSkill.trim().replace(/^\/+/, "").toLowerCase();
259
289
  if (!normalized) {
260
290
  return { error: "Missing skill name." };
261
291
  }
262
292
 
263
- const snapshot = watcher.getSnapshot("skill");
264
- const scopedEntries = [...snapshot.entries()].filter(([id, record]) => {
265
- const skill = record.item as SkillConfig;
266
- return isSkillAllowed(id, skill.name, allowedSkills);
267
- });
268
- const scopedSnapshot = new Map(scopedEntries);
269
- const exact = scopedSnapshot.get(normalized);
293
+ const configuredSkills = getConfiguredSkills(watcher, allowedSkillNames);
294
+ const exact = configuredSkills.find((entry) => entry.id === normalized);
270
295
  if (exact) {
271
- const skill = exact.item as SkillConfig;
296
+ const { skill } = exact;
272
297
  if (skill.disabled === true) {
273
298
  return {
274
299
  error: `Skill "${skill.name}" is configured but disabled.`,
275
300
  };
276
301
  }
277
302
  return {
278
- id: normalized,
303
+ id: exact.id,
279
304
  skill,
280
305
  };
281
306
  }
@@ -284,30 +309,38 @@ function resolveSkillRecord(
284
309
  ? (normalized.split(":").at(-1) ?? normalized)
285
310
  : normalized;
286
311
 
287
- const suffixMatches = [...scopedSnapshot.entries()].filter(([id]) => {
312
+ const suffixMatches = configuredSkills.filter(({ id }) => {
288
313
  if (id === bareName) {
289
314
  return true;
290
315
  }
291
316
  return id.endsWith(`:${bareName}`);
292
317
  });
293
318
 
319
+ const enabledSuffixMatches = suffixMatches.filter(
320
+ ({ skill }) => skill.disabled !== true,
321
+ );
322
+
323
+ if (enabledSuffixMatches.length === 1) {
324
+ const { id, skill } = enabledSuffixMatches[0];
325
+ return { id, skill };
326
+ }
327
+
328
+ if (enabledSuffixMatches.length > 1) {
329
+ return {
330
+ error: `Skill "${requestedSkill}" is ambiguous. Use one of: ${enabledSuffixMatches.map(({ id }) => id).join(", ")}`,
331
+ };
332
+ }
333
+
294
334
  if (suffixMatches.length === 1) {
295
- const [id, record] = suffixMatches[0];
296
- const skill = record.item as SkillConfig;
297
- if (skill.disabled === true) {
298
- return {
299
- error: `Skill "${skill.name}" is configured but disabled.`,
300
- };
301
- }
335
+ const { skill } = suffixMatches[0];
302
336
  return {
303
- id,
304
- skill,
337
+ error: `Skill "${skill.name}" is configured but disabled.`,
305
338
  };
306
339
  }
307
340
 
308
341
  if (suffixMatches.length > 1) {
309
342
  return {
310
- error: `Skill "${requestedSkill}" is ambiguous. Use one of: ${suffixMatches.map(([id]) => id).join(", ")}`,
343
+ error: `Skill "${requestedSkill}" is ambiguous, and all matches are disabled: ${suffixMatches.map(({ id }) => id).join(", ")}`,
311
344
  };
312
345
  }
313
346
 
@@ -354,7 +387,10 @@ function createSkillsExecutor(
354
387
  }
355
388
  };
356
389
  Object.defineProperty(executor, "configuredSkills", {
357
- get: () => listConfiguredSkills(watcher, allowedSkillNames),
390
+ get: () =>
391
+ getConfiguredSkills(watcher, allowedSkillNames).map(
392
+ ({ skill: _skill, ...metadata }) => metadata,
393
+ ),
358
394
  enumerable: true,
359
395
  configurable: false,
360
396
  });
@@ -386,6 +422,7 @@ function normalizeConfig(
386
422
  | "enableTools"
387
423
  | "enableSpawnAgent"
388
424
  | "enableAgentTeams"
425
+ | "disableMcpSettingsTools"
389
426
  | "yolo"
390
427
  | "missionLogIntervalSteps"
391
428
  | "missionLogIntervalMs"
@@ -407,6 +444,7 @@ function normalizeConfig(
407
444
  config.enableSpawnAgent ?? preset.enableSpawnAgent ?? true,
408
445
  enableAgentTeams:
409
446
  config.enableAgentTeams ?? preset.enableAgentTeams ?? true,
447
+ disableMcpSettingsTools: config.disableMcpSettingsTools === true,
410
448
  yolo: config.yolo === true,
411
449
  missionLogIntervalSteps:
412
450
  typeof config.missionLogIntervalSteps === "number" &&
@@ -422,7 +460,15 @@ function normalizeConfig(
422
460
  }
423
461
 
424
462
  export class DefaultRuntimeBuilder implements RuntimeBuilder {
425
- private readonly teamRuntimeRegistry = new TeamRuntimeRegistry();
463
+ private readonly teamRuntimeEntries = new Map<
464
+ string,
465
+ {
466
+ runtime?: AgentTeamsRuntime;
467
+ delegatedAgentConfigProvider: ReturnType<
468
+ typeof createDelegatedAgentConfigProvider
469
+ >;
470
+ }
471
+ >();
426
472
 
427
473
  async build(input: RuntimeBuilderInput): Promise<RuntimeEnvironment> {
428
474
  const {
@@ -440,6 +486,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
440
486
  const normalized = normalizeConfig(config);
441
487
  const tools: Tool[] = [];
442
488
  const effectiveTeamName = config.teamName?.trim() || createTeamName();
489
+ const hasLocalSkills = hasSkillsFiles(config.cwd);
443
490
  let teamToolsRegistered = false;
444
491
  const watcherProvided = Boolean(sharedUserInstructionWatcher);
445
492
  let userInstructionWatcher = sharedUserInstructionWatcher;
@@ -447,11 +494,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
447
494
  let skillsExecutor: SkillsExecutorWithMetadata | undefined;
448
495
  let mcpShutdown: (() => Promise<void>) | undefined;
449
496
 
450
- if (
451
- !userInstructionWatcher &&
452
- normalized.enableTools &&
453
- hasSkillsFiles(config.cwd)
454
- ) {
497
+ if (!userInstructionWatcher && normalized.enableTools && hasLocalSkills) {
455
498
  userInstructionWatcher = createUserInstructionConfigWatcher({
456
499
  skills: { workspacePath: config.cwd },
457
500
  rules: { workspacePath: config.cwd },
@@ -464,8 +507,8 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
464
507
  normalized.enableTools &&
465
508
  userInstructionWatcher &&
466
509
  (watcherProvided ||
467
- hasSkillsFiles(config.cwd) ||
468
- listConfiguredSkills(userInstructionWatcher, config.skills).length > 0)
510
+ hasLocalSkills ||
511
+ getConfiguredSkills(userInstructionWatcher, config.skills).length > 0)
469
512
  ) {
470
513
  skillsExecutor = createSkillsExecutor(
471
514
  userInstructionWatcher,
@@ -483,13 +526,16 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
483
526
  normalized.yolo,
484
527
  config.modelId,
485
528
  config.toolRoutingRules,
529
+ config.toolPolicies,
486
530
  skillsExecutor,
487
531
  defaultToolExecutors,
488
532
  ),
489
533
  );
490
- const mcpRuntime = await loadConfiguredMcpTools(config.logger);
491
- tools.push(...mcpRuntime.tools);
492
- mcpShutdown = mcpRuntime.shutdown;
534
+ if (!normalized.disableMcpSettingsTools) {
535
+ const mcpRuntime = await loadConfiguredMcpTools(config.logger);
536
+ tools.push(...mcpRuntime.tools);
537
+ mcpShutdown = mcpRuntime.shutdown;
538
+ }
493
539
  }
494
540
 
495
541
  let teamRuntime: AgentTeamsRuntime | undefined;
@@ -526,21 +572,21 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
526
572
  telemetry: input.telemetry ?? config.telemetry,
527
573
  workspaceMetadata: config.workspaceMetadata,
528
574
  });
529
- this.teamRuntimeRegistry.getOrCreate(registryKey, () => ({
530
- delegatedAgentConfigProvider,
531
- }));
575
+ if (!this.teamRuntimeEntries.has(registryKey)) {
576
+ this.teamRuntimeEntries.set(registryKey, {
577
+ delegatedAgentConfigProvider,
578
+ });
579
+ }
532
580
 
533
581
  const ensureTeamRuntime = (): AgentTeamsRuntime | undefined => {
534
582
  if (!normalized.enableAgentTeams) {
535
583
  return undefined;
536
584
  }
537
585
 
538
- const registryEntry = this.teamRuntimeRegistry.getOrCreate(
539
- registryKey,
540
- () => ({
541
- delegatedAgentConfigProvider,
542
- }),
543
- );
586
+ const registryEntry = this.teamRuntimeEntries.get(registryKey) ?? {
587
+ delegatedAgentConfigProvider,
588
+ };
589
+ this.teamRuntimeEntries.set(registryKey, registryEntry);
544
590
  teamRuntime = registryEntry.runtime;
545
591
 
546
592
  if (!teamRuntime) {
@@ -592,7 +638,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
592
638
  const factory = input.teamToolsFactory ?? bootstrapAgentTeams;
593
639
  const teamBootstrap = factory({
594
640
  runtime: teamRuntime,
595
- leadAgentId: "lead",
641
+ leadAgentId: config.sessionId || "lead",
596
642
  restoredFromPersistence: Boolean(restoredTeamState),
597
643
  restoredTeammates: restoredTeammateSpecs,
598
644
  includeLeadSpawnTool: true,
@@ -610,6 +656,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
610
656
  normalized.yolo,
611
657
  config.modelId,
612
658
  config.toolRoutingRules,
659
+ config.toolPolicies,
613
660
  skillsExecutor,
614
661
  defaultToolExecutors,
615
662
  )
@@ -643,7 +690,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
643
690
 
644
691
  const completionGuard = normalized.enableAgentTeams
645
692
  ? () => {
646
- const rt = this.teamRuntimeRegistry.get(registryKey)?.runtime;
693
+ const rt = this.teamRuntimeEntries.get(registryKey)?.runtime;
647
694
  if (!rt) return undefined;
648
695
  const tasks = rt.listTasks();
649
696
  const hasInProgress = tasks.some(
@@ -675,13 +722,13 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
675
722
  : undefined;
676
723
 
677
724
  return {
678
- tools,
725
+ tools: filterToolsByPolicies(tools, config.toolPolicies),
679
726
  logger: logger ?? config.logger,
680
727
  telemetry: telemetry ?? config.telemetry,
681
728
  teamRuntime,
682
729
  teamRestoredFromPersistence: Boolean(restoredTeamState),
683
730
  delegatedAgentConfigProvider:
684
- this.teamRuntimeRegistry.get(registryKey)
731
+ this.teamRuntimeEntries.get(registryKey)
685
732
  ?.delegatedAgentConfigProvider ?? delegatedAgentConfigProvider,
686
733
  completionGuard,
687
734
  registerLeadAgent: (agent) => {
@@ -692,7 +739,7 @@ export class DefaultRuntimeBuilder implements RuntimeBuilder {
692
739
  },
693
740
  shutdown: async (reason: string) => {
694
741
  shutdownTeamRuntime(teamRuntime, reason);
695
- this.teamRuntimeRegistry.delete(registryKey);
742
+ this.teamRuntimeEntries.delete(registryKey);
696
743
  await mcpShutdown?.();
697
744
  if (!watcherProvided) {
698
745
  userInstructionWatcher?.stop();