@zaly/agent 0.0.3
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/LICENSE +21 -0
- package/README.md +50 -0
- package/dist/agent-BaD2kax9.mjs +68 -0
- package/dist/agent-BaD2kax9.mjs.map +1 -0
- package/dist/agent-GDLdDc2w.mjs +902 -0
- package/dist/agent-GDLdDc2w.mjs.map +1 -0
- package/dist/bash-QNMTQ1PD.mjs +142 -0
- package/dist/bash-QNMTQ1PD.mjs.map +1 -0
- package/dist/compactions-3AjPqjeZ.mjs +190 -0
- package/dist/compactions-3AjPqjeZ.mjs.map +1 -0
- package/dist/edit-Qxl4w1C2.mjs +170 -0
- package/dist/edit-Qxl4w1C2.mjs.map +1 -0
- package/dist/env-UUFv5Mye.mjs +30 -0
- package/dist/env-UUFv5Mye.mjs.map +1 -0
- package/dist/fetch-D8cmmA3E.mjs +111 -0
- package/dist/fetch-D8cmmA3E.mjs.map +1 -0
- package/dist/find-Bah9SKWX.mjs +87 -0
- package/dist/find-Bah9SKWX.mjs.map +1 -0
- package/dist/grep-B2Ax7ewy.mjs +244 -0
- package/dist/grep-B2Ax7ewy.mjs.map +1 -0
- package/dist/index-node-esm-D6YQRKEz.mjs +1537 -0
- package/dist/index-node-esm-D6YQRKEz.mjs.map +1 -0
- package/dist/index.d.mts +1605 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +948 -0
- package/dist/index.mjs.map +1 -0
- package/dist/jsonl-Dw_VBp46.mjs +134 -0
- package/dist/jsonl-Dw_VBp46.mjs.map +1 -0
- package/dist/manager-D-lWBTHG.mjs +271 -0
- package/dist/manager-D-lWBTHG.mjs.map +1 -0
- package/dist/markdown-Bv4J8cg3.mjs +53 -0
- package/dist/markdown-Bv4J8cg3.mjs.map +1 -0
- package/dist/masker-Bv9KjkDF.mjs +396 -0
- package/dist/masker-Bv9KjkDF.mjs.map +1 -0
- package/dist/model-_3ZA2xqm.mjs +27 -0
- package/dist/model-_3ZA2xqm.mjs.map +1 -0
- package/dist/notify-DpG_m9iP.mjs +96 -0
- package/dist/notify-DpG_m9iP.mjs.map +1 -0
- package/dist/read-Djsx2NZl.mjs +205 -0
- package/dist/read-Djsx2NZl.mjs.map +1 -0
- package/dist/registry-CFmR5G3J.mjs +564 -0
- package/dist/registry-CFmR5G3J.mjs.map +1 -0
- package/dist/search-CRw247ES.mjs +121 -0
- package/dist/search-CRw247ES.mjs.map +1 -0
- package/dist/session/claude.d.mts +59 -0
- package/dist/session/claude.d.mts.map +1 -0
- package/dist/session/claude.mjs +455 -0
- package/dist/session/claude.mjs.map +1 -0
- package/dist/session-CUUdWFC5.mjs +450 -0
- package/dist/session-CUUdWFC5.mjs.map +1 -0
- package/dist/session-Ds5nJz5A.d.mts +346 -0
- package/dist/session-Ds5nJz5A.d.mts.map +1 -0
- package/dist/session.d.mts +37 -0
- package/dist/session.d.mts.map +1 -0
- package/dist/session.mjs +106 -0
- package/dist/session.mjs.map +1 -0
- package/dist/subagent-baqzjRMc.mjs +136 -0
- package/dist/subagent-baqzjRMc.mjs.map +1 -0
- package/dist/swarm-BJ3gQA1Z.mjs +194 -0
- package/dist/swarm-BJ3gQA1Z.mjs.map +1 -0
- package/dist/swarm-BuFx_xeY.mjs +109 -0
- package/dist/swarm-BuFx_xeY.mjs.map +1 -0
- package/dist/tasks-D8rARHZ1.mjs +65 -0
- package/dist/tasks-D8rARHZ1.mjs.map +1 -0
- package/dist/tasks-WmC270X2.mjs +615 -0
- package/dist/tasks-WmC270X2.mjs.map +1 -0
- package/dist/tools-DW1dbcCz.mjs +695 -0
- package/dist/tools-DW1dbcCz.mjs.map +1 -0
- package/dist/truncate-BSMXgiXj.mjs +102 -0
- package/dist/truncate-BSMXgiXj.mjs.map +1 -0
- package/dist/uuid-ByXP5SO5.mjs +59 -0
- package/dist/uuid-ByXP5SO5.mjs.map +1 -0
- package/dist/wakeup-BYqTnfSp.mjs +29 -0
- package/dist/wakeup-BYqTnfSp.mjs.map +1 -0
- package/dist/write-D3L-92vj.mjs +89 -0
- package/dist/write-D3L-92vj.mjs.map +1 -0
- package/package.json +63 -0
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
import { a as __toESM, n as parseBash, r as __commonJSMin, t as TOOLS } from "./tools-DW1dbcCz.mjs";
|
|
2
|
+
import { normPath, prettyPath } from "@zaly/shared";
|
|
3
|
+
import { createRegistry } from "@zaly/shared/registry";
|
|
4
|
+
import { dirname, isAbsolute, relative } from "pathe";
|
|
5
|
+
//#region src/permissions/handlers/bash.ts
|
|
6
|
+
/**
|
|
7
|
+
* Handler for `Rule<"bash">` — evaluates a bash command line against
|
|
8
|
+
* pattern rules, then delegates each segment's inferred and explicit
|
|
9
|
+
* file paths to the file handler via `ctx.validate("read"|"write", …)`.
|
|
10
|
+
*
|
|
11
|
+
* Composition keeps file-rule logic in one place: the bash handler
|
|
12
|
+
* doesn't know about workspaces, sensitive files, or path-pattern
|
|
13
|
+
* matching — it just asks the registered handler for those scopes.
|
|
14
|
+
*
|
|
15
|
+
* Sources of file paths per segment:
|
|
16
|
+
* - `seg.reads` / `seg.writes` — explicit `<` / `>` / `>>` redirects
|
|
17
|
+
* extracted by the parser.
|
|
18
|
+
* - `TOOLS[cmd].reads/writes(args)` — inferred from built-in tool
|
|
19
|
+
* specs (e.g. `cat foo.txt` reads `foo.txt`, `tee out` writes `out`).
|
|
20
|
+
*
|
|
21
|
+
* Suggestions and reasons from inner `validate` calls bubble up so the
|
|
22
|
+
* prompt UI can offer "allow `Bash(cat:*)`" alongside "add ~/Documents
|
|
23
|
+
* as workspace" in a single ask.
|
|
24
|
+
*/
|
|
25
|
+
const bashHandler = { validate(input, ctx) {
|
|
26
|
+
const parsed = parseBash(input);
|
|
27
|
+
if (!parsed.ok) return {
|
|
28
|
+
ask: `Allow this bash command? (${parsed.reason})`,
|
|
29
|
+
reason: parsed.reason,
|
|
30
|
+
verdict: "ask"
|
|
31
|
+
};
|
|
32
|
+
let verdict = "allow";
|
|
33
|
+
let reason;
|
|
34
|
+
const suggestions = [];
|
|
35
|
+
for (const seg of parsed.segments) {
|
|
36
|
+
const cmdVerdict = resolveRules(ctx.rules, seg) ?? "ask";
|
|
37
|
+
if (seg.hasCommandSubst && cmdVerdict !== "allow") return {
|
|
38
|
+
ask: `Allow command substitution in \`${seg.cmd}\`?`,
|
|
39
|
+
reason: "command substitution not allowed",
|
|
40
|
+
verdict: "ask"
|
|
41
|
+
};
|
|
42
|
+
const spec = TOOLS[seg.cmd];
|
|
43
|
+
if (spec?.unsafe?.(seg.args) && cmdVerdict !== "allow") return {
|
|
44
|
+
ask: `Allow invocation of \`${seg.cmd}\`? (unsafe)`,
|
|
45
|
+
reason: `${seg.cmd}: invocation mode not safely modelled`,
|
|
46
|
+
verdict: "ask"
|
|
47
|
+
};
|
|
48
|
+
let segVerdict = cmdVerdict;
|
|
49
|
+
let segReason = cmdVerdict === "allow" ? void 0 : `segment "${seg.cmd}" → ${cmdVerdict}`;
|
|
50
|
+
if (cmdVerdict === "ask") suggestions.push({
|
|
51
|
+
kind: "rule",
|
|
52
|
+
pattern: seg.args.length > 0 ? `${seg.cmd}:*` : seg.cmd,
|
|
53
|
+
scope: "bash"
|
|
54
|
+
});
|
|
55
|
+
const reads = [...spec?.reads?.(seg.args) ?? [], ...seg.reads];
|
|
56
|
+
const writes = [...spec?.writes?.(seg.args) ?? [], ...seg.writes.map((w) => w.path)];
|
|
57
|
+
for (const path of reads) {
|
|
58
|
+
if (seg.dyn?.has(path)) {
|
|
59
|
+
segVerdict = combine(segVerdict, "ask");
|
|
60
|
+
segReason ??= `${seg.cmd}: read ${path}: dynamic path`;
|
|
61
|
+
suggestions.push({
|
|
62
|
+
kind: "rule",
|
|
63
|
+
pattern: `${seg.cmd}:*`,
|
|
64
|
+
scope: "bash"
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const r = ctx.validate("read", path);
|
|
69
|
+
segVerdict = combine(segVerdict, r.verdict);
|
|
70
|
+
if (r.verdict !== "allow") {
|
|
71
|
+
segReason ??= `${seg.cmd}: read ${path}: ${r.reason}`;
|
|
72
|
+
if (r.suggestions) suggestions.push(...r.suggestions);
|
|
73
|
+
}
|
|
74
|
+
if (segVerdict === "deny") break;
|
|
75
|
+
}
|
|
76
|
+
if (segVerdict !== "deny") for (const path of writes) {
|
|
77
|
+
if (seg.dyn?.has(path)) {
|
|
78
|
+
segVerdict = combine(segVerdict, "ask");
|
|
79
|
+
segReason ??= `${seg.cmd}: write ${path}: dynamic path`;
|
|
80
|
+
suggestions.push({
|
|
81
|
+
kind: "rule",
|
|
82
|
+
pattern: `${seg.cmd}:*`,
|
|
83
|
+
scope: "bash"
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const w = ctx.validate("write", path);
|
|
88
|
+
segVerdict = combine(segVerdict, w.verdict);
|
|
89
|
+
if (w.verdict !== "allow") {
|
|
90
|
+
segReason ??= `${seg.cmd}: write ${path}: ${w.reason}`;
|
|
91
|
+
if (w.suggestions) suggestions.push(...w.suggestions);
|
|
92
|
+
}
|
|
93
|
+
if (segVerdict === "deny") break;
|
|
94
|
+
}
|
|
95
|
+
verdict = combine(verdict, segVerdict);
|
|
96
|
+
if (verdict !== "allow" && reason === void 0) reason = segReason;
|
|
97
|
+
if (verdict === "deny") break;
|
|
98
|
+
}
|
|
99
|
+
return verdict === "allow" ? { verdict: "allow" } : {
|
|
100
|
+
ask: `Allow this bash command?`,
|
|
101
|
+
reason: reason ?? "no rule matched",
|
|
102
|
+
suggestions: suggestions.length > 0 ? suggestions : void 0,
|
|
103
|
+
verdict
|
|
104
|
+
};
|
|
105
|
+
} };
|
|
106
|
+
/** Combine two verdicts: deny dominates, ask beats allow. */
|
|
107
|
+
function combine(a, b) {
|
|
108
|
+
if (a === "deny" || b === "deny") return "deny";
|
|
109
|
+
if (a === "ask" || b === "ask") return "ask";
|
|
110
|
+
return "allow";
|
|
111
|
+
}
|
|
112
|
+
const patternCache = /* @__PURE__ */ new Map();
|
|
113
|
+
function parsePattern(pattern) {
|
|
114
|
+
const cached = patternCache.get(pattern);
|
|
115
|
+
if (cached) return cached;
|
|
116
|
+
let body = pattern.trim();
|
|
117
|
+
let trailingWildcard = false;
|
|
118
|
+
if (body.endsWith(":*")) {
|
|
119
|
+
trailingWildcard = true;
|
|
120
|
+
body = body.slice(0, -2);
|
|
121
|
+
}
|
|
122
|
+
const tokens = body.split(/\s+/).filter((s) => s.length > 0);
|
|
123
|
+
const result = {
|
|
124
|
+
cmd: tokens[0] ?? "",
|
|
125
|
+
parts: tokens.slice(1),
|
|
126
|
+
trailingWildcard
|
|
127
|
+
};
|
|
128
|
+
patternCache.set(pattern, result);
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
131
|
+
function matchPart(part, value) {
|
|
132
|
+
if (part.endsWith("*")) return value.startsWith(part.slice(0, -1));
|
|
133
|
+
return part === value;
|
|
134
|
+
}
|
|
135
|
+
/** Test whether a parsed segment matches a rule pattern. */
|
|
136
|
+
function matchRule(rule, seg) {
|
|
137
|
+
const p = parsePattern(rule.pattern);
|
|
138
|
+
if (p.cmd === "*" && p.parts.length === 0) return true;
|
|
139
|
+
if (p.cmd !== seg.cmd) return false;
|
|
140
|
+
if (seg.args.length < p.parts.length) return false;
|
|
141
|
+
for (let i = 0; i < p.parts.length; i++) if (!matchPart(p.parts[i], seg.args[i])) return false;
|
|
142
|
+
if (p.trailingWildcard) return true;
|
|
143
|
+
return seg.args.length === p.parts.length;
|
|
144
|
+
}
|
|
145
|
+
/** First-match-wins resolution. Returns `undefined` if no rule matches. */
|
|
146
|
+
function resolveRules(rules, seg) {
|
|
147
|
+
for (const r of rules) if (matchRule(r, seg)) return r.policy;
|
|
148
|
+
}
|
|
149
|
+
//#endregion
|
|
150
|
+
//#region src/permissions/handlers/files.ts
|
|
151
|
+
var import_ignore = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((exports, module) => {
|
|
152
|
+
function makeArray(subject) {
|
|
153
|
+
return Array.isArray(subject) ? subject : [subject];
|
|
154
|
+
}
|
|
155
|
+
const UNDEFINED = void 0;
|
|
156
|
+
const EMPTY = "";
|
|
157
|
+
const SPACE = " ";
|
|
158
|
+
const ESCAPE = "\\";
|
|
159
|
+
const REGEX_TEST_BLANK_LINE = /^\s+$/;
|
|
160
|
+
const REGEX_INVALID_TRAILING_BACKSLASH = /(?:[^\\]|^)\\$/;
|
|
161
|
+
const REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION = /^\\!/;
|
|
162
|
+
const REGEX_REPLACE_LEADING_EXCAPED_HASH = /^\\#/;
|
|
163
|
+
const REGEX_SPLITALL_CRLF = /\r?\n/g;
|
|
164
|
+
const REGEX_TEST_INVALID_PATH = /^\.{0,2}\/|^\.{1,2}$/;
|
|
165
|
+
const REGEX_TEST_TRAILING_SLASH = /\/$/;
|
|
166
|
+
const SLASH = "/";
|
|
167
|
+
let TMP_KEY_IGNORE = "node-ignore";
|
|
168
|
+
/* istanbul ignore else */
|
|
169
|
+
if (typeof Symbol !== "undefined") TMP_KEY_IGNORE = Symbol.for("node-ignore");
|
|
170
|
+
const KEY_IGNORE = TMP_KEY_IGNORE;
|
|
171
|
+
const define = (object, key, value) => {
|
|
172
|
+
Object.defineProperty(object, key, { value });
|
|
173
|
+
return value;
|
|
174
|
+
};
|
|
175
|
+
const REGEX_REGEXP_RANGE = /([0-z])-([0-z])/g;
|
|
176
|
+
const RETURN_FALSE = () => false;
|
|
177
|
+
const sanitizeRange = (range) => range.replace(REGEX_REGEXP_RANGE, (match, from, to) => from.charCodeAt(0) <= to.charCodeAt(0) ? match : EMPTY);
|
|
178
|
+
const cleanRangeBackSlash = (slashes) => {
|
|
179
|
+
const { length } = slashes;
|
|
180
|
+
return slashes.slice(0, length - length % 2);
|
|
181
|
+
};
|
|
182
|
+
const REPLACERS = [
|
|
183
|
+
[/^\uFEFF/, () => EMPTY],
|
|
184
|
+
[/((?:\\\\)*?)(\\?\s+)$/, (_, m1, m2) => m1 + (m2.indexOf("\\") === 0 ? SPACE : EMPTY)],
|
|
185
|
+
[/(\\+?)\s/g, (_, m1) => {
|
|
186
|
+
const { length } = m1;
|
|
187
|
+
return m1.slice(0, length - length % 2) + SPACE;
|
|
188
|
+
}],
|
|
189
|
+
[/[\\$.|*+(){^]/g, (match) => `\\${match}`],
|
|
190
|
+
[/(?!\\)\?/g, () => "[^/]"],
|
|
191
|
+
[/^\//, () => "^"],
|
|
192
|
+
[/\//g, () => "\\/"],
|
|
193
|
+
[/^\^*\\\*\\\*\\\//, () => "^(?:.*\\/)?"],
|
|
194
|
+
[/^(?=[^^])/, function startingReplacer() {
|
|
195
|
+
return !/\/(?!$)/.test(this) ? "(?:^|\\/)" : "^";
|
|
196
|
+
}],
|
|
197
|
+
[/\\\/\\\*\\\*(?=\\\/|$)/g, (_, index, str) => index + 6 < str.length ? "(?:\\/[^\\/]+)*" : "\\/.+"],
|
|
198
|
+
[/(^|[^\\]+)(\\\*)+(?=.+)/g, (_, p1, p2) => {
|
|
199
|
+
return p1 + p2.replace(/\\\*/g, "[^\\/]*");
|
|
200
|
+
}],
|
|
201
|
+
[/\\\\\\(?=[$.|*+(){^])/g, () => ESCAPE],
|
|
202
|
+
[/\\\\/g, () => ESCAPE],
|
|
203
|
+
[/(\\)?\[([^\]/]*?)(\\*)($|\])/g, (match, leadEscape, range, endEscape, close) => leadEscape === ESCAPE ? `\\[${range}${cleanRangeBackSlash(endEscape)}${close}` : close === "]" ? endEscape.length % 2 === 0 ? `[${sanitizeRange(range)}${endEscape}]` : "[]" : "[]"],
|
|
204
|
+
[/(?:[^*])$/, (match) => /\/$/.test(match) ? `${match}$` : `${match}(?=$|\\/$)`]
|
|
205
|
+
];
|
|
206
|
+
const REGEX_REPLACE_TRAILING_WILDCARD = /(^|\\\/)?\\\*$/;
|
|
207
|
+
const MODE_IGNORE = "regex";
|
|
208
|
+
const MODE_CHECK_IGNORE = "checkRegex";
|
|
209
|
+
const TRAILING_WILD_CARD_REPLACERS = {
|
|
210
|
+
[MODE_IGNORE](_, p1) {
|
|
211
|
+
return `${p1 ? `${p1}[^/]+` : "[^/]*"}(?=$|\\/$)`;
|
|
212
|
+
},
|
|
213
|
+
[MODE_CHECK_IGNORE](_, p1) {
|
|
214
|
+
return `${p1 ? `${p1}[^/]*` : "[^/]*"}(?=$|\\/$)`;
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
const makeRegexPrefix = (pattern) => REPLACERS.reduce((prev, [matcher, replacer]) => prev.replace(matcher, replacer.bind(pattern)), pattern);
|
|
218
|
+
const isString = (subject) => typeof subject === "string";
|
|
219
|
+
const checkPattern = (pattern) => pattern && isString(pattern) && !REGEX_TEST_BLANK_LINE.test(pattern) && !REGEX_INVALID_TRAILING_BACKSLASH.test(pattern) && pattern.indexOf("#") !== 0;
|
|
220
|
+
const splitPattern = (pattern) => pattern.split(REGEX_SPLITALL_CRLF).filter(Boolean);
|
|
221
|
+
var IgnoreRule = class {
|
|
222
|
+
constructor(pattern, mark, body, ignoreCase, negative, prefix) {
|
|
223
|
+
this.pattern = pattern;
|
|
224
|
+
this.mark = mark;
|
|
225
|
+
this.negative = negative;
|
|
226
|
+
define(this, "body", body);
|
|
227
|
+
define(this, "ignoreCase", ignoreCase);
|
|
228
|
+
define(this, "regexPrefix", prefix);
|
|
229
|
+
}
|
|
230
|
+
get regex() {
|
|
231
|
+
const key = "_regex";
|
|
232
|
+
if (this[key]) return this[key];
|
|
233
|
+
return this._make(MODE_IGNORE, key);
|
|
234
|
+
}
|
|
235
|
+
get checkRegex() {
|
|
236
|
+
const key = "_checkRegex";
|
|
237
|
+
if (this[key]) return this[key];
|
|
238
|
+
return this._make(MODE_CHECK_IGNORE, key);
|
|
239
|
+
}
|
|
240
|
+
_make(mode, key) {
|
|
241
|
+
const str = this.regexPrefix.replace(REGEX_REPLACE_TRAILING_WILDCARD, TRAILING_WILD_CARD_REPLACERS[mode]);
|
|
242
|
+
const regex = this.ignoreCase ? new RegExp(str, "i") : new RegExp(str);
|
|
243
|
+
return define(this, key, regex);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
const createRule = ({ pattern, mark }, ignoreCase) => {
|
|
247
|
+
let negative = false;
|
|
248
|
+
let body = pattern;
|
|
249
|
+
if (body.indexOf("!") === 0) {
|
|
250
|
+
negative = true;
|
|
251
|
+
body = body.substr(1);
|
|
252
|
+
}
|
|
253
|
+
body = body.replace(REGEX_REPLACE_LEADING_EXCAPED_EXCLAMATION, "!").replace(REGEX_REPLACE_LEADING_EXCAPED_HASH, "#");
|
|
254
|
+
const regexPrefix = makeRegexPrefix(body);
|
|
255
|
+
return new IgnoreRule(pattern, mark, body, ignoreCase, negative, regexPrefix);
|
|
256
|
+
};
|
|
257
|
+
var RuleManager = class {
|
|
258
|
+
constructor(ignoreCase) {
|
|
259
|
+
this._ignoreCase = ignoreCase;
|
|
260
|
+
this._rules = [];
|
|
261
|
+
}
|
|
262
|
+
_add(pattern) {
|
|
263
|
+
if (pattern && pattern[KEY_IGNORE]) {
|
|
264
|
+
this._rules = this._rules.concat(pattern._rules._rules);
|
|
265
|
+
this._added = true;
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (isString(pattern)) pattern = { pattern };
|
|
269
|
+
if (checkPattern(pattern.pattern)) {
|
|
270
|
+
const rule = createRule(pattern, this._ignoreCase);
|
|
271
|
+
this._added = true;
|
|
272
|
+
this._rules.push(rule);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
add(pattern) {
|
|
276
|
+
this._added = false;
|
|
277
|
+
makeArray(isString(pattern) ? splitPattern(pattern) : pattern).forEach(this._add, this);
|
|
278
|
+
return this._added;
|
|
279
|
+
}
|
|
280
|
+
test(path, checkUnignored, mode) {
|
|
281
|
+
let ignored = false;
|
|
282
|
+
let unignored = false;
|
|
283
|
+
let matchedRule;
|
|
284
|
+
this._rules.forEach((rule) => {
|
|
285
|
+
const { negative } = rule;
|
|
286
|
+
if (unignored === negative && ignored !== unignored || negative && !ignored && !unignored && !checkUnignored) return;
|
|
287
|
+
if (!rule[mode].test(path)) return;
|
|
288
|
+
ignored = !negative;
|
|
289
|
+
unignored = negative;
|
|
290
|
+
matchedRule = negative ? UNDEFINED : rule;
|
|
291
|
+
});
|
|
292
|
+
const ret = {
|
|
293
|
+
ignored,
|
|
294
|
+
unignored
|
|
295
|
+
};
|
|
296
|
+
if (matchedRule) ret.rule = matchedRule;
|
|
297
|
+
return ret;
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
const throwError = (message, Ctor) => {
|
|
301
|
+
throw new Ctor(message);
|
|
302
|
+
};
|
|
303
|
+
const checkPath = (path, originalPath, doThrow) => {
|
|
304
|
+
if (!isString(path)) return doThrow(`path must be a string, but got \`${originalPath}\``, TypeError);
|
|
305
|
+
if (!path) return doThrow(`path must not be empty`, TypeError);
|
|
306
|
+
if (checkPath.isNotRelative(path)) return doThrow(`path should be a \`path.relative()\`d string, but got "${originalPath}"`, RangeError);
|
|
307
|
+
return true;
|
|
308
|
+
};
|
|
309
|
+
const isNotRelative = (path) => REGEX_TEST_INVALID_PATH.test(path);
|
|
310
|
+
checkPath.isNotRelative = isNotRelative;
|
|
311
|
+
/* istanbul ignore next */
|
|
312
|
+
checkPath.convert = (p) => p;
|
|
313
|
+
var Ignore = class {
|
|
314
|
+
constructor({ ignorecase = true, ignoreCase = ignorecase, allowRelativePaths = false } = {}) {
|
|
315
|
+
define(this, KEY_IGNORE, true);
|
|
316
|
+
this._rules = new RuleManager(ignoreCase);
|
|
317
|
+
this._strictPathCheck = !allowRelativePaths;
|
|
318
|
+
this._initCache();
|
|
319
|
+
}
|
|
320
|
+
_initCache() {
|
|
321
|
+
this._ignoreCache = Object.create(null);
|
|
322
|
+
this._testCache = Object.create(null);
|
|
323
|
+
}
|
|
324
|
+
add(pattern) {
|
|
325
|
+
if (this._rules.add(pattern)) this._initCache();
|
|
326
|
+
return this;
|
|
327
|
+
}
|
|
328
|
+
addPattern(pattern) {
|
|
329
|
+
return this.add(pattern);
|
|
330
|
+
}
|
|
331
|
+
_test(originalPath, cache, checkUnignored, slices) {
|
|
332
|
+
const path = originalPath && checkPath.convert(originalPath);
|
|
333
|
+
checkPath(path, originalPath, this._strictPathCheck ? throwError : RETURN_FALSE);
|
|
334
|
+
return this._t(path, cache, checkUnignored, slices);
|
|
335
|
+
}
|
|
336
|
+
checkIgnore(path) {
|
|
337
|
+
if (!REGEX_TEST_TRAILING_SLASH.test(path)) return this.test(path);
|
|
338
|
+
const slices = path.split(SLASH).filter(Boolean);
|
|
339
|
+
slices.pop();
|
|
340
|
+
if (slices.length) {
|
|
341
|
+
const parent = this._t(slices.join(SLASH) + SLASH, this._testCache, true, slices);
|
|
342
|
+
if (parent.ignored) return parent;
|
|
343
|
+
}
|
|
344
|
+
return this._rules.test(path, false, MODE_CHECK_IGNORE);
|
|
345
|
+
}
|
|
346
|
+
_t(path, cache, checkUnignored, slices) {
|
|
347
|
+
if (path in cache) return cache[path];
|
|
348
|
+
if (!slices) slices = path.split(SLASH).filter(Boolean);
|
|
349
|
+
slices.pop();
|
|
350
|
+
if (!slices.length) return cache[path] = this._rules.test(path, checkUnignored, MODE_IGNORE);
|
|
351
|
+
const parent = this._t(slices.join(SLASH) + SLASH, cache, checkUnignored, slices);
|
|
352
|
+
return cache[path] = parent.ignored ? parent : this._rules.test(path, checkUnignored, MODE_IGNORE);
|
|
353
|
+
}
|
|
354
|
+
ignores(path) {
|
|
355
|
+
return this._test(path, this._ignoreCache, false).ignored;
|
|
356
|
+
}
|
|
357
|
+
createFilter() {
|
|
358
|
+
return (path) => !this.ignores(path);
|
|
359
|
+
}
|
|
360
|
+
filter(paths) {
|
|
361
|
+
return makeArray(paths).filter(this.createFilter());
|
|
362
|
+
}
|
|
363
|
+
test(path) {
|
|
364
|
+
return this._test(path, this._testCache, true);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
const factory = (options) => new Ignore(options);
|
|
368
|
+
const isPathValid = (path) => checkPath(path && checkPath.convert(path), path, RETURN_FALSE);
|
|
369
|
+
/* istanbul ignore next */
|
|
370
|
+
const setupWindows = () => {
|
|
371
|
+
const makePosix = (str) => /^\\\\\?\\/.test(str) || /["<>|\u0000-\u001F]+/u.test(str) ? str : str.replace(/\\/g, "/");
|
|
372
|
+
checkPath.convert = makePosix;
|
|
373
|
+
const REGEX_TEST_WINDOWS_PATH_ABSOLUTE = /^[a-z]:\//i;
|
|
374
|
+
checkPath.isNotRelative = (path) => REGEX_TEST_WINDOWS_PATH_ABSOLUTE.test(path) || isNotRelative(path);
|
|
375
|
+
};
|
|
376
|
+
/* istanbul ignore next */
|
|
377
|
+
if (typeof process !== "undefined" && process.platform === "win32") setupWindows();
|
|
378
|
+
module.exports = factory;
|
|
379
|
+
factory.default = factory;
|
|
380
|
+
module.exports.isPathValid = isPathValid;
|
|
381
|
+
define(module.exports, Symbol.for("setupWindows"), setupWindows);
|
|
382
|
+
})))(), 1);
|
|
383
|
+
/**
|
|
384
|
+
* Handler for `Rule<"read" | "write">` — one impl serves both scopes
|
|
385
|
+
* because they share the same matcher and the same workspace state.
|
|
386
|
+
*
|
|
387
|
+
* Algorithm:
|
|
388
|
+
* 1. Normalize candidate to an absolute path.
|
|
389
|
+
* 2. Sensitive-file check (env, ssh keys, credentials) → deny.
|
|
390
|
+
* 3. Find the longest-prefix workspace containing the path. None →
|
|
391
|
+
* ask, with a `kind: "workspace"` suggestion for the containing
|
|
392
|
+
* directory.
|
|
393
|
+
* 4. Inside a workspace: resolve the first matching rule against the
|
|
394
|
+
* workspace-relative path. Earlier rules win.
|
|
395
|
+
* 5. Default when no rule matches but the path is inside a workspace:
|
|
396
|
+
* `allow` for reads (workspace = trust signal), `ask` for writes
|
|
397
|
+
* (mutating ops escalate by default; users add `Write(...)` rules
|
|
398
|
+
* to whitelist).
|
|
399
|
+
*
|
|
400
|
+
* Pattern grammar (per Claude-Code conventions):
|
|
401
|
+
* //abs/path absolute filesystem path
|
|
402
|
+
* ~/path path under home directory
|
|
403
|
+
* /path or path workspace-relative (passed through to gitignore)
|
|
404
|
+
*/
|
|
405
|
+
const fileHandler = { validate(input, ctx) {
|
|
406
|
+
const abs = normPath(ctx.cwd, input);
|
|
407
|
+
if (isSensitiveFile(abs)) return {
|
|
408
|
+
reason: `${abs}: sensitive file`,
|
|
409
|
+
verdict: "deny"
|
|
410
|
+
};
|
|
411
|
+
const ws = longestWorkspace(abs, ctx.workspaces);
|
|
412
|
+
const inWorkspace = ws !== void 0;
|
|
413
|
+
if (inWorkspace && abs === ws) return { verdict: "allow" };
|
|
414
|
+
const base = ws ?? "/";
|
|
415
|
+
const rel = relative(base, abs);
|
|
416
|
+
const verb = ctx.scope === "read" ? "reading" : "writing";
|
|
417
|
+
const rule = resolveRule$1(base, rel, ctx.rules);
|
|
418
|
+
if (rule?.policy === "deny") return {
|
|
419
|
+
reason: `${abs}: denied by rule`,
|
|
420
|
+
verdict: "deny"
|
|
421
|
+
};
|
|
422
|
+
if (rule?.policy === "ask") return {
|
|
423
|
+
ask: `Allow ${verb} ${prettyPath(abs)}?`,
|
|
424
|
+
reason: `${abs}: rule requires confirmation`,
|
|
425
|
+
suggestions: [{
|
|
426
|
+
kind: "rule",
|
|
427
|
+
pattern: `/${rel}`,
|
|
428
|
+
scope: ctx.scope
|
|
429
|
+
}],
|
|
430
|
+
verdict: "ask"
|
|
431
|
+
};
|
|
432
|
+
if (rule?.policy === "allow") return { verdict: "allow" };
|
|
433
|
+
if (!inWorkspace) {
|
|
434
|
+
const parent = dirname(abs);
|
|
435
|
+
return {
|
|
436
|
+
ask: `Allow ${verb} ${prettyPath(abs)}? (not in any workspace)`,
|
|
437
|
+
reason: `${abs}: outside any workspace`,
|
|
438
|
+
suggestions: [{
|
|
439
|
+
description: `add ${parent} as workspace`,
|
|
440
|
+
kind: "workspace",
|
|
441
|
+
path: parent
|
|
442
|
+
}],
|
|
443
|
+
verdict: "ask"
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (ctx.scope === "read") return { verdict: "allow" };
|
|
447
|
+
return {
|
|
448
|
+
ask: `Allow ${verb} ${prettyPath(abs)}?`,
|
|
449
|
+
reason: `${abs}: write requires confirmation`,
|
|
450
|
+
suggestions: [{
|
|
451
|
+
kind: "rule",
|
|
452
|
+
pattern: `/${rel}`,
|
|
453
|
+
scope: "write"
|
|
454
|
+
}],
|
|
455
|
+
verdict: "ask"
|
|
456
|
+
};
|
|
457
|
+
} };
|
|
458
|
+
/** True if `path` looks like a credentials/keys file we shouldn't
|
|
459
|
+
* expose to the model by default. Conservative — false positives are
|
|
460
|
+
* fine here, and users can override via `allow` rules. */
|
|
461
|
+
function isSensitiveFile(path) {
|
|
462
|
+
return SENSITIVE_PATTERNS.some((re) => re.test(path));
|
|
463
|
+
}
|
|
464
|
+
/** Common locations for credentials, keys, and other sensitive data.
|
|
465
|
+
* Matched against the path string directly — covers both absolute and
|
|
466
|
+
* relative inputs. Add to this list as new gotchas surface. */
|
|
467
|
+
const SENSITIVE_PATTERNS = [
|
|
468
|
+
/(?:^|\/)\.env(?:\.|$)/,
|
|
469
|
+
/(?:^|\/)\.git\/(?!hooks\/)/,
|
|
470
|
+
/(?:^|\/)\.ssh\//,
|
|
471
|
+
/(?:^|\/)\.aws\/credentials/,
|
|
472
|
+
/(?:^|\/)\.netrc$/,
|
|
473
|
+
/(?:^|\/)id_(?:rsa|dsa|ed25519|ecdsa)(?:\.pub)?$/,
|
|
474
|
+
/(?:^|\/)\.npmrc$/,
|
|
475
|
+
/(?:^|\/)\.pypirc$/,
|
|
476
|
+
/(?:^|\/)secrets?\//,
|
|
477
|
+
/(?:^|\/)credentials?(?:\.|\/)/
|
|
478
|
+
];
|
|
479
|
+
/** Longest-prefix match against the workspace list — returns the
|
|
480
|
+
* containing workspace's absolute path, or undefined if none contain
|
|
481
|
+
* the candidate. Workspaces arrive pre-normalized from the manager. */
|
|
482
|
+
function longestWorkspace(abs, workspaces) {
|
|
483
|
+
let best;
|
|
484
|
+
for (const root of workspaces) if (abs === root || abs.startsWith(root.endsWith("/") ? root : `${root}/`)) {
|
|
485
|
+
if (best === void 0 || root.length > best.length) best = root;
|
|
486
|
+
}
|
|
487
|
+
return best;
|
|
488
|
+
}
|
|
489
|
+
/** Resolve the first matching rule.
|
|
490
|
+
* Patterns get normalized (`//abs`, `~/`, `/rel`, `rel`) into
|
|
491
|
+
* workspace-rooted gitignore syntax; patterns that resolve outside the
|
|
492
|
+
* candidate's workspace are dropped (they don't apply here). */
|
|
493
|
+
function resolveRule$1(workspace, rel, rules) {
|
|
494
|
+
if (rel === "") return void 0;
|
|
495
|
+
for (const rule of rules) {
|
|
496
|
+
const norm = normalizePattern(rule.pattern, workspace);
|
|
497
|
+
if (norm === void 0) continue;
|
|
498
|
+
try {
|
|
499
|
+
if ((0, import_ignore.default)().add(norm).ignores(rel)) return rule;
|
|
500
|
+
} catch {}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
function normalizePattern(pattern, workspace) {
|
|
504
|
+
if (pattern.startsWith("//")) return relativizeToWorkspace(pattern.slice(1), workspace);
|
|
505
|
+
if (pattern.startsWith("~/")) return relativizeToWorkspace(normPath(pattern), workspace);
|
|
506
|
+
return pattern;
|
|
507
|
+
}
|
|
508
|
+
function relativizeToWorkspace(absPattern, workspace) {
|
|
509
|
+
const rel = relative(workspace, absPattern);
|
|
510
|
+
if (rel === "" || rel.startsWith("..") || isAbsolute(rel)) return void 0;
|
|
511
|
+
return `/${rel}`;
|
|
512
|
+
}
|
|
513
|
+
//#endregion
|
|
514
|
+
//#region src/permissions/handlers/tool.ts
|
|
515
|
+
/**
|
|
516
|
+
* Generic tool-scope handler.
|
|
517
|
+
*
|
|
518
|
+
* Used by `Tasks` to gate every tool dispatch on the tool name, and
|
|
519
|
+
* available to tools themselves for richer checks (e.g. fetch passing
|
|
520
|
+
* `fetch:example.com` so users can ask-for-domain).
|
|
521
|
+
*
|
|
522
|
+
* Pattern grammar is gitignore-style — reuses the `ignore` package so
|
|
523
|
+
* the syntax matches `read` / `write` patterns. Common shapes:
|
|
524
|
+
*
|
|
525
|
+
* "tool" → bare scope; matches every input (default-allow)
|
|
526
|
+
* "tool(bash)" → exact match for `"bash"`
|
|
527
|
+
* "tool(fetch:*)" → glob match `"fetch:..."`
|
|
528
|
+
* "tool(task_*)" → glob match `"task_..."`
|
|
529
|
+
*
|
|
530
|
+
* Resolution is first-match-wins. Default verdict when no rule matches is
|
|
531
|
+
* `allow` — an unconfigured `tool` scope should not surprise users by
|
|
532
|
+
* gating ordinary tool calls.
|
|
533
|
+
*/
|
|
534
|
+
const toolHandler = { validate(input, ctx) {
|
|
535
|
+
const rule = resolveRule(input, ctx.rules);
|
|
536
|
+
if (rule?.policy === "deny") return {
|
|
537
|
+
reason: `tool ${input}: denied by rule`,
|
|
538
|
+
verdict: "deny"
|
|
539
|
+
};
|
|
540
|
+
if (rule?.policy === "ask") return {
|
|
541
|
+
ask: `Allow tool ${input}?`,
|
|
542
|
+
reason: `tool ${input}: rule requires confirmation`,
|
|
543
|
+
suggestions: [{
|
|
544
|
+
kind: "rule",
|
|
545
|
+
pattern: input,
|
|
546
|
+
scope: "tool"
|
|
547
|
+
}],
|
|
548
|
+
verdict: "ask"
|
|
549
|
+
};
|
|
550
|
+
return { verdict: "allow" };
|
|
551
|
+
} };
|
|
552
|
+
function resolveRule(input, rules) {
|
|
553
|
+
for (const rule of rules) if ((0, import_ignore.default)().add(rule.pattern).ignores(input)) return rule;
|
|
554
|
+
}
|
|
555
|
+
const handlerRegistry = createRegistry("scope").from({
|
|
556
|
+
bash: () => bashHandler,
|
|
557
|
+
read: () => fileHandler,
|
|
558
|
+
tool: () => toolHandler,
|
|
559
|
+
write: () => fileHandler
|
|
560
|
+
});
|
|
561
|
+
//#endregion
|
|
562
|
+
export { handlerRegistry as t };
|
|
563
|
+
|
|
564
|
+
//# sourceMappingURL=registry-CFmR5G3J.mjs.map
|