@cjhyy/code-shell-core 0.7.0-beta.1 → 0.7.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/dist/arena/arena.d.ts +2 -0
- package/dist/arena/arena.js +30 -1
- package/dist/arena/phases/adjudication.d.ts +2 -1
- package/dist/arena/phases/adjudication.js +2 -1
- package/dist/arena/phases/build-consensus.d.ts +2 -1
- package/dist/arena/phases/build-consensus.js +3 -1
- package/dist/arena/phases/cross-review.d.ts +3 -1
- package/dist/arena/phases/cross-review.js +8 -2
- package/dist/arena/phases/debate-rounds.d.ts +2 -1
- package/dist/arena/phases/debate-rounds.js +4 -2
- package/dist/arena/phases/participant-research.d.ts +2 -1
- package/dist/arena/phases/participant-research.js +3 -1
- package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
- package/dist/arena/phases/planning-detail-expansion.js +2 -1
- package/dist/arena/planner.d.ts +2 -1
- package/dist/arena/planner.js +2 -1
- package/dist/arena/types.d.ts +5 -1
- package/dist/cc-orchestrator/agent-adapter.d.ts +2 -0
- package/dist/cc-orchestrator/agent-adapter.js +4 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +14 -1
- package/dist/cc-orchestrator/codex-session-history.js +64 -4
- package/dist/cc-orchestrator/external-agent-changes.js +22 -5
- package/dist/cc-orchestrator/external-agent-driver.d.ts +1 -1
- package/dist/cc-orchestrator/external-agent-driver.js +202 -38
- package/dist/cc-orchestrator/session-history.d.ts +35 -0
- package/dist/cc-orchestrator/session-history.js +96 -13
- package/dist/cli/agent-server-tcp.js +13 -2
- package/dist/context/manager.d.ts +3 -3
- package/dist/context/manager.js +6 -6
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +11 -1
- package/dist/credentials/access.js +77 -1
- package/dist/credentials/index.d.ts +3 -1
- package/dist/credentials/index.js +2 -0
- package/dist/credentials/oauth.d.ts +25 -0
- package/dist/credentials/oauth.js +179 -0
- package/dist/credentials/store.d.ts +2 -1
- package/dist/credentials/store.js +19 -8
- package/dist/credentials/types.d.ts +84 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +67 -34
- package/dist/engine/engine.js +448 -247
- package/dist/engine/goal.d.ts +19 -0
- package/dist/engine/goal.js +16 -6
- package/dist/engine/input-attachments.js +156 -13
- package/dist/engine/run-image-input.d.ts +22 -0
- package/dist/engine/run-image-input.js +195 -0
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/steer-queue.d.ts +3 -1
- package/dist/engine/steer-queue.js +10 -2
- package/dist/engine/turn-loop.d.ts +48 -1
- package/dist/engine/turn-loop.js +307 -37
- package/dist/engine/types.d.ts +6 -2
- package/dist/git/worktree/crud.d.ts +3 -0
- package/dist/git/worktree/crud.js +32 -3
- package/dist/git/worktree/git-exec.d.ts +2 -2
- package/dist/git/worktree/git-exec.js +47 -11
- package/dist/git/worktree/query.d.ts +8 -7
- package/dist/git/worktree/query.js +27 -20
- package/dist/hooks/events.d.ts +3 -0
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +44 -2
- package/dist/hooks/goal-stop-hook.js +775 -52
- package/dist/hooks/registry.js +3 -0
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +7 -6
- package/dist/index.js +6 -5
- package/dist/llm/client-base.js +12 -10
- package/dist/llm/types.d.ts +12 -5
- package/dist/plugins/pluginCommandHook.d.ts +4 -4
- package/dist/plugins/pluginCommandHook.js +111 -13
- package/dist/preset/index.js +14 -4
- package/dist/protocol/chat-session-manager.d.ts +13 -2
- package/dist/protocol/chat-session-manager.js +90 -18
- package/dist/protocol/chat-session.d.ts +12 -0
- package/dist/protocol/chat-session.js +30 -5
- package/dist/protocol/client.d.ts +5 -2
- package/dist/protocol/client.js +22 -1
- package/dist/protocol/server.d.ts +25 -11
- package/dist/protocol/server.js +291 -73
- package/dist/protocol/types.d.ts +36 -2
- package/dist/protocol/types.js +2 -0
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +4 -1
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +233 -98
- package/dist/session/session-manager.d.ts +35 -6
- package/dist/session/session-manager.js +396 -27
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- package/dist/tool-system/builtin/agent-notifications.d.ts +11 -4
- package/dist/tool-system/builtin/agent-notifications.js +19 -7
- package/dist/tool-system/builtin/agent.js +5 -1
- package/dist/tool-system/builtin/arena.js +1 -0
- package/dist/tool-system/builtin/background-jobs.d.ts +28 -5
- package/dist/tool-system/builtin/background-jobs.js +109 -7
- package/dist/tool-system/builtin/background-work.d.ts +6 -1
- package/dist/tool-system/builtin/background-work.js +5 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +10 -5
- package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
- package/dist/tool-system/builtin/cron-list.definition.d.ts +3 -0
- package/dist/tool-system/builtin/cron-list.definition.js +6 -0
- package/dist/tool-system/builtin/cron.d.ts +1 -2
- package/dist/tool-system/builtin/cron.js +9 -7
- package/dist/tool-system/builtin/drive-claude-code.d.ts +7 -0
- package/dist/tool-system/builtin/drive-claude-code.js +307 -20
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +12 -4
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +138 -21
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/index.d.ts +25 -11
- package/dist/tool-system/builtin/index.js +60 -5
- package/dist/tool-system/builtin/sleep.d.ts +1 -2
- package/dist/tool-system/builtin/sleep.definition.d.ts +8 -0
- package/dist/tool-system/builtin/sleep.definition.js +28 -0
- package/dist/tool-system/builtin/sleep.js +1 -22
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +44 -3
- package/dist/tool-system/builtin/worktree.js +25 -7
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/context.d.ts +35 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +20 -2
- package/dist/tool-system/mcp-manager.js +111 -12
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +43 -3
- package/dist/tool-system/permission.js +383 -30
- package/dist/tool-system/registry.d.ts +3 -2
- package/dist/tool-system/registry.js +52 -34
- package/dist/types.d.ts +38 -7
- package/package.json +1 -1
|
@@ -8,8 +8,10 @@ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"
|
|
|
8
8
|
import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
9
9
|
import { logger } from "../logging/logger.js";
|
|
10
10
|
import { getCredentialAccess } from "../credentials/access.js";
|
|
11
|
+
import { buildOAuthRefreshRequest, oauthCredentialStatus, parseOAuthCredentialSecret, } from "../credentials/oauth.js";
|
|
11
12
|
import { ENV_ALLOWLIST } from "../runtime/spawn-common.js";
|
|
12
13
|
import { mkdir, readdir, unlink, writeFile } from "node:fs/promises";
|
|
14
|
+
import { randomUUID } from "node:crypto";
|
|
13
15
|
import { join } from "node:path";
|
|
14
16
|
import { diagnoseMcpStdioMissingCommand, previewPath } from "./mcp-stdio-diagnostics.js";
|
|
15
17
|
import { codeShellHome } from "../session/session-manager.js";
|
|
@@ -73,14 +75,14 @@ export function buildStdioEnv(serverName, config) {
|
|
|
73
75
|
* base; env-sourced secrets (`bearerTokenEnvVar`, `envHeaders`) layer on top
|
|
74
76
|
* and win on conflict. Pure + exported for unit testing.
|
|
75
77
|
*/
|
|
76
|
-
export function buildHttpHeaders(serverName, config, resolveCredential) {
|
|
78
|
+
export function buildHttpHeaders(serverName, config, resolveCredential, options = {}) {
|
|
77
79
|
const headers = { ...(config.headers ?? {}) };
|
|
78
80
|
if (config.credentialRef) {
|
|
79
|
-
const
|
|
80
|
-
if (
|
|
81
|
+
const credential = normalizeResolvedMcpCredential(resolveCredential?.(config.credentialRef));
|
|
82
|
+
if (!credential || credential.secret === "") {
|
|
81
83
|
throw new Error(`MCP server "${serverName}": credential "${config.credentialRef}" not found or empty`);
|
|
82
84
|
}
|
|
83
|
-
headers["Authorization"] = `Bearer ${
|
|
85
|
+
headers["Authorization"] = `Bearer ${bearerTokenFromMcpCredential(serverName, config.credentialRef, credential, options)}`;
|
|
84
86
|
}
|
|
85
87
|
else if (config.bearerTokenEnvVar) {
|
|
86
88
|
headers["Authorization"] = `Bearer ${readRequiredEnv(serverName, "bearerTokenEnvVar", config.bearerTokenEnvVar)}`;
|
|
@@ -90,19 +92,116 @@ export function buildHttpHeaders(serverName, config, resolveCredential) {
|
|
|
90
92
|
}
|
|
91
93
|
return headers;
|
|
92
94
|
}
|
|
93
|
-
|
|
95
|
+
function normalizeResolvedMcpCredential(value) {
|
|
96
|
+
if (typeof value === "string")
|
|
97
|
+
return { secret: value };
|
|
98
|
+
if (!value)
|
|
99
|
+
return undefined;
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
export function bearerTokenFromMcpCredential(serverName, credentialId, credential, options = {}) {
|
|
103
|
+
if (credential.type === undefined)
|
|
104
|
+
return credential.secret;
|
|
105
|
+
if (credential.type !== "oauth") {
|
|
106
|
+
if (credential.type !== "token" && credential.type !== "link") {
|
|
107
|
+
throw new Error(`MCP server "${serverName}": credential "${credentialId}" type "${credential.type}" cannot be used as a bearer token`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
const parsed = JSON.parse(credential.secret);
|
|
111
|
+
if (parsed !== null && typeof parsed === "object") {
|
|
112
|
+
throw new Error(`MCP server "${serverName}": credential "${credentialId}" resolved to a structured secret that does not match token metadata; refusing bearer injection`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (err instanceof SyntaxError)
|
|
117
|
+
return credential.secret;
|
|
118
|
+
throw err;
|
|
119
|
+
}
|
|
120
|
+
return credential.secret;
|
|
121
|
+
}
|
|
122
|
+
const secret = parseOAuthCredentialSecret(credential.secret);
|
|
123
|
+
const status = oauthCredentialStatus(secret, {
|
|
124
|
+
now: options.now,
|
|
125
|
+
skewMs: options.oauthRefreshSkewMs,
|
|
126
|
+
});
|
|
127
|
+
if (status.state === "expired") {
|
|
128
|
+
const refresh = buildOAuthRefreshRequest(credentialId, secret);
|
|
129
|
+
const refreshHint = refresh
|
|
130
|
+
? "refresh data is present, but automatic OAuth refresh is reserved and not wired yet"
|
|
131
|
+
: "missing refreshToken or tokenEndpoint for automatic refresh";
|
|
132
|
+
throw new Error(`MCP server "${serverName}": oauth credential "${credentialId}" access token expired; ${refreshHint}`);
|
|
133
|
+
}
|
|
134
|
+
return secret.accessToken;
|
|
135
|
+
}
|
|
136
|
+
export async function buildHttpHeadersWithCredentialAccess(serverName, config, access = getCredentialAccess(), options = {}) {
|
|
94
137
|
if (!config.credentialRef)
|
|
95
|
-
return buildHttpHeaders(serverName, config);
|
|
138
|
+
return buildHttpHeaders(serverName, config, undefined, options);
|
|
96
139
|
if (!access.resolveValue) {
|
|
97
140
|
throw new Error(`MCP server "${serverName}": credential "${config.credentialRef}" not found or empty`);
|
|
98
141
|
}
|
|
142
|
+
const meta = access.resolveMeta?.(undefined, config.credentialRef, "full");
|
|
143
|
+
if (!meta) {
|
|
144
|
+
throw new Error(`MCP server "${serverName}": credential "${config.credentialRef}" metadata is unavailable; refusing bearer injection`);
|
|
145
|
+
}
|
|
146
|
+
if (meta.type !== "token" && meta.type !== "link" && meta.type !== "oauth") {
|
|
147
|
+
throw new Error(`MCP server "${serverName}": credential "${config.credentialRef}" type "${meta.type}" cannot be used as a bearer token`);
|
|
148
|
+
}
|
|
99
149
|
const secret = await access.resolveValue({
|
|
100
150
|
cwd: undefined,
|
|
101
151
|
id: config.credentialRef,
|
|
102
152
|
scope: "full",
|
|
103
153
|
purpose: "mcp",
|
|
104
154
|
});
|
|
105
|
-
return buildHttpHeaders(serverName, config, (id) => id === config.credentialRef ? secret : undefined);
|
|
155
|
+
return buildHttpHeaders(serverName, config, (id) => id === config.credentialRef ? { secret, type: meta?.type, label: meta?.label } : undefined, options);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Fetch adapter for long-lived HTTP MCP transports. OAuth access tokens are
|
|
159
|
+
* resolved for every request, refreshed by the host inside the skew window,
|
|
160
|
+
* and force-refreshed once after a replayable 401.
|
|
161
|
+
*/
|
|
162
|
+
export function createMcpAuthenticatedFetch(serverName, config, access = getCredentialAccess(), baseFetch = fetch) {
|
|
163
|
+
return (async (input, init) => {
|
|
164
|
+
const credentialId = config.credentialRef;
|
|
165
|
+
const meta = credentialId ? access.resolveMeta(undefined, credentialId, "full") : undefined;
|
|
166
|
+
const isOAuth = meta?.type === "oauth";
|
|
167
|
+
const headers = isOAuth
|
|
168
|
+
? buildHttpHeaders(serverName, { ...config, credentialRef: undefined })
|
|
169
|
+
: await buildHttpHeadersWithCredentialAccess(serverName, config, access);
|
|
170
|
+
let token;
|
|
171
|
+
if (isOAuth) {
|
|
172
|
+
if (!access.resolveOAuthAccess) {
|
|
173
|
+
throw new Error(`MCP server "${serverName}": oauth credential "${credentialId}" requires host OAuth access`);
|
|
174
|
+
}
|
|
175
|
+
token = (await access.resolveOAuthAccess({ id: credentialId, scope: "full" })).accessToken;
|
|
176
|
+
}
|
|
177
|
+
const source = new Request(input, init);
|
|
178
|
+
let replayable = true;
|
|
179
|
+
try {
|
|
180
|
+
source.clone();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
replayable = false;
|
|
184
|
+
}
|
|
185
|
+
const send = async (accessToken) => {
|
|
186
|
+
const request = replayable ? source.clone() : source;
|
|
187
|
+
const requestHeaders = new Headers(request.headers);
|
|
188
|
+
for (const [name, value] of Object.entries(headers))
|
|
189
|
+
requestHeaders.set(name, value);
|
|
190
|
+
if (accessToken)
|
|
191
|
+
requestHeaders.set("Authorization", `Bearer ${accessToken}`);
|
|
192
|
+
return baseFetch(new Request(request, { headers: requestHeaders }));
|
|
193
|
+
};
|
|
194
|
+
const first = await send(token);
|
|
195
|
+
if (first.status !== 401 || !isOAuth || !credentialId || !replayable || source.signal.aborted) {
|
|
196
|
+
return first;
|
|
197
|
+
}
|
|
198
|
+
const refreshed = await access.resolveOAuthAccess({
|
|
199
|
+
id: credentialId,
|
|
200
|
+
scope: "full",
|
|
201
|
+
forceRefresh: true,
|
|
202
|
+
});
|
|
203
|
+
return send(refreshed.accessToken);
|
|
204
|
+
});
|
|
106
205
|
}
|
|
107
206
|
/**
|
|
108
207
|
* Infer the transport when the config doesn't name one: a url-only entry is
|
|
@@ -137,9 +236,7 @@ const MAX_MCP_IMAGE_SPILLS_PER_TOOL = 50;
|
|
|
137
236
|
const MAX_MCP_IMAGE_BYTES = 8 * 1024 * 1024;
|
|
138
237
|
function spillTimestamp(filename, prefix) {
|
|
139
238
|
const suffix = filename.slice(prefix.length);
|
|
140
|
-
const
|
|
141
|
-
const raw = dot === -1 ? suffix : suffix.slice(0, dot);
|
|
142
|
-
const timestamp = Number(raw);
|
|
239
|
+
const timestamp = Number(/^\d+/.exec(suffix)?.[0]);
|
|
143
240
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
144
241
|
}
|
|
145
242
|
async function pruneMcpImageSpills(baseDir, prefix) {
|
|
@@ -183,7 +280,7 @@ export async function spillMcpImage(serverName, toolName, base64, mimeType, opts
|
|
|
183
280
|
const safeServer = serverName.replace(/[^\w.-]+/g, "_");
|
|
184
281
|
const safeTool = toolName.replace(/[^\w.-]+/g, "_");
|
|
185
282
|
const prefix = `${safeServer}-${safeTool}-`;
|
|
186
|
-
const filename = `${prefix}${now()}.${ext}`;
|
|
283
|
+
const filename = `${prefix}${now()}-${randomUUID().slice(0, 8)}.${ext}`;
|
|
187
284
|
const filePath = join(baseDir, filename);
|
|
188
285
|
try {
|
|
189
286
|
await mkdir(baseDir, { recursive: true });
|
|
@@ -423,9 +520,11 @@ export class MCPManager {
|
|
|
423
520
|
// credentialRef resolves against user-scope credential access. The shared
|
|
424
521
|
// MCPManager pool has no per-session cwd, and MCP-referenced credentials
|
|
425
522
|
// (e.g. a Figma token) remain user-global by design.
|
|
426
|
-
const
|
|
523
|
+
const access = getCredentialAccess();
|
|
524
|
+
const headers = buildHttpHeaders(name, { ...config, credentialRef: undefined });
|
|
427
525
|
transport = new StreamableHTTPClientTransport(new URL(config.url), {
|
|
428
526
|
requestInit: Object.keys(headers).length ? { headers } : undefined,
|
|
527
|
+
fetch: createMcpAuthenticatedFetch(name, config, access),
|
|
429
528
|
});
|
|
430
529
|
}
|
|
431
530
|
else {
|
|
@@ -49,11 +49,30 @@ export interface ClassifyOptions {
|
|
|
49
49
|
/** "read" or "write" — different defaults for sensitive paths. */
|
|
50
50
|
operation: PathOperation;
|
|
51
51
|
}
|
|
52
|
+
export interface FinalWritePathSnapshot {
|
|
53
|
+
resolvedPath: string;
|
|
54
|
+
workspacePath: string;
|
|
55
|
+
insideWorkspace: boolean;
|
|
56
|
+
}
|
|
52
57
|
export type PathApprovalScope = "once" | "session" | "project";
|
|
53
58
|
/** Test seam: clear the in-memory session grants. */
|
|
54
59
|
export declare function _resetSessionPathGrants(): void;
|
|
55
60
|
export declare function openSessionPathApprovals(sessionId: string): void;
|
|
56
61
|
export declare function clearSessionPathApprovals(sessionId: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Capture the concrete target approved by the executor's first path-policy
|
|
64
|
+
* pass. The snapshot is carried on the internal args object via a symbol, so
|
|
65
|
+
* it cannot collide with an LLM-supplied schema property or leak into logs.
|
|
66
|
+
*/
|
|
67
|
+
export declare function attachFinalWritePathSnapshot(args: Record<string, unknown>, filePath: string, workspaceRoot: string): Record<string, unknown>;
|
|
68
|
+
export declare function getFinalWritePathSnapshot(args: Record<string, unknown>, filePath: string, workspaceRoot: string): FinalWritePathSnapshot;
|
|
69
|
+
export declare function revalidateFinalWritePath(filePath: string, workspaceRoot: string, approved: FinalWritePathSnapshot): {
|
|
70
|
+
resolvedPath: string;
|
|
71
|
+
} | {
|
|
72
|
+
error: string;
|
|
73
|
+
};
|
|
74
|
+
/** Open the already-revalidated final path without following its last segment. */
|
|
75
|
+
export declare function writeFileNoFollow(filePath: string, content: string): Promise<void>;
|
|
57
76
|
/**
|
|
58
77
|
* Classify a file path against the workspace + sensitive-path policy.
|
|
59
78
|
*
|
|
@@ -29,10 +29,12 @@
|
|
|
29
29
|
* approval round-trip; it is not an authority to write anywhere on disk.
|
|
30
30
|
* Callers must consult classifyPath before honoring acceptEdits.
|
|
31
31
|
*/
|
|
32
|
-
import { realpathSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, } from "node:fs";
|
|
32
|
+
import { constants, realpathSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, } from "node:fs";
|
|
33
|
+
import { open } from "node:fs/promises";
|
|
33
34
|
import { homedir } from "node:os";
|
|
34
35
|
import { randomUUID } from "node:crypto";
|
|
35
36
|
import { dirname, isAbsolute, join, resolve as resolvePath, sep } from "node:path";
|
|
37
|
+
const FINAL_WRITE_PATH_SNAPSHOT = Symbol("codeshell.finalWritePathSnapshot");
|
|
36
38
|
/**
|
|
37
39
|
* Default sensitive path patterns. These are evaluated AFTER home-expansion
|
|
38
40
|
* and resolution, so a literal "$HOME/.ssh" and a symlink at /tmp/x → ~/.ssh
|
|
@@ -325,6 +327,65 @@ function safeRealpath(p) {
|
|
|
325
327
|
}
|
|
326
328
|
return abs;
|
|
327
329
|
}
|
|
330
|
+
/**
|
|
331
|
+
* Capture the concrete target approved by the executor's first path-policy
|
|
332
|
+
* pass. The snapshot is carried on the internal args object via a symbol, so
|
|
333
|
+
* it cannot collide with an LLM-supplied schema property or leak into logs.
|
|
334
|
+
*/
|
|
335
|
+
export function attachFinalWritePathSnapshot(args, filePath, workspaceRoot) {
|
|
336
|
+
const resolvedPath = safeRealpath(filePath);
|
|
337
|
+
const workspacePath = safeRealpath(workspaceRoot);
|
|
338
|
+
const snapshot = {
|
|
339
|
+
resolvedPath,
|
|
340
|
+
workspacePath,
|
|
341
|
+
insideWorkspace: isInsideDir(resolvedPath, workspacePath),
|
|
342
|
+
};
|
|
343
|
+
return { ...args, [FINAL_WRITE_PATH_SNAPSHOT]: snapshot };
|
|
344
|
+
}
|
|
345
|
+
export function getFinalWritePathSnapshot(args, filePath, workspaceRoot) {
|
|
346
|
+
const carried = args[FINAL_WRITE_PATH_SNAPSHOT];
|
|
347
|
+
if (carried && typeof carried === "object") {
|
|
348
|
+
return carried;
|
|
349
|
+
}
|
|
350
|
+
// Direct tool calls do not pass through ToolExecutor. Preserve that API by
|
|
351
|
+
// taking the baseline at handler entry, while still protecting Edit's
|
|
352
|
+
// read-to-write interval with a second check before its writeFile.
|
|
353
|
+
const resolvedPath = safeRealpath(filePath);
|
|
354
|
+
const workspacePath = safeRealpath(workspaceRoot);
|
|
355
|
+
return {
|
|
356
|
+
resolvedPath,
|
|
357
|
+
workspacePath,
|
|
358
|
+
insideWorkspace: isInsideDir(resolvedPath, workspacePath),
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export function revalidateFinalWritePath(filePath, workspaceRoot, approved) {
|
|
362
|
+
const currentPath = safeRealpath(filePath);
|
|
363
|
+
const currentWorkspace = safeRealpath(workspaceRoot);
|
|
364
|
+
const sameTarget = normPath(currentPath) === normPath(approved.resolvedPath);
|
|
365
|
+
const sameWorkspace = normPath(currentWorkspace) === normPath(approved.workspacePath);
|
|
366
|
+
const crossedWorkspaceBoundary = approved.insideWorkspace && !isInsideDir(currentPath, currentWorkspace);
|
|
367
|
+
if (crossedWorkspaceBoundary || !sameTarget || !sameWorkspace) {
|
|
368
|
+
const reason = crossedWorkspaceBoundary
|
|
369
|
+
? "final write path resolved outside the workspace after approval"
|
|
370
|
+
: "final write path changed after approval";
|
|
371
|
+
return {
|
|
372
|
+
error: `Error: ${reason}; refusing to write. ` +
|
|
373
|
+
`Approved target: ${approved.resolvedPath}. Current target: ${currentPath}`,
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
return { resolvedPath: currentPath };
|
|
377
|
+
}
|
|
378
|
+
/** Open the already-revalidated final path without following its last segment. */
|
|
379
|
+
export async function writeFileNoFollow(filePath, content) {
|
|
380
|
+
const flags = constants.O_WRONLY | constants.O_CREAT | constants.O_TRUNC | constants.O_NOFOLLOW;
|
|
381
|
+
const handle = await open(filePath, flags, 0o666);
|
|
382
|
+
try {
|
|
383
|
+
await handle.writeFile(content, "utf-8");
|
|
384
|
+
}
|
|
385
|
+
finally {
|
|
386
|
+
await handle.close();
|
|
387
|
+
}
|
|
388
|
+
}
|
|
328
389
|
function isInsideDir(child, parent) {
|
|
329
390
|
const c = normPath(child);
|
|
330
391
|
const par = normPath(parent);
|
|
@@ -6,6 +6,42 @@ import { logger as rootPermLogger } from "../logging/logger.js";
|
|
|
6
6
|
export interface ApprovalBackend {
|
|
7
7
|
requestApproval(request: ApprovalRequest): Promise<ApprovalResult>;
|
|
8
8
|
}
|
|
9
|
+
export interface ApprovalRouteTarget {
|
|
10
|
+
connectionId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
generation: number;
|
|
13
|
+
}
|
|
14
|
+
export interface ApprovalConnectionHandler {
|
|
15
|
+
requestApproval(request: ApprovalRequest, target: ApprovalRouteTarget): Promise<ApprovalResult>;
|
|
16
|
+
ownershipLost?(targets: ApprovalRouteTarget[], reason: string): void;
|
|
17
|
+
}
|
|
18
|
+
export type ApprovalRegistrationResult = {
|
|
19
|
+
ok: true;
|
|
20
|
+
target: ApprovalRouteTarget;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
conflict: ApprovalRouteTarget;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Process-local connection owner router. Handlers are connection-scoped while
|
|
27
|
+
* session ownership is an explicit binding with a monotonic generation.
|
|
28
|
+
*/
|
|
29
|
+
export declare class ApprovalRouter {
|
|
30
|
+
private readonly handlers;
|
|
31
|
+
private readonly owners;
|
|
32
|
+
private readonly fallbackConnections;
|
|
33
|
+
private nextGeneration;
|
|
34
|
+
addConnection(connectionId: string, handler: ApprovalConnectionHandler, options?: {
|
|
35
|
+
claimUnownedSessions?: boolean;
|
|
36
|
+
}): () => void;
|
|
37
|
+
register(sessionId: string, connectionId: string): ApprovalRegistrationResult;
|
|
38
|
+
resolve(request: ApprovalRequest): Promise<ApprovalResult> | null;
|
|
39
|
+
matches(target: ApprovalRouteTarget): boolean;
|
|
40
|
+
current(sessionId: string): ApprovalRouteTarget | null;
|
|
41
|
+
release(sessionId: string, connectionId: string, reason?: string): void;
|
|
42
|
+
deregister(connectionId: string, reason?: string): void;
|
|
43
|
+
hasConnections(): boolean;
|
|
44
|
+
}
|
|
9
45
|
export declare class HeadlessApprovalBackend implements ApprovalBackend {
|
|
10
46
|
private readonly mode;
|
|
11
47
|
constructor(mode: "approve-all" | "deny-all" | "approve-read-only");
|
|
@@ -45,18 +81,22 @@ export declare const CLOSED_SESSION_TOMBSTONE_LIMIT = 4096;
|
|
|
45
81
|
* the next time the user opens the project.
|
|
46
82
|
*/
|
|
47
83
|
export declare class InteractiveApprovalBackend implements ApprovalBackend {
|
|
84
|
+
private readonly approvalRouter;
|
|
48
85
|
private sessionStateById;
|
|
49
86
|
private closedSessionIds;
|
|
50
87
|
private promptFn;
|
|
51
88
|
private legacyPromptTurn;
|
|
52
89
|
private legacyContext;
|
|
90
|
+
constructor(approvalRouter?: ApprovalRouter);
|
|
53
91
|
setPromptFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
|
|
92
|
+
clearPromptFn(): void;
|
|
54
93
|
/**
|
|
55
94
|
* Has someone installed a real prompt callback? Engine consults this
|
|
56
95
|
* to decide whether to use the interactive backend (= talk to a UI)
|
|
57
96
|
* or fall back to HeadlessApprovalBackend (= deny-all). Without it,
|
|
58
|
-
*
|
|
59
|
-
*
|
|
97
|
+
* a protocol host registers its connection with ApprovalRouter during boot
|
|
98
|
+
* but before the first session is bound — without this check Engine would
|
|
99
|
+
* still fall through to deny-all because
|
|
60
100
|
* the engine couldn't tell the difference. */
|
|
61
101
|
hasPromptFn(): boolean;
|
|
62
102
|
/** Inject the project root so persistence writes to the right settings file. */
|
|
@@ -107,10 +147,10 @@ export declare function pathRuleArgsPattern(absPath: string, scope: "file" | "di
|
|
|
107
147
|
* JSON round-trip where a persisted RegExp comes back as a string.
|
|
108
148
|
*/
|
|
109
149
|
export declare function ruleMatches(rule: PermissionRule, toolName: string, args: Record<string, unknown>): boolean;
|
|
150
|
+
export declare function getApprovalRouter(): ApprovalRouter;
|
|
110
151
|
export declare function getInteractiveApprovalBackend(): InteractiveApprovalBackend;
|
|
111
152
|
export declare function openInteractiveApprovalSession(sessionId: string): void;
|
|
112
153
|
export declare function clearInteractiveApprovalSession(sessionId: string): void;
|
|
113
|
-
export declare function setInteractiveApprovalFn(fn: (request: ApprovalRequest) => Promise<ApprovalResult>): void;
|
|
114
154
|
type BashSafetyLevel = "safe-read" | "safe-write" | "unsafe" | "dangerous";
|
|
115
155
|
/**
|
|
116
156
|
* True when an otherwise-safe-read shell segment must be downgraded to `unsafe`
|