@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
|
@@ -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,10 +164,26 @@ 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 = [];
|
|
170
183
|
this.forEachSession((session) => {
|
|
184
|
+
const sessionManager = this.engineSessionManager(session.engine);
|
|
185
|
+
if (sessionManager?.isEphemeralSession?.(session.id))
|
|
186
|
+
return;
|
|
171
187
|
sessions.push({
|
|
172
188
|
sessionId: session.id,
|
|
173
189
|
busy: session.isBusy(),
|
|
@@ -280,6 +296,12 @@ export class ChatSessionManager {
|
|
|
280
296
|
sweepIdle() {
|
|
281
297
|
const cutoff = Date.now() - this.idleTtlMs;
|
|
282
298
|
for (const [id, s] of [...this.sessions]) {
|
|
299
|
+
// Quick Chat transcripts are process-local by design. Closing one from
|
|
300
|
+
// the generic idle sweeper destroys its inherited context while the
|
|
301
|
+
// renderer still owns (and displays) the panel. Its explicit claim/
|
|
302
|
+
// panel lifecycle is the sole authority that may close it.
|
|
303
|
+
if (id.startsWith("qchat-"))
|
|
304
|
+
continue;
|
|
283
305
|
if (s.lastActivityAt >= cutoff)
|
|
284
306
|
continue;
|
|
285
307
|
if (s.isBusy())
|
|
@@ -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
|
@@ -58,10 +58,60 @@ function runInputError(params) {
|
|
|
58
58
|
if (params.injected !== undefined && typeof params.injected !== "boolean") {
|
|
59
59
|
return "injected must be a boolean";
|
|
60
60
|
}
|
|
61
|
+
if (params.quickChatClaimId !== undefined &&
|
|
62
|
+
(typeof params.quickChatClaimId !== "string" || params.quickChatClaimId.length === 0)) {
|
|
63
|
+
return "quickChatClaimId must be a non-empty string";
|
|
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
|
+
}
|
|
61
86
|
if (params.behaviorMode !== undefined &&
|
|
62
87
|
(typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
|
|
63
88
|
return `invalid behavior mode: ${String(params.behaviorMode)}`;
|
|
64
89
|
}
|
|
90
|
+
for (const [field, value, maxItems] of [
|
|
91
|
+
["toolAllowlist", params.toolAllowlist, 32],
|
|
92
|
+
["skillAllowlist", params.skillAllowlist, 8],
|
|
93
|
+
]) {
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
if (!Array.isArray(value) ||
|
|
97
|
+
value.length > maxItems ||
|
|
98
|
+
value.some((name) => typeof name !== "string" || name.length === 0 || name.length > 128 || /[\r\n]/.test(name))) {
|
|
99
|
+
return `${field} must be an array of at most ${maxItems} bounded names`;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (params.ephemeral !== undefined && typeof params.ephemeral !== "boolean") {
|
|
103
|
+
return "ephemeral must be a boolean";
|
|
104
|
+
}
|
|
105
|
+
if (params.maxTurns !== undefined &&
|
|
106
|
+
(!Number.isInteger(params.maxTurns) || params.maxTurns < 1 || params.maxTurns > 30)) {
|
|
107
|
+
return "maxTurns must be an integer from 1 to 30";
|
|
108
|
+
}
|
|
109
|
+
if (params.maxContextTokens !== undefined &&
|
|
110
|
+
(!Number.isInteger(params.maxContextTokens) ||
|
|
111
|
+
params.maxContextTokens < 4_096 ||
|
|
112
|
+
params.maxContextTokens > 131_072)) {
|
|
113
|
+
return "maxContextTokens must be an integer from 4096 to 131072";
|
|
114
|
+
}
|
|
65
115
|
if (params.kind !== undefined && (typeof params.kind !== "string" || params.kind.length === 0)) {
|
|
66
116
|
return `invalid session kind: ${String(params.kind)}`;
|
|
67
117
|
}
|
|
@@ -75,6 +125,20 @@ function runInputError(params) {
|
|
|
75
125
|
// extension-owned param shapes) lives in ExtensionModule.validateRunParams.
|
|
76
126
|
return null;
|
|
77
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
|
+
}
|
|
78
142
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
79
143
|
"micro",
|
|
80
144
|
"summary",
|
|
@@ -1080,6 +1144,8 @@ export class AgentServer {
|
|
|
1080
1144
|
const sessionConfig = {
|
|
1081
1145
|
cwd: params.cwd,
|
|
1082
1146
|
projectTrusted: params.projectTrusted,
|
|
1147
|
+
maxTurns: params.maxTurns,
|
|
1148
|
+
maxContextTokens: params.maxContextTokens,
|
|
1083
1149
|
goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
|
|
1084
1150
|
? params.goal
|
|
1085
1151
|
: undefined,
|
|
@@ -1160,9 +1226,13 @@ export class AgentServer {
|
|
|
1160
1226
|
this.notify(Methods.StreamEvent, { sessionId: sid, event });
|
|
1161
1227
|
},
|
|
1162
1228
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
1229
|
+
archiveBeforeCurrentTurn: params.archiveBeforeCurrentTurn,
|
|
1163
1230
|
permissionMode: params.permissionMode,
|
|
1164
1231
|
planMode: params.planMode,
|
|
1165
1232
|
behaviorMode: params.behaviorMode,
|
|
1233
|
+
toolAllowlist: params.toolAllowlist,
|
|
1234
|
+
skillAllowlist: params.skillAllowlist,
|
|
1235
|
+
ephemeral: params.ephemeral,
|
|
1166
1236
|
profileParams: params.profileParams,
|
|
1167
1237
|
workspaceProfile: params.workspaceProfile,
|
|
1168
1238
|
sessionBrief: params.sessionBrief,
|
|
@@ -1290,6 +1360,9 @@ export class AgentServer {
|
|
|
1290
1360
|
permissionMode: params.permissionMode,
|
|
1291
1361
|
planMode: params.planMode,
|
|
1292
1362
|
behaviorMode: params.behaviorMode,
|
|
1363
|
+
toolAllowlist: params.toolAllowlist,
|
|
1364
|
+
skillAllowlist: params.skillAllowlist,
|
|
1365
|
+
ephemeral: params.ephemeral,
|
|
1293
1366
|
profileParams: params.profileParams,
|
|
1294
1367
|
workspaceProfile: params.workspaceProfile,
|
|
1295
1368
|
sessionBrief: params.sessionBrief,
|
|
@@ -1435,7 +1508,17 @@ export class AgentServer {
|
|
|
1435
1508
|
* limits, or an error if no session / no active run.
|
|
1436
1509
|
*/
|
|
1437
1510
|
handleGoalExtend(req) {
|
|
1438
|
-
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;
|
|
1439
1522
|
const ext = {
|
|
1440
1523
|
addTurns: params.addTurns,
|
|
1441
1524
|
addTokenBudget: params.addTokenBudget,
|
|
@@ -2099,7 +2182,7 @@ export class AgentServer {
|
|
|
2099
2182
|
resolved = session.engine;
|
|
2100
2183
|
}
|
|
2101
2184
|
else {
|
|
2102
|
-
resolved
|
|
2185
|
+
resolved ??= this.anyEngine();
|
|
2103
2186
|
}
|
|
2104
2187
|
}
|
|
2105
2188
|
if (!resolved) {
|
|
@@ -2110,16 +2193,16 @@ export class AgentServer {
|
|
|
2110
2193
|
}
|
|
2111
2194
|
async handleQuery(req) {
|
|
2112
2195
|
const params = (req.params ?? {});
|
|
2113
|
-
//
|
|
2114
|
-
//
|
|
2115
|
-
|
|
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();
|
|
2116
2200
|
switch (params.type) {
|
|
2117
2201
|
case "tools": {
|
|
2118
|
-
|
|
2119
|
-
|
|
2202
|
+
const toolsEngine = await this.resolveEngineForSessionQuery(req, params.sessionId, engine, "tools");
|
|
2203
|
+
if (!toolsEngine)
|
|
2120
2204
|
return;
|
|
2121
|
-
|
|
2122
|
-
const registry = engine.getToolRegistry();
|
|
2205
|
+
const registry = toolsEngine.getToolRegistry();
|
|
2123
2206
|
const tools = typeof registry.listToolsDetailed === "function"
|
|
2124
2207
|
? registry
|
|
2125
2208
|
.listToolsDetailed()
|
|
@@ -2146,16 +2229,15 @@ export class AgentServer {
|
|
|
2146
2229
|
break;
|
|
2147
2230
|
}
|
|
2148
2231
|
case "config": {
|
|
2149
|
-
|
|
2150
|
-
|
|
2232
|
+
const configEngine = await this.resolveEngineForSessionQuery(req, params.sessionId, engine, "config");
|
|
2233
|
+
if (!configEngine)
|
|
2151
2234
|
return;
|
|
2152
|
-
|
|
2153
|
-
const config = engine.getConfig();
|
|
2235
|
+
const config = configEngine.getConfig();
|
|
2154
2236
|
this.transport.send(createResponse(req.id, {
|
|
2155
2237
|
type: "config",
|
|
2156
2238
|
data: {
|
|
2157
2239
|
permissionMode: config.permissionMode ?? "default",
|
|
2158
|
-
planMode:
|
|
2240
|
+
planMode: configEngine.planMode ?? false,
|
|
2159
2241
|
preset: config.preset,
|
|
2160
2242
|
model: config.llm.model,
|
|
2161
2243
|
cwd: config.cwd,
|
|
@@ -2165,21 +2247,20 @@ export class AgentServer {
|
|
|
2165
2247
|
llm: redactLlmConfig(config.llm),
|
|
2166
2248
|
// Resolved feature flags (defaults merged with settings overlay)
|
|
2167
2249
|
// so the /features command can list current state.
|
|
2168
|
-
featureFlags:
|
|
2250
|
+
featureFlags: configEngine.getFeatureFlags(),
|
|
2169
2251
|
// Effective permission rules so /permissions can list them (TODO 5.1).
|
|
2170
|
-
permissionRules:
|
|
2252
|
+
permissionRules: configEngine.getPermissionRules(),
|
|
2171
2253
|
},
|
|
2172
2254
|
}));
|
|
2173
2255
|
break;
|
|
2174
2256
|
}
|
|
2175
2257
|
case "session_detail": {
|
|
2176
|
-
if (!engine) {
|
|
2177
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for session_detail query"));
|
|
2178
|
-
return;
|
|
2179
|
-
}
|
|
2180
2258
|
if (params.sessionId) {
|
|
2259
|
+
const detailEngine = await this.resolveEngineForSessionQuery(req, params.sessionId, engine, "session_detail");
|
|
2260
|
+
if (!detailEngine)
|
|
2261
|
+
return;
|
|
2181
2262
|
try {
|
|
2182
|
-
const bundle =
|
|
2263
|
+
const bundle = detailEngine.getSessionManager().resume(params.sessionId);
|
|
2183
2264
|
const data = {
|
|
2184
2265
|
state: bundle.state,
|
|
2185
2266
|
transcript: bundle.transcript.getEvents(),
|
|
@@ -3127,16 +3208,18 @@ export class AgentServer {
|
|
|
3127
3208
|
anyEngine() {
|
|
3128
3209
|
if (!this.chatManager)
|
|
3129
3210
|
return null;
|
|
3130
|
-
const
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
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;
|
|
3134
3220
|
if (this.globalQueryEngine)
|
|
3135
3221
|
return this.globalQueryEngine;
|
|
3136
|
-
|
|
3137
|
-
if (typeof factory !== "function")
|
|
3138
|
-
return null;
|
|
3139
|
-
this.globalQueryEngine = factory({});
|
|
3222
|
+
this.globalQueryEngine = this.chatManager.createDetachedEngine();
|
|
3140
3223
|
return this.globalQueryEngine;
|
|
3141
3224
|
}
|
|
3142
3225
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
package/dist/protocol/types.d.ts
CHANGED
|
@@ -80,6 +80,16 @@ 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
|
+
};
|
|
91
|
+
/** Desktop host ownership generation for process-local Quick Chat runs. */
|
|
92
|
+
quickChatClaimId?: string;
|
|
83
93
|
/**
|
|
84
94
|
* Host-generated system reminder. Persisted with an injected marker so disk
|
|
85
95
|
* transcript readers hide it as a user bubble while retaining the assistant reply.
|
|
@@ -116,6 +126,16 @@ export interface RunParams {
|
|
|
116
126
|
planMode?: boolean;
|
|
117
127
|
/** Named behavior profile for a product-specific per-run interaction mode. */
|
|
118
128
|
behaviorMode?: RunBehaviorMode;
|
|
129
|
+
/** Host-authorized per-run hard tool allowlist. Empty means no tools. */
|
|
130
|
+
toolAllowlist?: string[];
|
|
131
|
+
/** Host-authorized per-run hard Skill allowlist. Empty means no Skills. */
|
|
132
|
+
skillAllowlist?: string[];
|
|
133
|
+
/** Keep a fresh run Session process-local and out of normal Session pickers. */
|
|
134
|
+
ephemeral?: boolean;
|
|
135
|
+
/** Optional fresh-session turn ceiling used by bounded host Tasks. */
|
|
136
|
+
maxTurns?: number;
|
|
137
|
+
/** Optional fresh-session context ceiling used by bounded host Tasks. */
|
|
138
|
+
maxContextTokens?: number;
|
|
119
139
|
/**
|
|
120
140
|
* Generic per-run parameters consumed by the active behavior profile
|
|
121
141
|
* (delivered as non-durable run context; never appended to the task or
|
|
@@ -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;
|
package/dist/run/FileRunStore.js
CHANGED
|
@@ -9,34 +9,54 @@
|
|
|
9
9
|
* approvals/<id>.json — approval records
|
|
10
10
|
* artifacts/refs.jsonl — artifact reference log
|
|
11
11
|
*/
|
|
12
|
-
import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, renameSync, rmSync, } from "node:fs";
|
|
13
|
-
import { join } from "node:path";
|
|
12
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, closeSync, chmodSync, constants, fchmodSync, readdirSync, fstatSync, openSync, readSync, renameSync, rmSync, lstatSync, } from "node:fs";
|
|
13
|
+
import { dirname, join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { randomUUID } from "node:crypto";
|
|
16
16
|
import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
|
|
17
|
+
const MAX_RUN_JSON_BYTES = 8 * 1024 * 1024;
|
|
18
|
+
const MAX_RUN_JSONL_BYTES = 128 * 1024 * 1024;
|
|
19
|
+
const MAX_RUN_JSONL_RECORD_BYTES = 2 * 1024 * 1024;
|
|
20
|
+
const MAX_RUN_DIRECTORIES = 100_000;
|
|
21
|
+
const MAX_RUN_CHILD_FILES = 100_000;
|
|
17
22
|
export class FileRunStore {
|
|
18
23
|
runsDir;
|
|
19
24
|
constructor(storageDir) {
|
|
20
25
|
this.runsDir = storageDir ?? join(homedir(), ".code-shell", "runs");
|
|
21
|
-
mkdirSync(this.runsDir, { recursive: true });
|
|
26
|
+
mkdirSync(this.runsDir, { recursive: true, mode: 0o700 });
|
|
27
|
+
this.assertRealDirectory(this.runsDir);
|
|
28
|
+
if (process.platform !== "win32")
|
|
29
|
+
chmodSync(this.runsDir, 0o700);
|
|
22
30
|
}
|
|
23
31
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
24
32
|
runDir(runId) {
|
|
25
33
|
assertSafeRunId(runId);
|
|
26
|
-
|
|
34
|
+
const dir = join(this.runsDir, runId);
|
|
35
|
+
if (existsSync(dir))
|
|
36
|
+
this.assertRealDirectory(dir);
|
|
37
|
+
return dir;
|
|
27
38
|
}
|
|
28
39
|
ensureRunDir(runId) {
|
|
29
40
|
const dir = this.runDir(runId);
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
41
|
+
const directories = [dir, join(dir, "checkpoints"), join(dir, "approvals"), join(dir, "artifacts")];
|
|
42
|
+
for (const directory of directories) {
|
|
43
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
44
|
+
this.assertRealDirectory(directory);
|
|
45
|
+
if (process.platform !== "win32")
|
|
46
|
+
chmodSync(directory, 0o700);
|
|
47
|
+
}
|
|
34
48
|
return dir;
|
|
35
49
|
}
|
|
36
50
|
writeJson(filePath, data) {
|
|
51
|
+
this.assertRealDirectory(dirname(filePath));
|
|
52
|
+
this.assertSafeFileTarget(filePath);
|
|
53
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
54
|
+
if (Buffer.byteLength(serialized, "utf8") > MAX_RUN_JSON_BYTES) {
|
|
55
|
+
throw new Error(`Run JSON exceeds ${MAX_RUN_JSON_BYTES} bytes`);
|
|
56
|
+
}
|
|
37
57
|
const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
38
58
|
try {
|
|
39
|
-
writeFileSync(tmp,
|
|
59
|
+
writeFileSync(tmp, serialized, { encoding: "utf-8", mode: 0o600, flag: "wx" });
|
|
40
60
|
// Atomic rename — prevents partial writes on crash
|
|
41
61
|
renameSync(tmp, filePath);
|
|
42
62
|
}
|
|
@@ -49,7 +69,42 @@ export class FileRunStore {
|
|
|
49
69
|
readJson(filePath) {
|
|
50
70
|
if (!existsSync(filePath))
|
|
51
71
|
return null;
|
|
52
|
-
|
|
72
|
+
this.assertRealDirectory(dirname(filePath));
|
|
73
|
+
let fd;
|
|
74
|
+
try {
|
|
75
|
+
const entry = lstatSync(filePath);
|
|
76
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > MAX_RUN_JSON_BYTES) {
|
|
77
|
+
throw new Error(`Run JSON is not a bounded regular file: ${filePath}`);
|
|
78
|
+
}
|
|
79
|
+
fd = openSync(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
80
|
+
const opened = fstatSync(fd);
|
|
81
|
+
if (!opened.isFile() || opened.size > MAX_RUN_JSON_BYTES) {
|
|
82
|
+
throw new Error(`Run JSON is not a bounded regular file: ${filePath}`);
|
|
83
|
+
}
|
|
84
|
+
return JSON.parse(readFileSync(fd, "utf-8"));
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
if (fd !== undefined)
|
|
88
|
+
closeSync(fd);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
assertRealDirectory(directory) {
|
|
92
|
+
const info = lstatSync(directory);
|
|
93
|
+
if (info.isSymbolicLink() || !info.isDirectory()) {
|
|
94
|
+
throw new Error(`Run storage path is not a real directory: ${directory}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
assertSafeFileTarget(filePath) {
|
|
98
|
+
try {
|
|
99
|
+
const info = lstatSync(filePath);
|
|
100
|
+
if (info.isSymbolicLink() || !info.isFile()) {
|
|
101
|
+
throw new Error(`Run storage target is not a regular file: ${filePath}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error.code !== "ENOENT")
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
53
108
|
}
|
|
54
109
|
/** Serializes concurrent JSONL appends per file path. */
|
|
55
110
|
appendLocks = new Map();
|
|
@@ -68,7 +123,39 @@ export class FileRunStore {
|
|
|
68
123
|
this.appendLocks.set(filePath, lock);
|
|
69
124
|
try {
|
|
70
125
|
await prev.catch(() => { });
|
|
71
|
-
|
|
126
|
+
this.assertRealDirectory(dirname(filePath));
|
|
127
|
+
this.assertSafeFileTarget(filePath);
|
|
128
|
+
const record = JSON.stringify(data);
|
|
129
|
+
if (Buffer.byteLength(record, "utf8") > MAX_RUN_JSONL_RECORD_BYTES) {
|
|
130
|
+
throw new Error(`Run JSONL record exceeds ${MAX_RUN_JSONL_RECORD_BYTES} bytes`);
|
|
131
|
+
}
|
|
132
|
+
// A process crash can leave a partial final JSONL record. Without a
|
|
133
|
+
// separator, the first append after restart would concatenate a valid
|
|
134
|
+
// record onto that fragment and lose both records. Inspect and repair
|
|
135
|
+
// the boundary through the same append-mode descriptor used to write.
|
|
136
|
+
const fd = openSync(filePath, constants.O_APPEND |
|
|
137
|
+
constants.O_CREAT |
|
|
138
|
+
constants.O_RDWR |
|
|
139
|
+
(constants.O_NOFOLLOW ?? 0), 0o600);
|
|
140
|
+
try {
|
|
141
|
+
if (process.platform !== "win32")
|
|
142
|
+
fchmodSync(fd, 0o600);
|
|
143
|
+
const size = fstatSync(fd).size;
|
|
144
|
+
if (size + Buffer.byteLength(record, "utf8") + 2 > MAX_RUN_JSONL_BYTES) {
|
|
145
|
+
throw new Error(`Run JSONL exceeds ${MAX_RUN_JSONL_BYTES} bytes`);
|
|
146
|
+
}
|
|
147
|
+
let prefix = "";
|
|
148
|
+
if (size > 0) {
|
|
149
|
+
const lastByte = Buffer.allocUnsafe(1);
|
|
150
|
+
readSync(fd, lastByte, 0, 1, size - 1);
|
|
151
|
+
if (lastByte[0] !== 0x0a)
|
|
152
|
+
prefix = "\n";
|
|
153
|
+
}
|
|
154
|
+
appendFileSync(fd, prefix + record + "\n", "utf-8");
|
|
155
|
+
}
|
|
156
|
+
finally {
|
|
157
|
+
closeSync(fd);
|
|
158
|
+
}
|
|
72
159
|
}
|
|
73
160
|
finally {
|
|
74
161
|
// Release the lock for the next writer regardless of success/failure,
|
|
@@ -82,13 +169,41 @@ export class FileRunStore {
|
|
|
82
169
|
readJsonl(filePath) {
|
|
83
170
|
if (!existsSync(filePath))
|
|
84
171
|
return [];
|
|
85
|
-
|
|
172
|
+
this.assertRealDirectory(dirname(filePath));
|
|
173
|
+
let fd;
|
|
174
|
+
let content;
|
|
175
|
+
try {
|
|
176
|
+
const entry = lstatSync(filePath);
|
|
177
|
+
if (entry.isSymbolicLink() || !entry.isFile() || entry.size > MAX_RUN_JSONL_BYTES) {
|
|
178
|
+
throw new Error(`Run JSONL is not a bounded regular file: ${filePath}`);
|
|
179
|
+
}
|
|
180
|
+
fd = openSync(filePath, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
181
|
+
const opened = fstatSync(fd);
|
|
182
|
+
if (!opened.isFile() || opened.size > MAX_RUN_JSONL_BYTES) {
|
|
183
|
+
throw new Error(`Run JSONL is not a bounded regular file: ${filePath}`);
|
|
184
|
+
}
|
|
185
|
+
content = readFileSync(fd, "utf-8");
|
|
186
|
+
}
|
|
187
|
+
finally {
|
|
188
|
+
if (fd !== undefined)
|
|
189
|
+
closeSync(fd);
|
|
190
|
+
}
|
|
86
191
|
if (!content)
|
|
87
192
|
return [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
193
|
+
const records = [];
|
|
194
|
+
for (const line of content.split("\n")) {
|
|
195
|
+
if (!line.trim())
|
|
196
|
+
continue;
|
|
197
|
+
try {
|
|
198
|
+
records.push(JSON.parse(line));
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
// Append-only logs must remain readable after a torn final write. A
|
|
202
|
+
// malformed record is isolated to its line; later valid records still
|
|
203
|
+
// carry useful recovery state (same policy as Transcript.loadFromFile).
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return records;
|
|
92
207
|
}
|
|
93
208
|
// ─── Snapshot ──────────────────────────────────────────────────
|
|
94
209
|
async create(snapshot) {
|
|
@@ -109,11 +224,24 @@ export class FileRunStore {
|
|
|
109
224
|
if (!existsSync(this.runsDir))
|
|
110
225
|
return [];
|
|
111
226
|
const entries = readdirSync(this.runsDir, { withFileTypes: true });
|
|
227
|
+
if (entries.length > MAX_RUN_DIRECTORIES)
|
|
228
|
+
throw new Error("Run registry has too many entries");
|
|
112
229
|
const snapshots = [];
|
|
113
230
|
for (const entry of entries) {
|
|
114
231
|
if (!entry.isDirectory())
|
|
115
232
|
continue;
|
|
116
|
-
|
|
233
|
+
let snapshot;
|
|
234
|
+
try {
|
|
235
|
+
snapshot = this.readJson(join(this.runsDir, entry.name, "run.json"));
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
// One manually damaged/legacy partial snapshot must not make every
|
|
239
|
+
// healthy run disappear from the history list. Preserve real I/O
|
|
240
|
+
// failures, but isolate JSON corruption to the affected directory.
|
|
241
|
+
if (error instanceof SyntaxError)
|
|
242
|
+
continue;
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
117
245
|
if (!snapshot)
|
|
118
246
|
continue;
|
|
119
247
|
// Filter by status
|
|
@@ -123,8 +251,9 @@ export class FileRunStore {
|
|
|
123
251
|
continue;
|
|
124
252
|
}
|
|
125
253
|
// Filter by tag
|
|
126
|
-
if (query?.tag && !snapshot.tags.includes(query.tag))
|
|
254
|
+
if (query?.tag && (!Array.isArray(snapshot.tags) || !snapshot.tags.includes(query.tag))) {
|
|
127
255
|
continue;
|
|
256
|
+
}
|
|
128
257
|
snapshots.push(snapshot);
|
|
129
258
|
}
|
|
130
259
|
// Sort by createdAt descending (newest first)
|
|
@@ -165,7 +294,10 @@ export class FileRunStore {
|
|
|
165
294
|
const cpDir = join(this.runDir(runId), "checkpoints");
|
|
166
295
|
if (!existsSync(cpDir))
|
|
167
296
|
return null;
|
|
297
|
+
this.assertRealDirectory(cpDir);
|
|
168
298
|
const files = readdirSync(cpDir).filter((f) => f.endsWith(".json"));
|
|
299
|
+
if (files.length > MAX_RUN_CHILD_FILES)
|
|
300
|
+
throw new Error("Run has too many checkpoints");
|
|
169
301
|
if (files.length === 0)
|
|
170
302
|
return null;
|
|
171
303
|
// Find the latest by createdAt
|
|
@@ -194,7 +326,10 @@ export class FileRunStore {
|
|
|
194
326
|
const approvalDir = join(this.runDir(runId), "approvals");
|
|
195
327
|
if (!existsSync(approvalDir))
|
|
196
328
|
return null;
|
|
329
|
+
this.assertRealDirectory(approvalDir);
|
|
197
330
|
const files = readdirSync(approvalDir).filter((f) => f.endsWith(".json"));
|
|
331
|
+
if (files.length > MAX_RUN_CHILD_FILES)
|
|
332
|
+
throw new Error("Run has too many approvals");
|
|
198
333
|
for (const file of files) {
|
|
199
334
|
const approval = this.readJson(join(approvalDir, file));
|
|
200
335
|
if (approval?.status === "pending")
|