rulesync 16.23.0 → 16.24.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.
@@ -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
@@ -42504,15 +42519,129 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
42504
42519
  "sandbox",
42505
42520
  "sandboxImage",
42506
42521
  "disabled",
42507
- "visible"
42522
+ "visible",
42523
+ "listDirectory",
42524
+ "workflowsEnabled"
42508
42525
  ];
42509
42526
  const QWEN_OVERRIDE_SECURITY_KEYS = [
42510
42527
  "folderTrust",
42511
42528
  "allowedHttpHookUrls",
42512
- "allowPrivateNetworkHooks"
42529
+ "allowPrivateNetworkHooks",
42530
+ "allowedInsecureVoiceBaseUrls"
42513
42531
  ];
42514
- const QWEN_GLOBAL_ONLY_SECURITY_KEYS = ["allowPrivateNetworkHooks"];
42532
+ /**
42533
+ * What each rule means for the two directions, so a key's behavior is declared
42534
+ * once beside its rule rather than spelled out at every branch.
42535
+ *
42536
+ * `stripInProjectScope` separates the keys a project file cannot use at all from
42537
+ * the ones whose project value still does something, just not unconditionally —
42538
+ * only the former are dropped. `announceOnlyGrants` marks the rule whose risk is
42539
+ * a value that turns something on; for the others the dangerous value is often
42540
+ * the falsy one (an empty allow-all list, folder trust switched off, a sandbox
42541
+ * turned off), so every change is announced instead. `announceOnImport` marks
42542
+ * the keys whose meaning depends on the scope they are written in — importing
42543
+ * one and regenerating globally turns a value a workspace could not decide for
42544
+ * itself into one Qwen Code enforces everywhere. `global-machine-wide` keys are
42545
+ * honored identically in either scope, so import neither promotes nor weakens
42546
+ * them and the announcement on the generate side is the control point.
42547
+ * `projectNote` is `null` for a rule that has nothing to say about the project
42548
+ * scope. A `globalNote` here is the rule's default; a key whose meaning the
42549
+ * default does not describe carries its own.
42550
+ */
42551
+ const QWEN_SCOPE_RULES = {
42552
+ "workspace-stripped": {
42553
+ stripInProjectScope: true,
42554
+ announceOnlyGrants: true,
42555
+ announceOnImport: true,
42556
+ 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.`,
42557
+ 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."
42558
+ },
42559
+ "workspace-non-overriding": {
42560
+ stripInProjectScope: false,
42561
+ announceOnlyGrants: false,
42562
+ announceOnImport: true,
42563
+ 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.`,
42564
+ 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."
42565
+ },
42566
+ "user-scope-trust-check": {
42567
+ stripInProjectScope: false,
42568
+ announceOnlyGrants: false,
42569
+ announceOnImport: true,
42570
+ 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.`,
42571
+ 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."
42572
+ },
42573
+ "global-machine-wide": {
42574
+ stripInProjectScope: false,
42575
+ announceOnlyGrants: false,
42576
+ announceOnImport: false,
42577
+ 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.`,
42578
+ 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."
42579
+ },
42580
+ unmodeled: {
42581
+ stripInProjectScope: false,
42582
+ announceOnlyGrants: false,
42583
+ announceOnImport: false,
42584
+ 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.`,
42585
+ 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."
42586
+ }
42587
+ };
42588
+ const QWEN_OVERRIDE_GROUPS = [{
42589
+ groupName: "tools",
42590
+ overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
42591
+ scopedKeys: {
42592
+ workflowsEnabled: { rule: "workspace-stripped" },
42593
+ approvalMode: {
42594
+ rule: "global-machine-wide",
42595
+ projectNote: autonomyProjectNote
42596
+ },
42597
+ autoAccept: {
42598
+ rule: "global-machine-wide",
42599
+ projectNote: autonomyProjectNote
42600
+ },
42601
+ sandbox: {
42602
+ rule: "global-machine-wide",
42603
+ projectNote: autonomyProjectNote
42604
+ },
42605
+ sandboxImage: {
42606
+ rule: "global-machine-wide",
42607
+ 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.`,
42608
+ 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."
42609
+ },
42610
+ disabled: {
42611
+ rule: "global-machine-wide",
42612
+ 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.`,
42613
+ 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."
42614
+ },
42615
+ visible: {
42616
+ rule: "global-machine-wide",
42617
+ 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.`,
42618
+ 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."
42619
+ },
42620
+ listDirectory: {
42621
+ rule: "global-machine-wide",
42622
+ 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.`,
42623
+ 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."
42624
+ }
42625
+ }
42626
+ }, {
42627
+ groupName: "security",
42628
+ overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
42629
+ scopedKeys: {
42630
+ allowPrivateNetworkHooks: { rule: "workspace-stripped" },
42631
+ allowedInsecureVoiceBaseUrls: {
42632
+ rule: "workspace-stripped",
42633
+ grants: isNonEmptyArray
42634
+ },
42635
+ allowedHttpHookUrls: { rule: "workspace-non-overriding" },
42636
+ folderTrust: { rule: "user-scope-trust-check" }
42637
+ }
42638
+ }];
42515
42639
  const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
42640
+ const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
42641
+ rule: "global-machine-wide",
42642
+ 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.`,
42643
+ 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."
42644
+ } };
42516
42645
  function asPlainRecord(value) {
42517
42646
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
42518
42647
  }
@@ -42524,20 +42653,172 @@ function pickQwenOverrideKeys(group, keys) {
42524
42653
  return picked;
42525
42654
  }
42526
42655
  /**
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.
42656
+ * Whether a value written for a scoped key actually grants something. Qwen Code
42657
+ * reads the boolean ones with a plain truthiness check, so anything truthy turns
42658
+ * the capability on — `1` and the string `"false"` included, which is why this is
42659
+ * not a `=== true` test. Not all of them are booleans either:
42660
+ * `security.allowedInsecureVoiceBaseUrls` is a list of base URLs, and an empty
42661
+ * list grants nothing.
42662
+ *
42663
+ * Only the rules whose `announceOnlyGrants` is set consult this, because "empty"
42664
+ * does not mean the same thing for every list: an empty
42665
+ * `security.allowedHttpHookUrls` is Qwen Code's allow-all, the widest value there
42666
+ * is, so that key is announced whatever it says.
42667
+ */
42668
+ function isNonEmptyArray(value) {
42669
+ return Array.isArray(value) && value.length > 0;
42670
+ }
42671
+ /**
42672
+ * Whether a value turns something on, read the way Qwen Code reads it.
42673
+ *
42674
+ * Plain truthiness by default, because that is what Qwen Code applies to these
42675
+ * settings: `1` and the string `"false"` enable them, and so does an empty array
42676
+ * — which is why an empty list cannot be treated as harmless everywhere. A key
42677
+ * whose list Qwen Code matches against, rather than tests for truth, carries its
42678
+ * own `grants` instead.
42679
+ */
42680
+ function grantsSomething(value, scopedKey) {
42681
+ return (scopedKey?.grants ?? Boolean)(value);
42682
+ }
42683
+ /**
42684
+ * The autonomy and containment controls say the same thing in project scope, so
42685
+ * they share one note rather than repeating it three times.
42530
42686
  */
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.`);
42687
+ function autonomyProjectNote({ qualifiedKey, quotedValue, filePath }) {
42688
+ 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.`;
42689
+ }
42690
+ /**
42691
+ * Quote a `group.key` pair for a warning. The key goes through the same quoting
42692
+ * a value does, because it is one: the override's groups are `z.looseObject`s,
42693
+ * so a key the scope gate names may have been written by whoever authored the
42694
+ * `.rulesync/permissions.jsonc` rather than chosen from a list rulesync knows.
42695
+ * Serializing it — rather than wrapping it in quotes of our own — is what keeps
42696
+ * a key that contains a quote from closing it and continuing the sentence.
42697
+ */
42698
+ function quoteQualifiedKey({ groupName, key }) {
42699
+ return quoteValueForWarning(`${groupName}.${key}`);
42700
+ }
42701
+ /**
42702
+ * Compare two settings values without letting key order decide the answer, so
42703
+ * re-emitting `{ enabled: true, note: "x" }` as `{ note: "x", enabled: true }`
42704
+ * is not announced as a change.
42705
+ */
42706
+ function stableStringify(value) {
42707
+ 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";
42708
+ }
42709
+ function sameSettingsValue(a, b) {
42710
+ return stableStringify(a) === stableStringify(b);
42711
+ }
42712
+ /**
42713
+ * Apply the scope rules for a settings group's scoped keys.
42714
+ *
42715
+ * Generating project settings drops the keys Qwen Code strips from a workspace
42716
+ * file before the merge — writing one there would be dead configuration — and
42717
+ * explains the keys whose project value is honored only conditionally rather
42718
+ * than dropping those. Either way only the override copy is touched, so a value
42719
+ * the user already wrote into the project file stays where it is.
42720
+ *
42721
+ * Generating global settings keeps every key, and announces the write: the
42722
+ * `.rulesync/permissions.jsonc` carrying it may have arrived with a cloned or
42723
+ * fetched repository, and the global file applies to every project on this
42724
+ * machine. So the new value is named alongside the one it replaces, exactly as
42725
+ * the deepagents startup override announces its own relaxations. Every key the
42726
+ * override writes is announced, not only the ones with a scope rule — the
42727
+ * groups are `z.looseObject`s, so an unmodeled key falls back to the
42728
+ * `unmodeled` rule rather than slipping past the gate that the modeled ones
42729
+ * pass through.
42730
+ */
42731
+ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGroup, global, filePath, logger }) {
42732
+ const scoped = { ...asPlainRecord(overrideGroup) };
42733
+ const previous = asPlainRecord(existingGroup);
42734
+ const rulesByKey = new Map(Object.entries(scopedKeys));
42735
+ for (const key of Object.keys(scoped)) {
42736
+ const value = scoped[key];
42737
+ if (value === void 0) continue;
42738
+ const scopedKey = rulesByKey.get(key);
42739
+ const rule = scopedKey?.rule ?? "unmodeled";
42740
+ const { stripInProjectScope, announceOnlyGrants } = QWEN_SCOPE_RULES[rule];
42741
+ const projectNote = scopedKey?.projectNote ?? QWEN_SCOPE_RULES[rule].projectNote;
42742
+ const globalNote = scopedKey?.globalNote ?? QWEN_SCOPE_RULES[rule].globalNote;
42743
+ const previousValue = Object.hasOwn(previous, key) ? previous[key] : void 0;
42744
+ const unchanged = sameSettingsValue(previousValue, value);
42745
+ if (!global) {
42746
+ if (stripInProjectScope) delete scoped[key];
42747
+ else if (unchanged) continue;
42748
+ if (projectNote) warnWithFallback(logger, `Qwen permissions: ${projectNote({
42749
+ qualifiedKey: quoteQualifiedKey({
42750
+ groupName,
42751
+ key
42752
+ }),
42753
+ quotedValue: quoteValueForWarning(value),
42754
+ filePath
42755
+ })}`);
42756
+ continue;
42757
+ }
42758
+ if (unchanged) continue;
42759
+ if (announceOnlyGrants && !grantsSomething(value, scopedKey)) continue;
42760
+ const replaced = previousValue === void 0 ? "" : ` (was ${quoteValueForWarning(previousValue)})`;
42761
+ warnWithFallback(logger, `Qwen permissions: the qwencode override wrote ${quoteQualifiedKey({
42762
+ groupName,
42763
+ key
42764
+ })} = ${quoteValueForWarning(value)}${replaced} into ${filePath}, your global Qwen Code settings. ${globalNote}`);
42538
42765
  }
42539
42766
  return scoped;
42540
42767
  }
42768
+ /**
42769
+ * Build the `tools`/`security` patch groups contributed by the `qwencode`
42770
+ * override. Each group is shallow-merged over what `settings.json` already has,
42771
+ * after the scope gate has handled the keys Qwen Code honors in one scope only.
42772
+ */
42773
+ function buildOverrideGroupsPatch({ settings, override, global, filePath, logger }) {
42774
+ const patch = {};
42775
+ for (const { groupName, scopedKeys } of QWEN_OVERRIDE_GROUPS) {
42776
+ const overrideGroup = override?.[groupName];
42777
+ if (overrideGroup === void 0) continue;
42778
+ const existingGroup = settings[groupName];
42779
+ const scoped = scopeOverrideGroup(overrideGroup, {
42780
+ groupName,
42781
+ scopedKeys,
42782
+ existingGroup,
42783
+ global,
42784
+ filePath,
42785
+ logger
42786
+ });
42787
+ const merged = {
42788
+ ...asPlainRecord(existingGroup),
42789
+ ...scoped
42790
+ };
42791
+ if (Object.keys(merged).length > 0) patch[groupName] = merged;
42792
+ }
42793
+ return patch;
42794
+ }
42795
+ /**
42796
+ * Warn about a scoped key lifted out of a *project* settings file being
42797
+ * imported. The file carries no scope marker, so import keeps the key either
42798
+ * way — but regenerating in the global scope would turn a value a workspace
42799
+ * could not decide for itself into one Qwen Code enforces everywhere, and a
42800
+ * settings file read out of a cloned repository is exactly where such a value
42801
+ * comes from. Importing the global file is the case this has nothing to say
42802
+ * about: the value is already in the scope the warning would send it to.
42803
+ */
42804
+ function warnAboutImportedScopedKeys(groups) {
42805
+ for (const { groupName, scopedKeys } of QWEN_OVERRIDE_GROUPS) {
42806
+ const group = groups[groupName];
42807
+ for (const [key, scopedKey] of Object.entries(scopedKeys)) {
42808
+ const { rule } = scopedKey;
42809
+ const value = group[key];
42810
+ if (value === void 0) continue;
42811
+ const { announceOnlyGrants, announceOnImport } = QWEN_SCOPE_RULES[rule];
42812
+ if (!announceOnImport) continue;
42813
+ if (announceOnlyGrants && !grantsSomething(value, scopedKey)) continue;
42814
+ const globalNote = scopedKey.globalNote ?? QWEN_SCOPE_RULES[rule].globalNote;
42815
+ moduleLogger$1.warn(`Qwen permissions: imported ${quoteQualifiedKey({
42816
+ groupName,
42817
+ key
42818
+ })} = ${quoteValueForWarning(value)}. ${globalNote} Review it before generating with the global scope.`);
42819
+ }
42820
+ }
42821
+ }
42541
42822
  var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42542
42823
  constructor(params) {
42543
42824
  super({
@@ -42566,7 +42847,8 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42566
42847
  relativeDirPath: paths.relativeDirPath,
42567
42848
  relativeFilePath: paths.relativeFilePath,
42568
42849
  fileContent,
42569
- validate
42850
+ validate,
42851
+ global
42570
42852
  });
42571
42853
  }
42572
42854
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
@@ -42582,6 +42864,7 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42582
42864
  } catch (error) {
42583
42865
  throw new Error(`Failed to parse existing Qwen settings at ${filePath}: ${formatError(error)}`, { cause: error });
42584
42866
  }
42867
+ const displayPath = toPosixPath((0, node_path.join)(global ? "~" : ".", paths.relativeDirPath, paths.relativeFilePath));
42585
42868
  const config = rulesyncPermissions.getJson();
42586
42869
  const { allow, ask, deny } = convertRulesyncToQwenPermissions(config);
42587
42870
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toQwenToolName(category)));
@@ -42600,24 +42883,25 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42600
42883
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
42601
42884
  else delete mergedPermissions.deny;
42602
42885
  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, {
42886
+ if (override?.autoMode !== void 0) {
42887
+ const scopedAutoMode = scopeOverrideGroup({ autoMode: override.autoMode }, {
42888
+ groupName: "permissions",
42889
+ scopedKeys: QWEN_SCOPED_PERMISSIONS_KEYS,
42890
+ existingGroup: existingPermissions,
42611
42891
  global,
42612
- relativeFilePath: paths.relativeFilePath,
42892
+ filePath: displayPath,
42613
42893
  logger
42614
42894
  });
42615
- const mergedSecurity = {
42616
- ...asPlainRecord(settings.security),
42617
- ...scopedSecurity
42618
- };
42619
- if (Object.keys(mergedSecurity).length > 0) patch.security = mergedSecurity;
42895
+ if (scopedAutoMode.autoMode !== void 0) mergedPermissions.autoMode = scopedAutoMode.autoMode;
42620
42896
  }
42897
+ const patch = { permissions: mergedPermissions };
42898
+ Object.assign(patch, buildOverrideGroupsPatch({
42899
+ settings,
42900
+ override,
42901
+ global,
42902
+ filePath: displayPath,
42903
+ logger
42904
+ }));
42621
42905
  const fileContent = applySharedConfigPatch({
42622
42906
  fileKey: sharedConfigFileKey(paths),
42623
42907
  feature: "permissions",
@@ -42649,16 +42933,17 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42649
42933
  ask: permissions.ask ?? [],
42650
42934
  deny: permissions.deny ?? []
42651
42935
  });
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
- }
42936
+ const overrideGroups = {
42937
+ tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
42938
+ security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
42939
+ };
42940
+ if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
42658
42941
  const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
42659
42942
  const qwencodeOverride = {};
42660
- if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
42661
- if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
42943
+ for (const { groupName } of QWEN_OVERRIDE_GROUPS) {
42944
+ const group = overrideGroups[groupName];
42945
+ if (Object.keys(group).length > 0) qwencodeOverride[groupName] = group;
42946
+ }
42662
42947
  if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
42663
42948
  const result = { ...config };
42664
42949
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;