rulesync 16.38.0 → 16.39.0
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 +1 -1
- package/dist/cli/index.cjs +4 -4
- package/dist/cli/index.js +8 -8
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BiyWNVZY.js → import-B2ANxi1V.js} +1689 -412
- package/dist/import-B2ANxi1V.js.map +1 -0
- package/dist/{import-DKBaTqsY.cjs → import-DS3OrTAl.cjs} +1687 -410
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BiyWNVZY.js.map +0 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
2
|
import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
|
|
3
3
|
import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
-
import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
4
|
+
import path, { basename, dirname, extname, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
+
import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse as parse$1, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
6
6
|
import { constants } from "node:fs";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { intersection, kebabCase, uniq } from "es-toolkit";
|
|
@@ -14,7 +14,7 @@ import { YAMLException, dump, load } from "js-yaml";
|
|
|
14
14
|
import { omit } from "es-toolkit/object";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import * as smolToml from "smol-toml";
|
|
17
|
-
import { parse as parse$
|
|
17
|
+
import { parse as parse$2, stringify } from "smol-toml";
|
|
18
18
|
import { encode } from "@toon-format/toon";
|
|
19
19
|
//#region src/utils/control-characters.ts
|
|
20
20
|
/**
|
|
@@ -671,6 +671,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
671
671
|
"kiro-cli",
|
|
672
672
|
"kiro-ide",
|
|
673
673
|
"opencode",
|
|
674
|
+
"pool",
|
|
674
675
|
"qwencode",
|
|
675
676
|
"reasonix",
|
|
676
677
|
"roo",
|
|
@@ -752,6 +753,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
752
753
|
"crush",
|
|
753
754
|
"opencode",
|
|
754
755
|
"pi",
|
|
756
|
+
"pool",
|
|
755
757
|
"factorydroid",
|
|
756
758
|
"goose",
|
|
757
759
|
"hermesagent",
|
|
@@ -799,6 +801,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
799
801
|
"kiro-ide",
|
|
800
802
|
"opencode",
|
|
801
803
|
"pi",
|
|
804
|
+
"pool",
|
|
802
805
|
"qwencode",
|
|
803
806
|
"reasonix",
|
|
804
807
|
"roo",
|
|
@@ -1355,6 +1358,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
|
|
|
1355
1358
|
}));
|
|
1356
1359
|
}
|
|
1357
1360
|
/**
|
|
1361
|
+
* How many links {@link writeLandingPath} follows before it treats the chain as
|
|
1362
|
+
* a cycle it cannot write through. Linux itself gives up after 40.
|
|
1363
|
+
*/
|
|
1364
|
+
const MAX_LINK_HOPS = 40;
|
|
1365
|
+
/**
|
|
1366
|
+
* Whether `error` is the ENOTDIR a lookup through a file raises. On the way to
|
|
1367
|
+
* a write it means the same as a missing entry: nothing at or below the path
|
|
1368
|
+
* exists, so nothing there can be a link.
|
|
1369
|
+
*/
|
|
1370
|
+
function isNotADirectoryError(error) {
|
|
1371
|
+
return someErrorInChain(error, (candidate) => "code" in candidate && candidate.code === "ENOTDIR");
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Where a write to `targetPath` lands once the OS has followed every link on
|
|
1375
|
+
* the way there, or `null` when the links form a cycle no write can get
|
|
1376
|
+
* through. The target itself usually does not exist yet, so the walk goes
|
|
1377
|
+
* segment by segment from the filesystem root: an existing segment that is a
|
|
1378
|
+
* link is replaced by what it resolves to, and a dangling link — one whose
|
|
1379
|
+
* target does not exist yet, which is exactly the shape of a link planted at
|
|
1380
|
+
* `AGENTS.md` pointing at a `~/.zshenv` that is still to be created — has its
|
|
1381
|
+
* target spliced into the walk in its place, so the segments of that target
|
|
1382
|
+
* are looked up one by one too. Once a segment is missing, nothing below it
|
|
1383
|
+
* exists, and the rest of the path is where the write's `mkdir -p` will put it.
|
|
1384
|
+
*
|
|
1385
|
+
* Walking segment by segment is what keeps a `..` honest. A link target is a
|
|
1386
|
+
* path the OS resolves left to right, so in `sub/../victim` the `..` climbs out
|
|
1387
|
+
* of wherever `sub` leads, not out of the directory `sub` is spelled in; a
|
|
1388
|
+
* lexical `resolve` folds `sub/..` away before `sub` is ever looked at, which
|
|
1389
|
+
* is how a link chain could be spelled to pass the check and still land
|
|
1390
|
+
* outside the root.
|
|
1391
|
+
*/
|
|
1392
|
+
async function writeLandingPath(targetPath) {
|
|
1393
|
+
const start = splitAbsolutePath(isAbsolute(targetPath) ? targetPath : resolve(targetPath));
|
|
1394
|
+
let current = start.root;
|
|
1395
|
+
let pending = start.segments;
|
|
1396
|
+
let exists = true;
|
|
1397
|
+
let linkHops = 0;
|
|
1398
|
+
while (pending.length > 0) {
|
|
1399
|
+
const segment = pending.shift();
|
|
1400
|
+
if (segment === void 0 || segment === "" || segment === ".") continue;
|
|
1401
|
+
if (segment === "..") {
|
|
1402
|
+
current = dirname(current);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
current = join(current, segment);
|
|
1406
|
+
if (!exists) continue;
|
|
1407
|
+
let stats;
|
|
1408
|
+
try {
|
|
1409
|
+
stats = await lstat(current);
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
if (!isFileNotFoundError(error) && !isNotADirectoryError(error)) throw error;
|
|
1412
|
+
exists = false;
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (!stats.isSymbolicLink()) continue;
|
|
1416
|
+
linkHops += 1;
|
|
1417
|
+
if (linkHops > MAX_LINK_HOPS) return null;
|
|
1418
|
+
try {
|
|
1419
|
+
current = await realpath(current);
|
|
1420
|
+
continue;
|
|
1421
|
+
} catch {}
|
|
1422
|
+
const linkTarget = await readlink(current);
|
|
1423
|
+
if (isAbsolute(linkTarget)) {
|
|
1424
|
+
const target = splitAbsolutePath(linkTarget);
|
|
1425
|
+
current = target.root;
|
|
1426
|
+
pending = [...target.segments, ...pending];
|
|
1427
|
+
} else {
|
|
1428
|
+
current = dirname(current);
|
|
1429
|
+
pending = [...splitPathSegments(linkTarget), ...pending];
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return current;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Split an absolute path into its root and its raw segments, without folding a
|
|
1436
|
+
* `..` away: {@link writeLandingPath} has to see every `..` where it is spelled.
|
|
1437
|
+
*/
|
|
1438
|
+
function splitAbsolutePath(absolutePath) {
|
|
1439
|
+
const root = parse(absolutePath).root;
|
|
1440
|
+
return {
|
|
1441
|
+
root,
|
|
1442
|
+
segments: splitPathSegments(absolutePath.slice(root.length))
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether writing `targetPath` would land outside `rootPath` once every link on
|
|
1447
|
+
* the way there is resolved. Both sides are taken to where a write to them
|
|
1448
|
+
* would land (see {@link writeLandingPath}), so a root that does not fully
|
|
1449
|
+
* exist yet is judged the same way as the target below it.
|
|
1450
|
+
*
|
|
1451
|
+
* A link that stays inside the root passes. That is what sets this apart from
|
|
1452
|
+
* the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
|
|
1453
|
+
* dotfiles checkout linked from inside the home directory is a common shape for
|
|
1454
|
+
* a global write target, and refusing it would refuse `--global` outright.
|
|
1455
|
+
*
|
|
1456
|
+
* A link chain that cycles is reported as an escape: the write could not get
|
|
1457
|
+
* through it, and refusing it is the answer that never writes somewhere
|
|
1458
|
+
* unexpected.
|
|
1459
|
+
*/
|
|
1460
|
+
async function writablePathEscapesRoot({ rootPath, targetPath }) {
|
|
1461
|
+
const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
|
|
1462
|
+
if (rootLanding === null || targetLanding === null) return true;
|
|
1463
|
+
return pathEscapesRoot(relative(rootLanding, targetLanding));
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1358
1466
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1359
1467
|
*
|
|
1360
1468
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -2936,7 +3044,7 @@ const getDefaults = () => ({
|
|
|
2936
3044
|
const loadConfigFromFile = async (filePath) => {
|
|
2937
3045
|
if (!await fileExists(filePath)) return {};
|
|
2938
3046
|
const fileContent = await readFileContent(filePath);
|
|
2939
|
-
const jsonData = parse(fileContent);
|
|
3047
|
+
const jsonData = parse$1(fileContent);
|
|
2940
3048
|
const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
|
|
2941
3049
|
assertTargetsFeaturesExclusive({
|
|
2942
3050
|
targets: configParams.targets,
|
|
@@ -4654,6 +4762,34 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4654
4762
|
"stop"
|
|
4655
4763
|
];
|
|
4656
4764
|
/**
|
|
4765
|
+
* Hook events supported by Pool.
|
|
4766
|
+
*
|
|
4767
|
+
* Pool's `hooks` settings block fires six events, spelled the Claude Code
|
|
4768
|
+
* way: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` (← `beforeSubmitPrompt`),
|
|
4769
|
+
* `PreCompact`, `SessionStart` and `Stop`. Every entry carries a `matcher`
|
|
4770
|
+
* (required by Pool's schema; `""`/`*` match any tool, a bare name is an exact
|
|
4771
|
+
* match, `a|b` lists names, anything else is a regular expression), which only
|
|
4772
|
+
* `PreToolUse`/`PostToolUse` test against the tool name.
|
|
4773
|
+
* @see https://docs.poolside.ai/hooks
|
|
4774
|
+
*/
|
|
4775
|
+
const POOL_HOOK_EVENTS = [
|
|
4776
|
+
"preToolUse",
|
|
4777
|
+
"postToolUse",
|
|
4778
|
+
"beforeSubmitPrompt",
|
|
4779
|
+
"preCompact",
|
|
4780
|
+
"sessionStart",
|
|
4781
|
+
"stop"
|
|
4782
|
+
];
|
|
4783
|
+
const CANONICAL_TO_POOL_EVENT_NAMES = {
|
|
4784
|
+
preToolUse: "PreToolUse",
|
|
4785
|
+
postToolUse: "PostToolUse",
|
|
4786
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
4787
|
+
preCompact: "PreCompact",
|
|
4788
|
+
sessionStart: "SessionStart",
|
|
4789
|
+
stop: "Stop"
|
|
4790
|
+
};
|
|
4791
|
+
const POOL_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
4792
|
+
/**
|
|
4657
4793
|
* Hook events supported by Crush.
|
|
4658
4794
|
*
|
|
4659
4795
|
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
@@ -5248,6 +5384,7 @@ const HooksConfigSchema = z.looseObject({
|
|
|
5248
5384
|
"kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5249
5385
|
zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5250
5386
|
crush: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5387
|
+
pool: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5251
5388
|
bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5252
5389
|
cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5253
5390
|
commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
@@ -5922,7 +6059,7 @@ function parseJsonc(content) {
|
|
|
5922
6059
|
*/
|
|
5923
6060
|
function parseStrict(content) {
|
|
5924
6061
|
const errors = [];
|
|
5925
|
-
const result = parse(content, errors, {
|
|
6062
|
+
const result = parse$1(content, errors, {
|
|
5926
6063
|
allowTrailingComma: true,
|
|
5927
6064
|
disallowComments: false
|
|
5928
6065
|
});
|
|
@@ -7976,6 +8113,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
7976
8113
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7977
8114
|
crush: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7978
8115
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8116
|
+
pool: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7979
8117
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7980
8118
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
7981
8119
|
roo: z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -9818,6 +9956,22 @@ allowedMcpServers: z.optional(z.array(z.string())) })),
|
|
|
9818
9956
|
injectAgentsMd: z.optional(z.boolean()),
|
|
9819
9957
|
mcpServers: z.optional(z.array(z.string()))
|
|
9820
9958
|
})),
|
|
9959
|
+
pool: z.optional(z.looseObject({
|
|
9960
|
+
type: z.optional(z.enum([
|
|
9961
|
+
"in_process",
|
|
9962
|
+
"command",
|
|
9963
|
+
"agent_server"
|
|
9964
|
+
])),
|
|
9965
|
+
description: z.optional(z.string()),
|
|
9966
|
+
instructions: z.optional(z.string()),
|
|
9967
|
+
command: z.optional(z.string()),
|
|
9968
|
+
args: z.optional(z.array(z.string())),
|
|
9969
|
+
env: z.optional(z.record(z.string(), z.string())),
|
|
9970
|
+
agent_server: z.optional(z.string()),
|
|
9971
|
+
session_config_options: z.optional(z.record(z.string(), z.unknown())),
|
|
9972
|
+
inherit_agent_config: z.optional(z.boolean()),
|
|
9973
|
+
disabled: z.optional(z.boolean())
|
|
9974
|
+
})),
|
|
9821
9975
|
vibe: z.optional(z.looseObject({
|
|
9822
9976
|
agent_type: z.optional(z.enum(["agent", "subagent"])),
|
|
9823
9977
|
display_name: z.optional(z.string()),
|
|
@@ -10179,7 +10333,7 @@ function tryParseStructured({ filePath, content }) {
|
|
|
10179
10333
|
case ".json":
|
|
10180
10334
|
case ".jsonc": {
|
|
10181
10335
|
const errors = [];
|
|
10182
|
-
const value = parse(content, errors);
|
|
10336
|
+
const value = parse$1(content, errors);
|
|
10183
10337
|
return errors.length > 0 ? PARSE_FAILED : {
|
|
10184
10338
|
ok: true,
|
|
10185
10339
|
value
|
|
@@ -10405,6 +10559,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10405
10559
|
this.logger = logger;
|
|
10406
10560
|
}
|
|
10407
10561
|
/**
|
|
10562
|
+
* Whether `convertRulesyncFilesToToolFiles` still yields tool files for an
|
|
10563
|
+
* empty source list. A processor that aggregates into a shared file it may
|
|
10564
|
+
* never delete (Pool's `settings.yaml` subagents) answers true so a run
|
|
10565
|
+
* whose last source file is gone retracts what an earlier run generated;
|
|
10566
|
+
* the default keeps a run with no source from writing anything. Consulted
|
|
10567
|
+
* after `loadRulesyncFiles`, so the answer may depend on what that load saw.
|
|
10568
|
+
*/
|
|
10569
|
+
emitsToolFilesForEmptySource() {
|
|
10570
|
+
return false;
|
|
10571
|
+
}
|
|
10572
|
+
/**
|
|
10408
10573
|
* Return tool targets that this feature supports.
|
|
10409
10574
|
*/
|
|
10410
10575
|
static getToolTargets(_params = {}) {
|
|
@@ -10419,6 +10584,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10419
10584
|
const changedPaths = [];
|
|
10420
10585
|
for (const aiFile of aiFiles) {
|
|
10421
10586
|
const filePath = aiFile.getFilePath();
|
|
10587
|
+
if (await refusesWriteOutsideRoot({
|
|
10588
|
+
logger: this.logger,
|
|
10589
|
+
rootPath: aiFile.getOutputRoot(),
|
|
10590
|
+
targetPath: filePath
|
|
10591
|
+
})) continue;
|
|
10422
10592
|
const existingFileContent = await readFileContentOrNull(filePath);
|
|
10423
10593
|
if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
|
|
10424
10594
|
const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
|
|
@@ -10551,6 +10721,37 @@ function mergeByIdentity({ perRoot, identity }) {
|
|
|
10551
10721
|
return order.map((key) => winnerByKey.get(key));
|
|
10552
10722
|
}
|
|
10553
10723
|
/**
|
|
10724
|
+
* Whether a write to `targetPath` has to be refused because a link on the way
|
|
10725
|
+
* there leads out of `rootPath`, warning about the refusal.
|
|
10726
|
+
*
|
|
10727
|
+
* A checked-out repository can carry a symbolic link at an output path
|
|
10728
|
+
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`), and a write
|
|
10729
|
+
* through it would land wherever the link points. Every writer that lets the
|
|
10730
|
+
* OS follow links — file, directory, and config-rewrite alike — asks this first
|
|
10731
|
+
* so that one warning describes the one behavior.
|
|
10732
|
+
*/
|
|
10733
|
+
async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
10734
|
+
if (!await writablePathEscapesRoot({
|
|
10735
|
+
rootPath,
|
|
10736
|
+
targetPath
|
|
10737
|
+
})) return false;
|
|
10738
|
+
logger.warn(`Refusing to write ${quoteForLog(targetPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
|
|
10739
|
+
return true;
|
|
10740
|
+
}
|
|
10741
|
+
/**
|
|
10742
|
+
* {@link refusesWriteOutsideRoot} over every path a single unit of output
|
|
10743
|
+
* writes, stopping at the first refusal: a directory written as one unit is
|
|
10744
|
+
* held back whole when any one file in it would land outside the root.
|
|
10745
|
+
*/
|
|
10746
|
+
async function refusesAnyWriteOutsideRoot({ logger, rootPath, targetPaths }) {
|
|
10747
|
+
for (const targetPath of targetPaths) if (await refusesWriteOutsideRoot({
|
|
10748
|
+
logger,
|
|
10749
|
+
rootPath,
|
|
10750
|
+
targetPath
|
|
10751
|
+
})) return true;
|
|
10752
|
+
return false;
|
|
10753
|
+
}
|
|
10754
|
+
/**
|
|
10554
10755
|
* The key two spellings share when a case-insensitive filesystem would give
|
|
10555
10756
|
* them one file. `toLowerCase()` is locale-independent (unlike
|
|
10556
10757
|
* `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
|
|
@@ -12224,16 +12425,16 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
12224
12425
|
let parsed;
|
|
12225
12426
|
try {
|
|
12226
12427
|
if (format === "yaml") parsed = loadYaml(fileContent);
|
|
12227
|
-
else if (format === "toml") parsed = parse$
|
|
12428
|
+
else if (format === "toml") parsed = parse$2(fileContent);
|
|
12228
12429
|
else if (format === "json") parsed = JSON.parse(fileContent);
|
|
12229
12430
|
else if (jsoncParseErrors === "error") {
|
|
12230
12431
|
const errors = [];
|
|
12231
|
-
parsed = parse(fileContent, errors, { allowTrailingComma: true });
|
|
12432
|
+
parsed = parse$1(fileContent, errors, { allowTrailingComma: true });
|
|
12232
12433
|
if (errors.length > 0) {
|
|
12233
12434
|
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
12234
12435
|
throw new Error(details);
|
|
12235
12436
|
}
|
|
12236
|
-
} else parsed = parse(fileContent);
|
|
12437
|
+
} else parsed = parse$1(fileContent);
|
|
12237
12438
|
} catch (error) {
|
|
12238
12439
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
12239
12440
|
}
|
|
@@ -13015,15 +13216,33 @@ const KIMI_CODE_CONFIG_DECLARATION = {
|
|
|
13015
13216
|
* Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
|
|
13016
13217
|
* the user's primary Pool settings file, so an unparseable root fails closed
|
|
13017
13218
|
* rather than being replaced with generated output. `mcp_servers` is owned as
|
|
13018
|
-
* a whole key because the MCP writer emits the complete server map.
|
|
13219
|
+
* a whole key because the MCP writer emits the complete server map. The
|
|
13220
|
+
* permissions writer deep-merges: it rewrites the `allow`/`deny` lists of the
|
|
13221
|
+
* `tools.<name>` blocks it manages and the `paths` lists, leaving each tool's
|
|
13222
|
+
* `disabled` flag, unmanaged tools and every other key in place. `hooks` is
|
|
13223
|
+
* owned as a whole key because the hooks writer emits every event list.
|
|
13224
|
+
* `subagents` is owned as a whole key too: its writer recomputes the block
|
|
13225
|
+
* from the existing file (`default` and the built-in `general` agent carried
|
|
13226
|
+
* over, every other agent replaced) before applying the patch.
|
|
13019
13227
|
*/
|
|
13020
13228
|
const POOL_SETTINGS_DECLARATION = {
|
|
13021
13229
|
format: "yaml",
|
|
13022
13230
|
invalidRootPolicy: "error",
|
|
13023
|
-
features: {
|
|
13024
|
-
|
|
13025
|
-
|
|
13026
|
-
|
|
13231
|
+
features: {
|
|
13232
|
+
mcp: {
|
|
13233
|
+
kind: "replace-owned-keys",
|
|
13234
|
+
ownedKeys: ["mcp_servers"]
|
|
13235
|
+
},
|
|
13236
|
+
permissions: { kind: "deep-merge" },
|
|
13237
|
+
hooks: {
|
|
13238
|
+
kind: "replace-owned-keys",
|
|
13239
|
+
ownedKeys: ["hooks"]
|
|
13240
|
+
},
|
|
13241
|
+
subagents: {
|
|
13242
|
+
kind: "replace-owned-keys",
|
|
13243
|
+
ownedKeys: ["subagents"]
|
|
13244
|
+
}
|
|
13245
|
+
}
|
|
13027
13246
|
};
|
|
13028
13247
|
/**
|
|
13029
13248
|
* ZCode's settings file, which also carries model/theme/permission keys
|
|
@@ -19301,7 +19520,7 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
|
|
|
19301
19520
|
}
|
|
19302
19521
|
parseTomlContent(content) {
|
|
19303
19522
|
try {
|
|
19304
|
-
const parsed = parse$
|
|
19523
|
+
const parsed = parse$2(content);
|
|
19305
19524
|
const result = TabnineCommandFrontmatterSchema.safeParse(parsed);
|
|
19306
19525
|
if (!result.success) throw new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`);
|
|
19307
19526
|
return {
|
|
@@ -20418,6 +20637,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20418
20637
|
global: this.global,
|
|
20419
20638
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
20420
20639
|
}));
|
|
20640
|
+
if (await refusesWriteOutsideRoot({
|
|
20641
|
+
logger: this.logger,
|
|
20642
|
+
rootPath: this.outputRoot,
|
|
20643
|
+
targetPath: configPath
|
|
20644
|
+
})) return changedCount;
|
|
20421
20645
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20422
20646
|
if (currentContent === null) return changedCount;
|
|
20423
20647
|
const nextContent = getDisabledHermesCommandsPluginConfigContent({
|
|
@@ -20440,6 +20664,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20440
20664
|
if (this.toolTarget !== "goose" || !this.global) return 0;
|
|
20441
20665
|
if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
|
|
20442
20666
|
const configPath = join(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
|
|
20667
|
+
if (await refusesWriteOutsideRoot({
|
|
20668
|
+
logger: this.logger,
|
|
20669
|
+
rootPath: this.outputRoot,
|
|
20670
|
+
targetPath: configPath
|
|
20671
|
+
})) return 0;
|
|
20443
20672
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20444
20673
|
if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
|
|
20445
20674
|
const nextContent = getGooseSlashCommandsConfigContent({
|
|
@@ -27539,6 +27768,302 @@ var PiHooks = class PiHooks extends ToolHooks {
|
|
|
27539
27768
|
}
|
|
27540
27769
|
};
|
|
27541
27770
|
//#endregion
|
|
27771
|
+
//#region src/constants/pool-paths.ts
|
|
27772
|
+
/**
|
|
27773
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
27774
|
+
*
|
|
27775
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
27776
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
27777
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
27778
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
27779
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
27780
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
27781
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
27782
|
+
*
|
|
27783
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
27784
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
27785
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
27786
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
27787
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
27788
|
+
*
|
|
27789
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
27790
|
+
* @see https://docs.poolside.ai/skills
|
|
27791
|
+
* @see https://github.com/poolsideai/pool
|
|
27792
|
+
*/
|
|
27793
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
27794
|
+
const POOL_DIR = ".poolside";
|
|
27795
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
27796
|
+
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
27797
|
+
/** Project skills root, relative to the project root. */
|
|
27798
|
+
const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
|
|
27799
|
+
/** Global skills root, relative to the home directory. */
|
|
27800
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
|
|
27801
|
+
/**
|
|
27802
|
+
* Pool's settings file name. MCP servers live under the top-level
|
|
27803
|
+
* `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
|
|
27804
|
+
* `~/.config/poolside/settings.yaml` (global); the untracked
|
|
27805
|
+
* `.poolside/settings.local.yaml` overlay is left to the user.
|
|
27806
|
+
*
|
|
27807
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
27808
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
27809
|
+
*/
|
|
27810
|
+
const POOL_SETTINGS_FILE_NAME = "settings.yaml";
|
|
27811
|
+
/** Pool's untracked per-user overlay next to the settings file; never written. */
|
|
27812
|
+
const POOL_SETTINGS_LOCAL_FILE_NAME = "settings.local.yaml";
|
|
27813
|
+
/** Top-level key of Pool's settings file that holds the MCP server map. */
|
|
27814
|
+
const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
27815
|
+
/** Top-level key of Pool's settings file that holds per-tool permission rules. */
|
|
27816
|
+
const POOL_TOOLS_KEY = "tools";
|
|
27817
|
+
/** Top-level key of Pool's settings file that holds file access rules. */
|
|
27818
|
+
const POOL_PATHS_KEY = "paths";
|
|
27819
|
+
/** Top-level key of Pool's settings file that holds the per-event hook lists. */
|
|
27820
|
+
const POOL_HOOKS_KEY = "hooks";
|
|
27821
|
+
/**
|
|
27822
|
+
* Top-level key of Pool's settings file that holds the subagent registry:
|
|
27823
|
+
* `subagents.default` names the agent delegated to by default and
|
|
27824
|
+
* `subagents.agents.<name>` defines each agent.
|
|
27825
|
+
*
|
|
27826
|
+
* @see https://docs.poolside.ai/subagents
|
|
27827
|
+
*/
|
|
27828
|
+
const POOL_SUBAGENTS_KEY = "subagents";
|
|
27829
|
+
/** Key under `subagents` that holds the per-agent definitions. */
|
|
27830
|
+
const POOL_SUBAGENTS_AGENTS_KEY = "agents";
|
|
27831
|
+
/** Pool's reserved built-in agent, always `in_process` and never generated. */
|
|
27832
|
+
const POOL_GENERAL_AGENT_NAME = "general";
|
|
27833
|
+
//#endregion
|
|
27834
|
+
//#region src/features/hooks/pool-hooks.ts
|
|
27835
|
+
/** The matcher Pool reads as "any tool"; `""` means the same. */
|
|
27836
|
+
const POOL_MATCH_ALL = "*";
|
|
27837
|
+
const SUPPORTED_POOL_EVENTS = new Set(POOL_HOOK_EVENTS);
|
|
27838
|
+
/**
|
|
27839
|
+
* The Pool events that test `matcher` against a tool name. Pool ignores the
|
|
27840
|
+
* field on every other event (it stays required by the schema), so an
|
|
27841
|
+
* authored matcher there is dropped with a warning and `*` written instead.
|
|
27842
|
+
*/
|
|
27843
|
+
const POOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
27844
|
+
/**
|
|
27845
|
+
* Single spelling of the settings.yaml codec/policy, matching PoolMcp and
|
|
27846
|
+
* PoolPermissions: fail closed on an unparseable root rather than replacing
|
|
27847
|
+
* the user's primary Pool settings with generated output.
|
|
27848
|
+
*/
|
|
27849
|
+
function parsePoolSettings$3(fileContent, filePath) {
|
|
27850
|
+
return parseSharedConfig({
|
|
27851
|
+
format: "yaml",
|
|
27852
|
+
fileContent,
|
|
27853
|
+
filePath,
|
|
27854
|
+
invalidRootPolicy: "error"
|
|
27855
|
+
});
|
|
27856
|
+
}
|
|
27857
|
+
/**
|
|
27858
|
+
* Build the `hooks` event lists of Pool's settings file from a canonical hooks
|
|
27859
|
+
* config. Pool keys a flat list of `{name, matcher, command, timeout}` entries
|
|
27860
|
+
* by event name. Only `type: "command"` canonical hooks are emitted, since a
|
|
27861
|
+
* Pool hook is a shell command. A shared canonical event Pool does not fire is
|
|
27862
|
+
* skipped (the HooksProcessor reports it), while an event under the
|
|
27863
|
+
* `pool.hooks` override — such as one an import filed there — is written
|
|
27864
|
+
* verbatim so it round-trips.
|
|
27865
|
+
*/
|
|
27866
|
+
function canonicalToPoolHooks({ config, toolOverride, logger }) {
|
|
27867
|
+
const sharedHooks = {};
|
|
27868
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_POOL_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
27869
|
+
const effective = {
|
|
27870
|
+
...sharedHooks,
|
|
27871
|
+
...toolOverride
|
|
27872
|
+
};
|
|
27873
|
+
const hooks = {};
|
|
27874
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
27875
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
27876
|
+
const poolEvent = lookupOwn({
|
|
27877
|
+
record: CANONICAL_TO_POOL_EVENT_NAMES,
|
|
27878
|
+
key: event
|
|
27879
|
+
}) ?? event;
|
|
27880
|
+
const entries = [];
|
|
27881
|
+
for (const def of defs) {
|
|
27882
|
+
const entry = canonicalDefToPoolEntry({
|
|
27883
|
+
def,
|
|
27884
|
+
event,
|
|
27885
|
+
poolEvent,
|
|
27886
|
+
logger
|
|
27887
|
+
});
|
|
27888
|
+
if (entry !== null) entries.push(entry);
|
|
27889
|
+
}
|
|
27890
|
+
if (entries.length > 0) hooks[poolEvent] = entries;
|
|
27891
|
+
}
|
|
27892
|
+
return hooks;
|
|
27893
|
+
}
|
|
27894
|
+
/** Convert one canonical hook definition to a Pool entry, or null to skip. */
|
|
27895
|
+
function canonicalDefToPoolEntry({ def, event, poolEvent, logger }) {
|
|
27896
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
27897
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
27898
|
+
logger?.warn(`Pool hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Pool would reject, so it was skipped.`);
|
|
27899
|
+
return null;
|
|
27900
|
+
}
|
|
27901
|
+
const authoredMatcher = typeof def.matcher === "string" && def.matcher !== "" ? def.matcher : POOL_MATCH_ALL;
|
|
27902
|
+
let matcher = authoredMatcher;
|
|
27903
|
+
if (!POOL_MATCHER_EVENTS.has(poolEvent) && authoredMatcher !== POOL_MATCH_ALL) {
|
|
27904
|
+
logger?.warn(`Pool ignores "matcher" on "${poolEvent}", so the matcher ${quoteValueForWarning(authoredMatcher)} of the hook ${quoteValueForWarning(def.name ?? def.command)} under "${event}" was dropped.`);
|
|
27905
|
+
matcher = POOL_MATCH_ALL;
|
|
27906
|
+
}
|
|
27907
|
+
const entry = {
|
|
27908
|
+
...typeof def.name === "string" && def.name !== "" ? { name: def.name } : {},
|
|
27909
|
+
matcher,
|
|
27910
|
+
command: def.command
|
|
27911
|
+
};
|
|
27912
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
27913
|
+
return entry;
|
|
27914
|
+
}
|
|
27915
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
27916
|
+
function poolEntryToCanonicalDef({ raw, poolEvent }) {
|
|
27917
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string" || raw.command === "") return null;
|
|
27918
|
+
const def = {
|
|
27919
|
+
type: "command",
|
|
27920
|
+
command: raw.command
|
|
27921
|
+
};
|
|
27922
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
27923
|
+
if (POOL_MATCHER_EVENTS.has(poolEvent) && typeof raw.matcher === "string" && raw.matcher !== "" && raw.matcher !== POOL_MATCH_ALL) def.matcher = raw.matcher;
|
|
27924
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
27925
|
+
return def;
|
|
27926
|
+
}
|
|
27927
|
+
/**
|
|
27928
|
+
* Reverse {@link canonicalToPoolHooks}: parse the event lists of the `hooks`
|
|
27929
|
+
* block back into a canonical event → definition[] record. An event Pool does
|
|
27930
|
+
* not document is carried under its own name so `buildImportedHooksConfig`
|
|
27931
|
+
* files it under the `pool.hooks` override; a non-list sibling such as
|
|
27932
|
+
* `stop_hook_max_continuations` is not a hook and is left alone.
|
|
27933
|
+
*/
|
|
27934
|
+
function poolHooksToCanonical(hooksBlock) {
|
|
27935
|
+
const canonical = {};
|
|
27936
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
27937
|
+
for (const [poolEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
27938
|
+
if (isPrototypePollutionKey(poolEvent) || !Array.isArray(rawEntries)) continue;
|
|
27939
|
+
const canonicalEvent = lookupOwn({
|
|
27940
|
+
record: POOL_TO_CANONICAL_EVENT_NAMES,
|
|
27941
|
+
key: poolEvent
|
|
27942
|
+
}) ?? poolEvent;
|
|
27943
|
+
const defs = rawEntries.map((raw) => poolEntryToCanonicalDef({
|
|
27944
|
+
raw,
|
|
27945
|
+
poolEvent
|
|
27946
|
+
})).filter((def) => def !== null);
|
|
27947
|
+
if (defs.length === 0) continue;
|
|
27948
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
27949
|
+
record: canonical,
|
|
27950
|
+
key: canonicalEvent
|
|
27951
|
+
}) ?? [], ...defs];
|
|
27952
|
+
}
|
|
27953
|
+
return canonical;
|
|
27954
|
+
}
|
|
27955
|
+
/**
|
|
27956
|
+
* The keys of an existing `hooks` block that are not event lists —
|
|
27957
|
+
* `stop_hook_max_continuations` and anything else Pool may add beside the
|
|
27958
|
+
* events — which survive a regenerate untouched.
|
|
27959
|
+
*/
|
|
27960
|
+
function nonEventHookKeys(hooksBlock) {
|
|
27961
|
+
const kept = {};
|
|
27962
|
+
if (!isRecord$1(hooksBlock)) return kept;
|
|
27963
|
+
for (const [key, value] of Object.entries(hooksBlock)) if (!isPrototypePollutionKey(key) && !Array.isArray(value)) kept[key] = value;
|
|
27964
|
+
return kept;
|
|
27965
|
+
}
|
|
27966
|
+
/**
|
|
27967
|
+
* Pool hooks.
|
|
27968
|
+
*
|
|
27969
|
+
* Pool reads hooks from the `hooks` key of its settings file —
|
|
27970
|
+
* `.poolside/settings.yaml` at project scope and
|
|
27971
|
+
* `~/.config/poolside/settings.yaml` at user scope — as
|
|
27972
|
+
* `hooks.<Event>: [{name?, matcher, command, timeout?}]` for `PreToolUse`,
|
|
27973
|
+
* `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionStart` and `Stop`.
|
|
27974
|
+
* The event lists are rewritten as a whole; the block's non-list sibling
|
|
27975
|
+
* `stop_hook_max_continuations` and every other top-level key of the file
|
|
27976
|
+
* (`mcp_servers`, `tools`, `paths`, `pool`, ...) are preserved, and the file
|
|
27977
|
+
* is never deleted.
|
|
27978
|
+
*
|
|
27979
|
+
* @see https://docs.poolside.ai/hooks
|
|
27980
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
27981
|
+
*/
|
|
27982
|
+
var PoolHooks = class PoolHooks extends ToolHooks {
|
|
27983
|
+
settings;
|
|
27984
|
+
constructor(params) {
|
|
27985
|
+
super(params);
|
|
27986
|
+
this.settings = parsePoolSettings$3(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
27987
|
+
}
|
|
27988
|
+
getSettings() {
|
|
27989
|
+
return this.settings;
|
|
27990
|
+
}
|
|
27991
|
+
isDeletable() {
|
|
27992
|
+
return false;
|
|
27993
|
+
}
|
|
27994
|
+
static getSettablePaths({ global = false } = {}) {
|
|
27995
|
+
return {
|
|
27996
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
27997
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
27998
|
+
};
|
|
27999
|
+
}
|
|
28000
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
28001
|
+
const paths = this.getSettablePaths({ global });
|
|
28002
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
28003
|
+
return new PoolHooks({
|
|
28004
|
+
outputRoot,
|
|
28005
|
+
relativeDirPath: paths.relativeDirPath,
|
|
28006
|
+
relativeFilePath: paths.relativeFilePath,
|
|
28007
|
+
fileContent,
|
|
28008
|
+
validate,
|
|
28009
|
+
global
|
|
28010
|
+
});
|
|
28011
|
+
}
|
|
28012
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
28013
|
+
const paths = this.getSettablePaths({ global });
|
|
28014
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
28015
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
28016
|
+
const existing = parsePoolSettings$3(existingContent, filePath);
|
|
28017
|
+
const config = rulesyncHooks.getJson();
|
|
28018
|
+
const hooks = canonicalToPoolHooks({
|
|
28019
|
+
config,
|
|
28020
|
+
toolOverride: config.pool?.hooks,
|
|
28021
|
+
logger
|
|
28022
|
+
});
|
|
28023
|
+
return new PoolHooks({
|
|
28024
|
+
outputRoot,
|
|
28025
|
+
relativeDirPath: paths.relativeDirPath,
|
|
28026
|
+
relativeFilePath: paths.relativeFilePath,
|
|
28027
|
+
fileContent: applySharedConfigPatch({
|
|
28028
|
+
fileKey: sharedConfigFileKey(paths),
|
|
28029
|
+
feature: "hooks",
|
|
28030
|
+
existingContent,
|
|
28031
|
+
patch: { [POOL_HOOKS_KEY]: {
|
|
28032
|
+
...nonEventHookKeys(existing[POOL_HOOKS_KEY]),
|
|
28033
|
+
...hooks
|
|
28034
|
+
} },
|
|
28035
|
+
filePath,
|
|
28036
|
+
logger
|
|
28037
|
+
}),
|
|
28038
|
+
validate,
|
|
28039
|
+
global
|
|
28040
|
+
});
|
|
28041
|
+
}
|
|
28042
|
+
toRulesyncHooks() {
|
|
28043
|
+
const hooks = poolHooksToCanonical(this.settings[POOL_HOOKS_KEY]);
|
|
28044
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
28045
|
+
hooks,
|
|
28046
|
+
overrideKey: "pool"
|
|
28047
|
+
}), null, 2) });
|
|
28048
|
+
}
|
|
28049
|
+
validate() {
|
|
28050
|
+
return {
|
|
28051
|
+
success: true,
|
|
28052
|
+
error: null
|
|
28053
|
+
};
|
|
28054
|
+
}
|
|
28055
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
28056
|
+
return new PoolHooks({
|
|
28057
|
+
outputRoot,
|
|
28058
|
+
relativeDirPath,
|
|
28059
|
+
relativeFilePath,
|
|
28060
|
+
fileContent: "",
|
|
28061
|
+
validate: false,
|
|
28062
|
+
global
|
|
28063
|
+
});
|
|
28064
|
+
}
|
|
28065
|
+
};
|
|
28066
|
+
//#endregion
|
|
27542
28067
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
27543
28068
|
/**
|
|
27544
28069
|
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
@@ -28652,14 +29177,15 @@ const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
|
|
|
28652
29177
|
/**
|
|
28653
29178
|
* Event names present in the config that the target's adapter cannot emit.
|
|
28654
29179
|
*
|
|
28655
|
-
*
|
|
28656
|
-
*
|
|
28657
|
-
*
|
|
29180
|
+
* Shared (`hooks`) keys are always checked. Override-block keys are only
|
|
29181
|
+
* checked when the factory declares `dropsUnknownOverrideEvents`; otherwise
|
|
29182
|
+
* the adapter either emits them verbatim or reports its own drops, and a
|
|
29183
|
+
* generic "not supported" warning would be wrong or duplicated.
|
|
28658
29184
|
*/
|
|
28659
29185
|
function unsupportedEventNames(params) {
|
|
28660
29186
|
const { factory, sharedHooks, effectiveHooks } = params;
|
|
28661
29187
|
const supportedEvents = new Set(factory.supportedEvents);
|
|
28662
|
-
const eventNames = factory.
|
|
29188
|
+
const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
|
|
28663
29189
|
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
28664
29190
|
}
|
|
28665
29191
|
/**
|
|
@@ -28752,7 +29278,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28752
29278
|
supportedEvents: AMP_HOOK_EVENTS,
|
|
28753
29279
|
supportedHookTypes: ["command"],
|
|
28754
29280
|
supportsMatcher: true,
|
|
28755
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29281
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29282
|
+
dropsUnknownOverrideEvents: true
|
|
28756
29283
|
}],
|
|
28757
29284
|
["antigravity-cli", {
|
|
28758
29285
|
class: AntigravityCliHooks,
|
|
@@ -28785,7 +29312,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28785
29312
|
},
|
|
28786
29313
|
supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
|
|
28787
29314
|
supportedHookTypes: ["command"],
|
|
28788
|
-
supportsMatcher: true
|
|
29315
|
+
supportsMatcher: true,
|
|
29316
|
+
dropsUnknownOverrideEvents: true
|
|
28789
29317
|
}],
|
|
28790
29318
|
["cursor", {
|
|
28791
29319
|
class: CursorHooks,
|
|
@@ -28830,7 +29358,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28830
29358
|
"mcp_tool",
|
|
28831
29359
|
"agent"
|
|
28832
29360
|
],
|
|
28833
|
-
supportsMatcher: true
|
|
29361
|
+
supportsMatcher: true,
|
|
29362
|
+
dropsUnknownOverrideEvents: true
|
|
28834
29363
|
}],
|
|
28835
29364
|
["codexcli", {
|
|
28836
29365
|
class: CodexcliHooks,
|
|
@@ -28891,7 +29420,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28891
29420
|
supportedEvents: KILO_HOOK_EVENTS,
|
|
28892
29421
|
supportedHookTypes: ["command"],
|
|
28893
29422
|
supportsMatcher: true,
|
|
28894
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29423
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29424
|
+
dropsUnknownOverrideEvents: true
|
|
28895
29425
|
}],
|
|
28896
29426
|
["opencode", {
|
|
28897
29427
|
class: OpencodeHooks,
|
|
@@ -28903,7 +29433,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28903
29433
|
supportedEvents: OPENCODE_HOOK_EVENTS,
|
|
28904
29434
|
supportedHookTypes: ["command"],
|
|
28905
29435
|
supportsMatcher: true,
|
|
28906
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29436
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29437
|
+
dropsUnknownOverrideEvents: true
|
|
28907
29438
|
}],
|
|
28908
29439
|
["pi", {
|
|
28909
29440
|
class: PiHooks,
|
|
@@ -28914,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28914
29445
|
},
|
|
28915
29446
|
supportedEvents: PI_HOOK_EVENTS,
|
|
28916
29447
|
supportedHookTypes: ["command"],
|
|
28917
|
-
supportsMatcher: true
|
|
29448
|
+
supportsMatcher: true,
|
|
29449
|
+
dropsUnknownOverrideEvents: true
|
|
28918
29450
|
}],
|
|
28919
29451
|
["factorydroid", {
|
|
28920
29452
|
class: FactorydroidHooks,
|
|
@@ -28936,7 +29468,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28936
29468
|
},
|
|
28937
29469
|
supportedEvents: CLINE_HOOK_EVENTS,
|
|
28938
29470
|
supportedHookTypes: ["command"],
|
|
28939
|
-
supportsMatcher: false
|
|
29471
|
+
supportsMatcher: false,
|
|
29472
|
+
dropsUnknownOverrideEvents: true
|
|
28940
29473
|
}],
|
|
28941
29474
|
["goose", {
|
|
28942
29475
|
class: GooseHooks,
|
|
@@ -28958,8 +29491,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28958
29491
|
},
|
|
28959
29492
|
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
28960
29493
|
supportedHookTypes: ["command"],
|
|
28961
|
-
supportsMatcher: true
|
|
28962
|
-
passthroughOverrideEvents: true
|
|
29494
|
+
supportsMatcher: true
|
|
28963
29495
|
}],
|
|
28964
29496
|
["kimi-code", {
|
|
28965
29497
|
class: KimiCodeHooks,
|
|
@@ -28970,8 +29502,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28970
29502
|
},
|
|
28971
29503
|
supportedEvents: KIMI_CODE_HOOK_EVENTS,
|
|
28972
29504
|
supportedHookTypes: ["command"],
|
|
28973
|
-
supportsMatcher: true
|
|
28974
|
-
passthroughOverrideEvents: true
|
|
29505
|
+
supportsMatcher: true
|
|
28975
29506
|
}],
|
|
28976
29507
|
["deepagents", {
|
|
28977
29508
|
class: DeepagentsHooks,
|
|
@@ -28982,7 +29513,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28982
29513
|
},
|
|
28983
29514
|
supportedEvents: DEEPAGENTS_HOOK_EVENTS,
|
|
28984
29515
|
supportedHookTypes: ["command"],
|
|
28985
|
-
supportsMatcher: true
|
|
29516
|
+
supportsMatcher: true,
|
|
29517
|
+
dropsUnknownOverrideEvents: true
|
|
28986
29518
|
}],
|
|
28987
29519
|
["kiro", {
|
|
28988
29520
|
class: KiroHooks,
|
|
@@ -28993,8 +29525,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28993
29525
|
},
|
|
28994
29526
|
supportedEvents: KIRO_HOOK_EVENTS,
|
|
28995
29527
|
supportedHookTypes: ["command"],
|
|
28996
|
-
supportsMatcher: true
|
|
28997
|
-
passthroughOverrideEvents: true
|
|
29528
|
+
supportsMatcher: true
|
|
28998
29529
|
}],
|
|
28999
29530
|
["kiro-cli", {
|
|
29000
29531
|
class: KiroCliHooks,
|
|
@@ -29005,8 +29536,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29005
29536
|
},
|
|
29006
29537
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
29007
29538
|
supportedHookTypes: ["command", "prompt"],
|
|
29008
|
-
supportsMatcher: true
|
|
29009
|
-
passthroughOverrideEvents: true
|
|
29539
|
+
supportsMatcher: true
|
|
29010
29540
|
}],
|
|
29011
29541
|
["kiro-ide", {
|
|
29012
29542
|
class: KiroIdeHooks,
|
|
@@ -29017,8 +29547,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29017
29547
|
},
|
|
29018
29548
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
29019
29549
|
supportedHookTypes: ["command", "prompt"],
|
|
29020
|
-
supportsMatcher: true
|
|
29021
|
-
passthroughOverrideEvents: true
|
|
29550
|
+
supportsMatcher: true
|
|
29022
29551
|
}],
|
|
29023
29552
|
["devin", {
|
|
29024
29553
|
class: DevinHooks,
|
|
@@ -29136,6 +29665,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29136
29665
|
},
|
|
29137
29666
|
supportedEvents: VIBE_HOOK_EVENTS,
|
|
29138
29667
|
supportedHookTypes: ["command"],
|
|
29668
|
+
supportsMatcher: true,
|
|
29669
|
+
dropsUnknownOverrideEvents: true
|
|
29670
|
+
}],
|
|
29671
|
+
["pool", {
|
|
29672
|
+
class: PoolHooks,
|
|
29673
|
+
meta: {
|
|
29674
|
+
supportsProject: true,
|
|
29675
|
+
supportsGlobal: true,
|
|
29676
|
+
supportsImport: true
|
|
29677
|
+
},
|
|
29678
|
+
supportedEvents: POOL_HOOK_EVENTS,
|
|
29679
|
+
supportedHookTypes: ["command"],
|
|
29139
29680
|
supportsMatcher: true
|
|
29140
29681
|
}],
|
|
29141
29682
|
["qwencode", {
|
|
@@ -29163,7 +29704,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29163
29704
|
},
|
|
29164
29705
|
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
29165
29706
|
supportedHookTypes: ["command"],
|
|
29166
|
-
supportsMatcher: true
|
|
29707
|
+
supportsMatcher: true,
|
|
29708
|
+
dropsUnknownOverrideEvents: true
|
|
29167
29709
|
}],
|
|
29168
29710
|
["grokcli", {
|
|
29169
29711
|
class: GrokcliHooks,
|
|
@@ -29185,8 +29727,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29185
29727
|
},
|
|
29186
29728
|
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
29187
29729
|
supportedHookTypes: ["command"],
|
|
29188
|
-
supportsMatcher: true
|
|
29189
|
-
passthroughOverrideEvents: true
|
|
29730
|
+
supportsMatcher: true
|
|
29190
29731
|
}],
|
|
29191
29732
|
["zcode", {
|
|
29192
29733
|
class: ZcodeHooks,
|
|
@@ -35820,7 +36361,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
|
|
|
35820
36361
|
* that is not an object at all costs nothing.
|
|
35821
36362
|
*/
|
|
35822
36363
|
function readExistingKiloMcpEntries(fileContent) {
|
|
35823
|
-
const mcp = parse(fileContent || "{}")?.mcp;
|
|
36364
|
+
const mcp = parse$1(fileContent || "{}")?.mcp;
|
|
35824
36365
|
if (!isRecord$1(mcp)) return {};
|
|
35825
36366
|
const entries = {};
|
|
35826
36367
|
for (const [serverName, entry] of Object.entries(mcp)) {
|
|
@@ -35844,7 +36385,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
35844
36385
|
json;
|
|
35845
36386
|
constructor(params) {
|
|
35846
36387
|
super(params);
|
|
35847
|
-
this.json = KiloConfigSchema.parse(parse(this.fileContent || "{}"));
|
|
36388
|
+
this.json = KiloConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
35848
36389
|
}
|
|
35849
36390
|
getJson() {
|
|
35850
36391
|
return this.json;
|
|
@@ -35912,7 +36453,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
35912
36453
|
outputRoot,
|
|
35913
36454
|
global
|
|
35914
36455
|
});
|
|
35915
|
-
const json = parse(fileContent ?? "{\"mcp\":{}}");
|
|
36456
|
+
const json = parse$1(fileContent ?? "{\"mcp\":{}}");
|
|
35916
36457
|
const newJson = {
|
|
35917
36458
|
...json,
|
|
35918
36459
|
mcp: json.mcp ?? {}
|
|
@@ -35983,7 +36524,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
35983
36524
|
if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
|
|
35984
36525
|
}
|
|
35985
36526
|
if (instructions.length === 0 && fileContent === null) return null;
|
|
35986
|
-
const json = fileContent ? parse(fileContent) : {};
|
|
36527
|
+
const json = fileContent ? parse$1(fileContent) : {};
|
|
35987
36528
|
const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
|
|
35988
36529
|
const managedPrefix = `${toPosixPath(join(KILO_DIR, KILO_RULES_DIR_NAME))}/`;
|
|
35989
36530
|
const preservedInstructions = existingInstructions.filter((entry) => !toPosixPath(entry).replace(/^\.\//, "").startsWith(managedPrefix));
|
|
@@ -36947,7 +37488,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
36947
37488
|
json;
|
|
36948
37489
|
constructor(params) {
|
|
36949
37490
|
super(params);
|
|
36950
|
-
this.json = OpencodeConfigSchema.parse(parse(this.fileContent || "{}"));
|
|
37491
|
+
this.json = OpencodeConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
36951
37492
|
}
|
|
36952
37493
|
getJson() {
|
|
36953
37494
|
return this.json;
|
|
@@ -36980,7 +37521,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
36980
37521
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
36981
37522
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
36982
37523
|
}
|
|
36983
|
-
const json = parse(fileContent ?? "{\"mcp\":{}}");
|
|
37524
|
+
const json = parse$1(fileContent ?? "{\"mcp\":{}}");
|
|
36984
37525
|
const newJson = {
|
|
36985
37526
|
...json,
|
|
36986
37527
|
mcp: json.mcp ?? {}
|
|
@@ -37009,7 +37550,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37009
37550
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
37010
37551
|
replacement: "{env:$1}"
|
|
37011
37552
|
});
|
|
37012
|
-
const existingMcp = OpencodeConfigSchema.safeParse(parse(fileContent || "{}"));
|
|
37553
|
+
const existingMcp = OpencodeConfigSchema.safeParse(parse$1(fileContent || "{}"));
|
|
37013
37554
|
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
37014
37555
|
return new OpencodeMcp({
|
|
37015
37556
|
outputRoot,
|
|
@@ -37060,7 +37601,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37060
37601
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
37061
37602
|
}
|
|
37062
37603
|
if (instructions.length === 0 && fileContent === null) return null;
|
|
37063
|
-
const json = fileContent ? parse(fileContent) : {};
|
|
37604
|
+
const json = fileContent ? parse$1(fileContent) : {};
|
|
37064
37605
|
const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
|
|
37065
37606
|
const managedPrefixes = global ? ["memories/", `${configDirPrefix}memories/`] : [`${toPosixPath(OPENCODE_DIR)}/memories/`];
|
|
37066
37607
|
const preservedInstructions = existingInstructions.filter((entry) => {
|
|
@@ -37117,49 +37658,6 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37117
37658
|
}
|
|
37118
37659
|
};
|
|
37119
37660
|
//#endregion
|
|
37120
|
-
//#region src/constants/pool-paths.ts
|
|
37121
|
-
/**
|
|
37122
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
37123
|
-
*
|
|
37124
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
37125
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
37126
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
37127
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
37128
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
37129
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
37130
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
37131
|
-
*
|
|
37132
|
-
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
37133
|
-
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
37134
|
-
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
37135
|
-
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
37136
|
-
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
37137
|
-
*
|
|
37138
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
37139
|
-
* @see https://docs.poolside.ai/skills
|
|
37140
|
-
* @see https://github.com/poolsideai/pool
|
|
37141
|
-
*/
|
|
37142
|
-
/** Project-scoped `.poolside/` directory at the project root. */
|
|
37143
|
-
const POOL_DIR = ".poolside";
|
|
37144
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
37145
|
-
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
37146
|
-
/** Project skills root, relative to the project root. */
|
|
37147
|
-
const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
|
|
37148
|
-
/** Global skills root, relative to the home directory. */
|
|
37149
|
-
const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
|
|
37150
|
-
/**
|
|
37151
|
-
* Pool's settings file name. MCP servers live under the top-level
|
|
37152
|
-
* `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
|
|
37153
|
-
* `~/.config/poolside/settings.yaml` (global); the untracked
|
|
37154
|
-
* `.poolside/settings.local.yaml` overlay is left to the user.
|
|
37155
|
-
*
|
|
37156
|
-
* @see https://docs.poolside.ai/mcp-servers
|
|
37157
|
-
* @see https://docs.poolside.ai/settings-file-reference
|
|
37158
|
-
*/
|
|
37159
|
-
const POOL_SETTINGS_FILE_NAME = "settings.yaml";
|
|
37160
|
-
/** Top-level key of Pool's settings file that holds the MCP server map. */
|
|
37161
|
-
const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
37162
|
-
//#endregion
|
|
37163
37661
|
//#region src/features/mcp/pool-mcp.ts
|
|
37164
37662
|
/**
|
|
37165
37663
|
* Single spelling of the settings.yaml codec/policy, matching the
|
|
@@ -37167,7 +37665,7 @@ const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
|
37167
37665
|
* unparseable root rather than replacing the user's primary Pool settings with
|
|
37168
37666
|
* generated output.
|
|
37169
37667
|
*/
|
|
37170
|
-
function parsePoolSettings(fileContent, filePath) {
|
|
37668
|
+
function parsePoolSettings$2(fileContent, filePath) {
|
|
37171
37669
|
return parseSharedConfig({
|
|
37172
37670
|
format: "yaml",
|
|
37173
37671
|
fileContent,
|
|
@@ -37399,7 +37897,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
37399
37897
|
settings;
|
|
37400
37898
|
constructor(params) {
|
|
37401
37899
|
super(params);
|
|
37402
|
-
this.settings = parsePoolSettings(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
37900
|
+
this.settings = parsePoolSettings$2(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
37403
37901
|
}
|
|
37404
37902
|
getSettings() {
|
|
37405
37903
|
return this.settings;
|
|
@@ -41243,7 +41741,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
41243
41741
|
function toAntigravityCliToolName(canonical) {
|
|
41244
41742
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
41245
41743
|
}
|
|
41246
|
-
function toCanonicalToolName$
|
|
41744
|
+
function toCanonicalToolName$9(cliName) {
|
|
41247
41745
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
41248
41746
|
}
|
|
41249
41747
|
/**
|
|
@@ -41459,7 +41957,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
41459
41957
|
const processEntries = (entries, action) => {
|
|
41460
41958
|
for (const entry of entries) {
|
|
41461
41959
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
41462
|
-
const canonical = toCanonicalToolName$
|
|
41960
|
+
const canonical = toCanonicalToolName$9(toolName);
|
|
41463
41961
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41464
41962
|
permission[canonical][pattern] = action;
|
|
41465
41963
|
}
|
|
@@ -41738,7 +42236,7 @@ function toLegacyAugmentToolName(augmentName) {
|
|
|
41738
42236
|
key: augmentName
|
|
41739
42237
|
}) ?? augmentName;
|
|
41740
42238
|
}
|
|
41741
|
-
function toCanonicalToolName$
|
|
42239
|
+
function toCanonicalToolName$8(augmentName) {
|
|
41742
42240
|
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
41743
42241
|
return lookupOwn({
|
|
41744
42242
|
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
@@ -42191,7 +42689,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
42191
42689
|
const type = entry.permission.type;
|
|
42192
42690
|
if (!isBasicAugmentType(type)) continue;
|
|
42193
42691
|
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
42194
|
-
const canonical = toCanonicalToolName$
|
|
42692
|
+
const canonical = toCanonicalToolName$8(entry.toolName);
|
|
42195
42693
|
if (forbiddenMapKeys.has(canonical)) {
|
|
42196
42694
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
|
|
42197
42695
|
continue;
|
|
@@ -42436,7 +42934,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
42436
42934
|
function toClaudeToolName(canonical) {
|
|
42437
42935
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
42438
42936
|
}
|
|
42439
|
-
function toCanonicalToolName$
|
|
42937
|
+
function toCanonicalToolName$7(claudeName) {
|
|
42440
42938
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
42441
42939
|
}
|
|
42442
42940
|
/**
|
|
@@ -43350,7 +43848,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
43350
43848
|
const processEntries = (entries, action) => {
|
|
43351
43849
|
for (const entry of entries) {
|
|
43352
43850
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
43353
|
-
const canonical = toCanonicalToolName$
|
|
43851
|
+
const canonical = toCanonicalToolName$7(toolName);
|
|
43354
43852
|
if (!permission[canonical]) permission[canonical] = {};
|
|
43355
43853
|
permission[canonical][pattern] = action;
|
|
43356
43854
|
}
|
|
@@ -44211,8 +44709,8 @@ function mapBashActionToDecision(action) {
|
|
|
44211
44709
|
//#region src/features/permissions/commandcode-permissions.ts
|
|
44212
44710
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
44213
44711
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
44214
|
-
const CATCH_ALL_PATTERN$
|
|
44215
|
-
const MCP_CANONICAL_PREFIX$
|
|
44712
|
+
const CATCH_ALL_PATTERN$7 = "*";
|
|
44713
|
+
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
44216
44714
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
44217
44715
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
44218
44716
|
bash: "Shell",
|
|
@@ -44266,10 +44764,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
44266
44764
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
44267
44765
|
*/
|
|
44268
44766
|
function buildCommandcodeRule(category, pattern) {
|
|
44269
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
44270
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
44271
|
-
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$
|
|
44272
|
-
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$
|
|
44767
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$7 || pattern === "";
|
|
44768
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
|
|
44769
|
+
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
|
|
44770
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$7 : null;
|
|
44273
44771
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
44274
44772
|
if (tool === void 0) return null;
|
|
44275
44773
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -44305,7 +44803,7 @@ function splitCommandcodeRule(rule) {
|
|
|
44305
44803
|
}
|
|
44306
44804
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
44307
44805
|
function isCatchAllSpecifier(inner) {
|
|
44308
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
44806
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$7;
|
|
44309
44807
|
}
|
|
44310
44808
|
/**
|
|
44311
44809
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -44339,34 +44837,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
44339
44837
|
if (server.length === 0) return null;
|
|
44340
44838
|
const everyServer = {
|
|
44341
44839
|
category: "mcp",
|
|
44342
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44840
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44343
44841
|
notInAllow: true
|
|
44344
44842
|
};
|
|
44345
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
44346
|
-
if (server === CATCH_ALL_PATTERN$
|
|
44347
|
-
if (server.includes(CATCH_ALL_PATTERN$
|
|
44843
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
|
|
44844
|
+
if (server === CATCH_ALL_PATTERN$7) return everyServer;
|
|
44845
|
+
if (server.includes(CATCH_ALL_PATTERN$7)) return null;
|
|
44348
44846
|
return {
|
|
44349
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
44350
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44847
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$7 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
|
|
44848
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44351
44849
|
notInAllow: false
|
|
44352
44850
|
};
|
|
44353
44851
|
}
|
|
44354
|
-
const category = `${MCP_CANONICAL_PREFIX$
|
|
44355
|
-
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$
|
|
44852
|
+
const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
|
|
44853
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$7);
|
|
44356
44854
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
44357
44855
|
category,
|
|
44358
44856
|
pattern: inner,
|
|
44359
44857
|
notInAllow: true
|
|
44360
44858
|
} : null;
|
|
44361
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
44362
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
44363
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
44364
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44859
|
+
if (remainder === CATCH_ALL_PATTERN$7) return everyServer;
|
|
44860
|
+
if (rest === CATCH_ALL_PATTERN$7 && !server.includes(CATCH_ALL_PATTERN$7)) return {
|
|
44861
|
+
category: `${MCP_CANONICAL_PREFIX$6}${server}`,
|
|
44862
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44365
44863
|
notInAllow: true
|
|
44366
44864
|
};
|
|
44367
44865
|
return namesTool ? {
|
|
44368
44866
|
category,
|
|
44369
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44867
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44370
44868
|
notInAllow: false
|
|
44371
44869
|
} : null;
|
|
44372
44870
|
}
|
|
@@ -44390,14 +44888,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44390
44888
|
if (category === "bash") {
|
|
44391
44889
|
if (isCatchAllSpecifier(inner)) return {
|
|
44392
44890
|
category,
|
|
44393
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44891
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44394
44892
|
notInAllow: false
|
|
44395
44893
|
};
|
|
44396
44894
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
44397
44895
|
if (normalized === "" || normalized === ":*") return null;
|
|
44398
44896
|
return isCatchAllSpecifier(normalized) ? {
|
|
44399
44897
|
category,
|
|
44400
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44898
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44401
44899
|
notInAllow: true
|
|
44402
44900
|
} : {
|
|
44403
44901
|
category,
|
|
@@ -44407,7 +44905,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44407
44905
|
}
|
|
44408
44906
|
if (isCatchAllSpecifier(inner)) return {
|
|
44409
44907
|
category,
|
|
44410
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44908
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44411
44909
|
notInAllow: false
|
|
44412
44910
|
};
|
|
44413
44911
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -44426,13 +44924,13 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44426
44924
|
*/
|
|
44427
44925
|
function parseCommandcodeRule(rule) {
|
|
44428
44926
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44429
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
44927
|
+
if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
|
|
44430
44928
|
category: "*",
|
|
44431
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44929
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44432
44930
|
notInAllow: true
|
|
44433
44931
|
} : null;
|
|
44434
44932
|
const lowered = tool.toLowerCase();
|
|
44435
|
-
if (lowered.startsWith(MCP_CANONICAL_PREFIX$
|
|
44933
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
|
|
44436
44934
|
tool,
|
|
44437
44935
|
inner
|
|
44438
44936
|
});
|
|
@@ -44450,12 +44948,12 @@ function parseCommandcodeRule(rule) {
|
|
|
44450
44948
|
*/
|
|
44451
44949
|
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
44452
44950
|
const { tool } = splitCommandcodeRule(rule);
|
|
44453
|
-
return category.startsWith(MCP_CANONICAL_PREFIX$
|
|
44951
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
|
|
44454
44952
|
}
|
|
44455
44953
|
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
44456
44954
|
function scopedMcpRuleTool(rule) {
|
|
44457
44955
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44458
|
-
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
44956
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
|
|
44459
44957
|
}
|
|
44460
44958
|
/**
|
|
44461
44959
|
* The string entries of a Command Code permission list. Command Code reads
|
|
@@ -44498,7 +44996,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
44498
44996
|
}).filter((rule) => {
|
|
44499
44997
|
const parsed = parseCommandcodeRule(rule);
|
|
44500
44998
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
44501
|
-
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$
|
|
44999
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$7})`);
|
|
44502
45000
|
if (key !== "allow" && isStricterAction({
|
|
44503
45001
|
action: key,
|
|
44504
45002
|
existing: replacement
|
|
@@ -44555,7 +45053,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44555
45053
|
const ranked = /* @__PURE__ */ new Map();
|
|
44556
45054
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
44557
45055
|
for (const [category, rules] of Object.entries(permission)) {
|
|
44558
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
45056
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
|
|
44559
45057
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
44560
45058
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
44561
45059
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -44624,7 +45122,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44624
45122
|
function skippedAllowImportMessage({ rule, category }) {
|
|
44625
45123
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44626
45124
|
if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
|
|
44627
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
45125
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$6) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
|
|
44628
45126
|
return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
|
|
44629
45127
|
}
|
|
44630
45128
|
/**
|
|
@@ -44826,7 +45324,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
44826
45324
|
//#endregion
|
|
44827
45325
|
//#region src/features/permissions/continue-permissions.ts
|
|
44828
45326
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
44829
|
-
const CATCH_ALL_PATTERN$
|
|
45327
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
44830
45328
|
/**
|
|
44831
45329
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
44832
45330
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -44834,7 +45332,7 @@ const CATCH_ALL_PATTERN$5 = "*";
|
|
|
44834
45332
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
44835
45333
|
*/
|
|
44836
45334
|
function normalizeCatchAllPattern$1(pattern) {
|
|
44837
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45335
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
|
|
44838
45336
|
}
|
|
44839
45337
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
44840
45338
|
bash: "Bash",
|
|
@@ -44863,7 +45361,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
44863
45361
|
function toContinueToolName(canonical) {
|
|
44864
45362
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
44865
45363
|
}
|
|
44866
|
-
function toCanonicalToolName$
|
|
45364
|
+
function toCanonicalToolName$6(continueName) {
|
|
44867
45365
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
44868
45366
|
}
|
|
44869
45367
|
/**
|
|
@@ -44871,7 +45369,7 @@ function toCanonicalToolName$5(continueName) {
|
|
|
44871
45369
|
* `Tool(pattern)`.
|
|
44872
45370
|
*/
|
|
44873
45371
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
44874
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
45372
|
+
return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
|
|
44875
45373
|
}
|
|
44876
45374
|
/**
|
|
44877
45375
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -44885,7 +45383,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
44885
45383
|
if (toolName === "") return;
|
|
44886
45384
|
return {
|
|
44887
45385
|
toolName,
|
|
44888
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
45386
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
|
|
44889
45387
|
};
|
|
44890
45388
|
}
|
|
44891
45389
|
/**
|
|
@@ -45059,7 +45557,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
45059
45557
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
45060
45558
|
continue;
|
|
45061
45559
|
}
|
|
45062
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
45560
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
|
|
45063
45561
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
45064
45562
|
continue;
|
|
45065
45563
|
}
|
|
@@ -45084,7 +45582,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
45084
45582
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
45085
45583
|
if (parsedEntry === void 0) continue;
|
|
45086
45584
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
45087
|
-
const category = toCanonicalToolName$
|
|
45585
|
+
const category = toCanonicalToolName$6(parsedEntry.toolName);
|
|
45088
45586
|
permission[category] ??= {};
|
|
45089
45587
|
permission[category][parsedEntry.pattern] ??= action;
|
|
45090
45588
|
}
|
|
@@ -45408,14 +45906,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
45408
45906
|
};
|
|
45409
45907
|
//#endregion
|
|
45410
45908
|
//#region src/features/permissions/crush-permissions.ts
|
|
45411
|
-
const CATCH_ALL_PATTERN$
|
|
45909
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
45412
45910
|
/**
|
|
45413
45911
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
45414
45912
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
45415
45913
|
* still be written.
|
|
45416
45914
|
*/
|
|
45417
45915
|
function normalizeCatchAllPattern(pattern) {
|
|
45418
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45916
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
45419
45917
|
}
|
|
45420
45918
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
45421
45919
|
bash: "bash",
|
|
@@ -45427,14 +45925,14 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
|
45427
45925
|
webfetch: "fetch"
|
|
45428
45926
|
};
|
|
45429
45927
|
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
45430
|
-
const MCP_CANONICAL_PREFIX$
|
|
45928
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
45431
45929
|
const MCP_CRUSH_PREFIX = "mcp_";
|
|
45432
45930
|
function toCrushToolName(canonical) {
|
|
45433
45931
|
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
45434
|
-
if (canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
45932
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
45435
45933
|
return canonical;
|
|
45436
45934
|
}
|
|
45437
|
-
function toCanonicalToolName$
|
|
45935
|
+
function toCanonicalToolName$5(crushName) {
|
|
45438
45936
|
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
45439
45937
|
}
|
|
45440
45938
|
function isMcpToolName(crushName) {
|
|
@@ -45614,7 +46112,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
45614
46112
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
45615
46113
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
45616
46114
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
45617
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
46115
|
+
if (pattern === CATCH_ALL_PATTERN$5) {
|
|
45618
46116
|
catchAllAction = action;
|
|
45619
46117
|
continue;
|
|
45620
46118
|
}
|
|
@@ -45687,11 +46185,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
45687
46185
|
};
|
|
45688
46186
|
for (const entry of disabled) {
|
|
45689
46187
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
45690
|
-
bucketFor(toCanonicalToolName$
|
|
46188
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
|
|
45691
46189
|
}
|
|
45692
46190
|
for (const entry of allowed) {
|
|
45693
46191
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
45694
|
-
bucketFor(toCanonicalToolName$
|
|
46192
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
|
|
45695
46193
|
}
|
|
45696
46194
|
return { permission };
|
|
45697
46195
|
}
|
|
@@ -45738,13 +46236,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
45738
46236
|
WebFetch: "webfetch",
|
|
45739
46237
|
Mcp: "mcp"
|
|
45740
46238
|
};
|
|
45741
|
-
const MCP_CANONICAL_PREFIX$
|
|
46239
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
45742
46240
|
/**
|
|
45743
46241
|
* Returns true if the canonical category is the per-tool MCP form
|
|
45744
46242
|
* `mcp__<server>__<tool>`.
|
|
45745
46243
|
*/
|
|
45746
46244
|
function isMcpScopedCategory(canonical) {
|
|
45747
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46245
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
|
|
45748
46246
|
}
|
|
45749
46247
|
function toCursorType(canonical) {
|
|
45750
46248
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -45778,7 +46276,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
45778
46276
|
if (match) {
|
|
45779
46277
|
const server = match[1] ?? "*";
|
|
45780
46278
|
const tool = match[2] ?? "*";
|
|
45781
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
46279
|
+
return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
|
|
45782
46280
|
}
|
|
45783
46281
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
45784
46282
|
}
|
|
@@ -46071,7 +46569,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
46071
46569
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
46072
46570
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
46073
46571
|
if (!permission[canonical]) permission[canonical] = {};
|
|
46074
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46572
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
|
|
46075
46573
|
permission[canonical][canonicalPattern] = action;
|
|
46076
46574
|
}
|
|
46077
46575
|
};
|
|
@@ -47590,7 +48088,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
47590
48088
|
//#region src/features/permissions/goose-permissions.ts
|
|
47591
48089
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
47592
48090
|
const GOOSE_USER_KEY = "user";
|
|
47593
|
-
const CATCH_ALL_PATTERN$
|
|
48091
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
47594
48092
|
const RULESYNC_TO_GOOSE_TOOL_NAME = {
|
|
47595
48093
|
bash: "developer__shell",
|
|
47596
48094
|
edit: "developer__text_editor",
|
|
@@ -47752,7 +48250,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
47752
48250
|
for (const [category, rules] of orderedEntries) {
|
|
47753
48251
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
47754
48252
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
47755
|
-
if (pattern !== CATCH_ALL_PATTERN$
|
|
48253
|
+
if (pattern !== CATCH_ALL_PATTERN$4) {
|
|
47756
48254
|
logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
|
|
47757
48255
|
continue;
|
|
47758
48256
|
}
|
|
@@ -47781,7 +48279,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
47781
48279
|
for (const toolName of toolNames) {
|
|
47782
48280
|
const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
|
|
47783
48281
|
permission[category] ??= {};
|
|
47784
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
48282
|
+
permission[category][CATCH_ALL_PATTERN$4] = action;
|
|
47785
48283
|
}
|
|
47786
48284
|
}
|
|
47787
48285
|
return { permission };
|
|
@@ -47792,8 +48290,8 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
47792
48290
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
47793
48291
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
47794
48292
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
47795
|
-
const CATCH_ALL_PATTERN$
|
|
47796
|
-
const MCP_CANONICAL_PREFIX$
|
|
48293
|
+
const CATCH_ALL_PATTERN$3 = "*";
|
|
48294
|
+
const MCP_CANONICAL_PREFIX$3 = "mcp__";
|
|
47797
48295
|
const CATEGORY_TO_GROK_TOOL = {
|
|
47798
48296
|
bash: "Bash",
|
|
47799
48297
|
read: "Read",
|
|
@@ -47834,14 +48332,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
|
47834
48332
|
* concrete pattern emits `Tool(pattern)`.
|
|
47835
48333
|
*/
|
|
47836
48334
|
function buildGrokEntry(category, pattern) {
|
|
47837
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
48335
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
|
|
47838
48336
|
const remainder = category.slice(5);
|
|
47839
48337
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
47840
48338
|
}
|
|
47841
|
-
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$
|
|
48339
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
47842
48340
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
47843
48341
|
if (tool === void 0) return null;
|
|
47844
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
48342
|
+
return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
47845
48343
|
}
|
|
47846
48344
|
/**
|
|
47847
48345
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -47861,17 +48359,17 @@ function parseGrokEntry(entry) {
|
|
|
47861
48359
|
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
47862
48360
|
}
|
|
47863
48361
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
47864
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47865
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48362
|
+
category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
|
|
48363
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47866
48364
|
} : {
|
|
47867
48365
|
category: "mcp",
|
|
47868
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48366
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47869
48367
|
};
|
|
47870
48368
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
47871
48369
|
if (category === void 0) return null;
|
|
47872
48370
|
return {
|
|
47873
48371
|
category,
|
|
47874
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
48372
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
|
|
47875
48373
|
};
|
|
47876
48374
|
}
|
|
47877
48375
|
/**
|
|
@@ -47892,15 +48390,15 @@ function parseGrokRule(rule) {
|
|
|
47892
48390
|
if (typeof tool !== "string") return null;
|
|
47893
48391
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
47894
48392
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
47895
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
48393
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
|
|
47896
48394
|
const lowerTool = tool.trim().toLowerCase();
|
|
47897
|
-
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
48395
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$3 ? {
|
|
47898
48396
|
category: "mcp",
|
|
47899
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48397
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47900
48398
|
action
|
|
47901
48399
|
} : {
|
|
47902
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47903
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48400
|
+
category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
|
|
48401
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47904
48402
|
action
|
|
47905
48403
|
};
|
|
47906
48404
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -48048,7 +48546,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
48048
48546
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
48049
48547
|
}
|
|
48050
48548
|
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
48051
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
48549
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
|
|
48052
48550
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
48053
48551
|
}
|
|
48054
48552
|
validate() {
|
|
@@ -48784,7 +49282,7 @@ function isSharedKiloCategory(key) {
|
|
|
48784
49282
|
*/
|
|
48785
49283
|
function parseKiloJsoncStrict(content, filePath) {
|
|
48786
49284
|
const errors = [];
|
|
48787
|
-
const parsed = parse(content, errors, { allowTrailingComma: true });
|
|
49285
|
+
const parsed = parse$1(content, errors, { allowTrailingComma: true });
|
|
48788
49286
|
const first = errors[0];
|
|
48789
49287
|
if (first) throw new Error(`Failed to parse Kilo Code config at ${filePath}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
|
|
48790
49288
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
@@ -48828,7 +49326,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
48828
49326
|
json;
|
|
48829
49327
|
constructor(params) {
|
|
48830
49328
|
super(params);
|
|
48831
|
-
const parsed = parse(this.fileContent || "{}");
|
|
49329
|
+
const parsed = parse$1(this.fileContent || "{}");
|
|
48832
49330
|
if (params.validate !== false) this.json = KiloPermissionsConfigSchema.parse(parsed);
|
|
48833
49331
|
else {
|
|
48834
49332
|
const result = KiloPermissionsConfigSchema.safeParse(parsed);
|
|
@@ -48968,7 +49466,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
48968
49466
|
}
|
|
48969
49467
|
validate() {
|
|
48970
49468
|
try {
|
|
48971
|
-
const json = parse(this.fileContent || "{}");
|
|
49469
|
+
const json = parse$1(this.fileContent || "{}");
|
|
48972
49470
|
const result = KiloPermissionsConfigSchema.safeParse(json);
|
|
48973
49471
|
if (!result.success) return {
|
|
48974
49472
|
success: false,
|
|
@@ -49679,7 +50177,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
49679
50177
|
json;
|
|
49680
50178
|
constructor(params) {
|
|
49681
50179
|
super(params);
|
|
49682
|
-
this.json = OpencodePermissionsConfigSchema.parse(parse(this.fileContent || "{}"));
|
|
50180
|
+
this.json = OpencodePermissionsConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
49683
50181
|
}
|
|
49684
50182
|
getJson() {
|
|
49685
50183
|
return this.json;
|
|
@@ -49707,7 +50205,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
49707
50205
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
49708
50206
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
49709
50207
|
}
|
|
49710
|
-
const parsed = parse(fileContent ?? "{}");
|
|
50208
|
+
const parsed = parse$1(fileContent ?? "{}");
|
|
49711
50209
|
const record = isRecord$1(parsed) ? parsed : {};
|
|
49712
50210
|
const nextJson = {
|
|
49713
50211
|
...record,
|
|
@@ -49928,6 +50426,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
|
|
|
49928
50426
|
}
|
|
49929
50427
|
};
|
|
49930
50428
|
//#endregion
|
|
50429
|
+
//#region src/features/permissions/pool-permissions.ts
|
|
50430
|
+
const POOL_ALLOW_KEY = "allow";
|
|
50431
|
+
const POOL_DENY_KEY = "deny";
|
|
50432
|
+
const POOL_DISABLED_KEY = "disabled";
|
|
50433
|
+
const POOL_PATH_KEY = "path";
|
|
50434
|
+
const POOL_WRITE_KEY = "write";
|
|
50435
|
+
const CATCH_ALL_PATTERN$2 = "*";
|
|
50436
|
+
const PATHS_CATCH_ALL_PATTERN = "**";
|
|
50437
|
+
const CANONICAL_TO_POOL_TOOL_NAMES = {
|
|
50438
|
+
bash: "shell",
|
|
50439
|
+
webfetch: "web_fetch",
|
|
50440
|
+
websearch: "web_search"
|
|
50441
|
+
};
|
|
50442
|
+
const POOL_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
50443
|
+
const READ_CATEGORY = "read";
|
|
50444
|
+
const WRITE_CATEGORIES = ["edit", "write"];
|
|
50445
|
+
const FILE_CATEGORIES = [READ_CATEGORY, ...WRITE_CATEGORIES];
|
|
50446
|
+
const MCP_CANONICAL_PREFIX$2 = "mcp__";
|
|
50447
|
+
/**
|
|
50448
|
+
* Single spelling of the settings.yaml codec/policy, matching the
|
|
50449
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
50450
|
+
* unparseable root rather than replacing the user's primary Pool settings with
|
|
50451
|
+
* generated output.
|
|
50452
|
+
*/
|
|
50453
|
+
function parsePoolSettings$1(fileContent, filePath) {
|
|
50454
|
+
return parseSharedConfig({
|
|
50455
|
+
format: "yaml",
|
|
50456
|
+
fileContent,
|
|
50457
|
+
filePath,
|
|
50458
|
+
invalidRootPolicy: "error"
|
|
50459
|
+
});
|
|
50460
|
+
}
|
|
50461
|
+
function toPoolToolName(canonical) {
|
|
50462
|
+
return lookupOwn({
|
|
50463
|
+
record: CANONICAL_TO_POOL_TOOL_NAMES,
|
|
50464
|
+
key: canonical
|
|
50465
|
+
}) ?? canonical;
|
|
50466
|
+
}
|
|
50467
|
+
function toCanonicalToolName$4(poolName) {
|
|
50468
|
+
return lookupOwn({
|
|
50469
|
+
record: POOL_TO_CANONICAL_TOOL_NAMES,
|
|
50470
|
+
key: poolName
|
|
50471
|
+
}) ?? poolName;
|
|
50472
|
+
}
|
|
50473
|
+
function isMcpCategory(category) {
|
|
50474
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$2) && category.length > 5;
|
|
50475
|
+
}
|
|
50476
|
+
function isFileCategory(category) {
|
|
50477
|
+
return FILE_CATEGORIES.includes(category);
|
|
50478
|
+
}
|
|
50479
|
+
/**
|
|
50480
|
+
* Pool's tool rules know one wildcard, `*`, which matches anything including
|
|
50481
|
+
* `/` — the same set a canonical `*` names — so a `**` run is collapsed to
|
|
50482
|
+
* keep the file in Pool's own spelling. `?`, `[...]` and `{a,b}` are literal
|
|
50483
|
+
* characters to Pool, so a pattern that relies on them cannot be written.
|
|
50484
|
+
*/
|
|
50485
|
+
function toPoolToolPattern(pattern) {
|
|
50486
|
+
if (/[?[{]/.test(pattern)) return void 0;
|
|
50487
|
+
return pattern.replaceAll(/\*{2,}/g, "*");
|
|
50488
|
+
}
|
|
50489
|
+
/**
|
|
50490
|
+
* Pool's path globs know `*` and `**`, so a bare `*` run is written as `**`
|
|
50491
|
+
* to keep it matching anything. `?`, `[...]` and `{a,b}` are literal
|
|
50492
|
+
* characters to Pool here as well, so a pattern that relies on them cannot be
|
|
50493
|
+
* written.
|
|
50494
|
+
*/
|
|
50495
|
+
function toPoolPathPattern(pattern) {
|
|
50496
|
+
if (/[?[{]/.test(pattern)) return void 0;
|
|
50497
|
+
return /^\*+$/.test(pattern) ? PATHS_CATCH_ALL_PATTERN : pattern;
|
|
50498
|
+
}
|
|
50499
|
+
function isHomeOrAbsolutePath(pattern) {
|
|
50500
|
+
return isAbsolute(pattern) || pattern === "~" || pattern.startsWith("~/");
|
|
50501
|
+
}
|
|
50502
|
+
function rulesOf(permission, category) {
|
|
50503
|
+
return lookupOwn({
|
|
50504
|
+
record: permission,
|
|
50505
|
+
key: category
|
|
50506
|
+
}) ?? {};
|
|
50507
|
+
}
|
|
50508
|
+
function patternsWithAction(rules, actions) {
|
|
50509
|
+
return Object.entries(rules).filter(([pattern, action]) => !isPrototypePollutionKey(pattern) && actions.includes(action)).map(([pattern]) => pattern);
|
|
50510
|
+
}
|
|
50511
|
+
function asRestrictions(patterns) {
|
|
50512
|
+
return uniq(patterns).map((pattern) => ({
|
|
50513
|
+
pattern,
|
|
50514
|
+
fromAllToolsCategory: false
|
|
50515
|
+
}));
|
|
50516
|
+
}
|
|
50517
|
+
function listPatterns(patterns) {
|
|
50518
|
+
return patterns.map((pattern) => quoteValueForWarning(pattern)).join(", ");
|
|
50519
|
+
}
|
|
50520
|
+
/**
|
|
50521
|
+
* Permissions adapter for Pool (Poolside's coding agent CLI).
|
|
50522
|
+
*
|
|
50523
|
+
* Pool keeps its permission rules in the same YAML settings file as its MCP
|
|
50524
|
+
* servers — `.poolside/settings.yaml` (project) / `~/.config/poolside/settings.yaml`
|
|
50525
|
+
* (global) — under two top-level keys:
|
|
50526
|
+
* - `tools.<name>`: `allow` and `deny` pattern lists matched against the
|
|
50527
|
+
* tool's argument (the command for `shell`, the URL for `web_fetch`, ...),
|
|
50528
|
+
* plus `disabled: true` to remove the tool. A denied pattern wins over an
|
|
50529
|
+
* allowed one; a call that matches neither prompts. Only `*` is a wildcard.
|
|
50530
|
+
* - `paths`: `allow` entries (`{ path }`, read-only unless `write: true`)
|
|
50531
|
+
* and `deny` entries (`{ path }`) with `*`/`**` globs; deny wins.
|
|
50532
|
+
*
|
|
50533
|
+
* Mapping (rulesync canonical -> Pool):
|
|
50534
|
+
* - `bash` -> `tools.shell`, `webfetch` -> `tools.web_fetch`,
|
|
50535
|
+
* `websearch` -> `tools.web_search`; any other non-file category passes
|
|
50536
|
+
* through as `tools.<category>`. `allow` -> `allow`, `deny` -> `deny`,
|
|
50537
|
+
* `ask` -> nothing (prompting is Pool's default) — but an `allow` that
|
|
50538
|
+
* overlaps an `ask` of any category landing on the same Pool tool, or a
|
|
50539
|
+
* deny Pool cannot spell, is withheld, since an allowed pattern would
|
|
50540
|
+
* silence the prompt the config asks for.
|
|
50541
|
+
* - `read` / `edit` / `write` -> `paths`: a `read` allow is a read-only
|
|
50542
|
+
* entry, an `edit` or `write` allow adds `write: true`, a `read` deny is a
|
|
50543
|
+
* deny entry. Pool cannot deny writes without denying reads, so an
|
|
50544
|
+
* `edit`/`write` deny that is not also a `read` deny only withholds the
|
|
50545
|
+
* write flag of the entries it overlaps (Pool prompts for those writes).
|
|
50546
|
+
* - `mcp__<server>__<tool>` is skipped: Pool approves MCP tools per server
|
|
50547
|
+
* under `mcp_servers.<server>.allow`, authored as `poolAllow` in
|
|
50548
|
+
* `.rulesync/mcp.json`. The all-tools `*` category is mirrored onto
|
|
50549
|
+
* `bash` by `honorAllToolsOnBash` and otherwise skipped.
|
|
50550
|
+
*
|
|
50551
|
+
* Project settings take project-relative paths and the user settings absolute
|
|
50552
|
+
* or `~` paths, so a path of the other kind is skipped with a warning at each
|
|
50553
|
+
* scope. The managed tools' `allow`/`deny` lists and, whenever the canonical
|
|
50554
|
+
* config carries a file category, the `paths` lists are rebuilt; every other
|
|
50555
|
+
* key (`disabled`, unmanaged tools, `mcp_servers`, `pool`, `sandbox`, ...) is
|
|
50556
|
+
* kept, and the file is never deleted.
|
|
50557
|
+
*
|
|
50558
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
50559
|
+
*/
|
|
50560
|
+
var PoolPermissions = class PoolPermissions extends ToolPermissions {
|
|
50561
|
+
settings;
|
|
50562
|
+
constructor(params) {
|
|
50563
|
+
super({
|
|
50564
|
+
...params,
|
|
50565
|
+
fileContent: params.fileContent ?? ""
|
|
50566
|
+
});
|
|
50567
|
+
this.settings = parsePoolSettings$1(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
50568
|
+
}
|
|
50569
|
+
getSettings() {
|
|
50570
|
+
return this.settings;
|
|
50571
|
+
}
|
|
50572
|
+
isDeletable() {
|
|
50573
|
+
return false;
|
|
50574
|
+
}
|
|
50575
|
+
/**
|
|
50576
|
+
* settings.yaml is Pool's file: rulesync merges into it when it exists but
|
|
50577
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
50578
|
+
* absent rule both mean "prompt".
|
|
50579
|
+
*/
|
|
50580
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
50581
|
+
return true;
|
|
50582
|
+
}
|
|
50583
|
+
static getSettablePaths({ global = false } = {}) {
|
|
50584
|
+
return {
|
|
50585
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
50586
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
50587
|
+
};
|
|
50588
|
+
}
|
|
50589
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
50590
|
+
const paths = this.getSettablePaths({ global });
|
|
50591
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
50592
|
+
return new PoolPermissions({
|
|
50593
|
+
outputRoot,
|
|
50594
|
+
relativeDirPath: paths.relativeDirPath,
|
|
50595
|
+
relativeFilePath: paths.relativeFilePath,
|
|
50596
|
+
fileContent,
|
|
50597
|
+
validate,
|
|
50598
|
+
global
|
|
50599
|
+
});
|
|
50600
|
+
}
|
|
50601
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
50602
|
+
const paths = this.getSettablePaths({ global });
|
|
50603
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
50604
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
50605
|
+
const existing = parsePoolSettings$1(existingContent, filePath);
|
|
50606
|
+
const patch = buildPoolPermissionsPatch({
|
|
50607
|
+
config: rulesyncPermissions.getJson(),
|
|
50608
|
+
existing,
|
|
50609
|
+
global,
|
|
50610
|
+
logger
|
|
50611
|
+
});
|
|
50612
|
+
return new PoolPermissions({
|
|
50613
|
+
outputRoot,
|
|
50614
|
+
relativeDirPath: paths.relativeDirPath,
|
|
50615
|
+
relativeFilePath: paths.relativeFilePath,
|
|
50616
|
+
fileContent: applySharedConfigPatch({
|
|
50617
|
+
fileKey: sharedConfigFileKey(paths),
|
|
50618
|
+
feature: "permissions",
|
|
50619
|
+
existingContent,
|
|
50620
|
+
patch,
|
|
50621
|
+
filePath,
|
|
50622
|
+
logger
|
|
50623
|
+
}),
|
|
50624
|
+
validate: true,
|
|
50625
|
+
global
|
|
50626
|
+
});
|
|
50627
|
+
}
|
|
50628
|
+
toRulesyncPermissions() {
|
|
50629
|
+
const rulesyncConfig = convertPoolToRulesync({
|
|
50630
|
+
tools: isRecord$1(this.settings["tools"]) ? this.settings[POOL_TOOLS_KEY] : {},
|
|
50631
|
+
paths: isRecord$1(this.settings["paths"]) ? this.settings[POOL_PATHS_KEY] : {}
|
|
50632
|
+
});
|
|
50633
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
50634
|
+
}
|
|
50635
|
+
validate() {
|
|
50636
|
+
return {
|
|
50637
|
+
success: true,
|
|
50638
|
+
error: null
|
|
50639
|
+
};
|
|
50640
|
+
}
|
|
50641
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
50642
|
+
return new PoolPermissions({
|
|
50643
|
+
outputRoot,
|
|
50644
|
+
relativeDirPath,
|
|
50645
|
+
relativeFilePath,
|
|
50646
|
+
fileContent: "",
|
|
50647
|
+
validate: false,
|
|
50648
|
+
global
|
|
50649
|
+
});
|
|
50650
|
+
}
|
|
50651
|
+
};
|
|
50652
|
+
/**
|
|
50653
|
+
* The deep-merge patch that rewrites the managed `tools.<name>` lists and,
|
|
50654
|
+
* when the canonical config carries a file category, the `paths` lists. An
|
|
50655
|
+
* empty list retracts its key (`undefined` deletes under deep-merge), and a
|
|
50656
|
+
* block is only touched when there is something to write into it or it
|
|
50657
|
+
* already exists, so a config without rules does not grow an empty
|
|
50658
|
+
* `tools: {}` / `paths: {}`.
|
|
50659
|
+
*/
|
|
50660
|
+
function buildPoolPermissionsPatch({ config, existing, global, logger }) {
|
|
50661
|
+
const permission = honorAllToolsOnBash(config.permission);
|
|
50662
|
+
const existingTools = isRecord$1(existing["tools"]) ? existing[POOL_TOOLS_KEY] : void 0;
|
|
50663
|
+
const existingPaths = isRecord$1(existing["paths"]) ? existing[POOL_PATHS_KEY] : void 0;
|
|
50664
|
+
const patch = {};
|
|
50665
|
+
const toolsPatch = {};
|
|
50666
|
+
for (const [toolName, lists] of convertRulesyncToPoolTools({
|
|
50667
|
+
permission,
|
|
50668
|
+
logger
|
|
50669
|
+
})) {
|
|
50670
|
+
const existingTool = existingTools === void 0 ? void 0 : lookupOwn({
|
|
50671
|
+
record: existingTools,
|
|
50672
|
+
key: toolName
|
|
50673
|
+
});
|
|
50674
|
+
if (lists.allow.length === 0 && lists.deny.length === 0 && existingTool === void 0) continue;
|
|
50675
|
+
toolsPatch[toolName] = {
|
|
50676
|
+
[POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
|
|
50677
|
+
[POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
|
|
50678
|
+
};
|
|
50679
|
+
}
|
|
50680
|
+
if (Object.keys(toolsPatch).length > 0) patch[POOL_TOOLS_KEY] = toolsPatch;
|
|
50681
|
+
if (FILE_CATEGORIES.some((category) => lookupOwn({
|
|
50682
|
+
record: permission,
|
|
50683
|
+
key: category
|
|
50684
|
+
}))) {
|
|
50685
|
+
const lists = convertRulesyncToPoolPaths({
|
|
50686
|
+
permission,
|
|
50687
|
+
global,
|
|
50688
|
+
logger
|
|
50689
|
+
});
|
|
50690
|
+
if (lists.allow.length > 0 || lists.deny.length > 0 || existingPaths !== void 0) patch[POOL_PATHS_KEY] = {
|
|
50691
|
+
[POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
|
|
50692
|
+
[POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
|
|
50693
|
+
};
|
|
50694
|
+
}
|
|
50695
|
+
return patch;
|
|
50696
|
+
}
|
|
50697
|
+
/**
|
|
50698
|
+
* Convert one non-file category into a Pool tool's `allow`/`deny` lists. A
|
|
50699
|
+
* deny Pool can spell needs no withholding (Pool lets it win over an allow),
|
|
50700
|
+
* but an `ask` has no list of its own — a call Pool does not match prompts —
|
|
50701
|
+
* so an allow that overlaps one is withheld and reported, and the tool keeps
|
|
50702
|
+
* prompting. A deny Pool cannot spell (its pattern uses a glob form other
|
|
50703
|
+
* than `*`) is skipped, and then withholds the allows it overlaps the same
|
|
50704
|
+
* way: written, such an allow would auto-approve the very command the deny
|
|
50705
|
+
* was meant to stop. `restrictions` carries the ask patterns and the skipped
|
|
50706
|
+
* deny patterns of every category that lands on the same Pool tool, because
|
|
50707
|
+
* Pool matches the joined lists as one.
|
|
50708
|
+
*/
|
|
50709
|
+
function convertToolCategory({ category, toolName, rules, restrictions, logger }) {
|
|
50710
|
+
const lists = {
|
|
50711
|
+
allow: [],
|
|
50712
|
+
deny: []
|
|
50713
|
+
};
|
|
50714
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(asRestrictions(restrictions));
|
|
50715
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
50716
|
+
if (isPrototypePollutionKey(rawPattern) || action === "ask") continue;
|
|
50717
|
+
const pattern = toPoolToolPattern(rawPattern);
|
|
50718
|
+
if (pattern === void 0) {
|
|
50719
|
+
logger?.warn(`Pool tool rules treat only "*" as a wildcard, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
50720
|
+
continue;
|
|
50721
|
+
}
|
|
50722
|
+
if (action === "deny") {
|
|
50723
|
+
lists.deny.push(pattern);
|
|
50724
|
+
continue;
|
|
50725
|
+
}
|
|
50726
|
+
const shadowing = shadowingRestrictions(rawPattern);
|
|
50727
|
+
if (shadowing.length > 0) {
|
|
50728
|
+
logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched call prompts), so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool keeps prompting for "${toolName}".`);
|
|
50729
|
+
continue;
|
|
50730
|
+
}
|
|
50731
|
+
lists.allow.push(pattern);
|
|
50732
|
+
}
|
|
50733
|
+
return {
|
|
50734
|
+
allow: uniq(lists.allow),
|
|
50735
|
+
deny: uniq(lists.deny)
|
|
50736
|
+
};
|
|
50737
|
+
}
|
|
50738
|
+
function unrepresentableDenies(rules) {
|
|
50739
|
+
return patternsWithAction(rules, ["deny"]).filter((pattern) => toPoolToolPattern(pattern) === void 0);
|
|
50740
|
+
}
|
|
50741
|
+
/**
|
|
50742
|
+
* Group the non-file categories by the Pool tool they land on, reporting the
|
|
50743
|
+
* ones Pool has no place for. Two categories can resolve to the same tool
|
|
50744
|
+
* (`bash` and a pass-through `shell`).
|
|
50745
|
+
*/
|
|
50746
|
+
function groupCategoriesByPoolTool({ permission, logger }) {
|
|
50747
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
50748
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
50749
|
+
if (isPrototypePollutionKey(category) || isFileCategory(category)) continue;
|
|
50750
|
+
if (category === "*") {
|
|
50751
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
50752
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
50753
|
+
logger?.warn(`Pool has no rule that spans every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
50754
|
+
}
|
|
50755
|
+
continue;
|
|
50756
|
+
}
|
|
50757
|
+
if (isMcpCategory(category)) {
|
|
50758
|
+
logger?.warn(`Pool approves MCP tools per server under "mcp_servers.<server>.allow", so the rules for "${category}" were skipped; author them as "poolAllow" on the server in .rulesync/mcp.json instead.`);
|
|
50759
|
+
continue;
|
|
50760
|
+
}
|
|
50761
|
+
const toolName = toPoolToolName(category);
|
|
50762
|
+
byTool.set(toolName, [...byTool.get(toolName) ?? [], [category, rules]]);
|
|
50763
|
+
}
|
|
50764
|
+
return byTool;
|
|
50765
|
+
}
|
|
50766
|
+
/**
|
|
50767
|
+
* Convert the non-file categories into Pool tool lists, keyed by Pool tool
|
|
50768
|
+
* name. The categories that land on the same tool are converted against the
|
|
50769
|
+
* union of their `ask` patterns and their lists are joined, which is safe
|
|
50770
|
+
* because a deny wins over an allow in Pool whichever category wrote it.
|
|
50771
|
+
*/
|
|
50772
|
+
function convertRulesyncToPoolTools({ permission, logger }) {
|
|
50773
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
50774
|
+
for (const [toolName, categories] of groupCategoriesByPoolTool({
|
|
50775
|
+
permission,
|
|
50776
|
+
logger
|
|
50777
|
+
})) {
|
|
50778
|
+
const restrictions = categories.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...unrepresentableDenies(rules)]);
|
|
50779
|
+
const lists = {
|
|
50780
|
+
allow: [],
|
|
50781
|
+
deny: []
|
|
50782
|
+
};
|
|
50783
|
+
for (const [category, rules] of categories) {
|
|
50784
|
+
const converted = convertToolCategory({
|
|
50785
|
+
category,
|
|
50786
|
+
toolName,
|
|
50787
|
+
rules,
|
|
50788
|
+
restrictions,
|
|
50789
|
+
logger
|
|
50790
|
+
});
|
|
50791
|
+
lists.allow.push(...converted.allow);
|
|
50792
|
+
lists.deny.push(...converted.deny);
|
|
50793
|
+
}
|
|
50794
|
+
byTool.set(toolName, {
|
|
50795
|
+
allow: uniq(lists.allow),
|
|
50796
|
+
deny: uniq(lists.deny)
|
|
50797
|
+
});
|
|
50798
|
+
}
|
|
50799
|
+
return byTool;
|
|
50800
|
+
}
|
|
50801
|
+
/**
|
|
50802
|
+
* Convert the file categories into Pool's `paths` lists.
|
|
50803
|
+
*
|
|
50804
|
+
* An allow entry grants reading, and `write: true` grants writing on top, so
|
|
50805
|
+
* a `read` allow is withheld by an overlapping `read` ask and a write allow by
|
|
50806
|
+
* an overlapping `edit`/`write` ask — and by a `read` ask, since the write
|
|
50807
|
+
* entry would read too. A `read` deny lands in `paths.deny`, which Pool lets
|
|
50808
|
+
* win over any allow, and covers an `edit`/`write` deny of the same pattern,
|
|
50809
|
+
* so neither withholds anything; an `edit`/`write` deny without a `read` deny
|
|
50810
|
+
* cannot be written (Pool's deny blocks reads as well), so it is reported and
|
|
50811
|
+
* only withholds the write flags it overlaps. A deny Pool cannot spell is
|
|
50812
|
+
* skipped the same way and withholds the allows it overlaps, so it is honored
|
|
50813
|
+
* by prompting rather than dropped.
|
|
50814
|
+
*/
|
|
50815
|
+
function convertRulesyncToPoolPaths({ permission, global, logger }) {
|
|
50816
|
+
const readRules = rulesOf(permission, READ_CATEGORY);
|
|
50817
|
+
const writeRulesByCategory = WRITE_CATEGORIES.map((category) => [category, rulesOf(permission, category)]);
|
|
50818
|
+
const readAsks = patternsWithAction(readRules, ["ask"]);
|
|
50819
|
+
const fitsScope = ({ category, action, pattern }) => {
|
|
50820
|
+
if (isHomeOrAbsolutePath(pattern) === global) return true;
|
|
50821
|
+
logger?.warn(global ? `Pool's user settings take absolute or "~" paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ${join(POOL_DIR, POOL_SETTINGS_FILE_NAME)} (without --global) instead.` : `Pool's project settings take project-relative paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ~/${join(POOL_GLOBAL_DIR, POOL_SETTINGS_FILE_NAME)} (via --global) instead.`);
|
|
50822
|
+
return false;
|
|
50823
|
+
};
|
|
50824
|
+
const deny = [];
|
|
50825
|
+
const readRestrictions = [...readAsks];
|
|
50826
|
+
for (const pattern of patternsWithAction(readRules, ["deny"])) {
|
|
50827
|
+
const path = toPoolPathPattern(pattern);
|
|
50828
|
+
if (path === void 0) {
|
|
50829
|
+
warnSkippedPathPattern({
|
|
50830
|
+
category: READ_CATEGORY,
|
|
50831
|
+
action: "deny",
|
|
50832
|
+
pattern,
|
|
50833
|
+
logger
|
|
50834
|
+
});
|
|
50835
|
+
readRestrictions.push(pattern);
|
|
50836
|
+
continue;
|
|
50837
|
+
}
|
|
50838
|
+
if (!fitsScope({
|
|
50839
|
+
category: READ_CATEGORY,
|
|
50840
|
+
action: "deny",
|
|
50841
|
+
pattern
|
|
50842
|
+
})) continue;
|
|
50843
|
+
if (!deny.some((entry) => entry[POOL_PATH_KEY] === path)) deny.push({ [POOL_PATH_KEY]: path });
|
|
50844
|
+
}
|
|
50845
|
+
const isDeniedForRead = (pattern) => {
|
|
50846
|
+
const path = toPoolPathPattern(pattern);
|
|
50847
|
+
return path !== void 0 && deny.some((entry) => entry[POOL_PATH_KEY] === path);
|
|
50848
|
+
};
|
|
50849
|
+
const writeRestrictions = writeRulesByCategory.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...patternsWithAction(rules, ["deny"]).filter((pattern) => !isDeniedForRead(pattern))]);
|
|
50850
|
+
reportUnwrittenWriteDenies({
|
|
50851
|
+
writeRulesByCategory,
|
|
50852
|
+
isDeniedForRead,
|
|
50853
|
+
logger
|
|
50854
|
+
});
|
|
50855
|
+
const shadowingReadRestrictions = createShadowingRestrictionsTest(asRestrictions(readRestrictions));
|
|
50856
|
+
const shadowingWriteRestrictions = createShadowingRestrictionsTest(asRestrictions([...readRestrictions, ...writeRestrictions]));
|
|
50857
|
+
const allowByPath = /* @__PURE__ */ new Map();
|
|
50858
|
+
for (const pattern of patternsWithAction(readRules, ["allow"])) {
|
|
50859
|
+
if (!fitsScope({
|
|
50860
|
+
category: READ_CATEGORY,
|
|
50861
|
+
action: "allow",
|
|
50862
|
+
pattern
|
|
50863
|
+
})) continue;
|
|
50864
|
+
const path = toPoolPathPattern(pattern);
|
|
50865
|
+
if (path === void 0) {
|
|
50866
|
+
warnSkippedPathPattern({
|
|
50867
|
+
category: READ_CATEGORY,
|
|
50868
|
+
action: "allow",
|
|
50869
|
+
pattern,
|
|
50870
|
+
logger
|
|
50871
|
+
});
|
|
50872
|
+
continue;
|
|
50873
|
+
}
|
|
50874
|
+
const shadowing = shadowingReadRestrictions(pattern);
|
|
50875
|
+
if (shadowing.length > 0) {
|
|
50876
|
+
logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched path prompts), so the "allow" rule for "read" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}.`);
|
|
50877
|
+
continue;
|
|
50878
|
+
}
|
|
50879
|
+
allowByPath.set(path, { [POOL_PATH_KEY]: path });
|
|
50880
|
+
}
|
|
50881
|
+
for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["allow"])) {
|
|
50882
|
+
if (!fitsScope({
|
|
50883
|
+
category,
|
|
50884
|
+
action: "allow",
|
|
50885
|
+
pattern
|
|
50886
|
+
})) continue;
|
|
50887
|
+
const path = toPoolPathPattern(pattern);
|
|
50888
|
+
if (path === void 0) {
|
|
50889
|
+
warnSkippedPathPattern({
|
|
50890
|
+
category,
|
|
50891
|
+
action: "allow",
|
|
50892
|
+
pattern,
|
|
50893
|
+
logger
|
|
50894
|
+
});
|
|
50895
|
+
continue;
|
|
50896
|
+
}
|
|
50897
|
+
const shadowing = shadowingWriteRestrictions(pattern);
|
|
50898
|
+
if (shadowing.length > 0) {
|
|
50899
|
+
logger?.warn(`A Pool path allowed with "write: true" can be read and written freely, so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool prompts for those writes.`);
|
|
50900
|
+
continue;
|
|
50901
|
+
}
|
|
50902
|
+
allowByPath.set(path, {
|
|
50903
|
+
[POOL_PATH_KEY]: path,
|
|
50904
|
+
[POOL_WRITE_KEY]: true
|
|
50905
|
+
});
|
|
50906
|
+
}
|
|
50907
|
+
return {
|
|
50908
|
+
allow: [...allowByPath.values()],
|
|
50909
|
+
deny
|
|
50910
|
+
};
|
|
50911
|
+
}
|
|
50912
|
+
function warnSkippedPathPattern({ category, action, pattern, logger }) {
|
|
50913
|
+
logger?.warn(`Pool path rules treat only "*" and "**" as wildcards, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
50914
|
+
}
|
|
50915
|
+
function reportUnwrittenWriteDenies({ writeRulesByCategory, isDeniedForRead, logger }) {
|
|
50916
|
+
for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["deny"])) {
|
|
50917
|
+
if (isDeniedForRead(pattern)) continue;
|
|
50918
|
+
if (toPoolPathPattern(pattern) === void 0) {
|
|
50919
|
+
warnSkippedPathPattern({
|
|
50920
|
+
category,
|
|
50921
|
+
action: "deny",
|
|
50922
|
+
pattern,
|
|
50923
|
+
logger
|
|
50924
|
+
});
|
|
50925
|
+
continue;
|
|
50926
|
+
}
|
|
50927
|
+
logger?.warn(`Pool cannot deny writes without denying reads, so the "deny" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was not written and Pool prompts for those writes instead; deny the pattern under "read" as well to block it entirely.`);
|
|
50928
|
+
}
|
|
50929
|
+
}
|
|
50930
|
+
function isUsablePattern(pattern) {
|
|
50931
|
+
return typeof pattern === "string" && pattern !== "" && !isPrototypePollutionKey(pattern);
|
|
50932
|
+
}
|
|
50933
|
+
function pathOfEntry(entry) {
|
|
50934
|
+
return isRecord$1(entry) && isUsablePattern(entry[POOL_PATH_KEY]) ? entry[POOL_PATH_KEY] : void 0;
|
|
50935
|
+
}
|
|
50936
|
+
/**
|
|
50937
|
+
* Convert one `tools.<name>` block into its canonical bucket. A disabled tool
|
|
50938
|
+
* never runs, so it reads as a catch-all deny and its own lists are ignored;
|
|
50939
|
+
* a denied pattern wins over an allowed one, as it does in Pool.
|
|
50940
|
+
*/
|
|
50941
|
+
function importPoolTool({ toolConfig, bucket }) {
|
|
50942
|
+
if (toolConfig[POOL_DISABLED_KEY] === true) {
|
|
50943
|
+
bucket[CATCH_ALL_PATTERN$2] = "deny";
|
|
50944
|
+
return;
|
|
50945
|
+
}
|
|
50946
|
+
const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
|
|
50947
|
+
const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
|
|
50948
|
+
for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
|
|
50949
|
+
for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
|
|
50950
|
+
}
|
|
50951
|
+
/**
|
|
50952
|
+
* Convert Pool's `tools` and `paths` blocks back into a rulesync config.
|
|
50953
|
+
* `mcp_servers` approvals are the mcp feature's (`poolAllow`) and are not read.
|
|
50954
|
+
*/
|
|
50955
|
+
function convertPoolToRulesync({ tools, paths }) {
|
|
50956
|
+
const permission = {};
|
|
50957
|
+
const bucketFor = (category) => {
|
|
50958
|
+
const own = lookupOwn({
|
|
50959
|
+
record: permission,
|
|
50960
|
+
key: category
|
|
50961
|
+
});
|
|
50962
|
+
if (own !== void 0) return own;
|
|
50963
|
+
const created = {};
|
|
50964
|
+
permission[category] = created;
|
|
50965
|
+
return created;
|
|
50966
|
+
};
|
|
50967
|
+
for (const [toolName, toolConfig] of Object.entries(tools)) {
|
|
50968
|
+
if (isPrototypePollutionKey(toolName) || !isRecord$1(toolConfig)) continue;
|
|
50969
|
+
importPoolTool({
|
|
50970
|
+
toolConfig,
|
|
50971
|
+
bucket: bucketFor(toCanonicalToolName$4(toolName))
|
|
50972
|
+
});
|
|
50973
|
+
}
|
|
50974
|
+
const denyEntries = Array.isArray(paths[POOL_DENY_KEY]) ? paths[POOL_DENY_KEY] : [];
|
|
50975
|
+
const allowEntries = Array.isArray(paths[POOL_ALLOW_KEY]) ? paths[POOL_ALLOW_KEY] : [];
|
|
50976
|
+
for (const entry of denyEntries) {
|
|
50977
|
+
const path = pathOfEntry(entry);
|
|
50978
|
+
if (path === void 0) continue;
|
|
50979
|
+
for (const category of FILE_CATEGORIES) bucketFor(category)[path] = "deny";
|
|
50980
|
+
}
|
|
50981
|
+
for (const entry of allowEntries) {
|
|
50982
|
+
const path = pathOfEntry(entry);
|
|
50983
|
+
if (path === void 0) continue;
|
|
50984
|
+
bucketFor(READ_CATEGORY)[path] ??= "allow";
|
|
50985
|
+
if (isRecord$1(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
|
|
50986
|
+
}
|
|
50987
|
+
return { permission };
|
|
50988
|
+
}
|
|
50989
|
+
//#endregion
|
|
49931
50990
|
//#region src/features/permissions/qwencode-permissions.ts
|
|
49932
50991
|
/**
|
|
49933
50992
|
* Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
|
|
@@ -56030,6 +57089,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
56030
57089
|
supportsImport: true
|
|
56031
57090
|
}
|
|
56032
57091
|
}],
|
|
57092
|
+
["pool", {
|
|
57093
|
+
class: PoolPermissions,
|
|
57094
|
+
meta: {
|
|
57095
|
+
supportsProject: true,
|
|
57096
|
+
supportsGlobal: true,
|
|
57097
|
+
supportsImport: true
|
|
57098
|
+
}
|
|
57099
|
+
}],
|
|
56033
57100
|
["qwencode", {
|
|
56034
57101
|
class: QwencodePermissions,
|
|
56035
57102
|
meta: {
|
|
@@ -56693,8 +57760,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56693
57760
|
const changedPaths = [];
|
|
56694
57761
|
for (const aiDir of aiDirs) {
|
|
56695
57762
|
const dirPath = aiDir.getDirPath();
|
|
56696
|
-
let dirHasChanges = false;
|
|
56697
57763
|
const mainFile = aiDir.getMainFile();
|
|
57764
|
+
const otherFiles = aiDir.getOtherFiles();
|
|
57765
|
+
const writtenPaths = [
|
|
57766
|
+
dirPath,
|
|
57767
|
+
...mainFile ? [join(dirPath, mainFile.name)] : [],
|
|
57768
|
+
...otherFiles.map((file) => join(dirPath, file.relativeFilePathToDirPath))
|
|
57769
|
+
];
|
|
57770
|
+
if (await refusesAnyWriteOutsideRoot({
|
|
57771
|
+
logger: this.logger,
|
|
57772
|
+
rootPath: aiDir.getOutputRoot(),
|
|
57773
|
+
targetPaths: writtenPaths
|
|
57774
|
+
})) continue;
|
|
57775
|
+
let dirHasChanges = false;
|
|
56698
57776
|
let mainFileContent;
|
|
56699
57777
|
if (mainFile) {
|
|
56700
57778
|
const mainFilePath = join(dirPath, mainFile.name);
|
|
@@ -56706,7 +57784,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56706
57784
|
existing: existingContent
|
|
56707
57785
|
})) dirHasChanges = true;
|
|
56708
57786
|
}
|
|
56709
|
-
const otherFiles = aiDir.getOtherFiles();
|
|
56710
57787
|
for (const file of otherFiles) {
|
|
56711
57788
|
if (dirHasChanges) break;
|
|
56712
57789
|
const filePath = join(dirPath, file.relativeFilePathToDirPath);
|
|
@@ -68825,6 +69902,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
|
|
|
68825
69902
|
}
|
|
68826
69903
|
};
|
|
68827
69904
|
//#endregion
|
|
69905
|
+
//#region src/features/subagents/pool-subagent.ts
|
|
69906
|
+
/** How Pool runs an agent; a custom agent defaults to `in_process`. */
|
|
69907
|
+
const POOL_AGENT_TYPES = [
|
|
69908
|
+
"in_process",
|
|
69909
|
+
"command",
|
|
69910
|
+
"agent_server"
|
|
69911
|
+
];
|
|
69912
|
+
const DEFAULT_POOL_AGENT_TYPE = "in_process";
|
|
69913
|
+
/**
|
|
69914
|
+
* One entry of `subagents.agents.<name>` in Pool's settings file. `type` is
|
|
69915
|
+
* required by Pool and `description` by every enabled custom agent; the
|
|
69916
|
+
* remaining keys depend on the type (`command`/`args`/`env` for `command`,
|
|
69917
|
+
* `agent_server`/`session_config_options` for `agent_server`). Loose so a key
|
|
69918
|
+
* Pool adds later survives the round-trip.
|
|
69919
|
+
* @see https://docs.poolside.ai/subagents
|
|
69920
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
69921
|
+
*/
|
|
69922
|
+
const PoolAgentSchema = z.looseObject({
|
|
69923
|
+
type: z.optional(z.enum(POOL_AGENT_TYPES)),
|
|
69924
|
+
description: z.optional(z.string()),
|
|
69925
|
+
instructions: z.optional(z.string()),
|
|
69926
|
+
command: z.optional(z.string()),
|
|
69927
|
+
args: z.optional(z.array(z.string())),
|
|
69928
|
+
env: z.optional(z.record(z.string(), z.string())),
|
|
69929
|
+
agent_server: z.optional(z.string()),
|
|
69930
|
+
session_config_options: z.optional(z.record(z.string(), z.unknown())),
|
|
69931
|
+
inherit_agent_config: z.optional(z.boolean()),
|
|
69932
|
+
disabled: z.optional(z.boolean())
|
|
69933
|
+
});
|
|
69934
|
+
/**
|
|
69935
|
+
* Single spelling of the settings.yaml codec/policy, matching PoolMcp: fail
|
|
69936
|
+
* closed on an unparseable root rather than replacing the user's primary Pool
|
|
69937
|
+
* settings with generated output.
|
|
69938
|
+
*/
|
|
69939
|
+
function parsePoolSettings(fileContent, filePath) {
|
|
69940
|
+
return parseSharedConfig({
|
|
69941
|
+
format: "yaml",
|
|
69942
|
+
fileContent,
|
|
69943
|
+
filePath,
|
|
69944
|
+
invalidRootPolicy: "error"
|
|
69945
|
+
});
|
|
69946
|
+
}
|
|
69947
|
+
/**
|
|
69948
|
+
* The file stem an imported agent lands under in `.rulesync/subagents/`. Pool
|
|
69949
|
+
* keys agents by an arbitrary YAML mapping key, so the name is narrowed to a
|
|
69950
|
+
* safe file name before it becomes a path (defense in depth on top of the
|
|
69951
|
+
* central path-traversal guard); the agent keeps its real name in `name`.
|
|
69952
|
+
*/
|
|
69953
|
+
function sanitizePoolAgentFileStem(raw) {
|
|
69954
|
+
const stem = raw.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
69955
|
+
return stem.length > 0 ? stem : "agent";
|
|
69956
|
+
}
|
|
69957
|
+
/**
|
|
69958
|
+
* Read `subagents.agents` out of a parsed settings document. The reserved
|
|
69959
|
+
* `general` agent is Pool's built-in and stays the user's to configure, so it
|
|
69960
|
+
* is never imported; an entry that is not a mapping is skipped.
|
|
69961
|
+
*/
|
|
69962
|
+
function agentsOfSettings({ settings, filePath, logger }) {
|
|
69963
|
+
const block = settings[POOL_SUBAGENTS_KEY];
|
|
69964
|
+
const rawAgents = isRecord$1(block) ? block[POOL_SUBAGENTS_AGENTS_KEY] : void 0;
|
|
69965
|
+
const agents = {};
|
|
69966
|
+
if (!isRecord$1(rawAgents)) return agents;
|
|
69967
|
+
for (const [name, raw] of Object.entries(rawAgents)) {
|
|
69968
|
+
if (isPrototypePollutionKey(name) || name === "general") continue;
|
|
69969
|
+
const result = PoolAgentSchema.safeParse(raw);
|
|
69970
|
+
if (!result.success) {
|
|
69971
|
+
logger?.warn(`Skipped Pool subagent ${quoteValueForWarning(name)} in ${filePath}: ${formatError(result.error)}`);
|
|
69972
|
+
continue;
|
|
69973
|
+
}
|
|
69974
|
+
agents[name] = result.data;
|
|
69975
|
+
}
|
|
69976
|
+
return agents;
|
|
69977
|
+
}
|
|
69978
|
+
/**
|
|
69979
|
+
* Pool subagents.
|
|
69980
|
+
*
|
|
69981
|
+
* Pool defines subagents inside its settings file — `.poolside/settings.yaml`
|
|
69982
|
+
* at project scope and `~/.config/poolside/settings.yaml` at user scope —
|
|
69983
|
+
* under `subagents.agents.<name>`, so every targeted rulesync subagent is
|
|
69984
|
+
* collapsed into that one file: the body becomes `instructions`, the shared
|
|
69985
|
+
* `description` is written as-is and the `pool:` frontmatter section supplies
|
|
69986
|
+
* `type` (default `in_process`) and the type-specific keys. rulesync owns the
|
|
69987
|
+
* `agents` map except for Pool's built-in `general` entry, which is carried
|
|
69988
|
+
* over untouched together with `subagents.default` and every other top-level
|
|
69989
|
+
* key of the file (`mcp_servers`, `tools`, `pool`, ...); the file is never
|
|
69990
|
+
* deleted.
|
|
69991
|
+
*
|
|
69992
|
+
* @see https://docs.poolside.ai/subagents
|
|
69993
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
69994
|
+
*/
|
|
69995
|
+
var PoolSubagent = class extends ToolSubagent {
|
|
69996
|
+
agents;
|
|
69997
|
+
logger;
|
|
69998
|
+
/**
|
|
69999
|
+
* The settings document to write, built lazily: the writer hands over the
|
|
70000
|
+
* existing file through `setFileContent` first, and a file that does not
|
|
70001
|
+
* exist yet gets the block merged into an empty document. Lazy so that a
|
|
70002
|
+
* `forDeletion` instance never consults the gateway.
|
|
70003
|
+
*/
|
|
70004
|
+
mergedContent = null;
|
|
70005
|
+
constructor({ agents, logger, ...rest }) {
|
|
70006
|
+
super({
|
|
70007
|
+
...rest,
|
|
70008
|
+
fileContent: ""
|
|
70009
|
+
});
|
|
70010
|
+
this.agents = agents;
|
|
70011
|
+
this.logger = logger;
|
|
70012
|
+
}
|
|
70013
|
+
getAgents() {
|
|
70014
|
+
return this.agents;
|
|
70015
|
+
}
|
|
70016
|
+
isDeletable() {
|
|
70017
|
+
return false;
|
|
70018
|
+
}
|
|
70019
|
+
shouldMergeExistingFileContent() {
|
|
70020
|
+
return true;
|
|
70021
|
+
}
|
|
70022
|
+
setFileContent(newFileContent) {
|
|
70023
|
+
this.mergedContent = this.mergeInto(newFileContent);
|
|
70024
|
+
}
|
|
70025
|
+
getFileContent() {
|
|
70026
|
+
if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
|
|
70027
|
+
return this.mergedContent;
|
|
70028
|
+
}
|
|
70029
|
+
/**
|
|
70030
|
+
* Rebuild the `subagents` block on top of an existing settings document:
|
|
70031
|
+
* `default` and any other sibling of `agents` are kept, the built-in
|
|
70032
|
+
* `general` agent is carried over, and every other agent is replaced by the
|
|
70033
|
+
* generated set — an agent removed from `.rulesync/subagents/` must not
|
|
70034
|
+
* survive here. A block left empty is retracted rather than written as `{}`.
|
|
70035
|
+
*/
|
|
70036
|
+
mergeInto(existingContent) {
|
|
70037
|
+
const paths = {
|
|
70038
|
+
relativeDirPath: this.getRelativeDirPath(),
|
|
70039
|
+
relativeFilePath: this.getRelativeFilePath()
|
|
70040
|
+
};
|
|
70041
|
+
const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
|
|
70042
|
+
const existing = parsePoolSettings(existingContent, filePath);
|
|
70043
|
+
const existingBlock = isRecord$1(existing["subagents"]) ? existing[POOL_SUBAGENTS_KEY] : {};
|
|
70044
|
+
const block = {};
|
|
70045
|
+
for (const [key, value] of Object.entries(existingBlock)) if (!isPrototypePollutionKey(key) && key !== "agents") block[key] = value;
|
|
70046
|
+
const existingAgents = isRecord$1(existingBlock["agents"]) ? existingBlock[POOL_SUBAGENTS_AGENTS_KEY] : {};
|
|
70047
|
+
const general = existingAgents[POOL_GENERAL_AGENT_NAME];
|
|
70048
|
+
const agents = {
|
|
70049
|
+
...general !== void 0 ? { [POOL_GENERAL_AGENT_NAME]: general } : {},
|
|
70050
|
+
...this.agents
|
|
70051
|
+
};
|
|
70052
|
+
const dropped = Object.keys(existingAgents).filter((name) => name !== "general" && !Object.hasOwn(agents, name));
|
|
70053
|
+
if (dropped.length > 0) {
|
|
70054
|
+
const advice = this.global ? "hand-written agents belong in .rulesync/subagents/" : `hand-written agents belong in ${POOL_SETTINGS_LOCAL_FILE_NAME}`;
|
|
70055
|
+
this.logger?.warn(`Removed Pool subagent(s) ${dropped.map(quoteValueForWarning).join(", ")} from ${filePath}: rulesync recomputes "${POOL_SUBAGENTS_KEY}.${POOL_SUBAGENTS_AGENTS_KEY}" from its own subagents on every run, so ${advice}.`);
|
|
70056
|
+
}
|
|
70057
|
+
if (Object.keys(agents).length > 0) block[POOL_SUBAGENTS_AGENTS_KEY] = agents;
|
|
70058
|
+
return applySharedConfigPatch({
|
|
70059
|
+
fileKey: sharedConfigFileKey(paths),
|
|
70060
|
+
feature: "subagents",
|
|
70061
|
+
existingContent,
|
|
70062
|
+
patch: { [POOL_SUBAGENTS_KEY]: Object.keys(block).length > 0 ? block : void 0 },
|
|
70063
|
+
filePath,
|
|
70064
|
+
logger: this.logger
|
|
70065
|
+
});
|
|
70066
|
+
}
|
|
70067
|
+
/**
|
|
70068
|
+
* Every agent lives in the one settings file, which the MCP feature writes
|
|
70069
|
+
* too: naming the file here keeps the gitignore derivation from claiming the
|
|
70070
|
+
* whole directory and lets the shared-file derivation order the writers.
|
|
70071
|
+
*/
|
|
70072
|
+
static getSettablePaths({ global = false } = {}) {
|
|
70073
|
+
return {
|
|
70074
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
70075
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
70076
|
+
};
|
|
70077
|
+
}
|
|
70078
|
+
/**
|
|
70079
|
+
* Map a single rulesync subagent to a Pool agent entry. The body becomes
|
|
70080
|
+
* `instructions` and the shared `description` is written as-is; the
|
|
70081
|
+
* optional `pool:` section supplies `type` and the type-specific keys and
|
|
70082
|
+
* may override both. Returns `null` for the reserved `general` name, which
|
|
70083
|
+
* Pool defines itself.
|
|
70084
|
+
*/
|
|
70085
|
+
static toPoolAgent({ rulesyncSubagent, logger }) {
|
|
70086
|
+
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
70087
|
+
const name = frontmatter.name;
|
|
70088
|
+
if (name === "general") {
|
|
70089
|
+
logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: "${POOL_GENERAL_AGENT_NAME}" is Pool's built-in agent, so configure it in settings.yaml directly.`);
|
|
70090
|
+
return null;
|
|
70091
|
+
}
|
|
70092
|
+
if (isPrototypePollutionKey(name)) {
|
|
70093
|
+
logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: unsafe name.`);
|
|
70094
|
+
return null;
|
|
70095
|
+
}
|
|
70096
|
+
const rawSection = isRecord$1(frontmatter.pool) ? frontmatter.pool : {};
|
|
70097
|
+
const result = PoolAgentSchema.safeParse(rawSection);
|
|
70098
|
+
if (!result.success) throw new Error(`Invalid "pool" section of subagent ${quoteValueForWarning(name)}: ${formatError(result.error)}`);
|
|
70099
|
+
const { type, description, instructions, ...rest } = result.data;
|
|
70100
|
+
const body = rulesyncSubagent.getBody().trim();
|
|
70101
|
+
const effectiveDescription = description ?? frontmatter.description;
|
|
70102
|
+
const effectiveInstructions = instructions ?? (body !== "" ? body : void 0);
|
|
70103
|
+
if (effectiveDescription === void 0 && rest.disabled !== true) logger?.warn(`Pool subagent ${quoteValueForWarning(name)} has no description; Pool requires one for every enabled custom agent.`);
|
|
70104
|
+
return {
|
|
70105
|
+
name,
|
|
70106
|
+
agent: {
|
|
70107
|
+
type: type ?? DEFAULT_POOL_AGENT_TYPE,
|
|
70108
|
+
...effectiveDescription !== void 0 ? { description: effectiveDescription } : {},
|
|
70109
|
+
...effectiveInstructions !== void 0 ? { instructions: effectiveInstructions } : {},
|
|
70110
|
+
...rest
|
|
70111
|
+
}
|
|
70112
|
+
};
|
|
70113
|
+
}
|
|
70114
|
+
/**
|
|
70115
|
+
* Aggregate every targeted rulesync subagent into the one settings file.
|
|
70116
|
+
* Agents are keyed by their `name`; a repeated name keeps the last one, with
|
|
70117
|
+
* a warning, so the map stays what Pool would read.
|
|
70118
|
+
*/
|
|
70119
|
+
static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true, global = false, logger }) {
|
|
70120
|
+
const agents = {};
|
|
70121
|
+
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
70122
|
+
const converted = this.toPoolAgent({
|
|
70123
|
+
rulesyncSubagent,
|
|
70124
|
+
logger
|
|
70125
|
+
});
|
|
70126
|
+
if (converted === null) continue;
|
|
70127
|
+
if (Object.hasOwn(agents, converted.name)) logger?.warn(`Pool subagent ${quoteValueForWarning(converted.name)} is defined more than once; the last definition (${rulesyncSubagent.getRelativePathFromCwd()}) wins.`);
|
|
70128
|
+
agents[converted.name] = converted.agent;
|
|
70129
|
+
}
|
|
70130
|
+
return new this({
|
|
70131
|
+
outputRoot,
|
|
70132
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
70133
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME,
|
|
70134
|
+
agents,
|
|
70135
|
+
validate,
|
|
70136
|
+
global,
|
|
70137
|
+
logger
|
|
70138
|
+
});
|
|
70139
|
+
}
|
|
70140
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
70141
|
+
return this.fromRulesyncSubagents({
|
|
70142
|
+
outputRoot,
|
|
70143
|
+
rulesyncSubagents: [rulesyncSubagent],
|
|
70144
|
+
validate,
|
|
70145
|
+
global,
|
|
70146
|
+
logger
|
|
70147
|
+
});
|
|
70148
|
+
}
|
|
70149
|
+
/**
|
|
70150
|
+
* Convert every custom agent of the settings file back into an individual
|
|
70151
|
+
* rulesync subagent. `instructions` becomes the body, `description` the
|
|
70152
|
+
* shared field, and everything else — `type` only when it is not the
|
|
70153
|
+
* default — rides the `pool:` section so it survives the round-trip.
|
|
70154
|
+
*/
|
|
70155
|
+
toRulesyncSubagents() {
|
|
70156
|
+
return Object.entries(this.agents).map(([name, agent]) => {
|
|
70157
|
+
const { type, description, instructions, ...rest } = agent;
|
|
70158
|
+
const poolSection = {
|
|
70159
|
+
...type !== void 0 && type !== DEFAULT_POOL_AGENT_TYPE ? { type } : {},
|
|
70160
|
+
...rest
|
|
70161
|
+
};
|
|
70162
|
+
const rulesyncFrontmatter = {
|
|
70163
|
+
targets: ["pool"],
|
|
70164
|
+
name,
|
|
70165
|
+
...description !== void 0 ? { description } : {},
|
|
70166
|
+
...Object.keys(poolSection).length > 0 ? { pool: poolSection } : {}
|
|
70167
|
+
};
|
|
70168
|
+
return new RulesyncSubagent({
|
|
70169
|
+
outputRoot: process.cwd(),
|
|
70170
|
+
frontmatter: rulesyncFrontmatter,
|
|
70171
|
+
body: instructions ?? "",
|
|
70172
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
70173
|
+
relativeFilePath: `${sanitizePoolAgentFileStem(name)}.md`,
|
|
70174
|
+
validate: true
|
|
70175
|
+
});
|
|
70176
|
+
});
|
|
70177
|
+
}
|
|
70178
|
+
toRulesyncSubagent() {
|
|
70179
|
+
const first = this.toRulesyncSubagents()[0];
|
|
70180
|
+
if (!first) throw new Error(`No custom subagents found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())} to convert.`);
|
|
70181
|
+
return first;
|
|
70182
|
+
}
|
|
70183
|
+
validate() {
|
|
70184
|
+
return {
|
|
70185
|
+
success: true,
|
|
70186
|
+
error: null
|
|
70187
|
+
};
|
|
70188
|
+
}
|
|
70189
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
70190
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
70191
|
+
rulesyncSubagent,
|
|
70192
|
+
toolTarget: "pool"
|
|
70193
|
+
});
|
|
70194
|
+
}
|
|
70195
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, logger }) {
|
|
70196
|
+
const paths = this.getSettablePaths({ global });
|
|
70197
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
70198
|
+
const settings = parsePoolSettings(await readFileContentOrNull(filePath) ?? "", filePath);
|
|
70199
|
+
return new this({
|
|
70200
|
+
outputRoot,
|
|
70201
|
+
relativeDirPath: paths.relativeDirPath,
|
|
70202
|
+
relativeFilePath,
|
|
70203
|
+
agents: agentsOfSettings({
|
|
70204
|
+
settings,
|
|
70205
|
+
filePath,
|
|
70206
|
+
logger
|
|
70207
|
+
}),
|
|
70208
|
+
validate,
|
|
70209
|
+
global
|
|
70210
|
+
});
|
|
70211
|
+
}
|
|
70212
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
70213
|
+
return new this({
|
|
70214
|
+
outputRoot,
|
|
70215
|
+
relativeDirPath,
|
|
70216
|
+
relativeFilePath,
|
|
70217
|
+
agents: {},
|
|
70218
|
+
validate: false,
|
|
70219
|
+
global
|
|
70220
|
+
});
|
|
70221
|
+
}
|
|
70222
|
+
};
|
|
70223
|
+
//#endregion
|
|
68828
70224
|
//#region src/features/subagents/reasonix-subagent.ts
|
|
68829
70225
|
const ReasonixSubagentFrontmatterSchema = z.looseObject({
|
|
68830
70226
|
name: z.string(),
|
|
@@ -70145,6 +71541,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
70145
71541
|
filePattern: "*.md"
|
|
70146
71542
|
}
|
|
70147
71543
|
}],
|
|
71544
|
+
["pool", {
|
|
71545
|
+
class: PoolSubagent,
|
|
71546
|
+
meta: {
|
|
71547
|
+
supportsProject: true,
|
|
71548
|
+
supportsSimulated: false,
|
|
71549
|
+
supportsGlobal: true,
|
|
71550
|
+
emitsEmptyAggregate: true,
|
|
71551
|
+
filePattern: POOL_SETTINGS_FILE_NAME
|
|
71552
|
+
}
|
|
71553
|
+
}],
|
|
70148
71554
|
["qwencode", {
|
|
70149
71555
|
class: QwencodeSubagent,
|
|
70150
71556
|
meta: {
|
|
@@ -70257,6 +71663,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70257
71663
|
toolTarget;
|
|
70258
71664
|
global;
|
|
70259
71665
|
getFactory;
|
|
71666
|
+
/**
|
|
71667
|
+
* Whether the last `loadRulesyncFiles` found a `subagents/` directory under
|
|
71668
|
+
* any input root. A project without one has not adopted the feature, so the
|
|
71669
|
+
* agents in its Pool settings are its own and must not be retracted.
|
|
71670
|
+
*/
|
|
71671
|
+
rulesyncSourceDirFound = false;
|
|
70260
71672
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
70261
71673
|
super({
|
|
70262
71674
|
outputRoot,
|
|
@@ -70270,16 +71682,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70270
71682
|
this.global = global;
|
|
70271
71683
|
this.getFactory = getFactory;
|
|
70272
71684
|
}
|
|
71685
|
+
emitsToolFilesForEmptySource() {
|
|
71686
|
+
return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
|
|
71687
|
+
}
|
|
70273
71688
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
70274
71689
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
70275
71690
|
const factory = this.getFactory(this.toolTarget);
|
|
70276
71691
|
const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
|
|
70277
71692
|
if (factory.class.fromRulesyncSubagents) {
|
|
70278
|
-
if (targeted.length === 0) return [];
|
|
71693
|
+
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
70279
71694
|
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
70280
71695
|
outputRoot: this.outputRoot,
|
|
70281
71696
|
rulesyncSubagents: targeted,
|
|
70282
|
-
global: this.global
|
|
71697
|
+
global: this.global,
|
|
71698
|
+
logger: this.logger
|
|
70283
71699
|
});
|
|
70284
71700
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
70285
71701
|
}
|
|
@@ -70335,6 +71751,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70335
71751
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
70336
71752
|
return [];
|
|
70337
71753
|
}
|
|
71754
|
+
this.rulesyncSourceDirFound = true;
|
|
70338
71755
|
const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
|
|
70339
71756
|
if (mdFiles.length === 0) {
|
|
70340
71757
|
this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
|
|
@@ -70375,6 +71792,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70375
71792
|
* earlier root's copy.
|
|
70376
71793
|
*/
|
|
70377
71794
|
async loadRulesyncFiles() {
|
|
71795
|
+
this.rulesyncSourceDirFound = false;
|
|
70378
71796
|
const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
|
|
70379
71797
|
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
70380
71798
|
identity: (subagent) => subagent.getRelativeFilePath(),
|
|
@@ -70437,7 +71855,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70437
71855
|
outputRoot: rootOutputRoot,
|
|
70438
71856
|
relativeDirPath: dirPath,
|
|
70439
71857
|
relativeFilePath: toRelativeFilePath(path),
|
|
70440
|
-
global: this.global
|
|
71858
|
+
global: this.global,
|
|
71859
|
+
logger: this.logger
|
|
70441
71860
|
})));
|
|
70442
71861
|
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
70443
71862
|
loaded,
|
|
@@ -74359,22 +75778,30 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
74359
75778
|
}
|
|
74360
75779
|
};
|
|
74361
75780
|
//#endregion
|
|
74362
|
-
//#region src/features/rules/
|
|
75781
|
+
//#region src/features/rules/nested-agentsmd-rule.ts
|
|
74363
75782
|
/**
|
|
74364
|
-
*
|
|
75783
|
+
* Shared adapter for the targets that read instructions exactly the way the
|
|
75784
|
+
* AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
|
|
75785
|
+
* project root, nested per-directory `AGENTS.md` files loaded only while working
|
|
75786
|
+
* under that directory, and a single personal root file under a tool-specific
|
|
75787
|
+
* home directory. Such a target has no modular non-root instruction directory,
|
|
75788
|
+
* so every other non-root rule folds into the root file.
|
|
74365
75789
|
*
|
|
74366
|
-
*
|
|
74367
|
-
*
|
|
74368
|
-
*
|
|
74369
|
-
* `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
|
|
74370
|
-
* single root file with no local overlay, so non-root rules fold into it.
|
|
75790
|
+
* Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
|
|
75791
|
+
* directory and their tool target through {@link getFamily}; the vendor docs
|
|
75792
|
+
* describing each one's discovery walk live on the subclass.
|
|
74371
75793
|
*
|
|
74372
|
-
*
|
|
75794
|
+
* The nested-file scan mirrors the AGENTS.md standard's nested discovery —
|
|
75795
|
+
* same file name, same exclusions, import-only, project scope — because it
|
|
75796
|
+
* discovers literally the same files.
|
|
74373
75797
|
*/
|
|
74374
|
-
var
|
|
75798
|
+
var NestedAgentsmdRule = class extends ToolRule {
|
|
75799
|
+
static getFamily() {
|
|
75800
|
+
throw new Error("Please implement this method in the subclass.");
|
|
75801
|
+
}
|
|
74375
75802
|
static getSettablePaths({ global = false } = {}) {
|
|
74376
75803
|
if (global) return { root: {
|
|
74377
|
-
relativeDirPath:
|
|
75804
|
+
relativeDirPath: this.getFamily().globalDir,
|
|
74378
75805
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74379
75806
|
} };
|
|
74380
75807
|
return { root: {
|
|
@@ -74382,18 +75809,6 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74382
75809
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74383
75810
|
} };
|
|
74384
75811
|
}
|
|
74385
|
-
/**
|
|
74386
|
-
* DeepSeek Harness loads the project chain of `AGENTS.md` files from the
|
|
74387
|
-
* project root (the nearest ancestor holding `.git`) down through the session
|
|
74388
|
-
* cwd, broad to specific, so a per-directory file applies only while working
|
|
74389
|
-
* under that directory. Nested files are therefore a real scoping surface,
|
|
74390
|
-
* not just the root file's overflow.
|
|
74391
|
-
*
|
|
74392
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
74393
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
74394
|
-
* literally the same files.
|
|
74395
|
-
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
74396
|
-
*/
|
|
74397
75812
|
static getNestedFilePatterns() {
|
|
74398
75813
|
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
74399
75814
|
}
|
|
@@ -74408,7 +75823,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74408
75823
|
const { root } = this.getSettablePaths({ global });
|
|
74409
75824
|
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
74410
75825
|
const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
74411
|
-
return new
|
|
75826
|
+
return new this({
|
|
74412
75827
|
outputRoot,
|
|
74413
75828
|
relativeDirPath: overrideDirPath,
|
|
74414
75829
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74418,7 +75833,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74418
75833
|
});
|
|
74419
75834
|
}
|
|
74420
75835
|
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
74421
|
-
return new
|
|
75836
|
+
return new this({
|
|
74422
75837
|
outputRoot,
|
|
74423
75838
|
relativeDirPath: root.relativeDirPath,
|
|
74424
75839
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74432,7 +75847,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74432
75847
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
74433
75848
|
const isRoot = frontmatter.root ?? false;
|
|
74434
75849
|
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
74435
|
-
if (!global && !isRoot && subprojectPath) return new
|
|
75850
|
+
if (!global && !isRoot && subprojectPath) return new this({
|
|
74436
75851
|
outputRoot,
|
|
74437
75852
|
relativeDirPath: join(subprojectPath),
|
|
74438
75853
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74440,7 +75855,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74440
75855
|
validate,
|
|
74441
75856
|
root: false
|
|
74442
75857
|
});
|
|
74443
|
-
return new
|
|
75858
|
+
return new this({
|
|
74444
75859
|
outputRoot,
|
|
74445
75860
|
relativeDirPath: root.relativeDirPath,
|
|
74446
75861
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74461,23 +75876,47 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74461
75876
|
};
|
|
74462
75877
|
}
|
|
74463
75878
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
74464
|
-
|
|
75879
|
+
const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
|
|
75880
|
+
return new this({
|
|
74465
75881
|
outputRoot,
|
|
74466
75882
|
relativeDirPath,
|
|
74467
75883
|
relativeFilePath,
|
|
74468
75884
|
fileContent: "",
|
|
74469
75885
|
validate: false,
|
|
74470
|
-
root:
|
|
75886
|
+
root: isRoot
|
|
74471
75887
|
});
|
|
74472
75888
|
}
|
|
74473
75889
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
74474
75890
|
return this.isTargetedByRulesyncRuleDefault({
|
|
74475
75891
|
rulesyncRule,
|
|
74476
|
-
toolTarget:
|
|
75892
|
+
toolTarget: this.getFamily().toolTarget
|
|
74477
75893
|
});
|
|
74478
75894
|
}
|
|
74479
75895
|
};
|
|
74480
75896
|
//#endregion
|
|
75897
|
+
//#region src/features/rules/dsh-rule.ts
|
|
75898
|
+
/**
|
|
75899
|
+
* DeepSeek Harness (`dsh`) rules.
|
|
75900
|
+
*
|
|
75901
|
+
* Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
|
|
75902
|
+
* files — the project chain `dsh-agent-instructions` loads from the `.git`
|
|
75903
|
+
* root down to the session cwd, broad to specific, so a per-directory file
|
|
75904
|
+
* applies only while working under that directory. Global scope writes the
|
|
75905
|
+
* user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
|
|
75906
|
+
* reads as a single root file with no `.local.md` overlay, so non-root rules
|
|
75907
|
+
* fold into it.
|
|
75908
|
+
*
|
|
75909
|
+
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
75910
|
+
*/
|
|
75911
|
+
var DshRule = class extends NestedAgentsmdRule {
|
|
75912
|
+
static getFamily() {
|
|
75913
|
+
return {
|
|
75914
|
+
globalDir: DSH_DIR,
|
|
75915
|
+
toolTarget: "dsh"
|
|
75916
|
+
};
|
|
75917
|
+
}
|
|
75918
|
+
};
|
|
75919
|
+
//#endregion
|
|
74481
75920
|
//#region src/features/rules/factorydroid-rule.ts
|
|
74482
75921
|
/**
|
|
74483
75922
|
* Rule generator for Factory Droid.
|
|
@@ -75871,110 +77310,24 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
75871
77310
|
};
|
|
75872
77311
|
//#endregion
|
|
75873
77312
|
//#region src/features/rules/pool-rule.ts
|
|
75874
|
-
|
|
75875
|
-
|
|
75876
|
-
|
|
75877
|
-
|
|
75878
|
-
|
|
75879
|
-
|
|
75880
|
-
|
|
75881
|
-
|
|
75882
|
-
|
|
75883
|
-
|
|
75884
|
-
|
|
75885
|
-
|
|
75886
|
-
|
|
75887
|
-
|
|
75888
|
-
* through the working directory (deeper files take precedence), skipping
|
|
75889
|
-
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
75890
|
-
* just the root file's overflow.
|
|
75891
|
-
*
|
|
75892
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
75893
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
75894
|
-
* literally the same files.
|
|
75895
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
75896
|
-
*/
|
|
75897
|
-
static getNestedFilePatterns() {
|
|
75898
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75899
|
-
}
|
|
75900
|
-
/**
|
|
75901
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
75902
|
-
* (project or global).
|
|
75903
|
-
*/
|
|
75904
|
-
getSubprojectPath() {
|
|
75905
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75906
|
-
}
|
|
75907
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
75908
|
-
const { root } = this.getSettablePaths({ global });
|
|
75909
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
75910
|
-
const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
75911
|
-
return new PoolRule({
|
|
75912
|
-
outputRoot,
|
|
75913
|
-
relativeDirPath: overrideDirPath,
|
|
75914
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75915
|
-
fileContent,
|
|
75916
|
-
validate,
|
|
75917
|
-
root: false
|
|
75918
|
-
});
|
|
75919
|
-
}
|
|
75920
|
-
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
75921
|
-
return new PoolRule({
|
|
75922
|
-
outputRoot,
|
|
75923
|
-
relativeDirPath: root.relativeDirPath,
|
|
75924
|
-
relativeFilePath: root.relativeFilePath,
|
|
75925
|
-
fileContent,
|
|
75926
|
-
validate,
|
|
75927
|
-
root: true
|
|
75928
|
-
});
|
|
75929
|
-
}
|
|
75930
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
75931
|
-
const { root } = this.getSettablePaths({ global });
|
|
75932
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
75933
|
-
const isRoot = frontmatter.root ?? false;
|
|
75934
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
75935
|
-
if (!global && !isRoot && subprojectPath) return new PoolRule({
|
|
75936
|
-
outputRoot,
|
|
75937
|
-
relativeDirPath: join(subprojectPath),
|
|
75938
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75939
|
-
fileContent: rulesyncRule.getBody(),
|
|
75940
|
-
validate,
|
|
75941
|
-
root: false
|
|
75942
|
-
});
|
|
75943
|
-
return new PoolRule({
|
|
75944
|
-
outputRoot,
|
|
75945
|
-
relativeDirPath: root.relativeDirPath,
|
|
75946
|
-
relativeFilePath: root.relativeFilePath,
|
|
75947
|
-
fileContent: rulesyncRule.getBody(),
|
|
75948
|
-
validate,
|
|
75949
|
-
root: isRoot
|
|
75950
|
-
});
|
|
75951
|
-
}
|
|
75952
|
-
toRulesyncRule() {
|
|
75953
|
-
const subprojectPath = this.getSubprojectPath();
|
|
75954
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
75955
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
75956
|
-
}
|
|
75957
|
-
validate() {
|
|
77313
|
+
/**
|
|
77314
|
+
* Pool (Poolside) rules.
|
|
77315
|
+
*
|
|
77316
|
+
* Pool reads personal, project, and directory-level `AGENTS.md` files: inside
|
|
77317
|
+
* a git repository it loads every `AGENTS.md` from the repository root down
|
|
77318
|
+
* through the working directory (deeper files take precedence), skipping
|
|
77319
|
+
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
77320
|
+
* just the root file's overflow. The personal file sits below both at
|
|
77321
|
+
* `~/.config/poolside/AGENTS.md`.
|
|
77322
|
+
*
|
|
77323
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
77324
|
+
*/
|
|
77325
|
+
var PoolRule = class extends NestedAgentsmdRule {
|
|
77326
|
+
static getFamily() {
|
|
75958
77327
|
return {
|
|
75959
|
-
|
|
75960
|
-
error: null
|
|
75961
|
-
};
|
|
75962
|
-
}
|
|
75963
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
75964
|
-
return new PoolRule({
|
|
75965
|
-
outputRoot,
|
|
75966
|
-
relativeDirPath,
|
|
75967
|
-
relativeFilePath,
|
|
75968
|
-
fileContent: "",
|
|
75969
|
-
validate: false,
|
|
75970
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
|
|
75971
|
-
});
|
|
75972
|
-
}
|
|
75973
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
75974
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
75975
|
-
rulesyncRule,
|
|
77328
|
+
globalDir: POOL_GLOBAL_DIR,
|
|
75976
77329
|
toolTarget: "pool"
|
|
75977
|
-
}
|
|
77330
|
+
};
|
|
75978
77331
|
}
|
|
75979
77332
|
};
|
|
75980
77333
|
//#endregion
|
|
@@ -76938,110 +78291,23 @@ var TaktRule = class TaktRule extends ToolRule {
|
|
|
76938
78291
|
//#endregion
|
|
76939
78292
|
//#region src/features/rules/vibe-rule.ts
|
|
76940
78293
|
const VIBE_GLOBAL_DIR = ".vibe";
|
|
76941
|
-
|
|
76942
|
-
|
|
76943
|
-
|
|
76944
|
-
|
|
76945
|
-
|
|
76946
|
-
|
|
76947
|
-
|
|
76948
|
-
|
|
76949
|
-
|
|
76950
|
-
|
|
76951
|
-
|
|
76952
|
-
|
|
76953
|
-
|
|
76954
|
-
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
76955
|
-
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
76956
|
-
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
76957
|
-
* the root file's overflow.
|
|
76958
|
-
*
|
|
76959
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
76960
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
76961
|
-
* literally the same files.
|
|
76962
|
-
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
76963
|
-
*/
|
|
76964
|
-
static getNestedFilePatterns() {
|
|
76965
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76966
|
-
}
|
|
76967
|
-
/**
|
|
76968
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
76969
|
-
* (project or global).
|
|
76970
|
-
*/
|
|
76971
|
-
getSubprojectPath() {
|
|
76972
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76973
|
-
}
|
|
76974
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
76975
|
-
const { root } = this.getSettablePaths({ global });
|
|
76976
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
76977
|
-
const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
76978
|
-
return new VibeRule({
|
|
76979
|
-
outputRoot,
|
|
76980
|
-
relativeDirPath: overrideDirPath,
|
|
76981
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
76982
|
-
fileContent,
|
|
76983
|
-
validate,
|
|
76984
|
-
root: false
|
|
76985
|
-
});
|
|
76986
|
-
}
|
|
76987
|
-
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
76988
|
-
return new VibeRule({
|
|
76989
|
-
outputRoot,
|
|
76990
|
-
relativeDirPath: root.relativeDirPath,
|
|
76991
|
-
relativeFilePath: root.relativeFilePath,
|
|
76992
|
-
fileContent,
|
|
76993
|
-
validate,
|
|
76994
|
-
root: true
|
|
76995
|
-
});
|
|
76996
|
-
}
|
|
76997
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
76998
|
-
const { root } = this.getSettablePaths({ global });
|
|
76999
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
77000
|
-
const isRoot = frontmatter.root ?? false;
|
|
77001
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
77002
|
-
if (!global && !isRoot && subprojectPath) return new VibeRule({
|
|
77003
|
-
outputRoot,
|
|
77004
|
-
relativeDirPath: join(subprojectPath),
|
|
77005
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
77006
|
-
fileContent: rulesyncRule.getBody(),
|
|
77007
|
-
validate,
|
|
77008
|
-
root: false
|
|
77009
|
-
});
|
|
77010
|
-
return new VibeRule({
|
|
77011
|
-
outputRoot,
|
|
77012
|
-
relativeDirPath: root.relativeDirPath,
|
|
77013
|
-
relativeFilePath: root.relativeFilePath,
|
|
77014
|
-
fileContent: rulesyncRule.getBody(),
|
|
77015
|
-
validate,
|
|
77016
|
-
root: isRoot
|
|
77017
|
-
});
|
|
77018
|
-
}
|
|
77019
|
-
toRulesyncRule() {
|
|
77020
|
-
const subprojectPath = this.getSubprojectPath();
|
|
77021
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
77022
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
77023
|
-
}
|
|
77024
|
-
validate() {
|
|
78294
|
+
/**
|
|
78295
|
+
* Mistral Vibe rules.
|
|
78296
|
+
*
|
|
78297
|
+
* Vibe's harness manager walks the directories between the workspace root and
|
|
78298
|
+
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
78299
|
+
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
78300
|
+
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
78301
|
+
* the root file's overflow. The personal file is `~/.vibe/AGENTS.md`.
|
|
78302
|
+
*
|
|
78303
|
+
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
78304
|
+
*/
|
|
78305
|
+
var VibeRule = class extends NestedAgentsmdRule {
|
|
78306
|
+
static getFamily() {
|
|
77025
78307
|
return {
|
|
77026
|
-
|
|
77027
|
-
error: null
|
|
77028
|
-
};
|
|
77029
|
-
}
|
|
77030
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
77031
|
-
return new VibeRule({
|
|
77032
|
-
outputRoot,
|
|
77033
|
-
relativeDirPath,
|
|
77034
|
-
relativeFilePath,
|
|
77035
|
-
fileContent: "",
|
|
77036
|
-
validate: false,
|
|
77037
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
|
|
77038
|
-
});
|
|
77039
|
-
}
|
|
77040
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
77041
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
77042
|
-
rulesyncRule,
|
|
78308
|
+
globalDir: VIBE_GLOBAL_DIR,
|
|
77043
78309
|
toolTarget: "vibe"
|
|
77044
|
-
}
|
|
78310
|
+
};
|
|
77045
78311
|
}
|
|
77046
78312
|
};
|
|
77047
78313
|
//#endregion
|
|
@@ -79248,6 +80514,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
|
|
|
79248
80514
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
79249
80515
|
});
|
|
79250
80516
|
const configPath = join(configRoot, relativeConfigPath);
|
|
80517
|
+
if (await refusesWriteOutsideRoot({
|
|
80518
|
+
logger,
|
|
80519
|
+
rootPath: configRoot,
|
|
80520
|
+
targetPath: configPath
|
|
80521
|
+
})) return {
|
|
80522
|
+
count: 0,
|
|
80523
|
+
paths: [],
|
|
80524
|
+
hasDiff: false,
|
|
80525
|
+
sourceLoadFailed: false
|
|
80526
|
+
};
|
|
79251
80527
|
const expectedConfig = mergeEnabledPlugins({
|
|
79252
80528
|
existingContent: await readFileContentOrNull(configPath) ?? "",
|
|
79253
80529
|
filePath: configPath,
|
|
@@ -79715,7 +80991,8 @@ async function processEmptyFeatureGeneration(params) {
|
|
|
79715
80991
|
*/
|
|
79716
80992
|
async function processFeatureWithRulesyncFiles(params) {
|
|
79717
80993
|
const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
|
|
79718
|
-
|
|
80994
|
+
const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
|
|
80995
|
+
if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
|
|
79719
80996
|
config,
|
|
79720
80997
|
processor,
|
|
79721
80998
|
sweepPlan,
|
|
@@ -80947,4 +82224,4 @@ async function importChecksCore(params) {
|
|
|
80947
82224
|
//#endregion
|
|
80948
82225
|
export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
|
|
80949
82226
|
|
|
80950
|
-
//# sourceMappingURL=import-
|
|
82227
|
+
//# sourceMappingURL=import-B2ANxi1V.js.map
|