@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,11 +1,88 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
|
|
5
5
|
import { getDefaultCredentialCipher } from "./cipher.js";
|
|
6
6
|
import { logger } from "../logging/logger.js";
|
|
7
7
|
import { summarizeOAuthCredentialSecret } from "./oauth.js";
|
|
8
8
|
import { isBrowserOAuthLinkCredential } from "./oauth.js";
|
|
9
|
+
import { acquireFileLock, writeFileAtomic } from "../utils/file-mutex.js";
|
|
10
|
+
const MAX_CREDENTIALS = 4_096;
|
|
11
|
+
const MAX_CREDENTIAL_FILE_BYTES = 32 * 1024 * 1024;
|
|
12
|
+
const MAX_CREDENTIAL_SECRET_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
const MAX_CREDENTIAL_META_BYTES = 1024 * 1024;
|
|
14
|
+
const MAX_CREDENTIAL_ID_CHARS = 512;
|
|
15
|
+
const MAX_CREDENTIAL_LABEL_CHARS = 4_096;
|
|
16
|
+
function normalizeCredential(value, strict) {
|
|
17
|
+
const invalid = (message) => {
|
|
18
|
+
if (strict)
|
|
19
|
+
throw new Error(`invalid credential: ${message}`);
|
|
20
|
+
return undefined;
|
|
21
|
+
};
|
|
22
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
23
|
+
return invalid("object");
|
|
24
|
+
const raw = value;
|
|
25
|
+
if (typeof raw.id !== "string" ||
|
|
26
|
+
!raw.id ||
|
|
27
|
+
raw.id.length > MAX_CREDENTIAL_ID_CHARS ||
|
|
28
|
+
raw.id.includes("\0")) {
|
|
29
|
+
return invalid("id");
|
|
30
|
+
}
|
|
31
|
+
if (raw.type !== "token" &&
|
|
32
|
+
raw.type !== "link" &&
|
|
33
|
+
raw.type !== "cookie" &&
|
|
34
|
+
raw.type !== "oauth") {
|
|
35
|
+
return invalid("type");
|
|
36
|
+
}
|
|
37
|
+
if (typeof raw.label !== "string" ||
|
|
38
|
+
raw.label.length > MAX_CREDENTIAL_LABEL_CHARS ||
|
|
39
|
+
raw.label.includes("\0")) {
|
|
40
|
+
return invalid("label");
|
|
41
|
+
}
|
|
42
|
+
if (raw.secret !== undefined &&
|
|
43
|
+
(typeof raw.secret !== "string" || Buffer.byteLength(raw.secret) > MAX_CREDENTIAL_SECRET_BYTES)) {
|
|
44
|
+
return invalid("secret");
|
|
45
|
+
}
|
|
46
|
+
if (raw.exposeAsEnv !== undefined &&
|
|
47
|
+
(typeof raw.exposeAsEnv !== "string" ||
|
|
48
|
+
raw.exposeAsEnv.length > 512 ||
|
|
49
|
+
raw.exposeAsEnv.includes("\0"))) {
|
|
50
|
+
return invalid("exposeAsEnv");
|
|
51
|
+
}
|
|
52
|
+
if (raw.autoUseByAI !== undefined && typeof raw.autoUseByAI !== "boolean") {
|
|
53
|
+
return invalid("autoUseByAI");
|
|
54
|
+
}
|
|
55
|
+
if (raw.autoInjectByAI !== undefined && typeof raw.autoInjectByAI !== "boolean") {
|
|
56
|
+
return invalid("autoInjectByAI");
|
|
57
|
+
}
|
|
58
|
+
let meta;
|
|
59
|
+
if (raw.meta !== undefined) {
|
|
60
|
+
if (!raw.meta || typeof raw.meta !== "object" || Array.isArray(raw.meta)) {
|
|
61
|
+
return invalid("meta");
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const encoded = JSON.stringify(raw.meta);
|
|
65
|
+
if (Buffer.byteLength(encoded) > MAX_CREDENTIAL_META_BYTES)
|
|
66
|
+
return invalid("meta");
|
|
67
|
+
// Clone through JSON so callers cannot retain a mutable/cyclic object or
|
|
68
|
+
// smuggle a surprising prototype into later credential consumers.
|
|
69
|
+
meta = JSON.parse(encoded);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return invalid("meta");
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
id: raw.id,
|
|
77
|
+
type: raw.type,
|
|
78
|
+
label: raw.label,
|
|
79
|
+
...(typeof raw.secret === "string" ? { secret: raw.secret } : {}),
|
|
80
|
+
...(typeof raw.exposeAsEnv === "string" ? { exposeAsEnv: raw.exposeAsEnv } : {}),
|
|
81
|
+
...(typeof raw.autoUseByAI === "boolean" ? { autoUseByAI: raw.autoUseByAI } : {}),
|
|
82
|
+
...(typeof raw.autoInjectByAI === "boolean" ? { autoInjectByAI: raw.autoInjectByAI } : {}),
|
|
83
|
+
...(meta ? { meta } : {}),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
9
86
|
/** 测试可经 process.env.HOME 覆盖(镜像 settings/manager.ts userHome)。 */
|
|
10
87
|
function userHome() {
|
|
11
88
|
return process.env.HOME ?? homedir();
|
|
@@ -40,6 +117,9 @@ export class CredentialStore {
|
|
|
40
117
|
this.cipher = cipher ?? getDefaultCredentialCipher();
|
|
41
118
|
}
|
|
42
119
|
pathFor(scope) {
|
|
120
|
+
if (scope !== "user" && scope !== "project") {
|
|
121
|
+
throw new Error(`invalid credential scope: ${String(scope)}`);
|
|
122
|
+
}
|
|
43
123
|
if (scope === "user") {
|
|
44
124
|
return join(this.userDirOverride ?? join(userHome(), ".code-shell"), "credentials.json");
|
|
45
125
|
}
|
|
@@ -66,54 +146,165 @@ export class CredentialStore {
|
|
|
66
146
|
}
|
|
67
147
|
}
|
|
68
148
|
read(scope) {
|
|
149
|
+
return this.readGuarded(scope).file;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Read the store, reporting whether the on-disk state was fully understood.
|
|
153
|
+
*
|
|
154
|
+
* `readable: false` means "there is a file here but this build could not
|
|
155
|
+
* parse it" — NOT "there are no credentials". The distinction matters because
|
|
156
|
+
* mutate() commits whatever this returns: treating an unreadable file as an
|
|
157
|
+
* empty list turns any later save() into a wipe of every stored credential.
|
|
158
|
+
*
|
|
159
|
+
* `unknown` carries entries this build's schema rejects (for example a
|
|
160
|
+
* credential type added by a newer version). They are kept verbatim so a
|
|
161
|
+
* round-trip through an older build preserves rather than deletes them.
|
|
162
|
+
*/
|
|
163
|
+
readGuarded(scope) {
|
|
164
|
+
const empty = { ...EMPTY, credentials: [] };
|
|
69
165
|
const p = this.pathFor(scope);
|
|
70
|
-
if (!p
|
|
71
|
-
return {
|
|
166
|
+
if (!p)
|
|
167
|
+
return { file: empty, readable: true, unknown: [] };
|
|
168
|
+
let descriptor;
|
|
72
169
|
try {
|
|
73
|
-
const
|
|
74
|
-
|
|
170
|
+
const parent = lstatSync(dirname(p));
|
|
171
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) {
|
|
172
|
+
logger.warn("credentials.invalid_parent", { path: p });
|
|
173
|
+
return { file: empty, readable: false, unknown: [] };
|
|
174
|
+
}
|
|
175
|
+
const metadata = lstatSync(p);
|
|
176
|
+
if (metadata.isSymbolicLink() ||
|
|
177
|
+
!metadata.isFile() ||
|
|
178
|
+
metadata.size > MAX_CREDENTIAL_FILE_BYTES) {
|
|
179
|
+
logger.warn("credentials.file_too_large", { path: p });
|
|
180
|
+
return { file: empty, readable: false, unknown: [] };
|
|
181
|
+
}
|
|
182
|
+
descriptor = openSync(p, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
183
|
+
const opened = fstatSync(descriptor);
|
|
184
|
+
if (!opened.isFile() || opened.size > MAX_CREDENTIAL_FILE_BYTES) {
|
|
185
|
+
return { file: empty, readable: false, unknown: [] };
|
|
186
|
+
}
|
|
187
|
+
const raw = JSON.parse(readFileSync(descriptor, "utf8"));
|
|
188
|
+
const values = Array.isArray(raw.credentials)
|
|
189
|
+
? raw.credentials.slice(0, MAX_CREDENTIALS)
|
|
190
|
+
: [];
|
|
191
|
+
const creds = [];
|
|
192
|
+
const unknown = [];
|
|
75
193
|
// Decrypt secrets at the disk boundary so all callers see plaintext.
|
|
76
|
-
for (const
|
|
194
|
+
for (const value of values) {
|
|
195
|
+
const c = normalizeCredential(value, false);
|
|
196
|
+
if (!c) {
|
|
197
|
+
unknown.push(value);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
77
200
|
if (typeof c.secret === "string" && c.secret.length > 0) {
|
|
78
201
|
c.secret = this.decryptSecret(c.secret);
|
|
79
202
|
}
|
|
203
|
+
creds.push(c);
|
|
80
204
|
}
|
|
81
|
-
return { version: 1, credentials: creds };
|
|
205
|
+
return { file: { version: 1, credentials: creds }, readable: true, unknown };
|
|
82
206
|
}
|
|
83
|
-
catch {
|
|
84
|
-
|
|
207
|
+
catch (error) {
|
|
208
|
+
// A missing file is genuinely empty; anything else (torn JSON, EACCES)
|
|
209
|
+
// is unreadable and must not be committed over.
|
|
210
|
+
if (error.code === "ENOENT") {
|
|
211
|
+
return { file: empty, readable: true, unknown: [] };
|
|
212
|
+
}
|
|
213
|
+
logger.warn("credentials.unreadable", { path: p, error: error.message });
|
|
214
|
+
return { file: empty, readable: false, unknown: [] };
|
|
215
|
+
}
|
|
216
|
+
finally {
|
|
217
|
+
if (descriptor !== undefined)
|
|
218
|
+
closeSync(descriptor);
|
|
85
219
|
}
|
|
86
220
|
}
|
|
87
|
-
write(scope, file
|
|
221
|
+
write(scope, file,
|
|
222
|
+
/** Entries this build's schema rejected; re-emitted verbatim so an older
|
|
223
|
+
* build cannot delete a newer build's credential types. */
|
|
224
|
+
preserved = []) {
|
|
88
225
|
const p = this.pathFor(scope);
|
|
89
226
|
if (!p)
|
|
90
227
|
return;
|
|
91
|
-
mkdirSync(dirname(p), { recursive: true });
|
|
92
228
|
// Encrypt secrets at the disk boundary. `file` carries plaintext secrets
|
|
93
229
|
// (read() decrypted them); serialize a copy with each secret encrypted so
|
|
94
230
|
// we never persist plaintext under an encrypting cipher.
|
|
95
231
|
const onDisk = {
|
|
96
232
|
version: file.version,
|
|
97
|
-
credentials:
|
|
98
|
-
|
|
99
|
-
|
|
233
|
+
credentials: [
|
|
234
|
+
...file.credentials.map((c) => typeof c.secret === "string" && c.secret.length > 0
|
|
235
|
+
? { ...c, secret: this.cipher.encrypt(c.secret) }
|
|
236
|
+
: c),
|
|
237
|
+
...preserved,
|
|
238
|
+
],
|
|
100
239
|
};
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
240
|
+
const encoded = JSON.stringify(onDisk, null, 2);
|
|
241
|
+
if (Buffer.byteLength(encoded) > MAX_CREDENTIAL_FILE_BYTES) {
|
|
242
|
+
throw new Error("credential store exceeds the maximum file size");
|
|
243
|
+
}
|
|
244
|
+
const parentPath = dirname(p);
|
|
245
|
+
mkdirSync(parentPath, { recursive: true, mode: 0o700 });
|
|
246
|
+
const parent = lstatSync(parentPath);
|
|
247
|
+
if (parent.isSymbolicLink() || !parent.isDirectory()) {
|
|
248
|
+
throw new Error("credential store parent must be a real directory");
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
const target = lstatSync(p);
|
|
252
|
+
if (target.isSymbolicLink() || !target.isFile()) {
|
|
253
|
+
throw new Error("credential store target must be a regular file");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
catch (error) {
|
|
257
|
+
if (error.code !== "ENOENT")
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
writeFileAtomic(p, encoded, 0o600);
|
|
261
|
+
}
|
|
262
|
+
mutate(scope, change) {
|
|
263
|
+
const p = this.pathFor(scope);
|
|
264
|
+
if (!p)
|
|
265
|
+
return;
|
|
266
|
+
const parent = dirname(p);
|
|
267
|
+
mkdirSync(parent, { recursive: true, mode: 0o700 });
|
|
268
|
+
const parentInfo = lstatSync(parent);
|
|
269
|
+
if (parentInfo.isSymbolicLink() || !parentInfo.isDirectory()) {
|
|
270
|
+
throw new Error("credential store parent must be a real directory");
|
|
271
|
+
}
|
|
272
|
+
const release = acquireFileLock(p);
|
|
273
|
+
try {
|
|
274
|
+
// Reload only after acquiring the cross-process lock; otherwise two app
|
|
275
|
+
// instances can each write back a stale snapshot and silently lose one.
|
|
276
|
+
const { file, readable, unknown } = this.readGuarded(scope);
|
|
277
|
+
// Refuse to commit over state we could not parse. Writing here would
|
|
278
|
+
// replace every existing credential with whatever this mutation adds.
|
|
279
|
+
if (!readable) {
|
|
280
|
+
throw new Error("refusing to modify an unreadable credential store; " +
|
|
281
|
+
"move the corrupt file aside to start a new one");
|
|
282
|
+
}
|
|
283
|
+
if (change(file))
|
|
284
|
+
this.write(scope, file, unknown);
|
|
285
|
+
}
|
|
286
|
+
finally {
|
|
287
|
+
release();
|
|
288
|
+
}
|
|
104
289
|
}
|
|
105
290
|
/** Upsert by id within a scope. */
|
|
106
291
|
save(scope, cred) {
|
|
107
|
-
const
|
|
108
|
-
const safeCredential = credentialAllowsEnvExposure(
|
|
109
|
-
?
|
|
110
|
-
: { ...
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
292
|
+
const normalized = normalizeCredential(cred, true);
|
|
293
|
+
const safeCredential = credentialAllowsEnvExposure(normalized.type)
|
|
294
|
+
? normalized
|
|
295
|
+
: { ...normalized, exposeAsEnv: undefined };
|
|
296
|
+
this.mutate(scope, (file) => {
|
|
297
|
+
const idx = file.credentials.findIndex((c) => c.id === safeCredential.id);
|
|
298
|
+
if (idx >= 0)
|
|
299
|
+
file.credentials[idx] = safeCredential;
|
|
300
|
+
else {
|
|
301
|
+
if (file.credentials.length >= MAX_CREDENTIALS) {
|
|
302
|
+
throw new Error("credential store has reached its maximum entry count");
|
|
303
|
+
}
|
|
304
|
+
file.credentials.push(safeCredential);
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
});
|
|
117
308
|
}
|
|
118
309
|
/**
|
|
119
310
|
* 只改元数据(label / exposeAsEnv / autoUseByAI / meta),保留 secret 原样。
|
|
@@ -121,22 +312,34 @@ export class CredentialStore {
|
|
|
121
312
|
* id 不存在则 no-op。
|
|
122
313
|
*/
|
|
123
314
|
patch(scope, id, fields) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
315
|
+
if (typeof id !== "string" || !id || id.length > MAX_CREDENTIAL_ID_CHARS || id.includes("\0")) {
|
|
316
|
+
throw new Error("invalid credential id");
|
|
317
|
+
}
|
|
318
|
+
this.mutate(scope, (file) => {
|
|
319
|
+
const idx = file.credentials.findIndex((c) => c.id === id);
|
|
320
|
+
if (idx < 0)
|
|
321
|
+
return false;
|
|
322
|
+
const current = file.credentials[idx];
|
|
323
|
+
const updated = normalizeCredential({
|
|
324
|
+
...current,
|
|
325
|
+
...fields,
|
|
326
|
+
...(credentialAllowsEnvExposure(current.type) ? {} : { exposeAsEnv: undefined }),
|
|
327
|
+
}, true);
|
|
328
|
+
file.credentials[idx] = updated;
|
|
329
|
+
return true;
|
|
330
|
+
});
|
|
135
331
|
}
|
|
136
332
|
remove(scope, id) {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
333
|
+
if (typeof id !== "string" || !id || id.length > MAX_CREDENTIAL_ID_CHARS || id.includes("\0")) {
|
|
334
|
+
throw new Error("invalid credential id");
|
|
335
|
+
}
|
|
336
|
+
this.mutate(scope, (file) => {
|
|
337
|
+
const next = file.credentials.filter((c) => c.id !== id);
|
|
338
|
+
if (next.length === file.credentials.length)
|
|
339
|
+
return false;
|
|
340
|
+
file.credentials = next;
|
|
341
|
+
return true;
|
|
342
|
+
});
|
|
140
343
|
}
|
|
141
344
|
/**
|
|
142
345
|
* List credentials visible to an engine of the given settings scope.
|
package/dist/engine/engine.js
CHANGED
|
@@ -44,7 +44,7 @@ import { detectProviderFromApiKey, buildModelPool } from "../onboarding.js";
|
|
|
44
44
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
45
45
|
import { PromptCacheDiagnosticRecorder, promptCacheDropHint, } from "./prompt-cache-diagnostics.js";
|
|
46
46
|
import { buildRunUserMessageContent, prepareRunImageInput } from "./run-image-input.js";
|
|
47
|
-
import { QUICK_CHAT_RESTRICTED_PROFILE, } from "./run-types.js";
|
|
47
|
+
import { ISOLATED_TASK_PROFILE, QUICK_CHAT_RESTRICTED_PROFILE, } from "./run-types.js";
|
|
48
48
|
import { createSubAgentSpawner } from "./subagent-spawner.js";
|
|
49
49
|
import { AuxiliaryPipeline, sameLlmIdentity } from "./auxiliary-pipeline.js";
|
|
50
50
|
import { PermissionController } from "./permission-controller.js";
|
|
@@ -413,6 +413,7 @@ export class Engine {
|
|
|
413
413
|
// extension modules — later registrations override earlier ones by id.
|
|
414
414
|
this.behaviorProfiles = new Map([
|
|
415
415
|
QUICK_CHAT_RESTRICTED_PROFILE,
|
|
416
|
+
ISOLATED_TASK_PROFILE,
|
|
416
417
|
...(config.behaviorProfiles ?? []),
|
|
417
418
|
...(config.extensionModules ?? []).flatMap((module) => module.behaviorProfiles ?? []),
|
|
418
419
|
].map((profile) => [profile.id, profile]));
|
|
@@ -1069,10 +1070,6 @@ export class Engine {
|
|
|
1069
1070
|
// surfacing as `[-32603] Session not found: <sid>` on the very first
|
|
1070
1071
|
// TUI turn. Detection now uses `sessionManager.exists()` (one stat
|
|
1071
1072
|
// call) instead of a try/catch on resume.
|
|
1072
|
-
// wrappedOnStream (defined before the session opens, executed only after)
|
|
1073
|
-
// closes over `session`, so keep the declaration here and assign from the
|
|
1074
|
-
// opener's result.
|
|
1075
|
-
let session;
|
|
1076
1073
|
const openedResult = openRunSession({
|
|
1077
1074
|
sessionManager: this.sessionManager,
|
|
1078
1075
|
options,
|
|
@@ -1095,20 +1092,65 @@ export class Engine {
|
|
|
1095
1092
|
});
|
|
1096
1093
|
if (!openedResult.ok)
|
|
1097
1094
|
return openedResult.result;
|
|
1098
|
-
const { messages, freshImageMessage, resumedFromDisk, claimClientMessageId, releaseClientMessageId, } = openedResult.opened;
|
|
1099
|
-
session = openedResult.opened.session;
|
|
1095
|
+
const { messages: openedMessages, freshImageMessage, resumedFromDisk, claimClientMessageId, releaseClientMessageId, } = openedResult.opened;
|
|
1096
|
+
const session = openedResult.opened.session;
|
|
1097
|
+
let messages = openedMessages;
|
|
1098
|
+
// A host-owned time/topic boundary must be applied only after the current
|
|
1099
|
+
// user message exists in the transcript (it is the exclusive end anchor),
|
|
1100
|
+
// yet before this turn assembles prompts or calls the model. Doing this as
|
|
1101
|
+
// a separate host query races both sides of that boundary: too early and
|
|
1102
|
+
// the anchor is dead; too late and the first new-topic prompt still sees
|
|
1103
|
+
// the entire old topic. Fail open to full history if summarization fails.
|
|
1104
|
+
if (options?.archiveBeforeCurrentTurn && options.clientMessageId) {
|
|
1105
|
+
try {
|
|
1106
|
+
const archived = await this.archiveTurnRange(session.state.sessionId, { start: 0, end: 0 }, {
|
|
1107
|
+
toClientMessageId: options.clientMessageId,
|
|
1108
|
+
...(options.archiveBeforeCurrentTurn.fromClientMessageId
|
|
1109
|
+
? {
|
|
1110
|
+
fromClientMessageId: options.archiveBeforeCurrentTurn.fromClientMessageId,
|
|
1111
|
+
}
|
|
1112
|
+
: {}),
|
|
1113
|
+
...(options.archiveBeforeCurrentTurn.segmentId
|
|
1114
|
+
? { segmentId: options.archiveBeforeCurrentTurn.segmentId }
|
|
1115
|
+
: {}),
|
|
1116
|
+
});
|
|
1117
|
+
if (archived.before > archived.after) {
|
|
1118
|
+
options.onStream?.({
|
|
1119
|
+
type: "context_compact",
|
|
1120
|
+
strategy: "range",
|
|
1121
|
+
before: archived.before,
|
|
1122
|
+
after: archived.after,
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
// openRunSession captured the pre-marker replay. Rebuild from the
|
|
1126
|
+
// persisted marker so this very first post-boundary model call gets
|
|
1127
|
+
// the archived view rather than waiting until the following turn.
|
|
1128
|
+
messages =
|
|
1129
|
+
this.compactedMessagesBySession.get(session.state.sessionId) ??
|
|
1130
|
+
this.sessionManager.resume(session.state.sessionId).transcript.toMessages();
|
|
1131
|
+
}
|
|
1132
|
+
catch (error) {
|
|
1133
|
+
logger.warn("engine.pre_run_archive.failed", {
|
|
1134
|
+
sessionId: session.state.sessionId,
|
|
1135
|
+
segmentId: options.archiveBeforeCurrentTurn.segmentId,
|
|
1136
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1137
|
+
});
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1100
1140
|
this.stampRunToolContext(toolCtx, session, options);
|
|
1101
1141
|
const sessionRun = runWithSid(session.state.sessionId, async () => {
|
|
1102
|
-
const hookMessages =
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1142
|
+
const hookMessages = profile?.disableHooks
|
|
1143
|
+
? []
|
|
1144
|
+
: await this.runSessionStartHooks({
|
|
1145
|
+
session,
|
|
1146
|
+
task,
|
|
1147
|
+
cwd,
|
|
1148
|
+
runPermissionMode,
|
|
1149
|
+
resumedFromDisk,
|
|
1150
|
+
options,
|
|
1151
|
+
taskText,
|
|
1152
|
+
messages,
|
|
1153
|
+
});
|
|
1112
1154
|
const sid = session.state.sessionId;
|
|
1113
1155
|
const { contextManager, llmClientPromise, toolExecutor } = this.wireRunContextAndPermission({
|
|
1114
1156
|
session,
|
|
@@ -1939,17 +1981,23 @@ export class Engine {
|
|
|
1939
1981
|
// A profile whose tool allowlist excludes the Skill tool can never invoke
|
|
1940
1982
|
// a skill, so the full skills listing would be dead context for every one
|
|
1941
1983
|
// of its turns (e.g. the Pet manager) — inject none via an empty allowlist.
|
|
1942
|
-
const
|
|
1984
|
+
const runAllowedToolNames = toolCtx.allowedToolNames;
|
|
1985
|
+
const profileCanUseSkills = !runAllowedToolNames || runAllowedToolNames.has(skillToolDef.name);
|
|
1943
1986
|
const promptComposer = new PromptComposer(buildPromptComposerConfig({
|
|
1944
1987
|
cwd,
|
|
1945
1988
|
model: this.config.llm.model,
|
|
1946
1989
|
preset: this.preset,
|
|
1947
|
-
customSystemPrompt:
|
|
1948
|
-
|
|
1990
|
+
customSystemPrompt: profile?.disableInstructions
|
|
1991
|
+
? undefined
|
|
1992
|
+
: this.config.customSystemPrompt,
|
|
1993
|
+
appendSystemPrompt: [
|
|
1994
|
+
profile?.disableInstructions ? undefined : this.config.appendSystemPrompt,
|
|
1995
|
+
profile?.systemPromptAppend,
|
|
1996
|
+
]
|
|
1949
1997
|
.filter(Boolean)
|
|
1950
1998
|
.join("\n\n") || undefined,
|
|
1951
1999
|
responseLanguage: this.config.responseLanguage,
|
|
1952
|
-
userProfile: this.config.userProfile,
|
|
2000
|
+
userProfile: profile?.disableInstructions ? undefined : this.config.userProfile,
|
|
1953
2001
|
workspaceProfile: runWorkspaceProfile,
|
|
1954
2002
|
// Read from the Session's own persisted state, so every turn — not just
|
|
1955
2003
|
// the first — carries the standing brief.
|
|
@@ -1959,15 +2007,21 @@ export class Engine {
|
|
|
1959
2007
|
instructionBoundaryFinder: (scanCwd) => resolveInstructionBoundary(scanCwd, this.capabilities),
|
|
1960
2008
|
disabledSkills,
|
|
1961
2009
|
disabledPlugins,
|
|
1962
|
-
skillAllowlist: profileCanUseSkills ?
|
|
2010
|
+
skillAllowlist: profileCanUseSkills ? toolCtx.skillAllowlist : [],
|
|
1963
2011
|
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1964
2012
|
memoryCurrentProjectOnly: profile?.memoryCurrentProjectOnly,
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
2013
|
+
disableInstructions: profile?.disableInstructions,
|
|
2014
|
+
disableMemoryContext: profile?.disableMemoryContext,
|
|
2015
|
+
disableCapabilityContext: profile?.disableCapabilityContext,
|
|
2016
|
+
disableSourcesContext: profile?.disableSourcesContext,
|
|
2017
|
+
goalToolState: !runAllowedToolNames ||
|
|
2018
|
+
runAllowedToolNames.has("complete_goal") ||
|
|
2019
|
+
runAllowedToolNames.has("cancel_goal")
|
|
1968
2020
|
? { hasGoal: hasRunnableGoal }
|
|
1969
2021
|
: undefined,
|
|
1970
|
-
capabilityPromptSections:
|
|
2022
|
+
capabilityPromptSections: profile?.disableCapabilityContext
|
|
2023
|
+
? {}
|
|
2024
|
+
: this.capabilityPromptSections,
|
|
1971
2025
|
dynamicContextProviders: this.capabilityDynamicContextProviders,
|
|
1972
2026
|
getSettingsManager: () => this.getSettingsManager(),
|
|
1973
2027
|
toolCatalog: this.toolCatalog,
|
|
@@ -2024,7 +2078,7 @@ export class Engine {
|
|
|
2024
2078
|
toolRewriters: this.toolRewriters,
|
|
2025
2079
|
toolFeatureFlags: TOOL_FEATURE_FLAGS,
|
|
2026
2080
|
applyBuiltinOverrideVisibility,
|
|
2027
|
-
profileAllowedToolNames:
|
|
2081
|
+
profileAllowedToolNames: runAllowedToolNames,
|
|
2028
2082
|
runPlanMode,
|
|
2029
2083
|
});
|
|
2030
2084
|
return { promptComposer, toolDefs };
|
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import { FileHistory } from "../session/file-history.js";
|
|
2
2
|
export function registerFileHistoryHook(options) {
|
|
3
3
|
const history = FileHistory.loadFromDir(options.sessionDir);
|
|
4
|
-
const
|
|
4
|
+
const pendingCreates = new Map();
|
|
5
|
+
const startHandler = async (context) => {
|
|
5
6
|
const toolName = context.data?.toolName;
|
|
6
7
|
const args = context.data?.args;
|
|
8
|
+
const toolCallId = context.data?.toolCallId;
|
|
7
9
|
const turnSeq = options.getTurnSeq();
|
|
8
10
|
if ((toolName === "Write" || toolName === "Edit") && typeof args?.file_path === "string") {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
+
const marker = turnSeq === undefined ? null : history.prepareCreated(args.file_path, turnSeq);
|
|
12
|
+
if (marker && typeof toolCallId === "string") {
|
|
13
|
+
pendingCreates.set(toolCallId, marker);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
history.saveSnapshot(args.file_path, turnSeq);
|
|
11
17
|
}
|
|
12
18
|
}
|
|
13
19
|
else if (args) {
|
|
@@ -18,14 +24,27 @@ export function registerFileHistoryHook(options) {
|
|
|
18
24
|
}
|
|
19
25
|
return {};
|
|
20
26
|
};
|
|
21
|
-
|
|
27
|
+
const endHandler = async (context) => {
|
|
28
|
+
const toolCallId = context.data?.toolCallId;
|
|
29
|
+
if (typeof toolCallId !== "string")
|
|
30
|
+
return {};
|
|
31
|
+
const marker = pendingCreates.get(toolCallId);
|
|
32
|
+
pendingCreates.delete(toolCallId);
|
|
33
|
+
if (marker && context.data?.isError !== true)
|
|
34
|
+
history.commitCreated(marker);
|
|
35
|
+
return {};
|
|
36
|
+
};
|
|
37
|
+
options.hooks.register("on_tool_start", startHandler, 100, "file_history_backup");
|
|
38
|
+
options.hooks.register("on_tool_end", endHandler, 100, "file_history_backup");
|
|
22
39
|
let disposed = false;
|
|
23
40
|
return {
|
|
24
41
|
dispose() {
|
|
25
42
|
if (disposed)
|
|
26
43
|
return;
|
|
27
44
|
disposed = true;
|
|
28
|
-
|
|
45
|
+
pendingCreates.clear();
|
|
46
|
+
options.hooks.unregister("on_tool_start", startHandler);
|
|
47
|
+
options.hooks.unregister("on_tool_end", endHandler);
|
|
29
48
|
},
|
|
30
49
|
};
|
|
31
50
|
}
|
|
@@ -88,11 +88,13 @@ export async function finalizeRunSuccess(args) {
|
|
|
88
88
|
// the turn loop has resolved (completion, error, or abort). Handlers
|
|
89
89
|
// are notify-only — any returned messages are dropped because the run
|
|
90
90
|
// is already over and there's no next turn to inject into.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
if (profile?.disableHooks !== true) {
|
|
92
|
+
await args.emitHook("on_session_end", {
|
|
93
|
+
sessionId: session.state.sessionId,
|
|
94
|
+
reason: result.reason,
|
|
95
|
+
turnCount,
|
|
96
|
+
}, options?.signal);
|
|
97
|
+
}
|
|
96
98
|
// Ephemeral side chats must never leak into durable memory, even after
|
|
97
99
|
// the user explicitly elevates tool permissions for a turn. Lifecycle
|
|
98
100
|
// isolation is independent of the run-scoped behavior/permission mode.
|
|
@@ -106,7 +108,7 @@ export async function finalizeRunSuccess(args) {
|
|
|
106
108
|
// Reuses the already-resolved auxSummaryClient (aux model, cheap). Best-
|
|
107
109
|
// effort: failures never touch the run result. The renderer writes the
|
|
108
110
|
// title into the sidebar on receipt of the session_title stream event.
|
|
109
|
-
{
|
|
111
|
+
if (profile?.disableSessionTitle !== true) {
|
|
110
112
|
const messageEvents = session.transcript.getEvents("message");
|
|
111
113
|
const userMsgEvents = messageEvents.filter((e) => e.data.role === "user");
|
|
112
114
|
const userMsgCount = userMsgEvents.length;
|
|
@@ -113,7 +113,7 @@ export function openRunSession(args) {
|
|
|
113
113
|
else {
|
|
114
114
|
// Cold start: shape (2) reuses the host-supplied sid; shape (3)
|
|
115
115
|
// lets sessionManager generate one with nanoid.
|
|
116
|
-
session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind);
|
|
116
|
+
session = args.sessionManager.create(args.cwd, args.llmModel, args.llmProvider, options?.sessionId, args.isSubAgent ? getCurrentSid() : undefined, args.isSubAgent ? "subagent" : args.origin, args.sessionKind, options?.ephemeral === true);
|
|
117
117
|
const userMsg = { role: "user", content: args.userMessageContent };
|
|
118
118
|
claimClientMessageId(session, options?.clientMessageId, "submit");
|
|
119
119
|
if (args.parsedTask.hasImages)
|
|
@@ -33,6 +33,10 @@ export interface RunPromptComposerConfigInput {
|
|
|
33
33
|
skillAllowlist: ComposerOptions["skillAllowlist"];
|
|
34
34
|
memoriesMaxAgeDays: ComposerOptions["memoriesMaxAgeDays"];
|
|
35
35
|
memoryCurrentProjectOnly?: ComposerOptions["memoryCurrentProjectOnly"];
|
|
36
|
+
disableInstructions?: ComposerOptions["disableInstructions"];
|
|
37
|
+
disableMemoryContext?: ComposerOptions["disableMemoryContext"];
|
|
38
|
+
disableCapabilityContext?: ComposerOptions["disableCapabilityContext"];
|
|
39
|
+
disableSourcesContext?: ComposerOptions["disableSourcesContext"];
|
|
36
40
|
goalToolState: ComposerOptions["goalToolState"];
|
|
37
41
|
capabilityPromptSections: ComposerOptions["capabilityPromptSections"];
|
|
38
42
|
dynamicContextProviders: ComposerOptions["dynamicContextProviders"];
|
package/dist/engine/run-setup.js
CHANGED
|
@@ -23,7 +23,7 @@ export function resolveRunProfileState(args) {
|
|
|
23
23
|
}
|
|
24
24
|
/** Build the prompt-composer options for a run without capturing the Engine facade. */
|
|
25
25
|
export function buildPromptComposerConfig(args) {
|
|
26
|
-
const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
26
|
+
const { cwd, model, preset, customSystemPrompt, appendSystemPrompt, responseLanguage, userProfile, workspaceProfile, sessionBrief, profileMemoryDir, instructionCompatFileNames, instructionBoundaryFinder, disabledSkills, disabledPlugins, skillAllowlist, memoriesMaxAgeDays, memoryCurrentProjectOnly, disableInstructions, disableMemoryContext, disableCapabilityContext, disableSourcesContext, goalToolState, capabilityPromptSections, dynamicContextProviders, getSettingsManager, toolCatalog, } = args;
|
|
27
27
|
return {
|
|
28
28
|
cwd,
|
|
29
29
|
model,
|
|
@@ -50,6 +50,10 @@ export function buildPromptComposerConfig(args) {
|
|
|
50
50
|
skillAllowlist,
|
|
51
51
|
memoriesMaxAgeDays,
|
|
52
52
|
memoryCurrentProjectOnly,
|
|
53
|
+
disableInstructions,
|
|
54
|
+
disableMemoryContext,
|
|
55
|
+
disableCapabilityContext,
|
|
56
|
+
disableSourcesContext,
|
|
53
57
|
goalToolState,
|
|
54
58
|
capabilityPromptSections,
|
|
55
59
|
dynamicContextProviders,
|
|
@@ -51,8 +51,14 @@ export function buildRunToolContext(args) {
|
|
|
51
51
|
return reason;
|
|
52
52
|
},
|
|
53
53
|
},
|
|
54
|
+
skillAllowlist: options?.skillAllowlist !== undefined
|
|
55
|
+
? [...options.skillAllowlist]
|
|
56
|
+
: args.base.skillAllowlist,
|
|
54
57
|
};
|
|
55
|
-
if (
|
|
58
|
+
if (options?.toolAllowlist !== undefined) {
|
|
59
|
+
toolCtx.allowedToolNames = new Set(options.toolAllowlist);
|
|
60
|
+
}
|
|
61
|
+
else if (profile?.allowedToolNames) {
|
|
56
62
|
toolCtx.allowedToolNames = profile.allowedToolNames;
|
|
57
63
|
}
|
|
58
64
|
if (profile?.createRunServices) {
|