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
|
@@ -3029,8 +3029,8 @@ const DEVIN_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "skills");
|
|
|
3029
3029
|
const DEVIN_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_DIR, "agents");
|
|
3030
3030
|
const DEVIN_GLOBAL_CONFIG_DIR_PATH = (0, node_path.join)(".config", "devin");
|
|
3031
3031
|
const DEVIN_GLOBAL_AGENTS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
|
|
3032
|
+
const DEVIN_GLOBAL_SKILLS_DIR_PATH = (0, node_path.join)(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
|
|
3032
3033
|
const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "global_workflows");
|
|
3033
|
-
const CODEIUM_WINDSURF_SKILLS_DIR_PATH = (0, node_path.join)(CODEIUM_WINDSURF_DIR, "skills");
|
|
3034
3034
|
const DEVIN_CONFIG_FILE_NAME = "config.json";
|
|
3035
3035
|
const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
|
|
3036
3036
|
const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
|
|
@@ -5715,6 +5715,8 @@ const DEEPAGENTS_HOOK_EVENTS = [
|
|
|
5715
5715
|
"sessionEnd",
|
|
5716
5716
|
"beforeSubmitPrompt",
|
|
5717
5717
|
"permissionRequest",
|
|
5718
|
+
"preToolUse",
|
|
5719
|
+
"postToolUse",
|
|
5718
5720
|
"postToolUseFailure",
|
|
5719
5721
|
"stop",
|
|
5720
5722
|
"preCompact",
|
|
@@ -5740,7 +5742,13 @@ const CODEXCLI_HOOK_EVENTS = [
|
|
|
5740
5742
|
* Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
|
|
5741
5743
|
* maps PascalCase event names to matcher/handler arrays. Every Goose event has a
|
|
5742
5744
|
* 1:1 canonical equivalent, so no new canonical events are required.
|
|
5743
|
-
*
|
|
5745
|
+
*
|
|
5746
|
+
* Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
|
|
5747
|
+
* has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
|
|
5748
|
+
* Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
|
|
5749
|
+
* excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
|
|
5750
|
+
* @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
|
|
5751
|
+
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
|
|
5744
5752
|
*/
|
|
5745
5753
|
const GOOSE_HOOK_EVENTS = [
|
|
5746
5754
|
"sessionStart",
|
|
@@ -5753,9 +5761,7 @@ const GOOSE_HOOK_EVENTS = [
|
|
|
5753
5761
|
"beforeReadFile",
|
|
5754
5762
|
"afterFileEdit",
|
|
5755
5763
|
"beforeShellExecution",
|
|
5756
|
-
"afterShellExecution"
|
|
5757
|
-
"subagentStart",
|
|
5758
|
-
"subagentStop"
|
|
5764
|
+
"afterShellExecution"
|
|
5759
5765
|
];
|
|
5760
5766
|
/** Hook events supported by Kiro CLI. */
|
|
5761
5767
|
const KIRO_HOOK_EVENTS = [
|
|
@@ -6208,9 +6214,7 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
|
|
|
6208
6214
|
beforeReadFile: "BeforeReadFile",
|
|
6209
6215
|
afterFileEdit: "AfterFileEdit",
|
|
6210
6216
|
beforeShellExecution: "BeforeShellExecution",
|
|
6211
|
-
afterShellExecution: "AfterShellExecution"
|
|
6212
|
-
subagentStart: "SubagentStart",
|
|
6213
|
-
subagentStop: "SubagentStop"
|
|
6217
|
+
afterShellExecution: "AfterShellExecution"
|
|
6214
6218
|
};
|
|
6215
6219
|
/**
|
|
6216
6220
|
* Map Goose PascalCase event names to canonical camelCase.
|
|
@@ -6224,6 +6228,8 @@ const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
|
|
|
6224
6228
|
sessionEnd: "session.end",
|
|
6225
6229
|
beforeSubmitPrompt: "user.prompt",
|
|
6226
6230
|
permissionRequest: "permission.request",
|
|
6231
|
+
preToolUse: "tool.use",
|
|
6232
|
+
postToolUse: "tool.result",
|
|
6227
6233
|
postToolUseFailure: "tool.error",
|
|
6228
6234
|
stop: "task.complete",
|
|
6229
6235
|
preCompact: "context.compact",
|
|
@@ -8218,7 +8224,7 @@ const GOOSE_CONVERTER_CONFIG = {
|
|
|
8218
8224
|
*
|
|
8219
8225
|
* The JSON shape matches Claude Code's: each PascalCase event maps to an array of
|
|
8220
8226
|
* `{ matcher, hooks: [{ type: "command", command }] }` entries.
|
|
8221
|
-
* @see https://
|
|
8227
|
+
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
|
|
8222
8228
|
*/
|
|
8223
8229
|
var GooseHooks = class GooseHooks extends ToolHooks {
|
|
8224
8230
|
constructor(params) {
|
|
@@ -8269,7 +8275,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
|
|
|
8269
8275
|
throw new Error(`Failed to parse Goose hooks content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
8270
8276
|
}
|
|
8271
8277
|
const hooks = toolHooksToCanonical({
|
|
8272
|
-
hooks: parsed.hooks,
|
|
8278
|
+
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,
|
|
8273
8279
|
converterConfig: GOOSE_CONVERTER_CONFIG
|
|
8274
8280
|
});
|
|
8275
8281
|
return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
|
|
@@ -8324,6 +8330,24 @@ function mergeHermesConfig(fileContent, patch) {
|
|
|
8324
8330
|
...patch
|
|
8325
8331
|
});
|
|
8326
8332
|
}
|
|
8333
|
+
/**
|
|
8334
|
+
* Recursively merge `patch` into `base` (patch wins). Nested plain objects are
|
|
8335
|
+
* merged key-by-key; every other value (arrays, scalars) is replaced wholesale.
|
|
8336
|
+
* Used to overlay the Hermes-scoped permission override onto the natively
|
|
8337
|
+
* emitted `approvals`/`security` structures without one clobbering the other
|
|
8338
|
+
* (e.g. `approvals.deny` from canonical deny rules coexisting with an
|
|
8339
|
+
* `approvals.mode` from the override). Prototype-pollution keys are dropped.
|
|
8340
|
+
*/
|
|
8341
|
+
function deepMergeHermesConfig(base, patch) {
|
|
8342
|
+
const result = { ...base };
|
|
8343
|
+
for (const [key, patchValue] of Object.entries(patch)) {
|
|
8344
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
8345
|
+
const baseValue = result[key];
|
|
8346
|
+
if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = deepMergeHermesConfig(baseValue, patchValue);
|
|
8347
|
+
else result[key] = sanitizeHermesConfigValue(patchValue);
|
|
8348
|
+
}
|
|
8349
|
+
return result;
|
|
8350
|
+
}
|
|
8327
8351
|
//#endregion
|
|
8328
8352
|
//#region src/features/hooks/hermesagent-hooks.ts
|
|
8329
8353
|
/**
|
|
@@ -10489,6 +10513,66 @@ var AugmentcodeIgnore = class AugmentcodeIgnore extends ToolIgnore {
|
|
|
10489
10513
|
}
|
|
10490
10514
|
};
|
|
10491
10515
|
//#endregion
|
|
10516
|
+
//#region src/features/claudecode-settings-gateway.ts
|
|
10517
|
+
/**
|
|
10518
|
+
* Single owner of the `.claude/settings.json` `permissions` block, which both
|
|
10519
|
+
* `ignore` (writes `Read(...)` into `permissions.deny`) and `permissions`
|
|
10520
|
+
* (writes the whole `allow`/`ask`/`deny`) read-modify-write. The entry format,
|
|
10521
|
+
* the merge, and the cross-feature ownership rule (permissions' explicit `Read`
|
|
10522
|
+
* rules win over ignore-derived `Read` denies) used to be duplicated across both
|
|
10523
|
+
* feature files; they live here once so each feature just states its intent and
|
|
10524
|
+
* never reasons about the other's existence.
|
|
10525
|
+
*/
|
|
10526
|
+
const READ_TOOL_NAME = "Read";
|
|
10527
|
+
const isReadDenyEntry = (entry) => entry.startsWith(`${READ_TOOL_NAME}(`) && entry.endsWith(")");
|
|
10528
|
+
const buildReadDenyEntry = (pattern) => `${READ_TOOL_NAME}(${pattern})`;
|
|
10529
|
+
const parsePermissionsBlock = (settings) => {
|
|
10530
|
+
const permissions = settings.permissions ?? {};
|
|
10531
|
+
return {
|
|
10532
|
+
allow: permissions.allow ?? [],
|
|
10533
|
+
ask: permissions.ask ?? [],
|
|
10534
|
+
deny: permissions.deny ?? []
|
|
10535
|
+
};
|
|
10536
|
+
};
|
|
10537
|
+
const withPermissions = (settings, next) => {
|
|
10538
|
+
const permissions = { ...settings.permissions };
|
|
10539
|
+
const assign = (key, values) => {
|
|
10540
|
+
if (values.length > 0) permissions[key] = values;
|
|
10541
|
+
else delete permissions[key];
|
|
10542
|
+
};
|
|
10543
|
+
assign("allow", next.allow);
|
|
10544
|
+
assign("ask", next.ask);
|
|
10545
|
+
assign("deny", next.deny);
|
|
10546
|
+
return {
|
|
10547
|
+
...settings,
|
|
10548
|
+
permissions
|
|
10549
|
+
};
|
|
10550
|
+
};
|
|
10551
|
+
const applyIgnoreReadDenies = (params) => {
|
|
10552
|
+
const { settings, readDenies } = params;
|
|
10553
|
+
const current = parsePermissionsBlock(settings);
|
|
10554
|
+
const preservedDeny = current.deny.filter((entry) => !isReadDenyEntry(entry) || readDenies.includes(entry));
|
|
10555
|
+
return withPermissions(settings, {
|
|
10556
|
+
allow: current.allow,
|
|
10557
|
+
ask: current.ask,
|
|
10558
|
+
deny: (0, es_toolkit.uniq)([...preservedDeny, ...readDenies].toSorted())
|
|
10559
|
+
});
|
|
10560
|
+
};
|
|
10561
|
+
const applyPermissions = (params) => {
|
|
10562
|
+
const { settings, managedToolNames, toolNameOf, allow, ask, deny, logger } = params;
|
|
10563
|
+
const current = parsePermissionsBlock(settings);
|
|
10564
|
+
const keepUnmanaged = (entries) => entries.filter((entry) => !managedToolNames.has(toolNameOf(entry)));
|
|
10565
|
+
if (logger && managedToolNames.has(READ_TOOL_NAME)) {
|
|
10566
|
+
const overwrittenReadDenies = current.deny.filter((entry) => toolNameOf(entry) === READ_TOOL_NAME);
|
|
10567
|
+
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.`);
|
|
10568
|
+
}
|
|
10569
|
+
return withPermissions(settings, {
|
|
10570
|
+
allow: (0, es_toolkit.uniq)([...keepUnmanaged(current.allow), ...allow].toSorted()),
|
|
10571
|
+
ask: (0, es_toolkit.uniq)([...keepUnmanaged(current.ask), ...ask].toSorted()),
|
|
10572
|
+
deny: (0, es_toolkit.uniq)([...keepUnmanaged(current.deny), ...deny].toSorted())
|
|
10573
|
+
});
|
|
10574
|
+
};
|
|
10575
|
+
//#endregion
|
|
10492
10576
|
//#region src/features/ignore/claudecode-ignore.ts
|
|
10493
10577
|
const DEFAULT_FILE_MODE = "shared";
|
|
10494
10578
|
/**
|
|
@@ -10535,7 +10619,7 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
10535
10619
|
}
|
|
10536
10620
|
toRulesyncIgnore() {
|
|
10537
10621
|
const fileContent = this.patterns.map((pattern) => {
|
|
10538
|
-
if (
|
|
10622
|
+
if (isReadDenyEntry(pattern)) return pattern.slice(5, -1);
|
|
10539
10623
|
return pattern;
|
|
10540
10624
|
}).filter((pattern) => pattern.length > 0).join("\n");
|
|
10541
10625
|
return new RulesyncIgnore({
|
|
@@ -10546,22 +10630,20 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
10546
10630
|
});
|
|
10547
10631
|
}
|
|
10548
10632
|
static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, options }) {
|
|
10549
|
-
const deniedValues = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) =>
|
|
10633
|
+
const deniedValues = rulesyncIgnore.getFileContent().split(/\r?\n|\r/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).map((pattern) => buildReadDenyEntry(pattern));
|
|
10550
10634
|
const paths = this.getSettablePaths({ options });
|
|
10551
10635
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
10552
10636
|
const existingFileContent = await fileExists(filePath) ? await readFileContent(filePath) : "{}";
|
|
10553
|
-
|
|
10554
|
-
|
|
10555
|
-
|
|
10556
|
-
|
|
10637
|
+
let existingJsonValue;
|
|
10638
|
+
try {
|
|
10639
|
+
existingJsonValue = JSON.parse(existingFileContent);
|
|
10640
|
+
} catch (error) {
|
|
10641
|
+
throw new Error(`Failed to parse existing Claude settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
10642
|
+
}
|
|
10643
|
+
const jsonValue = applyIgnoreReadDenies({
|
|
10644
|
+
settings: existingJsonValue,
|
|
10645
|
+
readDenies: deniedValues
|
|
10557
10646
|
});
|
|
10558
|
-
const jsonValue = {
|
|
10559
|
-
...existingJsonValue,
|
|
10560
|
-
permissions: {
|
|
10561
|
-
...existingJsonValue.permissions,
|
|
10562
|
-
deny: (0, es_toolkit.uniq)([...preservedDenies, ...deniedValues].toSorted())
|
|
10563
|
-
}
|
|
10564
|
-
};
|
|
10565
10647
|
return new ClaudecodeIgnore({
|
|
10566
10648
|
outputRoot,
|
|
10567
10649
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -12248,6 +12330,38 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
|
|
|
12248
12330
|
envVars: "env_vars"
|
|
12249
12331
|
};
|
|
12250
12332
|
const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
|
|
12333
|
+
/**
|
|
12334
|
+
* Translate a server's `oauth` table from the canonical rulesync shape (Claude
|
|
12335
|
+
* Code style camelCase) into the shape Codex CLI understands. Codex expects the
|
|
12336
|
+
* OAuth client id under snake_case `client_id`; without it `codex mcp login`
|
|
12337
|
+
* falls back to dynamic client registration and fails for providers that do not
|
|
12338
|
+
* support it (e.g. Slack, see #2158). The canonical `clientId` is kept alongside
|
|
12339
|
+
* the added `client_id` so tools that read the camelCase shape keep working and
|
|
12340
|
+
* the round-trip stays stable.
|
|
12341
|
+
*/
|
|
12342
|
+
function mapOauthToCodex(oauth) {
|
|
12343
|
+
const result = omitPrototypePollutionKeys(oauth);
|
|
12344
|
+
if (typeof oauth["clientId"] === "string" && !("client_id" in result)) result["client_id"] = oauth["clientId"];
|
|
12345
|
+
return result;
|
|
12346
|
+
}
|
|
12347
|
+
/**
|
|
12348
|
+
* Reverse of {@link mapOauthToCodex}: collapse Codex's `oauth.client_id` back to
|
|
12349
|
+
* the canonical `clientId` on import. When both keys are present (the shape
|
|
12350
|
+
* rulesync itself emits) the canonical `clientId` wins and `client_id` is
|
|
12351
|
+
* dropped so a subsequent generate does not accumulate duplicates.
|
|
12352
|
+
*/
|
|
12353
|
+
function mapOauthFromCodex(oauth) {
|
|
12354
|
+
const result = {};
|
|
12355
|
+
for (const [key, value] of Object.entries(oauth)) {
|
|
12356
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12357
|
+
if (key === "client_id") {
|
|
12358
|
+
if (!("clientId" in oauth)) result["clientId"] = value;
|
|
12359
|
+
continue;
|
|
12360
|
+
}
|
|
12361
|
+
result[key] = value;
|
|
12362
|
+
}
|
|
12363
|
+
return result;
|
|
12364
|
+
}
|
|
12251
12365
|
function convertFromCodexFormat(codexMcp) {
|
|
12252
12366
|
const result = {};
|
|
12253
12367
|
for (const [name, config] of Object.entries(codexMcp)) {
|
|
@@ -12257,7 +12371,8 @@ function convertFromCodexFormat(codexMcp) {
|
|
|
12257
12371
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12258
12372
|
if (key === "enabled") {
|
|
12259
12373
|
if (value === false) converted["disabled"] = true;
|
|
12260
|
-
} else if (
|
|
12374
|
+
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
|
|
12375
|
+
else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
|
|
12261
12376
|
const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
|
|
12262
12377
|
if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
|
|
12263
12378
|
else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
|
|
@@ -12277,7 +12392,8 @@ function convertToCodexFormat(mcpServers) {
|
|
|
12277
12392
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
12278
12393
|
if (key === "disabled") {
|
|
12279
12394
|
if (value === true) converted["enabled"] = false;
|
|
12280
|
-
} else if (
|
|
12395
|
+
} else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
|
|
12396
|
+
else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
|
|
12281
12397
|
const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
|
|
12282
12398
|
if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
|
|
12283
12399
|
else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
|
|
@@ -13493,10 +13609,12 @@ function resolveHermesTimeout(config) {
|
|
|
13493
13609
|
*
|
|
13494
13610
|
* Hermes is close to the MCP spec but not identical: `command` must be a single
|
|
13495
13611
|
* executable string (an array's tail folds into `args`), a server is disabled
|
|
13496
|
-
* via `enabled: false` (not the canonical `disabled: true`),
|
|
13497
|
-
*
|
|
13498
|
-
* `
|
|
13499
|
-
*
|
|
13612
|
+
* via `enabled: false` (not the canonical `disabled: true`), remote servers use
|
|
13613
|
+
* `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
|
|
13614
|
+
* exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
|
|
13615
|
+
* fields Hermes understands are emitted, so the shared `config.yaml` is not
|
|
13616
|
+
* polluted with canonical-only aliases (`type`, `transport`, `httpUrl`,
|
|
13617
|
+
* `networkTimeout`, ...).
|
|
13500
13618
|
*/
|
|
13501
13619
|
function convertServerToHermes(config) {
|
|
13502
13620
|
const out = {};
|
|
@@ -13520,6 +13638,10 @@ function convertServerToHermes(config) {
|
|
|
13520
13638
|
if (config.disabled === true) out.enabled = false;
|
|
13521
13639
|
const timeout = resolveHermesTimeout(config);
|
|
13522
13640
|
if (timeout !== void 0) out.timeout = timeout;
|
|
13641
|
+
const tools = {};
|
|
13642
|
+
if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
|
|
13643
|
+
if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
|
|
13644
|
+
if (Object.keys(tools).length > 0) out.tools = tools;
|
|
13523
13645
|
return out;
|
|
13524
13646
|
}
|
|
13525
13647
|
/**
|
|
@@ -13561,6 +13683,10 @@ function convertFromHermesFormat(mcpServers) {
|
|
|
13561
13683
|
if (isPlainObject(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
|
|
13562
13684
|
if (config.enabled === false) server.disabled = true;
|
|
13563
13685
|
if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
|
|
13686
|
+
if (isRecord(config.tools)) {
|
|
13687
|
+
if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
|
|
13688
|
+
if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
|
|
13689
|
+
}
|
|
13564
13690
|
result[name] = server;
|
|
13565
13691
|
}
|
|
13566
13692
|
return result;
|
|
@@ -15716,17 +15842,232 @@ const PermissionActionSchema = zod_mini.z.enum([
|
|
|
15716
15842
|
*/
|
|
15717
15843
|
const PermissionRulesSchema = zod_mini.z.record(zod_mini.z.string(), PermissionActionSchema);
|
|
15718
15844
|
/**
|
|
15845
|
+
* OpenCode-specific permission value. Unlike the shared canonical block, which
|
|
15846
|
+
* only accepts a pattern-to-action map, OpenCode also allows a bare action
|
|
15847
|
+
* string that applies to the whole category (e.g. `"external_directory": "deny"`).
|
|
15848
|
+
* This mirrors OpenCode's own permission schema in `opencode-permissions.ts`.
|
|
15849
|
+
*/
|
|
15850
|
+
const OpencodeOverridePermissionValueSchema = zod_mini.z.union([PermissionActionSchema, PermissionRulesSchema]);
|
|
15851
|
+
/**
|
|
15852
|
+
* Tool-scoped override block for OpenCode. Permission categories placed here
|
|
15853
|
+
* (e.g. OpenCode-only categories such as `external_directory`) are emitted only
|
|
15854
|
+
* into OpenCode's config and never leak into other tools' permission files. It
|
|
15855
|
+
* also lets a shared category be overridden with an OpenCode-specific value.
|
|
15856
|
+
* Kept `looseObject` so future OpenCode categories are accepted.
|
|
15857
|
+
*
|
|
15858
|
+
* @example
|
|
15859
|
+
* { "permission": { "external_directory": "deny", "webfetch": "allow" } }
|
|
15860
|
+
*/
|
|
15861
|
+
const OpencodePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
|
|
15862
|
+
/**
|
|
15863
|
+
* Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
|
|
15864
|
+
* into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
|
|
15865
|
+
* It carries Hermes-specific approval/security controls that have no canonical
|
|
15866
|
+
* permission category — e.g. `approvals` (`mode`, `cron_mode`, ...),
|
|
15867
|
+
* `security` (`allow_private_urls`, ...), `skills.write_approval`,
|
|
15868
|
+
* `memory.write_approval`. Kept `looseObject` (a verbatim passthrough) so any
|
|
15869
|
+
* current or future Hermes config key can be authored without modeling each one.
|
|
15870
|
+
*
|
|
15871
|
+
* @example
|
|
15872
|
+
* { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
|
|
15873
|
+
*/
|
|
15874
|
+
const HermesPermissionsOverrideSchema = zod_mini.z.looseObject({});
|
|
15875
|
+
/**
|
|
15876
|
+
* Tool-scoped override block for Cline. Cline's `command-permissions.json`
|
|
15877
|
+
* carries a single global `allowRedirects` boolean (gates shell redirection
|
|
15878
|
+
* operators `>`/`>>`/`<`) that has no per-command dimension and therefore no
|
|
15879
|
+
* canonical permission category. Placing it here lets users author it
|
|
15880
|
+
* declaratively; it is emitted only into Cline's config. Kept `looseObject` so
|
|
15881
|
+
* future Cline-only knobs can be added.
|
|
15882
|
+
*
|
|
15883
|
+
* @example
|
|
15884
|
+
* { "allowRedirects": true }
|
|
15885
|
+
*/
|
|
15886
|
+
const ClinePermissionsOverrideSchema = zod_mini.z.looseObject({ allowRedirects: zod_mini.z.optional(zod_mini.z.boolean()) });
|
|
15887
|
+
/**
|
|
15888
|
+
* Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
|
|
15889
|
+
* object is a free-form record with tool-specific keys that have no canonical
|
|
15890
|
+
* category — OpenCode-inherited ones (`external_directory`, `doom_loop`, `lsp`,
|
|
15891
|
+
* `question`, `todowrite`, `skill`, `task`, `list`) and Kilo-unique ones
|
|
15892
|
+
* (`agent_manager`, `notebook_read`, `notebook_edit`, `notebook_execute`,
|
|
15893
|
+
* `repo_clone`, `repo_overview`). Placing them here makes them authorable and
|
|
15894
|
+
* portable and keeps them out of other tools' configs. Mirrors the OpenCode
|
|
15895
|
+
* override; each value may be a bare action string or a pattern map.
|
|
15896
|
+
*
|
|
15897
|
+
* @example
|
|
15898
|
+
* { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
|
|
15899
|
+
*/
|
|
15900
|
+
const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), OpencodeOverridePermissionValueSchema)) });
|
|
15901
|
+
/**
|
|
15902
|
+
* Tool-scoped override block for Claude Code. Claude Code's `permissions` object
|
|
15903
|
+
* (in `.claude/settings.json`) carries non-list fields that have no canonical
|
|
15904
|
+
* permission category — `defaultMode` (the session-start permission mode) and
|
|
15905
|
+
* `additionalDirectories` (extra working directories) being the primary ones.
|
|
15906
|
+
* Fields placed under `claudecode.permissions` are merged into the settings
|
|
15907
|
+
* `permissions` object and emitted only for Claude Code, while the shared
|
|
15908
|
+
* `permission` block continues to drive the `allow`/`ask`/`deny` arrays. Kept a
|
|
15909
|
+
* `looseObject` passthrough so any current or future `permissions` field can be
|
|
15910
|
+
* authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
|
|
15911
|
+
* ignored here (rulesync owns them).
|
|
15912
|
+
*
|
|
15913
|
+
* @example
|
|
15914
|
+
* { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
|
|
15915
|
+
*/
|
|
15916
|
+
const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({ permissions: zod_mini.z.optional(zod_mini.z.looseObject({})) });
|
|
15917
|
+
/**
|
|
15918
|
+
* Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
|
|
15919
|
+
* carries a `sensitive_patterns` list — patterns that escalate to ASK even when
|
|
15920
|
+
* the tool's base permission is ALWAYS (allow). The canonical model can only set
|
|
15921
|
+
* a pattern to a single `allow`/`ask`/`deny`, so an "allow by default but ask on
|
|
15922
|
+
* these patterns" escalation cannot be expressed. Entries under
|
|
15923
|
+
* `vibe.permission.<category>.sensitive_patterns` carry that list per canonical
|
|
15924
|
+
* category; the shared `permission` block still drives the base permission and
|
|
15925
|
+
* allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
|
|
15926
|
+
*
|
|
15927
|
+
* @example
|
|
15928
|
+
* { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
|
|
15929
|
+
*/
|
|
15930
|
+
const VibePermissionsOverrideSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({ sensitive_patterns: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) }))) });
|
|
15931
|
+
/**
|
|
15932
|
+
* Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
|
|
15933
|
+
* autonomy settings with no canonical permission category — `approvalMode`
|
|
15934
|
+
* (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
|
|
15935
|
+
* (`mode`/`networkAccess`). Fields placed here are merged into the top-level of
|
|
15936
|
+
* `.cursor/cli.json` (project) / `~/.cursor/cli-config.json` (global) and emitted
|
|
15937
|
+
* only for Cursor, while the shared `permission` block continues to drive the
|
|
15938
|
+
* `permissions.allow`/`permissions.deny` arrays. Kept a `looseObject` so extra
|
|
15939
|
+
* `cli.json` keys can be authored (they are merged verbatim on generate);
|
|
15940
|
+
* `sandbox`'s accepted values are not documented so it passes through verbatim.
|
|
15941
|
+
* Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
|
|
15942
|
+
* authored here reach `cli.json` on generate but are not re-extracted.
|
|
15943
|
+
*
|
|
15944
|
+
* @example
|
|
15945
|
+
* { "approvalMode": "auto-review" }
|
|
15946
|
+
*/
|
|
15947
|
+
const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
15948
|
+
approvalMode: zod_mini.z.optional(zod_mini.z.string()),
|
|
15949
|
+
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
15950
|
+
});
|
|
15951
|
+
/**
|
|
15952
|
+
* Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
|
|
15953
|
+
* autonomy/sandbox controls with no canonical permission category — under
|
|
15954
|
+
* `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
|
|
15955
|
+
* `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
|
|
15956
|
+
* placed here are merged into the matching `settings.json` group and emitted
|
|
15957
|
+
* only for Qwen, while the shared `permission` block continues to drive the
|
|
15958
|
+
* `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
|
|
15959
|
+
* passthrough) so any current or future `tools`/`security` key can be authored.
|
|
15960
|
+
*
|
|
15961
|
+
* @example
|
|
15962
|
+
* { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
|
|
15963
|
+
*/
|
|
15964
|
+
const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
15965
|
+
tools: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
15966
|
+
security: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
15967
|
+
});
|
|
15968
|
+
/**
|
|
15969
|
+
* Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
|
|
15970
|
+
* to per-tool allow/ask/deny with no canonical category — the `[sandbox]`
|
|
15971
|
+
* enforcement table (`workspace_root`, `allow_write`, `forbid_read`, `bash`,
|
|
15972
|
+
* `network`) and plan-mode read-only trust lists under `[agent]`
|
|
15973
|
+
* (`plan_mode_allowed_tools`, `plan_mode_read_only_commands`). Fields placed here
|
|
15974
|
+
* are merged into the matching `reasonix.toml` table and emitted only for
|
|
15975
|
+
* Reasonix, while the shared `permission` block continues to drive
|
|
15976
|
+
* `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
|
|
15977
|
+
* Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
|
|
15978
|
+
* re-extracted from `[agent]` on import — other `agent` keys authored here reach
|
|
15979
|
+
* `reasonix.toml` on generate but are not re-extracted back into the override.
|
|
15980
|
+
*
|
|
15981
|
+
* @example
|
|
15982
|
+
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
15983
|
+
*/
|
|
15984
|
+
const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
15985
|
+
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
15986
|
+
agent: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
15987
|
+
});
|
|
15988
|
+
/**
|
|
15989
|
+
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
15990
|
+
* exposes security controls with no canonical per-command allow/ask/deny slot —
|
|
15991
|
+
* `commandBlocklist` (a hard-block tier that can never be approved, distinct from
|
|
15992
|
+
* an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
|
|
15993
|
+
* (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
|
|
15994
|
+
* and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
|
|
15995
|
+
* `interactionMode`). Fields placed here are merged into `settings.json` and
|
|
15996
|
+
* emitted only for Factory Droid, while the shared `permission` block continues
|
|
15997
|
+
* to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
|
|
15998
|
+
*
|
|
15999
|
+
* @example
|
|
16000
|
+
* { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
|
|
16001
|
+
*/
|
|
16002
|
+
const FactorydroidPermissionsOverrideSchema = zod_mini.z.looseObject({ commandBlocklist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())) });
|
|
16003
|
+
/**
|
|
16004
|
+
* Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
|
|
16005
|
+
* file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
|
|
16006
|
+
* slot — `agent_mode_coding_permissions`
|
|
16007
|
+
* (`always_ask_before_reading` | `always_allow_reading` | `allow_reading_specific_files`),
|
|
16008
|
+
* `agent_mode_coding_file_read_allowlist` (a path array), and
|
|
16009
|
+
* `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
|
|
16010
|
+
* Fields placed here are merged into `[agents.profiles]` of Warp's global
|
|
16011
|
+
* `settings.toml`, while the shared `permission` block continues to drive the
|
|
16012
|
+
* `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
|
|
16013
|
+
* Warp permissions are global-only.
|
|
16014
|
+
*
|
|
16015
|
+
* @example
|
|
16016
|
+
* { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
|
|
16017
|
+
*/
|
|
16018
|
+
const WarpPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
16019
|
+
agent_mode_coding_permissions: zod_mini.z.optional(zod_mini.z.string()),
|
|
16020
|
+
agent_mode_coding_file_read_allowlist: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
16021
|
+
agent_mode_execute_readonly_commands: zod_mini.z.optional(zod_mini.z.boolean())
|
|
16022
|
+
});
|
|
16023
|
+
/**
|
|
16024
|
+
* Tool-scoped override block for JetBrains Junie. Junie's `allowlist.json` has
|
|
16025
|
+
* two top-level autonomy knobs with no canonical per-glob slot:
|
|
16026
|
+
* `allowReadonlyCommands` (a boolean auto-allowing read-only commands) and
|
|
16027
|
+
* `defaultBehavior` (the fallback action applied when no rule matches — Junie
|
|
16028
|
+
* documents only `allow`/`ask`). Fields placed here are merged onto the
|
|
16029
|
+
* top level of `allowlist.json`, while the shared `permission` block continues
|
|
16030
|
+
* to drive the per-category `rules` groups.
|
|
16031
|
+
*
|
|
16032
|
+
* @example
|
|
16033
|
+
* { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
|
|
16034
|
+
*/
|
|
16035
|
+
const JuniePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
16036
|
+
allowReadonlyCommands: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
16037
|
+
defaultBehavior: zod_mini.z.optional(zod_mini.z.string())
|
|
16038
|
+
});
|
|
16039
|
+
/**
|
|
15719
16040
|
* Permissions configuration.
|
|
15720
16041
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
15721
16042
|
* Values are pattern-to-action mappings for that tool category.
|
|
15722
16043
|
*
|
|
16044
|
+
* The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
|
|
16045
|
+
* `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie` keys are tool-scoped
|
|
16046
|
+
* overrides consumed only by their respective translator (see the matching
|
|
16047
|
+
* `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
|
|
16048
|
+
* block and ignores them.
|
|
16049
|
+
*
|
|
15723
16050
|
* @example
|
|
15724
16051
|
* {
|
|
15725
16052
|
* "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
|
|
15726
16053
|
* "edit": { "*": "deny", "src/**": "allow" }
|
|
15727
16054
|
* }
|
|
15728
16055
|
*/
|
|
15729
|
-
const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
16056
|
+
const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
16057
|
+
permission: zod_mini.z.record(zod_mini.z.string(), PermissionRulesSchema),
|
|
16058
|
+
opencode: zod_mini.z.optional(OpencodePermissionsOverrideSchema),
|
|
16059
|
+
hermes: zod_mini.z.optional(HermesPermissionsOverrideSchema),
|
|
16060
|
+
cline: zod_mini.z.optional(ClinePermissionsOverrideSchema),
|
|
16061
|
+
kilo: zod_mini.z.optional(KiloPermissionsOverrideSchema),
|
|
16062
|
+
claudecode: zod_mini.z.optional(ClaudecodePermissionsOverrideSchema),
|
|
16063
|
+
vibe: zod_mini.z.optional(VibePermissionsOverrideSchema),
|
|
16064
|
+
cursor: zod_mini.z.optional(CursorPermissionsOverrideSchema),
|
|
16065
|
+
qwencode: zod_mini.z.optional(QwencodePermissionsOverrideSchema),
|
|
16066
|
+
reasonix: zod_mini.z.optional(ReasonixPermissionsOverrideSchema),
|
|
16067
|
+
factorydroid: zod_mini.z.optional(FactorydroidPermissionsOverrideSchema),
|
|
16068
|
+
warp: zod_mini.z.optional(WarpPermissionsOverrideSchema),
|
|
16069
|
+
junie: zod_mini.z.optional(JuniePermissionsOverrideSchema)
|
|
16070
|
+
});
|
|
15730
16071
|
/**
|
|
15731
16072
|
* Full permissions file schema including optional $schema field.
|
|
15732
16073
|
*/
|
|
@@ -17097,32 +17438,24 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
17097
17438
|
}
|
|
17098
17439
|
const config = rulesyncPermissions.getJson();
|
|
17099
17440
|
const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
|
|
17100
|
-
const
|
|
17101
|
-
|
|
17102
|
-
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
17106
|
-
|
|
17107
|
-
const { toolName } = parseClaudePermissionEntry(entry);
|
|
17108
|
-
return toolName === "Read";
|
|
17109
|
-
});
|
|
17110
|
-
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.`);
|
|
17441
|
+
const overridePermissions = config.claudecode?.permissions;
|
|
17442
|
+
if (overridePermissions && typeof overridePermissions === "object") {
|
|
17443
|
+
const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
|
|
17444
|
+
settings.permissions = {
|
|
17445
|
+
...settings.permissions,
|
|
17446
|
+
...nonListFields
|
|
17447
|
+
};
|
|
17111
17448
|
}
|
|
17112
|
-
const
|
|
17113
|
-
const
|
|
17114
|
-
|
|
17115
|
-
|
|
17116
|
-
|
|
17117
|
-
|
|
17118
|
-
|
|
17119
|
-
|
|
17120
|
-
|
|
17121
|
-
|
|
17122
|
-
const merged = {
|
|
17123
|
-
...settings,
|
|
17124
|
-
permissions: mergedPermissions
|
|
17125
|
-
};
|
|
17449
|
+
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
|
|
17450
|
+
const merged = applyPermissions({
|
|
17451
|
+
settings,
|
|
17452
|
+
managedToolNames,
|
|
17453
|
+
toolNameOf: (entry) => parseClaudePermissionEntry(entry).toolName,
|
|
17454
|
+
allow,
|
|
17455
|
+
ask,
|
|
17456
|
+
deny,
|
|
17457
|
+
logger
|
|
17458
|
+
});
|
|
17126
17459
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
17127
17460
|
return new ClaudecodePermissions({
|
|
17128
17461
|
outputRoot,
|
|
@@ -17145,6 +17478,8 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
17145
17478
|
ask: permissions.ask ?? [],
|
|
17146
17479
|
deny: permissions.deny ?? []
|
|
17147
17480
|
});
|
|
17481
|
+
const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
|
|
17482
|
+
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
17148
17483
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
17149
17484
|
}
|
|
17150
17485
|
validate() {
|
|
@@ -17318,7 +17653,8 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17318
17653
|
} catch (error) {
|
|
17319
17654
|
throw new Error(`Failed to parse existing Cline command-permissions at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
17320
17655
|
}
|
|
17321
|
-
const
|
|
17656
|
+
const config = rulesyncPermissions.getJson();
|
|
17657
|
+
const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(config.permission);
|
|
17322
17658
|
warnClineTranslationNotices({
|
|
17323
17659
|
droppedCategories,
|
|
17324
17660
|
translatedAskPatterns,
|
|
@@ -17334,7 +17670,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17334
17670
|
...existing,
|
|
17335
17671
|
allow: dedupedAllow,
|
|
17336
17672
|
deny: mergedDeny,
|
|
17337
|
-
allowRedirects: existing.allowRedirects ?? false
|
|
17673
|
+
allowRedirects: config.cline?.allowRedirects ?? existing.allowRedirects ?? false
|
|
17338
17674
|
};
|
|
17339
17675
|
return new ClinePermissions({
|
|
17340
17676
|
outputRoot,
|
|
@@ -17358,6 +17694,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
|
|
|
17358
17694
|
for (const pattern of parsed.allow ?? []) bashRules[pattern] = "allow";
|
|
17359
17695
|
for (const pattern of parsed.deny ?? []) bashRules[pattern] = "deny";
|
|
17360
17696
|
const config = Object.keys(bashRules).length > 0 ? { permission: { bash: bashRules } } : { permission: {} };
|
|
17697
|
+
if (parsed.allowRedirects === true) config.cline = { allowRedirects: true };
|
|
17361
17698
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
17362
17699
|
}
|
|
17363
17700
|
validate() {
|
|
@@ -17794,13 +18131,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
17794
18131
|
WebFetch: "webfetch",
|
|
17795
18132
|
Mcp: "mcp"
|
|
17796
18133
|
};
|
|
17797
|
-
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
18134
|
+
const MCP_CANONICAL_PREFIX$1 = "mcp__";
|
|
17798
18135
|
/**
|
|
17799
18136
|
* Returns true if the canonical category is the per-tool MCP form
|
|
17800
18137
|
* `mcp__<server>__<tool>`.
|
|
17801
18138
|
*/
|
|
17802
18139
|
function isMcpScopedCategory(canonical) {
|
|
17803
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX) && canonical.length > 5;
|
|
18140
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
|
|
17804
18141
|
}
|
|
17805
18142
|
function toCursorType(canonical) {
|
|
17806
18143
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -17831,7 +18168,7 @@ function toCursorPattern(canonical, pattern) {
|
|
|
17831
18168
|
function toCanonicalCategory$1(cursorType, pattern) {
|
|
17832
18169
|
if (cursorType === "Mcp") {
|
|
17833
18170
|
const match = pattern.match(/^([^:()]+):([^:()]+)$/);
|
|
17834
|
-
if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
18171
|
+
if (match) return `${MCP_CANONICAL_PREFIX$1}${match[1] ?? "*"}__${match[2] ?? "*"}`;
|
|
17835
18172
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
17836
18173
|
}
|
|
17837
18174
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
@@ -17965,8 +18302,10 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
17965
18302
|
mergedPermissions.allow = mergedAllow;
|
|
17966
18303
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
17967
18304
|
else delete mergedPermissions.deny;
|
|
18305
|
+
const cursorOverride = config.cursor;
|
|
17968
18306
|
const merged = {
|
|
17969
18307
|
...settings,
|
|
18308
|
+
...cursorOverride,
|
|
17970
18309
|
version: settings.version ?? 1,
|
|
17971
18310
|
editor: {
|
|
17972
18311
|
...existingEditor,
|
|
@@ -17992,11 +18331,15 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
|
|
|
17992
18331
|
}
|
|
17993
18332
|
const permissionsRaw = settings.permissions;
|
|
17994
18333
|
const permissions = permissionsRaw !== null && typeof permissionsRaw === "object" && !Array.isArray(permissionsRaw) ? permissionsRaw : {};
|
|
17995
|
-
const
|
|
18334
|
+
const result = { ...convertCursorToRulesyncPermissions({
|
|
17996
18335
|
allow: asCursorPermissionEntryArray(permissions.allow),
|
|
17997
18336
|
deny: asCursorPermissionEntryArray(permissions.deny)
|
|
17998
|
-
});
|
|
17999
|
-
|
|
18337
|
+
}) };
|
|
18338
|
+
const cursorOverride = {};
|
|
18339
|
+
if (settings.approvalMode !== void 0) cursorOverride.approvalMode = settings.approvalMode;
|
|
18340
|
+
if (settings.sandbox !== null && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)) cursorOverride.sandbox = settings.sandbox;
|
|
18341
|
+
if (Object.keys(cursorOverride).length > 0) result.cursor = cursorOverride;
|
|
18342
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18000
18343
|
}
|
|
18001
18344
|
validate() {
|
|
18002
18345
|
return {
|
|
@@ -18055,7 +18398,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
18055
18398
|
const { type, pattern } = parseCursorPermissionEntry(entry);
|
|
18056
18399
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
18057
18400
|
if (!permission[canonical]) permission[canonical] = {};
|
|
18058
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
|
|
18401
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
|
|
18059
18402
|
permission[canonical][canonicalPattern] = action;
|
|
18060
18403
|
}
|
|
18061
18404
|
};
|
|
@@ -18304,6 +18647,16 @@ function convertDevinToRulesyncPermissions(params) {
|
|
|
18304
18647
|
}
|
|
18305
18648
|
//#endregion
|
|
18306
18649
|
//#region src/features/permissions/factorydroid-permissions.ts
|
|
18650
|
+
const FACTORYDROID_OVERRIDE_KEYS = [
|
|
18651
|
+
"commandBlocklist",
|
|
18652
|
+
"networkPolicy",
|
|
18653
|
+
"sandbox",
|
|
18654
|
+
"mcpPolicy",
|
|
18655
|
+
"enableDroidShield",
|
|
18656
|
+
"sessionDefaultSettings",
|
|
18657
|
+
"maxAutonomyLevel",
|
|
18658
|
+
"interactionMode"
|
|
18659
|
+
];
|
|
18307
18660
|
/**
|
|
18308
18661
|
* Permissions adapter for Factory Droid.
|
|
18309
18662
|
*
|
|
@@ -18321,18 +18674,14 @@ function convertDevinToRulesyncPermissions(params) {
|
|
|
18321
18674
|
* skipped (with a warning when they carry `deny` rules, to surface the gap).
|
|
18322
18675
|
*
|
|
18323
18676
|
* Factory Droid also has a stronger `commandBlocklist` tier — commands that can
|
|
18324
|
-
* never run, not even under full autonomy
|
|
18325
|
-
*
|
|
18326
|
-
*
|
|
18327
|
-
*
|
|
18328
|
-
*
|
|
18329
|
-
*
|
|
18330
|
-
*
|
|
18331
|
-
*
|
|
18332
|
-
* `commandBlocklist` and re-exporting it to a *fresh* config writes it back as
|
|
18333
|
-
* `commandDenylist`, not `commandBlocklist` — the hard-block tier is not
|
|
18334
|
-
* reconstructed. Re-running over the original file keeps it intact via the
|
|
18335
|
-
* verbatim preservation above.)
|
|
18677
|
+
* never run, not even under full autonomy — plus other security controls
|
|
18678
|
+
* (`networkPolicy`, `sandbox`, `mcpPolicy`, `enableDroidShield`, autonomy
|
|
18679
|
+
* settings) that do not fit the canonical `allow | ask | deny` per-command
|
|
18680
|
+
* model. These are authored and round-tripped through the `factorydroid`
|
|
18681
|
+
* override namespace (see `FactorydroidPermissionsOverrideSchema`): on **import**
|
|
18682
|
+
* they are lifted from `settings.json` into the override, and on **export** they
|
|
18683
|
+
* are merged back in — so `commandBlocklist`'s never-runs guarantee is preserved
|
|
18684
|
+
* faithfully rather than being collapsed onto an approvable `deny`.
|
|
18336
18685
|
*/
|
|
18337
18686
|
var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissions {
|
|
18338
18687
|
constructor(params) {
|
|
@@ -18375,11 +18724,16 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
|
|
|
18375
18724
|
} catch (error) {
|
|
18376
18725
|
throw new Error(`Failed to parse existing Factory Droid settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18377
18726
|
}
|
|
18727
|
+
const config = rulesyncPermissions.getJson();
|
|
18378
18728
|
const { allow, deny } = convertRulesyncToFactorydroidPermissions({
|
|
18379
|
-
config
|
|
18729
|
+
config,
|
|
18380
18730
|
logger
|
|
18381
18731
|
});
|
|
18382
|
-
const
|
|
18732
|
+
const override = config.factorydroid;
|
|
18733
|
+
const merged = {
|
|
18734
|
+
...settings,
|
|
18735
|
+
...override !== void 0 && typeof override === "object" ? override : {}
|
|
18736
|
+
};
|
|
18383
18737
|
const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
|
|
18384
18738
|
const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
|
|
18385
18739
|
if (mergedAllow.length > 0) merged.commandAllowlist = mergedAllow;
|
|
@@ -18403,10 +18757,13 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
|
|
|
18403
18757
|
}
|
|
18404
18758
|
const config = convertFactorydroidToRulesyncPermissions({
|
|
18405
18759
|
allow: Array.isArray(settings.commandAllowlist) ? settings.commandAllowlist : [],
|
|
18406
|
-
deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
|
|
18407
|
-
block: Array.isArray(settings.commandBlocklist) ? settings.commandBlocklist : []
|
|
18760
|
+
deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
|
|
18408
18761
|
});
|
|
18409
|
-
|
|
18762
|
+
const factorydroidOverride = {};
|
|
18763
|
+
for (const key of FACTORYDROID_OVERRIDE_KEYS) if (settings[key] !== void 0) factorydroidOverride[key] = settings[key];
|
|
18764
|
+
const result = { ...config };
|
|
18765
|
+
if (Object.keys(factorydroidOverride).length > 0) result.factorydroid = factorydroidOverride;
|
|
18766
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18410
18767
|
}
|
|
18411
18768
|
validate() {
|
|
18412
18769
|
return {
|
|
@@ -18453,18 +18810,18 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
|
|
|
18453
18810
|
};
|
|
18454
18811
|
}
|
|
18455
18812
|
/**
|
|
18456
|
-
* Convert Factory Droid allow/deny
|
|
18457
|
-
*
|
|
18813
|
+
* Convert Factory Droid allow/deny command lists back to rulesync config under
|
|
18814
|
+
* the `bash` category.
|
|
18458
18815
|
*
|
|
18459
|
-
* `commandBlocklist` (hard
|
|
18460
|
-
*
|
|
18461
|
-
* the
|
|
18816
|
+
* `commandBlocklist` (the hard-block tier) is no longer collapsed here — it has
|
|
18817
|
+
* no canonical equivalent and now round-trips through the `factorydroid`
|
|
18818
|
+
* override so the never-runs guarantee is preserved instead of being weakened to
|
|
18819
|
+
* an approvable `deny`.
|
|
18462
18820
|
*/
|
|
18463
|
-
function convertFactorydroidToRulesyncPermissions({ allow, deny
|
|
18821
|
+
function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
18464
18822
|
const bash = {};
|
|
18465
18823
|
for (const pattern of allow) bash[pattern] = "allow";
|
|
18466
18824
|
for (const pattern of deny) bash[pattern] = "deny";
|
|
18467
|
-
for (const pattern of block) bash[pattern] = "deny";
|
|
18468
18825
|
return { permission: Object.keys(bash).length > 0 ? { bash } : {} };
|
|
18469
18826
|
}
|
|
18470
18827
|
//#endregion
|
|
@@ -18658,30 +19015,114 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
18658
19015
|
const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
|
|
18659
19016
|
const GROKCLI_UI_KEY = "ui";
|
|
18660
19017
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
19018
|
+
const GROKCLI_PERMISSION_KEY = "permission";
|
|
18661
19019
|
const CATCH_ALL_PATTERN$2 = "*";
|
|
19020
|
+
const MCP_CANONICAL_PREFIX = "mcp__";
|
|
19021
|
+
const CATEGORY_TO_GROK_TOOL = {
|
|
19022
|
+
bash: "Bash",
|
|
19023
|
+
read: "Read",
|
|
19024
|
+
edit: "Edit",
|
|
19025
|
+
write: "Edit",
|
|
19026
|
+
grep: "Grep",
|
|
19027
|
+
webfetch: "WebFetch"
|
|
19028
|
+
};
|
|
19029
|
+
const GROK_TOOL_TO_CATEGORY = {
|
|
19030
|
+
Bash: "bash",
|
|
19031
|
+
Read: "read",
|
|
19032
|
+
Edit: "edit",
|
|
19033
|
+
Grep: "grep",
|
|
19034
|
+
WebFetch: "webfetch"
|
|
19035
|
+
};
|
|
19036
|
+
const GROK_MCP_TOOL = "MCPTool";
|
|
19037
|
+
/**
|
|
19038
|
+
* Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
|
|
19039
|
+
* `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
|
|
19040
|
+
* for categories Grok cannot express so callers can skip them.
|
|
19041
|
+
*
|
|
19042
|
+
* Scoped MCP categories (`mcp__<remainder>`) fold their address into the
|
|
19043
|
+
* parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
|
|
19044
|
+
* `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
|
|
19045
|
+
* concrete pattern emits `Tool(pattern)`.
|
|
19046
|
+
*/
|
|
19047
|
+
function buildGrokEntry(category, pattern) {
|
|
19048
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX)) {
|
|
19049
|
+
const remainder = category.slice(5);
|
|
19050
|
+
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
19051
|
+
}
|
|
19052
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
19053
|
+
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
19054
|
+
if (tool === void 0) return null;
|
|
19055
|
+
return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
|
|
19056
|
+
}
|
|
19057
|
+
/**
|
|
19058
|
+
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
19059
|
+
* pattern. Entries without parentheses are treated as catch-all (`*`). Returns
|
|
19060
|
+
* `null` for tool prefixes with no canonical equivalent (e.g. `any`).
|
|
19061
|
+
*/
|
|
19062
|
+
function parseGrokEntry(entry) {
|
|
19063
|
+
const trimmed = entry.trim();
|
|
19064
|
+
const parenIndex = trimmed.indexOf("(");
|
|
19065
|
+
let tool;
|
|
19066
|
+
let inner;
|
|
19067
|
+
if (parenIndex === -1 || !trimmed.endsWith(")")) {
|
|
19068
|
+
tool = trimmed;
|
|
19069
|
+
inner = "";
|
|
19070
|
+
} else {
|
|
19071
|
+
tool = trimmed.slice(0, parenIndex);
|
|
19072
|
+
inner = trimmed.slice(parenIndex + 1, -1).trim();
|
|
19073
|
+
}
|
|
19074
|
+
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
19075
|
+
category: `${MCP_CANONICAL_PREFIX}${inner}`,
|
|
19076
|
+
pattern: CATCH_ALL_PATTERN$2
|
|
19077
|
+
} : {
|
|
19078
|
+
category: "mcp",
|
|
19079
|
+
pattern: CATCH_ALL_PATTERN$2
|
|
19080
|
+
};
|
|
19081
|
+
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
19082
|
+
if (category === void 0) return null;
|
|
19083
|
+
return {
|
|
19084
|
+
category,
|
|
19085
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
|
|
19086
|
+
};
|
|
19087
|
+
}
|
|
18662
19088
|
/**
|
|
18663
19089
|
* Permissions adapter for the xAI Grok Build CLI (`grokcli`).
|
|
18664
19090
|
*
|
|
18665
|
-
* Grok
|
|
18666
|
-
*
|
|
18667
|
-
*
|
|
18668
|
-
*
|
|
18669
|
-
*
|
|
18670
|
-
*
|
|
18671
|
-
*
|
|
18672
|
-
*
|
|
18673
|
-
*
|
|
18674
|
-
*
|
|
18675
|
-
* `
|
|
18676
|
-
*
|
|
18677
|
-
*
|
|
18678
|
-
*
|
|
18679
|
-
* the
|
|
18680
|
-
*
|
|
18681
|
-
*
|
|
18682
|
-
*
|
|
18683
|
-
*
|
|
18684
|
-
*
|
|
19091
|
+
* Grok Build CLI ships a Claude-style rule system under `[permission]` in
|
|
19092
|
+
* `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
|
|
19093
|
+
* `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`, and
|
|
19094
|
+
* `WebFetch`, evaluated with precedence `deny > ask > allow`
|
|
19095
|
+
* (https://docs.x.ai/build/settings/reference). rulesync's canonical
|
|
19096
|
+
* per-category, per-pattern model maps almost 1:1:
|
|
19097
|
+
* - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
|
|
19098
|
+
* the matching Grok entry and is bucketed into the `[permission]` array for
|
|
19099
|
+
* that action. `bash|read|edit|grep|webfetch` map to their Grok tool;
|
|
19100
|
+
* `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*` maps to
|
|
19101
|
+
* `MCPTool(...)` (a scoped MCP category folds its address into the
|
|
19102
|
+
* parentheses, so a non-`*` argument pattern on it is not represented).
|
|
19103
|
+
* Categories with no Grok tool (`websearch`, `glob`, `notebookedit`,
|
|
19104
|
+
* `agent`) are skipped (with a warning when they carry a `deny` rule, to
|
|
19105
|
+
* surface the gap). When two canonical rules collapse onto the same Grok
|
|
19106
|
+
* entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
|
|
19107
|
+
* the strictest wins (`deny > ask > allow`) and a warning is logged, so the
|
|
19108
|
+
* entry never lands contradictorily in two arrays.
|
|
19109
|
+
* - Import: the `[permission]` arrays are parsed back into canonical
|
|
19110
|
+
* categories. When no `[permission]` section is present (older configs), the
|
|
19111
|
+
* coarse `[ui] permission_mode` is used as a fallback.
|
|
19112
|
+
*
|
|
19113
|
+
* The coarse `[ui] permission_mode` toggle is still written for backward
|
|
19114
|
+
* compatibility with older Grok versions: `always-approve` when the config is
|
|
19115
|
+
* pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
|
|
19116
|
+
* while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
|
|
19117
|
+
* arrays).
|
|
19118
|
+
*
|
|
19119
|
+
* This surface is **global only** — the adapter syncs the user-level
|
|
19120
|
+
* `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
|
|
19121
|
+
* file; project scope is not modeled here.) The shared config is merged in
|
|
19122
|
+
* place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
|
|
19123
|
+
* tools it models and the `[ui] permission_mode` value, while every other key
|
|
19124
|
+
* (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
|
|
19125
|
+
* entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
|
|
18685
19126
|
* file is never deleted.
|
|
18686
19127
|
*/
|
|
18687
19128
|
var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
@@ -18713,7 +19154,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18713
19154
|
global: true
|
|
18714
19155
|
});
|
|
18715
19156
|
}
|
|
18716
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
|
|
19157
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
|
|
18717
19158
|
if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
|
|
18718
19159
|
const paths = GrokcliPermissions.getSettablePaths({ global });
|
|
18719
19160
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
@@ -18724,7 +19165,16 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18724
19165
|
} catch (error) {
|
|
18725
19166
|
throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18726
19167
|
}
|
|
18727
|
-
const
|
|
19168
|
+
const config = rulesyncPermissions.getJson();
|
|
19169
|
+
const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
|
|
19170
|
+
const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
|
|
19171
|
+
parsed[GROKCLI_PERMISSION_KEY] = {
|
|
19172
|
+
...existingPermission,
|
|
19173
|
+
allow: buckets.allow,
|
|
19174
|
+
deny: buckets.deny,
|
|
19175
|
+
ask: buckets.ask
|
|
19176
|
+
};
|
|
19177
|
+
const mode = deriveGrokPermissionMode(config);
|
|
18728
19178
|
const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
|
|
18729
19179
|
parsed[GROKCLI_UI_KEY] = {
|
|
18730
19180
|
...existingUi,
|
|
@@ -18747,8 +19197,8 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18747
19197
|
} catch (error) {
|
|
18748
19198
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18749
19199
|
}
|
|
18750
|
-
const
|
|
18751
|
-
const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]:
|
|
19200
|
+
const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
19201
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
|
|
18752
19202
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
18753
19203
|
}
|
|
18754
19204
|
validate() {
|
|
@@ -18768,6 +19218,84 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
18768
19218
|
});
|
|
18769
19219
|
}
|
|
18770
19220
|
};
|
|
19221
|
+
const ACTION_RANK = {
|
|
19222
|
+
allow: 0,
|
|
19223
|
+
ask: 1,
|
|
19224
|
+
deny: 2
|
|
19225
|
+
};
|
|
19226
|
+
/**
|
|
19227
|
+
* Collect user-authored entries from an existing `[permission]` array whose
|
|
19228
|
+
* tool prefix rulesync cannot model (e.g. `WebSearch`, `any`). Such entries are
|
|
19229
|
+
* preserved verbatim so replacing the arrays does not silently drop them
|
|
19230
|
+
* (mirrors the Cursor adapter's preservation of unmanaged types).
|
|
19231
|
+
*/
|
|
19232
|
+
function unmanagedEntries(existingPermission, key) {
|
|
19233
|
+
return (isStringArray(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
|
|
19234
|
+
}
|
|
19235
|
+
/**
|
|
19236
|
+
* Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
|
|
19237
|
+
* Claude-style entries, resolving collapse collisions via `deny > ask > allow`.
|
|
19238
|
+
* Categories Grok cannot express are skipped (with a warning when they carry a
|
|
19239
|
+
* `deny` rule); entries the user authored for tools rulesync cannot model are
|
|
19240
|
+
* preserved from `existingPermission`.
|
|
19241
|
+
*/
|
|
19242
|
+
function buildGrokPermissionArrays(config, existingPermission, logger) {
|
|
19243
|
+
const ranked = /* @__PURE__ */ new Map();
|
|
19244
|
+
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
19245
|
+
const entry = buildGrokEntry(category, pattern);
|
|
19246
|
+
if (entry === null) {
|
|
19247
|
+
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.`);
|
|
19248
|
+
continue;
|
|
19249
|
+
}
|
|
19250
|
+
const existing = ranked.get(entry);
|
|
19251
|
+
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).`);
|
|
19252
|
+
if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(entry, action);
|
|
19253
|
+
}
|
|
19254
|
+
const allow = unmanagedEntries(existingPermission, "allow");
|
|
19255
|
+
const deny = unmanagedEntries(existingPermission, "deny");
|
|
19256
|
+
const ask = unmanagedEntries(existingPermission, "ask");
|
|
19257
|
+
for (const [entry, action] of ranked) if (action === "allow") allow.push(entry);
|
|
19258
|
+
else if (action === "deny") deny.push(entry);
|
|
19259
|
+
else ask.push(entry);
|
|
19260
|
+
return {
|
|
19261
|
+
allow: (0, es_toolkit.uniq)(allow.toSorted()),
|
|
19262
|
+
deny: (0, es_toolkit.uniq)(deny.toSorted()),
|
|
19263
|
+
ask: (0, es_toolkit.uniq)(ask.toSorted())
|
|
19264
|
+
};
|
|
19265
|
+
}
|
|
19266
|
+
/**
|
|
19267
|
+
* Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
|
|
19268
|
+
* permission map. Returns `null` when the section defines none of the three
|
|
19269
|
+
* arrays, so the caller can fall back to the coarse `permission_mode`.
|
|
19270
|
+
* Precedence `deny > ask > allow` is applied so a tool listed in multiple
|
|
19271
|
+
* arrays resolves to the strictest action.
|
|
19272
|
+
*/
|
|
19273
|
+
function parseGrokPermissionArrays(permission) {
|
|
19274
|
+
const allow = isStringArray(permission.allow) ? permission.allow : void 0;
|
|
19275
|
+
const deny = isStringArray(permission.deny) ? permission.deny : void 0;
|
|
19276
|
+
const ask = isStringArray(permission.ask) ? permission.ask : void 0;
|
|
19277
|
+
if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
|
|
19278
|
+
const result = {};
|
|
19279
|
+
const apply = (entries, action) => {
|
|
19280
|
+
for (const entry of entries ?? []) {
|
|
19281
|
+
const parsed = parseGrokEntry(entry);
|
|
19282
|
+
if (parsed === null) continue;
|
|
19283
|
+
const bucket = result[parsed.category] ??= {};
|
|
19284
|
+
bucket[parsed.pattern] = action;
|
|
19285
|
+
}
|
|
19286
|
+
};
|
|
19287
|
+
apply(allow, "allow");
|
|
19288
|
+
apply(ask, "ask");
|
|
19289
|
+
apply(deny, "deny");
|
|
19290
|
+
return result;
|
|
19291
|
+
}
|
|
19292
|
+
/**
|
|
19293
|
+
* Legacy coarse fallback: map `[ui] permission_mode` to a canonical action.
|
|
19294
|
+
* `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
|
|
19295
|
+
*/
|
|
19296
|
+
function legacyModeAction(parsed) {
|
|
19297
|
+
return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
|
|
19298
|
+
}
|
|
18771
19299
|
/**
|
|
18772
19300
|
* Collapse a rulesync permissions config into Grok's single coarse mode.
|
|
18773
19301
|
* Any `deny`/`ask` rule anywhere keeps prompting (`ask`); otherwise an existing
|
|
@@ -18783,6 +19311,10 @@ function deriveGrokPermissionMode(config) {
|
|
|
18783
19311
|
}
|
|
18784
19312
|
//#endregion
|
|
18785
19313
|
//#region src/features/permissions/hermesagent-permissions.ts
|
|
19314
|
+
/** Collect the glob patterns in a canonical category that carry a given action. */
|
|
19315
|
+
function patternsByAction(category, action) {
|
|
19316
|
+
return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
|
|
19317
|
+
}
|
|
18786
19318
|
var HermesagentPermissions = class HermesagentPermissions extends ToolPermissions {
|
|
18787
19319
|
static getSettablePaths() {
|
|
18788
19320
|
return {
|
|
@@ -18806,7 +19338,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18806
19338
|
return true;
|
|
18807
19339
|
}
|
|
18808
19340
|
setFileContent(fileContent) {
|
|
18809
|
-
|
|
19341
|
+
const existing = parseHermesConfig(fileContent);
|
|
19342
|
+
const generated = parseHermesConfig(this.fileContent);
|
|
19343
|
+
const merged = deepMergeHermesConfig(existing, generated);
|
|
19344
|
+
if (generated.permissions !== void 0) merged.permissions = generated.permissions;
|
|
19345
|
+
this.fileContent = stringifyHermesConfig(merged);
|
|
18810
19346
|
}
|
|
18811
19347
|
toRulesyncPermissions() {
|
|
18812
19348
|
const config = parseHermesConfig(this.getFileContent());
|
|
@@ -18819,12 +19355,22 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18819
19355
|
}
|
|
18820
19356
|
static fromRulesyncPermissions({ outputRoot, rulesyncPermissions }) {
|
|
18821
19357
|
const permissions = rulesyncPermissions.getJson();
|
|
19358
|
+
const permissionBlock = permissions.permission ?? {};
|
|
19359
|
+
const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
|
|
19360
|
+
const bashDeny = patternsByAction(permissionBlock.bash, "deny");
|
|
19361
|
+
const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
|
|
19362
|
+
let config = {};
|
|
19363
|
+
if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
|
|
19364
|
+
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
19365
|
+
if (webfetchDeny.length > 0) config.security = { website_blocklist: {
|
|
19366
|
+
enabled: true,
|
|
19367
|
+
domains: webfetchDeny
|
|
19368
|
+
} };
|
|
19369
|
+
if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
|
|
19370
|
+
config.permissions = { rulesync: permissions };
|
|
18822
19371
|
return new HermesagentPermissions({
|
|
18823
19372
|
outputRoot,
|
|
18824
|
-
fileContent: stringifyHermesConfig(
|
|
18825
|
-
command_allowlist: Object.entries(permissions.permission ?? {}).flatMap(([, patterns]) => Object.entries(patterns).filter(([, action]) => action === "allow").map(([command]) => command)),
|
|
18826
|
-
permissions: { rulesync: permissions }
|
|
18827
|
-
})
|
|
19373
|
+
fileContent: stringifyHermesConfig(config)
|
|
18828
19374
|
});
|
|
18829
19375
|
}
|
|
18830
19376
|
};
|
|
@@ -18845,14 +19391,18 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18845
19391
|
* "executables": [ { "prefix": "git ", "action": "allow" } ],
|
|
18846
19392
|
* "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
|
|
18847
19393
|
* "mcpTools": [ { "prefix": "search", "action": "allow" } ],
|
|
18848
|
-
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "
|
|
19394
|
+
* "readOutsideProject":[ { "pattern": "/etc/**", "action": "ask" } ]
|
|
18849
19395
|
* }
|
|
18850
19396
|
* }
|
|
18851
19397
|
* ```
|
|
18852
19398
|
*
|
|
18853
19399
|
* Each rule carries a literal `prefix` (matches commands that start with it) or
|
|
18854
|
-
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action
|
|
18855
|
-
*
|
|
19400
|
+
* a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`. Junie
|
|
19401
|
+
* documents only `allow` and `ask` as valid actions — there is **no `deny`**
|
|
19402
|
+
* (https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html). rulesync's
|
|
19403
|
+
* canonical `allow`/`ask` map 1:1; a canonical `deny` has no Junie equivalent
|
|
19404
|
+
* and is mapped to the nearest valid action, `ask` (still withholds
|
|
19405
|
+
* auto-approval), with a warning so the downgrade is surfaced.
|
|
18856
19406
|
*
|
|
18857
19407
|
* Category mapping (rulesync canonical <-> Junie rule group):
|
|
18858
19408
|
* - `bash` <-> `executables`
|
|
@@ -18862,8 +19412,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
18862
19412
|
*
|
|
18863
19413
|
* Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
|
|
18864
19414
|
* (with a warning when they carry rules). The top-level `defaultBehavior` and
|
|
18865
|
-
* `allowReadonlyCommands` settings have no canonical
|
|
18866
|
-
*
|
|
19415
|
+
* `allowReadonlyCommands` settings have no canonical per-glob slot: they are
|
|
19416
|
+
* authored and round-tripped through the `junie` override namespace (see
|
|
19417
|
+
* `JuniePermissionsOverrideSchema`) — lifted into the override on import and
|
|
19418
|
+
* merged back onto the top level on export — and any other unmodeled top-level
|
|
19419
|
+
* key is preserved verbatim.
|
|
18867
19420
|
*
|
|
18868
19421
|
* @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
|
|
18869
19422
|
*/
|
|
@@ -18886,7 +19439,6 @@ const JUNIE_GROUP_TO_CANONICAL = {
|
|
|
18886
19439
|
mcpTools: "mcp",
|
|
18887
19440
|
readOutsideProject: "read"
|
|
18888
19441
|
};
|
|
18889
|
-
const JUNIE_DEFAULT_BEHAVIOR = "ask";
|
|
18890
19442
|
function isPermissionAction$1(value) {
|
|
18891
19443
|
return PermissionActionSchema.safeParse(value).success;
|
|
18892
19444
|
}
|
|
@@ -18936,13 +19488,16 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
18936
19488
|
} catch (error) {
|
|
18937
19489
|
throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
18938
19490
|
}
|
|
19491
|
+
const config = rulesyncPermissions.getJson();
|
|
18939
19492
|
const rules = convertRulesyncToJunieRules({
|
|
18940
|
-
config
|
|
19493
|
+
config,
|
|
18941
19494
|
logger
|
|
18942
19495
|
});
|
|
19496
|
+
const override = config.junie;
|
|
19497
|
+
const overrideObj = override !== void 0 && typeof override === "object" ? override : {};
|
|
18943
19498
|
const merged = {
|
|
18944
19499
|
...existing,
|
|
18945
|
-
|
|
19500
|
+
...overrideObj,
|
|
18946
19501
|
rules
|
|
18947
19502
|
};
|
|
18948
19503
|
return new JuniePermissions({
|
|
@@ -18962,7 +19517,12 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
|
|
|
18962
19517
|
throw new Error(`Failed to parse Junie permissions content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
18963
19518
|
}
|
|
18964
19519
|
const config = convertJunieToRulesyncPermissions({ allowlist });
|
|
18965
|
-
|
|
19520
|
+
const junieOverride = {};
|
|
19521
|
+
if (typeof allowlist.allowReadonlyCommands === "boolean") junieOverride.allowReadonlyCommands = allowlist.allowReadonlyCommands;
|
|
19522
|
+
if (typeof allowlist.defaultBehavior === "string") junieOverride.defaultBehavior = allowlist.defaultBehavior;
|
|
19523
|
+
const result = { ...config };
|
|
19524
|
+
if (Object.keys(junieOverride).length > 0) result.junie = junieOverride;
|
|
19525
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
18966
19526
|
}
|
|
18967
19527
|
validate() {
|
|
18968
19528
|
return {
|
|
@@ -18994,12 +19554,13 @@ function convertRulesyncToJunieRules({ config, logger }) {
|
|
|
18994
19554
|
continue;
|
|
18995
19555
|
}
|
|
18996
19556
|
for (const [pattern, action] of Object.entries(patterns)) {
|
|
19557
|
+
const junieAction = toJunieAction(action, category, pattern, logger);
|
|
18997
19558
|
const rule = isGlobPattern(pattern) ? {
|
|
18998
19559
|
pattern,
|
|
18999
|
-
action
|
|
19560
|
+
action: junieAction
|
|
19000
19561
|
} : {
|
|
19001
19562
|
prefix: pattern,
|
|
19002
|
-
action
|
|
19563
|
+
action: junieAction
|
|
19003
19564
|
};
|
|
19004
19565
|
(rules[group] ??= []).push(rule);
|
|
19005
19566
|
}
|
|
@@ -19007,6 +19568,19 @@ function convertRulesyncToJunieRules({ config, logger }) {
|
|
|
19007
19568
|
return rules;
|
|
19008
19569
|
}
|
|
19009
19570
|
/**
|
|
19571
|
+
* Map a canonical action onto a valid Junie allowlist action. Junie supports
|
|
19572
|
+
* only `allow` and `ask`; a canonical `deny` is downgraded to `ask` (the
|
|
19573
|
+
* nearest valid action — both withhold auto-approval) with a warning, so
|
|
19574
|
+
* rulesync never emits a `deny` that Junie would silently ignore.
|
|
19575
|
+
*/
|
|
19576
|
+
function toJunieAction(action, category, pattern, logger) {
|
|
19577
|
+
if (action === "deny") {
|
|
19578
|
+
logger?.warn(`Junie's allowlist supports only 'allow'/'ask' actions; the '${category}' deny rule for '${pattern}' was downgraded to 'ask' (Junie has no 'deny').`);
|
|
19579
|
+
return "ask";
|
|
19580
|
+
}
|
|
19581
|
+
return action;
|
|
19582
|
+
}
|
|
19583
|
+
/**
|
|
19010
19584
|
* Convert a Junie allowlist back into rulesync permissions config. The
|
|
19011
19585
|
* top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
|
|
19012
19586
|
* canonical equivalent and are not imported.
|
|
@@ -19040,6 +19614,31 @@ const KiloPermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
|
19040
19614
|
]))]);
|
|
19041
19615
|
const KiloPermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.record(zod_mini.z.string(), KiloPermissionSchema)) });
|
|
19042
19616
|
/**
|
|
19617
|
+
* Kilo permission keys that share a name with a canonical rulesync category and
|
|
19618
|
+
* therefore stay in the shared `permission` block. Everything else (Kilo-only
|
|
19619
|
+
* keys such as `external_directory`, `doom_loop`, `notebook_edit`, ...) is
|
|
19620
|
+
* routed into the `kilo` override on import so it does not leak into other
|
|
19621
|
+
* tools' configs. Kilo folds `write` into `edit`, uses `notebook_edit`/`task`
|
|
19622
|
+
* rather than the canonical `notebookedit`/`agent`, and has no `mcp` key (MCP is
|
|
19623
|
+
* addressed via `mcp__*` tool-name keys), so those canonical names are not Kilo
|
|
19624
|
+
* keys in practice — they simply never appear on the shared side.
|
|
19625
|
+
*/
|
|
19626
|
+
const KILO_SHARED_CATEGORIES = /* @__PURE__ */ new Set([
|
|
19627
|
+
"bash",
|
|
19628
|
+
"read",
|
|
19629
|
+
"edit",
|
|
19630
|
+
"write",
|
|
19631
|
+
"webfetch",
|
|
19632
|
+
"websearch",
|
|
19633
|
+
"grep",
|
|
19634
|
+
"glob",
|
|
19635
|
+
"notebookedit",
|
|
19636
|
+
"agent"
|
|
19637
|
+
]);
|
|
19638
|
+
function isSharedKiloCategory(key) {
|
|
19639
|
+
return key === "*" || KILO_SHARED_CATEGORIES.has(key) || key.startsWith("mcp__");
|
|
19640
|
+
}
|
|
19641
|
+
/**
|
|
19043
19642
|
* Parse a JSONC string and throw on syntax errors. The `jsonc-parser` `parse()` function is
|
|
19044
19643
|
* non-throwing best-effort: invalid input silently yields a partial value (often `undefined`,
|
|
19045
19644
|
* coerced to `{}` by callers). That behavior would silently drop a user's existing `deny` rules
|
|
@@ -19151,12 +19750,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19151
19750
|
const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
|
|
19152
19751
|
const parsedPermission = parsed.permission;
|
|
19153
19752
|
const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
|
|
19154
|
-
const
|
|
19753
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
19754
|
+
const kiloOverride = rulesyncJson.kilo;
|
|
19755
|
+
const incomingPermission = {
|
|
19756
|
+
...rulesyncJson.permission,
|
|
19757
|
+
...kiloOverride?.permission
|
|
19758
|
+
};
|
|
19155
19759
|
const droppedDenyByKey = {};
|
|
19156
|
-
for (const key of Object.
|
|
19157
|
-
const
|
|
19158
|
-
const
|
|
19159
|
-
const nextDenyPatterns = new Set(collectKiloDenyPatterns(rulesyncPermission[key]));
|
|
19760
|
+
for (const [key, value] of Object.entries(incomingPermission)) {
|
|
19761
|
+
const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
|
|
19762
|
+
const nextDenyPatterns = new Set(collectKiloDenyPatterns(value));
|
|
19160
19763
|
const dropped = previousDenyPatterns.filter((p) => !nextDenyPatterns.has(p));
|
|
19161
19764
|
if (dropped.length > 0) droppedDenyByKey[key] = dropped;
|
|
19162
19765
|
}
|
|
@@ -19164,8 +19767,10 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19164
19767
|
const summary = Object.entries(droppedDenyByKey).map(([key, patterns]) => `${key}: [${patterns.join(", ")}]`).join("; ");
|
|
19165
19768
|
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'.`);
|
|
19166
19769
|
}
|
|
19167
|
-
const mergedPermission = {
|
|
19168
|
-
|
|
19770
|
+
const mergedPermission = {
|
|
19771
|
+
...existingPermission,
|
|
19772
|
+
...incomingPermission
|
|
19773
|
+
};
|
|
19169
19774
|
const nextJson = {
|
|
19170
19775
|
...parsed,
|
|
19171
19776
|
permission: mergedPermission
|
|
@@ -19179,8 +19784,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19179
19784
|
});
|
|
19180
19785
|
}
|
|
19181
19786
|
toRulesyncPermissions() {
|
|
19182
|
-
const
|
|
19183
|
-
|
|
19787
|
+
const rawPermission = this.json.permission ?? {};
|
|
19788
|
+
const shared = {};
|
|
19789
|
+
const overrideOnly = {};
|
|
19790
|
+
for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
|
|
19791
|
+
else overrideOnly[key] = value;
|
|
19792
|
+
const json = Object.keys(overrideOnly).length > 0 ? {
|
|
19793
|
+
permission: shared,
|
|
19794
|
+
kilo: { permission: overrideOnly }
|
|
19795
|
+
} : { permission: shared };
|
|
19796
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
19184
19797
|
}
|
|
19185
19798
|
validate() {
|
|
19186
19799
|
try {
|
|
@@ -19210,10 +19823,6 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
|
|
|
19210
19823
|
validate: false
|
|
19211
19824
|
});
|
|
19212
19825
|
}
|
|
19213
|
-
normalizePermission(permission) {
|
|
19214
|
-
if (!permission) return {};
|
|
19215
|
-
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
19216
|
-
}
|
|
19217
19826
|
};
|
|
19218
19827
|
//#endregion
|
|
19219
19828
|
//#region src/features/permissions/kiro-permissions.ts
|
|
@@ -19271,29 +19880,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
|
|
|
19271
19880
|
}
|
|
19272
19881
|
const permission = {};
|
|
19273
19882
|
const toolsSettings = parsed.toolsSettings ?? {};
|
|
19274
|
-
const
|
|
19275
|
-
|
|
19276
|
-
const
|
|
19277
|
-
|
|
19278
|
-
|
|
19279
|
-
|
|
19280
|
-
|
|
19281
|
-
|
|
19282
|
-
|
|
19283
|
-
|
|
19284
|
-
const readDeny = asStringArray(readSettings.deniedPaths);
|
|
19285
|
-
if (readAllow.length > 0 || readDeny.length > 0) {
|
|
19286
|
-
permission.read = {};
|
|
19287
|
-
for (const pattern of readAllow) permission.read[pattern] = "allow";
|
|
19288
|
-
for (const pattern of readDeny) permission.read[pattern] = "deny";
|
|
19289
|
-
}
|
|
19290
|
-
const writeSettings = asRecord$1(toolsSettings.write);
|
|
19291
|
-
const writeAllow = asStringArray(writeSettings.allowedPaths);
|
|
19292
|
-
const writeDeny = asStringArray(writeSettings.deniedPaths);
|
|
19293
|
-
if (writeAllow.length > 0 || writeDeny.length > 0) {
|
|
19294
|
-
permission.write = {};
|
|
19295
|
-
for (const pattern of writeAllow) permission.write[pattern] = "allow";
|
|
19296
|
-
for (const pattern of writeDeny) permission.write[pattern] = "deny";
|
|
19883
|
+
const shellRules = rulesFromArrays(asRecord$1(toolsSettings.shell), "allowedCommands", "deniedCommands");
|
|
19884
|
+
if (Object.keys(shellRules).length > 0) permission.bash = shellRules;
|
|
19885
|
+
for (const category of [
|
|
19886
|
+
"read",
|
|
19887
|
+
"write",
|
|
19888
|
+
"grep",
|
|
19889
|
+
"glob"
|
|
19890
|
+
]) {
|
|
19891
|
+
const rules = rulesFromArrays(asRecord$1(toolsSettings[category]), "allowedPaths", "deniedPaths");
|
|
19892
|
+
if (Object.keys(rules).length > 0) permission[category] = rules;
|
|
19297
19893
|
}
|
|
19298
19894
|
const allowedTools = new Set(parsed.allowedTools ?? []);
|
|
19299
19895
|
if (allowedTools.has("web_fetch")) permission.webfetch = { "*": "allow" };
|
|
@@ -19319,45 +19915,63 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
|
|
|
19319
19915
|
function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
|
|
19320
19916
|
const nextAllowedTools = new Set(existing.allowedTools ?? []);
|
|
19321
19917
|
const nextToolsSettings = { ...asRecord$1(existing.toolsSettings) };
|
|
19918
|
+
const pathBuckets = {};
|
|
19919
|
+
const pushPath = (key, action, pattern) => {
|
|
19920
|
+
const bucket = pathBuckets[key] ??= {
|
|
19921
|
+
allow: [],
|
|
19922
|
+
deny: []
|
|
19923
|
+
};
|
|
19924
|
+
(action === "allow" ? bucket.allow : bucket.deny).push(pattern);
|
|
19925
|
+
};
|
|
19322
19926
|
const shell = {
|
|
19323
19927
|
allowedCommands: [],
|
|
19324
19928
|
deniedCommands: []
|
|
19325
19929
|
};
|
|
19326
|
-
const read = {
|
|
19327
|
-
allowedPaths: [],
|
|
19328
|
-
deniedPaths: []
|
|
19329
|
-
};
|
|
19330
|
-
const write = {
|
|
19331
|
-
allowedPaths: [],
|
|
19332
|
-
deniedPaths: []
|
|
19333
|
-
};
|
|
19334
19930
|
for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
|
|
19335
19931
|
if (action === "ask") {
|
|
19336
19932
|
logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
|
|
19337
19933
|
continue;
|
|
19338
19934
|
}
|
|
19339
19935
|
if (category === "bash") (action === "allow" ? shell.allowedCommands : shell.deniedCommands).push(pattern);
|
|
19340
|
-
else if (category === "read"
|
|
19341
|
-
else if (category === "edit" || category === "write") (
|
|
19342
|
-
else if (category === "webfetch" || category === "websearch") {
|
|
19343
|
-
|
|
19344
|
-
|
|
19345
|
-
|
|
19346
|
-
|
|
19347
|
-
|
|
19348
|
-
|
|
19349
|
-
|
|
19350
|
-
} else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
|
|
19936
|
+
else if (category === "read" || category === "grep" || category === "glob") pushPath(category, action, pattern);
|
|
19937
|
+
else if (category === "edit" || category === "write") pushPath("write", action, pattern);
|
|
19938
|
+
else if (category === "webfetch" || category === "websearch") applyKiroWebPermission({
|
|
19939
|
+
category,
|
|
19940
|
+
pattern,
|
|
19941
|
+
action,
|
|
19942
|
+
nextAllowedTools,
|
|
19943
|
+
logger
|
|
19944
|
+
});
|
|
19945
|
+
else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
|
|
19351
19946
|
}
|
|
19352
19947
|
nextToolsSettings.shell = shell;
|
|
19353
|
-
nextToolsSettings.read = read;
|
|
19354
|
-
nextToolsSettings.write = write;
|
|
19948
|
+
nextToolsSettings.read = pathTable(pathBuckets.read);
|
|
19949
|
+
nextToolsSettings.write = pathTable(pathBuckets.write);
|
|
19950
|
+
for (const key of ["grep", "glob"]) {
|
|
19951
|
+
const bucket = pathBuckets[key];
|
|
19952
|
+
if (bucket && (bucket.allow.length > 0 || bucket.deny.length > 0)) nextToolsSettings[key] = pathTable(bucket);
|
|
19953
|
+
}
|
|
19355
19954
|
return {
|
|
19356
19955
|
...existing,
|
|
19357
19956
|
allowedTools: [...nextAllowedTools].toSorted(),
|
|
19358
19957
|
toolsSettings: nextToolsSettings
|
|
19359
19958
|
};
|
|
19360
19959
|
}
|
|
19960
|
+
function pathTable(bucket) {
|
|
19961
|
+
return {
|
|
19962
|
+
allowedPaths: bucket?.allow ?? [],
|
|
19963
|
+
deniedPaths: bucket?.deny ?? []
|
|
19964
|
+
};
|
|
19965
|
+
}
|
|
19966
|
+
function applyKiroWebPermission({ category, pattern, action, nextAllowedTools, logger }) {
|
|
19967
|
+
if (pattern !== "*") {
|
|
19968
|
+
logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
|
|
19969
|
+
return;
|
|
19970
|
+
}
|
|
19971
|
+
const toolName = category === "webfetch" ? "web_fetch" : "web_search";
|
|
19972
|
+
if (action === "allow") nextAllowedTools.add(toolName);
|
|
19973
|
+
else nextAllowedTools.delete(toolName);
|
|
19974
|
+
}
|
|
19361
19975
|
function asRecord$1(value) {
|
|
19362
19976
|
const result = UnknownRecordSchema.safeParse(value);
|
|
19363
19977
|
return result.success ? result.data : {};
|
|
@@ -19365,6 +19979,17 @@ function asRecord$1(value) {
|
|
|
19365
19979
|
function asStringArray(value) {
|
|
19366
19980
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
19367
19981
|
}
|
|
19982
|
+
/**
|
|
19983
|
+
* Build a canonical `{ pattern: action }` map from a Kiro tool settings record's
|
|
19984
|
+
* allow/deny string arrays (e.g. `allowedPaths`/`deniedPaths` or
|
|
19985
|
+
* `allowedCommands`/`deniedCommands`).
|
|
19986
|
+
*/
|
|
19987
|
+
function rulesFromArrays(settings, allowKey, denyKey) {
|
|
19988
|
+
const rules = {};
|
|
19989
|
+
for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
|
|
19990
|
+
for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
|
|
19991
|
+
return rules;
|
|
19992
|
+
}
|
|
19368
19993
|
//#endregion
|
|
19369
19994
|
//#region src/features/permissions/opencode-permissions.ts
|
|
19370
19995
|
const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
@@ -19376,6 +20001,28 @@ const OpencodePermissionSchema = zod_mini.z.union([zod_mini.z.enum([
|
|
|
19376
20001
|
"ask",
|
|
19377
20002
|
"deny"
|
|
19378
20003
|
]))]);
|
|
20004
|
+
/**
|
|
20005
|
+
* Canonical rulesync permission categories that carry a cross-tool meaning (see
|
|
20006
|
+
* the "Supported tool categories" list in `docs/reference/file-formats.md`).
|
|
20007
|
+
* On import, any OpenCode category outside this set — plus MCP tool names — is
|
|
20008
|
+
* treated as OpenCode-only and routed into the `opencode` override block so a
|
|
20009
|
+
* subsequent `rulesync generate` does not leak it into other tools' configs.
|
|
20010
|
+
*/
|
|
20011
|
+
const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
|
|
20012
|
+
"bash",
|
|
20013
|
+
"read",
|
|
20014
|
+
"edit",
|
|
20015
|
+
"write",
|
|
20016
|
+
"webfetch",
|
|
20017
|
+
"websearch",
|
|
20018
|
+
"grep",
|
|
20019
|
+
"glob",
|
|
20020
|
+
"notebookedit",
|
|
20021
|
+
"agent"
|
|
20022
|
+
]);
|
|
20023
|
+
function isSharedPermissionCategory(category) {
|
|
20024
|
+
return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
|
|
20025
|
+
}
|
|
19379
20026
|
const OpencodePermissionsConfigSchema = zod_mini.z.looseObject({ permission: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.enum([
|
|
19380
20027
|
"allow",
|
|
19381
20028
|
"ask",
|
|
@@ -19437,9 +20084,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19437
20084
|
fileContent = await readFileContentOrNull(jsonPath);
|
|
19438
20085
|
if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
|
|
19439
20086
|
}
|
|
20087
|
+
const parsed = (0, jsonc_parser.parse)(fileContent ?? "{}");
|
|
20088
|
+
const rulesyncJson = rulesyncPermissions.getJson();
|
|
20089
|
+
const overridePermission = rulesyncJson.opencode?.permission ?? {};
|
|
19440
20090
|
const nextJson = {
|
|
19441
|
-
...
|
|
19442
|
-
permission:
|
|
20091
|
+
...parsed,
|
|
20092
|
+
permission: {
|
|
20093
|
+
...rulesyncJson.permission,
|
|
20094
|
+
...overridePermission
|
|
20095
|
+
}
|
|
19443
20096
|
};
|
|
19444
20097
|
return new OpencodePermissions({
|
|
19445
20098
|
outputRoot,
|
|
@@ -19450,8 +20103,20 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19450
20103
|
});
|
|
19451
20104
|
}
|
|
19452
20105
|
toRulesyncPermissions() {
|
|
19453
|
-
const
|
|
19454
|
-
|
|
20106
|
+
const rawPermission = this.json.permission;
|
|
20107
|
+
if (rawPermission === void 0 || typeof rawPermission === "string") {
|
|
20108
|
+
const permission = this.normalizePermission(rawPermission);
|
|
20109
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
20110
|
+
}
|
|
20111
|
+
const shared = {};
|
|
20112
|
+
const overrideOnly = {};
|
|
20113
|
+
for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
|
|
20114
|
+
else overrideOnly[category] = value;
|
|
20115
|
+
const json = Object.keys(overrideOnly).length > 0 ? {
|
|
20116
|
+
permission: shared,
|
|
20117
|
+
opencode: { permission: overrideOnly }
|
|
20118
|
+
} : { permission: shared };
|
|
20119
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
19455
20120
|
}
|
|
19456
20121
|
validate() {
|
|
19457
20122
|
try {
|
|
@@ -19481,10 +20146,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
|
|
|
19481
20146
|
validate: false
|
|
19482
20147
|
});
|
|
19483
20148
|
}
|
|
20149
|
+
/**
|
|
20150
|
+
* Normalize the uniform/undefined forms of OpenCode's `permission` field into
|
|
20151
|
+
* the canonical rulesync shape. The object form is handled directly in
|
|
20152
|
+
* `toRulesyncPermissions` (it needs to split shared vs OpenCode-only
|
|
20153
|
+
* categories), so this only covers the two remaining cases.
|
|
20154
|
+
*/
|
|
19484
20155
|
normalizePermission(permission) {
|
|
19485
20156
|
if (!permission) return {};
|
|
19486
|
-
|
|
19487
|
-
return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
|
|
20157
|
+
return { "*": { "*": permission } };
|
|
19488
20158
|
}
|
|
19489
20159
|
};
|
|
19490
20160
|
//#endregion
|
|
@@ -19558,6 +20228,24 @@ function buildQwenPermissionEntry(toolName, pattern) {
|
|
|
19558
20228
|
if (pattern === "*") return toolName;
|
|
19559
20229
|
return `${toolName}(${pattern})`;
|
|
19560
20230
|
}
|
|
20231
|
+
const QWEN_OVERRIDE_TOOLS_KEYS = [
|
|
20232
|
+
"approvalMode",
|
|
20233
|
+
"autoAccept",
|
|
20234
|
+
"sandbox",
|
|
20235
|
+
"sandboxImage",
|
|
20236
|
+
"disabled"
|
|
20237
|
+
];
|
|
20238
|
+
const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
|
|
20239
|
+
function asPlainRecord(value) {
|
|
20240
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20241
|
+
}
|
|
20242
|
+
/** Pick the override-managed keys out of a settings group into a fresh record. */
|
|
20243
|
+
function pickQwenOverrideKeys(group, keys) {
|
|
20244
|
+
const source = asPlainRecord(group);
|
|
20245
|
+
const picked = {};
|
|
20246
|
+
for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
|
|
20247
|
+
return picked;
|
|
20248
|
+
}
|
|
19561
20249
|
var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
19562
20250
|
constructor(params) {
|
|
19563
20251
|
super({
|
|
@@ -19623,6 +20311,15 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
19623
20311
|
...settings,
|
|
19624
20312
|
permissions: mergedPermissions
|
|
19625
20313
|
};
|
|
20314
|
+
const override = config.qwencode;
|
|
20315
|
+
if (override?.tools !== void 0) merged.tools = {
|
|
20316
|
+
...asPlainRecord(settings.tools),
|
|
20317
|
+
...asPlainRecord(override.tools)
|
|
20318
|
+
};
|
|
20319
|
+
if (override?.security !== void 0) merged.security = {
|
|
20320
|
+
...asPlainRecord(settings.security),
|
|
20321
|
+
...asPlainRecord(override.security)
|
|
20322
|
+
};
|
|
19626
20323
|
const fileContent = JSON.stringify(merged, null, 2);
|
|
19627
20324
|
return new QwencodePermissions({
|
|
19628
20325
|
outputRoot,
|
|
@@ -19648,7 +20345,14 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
|
|
|
19648
20345
|
ask: permissions.ask ?? [],
|
|
19649
20346
|
deny: permissions.deny ?? []
|
|
19650
20347
|
});
|
|
19651
|
-
|
|
20348
|
+
const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
|
|
20349
|
+
const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
|
|
20350
|
+
const qwencodeOverride = {};
|
|
20351
|
+
if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
|
|
20352
|
+
if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
|
|
20353
|
+
const result = { ...config };
|
|
20354
|
+
if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
|
|
20355
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
19652
20356
|
}
|
|
19653
20357
|
validate() {
|
|
19654
20358
|
try {
|
|
@@ -19809,6 +20513,16 @@ function toPermissionsTable(value) {
|
|
|
19809
20513
|
if (!value || typeof value !== "object" || Array.isArray(value)) return {};
|
|
19810
20514
|
return { ...value };
|
|
19811
20515
|
}
|
|
20516
|
+
const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_allowed_tools", "plan_mode_read_only_commands"];
|
|
20517
|
+
function asReasonixRecord(value) {
|
|
20518
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
20519
|
+
}
|
|
20520
|
+
function pickReasonixKeys(source, keys) {
|
|
20521
|
+
const record = asReasonixRecord(source);
|
|
20522
|
+
const picked = {};
|
|
20523
|
+
for (const key of keys) if (record[key] !== void 0) picked[key] = record[key];
|
|
20524
|
+
return picked;
|
|
20525
|
+
}
|
|
19812
20526
|
var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
19813
20527
|
toml;
|
|
19814
20528
|
constructor(params) {
|
|
@@ -19870,6 +20584,15 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
19870
20584
|
...parsed,
|
|
19871
20585
|
permissions: mergedPermissions
|
|
19872
20586
|
};
|
|
20587
|
+
const override = config.reasonix;
|
|
20588
|
+
if (override?.sandbox !== void 0) merged.sandbox = {
|
|
20589
|
+
...asReasonixRecord(parsed.sandbox),
|
|
20590
|
+
...asReasonixRecord(override.sandbox)
|
|
20591
|
+
};
|
|
20592
|
+
if (override?.agent !== void 0) merged.agent = {
|
|
20593
|
+
...asReasonixRecord(parsed.agent),
|
|
20594
|
+
...asReasonixRecord(override.agent)
|
|
20595
|
+
};
|
|
19873
20596
|
const fileContent = smol_toml.stringify(merged);
|
|
19874
20597
|
return new ReasonixPermissions({
|
|
19875
20598
|
outputRoot,
|
|
@@ -19886,7 +20609,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
19886
20609
|
ask: toStringArray$1(permissions.ask),
|
|
19887
20610
|
deny: toStringArray$1(permissions.deny)
|
|
19888
20611
|
});
|
|
19889
|
-
|
|
20612
|
+
const sandbox = asReasonixRecord(this.toml.sandbox);
|
|
20613
|
+
const agentPlanMode = pickReasonixKeys(this.toml.agent, REASONIX_OVERRIDE_AGENT_KEYS);
|
|
20614
|
+
const reasonixOverride = {};
|
|
20615
|
+
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
20616
|
+
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
20617
|
+
const result = { ...config };
|
|
20618
|
+
if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
|
|
20619
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
19890
20620
|
}
|
|
19891
20621
|
validate() {
|
|
19892
20622
|
try {
|
|
@@ -20450,6 +21180,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20450
21180
|
if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
|
|
20451
21181
|
tools[vibeToolName] = nextTool;
|
|
20452
21182
|
}
|
|
21183
|
+
applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
|
|
20453
21184
|
config.tools = tools;
|
|
20454
21185
|
if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
|
|
20455
21186
|
else delete config.enabled_tools;
|
|
@@ -20466,16 +21197,25 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20466
21197
|
}
|
|
20467
21198
|
toRulesyncPermissions() {
|
|
20468
21199
|
const permission = {};
|
|
21200
|
+
const vibeOverridePermission = {};
|
|
20469
21201
|
for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
|
|
20470
21202
|
for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
|
|
20471
21203
|
for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
|
|
20472
|
-
const
|
|
21204
|
+
const category = toCanonicalToolName$1(vibeToolName);
|
|
21205
|
+
const rules = ensurePermission(permission, category);
|
|
20473
21206
|
const action = fromVibePermission(toolConfig.permission);
|
|
20474
21207
|
if (action !== void 0) rules["*"] = action;
|
|
20475
21208
|
for (const pattern of toStringArray(toolConfig.allow ?? toolConfig.allowlist)) rules[pattern] = "allow";
|
|
20476
21209
|
for (const pattern of toStringArray(toolConfig.deny ?? toolConfig.denylist)) rules[pattern] = "deny";
|
|
21210
|
+
const sensitivePatterns = toStringArray(toolConfig.sensitive_patterns);
|
|
21211
|
+
if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
|
|
20477
21212
|
}
|
|
20478
|
-
|
|
21213
|
+
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
21214
|
+
const json = Object.keys(vibeOverridePermission).length > 0 ? {
|
|
21215
|
+
permission,
|
|
21216
|
+
vibe: { permission: vibeOverridePermission }
|
|
21217
|
+
} : { permission };
|
|
21218
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
|
|
20479
21219
|
}
|
|
20480
21220
|
validate() {
|
|
20481
21221
|
try {
|
|
@@ -20502,6 +21242,22 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
|
|
|
20502
21242
|
});
|
|
20503
21243
|
}
|
|
20504
21244
|
};
|
|
21245
|
+
/**
|
|
21246
|
+
* Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
|
|
21247
|
+
* escalate to ASK even when the base permission is ALWAYS). rulesync owns this
|
|
21248
|
+
* list for any category the override names: a present list is set, an empty
|
|
21249
|
+
* one clears it. Categories not named keep whatever the existing file had.
|
|
21250
|
+
*/
|
|
21251
|
+
function applyVibeSensitivePatterns(tools, vibeOverride) {
|
|
21252
|
+
for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
|
|
21253
|
+
const vibeToolName = toVibeToolName(category);
|
|
21254
|
+
const nextTool = toVibeToolConfig(tools[vibeToolName]);
|
|
21255
|
+
const patterns = toStringArray(toolOverride.sensitive_patterns);
|
|
21256
|
+
if (patterns.length > 0) nextTool.sensitive_patterns = [...patterns].toSorted();
|
|
21257
|
+
else delete nextTool.sensitive_patterns;
|
|
21258
|
+
tools[vibeToolName] = nextTool;
|
|
21259
|
+
}
|
|
21260
|
+
}
|
|
20505
21261
|
function parseVibeConfig(fileContent) {
|
|
20506
21262
|
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
20507
21263
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -20546,6 +21302,11 @@ function ensurePermission(permission, category) {
|
|
|
20546
21302
|
const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
|
|
20547
21303
|
const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
|
|
20548
21304
|
const DENYLIST_KEY = "agent_mode_command_execution_denylist";
|
|
21305
|
+
const WARP_OVERRIDE_KEYS = [
|
|
21306
|
+
"agent_mode_coding_permissions",
|
|
21307
|
+
"agent_mode_coding_file_read_allowlist",
|
|
21308
|
+
"agent_mode_execute_readonly_commands"
|
|
21309
|
+
];
|
|
20549
21310
|
/**
|
|
20550
21311
|
* Warp's `settings.toml` lives in a different directory per platform (Stable
|
|
20551
21312
|
* channel). The home directory is resolved by the processor through
|
|
@@ -20581,8 +21342,16 @@ function warpSettingsDir() {
|
|
|
20581
21342
|
* patterns as regexes when targeting Warp (mirrors the Zed permissions
|
|
20582
21343
|
* adapter). Warp has no per-command "ask" list, so `ask` rules are dropped; and
|
|
20583
21344
|
* the command lists only model shell commands, so non-`bash` categories are
|
|
20584
|
-
* skipped (with a warning when they carry `deny` rules).
|
|
20585
|
-
*
|
|
21345
|
+
* skipped (with a warning when they carry `deny` rules).
|
|
21346
|
+
*
|
|
21347
|
+
* Warp's `[agents.profiles]` table also exposes file-read/read-only autonomy
|
|
21348
|
+
* knobs that do not fit the canonical `allow | ask | deny` per-command model:
|
|
21349
|
+
* `agent_mode_coding_permissions`, `agent_mode_coding_file_read_allowlist`, and
|
|
21350
|
+
* `agent_mode_execute_readonly_commands`. These are authored and round-tripped
|
|
21351
|
+
* through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
|
|
21352
|
+
* on **import** they are lifted from `settings.toml` into the override, and on
|
|
21353
|
+
* **export** they are merged back into `[agents.profiles]` (the override wins).
|
|
21354
|
+
* MCP allow/deny is a separate surface not modeled here.
|
|
20586
21355
|
*
|
|
20587
21356
|
* The `settings.toml` file holds all of Warp's settings, so the
|
|
20588
21357
|
* `[agents.profiles]` block is merged in place and the file is never deleted.
|
|
@@ -20627,12 +21396,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
20627
21396
|
} catch (error) {
|
|
20628
21397
|
throw new Error(`Failed to parse existing Warp settings at ${filePath}: ${formatError(error)}`, { cause: error });
|
|
20629
21398
|
}
|
|
21399
|
+
const config = rulesyncPermissions.getJson();
|
|
20630
21400
|
const { allow, deny } = convertRulesyncToWarpPermissions({
|
|
20631
|
-
config
|
|
21401
|
+
config,
|
|
20632
21402
|
logger
|
|
20633
21403
|
});
|
|
20634
21404
|
const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
|
|
20635
21405
|
const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
|
|
21406
|
+
const override = config.warp;
|
|
21407
|
+
if (isRecord(override)) Object.assign(profiles, override);
|
|
20636
21408
|
const mergedAllow = (0, es_toolkit.uniq)(allow.toSorted());
|
|
20637
21409
|
const mergedDeny = (0, es_toolkit.uniq)(deny.toSorted());
|
|
20638
21410
|
if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
|
|
@@ -20663,7 +21435,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
|
|
|
20663
21435
|
allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
|
|
20664
21436
|
deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
|
|
20665
21437
|
});
|
|
20666
|
-
|
|
21438
|
+
const warpOverride = {};
|
|
21439
|
+
for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
|
|
21440
|
+
const result = { ...config };
|
|
21441
|
+
if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
|
|
21442
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
20667
21443
|
}
|
|
20668
21444
|
validate() {
|
|
20669
21445
|
return {
|
|
@@ -24029,8 +24805,9 @@ const DevinSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
24029
24805
|
* Represents a Devin (now Devin Desktop) skill directory.
|
|
24030
24806
|
* Devin supports skills in both project mode under .devin/skills/
|
|
24031
24807
|
* (preferred since the Devin Desktop rebrand; .devin/skills/ is the legacy
|
|
24032
|
-
* fallback the tool still reads) and global mode under
|
|
24033
|
-
* (
|
|
24808
|
+
* fallback the tool still reads) and global mode under the Devin-native
|
|
24809
|
+
* ~/.config/devin/skills/ (consistent with Devin's global agents/rules paths;
|
|
24810
|
+
* the legacy channel-dependent ~/.codeium/<channel>/skills/ is no longer emitted).
|
|
24034
24811
|
*/
|
|
24035
24812
|
var DevinSkill = class DevinSkill extends ToolSkill {
|
|
24036
24813
|
constructor({ outputRoot = process.cwd(), relativeDirPath = DevinSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
|
|
@@ -24052,7 +24829,7 @@ var DevinSkill = class DevinSkill extends ToolSkill {
|
|
|
24052
24829
|
}
|
|
24053
24830
|
}
|
|
24054
24831
|
static getSettablePaths({ global = false } = {}) {
|
|
24055
|
-
if (global) return { relativeDirPath:
|
|
24832
|
+
if (global) return { relativeDirPath: DEVIN_GLOBAL_SKILLS_DIR_PATH };
|
|
24056
24833
|
return { relativeDirPath: DEVIN_SKILLS_DIR_PATH };
|
|
24057
24834
|
}
|
|
24058
24835
|
getFrontmatter() {
|
|
@@ -27111,7 +27888,7 @@ const QwencodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
27111
27888
|
disallowedTools: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
27112
27889
|
maxTurns: zod_mini.z.optional(zod_mini.z.number()),
|
|
27113
27890
|
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
27114
|
-
mcpServers: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
27891
|
+
mcpServers: zod_mini.z.optional(zod_mini.z.union([zod_mini.z.array(zod_mini.z.string()), zod_mini.z.record(zod_mini.z.string(), zod_mini.z.looseObject({}))])),
|
|
27115
27892
|
hooks: zod_mini.z.optional(zod_mini.z.unknown())
|
|
27116
27893
|
});
|
|
27117
27894
|
var QwencodeSubagent = class QwencodeSubagent extends ToolSubagent {
|