rulesync 18.0.0 → 19.0.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.
@@ -44111,6 +44111,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
44111
44111
  //#region src/features/permissions/codexcli-permissions.ts
44112
44112
  const RULESYNC_PROFILE_NAME = "rulesync";
44113
44113
  const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
44114
+ const CODEX_WORKSPACE_ROOT_SUBPATH = ".";
44114
44115
  const CODEX_WORKSPACE_BASELINE = ":workspace";
44115
44116
  const CODEX_READ_ONLY_BASELINE = ":read-only";
44116
44117
  const CODEX_DANGER_FULL_ACCESS_BASELINE = ":danger-full-access";
@@ -44331,7 +44332,15 @@ function convertRulesyncToCodexProfile({ config, logger }) {
44331
44332
  workspaceRootFilesystem
44332
44333
  });
44333
44334
  if (Object.keys(workspaceRootFilesystem).length > 0) {
44334
- if (typeof filesystem[CODEX_WORKSPACE_ROOTS_KEY] === "string") logger?.warn(`"${CODEX_WORKSPACE_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions, but it will be overwritten by workspace-root rules. Consider removing the direct "${CODEX_WORKSPACE_ROOTS_KEY}" entry.`);
44335
+ const directWorkspaceRootsAccess = filesystem[CODEX_WORKSPACE_ROOTS_KEY];
44336
+ if (isCodexFilesystemAccess(directWorkspaceRootsAccess)) {
44337
+ mergeIntoWorkspaceRootTable({
44338
+ workspaceRootFilesystem,
44339
+ pattern: CODEX_WORKSPACE_ROOT_SUBPATH,
44340
+ access: directWorkspaceRootsAccess
44341
+ });
44342
+ logger?.warn(`"${CODEX_WORKSPACE_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions alongside workspace-relative rules; it is emitted as the "${CODEX_WORKSPACE_ROOT_SUBPATH}" entry of the "${CODEX_WORKSPACE_ROOTS_KEY}" table.`);
44343
+ }
44335
44344
  if (Object.keys(workspaceRootFilesystem).some((pattern) => pattern.includes("**"))) filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
44336
44345
  filesystem[CODEX_WORKSPACE_ROOTS_KEY] = workspaceRootFilesystem;
44337
44346
  }
@@ -44348,9 +44357,13 @@ function convertRulesyncToCodexProfile({ config, logger }) {
44348
44357
  };
44349
44358
  }
44350
44359
  function addCodexFilesystemRules({ categoryRules, filesystem, workspaceRootFilesystem, logger }) {
44351
- for (const rule of mergeFilesystemCategoryRules({
44360
+ const normalizedCategoryRules = normalizeWorkspaceRelativeCategoryRules({
44352
44361
  categoryRules,
44353
44362
  logger
44363
+ });
44364
+ for (const rule of mergeFilesystemCategoryRules({
44365
+ categoryRules: normalizedCategoryRules,
44366
+ logger
44354
44367
  })) {
44355
44368
  const access = normalizeCodexFilesystemAccess({
44356
44369
  pattern: rule.pattern,
@@ -44517,9 +44530,59 @@ function addFilesystemRule({ filesystem, workspaceRootFilesystem, pattern, acces
44517
44530
  filesystem[pattern] = access;
44518
44531
  return;
44519
44532
  }
44520
- workspaceRootFilesystem[pattern] = access;
44533
+ mergeIntoWorkspaceRootTable({
44534
+ workspaceRootFilesystem,
44535
+ pattern,
44536
+ access
44537
+ });
44538
+ }
44539
+ const PERMISSION_ACTION_RESTRICTIVENESS = {
44540
+ deny: 2,
44541
+ ask: 1,
44542
+ allow: 0
44543
+ };
44544
+ function normalizeWorkspaceRelativeCategoryRules({ categoryRules, logger }) {
44545
+ const result = {};
44546
+ for (const category of [
44547
+ "read",
44548
+ "edit",
44549
+ "write"
44550
+ ]) {
44551
+ const rules = categoryRules[category];
44552
+ if (rules === void 0) continue;
44553
+ const normalized = {};
44554
+ for (const [pattern, action] of Object.entries(rules)) {
44555
+ const key = pattern.trim() === "" || canBeCodexFilesystemRoot(pattern) ? pattern : toWorkspaceRootSubpath(pattern);
44556
+ if (key === void 0) {
44557
+ logger?.warn(`Skipping Codex CLI ${category} filesystem entry for workspace-relative pattern "${pattern}": Codex rejects a ":workspace_roots" subpath containing a ".." segment or an empty leading segment and would refuse the whole config, so the rule is dropped entirely — even a deny — and the path is NOT protected by the generated profile. Use a normalized relative path instead.`);
44558
+ continue;
44559
+ }
44560
+ const existing = normalized[key];
44561
+ normalized[key] = existing === void 0 || PERMISSION_ACTION_RESTRICTIVENESS[action] > PERMISSION_ACTION_RESTRICTIVENESS[existing] ? action : existing;
44562
+ }
44563
+ result[category] = normalized;
44564
+ }
44565
+ return result;
44566
+ }
44567
+ function toWorkspaceRootSubpath(pattern) {
44568
+ let subpath = pattern;
44569
+ while (subpath.startsWith("./")) subpath = subpath.slice(2);
44570
+ if (subpath === "" || subpath === "**" && subpath !== pattern) return CODEX_WORKSPACE_ROOT_SUBPATH;
44571
+ if (subpath.startsWith("/") || subpath.split(/[\\/]/).some((segment) => segment === "..")) return;
44572
+ return subpath;
44573
+ }
44574
+ function mergeIntoWorkspaceRootTable({ workspaceRootFilesystem, pattern, access }) {
44575
+ const existing = workspaceRootFilesystem[pattern];
44576
+ workspaceRootFilesystem[pattern] = existing === void 0 ? access : moreRestrictiveCodexAccess({
44577
+ a: existing,
44578
+ b: access
44579
+ });
44521
44580
  }
44522
44581
  function normalizeCodexFilesystemAccess({ pattern, access, writeRestriction, logger }) {
44582
+ if (isWindowsDevicePath(pattern) && !canBeCodexFilesystemRoot(pattern)) {
44583
+ logger?.warn(`Skipping Codex CLI ${access} filesystem entry for Windows device path "${pattern}": Codex cannot express it as a workspace-root entry (it is rejected on Windows and matches nothing elsewhere), so the rule is dropped entirely — even a deny — and the path is NOT protected by the generated profile. Use a plain drive path such as "C:\\..." instead.`);
44584
+ return;
44585
+ }
44523
44586
  if ((access === "read" || access === "write") && isWindowsDevicePath(pattern)) {
44524
44587
  if (writeRestriction !== void 0) {
44525
44588
  logger?.warn(`Codex CLI does not support Windows device paths for read/write access: pattern "${pattern}" will be emitted as "deny" because its write-side ${writeRestriction} restriction cannot be represented as read-only access. Use a plain drive path such as "C:\\..." instead.`);
@@ -44560,7 +44623,9 @@ function addRulesyncFilesystemRule(permission, pattern, access) {
44560
44623
  permission.read ??= {};
44561
44624
  permission.read[pattern] = "allow";
44562
44625
  } else {
44626
+ permission.read ??= {};
44563
44627
  permission.edit ??= {};
44628
+ permission.read[pattern] = "allow";
44564
44629
  permission.edit[pattern] = "allow";
44565
44630
  }
44566
44631
  }
@@ -44696,10 +44761,13 @@ function mapWriteAction(action) {
44696
44761
  * - A write-side non-allow → `"read"` (readable but not writable — exactly
44697
44762
  * what Codex's `"read"` level expresses). An `ask` action is approximated
44698
44763
  * this way because Codex has no path-level write approval.
44699
- * - A write-side-only non-allow (no `read` rule for the same pattern) whose
44700
- * path is covered by a broader `read` deny/ask rule (e.g. `X/**` covering
44701
- * `X/id_rsa`, or `:root`) → `"deny"`. Codex resolves the most specific
44702
- * path, so emitting `"read"` there would re-open reads the user denied.
44764
+ * - A write-side-only rule (no `read` rule for the same pattern) whose path
44765
+ * is covered by a broader `read` deny/ask rule (e.g. `X/**` covering
44766
+ * `X/id_rsa`, or `:root`) → `"deny"`, for a write-side `allow` too (with a
44767
+ * warning). Codex resolves the most specific path, so emitting `"read"` or
44768
+ * `"write"` there would re-open reads the user denied. Only the most
44769
+ * specific covering read rule counts: a narrower `read: allow` between the
44770
+ * broad deny and the path keeps the path readable.
44703
44771
  * - A `read` non-allow on the same pattern → `"deny"` regardless of the
44704
44772
  * write side; a contradictory write-side `allow` (unreadable but writable
44705
44773
  * is not expressible in Codex) is warned about.
@@ -44747,10 +44815,11 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
44747
44815
  }
44748
44816
  const writeRestriction = writeAction === "allow" ? void 0 : writeAction;
44749
44817
  const readUnspecified = readAction === void 0;
44750
- if (readUnspecified && writeRestriction !== void 0 && isCoveredByReadRestriction({
44818
+ if (readUnspecified && isCoveredByReadRestriction({
44751
44819
  pattern,
44752
44820
  readRules
44753
44821
  })) {
44822
+ if (writeRestriction === void 0) logger?.warn(`Codex CLI cannot express "writable but not readable": pattern "${pattern}" has a write-side allow but is covered by a broader read deny/ask rule. Emitting "deny".`);
44754
44823
  merged.push({
44755
44824
  pattern,
44756
44825
  access: "deny"
@@ -44767,33 +44836,89 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
44767
44836
  return merged;
44768
44837
  }
44769
44838
  function collapseWriteSideRules(categoryRules) {
44770
- const writeSideRestrictiveness = {
44771
- deny: 2,
44772
- ask: 1,
44773
- allow: 0
44774
- };
44775
44839
  const writeSideRules = {};
44776
44840
  for (const category of ["edit", "write"]) for (const [pattern, action] of Object.entries(categoryRules[category] ?? {})) {
44777
44841
  const existing = writeSideRules[pattern];
44778
- if (existing === void 0 || writeSideRestrictiveness[action] > writeSideRestrictiveness[existing]) writeSideRules[pattern] = action;
44842
+ if (existing === void 0 || PERMISSION_ACTION_RESTRICTIVENESS[action] > PERMISSION_ACTION_RESTRICTIVENESS[existing]) writeSideRules[pattern] = action;
44779
44843
  }
44780
44844
  return writeSideRules;
44781
44845
  }
44782
44846
  function isCoveredByReadRestriction({ pattern, readRules }) {
44783
- const key = stripLeadingDotSlash(pattern);
44784
- return Object.entries(readRules).some(([readPattern, action]) => {
44785
- if (action === "allow" || readPattern === pattern) return false;
44786
- if (readPattern === ":root" || readPattern === "/**") return true;
44787
- if (readPattern === "**") return !canBeCodexFilesystemRoot(pattern);
44788
- const withoutTrailingGlob = readPattern.endsWith("/**") ? readPattern.slice(0, -3) : readPattern;
44789
- if (withoutTrailingGlob.startsWith(":") || hasCodexFilesystemGlob(withoutTrailingGlob)) return false;
44790
- const base = stripTrailingSlash(stripLeadingDotSlash(withoutTrailingGlob));
44791
- if (base === "") return false;
44792
- return key === base || key.startsWith(base.endsWith("/") ? base : `${base}/`);
44793
- });
44847
+ const key = normalizeCoverageKey(pattern);
44848
+ const isWorkspaceRelative = !canBeCodexFilesystemRoot(pattern);
44849
+ const keyHasDotSegment = hasDotPathSegment(pattern);
44850
+ let best;
44851
+ for (const [readPattern, action] of Object.entries(readRules)) {
44852
+ if (readPattern === pattern) continue;
44853
+ const specificity = coverageSpecificity({
44854
+ readPattern,
44855
+ key,
44856
+ isWorkspaceRelative
44857
+ });
44858
+ if (specificity === void 0) continue;
44859
+ const restrictive = action !== "allow";
44860
+ if (!restrictive && isSkippedCodexGrant(readPattern)) continue;
44861
+ if (!restrictive && (keyHasDotSegment || hasDotPathSegment(readPattern))) continue;
44862
+ if (best === void 0 || specificity > best.specificity || specificity === best.specificity && restrictive) best = {
44863
+ specificity,
44864
+ restrictive
44865
+ };
44866
+ }
44867
+ return best?.restrictive ?? false;
44868
+ }
44869
+ function coverageSpecificity({ readPattern, key, isWorkspaceRelative }) {
44870
+ if (readPattern === ":root" || readPattern === "/**") return 0;
44871
+ if (readPattern === CODEX_WORKSPACE_ROOTS_KEY) return isWorkspaceRelative ? 1 : void 0;
44872
+ if (canBeCodexFilesystemRoot(readPattern) === isWorkspaceRelative) return;
44873
+ const normalized = normalizeCoverageKey(readPattern);
44874
+ if (normalized === "**" || normalized === ".") return isWorkspaceRelative ? 1 : void 0;
44875
+ const withoutTrailingGlob = normalized.endsWith("/**") ? normalized.slice(0, -3) : normalized;
44876
+ if (withoutTrailingGlob.startsWith(":") || hasCodexFilesystemGlob(withoutTrailingGlob)) return;
44877
+ const base = stripTrailingSlash(withoutTrailingGlob);
44878
+ if (base === "") return;
44879
+ return key === base || key.startsWith(base.endsWith("/") ? base : `${base}/`) ? 2 + base.length : void 0;
44880
+ }
44881
+ function normalizeCoverageKey(pattern) {
44882
+ const form = toCoverageForm(pattern);
44883
+ return canBeCodexFilesystemRoot(pattern) && !pattern.startsWith(":") ? resolveDotSegments(form) : form;
44884
+ }
44885
+ function resolveDotSegments(path) {
44886
+ const [anchor = "", ...rest] = path.split("/");
44887
+ const stack = [];
44888
+ for (const segment of rest) {
44889
+ if (segment === ".") continue;
44890
+ if (segment === "..") {
44891
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") stack.pop();
44892
+ else if (anchor === "~") stack.push(segment);
44893
+ continue;
44894
+ }
44895
+ stack.push(segment);
44896
+ }
44897
+ const resolved = [anchor, ...stack].join("/");
44898
+ return resolved === "" ? "/" : stripTrailingSlash(resolved);
44794
44899
  }
44795
- function stripLeadingDotSlash(pattern) {
44796
- return pattern.startsWith("./") ? pattern.slice(2) : pattern;
44900
+ function toCoverageForm(pattern) {
44901
+ let normalized = /^[A-Za-z]:[\\/]/.test(pattern) ? `${pattern.charAt(0).toLowerCase()}${pattern.slice(1).replaceAll("\\", "/")}` : pattern;
44902
+ while (normalized.startsWith("./")) normalized = normalized.slice(2);
44903
+ normalized = stripTrailingSlash(normalized);
44904
+ return normalized === "" && pattern !== "" ? "." : normalized;
44905
+ }
44906
+ function hasDotPathSegment(pattern) {
44907
+ const form = toCoverageForm(pattern);
44908
+ if (form === ".") return false;
44909
+ return form.split(/[\\/]/).some((segment) => segment === "." || segment === "..");
44910
+ }
44911
+ function isSkippedCodexGrant(pattern) {
44912
+ return isWindowsDevicePath(pattern) || hasUnsupportedCodexFilesystemGlob(pattern);
44913
+ }
44914
+ function moreRestrictiveCodexAccess({ a, b }) {
44915
+ const rank = {
44916
+ deny: 0,
44917
+ none: 0,
44918
+ read: 1,
44919
+ write: 2
44920
+ };
44921
+ return rank[a] <= rank[b] ? a : b;
44797
44922
  }
44798
44923
  function stripTrailingSlash(pattern) {
44799
44924
  return pattern.length > 1 && pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
@@ -44086,6 +44086,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
44086
44086
  //#region src/features/permissions/codexcli-permissions.ts
44087
44087
  const RULESYNC_PROFILE_NAME = "rulesync";
44088
44088
  const CODEX_WORKSPACE_ROOTS_KEY = ":workspace_roots";
44089
+ const CODEX_WORKSPACE_ROOT_SUBPATH = ".";
44089
44090
  const CODEX_WORKSPACE_BASELINE = ":workspace";
44090
44091
  const CODEX_READ_ONLY_BASELINE = ":read-only";
44091
44092
  const CODEX_DANGER_FULL_ACCESS_BASELINE = ":danger-full-access";
@@ -44306,7 +44307,15 @@ function convertRulesyncToCodexProfile({ config, logger }) {
44306
44307
  workspaceRootFilesystem
44307
44308
  });
44308
44309
  if (Object.keys(workspaceRootFilesystem).length > 0) {
44309
- if (typeof filesystem[CODEX_WORKSPACE_ROOTS_KEY] === "string") logger?.warn(`"${CODEX_WORKSPACE_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions, but it will be overwritten by workspace-root rules. Consider removing the direct "${CODEX_WORKSPACE_ROOTS_KEY}" entry.`);
44310
+ const directWorkspaceRootsAccess = filesystem[CODEX_WORKSPACE_ROOTS_KEY];
44311
+ if (isCodexFilesystemAccess(directWorkspaceRootsAccess)) {
44312
+ mergeIntoWorkspaceRootTable({
44313
+ workspaceRootFilesystem,
44314
+ pattern: CODEX_WORKSPACE_ROOT_SUBPATH,
44315
+ access: directWorkspaceRootsAccess
44316
+ });
44317
+ logger?.warn(`"${CODEX_WORKSPACE_ROOTS_KEY}" is set as a direct filesystem access rule in the permissions alongside workspace-relative rules; it is emitted as the "${CODEX_WORKSPACE_ROOT_SUBPATH}" entry of the "${CODEX_WORKSPACE_ROOTS_KEY}" table.`);
44318
+ }
44310
44319
  if (Object.keys(workspaceRootFilesystem).some((pattern) => pattern.includes("**"))) filesystem.glob_scan_max_depth = CODEX_GLOB_SCAN_MAX_DEPTH;
44311
44320
  filesystem[CODEX_WORKSPACE_ROOTS_KEY] = workspaceRootFilesystem;
44312
44321
  }
@@ -44323,9 +44332,13 @@ function convertRulesyncToCodexProfile({ config, logger }) {
44323
44332
  };
44324
44333
  }
44325
44334
  function addCodexFilesystemRules({ categoryRules, filesystem, workspaceRootFilesystem, logger }) {
44326
- for (const rule of mergeFilesystemCategoryRules({
44335
+ const normalizedCategoryRules = normalizeWorkspaceRelativeCategoryRules({
44327
44336
  categoryRules,
44328
44337
  logger
44338
+ });
44339
+ for (const rule of mergeFilesystemCategoryRules({
44340
+ categoryRules: normalizedCategoryRules,
44341
+ logger
44329
44342
  })) {
44330
44343
  const access = normalizeCodexFilesystemAccess({
44331
44344
  pattern: rule.pattern,
@@ -44492,9 +44505,59 @@ function addFilesystemRule({ filesystem, workspaceRootFilesystem, pattern, acces
44492
44505
  filesystem[pattern] = access;
44493
44506
  return;
44494
44507
  }
44495
- workspaceRootFilesystem[pattern] = access;
44508
+ mergeIntoWorkspaceRootTable({
44509
+ workspaceRootFilesystem,
44510
+ pattern,
44511
+ access
44512
+ });
44513
+ }
44514
+ const PERMISSION_ACTION_RESTRICTIVENESS = {
44515
+ deny: 2,
44516
+ ask: 1,
44517
+ allow: 0
44518
+ };
44519
+ function normalizeWorkspaceRelativeCategoryRules({ categoryRules, logger }) {
44520
+ const result = {};
44521
+ for (const category of [
44522
+ "read",
44523
+ "edit",
44524
+ "write"
44525
+ ]) {
44526
+ const rules = categoryRules[category];
44527
+ if (rules === void 0) continue;
44528
+ const normalized = {};
44529
+ for (const [pattern, action] of Object.entries(rules)) {
44530
+ const key = pattern.trim() === "" || canBeCodexFilesystemRoot(pattern) ? pattern : toWorkspaceRootSubpath(pattern);
44531
+ if (key === void 0) {
44532
+ logger?.warn(`Skipping Codex CLI ${category} filesystem entry for workspace-relative pattern "${pattern}": Codex rejects a ":workspace_roots" subpath containing a ".." segment or an empty leading segment and would refuse the whole config, so the rule is dropped entirely — even a deny — and the path is NOT protected by the generated profile. Use a normalized relative path instead.`);
44533
+ continue;
44534
+ }
44535
+ const existing = normalized[key];
44536
+ normalized[key] = existing === void 0 || PERMISSION_ACTION_RESTRICTIVENESS[action] > PERMISSION_ACTION_RESTRICTIVENESS[existing] ? action : existing;
44537
+ }
44538
+ result[category] = normalized;
44539
+ }
44540
+ return result;
44541
+ }
44542
+ function toWorkspaceRootSubpath(pattern) {
44543
+ let subpath = pattern;
44544
+ while (subpath.startsWith("./")) subpath = subpath.slice(2);
44545
+ if (subpath === "" || subpath === "**" && subpath !== pattern) return CODEX_WORKSPACE_ROOT_SUBPATH;
44546
+ if (subpath.startsWith("/") || subpath.split(/[\\/]/).some((segment) => segment === "..")) return;
44547
+ return subpath;
44548
+ }
44549
+ function mergeIntoWorkspaceRootTable({ workspaceRootFilesystem, pattern, access }) {
44550
+ const existing = workspaceRootFilesystem[pattern];
44551
+ workspaceRootFilesystem[pattern] = existing === void 0 ? access : moreRestrictiveCodexAccess({
44552
+ a: existing,
44553
+ b: access
44554
+ });
44496
44555
  }
44497
44556
  function normalizeCodexFilesystemAccess({ pattern, access, writeRestriction, logger }) {
44557
+ if (isWindowsDevicePath(pattern) && !canBeCodexFilesystemRoot(pattern)) {
44558
+ logger?.warn(`Skipping Codex CLI ${access} filesystem entry for Windows device path "${pattern}": Codex cannot express it as a workspace-root entry (it is rejected on Windows and matches nothing elsewhere), so the rule is dropped entirely — even a deny — and the path is NOT protected by the generated profile. Use a plain drive path such as "C:\\..." instead.`);
44559
+ return;
44560
+ }
44498
44561
  if ((access === "read" || access === "write") && isWindowsDevicePath(pattern)) {
44499
44562
  if (writeRestriction !== void 0) {
44500
44563
  logger?.warn(`Codex CLI does not support Windows device paths for read/write access: pattern "${pattern}" will be emitted as "deny" because its write-side ${writeRestriction} restriction cannot be represented as read-only access. Use a plain drive path such as "C:\\..." instead.`);
@@ -44535,7 +44598,9 @@ function addRulesyncFilesystemRule(permission, pattern, access) {
44535
44598
  permission.read ??= {};
44536
44599
  permission.read[pattern] = "allow";
44537
44600
  } else {
44601
+ permission.read ??= {};
44538
44602
  permission.edit ??= {};
44603
+ permission.read[pattern] = "allow";
44539
44604
  permission.edit[pattern] = "allow";
44540
44605
  }
44541
44606
  }
@@ -44671,10 +44736,13 @@ function mapWriteAction(action) {
44671
44736
  * - A write-side non-allow → `"read"` (readable but not writable — exactly
44672
44737
  * what Codex's `"read"` level expresses). An `ask` action is approximated
44673
44738
  * this way because Codex has no path-level write approval.
44674
- * - A write-side-only non-allow (no `read` rule for the same pattern) whose
44675
- * path is covered by a broader `read` deny/ask rule (e.g. `X/**` covering
44676
- * `X/id_rsa`, or `:root`) → `"deny"`. Codex resolves the most specific
44677
- * path, so emitting `"read"` there would re-open reads the user denied.
44739
+ * - A write-side-only rule (no `read` rule for the same pattern) whose path
44740
+ * is covered by a broader `read` deny/ask rule (e.g. `X/**` covering
44741
+ * `X/id_rsa`, or `:root`) → `"deny"`, for a write-side `allow` too (with a
44742
+ * warning). Codex resolves the most specific path, so emitting `"read"` or
44743
+ * `"write"` there would re-open reads the user denied. Only the most
44744
+ * specific covering read rule counts: a narrower `read: allow` between the
44745
+ * broad deny and the path keeps the path readable.
44678
44746
  * - A `read` non-allow on the same pattern → `"deny"` regardless of the
44679
44747
  * write side; a contradictory write-side `allow` (unreadable but writable
44680
44748
  * is not expressible in Codex) is warned about.
@@ -44722,10 +44790,11 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
44722
44790
  }
44723
44791
  const writeRestriction = writeAction === "allow" ? void 0 : writeAction;
44724
44792
  const readUnspecified = readAction === void 0;
44725
- if (readUnspecified && writeRestriction !== void 0 && isCoveredByReadRestriction({
44793
+ if (readUnspecified && isCoveredByReadRestriction({
44726
44794
  pattern,
44727
44795
  readRules
44728
44796
  })) {
44797
+ if (writeRestriction === void 0) logger?.warn(`Codex CLI cannot express "writable but not readable": pattern "${pattern}" has a write-side allow but is covered by a broader read deny/ask rule. Emitting "deny".`);
44729
44798
  merged.push({
44730
44799
  pattern,
44731
44800
  access: "deny"
@@ -44742,33 +44811,89 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
44742
44811
  return merged;
44743
44812
  }
44744
44813
  function collapseWriteSideRules(categoryRules) {
44745
- const writeSideRestrictiveness = {
44746
- deny: 2,
44747
- ask: 1,
44748
- allow: 0
44749
- };
44750
44814
  const writeSideRules = {};
44751
44815
  for (const category of ["edit", "write"]) for (const [pattern, action] of Object.entries(categoryRules[category] ?? {})) {
44752
44816
  const existing = writeSideRules[pattern];
44753
- if (existing === void 0 || writeSideRestrictiveness[action] > writeSideRestrictiveness[existing]) writeSideRules[pattern] = action;
44817
+ if (existing === void 0 || PERMISSION_ACTION_RESTRICTIVENESS[action] > PERMISSION_ACTION_RESTRICTIVENESS[existing]) writeSideRules[pattern] = action;
44754
44818
  }
44755
44819
  return writeSideRules;
44756
44820
  }
44757
44821
  function isCoveredByReadRestriction({ pattern, readRules }) {
44758
- const key = stripLeadingDotSlash(pattern);
44759
- return Object.entries(readRules).some(([readPattern, action]) => {
44760
- if (action === "allow" || readPattern === pattern) return false;
44761
- if (readPattern === ":root" || readPattern === "/**") return true;
44762
- if (readPattern === "**") return !canBeCodexFilesystemRoot(pattern);
44763
- const withoutTrailingGlob = readPattern.endsWith("/**") ? readPattern.slice(0, -3) : readPattern;
44764
- if (withoutTrailingGlob.startsWith(":") || hasCodexFilesystemGlob(withoutTrailingGlob)) return false;
44765
- const base = stripTrailingSlash(stripLeadingDotSlash(withoutTrailingGlob));
44766
- if (base === "") return false;
44767
- return key === base || key.startsWith(base.endsWith("/") ? base : `${base}/`);
44768
- });
44822
+ const key = normalizeCoverageKey(pattern);
44823
+ const isWorkspaceRelative = !canBeCodexFilesystemRoot(pattern);
44824
+ const keyHasDotSegment = hasDotPathSegment(pattern);
44825
+ let best;
44826
+ for (const [readPattern, action] of Object.entries(readRules)) {
44827
+ if (readPattern === pattern) continue;
44828
+ const specificity = coverageSpecificity({
44829
+ readPattern,
44830
+ key,
44831
+ isWorkspaceRelative
44832
+ });
44833
+ if (specificity === void 0) continue;
44834
+ const restrictive = action !== "allow";
44835
+ if (!restrictive && isSkippedCodexGrant(readPattern)) continue;
44836
+ if (!restrictive && (keyHasDotSegment || hasDotPathSegment(readPattern))) continue;
44837
+ if (best === void 0 || specificity > best.specificity || specificity === best.specificity && restrictive) best = {
44838
+ specificity,
44839
+ restrictive
44840
+ };
44841
+ }
44842
+ return best?.restrictive ?? false;
44843
+ }
44844
+ function coverageSpecificity({ readPattern, key, isWorkspaceRelative }) {
44845
+ if (readPattern === ":root" || readPattern === "/**") return 0;
44846
+ if (readPattern === CODEX_WORKSPACE_ROOTS_KEY) return isWorkspaceRelative ? 1 : void 0;
44847
+ if (canBeCodexFilesystemRoot(readPattern) === isWorkspaceRelative) return;
44848
+ const normalized = normalizeCoverageKey(readPattern);
44849
+ if (normalized === "**" || normalized === ".") return isWorkspaceRelative ? 1 : void 0;
44850
+ const withoutTrailingGlob = normalized.endsWith("/**") ? normalized.slice(0, -3) : normalized;
44851
+ if (withoutTrailingGlob.startsWith(":") || hasCodexFilesystemGlob(withoutTrailingGlob)) return;
44852
+ const base = stripTrailingSlash(withoutTrailingGlob);
44853
+ if (base === "") return;
44854
+ return key === base || key.startsWith(base.endsWith("/") ? base : `${base}/`) ? 2 + base.length : void 0;
44855
+ }
44856
+ function normalizeCoverageKey(pattern) {
44857
+ const form = toCoverageForm(pattern);
44858
+ return canBeCodexFilesystemRoot(pattern) && !pattern.startsWith(":") ? resolveDotSegments(form) : form;
44859
+ }
44860
+ function resolveDotSegments(path) {
44861
+ const [anchor = "", ...rest] = path.split("/");
44862
+ const stack = [];
44863
+ for (const segment of rest) {
44864
+ if (segment === ".") continue;
44865
+ if (segment === "..") {
44866
+ if (stack.length > 0 && stack[stack.length - 1] !== "..") stack.pop();
44867
+ else if (anchor === "~") stack.push(segment);
44868
+ continue;
44869
+ }
44870
+ stack.push(segment);
44871
+ }
44872
+ const resolved = [anchor, ...stack].join("/");
44873
+ return resolved === "" ? "/" : stripTrailingSlash(resolved);
44769
44874
  }
44770
- function stripLeadingDotSlash(pattern) {
44771
- return pattern.startsWith("./") ? pattern.slice(2) : pattern;
44875
+ function toCoverageForm(pattern) {
44876
+ let normalized = /^[A-Za-z]:[\\/]/.test(pattern) ? `${pattern.charAt(0).toLowerCase()}${pattern.slice(1).replaceAll("\\", "/")}` : pattern;
44877
+ while (normalized.startsWith("./")) normalized = normalized.slice(2);
44878
+ normalized = stripTrailingSlash(normalized);
44879
+ return normalized === "" && pattern !== "" ? "." : normalized;
44880
+ }
44881
+ function hasDotPathSegment(pattern) {
44882
+ const form = toCoverageForm(pattern);
44883
+ if (form === ".") return false;
44884
+ return form.split(/[\\/]/).some((segment) => segment === "." || segment === "..");
44885
+ }
44886
+ function isSkippedCodexGrant(pattern) {
44887
+ return isWindowsDevicePath(pattern) || hasUnsupportedCodexFilesystemGlob(pattern);
44888
+ }
44889
+ function moreRestrictiveCodexAccess({ a, b }) {
44890
+ const rank = {
44891
+ deny: 0,
44892
+ none: 0,
44893
+ read: 1,
44894
+ write: 2
44895
+ };
44896
+ return rank[a] <= rank[b] ? a : b;
44772
44897
  }
44773
44898
  function stripTrailingSlash(pattern) {
44774
44899
  return pattern.length > 1 && pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
@@ -82459,4 +82584,4 @@ async function importChecksCore(params) {
82459
82584
  //#endregion
82460
82585
  export { RulesyncCommand as $, toPosixPath as $t, CLAUDECODE_MEMORIES_DIR_NAME as A, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as An, checkPathTraversal as At, RulesyncSubagent as B, stripControlCharacters as Bn, listDirectoryEntryNames as Bt, COMMANDCODE_DIR as C, RULESYNC_PERMISSIONS_SCHEMA_URL as Cn, withWarnOnceScope as Ct, CODEXCLI_DIR as D, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Dn, assertDirectoryIfExists as Dt, CODEXCLI_BASH_RULES_FILE_NAME as E, RULESYNC_SKILLS_RELATIVE_DIR_PATH as En, applyFileMode as Et, caseFoldIdentity as F, formatError as Fn, getFileSize as Ft, RulesyncRuleFrontmatterSchema as G, readFileContentOrNull as Gt, RulesyncSkill as H, stripHiddenCharacters as Hn, listSubdirectoryNames as Ht, groupSpellingsByCaseFoldedIdentity as I, truncateText as In, getHomeDirectory as It, RulesyncIgnore as J, removeFile as Jt, RulesyncPermissions as K, removeDirectory as Kt, AUGMENTCODE_DIR as L, hasDeceptiveHiddenCharacters as Ln, isFileNotFoundError as Lt, CLAUDECODE_SKILLS_DIR_PATH as M, ALL_FEATURES as Mn, directoryExists as Mt, FACTORYDROID_DIR as N, ALL_FEATURES_WITH_WILDCARD as Nn, ensureDir as Nt, CLAUDECODE_DIR as O, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as On, assertTreeContainsNoSymlinks as Ot, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, fileExists as Pt, parseJsonc as Q, runWithDirectoryRollback as Qt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as R, hasEnclosingMarkOutsideKeycap as Rn, isFileSystemError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Sn, resetRunWarningState as St, ChecksProcessor as T, RULESYNC_RULES_RELATIVE_DIR_PATH as Tn, ErrorCodes as Tt, RulesyncSkillFrontmatterSchema as U, pathEscapesRoot as Ut, RulesyncSubagentFrontmatterSchema as V, stripControlCharactersKeepingLineFeeds as Vn, listFilePathsRecursively as Vt, RulesyncRule as W, readFileContent as Wt, getRulesyncSourceCandidates as X, removeTempDirectory as Xt, RulesyncHooks as Y, removeFileStrict as Yt, resolveRulesyncSourceWritePath as Z, resolvePath as Zt, IgnoreProcessor as _, RULESYNC_IGNORE_RELATIVE_FILE_PATH as _n, JsonLogger as _t, getProcessorRegistryEntry as a, ToolTargetSchema as an, SHARED_USER_MANAGED_CONFIG_PATHS as at, CommandsProcessor as b, RULESYNC_MCP_SCHEMA_URL as bn, warnOnConflictingFlags as bt, RulesProcessor as c, MAX_FILE_SIZE as cn, mergeInputRootConfigs as ct, CODEBUDDY_DIR as d, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as dn, ConfigFileSchema as dt, writeFileBuffer as en, RulesyncCommandFrontmatterSchema as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_CONFIG_RELATIVE_FILE_PATH as fn, GITIGNORE_DESTINATION_KEY as ft, McpProcessor as g, RULESYNC_HOOKS_RELATIVE_FILE_PATH as gn, ConsoleLogger as gt, shortenToWidth as h, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as hn, findControlCharacter as ht, inspectInputRoots as i, PACKAGING_TOOL_TARGETS as in, loadYaml as it, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, createTempDirectory as jt, CLAUDECODE_LOCAL_RULE_FILE_NAME as k, RULESYNC_USER_CONFIG_DIR_NAME as kn, assertWritablePathInsideRoot as kt, SubagentsProcessor as l, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as ln, resolveEffectiveInputRoots as lt, displayWidthOf as m, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as mn, assertTargetsFeaturesExclusive as mt, formatSourceLoadFailure as n, ALL_TOOL_TARGETS as nn, RulesyncCheckFrontmatterSchema as nt, convertFromTool as o, CURATED_RULES_FEATURE_SUBDIR as on, SKILL_FILE_NAME as ot, ELLIPSIS_WIDTH as p, RULESYNC_CONFIG_SCHEMA_URL as pn, SourceEntrySchema as pt, RulesyncMcp as q, removeDirectoryStrict as qt, generate as r, ALL_TOOL_TARGETS_WITH_WILDCARD as rn, stringifyFrontmatter as rt, isPackagingToolTarget as s, FEATURE_SOURCE_TREE_ENTRIES as sn, ConfigResolver as st, importFromTool as t, writeFileContent as tn, RulesyncCheck as tt, SkillsProcessor as u, RULESYNC_CHECKS_RELATIVE_DIR_PATH as un, CONFLICTING_TARGET_PAIRS as ut, HooksProcessor as v, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as vn, WarningCollectingLogger as vt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as w, RULESYNC_RELATIVE_DIR_PATH as wn, CLIError as wt, QWENCODE_DIR as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, withFallbackLoggerTarget as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, fallbackLogger as yt, getLocalSkillDirNames as z, quoteForLog as zn, isSymlink as zt };
82461
82586
 
82462
- //# sourceMappingURL=import-CA3RFpGB.js.map
82587
+ //# sourceMappingURL=import-C-Jsxedt.js.map