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.
@@ -6633,10 +6633,25 @@ const CursorPermissionsOverrideSchema = z.looseObject({
6633
6633
  * Tool-scoped override block for Qwen Code. Qwen's `settings.json` exposes
6634
6634
  * autonomy/sandbox controls with no canonical permission category — under
6635
6635
  * `tools` (`approvalMode` = plan/default/auto-edit/auto/yolo, `autoAccept`,
6636
- * `sandbox`, `sandboxImage`, `disabled`) and `security` (`folderTrust`,
6637
- * `allowedHttpHookUrls`, `allowPrivateNetworkHooks` the latter is honored by
6638
- * Qwen Code only in user/system settings, so generate skips it in project scope).
6639
- * It also
6636
+ * `sandbox`, `sandboxImage`, `disabled`, `visible`, `listDirectory`,
6637
+ * `workflowsEnabled`) and `security` (`folderTrust`, `allowedHttpHookUrls`,
6638
+ * `allowPrivateNetworkHooks`, `allowedInsecureVoiceBaseUrls`). Qwen Code strips
6639
+ * `tools.workflowsEnabled`, `security.allowPrivateNetworkHooks` and
6640
+ * `security.allowedInsecureVoiceBaseUrls` out of workspace settings, so generate
6641
+ * skips those three in project scope and announces a granting value in global
6642
+ * scope. `security.allowedHttpHookUrls` (honored in a workspace only while no
6643
+ * higher scope sets it) and `security.folderTrust` (the initial trust decision
6644
+ * is made from user/system settings alone, before the workspace merge) are
6645
+ * written in both scopes, with a note in project scope and an announcement of
6646
+ * any global change. Every other key is honored in either scope, so a write that
6647
+ * changes what the file said is reported in either scope, naming what that key
6648
+ * decides there — the autonomy and containment
6649
+ * controls (`approvalMode`, `autoAccept`, `sandbox`, `sandboxImage`), the
6650
+ * registry controls (`disabled`, `visible`, `listDirectory`), the Auto Mode
6651
+ * classifier config, and, because these groups are loose objects, any key
6652
+ * rulesync does not model. Import flags a scope-dependent key only when it read
6653
+ * the project file, since that is the value a `--global` regenerate would
6654
+ * promote. It also
6640
6655
  * exposes `permissions.autoMode` (the Auto Mode classifier config:
6641
6656
  * `hints.{allow,softDeny,hardDeny}`, `environment`, `classifyAllShell` — see
6642
6657
  * https://qwenlm.github.io/qwen-code-docs/en/users/features/auto-mode/), which
@@ -22787,8 +22802,10 @@ function definitionsToHermesEntries({ event, sourceEvent = event, definitions, l
22787
22802
  for (const definition of definitions) {
22788
22803
  if ((definition.type ?? "command") !== "command" || typeof definition.command !== "string" || definition.command === "") continue;
22789
22804
  const entry = { command: definition.command };
22790
- if (typeof definition.matcher === "string" && definition.matcher !== "") if (supportsMatcher) entry.matcher = definition.matcher;
22791
- else logger?.warn(`matcher "${definition.matcher}" on "${sourceEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
22805
+ if (typeof definition.matcher === "string" && definition.matcher !== "") {
22806
+ if (supportsMatcher) entry.matcher = definition.matcher;
22807
+ else logger?.warn(`matcher "${definition.matcher}" on "${sourceEvent}" hook will be ignored — Hermes Agent only supports matchers on pre_tool_call/post_tool_call`);
22808
+ }
22792
22809
  if (typeof definition.timeout === "number") entry.timeout = definition.timeout;
22793
22810
  if (typeof definition.failClosed === "boolean") {
22794
22811
  if (event === HERMESAGENT_FAIL_CLOSED_EVENT) entry.fail_closed = definition.failClosed;
@@ -24016,8 +24033,10 @@ function canonicalToKiroHooks({ config, logger }) {
24016
24033
  key: eventName
24017
24034
  }) ?? eventName;
24018
24035
  const entries = buildKiroEntriesForEvent(definitions);
24019
- if (entries.length > 0) if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
24020
- else kiro[kiroEventName] = entries;
24036
+ if (entries.length > 0) {
24037
+ if (kiro[kiroEventName]) kiro[kiroEventName].push(...entries);
24038
+ else kiro[kiroEventName] = entries;
24039
+ }
24021
24040
  }
24022
24041
  return kiro;
24023
24042
  }
@@ -24843,8 +24862,10 @@ function canonicalToReasonixHooks({ config, toolOverrideHooks, logger }) {
24843
24862
  if ((def.type ?? "command") !== "command") continue;
24844
24863
  if (typeof def.command !== "string") continue;
24845
24864
  const entry = { command: def.command };
24846
- if (typeof def.matcher === "string" && def.matcher !== "") if (isMatcherEvent) entry.match = def.matcher;
24847
- else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
24865
+ if (typeof def.matcher === "string" && def.matcher !== "") {
24866
+ if (isMatcherEvent) entry.match = def.matcher;
24867
+ else logger?.warn(`matcher "${def.matcher}" on "${event}" hook will be ignored — Reasonix's "${reasonixEvent}" event does not support matchers`);
24868
+ }
24848
24869
  if (typeof def.description === "string" && def.description !== "") entry.description = def.description;
24849
24870
  if (typeof def.timeout === "number") entry.timeout = Math.round(def.timeout * 1e3);
24850
24871
  entries.push(entry);
@@ -28412,12 +28433,16 @@ function convertFromCodexFormat(codexMcp) {
28412
28433
  } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthFromCodex(value);
28413
28434
  else if (Object.hasOwn(CODEX_TO_RULESYNC_FIELD_MAP, key)) {
28414
28435
  const mappedKey = CODEX_TO_RULESYNC_FIELD_MAP[key];
28415
- if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28416
- else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
28436
+ if (mappedKey) {
28437
+ if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28438
+ else warnWithFallback(void 0, `Ignored malformed array for ${key} in MCP server ${name}`);
28439
+ }
28417
28440
  } else if (Object.hasOwn(CODEX_TO_RULESYNC_SCALAR_FIELD_MAP, key)) {
28418
28441
  const mappedKey = CODEX_TO_RULESYNC_SCALAR_FIELD_MAP[key];
28419
- if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
28420
- else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
28442
+ if (mappedKey) {
28443
+ if (typeof value === "string") converted[mappedKey] = value;
28444
+ else warnWithFallback(void 0, `Ignored malformed value for ${key} in MCP server ${name}: expected a string`);
28445
+ }
28421
28446
  } else converted[key] = value;
28422
28447
  }
28423
28448
  restateCanonicalTransport(converted);
@@ -28449,12 +28474,16 @@ function convertToCodexFormat(mcpServers) {
28449
28474
  } else if (key === "oauth" && isRecord$1(value)) converted[key] = mapOauthToCodex(value);
28450
28475
  else if (Object.hasOwn(RULESYNC_TO_CODEX_FIELD_MAP, key)) {
28451
28476
  const mappedKey = RULESYNC_TO_CODEX_FIELD_MAP[key];
28452
- if (mappedKey) if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28453
- else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
28477
+ if (mappedKey) {
28478
+ if (isValidRenamedArray(key, value)) converted[mappedKey] = value;
28479
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string array, got ${typeof value}`);
28480
+ }
28454
28481
  } else if (Object.hasOwn(RULESYNC_TO_CODEX_SCALAR_FIELD_MAP, key)) {
28455
28482
  const mappedKey = RULESYNC_TO_CODEX_SCALAR_FIELD_MAP[key];
28456
- if (mappedKey) if (typeof value === "string") converted[mappedKey] = value;
28457
- else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
28483
+ if (mappedKey) {
28484
+ if (typeof value === "string") converted[mappedKey] = value;
28485
+ else warnWithFallback(void 0, `[CodexCliMcp] Skipping invalid value type for mapped key '${key}': expected string, got ${typeof value}`);
28486
+ }
28458
28487
  } else converted[key] = value;
28459
28488
  }
28460
28489
  const previousName = originalNames.get(codexName);
@@ -28698,8 +28727,10 @@ function resolveRemoteMcpUrl(serverConfig) {
28698
28727
  /** The `command` array a `local` server is spawned with, `args` merged in. */
28699
28728
  function resolveLocalMcpCommand(serverConfig) {
28700
28729
  const commandArray = [];
28701
- if (serverConfig.command) if (Array.isArray(serverConfig.command)) commandArray.push(...serverConfig.command);
28702
- else commandArray.push(serverConfig.command);
28730
+ if (serverConfig.command) {
28731
+ if (Array.isArray(serverConfig.command)) commandArray.push(...serverConfig.command);
28732
+ else commandArray.push(serverConfig.command);
28733
+ }
28703
28734
  if (serverConfig.args) commandArray.push(...serverConfig.args);
28704
28735
  return commandArray;
28705
28736
  }
@@ -29236,8 +29267,10 @@ function toDeepagentsServer({ name, server, logger }) {
29236
29267
  const { enabledTools, disabledTools, type: _type, transport, ...rest } = server;
29237
29268
  const converted = { ...rest };
29238
29269
  const normalized = normalizeDeepagentsTransport(rawTransport);
29239
- if (normalized !== void 0) if (transport !== void 0) converted.transport = normalized;
29240
- else converted.type = normalized;
29270
+ if (normalized !== void 0) {
29271
+ if (transport !== void 0) converted.transport = normalized;
29272
+ else converted.type = normalized;
29273
+ }
29241
29274
  if (enabledTools !== void 0 && disabledTools !== void 0) return warnAndSkipMcpServer({
29242
29275
  toolName: TOOL_NAME,
29243
29276
  serverName: name,
@@ -29252,8 +29285,10 @@ function toDeepagentsServer({ name, server, logger }) {
29252
29285
  logger
29253
29286
  });
29254
29287
  converted.allowedTools = enabledTools;
29255
- } 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.`);
29256
- else converted.disabledTools = disabledTools;
29288
+ } else if (disabledTools !== void 0) {
29289
+ 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.`);
29290
+ else converted.disabledTools = disabledTools;
29291
+ }
29257
29292
  return converted;
29258
29293
  }
29259
29294
  /** Lift dcode's spellings back into the canonical model. */
@@ -32190,14 +32225,16 @@ function rulesyncMcpServerToReasonix(name, server, logger) {
32190
32225
  name,
32191
32226
  ...type !== void 0 && { type }
32192
32227
  };
32193
- if (server.command !== void 0) if (Array.isArray(server.command)) {
32194
- const [command, ...commandArgs] = server.command;
32195
- if (command !== void 0) plugin.command = command;
32196
- const args = [...commandArgs, ...server.args ?? []];
32197
- if (args.length > 0) plugin.args = args;
32198
- } else {
32199
- plugin.command = server.command;
32200
- if (server.args !== void 0) plugin.args = server.args;
32228
+ if (server.command !== void 0) {
32229
+ if (Array.isArray(server.command)) {
32230
+ const [command, ...commandArgs] = server.command;
32231
+ if (command !== void 0) plugin.command = command;
32232
+ const args = [...commandArgs, ...server.args ?? []];
32233
+ if (args.length > 0) plugin.args = args;
32234
+ } else {
32235
+ plugin.command = server.command;
32236
+ if (server.args !== void 0) plugin.args = server.args;
32237
+ }
32201
32238
  }
32202
32239
  for (const field of REASONIX_PLUGIN_FIELDS) {
32203
32240
  if (field === "type" || field === "command" || field === "args") continue;
@@ -33265,14 +33302,16 @@ function rulesyncMcpServerToVibe(name, server, existing) {
33265
33302
  name,
33266
33303
  ...transport !== void 0 && { transport }
33267
33304
  };
33268
- if (server.command !== void 0) if (Array.isArray(server.command)) {
33269
- const [command, ...commandArgs] = server.command;
33270
- if (command !== void 0) vibeServer.command = command;
33271
- const args = [...commandArgs, ...server.args ?? []];
33272
- if (args.length > 0) vibeServer.args = args;
33273
- } else {
33274
- vibeServer.command = server.command;
33275
- if (server.args !== void 0) vibeServer.args = server.args;
33305
+ if (server.command !== void 0) {
33306
+ if (Array.isArray(server.command)) {
33307
+ const [command, ...commandArgs] = server.command;
33308
+ if (command !== void 0) vibeServer.command = command;
33309
+ const args = [...commandArgs, ...server.args ?? []];
33310
+ if (args.length > 0) vibeServer.args = args;
33311
+ } else {
33312
+ vibeServer.command = server.command;
33313
+ if (server.args !== void 0) vibeServer.args = server.args;
33314
+ }
33276
33315
  }
33277
33316
  const hasStructuredAuth = serverRecord.auth !== void 0;
33278
33317
  for (const field of VIBE_MCP_SERVER_FIELDS) {
@@ -42479,15 +42518,129 @@ const QWEN_OVERRIDE_TOOLS_KEYS = [
42479
42518
  "sandbox",
42480
42519
  "sandboxImage",
42481
42520
  "disabled",
42482
- "visible"
42521
+ "visible",
42522
+ "listDirectory",
42523
+ "workflowsEnabled"
42483
42524
  ];
42484
42525
  const QWEN_OVERRIDE_SECURITY_KEYS = [
42485
42526
  "folderTrust",
42486
42527
  "allowedHttpHookUrls",
42487
- "allowPrivateNetworkHooks"
42528
+ "allowPrivateNetworkHooks",
42529
+ "allowedInsecureVoiceBaseUrls"
42488
42530
  ];
42489
- const QWEN_GLOBAL_ONLY_SECURITY_KEYS = ["allowPrivateNetworkHooks"];
42531
+ /**
42532
+ * What each rule means for the two directions, so a key's behavior is declared
42533
+ * once beside its rule rather than spelled out at every branch.
42534
+ *
42535
+ * `stripInProjectScope` separates the keys a project file cannot use at all from
42536
+ * the ones whose project value still does something, just not unconditionally —
42537
+ * only the former are dropped. `announceOnlyGrants` marks the rule whose risk is
42538
+ * a value that turns something on; for the others the dangerous value is often
42539
+ * the falsy one (an empty allow-all list, folder trust switched off, a sandbox
42540
+ * turned off), so every change is announced instead. `announceOnImport` marks
42541
+ * the keys whose meaning depends on the scope they are written in — importing
42542
+ * one and regenerating globally turns a value a workspace could not decide for
42543
+ * itself into one Qwen Code enforces everywhere. `global-machine-wide` keys are
42544
+ * honored identically in either scope, so import neither promotes nor weakens
42545
+ * them and the announcement on the generate side is the control point.
42546
+ * `projectNote` is `null` for a rule that has nothing to say about the project
42547
+ * scope. A `globalNote` here is the rule's default; a key whose meaning the
42548
+ * default does not describe carries its own.
42549
+ */
42550
+ const QWEN_SCOPE_RULES = {
42551
+ "workspace-stripped": {
42552
+ stripInProjectScope: true,
42553
+ announceOnlyGrants: true,
42554
+ announceOnImport: true,
42555
+ 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.`,
42556
+ 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."
42557
+ },
42558
+ "workspace-non-overriding": {
42559
+ stripInProjectScope: false,
42560
+ announceOnlyGrants: false,
42561
+ announceOnImport: true,
42562
+ 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.`,
42563
+ 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."
42564
+ },
42565
+ "user-scope-trust-check": {
42566
+ stripInProjectScope: false,
42567
+ announceOnlyGrants: false,
42568
+ announceOnImport: true,
42569
+ 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.`,
42570
+ 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."
42571
+ },
42572
+ "global-machine-wide": {
42573
+ stripInProjectScope: false,
42574
+ announceOnlyGrants: false,
42575
+ announceOnImport: false,
42576
+ 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.`,
42577
+ 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."
42578
+ },
42579
+ unmodeled: {
42580
+ stripInProjectScope: false,
42581
+ announceOnlyGrants: false,
42582
+ announceOnImport: false,
42583
+ 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.`,
42584
+ 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."
42585
+ }
42586
+ };
42587
+ const QWEN_OVERRIDE_GROUPS = [{
42588
+ groupName: "tools",
42589
+ overrideKeys: QWEN_OVERRIDE_TOOLS_KEYS,
42590
+ scopedKeys: {
42591
+ workflowsEnabled: { rule: "workspace-stripped" },
42592
+ approvalMode: {
42593
+ rule: "global-machine-wide",
42594
+ projectNote: autonomyProjectNote
42595
+ },
42596
+ autoAccept: {
42597
+ rule: "global-machine-wide",
42598
+ projectNote: autonomyProjectNote
42599
+ },
42600
+ sandbox: {
42601
+ rule: "global-machine-wide",
42602
+ projectNote: autonomyProjectNote
42603
+ },
42604
+ sandboxImage: {
42605
+ rule: "global-machine-wide",
42606
+ 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.`,
42607
+ 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."
42608
+ },
42609
+ disabled: {
42610
+ rule: "global-machine-wide",
42611
+ 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.`,
42612
+ 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."
42613
+ },
42614
+ visible: {
42615
+ rule: "global-machine-wide",
42616
+ 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.`,
42617
+ 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."
42618
+ },
42619
+ listDirectory: {
42620
+ rule: "global-machine-wide",
42621
+ 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.`,
42622
+ 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."
42623
+ }
42624
+ }
42625
+ }, {
42626
+ groupName: "security",
42627
+ overrideKeys: QWEN_OVERRIDE_SECURITY_KEYS,
42628
+ scopedKeys: {
42629
+ allowPrivateNetworkHooks: { rule: "workspace-stripped" },
42630
+ allowedInsecureVoiceBaseUrls: {
42631
+ rule: "workspace-stripped",
42632
+ grants: isNonEmptyArray
42633
+ },
42634
+ allowedHttpHookUrls: { rule: "workspace-non-overriding" },
42635
+ folderTrust: { rule: "user-scope-trust-check" }
42636
+ }
42637
+ }];
42490
42638
  const QWEN_OVERRIDE_PERMISSIONS_KEYS = ["autoMode"];
42639
+ const QWEN_SCOPED_PERMISSIONS_KEYS = { autoMode: {
42640
+ rule: "global-machine-wide",
42641
+ 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.`,
42642
+ 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."
42643
+ } };
42491
42644
  function asPlainRecord(value) {
42492
42645
  return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
42493
42646
  }
@@ -42499,20 +42652,172 @@ function pickQwenOverrideKeys(group, keys) {
42499
42652
  return picked;
42500
42653
  }
42501
42654
  /**
42502
- * Drop the global-only `security` keys from the override when generating project
42503
- * settings, warning once per dropped key. Only the override copy is filtered, so
42504
- * a value the user already wrote into the project file stays untouched.
42655
+ * Whether a value written for a scoped key actually grants something. Qwen Code
42656
+ * reads the boolean ones with a plain truthiness check, so anything truthy turns
42657
+ * the capability on — `1` and the string `"false"` included, which is why this is
42658
+ * not a `=== true` test. Not all of them are booleans either:
42659
+ * `security.allowedInsecureVoiceBaseUrls` is a list of base URLs, and an empty
42660
+ * list grants nothing.
42661
+ *
42662
+ * Only the rules whose `announceOnlyGrants` is set consult this, because "empty"
42663
+ * does not mean the same thing for every list: an empty
42664
+ * `security.allowedHttpHookUrls` is Qwen Code's allow-all, the widest value there
42665
+ * is, so that key is announced whatever it says.
42666
+ */
42667
+ function isNonEmptyArray(value) {
42668
+ return Array.isArray(value) && value.length > 0;
42669
+ }
42670
+ /**
42671
+ * Whether a value turns something on, read the way Qwen Code reads it.
42672
+ *
42673
+ * Plain truthiness by default, because that is what Qwen Code applies to these
42674
+ * settings: `1` and the string `"false"` enable them, and so does an empty array
42675
+ * — which is why an empty list cannot be treated as harmless everywhere. A key
42676
+ * whose list Qwen Code matches against, rather than tests for truth, carries its
42677
+ * own `grants` instead.
42678
+ */
42679
+ function grantsSomething(value, scopedKey) {
42680
+ return (scopedKey?.grants ?? Boolean)(value);
42681
+ }
42682
+ /**
42683
+ * The autonomy and containment controls say the same thing in project scope, so
42684
+ * they share one note rather than repeating it three times.
42685
+ */
42686
+ function autonomyProjectNote({ qualifiedKey, quotedValue, filePath }) {
42687
+ 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.`;
42688
+ }
42689
+ /**
42690
+ * Quote a `group.key` pair for a warning. The key goes through the same quoting
42691
+ * a value does, because it is one: the override's groups are `z.looseObject`s,
42692
+ * so a key the scope gate names may have been written by whoever authored the
42693
+ * `.rulesync/permissions.jsonc` rather than chosen from a list rulesync knows.
42694
+ * Serializing it — rather than wrapping it in quotes of our own — is what keeps
42695
+ * a key that contains a quote from closing it and continuing the sentence.
42696
+ */
42697
+ function quoteQualifiedKey({ groupName, key }) {
42698
+ return quoteValueForWarning(`${groupName}.${key}`);
42699
+ }
42700
+ /**
42701
+ * Compare two settings values without letting key order decide the answer, so
42702
+ * re-emitting `{ enabled: true, note: "x" }` as `{ note: "x", enabled: true }`
42703
+ * is not announced as a change.
42505
42704
  */
42506
- function scopeOverrideSecurity(overrideSecurity, { global, relativeFilePath, logger }) {
42507
- const scoped = { ...asPlainRecord(overrideSecurity) };
42508
- if (global) return scoped;
42509
- for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
42510
- if (scoped[key] === void 0) continue;
42511
- delete scoped[key];
42512
- 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.`);
42705
+ function stableStringify(value) {
42706
+ 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";
42707
+ }
42708
+ function sameSettingsValue(a, b) {
42709
+ return stableStringify(a) === stableStringify(b);
42710
+ }
42711
+ /**
42712
+ * Apply the scope rules for a settings group's scoped keys.
42713
+ *
42714
+ * Generating project settings drops the keys Qwen Code strips from a workspace
42715
+ * file before the merge — writing one there would be dead configuration — and
42716
+ * explains the keys whose project value is honored only conditionally rather
42717
+ * than dropping those. Either way only the override copy is touched, so a value
42718
+ * the user already wrote into the project file stays where it is.
42719
+ *
42720
+ * Generating global settings keeps every key, and announces the write: the
42721
+ * `.rulesync/permissions.jsonc` carrying it may have arrived with a cloned or
42722
+ * fetched repository, and the global file applies to every project on this
42723
+ * machine. So the new value is named alongside the one it replaces, exactly as
42724
+ * the deepagents startup override announces its own relaxations. Every key the
42725
+ * override writes is announced, not only the ones with a scope rule — the
42726
+ * groups are `z.looseObject`s, so an unmodeled key falls back to the
42727
+ * `unmodeled` rule rather than slipping past the gate that the modeled ones
42728
+ * pass through.
42729
+ */
42730
+ function scopeOverrideGroup(overrideGroup, { groupName, scopedKeys, existingGroup, global, filePath, logger }) {
42731
+ const scoped = { ...asPlainRecord(overrideGroup) };
42732
+ const previous = asPlainRecord(existingGroup);
42733
+ const rulesByKey = new Map(Object.entries(scopedKeys));
42734
+ for (const key of Object.keys(scoped)) {
42735
+ const value = scoped[key];
42736
+ if (value === void 0) continue;
42737
+ const scopedKey = rulesByKey.get(key);
42738
+ const rule = scopedKey?.rule ?? "unmodeled";
42739
+ const { stripInProjectScope, announceOnlyGrants } = QWEN_SCOPE_RULES[rule];
42740
+ const projectNote = scopedKey?.projectNote ?? QWEN_SCOPE_RULES[rule].projectNote;
42741
+ const globalNote = scopedKey?.globalNote ?? QWEN_SCOPE_RULES[rule].globalNote;
42742
+ const previousValue = Object.hasOwn(previous, key) ? previous[key] : void 0;
42743
+ const unchanged = sameSettingsValue(previousValue, value);
42744
+ if (!global) {
42745
+ if (stripInProjectScope) delete scoped[key];
42746
+ else if (unchanged) continue;
42747
+ if (projectNote) warnWithFallback(logger, `Qwen permissions: ${projectNote({
42748
+ qualifiedKey: quoteQualifiedKey({
42749
+ groupName,
42750
+ key
42751
+ }),
42752
+ quotedValue: quoteValueForWarning(value),
42753
+ filePath
42754
+ })}`);
42755
+ continue;
42756
+ }
42757
+ if (unchanged) continue;
42758
+ if (announceOnlyGrants && !grantsSomething(value, scopedKey)) continue;
42759
+ const replaced = previousValue === void 0 ? "" : ` (was ${quoteValueForWarning(previousValue)})`;
42760
+ warnWithFallback(logger, `Qwen permissions: the qwencode override wrote ${quoteQualifiedKey({
42761
+ groupName,
42762
+ key
42763
+ })} = ${quoteValueForWarning(value)}${replaced} into ${filePath}, your global Qwen Code settings. ${globalNote}`);
42513
42764
  }
42514
42765
  return scoped;
42515
42766
  }
42767
+ /**
42768
+ * Build the `tools`/`security` patch groups contributed by the `qwencode`
42769
+ * override. Each group is shallow-merged over what `settings.json` already has,
42770
+ * after the scope gate has handled the keys Qwen Code honors in one scope only.
42771
+ */
42772
+ function buildOverrideGroupsPatch({ settings, override, global, filePath, logger }) {
42773
+ const patch = {};
42774
+ for (const { groupName, scopedKeys } of QWEN_OVERRIDE_GROUPS) {
42775
+ const overrideGroup = override?.[groupName];
42776
+ if (overrideGroup === void 0) continue;
42777
+ const existingGroup = settings[groupName];
42778
+ const scoped = scopeOverrideGroup(overrideGroup, {
42779
+ groupName,
42780
+ scopedKeys,
42781
+ existingGroup,
42782
+ global,
42783
+ filePath,
42784
+ logger
42785
+ });
42786
+ const merged = {
42787
+ ...asPlainRecord(existingGroup),
42788
+ ...scoped
42789
+ };
42790
+ if (Object.keys(merged).length > 0) patch[groupName] = merged;
42791
+ }
42792
+ return patch;
42793
+ }
42794
+ /**
42795
+ * Warn about a scoped key lifted out of a *project* settings file being
42796
+ * imported. The file carries no scope marker, so import keeps the key either
42797
+ * way — but regenerating in the global scope would turn a value a workspace
42798
+ * could not decide for itself into one Qwen Code enforces everywhere, and a
42799
+ * settings file read out of a cloned repository is exactly where such a value
42800
+ * comes from. Importing the global file is the case this has nothing to say
42801
+ * about: the value is already in the scope the warning would send it to.
42802
+ */
42803
+ function warnAboutImportedScopedKeys(groups) {
42804
+ for (const { groupName, scopedKeys } of QWEN_OVERRIDE_GROUPS) {
42805
+ const group = groups[groupName];
42806
+ for (const [key, scopedKey] of Object.entries(scopedKeys)) {
42807
+ const { rule } = scopedKey;
42808
+ const value = group[key];
42809
+ if (value === void 0) continue;
42810
+ const { announceOnlyGrants, announceOnImport } = QWEN_SCOPE_RULES[rule];
42811
+ if (!announceOnImport) continue;
42812
+ if (announceOnlyGrants && !grantsSomething(value, scopedKey)) continue;
42813
+ const globalNote = scopedKey.globalNote ?? QWEN_SCOPE_RULES[rule].globalNote;
42814
+ moduleLogger$1.warn(`Qwen permissions: imported ${quoteQualifiedKey({
42815
+ groupName,
42816
+ key
42817
+ })} = ${quoteValueForWarning(value)}. ${globalNote} Review it before generating with the global scope.`);
42818
+ }
42819
+ }
42820
+ }
42516
42821
  var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42517
42822
  constructor(params) {
42518
42823
  super({
@@ -42541,7 +42846,8 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42541
42846
  relativeDirPath: paths.relativeDirPath,
42542
42847
  relativeFilePath: paths.relativeFilePath,
42543
42848
  fileContent,
42544
- validate
42849
+ validate,
42850
+ global
42545
42851
  });
42546
42852
  }
42547
42853
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
@@ -42557,6 +42863,7 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42557
42863
  } catch (error) {
42558
42864
  throw new Error(`Failed to parse existing Qwen settings at ${filePath}: ${formatError(error)}`, { cause: error });
42559
42865
  }
42866
+ const displayPath = toPosixPath(join(global ? "~" : ".", paths.relativeDirPath, paths.relativeFilePath));
42560
42867
  const config = rulesyncPermissions.getJson();
42561
42868
  const { allow, ask, deny } = convertRulesyncToQwenPermissions(config);
42562
42869
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => toQwenToolName(category)));
@@ -42575,24 +42882,25 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42575
42882
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
42576
42883
  else delete mergedPermissions.deny;
42577
42884
  const override = config.qwencode;
42578
- if (override?.autoMode !== void 0) mergedPermissions.autoMode = override.autoMode;
42579
- const patch = { permissions: mergedPermissions };
42580
- if (override?.tools !== void 0) patch.tools = {
42581
- ...asPlainRecord(settings.tools),
42582
- ...asPlainRecord(override.tools)
42583
- };
42584
- if (override?.security !== void 0) {
42585
- const scopedSecurity = scopeOverrideSecurity(override.security, {
42885
+ if (override?.autoMode !== void 0) {
42886
+ const scopedAutoMode = scopeOverrideGroup({ autoMode: override.autoMode }, {
42887
+ groupName: "permissions",
42888
+ scopedKeys: QWEN_SCOPED_PERMISSIONS_KEYS,
42889
+ existingGroup: existingPermissions,
42586
42890
  global,
42587
- relativeFilePath: paths.relativeFilePath,
42891
+ filePath: displayPath,
42588
42892
  logger
42589
42893
  });
42590
- const mergedSecurity = {
42591
- ...asPlainRecord(settings.security),
42592
- ...scopedSecurity
42593
- };
42594
- if (Object.keys(mergedSecurity).length > 0) patch.security = mergedSecurity;
42894
+ if (scopedAutoMode.autoMode !== void 0) mergedPermissions.autoMode = scopedAutoMode.autoMode;
42595
42895
  }
42896
+ const patch = { permissions: mergedPermissions };
42897
+ Object.assign(patch, buildOverrideGroupsPatch({
42898
+ settings,
42899
+ override,
42900
+ global,
42901
+ filePath: displayPath,
42902
+ logger
42903
+ }));
42596
42904
  const fileContent = applySharedConfigPatch({
42597
42905
  fileKey: sharedConfigFileKey(paths),
42598
42906
  feature: "permissions",
@@ -42624,16 +42932,17 @@ var QwencodePermissions = class QwencodePermissions extends ToolPermissions {
42624
42932
  ask: permissions.ask ?? [],
42625
42933
  deny: permissions.deny ?? []
42626
42934
  });
42627
- const overrideTools = pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS);
42628
- const overrideSecurity = pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS);
42629
- for (const key of QWEN_GLOBAL_ONLY_SECURITY_KEYS) {
42630
- if (overrideSecurity[key] === void 0) continue;
42631
- 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.`);
42632
- }
42935
+ const overrideGroups = {
42936
+ tools: pickQwenOverrideKeys(settings.tools, QWEN_OVERRIDE_TOOLS_KEYS),
42937
+ security: pickQwenOverrideKeys(settings.security, QWEN_OVERRIDE_SECURITY_KEYS)
42938
+ };
42939
+ if (!this.global) warnAboutImportedScopedKeys(overrideGroups);
42633
42940
  const overridePermissions = pickQwenOverrideKeys(settings.permissions, QWEN_OVERRIDE_PERMISSIONS_KEYS);
42634
42941
  const qwencodeOverride = {};
42635
- if (Object.keys(overrideTools).length > 0) qwencodeOverride.tools = overrideTools;
42636
- if (Object.keys(overrideSecurity).length > 0) qwencodeOverride.security = overrideSecurity;
42942
+ for (const { groupName } of QWEN_OVERRIDE_GROUPS) {
42943
+ const group = overrideGroups[groupName];
42944
+ if (Object.keys(group).length > 0) qwencodeOverride[groupName] = group;
42945
+ }
42637
42946
  if (overridePermissions.autoMode !== void 0) qwencodeOverride.autoMode = overridePermissions.autoMode;
42638
42947
  const result = { ...config };
42639
42948
  if (Object.keys(qwencodeOverride).length > 0) result.qwencode = qwencodeOverride;
@@ -64900,8 +65209,10 @@ var RooRule = class RooRule extends ToolRule {
64900
65209
  nonRootPath: this.getSettablePaths().nonRoot
64901
65210
  });
64902
65211
  const mode = rulesyncRule.getFrontmatter().roo?.mode;
64903
- 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.`);
64904
- else params.relativeDirPath = join(dirname(params.relativeDirPath), rooModeRulesDirName(mode));
65212
+ if (!params.root && mode !== void 0 && mode !== "") {
65213
+ 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.`);
65214
+ else params.relativeDirPath = join(dirname(params.relativeDirPath), rooModeRulesDirName(mode));
65215
+ }
64905
65216
  return new RooRule(params);
64906
65217
  }
64907
65218
  /**
@@ -69137,4 +69448,4 @@ async function importChecksCore(params) {
69137
69448
  //#endregion
69138
69449
  export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
69139
69450
 
69140
- //# sourceMappingURL=import-DIDEUv63.js.map
69451
+ //# sourceMappingURL=import-DL8paEMP.js.map