@cjhyy/code-shell-core 0.8.9 → 0.8.11
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 +45 -6
- package/dist/engine/file-history-hook.js +24 -5
- package/dist/engine/run-types.d.ts +9 -0
- 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/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/protocol/chat-session-manager.d.ts +9 -0
- package/dist/protocol/chat-session-manager.js +13 -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 +75 -29
- package/dist/protocol/types.d.ts +8 -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 +1 -0
- package/dist/session/session-manager.js +52 -21
- 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 +44 -0
- 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/registry.js +5 -0
- package/dist/tool-system/sandbox/index.d.ts +1 -0
- package/dist/tool-system/sandbox/index.js +4 -1
- package/dist/utils/file-mutex.d.ts +2 -0
- package/dist/utils/file-mutex.js +29 -4
- package/package.json +2 -1
|
@@ -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",
|
|
@@ -95,6 +95,15 @@ export declare class ChatSessionManager {
|
|
|
95
95
|
* the values so a callback that closes a session can't perturb the walk.
|
|
96
96
|
*/
|
|
97
97
|
forEachSession(fn: (s: ChatSession) => void): void;
|
|
98
|
+
/** First live Engine for legacy fallbacks that genuinely accept any owner. */
|
|
99
|
+
getAnyLiveEngine(): Engine | undefined;
|
|
100
|
+
/**
|
|
101
|
+
* Build a detached Engine for global model/provider/settings queries. Keeping
|
|
102
|
+
* this seam on the manager avoids protocol code reaching into its private
|
|
103
|
+
* factory and, importantly, avoids borrowing project-local state from an
|
|
104
|
+
* arbitrary live session.
|
|
105
|
+
*/
|
|
106
|
+
createDetachedEngine(slice?: Partial<EngineConfigSlice>): Engine;
|
|
98
107
|
/** Snapshot-safe, read-only source shared by query("sessions") and Pet projection. */
|
|
99
108
|
getLiveSessionSnapshot(): LiveChatSessionSnapshot;
|
|
100
109
|
close(sessionId: string): Promise<void>;
|
|
@@ -164,6 +164,19 @@ export class ChatSessionManager {
|
|
|
164
164
|
for (const s of [...this.sessions.values()])
|
|
165
165
|
fn(s);
|
|
166
166
|
}
|
|
167
|
+
/** First live Engine for legacy fallbacks that genuinely accept any owner. */
|
|
168
|
+
getAnyLiveEngine() {
|
|
169
|
+
return this.sessions.values().next().value?.engine;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Build a detached Engine for global model/provider/settings queries. Keeping
|
|
173
|
+
* this seam on the manager avoids protocol code reaching into its private
|
|
174
|
+
* factory and, importantly, avoids borrowing project-local state from an
|
|
175
|
+
* arbitrary live session.
|
|
176
|
+
*/
|
|
177
|
+
createDetachedEngine(slice = {}) {
|
|
178
|
+
return this.factory(slice);
|
|
179
|
+
}
|
|
167
180
|
/** Snapshot-safe, read-only source shared by query("sessions") and Pet projection. */
|
|
168
181
|
getLiveSessionSnapshot() {
|
|
169
182
|
const sessions = [];
|
|
@@ -33,6 +33,11 @@ export interface TurnOpts {
|
|
|
33
33
|
injected?: boolean;
|
|
34
34
|
/** Stable id for this user-intent; forwarded to Engine.run for idempotency. */
|
|
35
35
|
clientMessageId?: string;
|
|
36
|
+
/** Archive history before this turn at the engine's pre-model safe point. */
|
|
37
|
+
archiveBeforeCurrentTurn?: {
|
|
38
|
+
fromClientMessageId?: string;
|
|
39
|
+
segmentId?: string;
|
|
40
|
+
};
|
|
36
41
|
/** Structured input attachments for this turn. */
|
|
37
42
|
attachments?: InputAttachmentMeta[];
|
|
38
43
|
/** Permission mode snapshot for this queued turn only. */
|
|
@@ -290,6 +290,7 @@ export class ChatSession {
|
|
|
290
290
|
goal: next.opts.goal,
|
|
291
291
|
injected: next.opts.injected,
|
|
292
292
|
clientMessageId: next.opts.clientMessageId,
|
|
293
|
+
archiveBeforeCurrentTurn: next.opts.archiveBeforeCurrentTurn,
|
|
293
294
|
attachments: next.opts.attachments,
|
|
294
295
|
permissionMode: next.opts.permissionMode,
|
|
295
296
|
planMode: next.opts.planMode,
|
|
@@ -375,6 +375,8 @@ export declare class AgentServer {
|
|
|
375
375
|
* selector) can query models/providers without requiring a chat session first.
|
|
376
376
|
*/
|
|
377
377
|
private anyEngine;
|
|
378
|
+
/** Neutral engine for truly global queries; never inherits a live project's cwd or policy. */
|
|
379
|
+
private detachedQueryEngine;
|
|
378
380
|
close(): void;
|
|
379
381
|
/** Release only this transport's approval ownership. Safe on socket close
|
|
380
382
|
* even when the ChatSessionManager is shared by other TCP connections. */
|
package/dist/protocol/server.js
CHANGED
|
@@ -62,6 +62,27 @@ function runInputError(params) {
|
|
|
62
62
|
(typeof params.quickChatClaimId !== "string" || params.quickChatClaimId.length === 0)) {
|
|
63
63
|
return "quickChatClaimId must be a non-empty string";
|
|
64
64
|
}
|
|
65
|
+
if (params.archiveBeforeCurrentTurn !== undefined) {
|
|
66
|
+
const archive = params.archiveBeforeCurrentTurn;
|
|
67
|
+
if (!archive || typeof archive !== "object" || Array.isArray(archive)) {
|
|
68
|
+
return "archiveBeforeCurrentTurn must be an object";
|
|
69
|
+
}
|
|
70
|
+
if (typeof params.clientMessageId !== "string" || params.clientMessageId.length === 0) {
|
|
71
|
+
return "archiveBeforeCurrentTurn requires clientMessageId";
|
|
72
|
+
}
|
|
73
|
+
for (const [field, value] of [
|
|
74
|
+
["fromClientMessageId", archive.fromClientMessageId],
|
|
75
|
+
["segmentId", archive.segmentId],
|
|
76
|
+
]) {
|
|
77
|
+
if (value !== undefined &&
|
|
78
|
+
(typeof value !== "string" ||
|
|
79
|
+
value.length === 0 ||
|
|
80
|
+
value.length > 256 ||
|
|
81
|
+
/[\r\n]/.test(value))) {
|
|
82
|
+
return `archiveBeforeCurrentTurn.${field} must be a bounded non-empty string`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
65
86
|
if (params.behaviorMode !== undefined &&
|
|
66
87
|
(typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
|
|
67
88
|
return `invalid behavior mode: ${String(params.behaviorMode)}`;
|
|
@@ -104,6 +125,20 @@ function runInputError(params) {
|
|
|
104
125
|
// extension-owned param shapes) lives in ExtensionModule.validateRunParams.
|
|
105
126
|
return null;
|
|
106
127
|
}
|
|
128
|
+
function goalExtensionInputError(params) {
|
|
129
|
+
const fields = ["addTurns", "addTokenBudget", "addTimeBudgetMs", "addStopBlocks"];
|
|
130
|
+
let supplied = false;
|
|
131
|
+
for (const field of fields) {
|
|
132
|
+
const value = params[field];
|
|
133
|
+
if (value === undefined)
|
|
134
|
+
continue;
|
|
135
|
+
supplied = true;
|
|
136
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) {
|
|
137
|
+
return `${field} must be a positive safe integer`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return supplied ? null : "at least one goal extension is required";
|
|
141
|
+
}
|
|
107
142
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
108
143
|
"micro",
|
|
109
144
|
"summary",
|
|
@@ -1191,6 +1226,7 @@ export class AgentServer {
|
|
|
1191
1226
|
this.notify(Methods.StreamEvent, { sessionId: sid, event });
|
|
1192
1227
|
},
|
|
1193
1228
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
1229
|
+
archiveBeforeCurrentTurn: params.archiveBeforeCurrentTurn,
|
|
1194
1230
|
permissionMode: params.permissionMode,
|
|
1195
1231
|
planMode: params.planMode,
|
|
1196
1232
|
behaviorMode: params.behaviorMode,
|
|
@@ -1472,7 +1508,17 @@ export class AgentServer {
|
|
|
1472
1508
|
* limits, or an error if no session / no active run.
|
|
1473
1509
|
*/
|
|
1474
1510
|
handleGoalExtend(req) {
|
|
1475
|
-
const
|
|
1511
|
+
const rawParams = req.params;
|
|
1512
|
+
if (!rawParams || typeof rawParams !== "object" || Array.isArray(rawParams)) {
|
|
1513
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "goal extension params are required"));
|
|
1514
|
+
return;
|
|
1515
|
+
}
|
|
1516
|
+
const inputError = goalExtensionInputError(rawParams);
|
|
1517
|
+
if (inputError) {
|
|
1518
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
1519
|
+
return;
|
|
1520
|
+
}
|
|
1521
|
+
const params = rawParams;
|
|
1476
1522
|
const ext = {
|
|
1477
1523
|
addTurns: params.addTurns,
|
|
1478
1524
|
addTokenBudget: params.addTokenBudget,
|
|
@@ -2136,7 +2182,7 @@ export class AgentServer {
|
|
|
2136
2182
|
resolved = session.engine;
|
|
2137
2183
|
}
|
|
2138
2184
|
else {
|
|
2139
|
-
resolved
|
|
2185
|
+
resolved ??= this.anyEngine();
|
|
2140
2186
|
}
|
|
2141
2187
|
}
|
|
2142
2188
|
if (!resolved) {
|
|
@@ -2147,16 +2193,16 @@ export class AgentServer {
|
|
|
2147
2193
|
}
|
|
2148
2194
|
async handleQuery(req) {
|
|
2149
2195
|
const params = (req.params ?? {});
|
|
2150
|
-
//
|
|
2151
|
-
//
|
|
2152
|
-
|
|
2196
|
+
// Global queries use a detached manager-owned engine. Session-scoped cases
|
|
2197
|
+
// below resolve the exact owner from params.sessionId instead of borrowing
|
|
2198
|
+
// whichever live project's engine happens to be first in a Map.
|
|
2199
|
+
const engine = this.legacyEngine ?? this.detachedQueryEngine();
|
|
2153
2200
|
switch (params.type) {
|
|
2154
2201
|
case "tools": {
|
|
2155
|
-
|
|
2156
|
-
|
|
2202
|
+
const toolsEngine = await this.resolveEngineForSessionQuery(req, params.sessionId, engine, "tools");
|
|
2203
|
+
if (!toolsEngine)
|
|
2157
2204
|
return;
|
|
2158
|
-
|
|
2159
|
-
const registry = engine.getToolRegistry();
|
|
2205
|
+
const registry = toolsEngine.getToolRegistry();
|
|
2160
2206
|
const tools = typeof registry.listToolsDetailed === "function"
|
|
2161
2207
|
? registry
|
|
2162
2208
|
.listToolsDetailed()
|
|
@@ -2183,16 +2229,15 @@ export class AgentServer {
|
|
|
2183
2229
|
break;
|
|
2184
2230
|
}
|
|
2185
2231
|
case "config": {
|
|
2186
|
-
|
|
2187
|
-
|
|
2232
|
+
const configEngine = await this.resolveEngineForSessionQuery(req, params.sessionId, engine, "config");
|
|
2233
|
+
if (!configEngine)
|
|
2188
2234
|
return;
|
|
2189
|
-
|
|
2190
|
-
const config = engine.getConfig();
|
|
2235
|
+
const config = configEngine.getConfig();
|
|
2191
2236
|
this.transport.send(createResponse(req.id, {
|
|
2192
2237
|
type: "config",
|
|
2193
2238
|
data: {
|
|
2194
2239
|
permissionMode: config.permissionMode ?? "default",
|
|
2195
|
-
planMode:
|
|
2240
|
+
planMode: configEngine.planMode ?? false,
|
|
2196
2241
|
preset: config.preset,
|
|
2197
2242
|
model: config.llm.model,
|
|
2198
2243
|
cwd: config.cwd,
|
|
@@ -2202,21 +2247,20 @@ export class AgentServer {
|
|
|
2202
2247
|
llm: redactLlmConfig(config.llm),
|
|
2203
2248
|
// Resolved feature flags (defaults merged with settings overlay)
|
|
2204
2249
|
// so the /features command can list current state.
|
|
2205
|
-
featureFlags:
|
|
2250
|
+
featureFlags: configEngine.getFeatureFlags(),
|
|
2206
2251
|
// Effective permission rules so /permissions can list them (TODO 5.1).
|
|
2207
|
-
permissionRules:
|
|
2252
|
+
permissionRules: configEngine.getPermissionRules(),
|
|
2208
2253
|
},
|
|
2209
2254
|
}));
|
|
2210
2255
|
break;
|
|
2211
2256
|
}
|
|
2212
2257
|
case "session_detail": {
|
|
2213
|
-
if (!engine) {
|
|
2214
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for session_detail query"));
|
|
2215
|
-
return;
|
|
2216
|
-
}
|
|
2217
2258
|
if (params.sessionId) {
|
|
2259
|
+
const detailEngine = await this.resolveEngineForSessionQuery(req, params.sessionId, engine, "session_detail");
|
|
2260
|
+
if (!detailEngine)
|
|
2261
|
+
return;
|
|
2218
2262
|
try {
|
|
2219
|
-
const bundle =
|
|
2263
|
+
const bundle = detailEngine.getSessionManager().resume(params.sessionId);
|
|
2220
2264
|
const data = {
|
|
2221
2265
|
state: bundle.state,
|
|
2222
2266
|
transcript: bundle.transcript.getEvents(),
|
|
@@ -3164,16 +3208,18 @@ export class AgentServer {
|
|
|
3164
3208
|
anyEngine() {
|
|
3165
3209
|
if (!this.chatManager)
|
|
3166
3210
|
return null;
|
|
3167
|
-
const
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3211
|
+
const live = this.chatManager.getAnyLiveEngine();
|
|
3212
|
+
if (live)
|
|
3213
|
+
return live;
|
|
3214
|
+
return this.detachedQueryEngine();
|
|
3215
|
+
}
|
|
3216
|
+
/** Neutral engine for truly global queries; never inherits a live project's cwd or policy. */
|
|
3217
|
+
detachedQueryEngine() {
|
|
3218
|
+
if (!this.chatManager)
|
|
3219
|
+
return null;
|
|
3171
3220
|
if (this.globalQueryEngine)
|
|
3172
3221
|
return this.globalQueryEngine;
|
|
3173
|
-
|
|
3174
|
-
if (typeof factory !== "function")
|
|
3175
|
-
return null;
|
|
3176
|
-
this.globalQueryEngine = factory({});
|
|
3222
|
+
this.globalQueryEngine = this.chatManager.createDetachedEngine();
|
|
3177
3223
|
return this.globalQueryEngine;
|
|
3178
3224
|
}
|
|
3179
3225
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -80,6 +80,14 @@ export interface RunParams {
|
|
|
80
80
|
attachments?: InputAttachmentMeta[];
|
|
81
81
|
/** Stable id for the user's submit intent; duplicate ids are idempotent. */
|
|
82
82
|
clientMessageId?: string;
|
|
83
|
+
/**
|
|
84
|
+
* Archive the preceding history after this message is durably appended and
|
|
85
|
+
* before its first model call. `clientMessageId` is the exclusive end anchor.
|
|
86
|
+
*/
|
|
87
|
+
archiveBeforeCurrentTurn?: {
|
|
88
|
+
fromClientMessageId?: string;
|
|
89
|
+
segmentId?: string;
|
|
90
|
+
};
|
|
83
91
|
/** Desktop host ownership generation for process-local Quick Chat runs. */
|
|
84
92
|
quickChatClaimId?: string;
|
|
85
93
|
/**
|
|
@@ -18,6 +18,8 @@ export declare class FileRunStore implements RunStore {
|
|
|
18
18
|
private ensureRunDir;
|
|
19
19
|
private writeJson;
|
|
20
20
|
private readJson;
|
|
21
|
+
private assertRealDirectory;
|
|
22
|
+
private assertSafeFileTarget;
|
|
21
23
|
/** Serializes concurrent JSONL appends per file path. */
|
|
22
24
|
private readonly appendLocks;
|
|
23
25
|
private appendJsonl;
|