rulesync 43.6.1 → 43.6.3

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.
@@ -2291,6 +2291,190 @@ async function removeTempDirectory(tempDir) {
2291
2291
  });
2292
2292
  } catch {}
2293
2293
  }
2294
+ // src/utils/prototype-pollution.ts
2295
+ /**
2296
+ * Keys that, if walked into when constructing or merging objects from
2297
+ * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
2298
+ * chain) and propagate state to every other object in the runtime. Any code
2299
+ * that copies arbitrary user-supplied keys into a fresh object — frontmatter
2300
+ * parsing, MCP config conversion, settings round-trip — should skip these.
2301
+ */
2302
+ const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
2303
+ "__proto__",
2304
+ "constructor",
2305
+ "prototype"
2306
+ ]);
2307
+ function isPrototypePollutionKey(key) {
2308
+ return PROTOTYPE_POLLUTION_KEYS.has(key);
2309
+ }
2310
+ /**
2311
+ * Returns a shallow copy of a record's own entries with every
2312
+ * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
2313
+ *
2314
+ * Use when copying a nested, user-supplied string map — an MCP server's `env`
2315
+ * or `headers` table — into freshly generated config. Carrying such a map by
2316
+ * reference, or re-assigning its keys via bracket notation, would let a literal
2317
+ * `__proto__` key ride along (and re-assigning it would mutate the target's
2318
+ * prototype). Walking the entries through this helper severs that path while
2319
+ * preserving every legitimate key.
2320
+ */
2321
+ function omitPrototypePollutionKeys(record) {
2322
+ const sanitized = {};
2323
+ for (const [key, value] of Object.entries(record)) {
2324
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2325
+ sanitized[key] = value;
2326
+ }
2327
+ return sanitized;
2328
+ }
2329
+ /**
2330
+ * Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
2331
+ * reachable through the value (directly, or through an array) is copied with
2332
+ * its prototype-pollution keys dropped. Scalars and arrays of scalars come back
2333
+ * as they are.
2334
+ *
2335
+ * Use when passing an unknown, user-supplied value through unchanged — an MCP
2336
+ * server field a tool documents that rulesync does not model — so a
2337
+ * `__proto__` key nested anywhere inside it cannot ride into the generated
2338
+ * config, where the consuming tool may merge the object without the same care.
2339
+ */
2340
+ function omitPrototypePollutionKeysDeep(value) {
2341
+ if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
2342
+ if (value === null || typeof value !== "object") return value;
2343
+ const sanitized = {};
2344
+ for (const [key, entry] of Object.entries(value)) {
2345
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2346
+ sanitized[key] = omitPrototypePollutionKeysDeep(entry);
2347
+ }
2348
+ return sanitized;
2349
+ }
2350
+ // src/utils/jsonc.ts
2351
+ /**
2352
+ * Rebuild the parsed value from its own enumerable entries, dropping
2353
+ * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
2354
+ * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
2355
+ * literal `__proto__` key would replace the containing object's prototype
2356
+ * instead of becoming an own property; rebuilding gives every object a clean
2357
+ * `Object.prototype` again and severs that path.
2358
+ */
2359
+ function deepSanitize(value) {
2360
+ if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
2361
+ if (value !== null && typeof value === "object") {
2362
+ const sanitized = {};
2363
+ for (const [key, entry] of Object.entries(value)) {
2364
+ if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
2365
+ sanitized[key] = deepSanitize(entry);
2366
+ }
2367
+ return sanitized;
2368
+ }
2369
+ return value;
2370
+ }
2371
+ /**
2372
+ * Collect the paths of every prototype-pollution key in the document, walking
2373
+ * the syntax tree rather than the parsed value.
2374
+ *
2375
+ * The parsed value cannot answer this question for `__proto__`:
2376
+ * `obj["__proto__"] = "deny"` is discarded by the engine outright, and
2377
+ * `obj["__proto__"] = {...}` replaces the prototype instead of adding an own
2378
+ * property — either way the key is already gone by the time
2379
+ * {@link deepSanitize} could look for it, and only the source text still knows
2380
+ * the user wrote it. `constructor` and `prototype` do become own properties and
2381
+ * could be found in the parsed value, but they are collected here too so all
2382
+ * three keys are answered the same way.
2383
+ *
2384
+ * A pollution key's subtree is not descended into, matching
2385
+ * {@link deepSanitize}, which drops the whole value along with the key.
2386
+ */
2387
+ function collectPollutionKeyPaths({ node, path, found }) {
2388
+ if (node.type === "array") {
2389
+ for (const [index, child] of (node.children ?? []).entries()) collectPollutionKeyPaths({
2390
+ node: child,
2391
+ path: `${path}[${index}]`,
2392
+ found
2393
+ });
2394
+ return;
2395
+ }
2396
+ if (node.type !== "object") return;
2397
+ for (const property of node.children ?? []) {
2398
+ const [keyNode, valueNode] = property.children ?? [];
2399
+ if (typeof keyNode?.value !== "string") continue;
2400
+ const keyPath = path === "" ? keyNode.value : `${path}.${keyNode.value}`;
2401
+ if (PROTOTYPE_POLLUTION_KEYS.has(keyNode.value)) {
2402
+ found.push(keyPath);
2403
+ continue;
2404
+ }
2405
+ if (valueNode !== void 0) collectPollutionKeyPaths({
2406
+ node: valueNode,
2407
+ path: keyPath,
2408
+ found
2409
+ });
2410
+ }
2411
+ }
2412
+ /**
2413
+ * Parse a JSONC (JSON with Comments) document strictly.
2414
+ *
2415
+ * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
2416
+ * errors and returns a best-effort value), this throws on any parse error so
2417
+ * a malformed source file fails loudly instead of generating half-empty tool
2418
+ * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
2419
+ * `JSON.parse` on files that may contain comments or trailing commas.
2420
+ */
2421
+ function parseJsonc$6(content) {
2422
+ return deepSanitize(parseStrict(content));
2423
+ }
2424
+ /**
2425
+ * The strict parse shared by both entry points. Kept separate from the syntax
2426
+ * tree walk so the many callers that never look at `droppedKeys` do not pay
2427
+ * for building a second representation of the document.
2428
+ */
2429
+ function parseStrict(content) {
2430
+ const errors = [];
2431
+ const result = (0, jsonc_parser.parse)(content, errors, {
2432
+ allowTrailingComma: true,
2433
+ disallowComments: false
2434
+ });
2435
+ if (errors.length > 0) {
2436
+ const details = errors.map((e) => `${(0, jsonc_parser.printParseErrorCode)(e.error)} at offset ${e.offset}`).join(", ");
2437
+ throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
2438
+ }
2439
+ return result;
2440
+ }
2441
+ /**
2442
+ * The error a source document should fail with when
2443
+ * {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
2444
+ *
2445
+ * Shared by every source that reports them, so the three files a user can
2446
+ * author explain the same removal the same way rather than each inventing its
2447
+ * own wording.
2448
+ */
2449
+ function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
2450
+ return /* @__PURE__ */ new Error(`${JSON.stringify(sourcePath)} uses ${droppedKeys.map((key) => JSON.stringify(key)).join(", ")} as ${droppedKeys.length === 1 ? "a key" : "keys"}. Rulesync removes __proto__, constructor and prototype from every source document it parses, because assigning them would reach the prototype chain instead of the object. They are therefore never written to any tool's config — rename them rather than leaving entries that silently do nothing.`);
2451
+ }
2452
+ /**
2453
+ * The same parse as {@link parseJsonc}, additionally reporting which
2454
+ * prototype-pollution keys were removed, as dotted paths
2455
+ * (`permission.bash.__proto__`).
2456
+ *
2457
+ * The parse itself is identical — the paths only let a caller tell the user
2458
+ * about entries that vanished, which they would otherwise have to notice by
2459
+ * comparing their source file against the generated output.
2460
+ */
2461
+ function parseJsoncReportingDroppedKeys({ content }) {
2462
+ const result = parseStrict(content);
2463
+ const found = [];
2464
+ const tree = (0, jsonc_parser.parseTree)(content, [], {
2465
+ allowTrailingComma: true,
2466
+ disallowComments: false
2467
+ });
2468
+ if (tree !== void 0) collectPollutionKeyPaths({
2469
+ node: tree,
2470
+ path: "",
2471
+ found
2472
+ });
2473
+ return {
2474
+ value: deepSanitize(result),
2475
+ droppedKeys: [...new Set(found)]
2476
+ };
2477
+ }
2294
2478
  // src/types/json-output.ts
2295
2479
  /**
2296
2480
  * Error codes for CLI errors
@@ -3580,10 +3764,37 @@ const getDefaults = () => ({
3580
3764
  inputRoots: void 0,
3581
3765
  sources: []
3582
3766
  });
3767
+ /**
3768
+ * Parse the content of a config file (`rulesync.jsonc` / `rulesync.local.jsonc`)
3769
+ * through the same strict, sanitizing JSONC parser as every other rulesync
3770
+ * source.
3771
+ *
3772
+ * `jsonc-parser`'s bare `parse` recovers from syntax errors without reporting
3773
+ * them, which let a typo (a missing comma, unquoted keys, trailing garbage)
3774
+ * silently drop keys and run rulesync with a different config than the user
3775
+ * wrote. Prototype-pollution keys are rejected rather than silently removed,
3776
+ * matching the hooks, MCP and permissions sources.
3777
+ */
3778
+ function parseConfigFileContent({ content, filePath }) {
3779
+ let result;
3780
+ try {
3781
+ result = parseJsoncReportingDroppedKeys({ content });
3782
+ } catch (error) {
3783
+ const detail = error instanceof Error ? error.message : String(error);
3784
+ throw new Error(`Failed to parse ${JSON.stringify(filePath)}: ${detail}`, { cause: error });
3785
+ }
3786
+ if (result.droppedKeys.length > 0) throw droppedPollutionKeysError({
3787
+ sourcePath: filePath,
3788
+ droppedKeys: result.droppedKeys
3789
+ });
3790
+ return result.value;
3791
+ }
3583
3792
  const loadConfigFromFile = async (filePath) => {
3584
3793
  if (!await fileExists(filePath)) return {};
3585
- const fileContent = await readFileContent(filePath);
3586
- const jsonData = (0, jsonc_parser.parse)(fileContent);
3794
+ const jsonData = parseConfigFileContent({
3795
+ content: await readFileContent(filePath),
3796
+ filePath
3797
+ });
3587
3798
  const { $schema: _schema, ...configParams } = ConfigFileSchema.parse(jsonData);
3588
3799
  assertTargetsFeaturesExclusive({
3589
3800
  targets: configParams.targets,
@@ -4196,62 +4407,6 @@ function createBoundedWalk({ subject, limits, root }) {
4196
4407
  leave
4197
4408
  };
4198
4409
  }
4199
- // src/utils/prototype-pollution.ts
4200
- /**
4201
- * Keys that, if walked into when constructing or merging objects from
4202
- * untrusted input, can mutate `Object.prototype` (or otherwise the prototype
4203
- * chain) and propagate state to every other object in the runtime. Any code
4204
- * that copies arbitrary user-supplied keys into a fresh object — frontmatter
4205
- * parsing, MCP config conversion, settings round-trip — should skip these.
4206
- */
4207
- const PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
4208
- "__proto__",
4209
- "constructor",
4210
- "prototype"
4211
- ]);
4212
- function isPrototypePollutionKey(key) {
4213
- return PROTOTYPE_POLLUTION_KEYS.has(key);
4214
- }
4215
- /**
4216
- * Returns a shallow copy of a record's own entries with every
4217
- * prototype-pollution key (`__proto__`, `constructor`, `prototype`) dropped.
4218
- *
4219
- * Use when copying a nested, user-supplied string map — an MCP server's `env`
4220
- * or `headers` table — into freshly generated config. Carrying such a map by
4221
- * reference, or re-assigning its keys via bracket notation, would let a literal
4222
- * `__proto__` key ride along (and re-assigning it would mutate the target's
4223
- * prototype). Walking the entries through this helper severs that path while
4224
- * preserving every legitimate key.
4225
- */
4226
- function omitPrototypePollutionKeys(record) {
4227
- const sanitized = {};
4228
- for (const [key, value] of Object.entries(record)) {
4229
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4230
- sanitized[key] = value;
4231
- }
4232
- return sanitized;
4233
- }
4234
- /**
4235
- * Like {@link omitPrototypePollutionKeys}, but recursive: every plain object
4236
- * reachable through the value (directly, or through an array) is copied with
4237
- * its prototype-pollution keys dropped. Scalars and arrays of scalars come back
4238
- * as they are.
4239
- *
4240
- * Use when passing an unknown, user-supplied value through unchanged — an MCP
4241
- * server field a tool documents that rulesync does not model — so a
4242
- * `__proto__` key nested anywhere inside it cannot ride into the generated
4243
- * config, where the consuming tool may merge the object without the same care.
4244
- */
4245
- function omitPrototypePollutionKeysDeep(value) {
4246
- if (Array.isArray(value)) return value.map((entry) => omitPrototypePollutionKeysDeep(entry));
4247
- if (value === null || typeof value !== "object") return value;
4248
- const sanitized = {};
4249
- for (const [key, entry] of Object.entries(value)) {
4250
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
4251
- sanitized[key] = omitPrototypePollutionKeysDeep(entry);
4252
- }
4253
- return sanitized;
4254
- }
4255
4410
  // src/utils/type-guards.ts
4256
4411
  /**
4257
4412
  * Type guard to check if a value is a plain object (Record<string, unknown>).
@@ -4879,6 +5034,24 @@ function lookupOwn({ record, key }) {
4879
5034
  return Object.hasOwn(record, key) ? record[key] : void 0;
4880
5035
  }
4881
5036
  /**
5037
+ * Return the value a map holds under its own `key`, first storing `create()`
5038
+ * there when it has none. A bracket read would find an inherited member for a
5039
+ * key such as `toString`, so an importer bucketing rules by a user-supplied
5040
+ * name would write into an `Object.prototype` function instead of a fresh
5041
+ * bucket.
5042
+ */
5043
+ function getOrCreateOwn({ record, key, create }) {
5044
+ if (Object.hasOwn(record, key)) return record[key];
5045
+ const created = create();
5046
+ Object.defineProperty(record, key, {
5047
+ value: created,
5048
+ writable: true,
5049
+ enumerable: true,
5050
+ configurable: true
5051
+ });
5052
+ return created;
5053
+ }
5054
+ /**
4882
5055
  * Swap the keys and values of a name map, e.g. a canonical-to-tool map into
4883
5056
  * the tool-to-canonical map an importer reads. When two keys share a value,
4884
5057
  * the later key wins.
@@ -6413,14 +6586,16 @@ const CANONICAL_TO_GOOSE_EVENT_NAMES = {
6413
6586
  * Hook events supported by Goose.
6414
6587
  *
6415
6588
  * Goose adopts the Open Plugins hooks spec: each plugin's `hooks/hooks.json`
6416
- * maps PascalCase event names to matcher/handler arrays. Every Goose event has a
6417
- * 1:1 canonical equivalent, so no new canonical events are required.
6418
- *
6419
- * Goose's `HookEvent` enum defines exactly these 11 events (v1.41.0). Notably it
6420
- * has NO `SubagentStart`/`SubagentStop` arms — emitting them would write keys
6421
- * Goose silently ignores, so `subagentStart`/`subagentStop` are intentionally
6589
+ * maps PascalCase event names to matcher/handler arrays.
6590
+ *
6591
+ * Goose's `HookEvent` enum defines these 11 events plus `PreToolUseResult`
6592
+ * (added in v1.48.0), which has no canonical equivalent: it is authored under
6593
+ * the `goose.hooks` override block by its native name, emitted verbatim, and
6594
+ * imported back into that block. Notably the enum has NO
6595
+ * `SubagentStart`/`SubagentStop` arms — emitting them would write keys Goose
6596
+ * silently ignores, so `subagentStart`/`subagentStop` are intentionally
6422
6597
  * excluded here and from `CANONICAL_TO_GOOSE_EVENT_NAMES`.
6423
- * @see https://github.com/aaif-goose/goose/blob/v1.41.0/crates/goose/src/hooks/mod.rs
6598
+ * @see https://github.com/aaif-goose/goose/blob/v1.48.0/crates/goose/src/hooks/mod.rs
6424
6599
  * @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
6425
6600
  */
6426
6601
  const GOOSE_HOOK_EVENTS = eventsOf(CANONICAL_TO_GOOSE_EVENT_NAMES);
@@ -6898,134 +7073,6 @@ const GROKCLI_HOOK_EVENTS = eventsOf(CANONICAL_TO_GROKCLI_EVENT_NAMES);
6898
7073
  * Map Grok CLI PascalCase event names to canonical camelCase.
6899
7074
  */
6900
7075
  const GROKCLI_TO_CANONICAL_EVENT_NAMES = invertRecord(CANONICAL_TO_GROKCLI_EVENT_NAMES);
6901
- // src/utils/jsonc.ts
6902
- /**
6903
- * Rebuild the parsed value from its own enumerable entries, dropping
6904
- * prototype-pollution keys (`__proto__`, `constructor`, `prototype`).
6905
- * `jsonc-parser` assigns keys with plain `obj[key] = value` semantics, so a
6906
- * literal `__proto__` key would replace the containing object's prototype
6907
- * instead of becoming an own property; rebuilding gives every object a clean
6908
- * `Object.prototype` again and severs that path.
6909
- */
6910
- function deepSanitize(value) {
6911
- if (Array.isArray(value)) return value.map((item) => deepSanitize(item));
6912
- if (value !== null && typeof value === "object") {
6913
- const sanitized = {};
6914
- for (const [key, entry] of Object.entries(value)) {
6915
- if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
6916
- sanitized[key] = deepSanitize(entry);
6917
- }
6918
- return sanitized;
6919
- }
6920
- return value;
6921
- }
6922
- /**
6923
- * Collect the paths of every prototype-pollution key in the document, walking
6924
- * the syntax tree rather than the parsed value.
6925
- *
6926
- * The parsed value cannot answer this question for `__proto__`:
6927
- * `obj["__proto__"] = "deny"` is discarded by the engine outright, and
6928
- * `obj["__proto__"] = {...}` replaces the prototype instead of adding an own
6929
- * property — either way the key is already gone by the time
6930
- * {@link deepSanitize} could look for it, and only the source text still knows
6931
- * the user wrote it. `constructor` and `prototype` do become own properties and
6932
- * could be found in the parsed value, but they are collected here too so all
6933
- * three keys are answered the same way.
6934
- *
6935
- * A pollution key's subtree is not descended into, matching
6936
- * {@link deepSanitize}, which drops the whole value along with the key.
6937
- */
6938
- function collectPollutionKeyPaths({ node, path, found }) {
6939
- if (node.type === "array") {
6940
- for (const [index, child] of (node.children ?? []).entries()) collectPollutionKeyPaths({
6941
- node: child,
6942
- path: `${path}[${index}]`,
6943
- found
6944
- });
6945
- return;
6946
- }
6947
- if (node.type !== "object") return;
6948
- for (const property of node.children ?? []) {
6949
- const [keyNode, valueNode] = property.children ?? [];
6950
- if (typeof keyNode?.value !== "string") continue;
6951
- const keyPath = path === "" ? keyNode.value : `${path}.${keyNode.value}`;
6952
- if (PROTOTYPE_POLLUTION_KEYS.has(keyNode.value)) {
6953
- found.push(keyPath);
6954
- continue;
6955
- }
6956
- if (valueNode !== void 0) collectPollutionKeyPaths({
6957
- node: valueNode,
6958
- path: keyPath,
6959
- found
6960
- });
6961
- }
6962
- }
6963
- /**
6964
- * Parse a JSONC (JSON with Comments) document strictly.
6965
- *
6966
- * Unlike `jsonc-parser`'s bare `parse` (which silently tolerates syntax
6967
- * errors and returns a best-effort value), this throws on any parse error so
6968
- * a malformed source file fails loudly instead of generating half-empty tool
6969
- * configs. Plain JSON is valid JSONC, so this is a drop-in replacement for
6970
- * `JSON.parse` on files that may contain comments or trailing commas.
6971
- */
6972
- function parseJsonc$6(content) {
6973
- return deepSanitize(parseStrict(content));
6974
- }
6975
- /**
6976
- * The strict parse shared by both entry points. Kept separate from the syntax
6977
- * tree walk so the many callers that never look at `droppedKeys` do not pay
6978
- * for building a second representation of the document.
6979
- */
6980
- function parseStrict(content) {
6981
- const errors = [];
6982
- const result = (0, jsonc_parser.parse)(content, errors, {
6983
- allowTrailingComma: true,
6984
- disallowComments: false
6985
- });
6986
- if (errors.length > 0) {
6987
- const details = errors.map((e) => `${(0, jsonc_parser.printParseErrorCode)(e.error)} at offset ${e.offset}`).join(", ");
6988
- throw new SyntaxError(`Failed to parse JSONC content: ${details}`);
6989
- }
6990
- return result;
6991
- }
6992
- /**
6993
- * The error a source document should fail with when
6994
- * {@link parseJsoncReportingDroppedKeys} reports keys the parser removed.
6995
- *
6996
- * Shared by every source that reports them, so the three files a user can
6997
- * author explain the same removal the same way rather than each inventing its
6998
- * own wording.
6999
- */
7000
- function droppedPollutionKeysError({ sourcePath, droppedKeys }) {
7001
- return /* @__PURE__ */ new Error(`${JSON.stringify(sourcePath)} uses ${droppedKeys.map((key) => JSON.stringify(key)).join(", ")} as ${droppedKeys.length === 1 ? "a key" : "keys"}. Rulesync removes __proto__, constructor and prototype from every source document it parses, because assigning them would reach the prototype chain instead of the object. They are therefore never written to any tool's config — rename them rather than leaving entries that silently do nothing.`);
7002
- }
7003
- /**
7004
- * The same parse as {@link parseJsonc}, additionally reporting which
7005
- * prototype-pollution keys were removed, as dotted paths
7006
- * (`permission.bash.__proto__`).
7007
- *
7008
- * The parse itself is identical — the paths only let a caller tell the user
7009
- * about entries that vanished, which they would otherwise have to notice by
7010
- * comparing their source file against the generated output.
7011
- */
7012
- function parseJsoncReportingDroppedKeys({ content }) {
7013
- const result = parseStrict(content);
7014
- const found = [];
7015
- const tree = (0, jsonc_parser.parseTree)(content, [], {
7016
- allowTrailingComma: true,
7017
- disallowComments: false
7018
- });
7019
- if (tree !== void 0) collectPollutionKeyPaths({
7020
- node: tree,
7021
- path: "",
7022
- found
7023
- });
7024
- return {
7025
- value: deepSanitize(result),
7026
- droppedKeys: [...new Set(found)]
7027
- };
7028
- }
7029
7076
  // src/utils/rulesync-source-path.ts
7030
7077
  /**
7031
7078
  * No `.rulesync/` source file exists for a feature at any of its candidate
@@ -9698,11 +9745,9 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
9698
9745
  * `PERMISSION_OVERRIDE_KEY_ALIASES`) is replaced wholesale, and removed when
9699
9746
  * the import does not produce one: only that tool's import ever writes it, so
9700
9747
  * a setting removed from the tool's own config has to disappear here too.
9701
- * Any other tool's override block the import carries (Hermes restores them
9702
- * from its provenance) merges one level down: an imported field replaces the
9703
- * existing field, fields the import does not name are kept, and a
9704
- * `permission` record inside the block merges per category. Every other
9705
- * top-level key — `$schema` included — is kept unless the import names it.
9748
+ * Every other top-level key — `$schema` included — is kept unless the import
9749
+ * names it, in which case the imported value replaces it. No importer emits
9750
+ * another tool's override block, so those blocks are never touched here.
9706
9751
  */
9707
9752
  static mergeImportedJson({ existing, imported, toolTarget }) {
9708
9753
  const ownOverrideKey = PERMISSION_OVERRIDE_KEY_ALIASES[toolTarget] ?? toolTarget;
@@ -9714,14 +9759,7 @@ var RulesyncPermissions = class RulesyncPermissions extends RulesyncFile {
9714
9759
  existing: current,
9715
9760
  imported: value
9716
9761
  });
9717
- else if (isRecord(current) && isRecord(value)) {
9718
- const block = { ...current };
9719
- for (const [field, fieldValue] of Object.entries(value)) block[field] = field === "permission" ? mergePermissionCategories({
9720
- existing: block[field],
9721
- imported: fieldValue
9722
- }) : fieldValue;
9723
- merged[key] = block;
9724
- } else merged[key] = value;
9762
+ else merged[key] = value;
9725
9763
  }
9726
9764
  return merged;
9727
9765
  }
@@ -19580,9 +19618,6 @@ var KiloCommand = class extends FrontmatterMarkdownToolCommand {
19580
19618
  static getFrontmatterSchema() {
19581
19619
  return KiloCommandFrontmatterSchema;
19582
19620
  }
19583
- getRulesyncOutputRoot() {
19584
- return process.cwd();
19585
- }
19586
19621
  /**
19587
19622
  * Refuse to write a command Kilo reserves (see
19588
19623
  * {@link KILO_RESERVED_COMMAND_NAMES}). The name Kilo loads is the file
@@ -20871,9 +20906,6 @@ var OpenCodeCommand = class extends FrontmatterMarkdownToolCommand {
20871
20906
  ...Object.keys(restFields).length > 0 && { [toolTarget]: restFields }
20872
20907
  };
20873
20908
  }
20874
- getRulesyncOutputRoot() {
20875
- return process.cwd();
20876
- }
20877
20909
  /**
20878
20910
  * Imports commands defined inline in `opencode.json` / `opencode.jsonc` (and,
20879
20911
  * at project scope, `.opencode/opencode.json(c)`, which OpenCode merges over
@@ -52388,6 +52420,29 @@ var AmpHooks = class AmpHooks extends ToolHooks {
52388
52420
  });
52389
52421
  }
52390
52422
  };
52423
+ // src/features/hooks/hook-matcher-tool-names.ts
52424
+ const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
52425
+ function isMatcherName(token) {
52426
+ return !token.startsWith("\\") && !token.startsWith("[");
52427
+ }
52428
+ /**
52429
+ * The distinct whole names in a matcher that start with a capital letter, for
52430
+ * adapters whose runtime tool names are all lowercase: after translation, such
52431
+ * a name has no runtime equivalent and can never match.
52432
+ */
52433
+ function capitalizedMatcherNames(matcher) {
52434
+ const names = [...matcher.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
52435
+ return [...new Set(names)];
52436
+ }
52437
+ /**
52438
+ * Rewrites the whole tool names inside a hook `matcher` regex through `names`,
52439
+ * leaving escapes, character classes and unknown names as written. Used by
52440
+ * adapters whose runtime tool names differ from the canonical (Claude Code)
52441
+ * spelling.
52442
+ */
52443
+ function replaceMatcherNames(matcher, names) {
52444
+ return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
52445
+ }
52391
52446
  // src/utils/object.ts
52392
52447
  /**
52393
52448
  * Return a shallow copy of `obj` keeping only the entries whose value is
@@ -54308,6 +54363,48 @@ const ANTIGRAVITY_FLAT_HANDLER_EVENTS = new Set([...ANTIGRAVITY_NO_MATCHER_EVENT
54308
54363
  record: CANONICAL_TO_ANTIGRAVITY_EVENT_NAMES,
54309
54364
  key: event
54310
54365
  }) ?? event));
54366
+ const CANONICAL_TO_ANTIGRAVITY_TOOL_NAMES = {
54367
+ Bash: "run_command",
54368
+ Read: "(?:read_file|view_file)",
54369
+ Write: "(?:write_file|write_to_file)",
54370
+ Edit: "(?:edit_file|replace_file_content)",
54371
+ WebSearch: "search_web"
54372
+ };
54373
+ const ANTIGRAVITY_TO_CANONICAL_TOOL_NAMES = {
54374
+ run_command: "Bash",
54375
+ read_file: "Read",
54376
+ view_file: "Read",
54377
+ write_file: "Write",
54378
+ write_to_file: "Write",
54379
+ edit_file: "Edit",
54380
+ replace_file_content: "Edit",
54381
+ search_web: "WebSearch"
54382
+ };
54383
+ const ANTIGRAVITY_GROUP_TO_CANONICAL_TOOL_NAMES = Object.entries(CANONICAL_TO_ANTIGRAVITY_TOOL_NAMES).filter(([, name]) => name.startsWith("(?:")).map(([canonical, group]) => [group, canonical]);
54384
+ const ANTIGRAVITY_DOCUMENTED_TOOL_CANDIDATES = {
54385
+ Grep: "grep_search",
54386
+ Glob: "find_by_name",
54387
+ WebFetch: "read_url_content",
54388
+ MultiEdit: "multi_replace_file_content"
54389
+ };
54390
+ function canonicalToAntigravityMatcher({ matcher, eventName, warn }) {
54391
+ const translated = replaceMatcherNames(matcher, CANONICAL_TO_ANTIGRAVITY_TOOL_NAMES);
54392
+ const unmatched = capitalizedMatcherNames(translated);
54393
+ if (unmatched.length > 0) {
54394
+ const candidates = unmatched.flatMap((name) => {
54395
+ const candidate = lookupOwn({
54396
+ record: ANTIGRAVITY_DOCUMENTED_TOOL_CANDIDATES,
54397
+ key: name
54398
+ });
54399
+ return candidate ? [`\`${candidate}\` for ${quoteValueForWarning(name)}`] : [];
54400
+ });
54401
+ warn(`Antigravity reports lowercase tool names in the hook payload (\`toolCall.name\`), and ${unmatched.map((name) => quoteValueForWarning(name)).join(", ")} in the "${eventName}" matcher ${quoteValueForWarning(matcher)} has no verified Antigravity equivalent, so it is written as-is and will likely never match. Use the tool name Antigravity reports instead` + (candidates.length > 0 ? ` (the Antigravity hooks docs list ${candidates.join(", ")}, unverified).` : "."));
54402
+ }
54403
+ return translated;
54404
+ }
54405
+ function antigravityToCanonicalMatcher(matcher) {
54406
+ return replaceMatcherNames(ANTIGRAVITY_GROUP_TO_CANONICAL_TOOL_NAMES.reduce((current, [group, canonical]) => current.split(group).join(canonical), matcher), ANTIGRAVITY_TO_CANONICAL_TOOL_NAMES);
54407
+ }
54311
54408
  /**
54312
54409
  * Antigravity uses a Claude-Code-like `hooks.json`. The `PreToolUse` /
54313
54410
  * `PostToolUse` events use the matcher-based group shape, while `SessionStart`,
@@ -54321,7 +54418,11 @@ const ANTIGRAVITY_CONVERTER_CONFIG = {
54321
54418
  toolToCanonicalEventNames: ANTIGRAVITY_TO_CANONICAL_EVENT_NAMES,
54322
54419
  projectDirVar: "",
54323
54420
  noMatcherEvents: ANTIGRAVITY_NO_MATCHER_EVENTS,
54324
- supportedHookTypes: /* @__PURE__ */ new Set(["command"])
54421
+ supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
54422
+ matcherTranslation: {
54423
+ toTool: canonicalToAntigravityMatcher,
54424
+ toCanonical: antigravityToCanonicalMatcher
54425
+ }
54325
54426
  };
54326
54427
  /**
54327
54428
  * Turn the shared converter's matcher-group output into Antigravity's shape:
@@ -57354,20 +57455,6 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
57354
57455
  }), null, 2) });
57355
57456
  }
57356
57457
  };
57357
- // src/features/hooks/hook-matcher-tool-names.ts
57358
- const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
57359
- function isMatcherName(token) {
57360
- return !token.startsWith("\\") && !token.startsWith("[");
57361
- }
57362
- /**
57363
- * Rewrites the whole tool names inside a hook `matcher` regex through `names`,
57364
- * leaving escapes, character classes and unknown names as written. Used by
57365
- * adapters whose runtime tool names differ from the canonical (Claude Code)
57366
- * spelling.
57367
- */
57368
- function replaceMatcherNames(matcher, names) {
57369
- return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
57370
- }
57371
57458
  // src/features/hooks/cortexcode-hooks.ts
57372
57459
  const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
57373
57460
  const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
@@ -57381,8 +57468,8 @@ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
57381
57468
  const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
57382
57469
  function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
57383
57470
  const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
57384
- const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
57385
- if (unmatched.length > 0) warn(`Cortex Code matches hook matchers case-sensitively against lowercase runtime tool names, and ${[...new Set(unmatched)].map((name) => quoteValueForWarning(name)).join(", ")} in the "${eventName}" matcher ${quoteValueForWarning(matcher)} is not a tool Cortex Code documents a runtime ID for, so it is written as-is and will likely never match. Replace it with the lowercase name Cortex Code uses for that tool, if it has one.`);
57471
+ const unmatched = capitalizedMatcherNames(translated);
57472
+ if (unmatched.length > 0) warn(`Cortex Code matches hook matchers case-sensitively against lowercase runtime tool names, and ${unmatched.map((name) => quoteValueForWarning(name)).join(", ")} in the "${eventName}" matcher ${quoteValueForWarning(matcher)} is not a tool Cortex Code documents a runtime ID for, so it is written as-is and will likely never match. Replace it with the lowercase name Cortex Code uses for that tool, if it has one.`);
57386
57473
  return translated;
57387
57474
  }
57388
57475
  function cortexcodeToCanonicalMatcher(matcher) {
@@ -58630,12 +58717,11 @@ var GooseHooks = class GooseHooks extends ToolHooks {
58630
58717
  });
58631
58718
  }
58632
58719
  toRulesyncHooks({ logger } = {}) {
58633
- const parsed = this.parseFileContentOrThrow({
58634
- displayName: "Goose",
58635
- parse: (fileContent) => JSON.parse(fileContent)
58636
- });
58637
58720
  const hooks = toolHooksToCanonical({
58638
- hooks: parsed.hooks && typeof parsed.hooks === "object" && !Array.isArray(parsed.hooks) ? Object.fromEntries(Object.entries(parsed.hooks).filter(([eventName]) => Object.hasOwn(GOOSE_TO_CANONICAL_EVENT_NAMES, eventName))) : parsed.hooks,
58721
+ hooks: this.parseFileContentOrThrow({
58722
+ displayName: "Goose",
58723
+ parse: (fileContent) => JSON.parse(fileContent)
58724
+ }).hooks,
58639
58725
  converterConfig: GOOSE_CONVERTER_CONFIG,
58640
58726
  logger
58641
58727
  });
@@ -61902,12 +61988,14 @@ var ReasonixHooks = class ReasonixHooks extends ToolHooks {
61902
61988
  const paths = ReasonixHooks.getSettablePaths({ global });
61903
61989
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
61904
61990
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
61905
- let settings;
61991
+ let parsed;
61906
61992
  try {
61907
- settings = JSON.parse(existingContent);
61993
+ parsed = JSON.parse(existingContent);
61908
61994
  } catch (error) {
61909
61995
  throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: ${formatError(error)}`, { cause: error });
61910
61996
  }
61997
+ if (!isPlainObject(parsed)) throw new Error(`Failed to parse existing Reasonix settings at ${filePath}: expected a mapping at the root`);
61998
+ const settings = parsed;
61911
61999
  const config = rulesyncHooks.getJson();
61912
62000
  const reasonixHooks = canonicalToReasonixHooks({
61913
62001
  config,
@@ -62866,15 +62954,18 @@ function emittedEventsWithHook(params) {
62866
62954
  }
62867
62955
  /**
62868
62956
  * The capabilities an adapter built on the shared hooks converter already
62869
- * declares in its converter config: the events it emits and the hook types it
62870
- * keeps. Reading them from there keeps the processor's skip warnings in step
62871
- * with what the converter actually writes.
62957
+ * declares in its converter config: the events it emits, the hook types it
62958
+ * keeps, and whether it writes the per-hook `enabled` flag. Reading them from
62959
+ * there keeps the processor's skip and dropped-field warnings in step with
62960
+ * what the converter actually writes.
62872
62961
  */
62873
62962
  function converterCapabilities(config) {
62874
62963
  if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
62964
+ const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
62875
62965
  return {
62876
62966
  supportedEvents: config.supportedEvents,
62877
- supportedHookTypes: [...config.supportedHookTypes]
62967
+ supportedHookTypes: [...config.supportedHookTypes],
62968
+ ...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
62878
62969
  };
62879
62970
  }
62880
62971
  const toolHooksFactories = /* @__PURE__ */ new Map([
@@ -63496,8 +63587,7 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
63496
63587
  supportsImport: true
63497
63588
  },
63498
63589
  ...converterCapabilities(ZCODE_CONVERTER_CONFIG),
63499
- supportsMatcher: true,
63500
- supportsPerHookEnabled: true
63590
+ supportsMatcher: true
63501
63591
  }],
63502
63592
  ["zcode-plugin", {
63503
63593
  class: ZcodePluginHooks,
@@ -63507,10 +63597,12 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
63507
63597
  supportsImport: true
63508
63598
  },
63509
63599
  ...converterCapabilities(ZCODE_PLUGIN_CONVERTER_CONFIG),
63510
- supportsMatcher: true,
63511
- supportsPerHookEnabled: true
63600
+ supportsMatcher: true
63512
63601
  }]
63513
63602
  ]);
63603
+ function targetsSupporting(capability) {
63604
+ return [...toolHooksFactories.entries()].filter(([, f]) => f[capability]).map(([t]) => t).join(" / ");
63605
+ }
63514
63606
  const hooksProcessorToolTargets = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject).map(([t]) => t);
63515
63607
  const hooksProcessorToolTargetsGlobal = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsGlobal).map(([t]) => t);
63516
63608
  const hooksProcessorToolTargetsImportable = [...toolHooksFactories.entries()].filter(([, f]) => f.meta.supportsProject && f.meta.supportsImport).map(([t]) => t);
@@ -63671,7 +63763,7 @@ var HooksProcessor = class extends FeatureProcessor {
63671
63763
  effectiveHooks,
63672
63764
  predicate: (def) => def.enabled === false
63673
63765
  });
63674
- if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the kiro-cli / kiro-ide / zcode hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
63766
+ if (eventsWithDisabledHooks.length > 0) this.logger.warn(`Emitting "enabled: false" hook(s) as active for ${this.toolTarget} (only the ${targetsSupporting("supportsPerHookEnabled")} hooks formats support the flag): ${eventsWithDisabledHooks.join(", ")}`);
63675
63767
  }
63676
63768
  if (!factory.supportsPerHookConfirm) {
63677
63769
  const eventsWithConfirmHooks = emittedEventsWithHook({
@@ -63680,7 +63772,7 @@ var HooksProcessor = class extends FeatureProcessor {
63680
63772
  effectiveHooks,
63681
63773
  predicate: (def) => def.confirm !== void 0
63682
63774
  });
63683
- if (eventsWithConfirmHooks.length > 0) this.logger.warn(`Emitting hook(s) without their "confirm" prompt for ${this.toolTarget} (only the kiro-cli / kiro-ide hooks formats support it): ${eventsWithConfirmHooks.join(", ")}`);
63775
+ if (eventsWithConfirmHooks.length > 0) this.logger.warn(`Emitting hook(s) without their "confirm" prompt for ${this.toolTarget} (only the ${targetsSupporting("supportsPerHookConfirm")} hooks formats support it): ${eventsWithConfirmHooks.join(", ")}`);
63684
63776
  }
63685
63777
  }
63686
63778
  async convertToolFilesToRulesyncFiles(toolFiles) {
@@ -64943,12 +65035,7 @@ var ReasonixIgnore = class ReasonixIgnore extends ToolIgnore {
64943
65035
  }
64944
65036
  toRulesyncIgnore() {
64945
65037
  const rulesyncPatterns = (0, es_toolkit.uniq)(this.patterns.map((entry) => readDenyPatternOf(entry)).filter((pattern) => pattern !== null && pattern.length > 0));
64946
- return new RulesyncIgnore({
64947
- outputRoot: this.outputRoot,
64948
- relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
64949
- relativeFilePath: RulesyncIgnore.getSettablePaths().recommended.relativeFilePath,
64950
- fileContent: rulesyncPatterns.join("\n")
64951
- });
65038
+ return this.toRulesyncIgnoreDefault({ fileContent: rulesyncPatterns.join("\n") });
64952
65039
  }
64953
65040
  static async fromRulesyncIgnore({ outputRoot = process.cwd(), rulesyncIgnore, global = false, logger }) {
64954
65041
  await warnIfReasonixNestedProjectConfigShadowed({
@@ -78655,11 +78742,17 @@ function convertAmpToRulesync({ disable, permissions }) {
78655
78742
  const assign = (tool, pattern, action) => {
78656
78743
  if (isPrototypePollutionKey(tool)) return;
78657
78744
  if (isPrototypePollutionKey(pattern)) return;
78658
- const bucket = permission[tool] ??= {};
78659
- const existing = bucket[pattern];
78745
+ const bucket = getOrCreateOwn({
78746
+ record: permission,
78747
+ key: tool,
78748
+ create: () => ({})
78749
+ });
78660
78750
  if (isStricterAction({
78661
78751
  action,
78662
- existing
78752
+ existing: lookupOwn({
78753
+ record: bucket,
78754
+ key: pattern
78755
+ })
78663
78756
  })) bucket[pattern] = action;
78664
78757
  };
78665
78758
  for (const tool of disable) assign(tool, "*", "deny");
@@ -80062,8 +80155,12 @@ function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory,
80062
80155
  logger,
80063
80156
  toolLabel
80064
80157
  }) : target;
80065
- if (!permission[canonical]) permission[canonical] = {};
80066
- permission[canonical][pattern] = action;
80158
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
80159
+ getOrCreateOwn({
80160
+ record: permission,
80161
+ key: canonical,
80162
+ create: () => ({})
80163
+ })[pattern] = action;
80067
80164
  }
80068
80165
  };
80069
80166
  processEntries(allow, "allow");
@@ -80131,10 +80228,16 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
80131
80228
  };
80132
80229
  function toAntigravityCliToolName(canonical) {
80133
80230
  if (canonical.startsWith("mcp__")) return "mcp";
80134
- return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
80231
+ return lookupOwn({
80232
+ record: CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES,
80233
+ key: canonical
80234
+ }) ?? canonical;
80135
80235
  }
80136
80236
  function toCanonicalToolName$9(cliName) {
80137
- return ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES[cliName] ?? cliName;
80237
+ return lookupOwn({
80238
+ record: ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES,
80239
+ key: cliName
80240
+ }) ?? cliName;
80138
80241
  }
80139
80242
  /**
80140
80243
  * Permissions generator for the Google Antigravity CLI (`agy`, Antigravity 2.0).
@@ -80297,10 +80400,16 @@ const IDE_ACTION_TO_CANONICAL = {
80297
80400
  };
80298
80401
  function toIdeAction(canonical) {
80299
80402
  if (canonical.startsWith("mcp__")) return "mcp";
80300
- return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
80403
+ return lookupOwn({
80404
+ record: CANONICAL_TO_IDE_ACTION,
80405
+ key: canonical
80406
+ }) ?? canonical;
80301
80407
  }
80302
80408
  function toCanonicalCategory$2(ideAction) {
80303
- return IDE_ACTION_TO_CANONICAL[ideAction] ?? ideAction;
80409
+ return lookupOwn({
80410
+ record: IDE_ACTION_TO_CANONICAL,
80411
+ key: ideAction
80412
+ }) ?? ideAction;
80304
80413
  }
80305
80414
  /**
80306
80415
  * Permissions generator for the Google Antigravity IDE (Antigravity 2.0).
@@ -81839,10 +81948,16 @@ const CANONICAL_TO_CLAUDE_TOOL_NAMES = {
81839
81948
  */
81840
81949
  const CLAUDE_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_CLAUDE_TOOL_NAMES);
81841
81950
  function toClaudeToolName(canonical) {
81842
- return CANONICAL_TO_CLAUDE_TOOL_NAMES[canonical] ?? canonical;
81951
+ return lookupOwn({
81952
+ record: CANONICAL_TO_CLAUDE_TOOL_NAMES,
81953
+ key: canonical
81954
+ }) ?? canonical;
81843
81955
  }
81844
81956
  function toCanonicalToolName$7(claudeName) {
81845
- return CLAUDE_TO_CANONICAL_TOOL_NAMES[claudeName] ?? claudeName;
81957
+ return lookupOwn({
81958
+ record: CLAUDE_TO_CANONICAL_TOOL_NAMES,
81959
+ key: claudeName
81960
+ }) ?? claudeName;
81846
81961
  }
81847
81962
  /**
81848
81963
  * Merge `patch` into `base`, recursing into plain objects so a sibling key at
@@ -82814,7 +82929,10 @@ const CLAUDE_PATH_RULE_ALIASES = {
82814
82929
  */
82815
82930
  function buildClaudePermissionEntry(toolName, pattern) {
82816
82931
  if (pattern === "*") return toolName;
82817
- return `${CLAUDE_PATH_RULE_ALIASES[toolName] ?? toolName}(${pattern})`;
82932
+ return `${lookupOwn({
82933
+ record: CLAUDE_PATH_RULE_ALIASES,
82934
+ key: toolName
82935
+ }) ?? toolName}(${pattern})`;
82818
82936
  }
82819
82937
  /**
82820
82938
  * The Claude tool names the canonical config manages. Deliberately the tool
@@ -83052,8 +83170,12 @@ function convertClaudeToRulesyncPermissions(params) {
83052
83170
  for (const entry of entries) {
83053
83171
  const { toolName, pattern } = parseToolPatternEntry(entry);
83054
83172
  const canonical = toCanonicalToolName$7(toolName);
83055
- if (!permission[canonical]) permission[canonical] = {};
83056
- permission[canonical][pattern] = action;
83173
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
83174
+ getOrCreateOwn({
83175
+ record: permission,
83176
+ key: canonical,
83177
+ create: () => ({})
83178
+ })[pattern] = action;
83057
83179
  }
83058
83180
  };
83059
83181
  processEntries(params.allow, "allow");
@@ -83635,11 +83757,17 @@ var CodebuddyPermissions = class CodebuddyPermissions extends ToolPermissions {
83635
83757
  for (const action of CODEBUDDY_RULE_LISTS) for (const rule of toStringArray$1(permissions[action])) {
83636
83758
  const parsed = parseCodebuddyRule(rule);
83637
83759
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
83638
- const rules = permission[parsed.category] ??= {};
83639
- const existing = rules[parsed.pattern];
83760
+ const rules = getOrCreateOwn({
83761
+ record: permission,
83762
+ key: parsed.category,
83763
+ create: () => ({})
83764
+ });
83640
83765
  if (isStricterAction({
83641
83766
  action,
83642
- existing
83767
+ existing: lookupOwn({
83768
+ record: rules,
83769
+ key: parsed.pattern
83770
+ })
83643
83771
  })) rules[parsed.pattern] = action;
83644
83772
  }
83645
83773
  const codebuddy = liftCodebuddyOverride({
@@ -84149,7 +84277,10 @@ const CODEWHALE_WRITE_RESTRICTION_TOOLS = ["write_file", "apply_patch"];
84149
84277
  /** The Codewhale tools a canonical path rule is written for. */
84150
84278
  function pathCategoryTools(category, action) {
84151
84279
  if (category === "write" && action !== "allow") return CODEWHALE_WRITE_RESTRICTION_TOOLS;
84152
- return CODEWHALE_PATH_CATEGORY_TOOLS[category] ?? [];
84280
+ return lookupOwn({
84281
+ record: CODEWHALE_PATH_CATEGORY_TOOLS,
84282
+ key: category
84283
+ }) ?? [];
84153
84284
  }
84154
84285
  /** The canonical path categories a Codewhale file tool's rule can come from. */
84155
84286
  const CODEWHALE_TOOL_TO_PATH_CATEGORIES = {
@@ -84468,7 +84599,10 @@ function canonicalToCodewhaleRules({ config, preservedRules, logger }) {
84468
84599
  });
84469
84600
  for (const [category, rules] of Object.entries(config.permission)) {
84470
84601
  if (category === "bash" || category === "*") continue;
84471
- if (CODEWHALE_PATH_CATEGORY_TOOLS[category] === void 0) {
84602
+ if (lookupOwn({
84603
+ record: CODEWHALE_PATH_CATEGORY_TOOLS,
84604
+ key: category
84605
+ }) === void 0) {
84472
84606
  if (Object.keys(rules).length > 0) logger?.warn(`Codewhale permissions: skipping unsupported category "${category}"; Codewhale evaluates permission rules only for shell commands and file tools.`);
84473
84607
  continue;
84474
84608
  }
@@ -84578,7 +84712,10 @@ function toImportCandidates(rule) {
84578
84712
  }
84579
84713
  const pattern = pathRuleToPattern(rule);
84580
84714
  if (pattern === null) return [];
84581
- return (CODEWHALE_TOOL_TO_PATH_CATEGORIES[rule.tool] ?? []).map((category) => ({
84715
+ return (lookupOwn({
84716
+ record: CODEWHALE_TOOL_TO_PATH_CATEGORIES,
84717
+ key: rule.tool
84718
+ }) ?? []).map((category) => ({
84582
84719
  category,
84583
84720
  pattern,
84584
84721
  rule
@@ -86186,10 +86323,17 @@ function parseCommandcodeRuleLists(permissions) {
86186
86323
  rule,
86187
86324
  category
86188
86325
  })) fallbackLogger.warn(`Command Code matches '${rule}' in "${action}" against MCP tool names case-insensitively, but the '${category}' rule generated from it matches the server and tool names exactly as written; spell the rule the way the tool is registered if they differ.`);
86189
- const rules = permission[category] ??= {};
86326
+ const rules = getOrCreateOwn({
86327
+ record: permission,
86328
+ key: category,
86329
+ create: () => ({})
86330
+ });
86190
86331
  if (isStricterAction({
86191
86332
  action,
86192
- existing: rules[pattern]
86333
+ existing: lookupOwn({
86334
+ record: rules,
86335
+ key: pattern
86336
+ })
86193
86337
  })) rules[pattern] = action;
86194
86338
  }
86195
86339
  }
@@ -86550,9 +86694,12 @@ function convertContinuePermissionsToRulesync(lists) {
86550
86694
  const parsedEntry = parseContinuePermissionEntry(entry);
86551
86695
  if (parsedEntry === void 0) continue;
86552
86696
  if (isPrototypePollutionKey(parsedEntry.toolName) || isPrototypePollutionKey(parsedEntry.pattern)) continue;
86553
- const category = toCanonicalToolName$6(parsedEntry.toolName);
86554
- permission[category] ??= {};
86555
- permission[category][parsedEntry.pattern] ??= action;
86697
+ const rules = getOrCreateOwn({
86698
+ record: permission,
86699
+ key: toCanonicalToolName$6(parsedEntry.toolName),
86700
+ create: () => ({})
86701
+ });
86702
+ if (!Object.hasOwn(rules, parsedEntry.pattern)) rules[parsedEntry.pattern] = action;
86556
86703
  }
86557
86704
  }
86558
86705
  return { permission };
@@ -87181,7 +87328,9 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
87181
87328
  const settings = this.parseSettings();
87182
87329
  const rules = {};
87183
87330
  for (const pattern of toUrlList(settings[DENIED_URLS_KEY])) rules[pattern] = "deny";
87184
- if (this.global) for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) rules[pattern] ??= "allow";
87331
+ if (this.global) {
87332
+ for (const pattern of toUrlList(settings[ALLOWED_URLS_KEY])) if (!Object.hasOwn(rules, pattern)) rules[pattern] = "allow";
87333
+ }
87185
87334
  const permission = Object.keys(rules).length > 0 ? { [WEBFETCH_CATEGORY$1]: rules } : {};
87186
87335
  const defaultPermissionMode = this.global ? settings[DEFAULT_PERMISSION_MODE_KEY] : void 0;
87187
87336
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -87528,7 +87677,10 @@ function isMcpScopedCategory(canonical) {
87528
87677
  }
87529
87678
  function toCursorType(canonical) {
87530
87679
  if (isMcpScopedCategory(canonical)) return "Mcp";
87531
- return CANONICAL_TO_CURSOR_TYPE[canonical] ?? canonical;
87680
+ return lookupOwn({
87681
+ record: CANONICAL_TO_CURSOR_TYPE,
87682
+ key: canonical
87683
+ }) ?? canonical;
87532
87684
  }
87533
87685
  /**
87534
87686
  * For per-tool MCP canonical categories like `mcp__puppeteer__navigate`, the
@@ -87560,9 +87712,15 @@ function toCanonicalCategory$1(cursorType, pattern) {
87560
87712
  const tool = match[2] ?? "*";
87561
87713
  return `${MCP_CATEGORY_PREFIX}${server}__${tool}`;
87562
87714
  }
87563
- return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
87715
+ return lookupOwn({
87716
+ record: CURSOR_TYPE_TO_CANONICAL,
87717
+ key: cursorType
87718
+ }) ?? cursorType.toLowerCase();
87564
87719
  }
87565
- return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
87720
+ return lookupOwn({
87721
+ record: CURSOR_TYPE_TO_CANONICAL,
87722
+ key: cursorType
87723
+ }) ?? cursorType.toLowerCase();
87566
87724
  }
87567
87725
  /**
87568
87726
  * Narrow `JSON.parse` output to a plain object before treating it as a Cursor
@@ -87807,9 +87965,13 @@ function convertCursorToRulesyncPermissions(params) {
87807
87965
  for (const entry of entries) {
87808
87966
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
87809
87967
  const canonical = toCanonicalCategory$1(type, pattern);
87810
- if (!permission[canonical]) permission[canonical] = {};
87811
87968
  const canonicalPattern = type === "Mcp" && canonical.startsWith("mcp__") ? "*" : pattern;
87812
- permission[canonical][canonicalPattern] = action;
87969
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(canonicalPattern)) continue;
87970
+ getOrCreateOwn({
87971
+ record: permission,
87972
+ key: canonical,
87973
+ create: () => ({})
87974
+ })[canonicalPattern] = action;
87813
87975
  }
87814
87976
  };
87815
87977
  processEntries(params.allow, "allow");
@@ -88736,10 +88898,16 @@ const DEVIN_SCOPE_TO_CANONICAL = {
88736
88898
  */
88737
88899
  const DEVIN_BARE_ONLY_SCOPES = /* @__PURE__ */ new Set(["web_search"]);
88738
88900
  function toDevinScope(canonical) {
88739
- return CANONICAL_TO_DEVIN_SCOPE[canonical] ?? canonical;
88901
+ return lookupOwn({
88902
+ record: CANONICAL_TO_DEVIN_SCOPE,
88903
+ key: canonical
88904
+ }) ?? canonical;
88740
88905
  }
88741
88906
  function toCanonicalCategory(devinScope) {
88742
- return DEVIN_SCOPE_TO_CANONICAL[devinScope] ?? devinScope;
88907
+ return lookupOwn({
88908
+ record: DEVIN_SCOPE_TO_CANONICAL,
88909
+ key: devinScope
88910
+ }) ?? devinScope;
88743
88911
  }
88744
88912
  function asDevinRecord(value) {
88745
88913
  return isRecord(value) ? { ...value } : {};
@@ -89091,7 +89259,11 @@ function convertDevinToRulesyncPermissions(params) {
89091
89259
  const { toolName: scope, pattern } = parseToolPatternEntry(entry);
89092
89260
  if (isPrototypePollutionKey(scope) || isPrototypePollutionKey(pattern)) continue;
89093
89261
  const canonical = toCanonicalCategory(scope);
89094
- (permission[canonical] ??= {})[pattern] = action;
89262
+ getOrCreateOwn({
89263
+ record: permission,
89264
+ key: canonical,
89265
+ create: () => ({})
89266
+ })[pattern] = action;
89095
89267
  }
89096
89268
  };
89097
89269
  processEntries(params.allow, "allow");
@@ -89397,7 +89569,10 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
89397
89569
  };
89398
89570
  const assigned = /* @__PURE__ */ new Map();
89399
89571
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
89400
- const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
89572
+ const toolName = lookupOwn({
89573
+ record: RULESYNC_TO_GOOSE_TOOL_NAME,
89574
+ key: category
89575
+ }) ?? category;
89401
89576
  for (const [pattern, action] of Object.entries(rules)) if (pattern !== "*") logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
89402
89577
  const { action, downgraded } = failSafeCatchAllAction({
89403
89578
  rules,
@@ -89429,9 +89604,16 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
89429
89604
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
89430
89605
  const toolNames = isStringArray(userPermission[key]) ? userPermission[key] : [];
89431
89606
  const action = GOOSE_LIST_TO_ACTION[key];
89432
- for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
89433
- permission[category] ??= {};
89434
- permission[category]["*"] = action;
89607
+ for (const toolName of toolNames) for (const category of lookupOwn({
89608
+ record: GOOSE_TO_RULESYNC_TOOL_NAMES,
89609
+ key: toolName
89610
+ }) ?? [toolName]) {
89611
+ if (isPrototypePollutionKey(category)) continue;
89612
+ getOrCreateOwn({
89613
+ record: permission,
89614
+ key: category,
89615
+ create: () => ({})
89616
+ })["*"] = action;
89435
89617
  }
89436
89618
  }
89437
89619
  return { permission };
@@ -89512,7 +89694,10 @@ function buildGrokEntry(category, pattern) {
89512
89694
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
89513
89695
  }
89514
89696
  if (category === "mcp") return pattern === "*" || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
89515
- const tool = CATEGORY_TO_GROK_TOOL[category];
89697
+ const tool = lookupOwn({
89698
+ record: CATEGORY_TO_GROK_TOOL,
89699
+ key: category
89700
+ });
89516
89701
  if (tool === void 0) return null;
89517
89702
  return pattern === "*" || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
89518
89703
  }
@@ -89546,7 +89731,10 @@ function parseGrokEntry(entry) {
89546
89731
  category: "mcp",
89547
89732
  pattern: "*"
89548
89733
  };
89549
- const category = GROK_TOOL_TO_CATEGORY[tool];
89734
+ const category = lookupOwn({
89735
+ record: GROK_TOOL_TO_CATEGORY,
89736
+ key: tool
89737
+ });
89550
89738
  if (category === void 0) return null;
89551
89739
  return {
89552
89740
  category,
@@ -89867,11 +90055,17 @@ function parseGrokPermissionArrays(permission) {
89867
90055
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
89868
90056
  const result = {};
89869
90057
  const record = ({ category, pattern, action }) => {
89870
- const bucket = result[category] ??= {};
89871
- const existing = bucket[pattern];
90058
+ const bucket = getOrCreateOwn({
90059
+ record: result,
90060
+ key: category,
90061
+ create: () => ({})
90062
+ });
89872
90063
  if (isStricterAction({
89873
90064
  action,
89874
- existing
90065
+ existing: lookupOwn({
90066
+ record: bucket,
90067
+ key: pattern
90068
+ })
89875
90069
  })) bucket[pattern] = action;
89876
90070
  };
89877
90071
  const applyEntries = (entries, action) => {
@@ -89928,7 +90122,11 @@ function deleteRulesByAction(rules, action) {
89928
90122
  for (const [pattern, currentAction] of Object.entries(rules)) if (currentAction === action) delete rules[pattern];
89929
90123
  }
89930
90124
  function ensureCategory(permission, category) {
89931
- return permission[category] ??= {};
90125
+ return getOrCreateOwn({
90126
+ record: permission,
90127
+ key: category,
90128
+ create: () => ({})
90129
+ });
89932
90130
  }
89933
90131
  function removeEmptyCategories(permission) {
89934
90132
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
@@ -90109,6 +90307,22 @@ function unmanagedMcpAllowlistEntries({ existing, previousPermission, nextPermis
90109
90307
  return category === null || !Object.hasOwn(previousPermission, category) && !Object.hasOwn(nextPermission, category);
90110
90308
  });
90111
90309
  }
90310
+ /** Hermes's override key in the canonical document (`hermes`). */
90311
+ const HERMES_OVERRIDE_KEY = PERMISSION_OVERRIDE_KEY_ALIASES.hermesagent ?? "hermesagent";
90312
+ /**
90313
+ * The part of a canonical permissions document Hermes keeps as provenance: the
90314
+ * `$schema`, the canonical `permission` block, and Hermes's own override.
90315
+ */
90316
+ function hermesProvenanceOf(permissions) {
90317
+ const source = permissions;
90318
+ const provenance = {};
90319
+ for (const key of [
90320
+ "$schema",
90321
+ "permission",
90322
+ HERMES_OVERRIDE_KEY
90323
+ ]) if (source[key] !== void 0) provenance[key] = source[key];
90324
+ return provenance;
90325
+ }
90112
90326
  /** The validated canonical `permission` block of a config's `permissions.rulesync` blob. */
90113
90327
  function canonicalPermissionOf(config) {
90114
90328
  const parsed = RulesyncPermissionsFileSchema.safeParse({ permission: provenancePermissionOf(config) });
@@ -90276,10 +90490,10 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
90276
90490
  patterns: websiteBlocklist.enabled === true && isStringArray(websiteBlocklist.domains) ? websiteBlocklist.domains : []
90277
90491
  });
90278
90492
  removeEmptyCategories(permission);
90279
- const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
90280
90493
  const hermes = buildHermesOverride(config, provenance);
90494
+ const { $schema } = provenance;
90281
90495
  const imported = {
90282
- ...otherProvenance,
90496
+ ...$schema !== void 0 && { $schema },
90283
90497
  permission,
90284
90498
  ...Object.keys(hermes).length > 0 && { hermes }
90285
90499
  };
@@ -90337,7 +90551,7 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
90337
90551
  base: config,
90338
90552
  patch: permissions.hermes
90339
90553
  });
90340
- config.permissions = { rulesync: permissions };
90554
+ config.permissions = { rulesync: hermesProvenanceOf(permissions) };
90341
90555
  return new HermesagentPermissions({
90342
90556
  outputRoot,
90343
90557
  fileContent: stringifySharedConfig({
@@ -90571,7 +90785,10 @@ var JuniePermissions = class JuniePermissions extends ToolPermissions {
90571
90785
  function convertRulesyncToJunieRules({ config, logger, existingRules, overrideSecretFile, overrideRuleDefaults }) {
90572
90786
  const ruleLists = {};
90573
90787
  for (const [category, patterns] of Object.entries(honorAllToolsOnBash(config.permission))) {
90574
- const group = CANONICAL_TO_JUNIE_GROUP[category];
90788
+ const group = lookupOwn({
90789
+ record: CANONICAL_TO_JUNIE_GROUP,
90790
+ key: category
90791
+ });
90575
90792
  if (!group) {
90576
90793
  if (Object.keys(patterns).length > 0) logger?.warn(`Junie allowlist only models executables/fileEditing/mcpTools/readOutsideProject (canonical bash/edit/write/read/mcp); '${category}' rules cannot be represented and were skipped.`);
90577
90794
  continue;
@@ -90969,7 +91186,10 @@ const CATEGORY_TO_KIMI_CODE_TOOL = {
90969
91186
  function buildKimiCodePattern(category, pattern) {
90970
91187
  if (category.startsWith("mcp__")) return pattern === "*" || pattern === "" ? category : null;
90971
91188
  if (category === "mcp") return pattern === "*" || pattern === "" ? "mcp__*" : `mcp__${pattern}`;
90972
- const tool = CATEGORY_TO_KIMI_CODE_TOOL[category];
91189
+ const tool = lookupOwn({
91190
+ record: CATEGORY_TO_KIMI_CODE_TOOL,
91191
+ key: category
91192
+ });
90973
91193
  if (!tool) return null;
90974
91194
  return pattern === "*" || pattern === "" ? tool : `${tool}(${pattern})`;
90975
91195
  }
@@ -91065,7 +91285,10 @@ function canonicalToKimiCodeRules({ config, logger }) {
91065
91285
  const canonicalRules = [];
91066
91286
  const overrideRules = config["kimi-code"]?.rules ?? [];
91067
91287
  for (const [category, rules] of Object.entries(config.permission)) {
91068
- if (!(category === "mcp" || category.startsWith("mcp__") || CATEGORY_TO_KIMI_CODE_TOOL[category] !== void 0)) {
91288
+ if (!(category === "mcp" || category.startsWith("mcp__") || lookupOwn({
91289
+ record: CATEGORY_TO_KIMI_CODE_TOOL,
91290
+ key: category
91291
+ }) !== void 0)) {
91069
91292
  if (Object.keys(rules).length > 0) logger?.warn(`Kimi Code permissions: skipping unsupported category "${category}".`);
91070
91293
  continue;
91071
91294
  }
@@ -91882,9 +92105,16 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
91882
92105
  if (!isImportableRule(rule) || !isRecord(rule) || !isPermissionAction(rule.effect)) continue;
91883
92106
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
91884
92107
  const patterns = rule.match === void 0 ? ["*"] : isStringArray(rule.match) ? rule.match : [];
91885
- const categoryRules = permission[category] ??= {};
92108
+ const categoryRules = getOrCreateOwn({
92109
+ record: permission,
92110
+ key: category,
92111
+ create: () => ({})
92112
+ });
91886
92113
  for (const pattern of patterns) {
91887
- const previous = categoryRules[pattern];
92114
+ const previous = lookupOwn({
92115
+ record: categoryRules,
92116
+ key: pattern
92117
+ });
91888
92118
  if (isStricterAction({
91889
92119
  action: rule.effect,
91890
92120
  existing: previous
@@ -91902,7 +92132,10 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
91902
92132
  function buildKiroCapabilityRules({ config, logger }) {
91903
92133
  const buckets = /* @__PURE__ */ new Map();
91904
92134
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
91905
- const capability = CANONICAL_TO_KIRO_CAPABILITY[category];
92135
+ const capability = lookupOwn({
92136
+ record: CANONICAL_TO_KIRO_CAPABILITY,
92137
+ key: category
92138
+ });
91906
92139
  if (capability === void 0) {
91907
92140
  logger?.warn(`Kiro permissions.yaml has no capability for category: ${category}. Skipping.`);
91908
92141
  continue;
@@ -92062,7 +92295,10 @@ function toLettacodeBashPayload({ pattern, action, logger }) {
92062
92295
  return prefix === "" ? "" : `${prefix}${PREFIX_SUFFIX}`;
92063
92296
  }
92064
92297
  function toLettacodeRule({ category, pattern, action, logger }) {
92065
- const tool = CATEGORY_TO_LETTACODE_TOOL[category];
92298
+ const tool = lookupOwn({
92299
+ record: CATEGORY_TO_LETTACODE_TOOL,
92300
+ key: category
92301
+ });
92066
92302
  if (tool === void 0) return null;
92067
92303
  const payload = category === "bash" ? toLettacodeBashPayload({
92068
92304
  pattern,
@@ -92217,11 +92453,17 @@ var LettacodePermissions = class LettacodePermissions extends ToolPermissions {
92217
92453
  for (const [key, action] of Object.entries(LIST_TO_ACTION)) for (const rule of toStringArray$1(permissions[key])) {
92218
92454
  const parsed = fromLettacodeRule(rule);
92219
92455
  if (parsed === null || isPrototypePollutionKey(parsed.pattern)) continue;
92220
- const rules = permission[parsed.category] ??= {};
92221
- const existing = rules[parsed.pattern];
92456
+ const rules = getOrCreateOwn({
92457
+ record: permission,
92458
+ key: parsed.category,
92459
+ create: () => ({})
92460
+ });
92222
92461
  if (isStricterAction({
92223
92462
  action,
92224
- existing
92463
+ existing: lookupOwn({
92464
+ record: rules,
92465
+ key: parsed.pattern
92466
+ })
92225
92467
  })) rules[parsed.pattern] = action;
92226
92468
  }
92227
92469
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
@@ -92408,7 +92650,13 @@ function getOpencodeMcpServerNames(mcp) {
92408
92650
  return isRecord(lowered) ? Object.keys(lowered) : [];
92409
92651
  }
92410
92652
  function toOpencodePermissionKey(canonical) {
92411
- return CANONICAL_TO_OPENCODE_PERMISSION_KEYS[canonical] ?? CANONICAL_TO_OPENCODE_FOLDED_PERMISSION_KEYS[canonical] ?? canonical;
92653
+ return lookupOwn({
92654
+ record: CANONICAL_TO_OPENCODE_PERMISSION_KEYS,
92655
+ key: canonical
92656
+ }) ?? lookupOwn({
92657
+ record: CANONICAL_TO_OPENCODE_FOLDED_PERMISSION_KEYS,
92658
+ key: canonical
92659
+ }) ?? canonical;
92412
92660
  }
92413
92661
  /**
92414
92662
  * Merge two canonical pattern maps that land on the same OpenCode key.
@@ -92432,7 +92680,10 @@ function mergePatternRules(base, incoming) {
92432
92680
  return Object.fromEntries(Object.entries(merged).toSorted(([, a], [, b]) => PERMISSION_ACTION_PRIORITY[a] - PERMISSION_ACTION_PRIORITY[b]));
92433
92681
  }
92434
92682
  function toCanonicalPermissionKey(opencodeKey) {
92435
- return OPENCODE_TO_CANONICAL_PERMISSION_KEYS[opencodeKey] ?? opencodeKey;
92683
+ return lookupOwn({
92684
+ record: OPENCODE_TO_CANONICAL_PERMISSION_KEYS,
92685
+ key: opencodeKey
92686
+ }) ?? opencodeKey;
92436
92687
  }
92437
92688
  function isSharedPermissionCategory(category) {
92438
92689
  return category === "*" || CANONICAL_PERMISSION_CATEGORIES$1.has(category) || category.startsWith("mcp__");
@@ -93714,7 +93965,14 @@ function importPoolTool({ toolConfig, bucket }) {
93714
93965
  const denied = isStringArray(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
93715
93966
  const allowed = isStringArray(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
93716
93967
  for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
93717
- for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
93968
+ for (const pattern of allowed.filter(isUsablePattern)) if (!Object.hasOwn(bucket, pattern)) bucket[pattern] = "allow";
93969
+ }
93970
+ /**
93971
+ * Record `allow` for a path no deny has claimed. The check is on own
93972
+ * properties, so a path such as `toString` is not mistaken for a stored rule.
93973
+ */
93974
+ function allowUnlessSet(bucket, path) {
93975
+ if (!Object.hasOwn(bucket, path)) bucket[path] = "allow";
93718
93976
  }
93719
93977
  /**
93720
93978
  * Convert Pool's `tools` and `paths` blocks back into a rulesync config.
@@ -93749,8 +94007,8 @@ function convertPoolToRulesync({ tools, paths }) {
93749
94007
  for (const entry of allowEntries) {
93750
94008
  const path = pathOfEntry(entry);
93751
94009
  if (path === void 0) continue;
93752
- bucketFor(READ_CATEGORY)[path] ??= "allow";
93753
- if (isRecord(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) bucketFor(category)[path] ??= "allow";
94010
+ allowUnlessSet(bucketFor(READ_CATEGORY), path);
94011
+ if (isRecord(entry) && entry[POOL_WRITE_KEY] === true) for (const category of WRITE_CATEGORIES) allowUnlessSet(bucketFor(category), path);
93754
94012
  }
93755
94013
  return { permission };
93756
94014
  }
@@ -93879,7 +94137,10 @@ function toQoderRule({ category, pattern, logger }) {
93879
94137
  logger?.warn(`Qoder permissions: skipping the '${category}' rule '${pattern}' — Qoder matches MCP tools by name only.`);
93880
94138
  return null;
93881
94139
  }
93882
- const tool = CATEGORY_TO_QODER_TOOL[category];
94140
+ const tool = lookupOwn({
94141
+ record: CATEGORY_TO_QODER_TOOL,
94142
+ key: category
94143
+ });
93883
94144
  if (tool === void 0) return null;
93884
94145
  if (pattern === "*") return tool;
93885
94146
  return `${QODER_PATH_RULE_ALIASES[tool] ?? tool}(${pattern})`;
@@ -94165,11 +94426,17 @@ var QoderPermissions = class QoderPermissions extends ToolPermissions {
94165
94426
  for (const action of ACTIONS) for (const rule of toStringArray$1(permissions[action])) {
94166
94427
  const parsed = fromQoderRule(rule);
94167
94428
  if (parsed === null || isPrototypePollutionKey(parsed.category) || isPrototypePollutionKey(parsed.pattern)) continue;
94168
- const rules = permission[parsed.category] ??= {};
94169
- const existing = rules[parsed.pattern];
94429
+ const rules = getOrCreateOwn({
94430
+ record: permission,
94431
+ key: parsed.category,
94432
+ create: () => ({})
94433
+ });
94170
94434
  if (isStricterAction({
94171
94435
  action,
94172
- existing
94436
+ existing: lookupOwn({
94437
+ record: rules,
94438
+ key: parsed.pattern
94439
+ })
94173
94440
  })) rules[parsed.pattern] = action;
94174
94441
  }
94175
94442
  const outputStyle = settings[QODER_OUTPUT_STYLE_KEY];
@@ -94233,10 +94500,16 @@ const CANONICAL_TO_QWEN_TOOL_NAMES = {
94233
94500
  };
94234
94501
  const QWEN_TO_CANONICAL_TOOL_NAMES = invertRecord(CANONICAL_TO_QWEN_TOOL_NAMES);
94235
94502
  function toQwenToolName(canonical) {
94236
- return CANONICAL_TO_QWEN_TOOL_NAMES[canonical] ?? canonical;
94503
+ return lookupOwn({
94504
+ record: CANONICAL_TO_QWEN_TOOL_NAMES,
94505
+ key: canonical
94506
+ }) ?? canonical;
94237
94507
  }
94238
94508
  function toCanonicalToolName$3(qwenName) {
94239
- return QWEN_TO_CANONICAL_TOOL_NAMES[qwenName] ?? qwenName;
94509
+ return lookupOwn({
94510
+ record: QWEN_TO_CANONICAL_TOOL_NAMES,
94511
+ key: qwenName
94512
+ }) ?? qwenName;
94240
94513
  }
94241
94514
  function parseQwenPermissionEntry(entry, options = {}) {
94242
94515
  const parenIndex = entry.indexOf("(");
@@ -94852,15 +95125,23 @@ function convertQwenToRulesyncPermissions(params) {
94852
95125
  if (!parsed.ok) {
94853
95126
  if (action === "deny") {
94854
95127
  const canonical = toCanonicalToolName$3(parsed.toolName);
94855
- if (!permission[canonical]) permission[canonical] = {};
94856
- permission[canonical]["*"] = action;
95128
+ if (isPrototypePollutionKey(canonical)) continue;
95129
+ getOrCreateOwn({
95130
+ record: permission,
95131
+ key: canonical,
95132
+ create: () => ({})
95133
+ })["*"] = action;
94857
95134
  }
94858
95135
  continue;
94859
95136
  }
94860
95137
  const { toolName, pattern } = parsed;
94861
95138
  const canonical = toCanonicalToolName$3(toolName);
94862
- if (!permission[canonical]) permission[canonical] = {};
94863
- permission[canonical][pattern] = action;
95139
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
95140
+ getOrCreateOwn({
95141
+ record: permission,
95142
+ key: canonical,
95143
+ create: () => ({})
95144
+ })[pattern] = action;
94864
95145
  }
94865
95146
  };
94866
95147
  processEntries(params.allow, "allow");
@@ -94924,15 +95205,24 @@ const REASONIX_TO_CANONICAL_TOOL_NAMES = {
94924
95205
  };
94925
95206
  /** Every Reasonix tool name a canonical category owns on generate. */
94926
95207
  function reasonixToolNamesOf(canonical) {
94927
- const legacy = CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES[canonical];
95208
+ const legacy = lookupOwn({
95209
+ record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
95210
+ key: canonical
95211
+ });
94928
95212
  const current = toReasonixToolName(canonical);
94929
95213
  return legacy === void 0 ? [current] : [current, legacy];
94930
95214
  }
94931
95215
  function toReasonixToolName(canonical) {
94932
- return CANONICAL_TO_REASONIX_TOOL_NAMES[canonical] ?? canonical;
95216
+ return lookupOwn({
95217
+ record: CANONICAL_TO_REASONIX_TOOL_NAMES,
95218
+ key: canonical
95219
+ }) ?? canonical;
94933
95220
  }
94934
95221
  function toCanonicalToolName$2(reasonixName) {
94935
- return REASONIX_TO_CANONICAL_TOOL_NAMES[reasonixName] ?? reasonixName;
95222
+ return lookupOwn({
95223
+ record: REASONIX_TO_CANONICAL_TOOL_NAMES,
95224
+ key: reasonixName
95225
+ }) ?? reasonixName;
94936
95226
  }
94937
95227
  /**
94938
95228
  * Whether an entry uses the first-class `Tool=<literal>` exact-command form
@@ -95555,8 +95845,12 @@ function convertReasonixToRulesyncPermissions(params) {
95555
95845
  for (const entry of entries) {
95556
95846
  const { toolName, pattern } = parseToolPatternEntry(entry);
95557
95847
  const canonical = toCanonicalToolName$2(toolName);
95558
- if (!permission[canonical]) permission[canonical] = {};
95559
- permission[canonical][pattern] = action;
95848
+ if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
95849
+ getOrCreateOwn({
95850
+ record: permission,
95851
+ key: canonical,
95852
+ create: () => ({})
95853
+ })[pattern] = action;
95560
95854
  }
95561
95855
  };
95562
95856
  processEntries(params.allow, "allow");
@@ -96108,7 +96402,10 @@ function convertRulesyncToRovodevToolPermissions({ config, logger }) {
96108
96402
  if (bash) toolPermissions.bash = bash;
96109
96403
  continue;
96110
96404
  }
96111
- const toolKeys = CATEGORY_TO_TOOL_KEYS[category];
96405
+ const toolKeys = lookupOwn({
96406
+ record: CATEGORY_TO_TOOL_KEYS,
96407
+ key: category
96408
+ });
96112
96409
  if (!toolKeys) {
96113
96410
  logger?.warn(`Rovo Dev permissions have no target for the "${category}" category. Skipping it.`);
96114
96411
  markSharedToolKeyRestriction({
@@ -96256,7 +96553,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
96256
96553
  }
96257
96554
  if (isStringArray(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
96258
96555
  permission.read ??= {};
96259
- permission.read[path] ??= "allow";
96556
+ if (!Object.hasOwn(permission.read, path)) permission.read[path] = "allow";
96260
96557
  }
96261
96558
  return { permission };
96262
96559
  }
@@ -97993,11 +98290,17 @@ var TraePermissions = class TraePermissions extends ToolPermissions {
97993
98290
  const permission = {};
97994
98291
  const put = (category, pattern, action) => {
97995
98292
  if (isPrototypePollutionKey(category) || isPrototypePollutionKey(pattern)) return;
97996
- const rules = permission[category] ??= {};
97997
- const existing = rules[pattern];
98293
+ const rules = getOrCreateOwn({
98294
+ record: permission,
98295
+ key: category,
98296
+ create: () => ({})
98297
+ });
97998
98298
  if (isStricterAction({
97999
98299
  action,
98000
- existing
98300
+ existing: lookupOwn({
98301
+ record: rules,
98302
+ key: pattern
98303
+ })
98001
98304
  })) rules[pattern] = action;
98002
98305
  };
98003
98306
  for (const [key, value] of Object.entries(readRecord(customProfiles, COMMAND_RULES_PATH))) {
@@ -100340,7 +100643,10 @@ function toZedToolName(canonical) {
100340
100643
  const address = toolParts.length > 0 ? `${server}:${toolParts.join("__")}` : server ?? "";
100341
100644
  return `${MCP_ZED_PREFIX}${address}`;
100342
100645
  }
100343
- return CANONICAL_TO_ZED_TOOL_NAMES[canonical] ?? canonical;
100646
+ return lookupOwn({
100647
+ record: CANONICAL_TO_ZED_TOOL_NAMES,
100648
+ key: canonical
100649
+ }) ?? canonical;
100344
100650
  }
100345
100651
  function toCanonicalToolName(zedName) {
100346
100652
  if (zedName.startsWith(MCP_ZED_PREFIX)) {
@@ -100348,7 +100654,10 @@ function toCanonicalToolName(zedName) {
100348
100654
  const address = toolParts.length > 0 ? `${server}__${toolParts.join(":")}` : server ?? "";
100349
100655
  return `${MCP_CATEGORY_PREFIX}${address}`;
100350
100656
  }
100351
- return ZED_TO_CANONICAL_TOOL_NAMES[zedName] ?? zedName;
100657
+ return lookupOwn({
100658
+ record: ZED_TO_CANONICAL_TOOL_NAMES,
100659
+ key: zedName
100660
+ }) ?? zedName;
100352
100661
  }
100353
100662
  /**
100354
100663
  * Zed matches `always_allow`/`always_deny`/`always_confirm` regexes against the
@@ -100660,7 +100969,10 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
100660
100969
  const globalDefault = parsed.success ? parsed.data.default : void 0;
100661
100970
  const permission = {};
100662
100971
  const ensure = (category) => {
100663
- const existing = permission[category];
100972
+ const existing = lookupOwn({
100973
+ record: permission,
100974
+ key: category
100975
+ });
100664
100976
  if (existing) return existing;
100665
100977
  const created = {};
100666
100978
  permission[category] = created;
@@ -101287,7 +101599,7 @@ function resolveToolOutputRoot({ outputRoot, toolTarget, global }) {
101287
101599
  async function convertFromTool(params) {
101288
101600
  resetRunWarningState();
101289
101601
  const packagingTarget = [params.fromTool, ...params.toTools].find(isPackagingToolTarget);
101290
- if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import and generate with explicit plugin directories.`);
101602
+ if (packagingTarget) throw new Error(`Plugin packaging target '${packagingTarget}' is not supported by convert. Use import with the plugin directory as outputRoot (--output-root) and generate with it in outputRoots (--output-roots).`);
101291
101603
  const ctx = params;
101292
101604
  const [rulesCount, ignoreCount, mcpCount, commandsCount, subagentsCount, skillsCount, hooksCount, permissionsCount, checksCount] = [
101293
101605
  await runFeatureConvert(ctx, buildRulesStrategy(ctx)),
@@ -104535,12 +104847,6 @@ Object.defineProperty(exports, "isFileSystemError", {
104535
104847
  return isFileSystemError;
104536
104848
  }
104537
104849
  });
104538
- Object.defineProperty(exports, "isPackagingToolTarget", {
104539
- enumerable: true,
104540
- get: function() {
104541
- return isPackagingToolTarget;
104542
- }
104543
- });
104544
104850
  Object.defineProperty(exports, "isRecord", {
104545
104851
  enumerable: true,
104546
104852
  get: function() {
@@ -104607,6 +104913,12 @@ Object.defineProperty(exports, "parseCommaSeparatedList", {
104607
104913
  return parseCommaSeparatedList;
104608
104914
  }
104609
104915
  });
104916
+ Object.defineProperty(exports, "parseConfigFileContent", {
104917
+ enumerable: true,
104918
+ get: function() {
104919
+ return parseConfigFileContent;
104920
+ }
104921
+ });
104610
104922
  Object.defineProperty(exports, "parseJsonc", {
104611
104923
  enumerable: true,
104612
104924
  get: function() {