rulesync 17.1.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.
- package/dist/cli/index.cjs +4 -4
- package/dist/cli/index.js +4 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BtKWW__v.cjs → import-BFhL78qW.cjs} +315 -50
- package/dist/{import-Csmx6n4n.js → import-C-Jsxedt.js} +316 -51
- package/dist/{import-Csmx6n4n.js.map → import-C-Jsxedt.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
@@ -44360,7 +44373,7 @@ function addCodexFilesystemRules({ categoryRules, filesystem, workspaceRootFiles
|
|
|
44360
44373
|
});
|
|
44361
44374
|
if (access === void 0) continue;
|
|
44362
44375
|
if (access === "read" && rule.writeRestriction === "ask") logger?.warn(`Codex CLI cannot express "ask" for filesystem write permissions: pattern "${rule.pattern}" will be emitted as read-only.`);
|
|
44363
|
-
if (access === "read" && rule.writeRestriction === "deny") logger?.warn(`Codex CLI maps a write-side deny to read-only access: pattern "${rule.pattern}" will be emitted as "read". A broader read deny may be overridden by this more-specific path.`);
|
|
44376
|
+
if (access === "read" && rule.writeRestriction === "deny" && rule.readUnspecified) logger?.warn(`Codex CLI maps a write-side deny to read-only access: pattern "${rule.pattern}" will be emitted as "read". A broader read deny may be overridden by this more-specific path.`);
|
|
44364
44377
|
addFilesystemRule({
|
|
44365
44378
|
filesystem,
|
|
44366
44379
|
workspaceRootFilesystem,
|
|
@@ -44517,9 +44530,67 @@ function addFilesystemRule({ filesystem, workspaceRootFilesystem, pattern, acces
|
|
|
44517
44530
|
filesystem[pattern] = access;
|
|
44518
44531
|
return;
|
|
44519
44532
|
}
|
|
44520
|
-
|
|
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
|
+
}
|
|
44586
|
+
if ((access === "read" || access === "write") && isWindowsDevicePath(pattern)) {
|
|
44587
|
+
if (writeRestriction !== void 0) {
|
|
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.`);
|
|
44589
|
+
return "deny";
|
|
44590
|
+
}
|
|
44591
|
+
logger?.warn(`Skipping unsupported Codex CLI ${access} filesystem entry for Windows device path "${pattern}"; the base permission profile remains in effect. Use a plain drive path such as "C:\\..." instead.`);
|
|
44592
|
+
return;
|
|
44593
|
+
}
|
|
44523
44594
|
if ((access === "read" || access === "write") && hasUnsupportedCodexFilesystemGlob(pattern)) {
|
|
44524
44595
|
if (writeRestriction !== void 0) {
|
|
44525
44596
|
logger?.warn(`Codex CLI only supports deny access for non-trailing filesystem globs: pattern "${pattern}" will be emitted as "deny" because its write-side ${writeRestriction} restriction cannot be represented as read-only access. Use an exact path or trailing "/**" for read/write access.`);
|
|
@@ -44533,6 +44604,9 @@ function normalizeCodexFilesystemAccess({ pattern, access, writeRestriction, log
|
|
|
44533
44604
|
function hasCodexFilesystemGlob(pattern) {
|
|
44534
44605
|
return pattern.includes("*") || pattern.includes("?") || pattern.includes("[") || pattern.includes("]");
|
|
44535
44606
|
}
|
|
44607
|
+
function isWindowsDevicePath(pattern) {
|
|
44608
|
+
return pattern.startsWith("\\\\?\\") || pattern.startsWith("\\\\.\\");
|
|
44609
|
+
}
|
|
44536
44610
|
function hasUnsupportedCodexFilesystemGlob(pattern) {
|
|
44537
44611
|
return hasCodexFilesystemGlob(pattern.endsWith("/**") ? pattern.slice(0, -3) : pattern);
|
|
44538
44612
|
}
|
|
@@ -44549,7 +44623,9 @@ function addRulesyncFilesystemRule(permission, pattern, access) {
|
|
|
44549
44623
|
permission.read ??= {};
|
|
44550
44624
|
permission.read[pattern] = "allow";
|
|
44551
44625
|
} else {
|
|
44626
|
+
permission.read ??= {};
|
|
44552
44627
|
permission.edit ??= {};
|
|
44628
|
+
permission.read[pattern] = "allow";
|
|
44553
44629
|
permission.edit[pattern] = "allow";
|
|
44554
44630
|
}
|
|
44555
44631
|
}
|
|
@@ -44674,18 +44750,44 @@ function mapReadAction(action) {
|
|
|
44674
44750
|
function mapWriteAction(action) {
|
|
44675
44751
|
return action === "allow" ? "write" : "read";
|
|
44676
44752
|
}
|
|
44753
|
+
/**
|
|
44754
|
+
* Merge the canonical read/edit/write category rules into one Codex access
|
|
44755
|
+
* level per path pattern (Codex models a single `deny` < `read` < `write`
|
|
44756
|
+
* level, with no `ask`).
|
|
44757
|
+
*
|
|
44758
|
+
* - `edit` and `write` collapse onto Codex's write side; when both carry the
|
|
44759
|
+
* same pattern, the more restrictive action wins (`deny` > `ask` > `allow`).
|
|
44760
|
+
* - `read: allow` + write-side `allow` → `"write"`.
|
|
44761
|
+
* - A write-side non-allow → `"read"` (readable but not writable — exactly
|
|
44762
|
+
* what Codex's `"read"` level expresses). An `ask` action is approximated
|
|
44763
|
+
* this way because Codex has no path-level write approval.
|
|
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.
|
|
44771
|
+
* - A `read` non-allow on the same pattern → `"deny"` regardless of the
|
|
44772
|
+
* write side; a contradictory write-side `allow` (unreadable but writable
|
|
44773
|
+
* is not expressible in Codex) is warned about.
|
|
44774
|
+
* - Single-category patterns keep the existing mapReadAction/mapWriteAction
|
|
44775
|
+
* mappings.
|
|
44776
|
+
*
|
|
44777
|
+
* Iteration order is read → edit → write with first-seen pattern order, so
|
|
44778
|
+
* the emitted table is stable regardless of the authored category order.
|
|
44779
|
+
* Note the merge is one-way: `"{path}" = "read"` imports back as
|
|
44780
|
+
* `read: allow` only. The write-side restriction is implied by Codex's access
|
|
44781
|
+
* level and not re-materialized, so both `read: allow` + write-side
|
|
44782
|
+
* `ask`/`deny` and a write-side-only `ask`/`deny` (e.g. `edit: { "/x/**":
|
|
44783
|
+
* "deny" }` with no `read` rule) lose their write restriction on
|
|
44784
|
+
* `rulesync import`. This is deliberate: importing `"read"` as an extra
|
|
44785
|
+
* `edit: deny` would push a restriction to every other tool that the Codex
|
|
44786
|
+
* profile alone cannot prove was authored.
|
|
44787
|
+
*/
|
|
44677
44788
|
function mergeFilesystemCategoryRules({ categoryRules, logger }) {
|
|
44678
44789
|
const readRules = categoryRules.read ?? {};
|
|
44679
|
-
const
|
|
44680
|
-
deny: 2,
|
|
44681
|
-
ask: 1,
|
|
44682
|
-
allow: 0
|
|
44683
|
-
};
|
|
44684
|
-
const writeSideRules = {};
|
|
44685
|
-
for (const category of ["edit", "write"]) for (const [pattern, action] of Object.entries(categoryRules[category] ?? {})) {
|
|
44686
|
-
const existing = writeSideRules[pattern];
|
|
44687
|
-
if (existing === void 0 || writeSideRestrictiveness[action] > writeSideRestrictiveness[existing]) writeSideRules[pattern] = action;
|
|
44688
|
-
}
|
|
44790
|
+
const writeSideRules = collapseWriteSideRules(categoryRules);
|
|
44689
44791
|
const patterns = [];
|
|
44690
44792
|
const seen = /* @__PURE__ */ new Set();
|
|
44691
44793
|
for (const pattern of [...Object.keys(readRules), ...Object.keys(writeSideRules)]) if (!seen.has(pattern)) {
|
|
@@ -44696,37 +44798,131 @@ function mergeFilesystemCategoryRules({ categoryRules, logger }) {
|
|
|
44696
44798
|
for (const pattern of patterns) {
|
|
44697
44799
|
const readAction = readRules[pattern];
|
|
44698
44800
|
const writeAction = writeSideRules[pattern];
|
|
44699
|
-
if (
|
|
44801
|
+
if (writeAction === void 0) {
|
|
44700
44802
|
merged.push({
|
|
44701
44803
|
pattern,
|
|
44702
|
-
access:
|
|
44703
|
-
...writeAction === "ask" || writeAction === "deny" ? { writeRestriction: writeAction } : {}
|
|
44804
|
+
access: mapReadAction(readAction)
|
|
44704
44805
|
});
|
|
44705
44806
|
continue;
|
|
44706
44807
|
}
|
|
44707
|
-
if (
|
|
44808
|
+
if (readAction !== void 0 && readAction !== "allow") {
|
|
44809
|
+
if (writeAction === "allow") logger?.warn(`Codex CLI cannot express "writable but not readable": pattern "${pattern}" has read: ${readAction} and a write-side allow. Emitting "deny".`);
|
|
44708
44810
|
merged.push({
|
|
44709
44811
|
pattern,
|
|
44710
|
-
access:
|
|
44812
|
+
access: "deny"
|
|
44711
44813
|
});
|
|
44712
44814
|
continue;
|
|
44713
44815
|
}
|
|
44714
|
-
|
|
44816
|
+
const writeRestriction = writeAction === "allow" ? void 0 : writeAction;
|
|
44817
|
+
const readUnspecified = readAction === void 0;
|
|
44818
|
+
if (readUnspecified && isCoveredByReadRestriction({
|
|
44819
|
+
pattern,
|
|
44820
|
+
readRules
|
|
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".`);
|
|
44715
44823
|
merged.push({
|
|
44716
44824
|
pattern,
|
|
44717
|
-
access:
|
|
44718
|
-
...writeAction === "ask" || writeAction === "deny" ? { writeRestriction: writeAction } : {}
|
|
44825
|
+
access: "deny"
|
|
44719
44826
|
});
|
|
44720
44827
|
continue;
|
|
44721
44828
|
}
|
|
44722
|
-
if (writeAction === "allow") logger?.warn(`Codex CLI cannot express "writable but not readable": pattern "${pattern}" has read: ${readAction} and a write-side allow. Emitting "deny".`);
|
|
44723
44829
|
merged.push({
|
|
44724
44830
|
pattern,
|
|
44725
|
-
access:
|
|
44831
|
+
access: mapWriteAction(writeAction),
|
|
44832
|
+
...writeRestriction !== void 0 ? { writeRestriction } : {},
|
|
44833
|
+
...readUnspecified ? { readUnspecified } : {}
|
|
44726
44834
|
});
|
|
44727
44835
|
}
|
|
44728
44836
|
return merged;
|
|
44729
44837
|
}
|
|
44838
|
+
function collapseWriteSideRules(categoryRules) {
|
|
44839
|
+
const writeSideRules = {};
|
|
44840
|
+
for (const category of ["edit", "write"]) for (const [pattern, action] of Object.entries(categoryRules[category] ?? {})) {
|
|
44841
|
+
const existing = writeSideRules[pattern];
|
|
44842
|
+
if (existing === void 0 || PERMISSION_ACTION_RESTRICTIVENESS[action] > PERMISSION_ACTION_RESTRICTIVENESS[existing]) writeSideRules[pattern] = action;
|
|
44843
|
+
}
|
|
44844
|
+
return writeSideRules;
|
|
44845
|
+
}
|
|
44846
|
+
function isCoveredByReadRestriction({ pattern, readRules }) {
|
|
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);
|
|
44899
|
+
}
|
|
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;
|
|
44922
|
+
}
|
|
44923
|
+
function stripTrailingSlash(pattern) {
|
|
44924
|
+
return pattern.length > 1 && pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
|
|
44925
|
+
}
|
|
44730
44926
|
function buildCodexBashRulesContent(config) {
|
|
44731
44927
|
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
44732
44928
|
const entries = Object.entries(bashRules);
|
|
@@ -59571,21 +59767,43 @@ const CodexCliSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
|
59571
59767
|
*/
|
|
59572
59768
|
const CODEX_OPENAI_YAML_RELATIVE_PATH = CODEXCLI_OPENAI_YAML_RELATIVE_PATH;
|
|
59573
59769
|
/**
|
|
59574
|
-
*
|
|
59575
|
-
*
|
|
59576
|
-
*
|
|
59770
|
+
* Resolve Codex's `policy.allow_implicit_invocation` from the rulesync inputs.
|
|
59771
|
+
* An explicit `codexcli.policy.allow_implicit_invocation` wins; otherwise the root
|
|
59772
|
+
* `disable-model-invocation: true` maps onto `false`. With neither set the value is
|
|
59773
|
+
* left undefined, since Codex already defaults to `true`.
|
|
59774
|
+
* @see https://github.com/openai/codex/blob/main/codex-rs/skills/src/model.rs
|
|
59775
|
+
*/
|
|
59776
|
+
function resolveAllowImplicitInvocation({ codexcli, disableModelInvocation }) {
|
|
59777
|
+
const explicit = codexcli?.policy?.allow_implicit_invocation;
|
|
59778
|
+
if (explicit !== void 0) return explicit;
|
|
59779
|
+
return disableModelInvocation === true ? false : void 0;
|
|
59780
|
+
}
|
|
59781
|
+
/**
|
|
59782
|
+
* Build the `agents/openai.yaml` object from a rulesync `codexcli` section and the
|
|
59783
|
+
* root-level `disable-model-invocation` flag.
|
|
59784
|
+
* Only produced when the user opts in via `interface`, `policy`, or `dependencies`,
|
|
59785
|
+
* or when the root `disable-model-invocation: true` maps onto
|
|
59786
|
+
* `policy.allow_implicit_invocation: false`; a lone legacy `short-description` keeps
|
|
59787
|
+
* mapping to `SKILL.md` `metadata` only.
|
|
59577
59788
|
* When the sidecar is produced and `interface.short_description` is absent, the
|
|
59578
59789
|
* legacy `short-description` is routed there (its canonical home per Codex docs).
|
|
59579
59790
|
*/
|
|
59580
|
-
function buildOpenaiYamlObject(codexcli) {
|
|
59581
|
-
|
|
59582
|
-
|
|
59583
|
-
|
|
59584
|
-
|
|
59791
|
+
function buildOpenaiYamlObject({ codexcli, disableModelInvocation }) {
|
|
59792
|
+
const allowImplicitInvocation = resolveAllowImplicitInvocation({
|
|
59793
|
+
codexcli,
|
|
59794
|
+
disableModelInvocation
|
|
59795
|
+
});
|
|
59796
|
+
const policySection = {
|
|
59797
|
+
...codexcli?.policy,
|
|
59798
|
+
...allowImplicitInvocation !== void 0 && { allow_implicit_invocation: allowImplicitInvocation }
|
|
59799
|
+
};
|
|
59800
|
+
if (!Boolean(codexcli?.interface || codexcli?.policy || codexcli?.dependencies) && Object.keys(policySection).length === 0) return;
|
|
59801
|
+
const interfaceSection = { ...codexcli?.interface };
|
|
59802
|
+
if (interfaceSection.short_description === void 0 && codexcli?.["short-description"] !== void 0) interfaceSection.short_description = codexcli["short-description"];
|
|
59585
59803
|
const result = {};
|
|
59586
59804
|
if (Object.keys(interfaceSection).length > 0) result.interface = interfaceSection;
|
|
59587
|
-
if (
|
|
59588
|
-
if (codexcli
|
|
59805
|
+
if (Object.keys(policySection).length > 0) result.policy = policySection;
|
|
59806
|
+
if (codexcli?.dependencies && Object.keys(codexcli.dependencies).length > 0) result.dependencies = codexcli.dependencies;
|
|
59589
59807
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
59590
59808
|
}
|
|
59591
59809
|
/**
|
|
@@ -59614,6 +59832,11 @@ function extractOpenaiYamlFile(otherFiles) {
|
|
|
59614
59832
|
}
|
|
59615
59833
|
/**
|
|
59616
59834
|
* Map a parsed `agents/openai.yaml` object back into a rulesync `codexcli` section.
|
|
59835
|
+
* `policy.allow_implicit_invocation` stays under `codexcli.policy` rather than being
|
|
59836
|
+
* promoted to the root `disable-model-invocation`, matching how every other tool's
|
|
59837
|
+
* importer keeps its invocation flags in its own section: promoting it would change
|
|
59838
|
+
* the output of every other target on the next generate, while the section value
|
|
59839
|
+
* regenerates the same sidecar because it takes precedence over the root flag.
|
|
59617
59840
|
*/
|
|
59618
59841
|
function openaiYamlToCodexcliSection(parsed) {
|
|
59619
59842
|
if (!parsed) return;
|
|
@@ -59712,7 +59935,10 @@ var CodexCliSkill = class CodexCliSkill extends ToolSkill {
|
|
|
59712
59935
|
};
|
|
59713
59936
|
const target = toPosixPath(CODEX_OPENAI_YAML_RELATIVE_PATH);
|
|
59714
59937
|
const baseOtherFiles = rulesyncSkill.getOtherFiles().filter((file) => toPosixPath(file.relativeFilePathToDirPath) !== target);
|
|
59715
|
-
const openaiObject = buildOpenaiYamlObject(
|
|
59938
|
+
const openaiObject = buildOpenaiYamlObject({
|
|
59939
|
+
codexcli: rulesyncFrontmatter.codexcli,
|
|
59940
|
+
disableModelInvocation: rulesyncFrontmatter["disable-model-invocation"]
|
|
59941
|
+
});
|
|
59716
59942
|
const otherFiles = openaiObject ? [...baseOtherFiles, {
|
|
59717
59943
|
relativeFilePathToDirPath: CODEX_OPENAI_YAML_RELATIVE_PATH,
|
|
59718
59944
|
fileBuffer: Buffer.from((0, js_yaml.dump)(openaiObject, {
|
|
@@ -79137,6 +79363,7 @@ const findFilesWithFallback = async (primaryFilePaths, alternativeRoots, buildAl
|
|
|
79137
79363
|
* delete generated files it can no longer regenerate and report success.
|
|
79138
79364
|
*/
|
|
79139
79365
|
const rootRelativeGlob = (...segments) => node_path.posix.join(...segments.filter((segment) => segment !== void 0).map(toPosixPath));
|
|
79366
|
+
const isRootRule = (rule) => Boolean(rule.getFrontmatter().root || rule.getFrontmatter().localRoot);
|
|
79140
79367
|
var RulesProcessor = class extends FeatureProcessor {
|
|
79141
79368
|
toolTarget;
|
|
79142
79369
|
simulateCommands;
|
|
@@ -79148,7 +79375,8 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
79148
79375
|
getFactory;
|
|
79149
79376
|
skills;
|
|
79150
79377
|
featureOptions;
|
|
79151
|
-
|
|
79378
|
+
deleteOrphans;
|
|
79379
|
+
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, simulateCommands = false, simulateSubagents = false, simulateSkills = false, language, deriveSubprojectPathFromGlobs = false, global = false, getFactory = defaultGetFactory, skills, featureOptions, delete: deleteOrphans = false, dryRun = false, logger }) {
|
|
79152
79380
|
super({
|
|
79153
79381
|
outputRoot,
|
|
79154
79382
|
inputRoots,
|
|
@@ -79167,6 +79395,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
79167
79395
|
this.getFactory = getFactory;
|
|
79168
79396
|
this.skills = skills;
|
|
79169
79397
|
this.featureOptions = featureOptions;
|
|
79398
|
+
this.deleteOrphans = deleteOrphans;
|
|
79170
79399
|
}
|
|
79171
79400
|
async convertRulesyncFilesToToolFiles(rulesyncFiles) {
|
|
79172
79401
|
const rulesyncRules = rulesyncFiles.filter((file) => file instanceof RulesyncRule);
|
|
@@ -79174,7 +79403,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
79174
79403
|
const factory = this.getFactory(this.toolTarget);
|
|
79175
79404
|
const { meta } = factory;
|
|
79176
79405
|
this.warnForIncludeRootOption(meta);
|
|
79177
|
-
const emittedRules = this.omitsRootFiles() ?
|
|
79406
|
+
const emittedRules = this.omitsRootFiles() ? await this.dropRootRules(alignedRules) : alignedRules;
|
|
79178
79407
|
const localRootRules = emittedRules.filter((rule) => rule.getFrontmatter().localRoot);
|
|
79179
79408
|
const convertedRules = emittedRules.filter((rule) => !rule.getFrontmatter().localRoot).map((rulesyncRule) => {
|
|
79180
79409
|
if (!factory.class.isTargetedByRulesyncRule(rulesyncRule)) return null;
|
|
@@ -79319,8 +79548,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
79319
79548
|
* non-root `.claude/rules/*.md`. Claude Code falls back to `AGENTS.md` only
|
|
79320
79549
|
* when none of `CLAUDE.md`, `.claude/CLAUDE.md` or `CLAUDE.local.md` exists,
|
|
79321
79550
|
* so emitting either root file would shadow an `AGENTS.md` written by another
|
|
79322
|
-
* target.
|
|
79323
|
-
* they are no longer part of the generated output
|
|
79551
|
+
* target. With `delete` on, stale root files are swept by the regular orphan
|
|
79552
|
+
* deletion because they are no longer part of the generated output; without
|
|
79553
|
+
* it, conversion warns about them instead. Global scope keeps the
|
|
79324
79554
|
* root file: `~/.claude/CLAUDE.md` never shadows `AGENTS.md`, and Claude Code
|
|
79325
79555
|
* reads no global `AGENTS.md` to fall back to. `claudecode-legacy` is not
|
|
79326
79556
|
* covered: its `.claude/memories/*.md` are only reachable through the
|
|
@@ -79350,19 +79580,53 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
79350
79580
|
}) !== "auto") this.logger.warn("The rules option `ruleDiscoveryMode` has no effect with `includeRoot: false` on claudecode: the rule references it adds are written into CLAUDE.md, which is not generated.");
|
|
79351
79581
|
}
|
|
79352
79582
|
/**
|
|
79353
|
-
*
|
|
79354
|
-
*
|
|
79355
|
-
*
|
|
79356
|
-
*
|
|
79583
|
+
* Drop the root and local-root rules for `includeRoot: false`, reporting what
|
|
79584
|
+
* would otherwise go unnoticed: a dropped rule no other target emits, and a
|
|
79585
|
+
* root file on disk that keeps shadowing `AGENTS.md`. With `delete` on, the
|
|
79586
|
+
* orphan sweep removes the project-root files and reports the rest itself
|
|
79587
|
+
* (see {@link loadToolFiles}), so conversion only reports them when no sweep
|
|
79588
|
+
* runs.
|
|
79357
79589
|
*/
|
|
79358
|
-
async
|
|
79359
|
-
|
|
79360
|
-
|
|
79361
|
-
|
|
79362
|
-
|
|
79590
|
+
async dropRootRules(rules) {
|
|
79591
|
+
this.warnForClaudecodeOnlyRootRules(rules.filter(isRootRule));
|
|
79592
|
+
if (!this.deleteOrphans || this.hasRulesyncSourceLoadFailure()) await this.warnForShadowingRootFiles({ includePrimaryRoots: true });
|
|
79593
|
+
return rules.filter((rule) => !isRootRule(rule));
|
|
79594
|
+
}
|
|
79595
|
+
/**
|
|
79596
|
+
* A root or local-root rule that targets `claudecode` alone is dropped by
|
|
79597
|
+
* `includeRoot: false`, and no other target writes it either, so its content
|
|
79598
|
+
* would end up nowhere without a word.
|
|
79599
|
+
*/
|
|
79600
|
+
warnForClaudecodeOnlyRootRules(rootRules) {
|
|
79601
|
+
for (const rule of rootRules) {
|
|
79602
|
+
const targets = rule.getFrontmatter().targets ?? ["*"];
|
|
79603
|
+
if (targets.length === 0 || !targets.every((target) => target === "claudecode")) continue;
|
|
79604
|
+
this.logger.warn(`${stripControlCharacters(rule.getRelativeFilePath())} is a root rule that targets only claudecode, so \`includeRoot: false\` drops it and no target generates its content. Add the target that writes AGENTS.md to its \`targets\`, or turn it into a non-root rule.`);
|
|
79605
|
+
}
|
|
79606
|
+
}
|
|
79607
|
+
/**
|
|
79608
|
+
* With `includeRoot: false`, a root file still on disk keeps Claude Code
|
|
79609
|
+
* from reading `AGENTS.md`: a project-root `CLAUDE.md` or `CLAUDE.local.md`
|
|
79610
|
+
* (`includePrimaryRoots`), which only the `delete` sweep removes, and a
|
|
79611
|
+
* hand-authored `.claude/CLAUDE.md` at an alternative location, which
|
|
79612
|
+
* Rulesync never writes and so never deletes. `.claude/CLAUDE.local.md` is
|
|
79613
|
+
* not checked: it is not one of the files that shadow `AGENTS.md`.
|
|
79614
|
+
* @see https://code.claude.com/docs/en/memory#when-claude-code-reads-agents-md
|
|
79615
|
+
*/
|
|
79616
|
+
async warnForShadowingRootFiles({ includePrimaryRoots }) {
|
|
79617
|
+
const factory = this.getFactory(this.toolTarget);
|
|
79618
|
+
const settablePaths = factory.class.getSettablePaths({ global: this.global });
|
|
79619
|
+
const primaryDirPath = settablePaths.root?.relativeDirPath ?? ".";
|
|
79620
|
+
const primaryGlobs = (includePrimaryRoots && settablePaths.root ? [settablePaths.root.relativeFilePath, ...factory.meta.localRootFileName ? [factory.meta.localRootFileName] : []] : []).map((fileName) => rootRelativeGlob(primaryDirPath, fileName));
|
|
79621
|
+
const alternativeGlobs = (settablePaths.alternativeRoots ?? []).map((alt) => rootRelativeGlob(alt.relativeDirPath, alt.relativeFilePath));
|
|
79622
|
+
const primaryFilePaths = primaryGlobs.length > 0 ? await findFilesByGlobs(primaryGlobs, { cwd: this.outputRoot }) : [];
|
|
79623
|
+
const alternativeFilePaths = alternativeGlobs.length > 0 ? await findFilesByGlobs(alternativeGlobs, { cwd: this.outputRoot }) : [];
|
|
79624
|
+
const filePaths = [...primaryFilePaths, ...alternativeFilePaths];
|
|
79363
79625
|
if (filePaths.length === 0) return;
|
|
79364
79626
|
const names = filePaths.map((filePath) => stripControlCharacters((0, node_path.relative)(this.outputRoot, filePath)));
|
|
79365
|
-
|
|
79627
|
+
const one = names.length === 1;
|
|
79628
|
+
const deleteHint = primaryFilePaths.length > 0 ? " Enable `delete` to have Rulesync remove a project-root CLAUDE.md or CLAUDE.local.md itself." : "";
|
|
79629
|
+
this.logger.warn(`${names.join(", ")} still ${one ? "keeps" : "keep"} Claude Code from reading AGENTS.md although \`includeRoot: false\` is set for claudecode. Rulesync leaves ${one ? "it" : "them"} in place; remove ${one ? "it" : "them"} by hand once the content is covered by AGENTS.md.${deleteHint}`);
|
|
79366
79630
|
}
|
|
79367
79631
|
isClaudecodeTarget() {
|
|
79368
79632
|
return this.toolTarget === "claudecode" || this.toolTarget === "claudecode-legacy";
|
|
@@ -80003,7 +80267,7 @@ As this project's AI coding tool, you must follow the additional conventions bel
|
|
|
80003
80267
|
const rootToolRules = await (async () => {
|
|
80004
80268
|
if (!settablePaths.root) return [];
|
|
80005
80269
|
if (forDeletion && omitsRootFiles) {
|
|
80006
|
-
await this.
|
|
80270
|
+
await this.warnForShadowingRootFiles({ includePrimaryRoots: false });
|
|
80007
80271
|
return buildDeletionRulesFromPaths(primaryRootFilePaths);
|
|
80008
80272
|
}
|
|
80009
80273
|
const uniqueRootFilePaths = await findFilesWithFallback(primaryRootFilePaths, settablePaths.alternativeRoots, (alt) => rootRelativeGlob(alt.relativeDirPath, alt.relativeFilePath), this.outputRoot);
|
|
@@ -81565,6 +81829,7 @@ async function generateRulesCore(params) {
|
|
|
81565
81829
|
deriveSubprojectPathFromGlobs: config.getDeriveSubprojectPathFromGlobs(),
|
|
81566
81830
|
skills,
|
|
81567
81831
|
featureOptions: config.getFeatureOptions(toolTarget, "rules"),
|
|
81832
|
+
delete: config.getDelete(),
|
|
81568
81833
|
dryRun: config.isPreviewMode(),
|
|
81569
81834
|
logger
|
|
81570
81835
|
});
|