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
|
@@ -1815,28 +1815,6 @@ function splitAbsolutePath(absolutePath) {
|
|
|
1815
1815
|
};
|
|
1816
1816
|
}
|
|
1817
1817
|
/**
|
|
1818
|
-
* Whether writing `targetPath` would land outside `rootPath` once every link on
|
|
1819
|
-
* the way there is resolved. Both sides are taken to where a write to them
|
|
1820
|
-
* would land (see {@link writeLandingPath}), so a root that does not fully
|
|
1821
|
-
* exist yet is judged the same way as the target below it.
|
|
1822
|
-
*
|
|
1823
|
-
* A link that stays inside the root passes. That is what sets this apart from
|
|
1824
|
-
* the stricter {@link assertWritablePathInsideRoot} the sweep paths use: a
|
|
1825
|
-
* dotfiles checkout linked from inside the home directory is a common shape for
|
|
1826
|
-
* a global write target, and refusing it would refuse `--global` outright.
|
|
1827
|
-
*
|
|
1828
|
-
* A link chain that cycles is reported as an escape: the write could not get
|
|
1829
|
-
* through it, and refusing it is the answer that never writes somewhere
|
|
1830
|
-
* unexpected.
|
|
1831
|
-
*/
|
|
1832
|
-
async function writablePathEscapesRoot({ rootPath, targetPath }) {
|
|
1833
|
-
const relativeLanding = await relativeWriteLanding({
|
|
1834
|
-
rootPath,
|
|
1835
|
-
targetPath
|
|
1836
|
-
});
|
|
1837
|
-
return relativeLanding === null || pathEscapesRoot(relativeLanding);
|
|
1838
|
-
}
|
|
1839
|
-
/**
|
|
1840
1818
|
* Where a write to `targetPath` lands relative to where a write to `rootPath`
|
|
1841
1819
|
* lands, both taken through {@link writeLandingPath}: `""` when the two land on
|
|
1842
1820
|
* the same directory, and `null` when either side's links form a cycle. Errors
|
|
@@ -1849,6 +1827,59 @@ async function relativeWriteLanding({ rootPath, targetPath }) {
|
|
|
1849
1827
|
return (0, node_path.relative)(rootLanding, targetLanding);
|
|
1850
1828
|
}
|
|
1851
1829
|
/**
|
|
1830
|
+
* Directory names that hold version-control metadata. Bytes landing in one of
|
|
1831
|
+
* them change how the VCS behaves rather than what it tracks: `.git/config`
|
|
1832
|
+
* can set `core.fsmonitor`, `.git/hooks/*` run on the next commit.
|
|
1833
|
+
*/
|
|
1834
|
+
const VCS_METADATA_DIR_NAMES = /* @__PURE__ */ new Set([
|
|
1835
|
+
".git",
|
|
1836
|
+
".hg",
|
|
1837
|
+
".svn",
|
|
1838
|
+
".jj"
|
|
1839
|
+
]);
|
|
1840
|
+
function hasVcsMetadataSegment(path) {
|
|
1841
|
+
return splitPathSegments(path).some((segment) => {
|
|
1842
|
+
const folded = segment.replace(/:.*$/, "").replace(/[. ]+$/, "").toLowerCase();
|
|
1843
|
+
return VCS_METADATA_DIR_NAMES.has(folded) || /^(git|hg|svn|jj)~\d+$/.test(folded);
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
/**
|
|
1847
|
+
* Whether a link on the way to `targetPath` carries it into version-control
|
|
1848
|
+
* metadata (`.git/` and its siblings) that its spelling does not name.
|
|
1849
|
+
*
|
|
1850
|
+
* {@link writablePathEscapesRoot} lets a link that stays inside the root
|
|
1851
|
+
* through, but a checked-out repository can commit `CLAUDE.md -> .git/config`:
|
|
1852
|
+
* the landing is inside the root, yet a write there hands the repository author
|
|
1853
|
+
* the operator's next `git` command. `git clone` never transfers `.git/`, so a
|
|
1854
|
+
* path spelled into it is never something the repository chose; only a write
|
|
1855
|
+
* that lands exactly where it is spelled may touch it.
|
|
1856
|
+
*
|
|
1857
|
+
* `relativeLanding` is {@link relativeWriteLanding}'s answer for the same pair,
|
|
1858
|
+
* passed in so a caller that already has it does not resolve the links twice.
|
|
1859
|
+
*/
|
|
1860
|
+
function landingEntersVcsMetadata({ rootPath, targetPath, relativeLanding }) {
|
|
1861
|
+
return hasVcsMetadataSegment(relativeLanding) && relativeLanding !== (0, node_path.relative)((0, node_path.resolve)(rootPath), (0, node_path.resolve)(targetPath));
|
|
1862
|
+
}
|
|
1863
|
+
/**
|
|
1864
|
+
* Refuse a write target that a symbolic link carries into version-control
|
|
1865
|
+
* metadata (see {@link landingEntersVcsMetadata}). Meant for writes of remote
|
|
1866
|
+
* bytes, next to {@link assertPathResolvesInsideRoot}: an in-root link (a
|
|
1867
|
+
* skill directory linked to a shared one) stays allowed, but not one that
|
|
1868
|
+
* hands the bytes to `.git/config` or `.git/hooks/`.
|
|
1869
|
+
*/
|
|
1870
|
+
async function assertLandingOutsideVcsMetadata({ rootPath, targetPath }) {
|
|
1871
|
+
const relativeLanding = await relativeWriteLanding({
|
|
1872
|
+
rootPath,
|
|
1873
|
+
targetPath
|
|
1874
|
+
});
|
|
1875
|
+
if (relativeLanding === null) throw new PathOutsideRootError(`Refusing to write through a link cycle: ${targetPath}.`);
|
|
1876
|
+
if (landingEntersVcsMetadata({
|
|
1877
|
+
rootPath,
|
|
1878
|
+
targetPath,
|
|
1879
|
+
relativeLanding
|
|
1880
|
+
})) throw new PathOutsideRootError(`Refusing to write ${targetPath}: a symbolic link leads it into version-control metadata.`);
|
|
1881
|
+
}
|
|
1882
|
+
/**
|
|
1852
1883
|
* How many trailing segments `filePath` and `identity` have in common.
|
|
1853
1884
|
*
|
|
1854
1885
|
* A path that walked through no link at all shares all of its own segments with
|
|
@@ -5452,7 +5483,8 @@ const QWENCODE_HOOK_EVENTS = [
|
|
|
5452
5483
|
"todoCreated",
|
|
5453
5484
|
"todoCompleted",
|
|
5454
5485
|
"messageDisplay",
|
|
5455
|
-
"sessionDelete"
|
|
5486
|
+
"sessionDelete",
|
|
5487
|
+
"fileChanged"
|
|
5456
5488
|
];
|
|
5457
5489
|
const CANONICAL_TO_KIMI_CODE_EVENT_NAMES = {
|
|
5458
5490
|
sessionStart: "SessionStart",
|
|
@@ -6786,6 +6818,7 @@ const CANONICAL_TO_JUNIE_EVENT_NAMES = {
|
|
|
6786
6818
|
sessionStart: "SessionStart",
|
|
6787
6819
|
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6788
6820
|
preToolUse: "PreToolUse",
|
|
6821
|
+
postToolUse: "PostToolUse",
|
|
6789
6822
|
stop: "Stop",
|
|
6790
6823
|
stopFailure: "StopFailure",
|
|
6791
6824
|
permissionRequest: "PermissionRequest",
|
|
@@ -6794,11 +6827,12 @@ const CANONICAL_TO_JUNIE_EVENT_NAMES = {
|
|
|
6794
6827
|
/**
|
|
6795
6828
|
* Hook events supported by JetBrains Junie CLI.
|
|
6796
6829
|
*
|
|
6797
|
-
* Junie CLI exposes
|
|
6830
|
+
* Junie CLI exposes eight lifecycle events under the `"hooks"` key of
|
|
6798
6831
|
* `~/.junie/config.json`: `SessionStart`, `UserPromptSubmit`, `PreToolUse`,
|
|
6799
|
-
* `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`.
|
|
6800
|
-
* to `SessionStart` (source), `PreToolUse`
|
|
6801
|
-
* type), `PermissionRequest` (tool name), and
|
|
6832
|
+
* `PostToolUse`, `Stop`, `StopFailure`, `PermissionRequest`, and `SessionEnd`.
|
|
6833
|
+
* Matchers apply to `SessionStart` (source), `PreToolUse` and `PostToolUse`
|
|
6834
|
+
* (tool name), `StopFailure` (error type), `PermissionRequest` (tool name), and
|
|
6835
|
+
* `SessionEnd` (reason);
|
|
6802
6836
|
* `UserPromptSubmit` and `Stop` are matcher-less and always run. Only
|
|
6803
6837
|
* `type: "command"` hooks are supported. Project-local hooks are ignored for
|
|
6804
6838
|
* safety.
|
|
@@ -6977,7 +7011,8 @@ const CANONICAL_TO_QWENCODE_EVENT_NAMES = {
|
|
|
6977
7011
|
todoCreated: "TodoCreated",
|
|
6978
7012
|
todoCompleted: "TodoCompleted",
|
|
6979
7013
|
messageDisplay: "MessageDisplay",
|
|
6980
|
-
sessionDelete: "SessionDelete"
|
|
7014
|
+
sessionDelete: "SessionDelete",
|
|
7015
|
+
fileChanged: "FileChanged"
|
|
6981
7016
|
};
|
|
6982
7017
|
/**
|
|
6983
7018
|
* Map Qwen Code PascalCase event names to canonical camelCase.
|
|
@@ -9232,7 +9267,8 @@ const AntigravityCliPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
9232
9267
|
* The same override also carries the two plugin-consumption keys Auggie reads
|
|
9233
9268
|
* from `settings.json`: `recommendedMarketplaces` (`"owner/repo"` marketplaces
|
|
9234
9269
|
* a workspace prompts its contributors to install; honored only in the
|
|
9235
|
-
* project's `.augment/settings.json
|
|
9270
|
+
* project's `.augment/settings.json`, and only by Auggie 0.x — 1.x no longer
|
|
9271
|
+
* reads it) and `enabledPlugins` (plugin ids of the
|
|
9236
9272
|
* form `"plugin@marketplace"` mapped to on/off, deep-merged across the user,
|
|
9237
9273
|
* project and local tiers).
|
|
9238
9274
|
*
|
|
@@ -9416,6 +9452,14 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
|
|
|
9416
9452
|
* `true`; only an explicit `false` suppresses it. Like
|
|
9417
9453
|
* `base_permission_profile` it is consumed by the profile builder, not
|
|
9418
9454
|
* written as a top-level config key.
|
|
9455
|
+
* - `bash_canonical_matching` — when `true`, the `forbidden` (deny) and
|
|
9456
|
+
* `prompt` (ask) rules generated from `permission.bash` into
|
|
9457
|
+
* `.codex/rules/rulesync.rules` carry `canonical_matching = True`, so Codex
|
|
9458
|
+
* compares their tokens after Unicode canonical case-insensitive folding (`rm` also
|
|
9459
|
+
* matches `RM`). `allow` rules stay exact so the opt-in never widens what is
|
|
9460
|
+
* auto-approved. Requires Codex 0.167.0 or later — an older Codex rejects the
|
|
9461
|
+
* keyword and fails to load the rules file — so it defaults to `false`. Not
|
|
9462
|
+
* written as a config key.
|
|
9419
9463
|
*
|
|
9420
9464
|
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
9421
9465
|
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
@@ -9423,8 +9467,8 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
|
|
|
9423
9467
|
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`, `auto_review`,
|
|
9424
9468
|
* `tui`) are emitted, and
|
|
9425
9469
|
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
9426
|
-
* `base_permission_profile` and `
|
|
9427
|
-
*
|
|
9470
|
+
* `base_permission_profile`, `git_write_rules` and `bash_canonical_matching`
|
|
9471
|
+
* are consumed by the generators rather than written, as described above, and `permission` is the
|
|
9428
9472
|
* tool-scoped canonical block, which `RulesyncPermissions.forTarget` strips out
|
|
9429
9473
|
* of the override before it ever reaches the patch. The allowlist is what keeps
|
|
9430
9474
|
* the override from clobbering a feature-owned key: `mcp_servers.*` per-MCP
|
|
@@ -9481,7 +9525,8 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
9481
9525
|
* @see https://developers.openai.com/codex/config-reference
|
|
9482
9526
|
*/
|
|
9483
9527
|
tui: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9484
|
-
git_write_rules: zod_mini.z.optional(zod_mini.z.boolean())
|
|
9528
|
+
git_write_rules: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9529
|
+
bash_canonical_matching: zod_mini.z.optional(zod_mini.z.boolean())
|
|
9485
9530
|
});
|
|
9486
9531
|
/**
|
|
9487
9532
|
* Tool-scoped override block for Zed. These Zed `agent` keys sit outside the
|
|
@@ -9565,6 +9610,14 @@ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
9565
9610
|
* { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
|
|
9566
9611
|
* @example
|
|
9567
9612
|
* { "forbiddenPlugins": ["*"], "optionalPlugins": ["acme/approved"] }
|
|
9613
|
+
*
|
|
9614
|
+
* `permission_mode` (CLI v3000.12.17+) is the mode new local sessions start in
|
|
9615
|
+
* (`normal`, the upstream default, `accept-edits`, `smart` or `dangerous`).
|
|
9616
|
+
* Like `sandbox` it is a **User Config Only** key, so it is emitted at global
|
|
9617
|
+
* scope only. It is never derived from the canonical rules.
|
|
9618
|
+
*
|
|
9619
|
+
* @example
|
|
9620
|
+
* { "permission_mode": "accept-edits" }
|
|
9568
9621
|
* @see https://docs.devin.ai/cli/sandbox
|
|
9569
9622
|
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
9570
9623
|
* @see https://docs.devin.ai/cli/extensibility/plugins/overview
|
|
@@ -9580,12 +9633,25 @@ const DEVIN_PLUGIN_GOVERNANCE_SCHEMAS = {
|
|
|
9580
9633
|
optionalPlugins: DevinPluginSourceListSchema,
|
|
9581
9634
|
forbiddenPlugins: zod_mini.z.array(zod_mini.z.string())
|
|
9582
9635
|
};
|
|
9636
|
+
/**
|
|
9637
|
+
* Values Devin accepts for the user-config `permission_mode` key. Shared by the
|
|
9638
|
+
* override schema and the import, which drops a value that would not pass it.
|
|
9639
|
+
*
|
|
9640
|
+
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
9641
|
+
*/
|
|
9642
|
+
const DevinPermissionModeSchema = zod_mini.z.enum([
|
|
9643
|
+
"normal",
|
|
9644
|
+
"accept-edits",
|
|
9645
|
+
"smart",
|
|
9646
|
+
"dangerous"
|
|
9647
|
+
]);
|
|
9583
9648
|
const DevinPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
9584
9649
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
9585
9650
|
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9586
9651
|
requiredPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.requiredPlugins),
|
|
9587
9652
|
optionalPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.optionalPlugins),
|
|
9588
|
-
forbiddenPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins)
|
|
9653
|
+
forbiddenPlugins: zod_mini.z.optional(DEVIN_PLUGIN_GOVERNANCE_SCHEMAS.forbiddenPlugins),
|
|
9654
|
+
permission_mode: zod_mini.z.optional(DevinPermissionModeSchema)
|
|
9589
9655
|
});
|
|
9590
9656
|
/**
|
|
9591
9657
|
* Permissions configuration.
|
|
@@ -11997,6 +12063,98 @@ function resolveStandardSkillFields({ rootFrontmatter, section }) {
|
|
|
11997
12063
|
})
|
|
11998
12064
|
});
|
|
11999
12065
|
}
|
|
12066
|
+
// src/utils/remote-name.ts
|
|
12067
|
+
/**
|
|
12068
|
+
* Name checks for a directory or file name chosen by a remote source (a
|
|
12069
|
+
* fetched repository, an installed git, GitHub or npm source). Shared by
|
|
12070
|
+
* `rulesync fetch` and `rulesync install`, so a name one of them turns away is
|
|
12071
|
+
* not written, or printed raw, by the other.
|
|
12072
|
+
*/
|
|
12073
|
+
/** A name with no character in it that draws anything of its own. */
|
|
12074
|
+
const NOTHING_DRAWN_PATTERN = /^[\s\p{M}]*$/u;
|
|
12075
|
+
/**
|
|
12076
|
+
* The marks a name begins with. A mark is drawn over the character before it,
|
|
12077
|
+
* and a name has nothing before it of its own: a leading one is drawn over
|
|
12078
|
+
* whatever the terminal printed last — the checkbox, the quote around a name
|
|
12079
|
+
* in a warning — while the name reads as if it began with its first letter.
|
|
12080
|
+
*/
|
|
12081
|
+
const LEADING_MARKS_PATTERN = /^\p{M}+/u;
|
|
12082
|
+
/**
|
|
12083
|
+
* A name ending in a dot or an ASCII space, which Win32 strips before it opens
|
|
12084
|
+
* the path. Only the plain space (U+0020): Win32 keeps every other trailing
|
|
12085
|
+
* blank, so a name ending in a no-break or an ideographic space is written
|
|
12086
|
+
* where it says, and is left to the whitespace note of the confusable-name
|
|
12087
|
+
* check rather than dropped here with Windows named as the reason.
|
|
12088
|
+
*/
|
|
12089
|
+
const TRAILING_DOT_OR_SPACE_PATTERN = /[. ]$/;
|
|
12090
|
+
/** A name ending in the `NAME~1` shape of a Windows 8.3 short name, extension included. */
|
|
12091
|
+
const SHORT_NAME_ALIAS_PATTERN = /~\d+(?:\.[^.]*)?$/;
|
|
12092
|
+
/**
|
|
12093
|
+
* A name Win32 reserves for a device — `CON`, `PRN`, `AUX`, `NUL`, `CONIN$`,
|
|
12094
|
+
* `CONOUT$`, `COM1`–`COM9`, `LPT1`–`LPT9` — in any case and with any
|
|
12095
|
+
* extension: `nul.md` is the null device too, since the reservation is decided
|
|
12096
|
+
* on the part before the first dot, with any spaces before that dot dropped
|
|
12097
|
+
* first, so `nul .md` is the null device as well. The port digit may also be
|
|
12098
|
+
* one of the superscript digits ¹ ² ³, which Win32 reads as 1, 2 and 3
|
|
12099
|
+
* (`RtlIsDosDeviceName_U`; CPython's `ntpath.isreserved` follows the same
|
|
12100
|
+
* rules).
|
|
12101
|
+
*/
|
|
12102
|
+
const RESERVED_DEVICE_NAME_PATTERN = /^(?:CON|PRN|AUX|NUL|CONIN\$|CONOUT\$|COM[1-9\u00B9\u00B2\u00B3]|LPT[1-9\u00B9\u00B2\u00B3]) *(?:\..*)?$/i;
|
|
12103
|
+
/**
|
|
12104
|
+
* Whether Windows resolves a path segment to something other than the entry it
|
|
12105
|
+
* reads as.
|
|
12106
|
+
*
|
|
12107
|
+
* Win32 drops a trailing dot or ASCII space when it resolves a path, so a remote
|
|
12108
|
+
* `skills/deploy.` is the existing `skills/deploy` there. A name ending in the
|
|
12109
|
+
* `NAME~1` shape is the same class of problem: on a volume that generates
|
|
12110
|
+
* short names, it opens whatever long name it stands for. The shape only
|
|
12111
|
+
* means that at the end of a name, so `data~2parser` and `v1.0` are ordinary
|
|
12112
|
+
* names. A reserved device name is the extreme case: `nul` and `nul.md` are
|
|
12113
|
+
* not entries in any directory but the null device itself, so a write to one
|
|
12114
|
+
* lands nowhere, and a directory of that name cannot be created at all.
|
|
12115
|
+
*
|
|
12116
|
+
* The predicate is asked of every segment of a written path — the skill
|
|
12117
|
+
* directory, but equally a nested `references/notes. ` or a `scripts/con.sh`
|
|
12118
|
+
* — since a folded segment anywhere in a path lands the file beside, or on top
|
|
12119
|
+
* of, a sibling the repository never published. The check is not gated on the
|
|
12120
|
+
* platform: the same repository is checked out on several of them, and a name
|
|
12121
|
+
* that lands somewhere else on any one of them is one this tool would rather
|
|
12122
|
+
* turn away everywhere than write differently depending on where it runs.
|
|
12123
|
+
*/
|
|
12124
|
+
function hasWindowsFoldableName(name) {
|
|
12125
|
+
return TRAILING_DOT_OR_SPACE_PATTERN.test(name) || SHORT_NAME_ALIAS_PATTERN.test(name) || RESERVED_DEVICE_NAME_PATTERN.test(name);
|
|
12126
|
+
}
|
|
12127
|
+
/**
|
|
12128
|
+
* Whether a remote name is safe to write and to print as it is.
|
|
12129
|
+
*
|
|
12130
|
+
* A name of nothing but blank space is shown as the empty string, the same as
|
|
12131
|
+
* one that strips down to nothing: both draw as a row with no name on it. So
|
|
12132
|
+
* is a name of nothing but combining marks, which have no character of their
|
|
12133
|
+
* own to sit on and draw as a smear over whatever the terminal puts beside
|
|
12134
|
+
* them. A name that merely begins with a mark is turned away for the same
|
|
12135
|
+
* reason, and shown without it: the mark sits on nothing the name owns. A name
|
|
12136
|
+
* carrying control characters (an ANSI, C1 or OSC sequence) or deceptive
|
|
12137
|
+
* zero-width characters is turned away too, so it can neither rewrite the
|
|
12138
|
+
* terminal nor pass for another name. The Windows-folding check comes after
|
|
12139
|
+
* the hidden characters, so a name that is both is reported by the reason that
|
|
12140
|
+
* keeps its raw form off the terminal.
|
|
12141
|
+
*/
|
|
12142
|
+
function classifyRemoteName(name) {
|
|
12143
|
+
const stripped = stripHiddenCharacters(name);
|
|
12144
|
+
const beginsWithMark = LEADING_MARKS_PATTERN.test(stripped);
|
|
12145
|
+
const display = NOTHING_DRAWN_PATTERN.test(stripped) ? "" : stripped.replace(LEADING_MARKS_PATTERN, "");
|
|
12146
|
+
if (display === "" || beginsWithMark || hasDeceptiveHiddenCharacters(name)) return {
|
|
12147
|
+
kind: "unsafe",
|
|
12148
|
+
display,
|
|
12149
|
+
reason: "hidden"
|
|
12150
|
+
};
|
|
12151
|
+
if (hasWindowsFoldableName(name)) return {
|
|
12152
|
+
kind: "unsafe",
|
|
12153
|
+
display,
|
|
12154
|
+
reason: "foldable"
|
|
12155
|
+
};
|
|
12156
|
+
return { kind: "safe" };
|
|
12157
|
+
}
|
|
12000
12158
|
// src/features/rules/instruction-budget.ts
|
|
12001
12159
|
/**
|
|
12002
12160
|
* Normalizes a relative directory path for comparison: POSIX separators, no
|
|
@@ -13884,7 +14042,11 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13884
14042
|
},
|
|
13885
14043
|
permissions: {
|
|
13886
14044
|
kind: "replace-owned-keys",
|
|
13887
|
-
ownedKeys: [
|
|
14045
|
+
ownedKeys: [
|
|
14046
|
+
"permissions",
|
|
14047
|
+
"sandbox",
|
|
14048
|
+
"permission_mode"
|
|
14049
|
+
]
|
|
13888
14050
|
}
|
|
13889
14051
|
}
|
|
13890
14052
|
},
|
|
@@ -14658,6 +14820,16 @@ var RulesyncSourceConsumer = class {
|
|
|
14658
14820
|
this.dryRun = dryRun;
|
|
14659
14821
|
this.logger = logger;
|
|
14660
14822
|
}
|
|
14823
|
+
/**
|
|
14824
|
+
* Whether the orphan sweep may follow a symbolic link that stays inside the
|
|
14825
|
+
* output root. Only global scope answers true: its root is the operator's
|
|
14826
|
+
* own home directory, where a dotfiles checkout linked in (`~/.claude ->
|
|
14827
|
+
* ~/dotfiles/claude`) is a common shape. A project checkout may carry links
|
|
14828
|
+
* its author committed, so the default refuses them.
|
|
14829
|
+
*/
|
|
14830
|
+
sweepsThroughInRootLinks() {
|
|
14831
|
+
return false;
|
|
14832
|
+
}
|
|
14661
14833
|
recordRulesyncSourceLoadFailure() {
|
|
14662
14834
|
this.rulesyncSourceLoadFailed = true;
|
|
14663
14835
|
}
|
|
@@ -14790,7 +14962,8 @@ var FeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
14790
14962
|
refused = await refusesDeleteOutsideRoot({
|
|
14791
14963
|
logger: this.logger,
|
|
14792
14964
|
rootPath,
|
|
14793
|
-
dirPath
|
|
14965
|
+
dirPath,
|
|
14966
|
+
allowInRootLinks: this.sweepsThroughInRootLinks()
|
|
14794
14967
|
});
|
|
14795
14968
|
refusedDirs.set(cacheKey, refused);
|
|
14796
14969
|
}
|
|
@@ -14890,27 +15063,45 @@ function mergeByIdentity({ perRoot, identity }) {
|
|
|
14890
15063
|
return order.map((key) => winnerByKey.get(key));
|
|
14891
15064
|
}
|
|
14892
15065
|
/**
|
|
14893
|
-
*
|
|
14894
|
-
* there leads out of `rootPath`,
|
|
15066
|
+
* Why a write to `targetPath` has to be refused, or `null` when it may go
|
|
15067
|
+
* ahead: a link on the way there leads out of `rootPath`, or into
|
|
15068
|
+
* version-control metadata that the path as spelled does not name.
|
|
14895
15069
|
*
|
|
14896
15070
|
* A checked-out repository can carry a symbolic link at an output path
|
|
14897
|
-
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`
|
|
14898
|
-
* through it would land wherever the link points.
|
|
14899
|
-
|
|
14900
|
-
|
|
15071
|
+
* (`AGENTS.md -> ~/.bashrc`, a tool directory linked to `/etc`, `CLAUDE.md ->
|
|
15072
|
+
* .git/config`), and a write through it would land wherever the link points.
|
|
15073
|
+
*/
|
|
15074
|
+
async function writeRefusalReason({ rootPath, targetPath }) {
|
|
15075
|
+
const relativeLanding = await relativeWriteLanding({
|
|
15076
|
+
rootPath,
|
|
15077
|
+
targetPath
|
|
15078
|
+
});
|
|
15079
|
+
if (relativeLanding === null || pathEscapesRoot(relativeLanding)) return `it resolves outside ${quoteForLog(rootPath)} through a symbolic link`;
|
|
15080
|
+
if (landingEntersVcsMetadata({
|
|
15081
|
+
rootPath,
|
|
15082
|
+
targetPath,
|
|
15083
|
+
relativeLanding
|
|
15084
|
+
})) return "it resolves into version-control metadata through a symbolic link";
|
|
15085
|
+
return null;
|
|
15086
|
+
}
|
|
15087
|
+
/**
|
|
15088
|
+
* Whether a write to `targetPath` has to be refused (see
|
|
15089
|
+
* {@link writeRefusalReason}), warning about the refusal. Every writer that
|
|
15090
|
+
* lets the OS follow links — file, directory, and config-rewrite alike — asks
|
|
15091
|
+
* this first so that one warning describes the one behavior.
|
|
14901
15092
|
*/
|
|
14902
15093
|
async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
14903
|
-
|
|
15094
|
+
const reason = await writeRefusalReason({
|
|
14904
15095
|
rootPath,
|
|
14905
15096
|
targetPath
|
|
14906
|
-
})
|
|
14907
|
-
|
|
15097
|
+
});
|
|
15098
|
+
if (reason === null) return false;
|
|
15099
|
+
logger.warn(`Refusing to write ${quoteForLog(targetPath)}: ${reason}`);
|
|
14908
15100
|
return true;
|
|
14909
15101
|
}
|
|
14910
15102
|
/**
|
|
14911
|
-
*
|
|
14912
|
-
*
|
|
14913
|
-
* once for the whole directory.
|
|
15103
|
+
* Why the orphan sweep has to leave the files in `dirPath` alone, or `null`
|
|
15104
|
+
* when it may delete them.
|
|
14914
15105
|
*
|
|
14915
15106
|
* Deletion candidates are enumerated without following links *inside* the
|
|
14916
15107
|
* scanned directory, but the scanned directory itself is reached through
|
|
@@ -14919,13 +15110,40 @@ async function refusesWriteOutsideRoot({ logger, rootPath, targetPath }) {
|
|
|
14919
15110
|
* an orphan. Only the directory is judged, never the file in it, because
|
|
14920
15111
|
* removing a file that is itself a link unlinks the link where it is spelled
|
|
14921
15112
|
* and never touches its target.
|
|
15113
|
+
*
|
|
15114
|
+
* Beyond the write rules, a project checkout is content another party may
|
|
15115
|
+
* control: a committed `.claude/rules -> ..` would make every untracked `*.md`
|
|
15116
|
+
* in the project read as an orphan. Unless `allowInRootLinks` (global scope,
|
|
15117
|
+
* where the root is the operator's own home directory) the swept directory
|
|
15118
|
+
* must therefore be the one its path spells, with no link on the way.
|
|
14922
15119
|
*/
|
|
14923
|
-
async function
|
|
14924
|
-
|
|
15120
|
+
async function sweepRefusalReason({ rootPath, dirPath, allowInRootLinks }) {
|
|
15121
|
+
const relativeLanding = await relativeWriteLanding({
|
|
14925
15122
|
rootPath,
|
|
14926
15123
|
targetPath: dirPath
|
|
14927
|
-
})
|
|
14928
|
-
|
|
15124
|
+
});
|
|
15125
|
+
if (relativeLanding === null || pathEscapesRoot(relativeLanding)) return `it resolves outside ${quoteForLog(rootPath)} through a symbolic link`;
|
|
15126
|
+
if (landingEntersVcsMetadata({
|
|
15127
|
+
rootPath,
|
|
15128
|
+
targetPath: dirPath,
|
|
15129
|
+
relativeLanding
|
|
15130
|
+
})) return "it resolves into version-control metadata through a symbolic link";
|
|
15131
|
+
if (!allowInRootLinks && relativeLanding !== (0, node_path.relative)((0, node_path.resolve)(rootPath), (0, node_path.resolve)(dirPath))) return "it is reached through a symbolic link";
|
|
15132
|
+
return null;
|
|
15133
|
+
}
|
|
15134
|
+
/**
|
|
15135
|
+
* Whether deleting the files in `dirPath` has to be refused (see
|
|
15136
|
+
* {@link sweepRefusalReason}), warning about the refusal once for the whole
|
|
15137
|
+
* directory.
|
|
15138
|
+
*/
|
|
15139
|
+
async function refusesDeleteOutsideRoot({ logger, rootPath, dirPath, allowInRootLinks }) {
|
|
15140
|
+
const reason = await sweepRefusalReason({
|
|
15141
|
+
rootPath,
|
|
15142
|
+
dirPath,
|
|
15143
|
+
allowInRootLinks
|
|
15144
|
+
});
|
|
15145
|
+
if (reason === null) return false;
|
|
15146
|
+
logger.warn(`Refusing to delete files in ${quoteForLog(dirPath)}: ${reason}`);
|
|
14929
15147
|
return true;
|
|
14930
15148
|
}
|
|
14931
15149
|
/**
|
|
@@ -22636,6 +22854,9 @@ const commandsProcessorToolTargetsGlobal = allToolTargetKeys$3.filter((target) =
|
|
|
22636
22854
|
var CommandsProcessor = class extends FeatureProcessor {
|
|
22637
22855
|
toolTarget;
|
|
22638
22856
|
global;
|
|
22857
|
+
sweepsThroughInRootLinks() {
|
|
22858
|
+
return this.global;
|
|
22859
|
+
}
|
|
22639
22860
|
getFactory;
|
|
22640
22861
|
flattenedCommandNaming;
|
|
22641
22862
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$6, dryRun = false, flattenedCommandNaming = "basename", logger }) {
|
|
@@ -23207,6 +23428,15 @@ var DirFeatureProcessor = class extends RulesyncSourceConsumer {
|
|
|
23207
23428
|
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${stripControlCharacters(formatError(error))}`);
|
|
23208
23429
|
continue;
|
|
23209
23430
|
}
|
|
23431
|
+
const sweepRefusal = await sweepRefusalReason({
|
|
23432
|
+
rootPath: this.outputRoot,
|
|
23433
|
+
dirPath,
|
|
23434
|
+
allowInRootLinks: this.sweepsThroughInRootLinks()
|
|
23435
|
+
});
|
|
23436
|
+
if (sweepRefusal !== null) {
|
|
23437
|
+
this.logger.warn(`Refusing to sweep ${quotedDirPath}: ${sweepRefusal}`);
|
|
23438
|
+
continue;
|
|
23439
|
+
}
|
|
23210
23440
|
const generatedNames = /* @__PURE__ */ new Set();
|
|
23211
23441
|
const mainFile = aiDir.getMainFile();
|
|
23212
23442
|
if (mainFile) generatedNames.add(toPosixPath(mainFile.name));
|
|
@@ -25290,6 +25520,8 @@ const CRUSH_CONFIG_FILE_NAME = "crush.json";
|
|
|
25290
25520
|
const CRUSH_HIDDEN_CONFIG_FILE_NAME = ".crush.json";
|
|
25291
25521
|
const CRUSH_PERMISSIONS_KEY = "permissions";
|
|
25292
25522
|
const CRUSH_ALLOWED_TOOLS_KEY = "allowed_tools";
|
|
25523
|
+
const CRUSH_SAFE_COMMANDS_KEY = "safe_commands";
|
|
25524
|
+
const CRUSH_BLOCKED_COMMANDS_KEY = "blocked_commands";
|
|
25293
25525
|
const CRUSH_OPTIONS_KEY = "options";
|
|
25294
25526
|
const CRUSH_DISABLED_TOOLS_KEY = "disabled_tools";
|
|
25295
25527
|
const CRUSH_HOOKS_KEY = "hooks";
|
|
@@ -29970,6 +30202,9 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29970
30202
|
global: this.global
|
|
29971
30203
|
});
|
|
29972
30204
|
}
|
|
30205
|
+
sweepsThroughInRootLinks() {
|
|
30206
|
+
return this.global;
|
|
30207
|
+
}
|
|
29973
30208
|
/**
|
|
29974
30209
|
* The tool's skills roots that exist on disk, each paired with its absolute
|
|
29975
30210
|
* path and vetted as writable inside this run's output root. Shared by the
|
|
@@ -29999,6 +30234,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29999
30234
|
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
30000
30235
|
continue;
|
|
30001
30236
|
}
|
|
30237
|
+
const sweepRefusal = await sweepRefusalReason({
|
|
30238
|
+
rootPath: this.outputRoot,
|
|
30239
|
+
dirPath: skillsDirPath,
|
|
30240
|
+
allowInRootLinks: this.sweepsThroughInRootLinks()
|
|
30241
|
+
});
|
|
30242
|
+
if (sweepRefusal !== null) {
|
|
30243
|
+
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${sweepRefusal}`);
|
|
30244
|
+
continue;
|
|
30245
|
+
}
|
|
30002
30246
|
existingRoots.push({
|
|
30003
30247
|
root,
|
|
30004
30248
|
skillsDirPath
|
|
@@ -33225,11 +33469,17 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33225
33469
|
*/
|
|
33226
33470
|
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
33227
33471
|
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
33228
|
-
const
|
|
33472
|
+
const agentsDirFullPath = (0, node_path.join)(outputRoot, agentsDirPath);
|
|
33473
|
+
const writeRefused = await writeRefusalReason({
|
|
33229
33474
|
rootPath: outputRoot,
|
|
33230
|
-
targetPath: (0, node_path.join)(
|
|
33231
|
-
});
|
|
33232
|
-
const
|
|
33475
|
+
targetPath: (0, node_path.join)(agentsDirFullPath, "agent.md")
|
|
33476
|
+
}) !== null;
|
|
33477
|
+
const sweepRefused = deleteOrphans && await sweepRefusalReason({
|
|
33478
|
+
rootPath: outputRoot,
|
|
33479
|
+
dirPath: agentsDirFullPath,
|
|
33480
|
+
allowInRootLinks: global
|
|
33481
|
+
}) !== null;
|
|
33482
|
+
const existingAgentFileNames = new Set(writeRefused ? [] : await listDirectoryEntryNames(agentsDirFullPath));
|
|
33233
33483
|
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
33234
33484
|
outputRoot,
|
|
33235
33485
|
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
@@ -33245,8 +33495,8 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33245
33495
|
global
|
|
33246
33496
|
})),
|
|
33247
33497
|
existingAgentFileNames,
|
|
33248
|
-
deleteOrphans,
|
|
33249
|
-
frozen:
|
|
33498
|
+
deleteOrphans: deleteOrphans && !sweepRefused,
|
|
33499
|
+
frozen: writeRefused,
|
|
33250
33500
|
global,
|
|
33251
33501
|
logger
|
|
33252
33502
|
});
|
|
@@ -37364,6 +37614,9 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
37364
37614
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
37365
37615
|
toolTarget;
|
|
37366
37616
|
global;
|
|
37617
|
+
sweepsThroughInRootLinks() {
|
|
37618
|
+
return this.global;
|
|
37619
|
+
}
|
|
37367
37620
|
deleteOrphans;
|
|
37368
37621
|
getFactory;
|
|
37369
37622
|
/**
|
|
@@ -38085,6 +38338,39 @@ function warnAndSkipMcpServer({ toolName, serverName, reason, logger }) {
|
|
|
38085
38338
|
return null;
|
|
38086
38339
|
}
|
|
38087
38340
|
/**
|
|
38341
|
+
* Drops every server whose `enabledTools` allowlist is empty, warning about
|
|
38342
|
+
* each one, for a tool that reads an empty (or missing) allowlist as "no
|
|
38343
|
+
* filter". Canonically an empty allowlist allows no tools at all, so emitting
|
|
38344
|
+
* the server unfiltered would expose every tool it reports. With
|
|
38345
|
+
* `ignoreBlankNames`, blank entries count as absent, for a tool that drops
|
|
38346
|
+
* them before applying the list. `productName` names the tool in the reason
|
|
38347
|
+
* when it differs from the `toolName` the warning starts with.
|
|
38348
|
+
*/
|
|
38349
|
+
function skipServersWithEmptyEnabledTools({ mcpServers, toolName, logger, ignoreBlankNames = false, productName = toolName }) {
|
|
38350
|
+
const kept = {};
|
|
38351
|
+
for (const [name, server] of Object.entries(mcpServers)) {
|
|
38352
|
+
const { enabledTools } = server;
|
|
38353
|
+
if (Array.isArray(enabledTools)) {
|
|
38354
|
+
if ((ignoreBlankNames ? enabledTools.filter((tool) => typeof tool !== "string" || tool.trim() !== "") : enabledTools).length === 0) {
|
|
38355
|
+
warnAndSkipMcpServer({
|
|
38356
|
+
toolName,
|
|
38357
|
+
serverName: name,
|
|
38358
|
+
reason: `an empty enabledTools list, which allows no tools at all but which ${productName} treats as no filter`,
|
|
38359
|
+
logger
|
|
38360
|
+
});
|
|
38361
|
+
continue;
|
|
38362
|
+
}
|
|
38363
|
+
}
|
|
38364
|
+
Object.defineProperty(kept, name, {
|
|
38365
|
+
value: server,
|
|
38366
|
+
enumerable: true,
|
|
38367
|
+
writable: true,
|
|
38368
|
+
configurable: true
|
|
38369
|
+
});
|
|
38370
|
+
}
|
|
38371
|
+
return kept;
|
|
38372
|
+
}
|
|
38373
|
+
/**
|
|
38088
38374
|
* The `tools` map Kilo and OpenCode keep beside `mcp` is keyed `<server>_<tool>`
|
|
38089
38375
|
* and reaches servers `mcp` does not list — ones the global config or a
|
|
38090
38376
|
* marketplace defines — so entries naming no listed
|
|
@@ -38646,7 +38932,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
38646
38932
|
const existingMcp = readExistingKiloMcpEntries(fileContent);
|
|
38647
38933
|
const parsedExisting = (0, jsonc_parser.parse)(fileContent || "{}");
|
|
38648
38934
|
const existingDocument = isRecord(parsedExisting) ? parsedExisting : {};
|
|
38649
|
-
const mcpServers =
|
|
38935
|
+
const mcpServers = skipServersWithEmptyEnabledTools({
|
|
38936
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
38937
|
+
toolName: "Kilo",
|
|
38938
|
+
logger
|
|
38939
|
+
});
|
|
38650
38940
|
const { mcp: convertedMcp, toolPermissions } = convertToKiloFormat(resolveKiloEnvVarRefs({
|
|
38651
38941
|
mcpServers,
|
|
38652
38942
|
global,
|
|
@@ -38775,7 +39065,8 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
|
38775
39065
|
"timeout",
|
|
38776
39066
|
"oauth",
|
|
38777
39067
|
"protocol",
|
|
38778
|
-
"codemode"
|
|
39068
|
+
"codemode",
|
|
39069
|
+
"pool"
|
|
38779
39070
|
];
|
|
38780
39071
|
const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
38781
39072
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
@@ -38938,7 +39229,7 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
|
38938
39229
|
* - env -> environment
|
|
38939
39230
|
* - disabled -> enabled (inverted)
|
|
38940
39231
|
* - enabledTools/disabledTools -> top-level tools map (with server name prefix)
|
|
38941
|
-
* - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
|
|
39232
|
+
* - OpenCode-supported extras (timeout, oauth, protocol, codemode, pool) -> passed through verbatim
|
|
38942
39233
|
*/
|
|
38943
39234
|
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, configShape, logger) {
|
|
38944
39235
|
const serverRecord = serverConfig;
|
|
@@ -39178,7 +39469,11 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
39178
39469
|
if (fileContent) relativeFilePath = this.layout.jsonFileName;
|
|
39179
39470
|
}
|
|
39180
39471
|
const transformedServers = convertEnvVarRefsToToolFormat({
|
|
39181
|
-
mcpServers:
|
|
39472
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
39473
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
39474
|
+
toolName: "OpenCode",
|
|
39475
|
+
logger
|
|
39476
|
+
}),
|
|
39182
39477
|
replacement: "{env:$1}"
|
|
39183
39478
|
});
|
|
39184
39479
|
const existingJson = (0, jsonc_parser.parse)(fileContent || "{}");
|
|
@@ -48774,6 +49069,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
48774
49069
|
language;
|
|
48775
49070
|
deriveSubprojectPathFromGlobs;
|
|
48776
49071
|
global;
|
|
49072
|
+
sweepsThroughInRootLinks() {
|
|
49073
|
+
return this.global;
|
|
49074
|
+
}
|
|
48777
49075
|
getFactory;
|
|
48778
49076
|
skills;
|
|
48779
49077
|
featureOptions;
|
|
@@ -51358,12 +51656,29 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
51358
51656
|
};
|
|
51359
51657
|
}
|
|
51360
51658
|
}
|
|
51659
|
+
/**
|
|
51660
|
+
* The `.rulesync/checks/<stem>.md` name an imported spec gets. The slug is
|
|
51661
|
+
* read from a spec file that may come from a fetched or cloned repository,
|
|
51662
|
+
* so it is kept as written only when it is a single plain file name; a slug
|
|
51663
|
+
* such as `../../docs/OPERATOR` would otherwise move the write out of
|
|
51664
|
+
* `.rulesync/checks/`. Anything else is slugified, and an empty result falls
|
|
51665
|
+
* back to the spec file's own name.
|
|
51666
|
+
*/
|
|
51667
|
+
importedCheckStem(slug) {
|
|
51668
|
+
const isPlainFileName = (name) => name !== "" && name !== "." && name !== ".." && !/[/\\<>:"|?*]/.test(name) && !hasWindowsFoldableName(name) && stripControlCharacters(name) === name;
|
|
51669
|
+
if (isPlainFileName(slug)) return slug;
|
|
51670
|
+
for (const candidate of [slugifyCheckName(slug), slugifyCheckName((0, node_path.basename)(this.relativeFilePath, ".json"))]) {
|
|
51671
|
+
if (candidate === "") continue;
|
|
51672
|
+
return isPlainFileName(candidate) ? candidate : `${candidate}-check`;
|
|
51673
|
+
}
|
|
51674
|
+
return "check";
|
|
51675
|
+
}
|
|
51361
51676
|
toRulesyncCheck() {
|
|
51362
51677
|
const spec = HermesCheckSpecSchema.parse(JSON.parse(this.fileContent));
|
|
51363
51678
|
return new RulesyncCheck({
|
|
51364
51679
|
outputRoot: ".",
|
|
51365
51680
|
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
51366
|
-
relativeFilePath: `${spec.slug}.md`,
|
|
51681
|
+
relativeFilePath: `${this.importedCheckStem(spec.slug)}.md`,
|
|
51367
51682
|
frontmatter: {
|
|
51368
51683
|
targets: ["*"],
|
|
51369
51684
|
description: spec.description,
|
|
@@ -51871,6 +52186,9 @@ const checksProcessorToolTargetsGlobal = allToolTargetKeys.filter((target) => {
|
|
|
51871
52186
|
var ChecksProcessor = class extends FeatureProcessor {
|
|
51872
52187
|
toolTarget;
|
|
51873
52188
|
global;
|
|
52189
|
+
sweepsThroughInRootLinks() {
|
|
52190
|
+
return this.global;
|
|
52191
|
+
}
|
|
51874
52192
|
getFactory;
|
|
51875
52193
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
51876
52194
|
super({
|
|
@@ -54182,6 +54500,10 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
|
54182
54500
|
field: "tool",
|
|
54183
54501
|
value: h.tool
|
|
54184
54502
|
});
|
|
54503
|
+
for (const { canonical, tool } of converterConfig.stringPassthroughFields ?? []) if (canonical === "if" && h[tool] !== "") fields.push({
|
|
54504
|
+
field: "if",
|
|
54505
|
+
value: h[tool]
|
|
54506
|
+
});
|
|
54185
54507
|
return fields;
|
|
54186
54508
|
}
|
|
54187
54509
|
/**
|
|
@@ -62958,10 +63280,12 @@ function emittedEventsWithHook(params) {
|
|
|
62958
63280
|
function converterCapabilities(config) {
|
|
62959
63281
|
if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
|
|
62960
63282
|
const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
|
|
63283
|
+
const passesThroughIf = (config.stringPassthroughFields ?? []).some((field) => field.canonical === "if") && !(config.skippedWhenFields ?? []).includes("if");
|
|
62961
63284
|
return {
|
|
62962
63285
|
supportedEvents: config.supportedEvents,
|
|
62963
63286
|
supportedHookTypes: [...config.supportedHookTypes],
|
|
62964
|
-
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
|
|
63287
|
+
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {},
|
|
63288
|
+
...passesThroughIf ? { supportsPerHookIf: true } : {}
|
|
62965
63289
|
};
|
|
62966
63290
|
}
|
|
62967
63291
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
@@ -63606,6 +63930,9 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
63606
63930
|
var HooksProcessor = class extends FeatureProcessor {
|
|
63607
63931
|
toolTarget;
|
|
63608
63932
|
global;
|
|
63933
|
+
sweepsThroughInRootLinks() {
|
|
63934
|
+
return this.global;
|
|
63935
|
+
}
|
|
63609
63936
|
preserveUnownedHooks;
|
|
63610
63937
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, preserveUnownedHooks = false, logger }) {
|
|
63611
63938
|
super({
|
|
@@ -63684,7 +64011,8 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63684
64011
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
63685
64012
|
const config = rulesyncHooks.getJson();
|
|
63686
64013
|
const sharedHooks = config.hooks;
|
|
63687
|
-
const
|
|
64014
|
+
const overrideKey = HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget;
|
|
64015
|
+
const overrideHooks = config[overrideKey]?.hooks ?? {};
|
|
63688
64016
|
const effectiveHooks = {
|
|
63689
64017
|
...sharedHooks,
|
|
63690
64018
|
...overrideHooks
|
|
@@ -63722,7 +64050,10 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63722
64050
|
if (eventsWithUnsupportedMatcher.length > 0) this.logger.warn(`Skipped matcher hook(s) for ${this.toolTarget} (not supported): ${eventsWithUnsupportedMatcher.join(", ")}`);
|
|
63723
64051
|
const toolHooks = await factory.class.fromRulesyncHooks({
|
|
63724
64052
|
outputRoot: this.outputRoot,
|
|
63725
|
-
rulesyncHooks
|
|
64053
|
+
rulesyncHooks: factory.supportsPerHookIf ? rulesyncHooks : this.withoutConditionalHooks({
|
|
64054
|
+
rulesyncHooks,
|
|
64055
|
+
overrideKey
|
|
64056
|
+
}),
|
|
63726
64057
|
validate: true,
|
|
63727
64058
|
global: this.global,
|
|
63728
64059
|
preserveUnowned: this.preserveUnownedHooks,
|
|
@@ -63745,6 +64076,53 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63745
64076
|
return result;
|
|
63746
64077
|
}
|
|
63747
64078
|
/**
|
|
64079
|
+
* Returns the hooks source without the definitions that carry a per-hook
|
|
64080
|
+
* `if` condition, warning about each one. Used for targets that cannot
|
|
64081
|
+
* express `if`: emitting such a hook without its condition would make it
|
|
64082
|
+
* fire on every call its matcher selects, so a permission-granting hook
|
|
64083
|
+
* scoped by `if` would approve far more than its author meant.
|
|
64084
|
+
*/
|
|
64085
|
+
withoutConditionalHooks(params) {
|
|
64086
|
+
const { rulesyncHooks, overrideKey } = params;
|
|
64087
|
+
const config = rulesyncHooks.getJson();
|
|
64088
|
+
const skipped = [];
|
|
64089
|
+
const filterHooks = (hooks) => {
|
|
64090
|
+
if (!isPlainObject(hooks)) return hooks;
|
|
64091
|
+
const filtered = {};
|
|
64092
|
+
for (const [event, defs] of Object.entries(hooks)) {
|
|
64093
|
+
if (!Array.isArray(defs)) {
|
|
64094
|
+
filtered[event] = defs;
|
|
64095
|
+
continue;
|
|
64096
|
+
}
|
|
64097
|
+
filtered[event] = defs.filter((def) => {
|
|
64098
|
+
const condition = isPlainObject(def) ? def.if : void 0;
|
|
64099
|
+
if (typeof condition !== "string" || condition.trim() === "") return true;
|
|
64100
|
+
skipped.push(`${event} (${quoteValueForWarning(condition)})`);
|
|
64101
|
+
return false;
|
|
64102
|
+
});
|
|
64103
|
+
}
|
|
64104
|
+
return filtered;
|
|
64105
|
+
};
|
|
64106
|
+
const filteredConfig = {
|
|
64107
|
+
...config,
|
|
64108
|
+
hooks: filterHooks(config.hooks)
|
|
64109
|
+
};
|
|
64110
|
+
const override = config[overrideKey];
|
|
64111
|
+
if (Object.hasOwn(config, overrideKey) && isPlainObject(override)) filteredConfig[overrideKey] = {
|
|
64112
|
+
...override,
|
|
64113
|
+
hooks: filterHooks(override.hooks)
|
|
64114
|
+
};
|
|
64115
|
+
if (skipped.length === 0) return rulesyncHooks;
|
|
64116
|
+
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(", ")}`);
|
|
64117
|
+
return new RulesyncHooks({
|
|
64118
|
+
outputRoot: rulesyncHooks.getOutputRoot(),
|
|
64119
|
+
relativeDirPath: rulesyncHooks.getRelativeDirPath(),
|
|
64120
|
+
relativeFilePath: rulesyncHooks.getRelativeFilePath(),
|
|
64121
|
+
fileContent: JSON.stringify(filteredConfig),
|
|
64122
|
+
validate: false
|
|
64123
|
+
});
|
|
64124
|
+
}
|
|
64125
|
+
/**
|
|
63748
64126
|
* Warns about canonical per-hook fields that this target's hooks format
|
|
63749
64127
|
* cannot express, so a hook does not silently change behavior on generation.
|
|
63750
64128
|
* Only canonical definitions are considered: a tool-native field inside an
|
|
@@ -63989,6 +64367,35 @@ var BobIgnore = class extends PlainToolIgnore {
|
|
|
63989
64367
|
};
|
|
63990
64368
|
}
|
|
63991
64369
|
};
|
|
64370
|
+
// src/features/ignore/claude-read-deny-pattern.ts
|
|
64371
|
+
/**
|
|
64372
|
+
* Translate the path of a Claude Code style `Read(<path>)` deny into the
|
|
64373
|
+
* gitignore syntax of `.rulesync/.aiignore`.
|
|
64374
|
+
*
|
|
64375
|
+
* Claude Code reads the path forms differently from gitignore: `./path` is
|
|
64376
|
+
* relative to the current directory, `/path` to the project root, `//path` is
|
|
64377
|
+
* absolute and `~/path` is under the home directory. Copied verbatim, the
|
|
64378
|
+
* common `Read(./.env)` idiom would become `./.env`, which matches nothing in
|
|
64379
|
+
* a gitignore-syntax ignore file, so every other tool would lose the deny.
|
|
64380
|
+
*
|
|
64381
|
+
* - `./path` is anchored to the project root as `/path`.
|
|
64382
|
+
* - `//path` and `~/path` (or a bare `~`) name locations a project-relative
|
|
64383
|
+
* ignore file cannot express; they are skipped with a warning and `null` is
|
|
64384
|
+
* returned.
|
|
64385
|
+
* - Every other path (`.env`, `secrets/**`, `/path`) already means the same
|
|
64386
|
+
* project files in gitignore syntax and is kept as is.
|
|
64387
|
+
*/
|
|
64388
|
+
function claudeReadDenyPathToIgnorePattern({ path, toolLabel, logger }) {
|
|
64389
|
+
if (path.startsWith("//") || path === "~" || path.startsWith("~/")) {
|
|
64390
|
+
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.`);
|
|
64391
|
+
return null;
|
|
64392
|
+
}
|
|
64393
|
+
if (path.startsWith("./")) {
|
|
64394
|
+
const rest = path.slice(2).replace(/^(?:\.\/)+/, "");
|
|
64395
|
+
return rest.length > 0 ? `/${rest}` : null;
|
|
64396
|
+
}
|
|
64397
|
+
return path;
|
|
64398
|
+
}
|
|
63992
64399
|
// src/features/ignore/claudecode-ignore.ts
|
|
63993
64400
|
const DEFAULT_FILE_MODE = "shared";
|
|
63994
64401
|
/**
|
|
@@ -64047,9 +64454,12 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
64047
64454
|
}
|
|
64048
64455
|
toRulesyncIgnore() {
|
|
64049
64456
|
const fileContent = this.patterns.map((pattern) => {
|
|
64050
|
-
if (isReadDenyEntry(pattern)) return
|
|
64457
|
+
if (isReadDenyEntry(pattern)) return claudeReadDenyPathToIgnorePattern({
|
|
64458
|
+
path: pattern.slice(5, -1),
|
|
64459
|
+
toolLabel: "Claude Code"
|
|
64460
|
+
});
|
|
64051
64461
|
return pattern;
|
|
64052
|
-
}).filter((pattern) => pattern.length > 0).join("\n");
|
|
64462
|
+
}).filter((pattern) => pattern !== null && pattern.length > 0).join("\n");
|
|
64053
64463
|
return new RulesyncIgnore({
|
|
64054
64464
|
outputRoot: this.outputRoot,
|
|
64055
64465
|
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
@@ -64324,7 +64734,7 @@ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
|
64324
64734
|
const paths = this.getSettablePaths({ global });
|
|
64325
64735
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
64326
64736
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
64327
|
-
const managedPatterns =
|
|
64737
|
+
const managedPatterns = patterns.filter((pattern, index) => patterns.lastIndexOf(pattern) === index);
|
|
64328
64738
|
return new FirebenderIgnore({
|
|
64329
64739
|
outputRoot,
|
|
64330
64740
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -64979,7 +65389,10 @@ const asClaudeStyleSettings = (document) => {
|
|
|
64979
65389
|
};
|
|
64980
65390
|
/** The pattern of a `read_file(...)` (or legacy `Read(...)`) deny, else null. */
|
|
64981
65391
|
const readDenyPatternOf = (entry) => {
|
|
64982
|
-
for (const toolName of [REASONIX_READ_TOOL_NAME, REASONIX_LEGACY_READ_TOOL_NAME]) if (isReadDenyEntry(entry, toolName)) return
|
|
65392
|
+
for (const toolName of [REASONIX_READ_TOOL_NAME, REASONIX_LEGACY_READ_TOOL_NAME]) if (isReadDenyEntry(entry, toolName)) return claudeReadDenyPathToIgnorePattern({
|
|
65393
|
+
path: entry.slice(toolName.length + 1, -1),
|
|
65394
|
+
toolLabel: "Reasonix"
|
|
65395
|
+
});
|
|
64983
65396
|
return null;
|
|
64984
65397
|
};
|
|
64985
65398
|
/**
|
|
@@ -65319,6 +65732,9 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
65319
65732
|
getFactory;
|
|
65320
65733
|
featureOptions;
|
|
65321
65734
|
global;
|
|
65735
|
+
sweepsThroughInRootLinks() {
|
|
65736
|
+
return this.global;
|
|
65737
|
+
}
|
|
65322
65738
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$1, global = false, dryRun = false, logger, featureOptions }) {
|
|
65323
65739
|
super({
|
|
65324
65740
|
outputRoot,
|
|
@@ -65652,30 +66068,71 @@ function isEmptyValue(value) {
|
|
|
65652
66068
|
if (Array.isArray(value)) return value.length === 0;
|
|
65653
66069
|
return isPlainObject(value) && Object.keys(value).length === 0;
|
|
65654
66070
|
}
|
|
66071
|
+
const MILLISECONDS_PER_SECOND$4 = 1e3;
|
|
66072
|
+
/**
|
|
66073
|
+
* Convert the canonical `timeout` (milliseconds) into Antigravity's
|
|
66074
|
+
* `timeoutSeconds` (integer seconds), rounding up so a sub-second timeout does
|
|
66075
|
+
* not become `0`. A negative value disables the timeout upstream, so it is
|
|
66076
|
+
* written as `-1`. An authored native `timeoutSeconds` wins over `timeout`.
|
|
66077
|
+
*/
|
|
66078
|
+
function convertTimeoutToAntigravity({ serverName, config, logger }) {
|
|
66079
|
+
if (!("timeout" in config)) return config;
|
|
66080
|
+
const { timeout, ...rest } = config;
|
|
66081
|
+
if (rest.timeoutSeconds !== void 0) {
|
|
66082
|
+
logger?.warn(`Antigravity MCP: ${quoteValueForWarning(serverName)} sets both "timeout" and "timeoutSeconds"; keeping "timeoutSeconds" and dropping "timeout".`);
|
|
66083
|
+
return rest;
|
|
66084
|
+
}
|
|
66085
|
+
if (typeof timeout !== "number" || !Number.isFinite(timeout)) {
|
|
66086
|
+
logger?.warn(`Antigravity MCP: dropping the "timeout" of ${quoteValueForWarning(serverName)}: expected a number of milliseconds.`);
|
|
66087
|
+
return rest;
|
|
66088
|
+
}
|
|
66089
|
+
const timeoutSeconds = timeout < 0 ? -1 : Math.ceil(timeout / MILLISECONDS_PER_SECOND$4);
|
|
66090
|
+
return Object.fromEntries(Object.entries(config).map(([key, value]) => key === "timeout" ? ["timeoutSeconds", timeoutSeconds] : [key, value]));
|
|
66091
|
+
}
|
|
65655
66092
|
/**
|
|
65656
|
-
* Antigravity uses `serverUrl` (not the canonical `url`) for
|
|
66093
|
+
* Antigravity uses `serverUrl` (not the canonical `url`) for remote MCP
|
|
65657
66094
|
* servers. Convert the canonical shape into Antigravity's on the way out, and
|
|
65658
66095
|
* back to canonical on the way in.
|
|
65659
66096
|
*
|
|
65660
66097
|
* Antigravity documents no `type` field: the transport follows from `command`
|
|
65661
|
-
* (stdio) or `serverUrl` (Streamable HTTP
|
|
66098
|
+
* (stdio) or `serverUrl` (Streamable HTTP), and `agy plugin validate`
|
|
65662
66099
|
* reports the canonical `type` as an unknown field, so it is not written. A
|
|
65663
66100
|
* remote server that carries `command`, `args` or `env` was also observed to be
|
|
65664
66101
|
* refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
|
|
65665
66102
|
* not documented), so an empty `args` / `env` left on a `serverUrl` server is
|
|
65666
66103
|
* dropped.
|
|
66104
|
+
*
|
|
66105
|
+
* Remote servers must use Streamable HTTP: the legacy HTTP+SSE transport is
|
|
66106
|
+
* not supported, so a canonical `sse` server is still written but warned
|
|
66107
|
+
* about. The canonical `timeout` (milliseconds) becomes `timeoutSeconds`.
|
|
65667
66108
|
* https://antigravity.google/docs/mcp
|
|
65668
66109
|
*/
|
|
65669
|
-
function toAntigravityMcpServers(servers) {
|
|
66110
|
+
function toAntigravityMcpServers(servers, logger) {
|
|
65670
66111
|
const renamed = renameServerField(servers, "url", "serverUrl");
|
|
65671
66112
|
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
65672
66113
|
if (!isPlainObject(config)) return [name, config];
|
|
66114
|
+
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.`);
|
|
65673
66115
|
const isRemote = typeof config.serverUrl === "string";
|
|
65674
|
-
return [name,
|
|
66116
|
+
return [name, convertTimeoutToAntigravity({
|
|
66117
|
+
serverName: name,
|
|
66118
|
+
config: Object.fromEntries(Object.entries(config).filter(([key, value]) => key !== "type" && !(isRemote && (key === "args" || key === "env") && isEmptyValue(value)))),
|
|
66119
|
+
logger
|
|
66120
|
+
})];
|
|
65675
66121
|
}));
|
|
65676
66122
|
}
|
|
66123
|
+
/**
|
|
66124
|
+
* Convert Antigravity servers back to the canonical shape: `serverUrl` becomes
|
|
66125
|
+
* `url`, and `timeoutSeconds` becomes the canonical `timeout` in milliseconds
|
|
66126
|
+
* (a negative value, which disables the timeout upstream, stays `-1`).
|
|
66127
|
+
*/
|
|
65677
66128
|
function toCanonicalMcpServers(servers) {
|
|
65678
|
-
|
|
66129
|
+
const renamed = renameServerField(servers, "serverUrl", "url");
|
|
66130
|
+
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
66131
|
+
if (!isPlainObject(config) || typeof config.timeoutSeconds !== "number") return [name, config];
|
|
66132
|
+
const timeoutSeconds = config.timeoutSeconds;
|
|
66133
|
+
const timeout = timeoutSeconds < 0 ? -1 : timeoutSeconds * MILLISECONDS_PER_SECOND$4;
|
|
66134
|
+
return [name, Object.fromEntries(Object.entries(config).filter(([key]) => key !== "timeout").map(([key, value]) => key === "timeoutSeconds" ? ["timeout", timeout] : [key, value]))];
|
|
66135
|
+
}));
|
|
65679
66136
|
}
|
|
65680
66137
|
/**
|
|
65681
66138
|
* Parse `mcp_config.json` as JSONC and require a top-level object, so a file
|
|
@@ -65693,7 +66150,8 @@ function parseAntigravityMcpConfig(fileContent) {
|
|
|
65693
66150
|
* the IDE and the CLI.
|
|
65694
66151
|
*
|
|
65695
66152
|
* Antigravity writes a dedicated `mcp_config.json` whose `mcpServers` map lives
|
|
65696
|
-
* at the top level (`disabledTools` per server
|
|
66153
|
+
* at the top level (`enabledTools` / `disabledTools` per server are supported
|
|
66154
|
+
* under the same names). Unlike Gemini
|
|
65697
66155
|
* CLI's shared `settings.json`, this file is dedicated to MCP, so it is safely
|
|
65698
66156
|
* deletable. HTTP servers use `serverUrl` rather than the canonical `url`, so
|
|
65699
66157
|
* the field is translated in both directions.
|
|
@@ -65746,11 +66204,11 @@ var AntigravityMcp = class extends ToolMcp {
|
|
|
65746
66204
|
global
|
|
65747
66205
|
});
|
|
65748
66206
|
}
|
|
65749
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
66207
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
65750
66208
|
const paths = this.getSettablePaths({ global });
|
|
65751
66209
|
const newJson = {
|
|
65752
66210
|
...parseAntigravityMcpConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2)),
|
|
65753
|
-
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers())
|
|
66211
|
+
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers(), logger)
|
|
65754
66212
|
};
|
|
65755
66213
|
return new this({
|
|
65756
66214
|
outputRoot,
|
|
@@ -69419,7 +69877,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
69419
69877
|
ownedPaths: [["mcp"]],
|
|
69420
69878
|
logger
|
|
69421
69879
|
});
|
|
69422
|
-
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(
|
|
69880
|
+
const converted = convertToCrushFormat(Object.fromEntries(Object.entries(skipServersWithEmptyEnabledTools({
|
|
69881
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
69882
|
+
toolName: "Crush",
|
|
69883
|
+
logger
|
|
69884
|
+
})).map(([serverName, serverConfig]) => [serverName, {
|
|
69423
69885
|
...serverConfig,
|
|
69424
69886
|
...readCrushOnlyKeys({
|
|
69425
69887
|
serverName,
|
|
@@ -70379,31 +70841,6 @@ var DshMcp = class DshMcp extends ToolMcp {
|
|
|
70379
70841
|
};
|
|
70380
70842
|
// src/features/mcp/factorydroid-mcp.ts
|
|
70381
70843
|
const TOOL_NAME = "factorydroid";
|
|
70382
|
-
/**
|
|
70383
|
-
* Drop servers whose `enabledTools` allowlist is empty.
|
|
70384
|
-
*
|
|
70385
|
-
* Canonically an empty allowlist allows no tools, but Droid applies
|
|
70386
|
-
* `enabledTools` only "when the list is non-empty"
|
|
70387
|
-
* (https://docs.factory.com/harness/mcp), so writing `[]` would load every
|
|
70388
|
-
* tool the server reports. Skipping the server is the only form that does not
|
|
70389
|
-
* hand the model more tools than the canonical config allows.
|
|
70390
|
-
*/
|
|
70391
|
-
function skipEmptyAllowlistServers({ mcpServers, logger }) {
|
|
70392
|
-
const kept = {};
|
|
70393
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
70394
|
-
if (server.enabledTools?.length === 0) {
|
|
70395
|
-
warnAndSkipMcpServer({
|
|
70396
|
-
toolName: TOOL_NAME,
|
|
70397
|
-
serverName: name,
|
|
70398
|
-
reason: "an empty enabledTools list, which allows no tools at all but which Factory Droid treats as no filter",
|
|
70399
|
-
logger
|
|
70400
|
-
});
|
|
70401
|
-
continue;
|
|
70402
|
-
}
|
|
70403
|
-
kept[name] = server;
|
|
70404
|
-
}
|
|
70405
|
-
return kept;
|
|
70406
|
-
}
|
|
70407
70844
|
var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
70408
70845
|
json;
|
|
70409
70846
|
constructor(params) {
|
|
@@ -70430,8 +70867,10 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
70430
70867
|
});
|
|
70431
70868
|
}
|
|
70432
70869
|
static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger }) {
|
|
70433
|
-
const factorydroidConfig = { mcpServers:
|
|
70870
|
+
const factorydroidConfig = { mcpServers: skipServersWithEmptyEnabledTools({
|
|
70434
70871
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
70872
|
+
toolName: TOOL_NAME,
|
|
70873
|
+
productName: "Factory Droid",
|
|
70435
70874
|
logger
|
|
70436
70875
|
}) };
|
|
70437
70876
|
const fileContent = JSON.stringify(factorydroidConfig, null, 2);
|
|
@@ -71862,19 +72301,42 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
|
|
|
71862
72301
|
return hasCommand ? "stdio" : "http";
|
|
71863
72302
|
}
|
|
71864
72303
|
/**
|
|
71865
|
-
* The only `executor` value Kimi Code accepts on a stdio
|
|
71866
|
-
* the never-implemented `kaos` option and narrowed the
|
|
71867
|
-
* `z.enum(['local'])`, so
|
|
71868
|
-
*
|
|
72304
|
+
* The only `executor` value every current Kimi Code release accepts on a stdio
|
|
72305
|
+
* server. 2.2.0 removed the never-implemented `kaos` option and narrowed the
|
|
72306
|
+
* schema to `z.enum(['local'])`, so 2.2.0 through 2.4.x fail validation on any
|
|
72307
|
+
* other value when they load `mcp.json`. 2.5.0 widened the enum back to
|
|
72308
|
+
* `'local' | 'kaos'`, but `kaos` is still undocumented and would break the
|
|
72309
|
+
* older releases, so it keeps being dropped.
|
|
71869
72310
|
* @see https://github.com/MoonshotAI/kimi-code/pull/4184
|
|
72311
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4062
|
|
71870
72312
|
*/
|
|
71871
72313
|
const KIMI_CODE_MCP_EXECUTOR = "local";
|
|
71872
72314
|
/** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
|
|
71873
72315
|
function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
|
|
71874
72316
|
if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
|
|
71875
|
-
warnWithFallback(logger, `Kimi Code MCP: dropping executor ${JSON.stringify(server.executor)} from "${name}" because Kimi Code only
|
|
72317
|
+
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}".`);
|
|
71876
72318
|
delete server.executor;
|
|
71877
72319
|
}
|
|
72320
|
+
/**
|
|
72321
|
+
* Remove, with a warning, an `environment_id` Kimi Code cannot use. Kimi Code
|
|
72322
|
+
* 2.5.0 added it to stdio servers only, as a non-empty string naming an
|
|
72323
|
+
* `[environments.<id>]` entry of `config.toml`; an empty or non-string value
|
|
72324
|
+
* fails Kimi's validation. Mutates `server`.
|
|
72325
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4062
|
|
72326
|
+
*/
|
|
72327
|
+
function dropUnsupportedKimiCodeEnvironmentId({ name, server, isStdio, logger }) {
|
|
72328
|
+
const environmentId = server.environment_id;
|
|
72329
|
+
if (environmentId === void 0) return;
|
|
72330
|
+
if (!isStdio) {
|
|
72331
|
+
warnWithFallback(logger, `Kimi Code MCP: dropping environment_id from "${name}" because Kimi Code only accepts it on stdio servers.`);
|
|
72332
|
+
delete server.environment_id;
|
|
72333
|
+
return;
|
|
72334
|
+
}
|
|
72335
|
+
if (typeof environmentId !== "string" || environmentId.length === 0) {
|
|
72336
|
+
warnWithFallback(logger, `Kimi Code MCP: dropping environment_id ${JSON.stringify(environmentId)} from "${name}" because Kimi Code requires a non-empty string.`);
|
|
72337
|
+
delete server.environment_id;
|
|
72338
|
+
}
|
|
72339
|
+
}
|
|
71878
72340
|
function toKimiCodeServer({ name, server, logger }) {
|
|
71879
72341
|
const transport = server.transport ?? server.type;
|
|
71880
72342
|
if (transport === "ws") {
|
|
@@ -71902,6 +72364,7 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71902
72364
|
for (const field of [
|
|
71903
72365
|
"env",
|
|
71904
72366
|
"cwd",
|
|
72367
|
+
"environment_id",
|
|
71905
72368
|
"executor",
|
|
71906
72369
|
"runtime_id",
|
|
71907
72370
|
"headers",
|
|
@@ -71918,6 +72381,12 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71918
72381
|
server: converted,
|
|
71919
72382
|
logger
|
|
71920
72383
|
});
|
|
72384
|
+
dropUnsupportedKimiCodeEnvironmentId({
|
|
72385
|
+
name,
|
|
72386
|
+
server: converted,
|
|
72387
|
+
isStdio: normalizedTransport === "stdio",
|
|
72388
|
+
logger
|
|
72389
|
+
});
|
|
71921
72390
|
if (server.disabled === true) converted.enabled = false;
|
|
71922
72391
|
return converted;
|
|
71923
72392
|
}
|
|
@@ -71942,6 +72411,15 @@ function fromKimiCodeServers({ servers, logger }) {
|
|
|
71942
72411
|
logger
|
|
71943
72412
|
});
|
|
71944
72413
|
const type = transport === "stdio" || transport === "sse" || transport === "http" ? transport : void 0;
|
|
72414
|
+
dropUnsupportedKimiCodeEnvironmentId({
|
|
72415
|
+
name,
|
|
72416
|
+
server: rest,
|
|
72417
|
+
isStdio: normalizeKimiCodeTransport({
|
|
72418
|
+
transport,
|
|
72419
|
+
hasCommand: rest.command !== void 0
|
|
72420
|
+
}) === "stdio",
|
|
72421
|
+
logger
|
|
72422
|
+
});
|
|
71945
72423
|
return [name, {
|
|
71946
72424
|
...rest,
|
|
71947
72425
|
...type && { type },
|
|
@@ -73110,7 +73588,12 @@ var OpenclawMcp = class OpenclawMcp extends ToolMcp {
|
|
|
73110
73588
|
const existingMcp = isRecord(existing["mcp"]) ? existing["mcp"] : {};
|
|
73111
73589
|
const existingServers = isRecord(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
|
|
73112
73590
|
const converted = convertToOpenclawFormat({
|
|
73113
|
-
mcpServers:
|
|
73591
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
73592
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
73593
|
+
toolName: "OpenClaw",
|
|
73594
|
+
ignoreBlankNames: true,
|
|
73595
|
+
logger
|
|
73596
|
+
}),
|
|
73114
73597
|
logger
|
|
73115
73598
|
});
|
|
73116
73599
|
const servers = {
|
|
@@ -73738,7 +74221,11 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
73738
74221
|
const paths = this.getSettablePaths({ global });
|
|
73739
74222
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
73740
74223
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
73741
|
-
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(
|
|
74224
|
+
const converted = convertToPoolFormat(Object.fromEntries(Object.entries(skipServersWithEmptyEnabledTools({
|
|
74225
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
74226
|
+
toolName: "Pool",
|
|
74227
|
+
logger
|
|
74228
|
+
})).map(([serverName, serverConfig]) => {
|
|
73742
74229
|
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
73743
74230
|
const poolAllow = isRecord(rawServer) ? rawServer.poolAllow : void 0;
|
|
73744
74231
|
return [serverName, {
|
|
@@ -77264,7 +77751,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77264
77751
|
meta: {
|
|
77265
77752
|
supportsProject: true,
|
|
77266
77753
|
supportsGlobal: true,
|
|
77267
|
-
supportsEnabledTools:
|
|
77754
|
+
supportsEnabledTools: true,
|
|
77268
77755
|
supportsDisabledTools: true
|
|
77269
77756
|
}
|
|
77270
77757
|
}],
|
|
@@ -77273,7 +77760,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77273
77760
|
meta: {
|
|
77274
77761
|
supportsProject: true,
|
|
77275
77762
|
supportsGlobal: true,
|
|
77276
|
-
supportsEnabledTools:
|
|
77763
|
+
supportsEnabledTools: true,
|
|
77277
77764
|
supportsDisabledTools: true
|
|
77278
77765
|
}
|
|
77279
77766
|
}],
|
|
@@ -77282,7 +77769,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77282
77769
|
meta: {
|
|
77283
77770
|
supportsProject: true,
|
|
77284
77771
|
supportsGlobal: false,
|
|
77285
|
-
supportsEnabledTools:
|
|
77772
|
+
supportsEnabledTools: true,
|
|
77286
77773
|
supportsDisabledTools: true
|
|
77287
77774
|
}
|
|
77288
77775
|
}],
|
|
@@ -77849,6 +78336,9 @@ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger })
|
|
|
77849
78336
|
var McpProcessor = class extends FeatureProcessor {
|
|
77850
78337
|
toolTarget;
|
|
77851
78338
|
global;
|
|
78339
|
+
sweepsThroughInRootLinks() {
|
|
78340
|
+
return this.global;
|
|
78341
|
+
}
|
|
77852
78342
|
getFactory;
|
|
77853
78343
|
featureOptions;
|
|
77854
78344
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory, dryRun = false, logger, featureOptions }) {
|
|
@@ -79958,12 +80448,30 @@ function toPathTarget(pattern) {
|
|
|
79958
80448
|
"**",
|
|
79959
80449
|
"**/*"
|
|
79960
80450
|
].includes(path)) return { target: "." };
|
|
79961
|
-
const
|
|
80451
|
+
const stripped = path.replace(DIRECTORY_CONTENTS_SUFFIX, "");
|
|
80452
|
+
const directory = stripped === "" && path.startsWith("/") ? "/" : /^[A-Za-z]:$/.test(stripped) && stripped !== path ? `${stripped}/` : stripped;
|
|
79962
80453
|
if (directory !== path && directory.length > 0 && !hasGlob(directory)) return { target: directory };
|
|
79963
80454
|
if (!hasGlob(path)) return { target: path };
|
|
79964
80455
|
return { skipReason: "Antigravity file targets are paths, not globs. Only a plain path, `dir/**` or `*` can be expressed" };
|
|
79965
80456
|
}
|
|
79966
80457
|
/**
|
|
80458
|
+
* The canonical patterns a `read_file` / `write_file` target imports as. A
|
|
80459
|
+
* target can name a file or a directory, and a directory covers everything
|
|
80460
|
+
* inside it, but the target alone does not say which. A plain path such as
|
|
80461
|
+
* `src` imports as both `src` and `src/**`, so re-exporting it to a tool whose
|
|
80462
|
+
* plain path matches only that one path keeps covering the subtree; regenerating
|
|
80463
|
+
* collapses both back into one target. The bare `*`, and a target that already
|
|
80464
|
+
* holds glob characters (verbatim output of an older Rulesync), stay as written.
|
|
80465
|
+
*/
|
|
80466
|
+
function fromPathTarget(target) {
|
|
80467
|
+
if (target === "*" || hasGlob(target) || /^[A-Za-z]:$/.test(target)) return [target];
|
|
80468
|
+
if (target === ".") return [".", "./**"];
|
|
80469
|
+
const root = /^((?:[A-Za-z]:)?\/)\/*$/.exec(target)?.[1];
|
|
80470
|
+
if (root !== void 0) return [root, `${root}**`];
|
|
80471
|
+
const path = target.replace(/\/+$/, "");
|
|
80472
|
+
return [path, `${path}/**`];
|
|
80473
|
+
}
|
|
80474
|
+
/**
|
|
79967
80475
|
* A URL or domain pattern becomes its hostname. A leading `*.` is dropped,
|
|
79968
80476
|
* since a domain already covers its subdomains, and the URL path is ignored.
|
|
79969
80477
|
* Claude Code's `domain:` form is read as well.
|
|
@@ -80180,19 +80688,21 @@ function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory,
|
|
|
80180
80688
|
for (const entry of entries) {
|
|
80181
80689
|
const { toolName, pattern: target } = parseToolPatternEntry(entry);
|
|
80182
80690
|
const canonical = toCategory(toolName);
|
|
80183
|
-
const
|
|
80691
|
+
const patterns = ANTIGRAVITY_COMMAND_ACTIONS.has(toolName) ? [importAntigravityCommandTarget({
|
|
80184
80692
|
target,
|
|
80185
80693
|
entry,
|
|
80186
80694
|
decision: action,
|
|
80187
80695
|
logger,
|
|
80188
80696
|
toolLabel
|
|
80189
|
-
}) : PATH_ACTIONS.has(toolName) ? collapseLeadingHome(target, homeDirectory) : target;
|
|
80190
|
-
|
|
80191
|
-
|
|
80192
|
-
|
|
80193
|
-
|
|
80194
|
-
|
|
80195
|
-
|
|
80697
|
+
})] : PATH_ACTIONS.has(toolName) ? fromPathTarget(collapseLeadingHome(target, homeDirectory)) : [target];
|
|
80698
|
+
for (const pattern of patterns) {
|
|
80699
|
+
if (isPrototypePollutionKey(canonical) || isPrototypePollutionKey(pattern)) continue;
|
|
80700
|
+
getOrCreateOwn({
|
|
80701
|
+
record: permission,
|
|
80702
|
+
key: canonical,
|
|
80703
|
+
create: () => ({})
|
|
80704
|
+
})[pattern] = action;
|
|
80705
|
+
}
|
|
80196
80706
|
}
|
|
80197
80707
|
};
|
|
80198
80708
|
processEntries(allow, "allow");
|
|
@@ -80963,6 +81473,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
80963
81473
|
* in global mode it is dropped with a warning instead of written somewhere it
|
|
80964
81474
|
* does nothing.
|
|
80965
81475
|
*
|
|
81476
|
+
* Only Auggie 0.x reads `recommendedMarketplaces`: the 1.x plugin settings
|
|
81477
|
+
* reference lists `enabledPlugins` alone. The key is still written because the
|
|
81478
|
+
* stable CLI is 0.x, and the write warning says it is 0.x-only.
|
|
81479
|
+
*
|
|
80966
81480
|
* Both make Auggie load code the repository did not ship — a recommended
|
|
80967
81481
|
* marketplace is one prompt away from being cloned, and an enabled plugin can
|
|
80968
81482
|
* carry its own hooks and MCP servers — so writing either is logged, unless
|
|
@@ -80982,7 +81496,7 @@ function buildPluginConsumptionPatch({ override, existing, global, logger }) {
|
|
|
80982
81496
|
else {
|
|
80983
81497
|
patch.recommendedMarketplaces = recommendedMarketplaces;
|
|
80984
81498
|
const existingMarketplaces = existingKeys.recommendedMarketplaces;
|
|
80985
|
-
if (recommendedMarketplaces.length > 0 && !(0, node_util.isDeepStrictEqual)(recommendedMarketplaces, existingMarketplaces)) logger?.warn(`Writing AugmentCode \`recommendedMarketplaces\` (${recommendedMarketplaces.map(quoteValueForWarning).join(", ")}): Auggie prompts everyone who opens this workspace to install these plugin marketplaces.`);
|
|
81499
|
+
if (recommendedMarketplaces.length > 0 && !(0, node_util.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).`);
|
|
80986
81500
|
warnAboutRemovedPluginIds({
|
|
80987
81501
|
key: "recommendedMarketplaces",
|
|
80988
81502
|
removed: (existingMarketplaces ?? []).filter((marketplace) => !recommendedMarketplaces.includes(marketplace)),
|
|
@@ -81211,12 +81725,12 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
81211
81725
|
const regex = entry.shellInputRegex;
|
|
81212
81726
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
81213
81727
|
else {
|
|
81214
|
-
if (action
|
|
81215
|
-
logger?.warn(`AugmentCode permissions: skipping
|
|
81728
|
+
if (action === "allow" && !canApproximateShellRegexWithoutWidening(regex)) {
|
|
81729
|
+
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.`);
|
|
81216
81730
|
continue;
|
|
81217
81731
|
}
|
|
81218
|
-
if (action
|
|
81219
|
-
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
|
|
81732
|
+
if (action !== "allow") {
|
|
81733
|
+
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.`);
|
|
81220
81734
|
pattern = "*";
|
|
81221
81735
|
} else {
|
|
81222
81736
|
pattern = shellRegexToGlob(regex);
|
|
@@ -81302,6 +81816,62 @@ function classifyPattern(pattern, bareStarIsWildcard) {
|
|
|
81302
81816
|
};
|
|
81303
81817
|
return { kind: "literal" };
|
|
81304
81818
|
}
|
|
81819
|
+
/** The trimmed spellings of the literal (prefix) deny entries, minus the bare wildcard. */
|
|
81820
|
+
function collectLiteralDenyStems({ rules, bareStarIsWildcard }) {
|
|
81821
|
+
const stems = /* @__PURE__ */ new Set();
|
|
81822
|
+
for (const [pattern, action] of Object.entries(rules)) {
|
|
81823
|
+
if (action !== "deny" || pattern === "*") continue;
|
|
81824
|
+
const stem = pattern.trimEnd();
|
|
81825
|
+
if (stem.length > 0 && classifyPattern(pattern, bareStarIsWildcard).kind === "literal") stems.add(stem);
|
|
81826
|
+
}
|
|
81827
|
+
return stems;
|
|
81828
|
+
}
|
|
81829
|
+
function isCoveredByLiteralDeny({ pattern, literalDenyStems }) {
|
|
81830
|
+
if (!pattern.endsWith(" *")) return false;
|
|
81831
|
+
const stem = pattern.slice(0, -2).trimEnd();
|
|
81832
|
+
return stem.length > 0 && literalDenyStems.has(stem);
|
|
81833
|
+
}
|
|
81834
|
+
/**
|
|
81835
|
+
* Lower a VS Code style command allow/deny list pair into canonical `bash`
|
|
81836
|
+
* rules on import.
|
|
81837
|
+
*
|
|
81838
|
+
* The native entries are compared as literal command prefixes, while canonical
|
|
81839
|
+
* patterns are globs, so a verbatim copy changes their meaning:
|
|
81840
|
+
*
|
|
81841
|
+
* - A prefix deny such as `curl` blocks `curl https://…` natively but would
|
|
81842
|
+
* only block the exact command `curl` canonically, so a broad allow beside
|
|
81843
|
+
* it would auto-approve what the native config blocked. Each deny that does
|
|
81844
|
+
* not already end in `*` is imported as itself plus `<deny> *`.
|
|
81845
|
+
* - An allow holding glob syntax (`npm *`) is literal prefix text natively and
|
|
81846
|
+
* approves almost nothing, but canonically it is a wildcard. Such an allow is
|
|
81847
|
+
* not imported (with a warning) rather than imported wider than it matched.
|
|
81848
|
+
* The Roo Code lineage's bare `*` is a real wildcard natively and is kept.
|
|
81849
|
+
*
|
|
81850
|
+
* A pattern in both lists imports as `deny`, matching both tools' precedence
|
|
81851
|
+
* for identical entries.
|
|
81852
|
+
*/
|
|
81853
|
+
function importVscodeCommandLists({ allowed, denied, toolLabel, logger, prefixSemantics = "roo-lineage" }) {
|
|
81854
|
+
const isRooLineage = prefixSemantics === "roo-lineage";
|
|
81855
|
+
const rules = {};
|
|
81856
|
+
const skippedAllows = [];
|
|
81857
|
+
for (const pattern of allowed) {
|
|
81858
|
+
if (!(isRooLineage && pattern === "*") && /[*?]/.test(pattern)) {
|
|
81859
|
+
skippedAllows.push(pattern);
|
|
81860
|
+
continue;
|
|
81861
|
+
}
|
|
81862
|
+
rules[pattern] = "allow";
|
|
81863
|
+
}
|
|
81864
|
+
for (const pattern of denied) {
|
|
81865
|
+
rules[pattern] = "deny";
|
|
81866
|
+
const stem = pattern.trimEnd();
|
|
81867
|
+
if (!pattern.endsWith("*") && stem.length > 0) rules[`${stem} *`] = "deny";
|
|
81868
|
+
}
|
|
81869
|
+
if (skippedAllows.length > 0) {
|
|
81870
|
+
const count = skippedAllows.length;
|
|
81871
|
+
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.` : ""));
|
|
81872
|
+
}
|
|
81873
|
+
return rules;
|
|
81874
|
+
}
|
|
81305
81875
|
function pluralize(count, singular, plural) {
|
|
81306
81876
|
return count === 1 ? singular : plural;
|
|
81307
81877
|
}
|
|
@@ -81396,8 +81966,16 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
|
|
|
81396
81966
|
const matcherAllows = [];
|
|
81397
81967
|
const widenedDenies = [];
|
|
81398
81968
|
const inertDenies = [];
|
|
81969
|
+
const literalDenyStems = collectLiteralDenyStems({
|
|
81970
|
+
rules,
|
|
81971
|
+
bareStarIsWildcard: isRooLineage
|
|
81972
|
+
});
|
|
81399
81973
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
81400
81974
|
if (action !== "allow" && action !== "deny") continue;
|
|
81975
|
+
if (action === "deny" && isCoveredByLiteralDeny({
|
|
81976
|
+
pattern,
|
|
81977
|
+
literalDenyStems
|
|
81978
|
+
})) continue;
|
|
81401
81979
|
const shape = classifyPattern(pattern, isRooLineage);
|
|
81402
81980
|
if (action === "allow") {
|
|
81403
81981
|
allowed.push(pattern);
|
|
@@ -81760,16 +82338,20 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
|
|
|
81760
82338
|
validate: true
|
|
81761
82339
|
});
|
|
81762
82340
|
}
|
|
81763
|
-
toRulesyncPermissions() {
|
|
82341
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
81764
82342
|
const settings = parseBobSettings({
|
|
81765
82343
|
fileContent: this.getFileContent(),
|
|
81766
82344
|
filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
81767
82345
|
});
|
|
81768
82346
|
const approval = settings[APPROVAL_KEY$1];
|
|
81769
82347
|
const entry = (isPlainObject(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
|
|
81770
|
-
const rules = {
|
|
81771
|
-
|
|
81772
|
-
|
|
82348
|
+
const rules = importVscodeCommandLists({
|
|
82349
|
+
allowed: toStringArray$1(entry?.approvedCommands),
|
|
82350
|
+
denied: toStringArray$1(entry?.deniedCommands),
|
|
82351
|
+
toolLabel: TOOL_LABEL,
|
|
82352
|
+
logger,
|
|
82353
|
+
prefixSemantics: "documented-prefix"
|
|
82354
|
+
});
|
|
81773
82355
|
const permission = {};
|
|
81774
82356
|
if (Object.keys(rules).length > 0) permission[SHELL_PERMISSION_CATEGORY] = rules;
|
|
81775
82357
|
const bob = readBobOverride(settings);
|
|
@@ -85533,7 +86115,7 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
|
|
|
85533
86115
|
const patch = {};
|
|
85534
86116
|
const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
|
|
85535
86117
|
for (const [key, value] of Object.entries(override ?? {})) {
|
|
85536
|
-
if (key === "base_permission_profile" || key === "git_write_rules") continue;
|
|
86118
|
+
if (key === "base_permission_profile" || key === "git_write_rules" || key === "bash_canonical_matching") continue;
|
|
85537
86119
|
if (!allowed.has(key)) {
|
|
85538
86120
|
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.`);
|
|
85539
86121
|
continue;
|
|
@@ -85822,6 +86404,7 @@ function stripTrailingSlash(pattern) {
|
|
|
85822
86404
|
function buildCodexBashRulesContent({ config, logger }) {
|
|
85823
86405
|
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
85824
86406
|
const entries = Object.entries(bashRules);
|
|
86407
|
+
const canonicalMatching = config.codexcli?.bash_canonical_matching === true;
|
|
85825
86408
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
85826
86409
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
85827
86410
|
const ruleBlocks = entries.map(([pattern, action]) => {
|
|
@@ -85840,6 +86423,7 @@ function buildCodexBashRulesContent({ config, logger }) {
|
|
|
85840
86423
|
` pattern = [${serializedTokens}],`,
|
|
85841
86424
|
` decision = ${JSON.stringify(decision)},`,
|
|
85842
86425
|
` justification = ${JSON.stringify(`Generated from Rulesync permission.bash: ${pattern}`)},`,
|
|
86426
|
+
...canonicalMatching && decision !== "allow" ? [" canonical_matching = True,"] : [],
|
|
85843
86427
|
")"
|
|
85844
86428
|
].join("\n");
|
|
85845
86429
|
}).filter((block) => block !== null);
|
|
@@ -87424,9 +88008,126 @@ function toCanonicalToolName$5(crushName) {
|
|
|
87424
88008
|
function isMcpToolName(crushName) {
|
|
87425
88009
|
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
87426
88010
|
}
|
|
88011
|
+
const CRUSH_COMMAND_WRAPPERS = /* @__PURE__ */ new Set([
|
|
88012
|
+
"env",
|
|
88013
|
+
"nohup",
|
|
88014
|
+
"nice",
|
|
88015
|
+
"timeout"
|
|
88016
|
+
]);
|
|
88017
|
+
const CRUSH_COMMAND_PREFIXES = [...CRUSH_COMMAND_WRAPPERS, "time"];
|
|
88018
|
+
const CRUSH_COMMAND_NAME = /^[a-z0-9_][a-z0-9_.+-]*$/;
|
|
88019
|
+
const CRUSH_COMMAND_ARGUMENT = /^[A-Za-z0-9_@%+=:,./-]+$/;
|
|
88020
|
+
const CRUSH_COMMAND_NAME_SUFFIXES = [
|
|
88021
|
+
".exe",
|
|
88022
|
+
".bat",
|
|
88023
|
+
".cmd"
|
|
88024
|
+
];
|
|
88025
|
+
function isCrushCommandName(name) {
|
|
88026
|
+
return CRUSH_COMMAND_NAME.test(name) && !CRUSH_COMMAND_NAME_SUFFIXES.some((suffix) => name.endsWith(suffix)) && !isPrototypePollutionKey(name);
|
|
88027
|
+
}
|
|
88028
|
+
/**
|
|
88029
|
+
* Reduce a command name the way Crush matches it (`shell.NormalizeCommandName`):
|
|
88030
|
+
* trimmed, without its directory, lower-cased, and without a Windows
|
|
88031
|
+
* executable suffix.
|
|
88032
|
+
*/
|
|
88033
|
+
function normalizeCrushCommandName(raw) {
|
|
88034
|
+
const base = (raw.trim().split(/[\\/]/).at(-1) ?? "").toLowerCase();
|
|
88035
|
+
const suffix = CRUSH_COMMAND_NAME_SUFFIXES.find((candidate) => base.length > candidate.length && base.endsWith(candidate));
|
|
88036
|
+
return suffix === void 0 ? base : base.slice(0, -suffix.length);
|
|
88037
|
+
}
|
|
88038
|
+
/**
|
|
88039
|
+
* Whether a canonical pattern is a literal command line that a Crush
|
|
88040
|
+
* `safe_commands` entry reproduces exactly: Crush matches an entry only when
|
|
88041
|
+
* the command is written with exactly those tokens, no extra arguments and no
|
|
88042
|
+
* flags, which is what a wildcard-free glob means. A leading wrapper is
|
|
88043
|
+
* refused, since Crush would resolve it away and approve the bare command.
|
|
88044
|
+
*/
|
|
88045
|
+
function isExactCrushCommandLine(pattern) {
|
|
88046
|
+
const [name, ...args] = pattern.split(" ");
|
|
88047
|
+
return name !== void 0 && isCrushCommandName(name) && !CRUSH_COMMAND_WRAPPERS.has(name) && args.every((arg) => CRUSH_COMMAND_ARGUMENT.test(arg));
|
|
88048
|
+
}
|
|
88049
|
+
/**
|
|
88050
|
+
* The command name a canonical `<name>` / `<name> *` pattern names as a
|
|
88051
|
+
* whole, i.e. the `blocked_commands` entry for it, or undefined.
|
|
88052
|
+
*/
|
|
88053
|
+
function wholeCommandName(pattern) {
|
|
88054
|
+
const name = pattern.endsWith(" *") ? pattern.slice(0, -2) : pattern;
|
|
88055
|
+
return isCrushCommandName(name) ? name : void 0;
|
|
88056
|
+
}
|
|
88057
|
+
/**
|
|
88058
|
+
* The line a Crush `safe_commands` entry effectively stands for, in canonical
|
|
88059
|
+
* form (whitespace collapsed, command name normalized), or undefined when the
|
|
88060
|
+
* entry has no exact canonical spelling (wildcards, quoting, a wrapper, ...).
|
|
88061
|
+
*/
|
|
88062
|
+
function effectiveSafeCommandLine(entry) {
|
|
88063
|
+
const [name, ...args] = entry.trim().split(/\s+/);
|
|
88064
|
+
if (name === void 0 || name === "") return void 0;
|
|
88065
|
+
const line = [normalizeCrushCommandName(name), ...args].join(" ");
|
|
88066
|
+
return isExactCrushCommandLine(line) ? line : void 0;
|
|
88067
|
+
}
|
|
88068
|
+
/**
|
|
88069
|
+
* A restriction pattern with its command word reduced the way Crush reduces a
|
|
88070
|
+
* wrapper's name before peeling it (`shell.NormalizeCommandName`): any
|
|
88071
|
+
* directory dropped (`./nice`, `bin\env.exe`, `/usr/bin/env`), lower-cased,
|
|
88072
|
+
* and without a Windows executable suffix. A `*` in the command word may
|
|
88073
|
+
* stand for a directory and part of a name, so when what follows it is the
|
|
88074
|
+
* tail of a wrapper name, everything before it collapses into `*`
|
|
88075
|
+
* (`b*nice` -> `*nice`, `/u*v` -> `*v`; `rm*` is left alone). The rest of the pattern is lower-cased as well, for a
|
|
88076
|
+
* nested wrapper spelled in another case. Used only to compare against the
|
|
88077
|
+
* wrapper spellings of a safe line, where it can only withhold more.
|
|
88078
|
+
*/
|
|
88079
|
+
function normalizeRestrictionCommandWord(pattern) {
|
|
88080
|
+
const space = pattern.indexOf(" ");
|
|
88081
|
+
let head = space < 0 ? pattern : pattern.slice(0, space);
|
|
88082
|
+
const rest = space < 0 ? "" : pattern.slice(space);
|
|
88083
|
+
head = normalizeCrushCommandName(head);
|
|
88084
|
+
const star = head.lastIndexOf("*");
|
|
88085
|
+
const tail = head.slice(star + 1);
|
|
88086
|
+
if (star > 0 && tail !== "" && CRUSH_COMMAND_PREFIXES.some((prefix) => prefix.endsWith(tail))) head = `*${tail}`;
|
|
88087
|
+
return `${head}${rest.toLowerCase()}`;
|
|
88088
|
+
}
|
|
88089
|
+
/**
|
|
88090
|
+
* Build the test for a `safe_commands` line: which canonical `deny`/`ask`
|
|
88091
|
+
* rules govern a command Crush would approve through it? Besides the line
|
|
88092
|
+
* itself, Crush approves it followed by `--` and behind a wrapper (or the
|
|
88093
|
+
* `time` keyword), whose name it normalizes (`./nice`, `/usr/bin/env`, `NICE`,
|
|
88094
|
+
* `nohup.exe`). The line and its wrapper spellings are compared against the
|
|
88095
|
+
* restrictions as written, and the wrapper spellings also against the
|
|
88096
|
+
* restrictions with their command word normalized the same way — which keeps
|
|
88097
|
+
* an unrelated `rm *` from withholding the line, as a leading wildcard on the
|
|
88098
|
+
* spelling would. Canonically the stricter rule wins whatever its width
|
|
88099
|
+
* (`deny > ask > allow`), so any overlap withholds the line.
|
|
88100
|
+
*
|
|
88101
|
+
* Crush also approves a line composed with other safe lines (`&&`, `||`, `|`,
|
|
88102
|
+
* `;`, a parenthesized group); those compositions are not compared, the way
|
|
88103
|
+
* other tools' per-subcommand matching treats an allowed command.
|
|
88104
|
+
*/
|
|
88105
|
+
function createSafeCommandShadowTest(permission) {
|
|
88106
|
+
const restrictions = collectShellCommandRules(permission).rules.filter(({ action }) => action !== "allow");
|
|
88107
|
+
const test = createShadowingRestrictionsTest(restrictions);
|
|
88108
|
+
const normalized = restrictions.map((rule) => ({
|
|
88109
|
+
...rule,
|
|
88110
|
+
pattern: normalizeRestrictionCommandWord(rule.pattern)
|
|
88111
|
+
}));
|
|
88112
|
+
const testNormalized = createShadowingRestrictionsTest(normalized);
|
|
88113
|
+
const originalOf = /* @__PURE__ */ new Map();
|
|
88114
|
+
normalized.forEach((rule, index) => {
|
|
88115
|
+
const original = restrictions[index]?.pattern;
|
|
88116
|
+
if (original !== void 0 && !originalOf.has(rule.pattern)) originalOf.set(rule.pattern, original);
|
|
88117
|
+
});
|
|
88118
|
+
const withSeparator = (spellings) => spellings.flatMap((spelling) => [spelling, `${spelling} --*`]);
|
|
88119
|
+
const wrapped = (base) => CRUSH_COMMAND_PREFIXES.map((prefix) => `${prefix}*${base}`);
|
|
88120
|
+
return (line) => uniq$11([...withSeparator([line, ...wrapped(line)]).flatMap((spelling) => test(spelling)), ...withSeparator(wrapped(line.toLowerCase())).flatMap((spelling) => testNormalized(spelling).map((pattern) => originalOf.get(pattern) ?? pattern))]);
|
|
88121
|
+
}
|
|
87427
88122
|
function uniq$11(values) {
|
|
87428
88123
|
return [...new Set(values)];
|
|
87429
88124
|
}
|
|
88125
|
+
function stringList(value) {
|
|
88126
|
+
return isStringArray(value) ? value : [];
|
|
88127
|
+
}
|
|
88128
|
+
function listOrUndefined(values) {
|
|
88129
|
+
return values.length > 0 ? values : void 0;
|
|
88130
|
+
}
|
|
87430
88131
|
/**
|
|
87431
88132
|
* Permissions adapter for Crush.
|
|
87432
88133
|
*
|
|
@@ -87448,8 +88149,24 @@ function uniq$11(values) {
|
|
|
87448
88149
|
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
87449
88150
|
* any other category passes through verbatim.
|
|
87450
88151
|
*
|
|
87451
|
-
*
|
|
87452
|
-
*
|
|
88152
|
+
* Crush v0.100.0+ also carries two command lists, which the `bash` category's
|
|
88153
|
+
* pattern rules map onto (see `convertBashCommandRules`):
|
|
88154
|
+
* - `permissions.safe_commands`: exact command lines that skip the prompt
|
|
88155
|
+
* (no extra arguments, no flags) <- an `allow` on a literal command line
|
|
88156
|
+
* that no canonical `deny`/`ask` overlaps.
|
|
88157
|
+
* - `permissions.blocked_commands`: command names added to the dangerous
|
|
88158
|
+
* list <- a `deny` on `<name>` or `<name> *`. Not a full deny: a direct
|
|
88159
|
+
* invocation is still prompted for (and auto-approved in yolo mode or
|
|
88160
|
+
* through `allowed_tools: ["bash"]`); only the forms Crush would not
|
|
88161
|
+
* prompt for (its read-only commands, `safe_commands`) and indirect
|
|
88162
|
+
* invocations are refused at exec time. An `ask` is not written there,
|
|
88163
|
+
* since it would turn the read-only forms into a refusal.
|
|
88164
|
+
* `permissions.allowed_commands` only drops names from the dangerous list (the
|
|
88165
|
+
* commands keep prompting), so it has no canonical counterpart and is left
|
|
88166
|
+
* untouched.
|
|
88167
|
+
*
|
|
88168
|
+
* Any other pattern-specific rule cannot be expressed and is reported and
|
|
88169
|
+
* skipped. Because Crush cannot narrow an allowed tool, a category's catch-all `allow`
|
|
87453
88170
|
* is not written while the same category carries a pattern-specific `deny` or
|
|
87454
88171
|
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
87455
88172
|
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
@@ -87513,7 +88230,12 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87513
88230
|
const existingContent = location.fileContent ?? "";
|
|
87514
88231
|
warnCrushTwinLeftovers({
|
|
87515
88232
|
location,
|
|
87516
|
-
ownedPaths: [
|
|
88233
|
+
ownedPaths: [
|
|
88234
|
+
[CRUSH_PERMISSIONS_KEY, CRUSH_ALLOWED_TOOLS_KEY],
|
|
88235
|
+
[CRUSH_PERMISSIONS_KEY, CRUSH_SAFE_COMMANDS_KEY],
|
|
88236
|
+
[CRUSH_PERMISSIONS_KEY, CRUSH_BLOCKED_COMMANDS_KEY],
|
|
88237
|
+
[CRUSH_OPTIONS_KEY, CRUSH_DISABLED_TOOLS_KEY]
|
|
88238
|
+
],
|
|
87517
88239
|
logger
|
|
87518
88240
|
});
|
|
87519
88241
|
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
@@ -87525,12 +88247,22 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87525
88247
|
logger
|
|
87526
88248
|
});
|
|
87527
88249
|
const managedToolNames = managedCrushToolNames(config);
|
|
88250
|
+
const commands = rebuildCrushCommandLists({
|
|
88251
|
+
config,
|
|
88252
|
+
existingPermissions,
|
|
88253
|
+
logger
|
|
88254
|
+
});
|
|
87528
88255
|
const preservedAllowed = (isStringArray(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
87529
88256
|
const preservedDisabled = (isStringArray(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
87530
88257
|
const allowedList = uniq$11([...preservedAllowed, ...generated.allowed]);
|
|
87531
88258
|
const disabledList = uniq$11([...preservedDisabled, ...generated.disabled]);
|
|
88259
|
+
const permissionsHasContent = allowedList.length > 0 || commands.safe.length > 0 || commands.blocked.length > 0;
|
|
87532
88260
|
const patch = {};
|
|
87533
|
-
if (
|
|
88261
|
+
if (permissionsHasContent || existingPermissions !== void 0) patch[CRUSH_PERMISSIONS_KEY] = {
|
|
88262
|
+
[CRUSH_ALLOWED_TOOLS_KEY]: listOrUndefined(allowedList),
|
|
88263
|
+
[CRUSH_SAFE_COMMANDS_KEY]: listOrUndefined(commands.safe),
|
|
88264
|
+
[CRUSH_BLOCKED_COMMANDS_KEY]: listOrUndefined(commands.blocked)
|
|
88265
|
+
};
|
|
87534
88266
|
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
87535
88267
|
return new CrushPermissions({
|
|
87536
88268
|
outputRoot,
|
|
@@ -87548,17 +88280,79 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87548
88280
|
global
|
|
87549
88281
|
});
|
|
87550
88282
|
}
|
|
87551
|
-
toRulesyncPermissions() {
|
|
88283
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
87552
88284
|
const permissions = isRecord(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
87553
88285
|
const options = toRecord(this.json[CRUSH_OPTIONS_KEY]);
|
|
87554
88286
|
const rulesyncConfig = convertCrushListsToRulesync({
|
|
87555
|
-
allowed:
|
|
87556
|
-
disabled:
|
|
88287
|
+
allowed: stringList(permissions[CRUSH_ALLOWED_TOOLS_KEY]),
|
|
88288
|
+
disabled: stringList(options[CRUSH_DISABLED_TOOLS_KEY]),
|
|
88289
|
+
safe: stringList(permissions[CRUSH_SAFE_COMMANDS_KEY]),
|
|
88290
|
+
blocked: stringList(permissions[CRUSH_BLOCKED_COMMANDS_KEY]),
|
|
88291
|
+
logger
|
|
87557
88292
|
});
|
|
87558
88293
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
87559
88294
|
}
|
|
87560
88295
|
};
|
|
87561
88296
|
/**
|
|
88297
|
+
* The `safe_commands` / `blocked_commands` lists a generate writes: the
|
|
88298
|
+
* existing entries rulesync keeps, followed by the ones derived from the
|
|
88299
|
+
* canonical `bash` rules. Existing `blocked_commands` entries are all kept:
|
|
88300
|
+
* rulesync cannot tell its own from hand-written ones, and dropping one only
|
|
88301
|
+
* ever loosens. A derived blocked name that is the command of a written safe
|
|
88302
|
+
* line is withheld, since Crush would refuse that line at exec time.
|
|
88303
|
+
*/
|
|
88304
|
+
function rebuildCrushCommandLists({ config, existingPermissions, logger }) {
|
|
88305
|
+
const bashRules = honorAllToolsOnBash(config.permission)["bash"] ?? {};
|
|
88306
|
+
const shadowTest = createSafeCommandShadowTest(config.permission);
|
|
88307
|
+
const generated = convertBashCommandRules({
|
|
88308
|
+
bashRules,
|
|
88309
|
+
shadowTest,
|
|
88310
|
+
logger
|
|
88311
|
+
});
|
|
88312
|
+
const safe = uniq$11([...preserveSafeCommands({
|
|
88313
|
+
existing: stringList(existingPermissions?.[CRUSH_SAFE_COMMANDS_KEY]),
|
|
88314
|
+
bashRules,
|
|
88315
|
+
shadowTest,
|
|
88316
|
+
logger
|
|
88317
|
+
}), ...generated.safe]);
|
|
88318
|
+
const safeCommandNames = new Set(safe.flatMap((entry) => {
|
|
88319
|
+
const name = entry.trim().split(/\s+/)[0];
|
|
88320
|
+
return name === void 0 || name === "" ? [] : [normalizeCrushCommandName(name)];
|
|
88321
|
+
}));
|
|
88322
|
+
const blocked = generated.blocked.filter((name) => {
|
|
88323
|
+
if (!safeCommandNames.has(name)) return true;
|
|
88324
|
+
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.`);
|
|
88325
|
+
return false;
|
|
88326
|
+
});
|
|
88327
|
+
const existingBlocked = stringList(existingPermissions?.[CRUSH_BLOCKED_COMMANDS_KEY]);
|
|
88328
|
+
for (const entry of existingBlocked) {
|
|
88329
|
+
if (!safeCommandNames.has(normalizeCrushCommandName(entry))) continue;
|
|
88330
|
+
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.`);
|
|
88331
|
+
}
|
|
88332
|
+
return {
|
|
88333
|
+
safe,
|
|
88334
|
+
blocked: uniq$11([...existingBlocked, ...blocked])
|
|
88335
|
+
};
|
|
88336
|
+
}
|
|
88337
|
+
/**
|
|
88338
|
+
* The existing `safe_commands` entries a generate keeps: those the canonical
|
|
88339
|
+
* `bash` rules do not spell out themselves (an entry they do spell is rebuilt
|
|
88340
|
+
* from them), minus any entry a canonical `deny`/`ask` governs, which would
|
|
88341
|
+
* otherwise auto-approve what the config restricts. An entry whose exact
|
|
88342
|
+
* meaning cannot be read is dropped whenever the config restricts anything.
|
|
88343
|
+
*/
|
|
88344
|
+
function preserveSafeCommands({ existing, bashRules, shadowTest, logger }) {
|
|
88345
|
+
const managedLines = new Set(Object.keys(bashRules));
|
|
88346
|
+
const restricts = shadowTest("*").length > 0;
|
|
88347
|
+
return existing.filter((entry) => {
|
|
88348
|
+
if (managedLines.has(entry)) return false;
|
|
88349
|
+
const line = effectiveSafeCommandLine(entry);
|
|
88350
|
+
const shadowed = line === void 0 ? restricts : shadowTest(line).length > 0;
|
|
88351
|
+
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.`);
|
|
88352
|
+
return !shadowed;
|
|
88353
|
+
});
|
|
88354
|
+
}
|
|
88355
|
+
/**
|
|
87562
88356
|
* The Crush tool names the canonical config manages, i.e. the names its
|
|
87563
88357
|
* categories map to. Entries naming any other tool are the user's and survive
|
|
87564
88358
|
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
@@ -87583,7 +88377,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
87583
88377
|
catchAllAction = action;
|
|
87584
88378
|
continue;
|
|
87585
88379
|
}
|
|
87586
|
-
logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
88380
|
+
if (category !== "bash") logger?.warn(`Crush permissions are tool-wide (no argument patterns), so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
87587
88381
|
if (action !== "allow") restricted = true;
|
|
87588
88382
|
}
|
|
87589
88383
|
if (catchAllAction === "allow" && restricted) {
|
|
@@ -87634,11 +88428,71 @@ function convertRulesyncToCrushLists({ config, logger }) {
|
|
|
87634
88428
|
};
|
|
87635
88429
|
}
|
|
87636
88430
|
/**
|
|
88431
|
+
* Map the `bash` category's pattern rules onto Crush's command lists:
|
|
88432
|
+
* - `allow` on a literal command line -> `permissions.safe_commands`, which
|
|
88433
|
+
* Crush matches exactly (no extra arguments, no flags). It is withheld
|
|
88434
|
+
* when a canonical `deny`/`ask` governs any command Crush would approve
|
|
88435
|
+
* through it.
|
|
88436
|
+
* - `deny` on `<name>` / `<name> *` -> `permissions.blocked_commands`, the
|
|
88437
|
+
* strictest Crush can say about a single command: a direct invocation is
|
|
88438
|
+
* still prompted for, while the forms Crush would not prompt for and
|
|
88439
|
+
* indirect invocations are refused at exec time. Reported, since it is
|
|
88440
|
+
* not a full deny.
|
|
88441
|
+
* - Anything else is reported and skipped. A pattern `ask` is never written
|
|
88442
|
+
* to `blocked_commands`, which would refuse the command's read-only forms
|
|
88443
|
+
* outright, and an `allow` with a wildcard has no list that auto-approves
|
|
88444
|
+
* it.
|
|
88445
|
+
*/
|
|
88446
|
+
function convertBashCommandRules({ bashRules, shadowTest, logger }) {
|
|
88447
|
+
const lists = {
|
|
88448
|
+
safe: [],
|
|
88449
|
+
blocked: []
|
|
88450
|
+
};
|
|
88451
|
+
for (const [rawPattern, action] of Object.entries(bashRules)) {
|
|
88452
|
+
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
88453
|
+
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
88454
|
+
if (pattern === "*") continue;
|
|
88455
|
+
const quoted = quoteValueForWarning(pattern);
|
|
88456
|
+
if (action === "allow") {
|
|
88457
|
+
if (!isExactCrushCommandLine(pattern)) {
|
|
88458
|
+
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.`);
|
|
88459
|
+
continue;
|
|
88460
|
+
}
|
|
88461
|
+
const shadows = shadowTest(pattern);
|
|
88462
|
+
if (shadows.length > 0) {
|
|
88463
|
+
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.`);
|
|
88464
|
+
continue;
|
|
88465
|
+
}
|
|
88466
|
+
lists.safe.push(pattern);
|
|
88467
|
+
continue;
|
|
88468
|
+
}
|
|
88469
|
+
if (action === "ask") {
|
|
88470
|
+
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.`);
|
|
88471
|
+
continue;
|
|
88472
|
+
}
|
|
88473
|
+
const name = wholeCommandName(pattern);
|
|
88474
|
+
if (name !== void 0 && CRUSH_COMMAND_PREFIXES.includes(name)) {
|
|
88475
|
+
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.`);
|
|
88476
|
+
continue;
|
|
88477
|
+
}
|
|
88478
|
+
if (name === void 0) {
|
|
88479
|
+
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.`);
|
|
88480
|
+
continue;
|
|
88481
|
+
}
|
|
88482
|
+
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).`);
|
|
88483
|
+
lists.blocked.push(name);
|
|
88484
|
+
}
|
|
88485
|
+
return {
|
|
88486
|
+
safe: uniq$11(lists.safe),
|
|
88487
|
+
blocked: uniq$11(lists.blocked)
|
|
88488
|
+
};
|
|
88489
|
+
}
|
|
88490
|
+
/**
|
|
87637
88491
|
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
87638
88492
|
* entry scopes an allow to one action of a tool, which the canonical model
|
|
87639
88493
|
* cannot express, so it is left out rather than widened to the whole tool.
|
|
87640
88494
|
*/
|
|
87641
|
-
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
88495
|
+
function convertCrushListsToRulesync({ allowed, disabled, safe, blocked, logger }) {
|
|
87642
88496
|
const permission = {};
|
|
87643
88497
|
const bucketFor = (category) => {
|
|
87644
88498
|
const own = lookupOwn({
|
|
@@ -87654,6 +88508,29 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
87654
88508
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
87655
88509
|
bucketFor(toCanonicalToolName$5(entry))["*"] = "deny";
|
|
87656
88510
|
}
|
|
88511
|
+
const setBashRule = (pattern, action) => {
|
|
88512
|
+
const bucket = bucketFor(SHELL_PERMISSION_CATEGORY);
|
|
88513
|
+
if (!Object.hasOwn(bucket, pattern)) bucket[pattern] = action;
|
|
88514
|
+
};
|
|
88515
|
+
for (const entry of blocked) {
|
|
88516
|
+
const name = normalizeCrushCommandName(entry);
|
|
88517
|
+
if (name === "") continue;
|
|
88518
|
+
if (!isCrushCommandName(name)) {
|
|
88519
|
+
logger?.warn(`Crush permissions.blocked_commands entry ${quoteValueForWarning(entry)} is not a plain command name, so it was not imported.`);
|
|
88520
|
+
continue;
|
|
88521
|
+
}
|
|
88522
|
+
setBashRule(name, "deny");
|
|
88523
|
+
setBashRule(`${name} *`, "deny");
|
|
88524
|
+
}
|
|
88525
|
+
for (const entry of safe) {
|
|
88526
|
+
if (entry.trim() === "") continue;
|
|
88527
|
+
const line = effectiveSafeCommandLine(entry);
|
|
88528
|
+
if (line === void 0) {
|
|
88529
|
+
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.`);
|
|
88530
|
+
continue;
|
|
88531
|
+
}
|
|
88532
|
+
setBashRule(line, "allow");
|
|
88533
|
+
}
|
|
87657
88534
|
for (const entry of allowed) {
|
|
87658
88535
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
87659
88536
|
bucketFor(toCanonicalToolName$5(entry))["*"] ??= "allow";
|
|
@@ -89033,8 +89910,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
|
89033
89910
|
* In global mode the config file is shared with the hooks (`hooks`) feature
|
|
89034
89911
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
89035
89912
|
* merge into the existing JSON and the file is never deleted; only the keys
|
|
89036
|
-
* this feature manages are rewritten — `permissions`, plus `sandbox`
|
|
89037
|
-
* mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
|
|
89913
|
+
* this feature manages are rewritten — `permissions`, plus `sandbox` and
|
|
89914
|
+
* `permission_mode` in global mode and the plugin governance lists (`requiredPlugins` / `optionalPlugins` /
|
|
89038
89915
|
* `forbiddenPlugins`) in project mode when the `devin` override authors them.
|
|
89039
89916
|
*
|
|
89040
89917
|
* The sibling `sandbox` block — which decides what a permitted command may
|
|
@@ -89142,6 +90019,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89142
90019
|
});
|
|
89143
90020
|
} 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.");
|
|
89144
90021
|
}
|
|
90022
|
+
applyPermissionModeOverride({
|
|
90023
|
+
permissionMode: config.devin?.permission_mode,
|
|
90024
|
+
patch,
|
|
90025
|
+
global,
|
|
90026
|
+
logger
|
|
90027
|
+
});
|
|
89145
90028
|
applyPluginGovernanceOverride({
|
|
89146
90029
|
override: config.devin,
|
|
89147
90030
|
settings,
|
|
@@ -89185,6 +90068,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89185
90068
|
if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
|
|
89186
90069
|
else logger?.warn(`Skipped Devin \`${key}\` on import: ${key === "forbiddenPlugins" ? "expected an array of strings" : "expected an array of plugin sources (strings or objects)"}.`);
|
|
89187
90070
|
}
|
|
90071
|
+
const permissionMode = importPermissionMode({
|
|
90072
|
+
value: settings.permission_mode,
|
|
90073
|
+
isGlobalFile: toPosixPath(this.getRelativeDirPath()) === toPosixPath(DevinPermissions.getSettablePaths({ global: true }).relativeDirPath),
|
|
90074
|
+
logger
|
|
90075
|
+
});
|
|
90076
|
+
if (permissionMode !== void 0) devinOverride.permission_mode = permissionMode;
|
|
89188
90077
|
const result = { ...config };
|
|
89189
90078
|
if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
|
|
89190
90079
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
@@ -89201,6 +90090,48 @@ const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
|
|
|
89201
90090
|
"forbiddenPlugins"
|
|
89202
90091
|
];
|
|
89203
90092
|
/**
|
|
90093
|
+
* Decide what an imported `permission_mode` contributes to the `devin`
|
|
90094
|
+
* override. It is lifted from the user config only: unlike `sandbox`, a
|
|
90095
|
+
* project value Devin ignores is skipped with a warning rather than surfaced,
|
|
90096
|
+
* so it cannot slip into a `--global` generate later. `null` is Devin's
|
|
90097
|
+
* spelling of the default and carries nothing to keep; any other value the
|
|
90098
|
+
* override schema would reject is dropped with a warning so the next generate
|
|
90099
|
+
* cannot fail. `dangerous` is lifted but named, as on generate.
|
|
90100
|
+
*/
|
|
90101
|
+
function importPermissionMode({ value, isGlobalFile, logger }) {
|
|
90102
|
+
if (value === void 0 || value === null) return void 0;
|
|
90103
|
+
if (!isGlobalFile) {
|
|
90104
|
+
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.");
|
|
90105
|
+
return;
|
|
90106
|
+
}
|
|
90107
|
+
const parsed = DevinPermissionModeSchema.safeParse(value);
|
|
90108
|
+
if (!parsed.success) {
|
|
90109
|
+
logger?.warn(`Skipped Devin \`permission_mode\` ${quoteValueForWarning(value)} on import: expected one of ${DevinPermissionModeSchema.options.join(", ")}.`);
|
|
90110
|
+
return;
|
|
90111
|
+
}
|
|
90112
|
+
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.");
|
|
90113
|
+
return parsed.data;
|
|
90114
|
+
}
|
|
90115
|
+
/**
|
|
90116
|
+
* Patch the `permission_mode` the `devin` override authors into the user
|
|
90117
|
+
* `config.json`. It sets the mode new local sessions start in, and Devin reads
|
|
90118
|
+
* it from the user config only, so in project mode it is dropped with a
|
|
90119
|
+
* warning. It is only ever written as authored — never derived from the
|
|
90120
|
+
* canonical rules — and `dangerous` is named in a warning, because every new
|
|
90121
|
+
* session then starts with all permission prompts bypassed.
|
|
90122
|
+
*
|
|
90123
|
+
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
90124
|
+
*/
|
|
90125
|
+
function applyPermissionModeOverride({ permissionMode, patch, global, logger }) {
|
|
90126
|
+
if (permissionMode === void 0) return;
|
|
90127
|
+
if (!global) {
|
|
90128
|
+
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.");
|
|
90129
|
+
return;
|
|
90130
|
+
}
|
|
90131
|
+
patch.permission_mode = permissionMode;
|
|
90132
|
+
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.");
|
|
90133
|
+
}
|
|
90134
|
+
/**
|
|
89204
90135
|
* Patch the plugin governance lists the `devin` override authors into the
|
|
89205
90136
|
* project `.devin/config.json`. Each authored list replaces the file's list
|
|
89206
90137
|
* whole; a list it does not author is left as the file holds it. Devin reads these
|
|
@@ -95190,7 +96121,8 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
95190
96121
|
*
|
|
95191
96122
|
* Reasonix matches a rule's tool exactly against its registered tools, which
|
|
95192
96123
|
* are snake_case (`read_file`, `write_file`, `grep`, `glob`, `web_fetch`,
|
|
95193
|
-
* `web_search`, `notebook_edit`, `task`
|
|
96124
|
+
* `web_search`, `notebook_edit`, and `task` up to v2.57.x / `spawn_agents` from
|
|
96125
|
+
* v2.58.0). Only `Bash`, `Edit` (every file
|
|
95194
96126
|
* writer: `write_file`, `edit_file`, `multi_edit`, `notebook_edit`, ...),
|
|
95195
96127
|
* `Browser` and `Computer` are Claude-Code-style family aliases
|
|
95196
96128
|
* (`canonicalRuleTool` / `groupOf`), so a `Read(...)` or `Write(...)` rule
|
|
@@ -95213,6 +96145,21 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
|
95213
96145
|
agent: "task"
|
|
95214
96146
|
};
|
|
95215
96147
|
/**
|
|
96148
|
+
* Reasonix tool names a canonical category writes beside its primary name, for
|
|
96149
|
+
* a tool Reasonix renamed. studio-v2.58.0 replaced the `task` delegation family
|
|
96150
|
+
* with `spawn_agents` (commit ad2c96347), while the 1.x line and v2 up to
|
|
96151
|
+
* v2.57.x still register `task`. When Reasonix loads the config file it checks
|
|
96152
|
+
* only a rule's syntax: a rule naming a tool the running build lacks matches
|
|
96153
|
+
* nothing (v2.34.0+ reports it as dormant) and is not rejected. (The v2 Web UI
|
|
96154
|
+
* permissions editor refuses to newly add such a rule with `ErrUnknownTool`,
|
|
96155
|
+
* but keeps the ones already in the file.) So writing both names keeps an
|
|
96156
|
+
* `agent` rule in force on old and new builds alike. Both names are read back
|
|
96157
|
+
* to the same category.
|
|
96158
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.58.0/internal/runtime/delegation/spawn.go
|
|
96159
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/studio-v2.57.0/internal/runtime/delegation/task.go
|
|
96160
|
+
*/
|
|
96161
|
+
const CANONICAL_TO_EXTRA_REASONIX_TOOL_NAMES = { agent: ["spawn_agents"] };
|
|
96162
|
+
/**
|
|
95216
96163
|
* The Claude-Code-style spellings rulesync emitted before it followed the
|
|
95217
96164
|
* Reasonix tool vocabulary. None of them reaches a Reasonix tool, so they are
|
|
95218
96165
|
* read back on import (a file generated by an older rulesync still imports)
|
|
@@ -95235,7 +96182,8 @@ const CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES = {
|
|
|
95235
96182
|
*/
|
|
95236
96183
|
const REASONIX_TO_CANONICAL_TOOL_NAMES = {
|
|
95237
96184
|
...invertRecord(CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES),
|
|
95238
|
-
...invertRecord(CANONICAL_TO_REASONIX_TOOL_NAMES)
|
|
96185
|
+
...invertRecord(CANONICAL_TO_REASONIX_TOOL_NAMES),
|
|
96186
|
+
...Object.fromEntries(Object.entries(CANONICAL_TO_EXTRA_REASONIX_TOOL_NAMES).flatMap(([canonical, names]) => names.map((name) => [name, canonical])))
|
|
95239
96187
|
};
|
|
95240
96188
|
/** Every Reasonix tool name a canonical category owns on generate. */
|
|
95241
96189
|
function reasonixToolNamesOf(canonical) {
|
|
@@ -95243,14 +96191,20 @@ function reasonixToolNamesOf(canonical) {
|
|
|
95243
96191
|
record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
|
|
95244
96192
|
key: canonical
|
|
95245
96193
|
});
|
|
95246
|
-
const current =
|
|
95247
|
-
return legacy === void 0 ?
|
|
96194
|
+
const current = toReasonixToolNames(canonical);
|
|
96195
|
+
return legacy === void 0 ? current : [...current, legacy];
|
|
95248
96196
|
}
|
|
95249
|
-
|
|
95250
|
-
|
|
96197
|
+
/** The Reasonix tool names a canonical category's rules are written under. */
|
|
96198
|
+
function toReasonixToolNames(canonical) {
|
|
96199
|
+
const primary = lookupOwn({
|
|
95251
96200
|
record: CANONICAL_TO_REASONIX_TOOL_NAMES,
|
|
95252
96201
|
key: canonical
|
|
95253
96202
|
}) ?? canonical;
|
|
96203
|
+
const extra = lookupOwn({
|
|
96204
|
+
record: CANONICAL_TO_EXTRA_REASONIX_TOOL_NAMES,
|
|
96205
|
+
key: canonical
|
|
96206
|
+
});
|
|
96207
|
+
return extra === void 0 ? [primary] : [primary, ...extra];
|
|
95254
96208
|
}
|
|
95255
96209
|
function toCanonicalToolName$2(reasonixName) {
|
|
95256
96210
|
return lookupOwn({
|
|
@@ -95658,6 +96612,17 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
95658
96612
|
});
|
|
95659
96613
|
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.`);
|
|
95660
96614
|
}
|
|
96615
|
+
warnOnLoosenedAgentEntries({
|
|
96616
|
+
existingPermissions,
|
|
96617
|
+
managedToolNames,
|
|
96618
|
+
generated: {
|
|
96619
|
+
allow: [...allow, ...rawAllow],
|
|
96620
|
+
ask: [...ask, ...rawAsk],
|
|
96621
|
+
deny: [...deny, ...rawDeny]
|
|
96622
|
+
},
|
|
96623
|
+
relativeFilePath: relativeFilePathForLog,
|
|
96624
|
+
logger
|
|
96625
|
+
});
|
|
95661
96626
|
const mergedPermissions = { ...existingPermissions };
|
|
95662
96627
|
setOrDeleteEntries(mergedPermissions, "allow", [
|
|
95663
96628
|
...preservedAllow,
|
|
@@ -95833,6 +96798,55 @@ function preserveExistingEntries({ existingPermissions, managedToolNames, rawOwn
|
|
|
95833
96798
|
deny
|
|
95834
96799
|
};
|
|
95835
96800
|
}
|
|
96801
|
+
/**
|
|
96802
|
+
* `spawn_agents` became a managed name with Reasonix v2.58.0 support, so a
|
|
96803
|
+
* stricter rule the user wrote for it by hand (e.g. a v2.58-era
|
|
96804
|
+
* `deny = ["spawn_agents"]` next to an older `agent: allow` source) would be
|
|
96805
|
+
* silently replaced by a looser one. Name every such loosening.
|
|
96806
|
+
*/
|
|
96807
|
+
function warnOnLoosenedAgentEntries({ existingPermissions, managedToolNames, generated, relativeFilePath, logger }) {
|
|
96808
|
+
if (!logger || !managedToolNames.has("spawn_agents")) return;
|
|
96809
|
+
const loosened = findLoosenedEntries({
|
|
96810
|
+
existingPermissions,
|
|
96811
|
+
toolNames: new Set(reasonixToolNamesOf("agent")),
|
|
96812
|
+
generated
|
|
96813
|
+
});
|
|
96814
|
+
if (loosened.length === 0) return;
|
|
96815
|
+
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'.`);
|
|
96816
|
+
}
|
|
96817
|
+
const PERMISSION_STRICTNESS = {
|
|
96818
|
+
allow: 0,
|
|
96819
|
+
ask: 1,
|
|
96820
|
+
deny: 2
|
|
96821
|
+
};
|
|
96822
|
+
/**
|
|
96823
|
+
* Existing entries for `toolNames` whose action is stricter than the one the
|
|
96824
|
+
* generated lists give the same entry, formatted `<list> "<entry>"`. An entry
|
|
96825
|
+
* the generated lists drop counts as no stricter than `allow`.
|
|
96826
|
+
*/
|
|
96827
|
+
function findLoosenedEntries({ existingPermissions, toolNames, generated }) {
|
|
96828
|
+
const generatedStrictness = /* @__PURE__ */ new Map();
|
|
96829
|
+
for (const list of [
|
|
96830
|
+
"allow",
|
|
96831
|
+
"ask",
|
|
96832
|
+
"deny"
|
|
96833
|
+
]) for (const entry of generated[list]) {
|
|
96834
|
+
const key = normalizeToolPatternEntry(entry);
|
|
96835
|
+
generatedStrictness.set(key, Math.max(generatedStrictness.get(key) ?? 0, PERMISSION_STRICTNESS[list]));
|
|
96836
|
+
}
|
|
96837
|
+
const loosened = [];
|
|
96838
|
+
for (const list of ["ask", "deny"]) for (const entry of toStringArray$1(existingPermissions[list])) {
|
|
96839
|
+
if (isReasonixExactCommandEntry(entry)) continue;
|
|
96840
|
+
if (!toolNames.has(parseToolPatternEntry(entry).toolName)) continue;
|
|
96841
|
+
const key = normalizeToolPatternEntry(entry);
|
|
96842
|
+
if ((generatedStrictness.get(key) ?? 0) < PERMISSION_STRICTNESS[list]) loosened.push(`${list} "${entry}"`);
|
|
96843
|
+
}
|
|
96844
|
+
return loosened;
|
|
96845
|
+
}
|
|
96846
|
+
/** Spell an entry the way rulesync writes it, so `name(*)` equals `name`. */
|
|
96847
|
+
function normalizeToolPatternEntry(entry) {
|
|
96848
|
+
return buildToolPatternEntry(parseToolPatternEntry(entry));
|
|
96849
|
+
}
|
|
95836
96850
|
/** Sort, dedupe and set a permissions array key, deleting it when empty. */
|
|
95837
96851
|
function setOrDeleteEntries(table, key, entries) {
|
|
95838
96852
|
const merged = (0, es_toolkit.uniq)(entries.toSorted());
|
|
@@ -95846,22 +96860,19 @@ function convertRulesyncToReasonixPermissions(config) {
|
|
|
95846
96860
|
const allow = [];
|
|
95847
96861
|
const ask = [];
|
|
95848
96862
|
const deny = [];
|
|
95849
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
95850
|
-
const
|
|
95851
|
-
|
|
95852
|
-
|
|
95853
|
-
|
|
95854
|
-
|
|
95855
|
-
|
|
95856
|
-
|
|
95857
|
-
|
|
95858
|
-
|
|
95859
|
-
|
|
95860
|
-
|
|
95861
|
-
|
|
95862
|
-
break;
|
|
95863
|
-
case "deny": deny.push(entry);
|
|
95864
|
-
}
|
|
96863
|
+
for (const [category, rules] of Object.entries(config.permission)) for (const reasonixToolName of toReasonixToolNames(category)) for (const [pattern, action] of Object.entries(rules)) {
|
|
96864
|
+
const entry = buildToolPatternEntry({
|
|
96865
|
+
toolName: reasonixToolName,
|
|
96866
|
+
pattern
|
|
96867
|
+
});
|
|
96868
|
+
switch (action) {
|
|
96869
|
+
case "allow":
|
|
96870
|
+
allow.push(entry);
|
|
96871
|
+
break;
|
|
96872
|
+
case "ask":
|
|
96873
|
+
ask.push(entry);
|
|
96874
|
+
break;
|
|
96875
|
+
case "deny": deny.push(entry);
|
|
95865
96876
|
}
|
|
95866
96877
|
}
|
|
95867
96878
|
return {
|
|
@@ -96088,7 +97099,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96088
97099
|
validate: true
|
|
96089
97100
|
});
|
|
96090
97101
|
}
|
|
96091
|
-
toRulesyncPermissions() {
|
|
97102
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
96092
97103
|
const constructor = this.constructor;
|
|
96093
97104
|
let settings;
|
|
96094
97105
|
try {
|
|
@@ -96102,9 +97113,12 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96102
97113
|
} catch (error) {
|
|
96103
97114
|
throw new Error(`Failed to parse ${constructor.getToolLabel()} VS Code settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
96104
97115
|
}
|
|
96105
|
-
const rules = {
|
|
96106
|
-
|
|
96107
|
-
|
|
97116
|
+
const rules = importVscodeCommandLists({
|
|
97117
|
+
allowed: toStringArray$1(settings[constructor.getAllowedCommandsKey()]),
|
|
97118
|
+
denied: toStringArray$1(settings[constructor.getDeniedCommandsKey()]),
|
|
97119
|
+
toolLabel: constructor.getToolLabel(),
|
|
97120
|
+
logger
|
|
97121
|
+
});
|
|
96108
97122
|
const permission = {};
|
|
96109
97123
|
if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
|
|
96110
97124
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
@@ -101428,6 +102442,9 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
101428
102442
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
101429
102443
|
toolTarget;
|
|
101430
102444
|
global;
|
|
102445
|
+
sweepsThroughInRootLinks() {
|
|
102446
|
+
return this.global;
|
|
102447
|
+
}
|
|
101431
102448
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
101432
102449
|
super({
|
|
101433
102450
|
outputRoot,
|
|
@@ -104671,6 +105688,12 @@ Object.defineProperty(exports, "assertDirectoryIfExists", {
|
|
|
104671
105688
|
return assertDirectoryIfExists;
|
|
104672
105689
|
}
|
|
104673
105690
|
});
|
|
105691
|
+
Object.defineProperty(exports, "assertLandingOutsideVcsMetadata", {
|
|
105692
|
+
enumerable: true,
|
|
105693
|
+
get: function() {
|
|
105694
|
+
return assertLandingOutsideVcsMetadata;
|
|
105695
|
+
}
|
|
105696
|
+
});
|
|
104674
105697
|
Object.defineProperty(exports, "assertNoSymlinkBelowRoot", {
|
|
104675
105698
|
enumerable: true,
|
|
104676
105699
|
get: function() {
|
|
@@ -104713,6 +105736,12 @@ Object.defineProperty(exports, "checkPathTraversal", {
|
|
|
104713
105736
|
return checkPathTraversal;
|
|
104714
105737
|
}
|
|
104715
105738
|
});
|
|
105739
|
+
Object.defineProperty(exports, "classifyRemoteName", {
|
|
105740
|
+
enumerable: true,
|
|
105741
|
+
get: function() {
|
|
105742
|
+
return classifyRemoteName;
|
|
105743
|
+
}
|
|
105744
|
+
});
|
|
104716
105745
|
Object.defineProperty(exports, "convertFromTool", {
|
|
104717
105746
|
enumerable: true,
|
|
104718
105747
|
get: function() {
|
|
@@ -104845,16 +105874,16 @@ Object.defineProperty(exports, "groupSpellingsByCaseFoldedIdentity", {
|
|
|
104845
105874
|
return groupSpellingsByCaseFoldedIdentity;
|
|
104846
105875
|
}
|
|
104847
105876
|
});
|
|
104848
|
-
Object.defineProperty(exports, "
|
|
105877
|
+
Object.defineProperty(exports, "hasEnclosingMarkOutsideKeycap", {
|
|
104849
105878
|
enumerable: true,
|
|
104850
105879
|
get: function() {
|
|
104851
|
-
return
|
|
105880
|
+
return hasEnclosingMarkOutsideKeycap;
|
|
104852
105881
|
}
|
|
104853
105882
|
});
|
|
104854
|
-
Object.defineProperty(exports, "
|
|
105883
|
+
Object.defineProperty(exports, "hasWindowsFoldableName", {
|
|
104855
105884
|
enumerable: true,
|
|
104856
105885
|
get: function() {
|
|
104857
|
-
return
|
|
105886
|
+
return hasWindowsFoldableName;
|
|
104858
105887
|
}
|
|
104859
105888
|
});
|
|
104860
105889
|
Object.defineProperty(exports, "importFromTool", {
|
|
@@ -105103,6 +106132,12 @@ Object.defineProperty(exports, "stripHiddenCharacters", {
|
|
|
105103
106132
|
return stripHiddenCharacters;
|
|
105104
106133
|
}
|
|
105105
106134
|
});
|
|
106135
|
+
Object.defineProperty(exports, "sweepRefusalReason", {
|
|
106136
|
+
enumerable: true,
|
|
106137
|
+
get: function() {
|
|
106138
|
+
return sweepRefusalReason;
|
|
106139
|
+
}
|
|
106140
|
+
});
|
|
105106
106141
|
Object.defineProperty(exports, "toInstructionBudgetFile", {
|
|
105107
106142
|
enumerable: true,
|
|
105108
106143
|
get: function() {
|