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
|
@@ -444,36 +444,36 @@ const isFeatureValueEnabled = (value) => {
|
|
|
444
444
|
const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
445
445
|
//#endregion
|
|
446
446
|
//#region src/constants/rulesync-paths.ts
|
|
447
|
-
const { join: join$
|
|
447
|
+
const { join: join$352 } = node_path.posix;
|
|
448
448
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
449
449
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
450
450
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
451
451
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
452
452
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
453
453
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
454
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
454
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$352(RULES_FEATURE_SUBDIR, ".curated");
|
|
455
455
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
456
456
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
457
457
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
458
458
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
459
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
460
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
461
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
462
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
463
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
464
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
465
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
466
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
467
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
468
|
-
join$
|
|
469
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
470
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
459
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$352(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
460
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
461
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
462
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
463
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
464
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
465
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
466
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
467
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
468
|
+
join$352(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
469
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
470
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
471
471
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
472
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
472
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
473
473
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
474
474
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
475
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
476
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
475
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
476
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$352(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
477
477
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
478
478
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
479
479
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -696,6 +696,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
696
696
|
"kiro-cli",
|
|
697
697
|
"kiro-ide",
|
|
698
698
|
"opencode",
|
|
699
|
+
"pool",
|
|
699
700
|
"qwencode",
|
|
700
701
|
"reasonix",
|
|
701
702
|
"roo",
|
|
@@ -777,6 +778,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
777
778
|
"crush",
|
|
778
779
|
"opencode",
|
|
779
780
|
"pi",
|
|
781
|
+
"pool",
|
|
780
782
|
"factorydroid",
|
|
781
783
|
"goose",
|
|
782
784
|
"hermesagent",
|
|
@@ -824,6 +826,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
824
826
|
"kiro-ide",
|
|
825
827
|
"opencode",
|
|
826
828
|
"pi",
|
|
829
|
+
"pool",
|
|
827
830
|
"qwencode",
|
|
828
831
|
"reasonix",
|
|
829
832
|
"roo",
|
|
@@ -1380,6 +1383,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
|
|
|
1380
1383
|
}));
|
|
1381
1384
|
}
|
|
1382
1385
|
/**
|
|
1386
|
+
* How many links {@link writeLandingPath} follows before it treats the chain as
|
|
1387
|
+
* a cycle it cannot write through. Linux itself gives up after 40.
|
|
1388
|
+
*/
|
|
1389
|
+
const MAX_LINK_HOPS = 40;
|
|
1390
|
+
/**
|
|
1391
|
+
* Whether `error` is the ENOTDIR a lookup through a file raises. On the way to
|
|
1392
|
+
* a write it means the same as a missing entry: nothing at or below the path
|
|
1393
|
+
* exists, so nothing there can be a link.
|
|
1394
|
+
*/
|
|
1395
|
+
function isNotADirectoryError(error) {
|
|
1396
|
+
return someErrorInChain(error, (candidate) => "code" in candidate && candidate.code === "ENOTDIR");
|
|
1397
|
+
}
|
|
1398
|
+
/**
|
|
1399
|
+
* Where a write to `targetPath` lands once the OS has followed every link on
|
|
1400
|
+
* the way there, or `null` when the links form a cycle no write can get
|
|
1401
|
+
* through. The target itself usually does not exist yet, so the walk goes
|
|
1402
|
+
* segment by segment from the filesystem root: an existing segment that is a
|
|
1403
|
+
* link is replaced by what it resolves to, and a dangling link — one whose
|
|
1404
|
+
* target does not exist yet, which is exactly the shape of a link planted at
|
|
1405
|
+
* `AGENTS.md` pointing at a `~/.zshenv` that is still to be created — has its
|
|
1406
|
+
* target spliced into the walk in its place, so the segments of that target
|
|
1407
|
+
* are looked up one by one too. Once a segment is missing, nothing below it
|
|
1408
|
+
* exists, and the rest of the path is where the write's `mkdir -p` will put it.
|
|
1409
|
+
*
|
|
1410
|
+
* Walking segment by segment is what keeps a `..` honest. A link target is a
|
|
1411
|
+
* path the OS resolves left to right, so in `sub/../victim` the `..` climbs out
|
|
1412
|
+
* of wherever `sub` leads, not out of the directory `sub` is spelled in; a
|
|
1413
|
+
* lexical `resolve` folds `sub/..` away before `sub` is ever looked at, which
|
|
1414
|
+
* is how a link chain could be spelled to pass the check and still land
|
|
1415
|
+
* outside the root.
|
|
1416
|
+
*/
|
|
1417
|
+
async function writeLandingPath(targetPath) {
|
|
1418
|
+
const start = splitAbsolutePath((0, node_path.isAbsolute)(targetPath) ? targetPath : (0, node_path.resolve)(targetPath));
|
|
1419
|
+
let current = start.root;
|
|
1420
|
+
let pending = start.segments;
|
|
1421
|
+
let exists = true;
|
|
1422
|
+
let linkHops = 0;
|
|
1423
|
+
while (pending.length > 0) {
|
|
1424
|
+
const segment = pending.shift();
|
|
1425
|
+
if (segment === void 0 || segment === "" || segment === ".") continue;
|
|
1426
|
+
if (segment === "..") {
|
|
1427
|
+
current = (0, node_path.dirname)(current);
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
current = (0, node_path.join)(current, segment);
|
|
1431
|
+
if (!exists) continue;
|
|
1432
|
+
let stats;
|
|
1433
|
+
try {
|
|
1434
|
+
stats = await (0, node_fs_promises.lstat)(current);
|
|
1435
|
+
} catch (error) {
|
|
1436
|
+
if (!isFileNotFoundError(error) && !isNotADirectoryError(error)) throw error;
|
|
1437
|
+
exists = false;
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (!stats.isSymbolicLink()) continue;
|
|
1441
|
+
linkHops += 1;
|
|
1442
|
+
if (linkHops > MAX_LINK_HOPS) return null;
|
|
1443
|
+
try {
|
|
1444
|
+
current = await (0, node_fs_promises.realpath)(current);
|
|
1445
|
+
continue;
|
|
1446
|
+
} catch {}
|
|
1447
|
+
const linkTarget = await (0, node_fs_promises.readlink)(current);
|
|
1448
|
+
if ((0, node_path.isAbsolute)(linkTarget)) {
|
|
1449
|
+
const target = splitAbsolutePath(linkTarget);
|
|
1450
|
+
current = target.root;
|
|
1451
|
+
pending = [...target.segments, ...pending];
|
|
1452
|
+
} else {
|
|
1453
|
+
current = (0, node_path.dirname)(current);
|
|
1454
|
+
pending = [...splitPathSegments(linkTarget), ...pending];
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
return current;
|
|
1458
|
+
}
|
|
1459
|
+
/**
|
|
1460
|
+
* Split an absolute path into its root and its raw segments, without folding a
|
|
1461
|
+
* `..` away: {@link writeLandingPath} has to see every `..` where it is spelled.
|
|
1462
|
+
*/
|
|
1463
|
+
function splitAbsolutePath(absolutePath) {
|
|
1464
|
+
const root = (0, node_path.parse)(absolutePath).root;
|
|
1465
|
+
return {
|
|
1466
|
+
root,
|
|
1467
|
+
segments: splitPathSegments(absolutePath.slice(root.length))
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
/**
|
|
1471
|
+
* Whether writing `targetPath` would land outside `rootPath` once every link on
|
|
1472
|
+
* the way there is resolved. Both sides are taken to where a write to them
|
|
1473
|
+
* would land (see {@link writeLandingPath}), so a root that does not fully
|
|
1474
|
+
* exist yet is judged the same way as the target below it.
|
|
1475
|
+
*
|
|
1476
|
+
* A link that stays inside the root passes. That is what sets this apart from
|
|
1477
|
+
* the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
|
|
1478
|
+
* dotfiles checkout linked from inside the home directory is a common shape for
|
|
1479
|
+
* a global write target, and refusing it would refuse `--global` outright.
|
|
1480
|
+
*
|
|
1481
|
+
* A link chain that cycles is reported as an escape: the write could not get
|
|
1482
|
+
* through it, and refusing it is the answer that never writes somewhere
|
|
1483
|
+
* unexpected.
|
|
1484
|
+
*/
|
|
1485
|
+
async function writablePathEscapesRoot({ rootPath, targetPath }) {
|
|
1486
|
+
const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
|
|
1487
|
+
if (rootLanding === null || targetLanding === null) return true;
|
|
1488
|
+
return pathEscapesRoot((0, node_path.relative)(rootLanding, targetLanding));
|
|
1489
|
+
}
|
|
1490
|
+
/**
|
|
1383
1491
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1384
1492
|
*
|
|
1385
1493
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -4679,6 +4787,34 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4679
4787
|
"stop"
|
|
4680
4788
|
];
|
|
4681
4789
|
/**
|
|
4790
|
+
* Hook events supported by Pool.
|
|
4791
|
+
*
|
|
4792
|
+
* Pool's `hooks` settings block fires six events, spelled the Claude Code
|
|
4793
|
+
* way: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` (← `beforeSubmitPrompt`),
|
|
4794
|
+
* `PreCompact`, `SessionStart` and `Stop`. Every entry carries a `matcher`
|
|
4795
|
+
* (required by Pool's schema; `""`/`*` match any tool, a bare name is an exact
|
|
4796
|
+
* match, `a|b` lists names, anything else is a regular expression), which only
|
|
4797
|
+
* `PreToolUse`/`PostToolUse` test against the tool name.
|
|
4798
|
+
* @see https://docs.poolside.ai/hooks
|
|
4799
|
+
*/
|
|
4800
|
+
const POOL_HOOK_EVENTS = [
|
|
4801
|
+
"preToolUse",
|
|
4802
|
+
"postToolUse",
|
|
4803
|
+
"beforeSubmitPrompt",
|
|
4804
|
+
"preCompact",
|
|
4805
|
+
"sessionStart",
|
|
4806
|
+
"stop"
|
|
4807
|
+
];
|
|
4808
|
+
const CANONICAL_TO_POOL_EVENT_NAMES = {
|
|
4809
|
+
preToolUse: "PreToolUse",
|
|
4810
|
+
postToolUse: "PostToolUse",
|
|
4811
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
4812
|
+
preCompact: "PreCompact",
|
|
4813
|
+
sessionStart: "SessionStart",
|
|
4814
|
+
stop: "Stop"
|
|
4815
|
+
};
|
|
4816
|
+
const POOL_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
4817
|
+
/**
|
|
4682
4818
|
* Hook events supported by Crush.
|
|
4683
4819
|
*
|
|
4684
4820
|
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
@@ -5273,6 +5409,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
5273
5409
|
"kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5274
5410
|
zcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5275
5411
|
crush: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5412
|
+
pool: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5276
5413
|
bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5277
5414
|
cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5278
5415
|
commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
@@ -8001,6 +8138,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
8001
8138
|
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8002
8139
|
crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8003
8140
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8141
|
+
pool: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8004
8142
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8005
8143
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
8006
8144
|
roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -9843,6 +9981,22 @@ allowedMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })
|
|
|
9843
9981
|
injectAgentsMd: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9844
9982
|
mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
9845
9983
|
})),
|
|
9984
|
+
pool: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
9985
|
+
type: zod_mini.z.optional(zod_mini.z.enum([
|
|
9986
|
+
"in_process",
|
|
9987
|
+
"command",
|
|
9988
|
+
"agent_server"
|
|
9989
|
+
])),
|
|
9990
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
9991
|
+
instructions: zod_mini.z.optional(zod_mini.z.string()),
|
|
9992
|
+
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
9993
|
+
args: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
9994
|
+
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
9995
|
+
agent_server: zod_mini.z.optional(zod_mini.z.string()),
|
|
9996
|
+
session_config_options: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown())),
|
|
9997
|
+
inherit_agent_config: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9998
|
+
disabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
9999
|
+
})),
|
|
9846
10000
|
vibe: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
9847
10001
|
agent_type: zod_mini.z.optional(zod_mini.z.enum(["agent", "subagent"])),
|
|
9848
10002
|
display_name: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -10430,6 +10584,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10430
10584
|
this.logger = logger;
|
|
10431
10585
|
}
|
|
10432
10586
|
/**
|
|
10587
|
+
* Whether `convertRulesyncFilesToToolFiles` still yields tool files for an
|
|
10588
|
+
* empty source list. A processor that aggregates into a shared file it may
|
|
10589
|
+
* never delete (Pool's `settings.yaml` subagents) answers true so a run
|
|
10590
|
+
* whose last source file is gone retracts what an earlier run generated;
|
|
10591
|
+
* the default keeps a run with no source from writing anything. Consulted
|
|
10592
|
+
* after `loadRulesyncFiles`, so the answer may depend on what that load saw.
|
|
10593
|
+
*/
|
|
10594
|
+
emitsToolFilesForEmptySource() {
|
|
10595
|
+
return false;
|
|
10596
|
+
}
|
|
10597
|
+
/**
|
|
10433
10598
|
* Return tool targets that this feature supports.
|
|
10434
10599
|
*/
|
|
10435
10600
|
static getToolTargets(_params = {}) {
|
|
@@ -10444,6 +10609,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10444
10609
|
const changedPaths = [];
|
|
10445
10610
|
for (const aiFile of aiFiles) {
|
|
10446
10611
|
const filePath = aiFile.getFilePath();
|
|
10612
|
+
if (await refusesWriteOutsideRoot({
|
|
10613
|
+
logger: this.logger,
|
|
10614
|
+
rootPath: aiFile.getOutputRoot(),
|
|
10615
|
+
targetPath: filePath
|
|
10616
|
+
})) continue;
|
|
10447
10617
|
const existingFileContent = await readFileContentOrNull(filePath);
|
|
10448
10618
|
if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
|
|
10449
10619
|
const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
|
|
@@ -10576,6 +10746,37 @@ function mergeByIdentity({ perRoot, identity }) {
|
|
|
10576
10746
|
return order.map((key) => winnerByKey.get(key));
|
|
10577
10747
|
}
|
|
10578
10748
|
/**
|
|
10749
|
+
* Whether a write to `targetPath` has to be refused because a link on the way
|
|
10750
|
+
* there leads out of `rootPath`, warning about the refusal.
|
|
10751
|
+
*
|
|
10752
|
+
* A checked-out repository can carry a symbolic link at an output path
|
|
10753
|
+
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`), and a write
|
|
10754
|
+
* through it would land wherever the link points. Every writer that lets the
|
|
10755
|
+
* OS follow links — file, directory, and config-rewrite alike — asks this first
|
|
10756
|
+
* so that one warning describes the one behavior.
|
|
10757
|
+
*/
|
|
10758
|
+
async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
10759
|
+
if (!await writablePathEscapesRoot({
|
|
10760
|
+
rootPath,
|
|
10761
|
+
targetPath
|
|
10762
|
+
})) return false;
|
|
10763
|
+
logger.warn(`Refusing to write ${quoteForLog(targetPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
|
|
10764
|
+
return true;
|
|
10765
|
+
}
|
|
10766
|
+
/**
|
|
10767
|
+
* {@link refusesWriteOutsideRoot} over every path a single unit of output
|
|
10768
|
+
* writes, stopping at the first refusal: a directory written as one unit is
|
|
10769
|
+
* held back whole when any one file in it would land outside the root.
|
|
10770
|
+
*/
|
|
10771
|
+
async function refusesAnyWriteOutsideRoot({ logger, rootPath, targetPaths }) {
|
|
10772
|
+
for (const targetPath of targetPaths) if (await refusesWriteOutsideRoot({
|
|
10773
|
+
logger,
|
|
10774
|
+
rootPath,
|
|
10775
|
+
targetPath
|
|
10776
|
+
})) return true;
|
|
10777
|
+
return false;
|
|
10778
|
+
}
|
|
10779
|
+
/**
|
|
10579
10780
|
* The key two spellings share when a case-insensitive filesystem would give
|
|
10580
10781
|
* them one file. `toLowerCase()` is locale-independent (unlike
|
|
10581
10782
|
* `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
|
|
@@ -13040,15 +13241,33 @@ const KIMI_CODE_CONFIG_DECLARATION = {
|
|
|
13040
13241
|
* Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
|
|
13041
13242
|
* the user's primary Pool settings file, so an unparseable root fails closed
|
|
13042
13243
|
* rather than being replaced with generated output. `mcp_servers` is owned as
|
|
13043
|
-
* a whole key because the MCP writer emits the complete server map.
|
|
13244
|
+
* a whole key because the MCP writer emits the complete server map. The
|
|
13245
|
+
* permissions writer deep-merges: it rewrites the `allow`/`deny` lists of the
|
|
13246
|
+
* `tools.<name>` blocks it manages and the `paths` lists, leaving each tool's
|
|
13247
|
+
* `disabled` flag, unmanaged tools and every other key in place. `hooks` is
|
|
13248
|
+
* owned as a whole key because the hooks writer emits every event list.
|
|
13249
|
+
* `subagents` is owned as a whole key too: its writer recomputes the block
|
|
13250
|
+
* from the existing file (`default` and the built-in `general` agent carried
|
|
13251
|
+
* over, every other agent replaced) before applying the patch.
|
|
13044
13252
|
*/
|
|
13045
13253
|
const POOL_SETTINGS_DECLARATION = {
|
|
13046
13254
|
format: "yaml",
|
|
13047
13255
|
invalidRootPolicy: "error",
|
|
13048
|
-
features: {
|
|
13049
|
-
|
|
13050
|
-
|
|
13051
|
-
|
|
13256
|
+
features: {
|
|
13257
|
+
mcp: {
|
|
13258
|
+
kind: "replace-owned-keys",
|
|
13259
|
+
ownedKeys: ["mcp_servers"]
|
|
13260
|
+
},
|
|
13261
|
+
permissions: { kind: "deep-merge" },
|
|
13262
|
+
hooks: {
|
|
13263
|
+
kind: "replace-owned-keys",
|
|
13264
|
+
ownedKeys: ["hooks"]
|
|
13265
|
+
},
|
|
13266
|
+
subagents: {
|
|
13267
|
+
kind: "replace-owned-keys",
|
|
13268
|
+
ownedKeys: ["subagents"]
|
|
13269
|
+
}
|
|
13270
|
+
}
|
|
13052
13271
|
};
|
|
13053
13272
|
/**
|
|
13054
13273
|
* ZCode's settings file, which also carries model/theme/permission keys
|
|
@@ -20443,6 +20662,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20443
20662
|
global: this.global,
|
|
20444
20663
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
20445
20664
|
}));
|
|
20665
|
+
if (await refusesWriteOutsideRoot({
|
|
20666
|
+
logger: this.logger,
|
|
20667
|
+
rootPath: this.outputRoot,
|
|
20668
|
+
targetPath: configPath
|
|
20669
|
+
})) return changedCount;
|
|
20446
20670
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20447
20671
|
if (currentContent === null) return changedCount;
|
|
20448
20672
|
const nextContent = getDisabledHermesCommandsPluginConfigContent({
|
|
@@ -20465,6 +20689,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20465
20689
|
if (this.toolTarget !== "goose" || !this.global) return 0;
|
|
20466
20690
|
if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
|
|
20467
20691
|
const configPath = (0, node_path.join)(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
|
|
20692
|
+
if (await refusesWriteOutsideRoot({
|
|
20693
|
+
logger: this.logger,
|
|
20694
|
+
rootPath: this.outputRoot,
|
|
20695
|
+
targetPath: configPath
|
|
20696
|
+
})) return 0;
|
|
20468
20697
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20469
20698
|
if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
|
|
20470
20699
|
const nextContent = getGooseSlashCommandsConfigContent({
|
|
@@ -27564,6 +27793,302 @@ var PiHooks = class PiHooks extends ToolHooks {
|
|
|
27564
27793
|
}
|
|
27565
27794
|
};
|
|
27566
27795
|
//#endregion
|
|
27796
|
+
//#region src/constants/pool-paths.ts
|
|
27797
|
+
/**
|
|
27798
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
27799
|
+
*
|
|
27800
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
27801
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
27802
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
27803
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
27804
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
27805
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
27806
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
27807
|
+
*
|
|
27808
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
27809
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
27810
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
27811
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
27812
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
27813
|
+
*
|
|
27814
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
27815
|
+
* @see https://docs.poolside.ai/skills
|
|
27816
|
+
* @see https://github.com/poolsideai/pool
|
|
27817
|
+
*/
|
|
27818
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
27819
|
+
const POOL_DIR = ".poolside";
|
|
27820
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
27821
|
+
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
27822
|
+
/** Project skills root, relative to the project root. */
|
|
27823
|
+
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
27824
|
+
/** Global skills root, relative to the home directory. */
|
|
27825
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
27826
|
+
/**
|
|
27827
|
+
* Pool's settings file name. MCP servers live under the top-level
|
|
27828
|
+
* `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
|
|
27829
|
+
* `~/.config/poolside/settings.yaml` (global); the untracked
|
|
27830
|
+
* `.poolside/settings.local.yaml` overlay is left to the user.
|
|
27831
|
+
*
|
|
27832
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
27833
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
27834
|
+
*/
|
|
27835
|
+
const POOL_SETTINGS_FILE_NAME = "settings.yaml";
|
|
27836
|
+
/** Pool's untracked per-user overlay next to the settings file; never written. */
|
|
27837
|
+
const POOL_SETTINGS_LOCAL_FILE_NAME = "settings.local.yaml";
|
|
27838
|
+
/** Top-level key of Pool's settings file that holds the MCP server map. */
|
|
27839
|
+
const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
27840
|
+
/** Top-level key of Pool's settings file that holds per-tool permission rules. */
|
|
27841
|
+
const POOL_TOOLS_KEY = "tools";
|
|
27842
|
+
/** Top-level key of Pool's settings file that holds file access rules. */
|
|
27843
|
+
const POOL_PATHS_KEY = "paths";
|
|
27844
|
+
/** Top-level key of Pool's settings file that holds the per-event hook lists. */
|
|
27845
|
+
const POOL_HOOKS_KEY = "hooks";
|
|
27846
|
+
/**
|
|
27847
|
+
* Top-level key of Pool's settings file that holds the subagent registry:
|
|
27848
|
+
* `subagents.default` names the agent delegated to by default and
|
|
27849
|
+
* `subagents.agents.<name>` defines each agent.
|
|
27850
|
+
*
|
|
27851
|
+
* @see https://docs.poolside.ai/subagents
|
|
27852
|
+
*/
|
|
27853
|
+
const POOL_SUBAGENTS_KEY = "subagents";
|
|
27854
|
+
/** Key under `subagents` that holds the per-agent definitions. */
|
|
27855
|
+
const POOL_SUBAGENTS_AGENTS_KEY = "agents";
|
|
27856
|
+
/** Pool's reserved built-in agent, always `in_process` and never generated. */
|
|
27857
|
+
const POOL_GENERAL_AGENT_NAME = "general";
|
|
27858
|
+
//#endregion
|
|
27859
|
+
//#region src/features/hooks/pool-hooks.ts
|
|
27860
|
+
/** The matcher Pool reads as "any tool"; `""` means the same. */
|
|
27861
|
+
const POOL_MATCH_ALL = "*";
|
|
27862
|
+
const SUPPORTED_POOL_EVENTS = new Set(POOL_HOOK_EVENTS);
|
|
27863
|
+
/**
|
|
27864
|
+
* The Pool events that test `matcher` against a tool name. Pool ignores the
|
|
27865
|
+
* field on every other event (it stays required by the schema), so an
|
|
27866
|
+
* authored matcher there is dropped with a warning and `*` written instead.
|
|
27867
|
+
*/
|
|
27868
|
+
const POOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
27869
|
+
/**
|
|
27870
|
+
* Single spelling of the settings.yaml codec/policy, matching PoolMcp and
|
|
27871
|
+
* PoolPermissions: fail closed on an unparseable root rather than replacing
|
|
27872
|
+
* the user's primary Pool settings with generated output.
|
|
27873
|
+
*/
|
|
27874
|
+
function parsePoolSettings$3(fileContent, filePath) {
|
|
27875
|
+
return parseSharedConfig({
|
|
27876
|
+
format: "yaml",
|
|
27877
|
+
fileContent,
|
|
27878
|
+
filePath,
|
|
27879
|
+
invalidRootPolicy: "error"
|
|
27880
|
+
});
|
|
27881
|
+
}
|
|
27882
|
+
/**
|
|
27883
|
+
* Build the `hooks` event lists of Pool's settings file from a canonical hooks
|
|
27884
|
+
* config. Pool keys a flat list of `{name, matcher, command, timeout}` entries
|
|
27885
|
+
* by event name. Only `type: "command"` canonical hooks are emitted, since a
|
|
27886
|
+
* Pool hook is a shell command. A shared canonical event Pool does not fire is
|
|
27887
|
+
* skipped (the HooksProcessor reports it), while an event under the
|
|
27888
|
+
* `pool.hooks` override — such as one an import filed there — is written
|
|
27889
|
+
* verbatim so it round-trips.
|
|
27890
|
+
*/
|
|
27891
|
+
function canonicalToPoolHooks({ config, toolOverride, logger }) {
|
|
27892
|
+
const sharedHooks = {};
|
|
27893
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_POOL_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
27894
|
+
const effective = {
|
|
27895
|
+
...sharedHooks,
|
|
27896
|
+
...toolOverride
|
|
27897
|
+
};
|
|
27898
|
+
const hooks = {};
|
|
27899
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
27900
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
27901
|
+
const poolEvent = lookupOwn({
|
|
27902
|
+
record: CANONICAL_TO_POOL_EVENT_NAMES,
|
|
27903
|
+
key: event
|
|
27904
|
+
}) ?? event;
|
|
27905
|
+
const entries = [];
|
|
27906
|
+
for (const def of defs) {
|
|
27907
|
+
const entry = canonicalDefToPoolEntry({
|
|
27908
|
+
def,
|
|
27909
|
+
event,
|
|
27910
|
+
poolEvent,
|
|
27911
|
+
logger
|
|
27912
|
+
});
|
|
27913
|
+
if (entry !== null) entries.push(entry);
|
|
27914
|
+
}
|
|
27915
|
+
if (entries.length > 0) hooks[poolEvent] = entries;
|
|
27916
|
+
}
|
|
27917
|
+
return hooks;
|
|
27918
|
+
}
|
|
27919
|
+
/** Convert one canonical hook definition to a Pool entry, or null to skip. */
|
|
27920
|
+
function canonicalDefToPoolEntry({ def, event, poolEvent, logger }) {
|
|
27921
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
27922
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
27923
|
+
logger?.warn(`Pool hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Pool would reject, so it was skipped.`);
|
|
27924
|
+
return null;
|
|
27925
|
+
}
|
|
27926
|
+
const authoredMatcher = typeof def.matcher === "string" && def.matcher !== "" ? def.matcher : POOL_MATCH_ALL;
|
|
27927
|
+
let matcher = authoredMatcher;
|
|
27928
|
+
if (!POOL_MATCHER_EVENTS.has(poolEvent) && authoredMatcher !== POOL_MATCH_ALL) {
|
|
27929
|
+
logger?.warn(`Pool ignores "matcher" on "${poolEvent}", so the matcher ${quoteValueForWarning(authoredMatcher)} of the hook ${quoteValueForWarning(def.name ?? def.command)} under "${event}" was dropped.`);
|
|
27930
|
+
matcher = POOL_MATCH_ALL;
|
|
27931
|
+
}
|
|
27932
|
+
const entry = {
|
|
27933
|
+
...typeof def.name === "string" && def.name !== "" ? { name: def.name } : {},
|
|
27934
|
+
matcher,
|
|
27935
|
+
command: def.command
|
|
27936
|
+
};
|
|
27937
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
27938
|
+
return entry;
|
|
27939
|
+
}
|
|
27940
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
27941
|
+
function poolEntryToCanonicalDef({ raw, poolEvent }) {
|
|
27942
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string" || raw.command === "") return null;
|
|
27943
|
+
const def = {
|
|
27944
|
+
type: "command",
|
|
27945
|
+
command: raw.command
|
|
27946
|
+
};
|
|
27947
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
27948
|
+
if (POOL_MATCHER_EVENTS.has(poolEvent) && typeof raw.matcher === "string" && raw.matcher !== "" && raw.matcher !== POOL_MATCH_ALL) def.matcher = raw.matcher;
|
|
27949
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
27950
|
+
return def;
|
|
27951
|
+
}
|
|
27952
|
+
/**
|
|
27953
|
+
* Reverse {@link canonicalToPoolHooks}: parse the event lists of the `hooks`
|
|
27954
|
+
* block back into a canonical event → definition[] record. An event Pool does
|
|
27955
|
+
* not document is carried under its own name so `buildImportedHooksConfig`
|
|
27956
|
+
* files it under the `pool.hooks` override; a non-list sibling such as
|
|
27957
|
+
* `stop_hook_max_continuations` is not a hook and is left alone.
|
|
27958
|
+
*/
|
|
27959
|
+
function poolHooksToCanonical(hooksBlock) {
|
|
27960
|
+
const canonical = {};
|
|
27961
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
27962
|
+
for (const [poolEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
27963
|
+
if (isPrototypePollutionKey(poolEvent) || !Array.isArray(rawEntries)) continue;
|
|
27964
|
+
const canonicalEvent = lookupOwn({
|
|
27965
|
+
record: POOL_TO_CANONICAL_EVENT_NAMES,
|
|
27966
|
+
key: poolEvent
|
|
27967
|
+
}) ?? poolEvent;
|
|
27968
|
+
const defs = rawEntries.map((raw) => poolEntryToCanonicalDef({
|
|
27969
|
+
raw,
|
|
27970
|
+
poolEvent
|
|
27971
|
+
})).filter((def) => def !== null);
|
|
27972
|
+
if (defs.length === 0) continue;
|
|
27973
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
27974
|
+
record: canonical,
|
|
27975
|
+
key: canonicalEvent
|
|
27976
|
+
}) ?? [], ...defs];
|
|
27977
|
+
}
|
|
27978
|
+
return canonical;
|
|
27979
|
+
}
|
|
27980
|
+
/**
|
|
27981
|
+
* The keys of an existing `hooks` block that are not event lists —
|
|
27982
|
+
* `stop_hook_max_continuations` and anything else Pool may add beside the
|
|
27983
|
+
* events — which survive a regenerate untouched.
|
|
27984
|
+
*/
|
|
27985
|
+
function nonEventHookKeys(hooksBlock) {
|
|
27986
|
+
const kept = {};
|
|
27987
|
+
if (!isRecord$1(hooksBlock)) return kept;
|
|
27988
|
+
for (const [key, value] of Object.entries(hooksBlock)) if (!isPrototypePollutionKey(key) && !Array.isArray(value)) kept[key] = value;
|
|
27989
|
+
return kept;
|
|
27990
|
+
}
|
|
27991
|
+
/**
|
|
27992
|
+
* Pool hooks.
|
|
27993
|
+
*
|
|
27994
|
+
* Pool reads hooks from the `hooks` key of its settings file —
|
|
27995
|
+
* `.poolside/settings.yaml` at project scope and
|
|
27996
|
+
* `~/.config/poolside/settings.yaml` at user scope — as
|
|
27997
|
+
* `hooks.<Event>: [{name?, matcher, command, timeout?}]` for `PreToolUse`,
|
|
27998
|
+
* `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionStart` and `Stop`.
|
|
27999
|
+
* The event lists are rewritten as a whole; the block's non-list sibling
|
|
28000
|
+
* `stop_hook_max_continuations` and every other top-level key of the file
|
|
28001
|
+
* (`mcp_servers`, `tools`, `paths`, `pool`, ...) are preserved, and the file
|
|
28002
|
+
* is never deleted.
|
|
28003
|
+
*
|
|
28004
|
+
* @see https://docs.poolside.ai/hooks
|
|
28005
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
28006
|
+
*/
|
|
28007
|
+
var PoolHooks = class PoolHooks extends ToolHooks {
|
|
28008
|
+
settings;
|
|
28009
|
+
constructor(params) {
|
|
28010
|
+
super(params);
|
|
28011
|
+
this.settings = parsePoolSettings$3(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
28012
|
+
}
|
|
28013
|
+
getSettings() {
|
|
28014
|
+
return this.settings;
|
|
28015
|
+
}
|
|
28016
|
+
isDeletable() {
|
|
28017
|
+
return false;
|
|
28018
|
+
}
|
|
28019
|
+
static getSettablePaths({ global = false } = {}) {
|
|
28020
|
+
return {
|
|
28021
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
28022
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
28023
|
+
};
|
|
28024
|
+
}
|
|
28025
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
28026
|
+
const paths = this.getSettablePaths({ global });
|
|
28027
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
28028
|
+
return new PoolHooks({
|
|
28029
|
+
outputRoot,
|
|
28030
|
+
relativeDirPath: paths.relativeDirPath,
|
|
28031
|
+
relativeFilePath: paths.relativeFilePath,
|
|
28032
|
+
fileContent,
|
|
28033
|
+
validate,
|
|
28034
|
+
global
|
|
28035
|
+
});
|
|
28036
|
+
}
|
|
28037
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
28038
|
+
const paths = this.getSettablePaths({ global });
|
|
28039
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
28040
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
28041
|
+
const existing = parsePoolSettings$3(existingContent, filePath);
|
|
28042
|
+
const config = rulesyncHooks.getJson();
|
|
28043
|
+
const hooks = canonicalToPoolHooks({
|
|
28044
|
+
config,
|
|
28045
|
+
toolOverride: config.pool?.hooks,
|
|
28046
|
+
logger
|
|
28047
|
+
});
|
|
28048
|
+
return new PoolHooks({
|
|
28049
|
+
outputRoot,
|
|
28050
|
+
relativeDirPath: paths.relativeDirPath,
|
|
28051
|
+
relativeFilePath: paths.relativeFilePath,
|
|
28052
|
+
fileContent: applySharedConfigPatch({
|
|
28053
|
+
fileKey: sharedConfigFileKey(paths),
|
|
28054
|
+
feature: "hooks",
|
|
28055
|
+
existingContent,
|
|
28056
|
+
patch: { [POOL_HOOKS_KEY]: {
|
|
28057
|
+
...nonEventHookKeys(existing[POOL_HOOKS_KEY]),
|
|
28058
|
+
...hooks
|
|
28059
|
+
} },
|
|
28060
|
+
filePath,
|
|
28061
|
+
logger
|
|
28062
|
+
}),
|
|
28063
|
+
validate,
|
|
28064
|
+
global
|
|
28065
|
+
});
|
|
28066
|
+
}
|
|
28067
|
+
toRulesyncHooks() {
|
|
28068
|
+
const hooks = poolHooksToCanonical(this.settings[POOL_HOOKS_KEY]);
|
|
28069
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
28070
|
+
hooks,
|
|
28071
|
+
overrideKey: "pool"
|
|
28072
|
+
}), null, 2) });
|
|
28073
|
+
}
|
|
28074
|
+
validate() {
|
|
28075
|
+
return {
|
|
28076
|
+
success: true,
|
|
28077
|
+
error: null
|
|
28078
|
+
};
|
|
28079
|
+
}
|
|
28080
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
28081
|
+
return new PoolHooks({
|
|
28082
|
+
outputRoot,
|
|
28083
|
+
relativeDirPath,
|
|
28084
|
+
relativeFilePath,
|
|
28085
|
+
fileContent: "",
|
|
28086
|
+
validate: false,
|
|
28087
|
+
global
|
|
28088
|
+
});
|
|
28089
|
+
}
|
|
28090
|
+
};
|
|
28091
|
+
//#endregion
|
|
27567
28092
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
27568
28093
|
/**
|
|
27569
28094
|
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
@@ -28677,14 +29202,15 @@ const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetT
|
|
|
28677
29202
|
/**
|
|
28678
29203
|
* Event names present in the config that the target's adapter cannot emit.
|
|
28679
29204
|
*
|
|
28680
|
-
*
|
|
28681
|
-
*
|
|
28682
|
-
*
|
|
29205
|
+
* Shared (`hooks`) keys are always checked. Override-block keys are only
|
|
29206
|
+
* checked when the factory declares `dropsUnknownOverrideEvents`; otherwise
|
|
29207
|
+
* the adapter either emits them verbatim or reports its own drops, and a
|
|
29208
|
+
* generic "not supported" warning would be wrong or duplicated.
|
|
28683
29209
|
*/
|
|
28684
29210
|
function unsupportedEventNames(params) {
|
|
28685
29211
|
const { factory, sharedHooks, effectiveHooks } = params;
|
|
28686
29212
|
const supportedEvents = new Set(factory.supportedEvents);
|
|
28687
|
-
const eventNames = factory.
|
|
29213
|
+
const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
|
|
28688
29214
|
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
28689
29215
|
}
|
|
28690
29216
|
/**
|
|
@@ -28777,7 +29303,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28777
29303
|
supportedEvents: AMP_HOOK_EVENTS,
|
|
28778
29304
|
supportedHookTypes: ["command"],
|
|
28779
29305
|
supportsMatcher: true,
|
|
28780
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29306
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29307
|
+
dropsUnknownOverrideEvents: true
|
|
28781
29308
|
}],
|
|
28782
29309
|
["antigravity-cli", {
|
|
28783
29310
|
class: AntigravityCliHooks,
|
|
@@ -28810,7 +29337,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28810
29337
|
},
|
|
28811
29338
|
supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
|
|
28812
29339
|
supportedHookTypes: ["command"],
|
|
28813
|
-
supportsMatcher: true
|
|
29340
|
+
supportsMatcher: true,
|
|
29341
|
+
dropsUnknownOverrideEvents: true
|
|
28814
29342
|
}],
|
|
28815
29343
|
["cursor", {
|
|
28816
29344
|
class: CursorHooks,
|
|
@@ -28855,7 +29383,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28855
29383
|
"mcp_tool",
|
|
28856
29384
|
"agent"
|
|
28857
29385
|
],
|
|
28858
|
-
supportsMatcher: true
|
|
29386
|
+
supportsMatcher: true,
|
|
29387
|
+
dropsUnknownOverrideEvents: true
|
|
28859
29388
|
}],
|
|
28860
29389
|
["codexcli", {
|
|
28861
29390
|
class: CodexcliHooks,
|
|
@@ -28916,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28916
29445
|
supportedEvents: KILO_HOOK_EVENTS,
|
|
28917
29446
|
supportedHookTypes: ["command"],
|
|
28918
29447
|
supportsMatcher: true,
|
|
28919
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29448
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29449
|
+
dropsUnknownOverrideEvents: true
|
|
28920
29450
|
}],
|
|
28921
29451
|
["opencode", {
|
|
28922
29452
|
class: OpencodeHooks,
|
|
@@ -28928,7 +29458,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28928
29458
|
supportedEvents: OPENCODE_HOOK_EVENTS,
|
|
28929
29459
|
supportedHookTypes: ["command"],
|
|
28930
29460
|
supportsMatcher: true,
|
|
28931
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29461
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29462
|
+
dropsUnknownOverrideEvents: true
|
|
28932
29463
|
}],
|
|
28933
29464
|
["pi", {
|
|
28934
29465
|
class: PiHooks,
|
|
@@ -28939,7 +29470,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28939
29470
|
},
|
|
28940
29471
|
supportedEvents: PI_HOOK_EVENTS,
|
|
28941
29472
|
supportedHookTypes: ["command"],
|
|
28942
|
-
supportsMatcher: true
|
|
29473
|
+
supportsMatcher: true,
|
|
29474
|
+
dropsUnknownOverrideEvents: true
|
|
28943
29475
|
}],
|
|
28944
29476
|
["factorydroid", {
|
|
28945
29477
|
class: FactorydroidHooks,
|
|
@@ -28961,7 +29493,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28961
29493
|
},
|
|
28962
29494
|
supportedEvents: CLINE_HOOK_EVENTS,
|
|
28963
29495
|
supportedHookTypes: ["command"],
|
|
28964
|
-
supportsMatcher: false
|
|
29496
|
+
supportsMatcher: false,
|
|
29497
|
+
dropsUnknownOverrideEvents: true
|
|
28965
29498
|
}],
|
|
28966
29499
|
["goose", {
|
|
28967
29500
|
class: GooseHooks,
|
|
@@ -28983,8 +29516,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28983
29516
|
},
|
|
28984
29517
|
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
28985
29518
|
supportedHookTypes: ["command"],
|
|
28986
|
-
supportsMatcher: true
|
|
28987
|
-
passthroughOverrideEvents: true
|
|
29519
|
+
supportsMatcher: true
|
|
28988
29520
|
}],
|
|
28989
29521
|
["kimi-code", {
|
|
28990
29522
|
class: KimiCodeHooks,
|
|
@@ -28995,8 +29527,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28995
29527
|
},
|
|
28996
29528
|
supportedEvents: KIMI_CODE_HOOK_EVENTS,
|
|
28997
29529
|
supportedHookTypes: ["command"],
|
|
28998
|
-
supportsMatcher: true
|
|
28999
|
-
passthroughOverrideEvents: true
|
|
29530
|
+
supportsMatcher: true
|
|
29000
29531
|
}],
|
|
29001
29532
|
["deepagents", {
|
|
29002
29533
|
class: DeepagentsHooks,
|
|
@@ -29007,7 +29538,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29007
29538
|
},
|
|
29008
29539
|
supportedEvents: DEEPAGENTS_HOOK_EVENTS,
|
|
29009
29540
|
supportedHookTypes: ["command"],
|
|
29010
|
-
supportsMatcher: true
|
|
29541
|
+
supportsMatcher: true,
|
|
29542
|
+
dropsUnknownOverrideEvents: true
|
|
29011
29543
|
}],
|
|
29012
29544
|
["kiro", {
|
|
29013
29545
|
class: KiroHooks,
|
|
@@ -29018,8 +29550,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29018
29550
|
},
|
|
29019
29551
|
supportedEvents: KIRO_HOOK_EVENTS,
|
|
29020
29552
|
supportedHookTypes: ["command"],
|
|
29021
|
-
supportsMatcher: true
|
|
29022
|
-
passthroughOverrideEvents: true
|
|
29553
|
+
supportsMatcher: true
|
|
29023
29554
|
}],
|
|
29024
29555
|
["kiro-cli", {
|
|
29025
29556
|
class: KiroCliHooks,
|
|
@@ -29030,8 +29561,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29030
29561
|
},
|
|
29031
29562
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
29032
29563
|
supportedHookTypes: ["command", "prompt"],
|
|
29033
|
-
supportsMatcher: true
|
|
29034
|
-
passthroughOverrideEvents: true
|
|
29564
|
+
supportsMatcher: true
|
|
29035
29565
|
}],
|
|
29036
29566
|
["kiro-ide", {
|
|
29037
29567
|
class: KiroIdeHooks,
|
|
@@ -29042,8 +29572,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29042
29572
|
},
|
|
29043
29573
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
29044
29574
|
supportedHookTypes: ["command", "prompt"],
|
|
29045
|
-
supportsMatcher: true
|
|
29046
|
-
passthroughOverrideEvents: true
|
|
29575
|
+
supportsMatcher: true
|
|
29047
29576
|
}],
|
|
29048
29577
|
["devin", {
|
|
29049
29578
|
class: DevinHooks,
|
|
@@ -29161,6 +29690,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29161
29690
|
},
|
|
29162
29691
|
supportedEvents: VIBE_HOOK_EVENTS,
|
|
29163
29692
|
supportedHookTypes: ["command"],
|
|
29693
|
+
supportsMatcher: true,
|
|
29694
|
+
dropsUnknownOverrideEvents: true
|
|
29695
|
+
}],
|
|
29696
|
+
["pool", {
|
|
29697
|
+
class: PoolHooks,
|
|
29698
|
+
meta: {
|
|
29699
|
+
supportsProject: true,
|
|
29700
|
+
supportsGlobal: true,
|
|
29701
|
+
supportsImport: true
|
|
29702
|
+
},
|
|
29703
|
+
supportedEvents: POOL_HOOK_EVENTS,
|
|
29704
|
+
supportedHookTypes: ["command"],
|
|
29164
29705
|
supportsMatcher: true
|
|
29165
29706
|
}],
|
|
29166
29707
|
["qwencode", {
|
|
@@ -29188,7 +29729,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29188
29729
|
},
|
|
29189
29730
|
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
29190
29731
|
supportedHookTypes: ["command"],
|
|
29191
|
-
supportsMatcher: true
|
|
29732
|
+
supportsMatcher: true,
|
|
29733
|
+
dropsUnknownOverrideEvents: true
|
|
29192
29734
|
}],
|
|
29193
29735
|
["grokcli", {
|
|
29194
29736
|
class: GrokcliHooks,
|
|
@@ -29210,8 +29752,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29210
29752
|
},
|
|
29211
29753
|
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
29212
29754
|
supportedHookTypes: ["command"],
|
|
29213
|
-
supportsMatcher: true
|
|
29214
|
-
passthroughOverrideEvents: true
|
|
29755
|
+
supportsMatcher: true
|
|
29215
29756
|
}],
|
|
29216
29757
|
["zcode", {
|
|
29217
29758
|
class: ZcodeHooks,
|
|
@@ -37142,49 +37683,6 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37142
37683
|
}
|
|
37143
37684
|
};
|
|
37144
37685
|
//#endregion
|
|
37145
|
-
//#region src/constants/pool-paths.ts
|
|
37146
|
-
/**
|
|
37147
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
37148
|
-
*
|
|
37149
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
37150
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
37151
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
37152
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
37153
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
37154
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
37155
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
37156
|
-
*
|
|
37157
|
-
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
37158
|
-
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
37159
|
-
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
37160
|
-
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
37161
|
-
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
37162
|
-
*
|
|
37163
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
37164
|
-
* @see https://docs.poolside.ai/skills
|
|
37165
|
-
* @see https://github.com/poolsideai/pool
|
|
37166
|
-
*/
|
|
37167
|
-
/** Project-scoped `.poolside/` directory at the project root. */
|
|
37168
|
-
const POOL_DIR = ".poolside";
|
|
37169
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
37170
|
-
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
37171
|
-
/** Project skills root, relative to the project root. */
|
|
37172
|
-
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
37173
|
-
/** Global skills root, relative to the home directory. */
|
|
37174
|
-
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
37175
|
-
/**
|
|
37176
|
-
* Pool's settings file name. MCP servers live under the top-level
|
|
37177
|
-
* `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
|
|
37178
|
-
* `~/.config/poolside/settings.yaml` (global); the untracked
|
|
37179
|
-
* `.poolside/settings.local.yaml` overlay is left to the user.
|
|
37180
|
-
*
|
|
37181
|
-
* @see https://docs.poolside.ai/mcp-servers
|
|
37182
|
-
* @see https://docs.poolside.ai/settings-file-reference
|
|
37183
|
-
*/
|
|
37184
|
-
const POOL_SETTINGS_FILE_NAME = "settings.yaml";
|
|
37185
|
-
/** Top-level key of Pool's settings file that holds the MCP server map. */
|
|
37186
|
-
const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
37187
|
-
//#endregion
|
|
37188
37686
|
//#region src/features/mcp/pool-mcp.ts
|
|
37189
37687
|
/**
|
|
37190
37688
|
* Single spelling of the settings.yaml codec/policy, matching the
|
|
@@ -37192,7 +37690,7 @@ const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
|
37192
37690
|
* unparseable root rather than replacing the user's primary Pool settings with
|
|
37193
37691
|
* generated output.
|
|
37194
37692
|
*/
|
|
37195
|
-
function parsePoolSettings(fileContent, filePath) {
|
|
37693
|
+
function parsePoolSettings$2(fileContent, filePath) {
|
|
37196
37694
|
return parseSharedConfig({
|
|
37197
37695
|
format: "yaml",
|
|
37198
37696
|
fileContent,
|
|
@@ -37424,7 +37922,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
37424
37922
|
settings;
|
|
37425
37923
|
constructor(params) {
|
|
37426
37924
|
super(params);
|
|
37427
|
-
this.settings = parsePoolSettings(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
37925
|
+
this.settings = parsePoolSettings$2(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
37428
37926
|
}
|
|
37429
37927
|
getSettings() {
|
|
37430
37928
|
return this.settings;
|
|
@@ -41268,7 +41766,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
41268
41766
|
function toAntigravityCliToolName(canonical) {
|
|
41269
41767
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
41270
41768
|
}
|
|
41271
|
-
function toCanonicalToolName$
|
|
41769
|
+
function toCanonicalToolName$9(cliName) {
|
|
41272
41770
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
41273
41771
|
}
|
|
41274
41772
|
/**
|
|
@@ -41484,7 +41982,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
41484
41982
|
const processEntries = (entries, action) => {
|
|
41485
41983
|
for (const entry of entries) {
|
|
41486
41984
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
41487
|
-
const canonical = toCanonicalToolName$
|
|
41985
|
+
const canonical = toCanonicalToolName$9(toolName);
|
|
41488
41986
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41489
41987
|
permission[canonical][pattern] = action;
|
|
41490
41988
|
}
|
|
@@ -41763,7 +42261,7 @@ function toLegacyAugmentToolName(augmentName) {
|
|
|
41763
42261
|
key: augmentName
|
|
41764
42262
|
}) ?? augmentName;
|
|
41765
42263
|
}
|
|
41766
|
-
function toCanonicalToolName$
|
|
42264
|
+
function toCanonicalToolName$8(augmentName) {
|
|
41767
42265
|
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
41768
42266
|
return lookupOwn({
|
|
41769
42267
|
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
@@ -42216,7 +42714,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
42216
42714
|
const type = entry.permission.type;
|
|
42217
42715
|
if (!isBasicAugmentType(type)) continue;
|
|
42218
42716
|
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
42219
|
-
const canonical = toCanonicalToolName$
|
|
42717
|
+
const canonical = toCanonicalToolName$8(entry.toolName);
|
|
42220
42718
|
if (forbiddenMapKeys.has(canonical)) {
|
|
42221
42719
|
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.`);
|
|
42222
42720
|
continue;
|
|
@@ -42461,7 +42959,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
42461
42959
|
function toClaudeToolName(canonical) {
|
|
42462
42960
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
42463
42961
|
}
|
|
42464
|
-
function toCanonicalToolName$
|
|
42962
|
+
function toCanonicalToolName$7(claudeName) {
|
|
42465
42963
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
42466
42964
|
}
|
|
42467
42965
|
/**
|
|
@@ -43375,7 +43873,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
43375
43873
|
const processEntries = (entries, action) => {
|
|
43376
43874
|
for (const entry of entries) {
|
|
43377
43875
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
43378
|
-
const canonical = toCanonicalToolName$
|
|
43876
|
+
const canonical = toCanonicalToolName$7(toolName);
|
|
43379
43877
|
if (!permission[canonical]) permission[canonical] = {};
|
|
43380
43878
|
permission[canonical][pattern] = action;
|
|
43381
43879
|
}
|
|
@@ -44236,8 +44734,8 @@ function mapBashActionToDecision(action) {
|
|
|
44236
44734
|
//#region src/features/permissions/commandcode-permissions.ts
|
|
44237
44735
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
44238
44736
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
44239
|
-
const CATCH_ALL_PATTERN$
|
|
44240
|
-
const MCP_CANONICAL_PREFIX$
|
|
44737
|
+
const CATCH_ALL_PATTERN$7 = "*";
|
|
44738
|
+
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
44241
44739
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
44242
44740
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
44243
44741
|
bash: "Shell",
|
|
@@ -44291,10 +44789,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
44291
44789
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
44292
44790
|
*/
|
|
44293
44791
|
function buildCommandcodeRule(category, pattern) {
|
|
44294
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
44295
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
44296
|
-
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$
|
|
44297
|
-
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$
|
|
44792
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$7 || pattern === "";
|
|
44793
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
|
|
44794
|
+
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
|
|
44795
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$7 : null;
|
|
44298
44796
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
44299
44797
|
if (tool === void 0) return null;
|
|
44300
44798
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -44330,7 +44828,7 @@ function splitCommandcodeRule(rule) {
|
|
|
44330
44828
|
}
|
|
44331
44829
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
44332
44830
|
function isCatchAllSpecifier(inner) {
|
|
44333
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
44831
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$7;
|
|
44334
44832
|
}
|
|
44335
44833
|
/**
|
|
44336
44834
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -44364,34 +44862,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
44364
44862
|
if (server.length === 0) return null;
|
|
44365
44863
|
const everyServer = {
|
|
44366
44864
|
category: "mcp",
|
|
44367
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44865
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44368
44866
|
notInAllow: true
|
|
44369
44867
|
};
|
|
44370
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
44371
|
-
if (server === CATCH_ALL_PATTERN$
|
|
44372
|
-
if (server.includes(CATCH_ALL_PATTERN$
|
|
44868
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
|
|
44869
|
+
if (server === CATCH_ALL_PATTERN$7) return everyServer;
|
|
44870
|
+
if (server.includes(CATCH_ALL_PATTERN$7)) return null;
|
|
44373
44871
|
return {
|
|
44374
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
44375
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44872
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$7 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
|
|
44873
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44376
44874
|
notInAllow: false
|
|
44377
44875
|
};
|
|
44378
44876
|
}
|
|
44379
|
-
const category = `${MCP_CANONICAL_PREFIX$
|
|
44380
|
-
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$
|
|
44877
|
+
const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
|
|
44878
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$7);
|
|
44381
44879
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
44382
44880
|
category,
|
|
44383
44881
|
pattern: inner,
|
|
44384
44882
|
notInAllow: true
|
|
44385
44883
|
} : null;
|
|
44386
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
44387
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
44388
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
44389
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44884
|
+
if (remainder === CATCH_ALL_PATTERN$7) return everyServer;
|
|
44885
|
+
if (rest === CATCH_ALL_PATTERN$7 && !server.includes(CATCH_ALL_PATTERN$7)) return {
|
|
44886
|
+
category: `${MCP_CANONICAL_PREFIX$6}${server}`,
|
|
44887
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44390
44888
|
notInAllow: true
|
|
44391
44889
|
};
|
|
44392
44890
|
return namesTool ? {
|
|
44393
44891
|
category,
|
|
44394
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44892
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44395
44893
|
notInAllow: false
|
|
44396
44894
|
} : null;
|
|
44397
44895
|
}
|
|
@@ -44415,14 +44913,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44415
44913
|
if (category === "bash") {
|
|
44416
44914
|
if (isCatchAllSpecifier(inner)) return {
|
|
44417
44915
|
category,
|
|
44418
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44916
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44419
44917
|
notInAllow: false
|
|
44420
44918
|
};
|
|
44421
44919
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
44422
44920
|
if (normalized === "" || normalized === ":*") return null;
|
|
44423
44921
|
return isCatchAllSpecifier(normalized) ? {
|
|
44424
44922
|
category,
|
|
44425
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44923
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44426
44924
|
notInAllow: true
|
|
44427
44925
|
} : {
|
|
44428
44926
|
category,
|
|
@@ -44432,7 +44930,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44432
44930
|
}
|
|
44433
44931
|
if (isCatchAllSpecifier(inner)) return {
|
|
44434
44932
|
category,
|
|
44435
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44933
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44436
44934
|
notInAllow: false
|
|
44437
44935
|
};
|
|
44438
44936
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -44451,13 +44949,13 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44451
44949
|
*/
|
|
44452
44950
|
function parseCommandcodeRule(rule) {
|
|
44453
44951
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44454
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
44952
|
+
if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
|
|
44455
44953
|
category: "*",
|
|
44456
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44954
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44457
44955
|
notInAllow: true
|
|
44458
44956
|
} : null;
|
|
44459
44957
|
const lowered = tool.toLowerCase();
|
|
44460
|
-
if (lowered.startsWith(MCP_CANONICAL_PREFIX$
|
|
44958
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
|
|
44461
44959
|
tool,
|
|
44462
44960
|
inner
|
|
44463
44961
|
});
|
|
@@ -44475,12 +44973,12 @@ function parseCommandcodeRule(rule) {
|
|
|
44475
44973
|
*/
|
|
44476
44974
|
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
44477
44975
|
const { tool } = splitCommandcodeRule(rule);
|
|
44478
|
-
return category.startsWith(MCP_CANONICAL_PREFIX$
|
|
44976
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
|
|
44479
44977
|
}
|
|
44480
44978
|
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
44481
44979
|
function scopedMcpRuleTool(rule) {
|
|
44482
44980
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44483
|
-
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
44981
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
|
|
44484
44982
|
}
|
|
44485
44983
|
/**
|
|
44486
44984
|
* The string entries of a Command Code permission list. Command Code reads
|
|
@@ -44523,7 +45021,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
44523
45021
|
}).filter((rule) => {
|
|
44524
45022
|
const parsed = parseCommandcodeRule(rule);
|
|
44525
45023
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
44526
|
-
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$
|
|
45024
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$7})`);
|
|
44527
45025
|
if (key !== "allow" && isStricterAction({
|
|
44528
45026
|
action: key,
|
|
44529
45027
|
existing: replacement
|
|
@@ -44580,7 +45078,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44580
45078
|
const ranked = /* @__PURE__ */ new Map();
|
|
44581
45079
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
44582
45080
|
for (const [category, rules] of Object.entries(permission)) {
|
|
44583
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
45081
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
|
|
44584
45082
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
44585
45083
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
44586
45084
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -44649,7 +45147,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44649
45147
|
function skippedAllowImportMessage({ rule, category }) {
|
|
44650
45148
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44651
45149
|
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.`;
|
|
44652
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
45150
|
+
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.`;
|
|
44653
45151
|
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.`;
|
|
44654
45152
|
}
|
|
44655
45153
|
/**
|
|
@@ -44851,7 +45349,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
44851
45349
|
//#endregion
|
|
44852
45350
|
//#region src/features/permissions/continue-permissions.ts
|
|
44853
45351
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
44854
|
-
const CATCH_ALL_PATTERN$
|
|
45352
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
44855
45353
|
/**
|
|
44856
45354
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
44857
45355
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -44859,7 +45357,7 @@ const CATCH_ALL_PATTERN$5 = "*";
|
|
|
44859
45357
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
44860
45358
|
*/
|
|
44861
45359
|
function normalizeCatchAllPattern$1(pattern) {
|
|
44862
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45360
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
|
|
44863
45361
|
}
|
|
44864
45362
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
44865
45363
|
bash: "Bash",
|
|
@@ -44888,7 +45386,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
44888
45386
|
function toContinueToolName(canonical) {
|
|
44889
45387
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
44890
45388
|
}
|
|
44891
|
-
function toCanonicalToolName$
|
|
45389
|
+
function toCanonicalToolName$6(continueName) {
|
|
44892
45390
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
44893
45391
|
}
|
|
44894
45392
|
/**
|
|
@@ -44896,7 +45394,7 @@ function toCanonicalToolName$5(continueName) {
|
|
|
44896
45394
|
* `Tool(pattern)`.
|
|
44897
45395
|
*/
|
|
44898
45396
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
44899
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
45397
|
+
return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
|
|
44900
45398
|
}
|
|
44901
45399
|
/**
|
|
44902
45400
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -44910,7 +45408,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
44910
45408
|
if (toolName === "") return;
|
|
44911
45409
|
return {
|
|
44912
45410
|
toolName,
|
|
44913
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
45411
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
|
|
44914
45412
|
};
|
|
44915
45413
|
}
|
|
44916
45414
|
/**
|
|
@@ -45084,7 +45582,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
45084
45582
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
45085
45583
|
continue;
|
|
45086
45584
|
}
|
|
45087
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
45585
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
|
|
45088
45586
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
45089
45587
|
continue;
|
|
45090
45588
|
}
|
|
@@ -45109,7 +45607,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
45109
45607
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
45110
45608
|
if (parsedEntry === void 0) continue;
|
|
45111
45609
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
45112
|
-
const category = toCanonicalToolName$
|
|
45610
|
+
const category = toCanonicalToolName$6(parsedEntry.toolName);
|
|
45113
45611
|
permission[category] ??= {};
|
|
45114
45612
|
permission[category][parsedEntry.pattern] ??= action;
|
|
45115
45613
|
}
|
|
@@ -45433,14 +45931,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
45433
45931
|
};
|
|
45434
45932
|
//#endregion
|
|
45435
45933
|
//#region src/features/permissions/crush-permissions.ts
|
|
45436
|
-
const CATCH_ALL_PATTERN$
|
|
45934
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
45437
45935
|
/**
|
|
45438
45936
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
45439
45937
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
45440
45938
|
* still be written.
|
|
45441
45939
|
*/
|
|
45442
45940
|
function normalizeCatchAllPattern(pattern) {
|
|
45443
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45941
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
45444
45942
|
}
|
|
45445
45943
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
45446
45944
|
bash: "bash",
|
|
@@ -45452,20 +45950,20 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
|
45452
45950
|
webfetch: "fetch"
|
|
45453
45951
|
};
|
|
45454
45952
|
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
45455
|
-
const MCP_CANONICAL_PREFIX$
|
|
45953
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
45456
45954
|
const MCP_CRUSH_PREFIX = "mcp_";
|
|
45457
45955
|
function toCrushToolName(canonical) {
|
|
45458
45956
|
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
45459
|
-
if (canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
45957
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
45460
45958
|
return canonical;
|
|
45461
45959
|
}
|
|
45462
|
-
function toCanonicalToolName$
|
|
45960
|
+
function toCanonicalToolName$5(crushName) {
|
|
45463
45961
|
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
45464
45962
|
}
|
|
45465
45963
|
function isMcpToolName(crushName) {
|
|
45466
45964
|
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
45467
45965
|
}
|
|
45468
|
-
function uniq$
|
|
45966
|
+
function uniq$10(values) {
|
|
45469
45967
|
return [...new Set(values)];
|
|
45470
45968
|
}
|
|
45471
45969
|
/**
|
|
@@ -45571,8 +46069,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
45571
46069
|
const managedToolNames = managedCrushToolNames(config);
|
|
45572
46070
|
const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
45573
46071
|
const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
45574
|
-
const allowedList = uniq$
|
|
45575
|
-
const disabledList = uniq$
|
|
46072
|
+
const allowedList = uniq$10([...preservedAllowed, ...generated.allowed]);
|
|
46073
|
+
const disabledList = uniq$10([...preservedDisabled, ...generated.disabled]);
|
|
45576
46074
|
const patch = {};
|
|
45577
46075
|
if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
|
|
45578
46076
|
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
@@ -45639,7 +46137,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
45639
46137
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
45640
46138
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
45641
46139
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
45642
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
46140
|
+
if (pattern === CATCH_ALL_PATTERN$5) {
|
|
45643
46141
|
catchAllAction = action;
|
|
45644
46142
|
continue;
|
|
45645
46143
|
}
|
|
@@ -45689,8 +46187,8 @@ function convertRulesyncToCrushLists({ config, logger }) {
|
|
|
45689
46187
|
else if (catchAllAction === "deny") lists.disabled.push(toolName);
|
|
45690
46188
|
}
|
|
45691
46189
|
return {
|
|
45692
|
-
allowed: uniq$
|
|
45693
|
-
disabled: uniq$
|
|
46190
|
+
allowed: uniq$10(lists.allowed),
|
|
46191
|
+
disabled: uniq$10(lists.disabled)
|
|
45694
46192
|
};
|
|
45695
46193
|
}
|
|
45696
46194
|
/**
|
|
@@ -45712,11 +46210,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
45712
46210
|
};
|
|
45713
46211
|
for (const entry of disabled) {
|
|
45714
46212
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
45715
|
-
bucketFor(toCanonicalToolName$
|
|
46213
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
|
|
45716
46214
|
}
|
|
45717
46215
|
for (const entry of allowed) {
|
|
45718
46216
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
45719
|
-
bucketFor(toCanonicalToolName$
|
|
46217
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
|
|
45720
46218
|
}
|
|
45721
46219
|
return { permission };
|
|
45722
46220
|
}
|
|
@@ -45763,13 +46261,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
45763
46261
|
WebFetch: "webfetch",
|
|
45764
46262
|
Mcp: "mcp"
|
|
45765
46263
|
};
|
|
45766
|
-
const MCP_CANONICAL_PREFIX$
|
|
46264
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
45767
46265
|
/**
|
|
45768
46266
|
* Returns true if the canonical category is the per-tool MCP form
|
|
45769
46267
|
* `mcp__<server>__<tool>`.
|
|
45770
46268
|
*/
|
|
45771
46269
|
function isMcpScopedCategory(canonical) {
|
|
45772
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46270
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
|
|
45773
46271
|
}
|
|
45774
46272
|
function toCursorType(canonical) {
|
|
45775
46273
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -45803,7 +46301,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
45803
46301
|
if (match) {
|
|
45804
46302
|
const server = match[1] ?? "*";
|
|
45805
46303
|
const tool = match[2] ?? "*";
|
|
45806
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
46304
|
+
return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
|
|
45807
46305
|
}
|
|
45808
46306
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
45809
46307
|
}
|
|
@@ -46096,7 +46594,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
46096
46594
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
46097
46595
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
46098
46596
|
if (!permission[canonical]) permission[canonical] = {};
|
|
46099
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46597
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
|
|
46100
46598
|
permission[canonical][canonicalPattern] = action;
|
|
46101
46599
|
}
|
|
46102
46600
|
};
|
|
@@ -47615,7 +48113,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
47615
48113
|
//#region src/features/permissions/goose-permissions.ts
|
|
47616
48114
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
47617
48115
|
const GOOSE_USER_KEY = "user";
|
|
47618
|
-
const CATCH_ALL_PATTERN$
|
|
48116
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
47619
48117
|
const RULESYNC_TO_GOOSE_TOOL_NAME = {
|
|
47620
48118
|
bash: "developer__shell",
|
|
47621
48119
|
edit: "developer__text_editor",
|
|
@@ -47777,7 +48275,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
47777
48275
|
for (const [category, rules] of orderedEntries) {
|
|
47778
48276
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
47779
48277
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
47780
|
-
if (pattern !== CATCH_ALL_PATTERN$
|
|
48278
|
+
if (pattern !== CATCH_ALL_PATTERN$4) {
|
|
47781
48279
|
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.`);
|
|
47782
48280
|
continue;
|
|
47783
48281
|
}
|
|
@@ -47806,7 +48304,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
47806
48304
|
for (const toolName of toolNames) {
|
|
47807
48305
|
const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
|
|
47808
48306
|
permission[category] ??= {};
|
|
47809
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
48307
|
+
permission[category][CATCH_ALL_PATTERN$4] = action;
|
|
47810
48308
|
}
|
|
47811
48309
|
}
|
|
47812
48310
|
return { permission };
|
|
@@ -47817,8 +48315,8 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
47817
48315
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
47818
48316
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
47819
48317
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
47820
|
-
const CATCH_ALL_PATTERN$
|
|
47821
|
-
const MCP_CANONICAL_PREFIX$
|
|
48318
|
+
const CATCH_ALL_PATTERN$3 = "*";
|
|
48319
|
+
const MCP_CANONICAL_PREFIX$3 = "mcp__";
|
|
47822
48320
|
const CATEGORY_TO_GROK_TOOL = {
|
|
47823
48321
|
bash: "Bash",
|
|
47824
48322
|
read: "Read",
|
|
@@ -47859,14 +48357,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
|
47859
48357
|
* concrete pattern emits `Tool(pattern)`.
|
|
47860
48358
|
*/
|
|
47861
48359
|
function buildGrokEntry(category, pattern) {
|
|
47862
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
48360
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
|
|
47863
48361
|
const remainder = category.slice(5);
|
|
47864
48362
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
47865
48363
|
}
|
|
47866
|
-
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$
|
|
48364
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
47867
48365
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
47868
48366
|
if (tool === void 0) return null;
|
|
47869
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
48367
|
+
return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
47870
48368
|
}
|
|
47871
48369
|
/**
|
|
47872
48370
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -47886,17 +48384,17 @@ function parseGrokEntry(entry) {
|
|
|
47886
48384
|
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
47887
48385
|
}
|
|
47888
48386
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
47889
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47890
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48387
|
+
category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
|
|
48388
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47891
48389
|
} : {
|
|
47892
48390
|
category: "mcp",
|
|
47893
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48391
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47894
48392
|
};
|
|
47895
48393
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
47896
48394
|
if (category === void 0) return null;
|
|
47897
48395
|
return {
|
|
47898
48396
|
category,
|
|
47899
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
48397
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
|
|
47900
48398
|
};
|
|
47901
48399
|
}
|
|
47902
48400
|
/**
|
|
@@ -47917,15 +48415,15 @@ function parseGrokRule(rule) {
|
|
|
47917
48415
|
if (typeof tool !== "string") return null;
|
|
47918
48416
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
47919
48417
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
47920
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
48418
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
|
|
47921
48419
|
const lowerTool = tool.trim().toLowerCase();
|
|
47922
|
-
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
48420
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$3 ? {
|
|
47923
48421
|
category: "mcp",
|
|
47924
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48422
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47925
48423
|
action
|
|
47926
48424
|
} : {
|
|
47927
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47928
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48425
|
+
category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
|
|
48426
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47929
48427
|
action
|
|
47930
48428
|
};
|
|
47931
48429
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -48073,7 +48571,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
48073
48571
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
48074
48572
|
}
|
|
48075
48573
|
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
48076
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
48574
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
|
|
48077
48575
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
48078
48576
|
}
|
|
48079
48577
|
validate() {
|
|
@@ -49953,6 +50451,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
|
|
|
49953
50451
|
}
|
|
49954
50452
|
};
|
|
49955
50453
|
//#endregion
|
|
50454
|
+
//#region src/features/permissions/pool-permissions.ts
|
|
50455
|
+
const POOL_ALLOW_KEY = "allow";
|
|
50456
|
+
const POOL_DENY_KEY = "deny";
|
|
50457
|
+
const POOL_DISABLED_KEY = "disabled";
|
|
50458
|
+
const POOL_PATH_KEY = "path";
|
|
50459
|
+
const POOL_WRITE_KEY = "write";
|
|
50460
|
+
const CATCH_ALL_PATTERN$2 = "*";
|
|
50461
|
+
const PATHS_CATCH_ALL_PATTERN = "**";
|
|
50462
|
+
const CANONICAL_TO_POOL_TOOL_NAMES = {
|
|
50463
|
+
bash: "shell",
|
|
50464
|
+
webfetch: "web_fetch",
|
|
50465
|
+
websearch: "web_search"
|
|
50466
|
+
};
|
|
50467
|
+
const POOL_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
50468
|
+
const READ_CATEGORY = "read";
|
|
50469
|
+
const WRITE_CATEGORIES = ["edit", "write"];
|
|
50470
|
+
const FILE_CATEGORIES = [READ_CATEGORY, ...WRITE_CATEGORIES];
|
|
50471
|
+
const MCP_CANONICAL_PREFIX$2 = "mcp__";
|
|
50472
|
+
/**
|
|
50473
|
+
* Single spelling of the settings.yaml codec/policy, matching the
|
|
50474
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
50475
|
+
* unparseable root rather than replacing the user's primary Pool settings with
|
|
50476
|
+
* generated output.
|
|
50477
|
+
*/
|
|
50478
|
+
function parsePoolSettings$1(fileContent, filePath) {
|
|
50479
|
+
return parseSharedConfig({
|
|
50480
|
+
format: "yaml",
|
|
50481
|
+
fileContent,
|
|
50482
|
+
filePath,
|
|
50483
|
+
invalidRootPolicy: "error"
|
|
50484
|
+
});
|
|
50485
|
+
}
|
|
50486
|
+
function toPoolToolName(canonical) {
|
|
50487
|
+
return lookupOwn({
|
|
50488
|
+
record: CANONICAL_TO_POOL_TOOL_NAMES,
|
|
50489
|
+
key: canonical
|
|
50490
|
+
}) ?? canonical;
|
|
50491
|
+
}
|
|
50492
|
+
function toCanonicalToolName$4(poolName) {
|
|
50493
|
+
return lookupOwn({
|
|
50494
|
+
record: POOL_TO_CANONICAL_TOOL_NAMES,
|
|
50495
|
+
key: poolName
|
|
50496
|
+
}) ?? poolName;
|
|
50497
|
+
}
|
|
50498
|
+
function isMcpCategory(category) {
|
|
50499
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$2) && category.length > 5;
|
|
50500
|
+
}
|
|
50501
|
+
function isFileCategory(category) {
|
|
50502
|
+
return FILE_CATEGORIES.includes(category);
|
|
50503
|
+
}
|
|
50504
|
+
/**
|
|
50505
|
+
* Pool's tool rules know one wildcard, `*`, which matches anything including
|
|
50506
|
+
* `/` — the same set a canonical `*` names — so a `**` run is collapsed to
|
|
50507
|
+
* keep the file in Pool's own spelling. `?`, `[...]` and `{a,b}` are literal
|
|
50508
|
+
* characters to Pool, so a pattern that relies on them cannot be written.
|
|
50509
|
+
*/
|
|
50510
|
+
function toPoolToolPattern(pattern) {
|
|
50511
|
+
if (/[?[{]/.test(pattern)) return void 0;
|
|
50512
|
+
return pattern.replaceAll(/\*{2,}/g, "*");
|
|
50513
|
+
}
|
|
50514
|
+
/**
|
|
50515
|
+
* Pool's path globs know `*` and `**`, so a bare `*` run is written as `**`
|
|
50516
|
+
* to keep it matching anything. `?`, `[...]` and `{a,b}` are literal
|
|
50517
|
+
* characters to Pool here as well, so a pattern that relies on them cannot be
|
|
50518
|
+
* written.
|
|
50519
|
+
*/
|
|
50520
|
+
function toPoolPathPattern(pattern) {
|
|
50521
|
+
if (/[?[{]/.test(pattern)) return void 0;
|
|
50522
|
+
return /^\*+$/.test(pattern) ? PATHS_CATCH_ALL_PATTERN : pattern;
|
|
50523
|
+
}
|
|
50524
|
+
function isHomeOrAbsolutePath(pattern) {
|
|
50525
|
+
return (0, node_path.isAbsolute)(pattern) || pattern === "~" || pattern.startsWith("~/");
|
|
50526
|
+
}
|
|
50527
|
+
function rulesOf(permission, category) {
|
|
50528
|
+
return lookupOwn({
|
|
50529
|
+
record: permission,
|
|
50530
|
+
key: category
|
|
50531
|
+
}) ?? {};
|
|
50532
|
+
}
|
|
50533
|
+
function patternsWithAction(rules, actions) {
|
|
50534
|
+
return Object.entries(rules).filter(([pattern, action]) => !isPrototypePollutionKey(pattern) && actions.includes(action)).map(([pattern]) => pattern);
|
|
50535
|
+
}
|
|
50536
|
+
function asRestrictions(patterns) {
|
|
50537
|
+
return (0, es_toolkit.uniq)(patterns).map((pattern) => ({
|
|
50538
|
+
pattern,
|
|
50539
|
+
fromAllToolsCategory: false
|
|
50540
|
+
}));
|
|
50541
|
+
}
|
|
50542
|
+
function listPatterns(patterns) {
|
|
50543
|
+
return patterns.map((pattern) => quoteValueForWarning(pattern)).join(", ");
|
|
50544
|
+
}
|
|
50545
|
+
/**
|
|
50546
|
+
* Permissions adapter for Pool (Poolside's coding agent CLI).
|
|
50547
|
+
*
|
|
50548
|
+
* Pool keeps its permission rules in the same YAML settings file as its MCP
|
|
50549
|
+
* servers — `.poolside/settings.yaml` (project) / `~/.config/poolside/settings.yaml`
|
|
50550
|
+
* (global) — under two top-level keys:
|
|
50551
|
+
* - `tools.<name>`: `allow` and `deny` pattern lists matched against the
|
|
50552
|
+
* tool's argument (the command for `shell`, the URL for `web_fetch`, ...),
|
|
50553
|
+
* plus `disabled: true` to remove the tool. A denied pattern wins over an
|
|
50554
|
+
* allowed one; a call that matches neither prompts. Only `*` is a wildcard.
|
|
50555
|
+
* - `paths`: `allow` entries (`{ path }`, read-only unless `write: true`)
|
|
50556
|
+
* and `deny` entries (`{ path }`) with `*`/`**` globs; deny wins.
|
|
50557
|
+
*
|
|
50558
|
+
* Mapping (rulesync canonical -> Pool):
|
|
50559
|
+
* - `bash` -> `tools.shell`, `webfetch` -> `tools.web_fetch`,
|
|
50560
|
+
* `websearch` -> `tools.web_search`; any other non-file category passes
|
|
50561
|
+
* through as `tools.<category>`. `allow` -> `allow`, `deny` -> `deny`,
|
|
50562
|
+
* `ask` -> nothing (prompting is Pool's default) — but an `allow` that
|
|
50563
|
+
* overlaps an `ask` of any category landing on the same Pool tool, or a
|
|
50564
|
+
* deny Pool cannot spell, is withheld, since an allowed pattern would
|
|
50565
|
+
* silence the prompt the config asks for.
|
|
50566
|
+
* - `read` / `edit` / `write` -> `paths`: a `read` allow is a read-only
|
|
50567
|
+
* entry, an `edit` or `write` allow adds `write: true`, a `read` deny is a
|
|
50568
|
+
* deny entry. Pool cannot deny writes without denying reads, so an
|
|
50569
|
+
* `edit`/`write` deny that is not also a `read` deny only withholds the
|
|
50570
|
+
* write flag of the entries it overlaps (Pool prompts for those writes).
|
|
50571
|
+
* - `mcp__<server>__<tool>` is skipped: Pool approves MCP tools per server
|
|
50572
|
+
* under `mcp_servers.<server>.allow`, authored as `poolAllow` in
|
|
50573
|
+
* `.rulesync/mcp.json`. The all-tools `*` category is mirrored onto
|
|
50574
|
+
* `bash` by `honorAllToolsOnBash` and otherwise skipped.
|
|
50575
|
+
*
|
|
50576
|
+
* Project settings take project-relative paths and the user settings absolute
|
|
50577
|
+
* or `~` paths, so a path of the other kind is skipped with a warning at each
|
|
50578
|
+
* scope. The managed tools' `allow`/`deny` lists and, whenever the canonical
|
|
50579
|
+
* config carries a file category, the `paths` lists are rebuilt; every other
|
|
50580
|
+
* key (`disabled`, unmanaged tools, `mcp_servers`, `pool`, `sandbox`, ...) is
|
|
50581
|
+
* kept, and the file is never deleted.
|
|
50582
|
+
*
|
|
50583
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
50584
|
+
*/
|
|
50585
|
+
var PoolPermissions = class PoolPermissions extends ToolPermissions {
|
|
50586
|
+
settings;
|
|
50587
|
+
constructor(params) {
|
|
50588
|
+
super({
|
|
50589
|
+
...params,
|
|
50590
|
+
fileContent: params.fileContent ?? ""
|
|
50591
|
+
});
|
|
50592
|
+
this.settings = parsePoolSettings$1(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
50593
|
+
}
|
|
50594
|
+
getSettings() {
|
|
50595
|
+
return this.settings;
|
|
50596
|
+
}
|
|
50597
|
+
isDeletable() {
|
|
50598
|
+
return false;
|
|
50599
|
+
}
|
|
50600
|
+
/**
|
|
50601
|
+
* settings.yaml is Pool's file: rulesync merges into it when it exists but
|
|
50602
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
50603
|
+
* absent rule both mean "prompt".
|
|
50604
|
+
*/
|
|
50605
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
50606
|
+
return true;
|
|
50607
|
+
}
|
|
50608
|
+
static getSettablePaths({ global = false } = {}) {
|
|
50609
|
+
return {
|
|
50610
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
50611
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
50612
|
+
};
|
|
50613
|
+
}
|
|
50614
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
50615
|
+
const paths = this.getSettablePaths({ global });
|
|
50616
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
50617
|
+
return new PoolPermissions({
|
|
50618
|
+
outputRoot,
|
|
50619
|
+
relativeDirPath: paths.relativeDirPath,
|
|
50620
|
+
relativeFilePath: paths.relativeFilePath,
|
|
50621
|
+
fileContent,
|
|
50622
|
+
validate,
|
|
50623
|
+
global
|
|
50624
|
+
});
|
|
50625
|
+
}
|
|
50626
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
50627
|
+
const paths = this.getSettablePaths({ global });
|
|
50628
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
50629
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
50630
|
+
const existing = parsePoolSettings$1(existingContent, filePath);
|
|
50631
|
+
const patch = buildPoolPermissionsPatch({
|
|
50632
|
+
config: rulesyncPermissions.getJson(),
|
|
50633
|
+
existing,
|
|
50634
|
+
global,
|
|
50635
|
+
logger
|
|
50636
|
+
});
|
|
50637
|
+
return new PoolPermissions({
|
|
50638
|
+
outputRoot,
|
|
50639
|
+
relativeDirPath: paths.relativeDirPath,
|
|
50640
|
+
relativeFilePath: paths.relativeFilePath,
|
|
50641
|
+
fileContent: applySharedConfigPatch({
|
|
50642
|
+
fileKey: sharedConfigFileKey(paths),
|
|
50643
|
+
feature: "permissions",
|
|
50644
|
+
existingContent,
|
|
50645
|
+
patch,
|
|
50646
|
+
filePath,
|
|
50647
|
+
logger
|
|
50648
|
+
}),
|
|
50649
|
+
validate: true,
|
|
50650
|
+
global
|
|
50651
|
+
});
|
|
50652
|
+
}
|
|
50653
|
+
toRulesyncPermissions() {
|
|
50654
|
+
const rulesyncConfig = convertPoolToRulesync({
|
|
50655
|
+
tools: isRecord$1(this.settings["tools"]) ? this.settings[POOL_TOOLS_KEY] : {},
|
|
50656
|
+
paths: isRecord$1(this.settings["paths"]) ? this.settings[POOL_PATHS_KEY] : {}
|
|
50657
|
+
});
|
|
50658
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
50659
|
+
}
|
|
50660
|
+
validate() {
|
|
50661
|
+
return {
|
|
50662
|
+
success: true,
|
|
50663
|
+
error: null
|
|
50664
|
+
};
|
|
50665
|
+
}
|
|
50666
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
50667
|
+
return new PoolPermissions({
|
|
50668
|
+
outputRoot,
|
|
50669
|
+
relativeDirPath,
|
|
50670
|
+
relativeFilePath,
|
|
50671
|
+
fileContent: "",
|
|
50672
|
+
validate: false,
|
|
50673
|
+
global
|
|
50674
|
+
});
|
|
50675
|
+
}
|
|
50676
|
+
};
|
|
50677
|
+
/**
|
|
50678
|
+
* The deep-merge patch that rewrites the managed `tools.<name>` lists and,
|
|
50679
|
+
* when the canonical config carries a file category, the `paths` lists. An
|
|
50680
|
+
* empty list retracts its key (`undefined` deletes under deep-merge), and a
|
|
50681
|
+
* block is only touched when there is something to write into it or it
|
|
50682
|
+
* already exists, so a config without rules does not grow an empty
|
|
50683
|
+
* `tools: {}` / `paths: {}`.
|
|
50684
|
+
*/
|
|
50685
|
+
function buildPoolPermissionsPatch({ config, existing, global, logger }) {
|
|
50686
|
+
const permission = honorAllToolsOnBash(config.permission);
|
|
50687
|
+
const existingTools = isRecord$1(existing["tools"]) ? existing[POOL_TOOLS_KEY] : void 0;
|
|
50688
|
+
const existingPaths = isRecord$1(existing["paths"]) ? existing[POOL_PATHS_KEY] : void 0;
|
|
50689
|
+
const patch = {};
|
|
50690
|
+
const toolsPatch = {};
|
|
50691
|
+
for (const [toolName, lists] of convertRulesyncToPoolTools({
|
|
50692
|
+
permission,
|
|
50693
|
+
logger
|
|
50694
|
+
})) {
|
|
50695
|
+
const existingTool = existingTools === void 0 ? void 0 : lookupOwn({
|
|
50696
|
+
record: existingTools,
|
|
50697
|
+
key: toolName
|
|
50698
|
+
});
|
|
50699
|
+
if (lists.allow.length === 0 && lists.deny.length === 0 && existingTool === void 0) continue;
|
|
50700
|
+
toolsPatch[toolName] = {
|
|
50701
|
+
[POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
|
|
50702
|
+
[POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
|
|
50703
|
+
};
|
|
50704
|
+
}
|
|
50705
|
+
if (Object.keys(toolsPatch).length > 0) patch[POOL_TOOLS_KEY] = toolsPatch;
|
|
50706
|
+
if (FILE_CATEGORIES.some((category) => lookupOwn({
|
|
50707
|
+
record: permission,
|
|
50708
|
+
key: category
|
|
50709
|
+
}))) {
|
|
50710
|
+
const lists = convertRulesyncToPoolPaths({
|
|
50711
|
+
permission,
|
|
50712
|
+
global,
|
|
50713
|
+
logger
|
|
50714
|
+
});
|
|
50715
|
+
if (lists.allow.length > 0 || lists.deny.length > 0 || existingPaths !== void 0) patch[POOL_PATHS_KEY] = {
|
|
50716
|
+
[POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
|
|
50717
|
+
[POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
|
|
50718
|
+
};
|
|
50719
|
+
}
|
|
50720
|
+
return patch;
|
|
50721
|
+
}
|
|
50722
|
+
/**
|
|
50723
|
+
* Convert one non-file category into a Pool tool's `allow`/`deny` lists. A
|
|
50724
|
+
* deny Pool can spell needs no withholding (Pool lets it win over an allow),
|
|
50725
|
+
* but an `ask` has no list of its own — a call Pool does not match prompts —
|
|
50726
|
+
* so an allow that overlaps one is withheld and reported, and the tool keeps
|
|
50727
|
+
* prompting. A deny Pool cannot spell (its pattern uses a glob form other
|
|
50728
|
+
* than `*`) is skipped, and then withholds the allows it overlaps the same
|
|
50729
|
+
* way: written, such an allow would auto-approve the very command the deny
|
|
50730
|
+
* was meant to stop. `restrictions` carries the ask patterns and the skipped
|
|
50731
|
+
* deny patterns of every category that lands on the same Pool tool, because
|
|
50732
|
+
* Pool matches the joined lists as one.
|
|
50733
|
+
*/
|
|
50734
|
+
function convertToolCategory({ category, toolName, rules, restrictions, logger }) {
|
|
50735
|
+
const lists = {
|
|
50736
|
+
allow: [],
|
|
50737
|
+
deny: []
|
|
50738
|
+
};
|
|
50739
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(asRestrictions(restrictions));
|
|
50740
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
50741
|
+
if (isPrototypePollutionKey(rawPattern) || action === "ask") continue;
|
|
50742
|
+
const pattern = toPoolToolPattern(rawPattern);
|
|
50743
|
+
if (pattern === void 0) {
|
|
50744
|
+
logger?.warn(`Pool tool rules treat only "*" as a wildcard, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
50745
|
+
continue;
|
|
50746
|
+
}
|
|
50747
|
+
if (action === "deny") {
|
|
50748
|
+
lists.deny.push(pattern);
|
|
50749
|
+
continue;
|
|
50750
|
+
}
|
|
50751
|
+
const shadowing = shadowingRestrictions(rawPattern);
|
|
50752
|
+
if (shadowing.length > 0) {
|
|
50753
|
+
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}".`);
|
|
50754
|
+
continue;
|
|
50755
|
+
}
|
|
50756
|
+
lists.allow.push(pattern);
|
|
50757
|
+
}
|
|
50758
|
+
return {
|
|
50759
|
+
allow: (0, es_toolkit.uniq)(lists.allow),
|
|
50760
|
+
deny: (0, es_toolkit.uniq)(lists.deny)
|
|
50761
|
+
};
|
|
50762
|
+
}
|
|
50763
|
+
function unrepresentableDenies(rules) {
|
|
50764
|
+
return patternsWithAction(rules, ["deny"]).filter((pattern) => toPoolToolPattern(pattern) === void 0);
|
|
50765
|
+
}
|
|
50766
|
+
/**
|
|
50767
|
+
* Group the non-file categories by the Pool tool they land on, reporting the
|
|
50768
|
+
* ones Pool has no place for. Two categories can resolve to the same tool
|
|
50769
|
+
* (`bash` and a pass-through `shell`).
|
|
50770
|
+
*/
|
|
50771
|
+
function groupCategoriesByPoolTool({ permission, logger }) {
|
|
50772
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
50773
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
50774
|
+
if (isPrototypePollutionKey(category) || isFileCategory(category)) continue;
|
|
50775
|
+
if (category === "*") {
|
|
50776
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
50777
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
50778
|
+
logger?.warn(`Pool has no rule that spans every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
50779
|
+
}
|
|
50780
|
+
continue;
|
|
50781
|
+
}
|
|
50782
|
+
if (isMcpCategory(category)) {
|
|
50783
|
+
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.`);
|
|
50784
|
+
continue;
|
|
50785
|
+
}
|
|
50786
|
+
const toolName = toPoolToolName(category);
|
|
50787
|
+
byTool.set(toolName, [...byTool.get(toolName) ?? [], [category, rules]]);
|
|
50788
|
+
}
|
|
50789
|
+
return byTool;
|
|
50790
|
+
}
|
|
50791
|
+
/**
|
|
50792
|
+
* Convert the non-file categories into Pool tool lists, keyed by Pool tool
|
|
50793
|
+
* name. The categories that land on the same tool are converted against the
|
|
50794
|
+
* union of their `ask` patterns and their lists are joined, which is safe
|
|
50795
|
+
* because a deny wins over an allow in Pool whichever category wrote it.
|
|
50796
|
+
*/
|
|
50797
|
+
function convertRulesyncToPoolTools({ permission, logger }) {
|
|
50798
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
50799
|
+
for (const [toolName, categories] of groupCategoriesByPoolTool({
|
|
50800
|
+
permission,
|
|
50801
|
+
logger
|
|
50802
|
+
})) {
|
|
50803
|
+
const restrictions = categories.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...unrepresentableDenies(rules)]);
|
|
50804
|
+
const lists = {
|
|
50805
|
+
allow: [],
|
|
50806
|
+
deny: []
|
|
50807
|
+
};
|
|
50808
|
+
for (const [category, rules] of categories) {
|
|
50809
|
+
const converted = convertToolCategory({
|
|
50810
|
+
category,
|
|
50811
|
+
toolName,
|
|
50812
|
+
rules,
|
|
50813
|
+
restrictions,
|
|
50814
|
+
logger
|
|
50815
|
+
});
|
|
50816
|
+
lists.allow.push(...converted.allow);
|
|
50817
|
+
lists.deny.push(...converted.deny);
|
|
50818
|
+
}
|
|
50819
|
+
byTool.set(toolName, {
|
|
50820
|
+
allow: (0, es_toolkit.uniq)(lists.allow),
|
|
50821
|
+
deny: (0, es_toolkit.uniq)(lists.deny)
|
|
50822
|
+
});
|
|
50823
|
+
}
|
|
50824
|
+
return byTool;
|
|
50825
|
+
}
|
|
50826
|
+
/**
|
|
50827
|
+
* Convert the file categories into Pool's `paths` lists.
|
|
50828
|
+
*
|
|
50829
|
+
* An allow entry grants reading, and `write: true` grants writing on top, so
|
|
50830
|
+
* a `read` allow is withheld by an overlapping `read` ask and a write allow by
|
|
50831
|
+
* an overlapping `edit`/`write` ask — and by a `read` ask, since the write
|
|
50832
|
+
* entry would read too. A `read` deny lands in `paths.deny`, which Pool lets
|
|
50833
|
+
* win over any allow, and covers an `edit`/`write` deny of the same pattern,
|
|
50834
|
+
* so neither withholds anything; an `edit`/`write` deny without a `read` deny
|
|
50835
|
+
* cannot be written (Pool's deny blocks reads as well), so it is reported and
|
|
50836
|
+
* only withholds the write flags it overlaps. A deny Pool cannot spell is
|
|
50837
|
+
* skipped the same way and withholds the allows it overlaps, so it is honored
|
|
50838
|
+
* by prompting rather than dropped.
|
|
50839
|
+
*/
|
|
50840
|
+
function convertRulesyncToPoolPaths({ permission, global, logger }) {
|
|
50841
|
+
const readRules = rulesOf(permission, READ_CATEGORY);
|
|
50842
|
+
const writeRulesByCategory = WRITE_CATEGORIES.map((category) => [category, rulesOf(permission, category)]);
|
|
50843
|
+
const readAsks = patternsWithAction(readRules, ["ask"]);
|
|
50844
|
+
const fitsScope = ({ category, action, pattern }) => {
|
|
50845
|
+
if (isHomeOrAbsolutePath(pattern) === global) return true;
|
|
50846
|
+
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 ${(0, node_path.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 ~/${(0, node_path.join)(POOL_GLOBAL_DIR, POOL_SETTINGS_FILE_NAME)} (via --global) instead.`);
|
|
50847
|
+
return false;
|
|
50848
|
+
};
|
|
50849
|
+
const deny = [];
|
|
50850
|
+
const readRestrictions = [...readAsks];
|
|
50851
|
+
for (const pattern of patternsWithAction(readRules, ["deny"])) {
|
|
50852
|
+
const path = toPoolPathPattern(pattern);
|
|
50853
|
+
if (path === void 0) {
|
|
50854
|
+
warnSkippedPathPattern({
|
|
50855
|
+
category: READ_CATEGORY,
|
|
50856
|
+
action: "deny",
|
|
50857
|
+
pattern,
|
|
50858
|
+
logger
|
|
50859
|
+
});
|
|
50860
|
+
readRestrictions.push(pattern);
|
|
50861
|
+
continue;
|
|
50862
|
+
}
|
|
50863
|
+
if (!fitsScope({
|
|
50864
|
+
category: READ_CATEGORY,
|
|
50865
|
+
action: "deny",
|
|
50866
|
+
pattern
|
|
50867
|
+
})) continue;
|
|
50868
|
+
if (!deny.some((entry) => entry[POOL_PATH_KEY] === path)) deny.push({ [POOL_PATH_KEY]: path });
|
|
50869
|
+
}
|
|
50870
|
+
const isDeniedForRead = (pattern) => {
|
|
50871
|
+
const path = toPoolPathPattern(pattern);
|
|
50872
|
+
return path !== void 0 && deny.some((entry) => entry[POOL_PATH_KEY] === path);
|
|
50873
|
+
};
|
|
50874
|
+
const writeRestrictions = writeRulesByCategory.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...patternsWithAction(rules, ["deny"]).filter((pattern) => !isDeniedForRead(pattern))]);
|
|
50875
|
+
reportUnwrittenWriteDenies({
|
|
50876
|
+
writeRulesByCategory,
|
|
50877
|
+
isDeniedForRead,
|
|
50878
|
+
logger
|
|
50879
|
+
});
|
|
50880
|
+
const shadowingReadRestrictions = createShadowingRestrictionsTest(asRestrictions(readRestrictions));
|
|
50881
|
+
const shadowingWriteRestrictions = createShadowingRestrictionsTest(asRestrictions([...readRestrictions, ...writeRestrictions]));
|
|
50882
|
+
const allowByPath = /* @__PURE__ */ new Map();
|
|
50883
|
+
for (const pattern of patternsWithAction(readRules, ["allow"])) {
|
|
50884
|
+
if (!fitsScope({
|
|
50885
|
+
category: READ_CATEGORY,
|
|
50886
|
+
action: "allow",
|
|
50887
|
+
pattern
|
|
50888
|
+
})) continue;
|
|
50889
|
+
const path = toPoolPathPattern(pattern);
|
|
50890
|
+
if (path === void 0) {
|
|
50891
|
+
warnSkippedPathPattern({
|
|
50892
|
+
category: READ_CATEGORY,
|
|
50893
|
+
action: "allow",
|
|
50894
|
+
pattern,
|
|
50895
|
+
logger
|
|
50896
|
+
});
|
|
50897
|
+
continue;
|
|
50898
|
+
}
|
|
50899
|
+
const shadowing = shadowingReadRestrictions(pattern);
|
|
50900
|
+
if (shadowing.length > 0) {
|
|
50901
|
+
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)}.`);
|
|
50902
|
+
continue;
|
|
50903
|
+
}
|
|
50904
|
+
allowByPath.set(path, { [POOL_PATH_KEY]: path });
|
|
50905
|
+
}
|
|
50906
|
+
for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["allow"])) {
|
|
50907
|
+
if (!fitsScope({
|
|
50908
|
+
category,
|
|
50909
|
+
action: "allow",
|
|
50910
|
+
pattern
|
|
50911
|
+
})) continue;
|
|
50912
|
+
const path = toPoolPathPattern(pattern);
|
|
50913
|
+
if (path === void 0) {
|
|
50914
|
+
warnSkippedPathPattern({
|
|
50915
|
+
category,
|
|
50916
|
+
action: "allow",
|
|
50917
|
+
pattern,
|
|
50918
|
+
logger
|
|
50919
|
+
});
|
|
50920
|
+
continue;
|
|
50921
|
+
}
|
|
50922
|
+
const shadowing = shadowingWriteRestrictions(pattern);
|
|
50923
|
+
if (shadowing.length > 0) {
|
|
50924
|
+
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.`);
|
|
50925
|
+
continue;
|
|
50926
|
+
}
|
|
50927
|
+
allowByPath.set(path, {
|
|
50928
|
+
[POOL_PATH_KEY]: path,
|
|
50929
|
+
[POOL_WRITE_KEY]: true
|
|
50930
|
+
});
|
|
50931
|
+
}
|
|
50932
|
+
return {
|
|
50933
|
+
allow: [...allowByPath.values()],
|
|
50934
|
+
deny
|
|
50935
|
+
};
|
|
50936
|
+
}
|
|
50937
|
+
function warnSkippedPathPattern({ category, action, pattern, logger }) {
|
|
50938
|
+
logger?.warn(`Pool path rules treat only "*" and "**" as wildcards, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
50939
|
+
}
|
|
50940
|
+
function reportUnwrittenWriteDenies({ writeRulesByCategory, isDeniedForRead, logger }) {
|
|
50941
|
+
for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["deny"])) {
|
|
50942
|
+
if (isDeniedForRead(pattern)) continue;
|
|
50943
|
+
if (toPoolPathPattern(pattern) === void 0) {
|
|
50944
|
+
warnSkippedPathPattern({
|
|
50945
|
+
category,
|
|
50946
|
+
action: "deny",
|
|
50947
|
+
pattern,
|
|
50948
|
+
logger
|
|
50949
|
+
});
|
|
50950
|
+
continue;
|
|
50951
|
+
}
|
|
50952
|
+
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.`);
|
|
50953
|
+
}
|
|
50954
|
+
}
|
|
50955
|
+
function isUsablePattern(pattern) {
|
|
50956
|
+
return typeof pattern === "string" && pattern !== "" && !isPrototypePollutionKey(pattern);
|
|
50957
|
+
}
|
|
50958
|
+
function pathOfEntry(entry) {
|
|
50959
|
+
return isRecord$1(entry) && isUsablePattern(entry[POOL_PATH_KEY]) ? entry[POOL_PATH_KEY] : void 0;
|
|
50960
|
+
}
|
|
50961
|
+
/**
|
|
50962
|
+
* Convert one `tools.<name>` block into its canonical bucket. A disabled tool
|
|
50963
|
+
* never runs, so it reads as a catch-all deny and its own lists are ignored;
|
|
50964
|
+
* a denied pattern wins over an allowed one, as it does in Pool.
|
|
50965
|
+
*/
|
|
50966
|
+
function importPoolTool({ toolConfig, bucket }) {
|
|
50967
|
+
if (toolConfig[POOL_DISABLED_KEY] === true) {
|
|
50968
|
+
bucket[CATCH_ALL_PATTERN$2] = "deny";
|
|
50969
|
+
return;
|
|
50970
|
+
}
|
|
50971
|
+
const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
|
|
50972
|
+
const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
|
|
50973
|
+
for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
|
|
50974
|
+
for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
|
|
50975
|
+
}
|
|
50976
|
+
/**
|
|
50977
|
+
* Convert Pool's `tools` and `paths` blocks back into a rulesync config.
|
|
50978
|
+
* `mcp_servers` approvals are the mcp feature's (`poolAllow`) and are not read.
|
|
50979
|
+
*/
|
|
50980
|
+
function convertPoolToRulesync({ tools, paths }) {
|
|
50981
|
+
const permission = {};
|
|
50982
|
+
const bucketFor = (category) => {
|
|
50983
|
+
const own = lookupOwn({
|
|
50984
|
+
record: permission,
|
|
50985
|
+
key: category
|
|
50986
|
+
});
|
|
50987
|
+
if (own !== void 0) return own;
|
|
50988
|
+
const created = {};
|
|
50989
|
+
permission[category] = created;
|
|
50990
|
+
return created;
|
|
50991
|
+
};
|
|
50992
|
+
for (const [toolName, toolConfig] of Object.entries(tools)) {
|
|
50993
|
+
if (isPrototypePollutionKey(toolName) || !isRecord$1(toolConfig)) continue;
|
|
50994
|
+
importPoolTool({
|
|
50995
|
+
toolConfig,
|
|
50996
|
+
bucket: bucketFor(toCanonicalToolName$4(toolName))
|
|
50997
|
+
});
|
|
50998
|
+
}
|
|
50999
|
+
const denyEntries = Array.isArray(paths[POOL_DENY_KEY]) ? paths[POOL_DENY_KEY] : [];
|
|
51000
|
+
const allowEntries = Array.isArray(paths[POOL_ALLOW_KEY]) ? paths[POOL_ALLOW_KEY] : [];
|
|
51001
|
+
for (const entry of denyEntries) {
|
|
51002
|
+
const path = pathOfEntry(entry);
|
|
51003
|
+
if (path === void 0) continue;
|
|
51004
|
+
for (const category of FILE_CATEGORIES) bucketFor(category)[path] = "deny";
|
|
51005
|
+
}
|
|
51006
|
+
for (const entry of allowEntries) {
|
|
51007
|
+
const path = pathOfEntry(entry);
|
|
51008
|
+
if (path === void 0) continue;
|
|
51009
|
+
bucketFor(READ_CATEGORY)[path] ??= "allow";
|
|
51010
|
+
if (isRecord$1(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
|
|
51011
|
+
}
|
|
51012
|
+
return { permission };
|
|
51013
|
+
}
|
|
51014
|
+
//#endregion
|
|
49956
51015
|
//#region src/features/permissions/qwencode-permissions.ts
|
|
49957
51016
|
/**
|
|
49958
51017
|
* Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
|
|
@@ -56055,6 +57114,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
56055
57114
|
supportsImport: true
|
|
56056
57115
|
}
|
|
56057
57116
|
}],
|
|
57117
|
+
["pool", {
|
|
57118
|
+
class: PoolPermissions,
|
|
57119
|
+
meta: {
|
|
57120
|
+
supportsProject: true,
|
|
57121
|
+
supportsGlobal: true,
|
|
57122
|
+
supportsImport: true
|
|
57123
|
+
}
|
|
57124
|
+
}],
|
|
56058
57125
|
["qwencode", {
|
|
56059
57126
|
class: QwencodePermissions,
|
|
56060
57127
|
meta: {
|
|
@@ -56718,8 +57785,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56718
57785
|
const changedPaths = [];
|
|
56719
57786
|
for (const aiDir of aiDirs) {
|
|
56720
57787
|
const dirPath = aiDir.getDirPath();
|
|
56721
|
-
let dirHasChanges = false;
|
|
56722
57788
|
const mainFile = aiDir.getMainFile();
|
|
57789
|
+
const otherFiles = aiDir.getOtherFiles();
|
|
57790
|
+
const writtenPaths = [
|
|
57791
|
+
dirPath,
|
|
57792
|
+
...mainFile ? [(0, node_path.join)(dirPath, mainFile.name)] : [],
|
|
57793
|
+
...otherFiles.map((file) => (0, node_path.join)(dirPath, file.relativeFilePathToDirPath))
|
|
57794
|
+
];
|
|
57795
|
+
if (await refusesAnyWriteOutsideRoot({
|
|
57796
|
+
logger: this.logger,
|
|
57797
|
+
rootPath: aiDir.getOutputRoot(),
|
|
57798
|
+
targetPaths: writtenPaths
|
|
57799
|
+
})) continue;
|
|
57800
|
+
let dirHasChanges = false;
|
|
56723
57801
|
let mainFileContent;
|
|
56724
57802
|
if (mainFile) {
|
|
56725
57803
|
const mainFilePath = (0, node_path.join)(dirPath, mainFile.name);
|
|
@@ -56731,7 +57809,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56731
57809
|
existing: existingContent
|
|
56732
57810
|
})) dirHasChanges = true;
|
|
56733
57811
|
}
|
|
56734
|
-
const otherFiles = aiDir.getOtherFiles();
|
|
56735
57812
|
for (const file of otherFiles) {
|
|
56736
57813
|
if (dirHasChanges) break;
|
|
56737
57814
|
const filePath = (0, node_path.join)(dirPath, file.relativeFilePathToDirPath);
|
|
@@ -68850,6 +69927,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
|
|
|
68850
69927
|
}
|
|
68851
69928
|
};
|
|
68852
69929
|
//#endregion
|
|
69930
|
+
//#region src/features/subagents/pool-subagent.ts
|
|
69931
|
+
/** How Pool runs an agent; a custom agent defaults to `in_process`. */
|
|
69932
|
+
const POOL_AGENT_TYPES = [
|
|
69933
|
+
"in_process",
|
|
69934
|
+
"command",
|
|
69935
|
+
"agent_server"
|
|
69936
|
+
];
|
|
69937
|
+
const DEFAULT_POOL_AGENT_TYPE = "in_process";
|
|
69938
|
+
/**
|
|
69939
|
+
* One entry of `subagents.agents.<name>` in Pool's settings file. `type` is
|
|
69940
|
+
* required by Pool and `description` by every enabled custom agent; the
|
|
69941
|
+
* remaining keys depend on the type (`command`/`args`/`env` for `command`,
|
|
69942
|
+
* `agent_server`/`session_config_options` for `agent_server`). Loose so a key
|
|
69943
|
+
* Pool adds later survives the round-trip.
|
|
69944
|
+
* @see https://docs.poolside.ai/subagents
|
|
69945
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
69946
|
+
*/
|
|
69947
|
+
const PoolAgentSchema = zod_mini.z.looseObject({
|
|
69948
|
+
type: zod_mini.z.optional(zod_mini.z.enum(POOL_AGENT_TYPES)),
|
|
69949
|
+
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
69950
|
+
instructions: zod_mini.z.optional(zod_mini.z.string()),
|
|
69951
|
+
command: zod_mini.z.optional(zod_mini.z.string()),
|
|
69952
|
+
args: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
69953
|
+
env: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
69954
|
+
agent_server: zod_mini.z.optional(zod_mini.z.string()),
|
|
69955
|
+
session_config_options: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.unknown())),
|
|
69956
|
+
inherit_agent_config: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
69957
|
+
disabled: zod_mini.z.optional(zod_mini.z.boolean())
|
|
69958
|
+
});
|
|
69959
|
+
/**
|
|
69960
|
+
* Single spelling of the settings.yaml codec/policy, matching PoolMcp: fail
|
|
69961
|
+
* closed on an unparseable root rather than replacing the user's primary Pool
|
|
69962
|
+
* settings with generated output.
|
|
69963
|
+
*/
|
|
69964
|
+
function parsePoolSettings(fileContent, filePath) {
|
|
69965
|
+
return parseSharedConfig({
|
|
69966
|
+
format: "yaml",
|
|
69967
|
+
fileContent,
|
|
69968
|
+
filePath,
|
|
69969
|
+
invalidRootPolicy: "error"
|
|
69970
|
+
});
|
|
69971
|
+
}
|
|
69972
|
+
/**
|
|
69973
|
+
* The file stem an imported agent lands under in `.rulesync/subagents/`. Pool
|
|
69974
|
+
* keys agents by an arbitrary YAML mapping key, so the name is narrowed to a
|
|
69975
|
+
* safe file name before it becomes a path (defense in depth on top of the
|
|
69976
|
+
* central path-traversal guard); the agent keeps its real name in `name`.
|
|
69977
|
+
*/
|
|
69978
|
+
function sanitizePoolAgentFileStem(raw) {
|
|
69979
|
+
const stem = raw.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
69980
|
+
return stem.length > 0 ? stem : "agent";
|
|
69981
|
+
}
|
|
69982
|
+
/**
|
|
69983
|
+
* Read `subagents.agents` out of a parsed settings document. The reserved
|
|
69984
|
+
* `general` agent is Pool's built-in and stays the user's to configure, so it
|
|
69985
|
+
* is never imported; an entry that is not a mapping is skipped.
|
|
69986
|
+
*/
|
|
69987
|
+
function agentsOfSettings({ settings, filePath, logger }) {
|
|
69988
|
+
const block = settings[POOL_SUBAGENTS_KEY];
|
|
69989
|
+
const rawAgents = isRecord$1(block) ? block[POOL_SUBAGENTS_AGENTS_KEY] : void 0;
|
|
69990
|
+
const agents = {};
|
|
69991
|
+
if (!isRecord$1(rawAgents)) return agents;
|
|
69992
|
+
for (const [name, raw] of Object.entries(rawAgents)) {
|
|
69993
|
+
if (isPrototypePollutionKey(name) || name === "general") continue;
|
|
69994
|
+
const result = PoolAgentSchema.safeParse(raw);
|
|
69995
|
+
if (!result.success) {
|
|
69996
|
+
logger?.warn(`Skipped Pool subagent ${quoteValueForWarning(name)} in ${filePath}: ${formatError(result.error)}`);
|
|
69997
|
+
continue;
|
|
69998
|
+
}
|
|
69999
|
+
agents[name] = result.data;
|
|
70000
|
+
}
|
|
70001
|
+
return agents;
|
|
70002
|
+
}
|
|
70003
|
+
/**
|
|
70004
|
+
* Pool subagents.
|
|
70005
|
+
*
|
|
70006
|
+
* Pool defines subagents inside its settings file — `.poolside/settings.yaml`
|
|
70007
|
+
* at project scope and `~/.config/poolside/settings.yaml` at user scope —
|
|
70008
|
+
* under `subagents.agents.<name>`, so every targeted rulesync subagent is
|
|
70009
|
+
* collapsed into that one file: the body becomes `instructions`, the shared
|
|
70010
|
+
* `description` is written as-is and the `pool:` frontmatter section supplies
|
|
70011
|
+
* `type` (default `in_process`) and the type-specific keys. rulesync owns the
|
|
70012
|
+
* `agents` map except for Pool's built-in `general` entry, which is carried
|
|
70013
|
+
* over untouched together with `subagents.default` and every other top-level
|
|
70014
|
+
* key of the file (`mcp_servers`, `tools`, `pool`, ...); the file is never
|
|
70015
|
+
* deleted.
|
|
70016
|
+
*
|
|
70017
|
+
* @see https://docs.poolside.ai/subagents
|
|
70018
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
70019
|
+
*/
|
|
70020
|
+
var PoolSubagent = class extends ToolSubagent {
|
|
70021
|
+
agents;
|
|
70022
|
+
logger;
|
|
70023
|
+
/**
|
|
70024
|
+
* The settings document to write, built lazily: the writer hands over the
|
|
70025
|
+
* existing file through `setFileContent` first, and a file that does not
|
|
70026
|
+
* exist yet gets the block merged into an empty document. Lazy so that a
|
|
70027
|
+
* `forDeletion` instance never consults the gateway.
|
|
70028
|
+
*/
|
|
70029
|
+
mergedContent = null;
|
|
70030
|
+
constructor({ agents, logger, ...rest }) {
|
|
70031
|
+
super({
|
|
70032
|
+
...rest,
|
|
70033
|
+
fileContent: ""
|
|
70034
|
+
});
|
|
70035
|
+
this.agents = agents;
|
|
70036
|
+
this.logger = logger;
|
|
70037
|
+
}
|
|
70038
|
+
getAgents() {
|
|
70039
|
+
return this.agents;
|
|
70040
|
+
}
|
|
70041
|
+
isDeletable() {
|
|
70042
|
+
return false;
|
|
70043
|
+
}
|
|
70044
|
+
shouldMergeExistingFileContent() {
|
|
70045
|
+
return true;
|
|
70046
|
+
}
|
|
70047
|
+
setFileContent(newFileContent) {
|
|
70048
|
+
this.mergedContent = this.mergeInto(newFileContent);
|
|
70049
|
+
}
|
|
70050
|
+
getFileContent() {
|
|
70051
|
+
if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
|
|
70052
|
+
return this.mergedContent;
|
|
70053
|
+
}
|
|
70054
|
+
/**
|
|
70055
|
+
* Rebuild the `subagents` block on top of an existing settings document:
|
|
70056
|
+
* `default` and any other sibling of `agents` are kept, the built-in
|
|
70057
|
+
* `general` agent is carried over, and every other agent is replaced by the
|
|
70058
|
+
* generated set — an agent removed from `.rulesync/subagents/` must not
|
|
70059
|
+
* survive here. A block left empty is retracted rather than written as `{}`.
|
|
70060
|
+
*/
|
|
70061
|
+
mergeInto(existingContent) {
|
|
70062
|
+
const paths = {
|
|
70063
|
+
relativeDirPath: this.getRelativeDirPath(),
|
|
70064
|
+
relativeFilePath: this.getRelativeFilePath()
|
|
70065
|
+
};
|
|
70066
|
+
const filePath = (0, node_path.join)(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
|
|
70067
|
+
const existing = parsePoolSettings(existingContent, filePath);
|
|
70068
|
+
const existingBlock = isRecord$1(existing["subagents"]) ? existing[POOL_SUBAGENTS_KEY] : {};
|
|
70069
|
+
const block = {};
|
|
70070
|
+
for (const [key, value] of Object.entries(existingBlock)) if (!isPrototypePollutionKey(key) && key !== "agents") block[key] = value;
|
|
70071
|
+
const existingAgents = isRecord$1(existingBlock["agents"]) ? existingBlock[POOL_SUBAGENTS_AGENTS_KEY] : {};
|
|
70072
|
+
const general = existingAgents[POOL_GENERAL_AGENT_NAME];
|
|
70073
|
+
const agents = {
|
|
70074
|
+
...general !== void 0 ? { [POOL_GENERAL_AGENT_NAME]: general } : {},
|
|
70075
|
+
...this.agents
|
|
70076
|
+
};
|
|
70077
|
+
const dropped = Object.keys(existingAgents).filter((name) => name !== "general" && !Object.hasOwn(agents, name));
|
|
70078
|
+
if (dropped.length > 0) {
|
|
70079
|
+
const advice = this.global ? "hand-written agents belong in .rulesync/subagents/" : `hand-written agents belong in ${POOL_SETTINGS_LOCAL_FILE_NAME}`;
|
|
70080
|
+
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}.`);
|
|
70081
|
+
}
|
|
70082
|
+
if (Object.keys(agents).length > 0) block[POOL_SUBAGENTS_AGENTS_KEY] = agents;
|
|
70083
|
+
return applySharedConfigPatch({
|
|
70084
|
+
fileKey: sharedConfigFileKey(paths),
|
|
70085
|
+
feature: "subagents",
|
|
70086
|
+
existingContent,
|
|
70087
|
+
patch: { [POOL_SUBAGENTS_KEY]: Object.keys(block).length > 0 ? block : void 0 },
|
|
70088
|
+
filePath,
|
|
70089
|
+
logger: this.logger
|
|
70090
|
+
});
|
|
70091
|
+
}
|
|
70092
|
+
/**
|
|
70093
|
+
* Every agent lives in the one settings file, which the MCP feature writes
|
|
70094
|
+
* too: naming the file here keeps the gitignore derivation from claiming the
|
|
70095
|
+
* whole directory and lets the shared-file derivation order the writers.
|
|
70096
|
+
*/
|
|
70097
|
+
static getSettablePaths({ global = false } = {}) {
|
|
70098
|
+
return {
|
|
70099
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
70100
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
70101
|
+
};
|
|
70102
|
+
}
|
|
70103
|
+
/**
|
|
70104
|
+
* Map a single rulesync subagent to a Pool agent entry. The body becomes
|
|
70105
|
+
* `instructions` and the shared `description` is written as-is; the
|
|
70106
|
+
* optional `pool:` section supplies `type` and the type-specific keys and
|
|
70107
|
+
* may override both. Returns `null` for the reserved `general` name, which
|
|
70108
|
+
* Pool defines itself.
|
|
70109
|
+
*/
|
|
70110
|
+
static toPoolAgent({ rulesyncSubagent, logger }) {
|
|
70111
|
+
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
70112
|
+
const name = frontmatter.name;
|
|
70113
|
+
if (name === "general") {
|
|
70114
|
+
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.`);
|
|
70115
|
+
return null;
|
|
70116
|
+
}
|
|
70117
|
+
if (isPrototypePollutionKey(name)) {
|
|
70118
|
+
logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: unsafe name.`);
|
|
70119
|
+
return null;
|
|
70120
|
+
}
|
|
70121
|
+
const rawSection = isRecord$1(frontmatter.pool) ? frontmatter.pool : {};
|
|
70122
|
+
const result = PoolAgentSchema.safeParse(rawSection);
|
|
70123
|
+
if (!result.success) throw new Error(`Invalid "pool" section of subagent ${quoteValueForWarning(name)}: ${formatError(result.error)}`);
|
|
70124
|
+
const { type, description, instructions, ...rest } = result.data;
|
|
70125
|
+
const body = rulesyncSubagent.getBody().trim();
|
|
70126
|
+
const effectiveDescription = description ?? frontmatter.description;
|
|
70127
|
+
const effectiveInstructions = instructions ?? (body !== "" ? body : void 0);
|
|
70128
|
+
if (effectiveDescription === void 0 && rest.disabled !== true) logger?.warn(`Pool subagent ${quoteValueForWarning(name)} has no description; Pool requires one for every enabled custom agent.`);
|
|
70129
|
+
return {
|
|
70130
|
+
name,
|
|
70131
|
+
agent: {
|
|
70132
|
+
type: type ?? DEFAULT_POOL_AGENT_TYPE,
|
|
70133
|
+
...effectiveDescription !== void 0 ? { description: effectiveDescription } : {},
|
|
70134
|
+
...effectiveInstructions !== void 0 ? { instructions: effectiveInstructions } : {},
|
|
70135
|
+
...rest
|
|
70136
|
+
}
|
|
70137
|
+
};
|
|
70138
|
+
}
|
|
70139
|
+
/**
|
|
70140
|
+
* Aggregate every targeted rulesync subagent into the one settings file.
|
|
70141
|
+
* Agents are keyed by their `name`; a repeated name keeps the last one, with
|
|
70142
|
+
* a warning, so the map stays what Pool would read.
|
|
70143
|
+
*/
|
|
70144
|
+
static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true, global = false, logger }) {
|
|
70145
|
+
const agents = {};
|
|
70146
|
+
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
70147
|
+
const converted = this.toPoolAgent({
|
|
70148
|
+
rulesyncSubagent,
|
|
70149
|
+
logger
|
|
70150
|
+
});
|
|
70151
|
+
if (converted === null) continue;
|
|
70152
|
+
if (Object.hasOwn(agents, converted.name)) logger?.warn(`Pool subagent ${quoteValueForWarning(converted.name)} is defined more than once; the last definition (${rulesyncSubagent.getRelativePathFromCwd()}) wins.`);
|
|
70153
|
+
agents[converted.name] = converted.agent;
|
|
70154
|
+
}
|
|
70155
|
+
return new this({
|
|
70156
|
+
outputRoot,
|
|
70157
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
70158
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME,
|
|
70159
|
+
agents,
|
|
70160
|
+
validate,
|
|
70161
|
+
global,
|
|
70162
|
+
logger
|
|
70163
|
+
});
|
|
70164
|
+
}
|
|
70165
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
70166
|
+
return this.fromRulesyncSubagents({
|
|
70167
|
+
outputRoot,
|
|
70168
|
+
rulesyncSubagents: [rulesyncSubagent],
|
|
70169
|
+
validate,
|
|
70170
|
+
global,
|
|
70171
|
+
logger
|
|
70172
|
+
});
|
|
70173
|
+
}
|
|
70174
|
+
/**
|
|
70175
|
+
* Convert every custom agent of the settings file back into an individual
|
|
70176
|
+
* rulesync subagent. `instructions` becomes the body, `description` the
|
|
70177
|
+
* shared field, and everything else — `type` only when it is not the
|
|
70178
|
+
* default — rides the `pool:` section so it survives the round-trip.
|
|
70179
|
+
*/
|
|
70180
|
+
toRulesyncSubagents() {
|
|
70181
|
+
return Object.entries(this.agents).map(([name, agent]) => {
|
|
70182
|
+
const { type, description, instructions, ...rest } = agent;
|
|
70183
|
+
const poolSection = {
|
|
70184
|
+
...type !== void 0 && type !== DEFAULT_POOL_AGENT_TYPE ? { type } : {},
|
|
70185
|
+
...rest
|
|
70186
|
+
};
|
|
70187
|
+
const rulesyncFrontmatter = {
|
|
70188
|
+
targets: ["pool"],
|
|
70189
|
+
name,
|
|
70190
|
+
...description !== void 0 ? { description } : {},
|
|
70191
|
+
...Object.keys(poolSection).length > 0 ? { pool: poolSection } : {}
|
|
70192
|
+
};
|
|
70193
|
+
return new RulesyncSubagent({
|
|
70194
|
+
outputRoot: process.cwd(),
|
|
70195
|
+
frontmatter: rulesyncFrontmatter,
|
|
70196
|
+
body: instructions ?? "",
|
|
70197
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
70198
|
+
relativeFilePath: `${sanitizePoolAgentFileStem(name)}.md`,
|
|
70199
|
+
validate: true
|
|
70200
|
+
});
|
|
70201
|
+
});
|
|
70202
|
+
}
|
|
70203
|
+
toRulesyncSubagent() {
|
|
70204
|
+
const first = this.toRulesyncSubagents()[0];
|
|
70205
|
+
if (!first) throw new Error(`No custom subagents found in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())} to convert.`);
|
|
70206
|
+
return first;
|
|
70207
|
+
}
|
|
70208
|
+
validate() {
|
|
70209
|
+
return {
|
|
70210
|
+
success: true,
|
|
70211
|
+
error: null
|
|
70212
|
+
};
|
|
70213
|
+
}
|
|
70214
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
70215
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
70216
|
+
rulesyncSubagent,
|
|
70217
|
+
toolTarget: "pool"
|
|
70218
|
+
});
|
|
70219
|
+
}
|
|
70220
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, logger }) {
|
|
70221
|
+
const paths = this.getSettablePaths({ global });
|
|
70222
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
70223
|
+
const settings = parsePoolSettings(await readFileContentOrNull(filePath) ?? "", filePath);
|
|
70224
|
+
return new this({
|
|
70225
|
+
outputRoot,
|
|
70226
|
+
relativeDirPath: paths.relativeDirPath,
|
|
70227
|
+
relativeFilePath,
|
|
70228
|
+
agents: agentsOfSettings({
|
|
70229
|
+
settings,
|
|
70230
|
+
filePath,
|
|
70231
|
+
logger
|
|
70232
|
+
}),
|
|
70233
|
+
validate,
|
|
70234
|
+
global
|
|
70235
|
+
});
|
|
70236
|
+
}
|
|
70237
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
70238
|
+
return new this({
|
|
70239
|
+
outputRoot,
|
|
70240
|
+
relativeDirPath,
|
|
70241
|
+
relativeFilePath,
|
|
70242
|
+
agents: {},
|
|
70243
|
+
validate: false,
|
|
70244
|
+
global
|
|
70245
|
+
});
|
|
70246
|
+
}
|
|
70247
|
+
};
|
|
70248
|
+
//#endregion
|
|
68853
70249
|
//#region src/features/subagents/reasonix-subagent.ts
|
|
68854
70250
|
const ReasonixSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
68855
70251
|
name: zod_mini.z.string(),
|
|
@@ -70170,6 +71566,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
70170
71566
|
filePattern: "*.md"
|
|
70171
71567
|
}
|
|
70172
71568
|
}],
|
|
71569
|
+
["pool", {
|
|
71570
|
+
class: PoolSubagent,
|
|
71571
|
+
meta: {
|
|
71572
|
+
supportsProject: true,
|
|
71573
|
+
supportsSimulated: false,
|
|
71574
|
+
supportsGlobal: true,
|
|
71575
|
+
emitsEmptyAggregate: true,
|
|
71576
|
+
filePattern: POOL_SETTINGS_FILE_NAME
|
|
71577
|
+
}
|
|
71578
|
+
}],
|
|
70173
71579
|
["qwencode", {
|
|
70174
71580
|
class: QwencodeSubagent,
|
|
70175
71581
|
meta: {
|
|
@@ -70282,6 +71688,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70282
71688
|
toolTarget;
|
|
70283
71689
|
global;
|
|
70284
71690
|
getFactory;
|
|
71691
|
+
/**
|
|
71692
|
+
* Whether the last `loadRulesyncFiles` found a `subagents/` directory under
|
|
71693
|
+
* any input root. A project without one has not adopted the feature, so the
|
|
71694
|
+
* agents in its Pool settings are its own and must not be retracted.
|
|
71695
|
+
*/
|
|
71696
|
+
rulesyncSourceDirFound = false;
|
|
70285
71697
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
70286
71698
|
super({
|
|
70287
71699
|
outputRoot,
|
|
@@ -70295,16 +71707,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70295
71707
|
this.global = global;
|
|
70296
71708
|
this.getFactory = getFactory;
|
|
70297
71709
|
}
|
|
71710
|
+
emitsToolFilesForEmptySource() {
|
|
71711
|
+
return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
|
|
71712
|
+
}
|
|
70298
71713
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
70299
71714
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
70300
71715
|
const factory = this.getFactory(this.toolTarget);
|
|
70301
71716
|
const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
|
|
70302
71717
|
if (factory.class.fromRulesyncSubagents) {
|
|
70303
|
-
if (targeted.length === 0) return [];
|
|
71718
|
+
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
70304
71719
|
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
70305
71720
|
outputRoot: this.outputRoot,
|
|
70306
71721
|
rulesyncSubagents: targeted,
|
|
70307
|
-
global: this.global
|
|
71722
|
+
global: this.global,
|
|
71723
|
+
logger: this.logger
|
|
70308
71724
|
});
|
|
70309
71725
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
70310
71726
|
}
|
|
@@ -70360,6 +71776,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70360
71776
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
70361
71777
|
return [];
|
|
70362
71778
|
}
|
|
71779
|
+
this.rulesyncSourceDirFound = true;
|
|
70363
71780
|
const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
|
|
70364
71781
|
if (mdFiles.length === 0) {
|
|
70365
71782
|
this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
|
|
@@ -70400,6 +71817,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70400
71817
|
* earlier root's copy.
|
|
70401
71818
|
*/
|
|
70402
71819
|
async loadRulesyncFiles() {
|
|
71820
|
+
this.rulesyncSourceDirFound = false;
|
|
70403
71821
|
const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
|
|
70404
71822
|
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
70405
71823
|
identity: (subagent) => subagent.getRelativeFilePath(),
|
|
@@ -70462,7 +71880,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70462
71880
|
outputRoot: rootOutputRoot,
|
|
70463
71881
|
relativeDirPath: dirPath,
|
|
70464
71882
|
relativeFilePath: toRelativeFilePath(path),
|
|
70465
|
-
global: this.global
|
|
71883
|
+
global: this.global,
|
|
71884
|
+
logger: this.logger
|
|
70466
71885
|
})));
|
|
70467
71886
|
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
70468
71887
|
loaded,
|
|
@@ -74384,22 +75803,30 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
74384
75803
|
}
|
|
74385
75804
|
};
|
|
74386
75805
|
//#endregion
|
|
74387
|
-
//#region src/features/rules/
|
|
75806
|
+
//#region src/features/rules/nested-agentsmd-rule.ts
|
|
74388
75807
|
/**
|
|
74389
|
-
*
|
|
75808
|
+
* Shared adapter for the targets that read instructions exactly the way the
|
|
75809
|
+
* AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
|
|
75810
|
+
* project root, nested per-directory `AGENTS.md` files loaded only while working
|
|
75811
|
+
* under that directory, and a single personal root file under a tool-specific
|
|
75812
|
+
* home directory. Such a target has no modular non-root instruction directory,
|
|
75813
|
+
* so every other non-root rule folds into the root file.
|
|
74390
75814
|
*
|
|
74391
|
-
*
|
|
74392
|
-
*
|
|
74393
|
-
*
|
|
74394
|
-
* `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
|
|
74395
|
-
* single root file with no local overlay, so non-root rules fold into it.
|
|
75815
|
+
* Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
|
|
75816
|
+
* directory and their tool target through {@link getFamily}; the vendor docs
|
|
75817
|
+
* describing each one's discovery walk live on the subclass.
|
|
74396
75818
|
*
|
|
74397
|
-
*
|
|
75819
|
+
* The nested-file scan mirrors the AGENTS.md standard's nested discovery —
|
|
75820
|
+
* same file name, same exclusions, import-only, project scope — because it
|
|
75821
|
+
* discovers literally the same files.
|
|
74398
75822
|
*/
|
|
74399
|
-
var
|
|
75823
|
+
var NestedAgentsmdRule = class extends ToolRule {
|
|
75824
|
+
static getFamily() {
|
|
75825
|
+
throw new Error("Please implement this method in the subclass.");
|
|
75826
|
+
}
|
|
74400
75827
|
static getSettablePaths({ global = false } = {}) {
|
|
74401
75828
|
if (global) return { root: {
|
|
74402
|
-
relativeDirPath:
|
|
75829
|
+
relativeDirPath: this.getFamily().globalDir,
|
|
74403
75830
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74404
75831
|
} };
|
|
74405
75832
|
return { root: {
|
|
@@ -74407,18 +75834,6 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74407
75834
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74408
75835
|
} };
|
|
74409
75836
|
}
|
|
74410
|
-
/**
|
|
74411
|
-
* DeepSeek Harness loads the project chain of `AGENTS.md` files from the
|
|
74412
|
-
* project root (the nearest ancestor holding `.git`) down through the session
|
|
74413
|
-
* cwd, broad to specific, so a per-directory file applies only while working
|
|
74414
|
-
* under that directory. Nested files are therefore a real scoping surface,
|
|
74415
|
-
* not just the root file's overflow.
|
|
74416
|
-
*
|
|
74417
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
74418
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
74419
|
-
* literally the same files.
|
|
74420
|
-
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
74421
|
-
*/
|
|
74422
75837
|
static getNestedFilePatterns() {
|
|
74423
75838
|
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
74424
75839
|
}
|
|
@@ -74433,7 +75848,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74433
75848
|
const { root } = this.getSettablePaths({ global });
|
|
74434
75849
|
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
74435
75850
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
74436
|
-
return new
|
|
75851
|
+
return new this({
|
|
74437
75852
|
outputRoot,
|
|
74438
75853
|
relativeDirPath: overrideDirPath,
|
|
74439
75854
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74443,7 +75858,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74443
75858
|
});
|
|
74444
75859
|
}
|
|
74445
75860
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
74446
|
-
return new
|
|
75861
|
+
return new this({
|
|
74447
75862
|
outputRoot,
|
|
74448
75863
|
relativeDirPath: root.relativeDirPath,
|
|
74449
75864
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74457,7 +75872,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74457
75872
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
74458
75873
|
const isRoot = frontmatter.root ?? false;
|
|
74459
75874
|
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
74460
|
-
if (!global && !isRoot && subprojectPath) return new
|
|
75875
|
+
if (!global && !isRoot && subprojectPath) return new this({
|
|
74461
75876
|
outputRoot,
|
|
74462
75877
|
relativeDirPath: (0, node_path.join)(subprojectPath),
|
|
74463
75878
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74465,7 +75880,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74465
75880
|
validate,
|
|
74466
75881
|
root: false
|
|
74467
75882
|
});
|
|
74468
|
-
return new
|
|
75883
|
+
return new this({
|
|
74469
75884
|
outputRoot,
|
|
74470
75885
|
relativeDirPath: root.relativeDirPath,
|
|
74471
75886
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74486,23 +75901,47 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74486
75901
|
};
|
|
74487
75902
|
}
|
|
74488
75903
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
74489
|
-
|
|
75904
|
+
const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
|
|
75905
|
+
return new this({
|
|
74490
75906
|
outputRoot,
|
|
74491
75907
|
relativeDirPath,
|
|
74492
75908
|
relativeFilePath,
|
|
74493
75909
|
fileContent: "",
|
|
74494
75910
|
validate: false,
|
|
74495
|
-
root:
|
|
75911
|
+
root: isRoot
|
|
74496
75912
|
});
|
|
74497
75913
|
}
|
|
74498
75914
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
74499
75915
|
return this.isTargetedByRulesyncRuleDefault({
|
|
74500
75916
|
rulesyncRule,
|
|
74501
|
-
toolTarget:
|
|
75917
|
+
toolTarget: this.getFamily().toolTarget
|
|
74502
75918
|
});
|
|
74503
75919
|
}
|
|
74504
75920
|
};
|
|
74505
75921
|
//#endregion
|
|
75922
|
+
//#region src/features/rules/dsh-rule.ts
|
|
75923
|
+
/**
|
|
75924
|
+
* DeepSeek Harness (`dsh`) rules.
|
|
75925
|
+
*
|
|
75926
|
+
* Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
|
|
75927
|
+
* files — the project chain `dsh-agent-instructions` loads from the `.git`
|
|
75928
|
+
* root down to the session cwd, broad to specific, so a per-directory file
|
|
75929
|
+
* applies only while working under that directory. Global scope writes the
|
|
75930
|
+
* user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
|
|
75931
|
+
* reads as a single root file with no `.local.md` overlay, so non-root rules
|
|
75932
|
+
* fold into it.
|
|
75933
|
+
*
|
|
75934
|
+
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
75935
|
+
*/
|
|
75936
|
+
var DshRule = class extends NestedAgentsmdRule {
|
|
75937
|
+
static getFamily() {
|
|
75938
|
+
return {
|
|
75939
|
+
globalDir: DSH_DIR,
|
|
75940
|
+
toolTarget: "dsh"
|
|
75941
|
+
};
|
|
75942
|
+
}
|
|
75943
|
+
};
|
|
75944
|
+
//#endregion
|
|
74506
75945
|
//#region src/features/rules/factorydroid-rule.ts
|
|
74507
75946
|
/**
|
|
74508
75947
|
* Rule generator for Factory Droid.
|
|
@@ -75896,110 +77335,24 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
75896
77335
|
};
|
|
75897
77336
|
//#endregion
|
|
75898
77337
|
//#region src/features/rules/pool-rule.ts
|
|
75899
|
-
|
|
75900
|
-
|
|
75901
|
-
|
|
75902
|
-
|
|
75903
|
-
|
|
75904
|
-
|
|
75905
|
-
|
|
75906
|
-
|
|
75907
|
-
|
|
75908
|
-
|
|
75909
|
-
|
|
75910
|
-
|
|
75911
|
-
|
|
75912
|
-
|
|
75913
|
-
* through the working directory (deeper files take precedence), skipping
|
|
75914
|
-
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
75915
|
-
* just the root file's overflow.
|
|
75916
|
-
*
|
|
75917
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
75918
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
75919
|
-
* literally the same files.
|
|
75920
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
75921
|
-
*/
|
|
75922
|
-
static getNestedFilePatterns() {
|
|
75923
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75924
|
-
}
|
|
75925
|
-
/**
|
|
75926
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
75927
|
-
* (project or global).
|
|
75928
|
-
*/
|
|
75929
|
-
getSubprojectPath() {
|
|
75930
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75931
|
-
}
|
|
75932
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
75933
|
-
const { root } = this.getSettablePaths({ global });
|
|
75934
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
75935
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
75936
|
-
return new PoolRule({
|
|
75937
|
-
outputRoot,
|
|
75938
|
-
relativeDirPath: overrideDirPath,
|
|
75939
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75940
|
-
fileContent,
|
|
75941
|
-
validate,
|
|
75942
|
-
root: false
|
|
75943
|
-
});
|
|
75944
|
-
}
|
|
75945
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
75946
|
-
return new PoolRule({
|
|
75947
|
-
outputRoot,
|
|
75948
|
-
relativeDirPath: root.relativeDirPath,
|
|
75949
|
-
relativeFilePath: root.relativeFilePath,
|
|
75950
|
-
fileContent,
|
|
75951
|
-
validate,
|
|
75952
|
-
root: true
|
|
75953
|
-
});
|
|
75954
|
-
}
|
|
75955
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
75956
|
-
const { root } = this.getSettablePaths({ global });
|
|
75957
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
75958
|
-
const isRoot = frontmatter.root ?? false;
|
|
75959
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
75960
|
-
if (!global && !isRoot && subprojectPath) return new PoolRule({
|
|
75961
|
-
outputRoot,
|
|
75962
|
-
relativeDirPath: (0, node_path.join)(subprojectPath),
|
|
75963
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75964
|
-
fileContent: rulesyncRule.getBody(),
|
|
75965
|
-
validate,
|
|
75966
|
-
root: false
|
|
75967
|
-
});
|
|
75968
|
-
return new PoolRule({
|
|
75969
|
-
outputRoot,
|
|
75970
|
-
relativeDirPath: root.relativeDirPath,
|
|
75971
|
-
relativeFilePath: root.relativeFilePath,
|
|
75972
|
-
fileContent: rulesyncRule.getBody(),
|
|
75973
|
-
validate,
|
|
75974
|
-
root: isRoot
|
|
75975
|
-
});
|
|
75976
|
-
}
|
|
75977
|
-
toRulesyncRule() {
|
|
75978
|
-
const subprojectPath = this.getSubprojectPath();
|
|
75979
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
75980
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
75981
|
-
}
|
|
75982
|
-
validate() {
|
|
77338
|
+
/**
|
|
77339
|
+
* Pool (Poolside) rules.
|
|
77340
|
+
*
|
|
77341
|
+
* Pool reads personal, project, and directory-level `AGENTS.md` files: inside
|
|
77342
|
+
* a git repository it loads every `AGENTS.md` from the repository root down
|
|
77343
|
+
* through the working directory (deeper files take precedence), skipping
|
|
77344
|
+
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
77345
|
+
* just the root file's overflow. The personal file sits below both at
|
|
77346
|
+
* `~/.config/poolside/AGENTS.md`.
|
|
77347
|
+
*
|
|
77348
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
77349
|
+
*/
|
|
77350
|
+
var PoolRule = class extends NestedAgentsmdRule {
|
|
77351
|
+
static getFamily() {
|
|
75983
77352
|
return {
|
|
75984
|
-
|
|
75985
|
-
error: null
|
|
75986
|
-
};
|
|
75987
|
-
}
|
|
75988
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
75989
|
-
return new PoolRule({
|
|
75990
|
-
outputRoot,
|
|
75991
|
-
relativeDirPath,
|
|
75992
|
-
relativeFilePath,
|
|
75993
|
-
fileContent: "",
|
|
75994
|
-
validate: false,
|
|
75995
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
|
|
75996
|
-
});
|
|
75997
|
-
}
|
|
75998
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
75999
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
76000
|
-
rulesyncRule,
|
|
77353
|
+
globalDir: POOL_GLOBAL_DIR,
|
|
76001
77354
|
toolTarget: "pool"
|
|
76002
|
-
}
|
|
77355
|
+
};
|
|
76003
77356
|
}
|
|
76004
77357
|
};
|
|
76005
77358
|
//#endregion
|
|
@@ -76963,110 +78316,23 @@ var TaktRule = class TaktRule extends ToolRule {
|
|
|
76963
78316
|
//#endregion
|
|
76964
78317
|
//#region src/features/rules/vibe-rule.ts
|
|
76965
78318
|
const VIBE_GLOBAL_DIR = ".vibe";
|
|
76966
|
-
|
|
76967
|
-
|
|
76968
|
-
|
|
76969
|
-
|
|
76970
|
-
|
|
76971
|
-
|
|
76972
|
-
|
|
76973
|
-
|
|
76974
|
-
|
|
76975
|
-
|
|
76976
|
-
|
|
76977
|
-
|
|
76978
|
-
|
|
76979
|
-
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
76980
|
-
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
76981
|
-
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
76982
|
-
* the root file's overflow.
|
|
76983
|
-
*
|
|
76984
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
76985
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
76986
|
-
* literally the same files.
|
|
76987
|
-
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
76988
|
-
*/
|
|
76989
|
-
static getNestedFilePatterns() {
|
|
76990
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76991
|
-
}
|
|
76992
|
-
/**
|
|
76993
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
76994
|
-
* (project or global).
|
|
76995
|
-
*/
|
|
76996
|
-
getSubprojectPath() {
|
|
76997
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76998
|
-
}
|
|
76999
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
77000
|
-
const { root } = this.getSettablePaths({ global });
|
|
77001
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
77002
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
77003
|
-
return new VibeRule({
|
|
77004
|
-
outputRoot,
|
|
77005
|
-
relativeDirPath: overrideDirPath,
|
|
77006
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
77007
|
-
fileContent,
|
|
77008
|
-
validate,
|
|
77009
|
-
root: false
|
|
77010
|
-
});
|
|
77011
|
-
}
|
|
77012
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
77013
|
-
return new VibeRule({
|
|
77014
|
-
outputRoot,
|
|
77015
|
-
relativeDirPath: root.relativeDirPath,
|
|
77016
|
-
relativeFilePath: root.relativeFilePath,
|
|
77017
|
-
fileContent,
|
|
77018
|
-
validate,
|
|
77019
|
-
root: true
|
|
77020
|
-
});
|
|
77021
|
-
}
|
|
77022
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
77023
|
-
const { root } = this.getSettablePaths({ global });
|
|
77024
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
77025
|
-
const isRoot = frontmatter.root ?? false;
|
|
77026
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
77027
|
-
if (!global && !isRoot && subprojectPath) return new VibeRule({
|
|
77028
|
-
outputRoot,
|
|
77029
|
-
relativeDirPath: (0, node_path.join)(subprojectPath),
|
|
77030
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
77031
|
-
fileContent: rulesyncRule.getBody(),
|
|
77032
|
-
validate,
|
|
77033
|
-
root: false
|
|
77034
|
-
});
|
|
77035
|
-
return new VibeRule({
|
|
77036
|
-
outputRoot,
|
|
77037
|
-
relativeDirPath: root.relativeDirPath,
|
|
77038
|
-
relativeFilePath: root.relativeFilePath,
|
|
77039
|
-
fileContent: rulesyncRule.getBody(),
|
|
77040
|
-
validate,
|
|
77041
|
-
root: isRoot
|
|
77042
|
-
});
|
|
77043
|
-
}
|
|
77044
|
-
toRulesyncRule() {
|
|
77045
|
-
const subprojectPath = this.getSubprojectPath();
|
|
77046
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
77047
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
77048
|
-
}
|
|
77049
|
-
validate() {
|
|
78319
|
+
/**
|
|
78320
|
+
* Mistral Vibe rules.
|
|
78321
|
+
*
|
|
78322
|
+
* Vibe's harness manager walks the directories between the workspace root and
|
|
78323
|
+
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
78324
|
+
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
78325
|
+
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
78326
|
+
* the root file's overflow. The personal file is `~/.vibe/AGENTS.md`.
|
|
78327
|
+
*
|
|
78328
|
+
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
78329
|
+
*/
|
|
78330
|
+
var VibeRule = class extends NestedAgentsmdRule {
|
|
78331
|
+
static getFamily() {
|
|
77050
78332
|
return {
|
|
77051
|
-
|
|
77052
|
-
error: null
|
|
77053
|
-
};
|
|
77054
|
-
}
|
|
77055
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
77056
|
-
return new VibeRule({
|
|
77057
|
-
outputRoot,
|
|
77058
|
-
relativeDirPath,
|
|
77059
|
-
relativeFilePath,
|
|
77060
|
-
fileContent: "",
|
|
77061
|
-
validate: false,
|
|
77062
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
|
|
77063
|
-
});
|
|
77064
|
-
}
|
|
77065
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
77066
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
77067
|
-
rulesyncRule,
|
|
78333
|
+
globalDir: VIBE_GLOBAL_DIR,
|
|
77068
78334
|
toolTarget: "vibe"
|
|
77069
|
-
}
|
|
78335
|
+
};
|
|
77070
78336
|
}
|
|
77071
78337
|
};
|
|
77072
78338
|
//#endregion
|
|
@@ -79273,6 +80539,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
|
|
|
79273
80539
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
79274
80540
|
});
|
|
79275
80541
|
const configPath = (0, node_path.join)(configRoot, relativeConfigPath);
|
|
80542
|
+
if (await refusesWriteOutsideRoot({
|
|
80543
|
+
logger,
|
|
80544
|
+
rootPath: configRoot,
|
|
80545
|
+
targetPath: configPath
|
|
80546
|
+
})) return {
|
|
80547
|
+
count: 0,
|
|
80548
|
+
paths: [],
|
|
80549
|
+
hasDiff: false,
|
|
80550
|
+
sourceLoadFailed: false
|
|
80551
|
+
};
|
|
79276
80552
|
const expectedConfig = mergeEnabledPlugins({
|
|
79277
80553
|
existingContent: await readFileContentOrNull(configPath) ?? "",
|
|
79278
80554
|
filePath: configPath,
|
|
@@ -79740,7 +81016,8 @@ async function processEmptyFeatureGeneration(params) {
|
|
|
79740
81016
|
*/
|
|
79741
81017
|
async function processFeatureWithRulesyncFiles(params) {
|
|
79742
81018
|
const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
|
|
79743
|
-
|
|
81019
|
+
const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
|
|
81020
|
+
if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
|
|
79744
81021
|
config,
|
|
79745
81022
|
processor,
|
|
79746
81023
|
sweepPlan,
|