rulesync 16.23.0 → 16.24.1

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.
@@ -6658,10 +6658,25 @@ const CursorPermissionsOverrideSchema = zod_mini.z.looseObject({
6658
6658
  * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
6659
6659
  * autonomy/sandbox controls with no canonical permission category — under
6660
6660
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
6661
- * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`,
6662
- * `allowedHttpHookUrls`, `allowPrivateNetworkHooks` the latter is honored by
6663
- * Qwen Code only in user/system settings, so generate skips it in project scope).
6664
- * It also
6661
+ * `sandbox`, `sandboxImage`, `disabled`, `visible`, `listDirectory`,
6662
+ * `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
6663
+ * `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
6664
+ * `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
6665
+ * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
6666
+ * skips those three in project scope and announces a granting value in global
6667
+ * scope. `security.allowedHttpHookUrls` (honored in a workspace only while no
6668
+ * higher scope sets it) and `security.folderTrust` (the initial trust decision
6669
+ * is made from user/system settings alone, before the workspace merge) are
6670
+ * written in both scopes, with a note in project scope and an announcement of
6671
+ * any global change. Every other key is honored in either scope, so a write that
6672
+ * changes what the file said is reported in either scope, naming what that key
6673
+ * decides there — the autonomy and containment
6674
+ * controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
6675
+ * registry controls (`disabled`, `visible`, `listDirectory`), the Auto Mode
6676
+ * classifier config, and, because these groups are loose objects, any key
6677
+ * rulesync does not model. Import flags a scope-dependent key only when it read
6678
+ * the project file, since that is the value a `--global` regenerate would
6679
+ * promote. It also
6665
6680
  * exposes `permissions.autoMode` (the Auto Mode classifier config:
6666
6681
  * `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` — see
6667
6682
  * https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
@@ -22812,8 +22827,10 @@ function definitionsToHermesEntries({ event, sourceEvent = event, definitions, l
22812
22827
  for (const definition of definitions) {
22813
22828
  if ((definition.type ?? "command") !== "command" || typeof definition.command !== "string" || definition.command === "") continue;
22814
22829
  const entry = { command: definition.command };
22815
- if (typeof definition.matcher === "string" && definition.matcher !== "") if (supportsMatcher) entry.matcher = definition.matcher;
22816
- else logger?.warn(`matcher "${definition.matcher}" on "${sourceEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
22830
+ if (typeof definition.matcher === "string" && definition.matcher !== "") {
22831
+ if (supportsMatcher) entry.matcher = definition.matcher;
22832
+ else logger?.warn(`matcher "${definition.matcher}" on "${sourceEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
22833
+ }
22817
22834
  if (typeof definition.timeout === "number") entry.timeout = definition.timeout;
22818
22835
  if (typeof definition.failClosed === "boolean") {
22819
22836
  if (event === HERMESAGENT_FAIL_CLOSED_EVENT) entry.fail_closed = definition.failClosed;
@@ -24041,8 +24058,10 @@ function canonicalToKiroHooks({ config, logger }) {
24041
24058
  key: eventName
24042
24059
  }) ?? eventName;
24043
24060
  const entries = buildKiroEntriesForEvent(definitions);
24044
- if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
24045
- else kiro[kiroEventName] = entries;
24061
+ if (entries.length > 0) {
24062
+ if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
24063
+ else kiro[kiroEventName] = entries;
24064
+ }
24046
24065
  }
24047
24066
  return kiro;
24048
24067
  }
@@ -24868,8 +24887,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
24868
24887
  if ((def.type ?? "command") !== "command") continue;
24869
24888
  if (typeof def.command !== "string") continue;
24870
24889
  const entry = { command: def.command };
24871
- if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
24872
- else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
24890
+ if (typeof def.matcher === "string" && def.matcher !== "") {
24891
+ if (isMatcherEvent) entry.match = def.matcher;
24892
+ else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
24893
+ }
24873
24894
  if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
24874
24895
  if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
24875
24896
  entries.push(entry);
@@ -28437,12 +28458,16 @@ function convertFromCodexFormat(codexMcp) {
28437
28458
  } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
28438
28459
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
28439
28460
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
28440
- if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28441
- else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
28461
+ if (mappedKey) {
28462
+ if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28463
+ else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
28464
+ }
28442
28465
  } else if (Object.hasOwn(CODEX_TO_RULESYNC_SCALAR_FIELD_MAP, key)) {
28443
28466
  const mappedKey = CODEX_TO_RULESYNC_SCALAR_FIELD_MAP[key];
28444
- if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
28445
- else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
28467
+ if (mappedKey) {
28468
+ if (typeof value === "string") converted[mappedKey] = value;
28469
+ else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
28470
+ }
28446
28471
  } else converted[key] = value;
28447
28472
  }
28448
28473
  restateCanonicalTransport(converted);
@@ -28474,12 +28499,16 @@ function convertToCodexFormat(mcpServers) {
28474
28499
  } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
28475
28500
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
28476
28501
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
28477
- if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28478
- else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
28502
+ if (mappedKey) {
28503
+ if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28504
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
28505
+ }
28479
28506
  } else if (Object.hasOwn(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP, key)) {
28480
28507
  const mappedKey = RULESYNC_TO_CODEX_SCALAR_FIELD_MAP[key];
28481
- if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
28482
- else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
28508
+ if (mappedKey) {
28509
+ if (typeof value === "string") converted[mappedKey] = value;
28510
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
28511
+ }
28483
28512
  } else converted[key] = value;
28484
28513
  }
28485
28514
  const previousName = originalNames.get(codexName);
@@ -28723,8 +28752,10 @@ function resolveRemoteMcpUrl(serverConfig) {
28723
28752
  /** The `command` array a `local` server is spawned with, `args` merged in. */
28724
28753
  function resolveLocalMcpCommand(serverConfig) {
28725
28754
  const commandArray = [];
28726
- if (serverConfig.command) if (Array.isArray(serverConfig.command)) commandArray.push(...serverConfig.command);
28727
- else commandArray.push(serverConfig.command);
28755
+ if (serverConfig.command) {
28756
+ if (Array.isArray(serverConfig.command)) commandArray.push(...serverConfig.command);
28757
+ else commandArray.push(serverConfig.command);
28758
+ }
28728
28759
  if (serverConfig.args) commandArray.push(...serverConfig.args);
28729
28760
  return commandArray;
28730
28761
  }
@@ -29261,8 +29292,10 @@ function toDeepagentsServer({ name, server, logger }) {
29261
29292
  const { enabledTools, disabledTools, type: _type, transport, ...rest } = server;
29262
29293
  const converted = { ...rest };
29263
29294
  const normalized = normalizeDeepagentsTransport(rawTransport);
29264
- if (normalized !== void 0) if (transport !== void 0) converted.transport = normalized;
29265
- else converted.type = normalized;
29295
+ if (normalized !== void 0) {
29296
+ if (transport !== void 0) converted.transport = normalized;
29297
+ else converted.type = normalized;
29298
+ }
29266
29299
  if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
29267
29300
  toolName: TOOL_NAME,
29268
29301
  serverName: name,
@@ -29277,8 +29310,10 @@ function toDeepagentsServer({ name, server, logger }) {
29277
29310
  logger
29278
29311
  });
29279
29312
  converted.allowedTools = enabledTools;
29280
- } else if (disabledTools !== void 0) if (disabledTools.length === 0) logger?.warn(`${TOOL_NAME} MCP: dropping the empty disabledTools list on "${name}"; it denies nothing, and deepagents rejects the empty form.`);
29281
- else converted.disabledTools = disabledTools;
29313
+ } else if (disabledTools !== void 0) {
29314
+ if (disabledTools.length === 0) logger?.warn(`${TOOL_NAME} MCP: dropping the empty disabledTools list on "${name}"; it denies nothing, and deepagents rejects the empty form.`);
29315
+ else converted.disabledTools = disabledTools;
29316
+ }
29282
29317
  return converted;
29283
29318
  }
29284
29319
  /** Lift dcode's spellings back into the canonical model. */
@@ -32215,14 +32250,16 @@ function rulesyncMcpServerToReasonix(name, server, logger) {
32215
32250
  name,
32216
32251
  ...type !== void 0 && { type }
32217
32252
  };
32218
- if (server.command !== void 0) if (Array.isArray(server.command)) {
32219
- const [command, ...commandArgs] = server.command;
32220
- if (command !== void 0) plugin.command = command;
32221
- const args = [...commandArgs, ...server.args ?? []];
32222
- if (args.length > 0) plugin.args = args;
32223
- } else {
32224
- plugin.command = server.command;
32225
- if (server.args !== void 0) plugin.args = server.args;
32253
+ if (server.command !== void 0) {
32254
+ if (Array.isArray(server.command)) {
32255
+ const [command, ...commandArgs] = server.command;
32256
+ if (command !== void 0) plugin.command = command;
32257
+ const args = [...commandArgs, ...server.args ?? []];
32258
+ if (args.length > 0) plugin.args = args;
32259
+ } else {
32260
+ plugin.command = server.command;
32261
+ if (server.args !== void 0) plugin.args = server.args;
32262
+ }
32226
32263
  }
32227
32264
  for (const field of REASONIX_PLUGIN_FIELDS) {
32228
32265
  if (field === "type" || field === "command" || field === "args") continue;
@@ -33290,14 +33327,16 @@ function rulesyncMcpServerToVibe(name, server, existing) {
33290
33327
  name,
33291
33328
  ...transport !== void 0 && { transport }
33292
33329
  };
33293
- if (server.command !== void 0) if (Array.isArray(server.command)) {
33294
- const [command, ...commandArgs] = server.command;
33295
- if (command !== void 0) vibeServer.command = command;
33296
- const args = [...commandArgs, ...server.args ?? []];
33297
- if (args.length > 0) vibeServer.args = args;
33298
- } else {
33299
- vibeServer.command = server.command;
33300
- if (server.args !== void 0) vibeServer.args = server.args;
33330
+ if (server.command !== void 0) {
33331
+ if (Array.isArray(server.command)) {
33332
+ const [command, ...commandArgs] = server.command;
33333
+ if (command !== void 0) vibeServer.command = command;
33334
+ const args = [...commandArgs, ...server.args ?? []];
33335
+ if (args.length > 0) vibeServer.args = args;
33336
+ } else {
33337
+ vibeServer.command = server.command;
33338
+ if (server.args !== void 0) vibeServer.args = server.args;
33339
+ }
33301
33340
  }
33302
33341
  const hasStructuredAuth = serverRecord.auth !== void 0;
33303
33342
  for (const field of VIBE_MCP_SERVER_FIELDS) {
@@ -42504,15 +42543,129 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
42504
42543
  "sandbox",
42505
42544
  "sandboxImage",
42506
42545
  "disabled",
42507
- "visible"
42546
+ "visible",
42547
+ "listDirectory",
42548
+ "workflowsEnabled"
42508
42549
  ];
42509
42550
  const QWEN_OVERRIDE_SECURITY_KEYS = [
42510
42551
  "folderTrust",
42511
42552
  "allowedHttpHookUrls",
42512
- "allowPrivateNetworkHooks"
42553
+ "allowPrivateNetworkHooks",
42554
+ "allowedInsecureVoiceBaseUrls"
42513
42555
  ];
42514
- const QWEN_GLOBAL_ONLY_SECURITY_KEYS = ["allowPrivateNetworkHooks"];
42556
+ /**
42557
+ * What each rule means for the two directions, so a key's behavior is declared
42558
+ * once beside its rule rather than spelled out at every branch.
42559
+ *
42560
+ * `stripInProjectScope` separates the keys a project file cannot use at all from
42561
+ * the ones whose project value still does something, just not unconditionally —
42562
+ * only the former are dropped. `announceOnlyGrants` marks the rule whose risk is
42563
+ * a value that turns something on; for the others the dangerous value is often
42564
+ * the falsy one (an empty allow-all list, folder trust switched off, a sandbox
42565
+ * turned off), so every change is announced instead. `announceOnImport` marks
42566
+ * the keys whose meaning depends on the scope they are written in — importing
42567
+ * one and regenerating globally turns a value a workspace could not decide for
42568
+ * itself into one Qwen Code enforces everywhere. `global-machine-wide` keys are
42569
+ * honored identically in either scope, so import neither promotes nor weakens
42570
+ * them and the announcement on the generate side is the control point.
42571
+ * `projectNote` is `null` for a rule that has nothing to say about the project
42572
+ * scope. A `globalNote` here is the rule's default; a key whose meaning the
42573
+ * default does not describe carries its own.
42574
+ */
42575
+ const QWEN_SCOPE_RULES = {
42576
+ "workspace-stripped": {
42577
+ stripInProjectScope: true,
42578
+ announceOnlyGrants: true,
42579
+ announceOnImport: true,
42580
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} is only honored in user/system settings, so it is skipped for the project-scoped ${filePath} (a value already in that file is left as it is). Check it if the override arrived with a repository you cloned, and author it in the global scope only if that is a value you want every project on this machine to run under.`,
42581
+ globalNote: "Qwen Code ignores this key in workspace settings so a repository cannot grant it per project; in the global scope it applies to every project on this machine."
42582
+ },
42583
+ "workspace-non-overriding": {
42584
+ stripInProjectScope: false,
42585
+ announceOnlyGrants: false,
42586
+ announceOnImport: true,
42587
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, but Qwen Code honors a workspace value for it only while no user, system, or system-defaults scope sets the key — a repository cannot replace the list a user configured in a higher scope. It does replace one written by hand in this same file, and an empty list is Qwen Code's allow-all, so check what it now says. Author it in the global scope if it has to apply unconditionally.`,
42588
+ globalNote: "A global value outranks every repository's own list for this key, and an empty list means allow-all, so this decides where HTTP hooks may send data for every project on this machine."
42589
+ },
42590
+ "user-scope-trust-check": {
42591
+ stripInProjectScope: false,
42592
+ announceOnlyGrants: false,
42593
+ announceOnImport: true,
42594
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, but Qwen Code makes the initial trust decision from user and system settings alone, before a workspace file is merged, so a project value cannot decide whether this workspace is trusted (it still drives folder trust once the workspace is trusted). Check it if the override arrived with a repository you cloned, and author it in the global scope only if that is a value you want every project on this machine to run under.`,
42595
+ globalNote: "Qwen Code reads this key from the global scope when it decides whether a workspace is trusted, so this changes which projects on this machine it trusts."
42596
+ },
42597
+ "global-machine-wide": {
42598
+ stripInProjectScope: false,
42599
+ announceOnlyGrants: false,
42600
+ announceOnImport: false,
42601
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code honors this key in a workspace file, so it takes effect while you work in this repository. Check it if the override arrived with a repository you cloned.`,
42602
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope it settles how much the agent may do unattended — how far approvals are skipped, and whether tool calls are contained at all — for every project on this machine."
42603
+ },
42604
+ unmodeled: {
42605
+ stripInProjectScope: false,
42606
+ announceOnlyGrants: false,
42607
+ announceOnImport: false,
42608
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. This is not a key rulesync models, so it cannot say what Qwen Code does with it — and some settings in these groups are potent (\`tools.discoveryCommand\` and \`tools.callCommand\` are spawned as commands when Qwen Code starts). Check it if the override arrived with a repository you cloned.`,
42609
+ globalNote: "This is not a key rulesync models, so it cannot say what Qwen Code does with it — and some settings in these groups are potent (`tools.discoveryCommand` and `tools.callCommand` are spawned as commands when Qwen Code starts). Check what it now says for every project on this machine."
42610
+ }
42611
+ };
42612
+ const QWEN_OVERRIDE_GROUPS = [{
42613
+ groupName: "tools",
42614
+ overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
42615
+ scopedKeys: {
42616
+ workflowsEnabled: { rule: "workspace-stripped" },
42617
+ approvalMode: {
42618
+ rule: "global-machine-wide",
42619
+ projectNote: autonomyProjectNote
42620
+ },
42621
+ autoAccept: {
42622
+ rule: "global-machine-wide",
42623
+ projectNote: autonomyProjectNote
42624
+ },
42625
+ sandbox: {
42626
+ rule: "global-machine-wide",
42627
+ projectNote: autonomyProjectNote
42628
+ },
42629
+ sandboxImage: {
42630
+ rule: "global-machine-wide",
42631
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so a sandboxed run in this repository executes inside the named image — check that it is one you trust to run your code.`,
42632
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope every sandboxed run on this machine executes inside the named image — check that it is one you trust to run your code."
42633
+ },
42634
+ disabled: {
42635
+ rule: "global-machine-wide",
42636
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the list in that file rather than adding to it, so a name that is gone is a tool the model can call again in this repository unless another scope still disables it.`,
42637
+ globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the list in this file rather than adding to it, so in the global scope this decides which tools stay out of the registry for every project on this machine — dropping a name hands that tool back to the model unless another scope still disables it."
42638
+ },
42639
+ visible: {
42640
+ rule: "global-machine-wide",
42641
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides which deferred tools are visible at startup in this repository.`,
42642
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides which deferred tools are visible at startup for every project on this machine."
42643
+ },
42644
+ listDirectory: {
42645
+ rule: "global-machine-wide",
42646
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, so it decides whether the built-in \`list_directory\` tool is registered in this repository.`,
42647
+ globalNote: "Qwen Code honors this key wherever it is written, so in the global scope this decides whether the built-in `list_directory` tool is registered for every project on this machine."
42648
+ }
42649
+ }
42650
+ }, {
42651
+ groupName: "security",
42652
+ overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
42653
+ scopedKeys: {
42654
+ allowPrivateNetworkHooks: { rule: "workspace-stripped" },
42655
+ allowedInsecureVoiceBaseUrls: {
42656
+ rule: "workspace-stripped",
42657
+ grants: isNonEmptyArray
42658
+ },
42659
+ allowedHttpHookUrls: { rule: "workspace-non-overriding" },
42660
+ folderTrust: { rule: "user-scope-trust-check" }
42661
+ }
42662
+ }];
42515
42663
  const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
42664
+ const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
42665
+ rule: "global-machine-wide",
42666
+ projectNote: ({ qualifiedKey, quotedValue, filePath }) => `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}, replacing the whole block rather than merging into it, so it rewrites the instructions Auto Mode's classifier follows in this repository — including any deny hint you had configured.`,
42667
+ globalNote: "Qwen Code honors this key wherever it is written, and the override replaces the whole block rather than merging into it, so in the global scope this rewrites the instructions Auto Mode's classifier follows — including any deny hint you had configured — for every project on this machine."
42668
+ } };
42516
42669
  function asPlainRecord(value) {
42517
42670
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
42518
42671
  }
@@ -42524,20 +42677,172 @@ function pickQwenOverrideKeys(group, keys) {
42524
42677
  return picked;
42525
42678
  }
42526
42679
  /**
42527
- * Drop the global-only `security` keys from the override when generating project
42528
- * settings, warning once per dropped key. Only the override copy is filtered, so
42529
- * a value the user already wrote into the project file stays untouched.
42680
+ * Whether a value written for a scoped key actually grants something. Qwen Code
42681
+ * reads the boolean ones with a plain truthiness check, so anything truthy turns
42682
+ * the capability on — `1` and the string `"false"` included, which is why this is
42683
+ * not a `=== true` test. Not all of them are booleans either:
42684
+ * `security.allowedInsecureVoiceBaseUrls` is a list of base URLs, and an empty
42685
+ * list grants nothing.
42686
+ *
42687
+ * Only the rules whose `announceOnlyGrants` is set consult this, because "empty"
42688
+ * does not mean the same thing for every list: an empty
42689
+ * `security.allowedHttpHookUrls` is Qwen Code's allow-all, the widest value there
42690
+ * is, so that key is announced whatever it says.
42691
+ */
42692
+ function isNonEmptyArray(value) {
42693
+ return Array.isArray(value) && value.length > 0;
42694
+ }
42695
+ /**
42696
+ * Whether a value turns something on, read the way Qwen Code reads it.
42697
+ *
42698
+ * Plain truthiness by default, because that is what Qwen Code applies to these
42699
+ * settings: `1` and the string `"false"` enable them, and so does an empty array
42700
+ * — which is why an empty list cannot be treated as harmless everywhere. A key
42701
+ * whose list Qwen Code matches against, rather than tests for truth, carries its
42702
+ * own `grants` instead.
42703
+ */
42704
+ function grantsSomething(value, scopedKey) {
42705
+ return (scopedKey?.grants ?? Boolean)(value);
42706
+ }
42707
+ /**
42708
+ * The autonomy and containment controls say the same thing in project scope, so
42709
+ * they share one note rather than repeating it three times.
42710
+ */
42711
+ function autonomyProjectNote({ qualifiedKey, quotedValue, filePath }) {
42712
+ return `${qualifiedKey} = ${quotedValue} was written to the project-scoped ${filePath}. Qwen Code honors this key in a workspace file, so it settles how far approvals are skipped and whether tool calls are contained while you work in this repository. Check it if the override arrived with a repository you cloned.`;
42713
+ }
42714
+ /**
42715
+ * Quote a `group.key` pair for a warning. The key goes through the same quoting
42716
+ * a value does, because it is one: the override's groups are `z.looseObject`s,
42717
+ * so a key the scope gate names may have been written by whoever authored the
42718
+ * `.rulesync/permissions.jsonc` rather than chosen from a list rulesync knows.
42719
+ * Serializing it — rather than wrapping it in quotes of our own — is what keeps
42720
+ * a key that contains a quote from closing it and continuing the sentence.
42721
+ */
42722
+ function quoteQualifiedKey({ groupName, key }) {
42723
+ return quoteValueForWarning(`${groupName}.${key}`);
42724
+ }
42725
+ /**
42726
+ * Compare two settings values without letting key order decide the answer, so
42727
+ * re-emitting `{ enabled: true, note: "x" }` as `{ note: "x", enabled: true }`
42728
+ * is not announced as a change.
42530
42729
  */
42531
- function scopeOverrideSecurity(overrideSecurity, { global, relativeFilePath, logger }) {
42532
- const scoped = { ...asPlainRecord(overrideSecurity) };
42533
- if (global) return scoped;
42534
- for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
42535
- if (scoped[key] === void 0) continue;
42536
- delete scoped[key];
42537
- logger?.warn(`Qwen permissions: 'security.${key}' is only honored in user/system settings, so it is skipped for the project-scoped ${relativeFilePath}. Author it in the global scope instead.`);
42730
+ function stableStringify(value) {
42731
+ return JSON.stringify(value, (_key, nested) => nested !== null && typeof nested === "object" && !Array.isArray(nested) ? Object.fromEntries(Object.entries(nested).toSorted(([left], [right]) => left.localeCompare(right))) : nested) ?? "undefined";
42732
+ }
42733
+ function sameSettingsValue(a, b) {
42734
+ return stableStringify(a) === stableStringify(b);
42735
+ }
42736
+ /**
42737
+ * Apply the scope rules for a settings group's scoped keys.
42738
+ *
42739
+ * Generating project settings drops the keys Qwen Code strips from a workspace
42740
+ * file before the merge — writing one there would be dead configuration — and
42741
+ * explains the keys whose project value is honored only conditionally rather
42742
+ * than dropping those. Either way only the override copy is touched, so a value
42743
+ * the user already wrote into the project file stays where it is.
42744
+ *
42745
+ * Generating global settings keeps every key, and announces the write: the
42746
+ * `.rulesync/permissions.jsonc` carrying it may have arrived with a cloned or
42747
+ * fetched repository, and the global file applies to every project on this
42748
+ * machine. So the new value is named alongside the one it replaces, exactly as
42749
+ * the deepagents startup override announces its own relaxations. Every key the
42750
+ * override writes is announced, not only the ones with a scope rule — the
42751
+ * groups are `z.looseObject`s, so an unmodeled key falls back to the
42752
+ * `unmodeled` rule rather than slipping past the gate that the modeled ones
42753
+ * pass through.
42754
+ */
42755
+ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGroup, global, filePath, logger }) {
42756
+ const scoped = { ...asPlainRecord(overrideGroup) };
42757
+ const previous = asPlainRecord(existingGroup);
42758
+ const rulesByKey = new Map(Object.entries(scopedKeys));
42759
+ for (const key of Object.keys(scoped)) {
42760
+ const value = scoped[key];
42761
+ if (value === void 0) continue;
42762
+ const scopedKey = rulesByKey.get(key);
42763
+ const rule = scopedKey?.rule ?? "unmodeled";
42764
+ const { stripInProjectScope, announceOnlyGrants } = QWEN_SCOPE_RULES[rule];
42765
+ const projectNote = scopedKey?.projectNote ?? QWEN_SCOPE_RULES[rule].projectNote;
42766
+ const globalNote = scopedKey?.globalNote ?? QWEN_SCOPE_RULES[rule].globalNote;
42767
+ const previousValue = Object.hasOwn(previous, key) ? previous[key] : void 0;
42768
+ const unchanged = sameSettingsValue(previousValue, value);
42769
+ if (!global) {
42770
+ if (stripInProjectScope) delete scoped[key];
42771
+ else if (unchanged) continue;
42772
+ if (projectNote) warnWithFallback(logger, `Qwen permissions: ${projectNote({
42773
+ qualifiedKey: quoteQualifiedKey({
42774
+ groupName,
42775
+ key
42776
+ }),
42777
+ quotedValue: quoteValueForWarning(value),
42778
+ filePath
42779
+ })}`);
42780
+ continue;
42781
+ }
42782
+ if (unchanged) continue;
42783
+ if (announceOnlyGrants && !grantsSomething(value, scopedKey)) continue;
42784
+ const replaced = previousValue === void 0 ? "" : ` (was ${quoteValueForWarning(previousValue)})`;
42785
+ warnWithFallback(logger, `Qwen permissions: the qwencode override wrote ${quoteQualifiedKey({
42786
+ groupName,
42787
+ key
42788
+ })} = ${quoteValueForWarning(value)}${replaced} into ${filePath}, your global Qwen Code settings. ${globalNote}`);
42538
42789
  }
42539
42790
  return scoped;
42540
42791
  }
42792
+ /**
42793
+ * Build the `tools`/`security` patch groups contributed by the `qwencode`
42794
+ * override. Each group is shallow-merged over what `settings.json` already has,
42795
+ * after the scope gate has handled the keys Qwen Code honors in one scope only.
42796
+ */
42797
+ function buildOverrideGroupsPatch({ settings, override, global, filePath, logger }) {
42798
+ const patch = {};
42799
+ for (const { groupName, scopedKeys } of QWEN_OVERRIDE_GROUPS) {
42800
+ const overrideGroup = override?.[groupName];
42801
+ if (overrideGroup === void 0) continue;
42802
+ const existingGroup = settings[groupName];
42803
+ const scoped = scopeOverrideGroup(overrideGroup, {
42804
+ groupName,
42805
+ scopedKeys,
42806
+ existingGroup,
42807
+ global,
42808
+ filePath,
42809
+ logger
42810
+ });
42811
+ const merged = {
42812
+ ...asPlainRecord(existingGroup),
42813
+ ...scoped
42814
+ };
42815
+ if (Object.keys(merged).length > 0) patch[groupName] = merged;
42816
+ }
42817
+ return patch;
42818
+ }
42819
+ /**
42820
+ * Warn about a scoped key lifted out of a *project* settings file being
42821
+ * imported. The file carries no scope marker, so import keeps the key either
42822
+ * way — but regenerating in the global scope would turn a value a workspace
42823
+ * could not decide for itself into one Qwen Code enforces everywhere, and a
42824
+ * settings file read out of a cloned repository is exactly where such a value
42825
+ * comes from. Importing the global file is the case this has nothing to say
42826
+ * about: the value is already in the scope the warning would send it to.
42827
+ */
42828
+ function warnAboutImportedScopedKeys(groups) {
42829
+ for (const { groupName, scopedKeys } of QWEN_OVERRIDE_GROUPS) {
42830
+ const group = groups[groupName];
42831
+ for (const [key, scopedKey] of Object.entries(scopedKeys)) {
42832
+ const { rule } = scopedKey;
42833
+ const value = group[key];
42834
+ if (value === void 0) continue;
42835
+ const { announceOnlyGrants, announceOnImport } = QWEN_SCOPE_RULES[rule];
42836
+ if (!announceOnImport) continue;
42837
+ if (announceOnlyGrants && !grantsSomething(value, scopedKey)) continue;
42838
+ const globalNote = scopedKey.globalNote ?? QWEN_SCOPE_RULES[rule].globalNote;
42839
+ moduleLogger$1.warn(`Qwen permissions: imported ${quoteQualifiedKey({
42840
+ groupName,
42841
+ key
42842
+ })} = ${quoteValueForWarning(value)}. ${globalNote} Review it before generating with the global scope.`);
42843
+ }
42844
+ }
42845
+ }
42541
42846
  var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42542
42847
  constructor(params) {
42543
42848
  super({
@@ -42566,7 +42871,8 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42566
42871
  relativeDirPath: paths.relativeDirPath,
42567
42872
  relativeFilePath: paths.relativeFilePath,
42568
42873
  fileContent,
42569
- validate
42874
+ validate,
42875
+ global
42570
42876
  });
42571
42877
  }
42572
42878
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
@@ -42582,6 +42888,7 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42582
42888
  } catch (error) {
42583
42889
  throw new Error(`Failed to parse existing Qwen settings at ${filePath}: ${formatError(error)}`, { cause: error });
42584
42890
  }
42891
+ const displayPath = toPosixPath((0, node_path.join)(global ? "~" : ".", paths.relativeDirPath, paths.relativeFilePath));
42585
42892
  const config = rulesyncPermissions.getJson();
42586
42893
  const { allow, ask, deny } = convertRulesyncToQwenPermissions(config);
42587
42894
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toQwenToolName(category)));
@@ -42600,24 +42907,25 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42600
42907
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
42601
42908
  else delete mergedPermissions.deny;
42602
42909
  const override = config.qwencode;
42603
- if (override?.autoMode !== void 0) mergedPermissions.autoMode = override.autoMode;
42604
- const patch = { permissions: mergedPermissions };
42605
- if (override?.tools !== void 0) patch.tools = {
42606
- ...asPlainRecord(settings.tools),
42607
- ...asPlainRecord(override.tools)
42608
- };
42609
- if (override?.security !== void 0) {
42610
- const scopedSecurity = scopeOverrideSecurity(override.security, {
42910
+ if (override?.autoMode !== void 0) {
42911
+ const scopedAutoMode = scopeOverrideGroup({ autoMode: override.autoMode }, {
42912
+ groupName: "permissions",
42913
+ scopedKeys: QWEN_SCOPED_PERMISSIONS_KEYS,
42914
+ existingGroup: existingPermissions,
42611
42915
  global,
42612
- relativeFilePath: paths.relativeFilePath,
42916
+ filePath: displayPath,
42613
42917
  logger
42614
42918
  });
42615
- const mergedSecurity = {
42616
- ...asPlainRecord(settings.security),
42617
- ...scopedSecurity
42618
- };
42619
- if (Object.keys(mergedSecurity).length > 0) patch.security = mergedSecurity;
42919
+ if (scopedAutoMode.autoMode !== void 0) mergedPermissions.autoMode = scopedAutoMode.autoMode;
42620
42920
  }
42921
+ const patch = { permissions: mergedPermissions };
42922
+ Object.assign(patch, buildOverrideGroupsPatch({
42923
+ settings,
42924
+ override,
42925
+ global,
42926
+ filePath: displayPath,
42927
+ logger
42928
+ }));
42621
42929
  const fileContent = applySharedConfigPatch({
42622
42930
  fileKey: sharedConfigFileKey(paths),
42623
42931
  feature: "permissions",
@@ -42649,16 +42957,17 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42649
42957
  ask: permissions.ask ?? [],
42650
42958
  deny: permissions.deny ?? []
42651
42959
  });
42652
- const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
42653
- const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
42654
- for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
42655
- if (overrideSecurity[key] === void 0) continue;
42656
- moduleLogger$1.warn(`Qwen permissions: imported 'security.${key}'. Qwen Code ignores it in workspace settings but enforces it in user/system settings, so review it before generating with the global scope.`);
42657
- }
42960
+ const overrideGroups = {
42961
+ tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
42962
+ security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
42963
+ };
42964
+ if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
42658
42965
  const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
42659
42966
  const qwencodeOverride = {};
42660
- if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
42661
- if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
42967
+ for (const { groupName } of QWEN_OVERRIDE_GROUPS) {
42968
+ const group = overrideGroups[groupName];
42969
+ if (Object.keys(group).length > 0) qwencodeOverride[groupName] = group;
42970
+ }
42662
42971
  if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
42663
42972
  const result = { ...config };
42664
42973
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
@@ -64925,8 +65234,10 @@ var RooRule = class RooRule extends ToolRule {
64925
65234
  nonRootPath: this.getSettablePaths().nonRoot
64926
65235
  });
64927
65236
  const mode = rulesyncRule.getFrontmatter().roo?.mode;
64928
- if (!params.root && mode !== void 0 && mode !== "") if (!ROO_MODE_SLUG_PATTERN.test(mode)) warnWithFallback(void 0, `Ignoring roo.mode "${mode}" on ${rulesyncRule.getRelativeFilePath()}: a mode slug may contain only letters, digits and hyphens. Writing the rule to ${params.relativeDirPath} instead.`);
64929
- else params.relativeDirPath = (0, node_path.join)((0, node_path.dirname)(params.relativeDirPath), rooModeRulesDirName(mode));
65237
+ if (!params.root && mode !== void 0 && mode !== "") {
65238
+ if (!ROO_MODE_SLUG_PATTERN.test(mode)) warnWithFallback(void 0, `Ignoring roo.mode "${mode}" on ${rulesyncRule.getRelativeFilePath()}: a mode slug may contain only letters, digits and hyphens. Writing the rule to ${params.relativeDirPath} instead.`);
65239
+ else params.relativeDirPath = (0, node_path.join)((0, node_path.dirname)(params.relativeDirPath), rooModeRulesDirName(mode));
65240
+ }
64930
65241
  return new RooRule(params);
64931
65242
  }
64932
65243
  /**