@cjhyy/code-shell-core 0.8.8 → 0.8.10

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 (86) hide show
  1. package/dist/automation/scheduler.js +49 -0
  2. package/dist/automation/store.d.ts +1 -1
  3. package/dist/automation/store.js +184 -10
  4. package/dist/cli/agent-server-stdio.js +7 -0
  5. package/dist/credentials/store.d.ts +14 -0
  6. package/dist/credentials/store.js +245 -42
  7. package/dist/engine/engine.js +81 -27
  8. package/dist/engine/file-history-hook.js +24 -5
  9. package/dist/engine/run-finalize.js +8 -6
  10. package/dist/engine/run-session-open.js +1 -1
  11. package/dist/engine/run-setup.d.ts +4 -0
  12. package/dist/engine/run-setup.js +5 -1
  13. package/dist/engine/run-tooling.js +7 -1
  14. package/dist/engine/run-types.d.ts +33 -0
  15. package/dist/engine/run-types.js +21 -0
  16. package/dist/engine/run-workspace.js +11 -5
  17. package/dist/engine/turn-loop.js +9 -8
  18. package/dist/goal/lifecycle.d.ts +2 -0
  19. package/dist/goal/lifecycle.js +56 -33
  20. package/dist/index.d.ts +2 -3
  21. package/dist/index.internal.d.ts +1 -0
  22. package/dist/index.internal.js +1 -0
  23. package/dist/index.js +2 -2
  24. package/dist/links/cli.d.ts +2 -0
  25. package/dist/links/cli.js +11 -4
  26. package/dist/model-catalog/index.js +19 -4
  27. package/dist/model-catalog/save-entry.js +122 -61
  28. package/dist/model-catalog/types.js +27 -23
  29. package/dist/panel-apps/installer.js +27 -14
  30. package/dist/panel-apps/manifest.d.ts +11 -11
  31. package/dist/panel-apps/manifest.js +3 -1
  32. package/dist/panel-apps/registry.js +60 -12
  33. package/dist/plugins/installedPlugins.d.ts +4 -0
  34. package/dist/plugins/installedPlugins.js +70 -30
  35. package/dist/plugins/installer/types.d.ts +12 -12
  36. package/dist/plugins/installer/update.js +37 -38
  37. package/dist/plugins/knownMarketplaces.d.ts +7 -3
  38. package/dist/plugins/knownMarketplaces.js +127 -23
  39. package/dist/plugins/pluginCatalog.js +18 -4
  40. package/dist/plugins/pluginHookApproval.js +56 -60
  41. package/dist/plugins/pluginMcpApproval.js +50 -52
  42. package/dist/profile/catalog-store.js +39 -4
  43. package/dist/profile/catalog.js +55 -15
  44. package/dist/profile/store.js +51 -21
  45. package/dist/prompt/composer.d.ts +8 -0
  46. package/dist/prompt/composer.js +8 -0
  47. package/dist/protocol/chat-session-manager.d.ts +9 -0
  48. package/dist/protocol/chat-session-manager.js +22 -0
  49. package/dist/protocol/chat-session.d.ts +5 -0
  50. package/dist/protocol/chat-session.js +1 -0
  51. package/dist/protocol/server.d.ts +2 -0
  52. package/dist/protocol/server.js +112 -29
  53. package/dist/protocol/types.d.ts +20 -0
  54. package/dist/run/FileRunStore.d.ts +2 -0
  55. package/dist/run/FileRunStore.js +153 -18
  56. package/dist/run/Heartbeat.js +63 -4
  57. package/dist/services/auto-dream.js +39 -17
  58. package/dist/services/session-memory.js +107 -8
  59. package/dist/session/file-history.d.ts +63 -2
  60. package/dist/session/file-history.js +593 -86
  61. package/dist/session/session-manager.d.ts +4 -1
  62. package/dist/session/session-manager.js +94 -47
  63. package/dist/session/transcript.js +33 -3
  64. package/dist/session/undo-target.d.ts +15 -6
  65. package/dist/session/undo-target.js +26 -9
  66. package/dist/settings/manager.d.ts +22 -3
  67. package/dist/settings/manager.js +185 -50
  68. package/dist/settings/schema.d.ts +3 -3
  69. package/dist/sources/adapters/local-files.js +49 -4
  70. package/dist/sources/catalog.js +64 -18
  71. package/dist/sources/types.d.ts +3 -3
  72. package/dist/sources/types.js +7 -4
  73. package/dist/themes/installer.js +192 -28
  74. package/dist/tool-system/builtin/add-marketplace.js +21 -1
  75. package/dist/tool-system/builtin/cron.d.ts +2 -1
  76. package/dist/tool-system/builtin/cron.js +20 -6
  77. package/dist/tool-system/builtin/index.js +48 -1
  78. package/dist/tool-system/builtin/install-capability.d.ts +52 -0
  79. package/dist/tool-system/builtin/install-capability.js +1057 -0
  80. package/dist/tool-system/builtin/skill.js +3 -1
  81. package/dist/tool-system/executor.js +1 -0
  82. package/dist/tool-system/path-policy.js +25 -33
  83. package/dist/tool-system/registry.js +5 -0
  84. package/dist/utils/file-mutex.d.ts +2 -0
  85. package/dist/utils/file-mutex.js +29 -4
  86. package/package.json +2 -1
@@ -9,18 +9,21 @@ export const SOURCE_KINDS = ["mock", "mcp-resource", "local-files"];
9
9
  export const SourceDefinitionSchema = z.object({
10
10
  id: z.string().regex(SOURCE_ID_RE),
11
11
  kind: z.enum(SOURCE_KINDS),
12
- label: z.string().min(1),
13
- description: z.string().optional(),
12
+ label: z.string().min(1).max(512).refine((value) => !value.includes("\0")),
13
+ description: z.string().max(4_096).refine((value) => !value.includes("\0")).optional(),
14
14
  /** 按 kind 的 adapter 配置(如 mcp-resource: { server })。 */
15
15
  adapterConfig: z.record(z.unknown()).default({}),
16
16
  /** 指向全局 CredentialStore 的 id;local-files/mock 不需要。 */
17
- credentialRef: z.string().optional(),
17
+ credentialRef: z.string().min(1).max(128).regex(/^[A-Za-z0-9_.-]+$/).optional(),
18
18
  enabled: z.boolean().default(true),
19
19
  });
20
20
  export const WorkspaceSourceBindingSchema = z.object({
21
21
  sourceId: z.string().regex(SOURCE_ID_RE),
22
22
  /** 显式勾选的 scope id;空数组 = 什么都不可见(不是"全部")。 */
23
- scopes: z.array(z.string()),
23
+ scopes: z
24
+ .array(z.string().min(1).max(512).refine((value) => !value.includes("\0")))
25
+ .max(1_000)
26
+ .refine((scopes) => new Set(scopes).size === scopes.length, "source scopes must be unique"),
24
27
  /** ask(默认,ReadSource 每次审批)| deny(只许 list metadata,禁读内容)。无 allow 档(ADR §1.2)。 */
25
28
  readPolicy: z.enum(["ask", "deny"]).default("ask"),
26
29
  });
@@ -8,13 +8,18 @@
8
8
  * executable content, so there are no hooks/mcp/skills to approve.
9
9
  */
10
10
  import { createHash, randomUUID } from "node:crypto";
11
- import { mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
11
+ import { constants } from "node:fs";
12
+ import { chmod, lstat, mkdir, mkdtemp, open, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises";
12
13
  import { dirname, join, resolve } from "node:path";
13
14
  import { detectThemeImage } from "./image.js";
14
15
  import { parseThemeManifest } from "./manifest.js";
15
16
  import { THEME_ASSET_DIR, ThemeInstallError, ThemeReviewChangedError, assertSafeThemeName, themeInstallDir, themesRegistryPath, themesRoot, } from "./paths.js";
17
+ import { lock } from "../utils/lockfile.js";
16
18
  const MANIFEST_FILE = ".cs-theme.json";
17
19
  const MAX_ASSET_BYTES = 8 * 1024 * 1024;
20
+ const MAX_MANIFEST_BYTES = 1024 * 1024;
21
+ const MAX_REGISTRY_BYTES = 1024 * 1024;
22
+ const MAX_INSTALLED_THEMES = 1_000;
18
23
  async function readImageAsset(sourceDir, rel) {
19
24
  const abs = resolve(sourceDir, rel);
20
25
  if (!abs.startsWith(resolve(sourceDir) + "/") && abs !== resolve(sourceDir)) {
@@ -88,7 +93,7 @@ function digestProjection(manifest, bytesByName) {
88
93
  async function loadManifest(sourceDir) {
89
94
  let raw;
90
95
  try {
91
- raw = await readFile(join(sourceDir, MANIFEST_FILE), "utf-8");
96
+ raw = await readBoundedText(join(sourceDir, MANIFEST_FILE), MAX_MANIFEST_BYTES);
92
97
  }
93
98
  catch {
94
99
  throw new ThemeInstallError(`missing ${MANIFEST_FILE}`);
@@ -102,6 +107,23 @@ async function loadManifest(sourceDir) {
102
107
  }
103
108
  return parseThemeManifest(json);
104
109
  }
110
+ async function readBoundedText(path, maxBytes) {
111
+ const metadata = await lstat(path);
112
+ if (metadata.isSymbolicLink() || !metadata.isFile() || metadata.size > maxBytes) {
113
+ throw new Error("state must be a bounded regular file");
114
+ }
115
+ const handle = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
116
+ try {
117
+ const opened = await handle.stat();
118
+ if (!opened.isFile() || opened.size > maxBytes) {
119
+ throw new Error("state must be a bounded regular file");
120
+ }
121
+ return await handle.readFile("utf8");
122
+ }
123
+ finally {
124
+ await handle.close();
125
+ }
126
+ }
105
127
  function previewOf(manifest, bytesByName, warnings) {
106
128
  const petStates = ["idle", "running", "alert"].filter((s) => manifest.pet?.[s]);
107
129
  const wallpaperModes = ["light", "dark"].filter((m) => manifest.wallpaper?.[m]);
@@ -164,19 +186,68 @@ export async function installReviewedLocalTheme(sourceDir, reviewToken) {
164
186
  mode: 0o600,
165
187
  });
166
188
  }
167
- await rm(finalDir, { recursive: true, force: true });
168
- await rename(staging, finalDir);
189
+ const entry = {
190
+ id: manifest.id,
191
+ name: manifest.name,
192
+ version: manifest.version,
193
+ installedAt: Date.now(),
194
+ };
195
+ await withRegistryLock(async (registry) => {
196
+ const next = {
197
+ version: 1,
198
+ themes: [...registry.themes.filter((theme) => theme.id !== entry.id), entry],
199
+ };
200
+ assertRegistryCount(next);
201
+ // Keep the previous install recoverable until the registry commit has
202
+ // succeeded. Reading/validating the registry above happens before any
203
+ // directory mutation, so corrupt legacy state cannot delete a theme.
204
+ const backup = `${finalDir}.replace-${process.pid}-${randomUUID()}`;
205
+ let hasBackup = false;
206
+ let installedNew = false;
207
+ try {
208
+ try {
209
+ const existing = await lstat(finalDir);
210
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
211
+ throw new Error("installed theme target must be a real directory");
212
+ }
213
+ await rename(finalDir, backup);
214
+ hasBackup = true;
215
+ }
216
+ catch (error) {
217
+ if (error.code !== "ENOENT")
218
+ throw error;
219
+ }
220
+ await rename(staging, finalDir);
221
+ installedNew = true;
222
+ await writeRegistry(next);
223
+ }
224
+ catch (error) {
225
+ if (installedNew)
226
+ await rm(finalDir, { recursive: true, force: true });
227
+ if (hasBackup) {
228
+ try {
229
+ await rename(backup, finalDir);
230
+ hasBackup = false;
231
+ }
232
+ catch {
233
+ throw new Error("theme install failed and the previous version could not be restored", {
234
+ cause: error,
235
+ });
236
+ }
237
+ }
238
+ throw error;
239
+ }
240
+ // A committed install remains valid even if best-effort cleanup of the
241
+ // hidden old directory is interrupted; a later maintenance pass can
242
+ // safely remove the orphan.
243
+ if (hasBackup)
244
+ await rm(backup, { recursive: true, force: true }).catch(() => undefined);
245
+ });
169
246
  }
170
247
  catch (error) {
171
248
  await rm(staging, { recursive: true, force: true }).catch(() => undefined);
172
249
  throw error;
173
250
  }
174
- await appendRegistryEntry({
175
- id: manifest.id,
176
- name: manifest.name,
177
- version: manifest.version,
178
- installedAt: Date.now(),
179
- });
180
251
  return toInstalledTheme(canonicalManifestFor(manifest, assets));
181
252
  }
182
253
  /** Canonical manifest stored on disk: asset fields point at THEME_ASSET_DIR/*. */
@@ -233,27 +304,89 @@ function toInstalledTheme(manifest) {
233
304
  }
234
305
  async function readRegistry() {
235
306
  try {
236
- const raw = await readFile(themesRegistryPath(), "utf-8");
307
+ const raw = await readBoundedText(themesRegistryPath(), MAX_REGISTRY_BYTES);
237
308
  const parsed = JSON.parse(raw);
238
- if (parsed?.version === 1 && Array.isArray(parsed.themes))
309
+ if (parsed?.version === 1 &&
310
+ Array.isArray(parsed.themes) &&
311
+ parsed.themes.length <= MAX_INSTALLED_THEMES &&
312
+ parsed.themes.every((theme) => theme &&
313
+ typeof theme === "object" &&
314
+ typeof theme.id === "string" &&
315
+ typeof theme.name === "string" &&
316
+ theme.name.length <= 256 &&
317
+ typeof theme.version === "string" &&
318
+ theme.version.length <= 128 &&
319
+ Number.isSafeInteger(theme.installedAt) &&
320
+ theme.installedAt >= 0)) {
321
+ for (const theme of parsed.themes)
322
+ assertSafeThemeName(theme.id);
239
323
  return parsed;
324
+ }
325
+ throw new Error("theme registry is corrupt");
240
326
  }
241
- catch {
242
- /* missing/corrupt empty */
327
+ catch (error) {
328
+ if (error.code === "ENOENT")
329
+ return { version: 1, themes: [] };
330
+ throw error;
243
331
  }
244
- return { version: 1, themes: [] };
245
332
  }
246
333
  async function writeRegistry(registry) {
247
- await mkdir(dirname(themesRegistryPath()), { recursive: true, mode: 0o700 });
248
- const tmp = `${themesRegistryPath()}.${process.pid}.${randomUUID()}.tmp`;
249
- await writeFile(tmp, `${JSON.stringify(registry, null, 2)}\n`, { mode: 0o600 });
250
- await rename(tmp, themesRegistryPath());
334
+ const path = themesRegistryPath();
335
+ const parent = dirname(path);
336
+ await mkdir(parent, { recursive: true, mode: 0o700 });
337
+ const parentInfo = await lstat(parent);
338
+ if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory()) {
339
+ throw new Error("theme registry parent must be a real directory");
340
+ }
341
+ try {
342
+ const target = await lstat(path);
343
+ if (target.isSymbolicLink() || !target.isFile()) {
344
+ throw new Error("theme registry target must be a regular file");
345
+ }
346
+ }
347
+ catch (error) {
348
+ if (error.code !== "ENOENT")
349
+ throw error;
350
+ }
351
+ const serialized = `${JSON.stringify(registry, null, 2)}\n`;
352
+ if (Buffer.byteLength(serialized, "utf8") > MAX_REGISTRY_BYTES) {
353
+ throw new Error("theme registry is too large");
354
+ }
355
+ const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
356
+ try {
357
+ await writeFile(tmp, serialized, { mode: 0o600, flag: "wx" });
358
+ if (process.platform !== "win32")
359
+ await chmod(tmp, 0o600);
360
+ await rename(tmp, path);
361
+ }
362
+ finally {
363
+ await rm(tmp, { force: true }).catch(() => undefined);
364
+ }
365
+ }
366
+ async function withRegistryLock(operation) {
367
+ const directory = dirname(themesRegistryPath());
368
+ await mkdir(directory, { recursive: true, mode: 0o700 });
369
+ const metadata = await lstat(directory);
370
+ if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
371
+ throw new Error("theme registry parent must be a real directory");
372
+ }
373
+ const release = await lock(directory, {
374
+ realpath: true,
375
+ stale: 10_000,
376
+ retries: { retries: 80, minTimeout: 10, maxTimeout: 120, factor: 1.3 },
377
+ });
378
+ try {
379
+ const registry = await readRegistry();
380
+ return await operation(registry);
381
+ }
382
+ finally {
383
+ await release();
384
+ }
251
385
  }
252
- async function appendRegistryEntry(entry) {
253
- const registry = await readRegistry();
254
- registry.themes = registry.themes.filter((t) => t.id !== entry.id);
255
- registry.themes.push(entry);
256
- await writeRegistry(registry);
386
+ function assertRegistryCount(registry) {
387
+ if (registry.themes.length > MAX_INSTALLED_THEMES) {
388
+ throw new Error("too many installed themes");
389
+ }
257
390
  }
258
391
  /** Read all installed themes (manifest re-read from disk), skipping broken ones. */
259
392
  export async function listInstalledThemes() {
@@ -283,8 +416,39 @@ export async function listInstalledThemes() {
283
416
  /** Remove an installed theme and its registry entry. */
284
417
  export async function uninstallTheme(id) {
285
418
  assertSafeThemeName(id);
286
- await rm(themeInstallDir(id), { recursive: true, force: true });
287
- const registry = await readRegistry();
288
- registry.themes = registry.themes.filter((t) => t.id !== id);
289
- await writeRegistry(registry);
419
+ const finalDir = themeInstallDir(id);
420
+ await withRegistryLock(async (registry) => {
421
+ const next = {
422
+ version: 1,
423
+ themes: registry.themes.filter((theme) => theme.id !== id),
424
+ };
425
+ const quarantine = `${finalDir}.remove-${process.pid}-${randomUUID()}`;
426
+ let quarantined = false;
427
+ try {
428
+ const existing = await lstat(finalDir);
429
+ if (existing.isSymbolicLink() || !existing.isDirectory()) {
430
+ throw new Error("installed theme target must be a real directory");
431
+ }
432
+ await rename(finalDir, quarantine);
433
+ quarantined = true;
434
+ }
435
+ catch (error) {
436
+ if (error.code !== "ENOENT")
437
+ throw error;
438
+ }
439
+ try {
440
+ await writeRegistry(next);
441
+ }
442
+ catch (error) {
443
+ if (quarantined) {
444
+ await rename(quarantine, finalDir);
445
+ }
446
+ throw error;
447
+ }
448
+ // Registry commit is authoritative. Directory cleanup is retryable and
449
+ // must not turn a successful uninstall into a registry/files mismatch.
450
+ if (quarantined) {
451
+ await rm(quarantine, { recursive: true, force: true }).catch(() => undefined);
452
+ }
453
+ });
290
454
  }
@@ -8,6 +8,7 @@
8
8
  * Side effects (network + git clone + disk write) → permissionDefault "ask".
9
9
  */
10
10
  import { addMarketplace } from "../../plugins/marketplaceManager.js";
11
+ import { isValidMarketplaceName } from "../../plugins/knownMarketplaces.js";
11
12
  export const addMarketplaceToolDef = {
12
13
  name: "AddMarketplace",
13
14
  description: "Register a plugin marketplace source so the user can browse and install " +
@@ -44,6 +45,13 @@ export async function addMarketplaceTool(args) {
44
45
  if (typeof name !== "string" || !name.trim()) {
45
46
  return "Error: name is required";
46
47
  }
48
+ // Validate against the SAME rule the registry enforces on read. Accepting a
49
+ // name here that knownMarketplaces later rejects would clone the repo and
50
+ // then fail to persist it, leaving an orphaned directory behind.
51
+ if (!isValidMarketplaceName(name)) {
52
+ return (`Error: marketplace name '${name}' is not valid. Use letters or digits, ` +
53
+ "then letters, digits, '.', '_' or '-' (max 128 characters).");
54
+ }
47
55
  const sourceType = args.source_type;
48
56
  let source;
49
57
  if (sourceType === "github") {
@@ -68,7 +76,19 @@ export async function addMarketplaceTool(args) {
68
76
  if (!result.ok) {
69
77
  return `Error adding marketplace ${name}: ${result.error}`;
70
78
  }
71
- return `Marketplace '${name}' added. The user can now browse and install plugins from it in the Extensions → Market UI.`;
79
+ const preview = result.marketplace.plugins
80
+ .slice(0, 25)
81
+ .map((plugin) => `- ${plugin.name}${plugin.description ? ` — ${plugin.description}` : ""}`)
82
+ .join("\n");
83
+ const omitted = Math.max(0, result.marketplace.plugins.length - 25);
84
+ return [
85
+ `Marketplace '${name}' added (${result.marketplace.plugins.length} plugins).`,
86
+ preview,
87
+ omitted > 0 ? `… ${omitted} more plugins; browse them in Extensions → Market.` : "",
88
+ "To install one in this conversation, call InstallCapability with kind='plugin', its exact plugin name, and this marketplace name.",
89
+ ]
90
+ .filter(Boolean)
91
+ .join("\n");
72
92
  }
73
93
  catch (err) {
74
94
  return `Error adding marketplace ${name}: ${err.message}`;
@@ -2,6 +2,7 @@
2
2
  * Cron tools — CronCreate, CronDelete, CronList.
3
3
  */
4
4
  import type { ToolDefinition } from "../../types.js";
5
+ import type { ToolContext } from "../context.js";
5
6
  export { cronListToolDef } from "./cron-list.definition.js";
6
7
  /** Sink notified after a cron job is created/deleted, so the host (Electron
7
8
  * main) can reload+arm the scheduler that actually executes jobs. The worker
@@ -10,7 +11,7 @@ export { cronListToolDef } from "./cron-list.definition.js";
10
11
  type CronChangedSink = () => void;
11
12
  export declare function setCronChangedSink(sink: CronChangedSink | null): void;
12
13
  export declare const cronCreateToolDef: ToolDefinition;
13
- export declare function cronCreateTool(args: Record<string, unknown>): Promise<string>;
14
+ export declare function cronCreateTool(args: Record<string, unknown>, context?: ToolContext): Promise<string>;
14
15
  export declare const cronDeleteToolDef: ToolDefinition;
15
16
  export declare function cronDeleteTool(args: Record<string, unknown>): Promise<string>;
16
17
  export declare function cronListTool(_args: Record<string, unknown>): Promise<string>;
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Cron tools — CronCreate, CronDelete, CronList.
3
3
  */
4
+ import { resolve } from "node:path";
4
5
  import { cronScheduler } from "../../automation/scheduler.js";
5
6
  import { getCurrentSid } from "../../logging/logger.js";
6
7
  export { cronListToolDef } from "./cron-list.definition.js";
@@ -84,12 +85,16 @@ export const cronCreateToolDef = {
84
85
  required: ["name", "schedule", "prompt"],
85
86
  },
86
87
  };
87
- export async function cronCreateTool(args) {
88
- const name = args.name;
89
- const schedule = args.schedule;
90
- const prompt = args.prompt;
91
- if (!name || !schedule || !prompt)
88
+ export async function cronCreateTool(args, context) {
89
+ const name = typeof args.name === "string" ? args.name : "";
90
+ const schedule = typeof args.schedule === "string" ? args.schedule : "";
91
+ const prompt = typeof args.prompt === "string" ? args.prompt : "";
92
+ if (!name.trim() || !schedule.trim() || !prompt.trim()) {
92
93
  return "Error: name, schedule, and prompt are required";
94
+ }
95
+ if (name.length > 512 || schedule.length > 512 || prompt.length > 1024 * 1024) {
96
+ return "Error: automation payload is too large";
97
+ }
93
98
  const timezone = typeof args.timezone === "string"
94
99
  ? args.timezone
95
100
  : (() => {
@@ -100,7 +105,16 @@ export async function cronCreateTool(args) {
100
105
  return undefined;
101
106
  }
102
107
  })();
103
- const cwd = typeof args.cwd === "string" ? args.cwd : undefined;
108
+ const requestedCwd = typeof args.cwd === "string" && args.cwd ? args.cwd : undefined;
109
+ if (requestedCwd && requestedCwd.length > 32_768)
110
+ return "Error: automation cwd is too long";
111
+ if (context && requestedCwd && resolve(requestedCwd) !== resolve(context.cwd)) {
112
+ return "Error: CronCreate cannot schedule work outside the current session workspace";
113
+ }
114
+ // The model may omit cwd, but production tool calls always have a host-owned
115
+ // context. Bind to that authoritative workspace instead of creating a job
116
+ // that later falls back to an unrelated process cwd.
117
+ const cwd = context?.cwd ?? requestedCwd;
104
118
  const once = args.once === true;
105
119
  const permissionLevel = args.permissionLevel === "read-only" ||
106
120
  args.permissionLevel === "workspace-write" ||
@@ -32,6 +32,7 @@ import { memoryListToolDef, memoryListTool, memoryReadToolDef, memoryReadTool, m
32
32
  import { completeGoalToolDef, completeGoalTool } from "./complete-goal.js";
33
33
  import { cancelGoalToolDef, cancelGoalTool } from "./cancel-goal.js";
34
34
  import { addMarketplaceToolDef, addMarketplaceTool } from "./add-marketplace.js";
35
+ import { installCapabilityToolDef, installCapabilityTool } from "./install-capability.js";
35
36
  import { bashOutputToolDef, bashOutputTool, killShellToolDef, killShellTool, listShellsToolDef, listShellsTool, } from "./background-shell-tools.js";
36
37
  import { browserObserveToolDef, browserObserveTool, browserActToolDef, browserActTool, browserNavigateToolDef, browserNavigateTool, } from "./browser-tools.js";
37
38
  import { panelToolDef, panelTool } from "./panel.js";
@@ -310,7 +311,10 @@ const BUILTIN_CONTRIBUTIONS = [
310
311
  timeoutMs: 1_800_000, // 30min — sub-agent runs may execute many tool calls
311
312
  },
312
313
  execute: agentTool,
313
- exposure: expose(HARNESS_TAGS, { defaultPermissionRules: allow(agentToolDef.name) }),
314
+ exposure: expose(HARNESS_TAGS, {
315
+ defaultPermissionRules: allow(agentToolDef.name),
316
+ availability: (ctx) => ctx.behaviorProfile !== "quickChatRestricted",
317
+ }),
314
318
  },
315
319
  {
316
320
  definition: {
@@ -646,6 +650,49 @@ const BUILTIN_CONTRIBUTIONS = [
646
650
  execute: addMarketplaceTool,
647
651
  exposure: expose(GENERAL_TAGS),
648
652
  },
653
+ // ─── Conversational capability installation ───────────────────
654
+ // Mirrors the host-owned install lifecycle used by the settings UI: the
655
+ // exact plugin/Skill/MCP source is visible in the approval card, then the
656
+ // implementation delegates to the existing installer/settings services.
657
+ {
658
+ definition: {
659
+ ...installCapabilityToolDef,
660
+ source: "builtin",
661
+ permissionDefault: "ask",
662
+ isReadOnly: false,
663
+ isConcurrencySafe: false,
664
+ timeoutMs: 190_000,
665
+ },
666
+ execute: installCapabilityTool,
667
+ exposure: expose(GENERAL_TAGS, {
668
+ defaultPermissionRules: [
669
+ {
670
+ tool: installCapabilityToolDef.name,
671
+ argsPattern: { action: "^list$" },
672
+ decision: "allow",
673
+ reason: "Capability listing is read-only",
674
+ },
675
+ {
676
+ tool: installCapabilityToolDef.name,
677
+ argsPattern: { action: "^inspect$", kind: "^(plugin|mcp)$" },
678
+ decision: "allow",
679
+ reason: "Installed configuration and marketplace metadata inspection are read-only",
680
+ },
681
+ {
682
+ tool: installCapabilityToolDef.name,
683
+ argsPattern: { action: "^inspect$", kind: "^skill$" },
684
+ decision: "ask",
685
+ reason: "Skill repository inspection starts an external discovery command",
686
+ },
687
+ {
688
+ tool: installCapabilityToolDef.name,
689
+ argsPattern: { action: "^(install|update|enable|disable|uninstall)$" },
690
+ decision: "ask",
691
+ reason: "Capability lifecycle mutations change installed code or settings",
692
+ },
693
+ ],
694
+ }),
695
+ },
649
696
  // Browser automation — 3 semantic tools driving the in-app webview via the
650
697
  // BrowserBridge (CDP). All serial on one webview (isConcurrencySafe:false).
651
698
  // browser_observe is read-only. browser_act declares a UI hint of allow; its
@@ -0,0 +1,52 @@
1
+ /**
2
+ * First-party conversational lifecycle manager for CodeShell capabilities.
3
+ *
4
+ * Read-only list/inspect actions are preset-allowed; every mutation is narrowed
5
+ * by an action-specific permission rule and remains approval-gated. Arguments
6
+ * contain the exact source, scope and executable/URL that will be persisted or
7
+ * run. The implementation deliberately reuses host installers instead of
8
+ * asking the model to synthesize shell commands or edit settings JSON by hand.
9
+ */
10
+ import type { ToolDefinition } from "../../types.js";
11
+ import type { ToolContext } from "../context.js";
12
+ import { SettingsManager } from "../../settings/manager.js";
13
+ import { invalidateSkillCache, scanSkills } from "../../skills/scanner.js";
14
+ import { installPlugin, listInstalled, uninstallPlugin } from "../../plugins/pluginInstaller.js";
15
+ import { describePluginContent } from "../../plugins/pluginContent.js";
16
+ import { listPluginMcpTrust } from "../../plugins/pluginMcpApproval.js";
17
+ import { refreshMarketplace } from "../../plugins/marketplaceManager.js";
18
+ import { type LocalPluginPreview } from "../../plugins/installer/preview.js";
19
+ import type { PluginMarketplaceEntry } from "../../plugins/types.js";
20
+ import { computeEffectiveDisabledLists } from "../../capability-control/disabled-lists.js";
21
+ import { safeSpawn } from "../../runtime/safe-spawn.js";
22
+ import { resolveExecutable } from "../../utils/exec.js";
23
+ interface MarketplacePluginPreview {
24
+ entry: PluginMarketplaceEntry;
25
+ inventory: LocalPluginPreview | null;
26
+ note?: string;
27
+ }
28
+ type CapabilityChangedSink = () => void;
29
+ /** Host notification seam. Desktop maps this to agent/settingsChanged. */
30
+ export declare function setCapabilityChangedSink(sink: CapabilityChangedSink | null): void;
31
+ export declare const installCapabilityToolDef: ToolDefinition;
32
+ export interface InstallCapabilityDeps {
33
+ computeEffectiveDisabledLists: typeof computeEffectiveDisabledLists;
34
+ describePluginContent: typeof describePluginContent;
35
+ installPlugin: typeof installPlugin;
36
+ invalidateSkillCache: typeof invalidateSkillCache;
37
+ listInstalled: typeof listInstalled;
38
+ listPluginMcpTrust: typeof listPluginMcpTrust;
39
+ makeSettingsManager: (cwd: string, scope: "full" | "project") => SettingsManager;
40
+ previewMarketplacePlugin: (plugin: string, marketplace: string) => Promise<MarketplacePluginPreview | {
41
+ error: string;
42
+ }>;
43
+ refreshMarketplace: typeof refreshMarketplace;
44
+ resolveExecutable: typeof resolveExecutable;
45
+ safeSpawn: typeof safeSpawn;
46
+ scanSkills: typeof scanSkills;
47
+ uninstallPlugin: typeof uninstallPlugin;
48
+ }
49
+ export declare function installCapabilityTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
50
+ /** Test seam: production always calls {@link installCapabilityTool}. */
51
+ export declare function installCapabilityWithDeps(args: Record<string, unknown>, ctx: ToolContext | undefined, deps: InstallCapabilityDeps): Promise<string>;
52
+ export {};