@cjhyy/code-shell-core 0.7.0 → 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/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 +10 -1
- package/dist/credentials/access.js +76 -2
- package/dist/credentials/index.d.ts +3 -2
- package/dist/credentials/index.js +2 -1
- package/dist/credentials/oauth.d.ts +6 -1
- package/dist/credentials/oauth.js +66 -1
- package/dist/credentials/store.d.ts +1 -1
- package/dist/credentials/store.js +16 -7
- package/dist/credentials/types.d.ts +38 -1
- package/dist/credentials/types.js +16 -1
- package/dist/engine/engine.d.ts +61 -32
- package/dist/engine/engine.js +289 -55
- package/dist/engine/goal.d.ts +5 -3
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/turn-loop.d.ts +18 -0
- package/dist/engine/turn-loop.js +202 -27
- 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 +14 -4
- package/dist/hooks/goal-stop-hook.js +626 -71
- 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/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 +290 -72
- package/dist/protocol/types.d.ts +34 -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 +362 -26
- package/dist/session/transcript.d.ts +30 -1
- package/dist/session/transcript.js +119 -4
- 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 +8 -5
- package/dist/tool-system/builtin/background-jobs.js +15 -11
- 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/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 +45 -2
- 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 +17 -5
- package/dist/tool-system/executor.js +24 -8
- package/dist/tool-system/mcp-manager.d.ts +6 -0
- package/dist/tool-system/mcp-manager.js +55 -5
- 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 +15 -0
- package/package.json +1 -1
|
@@ -22,7 +22,8 @@
|
|
|
22
22
|
* parallel Read results), persist the largest ones first until under.
|
|
23
23
|
*/
|
|
24
24
|
import { mkdirSync, writeFileSync } from "node:fs";
|
|
25
|
-
import {
|
|
25
|
+
import { createHash } from "node:crypto";
|
|
26
|
+
import { dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
26
27
|
import { logger } from "../logging/logger.js";
|
|
27
28
|
// ─── Tunables ───────────────────────────────────────────────────────
|
|
28
29
|
/** Per-result threshold. Larger than this → persist + replace. */
|
|
@@ -77,12 +78,32 @@ function ensureDir(dir) {
|
|
|
77
78
|
}
|
|
78
79
|
}
|
|
79
80
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
81
|
+
* Convert an externally supplied tool_use_id to a bounded internal filename.
|
|
82
|
+
* The original id remains the key in ContentReplacementState/onPersist; it is
|
|
83
|
+
* never used as a path component.
|
|
82
84
|
*/
|
|
85
|
+
export function safeToolResultFilename(toolUseId) {
|
|
86
|
+
if (toolUseId.length > 256) {
|
|
87
|
+
throw new RangeError("tool_use_id exceeds the 256 character limit");
|
|
88
|
+
}
|
|
89
|
+
if (isAbsolute(toolUseId) ||
|
|
90
|
+
win32.isAbsolute(toolUseId) ||
|
|
91
|
+
toolUseId.includes("/") ||
|
|
92
|
+
toolUseId.includes("\\")) {
|
|
93
|
+
throw new Error("tool_use_id must not contain a path");
|
|
94
|
+
}
|
|
95
|
+
return `${createHash("sha256").update(toolUseId, "utf8").digest("hex")}.txt`;
|
|
96
|
+
}
|
|
97
|
+
/** Write content to the contained hashed path. Idempotent by original id. */
|
|
83
98
|
function persistToFile(dir, toolUseId, content) {
|
|
99
|
+
const root = resolve(dir);
|
|
100
|
+
const filename = safeToolResultFilename(toolUseId);
|
|
101
|
+
const filepath = resolve(root, filename);
|
|
102
|
+
const rel = relative(root, filepath);
|
|
103
|
+
if (rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
104
|
+
throw new Error("tool_result path escaped its storage root");
|
|
105
|
+
}
|
|
84
106
|
ensureDir(dir);
|
|
85
|
-
const filepath = join(dir, `${toolUseId}.txt`);
|
|
86
107
|
try {
|
|
87
108
|
// 'wx' = fail if exists. Skipping on collision is correct: same id =
|
|
88
109
|
// same content. Avoids re-writing the same bytes every turn.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Credential, type CredentialType } from "./types.js";
|
|
2
2
|
import type { SettingsScope } from "../settings/manager.js";
|
|
3
3
|
import type { Transport } from "../protocol/transport.js";
|
|
4
4
|
export type CredentialAccessScope = "full" | "project";
|
|
@@ -24,6 +24,15 @@ export interface CredentialAccess {
|
|
|
24
24
|
scope: CredentialAccessScope;
|
|
25
25
|
purpose: "use" | "mcp";
|
|
26
26
|
}): Promise<string>;
|
|
27
|
+
/** Resolve only the bearer material needed by an MCP request. */
|
|
28
|
+
resolveOAuthAccess?(req: {
|
|
29
|
+
id: string;
|
|
30
|
+
scope: "full";
|
|
31
|
+
forceRefresh?: boolean;
|
|
32
|
+
}): Promise<{
|
|
33
|
+
accessToken: string;
|
|
34
|
+
expiresAt?: string;
|
|
35
|
+
}>;
|
|
27
36
|
materializeCookie?(req: {
|
|
28
37
|
cwd?: string;
|
|
29
38
|
id: string;
|
|
@@ -2,9 +2,11 @@ import { existsSync, readdirSync, rmSync, statSync, writeFileSync } from "node:f
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
|
+
import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
|
|
5
6
|
import { CredentialStore } from "./store.js";
|
|
6
7
|
import { formatNetscapeCookies, parseCookieJar } from "./cookie-jar.js";
|
|
7
|
-
import { summarizeOAuthCredentialSecret } from "./oauth.js";
|
|
8
|
+
import { parseOAuthCredentialSecret, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, } from "./oauth.js";
|
|
9
|
+
import { refreshToken } from "../services/oauth.js";
|
|
8
10
|
const COOKIE_FILE_PREFIX = "codeshell-cred-cookie-";
|
|
9
11
|
const COOKIE_FILE_MAX_AGE_MS = 30 * 60 * 1000;
|
|
10
12
|
let defaultCredentialAccess = null;
|
|
@@ -85,6 +87,16 @@ export function createIpcCredentialAccess(transport) {
|
|
|
85
87
|
throw new Error(`credential "${req.id}" is unavailable`);
|
|
86
88
|
return result.value;
|
|
87
89
|
},
|
|
90
|
+
async resolveOAuthAccess(req) {
|
|
91
|
+
const result = (await request("desktop/oauthAccessResolve", req));
|
|
92
|
+
if (typeof result?.accessToken !== "string") {
|
|
93
|
+
throw new Error(`oauth credential "${req.id}" requires login`);
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
accessToken: result.accessToken,
|
|
97
|
+
expiresAt: typeof result.expiresAt === "string" ? result.expiresAt : undefined,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
88
100
|
async materializeCookie(req) {
|
|
89
101
|
const result = (await request("desktop/credentialMaterializeCookie", req));
|
|
90
102
|
if (typeof result?.cookiesFile !== "string" || typeof result.count !== "number") {
|
|
@@ -106,8 +118,9 @@ function toMetadata(cred) {
|
|
|
106
118
|
const { secret: _secret, ...rest } = cred;
|
|
107
119
|
return {
|
|
108
120
|
...rest,
|
|
121
|
+
...(credentialAllowsEnvExposure(cred.type) ? {} : { exposeAsEnv: undefined }),
|
|
109
122
|
hasSecret: available,
|
|
110
|
-
secretHint: available ? (
|
|
123
|
+
secretHint: available ? credentialSecretHint(cred.type, secret) : undefined,
|
|
111
124
|
...(cred.type === "oauth" ? { oauthStatus: summarizeOAuthCredentialSecret(secret) } : {}),
|
|
112
125
|
};
|
|
113
126
|
}
|
|
@@ -129,6 +142,8 @@ export const localCredentialAccess = {
|
|
|
129
142
|
const out = {};
|
|
130
143
|
const creds = storeFor(cwd).list(scope);
|
|
131
144
|
for (const cred of creds) {
|
|
145
|
+
if (!credentialAllowsEnvExposure(cred.type))
|
|
146
|
+
continue;
|
|
132
147
|
const name = cred.exposeAsEnv?.trim();
|
|
133
148
|
if (name && isCredentialSecretAvailable(cred.secret))
|
|
134
149
|
out[name] = cred.secret;
|
|
@@ -142,6 +157,9 @@ export const localCredentialAccess = {
|
|
|
142
157
|
}
|
|
143
158
|
return cred.secret;
|
|
144
159
|
},
|
|
160
|
+
async resolveOAuthAccess(req) {
|
|
161
|
+
return resolveLocalOAuthAccess(req.id, Boolean(req.forceRefresh));
|
|
162
|
+
},
|
|
145
163
|
async materializeCookie(req) {
|
|
146
164
|
const cred = storeFor(req.cwd).resolve(req.id, req.scope);
|
|
147
165
|
if (!cred || cred.type !== "cookie" || !isCredentialSecretAvailable(cred.secret)) {
|
|
@@ -150,6 +168,62 @@ export const localCredentialAccess = {
|
|
|
150
168
|
return materializeCookieSecret(cred.id, cred.secret);
|
|
151
169
|
},
|
|
152
170
|
};
|
|
171
|
+
const localOAuthRefreshes = new Map();
|
|
172
|
+
async function resolveLocalOAuthAccess(id, forceRefresh) {
|
|
173
|
+
const store = new CredentialStore(undefined);
|
|
174
|
+
const cred = store.resolve(id, "full");
|
|
175
|
+
if (!cred || cred.type !== "oauth" || !isCredentialSecretAvailable(cred.secret)) {
|
|
176
|
+
throw new Error(`oauth credential "${id}" requires login`);
|
|
177
|
+
}
|
|
178
|
+
const parsed = parseOAuthCredentialSecret(cred.secret);
|
|
179
|
+
const decision = shouldRefreshOAuthCredential(parsed);
|
|
180
|
+
if (!forceRefresh && decision === "no") {
|
|
181
|
+
return { accessToken: parsed.accessToken, expiresAt: parsed.expiresAt };
|
|
182
|
+
}
|
|
183
|
+
if (!parsed.refreshToken || !parsed.tokenEndpoint || !parsed.clientId) {
|
|
184
|
+
throw new Error(`oauth credential "${id}" requires login`);
|
|
185
|
+
}
|
|
186
|
+
const inflight = localOAuthRefreshes.get(id);
|
|
187
|
+
if (inflight)
|
|
188
|
+
return inflight;
|
|
189
|
+
const pending = (async () => {
|
|
190
|
+
const latestCred = store.resolve(id, "full");
|
|
191
|
+
if (!latestCred || latestCred.type !== "oauth" || !latestCred.secret) {
|
|
192
|
+
throw new Error(`oauth credential "${id}" requires login`);
|
|
193
|
+
}
|
|
194
|
+
const latest = parseOAuthCredentialSecret(latestCred.secret);
|
|
195
|
+
if (!forceRefresh && shouldRefreshOAuthCredential(latest) === "no") {
|
|
196
|
+
return { accessToken: latest.accessToken, expiresAt: latest.expiresAt };
|
|
197
|
+
}
|
|
198
|
+
if (!latest.refreshToken || !latest.tokenEndpoint || !latest.clientId) {
|
|
199
|
+
throw new Error(`oauth credential "${id}" requires login`);
|
|
200
|
+
}
|
|
201
|
+
const tokens = await refreshToken({
|
|
202
|
+
clientId: latest.clientId,
|
|
203
|
+
clientSecret: latest.clientSecret,
|
|
204
|
+
tokenEndpoint: latest.tokenEndpoint,
|
|
205
|
+
authorizationEndpoint: "",
|
|
206
|
+
scopes: latest.scopes,
|
|
207
|
+
resource: latest.resource,
|
|
208
|
+
}, latest.refreshToken);
|
|
209
|
+
const next = {
|
|
210
|
+
...latest,
|
|
211
|
+
accessToken: tokens.accessToken,
|
|
212
|
+
refreshToken: tokens.refreshToken ?? latest.refreshToken,
|
|
213
|
+
expiresAt: tokens.expiresAt ? new Date(tokens.expiresAt).toISOString() : undefined,
|
|
214
|
+
tokenType: tokens.tokenType,
|
|
215
|
+
scope: tokens.scope ?? latest.scope,
|
|
216
|
+
};
|
|
217
|
+
store.save("user", {
|
|
218
|
+
...latestCred,
|
|
219
|
+
secret: JSON.stringify(next),
|
|
220
|
+
meta: { ...latestCred.meta, lastRefreshAt: new Date().toISOString() },
|
|
221
|
+
});
|
|
222
|
+
return { accessToken: next.accessToken, expiresAt: next.expiresAt };
|
|
223
|
+
})().finally(() => localOAuthRefreshes.delete(id));
|
|
224
|
+
localOAuthRefreshes.set(id, pending);
|
|
225
|
+
return pending;
|
|
226
|
+
}
|
|
153
227
|
export function materializeCookieSecret(credentialId, secret) {
|
|
154
228
|
const jar = parseCookieJar(secret);
|
|
155
229
|
if (jar.length === 0)
|
|
@@ -2,8 +2,9 @@ export { CredentialStore } from "./store.js";
|
|
|
2
2
|
export type { CredentialScope, MaskedCredential } from "./store.js";
|
|
3
3
|
export { type EncryptionCipher, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./cipher.js";
|
|
4
4
|
export { getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, type CredentialAccess, type CredentialAccessScope, type CredentialMetadata, type CredentialSnapshot, type CredentialSnapshotEntry, } from "./access.js";
|
|
5
|
-
export type { Credential, CredentialType, CredentialStoreFile, OAuthCredentialPublicStatus, OAuthCredentialSecret, } from "./types.js";
|
|
6
|
-
export {
|
|
5
|
+
export type { Credential, CredentialType, CredentialStoreFile, OAuthCredentialPublicStatus, OAuthCredentialSecret, OAuthTokenResponse, } from "./types.js";
|
|
6
|
+
export { credentialAllowsEnvExposure, credentialSecretHint } from "./types.js";
|
|
7
|
+
export { buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, type OAuthClockOptions, type OAuthRefreshHandler, type OAuthRefreshRequest, } from "./oauth.js";
|
|
7
8
|
export { formatNetscapeCookies, parseCookieJar, type CookieLike } from "./cookie-jar.js";
|
|
8
9
|
export { useCredentialToolDef, useCredentialToolDefFor, useCredentialTool, sweepStaleCredentialCookies, } from "./use-credential-tool.js";
|
|
9
10
|
export { credentialUseGate } from "./use-gate.js";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
export { CredentialStore } from "./store.js";
|
|
2
2
|
export { PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./cipher.js";
|
|
3
3
|
export { getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, } from "./access.js";
|
|
4
|
-
export {
|
|
4
|
+
export { credentialAllowsEnvExposure, credentialSecretHint } from "./types.js";
|
|
5
|
+
export { buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, } from "./oauth.js";
|
|
5
6
|
export { formatNetscapeCookies, parseCookieJar } from "./cookie-jar.js";
|
|
6
7
|
export { useCredentialToolDef, useCredentialToolDefFor, useCredentialTool, sweepStaleCredentialCookies, } from "./use-credential-tool.js";
|
|
7
8
|
export { credentialUseGate } from "./use-gate.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { OAuthCredentialPublicStatus, OAuthCredentialSecret } from "./types.js";
|
|
1
|
+
import type { OAuthCredentialPublicStatus, OAuthCredentialSecret, OAuthTokenResponse } from "./types.js";
|
|
2
2
|
export interface OAuthClockOptions {
|
|
3
3
|
now?: number | (() => number);
|
|
4
4
|
skewMs?: number;
|
|
@@ -14,6 +14,11 @@ export interface OAuthRefreshRequest {
|
|
|
14
14
|
}
|
|
15
15
|
export type OAuthRefreshHandler = (req: OAuthRefreshRequest) => Promise<OAuthCredentialSecret>;
|
|
16
16
|
export declare function parseOAuthCredentialSecret(secret: string): OAuthCredentialSecret;
|
|
17
|
+
/** Merge and validate an OAuth token endpoint response without losing rotation state. */
|
|
18
|
+
export declare function mergeOAuthTokenResponse(previous: OAuthCredentialSecret | undefined, response: OAuthTokenResponse, opts?: {
|
|
19
|
+
now?: number;
|
|
20
|
+
}): OAuthCredentialSecret;
|
|
21
|
+
export declare function shouldRefreshOAuthCredential(secret: OAuthCredentialSecret, opts?: OAuthClockOptions): "no" | "refresh" | "login_required";
|
|
17
22
|
export declare function isOAuthAccessTokenExpired(secret: Pick<OAuthCredentialSecret, "accessToken" | "expiresAt">, opts?: OAuthClockOptions): boolean;
|
|
18
23
|
export declare function oauthCredentialStatus(secret: Pick<OAuthCredentialSecret, "accessToken" | "expiresAt">, opts?: OAuthClockOptions): Pick<OAuthCredentialPublicStatus, "state" | "expiresAt" | "expiresInMs">;
|
|
19
24
|
export declare function buildOAuthRefreshRequest(credentialId: string, secret: OAuthCredentialSecret): OAuthRefreshRequest | undefined;
|
|
@@ -15,6 +15,17 @@ function optionalStringArray(value) {
|
|
|
15
15
|
const out = value.filter((x) => typeof x === "string" && x.length > 0);
|
|
16
16
|
return out.length > 0 ? out : undefined;
|
|
17
17
|
}
|
|
18
|
+
function optionalFiniteNumber(value) {
|
|
19
|
+
if (value === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
22
|
+
throw new Error("OAuth token response expires_in must be a finite non-negative number");
|
|
23
|
+
}
|
|
24
|
+
if (value < 0) {
|
|
25
|
+
throw new Error("OAuth token response expires_in must be a finite non-negative number");
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
18
29
|
function parseExpiresAt(value) {
|
|
19
30
|
if (!value)
|
|
20
31
|
return undefined;
|
|
@@ -45,19 +56,73 @@ export function parseOAuthCredentialSecret(secret) {
|
|
|
45
56
|
}
|
|
46
57
|
const expiresAt = optionalString(raw.expiresAt);
|
|
47
58
|
parseExpiresAt(expiresAt);
|
|
59
|
+
const tokenType = optionalString(raw.tokenType);
|
|
60
|
+
if (tokenType && tokenType.toLowerCase() !== "bearer") {
|
|
61
|
+
throw new Error("OAuth credential tokenType must be Bearer");
|
|
62
|
+
}
|
|
48
63
|
return {
|
|
49
64
|
version: raw.version === 1 ? 1 : undefined,
|
|
50
65
|
accessToken,
|
|
51
66
|
refreshToken: optionalString(raw.refreshToken),
|
|
52
67
|
expiresAt,
|
|
53
|
-
tokenType
|
|
68
|
+
tokenType,
|
|
54
69
|
scope: optionalString(raw.scope),
|
|
55
70
|
scopes: optionalStringArray(raw.scopes),
|
|
56
71
|
tokenEndpoint: optionalString(raw.tokenEndpoint),
|
|
57
72
|
clientId: optionalString(raw.clientId),
|
|
58
73
|
clientSecret: optionalString(raw.clientSecret),
|
|
74
|
+
issuer: optionalString(raw.issuer),
|
|
75
|
+
resource: optionalString(raw.resource),
|
|
76
|
+
revocationEndpoint: optionalString(raw.revocationEndpoint),
|
|
77
|
+
clientRegistration: raw.clientRegistration &&
|
|
78
|
+
typeof raw.clientRegistration === "object" &&
|
|
79
|
+
!Array.isArray(raw.clientRegistration) &&
|
|
80
|
+
optionalString(raw.clientRegistration.clientId)
|
|
81
|
+
? {
|
|
82
|
+
clientId: optionalString(raw.clientRegistration.clientId),
|
|
83
|
+
clientSecret: optionalString(raw.clientRegistration.clientSecret),
|
|
84
|
+
clientIdIssuedAt: typeof raw.clientRegistration.clientIdIssuedAt ===
|
|
85
|
+
"number"
|
|
86
|
+
? raw.clientRegistration.clientIdIssuedAt
|
|
87
|
+
: undefined,
|
|
88
|
+
clientSecretExpiresAt: typeof raw.clientRegistration.clientSecretExpiresAt ===
|
|
89
|
+
"number"
|
|
90
|
+
? raw.clientRegistration
|
|
91
|
+
.clientSecretExpiresAt
|
|
92
|
+
: undefined,
|
|
93
|
+
}
|
|
94
|
+
: undefined,
|
|
59
95
|
};
|
|
60
96
|
}
|
|
97
|
+
/** Merge and validate an OAuth token endpoint response without losing rotation state. */
|
|
98
|
+
export function mergeOAuthTokenResponse(previous, response, opts = {}) {
|
|
99
|
+
const accessToken = optionalString(response.access_token);
|
|
100
|
+
if (!accessToken)
|
|
101
|
+
throw new Error("OAuth token response must include access_token");
|
|
102
|
+
const tokenType = optionalString(response.token_type) ?? "Bearer";
|
|
103
|
+
if (tokenType.toLowerCase() !== "bearer") {
|
|
104
|
+
throw new Error(`OAuth token response token_type must be Bearer`);
|
|
105
|
+
}
|
|
106
|
+
const expiresIn = optionalFiniteNumber(response.expires_in);
|
|
107
|
+
const now = opts.now ?? Date.now();
|
|
108
|
+
const responseScope = optionalString(response.scope);
|
|
109
|
+
const refreshToken = optionalString(response.refresh_token) ?? previous?.refreshToken;
|
|
110
|
+
return {
|
|
111
|
+
...previous,
|
|
112
|
+
version: 1,
|
|
113
|
+
accessToken,
|
|
114
|
+
refreshToken,
|
|
115
|
+
expiresAt: expiresIn === undefined ? undefined : new Date(now + expiresIn * 1000).toISOString(),
|
|
116
|
+
tokenType: "Bearer",
|
|
117
|
+
scope: responseScope ?? previous?.scope,
|
|
118
|
+
scopes: responseScope ? responseScope.split(/\s+/).filter(Boolean) : previous?.scopes,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function shouldRefreshOAuthCredential(secret, opts = {}) {
|
|
122
|
+
if (!isOAuthAccessTokenExpired(secret, opts))
|
|
123
|
+
return "no";
|
|
124
|
+
return secret.refreshToken && secret.tokenEndpoint ? "refresh" : "login_required";
|
|
125
|
+
}
|
|
61
126
|
export function isOAuthAccessTokenExpired(secret, opts = {}) {
|
|
62
127
|
const expiresAtMs = parseExpiresAt(secret.expiresAt);
|
|
63
128
|
if (expiresAtMs === undefined)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Credential } from "./types.js";
|
|
2
2
|
import { type EncryptionCipher } from "./cipher.js";
|
|
3
3
|
export type CredentialScope = "user" | "project";
|
|
4
4
|
export interface MaskedCredential extends Omit<Credential, "secret"> {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync } from "node:fs";
|
|
2
2
|
import { join, dirname } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
+
import { credentialAllowsEnvExposure, credentialSecretHint, } from "./types.js";
|
|
4
5
|
import { getDefaultCredentialCipher } from "./cipher.js";
|
|
5
6
|
import { logger } from "../logging/logger.js";
|
|
6
7
|
import { summarizeOAuthCredentialSecret } from "./oauth.js";
|
|
@@ -93,11 +94,14 @@ export class CredentialStore {
|
|
|
93
94
|
/** Upsert by id within a scope. */
|
|
94
95
|
save(scope, cred) {
|
|
95
96
|
const file = this.read(scope);
|
|
97
|
+
const safeCredential = credentialAllowsEnvExposure(cred.type)
|
|
98
|
+
? cred
|
|
99
|
+
: { ...cred, exposeAsEnv: undefined };
|
|
96
100
|
const idx = file.credentials.findIndex((c) => c.id === cred.id);
|
|
97
101
|
if (idx >= 0)
|
|
98
|
-
file.credentials[idx] =
|
|
102
|
+
file.credentials[idx] = safeCredential;
|
|
99
103
|
else
|
|
100
|
-
file.credentials.push(
|
|
104
|
+
file.credentials.push(safeCredential);
|
|
101
105
|
this.write(scope, file);
|
|
102
106
|
}
|
|
103
107
|
/**
|
|
@@ -110,7 +114,12 @@ export class CredentialStore {
|
|
|
110
114
|
const idx = file.credentials.findIndex((c) => c.id === id);
|
|
111
115
|
if (idx < 0)
|
|
112
116
|
return;
|
|
113
|
-
|
|
117
|
+
const current = file.credentials[idx];
|
|
118
|
+
file.credentials[idx] = {
|
|
119
|
+
...current,
|
|
120
|
+
...fields,
|
|
121
|
+
...(credentialAllowsEnvExposure(current.type) ? {} : { exposeAsEnv: undefined }),
|
|
122
|
+
};
|
|
114
123
|
this.write(scope, file);
|
|
115
124
|
}
|
|
116
125
|
remove(scope, id) {
|
|
@@ -161,6 +170,8 @@ export class CredentialStore {
|
|
|
161
170
|
: [this.read("user").credentials, this.read("project").credentials];
|
|
162
171
|
for (const layer of layers) {
|
|
163
172
|
for (const c of layer) {
|
|
173
|
+
if (!credentialAllowsEnvExposure(c.type))
|
|
174
|
+
continue;
|
|
164
175
|
const name = c.exposeAsEnv?.trim();
|
|
165
176
|
if (name && typeof c.secret === "string" && c.secret.length > 0) {
|
|
166
177
|
out[name] = c.secret;
|
|
@@ -174,11 +185,9 @@ export class CredentialStore {
|
|
|
174
185
|
const { secret, ...rest } = c;
|
|
175
186
|
return {
|
|
176
187
|
...rest,
|
|
188
|
+
...(credentialAllowsEnvExposure(c.type) ? {} : { exposeAsEnv: undefined }),
|
|
177
189
|
hasSecret: typeof secret === "string" && secret.length > 0,
|
|
178
|
-
|
|
179
|
-
// than 4, so those chars aren't the whole secret. `"ab".slice(-4)` is
|
|
180
|
-
// "ab", so a short secret would otherwise leak in full through the hint.
|
|
181
|
-
secretHint: secret ? (secret.length > 4 ? `****${secret.slice(-4)}` : "****") : undefined,
|
|
190
|
+
secretHint: credentialSecretHint(c.type, secret),
|
|
182
191
|
...(c.type === "oauth" ? { oauthStatus: summarizeOAuthCredentialSecret(secret) } : {}),
|
|
183
192
|
};
|
|
184
193
|
});
|
|
@@ -4,6 +4,14 @@
|
|
|
4
4
|
* (用户主动按域拓取存,支持同域多账号),见 credential-use-gate 设计稿。
|
|
5
5
|
*/
|
|
6
6
|
export type CredentialType = "token" | "link" | "cookie" | "oauth";
|
|
7
|
+
/**
|
|
8
|
+
* Only established single-value credentials may be copied into a process
|
|
9
|
+
* environment. OAuth records and cookie jars are structured multi-secret
|
|
10
|
+
* values and must remain behind the credential resolver boundary.
|
|
11
|
+
*/
|
|
12
|
+
export declare function credentialAllowsEnvExposure(type: CredentialType): boolean;
|
|
13
|
+
/** Build a renderer-safe hint without deriving bytes from structured JSON. */
|
|
14
|
+
export declare function credentialSecretHint(type: CredentialType, secret: string | undefined): string | undefined;
|
|
7
15
|
export interface OAuthCredentialSecret {
|
|
8
16
|
/** Schema version for the JSON stored in Credential.secret. */
|
|
9
17
|
version?: 1;
|
|
@@ -25,6 +33,26 @@ export interface OAuthCredentialSecret {
|
|
|
25
33
|
clientId?: string;
|
|
26
34
|
/** Optional confidential client secret, if the integration requires it. */
|
|
27
35
|
clientSecret?: string;
|
|
36
|
+
/** OAuth issuer discovered for the MCP protected resource. */
|
|
37
|
+
issuer?: string;
|
|
38
|
+
/** RFC 8707 resource indicator used for authorization/token requests. */
|
|
39
|
+
resource?: string;
|
|
40
|
+
/** Optional endpoint used to revoke the refresh/access token on logout. */
|
|
41
|
+
revocationEndpoint?: string;
|
|
42
|
+
/** Dynamic registration result, retained with the encrypted secret. */
|
|
43
|
+
clientRegistration?: {
|
|
44
|
+
clientId: string;
|
|
45
|
+
clientSecret?: string;
|
|
46
|
+
clientIdIssuedAt?: number;
|
|
47
|
+
clientSecretExpiresAt?: number;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export interface OAuthTokenResponse {
|
|
51
|
+
access_token: unknown;
|
|
52
|
+
refresh_token?: unknown;
|
|
53
|
+
expires_in?: unknown;
|
|
54
|
+
token_type?: unknown;
|
|
55
|
+
scope?: unknown;
|
|
28
56
|
}
|
|
29
57
|
export interface OAuthCredentialPublicStatus {
|
|
30
58
|
state: "valid" | "expired" | "missing" | "invalid";
|
|
@@ -84,6 +112,11 @@ export interface Credential {
|
|
|
84
112
|
switchMode?: "clear" | "merge";
|
|
85
113
|
/** OAuth provider/integration id, e.g. "figma". */
|
|
86
114
|
oauthProvider?: string;
|
|
115
|
+
/** MCP server identity associated with an advanced OAuth login. */
|
|
116
|
+
mcpServerName?: string;
|
|
117
|
+
mcpServerUrl?: string;
|
|
118
|
+
issuer?: string;
|
|
119
|
+
resource?: string;
|
|
87
120
|
/** OAuth authorization endpoint, used by UI/login launchers. */
|
|
88
121
|
authUrl?: string;
|
|
89
122
|
/** OAuth token endpoint, duplicated from secret for non-secret UI display. */
|
|
@@ -92,8 +125,12 @@ export interface Credential {
|
|
|
92
125
|
clientId?: string;
|
|
93
126
|
/** OAuth scopes requested for this credential. */
|
|
94
127
|
scopes?: string[];
|
|
95
|
-
|
|
128
|
+
revocationEndpoint?: string;
|
|
129
|
+
/** Last successful refresh time. */
|
|
96
130
|
lastRefreshAt?: string;
|
|
131
|
+
/** Normalized refresh diagnostics; never contains endpoint response bodies. */
|
|
132
|
+
lastRefreshFailedAt?: string;
|
|
133
|
+
lastRefreshErrorCode?: "invalid_grant" | "network" | "server_error" | "invalid_response";
|
|
97
134
|
};
|
|
98
135
|
}
|
|
99
136
|
export interface CredentialStoreFile {
|
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Only established single-value credentials may be copied into a process
|
|
3
|
+
* environment. OAuth records and cookie jars are structured multi-secret
|
|
4
|
+
* values and must remain behind the credential resolver boundary.
|
|
5
|
+
*/
|
|
6
|
+
export function credentialAllowsEnvExposure(type) {
|
|
7
|
+
return type === "token" || type === "link";
|
|
8
|
+
}
|
|
9
|
+
/** Build a renderer-safe hint without deriving bytes from structured JSON. */
|
|
10
|
+
export function credentialSecretHint(type, secret) {
|
|
11
|
+
if (!secret)
|
|
12
|
+
return undefined;
|
|
13
|
+
if (type === "oauth" || type === "cookie")
|
|
14
|
+
return "****";
|
|
15
|
+
return secret.length > 4 ? `****${secret.slice(-4)}` : "****";
|
|
16
|
+
}
|
package/dist/engine/engine.d.ts
CHANGED
|
@@ -4,10 +4,11 @@
|
|
|
4
4
|
import type { LLMConfig, StreamCallback } from "../types.js";
|
|
5
5
|
import { ToolRegistry } from "../tool-system/registry.js";
|
|
6
6
|
import { type BuiltinToolFn } from "../tool-system/builtin/index.js";
|
|
7
|
+
import { type ApprovalRouter } from "../tool-system/permission.js";
|
|
7
8
|
import { HookRegistry } from "../hooks/registry.js";
|
|
8
9
|
import { type GoalConfig, type GoalExtension } from "./goal.js";
|
|
9
10
|
import { type CompactStrategy } from "../context/manager.js";
|
|
10
|
-
import { SessionManager } from "../session/session-manager.js";
|
|
11
|
+
import { SessionManager, type ForkSessionOptions, type ForkSessionResult } from "../session/session-manager.js";
|
|
11
12
|
import type { AskUserFn } from "../tool-system/builtin/ask-user.js";
|
|
12
13
|
import type { CapabilityOverride } from "../settings/schema.js";
|
|
13
14
|
import { type FeatureFlagName } from "../settings/feature-flags.js";
|
|
@@ -34,6 +35,38 @@ export interface EnqueueSteerResult {
|
|
|
34
35
|
accepted: boolean;
|
|
35
36
|
id: string;
|
|
36
37
|
}
|
|
38
|
+
interface EngineRunOptions {
|
|
39
|
+
cwd?: string;
|
|
40
|
+
onStream?: StreamCallback;
|
|
41
|
+
signal?: AbortSignal;
|
|
42
|
+
sessionId?: string;
|
|
43
|
+
/** Immutable permission context for this run only. Omitted uses Engine config. */
|
|
44
|
+
permissionMode?: NonNullable<EngineConfig["permissionMode"]>;
|
|
45
|
+
/** Immutable plan context for this run only. Omitted follows permissionMode. */
|
|
46
|
+
planMode?: boolean;
|
|
47
|
+
/** Protocol connection's approval owner router for this run. */
|
|
48
|
+
approvalRouter?: ApprovalRouter;
|
|
49
|
+
/**
|
|
50
|
+
* Goal mode for this run: the engine registers a GoalStopHook so the
|
|
51
|
+
* turn loop runs until the session model judges this goal met. Falls
|
|
52
|
+
* back to config.goal. Orthogonal to permissionMode. Accepts a raw
|
|
53
|
+
* string or a full GoalConfig; normalized once at the run boundary.
|
|
54
|
+
*/
|
|
55
|
+
goal?: string | GoalConfig;
|
|
56
|
+
/**
|
|
57
|
+
* Marks this turn's task as a SYNTHETIC system-reminder injection (e.g. a
|
|
58
|
+
* background-job completion notification the server re-injects to wake an
|
|
59
|
+
* idle session) rather than the user's own input. The task is still sent
|
|
60
|
+
* to the model as a `role:"user"` message, but it is persisted with an
|
|
61
|
+
* `injected` flag so the disk reader skips rendering it as a user bubble
|
|
62
|
+
* on replay (matching the live UI, which shows only the assistant reply).
|
|
63
|
+
*/
|
|
64
|
+
injected?: boolean;
|
|
65
|
+
/** Stable id for this user-intent, used to make duplicate submits idempotent. */
|
|
66
|
+
clientMessageId?: string;
|
|
67
|
+
/** Structured input attachments. Legacy `<codeshell-image>` blocks remain supported. */
|
|
68
|
+
attachments?: InputAttachmentMeta[];
|
|
69
|
+
}
|
|
37
70
|
export { diskDefaultsFrom, type DiskDefaultPatch } from "../settings/disk-defaults.js";
|
|
38
71
|
/**
|
|
39
72
|
* Resolve the LLM config for a spawned child Engine.
|
|
@@ -122,6 +155,8 @@ export declare class Engine {
|
|
|
122
155
|
/** Shared resources supplied at construction (adapter pattern — null when self-constructed). */
|
|
123
156
|
readonly runtime: EngineRuntime | null;
|
|
124
157
|
private readonly sandboxCache;
|
|
158
|
+
private readonly interactiveBackends;
|
|
159
|
+
private activeApprovalRouter;
|
|
125
160
|
/** Active permission mode for this Engine instance. */
|
|
126
161
|
permissionMode: NonNullable<EngineConfig["permissionMode"]>;
|
|
127
162
|
/** True when permissionMode === "plan". */
|
|
@@ -188,6 +223,19 @@ export declare class Engine {
|
|
|
188
223
|
* Null when idle; set at run start, cleared in run's finally.
|
|
189
224
|
*/
|
|
190
225
|
private activeRunSession;
|
|
226
|
+
/**
|
|
227
|
+
* Same-instance run guard. Engine owns single-valued live controls and one
|
|
228
|
+
* HookRegistry, so a second run must not enter until the first has completed
|
|
229
|
+
* all state persistence and end hooks. This prevents handle contamination and
|
|
230
|
+
* whole-state saveState overlap only within this Engine instance. It does not
|
|
231
|
+
* coordinate different Engine instances sharing a sessionId, Workers, or
|
|
232
|
+
* processes; session-level locking/CAS for those cases is a separate finding.
|
|
233
|
+
*/
|
|
234
|
+
private runInProgress;
|
|
235
|
+
/** Permission update requested while runInProgress. Applied in run() finally. */
|
|
236
|
+
private pendingPermissionMode;
|
|
237
|
+
/** Plan update paired with pendingPermissionMode for one atomic boundary apply. */
|
|
238
|
+
private pendingPlanMode;
|
|
191
239
|
/** Public accessor so UI/clients can read the resolved per-model window. */
|
|
192
240
|
get maxContextTokens(): number;
|
|
193
241
|
private resolveMaxContextTokens;
|
|
@@ -306,35 +354,14 @@ export declare class Engine {
|
|
|
306
354
|
* against a blank session. A stat probe, not a load.
|
|
307
355
|
*/
|
|
308
356
|
sessionExistsOnDisk(sessionId: string): boolean;
|
|
357
|
+
forkSession(sourceSessionId: string, options?: ForkSessionOptions): ForkSessionResult;
|
|
309
358
|
/**
|
|
310
|
-
* Run a task from start to finish.
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
sessionId?: string;
|
|
317
|
-
/**
|
|
318
|
-
* Goal mode for this run: the engine registers a GoalStopHook so the
|
|
319
|
-
* turn loop runs until the session model judges this goal met. Falls
|
|
320
|
-
* back to config.goal. Orthogonal to permissionMode. Accepts a raw
|
|
321
|
-
* string or a full GoalConfig; normalized once at the run boundary.
|
|
322
|
-
*/
|
|
323
|
-
goal?: string | GoalConfig;
|
|
324
|
-
/**
|
|
325
|
-
* Marks this turn's task as a SYNTHETIC system-reminder injection (e.g. a
|
|
326
|
-
* background-job completion notification the server re-injects to wake an
|
|
327
|
-
* idle session) rather than the user's own input. The task is still sent
|
|
328
|
-
* to the model as a `role:"user"` message, but it is persisted with an
|
|
329
|
-
* `injected` flag so the disk reader skips rendering it as a user bubble
|
|
330
|
-
* on replay (matching the live UI, which shows only the assistant reply).
|
|
331
|
-
*/
|
|
332
|
-
injected?: boolean;
|
|
333
|
-
/** Stable id for this user-intent, used to make duplicate submits idempotent. */
|
|
334
|
-
clientMessageId?: string;
|
|
335
|
-
/** Structured input attachments. Legacy `<codeshell-image>` blocks remain supported. */
|
|
336
|
-
attachments?: InputAttachmentMeta[];
|
|
337
|
-
}): Promise<EngineResult>;
|
|
359
|
+
* Run a task from start to finish. Rejects immediately when this Engine
|
|
360
|
+
* instance already has a run in progress; hosts that want queueing own that
|
|
361
|
+
* policy (for example ChatSession's FIFO queue).
|
|
362
|
+
*/
|
|
363
|
+
run(task: string, options?: EngineRunOptions): Promise<EngineResult>;
|
|
364
|
+
private runExclusive;
|
|
338
365
|
/**
|
|
339
366
|
* Run the end-of-session memory pipeline as a fire-and-forget background
|
|
340
367
|
* task. Extracts durable memories from the transcript, saves a session
|
|
@@ -499,12 +526,14 @@ export declare class Engine {
|
|
|
499
526
|
readSetting(key: string): unknown;
|
|
500
527
|
private buildPermissionConfig;
|
|
501
528
|
/**
|
|
502
|
-
* Switch permission mode at runtime.
|
|
503
|
-
*
|
|
504
|
-
* and
|
|
529
|
+
* Switch permission mode at runtime. Idle updates apply immediately; busy
|
|
530
|
+
* updates are committed atomically when the current run settles, so its
|
|
531
|
+
* classifier and ToolContext retain the immutable start-of-run snapshot.
|
|
505
532
|
* Session-only — does not persist to settings.
|
|
506
533
|
*/
|
|
507
534
|
setPermissionMode(mode: NonNullable<EngineConfig["permissionMode"]>): void;
|
|
535
|
+
private applyPermissionState;
|
|
536
|
+
private applyPendingPermissionState;
|
|
508
537
|
getPermissionMode(): NonNullable<EngineConfig["permissionMode"]>;
|
|
509
538
|
/**
|
|
510
539
|
* Extend the in-flight run's turn ceiling and/or goal budgets (TODO 3.1 —
|