@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.
- package/dist/automation/scheduler.js +49 -0
- package/dist/automation/store.d.ts +1 -1
- package/dist/automation/store.js +184 -10
- package/dist/cli/agent-server-stdio.js +7 -0
- package/dist/credentials/store.d.ts +14 -0
- package/dist/credentials/store.js +245 -42
- package/dist/engine/engine.js +81 -27
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-finalize.js +8 -6
- package/dist/engine/run-session-open.js +1 -1
- package/dist/engine/run-setup.d.ts +4 -0
- package/dist/engine/run-setup.js +5 -1
- package/dist/engine/run-tooling.js +7 -1
- package/dist/engine/run-types.d.ts +33 -0
- package/dist/engine/run-types.js +21 -0
- package/dist/engine/run-workspace.js +11 -5
- package/dist/engine/turn-loop.js +9 -8
- package/dist/goal/lifecycle.d.ts +2 -0
- package/dist/goal/lifecycle.js +56 -33
- package/dist/index.d.ts +2 -3
- package/dist/index.internal.d.ts +1 -0
- package/dist/index.internal.js +1 -0
- package/dist/index.js +2 -2
- package/dist/links/cli.d.ts +2 -0
- package/dist/links/cli.js +11 -4
- package/dist/model-catalog/index.js +19 -4
- package/dist/model-catalog/save-entry.js +122 -61
- package/dist/model-catalog/types.js +27 -23
- package/dist/panel-apps/installer.js +27 -14
- package/dist/panel-apps/manifest.d.ts +11 -11
- package/dist/panel-apps/manifest.js +3 -1
- package/dist/panel-apps/registry.js +60 -12
- package/dist/plugins/installedPlugins.d.ts +4 -0
- package/dist/plugins/installedPlugins.js +70 -30
- package/dist/plugins/installer/types.d.ts +12 -12
- package/dist/plugins/installer/update.js +37 -38
- package/dist/plugins/knownMarketplaces.d.ts +7 -3
- package/dist/plugins/knownMarketplaces.js +127 -23
- package/dist/plugins/pluginCatalog.js +18 -4
- package/dist/plugins/pluginHookApproval.js +56 -60
- package/dist/plugins/pluginMcpApproval.js +50 -52
- package/dist/profile/catalog-store.js +39 -4
- package/dist/profile/catalog.js +55 -15
- package/dist/profile/store.js +51 -21
- package/dist/prompt/composer.d.ts +8 -0
- package/dist/prompt/composer.js +8 -0
- package/dist/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +22 -0
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +1 -0
- package/dist/protocol/server.d.ts +2 -0
- package/dist/protocol/server.js +112 -29
- package/dist/protocol/types.d.ts +20 -0
- package/dist/run/FileRunStore.d.ts +2 -0
- package/dist/run/FileRunStore.js +153 -18
- package/dist/run/Heartbeat.js +63 -4
- package/dist/services/auto-dream.js +39 -17
- package/dist/services/session-memory.js +107 -8
- package/dist/session/file-history.d.ts +63 -2
- package/dist/session/file-history.js +593 -86
- package/dist/session/session-manager.d.ts +4 -1
- package/dist/session/session-manager.js +94 -47
- package/dist/session/transcript.js +33 -3
- package/dist/session/undo-target.d.ts +15 -6
- package/dist/session/undo-target.js +26 -9
- package/dist/settings/manager.d.ts +22 -3
- package/dist/settings/manager.js +185 -50
- package/dist/settings/schema.d.ts +3 -3
- package/dist/sources/adapters/local-files.js +49 -4
- package/dist/sources/catalog.js +64 -18
- package/dist/sources/types.d.ts +3 -3
- package/dist/sources/types.js +7 -4
- package/dist/themes/installer.js +192 -28
- package/dist/tool-system/builtin/add-marketplace.js +21 -1
- package/dist/tool-system/builtin/cron.d.ts +2 -1
- package/dist/tool-system/builtin/cron.js +20 -6
- package/dist/tool-system/builtin/index.js +48 -1
- package/dist/tool-system/builtin/install-capability.d.ts +52 -0
- package/dist/tool-system/builtin/install-capability.js +1057 -0
- package/dist/tool-system/builtin/skill.js +3 -1
- package/dist/tool-system/executor.js +1 -0
- package/dist/tool-system/path-policy.js +25 -33
- package/dist/tool-system/registry.js +5 -0
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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
|
-
|
|
158
|
-
writeInstalledPlugins(data);
|
|
159
|
-
return out;
|
|
154
|
+
return { ...(writeNeeded ? { value: data } : {}), result: out };
|
|
155
|
+
}) ?? []);
|
|
160
156
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
.
|
|
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
|
/**
|
package/dist/profile/catalog.js
CHANGED
|
@@ -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"
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
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
|
|
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"
|
|
92
|
+
...(typeof entry.description === "string" && entry.description.length <= 1_000
|
|
93
|
+
? { description: entry.description }
|
|
94
|
+
: {}),
|
|
75
95
|
members,
|
|
76
|
-
...(typeof entry.lead === "string" &&
|
|
77
|
-
|
|
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(
|
|
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(
|
|
155
|
+
const profile = WorkspaceProfileSchema.parse(JSON.parse(readBoundedRegularFile(profilePath, MAX_PROFILE_BYTES)));
|
|
116
156
|
entries.push({
|
|
117
157
|
profile,
|
|
118
158
|
sourceRepo,
|
package/dist/profile/store.js
CHANGED
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
package/dist/prompt/composer.js
CHANGED
|
@@ -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
|