rulesync 9.8.0 → 10.0.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/dist/cli/index.cjs +96 -31
- package/dist/cli/index.js +96 -31
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-Dioh9ca8.js → import-B02AyWkQ.js} +506 -55
- package/dist/import-B02AyWkQ.js.map +1 -0
- package/dist/{import-l0o1jVdc.cjs → import-Dcj5HiOE.cjs} +544 -63
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-Dioh9ca8.js.map +0 -1
|
@@ -99,6 +99,9 @@ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH,
|
|
|
99
99
|
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
100
100
|
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
101
101
|
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
102
|
+
const RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
103
|
+
const RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
104
|
+
const RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
102
105
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
103
106
|
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$1(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
104
107
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
@@ -109,8 +112,12 @@ const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
|
109
112
|
const RULESYNC_MCP_FILE_NAME = "mcp.json";
|
|
110
113
|
const RULESYNC_HOOKS_FILE_NAME = "hooks.json";
|
|
111
114
|
const RULESYNC_PERMISSIONS_FILE_NAME = "permissions.json";
|
|
115
|
+
const RULESYNC_MCP_JSONC_FILE_NAME = "mcp.jsonc";
|
|
116
|
+
const RULESYNC_HOOKS_JSONC_FILE_NAME = "hooks.jsonc";
|
|
117
|
+
const RULESYNC_PERMISSIONS_JSONC_FILE_NAME = "permissions.jsonc";
|
|
112
118
|
const RULESYNC_CONFIG_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/config-schema.json";
|
|
113
119
|
const RULESYNC_MCP_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/mcp-schema.json";
|
|
120
|
+
const RULESYNC_PERMISSIONS_SCHEMA_URL = "https://github.com/dyoshikawa/rulesync/releases/latest/download/permissions-schema.json";
|
|
114
121
|
const MAX_FILE_SIZE = 10 * 1024 * 1024;
|
|
115
122
|
//#endregion
|
|
116
123
|
//#region src/types/tool-target-tuples.ts
|
|
@@ -7317,12 +7324,55 @@ function toolHooksToCanonical({ hooks, converterConfig }) {
|
|
|
7317
7324
|
return canonical;
|
|
7318
7325
|
}
|
|
7319
7326
|
//#endregion
|
|
7327
|
+
//#region src/utils/jsonc.ts
|
|
7328
|
+
/**
|
|
7329
|
+
* Rebuild the parsed value from its own enumerable entries, dropping
|
|
7330
|
+
* prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
|
|
7331
|
+
* `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
|
|
7332
|
+
* literal `__proto__` key would replace the containing object's prototype
|
|
7333
|
+
* instead of becoming an own property; rebuilding gives every object a clean
|
|
7334
|
+
* `Object.prototype` again and severs that path.
|
|
7335
|
+
*/
|
|
7336
|
+
function deepSanitize(value) {
|
|
7337
|
+
if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
|
|
7338
|
+
if (value !== null && typeof value === "object") {
|
|
7339
|
+
const sanitized = {};
|
|
7340
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7341
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
7342
|
+
sanitized[key] = deepSanitize(entry);
|
|
7343
|
+
}
|
|
7344
|
+
return sanitized;
|
|
7345
|
+
}
|
|
7346
|
+
return value;
|
|
7347
|
+
}
|
|
7348
|
+
/**
|
|
7349
|
+
* Parse a JSONC (JSON with Comments) document strictly.
|
|
7350
|
+
*
|
|
7351
|
+
* Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
|
|
7352
|
+
* errors and returns a best-effort value), this throws on any parse error so
|
|
7353
|
+
* a malformed source file fails loudly instead of generating half-empty tool
|
|
7354
|
+
* configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
|
|
7355
|
+
* `JSON.parse` on files that may contain comments or trailing commas.
|
|
7356
|
+
*/
|
|
7357
|
+
function parseJsonc(content) {
|
|
7358
|
+
const errors = [];
|
|
7359
|
+
const result = parse(content, errors, {
|
|
7360
|
+
allowTrailingComma: true,
|
|
7361
|
+
disallowComments: false
|
|
7362
|
+
});
|
|
7363
|
+
if (errors.length > 0) {
|
|
7364
|
+
const details = errors.map((e) => `${printParseErrorCode(e.error)} at offset ${e.offset}`).join(", ");
|
|
7365
|
+
throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
|
|
7366
|
+
}
|
|
7367
|
+
return deepSanitize(result);
|
|
7368
|
+
}
|
|
7369
|
+
//#endregion
|
|
7320
7370
|
//#region src/features/hooks/rulesync-hooks.ts
|
|
7321
7371
|
var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
7322
7372
|
json;
|
|
7323
7373
|
constructor(params) {
|
|
7324
7374
|
super({ ...params });
|
|
7325
|
-
this.json =
|
|
7375
|
+
this.json = parseJsonc(this.fileContent);
|
|
7326
7376
|
if (params.validate) {
|
|
7327
7377
|
const result = this.validate();
|
|
7328
7378
|
if (!result.success) throw result.error;
|
|
@@ -7331,7 +7381,11 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
|
7331
7381
|
static getSettablePaths() {
|
|
7332
7382
|
return {
|
|
7333
7383
|
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
7334
|
-
relativeFilePath:
|
|
7384
|
+
relativeFilePath: RULESYNC_HOOKS_FILE_NAME,
|
|
7385
|
+
jsonc: {
|
|
7386
|
+
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
7387
|
+
relativeFilePath: RULESYNC_HOOKS_JSONC_FILE_NAME
|
|
7388
|
+
}
|
|
7335
7389
|
};
|
|
7336
7390
|
}
|
|
7337
7391
|
validate() {
|
|
@@ -7347,16 +7401,23 @@ var RulesyncHooks = class RulesyncHooks extends RulesyncFile {
|
|
|
7347
7401
|
}
|
|
7348
7402
|
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
7349
7403
|
const paths = RulesyncHooks.getSettablePaths();
|
|
7350
|
-
const
|
|
7351
|
-
if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} found.`);
|
|
7352
|
-
const fileContent = await readFileContent(filePath);
|
|
7353
|
-
return new RulesyncHooks({
|
|
7354
|
-
outputRoot,
|
|
7404
|
+
const candidates = [paths.jsonc, {
|
|
7355
7405
|
relativeDirPath: paths.relativeDirPath,
|
|
7356
|
-
relativeFilePath: paths.relativeFilePath
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7406
|
+
relativeFilePath: paths.relativeFilePath
|
|
7407
|
+
}];
|
|
7408
|
+
for (const candidate of candidates) {
|
|
7409
|
+
const filePath = join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
|
|
7410
|
+
if (!await fileExists(filePath)) continue;
|
|
7411
|
+
const fileContent = await readFileContent(filePath);
|
|
7412
|
+
return new RulesyncHooks({
|
|
7413
|
+
outputRoot,
|
|
7414
|
+
relativeDirPath: candidate.relativeDirPath,
|
|
7415
|
+
relativeFilePath: candidate.relativeFilePath,
|
|
7416
|
+
fileContent,
|
|
7417
|
+
validate
|
|
7418
|
+
});
|
|
7419
|
+
}
|
|
7420
|
+
throw new Error(`No ${RULESYNC_HOOKS_RELATIVE_FILE_PATH} or ${RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH} found.`);
|
|
7360
7421
|
}
|
|
7361
7422
|
getJson() {
|
|
7362
7423
|
return this.json;
|
|
@@ -12364,15 +12425,51 @@ const RulesyncMcpServerSchema = z.extend(McpServerSchema, {
|
|
|
12364
12425
|
exposed: z.optional(z.boolean())
|
|
12365
12426
|
});
|
|
12366
12427
|
const RulesyncMcpConfigSchema = z.object({ mcpServers: z.record(z.string(), RulesyncMcpServerSchema) });
|
|
12428
|
+
/**
|
|
12429
|
+
* Tool-scoped MCP block: servers that apply only to one tool. A named entry
|
|
12430
|
+
* replaces/adds the same-named shared server wholesale for that tool; `null`
|
|
12431
|
+
* removes the shared server for that tool. Mirrors `{toolname}.hooks` in
|
|
12432
|
+
* `.rulesync/hooks.json` and `{toolname}.permission` in
|
|
12433
|
+
* `.rulesync/permissions.json`.
|
|
12434
|
+
*/
|
|
12435
|
+
const toolScopedMcpSchema = z.looseObject({ mcpServers: z.optional(z.record(z.string(), z.nullable(RulesyncMcpServerSchema))) });
|
|
12367
12436
|
const RulesyncMcpFileSchema = z.looseObject({
|
|
12368
12437
|
$schema: z.optional(z.string()),
|
|
12369
|
-
...RulesyncMcpConfigSchema.shape
|
|
12438
|
+
...RulesyncMcpConfigSchema.shape,
|
|
12439
|
+
amp: z.optional(toolScopedMcpSchema),
|
|
12440
|
+
"antigravity-cli": z.optional(toolScopedMcpSchema),
|
|
12441
|
+
"antigravity-ide": z.optional(toolScopedMcpSchema),
|
|
12442
|
+
augmentcode: z.optional(toolScopedMcpSchema),
|
|
12443
|
+
claudecode: z.optional(toolScopedMcpSchema),
|
|
12444
|
+
cline: z.optional(toolScopedMcpSchema),
|
|
12445
|
+
codexcli: z.optional(toolScopedMcpSchema),
|
|
12446
|
+
copilot: z.optional(toolScopedMcpSchema),
|
|
12447
|
+
copilotcli: z.optional(toolScopedMcpSchema),
|
|
12448
|
+
cursor: z.optional(toolScopedMcpSchema),
|
|
12449
|
+
deepagents: z.optional(toolScopedMcpSchema),
|
|
12450
|
+
devin: z.optional(toolScopedMcpSchema),
|
|
12451
|
+
factorydroid: z.optional(toolScopedMcpSchema),
|
|
12452
|
+
goose: z.optional(toolScopedMcpSchema),
|
|
12453
|
+
grokcli: z.optional(toolScopedMcpSchema),
|
|
12454
|
+
hermesagent: z.optional(toolScopedMcpSchema),
|
|
12455
|
+
junie: z.optional(toolScopedMcpSchema),
|
|
12456
|
+
kilo: z.optional(toolScopedMcpSchema),
|
|
12457
|
+
kiro: z.optional(toolScopedMcpSchema),
|
|
12458
|
+
opencode: z.optional(toolScopedMcpSchema),
|
|
12459
|
+
qwencode: z.optional(toolScopedMcpSchema),
|
|
12460
|
+
reasonix: z.optional(toolScopedMcpSchema),
|
|
12461
|
+
roo: z.optional(toolScopedMcpSchema),
|
|
12462
|
+
rovodev: z.optional(toolScopedMcpSchema),
|
|
12463
|
+
takt: z.optional(toolScopedMcpSchema),
|
|
12464
|
+
vibe: z.optional(toolScopedMcpSchema),
|
|
12465
|
+
warp: z.optional(toolScopedMcpSchema),
|
|
12466
|
+
zed: z.optional(toolScopedMcpSchema)
|
|
12370
12467
|
});
|
|
12371
12468
|
var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
12372
12469
|
json;
|
|
12373
12470
|
constructor(params) {
|
|
12374
12471
|
super(params);
|
|
12375
|
-
this.json =
|
|
12472
|
+
this.json = parseJsonc(this.fileContent);
|
|
12376
12473
|
if (params.validate) {
|
|
12377
12474
|
const result = this.validate();
|
|
12378
12475
|
if (!result.success) throw result.error;
|
|
@@ -12382,7 +12479,11 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
12382
12479
|
return {
|
|
12383
12480
|
recommended: {
|
|
12384
12481
|
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
12385
|
-
relativeFilePath:
|
|
12482
|
+
relativeFilePath: RULESYNC_MCP_FILE_NAME
|
|
12483
|
+
},
|
|
12484
|
+
jsonc: {
|
|
12485
|
+
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
12486
|
+
relativeFilePath: RULESYNC_MCP_JSONC_FILE_NAME
|
|
12386
12487
|
},
|
|
12387
12488
|
legacy: {
|
|
12388
12489
|
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
@@ -12404,7 +12505,18 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
12404
12505
|
static async fromFile({ outputRoot = process.cwd(), validate = true, logger }) {
|
|
12405
12506
|
const paths = this.getSettablePaths();
|
|
12406
12507
|
const recommendedPath = join(outputRoot, paths.recommended.relativeDirPath, paths.recommended.relativeFilePath);
|
|
12508
|
+
const jsoncPath = join(outputRoot, paths.jsonc.relativeDirPath, paths.jsonc.relativeFilePath);
|
|
12407
12509
|
const legacyPath = join(outputRoot, paths.legacy.relativeDirPath, paths.legacy.relativeFilePath);
|
|
12510
|
+
if (await fileExists(jsoncPath)) {
|
|
12511
|
+
const fileContent = await readFileContent(jsoncPath);
|
|
12512
|
+
return new RulesyncMcp({
|
|
12513
|
+
outputRoot,
|
|
12514
|
+
relativeDirPath: paths.jsonc.relativeDirPath,
|
|
12515
|
+
relativeFilePath: paths.jsonc.relativeFilePath,
|
|
12516
|
+
fileContent,
|
|
12517
|
+
validate
|
|
12518
|
+
});
|
|
12519
|
+
}
|
|
12408
12520
|
if (await fileExists(recommendedPath)) {
|
|
12409
12521
|
const fileContent = await readFileContent(recommendedPath);
|
|
12410
12522
|
return new RulesyncMcp({
|
|
@@ -12464,7 +12576,141 @@ var RulesyncMcp = class RulesyncMcp extends RulesyncFile {
|
|
|
12464
12576
|
getJson() {
|
|
12465
12577
|
return this.json;
|
|
12466
12578
|
}
|
|
12467
|
-
|
|
12579
|
+
/**
|
|
12580
|
+
* Build the effective MCP config for one tool target:
|
|
12581
|
+
*
|
|
12582
|
+
* 1. Filter shared servers by the DEPRECATED per-server `targets` field
|
|
12583
|
+
* (missing/`["*"]` means every tool). A deprecation warning points at
|
|
12584
|
+
* the tool-scoped blocks that replace it.
|
|
12585
|
+
* 2. Overlay the tool-scoped `{toolname}.mcpServers` block(s): a named
|
|
12586
|
+
* entry replaces/adds the shared server wholesale for this tool; `null`
|
|
12587
|
+
* removes it.
|
|
12588
|
+
*
|
|
12589
|
+
* Targets that share one output file resolve identically so the shared
|
|
12590
|
+
* file's content never depends on which of them generates last — see
|
|
12591
|
+
* `resolveMcpTarget` for the alias groups (kiro trio, claudecode/-legacy,
|
|
12592
|
+
* and the Antigravity pair).
|
|
12593
|
+
*
|
|
12594
|
+
* Returns the same instance when neither mechanism is used.
|
|
12595
|
+
*/
|
|
12596
|
+
forTarget({ toolTarget, logger }) {
|
|
12597
|
+
const { blockKeys, acceptedTargetNames } = resolveMcpTarget({ toolTarget });
|
|
12598
|
+
const json = this.json;
|
|
12599
|
+
const sharedServers = this.json.mcpServers ?? {};
|
|
12600
|
+
const serverNamesWithTargets = Object.entries(sharedServers).filter(([, serverConfig]) => serverConfig.targets !== void 0).map(([serverName]) => serverName);
|
|
12601
|
+
if (serverNamesWithTargets.length > 0) this.warnTargetsDeprecationOnce({
|
|
12602
|
+
serverNamesWithTargets,
|
|
12603
|
+
logger
|
|
12604
|
+
});
|
|
12605
|
+
for (const ignoredKey of MCP_IGNORED_ALIAS_SOURCE_KEYS) {
|
|
12606
|
+
if (!isRecord(json[ignoredKey])) continue;
|
|
12607
|
+
this.warnOncePerFile(`alias:${ignoredKey}`, `The "${ignoredKey}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${MCP_BLOCK_KEY_ALIASES[ignoredKey]}" key instead.`, logger);
|
|
12608
|
+
}
|
|
12609
|
+
const toolBlockKeys = Object.keys(json).filter((key) => MCP_TOOL_BLOCK_KEYS.has(key));
|
|
12610
|
+
if (serverNamesWithTargets.length === 0 && toolBlockKeys.length === 0) return this;
|
|
12611
|
+
const effectiveServers = Object.fromEntries(Object.entries(sharedServers).filter(([, serverConfig]) => {
|
|
12612
|
+
const targets = serverConfig.targets;
|
|
12613
|
+
if (targets === void 0) return true;
|
|
12614
|
+
return targets.some((target) => target === "*" || acceptedTargetNames.has(target));
|
|
12615
|
+
}));
|
|
12616
|
+
for (const blockKey of blockKeys) {
|
|
12617
|
+
const toolBlock = json[blockKey];
|
|
12618
|
+
const toolServers = isRecord(toolBlock) && isRecord(toolBlock.mcpServers) ? toolBlock.mcpServers : void 0;
|
|
12619
|
+
for (const [serverName, serverConfig] of Object.entries(toolServers ?? {})) {
|
|
12620
|
+
if (isPrototypePollutionKey(serverName)) continue;
|
|
12621
|
+
if (serverConfig === null) delete effectiveServers[serverName];
|
|
12622
|
+
else effectiveServers[serverName] = serverConfig;
|
|
12623
|
+
}
|
|
12624
|
+
}
|
|
12625
|
+
const rest = Object.fromEntries(Object.entries(json).filter(([key]) => !MCP_TOOL_BLOCK_KEYS.has(key)));
|
|
12626
|
+
return new RulesyncMcp({
|
|
12627
|
+
outputRoot: this.outputRoot,
|
|
12628
|
+
relativeDirPath: this.relativeDirPath,
|
|
12629
|
+
relativeFilePath: this.relativeFilePath,
|
|
12630
|
+
fileContent: JSON.stringify({
|
|
12631
|
+
...rest,
|
|
12632
|
+
mcpServers: effectiveServers
|
|
12633
|
+
}, null, 2)
|
|
12634
|
+
});
|
|
12635
|
+
}
|
|
12636
|
+
/**
|
|
12637
|
+
* The deprecation warning would otherwise repeat once per generated tool
|
|
12638
|
+
* target (a full `--targets "*"` run creates one RulesyncMcp per target),
|
|
12639
|
+
* so it is deduplicated per source file path.
|
|
12640
|
+
*/
|
|
12641
|
+
warnTargetsDeprecationOnce({ serverNamesWithTargets, logger }) {
|
|
12642
|
+
this.warnOncePerFile("targets-deprecation", `The per-server "targets" field in ${join(this.relativeDirPath, this.relativeFilePath)} is deprecated (servers: ${serverNamesWithTargets.join(", ")}). Author tool-scoped "{toolname}.mcpServers" blocks instead.`, logger);
|
|
12643
|
+
}
|
|
12644
|
+
warnOncePerFile(kind, message, logger) {
|
|
12645
|
+
const dedupeKey = `${this.getFilePath()}#${kind}`;
|
|
12646
|
+
if (warnedOnceKeys.has(dedupeKey)) return;
|
|
12647
|
+
warnedOnceKeys.add(dedupeKey);
|
|
12648
|
+
logger?.warn(message);
|
|
12649
|
+
}
|
|
12650
|
+
};
|
|
12651
|
+
/**
|
|
12652
|
+
* All keys that may hold a tool-scoped `{toolname}.mcpServers` block. Derived
|
|
12653
|
+
* from the MCP processor's target tuple so a newly added MCP-capable tool is
|
|
12654
|
+
* covered automatically.
|
|
12655
|
+
*/
|
|
12656
|
+
const MCP_TOOL_BLOCK_KEYS = new Set(mcpProcessorToolTargetTuple);
|
|
12657
|
+
/**
|
|
12658
|
+
* Targets whose `{toolname}.mcpServers` block key is ALWAYS another target's
|
|
12659
|
+
* key (at every scope): `claudecode-legacy` is a deprecated alias of
|
|
12660
|
+
* `claudecode`, and the Kiro IDE/CLI targets share the `kiro` block because
|
|
12661
|
+
* all three write the same `.kiro/settings/mcp.json` at both scopes. Blocks
|
|
12662
|
+
* authored under these source names are never read (a warning is emitted).
|
|
12663
|
+
*/
|
|
12664
|
+
const MCP_BLOCK_KEY_ALIASES = {
|
|
12665
|
+
"claudecode-legacy": "claudecode",
|
|
12666
|
+
"kiro-cli": "kiro",
|
|
12667
|
+
"kiro-ide": "kiro"
|
|
12668
|
+
};
|
|
12669
|
+
const MCP_IGNORED_ALIAS_SOURCE_KEYS = Object.keys(MCP_BLOCK_KEY_ALIASES);
|
|
12670
|
+
/**
|
|
12671
|
+
* Resolve which tool-scoped blocks a target reads and which deprecated
|
|
12672
|
+
* `targets` names match it. Targets that share one output file must resolve
|
|
12673
|
+
* identically, otherwise the shared file's content would depend on which
|
|
12674
|
+
* target happened to generate last:
|
|
12675
|
+
*
|
|
12676
|
+
* - `claudecode-legacy` aliases `claudecode`; the Kiro trio shares `kiro`
|
|
12677
|
+
* (same output file at both scopes).
|
|
12678
|
+
* - `antigravity-ide` / `antigravity-cli` share their output file at BOTH
|
|
12679
|
+
* scopes — `.agents/mcp_config.json` (project) and
|
|
12680
|
+
* `~/.gemini/config/mcp_config.json` (global; both global subdirs are
|
|
12681
|
+
* `config`) — so both targets always apply both blocks in a fixed order
|
|
12682
|
+
* (`antigravity-ide` first, `antigravity-cli` second — the CLI block wins
|
|
12683
|
+
* per server on conflict).
|
|
12684
|
+
*/
|
|
12685
|
+
function resolveMcpTarget({ toolTarget }) {
|
|
12686
|
+
if (toolTarget === "claudecode" || toolTarget === "claudecode-legacy") return {
|
|
12687
|
+
blockKeys: ["claudecode"],
|
|
12688
|
+
acceptedTargetNames: /* @__PURE__ */ new Set(["claudecode", "claudecode-legacy"])
|
|
12689
|
+
};
|
|
12690
|
+
if (toolTarget === "kiro" || toolTarget === "kiro-cli" || toolTarget === "kiro-ide") return {
|
|
12691
|
+
blockKeys: ["kiro"],
|
|
12692
|
+
acceptedTargetNames: /* @__PURE__ */ new Set([
|
|
12693
|
+
"kiro",
|
|
12694
|
+
"kiro-cli",
|
|
12695
|
+
"kiro-ide"
|
|
12696
|
+
])
|
|
12697
|
+
};
|
|
12698
|
+
if (toolTarget === "antigravity-ide" || toolTarget === "antigravity-cli") return {
|
|
12699
|
+
blockKeys: ["antigravity-ide", "antigravity-cli"],
|
|
12700
|
+
acceptedTargetNames: /* @__PURE__ */ new Set(["antigravity-ide", "antigravity-cli"])
|
|
12701
|
+
};
|
|
12702
|
+
return {
|
|
12703
|
+
blockKeys: [toolTarget],
|
|
12704
|
+
acceptedTargetNames: /* @__PURE__ */ new Set([toolTarget])
|
|
12705
|
+
};
|
|
12706
|
+
}
|
|
12707
|
+
/**
|
|
12708
|
+
* Deduplication set for once-per-source-file warnings, keyed by
|
|
12709
|
+
* `<absolute file path>#<warning kind>`. Never cleared: rulesync CLI runs
|
|
12710
|
+
* are one-shot processes, and in a long-lived embedding (the rulesync MCP
|
|
12711
|
+
* server) repeating the same warning per generate would only add noise.
|
|
12712
|
+
*/
|
|
12713
|
+
const warnedOnceKeys = /* @__PURE__ */ new Set();
|
|
12468
12714
|
//#endregion
|
|
12469
12715
|
//#region src/features/mcp/tool-mcp.ts
|
|
12470
12716
|
var ToolMcp = class extends ToolFile {
|
|
@@ -16714,10 +16960,14 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
16714
16960
|
if (!rulesyncMcp) throw new Error(`No ${RULESYNC_MCP_RELATIVE_FILE_PATH} found.`);
|
|
16715
16961
|
const factory = this.getFactory(this.toolTarget);
|
|
16716
16962
|
return await Promise.all([rulesyncMcp].map(async (mcp) => {
|
|
16963
|
+
const targetedRulesyncMcp = mcp.forTarget({
|
|
16964
|
+
toolTarget: this.toolTarget,
|
|
16965
|
+
logger: this.logger
|
|
16966
|
+
});
|
|
16717
16967
|
const fieldsToStrip = [];
|
|
16718
16968
|
if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
|
|
16719
16969
|
if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
|
|
16720
|
-
const filteredRulesyncMcp =
|
|
16970
|
+
const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
|
|
16721
16971
|
return await factory.class.fromRulesyncMcp({
|
|
16722
16972
|
outputRoot: this.outputRoot,
|
|
16723
16973
|
rulesyncMcp: filteredRulesyncMcp,
|
|
@@ -16767,6 +17017,25 @@ const PermissionActionSchema = z.enum([
|
|
|
16767
17017
|
*/
|
|
16768
17018
|
const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
|
|
16769
17019
|
/**
|
|
17020
|
+
* Tool-scoped canonical `permission` block: the same shape as the shared
|
|
17021
|
+
* top-level `permission` record, but applied only to the tool whose override
|
|
17022
|
+
* key it appears under. During generation the categories placed here are
|
|
17023
|
+
* merged over the shared `permission` block per category (the tool-scoped
|
|
17024
|
+
* category replaces the shared one wholesale), mirroring how
|
|
17025
|
+
* `.rulesync/hooks.json` merges `{toolname}.hooks` per event.
|
|
17026
|
+
*
|
|
17027
|
+
* @example
|
|
17028
|
+
* { "claudecode": { "permission": { "bash": { "git push *": "deny" } } } }
|
|
17029
|
+
*/
|
|
17030
|
+
const ToolScopedPermissionSchema = z.record(z.string(), PermissionRulesSchema);
|
|
17031
|
+
/**
|
|
17032
|
+
* Generic tool-scoped override block for tools that have no tool-specific
|
|
17033
|
+
* override keys of their own; it carries only the canonical tool-scoped
|
|
17034
|
+
* `permission` block. Kept `looseObject` so future tool-specific keys can be
|
|
17035
|
+
* added without a schema break.
|
|
17036
|
+
*/
|
|
17037
|
+
const CanonicalPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
|
|
17038
|
+
/**
|
|
16770
17039
|
* OpenCode-specific permission value. Unlike the shared canonical block, which
|
|
16771
17040
|
* only accepts a pattern-to-action map, OpenCode also allows a bare action
|
|
16772
17041
|
* string that applies to the whole category (e.g. `"external_directory": "deny"`).
|
|
@@ -16796,7 +17065,7 @@ const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional
|
|
|
16796
17065
|
* @example
|
|
16797
17066
|
* { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
|
|
16798
17067
|
*/
|
|
16799
|
-
const HermesPermissionsOverrideSchema = z.looseObject({});
|
|
17068
|
+
const HermesPermissionsOverrideSchema = z.looseObject({ permission: z.optional(ToolScopedPermissionSchema) });
|
|
16800
17069
|
/**
|
|
16801
17070
|
* Tool-scoped override block for Cline. Cline's `command-permissions.json`
|
|
16802
17071
|
* carries a single global `allowRedirects` boolean (gates shell redirection
|
|
@@ -16808,7 +17077,10 @@ const HermesPermissionsOverrideSchema = z.looseObject({});
|
|
|
16808
17077
|
* @example
|
|
16809
17078
|
* { "allowRedirects": true }
|
|
16810
17079
|
*/
|
|
16811
|
-
const ClinePermissionsOverrideSchema = z.looseObject({
|
|
17080
|
+
const ClinePermissionsOverrideSchema = z.looseObject({
|
|
17081
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17082
|
+
allowRedirects: z.optional(z.boolean())
|
|
17083
|
+
});
|
|
16812
17084
|
/**
|
|
16813
17085
|
* Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
|
|
16814
17086
|
* object is a free-form record with tool-specific keys that have no canonical
|
|
@@ -16838,7 +17110,10 @@ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
|
|
|
16838
17110
|
* @example
|
|
16839
17111
|
* { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
|
|
16840
17112
|
*/
|
|
16841
|
-
const ClaudecodePermissionsOverrideSchema = z.looseObject({
|
|
17113
|
+
const ClaudecodePermissionsOverrideSchema = z.looseObject({
|
|
17114
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17115
|
+
permissions: z.optional(z.looseObject({}))
|
|
17116
|
+
});
|
|
16842
17117
|
/**
|
|
16843
17118
|
* Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
|
|
16844
17119
|
* carries a `sensitive_patterns` list — patterns that escalate to ASK even when
|
|
@@ -16870,6 +17145,7 @@ const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.r
|
|
|
16870
17145
|
* { "approvalMode": "auto-review" }
|
|
16871
17146
|
*/
|
|
16872
17147
|
const CursorPermissionsOverrideSchema = z.looseObject({
|
|
17148
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
16873
17149
|
approvalMode: z.optional(z.string()),
|
|
16874
17150
|
sandbox: z.optional(z.looseObject({}))
|
|
16875
17151
|
});
|
|
@@ -16895,6 +17171,7 @@ const CursorPermissionsOverrideSchema = z.looseObject({
|
|
|
16895
17171
|
* }
|
|
16896
17172
|
*/
|
|
16897
17173
|
const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
17174
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
16898
17175
|
tools: z.optional(z.looseObject({})),
|
|
16899
17176
|
security: z.optional(z.looseObject({})),
|
|
16900
17177
|
autoMode: z.optional(z.looseObject({}))
|
|
@@ -16916,6 +17193,7 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
|
16916
17193
|
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
16917
17194
|
*/
|
|
16918
17195
|
const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
17196
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
16919
17197
|
sandbox: z.optional(z.looseObject({})),
|
|
16920
17198
|
agent: z.optional(z.looseObject({}))
|
|
16921
17199
|
});
|
|
@@ -16935,7 +17213,10 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
16935
17213
|
* @example
|
|
16936
17214
|
* { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
|
|
16937
17215
|
*/
|
|
16938
|
-
const FactorydroidPermissionsOverrideSchema = z.looseObject({
|
|
17216
|
+
const FactorydroidPermissionsOverrideSchema = z.looseObject({
|
|
17217
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17218
|
+
commandBlocklist: z.optional(z.array(z.string()))
|
|
17219
|
+
});
|
|
16939
17220
|
/**
|
|
16940
17221
|
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
16941
17222
|
* file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
|
|
@@ -16952,6 +17233,7 @@ const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist:
|
|
|
16952
17233
|
* { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
|
|
16953
17234
|
*/
|
|
16954
17235
|
const WarpPermissionsOverrideSchema = z.looseObject({
|
|
17236
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
16955
17237
|
agent_mode_coding_permissions: z.optional(z.string()),
|
|
16956
17238
|
agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
|
|
16957
17239
|
agent_mode_execute_readonly_commands: z.optional(z.boolean())
|
|
@@ -16969,6 +17251,7 @@ const WarpPermissionsOverrideSchema = z.looseObject({
|
|
|
16969
17251
|
* { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
|
|
16970
17252
|
*/
|
|
16971
17253
|
const JuniePermissionsOverrideSchema = z.looseObject({
|
|
17254
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
16972
17255
|
allowReadonlyCommands: z.optional(z.boolean()),
|
|
16973
17256
|
defaultBehavior: z.optional(z.string())
|
|
16974
17257
|
});
|
|
@@ -16996,6 +17279,7 @@ const JuniePermissionsOverrideSchema = z.looseObject({
|
|
|
16996
17279
|
* { "step_permission_overrides": { "ai_review": "readonly" }, "provider_options": { "codex": { "network_access": true } } }
|
|
16997
17280
|
*/
|
|
16998
17281
|
const TaktPermissionsOverrideSchema = z.looseObject({
|
|
17282
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
16999
17283
|
step_permission_overrides: z.optional(z.record(z.string(), z.string())),
|
|
17000
17284
|
provider_options: z.optional(z.looseObject({}))
|
|
17001
17285
|
});
|
|
@@ -17023,6 +17307,7 @@ const TaktPermissionsOverrideSchema = z.looseObject({
|
|
|
17023
17307
|
* "permissions": [{ "tool": "Bash", "action": "delegate", "to": "approve.sh" }] }
|
|
17024
17308
|
*/
|
|
17025
17309
|
const AmpPermissionsOverrideSchema = z.looseObject({
|
|
17310
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17026
17311
|
permissions: z.optional(z.array(z.looseObject({
|
|
17027
17312
|
tool: z.string(),
|
|
17028
17313
|
action: z.string()
|
|
@@ -17051,6 +17336,7 @@ const AmpPermissionsOverrideSchema = z.looseObject({
|
|
|
17051
17336
|
* { "toolPermission": "strict", "enableTerminalSandbox": true }
|
|
17052
17337
|
*/
|
|
17053
17338
|
const AntigravityCliPermissionsOverrideSchema = z.looseObject({
|
|
17339
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17054
17340
|
toolPermission: z.optional(z.string()),
|
|
17055
17341
|
enableTerminalSandbox: z.optional(z.boolean())
|
|
17056
17342
|
});
|
|
@@ -17076,10 +17362,13 @@ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
|
|
|
17076
17362
|
* "permission": { "type": "webhook-policy", "webhookUrl": "https://api.example.com/validate" } },
|
|
17077
17363
|
* { "toolName": "view", "eventType": "tool-response", "permission": { "type": "allow" } } ] }
|
|
17078
17364
|
*/
|
|
17079
|
-
const AugmentcodePermissionsOverrideSchema = z.looseObject({
|
|
17080
|
-
|
|
17081
|
-
|
|
17082
|
-
|
|
17365
|
+
const AugmentcodePermissionsOverrideSchema = z.looseObject({
|
|
17366
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17367
|
+
toolPermissions: z.optional(z.array(z.looseObject({
|
|
17368
|
+
toolName: z.string(),
|
|
17369
|
+
permission: z.looseObject({ type: z.string() })
|
|
17370
|
+
})))
|
|
17371
|
+
});
|
|
17083
17372
|
/**
|
|
17084
17373
|
* Tool-scoped override block for Kiro. Kiro's agent config (`.kiro/agents/<name>.json`)
|
|
17085
17374
|
* exposes per-tool `toolsSettings` knobs with no canonical allow/ask/deny
|
|
@@ -17105,21 +17394,24 @@ const AugmentcodePermissionsOverrideSchema = z.looseObject({ toolPermissions: z.
|
|
|
17105
17394
|
* "aws": { "allowedServices": ["s3"], "deniedServices": ["eks"] },
|
|
17106
17395
|
* "web_fetch": { "trusted": [".*github\\.com.*"] } } }
|
|
17107
17396
|
*/
|
|
17108
|
-
const KiroPermissionsOverrideSchema = z.looseObject({
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17397
|
+
const KiroPermissionsOverrideSchema = z.looseObject({
|
|
17398
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17399
|
+
toolsSettings: z.optional(z.looseObject({
|
|
17400
|
+
shell: z.optional(z.looseObject({
|
|
17401
|
+
autoAllowReadonly: z.optional(z.boolean()),
|
|
17402
|
+
denyByDefault: z.optional(z.boolean())
|
|
17403
|
+
})),
|
|
17404
|
+
aws: z.optional(z.looseObject({
|
|
17405
|
+
allowedServices: z.optional(z.array(z.string())),
|
|
17406
|
+
deniedServices: z.optional(z.array(z.string())),
|
|
17407
|
+
autoAllowReadonly: z.optional(z.boolean())
|
|
17408
|
+
})),
|
|
17409
|
+
web_fetch: z.optional(z.looseObject({
|
|
17410
|
+
trusted: z.optional(z.array(z.string())),
|
|
17411
|
+
blocked: z.optional(z.array(z.string()))
|
|
17412
|
+
}))
|
|
17121
17413
|
}))
|
|
17122
|
-
})
|
|
17414
|
+
});
|
|
17123
17415
|
/**
|
|
17124
17416
|
* Codex CLI's approval-workflow policy. Serialized as a kebab-case string in
|
|
17125
17417
|
* `.codex/config.toml`. `on-failure` is a legacy alias for `on-request` that
|
|
@@ -17191,6 +17483,7 @@ const CodexApprovalsReviewerSchema = z.enum([
|
|
|
17191
17483
|
* "sandbox_workspace_write": { "network_access": true } }
|
|
17192
17484
|
*/
|
|
17193
17485
|
const CodexcliPermissionsOverrideSchema = z.looseObject({
|
|
17486
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
17194
17487
|
approval_policy: z.optional(z.union([CodexApprovalPolicySchema, z.looseObject({})])),
|
|
17195
17488
|
sandbox_mode: z.optional(CodexSandboxModeSchema),
|
|
17196
17489
|
sandbox_workspace_write: z.optional(z.looseObject({})),
|
|
@@ -17209,6 +17502,15 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
|
|
|
17209
17502
|
* `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
|
|
17210
17503
|
* block and ignores them.
|
|
17211
17504
|
*
|
|
17505
|
+
* Additionally, every permissions-capable tool accepts a canonical tool-scoped
|
|
17506
|
+
* `permission` block under its override key (`{toolname}.permission`, same
|
|
17507
|
+
* shape as the shared `permission` record). Its categories apply only to that
|
|
17508
|
+
* tool, merged over the shared block per category — see
|
|
17509
|
+
* `RulesyncPermissions.forTarget`. `kiro-cli`/`kiro-ide` alias to the `kiro`
|
|
17510
|
+
* key and `hermesagent` to `hermes` (matching the shared output file each
|
|
17511
|
+
* writes). OpenCode/Kilo/Vibe keep their existing tool-native `permission`
|
|
17512
|
+
* override semantics (handled by their translators, not the central merge).
|
|
17513
|
+
*
|
|
17212
17514
|
* @example
|
|
17213
17515
|
* {
|
|
17214
17516
|
* "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
|
|
@@ -17234,7 +17536,13 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
17234
17536
|
"antigravity-cli": z.optional(AntigravityCliPermissionsOverrideSchema),
|
|
17235
17537
|
augmentcode: z.optional(AugmentcodePermissionsOverrideSchema),
|
|
17236
17538
|
kiro: z.optional(KiroPermissionsOverrideSchema),
|
|
17237
|
-
codexcli: z.optional(CodexcliPermissionsOverrideSchema)
|
|
17539
|
+
codexcli: z.optional(CodexcliPermissionsOverrideSchema),
|
|
17540
|
+
"antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
|
|
17541
|
+
devin: z.optional(CanonicalPermissionsOverrideSchema),
|
|
17542
|
+
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
17543
|
+
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
17544
|
+
rovodev: z.optional(CanonicalPermissionsOverrideSchema),
|
|
17545
|
+
zed: z.optional(CanonicalPermissionsOverrideSchema)
|
|
17238
17546
|
});
|
|
17239
17547
|
/**
|
|
17240
17548
|
* Full permissions file schema including optional $schema field.
|
|
@@ -17249,7 +17557,7 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
17249
17557
|
json;
|
|
17250
17558
|
constructor(params) {
|
|
17251
17559
|
super({ ...params });
|
|
17252
|
-
this.json =
|
|
17560
|
+
this.json = parseJsonc(this.fileContent);
|
|
17253
17561
|
if (params.validate) {
|
|
17254
17562
|
const result = this.validate();
|
|
17255
17563
|
if (!result.success) throw result.error;
|
|
@@ -17258,7 +17566,11 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
17258
17566
|
static getSettablePaths() {
|
|
17259
17567
|
return {
|
|
17260
17568
|
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
17261
|
-
relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME
|
|
17569
|
+
relativeFilePath: RULESYNC_PERMISSIONS_FILE_NAME,
|
|
17570
|
+
jsonc: {
|
|
17571
|
+
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
|
|
17572
|
+
relativeFilePath: RULESYNC_PERMISSIONS_JSONC_FILE_NAME
|
|
17573
|
+
}
|
|
17262
17574
|
};
|
|
17263
17575
|
}
|
|
17264
17576
|
validate() {
|
|
@@ -17274,20 +17586,85 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
|
|
|
17274
17586
|
}
|
|
17275
17587
|
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
17276
17588
|
const paths = RulesyncPermissions.getSettablePaths();
|
|
17277
|
-
const
|
|
17278
|
-
if (!await fileExists(filePath)) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
|
|
17279
|
-
const fileContent = await readFileContent(filePath);
|
|
17280
|
-
return new RulesyncPermissions({
|
|
17281
|
-
outputRoot,
|
|
17589
|
+
const candidates = [paths.jsonc, {
|
|
17282
17590
|
relativeDirPath: paths.relativeDirPath,
|
|
17283
|
-
relativeFilePath: paths.relativeFilePath
|
|
17284
|
-
|
|
17285
|
-
|
|
17286
|
-
|
|
17591
|
+
relativeFilePath: paths.relativeFilePath
|
|
17592
|
+
}];
|
|
17593
|
+
for (const candidate of candidates) {
|
|
17594
|
+
const filePath = join(outputRoot, candidate.relativeDirPath, candidate.relativeFilePath);
|
|
17595
|
+
if (!await fileExists(filePath)) continue;
|
|
17596
|
+
const fileContent = await readFileContent(filePath);
|
|
17597
|
+
return new RulesyncPermissions({
|
|
17598
|
+
outputRoot,
|
|
17599
|
+
relativeDirPath: candidate.relativeDirPath,
|
|
17600
|
+
relativeFilePath: candidate.relativeFilePath,
|
|
17601
|
+
fileContent,
|
|
17602
|
+
validate
|
|
17603
|
+
});
|
|
17604
|
+
}
|
|
17605
|
+
throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} or ${RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH} found.`);
|
|
17287
17606
|
}
|
|
17288
17607
|
getJson() {
|
|
17289
17608
|
return this.json;
|
|
17290
17609
|
}
|
|
17610
|
+
/**
|
|
17611
|
+
* Build the effective permissions config for one tool target by merging the
|
|
17612
|
+
* tool-scoped `{toolname}.permission` block over the shared `permission`
|
|
17613
|
+
* block, per category (the tool-scoped category replaces the shared one
|
|
17614
|
+
* wholesale — mirroring how `{toolname}.hooks` merges per event in
|
|
17615
|
+
* `.rulesync/hooks.json`). The consumed `permission` key is stripped from
|
|
17616
|
+
* the override block so verbatim-passthrough translators (e.g. the Hermes
|
|
17617
|
+
* deep merge or the Codex CLI top-level key whitelist) never see it.
|
|
17618
|
+
*
|
|
17619
|
+
* Returns the same instance when the target has no tool-scoped canonical
|
|
17620
|
+
* `permission` block, or when the target's translator natively consumes its
|
|
17621
|
+
* own `permission` override shape (OpenCode / Kilo / Vibe).
|
|
17622
|
+
*/
|
|
17623
|
+
forTarget({ toolTarget, logger }) {
|
|
17624
|
+
if (NATIVE_PERMISSION_OVERRIDE_TARGETS.has(toolTarget)) return this;
|
|
17625
|
+
const overrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
|
|
17626
|
+
const json = this.json;
|
|
17627
|
+
if (overrideKey !== toolTarget && isRecord(json[toolTarget])) logger?.warn(`The "${toolTarget}" block in ${join(this.relativeDirPath, this.relativeFilePath)} is ignored. Author it under the "${overrideKey}" key instead (the ${toolTarget} target reads that block).`);
|
|
17628
|
+
const overrideBlock = json[overrideKey];
|
|
17629
|
+
if (!isRecord(overrideBlock) || !isRecord(overrideBlock.permission)) return this;
|
|
17630
|
+
const { permission: toolScopedPermission, ...restOverride } = overrideBlock;
|
|
17631
|
+
const merged = {
|
|
17632
|
+
...json,
|
|
17633
|
+
permission: {
|
|
17634
|
+
...this.json.permission,
|
|
17635
|
+
...toolScopedPermission
|
|
17636
|
+
}
|
|
17637
|
+
};
|
|
17638
|
+
if (Object.keys(restOverride).length > 0) merged[overrideKey] = restOverride;
|
|
17639
|
+
else delete merged[overrideKey];
|
|
17640
|
+
return new RulesyncPermissions({
|
|
17641
|
+
outputRoot: this.outputRoot,
|
|
17642
|
+
relativeDirPath: this.relativeDirPath,
|
|
17643
|
+
relativeFilePath: this.relativeFilePath,
|
|
17644
|
+
fileContent: JSON.stringify(merged, null, 2)
|
|
17645
|
+
});
|
|
17646
|
+
}
|
|
17647
|
+
};
|
|
17648
|
+
/**
|
|
17649
|
+
* Targets whose tool-scoped `permission` override uses tool-native semantics
|
|
17650
|
+
* consumed directly by their translator (bare action strings / tool-only
|
|
17651
|
+
* categories for OpenCode and Kilo, `sensitive_patterns` objects for Vibe).
|
|
17652
|
+
* The central canonical merge must not touch those blocks.
|
|
17653
|
+
*/
|
|
17654
|
+
const NATIVE_PERMISSION_OVERRIDE_TARGETS = /* @__PURE__ */ new Set([
|
|
17655
|
+
"opencode",
|
|
17656
|
+
"kilo",
|
|
17657
|
+
"vibe"
|
|
17658
|
+
]);
|
|
17659
|
+
/**
|
|
17660
|
+
* Targets that read their tool-scoped override from a differently named key:
|
|
17661
|
+
* Kiro IDE/CLI share the `kiro` block (they write the same agent config) and
|
|
17662
|
+
* Hermes Agent's established override key is `hermes`.
|
|
17663
|
+
*/
|
|
17664
|
+
const PERMISSION_OVERRIDE_KEY_ALIASES = {
|
|
17665
|
+
"kiro-cli": "kiro",
|
|
17666
|
+
"kiro-ide": "kiro",
|
|
17667
|
+
hermesagent: "hermes"
|
|
17291
17668
|
};
|
|
17292
17669
|
//#endregion
|
|
17293
17670
|
//#region src/features/permissions/tool-permissions.ts
|
|
@@ -19127,10 +19504,15 @@ var CodexcliPermissions = class CodexcliPermissions extends ToolPermissions {
|
|
|
19127
19504
|
existingDomainsHadUnknown,
|
|
19128
19505
|
logger
|
|
19129
19506
|
});
|
|
19130
|
-
|
|
19507
|
+
const profile = mergeWithExistingProfile({
|
|
19131
19508
|
newProfile,
|
|
19132
19509
|
existingProfile
|
|
19133
19510
|
});
|
|
19511
|
+
permissionsTable[RULESYNC_PROFILE_NAME] = preserveUnmanagedProfileKeys({
|
|
19512
|
+
rawExistingProfile: permissionsTable[RULESYNC_PROFILE_NAME],
|
|
19513
|
+
profile,
|
|
19514
|
+
logger
|
|
19515
|
+
});
|
|
19134
19516
|
const overridePatch = computeCodexcliOverridePatch({
|
|
19135
19517
|
existing,
|
|
19136
19518
|
override: rulesyncPermissions.getJson().codexcli,
|
|
@@ -19326,6 +19708,43 @@ function warnAboutPreservedProfileState({ existingProfile, newProfile, existingD
|
|
|
19326
19708
|
if (existingProfile?.network?.mode !== void 0) logger?.warn(`Preserving existing "network.mode" from config. Review this value manually as it may grant broader network access than the Rulesync-managed domain rules.`);
|
|
19327
19709
|
if (existingDomainsHadUnknown) logger?.warn(`Existing "network.domains" contained unrecognized values. These entries were skipped and will not be imported.`);
|
|
19328
19710
|
}
|
|
19711
|
+
const MANAGED_PROFILE_KEYS = /* @__PURE__ */ new Set([
|
|
19712
|
+
"description",
|
|
19713
|
+
"extends",
|
|
19714
|
+
"filesystem",
|
|
19715
|
+
"network"
|
|
19716
|
+
]);
|
|
19717
|
+
const MANAGED_NETWORK_KEYS = /* @__PURE__ */ new Set([
|
|
19718
|
+
"enabled",
|
|
19719
|
+
"mode",
|
|
19720
|
+
"domains",
|
|
19721
|
+
"unix_sockets"
|
|
19722
|
+
]);
|
|
19723
|
+
/**
|
|
19724
|
+
* Re-attach the keys of the existing rulesync profile that rulesync does not
|
|
19725
|
+
* manage. The managed keys always come from `profile` (the freshly computed
|
|
19726
|
+
* merge result); unmanaged siblings — at the profile level and inside the
|
|
19727
|
+
* `network` table — are preserved verbatim from the existing config so a
|
|
19728
|
+
* regeneration never deletes user-authored Codex settings.
|
|
19729
|
+
*/
|
|
19730
|
+
function preserveUnmanagedProfileKeys({ rawExistingProfile, profile, logger }) {
|
|
19731
|
+
const rawProfileTable = toMutableTable(rawExistingProfile);
|
|
19732
|
+
const profileExtras = Object.fromEntries(Object.entries(rawProfileTable).filter(([key]) => !MANAGED_PROFILE_KEYS.has(key)));
|
|
19733
|
+
const rawNetworkTable = toMutableTable(rawProfileTable.network);
|
|
19734
|
+
const networkExtras = Object.fromEntries(Object.entries(rawNetworkTable).filter(([key]) => !MANAGED_NETWORK_KEYS.has(key)));
|
|
19735
|
+
const preservedKeyNames = [...Object.keys(profileExtras), ...Object.keys(networkExtras).map((key) => `network.${key}`)];
|
|
19736
|
+
if (preservedKeyNames.length > 0) logger?.warn(`Preserving unmanaged keys in the "${RULESYNC_PROFILE_NAME}" permissions profile: ${preservedKeyNames.join(", ")}. Review them manually; rulesync carries them forward verbatim.`);
|
|
19737
|
+
const result = {
|
|
19738
|
+
...profileExtras,
|
|
19739
|
+
...profile
|
|
19740
|
+
};
|
|
19741
|
+
const mergedNetwork = {
|
|
19742
|
+
...networkExtras,
|
|
19743
|
+
...profile.network
|
|
19744
|
+
};
|
|
19745
|
+
if (Object.keys(mergedNetwork).length > 0) result.network = mergedNetwork;
|
|
19746
|
+
return result;
|
|
19747
|
+
}
|
|
19329
19748
|
function mergeWithExistingProfile({ newProfile, existingProfile }) {
|
|
19330
19749
|
if (!existingProfile) return newProfile;
|
|
19331
19750
|
const mergedNetwork = { ...newProfile.network };
|
|
@@ -23587,16 +24006,20 @@ var PermissionsProcessor = class extends FeatureProcessor {
|
|
|
23587
24006
|
if (!rulesyncPermissions) throw new Error(`No ${RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH} found.`);
|
|
23588
24007
|
const factory = toolPermissionsFactories.get(this.toolTarget);
|
|
23589
24008
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
24009
|
+
const effectivePermissions = rulesyncPermissions.forTarget({
|
|
24010
|
+
toolTarget: this.toolTarget,
|
|
24011
|
+
logger: this.logger
|
|
24012
|
+
});
|
|
23590
24013
|
const toolPermissions = await factory.class.fromRulesyncPermissions({
|
|
23591
24014
|
outputRoot: this.outputRoot,
|
|
23592
|
-
rulesyncPermissions,
|
|
24015
|
+
rulesyncPermissions: effectivePermissions,
|
|
23593
24016
|
logger: this.logger,
|
|
23594
24017
|
global: this.global
|
|
23595
24018
|
});
|
|
23596
24019
|
if (this.toolTarget !== "codexcli") return [toolPermissions];
|
|
23597
24020
|
return [toolPermissions, createCodexcliBashRulesFile({
|
|
23598
24021
|
outputRoot: this.outputRoot,
|
|
23599
|
-
config:
|
|
24022
|
+
config: effectivePermissions.getJson()
|
|
23600
24023
|
})];
|
|
23601
24024
|
}
|
|
23602
24025
|
async convertToolFilesToRulesyncFiles(toolFiles) {
|
|
@@ -39692,6 +40115,16 @@ async function generatePermissionsCore(params) {
|
|
|
39692
40115
|
//#endregion
|
|
39693
40116
|
//#region src/lib/import.ts
|
|
39694
40117
|
/**
|
|
40118
|
+
* Import always writes the `.json` variant of a fixed-path source file, but a
|
|
40119
|
+
* sibling `.jsonc` variant takes precedence at read time — so an import into a
|
|
40120
|
+
* project that authors the `.jsonc` variant would be silently shadowed. Warn
|
|
40121
|
+
* so the user merges (or removes) one of the two by hand.
|
|
40122
|
+
*/
|
|
40123
|
+
async function warnIfShadowedByJsonc(params) {
|
|
40124
|
+
const { outputRoot, jsonRelativePath, jsoncRelativePath, logger } = params;
|
|
40125
|
+
if (await fileExists(join(outputRoot, jsoncRelativePath))) logger.warn(`${jsoncRelativePath} exists and takes precedence over the imported ${jsonRelativePath}. Merge the imported content into ${jsoncRelativePath} (or remove it) so the import takes effect.`);
|
|
40126
|
+
}
|
|
40127
|
+
/**
|
|
39695
40128
|
* Import configuration files from AI tools.
|
|
39696
40129
|
*/
|
|
39697
40130
|
async function importFromTool(params) {
|
|
@@ -39803,6 +40236,12 @@ async function importMcpCore(params) {
|
|
|
39803
40236
|
}
|
|
39804
40237
|
const rulesyncFiles = await mcpProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
39805
40238
|
const { count: writtenCount } = await mcpProcessor.writeAiFiles(rulesyncFiles);
|
|
40239
|
+
if (writtenCount > 0) await warnIfShadowedByJsonc({
|
|
40240
|
+
outputRoot: config.getOutputRoots()[0] ?? ".",
|
|
40241
|
+
jsonRelativePath: RULESYNC_MCP_RELATIVE_FILE_PATH,
|
|
40242
|
+
jsoncRelativePath: RULESYNC_MCP_JSONC_RELATIVE_FILE_PATH,
|
|
40243
|
+
logger
|
|
40244
|
+
});
|
|
39806
40245
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} MCP files`);
|
|
39807
40246
|
return writtenCount;
|
|
39808
40247
|
}
|
|
@@ -39902,6 +40341,12 @@ async function importHooksCore(params) {
|
|
|
39902
40341
|
}
|
|
39903
40342
|
const rulesyncFiles = await hooksProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
39904
40343
|
const { count: writtenCount } = await hooksProcessor.writeAiFiles(rulesyncFiles);
|
|
40344
|
+
if (writtenCount > 0) await warnIfShadowedByJsonc({
|
|
40345
|
+
outputRoot: config.getOutputRoots()[0] ?? ".",
|
|
40346
|
+
jsonRelativePath: RULESYNC_HOOKS_RELATIVE_FILE_PATH,
|
|
40347
|
+
jsoncRelativePath: RULESYNC_HOOKS_JSONC_RELATIVE_FILE_PATH,
|
|
40348
|
+
logger
|
|
40349
|
+
});
|
|
39905
40350
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} hooks file(s)`);
|
|
39906
40351
|
return writtenCount;
|
|
39907
40352
|
}
|
|
@@ -39931,10 +40376,16 @@ async function importPermissionsCore(params) {
|
|
|
39931
40376
|
}
|
|
39932
40377
|
const rulesyncFiles = await permissionsProcessor.convertToolFilesToRulesyncFiles(toolFiles);
|
|
39933
40378
|
const { count: writtenCount } = await permissionsProcessor.writeAiFiles(rulesyncFiles);
|
|
40379
|
+
if (writtenCount > 0) await warnIfShadowedByJsonc({
|
|
40380
|
+
outputRoot: config.getOutputRoots()[0] ?? ".",
|
|
40381
|
+
jsonRelativePath: RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH,
|
|
40382
|
+
jsoncRelativePath: RULESYNC_PERMISSIONS_JSONC_RELATIVE_FILE_PATH,
|
|
40383
|
+
logger
|
|
40384
|
+
});
|
|
39934
40385
|
if (config.getVerbose() && writtenCount > 0) logger.success(`Created ${writtenCount} permissions file(s)`);
|
|
39935
40386
|
return writtenCount;
|
|
39936
40387
|
}
|
|
39937
40388
|
//#endregion
|
|
39938
|
-
export {
|
|
40389
|
+
export { removeFile as $, CLAUDECODE_SKILLS_DIR_PATH as A, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as At, ErrorCodes as B, RulesyncHooks as C, RULESYNC_PERMISSIONS_FILE_NAME as Ct, CLAUDECODE_LOCAL_RULE_FILE_NAME as D, RULESYNC_RELATIVE_DIR_PATH as Dt, CLAUDECODE_DIR as E, RULESYNC_PERMISSIONS_SCHEMA_URL as Et, ConfigResolver as F, fileExists as G, createTempDirectory as H, findControlCharacter as I, getHomeDirectory as J, findFilesByGlobs as K, ConsoleLogger as L, RulesyncCommandFrontmatterSchema as M, formatError as Mt, stringifyFrontmatter as N, ALL_FEATURES as Nt, CLAUDECODE_MEMORIES_DIR_NAME as O, RULESYNC_RULES_RELATIVE_DIR_PATH as Ot, loadYaml as P, ALL_FEATURES_WITH_WILDCARD as Pt, removeDirectory as Q, JsonLogger as R, HooksProcessor as S, RULESYNC_OVERVIEW_FILE_NAME as St, CODEXCLI_DIR as T, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Tt, directoryExists as U, checkPathTraversal as V, ensureDir as W, listDirectoryFiles as X, isSymlink as Y, readFileContent as Z, RulesyncPermissions as _, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as _t, convertFromTool as a, ToolTargetSchema as at, IgnoreProcessor as b, RULESYNC_MCP_RELATIVE_FILE_PATH as bt, RulesyncRuleFrontmatterSchema as c, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ct, RulesyncSubagentFrontmatterSchema as d, RULESYNC_CONFIG_SCHEMA_URL as dt, removeTempDirectory as et, SkillsProcessor as f, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as ft, SKILL_FILE_NAME as g, RULESYNC_IGNORE_RELATIVE_FILE_PATH as gt, RulesyncSkillFrontmatterSchema as h, RULESYNC_HOOKS_RELATIVE_FILE_PATH as ht, getProcessorRegistryEntry as i, ALL_TOOL_TARGETS_WITH_WILDCARD as it, RulesyncCommand as j, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as jt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as k, RULESYNC_SKILLS_RELATIVE_DIR_PATH as kt, SubagentsProcessor as l, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as lt, RulesyncSkill as m, RULESYNC_HOOKS_JSONC_FILE_NAME as mt, checkRulesyncDirExists as n, writeFileContent as nt, RulesProcessor as o, MAX_FILE_SIZE as ot, getLocalSkillDirNames as p, RULESYNC_HOOKS_FILE_NAME as pt, getFileSize as q, generate as r, ALL_TOOL_TARGETS as rt, RulesyncRule as s, RULESYNC_AIIGNORE_FILE_NAME as st, importFromTool as t, toPosixPath as tt, RulesyncSubagent as u, RULESYNC_CONFIG_RELATIVE_FILE_PATH as ut, McpProcessor as v, RULESYNC_MCP_FILE_NAME as vt, CommandsProcessor as w, RULESYNC_PERMISSIONS_JSONC_FILE_NAME as wt, RulesyncIgnore as x, RULESYNC_MCP_SCHEMA_URL as xt, RulesyncMcp as y, RULESYNC_MCP_JSONC_FILE_NAME as yt, CLIError as z };
|
|
39939
40390
|
|
|
39940
|
-
//# sourceMappingURL=import-
|
|
40391
|
+
//# sourceMappingURL=import-B02AyWkQ.js.map
|