@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
@@ -70,7 +70,9 @@ export async function skillTool(args, ctx) {
70
70
  });
71
71
  const found = skills.find((s) => s.name === skillName);
72
72
  if (!found) {
73
- return `Skill "${skillName}" not found. Run /skills to list available skills.`;
73
+ return (`Skill "${skillName}" not found. Run /skills to list available skills. ` +
74
+ "If the user asked to add it and you know its trusted GitHub source, use " +
75
+ "InstallCapability with kind='skill' instead of constructing a shell command.");
74
76
  }
75
77
  const skillDir = dirname(found.filePath);
76
78
  const body = found.content
@@ -502,6 +502,7 @@ export class ToolExecutor {
502
502
  toolCallId: call.id,
503
503
  result: observerResult,
504
504
  error: result.error,
505
+ isError: result.isError,
505
506
  signal: this.signal ?? this.toolCtx?.signal,
506
507
  });
507
508
  // 5. Post-tool-use hook (after execution, can observe/modify result)
@@ -449,8 +449,6 @@ function isRegisteredSkillResourceRead(resolved) {
449
449
  return false;
450
450
  if (isSkillTreeResource(resolved, join(codeShellRoot, "skills")))
451
451
  return true;
452
- if (isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot))
453
- return true;
454
452
  let cacheRoot;
455
453
  try {
456
454
  cacheRoot = realpathSync(join(codeShellRoot, "plugins", "cache"));
@@ -479,20 +477,26 @@ function isRegisteredSkillResourceRead(resolved) {
479
477
  return false;
480
478
  }
481
479
  /**
482
- * Panel App Skills live under the otherwise-sensitive
483
- * `~/.code-shell/panel-apps` tree. Installation already reviews and copies the
484
- * package, and the Skill scanner exposes only entries declared by the app
485
- * manifest. Mirror that exact boundary here so reading a Skill reference does
486
- * not trigger a second approval prompt.
480
+ * Installed Panel Apps live under the otherwise-sensitive
481
+ * `~/.code-shell/panel-apps` tree. Their package contents are reviewed and
482
+ * copied by the installer, so ordinary source, manifests, assets and declared
483
+ * Agent resources should not require a second approval merely because their
484
+ * parent directory is `~/.code-shell`.
487
485
  *
488
- * Registry, app root, manifest, declared SKILL.md and target are all
489
- * realpathed and containment-checked. This deliberately does not trust an
490
- * undeclared Skill directory or a symlink escaping the installed app.
486
+ * Keep the exception read-only at the call site and require a valid installed
487
+ * registry entry plus a matching V2 manifest. Registry, app root, manifest and
488
+ * target are all realpathed and containment-checked. Credential-shaped files
489
+ * are still caught by `SENSITIVE_FILE_PATTERNS` before this exception, and a
490
+ * symlink escaping the installed package never inherits its read authority.
491
491
  */
492
- function isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot) {
492
+ function isInstalledPanelAppResourceRead(resolved) {
493
+ let codeShellRoot;
493
494
  let appsRoot;
494
495
  let registryPath;
495
496
  try {
497
+ codeShellRoot = realpathSync(join(configuredUserHome(), ".code-shell"));
498
+ if (!isInsideDir(resolved, codeShellRoot))
499
+ return false;
496
500
  appsRoot = realpathSync(join(codeShellRoot, "panel-apps"));
497
501
  if (!isInsideDir(appsRoot, codeShellRoot))
498
502
  return false;
@@ -526,29 +530,10 @@ function isInstalledPanelAppSkillResourceRead(resolved, codeShellRoot) {
526
530
  if (!isInsideDir(manifestPath, appRoot) || !statSync(manifestPath).isFile())
527
531
  continue;
528
532
  const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
529
- if (manifest.schemaVersion !== 2 ||
530
- manifest.id !== id ||
531
- !Array.isArray(manifest.agent?.skills)) {
533
+ if (manifest.schemaVersion !== 2 || manifest.id !== id)
532
534
  continue;
533
- }
534
- for (const skillEntry of manifest.agent.skills) {
535
- if (typeof skillEntry !== "string")
536
- continue;
537
- const segments = skillEntry.split("/");
538
- if (segments.length !== 4 ||
539
- segments[0] !== "agent" ||
540
- segments[1] !== "skills" ||
541
- !/^[a-z][a-z0-9-]{0,63}$/.test(segments[2] ?? "") ||
542
- segments[3] !== "SKILL.md") {
543
- continue;
544
- }
545
- const skillManifest = realpathSync(join(appRoot, ...segments));
546
- if (!isInsideDir(skillManifest, appRoot) || !statSync(skillManifest).isFile())
547
- continue;
548
- const skillRoot = dirname(skillManifest);
549
- if (isInsideDir(resolved, skillRoot))
550
- return true;
551
- }
535
+ if (isInsideDir(resolved, appRoot))
536
+ return true;
552
537
  }
553
538
  catch {
554
539
  // A stale, malformed, or tampered Panel App entry grants no read access.
@@ -652,6 +637,13 @@ export function classifyPath(rawPath, opts) {
652
637
  resolvedPath: resolved,
653
638
  };
654
639
  }
640
+ if (opts.operation === "read" && !sensitiveFile && isInstalledPanelAppResourceRead(resolved)) {
641
+ return {
642
+ decision: "allow",
643
+ reason: "installed Panel App resource read",
644
+ resolvedPath: resolved,
645
+ };
646
+ }
655
647
  // Sensitive: write is always denied, read always asks. Workspace placement
656
648
  // doesn't soften the rule — an `.env` in the project still asks on read.
657
649
  if (sensitiveLabel) {
@@ -57,6 +57,11 @@ export class ToolRegistry {
57
57
  const fork = new ToolRegistry({ builtinTools: [] });
58
58
  fork.tools = new Map(this.tools);
59
59
  fork.builtinExecutors = new Map(this.builtinExecutors);
60
+ // Availability is part of the executable tool contract, not just prompt
61
+ // decoration. Engine instances run against this fork, so dropping the
62
+ // guards here lets a model invoke a tool that was correctly hidden from
63
+ // its current turn (for example GatewayReply in a desktop Mimi chat).
64
+ fork.availabilityGuards = new Map(this.availabilityGuards);
60
65
  return fork;
61
66
  }
62
67
  unregisterTool(name) {
@@ -62,4 +62,6 @@ export declare function mutateJsonFile<T, R = void>(file: string, options: {
62
62
  result?: R;
63
63
  };
64
64
  mode?: number;
65
+ /** Bounds both the current file and newly serialized state. */
66
+ maxBytes?: number;
65
67
  }): R | undefined;
@@ -19,7 +19,7 @@
19
19
  * `<dir>.lock`), matching CronStore. Locking the directory rather than the file
20
20
  * means a writer that creates the file for the first time is still serialized.
21
21
  */
22
- import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
22
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
23
23
  import { dirname } from "node:path";
24
24
  import { randomUUID } from "node:crypto";
25
25
  import { lockSync } from "./lockfile.js";
@@ -39,6 +39,7 @@ const LOCK_STALE_MS = 10_000;
39
39
  /** Backoff bounds for the retry loop (see acquireLockOn). */
40
40
  const LOCK_RETRY_MIN_MS = 5;
41
41
  const LOCK_RETRY_MAX_MS = 50;
42
+ const DEFAULT_JSON_FILE_MAX_BYTES = 16 * 1024 * 1024;
42
43
  // Atomics.wait only ever reads slot 0, which stays 0, so one shared buffer is
43
44
  // safe and avoids allocating per retry.
44
45
  const SLEEP_SIGNAL = new Int32Array(new SharedArrayBuffer(4));
@@ -123,19 +124,43 @@ export function writeFileAtomic(file, contents, mode) {
123
124
  * skips the write entirely (no-op mutations should not rewrite the file).
124
125
  */
125
126
  export function mutateJsonFile(file, options) {
127
+ const maxBytes = options.maxBytes ?? DEFAULT_JSON_FILE_MAX_BYTES;
128
+ if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0) {
129
+ throw new Error("JSON state maxBytes must be a positive safe integer");
130
+ }
126
131
  const release = acquireFileLock(file);
127
132
  try {
128
133
  let raw;
129
134
  try {
130
- raw = readFileSync(file, "utf-8");
135
+ const pathInfo = lstatSync(file);
136
+ if (pathInfo.isSymbolicLink() || !pathInfo.isFile() || pathInfo.size > maxBytes) {
137
+ throw new Error(`JSON state is not a bounded regular file: ${file}`);
138
+ }
139
+ const descriptor = openSync(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
140
+ try {
141
+ const opened = fstatSync(descriptor);
142
+ if (!opened.isFile() || opened.size > maxBytes) {
143
+ throw new Error(`JSON state is not a bounded regular file: ${file}`);
144
+ }
145
+ raw = readFileSync(descriptor, "utf-8");
146
+ }
147
+ finally {
148
+ closeSync(descriptor);
149
+ }
131
150
  }
132
- catch {
151
+ catch (error) {
152
+ if (error.code !== "ENOENT")
153
+ throw error;
133
154
  raw = undefined;
134
155
  }
135
156
  const current = options.parse(raw);
136
157
  const { value, result } = options.mutation(current);
137
158
  if (value !== undefined) {
138
- writeFileAtomic(file, options.serialize(value), options.mode);
159
+ const serialized = options.serialize(value);
160
+ if (Buffer.byteLength(serialized, "utf8") > maxBytes) {
161
+ throw new Error(`JSON state exceeds ${maxBytes} bytes`);
162
+ }
163
+ writeFileAtomic(file, serialized, options.mode);
139
164
  }
140
165
  return result;
141
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cjhyy/code-shell-core",
3
- "version": "0.8.8",
3
+ "version": "0.8.10",
4
4
  "description": "Core engine for code-shell — agent orchestration, tool execution, hooks, protocol. UI-agnostic.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -38,6 +38,7 @@
38
38
  ],
39
39
  "scripts": {
40
40
  "build": "bun run clean && tsc -p tsconfig.json && bun run copy-assets",
41
+ "typecheck": "tsc -p tsconfig.json --noEmit",
41
42
  "copy-assets": "node ../../scripts/copy-assets.mjs dist/prompt/sections src/prompt/sections/*.md && node ../../scripts/copy-assets.mjs dist/data src/data/*.json",
42
43
  "dev": "bun run copy-assets && tsc -p tsconfig.json --watch --preserveWatchOutput",
43
44
  "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\""