rulesync 9.1.1 → 9.3.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 +12 -12
- package/dist/cli/index.js +12 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-DywcWsgJ.js → import-B7VzSVUK.js} +981 -204
- package/dist/import-B7VzSVUK.js.map +1 -0
- package/dist/{import-cNqo0ZxT.cjs → import-CE3Rx3Wt.cjs} +980 -203
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +16 -16
- package/dist/import-DywcWsgJ.js.map +0 -1
|
@@ -3003,8 +3003,8 @@ const DEVIN_SKILLS_DIR_PATH = join(DEVIN_DIR, "skills");
|
|
|
3003
3003
|
const DEVIN_AGENTS_DIR_PATH = join(DEVIN_DIR, "agents");
|
|
3004
3004
|
const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
|
|
3005
3005
|
const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
|
|
3006
|
+
const DEVIN_GLOBAL_SKILLS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
|
|
3006
3007
|
const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "global_workflows");
|
|
3007
|
-
const CODEIUM_WINDSURF_SKILLS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "skills");
|
|
3008
3008
|
const DEVIN_CONFIG_FILE_NAME = "config.json";
|
|
3009
3009
|
const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
3010
3010
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
@@ -5688,6 +5688,8 @@ const DEEPAGENTS_HOOK_EVENTS = [
|
|
|
5688
5688
|
"sessionEnd",
|
|
5689
5689
|
"beforeSubmitPrompt",
|
|
5690
5690
|
"permissionRequest",
|
|
5691
|
+
"preToolUse",
|
|
5692
|
+
"postToolUse",
|
|
5691
5693
|
"postToolUseFailure",
|
|
5692
5694
|
"stop",
|
|
5693
5695
|
"preCompact",
|
|
@@ -5713,7 +5715,13 @@ const CODEXCLI_HOOK_EVENTS = [
|
|
|
5713
5715
|
* Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
|
|
5714
5716
|
* maps PascalCase event names to matcher/handler arrays. Every Goose event has a
|
|
5715
5717
|
* 1:1 canonical equivalent, so no new canonical events are required.
|
|
5716
|
-
*
|
|
5718
|
+
*
|
|
5719
|
+
* Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
|
|
5720
|
+
* has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
|
|
5721
|
+
* Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
|
|
5722
|
+
* excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
|
|
5723
|
+
* @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
|
|
5724
|
+
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
|
|
5717
5725
|
*/
|
|
5718
5726
|
const GOOSE_HOOK_EVENTS = [
|
|
5719
5727
|
"sessionStart",
|
|
@@ -5726,9 +5734,7 @@ const GOOSE_HOOK_EVENTS = [
|
|
|
5726
5734
|
"beforeReadFile",
|
|
5727
5735
|
"afterFileEdit",
|
|
5728
5736
|
"beforeShellExecution",
|
|
5729
|
-
"afterShellExecution"
|
|
5730
|
-
"subagentStart",
|
|
5731
|
-
"subagentStop"
|
|
5737
|
+
"afterShellExecution"
|
|
5732
5738
|
];
|
|
5733
5739
|
/** Hook events supported by Kiro CLI. */
|
|
5734
5740
|
const KIRO_HOOK_EVENTS = [
|
|
@@ -6181,9 +6187,7 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
|
|
|
6181
6187
|
beforeReadFile: "BeforeReadFile",
|
|
6182
6188
|
afterFileEdit: "AfterFileEdit",
|
|
6183
6189
|
beforeShellExecution: "BeforeShellExecution",
|
|
6184
|
-
afterShellExecution: "AfterShellExecution"
|
|
6185
|
-
subagentStart: "SubagentStart",
|
|
6186
|
-
subagentStop: "SubagentStop"
|
|
6190
|
+
afterShellExecution: "AfterShellExecution"
|
|
6187
6191
|
};
|
|
6188
6192
|
/**
|
|
6189
6193
|
* Map Goose PascalCase event names to canonical camelCase.
|
|
@@ -6197,6 +6201,8 @@ const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
|
|
|
6197
6201
|
sessionEnd: "session.end",
|
|
6198
6202
|
beforeSubmitPrompt: "user.prompt",
|
|
6199
6203
|
permissionRequest: "permission.request",
|
|
6204
|
+
preToolUse: "tool.use",
|
|
6205
|
+
postToolUse: "tool.result",
|
|
6200
6206
|
postToolUseFailure: "tool.error",
|
|
6201
6207
|
stop: "task.complete",
|
|
6202
6208
|
preCompact: "context.compact",
|
|
@@ -8191,7 +8197,7 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
8191
8197
|
*
|
|
8192
8198
|
* The JSON shape matches Claude Code's: each PascalCase event maps to an array of
|
|
8193
8199
|
* `{ matcher, hooks: [{ type: "command", command }] }` entries.
|
|
8194
|
-
* @see https://
|
|
8200
|
+
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
|
|
8195
8201
|
*/
|
|
8196
8202
|
var GooseHooks = class GooseHooks extends ToolHooks {
|
|
8197
8203
|
constructor(params) {
|
|
@@ -8242,7 +8248,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
8242
8248
|
throw new Error(`Failed to parse Goose hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
8243
8249
|
}
|
|
8244
8250
|
const hooks = toolHooksToCanonical({
|
|
8245
|
-
hooks: parsed.hooks,
|
|
8251
|
+
hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
|
|
8246
8252
|
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
8247
8253
|
});
|
|
8248
8254
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
@@ -8297,6 +8303,24 @@ function mergeHermesConfig(fileContent, patch) {
|
|
|
8297
8303
|
...patch
|
|
8298
8304
|
});
|
|
8299
8305
|
}
|
|
8306
|
+
/**
|
|
8307
|
+
* Recursively merge `patch` into `base` (patch wins). Nested plain objects are
|
|
8308
|
+
* merged key-by-key; every other value (arrays, scalars) is replaced wholesale.
|
|
8309
|
+
* Used to overlay the Hermes-scoped permission override onto the natively
|
|
8310
|
+
* emitted `approvals`/`security` structures without one clobbering the other
|
|
8311
|
+
* (e.g. `approvals.deny` from canonical deny rules coexisting with an
|
|
8312
|
+
* `approvals.mode` from the override). Prototype-pollution keys are dropped.
|
|
8313
|
+
*/
|
|
8314
|
+
function deepMergeHermesConfig(base, patch) {
|
|
8315
|
+
const result = { ...base };
|
|
8316
|
+
for (const [key, patchValue] of Object.entries(patch)) {
|
|
8317
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
8318
|
+
const baseValue = result[key];
|
|
8319
|
+
if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = deepMergeHermesConfig(baseValue, patchValue);
|
|
8320
|
+
else result[key] = sanitizeHermesConfigValue(patchValue);
|
|
8321
|
+
}
|
|
8322
|
+
return result;
|
|
8323
|
+
}
|
|
8300
8324
|
//#endregion
|
|
8301
8325
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8302
8326
|
/**
|
|
@@ -10462,6 +10486,66 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
|
|
|
10462
10486
|
}
|
|
10463
10487
|
};
|
|
10464
10488
|
//#endregion
|
|
10489
|
+
//#region src/features/claudecode-settings-gateway.ts
|
|
10490
|
+
/**
|
|
10491
|
+
* Single owner of the `.claude/settings.json` `permissions` block, which both
|
|
10492
|
+
* `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
|
|
10493
|
+
* (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
|
|
10494
|
+
* the merge, and the cross-feature ownership rule (permissions' explicit `Read`
|
|
10495
|
+
* rules win over ignore-derived `Read` denies) used to be duplicated across both
|
|
10496
|
+
* feature files; they live here once so each feature just states its intent and
|
|
10497
|
+
* never reasons about the other's existence.
|
|
10498
|
+
*/
|
|
10499
|
+
const READ_TOOL_NAME = "Read";
|
|
10500
|
+
const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
|
|
10501
|
+
const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
|
|
10502
|
+
const parsePermissionsBlock = (settings) => {
|
|
10503
|
+
const permissions = settings.permissions ?? {};
|
|
10504
|
+
return {
|
|
10505
|
+
allow: permissions.allow ?? [],
|
|
10506
|
+
ask: permissions.ask ?? [],
|
|
10507
|
+
deny: permissions.deny ?? []
|
|
10508
|
+
};
|
|
10509
|
+
};
|
|
10510
|
+
const withPermissions = (settings, next) => {
|
|
10511
|
+
const permissions = { ...settings.permissions };
|
|
10512
|
+
const assign = (key, values) => {
|
|
10513
|
+
if (values.length > 0) permissions[key] = values;
|
|
10514
|
+
else delete permissions[key];
|
|
10515
|
+
};
|
|
10516
|
+
assign("allow", next.allow);
|
|
10517
|
+
assign("ask", next.ask);
|
|
10518
|
+
assign("deny", next.deny);
|
|
10519
|
+
return {
|
|
10520
|
+
...settings,
|
|
10521
|
+
permissions
|
|
10522
|
+
};
|
|
10523
|
+
};
|
|
10524
|
+
const applyIgnoreReadDenies = (params) => {
|
|
10525
|
+
const { settings, readDenies } = params;
|
|
10526
|
+
const current = parsePermissionsBlock(settings);
|
|
10527
|
+
const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
|
|
10528
|
+
return withPermissions(settings, {
|
|
10529
|
+
allow: current.allow,
|
|
10530
|
+
ask: current.ask,
|
|
10531
|
+
deny: uniq([...preservedDeny, ...readDenies].toSorted())
|
|
10532
|
+
});
|
|
10533
|
+
};
|
|
10534
|
+
const applyPermissions = (params) => {
|
|
10535
|
+
const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
|
|
10536
|
+
const current = parsePermissionsBlock(settings);
|
|
10537
|
+
const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
|
|
10538
|
+
if (logger && managedToolNames.has(READ_TOOL_NAME)) {
|
|
10539
|
+
const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
|
|
10540
|
+
if (overwrittenReadDenies.length > 0) logger.warn(`Permissions feature manages '${READ_TOOL_NAME}' tool and will overwrite ${overwrittenReadDenies.length} existing ${READ_TOOL_NAME} deny entries. Permissions take precedence.`);
|
|
10541
|
+
}
|
|
10542
|
+
return withPermissions(settings, {
|
|
10543
|
+
allow: uniq([...keepUnmanaged(current.allow), ...allow].toSorted()),
|
|
10544
|
+
ask: uniq([...keepUnmanaged(current.ask), ...ask].toSorted()),
|
|
10545
|
+
deny: uniq([...keepUnmanaged(current.deny), ...deny].toSorted())
|
|
10546
|
+
});
|
|
10547
|
+
};
|
|
10548
|
+
//#endregion
|
|
10465
10549
|
//#region src/features/ignore/claudecode-ignore.ts
|
|
10466
10550
|
const DEFAULT_FILE_MODE = "shared";
|
|
10467
10551
|
/**
|
|
@@ -10508,7 +10592,7 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
10508
10592
|
}
|
|
10509
10593
|
toRulesyncIgnore() {
|
|
10510
10594
|
const fileContent = this.patterns.map((pattern) => {
|
|
10511
|
-
if (
|
|
10595
|
+
if (isReadDenyEntry(pattern)) return pattern.slice(5, -1);
|
|
10512
10596
|
return pattern;
|
|
10513
10597
|
}).filter((pattern) => pattern.length > 0).join("\n");
|
|
10514
10598
|
return new RulesyncIgnore({
|
|
@@ -10519,22 +10603,20 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
10519
10603
|
});
|
|
10520
10604
|
}
|
|
10521
10605
|
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, options }) {
|
|
10522
|
-
const deniedValues = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) =>
|
|
10606
|
+
const deniedValues = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
|
|
10523
10607
|
const paths = this.getSettablePaths({ options });
|
|
10524
10608
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
10525
10609
|
const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
|
|
10526
|
-
|
|
10527
|
-
|
|
10528
|
-
|
|
10529
|
-
|
|
10610
|
+
let existingJsonValue;
|
|
10611
|
+
try {
|
|
10612
|
+
existingJsonValue = JSON.parse(existingFileContent);
|
|
10613
|
+
} catch (error) {
|
|
10614
|
+
throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
10615
|
+
}
|
|
10616
|
+
const jsonValue = applyIgnoreReadDenies({
|
|
10617
|
+
settings: existingJsonValue,
|
|
10618
|
+
readDenies: deniedValues
|
|
10530
10619
|
});
|
|
10531
|
-
const jsonValue = {
|
|
10532
|
-
...existingJsonValue,
|
|
10533
|
-
permissions: {
|
|
10534
|
-
...existingJsonValue.permissions,
|
|
10535
|
-
deny: uniq([...preservedDenies, ...deniedValues].toSorted())
|
|
10536
|
-
}
|
|
10537
|
-
};
|
|
10538
10620
|
return new ClaudecodeIgnore({
|
|
10539
10621
|
outputRoot,
|
|
10540
10622
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -12221,6 +12303,38 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
|
|
|
12221
12303
|
envVars: "env_vars"
|
|
12222
12304
|
};
|
|
12223
12305
|
const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
12306
|
+
/**
|
|
12307
|
+
* Translate a server's `oauth` table from the canonical rulesync shape (Claude
|
|
12308
|
+
* Code style camelCase) into the shape Codex CLI understands. Codex expects the
|
|
12309
|
+
* OAuth client id under snake_case `client_id`; without it `codex mcp login`
|
|
12310
|
+
* falls back to dynamic client registration and fails for providers that do not
|
|
12311
|
+
* support it (e.g. Slack, see #2158). The canonical `clientId` is kept alongside
|
|
12312
|
+
* the added `client_id` so tools that read the camelCase shape keep working and
|
|
12313
|
+
* the round-trip stays stable.
|
|
12314
|
+
*/
|
|
12315
|
+
function mapOauthToCodex(oauth) {
|
|
12316
|
+
const result = omitPrototypePollutionKeys(oauth);
|
|
12317
|
+
if (typeof oauth["clientId"] === "string" && !("client_id" in result)) result["client_id"] = oauth["clientId"];
|
|
12318
|
+
return result;
|
|
12319
|
+
}
|
|
12320
|
+
/**
|
|
12321
|
+
* Reverse of {@link mapOauthToCodex}: collapse Codex's `oauth.client_id` back to
|
|
12322
|
+
* the canonical `clientId` on import. When both keys are present (the shape
|
|
12323
|
+
* rulesync itself emits) the canonical `clientId` wins and `client_id` is
|
|
12324
|
+
* dropped so a subsequent generate does not accumulate duplicates.
|
|
12325
|
+
*/
|
|
12326
|
+
function mapOauthFromCodex(oauth) {
|
|
12327
|
+
const result = {};
|
|
12328
|
+
for (const [key, value] of Object.entries(oauth)) {
|
|
12329
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12330
|
+
if (key === "client_id") {
|
|
12331
|
+
if (!("clientId" in oauth)) result["clientId"] = value;
|
|
12332
|
+
continue;
|
|
12333
|
+
}
|
|
12334
|
+
result[key] = value;
|
|
12335
|
+
}
|
|
12336
|
+
return result;
|
|
12337
|
+
}
|
|
12224
12338
|
function convertFromCodexFormat(codexMcp) {
|
|
12225
12339
|
const result = {};
|
|
12226
12340
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
@@ -12230,7 +12344,8 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
12230
12344
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12231
12345
|
if (key === "enabled") {
|
|
12232
12346
|
if (value === false) converted["disabled"] = true;
|
|
12233
|
-
} else if (
|
|
12347
|
+
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
|
|
12348
|
+
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
12234
12349
|
const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
|
|
12235
12350
|
if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
|
|
12236
12351
|
else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
|
|
@@ -12250,7 +12365,8 @@ function convertToCodexFormat(mcpServers) {
|
|
|
12250
12365
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12251
12366
|
if (key === "disabled") {
|
|
12252
12367
|
if (value === true) converted["enabled"] = false;
|
|
12253
|
-
} else if (
|
|
12368
|
+
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
|
|
12369
|
+
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
12254
12370
|
const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
|
|
12255
12371
|
if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
|
|
12256
12372
|
else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
|
|
@@ -13466,10 +13582,12 @@ function resolveHermesTimeout(config) {
|
|
|
13466
13582
|
*
|
|
13467
13583
|
* Hermes is close to the MCP spec but not identical: `command` must be a single
|
|
13468
13584
|
* executable string (an array's tail folds into `args`), a server is disabled
|
|
13469
|
-
* via `enabled: false` (not the canonical `disabled: true`),
|
|
13470
|
-
*
|
|
13471
|
-
* `
|
|
13472
|
-
*
|
|
13585
|
+
* via `enabled: false` (not the canonical `disabled: true`), remote servers use
|
|
13586
|
+
* `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
|
|
13587
|
+
* exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
|
|
13588
|
+
* fields Hermes understands are emitted, so the shared `config.yaml` is not
|
|
13589
|
+
* polluted with canonical-only aliases (`type`, `transport`, `httpUrl`,
|
|
13590
|
+
* `networkTimeout`, ...).
|
|
13473
13591
|
*/
|
|
13474
13592
|
function convertServerToHermes(config) {
|
|
13475
13593
|
const out = {};
|
|
@@ -13493,6 +13611,10 @@ function convertServerToHermes(config) {
|
|
|
13493
13611
|
if (config.disabled === true) out.enabled = false;
|
|
13494
13612
|
const timeout = resolveHermesTimeout(config);
|
|
13495
13613
|
if (timeout !== void 0) out.timeout = timeout;
|
|
13614
|
+
const tools = {};
|
|
13615
|
+
if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
|
|
13616
|
+
if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
|
|
13617
|
+
if (Object.keys(tools).length > 0) out.tools = tools;
|
|
13496
13618
|
return out;
|
|
13497
13619
|
}
|
|
13498
13620
|
/**
|
|
@@ -13534,6 +13656,10 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
13534
13656
|
if (isPlainObject(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
13535
13657
|
if (config.enabled === false) server.disabled = true;
|
|
13536
13658
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
13659
|
+
if (isRecord(config.tools)) {
|
|
13660
|
+
if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
|
|
13661
|
+
if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
|
|
13662
|
+
}
|
|
13537
13663
|
result[name] = server;
|
|
13538
13664
|
}
|
|
13539
13665
|
return result;
|
|
@@ -15689,17 +15815,232 @@ const PermissionActionSchema = z.enum([
|
|
|
15689
15815
|
*/
|
|
15690
15816
|
const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
|
|
15691
15817
|
/**
|
|
15818
|
+
* OpenCode-specific permission value. Unlike the shared canonical block, which
|
|
15819
|
+
* only accepts a pattern-to-action map, OpenCode also allows a bare action
|
|
15820
|
+
* string that applies to the whole category (e.g. `"external_directory": "deny"`).
|
|
15821
|
+
* This mirrors OpenCode's own permission schema in `opencode-permissions.ts`.
|
|
15822
|
+
*/
|
|
15823
|
+
const OpencodeOverridePermissionValueSchema = z.union([PermissionActionSchema, PermissionRulesSchema]);
|
|
15824
|
+
/**
|
|
15825
|
+
* Tool-scoped override block for OpenCode. Permission categories placed here
|
|
15826
|
+
* (e.g. OpenCode-only categories such as `external_directory`) are emitted only
|
|
15827
|
+
* into OpenCode's config and never leak into other tools' permission files. It
|
|
15828
|
+
* also lets a shared category be overridden with an OpenCode-specific value.
|
|
15829
|
+
* Kept `looseObject` so future OpenCode categories are accepted.
|
|
15830
|
+
*
|
|
15831
|
+
* @example
|
|
15832
|
+
* { "permission": { "external_directory": "deny", "webfetch": "allow" } }
|
|
15833
|
+
*/
|
|
15834
|
+
const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
|
|
15835
|
+
/**
|
|
15836
|
+
* Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
|
|
15837
|
+
* into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
|
|
15838
|
+
* It carries Hermes-specific approval/security controls that have no canonical
|
|
15839
|
+
* permission category — e.g. `approvals` (`mode`, `cron_mode`, ...),
|
|
15840
|
+
* `security` (`allow_private_urls`, ...), `skills.write_approval`,
|
|
15841
|
+
* `memory.write_approval`. Kept `looseObject` (a verbatim passthrough) so any
|
|
15842
|
+
* current or future Hermes config key can be authored without modeling each one.
|
|
15843
|
+
*
|
|
15844
|
+
* @example
|
|
15845
|
+
* { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
|
|
15846
|
+
*/
|
|
15847
|
+
const HermesPermissionsOverrideSchema = z.looseObject({});
|
|
15848
|
+
/**
|
|
15849
|
+
* Tool-scoped override block for Cline. Cline's `command-permissions.json`
|
|
15850
|
+
* carries a single global `allowRedirects` boolean (gates shell redirection
|
|
15851
|
+
* operators `>`/`>>`/`<`) that has no per-command dimension and therefore no
|
|
15852
|
+
* canonical permission category. Placing it here lets users author it
|
|
15853
|
+
* declaratively; it is emitted only into Cline's config. Kept `looseObject` so
|
|
15854
|
+
* future Cline-only knobs can be added.
|
|
15855
|
+
*
|
|
15856
|
+
* @example
|
|
15857
|
+
* { "allowRedirects": true }
|
|
15858
|
+
*/
|
|
15859
|
+
const ClinePermissionsOverrideSchema = z.looseObject({ allowRedirects: z.optional(z.boolean()) });
|
|
15860
|
+
/**
|
|
15861
|
+
* Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
|
|
15862
|
+
* object is a free-form record with tool-specific keys that have no canonical
|
|
15863
|
+
* category — OpenCode-inherited ones (`external_directory`, `doom_loop`, `lsp`,
|
|
15864
|
+
* `question`, `todowrite`, `skill`, `task`, `list`) and Kilo-unique ones
|
|
15865
|
+
* (`agent_manager`, `notebook_read`, `notebook_edit`, `notebook_execute`,
|
|
15866
|
+
* `repo_clone`, `repo_overview`). Placing them here makes them authorable and
|
|
15867
|
+
* portable and keeps them out of other tools' configs. Mirrors the OpenCode
|
|
15868
|
+
* override; each value may be a bare action string or a pattern map.
|
|
15869
|
+
*
|
|
15870
|
+
* @example
|
|
15871
|
+
* { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
|
|
15872
|
+
*/
|
|
15873
|
+
const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
|
|
15874
|
+
/**
|
|
15875
|
+
* Tool-scoped override block for Claude Code. Claude Code's `permissions` object
|
|
15876
|
+
* (in `.claude/settings.json`) carries non-list fields that have no canonical
|
|
15877
|
+
* permission category — `defaultMode` (the session-start permission mode) and
|
|
15878
|
+
* `additionalDirectories` (extra working directories) being the primary ones.
|
|
15879
|
+
* Fields placed under `claudecode.permissions` are merged into the settings
|
|
15880
|
+
* `permissions` object and emitted only for Claude Code, while the shared
|
|
15881
|
+
* `permission` block continues to drive the `allow`/`ask`/`deny` arrays. Kept a
|
|
15882
|
+
* `looseObject` passthrough so any current or future `permissions` field can be
|
|
15883
|
+
* authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
|
|
15884
|
+
* ignored here (rulesync owns them).
|
|
15885
|
+
*
|
|
15886
|
+
* @example
|
|
15887
|
+
* { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
|
|
15888
|
+
*/
|
|
15889
|
+
const ClaudecodePermissionsOverrideSchema = z.looseObject({ permissions: z.optional(z.looseObject({})) });
|
|
15890
|
+
/**
|
|
15891
|
+
* Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
|
|
15892
|
+
* carries a `sensitive_patterns` list — patterns that escalate to ASK even when
|
|
15893
|
+
* the tool's base permission is ALWAYS (allow). The canonical model can only set
|
|
15894
|
+
* a pattern to a single `allow`/`ask`/`deny`, so an "allow by default but ask on
|
|
15895
|
+
* these patterns" escalation cannot be expressed. Entries under
|
|
15896
|
+
* `vibe.permission.<category>.sensitive_patterns` carry that list per canonical
|
|
15897
|
+
* category; the shared `permission` block still drives the base permission and
|
|
15898
|
+
* allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
|
|
15899
|
+
*
|
|
15900
|
+
* @example
|
|
15901
|
+
* { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
|
|
15902
|
+
*/
|
|
15903
|
+
const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))) });
|
|
15904
|
+
/**
|
|
15905
|
+
* Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
|
|
15906
|
+
* autonomy settings with no canonical permission category — `approvalMode`
|
|
15907
|
+
* (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
|
|
15908
|
+
* (`mode`/`networkAccess`). Fields placed here are merged into the top-level of
|
|
15909
|
+
* `.cursor/cli.json` (project) / `~/.cursor/cli-config.json` (global) and emitted
|
|
15910
|
+
* only for Cursor, while the shared `permission` block continues to drive the
|
|
15911
|
+
* `permissions.allow`/`permissions.deny` arrays. Kept a `looseObject` so extra
|
|
15912
|
+
* `cli.json` keys can be authored (they are merged verbatim on generate);
|
|
15913
|
+
* `sandbox`'s accepted values are not documented so it passes through verbatim.
|
|
15914
|
+
* Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
|
|
15915
|
+
* authored here reach `cli.json` on generate but are not re-extracted.
|
|
15916
|
+
*
|
|
15917
|
+
* @example
|
|
15918
|
+
* { "approvalMode": "auto-review" }
|
|
15919
|
+
*/
|
|
15920
|
+
const CursorPermissionsOverrideSchema = z.looseObject({
|
|
15921
|
+
approvalMode: z.optional(z.string()),
|
|
15922
|
+
sandbox: z.optional(z.looseObject({}))
|
|
15923
|
+
});
|
|
15924
|
+
/**
|
|
15925
|
+
* Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
|
|
15926
|
+
* autonomy/sandbox controls with no canonical permission category — under
|
|
15927
|
+
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
15928
|
+
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
|
|
15929
|
+
* placed here are merged into the matching `settings.json` group and emitted
|
|
15930
|
+
* only for Qwen, while the shared `permission` block continues to drive the
|
|
15931
|
+
* `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
|
|
15932
|
+
* passthrough) so any current or future `tools`/`security` key can be authored.
|
|
15933
|
+
*
|
|
15934
|
+
* @example
|
|
15935
|
+
* { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
|
|
15936
|
+
*/
|
|
15937
|
+
const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
15938
|
+
tools: z.optional(z.looseObject({})),
|
|
15939
|
+
security: z.optional(z.looseObject({}))
|
|
15940
|
+
});
|
|
15941
|
+
/**
|
|
15942
|
+
* Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
|
|
15943
|
+
* to per-tool allow/ask/deny with no canonical category — the `[sandbox]`
|
|
15944
|
+
* enforcement table (`workspace_root`, `allow_write`, `forbid_read`, `bash`,
|
|
15945
|
+
* `network`) and plan-mode read-only trust lists under `[agent]`
|
|
15946
|
+
* (`plan_mode_allowed_tools`, `plan_mode_read_only_commands`). Fields placed here
|
|
15947
|
+
* are merged into the matching `reasonix.toml` table and emitted only for
|
|
15948
|
+
* Reasonix, while the shared `permission` block continues to drive
|
|
15949
|
+
* `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
|
|
15950
|
+
* Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
|
|
15951
|
+
* re-extracted from `[agent]` on import — other `agent` keys authored here reach
|
|
15952
|
+
* `reasonix.toml` on generate but are not re-extracted back into the override.
|
|
15953
|
+
*
|
|
15954
|
+
* @example
|
|
15955
|
+
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
15956
|
+
*/
|
|
15957
|
+
const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
15958
|
+
sandbox: z.optional(z.looseObject({})),
|
|
15959
|
+
agent: z.optional(z.looseObject({}))
|
|
15960
|
+
});
|
|
15961
|
+
/**
|
|
15962
|
+
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
15963
|
+
* exposes security controls with no canonical per-command allow/ask/deny slot —
|
|
15964
|
+
* `commandBlocklist` (a hard-block tier that can never be approved, distinct from
|
|
15965
|
+
* an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
|
|
15966
|
+
* (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
|
|
15967
|
+
* and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
|
|
15968
|
+
* `interactionMode`). Fields placed here are merged into `settings.json` and
|
|
15969
|
+
* emitted only for Factory Droid, while the shared `permission` block continues
|
|
15970
|
+
* to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
|
|
15971
|
+
*
|
|
15972
|
+
* @example
|
|
15973
|
+
* { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
|
|
15974
|
+
*/
|
|
15975
|
+
const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist: z.optional(z.array(z.string())) });
|
|
15976
|
+
/**
|
|
15977
|
+
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
15978
|
+
* file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
|
|
15979
|
+
* slot — `agent_mode_coding_permissions`
|
|
15980
|
+
* (`always_ask_before_reading` | `always_allow_reading` | `allow_reading_specific_files`),
|
|
15981
|
+
* `agent_mode_coding_file_read_allowlist` (a path array), and
|
|
15982
|
+
* `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
|
|
15983
|
+
* Fields placed here are merged into `[agents.profiles]` of Warp's global
|
|
15984
|
+
* `settings.toml`, while the shared `permission` block continues to drive the
|
|
15985
|
+
* `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
|
|
15986
|
+
* Warp permissions are global-only.
|
|
15987
|
+
*
|
|
15988
|
+
* @example
|
|
15989
|
+
* { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
|
|
15990
|
+
*/
|
|
15991
|
+
const WarpPermissionsOverrideSchema = z.looseObject({
|
|
15992
|
+
agent_mode_coding_permissions: z.optional(z.string()),
|
|
15993
|
+
agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
|
|
15994
|
+
agent_mode_execute_readonly_commands: z.optional(z.boolean())
|
|
15995
|
+
});
|
|
15996
|
+
/**
|
|
15997
|
+
* Tool-scoped override block for JetBrains Junie. Junie's `allowlist.json` has
|
|
15998
|
+
* two top-level autonomy knobs with no canonical per-glob slot:
|
|
15999
|
+
* `allowReadonlyCommands` (a boolean auto-allowing read-only commands) and
|
|
16000
|
+
* `defaultBehavior` (the fallback action applied when no rule matches — Junie
|
|
16001
|
+
* documents only `allow`/`ask`). Fields placed here are merged onto the
|
|
16002
|
+
* top level of `allowlist.json`, while the shared `permission` block continues
|
|
16003
|
+
* to drive the per-category `rules` groups.
|
|
16004
|
+
*
|
|
16005
|
+
* @example
|
|
16006
|
+
* { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
|
|
16007
|
+
*/
|
|
16008
|
+
const JuniePermissionsOverrideSchema = z.looseObject({
|
|
16009
|
+
allowReadonlyCommands: z.optional(z.boolean()),
|
|
16010
|
+
defaultBehavior: z.optional(z.string())
|
|
16011
|
+
});
|
|
16012
|
+
/**
|
|
15692
16013
|
* Permissions configuration.
|
|
15693
16014
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
15694
16015
|
* Values are pattern-to-action mappings for that tool category.
|
|
15695
16016
|
*
|
|
16017
|
+
* The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
|
|
16018
|
+
* `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie` keys are tool-scoped
|
|
16019
|
+
* overrides consumed only by their respective translator (see the matching
|
|
16020
|
+
* `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
|
|
16021
|
+
* block and ignores them.
|
|
16022
|
+
*
|
|
15696
16023
|
* @example
|
|
15697
16024
|
* {
|
|
15698
16025
|
* "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
|
|
15699
16026
|
* "edit": { "*": "deny", "src/**": "allow" }
|
|
15700
16027
|
* }
|
|
15701
16028
|
*/
|
|
15702
|
-
const PermissionsConfigSchema = z.looseObject({
|
|
16029
|
+
const PermissionsConfigSchema = z.looseObject({
|
|
16030
|
+
permission: z.record(z.string(), PermissionRulesSchema),
|
|
16031
|
+
opencode: z.optional(OpencodePermissionsOverrideSchema),
|
|
16032
|
+
hermes: z.optional(HermesPermissionsOverrideSchema),
|
|
16033
|
+
cline: z.optional(ClinePermissionsOverrideSchema),
|
|
16034
|
+
kilo: z.optional(KiloPermissionsOverrideSchema),
|
|
16035
|
+
claudecode: z.optional(ClaudecodePermissionsOverrideSchema),
|
|
16036
|
+
vibe: z.optional(VibePermissionsOverrideSchema),
|
|
16037
|
+
cursor: z.optional(CursorPermissionsOverrideSchema),
|
|
16038
|
+
qwencode: z.optional(QwencodePermissionsOverrideSchema),
|
|
16039
|
+
reasonix: z.optional(ReasonixPermissionsOverrideSchema),
|
|
16040
|
+
factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
|
|
16041
|
+
warp: z.optional(WarpPermissionsOverrideSchema),
|
|
16042
|
+
junie: z.optional(JuniePermissionsOverrideSchema)
|
|
16043
|
+
});
|
|
15703
16044
|
/**
|
|
15704
16045
|
* Full permissions file schema including optional $schema field.
|
|
15705
16046
|
*/
|
|
@@ -17070,32 +17411,24 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
17070
17411
|
}
|
|
17071
17412
|
const config = rulesyncPermissions.getJson();
|
|
17072
17413
|
const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
|
|
17073
|
-
const
|
|
17074
|
-
|
|
17075
|
-
|
|
17076
|
-
|
|
17077
|
-
|
|
17078
|
-
|
|
17079
|
-
|
|
17080
|
-
const { toolName } = parseClaudePermissionEntry(entry);
|
|
17081
|
-
return toolName === "Read";
|
|
17082
|
-
});
|
|
17083
|
-
if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages 'Read' tool and will overwrite ${droppedReadDenyEntries.length} existing Read deny entries (possibly from ignore feature). Permissions take precedence.`);
|
|
17414
|
+
const overridePermissions = config.claudecode?.permissions;
|
|
17415
|
+
if (overridePermissions && typeof overridePermissions === "object") {
|
|
17416
|
+
const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
|
|
17417
|
+
settings.permissions = {
|
|
17418
|
+
...settings.permissions,
|
|
17419
|
+
...nonListFields
|
|
17420
|
+
};
|
|
17084
17421
|
}
|
|
17085
|
-
const
|
|
17086
|
-
const
|
|
17087
|
-
|
|
17088
|
-
|
|
17089
|
-
|
|
17090
|
-
|
|
17091
|
-
|
|
17092
|
-
|
|
17093
|
-
|
|
17094
|
-
|
|
17095
|
-
const merged = {
|
|
17096
|
-
...settings,
|
|
17097
|
-
permissions: mergedPermissions
|
|
17098
|
-
};
|
|
17422
|
+
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
|
|
17423
|
+
const merged = applyPermissions({
|
|
17424
|
+
settings,
|
|
17425
|
+
managedToolNames,
|
|
17426
|
+
toolNameOf: (entry) => parseClaudePermissionEntry(entry).toolName,
|
|
17427
|
+
allow,
|
|
17428
|
+
ask,
|
|
17429
|
+
deny,
|
|
17430
|
+
logger
|
|
17431
|
+
});
|
|
17099
17432
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
17100
17433
|
return new ClaudecodePermissions({
|
|
17101
17434
|
outputRoot,
|
|
@@ -17118,6 +17451,8 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
17118
17451
|
ask: permissions.ask ?? [],
|
|
17119
17452
|
deny: permissions.deny ?? []
|
|
17120
17453
|
});
|
|
17454
|
+
const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
|
|
17455
|
+
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
17121
17456
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
17122
17457
|
}
|
|
17123
17458
|
validate() {
|
|
@@ -17291,7 +17626,8 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17291
17626
|
} catch (error) {
|
|
17292
17627
|
throw new Error(`Failed to parse existing Cline command-permissions at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17293
17628
|
}
|
|
17294
|
-
const
|
|
17629
|
+
const config = rulesyncPermissions.getJson();
|
|
17630
|
+
const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(config.permission);
|
|
17295
17631
|
warnClineTranslationNotices({
|
|
17296
17632
|
droppedCategories,
|
|
17297
17633
|
translatedAskPatterns,
|
|
@@ -17307,7 +17643,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17307
17643
|
...existing,
|
|
17308
17644
|
allow: dedupedAllow,
|
|
17309
17645
|
deny: mergedDeny,
|
|
17310
|
-
allowRedirects: existing.allowRedirects ?? false
|
|
17646
|
+
allowRedirects: config.cline?.allowRedirects ?? existing.allowRedirects ?? false
|
|
17311
17647
|
};
|
|
17312
17648
|
return new ClinePermissions({
|
|
17313
17649
|
outputRoot,
|
|
@@ -17331,6 +17667,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17331
17667
|
for (const pattern of parsed.allow ?? []) bashRules[pattern] = "allow";
|
|
17332
17668
|
for (const pattern of parsed.deny ?? []) bashRules[pattern] = "deny";
|
|
17333
17669
|
const config = Object.keys(bashRules).length > 0 ? { permission: { bash: bashRules } } : { permission: {} };
|
|
17670
|
+
if (parsed.allowRedirects === true) config.cline = { allowRedirects: true };
|
|
17334
17671
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
17335
17672
|
}
|
|
17336
17673
|
validate() {
|
|
@@ -17767,13 +18104,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
17767
18104
|
WebFetch: "webfetch",
|
|
17768
18105
|
Mcp: "mcp"
|
|
17769
18106
|
};
|
|
17770
|
-
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
18107
|
+
const MCP_CANONICAL_PREFIX$1 = "mcp__";
|
|
17771
18108
|
/**
|
|
17772
18109
|
* Returns true if the canonical category is the per-tool MCP form
|
|
17773
18110
|
* `mcp__<server>__<tool>`.
|
|
17774
18111
|
*/
|
|
17775
18112
|
function isMcpScopedCategory(canonical) {
|
|
17776
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX) && canonical.length > 5;
|
|
18113
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
|
|
17777
18114
|
}
|
|
17778
18115
|
function toCursorType(canonical) {
|
|
17779
18116
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -17804,7 +18141,7 @@ function toCursorPattern(canonical, pattern) {
|
|
|
17804
18141
|
function toCanonicalCategory$1(cursorType, pattern) {
|
|
17805
18142
|
if (cursorType === "Mcp") {
|
|
17806
18143
|
const match = pattern.match(/^([^:()]+):([^:()]+)$/);
|
|
17807
|
-
if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
18144
|
+
if (match) return `${MCP_CANONICAL_PREFIX$1}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
17808
18145
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
17809
18146
|
}
|
|
17810
18147
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
@@ -17938,8 +18275,10 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
17938
18275
|
mergedPermissions.allow = mergedAllow;
|
|
17939
18276
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
17940
18277
|
else delete mergedPermissions.deny;
|
|
18278
|
+
const cursorOverride = config.cursor;
|
|
17941
18279
|
const merged = {
|
|
17942
18280
|
...settings,
|
|
18281
|
+
...cursorOverride,
|
|
17943
18282
|
version: settings.version ?? 1,
|
|
17944
18283
|
editor: {
|
|
17945
18284
|
...existingEditor,
|
|
@@ -17965,11 +18304,15 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
17965
18304
|
}
|
|
17966
18305
|
const permissionsRaw = settings.permissions;
|
|
17967
18306
|
const permissions = permissionsRaw !== null && typeof permissionsRaw === "object" && !Array.isArray(permissionsRaw) ? permissionsRaw : {};
|
|
17968
|
-
const
|
|
18307
|
+
const result = { ...convertCursorToRulesyncPermissions({
|
|
17969
18308
|
allow: asCursorPermissionEntryArray(permissions.allow),
|
|
17970
18309
|
deny: asCursorPermissionEntryArray(permissions.deny)
|
|
17971
|
-
});
|
|
17972
|
-
|
|
18310
|
+
}) };
|
|
18311
|
+
const cursorOverride = {};
|
|
18312
|
+
if (settings.approvalMode !== void 0) cursorOverride.approvalMode = settings.approvalMode;
|
|
18313
|
+
if (settings.sandbox !== null && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)) cursorOverride.sandbox = settings.sandbox;
|
|
18314
|
+
if (Object.keys(cursorOverride).length > 0) result.cursor = cursorOverride;
|
|
18315
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
17973
18316
|
}
|
|
17974
18317
|
validate() {
|
|
17975
18318
|
return {
|
|
@@ -18028,7 +18371,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
18028
18371
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
18029
18372
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
18030
18373
|
if (!permission[canonical]) permission[canonical] = {};
|
|
18031
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
|
|
18374
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
|
|
18032
18375
|
permission[canonical][canonicalPattern] = action;
|
|
18033
18376
|
}
|
|
18034
18377
|
};
|
|
@@ -18277,6 +18620,16 @@ function convertDevinToRulesyncPermissions(params) {
|
|
|
18277
18620
|
}
|
|
18278
18621
|
//#endregion
|
|
18279
18622
|
//#region src/features/permissions/factorydroid-permissions.ts
|
|
18623
|
+
const FACTORYDROID_OVERRIDE_KEYS = [
|
|
18624
|
+
"commandBlocklist",
|
|
18625
|
+
"networkPolicy",
|
|
18626
|
+
"sandbox",
|
|
18627
|
+
"mcpPolicy",
|
|
18628
|
+
"enableDroidShield",
|
|
18629
|
+
"sessionDefaultSettings",
|
|
18630
|
+
"maxAutonomyLevel",
|
|
18631
|
+
"interactionMode"
|
|
18632
|
+
];
|
|
18280
18633
|
/**
|
|
18281
18634
|
* Permissions adapter for Factory Droid.
|
|
18282
18635
|
*
|
|
@@ -18294,18 +18647,14 @@ function convertDevinToRulesyncPermissions(params) {
|
|
|
18294
18647
|
* skipped (with a warning when they carry `deny` rules, to surface the gap).
|
|
18295
18648
|
*
|
|
18296
18649
|
* Factory Droid also has a stronger `commandBlocklist` tier — commands that can
|
|
18297
|
-
* never run, not even under full autonomy
|
|
18298
|
-
*
|
|
18299
|
-
*
|
|
18300
|
-
*
|
|
18301
|
-
*
|
|
18302
|
-
*
|
|
18303
|
-
*
|
|
18304
|
-
*
|
|
18305
|
-
* `commandBlocklist` and re-exporting it to a *fresh* config writes it back as
|
|
18306
|
-
* `commandDenylist`, not `commandBlocklist` — the hard-block tier is not
|
|
18307
|
-
* reconstructed. Re-running over the original file keeps it intact via the
|
|
18308
|
-
* verbatim preservation above.)
|
|
18650
|
+
* never run, not even under full autonomy — plus other security controls
|
|
18651
|
+
* (`networkPolicy`, `sandbox`, `mcpPolicy`, `enableDroidShield`, autonomy
|
|
18652
|
+
* settings) that do not fit the canonical `allow | ask | deny` per-command
|
|
18653
|
+
* model. These are authored and round-tripped through the `factorydroid`
|
|
18654
|
+
* override namespace (see `FactorydroidPermissionsOverrideSchema`): on **import**
|
|
18655
|
+
* they are lifted from `settings.json` into the override, and on **export** they
|
|
18656
|
+
* are merged back in — so `commandBlocklist`'s never-runs guarantee is preserved
|
|
18657
|
+
* faithfully rather than being collapsed onto an approvable `deny`.
|
|
18309
18658
|
*/
|
|
18310
18659
|
var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissions {
|
|
18311
18660
|
constructor(params) {
|
|
@@ -18348,11 +18697,16 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
|
|
|
18348
18697
|
} catch (error) {
|
|
18349
18698
|
throw new Error(`Failed to parse existing Factory Droid settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18350
18699
|
}
|
|
18700
|
+
const config = rulesyncPermissions.getJson();
|
|
18351
18701
|
const { allow, deny } = convertRulesyncToFactorydroidPermissions({
|
|
18352
|
-
config
|
|
18702
|
+
config,
|
|
18353
18703
|
logger
|
|
18354
18704
|
});
|
|
18355
|
-
const
|
|
18705
|
+
const override = config.factorydroid;
|
|
18706
|
+
const merged = {
|
|
18707
|
+
...settings,
|
|
18708
|
+
...override !== void 0 && typeof override === "object" ? override : {}
|
|
18709
|
+
};
|
|
18356
18710
|
const mergedAllow = uniq(allow.toSorted());
|
|
18357
18711
|
const mergedDeny = uniq(deny.toSorted());
|
|
18358
18712
|
if (mergedAllow.length > 0) merged.commandAllowlist = mergedAllow;
|
|
@@ -18376,10 +18730,13 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
|
|
|
18376
18730
|
}
|
|
18377
18731
|
const config = convertFactorydroidToRulesyncPermissions({
|
|
18378
18732
|
allow: Array.isArray(settings.commandAllowlist) ? settings.commandAllowlist : [],
|
|
18379
|
-
deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
|
|
18380
|
-
block: Array.isArray(settings.commandBlocklist) ? settings.commandBlocklist : []
|
|
18733
|
+
deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
|
|
18381
18734
|
});
|
|
18382
|
-
|
|
18735
|
+
const factorydroidOverride = {};
|
|
18736
|
+
for (const key of FACTORYDROID_OVERRIDE_KEYS) if (settings[key] !== void 0) factorydroidOverride[key] = settings[key];
|
|
18737
|
+
const result = { ...config };
|
|
18738
|
+
if (Object.keys(factorydroidOverride).length > 0) result.factorydroid = factorydroidOverride;
|
|
18739
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18383
18740
|
}
|
|
18384
18741
|
validate() {
|
|
18385
18742
|
return {
|
|
@@ -18426,18 +18783,18 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
|
|
|
18426
18783
|
};
|
|
18427
18784
|
}
|
|
18428
18785
|
/**
|
|
18429
|
-
* Convert Factory Droid allow/deny
|
|
18430
|
-
*
|
|
18786
|
+
* Convert Factory Droid allow/deny command lists back to rulesync config under
|
|
18787
|
+
* the `bash` category.
|
|
18431
18788
|
*
|
|
18432
|
-
* `commandBlocklist` (hard
|
|
18433
|
-
*
|
|
18434
|
-
* the
|
|
18789
|
+
* `commandBlocklist` (the hard-block tier) is no longer collapsed here — it has
|
|
18790
|
+
* no canonical equivalent and now round-trips through the `factorydroid`
|
|
18791
|
+
* override so the never-runs guarantee is preserved instead of being weakened to
|
|
18792
|
+
* an approvable `deny`.
|
|
18435
18793
|
*/
|
|
18436
|
-
function convertFactorydroidToRulesyncPermissions({ allow, deny
|
|
18794
|
+
function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
18437
18795
|
const bash = {};
|
|
18438
18796
|
for (const pattern of allow) bash[pattern] = "allow";
|
|
18439
18797
|
for (const pattern of deny) bash[pattern] = "deny";
|
|
18440
|
-
for (const pattern of block) bash[pattern] = "deny";
|
|
18441
18798
|
return { permission: Object.keys(bash).length > 0 ? { bash } : {} };
|
|
18442
18799
|
}
|
|
18443
18800
|
//#endregion
|
|
@@ -18631,30 +18988,114 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
18631
18988
|
const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
|
|
18632
18989
|
const GROKCLI_UI_KEY = "ui";
|
|
18633
18990
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
18991
|
+
const GROKCLI_PERMISSION_KEY = "permission";
|
|
18634
18992
|
const CATCH_ALL_PATTERN$2 = "*";
|
|
18993
|
+
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
18994
|
+
const CATEGORY_TO_GROK_TOOL = {
|
|
18995
|
+
bash: "Bash",
|
|
18996
|
+
read: "Read",
|
|
18997
|
+
edit: "Edit",
|
|
18998
|
+
write: "Edit",
|
|
18999
|
+
grep: "Grep",
|
|
19000
|
+
webfetch: "WebFetch"
|
|
19001
|
+
};
|
|
19002
|
+
const GROK_TOOL_TO_CATEGORY = {
|
|
19003
|
+
Bash: "bash",
|
|
19004
|
+
Read: "read",
|
|
19005
|
+
Edit: "edit",
|
|
19006
|
+
Grep: "grep",
|
|
19007
|
+
WebFetch: "webfetch"
|
|
19008
|
+
};
|
|
19009
|
+
const GROK_MCP_TOOL = "MCPTool";
|
|
19010
|
+
/**
|
|
19011
|
+
* Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
|
|
19012
|
+
* `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
|
|
19013
|
+
* for categories Grok cannot express so callers can skip them.
|
|
19014
|
+
*
|
|
19015
|
+
* Scoped MCP categories (`mcp__<remainder>`) fold their address into the
|
|
19016
|
+
* parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
|
|
19017
|
+
* `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
|
|
19018
|
+
* concrete pattern emits `Tool(pattern)`.
|
|
19019
|
+
*/
|
|
19020
|
+
function buildGrokEntry(category, pattern) {
|
|
19021
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
19022
|
+
const remainder = category.slice(5);
|
|
19023
|
+
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
19024
|
+
}
|
|
19025
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
19026
|
+
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
19027
|
+
if (tool === void 0) return null;
|
|
19028
|
+
return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
19029
|
+
}
|
|
19030
|
+
/**
|
|
19031
|
+
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
19032
|
+
* pattern. Entries without parentheses are treated as catch-all (`*`). Returns
|
|
19033
|
+
* `null` for tool prefixes with no canonical equivalent (e.g. `any`).
|
|
19034
|
+
*/
|
|
19035
|
+
function parseGrokEntry(entry) {
|
|
19036
|
+
const trimmed = entry.trim();
|
|
19037
|
+
const parenIndex = trimmed.indexOf("(");
|
|
19038
|
+
let tool;
|
|
19039
|
+
let inner;
|
|
19040
|
+
if (parenIndex === -1 || !trimmed.endsWith(")")) {
|
|
19041
|
+
tool = trimmed;
|
|
19042
|
+
inner = "";
|
|
19043
|
+
} else {
|
|
19044
|
+
tool = trimmed.slice(0, parenIndex);
|
|
19045
|
+
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
19046
|
+
}
|
|
19047
|
+
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
19048
|
+
category: `${MCP_CANONICAL_PREFIX}${inner}`,
|
|
19049
|
+
pattern: CATCH_ALL_PATTERN$2
|
|
19050
|
+
} : {
|
|
19051
|
+
category: "mcp",
|
|
19052
|
+
pattern: CATCH_ALL_PATTERN$2
|
|
19053
|
+
};
|
|
19054
|
+
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
19055
|
+
if (category === void 0) return null;
|
|
19056
|
+
return {
|
|
19057
|
+
category,
|
|
19058
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
|
|
19059
|
+
};
|
|
19060
|
+
}
|
|
18635
19061
|
/**
|
|
18636
19062
|
* Permissions adapter for the xAI Grok Build CLI (`grokcli`).
|
|
18637
19063
|
*
|
|
18638
|
-
* Grok
|
|
18639
|
-
*
|
|
18640
|
-
*
|
|
18641
|
-
*
|
|
18642
|
-
*
|
|
18643
|
-
*
|
|
18644
|
-
*
|
|
18645
|
-
*
|
|
18646
|
-
*
|
|
18647
|
-
*
|
|
18648
|
-
* `
|
|
18649
|
-
*
|
|
18650
|
-
*
|
|
18651
|
-
*
|
|
18652
|
-
* the
|
|
18653
|
-
*
|
|
18654
|
-
*
|
|
18655
|
-
*
|
|
18656
|
-
*
|
|
18657
|
-
*
|
|
19064
|
+
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
19065
|
+
* `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
|
|
19066
|
+
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`, and
|
|
19067
|
+
* `WebFetch`, evaluated with precedence `deny > ask > allow`
|
|
19068
|
+
* (https://docs.x.ai/build/settings/reference). rulesync's canonical
|
|
19069
|
+
* per-category, per-pattern model maps almost 1:1:
|
|
19070
|
+
* - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
|
|
19071
|
+
* the matching Grok entry and is bucketed into the `[permission]` array for
|
|
19072
|
+
* that action. `bash|read|edit|grep|webfetch` map to their Grok tool;
|
|
19073
|
+
* `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*` maps to
|
|
19074
|
+
* `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
19075
|
+
* parentheses, so a non-`*` argument pattern on it is not represented).
|
|
19076
|
+
* Categories with no Grok tool (`websearch`, `glob`, `notebookedit`,
|
|
19077
|
+
* `agent`) are skipped (with a warning when they carry a `deny` rule, to
|
|
19078
|
+
* surface the gap). When two canonical rules collapse onto the same Grok
|
|
19079
|
+
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
19080
|
+
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
19081
|
+
* entry never lands contradictorily in two arrays.
|
|
19082
|
+
* - Import: the `[permission]` arrays are parsed back into canonical
|
|
19083
|
+
* categories. When no `[permission]` section is present (older configs), the
|
|
19084
|
+
* coarse `[ui] permission_mode` is used as a fallback.
|
|
19085
|
+
*
|
|
19086
|
+
* The coarse `[ui] permission_mode` toggle is still written for backward
|
|
19087
|
+
* compatibility with older Grok versions: `always-approve` when the config is
|
|
19088
|
+
* pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
|
|
19089
|
+
* while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
|
|
19090
|
+
* arrays).
|
|
19091
|
+
*
|
|
19092
|
+
* This surface is **global only** — the adapter syncs the user-level
|
|
19093
|
+
* `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
|
|
19094
|
+
* file; project scope is not modeled here.) The shared config is merged in
|
|
19095
|
+
* place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
|
|
19096
|
+
* tools it models and the `[ui] permission_mode` value, while every other key
|
|
19097
|
+
* (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
|
|
19098
|
+
* entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
|
|
18658
19099
|
* file is never deleted.
|
|
18659
19100
|
*/
|
|
18660
19101
|
var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
@@ -18686,7 +19127,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18686
19127
|
global: true
|
|
18687
19128
|
});
|
|
18688
19129
|
}
|
|
18689
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
19130
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
18690
19131
|
if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
|
|
18691
19132
|
const paths = GrokcliPermissions.getSettablePaths({ global });
|
|
18692
19133
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
@@ -18697,7 +19138,16 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18697
19138
|
} catch (error) {
|
|
18698
19139
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18699
19140
|
}
|
|
18700
|
-
const
|
|
19141
|
+
const config = rulesyncPermissions.getJson();
|
|
19142
|
+
const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
19143
|
+
const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
|
|
19144
|
+
parsed[GROKCLI_PERMISSION_KEY] = {
|
|
19145
|
+
...existingPermission,
|
|
19146
|
+
allow: buckets.allow,
|
|
19147
|
+
deny: buckets.deny,
|
|
19148
|
+
ask: buckets.ask
|
|
19149
|
+
};
|
|
19150
|
+
const mode = deriveGrokPermissionMode(config);
|
|
18701
19151
|
const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
|
|
18702
19152
|
parsed[GROKCLI_UI_KEY] = {
|
|
18703
19153
|
...existingUi,
|
|
@@ -18720,8 +19170,8 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18720
19170
|
} catch (error) {
|
|
18721
19171
|
throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18722
19172
|
}
|
|
18723
|
-
const
|
|
18724
|
-
const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]:
|
|
19173
|
+
const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
19174
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
|
|
18725
19175
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
18726
19176
|
}
|
|
18727
19177
|
validate() {
|
|
@@ -18741,6 +19191,84 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18741
19191
|
});
|
|
18742
19192
|
}
|
|
18743
19193
|
};
|
|
19194
|
+
const ACTION_RANK = {
|
|
19195
|
+
allow: 0,
|
|
19196
|
+
ask: 1,
|
|
19197
|
+
deny: 2
|
|
19198
|
+
};
|
|
19199
|
+
/**
|
|
19200
|
+
* Collect user-authored entries from an existing `[permission]` array whose
|
|
19201
|
+
* tool prefix rulesync cannot model (e.g. `WebSearch`, `any`). Such entries are
|
|
19202
|
+
* preserved verbatim so replacing the arrays does not silently drop them
|
|
19203
|
+
* (mirrors the Cursor adapter's preservation of unmanaged types).
|
|
19204
|
+
*/
|
|
19205
|
+
function unmanagedEntries(existingPermission, key) {
|
|
19206
|
+
return (isStringArray(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
|
|
19207
|
+
}
|
|
19208
|
+
/**
|
|
19209
|
+
* Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
|
|
19210
|
+
* Claude-style entries, resolving collapse collisions via `deny > ask > allow`.
|
|
19211
|
+
* Categories Grok cannot express are skipped (with a warning when they carry a
|
|
19212
|
+
* `deny` rule); entries the user authored for tools rulesync cannot model are
|
|
19213
|
+
* preserved from `existingPermission`.
|
|
19214
|
+
*/
|
|
19215
|
+
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
19216
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
19217
|
+
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
19218
|
+
const entry = buildGrokEntry(category, pattern);
|
|
19219
|
+
if (entry === null) {
|
|
19220
|
+
if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
|
|
19221
|
+
continue;
|
|
19222
|
+
}
|
|
19223
|
+
const existing = ranked.get(entry);
|
|
19224
|
+
if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
|
|
19225
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(entry, action);
|
|
19226
|
+
}
|
|
19227
|
+
const allow = unmanagedEntries(existingPermission, "allow");
|
|
19228
|
+
const deny = unmanagedEntries(existingPermission, "deny");
|
|
19229
|
+
const ask = unmanagedEntries(existingPermission, "ask");
|
|
19230
|
+
for (const [entry, action] of ranked) if (action === "allow") allow.push(entry);
|
|
19231
|
+
else if (action === "deny") deny.push(entry);
|
|
19232
|
+
else ask.push(entry);
|
|
19233
|
+
return {
|
|
19234
|
+
allow: uniq(allow.toSorted()),
|
|
19235
|
+
deny: uniq(deny.toSorted()),
|
|
19236
|
+
ask: uniq(ask.toSorted())
|
|
19237
|
+
};
|
|
19238
|
+
}
|
|
19239
|
+
/**
|
|
19240
|
+
* Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
|
|
19241
|
+
* permission map. Returns `null` when the section defines none of the three
|
|
19242
|
+
* arrays, so the caller can fall back to the coarse `permission_mode`.
|
|
19243
|
+
* Precedence `deny > ask > allow` is applied so a tool listed in multiple
|
|
19244
|
+
* arrays resolves to the strictest action.
|
|
19245
|
+
*/
|
|
19246
|
+
function parseGrokPermissionArrays(permission) {
|
|
19247
|
+
const allow = isStringArray(permission.allow) ? permission.allow : void 0;
|
|
19248
|
+
const deny = isStringArray(permission.deny) ? permission.deny : void 0;
|
|
19249
|
+
const ask = isStringArray(permission.ask) ? permission.ask : void 0;
|
|
19250
|
+
if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
|
|
19251
|
+
const result = {};
|
|
19252
|
+
const apply = (entries, action) => {
|
|
19253
|
+
for (const entry of entries ?? []) {
|
|
19254
|
+
const parsed = parseGrokEntry(entry);
|
|
19255
|
+
if (parsed === null) continue;
|
|
19256
|
+
const bucket = result[parsed.category] ??= {};
|
|
19257
|
+
bucket[parsed.pattern] = action;
|
|
19258
|
+
}
|
|
19259
|
+
};
|
|
19260
|
+
apply(allow, "allow");
|
|
19261
|
+
apply(ask, "ask");
|
|
19262
|
+
apply(deny, "deny");
|
|
19263
|
+
return result;
|
|
19264
|
+
}
|
|
19265
|
+
/**
|
|
19266
|
+
* Legacy coarse fallback: map `[ui] permission_mode` to a canonical action.
|
|
19267
|
+
* `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
|
|
19268
|
+
*/
|
|
19269
|
+
function legacyModeAction(parsed) {
|
|
19270
|
+
return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
19271
|
+
}
|
|
18744
19272
|
/**
|
|
18745
19273
|
* Collapse a rulesync permissions config into Grok's single coarse mode.
|
|
18746
19274
|
* Any `deny`/`ask` rule anywhere keeps prompting (`ask`); otherwise an existing
|
|
@@ -18756,6 +19284,10 @@ function deriveGrokPermissionMode(config) {
|
|
|
18756
19284
|
}
|
|
18757
19285
|
//#endregion
|
|
18758
19286
|
//#region src/features/permissions/hermesagent-permissions.ts
|
|
19287
|
+
/** Collect the glob patterns in a canonical category that carry a given action. */
|
|
19288
|
+
function patternsByAction(category, action) {
|
|
19289
|
+
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
19290
|
+
}
|
|
18759
19291
|
var HermesagentPermissions = class HermesagentPermissions extends ToolPermissions {
|
|
18760
19292
|
static getSettablePaths() {
|
|
18761
19293
|
return {
|
|
@@ -18779,7 +19311,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18779
19311
|
return true;
|
|
18780
19312
|
}
|
|
18781
19313
|
setFileContent(fileContent) {
|
|
18782
|
-
|
|
19314
|
+
const existing = parseHermesConfig(fileContent);
|
|
19315
|
+
const generated = parseHermesConfig(this.fileContent);
|
|
19316
|
+
const merged = deepMergeHermesConfig(existing, generated);
|
|
19317
|
+
if (generated.permissions !== void 0) merged.permissions = generated.permissions;
|
|
19318
|
+
this.fileContent = stringifyHermesConfig(merged);
|
|
18783
19319
|
}
|
|
18784
19320
|
toRulesyncPermissions() {
|
|
18785
19321
|
const config = parseHermesConfig(this.getFileContent());
|
|
@@ -18792,12 +19328,22 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18792
19328
|
}
|
|
18793
19329
|
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions }) {
|
|
18794
19330
|
const permissions = rulesyncPermissions.getJson();
|
|
19331
|
+
const permissionBlock = permissions.permission ?? {};
|
|
19332
|
+
const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
|
|
19333
|
+
const bashDeny = patternsByAction(permissionBlock.bash, "deny");
|
|
19334
|
+
const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
|
|
19335
|
+
let config = {};
|
|
19336
|
+
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
19337
|
+
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
19338
|
+
if (webfetchDeny.length > 0) config.security = { website_blocklist: {
|
|
19339
|
+
enabled: true,
|
|
19340
|
+
domains: webfetchDeny
|
|
19341
|
+
} };
|
|
19342
|
+
if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
|
|
19343
|
+
config.permissions = { rulesync: permissions };
|
|
18795
19344
|
return new HermesagentPermissions({
|
|
18796
19345
|
outputRoot,
|
|
18797
|
-
fileContent: stringifyHermesConfig(
|
|
18798
|
-
command_allowlist: Object.entries(permissions.permission ?? {}).flatMap(([, patterns]) => Object.entries(patterns).filter(([, action]) => action === "allow").map(([command]) => command)),
|
|
18799
|
-
permissions: { rulesync: permissions }
|
|
18800
|
-
})
|
|
19346
|
+
fileContent: stringifyHermesConfig(config)
|
|
18801
19347
|
});
|
|
18802
19348
|
}
|
|
18803
19349
|
};
|
|
@@ -18818,14 +19364,18 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18818
19364
|
* "executables": [ { "prefix": "git ", "action": "allow" } ],
|
|
18819
19365
|
* "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
|
|
18820
19366
|
* "mcpTools": [ { "prefix": "search", "action": "allow" } ],
|
|
18821
|
-
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "
|
|
19367
|
+
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "ask" } ]
|
|
18822
19368
|
* }
|
|
18823
19369
|
* }
|
|
18824
19370
|
* ```
|
|
18825
19371
|
*
|
|
18826
19372
|
* Each rule carries a literal `prefix` (matches commands that start with it) or
|
|
18827
|
-
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action
|
|
18828
|
-
*
|
|
19373
|
+
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`. Junie
|
|
19374
|
+
* documents only `allow` and `ask` as valid actions — there is **no `deny`**
|
|
19375
|
+
* (https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html). rulesync's
|
|
19376
|
+
* canonical `allow`/`ask` map 1:1; a canonical `deny` has no Junie equivalent
|
|
19377
|
+
* and is mapped to the nearest valid action, `ask` (still withholds
|
|
19378
|
+
* auto-approval), with a warning so the downgrade is surfaced.
|
|
18829
19379
|
*
|
|
18830
19380
|
* Category mapping (rulesync canonical <-> Junie rule group):
|
|
18831
19381
|
* - `bash` <-> `executables`
|
|
@@ -18835,8 +19385,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18835
19385
|
*
|
|
18836
19386
|
* Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
|
|
18837
19387
|
* (with a warning when they carry rules). The top-level `defaultBehavior` and
|
|
18838
|
-
* `allowReadonlyCommands` settings have no canonical
|
|
18839
|
-
*
|
|
19388
|
+
* `allowReadonlyCommands` settings have no canonical per-glob slot: they are
|
|
19389
|
+
* authored and round-tripped through the `junie` override namespace (see
|
|
19390
|
+
* `JuniePermissionsOverrideSchema`) — lifted into the override on import and
|
|
19391
|
+
* merged back onto the top level on export — and any other unmodeled top-level
|
|
19392
|
+
* key is preserved verbatim.
|
|
18840
19393
|
*
|
|
18841
19394
|
* @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
|
|
18842
19395
|
*/
|
|
@@ -18859,7 +19412,6 @@ const JUNIE_GROUP_TO_CANONICAL = {
|
|
|
18859
19412
|
mcpTools: "mcp",
|
|
18860
19413
|
readOutsideProject: "read"
|
|
18861
19414
|
};
|
|
18862
|
-
const JUNIE_DEFAULT_BEHAVIOR = "ask";
|
|
18863
19415
|
function isPermissionAction$1(value) {
|
|
18864
19416
|
return PermissionActionSchema.safeParse(value).success;
|
|
18865
19417
|
}
|
|
@@ -18909,13 +19461,16 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
18909
19461
|
} catch (error) {
|
|
18910
19462
|
throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18911
19463
|
}
|
|
19464
|
+
const config = rulesyncPermissions.getJson();
|
|
18912
19465
|
const rules = convertRulesyncToJunieRules({
|
|
18913
|
-
config
|
|
19466
|
+
config,
|
|
18914
19467
|
logger
|
|
18915
19468
|
});
|
|
19469
|
+
const override = config.junie;
|
|
19470
|
+
const overrideObj = override !== void 0 && typeof override === "object" ? override : {};
|
|
18916
19471
|
const merged = {
|
|
18917
19472
|
...existing,
|
|
18918
|
-
|
|
19473
|
+
...overrideObj,
|
|
18919
19474
|
rules
|
|
18920
19475
|
};
|
|
18921
19476
|
return new JuniePermissions({
|
|
@@ -18935,7 +19490,12 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
18935
19490
|
throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18936
19491
|
}
|
|
18937
19492
|
const config = convertJunieToRulesyncPermissions({ allowlist });
|
|
18938
|
-
|
|
19493
|
+
const junieOverride = {};
|
|
19494
|
+
if (typeof allowlist.allowReadonlyCommands === "boolean") junieOverride.allowReadonlyCommands = allowlist.allowReadonlyCommands;
|
|
19495
|
+
if (typeof allowlist.defaultBehavior === "string") junieOverride.defaultBehavior = allowlist.defaultBehavior;
|
|
19496
|
+
const result = { ...config };
|
|
19497
|
+
if (Object.keys(junieOverride).length > 0) result.junie = junieOverride;
|
|
19498
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18939
19499
|
}
|
|
18940
19500
|
validate() {
|
|
18941
19501
|
return {
|
|
@@ -18967,12 +19527,13 @@ function convertRulesyncToJunieRules({ config, logger }) {
|
|
|
18967
19527
|
continue;
|
|
18968
19528
|
}
|
|
18969
19529
|
for (const [pattern, action] of Object.entries(patterns)) {
|
|
19530
|
+
const junieAction = toJunieAction(action, category, pattern, logger);
|
|
18970
19531
|
const rule = isGlobPattern(pattern) ? {
|
|
18971
19532
|
pattern,
|
|
18972
|
-
action
|
|
19533
|
+
action: junieAction
|
|
18973
19534
|
} : {
|
|
18974
19535
|
prefix: pattern,
|
|
18975
|
-
action
|
|
19536
|
+
action: junieAction
|
|
18976
19537
|
};
|
|
18977
19538
|
(rules[group] ??= []).push(rule);
|
|
18978
19539
|
}
|
|
@@ -18980,6 +19541,19 @@ function convertRulesyncToJunieRules({ config, logger }) {
|
|
|
18980
19541
|
return rules;
|
|
18981
19542
|
}
|
|
18982
19543
|
/**
|
|
19544
|
+
* Map a canonical action onto a valid Junie allowlist action. Junie supports
|
|
19545
|
+
* only `allow` and `ask`; a canonical `deny` is downgraded to `ask` (the
|
|
19546
|
+
* nearest valid action — both withhold auto-approval) with a warning, so
|
|
19547
|
+
* rulesync never emits a `deny` that Junie would silently ignore.
|
|
19548
|
+
*/
|
|
19549
|
+
function toJunieAction(action, category, pattern, logger) {
|
|
19550
|
+
if (action === "deny") {
|
|
19551
|
+
logger?.warn(`Junie's allowlist supports only 'allow'/'ask' actions; the '${category}' deny rule for '${pattern}' was downgraded to 'ask' (Junie has no 'deny').`);
|
|
19552
|
+
return "ask";
|
|
19553
|
+
}
|
|
19554
|
+
return action;
|
|
19555
|
+
}
|
|
19556
|
+
/**
|
|
18983
19557
|
* Convert a Junie allowlist back into rulesync permissions config. The
|
|
18984
19558
|
* top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
|
|
18985
19559
|
* canonical equivalent and are not imported.
|
|
@@ -19013,6 +19587,31 @@ const KiloPermissionSchema = z.union([z.enum([
|
|
|
19013
19587
|
]))]);
|
|
19014
19588
|
const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
|
|
19015
19589
|
/**
|
|
19590
|
+
* Kilo permission keys that share a name with a canonical rulesync category and
|
|
19591
|
+
* therefore stay in the shared `permission` block. Everything else (Kilo-only
|
|
19592
|
+
* keys such as `external_directory`, `doom_loop`, `notebook_edit`, ...) is
|
|
19593
|
+
* routed into the `kilo` override on import so it does not leak into other
|
|
19594
|
+
* tools' configs. Kilo folds `write` into `edit`, uses `notebook_edit`/`task`
|
|
19595
|
+
* rather than the canonical `notebookedit`/`agent`, and has no `mcp` key (MCP is
|
|
19596
|
+
* addressed via `mcp__*` tool-name keys), so those canonical names are not Kilo
|
|
19597
|
+
* keys in practice — they simply never appear on the shared side.
|
|
19598
|
+
*/
|
|
19599
|
+
const KILO_SHARED_CATEGORIES = /* @__PURE__ */ new Set([
|
|
19600
|
+
"bash",
|
|
19601
|
+
"read",
|
|
19602
|
+
"edit",
|
|
19603
|
+
"write",
|
|
19604
|
+
"webfetch",
|
|
19605
|
+
"websearch",
|
|
19606
|
+
"grep",
|
|
19607
|
+
"glob",
|
|
19608
|
+
"notebookedit",
|
|
19609
|
+
"agent"
|
|
19610
|
+
]);
|
|
19611
|
+
function isSharedKiloCategory(key) {
|
|
19612
|
+
return key === "*" || KILO_SHARED_CATEGORIES.has(key) || key.startsWith("mcp__");
|
|
19613
|
+
}
|
|
19614
|
+
/**
|
|
19016
19615
|
* Parse a JSONC string and throw on syntax errors. The `jsonc-parser` `parse()` function is
|
|
19017
19616
|
* non-throwing best-effort: invalid input silently yields a partial value (often `undefined`,
|
|
19018
19617
|
* coerced to `{}` by callers). That behavior would silently drop a user's existing `deny` rules
|
|
@@ -19124,12 +19723,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19124
19723
|
const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
|
|
19125
19724
|
const parsedPermission = parsed.permission;
|
|
19126
19725
|
const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
|
|
19127
|
-
const
|
|
19726
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
19727
|
+
const kiloOverride = rulesyncJson.kilo;
|
|
19728
|
+
const incomingPermission = {
|
|
19729
|
+
...rulesyncJson.permission,
|
|
19730
|
+
...kiloOverride?.permission
|
|
19731
|
+
};
|
|
19128
19732
|
const droppedDenyByKey = {};
|
|
19129
|
-
for (const key of Object.
|
|
19130
|
-
const
|
|
19131
|
-
const
|
|
19132
|
-
const nextDenyPatterns = new Set(collectKiloDenyPatterns(rulesyncPermission[key]));
|
|
19733
|
+
for (const [key, value] of Object.entries(incomingPermission)) {
|
|
19734
|
+
const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
|
|
19735
|
+
const nextDenyPatterns = new Set(collectKiloDenyPatterns(value));
|
|
19133
19736
|
const dropped = previousDenyPatterns.filter((p) => !nextDenyPatterns.has(p));
|
|
19134
19737
|
if (dropped.length > 0) droppedDenyByKey[key] = dropped;
|
|
19135
19738
|
}
|
|
@@ -19137,8 +19740,10 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19137
19740
|
const summary = Object.entries(droppedDenyByKey).map(([key, patterns]) => `${key}: [${patterns.join(", ")}]`).join("; ");
|
|
19138
19741
|
logger?.warn(`WARNING: Kilo permissions regeneration drops existing 'deny' rule(s) because rulesync output owns these tool keys. Dropped — ${summary}. To preserve these denies, add them to '.rulesync/permissions.json'.`);
|
|
19139
19742
|
}
|
|
19140
|
-
const mergedPermission = {
|
|
19141
|
-
|
|
19743
|
+
const mergedPermission = {
|
|
19744
|
+
...existingPermission,
|
|
19745
|
+
...incomingPermission
|
|
19746
|
+
};
|
|
19142
19747
|
const nextJson = {
|
|
19143
19748
|
...parsed,
|
|
19144
19749
|
permission: mergedPermission
|
|
@@ -19152,8 +19757,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19152
19757
|
});
|
|
19153
19758
|
}
|
|
19154
19759
|
toRulesyncPermissions() {
|
|
19155
|
-
const
|
|
19156
|
-
|
|
19760
|
+
const rawPermission = this.json.permission ?? {};
|
|
19761
|
+
const shared = {};
|
|
19762
|
+
const overrideOnly = {};
|
|
19763
|
+
for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
|
|
19764
|
+
else overrideOnly[key] = value;
|
|
19765
|
+
const json = Object.keys(overrideOnly).length > 0 ? {
|
|
19766
|
+
permission: shared,
|
|
19767
|
+
kilo: { permission: overrideOnly }
|
|
19768
|
+
} : { permission: shared };
|
|
19769
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
19157
19770
|
}
|
|
19158
19771
|
validate() {
|
|
19159
19772
|
try {
|
|
@@ -19183,10 +19796,6 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19183
19796
|
validate: false
|
|
19184
19797
|
});
|
|
19185
19798
|
}
|
|
19186
|
-
normalizePermission(permission) {
|
|
19187
|
-
if (!permission) return {};
|
|
19188
|
-
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
19189
|
-
}
|
|
19190
19799
|
};
|
|
19191
19800
|
//#endregion
|
|
19192
19801
|
//#region src/features/permissions/kiro-permissions.ts
|
|
@@ -19244,29 +19853,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
|
|
|
19244
19853
|
}
|
|
19245
19854
|
const permission = {};
|
|
19246
19855
|
const toolsSettings = parsed.toolsSettings ?? {};
|
|
19247
|
-
const
|
|
19248
|
-
|
|
19249
|
-
const
|
|
19250
|
-
|
|
19251
|
-
|
|
19252
|
-
|
|
19253
|
-
|
|
19254
|
-
|
|
19255
|
-
|
|
19256
|
-
|
|
19257
|
-
const readDeny = asStringArray(readSettings.deniedPaths);
|
|
19258
|
-
if (readAllow.length > 0 || readDeny.length > 0) {
|
|
19259
|
-
permission.read = {};
|
|
19260
|
-
for (const pattern of readAllow) permission.read[pattern] = "allow";
|
|
19261
|
-
for (const pattern of readDeny) permission.read[pattern] = "deny";
|
|
19262
|
-
}
|
|
19263
|
-
const writeSettings = asRecord$1(toolsSettings.write);
|
|
19264
|
-
const writeAllow = asStringArray(writeSettings.allowedPaths);
|
|
19265
|
-
const writeDeny = asStringArray(writeSettings.deniedPaths);
|
|
19266
|
-
if (writeAllow.length > 0 || writeDeny.length > 0) {
|
|
19267
|
-
permission.write = {};
|
|
19268
|
-
for (const pattern of writeAllow) permission.write[pattern] = "allow";
|
|
19269
|
-
for (const pattern of writeDeny) permission.write[pattern] = "deny";
|
|
19856
|
+
const shellRules = rulesFromArrays(asRecord$1(toolsSettings.shell), "allowedCommands", "deniedCommands");
|
|
19857
|
+
if (Object.keys(shellRules).length > 0) permission.bash = shellRules;
|
|
19858
|
+
for (const category of [
|
|
19859
|
+
"read",
|
|
19860
|
+
"write",
|
|
19861
|
+
"grep",
|
|
19862
|
+
"glob"
|
|
19863
|
+
]) {
|
|
19864
|
+
const rules = rulesFromArrays(asRecord$1(toolsSettings[category]), "allowedPaths", "deniedPaths");
|
|
19865
|
+
if (Object.keys(rules).length > 0) permission[category] = rules;
|
|
19270
19866
|
}
|
|
19271
19867
|
const allowedTools = new Set(parsed.allowedTools ?? []);
|
|
19272
19868
|
if (allowedTools.has("web_fetch")) permission.webfetch = { "*": "allow" };
|
|
@@ -19292,45 +19888,63 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
|
|
|
19292
19888
|
function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
19293
19889
|
const nextAllowedTools = new Set(existing.allowedTools ?? []);
|
|
19294
19890
|
const nextToolsSettings = { ...asRecord$1(existing.toolsSettings) };
|
|
19891
|
+
const pathBuckets = {};
|
|
19892
|
+
const pushPath = (key, action, pattern) => {
|
|
19893
|
+
const bucket = pathBuckets[key] ??= {
|
|
19894
|
+
allow: [],
|
|
19895
|
+
deny: []
|
|
19896
|
+
};
|
|
19897
|
+
(action === "allow" ? bucket.allow : bucket.deny).push(pattern);
|
|
19898
|
+
};
|
|
19295
19899
|
const shell = {
|
|
19296
19900
|
allowedCommands: [],
|
|
19297
19901
|
deniedCommands: []
|
|
19298
19902
|
};
|
|
19299
|
-
const read = {
|
|
19300
|
-
allowedPaths: [],
|
|
19301
|
-
deniedPaths: []
|
|
19302
|
-
};
|
|
19303
|
-
const write = {
|
|
19304
|
-
allowedPaths: [],
|
|
19305
|
-
deniedPaths: []
|
|
19306
|
-
};
|
|
19307
19903
|
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
19308
19904
|
if (action === "ask") {
|
|
19309
19905
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
19310
19906
|
continue;
|
|
19311
19907
|
}
|
|
19312
19908
|
if (category === "bash") (action === "allow" ? shell.allowedCommands : shell.deniedCommands).push(pattern);
|
|
19313
|
-
else if (category === "read"
|
|
19314
|
-
else if (category === "edit" || category === "write") (
|
|
19315
|
-
else if (category === "webfetch" || category === "websearch") {
|
|
19316
|
-
|
|
19317
|
-
|
|
19318
|
-
|
|
19319
|
-
|
|
19320
|
-
|
|
19321
|
-
|
|
19322
|
-
|
|
19323
|
-
} else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
|
|
19909
|
+
else if (category === "read" || category === "grep" || category === "glob") pushPath(category, action, pattern);
|
|
19910
|
+
else if (category === "edit" || category === "write") pushPath("write", action, pattern);
|
|
19911
|
+
else if (category === "webfetch" || category === "websearch") applyKiroWebPermission({
|
|
19912
|
+
category,
|
|
19913
|
+
pattern,
|
|
19914
|
+
action,
|
|
19915
|
+
nextAllowedTools,
|
|
19916
|
+
logger
|
|
19917
|
+
});
|
|
19918
|
+
else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
|
|
19324
19919
|
}
|
|
19325
19920
|
nextToolsSettings.shell = shell;
|
|
19326
|
-
nextToolsSettings.read = read;
|
|
19327
|
-
nextToolsSettings.write = write;
|
|
19921
|
+
nextToolsSettings.read = pathTable(pathBuckets.read);
|
|
19922
|
+
nextToolsSettings.write = pathTable(pathBuckets.write);
|
|
19923
|
+
for (const key of ["grep", "glob"]) {
|
|
19924
|
+
const bucket = pathBuckets[key];
|
|
19925
|
+
if (bucket && (bucket.allow.length > 0 || bucket.deny.length > 0)) nextToolsSettings[key] = pathTable(bucket);
|
|
19926
|
+
}
|
|
19328
19927
|
return {
|
|
19329
19928
|
...existing,
|
|
19330
19929
|
allowedTools: [...nextAllowedTools].toSorted(),
|
|
19331
19930
|
toolsSettings: nextToolsSettings
|
|
19332
19931
|
};
|
|
19333
19932
|
}
|
|
19933
|
+
function pathTable(bucket) {
|
|
19934
|
+
return {
|
|
19935
|
+
allowedPaths: bucket?.allow ?? [],
|
|
19936
|
+
deniedPaths: bucket?.deny ?? []
|
|
19937
|
+
};
|
|
19938
|
+
}
|
|
19939
|
+
function applyKiroWebPermission({ category, pattern, action, nextAllowedTools, logger }) {
|
|
19940
|
+
if (pattern !== "*") {
|
|
19941
|
+
logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
|
|
19942
|
+
return;
|
|
19943
|
+
}
|
|
19944
|
+
const toolName = category === "webfetch" ? "web_fetch" : "web_search";
|
|
19945
|
+
if (action === "allow") nextAllowedTools.add(toolName);
|
|
19946
|
+
else nextAllowedTools.delete(toolName);
|
|
19947
|
+
}
|
|
19334
19948
|
function asRecord$1(value) {
|
|
19335
19949
|
const result = UnknownRecordSchema.safeParse(value);
|
|
19336
19950
|
return result.success ? result.data : {};
|
|
@@ -19338,6 +19952,17 @@ function asRecord$1(value) {
|
|
|
19338
19952
|
function asStringArray(value) {
|
|
19339
19953
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
19340
19954
|
}
|
|
19955
|
+
/**
|
|
19956
|
+
* Build a canonical `{ pattern: action }` map from a Kiro tool settings record's
|
|
19957
|
+
* allow/deny string arrays (e.g. `allowedPaths`/`deniedPaths` or
|
|
19958
|
+
* `allowedCommands`/`deniedCommands`).
|
|
19959
|
+
*/
|
|
19960
|
+
function rulesFromArrays(settings, allowKey, denyKey) {
|
|
19961
|
+
const rules = {};
|
|
19962
|
+
for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
|
|
19963
|
+
for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
|
|
19964
|
+
return rules;
|
|
19965
|
+
}
|
|
19341
19966
|
//#endregion
|
|
19342
19967
|
//#region src/features/permissions/opencode-permissions.ts
|
|
19343
19968
|
const OpencodePermissionSchema = z.union([z.enum([
|
|
@@ -19349,6 +19974,28 @@ const OpencodePermissionSchema = z.union([z.enum([
|
|
|
19349
19974
|
"ask",
|
|
19350
19975
|
"deny"
|
|
19351
19976
|
]))]);
|
|
19977
|
+
/**
|
|
19978
|
+
* Canonical rulesync permission categories that carry a cross-tool meaning (see
|
|
19979
|
+
* the "Supported tool categories" list in `docs/reference/file-formats.md`).
|
|
19980
|
+
* On import, any OpenCode category outside this set — plus MCP tool names — is
|
|
19981
|
+
* treated as OpenCode-only and routed into the `opencode` override block so a
|
|
19982
|
+
* subsequent `rulesync generate` does not leak it into other tools' configs.
|
|
19983
|
+
*/
|
|
19984
|
+
const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
|
|
19985
|
+
"bash",
|
|
19986
|
+
"read",
|
|
19987
|
+
"edit",
|
|
19988
|
+
"write",
|
|
19989
|
+
"webfetch",
|
|
19990
|
+
"websearch",
|
|
19991
|
+
"grep",
|
|
19992
|
+
"glob",
|
|
19993
|
+
"notebookedit",
|
|
19994
|
+
"agent"
|
|
19995
|
+
]);
|
|
19996
|
+
function isSharedPermissionCategory(category) {
|
|
19997
|
+
return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
|
|
19998
|
+
}
|
|
19352
19999
|
const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
|
|
19353
20000
|
"allow",
|
|
19354
20001
|
"ask",
|
|
@@ -19410,9 +20057,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19410
20057
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
19411
20058
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
19412
20059
|
}
|
|
20060
|
+
const parsed = parse(fileContent ?? "{}");
|
|
20061
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
20062
|
+
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
19413
20063
|
const nextJson = {
|
|
19414
|
-
...
|
|
19415
|
-
permission:
|
|
20064
|
+
...parsed,
|
|
20065
|
+
permission: {
|
|
20066
|
+
...rulesyncJson.permission,
|
|
20067
|
+
...overridePermission
|
|
20068
|
+
}
|
|
19416
20069
|
};
|
|
19417
20070
|
return new OpencodePermissions({
|
|
19418
20071
|
outputRoot,
|
|
@@ -19423,8 +20076,20 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19423
20076
|
});
|
|
19424
20077
|
}
|
|
19425
20078
|
toRulesyncPermissions() {
|
|
19426
|
-
const
|
|
19427
|
-
|
|
20079
|
+
const rawPermission = this.json.permission;
|
|
20080
|
+
if (rawPermission === void 0 || typeof rawPermission === "string") {
|
|
20081
|
+
const permission = this.normalizePermission(rawPermission);
|
|
20082
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
20083
|
+
}
|
|
20084
|
+
const shared = {};
|
|
20085
|
+
const overrideOnly = {};
|
|
20086
|
+
for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
|
|
20087
|
+
else overrideOnly[category] = value;
|
|
20088
|
+
const json = Object.keys(overrideOnly).length > 0 ? {
|
|
20089
|
+
permission: shared,
|
|
20090
|
+
opencode: { permission: overrideOnly }
|
|
20091
|
+
} : { permission: shared };
|
|
20092
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
19428
20093
|
}
|
|
19429
20094
|
validate() {
|
|
19430
20095
|
try {
|
|
@@ -19454,10 +20119,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19454
20119
|
validate: false
|
|
19455
20120
|
});
|
|
19456
20121
|
}
|
|
20122
|
+
/**
|
|
20123
|
+
* Normalize the uniform/undefined forms of OpenCode's `permission` field into
|
|
20124
|
+
* the canonical rulesync shape. The object form is handled directly in
|
|
20125
|
+
* `toRulesyncPermissions` (it needs to split shared vs OpenCode-only
|
|
20126
|
+
* categories), so this only covers the two remaining cases.
|
|
20127
|
+
*/
|
|
19457
20128
|
normalizePermission(permission) {
|
|
19458
20129
|
if (!permission) return {};
|
|
19459
|
-
|
|
19460
|
-
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
20130
|
+
return { "*": { "*": permission } };
|
|
19461
20131
|
}
|
|
19462
20132
|
};
|
|
19463
20133
|
//#endregion
|
|
@@ -19531,6 +20201,24 @@ function buildQwenPermissionEntry(toolName, pattern) {
|
|
|
19531
20201
|
if (pattern === "*") return toolName;
|
|
19532
20202
|
return `${toolName}(${pattern})`;
|
|
19533
20203
|
}
|
|
20204
|
+
const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
20205
|
+
"approvalMode",
|
|
20206
|
+
"autoAccept",
|
|
20207
|
+
"sandbox",
|
|
20208
|
+
"sandboxImage",
|
|
20209
|
+
"disabled"
|
|
20210
|
+
];
|
|
20211
|
+
const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
|
|
20212
|
+
function asPlainRecord(value) {
|
|
20213
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20214
|
+
}
|
|
20215
|
+
/** Pick the override-managed keys out of a settings group into a fresh record. */
|
|
20216
|
+
function pickQwenOverrideKeys(group, keys) {
|
|
20217
|
+
const source = asPlainRecord(group);
|
|
20218
|
+
const picked = {};
|
|
20219
|
+
for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
|
|
20220
|
+
return picked;
|
|
20221
|
+
}
|
|
19534
20222
|
var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
19535
20223
|
constructor(params) {
|
|
19536
20224
|
super({
|
|
@@ -19596,6 +20284,15 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
19596
20284
|
...settings,
|
|
19597
20285
|
permissions: mergedPermissions
|
|
19598
20286
|
};
|
|
20287
|
+
const override = config.qwencode;
|
|
20288
|
+
if (override?.tools !== void 0) merged.tools = {
|
|
20289
|
+
...asPlainRecord(settings.tools),
|
|
20290
|
+
...asPlainRecord(override.tools)
|
|
20291
|
+
};
|
|
20292
|
+
if (override?.security !== void 0) merged.security = {
|
|
20293
|
+
...asPlainRecord(settings.security),
|
|
20294
|
+
...asPlainRecord(override.security)
|
|
20295
|
+
};
|
|
19599
20296
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
19600
20297
|
return new QwencodePermissions({
|
|
19601
20298
|
outputRoot,
|
|
@@ -19621,7 +20318,14 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
19621
20318
|
ask: permissions.ask ?? [],
|
|
19622
20319
|
deny: permissions.deny ?? []
|
|
19623
20320
|
});
|
|
19624
|
-
|
|
20321
|
+
const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
|
|
20322
|
+
const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
|
|
20323
|
+
const qwencodeOverride = {};
|
|
20324
|
+
if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
|
|
20325
|
+
if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
|
|
20326
|
+
const result = { ...config };
|
|
20327
|
+
if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
|
|
20328
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
19625
20329
|
}
|
|
19626
20330
|
validate() {
|
|
19627
20331
|
try {
|
|
@@ -19782,6 +20486,16 @@ function toPermissionsTable(value) {
|
|
|
19782
20486
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
19783
20487
|
return { ...value };
|
|
19784
20488
|
}
|
|
20489
|
+
const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_allowed_tools", "plan_mode_read_only_commands"];
|
|
20490
|
+
function asReasonixRecord(value) {
|
|
20491
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20492
|
+
}
|
|
20493
|
+
function pickReasonixKeys(source, keys) {
|
|
20494
|
+
const record = asReasonixRecord(source);
|
|
20495
|
+
const picked = {};
|
|
20496
|
+
for (const key of keys) if (record[key] !== void 0) picked[key] = record[key];
|
|
20497
|
+
return picked;
|
|
20498
|
+
}
|
|
19785
20499
|
var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
19786
20500
|
toml;
|
|
19787
20501
|
constructor(params) {
|
|
@@ -19843,6 +20557,15 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
19843
20557
|
...parsed,
|
|
19844
20558
|
permissions: mergedPermissions
|
|
19845
20559
|
};
|
|
20560
|
+
const override = config.reasonix;
|
|
20561
|
+
if (override?.sandbox !== void 0) merged.sandbox = {
|
|
20562
|
+
...asReasonixRecord(parsed.sandbox),
|
|
20563
|
+
...asReasonixRecord(override.sandbox)
|
|
20564
|
+
};
|
|
20565
|
+
if (override?.agent !== void 0) merged.agent = {
|
|
20566
|
+
...asReasonixRecord(parsed.agent),
|
|
20567
|
+
...asReasonixRecord(override.agent)
|
|
20568
|
+
};
|
|
19846
20569
|
const fileContent = smolToml.stringify(merged);
|
|
19847
20570
|
return new ReasonixPermissions({
|
|
19848
20571
|
outputRoot,
|
|
@@ -19859,7 +20582,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
19859
20582
|
ask: toStringArray$1(permissions.ask),
|
|
19860
20583
|
deny: toStringArray$1(permissions.deny)
|
|
19861
20584
|
});
|
|
19862
|
-
|
|
20585
|
+
const sandbox = asReasonixRecord(this.toml.sandbox);
|
|
20586
|
+
const agentPlanMode = pickReasonixKeys(this.toml.agent, REASONIX_OVERRIDE_AGENT_KEYS);
|
|
20587
|
+
const reasonixOverride = {};
|
|
20588
|
+
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
20589
|
+
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
20590
|
+
const result = { ...config };
|
|
20591
|
+
if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
|
|
20592
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
19863
20593
|
}
|
|
19864
20594
|
validate() {
|
|
19865
20595
|
try {
|
|
@@ -20423,6 +21153,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20423
21153
|
if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
|
|
20424
21154
|
tools[vibeToolName] = nextTool;
|
|
20425
21155
|
}
|
|
21156
|
+
applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
|
|
20426
21157
|
config.tools = tools;
|
|
20427
21158
|
if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
|
|
20428
21159
|
else delete config.enabled_tools;
|
|
@@ -20439,16 +21170,25 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20439
21170
|
}
|
|
20440
21171
|
toRulesyncPermissions() {
|
|
20441
21172
|
const permission = {};
|
|
21173
|
+
const vibeOverridePermission = {};
|
|
20442
21174
|
for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
|
|
20443
21175
|
for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
|
|
20444
21176
|
for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
|
|
20445
|
-
const
|
|
21177
|
+
const category = toCanonicalToolName$1(vibeToolName);
|
|
21178
|
+
const rules = ensurePermission(permission, category);
|
|
20446
21179
|
const action = fromVibePermission(toolConfig.permission);
|
|
20447
21180
|
if (action !== void 0) rules["*"] = action;
|
|
20448
21181
|
for (const pattern of toStringArray(toolConfig.allow ?? toolConfig.allowlist)) rules[pattern] = "allow";
|
|
20449
21182
|
for (const pattern of toStringArray(toolConfig.deny ?? toolConfig.denylist)) rules[pattern] = "deny";
|
|
21183
|
+
const sensitivePatterns = toStringArray(toolConfig.sensitive_patterns);
|
|
21184
|
+
if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
|
|
20450
21185
|
}
|
|
20451
|
-
|
|
21186
|
+
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
21187
|
+
const json = Object.keys(vibeOverridePermission).length > 0 ? {
|
|
21188
|
+
permission,
|
|
21189
|
+
vibe: { permission: vibeOverridePermission }
|
|
21190
|
+
} : { permission };
|
|
21191
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
20452
21192
|
}
|
|
20453
21193
|
validate() {
|
|
20454
21194
|
try {
|
|
@@ -20475,6 +21215,22 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20475
21215
|
});
|
|
20476
21216
|
}
|
|
20477
21217
|
};
|
|
21218
|
+
/**
|
|
21219
|
+
* Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
|
|
21220
|
+
* escalate to ASK even when the base permission is ALWAYS). rulesync owns this
|
|
21221
|
+
* list for any category the override names: a present list is set, an empty
|
|
21222
|
+
* one clears it. Categories not named keep whatever the existing file had.
|
|
21223
|
+
*/
|
|
21224
|
+
function applyVibeSensitivePatterns(tools, vibeOverride) {
|
|
21225
|
+
for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
|
|
21226
|
+
const vibeToolName = toVibeToolName(category);
|
|
21227
|
+
const nextTool = toVibeToolConfig(tools[vibeToolName]);
|
|
21228
|
+
const patterns = toStringArray(toolOverride.sensitive_patterns);
|
|
21229
|
+
if (patterns.length > 0) nextTool.sensitive_patterns = [...patterns].toSorted();
|
|
21230
|
+
else delete nextTool.sensitive_patterns;
|
|
21231
|
+
tools[vibeToolName] = nextTool;
|
|
21232
|
+
}
|
|
21233
|
+
}
|
|
20478
21234
|
function parseVibeConfig(fileContent) {
|
|
20479
21235
|
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
20480
21236
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -20519,6 +21275,11 @@ function ensurePermission(permission, category) {
|
|
|
20519
21275
|
const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
|
|
20520
21276
|
const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
|
|
20521
21277
|
const DENYLIST_KEY = "agent_mode_command_execution_denylist";
|
|
21278
|
+
const WARP_OVERRIDE_KEYS = [
|
|
21279
|
+
"agent_mode_coding_permissions",
|
|
21280
|
+
"agent_mode_coding_file_read_allowlist",
|
|
21281
|
+
"agent_mode_execute_readonly_commands"
|
|
21282
|
+
];
|
|
20522
21283
|
/**
|
|
20523
21284
|
* Warp's `settings.toml` lives in a different directory per platform (Stable
|
|
20524
21285
|
* channel). The home directory is resolved by the processor through
|
|
@@ -20554,8 +21315,16 @@ function warpSettingsDir() {
|
|
|
20554
21315
|
* patterns as regexes when targeting Warp (mirrors the Zed permissions
|
|
20555
21316
|
* adapter). Warp has no per-command "ask" list, so `ask` rules are dropped; and
|
|
20556
21317
|
* the command lists only model shell commands, so non-`bash` categories are
|
|
20557
|
-
* skipped (with a warning when they carry `deny` rules).
|
|
20558
|
-
*
|
|
21318
|
+
* skipped (with a warning when they carry `deny` rules).
|
|
21319
|
+
*
|
|
21320
|
+
* Warp's `[agents.profiles]` table also exposes file-read/read-only autonomy
|
|
21321
|
+
* knobs that do not fit the canonical `allow | ask | deny` per-command model:
|
|
21322
|
+
* `agent_mode_coding_permissions`, `agent_mode_coding_file_read_allowlist`, and
|
|
21323
|
+
* `agent_mode_execute_readonly_commands`. These are authored and round-tripped
|
|
21324
|
+
* through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
|
|
21325
|
+
* on **import** they are lifted from `settings.toml` into the override, and on
|
|
21326
|
+
* **export** they are merged back into `[agents.profiles]` (the override wins).
|
|
21327
|
+
* MCP allow/deny is a separate surface not modeled here.
|
|
20559
21328
|
*
|
|
20560
21329
|
* The `settings.toml` file holds all of Warp's settings, so the
|
|
20561
21330
|
* `[agents.profiles]` block is merged in place and the file is never deleted.
|
|
@@ -20600,12 +21369,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
20600
21369
|
} catch (error) {
|
|
20601
21370
|
throw new Error(`Failed to parse existing Warp settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
20602
21371
|
}
|
|
21372
|
+
const config = rulesyncPermissions.getJson();
|
|
20603
21373
|
const { allow, deny } = convertRulesyncToWarpPermissions({
|
|
20604
|
-
config
|
|
21374
|
+
config,
|
|
20605
21375
|
logger
|
|
20606
21376
|
});
|
|
20607
21377
|
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
20608
21378
|
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
21379
|
+
const override = config.warp;
|
|
21380
|
+
if (isRecord(override)) Object.assign(profiles, override);
|
|
20609
21381
|
const mergedAllow = uniq(allow.toSorted());
|
|
20610
21382
|
const mergedDeny = uniq(deny.toSorted());
|
|
20611
21383
|
if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
|
|
@@ -20636,7 +21408,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
20636
21408
|
allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
20637
21409
|
deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
20638
21410
|
});
|
|
20639
|
-
|
|
21411
|
+
const warpOverride = {};
|
|
21412
|
+
for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
|
|
21413
|
+
const result = { ...config };
|
|
21414
|
+
if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
|
|
21415
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
20640
21416
|
}
|
|
20641
21417
|
validate() {
|
|
20642
21418
|
return {
|
|
@@ -24002,8 +24778,9 @@ const DevinSkillFrontmatterSchema = z.looseObject({
|
|
|
24002
24778
|
* Represents a Devin (now Devin Desktop) skill directory.
|
|
24003
24779
|
* Devin supports skills in both project mode under .devin/skills/
|
|
24004
24780
|
* (preferred since the Devin Desktop rebrand; .devin/skills/ is the legacy
|
|
24005
|
-
* fallback the tool still reads) and global mode under
|
|
24006
|
-
* (
|
|
24781
|
+
* fallback the tool still reads) and global mode under the Devin-native
|
|
24782
|
+
* ~/.config/devin/skills/ (consistent with Devin's global agents/rules paths;
|
|
24783
|
+
* the legacy channel-dependent ~/.codeium/<channel>/skills/ is no longer emitted).
|
|
24007
24784
|
*/
|
|
24008
24785
|
var DevinSkill = class DevinSkill extends ToolSkill {
|
|
24009
24786
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DevinSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -24025,7 +24802,7 @@ var DevinSkill = class DevinSkill extends ToolSkill {
|
|
|
24025
24802
|
}
|
|
24026
24803
|
}
|
|
24027
24804
|
static getSettablePaths({ global = false } = {}) {
|
|
24028
|
-
if (global) return { relativeDirPath:
|
|
24805
|
+
if (global) return { relativeDirPath: DEVIN_GLOBAL_SKILLS_DIR_PATH };
|
|
24029
24806
|
return { relativeDirPath: DEVIN_SKILLS_DIR_PATH };
|
|
24030
24807
|
}
|
|
24031
24808
|
getFrontmatter() {
|
|
@@ -27084,7 +27861,7 @@ const QwencodeSubagentFrontmatterSchema = z.looseObject({
|
|
|
27084
27861
|
disallowedTools: z.optional(z.array(z.string())),
|
|
27085
27862
|
maxTurns: z.optional(z.number()),
|
|
27086
27863
|
color: z.optional(z.string()),
|
|
27087
|
-
mcpServers: z.optional(z.array(z.string())),
|
|
27864
|
+
mcpServers: z.optional(z.union([z.array(z.string()), z.record(z.string(), z.looseObject({}))])),
|
|
27088
27865
|
hooks: z.optional(z.unknown())
|
|
27089
27866
|
});
|
|
27090
27867
|
var QwencodeSubagent = class QwencodeSubagent extends ToolSubagent {
|
|
@@ -37065,4 +37842,4 @@ async function importPermissionsCore(params) {
|
|
|
37065
37842
|
//#endregion
|
|
37066
37843
|
export { CLIError as $, IgnoreProcessor as A, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as At, toolCommandFactories as B, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Bt, PermissionsProcessorToolTargetSchema as C, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ct, McpProcessorToolTargetSchema as D, RULESYNC_HOOKS_FILE_NAME as Dt, McpProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Et, HooksProcessorToolTargetSchema as F, RULESYNC_PERMISSIONS_FILE_NAME as Ft, CLAUDECODE_SKILLS_DIR_PATH as G, CLAUDECODE_LOCAL_RULE_FILE_NAME as H, formatError as Ht, toolHooksFactories as I, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as It, stringifyFrontmatter as J, RulesyncCommand as K, RulesyncHooks as L, RULESYNC_RELATIVE_DIR_PATH as Lt, toolIgnoreFactories as M, RULESYNC_MCP_RELATIVE_FILE_PATH as Mt, RulesyncIgnore as N, RULESYNC_MCP_SCHEMA_URL as Nt, toolMcpFactories as O, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ot, HooksProcessor as P, RULESYNC_OVERVIEW_FILE_NAME as Pt, JsonLogger as Q, CommandsProcessor as R, RULESYNC_RULES_RELATIVE_DIR_PATH as Rt, PermissionsProcessor as S, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as St, RulesyncPermissions as T, RULESYNC_CONFIG_SCHEMA_URL as Tt, CLAUDECODE_MEMORIES_DIR_NAME as U, ALL_FEATURES as Ut, CLAUDECODE_DIR as V, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Vt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as W, ALL_FEATURES_WITH_WILDCARD as Wt, findControlCharacter as X, ConfigResolver as Y, ConsoleLogger as Z, toolSkillFactories as _, ALL_TOOL_TARGETS as _t, RulesProcessor as a, fileExists as at, RulesyncSkillFrontmatterSchema as b, MAX_FILE_SIZE as bt, RulesyncRule as c, getHomeDirectory as ct, SubagentsProcessorToolTargetSchema as d, readFileContent as dt, ErrorCodes as et, toolSubagentFactories as f, removeDirectory as ft, SkillsProcessorToolTargetSchema as g, writeFileContent as gt, SkillsProcessor as h, toPosixPath as ht, convertFromTool as i, ensureDir as it, IgnoreProcessorToolTargetSchema as j, RULESYNC_MCP_FILE_NAME as jt, RulesyncMcp as k, RULESYNC_IGNORE_RELATIVE_FILE_PATH as kt, RulesyncRuleFrontmatterSchema as l, isSymlink as lt, RulesyncSubagentFrontmatterSchema as m, removeTempDirectory as mt, checkRulesyncDirExists as n, createTempDirectory as nt, RulesProcessorToolTargetSchema as o, findFilesByGlobs as ot, RulesyncSubagent as p, removeFile as pt, RulesyncCommandFrontmatterSchema as q, generate as r, directoryExists as rt, toolRuleFactories as s, getFileSize as st, importFromTool as t, checkPathTraversal as tt, SubagentsProcessor as u, listDirectoryFiles as ut, getLocalSkillDirNames as v, ALL_TOOL_TARGETS_WITH_WILDCARD as vt, toolPermissionsFactories as w, RULESYNC_CONFIG_RELATIVE_FILE_PATH as wt, SKILL_FILE_NAME as x, RULESYNC_AIIGNORE_FILE_NAME as xt, RulesyncSkill as y, ToolTargetSchema as yt, CommandsProcessorToolTargetSchema as z, RULESYNC_SKILLS_RELATIVE_DIR_PATH as zt };
|
|
37067
37844
|
|
|
37068
|
-
//# sourceMappingURL=import-
|
|
37845
|
+
//# sourceMappingURL=import-B7VzSVUK.js.map
|