@cjhyy/code-shell-core 0.6.0-rc.8 → 0.7.0-beta.1
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/THIRD_PARTY_NOTICES.md +206 -0
- package/dist/automation/scheduler.d.ts +13 -7
- package/dist/automation/scheduler.js +116 -37
- package/dist/capability-control/service.d.ts +2 -0
- package/dist/capability-control/service.js +4 -2
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +7 -1
- package/dist/cc-orchestrator/codex-session-history.js +1 -1
- package/dist/cc-orchestrator/cwd-normalize.d.ts +2 -0
- package/dist/cc-orchestrator/cwd-normalize.js +19 -0
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +27 -0
- package/dist/cc-orchestrator/external-agent-bindings.js +150 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -0
- package/dist/cc-orchestrator/external-agent-driver.js +102 -51
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +23 -0
- package/dist/cc-orchestrator/external-agent-session-store.js +146 -0
- package/dist/cc-orchestrator/session-history.js +2 -2
- package/dist/cli/agent-server-stdio.js +9 -2
- package/dist/context/compaction.d.ts +38 -1
- package/dist/context/compaction.js +138 -0
- package/dist/context/manager.d.ts +34 -2
- package/dist/context/manager.js +254 -58
- package/dist/context/token-counter.js +13 -0
- package/dist/credentials/access.d.ts +56 -0
- package/dist/credentials/access.js +183 -0
- package/dist/credentials/index.d.ts +1 -0
- package/dist/credentials/index.js +1 -0
- package/dist/credentials/inject-credential-tool.d.ts +3 -1
- package/dist/credentials/inject-credential-tool.js +30 -11
- package/dist/credentials/types.d.ts +2 -2
- package/dist/credentials/use-credential-tool.d.ts +9 -1
- package/dist/credentials/use-credential-tool.js +58 -45
- package/dist/engine/engine.d.ts +38 -12
- package/dist/engine/engine.js +477 -163
- package/dist/engine/image-policy.d.ts +6 -0
- package/dist/engine/image-policy.js +17 -6
- package/dist/engine/input-attachments.d.ts +13 -0
- package/dist/engine/input-attachments.js +255 -0
- package/dist/engine/model-facade.d.ts +5 -2
- package/dist/engine/model-facade.js +6 -16
- package/dist/engine/parse-task.d.ts +10 -0
- package/dist/engine/parse-task.js +5 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/session-usage.d.ts +12 -0
- package/dist/engine/session-usage.js +56 -0
- package/dist/engine/steer-queue.d.ts +2 -1
- package/dist/engine/steer-queue.js +2 -2
- package/dist/engine/streaming-tool-queue.d.ts +11 -7
- package/dist/engine/streaming-tool-queue.js +11 -7
- package/dist/engine/turn-loop.d.ts +34 -2
- package/dist/engine/turn-loop.js +269 -52
- package/dist/engine/types.d.ts +8 -0
- package/dist/git/worktree/crud.d.ts +69 -0
- package/dist/git/worktree/crud.js +206 -0
- package/dist/git/worktree/diff.d.ts +14 -0
- package/dist/git/worktree/diff.js +82 -0
- package/dist/git/worktree/git-exec.d.ts +7 -0
- package/dist/git/worktree/git-exec.js +51 -0
- package/dist/git/worktree/index.d.ts +5 -0
- package/dist/git/worktree/index.js +5 -0
- package/dist/git/worktree/query.d.ts +42 -0
- package/dist/git/worktree/query.js +121 -0
- package/dist/git/worktree/slug.d.ts +11 -0
- package/dist/git/worktree/slug.js +58 -0
- package/dist/git/worktree.d.ts +1 -84
- package/dist/git/worktree.js +5 -230
- package/dist/index.d.ts +31 -28
- package/dist/index.js +27 -25
- package/dist/logging/logger.js +6 -6
- package/dist/logging/sanitize-messages.d.ts +10 -2
- package/dist/logging/sanitize-messages.js +21 -6
- package/dist/plugins/installer/checkUpdate.d.ts +4 -1
- package/dist/plugins/installer/checkUpdate.js +4 -2
- package/dist/plugins/installer/install.js +2 -0
- package/dist/plugins/installer/installFromSource.js +9 -1
- package/dist/plugins/installer/parseSource.d.ts +4 -1
- package/dist/plugins/installer/parseSource.js +28 -10
- package/dist/plugins/installer/sourcePath.d.ts +9 -0
- package/dist/plugins/installer/sourcePath.js +50 -0
- package/dist/plugins/installer/update.d.ts +4 -1
- package/dist/plugins/installer/update.js +5 -3
- package/dist/plugins/parseMarketplaceInput.d.ts +4 -1
- package/dist/plugins/parseMarketplaceInput.js +4 -3
- package/dist/plugins/pluginInstaller.js +24 -22
- package/dist/preset/index.d.ts +1 -0
- package/dist/preset/index.js +16 -9
- package/dist/prompt/sections/base.md +1 -1
- package/dist/protocol/chat-session-manager.d.ts +2 -0
- package/dist/protocol/chat-session-manager.js +38 -2
- package/dist/protocol/chat-session.d.ts +5 -0
- package/dist/protocol/chat-session.js +2 -0
- package/dist/protocol/client.d.ts +13 -5
- package/dist/protocol/client.js +26 -3
- package/dist/protocol/server.d.ts +43 -12
- package/dist/protocol/server.js +326 -104
- package/dist/protocol/types.d.ts +51 -0
- package/dist/protocol/types.js +6 -0
- package/dist/run/FileRunStore.js +10 -1
- package/dist/run/Heartbeat.js +12 -0
- package/dist/run/RunApprovalBackend.d.ts +3 -0
- package/dist/run/RunApprovalBackend.js +41 -6
- package/dist/run/RunLock.js +2 -0
- package/dist/run/RunManager.d.ts +2 -0
- package/dist/run/RunManager.js +64 -24
- package/dist/run/ids.d.ts +2 -0
- package/dist/run/ids.js +23 -0
- package/dist/runtime/background-shell.d.ts +1 -0
- package/dist/runtime/background-shell.js +23 -13
- package/dist/runtime/safe-spawn.js +74 -11
- package/dist/runtime/spawn-common.js +10 -0
- package/dist/services/auto-dream.d.ts +15 -4
- package/dist/services/auto-dream.js +20 -20
- package/dist/services/dream-consolidation.d.ts +2 -3
- package/dist/services/dream-consolidation.js +65 -15
- package/dist/services/extract-memories.d.ts +14 -5
- package/dist/services/extract-memories.js +20 -3
- package/dist/services/global-dream-promotion.d.ts +23 -0
- package/dist/services/global-dream-promotion.js +112 -0
- package/dist/services/memory-orchestrator.js +347 -34
- package/dist/session/memory.d.ts +61 -18
- package/dist/session/memory.js +342 -79
- package/dist/session/session-manager.d.ts +35 -1
- package/dist/session/session-manager.js +197 -4
- package/dist/session/transcript.d.ts +5 -1
- package/dist/session/transcript.js +38 -5
- package/dist/settings/manager.d.ts +1 -0
- package/dist/settings/manager.js +87 -37
- package/dist/settings/schema-export.d.ts +2 -3
- package/dist/settings/schema-export.js +2 -3
- package/dist/settings/schema.d.ts +21 -0
- package/dist/settings/schema.js +12 -0
- package/dist/skills/scanner.d.ts +3 -2
- package/dist/skills/scanner.js +12 -9
- package/dist/tool-system/builtin/background-jobs.d.ts +10 -1
- package/dist/tool-system/builtin/background-jobs.js +12 -1
- package/dist/tool-system/builtin/background-work.d.ts +17 -18
- package/dist/tool-system/builtin/background-work.js +51 -5
- package/dist/tool-system/builtin/config.d.ts +2 -1
- package/dist/tool-system/builtin/config.js +16 -11
- package/dist/tool-system/builtin/drive-claude-code.d.ts +16 -2
- package/dist/tool-system/builtin/drive-claude-code.js +301 -47
- package/dist/tool-system/builtin/edit.js +5 -2
- package/dist/tool-system/builtin/generate-video.d.ts +1 -0
- package/dist/tool-system/builtin/generate-video.js +13 -4
- package/dist/tool-system/builtin/index.d.ts +8 -3
- package/dist/tool-system/builtin/index.js +37 -21
- package/dist/tool-system/builtin/lsp.d.ts +2 -1
- package/dist/tool-system/builtin/lsp.js +6 -3
- package/dist/tool-system/builtin/memory.js +40 -8
- package/dist/tool-system/builtin/notebook-edit.js +5 -2
- package/dist/tool-system/builtin/powershell.d.ts +5 -2
- package/dist/tool-system/builtin/powershell.js +11 -7
- package/dist/tool-system/builtin/read.js +118 -6
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/view-image.js +109 -19
- package/dist/tool-system/builtin/worktree.d.ts +4 -4
- package/dist/tool-system/builtin/worktree.js +297 -74
- package/dist/tool-system/builtin/write.js +5 -3
- package/dist/tool-system/context.d.ts +23 -1
- package/dist/tool-system/executor.d.ts +1 -5
- package/dist/tool-system/executor.js +94 -115
- package/dist/tool-system/mcp-manager.d.ts +18 -5
- package/dist/tool-system/mcp-manager.js +157 -74
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- package/dist/tool-system/path-policy.d.ts +2 -0
- package/dist/tool-system/path-policy.js +41 -12
- package/dist/tool-system/permission.d.ts +28 -7
- package/dist/tool-system/permission.js +130 -49
- package/dist/tool-system/registry.js +11 -4
- package/dist/tool-system/tool-result-redaction.d.ts +7 -0
- package/dist/tool-system/tool-result-redaction.js +48 -0
- package/dist/tool-system/workspace-bridge.d.ts +11 -0
- package/dist/tool-system/workspace-bridge.js +1 -0
- package/dist/types.d.ts +86 -5
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +4 -3
package/dist/protocol/types.d.ts
CHANGED
|
@@ -47,10 +47,43 @@ export declare const ErrorCodes: {
|
|
|
47
47
|
*/
|
|
48
48
|
readonly Cancelled: -32005;
|
|
49
49
|
};
|
|
50
|
+
export type InputAttachmentKind = "image" | "file" | "directory";
|
|
51
|
+
export type InputAttachmentOrigin = "paste" | "os-drop" | "file-panel" | "picker" | "mention" | "generated" | "tool";
|
|
52
|
+
export interface InputAttachmentMeta {
|
|
53
|
+
id: string;
|
|
54
|
+
sessionId: string;
|
|
55
|
+
kind: InputAttachmentKind;
|
|
56
|
+
origin: InputAttachmentOrigin;
|
|
57
|
+
path: string;
|
|
58
|
+
absPath: string;
|
|
59
|
+
relPath?: string;
|
|
60
|
+
mime?: string;
|
|
61
|
+
size: number;
|
|
62
|
+
sha256: string;
|
|
63
|
+
originalName?: string;
|
|
64
|
+
createdAt: number;
|
|
65
|
+
sourcePath?: string;
|
|
66
|
+
width?: number;
|
|
67
|
+
height?: number;
|
|
68
|
+
vision?: {
|
|
69
|
+
include: boolean;
|
|
70
|
+
mediaPath?: string;
|
|
71
|
+
detail?: "low" | "standard" | "high";
|
|
72
|
+
};
|
|
73
|
+
directory?: {
|
|
74
|
+
treePath?: string;
|
|
75
|
+
truncated?: boolean;
|
|
76
|
+
entryCount?: number;
|
|
77
|
+
};
|
|
78
|
+
}
|
|
50
79
|
/** Start an agent run with a user task. */
|
|
51
80
|
export interface RunParams {
|
|
52
81
|
sessionId: string;
|
|
53
82
|
task: string;
|
|
83
|
+
/** Structured input attachments. Legacy `<codeshell-image>` task blocks remain supported. */
|
|
84
|
+
attachments?: InputAttachmentMeta[];
|
|
85
|
+
/** Stable id for the user's submit intent; duplicate ids are idempotent. */
|
|
86
|
+
clientMessageId?: string;
|
|
54
87
|
/**
|
|
55
88
|
* Working directory for this run. When omitted, the Engine uses its
|
|
56
89
|
* configured cwd.
|
|
@@ -121,6 +154,10 @@ export interface CancelParams {
|
|
|
121
154
|
export interface CloseSessionParams {
|
|
122
155
|
sessionId: string;
|
|
123
156
|
}
|
|
157
|
+
/** Reset a live session's workspace binding back to main. */
|
|
158
|
+
export interface ReleaseWorkspaceParams {
|
|
159
|
+
sessionId: string;
|
|
160
|
+
}
|
|
124
161
|
/** Inject context into a session transcript. */
|
|
125
162
|
export interface InjectParams {
|
|
126
163
|
sessionId: string;
|
|
@@ -133,6 +170,8 @@ export interface SteerParams {
|
|
|
133
170
|
/** Stable host-side id for this queued draft. Rides through to the
|
|
134
171
|
* steer_injected event and is the handle Unsteer uses to revoke it. */
|
|
135
172
|
id?: string;
|
|
173
|
+
/** Stable submit-intent id, distinct from the queued steer id. */
|
|
174
|
+
clientMessageId?: string;
|
|
136
175
|
}
|
|
137
176
|
/** Revoke a still-pending steer entry by id (before the loop consumes it). */
|
|
138
177
|
export interface UnsteerParams {
|
|
@@ -256,6 +295,12 @@ export interface ApprovalRequestNotification {
|
|
|
256
295
|
requestId: string;
|
|
257
296
|
request: ApprovalRequest;
|
|
258
297
|
}
|
|
298
|
+
/** Server tells clients a pending approval/ask has been resolved elsewhere. */
|
|
299
|
+
export interface ApprovalResolvedNotification {
|
|
300
|
+
/** Originating engine session when known. */
|
|
301
|
+
sessionId?: string;
|
|
302
|
+
requestId: string;
|
|
303
|
+
}
|
|
259
304
|
/** Server status changed. */
|
|
260
305
|
export interface StatusNotification {
|
|
261
306
|
status: "ready" | "running" | "error" | "shutdown";
|
|
@@ -275,6 +320,8 @@ export declare const Methods: {
|
|
|
275
320
|
readonly Unsteer: "agent/unsteer";
|
|
276
321
|
/** Close (destroy) a session. */
|
|
277
322
|
readonly CloseSession: "agent/closeSession";
|
|
323
|
+
/** Reset a session's workspace binding to main without closing it. */
|
|
324
|
+
readonly ReleaseWorkspace: "agent/releaseWorkspace";
|
|
278
325
|
/** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
|
|
279
326
|
readonly GoalExtend: "agent/goalExtend";
|
|
280
327
|
/** Clear a session's persisted active goal (CC /goal clear). */
|
|
@@ -289,6 +336,10 @@ export declare const Methods: {
|
|
|
289
336
|
readonly BackgroundWork: "agent/backgroundWork";
|
|
290
337
|
readonly StreamEvent: "agent/streamEvent";
|
|
291
338
|
readonly ApprovalRequest: "agent/approvalRequest";
|
|
339
|
+
/** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
|
|
340
|
+
* AskUserQuestion that timed out) so every client can dismiss its stale card
|
|
341
|
+
* without a user decision. Same envelope the desktop main broadcasts. */
|
|
342
|
+
readonly ApprovalResolved: "agent/approvalResolved";
|
|
292
343
|
readonly Status: "agent/status";
|
|
293
344
|
};
|
|
294
345
|
export declare function createRequest(method: string, params?: Record<string, unknown>): RpcRequest;
|
package/dist/protocol/types.js
CHANGED
|
@@ -41,6 +41,8 @@ export const Methods = {
|
|
|
41
41
|
Unsteer: "agent/unsteer",
|
|
42
42
|
/** Close (destroy) a session. */
|
|
43
43
|
CloseSession: "agent/closeSession",
|
|
44
|
+
/** Reset a session's workspace binding to main without closing it. */
|
|
45
|
+
ReleaseWorkspace: "agent/releaseWorkspace",
|
|
44
46
|
/** Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). */
|
|
45
47
|
GoalExtend: "agent/goalExtend",
|
|
46
48
|
/** Clear a session's persisted active goal (CC /goal clear). */
|
|
@@ -56,6 +58,10 @@ export const Methods = {
|
|
|
56
58
|
// Server → Client (notifications, no id)
|
|
57
59
|
StreamEvent: "agent/streamEvent",
|
|
58
60
|
ApprovalRequest: "agent/approvalRequest",
|
|
61
|
+
/** Server-initiated resolution of a pending approval/ask (e.g. a goal-mode
|
|
62
|
+
* AskUserQuestion that timed out) so every client can dismiss its stale card
|
|
63
|
+
* without a user decision. Same envelope the desktop main broadcasts. */
|
|
64
|
+
ApprovalResolved: "agent/approvalResolved",
|
|
59
65
|
Status: "agent/status",
|
|
60
66
|
};
|
|
61
67
|
// ─── Helpers ────────────────────────────────────────────────────────
|
package/dist/run/FileRunStore.js
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
import { mkdirSync, existsSync, readFileSync, writeFileSync, appendFileSync, readdirSync, renameSync, rmSync, } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
|
|
15
17
|
export class FileRunStore {
|
|
16
18
|
runsDir;
|
|
17
19
|
constructor(storageDir) {
|
|
@@ -20,6 +22,7 @@ export class FileRunStore {
|
|
|
20
22
|
}
|
|
21
23
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
22
24
|
runDir(runId) {
|
|
25
|
+
assertSafeRunId(runId);
|
|
23
26
|
return join(this.runsDir, runId);
|
|
24
27
|
}
|
|
25
28
|
ensureRunDir(runId) {
|
|
@@ -31,7 +34,7 @@ export class FileRunStore {
|
|
|
31
34
|
return dir;
|
|
32
35
|
}
|
|
33
36
|
writeJson(filePath, data) {
|
|
34
|
-
const tmp = filePath
|
|
37
|
+
const tmp = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
35
38
|
try {
|
|
36
39
|
writeFileSync(tmp, JSON.stringify(data, null, 2), "utf-8");
|
|
37
40
|
// Atomic rename — prevents partial writes on crash
|
|
@@ -153,6 +156,8 @@ export class FileRunStore {
|
|
|
153
156
|
}
|
|
154
157
|
// ─── Checkpoints ───────────────────────────────────────────────
|
|
155
158
|
async saveCheckpoint(cp) {
|
|
159
|
+
assertSafeRunId(cp.runId);
|
|
160
|
+
assertSafeRunFileId(cp.checkpointId, "checkpoint id");
|
|
156
161
|
const dir = this.runDir(cp.runId);
|
|
157
162
|
this.writeJson(join(dir, "checkpoints", `${cp.checkpointId}.json`), cp);
|
|
158
163
|
}
|
|
@@ -175,10 +180,14 @@ export class FileRunStore {
|
|
|
175
180
|
}
|
|
176
181
|
// ─── Approvals ─────────────────────────────────────────────────
|
|
177
182
|
async saveApproval(approval) {
|
|
183
|
+
assertSafeRunId(approval.runId);
|
|
184
|
+
assertSafeRunFileId(approval.approvalId, "approval id");
|
|
178
185
|
const dir = this.runDir(approval.runId);
|
|
179
186
|
this.writeJson(join(dir, "approvals", `${approval.approvalId}.json`), approval);
|
|
180
187
|
}
|
|
181
188
|
async getApproval(runId, approvalId) {
|
|
189
|
+
assertSafeRunId(runId);
|
|
190
|
+
assertSafeRunFileId(approvalId, "approval id");
|
|
182
191
|
return this.readJson(join(this.runDir(runId), "approvals", `${approvalId}.json`));
|
|
183
192
|
}
|
|
184
193
|
async getPendingApproval(runId) {
|
package/dist/run/Heartbeat.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
|
+
import { assertSafeRunId } from "./ids.js";
|
|
14
15
|
export class Heartbeat {
|
|
15
16
|
runsDir;
|
|
16
17
|
intervalMs;
|
|
@@ -23,6 +24,12 @@ export class Heartbeat {
|
|
|
23
24
|
* Start heartbeat for a run. Writes immediately, then repeats on interval.
|
|
24
25
|
*/
|
|
25
26
|
start(runId) {
|
|
27
|
+
assertSafeRunId(runId);
|
|
28
|
+
const existing = this.timers.get(runId);
|
|
29
|
+
if (existing) {
|
|
30
|
+
clearInterval(existing);
|
|
31
|
+
this.timers.delete(runId);
|
|
32
|
+
}
|
|
26
33
|
// Write first heartbeat immediately
|
|
27
34
|
this.write(runId);
|
|
28
35
|
// Schedule periodic heartbeats
|
|
@@ -37,6 +44,7 @@ export class Heartbeat {
|
|
|
37
44
|
* Stop heartbeat for a run and remove the heartbeat file.
|
|
38
45
|
*/
|
|
39
46
|
stop(runId) {
|
|
47
|
+
assertSafeRunId(runId);
|
|
40
48
|
const timer = this.timers.get(runId);
|
|
41
49
|
if (timer) {
|
|
42
50
|
clearInterval(timer);
|
|
@@ -56,6 +64,7 @@ export class Heartbeat {
|
|
|
56
64
|
* Read the last heartbeat for a run. Returns null if no heartbeat exists.
|
|
57
65
|
*/
|
|
58
66
|
read(runId) {
|
|
67
|
+
assertSafeRunId(runId);
|
|
59
68
|
const filePath = this.filePath(runId);
|
|
60
69
|
if (!existsSync(filePath))
|
|
61
70
|
return null;
|
|
@@ -71,6 +80,7 @@ export class Heartbeat {
|
|
|
71
80
|
* Returns true if stale or missing, false if recent.
|
|
72
81
|
*/
|
|
73
82
|
isStale(runId, thresholdMs) {
|
|
83
|
+
assertSafeRunId(runId);
|
|
74
84
|
const threshold = thresholdMs ?? this.intervalMs * 3;
|
|
75
85
|
const data = this.read(runId);
|
|
76
86
|
if (!data)
|
|
@@ -81,6 +91,7 @@ export class Heartbeat {
|
|
|
81
91
|
* Check if the process that wrote the heartbeat is still alive.
|
|
82
92
|
*/
|
|
83
93
|
isProcessAlive(runId) {
|
|
94
|
+
assertSafeRunId(runId);
|
|
84
95
|
const data = this.read(runId);
|
|
85
96
|
if (!data)
|
|
86
97
|
return false;
|
|
@@ -118,6 +129,7 @@ export class Heartbeat {
|
|
|
118
129
|
}
|
|
119
130
|
}
|
|
120
131
|
filePath(runId) {
|
|
132
|
+
assertSafeRunId(runId);
|
|
121
133
|
return join(this.runsDir, runId, "heartbeat");
|
|
122
134
|
}
|
|
123
135
|
}
|
|
@@ -16,6 +16,7 @@ import type { AskUserFn } from "../tool-system/builtin/ask-user.js";
|
|
|
16
16
|
export interface PendingApproval {
|
|
17
17
|
resolve: (result: ApprovalResult) => void;
|
|
18
18
|
request: ApprovalRequest;
|
|
19
|
+
timer: ReturnType<typeof setTimeout>;
|
|
19
20
|
}
|
|
20
21
|
export interface PendingInput {
|
|
21
22
|
resolve: (answer: string) => void;
|
|
@@ -32,6 +33,7 @@ export interface RunLifecycleHooks {
|
|
|
32
33
|
export declare class RunApprovalBackend implements ApprovalBackend {
|
|
33
34
|
private pendingApproval;
|
|
34
35
|
private hooks;
|
|
36
|
+
private approvalRequestSeq;
|
|
35
37
|
setHooks(hooks: RunLifecycleHooks): void;
|
|
36
38
|
/** Max time (ms) to wait for approval before auto-rejecting. Default: 24h */
|
|
37
39
|
private timeoutMs;
|
|
@@ -42,6 +44,7 @@ export declare class RunApprovalBackend implements ApprovalBackend {
|
|
|
42
44
|
*/
|
|
43
45
|
resolveApproval(result: ApprovalResult): boolean;
|
|
44
46
|
hasPendingApproval(): boolean;
|
|
47
|
+
private supersedePendingApproval;
|
|
45
48
|
}
|
|
46
49
|
/**
|
|
47
50
|
* Creates an AskUserFn that suspends the run when user input is needed.
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
export class RunApprovalBackend {
|
|
15
15
|
pendingApproval = null;
|
|
16
16
|
hooks = null;
|
|
17
|
+
approvalRequestSeq = 0;
|
|
17
18
|
setHooks(hooks) {
|
|
18
19
|
this.hooks = hooks;
|
|
19
20
|
}
|
|
@@ -23,7 +24,8 @@ export class RunApprovalBackend {
|
|
|
23
24
|
this.timeoutMs = ms;
|
|
24
25
|
}
|
|
25
26
|
async requestApproval(request) {
|
|
26
|
-
|
|
27
|
+
const hooks = this.hooks;
|
|
28
|
+
if (!hooks) {
|
|
27
29
|
// No hooks wired — fail CLOSED. This backend is the engine's approval
|
|
28
30
|
// path for an interactive run; if a host forgot to setHooks() there is
|
|
29
31
|
// no UI to ask, so auto-approving every tool (the old behavior) silently
|
|
@@ -34,11 +36,8 @@ export class RunApprovalBackend {
|
|
|
34
36
|
reason: "run approval backend has no lifecycle hooks wired (denied fail-closed)",
|
|
35
37
|
};
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
const { approvalId } = await this.hooks.onApprovalNeeded(request);
|
|
39
|
-
// Suspend execution until resolved (with timeout safety net)
|
|
39
|
+
const requestSeq = ++this.approvalRequestSeq;
|
|
40
40
|
return new Promise((resolve) => {
|
|
41
|
-
this.pendingApproval = { resolve, request };
|
|
42
41
|
const timer = setTimeout(() => {
|
|
43
42
|
if (this.pendingApproval?.resolve === resolve) {
|
|
44
43
|
this.pendingApproval = null;
|
|
@@ -48,6 +47,30 @@ export class RunApprovalBackend {
|
|
|
48
47
|
if (typeof timer === "object" && "unref" in timer) {
|
|
49
48
|
timer.unref();
|
|
50
49
|
}
|
|
50
|
+
this.supersedePendingApproval();
|
|
51
|
+
this.pendingApproval = { resolve, request, timer };
|
|
52
|
+
void Promise.resolve()
|
|
53
|
+
.then(() => hooks.onApprovalNeeded(request))
|
|
54
|
+
.then(() => {
|
|
55
|
+
if (requestSeq !== this.approvalRequestSeq &&
|
|
56
|
+
this.pendingApproval?.resolve === resolve) {
|
|
57
|
+
this.pendingApproval = null;
|
|
58
|
+
clearTimeout(timer);
|
|
59
|
+
resolve({
|
|
60
|
+
approved: false,
|
|
61
|
+
reason: "superseded: a newer approval request replaced this one before it was answered",
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}, (err) => {
|
|
65
|
+
if (this.pendingApproval?.resolve !== resolve)
|
|
66
|
+
return;
|
|
67
|
+
this.pendingApproval = null;
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
resolve({
|
|
70
|
+
approved: false,
|
|
71
|
+
reason: `approval request failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
72
|
+
});
|
|
73
|
+
});
|
|
51
74
|
});
|
|
52
75
|
}
|
|
53
76
|
/**
|
|
@@ -56,14 +79,26 @@ export class RunApprovalBackend {
|
|
|
56
79
|
resolveApproval(result) {
|
|
57
80
|
if (!this.pendingApproval)
|
|
58
81
|
return false;
|
|
59
|
-
const { resolve } = this.pendingApproval;
|
|
82
|
+
const { resolve, timer } = this.pendingApproval;
|
|
60
83
|
this.pendingApproval = null;
|
|
84
|
+
clearTimeout(timer);
|
|
61
85
|
resolve(result);
|
|
62
86
|
return true;
|
|
63
87
|
}
|
|
64
88
|
hasPendingApproval() {
|
|
65
89
|
return this.pendingApproval !== null;
|
|
66
90
|
}
|
|
91
|
+
supersedePendingApproval() {
|
|
92
|
+
if (!this.pendingApproval)
|
|
93
|
+
return;
|
|
94
|
+
const { resolve, timer } = this.pendingApproval;
|
|
95
|
+
this.pendingApproval = null;
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
resolve({
|
|
98
|
+
approved: false,
|
|
99
|
+
reason: "superseded: a newer approval request replaced this one before it was answered",
|
|
100
|
+
});
|
|
101
|
+
}
|
|
67
102
|
}
|
|
68
103
|
// ─── RunAskUserAdapter ───────────────────────────────────────────
|
|
69
104
|
/**
|
package/dist/run/RunLock.js
CHANGED
|
@@ -10,6 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { lock, check, unlock } from "../utils/lockfile.js";
|
|
12
12
|
import { logger } from "../logging/logger.js";
|
|
13
|
+
import { assertSafeRunId } from "./ids.js";
|
|
13
14
|
export class RunLock {
|
|
14
15
|
runsDir;
|
|
15
16
|
staleMs;
|
|
@@ -135,6 +136,7 @@ export class RunLock {
|
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
lockTarget(runId) {
|
|
139
|
+
assertSafeRunId(runId);
|
|
138
140
|
return join(this.runsDir, runId, "run.json");
|
|
139
141
|
}
|
|
140
142
|
}
|
package/dist/run/RunManager.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare class RunManager {
|
|
|
66
66
|
resume(runId: string, input?: ResumeRunInput): Promise<void>;
|
|
67
67
|
private resumeInner;
|
|
68
68
|
cancel(runId: string, reason?: string): Promise<void>;
|
|
69
|
+
private cancelInner;
|
|
69
70
|
get(runId: string): Promise<RunSnapshot | null>;
|
|
70
71
|
list(query?: ListRunsQuery): Promise<RunSnapshot[]>;
|
|
71
72
|
getEvents(runId: string): Promise<RunEvent[]>;
|
|
@@ -90,5 +91,6 @@ export declare class RunManager {
|
|
|
90
91
|
private transition;
|
|
91
92
|
private emitRunEvent;
|
|
92
93
|
private notifySubscribers;
|
|
94
|
+
private getPendingApprovalForResume;
|
|
93
95
|
private getOrThrow;
|
|
94
96
|
}
|
package/dist/run/RunManager.js
CHANGED
|
@@ -19,7 +19,13 @@ import { ArtifactTracker } from "./ArtifactTracker.js";
|
|
|
19
19
|
import { RunLock } from "./RunLock.js";
|
|
20
20
|
import { Heartbeat } from "./Heartbeat.js";
|
|
21
21
|
import { NoopEvaluator } from "./Evaluator.js";
|
|
22
|
+
import { assertSafeRunFileId, assertSafeRunId } from "./ids.js";
|
|
22
23
|
import { VALID_TRANSITIONS } from "./types.js";
|
|
24
|
+
function hasOwnString(value, key) {
|
|
25
|
+
return (value !== undefined &&
|
|
26
|
+
Object.prototype.hasOwnProperty.call(value, key) &&
|
|
27
|
+
typeof value[key] === "string");
|
|
28
|
+
}
|
|
23
29
|
export class RunManager {
|
|
24
30
|
store;
|
|
25
31
|
queue;
|
|
@@ -69,6 +75,9 @@ export class RunManager {
|
|
|
69
75
|
async submit(input) {
|
|
70
76
|
const now = Date.now();
|
|
71
77
|
const runId = nanoid(16);
|
|
78
|
+
if (input.parentRunId !== undefined && input.parentRunId !== null) {
|
|
79
|
+
assertSafeRunId(input.parentRunId);
|
|
80
|
+
}
|
|
72
81
|
const snapshot = {
|
|
73
82
|
runId,
|
|
74
83
|
objective: input.objective,
|
|
@@ -110,6 +119,7 @@ export class RunManager {
|
|
|
110
119
|
}
|
|
111
120
|
// ─── Resume ────────────────────────────────────────────────────
|
|
112
121
|
async resume(runId, input) {
|
|
122
|
+
assertSafeRunId(runId);
|
|
113
123
|
// Serialize resume/cancel decisions per run. Without this, two concurrent
|
|
114
124
|
// resumes (or resume racing cancel) both pass the status check below —
|
|
115
125
|
// status stays `waiting_*` until a later `await transition()` — and both
|
|
@@ -139,18 +149,15 @@ export class RunManager {
|
|
|
139
149
|
if (handle) {
|
|
140
150
|
if (run.status === "waiting_approval" && input?.approvalDecision) {
|
|
141
151
|
const { approvalId, approved, reason } = input.approvalDecision;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
reason,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
152
|
+
const approval = await this.getPendingApprovalForResume(run, approvalId);
|
|
153
|
+
approval.status = approved ? "approved" : "rejected";
|
|
154
|
+
approval.resolvedAt = Date.now();
|
|
155
|
+
await this.store.saveApproval(approval);
|
|
156
|
+
await this.emitRunEvent(runId, "approval_resolved", {
|
|
157
|
+
approvalId,
|
|
158
|
+
approved,
|
|
159
|
+
reason,
|
|
160
|
+
});
|
|
154
161
|
// Transition back to running and resolve the suspended Engine
|
|
155
162
|
await this.transition(run, "queued");
|
|
156
163
|
await this.transition(run, "running");
|
|
@@ -158,7 +165,7 @@ export class RunManager {
|
|
|
158
165
|
handle.resolveApproval(approved, reason);
|
|
159
166
|
return;
|
|
160
167
|
}
|
|
161
|
-
if (run.status === "waiting_input" && input
|
|
168
|
+
if (run.status === "waiting_input" && hasOwnString(input, "userInput")) {
|
|
162
169
|
await this.transition(run, "queued");
|
|
163
170
|
await this.transition(run, "running");
|
|
164
171
|
await this.emitRunEvent(runId, "run_resumed", { userInput: input.userInput });
|
|
@@ -177,17 +184,15 @@ export class RunManager {
|
|
|
177
184
|
// Re-queue the run for a fresh execution attempt
|
|
178
185
|
if (input?.approvalDecision) {
|
|
179
186
|
const { approvalId, approved, reason } = input.approvalDecision;
|
|
180
|
-
const approval = await this.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
});
|
|
190
|
-
}
|
|
187
|
+
const approval = await this.getPendingApprovalForResume(run, approvalId);
|
|
188
|
+
approval.status = approved ? "approved" : "rejected";
|
|
189
|
+
approval.resolvedAt = Date.now();
|
|
190
|
+
await this.store.saveApproval(approval);
|
|
191
|
+
await this.emitRunEvent(runId, "approval_resolved", {
|
|
192
|
+
approvalId,
|
|
193
|
+
approved,
|
|
194
|
+
reason,
|
|
195
|
+
});
|
|
191
196
|
}
|
|
192
197
|
await this.transition(run, "queued");
|
|
193
198
|
await this.emitRunEvent(runId, "run_resumed", {
|
|
@@ -197,6 +202,19 @@ export class RunManager {
|
|
|
197
202
|
}
|
|
198
203
|
// ─── Cancel ────────────────────────────────────────────────────
|
|
199
204
|
async cancel(runId, reason) {
|
|
205
|
+
assertSafeRunId(runId);
|
|
206
|
+
if (this.resolvingRuns.has(runId)) {
|
|
207
|
+
throw new Error(`Cannot cancel run ${runId}: another resume/cancel is already in progress`);
|
|
208
|
+
}
|
|
209
|
+
this.resolvingRuns.add(runId);
|
|
210
|
+
try {
|
|
211
|
+
await this.cancelInner(runId, reason);
|
|
212
|
+
}
|
|
213
|
+
finally {
|
|
214
|
+
this.resolvingRuns.delete(runId);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
async cancelInner(runId, reason) {
|
|
200
218
|
const run = await this.getOrThrow(runId);
|
|
201
219
|
if (run.status === "completed" || run.status === "failed" || run.status === "cancelled") {
|
|
202
220
|
throw new Error(`Cannot cancel run ${runId}: already in terminal state ${run.status}`);
|
|
@@ -225,16 +243,19 @@ export class RunManager {
|
|
|
225
243
|
}
|
|
226
244
|
// ─── Query ─────────────────────────────────────────────────────
|
|
227
245
|
async get(runId) {
|
|
246
|
+
assertSafeRunId(runId);
|
|
228
247
|
return this.store.get(runId);
|
|
229
248
|
}
|
|
230
249
|
async list(query) {
|
|
231
250
|
return this.store.list(query);
|
|
232
251
|
}
|
|
233
252
|
async getEvents(runId) {
|
|
253
|
+
assertSafeRunId(runId);
|
|
234
254
|
return this.store.listEvents(runId);
|
|
235
255
|
}
|
|
236
256
|
// ─── Attach (live stream) ──────────────────────────────────────
|
|
237
257
|
attach(runId, cb) {
|
|
258
|
+
assertSafeRunId(runId);
|
|
238
259
|
if (!this.subscribers.has(runId)) {
|
|
239
260
|
this.subscribers.set(runId, new Set());
|
|
240
261
|
}
|
|
@@ -326,6 +347,7 @@ export class RunManager {
|
|
|
326
347
|
}
|
|
327
348
|
// ─── Internal: Execute a run ───────────────────────────────────
|
|
328
349
|
async executeRun(runId) {
|
|
350
|
+
assertSafeRunId(runId);
|
|
329
351
|
const run = await this.getOrThrow(runId);
|
|
330
352
|
// Acquire run lock — prevents concurrent execution by multiple workers
|
|
331
353
|
const lockResult = await this.lock.acquire(runId);
|
|
@@ -657,7 +679,25 @@ export class RunManager {
|
|
|
657
679
|
}
|
|
658
680
|
}
|
|
659
681
|
// ─── Helpers ───────────────────────────────────────────────────
|
|
682
|
+
async getPendingApprovalForResume(run, approvalId) {
|
|
683
|
+
assertSafeRunFileId(approvalId, "approval id");
|
|
684
|
+
if (run.latestApprovalId !== approvalId) {
|
|
685
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} is not the latest pending approval`);
|
|
686
|
+
}
|
|
687
|
+
const approval = await this.store.getApproval(run.runId, approvalId);
|
|
688
|
+
if (!approval) {
|
|
689
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} was not found`);
|
|
690
|
+
}
|
|
691
|
+
if (approval.runId !== run.runId || approval.approvalId !== approvalId) {
|
|
692
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} does not match this run`);
|
|
693
|
+
}
|
|
694
|
+
if (approval.status !== "pending") {
|
|
695
|
+
throw new Error(`Cannot resume run ${run.runId}: approval ${approvalId} is ${approval.status}, not pending`);
|
|
696
|
+
}
|
|
697
|
+
return approval;
|
|
698
|
+
}
|
|
660
699
|
async getOrThrow(runId) {
|
|
700
|
+
assertSafeRunId(runId);
|
|
661
701
|
const run = await this.store.get(runId);
|
|
662
702
|
if (!run)
|
|
663
703
|
throw new Error(`Run not found: ${runId}`);
|
package/dist/run/ids.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
function assertSafeRunSegment(value, label) {
|
|
2
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
3
|
+
throw new Error(`invalid ${label}: must be a non-empty string`);
|
|
4
|
+
}
|
|
5
|
+
if (value.includes("/") || value.includes("\\")) {
|
|
6
|
+
throw new Error(`invalid ${label}: contains path separator: ${value}`);
|
|
7
|
+
}
|
|
8
|
+
if (value === "." || value === ".." || value.includes("..")) {
|
|
9
|
+
throw new Error(`invalid ${label}: contains parent-dir token: ${value}`);
|
|
10
|
+
}
|
|
11
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(value)) {
|
|
12
|
+
throw new Error(`invalid ${label}: unexpected characters: ${value}`);
|
|
13
|
+
}
|
|
14
|
+
if (value.length > 128) {
|
|
15
|
+
throw new Error(`invalid ${label}: too long (max 128 chars)`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function assertSafeRunId(runId) {
|
|
19
|
+
assertSafeRunSegment(runId, "run id");
|
|
20
|
+
}
|
|
21
|
+
export function assertSafeRunFileId(value, label) {
|
|
22
|
+
assertSafeRunSegment(value, label);
|
|
23
|
+
}
|
|
@@ -103,6 +103,7 @@ export declare class BackgroundShellManager {
|
|
|
103
103
|
/** Raw retained output (no cleaning) — test/UI helper. */
|
|
104
104
|
readOutputRaw(shellId: string): string | undefined;
|
|
105
105
|
listForSession(sessionId: string): BgShell[];
|
|
106
|
+
list(): BgShell[];
|
|
106
107
|
/**
|
|
107
108
|
* Read a shell's output. `mode=incremental` (default) returns text since the
|
|
108
109
|
* agent's last read; `mode=all` returns the full retained buffer. Output is
|