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
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { ZodError } from "zod";
|
|
2
2
|
import { meta, minLength, nonnegative, optional, refine, z } from "zod/mini";
|
|
3
3
|
import { cp, lstat, mkdir, mkdtemp, open, readFile, readdir, readlink, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import path, { basename, dirname, extname, isAbsolute, join, normalize, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
-
import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
4
|
+
import path, { basename, dirname, extname, isAbsolute, join, normalize, parse, posix, relative, resolve, sep, win32 } from "node:path";
|
|
5
|
+
import { applyEdits, findNodeAtLocation, getNodeValue, modify, parse as parse$1, parseTree, printParseErrorCode } from "jsonc-parser";
|
|
6
6
|
import { constants } from "node:fs";
|
|
7
7
|
import os from "node:os";
|
|
8
8
|
import { intersection, kebabCase, uniq } from "es-toolkit";
|
|
@@ -14,7 +14,7 @@ import { YAMLException, dump, load } from "js-yaml";
|
|
|
14
14
|
import { omit } from "es-toolkit/object";
|
|
15
15
|
import { createHash } from "node:crypto";
|
|
16
16
|
import * as smolToml from "smol-toml";
|
|
17
|
-
import { parse as parse$
|
|
17
|
+
import { parse as parse$2, stringify } from "smol-toml";
|
|
18
18
|
import { encode } from "@toon-format/toon";
|
|
19
19
|
//#region src/utils/control-characters.ts
|
|
20
20
|
/**
|
|
@@ -591,6 +591,7 @@ const mcpProcessorToolTargetTuple = [
|
|
|
591
591
|
"junie",
|
|
592
592
|
"musecode",
|
|
593
593
|
"opencode",
|
|
594
|
+
"pool",
|
|
594
595
|
"qwencode",
|
|
595
596
|
"reasonix",
|
|
596
597
|
"roo",
|
|
@@ -670,6 +671,7 @@ const subagentsProcessorToolTargetTuple = [
|
|
|
670
671
|
"kiro-cli",
|
|
671
672
|
"kiro-ide",
|
|
672
673
|
"opencode",
|
|
674
|
+
"pool",
|
|
673
675
|
"qwencode",
|
|
674
676
|
"reasonix",
|
|
675
677
|
"roo",
|
|
@@ -751,6 +753,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
751
753
|
"crush",
|
|
752
754
|
"opencode",
|
|
753
755
|
"pi",
|
|
756
|
+
"pool",
|
|
754
757
|
"factorydroid",
|
|
755
758
|
"goose",
|
|
756
759
|
"hermesagent",
|
|
@@ -798,6 +801,7 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
798
801
|
"kiro-ide",
|
|
799
802
|
"opencode",
|
|
800
803
|
"pi",
|
|
804
|
+
"pool",
|
|
801
805
|
"qwencode",
|
|
802
806
|
"reasonix",
|
|
803
807
|
"roo",
|
|
@@ -1354,6 +1358,111 @@ async function resolvedPathEscapesRoot({ rootPath, targetPath }) {
|
|
|
1354
1358
|
}));
|
|
1355
1359
|
}
|
|
1356
1360
|
/**
|
|
1361
|
+
* How many links {@link writeLandingPath} follows before it treats the chain as
|
|
1362
|
+
* a cycle it cannot write through. Linux itself gives up after 40.
|
|
1363
|
+
*/
|
|
1364
|
+
const MAX_LINK_HOPS = 40;
|
|
1365
|
+
/**
|
|
1366
|
+
* Whether `error` is the ENOTDIR a lookup through a file raises. On the way to
|
|
1367
|
+
* a write it means the same as a missing entry: nothing at or below the path
|
|
1368
|
+
* exists, so nothing there can be a link.
|
|
1369
|
+
*/
|
|
1370
|
+
function isNotADirectoryError(error) {
|
|
1371
|
+
return someErrorInChain(error, (candidate) => "code" in candidate && candidate.code === "ENOTDIR");
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Where a write to `targetPath` lands once the OS has followed every link on
|
|
1375
|
+
* the way there, or `null` when the links form a cycle no write can get
|
|
1376
|
+
* through. The target itself usually does not exist yet, so the walk goes
|
|
1377
|
+
* segment by segment from the filesystem root: an existing segment that is a
|
|
1378
|
+
* link is replaced by what it resolves to, and a dangling link — one whose
|
|
1379
|
+
* target does not exist yet, which is exactly the shape of a link planted at
|
|
1380
|
+
* `AGENTS.md` pointing at a `~/.zshenv` that is still to be created — has its
|
|
1381
|
+
* target spliced into the walk in its place, so the segments of that target
|
|
1382
|
+
* are looked up one by one too. Once a segment is missing, nothing below it
|
|
1383
|
+
* exists, and the rest of the path is where the write's `mkdir -p` will put it.
|
|
1384
|
+
*
|
|
1385
|
+
* Walking segment by segment is what keeps a `..` honest. A link target is a
|
|
1386
|
+
* path the OS resolves left to right, so in `sub/../victim` the `..` climbs out
|
|
1387
|
+
* of wherever `sub` leads, not out of the directory `sub` is spelled in; a
|
|
1388
|
+
* lexical `resolve` folds `sub/..` away before `sub` is ever looked at, which
|
|
1389
|
+
* is how a link chain could be spelled to pass the check and still land
|
|
1390
|
+
* outside the root.
|
|
1391
|
+
*/
|
|
1392
|
+
async function writeLandingPath(targetPath) {
|
|
1393
|
+
const start = splitAbsolutePath(isAbsolute(targetPath) ? targetPath : resolve(targetPath));
|
|
1394
|
+
let current = start.root;
|
|
1395
|
+
let pending = start.segments;
|
|
1396
|
+
let exists = true;
|
|
1397
|
+
let linkHops = 0;
|
|
1398
|
+
while (pending.length > 0) {
|
|
1399
|
+
const segment = pending.shift();
|
|
1400
|
+
if (segment === void 0 || segment === "" || segment === ".") continue;
|
|
1401
|
+
if (segment === "..") {
|
|
1402
|
+
current = dirname(current);
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
current = join(current, segment);
|
|
1406
|
+
if (!exists) continue;
|
|
1407
|
+
let stats;
|
|
1408
|
+
try {
|
|
1409
|
+
stats = await lstat(current);
|
|
1410
|
+
} catch (error) {
|
|
1411
|
+
if (!isFileNotFoundError(error) && !isNotADirectoryError(error)) throw error;
|
|
1412
|
+
exists = false;
|
|
1413
|
+
continue;
|
|
1414
|
+
}
|
|
1415
|
+
if (!stats.isSymbolicLink()) continue;
|
|
1416
|
+
linkHops += 1;
|
|
1417
|
+
if (linkHops > MAX_LINK_HOPS) return null;
|
|
1418
|
+
try {
|
|
1419
|
+
current = await realpath(current);
|
|
1420
|
+
continue;
|
|
1421
|
+
} catch {}
|
|
1422
|
+
const linkTarget = await readlink(current);
|
|
1423
|
+
if (isAbsolute(linkTarget)) {
|
|
1424
|
+
const target = splitAbsolutePath(linkTarget);
|
|
1425
|
+
current = target.root;
|
|
1426
|
+
pending = [...target.segments, ...pending];
|
|
1427
|
+
} else {
|
|
1428
|
+
current = dirname(current);
|
|
1429
|
+
pending = [...splitPathSegments(linkTarget), ...pending];
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
return current;
|
|
1433
|
+
}
|
|
1434
|
+
/**
|
|
1435
|
+
* Split an absolute path into its root and its raw segments, without folding a
|
|
1436
|
+
* `..` away: {@link writeLandingPath} has to see every `..` where it is spelled.
|
|
1437
|
+
*/
|
|
1438
|
+
function splitAbsolutePath(absolutePath) {
|
|
1439
|
+
const root = parse(absolutePath).root;
|
|
1440
|
+
return {
|
|
1441
|
+
root,
|
|
1442
|
+
segments: splitPathSegments(absolutePath.slice(root.length))
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether writing `targetPath` would land outside `rootPath` once every link on
|
|
1447
|
+
* the way there is resolved. Both sides are taken to where a write to them
|
|
1448
|
+
* would land (see {@link writeLandingPath}), so a root that does not fully
|
|
1449
|
+
* exist yet is judged the same way as the target below it.
|
|
1450
|
+
*
|
|
1451
|
+
* A link that stays inside the root passes. That is what sets this apart from
|
|
1452
|
+
* the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
|
|
1453
|
+
* dotfiles checkout linked from inside the home directory is a common shape for
|
|
1454
|
+
* a global write target, and refusing it would refuse `--global` outright.
|
|
1455
|
+
*
|
|
1456
|
+
* A link chain that cycles is reported as an escape: the write could not get
|
|
1457
|
+
* through it, and refusing it is the answer that never writes somewhere
|
|
1458
|
+
* unexpected.
|
|
1459
|
+
*/
|
|
1460
|
+
async function writablePathEscapesRoot({ rootPath, targetPath }) {
|
|
1461
|
+
const [rootLanding, targetLanding] = await Promise.all([writeLandingPath(rootPath), writeLandingPath(targetPath)]);
|
|
1462
|
+
if (rootLanding === null || targetLanding === null) return true;
|
|
1463
|
+
return pathEscapesRoot(relative(rootLanding, targetLanding));
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1357
1466
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1358
1467
|
*
|
|
1359
1468
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -2935,7 +3044,7 @@ const getDefaults = () => ({
|
|
|
2935
3044
|
const loadConfigFromFile = async (filePath) => {
|
|
2936
3045
|
if (!await fileExists(filePath)) return {};
|
|
2937
3046
|
const fileContent = await readFileContent(filePath);
|
|
2938
|
-
const jsonData = parse(fileContent);
|
|
3047
|
+
const jsonData = parse$1(fileContent);
|
|
2939
3048
|
const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
|
|
2940
3049
|
assertTargetsFeaturesExclusive({
|
|
2941
3050
|
targets: configParams.targets,
|
|
@@ -3304,6 +3413,8 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3304
3413
|
".grok/config.toml",
|
|
3305
3414
|
".rovodev/config.yml",
|
|
3306
3415
|
".rovodev/mcp.json",
|
|
3416
|
+
".poolside/settings.yaml",
|
|
3417
|
+
".config/poolside/settings.yaml",
|
|
3307
3418
|
".tabnine/agent/settings.json",
|
|
3308
3419
|
".vibe/config.toml",
|
|
3309
3420
|
".zcode/config.json",
|
|
@@ -4353,7 +4464,7 @@ const PI_HOOK_EVENTS = [
|
|
|
4353
4464
|
* adapter. Amp's `agent.start` / `agent.end` events describe the main agent
|
|
4354
4465
|
* turn, not a subagent lifecycle, so they map to `beforeSubmitPrompt` / `stop`.
|
|
4355
4466
|
*
|
|
4356
|
-
* @see https://ampcode.com/
|
|
4467
|
+
* @see https://ampcode.com/docs/plugin-api
|
|
4357
4468
|
*/
|
|
4358
4469
|
const AMP_HOOK_EVENTS = [
|
|
4359
4470
|
"sessionStart",
|
|
@@ -4651,6 +4762,34 @@ const VIBE_HOOK_EVENTS = [
|
|
|
4651
4762
|
"stop"
|
|
4652
4763
|
];
|
|
4653
4764
|
/**
|
|
4765
|
+
* Hook events supported by Pool.
|
|
4766
|
+
*
|
|
4767
|
+
* Pool's `hooks` settings block fires six events, spelled the Claude Code
|
|
4768
|
+
* way: `PreToolUse`, `PostToolUse`, `UserPromptSubmit` (← `beforeSubmitPrompt`),
|
|
4769
|
+
* `PreCompact`, `SessionStart` and `Stop`. Every entry carries a `matcher`
|
|
4770
|
+
* (required by Pool's schema; `""`/`*` match any tool, a bare name is an exact
|
|
4771
|
+
* match, `a|b` lists names, anything else is a regular expression), which only
|
|
4772
|
+
* `PreToolUse`/`PostToolUse` test against the tool name.
|
|
4773
|
+
* @see https://docs.poolside.ai/hooks
|
|
4774
|
+
*/
|
|
4775
|
+
const POOL_HOOK_EVENTS = [
|
|
4776
|
+
"preToolUse",
|
|
4777
|
+
"postToolUse",
|
|
4778
|
+
"beforeSubmitPrompt",
|
|
4779
|
+
"preCompact",
|
|
4780
|
+
"sessionStart",
|
|
4781
|
+
"stop"
|
|
4782
|
+
];
|
|
4783
|
+
const CANONICAL_TO_POOL_EVENT_NAMES = {
|
|
4784
|
+
preToolUse: "PreToolUse",
|
|
4785
|
+
postToolUse: "PostToolUse",
|
|
4786
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
4787
|
+
preCompact: "PreCompact",
|
|
4788
|
+
sessionStart: "SessionStart",
|
|
4789
|
+
stop: "Stop"
|
|
4790
|
+
};
|
|
4791
|
+
const POOL_TO_CANONICAL_EVENT_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_EVENT_NAMES).map(([k, v]) => [v, k]));
|
|
4792
|
+
/**
|
|
4654
4793
|
* Hook events supported by Crush.
|
|
4655
4794
|
*
|
|
4656
4795
|
* Crush's `hooks` config block currently fires a single event, `PreToolUse`
|
|
@@ -5245,6 +5384,7 @@ const HooksConfigSchema = z.looseObject({
|
|
|
5245
5384
|
"kimi-code": z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5246
5385
|
zcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5247
5386
|
crush: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5387
|
+
pool: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5248
5388
|
bob: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5249
5389
|
cortexcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
5250
5390
|
commandcode: z.optional(z.looseObject({ hooks: z.optional(hooksRecordSchema) })),
|
|
@@ -5919,7 +6059,7 @@ function parseJsonc(content) {
|
|
|
5919
6059
|
*/
|
|
5920
6060
|
function parseStrict(content) {
|
|
5921
6061
|
const errors = [];
|
|
5922
|
-
const result = parse(content, errors, {
|
|
6062
|
+
const result = parse$1(content, errors, {
|
|
5923
6063
|
allowTrailingComma: true,
|
|
5924
6064
|
disallowComments: false
|
|
5925
6065
|
});
|
|
@@ -6198,6 +6338,7 @@ const McpServerSchema = z.looseObject({
|
|
|
6198
6338
|
crushOauthClientSecret: z.optional(z.string()),
|
|
6199
6339
|
crushOauthCallbackPort: z.optional(z.int()),
|
|
6200
6340
|
crushSessionless: z.optional(z.boolean()),
|
|
6341
|
+
poolAllow: z.optional(z.array(z.string())),
|
|
6201
6342
|
headers: z.optional(z.record(z.string(), z.string())),
|
|
6202
6343
|
/**
|
|
6203
6344
|
* The canonical per-server tool allowlist.
|
|
@@ -6629,6 +6770,7 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
6629
6770
|
"crushOauthClientSecret",
|
|
6630
6771
|
"crushOauthCallbackPort",
|
|
6631
6772
|
"crushSessionless",
|
|
6773
|
+
"poolAllow",
|
|
6632
6774
|
"oauth_client_id",
|
|
6633
6775
|
"oauth_client_secret",
|
|
6634
6776
|
"oauth_callback_port",
|
|
@@ -7971,6 +8113,7 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
7971
8113
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7972
8114
|
crush: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7973
8115
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
8116
|
+
pool: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7974
8117
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7975
8118
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
7976
8119
|
roo: z.optional(CanonicalPermissionsOverrideSchema),
|
|
@@ -8316,7 +8459,7 @@ const PERMISSION_OVERRIDE_KEY_ALIASES = {
|
|
|
8316
8459
|
* them; treating a segment that carries one as static could name a directory
|
|
8317
8460
|
* that no matched file actually lives under.
|
|
8318
8461
|
*/
|
|
8319
|
-
const GLOB_METACHARACTERS = /[*?[\]{}()!]/;
|
|
8462
|
+
const GLOB_METACHARACTERS$1 = /[*?[\]{}()!]/;
|
|
8320
8463
|
/**
|
|
8321
8464
|
* The directory every file matched by `glob` lives under, or `undefined` when
|
|
8322
8465
|
* the pattern pins down no such directory.
|
|
@@ -8349,7 +8492,7 @@ function getGlobStaticPrefix(glob) {
|
|
|
8349
8492
|
const lastIndex = segments.length - 1;
|
|
8350
8493
|
const prefix = [];
|
|
8351
8494
|
for (const [index, segment] of segments.entries()) {
|
|
8352
|
-
if (segment === "" || GLOB_METACHARACTERS.test(segment)) break;
|
|
8495
|
+
if (segment === "" || GLOB_METACHARACTERS$1.test(segment)) break;
|
|
8353
8496
|
if (segment === ".") continue;
|
|
8354
8497
|
if (index === lastIndex) break;
|
|
8355
8498
|
prefix.push(segment);
|
|
@@ -9813,6 +9956,22 @@ allowedMcpServers: z.optional(z.array(z.string())) })),
|
|
|
9813
9956
|
injectAgentsMd: z.optional(z.boolean()),
|
|
9814
9957
|
mcpServers: z.optional(z.array(z.string()))
|
|
9815
9958
|
})),
|
|
9959
|
+
pool: z.optional(z.looseObject({
|
|
9960
|
+
type: z.optional(z.enum([
|
|
9961
|
+
"in_process",
|
|
9962
|
+
"command",
|
|
9963
|
+
"agent_server"
|
|
9964
|
+
])),
|
|
9965
|
+
description: z.optional(z.string()),
|
|
9966
|
+
instructions: z.optional(z.string()),
|
|
9967
|
+
command: z.optional(z.string()),
|
|
9968
|
+
args: z.optional(z.array(z.string())),
|
|
9969
|
+
env: z.optional(z.record(z.string(), z.string())),
|
|
9970
|
+
agent_server: z.optional(z.string()),
|
|
9971
|
+
session_config_options: z.optional(z.record(z.string(), z.unknown())),
|
|
9972
|
+
inherit_agent_config: z.optional(z.boolean()),
|
|
9973
|
+
disabled: z.optional(z.boolean())
|
|
9974
|
+
})),
|
|
9816
9975
|
vibe: z.optional(z.looseObject({
|
|
9817
9976
|
agent_type: z.optional(z.enum(["agent", "subagent"])),
|
|
9818
9977
|
display_name: z.optional(z.string()),
|
|
@@ -10174,7 +10333,7 @@ function tryParseStructured({ filePath, content }) {
|
|
|
10174
10333
|
case ".json":
|
|
10175
10334
|
case ".jsonc": {
|
|
10176
10335
|
const errors = [];
|
|
10177
|
-
const value = parse(content, errors);
|
|
10336
|
+
const value = parse$1(content, errors);
|
|
10178
10337
|
return errors.length > 0 ? PARSE_FAILED : {
|
|
10179
10338
|
ok: true,
|
|
10180
10339
|
value
|
|
@@ -10400,6 +10559,17 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10400
10559
|
this.logger = logger;
|
|
10401
10560
|
}
|
|
10402
10561
|
/**
|
|
10562
|
+
* Whether `convertRulesyncFilesToToolFiles` still yields tool files for an
|
|
10563
|
+
* empty source list. A processor that aggregates into a shared file it may
|
|
10564
|
+
* never delete (Pool's `settings.yaml` subagents) answers true so a run
|
|
10565
|
+
* whose last source file is gone retracts what an earlier run generated;
|
|
10566
|
+
* the default keeps a run with no source from writing anything. Consulted
|
|
10567
|
+
* after `loadRulesyncFiles`, so the answer may depend on what that load saw.
|
|
10568
|
+
*/
|
|
10569
|
+
emitsToolFilesForEmptySource() {
|
|
10570
|
+
return false;
|
|
10571
|
+
}
|
|
10572
|
+
/**
|
|
10403
10573
|
* Return tool targets that this feature supports.
|
|
10404
10574
|
*/
|
|
10405
10575
|
static getToolTargets(_params = {}) {
|
|
@@ -10414,6 +10584,11 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
10414
10584
|
const changedPaths = [];
|
|
10415
10585
|
for (const aiFile of aiFiles) {
|
|
10416
10586
|
const filePath = aiFile.getFilePath();
|
|
10587
|
+
if (await refusesWriteOutsideRoot({
|
|
10588
|
+
logger: this.logger,
|
|
10589
|
+
rootPath: aiFile.getOutputRoot(),
|
|
10590
|
+
targetPath: filePath
|
|
10591
|
+
})) continue;
|
|
10417
10592
|
const existingFileContent = await readFileContentOrNull(filePath);
|
|
10418
10593
|
if (existingFileContent !== null && aiFile.shouldMergeExistingFileContent()) aiFile.setFileContent(existingFileContent);
|
|
10419
10594
|
const contentWithNewline = addTrailingNewline(aiFile.getFileContent());
|
|
@@ -10546,6 +10721,37 @@ function mergeByIdentity({ perRoot, identity }) {
|
|
|
10546
10721
|
return order.map((key) => winnerByKey.get(key));
|
|
10547
10722
|
}
|
|
10548
10723
|
/**
|
|
10724
|
+
* Whether a write to `targetPath` has to be refused because a link on the way
|
|
10725
|
+
* there leads out of `rootPath`, warning about the refusal.
|
|
10726
|
+
*
|
|
10727
|
+
* A checked-out repository can carry a symbolic link at an output path
|
|
10728
|
+
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`), and a write
|
|
10729
|
+
* through it would land wherever the link points. Every writer that lets the
|
|
10730
|
+
* OS follow links — file, directory, and config-rewrite alike — asks this first
|
|
10731
|
+
* so that one warning describes the one behavior.
|
|
10732
|
+
*/
|
|
10733
|
+
async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
10734
|
+
if (!await writablePathEscapesRoot({
|
|
10735
|
+
rootPath,
|
|
10736
|
+
targetPath
|
|
10737
|
+
})) return false;
|
|
10738
|
+
logger.warn(`Refusing to write ${quoteForLog(targetPath)}: it resolves outside ${quoteForLog(rootPath)} through a symbolic link`);
|
|
10739
|
+
return true;
|
|
10740
|
+
}
|
|
10741
|
+
/**
|
|
10742
|
+
* {@link refusesWriteOutsideRoot} over every path a single unit of output
|
|
10743
|
+
* writes, stopping at the first refusal: a directory written as one unit is
|
|
10744
|
+
* held back whole when any one file in it would land outside the root.
|
|
10745
|
+
*/
|
|
10746
|
+
async function refusesAnyWriteOutsideRoot({ logger, rootPath, targetPaths }) {
|
|
10747
|
+
for (const targetPath of targetPaths) if (await refusesWriteOutsideRoot({
|
|
10748
|
+
logger,
|
|
10749
|
+
rootPath,
|
|
10750
|
+
targetPath
|
|
10751
|
+
})) return true;
|
|
10752
|
+
return false;
|
|
10753
|
+
}
|
|
10754
|
+
/**
|
|
10549
10755
|
* The key two spellings share when a case-insensitive filesystem would give
|
|
10550
10756
|
* them one file. `toLowerCase()` is locale-independent (unlike
|
|
10551
10757
|
* `toLocaleLowerCase`, it does not turn `I` into the Turkish `ı` under a Turkish
|
|
@@ -12219,16 +12425,16 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
12219
12425
|
let parsed;
|
|
12220
12426
|
try {
|
|
12221
12427
|
if (format === "yaml") parsed = loadYaml(fileContent);
|
|
12222
|
-
else if (format === "toml") parsed = parse$
|
|
12428
|
+
else if (format === "toml") parsed = parse$2(fileContent);
|
|
12223
12429
|
else if (format === "json") parsed = JSON.parse(fileContent);
|
|
12224
12430
|
else if (jsoncParseErrors === "error") {
|
|
12225
12431
|
const errors = [];
|
|
12226
|
-
parsed = parse(fileContent, errors, { allowTrailingComma: true });
|
|
12432
|
+
parsed = parse$1(fileContent, errors, { allowTrailingComma: true });
|
|
12227
12433
|
if (errors.length > 0) {
|
|
12228
12434
|
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
12229
12435
|
throw new Error(details);
|
|
12230
12436
|
}
|
|
12231
|
-
} else parsed = parse(fileContent);
|
|
12437
|
+
} else parsed = parse$1(fileContent);
|
|
12232
12438
|
} catch (error) {
|
|
12233
12439
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
12234
12440
|
}
|
|
@@ -12924,6 +13130,8 @@ const KIMI_CODE_HOME_CONFIG_SHARED_FILE_KEY = "config.toml";
|
|
|
12924
13130
|
const REASONIX_PROJECT_CONFIG_SHARED_FILE_KEY = "reasonix.toml";
|
|
12925
13131
|
const REASONIX_GLOBAL_CONFIG_SHARED_FILE_KEY = ".reasonix/config.toml";
|
|
12926
13132
|
const ROVODEV_CONFIG_SHARED_FILE_KEY = ".rovodev/config.yml";
|
|
13133
|
+
const POOL_PROJECT_SETTINGS_SHARED_FILE_KEY = ".poolside/settings.yaml";
|
|
13134
|
+
const POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY = ".config/poolside/settings.yaml";
|
|
12927
13135
|
/**
|
|
12928
13136
|
* Build the `SHARED_CONFIG_OWNERSHIP` lookup key from a tool's settable paths.
|
|
12929
13137
|
* Mirrors `sharedFileKey` in `src/lib/shared-file-derive.ts` (kept separate so
|
|
@@ -13005,6 +13213,38 @@ const KIMI_CODE_CONFIG_DECLARATION = {
|
|
|
13005
13213
|
}
|
|
13006
13214
|
};
|
|
13007
13215
|
/**
|
|
13216
|
+
* Pool settings (`.poolside/settings.yaml` / `~/.config/poolside/settings.yaml`):
|
|
13217
|
+
* the user's primary Pool settings file, so an unparseable root fails closed
|
|
13218
|
+
* rather than being replaced with generated output. `mcp_servers` is owned as
|
|
13219
|
+
* a whole key because the MCP writer emits the complete server map. The
|
|
13220
|
+
* permissions writer deep-merges: it rewrites the `allow`/`deny` lists of the
|
|
13221
|
+
* `tools.<name>` blocks it manages and the `paths` lists, leaving each tool's
|
|
13222
|
+
* `disabled` flag, unmanaged tools and every other key in place. `hooks` is
|
|
13223
|
+
* owned as a whole key because the hooks writer emits every event list.
|
|
13224
|
+
* `subagents` is owned as a whole key too: its writer recomputes the block
|
|
13225
|
+
* from the existing file (`default` and the built-in `general` agent carried
|
|
13226
|
+
* over, every other agent replaced) before applying the patch.
|
|
13227
|
+
*/
|
|
13228
|
+
const POOL_SETTINGS_DECLARATION = {
|
|
13229
|
+
format: "yaml",
|
|
13230
|
+
invalidRootPolicy: "error",
|
|
13231
|
+
features: {
|
|
13232
|
+
mcp: {
|
|
13233
|
+
kind: "replace-owned-keys",
|
|
13234
|
+
ownedKeys: ["mcp_servers"]
|
|
13235
|
+
},
|
|
13236
|
+
permissions: { kind: "deep-merge" },
|
|
13237
|
+
hooks: {
|
|
13238
|
+
kind: "replace-owned-keys",
|
|
13239
|
+
ownedKeys: ["hooks"]
|
|
13240
|
+
},
|
|
13241
|
+
subagents: {
|
|
13242
|
+
kind: "replace-owned-keys",
|
|
13243
|
+
ownedKeys: ["subagents"]
|
|
13244
|
+
}
|
|
13245
|
+
}
|
|
13246
|
+
};
|
|
13247
|
+
/**
|
|
13008
13248
|
* ZCode's settings file, which also carries model/theme/permission keys
|
|
13009
13249
|
* rulesync does not own. Both copies are the user's primary ZCode config, so
|
|
13010
13250
|
* every writer refuses to read-modify-write a file it could not parse rather
|
|
@@ -13109,6 +13349,8 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13109
13349
|
[HERMES_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13110
13350
|
[HERMES_WIN32_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13111
13351
|
[HERMES_HOME_CONFIG_SHARED_FILE_KEY]: HERMES_CONFIG_DECLARATION,
|
|
13352
|
+
[POOL_PROJECT_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
|
|
13353
|
+
[POOL_GLOBAL_SETTINGS_SHARED_FILE_KEY]: POOL_SETTINGS_DECLARATION,
|
|
13112
13354
|
[TAKT_CONFIG_SHARED_FILE_KEY]: {
|
|
13113
13355
|
format: "yaml",
|
|
13114
13356
|
invalidRootPolicy: "error",
|
|
@@ -19278,7 +19520,7 @@ var TabnineCommand = class TabnineCommand extends ToolCommand {
|
|
|
19278
19520
|
}
|
|
19279
19521
|
parseTomlContent(content) {
|
|
19280
19522
|
try {
|
|
19281
|
-
const parsed = parse$
|
|
19523
|
+
const parsed = parse$2(content);
|
|
19282
19524
|
const result = TabnineCommandFrontmatterSchema.safeParse(parsed);
|
|
19283
19525
|
if (!result.success) throw new Error(`Invalid frontmatter in ${join(this.relativeDirPath, this.relativeFilePath)}: ${formatError(result.error)}`);
|
|
19284
19526
|
return {
|
|
@@ -20395,6 +20637,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20395
20637
|
global: this.global,
|
|
20396
20638
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
20397
20639
|
}));
|
|
20640
|
+
if (await refusesWriteOutsideRoot({
|
|
20641
|
+
logger: this.logger,
|
|
20642
|
+
rootPath: this.outputRoot,
|
|
20643
|
+
targetPath: configPath
|
|
20644
|
+
})) return changedCount;
|
|
20398
20645
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20399
20646
|
if (currentContent === null) return changedCount;
|
|
20400
20647
|
const nextContent = getDisabledHermesCommandsPluginConfigContent({
|
|
@@ -20417,6 +20664,11 @@ var CommandsProcessor = class extends FeatureProcessor {
|
|
|
20417
20664
|
if (this.toolTarget !== "goose" || !this.global) return 0;
|
|
20418
20665
|
if (generatedFiles.some((file) => file instanceof GooseCommand)) return 0;
|
|
20419
20666
|
const configPath = join(this.outputRoot, GOOSE_GLOBAL_DIR, GOOSE_MCP_FILE_NAME);
|
|
20667
|
+
if (await refusesWriteOutsideRoot({
|
|
20668
|
+
logger: this.logger,
|
|
20669
|
+
rootPath: this.outputRoot,
|
|
20670
|
+
targetPath: configPath
|
|
20671
|
+
})) return 0;
|
|
20420
20672
|
const currentContent = await readFileContentOrNull(configPath);
|
|
20421
20673
|
if (currentContent === null || !hasManagedGooseSlashCommands(currentContent)) return 0;
|
|
20422
20674
|
const nextContent = getGooseSlashCommandsConfigContent({
|
|
@@ -27516,6 +27768,302 @@ var PiHooks = class PiHooks extends ToolHooks {
|
|
|
27516
27768
|
}
|
|
27517
27769
|
};
|
|
27518
27770
|
//#endregion
|
|
27771
|
+
//#region src/constants/pool-paths.ts
|
|
27772
|
+
/**
|
|
27773
|
+
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
27774
|
+
*
|
|
27775
|
+
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
27776
|
+
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
27777
|
+
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
27778
|
+
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
27779
|
+
* `AGENTS.md` files from the repository root down through the working
|
|
27780
|
+
* directory, deeper files taking precedence. It skips ignored directories
|
|
27781
|
+
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
27782
|
+
*
|
|
27783
|
+
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
27784
|
+
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
27785
|
+
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
27786
|
+
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
27787
|
+
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
27788
|
+
*
|
|
27789
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
27790
|
+
* @see https://docs.poolside.ai/skills
|
|
27791
|
+
* @see https://github.com/poolsideai/pool
|
|
27792
|
+
*/
|
|
27793
|
+
/** Project-scoped `.poolside/` directory at the project root. */
|
|
27794
|
+
const POOL_DIR = ".poolside";
|
|
27795
|
+
/** Global config directory for Pool, relative to the home directory. */
|
|
27796
|
+
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
27797
|
+
/** Project skills root, relative to the project root. */
|
|
27798
|
+
const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
|
|
27799
|
+
/** Global skills root, relative to the home directory. */
|
|
27800
|
+
const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
|
|
27801
|
+
/**
|
|
27802
|
+
* Pool's settings file name. MCP servers live under the top-level
|
|
27803
|
+
* `mcp_servers` key of `.poolside/settings.yaml` (project, committed) and
|
|
27804
|
+
* `~/.config/poolside/settings.yaml` (global); the untracked
|
|
27805
|
+
* `.poolside/settings.local.yaml` overlay is left to the user.
|
|
27806
|
+
*
|
|
27807
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
27808
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
27809
|
+
*/
|
|
27810
|
+
const POOL_SETTINGS_FILE_NAME = "settings.yaml";
|
|
27811
|
+
/** Pool's untracked per-user overlay next to the settings file; never written. */
|
|
27812
|
+
const POOL_SETTINGS_LOCAL_FILE_NAME = "settings.local.yaml";
|
|
27813
|
+
/** Top-level key of Pool's settings file that holds the MCP server map. */
|
|
27814
|
+
const POOL_MCP_SERVERS_KEY = "mcp_servers";
|
|
27815
|
+
/** Top-level key of Pool's settings file that holds per-tool permission rules. */
|
|
27816
|
+
const POOL_TOOLS_KEY = "tools";
|
|
27817
|
+
/** Top-level key of Pool's settings file that holds file access rules. */
|
|
27818
|
+
const POOL_PATHS_KEY = "paths";
|
|
27819
|
+
/** Top-level key of Pool's settings file that holds the per-event hook lists. */
|
|
27820
|
+
const POOL_HOOKS_KEY = "hooks";
|
|
27821
|
+
/**
|
|
27822
|
+
* Top-level key of Pool's settings file that holds the subagent registry:
|
|
27823
|
+
* `subagents.default` names the agent delegated to by default and
|
|
27824
|
+
* `subagents.agents.<name>` defines each agent.
|
|
27825
|
+
*
|
|
27826
|
+
* @see https://docs.poolside.ai/subagents
|
|
27827
|
+
*/
|
|
27828
|
+
const POOL_SUBAGENTS_KEY = "subagents";
|
|
27829
|
+
/** Key under `subagents` that holds the per-agent definitions. */
|
|
27830
|
+
const POOL_SUBAGENTS_AGENTS_KEY = "agents";
|
|
27831
|
+
/** Pool's reserved built-in agent, always `in_process` and never generated. */
|
|
27832
|
+
const POOL_GENERAL_AGENT_NAME = "general";
|
|
27833
|
+
//#endregion
|
|
27834
|
+
//#region src/features/hooks/pool-hooks.ts
|
|
27835
|
+
/** The matcher Pool reads as "any tool"; `""` means the same. */
|
|
27836
|
+
const POOL_MATCH_ALL = "*";
|
|
27837
|
+
const SUPPORTED_POOL_EVENTS = new Set(POOL_HOOK_EVENTS);
|
|
27838
|
+
/**
|
|
27839
|
+
* The Pool events that test `matcher` against a tool name. Pool ignores the
|
|
27840
|
+
* field on every other event (it stays required by the schema), so an
|
|
27841
|
+
* authored matcher there is dropped with a warning and `*` written instead.
|
|
27842
|
+
*/
|
|
27843
|
+
const POOL_MATCHER_EVENTS = /* @__PURE__ */ new Set(["PreToolUse", "PostToolUse"]);
|
|
27844
|
+
/**
|
|
27845
|
+
* Single spelling of the settings.yaml codec/policy, matching PoolMcp and
|
|
27846
|
+
* PoolPermissions: fail closed on an unparseable root rather than replacing
|
|
27847
|
+
* the user's primary Pool settings with generated output.
|
|
27848
|
+
*/
|
|
27849
|
+
function parsePoolSettings$3(fileContent, filePath) {
|
|
27850
|
+
return parseSharedConfig({
|
|
27851
|
+
format: "yaml",
|
|
27852
|
+
fileContent,
|
|
27853
|
+
filePath,
|
|
27854
|
+
invalidRootPolicy: "error"
|
|
27855
|
+
});
|
|
27856
|
+
}
|
|
27857
|
+
/**
|
|
27858
|
+
* Build the `hooks` event lists of Pool's settings file from a canonical hooks
|
|
27859
|
+
* config. Pool keys a flat list of `{name, matcher, command, timeout}` entries
|
|
27860
|
+
* by event name. Only `type: "command"` canonical hooks are emitted, since a
|
|
27861
|
+
* Pool hook is a shell command. A shared canonical event Pool does not fire is
|
|
27862
|
+
* skipped (the HooksProcessor reports it), while an event under the
|
|
27863
|
+
* `pool.hooks` override — such as one an import filed there — is written
|
|
27864
|
+
* verbatim so it round-trips.
|
|
27865
|
+
*/
|
|
27866
|
+
function canonicalToPoolHooks({ config, toolOverride, logger }) {
|
|
27867
|
+
const sharedHooks = {};
|
|
27868
|
+
for (const [event, defs] of Object.entries(config.hooks)) if (SUPPORTED_POOL_EVENTS.has(event)) sharedHooks[event] = defs;
|
|
27869
|
+
const effective = {
|
|
27870
|
+
...sharedHooks,
|
|
27871
|
+
...toolOverride
|
|
27872
|
+
};
|
|
27873
|
+
const hooks = {};
|
|
27874
|
+
for (const [event, defs] of Object.entries(effective)) {
|
|
27875
|
+
if (isPrototypePollutionKey(event)) continue;
|
|
27876
|
+
const poolEvent = lookupOwn({
|
|
27877
|
+
record: CANONICAL_TO_POOL_EVENT_NAMES,
|
|
27878
|
+
key: event
|
|
27879
|
+
}) ?? event;
|
|
27880
|
+
const entries = [];
|
|
27881
|
+
for (const def of defs) {
|
|
27882
|
+
const entry = canonicalDefToPoolEntry({
|
|
27883
|
+
def,
|
|
27884
|
+
event,
|
|
27885
|
+
poolEvent,
|
|
27886
|
+
logger
|
|
27887
|
+
});
|
|
27888
|
+
if (entry !== null) entries.push(entry);
|
|
27889
|
+
}
|
|
27890
|
+
if (entries.length > 0) hooks[poolEvent] = entries;
|
|
27891
|
+
}
|
|
27892
|
+
return hooks;
|
|
27893
|
+
}
|
|
27894
|
+
/** Convert one canonical hook definition to a Pool entry, or null to skip. */
|
|
27895
|
+
function canonicalDefToPoolEntry({ def, event, poolEvent, logger }) {
|
|
27896
|
+
if ((def.type ?? "command") !== "command") return null;
|
|
27897
|
+
if (typeof def.command !== "string" || def.command === "") {
|
|
27898
|
+
logger?.warn(`Pool hook ${quoteValueForWarning(def.name ?? "")} under "${event}" has no "command", which Pool would reject, so it was skipped.`);
|
|
27899
|
+
return null;
|
|
27900
|
+
}
|
|
27901
|
+
const authoredMatcher = typeof def.matcher === "string" && def.matcher !== "" ? def.matcher : POOL_MATCH_ALL;
|
|
27902
|
+
let matcher = authoredMatcher;
|
|
27903
|
+
if (!POOL_MATCHER_EVENTS.has(poolEvent) && authoredMatcher !== POOL_MATCH_ALL) {
|
|
27904
|
+
logger?.warn(`Pool ignores "matcher" on "${poolEvent}", so the matcher ${quoteValueForWarning(authoredMatcher)} of the hook ${quoteValueForWarning(def.name ?? def.command)} under "${event}" was dropped.`);
|
|
27905
|
+
matcher = POOL_MATCH_ALL;
|
|
27906
|
+
}
|
|
27907
|
+
const entry = {
|
|
27908
|
+
...typeof def.name === "string" && def.name !== "" ? { name: def.name } : {},
|
|
27909
|
+
matcher,
|
|
27910
|
+
command: def.command
|
|
27911
|
+
};
|
|
27912
|
+
if (typeof def.timeout === "number") entry.timeout = Math.ceil(def.timeout);
|
|
27913
|
+
return entry;
|
|
27914
|
+
}
|
|
27915
|
+
/** Convert one raw hook entry to a canonical definition, or null to skip. */
|
|
27916
|
+
function poolEntryToCanonicalDef({ raw, poolEvent }) {
|
|
27917
|
+
if (!isRecord$1(raw) || typeof raw.command !== "string" || raw.command === "") return null;
|
|
27918
|
+
const def = {
|
|
27919
|
+
type: "command",
|
|
27920
|
+
command: raw.command
|
|
27921
|
+
};
|
|
27922
|
+
if (typeof raw.name === "string" && raw.name !== "") def.name = raw.name;
|
|
27923
|
+
if (POOL_MATCHER_EVENTS.has(poolEvent) && typeof raw.matcher === "string" && raw.matcher !== "" && raw.matcher !== POOL_MATCH_ALL) def.matcher = raw.matcher;
|
|
27924
|
+
if (typeof raw.timeout === "number") def.timeout = raw.timeout;
|
|
27925
|
+
return def;
|
|
27926
|
+
}
|
|
27927
|
+
/**
|
|
27928
|
+
* Reverse {@link canonicalToPoolHooks}: parse the event lists of the `hooks`
|
|
27929
|
+
* block back into a canonical event → definition[] record. An event Pool does
|
|
27930
|
+
* not document is carried under its own name so `buildImportedHooksConfig`
|
|
27931
|
+
* files it under the `pool.hooks` override; a non-list sibling such as
|
|
27932
|
+
* `stop_hook_max_continuations` is not a hook and is left alone.
|
|
27933
|
+
*/
|
|
27934
|
+
function poolHooksToCanonical(hooksBlock) {
|
|
27935
|
+
const canonical = {};
|
|
27936
|
+
if (!isRecord$1(hooksBlock)) return canonical;
|
|
27937
|
+
for (const [poolEvent, rawEntries] of Object.entries(hooksBlock)) {
|
|
27938
|
+
if (isPrototypePollutionKey(poolEvent) || !Array.isArray(rawEntries)) continue;
|
|
27939
|
+
const canonicalEvent = lookupOwn({
|
|
27940
|
+
record: POOL_TO_CANONICAL_EVENT_NAMES,
|
|
27941
|
+
key: poolEvent
|
|
27942
|
+
}) ?? poolEvent;
|
|
27943
|
+
const defs = rawEntries.map((raw) => poolEntryToCanonicalDef({
|
|
27944
|
+
raw,
|
|
27945
|
+
poolEvent
|
|
27946
|
+
})).filter((def) => def !== null);
|
|
27947
|
+
if (defs.length === 0) continue;
|
|
27948
|
+
canonical[canonicalEvent] = [...lookupOwn({
|
|
27949
|
+
record: canonical,
|
|
27950
|
+
key: canonicalEvent
|
|
27951
|
+
}) ?? [], ...defs];
|
|
27952
|
+
}
|
|
27953
|
+
return canonical;
|
|
27954
|
+
}
|
|
27955
|
+
/**
|
|
27956
|
+
* The keys of an existing `hooks` block that are not event lists —
|
|
27957
|
+
* `stop_hook_max_continuations` and anything else Pool may add beside the
|
|
27958
|
+
* events — which survive a regenerate untouched.
|
|
27959
|
+
*/
|
|
27960
|
+
function nonEventHookKeys(hooksBlock) {
|
|
27961
|
+
const kept = {};
|
|
27962
|
+
if (!isRecord$1(hooksBlock)) return kept;
|
|
27963
|
+
for (const [key, value] of Object.entries(hooksBlock)) if (!isPrototypePollutionKey(key) && !Array.isArray(value)) kept[key] = value;
|
|
27964
|
+
return kept;
|
|
27965
|
+
}
|
|
27966
|
+
/**
|
|
27967
|
+
* Pool hooks.
|
|
27968
|
+
*
|
|
27969
|
+
* Pool reads hooks from the `hooks` key of its settings file —
|
|
27970
|
+
* `.poolside/settings.yaml` at project scope and
|
|
27971
|
+
* `~/.config/poolside/settings.yaml` at user scope — as
|
|
27972
|
+
* `hooks.<Event>: [{name?, matcher, command, timeout?}]` for `PreToolUse`,
|
|
27973
|
+
* `PostToolUse`, `UserPromptSubmit`, `PreCompact`, `SessionStart` and `Stop`.
|
|
27974
|
+
* The event lists are rewritten as a whole; the block's non-list sibling
|
|
27975
|
+
* `stop_hook_max_continuations` and every other top-level key of the file
|
|
27976
|
+
* (`mcp_servers`, `tools`, `paths`, `pool`, ...) are preserved, and the file
|
|
27977
|
+
* is never deleted.
|
|
27978
|
+
*
|
|
27979
|
+
* @see https://docs.poolside.ai/hooks
|
|
27980
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
27981
|
+
*/
|
|
27982
|
+
var PoolHooks = class PoolHooks extends ToolHooks {
|
|
27983
|
+
settings;
|
|
27984
|
+
constructor(params) {
|
|
27985
|
+
super(params);
|
|
27986
|
+
this.settings = parsePoolSettings$3(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
27987
|
+
}
|
|
27988
|
+
getSettings() {
|
|
27989
|
+
return this.settings;
|
|
27990
|
+
}
|
|
27991
|
+
isDeletable() {
|
|
27992
|
+
return false;
|
|
27993
|
+
}
|
|
27994
|
+
static getSettablePaths({ global = false } = {}) {
|
|
27995
|
+
return {
|
|
27996
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
27997
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
27998
|
+
};
|
|
27999
|
+
}
|
|
28000
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
28001
|
+
const paths = this.getSettablePaths({ global });
|
|
28002
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
28003
|
+
return new PoolHooks({
|
|
28004
|
+
outputRoot,
|
|
28005
|
+
relativeDirPath: paths.relativeDirPath,
|
|
28006
|
+
relativeFilePath: paths.relativeFilePath,
|
|
28007
|
+
fileContent,
|
|
28008
|
+
validate,
|
|
28009
|
+
global
|
|
28010
|
+
});
|
|
28011
|
+
}
|
|
28012
|
+
static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
|
|
28013
|
+
const paths = this.getSettablePaths({ global });
|
|
28014
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
28015
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
28016
|
+
const existing = parsePoolSettings$3(existingContent, filePath);
|
|
28017
|
+
const config = rulesyncHooks.getJson();
|
|
28018
|
+
const hooks = canonicalToPoolHooks({
|
|
28019
|
+
config,
|
|
28020
|
+
toolOverride: config.pool?.hooks,
|
|
28021
|
+
logger
|
|
28022
|
+
});
|
|
28023
|
+
return new PoolHooks({
|
|
28024
|
+
outputRoot,
|
|
28025
|
+
relativeDirPath: paths.relativeDirPath,
|
|
28026
|
+
relativeFilePath: paths.relativeFilePath,
|
|
28027
|
+
fileContent: applySharedConfigPatch({
|
|
28028
|
+
fileKey: sharedConfigFileKey(paths),
|
|
28029
|
+
feature: "hooks",
|
|
28030
|
+
existingContent,
|
|
28031
|
+
patch: { [POOL_HOOKS_KEY]: {
|
|
28032
|
+
...nonEventHookKeys(existing[POOL_HOOKS_KEY]),
|
|
28033
|
+
...hooks
|
|
28034
|
+
} },
|
|
28035
|
+
filePath,
|
|
28036
|
+
logger
|
|
28037
|
+
}),
|
|
28038
|
+
validate,
|
|
28039
|
+
global
|
|
28040
|
+
});
|
|
28041
|
+
}
|
|
28042
|
+
toRulesyncHooks() {
|
|
28043
|
+
const hooks = poolHooksToCanonical(this.settings[POOL_HOOKS_KEY]);
|
|
28044
|
+
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify(buildImportedHooksConfig({
|
|
28045
|
+
hooks,
|
|
28046
|
+
overrideKey: "pool"
|
|
28047
|
+
}), null, 2) });
|
|
28048
|
+
}
|
|
28049
|
+
validate() {
|
|
28050
|
+
return {
|
|
28051
|
+
success: true,
|
|
28052
|
+
error: null
|
|
28053
|
+
};
|
|
28054
|
+
}
|
|
28055
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
28056
|
+
return new PoolHooks({
|
|
28057
|
+
outputRoot,
|
|
28058
|
+
relativeDirPath,
|
|
28059
|
+
relativeFilePath,
|
|
28060
|
+
fileContent: "",
|
|
28061
|
+
validate: false,
|
|
28062
|
+
global
|
|
28063
|
+
});
|
|
28064
|
+
}
|
|
28065
|
+
};
|
|
28066
|
+
//#endregion
|
|
27519
28067
|
//#region src/features/hooks/qwencode-hooks.ts
|
|
27520
28068
|
/**
|
|
27521
28069
|
* Build a single Qwen Code hook object from a canonical hook definition.
|
|
@@ -28629,14 +29177,15 @@ const HooksProcessorToolTargetSchema = z.enum(hooksProcessorToolTargetTuple);
|
|
|
28629
29177
|
/**
|
|
28630
29178
|
* Event names present in the config that the target's adapter cannot emit.
|
|
28631
29179
|
*
|
|
28632
|
-
*
|
|
28633
|
-
*
|
|
28634
|
-
*
|
|
29180
|
+
* Shared (`hooks`) keys are always checked. Override-block keys are only
|
|
29181
|
+
* checked when the factory declares `dropsUnknownOverrideEvents`; otherwise
|
|
29182
|
+
* the adapter either emits them verbatim or reports its own drops, and a
|
|
29183
|
+
* generic "not supported" warning would be wrong or duplicated.
|
|
28635
29184
|
*/
|
|
28636
29185
|
function unsupportedEventNames(params) {
|
|
28637
29186
|
const { factory, sharedHooks, effectiveHooks } = params;
|
|
28638
29187
|
const supportedEvents = new Set(factory.supportedEvents);
|
|
28639
|
-
const eventNames = factory.
|
|
29188
|
+
const eventNames = factory.dropsUnknownOverrideEvents ? Object.keys(effectiveHooks) : Object.keys(sharedHooks);
|
|
28640
29189
|
return [...new Set(eventNames)].filter((e) => !supportedEvents.has(e));
|
|
28641
29190
|
}
|
|
28642
29191
|
/**
|
|
@@ -28729,7 +29278,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28729
29278
|
supportedEvents: AMP_HOOK_EVENTS,
|
|
28730
29279
|
supportedHookTypes: ["command"],
|
|
28731
29280
|
supportsMatcher: true,
|
|
28732
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29281
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29282
|
+
dropsUnknownOverrideEvents: true
|
|
28733
29283
|
}],
|
|
28734
29284
|
["antigravity-cli", {
|
|
28735
29285
|
class: AntigravityCliHooks,
|
|
@@ -28762,7 +29312,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28762
29312
|
},
|
|
28763
29313
|
supportedEvents: ANTIGRAVITY_HOOK_EVENTS,
|
|
28764
29314
|
supportedHookTypes: ["command"],
|
|
28765
|
-
supportsMatcher: true
|
|
29315
|
+
supportsMatcher: true,
|
|
29316
|
+
dropsUnknownOverrideEvents: true
|
|
28766
29317
|
}],
|
|
28767
29318
|
["cursor", {
|
|
28768
29319
|
class: CursorHooks,
|
|
@@ -28807,7 +29358,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28807
29358
|
"mcp_tool",
|
|
28808
29359
|
"agent"
|
|
28809
29360
|
],
|
|
28810
|
-
supportsMatcher: true
|
|
29361
|
+
supportsMatcher: true,
|
|
29362
|
+
dropsUnknownOverrideEvents: true
|
|
28811
29363
|
}],
|
|
28812
29364
|
["codexcli", {
|
|
28813
29365
|
class: CodexcliHooks,
|
|
@@ -28868,7 +29420,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28868
29420
|
supportedEvents: KILO_HOOK_EVENTS,
|
|
28869
29421
|
supportedHookTypes: ["command"],
|
|
28870
29422
|
supportsMatcher: true,
|
|
28871
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29423
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29424
|
+
dropsUnknownOverrideEvents: true
|
|
28872
29425
|
}],
|
|
28873
29426
|
["opencode", {
|
|
28874
29427
|
class: OpencodeHooks,
|
|
@@ -28880,7 +29433,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28880
29433
|
supportedEvents: OPENCODE_HOOK_EVENTS,
|
|
28881
29434
|
supportedHookTypes: ["command"],
|
|
28882
29435
|
supportsMatcher: true,
|
|
28883
|
-
matcherEvents: ["preToolUse", "postToolUse"]
|
|
29436
|
+
matcherEvents: ["preToolUse", "postToolUse"],
|
|
29437
|
+
dropsUnknownOverrideEvents: true
|
|
28884
29438
|
}],
|
|
28885
29439
|
["pi", {
|
|
28886
29440
|
class: PiHooks,
|
|
@@ -28891,7 +29445,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28891
29445
|
},
|
|
28892
29446
|
supportedEvents: PI_HOOK_EVENTS,
|
|
28893
29447
|
supportedHookTypes: ["command"],
|
|
28894
|
-
supportsMatcher: true
|
|
29448
|
+
supportsMatcher: true,
|
|
29449
|
+
dropsUnknownOverrideEvents: true
|
|
28895
29450
|
}],
|
|
28896
29451
|
["factorydroid", {
|
|
28897
29452
|
class: FactorydroidHooks,
|
|
@@ -28913,7 +29468,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28913
29468
|
},
|
|
28914
29469
|
supportedEvents: CLINE_HOOK_EVENTS,
|
|
28915
29470
|
supportedHookTypes: ["command"],
|
|
28916
|
-
supportsMatcher: false
|
|
29471
|
+
supportsMatcher: false,
|
|
29472
|
+
dropsUnknownOverrideEvents: true
|
|
28917
29473
|
}],
|
|
28918
29474
|
["goose", {
|
|
28919
29475
|
class: GooseHooks,
|
|
@@ -28935,8 +29491,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28935
29491
|
},
|
|
28936
29492
|
supportedEvents: HERMESAGENT_HOOK_EVENTS,
|
|
28937
29493
|
supportedHookTypes: ["command"],
|
|
28938
|
-
supportsMatcher: true
|
|
28939
|
-
passthroughOverrideEvents: true
|
|
29494
|
+
supportsMatcher: true
|
|
28940
29495
|
}],
|
|
28941
29496
|
["kimi-code", {
|
|
28942
29497
|
class: KimiCodeHooks,
|
|
@@ -28947,8 +29502,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28947
29502
|
},
|
|
28948
29503
|
supportedEvents: KIMI_CODE_HOOK_EVENTS,
|
|
28949
29504
|
supportedHookTypes: ["command"],
|
|
28950
|
-
supportsMatcher: true
|
|
28951
|
-
passthroughOverrideEvents: true
|
|
29505
|
+
supportsMatcher: true
|
|
28952
29506
|
}],
|
|
28953
29507
|
["deepagents", {
|
|
28954
29508
|
class: DeepagentsHooks,
|
|
@@ -28959,7 +29513,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28959
29513
|
},
|
|
28960
29514
|
supportedEvents: DEEPAGENTS_HOOK_EVENTS,
|
|
28961
29515
|
supportedHookTypes: ["command"],
|
|
28962
|
-
supportsMatcher: true
|
|
29516
|
+
supportsMatcher: true,
|
|
29517
|
+
dropsUnknownOverrideEvents: true
|
|
28963
29518
|
}],
|
|
28964
29519
|
["kiro", {
|
|
28965
29520
|
class: KiroHooks,
|
|
@@ -28970,8 +29525,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28970
29525
|
},
|
|
28971
29526
|
supportedEvents: KIRO_HOOK_EVENTS,
|
|
28972
29527
|
supportedHookTypes: ["command"],
|
|
28973
|
-
supportsMatcher: true
|
|
28974
|
-
passthroughOverrideEvents: true
|
|
29528
|
+
supportsMatcher: true
|
|
28975
29529
|
}],
|
|
28976
29530
|
["kiro-cli", {
|
|
28977
29531
|
class: KiroCliHooks,
|
|
@@ -28982,8 +29536,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28982
29536
|
},
|
|
28983
29537
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
28984
29538
|
supportedHookTypes: ["command", "prompt"],
|
|
28985
|
-
supportsMatcher: true
|
|
28986
|
-
passthroughOverrideEvents: true
|
|
29539
|
+
supportsMatcher: true
|
|
28987
29540
|
}],
|
|
28988
29541
|
["kiro-ide", {
|
|
28989
29542
|
class: KiroIdeHooks,
|
|
@@ -28994,8 +29547,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
28994
29547
|
},
|
|
28995
29548
|
supportedEvents: KIRO_IDE_HOOK_EVENTS,
|
|
28996
29549
|
supportedHookTypes: ["command", "prompt"],
|
|
28997
|
-
supportsMatcher: true
|
|
28998
|
-
passthroughOverrideEvents: true
|
|
29550
|
+
supportsMatcher: true
|
|
28999
29551
|
}],
|
|
29000
29552
|
["devin", {
|
|
29001
29553
|
class: DevinHooks,
|
|
@@ -29113,6 +29665,18 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29113
29665
|
},
|
|
29114
29666
|
supportedEvents: VIBE_HOOK_EVENTS,
|
|
29115
29667
|
supportedHookTypes: ["command"],
|
|
29668
|
+
supportsMatcher: true,
|
|
29669
|
+
dropsUnknownOverrideEvents: true
|
|
29670
|
+
}],
|
|
29671
|
+
["pool", {
|
|
29672
|
+
class: PoolHooks,
|
|
29673
|
+
meta: {
|
|
29674
|
+
supportsProject: true,
|
|
29675
|
+
supportsGlobal: true,
|
|
29676
|
+
supportsImport: true
|
|
29677
|
+
},
|
|
29678
|
+
supportedEvents: POOL_HOOK_EVENTS,
|
|
29679
|
+
supportedHookTypes: ["command"],
|
|
29116
29680
|
supportsMatcher: true
|
|
29117
29681
|
}],
|
|
29118
29682
|
["qwencode", {
|
|
@@ -29140,7 +29704,8 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29140
29704
|
},
|
|
29141
29705
|
supportedEvents: REASONIX_HOOK_EVENTS,
|
|
29142
29706
|
supportedHookTypes: ["command"],
|
|
29143
|
-
supportsMatcher: true
|
|
29707
|
+
supportsMatcher: true,
|
|
29708
|
+
dropsUnknownOverrideEvents: true
|
|
29144
29709
|
}],
|
|
29145
29710
|
["grokcli", {
|
|
29146
29711
|
class: GrokcliHooks,
|
|
@@ -29162,8 +29727,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
29162
29727
|
},
|
|
29163
29728
|
supportedEvents: CRUSH_HOOK_EVENTS,
|
|
29164
29729
|
supportedHookTypes: ["command"],
|
|
29165
|
-
supportsMatcher: true
|
|
29166
|
-
passthroughOverrideEvents: true
|
|
29730
|
+
supportsMatcher: true
|
|
29167
29731
|
}],
|
|
29168
29732
|
["zcode", {
|
|
29169
29733
|
class: ZcodeHooks,
|
|
@@ -35797,7 +36361,7 @@ function convertServerToKiloFormat(serverName, serverConfig, existingEntry, logg
|
|
|
35797
36361
|
* that is not an object at all costs nothing.
|
|
35798
36362
|
*/
|
|
35799
36363
|
function readExistingKiloMcpEntries(fileContent) {
|
|
35800
|
-
const mcp = parse(fileContent || "{}")?.mcp;
|
|
36364
|
+
const mcp = parse$1(fileContent || "{}")?.mcp;
|
|
35801
36365
|
if (!isRecord$1(mcp)) return {};
|
|
35802
36366
|
const entries = {};
|
|
35803
36367
|
for (const [serverName, entry] of Object.entries(mcp)) {
|
|
@@ -35821,7 +36385,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
35821
36385
|
json;
|
|
35822
36386
|
constructor(params) {
|
|
35823
36387
|
super(params);
|
|
35824
|
-
this.json = KiloConfigSchema.parse(parse(this.fileContent || "{}"));
|
|
36388
|
+
this.json = KiloConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
35825
36389
|
}
|
|
35826
36390
|
getJson() {
|
|
35827
36391
|
return this.json;
|
|
@@ -35889,7 +36453,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
35889
36453
|
outputRoot,
|
|
35890
36454
|
global
|
|
35891
36455
|
});
|
|
35892
|
-
const json = parse(fileContent ?? "{\"mcp\":{}}");
|
|
36456
|
+
const json = parse$1(fileContent ?? "{\"mcp\":{}}");
|
|
35893
36457
|
const newJson = {
|
|
35894
36458
|
...json,
|
|
35895
36459
|
mcp: json.mcp ?? {}
|
|
@@ -35960,7 +36524,7 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
35960
36524
|
if (fileContent) relativeFilePath = KILO_JSON_FILE_NAME;
|
|
35961
36525
|
}
|
|
35962
36526
|
if (instructions.length === 0 && fileContent === null) return null;
|
|
35963
|
-
const json = fileContent ? parse(fileContent) : {};
|
|
36527
|
+
const json = fileContent ? parse$1(fileContent) : {};
|
|
35964
36528
|
const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
|
|
35965
36529
|
const managedPrefix = `${toPosixPath(join(KILO_DIR, KILO_RULES_DIR_NAME))}/`;
|
|
35966
36530
|
const preservedInstructions = existingInstructions.filter((entry) => !toPosixPath(entry).replace(/^\.\//, "").startsWith(managedPrefix));
|
|
@@ -36924,7 +37488,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
36924
37488
|
json;
|
|
36925
37489
|
constructor(params) {
|
|
36926
37490
|
super(params);
|
|
36927
|
-
this.json = OpencodeConfigSchema.parse(parse(this.fileContent || "{}"));
|
|
37491
|
+
this.json = OpencodeConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
36928
37492
|
}
|
|
36929
37493
|
getJson() {
|
|
36930
37494
|
return this.json;
|
|
@@ -36957,7 +37521,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
36957
37521
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
36958
37522
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
36959
37523
|
}
|
|
36960
|
-
const json = parse(fileContent ?? "{\"mcp\":{}}");
|
|
37524
|
+
const json = parse$1(fileContent ?? "{\"mcp\":{}}");
|
|
36961
37525
|
const newJson = {
|
|
36962
37526
|
...json,
|
|
36963
37527
|
mcp: json.mcp ?? {}
|
|
@@ -36986,7 +37550,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
36986
37550
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
36987
37551
|
replacement: "{env:$1}"
|
|
36988
37552
|
});
|
|
36989
|
-
const existingMcp = OpencodeConfigSchema.safeParse(parse(fileContent || "{}"));
|
|
37553
|
+
const existingMcp = OpencodeConfigSchema.safeParse(parse$1(fileContent || "{}"));
|
|
36990
37554
|
const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, logger);
|
|
36991
37555
|
return new OpencodeMcp({
|
|
36992
37556
|
outputRoot,
|
|
@@ -37037,7 +37601,7 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37037
37601
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
37038
37602
|
}
|
|
37039
37603
|
if (instructions.length === 0 && fileContent === null) return null;
|
|
37040
|
-
const json = fileContent ? parse(fileContent) : {};
|
|
37604
|
+
const json = fileContent ? parse$1(fileContent) : {};
|
|
37041
37605
|
const existingInstructions = Array.isArray(json.instructions) ? json.instructions.filter((entry) => typeof entry === "string") : [];
|
|
37042
37606
|
const managedPrefixes = global ? ["memories/", `${configDirPrefix}memories/`] : [`${toPosixPath(OPENCODE_DIR)}/memories/`];
|
|
37043
37607
|
const preservedInstructions = existingInstructions.filter((entry) => {
|
|
@@ -37094,6 +37658,320 @@ var OpencodeMcp = class OpencodeMcp extends ToolMcp {
|
|
|
37094
37658
|
}
|
|
37095
37659
|
};
|
|
37096
37660
|
//#endregion
|
|
37661
|
+
//#region src/features/mcp/pool-mcp.ts
|
|
37662
|
+
/**
|
|
37663
|
+
* Single spelling of the settings.yaml codec/policy, matching the
|
|
37664
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
37665
|
+
* unparseable root rather than replacing the user's primary Pool settings with
|
|
37666
|
+
* generated output.
|
|
37667
|
+
*/
|
|
37668
|
+
function parsePoolSettings$2(fileContent, filePath) {
|
|
37669
|
+
return parseSharedConfig({
|
|
37670
|
+
format: "yaml",
|
|
37671
|
+
fileContent,
|
|
37672
|
+
filePath,
|
|
37673
|
+
invalidRootPolicy: "error"
|
|
37674
|
+
});
|
|
37675
|
+
}
|
|
37676
|
+
/**
|
|
37677
|
+
* The remote transports Pool's `transport.type` accepts. `streamable-http` is
|
|
37678
|
+
* the canonical rulesync name for what Pool calls `http`; everything else is
|
|
37679
|
+
* left to the caller to reject.
|
|
37680
|
+
*
|
|
37681
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37682
|
+
*/
|
|
37683
|
+
function asPoolRemoteType(stated, url) {
|
|
37684
|
+
if (stated === "sse") return "sse";
|
|
37685
|
+
if (stated === "http" || stated === "streamable-http") return "http";
|
|
37686
|
+
if (stated === void 0) return /^wss?:\/\//i.test(url) ? void 0 : "http";
|
|
37687
|
+
}
|
|
37688
|
+
/**
|
|
37689
|
+
* Pool spells remote headers as a list of `"Name: value"` strings rather than
|
|
37690
|
+
* a map, so the canonical record is flattened at generate time and split back
|
|
37691
|
+
* at the first `:` on import. A list entry without a `:` names no header and
|
|
37692
|
+
* is dropped with a warning — silently losing what may be a mistyped auth
|
|
37693
|
+
* header would leave the canonical file short of one without a trace.
|
|
37694
|
+
*/
|
|
37695
|
+
function headersRecordToPoolList(headers) {
|
|
37696
|
+
return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
|
|
37697
|
+
}
|
|
37698
|
+
function poolHeadersListToRecord(serverName, headers) {
|
|
37699
|
+
if (!isStringArray$2(headers)) return void 0;
|
|
37700
|
+
const result = {};
|
|
37701
|
+
for (const entry of headers) {
|
|
37702
|
+
const separator = entry.indexOf(":");
|
|
37703
|
+
const name = separator > 0 ? entry.slice(0, separator).trim() : "";
|
|
37704
|
+
if (name === "" || PROTOTYPE_POLLUTION_KEYS.has(name)) {
|
|
37705
|
+
warnWithFallback(void 0, `Ignored malformed header ${quoteValueForWarning(entry)} in Pool MCP server ${quoteValueForWarning(serverName)}: expected "Name: value"`);
|
|
37706
|
+
continue;
|
|
37707
|
+
}
|
|
37708
|
+
result[name] = entry.slice(separator + 1).trim();
|
|
37709
|
+
}
|
|
37710
|
+
return Object.keys(result).length > 0 ? result : void 0;
|
|
37711
|
+
}
|
|
37712
|
+
/**
|
|
37713
|
+
* Pool's `deny`/`enabled_tools` entries are glob patterns, while the canonical
|
|
37714
|
+
* `enabledTools`/`disabledTools` lists are literal tool names that the other
|
|
37715
|
+
* targets match verbatim. A pattern therefore imports as a name that matches
|
|
37716
|
+
* nothing elsewhere — a `deny` that quietly stops denying is the case worth
|
|
37717
|
+
* calling out — so it is copied as-is but warned about. `allow` is exempt: it
|
|
37718
|
+
* imports to the Pool-only `poolAllow`, where globs are exactly what Pool
|
|
37719
|
+
* expects and no other tool ever reads them.
|
|
37720
|
+
*/
|
|
37721
|
+
const GLOB_METACHARACTERS = /[*?[]/;
|
|
37722
|
+
/** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
|
|
37723
|
+
const MAX_LISTED_GLOB_PATTERNS = 10;
|
|
37724
|
+
function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
|
|
37725
|
+
if (!isStringArray$2(value)) {
|
|
37726
|
+
warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
|
|
37727
|
+
return;
|
|
37728
|
+
}
|
|
37729
|
+
if (!warnOnGlobs) return value;
|
|
37730
|
+
const patterns = value.filter((entry) => GLOB_METACHARACTERS.test(entry));
|
|
37731
|
+
if (patterns.length > 0) {
|
|
37732
|
+
const listed = patterns.slice(0, MAX_LISTED_GLOB_PATTERNS).map(quoteValueForWarning);
|
|
37733
|
+
const overflow = patterns.length - listed.length;
|
|
37734
|
+
const shown = overflow > 0 ? `${listed.join(", ")} (+${overflow} more)` : listed.join(", ");
|
|
37735
|
+
warnWithFallback(void 0, `${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)} contains glob patterns (${shown}); other tools match the imported tool names literally`);
|
|
37736
|
+
}
|
|
37737
|
+
return value;
|
|
37738
|
+
}
|
|
37739
|
+
/**
|
|
37740
|
+
* Pool's `transport` block for a remote server, or `null` (after a warning)
|
|
37741
|
+
* when the server has no url or names a transport Pool cannot reach.
|
|
37742
|
+
*/
|
|
37743
|
+
function toPoolTransport(name, config, logger) {
|
|
37744
|
+
const url = resolveRemoteMcpUrl(config);
|
|
37745
|
+
if (!url) return warnAndSkipMcpServer({
|
|
37746
|
+
toolName: "Pool",
|
|
37747
|
+
serverName: name,
|
|
37748
|
+
reason: "a remote transport without a url",
|
|
37749
|
+
logger
|
|
37750
|
+
});
|
|
37751
|
+
const stated = config.type ?? config.transport;
|
|
37752
|
+
const type = asPoolRemoteType(stated, url);
|
|
37753
|
+
if (type === void 0) return warnAndSkipMcpServer({
|
|
37754
|
+
toolName: "Pool",
|
|
37755
|
+
serverName: name,
|
|
37756
|
+
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)`,
|
|
37757
|
+
logger
|
|
37758
|
+
});
|
|
37759
|
+
const transport = {
|
|
37760
|
+
type,
|
|
37761
|
+
url
|
|
37762
|
+
};
|
|
37763
|
+
if (config.headers && Object.keys(config.headers).length > 0) transport.headers = headersRecordToPoolList(config.headers);
|
|
37764
|
+
return transport;
|
|
37765
|
+
}
|
|
37766
|
+
/**
|
|
37767
|
+
* The transport-specific part of a Pool server entry: a `transport` block for
|
|
37768
|
+
* remote servers, `command`/`args`/`cwd` for stdio ones. `null` (after a
|
|
37769
|
+
* warning) when the server cannot be written.
|
|
37770
|
+
*/
|
|
37771
|
+
function toPoolTransportFields(name, config, logger) {
|
|
37772
|
+
if (isRemoteMcpServer(config)) {
|
|
37773
|
+
const transport = toPoolTransport(name, config, logger);
|
|
37774
|
+
return transport === null ? null : { transport };
|
|
37775
|
+
}
|
|
37776
|
+
const [command, ...args] = resolveLocalMcpCommand(config);
|
|
37777
|
+
if (!command) return warnAndSkipMcpServer({
|
|
37778
|
+
toolName: "Pool",
|
|
37779
|
+
serverName: name,
|
|
37780
|
+
reason: "a stdio transport without a command",
|
|
37781
|
+
logger
|
|
37782
|
+
});
|
|
37783
|
+
const fields = {
|
|
37784
|
+
command,
|
|
37785
|
+
args
|
|
37786
|
+
};
|
|
37787
|
+
if (config.cwd) fields.cwd = config.cwd;
|
|
37788
|
+
return fields;
|
|
37789
|
+
}
|
|
37790
|
+
/**
|
|
37791
|
+
* The fields shared by stdio and remote entries. Tool filters map onto Pool's
|
|
37792
|
+
* own switches — `enabledTools` becomes `enabled_tools` (the tools Pool
|
|
37793
|
+
* exposes at all) and `disabledTools` becomes `deny` (patterns agents may
|
|
37794
|
+
* never use); the Pool-only approval allowlist is authored as `poolAllow` and
|
|
37795
|
+
* written as `allow`. `disabled: true` is spelled the same on both sides.
|
|
37796
|
+
*/
|
|
37797
|
+
function toPoolCommonFields(config) {
|
|
37798
|
+
const fields = {};
|
|
37799
|
+
if (config.env && Object.keys(config.env).length > 0) fields.env = config.env;
|
|
37800
|
+
if (config.enabledTools && config.enabledTools.length > 0) fields.enabled_tools = config.enabledTools;
|
|
37801
|
+
if (config.poolAllow && config.poolAllow.length > 0) fields.allow = config.poolAllow;
|
|
37802
|
+
if (config.disabledTools && config.disabledTools.length > 0) fields.deny = config.disabledTools;
|
|
37803
|
+
if (config.disabled === true) fields.disabled = true;
|
|
37804
|
+
return fields;
|
|
37805
|
+
}
|
|
37806
|
+
/**
|
|
37807
|
+
* Convert canonical rulesync servers to Pool's native `mcp_servers` shape:
|
|
37808
|
+
* stdio servers carry `command`/`args`/`cwd`/`env`, remote servers carry a
|
|
37809
|
+
* `transport` block (`type` of `http` or `sse`, `url`, optional `headers`
|
|
37810
|
+
* list) plus optional `env`; see `toPoolCommonFields` for the tool filters.
|
|
37811
|
+
*
|
|
37812
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37813
|
+
*/
|
|
37814
|
+
function convertToPoolFormat(mcpServers, logger) {
|
|
37815
|
+
const result = {};
|
|
37816
|
+
for (const [name, config] of Object.entries(mcpServers)) {
|
|
37817
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name)) continue;
|
|
37818
|
+
if (!isRecord$1(config)) continue;
|
|
37819
|
+
if (declaresNoTransport(config)) {
|
|
37820
|
+
warnAndSkipMcpServer({
|
|
37821
|
+
toolName: "Pool",
|
|
37822
|
+
serverName: name,
|
|
37823
|
+
reason: "no transport",
|
|
37824
|
+
logger
|
|
37825
|
+
});
|
|
37826
|
+
continue;
|
|
37827
|
+
}
|
|
37828
|
+
const transportFields = toPoolTransportFields(name, config, logger);
|
|
37829
|
+
if (transportFields === null) continue;
|
|
37830
|
+
result[name] = {
|
|
37831
|
+
...transportFields,
|
|
37832
|
+
...toPoolCommonFields(config)
|
|
37833
|
+
};
|
|
37834
|
+
}
|
|
37835
|
+
return result;
|
|
37836
|
+
}
|
|
37837
|
+
const POOL_TO_CANONICAL_TOOL_LIST_KEYS = {
|
|
37838
|
+
enabled_tools: "enabledTools",
|
|
37839
|
+
deny: "disabledTools",
|
|
37840
|
+
allow: "poolAllow"
|
|
37841
|
+
};
|
|
37842
|
+
/**
|
|
37843
|
+
* Convert Pool's native `mcp_servers` shape back to canonical rulesync
|
|
37844
|
+
* servers: the `transport` block is flattened to `type`/`url`/`headers`,
|
|
37845
|
+
* `enabled_tools` maps back to `enabledTools`, `deny` to `disabledTools` and
|
|
37846
|
+
* `allow` to the namespaced `poolAllow` (see `McpServerSchema`). Unknown keys
|
|
37847
|
+
* pass through untouched so an import keeps whatever a hand-authored entry
|
|
37848
|
+
* declared; generation back out is a whitelist, so only the keys
|
|
37849
|
+
* `convertToPoolFormat` writes are re-emitted.
|
|
37850
|
+
*/
|
|
37851
|
+
function convertFromPoolFormat(poolServers) {
|
|
37852
|
+
const result = {};
|
|
37853
|
+
for (const [name, config] of Object.entries(poolServers)) {
|
|
37854
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
|
|
37855
|
+
const converted = {};
|
|
37856
|
+
for (const [key, value] of Object.entries(config)) {
|
|
37857
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
37858
|
+
if (key === "transport") {
|
|
37859
|
+
if (!isRecord$1(value)) continue;
|
|
37860
|
+
if (typeof value.type === "string") converted.type = value.type;
|
|
37861
|
+
if (typeof value.url === "string") converted.url = value.url;
|
|
37862
|
+
const headers = poolHeadersListToRecord(name, value.headers);
|
|
37863
|
+
if (headers) converted.headers = headers;
|
|
37864
|
+
continue;
|
|
37865
|
+
}
|
|
37866
|
+
const canonicalKey = Object.hasOwn(POOL_TO_CANONICAL_TOOL_LIST_KEYS, key) ? POOL_TO_CANONICAL_TOOL_LIST_KEYS[key] : void 0;
|
|
37867
|
+
if (canonicalKey) {
|
|
37868
|
+
const list = importPoolToolList({
|
|
37869
|
+
serverName: name,
|
|
37870
|
+
poolKey: key,
|
|
37871
|
+
value,
|
|
37872
|
+
warnOnGlobs: canonicalKey !== "poolAllow"
|
|
37873
|
+
});
|
|
37874
|
+
if (list) converted[canonicalKey] = list;
|
|
37875
|
+
continue;
|
|
37876
|
+
}
|
|
37877
|
+
converted[key] = value;
|
|
37878
|
+
}
|
|
37879
|
+
result[name] = converted;
|
|
37880
|
+
}
|
|
37881
|
+
return result;
|
|
37882
|
+
}
|
|
37883
|
+
/**
|
|
37884
|
+
* Pool MCP servers.
|
|
37885
|
+
*
|
|
37886
|
+
* Pool reads MCP servers from the `mcp_servers` block of its settings file:
|
|
37887
|
+
* `<project>/.poolside/settings.yaml` at project scope (shared and committed)
|
|
37888
|
+
* and `~/.config/poolside/settings.yaml` at user scope. The untracked
|
|
37889
|
+
* `.poolside/settings.local.yaml` overlay, which Pool layers on top of both,
|
|
37890
|
+
* is left to the user. Every other top-level settings key is preserved and
|
|
37891
|
+
* the file is never deleted.
|
|
37892
|
+
*
|
|
37893
|
+
* @see https://docs.poolside.ai/mcp-servers
|
|
37894
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
37895
|
+
*/
|
|
37896
|
+
var PoolMcp = class PoolMcp extends ToolMcp {
|
|
37897
|
+
settings;
|
|
37898
|
+
constructor(params) {
|
|
37899
|
+
super(params);
|
|
37900
|
+
this.settings = parsePoolSettings$2(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
37901
|
+
}
|
|
37902
|
+
getSettings() {
|
|
37903
|
+
return this.settings;
|
|
37904
|
+
}
|
|
37905
|
+
isDeletable() {
|
|
37906
|
+
return false;
|
|
37907
|
+
}
|
|
37908
|
+
static getSettablePaths({ global = false } = {}) {
|
|
37909
|
+
return {
|
|
37910
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
37911
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
37912
|
+
};
|
|
37913
|
+
}
|
|
37914
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
37915
|
+
const paths = this.getSettablePaths({ global });
|
|
37916
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
37917
|
+
return new PoolMcp({
|
|
37918
|
+
outputRoot,
|
|
37919
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37920
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37921
|
+
fileContent,
|
|
37922
|
+
validate,
|
|
37923
|
+
global
|
|
37924
|
+
});
|
|
37925
|
+
}
|
|
37926
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
37927
|
+
const paths = this.getSettablePaths({ global });
|
|
37928
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
37929
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
37930
|
+
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).map(([serverName, serverConfig]) => {
|
|
37931
|
+
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
37932
|
+
const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
|
|
37933
|
+
return [serverName, {
|
|
37934
|
+
...serverConfig,
|
|
37935
|
+
...isStringArray$2(poolAllow) && { poolAllow }
|
|
37936
|
+
}];
|
|
37937
|
+
})), logger);
|
|
37938
|
+
return new PoolMcp({
|
|
37939
|
+
outputRoot,
|
|
37940
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37941
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37942
|
+
fileContent: applySharedConfigPatch({
|
|
37943
|
+
fileKey: sharedConfigFileKey(paths),
|
|
37944
|
+
feature: "mcp",
|
|
37945
|
+
existingContent,
|
|
37946
|
+
patch: { [POOL_MCP_SERVERS_KEY]: converted },
|
|
37947
|
+
filePath
|
|
37948
|
+
}),
|
|
37949
|
+
validate,
|
|
37950
|
+
global
|
|
37951
|
+
});
|
|
37952
|
+
}
|
|
37953
|
+
toRulesyncMcp() {
|
|
37954
|
+
const converted = convertFromPoolFormat(isRecord$1(this.settings["mcp_servers"]) ? this.settings[POOL_MCP_SERVERS_KEY] : {});
|
|
37955
|
+
return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers: converted }, null, 2) });
|
|
37956
|
+
}
|
|
37957
|
+
validate() {
|
|
37958
|
+
return {
|
|
37959
|
+
success: true,
|
|
37960
|
+
error: null
|
|
37961
|
+
};
|
|
37962
|
+
}
|
|
37963
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
37964
|
+
return new PoolMcp({
|
|
37965
|
+
outputRoot,
|
|
37966
|
+
relativeDirPath,
|
|
37967
|
+
relativeFilePath,
|
|
37968
|
+
fileContent: "",
|
|
37969
|
+
validate: false,
|
|
37970
|
+
global
|
|
37971
|
+
});
|
|
37972
|
+
}
|
|
37973
|
+
};
|
|
37974
|
+
//#endregion
|
|
37097
37975
|
//#region src/features/mcp/qwencode-mcp.ts
|
|
37098
37976
|
/**
|
|
37099
37977
|
* Map rulesync's canonical per-server tool filters to Qwen Code's field names.
|
|
@@ -39541,6 +40419,15 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
39541
40419
|
supportsDisabledTools: true
|
|
39542
40420
|
}
|
|
39543
40421
|
}],
|
|
40422
|
+
["pool", {
|
|
40423
|
+
class: PoolMcp,
|
|
40424
|
+
meta: {
|
|
40425
|
+
supportsProject: true,
|
|
40426
|
+
supportsGlobal: true,
|
|
40427
|
+
supportsEnabledTools: true,
|
|
40428
|
+
supportsDisabledTools: true
|
|
40429
|
+
}
|
|
40430
|
+
}],
|
|
39544
40431
|
["qwencode", {
|
|
39545
40432
|
class: QwencodeMcp,
|
|
39546
40433
|
meta: {
|
|
@@ -40854,7 +41741,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
40854
41741
|
function toAntigravityCliToolName(canonical) {
|
|
40855
41742
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
40856
41743
|
}
|
|
40857
|
-
function toCanonicalToolName$
|
|
41744
|
+
function toCanonicalToolName$9(cliName) {
|
|
40858
41745
|
return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
|
|
40859
41746
|
}
|
|
40860
41747
|
/**
|
|
@@ -41070,7 +41957,7 @@ function convertAntigravityCliToRulesyncPermissions(params) {
|
|
|
41070
41957
|
const processEntries = (entries, action) => {
|
|
41071
41958
|
for (const entry of entries) {
|
|
41072
41959
|
const { toolName, pattern } = parsePermissionEntry$1(entry);
|
|
41073
|
-
const canonical = toCanonicalToolName$
|
|
41960
|
+
const canonical = toCanonicalToolName$9(toolName);
|
|
41074
41961
|
if (!permission[canonical]) permission[canonical] = {};
|
|
41075
41962
|
permission[canonical][pattern] = action;
|
|
41076
41963
|
}
|
|
@@ -41349,7 +42236,7 @@ function toLegacyAugmentToolName(augmentName) {
|
|
|
41349
42236
|
key: augmentName
|
|
41350
42237
|
}) ?? augmentName;
|
|
41351
42238
|
}
|
|
41352
|
-
function toCanonicalToolName$
|
|
42239
|
+
function toCanonicalToolName$8(augmentName) {
|
|
41353
42240
|
const legacyName = toLegacyAugmentToolName(augmentName);
|
|
41354
42241
|
return lookupOwn({
|
|
41355
42242
|
record: AUGMENT_TO_CANONICAL_TOOL_NAMES,
|
|
@@ -41802,7 +42689,7 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
41802
42689
|
const type = entry.permission.type;
|
|
41803
42690
|
if (!isBasicAugmentType(type)) continue;
|
|
41804
42691
|
const legacyToolName = toLegacyAugmentToolName(entry.toolName);
|
|
41805
|
-
const canonical = toCanonicalToolName$
|
|
42692
|
+
const canonical = toCanonicalToolName$8(entry.toolName);
|
|
41806
42693
|
if (forbiddenMapKeys.has(canonical)) {
|
|
41807
42694
|
logger?.warn(`AugmentCode permissions: skipping entry for tool '${entry.toolName}' because it maps to the reserved object key '${canonical}', which cannot be used as a permission key.`);
|
|
41808
42695
|
continue;
|
|
@@ -42047,7 +42934,7 @@ const CLAUDE_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONIC
|
|
|
42047
42934
|
function toClaudeToolName(canonical) {
|
|
42048
42935
|
return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
|
|
42049
42936
|
}
|
|
42050
|
-
function toCanonicalToolName$
|
|
42937
|
+
function toCanonicalToolName$7(claudeName) {
|
|
42051
42938
|
return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
|
|
42052
42939
|
}
|
|
42053
42940
|
/**
|
|
@@ -42961,7 +43848,7 @@ function convertClaudeToRulesyncPermissions(params) {
|
|
|
42961
43848
|
const processEntries = (entries, action) => {
|
|
42962
43849
|
for (const entry of entries) {
|
|
42963
43850
|
const { toolName, pattern } = parseClaudePermissionEntry(entry);
|
|
42964
|
-
const canonical = toCanonicalToolName$
|
|
43851
|
+
const canonical = toCanonicalToolName$7(toolName);
|
|
42965
43852
|
if (!permission[canonical]) permission[canonical] = {};
|
|
42966
43853
|
permission[canonical][pattern] = action;
|
|
42967
43854
|
}
|
|
@@ -43822,8 +44709,8 @@ function mapBashActionToDecision(action) {
|
|
|
43822
44709
|
//#region src/features/permissions/commandcode-permissions.ts
|
|
43823
44710
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
43824
44711
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
43825
|
-
const CATCH_ALL_PATTERN$
|
|
43826
|
-
const MCP_CANONICAL_PREFIX$
|
|
44712
|
+
const CATCH_ALL_PATTERN$7 = "*";
|
|
44713
|
+
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
43827
44714
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
43828
44715
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
43829
44716
|
bash: "Shell",
|
|
@@ -43877,10 +44764,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
43877
44764
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
43878
44765
|
*/
|
|
43879
44766
|
function buildCommandcodeRule(category, pattern) {
|
|
43880
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
43881
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
43882
|
-
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$
|
|
43883
|
-
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$
|
|
44767
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$7 || pattern === "";
|
|
44768
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$6)) return catchAll ? category : `${category}(${pattern})`;
|
|
44769
|
+
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$6}${pattern}`;
|
|
44770
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$7 : null;
|
|
43884
44771
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
43885
44772
|
if (tool === void 0) return null;
|
|
43886
44773
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -43916,7 +44803,7 @@ function splitCommandcodeRule(rule) {
|
|
|
43916
44803
|
}
|
|
43917
44804
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
43918
44805
|
function isCatchAllSpecifier(inner) {
|
|
43919
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
44806
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$7;
|
|
43920
44807
|
}
|
|
43921
44808
|
/**
|
|
43922
44809
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -43950,34 +44837,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
43950
44837
|
if (server.length === 0) return null;
|
|
43951
44838
|
const everyServer = {
|
|
43952
44839
|
category: "mcp",
|
|
43953
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44840
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
43954
44841
|
notInAllow: true
|
|
43955
44842
|
};
|
|
43956
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
43957
|
-
if (server === CATCH_ALL_PATTERN$
|
|
43958
|
-
if (server.includes(CATCH_ALL_PATTERN$
|
|
44843
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$6)) {
|
|
44844
|
+
if (server === CATCH_ALL_PATTERN$7) return everyServer;
|
|
44845
|
+
if (server.includes(CATCH_ALL_PATTERN$7)) return null;
|
|
43959
44846
|
return {
|
|
43960
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
43961
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44847
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$7 ? `${MCP_CANONICAL_PREFIX$6}${server}` : `${MCP_CANONICAL_PREFIX$6}${remainder}`,
|
|
44848
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
43962
44849
|
notInAllow: false
|
|
43963
44850
|
};
|
|
43964
44851
|
}
|
|
43965
|
-
const category = `${MCP_CANONICAL_PREFIX$
|
|
43966
|
-
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$
|
|
44852
|
+
const category = `${MCP_CANONICAL_PREFIX$6}${remainder}`;
|
|
44853
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$7);
|
|
43967
44854
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
43968
44855
|
category,
|
|
43969
44856
|
pattern: inner,
|
|
43970
44857
|
notInAllow: true
|
|
43971
44858
|
} : null;
|
|
43972
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
43973
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
43974
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
43975
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44859
|
+
if (remainder === CATCH_ALL_PATTERN$7) return everyServer;
|
|
44860
|
+
if (rest === CATCH_ALL_PATTERN$7 && !server.includes(CATCH_ALL_PATTERN$7)) return {
|
|
44861
|
+
category: `${MCP_CANONICAL_PREFIX$6}${server}`,
|
|
44862
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
43976
44863
|
notInAllow: true
|
|
43977
44864
|
};
|
|
43978
44865
|
return namesTool ? {
|
|
43979
44866
|
category,
|
|
43980
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44867
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
43981
44868
|
notInAllow: false
|
|
43982
44869
|
} : null;
|
|
43983
44870
|
}
|
|
@@ -44001,14 +44888,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44001
44888
|
if (category === "bash") {
|
|
44002
44889
|
if (isCatchAllSpecifier(inner)) return {
|
|
44003
44890
|
category,
|
|
44004
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44891
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44005
44892
|
notInAllow: false
|
|
44006
44893
|
};
|
|
44007
44894
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
44008
44895
|
if (normalized === "" || normalized === ":*") return null;
|
|
44009
44896
|
return isCatchAllSpecifier(normalized) ? {
|
|
44010
44897
|
category,
|
|
44011
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44898
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44012
44899
|
notInAllow: true
|
|
44013
44900
|
} : {
|
|
44014
44901
|
category,
|
|
@@ -44018,7 +44905,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44018
44905
|
}
|
|
44019
44906
|
if (isCatchAllSpecifier(inner)) return {
|
|
44020
44907
|
category,
|
|
44021
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44908
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44022
44909
|
notInAllow: false
|
|
44023
44910
|
};
|
|
44024
44911
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -44037,13 +44924,13 @@ function friendlyToolRule({ category, inner }) {
|
|
|
44037
44924
|
*/
|
|
44038
44925
|
function parseCommandcodeRule(rule) {
|
|
44039
44926
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44040
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
44927
|
+
if (tool === CATCH_ALL_PATTERN$7) return isCatchAllSpecifier(inner) ? {
|
|
44041
44928
|
category: "*",
|
|
44042
|
-
pattern: CATCH_ALL_PATTERN$
|
|
44929
|
+
pattern: CATCH_ALL_PATTERN$7,
|
|
44043
44930
|
notInAllow: true
|
|
44044
44931
|
} : null;
|
|
44045
44932
|
const lowered = tool.toLowerCase();
|
|
44046
|
-
if (lowered.startsWith(MCP_CANONICAL_PREFIX$
|
|
44933
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$6)) return parseMcpCommandcodeRule({
|
|
44047
44934
|
tool,
|
|
44048
44935
|
inner
|
|
44049
44936
|
});
|
|
@@ -44061,12 +44948,12 @@ function parseCommandcodeRule(rule) {
|
|
|
44061
44948
|
*/
|
|
44062
44949
|
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
44063
44950
|
const { tool } = splitCommandcodeRule(rule);
|
|
44064
|
-
return category.startsWith(MCP_CANONICAL_PREFIX$
|
|
44951
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$6) && !tool.startsWith(MCP_CANONICAL_PREFIX$6);
|
|
44065
44952
|
}
|
|
44066
44953
|
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
44067
44954
|
function scopedMcpRuleTool(rule) {
|
|
44068
44955
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44069
|
-
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
44956
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$6) ? tool : null;
|
|
44070
44957
|
}
|
|
44071
44958
|
/**
|
|
44072
44959
|
* The string entries of a Command Code permission list. Command Code reads
|
|
@@ -44109,7 +44996,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
44109
44996
|
}).filter((rule) => {
|
|
44110
44997
|
const parsed = parseCommandcodeRule(rule);
|
|
44111
44998
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
44112
|
-
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$
|
|
44999
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$7})`);
|
|
44113
45000
|
if (key !== "allow" && isStricterAction({
|
|
44114
45001
|
action: key,
|
|
44115
45002
|
existing: replacement
|
|
@@ -44166,7 +45053,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44166
45053
|
const ranked = /* @__PURE__ */ new Map();
|
|
44167
45054
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
44168
45055
|
for (const [category, rules] of Object.entries(permission)) {
|
|
44169
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
45056
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$7);
|
|
44170
45057
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
44171
45058
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
44172
45059
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -44235,7 +45122,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
44235
45122
|
function skippedAllowImportMessage({ rule, category }) {
|
|
44236
45123
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
44237
45124
|
if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
|
|
44238
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
45125
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$6) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
|
|
44239
45126
|
return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
|
|
44240
45127
|
}
|
|
44241
45128
|
/**
|
|
@@ -44437,7 +45324,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
44437
45324
|
//#endregion
|
|
44438
45325
|
//#region src/features/permissions/continue-permissions.ts
|
|
44439
45326
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
44440
|
-
const CATCH_ALL_PATTERN$
|
|
45327
|
+
const CATCH_ALL_PATTERN$6 = "*";
|
|
44441
45328
|
/**
|
|
44442
45329
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
44443
45330
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -44445,7 +45332,7 @@ const CATCH_ALL_PATTERN$5 = "*";
|
|
|
44445
45332
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
44446
45333
|
*/
|
|
44447
45334
|
function normalizeCatchAllPattern$1(pattern) {
|
|
44448
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45335
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$6 : pattern;
|
|
44449
45336
|
}
|
|
44450
45337
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
44451
45338
|
bash: "Bash",
|
|
@@ -44474,7 +45361,7 @@ const CONTINUE_PATTERN_RE = /^([^(]+)(?:\(([^)]*)\))?$/;
|
|
|
44474
45361
|
function toContinueToolName(canonical) {
|
|
44475
45362
|
return Object.hasOwn(CANONICAL_TO_CONTINUE_TOOL_NAMES, canonical) ? CANONICAL_TO_CONTINUE_TOOL_NAMES[canonical] ?? canonical : canonical;
|
|
44476
45363
|
}
|
|
44477
|
-
function toCanonicalToolName$
|
|
45364
|
+
function toCanonicalToolName$6(continueName) {
|
|
44478
45365
|
return Object.hasOwn(CONTINUE_TO_CANONICAL_TOOL_NAMES, continueName) ? CONTINUE_TO_CANONICAL_TOOL_NAMES[continueName] ?? continueName : continueName;
|
|
44479
45366
|
}
|
|
44480
45367
|
/**
|
|
@@ -44482,7 +45369,7 @@ function toCanonicalToolName$5(continueName) {
|
|
|
44482
45369
|
* `Tool(pattern)`.
|
|
44483
45370
|
*/
|
|
44484
45371
|
function buildContinuePermissionEntry(toolName, pattern) {
|
|
44485
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
45372
|
+
return pattern === CATCH_ALL_PATTERN$6 ? toolName : `${toolName}(${pattern})`;
|
|
44486
45373
|
}
|
|
44487
45374
|
/**
|
|
44488
45375
|
* Split a permissions.yaml entry into its tool name and argument pattern, or
|
|
@@ -44496,7 +45383,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
44496
45383
|
if (toolName === "") return;
|
|
44497
45384
|
return {
|
|
44498
45385
|
toolName,
|
|
44499
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
45386
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$6 : pattern
|
|
44500
45387
|
};
|
|
44501
45388
|
}
|
|
44502
45389
|
/**
|
|
@@ -44670,7 +45557,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
44670
45557
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
44671
45558
|
continue;
|
|
44672
45559
|
}
|
|
44673
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
45560
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$6) {
|
|
44674
45561
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
44675
45562
|
continue;
|
|
44676
45563
|
}
|
|
@@ -44695,7 +45582,7 @@ function convertContinuePermissionsToRulesync(lists) {
|
|
|
44695
45582
|
const parsedEntry = parseContinuePermissionEntry(entry);
|
|
44696
45583
|
if (parsedEntry === void 0) continue;
|
|
44697
45584
|
if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
|
|
44698
|
-
const category = toCanonicalToolName$
|
|
45585
|
+
const category = toCanonicalToolName$6(parsedEntry.toolName);
|
|
44699
45586
|
permission[category] ??= {};
|
|
44700
45587
|
permission[category][parsedEntry.pattern] ??= action;
|
|
44701
45588
|
}
|
|
@@ -45019,14 +45906,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
45019
45906
|
};
|
|
45020
45907
|
//#endregion
|
|
45021
45908
|
//#region src/features/permissions/crush-permissions.ts
|
|
45022
|
-
const CATCH_ALL_PATTERN$
|
|
45909
|
+
const CATCH_ALL_PATTERN$5 = "*";
|
|
45023
45910
|
/**
|
|
45024
45911
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
45025
45912
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
45026
45913
|
* still be written.
|
|
45027
45914
|
*/
|
|
45028
45915
|
function normalizeCatchAllPattern(pattern) {
|
|
45029
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
45916
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$5 : pattern;
|
|
45030
45917
|
}
|
|
45031
45918
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
45032
45919
|
bash: "bash",
|
|
@@ -45038,14 +45925,14 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
|
45038
45925
|
webfetch: "fetch"
|
|
45039
45926
|
};
|
|
45040
45927
|
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
45041
|
-
const MCP_CANONICAL_PREFIX$
|
|
45928
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
45042
45929
|
const MCP_CRUSH_PREFIX = "mcp_";
|
|
45043
45930
|
function toCrushToolName(canonical) {
|
|
45044
45931
|
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
45045
|
-
if (canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
45932
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
45046
45933
|
return canonical;
|
|
45047
45934
|
}
|
|
45048
|
-
function toCanonicalToolName$
|
|
45935
|
+
function toCanonicalToolName$5(crushName) {
|
|
45049
45936
|
return Object.hasOwn(CRUSH_TO_CANONICAL_TOOL_NAMES, crushName) ? CRUSH_TO_CANONICAL_TOOL_NAMES[crushName] ?? crushName : crushName;
|
|
45050
45937
|
}
|
|
45051
45938
|
function isMcpToolName(crushName) {
|
|
@@ -45225,7 +46112,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
45225
46112
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
45226
46113
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
45227
46114
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
45228
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
46115
|
+
if (pattern === CATCH_ALL_PATTERN$5) {
|
|
45229
46116
|
catchAllAction = action;
|
|
45230
46117
|
continue;
|
|
45231
46118
|
}
|
|
@@ -45298,11 +46185,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
45298
46185
|
};
|
|
45299
46186
|
for (const entry of disabled) {
|
|
45300
46187
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
45301
|
-
bucketFor(toCanonicalToolName$
|
|
46188
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] = "deny";
|
|
45302
46189
|
}
|
|
45303
46190
|
for (const entry of allowed) {
|
|
45304
46191
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
45305
|
-
bucketFor(toCanonicalToolName$
|
|
46192
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$5] ??= "allow";
|
|
45306
46193
|
}
|
|
45307
46194
|
return { permission };
|
|
45308
46195
|
}
|
|
@@ -45349,13 +46236,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
45349
46236
|
WebFetch: "webfetch",
|
|
45350
46237
|
Mcp: "mcp"
|
|
45351
46238
|
};
|
|
45352
|
-
const MCP_CANONICAL_PREFIX$
|
|
46239
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
45353
46240
|
/**
|
|
45354
46241
|
* Returns true if the canonical category is the per-tool MCP form
|
|
45355
46242
|
* `mcp__<server>__<tool>`.
|
|
45356
46243
|
*/
|
|
45357
46244
|
function isMcpScopedCategory(canonical) {
|
|
45358
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46245
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$4) && canonical.length > 5;
|
|
45359
46246
|
}
|
|
45360
46247
|
function toCursorType(canonical) {
|
|
45361
46248
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -45389,7 +46276,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
45389
46276
|
if (match) {
|
|
45390
46277
|
const server = match[1] ?? "*";
|
|
45391
46278
|
const tool = match[2] ?? "*";
|
|
45392
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
46279
|
+
return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
|
|
45393
46280
|
}
|
|
45394
46281
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
45395
46282
|
}
|
|
@@ -45682,7 +46569,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
45682
46569
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
45683
46570
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
45684
46571
|
if (!permission[canonical]) permission[canonical] = {};
|
|
45685
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
46572
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$4) ? "*" : pattern;
|
|
45686
46573
|
permission[canonical][canonicalPattern] = action;
|
|
45687
46574
|
}
|
|
45688
46575
|
};
|
|
@@ -47201,7 +48088,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
47201
48088
|
//#region src/features/permissions/goose-permissions.ts
|
|
47202
48089
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
47203
48090
|
const GOOSE_USER_KEY = "user";
|
|
47204
|
-
const CATCH_ALL_PATTERN$
|
|
48091
|
+
const CATCH_ALL_PATTERN$4 = "*";
|
|
47205
48092
|
const RULESYNC_TO_GOOSE_TOOL_NAME = {
|
|
47206
48093
|
bash: "developer__shell",
|
|
47207
48094
|
edit: "developer__text_editor",
|
|
@@ -47363,7 +48250,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
47363
48250
|
for (const [category, rules] of orderedEntries) {
|
|
47364
48251
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
47365
48252
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
47366
|
-
if (pattern !== CATCH_ALL_PATTERN$
|
|
48253
|
+
if (pattern !== CATCH_ALL_PATTERN$4) {
|
|
47367
48254
|
logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
|
|
47368
48255
|
continue;
|
|
47369
48256
|
}
|
|
@@ -47392,7 +48279,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
47392
48279
|
for (const toolName of toolNames) {
|
|
47393
48280
|
const category = GOOSE_TO_RULESYNC_TOOL_NAME[toolName] ?? toolName;
|
|
47394
48281
|
permission[category] ??= {};
|
|
47395
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
48282
|
+
permission[category][CATCH_ALL_PATTERN$4] = action;
|
|
47396
48283
|
}
|
|
47397
48284
|
}
|
|
47398
48285
|
return { permission };
|
|
@@ -47403,8 +48290,8 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
47403
48290
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
47404
48291
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
47405
48292
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
47406
|
-
const CATCH_ALL_PATTERN$
|
|
47407
|
-
const MCP_CANONICAL_PREFIX$
|
|
48293
|
+
const CATCH_ALL_PATTERN$3 = "*";
|
|
48294
|
+
const MCP_CANONICAL_PREFIX$3 = "mcp__";
|
|
47408
48295
|
const CATEGORY_TO_GROK_TOOL = {
|
|
47409
48296
|
bash: "Bash",
|
|
47410
48297
|
read: "Read",
|
|
@@ -47445,14 +48332,14 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
|
47445
48332
|
* concrete pattern emits `Tool(pattern)`.
|
|
47446
48333
|
*/
|
|
47447
48334
|
function buildGrokEntry(category, pattern) {
|
|
47448
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
48335
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$3)) {
|
|
47449
48336
|
const remainder = category.slice(5);
|
|
47450
48337
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
47451
48338
|
}
|
|
47452
|
-
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$
|
|
48339
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
47453
48340
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
47454
48341
|
if (tool === void 0) return null;
|
|
47455
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
48342
|
+
return pattern === CATCH_ALL_PATTERN$3 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
47456
48343
|
}
|
|
47457
48344
|
/**
|
|
47458
48345
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -47472,17 +48359,17 @@ function parseGrokEntry(entry) {
|
|
|
47472
48359
|
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
47473
48360
|
}
|
|
47474
48361
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
47475
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47476
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48362
|
+
category: `${MCP_CANONICAL_PREFIX$3}${inner}`,
|
|
48363
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47477
48364
|
} : {
|
|
47478
48365
|
category: "mcp",
|
|
47479
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48366
|
+
pattern: CATCH_ALL_PATTERN$3
|
|
47480
48367
|
};
|
|
47481
48368
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
47482
48369
|
if (category === void 0) return null;
|
|
47483
48370
|
return {
|
|
47484
48371
|
category,
|
|
47485
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
48372
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$3
|
|
47486
48373
|
};
|
|
47487
48374
|
}
|
|
47488
48375
|
/**
|
|
@@ -47503,15 +48390,15 @@ function parseGrokRule(rule) {
|
|
|
47503
48390
|
if (typeof tool !== "string") return null;
|
|
47504
48391
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
47505
48392
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
47506
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
48393
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$3;
|
|
47507
48394
|
const lowerTool = tool.trim().toLowerCase();
|
|
47508
|
-
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
48395
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$3 ? {
|
|
47509
48396
|
category: "mcp",
|
|
47510
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48397
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47511
48398
|
action
|
|
47512
48399
|
} : {
|
|
47513
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
47514
|
-
pattern: CATCH_ALL_PATTERN$
|
|
48400
|
+
category: `${MCP_CANONICAL_PREFIX$3}${resolvedPattern}`,
|
|
48401
|
+
pattern: CATCH_ALL_PATTERN$3,
|
|
47515
48402
|
action
|
|
47516
48403
|
};
|
|
47517
48404
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -47659,7 +48546,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
47659
48546
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
47660
48547
|
}
|
|
47661
48548
|
const fineGrained = parseGrokPermissionArrays(isRecord$1(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
47662
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
48549
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$3]: legacyModeAction(parsed) } } };
|
|
47663
48550
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
47664
48551
|
}
|
|
47665
48552
|
validate() {
|
|
@@ -48395,7 +49282,7 @@ function isSharedKiloCategory(key) {
|
|
|
48395
49282
|
*/
|
|
48396
49283
|
function parseKiloJsoncStrict(content, filePath) {
|
|
48397
49284
|
const errors = [];
|
|
48398
|
-
const parsed = parse(content, errors, { allowTrailingComma: true });
|
|
49285
|
+
const parsed = parse$1(content, errors, { allowTrailingComma: true });
|
|
48399
49286
|
const first = errors[0];
|
|
48400
49287
|
if (first) throw new Error(`Failed to parse Kilo Code config at ${filePath}: ${printParseErrorCode(first.error)} at offset ${first.offset}`);
|
|
48401
49288
|
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
@@ -48439,7 +49326,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
48439
49326
|
json;
|
|
48440
49327
|
constructor(params) {
|
|
48441
49328
|
super(params);
|
|
48442
|
-
const parsed = parse(this.fileContent || "{}");
|
|
49329
|
+
const parsed = parse$1(this.fileContent || "{}");
|
|
48443
49330
|
if (params.validate !== false) this.json = KiloPermissionsConfigSchema.parse(parsed);
|
|
48444
49331
|
else {
|
|
48445
49332
|
const result = KiloPermissionsConfigSchema.safeParse(parsed);
|
|
@@ -48579,7 +49466,7 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
48579
49466
|
}
|
|
48580
49467
|
validate() {
|
|
48581
49468
|
try {
|
|
48582
|
-
const json = parse(this.fileContent || "{}");
|
|
49469
|
+
const json = parse$1(this.fileContent || "{}");
|
|
48583
49470
|
const result = KiloPermissionsConfigSchema.safeParse(json);
|
|
48584
49471
|
if (!result.success) return {
|
|
48585
49472
|
success: false,
|
|
@@ -49290,7 +50177,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
49290
50177
|
json;
|
|
49291
50178
|
constructor(params) {
|
|
49292
50179
|
super(params);
|
|
49293
|
-
this.json = OpencodePermissionsConfigSchema.parse(parse(this.fileContent || "{}"));
|
|
50180
|
+
this.json = OpencodePermissionsConfigSchema.parse(parse$1(this.fileContent || "{}"));
|
|
49294
50181
|
}
|
|
49295
50182
|
getJson() {
|
|
49296
50183
|
return this.json;
|
|
@@ -49318,7 +50205,7 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
49318
50205
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
49319
50206
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
49320
50207
|
}
|
|
49321
|
-
const parsed = parse(fileContent ?? "{}");
|
|
50208
|
+
const parsed = parse$1(fileContent ?? "{}");
|
|
49322
50209
|
const record = isRecord$1(parsed) ? parsed : {};
|
|
49323
50210
|
const nextJson = {
|
|
49324
50211
|
...record,
|
|
@@ -49539,6 +50426,567 @@ var PiPermissions = class PiPermissions extends ToolPermissions {
|
|
|
49539
50426
|
}
|
|
49540
50427
|
};
|
|
49541
50428
|
//#endregion
|
|
50429
|
+
//#region src/features/permissions/pool-permissions.ts
|
|
50430
|
+
const POOL_ALLOW_KEY = "allow";
|
|
50431
|
+
const POOL_DENY_KEY = "deny";
|
|
50432
|
+
const POOL_DISABLED_KEY = "disabled";
|
|
50433
|
+
const POOL_PATH_KEY = "path";
|
|
50434
|
+
const POOL_WRITE_KEY = "write";
|
|
50435
|
+
const CATCH_ALL_PATTERN$2 = "*";
|
|
50436
|
+
const PATHS_CATCH_ALL_PATTERN = "**";
|
|
50437
|
+
const CANONICAL_TO_POOL_TOOL_NAMES = {
|
|
50438
|
+
bash: "shell",
|
|
50439
|
+
webfetch: "web_fetch",
|
|
50440
|
+
websearch: "web_search"
|
|
50441
|
+
};
|
|
50442
|
+
const POOL_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_POOL_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
50443
|
+
const READ_CATEGORY = "read";
|
|
50444
|
+
const WRITE_CATEGORIES = ["edit", "write"];
|
|
50445
|
+
const FILE_CATEGORIES = [READ_CATEGORY, ...WRITE_CATEGORIES];
|
|
50446
|
+
const MCP_CANONICAL_PREFIX$2 = "mcp__";
|
|
50447
|
+
/**
|
|
50448
|
+
* Single spelling of the settings.yaml codec/policy, matching the
|
|
50449
|
+
* `SHARED_CONFIG_OWNERSHIP` declaration for both scopes: fail closed on an
|
|
50450
|
+
* unparseable root rather than replacing the user's primary Pool settings with
|
|
50451
|
+
* generated output.
|
|
50452
|
+
*/
|
|
50453
|
+
function parsePoolSettings$1(fileContent, filePath) {
|
|
50454
|
+
return parseSharedConfig({
|
|
50455
|
+
format: "yaml",
|
|
50456
|
+
fileContent,
|
|
50457
|
+
filePath,
|
|
50458
|
+
invalidRootPolicy: "error"
|
|
50459
|
+
});
|
|
50460
|
+
}
|
|
50461
|
+
function toPoolToolName(canonical) {
|
|
50462
|
+
return lookupOwn({
|
|
50463
|
+
record: CANONICAL_TO_POOL_TOOL_NAMES,
|
|
50464
|
+
key: canonical
|
|
50465
|
+
}) ?? canonical;
|
|
50466
|
+
}
|
|
50467
|
+
function toCanonicalToolName$4(poolName) {
|
|
50468
|
+
return lookupOwn({
|
|
50469
|
+
record: POOL_TO_CANONICAL_TOOL_NAMES,
|
|
50470
|
+
key: poolName
|
|
50471
|
+
}) ?? poolName;
|
|
50472
|
+
}
|
|
50473
|
+
function isMcpCategory(category) {
|
|
50474
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$2) && category.length > 5;
|
|
50475
|
+
}
|
|
50476
|
+
function isFileCategory(category) {
|
|
50477
|
+
return FILE_CATEGORIES.includes(category);
|
|
50478
|
+
}
|
|
50479
|
+
/**
|
|
50480
|
+
* Pool's tool rules know one wildcard, `*`, which matches anything including
|
|
50481
|
+
* `/` — the same set a canonical `*` names — so a `**` run is collapsed to
|
|
50482
|
+
* keep the file in Pool's own spelling. `?`, `[...]` and `{a,b}` are literal
|
|
50483
|
+
* characters to Pool, so a pattern that relies on them cannot be written.
|
|
50484
|
+
*/
|
|
50485
|
+
function toPoolToolPattern(pattern) {
|
|
50486
|
+
if (/[?[{]/.test(pattern)) return void 0;
|
|
50487
|
+
return pattern.replaceAll(/\*{2,}/g, "*");
|
|
50488
|
+
}
|
|
50489
|
+
/**
|
|
50490
|
+
* Pool's path globs know `*` and `**`, so a bare `*` run is written as `**`
|
|
50491
|
+
* to keep it matching anything. `?`, `[...]` and `{a,b}` are literal
|
|
50492
|
+
* characters to Pool here as well, so a pattern that relies on them cannot be
|
|
50493
|
+
* written.
|
|
50494
|
+
*/
|
|
50495
|
+
function toPoolPathPattern(pattern) {
|
|
50496
|
+
if (/[?[{]/.test(pattern)) return void 0;
|
|
50497
|
+
return /^\*+$/.test(pattern) ? PATHS_CATCH_ALL_PATTERN : pattern;
|
|
50498
|
+
}
|
|
50499
|
+
function isHomeOrAbsolutePath(pattern) {
|
|
50500
|
+
return isAbsolute(pattern) || pattern === "~" || pattern.startsWith("~/");
|
|
50501
|
+
}
|
|
50502
|
+
function rulesOf(permission, category) {
|
|
50503
|
+
return lookupOwn({
|
|
50504
|
+
record: permission,
|
|
50505
|
+
key: category
|
|
50506
|
+
}) ?? {};
|
|
50507
|
+
}
|
|
50508
|
+
function patternsWithAction(rules, actions) {
|
|
50509
|
+
return Object.entries(rules).filter(([pattern, action]) => !isPrototypePollutionKey(pattern) && actions.includes(action)).map(([pattern]) => pattern);
|
|
50510
|
+
}
|
|
50511
|
+
function asRestrictions(patterns) {
|
|
50512
|
+
return uniq(patterns).map((pattern) => ({
|
|
50513
|
+
pattern,
|
|
50514
|
+
fromAllToolsCategory: false
|
|
50515
|
+
}));
|
|
50516
|
+
}
|
|
50517
|
+
function listPatterns(patterns) {
|
|
50518
|
+
return patterns.map((pattern) => quoteValueForWarning(pattern)).join(", ");
|
|
50519
|
+
}
|
|
50520
|
+
/**
|
|
50521
|
+
* Permissions adapter for Pool (Poolside's coding agent CLI).
|
|
50522
|
+
*
|
|
50523
|
+
* Pool keeps its permission rules in the same YAML settings file as its MCP
|
|
50524
|
+
* servers — `.poolside/settings.yaml` (project) / `~/.config/poolside/settings.yaml`
|
|
50525
|
+
* (global) — under two top-level keys:
|
|
50526
|
+
* - `tools.<name>`: `allow` and `deny` pattern lists matched against the
|
|
50527
|
+
* tool's argument (the command for `shell`, the URL for `web_fetch`, ...),
|
|
50528
|
+
* plus `disabled: true` to remove the tool. A denied pattern wins over an
|
|
50529
|
+
* allowed one; a call that matches neither prompts. Only `*` is a wildcard.
|
|
50530
|
+
* - `paths`: `allow` entries (`{ path }`, read-only unless `write: true`)
|
|
50531
|
+
* and `deny` entries (`{ path }`) with `*`/`**` globs; deny wins.
|
|
50532
|
+
*
|
|
50533
|
+
* Mapping (rulesync canonical -> Pool):
|
|
50534
|
+
* - `bash` -> `tools.shell`, `webfetch` -> `tools.web_fetch`,
|
|
50535
|
+
* `websearch` -> `tools.web_search`; any other non-file category passes
|
|
50536
|
+
* through as `tools.<category>`. `allow` -> `allow`, `deny` -> `deny`,
|
|
50537
|
+
* `ask` -> nothing (prompting is Pool's default) — but an `allow` that
|
|
50538
|
+
* overlaps an `ask` of any category landing on the same Pool tool, or a
|
|
50539
|
+
* deny Pool cannot spell, is withheld, since an allowed pattern would
|
|
50540
|
+
* silence the prompt the config asks for.
|
|
50541
|
+
* - `read` / `edit` / `write` -> `paths`: a `read` allow is a read-only
|
|
50542
|
+
* entry, an `edit` or `write` allow adds `write: true`, a `read` deny is a
|
|
50543
|
+
* deny entry. Pool cannot deny writes without denying reads, so an
|
|
50544
|
+
* `edit`/`write` deny that is not also a `read` deny only withholds the
|
|
50545
|
+
* write flag of the entries it overlaps (Pool prompts for those writes).
|
|
50546
|
+
* - `mcp__<server>__<tool>` is skipped: Pool approves MCP tools per server
|
|
50547
|
+
* under `mcp_servers.<server>.allow`, authored as `poolAllow` in
|
|
50548
|
+
* `.rulesync/mcp.json`. The all-tools `*` category is mirrored onto
|
|
50549
|
+
* `bash` by `honorAllToolsOnBash` and otherwise skipped.
|
|
50550
|
+
*
|
|
50551
|
+
* Project settings take project-relative paths and the user settings absolute
|
|
50552
|
+
* or `~` paths, so a path of the other kind is skipped with a warning at each
|
|
50553
|
+
* scope. The managed tools' `allow`/`deny` lists and, whenever the canonical
|
|
50554
|
+
* config carries a file category, the `paths` lists are rebuilt; every other
|
|
50555
|
+
* key (`disabled`, unmanaged tools, `mcp_servers`, `pool`, `sandbox`, ...) is
|
|
50556
|
+
* kept, and the file is never deleted.
|
|
50557
|
+
*
|
|
50558
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
50559
|
+
*/
|
|
50560
|
+
var PoolPermissions = class PoolPermissions extends ToolPermissions {
|
|
50561
|
+
settings;
|
|
50562
|
+
constructor(params) {
|
|
50563
|
+
super({
|
|
50564
|
+
...params,
|
|
50565
|
+
fileContent: params.fileContent ?? ""
|
|
50566
|
+
});
|
|
50567
|
+
this.settings = parsePoolSettings$1(this.fileContent ?? "", join(this.relativeDirPath, this.relativeFilePath));
|
|
50568
|
+
}
|
|
50569
|
+
getSettings() {
|
|
50570
|
+
return this.settings;
|
|
50571
|
+
}
|
|
50572
|
+
isDeletable() {
|
|
50573
|
+
return false;
|
|
50574
|
+
}
|
|
50575
|
+
/**
|
|
50576
|
+
* settings.yaml is Pool's file: rulesync merges into it when it exists but
|
|
50577
|
+
* does not create one that holds nothing of its own — an absent file and an
|
|
50578
|
+
* absent rule both mean "prompt".
|
|
50579
|
+
*/
|
|
50580
|
+
shouldSkipCreationWhenPayloadEmpty() {
|
|
50581
|
+
return true;
|
|
50582
|
+
}
|
|
50583
|
+
static getSettablePaths({ global = false } = {}) {
|
|
50584
|
+
return {
|
|
50585
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
50586
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
50587
|
+
};
|
|
50588
|
+
}
|
|
50589
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true, global = false }) {
|
|
50590
|
+
const paths = this.getSettablePaths({ global });
|
|
50591
|
+
const fileContent = await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
|
|
50592
|
+
return new PoolPermissions({
|
|
50593
|
+
outputRoot,
|
|
50594
|
+
relativeDirPath: paths.relativeDirPath,
|
|
50595
|
+
relativeFilePath: paths.relativeFilePath,
|
|
50596
|
+
fileContent,
|
|
50597
|
+
validate,
|
|
50598
|
+
global
|
|
50599
|
+
});
|
|
50600
|
+
}
|
|
50601
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
50602
|
+
const paths = this.getSettablePaths({ global });
|
|
50603
|
+
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
50604
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
50605
|
+
const existing = parsePoolSettings$1(existingContent, filePath);
|
|
50606
|
+
const patch = buildPoolPermissionsPatch({
|
|
50607
|
+
config: rulesyncPermissions.getJson(),
|
|
50608
|
+
existing,
|
|
50609
|
+
global,
|
|
50610
|
+
logger
|
|
50611
|
+
});
|
|
50612
|
+
return new PoolPermissions({
|
|
50613
|
+
outputRoot,
|
|
50614
|
+
relativeDirPath: paths.relativeDirPath,
|
|
50615
|
+
relativeFilePath: paths.relativeFilePath,
|
|
50616
|
+
fileContent: applySharedConfigPatch({
|
|
50617
|
+
fileKey: sharedConfigFileKey(paths),
|
|
50618
|
+
feature: "permissions",
|
|
50619
|
+
existingContent,
|
|
50620
|
+
patch,
|
|
50621
|
+
filePath,
|
|
50622
|
+
logger
|
|
50623
|
+
}),
|
|
50624
|
+
validate: true,
|
|
50625
|
+
global
|
|
50626
|
+
});
|
|
50627
|
+
}
|
|
50628
|
+
toRulesyncPermissions() {
|
|
50629
|
+
const rulesyncConfig = convertPoolToRulesync({
|
|
50630
|
+
tools: isRecord$1(this.settings["tools"]) ? this.settings[POOL_TOOLS_KEY] : {},
|
|
50631
|
+
paths: isRecord$1(this.settings["paths"]) ? this.settings[POOL_PATHS_KEY] : {}
|
|
50632
|
+
});
|
|
50633
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
50634
|
+
}
|
|
50635
|
+
validate() {
|
|
50636
|
+
return {
|
|
50637
|
+
success: true,
|
|
50638
|
+
error: null
|
|
50639
|
+
};
|
|
50640
|
+
}
|
|
50641
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
50642
|
+
return new PoolPermissions({
|
|
50643
|
+
outputRoot,
|
|
50644
|
+
relativeDirPath,
|
|
50645
|
+
relativeFilePath,
|
|
50646
|
+
fileContent: "",
|
|
50647
|
+
validate: false,
|
|
50648
|
+
global
|
|
50649
|
+
});
|
|
50650
|
+
}
|
|
50651
|
+
};
|
|
50652
|
+
/**
|
|
50653
|
+
* The deep-merge patch that rewrites the managed `tools.<name>` lists and,
|
|
50654
|
+
* when the canonical config carries a file category, the `paths` lists. An
|
|
50655
|
+
* empty list retracts its key (`undefined` deletes under deep-merge), and a
|
|
50656
|
+
* block is only touched when there is something to write into it or it
|
|
50657
|
+
* already exists, so a config without rules does not grow an empty
|
|
50658
|
+
* `tools: {}` / `paths: {}`.
|
|
50659
|
+
*/
|
|
50660
|
+
function buildPoolPermissionsPatch({ config, existing, global, logger }) {
|
|
50661
|
+
const permission = honorAllToolsOnBash(config.permission);
|
|
50662
|
+
const existingTools = isRecord$1(existing["tools"]) ? existing[POOL_TOOLS_KEY] : void 0;
|
|
50663
|
+
const existingPaths = isRecord$1(existing["paths"]) ? existing[POOL_PATHS_KEY] : void 0;
|
|
50664
|
+
const patch = {};
|
|
50665
|
+
const toolsPatch = {};
|
|
50666
|
+
for (const [toolName, lists] of convertRulesyncToPoolTools({
|
|
50667
|
+
permission,
|
|
50668
|
+
logger
|
|
50669
|
+
})) {
|
|
50670
|
+
const existingTool = existingTools === void 0 ? void 0 : lookupOwn({
|
|
50671
|
+
record: existingTools,
|
|
50672
|
+
key: toolName
|
|
50673
|
+
});
|
|
50674
|
+
if (lists.allow.length === 0 && lists.deny.length === 0 && existingTool === void 0) continue;
|
|
50675
|
+
toolsPatch[toolName] = {
|
|
50676
|
+
[POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
|
|
50677
|
+
[POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
|
|
50678
|
+
};
|
|
50679
|
+
}
|
|
50680
|
+
if (Object.keys(toolsPatch).length > 0) patch[POOL_TOOLS_KEY] = toolsPatch;
|
|
50681
|
+
if (FILE_CATEGORIES.some((category) => lookupOwn({
|
|
50682
|
+
record: permission,
|
|
50683
|
+
key: category
|
|
50684
|
+
}))) {
|
|
50685
|
+
const lists = convertRulesyncToPoolPaths({
|
|
50686
|
+
permission,
|
|
50687
|
+
global,
|
|
50688
|
+
logger
|
|
50689
|
+
});
|
|
50690
|
+
if (lists.allow.length > 0 || lists.deny.length > 0 || existingPaths !== void 0) patch[POOL_PATHS_KEY] = {
|
|
50691
|
+
[POOL_ALLOW_KEY]: lists.allow.length > 0 ? lists.allow : void 0,
|
|
50692
|
+
[POOL_DENY_KEY]: lists.deny.length > 0 ? lists.deny : void 0
|
|
50693
|
+
};
|
|
50694
|
+
}
|
|
50695
|
+
return patch;
|
|
50696
|
+
}
|
|
50697
|
+
/**
|
|
50698
|
+
* Convert one non-file category into a Pool tool's `allow`/`deny` lists. A
|
|
50699
|
+
* deny Pool can spell needs no withholding (Pool lets it win over an allow),
|
|
50700
|
+
* but an `ask` has no list of its own — a call Pool does not match prompts —
|
|
50701
|
+
* so an allow that overlaps one is withheld and reported, and the tool keeps
|
|
50702
|
+
* prompting. A deny Pool cannot spell (its pattern uses a glob form other
|
|
50703
|
+
* than `*`) is skipped, and then withholds the allows it overlaps the same
|
|
50704
|
+
* way: written, such an allow would auto-approve the very command the deny
|
|
50705
|
+
* was meant to stop. `restrictions` carries the ask patterns and the skipped
|
|
50706
|
+
* deny patterns of every category that lands on the same Pool tool, because
|
|
50707
|
+
* Pool matches the joined lists as one.
|
|
50708
|
+
*/
|
|
50709
|
+
function convertToolCategory({ category, toolName, rules, restrictions, logger }) {
|
|
50710
|
+
const lists = {
|
|
50711
|
+
allow: [],
|
|
50712
|
+
deny: []
|
|
50713
|
+
};
|
|
50714
|
+
const shadowingRestrictions = createShadowingRestrictionsTest(asRestrictions(restrictions));
|
|
50715
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
50716
|
+
if (isPrototypePollutionKey(rawPattern) || action === "ask") continue;
|
|
50717
|
+
const pattern = toPoolToolPattern(rawPattern);
|
|
50718
|
+
if (pattern === void 0) {
|
|
50719
|
+
logger?.warn(`Pool tool rules treat only "*" as a wildcard, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
50720
|
+
continue;
|
|
50721
|
+
}
|
|
50722
|
+
if (action === "deny") {
|
|
50723
|
+
lists.deny.push(pattern);
|
|
50724
|
+
continue;
|
|
50725
|
+
}
|
|
50726
|
+
const shadowing = shadowingRestrictions(rawPattern);
|
|
50727
|
+
if (shadowing.length > 0) {
|
|
50728
|
+
logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched call prompts), so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(rawPattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool keeps prompting for "${toolName}".`);
|
|
50729
|
+
continue;
|
|
50730
|
+
}
|
|
50731
|
+
lists.allow.push(pattern);
|
|
50732
|
+
}
|
|
50733
|
+
return {
|
|
50734
|
+
allow: uniq(lists.allow),
|
|
50735
|
+
deny: uniq(lists.deny)
|
|
50736
|
+
};
|
|
50737
|
+
}
|
|
50738
|
+
function unrepresentableDenies(rules) {
|
|
50739
|
+
return patternsWithAction(rules, ["deny"]).filter((pattern) => toPoolToolPattern(pattern) === void 0);
|
|
50740
|
+
}
|
|
50741
|
+
/**
|
|
50742
|
+
* Group the non-file categories by the Pool tool they land on, reporting the
|
|
50743
|
+
* ones Pool has no place for. Two categories can resolve to the same tool
|
|
50744
|
+
* (`bash` and a pass-through `shell`).
|
|
50745
|
+
*/
|
|
50746
|
+
function groupCategoriesByPoolTool({ permission, logger }) {
|
|
50747
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
50748
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
50749
|
+
if (isPrototypePollutionKey(category) || isFileCategory(category)) continue;
|
|
50750
|
+
if (category === "*") {
|
|
50751
|
+
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
50752
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
50753
|
+
logger?.warn(`Pool has no rule that spans every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(rawPattern)}) was skipped.`);
|
|
50754
|
+
}
|
|
50755
|
+
continue;
|
|
50756
|
+
}
|
|
50757
|
+
if (isMcpCategory(category)) {
|
|
50758
|
+
logger?.warn(`Pool approves MCP tools per server under "mcp_servers.<server>.allow", so the rules for "${category}" were skipped; author them as "poolAllow" on the server in .rulesync/mcp.json instead.`);
|
|
50759
|
+
continue;
|
|
50760
|
+
}
|
|
50761
|
+
const toolName = toPoolToolName(category);
|
|
50762
|
+
byTool.set(toolName, [...byTool.get(toolName) ?? [], [category, rules]]);
|
|
50763
|
+
}
|
|
50764
|
+
return byTool;
|
|
50765
|
+
}
|
|
50766
|
+
/**
|
|
50767
|
+
* Convert the non-file categories into Pool tool lists, keyed by Pool tool
|
|
50768
|
+
* name. The categories that land on the same tool are converted against the
|
|
50769
|
+
* union of their `ask` patterns and their lists are joined, which is safe
|
|
50770
|
+
* because a deny wins over an allow in Pool whichever category wrote it.
|
|
50771
|
+
*/
|
|
50772
|
+
function convertRulesyncToPoolTools({ permission, logger }) {
|
|
50773
|
+
const byTool = /* @__PURE__ */ new Map();
|
|
50774
|
+
for (const [toolName, categories] of groupCategoriesByPoolTool({
|
|
50775
|
+
permission,
|
|
50776
|
+
logger
|
|
50777
|
+
})) {
|
|
50778
|
+
const restrictions = categories.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...unrepresentableDenies(rules)]);
|
|
50779
|
+
const lists = {
|
|
50780
|
+
allow: [],
|
|
50781
|
+
deny: []
|
|
50782
|
+
};
|
|
50783
|
+
for (const [category, rules] of categories) {
|
|
50784
|
+
const converted = convertToolCategory({
|
|
50785
|
+
category,
|
|
50786
|
+
toolName,
|
|
50787
|
+
rules,
|
|
50788
|
+
restrictions,
|
|
50789
|
+
logger
|
|
50790
|
+
});
|
|
50791
|
+
lists.allow.push(...converted.allow);
|
|
50792
|
+
lists.deny.push(...converted.deny);
|
|
50793
|
+
}
|
|
50794
|
+
byTool.set(toolName, {
|
|
50795
|
+
allow: uniq(lists.allow),
|
|
50796
|
+
deny: uniq(lists.deny)
|
|
50797
|
+
});
|
|
50798
|
+
}
|
|
50799
|
+
return byTool;
|
|
50800
|
+
}
|
|
50801
|
+
/**
|
|
50802
|
+
* Convert the file categories into Pool's `paths` lists.
|
|
50803
|
+
*
|
|
50804
|
+
* An allow entry grants reading, and `write: true` grants writing on top, so
|
|
50805
|
+
* a `read` allow is withheld by an overlapping `read` ask and a write allow by
|
|
50806
|
+
* an overlapping `edit`/`write` ask — and by a `read` ask, since the write
|
|
50807
|
+
* entry would read too. A `read` deny lands in `paths.deny`, which Pool lets
|
|
50808
|
+
* win over any allow, and covers an `edit`/`write` deny of the same pattern,
|
|
50809
|
+
* so neither withholds anything; an `edit`/`write` deny without a `read` deny
|
|
50810
|
+
* cannot be written (Pool's deny blocks reads as well), so it is reported and
|
|
50811
|
+
* only withholds the write flags it overlaps. A deny Pool cannot spell is
|
|
50812
|
+
* skipped the same way and withholds the allows it overlaps, so it is honored
|
|
50813
|
+
* by prompting rather than dropped.
|
|
50814
|
+
*/
|
|
50815
|
+
function convertRulesyncToPoolPaths({ permission, global, logger }) {
|
|
50816
|
+
const readRules = rulesOf(permission, READ_CATEGORY);
|
|
50817
|
+
const writeRulesByCategory = WRITE_CATEGORIES.map((category) => [category, rulesOf(permission, category)]);
|
|
50818
|
+
const readAsks = patternsWithAction(readRules, ["ask"]);
|
|
50819
|
+
const fitsScope = ({ category, action, pattern }) => {
|
|
50820
|
+
if (isHomeOrAbsolutePath(pattern) === global) return true;
|
|
50821
|
+
logger?.warn(global ? `Pool's user settings take absolute or "~" paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ${join(POOL_DIR, POOL_SETTINGS_FILE_NAME)} (without --global) instead.` : `Pool's project settings take project-relative paths, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped; author it in ~/${join(POOL_GLOBAL_DIR, POOL_SETTINGS_FILE_NAME)} (via --global) instead.`);
|
|
50822
|
+
return false;
|
|
50823
|
+
};
|
|
50824
|
+
const deny = [];
|
|
50825
|
+
const readRestrictions = [...readAsks];
|
|
50826
|
+
for (const pattern of patternsWithAction(readRules, ["deny"])) {
|
|
50827
|
+
const path = toPoolPathPattern(pattern);
|
|
50828
|
+
if (path === void 0) {
|
|
50829
|
+
warnSkippedPathPattern({
|
|
50830
|
+
category: READ_CATEGORY,
|
|
50831
|
+
action: "deny",
|
|
50832
|
+
pattern,
|
|
50833
|
+
logger
|
|
50834
|
+
});
|
|
50835
|
+
readRestrictions.push(pattern);
|
|
50836
|
+
continue;
|
|
50837
|
+
}
|
|
50838
|
+
if (!fitsScope({
|
|
50839
|
+
category: READ_CATEGORY,
|
|
50840
|
+
action: "deny",
|
|
50841
|
+
pattern
|
|
50842
|
+
})) continue;
|
|
50843
|
+
if (!deny.some((entry) => entry[POOL_PATH_KEY] === path)) deny.push({ [POOL_PATH_KEY]: path });
|
|
50844
|
+
}
|
|
50845
|
+
const isDeniedForRead = (pattern) => {
|
|
50846
|
+
const path = toPoolPathPattern(pattern);
|
|
50847
|
+
return path !== void 0 && deny.some((entry) => entry[POOL_PATH_KEY] === path);
|
|
50848
|
+
};
|
|
50849
|
+
const writeRestrictions = writeRulesByCategory.flatMap(([, rules]) => [...patternsWithAction(rules, ["ask"]), ...patternsWithAction(rules, ["deny"]).filter((pattern) => !isDeniedForRead(pattern))]);
|
|
50850
|
+
reportUnwrittenWriteDenies({
|
|
50851
|
+
writeRulesByCategory,
|
|
50852
|
+
isDeniedForRead,
|
|
50853
|
+
logger
|
|
50854
|
+
});
|
|
50855
|
+
const shadowingReadRestrictions = createShadowingRestrictionsTest(asRestrictions(readRestrictions));
|
|
50856
|
+
const shadowingWriteRestrictions = createShadowingRestrictionsTest(asRestrictions([...readRestrictions, ...writeRestrictions]));
|
|
50857
|
+
const allowByPath = /* @__PURE__ */ new Map();
|
|
50858
|
+
for (const pattern of patternsWithAction(readRules, ["allow"])) {
|
|
50859
|
+
if (!fitsScope({
|
|
50860
|
+
category: READ_CATEGORY,
|
|
50861
|
+
action: "allow",
|
|
50862
|
+
pattern
|
|
50863
|
+
})) continue;
|
|
50864
|
+
const path = toPoolPathPattern(pattern);
|
|
50865
|
+
if (path === void 0) {
|
|
50866
|
+
warnSkippedPathPattern({
|
|
50867
|
+
category: READ_CATEGORY,
|
|
50868
|
+
action: "allow",
|
|
50869
|
+
pattern,
|
|
50870
|
+
logger
|
|
50871
|
+
});
|
|
50872
|
+
continue;
|
|
50873
|
+
}
|
|
50874
|
+
const shadowing = shadowingReadRestrictions(pattern);
|
|
50875
|
+
if (shadowing.length > 0) {
|
|
50876
|
+
logger?.warn(`Pool has no list for an "ask" or for a deny it cannot spell (an unmatched path prompts), so the "allow" rule for "read" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}.`);
|
|
50877
|
+
continue;
|
|
50878
|
+
}
|
|
50879
|
+
allowByPath.set(path, { [POOL_PATH_KEY]: path });
|
|
50880
|
+
}
|
|
50881
|
+
for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["allow"])) {
|
|
50882
|
+
if (!fitsScope({
|
|
50883
|
+
category,
|
|
50884
|
+
action: "allow",
|
|
50885
|
+
pattern
|
|
50886
|
+
})) continue;
|
|
50887
|
+
const path = toPoolPathPattern(pattern);
|
|
50888
|
+
if (path === void 0) {
|
|
50889
|
+
warnSkippedPathPattern({
|
|
50890
|
+
category,
|
|
50891
|
+
action: "allow",
|
|
50892
|
+
pattern,
|
|
50893
|
+
logger
|
|
50894
|
+
});
|
|
50895
|
+
continue;
|
|
50896
|
+
}
|
|
50897
|
+
const shadowing = shadowingWriteRestrictions(pattern);
|
|
50898
|
+
if (shadowing.length > 0) {
|
|
50899
|
+
logger?.warn(`A Pool path allowed with "write: true" can be read and written freely, so the "allow" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was withheld because it overlaps the restriction(s) ${listPatterns(shadowing)}; Pool prompts for those writes.`);
|
|
50900
|
+
continue;
|
|
50901
|
+
}
|
|
50902
|
+
allowByPath.set(path, {
|
|
50903
|
+
[POOL_PATH_KEY]: path,
|
|
50904
|
+
[POOL_WRITE_KEY]: true
|
|
50905
|
+
});
|
|
50906
|
+
}
|
|
50907
|
+
return {
|
|
50908
|
+
allow: [...allowByPath.values()],
|
|
50909
|
+
deny
|
|
50910
|
+
};
|
|
50911
|
+
}
|
|
50912
|
+
function warnSkippedPathPattern({ category, action, pattern, logger }) {
|
|
50913
|
+
logger?.warn(`Pool path rules treat only "*" and "**" as wildcards, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
50914
|
+
}
|
|
50915
|
+
function reportUnwrittenWriteDenies({ writeRulesByCategory, isDeniedForRead, logger }) {
|
|
50916
|
+
for (const [category, rules] of writeRulesByCategory) for (const pattern of patternsWithAction(rules, ["deny"])) {
|
|
50917
|
+
if (isDeniedForRead(pattern)) continue;
|
|
50918
|
+
if (toPoolPathPattern(pattern) === void 0) {
|
|
50919
|
+
warnSkippedPathPattern({
|
|
50920
|
+
category,
|
|
50921
|
+
action: "deny",
|
|
50922
|
+
pattern,
|
|
50923
|
+
logger
|
|
50924
|
+
});
|
|
50925
|
+
continue;
|
|
50926
|
+
}
|
|
50927
|
+
logger?.warn(`Pool cannot deny writes without denying reads, so the "deny" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was not written and Pool prompts for those writes instead; deny the pattern under "read" as well to block it entirely.`);
|
|
50928
|
+
}
|
|
50929
|
+
}
|
|
50930
|
+
function isUsablePattern(pattern) {
|
|
50931
|
+
return typeof pattern === "string" && pattern !== "" && !isPrototypePollutionKey(pattern);
|
|
50932
|
+
}
|
|
50933
|
+
function pathOfEntry(entry) {
|
|
50934
|
+
return isRecord$1(entry) && isUsablePattern(entry[POOL_PATH_KEY]) ? entry[POOL_PATH_KEY] : void 0;
|
|
50935
|
+
}
|
|
50936
|
+
/**
|
|
50937
|
+
* Convert one `tools.<name>` block into its canonical bucket. A disabled tool
|
|
50938
|
+
* never runs, so it reads as a catch-all deny and its own lists are ignored;
|
|
50939
|
+
* a denied pattern wins over an allowed one, as it does in Pool.
|
|
50940
|
+
*/
|
|
50941
|
+
function importPoolTool({ toolConfig, bucket }) {
|
|
50942
|
+
if (toolConfig[POOL_DISABLED_KEY] === true) {
|
|
50943
|
+
bucket[CATCH_ALL_PATTERN$2] = "deny";
|
|
50944
|
+
return;
|
|
50945
|
+
}
|
|
50946
|
+
const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
|
|
50947
|
+
const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
|
|
50948
|
+
for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
|
|
50949
|
+
for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
|
|
50950
|
+
}
|
|
50951
|
+
/**
|
|
50952
|
+
* Convert Pool's `tools` and `paths` blocks back into a rulesync config.
|
|
50953
|
+
* `mcp_servers` approvals are the mcp feature's (`poolAllow`) and are not read.
|
|
50954
|
+
*/
|
|
50955
|
+
function convertPoolToRulesync({ tools, paths }) {
|
|
50956
|
+
const permission = {};
|
|
50957
|
+
const bucketFor = (category) => {
|
|
50958
|
+
const own = lookupOwn({
|
|
50959
|
+
record: permission,
|
|
50960
|
+
key: category
|
|
50961
|
+
});
|
|
50962
|
+
if (own !== void 0) return own;
|
|
50963
|
+
const created = {};
|
|
50964
|
+
permission[category] = created;
|
|
50965
|
+
return created;
|
|
50966
|
+
};
|
|
50967
|
+
for (const [toolName, toolConfig] of Object.entries(tools)) {
|
|
50968
|
+
if (isPrototypePollutionKey(toolName) || !isRecord$1(toolConfig)) continue;
|
|
50969
|
+
importPoolTool({
|
|
50970
|
+
toolConfig,
|
|
50971
|
+
bucket: bucketFor(toCanonicalToolName$4(toolName))
|
|
50972
|
+
});
|
|
50973
|
+
}
|
|
50974
|
+
const denyEntries = Array.isArray(paths[POOL_DENY_KEY]) ? paths[POOL_DENY_KEY] : [];
|
|
50975
|
+
const allowEntries = Array.isArray(paths[POOL_ALLOW_KEY]) ? paths[POOL_ALLOW_KEY] : [];
|
|
50976
|
+
for (const entry of denyEntries) {
|
|
50977
|
+
const path = pathOfEntry(entry);
|
|
50978
|
+
if (path === void 0) continue;
|
|
50979
|
+
for (const category of FILE_CATEGORIES) bucketFor(category)[path] = "deny";
|
|
50980
|
+
}
|
|
50981
|
+
for (const entry of allowEntries) {
|
|
50982
|
+
const path = pathOfEntry(entry);
|
|
50983
|
+
if (path === void 0) continue;
|
|
50984
|
+
bucketFor(READ_CATEGORY)[path] ??= "allow";
|
|
50985
|
+
if (isRecord$1(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
|
|
50986
|
+
}
|
|
50987
|
+
return { permission };
|
|
50988
|
+
}
|
|
50989
|
+
//#endregion
|
|
49542
50990
|
//#region src/features/permissions/qwencode-permissions.ts
|
|
49543
50991
|
/**
|
|
49544
50992
|
* Qwen Code uses a settings.json file in `.qwen/` (project) or `~/.qwen/` (global).
|
|
@@ -55641,6 +57089,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
55641
57089
|
supportsImport: true
|
|
55642
57090
|
}
|
|
55643
57091
|
}],
|
|
57092
|
+
["pool", {
|
|
57093
|
+
class: PoolPermissions,
|
|
57094
|
+
meta: {
|
|
57095
|
+
supportsProject: true,
|
|
57096
|
+
supportsGlobal: true,
|
|
57097
|
+
supportsImport: true
|
|
57098
|
+
}
|
|
57099
|
+
}],
|
|
55644
57100
|
["qwencode", {
|
|
55645
57101
|
class: QwencodePermissions,
|
|
55646
57102
|
meta: {
|
|
@@ -56304,8 +57760,19 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56304
57760
|
const changedPaths = [];
|
|
56305
57761
|
for (const aiDir of aiDirs) {
|
|
56306
57762
|
const dirPath = aiDir.getDirPath();
|
|
56307
|
-
let dirHasChanges = false;
|
|
56308
57763
|
const mainFile = aiDir.getMainFile();
|
|
57764
|
+
const otherFiles = aiDir.getOtherFiles();
|
|
57765
|
+
const writtenPaths = [
|
|
57766
|
+
dirPath,
|
|
57767
|
+
...mainFile ? [join(dirPath, mainFile.name)] : [],
|
|
57768
|
+
...otherFiles.map((file) => join(dirPath, file.relativeFilePathToDirPath))
|
|
57769
|
+
];
|
|
57770
|
+
if (await refusesAnyWriteOutsideRoot({
|
|
57771
|
+
logger: this.logger,
|
|
57772
|
+
rootPath: aiDir.getOutputRoot(),
|
|
57773
|
+
targetPaths: writtenPaths
|
|
57774
|
+
})) continue;
|
|
57775
|
+
let dirHasChanges = false;
|
|
56309
57776
|
let mainFileContent;
|
|
56310
57777
|
if (mainFile) {
|
|
56311
57778
|
const mainFilePath = join(dirPath, mainFile.name);
|
|
@@ -56317,7 +57784,6 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
56317
57784
|
existing: existingContent
|
|
56318
57785
|
})) dirHasChanges = true;
|
|
56319
57786
|
}
|
|
56320
|
-
const otherFiles = aiDir.getOtherFiles();
|
|
56321
57787
|
for (const file of otherFiles) {
|
|
56322
57788
|
if (dirHasChanges) break;
|
|
56323
57789
|
const filePath = join(dirPath, file.relativeFilePathToDirPath);
|
|
@@ -56762,7 +58228,7 @@ const AmpSkillFrontmatterSchema = z.looseObject({
|
|
|
56762
58228
|
* Amp natively reads Agent Skills following the open standard: a directory with a
|
|
56763
58229
|
* `SKILL.md` carrying `name`/`description` frontmatter, scoped to the project
|
|
56764
58230
|
* (`.agents/skills/`) and user-wide (`~/.config/agents/skills/`).
|
|
56765
|
-
* @see https://ampcode.com/
|
|
58231
|
+
* @see https://ampcode.com/docs/customize/skills
|
|
56766
58232
|
*/
|
|
56767
58233
|
var AmpSkill = class AmpSkill extends ToolSkill {
|
|
56768
58234
|
constructor({ outputRoot = process.cwd(), relativeDirPath = AMP_SKILLS_PROJECT_DIR, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -61731,37 +63197,6 @@ var PiSkill = class PiSkill extends ToolSkill {
|
|
|
61731
63197
|
}
|
|
61732
63198
|
};
|
|
61733
63199
|
//#endregion
|
|
61734
|
-
//#region src/constants/pool-paths.ts
|
|
61735
|
-
/**
|
|
61736
|
-
* Pool (Poolside's coding agent CLI) configuration-layout conventions.
|
|
61737
|
-
*
|
|
61738
|
-
* Pool reads `AGENTS.md` instruction files the same way the AGENTS.md standard
|
|
61739
|
-
* describes them: the personal `~/.config/poolside/AGENTS.md` (Pool itself
|
|
61740
|
-
* honours `XDG_CONFIG_HOME` upstream; rulesync writes only the XDG-default
|
|
61741
|
-
* path), the project-root `AGENTS.md`, and nested per-directory
|
|
61742
|
-
* `AGENTS.md` files from the repository root down through the working
|
|
61743
|
-
* directory, deeper files taking precedence. It skips ignored directories
|
|
61744
|
-
* (`.git/`, `node_modules/`, cache directories, repository ignore rules).
|
|
61745
|
-
*
|
|
61746
|
-
* Skills follow the Agent Skills format (`<name>/SKILL.md` bundles). Pool
|
|
61747
|
-
* scans `.poolside/skills/` (project) and `~/.config/poolside/skills/`
|
|
61748
|
-
* (global) plus the shared `.agents/skills/` / `~/.agents/skills/` roots and
|
|
61749
|
-
* the skill directories of other Agent Skills tools; rulesync writes only the
|
|
61750
|
-
* Pool-specific roots and leaves the shared ones to their own targets.
|
|
61751
|
-
*
|
|
61752
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
61753
|
-
* @see https://docs.poolside.ai/skills
|
|
61754
|
-
* @see https://github.com/poolsideai/pool
|
|
61755
|
-
*/
|
|
61756
|
-
/** Project-scoped `.poolside/` directory at the project root. */
|
|
61757
|
-
const POOL_DIR = ".poolside";
|
|
61758
|
-
/** Global config directory for Pool, relative to the home directory. */
|
|
61759
|
-
const POOL_GLOBAL_DIR = join(".config", "poolside");
|
|
61760
|
-
/** Project skills root, relative to the project root. */
|
|
61761
|
-
const POOL_SKILLS_DIR_PATH = join(POOL_DIR, "skills");
|
|
61762
|
-
/** Global skills root, relative to the home directory. */
|
|
61763
|
-
const POOL_GLOBAL_SKILLS_DIR_PATH = join(POOL_GLOBAL_DIR, "skills");
|
|
61764
|
-
//#endregion
|
|
61765
63200
|
//#region src/features/skills/pool-skill.ts
|
|
61766
63201
|
const PoolSkillFrontmatterSchema = z.looseObject({
|
|
61767
63202
|
name: z.string(),
|
|
@@ -68467,6 +69902,325 @@ var OpenCodeSubagent = class OpenCodeSubagent extends OpenCodeStyleSubagent {
|
|
|
68467
69902
|
}
|
|
68468
69903
|
};
|
|
68469
69904
|
//#endregion
|
|
69905
|
+
//#region src/features/subagents/pool-subagent.ts
|
|
69906
|
+
/** How Pool runs an agent; a custom agent defaults to `in_process`. */
|
|
69907
|
+
const POOL_AGENT_TYPES = [
|
|
69908
|
+
"in_process",
|
|
69909
|
+
"command",
|
|
69910
|
+
"agent_server"
|
|
69911
|
+
];
|
|
69912
|
+
const DEFAULT_POOL_AGENT_TYPE = "in_process";
|
|
69913
|
+
/**
|
|
69914
|
+
* One entry of `subagents.agents.<name>` in Pool's settings file. `type` is
|
|
69915
|
+
* required by Pool and `description` by every enabled custom agent; the
|
|
69916
|
+
* remaining keys depend on the type (`command`/`args`/`env` for `command`,
|
|
69917
|
+
* `agent_server`/`session_config_options` for `agent_server`). Loose so a key
|
|
69918
|
+
* Pool adds later survives the round-trip.
|
|
69919
|
+
* @see https://docs.poolside.ai/subagents
|
|
69920
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
69921
|
+
*/
|
|
69922
|
+
const PoolAgentSchema = z.looseObject({
|
|
69923
|
+
type: z.optional(z.enum(POOL_AGENT_TYPES)),
|
|
69924
|
+
description: z.optional(z.string()),
|
|
69925
|
+
instructions: z.optional(z.string()),
|
|
69926
|
+
command: z.optional(z.string()),
|
|
69927
|
+
args: z.optional(z.array(z.string())),
|
|
69928
|
+
env: z.optional(z.record(z.string(), z.string())),
|
|
69929
|
+
agent_server: z.optional(z.string()),
|
|
69930
|
+
session_config_options: z.optional(z.record(z.string(), z.unknown())),
|
|
69931
|
+
inherit_agent_config: z.optional(z.boolean()),
|
|
69932
|
+
disabled: z.optional(z.boolean())
|
|
69933
|
+
});
|
|
69934
|
+
/**
|
|
69935
|
+
* Single spelling of the settings.yaml codec/policy, matching PoolMcp: fail
|
|
69936
|
+
* closed on an unparseable root rather than replacing the user's primary Pool
|
|
69937
|
+
* settings with generated output.
|
|
69938
|
+
*/
|
|
69939
|
+
function parsePoolSettings(fileContent, filePath) {
|
|
69940
|
+
return parseSharedConfig({
|
|
69941
|
+
format: "yaml",
|
|
69942
|
+
fileContent,
|
|
69943
|
+
filePath,
|
|
69944
|
+
invalidRootPolicy: "error"
|
|
69945
|
+
});
|
|
69946
|
+
}
|
|
69947
|
+
/**
|
|
69948
|
+
* The file stem an imported agent lands under in `.rulesync/subagents/`. Pool
|
|
69949
|
+
* keys agents by an arbitrary YAML mapping key, so the name is narrowed to a
|
|
69950
|
+
* safe file name before it becomes a path (defense in depth on top of the
|
|
69951
|
+
* central path-traversal guard); the agent keeps its real name in `name`.
|
|
69952
|
+
*/
|
|
69953
|
+
function sanitizePoolAgentFileStem(raw) {
|
|
69954
|
+
const stem = raw.replace(/[^A-Za-z0-9_-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
69955
|
+
return stem.length > 0 ? stem : "agent";
|
|
69956
|
+
}
|
|
69957
|
+
/**
|
|
69958
|
+
* Read `subagents.agents` out of a parsed settings document. The reserved
|
|
69959
|
+
* `general` agent is Pool's built-in and stays the user's to configure, so it
|
|
69960
|
+
* is never imported; an entry that is not a mapping is skipped.
|
|
69961
|
+
*/
|
|
69962
|
+
function agentsOfSettings({ settings, filePath, logger }) {
|
|
69963
|
+
const block = settings[POOL_SUBAGENTS_KEY];
|
|
69964
|
+
const rawAgents = isRecord$1(block) ? block[POOL_SUBAGENTS_AGENTS_KEY] : void 0;
|
|
69965
|
+
const agents = {};
|
|
69966
|
+
if (!isRecord$1(rawAgents)) return agents;
|
|
69967
|
+
for (const [name, raw] of Object.entries(rawAgents)) {
|
|
69968
|
+
if (isPrototypePollutionKey(name) || name === "general") continue;
|
|
69969
|
+
const result = PoolAgentSchema.safeParse(raw);
|
|
69970
|
+
if (!result.success) {
|
|
69971
|
+
logger?.warn(`Skipped Pool subagent ${quoteValueForWarning(name)} in ${filePath}: ${formatError(result.error)}`);
|
|
69972
|
+
continue;
|
|
69973
|
+
}
|
|
69974
|
+
agents[name] = result.data;
|
|
69975
|
+
}
|
|
69976
|
+
return agents;
|
|
69977
|
+
}
|
|
69978
|
+
/**
|
|
69979
|
+
* Pool subagents.
|
|
69980
|
+
*
|
|
69981
|
+
* Pool defines subagents inside its settings file — `.poolside/settings.yaml`
|
|
69982
|
+
* at project scope and `~/.config/poolside/settings.yaml` at user scope —
|
|
69983
|
+
* under `subagents.agents.<name>`, so every targeted rulesync subagent is
|
|
69984
|
+
* collapsed into that one file: the body becomes `instructions`, the shared
|
|
69985
|
+
* `description` is written as-is and the `pool:` frontmatter section supplies
|
|
69986
|
+
* `type` (default `in_process`) and the type-specific keys. rulesync owns the
|
|
69987
|
+
* `agents` map except for Pool's built-in `general` entry, which is carried
|
|
69988
|
+
* over untouched together with `subagents.default` and every other top-level
|
|
69989
|
+
* key of the file (`mcp_servers`, `tools`, `pool`, ...); the file is never
|
|
69990
|
+
* deleted.
|
|
69991
|
+
*
|
|
69992
|
+
* @see https://docs.poolside.ai/subagents
|
|
69993
|
+
* @see https://docs.poolside.ai/settings-file-reference
|
|
69994
|
+
*/
|
|
69995
|
+
var PoolSubagent = class extends ToolSubagent {
|
|
69996
|
+
agents;
|
|
69997
|
+
logger;
|
|
69998
|
+
/**
|
|
69999
|
+
* The settings document to write, built lazily: the writer hands over the
|
|
70000
|
+
* existing file through `setFileContent` first, and a file that does not
|
|
70001
|
+
* exist yet gets the block merged into an empty document. Lazy so that a
|
|
70002
|
+
* `forDeletion` instance never consults the gateway.
|
|
70003
|
+
*/
|
|
70004
|
+
mergedContent = null;
|
|
70005
|
+
constructor({ agents, logger, ...rest }) {
|
|
70006
|
+
super({
|
|
70007
|
+
...rest,
|
|
70008
|
+
fileContent: ""
|
|
70009
|
+
});
|
|
70010
|
+
this.agents = agents;
|
|
70011
|
+
this.logger = logger;
|
|
70012
|
+
}
|
|
70013
|
+
getAgents() {
|
|
70014
|
+
return this.agents;
|
|
70015
|
+
}
|
|
70016
|
+
isDeletable() {
|
|
70017
|
+
return false;
|
|
70018
|
+
}
|
|
70019
|
+
shouldMergeExistingFileContent() {
|
|
70020
|
+
return true;
|
|
70021
|
+
}
|
|
70022
|
+
setFileContent(newFileContent) {
|
|
70023
|
+
this.mergedContent = this.mergeInto(newFileContent);
|
|
70024
|
+
}
|
|
70025
|
+
getFileContent() {
|
|
70026
|
+
if (this.mergedContent === null) this.mergedContent = this.mergeInto("");
|
|
70027
|
+
return this.mergedContent;
|
|
70028
|
+
}
|
|
70029
|
+
/**
|
|
70030
|
+
* Rebuild the `subagents` block on top of an existing settings document:
|
|
70031
|
+
* `default` and any other sibling of `agents` are kept, the built-in
|
|
70032
|
+
* `general` agent is carried over, and every other agent is replaced by the
|
|
70033
|
+
* generated set — an agent removed from `.rulesync/subagents/` must not
|
|
70034
|
+
* survive here. A block left empty is retracted rather than written as `{}`.
|
|
70035
|
+
*/
|
|
70036
|
+
mergeInto(existingContent) {
|
|
70037
|
+
const paths = {
|
|
70038
|
+
relativeDirPath: this.getRelativeDirPath(),
|
|
70039
|
+
relativeFilePath: this.getRelativeFilePath()
|
|
70040
|
+
};
|
|
70041
|
+
const filePath = join(this.getOutputRoot(), paths.relativeDirPath, paths.relativeFilePath);
|
|
70042
|
+
const existing = parsePoolSettings(existingContent, filePath);
|
|
70043
|
+
const existingBlock = isRecord$1(existing["subagents"]) ? existing[POOL_SUBAGENTS_KEY] : {};
|
|
70044
|
+
const block = {};
|
|
70045
|
+
for (const [key, value] of Object.entries(existingBlock)) if (!isPrototypePollutionKey(key) && key !== "agents") block[key] = value;
|
|
70046
|
+
const existingAgents = isRecord$1(existingBlock["agents"]) ? existingBlock[POOL_SUBAGENTS_AGENTS_KEY] : {};
|
|
70047
|
+
const general = existingAgents[POOL_GENERAL_AGENT_NAME];
|
|
70048
|
+
const agents = {
|
|
70049
|
+
...general !== void 0 ? { [POOL_GENERAL_AGENT_NAME]: general } : {},
|
|
70050
|
+
...this.agents
|
|
70051
|
+
};
|
|
70052
|
+
const dropped = Object.keys(existingAgents).filter((name) => name !== "general" && !Object.hasOwn(agents, name));
|
|
70053
|
+
if (dropped.length > 0) {
|
|
70054
|
+
const advice = this.global ? "hand-written agents belong in .rulesync/subagents/" : `hand-written agents belong in ${POOL_SETTINGS_LOCAL_FILE_NAME}`;
|
|
70055
|
+
this.logger?.warn(`Removed Pool subagent(s) ${dropped.map(quoteValueForWarning).join(", ")} from ${filePath}: rulesync recomputes "${POOL_SUBAGENTS_KEY}.${POOL_SUBAGENTS_AGENTS_KEY}" from its own subagents on every run, so ${advice}.`);
|
|
70056
|
+
}
|
|
70057
|
+
if (Object.keys(agents).length > 0) block[POOL_SUBAGENTS_AGENTS_KEY] = agents;
|
|
70058
|
+
return applySharedConfigPatch({
|
|
70059
|
+
fileKey: sharedConfigFileKey(paths),
|
|
70060
|
+
feature: "subagents",
|
|
70061
|
+
existingContent,
|
|
70062
|
+
patch: { [POOL_SUBAGENTS_KEY]: Object.keys(block).length > 0 ? block : void 0 },
|
|
70063
|
+
filePath,
|
|
70064
|
+
logger: this.logger
|
|
70065
|
+
});
|
|
70066
|
+
}
|
|
70067
|
+
/**
|
|
70068
|
+
* Every agent lives in the one settings file, which the MCP feature writes
|
|
70069
|
+
* too: naming the file here keeps the gitignore derivation from claiming the
|
|
70070
|
+
* whole directory and lets the shared-file derivation order the writers.
|
|
70071
|
+
*/
|
|
70072
|
+
static getSettablePaths({ global = false } = {}) {
|
|
70073
|
+
return {
|
|
70074
|
+
relativeDirPath: global ? POOL_GLOBAL_DIR : POOL_DIR,
|
|
70075
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME
|
|
70076
|
+
};
|
|
70077
|
+
}
|
|
70078
|
+
/**
|
|
70079
|
+
* Map a single rulesync subagent to a Pool agent entry. The body becomes
|
|
70080
|
+
* `instructions` and the shared `description` is written as-is; the
|
|
70081
|
+
* optional `pool:` section supplies `type` and the type-specific keys and
|
|
70082
|
+
* may override both. Returns `null` for the reserved `general` name, which
|
|
70083
|
+
* Pool defines itself.
|
|
70084
|
+
*/
|
|
70085
|
+
static toPoolAgent({ rulesyncSubagent, logger }) {
|
|
70086
|
+
const frontmatter = rulesyncSubagent.getFrontmatter();
|
|
70087
|
+
const name = frontmatter.name;
|
|
70088
|
+
if (name === "general") {
|
|
70089
|
+
logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: "${POOL_GENERAL_AGENT_NAME}" is Pool's built-in agent, so configure it in settings.yaml directly.`);
|
|
70090
|
+
return null;
|
|
70091
|
+
}
|
|
70092
|
+
if (isPrototypePollutionKey(name)) {
|
|
70093
|
+
logger?.warn(`Pool subagent ${quoteValueForWarning(name)} was skipped: unsafe name.`);
|
|
70094
|
+
return null;
|
|
70095
|
+
}
|
|
70096
|
+
const rawSection = isRecord$1(frontmatter.pool) ? frontmatter.pool : {};
|
|
70097
|
+
const result = PoolAgentSchema.safeParse(rawSection);
|
|
70098
|
+
if (!result.success) throw new Error(`Invalid "pool" section of subagent ${quoteValueForWarning(name)}: ${formatError(result.error)}`);
|
|
70099
|
+
const { type, description, instructions, ...rest } = result.data;
|
|
70100
|
+
const body = rulesyncSubagent.getBody().trim();
|
|
70101
|
+
const effectiveDescription = description ?? frontmatter.description;
|
|
70102
|
+
const effectiveInstructions = instructions ?? (body !== "" ? body : void 0);
|
|
70103
|
+
if (effectiveDescription === void 0 && rest.disabled !== true) logger?.warn(`Pool subagent ${quoteValueForWarning(name)} has no description; Pool requires one for every enabled custom agent.`);
|
|
70104
|
+
return {
|
|
70105
|
+
name,
|
|
70106
|
+
agent: {
|
|
70107
|
+
type: type ?? DEFAULT_POOL_AGENT_TYPE,
|
|
70108
|
+
...effectiveDescription !== void 0 ? { description: effectiveDescription } : {},
|
|
70109
|
+
...effectiveInstructions !== void 0 ? { instructions: effectiveInstructions } : {},
|
|
70110
|
+
...rest
|
|
70111
|
+
}
|
|
70112
|
+
};
|
|
70113
|
+
}
|
|
70114
|
+
/**
|
|
70115
|
+
* Aggregate every targeted rulesync subagent into the one settings file.
|
|
70116
|
+
* Agents are keyed by their `name`; a repeated name keeps the last one, with
|
|
70117
|
+
* a warning, so the map stays what Pool would read.
|
|
70118
|
+
*/
|
|
70119
|
+
static fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, validate = true, global = false, logger }) {
|
|
70120
|
+
const agents = {};
|
|
70121
|
+
for (const rulesyncSubagent of rulesyncSubagents) {
|
|
70122
|
+
const converted = this.toPoolAgent({
|
|
70123
|
+
rulesyncSubagent,
|
|
70124
|
+
logger
|
|
70125
|
+
});
|
|
70126
|
+
if (converted === null) continue;
|
|
70127
|
+
if (Object.hasOwn(agents, converted.name)) logger?.warn(`Pool subagent ${quoteValueForWarning(converted.name)} is defined more than once; the last definition (${rulesyncSubagent.getRelativePathFromCwd()}) wins.`);
|
|
70128
|
+
agents[converted.name] = converted.agent;
|
|
70129
|
+
}
|
|
70130
|
+
return new this({
|
|
70131
|
+
outputRoot,
|
|
70132
|
+
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
70133
|
+
relativeFilePath: POOL_SETTINGS_FILE_NAME,
|
|
70134
|
+
agents,
|
|
70135
|
+
validate,
|
|
70136
|
+
global,
|
|
70137
|
+
logger
|
|
70138
|
+
});
|
|
70139
|
+
}
|
|
70140
|
+
static fromRulesyncSubagent({ outputRoot = process.cwd(), rulesyncSubagent, validate = true, global = false, logger }) {
|
|
70141
|
+
return this.fromRulesyncSubagents({
|
|
70142
|
+
outputRoot,
|
|
70143
|
+
rulesyncSubagents: [rulesyncSubagent],
|
|
70144
|
+
validate,
|
|
70145
|
+
global,
|
|
70146
|
+
logger
|
|
70147
|
+
});
|
|
70148
|
+
}
|
|
70149
|
+
/**
|
|
70150
|
+
* Convert every custom agent of the settings file back into an individual
|
|
70151
|
+
* rulesync subagent. `instructions` becomes the body, `description` the
|
|
70152
|
+
* shared field, and everything else — `type` only when it is not the
|
|
70153
|
+
* default — rides the `pool:` section so it survives the round-trip.
|
|
70154
|
+
*/
|
|
70155
|
+
toRulesyncSubagents() {
|
|
70156
|
+
return Object.entries(this.agents).map(([name, agent]) => {
|
|
70157
|
+
const { type, description, instructions, ...rest } = agent;
|
|
70158
|
+
const poolSection = {
|
|
70159
|
+
...type !== void 0 && type !== DEFAULT_POOL_AGENT_TYPE ? { type } : {},
|
|
70160
|
+
...rest
|
|
70161
|
+
};
|
|
70162
|
+
const rulesyncFrontmatter = {
|
|
70163
|
+
targets: ["pool"],
|
|
70164
|
+
name,
|
|
70165
|
+
...description !== void 0 ? { description } : {},
|
|
70166
|
+
...Object.keys(poolSection).length > 0 ? { pool: poolSection } : {}
|
|
70167
|
+
};
|
|
70168
|
+
return new RulesyncSubagent({
|
|
70169
|
+
outputRoot: process.cwd(),
|
|
70170
|
+
frontmatter: rulesyncFrontmatter,
|
|
70171
|
+
body: instructions ?? "",
|
|
70172
|
+
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
|
|
70173
|
+
relativeFilePath: `${sanitizePoolAgentFileStem(name)}.md`,
|
|
70174
|
+
validate: true
|
|
70175
|
+
});
|
|
70176
|
+
});
|
|
70177
|
+
}
|
|
70178
|
+
toRulesyncSubagent() {
|
|
70179
|
+
const first = this.toRulesyncSubagents()[0];
|
|
70180
|
+
if (!first) throw new Error(`No custom subagents found in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())} to convert.`);
|
|
70181
|
+
return first;
|
|
70182
|
+
}
|
|
70183
|
+
validate() {
|
|
70184
|
+
return {
|
|
70185
|
+
success: true,
|
|
70186
|
+
error: null
|
|
70187
|
+
};
|
|
70188
|
+
}
|
|
70189
|
+
static isTargetedByRulesyncSubagent(rulesyncSubagent) {
|
|
70190
|
+
return this.isTargetedByRulesyncSubagentDefault({
|
|
70191
|
+
rulesyncSubagent,
|
|
70192
|
+
toolTarget: "pool"
|
|
70193
|
+
});
|
|
70194
|
+
}
|
|
70195
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true, global = false, logger }) {
|
|
70196
|
+
const paths = this.getSettablePaths({ global });
|
|
70197
|
+
const filePath = join(outputRoot, paths.relativeDirPath, relativeFilePath);
|
|
70198
|
+
const settings = parsePoolSettings(await readFileContentOrNull(filePath) ?? "", filePath);
|
|
70199
|
+
return new this({
|
|
70200
|
+
outputRoot,
|
|
70201
|
+
relativeDirPath: paths.relativeDirPath,
|
|
70202
|
+
relativeFilePath,
|
|
70203
|
+
agents: agentsOfSettings({
|
|
70204
|
+
settings,
|
|
70205
|
+
filePath,
|
|
70206
|
+
logger
|
|
70207
|
+
}),
|
|
70208
|
+
validate,
|
|
70209
|
+
global
|
|
70210
|
+
});
|
|
70211
|
+
}
|
|
70212
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
70213
|
+
return new this({
|
|
70214
|
+
outputRoot,
|
|
70215
|
+
relativeDirPath,
|
|
70216
|
+
relativeFilePath,
|
|
70217
|
+
agents: {},
|
|
70218
|
+
validate: false,
|
|
70219
|
+
global
|
|
70220
|
+
});
|
|
70221
|
+
}
|
|
70222
|
+
};
|
|
70223
|
+
//#endregion
|
|
68470
70224
|
//#region src/features/subagents/reasonix-subagent.ts
|
|
68471
70225
|
const ReasonixSubagentFrontmatterSchema = z.looseObject({
|
|
68472
70226
|
name: z.string(),
|
|
@@ -69787,6 +71541,16 @@ const toolSubagentFactories = /* @__PURE__ */ new Map([
|
|
|
69787
71541
|
filePattern: "*.md"
|
|
69788
71542
|
}
|
|
69789
71543
|
}],
|
|
71544
|
+
["pool", {
|
|
71545
|
+
class: PoolSubagent,
|
|
71546
|
+
meta: {
|
|
71547
|
+
supportsProject: true,
|
|
71548
|
+
supportsSimulated: false,
|
|
71549
|
+
supportsGlobal: true,
|
|
71550
|
+
emitsEmptyAggregate: true,
|
|
71551
|
+
filePattern: POOL_SETTINGS_FILE_NAME
|
|
71552
|
+
}
|
|
71553
|
+
}],
|
|
69790
71554
|
["qwencode", {
|
|
69791
71555
|
class: QwencodeSubagent,
|
|
69792
71556
|
meta: {
|
|
@@ -69899,6 +71663,12 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
69899
71663
|
toolTarget;
|
|
69900
71664
|
global;
|
|
69901
71665
|
getFactory;
|
|
71666
|
+
/**
|
|
71667
|
+
* Whether the last `loadRulesyncFiles` found a `subagents/` directory under
|
|
71668
|
+
* any input root. A project without one has not adopted the feature, so the
|
|
71669
|
+
* agents in its Pool settings are its own and must not be retracted.
|
|
71670
|
+
*/
|
|
71671
|
+
rulesyncSourceDirFound = false;
|
|
69902
71672
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$1, dryRun = false, logger }) {
|
|
69903
71673
|
super({
|
|
69904
71674
|
outputRoot,
|
|
@@ -69912,16 +71682,20 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
69912
71682
|
this.global = global;
|
|
69913
71683
|
this.getFactory = getFactory;
|
|
69914
71684
|
}
|
|
71685
|
+
emitsToolFilesForEmptySource() {
|
|
71686
|
+
return this.rulesyncSourceDirFound && this.getFactory(this.toolTarget).meta.emitsEmptyAggregate === true;
|
|
71687
|
+
}
|
|
69915
71688
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
69916
71689
|
const rulesyncSubagents = rulesyncFiles.filter((file) => file instanceof RulesyncSubagent);
|
|
69917
71690
|
const factory = this.getFactory(this.toolTarget);
|
|
69918
71691
|
const targeted = rulesyncSubagents.filter((rulesyncSubagent) => factory.class.isTargetedByRulesyncSubagent(rulesyncSubagent));
|
|
69919
71692
|
if (factory.class.fromRulesyncSubagents) {
|
|
69920
|
-
if (targeted.length === 0) return [];
|
|
71693
|
+
if (targeted.length === 0 && !this.emitsToolFilesForEmptySource()) return [];
|
|
69921
71694
|
const toolSubagents = factory.class.fromRulesyncSubagents({
|
|
69922
71695
|
outputRoot: this.outputRoot,
|
|
69923
71696
|
rulesyncSubagents: targeted,
|
|
69924
|
-
global: this.global
|
|
71697
|
+
global: this.global,
|
|
71698
|
+
logger: this.logger
|
|
69925
71699
|
});
|
|
69926
71700
|
return Array.isArray(toolSubagents) ? toolSubagents : [toolSubagents];
|
|
69927
71701
|
}
|
|
@@ -69977,6 +71751,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
69977
71751
|
this.logger.debug(`Rulesync subagents directory not found: ${subagentsDir}`);
|
|
69978
71752
|
return [];
|
|
69979
71753
|
}
|
|
71754
|
+
this.rulesyncSourceDirFound = true;
|
|
69980
71755
|
const mdFiles = (await listDirectoryEntryNames(subagentsDir)).filter((file) => file.endsWith(".md"));
|
|
69981
71756
|
if (mdFiles.length === 0) {
|
|
69982
71757
|
this.logger.debug(`No markdown files found in rulesync subagents directory: ${subagentsDir}`);
|
|
@@ -70017,6 +71792,7 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70017
71792
|
* earlier root's copy.
|
|
70018
71793
|
*/
|
|
70019
71794
|
async loadRulesyncFiles() {
|
|
71795
|
+
this.rulesyncSourceDirFound = false;
|
|
70020
71796
|
const rulesyncSubagents = mergeByCaseInsensitiveIdentity({
|
|
70021
71797
|
perRoot: await Promise.all(this.inputRoots.map((root) => this.loadRulesyncFilesForRoot(root))),
|
|
70022
71798
|
identity: (subagent) => subagent.getRelativeFilePath(),
|
|
@@ -70079,7 +71855,8 @@ var SubagentsProcessor = class extends FeatureProcessor {
|
|
|
70079
71855
|
outputRoot: rootOutputRoot,
|
|
70080
71856
|
relativeDirPath: dirPath,
|
|
70081
71857
|
relativeFilePath: toRelativeFilePath(path),
|
|
70082
|
-
global: this.global
|
|
71858
|
+
global: this.global,
|
|
71859
|
+
logger: this.logger
|
|
70083
71860
|
})));
|
|
70084
71861
|
toolSubagents.push(...this.claimStandaloneSubagents({
|
|
70085
71862
|
loaded,
|
|
@@ -74001,22 +75778,30 @@ var DevinRule = class DevinRule extends ToolRule {
|
|
|
74001
75778
|
}
|
|
74002
75779
|
};
|
|
74003
75780
|
//#endregion
|
|
74004
|
-
//#region src/features/rules/
|
|
75781
|
+
//#region src/features/rules/nested-agentsmd-rule.ts
|
|
74005
75782
|
/**
|
|
74006
|
-
*
|
|
75783
|
+
* Shared adapter for the targets that read instructions exactly the way the
|
|
75784
|
+
* AGENTS.md standard describes them and nothing more: a root `AGENTS.md` at the
|
|
75785
|
+
* project root, nested per-directory `AGENTS.md` files loaded only while working
|
|
75786
|
+
* under that directory, and a single personal root file under a tool-specific
|
|
75787
|
+
* home directory. Such a target has no modular non-root instruction directory,
|
|
75788
|
+
* so every other non-root rule folds into the root file.
|
|
74007
75789
|
*
|
|
74008
|
-
*
|
|
74009
|
-
*
|
|
74010
|
-
*
|
|
74011
|
-
* `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness reads as a
|
|
74012
|
-
* single root file with no local overlay, so non-root rules fold into it.
|
|
75790
|
+
* Concrete targets (Pool, Vibe, DeepSeek Harness) only supply the global
|
|
75791
|
+
* directory and their tool target through {@link getFamily}; the vendor docs
|
|
75792
|
+
* describing each one's discovery walk live on the subclass.
|
|
74013
75793
|
*
|
|
74014
|
-
*
|
|
75794
|
+
* The nested-file scan mirrors the AGENTS.md standard's nested discovery —
|
|
75795
|
+
* same file name, same exclusions, import-only, project scope — because it
|
|
75796
|
+
* discovers literally the same files.
|
|
74015
75797
|
*/
|
|
74016
|
-
var
|
|
75798
|
+
var NestedAgentsmdRule = class extends ToolRule {
|
|
75799
|
+
static getFamily() {
|
|
75800
|
+
throw new Error("Please implement this method in the subclass.");
|
|
75801
|
+
}
|
|
74017
75802
|
static getSettablePaths({ global = false } = {}) {
|
|
74018
75803
|
if (global) return { root: {
|
|
74019
|
-
relativeDirPath:
|
|
75804
|
+
relativeDirPath: this.getFamily().globalDir,
|
|
74020
75805
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74021
75806
|
} };
|
|
74022
75807
|
return { root: {
|
|
@@ -74024,18 +75809,6 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74024
75809
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME
|
|
74025
75810
|
} };
|
|
74026
75811
|
}
|
|
74027
|
-
/**
|
|
74028
|
-
* DeepSeek Harness loads the project chain of `AGENTS.md` files from the
|
|
74029
|
-
* project root (the nearest ancestor holding `.git`) down through the session
|
|
74030
|
-
* cwd, broad to specific, so a per-directory file applies only while working
|
|
74031
|
-
* under that directory. Nested files are therefore a real scoping surface,
|
|
74032
|
-
* not just the root file's overflow.
|
|
74033
|
-
*
|
|
74034
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
74035
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
74036
|
-
* literally the same files.
|
|
74037
|
-
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
74038
|
-
*/
|
|
74039
75812
|
static getNestedFilePatterns() {
|
|
74040
75813
|
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
74041
75814
|
}
|
|
@@ -74050,7 +75823,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74050
75823
|
const { root } = this.getSettablePaths({ global });
|
|
74051
75824
|
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
74052
75825
|
const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
74053
|
-
return new
|
|
75826
|
+
return new this({
|
|
74054
75827
|
outputRoot,
|
|
74055
75828
|
relativeDirPath: overrideDirPath,
|
|
74056
75829
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74060,7 +75833,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74060
75833
|
});
|
|
74061
75834
|
}
|
|
74062
75835
|
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
74063
|
-
return new
|
|
75836
|
+
return new this({
|
|
74064
75837
|
outputRoot,
|
|
74065
75838
|
relativeDirPath: root.relativeDirPath,
|
|
74066
75839
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74074,7 +75847,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74074
75847
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
74075
75848
|
const isRoot = frontmatter.root ?? false;
|
|
74076
75849
|
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
74077
|
-
if (!global && !isRoot && subprojectPath) return new
|
|
75850
|
+
if (!global && !isRoot && subprojectPath) return new this({
|
|
74078
75851
|
outputRoot,
|
|
74079
75852
|
relativeDirPath: join(subprojectPath),
|
|
74080
75853
|
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
@@ -74082,7 +75855,7 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74082
75855
|
validate,
|
|
74083
75856
|
root: false
|
|
74084
75857
|
});
|
|
74085
|
-
return new
|
|
75858
|
+
return new this({
|
|
74086
75859
|
outputRoot,
|
|
74087
75860
|
relativeDirPath: root.relativeDirPath,
|
|
74088
75861
|
relativeFilePath: root.relativeFilePath,
|
|
@@ -74103,23 +75876,47 @@ var DshRule = class DshRule extends ToolRule {
|
|
|
74103
75876
|
};
|
|
74104
75877
|
}
|
|
74105
75878
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
74106
|
-
|
|
75879
|
+
const isRoot = relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === this.getFamily().globalDir);
|
|
75880
|
+
return new this({
|
|
74107
75881
|
outputRoot,
|
|
74108
75882
|
relativeDirPath,
|
|
74109
75883
|
relativeFilePath,
|
|
74110
75884
|
fileContent: "",
|
|
74111
75885
|
validate: false,
|
|
74112
|
-
root:
|
|
75886
|
+
root: isRoot
|
|
74113
75887
|
});
|
|
74114
75888
|
}
|
|
74115
75889
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
74116
75890
|
return this.isTargetedByRulesyncRuleDefault({
|
|
74117
75891
|
rulesyncRule,
|
|
74118
|
-
toolTarget:
|
|
75892
|
+
toolTarget: this.getFamily().toolTarget
|
|
74119
75893
|
});
|
|
74120
75894
|
}
|
|
74121
75895
|
};
|
|
74122
75896
|
//#endregion
|
|
75897
|
+
//#region src/features/rules/dsh-rule.ts
|
|
75898
|
+
/**
|
|
75899
|
+
* DeepSeek Harness (`dsh`) rules.
|
|
75900
|
+
*
|
|
75901
|
+
* Project scope writes the root `AGENTS.md` plus nested `<dir>/AGENTS.md`
|
|
75902
|
+
* files — the project chain `dsh-agent-instructions` loads from the `.git`
|
|
75903
|
+
* root down to the session cwd, broad to specific, so a per-directory file
|
|
75904
|
+
* applies only while working under that directory. Global scope writes the
|
|
75905
|
+
* user-global `~/.dsh/AGENTS.md` (`$DSH_HOME` default), which the harness
|
|
75906
|
+
* reads as a single root file with no `.local.md` overlay, so non-root rules
|
|
75907
|
+
* fold into it.
|
|
75908
|
+
*
|
|
75909
|
+
* @see https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/context/agent-instructions/README.md
|
|
75910
|
+
*/
|
|
75911
|
+
var DshRule = class extends NestedAgentsmdRule {
|
|
75912
|
+
static getFamily() {
|
|
75913
|
+
return {
|
|
75914
|
+
globalDir: DSH_DIR,
|
|
75915
|
+
toolTarget: "dsh"
|
|
75916
|
+
};
|
|
75917
|
+
}
|
|
75918
|
+
};
|
|
75919
|
+
//#endregion
|
|
74123
75920
|
//#region src/features/rules/factorydroid-rule.ts
|
|
74124
75921
|
/**
|
|
74125
75922
|
* Rule generator for Factory Droid.
|
|
@@ -75513,110 +77310,24 @@ var PiRule = class PiRule extends ToolRule {
|
|
|
75513
77310
|
};
|
|
75514
77311
|
//#endregion
|
|
75515
77312
|
//#region src/features/rules/pool-rule.ts
|
|
75516
|
-
|
|
75517
|
-
|
|
75518
|
-
|
|
75519
|
-
|
|
75520
|
-
|
|
75521
|
-
|
|
75522
|
-
|
|
75523
|
-
|
|
75524
|
-
|
|
75525
|
-
|
|
75526
|
-
|
|
75527
|
-
|
|
75528
|
-
|
|
75529
|
-
|
|
75530
|
-
* through the working directory (deeper files take precedence), skipping
|
|
75531
|
-
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
75532
|
-
* just the root file's overflow.
|
|
75533
|
-
*
|
|
75534
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
75535
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
75536
|
-
* literally the same files.
|
|
75537
|
-
* @see https://docs.poolside.ai/agent-instructions
|
|
75538
|
-
*/
|
|
75539
|
-
static getNestedFilePatterns() {
|
|
75540
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75541
|
-
}
|
|
75542
|
-
/**
|
|
75543
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
75544
|
-
* (project or global).
|
|
75545
|
-
*/
|
|
75546
|
-
getSubprojectPath() {
|
|
75547
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
75548
|
-
}
|
|
75549
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
75550
|
-
const { root } = this.getSettablePaths({ global });
|
|
75551
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
75552
|
-
const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
75553
|
-
return new PoolRule({
|
|
75554
|
-
outputRoot,
|
|
75555
|
-
relativeDirPath: overrideDirPath,
|
|
75556
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75557
|
-
fileContent,
|
|
75558
|
-
validate,
|
|
75559
|
-
root: false
|
|
75560
|
-
});
|
|
75561
|
-
}
|
|
75562
|
-
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
75563
|
-
return new PoolRule({
|
|
75564
|
-
outputRoot,
|
|
75565
|
-
relativeDirPath: root.relativeDirPath,
|
|
75566
|
-
relativeFilePath: root.relativeFilePath,
|
|
75567
|
-
fileContent,
|
|
75568
|
-
validate,
|
|
75569
|
-
root: true
|
|
75570
|
-
});
|
|
75571
|
-
}
|
|
75572
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
75573
|
-
const { root } = this.getSettablePaths({ global });
|
|
75574
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
75575
|
-
const isRoot = frontmatter.root ?? false;
|
|
75576
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
75577
|
-
if (!global && !isRoot && subprojectPath) return new PoolRule({
|
|
75578
|
-
outputRoot,
|
|
75579
|
-
relativeDirPath: join(subprojectPath),
|
|
75580
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
75581
|
-
fileContent: rulesyncRule.getBody(),
|
|
75582
|
-
validate,
|
|
75583
|
-
root: false
|
|
75584
|
-
});
|
|
75585
|
-
return new PoolRule({
|
|
75586
|
-
outputRoot,
|
|
75587
|
-
relativeDirPath: root.relativeDirPath,
|
|
75588
|
-
relativeFilePath: root.relativeFilePath,
|
|
75589
|
-
fileContent: rulesyncRule.getBody(),
|
|
75590
|
-
validate,
|
|
75591
|
-
root: isRoot
|
|
75592
|
-
});
|
|
75593
|
-
}
|
|
75594
|
-
toRulesyncRule() {
|
|
75595
|
-
const subprojectPath = this.getSubprojectPath();
|
|
75596
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
75597
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
75598
|
-
}
|
|
75599
|
-
validate() {
|
|
77313
|
+
/**
|
|
77314
|
+
* Pool (Poolside) rules.
|
|
77315
|
+
*
|
|
77316
|
+
* Pool reads personal, project, and directory-level `AGENTS.md` files: inside
|
|
77317
|
+
* a git repository it loads every `AGENTS.md` from the repository root down
|
|
77318
|
+
* through the working directory (deeper files take precedence), skipping
|
|
77319
|
+
* ignored directories. Nested files are therefore a real scoping surface, not
|
|
77320
|
+
* just the root file's overflow. The personal file sits below both at
|
|
77321
|
+
* `~/.config/poolside/AGENTS.md`.
|
|
77322
|
+
*
|
|
77323
|
+
* @see https://docs.poolside.ai/agent-instructions
|
|
77324
|
+
*/
|
|
77325
|
+
var PoolRule = class extends NestedAgentsmdRule {
|
|
77326
|
+
static getFamily() {
|
|
75600
77327
|
return {
|
|
75601
|
-
|
|
75602
|
-
error: null
|
|
75603
|
-
};
|
|
75604
|
-
}
|
|
75605
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
75606
|
-
return new PoolRule({
|
|
75607
|
-
outputRoot,
|
|
75608
|
-
relativeDirPath,
|
|
75609
|
-
relativeFilePath,
|
|
75610
|
-
fileContent: "",
|
|
75611
|
-
validate: false,
|
|
75612
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === POOL_GLOBAL_DIR)
|
|
75613
|
-
});
|
|
75614
|
-
}
|
|
75615
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
75616
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
75617
|
-
rulesyncRule,
|
|
77328
|
+
globalDir: POOL_GLOBAL_DIR,
|
|
75618
77329
|
toolTarget: "pool"
|
|
75619
|
-
}
|
|
77330
|
+
};
|
|
75620
77331
|
}
|
|
75621
77332
|
};
|
|
75622
77333
|
//#endregion
|
|
@@ -76580,110 +78291,23 @@ var TaktRule = class TaktRule extends ToolRule {
|
|
|
76580
78291
|
//#endregion
|
|
76581
78292
|
//#region src/features/rules/vibe-rule.ts
|
|
76582
78293
|
const VIBE_GLOBAL_DIR = ".vibe";
|
|
76583
|
-
|
|
76584
|
-
|
|
76585
|
-
|
|
76586
|
-
|
|
76587
|
-
|
|
76588
|
-
|
|
76589
|
-
|
|
76590
|
-
|
|
76591
|
-
|
|
76592
|
-
|
|
76593
|
-
|
|
76594
|
-
|
|
76595
|
-
|
|
76596
|
-
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
76597
|
-
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
76598
|
-
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
76599
|
-
* the root file's overflow.
|
|
76600
|
-
*
|
|
76601
|
-
* The scan mirrors the AGENTS.md standard's nested discovery — same file
|
|
76602
|
-
* name, same exclusions, import-only, project scope — because it discovers
|
|
76603
|
-
* literally the same files.
|
|
76604
|
-
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
76605
|
-
*/
|
|
76606
|
-
static getNestedFilePatterns() {
|
|
76607
|
-
return this.buildNestedFilePatterns({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76608
|
-
}
|
|
76609
|
-
/**
|
|
76610
|
-
* The subproject directory this rule scopes, or `undefined` for the root file
|
|
76611
|
-
* (project or global).
|
|
76612
|
-
*/
|
|
76613
|
-
getSubprojectPath() {
|
|
76614
|
-
return this.getNestedSubprojectPath({ fileName: AGENTSMD_RULE_FILE_NAME });
|
|
76615
|
-
}
|
|
76616
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath: _relativeFilePath, relativeDirPath: overrideDirPath, validate = true, global = false }) {
|
|
76617
|
-
const { root } = this.getSettablePaths({ global });
|
|
76618
|
-
if (overrideDirPath !== void 0 && overrideDirPath !== root.relativeDirPath && overrideDirPath !== ".") {
|
|
76619
|
-
const fileContent = await readFileContent(join(outputRoot, overrideDirPath, AGENTSMD_RULE_FILE_NAME));
|
|
76620
|
-
return new VibeRule({
|
|
76621
|
-
outputRoot,
|
|
76622
|
-
relativeDirPath: overrideDirPath,
|
|
76623
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
76624
|
-
fileContent,
|
|
76625
|
-
validate,
|
|
76626
|
-
root: false
|
|
76627
|
-
});
|
|
76628
|
-
}
|
|
76629
|
-
const fileContent = await readFileContent(join(outputRoot, root.relativeDirPath, root.relativeFilePath));
|
|
76630
|
-
return new VibeRule({
|
|
76631
|
-
outputRoot,
|
|
76632
|
-
relativeDirPath: root.relativeDirPath,
|
|
76633
|
-
relativeFilePath: root.relativeFilePath,
|
|
76634
|
-
fileContent,
|
|
76635
|
-
validate,
|
|
76636
|
-
root: true
|
|
76637
|
-
});
|
|
76638
|
-
}
|
|
76639
|
-
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true, global = false }) {
|
|
76640
|
-
const { root } = this.getSettablePaths({ global });
|
|
76641
|
-
const frontmatter = rulesyncRule.getFrontmatter();
|
|
76642
|
-
const isRoot = frontmatter.root ?? false;
|
|
76643
|
-
const subprojectPath = frontmatter.agentsmd?.subprojectPath;
|
|
76644
|
-
if (!global && !isRoot && subprojectPath) return new VibeRule({
|
|
76645
|
-
outputRoot,
|
|
76646
|
-
relativeDirPath: join(subprojectPath),
|
|
76647
|
-
relativeFilePath: AGENTSMD_RULE_FILE_NAME,
|
|
76648
|
-
fileContent: rulesyncRule.getBody(),
|
|
76649
|
-
validate,
|
|
76650
|
-
root: false
|
|
76651
|
-
});
|
|
76652
|
-
return new VibeRule({
|
|
76653
|
-
outputRoot,
|
|
76654
|
-
relativeDirPath: root.relativeDirPath,
|
|
76655
|
-
relativeFilePath: root.relativeFilePath,
|
|
76656
|
-
fileContent: rulesyncRule.getBody(),
|
|
76657
|
-
validate,
|
|
76658
|
-
root: isRoot
|
|
76659
|
-
});
|
|
76660
|
-
}
|
|
76661
|
-
toRulesyncRule() {
|
|
76662
|
-
const subprojectPath = this.getSubprojectPath();
|
|
76663
|
-
if (subprojectPath === void 0) return this.toRulesyncRuleDefault();
|
|
76664
|
-
return this.toRulesyncRuleNestedAgentsmd({ subprojectPath });
|
|
76665
|
-
}
|
|
76666
|
-
validate() {
|
|
78294
|
+
/**
|
|
78295
|
+
* Mistral Vibe rules.
|
|
78296
|
+
*
|
|
78297
|
+
* Vibe's harness manager walks the directories between the workspace root and
|
|
78298
|
+
* the file being read and loads every `AGENTS.md` it finds along the way
|
|
78299
|
+
* (`find_subdirectory_agents_md`), injecting the result into the `read_file`
|
|
78300
|
+
* tool's output. Nested files are therefore a real scoping surface, not just
|
|
78301
|
+
* the root file's overflow. The personal file is `~/.vibe/AGENTS.md`.
|
|
78302
|
+
*
|
|
78303
|
+
* @see https://github.com/mistralai/mistral-vibe/blob/main/vibe/core/config/harness_files/_harness_manager.py
|
|
78304
|
+
*/
|
|
78305
|
+
var VibeRule = class extends NestedAgentsmdRule {
|
|
78306
|
+
static getFamily() {
|
|
76667
78307
|
return {
|
|
76668
|
-
|
|
76669
|
-
error: null
|
|
76670
|
-
};
|
|
76671
|
-
}
|
|
76672
|
-
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
76673
|
-
return new VibeRule({
|
|
76674
|
-
outputRoot,
|
|
76675
|
-
relativeDirPath,
|
|
76676
|
-
relativeFilePath,
|
|
76677
|
-
fileContent: "",
|
|
76678
|
-
validate: false,
|
|
76679
|
-
root: relativeFilePath === "AGENTS.md" && (relativeDirPath === "." || relativeDirPath === ".vibe")
|
|
76680
|
-
});
|
|
76681
|
-
}
|
|
76682
|
-
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
76683
|
-
return this.isTargetedByRulesyncRuleDefault({
|
|
76684
|
-
rulesyncRule,
|
|
78308
|
+
globalDir: VIBE_GLOBAL_DIR,
|
|
76685
78309
|
toolTarget: "vibe"
|
|
76686
|
-
}
|
|
78310
|
+
};
|
|
76687
78311
|
}
|
|
76688
78312
|
};
|
|
76689
78313
|
//#endregion
|
|
@@ -78890,6 +80514,16 @@ async function activateHermesProjectPlugins({ pluginNames, dryRun, logger }) {
|
|
|
78890
80514
|
relativeFilePath: HERMESAGENT_CONFIG_FILE_PATH
|
|
78891
80515
|
});
|
|
78892
80516
|
const configPath = join(configRoot, relativeConfigPath);
|
|
80517
|
+
if (await refusesWriteOutsideRoot({
|
|
80518
|
+
logger,
|
|
80519
|
+
rootPath: configRoot,
|
|
80520
|
+
targetPath: configPath
|
|
80521
|
+
})) return {
|
|
80522
|
+
count: 0,
|
|
80523
|
+
paths: [],
|
|
80524
|
+
hasDiff: false,
|
|
80525
|
+
sourceLoadFailed: false
|
|
80526
|
+
};
|
|
78893
80527
|
const expectedConfig = mergeEnabledPlugins({
|
|
78894
80528
|
existingContent: await readFileContentOrNull(configPath) ?? "",
|
|
78895
80529
|
filePath: configPath,
|
|
@@ -79357,7 +80991,8 @@ async function processEmptyFeatureGeneration(params) {
|
|
|
79357
80991
|
*/
|
|
79358
80992
|
async function processFeatureWithRulesyncFiles(params) {
|
|
79359
80993
|
const { config, processor, rulesyncFiles, sweepPlan, skipFilePaths, onToolFiles } = params;
|
|
79360
|
-
|
|
80994
|
+
const retractsForEmptySource = processor.emitsToolFilesForEmptySource() && !processor.hasRulesyncSourceLoadFailure();
|
|
80995
|
+
if (rulesyncFiles.length === 0 && !retractsForEmptySource) return processEmptyFeatureGeneration({
|
|
79361
80996
|
config,
|
|
79362
80997
|
processor,
|
|
79363
80998
|
sweepPlan,
|
|
@@ -80589,4 +82224,4 @@ async function importChecksCore(params) {
|
|
|
80589
82224
|
//#endregion
|
|
80590
82225
|
export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
|
|
80591
82226
|
|
|
80592
|
-
//# sourceMappingURL=import-
|
|
82227
|
+
//# sourceMappingURL=import-B2ANxi1V.js.map
|