rulesync 43.7.0 → 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-CfxyzbvB.cjs → import-C5ETusOs.cjs} +1209 -174
- package/dist/{import-tr3jHcG2.js → import-Cu5o6uKO.js} +1189 -172
- package/dist/{import-tr3jHcG2.js.map → import-Cu5o6uKO.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -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";
|
|
@@ -29948,6 +30180,9 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29948
30180
|
global: this.global
|
|
29949
30181
|
});
|
|
29950
30182
|
}
|
|
30183
|
+
sweepsThroughInRootLinks() {
|
|
30184
|
+
return this.global;
|
|
30185
|
+
}
|
|
29951
30186
|
/**
|
|
29952
30187
|
* The tool's skills roots that exist on disk, each paired with its absolute
|
|
29953
30188
|
* path and vetted as writable inside this run's output root. Shared by the
|
|
@@ -29977,6 +30212,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29977
30212
|
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
29978
30213
|
continue;
|
|
29979
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
|
+
}
|
|
29980
30224
|
existingRoots.push({
|
|
29981
30225
|
root,
|
|
29982
30226
|
skillsDirPath
|
|
@@ -33203,11 +33447,17 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33203
33447
|
*/
|
|
33204
33448
|
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
33205
33449
|
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
33206
|
-
const
|
|
33450
|
+
const agentsDirFullPath = join(outputRoot, agentsDirPath);
|
|
33451
|
+
const writeRefused = await writeRefusalReason({
|
|
33207
33452
|
rootPath: outputRoot,
|
|
33208
|
-
targetPath: join(
|
|
33209
|
-
});
|
|
33210
|
-
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));
|
|
33211
33461
|
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
33212
33462
|
outputRoot,
|
|
33213
33463
|
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
@@ -33223,8 +33473,8 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33223
33473
|
global
|
|
33224
33474
|
})),
|
|
33225
33475
|
existingAgentFileNames,
|
|
33226
|
-
deleteOrphans,
|
|
33227
|
-
frozen:
|
|
33476
|
+
deleteOrphans: deleteOrphans && !sweepRefused,
|
|
33477
|
+
frozen: writeRefused,
|
|
33228
33478
|
global,
|
|
33229
33479
|
logger
|
|
33230
33480
|
});
|
|
@@ -37342,6 +37592,9 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
37342
37592
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
37343
37593
|
toolTarget;
|
|
37344
37594
|
global;
|
|
37595
|
+
sweepsThroughInRootLinks() {
|
|
37596
|
+
return this.global;
|
|
37597
|
+
}
|
|
37345
37598
|
deleteOrphans;
|
|
37346
37599
|
getFactory;
|
|
37347
37600
|
/**
|
|
@@ -38063,6 +38316,39 @@ function warnAndSkipMcpServer({ toolName, serverName, reason, logger }) {
|
|
|
38063
38316
|
return null;
|
|
38064
38317
|
}
|
|
38065
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
|
+
/**
|
|
38066
38352
|
* The `tools` map Kilo and OpenCode keep beside `mcp` is keyed `<server>_<tool>`
|
|
38067
38353
|
* and reaches servers `mcp` does not list — ones the global config or a
|
|
38068
38354
|
* marketplace defines — so entries naming no listed
|
|
@@ -38624,7 +38910,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
38624
38910
|
const existingMcp = readExistingKiloMcpEntries(fileContent);
|
|
38625
38911
|
const parsedExisting = parse$1(fileContent || "{}");
|
|
38626
38912
|
const existingDocument = isRecord(parsedExisting) ? parsedExisting : {};
|
|
38627
|
-
const mcpServers =
|
|
38913
|
+
const mcpServers = skipServersWithEmptyEnabledTools({
|
|
38914
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
38915
|
+
toolName: "Kilo",
|
|
38916
|
+
logger
|
|
38917
|
+
});
|
|
38628
38918
|
const { mcp: convertedMcp, toolPermissions } = convertToKiloFormat(resolveKiloEnvVarRefs({
|
|
38629
38919
|
mcpServers,
|
|
38630
38920
|
global,
|
|
@@ -38753,7 +39043,8 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
|
38753
39043
|
"timeout",
|
|
38754
39044
|
"oauth",
|
|
38755
39045
|
"protocol",
|
|
38756
|
-
"codemode"
|
|
39046
|
+
"codemode",
|
|
39047
|
+
"pool"
|
|
38757
39048
|
];
|
|
38758
39049
|
const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
38759
39050
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
@@ -38916,7 +39207,7 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
|
38916
39207
|
* - env -> environment
|
|
38917
39208
|
* - disabled -> enabled (inverted)
|
|
38918
39209
|
* - enabledTools/disabledTools -> top-level tools map (with server name prefix)
|
|
38919
|
-
* - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
|
|
39210
|
+
* - OpenCode-supported extras (timeout, oauth, protocol, codemode, pool) -> passed through verbatim
|
|
38920
39211
|
*/
|
|
38921
39212
|
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, configShape, logger) {
|
|
38922
39213
|
const serverRecord = serverConfig;
|
|
@@ -39156,7 +39447,11 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
39156
39447
|
if (fileContent) relativeFilePath = this.layout.jsonFileName;
|
|
39157
39448
|
}
|
|
39158
39449
|
const transformedServers = convertEnvVarRefsToToolFormat({
|
|
39159
|
-
mcpServers:
|
|
39450
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
39451
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
39452
|
+
toolName: "OpenCode",
|
|
39453
|
+
logger
|
|
39454
|
+
}),
|
|
39160
39455
|
replacement: "{env:$1}"
|
|
39161
39456
|
});
|
|
39162
39457
|
const existingJson = parse$1(fileContent || "{}");
|
|
@@ -48752,6 +49047,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
48752
49047
|
language;
|
|
48753
49048
|
deriveSubprojectPathFromGlobs;
|
|
48754
49049
|
global;
|
|
49050
|
+
sweepsThroughInRootLinks() {
|
|
49051
|
+
return this.global;
|
|
49052
|
+
}
|
|
48755
49053
|
getFactory;
|
|
48756
49054
|
skills;
|
|
48757
49055
|
featureOptions;
|
|
@@ -51336,12 +51634,29 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
51336
51634
|
};
|
|
51337
51635
|
}
|
|
51338
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
|
+
}
|
|
51339
51654
|
toRulesyncCheck() {
|
|
51340
51655
|
const spec = HermesCheckSpecSchema.parse(JSON.parse(this.fileContent));
|
|
51341
51656
|
return new RulesyncCheck({
|
|
51342
51657
|
outputRoot: ".",
|
|
51343
51658
|
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
51344
|
-
relativeFilePath: `${spec.slug}.md`,
|
|
51659
|
+
relativeFilePath: `${this.importedCheckStem(spec.slug)}.md`,
|
|
51345
51660
|
frontmatter: {
|
|
51346
51661
|
targets: ["*"],
|
|
51347
51662
|
description: spec.description,
|
|
@@ -51849,6 +52164,9 @@ const checksProcessorToolTargetsGlobal = allToolTargetKeys.filter((target) => {
|
|
|
51849
52164
|
var ChecksProcessor = class extends FeatureProcessor {
|
|
51850
52165
|
toolTarget;
|
|
51851
52166
|
global;
|
|
52167
|
+
sweepsThroughInRootLinks() {
|
|
52168
|
+
return this.global;
|
|
52169
|
+
}
|
|
51852
52170
|
getFactory;
|
|
51853
52171
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
51854
52172
|
super({
|
|
@@ -54160,6 +54478,10 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
|
54160
54478
|
field: "tool",
|
|
54161
54479
|
value: h.tool
|
|
54162
54480
|
});
|
|
54481
|
+
for (const { canonical, tool } of converterConfig.stringPassthroughFields ?? []) if (canonical === "if" && h[tool] !== "") fields.push({
|
|
54482
|
+
field: "if",
|
|
54483
|
+
value: h[tool]
|
|
54484
|
+
});
|
|
54163
54485
|
return fields;
|
|
54164
54486
|
}
|
|
54165
54487
|
/**
|
|
@@ -62936,10 +63258,12 @@ function emittedEventsWithHook(params) {
|
|
|
62936
63258
|
function converterCapabilities(config) {
|
|
62937
63259
|
if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
|
|
62938
63260
|
const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
|
|
63261
|
+
const passesThroughIf = (config.stringPassthroughFields ?? []).some((field) => field.canonical === "if") && !(config.skippedWhenFields ?? []).includes("if");
|
|
62939
63262
|
return {
|
|
62940
63263
|
supportedEvents: config.supportedEvents,
|
|
62941
63264
|
supportedHookTypes: [...config.supportedHookTypes],
|
|
62942
|
-
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
|
|
63265
|
+
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {},
|
|
63266
|
+
...passesThroughIf ? { supportsPerHookIf: true } : {}
|
|
62943
63267
|
};
|
|
62944
63268
|
}
|
|
62945
63269
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
@@ -63584,6 +63908,9 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
63584
63908
|
var HooksProcessor = class extends FeatureProcessor {
|
|
63585
63909
|
toolTarget;
|
|
63586
63910
|
global;
|
|
63911
|
+
sweepsThroughInRootLinks() {
|
|
63912
|
+
return this.global;
|
|
63913
|
+
}
|
|
63587
63914
|
preserveUnownedHooks;
|
|
63588
63915
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, preserveUnownedHooks = false, logger }) {
|
|
63589
63916
|
super({
|
|
@@ -63662,7 +63989,8 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63662
63989
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
63663
63990
|
const config = rulesyncHooks.getJson();
|
|
63664
63991
|
const sharedHooks = config.hooks;
|
|
63665
|
-
const
|
|
63992
|
+
const overrideKey = HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget;
|
|
63993
|
+
const overrideHooks = config[overrideKey]?.hooks ?? {};
|
|
63666
63994
|
const effectiveHooks = {
|
|
63667
63995
|
...sharedHooks,
|
|
63668
63996
|
...overrideHooks
|
|
@@ -63700,7 +64028,10 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63700
64028
|
if (eventsWithUnsupportedMatcher.length > 0) this.logger.warn(`Skipped matcher hook(s) for ${this.toolTarget} (not supported): ${eventsWithUnsupportedMatcher.join(", ")}`);
|
|
63701
64029
|
const toolHooks = await factory.class.fromRulesyncHooks({
|
|
63702
64030
|
outputRoot: this.outputRoot,
|
|
63703
|
-
rulesyncHooks
|
|
64031
|
+
rulesyncHooks: factory.supportsPerHookIf ? rulesyncHooks : this.withoutConditionalHooks({
|
|
64032
|
+
rulesyncHooks,
|
|
64033
|
+
overrideKey
|
|
64034
|
+
}),
|
|
63704
64035
|
validate: true,
|
|
63705
64036
|
global: this.global,
|
|
63706
64037
|
preserveUnowned: this.preserveUnownedHooks,
|
|
@@ -63723,6 +64054,53 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63723
64054
|
return result;
|
|
63724
64055
|
}
|
|
63725
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
|
+
/**
|
|
63726
64104
|
* Warns about canonical per-hook fields that this target's hooks format
|
|
63727
64105
|
* cannot express, so a hook does not silently change behavior on generation.
|
|
63728
64106
|
* Only canonical definitions are considered: a tool-native field inside an
|
|
@@ -63967,6 +64345,35 @@ var BobIgnore = class extends PlainToolIgnore {
|
|
|
63967
64345
|
};
|
|
63968
64346
|
}
|
|
63969
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
|
+
}
|
|
63970
64377
|
// src/features/ignore/claudecode-ignore.ts
|
|
63971
64378
|
const DEFAULT_FILE_MODE = "shared";
|
|
63972
64379
|
/**
|
|
@@ -64025,9 +64432,12 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
64025
64432
|
}
|
|
64026
64433
|
toRulesyncIgnore() {
|
|
64027
64434
|
const fileContent = this.patterns.map((pattern) => {
|
|
64028
|
-
if (isReadDenyEntry(pattern)) return
|
|
64435
|
+
if (isReadDenyEntry(pattern)) return claudeReadDenyPathToIgnorePattern({
|
|
64436
|
+
path: pattern.slice(5, -1),
|
|
64437
|
+
toolLabel: "Claude Code"
|
|
64438
|
+
});
|
|
64029
64439
|
return pattern;
|
|
64030
|
-
}).filter((pattern) => pattern.length > 0).join("\n");
|
|
64440
|
+
}).filter((pattern) => pattern !== null && pattern.length > 0).join("\n");
|
|
64031
64441
|
return new RulesyncIgnore({
|
|
64032
64442
|
outputRoot: this.outputRoot,
|
|
64033
64443
|
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
@@ -64302,7 +64712,7 @@ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
|
64302
64712
|
const paths = this.getSettablePaths({ global });
|
|
64303
64713
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
64304
64714
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
64305
|
-
const managedPatterns =
|
|
64715
|
+
const managedPatterns = patterns.filter((pattern, index) => patterns.lastIndexOf(pattern) === index);
|
|
64306
64716
|
return new FirebenderIgnore({
|
|
64307
64717
|
outputRoot,
|
|
64308
64718
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -64957,7 +65367,10 @@ const asClaudeStyleSettings = (document) => {
|
|
|
64957
65367
|
};
|
|
64958
65368
|
/** The pattern of a `read_file(...)` (or legacy `Read(...)`) deny, else null. */
|
|
64959
65369
|
const readDenyPatternOf = (entry) => {
|
|
64960
|
-
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
|
+
});
|
|
64961
65374
|
return null;
|
|
64962
65375
|
};
|
|
64963
65376
|
/**
|
|
@@ -65297,6 +65710,9 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
65297
65710
|
getFactory;
|
|
65298
65711
|
featureOptions;
|
|
65299
65712
|
global;
|
|
65713
|
+
sweepsThroughInRootLinks() {
|
|
65714
|
+
return this.global;
|
|
65715
|
+
}
|
|
65300
65716
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$1, global = false, dryRun = false, logger, featureOptions }) {
|
|
65301
65717
|
super({
|
|
65302
65718
|
outputRoot,
|
|
@@ -65630,30 +66046,71 @@ function isEmptyValue(value) {
|
|
|
65630
66046
|
if (Array.isArray(value)) return value.length === 0;
|
|
65631
66047
|
return isPlainObject(value) && Object.keys(value).length === 0;
|
|
65632
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
|
+
}
|
|
65633
66070
|
/**
|
|
65634
|
-
* Antigravity uses `serverUrl` (not the canonical `url`) for
|
|
66071
|
+
* Antigravity uses `serverUrl` (not the canonical `url`) for remote MCP
|
|
65635
66072
|
* servers. Convert the canonical shape into Antigravity's on the way out, and
|
|
65636
66073
|
* back to canonical on the way in.
|
|
65637
66074
|
*
|
|
65638
66075
|
* Antigravity documents no `type` field: the transport follows from `command`
|
|
65639
|
-
* (stdio) or `serverUrl` (Streamable HTTP
|
|
66076
|
+
* (stdio) or `serverUrl` (Streamable HTTP), and `agy plugin validate`
|
|
65640
66077
|
* reports the canonical `type` as an unknown field, so it is not written. A
|
|
65641
66078
|
* remote server that carries `command`, `args` or `env` was also observed to be
|
|
65642
66079
|
* refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
|
|
65643
66080
|
* not documented), so an empty `args` / `env` left on a `serverUrl` server is
|
|
65644
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`.
|
|
65645
66086
|
* https://antigravity.google/docs/mcp
|
|
65646
66087
|
*/
|
|
65647
|
-
function toAntigravityMcpServers(servers) {
|
|
66088
|
+
function toAntigravityMcpServers(servers, logger) {
|
|
65648
66089
|
const renamed = renameServerField(servers, "url", "serverUrl");
|
|
65649
66090
|
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
65650
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.`);
|
|
65651
66093
|
const isRemote = typeof config.serverUrl === "string";
|
|
65652
|
-
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
|
+
})];
|
|
65653
66099
|
}));
|
|
65654
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
|
+
*/
|
|
65655
66106
|
function toCanonicalMcpServers(servers) {
|
|
65656
|
-
|
|
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
|
+
}));
|
|
65657
66114
|
}
|
|
65658
66115
|
/**
|
|
65659
66116
|
* Parse `mcp_config.json` as JSONC and require a top-level object, so a file
|
|
@@ -65671,7 +66128,8 @@ function parseAntigravityMcpConfig(fileContent) {
|
|
|
65671
66128
|
* the IDE and the CLI.
|
|
65672
66129
|
*
|
|
65673
66130
|
* Antigravity writes a dedicated `mcp_config.json` whose `mcpServers` map lives
|
|
65674
|
-
* 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
|
|
65675
66133
|
* CLI's shared `settings.json`, this file is dedicated to MCP, so it is safely
|
|
65676
66134
|
* deletable. HTTP servers use `serverUrl` rather than the canonical `url`, so
|
|
65677
66135
|
* the field is translated in both directions.
|
|
@@ -65724,11 +66182,11 @@ var AntigravityMcp = class extends ToolMcp {
|
|
|
65724
66182
|
global
|
|
65725
66183
|
});
|
|
65726
66184
|
}
|
|
65727
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
66185
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
65728
66186
|
const paths = this.getSettablePaths({ global });
|
|
65729
66187
|
const newJson = {
|
|
65730
66188
|
...parseAntigravityMcpConfig(await readFileContentOrNull(join(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2)),
|
|
65731
|
-
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers())
|
|
66189
|
+
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers(), logger)
|
|
65732
66190
|
};
|
|
65733
66191
|
return new this({
|
|
65734
66192
|
outputRoot,
|
|
@@ -69397,7 +69855,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
69397
69855
|
ownedPaths: [["mcp"]],
|
|
69398
69856
|
logger
|
|
69399
69857
|
});
|
|
69400
|
-
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, {
|
|
69401
69863
|
...serverConfig,
|
|
69402
69864
|
...readCrushOnlyKeys({
|
|
69403
69865
|
serverName,
|
|
@@ -70357,31 +70819,6 @@ var DshMcp = class DshMcp extends ToolMcp {
|
|
|
70357
70819
|
};
|
|
70358
70820
|
// src/features/mcp/factorydroid-mcp.ts
|
|
70359
70821
|
const TOOL_NAME = "factorydroid";
|
|
70360
|
-
/**
|
|
70361
|
-
* Drop servers whose `enabledTools` allowlist is empty.
|
|
70362
|
-
*
|
|
70363
|
-
* Canonically an empty allowlist allows no tools, but Droid applies
|
|
70364
|
-
* `enabledTools` only "when the list is non-empty"
|
|
70365
|
-
* (https://docs.factory.com/harness/mcp), so writing `[]` would load every
|
|
70366
|
-
* tool the server reports. Skipping the server is the only form that does not
|
|
70367
|
-
* hand the model more tools than the canonical config allows.
|
|
70368
|
-
*/
|
|
70369
|
-
function skipEmptyAllowlistServers({ mcpServers, logger }) {
|
|
70370
|
-
const kept = {};
|
|
70371
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
70372
|
-
if (server.enabledTools?.length === 0) {
|
|
70373
|
-
warnAndSkipMcpServer({
|
|
70374
|
-
toolName: TOOL_NAME,
|
|
70375
|
-
serverName: name,
|
|
70376
|
-
reason: "an empty enabledTools list, which allows no tools at all but which Factory Droid treats as no filter",
|
|
70377
|
-
logger
|
|
70378
|
-
});
|
|
70379
|
-
continue;
|
|
70380
|
-
}
|
|
70381
|
-
kept[name] = server;
|
|
70382
|
-
}
|
|
70383
|
-
return kept;
|
|
70384
|
-
}
|
|
70385
70822
|
var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
70386
70823
|
json;
|
|
70387
70824
|
constructor(params) {
|
|
@@ -70408,8 +70845,10 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
70408
70845
|
});
|
|
70409
70846
|
}
|
|
70410
70847
|
static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger }) {
|
|
70411
|
-
const factorydroidConfig = { mcpServers:
|
|
70848
|
+
const factorydroidConfig = { mcpServers: skipServersWithEmptyEnabledTools({
|
|
70412
70849
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
70850
|
+
toolName: TOOL_NAME,
|
|
70851
|
+
productName: "Factory Droid",
|
|
70413
70852
|
logger
|
|
70414
70853
|
}) };
|
|
70415
70854
|
const fileContent = JSON.stringify(factorydroidConfig, null, 2);
|
|
@@ -71840,19 +72279,42 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
|
|
|
71840
72279
|
return hasCommand ? "stdio" : "http";
|
|
71841
72280
|
}
|
|
71842
72281
|
/**
|
|
71843
|
-
* The only `executor` value Kimi Code accepts on a stdio
|
|
71844
|
-
* the never-implemented `kaos` option and narrowed the
|
|
71845
|
-
* `z.enum(['local'])`, so
|
|
71846
|
-
*
|
|
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.
|
|
71847
72288
|
* @see https://github.com/MoonshotAI/kimi-code/pull/4184
|
|
72289
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4062
|
|
71848
72290
|
*/
|
|
71849
72291
|
const KIMI_CODE_MCP_EXECUTOR = "local";
|
|
71850
72292
|
/** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
|
|
71851
72293
|
function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
|
|
71852
72294
|
if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
|
|
71853
|
-
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}".`);
|
|
71854
72296
|
delete server.executor;
|
|
71855
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
|
+
}
|
|
71856
72318
|
function toKimiCodeServer({ name, server, logger }) {
|
|
71857
72319
|
const transport = server.transport ?? server.type;
|
|
71858
72320
|
if (transport === "ws") {
|
|
@@ -71880,6 +72342,7 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71880
72342
|
for (const field of [
|
|
71881
72343
|
"env",
|
|
71882
72344
|
"cwd",
|
|
72345
|
+
"environment_id",
|
|
71883
72346
|
"executor",
|
|
71884
72347
|
"runtime_id",
|
|
71885
72348
|
"headers",
|
|
@@ -71896,6 +72359,12 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71896
72359
|
server: converted,
|
|
71897
72360
|
logger
|
|
71898
72361
|
});
|
|
72362
|
+
dropUnsupportedKimiCodeEnvironmentId({
|
|
72363
|
+
name,
|
|
72364
|
+
server: converted,
|
|
72365
|
+
isStdio: normalizedTransport === "stdio",
|
|
72366
|
+
logger
|
|
72367
|
+
});
|
|
71899
72368
|
if (server.disabled === true) converted.enabled = false;
|
|
71900
72369
|
return converted;
|
|
71901
72370
|
}
|
|
@@ -71920,6 +72389,15 @@ function fromKimiCodeServers({ servers, logger }) {
|
|
|
71920
72389
|
logger
|
|
71921
72390
|
});
|
|
71922
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
|
+
});
|
|
71923
72401
|
return [name, {
|
|
71924
72402
|
...rest,
|
|
71925
72403
|
...type && { type },
|
|
@@ -73088,7 +73566,12 @@ var OpenclawMcp = class OpenclawMcp extends ToolMcp {
|
|
|
73088
73566
|
const existingMcp = isRecord(existing["mcp"]) ? existing["mcp"] : {};
|
|
73089
73567
|
const existingServers = isRecord(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
|
|
73090
73568
|
const converted = convertToOpenclawFormat({
|
|
73091
|
-
mcpServers:
|
|
73569
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
73570
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
73571
|
+
toolName: "OpenClaw",
|
|
73572
|
+
ignoreBlankNames: true,
|
|
73573
|
+
logger
|
|
73574
|
+
}),
|
|
73092
73575
|
logger
|
|
73093
73576
|
});
|
|
73094
73577
|
const servers = {
|
|
@@ -73716,7 +74199,11 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
73716
74199
|
const paths = this.getSettablePaths({ global });
|
|
73717
74200
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
73718
74201
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
73719
|
-
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]) => {
|
|
73720
74207
|
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
73721
74208
|
const poolAllow = isRecord(rawServer) ? rawServer.poolAllow : void 0;
|
|
73722
74209
|
return [serverName, {
|
|
@@ -77242,7 +77729,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77242
77729
|
meta: {
|
|
77243
77730
|
supportsProject: true,
|
|
77244
77731
|
supportsGlobal: true,
|
|
77245
|
-
supportsEnabledTools:
|
|
77732
|
+
supportsEnabledTools: true,
|
|
77246
77733
|
supportsDisabledTools: true
|
|
77247
77734
|
}
|
|
77248
77735
|
}],
|
|
@@ -77251,7 +77738,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77251
77738
|
meta: {
|
|
77252
77739
|
supportsProject: true,
|
|
77253
77740
|
supportsGlobal: true,
|
|
77254
|
-
supportsEnabledTools:
|
|
77741
|
+
supportsEnabledTools: true,
|
|
77255
77742
|
supportsDisabledTools: true
|
|
77256
77743
|
}
|
|
77257
77744
|
}],
|
|
@@ -77260,7 +77747,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77260
77747
|
meta: {
|
|
77261
77748
|
supportsProject: true,
|
|
77262
77749
|
supportsGlobal: false,
|
|
77263
|
-
supportsEnabledTools:
|
|
77750
|
+
supportsEnabledTools: true,
|
|
77264
77751
|
supportsDisabledTools: true
|
|
77265
77752
|
}
|
|
77266
77753
|
}],
|
|
@@ -77827,6 +78314,9 @@ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger })
|
|
|
77827
78314
|
var McpProcessor = class extends FeatureProcessor {
|
|
77828
78315
|
toolTarget;
|
|
77829
78316
|
global;
|
|
78317
|
+
sweepsThroughInRootLinks() {
|
|
78318
|
+
return this.global;
|
|
78319
|
+
}
|
|
77830
78320
|
getFactory;
|
|
77831
78321
|
featureOptions;
|
|
77832
78322
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory, dryRun = false, logger, featureOptions }) {
|
|
@@ -79936,12 +80426,30 @@ function toPathTarget(pattern) {
|
|
|
79936
80426
|
"**",
|
|
79937
80427
|
"**/*"
|
|
79938
80428
|
].includes(path)) return { target: "." };
|
|
79939
|
-
const
|
|
80429
|
+
const stripped = path.replace(DIRECTORY_CONTENTS_SUFFIX, "");
|
|
80430
|
+
const directory = stripped === "" && path.startsWith("/") ? "/" : /^[A-Za-z]:$/.test(stripped) && stripped !== path ? `${stripped}/` : stripped;
|
|
79940
80431
|
if (directory !== path && directory.length > 0 && !hasGlob(directory)) return { target: directory };
|
|
79941
80432
|
if (!hasGlob(path)) return { target: path };
|
|
79942
80433
|
return { skipReason: "Antigravity file targets are paths, not globs. Only a plain path, `dir/**` or `*` can be expressed" };
|
|
79943
80434
|
}
|
|
79944
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
|
+
/**
|
|
79945
80453
|
* A URL or domain pattern becomes its hostname. A leading `*.` is dropped,
|
|
79946
80454
|
* since a domain already covers its subdomains, and the URL path is ignored.
|
|
79947
80455
|
* Claude Code's `domain:` form is read as well.
|
|
@@ -80158,19 +80666,21 @@ function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory,
|
|
|
80158
80666
|
for (const entry of entries) {
|
|
80159
80667
|
const { toolName, pattern: target } = parseToolPatternEntry(entry);
|
|
80160
80668
|
const canonical = toCategory(toolName);
|
|
80161
|
-
const
|
|
80669
|
+
const patterns = ANTIGRAVITY_COMMAND_ACTIONS.has(toolName) ? [importAntigravityCommandTarget({
|
|
80162
80670
|
target,
|
|
80163
80671
|
entry,
|
|
80164
80672
|
decision: action,
|
|
80165
80673
|
logger,
|
|
80166
80674
|
toolLabel
|
|
80167
|
-
}) : PATH_ACTIONS.has(toolName) ? collapseLeadingHome(target, homeDirectory) : target;
|
|
80168
|
-
|
|
80169
|
-
|
|
80170
|
-
|
|
80171
|
-
|
|
80172
|
-
|
|
80173
|
-
|
|
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
|
+
}
|
|
80174
80684
|
}
|
|
80175
80685
|
};
|
|
80176
80686
|
processEntries(allow, "allow");
|
|
@@ -80941,6 +81451,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
80941
81451
|
* in global mode it is dropped with a warning instead of written somewhere it
|
|
80942
81452
|
* does nothing.
|
|
80943
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
|
+
*
|
|
80944
81458
|
* Both make Auggie load code the repository did not ship — a recommended
|
|
80945
81459
|
* marketplace is one prompt away from being cloned, and an enabled plugin can
|
|
80946
81460
|
* carry its own hooks and MCP servers — so writing either is logged, unless
|
|
@@ -80960,7 +81474,7 @@ function buildPluginConsumptionPatch({ override, existing, global, logger }) {
|
|
|
80960
81474
|
else {
|
|
80961
81475
|
patch.recommendedMarketplaces = recommendedMarketplaces;
|
|
80962
81476
|
const existingMarketplaces = existingKeys.recommendedMarketplaces;
|
|
80963
|
-
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).`);
|
|
80964
81478
|
warnAboutRemovedPluginIds({
|
|
80965
81479
|
key: "recommendedMarketplaces",
|
|
80966
81480
|
removed: (existingMarketplaces ?? []).filter((marketplace) => !recommendedMarketplaces.includes(marketplace)),
|
|
@@ -81189,12 +81703,12 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
81189
81703
|
const regex = entry.shellInputRegex;
|
|
81190
81704
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
81191
81705
|
else {
|
|
81192
|
-
if (action
|
|
81193
|
-
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.`);
|
|
81194
81708
|
continue;
|
|
81195
81709
|
}
|
|
81196
|
-
if (action
|
|
81197
|
-
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.`);
|
|
81198
81712
|
pattern = "*";
|
|
81199
81713
|
} else {
|
|
81200
81714
|
pattern = shellRegexToGlob(regex);
|
|
@@ -81280,6 +81794,62 @@ function classifyPattern(pattern, bareStarIsWildcard) {
|
|
|
81280
81794
|
};
|
|
81281
81795
|
return { kind: "literal" };
|
|
81282
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
|
+
}
|
|
81283
81853
|
function pluralize(count, singular, plural) {
|
|
81284
81854
|
return count === 1 ? singular : plural;
|
|
81285
81855
|
}
|
|
@@ -81374,8 +81944,16 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
|
|
|
81374
81944
|
const matcherAllows = [];
|
|
81375
81945
|
const widenedDenies = [];
|
|
81376
81946
|
const inertDenies = [];
|
|
81947
|
+
const literalDenyStems = collectLiteralDenyStems({
|
|
81948
|
+
rules,
|
|
81949
|
+
bareStarIsWildcard: isRooLineage
|
|
81950
|
+
});
|
|
81377
81951
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
81378
81952
|
if (action !== "allow" && action !== "deny") continue;
|
|
81953
|
+
if (action === "deny" && isCoveredByLiteralDeny({
|
|
81954
|
+
pattern,
|
|
81955
|
+
literalDenyStems
|
|
81956
|
+
})) continue;
|
|
81379
81957
|
const shape = classifyPattern(pattern, isRooLineage);
|
|
81380
81958
|
if (action === "allow") {
|
|
81381
81959
|
allowed.push(pattern);
|
|
@@ -81738,16 +82316,20 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
|
|
|
81738
82316
|
validate: true
|
|
81739
82317
|
});
|
|
81740
82318
|
}
|
|
81741
|
-
toRulesyncPermissions() {
|
|
82319
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
81742
82320
|
const settings = parseBobSettings({
|
|
81743
82321
|
fileContent: this.getFileContent(),
|
|
81744
82322
|
filePath: join(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
81745
82323
|
});
|
|
81746
82324
|
const approval = settings[APPROVAL_KEY$1];
|
|
81747
82325
|
const entry = (isPlainObject(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
|
|
81748
|
-
const rules = {
|
|
81749
|
-
|
|
81750
|
-
|
|
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
|
+
});
|
|
81751
82333
|
const permission = {};
|
|
81752
82334
|
if (Object.keys(rules).length > 0) permission[SHELL_PERMISSION_CATEGORY] = rules;
|
|
81753
82335
|
const bob = readBobOverride(settings);
|
|
@@ -85511,7 +86093,7 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
|
|
|
85511
86093
|
const patch = {};
|
|
85512
86094
|
const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
|
|
85513
86095
|
for (const [key, value] of Object.entries(override ?? {})) {
|
|
85514
|
-
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;
|
|
85515
86097
|
if (!allowed.has(key)) {
|
|
85516
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.`);
|
|
85517
86099
|
continue;
|
|
@@ -85800,6 +86382,7 @@ function stripTrailingSlash(pattern) {
|
|
|
85800
86382
|
function buildCodexBashRulesContent({ config, logger }) {
|
|
85801
86383
|
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
85802
86384
|
const entries = Object.entries(bashRules);
|
|
86385
|
+
const canonicalMatching = config.codexcli?.bash_canonical_matching === true;
|
|
85803
86386
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
85804
86387
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
85805
86388
|
const ruleBlocks = entries.map(([pattern, action]) => {
|
|
@@ -85818,6 +86401,7 @@ function buildCodexBashRulesContent({ config, logger }) {
|
|
|
85818
86401
|
` pattern = [${serializedTokens}],`,
|
|
85819
86402
|
` decision = ${JSON.stringify(decision)},`,
|
|
85820
86403
|
` justification = ${JSON.stringify(`Generated from Rulesync permission.bash: ${pattern}`)},`,
|
|
86404
|
+
...canonicalMatching && decision !== "allow" ? [" canonical_matching = True,"] : [],
|
|
85821
86405
|
")"
|
|
85822
86406
|
].join("\n");
|
|
85823
86407
|
}).filter((block) => block !== null);
|
|
@@ -87402,9 +87986,126 @@ function toCanonicalToolName$5(crushName) {
|
|
|
87402
87986
|
function isMcpToolName(crushName) {
|
|
87403
87987
|
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
87404
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
|
+
}
|
|
87405
88100
|
function uniq$1(values) {
|
|
87406
88101
|
return [...new Set(values)];
|
|
87407
88102
|
}
|
|
88103
|
+
function stringList(value) {
|
|
88104
|
+
return isStringArray(value) ? value : [];
|
|
88105
|
+
}
|
|
88106
|
+
function listOrUndefined(values) {
|
|
88107
|
+
return values.length > 0 ? values : void 0;
|
|
88108
|
+
}
|
|
87408
88109
|
/**
|
|
87409
88110
|
* Permissions adapter for Crush.
|
|
87410
88111
|
*
|
|
@@ -87426,8 +88127,24 @@ function uniq$1(values) {
|
|
|
87426
88127
|
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
87427
88128
|
* any other category passes through verbatim.
|
|
87428
88129
|
*
|
|
87429
|
-
*
|
|
87430
|
-
*
|
|
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`
|
|
87431
88148
|
* is not written while the same category carries a pattern-specific `deny` or
|
|
87432
88149
|
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
87433
88150
|
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
@@ -87491,7 +88208,12 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87491
88208
|
const existingContent = location.fileContent ?? "";
|
|
87492
88209
|
warnCrushTwinLeftovers({
|
|
87493
88210
|
location,
|
|
87494
|
-
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
|
+
],
|
|
87495
88217
|
logger
|
|
87496
88218
|
});
|
|
87497
88219
|
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
@@ -87503,12 +88225,22 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87503
88225
|
logger
|
|
87504
88226
|
});
|
|
87505
88227
|
const managedToolNames = managedCrushToolNames(config);
|
|
88228
|
+
const commands = rebuildCrushCommandLists({
|
|
88229
|
+
config,
|
|
88230
|
+
existingPermissions,
|
|
88231
|
+
logger
|
|
88232
|
+
});
|
|
87506
88233
|
const preservedAllowed = (isStringArray(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
87507
88234
|
const preservedDisabled = (isStringArray(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
87508
88235
|
const allowedList = uniq$1([...preservedAllowed, ...generated.allowed]);
|
|
87509
88236
|
const disabledList = uniq$1([...preservedDisabled, ...generated.disabled]);
|
|
88237
|
+
const permissionsHasContent = allowedList.length > 0 || commands.safe.length > 0 || commands.blocked.length > 0;
|
|
87510
88238
|
const patch = {};
|
|
87511
|
-
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
|
+
};
|
|
87512
88244
|
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
87513
88245
|
return new CrushPermissions({
|
|
87514
88246
|
outputRoot,
|
|
@@ -87526,17 +88258,79 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87526
88258
|
global
|
|
87527
88259
|
});
|
|
87528
88260
|
}
|
|
87529
|
-
toRulesyncPermissions() {
|
|
88261
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
87530
88262
|
const permissions = isRecord(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
87531
88263
|
const options = toRecord(this.json[CRUSH_OPTIONS_KEY]);
|
|
87532
88264
|
const rulesyncConfig = convertCrushListsToRulesync({
|
|
87533
|
-
allowed:
|
|
87534
|
-
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
|
|
87535
88270
|
});
|
|
87536
88271
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
87537
88272
|
}
|
|
87538
88273
|
};
|
|
87539
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
|
+
/**
|
|
87540
88334
|
* The Crush tool names the canonical config manages, i.e. the names its
|
|
87541
88335
|
* categories map to. Entries naming any other tool are the user's and survive
|
|
87542
88336
|
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
@@ -87561,7 +88355,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
87561
88355
|
catchAllAction = action;
|
|
87562
88356
|
continue;
|
|
87563
88357
|
}
|
|
87564
|
-
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.`);
|
|
87565
88359
|
if (action !== "allow") restricted = true;
|
|
87566
88360
|
}
|
|
87567
88361
|
if (catchAllAction === "allow" && restricted) {
|
|
@@ -87612,11 +88406,71 @@ function convertRulesyncToCrushLists({ config, logger }) {
|
|
|
87612
88406
|
};
|
|
87613
88407
|
}
|
|
87614
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
|
+
/**
|
|
87615
88469
|
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
87616
88470
|
* entry scopes an allow to one action of a tool, which the canonical model
|
|
87617
88471
|
* cannot express, so it is left out rather than widened to the whole tool.
|
|
87618
88472
|
*/
|
|
87619
|
-
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
88473
|
+
function convertCrushListsToRulesync({ allowed, disabled, safe, blocked, logger }) {
|
|
87620
88474
|
const permission = {};
|
|
87621
88475
|
const bucketFor = (category) => {
|
|
87622
88476
|
const own = lookupOwn({
|
|
@@ -87632,6 +88486,29 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
87632
88486
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
87633
88487
|
bucketFor(toCanonicalToolName$5(entry))["*"] = "deny";
|
|
87634
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
|
+
}
|
|
87635
88512
|
for (const entry of allowed) {
|
|
87636
88513
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
87637
88514
|
bucketFor(toCanonicalToolName$5(entry))["*"] ??= "allow";
|
|
@@ -89011,8 +89888,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
|
89011
89888
|
* In global mode the config file is shared with the hooks (`hooks`) feature
|
|
89012
89889
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
89013
89890
|
* merge into the existing JSON and the file is never deleted; only the keys
|
|
89014
|
-
* this feature manages are rewritten — `permissions`, plus `sandbox`
|
|
89015
|
-
* 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` /
|
|
89016
89893
|
* `forbiddenPlugins`) in project mode when the `devin` override authors them.
|
|
89017
89894
|
*
|
|
89018
89895
|
* The sibling `sandbox` block — which decides what a permitted command may
|
|
@@ -89120,6 +89997,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89120
89997
|
});
|
|
89121
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.");
|
|
89122
89999
|
}
|
|
90000
|
+
applyPermissionModeOverride({
|
|
90001
|
+
permissionMode: config.devin?.permission_mode,
|
|
90002
|
+
patch,
|
|
90003
|
+
global,
|
|
90004
|
+
logger
|
|
90005
|
+
});
|
|
89123
90006
|
applyPluginGovernanceOverride({
|
|
89124
90007
|
override: config.devin,
|
|
89125
90008
|
settings,
|
|
@@ -89163,6 +90046,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89163
90046
|
if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
|
|
89164
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)"}.`);
|
|
89165
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;
|
|
89166
90055
|
const result = { ...config };
|
|
89167
90056
|
if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
|
|
89168
90057
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
@@ -89179,6 +90068,48 @@ const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
|
|
|
89179
90068
|
"forbiddenPlugins"
|
|
89180
90069
|
];
|
|
89181
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
|
+
/**
|
|
89182
90113
|
* Patch the plugin governance lists the `devin` override authors into the
|
|
89183
90114
|
* project `.devin/config.json`. Each authored list replaces the file's list
|
|
89184
90115
|
* whole; a list it does not author is left as the file holds it. Devin reads these
|
|
@@ -95168,7 +96099,8 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
95168
96099
|
*
|
|
95169
96100
|
* Reasonix matches a rule's tool exactly against its registered tools, which
|
|
95170
96101
|
* are snake_case (`read_file`, `write_file`, `grep`, `glob`, `web_fetch`,
|
|
95171
|
-
* `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
|
|
95172
96104
|
* writer: `write_file`, `edit_file`, `multi_edit`, `notebook_edit`, ...),
|
|
95173
96105
|
* `Browser` and `Computer` are Claude-Code-style family aliases
|
|
95174
96106
|
* (`canonicalRuleTool` / `groupOf`), so a `Read(...)` or `Write(...)` rule
|
|
@@ -95191,6 +96123,21 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
|
95191
96123
|
agent: "task"
|
|
95192
96124
|
};
|
|
95193
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
|
+
/**
|
|
95194
96141
|
* The Claude-Code-style spellings rulesync emitted before it followed the
|
|
95195
96142
|
* Reasonix tool vocabulary. None of them reaches a Reasonix tool, so they are
|
|
95196
96143
|
* read back on import (a file generated by an older rulesync still imports)
|
|
@@ -95213,7 +96160,8 @@ const CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES = {
|
|
|
95213
96160
|
*/
|
|
95214
96161
|
const REASONIX_TO_CANONICAL_TOOL_NAMES = {
|
|
95215
96162
|
...invertRecord(CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES),
|
|
95216
|
-
...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])))
|
|
95217
96165
|
};
|
|
95218
96166
|
/** Every Reasonix tool name a canonical category owns on generate. */
|
|
95219
96167
|
function reasonixToolNamesOf(canonical) {
|
|
@@ -95221,14 +96169,20 @@ function reasonixToolNamesOf(canonical) {
|
|
|
95221
96169
|
record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
|
|
95222
96170
|
key: canonical
|
|
95223
96171
|
});
|
|
95224
|
-
const current =
|
|
95225
|
-
return legacy === void 0 ?
|
|
96172
|
+
const current = toReasonixToolNames(canonical);
|
|
96173
|
+
return legacy === void 0 ? current : [...current, legacy];
|
|
95226
96174
|
}
|
|
95227
|
-
|
|
95228
|
-
|
|
96175
|
+
/** The Reasonix tool names a canonical category's rules are written under. */
|
|
96176
|
+
function toReasonixToolNames(canonical) {
|
|
96177
|
+
const primary = lookupOwn({
|
|
95229
96178
|
record: CANONICAL_TO_REASONIX_TOOL_NAMES,
|
|
95230
96179
|
key: canonical
|
|
95231
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];
|
|
95232
96186
|
}
|
|
95233
96187
|
function toCanonicalToolName$2(reasonixName) {
|
|
95234
96188
|
return lookupOwn({
|
|
@@ -95636,6 +96590,17 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
95636
96590
|
});
|
|
95637
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.`);
|
|
95638
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
|
+
});
|
|
95639
96604
|
const mergedPermissions = { ...existingPermissions };
|
|
95640
96605
|
setOrDeleteEntries(mergedPermissions, "allow", [
|
|
95641
96606
|
...preservedAllow,
|
|
@@ -95811,6 +96776,55 @@ function preserveExistingEntries({ existingPermissions, managedToolNames, rawOwn
|
|
|
95811
96776
|
deny
|
|
95812
96777
|
};
|
|
95813
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
|
+
}
|
|
95814
96828
|
/** Sort, dedupe and set a permissions array key, deleting it when empty. */
|
|
95815
96829
|
function setOrDeleteEntries(table, key, entries) {
|
|
95816
96830
|
const merged = uniq(entries.toSorted());
|
|
@@ -95824,22 +96838,19 @@ function convertRulesyncToReasonixPermissions(config) {
|
|
|
95824
96838
|
const allow = [];
|
|
95825
96839
|
const ask = [];
|
|
95826
96840
|
const deny = [];
|
|
95827
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
95828
|
-
const
|
|
95829
|
-
|
|
95830
|
-
|
|
95831
|
-
|
|
95832
|
-
|
|
95833
|
-
|
|
95834
|
-
|
|
95835
|
-
|
|
95836
|
-
|
|
95837
|
-
|
|
95838
|
-
|
|
95839
|
-
|
|
95840
|
-
break;
|
|
95841
|
-
case "deny": deny.push(entry);
|
|
95842
|
-
}
|
|
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);
|
|
95843
96854
|
}
|
|
95844
96855
|
}
|
|
95845
96856
|
return {
|
|
@@ -96066,7 +97077,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96066
97077
|
validate: true
|
|
96067
97078
|
});
|
|
96068
97079
|
}
|
|
96069
|
-
toRulesyncPermissions() {
|
|
97080
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
96070
97081
|
const constructor = this.constructor;
|
|
96071
97082
|
let settings;
|
|
96072
97083
|
try {
|
|
@@ -96080,9 +97091,12 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96080
97091
|
} catch (error) {
|
|
96081
97092
|
throw new Error(`Failed to parse ${constructor.getToolLabel()} VS Code settings in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
96082
97093
|
}
|
|
96083
|
-
const rules = {
|
|
96084
|
-
|
|
96085
|
-
|
|
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
|
+
});
|
|
96086
97100
|
const permission = {};
|
|
96087
97101
|
if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
|
|
96088
97102
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
@@ -101406,6 +102420,9 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
101406
102420
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
101407
102421
|
toolTarget;
|
|
101408
102422
|
global;
|
|
102423
|
+
sweepsThroughInRootLinks() {
|
|
102424
|
+
return this.global;
|
|
102425
|
+
}
|
|
101409
102426
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
101410
102427
|
super({
|
|
101411
102428
|
outputRoot,
|
|
@@ -104091,6 +105108,6 @@ const CHECKS_IMPORT_STRATEGY = {
|
|
|
104091
105108
|
missingLabel: "check files",
|
|
104092
105109
|
createdLabel: "check files"
|
|
104093
105110
|
};
|
|
104094
|
-
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 };
|
|
104095
105112
|
|
|
104096
|
-
//# sourceMappingURL=import-
|
|
105113
|
+
//# sourceMappingURL=import-Cu5o6uKO.js.map
|