rulesync 43.6.3 → 43.8.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 +183 -163
- package/dist/cli/index.js +181 -161
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-DCvPujUW.cjs → import-C5ETusOs.cjs} +1257 -188
- package/dist/{import-w0JlEoNl.js → import-Cu5o6uKO.js} +1237 -186
- package/dist/{import-w0JlEoNl.js.map → import-Cu5o6uKO.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -7
|
@@ -1793,28 +1793,6 @@ function splitAbsolutePath(absolutePath) {
|
|
|
1793
1793
|
};
|
|
1794
1794
|
}
|
|
1795
1795
|
/**
|
|
1796
|
-
* Whether writing `targetPath` would land outside `rootPath` once every link on
|
|
1797
|
-
* the way there is resolved. Both sides are taken to where a write to them
|
|
1798
|
-
* would land (see {@link writeLandingPath}), so a root that does not fully
|
|
1799
|
-
* exist yet is judged the same way as the target below it.
|
|
1800
|
-
*
|
|
1801
|
-
* A link that stays inside the root passes. That is what sets this apart from
|
|
1802
|
-
* the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
|
|
1803
|
-
* dotfiles checkout linked from inside the home directory is a common shape for
|
|
1804
|
-
* a global write target, and refusing it would refuse `--global` outright.
|
|
1805
|
-
*
|
|
1806
|
-
* A link chain that cycles is reported as an escape: the write could not get
|
|
1807
|
-
* through it, and refusing it is the answer that never writes somewhere
|
|
1808
|
-
* unexpected.
|
|
1809
|
-
*/
|
|
1810
|
-
async function writablePathEscapesRoot({ rootPath, targetPath }) {
|
|
1811
|
-
const relativeLanding = await relativeWriteLanding({
|
|
1812
|
-
rootPath,
|
|
1813
|
-
targetPath
|
|
1814
|
-
});
|
|
1815
|
-
return relativeLanding === null || pathEscapesRoot(relativeLanding);
|
|
1816
|
-
}
|
|
1817
|
-
/**
|
|
1818
1796
|
* Where a write to `targetPath` lands relative to where a write to `rootPath`
|
|
1819
1797
|
* lands, both taken through {@link writeLandingPath}: `""` when the two land on
|
|
1820
1798
|
* the same directory, and `null` when either side's links form a cycle. Errors
|
|
@@ -1827,6 +1805,59 @@ async function relativeWriteLanding({ rootPath, targetPath }) {
|
|
|
1827
1805
|
return relative(rootLanding, targetLanding);
|
|
1828
1806
|
}
|
|
1829
1807
|
/**
|
|
1808
|
+
* Directory names that hold version-control metadata. Bytes landing in one of
|
|
1809
|
+
* them change how the VCS behaves rather than what it tracks: `.git/config`
|
|
1810
|
+
* can set `core.fsmonitor`, `.git/hooks/*` run on the next commit.
|
|
1811
|
+
*/
|
|
1812
|
+
const VCS_METADATA_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
1813
|
+
".git",
|
|
1814
|
+
".hg",
|
|
1815
|
+
".svn",
|
|
1816
|
+
".jj"
|
|
1817
|
+
]);
|
|
1818
|
+
function hasVcsMetadataSegment(path) {
|
|
1819
|
+
return splitPathSegments(path).some((segment) => {
|
|
1820
|
+
const folded = segment.replace(/:.*$/, "").replace(/[. ]+$/, "").toLowerCase();
|
|
1821
|
+
return VCS_METADATA_DIR_NAMES.has(folded) || /^(git|hg|svn|jj)~\d+$/.test(folded);
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Whether a link on the way to `targetPath` carries it into version-control
|
|
1826
|
+
* metadata (`.git/` and its siblings) that its spelling does not name.
|
|
1827
|
+
*
|
|
1828
|
+
* {@link writablePathEscapesRoot} lets a link that stays inside the root
|
|
1829
|
+
* through, but a checked-out repository can commit `CLAUDE.md -> .git/config`:
|
|
1830
|
+
* the landing is inside the root, yet a write there hands the repository author
|
|
1831
|
+
* the operator's next `git` command. `git clone` never transfers `.git/`, so a
|
|
1832
|
+
* path spelled into it is never something the repository chose; only a write
|
|
1833
|
+
* that lands exactly where it is spelled may touch it.
|
|
1834
|
+
*
|
|
1835
|
+
* `relativeLanding` is {@link relativeWriteLanding}'s answer for the same pair,
|
|
1836
|
+
* passed in so a caller that already has it does not resolve the links twice.
|
|
1837
|
+
*/
|
|
1838
|
+
function landingEntersVcsMetadata({ rootPath, targetPath, relativeLanding }) {
|
|
1839
|
+
return hasVcsMetadataSegment(relativeLanding) && relativeLanding !== relative(resolve(rootPath), resolve(targetPath));
|
|
1840
|
+
}
|
|
1841
|
+
/**
|
|
1842
|
+
* Refuse a write target that a symbolic link carries into version-control
|
|
1843
|
+
* metadata (see {@link landingEntersVcsMetadata}). Meant for writes of remote
|
|
1844
|
+
* bytes, next to {@link assertPathResolvesInsideRoot}: an in-root link (a
|
|
1845
|
+
* skill directory linked to a shared one) stays allowed, but not one that
|
|
1846
|
+
* hands the bytes to `.git/config` or `.git/hooks/`.
|
|
1847
|
+
*/
|
|
1848
|
+
async function assertLandingOutsideVcsMetadata({ rootPath, targetPath }) {
|
|
1849
|
+
const relativeLanding = await relativeWriteLanding({
|
|
1850
|
+
rootPath,
|
|
1851
|
+
targetPath
|
|
1852
|
+
});
|
|
1853
|
+
if (relativeLanding === null) throw new PathOutsideRootError(`Refusing to write through a link cycle: ${targetPath}.`);
|
|
1854
|
+
if (landingEntersVcsMetadata({
|
|
1855
|
+
rootPath,
|
|
1856
|
+
targetPath,
|
|
1857
|
+
relativeLanding
|
|
1858
|
+
})) throw new PathOutsideRootError(`Refusing to write ${targetPath}: a symbolic link leads it into version-control metadata.`);
|
|
1859
|
+
}
|
|
1860
|
+
/**
|
|
1830
1861
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1831
1862
|
*
|
|
1832
1863
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -5430,7 +5461,8 @@ const QWENCODE_HOOK_EVENTS = [
|
|
|
5430
5461
|
"todoCreated",
|
|
5431
5462
|
"todoCompleted",
|
|
5432
5463
|
"messageDisplay",
|
|
5433
|
-
"sessionDelete"
|
|
5464
|
+
"sessionDelete",
|
|
5465
|
+
"fileChanged"
|
|
5434
5466
|
];
|
|
5435
5467
|
const CANONICAL_TO_KIMI_CODE_EVENT_NAMES = {
|
|
5436
5468
|
sessionStart: "SessionStart",
|
|
@@ -6764,6 +6796,7 @@ const CANONICAL_TO_JUNIE_EVENT_NAMES = {
|
|
|
6764
6796
|
sessionStart: "SessionStart",
|
|
6765
6797
|
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6766
6798
|
preToolUse: "PreToolUse",
|
|
6799
|
+
postToolUse: "PostToolUse",
|
|
6767
6800
|
stop: "Stop",
|
|
6768
6801
|
stopFailure: "StopFailure",
|
|
6769
6802
|
permissionRequest: "PermissionRequest",
|
|
@@ -6772,11 +6805,12 @@ const CANONICAL_TO_JUNIE_EVENT_NAMES = {
|
|
|
6772
6805
|
/**
|
|
6773
6806
|
* Hook events supported by JetBrains Junie CLI.
|
|
6774
6807
|
*
|
|
6775
|
-
* Junie CLI exposes
|
|
6808
|
+
* Junie CLI exposes eight lifecycle events under the `"hooks"` key of
|
|
6776
6809
|
* `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
|
6777
|
-
* `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`.
|
|
6778
|
-
* to `SessionStart` (source), `PreToolUse`
|
|
6779
|
-
* type), `PermissionRequest` (tool name), and
|
|
6810
|
+
* `PostToolUse`, `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`.
|
|
6811
|
+
* Matchers apply to `SessionStart` (source), `PreToolUse` and `PostToolUse`
|
|
6812
|
+
* (tool name), `StopFailure` (error type), `PermissionRequest` (tool name), and
|
|
6813
|
+
* `SessionEnd` (reason);
|
|
6780
6814
|
* `UserPromptSubmit` and `Stop` are matcher-less and always run. Only
|
|
6781
6815
|
* `type: "command"` hooks are supported. Project-local hooks are ignored for
|
|
6782
6816
|
* safety.
|
|
@@ -6955,7 +6989,8 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
|
|
|
6955
6989
|
todoCreated: "TodoCreated",
|
|
6956
6990
|
todoCompleted: "TodoCompleted",
|
|
6957
6991
|
messageDisplay: "MessageDisplay",
|
|
6958
|
-
sessionDelete: "SessionDelete"
|
|
6992
|
+
sessionDelete: "SessionDelete",
|
|
6993
|
+
fileChanged: "FileChanged"
|
|
6959
6994
|
};
|
|
6960
6995
|
/**
|
|
6961
6996
|
* Map Qwen Code PascalCase event names to canonical camelCase.
|
|
@@ -9210,7 +9245,8 @@ const AntigravityCliPermissionsOverrideSchema = z.looseObject({
|
|
|
9210
9245
|
* The same override also carries the two plugin-consumption keys Auggie reads
|
|
9211
9246
|
* from `settings.json`: `recommendedMarketplaces` (`"owner/repo"` marketplaces
|
|
9212
9247
|
* a workspace prompts its contributors to install; honored only in the
|
|
9213
|
-
* project's `.augment/settings.json
|
|
9248
|
+
* project's `.augment/settings.json`, and only by Auggie 0.x — 1.x no longer
|
|
9249
|
+
* reads it) and `enabledPlugins` (plugin ids of the
|
|
9214
9250
|
* form `"plugin@marketplace"` mapped to on/off, deep-merged across the user,
|
|
9215
9251
|
* project and local tiers).
|
|
9216
9252
|
*
|
|
@@ -9394,6 +9430,14 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
|
|
|
9394
9430
|
* `true`; only an explicit `false` suppresses it. Like
|
|
9395
9431
|
* `base_permission_profile` it is consumed by the profile builder, not
|
|
9396
9432
|
* written as a top-level config key.
|
|
9433
|
+
* - `bash_canonical_matching` — when `true`, the `forbidden` (deny) and
|
|
9434
|
+
* `prompt` (ask) rules generated from `permission.bash` into
|
|
9435
|
+
* `.codex/rules/rulesync.rules` carry `canonical_matching = True`, so Codex
|
|
9436
|
+
* compares their tokens after Unicode canonical case-insensitive folding (`rm` also
|
|
9437
|
+
* matches `RM`). `allow` rules stay exact so the opt-in never widens what is
|
|
9438
|
+
* auto-approved. Requires Codex 0.167.0 or later — an older Codex rejects the
|
|
9439
|
+
* keyword and fails to load the rules file — so it defaults to `false`. Not
|
|
9440
|
+
* written as a config key.
|
|
9397
9441
|
*
|
|
9398
9442
|
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
9399
9443
|
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
@@ -9401,8 +9445,8 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
|
|
|
9401
9445
|
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`, `auto_review`,
|
|
9402
9446
|
* `tui`) are emitted, and
|
|
9403
9447
|
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
9404
|
-
* `base_permission_profile` and `
|
|
9405
|
-
*
|
|
9448
|
+
* `base_permission_profile`, `git_write_rules` and `bash_canonical_matching`
|
|
9449
|
+
* are consumed by the generators rather than written, as described above, and `permission` is the
|
|
9406
9450
|
* tool-scoped canonical block, which `RulesyncPermissions.forTarget` strips out
|
|
9407
9451
|
* of the override before it ever reaches the patch. The allowlist is what keeps
|
|
9408
9452
|
* the override from clobbering a feature-owned key: `mcp_servers.*` per-MCP
|
|
@@ -9459,7 +9503,8 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
|
|
|
9459
9503
|
* @see https://developers.openai.com/codex/config-reference
|
|
9460
9504
|
*/
|
|
9461
9505
|
tui: z.optional(z.looseObject({})),
|
|
9462
|
-
git_write_rules: z.optional(z.boolean())
|
|
9506
|
+
git_write_rules: z.optional(z.boolean()),
|
|
9507
|
+
bash_canonical_matching: z.optional(z.boolean())
|
|
9463
9508
|
});
|
|
9464
9509
|
/**
|
|
9465
9510
|
* Tool-scoped override block for Zed. These Zed `agent` keys sit outside the
|
|
@@ -9543,6 +9588,14 @@ const ZedPermissionsOverrideSchema = z.looseObject({
|
|
|
9543
9588
|
* { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
|
|
9544
9589
|
* @example
|
|
9545
9590
|
* { "forbiddenPlugins": ["*"], "optionalPlugins": ["acme/approved"] }
|
|
9591
|
+
*
|
|
9592
|
+
* `permission_mode` (CLI v3000.12.17+) is the mode new local sessions start in
|
|
9593
|
+
* (`normal`, the upstream default, `accept-edits`, `smart` or `dangerous`).
|
|
9594
|
+
* Like `sandbox` it is a **User Config Only** key, so it is emitted at global
|
|
9595
|
+
* scope only. It is never derived from the canonical rules.
|
|
9596
|
+
*
|
|
9597
|
+
* @example
|
|
9598
|
+
* { "permission_mode": "accept-edits" }
|
|
9546
9599
|
* @see https://docs.devin.ai/cli/sandbox
|
|
9547
9600
|
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
9548
9601
|
* @see https://docs.devin.ai/cli/extensibility/plugins/overview
|
|
@@ -9558,12 +9611,25 @@ const DEVIN_PLUGIN_GOVERNANCE_SCHEMAS = {
|
|
|
9558
9611
|
optionalPlugins: DevinPluginSourceListSchema,
|
|
9559
9612
|
forbiddenPlugins: z.array(z.string())
|
|
9560
9613
|
};
|
|
9614
|
+
/**
|
|
9615
|
+
* Values Devin accepts for the user-config `permission_mode` key. Shared by the
|
|
9616
|
+
* override schema and the import, which drops a value that would not pass it.
|
|
9617
|
+
*
|
|
9618
|
+
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
9619
|
+
*/
|
|
9620
|
+
const DevinPermissionModeSchema = z.enum([
|
|
9621
|
+
"normal",
|
|
9622
|
+
"accept-edits",
|
|
9623
|
+
"smart",
|
|
9624
|
+
"dangerous"
|
|
9625
|
+
]);
|
|
9561
9626
|
const DevinPermissionsOverrideSchema = z.looseObject({
|
|
9562
9627
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
9563
9628
|
sandbox: z.optional(z.looseObject({})),
|
|
9564
9629
|
requiredPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.requiredPlugins),
|
|
9565
9630
|
optionalPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.optionalPlugins),
|
|
9566
|
-
forbiddenPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins)
|
|
9631
|
+
forbiddenPlugins: z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins),
|
|
9632
|
+
permission_mode: z.optional(DevinPermissionModeSchema)
|
|
9567
9633
|
});
|
|
9568
9634
|
/**
|
|
9569
9635
|
* Permissions configuration.
|
|
@@ -11975,6 +12041,98 @@ function resolveStandardSkillFields({ rootFrontmatter, section }) {
|
|
|
11975
12041
|
})
|
|
11976
12042
|
});
|
|
11977
12043
|
}
|
|
12044
|
+
// src/utils/remote-name.ts
|
|
12045
|
+
/**
|
|
12046
|
+
* Name checks for a directory or file name chosen by a remote source (a
|
|
12047
|
+
* fetched repository, an installed git, GitHub or npm source). Shared by
|
|
12048
|
+
* `rulesync fetch` and `rulesync install`, so a name one of them turns away is
|
|
12049
|
+
* not written, or printed raw, by the other.
|
|
12050
|
+
*/
|
|
12051
|
+
/** A name with no character in it that draws anything of its own. */
|
|
12052
|
+
const NOTHING_DRAWN_PATTERN = /^[\s\p{M}]*$/u;
|
|
12053
|
+
/**
|
|
12054
|
+
* The marks a name begins with. A mark is drawn over the character before it,
|
|
12055
|
+
* and a name has nothing before it of its own: a leading one is drawn over
|
|
12056
|
+
* whatever the terminal printed last — the checkbox, the quote around a name
|
|
12057
|
+
* in a warning — while the name reads as if it began with its first letter.
|
|
12058
|
+
*/
|
|
12059
|
+
const LEADING_MARKS_PATTERN = /^\p{M}+/u;
|
|
12060
|
+
/**
|
|
12061
|
+
* A name ending in a dot or an ASCII space, which Win32 strips before it opens
|
|
12062
|
+
* the path. Only the plain space (U+0020): Win32 keeps every other trailing
|
|
12063
|
+
* blank, so a name ending in a no-break or an ideographic space is written
|
|
12064
|
+
* where it says, and is left to the whitespace note of the confusable-name
|
|
12065
|
+
* check rather than dropped here with Windows named as the reason.
|
|
12066
|
+
*/
|
|
12067
|
+
const TRAILING_DOT_OR_SPACE_PATTERN = /[. ]$/;
|
|
12068
|
+
/** A name ending in the `NAME~1` shape of a Windows 8.3 short name, extension included. */
|
|
12069
|
+
const SHORT_NAME_ALIAS_PATTERN = /~\d+(?:\.[^.]*)?$/;
|
|
12070
|
+
/**
|
|
12071
|
+
* A name Win32 reserves for a device — `CON`, `PRN`, `AUX`, `NUL`, `CONIN$`,
|
|
12072
|
+
* `CONOUT$`, `COM1`–`COM9`, `LPT1`–`LPT9` — in any case and with any
|
|
12073
|
+
* extension: `nul.md` is the null device too, since the reservation is decided
|
|
12074
|
+
* on the part before the first dot, with any spaces before that dot dropped
|
|
12075
|
+
* first, so `nul .md` is the null device as well. The port digit may also be
|
|
12076
|
+
* one of the superscript digits ¹ ² ³, which Win32 reads as 1, 2 and 3
|
|
12077
|
+
* (`RtlIsDosDeviceName_U`; CPython's `ntpath.isreserved` follows the same
|
|
12078
|
+
* rules).
|
|
12079
|
+
*/
|
|
12080
|
+
const RESERVED_DEVICE_NAME_PATTERN = /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9\u00B9\u00B2\u00B3]|LPT[1-9\u00B9\u00B2\u00B3]) *(?:\..*)?$/i;
|
|
12081
|
+
/**
|
|
12082
|
+
* Whether Windows resolves a path segment to something other than the entry it
|
|
12083
|
+
* reads as.
|
|
12084
|
+
*
|
|
12085
|
+
* Win32 drops a trailing dot or ASCII space when it resolves a path, so a remote
|
|
12086
|
+
* `skills/deploy.` is the existing `skills/deploy` there. A name ending in the
|
|
12087
|
+
* `NAME~1` shape is the same class of problem: on a volume that generates
|
|
12088
|
+
* short names, it opens whatever long name it stands for. The shape only
|
|
12089
|
+
* means that at the end of a name, so `data~2parser` and `v1.0` are ordinary
|
|
12090
|
+
* names. A reserved device name is the extreme case: `nul` and `nul.md` are
|
|
12091
|
+
* not entries in any directory but the null device itself, so a write to one
|
|
12092
|
+
* lands nowhere, and a directory of that name cannot be created at all.
|
|
12093
|
+
*
|
|
12094
|
+
* The predicate is asked of every segment of a written path — the skill
|
|
12095
|
+
* directory, but equally a nested `references/notes. ` or a `scripts/con.sh`
|
|
12096
|
+
* — since a folded segment anywhere in a path lands the file beside, or on top
|
|
12097
|
+
* of, a sibling the repository never published. The check is not gated on the
|
|
12098
|
+
* platform: the same repository is checked out on several of them, and a name
|
|
12099
|
+
* that lands somewhere else on any one of them is one this tool would rather
|
|
12100
|
+
* turn away everywhere than write differently depending on where it runs.
|
|
12101
|
+
*/
|
|
12102
|
+
function hasWindowsFoldableName(name) {
|
|
12103
|
+
return TRAILING_DOT_OR_SPACE_PATTERN.test(name) || SHORT_NAME_ALIAS_PATTERN.test(name) || RESERVED_DEVICE_NAME_PATTERN.test(name);
|
|
12104
|
+
}
|
|
12105
|
+
/**
|
|
12106
|
+
* Whether a remote name is safe to write and to print as it is.
|
|
12107
|
+
*
|
|
12108
|
+
* A name of nothing but blank space is shown as the empty string, the same as
|
|
12109
|
+
* one that strips down to nothing: both draw as a row with no name on it. So
|
|
12110
|
+
* is a name of nothing but combining marks, which have no character of their
|
|
12111
|
+
* own to sit on and draw as a smear over whatever the terminal puts beside
|
|
12112
|
+
* them. A name that merely begins with a mark is turned away for the same
|
|
12113
|
+
* reason, and shown without it: the mark sits on nothing the name owns. A name
|
|
12114
|
+
* carrying control characters (an ANSI, C1 or OSC sequence) or deceptive
|
|
12115
|
+
* zero-width characters is turned away too, so it can neither rewrite the
|
|
12116
|
+
* terminal nor pass for another name. The Windows-folding check comes after
|
|
12117
|
+
* the hidden characters, so a name that is both is reported by the reason that
|
|
12118
|
+
* keeps its raw form off the terminal.
|
|
12119
|
+
*/
|
|
12120
|
+
function classifyRemoteName(name) {
|
|
12121
|
+
const stripped = stripHiddenCharacters(name);
|
|
12122
|
+
const beginsWithMark = LEADING_MARKS_PATTERN.test(stripped);
|
|
12123
|
+
const display = NOTHING_DRAWN_PATTERN.test(stripped) ? "" : stripped.replace(LEADING_MARKS_PATTERN, "");
|
|
12124
|
+
if (display === "" || beginsWithMark || hasDeceptiveHiddenCharacters(name)) return {
|
|
12125
|
+
kind: "unsafe",
|
|
12126
|
+
display,
|
|
12127
|
+
reason: "hidden"
|
|
12128
|
+
};
|
|
12129
|
+
if (hasWindowsFoldableName(name)) return {
|
|
12130
|
+
kind: "unsafe",
|
|
12131
|
+
display,
|
|
12132
|
+
reason: "foldable"
|
|
12133
|
+
};
|
|
12134
|
+
return { kind: "safe" };
|
|
12135
|
+
}
|
|
11978
12136
|
// src/features/rules/instruction-budget.ts
|
|
11979
12137
|
/**
|
|
11980
12138
|
* Normalizes a relative directory path for comparison: POSIX separators, no
|
|
@@ -13862,7 +14020,11 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13862
14020
|
},
|
|
13863
14021
|
permissions: {
|
|
13864
14022
|
kind: "replace-owned-keys",
|
|
13865
|
-
ownedKeys: [
|
|
14023
|
+
ownedKeys: [
|
|
14024
|
+
"permissions",
|
|
14025
|
+
"sandbox",
|
|
14026
|
+
"permission_mode"
|
|
14027
|
+
]
|
|
13866
14028
|
}
|
|
13867
14029
|
}
|
|
13868
14030
|
},
|
|
@@ -14636,6 +14798,16 @@ var RulesyncSourceConsumer = class {
|
|
|
14636
14798
|
this.dryRun = dryRun;
|
|
14637
14799
|
this.logger = logger;
|
|
14638
14800
|
}
|
|
14801
|
+
/**
|
|
14802
|
+
* Whether the orphan sweep may follow a symbolic link that stays inside the
|
|
14803
|
+
* output root. Only global scope answers true: its root is the operator's
|
|
14804
|
+
* own home directory, where a dotfiles checkout linked in (`~/.claude ->
|
|
14805
|
+
* ~/dotfiles/claude`) is a common shape. A project checkout may carry links
|
|
14806
|
+
* its author committed, so the default refuses them.
|
|
14807
|
+
*/
|
|
14808
|
+
sweepsThroughInRootLinks() {
|
|
14809
|
+
return false;
|
|
14810
|
+
}
|
|
14639
14811
|
recordRulesyncSourceLoadFailure() {
|
|
14640
14812
|
this.rulesyncSourceLoadFailed = true;
|
|
14641
14813
|
}
|
|
@@ -14768,7 +14940,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
14768
14940
|
refused = await refusesDeleteOutsideRoot({
|
|
14769
14941
|
logger: this.logger,
|
|
14770
14942
|
rootPath,
|
|
14771
|
-
dirPath
|
|
14943
|
+
dirPath,
|
|
14944
|
+
allowInRootLinks: this.sweepsThroughInRootLinks()
|
|
14772
14945
|
});
|
|
14773
14946
|
refusedDirs.set(cacheKey, refused);
|
|
14774
14947
|
}
|
|
@@ -14868,27 +15041,45 @@ function mergeByIdentity({ perRoot, identity }) {
|
|
|
14868
15041
|
return order.map((key) => winnerByKey.get(key));
|
|
14869
15042
|
}
|
|
14870
15043
|
/**
|
|
14871
|
-
*
|
|
14872
|
-
* there leads out of `rootPath`,
|
|
15044
|
+
* Why a write to `targetPath` has to be refused, or `null` when it may go
|
|
15045
|
+
* ahead: a link on the way there leads out of `rootPath`, or into
|
|
15046
|
+
* version-control metadata that the path as spelled does not name.
|
|
14873
15047
|
*
|
|
14874
15048
|
* A checked-out repository can carry a symbolic link at an output path
|
|
14875
|
-
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`
|
|
14876
|
-
* through it would land wherever the link points.
|
|
14877
|
-
|
|
14878
|
-
|
|
15049
|
+
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`, `CLAUDE.md ->
|
|
15050
|
+
* .git/config`), and a write through it would land wherever the link points.
|
|
15051
|
+
*/
|
|
15052
|
+
async function writeRefusalReason({ rootPath, targetPath }) {
|
|
15053
|
+
const relativeLanding = await relativeWriteLanding({
|
|
15054
|
+
rootPath,
|
|
15055
|
+
targetPath
|
|
15056
|
+
});
|
|
15057
|
+
if (relativeLanding === null || pathEscapesRoot(relativeLanding)) return `it resolves outside ${quoteForLog(rootPath)} through a symbolic link`;
|
|
15058
|
+
if (landingEntersVcsMetadata({
|
|
15059
|
+
rootPath,
|
|
15060
|
+
targetPath,
|
|
15061
|
+
relativeLanding
|
|
15062
|
+
})) return "it resolves into version-control metadata through a symbolic link";
|
|
15063
|
+
return null;
|
|
15064
|
+
}
|
|
15065
|
+
/**
|
|
15066
|
+
* Whether a write to `targetPath` has to be refused (see
|
|
15067
|
+
* {@link writeRefusalReason}), warning about the refusal. Every writer that
|
|
15068
|
+
* lets the OS follow links — file, directory, and config-rewrite alike — asks
|
|
15069
|
+
* this first so that one warning describes the one behavior.
|
|
14879
15070
|
*/
|
|
14880
15071
|
async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
14881
|
-
|
|
15072
|
+
const reason = await writeRefusalReason({
|
|
14882
15073
|
rootPath,
|
|
14883
15074
|
targetPath
|
|
14884
|
-
})
|
|
14885
|
-
|
|
15075
|
+
});
|
|
15076
|
+
if (reason === null) return false;
|
|
15077
|
+
logger.warn(`Refusing to write ${quoteForLog(targetPath)}: ${reason}`);
|
|
14886
15078
|
return true;
|
|
14887
15079
|
}
|
|
14888
15080
|
/**
|
|
14889
|
-
*
|
|
14890
|
-
*
|
|
14891
|
-
* once for the whole directory.
|
|
15081
|
+
* Why the orphan sweep has to leave the files in `dirPath` alone, or `null`
|
|
15082
|
+
* when it may delete them.
|
|
14892
15083
|
*
|
|
14893
15084
|
* Deletion candidates are enumerated without following links *inside* the
|
|
14894
15085
|
* scanned directory, but the scanned directory itself is reached through
|
|
@@ -14897,13 +15088,40 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
|
14897
15088
|
* an orphan. Only the directory is judged, never the file in it, because
|
|
14898
15089
|
* removing a file that is itself a link unlinks the link where it is spelled
|
|
14899
15090
|
* and never touches its target.
|
|
15091
|
+
*
|
|
15092
|
+
* Beyond the write rules, a project checkout is content another party may
|
|
15093
|
+
* control: a committed `.claude/rules -> ..` would make every untracked `*.md`
|
|
15094
|
+
* in the project read as an orphan. Unless `allowInRootLinks` (global scope,
|
|
15095
|
+
* where the root is the operator's own home directory) the swept directory
|
|
15096
|
+
* must therefore be the one its path spells, with no link on the way.
|
|
14900
15097
|
*/
|
|
14901
|
-
async function
|
|
14902
|
-
|
|
15098
|
+
async function sweepRefusalReason({ rootPath, dirPath, allowInRootLinks }) {
|
|
15099
|
+
const relativeLanding = await relativeWriteLanding({
|
|
14903
15100
|
rootPath,
|
|
14904
15101
|
targetPath: dirPath
|
|
14905
|
-
})
|
|
14906
|
-
|
|
15102
|
+
});
|
|
15103
|
+
if (relativeLanding === null || pathEscapesRoot(relativeLanding)) return `it resolves outside ${quoteForLog(rootPath)} through a symbolic link`;
|
|
15104
|
+
if (landingEntersVcsMetadata({
|
|
15105
|
+
rootPath,
|
|
15106
|
+
targetPath: dirPath,
|
|
15107
|
+
relativeLanding
|
|
15108
|
+
})) return "it resolves into version-control metadata through a symbolic link";
|
|
15109
|
+
if (!allowInRootLinks && relativeLanding !== relative(resolve(rootPath), resolve(dirPath))) return "it is reached through a symbolic link";
|
|
15110
|
+
return null;
|
|
15111
|
+
}
|
|
15112
|
+
/**
|
|
15113
|
+
* Whether deleting the files in `dirPath` has to be refused (see
|
|
15114
|
+
* {@link sweepRefusalReason}), warning about the refusal once for the whole
|
|
15115
|
+
* directory.
|
|
15116
|
+
*/
|
|
15117
|
+
async function refusesDeleteOutsideRoot({ logger, rootPath, dirPath, allowInRootLinks }) {
|
|
15118
|
+
const reason = await sweepRefusalReason({
|
|
15119
|
+
rootPath,
|
|
15120
|
+
dirPath,
|
|
15121
|
+
allowInRootLinks
|
|
15122
|
+
});
|
|
15123
|
+
if (reason === null) return false;
|
|
15124
|
+
logger.warn(`Refusing to delete files in ${quoteForLog(dirPath)}: ${reason}`);
|
|
14907
15125
|
return true;
|
|
14908
15126
|
}
|
|
14909
15127
|
/**
|
|
@@ -22614,6 +22832,9 @@ const commandsProcessorToolTargetsGlobal = allToolTargetKeys$3.filter((target) =
|
|
|
22614
22832
|
var CommandsProcessor = class extends FeatureProcessor {
|
|
22615
22833
|
toolTarget;
|
|
22616
22834
|
global;
|
|
22835
|
+
sweepsThroughInRootLinks() {
|
|
22836
|
+
return this.global;
|
|
22837
|
+
}
|
|
22617
22838
|
getFactory;
|
|
22618
22839
|
flattenedCommandNaming;
|
|
22619
22840
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$6, dryRun = false, flattenedCommandNaming = "basename", logger }) {
|
|
@@ -23185,6 +23406,15 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
23185
23406
|
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${stripControlCharacters(formatError(error))}`);
|
|
23186
23407
|
continue;
|
|
23187
23408
|
}
|
|
23409
|
+
const sweepRefusal = await sweepRefusalReason({
|
|
23410
|
+
rootPath: this.outputRoot,
|
|
23411
|
+
dirPath,
|
|
23412
|
+
allowInRootLinks: this.sweepsThroughInRootLinks()
|
|
23413
|
+
});
|
|
23414
|
+
if (sweepRefusal !== null) {
|
|
23415
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${sweepRefusal}`);
|
|
23416
|
+
continue;
|
|
23417
|
+
}
|
|
23188
23418
|
const generatedNames = /* @__PURE__ */ new Set();
|
|
23189
23419
|
const mainFile = aiDir.getMainFile();
|
|
23190
23420
|
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
@@ -25268,6 +25498,8 @@ const CRUSH_CONFIG_FILE_NAME = "crush.json";
|
|
|
25268
25498
|
const CRUSH_HIDDEN_CONFIG_FILE_NAME = ".crush.json";
|
|
25269
25499
|
const CRUSH_PERMISSIONS_KEY = "permissions";
|
|
25270
25500
|
const CRUSH_ALLOWED_TOOLS_KEY = "allowed_tools";
|
|
25501
|
+
const CRUSH_SAFE_COMMANDS_KEY = "safe_commands";
|
|
25502
|
+
const CRUSH_BLOCKED_COMMANDS_KEY = "blocked_commands";
|
|
25271
25503
|
const CRUSH_OPTIONS_KEY = "options";
|
|
25272
25504
|
const CRUSH_DISABLED_TOOLS_KEY = "disabled_tools";
|
|
25273
25505
|
const CRUSH_HOOKS_KEY = "hooks";
|
|
@@ -29100,11 +29332,7 @@ var ZoocodeSkill = class extends RooSkill {
|
|
|
29100
29332
|
static toolTarget = "zoocode";
|
|
29101
29333
|
};
|
|
29102
29334
|
// src/features/skills/skills-processor.ts
|
|
29103
|
-
var skills_processor_exports = /* @__PURE__ */ __exportAll({
|
|
29104
|
-
SkillsProcessor: () => SkillsProcessor,
|
|
29105
|
-
SkillsProcessorToolTargetSchema: () => SkillsProcessorToolTargetSchema,
|
|
29106
|
-
toolSkillFactories: () => toolSkillFactories
|
|
29107
|
-
});
|
|
29335
|
+
var skills_processor_exports = /* @__PURE__ */ __exportAll({ toolSkillFactories: () => toolSkillFactories });
|
|
29108
29336
|
const SkillsProcessorToolTargetSchema = z.enum(skillsProcessorToolTargetTuple);
|
|
29109
29337
|
/**
|
|
29110
29338
|
* Factory Map mapping tool targets to their skill factories.
|
|
@@ -29952,6 +30180,9 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29952
30180
|
global: this.global
|
|
29953
30181
|
});
|
|
29954
30182
|
}
|
|
30183
|
+
sweepsThroughInRootLinks() {
|
|
30184
|
+
return this.global;
|
|
30185
|
+
}
|
|
29955
30186
|
/**
|
|
29956
30187
|
* The tool's skills roots that exist on disk, each paired with its absolute
|
|
29957
30188
|
* path and vetted as writable inside this run's output root. Shared by the
|
|
@@ -29981,6 +30212,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29981
30212
|
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
29982
30213
|
continue;
|
|
29983
30214
|
}
|
|
30215
|
+
const sweepRefusal = await sweepRefusalReason({
|
|
30216
|
+
rootPath: this.outputRoot,
|
|
30217
|
+
dirPath: skillsDirPath,
|
|
30218
|
+
allowInRootLinks: this.sweepsThroughInRootLinks()
|
|
30219
|
+
});
|
|
30220
|
+
if (sweepRefusal !== null) {
|
|
30221
|
+
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${sweepRefusal}`);
|
|
30222
|
+
continue;
|
|
30223
|
+
}
|
|
29984
30224
|
existingRoots.push({
|
|
29985
30225
|
root,
|
|
29986
30226
|
skillsDirPath
|
|
@@ -33207,11 +33447,17 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33207
33447
|
*/
|
|
33208
33448
|
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
33209
33449
|
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
33210
|
-
const
|
|
33450
|
+
const agentsDirFullPath = join(outputRoot, agentsDirPath);
|
|
33451
|
+
const writeRefused = await writeRefusalReason({
|
|
33211
33452
|
rootPath: outputRoot,
|
|
33212
|
-
targetPath: join(
|
|
33213
|
-
});
|
|
33214
|
-
const
|
|
33453
|
+
targetPath: join(agentsDirFullPath, "agent.md")
|
|
33454
|
+
}) !== null;
|
|
33455
|
+
const sweepRefused = deleteOrphans && await sweepRefusalReason({
|
|
33456
|
+
rootPath: outputRoot,
|
|
33457
|
+
dirPath: agentsDirFullPath,
|
|
33458
|
+
allowInRootLinks: global
|
|
33459
|
+
}) !== null;
|
|
33460
|
+
const existingAgentFileNames = new Set(writeRefused ? [] : await listDirectoryEntryNames(agentsDirFullPath));
|
|
33215
33461
|
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
33216
33462
|
outputRoot,
|
|
33217
33463
|
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
@@ -33227,8 +33473,8 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33227
33473
|
global
|
|
33228
33474
|
})),
|
|
33229
33475
|
existingAgentFileNames,
|
|
33230
|
-
deleteOrphans,
|
|
33231
|
-
frozen:
|
|
33476
|
+
deleteOrphans: deleteOrphans && !sweepRefused,
|
|
33477
|
+
frozen: writeRefused,
|
|
33232
33478
|
global,
|
|
33233
33479
|
logger
|
|
33234
33480
|
});
|
|
@@ -37346,6 +37592,9 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
37346
37592
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
37347
37593
|
toolTarget;
|
|
37348
37594
|
global;
|
|
37595
|
+
sweepsThroughInRootLinks() {
|
|
37596
|
+
return this.global;
|
|
37597
|
+
}
|
|
37349
37598
|
deleteOrphans;
|
|
37350
37599
|
getFactory;
|
|
37351
37600
|
/**
|
|
@@ -38067,6 +38316,39 @@ function warnAndSkipMcpServer({ toolName, serverName, reason, logger }) {
|
|
|
38067
38316
|
return null;
|
|
38068
38317
|
}
|
|
38069
38318
|
/**
|
|
38319
|
+
* Drops every server whose `enabledTools` allowlist is empty, warning about
|
|
38320
|
+
* each one, for a tool that reads an empty (or missing) allowlist as "no
|
|
38321
|
+
* filter". Canonically an empty allowlist allows no tools at all, so emitting
|
|
38322
|
+
* the server unfiltered would expose every tool it reports. With
|
|
38323
|
+
* `ignoreBlankNames`, blank entries count as absent, for a tool that drops
|
|
38324
|
+
* them before applying the list. `productName` names the tool in the reason
|
|
38325
|
+
* when it differs from the `toolName` the warning starts with.
|
|
38326
|
+
*/
|
|
38327
|
+
function skipServersWithEmptyEnabledTools({ mcpServers, toolName, logger, ignoreBlankNames = false, productName = toolName }) {
|
|
38328
|
+
const kept = {};
|
|
38329
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
38330
|
+
const { enabledTools } = server;
|
|
38331
|
+
if (Array.isArray(enabledTools)) {
|
|
38332
|
+
if ((ignoreBlankNames ? enabledTools.filter((tool) => typeof tool !== "string" || tool.trim() !== "") : enabledTools).length === 0) {
|
|
38333
|
+
warnAndSkipMcpServer({
|
|
38334
|
+
toolName,
|
|
38335
|
+
serverName: name,
|
|
38336
|
+
reason: `an empty enabledTools list, which allows no tools at all but which ${productName} treats as no filter`,
|
|
38337
|
+
logger
|
|
38338
|
+
});
|
|
38339
|
+
continue;
|
|
38340
|
+
}
|
|
38341
|
+
}
|
|
38342
|
+
Object.defineProperty(kept, name, {
|
|
38343
|
+
value: server,
|
|
38344
|
+
enumerable: true,
|
|
38345
|
+
writable: true,
|
|
38346
|
+
configurable: true
|
|
38347
|
+
});
|
|
38348
|
+
}
|
|
38349
|
+
return kept;
|
|
38350
|
+
}
|
|
38351
|
+
/**
|
|
38070
38352
|
* The `tools` map Kilo and OpenCode keep beside `mcp` is keyed `<server>_<tool>`
|
|
38071
38353
|
* and reaches servers `mcp` does not list — ones the global config or a
|
|
38072
38354
|
* marketplace defines — so entries naming no listed
|
|
@@ -38628,7 +38910,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
38628
38910
|
const existingMcp = readExistingKiloMcpEntries(fileContent);
|
|
38629
38911
|
const parsedExisting = parse$1(fileContent || "{}");
|
|
38630
38912
|
const existingDocument = isRecord(parsedExisting) ? parsedExisting : {};
|
|
38631
|
-
const mcpServers =
|
|
38913
|
+
const mcpServers = skipServersWithEmptyEnabledTools({
|
|
38914
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
38915
|
+
toolName: "Kilo",
|
|
38916
|
+
logger
|
|
38917
|
+
});
|
|
38632
38918
|
const { mcp: convertedMcp, toolPermissions } = convertToKiloFormat(resolveKiloEnvVarRefs({
|
|
38633
38919
|
mcpServers,
|
|
38634
38920
|
global,
|
|
@@ -38757,7 +39043,8 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
|
38757
39043
|
"timeout",
|
|
38758
39044
|
"oauth",
|
|
38759
39045
|
"protocol",
|
|
38760
|
-
"codemode"
|
|
39046
|
+
"codemode",
|
|
39047
|
+
"pool"
|
|
38761
39048
|
];
|
|
38762
39049
|
const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
38763
39050
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
@@ -38920,7 +39207,7 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
|
38920
39207
|
* - env -> environment
|
|
38921
39208
|
* - disabled -> enabled (inverted)
|
|
38922
39209
|
* - enabledTools/disabledTools -> top-level tools map (with server name prefix)
|
|
38923
|
-
* - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
|
|
39210
|
+
* - OpenCode-supported extras (timeout, oauth, protocol, codemode, pool) -> passed through verbatim
|
|
38924
39211
|
*/
|
|
38925
39212
|
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, configShape, logger) {
|
|
38926
39213
|
const serverRecord = serverConfig;
|
|
@@ -39160,7 +39447,11 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
39160
39447
|
if (fileContent) relativeFilePath = this.layout.jsonFileName;
|
|
39161
39448
|
}
|
|
39162
39449
|
const transformedServers = convertEnvVarRefsToToolFormat({
|
|
39163
|
-
mcpServers:
|
|
39450
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
39451
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
39452
|
+
toolName: "OpenCode",
|
|
39453
|
+
logger
|
|
39454
|
+
}),
|
|
39164
39455
|
replacement: "{env:$1}"
|
|
39165
39456
|
});
|
|
39166
39457
|
const existingJson = parse$1(fileContent || "{}");
|
|
@@ -48756,6 +49047,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
48756
49047
|
language;
|
|
48757
49048
|
deriveSubprojectPathFromGlobs;
|
|
48758
49049
|
global;
|
|
49050
|
+
sweepsThroughInRootLinks() {
|
|
49051
|
+
return this.global;
|
|
49052
|
+
}
|
|
48759
49053
|
getFactory;
|
|
48760
49054
|
skills;
|
|
48761
49055
|
featureOptions;
|
|
@@ -51340,12 +51634,29 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
51340
51634
|
};
|
|
51341
51635
|
}
|
|
51342
51636
|
}
|
|
51637
|
+
/**
|
|
51638
|
+
* The `.rulesync/checks/<stem>.md` name an imported spec gets. The slug is
|
|
51639
|
+
* read from a spec file that may come from a fetched or cloned repository,
|
|
51640
|
+
* so it is kept as written only when it is a single plain file name; a slug
|
|
51641
|
+
* such as `../../docs/OPERATOR` would otherwise move the write out of
|
|
51642
|
+
* `.rulesync/checks/`. Anything else is slugified, and an empty result falls
|
|
51643
|
+
* back to the spec file's own name.
|
|
51644
|
+
*/
|
|
51645
|
+
importedCheckStem(slug) {
|
|
51646
|
+
const isPlainFileName = (name) => name !== "" && name !== "." && name !== ".." && !/[/\\<>:"|?*]/.test(name) && !hasWindowsFoldableName(name) && stripControlCharacters(name) === name;
|
|
51647
|
+
if (isPlainFileName(slug)) return slug;
|
|
51648
|
+
for (const candidate of [slugifyCheckName(slug), slugifyCheckName(basename(this.relativeFilePath, ".json"))]) {
|
|
51649
|
+
if (candidate === "") continue;
|
|
51650
|
+
return isPlainFileName(candidate) ? candidate : `${candidate}-check`;
|
|
51651
|
+
}
|
|
51652
|
+
return "check";
|
|
51653
|
+
}
|
|
51343
51654
|
toRulesyncCheck() {
|
|
51344
51655
|
const spec = HermesCheckSpecSchema.parse(JSON.parse(this.fileContent));
|
|
51345
51656
|
return new RulesyncCheck({
|
|
51346
51657
|
outputRoot: ".",
|
|
51347
51658
|
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
51348
|
-
relativeFilePath: `${spec.slug}.md`,
|
|
51659
|
+
relativeFilePath: `${this.importedCheckStem(spec.slug)}.md`,
|
|
51349
51660
|
frontmatter: {
|
|
51350
51661
|
targets: ["*"],
|
|
51351
51662
|
description: spec.description,
|
|
@@ -51853,6 +52164,9 @@ const checksProcessorToolTargetsGlobal = allToolTargetKeys.filter((target) => {
|
|
|
51853
52164
|
var ChecksProcessor = class extends FeatureProcessor {
|
|
51854
52165
|
toolTarget;
|
|
51855
52166
|
global;
|
|
52167
|
+
sweepsThroughInRootLinks() {
|
|
52168
|
+
return this.global;
|
|
52169
|
+
}
|
|
51856
52170
|
getFactory;
|
|
51857
52171
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
51858
52172
|
super({
|
|
@@ -54164,6 +54478,10 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
|
54164
54478
|
field: "tool",
|
|
54165
54479
|
value: h.tool
|
|
54166
54480
|
});
|
|
54481
|
+
for (const { canonical, tool } of converterConfig.stringPassthroughFields ?? []) if (canonical === "if" && h[tool] !== "") fields.push({
|
|
54482
|
+
field: "if",
|
|
54483
|
+
value: h[tool]
|
|
54484
|
+
});
|
|
54167
54485
|
return fields;
|
|
54168
54486
|
}
|
|
54169
54487
|
/**
|
|
@@ -62940,10 +63258,12 @@ function emittedEventsWithHook(params) {
|
|
|
62940
63258
|
function converterCapabilities(config) {
|
|
62941
63259
|
if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
|
|
62942
63260
|
const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
|
|
63261
|
+
const passesThroughIf = (config.stringPassthroughFields ?? []).some((field) => field.canonical === "if") && !(config.skippedWhenFields ?? []).includes("if");
|
|
62943
63262
|
return {
|
|
62944
63263
|
supportedEvents: config.supportedEvents,
|
|
62945
63264
|
supportedHookTypes: [...config.supportedHookTypes],
|
|
62946
|
-
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
|
|
63265
|
+
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {},
|
|
63266
|
+
...passesThroughIf ? { supportsPerHookIf: true } : {}
|
|
62947
63267
|
};
|
|
62948
63268
|
}
|
|
62949
63269
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
@@ -63588,6 +63908,9 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
63588
63908
|
var HooksProcessor = class extends FeatureProcessor {
|
|
63589
63909
|
toolTarget;
|
|
63590
63910
|
global;
|
|
63911
|
+
sweepsThroughInRootLinks() {
|
|
63912
|
+
return this.global;
|
|
63913
|
+
}
|
|
63591
63914
|
preserveUnownedHooks;
|
|
63592
63915
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, preserveUnownedHooks = false, logger }) {
|
|
63593
63916
|
super({
|
|
@@ -63666,7 +63989,8 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63666
63989
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
63667
63990
|
const config = rulesyncHooks.getJson();
|
|
63668
63991
|
const sharedHooks = config.hooks;
|
|
63669
|
-
const
|
|
63992
|
+
const overrideKey = HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget;
|
|
63993
|
+
const overrideHooks = config[overrideKey]?.hooks ?? {};
|
|
63670
63994
|
const effectiveHooks = {
|
|
63671
63995
|
...sharedHooks,
|
|
63672
63996
|
...overrideHooks
|
|
@@ -63704,7 +64028,10 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63704
64028
|
if (eventsWithUnsupportedMatcher.length > 0) this.logger.warn(`Skipped matcher hook(s) for ${this.toolTarget} (not supported): ${eventsWithUnsupportedMatcher.join(", ")}`);
|
|
63705
64029
|
const toolHooks = await factory.class.fromRulesyncHooks({
|
|
63706
64030
|
outputRoot: this.outputRoot,
|
|
63707
|
-
rulesyncHooks
|
|
64031
|
+
rulesyncHooks: factory.supportsPerHookIf ? rulesyncHooks : this.withoutConditionalHooks({
|
|
64032
|
+
rulesyncHooks,
|
|
64033
|
+
overrideKey
|
|
64034
|
+
}),
|
|
63708
64035
|
validate: true,
|
|
63709
64036
|
global: this.global,
|
|
63710
64037
|
preserveUnowned: this.preserveUnownedHooks,
|
|
@@ -63727,6 +64054,53 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63727
64054
|
return result;
|
|
63728
64055
|
}
|
|
63729
64056
|
/**
|
|
64057
|
+
* Returns the hooks source without the definitions that carry a per-hook
|
|
64058
|
+
* `if` condition, warning about each one. Used for targets that cannot
|
|
64059
|
+
* express `if`: emitting such a hook without its condition would make it
|
|
64060
|
+
* fire on every call its matcher selects, so a permission-granting hook
|
|
64061
|
+
* scoped by `if` would approve far more than its author meant.
|
|
64062
|
+
*/
|
|
64063
|
+
withoutConditionalHooks(params) {
|
|
64064
|
+
const { rulesyncHooks, overrideKey } = params;
|
|
64065
|
+
const config = rulesyncHooks.getJson();
|
|
64066
|
+
const skipped = [];
|
|
64067
|
+
const filterHooks = (hooks) => {
|
|
64068
|
+
if (!isPlainObject(hooks)) return hooks;
|
|
64069
|
+
const filtered = {};
|
|
64070
|
+
for (const [event, defs] of Object.entries(hooks)) {
|
|
64071
|
+
if (!Array.isArray(defs)) {
|
|
64072
|
+
filtered[event] = defs;
|
|
64073
|
+
continue;
|
|
64074
|
+
}
|
|
64075
|
+
filtered[event] = defs.filter((def) => {
|
|
64076
|
+
const condition = isPlainObject(def) ? def.if : void 0;
|
|
64077
|
+
if (typeof condition !== "string" || condition.trim() === "") return true;
|
|
64078
|
+
skipped.push(`${event} (${quoteValueForWarning(condition)})`);
|
|
64079
|
+
return false;
|
|
64080
|
+
});
|
|
64081
|
+
}
|
|
64082
|
+
return filtered;
|
|
64083
|
+
};
|
|
64084
|
+
const filteredConfig = {
|
|
64085
|
+
...config,
|
|
64086
|
+
hooks: filterHooks(config.hooks)
|
|
64087
|
+
};
|
|
64088
|
+
const override = config[overrideKey];
|
|
64089
|
+
if (Object.hasOwn(config, overrideKey) && isPlainObject(override)) filteredConfig[overrideKey] = {
|
|
64090
|
+
...override,
|
|
64091
|
+
hooks: filterHooks(override.hooks)
|
|
64092
|
+
};
|
|
64093
|
+
if (skipped.length === 0) return rulesyncHooks;
|
|
64094
|
+
this.logger.warn(`Skipped hook(s) with an "if" condition for ${this.toolTarget} (only the ${targetsSupporting("supportsPerHookIf")} hooks formats support it, and without it the hook would run on every call its matcher selects): ${skipped.join(", ")}`);
|
|
64095
|
+
return new RulesyncHooks({
|
|
64096
|
+
outputRoot: rulesyncHooks.getOutputRoot(),
|
|
64097
|
+
relativeDirPath: rulesyncHooks.getRelativeDirPath(),
|
|
64098
|
+
relativeFilePath: rulesyncHooks.getRelativeFilePath(),
|
|
64099
|
+
fileContent: JSON.stringify(filteredConfig),
|
|
64100
|
+
validate: false
|
|
64101
|
+
});
|
|
64102
|
+
}
|
|
64103
|
+
/**
|
|
63730
64104
|
* Warns about canonical per-hook fields that this target's hooks format
|
|
63731
64105
|
* cannot express, so a hook does not silently change behavior on generation.
|
|
63732
64106
|
* Only canonical definitions are considered: a tool-native field inside an
|
|
@@ -63971,6 +64345,35 @@ var BobIgnore = class extends PlainToolIgnore {
|
|
|
63971
64345
|
};
|
|
63972
64346
|
}
|
|
63973
64347
|
};
|
|
64348
|
+
// src/features/ignore/claude-read-deny-pattern.ts
|
|
64349
|
+
/**
|
|
64350
|
+
* Translate the path of a Claude Code style `Read(<path>)` deny into the
|
|
64351
|
+
* gitignore syntax of `.rulesync/.aiignore`.
|
|
64352
|
+
*
|
|
64353
|
+
* Claude Code reads the path forms differently from gitignore: `./path` is
|
|
64354
|
+
* relative to the current directory, `/path` to the project root, `//path` is
|
|
64355
|
+
* absolute and `~/path` is under the home directory. Copied verbatim, the
|
|
64356
|
+
* common `Read(./.env)` idiom would become `./.env`, which matches nothing in
|
|
64357
|
+
* a gitignore-syntax ignore file, so every other tool would lose the deny.
|
|
64358
|
+
*
|
|
64359
|
+
* - `./path` is anchored to the project root as `/path`.
|
|
64360
|
+
* - `//path` and `~/path` (or a bare `~`) name locations a project-relative
|
|
64361
|
+
* ignore file cannot express; they are skipped with a warning and `null` is
|
|
64362
|
+
* returned.
|
|
64363
|
+
* - Every other path (`.env`, `secrets/**`, `/path`) already means the same
|
|
64364
|
+
* project files in gitignore syntax and is kept as is.
|
|
64365
|
+
*/
|
|
64366
|
+
function claudeReadDenyPathToIgnorePattern({ path, toolLabel, logger }) {
|
|
64367
|
+
if (path.startsWith("//") || path === "~" || path.startsWith("~/")) {
|
|
64368
|
+
warnWithFallback(logger, `Skipping ${toolLabel} read deny ${quoteValueForWarning(path)} on ignore import: an absolute or home-directory path cannot be expressed in the project-relative .rulesync/.aiignore.`);
|
|
64369
|
+
return null;
|
|
64370
|
+
}
|
|
64371
|
+
if (path.startsWith("./")) {
|
|
64372
|
+
const rest = path.slice(2).replace(/^(?:\.\/)+/, "");
|
|
64373
|
+
return rest.length > 0 ? `/${rest}` : null;
|
|
64374
|
+
}
|
|
64375
|
+
return path;
|
|
64376
|
+
}
|
|
63974
64377
|
// src/features/ignore/claudecode-ignore.ts
|
|
63975
64378
|
const DEFAULT_FILE_MODE = "shared";
|
|
63976
64379
|
/**
|
|
@@ -64029,9 +64432,12 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
64029
64432
|
}
|
|
64030
64433
|
toRulesyncIgnore() {
|
|
64031
64434
|
const fileContent = this.patterns.map((pattern) => {
|
|
64032
|
-
if (isReadDenyEntry(pattern)) return
|
|
64435
|
+
if (isReadDenyEntry(pattern)) return claudeReadDenyPathToIgnorePattern({
|
|
64436
|
+
path: pattern.slice(5, -1),
|
|
64437
|
+
toolLabel: "Claude Code"
|
|
64438
|
+
});
|
|
64033
64439
|
return pattern;
|
|
64034
|
-
}).filter((pattern) => pattern.length > 0).join("\n");
|
|
64440
|
+
}).filter((pattern) => pattern !== null && pattern.length > 0).join("\n");
|
|
64035
64441
|
return new RulesyncIgnore({
|
|
64036
64442
|
outputRoot: this.outputRoot,
|
|
64037
64443
|
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
@@ -64306,7 +64712,7 @@ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
|
64306
64712
|
const paths = this.getSettablePaths({ global });
|
|
64307
64713
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
64308
64714
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
64309
|
-
const managedPatterns =
|
|
64715
|
+
const managedPatterns = patterns.filter((pattern, index) => patterns.lastIndexOf(pattern) === index);
|
|
64310
64716
|
return new FirebenderIgnore({
|
|
64311
64717
|
outputRoot,
|
|
64312
64718
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -64961,7 +65367,10 @@ const asClaudeStyleSettings = (document) => {
|
|
|
64961
65367
|
};
|
|
64962
65368
|
/** The pattern of a `read_file(...)` (or legacy `Read(...)`) deny, else null. */
|
|
64963
65369
|
const readDenyPatternOf = (entry) => {
|
|
64964
|
-
for (const toolName of [REASONIX_READ_TOOL_NAME, REASONIX_LEGACY_READ_TOOL_NAME]) if (isReadDenyEntry(entry, toolName)) return
|
|
65370
|
+
for (const toolName of [REASONIX_READ_TOOL_NAME, REASONIX_LEGACY_READ_TOOL_NAME]) if (isReadDenyEntry(entry, toolName)) return claudeReadDenyPathToIgnorePattern({
|
|
65371
|
+
path: entry.slice(toolName.length + 1, -1),
|
|
65372
|
+
toolLabel: "Reasonix"
|
|
65373
|
+
});
|
|
64965
65374
|
return null;
|
|
64966
65375
|
};
|
|
64967
65376
|
/**
|
|
@@ -65301,6 +65710,9 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
65301
65710
|
getFactory;
|
|
65302
65711
|
featureOptions;
|
|
65303
65712
|
global;
|
|
65713
|
+
sweepsThroughInRootLinks() {
|
|
65714
|
+
return this.global;
|
|
65715
|
+
}
|
|
65304
65716
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$1, global = false, dryRun = false, logger, featureOptions }) {
|
|
65305
65717
|
super({
|
|
65306
65718
|
outputRoot,
|
|
@@ -65634,30 +66046,71 @@ function isEmptyValue(value) {
|
|
|
65634
66046
|
if (Array.isArray(value)) return value.length === 0;
|
|
65635
66047
|
return isPlainObject(value) && Object.keys(value).length === 0;
|
|
65636
66048
|
}
|
|
66049
|
+
const MILLISECONDS_PER_SECOND$4 = 1e3;
|
|
66050
|
+
/**
|
|
66051
|
+
* Convert the canonical `timeout` (milliseconds) into Antigravity's
|
|
66052
|
+
* `timeoutSeconds` (integer seconds), rounding up so a sub-second timeout does
|
|
66053
|
+
* not become `0`. A negative value disables the timeout upstream, so it is
|
|
66054
|
+
* written as `-1`. An authored native `timeoutSeconds` wins over `timeout`.
|
|
66055
|
+
*/
|
|
66056
|
+
function convertTimeoutToAntigravity({ serverName, config, logger }) {
|
|
66057
|
+
if (!("timeout" in config)) return config;
|
|
66058
|
+
const { timeout, ...rest } = config;
|
|
66059
|
+
if (rest.timeoutSeconds !== void 0) {
|
|
66060
|
+
logger?.warn(`Antigravity MCP: ${quoteValueForWarning(serverName)} sets both "timeout" and "timeoutSeconds"; keeping "timeoutSeconds" and dropping "timeout".`);
|
|
66061
|
+
return rest;
|
|
66062
|
+
}
|
|
66063
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
|
|
66064
|
+
logger?.warn(`Antigravity MCP: dropping the "timeout" of ${quoteValueForWarning(serverName)}: expected a number of milliseconds.`);
|
|
66065
|
+
return rest;
|
|
66066
|
+
}
|
|
66067
|
+
const timeoutSeconds = timeout < 0 ? -1 : Math.ceil(timeout / MILLISECONDS_PER_SECOND$4);
|
|
66068
|
+
return Object.fromEntries(Object.entries(config).map(([key, value]) => key === "timeout" ? ["timeoutSeconds", timeoutSeconds] : [key, value]));
|
|
66069
|
+
}
|
|
65637
66070
|
/**
|
|
65638
|
-
* Antigravity uses `serverUrl` (not the canonical `url`) for
|
|
66071
|
+
* Antigravity uses `serverUrl` (not the canonical `url`) for remote MCP
|
|
65639
66072
|
* servers. Convert the canonical shape into Antigravity's on the way out, and
|
|
65640
66073
|
* back to canonical on the way in.
|
|
65641
66074
|
*
|
|
65642
66075
|
* Antigravity documents no `type` field: the transport follows from `command`
|
|
65643
|
-
* (stdio) or `serverUrl` (Streamable HTTP
|
|
66076
|
+
* (stdio) or `serverUrl` (Streamable HTTP), and `agy plugin validate`
|
|
65644
66077
|
* reports the canonical `type` as an unknown field, so it is not written. A
|
|
65645
66078
|
* remote server that carries `command`, `args` or `env` was also observed to be
|
|
65646
66079
|
* refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
|
|
65647
66080
|
* not documented), so an empty `args` / `env` left on a `serverUrl` server is
|
|
65648
66081
|
* dropped.
|
|
66082
|
+
*
|
|
66083
|
+
* Remote servers must use Streamable HTTP: the legacy HTTP+SSE transport is
|
|
66084
|
+
* not supported, so a canonical `sse` server is still written but warned
|
|
66085
|
+
* about. The canonical `timeout` (milliseconds) becomes `timeoutSeconds`.
|
|
65649
66086
|
* https://antigravity.google/docs/mcp
|
|
65650
66087
|
*/
|
|
65651
|
-
function toAntigravityMcpServers(servers) {
|
|
66088
|
+
function toAntigravityMcpServers(servers, logger) {
|
|
65652
66089
|
const renamed = renameServerField(servers, "url", "serverUrl");
|
|
65653
66090
|
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
65654
66091
|
if (!isPlainObject(config)) return [name, config];
|
|
66092
|
+
if (config.type === "sse" || config.transport === "sse") logger?.warn(`Antigravity MCP: ${quoteValueForWarning(name)} uses the SSE transport, which Antigravity does not support (remote servers must use Streamable HTTP); it is written as "serverUrl" and may fail to connect.`);
|
|
65655
66093
|
const isRemote = typeof config.serverUrl === "string";
|
|
65656
|
-
return [name,
|
|
66094
|
+
return [name, convertTimeoutToAntigravity({
|
|
66095
|
+
serverName: name,
|
|
66096
|
+
config: Object.fromEntries(Object.entries(config).filter(([key, value]) => key !== "type" && !(isRemote && (key === "args" || key === "env") && isEmptyValue(value)))),
|
|
66097
|
+
logger
|
|
66098
|
+
})];
|
|
65657
66099
|
}));
|
|
65658
66100
|
}
|
|
66101
|
+
/**
|
|
66102
|
+
* Convert Antigravity servers back to the canonical shape: `serverUrl` becomes
|
|
66103
|
+
* `url`, and `timeoutSeconds` becomes the canonical `timeout` in milliseconds
|
|
66104
|
+
* (a negative value, which disables the timeout upstream, stays `-1`).
|
|
66105
|
+
*/
|
|
65659
66106
|
function toCanonicalMcpServers(servers) {
|
|
65660
|
-
|
|
66107
|
+
const renamed = renameServerField(servers, "serverUrl", "url");
|
|
66108
|
+
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
66109
|
+
if (!isPlainObject(config) || typeof config.timeoutSeconds !== "number") return [name, config];
|
|
66110
|
+
const timeoutSeconds = config.timeoutSeconds;
|
|
66111
|
+
const timeout = timeoutSeconds < 0 ? -1 : timeoutSeconds * MILLISECONDS_PER_SECOND$4;
|
|
66112
|
+
return [name, Object.fromEntries(Object.entries(config).filter(([key]) => key !== "timeout").map(([key, value]) => key === "timeoutSeconds" ? ["timeout", timeout] : [key, value]))];
|
|
66113
|
+
}));
|
|
65661
66114
|
}
|
|
65662
66115
|
/**
|
|
65663
66116
|
* Parse `mcp_config.json` as JSONC and require a top-level object, so a file
|
|
@@ -65675,7 +66128,8 @@ function parseAntigravityMcpConfig(fileContent) {
|
|
|
65675
66128
|
* the IDE and the CLI.
|
|
65676
66129
|
*
|
|
65677
66130
|
* Antigravity writes a dedicated `mcp_config.json` whose `mcpServers` map lives
|
|
65678
|
-
* at the top level (`disabledTools` per server
|
|
66131
|
+
* at the top level (`enabledTools` / `disabledTools` per server are supported
|
|
66132
|
+
* under the same names). Unlike Gemini
|
|
65679
66133
|
* CLI's shared `settings.json`, this file is dedicated to MCP, so it is safely
|
|
65680
66134
|
* deletable. HTTP servers use `serverUrl` rather than the canonical `url`, so
|
|
65681
66135
|
* the field is translated in both directions.
|
|
@@ -65728,11 +66182,11 @@ var AntigravityMcp = class extends ToolMcp {
|
|
|
65728
66182
|
global
|
|
65729
66183
|
});
|
|
65730
66184
|
}
|
|
65731
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
66185
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
65732
66186
|
const paths = this.getSettablePaths({ global });
|
|
65733
66187
|
const newJson = {
|
|
65734
66188
|
...parseAntigravityMcpConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2)),
|
|
65735
|
-
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers())
|
|
66189
|
+
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers(), logger)
|
|
65736
66190
|
};
|
|
65737
66191
|
return new this({
|
|
65738
66192
|
outputRoot,
|
|
@@ -69401,7 +69855,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
69401
69855
|
ownedPaths: [["mcp"]],
|
|
69402
69856
|
logger
|
|
69403
69857
|
});
|
|
69404
|
-
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(
|
|
69858
|
+
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(skipServersWithEmptyEnabledTools({
|
|
69859
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
69860
|
+
toolName: "Crush",
|
|
69861
|
+
logger
|
|
69862
|
+
})).map(([serverName, serverConfig]) => [serverName, {
|
|
69405
69863
|
...serverConfig,
|
|
69406
69864
|
...readCrushOnlyKeys({
|
|
69407
69865
|
serverName,
|
|
@@ -70361,31 +70819,6 @@ var DshMcp = class DshMcp extends ToolMcp {
|
|
|
70361
70819
|
};
|
|
70362
70820
|
// src/features/mcp/factorydroid-mcp.ts
|
|
70363
70821
|
const TOOL_NAME = "factorydroid";
|
|
70364
|
-
/**
|
|
70365
|
-
* Drop servers whose `enabledTools` allowlist is empty.
|
|
70366
|
-
*
|
|
70367
|
-
* Canonically an empty allowlist allows no tools, but Droid applies
|
|
70368
|
-
* `enabledTools` only "when the list is non-empty"
|
|
70369
|
-
* (https://docs.factory.com/harness/mcp), so writing `[]` would load every
|
|
70370
|
-
* tool the server reports. Skipping the server is the only form that does not
|
|
70371
|
-
* hand the model more tools than the canonical config allows.
|
|
70372
|
-
*/
|
|
70373
|
-
function skipEmptyAllowlistServers({ mcpServers, logger }) {
|
|
70374
|
-
const kept = {};
|
|
70375
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
70376
|
-
if (server.enabledTools?.length === 0) {
|
|
70377
|
-
warnAndSkipMcpServer({
|
|
70378
|
-
toolName: TOOL_NAME,
|
|
70379
|
-
serverName: name,
|
|
70380
|
-
reason: "an empty enabledTools list, which allows no tools at all but which Factory Droid treats as no filter",
|
|
70381
|
-
logger
|
|
70382
|
-
});
|
|
70383
|
-
continue;
|
|
70384
|
-
}
|
|
70385
|
-
kept[name] = server;
|
|
70386
|
-
}
|
|
70387
|
-
return kept;
|
|
70388
|
-
}
|
|
70389
70822
|
var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
70390
70823
|
json;
|
|
70391
70824
|
constructor(params) {
|
|
@@ -70412,8 +70845,10 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
70412
70845
|
});
|
|
70413
70846
|
}
|
|
70414
70847
|
static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger }) {
|
|
70415
|
-
const factorydroidConfig = { mcpServers:
|
|
70848
|
+
const factorydroidConfig = { mcpServers: skipServersWithEmptyEnabledTools({
|
|
70416
70849
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
70850
|
+
toolName: TOOL_NAME,
|
|
70851
|
+
productName: "Factory Droid",
|
|
70417
70852
|
logger
|
|
70418
70853
|
}) };
|
|
70419
70854
|
const fileContent = JSON.stringify(factorydroidConfig, null, 2);
|
|
@@ -71844,19 +72279,42 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
|
|
|
71844
72279
|
return hasCommand ? "stdio" : "http";
|
|
71845
72280
|
}
|
|
71846
72281
|
/**
|
|
71847
|
-
* The only `executor` value Kimi Code accepts on a stdio
|
|
71848
|
-
* the never-implemented `kaos` option and narrowed the
|
|
71849
|
-
* `z.enum(['local'])`, so
|
|
71850
|
-
*
|
|
72282
|
+
* The only `executor` value every current Kimi Code release accepts on a stdio
|
|
72283
|
+
* server. 2.2.0 removed the never-implemented `kaos` option and narrowed the
|
|
72284
|
+
* schema to `z.enum(['local'])`, so 2.2.0 through 2.4.x fail validation on any
|
|
72285
|
+
* other value when they load `mcp.json`. 2.5.0 widened the enum back to
|
|
72286
|
+
* `'local' | 'kaos'`, but `kaos` is still undocumented and would break the
|
|
72287
|
+
* older releases, so it keeps being dropped.
|
|
71851
72288
|
* @see https://github.com/MoonshotAI/kimi-code/pull/4184
|
|
72289
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4062
|
|
71852
72290
|
*/
|
|
71853
72291
|
const KIMI_CODE_MCP_EXECUTOR = "local";
|
|
71854
72292
|
/** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
|
|
71855
72293
|
function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
|
|
71856
72294
|
if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
|
|
71857
|
-
warnWithFallback(logger, `Kimi Code MCP: dropping executor ${JSON.stringify(server.executor)} from "${name}" because Kimi Code only
|
|
72295
|
+
warnWithFallback(logger, `Kimi Code MCP: dropping executor ${JSON.stringify(server.executor)} from "${name}" because Kimi Code 2.2.0 through 2.4.x accept only "${KIMI_CODE_MCP_EXECUTOR}".`);
|
|
71858
72296
|
delete server.executor;
|
|
71859
72297
|
}
|
|
72298
|
+
/**
|
|
72299
|
+
* Remove, with a warning, an `environment_id` Kimi Code cannot use. Kimi Code
|
|
72300
|
+
* 2.5.0 added it to stdio servers only, as a non-empty string naming an
|
|
72301
|
+
* `[environments.<id>]` entry of `config.toml`; an empty or non-string value
|
|
72302
|
+
* fails Kimi's validation. Mutates `server`.
|
|
72303
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4062
|
|
72304
|
+
*/
|
|
72305
|
+
function dropUnsupportedKimiCodeEnvironmentId({ name, server, isStdio, logger }) {
|
|
72306
|
+
const environmentId = server.environment_id;
|
|
72307
|
+
if (environmentId === void 0) return;
|
|
72308
|
+
if (!isStdio) {
|
|
72309
|
+
warnWithFallback(logger, `Kimi Code MCP: dropping environment_id from "${name}" because Kimi Code only accepts it on stdio servers.`);
|
|
72310
|
+
delete server.environment_id;
|
|
72311
|
+
return;
|
|
72312
|
+
}
|
|
72313
|
+
if (typeof environmentId !== "string" || environmentId.length === 0) {
|
|
72314
|
+
warnWithFallback(logger, `Kimi Code MCP: dropping environment_id ${JSON.stringify(environmentId)} from "${name}" because Kimi Code requires a non-empty string.`);
|
|
72315
|
+
delete server.environment_id;
|
|
72316
|
+
}
|
|
72317
|
+
}
|
|
71860
72318
|
function toKimiCodeServer({ name, server, logger }) {
|
|
71861
72319
|
const transport = server.transport ?? server.type;
|
|
71862
72320
|
if (transport === "ws") {
|
|
@@ -71884,6 +72342,7 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71884
72342
|
for (const field of [
|
|
71885
72343
|
"env",
|
|
71886
72344
|
"cwd",
|
|
72345
|
+
"environment_id",
|
|
71887
72346
|
"executor",
|
|
71888
72347
|
"runtime_id",
|
|
71889
72348
|
"headers",
|
|
@@ -71900,6 +72359,12 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71900
72359
|
server: converted,
|
|
71901
72360
|
logger
|
|
71902
72361
|
});
|
|
72362
|
+
dropUnsupportedKimiCodeEnvironmentId({
|
|
72363
|
+
name,
|
|
72364
|
+
server: converted,
|
|
72365
|
+
isStdio: normalizedTransport === "stdio",
|
|
72366
|
+
logger
|
|
72367
|
+
});
|
|
71903
72368
|
if (server.disabled === true) converted.enabled = false;
|
|
71904
72369
|
return converted;
|
|
71905
72370
|
}
|
|
@@ -71924,6 +72389,15 @@ function fromKimiCodeServers({ servers, logger }) {
|
|
|
71924
72389
|
logger
|
|
71925
72390
|
});
|
|
71926
72391
|
const type = transport === "stdio" || transport === "sse" || transport === "http" ? transport : void 0;
|
|
72392
|
+
dropUnsupportedKimiCodeEnvironmentId({
|
|
72393
|
+
name,
|
|
72394
|
+
server: rest,
|
|
72395
|
+
isStdio: normalizeKimiCodeTransport({
|
|
72396
|
+
transport,
|
|
72397
|
+
hasCommand: rest.command !== void 0
|
|
72398
|
+
}) === "stdio",
|
|
72399
|
+
logger
|
|
72400
|
+
});
|
|
71927
72401
|
return [name, {
|
|
71928
72402
|
...rest,
|
|
71929
72403
|
...type && { type },
|
|
@@ -73092,7 +73566,12 @@ var OpenclawMcp = class OpenclawMcp extends ToolMcp {
|
|
|
73092
73566
|
const existingMcp = isRecord(existing["mcp"]) ? existing["mcp"] : {};
|
|
73093
73567
|
const existingServers = isRecord(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
|
|
73094
73568
|
const converted = convertToOpenclawFormat({
|
|
73095
|
-
mcpServers:
|
|
73569
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
73570
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
73571
|
+
toolName: "OpenClaw",
|
|
73572
|
+
ignoreBlankNames: true,
|
|
73573
|
+
logger
|
|
73574
|
+
}),
|
|
73096
73575
|
logger
|
|
73097
73576
|
});
|
|
73098
73577
|
const servers = {
|
|
@@ -73720,7 +74199,11 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
73720
74199
|
const paths = this.getSettablePaths({ global });
|
|
73721
74200
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
73722
74201
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
73723
|
-
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(
|
|
74202
|
+
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(skipServersWithEmptyEnabledTools({
|
|
74203
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
74204
|
+
toolName: "Pool",
|
|
74205
|
+
logger
|
|
74206
|
+
})).map(([serverName, serverConfig]) => {
|
|
73724
74207
|
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
73725
74208
|
const poolAllow = isRecord(rawServer) ? rawServer.poolAllow : void 0;
|
|
73726
74209
|
return [serverName, {
|
|
@@ -77246,7 +77729,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77246
77729
|
meta: {
|
|
77247
77730
|
supportsProject: true,
|
|
77248
77731
|
supportsGlobal: true,
|
|
77249
|
-
supportsEnabledTools:
|
|
77732
|
+
supportsEnabledTools: true,
|
|
77250
77733
|
supportsDisabledTools: true
|
|
77251
77734
|
}
|
|
77252
77735
|
}],
|
|
@@ -77255,7 +77738,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77255
77738
|
meta: {
|
|
77256
77739
|
supportsProject: true,
|
|
77257
77740
|
supportsGlobal: true,
|
|
77258
|
-
supportsEnabledTools:
|
|
77741
|
+
supportsEnabledTools: true,
|
|
77259
77742
|
supportsDisabledTools: true
|
|
77260
77743
|
}
|
|
77261
77744
|
}],
|
|
@@ -77264,7 +77747,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77264
77747
|
meta: {
|
|
77265
77748
|
supportsProject: true,
|
|
77266
77749
|
supportsGlobal: false,
|
|
77267
|
-
supportsEnabledTools:
|
|
77750
|
+
supportsEnabledTools: true,
|
|
77268
77751
|
supportsDisabledTools: true
|
|
77269
77752
|
}
|
|
77270
77753
|
}],
|
|
@@ -77831,6 +78314,9 @@ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger })
|
|
|
77831
78314
|
var McpProcessor = class extends FeatureProcessor {
|
|
77832
78315
|
toolTarget;
|
|
77833
78316
|
global;
|
|
78317
|
+
sweepsThroughInRootLinks() {
|
|
78318
|
+
return this.global;
|
|
78319
|
+
}
|
|
77834
78320
|
getFactory;
|
|
77835
78321
|
featureOptions;
|
|
77836
78322
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory, dryRun = false, logger, featureOptions }) {
|
|
@@ -79883,6 +80369,39 @@ const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
|
|
|
79883
80369
|
]);
|
|
79884
80370
|
const DIRECTORY_CONTENTS_SUFFIX = /\/\*\*(?:\/\*)?$/;
|
|
79885
80371
|
const HOME_OR_VARIABLE_PATH = /^~(?:\/|$)|^\$[A-Za-z_]|\$\{[A-Za-z_]/;
|
|
80372
|
+
const LEADING_HOME = /^~(?:\/|$)/;
|
|
80373
|
+
/**
|
|
80374
|
+
* The home directory as an Antigravity path: forward slashes, no trailing
|
|
80375
|
+
* slash. agy accepts `C:/Users/...` on Windows, so one spelling serves both.
|
|
80376
|
+
*/
|
|
80377
|
+
function toAntigravityHomePath(homeDirectory) {
|
|
80378
|
+
return homeDirectory.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
80379
|
+
}
|
|
80380
|
+
/**
|
|
80381
|
+
* Rewrite a leading `~` or `~/` of a path target to the absolute home
|
|
80382
|
+
* directory. agy 1.5.1 matches `~` literally, so a `~/...` rule would never
|
|
80383
|
+
* apply (rulesync#3324). Only a file the user's own home holds, such as the
|
|
80384
|
+
* global Antigravity CLI settings, may carry a machine-specific path, so the
|
|
80385
|
+
* caller passes `homeDirectory` only then. `$HOME`, `${HOME}`, `%VAR%` and
|
|
80386
|
+
* `~user/` are left as written (the variable forms are still warned about).
|
|
80387
|
+
*/
|
|
80388
|
+
function expandLeadingHome(target, homeDirectory) {
|
|
80389
|
+
if (homeDirectory === void 0 || !LEADING_HOME.test(target)) return target;
|
|
80390
|
+
const home = toAntigravityHomePath(homeDirectory);
|
|
80391
|
+
return target === "~" ? home || "/" : `${home}${target.slice(1)}`;
|
|
80392
|
+
}
|
|
80393
|
+
/**
|
|
80394
|
+
* The reverse of {@link expandLeadingHome} on import: an absolute path in the
|
|
80395
|
+
* home directory goes back to `~/...`, so `.rulesync/permissions.jsonc` stays
|
|
80396
|
+
* the same on every machine.
|
|
80397
|
+
*/
|
|
80398
|
+
function collapseLeadingHome(target, homeDirectory) {
|
|
80399
|
+
if (homeDirectory === void 0) return target;
|
|
80400
|
+
const home = toAntigravityHomePath(homeDirectory);
|
|
80401
|
+
if (home.length === 0) return target;
|
|
80402
|
+
if (target === home) return "~";
|
|
80403
|
+
return target.startsWith(`${home}/`) ? `~${target.slice(home.length)}` : target;
|
|
80404
|
+
}
|
|
79886
80405
|
const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
|
|
79887
80406
|
const DENY_HINTS = {
|
|
79888
80407
|
read_file: "deny a directory or file path instead",
|
|
@@ -79907,12 +80426,30 @@ function toPathTarget(pattern) {
|
|
|
79907
80426
|
"**",
|
|
79908
80427
|
"**/*"
|
|
79909
80428
|
].includes(path)) return { target: "." };
|
|
79910
|
-
const
|
|
80429
|
+
const stripped = path.replace(DIRECTORY_CONTENTS_SUFFIX, "");
|
|
80430
|
+
const directory = stripped === "" && path.startsWith("/") ? "/" : /^[A-Za-z]:$/.test(stripped) && stripped !== path ? `${stripped}/` : stripped;
|
|
79911
80431
|
if (directory !== path && directory.length > 0 && !hasGlob(directory)) return { target: directory };
|
|
79912
80432
|
if (!hasGlob(path)) return { target: path };
|
|
79913
80433
|
return { skipReason: "Antigravity file targets are paths, not globs. Only a plain path, `dir/**` or `*` can be expressed" };
|
|
79914
80434
|
}
|
|
79915
80435
|
/**
|
|
80436
|
+
* The canonical patterns a `read_file` / `write_file` target imports as. A
|
|
80437
|
+
* target can name a file or a directory, and a directory covers everything
|
|
80438
|
+
* inside it, but the target alone does not say which. A plain path such as
|
|
80439
|
+
* `src` imports as both `src` and `src/**`, so re-exporting it to a tool whose
|
|
80440
|
+
* plain path matches only that one path keeps covering the subtree; regenerating
|
|
80441
|
+
* collapses both back into one target. The bare `*`, and a target that already
|
|
80442
|
+
* holds glob characters (verbatim output of an older Rulesync), stay as written.
|
|
80443
|
+
*/
|
|
80444
|
+
function fromPathTarget(target) {
|
|
80445
|
+
if (target === "*" || hasGlob(target) || /^[A-Za-z]:$/.test(target)) return [target];
|
|
80446
|
+
if (target === ".") return [".", "./**"];
|
|
80447
|
+
const root = /^((?:[A-Za-z]:)?\/)\/*$/.exec(target)?.[1];
|
|
80448
|
+
if (root !== void 0) return [root, `${root}**`];
|
|
80449
|
+
const path = target.replace(/\/+$/, "");
|
|
80450
|
+
return [path, `${path}/**`];
|
|
80451
|
+
}
|
|
80452
|
+
/**
|
|
79916
80453
|
* A URL or domain pattern becomes its hostname. A leading `*.` is dropped,
|
|
79917
80454
|
* since a domain already covers its subdomains, and the URL path is ignored.
|
|
79918
80455
|
* Claude Code's `domain:` form is read as well.
|
|
@@ -80035,13 +80572,14 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
|
|
|
80035
80572
|
* nothing enforces it, and a widened one is warned about too, because it
|
|
80036
80573
|
* blocks more than it says.
|
|
80037
80574
|
*/
|
|
80038
|
-
function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
|
|
80575
|
+
function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel, homeDirectory }) {
|
|
80039
80576
|
const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
|
|
80040
80577
|
const action = isMcp ? "mcp" : toolAction;
|
|
80041
80578
|
const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
|
|
80042
80579
|
const rule = `${category}: { "${pattern}": "${decision}" }`;
|
|
80043
80580
|
if ("skipReason" in result) {
|
|
80044
|
-
const
|
|
80581
|
+
const lead = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
|
|
80582
|
+
const prefix = lead !== void 0 && PATH_ACTIONS.has(action) ? expandLeadingHome(lead, homeDirectory) : lead;
|
|
80045
80583
|
if (prefix !== void 0) {
|
|
80046
80584
|
const { entry, scope } = describeWidenedDeny({
|
|
80047
80585
|
action,
|
|
@@ -80067,22 +80605,23 @@ function buildAntigravityPermissionEntry({ action: toolAction, category, pattern
|
|
|
80067
80605
|
const consequence = decision === "deny" ? ", so the deny also overrides any allow or ask there" : "";
|
|
80068
80606
|
logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}${consequence}.`);
|
|
80069
80607
|
}
|
|
80608
|
+
const target = PATH_ACTIONS.has(action) ? expandLeadingHome(result.target, homeDirectory) : result.target;
|
|
80070
80609
|
warnHomeOrVariablePath({
|
|
80071
80610
|
action,
|
|
80072
|
-
target
|
|
80611
|
+
target,
|
|
80073
80612
|
rule,
|
|
80074
80613
|
decision,
|
|
80075
80614
|
logger,
|
|
80076
80615
|
toolLabel
|
|
80077
80616
|
});
|
|
80078
|
-
return
|
|
80617
|
+
return target === "*" ? action : `${action}(${target})`;
|
|
80079
80618
|
}
|
|
80080
80619
|
/**
|
|
80081
80620
|
* Convert rulesync permissions config to Antigravity allow/ask/deny arrays,
|
|
80082
80621
|
* shared by the CLI and IDE generators; `toAction` maps a canonical category to
|
|
80083
80622
|
* the target's tool or action name.
|
|
80084
80623
|
*/
|
|
80085
|
-
function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel, logger }) {
|
|
80624
|
+
function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel, logger, homeDirectory }) {
|
|
80086
80625
|
const allow = [];
|
|
80087
80626
|
const ask = [];
|
|
80088
80627
|
const deny = [];
|
|
@@ -80095,7 +80634,8 @@ function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel,
|
|
|
80095
80634
|
pattern,
|
|
80096
80635
|
decision: permissionAction,
|
|
80097
80636
|
logger,
|
|
80098
|
-
toolLabel
|
|
80637
|
+
toolLabel,
|
|
80638
|
+
homeDirectory
|
|
80099
80639
|
});
|
|
80100
80640
|
if (entry === void 0) continue;
|
|
80101
80641
|
switch (permissionAction) {
|
|
@@ -80120,25 +80660,27 @@ function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel,
|
|
|
80120
80660
|
* shared by the CLI and IDE adapters; `toCategory` maps the target's tool or
|
|
80121
80661
|
* action name back to a canonical category.
|
|
80122
80662
|
*/
|
|
80123
|
-
function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory, toolLabel, logger }) {
|
|
80663
|
+
function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory, toolLabel, logger, homeDirectory }) {
|
|
80124
80664
|
const permission = {};
|
|
80125
80665
|
const processEntries = (entries, action) => {
|
|
80126
80666
|
for (const entry of entries) {
|
|
80127
80667
|
const { toolName, pattern: target } = parseToolPatternEntry(entry);
|
|
80128
80668
|
const canonical = toCategory(toolName);
|
|
80129
|
-
const
|
|
80669
|
+
const patterns = ANTIGRAVITY_COMMAND_ACTIONS.has(toolName) ? [importAntigravityCommandTarget({
|
|
80130
80670
|
target,
|
|
80131
80671
|
entry,
|
|
80132
80672
|
decision: action,
|
|
80133
80673
|
logger,
|
|
80134
80674
|
toolLabel
|
|
80135
|
-
}) : target;
|
|
80136
|
-
|
|
80137
|
-
|
|
80138
|
-
|
|
80139
|
-
|
|
80140
|
-
|
|
80141
|
-
|
|
80675
|
+
})] : PATH_ACTIONS.has(toolName) ? fromPathTarget(collapseLeadingHome(target, homeDirectory)) : [target];
|
|
80676
|
+
for (const pattern of patterns) {
|
|
80677
|
+
if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
|
|
80678
|
+
getOrCreateOwn({
|
|
80679
|
+
record: permission,
|
|
80680
|
+
key: canonical,
|
|
80681
|
+
create: () => ({})
|
|
80682
|
+
})[pattern] = action;
|
|
80683
|
+
}
|
|
80142
80684
|
}
|
|
80143
80685
|
};
|
|
80144
80686
|
processEntries(allow, "allow");
|
|
@@ -80271,7 +80813,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
80271
80813
|
config,
|
|
80272
80814
|
toAction: toAntigravityCliToolName,
|
|
80273
80815
|
toolLabel: "Antigravity CLI",
|
|
80274
|
-
logger
|
|
80816
|
+
logger,
|
|
80817
|
+
homeDirectory: outputRoot
|
|
80275
80818
|
});
|
|
80276
80819
|
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
|
|
80277
80820
|
const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
|
|
@@ -80322,7 +80865,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
80322
80865
|
deny: permissions.deny ?? [],
|
|
80323
80866
|
toCategory: toCanonicalToolName$9,
|
|
80324
80867
|
toolLabel: "Antigravity CLI",
|
|
80325
|
-
logger: moduleLogger$4
|
|
80868
|
+
logger: moduleLogger$4,
|
|
80869
|
+
homeDirectory: this.getOutputRoot()
|
|
80326
80870
|
});
|
|
80327
80871
|
const override = {};
|
|
80328
80872
|
const toolPermission = pickDocumentedValue({
|
|
@@ -80907,6 +81451,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
80907
81451
|
* in global mode it is dropped with a warning instead of written somewhere it
|
|
80908
81452
|
* does nothing.
|
|
80909
81453
|
*
|
|
81454
|
+
* Only Auggie 0.x reads `recommendedMarketplaces`: the 1.x plugin settings
|
|
81455
|
+
* reference lists `enabledPlugins` alone. The key is still written because the
|
|
81456
|
+
* stable CLI is 0.x, and the write warning says it is 0.x-only.
|
|
81457
|
+
*
|
|
80910
81458
|
* Both make Auggie load code the repository did not ship — a recommended
|
|
80911
81459
|
* marketplace is one prompt away from being cloned, and an enabled plugin can
|
|
80912
81460
|
* carry its own hooks and MCP servers — so writing either is logged, unless
|
|
@@ -80926,7 +81474,7 @@ function buildPluginConsumptionPatch({ override, existing, global, logger }) {
|
|
|
80926
81474
|
else {
|
|
80927
81475
|
patch.recommendedMarketplaces = recommendedMarketplaces;
|
|
80928
81476
|
const existingMarketplaces = existingKeys.recommendedMarketplaces;
|
|
80929
|
-
if (recommendedMarketplaces.length > 0 && !isDeepStrictEqual(recommendedMarketplaces, existingMarketplaces)) logger?.warn(`Writing AugmentCode \`recommendedMarketplaces\` (${recommendedMarketplaces.map(quoteValueForWarning).join(", ")}): Auggie prompts everyone who opens this workspace to install these plugin marketplaces.`);
|
|
81477
|
+
if (recommendedMarketplaces.length > 0 && !isDeepStrictEqual(recommendedMarketplaces, existingMarketplaces)) logger?.warn(`Writing AugmentCode \`recommendedMarketplaces\` (${recommendedMarketplaces.map(quoteValueForWarning).join(", ")}): Auggie 0.x prompts everyone who opens this workspace to install these plugin marketplaces (Auggie 1.x no longer reads this key).`);
|
|
80930
81478
|
warnAboutRemovedPluginIds({
|
|
80931
81479
|
key: "recommendedMarketplaces",
|
|
80932
81480
|
removed: (existingMarketplaces ?? []).filter((marketplace) => !recommendedMarketplaces.includes(marketplace)),
|
|
@@ -81155,12 +81703,12 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
81155
81703
|
const regex = entry.shellInputRegex;
|
|
81156
81704
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
81157
81705
|
else {
|
|
81158
|
-
if (action
|
|
81159
|
-
logger?.warn(`AugmentCode permissions: skipping
|
|
81706
|
+
if (action === "allow" && !canApproximateShellRegexWithoutWidening(regex)) {
|
|
81707
|
+
logger?.warn(`AugmentCode permissions: skipping allow entry for tool '${entry.toolName}': shellInputRegex '${regex}' uses a quantifier or a literal '*' / '?' that a glob cannot express, and converting it would widen the rule. The entry is not imported.`);
|
|
81160
81708
|
continue;
|
|
81161
81709
|
}
|
|
81162
|
-
if (action
|
|
81163
|
-
logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as the catch-all '*' pattern (fail-closed) so
|
|
81710
|
+
if (action !== "allow") {
|
|
81711
|
+
logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing the ${action} rule as the catch-all '*' pattern (fail-closed) so it cannot be silently narrowed on regenerate.`);
|
|
81164
81712
|
pattern = "*";
|
|
81165
81713
|
} else {
|
|
81166
81714
|
pattern = shellRegexToGlob(regex);
|
|
@@ -81246,6 +81794,62 @@ function classifyPattern(pattern, bareStarIsWildcard) {
|
|
|
81246
81794
|
};
|
|
81247
81795
|
return { kind: "literal" };
|
|
81248
81796
|
}
|
|
81797
|
+
/** The trimmed spellings of the literal (prefix) deny entries, minus the bare wildcard. */
|
|
81798
|
+
function collectLiteralDenyStems({ rules, bareStarIsWildcard }) {
|
|
81799
|
+
const stems = /* @__PURE__ */ new Set();
|
|
81800
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
81801
|
+
if (action !== "deny" || pattern === "*") continue;
|
|
81802
|
+
const stem = pattern.trimEnd();
|
|
81803
|
+
if (stem.length > 0 && classifyPattern(pattern, bareStarIsWildcard).kind === "literal") stems.add(stem);
|
|
81804
|
+
}
|
|
81805
|
+
return stems;
|
|
81806
|
+
}
|
|
81807
|
+
function isCoveredByLiteralDeny({ pattern, literalDenyStems }) {
|
|
81808
|
+
if (!pattern.endsWith(" *")) return false;
|
|
81809
|
+
const stem = pattern.slice(0, -2).trimEnd();
|
|
81810
|
+
return stem.length > 0 && literalDenyStems.has(stem);
|
|
81811
|
+
}
|
|
81812
|
+
/**
|
|
81813
|
+
* Lower a VS Code style command allow/deny list pair into canonical `bash`
|
|
81814
|
+
* rules on import.
|
|
81815
|
+
*
|
|
81816
|
+
* The native entries are compared as literal command prefixes, while canonical
|
|
81817
|
+
* patterns are globs, so a verbatim copy changes their meaning:
|
|
81818
|
+
*
|
|
81819
|
+
* - A prefix deny such as `curl` blocks `curl https://…` natively but would
|
|
81820
|
+
* only block the exact command `curl` canonically, so a broad allow beside
|
|
81821
|
+
* it would auto-approve what the native config blocked. Each deny that does
|
|
81822
|
+
* not already end in `*` is imported as itself plus `<deny> *`.
|
|
81823
|
+
* - An allow holding glob syntax (`npm *`) is literal prefix text natively and
|
|
81824
|
+
* approves almost nothing, but canonically it is a wildcard. Such an allow is
|
|
81825
|
+
* not imported (with a warning) rather than imported wider than it matched.
|
|
81826
|
+
* The Roo Code lineage's bare `*` is a real wildcard natively and is kept.
|
|
81827
|
+
*
|
|
81828
|
+
* A pattern in both lists imports as `deny`, matching both tools' precedence
|
|
81829
|
+
* for identical entries.
|
|
81830
|
+
*/
|
|
81831
|
+
function importVscodeCommandLists({ allowed, denied, toolLabel, logger, prefixSemantics = "roo-lineage" }) {
|
|
81832
|
+
const isRooLineage = prefixSemantics === "roo-lineage";
|
|
81833
|
+
const rules = {};
|
|
81834
|
+
const skippedAllows = [];
|
|
81835
|
+
for (const pattern of allowed) {
|
|
81836
|
+
if (!(isRooLineage && pattern === "*") && /[*?]/.test(pattern)) {
|
|
81837
|
+
skippedAllows.push(pattern);
|
|
81838
|
+
continue;
|
|
81839
|
+
}
|
|
81840
|
+
rules[pattern] = "allow";
|
|
81841
|
+
}
|
|
81842
|
+
for (const pattern of denied) {
|
|
81843
|
+
rules[pattern] = "deny";
|
|
81844
|
+
const stem = pattern.trimEnd();
|
|
81845
|
+
if (!pattern.endsWith("*") && stem.length > 0) rules[`${stem} *`] = "deny";
|
|
81846
|
+
}
|
|
81847
|
+
if (skippedAllows.length > 0) {
|
|
81848
|
+
const count = skippedAllows.length;
|
|
81849
|
+
warnWithFallback(logger, `${toolLabel}: allow ${pluralize(count, "pattern", "patterns")} ${formatPatterns(skippedAllows)} ${pluralize(count, "was", "were")} not imported. ${toolLabel} compares ${pluralize(count, "it", "them")} as literal command prefix text, while a rulesync pattern is a glob, so importing ${pluralize(count, "it", "them")} would approve more commands than the native config does.` + (isRooLineage ? ` Only a bare "*" is a wildcard there.` : ""));
|
|
81850
|
+
}
|
|
81851
|
+
return rules;
|
|
81852
|
+
}
|
|
81249
81853
|
function pluralize(count, singular, plural) {
|
|
81250
81854
|
return count === 1 ? singular : plural;
|
|
81251
81855
|
}
|
|
@@ -81340,8 +81944,16 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
|
|
|
81340
81944
|
const matcherAllows = [];
|
|
81341
81945
|
const widenedDenies = [];
|
|
81342
81946
|
const inertDenies = [];
|
|
81947
|
+
const literalDenyStems = collectLiteralDenyStems({
|
|
81948
|
+
rules,
|
|
81949
|
+
bareStarIsWildcard: isRooLineage
|
|
81950
|
+
});
|
|
81343
81951
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
81344
81952
|
if (action !== "allow" && action !== "deny") continue;
|
|
81953
|
+
if (action === "deny" && isCoveredByLiteralDeny({
|
|
81954
|
+
pattern,
|
|
81955
|
+
literalDenyStems
|
|
81956
|
+
})) continue;
|
|
81345
81957
|
const shape = classifyPattern(pattern, isRooLineage);
|
|
81346
81958
|
if (action === "allow") {
|
|
81347
81959
|
allowed.push(pattern);
|
|
@@ -81704,16 +82316,20 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
|
|
|
81704
82316
|
validate: true
|
|
81705
82317
|
});
|
|
81706
82318
|
}
|
|
81707
|
-
toRulesyncPermissions() {
|
|
82319
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
81708
82320
|
const settings = parseBobSettings({
|
|
81709
82321
|
fileContent: this.getFileContent(),
|
|
81710
82322
|
filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
81711
82323
|
});
|
|
81712
82324
|
const approval = settings[APPROVAL_KEY$1];
|
|
81713
82325
|
const entry = (isPlainObject(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
|
|
81714
|
-
const rules = {
|
|
81715
|
-
|
|
81716
|
-
|
|
82326
|
+
const rules = importVscodeCommandLists({
|
|
82327
|
+
allowed: toStringArray$1(entry?.approvedCommands),
|
|
82328
|
+
denied: toStringArray$1(entry?.deniedCommands),
|
|
82329
|
+
toolLabel: TOOL_LABEL,
|
|
82330
|
+
logger,
|
|
82331
|
+
prefixSemantics: "documented-prefix"
|
|
82332
|
+
});
|
|
81717
82333
|
const permission = {};
|
|
81718
82334
|
if (Object.keys(rules).length > 0) permission[SHELL_PERMISSION_CATEGORY] = rules;
|
|
81719
82335
|
const bob = readBobOverride(settings);
|
|
@@ -85477,7 +86093,7 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
|
|
|
85477
86093
|
const patch = {};
|
|
85478
86094
|
const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
|
|
85479
86095
|
for (const [key, value] of Object.entries(override ?? {})) {
|
|
85480
|
-
if (key === "base_permission_profile" || key === "git_write_rules") continue;
|
|
86096
|
+
if (key === "base_permission_profile" || key === "git_write_rules" || key === "bash_canonical_matching") continue;
|
|
85481
86097
|
if (!allowed.has(key)) {
|
|
85482
86098
|
logger?.warn(`Codex CLI permission override key "${key}" is not managed and was skipped. "permissions"/"default_permissions" are owned by the canonical permission model and "mcp_servers" gating by the MCP feature.`);
|
|
85483
86099
|
continue;
|
|
@@ -85766,6 +86382,7 @@ function stripTrailingSlash(pattern) {
|
|
|
85766
86382
|
function buildCodexBashRulesContent({ config, logger }) {
|
|
85767
86383
|
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
85768
86384
|
const entries = Object.entries(bashRules);
|
|
86385
|
+
const canonicalMatching = config.codexcli?.bash_canonical_matching === true;
|
|
85769
86386
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
85770
86387
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
85771
86388
|
const ruleBlocks = entries.map(([pattern, action]) => {
|
|
@@ -85784,6 +86401,7 @@ function buildCodexBashRulesContent({ config, logger }) {
|
|
|
85784
86401
|
` pattern = [${serializedTokens}],`,
|
|
85785
86402
|
` decision = ${JSON.stringify(decision)},`,
|
|
85786
86403
|
` justification = ${JSON.stringify(`Generated from Rulesync permission.bash: ${pattern}`)},`,
|
|
86404
|
+
...canonicalMatching && decision !== "allow" ? [" canonical_matching = True,"] : [],
|
|
85787
86405
|
")"
|
|
85788
86406
|
].join("\n");
|
|
85789
86407
|
}).filter((block) => block !== null);
|
|
@@ -87368,9 +87986,126 @@ function toCanonicalToolName$5(crushName) {
|
|
|
87368
87986
|
function isMcpToolName(crushName) {
|
|
87369
87987
|
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
87370
87988
|
}
|
|
87989
|
+
const CRUSH_COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
87990
|
+
"env",
|
|
87991
|
+
"nohup",
|
|
87992
|
+
"nice",
|
|
87993
|
+
"timeout"
|
|
87994
|
+
]);
|
|
87995
|
+
const CRUSH_COMMAND_PREFIXES = [...CRUSH_COMMAND_WRAPPERS, "time"];
|
|
87996
|
+
const CRUSH_COMMAND_NAME = /^[a-z0-9_][a-z0-9_.+-]*$/;
|
|
87997
|
+
const CRUSH_COMMAND_ARGUMENT = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
87998
|
+
const CRUSH_COMMAND_NAME_SUFFIXES = [
|
|
87999
|
+
".exe",
|
|
88000
|
+
".bat",
|
|
88001
|
+
".cmd"
|
|
88002
|
+
];
|
|
88003
|
+
function isCrushCommandName(name) {
|
|
88004
|
+
return CRUSH_COMMAND_NAME.test(name) && !CRUSH_COMMAND_NAME_SUFFIXES.some((suffix) => name.endsWith(suffix)) && !isPrototypePollutionKey(name);
|
|
88005
|
+
}
|
|
88006
|
+
/**
|
|
88007
|
+
* Reduce a command name the way Crush matches it (`shell.NormalizeCommandName`):
|
|
88008
|
+
* trimmed, without its directory, lower-cased, and without a Windows
|
|
88009
|
+
* executable suffix.
|
|
88010
|
+
*/
|
|
88011
|
+
function normalizeCrushCommandName(raw) {
|
|
88012
|
+
const base = (raw.trim().split(/[\\/]/).at(-1) ?? "").toLowerCase();
|
|
88013
|
+
const suffix = CRUSH_COMMAND_NAME_SUFFIXES.find((candidate) => base.length > candidate.length && base.endsWith(candidate));
|
|
88014
|
+
return suffix === void 0 ? base : base.slice(0, -suffix.length);
|
|
88015
|
+
}
|
|
88016
|
+
/**
|
|
88017
|
+
* Whether a canonical pattern is a literal command line that a Crush
|
|
88018
|
+
* `safe_commands` entry reproduces exactly: Crush matches an entry only when
|
|
88019
|
+
* the command is written with exactly those tokens, no extra arguments and no
|
|
88020
|
+
* flags, which is what a wildcard-free glob means. A leading wrapper is
|
|
88021
|
+
* refused, since Crush would resolve it away and approve the bare command.
|
|
88022
|
+
*/
|
|
88023
|
+
function isExactCrushCommandLine(pattern) {
|
|
88024
|
+
const [name, ...args] = pattern.split(" ");
|
|
88025
|
+
return name !== void 0 && isCrushCommandName(name) && !CRUSH_COMMAND_WRAPPERS.has(name) && args.every((arg) => CRUSH_COMMAND_ARGUMENT.test(arg));
|
|
88026
|
+
}
|
|
88027
|
+
/**
|
|
88028
|
+
* The command name a canonical `<name>` / `<name> *` pattern names as a
|
|
88029
|
+
* whole, i.e. the `blocked_commands` entry for it, or undefined.
|
|
88030
|
+
*/
|
|
88031
|
+
function wholeCommandName(pattern) {
|
|
88032
|
+
const name = pattern.endsWith(" *") ? pattern.slice(0, -2) : pattern;
|
|
88033
|
+
return isCrushCommandName(name) ? name : void 0;
|
|
88034
|
+
}
|
|
88035
|
+
/**
|
|
88036
|
+
* The line a Crush `safe_commands` entry effectively stands for, in canonical
|
|
88037
|
+
* form (whitespace collapsed, command name normalized), or undefined when the
|
|
88038
|
+
* entry has no exact canonical spelling (wildcards, quoting, a wrapper, ...).
|
|
88039
|
+
*/
|
|
88040
|
+
function effectiveSafeCommandLine(entry) {
|
|
88041
|
+
const [name, ...args] = entry.trim().split(/\s+/);
|
|
88042
|
+
if (name === void 0 || name === "") return void 0;
|
|
88043
|
+
const line = [normalizeCrushCommandName(name), ...args].join(" ");
|
|
88044
|
+
return isExactCrushCommandLine(line) ? line : void 0;
|
|
88045
|
+
}
|
|
88046
|
+
/**
|
|
88047
|
+
* A restriction pattern with its command word reduced the way Crush reduces a
|
|
88048
|
+
* wrapper's name before peeling it (`shell.NormalizeCommandName`): any
|
|
88049
|
+
* directory dropped (`./nice`, `bin\env.exe`, `/usr/bin/env`), lower-cased,
|
|
88050
|
+
* and without a Windows executable suffix. A `*` in the command word may
|
|
88051
|
+
* stand for a directory and part of a name, so when what follows it is the
|
|
88052
|
+
* tail of a wrapper name, everything before it collapses into `*`
|
|
88053
|
+
* (`b*nice` -> `*nice`, `/u*v` -> `*v`; `rm*` is left alone). The rest of the pattern is lower-cased as well, for a
|
|
88054
|
+
* nested wrapper spelled in another case. Used only to compare against the
|
|
88055
|
+
* wrapper spellings of a safe line, where it can only withhold more.
|
|
88056
|
+
*/
|
|
88057
|
+
function normalizeRestrictionCommandWord(pattern) {
|
|
88058
|
+
const space = pattern.indexOf(" ");
|
|
88059
|
+
let head = space < 0 ? pattern : pattern.slice(0, space);
|
|
88060
|
+
const rest = space < 0 ? "" : pattern.slice(space);
|
|
88061
|
+
head = normalizeCrushCommandName(head);
|
|
88062
|
+
const star = head.lastIndexOf("*");
|
|
88063
|
+
const tail = head.slice(star + 1);
|
|
88064
|
+
if (star > 0 && tail !== "" && CRUSH_COMMAND_PREFIXES.some((prefix) => prefix.endsWith(tail))) head = `*${tail}`;
|
|
88065
|
+
return `${head}${rest.toLowerCase()}`;
|
|
88066
|
+
}
|
|
88067
|
+
/**
|
|
88068
|
+
* Build the test for a `safe_commands` line: which canonical `deny`/`ask`
|
|
88069
|
+
* rules govern a command Crush would approve through it? Besides the line
|
|
88070
|
+
* itself, Crush approves it followed by `--` and behind a wrapper (or the
|
|
88071
|
+
* `time` keyword), whose name it normalizes (`./nice`, `/usr/bin/env`, `NICE`,
|
|
88072
|
+
* `nohup.exe`). The line and its wrapper spellings are compared against the
|
|
88073
|
+
* restrictions as written, and the wrapper spellings also against the
|
|
88074
|
+
* restrictions with their command word normalized the same way — which keeps
|
|
88075
|
+
* an unrelated `rm *` from withholding the line, as a leading wildcard on the
|
|
88076
|
+
* spelling would. Canonically the stricter rule wins whatever its width
|
|
88077
|
+
* (`deny > ask > allow`), so any overlap withholds the line.
|
|
88078
|
+
*
|
|
88079
|
+
* Crush also approves a line composed with other safe lines (`&&`, `||`, `|`,
|
|
88080
|
+
* `;`, a parenthesized group); those compositions are not compared, the way
|
|
88081
|
+
* other tools' per-subcommand matching treats an allowed command.
|
|
88082
|
+
*/
|
|
88083
|
+
function createSafeCommandShadowTest(permission) {
|
|
88084
|
+
const restrictions = collectShellCommandRules(permission).rules.filter(({ action }) => action !== "allow");
|
|
88085
|
+
const test = createShadowingRestrictionsTest(restrictions);
|
|
88086
|
+
const normalized = restrictions.map((rule) => ({
|
|
88087
|
+
...rule,
|
|
88088
|
+
pattern: normalizeRestrictionCommandWord(rule.pattern)
|
|
88089
|
+
}));
|
|
88090
|
+
const testNormalized = createShadowingRestrictionsTest(normalized);
|
|
88091
|
+
const originalOf = /* @__PURE__ */ new Map();
|
|
88092
|
+
normalized.forEach((rule, index) => {
|
|
88093
|
+
const original = restrictions[index]?.pattern;
|
|
88094
|
+
if (original !== void 0 && !originalOf.has(rule.pattern)) originalOf.set(rule.pattern, original);
|
|
88095
|
+
});
|
|
88096
|
+
const withSeparator = (spellings) => spellings.flatMap((spelling) => [spelling, `${spelling} --*`]);
|
|
88097
|
+
const wrapped = (base) => CRUSH_COMMAND_PREFIXES.map((prefix) => `${prefix}*${base}`);
|
|
88098
|
+
return (line) => uniq$1([...withSeparator([line, ...wrapped(line)]).flatMap((spelling) => test(spelling)), ...withSeparator(wrapped(line.toLowerCase())).flatMap((spelling) => testNormalized(spelling).map((pattern) => originalOf.get(pattern) ?? pattern))]);
|
|
88099
|
+
}
|
|
87371
88100
|
function uniq$1(values) {
|
|
87372
88101
|
return [...new Set(values)];
|
|
87373
88102
|
}
|
|
88103
|
+
function stringList(value) {
|
|
88104
|
+
return isStringArray(value) ? value : [];
|
|
88105
|
+
}
|
|
88106
|
+
function listOrUndefined(values) {
|
|
88107
|
+
return values.length > 0 ? values : void 0;
|
|
88108
|
+
}
|
|
87374
88109
|
/**
|
|
87375
88110
|
* Permissions adapter for Crush.
|
|
87376
88111
|
*
|
|
@@ -87392,8 +88127,24 @@ function uniq$1(values) {
|
|
|
87392
88127
|
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
87393
88128
|
* any other category passes through verbatim.
|
|
87394
88129
|
*
|
|
87395
|
-
*
|
|
87396
|
-
*
|
|
88130
|
+
* Crush v0.100.0+ also carries two command lists, which the `bash` category's
|
|
88131
|
+
* pattern rules map onto (see `convertBashCommandRules`):
|
|
88132
|
+
* - `permissions.safe_commands`: exact command lines that skip the prompt
|
|
88133
|
+
* (no extra arguments, no flags) <- an `allow` on a literal command line
|
|
88134
|
+
* that no canonical `deny`/`ask` overlaps.
|
|
88135
|
+
* - `permissions.blocked_commands`: command names added to the dangerous
|
|
88136
|
+
* list <- a `deny` on `<name>` or `<name> *`. Not a full deny: a direct
|
|
88137
|
+
* invocation is still prompted for (and auto-approved in yolo mode or
|
|
88138
|
+
* through `allowed_tools: ["bash"]`); only the forms Crush would not
|
|
88139
|
+
* prompt for (its read-only commands, `safe_commands`) and indirect
|
|
88140
|
+
* invocations are refused at exec time. An `ask` is not written there,
|
|
88141
|
+
* since it would turn the read-only forms into a refusal.
|
|
88142
|
+
* `permissions.allowed_commands` only drops names from the dangerous list (the
|
|
88143
|
+
* commands keep prompting), so it has no canonical counterpart and is left
|
|
88144
|
+
* untouched.
|
|
88145
|
+
*
|
|
88146
|
+
* Any other pattern-specific rule cannot be expressed and is reported and
|
|
88147
|
+
* skipped. Because Crush cannot narrow an allowed tool, a category's catch-all `allow`
|
|
87397
88148
|
* is not written while the same category carries a pattern-specific `deny` or
|
|
87398
88149
|
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
87399
88150
|
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
@@ -87457,7 +88208,12 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87457
88208
|
const existingContent = location.fileContent ?? "";
|
|
87458
88209
|
warnCrushTwinLeftovers({
|
|
87459
88210
|
location,
|
|
87460
|
-
ownedPaths: [
|
|
88211
|
+
ownedPaths: [
|
|
88212
|
+
[CRUSH_PERMISSIONS_KEY, CRUSH_ALLOWED_TOOLS_KEY],
|
|
88213
|
+
[CRUSH_PERMISSIONS_KEY, CRUSH_SAFE_COMMANDS_KEY],
|
|
88214
|
+
[CRUSH_PERMISSIONS_KEY, CRUSH_BLOCKED_COMMANDS_KEY],
|
|
88215
|
+
[CRUSH_OPTIONS_KEY, CRUSH_DISABLED_TOOLS_KEY]
|
|
88216
|
+
],
|
|
87461
88217
|
logger
|
|
87462
88218
|
});
|
|
87463
88219
|
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
@@ -87469,12 +88225,22 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87469
88225
|
logger
|
|
87470
88226
|
});
|
|
87471
88227
|
const managedToolNames = managedCrushToolNames(config);
|
|
88228
|
+
const commands = rebuildCrushCommandLists({
|
|
88229
|
+
config,
|
|
88230
|
+
existingPermissions,
|
|
88231
|
+
logger
|
|
88232
|
+
});
|
|
87472
88233
|
const preservedAllowed = (isStringArray(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
87473
88234
|
const preservedDisabled = (isStringArray(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
87474
88235
|
const allowedList = uniq$1([...preservedAllowed, ...generated.allowed]);
|
|
87475
88236
|
const disabledList = uniq$1([...preservedDisabled, ...generated.disabled]);
|
|
88237
|
+
const permissionsHasContent = allowedList.length > 0 || commands.safe.length > 0 || commands.blocked.length > 0;
|
|
87476
88238
|
const patch = {};
|
|
87477
|
-
if (
|
|
88239
|
+
if (permissionsHasContent || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = {
|
|
88240
|
+
[CRUSH_ALLOWED_TOOLS_KEY]: listOrUndefined(allowedList),
|
|
88241
|
+
[CRUSH_SAFE_COMMANDS_KEY]: listOrUndefined(commands.safe),
|
|
88242
|
+
[CRUSH_BLOCKED_COMMANDS_KEY]: listOrUndefined(commands.blocked)
|
|
88243
|
+
};
|
|
87478
88244
|
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
87479
88245
|
return new CrushPermissions({
|
|
87480
88246
|
outputRoot,
|
|
@@ -87492,17 +88258,79 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87492
88258
|
global
|
|
87493
88259
|
});
|
|
87494
88260
|
}
|
|
87495
|
-
toRulesyncPermissions() {
|
|
88261
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
87496
88262
|
const permissions = isRecord(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
87497
88263
|
const options = toRecord(this.json[CRUSH_OPTIONS_KEY]);
|
|
87498
88264
|
const rulesyncConfig = convertCrushListsToRulesync({
|
|
87499
|
-
allowed:
|
|
87500
|
-
disabled:
|
|
88265
|
+
allowed: stringList(permissions[CRUSH_ALLOWED_TOOLS_KEY]),
|
|
88266
|
+
disabled: stringList(options[CRUSH_DISABLED_TOOLS_KEY]),
|
|
88267
|
+
safe: stringList(permissions[CRUSH_SAFE_COMMANDS_KEY]),
|
|
88268
|
+
blocked: stringList(permissions[CRUSH_BLOCKED_COMMANDS_KEY]),
|
|
88269
|
+
logger
|
|
87501
88270
|
});
|
|
87502
88271
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
87503
88272
|
}
|
|
87504
88273
|
};
|
|
87505
88274
|
/**
|
|
88275
|
+
* The `safe_commands` / `blocked_commands` lists a generate writes: the
|
|
88276
|
+
* existing entries rulesync keeps, followed by the ones derived from the
|
|
88277
|
+
* canonical `bash` rules. Existing `blocked_commands` entries are all kept:
|
|
88278
|
+
* rulesync cannot tell its own from hand-written ones, and dropping one only
|
|
88279
|
+
* ever loosens. A derived blocked name that is the command of a written safe
|
|
88280
|
+
* line is withheld, since Crush would refuse that line at exec time.
|
|
88281
|
+
*/
|
|
88282
|
+
function rebuildCrushCommandLists({ config, existingPermissions, logger }) {
|
|
88283
|
+
const bashRules = honorAllToolsOnBash(config.permission)["bash"] ?? {};
|
|
88284
|
+
const shadowTest = createSafeCommandShadowTest(config.permission);
|
|
88285
|
+
const generated = convertBashCommandRules({
|
|
88286
|
+
bashRules,
|
|
88287
|
+
shadowTest,
|
|
88288
|
+
logger
|
|
88289
|
+
});
|
|
88290
|
+
const safe = uniq$1([...preserveSafeCommands({
|
|
88291
|
+
existing: stringList(existingPermissions?.[CRUSH_SAFE_COMMANDS_KEY]),
|
|
88292
|
+
bashRules,
|
|
88293
|
+
shadowTest,
|
|
88294
|
+
logger
|
|
88295
|
+
}), ...generated.safe]);
|
|
88296
|
+
const safeCommandNames = new Set(safe.flatMap((entry) => {
|
|
88297
|
+
const name = entry.trim().split(/\s+/)[0];
|
|
88298
|
+
return name === void 0 || name === "" ? [] : [normalizeCrushCommandName(name)];
|
|
88299
|
+
}));
|
|
88300
|
+
const blocked = generated.blocked.filter((name) => {
|
|
88301
|
+
if (!safeCommandNames.has(name)) return true;
|
|
88302
|
+
logger?.warn(`The "deny" rule for "bash" on ${quoteValueForWarning(name)} is not added to permissions.blocked_commands because Crush would then refuse the allowed permissions.safe_commands line(s) for that command; Crush keeps prompting for it.`);
|
|
88303
|
+
return false;
|
|
88304
|
+
});
|
|
88305
|
+
const existingBlocked = stringList(existingPermissions?.[CRUSH_BLOCKED_COMMANDS_KEY]);
|
|
88306
|
+
for (const entry of existingBlocked) {
|
|
88307
|
+
if (!safeCommandNames.has(normalizeCrushCommandName(entry))) continue;
|
|
88308
|
+
logger?.warn(`The existing permissions.blocked_commands entry ${quoteValueForWarning(entry)} is kept, but Crush will refuse the permissions.safe_commands line(s) for that command at exec time; remove one of them by hand.`);
|
|
88309
|
+
}
|
|
88310
|
+
return {
|
|
88311
|
+
safe,
|
|
88312
|
+
blocked: uniq$1([...existingBlocked, ...blocked])
|
|
88313
|
+
};
|
|
88314
|
+
}
|
|
88315
|
+
/**
|
|
88316
|
+
* The existing `safe_commands` entries a generate keeps: those the canonical
|
|
88317
|
+
* `bash` rules do not spell out themselves (an entry they do spell is rebuilt
|
|
88318
|
+
* from them), minus any entry a canonical `deny`/`ask` governs, which would
|
|
88319
|
+
* otherwise auto-approve what the config restricts. An entry whose exact
|
|
88320
|
+
* meaning cannot be read is dropped whenever the config restricts anything.
|
|
88321
|
+
*/
|
|
88322
|
+
function preserveSafeCommands({ existing, bashRules, shadowTest, logger }) {
|
|
88323
|
+
const managedLines = new Set(Object.keys(bashRules));
|
|
88324
|
+
const restricts = shadowTest("*").length > 0;
|
|
88325
|
+
return existing.filter((entry) => {
|
|
88326
|
+
if (managedLines.has(entry)) return false;
|
|
88327
|
+
const line = effectiveSafeCommandLine(entry);
|
|
88328
|
+
const shadowed = line === void 0 ? restricts : shadowTest(line).length > 0;
|
|
88329
|
+
if (shadowed) logger?.warn(`Crush permissions.safe_commands entry ${quoteValueForWarning(entry)} was removed because a canonical deny/ask rule may cover the commands it approves.`);
|
|
88330
|
+
return !shadowed;
|
|
88331
|
+
});
|
|
88332
|
+
}
|
|
88333
|
+
/**
|
|
87506
88334
|
* The Crush tool names the canonical config manages, i.e. the names its
|
|
87507
88335
|
* categories map to. Entries naming any other tool are the user's and survive
|
|
87508
88336
|
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
@@ -87527,7 +88355,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
87527
88355
|
catchAllAction = action;
|
|
87528
88356
|
continue;
|
|
87529
88357
|
}
|
|
87530
|
-
logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
88358
|
+
if (category !== "bash") logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
87531
88359
|
if (action !== "allow") restricted = true;
|
|
87532
88360
|
}
|
|
87533
88361
|
if (catchAllAction === "allow" && restricted) {
|
|
@@ -87578,11 +88406,71 @@ function convertRulesyncToCrushLists({ config, logger }) {
|
|
|
87578
88406
|
};
|
|
87579
88407
|
}
|
|
87580
88408
|
/**
|
|
88409
|
+
* Map the `bash` category's pattern rules onto Crush's command lists:
|
|
88410
|
+
* - `allow` on a literal command line -> `permissions.safe_commands`, which
|
|
88411
|
+
* Crush matches exactly (no extra arguments, no flags). It is withheld
|
|
88412
|
+
* when a canonical `deny`/`ask` governs any command Crush would approve
|
|
88413
|
+
* through it.
|
|
88414
|
+
* - `deny` on `<name>` / `<name> *` -> `permissions.blocked_commands`, the
|
|
88415
|
+
* strictest Crush can say about a single command: a direct invocation is
|
|
88416
|
+
* still prompted for, while the forms Crush would not prompt for and
|
|
88417
|
+
* indirect invocations are refused at exec time. Reported, since it is
|
|
88418
|
+
* not a full deny.
|
|
88419
|
+
* - Anything else is reported and skipped. A pattern `ask` is never written
|
|
88420
|
+
* to `blocked_commands`, which would refuse the command's read-only forms
|
|
88421
|
+
* outright, and an `allow` with a wildcard has no list that auto-approves
|
|
88422
|
+
* it.
|
|
88423
|
+
*/
|
|
88424
|
+
function convertBashCommandRules({ bashRules, shadowTest, logger }) {
|
|
88425
|
+
const lists = {
|
|
88426
|
+
safe: [],
|
|
88427
|
+
blocked: []
|
|
88428
|
+
};
|
|
88429
|
+
for (const [rawPattern, action] of Object.entries(bashRules)) {
|
|
88430
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
88431
|
+
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
88432
|
+
if (pattern === "*") continue;
|
|
88433
|
+
const quoted = quoteValueForWarning(pattern);
|
|
88434
|
+
if (action === "allow") {
|
|
88435
|
+
if (!isExactCrushCommandLine(pattern)) {
|
|
88436
|
+
logger?.warn(`Crush auto-approves a command only as an exact line in permissions.safe_commands (no wildcards or shell syntax), so the "allow" rule for "bash" (pattern ${quoted}) was skipped; Crush keeps prompting for it.`);
|
|
88437
|
+
continue;
|
|
88438
|
+
}
|
|
88439
|
+
const shadows = shadowTest(pattern);
|
|
88440
|
+
if (shadows.length > 0) {
|
|
88441
|
+
logger?.warn(`The "allow" rule for "bash" (pattern ${quoted}) is not added to permissions.safe_commands because the deny/ask rule(s) ${shadows.map(quoteValueForWarning).join(", ")} also cover commands Crush would approve through it; Crush keeps prompting for it.`);
|
|
88442
|
+
continue;
|
|
88443
|
+
}
|
|
88444
|
+
lists.safe.push(pattern);
|
|
88445
|
+
continue;
|
|
88446
|
+
}
|
|
88447
|
+
if (action === "ask") {
|
|
88448
|
+
logger?.warn(`Crush has no per-command prompt list (permissions.blocked_commands would refuse the command's read-only forms outright), so the "ask" rule for "bash" (pattern ${quoted}) was skipped; Crush still prompts for it unless it is one of Crush's built-in read-only commands.`);
|
|
88449
|
+
continue;
|
|
88450
|
+
}
|
|
88451
|
+
const name = wholeCommandName(pattern);
|
|
88452
|
+
if (name !== void 0 && CRUSH_COMMAND_PREFIXES.includes(name)) {
|
|
88453
|
+
logger?.warn(`Crush resolves the command wrapper or keyword ${quoteValueForWarning(name)} before matching permissions.blocked_commands, so an entry for it would have no effect; the "deny" rule for "bash" (pattern ${quoted}) was skipped and Crush still prompts for it.`);
|
|
88454
|
+
continue;
|
|
88455
|
+
}
|
|
88456
|
+
if (name === void 0) {
|
|
88457
|
+
logger?.warn(`Crush can only restrict a whole command (permissions.blocked_commands), so the "deny" rule for "bash" (pattern ${quoted}) was skipped; Crush still prompts for it unless it is one of Crush's built-in read-only commands.`);
|
|
88458
|
+
continue;
|
|
88459
|
+
}
|
|
88460
|
+
logger?.warn(`The "deny" rule for "bash" (pattern ${quoted}) is written as permissions.blocked_commands entry ${quoteValueForWarning(name)}, which is not a full deny: Crush still prompts for a direct invocation (and runs it unprompted in yolo mode or with "bash" in allowed_tools).`);
|
|
88461
|
+
lists.blocked.push(name);
|
|
88462
|
+
}
|
|
88463
|
+
return {
|
|
88464
|
+
safe: uniq$1(lists.safe),
|
|
88465
|
+
blocked: uniq$1(lists.blocked)
|
|
88466
|
+
};
|
|
88467
|
+
}
|
|
88468
|
+
/**
|
|
87581
88469
|
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
87582
88470
|
* entry scopes an allow to one action of a tool, which the canonical model
|
|
87583
88471
|
* cannot express, so it is left out rather than widened to the whole tool.
|
|
87584
88472
|
*/
|
|
87585
|
-
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
88473
|
+
function convertCrushListsToRulesync({ allowed, disabled, safe, blocked, logger }) {
|
|
87586
88474
|
const permission = {};
|
|
87587
88475
|
const bucketFor = (category) => {
|
|
87588
88476
|
const own = lookupOwn({
|
|
@@ -87598,6 +88486,29 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
87598
88486
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
87599
88487
|
bucketFor(toCanonicalToolName$5(entry))["*"] = "deny";
|
|
87600
88488
|
}
|
|
88489
|
+
const setBashRule = (pattern, action) => {
|
|
88490
|
+
const bucket = bucketFor(SHELL_PERMISSION_CATEGORY);
|
|
88491
|
+
if (!Object.hasOwn(bucket, pattern)) bucket[pattern] = action;
|
|
88492
|
+
};
|
|
88493
|
+
for (const entry of blocked) {
|
|
88494
|
+
const name = normalizeCrushCommandName(entry);
|
|
88495
|
+
if (name === "") continue;
|
|
88496
|
+
if (!isCrushCommandName(name)) {
|
|
88497
|
+
logger?.warn(`Crush permissions.blocked_commands entry ${quoteValueForWarning(entry)} is not a plain command name, so it was not imported.`);
|
|
88498
|
+
continue;
|
|
88499
|
+
}
|
|
88500
|
+
setBashRule(name, "deny");
|
|
88501
|
+
setBashRule(`${name} *`, "deny");
|
|
88502
|
+
}
|
|
88503
|
+
for (const entry of safe) {
|
|
88504
|
+
if (entry.trim() === "") continue;
|
|
88505
|
+
const line = effectiveSafeCommandLine(entry);
|
|
88506
|
+
if (line === void 0) {
|
|
88507
|
+
logger?.warn(`Crush permissions.safe_commands entry ${quoteValueForWarning(entry)} has no exact rulesync pattern (wildcards, shell syntax or a command wrapper), so it was not imported.`);
|
|
88508
|
+
continue;
|
|
88509
|
+
}
|
|
88510
|
+
setBashRule(line, "allow");
|
|
88511
|
+
}
|
|
87601
88512
|
for (const entry of allowed) {
|
|
87602
88513
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
87603
88514
|
bucketFor(toCanonicalToolName$5(entry))["*"] ??= "allow";
|
|
@@ -88977,8 +89888,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
|
88977
89888
|
* In global mode the config file is shared with the hooks (`hooks`) feature
|
|
88978
89889
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
88979
89890
|
* merge into the existing JSON and the file is never deleted; only the keys
|
|
88980
|
-
* this feature manages are rewritten — `permissions`, plus `sandbox`
|
|
88981
|
-
* mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
|
|
89891
|
+
* this feature manages are rewritten — `permissions`, plus `sandbox` and
|
|
89892
|
+
* `permission_mode` in global mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
|
|
88982
89893
|
* `forbiddenPlugins`) in project mode when the `devin` override authors them.
|
|
88983
89894
|
*
|
|
88984
89895
|
* The sibling `sandbox` block — which decides what a permitted command may
|
|
@@ -89086,6 +89997,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89086
89997
|
});
|
|
89087
89998
|
} 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.");
|
|
89088
89999
|
}
|
|
90000
|
+
applyPermissionModeOverride({
|
|
90001
|
+
permissionMode: config.devin?.permission_mode,
|
|
90002
|
+
patch,
|
|
90003
|
+
global,
|
|
90004
|
+
logger
|
|
90005
|
+
});
|
|
89089
90006
|
applyPluginGovernanceOverride({
|
|
89090
90007
|
override: config.devin,
|
|
89091
90008
|
settings,
|
|
@@ -89129,6 +90046,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89129
90046
|
if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
|
|
89130
90047
|
else logger?.warn(`Skipped Devin \`${key}\` on import: ${key === "forbiddenPlugins" ? "expected an array of strings" : "expected an array of plugin sources (strings or objects)"}.`);
|
|
89131
90048
|
}
|
|
90049
|
+
const permissionMode = importPermissionMode({
|
|
90050
|
+
value: settings.permission_mode,
|
|
90051
|
+
isGlobalFile: toPosixPath(this.getRelativeDirPath()) === toPosixPath(DevinPermissions.getSettablePaths({ global: true }).relativeDirPath),
|
|
90052
|
+
logger
|
|
90053
|
+
});
|
|
90054
|
+
if (permissionMode !== void 0) devinOverride.permission_mode = permissionMode;
|
|
89132
90055
|
const result = { ...config };
|
|
89133
90056
|
if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
|
|
89134
90057
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
@@ -89145,6 +90068,48 @@ const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
|
|
|
89145
90068
|
"forbiddenPlugins"
|
|
89146
90069
|
];
|
|
89147
90070
|
/**
|
|
90071
|
+
* Decide what an imported `permission_mode` contributes to the `devin`
|
|
90072
|
+
* override. It is lifted from the user config only: unlike `sandbox`, a
|
|
90073
|
+
* project value Devin ignores is skipped with a warning rather than surfaced,
|
|
90074
|
+
* so it cannot slip into a `--global` generate later. `null` is Devin's
|
|
90075
|
+
* spelling of the default and carries nothing to keep; any other value the
|
|
90076
|
+
* override schema would reject is dropped with a warning so the next generate
|
|
90077
|
+
* cannot fail. `dangerous` is lifted but named, as on generate.
|
|
90078
|
+
*/
|
|
90079
|
+
function importPermissionMode({ value, isGlobalFile, logger }) {
|
|
90080
|
+
if (value === void 0 || value === null) return void 0;
|
|
90081
|
+
if (!isGlobalFile) {
|
|
90082
|
+
logger?.warn("Skipped Devin `permission_mode` on import: Devin reads it from the user config only, so the project config's value has no effect. Import with --global to lift it.");
|
|
90083
|
+
return;
|
|
90084
|
+
}
|
|
90085
|
+
const parsed = DevinPermissionModeSchema.safeParse(value);
|
|
90086
|
+
if (!parsed.success) {
|
|
90087
|
+
logger?.warn(`Skipped Devin \`permission_mode\` ${quoteValueForWarning(value)} on import: expected one of ${DevinPermissionModeSchema.options.join(", ")}.`);
|
|
90088
|
+
return;
|
|
90089
|
+
}
|
|
90090
|
+
if (parsed.data === "dangerous") logger?.warn("Imported Devin `permission_mode: \"dangerous\"`: every new local session starts with all permission prompts bypassed, and the next --global generate writes it back.");
|
|
90091
|
+
return parsed.data;
|
|
90092
|
+
}
|
|
90093
|
+
/**
|
|
90094
|
+
* Patch the `permission_mode` the `devin` override authors into the user
|
|
90095
|
+
* `config.json`. It sets the mode new local sessions start in, and Devin reads
|
|
90096
|
+
* it from the user config only, so in project mode it is dropped with a
|
|
90097
|
+
* warning. It is only ever written as authored — never derived from the
|
|
90098
|
+
* canonical rules — and `dangerous` is named in a warning, because every new
|
|
90099
|
+
* session then starts with all permission prompts bypassed.
|
|
90100
|
+
*
|
|
90101
|
+
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
90102
|
+
*/
|
|
90103
|
+
function applyPermissionModeOverride({ permissionMode, patch, global, logger }) {
|
|
90104
|
+
if (permissionMode === void 0) return;
|
|
90105
|
+
if (!global) {
|
|
90106
|
+
logger?.warn("Devin reads 'permission_mode' from the user config only, so the 'devin.permission_mode' override was dropped from the project config. Generate with --global to author it.");
|
|
90107
|
+
return;
|
|
90108
|
+
}
|
|
90109
|
+
patch.permission_mode = permissionMode;
|
|
90110
|
+
if (permissionMode === "dangerous") logger?.warn("Writing Devin `permission_mode: \"dangerous\"` to the user config: every new local session then starts with all permission prompts bypassed.");
|
|
90111
|
+
}
|
|
90112
|
+
/**
|
|
89148
90113
|
* Patch the plugin governance lists the `devin` override authors into the
|
|
89149
90114
|
* project `.devin/config.json`. Each authored list replaces the file's list
|
|
89150
90115
|
* whole; a list it does not author is left as the file holds it. Devin reads these
|
|
@@ -95134,7 +96099,8 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
95134
96099
|
*
|
|
95135
96100
|
* Reasonix matches a rule's tool exactly against its registered tools, which
|
|
95136
96101
|
* are snake_case (`read_file`, `write_file`, `grep`, `glob`, `web_fetch`,
|
|
95137
|
-
* `web_search`, `notebook_edit`, `task`
|
|
96102
|
+
* `web_search`, `notebook_edit`, and `task` up to v2.57.x / `spawn_agents` from
|
|
96103
|
+
* v2.58.0). Only `Bash`, `Edit` (every file
|
|
95138
96104
|
* writer: `write_file`, `edit_file`, `multi_edit`, `notebook_edit`, ...),
|
|
95139
96105
|
* `Browser` and `Computer` are Claude-Code-style family aliases
|
|
95140
96106
|
* (`canonicalRuleTool` / `groupOf`), so a `Read(...)` or `Write(...)` rule
|
|
@@ -95157,6 +96123,21 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
|
95157
96123
|
agent: "task"
|
|
95158
96124
|
};
|
|
95159
96125
|
/**
|
|
96126
|
+
* Reasonix tool names a canonical category writes beside its primary name, for
|
|
96127
|
+
* a tool Reasonix renamed. studio-v2.58.0 replaced the `task` delegation family
|
|
96128
|
+
* with `spawn_agents` (commit ad2c96347), while the 1.x line and v2 up to
|
|
96129
|
+
* v2.57.x still register `task`. When Reasonix loads the config file it checks
|
|
96130
|
+
* only a rule's syntax: a rule naming a tool the running build lacks matches
|
|
96131
|
+
* nothing (v2.34.0+ reports it as dormant) and is not rejected. (The v2 Web UI
|
|
96132
|
+
* permissions editor refuses to newly add such a rule with `ErrUnknownTool`,
|
|
96133
|
+
* but keeps the ones already in the file.) So writing both names keeps an
|
|
96134
|
+
* `agent` rule in force on old and new builds alike. Both names are read back
|
|
96135
|
+
* to the same category.
|
|
96136
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.58.0/internal/runtime/delegation/spawn.go
|
|
96137
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.57.0/internal/runtime/delegation/task.go
|
|
96138
|
+
*/
|
|
96139
|
+
const CANONICAL_TO_EXTRA_REASONIX_TOOL_NAMES = { agent: ["spawn_agents"] };
|
|
96140
|
+
/**
|
|
95160
96141
|
* The Claude-Code-style spellings rulesync emitted before it followed the
|
|
95161
96142
|
* Reasonix tool vocabulary. None of them reaches a Reasonix tool, so they are
|
|
95162
96143
|
* read back on import (a file generated by an older rulesync still imports)
|
|
@@ -95179,7 +96160,8 @@ const CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES = {
|
|
|
95179
96160
|
*/
|
|
95180
96161
|
const REASONIX_TO_CANONICAL_TOOL_NAMES = {
|
|
95181
96162
|
...invertRecord(CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES),
|
|
95182
|
-
...invertRecord(CANONICAL_TO_REASONIX_TOOL_NAMES)
|
|
96163
|
+
...invertRecord(CANONICAL_TO_REASONIX_TOOL_NAMES),
|
|
96164
|
+
...Object.fromEntries(Object.entries(CANONICAL_TO_EXTRA_REASONIX_TOOL_NAMES).flatMap(([canonical, names]) => names.map((name) => [name, canonical])))
|
|
95183
96165
|
};
|
|
95184
96166
|
/** Every Reasonix tool name a canonical category owns on generate. */
|
|
95185
96167
|
function reasonixToolNamesOf(canonical) {
|
|
@@ -95187,14 +96169,20 @@ function reasonixToolNamesOf(canonical) {
|
|
|
95187
96169
|
record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
|
|
95188
96170
|
key: canonical
|
|
95189
96171
|
});
|
|
95190
|
-
const current =
|
|
95191
|
-
return legacy === void 0 ?
|
|
96172
|
+
const current = toReasonixToolNames(canonical);
|
|
96173
|
+
return legacy === void 0 ? current : [...current, legacy];
|
|
95192
96174
|
}
|
|
95193
|
-
|
|
95194
|
-
|
|
96175
|
+
/** The Reasonix tool names a canonical category's rules are written under. */
|
|
96176
|
+
function toReasonixToolNames(canonical) {
|
|
96177
|
+
const primary = lookupOwn({
|
|
95195
96178
|
record: CANONICAL_TO_REASONIX_TOOL_NAMES,
|
|
95196
96179
|
key: canonical
|
|
95197
96180
|
}) ?? canonical;
|
|
96181
|
+
const extra = lookupOwn({
|
|
96182
|
+
record: CANONICAL_TO_EXTRA_REASONIX_TOOL_NAMES,
|
|
96183
|
+
key: canonical
|
|
96184
|
+
});
|
|
96185
|
+
return extra === void 0 ? [primary] : [primary, ...extra];
|
|
95198
96186
|
}
|
|
95199
96187
|
function toCanonicalToolName$2(reasonixName) {
|
|
95200
96188
|
return lookupOwn({
|
|
@@ -95602,6 +96590,17 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
95602
96590
|
});
|
|
95603
96591
|
if (droppedReadDenyEntries.length > 0) logger.warn(`Permissions feature manages '${REASONIX_READ_TOOL_NAME}' tool and will overwrite ${droppedReadDenyEntries.length} existing read deny entries (possibly from ignore feature). Permissions take precedence.`);
|
|
95604
96592
|
}
|
|
96593
|
+
warnOnLoosenedAgentEntries({
|
|
96594
|
+
existingPermissions,
|
|
96595
|
+
managedToolNames,
|
|
96596
|
+
generated: {
|
|
96597
|
+
allow: [...allow, ...rawAllow],
|
|
96598
|
+
ask: [...ask, ...rawAsk],
|
|
96599
|
+
deny: [...deny, ...rawDeny]
|
|
96600
|
+
},
|
|
96601
|
+
relativeFilePath: relativeFilePathForLog,
|
|
96602
|
+
logger
|
|
96603
|
+
});
|
|
95605
96604
|
const mergedPermissions = { ...existingPermissions };
|
|
95606
96605
|
setOrDeleteEntries(mergedPermissions, "allow", [
|
|
95607
96606
|
...preservedAllow,
|
|
@@ -95777,6 +96776,55 @@ function preserveExistingEntries({ existingPermissions, managedToolNames, rawOwn
|
|
|
95777
96776
|
deny
|
|
95778
96777
|
};
|
|
95779
96778
|
}
|
|
96779
|
+
/**
|
|
96780
|
+
* `spawn_agents` became a managed name with Reasonix v2.58.0 support, so a
|
|
96781
|
+
* stricter rule the user wrote for it by hand (e.g. a v2.58-era
|
|
96782
|
+
* `deny = ["spawn_agents"]` next to an older `agent: allow` source) would be
|
|
96783
|
+
* silently replaced by a looser one. Name every such loosening.
|
|
96784
|
+
*/
|
|
96785
|
+
function warnOnLoosenedAgentEntries({ existingPermissions, managedToolNames, generated, relativeFilePath, logger }) {
|
|
96786
|
+
if (!logger || !managedToolNames.has("spawn_agents")) return;
|
|
96787
|
+
const loosened = findLoosenedEntries({
|
|
96788
|
+
existingPermissions,
|
|
96789
|
+
toolNames: new Set(reasonixToolNamesOf("agent")),
|
|
96790
|
+
generated
|
|
96791
|
+
});
|
|
96792
|
+
if (loosened.length === 0) return;
|
|
96793
|
+
logger.warn(`Permissions feature manages the Reasonix delegation tools ('task', 'spawn_agents') through the 'agent' category and will replace ${loosened.length} stricter existing entries in ${relativeFilePath}: ${loosened.join(", ")}. To keep them, tighten the 'agent' category in ${RULESYNC_RELATIVE_DIR_PATH}/${RULESYNC_PERMISSIONS_FILE_NAME} or move them to 'reasonix.rawDeny' / 'reasonix.rawAsk'.`);
|
|
96794
|
+
}
|
|
96795
|
+
const PERMISSION_STRICTNESS = {
|
|
96796
|
+
allow: 0,
|
|
96797
|
+
ask: 1,
|
|
96798
|
+
deny: 2
|
|
96799
|
+
};
|
|
96800
|
+
/**
|
|
96801
|
+
* Existing entries for `toolNames` whose action is stricter than the one the
|
|
96802
|
+
* generated lists give the same entry, formatted `<list> "<entry>"`. An entry
|
|
96803
|
+
* the generated lists drop counts as no stricter than `allow`.
|
|
96804
|
+
*/
|
|
96805
|
+
function findLoosenedEntries({ existingPermissions, toolNames, generated }) {
|
|
96806
|
+
const generatedStrictness = /* @__PURE__ */ new Map();
|
|
96807
|
+
for (const list of [
|
|
96808
|
+
"allow",
|
|
96809
|
+
"ask",
|
|
96810
|
+
"deny"
|
|
96811
|
+
]) for (const entry of generated[list]) {
|
|
96812
|
+
const key = normalizeToolPatternEntry(entry);
|
|
96813
|
+
generatedStrictness.set(key, Math.max(generatedStrictness.get(key) ?? 0, PERMISSION_STRICTNESS[list]));
|
|
96814
|
+
}
|
|
96815
|
+
const loosened = [];
|
|
96816
|
+
for (const list of ["ask", "deny"]) for (const entry of toStringArray$1(existingPermissions[list])) {
|
|
96817
|
+
if (isReasonixExactCommandEntry(entry)) continue;
|
|
96818
|
+
if (!toolNames.has(parseToolPatternEntry(entry).toolName)) continue;
|
|
96819
|
+
const key = normalizeToolPatternEntry(entry);
|
|
96820
|
+
if ((generatedStrictness.get(key) ?? 0) < PERMISSION_STRICTNESS[list]) loosened.push(`${list} "${entry}"`);
|
|
96821
|
+
}
|
|
96822
|
+
return loosened;
|
|
96823
|
+
}
|
|
96824
|
+
/** Spell an entry the way rulesync writes it, so `name(*)` equals `name`. */
|
|
96825
|
+
function normalizeToolPatternEntry(entry) {
|
|
96826
|
+
return buildToolPatternEntry(parseToolPatternEntry(entry));
|
|
96827
|
+
}
|
|
95780
96828
|
/** Sort, dedupe and set a permissions array key, deleting it when empty. */
|
|
95781
96829
|
function setOrDeleteEntries(table, key, entries) {
|
|
95782
96830
|
const merged = uniq(entries.toSorted());
|
|
@@ -95790,22 +96838,19 @@ function convertRulesyncToReasonixPermissions(config) {
|
|
|
95790
96838
|
const allow = [];
|
|
95791
96839
|
const ask = [];
|
|
95792
96840
|
const deny = [];
|
|
95793
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
95794
|
-
const
|
|
95795
|
-
|
|
95796
|
-
|
|
95797
|
-
|
|
95798
|
-
|
|
95799
|
-
|
|
95800
|
-
|
|
95801
|
-
|
|
95802
|
-
|
|
95803
|
-
|
|
95804
|
-
|
|
95805
|
-
|
|
95806
|
-
break;
|
|
95807
|
-
case "deny": deny.push(entry);
|
|
95808
|
-
}
|
|
96841
|
+
for (const [category, rules] of Object.entries(config.permission)) for (const reasonixToolName of toReasonixToolNames(category)) for (const [pattern, action] of Object.entries(rules)) {
|
|
96842
|
+
const entry = buildToolPatternEntry({
|
|
96843
|
+
toolName: reasonixToolName,
|
|
96844
|
+
pattern
|
|
96845
|
+
});
|
|
96846
|
+
switch (action) {
|
|
96847
|
+
case "allow":
|
|
96848
|
+
allow.push(entry);
|
|
96849
|
+
break;
|
|
96850
|
+
case "ask":
|
|
96851
|
+
ask.push(entry);
|
|
96852
|
+
break;
|
|
96853
|
+
case "deny": deny.push(entry);
|
|
95809
96854
|
}
|
|
95810
96855
|
}
|
|
95811
96856
|
return {
|
|
@@ -96032,7 +97077,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96032
97077
|
validate: true
|
|
96033
97078
|
});
|
|
96034
97079
|
}
|
|
96035
|
-
toRulesyncPermissions() {
|
|
97080
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
96036
97081
|
const constructor = this.constructor;
|
|
96037
97082
|
let settings;
|
|
96038
97083
|
try {
|
|
@@ -96046,9 +97091,12 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96046
97091
|
} catch (error) {
|
|
96047
97092
|
throw new Error(`Failed to parse ${constructor.getToolLabel()} VS Code settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
96048
97093
|
}
|
|
96049
|
-
const rules = {
|
|
96050
|
-
|
|
96051
|
-
|
|
97094
|
+
const rules = importVscodeCommandLists({
|
|
97095
|
+
allowed: toStringArray$1(settings[constructor.getAllowedCommandsKey()]),
|
|
97096
|
+
denied: toStringArray$1(settings[constructor.getDeniedCommandsKey()]),
|
|
97097
|
+
toolLabel: constructor.getToolLabel(),
|
|
97098
|
+
logger
|
|
97099
|
+
});
|
|
96052
97100
|
const permission = {};
|
|
96053
97101
|
if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
|
|
96054
97102
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
@@ -101372,6 +102420,9 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
101372
102420
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
101373
102421
|
toolTarget;
|
|
101374
102422
|
global;
|
|
102423
|
+
sweepsThroughInRootLinks() {
|
|
102424
|
+
return this.global;
|
|
102425
|
+
}
|
|
101375
102426
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
101376
102427
|
super({
|
|
101377
102428
|
outputRoot,
|
|
@@ -104057,6 +105108,6 @@ const CHECKS_IMPORT_STRATEGY = {
|
|
|
104057
105108
|
missingLabel: "check files",
|
|
104058
105109
|
createdLabel: "check files"
|
|
104059
105110
|
};
|
|
104060
|
-
export {
|
|
105111
|
+
export { RulesyncRule as $, RULESYNC_RULES_RELATIVE_DIR_PATH as $n, getFileSizeAndExecutableBit as $t, CODEBUDDY_LOCAL_RULE_FILE_NAME as A, ToolTargetSchema as An, fallbackLogger as At, CLAUDECODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Bn, assertDirectoryIfExists as Bt, LETTACODE_DIR as C, toPosixPath as Cn, SourceEntrySchema as Ct, COMMANDCODE_DIR as D, ALL_TOOL_TARGETS as Dn, ConsoleLogger as Dt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as E, mapWithConcurrency as En, findControlCharacter as Et, sweepRefusalReason as F, FEATURE_SOURCE_TREE_ENTRIES as Fn, CLIError as Ft, toInstructionBudgetFile as G, RULESYNC_IGNORE_RELATIVE_FILE_PATH as Gn, assertWritablePathInsideRoot as Gt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as H, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Hn, assertNoSymlinkBelowRoot as Ht, CODEXCLI_BASH_RULES_FILE_NAME as I, MAX_FILE_SIZE as In, ErrorCodes as It, getLocalSkillDirNames as J, RULESYNC_MCP_SCHEMA_URL as Jn, directoryExists as Jt, classifyRemoteName as K, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Kn, checkPathTraversal as Kt, CODEXCLI_DIR as L, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as Ln, parseJsonc as Lt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as M, ALL_FEATURES_WITH_WILDCARD as Mn, withFallbackLoggerTarget as Mt, caseFoldIdentity as N, DEPRECATED_FEATURE_REPLACEMENTS as Nn, resetRunWarningState as Nt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as O, ALL_TOOL_TARGETS_WITH_WILDCARD as On, JsonLogger as Ot, groupSpellingsByCaseFoldedIdentity as P, CURATED_RULES_FEATURE_SUBDIR as Pn, withWarnOnceScope as Pt, RulesyncSkillFrontmatterSchema as Q, RULESYNC_RELATIVE_DIR_PATH as Qn, findFilesByGlobs as Qt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as R, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Rn, PathOutsideRootError as Rt, QWENCODE_LOCAL_RULE_FILE_NAME as S, splitPathSegments as Sn, GITIGNORE_DESTINATION_KEY as St, FACTORYDROID_DIR as T, writeFileContent as Tn, getSourceTransport as Tt, CLAUDECODE_SKILLS_DIR_PATH as U, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Un, assertPathResolvesInsideRoot as Ut, CLAUDECODE_MEMORIES_DIR_NAME as V, RULESYNC_CONFIG_SCHEMA_URL as Vn, assertLandingOutsideVcsMetadata as Vt, findProjectInstructionBudgetFindings as W, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Wn, assertTreeContainsNoSymlinks as Wt, RulesyncSubagentFrontmatterSchema as X, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Xn, fileExists as Xt, RulesyncSubagent as Y, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Yn, ensureDir as Yt, RulesyncSkill as Z, RULESYNC_PERMISSIONS_SCHEMA_URL as Zn, fileExistsStrict as Zt, SkillsProcessor as _, removeFileWithoutFollowingSymlinks as _n, mergeInputRootConfigs as _t, convertFromTool as a, listDirectoryEntryNames as an, formatMegabytes as ar, getRulesyncSourceCandidates as at, REASONIX_LOCAL_RULE_FILE_NAME as b, resolvedRelativePath as bn, CONFLICTING_TARGET_PAIRS as bt, ELLIPSIS_WIDTH as c, normalizePosixDirPath as cn, hasEnclosingMarkOutsideKeycap as cr, RulesyncCommandFrontmatterSchema as ct, McpProcessor as d, readFileContent as dn, stripControlCharactersKeepingLineFeeds as dr, stringifyFrontmatter as dt, getHomeDirectory as en, RULESYNC_SKILLS_RELATIVE_DIR_PATH as er, RulesyncRuleFrontmatterSchema as et, IgnoreProcessor as f, readFileContentOrNull as fn, stripControlCharactersKeepingLineFeedsAndTabs as fr, loadYaml as ft, SubagentsProcessor as g, removeFileStrict as gn, ConfigResolver as gt, RulesProcessor as h, removeFile as hn, SKILL_FILE_NAME as ht, inspectInputRoots as i, isSymlink as in, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as ir, RulesyncHooks as it, AUGMENTCODE_DIR as j, ALL_FEATURES as jn, warnOnConflictingFlags as jt, CODEBUDDY_DIR as k, PACKAGING_TOOL_TARGETS as kn, WarningCollectingLogger as kt, displayWidthOf as l, pathEscapesRoot as ln, quoteForLog as lr, RulesyncCheck as lt, ChecksProcessor as m, removeDirectoryStrict as mn, SHARED_USER_MANAGED_CONFIG_PATHS as mt, formatSourceLoadFailure as n, isFileSystemError as nn, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as nr, RulesyncMcp as nt, getProcessorRegistryEntry as o, listFilePathsRecursively as on, formatError as or, resolveRulesyncSourceWritePath as ot, HooksProcessor as p, removeDirectory as pn, stripHiddenCharacters as pr, isRecord as pt, hasWindowsFoldableName as q, RULESYNC_MCP_RELATIVE_FILE_PATH as qn, createTempDirectory as qt, generate as r, isRootDirPath as rn, RULESYNC_USER_CONFIG_DIR_NAME as rr, RulesyncIgnore as rt, parseCommaSeparatedList as s, listSubdirectoryNames as sn, truncateText as sr, RulesyncCommand as st, importFromTool as t, isFileNotFoundError as tn, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as tr, RulesyncPermissions as tt, shortenToWidth as u, posixRelativePathEscapesRoot as un, stripControlCharacters as ur, RulesyncCheckFrontmatterSchema as ut, CRUSH_LOCAL_RULE_FILE_NAME as v, removeTempDirectory as vn, parseConfigFileContent as vt, LETTACODE_SETTINGS_LOCAL_FILE_NAME as w, writeFileBuffer as wn, assertTargetsFeaturesExclusive as wt, QWENCODE_DIR as x, runWithDirectoryRollback as xn, ConfigFileSchema as xt, CommandsProcessor as y, resolvePath as yn, resolveEffectiveInputRoots as yt, CLAUDECODE_DIR as z, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as zn, applyFileMode as zt };
|
|
104061
105112
|
|
|
104062
|
-
//# sourceMappingURL=import-
|
|
105113
|
+
//# sourceMappingURL=import-Cu5o6uKO.js.map
|