rulesync 16.22.1 → 16.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6674,6 +6674,8 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
6674
6674
  *
6675
6675
  * @example
6676
6676
  * { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
6677
+ * @example
6678
+ * { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
6677
6679
  */
6678
6680
  const ReasonixPermissionsOverrideSchema = z.looseObject({
6679
6681
  permission: z.optional(ToolScopedPermissionSchema),
@@ -6681,7 +6683,8 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
6681
6683
  agent: z.optional(z.looseObject({})),
6682
6684
  rawAllow: z.optional(z.array(z.string())),
6683
6685
  rawAsk: z.optional(z.array(z.string())),
6684
- rawDeny: z.optional(z.array(z.string()))
6686
+ rawDeny: z.optional(z.array(z.string())),
6687
+ allowDynamicBash: z.optional(z.boolean())
6685
6688
  });
6686
6689
  /**
6687
6690
  * Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
@@ -7325,6 +7328,32 @@ const ZedPermissionsOverrideSchema = z.looseObject({
7325
7328
  })))
7326
7329
  });
7327
7330
  /**
7331
+ * Tool-scoped override block for Devin Local. `sandbox` is the sibling
7332
+ * top-level `config.json` block that governs the sandbox Devin runs commands
7333
+ * in: `allowed_domains` / `denied_domains` (proxy domain patterns, deny beating
7334
+ * allow), `network_mode` (`full`, the upstream default, allows every HTTP
7335
+ * method; `limited` only GET/HEAD/OPTIONS) and `excluded` (`allow` / `ask` /
7336
+ * `deny` lists of `Exec(...)` matchers deciding which commands run *outside* the
7337
+ * sandbox — `deny` pins them inside it). It constrains how
7338
+ * a permitted command runs rather than which commands are permitted, so it has
7339
+ * no canonical category and is authored here.
7340
+ *
7341
+ * Upstream lists `sandbox` as a **User Config Only** key, so it is emitted at
7342
+ * global scope only; at project scope it is dropped with a warning rather than
7343
+ * written into a file Devin would ignore.
7344
+ *
7345
+ * @example
7346
+ * { "sandbox": { "allowed_domains": ["github.com"], "network_mode": "limited" } }
7347
+ * @example
7348
+ * { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
7349
+ * @see https://docs.devin.ai/cli/sandbox
7350
+ * @see https://docs.devin.ai/cli/reference/configuration/config-file
7351
+ */
7352
+ const DevinPermissionsOverrideSchema = z.looseObject({
7353
+ permission: z.optional(ToolScopedPermissionSchema),
7354
+ sandbox: z.optional(z.looseObject({}))
7355
+ });
7356
+ /**
7328
7357
  * Permissions configuration.
7329
7358
  * Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
7330
7359
  * Values are pattern-to-action mappings for that tool category.
@@ -7374,10 +7403,10 @@ const PermissionsConfigSchema = z.looseObject({
7374
7403
  kiro: z.optional(KiroPermissionsOverrideSchema),
7375
7404
  codexcli: z.optional(CodexcliPermissionsOverrideSchema),
7376
7405
  zed: z.optional(ZedPermissionsOverrideSchema),
7406
+ devin: z.optional(DevinPermissionsOverrideSchema),
7377
7407
  "antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
7378
7408
  copilot: z.optional(CanonicalPermissionsOverrideSchema),
7379
7409
  copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
7380
- devin: z.optional(CanonicalPermissionsOverrideSchema),
7381
7410
  goose: z.optional(CanonicalPermissionsOverrideSchema),
7382
7411
  grokcli: z.optional(CanonicalPermissionsOverrideSchema),
7383
7412
  "kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
@@ -12603,7 +12632,7 @@ const SHARED_CONFIG_OWNERSHIP = {
12603
12632
  },
12604
12633
  permissions: {
12605
12634
  kind: "replace-owned-keys",
12606
- ownedKeys: ["permissions"]
12635
+ ownedKeys: ["permissions", "sandbox"]
12607
12636
  }
12608
12637
  }
12609
12638
  },
@@ -36324,6 +36353,180 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
36324
36353
  return { permission };
36325
36354
  }
36326
36355
  //#endregion
36356
+ //#region src/features/permissions/sandbox-trust.ts
36357
+ /** A key whose quiet value is an explicit `false`. */
36358
+ const isNotFalse = (value) => value !== false;
36359
+ /** A key whose quiet value is an explicit `true`. */
36360
+ const isNotTrue = (value) => value !== true;
36361
+ /** A list-valued key whose quiet value is the empty list. */
36362
+ const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
36363
+ /** The map-valued counterpart of {@link isNonEmptyList}. */
36364
+ const isNonEmptyMap = (value) => !isRecord$1(value) || Object.keys(value).length > 0;
36365
+ /**
36366
+ * What {@link readSandboxPath} returns when a container on the way to the leaf
36367
+ * is present but is not an object, so the leaf cannot be read at all. It is not
36368
+ * `undefined`, because the two mean opposite things to a caller: `undefined` is
36369
+ * "this path is not being written", while this is "something is being written
36370
+ * here and its shape hides what". The same fail-safe rule the predicates follow
36371
+ * applies to the walk — silence must mean "this cannot loosen anything", not
36372
+ * "this is not the shape the table expected".
36373
+ */
36374
+ const UNREADABLE_SANDBOX_PATH = Symbol("unreadable-sandbox-path");
36375
+ /**
36376
+ * Reads `sandbox` at `path`. Returns `undefined` when a segment is absent, and
36377
+ * {@link UNREADABLE_SANDBOX_PATH} when one is present but is not an object.
36378
+ * Shared by everything that addresses a `sandbox` path so a nested path added to
36379
+ * one of the tables is actually traversed rather than silently skipped, and so a
36380
+ * hostile shape (an array, a string, `null`) is reported rather than throwing.
36381
+ */
36382
+ function readSandboxPath({ sandbox, path }) {
36383
+ let cursor = sandbox;
36384
+ for (const segment of path) {
36385
+ if (cursor === void 0) return void 0;
36386
+ if (!isRecord$1(cursor)) return UNREADABLE_SANDBOX_PATH;
36387
+ cursor = cursor[segment];
36388
+ }
36389
+ return cursor;
36390
+ }
36391
+ /**
36392
+ * Every path in `paths` whose value in `sandbox` loosens the policy. Nothing is
36393
+ * removed — the values are written, just not silently. Call it on the block this
36394
+ * generate authored, after any scope filter has run: a value the file already
36395
+ * held is the user's own, not something rulesync opened, and a path a filter
36396
+ * dropped is not being written at all.
36397
+ */
36398
+ function collectTrustAffectingSandboxPaths({ sandbox, paths }) {
36399
+ const entries = [];
36400
+ const reportedContainers = /* @__PURE__ */ new Set();
36401
+ for (const { path, reason, widens } of paths) {
36402
+ const value = readSandboxPath({
36403
+ sandbox,
36404
+ path
36405
+ });
36406
+ if (value === void 0) continue;
36407
+ if (value === UNREADABLE_SANDBOX_PATH) {
36408
+ const label = findUnreadableContainer({
36409
+ sandbox,
36410
+ path
36411
+ });
36412
+ if (label === void 0 || reportedContainers.has(label)) continue;
36413
+ reportedContainers.add(label);
36414
+ entries.push({
36415
+ label,
36416
+ reason: UNREADABLE_CONTAINER_REASON
36417
+ });
36418
+ continue;
36419
+ }
36420
+ if (!widens(value)) continue;
36421
+ entries.push({
36422
+ label: `sandbox.${path.join(".")}`,
36423
+ reason
36424
+ });
36425
+ }
36426
+ return entries;
36427
+ }
36428
+ /** The reason printed for a container that hides the settings underneath it. */
36429
+ const UNREADABLE_CONTAINER_REASON = "is not the object it has to be, so nothing under it can be checked for what it opens";
36430
+ /**
36431
+ * The prefix of `path` that {@link readSandboxPath} could not walk past, as a
36432
+ * label. `undefined` when the walk was not blocked at all. Callers that report
36433
+ * an unreadable path name the container rather than the leaf, because the leaf
36434
+ * is not what the file actually holds.
36435
+ */
36436
+ function findUnreadableContainer({ sandbox, path }) {
36437
+ let cursor = sandbox;
36438
+ const walked = [];
36439
+ for (const segment of path) {
36440
+ if (cursor === void 0) return void 0;
36441
+ if (!isRecord$1(cursor)) return walked.length === 0 ? "sandbox" : `sandbox.${walked.join(".")}`;
36442
+ walked.push(segment);
36443
+ cursor = cursor[segment];
36444
+ }
36445
+ }
36446
+ /**
36447
+ * The reason printed for a value the file held in a shape that cannot be read,
36448
+ * which this generate is about to replace. `shape` names what the tool documents
36449
+ * there, so the message says which expectation the file's value missed.
36450
+ */
36451
+ const replacedUnreadableReason = ({ shape, toolLabel }) => `replaces a value already in the file that is not the ${shape} ${toolLabel} documents, so what it restricted cannot be read`;
36452
+ /**
36453
+ * The restrictions this generate would weaken, compared between the `sandbox`
36454
+ * already in the file and the one about to replace it. A `before` that is
36455
+ * present but not a list is reported outright: a shape the tool may still honor
36456
+ * is not something to go quiet about just because it cannot be diffed. Shared by
36457
+ * every tool whose override replaces a restricting list whole rather than
36458
+ * merging into it — Claude Code needs no equivalent, because it merges its lists
36459
+ * across settings scopes, so a file can only ever add to them.
36460
+ */
36461
+ function collectRestrictionLosingSandboxEntries({ existing, merged, paths, toolLabel }) {
36462
+ const entries = [];
36463
+ const reportedContainers = /* @__PURE__ */ new Set();
36464
+ for (const { path, reason, loosens } of paths) {
36465
+ const before = readSandboxPath({
36466
+ sandbox: existing,
36467
+ path
36468
+ });
36469
+ if (before === void 0) continue;
36470
+ const [rootKey] = path;
36471
+ if (rootKey !== void 0 && existing[rootKey] === merged[rootKey]) continue;
36472
+ const after = readSandboxPath({
36473
+ sandbox: merged,
36474
+ path
36475
+ });
36476
+ const label = `sandbox.${path.join(".")}`;
36477
+ if (before === UNREADABLE_SANDBOX_PATH) {
36478
+ const container = findUnreadableContainer({
36479
+ sandbox: existing,
36480
+ path
36481
+ });
36482
+ if (container === void 0 || reportedContainers.has(container)) continue;
36483
+ reportedContainers.add(container);
36484
+ entries.push({
36485
+ label: container,
36486
+ reason: replacedUnreadableReason({
36487
+ shape: "object",
36488
+ toolLabel
36489
+ })
36490
+ });
36491
+ continue;
36492
+ }
36493
+ if (!Array.isArray(before)) {
36494
+ entries.push({
36495
+ label,
36496
+ reason: replacedUnreadableReason({
36497
+ shape: "list",
36498
+ toolLabel
36499
+ })
36500
+ });
36501
+ continue;
36502
+ }
36503
+ if (before.length === 0) continue;
36504
+ if (!loosens({
36505
+ before,
36506
+ after: Array.isArray(after) ? after : []
36507
+ })) continue;
36508
+ entries.push({
36509
+ label,
36510
+ reason
36511
+ });
36512
+ }
36513
+ return entries;
36514
+ }
36515
+ /**
36516
+ * The one warning that names every trust-affecting setting this generate wrote
36517
+ * to `relativeFilePath`. Emitted once per file: the individual reasons are what
36518
+ * matter, but the "review this as you would a hook" framing only needs saying
36519
+ * once, and repeating it per key buries the reasons in boilerplate. `noun` lets
36520
+ * a tool whose entries are not all additions call them something more accurate
36521
+ * than "setting".
36522
+ */
36523
+ function warnOnTrustAffectingEntries({ toolLabel, noun = "setting", entries, relativeFilePath, logger }) {
36524
+ if (entries.length === 0) return;
36525
+ const one = entries.length === 1;
36526
+ const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
36527
+ logger?.warn(`${toolLabel} permissions: writing ${entries.length} trust-affecting ${noun}${one ? "" : "s"} to ${relativeFilePath}; review ${one ? "it" : "them"} as you would a hook, especially if this permissions file came from 'rulesync fetch'. ${details}.`);
36528
+ }
36529
+ //#endregion
36327
36530
  //#region src/features/permissions/claudecode-permissions.ts
36328
36531
  /**
36329
36532
  * Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
@@ -36372,19 +36575,6 @@ function parseClaudePermissionEntry(entry) {
36372
36575
  };
36373
36576
  }
36374
36577
  /**
36375
- * Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
36376
- * rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
36377
- * but never matched by those checks, so Claude Code warns at startup for each
36378
- * allow, deny, or ask rule in one of these unmatched forms" — so a canonical
36379
- * `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
36380
- * docs prescribe instead. A tool-name rule with no path is unaffected: it
36381
- * matches the tool everywhere and produces no warning.
36382
- * @see https://code.claude.com/docs/en/permissions
36383
- */
36384
- function isPlainRecord(value) {
36385
- return typeof value === "object" && value !== null && !Array.isArray(value);
36386
- }
36387
- /**
36388
36578
  * Merge `patch` into `base`, recursing into plain objects so a sibling key at
36389
36579
  * any depth survives. Arrays and scalars are replaced, since a list the author
36390
36580
  * states is the list they mean.
@@ -36394,7 +36584,7 @@ function deepMergeRecords(base, patch) {
36394
36584
  for (const [key, value] of Object.entries(patch)) {
36395
36585
  if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
36396
36586
  const existing = merged[key];
36397
- merged[key] = isPlainRecord(existing) && isPlainRecord(value) ? deepMergeRecords(existing, value) : value;
36587
+ merged[key] = isRecord$1(existing) && isRecord$1(value) ? deepMergeRecords(existing, value) : value;
36398
36588
  }
36399
36589
  return merged;
36400
36590
  }
@@ -36459,13 +36649,11 @@ const CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS = [["filesystem", "allowManagedReadP
36459
36649
  * traversed rather than silently skipped.
36460
36650
  */
36461
36651
  function resolveSandboxParent({ root, segments }) {
36462
- let parent = root;
36463
- for (const segment of segments) {
36464
- const next = parent[segment];
36465
- if (!isPlainRecord(next)) return void 0;
36466
- parent = next;
36467
- }
36468
- return parent;
36652
+ const resolved = readSandboxPath({
36653
+ sandbox: root,
36654
+ path: segments
36655
+ });
36656
+ return isRecord$1(resolved) ? resolved : void 0;
36469
36657
  }
36470
36658
  /**
36471
36659
  * Deletes `path` from `target` in place and reports whether anything was there,
@@ -36499,18 +36687,6 @@ function deleteSandboxPath({ target, path }) {
36499
36687
  return true;
36500
36688
  }
36501
36689
  /**
36502
- * The one warning that names every trust-affecting setting this generate wrote
36503
- * to `relativeFilePath`. Emitted once per file: the individual reasons are what
36504
- * matter, but the "review this as you would a hook" framing only needs saying
36505
- * once, and repeating it per key buries the reasons in boilerplate.
36506
- */
36507
- function warnOnTrustAffectingEntries({ entries, relativeFilePath, logger }) {
36508
- if (entries.length === 0) return;
36509
- const one = entries.length === 1;
36510
- const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
36511
- logger?.warn(`Claude Code permissions: writing ${entries.length} trust-affecting ${one ? "setting" : "settings"} to ${relativeFilePath}; review ${one ? "it" : "them"} as you would a hook, especially if this permissions file came from 'rulesync fetch'. ${details}.`);
36512
- }
36513
- /**
36514
36690
  * The `permissions.defaultMode` values that start a session with fewer prompts
36515
36691
  * than the default. `plan` and `default` are absent because they do not widen
36516
36692
  * anything.
@@ -36555,18 +36731,6 @@ const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
36555
36731
  ["socatPath"]
36556
36732
  ];
36557
36733
  /**
36558
- * The predicates the "which value actually widens?" tables are built from.
36559
- * Each names the value that does *not* widen and reports everything else, never
36560
- * the reverse: the override is authored JSONC, so a key can carry any value at
36561
- * all, and one Claude Code coerces is still honored. Reporting an off-type value
36562
- * keeps the warning fail-safe — silence has to mean "this cannot loosen
36563
- * anything", not "this is not the type the table expected".
36564
- */
36565
- const isNotFalse = (value) => value !== false;
36566
- const isNotTrue = (value) => value !== true;
36567
- const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
36568
- const isNonEmptyMap = (value) => !isPlainRecord(value) || Object.keys(value).length > 0;
36569
- /**
36570
36734
  * `sandbox` paths that loosen the sandbox rather than naming something to run:
36571
36735
  * they let commands out of it, weaken the isolation it provides, or redirect
36572
36736
  * where its traffic goes. They are written like `env` is — the ordinary uses are
@@ -36669,30 +36833,6 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
36669
36833
  widens: () => true
36670
36834
  }
36671
36835
  ];
36672
- /**
36673
- * Every authored `sandbox` path that loosens the sandbox. Nothing is removed —
36674
- * the values are written, just not silently. Called on the filtered `sandbox`
36675
- * so it never claims to be writing a path the scope filters dropped.
36676
- */
36677
- function collectTrustAffectingSandboxPaths({ sandbox }) {
36678
- const entries = [];
36679
- for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
36680
- const leaf = path.at(-1);
36681
- if (leaf === void 0) continue;
36682
- const parent = resolveSandboxParent({
36683
- root: sandbox,
36684
- segments: path.slice(0, -1)
36685
- });
36686
- if (parent === void 0) continue;
36687
- const value = parent[leaf];
36688
- if (value === void 0 || !widens(value)) continue;
36689
- entries.push({
36690
- label: `sandbox.${path.join(".")}`,
36691
- reason
36692
- });
36693
- }
36694
- return entries;
36695
- }
36696
36836
  /** Paths that name an executable Claude Code runs. Refused in both scopes. */
36697
36837
  const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL = {
36698
36838
  paths: CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS,
@@ -36763,13 +36903,13 @@ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
36763
36903
  */
36764
36904
  function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
36765
36905
  const credentials = sandbox.credentials;
36766
- if (!isPlainRecord(credentials)) return sandbox;
36906
+ if (!isRecord$1(credentials)) return sandbox;
36767
36907
  const filteredCredentials = { ...credentials };
36768
36908
  let changed = false;
36769
36909
  for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
36770
36910
  const list = filteredCredentials[listKey];
36771
36911
  if (!Array.isArray(list)) continue;
36772
- const kept = list.filter((entry) => !(isPlainRecord(entry) && entry.mode === "mask"));
36912
+ const kept = list.filter((entry) => !(isRecord$1(entry) && entry.mode === "mask"));
36773
36913
  if (kept.length === list.length) continue;
36774
36914
  changed = true;
36775
36915
  const dropped = list.length - kept.length;
@@ -37048,6 +37188,16 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
37048
37188
  trustAffecting
37049
37189
  };
37050
37190
  }
37191
+ /**
37192
+ * Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
37193
+ * rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
37194
+ * but never matched by those checks, so Claude Code warns at startup for each
37195
+ * allow, deny, or ask rule in one of these unmatched forms" — so a canonical
37196
+ * `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
37197
+ * docs prescribe instead. A tool-name rule with no path is unaffected: it
37198
+ * matches the tool everywhere and produces no warning.
37199
+ * @see https://code.claude.com/docs/en/permissions
37200
+ */
37051
37201
  const CLAUDE_PATH_RULE_ALIASES = {
37052
37202
  Write: "Edit",
37053
37203
  NotebookEdit: "Edit",
@@ -37128,7 +37278,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
37128
37278
  };
37129
37279
  }
37130
37280
  const overrideSandbox = config.claudecode?.sandbox;
37131
- if (isPlainRecord(overrideSandbox)) {
37281
+ if (isRecord$1(overrideSandbox)) {
37132
37282
  const honorableSandbox = stripSandboxPaths({
37133
37283
  sandbox: overrideSandbox,
37134
37284
  refusals: [
@@ -37144,8 +37294,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
37144
37294
  relativeFilePath: paths.relativeFilePath,
37145
37295
  logger
37146
37296
  });
37147
- trustAffecting.push(...collectTrustAffectingSandboxPaths({ sandbox: scopedSandbox }));
37148
- if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
37297
+ trustAffecting.push(...collectTrustAffectingSandboxPaths({
37298
+ sandbox: scopedSandbox,
37299
+ paths: CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS
37300
+ }));
37301
+ if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isRecord$1(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
37149
37302
  }
37150
37303
  const overrideTopLevel = {};
37151
37304
  for (const [key, value] of Object.entries(config.claudecode ?? {})) {
@@ -37163,8 +37316,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
37163
37316
  trustAffecting.push(...trustAffectingTopLevel);
37164
37317
  if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
37165
37318
  warnOnTrustAffectingEntries({
37319
+ toolLabel: "Claude Code",
37166
37320
  entries: trustAffecting,
37167
- relativeFilePath: paths.relativeFilePath,
37321
+ relativeFilePath: toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath)),
37168
37322
  logger
37169
37323
  });
37170
37324
  const managedToolNames = managedClaudeToolNames(config);
@@ -37207,7 +37361,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
37207
37361
  const nonListFields = Object.fromEntries(Object.entries(permissionsRest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
37208
37362
  if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
37209
37363
  const { sandbox } = settings;
37210
- if (isPlainRecord(sandbox)) {
37364
+ if (isRecord$1(sandbox)) {
37211
37365
  const importedSandbox = structuredClone(sandbox);
37212
37366
  for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) deleteSandboxPath({
37213
37367
  target: importedSandbox,
@@ -39460,6 +39614,80 @@ function buildDevinPermissionEntry(scope, pattern) {
39460
39614
  if (pattern === "*") return scope;
39461
39615
  return `${scope}(${pattern})`;
39462
39616
  }
39617
+ function asDevinRecord(value) {
39618
+ return isRecord$1(value) ? { ...value } : {};
39619
+ }
39620
+ /**
39621
+ * `sandbox` paths whose authored value loosens the sandbox on its own: they let
39622
+ * a command out of it, or widen what a command left inside it may reach. They
39623
+ * are written — the ordinary uses are far too common to refuse — but never
39624
+ * silently, because a permissions file is shareable (`rulesync fetch` copies one
39625
+ * into a project) and should not be able to open the sandbox without saying so.
39626
+ * This is the same stance `CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS` takes for
39627
+ * the equivalent Claude Code keys, and `widens` follows the same convention of
39628
+ * naming the restrictive value rather than the permissive ones, so a spelling
39629
+ * Devin does not recognize is reported rather than waved through.
39630
+ *
39631
+ * The three keys that restrict — `allowed_domains` (an allowlist only while it
39632
+ * has entries), `denied_domains` and `excluded.deny` — are not here: they loosen
39633
+ * by losing entries, which `DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS` covers.
39634
+ *
39635
+ * @see https://docs.devin.ai/cli/sandbox
39636
+ */
39637
+ const DEVIN_TRUST_AFFECTING_SANDBOX_PATHS = [
39638
+ {
39639
+ path: ["network_mode"],
39640
+ reason: "anything but 'limited' lets sandboxed requests use every HTTP method, not just GET/HEAD/OPTIONS",
39641
+ widens: (value) => value !== "limited"
39642
+ },
39643
+ {
39644
+ path: ["excluded", "allow"],
39645
+ reason: "names commands that run outside the sandbox with no prompt and no sandbox policy",
39646
+ widens: isNonEmptyList
39647
+ },
39648
+ {
39649
+ path: ["excluded", "ask"],
39650
+ reason: "names commands that run outside the sandbox once confirmed, with no sandbox policy",
39651
+ widens: isNonEmptyList
39652
+ }
39653
+ ];
39654
+ /** How Devin is named in the warnings this file emits. */
39655
+ const DEVIN_TOOL_LABEL = "Devin";
39656
+ /**
39657
+ * `sandbox` paths that restrict, and that therefore loosen the policy by losing
39658
+ * entries rather than by holding a value. Devin's config is one file rather than
39659
+ * a stack of settings scopes, and the override is shallow-merged over the
39660
+ * existing `sandbox` at its top level: each of these lists is replaced whole,
39661
+ * and `excluded.deny` vanishes as soon as the override states any other
39662
+ * `excluded` key. Losing an entry has the same effect as adding one to the
39663
+ * permissive keys above, so it is announced the same way. Claude Code needs no
39664
+ * equivalent — it merges its lists across settings scopes, so a file can only
39665
+ * ever add to them.
39666
+ *
39667
+ * `loosens` is asked only about a `before` that actually restricted something,
39668
+ * and the two directions are not symmetric: `allowed_domains` restricts by
39669
+ * listing what is reachable, so it loosens by gaining entries or by emptying
39670
+ * out altogether, while the deny lists loosen by losing entries.
39671
+ *
39672
+ * @see https://docs.devin.ai/cli/sandbox
39673
+ */
39674
+ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
39675
+ {
39676
+ path: ["allowed_domains"],
39677
+ reason: "adds to the proxy allowlist already in the file, or empties it so every domain becomes reachable again",
39678
+ loosens: ({ before, after }) => after.length === 0 || after.some((entry) => !before.includes(entry))
39679
+ },
39680
+ {
39681
+ path: ["denied_domains"],
39682
+ reason: "drops domains the deny list already in the file kept out of reach",
39683
+ loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
39684
+ },
39685
+ {
39686
+ path: ["excluded", "deny"],
39687
+ reason: "drops commands the list already in the file pinned inside the sandbox",
39688
+ loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
39689
+ }
39690
+ ];
39463
39691
  /**
39464
39692
  * Permissions generator for Devin Local (native `.devin/` configuration).
39465
39693
  *
@@ -39475,10 +39703,18 @@ function buildDevinPermissionEntry(scope, pattern) {
39475
39703
  *
39476
39704
  * In global mode the config file is shared with the hooks (`hooks`) feature
39477
39705
  * (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
39478
- * merge into the existing JSON and the file is never deleted; only the managed
39479
- * `permissions` key is rewritten.
39706
+ * merge into the existing JSON and the file is never deleted; only the keys
39707
+ * this feature manages are rewritten — `permissions`, plus `sandbox` in global
39708
+ * mode when the `devin` override authors it.
39709
+ *
39710
+ * The sibling `sandbox` block — which decides what a permitted command may
39711
+ * reach rather than which commands are permitted — has no canonical category
39712
+ * and is authored through the `devin` override in `.rulesync/permissions.jsonc`.
39713
+ * Devin documents it as a user-config-only key, so it is written at global
39714
+ * scope only.
39480
39715
  *
39481
39716
  * @see https://docs.devin.ai/cli/reference/permissions
39717
+ * @see https://docs.devin.ai/cli/sandbox
39482
39718
  */
39483
39719
  var DevinPermissions = class DevinPermissions extends ToolPermissions {
39484
39720
  constructor(params) {
@@ -39489,7 +39725,8 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
39489
39725
  }
39490
39726
  /**
39491
39727
  * config.json may carry the MCP/hooks features' keys, so it is never deleted;
39492
- * only the managed `permissions` key is rewritten.
39728
+ * only the keys this feature manages are rewritten — `permissions`, plus
39729
+ * `sandbox` in global mode when the `devin` override authors it.
39493
39730
  */
39494
39731
  isDeletable() {
39495
39732
  return false;
@@ -39515,7 +39752,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
39515
39752
  validate
39516
39753
  });
39517
39754
  }
39518
- static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
39755
+ static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true, logger }) {
39519
39756
  const paths = DevinPermissions.getSettablePaths({ global });
39520
39757
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
39521
39758
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
@@ -39541,6 +39778,46 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
39541
39778
  else delete mergedPermissions.ask;
39542
39779
  if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
39543
39780
  else delete mergedPermissions.deny;
39781
+ const patch = { permissions: mergedPermissions };
39782
+ const authoredSandbox = config.devin?.sandbox;
39783
+ if (authoredSandbox !== void 0) {
39784
+ const authoredSandboxRecord = asDevinRecord(authoredSandbox);
39785
+ if (global) {
39786
+ const existingSandbox = asDevinRecord(settings.sandbox);
39787
+ const mergedSandbox = {
39788
+ ...existingSandbox,
39789
+ ...authoredSandboxRecord
39790
+ };
39791
+ const writesSandbox = Object.keys(mergedSandbox).length > 0;
39792
+ if (writesSandbox) patch.sandbox = mergedSandbox;
39793
+ const replacesUnreadableSandbox = writesSandbox && settings.sandbox !== void 0 && !isRecord$1(settings.sandbox);
39794
+ warnOnTrustAffectingEntries({
39795
+ toolLabel: DEVIN_TOOL_LABEL,
39796
+ noun: "sandbox change",
39797
+ entries: [
39798
+ ...replacesUnreadableSandbox ? [{
39799
+ label: "sandbox",
39800
+ reason: replacedUnreadableReason({
39801
+ shape: "object",
39802
+ toolLabel: DEVIN_TOOL_LABEL
39803
+ })
39804
+ }] : [],
39805
+ ...collectTrustAffectingSandboxPaths({
39806
+ sandbox: authoredSandboxRecord,
39807
+ paths: DEVIN_TRUST_AFFECTING_SANDBOX_PATHS
39808
+ }),
39809
+ ...collectRestrictionLosingSandboxEntries({
39810
+ existing: existingSandbox,
39811
+ merged: mergedSandbox,
39812
+ paths: DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS,
39813
+ toolLabel: DEVIN_TOOL_LABEL
39814
+ })
39815
+ ],
39816
+ relativeFilePath: toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath)),
39817
+ logger
39818
+ });
39819
+ } else if (Object.keys(authoredSandboxRecord).length > 0) logger?.warn("Devin reads 'sandbox' from the user config only, so the 'devin.sandbox' override was dropped from the project config. Generate with --global to author it.");
39820
+ }
39544
39821
  return new DevinPermissions({
39545
39822
  outputRoot,
39546
39823
  relativeDirPath: paths.relativeDirPath,
@@ -39549,7 +39826,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
39549
39826
  fileKey: sharedConfigFileKey(paths),
39550
39827
  feature: "permissions",
39551
39828
  existingContent,
39552
- patch: { permissions: mergedPermissions },
39829
+ patch,
39553
39830
  filePath
39554
39831
  }),
39555
39832
  validate
@@ -39569,7 +39846,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
39569
39846
  ask: Array.isArray(permissions.ask) ? permissions.ask : [],
39570
39847
  deny: Array.isArray(permissions.deny) ? permissions.deny : []
39571
39848
  });
39572
- return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
39849
+ const sandbox = asDevinRecord(settings.sandbox);
39850
+ const result = { ...config };
39851
+ if (Object.keys(sandbox).length > 0) result.devin = { sandbox };
39852
+ return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
39573
39853
  }
39574
39854
  validate() {
39575
39855
  return {
@@ -42517,6 +42797,137 @@ function buildReasonixPermissionEntry(toolName, pattern) {
42517
42797
  if (pattern === "*") return toolName;
42518
42798
  return `${toolName}(${pattern})`;
42519
42799
  }
42800
+ /** How Reasonix is named in the warnings this file emits. */
42801
+ const REASONIX_TOOL_LABEL = "Reasonix";
42802
+ /** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
42803
+ const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
42804
+ /**
42805
+ * `[sandbox]` keys whose authored value loosens the enforcement layer beneath
42806
+ * the permission policy: they take Bash out of its OS jail, open that jail to
42807
+ * the network, or widen where the file-writing built-ins may write. Written —
42808
+ * the ordinary uses are far too common to refuse — but never silently, the same
42809
+ * stance `DEVIN_TRUST_AFFECTING_SANDBOX_PATHS` takes, and `widens` likewise
42810
+ * names the restrictive value so a spelling Reasonix does not recognize is
42811
+ * reported rather than waved through.
42812
+ *
42813
+ * `forbid_read` is not here: it restricts, so it loosens by losing entries
42814
+ * rather than by holding one, which needs the before/after comparison
42815
+ * {@link REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS} below does instead.
42816
+ *
42817
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
42818
+ */
42819
+ const REASONIX_TRUST_AFFECTING_SANDBOX_PATHS = [
42820
+ {
42821
+ path: ["bash"],
42822
+ reason: "anything but 'enforce' takes Bash out of the OS sandbox, so a command may write and read wherever the user can",
42823
+ widens: (value) => value !== "enforce"
42824
+ },
42825
+ {
42826
+ path: ["network"],
42827
+ reason: "lets sandboxed Bash reach the network",
42828
+ widens: isNotFalse
42829
+ },
42830
+ {
42831
+ path: ["allow_write"],
42832
+ reason: "adds directories the file-writing tools may modify outside the workspace root, which a headless run would otherwise refuse",
42833
+ widens: isNonEmptyList
42834
+ },
42835
+ {
42836
+ path: ["workspace_root"],
42837
+ reason: "moves the root the file-writing tools and sandboxed Bash are confined to, so what they may reach is decided by this path rather than by the project directory",
42838
+ widens: escapesTheProject
42839
+ }
42840
+ ];
42841
+ /**
42842
+ * Whether a `workspace_root` points somewhere other than inside the project the
42843
+ * generate runs in. The key moves the write confinement rather than adding to
42844
+ * it, so the ordinary value — the project directory, spelled relatively — would
42845
+ * otherwise be announced on every generate; anything else is the case worth
42846
+ * naming, since it is how a fetched permissions file would put `~/.ssh` or
42847
+ * `C:\\Users\\<user>` inside the jail: an absolute path in either flavour, one
42848
+ * carrying a drive letter, a home-relative one, a shell or environment
42849
+ * expansion, and any path holding a `..` segment — even one that would land back
42850
+ * inside, since resolving it here would only be a guess at what Reasonix does.
42851
+ * Both path flavours are asked because the file is authored on one machine and
42852
+ * generated on another, so a Windows-shaped root reaching a POSIX check must not
42853
+ * read as relative. Anything that is not a string is reported, per the fail-safe
42854
+ * rule the predicates in `sandbox-trust.ts` follow.
42855
+ */
42856
+ function escapesTheProject(value) {
42857
+ if (typeof value !== "string") return true;
42858
+ const trimmed = value.trim();
42859
+ if (trimmed === "") return false;
42860
+ if (trimmed.startsWith("~")) return true;
42861
+ if (posix.isAbsolute(trimmed) || win32.isAbsolute(trimmed)) return true;
42862
+ if (/^[A-Za-z]:/.test(trimmed)) return true;
42863
+ if (trimmed.includes("$") || /%[^%]+%/.test(trimmed)) return true;
42864
+ return trimmed.split(/[\\/]/).includes("..");
42865
+ }
42866
+ /**
42867
+ * The `[sandbox]` key that restricts, and so loosens by losing entries rather
42868
+ * than by holding a value. The override is shallow-merged over the existing
42869
+ * `[sandbox]` at its top level, so an authored `forbid_read` replaces the list
42870
+ * the file had whole — emptying it, or dropping the `${HOME}/.ssh` entry
42871
+ * Reasonix's own example recommends, opens exactly what the list kept closed.
42872
+ *
42873
+ * @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
42874
+ */
42875
+ const REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS = [{
42876
+ path: ["forbid_read"],
42877
+ reason: "drops paths the list already in the file kept out of read, list and search",
42878
+ loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
42879
+ }];
42880
+ /**
42881
+ * Everything worth naming about the `[sandbox]` table this generate is about to
42882
+ * write: the authored values that loosen enforcement, the restrictions the
42883
+ * shallow merge would drop, and a `[sandbox]` the file holds in some shape other
42884
+ * than a table, which the write replaces wholesale. The widening check reads the
42885
+ * authored block alone — a loosening value the file already held is the user's
42886
+ * own, and re-announcing it on every generate would bury the values rulesync
42887
+ * actually wrote — while a loss can only be seen from both sides.
42888
+ */
42889
+ function collectSandboxOverlayEntries({ existing, authored, merged }) {
42890
+ return [
42891
+ ...existing !== void 0 && !isRecord$1(existing) ? [{
42892
+ label: "sandbox",
42893
+ reason: replacedUnreadableReason({
42894
+ shape: "object",
42895
+ toolLabel: REASONIX_TOOL_LABEL
42896
+ })
42897
+ }] : [],
42898
+ ...collectTrustAffectingSandboxPaths({
42899
+ sandbox: asReasonixRecord(authored),
42900
+ paths: REASONIX_TRUST_AFFECTING_SANDBOX_PATHS
42901
+ }),
42902
+ ...collectRestrictionLosingSandboxEntries({
42903
+ existing: asReasonixRecord(existing),
42904
+ merged,
42905
+ paths: REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS,
42906
+ toolLabel: REASONIX_TOOL_LABEL
42907
+ })
42908
+ ];
42909
+ }
42910
+ /**
42911
+ * Writes the override's `allow_dynamic_bash` into `[permissions]`, where it sits
42912
+ * beside allow/ask/deny rather than in a table of its own, and reports it when
42913
+ * it is being turned on: it widens what a shareable permissions file lets run
42914
+ * with no human in the loop. Only an authored value is written — leaving the key
42915
+ * out of the override keeps whatever the file already had — and turning it off
42916
+ * narrows, so that stays quiet. Only a literal `false` is quiet, not everything
42917
+ * falsy: `getJson()` casts rather than parses, so a `--no-validate` run can put
42918
+ * a value the schema forbids here, and a value Reasonix might still coerce is
42919
+ * not something to write in silence. The entries are returned rather than logged so
42920
+ * one generate still produces one warning naming everything it wrote.
42921
+ */
42922
+ function applyAllowDynamicBash({ permissions, authored }) {
42923
+ if (authored === void 0) return [];
42924
+ permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY] = authored;
42925
+ if (!isNotFalse(authored)) return [];
42926
+ return [{
42927
+ label: `permissions.${REASONIX_ALLOW_DYNAMIC_BASH_KEY}`,
42928
+ reason: "lets an Allow fallback, Auto included, run the nested and indirect Bash that otherwise needs a human or an exact-literal grant — command and process substitution, a dynamic command name, 'eval', 'source', 'sh -c' and their kind"
42929
+ }];
42930
+ }
42520
42931
  function parseReasonixConfig(fileContent) {
42521
42932
  const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
42522
42933
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
@@ -42573,6 +42984,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
42573
42984
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
42574
42985
  const paths = this.getSettablePaths({ global });
42575
42986
  const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
42987
+ const relativeFilePathForLog = toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath));
42576
42988
  const existingContent = await readFileContentOrNull(filePath) ?? "";
42577
42989
  const parsed = parseReasonixConfig(existingContent);
42578
42990
  const config = rulesyncPermissions.getJson();
@@ -42617,11 +43029,23 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
42617
43029
  ...deny,
42618
43030
  ...rawDeny
42619
43031
  ]);
43032
+ const trustAffecting = applyAllowDynamicBash({
43033
+ permissions: mergedPermissions,
43034
+ authored: override?.allowDynamicBash
43035
+ });
42620
43036
  const patch = { permissions: mergedPermissions };
42621
- if (override?.sandbox !== void 0) patch.sandbox = {
42622
- ...asReasonixRecord(parsed.sandbox),
42623
- ...asReasonixRecord(override.sandbox)
42624
- };
43037
+ if (override?.sandbox !== void 0) {
43038
+ const mergedSandbox = {
43039
+ ...asReasonixRecord(parsed.sandbox),
43040
+ ...asReasonixRecord(override.sandbox)
43041
+ };
43042
+ patch.sandbox = mergedSandbox;
43043
+ trustAffecting.push(...collectSandboxOverlayEntries({
43044
+ existing: parsed.sandbox,
43045
+ authored: override.sandbox,
43046
+ merged: mergedSandbox
43047
+ }));
43048
+ }
42625
43049
  if (override?.agent !== void 0) {
42626
43050
  const mergedAgent = {
42627
43051
  ...asReasonixRecord(parsed.agent),
@@ -42629,9 +43053,16 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
42629
43053
  };
42630
43054
  const retired = REASONIX_RETIRED_AGENT_KEYS.filter((key) => mergedAgent[key] !== void 0);
42631
43055
  for (const key of retired) delete mergedAgent[key];
42632
- if (retired.length > 0) logger?.warn(`Reasonix permissions: removing ${retired.map((key) => `"${key}"`).join(", ")} from [agent] in ${filePath}; Reasonix took the key off its config surface in v1.17.18, so what it used to express now belongs in the shared \`permission\` block.`);
43056
+ if (retired.length > 0) logger?.warn(`Reasonix permissions: removing ${retired.map((key) => `"${key}"`).join(", ")} from [agent] in ${relativeFilePathForLog}; Reasonix took the key off its config surface in v1.17.18, so what it used to express now belongs in the shared \`permission\` block.`);
42633
43057
  patch.agent = mergedAgent;
42634
43058
  }
43059
+ warnOnTrustAffectingEntries({
43060
+ toolLabel: REASONIX_TOOL_LABEL,
43061
+ noun: "change",
43062
+ entries: trustAffecting,
43063
+ relativeFilePath: relativeFilePathForLog,
43064
+ logger
43065
+ });
42635
43066
  return new ReasonixPermissions({
42636
43067
  outputRoot,
42637
43068
  relativeDirPath: paths.relativeDirPath,
@@ -42666,6 +43097,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
42666
43097
  const sandbox = asReasonixRecord(this.toml.sandbox);
42667
43098
  const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
42668
43099
  const reasonixOverride = {};
43100
+ const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
43101
+ if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
42669
43102
  if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
42670
43103
  if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
42671
43104
  if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
@@ -68704,4 +69137,4 @@ async function importChecksCore(params) {
68704
69137
  //#endregion
68705
69138
  export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
68706
69139
 
68707
- //# sourceMappingURL=import-DUE1P1zV.js.map
69140
+ //# sourceMappingURL=import-DIDEUv63.js.map