@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
@@ -1,4 +1,4 @@
1
- import { readInstalledPlugins, writeInstalledPlugins } from "./installedPlugins.js";
1
+ import { mutateInstalledPlugins, readInstalledPlugins } from "./installedPlugins.js";
2
2
  import { inspectPluginHooks, pluginHookReviewSnapshot, pluginHookApprovalState, } from "./pluginHookIntegrity.js";
3
3
  function pluginNameFromKey(key) {
4
4
  const at = key.lastIndexOf("@");
@@ -86,75 +86,71 @@ export function reviewPluginHooks(target) {
86
86
  }
87
87
  /** Approve the exact install-time digest; changed-on-disk hooks cannot be blessed. */
88
88
  export function approvePluginHooks(target) {
89
- const data = readInstalledPlugins();
90
- const keys = matchingInstallKeys(data, target);
91
- const out = [];
92
- let writeNeeded = false;
93
- for (const installKey of keys) {
94
- for (const entry of data.plugins[installKey] ?? []) {
95
- let entryChanged = false;
96
- const snapshot = inspectPluginHooks(entry.installPath);
97
- if (snapshot.state === "invalid") {
98
- throw new Error(`plugin "${installKey}" hooks definition is invalid and cannot be approved: ${snapshot.error ?? "unknown error"}`);
99
- }
100
- if (entry.hookDigest && snapshot.digest !== entry.hookDigest) {
101
- throw new Error(`plugin "${installKey}" hooks changed after install; reinstall or update before approving`);
102
- }
103
- if (!entry.hookDigest) {
104
- entry.hookDigest = snapshot.digest;
105
- entryChanged = true;
106
- writeNeeded = true;
107
- }
108
- if (entry.approvedHookDigest !== entry.hookDigest) {
109
- entry.approvedHookDigest = entry.hookDigest;
110
- entryChanged = true;
111
- writeNeeded = true;
112
- }
113
- const nextReview = pluginHookReviewSnapshot(snapshot);
114
- if (JSON.stringify(entry.approvedHookSnapshot) !== JSON.stringify(nextReview)) {
115
- entry.approvedHookSnapshot = nextReview;
116
- entryChanged = true;
117
- writeNeeded = true;
89
+ return (mutateInstalledPlugins((data) => {
90
+ const keys = matchingInstallKeys(data, target);
91
+ const out = [];
92
+ let writeNeeded = false;
93
+ for (const installKey of keys) {
94
+ for (const entry of data.plugins[installKey] ?? []) {
95
+ let entryChanged = false;
96
+ const snapshot = inspectPluginHooks(entry.installPath);
97
+ if (snapshot.state === "invalid") {
98
+ throw new Error(`plugin "${installKey}" hooks definition is invalid and cannot be approved: ${snapshot.error ?? "unknown error"}`);
99
+ }
100
+ if (entry.hookDigest && snapshot.digest !== entry.hookDigest) {
101
+ throw new Error(`plugin "${installKey}" hooks changed after install; reinstall or update before approving`);
102
+ }
103
+ if (!entry.hookDigest) {
104
+ entry.hookDigest = snapshot.digest;
105
+ entryChanged = true;
106
+ }
107
+ if (entry.approvedHookDigest !== entry.hookDigest) {
108
+ entry.approvedHookDigest = entry.hookDigest;
109
+ entryChanged = true;
110
+ }
111
+ const nextReview = pluginHookReviewSnapshot(snapshot);
112
+ if (JSON.stringify(entry.approvedHookSnapshot) !== JSON.stringify(nextReview)) {
113
+ entry.approvedHookSnapshot = nextReview;
114
+ entryChanged = true;
115
+ }
116
+ writeNeeded ||= entryChanged;
117
+ out.push(result(installKey, entry, entryChanged, snapshot));
118
118
  }
119
- out.push(result(installKey, entry, entryChanged, snapshot));
120
119
  }
121
- }
122
- if (writeNeeded)
123
- writeInstalledPlugins(data);
124
- return out;
120
+ return { ...(writeNeeded ? { value: data } : {}), result: out };
121
+ }) ?? []);
125
122
  }
126
123
  /**
127
124
  * Revoke hook execution. Legacy entries are first migrated to a recorded
128
125
  * digest so revocation fails closed instead of falling back to legacy trust.
129
126
  */
130
127
  export function revokePluginHooks(target) {
131
- const data = readInstalledPlugins();
132
- const keys = matchingInstallKeys(data, target);
133
- const out = [];
134
- let writeNeeded = false;
135
- for (const installKey of keys) {
136
- for (const entry of data.plugins[installKey] ?? []) {
137
- let entryChanged = false;
138
- const snapshot = inspectPluginHooks(entry.installPath);
139
- if (!entry.hookDigest) {
140
- entry.hookDigest = snapshot.digest;
141
- entryChanged = true;
142
- }
143
- if (snapshot.hasExecutableHooks || snapshot.state === "invalid") {
144
- if (entry.approvedHookDigest !== undefined) {
145
- delete entry.approvedHookDigest;
128
+ return (mutateInstalledPlugins((data) => {
129
+ const keys = matchingInstallKeys(data, target);
130
+ const out = [];
131
+ let writeNeeded = false;
132
+ for (const installKey of keys) {
133
+ for (const entry of data.plugins[installKey] ?? []) {
134
+ let entryChanged = false;
135
+ const snapshot = inspectPluginHooks(entry.installPath);
136
+ if (!entry.hookDigest) {
137
+ entry.hookDigest = snapshot.digest;
146
138
  entryChanged = true;
147
139
  }
140
+ if (snapshot.hasExecutableHooks || snapshot.state === "invalid") {
141
+ if (entry.approvedHookDigest !== undefined) {
142
+ delete entry.approvedHookDigest;
143
+ entryChanged = true;
144
+ }
145
+ }
146
+ else if (entry.approvedHookDigest !== entry.hookDigest) {
147
+ entry.approvedHookDigest = entry.hookDigest;
148
+ entryChanged = true;
149
+ }
150
+ writeNeeded ||= entryChanged;
151
+ out.push(result(installKey, entry, entryChanged, snapshot));
148
152
  }
149
- else if (entry.approvedHookDigest !== entry.hookDigest) {
150
- entry.approvedHookDigest = entry.hookDigest;
151
- entryChanged = true;
152
- }
153
- writeNeeded ||= entryChanged;
154
- out.push(result(installKey, entry, entryChanged, snapshot));
155
153
  }
156
- }
157
- if (writeNeeded)
158
- writeInstalledPlugins(data);
159
- return out;
154
+ return { ...(writeNeeded ? { value: data } : {}), result: out };
155
+ }) ?? []);
160
156
  }
@@ -1,4 +1,4 @@
1
- import { readInstalledPlugins, writeInstalledPlugins } from "./installedPlugins.js";
1
+ import { mutateInstalledPlugins, readInstalledPlugins } from "./installedPlugins.js";
2
2
  import { readPluginMcp } from "./installer/loadPluginMcp.js";
3
3
  import { pluginMcpApprovalState, pluginMcpDigest, } from "./pluginMcpIntegrity.js";
4
4
  function pluginNameFromKey(key) {
@@ -44,67 +44,65 @@ export function listPluginMcpTrust() {
44
44
  }
45
45
  /** Approve only the install-time MCP bytes; tampered bytes require update/reinstall. */
46
46
  export function approvePluginMcp(target) {
47
- const data = readInstalledPlugins();
48
- const keys = matchingInstallKeys(data, target);
49
- const out = [];
50
- let writeNeeded = false;
51
- for (const installKey of keys) {
52
- for (const entry of data.plugins[installKey] ?? []) {
53
- let entryChanged = false;
54
- const currentDigest = pluginMcpDigest(entry.installPath);
55
- if (entry.mcpDigest && currentDigest !== entry.mcpDigest) {
56
- throw new Error(`plugin "${installKey}" MCP config changed after install; reinstall or update before approving`);
57
- }
58
- if (!entry.mcpDigest) {
59
- entry.mcpDigest = currentDigest;
60
- entryChanged = true;
61
- }
62
- if (entry.approvedMcpDigest !== entry.mcpDigest) {
63
- entry.approvedMcpDigest = entry.mcpDigest;
64
- entryChanged = true;
47
+ return (mutateInstalledPlugins((data) => {
48
+ const keys = matchingInstallKeys(data, target);
49
+ const out = [];
50
+ let writeNeeded = false;
51
+ for (const installKey of keys) {
52
+ for (const entry of data.plugins[installKey] ?? []) {
53
+ let entryChanged = false;
54
+ const currentDigest = pluginMcpDigest(entry.installPath);
55
+ if (entry.mcpDigest && currentDigest !== entry.mcpDigest) {
56
+ throw new Error(`plugin "${installKey}" MCP config changed after install; reinstall or update before approving`);
57
+ }
58
+ if (!entry.mcpDigest) {
59
+ entry.mcpDigest = currentDigest;
60
+ entryChanged = true;
61
+ }
62
+ if (entry.approvedMcpDigest !== entry.mcpDigest) {
63
+ entry.approvedMcpDigest = entry.mcpDigest;
64
+ entryChanged = true;
65
+ }
66
+ writeNeeded ||= entryChanged;
67
+ out.push(trustEntry(installKey, entry, entryChanged));
65
68
  }
66
- writeNeeded ||= entryChanged;
67
- out.push(trustEntry(installKey, entry, entryChanged));
68
69
  }
69
- }
70
- if (writeNeeded)
71
- writeInstalledPlugins(data);
72
- return out;
70
+ return { ...(writeNeeded ? { value: data } : {}), result: out };
71
+ }) ?? []);
73
72
  }
74
73
  /**
75
74
  * Revoke plugin MCP execution. Legacy entries first record their current
76
75
  * digest, preventing revocation from falling back to legacy compatibility.
77
76
  */
78
77
  export function revokePluginMcp(target) {
79
- const data = readInstalledPlugins();
80
- const keys = matchingInstallKeys(data, target);
81
- const out = [];
82
- let writeNeeded = false;
83
- for (const installKey of keys) {
84
- const plugin = pluginNameFromKey(installKey);
85
- for (const entry of data.plugins[installKey] ?? []) {
86
- let entryChanged = false;
87
- const hasServers = Object.keys(readPluginMcp(entry.installPath, plugin)).length > 0;
88
- const currentDigest = pluginMcpDigest(entry.installPath);
89
- if (!entry.mcpDigest) {
90
- entry.mcpDigest = currentDigest;
91
- entryChanged = true;
92
- }
93
- if (hasServers || currentDigest !== entry.mcpDigest) {
94
- if (entry.approvedMcpDigest !== undefined) {
95
- delete entry.approvedMcpDigest;
78
+ return (mutateInstalledPlugins((data) => {
79
+ const keys = matchingInstallKeys(data, target);
80
+ const out = [];
81
+ let writeNeeded = false;
82
+ for (const installKey of keys) {
83
+ const plugin = pluginNameFromKey(installKey);
84
+ for (const entry of data.plugins[installKey] ?? []) {
85
+ let entryChanged = false;
86
+ const hasServers = Object.keys(readPluginMcp(entry.installPath, plugin)).length > 0;
87
+ const currentDigest = pluginMcpDigest(entry.installPath);
88
+ if (!entry.mcpDigest) {
89
+ entry.mcpDigest = currentDigest;
96
90
  entryChanged = true;
97
91
  }
92
+ if (hasServers || currentDigest !== entry.mcpDigest) {
93
+ if (entry.approvedMcpDigest !== undefined) {
94
+ delete entry.approvedMcpDigest;
95
+ entryChanged = true;
96
+ }
97
+ }
98
+ else if (entry.approvedMcpDigest !== entry.mcpDigest) {
99
+ entry.approvedMcpDigest = entry.mcpDigest;
100
+ entryChanged = true;
101
+ }
102
+ writeNeeded ||= entryChanged;
103
+ out.push(trustEntry(installKey, entry, entryChanged));
98
104
  }
99
- else if (entry.approvedMcpDigest !== entry.mcpDigest) {
100
- entry.approvedMcpDigest = entry.mcpDigest;
101
- entryChanged = true;
102
- }
103
- writeNeeded ||= entryChanged;
104
- out.push(trustEntry(installKey, entry, entryChanged));
105
105
  }
106
- }
107
- if (writeNeeded)
108
- writeInstalledPlugins(data);
109
- return out;
106
+ return { ...(writeNeeded ? { value: data } : {}), result: out };
107
+ }) ?? []);
110
108
  }
@@ -8,7 +8,7 @@
8
8
  * ~/.code-shell/human-repos.json —— 注册表(owner/repo 列表)
9
9
  * ~/.code-shell/human-repos/<key>/ —— 各仓库的浅克隆
10
10
  */
11
- import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
11
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readdirSync, readFileSync, renameSync, rmSync, writeFileSync, } from "node:fs";
12
12
  import { randomUUID } from "node:crypto";
13
13
  import { basename, dirname, join } from "node:path";
14
14
  import { codeShellHome } from "../session/session-manager.js";
@@ -19,6 +19,7 @@ import { acquireLockOnPath, mutateJsonFile } from "../utils/file-mutex.js";
19
19
  import { gitClone, githubRepoToCloneUrl } from "../plugins/gitOps.js";
20
20
  import { CATALOG_REPO_RE, readCatalogFromDir, sourceToRepoKey, } from "./catalog.js";
21
21
  const MAX_REPOS = 32;
22
+ const MAX_REPO_REGISTRY_BYTES = 256 * 1024;
22
23
  export function humanReposRoot() {
23
24
  return join(codeShellHome(), "human-repos");
24
25
  }
@@ -29,13 +30,32 @@ export function humanRepoDir(repo) {
29
30
  return join(humanReposRoot(), sourceToRepoKey(repo));
30
31
  }
31
32
  export function listHumanRepos() {
33
+ let descriptor;
32
34
  try {
33
35
  // Missing or corrupt registry reads as "no repos" — never fatal.
34
- return parseRegistry(readFileSync(registryPath(), "utf-8"));
36
+ const path = registryPath();
37
+ const metadata = lstatSync(path);
38
+ if (metadata.isSymbolicLink() ||
39
+ !metadata.isFile() ||
40
+ metadata.size > MAX_REPO_REGISTRY_BYTES) {
41
+ throw new Error("invalid digital-human repo registry");
42
+ }
43
+ descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
44
+ const opened = fstatSync(descriptor);
45
+ if (!opened.isFile() || opened.size > MAX_REPO_REGISTRY_BYTES) {
46
+ throw new Error("invalid digital-human repo registry");
47
+ }
48
+ return parseRegistry(readFileSync(descriptor, "utf-8"));
35
49
  }
36
- catch {
50
+ catch (error) {
51
+ if (error.code === "ENOENT")
52
+ return [];
37
53
  return [];
38
54
  }
55
+ finally {
56
+ if (descriptor !== undefined)
57
+ closeSync(descriptor);
58
+ }
39
59
  }
40
60
  function parseRegistry(raw) {
41
61
  if (raw === undefined)
@@ -44,12 +64,26 @@ function parseRegistry(raw) {
44
64
  const parsed = JSON.parse(raw);
45
65
  if (!Array.isArray(parsed.repos))
46
66
  return [];
67
+ const seen = new Set();
47
68
  return parsed.repos
69
+ .slice(0, MAX_REPOS)
48
70
  .filter((r) => !!r &&
49
71
  typeof r === "object" &&
50
72
  typeof r.repo === "string" &&
51
73
  CATALOG_REPO_RE.test(r.repo))
52
- .map((r) => ({ repo: r.repo, addedAt: Number(r.addedAt) || 0 }));
74
+ .filter((r) => {
75
+ const normalized = r.repo.toLowerCase();
76
+ if (seen.has(normalized))
77
+ return false;
78
+ seen.add(normalized);
79
+ return true;
80
+ })
81
+ .map((r) => ({
82
+ repo: r.repo,
83
+ addedAt: typeof r.addedAt === "number" && Number.isSafeInteger(r.addedAt) && r.addedAt >= 0
84
+ ? r.addedAt
85
+ : 0,
86
+ }));
53
87
  }
54
88
  catch {
55
89
  return [];
@@ -101,6 +135,7 @@ function updateRegistry(change) {
101
135
  serialize: (repos) => `${JSON.stringify({ repos }, null, 2)}\n`,
102
136
  mutation: (current) => ({ value: change(current) }),
103
137
  mode: 0o600,
138
+ maxBytes: MAX_REPO_REGISTRY_BYTES,
104
139
  });
105
140
  }
106
141
  /**
@@ -9,13 +9,18 @@
9
9
  *
10
10
  * 本模块只做**读取与校验**,克隆/更新由 host 驱动(涉及网络与磁盘写入)。
11
11
  */
12
- import { existsSync, readFileSync } from "node:fs";
12
+ import { closeSync, constants, existsSync, fstatSync, lstatSync, openSync, readFileSync, } from "node:fs";
13
13
  import { join } from "node:path";
14
14
  import { WorkspaceProfileSchema } from "./types.js";
15
15
  /** `owner/repo`,与 SKILL_REPO_RE 同源,供仓库地址复用。 */
16
16
  export const CATALOG_REPO_RE = /^[A-Za-z0-9][A-Za-z0-9-]{0,38}\/[A-Za-z0-9][A-Za-z0-9._-]{0,99}$/;
17
17
  /** 单个目录段的安全名,挡住 `..`、分隔符与 NUL。 */
18
18
  const SAFE_SEGMENT_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/;
19
+ const MAX_MANIFEST_BYTES = 1024 * 1024;
20
+ const MAX_PROFILE_BYTES = 256 * 1024;
21
+ const MAX_MANIFEST_HUMANS = 1_000;
22
+ const MAX_MANIFEST_TEAMS = 500;
23
+ const MAX_ENTRY_TAGS = 64;
19
24
  /**
20
25
  * `owner/repo` → 一个安全的目录段。小写化保证同一仓库不同大小写写法命中同一份
21
26
  * 缓存;斜杠换成 `-` 后仍需通过 SAFE_SEGMENT_RE,杜绝路径逃逸。
@@ -37,44 +42,63 @@ export function sourceToRepoKey(repo) {
37
42
  export function parseHumansManifest(raw) {
38
43
  const value = JSON.parse(raw);
39
44
  const rawHumans = Array.isArray(value.humans) ? value.humans : [];
45
+ if (rawHumans.length > MAX_MANIFEST_HUMANS)
46
+ throw new Error("too many humans in manifest");
40
47
  const humans = [];
41
48
  for (const item of rawHumans) {
42
49
  if (!item || typeof item !== "object" || Array.isArray(item))
43
50
  continue;
44
51
  const entry = item;
45
- if (typeof entry.name !== "string" || !entry.name)
52
+ if (typeof entry.name !== "string" || !entry.name || entry.name.length > 64)
46
53
  continue;
47
54
  humans.push({
48
55
  name: entry.name,
49
- ...(typeof entry.label === "string" ? { label: entry.label } : {}),
50
- ...(typeof entry.description === "string" ? { description: entry.description } : {}),
51
- ...(typeof entry.category === "string" ? { category: entry.category } : {}),
56
+ ...(typeof entry.label === "string" && entry.label.length <= 120
57
+ ? { label: entry.label }
58
+ : {}),
59
+ ...(typeof entry.description === "string" && entry.description.length <= 4_096
60
+ ? { description: entry.description }
61
+ : {}),
62
+ ...(typeof entry.category === "string" && entry.category.length <= 120
63
+ ? { category: entry.category }
64
+ : {}),
52
65
  tags: Array.isArray(entry.tags)
53
- ? entry.tags.filter((t) => typeof t === "string")
66
+ ? entry.tags
67
+ .slice(0, MAX_ENTRY_TAGS)
68
+ .filter((t) => typeof t === "string" && t.length <= 120)
54
69
  : [],
55
70
  });
56
71
  }
57
72
  const rawTeams = Array.isArray(value.teams) ? value.teams : [];
73
+ if (rawTeams.length > MAX_MANIFEST_TEAMS)
74
+ throw new Error("too many teams in manifest");
58
75
  const teams = [];
59
76
  for (const item of rawTeams) {
60
77
  if (!item || typeof item !== "object" || Array.isArray(item))
61
78
  continue;
62
79
  const entry = item;
63
- if (typeof entry.id !== "string" || !entry.id)
80
+ if (typeof entry.id !== "string" || !entry.id || entry.id.length > 64)
64
81
  continue;
65
- if (typeof entry.name !== "string" || !entry.name)
82
+ if (typeof entry.name !== "string" || !entry.name || entry.name.length > 120)
66
83
  continue;
67
84
  const rawMembers = Array.isArray(entry.members) ? entry.members : [];
68
- const members = rawMembers.filter((m) => typeof m === "string" && m.length > 0);
69
- if (members.length < 2)
85
+ const members = rawMembers.filter((m) => typeof m === "string" && SAFE_SEGMENT_RE.test(m));
86
+ if (members.length < 2 || members.length > 8 || new Set(members).size !== members.length) {
70
87
  continue;
88
+ }
71
89
  teams.push({
72
90
  id: entry.id,
73
91
  name: entry.name,
74
- ...(typeof entry.description === "string" ? { description: entry.description } : {}),
92
+ ...(typeof entry.description === "string" && entry.description.length <= 1_000
93
+ ? { description: entry.description }
94
+ : {}),
75
95
  members,
76
- ...(typeof entry.lead === "string" && entry.lead ? { lead: entry.lead } : {}),
77
- ...(typeof entry.playbook === "string" && entry.playbook ? { playbook: entry.playbook } : {}),
96
+ ...(typeof entry.lead === "string" && SAFE_SEGMENT_RE.test(entry.lead)
97
+ ? { lead: entry.lead }
98
+ : {}),
99
+ ...(typeof entry.playbook === "string" && entry.playbook.length <= 4_000
100
+ ? { playbook: entry.playbook }
101
+ : {}),
78
102
  });
79
103
  }
80
104
  return {
@@ -84,6 +108,22 @@ export function parseHumansManifest(raw) {
84
108
  teams,
85
109
  };
86
110
  }
111
+ function readBoundedRegularFile(path, maxBytes) {
112
+ const pathInfo = lstatSync(path);
113
+ if (pathInfo.isSymbolicLink() || !pathInfo.isFile() || pathInfo.size > maxBytes) {
114
+ throw new Error("not a bounded regular file");
115
+ }
116
+ const descriptor = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
117
+ try {
118
+ const opened = fstatSync(descriptor);
119
+ if (!opened.isFile() || opened.size > maxBytes)
120
+ throw new Error("not a bounded regular file");
121
+ return readFileSync(descriptor, "utf8");
122
+ }
123
+ finally {
124
+ closeSync(descriptor);
125
+ }
126
+ }
87
127
  /** 读取一个已克隆的数字人仓库目录。 */
88
128
  export function readCatalogFromDir(dir, sourceRepo) {
89
129
  const manifestPath = join(dir, "humans.json");
@@ -92,7 +132,7 @@ export function readCatalogFromDir(dir, sourceRepo) {
92
132
  }
93
133
  let manifest;
94
134
  try {
95
- manifest = parseHumansManifest(readFileSync(manifestPath, "utf-8"));
135
+ manifest = parseHumansManifest(readBoundedRegularFile(manifestPath, MAX_MANIFEST_BYTES));
96
136
  }
97
137
  catch (error) {
98
138
  return {
@@ -112,7 +152,7 @@ export function readCatalogFromDir(dir, sourceRepo) {
112
152
  }
113
153
  const profilePath = join(dir, "humans", item.name, "profile.json");
114
154
  try {
115
- const profile = WorkspaceProfileSchema.parse(JSON.parse(readFileSync(profilePath, "utf-8")));
155
+ const profile = WorkspaceProfileSchema.parse(JSON.parse(readBoundedRegularFile(profilePath, MAX_PROFILE_BYTES)));
116
156
  entries.push({
117
157
  profile,
118
158
  sourceRepo,
@@ -4,11 +4,13 @@
4
4
  * core 不内置任何领域 profile;样例见 docs/examples/workspace-profile-sample.md。
5
5
  */
6
6
  import { randomUUID } from "node:crypto";
7
- import { lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
7
+ import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, opendirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync, } from "node:fs";
8
8
  import { isAbsolute, join, relative, sep } from "node:path";
9
9
  import { logger } from "../logging/logger.js";
10
10
  import { codeShellHome } from "../session/session-manager.js";
11
11
  import { WORKSPACE_PROFILE_NAME_RE, WorkspaceProfileSchema, } from "./types.js";
12
+ const MAX_PROFILE_FILE_BYTES = 256 * 1024;
13
+ const MAX_PROFILE_LIBRARY_ENTRIES = 10_000;
12
14
  export function workspaceProfilesRoot() {
13
15
  return join(codeShellHome(), "profiles");
14
16
  }
@@ -72,7 +74,7 @@ function checkedProfileFile(name) {
72
74
  const info = lstatIfPresent(path);
73
75
  if (!info)
74
76
  return undefined;
75
- if (info.isSymbolicLink() || !info.isFile()) {
77
+ if (info.isSymbolicLink() || !info.isFile() || info.size > MAX_PROFILE_FILE_BYTES) {
76
78
  throw new Error(`Invalid workspace profile file: ${path}`);
77
79
  }
78
80
  return path;
@@ -81,45 +83,69 @@ export function readWorkspaceProfile(name) {
81
83
  if (!WORKSPACE_PROFILE_NAME_RE.test(name))
82
84
  return undefined;
83
85
  const path = join(workspaceProfileDir(name), "profile.json");
86
+ let descriptor;
84
87
  try {
85
88
  const checkedPath = checkedProfileFile(name);
86
89
  if (!checkedPath)
87
90
  return undefined;
88
- return WorkspaceProfileSchema.parse(JSON.parse(readFileSync(checkedPath, "utf-8")));
91
+ descriptor = openSync(checkedPath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
92
+ const opened = fstatSync(descriptor);
93
+ if (!opened.isFile() || opened.size > MAX_PROFILE_FILE_BYTES) {
94
+ throw new Error("workspace profile file is too large");
95
+ }
96
+ return WorkspaceProfileSchema.parse(JSON.parse(readFileSync(descriptor, "utf-8")));
89
97
  }
90
98
  catch (error) {
91
99
  throw new Error(`Invalid workspace profile "${name}" at ${path}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
92
100
  }
101
+ finally {
102
+ if (descriptor !== undefined)
103
+ closeSync(descriptor);
104
+ }
93
105
  }
94
106
  export function listWorkspaceProfiles() {
95
107
  const root = checkedProfilesRoot();
96
108
  if (!root)
97
109
  return [];
98
110
  const out = [];
99
- for (const entry of readdirSync(root, { withFileTypes: true })) {
100
- if (!entry.isDirectory()) {
101
- if (entry.isSymbolicLink()) {
111
+ const directory = opendirSync(root);
112
+ let seen = 0;
113
+ try {
114
+ for (;;) {
115
+ const entry = directory.readSync();
116
+ if (!entry)
117
+ break;
118
+ seen += 1;
119
+ if (seen > MAX_PROFILE_LIBRARY_ENTRIES) {
120
+ throw new Error("Workspace profile library contains too many entries");
121
+ }
122
+ if (!entry.isDirectory()) {
123
+ if (entry.isSymbolicLink()) {
124
+ logger.warn("profile.library_entry_invalid", {
125
+ cat: "profile",
126
+ name: entry.name,
127
+ error: "symbolic links are not allowed",
128
+ });
129
+ }
130
+ continue;
131
+ }
132
+ try {
133
+ const profile = readWorkspaceProfile(entry.name);
134
+ if (profile)
135
+ out.push(profile);
136
+ }
137
+ catch (error) {
102
138
  logger.warn("profile.library_entry_invalid", {
103
139
  cat: "profile",
104
140
  name: entry.name,
105
- error: "symbolic links are not allowed",
141
+ error: error instanceof Error ? error.message : String(error),
106
142
  });
107
143
  }
108
- continue;
109
- }
110
- try {
111
- const profile = readWorkspaceProfile(entry.name);
112
- if (profile)
113
- out.push(profile);
114
- }
115
- catch (error) {
116
- logger.warn("profile.library_entry_invalid", {
117
- cat: "profile",
118
- name: entry.name,
119
- error: error instanceof Error ? error.message : String(error),
120
- });
121
144
  }
122
145
  }
146
+ finally {
147
+ directory.closeSync();
148
+ }
123
149
  return out.sort((a, b) => a.name.localeCompare(b.name));
124
150
  }
125
151
  /**
@@ -144,7 +170,11 @@ export function saveWorkspaceProfile(profile) {
144
170
  }
145
171
  const tmp = `${path}.${process.pid}.${randomUUID()}.tmp`;
146
172
  try {
147
- writeFileSync(tmp, `${JSON.stringify(parsed, null, 2)}\n`, {
173
+ const serialized = `${JSON.stringify(parsed, null, 2)}\n`;
174
+ if (Buffer.byteLength(serialized, "utf8") > MAX_PROFILE_FILE_BYTES) {
175
+ throw new Error("workspace profile file is too large");
176
+ }
177
+ writeFileSync(tmp, serialized, {
148
178
  encoding: "utf-8",
149
179
  mode: 0o600,
150
180
  flag: "wx",
@@ -96,6 +96,14 @@ export interface ComposerOptions {
96
96
  * See MemoryManager.buildInjectionIndex(currentProjectOnly).
97
97
  */
98
98
  memoryCurrentProjectOnly?: boolean;
99
+ /** Isolated runs can omit repository/user instruction discovery entirely. */
100
+ disableInstructions?: boolean;
101
+ /** Isolated runs can omit persistent-memory context entirely. */
102
+ disableMemoryContext?: boolean;
103
+ /** Isolated runs can omit capability-owned volatile context providers. */
104
+ disableCapabilityContext?: boolean;
105
+ /** Isolated runs can omit bound-source metadata. */
106
+ disableSourcesContext?: boolean;
99
107
  }
100
108
  export declare class PromptComposer {
101
109
  private readonly options;
@@ -30,6 +30,8 @@ export class PromptComposer {
30
30
  * Build the userContext prefix message (CLAUDE.md content as <system-reminder>).
31
31
  */
32
32
  buildUserContextMessage() {
33
+ if (this.options.disableInstructions === true)
34
+ return null;
33
35
  const instructions = this.getInstructions();
34
36
  // NOTE: memory is intentionally NOT here. It used to be, but memory mutates
35
37
  // constantly (extraction, recall usage++/lastUsed, approve/demote), and this
@@ -51,6 +53,8 @@ export class PromptComposer {
51
53
  }
52
54
  /** Build volatile context contributed by installed capability modules. */
53
55
  async buildSystemContext() {
56
+ if (this.options.disableCapabilityContext === true)
57
+ return "";
54
58
  const preset = this.options.preset ?? resolveAgentPreset();
55
59
  const parts = await Promise.all((this.options.dynamicContextProviders ?? []).map(async (provider) => {
56
60
  try {
@@ -107,6 +111,8 @@ export class PromptComposer {
107
111
  const [capabilityContext, sourcesContext] = await Promise.all([
108
112
  this.buildSystemContext(),
109
113
  (async () => {
114
+ if (this.options.disableSourcesContext === true)
115
+ return "";
110
116
  try {
111
117
  return (await this.options.sourcesContextProvider?.()) ?? "";
112
118
  }
@@ -265,6 +271,8 @@ export class PromptComposer {
265
271
  return this.cachedInstructions;
266
272
  }
267
273
  getMemoryContext() {
274
+ if (this.options.disableMemoryContext === true)
275
+ return "";
268
276
  try {
269
277
  // Three-layer injection (用户拍板): a compact index merging GLOBAL +
270
278
  // DIGITAL-HUMAN + PROJECT memories. Global memories are surfaced every