rulesync 16.37.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-BnF2J75x.js → import-B2ANxi1V.js} +2032 -397
- package/dist/import-B2ANxi1V.js.map +1 -0
- package/dist/{import-xo5me5dm.cjs → import-DS3OrTAl.cjs} +2030 -395
- 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-BnF2J75x.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";
|
|
@@ -616,6 +616,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
616
616
|
"junie",
|
|
617
617
|
"musecode",
|
|
618
618
|
"opencode",
|
|
619
|
+
"pool",
|
|
619
620
|
"qwencode",
|
|
620
621
|
"reasonix",
|
|
621
622
|
"roo",
|
|
@@ -695,6 +696,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
695
696
|
"kiro-cli",
|
|
696
697
|
"kiro-ide",
|
|
697
698
|
"opencode",
|
|
699
|
+
"pool",
|
|
698
700
|
"qwencode",
|
|
699
701
|
"reasonix",
|
|
700
702
|
"roo",
|
|
@@ -776,6 +778,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
776
778
|
"crush",
|
|
777
779
|
"opencode",
|
|
778
780
|
"pi",
|
|
781
|
+
"pool",
|
|
779
782
|
"factorydroid",
|
|
780
783
|
"goose",
|
|
781
784
|
"hermesagent",
|
|
@@ -823,6 +826,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
823
826
|
"kiro-ide",
|
|
824
827
|
"opencode",
|
|
825
828
|
"pi",
|
|
829
|
+
"pool",
|
|
826
830
|
"qwencode",
|
|
827
831
|
"reasonix",
|
|
828
832
|
"roo",
|
|
@@ -1379,6 +1383,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
|
|
|
1379
1383
|
}));
|
|
1380
1384
|
}
|
|
1381
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
|
+
/**
|
|
1382
1491
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1383
1492
|
*
|
|
1384
1493
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -3329,6 +3438,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3329
3438
|
".grok/config.toml",
|
|
3330
3439
|
".rovodev/config.yml",
|
|
3331
3440
|
".rovodev/mcp.json",
|
|
3441
|
+
".poolside/settings.yaml",
|
|
3442
|
+
".config/poolside/settings.yaml",
|
|
3332
3443
|
".tabnine/agent/settings.json",
|
|
3333
3444
|
".vibe/config.toml",
|
|
3334
3445
|
".zcode/config.json",
|
|
@@ -4378,7 +4489,7 @@ const PI_HOOK_EVENTS = [
|
|
|
4378
4489
|
* adapter. Amp's `agent.start` / `agent.end` events describe the main agent
|
|
4379
4490
|
* turn, not a subagent lifecycle, so they map to `beforeSubmitPrompt` / `stop`.
|
|
4380
4491
|
*
|
|
4381
|
-
* @see https://ampcode.com/
|
|
4492
|
+
* @see https://ampcode.com/docs/plugin-api
|
|
4382
4493
|
*/
|
|
4383
4494
|
const AMP_HOOK_EVENTS = [
|
|
4384
4495
|
"sessionStart",
|
|
@@ -4676,6 +4787,34 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4676
4787
|
"stop"
|
|
4677
4788
|
];
|
|
4678
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
|
+
/**
|
|
4679
4818
|
* Hook events supported by Crush.
|
|
4680
4819
|
*
|
|
4681
4820
|
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
@@ -5270,6 +5409,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
5270
5409
|
"kimi-code": zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5271
5410
|
zcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5272
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) })),
|
|
5273
5413
|
bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5274
5414
|
cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
5275
5415
|
commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
@@ -6223,6 +6363,7 @@ const McpServerSchema = zod_mini.z.looseObject({
|
|
|
6223
6363
|
crushOauthClientSecret: zod_mini.z.optional(zod_mini.z.string()),
|
|
6224
6364
|
crushOauthCallbackPort: zod_mini.z.optional(zod_mini.z.int()),
|
|
6225
6365
|
crushSessionless: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
6366
|
+
poolAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6226
6367
|
headers: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.string())),
|
|
6227
6368
|
/**
|
|
6228
6369
|
* The canonical per-server tool allowlist.
|
|
@@ -6654,6 +6795,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6654
6795
|
"crushOauthClientSecret",
|
|
6655
6796
|
"crushOauthCallbackPort",
|
|
6656
6797
|
"crushSessionless",
|
|
6798
|
+
"poolAllow",
|
|
6657
6799
|
"oauth_client_id",
|
|
6658
6800
|
"oauth_client_secret",
|
|
6659
6801
|
"oauth_callback_port",
|
|
@@ -7996,6 +8138,7 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
7996
8138
|
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7997
8139
|
crush: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7998
8140
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8141
|
+
pool: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7999
8142
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
8000
8143
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
8001
8144
|
roo: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -8341,7 +8484,7 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
|
|
|
8341
8484
|
* them; treating a segment that carries one as static could name a directory
|
|
8342
8485
|
* that no matched file actually lives under.
|
|
8343
8486
|
*/
|
|
8344
|
-
const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
|
|
8487
|
+
const GLOB_METACHARACTERS$1 = /[*?[\]{}()!]/;
|
|
8345
8488
|
/**
|
|
8346
8489
|
* The directory every file matched by `glob` lives under, or `undefined` when
|
|
8347
8490
|
* the pattern pins down no such directory.
|
|
@@ -8374,7 +8517,7 @@ function getGlobStaticPrefix(glob) {
|
|
|
8374
8517
|
const lastIndex = segments.length - 1;
|
|
8375
8518
|
const prefix = [];
|
|
8376
8519
|
for (const [index, segment] of segments.entries()) {
|
|
8377
|
-
if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
|
|
8520
|
+
if (segment === "" || GLOB_METACHARACTERS$1.test(segment)) break;
|
|
8378
8521
|
if (segment === ".") continue;
|
|
8379
8522
|
if (index === lastIndex) break;
|
|
8380
8523
|
prefix.push(segment);
|
|
@@ -9838,6 +9981,22 @@ allowedMcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) })
|
|
|
9838
9981
|
injectAgentsMd: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9839
9982
|
mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
9840
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
|
+
})),
|
|
9841
10000
|
vibe: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
9842
10001
|
agent_type: zod_mini.z.optional(zod_mini.z.enum(["agent", "subagent"])),
|
|
9843
10002
|
display_name: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -10425,6 +10584,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10425
10584
|
this.logger = logger;
|
|
10426
10585
|
}
|
|
10427
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
|
+
/**
|
|
10428
10598
|
* Return tool targets that this feature supports.
|
|
10429
10599
|
*/
|
|
10430
10600
|
static getToolTargets(_params = {}) {
|
|
@@ -10439,6 +10609,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10439
10609
|
const changedPaths = [];
|
|
10440
10610
|
for (const aiFile of aiFiles) {
|
|
10441
10611
|
const filePath = aiFile.getFilePath();
|
|
10612
|
+
if (await refusesWriteOutsideRoot({
|
|
10613
|
+
logger: this.logger,
|
|
10614
|
+
rootPath: aiFile.getOutputRoot(),
|
|
10615
|
+
targetPath: filePath
|
|
10616
|
+
})) continue;
|
|
10442
10617
|
const existingFileContent = await readFileContentOrNull(filePath);
|
|
10443
10618
|
if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
|
|
10444
10619
|
const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
|
|
@@ -10571,6 +10746,37 @@ function mergeByIdentity({ perRoot, identity }) {
|
|
|
10571
10746
|
return order.map((key) => winnerByKey.get(key));
|
|
10572
10747
|
}
|
|
10573
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
|
+
/**
|
|
10574
10780
|
* The key two spellings share when a case-insensitive filesystem would give
|
|
10575
10781
|
* them one file. `toLowerCase()` is locale-independent (unlike
|
|
10576
10782
|
* `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
|
|
@@ -12949,6 +13155,8 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
|
|
|
12949
13155
|
const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
|
|
12950
13156
|
const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
|
|
12951
13157
|
const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
|
|
13158
|
+
const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
|
|
13159
|
+
const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
|
|
12952
13160
|
/**
|
|
12953
13161
|
* Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
|
|
12954
13162
|
* Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
|
|
@@ -13030,6 +13238,38 @@ const KIMI_CODE_CONFIG_DECLARATION = {
|
|
|
13030
13238
|
}
|
|
13031
13239
|
};
|
|
13032
13240
|
/**
|
|
13241
|
+
* Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
|
|
13242
|
+
* the user's primary Pool settings file, so an unparseable root fails closed
|
|
13243
|
+
* rather than being replaced with generated output. `mcp_servers` is owned as
|
|
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.
|
|
13252
|
+
*/
|
|
13253
|
+
const POOL_SETTINGS_DECLARATION = {
|
|
13254
|
+
format: "yaml",
|
|
13255
|
+
invalidRootPolicy: "error",
|
|
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
|
+
}
|
|
13271
|
+
};
|
|
13272
|
+
/**
|
|
13033
13273
|
* ZCode's settings file, which also carries model/theme/permission keys
|
|
13034
13274
|
* rulesync does not own. Both copies are the user's primary ZCode config, so
|
|
13035
13275
|
* every writer refuses to read-modify-write a file it could not parse rather
|
|
@@ -13134,6 +13374,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13134
13374
|
[HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13135
13375
|
[HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13136
13376
|
[HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13377
|
+
[POOL_PROJECT_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
|
|
13378
|
+
[POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
|
|
13137
13379
|
[TAKT_CONFIG_SHARED_FILE_KEY]: {
|
|
13138
13380
|
format: "yaml",
|
|
13139
13381
|
invalidRootPolicy: "error",
|
|
@@ -20420,6 +20662,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20420
20662
|
global: this.global,
|
|
20421
20663
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
20422
20664
|
}));
|
|
20665
|
+
if (await refusesWriteOutsideRoot({
|
|
20666
|
+
logger: this.logger,
|
|
20667
|
+
rootPath: this.outputRoot,
|
|
20668
|
+
targetPath: configPath
|
|
20669
|
+
})) return changedCount;
|
|
20423
20670
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20424
20671
|
if (currentContent === null) return changedCount;
|
|
20425
20672
|
const nextContent = getDisabledHermesCommandsPluginConfigContent({
|
|
@@ -20442,6 +20689,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20442
20689
|
if (this.toolTarget !== "goose" || !this.global) return 0;
|
|
20443
20690
|
if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
|
|
20444
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;
|
|
20445
20697
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20446
20698
|
if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
|
|
20447
20699
|
const nextContent = getGooseSlashCommandsConfigContent({
|
|
@@ -27541,6 +27793,302 @@ var PiHooks = class PiHooks extends ToolHooks {
|
|
|
27541
27793
|
}
|
|
27542
27794
|
};
|
|
27543
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
|
|
27544
28092
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
27545
28093
|
/**
|
|
27546
28094
|
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
@@ -28654,14 +29202,15 @@ const HooksProcessorToolTargetSchema = zod_mini.z.enum(hooksProcessorToolTargetT
|
|
|
28654
29202
|
/**
|
|
28655
29203
|
* Event names present in the config that the target's adapter cannot emit.
|
|
28656
29204
|
*
|
|
28657
|
-
*
|
|
28658
|
-
*
|
|
28659
|
-
*
|
|
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.
|
|
28660
29209
|
*/
|
|
28661
29210
|
function unsupportedEventNames(params) {
|
|
28662
29211
|
const { factory, sharedHooks, effectiveHooks } = params;
|
|
28663
29212
|
const supportedEvents = new Set(factory.supportedEvents);
|
|
28664
|
-
const eventNames = factory.
|
|
29213
|
+
const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
|
|
28665
29214
|
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
28666
29215
|
}
|
|
28667
29216
|
/**
|
|
@@ -28754,7 +29303,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28754
29303
|
supportedEvents: AMP_HOOK_EVENTS,
|
|
28755
29304
|
supportedHookTypes: ["command"],
|
|
28756
29305
|
supportsMatcher: true,
|
|
28757
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29306
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29307
|
+
dropsUnknownOverrideEvents: true
|
|
28758
29308
|
}],
|
|
28759
29309
|
["antigravity-cli", {
|
|
28760
29310
|
class: AntigravityCliHooks,
|
|
@@ -28787,7 +29337,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28787
29337
|
},
|
|
28788
29338
|
supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
|
|
28789
29339
|
supportedHookTypes: ["command"],
|
|
28790
|
-
supportsMatcher: true
|
|
29340
|
+
supportsMatcher: true,
|
|
29341
|
+
dropsUnknownOverrideEvents: true
|
|
28791
29342
|
}],
|
|
28792
29343
|
["cursor", {
|
|
28793
29344
|
class: CursorHooks,
|
|
@@ -28832,7 +29383,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28832
29383
|
"mcp_tool",
|
|
28833
29384
|
"agent"
|
|
28834
29385
|
],
|
|
28835
|
-
supportsMatcher: true
|
|
29386
|
+
supportsMatcher: true,
|
|
29387
|
+
dropsUnknownOverrideEvents: true
|
|
28836
29388
|
}],
|
|
28837
29389
|
["codexcli", {
|
|
28838
29390
|
class: CodexcliHooks,
|
|
@@ -28893,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28893
29445
|
supportedEvents: KILO_HOOK_EVENTS,
|
|
28894
29446
|
supportedHookTypes: ["command"],
|
|
28895
29447
|
supportsMatcher: true,
|
|
28896
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29448
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29449
|
+
dropsUnknownOverrideEvents: true
|
|
28897
29450
|
}],
|
|
28898
29451
|
["opencode", {
|
|
28899
29452
|
class: OpencodeHooks,
|
|
@@ -28905,7 +29458,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28905
29458
|
supportedEvents: OPENCODE_HOOK_EVENTS,
|
|
28906
29459
|
supportedHookTypes: ["command"],
|
|
28907
29460
|
supportsMatcher: true,
|
|
28908
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29461
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29462
|
+
dropsUnknownOverrideEvents: true
|
|
28909
29463
|
}],
|
|
28910
29464
|
["pi", {
|
|
28911
29465
|
class: PiHooks,
|
|
@@ -28916,7 +29470,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28916
29470
|
},
|
|
28917
29471
|
supportedEvents: PI_HOOK_EVENTS,
|
|
28918
29472
|
supportedHookTypes: ["command"],
|
|
28919
|
-
supportsMatcher: true
|
|
29473
|
+
supportsMatcher: true,
|
|
29474
|
+
dropsUnknownOverrideEvents: true
|
|
28920
29475
|
}],
|
|
28921
29476
|
["factorydroid", {
|
|
28922
29477
|
class: FactorydroidHooks,
|
|
@@ -28938,7 +29493,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28938
29493
|
},
|
|
28939
29494
|
supportedEvents: CLINE_HOOK_EVENTS,
|
|
28940
29495
|
supportedHookTypes: ["command"],
|
|
28941
|
-
supportsMatcher: false
|
|
29496
|
+
supportsMatcher: false,
|
|
29497
|
+
dropsUnknownOverrideEvents: true
|
|
28942
29498
|
}],
|
|
28943
29499
|
["goose", {
|
|
28944
29500
|
class: GooseHooks,
|
|
@@ -28960,8 +29516,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28960
29516
|
},
|
|
28961
29517
|
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
28962
29518
|
supportedHookTypes: ["command"],
|
|
28963
|
-
supportsMatcher: true
|
|
28964
|
-
passthroughOverrideEvents: true
|
|
29519
|
+
supportsMatcher: true
|
|
28965
29520
|
}],
|
|
28966
29521
|
["kimi-code", {
|
|
28967
29522
|
class: KimiCodeHooks,
|
|
@@ -28972,8 +29527,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28972
29527
|
},
|
|
28973
29528
|
supportedEvents: KIMI_CODE_HOOK_EVENTS,
|
|
28974
29529
|
supportedHookTypes: ["command"],
|
|
28975
|
-
supportsMatcher: true
|
|
28976
|
-
passthroughOverrideEvents: true
|
|
29530
|
+
supportsMatcher: true
|
|
28977
29531
|
}],
|
|
28978
29532
|
["deepagents", {
|
|
28979
29533
|
class: DeepagentsHooks,
|
|
@@ -28984,7 +29538,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28984
29538
|
},
|
|
28985
29539
|
supportedEvents: DEEPAGENTS_HOOK_EVENTS,
|
|
28986
29540
|
supportedHookTypes: ["command"],
|
|
28987
|
-
supportsMatcher: true
|
|
29541
|
+
supportsMatcher: true,
|
|
29542
|
+
dropsUnknownOverrideEvents: true
|
|
28988
29543
|
}],
|
|
28989
29544
|
["kiro", {
|
|
28990
29545
|
class: KiroHooks,
|
|
@@ -28995,8 +29550,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28995
29550
|
},
|
|
28996
29551
|
supportedEvents: KIRO_HOOK_EVENTS,
|
|
28997
29552
|
supportedHookTypes: ["command"],
|
|
28998
|
-
supportsMatcher: true
|
|
28999
|
-
passthroughOverrideEvents: true
|
|
29553
|
+
supportsMatcher: true
|
|
29000
29554
|
}],
|
|
29001
29555
|
["kiro-cli", {
|
|
29002
29556
|
class: KiroCliHooks,
|
|
@@ -29007,8 +29561,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29007
29561
|
},
|
|
29008
29562
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
29009
29563
|
supportedHookTypes: ["command", "prompt"],
|
|
29010
|
-
supportsMatcher: true
|
|
29011
|
-
passthroughOverrideEvents: true
|
|
29564
|
+
supportsMatcher: true
|
|
29012
29565
|
}],
|
|
29013
29566
|
["kiro-ide", {
|
|
29014
29567
|
class: KiroIdeHooks,
|
|
@@ -29019,8 +29572,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29019
29572
|
},
|
|
29020
29573
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
29021
29574
|
supportedHookTypes: ["command", "prompt"],
|
|
29022
|
-
supportsMatcher: true
|
|
29023
|
-
passthroughOverrideEvents: true
|
|
29575
|
+
supportsMatcher: true
|
|
29024
29576
|
}],
|
|
29025
29577
|
["devin", {
|
|
29026
29578
|
class: DevinHooks,
|
|
@@ -29138,6 +29690,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29138
29690
|
},
|
|
29139
29691
|
supportedEvents: VIBE_HOOK_EVENTS,
|
|
29140
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"],
|
|
29141
29705
|
supportsMatcher: true
|
|
29142
29706
|
}],
|
|
29143
29707
|
["qwencode", {
|
|
@@ -29165,7 +29729,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29165
29729
|
},
|
|
29166
29730
|
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
29167
29731
|
supportedHookTypes: ["command"],
|
|
29168
|
-
supportsMatcher: true
|
|
29732
|
+
supportsMatcher: true,
|
|
29733
|
+
dropsUnknownOverrideEvents: true
|
|
29169
29734
|
}],
|
|
29170
29735
|
["grokcli", {
|
|
29171
29736
|
class: GrokcliHooks,
|
|
@@ -29187,8 +29752,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29187
29752
|
},
|
|
29188
29753
|
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
29189
29754
|
supportedHookTypes: ["command"],
|
|
29190
|
-
supportsMatcher: true
|
|
29191
|
-
passthroughOverrideEvents: true
|
|
29755
|
+
supportsMatcher: true
|
|
29192
29756
|
}],
|
|
29193
29757
|
["zcode", {
|
|
29194
29758
|
class: ZcodeHooks,
|
|
@@ -37119,6 +37683,320 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37119
37683
|
}
|
|
37120
37684
|
};
|
|
37121
37685
|
//#endregion
|
|
37686
|
+
//#region src/features/mcp/pool-mcp.ts
|
|
37687
|
+
/**
|
|
37688
|
+
* Single spelling of the settings.yaml codec/policy, matching the
|
|
37689
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
37690
|
+
* unparseable root rather than replacing the user's primary Pool settings with
|
|
37691
|
+
* generated output.
|
|
37692
|
+
*/
|
|
37693
|
+
function parsePoolSettings$2(fileContent, filePath) {
|
|
37694
|
+
return parseSharedConfig({
|
|
37695
|
+
format: "yaml",
|
|
37696
|
+
fileContent,
|
|
37697
|
+
filePath,
|
|
37698
|
+
invalidRootPolicy: "error"
|
|
37699
|
+
});
|
|
37700
|
+
}
|
|
37701
|
+
/**
|
|
37702
|
+
* The remote transports Pool's `transport.type` accepts. `streamable-http` is
|
|
37703
|
+
* the canonical rulesync name for what Pool calls `http`; everything else is
|
|
37704
|
+
* left to the caller to reject.
|
|
37705
|
+
*
|
|
37706
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37707
|
+
*/
|
|
37708
|
+
function asPoolRemoteType(stated, url) {
|
|
37709
|
+
if (stated === "sse") return "sse";
|
|
37710
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
37711
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
37712
|
+
}
|
|
37713
|
+
/**
|
|
37714
|
+
* Pool spells remote headers as a list of `"Name: value"` strings rather than
|
|
37715
|
+
* a map, so the canonical record is flattened at generate time and split back
|
|
37716
|
+
* at the first `:` on import. A list entry without a `:` names no header and
|
|
37717
|
+
* is dropped with a warning — silently losing what may be a mistyped auth
|
|
37718
|
+
* header would leave the canonical file short of one without a trace.
|
|
37719
|
+
*/
|
|
37720
|
+
function headersRecordToPoolList(headers) {
|
|
37721
|
+
return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
37722
|
+
}
|
|
37723
|
+
function poolHeadersListToRecord(serverName, headers) {
|
|
37724
|
+
if (!isStringArray$2(headers)) return void 0;
|
|
37725
|
+
const result = {};
|
|
37726
|
+
for (const entry of headers) {
|
|
37727
|
+
const separator = entry.indexOf(":");
|
|
37728
|
+
const name = separator > 0 ? entry.slice(0, separator).trim() : "";
|
|
37729
|
+
if (name === "" || PROTOTYPE_POLLUTION_KEYS.has(name)) {
|
|
37730
|
+
warnWithFallback(void 0, `Ignored malformed header ${quoteValueForWarning(entry)} in Pool MCP server ${quoteValueForWarning(serverName)}: expected "Name: value"`);
|
|
37731
|
+
continue;
|
|
37732
|
+
}
|
|
37733
|
+
result[name] = entry.slice(separator + 1).trim();
|
|
37734
|
+
}
|
|
37735
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
37736
|
+
}
|
|
37737
|
+
/**
|
|
37738
|
+
* Pool's `deny`/`enabled_tools` entries are glob patterns, while the canonical
|
|
37739
|
+
* `enabledTools`/`disabledTools` lists are literal tool names that the other
|
|
37740
|
+
* targets match verbatim. A pattern therefore imports as a name that matches
|
|
37741
|
+
* nothing elsewhere — a `deny` that quietly stops denying is the case worth
|
|
37742
|
+
* calling out — so it is copied as-is but warned about. `allow` is exempt: it
|
|
37743
|
+
* imports to the Pool-only `poolAllow`, where globs are exactly what Pool
|
|
37744
|
+
* expects and no other tool ever reads them.
|
|
37745
|
+
*/
|
|
37746
|
+
const GLOB_METACHARACTERS = /[*?[]/;
|
|
37747
|
+
/** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
|
|
37748
|
+
const MAX_LISTED_GLOB_PATTERNS = 10;
|
|
37749
|
+
function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
|
|
37750
|
+
if (!isStringArray$2(value)) {
|
|
37751
|
+
warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
|
|
37752
|
+
return;
|
|
37753
|
+
}
|
|
37754
|
+
if (!warnOnGlobs) return value;
|
|
37755
|
+
const patterns = value.filter((entry) => GLOB_METACHARACTERS.test(entry));
|
|
37756
|
+
if (patterns.length > 0) {
|
|
37757
|
+
const listed = patterns.slice(0, MAX_LISTED_GLOB_PATTERNS).map(quoteValueForWarning);
|
|
37758
|
+
const overflow = patterns.length - listed.length;
|
|
37759
|
+
const shown = overflow > 0 ? `${listed.join(", ")} (+${overflow} more)` : listed.join(", ");
|
|
37760
|
+
warnWithFallback(void 0, `${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)} contains glob patterns (${shown}); other tools match the imported tool names literally`);
|
|
37761
|
+
}
|
|
37762
|
+
return value;
|
|
37763
|
+
}
|
|
37764
|
+
/**
|
|
37765
|
+
* Pool's `transport` block for a remote server, or `null` (after a warning)
|
|
37766
|
+
* when the server has no url or names a transport Pool cannot reach.
|
|
37767
|
+
*/
|
|
37768
|
+
function toPoolTransport(name, config, logger) {
|
|
37769
|
+
const url = resolveRemoteMcpUrl(config);
|
|
37770
|
+
if (!url) return warnAndSkipMcpServer({
|
|
37771
|
+
toolName: "Pool",
|
|
37772
|
+
serverName: name,
|
|
37773
|
+
reason: "a remote transport without a url",
|
|
37774
|
+
logger
|
|
37775
|
+
});
|
|
37776
|
+
const stated = config.type ?? config.transport;
|
|
37777
|
+
const type = asPoolRemoteType(stated, url);
|
|
37778
|
+
if (type === void 0) return warnAndSkipMcpServer({
|
|
37779
|
+
toolName: "Pool",
|
|
37780
|
+
serverName: name,
|
|
37781
|
+
reason: stated === void 0 ? "a WebSocket url, which Pool's remote transports (http and sse) cannot reach" : `the "${stated}" transport, which Pool does not offer for remote servers (only http and sse)`,
|
|
37782
|
+
logger
|
|
37783
|
+
});
|
|
37784
|
+
const transport = {
|
|
37785
|
+
type,
|
|
37786
|
+
url
|
|
37787
|
+
};
|
|
37788
|
+
if (config.headers && Object.keys(config.headers).length > 0) transport.headers = headersRecordToPoolList(config.headers);
|
|
37789
|
+
return transport;
|
|
37790
|
+
}
|
|
37791
|
+
/**
|
|
37792
|
+
* The transport-specific part of a Pool server entry: a `transport` block for
|
|
37793
|
+
* remote servers, `command`/`args`/`cwd` for stdio ones. `null` (after a
|
|
37794
|
+
* warning) when the server cannot be written.
|
|
37795
|
+
*/
|
|
37796
|
+
function toPoolTransportFields(name, config, logger) {
|
|
37797
|
+
if (isRemoteMcpServer(config)) {
|
|
37798
|
+
const transport = toPoolTransport(name, config, logger);
|
|
37799
|
+
return transport === null ? null : { transport };
|
|
37800
|
+
}
|
|
37801
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
37802
|
+
if (!command) return warnAndSkipMcpServer({
|
|
37803
|
+
toolName: "Pool",
|
|
37804
|
+
serverName: name,
|
|
37805
|
+
reason: "a stdio transport without a command",
|
|
37806
|
+
logger
|
|
37807
|
+
});
|
|
37808
|
+
const fields = {
|
|
37809
|
+
command,
|
|
37810
|
+
args
|
|
37811
|
+
};
|
|
37812
|
+
if (config.cwd) fields.cwd = config.cwd;
|
|
37813
|
+
return fields;
|
|
37814
|
+
}
|
|
37815
|
+
/**
|
|
37816
|
+
* The fields shared by stdio and remote entries. Tool filters map onto Pool's
|
|
37817
|
+
* own switches — `enabledTools` becomes `enabled_tools` (the tools Pool
|
|
37818
|
+
* exposes at all) and `disabledTools` becomes `deny` (patterns agents may
|
|
37819
|
+
* never use); the Pool-only approval allowlist is authored as `poolAllow` and
|
|
37820
|
+
* written as `allow`. `disabled: true` is spelled the same on both sides.
|
|
37821
|
+
*/
|
|
37822
|
+
function toPoolCommonFields(config) {
|
|
37823
|
+
const fields = {};
|
|
37824
|
+
if (config.env && Object.keys(config.env).length > 0) fields.env = config.env;
|
|
37825
|
+
if (config.enabledTools && config.enabledTools.length > 0) fields.enabled_tools = config.enabledTools;
|
|
37826
|
+
if (config.poolAllow && config.poolAllow.length > 0) fields.allow = config.poolAllow;
|
|
37827
|
+
if (config.disabledTools && config.disabledTools.length > 0) fields.deny = config.disabledTools;
|
|
37828
|
+
if (config.disabled === true) fields.disabled = true;
|
|
37829
|
+
return fields;
|
|
37830
|
+
}
|
|
37831
|
+
/**
|
|
37832
|
+
* Convert canonical rulesync servers to Pool's native `mcp_servers` shape:
|
|
37833
|
+
* stdio servers carry `command`/`args`/`cwd`/`env`, remote servers carry a
|
|
37834
|
+
* `transport` block (`type` of `http` or `sse`, `url`, optional `headers`
|
|
37835
|
+
* list) plus optional `env`; see `toPoolCommonFields` for the tool filters.
|
|
37836
|
+
*
|
|
37837
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37838
|
+
*/
|
|
37839
|
+
function convertToPoolFormat(mcpServers, logger) {
|
|
37840
|
+
const result = {};
|
|
37841
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
37842
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
37843
|
+
if (!isRecord$1(config)) continue;
|
|
37844
|
+
if (declaresNoTransport(config)) {
|
|
37845
|
+
warnAndSkipMcpServer({
|
|
37846
|
+
toolName: "Pool",
|
|
37847
|
+
serverName: name,
|
|
37848
|
+
reason: "no transport",
|
|
37849
|
+
logger
|
|
37850
|
+
});
|
|
37851
|
+
continue;
|
|
37852
|
+
}
|
|
37853
|
+
const transportFields = toPoolTransportFields(name, config, logger);
|
|
37854
|
+
if (transportFields === null) continue;
|
|
37855
|
+
result[name] = {
|
|
37856
|
+
...transportFields,
|
|
37857
|
+
...toPoolCommonFields(config)
|
|
37858
|
+
};
|
|
37859
|
+
}
|
|
37860
|
+
return result;
|
|
37861
|
+
}
|
|
37862
|
+
const POOL_TO_CANONICAL_TOOL_LIST_KEYS = {
|
|
37863
|
+
enabled_tools: "enabledTools",
|
|
37864
|
+
deny: "disabledTools",
|
|
37865
|
+
allow: "poolAllow"
|
|
37866
|
+
};
|
|
37867
|
+
/**
|
|
37868
|
+
* Convert Pool's native `mcp_servers` shape back to canonical rulesync
|
|
37869
|
+
* servers: the `transport` block is flattened to `type`/`url`/`headers`,
|
|
37870
|
+
* `enabled_tools` maps back to `enabledTools`, `deny` to `disabledTools` and
|
|
37871
|
+
* `allow` to the namespaced `poolAllow` (see `McpServerSchema`). Unknown keys
|
|
37872
|
+
* pass through untouched so an import keeps whatever a hand-authored entry
|
|
37873
|
+
* declared; generation back out is a whitelist, so only the keys
|
|
37874
|
+
* `convertToPoolFormat` writes are re-emitted.
|
|
37875
|
+
*/
|
|
37876
|
+
function convertFromPoolFormat(poolServers) {
|
|
37877
|
+
const result = {};
|
|
37878
|
+
for (const [name, config] of Object.entries(poolServers)) {
|
|
37879
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
37880
|
+
const converted = {};
|
|
37881
|
+
for (const [key, value] of Object.entries(config)) {
|
|
37882
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
37883
|
+
if (key === "transport") {
|
|
37884
|
+
if (!isRecord$1(value)) continue;
|
|
37885
|
+
if (typeof value.type === "string") converted.type = value.type;
|
|
37886
|
+
if (typeof value.url === "string") converted.url = value.url;
|
|
37887
|
+
const headers = poolHeadersListToRecord(name, value.headers);
|
|
37888
|
+
if (headers) converted.headers = headers;
|
|
37889
|
+
continue;
|
|
37890
|
+
}
|
|
37891
|
+
const canonicalKey = Object.hasOwn(POOL_TO_CANONICAL_TOOL_LIST_KEYS, key) ? POOL_TO_CANONICAL_TOOL_LIST_KEYS[key] : void 0;
|
|
37892
|
+
if (canonicalKey) {
|
|
37893
|
+
const list = importPoolToolList({
|
|
37894
|
+
serverName: name,
|
|
37895
|
+
poolKey: key,
|
|
37896
|
+
value,
|
|
37897
|
+
warnOnGlobs: canonicalKey !== "poolAllow"
|
|
37898
|
+
});
|
|
37899
|
+
if (list) converted[canonicalKey] = list;
|
|
37900
|
+
continue;
|
|
37901
|
+
}
|
|
37902
|
+
converted[key] = value;
|
|
37903
|
+
}
|
|
37904
|
+
result[name] = converted;
|
|
37905
|
+
}
|
|
37906
|
+
return result;
|
|
37907
|
+
}
|
|
37908
|
+
/**
|
|
37909
|
+
* Pool MCP servers.
|
|
37910
|
+
*
|
|
37911
|
+
* Pool reads MCP servers from the `mcp_servers` block of its settings file:
|
|
37912
|
+
* `<project>/.poolside/settings.yaml` at project scope (shared and committed)
|
|
37913
|
+
* and `~/.config/poolside/settings.yaml` at user scope. The untracked
|
|
37914
|
+
* `.poolside/settings.local.yaml` overlay, which Pool layers on top of both,
|
|
37915
|
+
* is left to the user. Every other top-level settings key is preserved and
|
|
37916
|
+
* the file is never deleted.
|
|
37917
|
+
*
|
|
37918
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37919
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
37920
|
+
*/
|
|
37921
|
+
var PoolMcp = class PoolMcp extends ToolMcp {
|
|
37922
|
+
settings;
|
|
37923
|
+
constructor(params) {
|
|
37924
|
+
super(params);
|
|
37925
|
+
this.settings = parsePoolSettings$2(this.fileContent ?? "", (0, node_path.join)(this.relativeDirPath, this.relativeFilePath));
|
|
37926
|
+
}
|
|
37927
|
+
getSettings() {
|
|
37928
|
+
return this.settings;
|
|
37929
|
+
}
|
|
37930
|
+
isDeletable() {
|
|
37931
|
+
return false;
|
|
37932
|
+
}
|
|
37933
|
+
static getSettablePaths({ global = false } = {}) {
|
|
37934
|
+
return {
|
|
37935
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
37936
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
37937
|
+
};
|
|
37938
|
+
}
|
|
37939
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
37940
|
+
const paths = this.getSettablePaths({ global });
|
|
37941
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
37942
|
+
return new PoolMcp({
|
|
37943
|
+
outputRoot,
|
|
37944
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37945
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37946
|
+
fileContent,
|
|
37947
|
+
validate,
|
|
37948
|
+
global
|
|
37949
|
+
});
|
|
37950
|
+
}
|
|
37951
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
37952
|
+
const paths = this.getSettablePaths({ global });
|
|
37953
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
37954
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
37955
|
+
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
37956
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
37957
|
+
const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
|
|
37958
|
+
return [serverName, {
|
|
37959
|
+
...serverConfig,
|
|
37960
|
+
...isStringArray$2(poolAllow) && { poolAllow }
|
|
37961
|
+
}];
|
|
37962
|
+
})), logger);
|
|
37963
|
+
return new PoolMcp({
|
|
37964
|
+
outputRoot,
|
|
37965
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37966
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37967
|
+
fileContent: applySharedConfigPatch({
|
|
37968
|
+
fileKey: sharedConfigFileKey(paths),
|
|
37969
|
+
feature: "mcp",
|
|
37970
|
+
existingContent,
|
|
37971
|
+
patch: { [POOL_MCP_SERVERS_KEY]: converted },
|
|
37972
|
+
filePath
|
|
37973
|
+
}),
|
|
37974
|
+
validate,
|
|
37975
|
+
global
|
|
37976
|
+
});
|
|
37977
|
+
}
|
|
37978
|
+
toRulesyncMcp() {
|
|
37979
|
+
const converted = convertFromPoolFormat(isRecord$1(this.settings["mcp_servers"]) ? this.settings[POOL_MCP_SERVERS_KEY] : {});
|
|
37980
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
37981
|
+
}
|
|
37982
|
+
validate() {
|
|
37983
|
+
return {
|
|
37984
|
+
success: true,
|
|
37985
|
+
error: null
|
|
37986
|
+
};
|
|
37987
|
+
}
|
|
37988
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
37989
|
+
return new PoolMcp({
|
|
37990
|
+
outputRoot,
|
|
37991
|
+
relativeDirPath,
|
|
37992
|
+
relativeFilePath,
|
|
37993
|
+
fileContent: "",
|
|
37994
|
+
validate: false,
|
|
37995
|
+
global
|
|
37996
|
+
});
|
|
37997
|
+
}
|
|
37998
|
+
};
|
|
37999
|
+
//#endregion
|
|
37122
38000
|
//#region src/features/mcp/qwencode-mcp.ts
|
|
37123
38001
|
/**
|
|
37124
38002
|
* Map rulesync's canonical per-server tool filters to Qwen Code's field names.
|
|
@@ -39566,6 +40444,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
39566
40444
|
supportsDisabledTools: true
|
|
39567
40445
|
}
|
|
39568
40446
|
}],
|
|
40447
|
+
["pool", {
|
|
40448
|
+
class: PoolMcp,
|
|
40449
|
+
meta: {
|
|
40450
|
+
supportsProject: true,
|
|
40451
|
+
supportsGlobal: true,
|
|
40452
|
+
supportsEnabledTools: true,
|
|
40453
|
+
supportsDisabledTools: true
|
|
40454
|
+
}
|
|
40455
|
+
}],
|
|
39569
40456
|
["qwencode", {
|
|
39570
40457
|
class: QwencodeMcp,
|
|
39571
40458
|
meta: {
|
|
@@ -40879,7 +41766,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
40879
41766
|
function toAntigravityCliToolName(canonical) {
|
|
40880
41767
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
40881
41768
|
}
|
|
40882
|
-
function toCanonicalToolName$
|
|
41769
|
+
function toCanonicalToolName$9(cliName) {
|
|
40883
41770
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
40884
41771
|
}
|
|
40885
41772
|
/**
|
|
@@ -41095,7 +41982,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
41095
41982
|
const processEntries = (entries, action) => {
|
|
41096
41983
|
for (const entry of entries) {
|
|
41097
41984
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
41098
|
-
const canonical = toCanonicalToolName$
|
|
41985
|
+
const canonical = toCanonicalToolName$9(toolName);
|
|
41099
41986
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41100
41987
|
permission[canonical][pattern] = action;
|
|
41101
41988
|
}
|
|
@@ -41374,7 +42261,7 @@ function toLegacyAugmentToolName(augmentName) {
|
|
|
41374
42261
|
key: augmentName
|
|
41375
42262
|
}) ?? augmentName;
|
|
41376
42263
|
}
|
|
41377
|
-
function toCanonicalToolName$
|
|
42264
|
+
function toCanonicalToolName$8(augmentName) {
|
|
41378
42265
|
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
41379
42266
|
return lookupOwn({
|
|
41380
42267
|
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
@@ -41827,7 +42714,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
41827
42714
|
const type = entry.permission.type;
|
|
41828
42715
|
if (!isBasicAugmentType(type)) continue;
|
|
41829
42716
|
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
41830
|
-
const canonical = toCanonicalToolName$
|
|
42717
|
+
const canonical = toCanonicalToolName$8(entry.toolName);
|
|
41831
42718
|
if (forbiddenMapKeys.has(canonical)) {
|
|
41832
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.`);
|
|
41833
42720
|
continue;
|
|
@@ -42072,7 +42959,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
42072
42959
|
function toClaudeToolName(canonical) {
|
|
42073
42960
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
42074
42961
|
}
|
|
42075
|
-
function toCanonicalToolName$
|
|
42962
|
+
function toCanonicalToolName$7(claudeName) {
|
|
42076
42963
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
42077
42964
|
}
|
|
42078
42965
|
/**
|
|
@@ -42986,7 +43873,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
42986
43873
|
const processEntries = (entries, action) => {
|
|
42987
43874
|
for (const entry of entries) {
|
|
42988
43875
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
42989
|
-
const canonical = toCanonicalToolName$
|
|
43876
|
+
const canonical = toCanonicalToolName$7(toolName);
|
|
42990
43877
|
if (!permission[canonical]) permission[canonical] = {};
|
|
42991
43878
|
permission[canonical][pattern] = action;
|
|
42992
43879
|
}
|
|
@@ -43847,8 +44734,8 @@ function mapBashActionToDecision(action) {
|
|
|
43847
44734
|
//#region src/features/permissions/commandcode-permissions.ts
|
|
43848
44735
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
43849
44736
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
43850
|
-
const CATCH_ALL_PATTERN$
|
|
43851
|
-
const MCP_CANONICAL_PREFIX$
|
|
44737
|
+
const CATCH_ALL_PATTERN$7 = "*";
|
|
44738
|
+
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
43852
44739
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
43853
44740
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
43854
44741
|
bash: "Shell",
|
|
@@ -43902,10 +44789,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
43902
44789
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
43903
44790
|
*/
|
|
43904
44791
|
function buildCommandcodeRule(category, pattern) {
|
|
43905
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
43906
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
43907
|
-
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$
|
|
43908
|
-
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;
|
|
43909
44796
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
43910
44797
|
if (tool === void 0) return null;
|
|
43911
44798
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -43941,7 +44828,7 @@ function splitCommandcodeRule(rule) {
|
|
|
43941
44828
|
}
|
|
43942
44829
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
43943
44830
|
function isCatchAllSpecifier(inner) {
|
|
43944
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
44831
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$7;
|
|
43945
44832
|
}
|
|
43946
44833
|
/**
|
|
43947
44834
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -43975,34 +44862,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
43975
44862
|
if (server.length === 0) return null;
|
|
43976
44863
|
const everyServer = {
|
|
43977
44864
|
category: "mcp",
|
|
43978
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44865
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
43979
44866
|
notInAllow: true
|
|
43980
44867
|
};
|
|
43981
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
43982
|
-
if (server === CATCH_ALL_PATTERN$
|
|
43983
|
-
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;
|
|
43984
44871
|
return {
|
|
43985
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
43986
|
-
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,
|
|
43987
44874
|
notInAllow: false
|
|
43988
44875
|
};
|
|
43989
44876
|
}
|
|
43990
|
-
const category = `${MCP_CANONICAL_PREFIX$
|
|
43991
|
-
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);
|
|
43992
44879
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
43993
44880
|
category,
|
|
43994
44881
|
pattern: inner,
|
|
43995
44882
|
notInAllow: true
|
|
43996
44883
|
} : null;
|
|
43997
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
43998
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
43999
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
44000
|
-
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,
|
|
44001
44888
|
notInAllow: true
|
|
44002
44889
|
};
|
|
44003
44890
|
return namesTool ? {
|
|
44004
44891
|
category,
|
|
44005
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44892
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44006
44893
|
notInAllow: false
|
|
44007
44894
|
} : null;
|
|
44008
44895
|
}
|
|
@@ -44026,14 +44913,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44026
44913
|
if (category === "bash") {
|
|
44027
44914
|
if (isCatchAllSpecifier(inner)) return {
|
|
44028
44915
|
category,
|
|
44029
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44916
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44030
44917
|
notInAllow: false
|
|
44031
44918
|
};
|
|
44032
44919
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
44033
44920
|
if (normalized === "" || normalized === ":*") return null;
|
|
44034
44921
|
return isCatchAllSpecifier(normalized) ? {
|
|
44035
44922
|
category,
|
|
44036
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44923
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44037
44924
|
notInAllow: true
|
|
44038
44925
|
} : {
|
|
44039
44926
|
category,
|
|
@@ -44043,7 +44930,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44043
44930
|
}
|
|
44044
44931
|
if (isCatchAllSpecifier(inner)) return {
|
|
44045
44932
|
category,
|
|
44046
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44933
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44047
44934
|
notInAllow: false
|
|
44048
44935
|
};
|
|
44049
44936
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -44062,13 +44949,13 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44062
44949
|
*/
|
|
44063
44950
|
function parseCommandcodeRule(rule) {
|
|
44064
44951
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44065
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
44952
|
+
if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
|
|
44066
44953
|
category: "*",
|
|
44067
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44954
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44068
44955
|
notInAllow: true
|
|
44069
44956
|
} : null;
|
|
44070
44957
|
const lowered = tool.toLowerCase();
|
|
44071
|
-
if (lowered.startsWith(MCP_CANONICAL_PREFIX$
|
|
44958
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
|
|
44072
44959
|
tool,
|
|
44073
44960
|
inner
|
|
44074
44961
|
});
|
|
@@ -44086,12 +44973,12 @@ function parseCommandcodeRule(rule) {
|
|
|
44086
44973
|
*/
|
|
44087
44974
|
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
44088
44975
|
const { tool } = splitCommandcodeRule(rule);
|
|
44089
|
-
return category.startsWith(MCP_CANONICAL_PREFIX$
|
|
44976
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
|
|
44090
44977
|
}
|
|
44091
44978
|
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
44092
44979
|
function scopedMcpRuleTool(rule) {
|
|
44093
44980
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44094
|
-
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
44981
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
|
|
44095
44982
|
}
|
|
44096
44983
|
/**
|
|
44097
44984
|
* The string entries of a Command Code permission list. Command Code reads
|
|
@@ -44134,7 +45021,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
44134
45021
|
}).filter((rule) => {
|
|
44135
45022
|
const parsed = parseCommandcodeRule(rule);
|
|
44136
45023
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
44137
|
-
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})`);
|
|
44138
45025
|
if (key !== "allow" && isStricterAction({
|
|
44139
45026
|
action: key,
|
|
44140
45027
|
existing: replacement
|
|
@@ -44191,7 +45078,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44191
45078
|
const ranked = /* @__PURE__ */ new Map();
|
|
44192
45079
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
44193
45080
|
for (const [category, rules] of Object.entries(permission)) {
|
|
44194
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
45081
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
|
|
44195
45082
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
44196
45083
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
44197
45084
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -44260,7 +45147,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44260
45147
|
function skippedAllowImportMessage({ rule, category }) {
|
|
44261
45148
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44262
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.`;
|
|
44263
|
-
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.`;
|
|
44264
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.`;
|
|
44265
45152
|
}
|
|
44266
45153
|
/**
|
|
@@ -44462,7 +45349,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
44462
45349
|
//#endregion
|
|
44463
45350
|
//#region src/features/permissions/continue-permissions.ts
|
|
44464
45351
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
44465
|
-
const CATCH_ALL_PATTERN$
|
|
45352
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
44466
45353
|
/**
|
|
44467
45354
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
44468
45355
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -44470,7 +45357,7 @@ const CATCH_ALL_PATTERN$5 = "*";
|
|
|
44470
45357
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
44471
45358
|
*/
|
|
44472
45359
|
function normalizeCatchAllPattern$1(pattern) {
|
|
44473
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45360
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
|
|
44474
45361
|
}
|
|
44475
45362
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
44476
45363
|
bash: "Bash",
|
|
@@ -44499,7 +45386,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
44499
45386
|
function toContinueToolName(canonical) {
|
|
44500
45387
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
44501
45388
|
}
|
|
44502
|
-
function toCanonicalToolName$
|
|
45389
|
+
function toCanonicalToolName$6(continueName) {
|
|
44503
45390
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
44504
45391
|
}
|
|
44505
45392
|
/**
|
|
@@ -44507,7 +45394,7 @@ function toCanonicalToolName$5(continueName) {
|
|
|
44507
45394
|
* `Tool(pattern)`.
|
|
44508
45395
|
*/
|
|
44509
45396
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
44510
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
45397
|
+
return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
|
|
44511
45398
|
}
|
|
44512
45399
|
/**
|
|
44513
45400
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -44521,7 +45408,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
44521
45408
|
if (toolName === "") return;
|
|
44522
45409
|
return {
|
|
44523
45410
|
toolName,
|
|
44524
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
45411
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
|
|
44525
45412
|
};
|
|
44526
45413
|
}
|
|
44527
45414
|
/**
|
|
@@ -44695,7 +45582,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
44695
45582
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
44696
45583
|
continue;
|
|
44697
45584
|
}
|
|
44698
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
45585
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
|
|
44699
45586
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
44700
45587
|
continue;
|
|
44701
45588
|
}
|
|
@@ -44720,7 +45607,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
44720
45607
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
44721
45608
|
if (parsedEntry === void 0) continue;
|
|
44722
45609
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
44723
|
-
const category = toCanonicalToolName$
|
|
45610
|
+
const category = toCanonicalToolName$6(parsedEntry.toolName);
|
|
44724
45611
|
permission[category] ??= {};
|
|
44725
45612
|
permission[category][parsedEntry.pattern] ??= action;
|
|
44726
45613
|
}
|
|
@@ -45044,14 +45931,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
45044
45931
|
};
|
|
45045
45932
|
//#endregion
|
|
45046
45933
|
//#region src/features/permissions/crush-permissions.ts
|
|
45047
|
-
const CATCH_ALL_PATTERN$
|
|
45934
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
45048
45935
|
/**
|
|
45049
45936
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
45050
45937
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
45051
45938
|
* still be written.
|
|
45052
45939
|
*/
|
|
45053
45940
|
function normalizeCatchAllPattern(pattern) {
|
|
45054
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45941
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
45055
45942
|
}
|
|
45056
45943
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
45057
45944
|
bash: "bash",
|
|
@@ -45063,20 +45950,20 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
|
45063
45950
|
webfetch: "fetch"
|
|
45064
45951
|
};
|
|
45065
45952
|
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
45066
|
-
const MCP_CANONICAL_PREFIX$
|
|
45953
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
45067
45954
|
const MCP_CRUSH_PREFIX = "mcp_";
|
|
45068
45955
|
function toCrushToolName(canonical) {
|
|
45069
45956
|
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
45070
|
-
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("__", "_")}`;
|
|
45071
45958
|
return canonical;
|
|
45072
45959
|
}
|
|
45073
|
-
function toCanonicalToolName$
|
|
45960
|
+
function toCanonicalToolName$5(crushName) {
|
|
45074
45961
|
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
45075
45962
|
}
|
|
45076
45963
|
function isMcpToolName(crushName) {
|
|
45077
45964
|
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
45078
45965
|
}
|
|
45079
|
-
function uniq$
|
|
45966
|
+
function uniq$10(values) {
|
|
45080
45967
|
return [...new Set(values)];
|
|
45081
45968
|
}
|
|
45082
45969
|
/**
|
|
@@ -45182,8 +46069,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
45182
46069
|
const managedToolNames = managedCrushToolNames(config);
|
|
45183
46070
|
const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
45184
46071
|
const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
45185
|
-
const allowedList = uniq$
|
|
45186
|
-
const disabledList = uniq$
|
|
46072
|
+
const allowedList = uniq$10([...preservedAllowed, ...generated.allowed]);
|
|
46073
|
+
const disabledList = uniq$10([...preservedDisabled, ...generated.disabled]);
|
|
45187
46074
|
const patch = {};
|
|
45188
46075
|
if (allowedList.length > 0 || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = { [CRUSH_ALLOWED_TOOLS_KEY]: allowedList.length > 0 ? allowedList : void 0 };
|
|
45189
46076
|
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
@@ -45250,7 +46137,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
45250
46137
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
45251
46138
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
45252
46139
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
45253
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
46140
|
+
if (pattern === CATCH_ALL_PATTERN$5) {
|
|
45254
46141
|
catchAllAction = action;
|
|
45255
46142
|
continue;
|
|
45256
46143
|
}
|
|
@@ -45300,8 +46187,8 @@ function convertRulesyncToCrushLists({ config, logger }) {
|
|
|
45300
46187
|
else if (catchAllAction === "deny") lists.disabled.push(toolName);
|
|
45301
46188
|
}
|
|
45302
46189
|
return {
|
|
45303
|
-
allowed: uniq$
|
|
45304
|
-
disabled: uniq$
|
|
46190
|
+
allowed: uniq$10(lists.allowed),
|
|
46191
|
+
disabled: uniq$10(lists.disabled)
|
|
45305
46192
|
};
|
|
45306
46193
|
}
|
|
45307
46194
|
/**
|
|
@@ -45323,11 +46210,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
45323
46210
|
};
|
|
45324
46211
|
for (const entry of disabled) {
|
|
45325
46212
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
45326
|
-
bucketFor(toCanonicalToolName$
|
|
46213
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
|
|
45327
46214
|
}
|
|
45328
46215
|
for (const entry of allowed) {
|
|
45329
46216
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
45330
|
-
bucketFor(toCanonicalToolName$
|
|
46217
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
|
|
45331
46218
|
}
|
|
45332
46219
|
return { permission };
|
|
45333
46220
|
}
|
|
@@ -45374,13 +46261,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
45374
46261
|
WebFetch: "webfetch",
|
|
45375
46262
|
Mcp: "mcp"
|
|
45376
46263
|
};
|
|
45377
|
-
const MCP_CANONICAL_PREFIX$
|
|
46264
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
45378
46265
|
/**
|
|
45379
46266
|
* Returns true if the canonical category is the per-tool MCP form
|
|
45380
46267
|
* `mcp__<server>__<tool>`.
|
|
45381
46268
|
*/
|
|
45382
46269
|
function isMcpScopedCategory(canonical) {
|
|
45383
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46270
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
|
|
45384
46271
|
}
|
|
45385
46272
|
function toCursorType(canonical) {
|
|
45386
46273
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -45414,7 +46301,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
45414
46301
|
if (match) {
|
|
45415
46302
|
const server = match[1] ?? "*";
|
|
45416
46303
|
const tool = match[2] ?? "*";
|
|
45417
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
46304
|
+
return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
|
|
45418
46305
|
}
|
|
45419
46306
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
45420
46307
|
}
|
|
@@ -45707,7 +46594,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
45707
46594
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
45708
46595
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
45709
46596
|
if (!permission[canonical]) permission[canonical] = {};
|
|
45710
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46597
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
|
|
45711
46598
|
permission[canonical][canonicalPattern] = action;
|
|
45712
46599
|
}
|
|
45713
46600
|
};
|
|
@@ -47226,7 +48113,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
47226
48113
|
//#region src/features/permissions/goose-permissions.ts
|
|
47227
48114
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
47228
48115
|
const GOOSE_USER_KEY = "user";
|
|
47229
|
-
const CATCH_ALL_PATTERN$
|
|
48116
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
47230
48117
|
const RULESYNC_TO_GOOSE_TOOL_NAME = {
|
|
47231
48118
|
bash: "developer__shell",
|
|
47232
48119
|
edit: "developer__text_editor",
|
|
@@ -47388,7 +48275,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
47388
48275
|
for (const [category, rules] of orderedEntries) {
|
|
47389
48276
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
47390
48277
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
47391
|
-
if (pattern !== CATCH_ALL_PATTERN$
|
|
48278
|
+
if (pattern !== CATCH_ALL_PATTERN$4) {
|
|
47392
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.`);
|
|
47393
48280
|
continue;
|
|
47394
48281
|
}
|
|
@@ -47417,7 +48304,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
47417
48304
|
for (const toolName of toolNames) {
|
|
47418
48305
|
const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
|
|
47419
48306
|
permission[category] ??= {};
|
|
47420
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
48307
|
+
permission[category][CATCH_ALL_PATTERN$4] = action;
|
|
47421
48308
|
}
|
|
47422
48309
|
}
|
|
47423
48310
|
return { permission };
|
|
@@ -47428,8 +48315,8 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
47428
48315
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
47429
48316
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
47430
48317
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
47431
|
-
const CATCH_ALL_PATTERN$
|
|
47432
|
-
const MCP_CANONICAL_PREFIX$
|
|
48318
|
+
const CATCH_ALL_PATTERN$3 = "*";
|
|
48319
|
+
const MCP_CANONICAL_PREFIX$3 = "mcp__";
|
|
47433
48320
|
const CATEGORY_TO_GROK_TOOL = {
|
|
47434
48321
|
bash: "Bash",
|
|
47435
48322
|
read: "Read",
|
|
@@ -47470,14 +48357,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
|
47470
48357
|
* concrete pattern emits `Tool(pattern)`.
|
|
47471
48358
|
*/
|
|
47472
48359
|
function buildGrokEntry(category, pattern) {
|
|
47473
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
48360
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
|
|
47474
48361
|
const remainder = category.slice(5);
|
|
47475
48362
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
47476
48363
|
}
|
|
47477
|
-
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})`;
|
|
47478
48365
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
47479
48366
|
if (tool === void 0) return null;
|
|
47480
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
48367
|
+
return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
47481
48368
|
}
|
|
47482
48369
|
/**
|
|
47483
48370
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -47497,17 +48384,17 @@ function parseGrokEntry(entry) {
|
|
|
47497
48384
|
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
47498
48385
|
}
|
|
47499
48386
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
47500
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47501
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48387
|
+
category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
|
|
48388
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47502
48389
|
} : {
|
|
47503
48390
|
category: "mcp",
|
|
47504
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48391
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47505
48392
|
};
|
|
47506
48393
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
47507
48394
|
if (category === void 0) return null;
|
|
47508
48395
|
return {
|
|
47509
48396
|
category,
|
|
47510
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
48397
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
|
|
47511
48398
|
};
|
|
47512
48399
|
}
|
|
47513
48400
|
/**
|
|
@@ -47528,15 +48415,15 @@ function parseGrokRule(rule) {
|
|
|
47528
48415
|
if (typeof tool !== "string") return null;
|
|
47529
48416
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
47530
48417
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
47531
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
48418
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
|
|
47532
48419
|
const lowerTool = tool.trim().toLowerCase();
|
|
47533
|
-
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 ? {
|
|
47534
48421
|
category: "mcp",
|
|
47535
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48422
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47536
48423
|
action
|
|
47537
48424
|
} : {
|
|
47538
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47539
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48425
|
+
category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
|
|
48426
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47540
48427
|
action
|
|
47541
48428
|
};
|
|
47542
48429
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -47684,7 +48571,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
47684
48571
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
47685
48572
|
}
|
|
47686
48573
|
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
47687
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
48574
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
|
|
47688
48575
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
47689
48576
|
}
|
|
47690
48577
|
validate() {
|
|
@@ -49564,6 +50451,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
|
|
|
49564
50451
|
}
|
|
49565
50452
|
};
|
|
49566
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
|
|
49567
51015
|
//#region src/features/permissions/qwencode-permissions.ts
|
|
49568
51016
|
/**
|
|
49569
51017
|
* Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
|
|
@@ -55666,6 +57114,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
55666
57114
|
supportsImport: true
|
|
55667
57115
|
}
|
|
55668
57116
|
}],
|
|
57117
|
+
["pool", {
|
|
57118
|
+
class: PoolPermissions,
|
|
57119
|
+
meta: {
|
|
57120
|
+
supportsProject: true,
|
|
57121
|
+
supportsGlobal: true,
|
|
57122
|
+
supportsImport: true
|
|
57123
|
+
}
|
|
57124
|
+
}],
|
|
55669
57125
|
["qwencode", {
|
|
55670
57126
|
class: QwencodePermissions,
|
|
55671
57127
|
meta: {
|
|
@@ -56329,8 +57785,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56329
57785
|
const changedPaths = [];
|
|
56330
57786
|
for (const aiDir of aiDirs) {
|
|
56331
57787
|
const dirPath = aiDir.getDirPath();
|
|
56332
|
-
let dirHasChanges = false;
|
|
56333
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;
|
|
56334
57801
|
let mainFileContent;
|
|
56335
57802
|
if (mainFile) {
|
|
56336
57803
|
const mainFilePath = (0, node_path.join)(dirPath, mainFile.name);
|
|
@@ -56342,7 +57809,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56342
57809
|
existing: existingContent
|
|
56343
57810
|
})) dirHasChanges = true;
|
|
56344
57811
|
}
|
|
56345
|
-
const otherFiles = aiDir.getOtherFiles();
|
|
56346
57812
|
for (const file of otherFiles) {
|
|
56347
57813
|
if (dirHasChanges) break;
|
|
56348
57814
|
const filePath = (0, node_path.join)(dirPath, file.relativeFilePathToDirPath);
|
|
@@ -56787,7 +58253,7 @@ const AmpSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
56787
58253
|
* Amp natively reads Agent Skills following the open standard: a directory with a
|
|
56788
58254
|
* `SKILL.md` carrying `name`/`description` frontmatter, scoped to the project
|
|
56789
58255
|
* (`.agents/skills/`) and user-wide (`~/.config/agents/skills/`).
|
|
56790
|
-
* @see https://ampcode.com/
|
|
58256
|
+
* @see https://ampcode.com/docs/customize/skills
|
|
56791
58257
|
*/
|
|
56792
58258
|
var AmpSkill = class AmpSkill extends ToolSkill {
|
|
56793
58259
|
constructor({ outputRoot = process.cwd(), relativeDirPath = AMP_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -61756,37 +63222,6 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
61756
63222
|
}
|
|
61757
63223
|
};
|
|
61758
63224
|
//#endregion
|
|
61759
|
-
//#region src/constants/pool-paths.ts
|
|
61760
|
-
/**
|
|
61761
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
61762
|
-
*
|
|
61763
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
61764
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
61765
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
61766
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
61767
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
61768
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
61769
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
61770
|
-
*
|
|
61771
|
-
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
61772
|
-
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
61773
|
-
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
61774
|
-
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
61775
|
-
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
61776
|
-
*
|
|
61777
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
61778
|
-
* @see https://docs.poolside.ai/skills
|
|
61779
|
-
* @see https://github.com/poolsideai/pool
|
|
61780
|
-
*/
|
|
61781
|
-
/** Project-scoped `.poolside/` directory at the project root. */
|
|
61782
|
-
const POOL_DIR = ".poolside";
|
|
61783
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
61784
|
-
const POOL_GLOBAL_DIR = (0, node_path.join)(".config", "poolside");
|
|
61785
|
-
/** Project skills root, relative to the project root. */
|
|
61786
|
-
const POOL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_DIR, "skills");
|
|
61787
|
-
/** Global skills root, relative to the home directory. */
|
|
61788
|
-
const POOL_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(POOL_GLOBAL_DIR, "skills");
|
|
61789
|
-
//#endregion
|
|
61790
63225
|
//#region src/features/skills/pool-skill.ts
|
|
61791
63226
|
const PoolSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
61792
63227
|
name: zod_mini.z.string(),
|
|
@@ -68492,6 +69927,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
|
|
|
68492
69927
|
}
|
|
68493
69928
|
};
|
|
68494
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
|
|
68495
70249
|
//#region src/features/subagents/reasonix-subagent.ts
|
|
68496
70250
|
const ReasonixSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
68497
70251
|
name: zod_mini.z.string(),
|
|
@@ -69812,6 +71566,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
69812
71566
|
filePattern: "*.md"
|
|
69813
71567
|
}
|
|
69814
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
|
+
}],
|
|
69815
71579
|
["qwencode", {
|
|
69816
71580
|
class: QwencodeSubagent,
|
|
69817
71581
|
meta: {
|
|
@@ -69924,6 +71688,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
69924
71688
|
toolTarget;
|
|
69925
71689
|
global;
|
|
69926
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;
|
|
69927
71697
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
69928
71698
|
super({
|
|
69929
71699
|
outputRoot,
|
|
@@ -69937,16 +71707,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
69937
71707
|
this.global = global;
|
|
69938
71708
|
this.getFactory = getFactory;
|
|
69939
71709
|
}
|
|
71710
|
+
emitsToolFilesForEmptySource() {
|
|
71711
|
+
return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
|
|
71712
|
+
}
|
|
69940
71713
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
69941
71714
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
69942
71715
|
const factory = this.getFactory(this.toolTarget);
|
|
69943
71716
|
const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
|
|
69944
71717
|
if (factory.class.fromRulesyncSubagents) {
|
|
69945
|
-
if (targeted.length === 0) return [];
|
|
71718
|
+
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
69946
71719
|
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
69947
71720
|
outputRoot: this.outputRoot,
|
|
69948
71721
|
rulesyncSubagents: targeted,
|
|
69949
|
-
global: this.global
|
|
71722
|
+
global: this.global,
|
|
71723
|
+
logger: this.logger
|
|
69950
71724
|
});
|
|
69951
71725
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
69952
71726
|
}
|
|
@@ -70002,6 +71776,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70002
71776
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
70003
71777
|
return [];
|
|
70004
71778
|
}
|
|
71779
|
+
this.rulesyncSourceDirFound = true;
|
|
70005
71780
|
const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
|
|
70006
71781
|
if (mdFiles.length === 0) {
|
|
70007
71782
|
this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
|
|
@@ -70042,6 +71817,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70042
71817
|
* earlier root's copy.
|
|
70043
71818
|
*/
|
|
70044
71819
|
async loadRulesyncFiles() {
|
|
71820
|
+
this.rulesyncSourceDirFound = false;
|
|
70045
71821
|
const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
|
|
70046
71822
|
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
70047
71823
|
identity: (subagent) => subagent.getRelativeFilePath(),
|
|
@@ -70104,7 +71880,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70104
71880
|
outputRoot: rootOutputRoot,
|
|
70105
71881
|
relativeDirPath: dirPath,
|
|
70106
71882
|
relativeFilePath: toRelativeFilePath(path),
|
|
70107
|
-
global: this.global
|
|
71883
|
+
global: this.global,
|
|
71884
|
+
logger: this.logger
|
|
70108
71885
|
})));
|
|
70109
71886
|
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
70110
71887
|
loaded,
|
|
@@ -74026,22 +75803,30 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
74026
75803
|
}
|
|
74027
75804
|
};
|
|
74028
75805
|
//#endregion
|
|
74029
|
-
//#region src/features/rules/
|
|
75806
|
+
//#region src/features/rules/nested-agentsmd-rule.ts
|
|
74030
75807
|
/**
|
|
74031
|
-
*
|
|
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.
|
|
74032
75814
|
*
|
|
74033
|
-
*
|
|
74034
|
-
*
|
|
74035
|
-
*
|
|
74036
|
-
* `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
|
|
74037
|
-
* 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.
|
|
74038
75818
|
*
|
|
74039
|
-
*
|
|
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.
|
|
74040
75822
|
*/
|
|
74041
|
-
var
|
|
75823
|
+
var NestedAgentsmdRule = class extends ToolRule {
|
|
75824
|
+
static getFamily() {
|
|
75825
|
+
throw new Error("Please implement this method in the subclass.");
|
|
75826
|
+
}
|
|
74042
75827
|
static getSettablePaths({ global = false } = {}) {
|
|
74043
75828
|
if (global) return { root: {
|
|
74044
|
-
relativeDirPath:
|
|
75829
|
+
relativeDirPath: this.getFamily().globalDir,
|
|
74045
75830
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74046
75831
|
} };
|
|
74047
75832
|
return { root: {
|
|
@@ -74049,18 +75834,6 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74049
75834
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74050
75835
|
} };
|
|
74051
75836
|
}
|
|
74052
|
-
/**
|
|
74053
|
-
* DeepSeek Harness loads the project chain of `AGENTS.md` files from the
|
|
74054
|
-
* project root (the nearest ancestor holding `.git`) down through the session
|
|
74055
|
-
* cwd, broad to specific, so a per-directory file applies only while working
|
|
74056
|
-
* under that directory. Nested files are therefore a real scoping surface,
|
|
74057
|
-
* not just the root file's overflow.
|
|
74058
|
-
*
|
|
74059
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
74060
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
74061
|
-
* literally the same files.
|
|
74062
|
-
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
74063
|
-
*/
|
|
74064
75837
|
static getNestedFilePatterns() {
|
|
74065
75838
|
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
74066
75839
|
}
|
|
@@ -74075,7 +75848,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74075
75848
|
const { root } = this.getSettablePaths({ global });
|
|
74076
75849
|
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
74077
75850
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
74078
|
-
return new
|
|
75851
|
+
return new this({
|
|
74079
75852
|
outputRoot,
|
|
74080
75853
|
relativeDirPath: overrideDirPath,
|
|
74081
75854
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74085,7 +75858,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74085
75858
|
});
|
|
74086
75859
|
}
|
|
74087
75860
|
const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
74088
|
-
return new
|
|
75861
|
+
return new this({
|
|
74089
75862
|
outputRoot,
|
|
74090
75863
|
relativeDirPath: root.relativeDirPath,
|
|
74091
75864
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74099,7 +75872,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74099
75872
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
74100
75873
|
const isRoot = frontmatter.root ?? false;
|
|
74101
75874
|
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
74102
|
-
if (!global && !isRoot && subprojectPath) return new
|
|
75875
|
+
if (!global && !isRoot && subprojectPath) return new this({
|
|
74103
75876
|
outputRoot,
|
|
74104
75877
|
relativeDirPath: (0, node_path.join)(subprojectPath),
|
|
74105
75878
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74107,7 +75880,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74107
75880
|
validate,
|
|
74108
75881
|
root: false
|
|
74109
75882
|
});
|
|
74110
|
-
return new
|
|
75883
|
+
return new this({
|
|
74111
75884
|
outputRoot,
|
|
74112
75885
|
relativeDirPath: root.relativeDirPath,
|
|
74113
75886
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74128,23 +75901,47 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74128
75901
|
};
|
|
74129
75902
|
}
|
|
74130
75903
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
74131
|
-
|
|
75904
|
+
const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
|
|
75905
|
+
return new this({
|
|
74132
75906
|
outputRoot,
|
|
74133
75907
|
relativeDirPath,
|
|
74134
75908
|
relativeFilePath,
|
|
74135
75909
|
fileContent: "",
|
|
74136
75910
|
validate: false,
|
|
74137
|
-
root:
|
|
75911
|
+
root: isRoot
|
|
74138
75912
|
});
|
|
74139
75913
|
}
|
|
74140
75914
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
74141
75915
|
return this.isTargetedByRulesyncRuleDefault({
|
|
74142
75916
|
rulesyncRule,
|
|
74143
|
-
toolTarget:
|
|
75917
|
+
toolTarget: this.getFamily().toolTarget
|
|
74144
75918
|
});
|
|
74145
75919
|
}
|
|
74146
75920
|
};
|
|
74147
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
|
|
74148
75945
|
//#region src/features/rules/factorydroid-rule.ts
|
|
74149
75946
|
/**
|
|
74150
75947
|
* Rule generator for Factory Droid.
|
|
@@ -75538,110 +77335,24 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
75538
77335
|
};
|
|
75539
77336
|
//#endregion
|
|
75540
77337
|
//#region src/features/rules/pool-rule.ts
|
|
75541
|
-
|
|
75542
|
-
|
|
75543
|
-
|
|
75544
|
-
|
|
75545
|
-
|
|
75546
|
-
|
|
75547
|
-
|
|
75548
|
-
|
|
75549
|
-
|
|
75550
|
-
|
|
75551
|
-
|
|
75552
|
-
|
|
75553
|
-
|
|
75554
|
-
|
|
75555
|
-
* through the working directory (deeper files take precedence), skipping
|
|
75556
|
-
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
75557
|
-
* just the root file's overflow.
|
|
75558
|
-
*
|
|
75559
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
75560
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
75561
|
-
* literally the same files.
|
|
75562
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
75563
|
-
*/
|
|
75564
|
-
static getNestedFilePatterns() {
|
|
75565
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75566
|
-
}
|
|
75567
|
-
/**
|
|
75568
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
75569
|
-
* (project or global).
|
|
75570
|
-
*/
|
|
75571
|
-
getSubprojectPath() {
|
|
75572
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75573
|
-
}
|
|
75574
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
75575
|
-
const { root } = this.getSettablePaths({ global });
|
|
75576
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
75577
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
75578
|
-
return new PoolRule({
|
|
75579
|
-
outputRoot,
|
|
75580
|
-
relativeDirPath: overrideDirPath,
|
|
75581
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75582
|
-
fileContent,
|
|
75583
|
-
validate,
|
|
75584
|
-
root: false
|
|
75585
|
-
});
|
|
75586
|
-
}
|
|
75587
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
75588
|
-
return new PoolRule({
|
|
75589
|
-
outputRoot,
|
|
75590
|
-
relativeDirPath: root.relativeDirPath,
|
|
75591
|
-
relativeFilePath: root.relativeFilePath,
|
|
75592
|
-
fileContent,
|
|
75593
|
-
validate,
|
|
75594
|
-
root: true
|
|
75595
|
-
});
|
|
75596
|
-
}
|
|
75597
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
75598
|
-
const { root } = this.getSettablePaths({ global });
|
|
75599
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
75600
|
-
const isRoot = frontmatter.root ?? false;
|
|
75601
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
75602
|
-
if (!global && !isRoot && subprojectPath) return new PoolRule({
|
|
75603
|
-
outputRoot,
|
|
75604
|
-
relativeDirPath: (0, node_path.join)(subprojectPath),
|
|
75605
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75606
|
-
fileContent: rulesyncRule.getBody(),
|
|
75607
|
-
validate,
|
|
75608
|
-
root: false
|
|
75609
|
-
});
|
|
75610
|
-
return new PoolRule({
|
|
75611
|
-
outputRoot,
|
|
75612
|
-
relativeDirPath: root.relativeDirPath,
|
|
75613
|
-
relativeFilePath: root.relativeFilePath,
|
|
75614
|
-
fileContent: rulesyncRule.getBody(),
|
|
75615
|
-
validate,
|
|
75616
|
-
root: isRoot
|
|
75617
|
-
});
|
|
75618
|
-
}
|
|
75619
|
-
toRulesyncRule() {
|
|
75620
|
-
const subprojectPath = this.getSubprojectPath();
|
|
75621
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
75622
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
75623
|
-
}
|
|
75624
|
-
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() {
|
|
75625
77352
|
return {
|
|
75626
|
-
|
|
75627
|
-
error: null
|
|
75628
|
-
};
|
|
75629
|
-
}
|
|
75630
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
75631
|
-
return new PoolRule({
|
|
75632
|
-
outputRoot,
|
|
75633
|
-
relativeDirPath,
|
|
75634
|
-
relativeFilePath,
|
|
75635
|
-
fileContent: "",
|
|
75636
|
-
validate: false,
|
|
75637
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
|
|
75638
|
-
});
|
|
75639
|
-
}
|
|
75640
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
75641
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
75642
|
-
rulesyncRule,
|
|
77353
|
+
globalDir: POOL_GLOBAL_DIR,
|
|
75643
77354
|
toolTarget: "pool"
|
|
75644
|
-
}
|
|
77355
|
+
};
|
|
75645
77356
|
}
|
|
75646
77357
|
};
|
|
75647
77358
|
//#endregion
|
|
@@ -76605,110 +78316,23 @@ var TaktRule = class TaktRule extends ToolRule {
|
|
|
76605
78316
|
//#endregion
|
|
76606
78317
|
//#region src/features/rules/vibe-rule.ts
|
|
76607
78318
|
const VIBE_GLOBAL_DIR = ".vibe";
|
|
76608
|
-
|
|
76609
|
-
|
|
76610
|
-
|
|
76611
|
-
|
|
76612
|
-
|
|
76613
|
-
|
|
76614
|
-
|
|
76615
|
-
|
|
76616
|
-
|
|
76617
|
-
|
|
76618
|
-
|
|
76619
|
-
|
|
76620
|
-
|
|
76621
|
-
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
76622
|
-
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
76623
|
-
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
76624
|
-
* the root file's overflow.
|
|
76625
|
-
*
|
|
76626
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
76627
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
76628
|
-
* literally the same files.
|
|
76629
|
-
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
76630
|
-
*/
|
|
76631
|
-
static getNestedFilePatterns() {
|
|
76632
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76633
|
-
}
|
|
76634
|
-
/**
|
|
76635
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
76636
|
-
* (project or global).
|
|
76637
|
-
*/
|
|
76638
|
-
getSubprojectPath() {
|
|
76639
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76640
|
-
}
|
|
76641
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
76642
|
-
const { root } = this.getSettablePaths({ global });
|
|
76643
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
76644
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
76645
|
-
return new VibeRule({
|
|
76646
|
-
outputRoot,
|
|
76647
|
-
relativeDirPath: overrideDirPath,
|
|
76648
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
76649
|
-
fileContent,
|
|
76650
|
-
validate,
|
|
76651
|
-
root: false
|
|
76652
|
-
});
|
|
76653
|
-
}
|
|
76654
|
-
const fileContent = await readFileContent((0, node_path.join)(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
76655
|
-
return new VibeRule({
|
|
76656
|
-
outputRoot,
|
|
76657
|
-
relativeDirPath: root.relativeDirPath,
|
|
76658
|
-
relativeFilePath: root.relativeFilePath,
|
|
76659
|
-
fileContent,
|
|
76660
|
-
validate,
|
|
76661
|
-
root: true
|
|
76662
|
-
});
|
|
76663
|
-
}
|
|
76664
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
76665
|
-
const { root } = this.getSettablePaths({ global });
|
|
76666
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
76667
|
-
const isRoot = frontmatter.root ?? false;
|
|
76668
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
76669
|
-
if (!global && !isRoot && subprojectPath) return new VibeRule({
|
|
76670
|
-
outputRoot,
|
|
76671
|
-
relativeDirPath: (0, node_path.join)(subprojectPath),
|
|
76672
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
76673
|
-
fileContent: rulesyncRule.getBody(),
|
|
76674
|
-
validate,
|
|
76675
|
-
root: false
|
|
76676
|
-
});
|
|
76677
|
-
return new VibeRule({
|
|
76678
|
-
outputRoot,
|
|
76679
|
-
relativeDirPath: root.relativeDirPath,
|
|
76680
|
-
relativeFilePath: root.relativeFilePath,
|
|
76681
|
-
fileContent: rulesyncRule.getBody(),
|
|
76682
|
-
validate,
|
|
76683
|
-
root: isRoot
|
|
76684
|
-
});
|
|
76685
|
-
}
|
|
76686
|
-
toRulesyncRule() {
|
|
76687
|
-
const subprojectPath = this.getSubprojectPath();
|
|
76688
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
76689
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
76690
|
-
}
|
|
76691
|
-
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() {
|
|
76692
78332
|
return {
|
|
76693
|
-
|
|
76694
|
-
error: null
|
|
76695
|
-
};
|
|
76696
|
-
}
|
|
76697
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
76698
|
-
return new VibeRule({
|
|
76699
|
-
outputRoot,
|
|
76700
|
-
relativeDirPath,
|
|
76701
|
-
relativeFilePath,
|
|
76702
|
-
fileContent: "",
|
|
76703
|
-
validate: false,
|
|
76704
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
|
|
76705
|
-
});
|
|
76706
|
-
}
|
|
76707
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
76708
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
76709
|
-
rulesyncRule,
|
|
78333
|
+
globalDir: VIBE_GLOBAL_DIR,
|
|
76710
78334
|
toolTarget: "vibe"
|
|
76711
|
-
}
|
|
78335
|
+
};
|
|
76712
78336
|
}
|
|
76713
78337
|
};
|
|
76714
78338
|
//#endregion
|
|
@@ -78915,6 +80539,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
|
|
|
78915
80539
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
78916
80540
|
});
|
|
78917
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
|
+
};
|
|
78918
80552
|
const expectedConfig = mergeEnabledPlugins({
|
|
78919
80553
|
existingContent: await readFileContentOrNull(configPath) ?? "",
|
|
78920
80554
|
filePath: configPath,
|
|
@@ -79382,7 +81016,8 @@ async function processEmptyFeatureGeneration(params) {
|
|
|
79382
81016
|
*/
|
|
79383
81017
|
async function processFeatureWithRulesyncFiles(params) {
|
|
79384
81018
|
const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
|
|
79385
|
-
|
|
81019
|
+
const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
|
|
81020
|
+
if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
|
|
79386
81021
|
config,
|
|
79387
81022
|
processor,
|
|
79388
81023
|
sweepPlan,
|