@cjhyy/code-shell-core 0.8.0 → 0.8.2
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/engine/run-tooling.js +4 -0
- package/dist/engine/turn-loop.d.ts +3 -0
- package/dist/engine/turn-loop.js +80 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/tool-system/builtin/tool-search.js +44 -2
- package/dist/tool-system/context.d.ts +8 -1
- package/dist/tool-system/path-policy.js +99 -2
- package/package.json +1 -1
|
@@ -279,5 +279,9 @@ export function assembleRunToolDefs(args) {
|
|
|
279
279
|
const toolDefs = args.runPlanMode
|
|
280
280
|
? profileToolDefs.filter((t) => PLAN_MODE_ALLOWED_TOOLS.has(t.name))
|
|
281
281
|
: profileToolDefs;
|
|
282
|
+
// ToolSearch shares the worker registry across Sessions, but discovery must
|
|
283
|
+
// reflect this exact run's filtered and rewritten surface. Keep the context
|
|
284
|
+
// snapshot in lockstep with the definitions sent to the model.
|
|
285
|
+
toolCtx.searchableToolDefinitions = toolDefs;
|
|
282
286
|
return toolDefs;
|
|
283
287
|
}
|
|
@@ -202,6 +202,9 @@ export declare class TurnLoop {
|
|
|
202
202
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
203
203
|
*/
|
|
204
204
|
private stopBlockCount;
|
|
205
|
+
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
206
|
+
private repeatedToolBatchFingerprint;
|
|
207
|
+
private repeatedToolBatchCount;
|
|
205
208
|
/**
|
|
206
209
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
207
210
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
package/dist/engine/turn-loop.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Following Claude Code's po_() pattern:
|
|
5
5
|
* pre_check → model_call → post_check → tool_exec → context_mgmt → hook_notify → next turn
|
|
6
6
|
*/
|
|
7
|
-
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
8
8
|
import { buildAgentDirectionMessage } from "../tool-system/builtin/agent-notifications.js";
|
|
9
9
|
import { newTurnId } from "./turn-state.js";
|
|
10
10
|
import { formatFriendlyError } from "./friendly-error.js";
|
|
@@ -40,6 +40,39 @@ export function toolResultToBlock(result) {
|
|
|
40
40
|
block.is_error = true;
|
|
41
41
|
return block;
|
|
42
42
|
}
|
|
43
|
+
const REPEATED_TOOL_BATCH_LIMIT = 3;
|
|
44
|
+
function canonicalToolValue(value) {
|
|
45
|
+
if (value === null)
|
|
46
|
+
return "null";
|
|
47
|
+
if (value === undefined)
|
|
48
|
+
return "undefined";
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return `[${value.map(canonicalToolValue).join(",")}]`;
|
|
51
|
+
if (typeof value === "object") {
|
|
52
|
+
return `{${Object.entries(value)
|
|
53
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
54
|
+
.map(([key, entry]) => `${JSON.stringify(key)}:${canonicalToolValue(entry)}`)
|
|
55
|
+
.join(",")}}`;
|
|
56
|
+
}
|
|
57
|
+
return JSON.stringify(value);
|
|
58
|
+
}
|
|
59
|
+
function repeatedToolBatchFingerprint(toolCalls, results) {
|
|
60
|
+
// Hash immediately and never log the canonical source: tool results may
|
|
61
|
+
// contain credentials or large media payloads. Call ids are deliberately
|
|
62
|
+
// omitted because providers generate a fresh id for every identical retry.
|
|
63
|
+
return createHash("sha256")
|
|
64
|
+
.update(canonicalToolValue({
|
|
65
|
+
calls: toolCalls.map((call) => ({ toolName: call.toolName, args: call.args })),
|
|
66
|
+
results: results.map((result) => ({
|
|
67
|
+
toolName: result.toolName,
|
|
68
|
+
isError: result.isError === true || Boolean(result.error),
|
|
69
|
+
error: result.error,
|
|
70
|
+
result: result.result,
|
|
71
|
+
contentBlocks: result.contentBlocks,
|
|
72
|
+
})),
|
|
73
|
+
}))
|
|
74
|
+
.digest("hex");
|
|
75
|
+
}
|
|
43
76
|
export class TurnLoop {
|
|
44
77
|
deps;
|
|
45
78
|
config;
|
|
@@ -76,6 +109,9 @@ export class TurnLoop {
|
|
|
76
109
|
* loop forces a stop so a stuck goal can't loop forever.
|
|
77
110
|
*/
|
|
78
111
|
stopBlockCount = 0;
|
|
112
|
+
/** Consecutive identical tool-call/result batches, ignoring provider call ids. */
|
|
113
|
+
repeatedToolBatchFingerprint;
|
|
114
|
+
repeatedToolBatchCount = 0;
|
|
79
115
|
/**
|
|
80
116
|
* Run-scoped goal budget tracker (Goal mode). Hoisted to an instance field
|
|
81
117
|
* (not a run() local) so extend() can bump its budgets mid-run. Null when no
|
|
@@ -1409,6 +1445,49 @@ export class TurnLoop {
|
|
|
1409
1445
|
tlog.info("guard.stale_task", { cat: "guard", turn: this.turnCount });
|
|
1410
1446
|
}
|
|
1411
1447
|
}
|
|
1448
|
+
const toolBatchFingerprint = repeatedToolBatchFingerprint(toolCalls, results);
|
|
1449
|
+
if (toolBatchFingerprint === this.repeatedToolBatchFingerprint) {
|
|
1450
|
+
this.repeatedToolBatchCount++;
|
|
1451
|
+
}
|
|
1452
|
+
else {
|
|
1453
|
+
this.repeatedToolBatchFingerprint = toolBatchFingerprint;
|
|
1454
|
+
this.repeatedToolBatchCount = 1;
|
|
1455
|
+
}
|
|
1456
|
+
if (this.repeatedToolBatchCount >= REPEATED_TOOL_BATCH_LIMIT) {
|
|
1457
|
+
tlog.warn("turn.repeated_tool_batch_stopped", {
|
|
1458
|
+
cat: "turn",
|
|
1459
|
+
repeatedCount: this.repeatedToolBatchCount,
|
|
1460
|
+
tools: toolCalls.map((call) => call.toolName),
|
|
1461
|
+
});
|
|
1462
|
+
await this.emitHook("on_turn_end", {
|
|
1463
|
+
turnNumber: this.turnCount,
|
|
1464
|
+
hasToolUse: true,
|
|
1465
|
+
toolCallCount: toolCalls.length,
|
|
1466
|
+
});
|
|
1467
|
+
finalText =
|
|
1468
|
+
`检测到同一组工具调用及其结果连续重复 ${REPEATED_TOOL_BATCH_LIMIT} 次,` +
|
|
1469
|
+
"已自动停止,避免继续空转。请调整请求或让 Session 获取新的上下文后再试。";
|
|
1470
|
+
this.deps.transcript.appendMessage("assistant", finalText);
|
|
1471
|
+
messages.push({ role: "assistant", content: finalText });
|
|
1472
|
+
this.config.onStream?.({
|
|
1473
|
+
type: "assistant_message",
|
|
1474
|
+
messageId: assistantMessageId,
|
|
1475
|
+
message: { role: "assistant", content: finalText },
|
|
1476
|
+
});
|
|
1477
|
+
this.finalizeModelTurn();
|
|
1478
|
+
if (await this.consumeQueuedSteer(messages, "finalize_backfill")) {
|
|
1479
|
+
this.repeatedToolBatchFingerprint = undefined;
|
|
1480
|
+
this.repeatedToolBatchCount = 0;
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
messages = this.redactConsumedSensitiveToolResults(messages);
|
|
1484
|
+
return {
|
|
1485
|
+
text: finalText,
|
|
1486
|
+
reason: "completed",
|
|
1487
|
+
messages,
|
|
1488
|
+
completionKind: "limit_stop",
|
|
1489
|
+
};
|
|
1490
|
+
}
|
|
1412
1491
|
// Hook: turn end
|
|
1413
1492
|
await this.emitHook("on_turn_end", {
|
|
1414
1493
|
turnNumber: this.turnCount,
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export declare const VERSION = "0.8.
|
|
6
|
+
export declare const VERSION = "0.8.2";
|
|
7
7
|
export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
|
|
8
8
|
export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
|
|
9
9
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Public API exports.
|
|
5
5
|
*/
|
|
6
|
-
export const VERSION = "0.8.
|
|
6
|
+
export const VERSION = "0.8.2";
|
|
7
7
|
// ─── Exceptions ──────────────────────────────────────────────────
|
|
8
8
|
export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
|
|
9
9
|
// ─── Engine (primary API) ────────────────────────────────────────
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
import { isRegisteredMcpToolAllowed } from "../mcp-tool-policy.js";
|
|
9
9
|
export const toolSearchToolDef = {
|
|
10
10
|
name: "ToolSearch",
|
|
11
|
-
description: "Search
|
|
11
|
+
description: "Search only the tools available in the current Session context by name or keyword. " +
|
|
12
12
|
"Some tools (especially from MCP servers) are deferred — their full schemas " +
|
|
13
|
-
"are only loaded when you search for them.
|
|
13
|
+
"are only loaded when you search for them. If an exact tool is reported unavailable, " +
|
|
14
|
+
"do not retry unless the Session context changes.",
|
|
14
15
|
inputSchema: {
|
|
15
16
|
type: "object",
|
|
16
17
|
properties: {
|
|
@@ -37,6 +38,18 @@ export async function toolSearchTool(args, ctx) {
|
|
|
37
38
|
// returning the wrong tool set. `|| 5` only caught 0/NaN, not negatives.
|
|
38
39
|
const rawMax = args.max_results;
|
|
39
40
|
const maxResults = Math.min(typeof rawMax === "number" && rawMax > 0 ? rawMax : 5, 20);
|
|
41
|
+
const currentDefinitions = ctx.searchableToolDefinitions;
|
|
42
|
+
if (currentDefinitions) {
|
|
43
|
+
const currentTools = currentDefinitions.map((definition) => definitionToSearchableTool(definition, ctx.toolRegistry));
|
|
44
|
+
if (query.startsWith("select:")) {
|
|
45
|
+
const names = query
|
|
46
|
+
.slice(7)
|
|
47
|
+
.split(",")
|
|
48
|
+
.map((name) => name.trim());
|
|
49
|
+
return matchCurrentExact(currentTools, names);
|
|
50
|
+
}
|
|
51
|
+
return searchCurrentByKeyword(currentTools, query, maxResults);
|
|
52
|
+
}
|
|
40
53
|
// The tool registry is worker-SHARED (B1): it holds MCP tools registered by
|
|
41
54
|
// every session, including servers another project enabled. ToolSearch is a
|
|
42
55
|
// side door around the per-turn toolDefs filter — without this gate it would
|
|
@@ -63,6 +76,32 @@ export async function toolSearchTool(args, ctx) {
|
|
|
63
76
|
// Keyword search
|
|
64
77
|
return searchByKeyword(ctx.toolRegistry, query, maxResults, visible);
|
|
65
78
|
}
|
|
79
|
+
function definitionToSearchableTool(definition, registry) {
|
|
80
|
+
const registered = registry.getTool(definition.name);
|
|
81
|
+
return {
|
|
82
|
+
name: definition.name,
|
|
83
|
+
description: definition.description,
|
|
84
|
+
inputSchema: definition.inputSchema,
|
|
85
|
+
source: registered?.source ?? "builtin",
|
|
86
|
+
...(registered?.serverName ? { serverName: registered.serverName } : {}),
|
|
87
|
+
...(registered?.mcpToolName ? { mcpToolName: registered.mcpToolName } : {}),
|
|
88
|
+
permissionDefault: registered?.permissionDefault ?? "ask",
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
function matchCurrentExact(tools, names) {
|
|
92
|
+
const byName = new Map(tools.map((tool) => [tool.name, tool]));
|
|
93
|
+
return names
|
|
94
|
+
.map((name) => {
|
|
95
|
+
const tool = byName.get(name);
|
|
96
|
+
return tool
|
|
97
|
+
? formatTool(tool)
|
|
98
|
+
: `Tool "${name}" is not available in the current Session context. Do not retry unless the Session context changes.`;
|
|
99
|
+
})
|
|
100
|
+
.join("\n\n---\n\n");
|
|
101
|
+
}
|
|
102
|
+
function searchCurrentByKeyword(tools, query, maxResults) {
|
|
103
|
+
return searchDetailedTools(tools, query, maxResults);
|
|
104
|
+
}
|
|
66
105
|
function matchExact(registry, names, visible) {
|
|
67
106
|
const results = [];
|
|
68
107
|
for (const name of names) {
|
|
@@ -78,6 +117,9 @@ function matchExact(registry, names, visible) {
|
|
|
78
117
|
}
|
|
79
118
|
function searchByKeyword(registry, query, maxResults, visible) {
|
|
80
119
|
const allTools = registry.listToolsDetailed().filter(visible);
|
|
120
|
+
return searchDetailedTools(allTools, query, maxResults);
|
|
121
|
+
}
|
|
122
|
+
function searchDetailedTools(allTools, query, maxResults) {
|
|
81
123
|
const queryLower = query.toLowerCase();
|
|
82
124
|
const keywords = queryLower.split(/\s+/);
|
|
83
125
|
// Score each tool
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* Tools that don't need any context (Read/Write/Bash/...) just ignore
|
|
13
13
|
* the second argument; the type is purely additive.
|
|
14
14
|
*/
|
|
15
|
-
import type { LLMConfig, StreamCallback, TokenUsage } from "../types.js";
|
|
15
|
+
import type { LLMConfig, StreamCallback, TokenUsage, ToolDefinition } from "../types.js";
|
|
16
16
|
import type { ModelPool } from "../llm/model-pool.js";
|
|
17
17
|
import type { ToolRegistry } from "./registry.js";
|
|
18
18
|
import type { AgentPresetName } from "../preset/index.js";
|
|
@@ -242,6 +242,13 @@ export interface ToolContext {
|
|
|
242
242
|
modelPool?: ModelPool;
|
|
243
243
|
/** Tool registry (ToolSearch reads this to enumerate available tools). */
|
|
244
244
|
toolRegistry: ToolRegistry;
|
|
245
|
+
/**
|
|
246
|
+
* Final per-turn tool surface after availability guards, feature flags,
|
|
247
|
+
* behavior-profile allowlists, plan-mode filtering, and definition rewrites.
|
|
248
|
+
* ToolSearch must prefer this list over the worker-shared registry so it
|
|
249
|
+
* cannot advertise a tool the current Session cannot actually call.
|
|
250
|
+
*/
|
|
251
|
+
searchableToolDefinitions?: readonly ToolDefinition[];
|
|
245
252
|
/** Opaque services contributed by capability modules, keyed by capability id. */
|
|
246
253
|
capabilityServices?: Readonly<Record<string, unknown>>;
|
|
247
254
|
/**
|
|
@@ -29,11 +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 { constants, realpathSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, } from "node:fs";
|
|
32
|
+
import { constants, realpathSync, existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, statSync, } from "node:fs";
|
|
33
33
|
import { open } from "node:fs/promises";
|
|
34
34
|
import { homedir } from "node:os";
|
|
35
35
|
import { randomUUID } from "node:crypto";
|
|
36
|
-
import { dirname, isAbsolute, join, resolve as resolvePath, sep } from "node:path";
|
|
36
|
+
import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from "node:path";
|
|
37
|
+
import { readInstalledPlugins } from "../plugins/installedPlugins.js";
|
|
37
38
|
const FINAL_WRITE_PATH_SNAPSHOT = Symbol("codeshell.finalWritePathSnapshot");
|
|
38
39
|
/**
|
|
39
40
|
* Default sensitive path patterns. These are evaluated AFTER home-expansion
|
|
@@ -392,6 +393,88 @@ function isInsideDir(child, parent) {
|
|
|
392
393
|
const p = par.endsWith(sep) ? par : par + sep;
|
|
393
394
|
return c === par || c.startsWith(p);
|
|
394
395
|
}
|
|
396
|
+
function configuredUserHome() {
|
|
397
|
+
return process.env.HOME ?? homedir();
|
|
398
|
+
}
|
|
399
|
+
/**
|
|
400
|
+
* Whether `resolved` belongs to one concrete Skill tree rooted below
|
|
401
|
+
* `<skillsRoot>/<skill>/`.
|
|
402
|
+
*
|
|
403
|
+
* Both the root and manifest are realpathed, so a reference symlink cannot
|
|
404
|
+
* turn this read-only exception into access outside the managed Skill tree.
|
|
405
|
+
*/
|
|
406
|
+
function isSkillTreeResource(resolved, skillsRoot) {
|
|
407
|
+
try {
|
|
408
|
+
const realSkillsRoot = realpathSync(skillsRoot);
|
|
409
|
+
if (!isInsideDir(resolved, realSkillsRoot))
|
|
410
|
+
return false;
|
|
411
|
+
const rel = relative(realSkillsRoot, resolved);
|
|
412
|
+
const skillName = rel.split(sep).filter(Boolean)[0];
|
|
413
|
+
if (!skillName || skillName === "..")
|
|
414
|
+
return false;
|
|
415
|
+
const skillRoot = realpathSync(join(realSkillsRoot, skillName));
|
|
416
|
+
if (!isInsideDir(skillRoot, realSkillsRoot))
|
|
417
|
+
return false;
|
|
418
|
+
const manifest = realpathSync(join(skillRoot, "SKILL.md"));
|
|
419
|
+
if (!isInsideDir(manifest, skillRoot) || !statSync(manifest).isFile())
|
|
420
|
+
return false;
|
|
421
|
+
return isInsideDir(resolved, skillRoot);
|
|
422
|
+
}
|
|
423
|
+
catch {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Skill instructions routinely link to sibling references/scripts/assets.
|
|
429
|
+
* The Skill builtin can already read the registered SKILL.md, so asking again
|
|
430
|
+
* for every referenced file is both inconsistent and capable of wedging a
|
|
431
|
+
* headless run.
|
|
432
|
+
*
|
|
433
|
+
* Keep the exception narrow:
|
|
434
|
+
* - read-only (the caller checks the operation);
|
|
435
|
+
* - user Skills, or an installed plugin recorded in the V2 registry;
|
|
436
|
+
* - plugin installs must realpath beneath the managed plugin cache;
|
|
437
|
+
* - the target must remain inside a directory containing a real SKILL.md.
|
|
438
|
+
*/
|
|
439
|
+
function isRegisteredSkillResourceRead(resolved) {
|
|
440
|
+
let codeShellRoot;
|
|
441
|
+
try {
|
|
442
|
+
codeShellRoot = realpathSync(join(configuredUserHome(), ".code-shell"));
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
if (!isInsideDir(resolved, codeShellRoot))
|
|
448
|
+
return false;
|
|
449
|
+
if (isSkillTreeResource(resolved, join(codeShellRoot, "skills")))
|
|
450
|
+
return true;
|
|
451
|
+
let cacheRoot;
|
|
452
|
+
try {
|
|
453
|
+
cacheRoot = realpathSync(join(codeShellRoot, "plugins", "cache"));
|
|
454
|
+
}
|
|
455
|
+
catch {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
const installed = readInstalledPlugins();
|
|
459
|
+
for (const entries of Object.values(installed.plugins)) {
|
|
460
|
+
for (const entry of entries) {
|
|
461
|
+
try {
|
|
462
|
+
const installRoot = realpathSync(entry.installPath);
|
|
463
|
+
if (installRoot === cacheRoot || !isInsideDir(installRoot, cacheRoot))
|
|
464
|
+
continue;
|
|
465
|
+
const skillsRoot = realpathSync(join(installRoot, "skills"));
|
|
466
|
+
if (!isInsideDir(skillsRoot, installRoot))
|
|
467
|
+
continue;
|
|
468
|
+
if (isSkillTreeResource(resolved, skillsRoot))
|
|
469
|
+
return true;
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
// A stale/tampered registry entry must never broaden read authority.
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return false;
|
|
477
|
+
}
|
|
395
478
|
/**
|
|
396
479
|
* Returns the matching sensitive-dir entry (with the user's home prefix) if
|
|
397
480
|
* `resolved` lives underneath any sensitive directory, else undefined.
|
|
@@ -476,6 +559,20 @@ export function classifyPath(rawPath, opts) {
|
|
|
476
559
|
const sensitiveFile = matchSensitiveFile(resolved);
|
|
477
560
|
const sensitiveLabel = sensitiveDir ?? sensitiveFile;
|
|
478
561
|
const insideWorkspace = isInsideDir(resolved, workspace);
|
|
562
|
+
// Registered Skill resources are managed runtime inputs. Their SKILL.md is
|
|
563
|
+
// already readable through the Skill builtin; allow its contained reference
|
|
564
|
+
// files through the ordinary Read tool as well. A credential-shaped basename
|
|
565
|
+
// (.env, token.txt, key files, ...) deliberately keeps the sensitive-file
|
|
566
|
+
// gate even inside a Skill tree.
|
|
567
|
+
if (opts.operation === "read" &&
|
|
568
|
+
!sensitiveFile &&
|
|
569
|
+
isRegisteredSkillResourceRead(resolved)) {
|
|
570
|
+
return {
|
|
571
|
+
decision: "allow",
|
|
572
|
+
reason: "registered Skill resource read",
|
|
573
|
+
resolvedPath: resolved,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
479
576
|
// Sensitive: write is always denied, read always asks. Workspace placement
|
|
480
577
|
// doesn't soften the rule — an `.env` in the project still asks on read.
|
|
481
578
|
if (sensitiveLabel) {
|
package/package.json
CHANGED