rulesync 9.2.0 → 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.
@@ -3003,8 +3003,8 @@ const DEVIN_SKILLS_DIR_PATH = join(DEVIN_DIR, "skills");
3003
3003
  const DEVIN_AGENTS_DIR_PATH = join(DEVIN_DIR, "agents");
3004
3004
  const DEVIN_GLOBAL_CONFIG_DIR_PATH = join(".config", "devin");
3005
3005
  const DEVIN_GLOBAL_AGENTS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "agents");
3006
+ const DEVIN_GLOBAL_SKILLS_DIR_PATH = join(DEVIN_GLOBAL_CONFIG_DIR_PATH, "skills");
3006
3007
  const CODEIUM_WINDSURF_GLOBAL_WORKFLOWS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "global_workflows");
3007
- const CODEIUM_WINDSURF_SKILLS_DIR_PATH = join(CODEIUM_WINDSURF_DIR, "skills");
3008
3008
  const DEVIN_CONFIG_FILE_NAME = "config.json";
3009
3009
  const DEVIN_HOOKS_V1_FILE_NAME = "hooks.v1.json";
3010
3010
  const DEVIN_GLOBAL_AGENTS_FILE_NAME = "AGENTS.md";
@@ -5688,6 +5688,8 @@ const DEEPAGENTS_HOOK_EVENTS = [
5688
5688
  "sessionEnd",
5689
5689
  "beforeSubmitPrompt",
5690
5690
  "permissionRequest",
5691
+ "preToolUse",
5692
+ "postToolUse",
5691
5693
  "postToolUseFailure",
5692
5694
  "stop",
5693
5695
  "preCompact",
@@ -5713,7 +5715,13 @@ const CODEXCLI_HOOK_EVENTS = [
5713
5715
  * Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
5714
5716
  * maps PascalCase event names to matcher/handler arrays. Every Goose event has a
5715
5717
  * 1:1 canonical equivalent, so no new canonical events are required.
5716
- * @see https://goose-docs.ai/blog/2026/05/14/goose-hooks/
5718
+ *
5719
+ * Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
5720
+ * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
5721
+ * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
5722
+ * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
5723
+ * @see https://github.com/block/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
5724
+ * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
5717
5725
  */
5718
5726
  const GOOSE_HOOK_EVENTS = [
5719
5727
  "sessionStart",
@@ -5726,9 +5734,7 @@ const GOOSE_HOOK_EVENTS = [
5726
5734
  "beforeReadFile",
5727
5735
  "afterFileEdit",
5728
5736
  "beforeShellExecution",
5729
- "afterShellExecution",
5730
- "subagentStart",
5731
- "subagentStop"
5737
+ "afterShellExecution"
5732
5738
  ];
5733
5739
  /** Hook events supported by Kiro CLI. */
5734
5740
  const KIRO_HOOK_EVENTS = [
@@ -6181,9 +6187,7 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
6181
6187
  beforeReadFile: "BeforeReadFile",
6182
6188
  afterFileEdit: "AfterFileEdit",
6183
6189
  beforeShellExecution: "BeforeShellExecution",
6184
- afterShellExecution: "AfterShellExecution",
6185
- subagentStart: "SubagentStart",
6186
- subagentStop: "SubagentStop"
6190
+ afterShellExecution: "AfterShellExecution"
6187
6191
  };
6188
6192
  /**
6189
6193
  * Map Goose PascalCase event names to canonical camelCase.
@@ -6197,6 +6201,8 @@ const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES = {
6197
6201
  sessionEnd: "session.end",
6198
6202
  beforeSubmitPrompt: "user.prompt",
6199
6203
  permissionRequest: "permission.request",
6204
+ preToolUse: "tool.use",
6205
+ postToolUse: "tool.result",
6200
6206
  postToolUseFailure: "tool.error",
6201
6207
  stop: "task.complete",
6202
6208
  preCompact: "context.compact",
@@ -8191,7 +8197,7 @@ const GOOSE_CONVERTER_CONFIG = {
8191
8197
  *
8192
8198
  * The JSON shape matches Claude Code's: each PascalCase event maps to an array of
8193
8199
  * `{ matcher, hooks: [{ type: "command", command }] }` entries.
8194
- * @see https://goose-docs.ai/blog/2026/05/14/goose-hooks/
8200
+ * @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
8195
8201
  */
8196
8202
  var GooseHooks = class GooseHooks extends ToolHooks {
8197
8203
  constructor(params) {
@@ -8242,7 +8248,7 @@ var GooseHooks = class GooseHooks extends ToolHooks {
8242
8248
  throw new Error(`Failed to parse Goose hooks content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
8243
8249
  }
8244
8250
  const hooks = toolHooksToCanonical({
8245
- hooks: parsed.hooks,
8251
+ hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
8246
8252
  converterConfig: GOOSE_CONVERTER_CONFIG
8247
8253
  });
8248
8254
  return this.toRulesyncHooksDefault({ fileContent: JSON.stringify({
@@ -8297,6 +8303,24 @@ function mergeHermesConfig(fileContent, patch) {
8297
8303
  ...patch
8298
8304
  });
8299
8305
  }
8306
+ /**
8307
+ * Recursively merge `patch` into `base` (patch wins). Nested plain objects are
8308
+ * merged key-by-key; every other value (arrays, scalars) is replaced wholesale.
8309
+ * Used to overlay the Hermes-scoped permission override onto the natively
8310
+ * emitted `approvals`/`security` structures without one clobbering the other
8311
+ * (e.g. `approvals.deny` from canonical deny rules coexisting with an
8312
+ * `approvals.mode` from the override). Prototype-pollution keys are dropped.
8313
+ */
8314
+ function deepMergeHermesConfig(base, patch) {
8315
+ const result = { ...base };
8316
+ for (const [key, patchValue] of Object.entries(patch)) {
8317
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
8318
+ const baseValue = result[key];
8319
+ if (isPlainObject(baseValue) && isPlainObject(patchValue)) result[key] = deepMergeHermesConfig(baseValue, patchValue);
8320
+ else result[key] = sanitizeHermesConfigValue(patchValue);
8321
+ }
8322
+ return result;
8323
+ }
8300
8324
  //#endregion
8301
8325
  //#region src/features/hooks/hermesagent-hooks.ts
8302
8326
  /**
@@ -12279,6 +12303,38 @@ const RULESYNC_TO_CODEX_FIELD_MAP = {
12279
12303
  envVars: "env_vars"
12280
12304
  };
12281
12305
  const MAX_REMOVE_EMPTY_ENTRIES_DEPTH$1 = 32;
12306
+ /**
12307
+ * Translate a server's `oauth` table from the canonical rulesync shape (Claude
12308
+ * Code style camelCase) into the shape Codex CLI understands. Codex expects the
12309
+ * OAuth client id under snake_case `client_id`; without it `codex mcp login`
12310
+ * falls back to dynamic client registration and fails for providers that do not
12311
+ * support it (e.g. Slack, see #2158). The canonical `clientId` is kept alongside
12312
+ * the added `client_id` so tools that read the camelCase shape keep working and
12313
+ * the round-trip stays stable.
12314
+ */
12315
+ function mapOauthToCodex(oauth) {
12316
+ const result = omitPrototypePollutionKeys(oauth);
12317
+ if (typeof oauth["clientId"] === "string" && !("client_id" in result)) result["client_id"] = oauth["clientId"];
12318
+ return result;
12319
+ }
12320
+ /**
12321
+ * Reverse of {@link mapOauthToCodex}: collapse Codex's `oauth.client_id` back to
12322
+ * the canonical `clientId` on import. When both keys are present (the shape
12323
+ * rulesync itself emits) the canonical `clientId` wins and `client_id` is
12324
+ * dropped so a subsequent generate does not accumulate duplicates.
12325
+ */
12326
+ function mapOauthFromCodex(oauth) {
12327
+ const result = {};
12328
+ for (const [key, value] of Object.entries(oauth)) {
12329
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12330
+ if (key === "client_id") {
12331
+ if (!("clientId" in oauth)) result["clientId"] = value;
12332
+ continue;
12333
+ }
12334
+ result[key] = value;
12335
+ }
12336
+ return result;
12337
+ }
12282
12338
  function convertFromCodexFormat(codexMcp) {
12283
12339
  const result = {};
12284
12340
  for (const [name, config] of Object.entries(codexMcp)) {
@@ -12288,7 +12344,8 @@ function convertFromCodexFormat(codexMcp) {
12288
12344
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12289
12345
  if (key === "enabled") {
12290
12346
  if (value === false) converted["disabled"] = true;
12291
- } else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
12347
+ } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthFromCodex(value);
12348
+ else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
12292
12349
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
12293
12350
  if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
12294
12351
  else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
@@ -12308,7 +12365,8 @@ function convertToCodexFormat(mcpServers) {
12308
12365
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
12309
12366
  if (key === "disabled") {
12310
12367
  if (value === true) converted["enabled"] = false;
12311
- } else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
12368
+ } else if (key === "oauth" && isRecord(value)) converted[key] = mapOauthToCodex(value);
12369
+ else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
12312
12370
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
12313
12371
  if (mappedKey) if (isStringArray(value)) converted[mappedKey] = value;
12314
12372
  else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
@@ -13524,10 +13582,12 @@ function resolveHermesTimeout(config) {
13524
13582
  *
13525
13583
  * Hermes is close to the MCP spec but not identical: `command` must be a single
13526
13584
  * executable string (an array's tail folds into `args`), a server is disabled
13527
- * via `enabled: false` (not the canonical `disabled: true`), and remote servers
13528
- * use `url`/`headers`. Only fields Hermes understands are emitted, so the shared
13529
- * `config.yaml` is not polluted with canonical-only aliases (`type`, `transport`,
13530
- * `httpUrl`, `networkTimeout`, tool-filter keys, ...).
13585
+ * via `enabled: false` (not the canonical `disabled: true`), remote servers use
13586
+ * `url`/`headers`, and per-server tool scoping lives under a `tools: { include,
13587
+ * exclude }` block (from the canonical `enabledTools`/`disabledTools`). Only
13588
+ * fields Hermes understands are emitted, so the shared `config.yaml` is not
13589
+ * polluted with canonical-only aliases (`type`, `transport`, `httpUrl`,
13590
+ * `networkTimeout`, ...).
13531
13591
  */
13532
13592
  function convertServerToHermes(config) {
13533
13593
  const out = {};
@@ -13551,6 +13611,10 @@ function convertServerToHermes(config) {
13551
13611
  if (config.disabled === true) out.enabled = false;
13552
13612
  const timeout = resolveHermesTimeout(config);
13553
13613
  if (timeout !== void 0) out.timeout = timeout;
13614
+ const tools = {};
13615
+ if (isStringArray(config.enabledTools)) tools.include = config.enabledTools;
13616
+ if (isStringArray(config.disabledTools)) tools.exclude = config.disabledTools;
13617
+ if (Object.keys(tools).length > 0) out.tools = tools;
13554
13618
  return out;
13555
13619
  }
13556
13620
  /**
@@ -13592,6 +13656,10 @@ function convertFromHermesFormat(mcpServers) {
13592
13656
  if (isPlainObject(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
13593
13657
  if (config.enabled === false) server.disabled = true;
13594
13658
  if (typeof config.timeout === "number") server.networkTimeout = config.timeout;
13659
+ if (isRecord(config.tools)) {
13660
+ if (isStringArray(config.tools.include)) server.enabledTools = config.tools.include;
13661
+ if (isStringArray(config.tools.exclude)) server.disabledTools = config.tools.exclude;
13662
+ }
13595
13663
  result[name] = server;
13596
13664
  }
13597
13665
  return result;
@@ -15747,17 +15815,232 @@ const PermissionActionSchema = z.enum([
15747
15815
  */
15748
15816
  const PermissionRulesSchema = z.record(z.string(), PermissionActionSchema);
15749
15817
  /**
15818
+ * OpenCode-specific permission value. Unlike the shared canonical block, which
15819
+ * only accepts a pattern-to-action map, OpenCode also allows a bare action
15820
+ * string that applies to the whole category (e.g. `"external_directory": "deny"`).
15821
+ * This mirrors OpenCode's own permission schema in `opencode-permissions.ts`.
15822
+ */
15823
+ const OpencodeOverridePermissionValueSchema = z.union([PermissionActionSchema, PermissionRulesSchema]);
15824
+ /**
15825
+ * Tool-scoped override block for OpenCode. Permission categories placed here
15826
+ * (e.g. OpenCode-only categories such as `external_directory`) are emitted only
15827
+ * into OpenCode's config and never leak into other tools' permission files. It
15828
+ * also lets a shared category be overridden with an OpenCode-specific value.
15829
+ * Kept `looseObject` so future OpenCode categories are accepted.
15830
+ *
15831
+ * @example
15832
+ * { "permission": { "external_directory": "deny", "webfetch": "allow" } }
15833
+ */
15834
+ const OpencodePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
15835
+ /**
15836
+ * Tool-scoped override block for Hermes Agent. Keys placed here are deep-merged
15837
+ * into Hermes's `~/.hermes/config.yaml` and never leak into other tools' configs.
15838
+ * It carries Hermes-specific approval/security controls that have no canonical
15839
+ * permission category — e.g. `approvals` (`mode`, `cron_mode`, ...),
15840
+ * `security` (`allow_private_urls`, ...), `skills.write_approval`,
15841
+ * `memory.write_approval`. Kept `looseObject` (a verbatim passthrough) so any
15842
+ * current or future Hermes config key can be authored without modeling each one.
15843
+ *
15844
+ * @example
15845
+ * { "approvals": { "mode": "smart" }, "security": { "allow_private_urls": false } }
15846
+ */
15847
+ const HermesPermissionsOverrideSchema = z.looseObject({});
15848
+ /**
15849
+ * Tool-scoped override block for Cline. Cline's `command-permissions.json`
15850
+ * carries a single global `allowRedirects` boolean (gates shell redirection
15851
+ * operators `>`/`>>`/`<`) that has no per-command dimension and therefore no
15852
+ * canonical permission category. Placing it here lets users author it
15853
+ * declaratively; it is emitted only into Cline's config. Kept `looseObject` so
15854
+ * future Cline-only knobs can be added.
15855
+ *
15856
+ * @example
15857
+ * { "allowRedirects": true }
15858
+ */
15859
+ const ClinePermissionsOverrideSchema = z.looseObject({ allowRedirects: z.optional(z.boolean()) });
15860
+ /**
15861
+ * Tool-scoped override block for Kilo Code (an OpenCode fork). Kilo's permission
15862
+ * object is a free-form record with tool-specific keys that have no canonical
15863
+ * category — OpenCode-inherited ones (`external_directory`, `doom_loop`, `lsp`,
15864
+ * `question`, `todowrite`, `skill`, `task`, `list`) and Kilo-unique ones
15865
+ * (`agent_manager`, `notebook_read`, `notebook_edit`, `notebook_execute`,
15866
+ * `repo_clone`, `repo_overview`). Placing them here makes them authorable and
15867
+ * portable and keeps them out of other tools' configs. Mirrors the OpenCode
15868
+ * override; each value may be a bare action string or a pattern map.
15869
+ *
15870
+ * @example
15871
+ * { "permission": { "external_directory": "deny", "doom_loop": "ask" } }
15872
+ */
15873
+ const KiloPermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), OpencodeOverridePermissionValueSchema)) });
15874
+ /**
15875
+ * Tool-scoped override block for Claude Code. Claude Code's `permissions` object
15876
+ * (in `.claude/settings.json`) carries non-list fields that have no canonical
15877
+ * permission category — `defaultMode` (the session-start permission mode) and
15878
+ * `additionalDirectories` (extra working directories) being the primary ones.
15879
+ * Fields placed under `claudecode.permissions` are merged into the settings
15880
+ * `permissions` object and emitted only for Claude Code, while the shared
15881
+ * `permission` block continues to drive the `allow`/`ask`/`deny` arrays. Kept a
15882
+ * `looseObject` passthrough so any current or future `permissions` field can be
15883
+ * authored without modeling each one; the managed `allow`/`ask`/`deny` arrays are
15884
+ * ignored here (rulesync owns them).
15885
+ *
15886
+ * @example
15887
+ * { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
15888
+ */
15889
+ const ClaudecodePermissionsOverrideSchema = z.looseObject({ permissions: z.optional(z.looseObject({})) });
15890
+ /**
15891
+ * Tool-scoped override block for Mistral Vibe. Vibe's per-tool `BaseToolConfig`
15892
+ * carries a `sensitive_patterns` list — patterns that escalate to ASK even when
15893
+ * the tool's base permission is ALWAYS (allow). The canonical model can only set
15894
+ * a pattern to a single `allow`/`ask`/`deny`, so an "allow by default but ask on
15895
+ * these patterns" escalation cannot be expressed. Entries under
15896
+ * `vibe.permission.<category>.sensitive_patterns` carry that list per canonical
15897
+ * category; the shared `permission` block still drives the base permission and
15898
+ * allow/deny lists. Keyed by canonical category (e.g. `bash`, `edit`).
15899
+ *
15900
+ * @example
15901
+ * { "permission": { "bash": { "sensitive_patterns": ["rm *", "sudo *"] } } }
15902
+ */
15903
+ const VibePermissionsOverrideSchema = z.looseObject({ permission: z.optional(z.record(z.string(), z.looseObject({ sensitive_patterns: z.optional(z.array(z.string())) }))) });
15904
+ /**
15905
+ * Tool-scoped override block for Cursor CLI. Cursor's `cli.json` carries scalar
15906
+ * autonomy settings with no canonical permission category — `approvalMode`
15907
+ * (`allowlist` | `auto-review` | `unrestricted`) and a `sandbox` object
15908
+ * (`mode`/`networkAccess`). Fields placed here are merged into the top-level of
15909
+ * `.cursor/cli.json` (project) / `~/.cursor/cli-config.json` (global) and emitted
15910
+ * only for Cursor, while the shared `permission` block continues to drive the
15911
+ * `permissions.allow`/`permissions.deny` arrays. Kept a `looseObject` so extra
15912
+ * `cli.json` keys can be authored (they are merged verbatim on generate);
15913
+ * `sandbox`'s accepted values are not documented so it passes through verbatim.
15914
+ * Note: only `approvalMode` and `sandbox` round-trip back on import — other keys
15915
+ * authored here reach `cli.json` on generate but are not re-extracted.
15916
+ *
15917
+ * @example
15918
+ * { "approvalMode": "auto-review" }
15919
+ */
15920
+ const CursorPermissionsOverrideSchema = z.looseObject({
15921
+ approvalMode: z.optional(z.string()),
15922
+ sandbox: z.optional(z.looseObject({}))
15923
+ });
15924
+ /**
15925
+ * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
15926
+ * autonomy/sandbox controls with no canonical permission category — under
15927
+ * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
15928
+ * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`). Fields
15929
+ * placed here are merged into the matching `settings.json` group and emitted
15930
+ * only for Qwen, while the shared `permission` block continues to drive the
15931
+ * `permissions.allow`/`ask`/`deny` arrays. Kept `looseObject` (verbatim
15932
+ * passthrough) so any current or future `tools`/`security` key can be authored.
15933
+ *
15934
+ * @example
15935
+ * { "tools": { "approvalMode": "auto-edit" }, "security": { "folderTrust": { "enabled": true } } }
15936
+ */
15937
+ const QwencodePermissionsOverrideSchema = z.looseObject({
15938
+ tools: z.optional(z.looseObject({})),
15939
+ security: z.optional(z.looseObject({}))
15940
+ });
15941
+ /**
15942
+ * Tool-scoped override block for Reasonix. Reasonix has security axes orthogonal
15943
+ * to per-tool allow/ask/deny with no canonical category — the `[sandbox]`
15944
+ * enforcement table (`workspace_root`, `allow_write`, `forbid_read`, `bash`,
15945
+ * `network`) and plan-mode read-only trust lists under `[agent]`
15946
+ * (`plan_mode_allowed_tools`, `plan_mode_read_only_commands`). Fields placed here
15947
+ * are merged into the matching `reasonix.toml` table and emitted only for
15948
+ * Reasonix, while the shared `permission` block continues to drive
15949
+ * `[permissions].allow`/`ask`/`deny`. Kept `looseObject` (verbatim passthrough).
15950
+ * Note: the whole `[sandbox]` table round-trips, but only the plan-mode keys are
15951
+ * re-extracted from `[agent]` on import — other `agent` keys authored here reach
15952
+ * `reasonix.toml` on generate but are not re-extracted back into the override.
15953
+ *
15954
+ * @example
15955
+ * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
15956
+ */
15957
+ const ReasonixPermissionsOverrideSchema = z.looseObject({
15958
+ sandbox: z.optional(z.looseObject({})),
15959
+ agent: z.optional(z.looseObject({}))
15960
+ });
15961
+ /**
15962
+ * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
15963
+ * exposes security controls with no canonical per-command allow/ask/deny slot —
15964
+ * `commandBlocklist` (a hard-block tier that can never be approved, distinct from
15965
+ * an approvable `deny`), `networkPolicy` (`allowedIps`), `sandbox`
15966
+ * (`enabled`/`mode`/`filesystem`/`network`), `mcpPolicy`, `enableDroidShield`,
15967
+ * and autonomy settings (`sessionDefaultSettings`, `maxAutonomyLevel`,
15968
+ * `interactionMode`). Fields placed here are merged into `settings.json` and
15969
+ * emitted only for Factory Droid, while the shared `permission` block continues
15970
+ * to drive `commandAllowlist`/`commandDenylist`. Kept `looseObject` passthrough.
15971
+ *
15972
+ * @example
15973
+ * { "commandBlocklist": ["rm -rf /*"], "sandbox": { "enabled": true } }
15974
+ */
15975
+ const FactorydroidPermissionsOverrideSchema = z.looseObject({ commandBlocklist: z.optional(z.array(z.string())) });
15976
+ /**
15977
+ * Tool-scoped override block for Warp. Warp's `[agents.profiles]` table exposes
15978
+ * file-read/read-only autonomy keys with no canonical per-command allow/ask/deny
15979
+ * slot — `agent_mode_coding_permissions`
15980
+ * (`always_ask_before_reading` | `always_allow_reading` | `allow_reading_specific_files`),
15981
+ * `agent_mode_coding_file_read_allowlist` (a path array), and
15982
+ * `agent_mode_execute_readonly_commands` (a read-only auto-execution boolean).
15983
+ * Fields placed here are merged into `[agents.profiles]` of Warp's global
15984
+ * `settings.toml`, while the shared `permission` block continues to drive the
15985
+ * `agent_mode_command_execution_allowlist`/`_denylist` command regex arrays.
15986
+ * Warp permissions are global-only.
15987
+ *
15988
+ * @example
15989
+ * { "agent_mode_coding_permissions": "always_allow_reading", "agent_mode_execute_readonly_commands": true }
15990
+ */
15991
+ const WarpPermissionsOverrideSchema = z.looseObject({
15992
+ agent_mode_coding_permissions: z.optional(z.string()),
15993
+ agent_mode_coding_file_read_allowlist: z.optional(z.array(z.string())),
15994
+ agent_mode_execute_readonly_commands: z.optional(z.boolean())
15995
+ });
15996
+ /**
15997
+ * Tool-scoped override block for JetBrains Junie. Junie's `allowlist.json` has
15998
+ * two top-level autonomy knobs with no canonical per-glob slot:
15999
+ * `allowReadonlyCommands` (a boolean auto-allowing read-only commands) and
16000
+ * `defaultBehavior` (the fallback action applied when no rule matches — Junie
16001
+ * documents only `allow`/`ask`). Fields placed here are merged onto the
16002
+ * top level of `allowlist.json`, while the shared `permission` block continues
16003
+ * to drive the per-category `rules` groups.
16004
+ *
16005
+ * @example
16006
+ * { "allowReadonlyCommands": true, "defaultBehavior": "ask" }
16007
+ */
16008
+ const JuniePermissionsOverrideSchema = z.looseObject({
16009
+ allowReadonlyCommands: z.optional(z.boolean()),
16010
+ defaultBehavior: z.optional(z.string())
16011
+ });
16012
+ /**
15750
16013
  * Permissions configuration.
15751
16014
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
15752
16015
  * Values are pattern-to-action mappings for that tool category.
15753
16016
  *
16017
+ * The optional `opencode`/`hermes`/`cline`/`kilo`/`claudecode`/`vibe`/`cursor`/
16018
+ * `qwencode`/`reasonix`/`factorydroid`/`warp`/`junie` keys are tool-scoped
16019
+ * overrides consumed only by their respective translator (see the matching
16020
+ * `*PermissionsOverrideSchema`); every other tool reads the shared `permission`
16021
+ * block and ignores them.
16022
+ *
15754
16023
  * @example
15755
16024
  * {
15756
16025
  * "bash": { "*": "ask", "git *": "allow", "rm *": "deny" },
15757
16026
  * "edit": { "*": "deny", "src/**": "allow" }
15758
16027
  * }
15759
16028
  */
15760
- const PermissionsConfigSchema = z.looseObject({ permission: z.record(z.string(), PermissionRulesSchema) });
16029
+ const PermissionsConfigSchema = z.looseObject({
16030
+ permission: z.record(z.string(), PermissionRulesSchema),
16031
+ opencode: z.optional(OpencodePermissionsOverrideSchema),
16032
+ hermes: z.optional(HermesPermissionsOverrideSchema),
16033
+ cline: z.optional(ClinePermissionsOverrideSchema),
16034
+ kilo: z.optional(KiloPermissionsOverrideSchema),
16035
+ claudecode: z.optional(ClaudecodePermissionsOverrideSchema),
16036
+ vibe: z.optional(VibePermissionsOverrideSchema),
16037
+ cursor: z.optional(CursorPermissionsOverrideSchema),
16038
+ qwencode: z.optional(QwencodePermissionsOverrideSchema),
16039
+ reasonix: z.optional(ReasonixPermissionsOverrideSchema),
16040
+ factorydroid: z.optional(FactorydroidPermissionsOverrideSchema),
16041
+ warp: z.optional(WarpPermissionsOverrideSchema),
16042
+ junie: z.optional(JuniePermissionsOverrideSchema)
16043
+ });
15761
16044
  /**
15762
16045
  * Full permissions file schema including optional $schema field.
15763
16046
  */
@@ -17128,6 +17411,14 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
17128
17411
  }
17129
17412
  const config = rulesyncPermissions.getJson();
17130
17413
  const { allow, ask, deny } = convertRulesyncToClaudePermissions(config);
17414
+ const overridePermissions = config.claudecode?.permissions;
17415
+ if (overridePermissions && typeof overridePermissions === "object") {
17416
+ const { allow: _a, ask: _k, deny: _d, ...nonListFields } = overridePermissions;
17417
+ settings.permissions = {
17418
+ ...settings.permissions,
17419
+ ...nonListFields
17420
+ };
17421
+ }
17131
17422
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toClaudeToolName(category)));
17132
17423
  const merged = applyPermissions({
17133
17424
  settings,
@@ -17160,6 +17451,8 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
17160
17451
  ask: permissions.ask ?? [],
17161
17452
  deny: permissions.deny ?? []
17162
17453
  });
17454
+ const { allow: _a, ask: _k, deny: _d, ...nonListFields } = permissions;
17455
+ if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
17163
17456
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17164
17457
  }
17165
17458
  validate() {
@@ -17333,7 +17626,8 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17333
17626
  } catch (error) {
17334
17627
  throw new Error(`Failed to parse existing Cline command-permissions at ${filePath}: ${formatError(error)}`, { cause: error });
17335
17628
  }
17336
- const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(rulesyncPermissions.getJson().permission);
17629
+ const config = rulesyncPermissions.getJson();
17630
+ const { allow, deny, droppedCategories, translatedAskPatterns } = translateClinePermissions(config.permission);
17337
17631
  warnClineTranslationNotices({
17338
17632
  droppedCategories,
17339
17633
  translatedAskPatterns,
@@ -17349,7 +17643,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17349
17643
  ...existing,
17350
17644
  allow: dedupedAllow,
17351
17645
  deny: mergedDeny,
17352
- allowRedirects: existing.allowRedirects ?? false
17646
+ allowRedirects: config.cline?.allowRedirects ?? existing.allowRedirects ?? false
17353
17647
  };
17354
17648
  return new ClinePermissions({
17355
17649
  outputRoot,
@@ -17373,6 +17667,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
17373
17667
  for (const pattern of parsed.allow ?? []) bashRules[pattern] = "allow";
17374
17668
  for (const pattern of parsed.deny ?? []) bashRules[pattern] = "deny";
17375
17669
  const config = Object.keys(bashRules).length > 0 ? { permission: { bash: bashRules } } : { permission: {} };
17670
+ if (parsed.allowRedirects === true) config.cline = { allowRedirects: true };
17376
17671
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
17377
17672
  }
17378
17673
  validate() {
@@ -17809,13 +18104,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
17809
18104
  WebFetch: "webfetch",
17810
18105
  Mcp: "mcp"
17811
18106
  };
17812
- const MCP_CANONICAL_PREFIX = "mcp__";
18107
+ const MCP_CANONICAL_PREFIX$1 = "mcp__";
17813
18108
  /**
17814
18109
  * Returns true if the canonical category is the per-tool MCP form
17815
18110
  * `mcp__<server>__<tool>`.
17816
18111
  */
17817
18112
  function isMcpScopedCategory(canonical) {
17818
- return canonical.startsWith(MCP_CANONICAL_PREFIX) && canonical.length > 5;
18113
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$1) && canonical.length > 5;
17819
18114
  }
17820
18115
  function toCursorType(canonical) {
17821
18116
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -17846,7 +18141,7 @@ function toCursorPattern(canonical, pattern) {
17846
18141
  function toCanonicalCategory$1(cursorType, pattern) {
17847
18142
  if (cursorType === "Mcp") {
17848
18143
  const match = pattern.match(/^([^:()]+):([^:()]+)$/);
17849
- if (match) return `${MCP_CANONICAL_PREFIX}${match[1] ?? "*"}__${match[2] ?? "*"}`;
18144
+ if (match) return `${MCP_CANONICAL_PREFIX$1}${match[1] ?? "*"}__${match[2] ?? "*"}`;
17850
18145
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
17851
18146
  }
17852
18147
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
@@ -17980,8 +18275,10 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
17980
18275
  mergedPermissions.allow = mergedAllow;
17981
18276
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
17982
18277
  else delete mergedPermissions.deny;
18278
+ const cursorOverride = config.cursor;
17983
18279
  const merged = {
17984
18280
  ...settings,
18281
+ ...cursorOverride,
17985
18282
  version: settings.version ?? 1,
17986
18283
  editor: {
17987
18284
  ...existingEditor,
@@ -18007,11 +18304,15 @@ var CursorPermissions = class CursorPermissions extends ToolPermissions {
18007
18304
  }
18008
18305
  const permissionsRaw = settings.permissions;
18009
18306
  const permissions = permissionsRaw !== null && typeof permissionsRaw === "object" && !Array.isArray(permissionsRaw) ? permissionsRaw : {};
18010
- const config = convertCursorToRulesyncPermissions({
18307
+ const result = { ...convertCursorToRulesyncPermissions({
18011
18308
  allow: asCursorPermissionEntryArray(permissions.allow),
18012
18309
  deny: asCursorPermissionEntryArray(permissions.deny)
18013
- });
18014
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18310
+ }) };
18311
+ const cursorOverride = {};
18312
+ if (settings.approvalMode !== void 0) cursorOverride.approvalMode = settings.approvalMode;
18313
+ if (settings.sandbox !== null && typeof settings.sandbox === "object" && !Array.isArray(settings.sandbox)) cursorOverride.sandbox = settings.sandbox;
18314
+ if (Object.keys(cursorOverride).length > 0) result.cursor = cursorOverride;
18315
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18015
18316
  }
18016
18317
  validate() {
18017
18318
  return {
@@ -18070,7 +18371,7 @@ function convertCursorToRulesyncPermissions(params) {
18070
18371
  const { type, pattern } = parseCursorPermissionEntry(entry);
18071
18372
  const canonical = toCanonicalCategory$1(type, pattern);
18072
18373
  if (!permission[canonical]) permission[canonical] = {};
18073
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX) ? "*" : pattern;
18374
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$1) ? "*" : pattern;
18074
18375
  permission[canonical][canonicalPattern] = action;
18075
18376
  }
18076
18377
  };
@@ -18319,6 +18620,16 @@ function convertDevinToRulesyncPermissions(params) {
18319
18620
  }
18320
18621
  //#endregion
18321
18622
  //#region src/features/permissions/factorydroid-permissions.ts
18623
+ const FACTORYDROID_OVERRIDE_KEYS = [
18624
+ "commandBlocklist",
18625
+ "networkPolicy",
18626
+ "sandbox",
18627
+ "mcpPolicy",
18628
+ "enableDroidShield",
18629
+ "sessionDefaultSettings",
18630
+ "maxAutonomyLevel",
18631
+ "interactionMode"
18632
+ ];
18322
18633
  /**
18323
18634
  * Permissions adapter for Factory Droid.
18324
18635
  *
@@ -18336,18 +18647,14 @@ function convertDevinToRulesyncPermissions(params) {
18336
18647
  * skipped (with a warning when they carry `deny` rules, to surface the gap).
18337
18648
  *
18338
18649
  * Factory Droid also has a stronger `commandBlocklist` tier — commands that can
18339
- * never run, not even under full autonomy. rulesync's canonical action model
18340
- * has only `allow | ask | deny`, with no equivalent of a hard block that can
18341
- * never be approved. So on **import** a `commandBlocklist` entry is collapsed
18342
- * onto canonical `deny` (lossy: the never-runs guarantee is weakened to a deny
18343
- * the user can still approve), rather than being silently dropped. On **export**
18344
- * there is no canonical `block` to emit one from, so rulesync never writes
18345
- * `commandBlocklist`; an existing one on disk is preserved verbatim as an
18346
- * unmanaged key. (A consequence of the lossy collapse: importing a
18347
- * `commandBlocklist` and re-exporting it to a *fresh* config writes it back as
18348
- * `commandDenylist`, not `commandBlocklist` — the hard-block tier is not
18349
- * reconstructed. Re-running over the original file keeps it intact via the
18350
- * verbatim preservation above.)
18650
+ * never run, not even under full autonomy plus other security controls
18651
+ * (`networkPolicy`, `sandbox`, `mcpPolicy`, `enableDroidShield`, autonomy
18652
+ * settings) that do not fit the canonical `allow | ask | deny` per-command
18653
+ * model. These are authored and round-tripped through the `factorydroid`
18654
+ * override namespace (see `FactorydroidPermissionsOverrideSchema`): on **import**
18655
+ * they are lifted from `settings.json` into the override, and on **export** they
18656
+ * are merged back in so `commandBlocklist`'s never-runs guarantee is preserved
18657
+ * faithfully rather than being collapsed onto an approvable `deny`.
18351
18658
  */
18352
18659
  var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissions {
18353
18660
  constructor(params) {
@@ -18390,11 +18697,16 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
18390
18697
  } catch (error) {
18391
18698
  throw new Error(`Failed to parse existing Factory Droid settings at ${filePath}: ${formatError(error)}`, { cause: error });
18392
18699
  }
18700
+ const config = rulesyncPermissions.getJson();
18393
18701
  const { allow, deny } = convertRulesyncToFactorydroidPermissions({
18394
- config: rulesyncPermissions.getJson(),
18702
+ config,
18395
18703
  logger
18396
18704
  });
18397
- const merged = { ...settings };
18705
+ const override = config.factorydroid;
18706
+ const merged = {
18707
+ ...settings,
18708
+ ...override !== void 0 && typeof override === "object" ? override : {}
18709
+ };
18398
18710
  const mergedAllow = uniq(allow.toSorted());
18399
18711
  const mergedDeny = uniq(deny.toSorted());
18400
18712
  if (mergedAllow.length > 0) merged.commandAllowlist = mergedAllow;
@@ -18418,10 +18730,13 @@ var FactorydroidPermissions = class FactorydroidPermissions extends ToolPermissi
18418
18730
  }
18419
18731
  const config = convertFactorydroidToRulesyncPermissions({
18420
18732
  allow: Array.isArray(settings.commandAllowlist) ? settings.commandAllowlist : [],
18421
- deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : [],
18422
- block: Array.isArray(settings.commandBlocklist) ? settings.commandBlocklist : []
18733
+ deny: Array.isArray(settings.commandDenylist) ? settings.commandDenylist : []
18423
18734
  });
18424
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
18735
+ const factorydroidOverride = {};
18736
+ for (const key of FACTORYDROID_OVERRIDE_KEYS) if (settings[key] !== void 0) factorydroidOverride[key] = settings[key];
18737
+ const result = { ...config };
18738
+ if (Object.keys(factorydroidOverride).length > 0) result.factorydroid = factorydroidOverride;
18739
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18425
18740
  }
18426
18741
  validate() {
18427
18742
  return {
@@ -18468,18 +18783,18 @@ function convertRulesyncToFactorydroidPermissions({ config, logger }) {
18468
18783
  };
18469
18784
  }
18470
18785
  /**
18471
- * Convert Factory Droid allow/deny/block command lists back to rulesync config
18472
- * under the `bash` category.
18786
+ * Convert Factory Droid allow/deny command lists back to rulesync config under
18787
+ * the `bash` category.
18473
18788
  *
18474
- * `commandBlocklist` (hard block) has no canonical equivalent, so it collapses
18475
- * onto `deny` lossy (a deny can still be approved), but preferable to dropping
18476
- * the rule entirely.
18789
+ * `commandBlocklist` (the hard-block tier) is no longer collapsed here it has
18790
+ * no canonical equivalent and now round-trips through the `factorydroid`
18791
+ * override so the never-runs guarantee is preserved instead of being weakened to
18792
+ * an approvable `deny`.
18477
18793
  */
18478
- function convertFactorydroidToRulesyncPermissions({ allow, deny, block }) {
18794
+ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
18479
18795
  const bash = {};
18480
18796
  for (const pattern of allow) bash[pattern] = "allow";
18481
18797
  for (const pattern of deny) bash[pattern] = "deny";
18482
- for (const pattern of block) bash[pattern] = "deny";
18483
18798
  return { permission: Object.keys(bash).length > 0 ? { bash } : {} };
18484
18799
  }
18485
18800
  //#endregion
@@ -18673,30 +18988,114 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
18673
18988
  const GROKCLI_GLOBAL_ONLY_MESSAGE = "Grok CLI permissions are global-only; use --global to sync ~/.grok/config.toml";
18674
18989
  const GROKCLI_UI_KEY = "ui";
18675
18990
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
18991
+ const GROKCLI_PERMISSION_KEY = "permission";
18676
18992
  const CATCH_ALL_PATTERN$2 = "*";
18993
+ const MCP_CANONICAL_PREFIX = "mcp__";
18994
+ const CATEGORY_TO_GROK_TOOL = {
18995
+ bash: "Bash",
18996
+ read: "Read",
18997
+ edit: "Edit",
18998
+ write: "Edit",
18999
+ grep: "Grep",
19000
+ webfetch: "WebFetch"
19001
+ };
19002
+ const GROK_TOOL_TO_CATEGORY = {
19003
+ Bash: "bash",
19004
+ Read: "read",
19005
+ Edit: "edit",
19006
+ Grep: "grep",
19007
+ WebFetch: "webfetch"
19008
+ };
19009
+ const GROK_MCP_TOOL = "MCPTool";
19010
+ /**
19011
+ * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
19012
+ * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
19013
+ * for categories Grok cannot express so callers can skip them.
19014
+ *
19015
+ * Scoped MCP categories (`mcp__<remainder>`) fold their address into the
19016
+ * parentheses (`MCPTool(<remainder>)`); the bare `mcp` category becomes
19017
+ * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
19018
+ * concrete pattern emits `Tool(pattern)`.
19019
+ */
19020
+ function buildGrokEntry(category, pattern) {
19021
+ if (category.startsWith(MCP_CANONICAL_PREFIX)) {
19022
+ const remainder = category.slice(5);
19023
+ return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
19024
+ }
19025
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
19026
+ const tool = CATEGORY_TO_GROK_TOOL[category];
19027
+ if (tool === void 0) return null;
19028
+ return pattern === CATCH_ALL_PATTERN$2 || pattern === "" ? tool : `${tool}(${pattern})`;
19029
+ }
19030
+ /**
19031
+ * Parse a Grok Claude-style permission entry back into a canonical category +
19032
+ * pattern. Entries without parentheses are treated as catch-all (`*`). Returns
19033
+ * `null` for tool prefixes with no canonical equivalent (e.g. `any`).
19034
+ */
19035
+ function parseGrokEntry(entry) {
19036
+ const trimmed = entry.trim();
19037
+ const parenIndex = trimmed.indexOf("(");
19038
+ let tool;
19039
+ let inner;
19040
+ if (parenIndex === -1 || !trimmed.endsWith(")")) {
19041
+ tool = trimmed;
19042
+ inner = "";
19043
+ } else {
19044
+ tool = trimmed.slice(0, parenIndex);
19045
+ inner = trimmed.slice(parenIndex + 1, -1).trim();
19046
+ }
19047
+ if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
19048
+ category: `${MCP_CANONICAL_PREFIX}${inner}`,
19049
+ pattern: CATCH_ALL_PATTERN$2
19050
+ } : {
19051
+ category: "mcp",
19052
+ pattern: CATCH_ALL_PATTERN$2
19053
+ };
19054
+ const category = GROK_TOOL_TO_CATEGORY[tool];
19055
+ if (category === void 0) return null;
19056
+ return {
19057
+ category,
19058
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$2
19059
+ };
19060
+ }
18677
19061
  /**
18678
19062
  * Permissions adapter for the xAI Grok Build CLI (`grokcli`).
18679
19063
  *
18680
- * Grok has no per-tool / per-pattern permission rules. Tool gating is a single
18681
- * coarse toggle, `[ui] permission_mode`, in `~/.grok/config.toml`:
18682
- * - `ask` prompt before each tool call (Grok's default).
18683
- * - `always-approve` skip prompts.
18684
- *
18685
- * rulesync's permission model is per-category, per-pattern `allow`/`ask`/`deny`,
18686
- * so the mapping is **lossy** (a single mode cannot express per-pattern rules):
18687
- * - Generate: any `deny` or `ask` rule anywhere `ask` (conservative — keep
18688
- * prompting whenever the user expressed any restriction); otherwise, if at
18689
- * least one `allow` rule exists and nothing is denied/asked
18690
- * `always-approve`; an empty config falls back to the safe default `ask`.
18691
- * - Import: `always-approve` `bash: { "*": "allow" }`; `ask` (or unset)
18692
- * `bash: { "*": "ask" }`. `bash` is used as the representative catch-all
18693
- * because it is the primary permission-gated surface, and this round-trips
18694
- * the generate mapping.
18695
- *
18696
- * This surface is **global only** `permission_mode` lives in the user-level
18697
- * `~/.grok/config.toml`; Grok has no project-scoped permission file. The shared
18698
- * config is merged in place: the `[ui] permission_mode` value is set and every
18699
- * other key (e.g. `[mcp_servers]`, legacy `approval_mode`) is preserved. The
19064
+ * Grok Build CLI ships a Claude-style rule system under `[permission]` in
19065
+ * `~/.grok/config.toml`: `allow` / `deny` / `ask` arrays of entries such as
19066
+ * `Bash(git *)`, `Read(src/**)`, `Edit`, `Grep`, `MCPTool(server__tool)`, and
19067
+ * `WebFetch`, evaluated with precedence `deny > ask > allow`
19068
+ * (https://docs.x.ai/build/settings/reference). rulesync's canonical
19069
+ * per-category, per-pattern model maps almost 1:1:
19070
+ * - Generate: each `permission.<category>.<pattern> = allow|ask|deny` becomes
19071
+ * the matching Grok entry and is bucketed into the `[permission]` array for
19072
+ * that action. `bash|read|edit|grep|webfetch` map to their Grok tool;
19073
+ * `write` collapses onto `Edit` (Grok has no `Write` tool); `mcp__*` maps to
19074
+ * `MCPTool(...)` (a scoped MCP category folds its address into the
19075
+ * parentheses, so a non-`*` argument pattern on it is not represented).
19076
+ * Categories with no Grok tool (`websearch`, `glob`, `notebookedit`,
19077
+ * `agent`) are skipped (with a warning when they carry a `deny` rule, to
19078
+ * surface the gap). When two canonical rules collapse onto the same Grok
19079
+ * entry with different actions (e.g. `edit` allow + `write` deny → `Edit`),
19080
+ * the strictest wins (`deny > ask > allow`) and a warning is logged, so the
19081
+ * entry never lands contradictorily in two arrays.
19082
+ * - Import: the `[permission]` arrays are parsed back into canonical
19083
+ * categories. When no `[permission]` section is present (older configs), the
19084
+ * coarse `[ui] permission_mode` is used as a fallback.
19085
+ *
19086
+ * The coarse `[ui] permission_mode` toggle is still written for backward
19087
+ * compatibility with older Grok versions: `always-approve` when the config is
19088
+ * pure-`allow`, otherwise `ask` (conservative — it is never `always-approve`
19089
+ * while any `deny`/`ask` rule exists, so it never contradicts the fine-grained
19090
+ * arrays).
19091
+ *
19092
+ * This surface is **global only** — the adapter syncs the user-level
19093
+ * `~/.grok/config.toml`. (Grok also supports a project-scoped `[permission]`
19094
+ * file; project scope is not modeled here.) The shared config is merged in
19095
+ * place: rulesync owns the `[permission]` `allow`/`deny`/`ask` entries for the
19096
+ * tools it models and the `[ui] permission_mode` value, while every other key
19097
+ * (e.g. `[mcp_servers]`, `[permission] rules`, `[sandbox]`) and any user-authored
19098
+ * entries for tools rulesync cannot model (e.g. `WebSearch`) are preserved. The
18700
19099
  * file is never deleted.
18701
19100
  */
18702
19101
  var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
@@ -18728,7 +19127,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18728
19127
  global: true
18729
19128
  });
18730
19129
  }
18731
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false }) {
19130
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, logger, global = false }) {
18732
19131
  if (!global) throw new Error(GROKCLI_GLOBAL_ONLY_MESSAGE);
18733
19132
  const paths = GrokcliPermissions.getSettablePaths({ global });
18734
19133
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
@@ -18739,7 +19138,16 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18739
19138
  } catch (error) {
18740
19139
  throw new Error(`Failed to parse existing Grok config.toml at ${filePath}: ${formatError(error)}`, { cause: error });
18741
19140
  }
18742
- const mode = deriveGrokPermissionMode(rulesyncPermissions.getJson());
19141
+ const config = rulesyncPermissions.getJson();
19142
+ const existingPermission = isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {};
19143
+ const buckets = buildGrokPermissionArrays(config, existingPermission, logger);
19144
+ parsed[GROKCLI_PERMISSION_KEY] = {
19145
+ ...existingPermission,
19146
+ allow: buckets.allow,
19147
+ deny: buckets.deny,
19148
+ ask: buckets.ask
19149
+ };
19150
+ const mode = deriveGrokPermissionMode(config);
18743
19151
  const existingUi = isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {};
18744
19152
  parsed[GROKCLI_UI_KEY] = {
18745
19153
  ...existingUi,
@@ -18762,8 +19170,8 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18762
19170
  } catch (error) {
18763
19171
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18764
19172
  }
18765
- const action = (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
18766
- const rulesyncConfig = { permission: { bash: { [CATCH_ALL_PATTERN$2]: action } } };
19173
+ const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
19174
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$2]: legacyModeAction(parsed) } } };
18767
19175
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
18768
19176
  }
18769
19177
  validate() {
@@ -18783,6 +19191,84 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
18783
19191
  });
18784
19192
  }
18785
19193
  };
19194
+ const ACTION_RANK = {
19195
+ allow: 0,
19196
+ ask: 1,
19197
+ deny: 2
19198
+ };
19199
+ /**
19200
+ * Collect user-authored entries from an existing `[permission]` array whose
19201
+ * tool prefix rulesync cannot model (e.g. `WebSearch`, `any`). Such entries are
19202
+ * preserved verbatim so replacing the arrays does not silently drop them
19203
+ * (mirrors the Cursor adapter's preservation of unmanaged types).
19204
+ */
19205
+ function unmanagedEntries(existingPermission, key) {
19206
+ return (isStringArray(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
19207
+ }
19208
+ /**
19209
+ * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
19210
+ * Claude-style entries, resolving collapse collisions via `deny > ask > allow`.
19211
+ * Categories Grok cannot express are skipped (with a warning when they carry a
19212
+ * `deny` rule); entries the user authored for tools rulesync cannot model are
19213
+ * preserved from `existingPermission`.
19214
+ */
19215
+ function buildGrokPermissionArrays(config, existingPermission, logger) {
19216
+ const ranked = /* @__PURE__ */ new Map();
19217
+ for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
19218
+ const entry = buildGrokEntry(category, pattern);
19219
+ if (entry === null) {
19220
+ if (action === "deny" && logger) logger.warn(`Grok CLI has no permission tool for the '${category}' category; its 'deny' rule could not be represented and was skipped.`);
19221
+ continue;
19222
+ }
19223
+ const existing = ranked.get(entry);
19224
+ if (existing !== void 0 && existing !== action) logger?.warn(`Grok permission entry '${entry}' received conflicting actions ('${existing}' and '${action}'); keeping the stricter one (deny > ask > allow).`);
19225
+ if (existing === void 0 || ACTION_RANK[action] > ACTION_RANK[existing]) ranked.set(entry, action);
19226
+ }
19227
+ const allow = unmanagedEntries(existingPermission, "allow");
19228
+ const deny = unmanagedEntries(existingPermission, "deny");
19229
+ const ask = unmanagedEntries(existingPermission, "ask");
19230
+ for (const [entry, action] of ranked) if (action === "allow") allow.push(entry);
19231
+ else if (action === "deny") deny.push(entry);
19232
+ else ask.push(entry);
19233
+ return {
19234
+ allow: uniq(allow.toSorted()),
19235
+ deny: uniq(deny.toSorted()),
19236
+ ask: uniq(ask.toSorted())
19237
+ };
19238
+ }
19239
+ /**
19240
+ * Parse Grok's `[permission]` allow/deny/ask arrays back into a canonical
19241
+ * permission map. Returns `null` when the section defines none of the three
19242
+ * arrays, so the caller can fall back to the coarse `permission_mode`.
19243
+ * Precedence `deny > ask > allow` is applied so a tool listed in multiple
19244
+ * arrays resolves to the strictest action.
19245
+ */
19246
+ function parseGrokPermissionArrays(permission) {
19247
+ const allow = isStringArray(permission.allow) ? permission.allow : void 0;
19248
+ const deny = isStringArray(permission.deny) ? permission.deny : void 0;
19249
+ const ask = isStringArray(permission.ask) ? permission.ask : void 0;
19250
+ if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) === 0) return null;
19251
+ const result = {};
19252
+ const apply = (entries, action) => {
19253
+ for (const entry of entries ?? []) {
19254
+ const parsed = parseGrokEntry(entry);
19255
+ if (parsed === null) continue;
19256
+ const bucket = result[parsed.category] ??= {};
19257
+ bucket[parsed.pattern] = action;
19258
+ }
19259
+ };
19260
+ apply(allow, "allow");
19261
+ apply(ask, "ask");
19262
+ apply(deny, "deny");
19263
+ return result;
19264
+ }
19265
+ /**
19266
+ * Legacy coarse fallback: map `[ui] permission_mode` to a canonical action.
19267
+ * `always-approve` ⇒ `allow`; anything else (including a missing mode) ⇒ `ask`.
19268
+ */
19269
+ function legacyModeAction(parsed) {
19270
+ return (isRecord(parsed[GROKCLI_UI_KEY]) ? parsed[GROKCLI_UI_KEY] : {})[GROKCLI_PERMISSION_MODE_KEY] === "always-approve" ? "allow" : "ask";
19271
+ }
18786
19272
  /**
18787
19273
  * Collapse a rulesync permissions config into Grok's single coarse mode.
18788
19274
  * Any `deny`/`ask` rule anywhere keeps prompting (`ask`); otherwise an existing
@@ -18798,6 +19284,10 @@ function deriveGrokPermissionMode(config) {
18798
19284
  }
18799
19285
  //#endregion
18800
19286
  //#region src/features/permissions/hermesagent-permissions.ts
19287
+ /** Collect the glob patterns in a canonical category that carry a given action. */
19288
+ function patternsByAction(category, action) {
19289
+ return Object.entries(category ?? {}).filter(([, value]) => value === action).map(([pattern]) => pattern);
19290
+ }
18801
19291
  var HermesagentPermissions = class HermesagentPermissions extends ToolPermissions {
18802
19292
  static getSettablePaths() {
18803
19293
  return {
@@ -18821,7 +19311,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18821
19311
  return true;
18822
19312
  }
18823
19313
  setFileContent(fileContent) {
18824
- this.fileContent = mergeHermesConfig(fileContent, parseHermesConfig(this.fileContent));
19314
+ const existing = parseHermesConfig(fileContent);
19315
+ const generated = parseHermesConfig(this.fileContent);
19316
+ const merged = deepMergeHermesConfig(existing, generated);
19317
+ if (generated.permissions !== void 0) merged.permissions = generated.permissions;
19318
+ this.fileContent = stringifyHermesConfig(merged);
18825
19319
  }
18826
19320
  toRulesyncPermissions() {
18827
19321
  const config = parseHermesConfig(this.getFileContent());
@@ -18834,12 +19328,22 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18834
19328
  }
18835
19329
  static fromRulesyncPermissions({ outputRoot, rulesyncPermissions }) {
18836
19330
  const permissions = rulesyncPermissions.getJson();
19331
+ const permissionBlock = permissions.permission ?? {};
19332
+ const commandAllowlist = Object.entries(permissionBlock).flatMap(([, patterns]) => patternsByAction(patterns, "allow"));
19333
+ const bashDeny = patternsByAction(permissionBlock.bash, "deny");
19334
+ const webfetchDeny = patternsByAction(permissionBlock.webfetch, "deny");
19335
+ let config = {};
19336
+ if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
19337
+ if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
19338
+ if (webfetchDeny.length > 0) config.security = { website_blocklist: {
19339
+ enabled: true,
19340
+ domains: webfetchDeny
19341
+ } };
19342
+ if (permissions.hermes && typeof permissions.hermes === "object") config = deepMergeHermesConfig(config, permissions.hermes);
19343
+ config.permissions = { rulesync: permissions };
18837
19344
  return new HermesagentPermissions({
18838
19345
  outputRoot,
18839
- fileContent: stringifyHermesConfig({
18840
- command_allowlist: Object.entries(permissions.permission ?? {}).flatMap(([, patterns]) => Object.entries(patterns).filter(([, action]) => action === "allow").map(([command]) => command)),
18841
- permissions: { rulesync: permissions }
18842
- })
19346
+ fileContent: stringifyHermesConfig(config)
18843
19347
  });
18844
19348
  }
18845
19349
  };
@@ -18860,14 +19364,18 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18860
19364
  * "executables": [ { "prefix": "git ", "action": "allow" } ],
18861
19365
  * "fileEditing": [ { "pattern": "src/**", "action": "allow" } ],
18862
19366
  * "mcpTools": [ { "prefix": "search", "action": "allow" } ],
18863
- * "readOutsideProject":[ { "pattern": "/etc/**", "action": "deny" } ]
19367
+ * "readOutsideProject":[ { "pattern": "/etc/**", "action": "ask" } ]
18864
19368
  * }
18865
19369
  * }
18866
19370
  * ```
18867
19371
  *
18868
19372
  * Each rule carries a literal `prefix` (matches commands that start with it) or
18869
- * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`
18870
- * (`allow` | `ask` | `deny`). rulesync's canonical actions map 1:1 onto Junie's.
19373
+ * a glob `pattern` (`*`, `**`, `?`, `[abc]`, `[!abc]`) plus an `action`. Junie
19374
+ * documents only `allow` and `ask` as valid actions there is **no `deny`**
19375
+ * (https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html). rulesync's
19376
+ * canonical `allow`/`ask` map 1:1; a canonical `deny` has no Junie equivalent
19377
+ * and is mapped to the nearest valid action, `ask` (still withholds
19378
+ * auto-approval), with a warning so the downgrade is surfaced.
18871
19379
  *
18872
19380
  * Category mapping (rulesync canonical <-> Junie rule group):
18873
19381
  * - `bash` <-> `executables`
@@ -18877,8 +19385,11 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
18877
19385
  *
18878
19386
  * Categories Junie cannot represent (e.g. `webfetch`) are skipped on export
18879
19387
  * (with a warning when they carry rules). The top-level `defaultBehavior` and
18880
- * `allowReadonlyCommands` settings have no canonical equivalent: they are
18881
- * preserved verbatim on export but not imported into the rulesync model.
19388
+ * `allowReadonlyCommands` settings have no canonical per-glob slot: they are
19389
+ * authored and round-tripped through the `junie` override namespace (see
19390
+ * `JuniePermissionsOverrideSchema`) — lifted into the override on import and
19391
+ * merged back onto the top level on export — and any other unmodeled top-level
19392
+ * key is preserved verbatim.
18882
19393
  *
18883
19394
  * @see https://junie.jetbrains.com/docs/action-allowlist-junie-cli.html
18884
19395
  */
@@ -18901,7 +19412,6 @@ const JUNIE_GROUP_TO_CANONICAL = {
18901
19412
  mcpTools: "mcp",
18902
19413
  readOutsideProject: "read"
18903
19414
  };
18904
- const JUNIE_DEFAULT_BEHAVIOR = "ask";
18905
19415
  function isPermissionAction$1(value) {
18906
19416
  return PermissionActionSchema.safeParse(value).success;
18907
19417
  }
@@ -18951,13 +19461,16 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18951
19461
  } catch (error) {
18952
19462
  throw new Error(`Failed to parse existing Junie allowlist at ${filePath}: ${formatError(error)}`, { cause: error });
18953
19463
  }
19464
+ const config = rulesyncPermissions.getJson();
18954
19465
  const rules = convertRulesyncToJunieRules({
18955
- config: rulesyncPermissions.getJson(),
19466
+ config,
18956
19467
  logger
18957
19468
  });
19469
+ const override = config.junie;
19470
+ const overrideObj = override !== void 0 && typeof override === "object" ? override : {};
18958
19471
  const merged = {
18959
19472
  ...existing,
18960
- defaultBehavior: existing.defaultBehavior ?? JUNIE_DEFAULT_BEHAVIOR,
19473
+ ...overrideObj,
18961
19474
  rules
18962
19475
  };
18963
19476
  return new JuniePermissions({
@@ -18977,7 +19490,12 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
18977
19490
  throw new Error(`Failed to parse Junie permissions content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
18978
19491
  }
18979
19492
  const config = convertJunieToRulesyncPermissions({ allowlist });
18980
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
19493
+ const junieOverride = {};
19494
+ if (typeof allowlist.allowReadonlyCommands === "boolean") junieOverride.allowReadonlyCommands = allowlist.allowReadonlyCommands;
19495
+ if (typeof allowlist.defaultBehavior === "string") junieOverride.defaultBehavior = allowlist.defaultBehavior;
19496
+ const result = { ...config };
19497
+ if (Object.keys(junieOverride).length > 0) result.junie = junieOverride;
19498
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
18981
19499
  }
18982
19500
  validate() {
18983
19501
  return {
@@ -19009,12 +19527,13 @@ function convertRulesyncToJunieRules({ config, logger }) {
19009
19527
  continue;
19010
19528
  }
19011
19529
  for (const [pattern, action] of Object.entries(patterns)) {
19530
+ const junieAction = toJunieAction(action, category, pattern, logger);
19012
19531
  const rule = isGlobPattern(pattern) ? {
19013
19532
  pattern,
19014
- action
19533
+ action: junieAction
19015
19534
  } : {
19016
19535
  prefix: pattern,
19017
- action
19536
+ action: junieAction
19018
19537
  };
19019
19538
  (rules[group] ??= []).push(rule);
19020
19539
  }
@@ -19022,6 +19541,19 @@ function convertRulesyncToJunieRules({ config, logger }) {
19022
19541
  return rules;
19023
19542
  }
19024
19543
  /**
19544
+ * Map a canonical action onto a valid Junie allowlist action. Junie supports
19545
+ * only `allow` and `ask`; a canonical `deny` is downgraded to `ask` (the
19546
+ * nearest valid action — both withhold auto-approval) with a warning, so
19547
+ * rulesync never emits a `deny` that Junie would silently ignore.
19548
+ */
19549
+ function toJunieAction(action, category, pattern, logger) {
19550
+ if (action === "deny") {
19551
+ logger?.warn(`Junie's allowlist supports only 'allow'/'ask' actions; the '${category}' deny rule for '${pattern}' was downgraded to 'ask' (Junie has no 'deny').`);
19552
+ return "ask";
19553
+ }
19554
+ return action;
19555
+ }
19556
+ /**
19025
19557
  * Convert a Junie allowlist back into rulesync permissions config. The
19026
19558
  * top-level `defaultBehavior` / `allowReadonlyCommands` settings have no
19027
19559
  * canonical equivalent and are not imported.
@@ -19055,6 +19587,31 @@ const KiloPermissionSchema = z.union([z.enum([
19055
19587
  ]))]);
19056
19588
  const KiloPermissionsConfigSchema = z.looseObject({ permission: z.optional(z.record(z.string(), KiloPermissionSchema)) });
19057
19589
  /**
19590
+ * Kilo permission keys that share a name with a canonical rulesync category and
19591
+ * therefore stay in the shared `permission` block. Everything else (Kilo-only
19592
+ * keys such as `external_directory`, `doom_loop`, `notebook_edit`, ...) is
19593
+ * routed into the `kilo` override on import so it does not leak into other
19594
+ * tools' configs. Kilo folds `write` into `edit`, uses `notebook_edit`/`task`
19595
+ * rather than the canonical `notebookedit`/`agent`, and has no `mcp` key (MCP is
19596
+ * addressed via `mcp__*` tool-name keys), so those canonical names are not Kilo
19597
+ * keys in practice — they simply never appear on the shared side.
19598
+ */
19599
+ const KILO_SHARED_CATEGORIES = /* @__PURE__ */ new Set([
19600
+ "bash",
19601
+ "read",
19602
+ "edit",
19603
+ "write",
19604
+ "webfetch",
19605
+ "websearch",
19606
+ "grep",
19607
+ "glob",
19608
+ "notebookedit",
19609
+ "agent"
19610
+ ]);
19611
+ function isSharedKiloCategory(key) {
19612
+ return key === "*" || KILO_SHARED_CATEGORIES.has(key) || key.startsWith("mcp__");
19613
+ }
19614
+ /**
19058
19615
  * Parse a JSONC string and throw on syntax errors. The `jsonc-parser` `parse()` function is
19059
19616
  * non-throwing best-effort: invalid input silently yields a partial value (often `undefined`,
19060
19617
  * coerced to `{}` by callers). That behavior would silently drop a user's existing `deny` rules
@@ -19166,12 +19723,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19166
19723
  const parsed = parseKiloJsoncStrict(await readFileContentOrNull(filePath) ?? "{}", filePath);
19167
19724
  const parsedPermission = parsed.permission;
19168
19725
  const existingPermission = parsedPermission && typeof parsedPermission === "object" && !Array.isArray(parsedPermission) ? { ...parsedPermission } : {};
19169
- const rulesyncPermission = rulesyncPermissions.getJson().permission;
19726
+ const rulesyncJson = rulesyncPermissions.getJson();
19727
+ const kiloOverride = rulesyncJson.kilo;
19728
+ const incomingPermission = {
19729
+ ...rulesyncJson.permission,
19730
+ ...kiloOverride?.permission
19731
+ };
19170
19732
  const droppedDenyByKey = {};
19171
- for (const key of Object.keys(rulesyncPermission)) {
19172
- const previous = existingPermission[key];
19173
- const previousDenyPatterns = collectKiloDenyPatterns(previous);
19174
- const nextDenyPatterns = new Set(collectKiloDenyPatterns(rulesyncPermission[key]));
19733
+ for (const [key, value] of Object.entries(incomingPermission)) {
19734
+ const previousDenyPatterns = collectKiloDenyPatterns(existingPermission[key]);
19735
+ const nextDenyPatterns = new Set(collectKiloDenyPatterns(value));
19175
19736
  const dropped = previousDenyPatterns.filter((p) => !nextDenyPatterns.has(p));
19176
19737
  if (dropped.length > 0) droppedDenyByKey[key] = dropped;
19177
19738
  }
@@ -19179,8 +19740,10 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19179
19740
  const summary = Object.entries(droppedDenyByKey).map(([key, patterns]) => `${key}: [${patterns.join(", ")}]`).join("; ");
19180
19741
  logger?.warn(`WARNING: Kilo permissions regeneration drops existing 'deny' rule(s) because rulesync output owns these tool keys. Dropped — ${summary}. To preserve these denies, add them to '.rulesync/permissions.json'.`);
19181
19742
  }
19182
- const mergedPermission = { ...existingPermission };
19183
- for (const [key, value] of Object.entries(rulesyncPermission)) mergedPermission[key] = value;
19743
+ const mergedPermission = {
19744
+ ...existingPermission,
19745
+ ...incomingPermission
19746
+ };
19184
19747
  const nextJson = {
19185
19748
  ...parsed,
19186
19749
  permission: mergedPermission
@@ -19194,8 +19757,16 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19194
19757
  });
19195
19758
  }
19196
19759
  toRulesyncPermissions() {
19197
- const permission = this.normalizePermission(this.json.permission);
19198
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
19760
+ const rawPermission = this.json.permission ?? {};
19761
+ const shared = {};
19762
+ const overrideOnly = {};
19763
+ for (const [key, value] of Object.entries(rawPermission)) if (isSharedKiloCategory(key)) shared[key] = typeof value === "string" ? { "*": value } : value;
19764
+ else overrideOnly[key] = value;
19765
+ const json = Object.keys(overrideOnly).length > 0 ? {
19766
+ permission: shared,
19767
+ kilo: { permission: overrideOnly }
19768
+ } : { permission: shared };
19769
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
19199
19770
  }
19200
19771
  validate() {
19201
19772
  try {
@@ -19225,10 +19796,6 @@ var KiloPermissions = class KiloPermissions extends ToolPermissions {
19225
19796
  validate: false
19226
19797
  });
19227
19798
  }
19228
- normalizePermission(permission) {
19229
- if (!permission) return {};
19230
- return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
19231
- }
19232
19799
  };
19233
19800
  //#endregion
19234
19801
  //#region src/features/permissions/kiro-permissions.ts
@@ -19286,29 +19853,16 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
19286
19853
  }
19287
19854
  const permission = {};
19288
19855
  const toolsSettings = parsed.toolsSettings ?? {};
19289
- const shellSettings = asRecord$1(toolsSettings.shell);
19290
- const shellAllow = asStringArray(shellSettings.allowedCommands);
19291
- const shellDeny = asStringArray(shellSettings.deniedCommands);
19292
- if (shellAllow.length > 0 || shellDeny.length > 0) {
19293
- permission.bash = {};
19294
- for (const pattern of shellAllow) permission.bash[pattern] = "allow";
19295
- for (const pattern of shellDeny) permission.bash[pattern] = "deny";
19296
- }
19297
- const readSettings = asRecord$1(toolsSettings.read);
19298
- const readAllow = asStringArray(readSettings.allowedPaths);
19299
- const readDeny = asStringArray(readSettings.deniedPaths);
19300
- if (readAllow.length > 0 || readDeny.length > 0) {
19301
- permission.read = {};
19302
- for (const pattern of readAllow) permission.read[pattern] = "allow";
19303
- for (const pattern of readDeny) permission.read[pattern] = "deny";
19304
- }
19305
- const writeSettings = asRecord$1(toolsSettings.write);
19306
- const writeAllow = asStringArray(writeSettings.allowedPaths);
19307
- const writeDeny = asStringArray(writeSettings.deniedPaths);
19308
- if (writeAllow.length > 0 || writeDeny.length > 0) {
19309
- permission.write = {};
19310
- for (const pattern of writeAllow) permission.write[pattern] = "allow";
19311
- for (const pattern of writeDeny) permission.write[pattern] = "deny";
19856
+ const shellRules = rulesFromArrays(asRecord$1(toolsSettings.shell), "allowedCommands", "deniedCommands");
19857
+ if (Object.keys(shellRules).length > 0) permission.bash = shellRules;
19858
+ for (const category of [
19859
+ "read",
19860
+ "write",
19861
+ "grep",
19862
+ "glob"
19863
+ ]) {
19864
+ const rules = rulesFromArrays(asRecord$1(toolsSettings[category]), "allowedPaths", "deniedPaths");
19865
+ if (Object.keys(rules).length > 0) permission[category] = rules;
19312
19866
  }
19313
19867
  const allowedTools = new Set(parsed.allowedTools ?? []);
19314
19868
  if (allowedTools.has("web_fetch")) permission.webfetch = { "*": "allow" };
@@ -19334,45 +19888,63 @@ var KiroPermissions = class KiroPermissions extends ToolPermissions {
19334
19888
  function buildKiroPermissionsFromRulesync({ config, logger, existing }) {
19335
19889
  const nextAllowedTools = new Set(existing.allowedTools ?? []);
19336
19890
  const nextToolsSettings = { ...asRecord$1(existing.toolsSettings) };
19891
+ const pathBuckets = {};
19892
+ const pushPath = (key, action, pattern) => {
19893
+ const bucket = pathBuckets[key] ??= {
19894
+ allow: [],
19895
+ deny: []
19896
+ };
19897
+ (action === "allow" ? bucket.allow : bucket.deny).push(pattern);
19898
+ };
19337
19899
  const shell = {
19338
19900
  allowedCommands: [],
19339
19901
  deniedCommands: []
19340
19902
  };
19341
- const read = {
19342
- allowedPaths: [],
19343
- deniedPaths: []
19344
- };
19345
- const write = {
19346
- allowedPaths: [],
19347
- deniedPaths: []
19348
- };
19349
19903
  for (const [category, rules] of Object.entries(config.permission)) for (const [pattern, action] of Object.entries(rules)) {
19350
19904
  if (action === "ask") {
19351
19905
  logger?.warn(`Kiro permissions do not support "ask". Skipping ${category}:${pattern}`);
19352
19906
  continue;
19353
19907
  }
19354
19908
  if (category === "bash") (action === "allow" ? shell.allowedCommands : shell.deniedCommands).push(pattern);
19355
- else if (category === "read") (action === "allow" ? read.allowedPaths : read.deniedPaths).push(pattern);
19356
- else if (category === "edit" || category === "write") (action === "allow" ? write.allowedPaths : write.deniedPaths).push(pattern);
19357
- else if (category === "webfetch" || category === "websearch") {
19358
- if (pattern !== "*") {
19359
- logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
19360
- continue;
19361
- }
19362
- const toolName = category === "webfetch" ? "web_fetch" : "web_search";
19363
- if (action === "allow") nextAllowedTools.add(toolName);
19364
- else nextAllowedTools.delete(toolName);
19365
- } else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
19909
+ else if (category === "read" || category === "grep" || category === "glob") pushPath(category, action, pattern);
19910
+ else if (category === "edit" || category === "write") pushPath("write", action, pattern);
19911
+ else if (category === "webfetch" || category === "websearch") applyKiroWebPermission({
19912
+ category,
19913
+ pattern,
19914
+ action,
19915
+ nextAllowedTools,
19916
+ logger
19917
+ });
19918
+ else logger?.warn(`Kiro permissions do not support category: ${category}. Skipping.`);
19366
19919
  }
19367
19920
  nextToolsSettings.shell = shell;
19368
- nextToolsSettings.read = read;
19369
- nextToolsSettings.write = write;
19921
+ nextToolsSettings.read = pathTable(pathBuckets.read);
19922
+ nextToolsSettings.write = pathTable(pathBuckets.write);
19923
+ for (const key of ["grep", "glob"]) {
19924
+ const bucket = pathBuckets[key];
19925
+ if (bucket && (bucket.allow.length > 0 || bucket.deny.length > 0)) nextToolsSettings[key] = pathTable(bucket);
19926
+ }
19370
19927
  return {
19371
19928
  ...existing,
19372
19929
  allowedTools: [...nextAllowedTools].toSorted(),
19373
19930
  toolsSettings: nextToolsSettings
19374
19931
  };
19375
19932
  }
19933
+ function pathTable(bucket) {
19934
+ return {
19935
+ allowedPaths: bucket?.allow ?? [],
19936
+ deniedPaths: bucket?.deny ?? []
19937
+ };
19938
+ }
19939
+ function applyKiroWebPermission({ category, pattern, action, nextAllowedTools, logger }) {
19940
+ if (pattern !== "*") {
19941
+ logger?.warn(`Kiro ${category} supports only wildcard (*) via allowedTools. Skipping rule: ${pattern}`);
19942
+ return;
19943
+ }
19944
+ const toolName = category === "webfetch" ? "web_fetch" : "web_search";
19945
+ if (action === "allow") nextAllowedTools.add(toolName);
19946
+ else nextAllowedTools.delete(toolName);
19947
+ }
19376
19948
  function asRecord$1(value) {
19377
19949
  const result = UnknownRecordSchema.safeParse(value);
19378
19950
  return result.success ? result.data : {};
@@ -19380,6 +19952,17 @@ function asRecord$1(value) {
19380
19952
  function asStringArray(value) {
19381
19953
  return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
19382
19954
  }
19955
+ /**
19956
+ * Build a canonical `{ pattern: action }` map from a Kiro tool settings record's
19957
+ * allow/deny string arrays (e.g. `allowedPaths`/`deniedPaths` or
19958
+ * `allowedCommands`/`deniedCommands`).
19959
+ */
19960
+ function rulesFromArrays(settings, allowKey, denyKey) {
19961
+ const rules = {};
19962
+ for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
19963
+ for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
19964
+ return rules;
19965
+ }
19383
19966
  //#endregion
19384
19967
  //#region src/features/permissions/opencode-permissions.ts
19385
19968
  const OpencodePermissionSchema = z.union([z.enum([
@@ -19391,6 +19974,28 @@ const OpencodePermissionSchema = z.union([z.enum([
19391
19974
  "ask",
19392
19975
  "deny"
19393
19976
  ]))]);
19977
+ /**
19978
+ * Canonical rulesync permission categories that carry a cross-tool meaning (see
19979
+ * the "Supported tool categories" list in `docs/reference/file-formats.md`).
19980
+ * On import, any OpenCode category outside this set — plus MCP tool names — is
19981
+ * treated as OpenCode-only and routed into the `opencode` override block so a
19982
+ * subsequent `rulesync generate` does not leak it into other tools' configs.
19983
+ */
19984
+ const CANONICAL_PERMISSION_CATEGORIES = /* @__PURE__ */ new Set([
19985
+ "bash",
19986
+ "read",
19987
+ "edit",
19988
+ "write",
19989
+ "webfetch",
19990
+ "websearch",
19991
+ "grep",
19992
+ "glob",
19993
+ "notebookedit",
19994
+ "agent"
19995
+ ]);
19996
+ function isSharedPermissionCategory(category) {
19997
+ return category === "*" || CANONICAL_PERMISSION_CATEGORIES.has(category) || category.startsWith("mcp__");
19998
+ }
19394
19999
  const OpencodePermissionsConfigSchema = z.looseObject({ permission: z.optional(z.union([z.enum([
19395
20000
  "allow",
19396
20001
  "ask",
@@ -19452,9 +20057,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19452
20057
  fileContent = await readFileContentOrNull(jsonPath);
19453
20058
  if (fileContent) relativeFilePath = OPENCODE_JSON_FILE_NAME;
19454
20059
  }
20060
+ const parsed = parse(fileContent ?? "{}");
20061
+ const rulesyncJson = rulesyncPermissions.getJson();
20062
+ const overridePermission = rulesyncJson.opencode?.permission ?? {};
19455
20063
  const nextJson = {
19456
- ...parse(fileContent ?? "{}"),
19457
- permission: rulesyncPermissions.getJson().permission
20064
+ ...parsed,
20065
+ permission: {
20066
+ ...rulesyncJson.permission,
20067
+ ...overridePermission
20068
+ }
19458
20069
  };
19459
20070
  return new OpencodePermissions({
19460
20071
  outputRoot,
@@ -19465,8 +20076,20 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19465
20076
  });
19466
20077
  }
19467
20078
  toRulesyncPermissions() {
19468
- const permission = this.normalizePermission(this.json.permission);
19469
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20079
+ const rawPermission = this.json.permission;
20080
+ if (rawPermission === void 0 || typeof rawPermission === "string") {
20081
+ const permission = this.normalizePermission(rawPermission);
20082
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
20083
+ }
20084
+ const shared = {};
20085
+ const overrideOnly = {};
20086
+ for (const [category, value] of Object.entries(rawPermission)) if (isSharedPermissionCategory(category)) shared[category] = typeof value === "string" ? { "*": value } : value;
20087
+ else overrideOnly[category] = value;
20088
+ const json = Object.keys(overrideOnly).length > 0 ? {
20089
+ permission: shared,
20090
+ opencode: { permission: overrideOnly }
20091
+ } : { permission: shared };
20092
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
19470
20093
  }
19471
20094
  validate() {
19472
20095
  try {
@@ -19496,10 +20119,15 @@ var OpencodePermissions = class OpencodePermissions extends ToolPermissions {
19496
20119
  validate: false
19497
20120
  });
19498
20121
  }
20122
+ /**
20123
+ * Normalize the uniform/undefined forms of OpenCode's `permission` field into
20124
+ * the canonical rulesync shape. The object form is handled directly in
20125
+ * `toRulesyncPermissions` (it needs to split shared vs OpenCode-only
20126
+ * categories), so this only covers the two remaining cases.
20127
+ */
19499
20128
  normalizePermission(permission) {
19500
20129
  if (!permission) return {};
19501
- if (typeof permission === "string") return { "*": { "*": permission } };
19502
- return Object.fromEntries(Object.entries(permission).map(([tool, value]) => [tool, typeof value === "string" ? { "*": value } : value]));
20130
+ return { "*": { "*": permission } };
19503
20131
  }
19504
20132
  };
19505
20133
  //#endregion
@@ -19573,6 +20201,24 @@ function buildQwenPermissionEntry(toolName, pattern) {
19573
20201
  if (pattern === "*") return toolName;
19574
20202
  return `${toolName}(${pattern})`;
19575
20203
  }
20204
+ const QWEN_OVERRIDE_TOOLS_KEYS = [
20205
+ "approvalMode",
20206
+ "autoAccept",
20207
+ "sandbox",
20208
+ "sandboxImage",
20209
+ "disabled"
20210
+ ];
20211
+ const QWEN_OVERRIDE_SECURITY_KEYS = ["folderTrust"];
20212
+ function asPlainRecord(value) {
20213
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20214
+ }
20215
+ /** Pick the override-managed keys out of a settings group into a fresh record. */
20216
+ function pickQwenOverrideKeys(group, keys) {
20217
+ const source = asPlainRecord(group);
20218
+ const picked = {};
20219
+ for (const key of keys) if (source[key] !== void 0) picked[key] = source[key];
20220
+ return picked;
20221
+ }
19576
20222
  var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19577
20223
  constructor(params) {
19578
20224
  super({
@@ -19638,6 +20284,15 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19638
20284
  ...settings,
19639
20285
  permissions: mergedPermissions
19640
20286
  };
20287
+ const override = config.qwencode;
20288
+ if (override?.tools !== void 0) merged.tools = {
20289
+ ...asPlainRecord(settings.tools),
20290
+ ...asPlainRecord(override.tools)
20291
+ };
20292
+ if (override?.security !== void 0) merged.security = {
20293
+ ...asPlainRecord(settings.security),
20294
+ ...asPlainRecord(override.security)
20295
+ };
19641
20296
  const fileContent = JSON.stringify(merged, null, 2);
19642
20297
  return new QwencodePermissions({
19643
20298
  outputRoot,
@@ -19663,7 +20318,14 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
19663
20318
  ask: permissions.ask ?? [],
19664
20319
  deny: permissions.deny ?? []
19665
20320
  });
19666
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
20321
+ const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
20322
+ const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
20323
+ const qwencodeOverride = {};
20324
+ if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
20325
+ if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
20326
+ const result = { ...config };
20327
+ if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
20328
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19667
20329
  }
19668
20330
  validate() {
19669
20331
  try {
@@ -19824,6 +20486,16 @@ function toPermissionsTable(value) {
19824
20486
  if (!value || typeof value !== "object" || Array.isArray(value)) return {};
19825
20487
  return { ...value };
19826
20488
  }
20489
+ const REASONIX_OVERRIDE_AGENT_KEYS = ["plan_mode_allowed_tools", "plan_mode_read_only_commands"];
20490
+ function asReasonixRecord(value) {
20491
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
20492
+ }
20493
+ function pickReasonixKeys(source, keys) {
20494
+ const record = asReasonixRecord(source);
20495
+ const picked = {};
20496
+ for (const key of keys) if (record[key] !== void 0) picked[key] = record[key];
20497
+ return picked;
20498
+ }
19827
20499
  var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19828
20500
  toml;
19829
20501
  constructor(params) {
@@ -19885,6 +20557,15 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19885
20557
  ...parsed,
19886
20558
  permissions: mergedPermissions
19887
20559
  };
20560
+ const override = config.reasonix;
20561
+ if (override?.sandbox !== void 0) merged.sandbox = {
20562
+ ...asReasonixRecord(parsed.sandbox),
20563
+ ...asReasonixRecord(override.sandbox)
20564
+ };
20565
+ if (override?.agent !== void 0) merged.agent = {
20566
+ ...asReasonixRecord(parsed.agent),
20567
+ ...asReasonixRecord(override.agent)
20568
+ };
19888
20569
  const fileContent = smolToml.stringify(merged);
19889
20570
  return new ReasonixPermissions({
19890
20571
  outputRoot,
@@ -19901,7 +20582,14 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
19901
20582
  ask: toStringArray$1(permissions.ask),
19902
20583
  deny: toStringArray$1(permissions.deny)
19903
20584
  });
19904
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
20585
+ const sandbox = asReasonixRecord(this.toml.sandbox);
20586
+ const agentPlanMode = pickReasonixKeys(this.toml.agent, REASONIX_OVERRIDE_AGENT_KEYS);
20587
+ const reasonixOverride = {};
20588
+ if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
20589
+ if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
20590
+ const result = { ...config };
20591
+ if (Object.keys(reasonixOverride).length > 0) result.reasonix = reasonixOverride;
20592
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
19905
20593
  }
19906
20594
  validate() {
19907
20595
  try {
@@ -20465,6 +21153,7 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20465
21153
  if (deny.size > 0) nextTool.denylist = [...deny].toSorted();
20466
21154
  tools[vibeToolName] = nextTool;
20467
21155
  }
21156
+ applyVibeSensitivePatterns(tools, rulesyncPermissions.getJson().vibe);
20468
21157
  config.tools = tools;
20469
21158
  if (enabledTools.size > 0) config.enabled_tools = [...enabledTools].toSorted();
20470
21159
  else delete config.enabled_tools;
@@ -20481,16 +21170,25 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20481
21170
  }
20482
21171
  toRulesyncPermissions() {
20483
21172
  const permission = {};
21173
+ const vibeOverridePermission = {};
20484
21174
  for (const tool of toStringArray(this.toml.enabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "allow";
20485
21175
  for (const tool of toStringArray(this.toml.disabled_tools)) ensurePermission(permission, toCanonicalToolName$1(tool))["*"] = "deny";
20486
21176
  for (const [vibeToolName, toolConfig] of Object.entries(toVibeToolsRecord(this.toml.tools))) {
20487
- const rules = ensurePermission(permission, toCanonicalToolName$1(vibeToolName));
21177
+ const category = toCanonicalToolName$1(vibeToolName);
21178
+ const rules = ensurePermission(permission, category);
20488
21179
  const action = fromVibePermission(toolConfig.permission);
20489
21180
  if (action !== void 0) rules["*"] = action;
20490
21181
  for (const pattern of toStringArray(toolConfig.allow ?? toolConfig.allowlist)) rules[pattern] = "allow";
20491
21182
  for (const pattern of toStringArray(toolConfig.deny ?? toolConfig.denylist)) rules[pattern] = "deny";
21183
+ const sensitivePatterns = toStringArray(toolConfig.sensitive_patterns);
21184
+ if (sensitivePatterns.length > 0) vibeOverridePermission[category] = { sensitive_patterns: sensitivePatterns };
20492
21185
  }
20493
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
21186
+ for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
21187
+ const json = Object.keys(vibeOverridePermission).length > 0 ? {
21188
+ permission,
21189
+ vibe: { permission: vibeOverridePermission }
21190
+ } : { permission };
21191
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(json, null, 2) });
20494
21192
  }
20495
21193
  validate() {
20496
21194
  try {
@@ -20517,6 +21215,22 @@ var VibePermissions = class VibePermissions extends ToolPermissions {
20517
21215
  });
20518
21216
  }
20519
21217
  };
21218
+ /**
21219
+ * Apply the Vibe-scoped override's per-tool `sensitive_patterns` (patterns that
21220
+ * escalate to ASK even when the base permission is ALWAYS). rulesync owns this
21221
+ * list for any category the override names: a present list is set, an empty
21222
+ * one clears it. Categories not named keep whatever the existing file had.
21223
+ */
21224
+ function applyVibeSensitivePatterns(tools, vibeOverride) {
21225
+ for (const [category, toolOverride] of Object.entries(vibeOverride?.permission ?? {})) {
21226
+ const vibeToolName = toVibeToolName(category);
21227
+ const nextTool = toVibeToolConfig(tools[vibeToolName]);
21228
+ const patterns = toStringArray(toolOverride.sensitive_patterns);
21229
+ if (patterns.length > 0) nextTool.sensitive_patterns = [...patterns].toSorted();
21230
+ else delete nextTool.sensitive_patterns;
21231
+ tools[vibeToolName] = nextTool;
21232
+ }
21233
+ }
20520
21234
  function parseVibeConfig(fileContent) {
20521
21235
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
20522
21236
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -20561,6 +21275,11 @@ function ensurePermission(permission, category) {
20561
21275
  const WARP_GLOBAL_ONLY_MESSAGE = "Warp permissions are global-only; use --global to sync Warp's settings.toml";
20562
21276
  const ALLOWLIST_KEY = "agent_mode_command_execution_allowlist";
20563
21277
  const DENYLIST_KEY = "agent_mode_command_execution_denylist";
21278
+ const WARP_OVERRIDE_KEYS = [
21279
+ "agent_mode_coding_permissions",
21280
+ "agent_mode_coding_file_read_allowlist",
21281
+ "agent_mode_execute_readonly_commands"
21282
+ ];
20564
21283
  /**
20565
21284
  * Warp's `settings.toml` lives in a different directory per platform (Stable
20566
21285
  * channel). The home directory is resolved by the processor through
@@ -20596,8 +21315,16 @@ function warpSettingsDir() {
20596
21315
  * patterns as regexes when targeting Warp (mirrors the Zed permissions
20597
21316
  * adapter). Warp has no per-command "ask" list, so `ask` rules are dropped; and
20598
21317
  * the command lists only model shell commands, so non-`bash` categories are
20599
- * skipped (with a warning when they carry `deny` rules). MCP allow/deny and the
20600
- * file-read permissions are separate surfaces not modeled here.
21318
+ * skipped (with a warning when they carry `deny` rules).
21319
+ *
21320
+ * Warp's `[agents.profiles]` table also exposes file-read/read-only autonomy
21321
+ * knobs that do not fit the canonical `allow | ask | deny` per-command model:
21322
+ * `agent_mode_coding_permissions`, `agent_mode_coding_file_read_allowlist`, and
21323
+ * `agent_mode_execute_readonly_commands`. These are authored and round-tripped
21324
+ * through the `warp` override namespace (see `WarpPermissionsOverrideSchema`):
21325
+ * on **import** they are lifted from `settings.toml` into the override, and on
21326
+ * **export** they are merged back into `[agents.profiles]` (the override wins).
21327
+ * MCP allow/deny is a separate surface not modeled here.
20601
21328
  *
20602
21329
  * The `settings.toml` file holds all of Warp's settings, so the
20603
21330
  * `[agents.profiles]` block is merged in place and the file is never deleted.
@@ -20642,12 +21369,15 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
20642
21369
  } catch (error) {
20643
21370
  throw new Error(`Failed to parse existing Warp settings at ${filePath}: ${formatError(error)}`, { cause: error });
20644
21371
  }
21372
+ const config = rulesyncPermissions.getJson();
20645
21373
  const { allow, deny } = convertRulesyncToWarpPermissions({
20646
- config: rulesyncPermissions.getJson(),
21374
+ config,
20647
21375
  logger
20648
21376
  });
20649
21377
  const agents = isRecord(settings.agents) ? { ...settings.agents } : {};
20650
21378
  const profiles = isRecord(agents.profiles) ? { ...agents.profiles } : {};
21379
+ const override = config.warp;
21380
+ if (isRecord(override)) Object.assign(profiles, override);
20651
21381
  const mergedAllow = uniq(allow.toSorted());
20652
21382
  const mergedDeny = uniq(deny.toSorted());
20653
21383
  if (mergedAllow.length > 0) profiles[ALLOWLIST_KEY] = mergedAllow;
@@ -20678,7 +21408,11 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
20678
21408
  allow: isStringArray(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
20679
21409
  deny: isStringArray(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
20680
21410
  });
20681
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
21411
+ const warpOverride = {};
21412
+ for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
21413
+ const result = { ...config };
21414
+ if (Object.keys(warpOverride).length > 0) result.warp = warpOverride;
21415
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
20682
21416
  }
20683
21417
  validate() {
20684
21418
  return {
@@ -24044,8 +24778,9 @@ const DevinSkillFrontmatterSchema = z.looseObject({
24044
24778
  * Represents a Devin (now Devin Desktop) skill directory.
24045
24779
  * Devin supports skills in both project mode under .devin/skills/
24046
24780
  * (preferred since the Devin Desktop rebrand; .devin/skills/ is the legacy
24047
- * fallback the tool still reads) and global mode under ~/.codeium/windsurf/skills/
24048
- * (unchanged by the rebrand).
24781
+ * fallback the tool still reads) and global mode under the Devin-native
24782
+ * ~/.config/devin/skills/ (consistent with Devin's global agents/rules paths;
24783
+ * the legacy channel-dependent ~/.codeium/<channel>/skills/ is no longer emitted).
24049
24784
  */
24050
24785
  var DevinSkill = class DevinSkill extends ToolSkill {
24051
24786
  constructor({ outputRoot = process.cwd(), relativeDirPath = DevinSkill.getSettablePaths().relativeDirPath, dirName, frontmatter, body, otherFiles = [], validate = true, global = false }) {
@@ -24067,7 +24802,7 @@ var DevinSkill = class DevinSkill extends ToolSkill {
24067
24802
  }
24068
24803
  }
24069
24804
  static getSettablePaths({ global = false } = {}) {
24070
- if (global) return { relativeDirPath: CODEIUM_WINDSURF_SKILLS_DIR_PATH };
24805
+ if (global) return { relativeDirPath: DEVIN_GLOBAL_SKILLS_DIR_PATH };
24071
24806
  return { relativeDirPath: DEVIN_SKILLS_DIR_PATH };
24072
24807
  }
24073
24808
  getFrontmatter() {
@@ -27126,7 +27861,7 @@ const QwencodeSubagentFrontmatterSchema = z.looseObject({
27126
27861
  disallowedTools: z.optional(z.array(z.string())),
27127
27862
  maxTurns: z.optional(z.number()),
27128
27863
  color: z.optional(z.string()),
27129
- mcpServers: z.optional(z.array(z.string())),
27864
+ mcpServers: z.optional(z.union([z.array(z.string()), z.record(z.string(), z.looseObject({}))])),
27130
27865
  hooks: z.optional(z.unknown())
27131
27866
  });
27132
27867
  var QwencodeSubagent = class QwencodeSubagent extends ToolSubagent {
@@ -37107,4 +37842,4 @@ async function importPermissionsCore(params) {
37107
37842
  //#endregion
37108
37843
  export { CLIError as $, IgnoreProcessor as A, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as At, toolCommandFactories as B, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Bt, PermissionsProcessorToolTargetSchema as C, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Ct, McpProcessorToolTargetSchema as D, RULESYNC_HOOKS_FILE_NAME as Dt, McpProcessor as E, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Et, HooksProcessorToolTargetSchema as F, RULESYNC_PERMISSIONS_FILE_NAME as Ft, CLAUDECODE_SKILLS_DIR_PATH as G, CLAUDECODE_LOCAL_RULE_FILE_NAME as H, formatError as Ht, toolHooksFactories as I, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as It, stringifyFrontmatter as J, RulesyncCommand as K, RulesyncHooks as L, RULESYNC_RELATIVE_DIR_PATH as Lt, toolIgnoreFactories as M, RULESYNC_MCP_RELATIVE_FILE_PATH as Mt, RulesyncIgnore as N, RULESYNC_MCP_SCHEMA_URL as Nt, toolMcpFactories as O, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Ot, HooksProcessor as P, RULESYNC_OVERVIEW_FILE_NAME as Pt, JsonLogger as Q, CommandsProcessor as R, RULESYNC_RULES_RELATIVE_DIR_PATH as Rt, PermissionsProcessor as S, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as St, RulesyncPermissions as T, RULESYNC_CONFIG_SCHEMA_URL as Tt, CLAUDECODE_MEMORIES_DIR_NAME as U, ALL_FEATURES as Ut, CLAUDECODE_DIR as V, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Vt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as W, ALL_FEATURES_WITH_WILDCARD as Wt, findControlCharacter as X, ConfigResolver as Y, ConsoleLogger as Z, toolSkillFactories as _, ALL_TOOL_TARGETS as _t, RulesProcessor as a, fileExists as at, RulesyncSkillFrontmatterSchema as b, MAX_FILE_SIZE as bt, RulesyncRule as c, getHomeDirectory as ct, SubagentsProcessorToolTargetSchema as d, readFileContent as dt, ErrorCodes as et, toolSubagentFactories as f, removeDirectory as ft, SkillsProcessorToolTargetSchema as g, writeFileContent as gt, SkillsProcessor as h, toPosixPath as ht, convertFromTool as i, ensureDir as it, IgnoreProcessorToolTargetSchema as j, RULESYNC_MCP_FILE_NAME as jt, RulesyncMcp as k, RULESYNC_IGNORE_RELATIVE_FILE_PATH as kt, RulesyncRuleFrontmatterSchema as l, isSymlink as lt, RulesyncSubagentFrontmatterSchema as m, removeTempDirectory as mt, checkRulesyncDirExists as n, createTempDirectory as nt, RulesProcessorToolTargetSchema as o, findFilesByGlobs as ot, RulesyncSubagent as p, removeFile as pt, RulesyncCommandFrontmatterSchema as q, generate as r, directoryExists as rt, toolRuleFactories as s, getFileSize as st, importFromTool as t, checkPathTraversal as tt, SubagentsProcessor as u, listDirectoryFiles as ut, getLocalSkillDirNames as v, ALL_TOOL_TARGETS_WITH_WILDCARD as vt, toolPermissionsFactories as w, RULESYNC_CONFIG_RELATIVE_FILE_PATH as wt, SKILL_FILE_NAME as x, RULESYNC_AIIGNORE_FILE_NAME as xt, RulesyncSkill as y, ToolTargetSchema as yt, CommandsProcessorToolTargetSchema as z, RULESYNC_SKILLS_RELATIVE_DIR_PATH as zt };
37109
37844
 
37110
- //# sourceMappingURL=import-BkxwFCDM.js.map
37845
+ //# sourceMappingURL=import-B7VzSVUK.js.map