@swifty.js/swifty 0.0.27 → 0.0.29
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/README.md +47 -37
- package/dist/agent-YR6YK26C.js +4 -0
- package/dist/anthropic-5GRR5JCT.js +4 -0
- package/dist/checker-N5TIJEN5.js +4 -0
- package/dist/chunk-D34FUVGU.js +4 -0
- package/dist/{chunk-UGJVMFGH.js → chunk-K5ZK27BX.js} +1 -1
- package/dist/chunk-LIMEBKCY.js +408 -0
- package/dist/chunk-OTVZGPHD.js +121 -0
- package/dist/chunk-POHIQUFP.js +301 -0
- package/dist/chunk-QCKJLO6X.js +4 -0
- package/dist/chunk-QFSCPD63.js +4 -0
- package/dist/chunk-TMEX7RFA.js +4 -0
- package/dist/chunk-Y6SQB5FG.js +4 -0
- package/dist/lib/agent-5WEKVRTD.js +11 -0
- package/dist/lib/{anthropic-RYPJDHQM.js → anthropic-KWO3KLNV.js} +4 -5
- package/dist/lib/{checker-6BW4222R.js → checker-AXHPKVBY.js} +2 -2
- package/dist/lib/{chunk-HK2Z6WP4.js → chunk-2IVEVG5N.js} +5 -1
- package/dist/lib/{chunk-XFSN4LMA.js → chunk-3FBS5NB7.js} +71 -18
- package/dist/lib/{chunk-GNI7YX6F.js → chunk-DJ6AILPN.js} +70 -24
- package/dist/lib/chunk-GCY44T7S.js +2346 -0
- package/dist/lib/{chunk-PZ42NAFA.js → chunk-KG4MJGKQ.js} +19 -21
- package/dist/lib/{chunk-2AUSNVIB.js → chunk-KZSBR4FO.js} +44 -37
- package/dist/lib/chunk-PIL7M52F.js +127 -0
- package/dist/lib/chunk-Z4CYHTII.js +392 -0
- package/dist/lib/index.d.ts +227 -132
- package/dist/lib/index.js +2195 -1602
- package/dist/lib/{openai-VX5VBXZ4.js → openai-PBF7EWNJ.js} +2 -3
- package/dist/lib/{tool-filter-VF7TZRE5.js → tool-filter-R6HDDJHE.js} +1 -1
- package/dist/main.js +203 -194
- package/dist/{openai-5FDSLJNM.js → openai-QQ2K7GPF.js} +14 -14
- package/dist/{server-F666APPN.js → server-G23HCFLI.js} +21 -21
- package/dist/{tool-filter-NO7I3HFD.js → tool-filter-VBP6WOGO.js} +1 -1
- package/package.json +2 -2
- package/dist/agent-OMC6YIMW.js +0 -4
- package/dist/anthropic-T5K4BQPB.js +0 -4
- package/dist/checker-TOQVEG3P.js +0 -4
- package/dist/chunk-5I2WXSGV.js +0 -90
- package/dist/chunk-A2VR3BC4.js +0 -4
- package/dist/chunk-A57FEYSN.js +0 -4
- package/dist/chunk-BRC5L644.js +0 -130
- package/dist/chunk-FZPTNGTU.js +0 -4
- package/dist/chunk-G6YIQW3Z.js +0 -238
- package/dist/chunk-I2XG2PXV.js +0 -4
- package/dist/chunk-LVSHXIBK.js +0 -389
- package/dist/chunk-SULWNDNC.js +0 -4
- package/dist/chunk-WO6DL7OB.js +0 -35
- package/dist/lib/agent-U7MIUCKT.js +0 -9
- package/dist/lib/chunk-2QILP24G.js +0 -1120
- package/dist/lib/chunk-3LU4APFB.js +0 -339
- package/dist/lib/chunk-OO2CLOEE.js +0 -88
- package/dist/lib/chunk-RKJYTYQM.js +0 -1285
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
getMaxOutputTokens,
|
|
3
|
-
resolveAPIKey
|
|
4
|
-
} from "./chunk-3LU4APFB.js";
|
|
5
1
|
import {
|
|
6
2
|
AuthenticationError,
|
|
7
3
|
ContextTooLongError,
|
|
8
4
|
LLMError,
|
|
9
5
|
NetworkError,
|
|
10
6
|
RateLimitError,
|
|
11
|
-
ensureToolPairing
|
|
12
|
-
|
|
7
|
+
ensureToolPairing,
|
|
8
|
+
getMaxOutputTokens,
|
|
9
|
+
getThinkingLevel,
|
|
10
|
+
resolveAPIKey,
|
|
11
|
+
toReasoningEffort
|
|
12
|
+
} from "./chunk-Z4CYHTII.js";
|
|
13
13
|
import {
|
|
14
14
|
asRecord,
|
|
15
15
|
asString,
|
|
@@ -27,11 +27,12 @@ var OpenAIClient = class {
|
|
|
27
27
|
model;
|
|
28
28
|
systemPrompt;
|
|
29
29
|
maxOutputTokens;
|
|
30
|
+
thinkingLevel;
|
|
30
31
|
constructor(config, systemPrompt) {
|
|
31
32
|
const apiKey = resolveAPIKey(config);
|
|
32
33
|
if (!apiKey) {
|
|
33
34
|
throw new AuthenticationError(
|
|
34
|
-
"OpenAI API key not found, set OPENAI_API_KEY in
|
|
35
|
+
"OpenAI API key not found, set OPENAI_API_KEY in ~/.swifty/config.yaml, or via OPENAI_API_KEY env variable."
|
|
35
36
|
);
|
|
36
37
|
}
|
|
37
38
|
this.client = new OpenAI({
|
|
@@ -41,6 +42,7 @@ var OpenAIClient = class {
|
|
|
41
42
|
this.model = config.model;
|
|
42
43
|
this.systemPrompt = systemPrompt;
|
|
43
44
|
this.maxOutputTokens = getMaxOutputTokens(config);
|
|
45
|
+
this.thinkingLevel = getThinkingLevel(config);
|
|
44
46
|
}
|
|
45
47
|
async *stream(conversation, toolSchemas, abortSignal) {
|
|
46
48
|
const messages = buildOpenAIInput(ensureToolPairing(conversation.getMessages()));
|
|
@@ -67,7 +69,10 @@ var OpenAIClient = class {
|
|
|
67
69
|
input,
|
|
68
70
|
stream: true,
|
|
69
71
|
max_output_tokens: this.maxOutputTokens,
|
|
70
|
-
...tools.length > 0 ? { tools } : {}
|
|
72
|
+
...tools.length > 0 ? { tools } : {},
|
|
73
|
+
// PI-equivalent thinking level → Responses API reasoning effort. Omitted
|
|
74
|
+
// entirely when thinking is off so non-reasoning models are unaffected.
|
|
75
|
+
...this.thinkingLevel !== "off" ? { reasoning: { effort: toReasoningEffort(this.thinkingLevel), summary: "auto" } } : {}
|
|
71
76
|
};
|
|
72
77
|
let inputTokens = 0;
|
|
73
78
|
let outputTokens = 0;
|
|
@@ -82,6 +87,7 @@ var OpenAIClient = class {
|
|
|
82
87
|
let jsonAccumulate = "";
|
|
83
88
|
let reasoningId = "";
|
|
84
89
|
let reasoningText = "";
|
|
90
|
+
let sawTerminalResponse = false;
|
|
85
91
|
for await (const event of stream) {
|
|
86
92
|
if (event.type === "response.output_text.delta") {
|
|
87
93
|
yield {
|
|
@@ -139,19 +145,22 @@ var OpenAIClient = class {
|
|
|
139
145
|
currentToolId = "";
|
|
140
146
|
jsonAccumulate = "";
|
|
141
147
|
}
|
|
142
|
-
} else if (event.type === "response.completed") {
|
|
148
|
+
} else if (event.type === "response.completed" || event.type === "response.incomplete") {
|
|
149
|
+
sawTerminalResponse = true;
|
|
143
150
|
const usage = event.response.usage;
|
|
144
151
|
if (usage) {
|
|
145
152
|
outputTokens = usage.output_tokens;
|
|
146
|
-
cacheReadInputTokens = usage.input_tokens_details
|
|
153
|
+
cacheReadInputTokens = usage.input_tokens_details?.cached_tokens ?? 0;
|
|
147
154
|
inputTokens = Math.max(0, usage.input_tokens - cacheReadInputTokens);
|
|
148
155
|
}
|
|
149
156
|
let stopReason = "end_turn";
|
|
150
157
|
const resp = event.response;
|
|
151
|
-
if (resp.status === "incomplete") {
|
|
158
|
+
if (event.type === "response.incomplete" || resp.status === "incomplete") {
|
|
152
159
|
const details = resp.incomplete_details;
|
|
153
160
|
if (details?.reason === "max_output_tokens") {
|
|
154
161
|
stopReason = "max_tokens";
|
|
162
|
+
} else {
|
|
163
|
+
throw new LLMError(`Response incomplete: ${details?.reason ?? "unknown reason"}`);
|
|
155
164
|
}
|
|
156
165
|
}
|
|
157
166
|
yield {
|
|
@@ -165,8 +174,18 @@ var OpenAIClient = class {
|
|
|
165
174
|
// 0
|
|
166
175
|
}
|
|
167
176
|
};
|
|
177
|
+
} else if (event.type === "response.failed") {
|
|
178
|
+
const error = event.response.error;
|
|
179
|
+
const message = `${error?.code ?? "unknown"}: ${error?.message ?? "Response failed"}`;
|
|
180
|
+
throw containsContextLengthError(message) ? new ContextTooLongError(message) : new LLMError(message);
|
|
181
|
+
} else if (event.type === "error") {
|
|
182
|
+
const message = `${event.code ?? "unknown"}: ${event.message}`;
|
|
183
|
+
throw containsContextLengthError(message) ? new ContextTooLongError(message) : new LLMError(message);
|
|
168
184
|
}
|
|
169
185
|
}
|
|
186
|
+
if (!sawTerminalResponse) {
|
|
187
|
+
throw new NetworkError("OpenAI Responses stream ended before a terminal response event");
|
|
188
|
+
}
|
|
170
189
|
} catch (err) {
|
|
171
190
|
log.error({ err }, "llm operation failed");
|
|
172
191
|
throw classifyOpenAIError(err);
|
|
@@ -178,6 +197,12 @@ var OpenAIClient = class {
|
|
|
178
197
|
setMaxOutputTokens(maxTokens) {
|
|
179
198
|
this.maxOutputTokens = maxTokens;
|
|
180
199
|
}
|
|
200
|
+
setThinkingLevel(level) {
|
|
201
|
+
this.thinkingLevel = level;
|
|
202
|
+
}
|
|
203
|
+
getThinkingLevel() {
|
|
204
|
+
return this.thinkingLevel;
|
|
205
|
+
}
|
|
181
206
|
};
|
|
182
207
|
function imageDataUrl(block) {
|
|
183
208
|
if (!isRecord(block) || block.type !== "image" || !isRecord(block.source)) {
|
|
@@ -349,6 +374,9 @@ function buildOpenAIInput(messages) {
|
|
|
349
374
|
output: toolOutputForResponses(tr)
|
|
350
375
|
});
|
|
351
376
|
}
|
|
377
|
+
if (m.content.length > 0) {
|
|
378
|
+
result.push({ role: "user", content: userContentsFor(m.content) });
|
|
379
|
+
}
|
|
352
380
|
} else if (m.role === "assistant") {
|
|
353
381
|
result.push({
|
|
354
382
|
role: "assistant",
|
|
@@ -371,17 +399,19 @@ var OpenAICompatClient = class {
|
|
|
371
399
|
model;
|
|
372
400
|
systemPrompt;
|
|
373
401
|
maxOutputTokens;
|
|
402
|
+
thinkingLevel;
|
|
374
403
|
constructor(config, systemPrompt) {
|
|
375
404
|
const apiKey = resolveAPIKey(config);
|
|
376
405
|
if (!apiKey) {
|
|
377
406
|
throw new AuthenticationError(
|
|
378
|
-
"OpenAI API key not found. Set OPENAI_API_KEY in
|
|
407
|
+
"OpenAI API key not found. Set OPENAI_API_KEY in ~/.swifty/config.yaml, or via OPENAI_API_KEY env variable."
|
|
379
408
|
);
|
|
380
409
|
}
|
|
381
410
|
this.client = new OpenAI({ apiKey, baseURL: config.base_url });
|
|
382
411
|
this.model = config.model;
|
|
383
412
|
this.systemPrompt = systemPrompt;
|
|
384
413
|
this.maxOutputTokens = getMaxOutputTokens(config);
|
|
414
|
+
this.thinkingLevel = getThinkingLevel(config);
|
|
385
415
|
}
|
|
386
416
|
setSystemPrompt(prompt) {
|
|
387
417
|
this.systemPrompt = prompt;
|
|
@@ -389,6 +419,12 @@ var OpenAICompatClient = class {
|
|
|
389
419
|
setMaxOutputTokens(maxTokens) {
|
|
390
420
|
this.maxOutputTokens = maxTokens;
|
|
391
421
|
}
|
|
422
|
+
setThinkingLevel(level) {
|
|
423
|
+
this.thinkingLevel = level;
|
|
424
|
+
}
|
|
425
|
+
getThinkingLevel() {
|
|
426
|
+
return this.thinkingLevel;
|
|
427
|
+
}
|
|
392
428
|
async *stream(conversation, toolSchemas, abortSignal) {
|
|
393
429
|
const messages = [
|
|
394
430
|
{
|
|
@@ -414,7 +450,10 @@ var OpenAICompatClient = class {
|
|
|
414
450
|
stream: true,
|
|
415
451
|
stream_options: { include_usage: true },
|
|
416
452
|
max_tokens: this.maxOutputTokens,
|
|
417
|
-
...tools.length > 0 ? { tools } : {}
|
|
453
|
+
...tools.length > 0 ? { tools } : {},
|
|
454
|
+
// PI-equivalent thinking level → Chat Completions reasoning_effort.
|
|
455
|
+
// Omitted when thinking is off so non-reasoning models are unaffected.
|
|
456
|
+
...this.thinkingLevel !== "off" ? { reasoning_effort: toReasoningEffort(this.thinkingLevel) } : {}
|
|
418
457
|
};
|
|
419
458
|
let inputTokens = 0;
|
|
420
459
|
let outputTokens = 0;
|
|
@@ -510,6 +549,9 @@ var OpenAICompatClient = class {
|
|
|
510
549
|
}
|
|
511
550
|
}
|
|
512
551
|
}
|
|
552
|
+
if (finishReason === null) {
|
|
553
|
+
throw new NetworkError("Chat Completions stream ended before a finish reason");
|
|
554
|
+
}
|
|
513
555
|
let stopReason;
|
|
514
556
|
if (finishReason === "length") {
|
|
515
557
|
stopReason = "max_tokens";
|
|
@@ -535,6 +577,9 @@ var OpenAICompatClient = class {
|
|
|
535
577
|
}
|
|
536
578
|
};
|
|
537
579
|
function classifyOpenAIError(err) {
|
|
580
|
+
if (err instanceof LLMError) {
|
|
581
|
+
return err;
|
|
582
|
+
}
|
|
538
583
|
if (err instanceof OpenAI.APIError) {
|
|
539
584
|
if (err.status === 413 /* PromptTooLong */ || err.status === 400 /* BadRequest */ && containsContextLengthError(err.message)) {
|
|
540
585
|
return new ContextTooLongError(`Context Too Long: ${err.message}`);
|
|
@@ -543,7 +588,11 @@ function classifyOpenAIError(err) {
|
|
|
543
588
|
return new AuthenticationError(`Invalid API key: ${err.message}`);
|
|
544
589
|
}
|
|
545
590
|
if (err.status === 429 /* RateLimitError */) {
|
|
546
|
-
|
|
591
|
+
const headers = err.headers;
|
|
592
|
+
return new RateLimitError(
|
|
593
|
+
"Rate limit error, please wait.",
|
|
594
|
+
headers instanceof Headers ? headers.get("retry-after") ?? void 0 : void 0
|
|
595
|
+
);
|
|
547
596
|
}
|
|
548
597
|
return new LLMError(`OpenAI API error (${asString(err.status)}): ${err.message}`);
|
|
549
598
|
}
|
|
@@ -558,16 +607,14 @@ function buildChatCompletionMessages(messages) {
|
|
|
558
607
|
params.push({
|
|
559
608
|
role: "assistant",
|
|
560
609
|
content: assistantText || null,
|
|
610
|
+
...reasoning ? { reasoning_content: reasoning } : {},
|
|
561
611
|
tool_calls: m.toolUses.map((tu) => ({
|
|
562
612
|
id: tu.toolUseId,
|
|
563
613
|
type: "function",
|
|
564
614
|
function: {
|
|
565
615
|
name: tu.toolName,
|
|
566
616
|
arguments: JSON.stringify(tu.arguments)
|
|
567
|
-
}
|
|
568
|
-
...reasoning ? {
|
|
569
|
-
reasoning_content: reasoning
|
|
570
|
-
} : {}
|
|
617
|
+
}
|
|
571
618
|
}))
|
|
572
619
|
});
|
|
573
620
|
} else if (m.toolResults && m.toolResults.length > 0) {
|
|
@@ -580,6 +627,12 @@ function buildChatCompletionMessages(messages) {
|
|
|
580
627
|
});
|
|
581
628
|
pendingRichParts.push(...collectRichParts(tr));
|
|
582
629
|
}
|
|
630
|
+
if (m.content.length > 0) {
|
|
631
|
+
const content = userPartsFor(m.content);
|
|
632
|
+
pendingRichParts.push(
|
|
633
|
+
...typeof content === "string" ? [{ type: "text", text: content }] : content
|
|
634
|
+
);
|
|
635
|
+
}
|
|
583
636
|
if (pendingRichParts.length > 0) {
|
|
584
637
|
params.push({
|
|
585
638
|
role: "user",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
mcpCallPermissionContent
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-KG4MJGKQ.js";
|
|
4
4
|
import {
|
|
5
5
|
createChildLogger,
|
|
6
6
|
strArg
|
|
@@ -12,9 +12,38 @@ import {
|
|
|
12
12
|
// src/permissions/checker.ts
|
|
13
13
|
import { readFileSync, writeFileSync, mkdirSync, statSync } from "fs";
|
|
14
14
|
import { homedir, tmpdir } from "os";
|
|
15
|
-
import { join, resolve, dirname } from "path";
|
|
15
|
+
import { join as join2, resolve as resolve2, dirname as dirname2 } from "path";
|
|
16
16
|
import yaml from "js-yaml";
|
|
17
17
|
import z, { parse } from "zod";
|
|
18
|
+
|
|
19
|
+
// src/utils/paths.ts
|
|
20
|
+
import { realpathSync } from "fs";
|
|
21
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "path";
|
|
22
|
+
function canonicalPath(path) {
|
|
23
|
+
let current = resolve(path);
|
|
24
|
+
const missing = [];
|
|
25
|
+
for (; ; ) {
|
|
26
|
+
try {
|
|
27
|
+
return join(realpathSync(current), ...missing);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT" && error.code !== "ENOTDIR") {
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
const parent = dirname(current);
|
|
33
|
+
if (parent === current) {
|
|
34
|
+
return resolve(path);
|
|
35
|
+
}
|
|
36
|
+
missing.unshift(basename(current));
|
|
37
|
+
current = parent;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function isPathWithin(root, path) {
|
|
42
|
+
const child = relative(root, path);
|
|
43
|
+
return child === "" || child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute(child);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// src/permissions/checker.ts
|
|
18
47
|
var log = createChildLogger({ module: "permissions" });
|
|
19
48
|
var DANGEROUS_PATTERNS = [];
|
|
20
49
|
var SAFE_PREFIXES = [
|
|
@@ -56,7 +85,8 @@ var CONTENT_FIELDS = {
|
|
|
56
85
|
WriteFile: "file_path",
|
|
57
86
|
EditFile: "file_path",
|
|
58
87
|
Glob: "pattern",
|
|
59
|
-
Grep: "pattern"
|
|
88
|
+
Grep: "pattern",
|
|
89
|
+
InstallSkill: "source"
|
|
60
90
|
};
|
|
61
91
|
var DEFAULT_DENY_WRITE = [
|
|
62
92
|
".swifty/config.yaml",
|
|
@@ -79,25 +109,26 @@ var PathSandbox = class {
|
|
|
79
109
|
denyWritePaths;
|
|
80
110
|
projectDir;
|
|
81
111
|
constructor(projectDir) {
|
|
82
|
-
this.projectDir =
|
|
112
|
+
this.projectDir = resolve2(projectDir);
|
|
83
113
|
this.allowedRoots = [this.projectDir, tmpdir()];
|
|
84
|
-
this.denyWritePaths = DEFAULT_DENY_WRITE.map((p) =>
|
|
114
|
+
this.denyWritePaths = DEFAULT_DENY_WRITE.map((p) => join2(this.projectDir, p));
|
|
85
115
|
}
|
|
86
116
|
addRoot(root) {
|
|
87
|
-
this.allowedRoots.push(
|
|
117
|
+
this.allowedRoots.push(resolve2(root));
|
|
88
118
|
}
|
|
89
119
|
// Add custom deny-write paths
|
|
90
120
|
addDenyWrite(path) {
|
|
91
|
-
this.denyWritePaths.push(
|
|
121
|
+
this.denyWritePaths.push(resolve2(path));
|
|
92
122
|
}
|
|
93
123
|
/**
|
|
94
124
|
* Check whether a path is in the deny-write list.
|
|
95
125
|
* denyWrite has the highest priority — even if the path is within an allowed root, writes are still denied.
|
|
96
126
|
*/
|
|
97
127
|
checkDenyWrite(filePath) {
|
|
98
|
-
const absolute =
|
|
128
|
+
const absolute = resolve2(this.projectDir, filePath);
|
|
129
|
+
const canonical = canonicalPath(absolute);
|
|
99
130
|
for (const denied of this.denyWritePaths) {
|
|
100
|
-
if (absolute
|
|
131
|
+
if (isPathWithin(denied, absolute) || isPathWithin(canonicalPath(denied), canonical)) {
|
|
101
132
|
return {
|
|
102
133
|
effect: "deny",
|
|
103
134
|
reason: `Path ${filePath} is in deny-write list`
|
|
@@ -107,9 +138,9 @@ var PathSandbox = class {
|
|
|
107
138
|
return null;
|
|
108
139
|
}
|
|
109
140
|
check(filePath) {
|
|
110
|
-
const absolute =
|
|
141
|
+
const absolute = canonicalPath(resolve2(this.projectDir, filePath));
|
|
111
142
|
for (const root of this.allowedRoots) {
|
|
112
|
-
if (
|
|
143
|
+
if (isPathWithin(canonicalPath(root), absolute)) {
|
|
113
144
|
return null;
|
|
114
145
|
}
|
|
115
146
|
}
|
|
@@ -190,9 +221,9 @@ var RuleEngine = class {
|
|
|
190
221
|
localPath;
|
|
191
222
|
cache = /* @__PURE__ */ new Map();
|
|
192
223
|
constructor(workDir) {
|
|
193
|
-
this.userPath =
|
|
194
|
-
this.projectPath =
|
|
195
|
-
this.localPath =
|
|
224
|
+
this.userPath = join2(homedir(), ".swifty", "permissions.yaml");
|
|
225
|
+
this.projectPath = join2(workDir, ".swifty", "permissions.yaml");
|
|
226
|
+
this.localPath = join2(workDir, ".swifty", "permissions.local.yaml");
|
|
196
227
|
}
|
|
197
228
|
// Read a single rules file; skips disk I/O and parsing on cache hit.
|
|
198
229
|
rulesFor(path) {
|
|
@@ -229,7 +260,7 @@ var RuleEngine = class {
|
|
|
229
260
|
// Persists a rule to the project-local YAML file in the `Tool(pattern)`
|
|
230
261
|
// format so "allow always" survives a restart.
|
|
231
262
|
appendLocalRule(rule) {
|
|
232
|
-
mkdirSync(
|
|
263
|
+
mkdirSync(dirname2(this.localPath), { recursive: true });
|
|
233
264
|
const rules = loadRulesFile(this.localPath);
|
|
234
265
|
const exists = rules.some(
|
|
235
266
|
(r) => r.tool === rule.tool && r.pattern === rule.pattern && r.effect === rule.effect
|
|
@@ -255,7 +286,7 @@ function detectDangerous(command) {
|
|
|
255
286
|
}
|
|
256
287
|
function isSafeCommand(command) {
|
|
257
288
|
const trimmed = command.trim();
|
|
258
|
-
if (trimmed.includes(">") || trimmed.includes("|") || trimmed.includes(";") || trimmed.includes("&&") || trimmed.includes("$(") || trimmed.includes("`")) {
|
|
289
|
+
if (trimmed.includes(">") || trimmed.includes("<") || /[\r\n&]/.test(trimmed) || trimmed.includes("|") || trimmed.includes(";") || trimmed.includes("&&") || trimmed.includes("$(") || trimmed.includes("`")) {
|
|
259
290
|
return false;
|
|
260
291
|
}
|
|
261
292
|
return SAFE_PREFIXES.some(
|
|
@@ -275,7 +306,14 @@ function modeDecide(mode, category) {
|
|
|
275
306
|
return category === "read" ? "allow" : "ask";
|
|
276
307
|
}
|
|
277
308
|
}
|
|
278
|
-
var PermissionChecker = class {
|
|
309
|
+
var PermissionChecker = class _PermissionChecker {
|
|
310
|
+
constructor(workDir, mode = "default") {
|
|
311
|
+
this.workDir = workDir;
|
|
312
|
+
this.mode = mode;
|
|
313
|
+
this.sandbox = new PathSandbox(workDir);
|
|
314
|
+
this.ruleEngine = new RuleEngine(workDir);
|
|
315
|
+
}
|
|
316
|
+
workDir;
|
|
279
317
|
mode;
|
|
280
318
|
planFilePath = "";
|
|
281
319
|
// Sandbox mode: when enabled, command-category tools run through OS sandbox isolation, with optional auto-allow
|
|
@@ -283,18 +321,24 @@ var PermissionChecker = class {
|
|
|
283
321
|
sandboxAutoAllow = false;
|
|
284
322
|
sandbox;
|
|
285
323
|
ruleEngine;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
324
|
+
forWorkDir(workDir) {
|
|
325
|
+
const checker = new _PermissionChecker(workDir, this.mode);
|
|
326
|
+
checker.ruleEngine = this.ruleEngine;
|
|
327
|
+
checker.sandboxEnabled = this.sandboxEnabled;
|
|
328
|
+
checker.sandboxAutoAllow = this.sandboxAutoAllow;
|
|
329
|
+
return checker;
|
|
290
330
|
}
|
|
291
331
|
check(toolName, category, args) {
|
|
292
332
|
const content = extractContent(toolName, args);
|
|
293
333
|
let snapshot = null;
|
|
294
334
|
const rules = () => snapshot ??= this.ruleEngine.snapshot();
|
|
335
|
+
const explicitEffect = evaluateRules(rules(), toolName, content);
|
|
336
|
+
if (explicitEffect === "deny" || explicitEffect === "ask") {
|
|
337
|
+
return { effect: explicitEffect, reason: `Permission rule: ${explicitEffect}` };
|
|
338
|
+
}
|
|
295
339
|
if (this.mode === "plan" && (toolName === "WriteFile" || toolName === "EditFile")) {
|
|
296
340
|
const path = strArg(args, "file_path", "");
|
|
297
|
-
if (
|
|
341
|
+
if (this.planFilePath && canonicalPath(resolve2(this.workDir, path)) === canonicalPath(resolve2(this.workDir, this.planFilePath)) && !this.sandbox.checkDenyWrite(path)) {
|
|
298
342
|
return {
|
|
299
343
|
effect: "allow",
|
|
300
344
|
reason: "Plan file write allowed in plan mode"
|
|
@@ -377,13 +421,13 @@ var PermissionChecker = class {
|
|
|
377
421
|
const isFilePath = toolName === "ReadFile" || toolName === "WriteFile" || toolName === "EditFile";
|
|
378
422
|
let pattern;
|
|
379
423
|
if (isFilePath && content) {
|
|
380
|
-
const abs =
|
|
424
|
+
const abs = resolve2(this.workDir, content);
|
|
381
425
|
let isDir = false;
|
|
382
426
|
try {
|
|
383
427
|
isDir = statSync(abs).isDirectory();
|
|
384
428
|
} catch {
|
|
385
429
|
}
|
|
386
|
-
pattern =
|
|
430
|
+
pattern = join2(isDir ? abs : dirname2(abs), "*");
|
|
387
431
|
} else {
|
|
388
432
|
const words = content.trim().split(/\s+/).slice(0, 2);
|
|
389
433
|
pattern = words.join(" ") + "*";
|
|
@@ -417,6 +461,8 @@ var PermissionChecker = class {
|
|
|
417
461
|
};
|
|
418
462
|
|
|
419
463
|
export {
|
|
464
|
+
canonicalPath,
|
|
465
|
+
isPathWithin,
|
|
420
466
|
extractContent,
|
|
421
467
|
PathSandbox,
|
|
422
468
|
evaluateRules,
|