@nexrall/code-core 1.4.76 → 1.4.78
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/agent/effortCap.d.ts +24 -0
- package/dist/agent/effortCap.d.ts.map +1 -0
- package/dist/agent/effortCap.js +66 -0
- package/dist/agent/goal.d.ts +55 -0
- package/dist/agent/goal.d.ts.map +1 -0
- package/dist/agent/goal.js +120 -0
- package/dist/agent/handoffBrief.d.ts +82 -0
- package/dist/agent/handoffBrief.d.ts.map +1 -0
- package/dist/agent/handoffBrief.js +121 -0
- package/dist/agent/hooks.d.ts +10 -2
- package/dist/agent/hooks.d.ts.map +1 -1
- package/dist/agent/hooks.js +9 -3
- package/dist/agent/modelCatalogue.d.ts +23 -0
- package/dist/agent/modelCatalogue.d.ts.map +1 -1
- package/dist/agent/modelCatalogue.js +28 -0
- package/dist/agent/outputStyles.d.ts +27 -0
- package/dist/agent/outputStyles.d.ts.map +1 -0
- package/dist/agent/outputStyles.js +174 -0
- package/dist/agent/skills.d.ts.map +1 -1
- package/dist/agent/skills.js +21 -1
- package/dist/api/billingLines.d.ts +11 -0
- package/dist/api/billingLines.d.ts.map +1 -0
- package/dist/api/billingLines.js +56 -0
- package/dist/api/client.d.ts +2 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/scheduleLines.d.ts +53 -0
- package/dist/api/scheduleLines.d.ts.map +1 -0
- package/dist/api/scheduleLines.js +150 -0
- package/dist/context/contextFiles.d.ts +81 -0
- package/dist/context/contextFiles.d.ts.map +1 -0
- package/dist/context/contextFiles.js +310 -0
- package/dist/diff/collect.d.ts +25 -0
- package/dist/diff/collect.d.ts.map +1 -0
- package/dist/diff/collect.js +169 -0
- package/dist/diff/diffView.d.ts +57 -0
- package/dist/diff/diffView.d.ts.map +1 -0
- package/dist/diff/diffView.js +231 -0
- package/dist/diff/reviewQueue.d.ts +53 -0
- package/dist/diff/reviewQueue.d.ts.map +1 -0
- package/dist/diff/reviewQueue.js +349 -0
- package/dist/events/recap.d.ts +54 -0
- package/dist/events/recap.d.ts.map +1 -0
- package/dist/events/recap.js +161 -0
- package/dist/events/sessionEvents.d.ts +54 -0
- package/dist/events/sessionEvents.d.ts.map +1 -0
- package/dist/events/sessionEvents.js +324 -0
- package/dist/events/sessionStats.d.ts +89 -0
- package/dist/events/sessionStats.d.ts.map +1 -0
- package/dist/events/sessionStats.js +264 -0
- package/dist/import/claudeImport.d.ts +44 -0
- package/dist/import/claudeImport.d.ts.map +1 -0
- package/dist/import/claudeImport.js +337 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +38 -0
- package/dist/mcp/manager.d.ts +8 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +64 -57
- package/dist/permissions/destructiveTokens.d.ts +1 -0
- package/dist/permissions/destructiveTokens.d.ts.map +1 -1
- package/dist/permissions/destructiveTokens.js +93 -0
- package/dist/permissions/rules.d.ts +12 -0
- package/dist/permissions/rules.d.ts.map +1 -1
- package/dist/permissions/rules.js +89 -7
- package/dist/plugins/configStore.d.ts +43 -0
- package/dist/plugins/configStore.d.ts.map +1 -0
- package/dist/plugins/configStore.js +253 -0
- package/dist/plugins/files.d.ts +10 -0
- package/dist/plugins/files.d.ts.map +1 -0
- package/dist/plugins/files.js +85 -0
- package/dist/plugins/index.d.ts +3 -22
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +30 -40
- package/dist/plugins/resolve.d.ts +18 -0
- package/dist/plugins/resolve.d.ts.map +1 -0
- package/dist/plugins/resolve.js +152 -0
- package/dist/plugins/userConfig.d.ts +90 -0
- package/dist/plugins/userConfig.d.ts.map +1 -0
- package/dist/plugins/userConfig.js +332 -0
- package/dist/plugins/validate.d.ts +14 -0
- package/dist/plugins/validate.d.ts.map +1 -0
- package/dist/plugins/validate.js +222 -0
- package/dist/project/projectCommands.d.ts +33 -0
- package/dist/project/projectCommands.d.ts.map +1 -0
- package/dist/project/projectCommands.js +246 -0
- package/dist/search/conversationTurns.d.ts +53 -0
- package/dist/search/conversationTurns.d.ts.map +1 -0
- package/dist/search/conversationTurns.js +170 -0
- package/dist/search/globalSearch.d.ts +144 -0
- package/dist/search/globalSearch.d.ts.map +1 -0
- package/dist/search/globalSearch.js +350 -0
- package/dist/share/sessionChannel.d.ts +136 -0
- package/dist/share/sessionChannel.d.ts.map +1 -0
- package/dist/share/sessionChannel.js +373 -0
- package/dist/share/sessionShare.d.ts +104 -0
- package/dist/share/sessionShare.d.ts.map +1 -0
- package/dist/share/sessionShare.js +336 -0
- package/dist/ui/configCmd.d.ts +48 -0
- package/dist/ui/configCmd.d.ts.map +1 -0
- package/dist/ui/configCmd.js +146 -0
- package/dist/ui/deployPanel.d.ts +91 -0
- package/dist/ui/deployPanel.d.ts.map +1 -0
- package/dist/ui/deployPanel.js +442 -0
- package/dist/ui/mergeAssistant.d.ts +120 -0
- package/dist/ui/mergeAssistant.d.ts.map +1 -0
- package/dist/ui/mergeAssistant.js +391 -0
- package/dist/ui/notify.d.ts +114 -0
- package/dist/ui/notify.d.ts.map +1 -0
- package/dist/ui/notify.js +235 -0
- package/dist/ui/promptSuggest.d.ts +56 -0
- package/dist/ui/promptSuggest.d.ts.map +1 -0
- package/dist/ui/promptSuggest.js +160 -0
- package/dist/ui/taskGraph.d.ts +44 -0
- package/dist/ui/taskGraph.d.ts.map +1 -0
- package/dist/ui/taskGraph.js +289 -0
- package/dist/util/addDir.d.ts +31 -0
- package/dist/util/addDir.d.ts.map +1 -0
- package/dist/util/addDir.js +124 -0
- package/dist/util/batch.d.ts +16 -0
- package/dist/util/batch.d.ts.map +1 -0
- package/dist/util/batch.js +92 -0
- package/dist/util/cellWidth.d.ts +12 -0
- package/dist/util/cellWidth.d.ts.map +1 -0
- package/dist/util/cellWidth.js +161 -0
- package/dist/util/duration.d.ts +8 -0
- package/dist/util/duration.d.ts.map +1 -0
- package/dist/util/duration.js +50 -0
- package/dist/util/loopJobs.d.ts +67 -0
- package/dist/util/loopJobs.d.ts.map +1 -0
- package/dist/util/loopJobs.js +169 -0
- package/dist/util/settingsFile.d.ts +14 -0
- package/dist/util/settingsFile.d.ts.map +1 -0
- package/dist/util/settingsFile.js +99 -0
- package/dist/util/toolLabel.d.ts +2 -0
- package/dist/util/toolLabel.d.ts.map +1 -0
- package/dist/util/toolLabel.js +17 -0
- package/dist/verify/failureTimeline.d.ts +100 -0
- package/dist/verify/failureTimeline.d.ts.map +1 -0
- package/dist/verify/failureTimeline.js +253 -0
- package/dist/verify/verification.d.ts +75 -0
- package/dist/verify/verification.d.ts.map +1 -0
- package/dist/verify/verification.js +315 -0
- package/package.json +5 -4
package/dist/mcp/manager.js
CHANGED
|
@@ -41,6 +41,7 @@ const client_1 = require("./client");
|
|
|
41
41
|
const httpClient_1 = require("./httpClient");
|
|
42
42
|
const sseClient_1 = require("./sseClient");
|
|
43
43
|
const index_1 = require("../plugins/index");
|
|
44
|
+
const resolve_1 = require("../plugins/resolve");
|
|
44
45
|
const stats_1 = require("./stats");
|
|
45
46
|
// ─── McpManager ───────────────────────────────────────────────────────────────
|
|
46
47
|
class McpManager {
|
|
@@ -119,7 +120,8 @@ class McpManager {
|
|
|
119
120
|
// Plugins first, then user, then project — later assignments override, so
|
|
120
121
|
// a user/project server definition beats a plugin's same-name one.
|
|
121
122
|
try {
|
|
122
|
-
|
|
123
|
+
// resolvePluginMcpServers = pluginMcpServers + `${user_config.KEY}` substitution (S24).
|
|
124
|
+
Object.assign(merged.mcpServers, (0, resolve_1.resolvePluginMcpServers)(workDir));
|
|
123
125
|
}
|
|
124
126
|
catch {
|
|
125
127
|
/* plugin scan is best-effort */
|
|
@@ -196,23 +198,6 @@ class McpManager {
|
|
|
196
198
|
}
|
|
197
199
|
return tools;
|
|
198
200
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Execute an MCP tool call. Returns null if this tool is not an MCP tool.
|
|
201
|
-
*
|
|
202
|
-
* `abortSignal` is optional and best-effort: the underlying transports (stdio
|
|
203
|
-
* JSON-RPC / HTTP / SSE) have no cancellation message in this client, so an
|
|
204
|
-
* abort here does NOT stop the server from finishing its work — it only stops
|
|
205
|
-
* THIS CALL from making the caller wait for it. Before this was added, Ctrl+C
|
|
206
|
-
* during an MCP tool call was silently swallowed: loop.ts set
|
|
207
|
-
* `abortSignal.aborted = true` but nothing downstream of `options.mcpManager
|
|
208
|
-
* .callTool(name, input)` ever looked at it, so the user's only way out was
|
|
209
|
-
* to wait out the full 60s MCP_TOOL_TIMEOUT_MS (or however long the server
|
|
210
|
-
* actually took). Racing the same abortSignal the 60s timeout already races
|
|
211
|
-
* against makes Ctrl+C return control immediately instead of up to a minute
|
|
212
|
-
* later — matching how the `bash` tool already behaves (its own 200ms abort
|
|
213
|
-
* poll in executor.ts), just without the ability to also kill a remote
|
|
214
|
-
* server's in-flight work the way `bash` can kill a local child process.
|
|
215
|
-
*/
|
|
216
201
|
async callTool(prefixedName, input, abortSignal) {
|
|
217
202
|
const serverName = this._toolMap.get(prefixedName);
|
|
218
203
|
if (!serverName)
|
|
@@ -222,48 +207,70 @@ class McpManager {
|
|
|
222
207
|
return null;
|
|
223
208
|
// Strip the server prefix to get the real tool name
|
|
224
209
|
const toolName = prefixedName.slice(serverName.length + 2);
|
|
225
|
-
//
|
|
210
|
+
// One attempt's full race — a fresh timeout, abort poll and stat pairing per try,
|
|
211
|
+
// because a promise cannot be awaited twice and S25e's retry below builds new ones.
|
|
226
212
|
const MCP_TOOL_TIMEOUT_MS = 60000;
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const racers = [timed(client.callTool(toolName, input)), timed(timeoutPromise)];
|
|
250
|
-
if (abortSignal) {
|
|
251
|
-
let pollId;
|
|
252
|
-
const abortPromise = new Promise((_, reject) => {
|
|
253
|
-
pollId = setInterval(() => {
|
|
254
|
-
if (abortSignal.aborted) {
|
|
255
|
-
clearInterval(pollId);
|
|
256
|
-
reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
|
|
257
|
-
}
|
|
258
|
-
}, 100);
|
|
213
|
+
const once = () => {
|
|
214
|
+
const timeoutPromise = new Promise((_, reject) => setTimeout(() => reject(new Error(`MCP tool "${prefixedName}" timed out after ${MCP_TOOL_TIMEOUT_MS / 1000}s`)), MCP_TOOL_TIMEOUT_MS));
|
|
215
|
+
// Timing covers the whole path the user waits on (server round-trip AND the
|
|
216
|
+
// race against the 60s timeout / abort), which is what "how long did that take"
|
|
217
|
+
// means from the outside. The tool name recorded is the PREFIXED one, so the
|
|
218
|
+
// stats of two servers with a same-named tool never merge.
|
|
219
|
+
const startedAt = Date.now();
|
|
220
|
+
this.stats.begin();
|
|
221
|
+
const timed = (p) => p.then((v) => {
|
|
222
|
+
this.stats.ok(prefixedName, Date.now() - startedAt);
|
|
223
|
+
return v;
|
|
224
|
+
}, (e) => {
|
|
225
|
+
this.stats.fail(prefixedName, Date.now() - startedAt, e.message || String(e));
|
|
226
|
+
throw e;
|
|
227
|
+
});
|
|
228
|
+
// A 401 mid-CALL is different from one at connect: the server is already up and
|
|
229
|
+
// some tools work — record it so /mcp flips to needs-auth (with the missing
|
|
230
|
+
// scope) and let the enriched message reach the caller (the model sees WHY).
|
|
231
|
+
const trackAuth = (p) => p.catch((err) => {
|
|
232
|
+
if (err instanceof httpClient_1.McpOAuthRequiredError)
|
|
233
|
+
this._needsAuth.set(serverName, err.wwwAuthenticate);
|
|
234
|
+
throw err;
|
|
259
235
|
});
|
|
260
|
-
racers.
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
236
|
+
const racers = [timed(client.callTool(toolName, input)), timed(timeoutPromise)];
|
|
237
|
+
if (abortSignal) {
|
|
238
|
+
let pollId;
|
|
239
|
+
const abortPromise = new Promise((_, reject) => {
|
|
240
|
+
pollId = setInterval(() => {
|
|
241
|
+
if (abortSignal.aborted) {
|
|
242
|
+
clearInterval(pollId);
|
|
243
|
+
reject(Object.assign(new Error('Aborted'), { name: 'AbortError' }));
|
|
244
|
+
}
|
|
245
|
+
}, 100);
|
|
246
|
+
});
|
|
247
|
+
racers.push(timed(abortPromise));
|
|
248
|
+
// Clearing the poll on whichever branch wins avoids leaving a live interval
|
|
249
|
+
// (and the closure it holds) running for the rest of the process lifetime —
|
|
250
|
+
// the same leak-prevention pattern client.ts uses for its own abort poll.
|
|
251
|
+
return trackAuth(Promise.race(racers).finally(() => clearInterval(pollId)));
|
|
252
|
+
}
|
|
253
|
+
return trackAuth(Promise.race(racers));
|
|
254
|
+
};
|
|
255
|
+
try {
|
|
256
|
+
return await once();
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
const hook = this.onAuthRequired;
|
|
260
|
+
if (!hook || !(err instanceof httpClient_1.McpOAuthRequiredError))
|
|
261
|
+
throw err;
|
|
262
|
+
let retry;
|
|
263
|
+
try {
|
|
264
|
+
retry = await hook(serverName, err.missingScope ?? '');
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
retry = false; // a broken host hook must not mask the OAuth error
|
|
268
|
+
}
|
|
269
|
+
if (!retry)
|
|
270
|
+
throw err;
|
|
271
|
+
// The host re-authorized (fresh tokens are in): one retry, no loop.
|
|
272
|
+
return await once();
|
|
265
273
|
}
|
|
266
|
-
return trackAuth(Promise.race(racers));
|
|
267
274
|
}
|
|
268
275
|
/** Resources across connected, enabled servers. One server failing never hides the others. */
|
|
269
276
|
async listResources(server) {
|
|
@@ -32,4 +32,5 @@ export interface TokenMatch {
|
|
|
32
32
|
/** Split `src` into simple commands, each a list of unquoted words. */
|
|
33
33
|
export declare function tokenize(src: string): string[][];
|
|
34
34
|
export declare function classifyTokens(tokens: string[], recurse: (command: string) => TokenMatch | null, rawSql: (command: string) => TokenMatch | null): TokenMatch | null;
|
|
35
|
+
export declare function unwrappedViews(command: string): string[];
|
|
35
36
|
//# sourceMappingURL=destructiveTokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA6DhD;AAsVD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CA+HnB"}
|
|
1
|
+
{"version":3,"file":"destructiveTokens.d.ts","sourceRoot":"","sources":["../../src/permissions/destructiveTokens.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAID,uEAAuE;AACvE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,CA6DhD;AAsVD,wBAAgB,cAAc,CAC5B,MAAM,EAAE,MAAM,EAAE,EAChB,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,EAC/C,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,UAAU,GAAG,IAAI,GAC7C,UAAU,GAAG,IAAI,CA+HnB;AAyBD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAKxD"}
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
24
|
exports.tokenize = tokenize;
|
|
25
25
|
exports.classifyTokens = classifyTokens;
|
|
26
|
+
exports.unwrappedViews = unwrappedViews;
|
|
26
27
|
const hit = (category, reason) => ({ category, reason });
|
|
27
28
|
/** Split `src` into simple commands, each a list of unquoted words. */
|
|
28
29
|
function tokenize(src) {
|
|
@@ -552,4 +553,96 @@ function classifyTokens(tokens, recurse, rawSql) {
|
|
|
552
553
|
}
|
|
553
554
|
return null;
|
|
554
555
|
}
|
|
556
|
+
// ─── Every spelling of the command, for the permission-rule matcher ─────────
|
|
557
|
+
//
|
|
558
|
+
// A deny/ask rule must not be skippable by dressing the command up. Claude Code
|
|
559
|
+
// fixed exactly this class (2.1.288/289): "a deny or ask rule … missing a
|
|
560
|
+
// command behind an environment variable prefix with an expanded value (e.g.
|
|
561
|
+
// `TZ="$HOME" rm -rf build`)" and "a bare variable assignment came before the
|
|
562
|
+
// command". The destructive guard has always seen through these (tokenize +
|
|
563
|
+
// classifyTokens above); the RULE matcher had not, so `permissions.deny:
|
|
564
|
+
// ["Bash(rm:*)"]` silently did nothing against `FOO=bar rm -rf build`.
|
|
565
|
+
//
|
|
566
|
+
// unwrappedViews returns every "command actually run" spelling hiding in one
|
|
567
|
+
// command line, using this module's tokenizer and the same wrapper tables as
|
|
568
|
+
// classifyTokens, so anything the guard can see, a rule can see too:
|
|
569
|
+
//
|
|
570
|
+
// 'FOO=bar sudo rm -rf build' → 'sudo rm -rf build', 'rm -rf build'
|
|
571
|
+
// 'cd /tmp && FOO=1 rm -rf x' → 'cd /tmp', 'rm -rf x'
|
|
572
|
+
// 'echo $(rm -rf build)' → 'echo $', 'rm -rf build'
|
|
573
|
+
//
|
|
574
|
+
// Deliberately NOT unwrapped (same as classifyTokens's guard scope):
|
|
575
|
+
// ssh / docker exec / kubectl exec — reaching into those needs the remote or
|
|
576
|
+
// container branch, tracked separately.
|
|
577
|
+
//
|
|
578
|
+
// Order: most literal first (wrappers still attached), then each peeled form.
|
|
579
|
+
function unwrappedViews(command) {
|
|
580
|
+
if (typeof command !== 'string' || command.length === 0)
|
|
581
|
+
return [];
|
|
582
|
+
const out = new Set();
|
|
583
|
+
collectViews(command, out, 0);
|
|
584
|
+
return [...out];
|
|
585
|
+
}
|
|
586
|
+
/** Flags of a peelable wrapper that consume a following value (mirror of classifyTokens). */
|
|
587
|
+
const WRAPPER_VALUE_FLAGS = {
|
|
588
|
+
sudo: new Set(['-u', '-g', '-h', '-p', '-C', '-D', '-R', '-T', '-U']),
|
|
589
|
+
doas: new Set(['-u', '-g', '-h', '-p', '-C', '-D', '-R', '-T', '-U']),
|
|
590
|
+
env: new Set(['-u', '-C', '-S']),
|
|
591
|
+
nice: new Set(['-n']),
|
|
592
|
+
ionice: new Set(['-c', '-n', '-p']),
|
|
593
|
+
command: new Set(),
|
|
594
|
+
builtin: new Set(),
|
|
595
|
+
nohup: new Set(),
|
|
596
|
+
exec: new Set(),
|
|
597
|
+
time: new Set(),
|
|
598
|
+
setsid: new Set(),
|
|
599
|
+
caffeinate: new Set(),
|
|
600
|
+
stdbuf: new Set(),
|
|
601
|
+
unbuffer: new Set(),
|
|
602
|
+
};
|
|
603
|
+
/** `command -v rm` (or `-V`, `-pv`) only PRINTS where rm is — never peel it into `rm`. */
|
|
604
|
+
const LOOKUP_FLAG = /^-p?[vV]+$/;
|
|
605
|
+
function collectViews(src, out, depth) {
|
|
606
|
+
if (depth > 3)
|
|
607
|
+
return;
|
|
608
|
+
for (const tokens of tokenize(src)) {
|
|
609
|
+
let t = tokens;
|
|
610
|
+
for (let guard = 0; guard < 12; guard++) {
|
|
611
|
+
while (t.length && (ENV_ASSIGN.test(t[0]) || CONTROL_WORDS.has(t[0])))
|
|
612
|
+
t = t.slice(1);
|
|
613
|
+
if (t.length === 0)
|
|
614
|
+
break;
|
|
615
|
+
out.add(t.join(' '));
|
|
616
|
+
const bin = base(t[0]);
|
|
617
|
+
const args = t.slice(1);
|
|
618
|
+
// bash -c "…" / eval "…" carry the command as a STRING — read it too.
|
|
619
|
+
if (SHELLS.has(bin)) {
|
|
620
|
+
const i = args.findIndex((a) => /^-[A-Za-z]*c[A-Za-z]*$/.test(a));
|
|
621
|
+
if (i >= 0 && args[i + 1] !== undefined)
|
|
622
|
+
collectViews(args[i + 1], out, depth + 1);
|
|
623
|
+
break;
|
|
624
|
+
}
|
|
625
|
+
if (bin === 'eval') {
|
|
626
|
+
collectViews(args.join(' '), out, depth + 1);
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
if (bin === 'timeout') {
|
|
630
|
+
t = dropOpts(args, new Set(['-s', '-k'])).slice(1); // timeout DURATION cmd
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (bin === 'xargs') {
|
|
634
|
+
t = dropOpts(args, new Set(['-I', '-n', '-P', '-L', '-s', '-d', '-E', '-a', '-J']));
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if ((bin === 'command' || bin === 'builtin') && args.some((a) => LOOKUP_FLAG.test(a)))
|
|
638
|
+
break;
|
|
639
|
+
const valueFlags = WRAPPER_VALUE_FLAGS[bin];
|
|
640
|
+
if (valueFlags) {
|
|
641
|
+
t = dropOpts(args, valueFlags);
|
|
642
|
+
continue;
|
|
643
|
+
}
|
|
644
|
+
break;
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
}
|
|
555
648
|
//# sourceMappingURL=destructiveTokens.js.map
|
|
@@ -15,6 +15,18 @@ export declare function setSessionRules(r: Partial<PermissionRules>): void;
|
|
|
15
15
|
export declare function setSessionSettings(obj: Record<string, unknown> | null): void;
|
|
16
16
|
export declare function getSessionSettings(): Record<string, unknown> | null;
|
|
17
17
|
export declare function loadSettings(workDir: string): MergedSettings;
|
|
18
|
+
/**
|
|
19
|
+
* S25b: the raw object of every settings tier, in precedence order (managed →
|
|
20
|
+
* user → project → local → `--settings` session settings). For resolvers where
|
|
21
|
+
* the STRICTEST value must win regardless of tier — `maxEffortLevel` (a cap can
|
|
22
|
+
* only be tightened by a lower-precedence file, never loosened), and anything
|
|
23
|
+
* with the same one-way semantics added later. `loadSettings().raw` cannot serve
|
|
24
|
+
* those: it merges with LAST-wins, which is the opposite direction.
|
|
25
|
+
*/
|
|
26
|
+
export declare function settingsTiersRaw(workDir: string): Array<{
|
|
27
|
+
tier: string;
|
|
28
|
+
raw: Record<string, unknown>;
|
|
29
|
+
}>;
|
|
18
30
|
/** The first ALLOW rule that matches this call, or null. S20's classifier gate
|
|
19
31
|
* reads this to tell a NARROW pre-approval (wins, no review) from a BROAD one
|
|
20
32
|
* (suspended while the classifier is on — see isBroadAllowRule). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"rules.d.ts","sourceRoot":"","sources":["../../src/permissions/rules.ts"],"names":[],"mappings":"AA0BA,MAAM,MAAM,kBAAkB,GAAG,OAAO,GAAG,KAAK,GAAG,MAAM,CAAC;AAE1D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,EAAE,MAAM,EAAE,CAAC;IACd,IAAI,EAAE,MAAM,EAAE,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,WAAW,EAAE,eAAe,CAAC;IAC7B,yDAAyD;IACzD,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC9B;AAED,wBAAgB,mBAAmB,IAAI,MAAM,CAQ5C;AAsBD,oFAAoF;AACpF,wBAAgB,eAAe,CAAC,CAAC,EAAE,OAAO,CAAC,eAAe,CAAC,GAAG,IAAI,CAEjE;AAQD,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,GAAG,IAAI,CAE5E;AACD,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAEnE;AAED,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,GAAG,cAAc,CA8B5D;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,CAAC,CAcvG;AAsMD;;oEAEoE;AACpE,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAEf;AAYD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOtD;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,eAAe,EACtB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,OAAO,EAAE,MAAM,GACd,kBAAkB,GAAG,IAAI,CAK3B"}
|
|
@@ -38,12 +38,15 @@ exports.setSessionRules = setSessionRules;
|
|
|
38
38
|
exports.setSessionSettings = setSessionSettings;
|
|
39
39
|
exports.getSessionSettings = getSessionSettings;
|
|
40
40
|
exports.loadSettings = loadSettings;
|
|
41
|
+
exports.settingsTiersRaw = settingsTiersRaw;
|
|
41
42
|
exports.matchedAllowRule = matchedAllowRule;
|
|
42
43
|
exports.isBroadAllowRule = isBroadAllowRule;
|
|
43
44
|
exports.evaluatePermission = evaluatePermission;
|
|
44
45
|
const fs = __importStar(require("fs"));
|
|
45
46
|
const path = __importStar(require("path"));
|
|
46
47
|
const os = __importStar(require("os"));
|
|
48
|
+
const modePolicy_1 = require("./modePolicy");
|
|
49
|
+
const destructiveTokens_1 = require("./destructiveTokens");
|
|
47
50
|
function managedSettingsPath() {
|
|
48
51
|
// Tests (and a sandboxed doctor run) point this at a fixture. Checked first so it
|
|
49
52
|
// can never be shadowed by a real machine-level file.
|
|
@@ -120,6 +123,31 @@ function loadSettings(workDir) {
|
|
|
120
123
|
merged.deny.push(...sessionRules.deny);
|
|
121
124
|
return { permissions: merged, raw };
|
|
122
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* S25b: the raw object of every settings tier, in precedence order (managed →
|
|
128
|
+
* user → project → local → `--settings` session settings). For resolvers where
|
|
129
|
+
* the STRICTEST value must win regardless of tier — `maxEffortLevel` (a cap can
|
|
130
|
+
* only be tightened by a lower-precedence file, never loosened), and anything
|
|
131
|
+
* with the same one-way semantics added later. `loadSettings().raw` cannot serve
|
|
132
|
+
* those: it merges with LAST-wins, which is the opposite direction.
|
|
133
|
+
*/
|
|
134
|
+
function settingsTiersRaw(workDir) {
|
|
135
|
+
const files = [
|
|
136
|
+
['managed', managedSettingsPath()],
|
|
137
|
+
['user', path.join(os.homedir(), '.nexrall', 'settings.json')],
|
|
138
|
+
['project', path.join(workDir, '.nexrall', 'settings.json')],
|
|
139
|
+
['local', path.join(workDir, '.nexrall', 'settings.local.json')],
|
|
140
|
+
];
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const [tier, file] of files) {
|
|
143
|
+
const obj = readJson(file);
|
|
144
|
+
if (obj)
|
|
145
|
+
out.push({ tier, raw: obj });
|
|
146
|
+
}
|
|
147
|
+
if (sessionSettings)
|
|
148
|
+
out.push({ tier: 'session', raw: sessionSettings });
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
123
151
|
// ─── Rule matching ──────────────────────────────────────────────────────────
|
|
124
152
|
function parseRule(rule) {
|
|
125
153
|
const m = /^([A-Za-z0-9_]+)\((.*)\)$/.exec(rule.trim());
|
|
@@ -180,8 +208,21 @@ function matchPattern(pattern, value) {
|
|
|
180
208
|
}
|
|
181
209
|
return value === pattern;
|
|
182
210
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
211
|
+
/** The command line of a bash-family call: `command`, falling back to the `cmd`
|
|
212
|
+
* alias every other reader accepts (destructive guard, CLI handler). */
|
|
213
|
+
function bashCommand(input) {
|
|
214
|
+
const v = input.command ?? input.cmd;
|
|
215
|
+
return typeof v === 'string' ? v : '';
|
|
216
|
+
}
|
|
217
|
+
/** Map a tool's input to the string(s) a rule pattern should match against.
|
|
218
|
+
*
|
|
219
|
+
* S25f: deny/ask matching ALSO sees a path's real identity behind symlinks —
|
|
220
|
+
* Claude Code 2.1.289 fixed exactly this class ("Read deny rules not applying
|
|
221
|
+
* to files @-mentioned, changed, or selected in the IDE through a symlink").
|
|
222
|
+
* The allow path skips those views (`followSymlinks: false`): an exception
|
|
223
|
+
* stays as NARROW as it was written — the same asymmetry the unwrapped bash
|
|
224
|
+
* views in destructiveTokens use. */
|
|
225
|
+
function matchValues(tool, input, workDir, followSymlinks = false) {
|
|
185
226
|
const str = (v) => (typeof v === 'string' ? v : '');
|
|
186
227
|
const vals = [];
|
|
187
228
|
const pushPath = (p) => {
|
|
@@ -192,10 +233,36 @@ function matchValues(tool, input, workDir) {
|
|
|
192
233
|
vals.push(path.relative(workDir, p));
|
|
193
234
|
else
|
|
194
235
|
vals.push(path.resolve(workDir, p));
|
|
236
|
+
if (!followSymlinks)
|
|
237
|
+
return;
|
|
238
|
+
const abs = path.resolve(workDir, p);
|
|
239
|
+
try {
|
|
240
|
+
const real = fs.realpathSync(abs);
|
|
241
|
+
if (real !== abs)
|
|
242
|
+
vals.push(real, path.relative(workDir, real));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
// A path being CREATED through a symlinked directory: resolve the parent.
|
|
246
|
+
try {
|
|
247
|
+
const realDir = fs.realpathSync(path.dirname(abs));
|
|
248
|
+
const realAbs = path.join(realDir, path.basename(abs));
|
|
249
|
+
vals.push(realAbs, path.relative(workDir, realAbs));
|
|
250
|
+
}
|
|
251
|
+
catch {
|
|
252
|
+
/* neither exists: nothing to resolve */
|
|
253
|
+
}
|
|
254
|
+
}
|
|
195
255
|
};
|
|
196
256
|
switch (tool) {
|
|
257
|
+
// The whole bash family, matching modePolicy.BASH_TOOLS: the executor emits
|
|
258
|
+
// `bash`, and run_command / execute_command are the same tool accepted
|
|
259
|
+
// everywhere else (destructive guard, CLI handler). A `Bash(…)` rule must
|
|
260
|
+
// not silently skip a call that arrives under one of those names — the
|
|
261
|
+
// "looks configured and isn't" failure class the task rules had.
|
|
197
262
|
case 'bash':
|
|
198
|
-
|
|
263
|
+
case 'run_command':
|
|
264
|
+
case 'execute_command':
|
|
265
|
+
vals.push(bashCommand(input));
|
|
199
266
|
break;
|
|
200
267
|
case 'move_file':
|
|
201
268
|
pushPath(str(input.source));
|
|
@@ -241,7 +308,7 @@ function ruleMatches(rule, tool, input, workDir, opts) {
|
|
|
241
308
|
Read: ['read_file', 'list_directory', 'search_files', 'glob', 'notebook_read'],
|
|
242
309
|
Edit: ['edit_file', 'multi_edit', 'write_file', 'notebook_edit'],
|
|
243
310
|
Write: ['write_file', 'write_docx', 'write_xlsx', 'write_pptx'],
|
|
244
|
-
Bash: ['bash'],
|
|
311
|
+
Bash: ['bash', 'run_command', 'execute_command'],
|
|
245
312
|
WebFetch: ['fetch_url'],
|
|
246
313
|
Image: ['generate_image', 'stock_photo'],
|
|
247
314
|
};
|
|
@@ -255,15 +322,30 @@ function ruleMatches(rule, tool, input, workDir, opts) {
|
|
|
255
322
|
// "Bash(npm run test:*)" allow rule straight past the user prompt.
|
|
256
323
|
// Deny rules stay aggressive (they SHOULD match chained commands), and a
|
|
257
324
|
// bare "Bash" allow (pattern === null) is an explicit "allow everything".
|
|
258
|
-
if (opts?.forAllow && tool
|
|
259
|
-
const cmd =
|
|
325
|
+
if (opts?.forAllow && modePolicy_1.BASH_TOOLS.has(tool) && pattern !== null && pattern !== '' && pattern !== '*') {
|
|
326
|
+
const cmd = bashCommand(input);
|
|
260
327
|
// newline / ; / && / || / | / trailing & / command-substitution / backtick
|
|
261
328
|
if (/[\r\n;`]|&&|\|\||\||&\s*$|\$\(/.test(cmd))
|
|
262
329
|
return false;
|
|
263
330
|
}
|
|
264
331
|
if (pattern === null || pattern === '' || pattern === '*')
|
|
265
332
|
return true;
|
|
266
|
-
|
|
333
|
+
if (matchValues(tool, input, workDir, !opts?.forAllow).some((v) => matchPattern(pattern, v)))
|
|
334
|
+
return true;
|
|
335
|
+
// Deny / ask rules must also see the command BEHIND a costume: env
|
|
336
|
+
// assignments (`TZ="$HOME" rm -rf build` — Claude Code's own fix class,
|
|
337
|
+
// 2.1.288/289), wrappers (`sudo`, `env`, `nice`, `timeout`, `xargs`, shell
|
|
338
|
+
// `-c`, `eval`), compound separators (`;` `&&` `||` `|`) and `$(…)` /
|
|
339
|
+
// backtick bodies. The destructive guard has always read these spellings
|
|
340
|
+
// (destructiveTokens); a user's deny rule must not be the one thing a
|
|
341
|
+
// prefix can skip.
|
|
342
|
+
//
|
|
343
|
+
// ALLOW deliberately stays exact — an exception is only as WIDE as it was
|
|
344
|
+
// written, and env vars can execute code (NODE_OPTIONS, LD_PRELOAD…), so
|
|
345
|
+
// `FOO=x npm run test` must not ride `Bash(npm run test:*)` without asking.
|
|
346
|
+
if (opts?.forAllow || !modePolicy_1.BASH_TOOLS.has(tool))
|
|
347
|
+
return false;
|
|
348
|
+
return (0, destructiveTokens_1.unwrappedViews)(bashCommand(input)).some((v) => matchPattern(pattern, v));
|
|
267
349
|
}
|
|
268
350
|
/** The first ALLOW rule that matches this call, or null. S20's classifier gate
|
|
269
351
|
* reads this to tell a NARROW pre-approval (wins, no review) from a BROAD one
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type UserConfigOption } from './userConfig';
|
|
2
|
+
type SecretMap = Record<string, Record<string, unknown>>;
|
|
3
|
+
/** The whole secrets file ({} when absent/unreadable/malformed). */
|
|
4
|
+
export declare function readPluginSecrets(): SecretMap;
|
|
5
|
+
/**
|
|
6
|
+
* `pluginConfigs` from every settings tier, merged per plugin per key
|
|
7
|
+
* (last tier wins — the same direction loadSettings uses for `raw`).
|
|
8
|
+
*/
|
|
9
|
+
export declare function pluginConfigsFromSettings(workDir: string): Record<string, Record<string, unknown>>;
|
|
10
|
+
export interface ResolvedPluginConfig {
|
|
11
|
+
/** key → effective value (valid sources only, default included). */
|
|
12
|
+
values: Record<string, unknown>;
|
|
13
|
+
/** key → where the value came from. */
|
|
14
|
+
sources: Record<string, 'secret' | 'settings' | 'default'>;
|
|
15
|
+
/** required options with no value anywhere (not even a default). */
|
|
16
|
+
missing: string[];
|
|
17
|
+
/** Values that failed validation, with their source — reported, not used. */
|
|
18
|
+
invalid: Array<{
|
|
19
|
+
key: string;
|
|
20
|
+
source: 'secret' | 'settings';
|
|
21
|
+
error: string;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
/** Resolve every declared option to its effective value. Pure reads. */
|
|
25
|
+
export declare function resolvePluginConfig(workDir: string, pluginName: string, options: UserConfigOption[]): ResolvedPluginConfig;
|
|
26
|
+
export interface SavePluginConfigResult {
|
|
27
|
+
ok: boolean;
|
|
28
|
+
/** Nothing was written when this is non-empty. */
|
|
29
|
+
errors: string[];
|
|
30
|
+
/** Paths actually written. */
|
|
31
|
+
wroteSettings?: string;
|
|
32
|
+
wroteSecrets?: boolean;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Validate + save values. Unknown keys are errors (a typo must not create a
|
|
36
|
+
* silent no-op entry), as are values that fail their option's validation. The
|
|
37
|
+
* two halves land in different stores (see the file header).
|
|
38
|
+
*/
|
|
39
|
+
export declare function savePluginConfig(workDir: string, pluginName: string, options: UserConfigOption[], values: Record<string, unknown>, opts?: {
|
|
40
|
+
scope?: 'user' | 'project';
|
|
41
|
+
}): SavePluginConfigResult;
|
|
42
|
+
export {};
|
|
43
|
+
//# sourceMappingURL=configStore.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"configStore.d.ts","sourceRoot":"","sources":["../../src/plugins/configStore.ts"],"names":[],"mappings":"AA2BA,OAAO,EAA2B,KAAK,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAmD9E,KAAK,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAEzD,oEAAoE;AACpE,wBAAgB,iBAAiB,IAAI,SAAS,CAQ7C;AAgBD;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAWlG;AAED,MAAM,WAAW,oBAAoB;IACnC,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,uCAAuC;IACvC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,GAAG,SAAS,CAAC,CAAC;IAC3D,oEAAoE;IACpE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,6EAA6E;IAC7E,OAAO,EAAE,KAAK,CAAC;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CAC/E;AAED,wEAAwE;AACxE,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,gBAAgB,EAAE,GAC1B,oBAAoB,CA+BtB;AAED,MAAM,WAAW,sBAAsB;IACrC,EAAE,EAAE,OAAO,CAAC;IACZ,kDAAkD;IAClD,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,8BAA8B;IAC9B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,gBAAgB,EAAE,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;CAAO,GACxC,sBAAsB,CAkDxB"}
|