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.
- package/dist/cli/index.cjs +25 -19
- package/dist/cli/index.js +25 -19
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-D420jbVB.cjs → import-CI0pN6s5.cjs} +389 -78
- package/dist/{import-DIDEUv63.js → import-DL8paEMP.js} +390 -79
- package/dist/import-DL8paEMP.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +9 -9
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +20 -20
- package/dist/import-DIDEUv63.js.map +0 -1
|
@@ -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
|
|
6662
|
-
* `
|
|
6663
|
-
* Qwen Code
|
|
6664
|
-
*
|
|
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 !== "")
|
|
22816
|
-
|
|
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)
|
|
24045
|
-
|
|
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 !== "")
|
|
24872
|
-
|
|
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)
|
|
28441
|
-
|
|
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)
|
|
28445
|
-
|
|
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)
|
|
28478
|
-
|
|
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)
|
|
28482
|
-
|
|
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)
|
|
28727
|
-
|
|
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)
|
|
29265
|
-
|
|
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)
|
|
29281
|
-
|
|
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)
|
|
32219
|
-
|
|
32220
|
-
|
|
32221
|
-
|
|
32222
|
-
|
|
32223
|
-
|
|
32224
|
-
|
|
32225
|
-
|
|
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)
|
|
33294
|
-
|
|
33295
|
-
|
|
33296
|
-
|
|
33297
|
-
|
|
33298
|
-
|
|
33299
|
-
|
|
33300
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
42528
|
-
*
|
|
42529
|
-
*
|
|
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
|
|
42532
|
-
|
|
42533
|
-
|
|
42534
|
-
|
|
42535
|
-
|
|
42536
|
-
|
|
42537
|
-
|
|
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)
|
|
42604
|
-
|
|
42605
|
-
|
|
42606
|
-
|
|
42607
|
-
|
|
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
|
-
|
|
42916
|
+
filePath: displayPath,
|
|
42613
42917
|
logger
|
|
42614
42918
|
});
|
|
42615
|
-
|
|
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
|
|
42653
|
-
|
|
42654
|
-
|
|
42655
|
-
|
|
42656
|
-
|
|
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
|
-
|
|
42661
|
-
|
|
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 !== "")
|
|
64929
|
-
|
|
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
|
/**
|