rulesync 43.6.3 → 43.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/index.cjs +183 -163
- package/dist/cli/index.js +181 -161
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-DCvPujUW.cjs → import-C5ETusOs.cjs} +1257 -188
- package/dist/{import-w0JlEoNl.js → import-Cu5o6uKO.js} +1237 -186
- package/dist/{import-w0JlEoNl.js.map → import-Cu5o6uKO.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +7 -7
|
@@ -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";
|
|
@@ -29122,11 +29354,7 @@ var ZoocodeSkill = class extends RooSkill {
|
|
|
29122
29354
|
static toolTarget = "zoocode";
|
|
29123
29355
|
};
|
|
29124
29356
|
// src/features/skills/skills-processor.ts
|
|
29125
|
-
var skills_processor_exports = /* @__PURE__ */ __exportAll({
|
|
29126
|
-
SkillsProcessor: () => SkillsProcessor,
|
|
29127
|
-
SkillsProcessorToolTargetSchema: () => SkillsProcessorToolTargetSchema,
|
|
29128
|
-
toolSkillFactories: () => toolSkillFactories
|
|
29129
|
-
});
|
|
29357
|
+
var skills_processor_exports = /* @__PURE__ */ __exportAll({ toolSkillFactories: () => toolSkillFactories });
|
|
29130
29358
|
const SkillsProcessorToolTargetSchema = zod_mini.z.enum(skillsProcessorToolTargetTuple);
|
|
29131
29359
|
/**
|
|
29132
29360
|
* Factory Map mapping tool targets to their skill factories.
|
|
@@ -29974,6 +30202,9 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
29974
30202
|
global: this.global
|
|
29975
30203
|
});
|
|
29976
30204
|
}
|
|
30205
|
+
sweepsThroughInRootLinks() {
|
|
30206
|
+
return this.global;
|
|
30207
|
+
}
|
|
29977
30208
|
/**
|
|
29978
30209
|
* The tool's skills roots that exist on disk, each paired with its absolute
|
|
29979
30210
|
* path and vetted as writable inside this run's output root. Shared by the
|
|
@@ -30003,6 +30234,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
30003
30234
|
this.logger.warn(`Skipping the orphan sweep for ${quoteForLog(skillsDirPath)}; nothing under it is deleted: ${stripControlCharacters(formatError(error))}`);
|
|
30004
30235
|
continue;
|
|
30005
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
|
+
}
|
|
30006
30246
|
existingRoots.push({
|
|
30007
30247
|
root,
|
|
30008
30248
|
skillsDirPath
|
|
@@ -33229,11 +33469,17 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33229
33469
|
*/
|
|
33230
33470
|
static async fromRulesyncSubagents({ outputRoot = process.cwd(), rulesyncSubagents, global = false, logger, deleteOrphans = false }) {
|
|
33231
33471
|
const agentsDirPath = this.getSettablePaths({ global }).relativeDirPath;
|
|
33232
|
-
const
|
|
33472
|
+
const agentsDirFullPath = (0, node_path.join)(outputRoot, agentsDirPath);
|
|
33473
|
+
const writeRefused = await writeRefusalReason({
|
|
33233
33474
|
rootPath: outputRoot,
|
|
33234
|
-
targetPath: (0, node_path.join)(
|
|
33235
|
-
});
|
|
33236
|
-
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));
|
|
33237
33483
|
const subagents = rulesyncSubagents.map((rulesyncSubagent) => this.fromRulesyncSubagent({
|
|
33238
33484
|
outputRoot,
|
|
33239
33485
|
relativeDirPath: this.getSettablePaths({ global }).relativeDirPath,
|
|
@@ -33249,8 +33495,8 @@ var FirebenderSubagent = class FirebenderSubagent extends ToolSubagent {
|
|
|
33249
33495
|
global
|
|
33250
33496
|
})),
|
|
33251
33497
|
existingAgentFileNames,
|
|
33252
|
-
deleteOrphans,
|
|
33253
|
-
frozen:
|
|
33498
|
+
deleteOrphans: deleteOrphans && !sweepRefused,
|
|
33499
|
+
frozen: writeRefused,
|
|
33254
33500
|
global,
|
|
33255
33501
|
logger
|
|
33256
33502
|
});
|
|
@@ -37368,6 +37614,9 @@ function compareRelativePathsShallowestFirst(left, right) {
|
|
|
37368
37614
|
var SubagentsProcessor = class extends FeatureProcessor {
|
|
37369
37615
|
toolTarget;
|
|
37370
37616
|
global;
|
|
37617
|
+
sweepsThroughInRootLinks() {
|
|
37618
|
+
return this.global;
|
|
37619
|
+
}
|
|
37371
37620
|
deleteOrphans;
|
|
37372
37621
|
getFactory;
|
|
37373
37622
|
/**
|
|
@@ -38089,6 +38338,39 @@ function warnAndSkipMcpServer({ toolName, serverName, reason, logger }) {
|
|
|
38089
38338
|
return null;
|
|
38090
38339
|
}
|
|
38091
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
|
+
/**
|
|
38092
38374
|
* The `tools` map Kilo and OpenCode keep beside `mcp` is keyed `<server>_<tool>`
|
|
38093
38375
|
* and reaches servers `mcp` does not list — ones the global config or a
|
|
38094
38376
|
* marketplace defines — so entries naming no listed
|
|
@@ -38650,7 +38932,11 @@ var KiloMcp = class KiloMcp extends ToolMcp {
|
|
|
38650
38932
|
const existingMcp = readExistingKiloMcpEntries(fileContent);
|
|
38651
38933
|
const parsedExisting = (0, jsonc_parser.parse)(fileContent || "{}");
|
|
38652
38934
|
const existingDocument = isRecord(parsedExisting) ? parsedExisting : {};
|
|
38653
|
-
const mcpServers =
|
|
38935
|
+
const mcpServers = skipServersWithEmptyEnabledTools({
|
|
38936
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
38937
|
+
toolName: "Kilo",
|
|
38938
|
+
logger
|
|
38939
|
+
});
|
|
38654
38940
|
const { mcp: convertedMcp, toolPermissions } = convertToKiloFormat(resolveKiloEnvVarRefs({
|
|
38655
38941
|
mcpServers,
|
|
38656
38942
|
global,
|
|
@@ -38779,7 +39065,8 @@ const OPENCODE_PASSTHROUGH_SERVER_FIELDS = [
|
|
|
38779
39065
|
"timeout",
|
|
38780
39066
|
"oauth",
|
|
38781
39067
|
"protocol",
|
|
38782
|
-
"codemode"
|
|
39068
|
+
"codemode",
|
|
39069
|
+
"pool"
|
|
38783
39070
|
];
|
|
38784
39071
|
const OPENCODE_MCP_TRANSPORT_KEYS = [
|
|
38785
39072
|
...Object.keys(OpencodeMcpLocalServerSchema.def.shape),
|
|
@@ -38942,7 +39229,7 @@ function warnAboutToggleDroppedKeys(serverName, serverConfig, logger) {
|
|
|
38942
39229
|
* - env -> environment
|
|
38943
39230
|
* - disabled -> enabled (inverted)
|
|
38944
39231
|
* - enabledTools/disabledTools -> top-level tools map (with server name prefix)
|
|
38945
|
-
* - OpenCode-supported extras (timeout, oauth, protocol, codemode) -> passed through verbatim
|
|
39232
|
+
* - OpenCode-supported extras (timeout, oauth, protocol, codemode, pool) -> passed through verbatim
|
|
38946
39233
|
*/
|
|
38947
39234
|
function convertServerToOpencodeFormat(serverName, serverConfig, existingEntry, configShape, logger) {
|
|
38948
39235
|
const serverRecord = serverConfig;
|
|
@@ -39182,7 +39469,11 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
39182
39469
|
if (fileContent) relativeFilePath = this.layout.jsonFileName;
|
|
39183
39470
|
}
|
|
39184
39471
|
const transformedServers = convertEnvVarRefsToToolFormat({
|
|
39185
|
-
mcpServers:
|
|
39472
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
39473
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
39474
|
+
toolName: "OpenCode",
|
|
39475
|
+
logger
|
|
39476
|
+
}),
|
|
39186
39477
|
replacement: "{env:$1}"
|
|
39187
39478
|
});
|
|
39188
39479
|
const existingJson = (0, jsonc_parser.parse)(fileContent || "{}");
|
|
@@ -48778,6 +49069,9 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
48778
49069
|
language;
|
|
48779
49070
|
deriveSubprojectPathFromGlobs;
|
|
48780
49071
|
global;
|
|
49072
|
+
sweepsThroughInRootLinks() {
|
|
49073
|
+
return this.global;
|
|
49074
|
+
}
|
|
48781
49075
|
getFactory;
|
|
48782
49076
|
skills;
|
|
48783
49077
|
featureOptions;
|
|
@@ -51362,12 +51656,29 @@ var HermesagentCheck = class HermesagentCheck extends ToolCheck {
|
|
|
51362
51656
|
};
|
|
51363
51657
|
}
|
|
51364
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
|
+
}
|
|
51365
51676
|
toRulesyncCheck() {
|
|
51366
51677
|
const spec = HermesCheckSpecSchema.parse(JSON.parse(this.fileContent));
|
|
51367
51678
|
return new RulesyncCheck({
|
|
51368
51679
|
outputRoot: ".",
|
|
51369
51680
|
relativeDirPath: RULESYNC_CHECKS_RELATIVE_DIR_PATH,
|
|
51370
|
-
relativeFilePath: `${spec.slug}.md`,
|
|
51681
|
+
relativeFilePath: `${this.importedCheckStem(spec.slug)}.md`,
|
|
51371
51682
|
frontmatter: {
|
|
51372
51683
|
targets: ["*"],
|
|
51373
51684
|
description: spec.description,
|
|
@@ -51875,6 +52186,9 @@ const checksProcessorToolTargetsGlobal = allToolTargetKeys.filter((target) => {
|
|
|
51875
52186
|
var ChecksProcessor = class extends FeatureProcessor {
|
|
51876
52187
|
toolTarget;
|
|
51877
52188
|
global;
|
|
52189
|
+
sweepsThroughInRootLinks() {
|
|
52190
|
+
return this.global;
|
|
52191
|
+
}
|
|
51878
52192
|
getFactory;
|
|
51879
52193
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory$2, dryRun = false, logger }) {
|
|
51880
52194
|
super({
|
|
@@ -54186,6 +54500,10 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
|
54186
54500
|
field: "tool",
|
|
54187
54501
|
value: h.tool
|
|
54188
54502
|
});
|
|
54503
|
+
for (const { canonical, tool } of converterConfig.stringPassthroughFields ?? []) if (canonical === "if" && h[tool] !== "") fields.push({
|
|
54504
|
+
field: "if",
|
|
54505
|
+
value: h[tool]
|
|
54506
|
+
});
|
|
54189
54507
|
return fields;
|
|
54190
54508
|
}
|
|
54191
54509
|
/**
|
|
@@ -62962,10 +63280,12 @@ function emittedEventsWithHook(params) {
|
|
|
62962
63280
|
function converterCapabilities(config) {
|
|
62963
63281
|
if (!config.supportedHookTypes) throw new Error("Hooks converter config must declare its supportedHookTypes");
|
|
62964
63282
|
const passesThroughEnabled = (config.booleanPassthroughFields ?? []).some((field) => field.canonical === "enabled");
|
|
63283
|
+
const passesThroughIf = (config.stringPassthroughFields ?? []).some((field) => field.canonical === "if") && !(config.skippedWhenFields ?? []).includes("if");
|
|
62965
63284
|
return {
|
|
62966
63285
|
supportedEvents: config.supportedEvents,
|
|
62967
63286
|
supportedHookTypes: [...config.supportedHookTypes],
|
|
62968
|
-
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {}
|
|
63287
|
+
...passesThroughEnabled ? { supportsPerHookEnabled: true } : {},
|
|
63288
|
+
...passesThroughIf ? { supportsPerHookIf: true } : {}
|
|
62969
63289
|
};
|
|
62970
63290
|
}
|
|
62971
63291
|
const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
@@ -63610,6 +63930,9 @@ const hooksProcessorToolTargetsGlobalImportable = [...toolHooksFactories.entries
|
|
|
63610
63930
|
var HooksProcessor = class extends FeatureProcessor {
|
|
63611
63931
|
toolTarget;
|
|
63612
63932
|
global;
|
|
63933
|
+
sweepsThroughInRootLinks() {
|
|
63934
|
+
return this.global;
|
|
63935
|
+
}
|
|
63613
63936
|
preserveUnownedHooks;
|
|
63614
63937
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, preserveUnownedHooks = false, logger }) {
|
|
63615
63938
|
super({
|
|
@@ -63688,7 +64011,8 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63688
64011
|
if (!factory) throw new Error(`Unsupported tool target: ${this.toolTarget}`);
|
|
63689
64012
|
const config = rulesyncHooks.getJson();
|
|
63690
64013
|
const sharedHooks = config.hooks;
|
|
63691
|
-
const
|
|
64014
|
+
const overrideKey = HOOKS_OVERRIDE_KEY_ALIASES[this.toolTarget] ?? this.toolTarget;
|
|
64015
|
+
const overrideHooks = config[overrideKey]?.hooks ?? {};
|
|
63692
64016
|
const effectiveHooks = {
|
|
63693
64017
|
...sharedHooks,
|
|
63694
64018
|
...overrideHooks
|
|
@@ -63726,7 +64050,10 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63726
64050
|
if (eventsWithUnsupportedMatcher.length > 0) this.logger.warn(`Skipped matcher hook(s) for ${this.toolTarget} (not supported): ${eventsWithUnsupportedMatcher.join(", ")}`);
|
|
63727
64051
|
const toolHooks = await factory.class.fromRulesyncHooks({
|
|
63728
64052
|
outputRoot: this.outputRoot,
|
|
63729
|
-
rulesyncHooks
|
|
64053
|
+
rulesyncHooks: factory.supportsPerHookIf ? rulesyncHooks : this.withoutConditionalHooks({
|
|
64054
|
+
rulesyncHooks,
|
|
64055
|
+
overrideKey
|
|
64056
|
+
}),
|
|
63730
64057
|
validate: true,
|
|
63731
64058
|
global: this.global,
|
|
63732
64059
|
preserveUnowned: this.preserveUnownedHooks,
|
|
@@ -63749,6 +64076,53 @@ var HooksProcessor = class extends FeatureProcessor {
|
|
|
63749
64076
|
return result;
|
|
63750
64077
|
}
|
|
63751
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
|
+
/**
|
|
63752
64126
|
* Warns about canonical per-hook fields that this target's hooks format
|
|
63753
64127
|
* cannot express, so a hook does not silently change behavior on generation.
|
|
63754
64128
|
* Only canonical definitions are considered: a tool-native field inside an
|
|
@@ -63993,6 +64367,35 @@ var BobIgnore = class extends PlainToolIgnore {
|
|
|
63993
64367
|
};
|
|
63994
64368
|
}
|
|
63995
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
|
+
}
|
|
63996
64399
|
// src/features/ignore/claudecode-ignore.ts
|
|
63997
64400
|
const DEFAULT_FILE_MODE = "shared";
|
|
63998
64401
|
/**
|
|
@@ -64051,9 +64454,12 @@ var ClaudecodeIgnore = class ClaudecodeIgnore extends ToolIgnore {
|
|
|
64051
64454
|
}
|
|
64052
64455
|
toRulesyncIgnore() {
|
|
64053
64456
|
const fileContent = this.patterns.map((pattern) => {
|
|
64054
|
-
if (isReadDenyEntry(pattern)) return
|
|
64457
|
+
if (isReadDenyEntry(pattern)) return claudeReadDenyPathToIgnorePattern({
|
|
64458
|
+
path: pattern.slice(5, -1),
|
|
64459
|
+
toolLabel: "Claude Code"
|
|
64460
|
+
});
|
|
64055
64461
|
return pattern;
|
|
64056
|
-
}).filter((pattern) => pattern.length > 0).join("\n");
|
|
64462
|
+
}).filter((pattern) => pattern !== null && pattern.length > 0).join("\n");
|
|
64057
64463
|
return new RulesyncIgnore({
|
|
64058
64464
|
outputRoot: this.outputRoot,
|
|
64059
64465
|
relativeDirPath: RulesyncIgnore.getSettablePaths().recommended.relativeDirPath,
|
|
@@ -64328,7 +64734,7 @@ var FirebenderIgnore = class FirebenderIgnore extends ToolIgnore {
|
|
|
64328
64734
|
const paths = this.getSettablePaths({ global });
|
|
64329
64735
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
64330
64736
|
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
64331
|
-
const managedPatterns =
|
|
64737
|
+
const managedPatterns = patterns.filter((pattern, index) => patterns.lastIndexOf(pattern) === index);
|
|
64332
64738
|
return new FirebenderIgnore({
|
|
64333
64739
|
outputRoot,
|
|
64334
64740
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -64983,7 +65389,10 @@ const asClaudeStyleSettings = (document) => {
|
|
|
64983
65389
|
};
|
|
64984
65390
|
/** The pattern of a `read_file(...)` (or legacy `Read(...)`) deny, else null. */
|
|
64985
65391
|
const readDenyPatternOf = (entry) => {
|
|
64986
|
-
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
|
+
});
|
|
64987
65396
|
return null;
|
|
64988
65397
|
};
|
|
64989
65398
|
/**
|
|
@@ -65323,6 +65732,9 @@ var IgnoreProcessor = class extends FeatureProcessor {
|
|
|
65323
65732
|
getFactory;
|
|
65324
65733
|
featureOptions;
|
|
65325
65734
|
global;
|
|
65735
|
+
sweepsThroughInRootLinks() {
|
|
65736
|
+
return this.global;
|
|
65737
|
+
}
|
|
65326
65738
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, getFactory = defaultGetFactory$1, global = false, dryRun = false, logger, featureOptions }) {
|
|
65327
65739
|
super({
|
|
65328
65740
|
outputRoot,
|
|
@@ -65656,30 +66068,71 @@ function isEmptyValue(value) {
|
|
|
65656
66068
|
if (Array.isArray(value)) return value.length === 0;
|
|
65657
66069
|
return isPlainObject(value) && Object.keys(value).length === 0;
|
|
65658
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
|
+
}
|
|
65659
66092
|
/**
|
|
65660
|
-
* Antigravity uses `serverUrl` (not the canonical `url`) for
|
|
66093
|
+
* Antigravity uses `serverUrl` (not the canonical `url`) for remote MCP
|
|
65661
66094
|
* servers. Convert the canonical shape into Antigravity's on the way out, and
|
|
65662
66095
|
* back to canonical on the way in.
|
|
65663
66096
|
*
|
|
65664
66097
|
* Antigravity documents no `type` field: the transport follows from `command`
|
|
65665
|
-
* (stdio) or `serverUrl` (Streamable HTTP
|
|
66098
|
+
* (stdio) or `serverUrl` (Streamable HTTP), and `agy plugin validate`
|
|
65666
66099
|
* reports the canonical `type` as an unknown field, so it is not written. A
|
|
65667
66100
|
* remote server that carries `command`, `args` or `env` was also observed to be
|
|
65668
66101
|
* refused at startup (runtime behavior reported by `agy plugin validate` 1.4.4,
|
|
65669
66102
|
* not documented), so an empty `args` / `env` left on a `serverUrl` server is
|
|
65670
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`.
|
|
65671
66108
|
* https://antigravity.google/docs/mcp
|
|
65672
66109
|
*/
|
|
65673
|
-
function toAntigravityMcpServers(servers) {
|
|
66110
|
+
function toAntigravityMcpServers(servers, logger) {
|
|
65674
66111
|
const renamed = renameServerField(servers, "url", "serverUrl");
|
|
65675
66112
|
return Object.fromEntries(Object.entries(renamed).map(([name, config]) => {
|
|
65676
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.`);
|
|
65677
66115
|
const isRemote = typeof config.serverUrl === "string";
|
|
65678
|
-
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
|
+
})];
|
|
65679
66121
|
}));
|
|
65680
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
|
+
*/
|
|
65681
66128
|
function toCanonicalMcpServers(servers) {
|
|
65682
|
-
|
|
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
|
+
}));
|
|
65683
66136
|
}
|
|
65684
66137
|
/**
|
|
65685
66138
|
* Parse `mcp_config.json` as JSONC and require a top-level object, so a file
|
|
@@ -65697,7 +66150,8 @@ function parseAntigravityMcpConfig(fileContent) {
|
|
|
65697
66150
|
* the IDE and the CLI.
|
|
65698
66151
|
*
|
|
65699
66152
|
* Antigravity writes a dedicated `mcp_config.json` whose `mcpServers` map lives
|
|
65700
|
-
* 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
|
|
65701
66155
|
* CLI's shared `settings.json`, this file is dedicated to MCP, so it is safely
|
|
65702
66156
|
* deletable. HTTP servers use `serverUrl` rather than the canonical `url`, so
|
|
65703
66157
|
* the field is translated in both directions.
|
|
@@ -65750,11 +66204,11 @@ var AntigravityMcp = class extends ToolMcp {
|
|
|
65750
66204
|
global
|
|
65751
66205
|
});
|
|
65752
66206
|
}
|
|
65753
|
-
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false }) {
|
|
66207
|
+
static async fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, global = false, logger }) {
|
|
65754
66208
|
const paths = this.getSettablePaths({ global });
|
|
65755
66209
|
const newJson = {
|
|
65756
66210
|
...parseAntigravityMcpConfig(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2)),
|
|
65757
|
-
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers())
|
|
66211
|
+
mcpServers: toAntigravityMcpServers(rulesyncMcp.getMcpServers(), logger)
|
|
65758
66212
|
};
|
|
65759
66213
|
return new this({
|
|
65760
66214
|
outputRoot,
|
|
@@ -69423,7 +69877,11 @@ var CrushMcp = class CrushMcp extends ToolMcp {
|
|
|
69423
69877
|
ownedPaths: [["mcp"]],
|
|
69424
69878
|
logger
|
|
69425
69879
|
});
|
|
69426
|
-
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, {
|
|
69427
69885
|
...serverConfig,
|
|
69428
69886
|
...readCrushOnlyKeys({
|
|
69429
69887
|
serverName,
|
|
@@ -70383,31 +70841,6 @@ var DshMcp = class DshMcp extends ToolMcp {
|
|
|
70383
70841
|
};
|
|
70384
70842
|
// src/features/mcp/factorydroid-mcp.ts
|
|
70385
70843
|
const TOOL_NAME = "factorydroid";
|
|
70386
|
-
/**
|
|
70387
|
-
* Drop servers whose `enabledTools` allowlist is empty.
|
|
70388
|
-
*
|
|
70389
|
-
* Canonically an empty allowlist allows no tools, but Droid applies
|
|
70390
|
-
* `enabledTools` only "when the list is non-empty"
|
|
70391
|
-
* (https://docs.factory.com/harness/mcp), so writing `[]` would load every
|
|
70392
|
-
* tool the server reports. Skipping the server is the only form that does not
|
|
70393
|
-
* hand the model more tools than the canonical config allows.
|
|
70394
|
-
*/
|
|
70395
|
-
function skipEmptyAllowlistServers({ mcpServers, logger }) {
|
|
70396
|
-
const kept = {};
|
|
70397
|
-
for (const [name, server] of Object.entries(mcpServers)) {
|
|
70398
|
-
if (server.enabledTools?.length === 0) {
|
|
70399
|
-
warnAndSkipMcpServer({
|
|
70400
|
-
toolName: TOOL_NAME,
|
|
70401
|
-
serverName: name,
|
|
70402
|
-
reason: "an empty enabledTools list, which allows no tools at all but which Factory Droid treats as no filter",
|
|
70403
|
-
logger
|
|
70404
|
-
});
|
|
70405
|
-
continue;
|
|
70406
|
-
}
|
|
70407
|
-
kept[name] = server;
|
|
70408
|
-
}
|
|
70409
|
-
return kept;
|
|
70410
|
-
}
|
|
70411
70844
|
var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
70412
70845
|
json;
|
|
70413
70846
|
constructor(params) {
|
|
@@ -70434,8 +70867,10 @@ var FactorydroidMcp = class FactorydroidMcp extends ToolMcp {
|
|
|
70434
70867
|
});
|
|
70435
70868
|
}
|
|
70436
70869
|
static fromRulesyncMcp({ outputRoot = process.cwd(), rulesyncMcp, validate = true, logger }) {
|
|
70437
|
-
const factorydroidConfig = { mcpServers:
|
|
70870
|
+
const factorydroidConfig = { mcpServers: skipServersWithEmptyEnabledTools({
|
|
70438
70871
|
mcpServers: rulesyncMcp.getMcpServers(),
|
|
70872
|
+
toolName: TOOL_NAME,
|
|
70873
|
+
productName: "Factory Droid",
|
|
70439
70874
|
logger
|
|
70440
70875
|
}) };
|
|
70441
70876
|
const fileContent = JSON.stringify(factorydroidConfig, null, 2);
|
|
@@ -71866,19 +72301,42 @@ function normalizeKimiCodeTransport({ transport, hasCommand }) {
|
|
|
71866
72301
|
return hasCommand ? "stdio" : "http";
|
|
71867
72302
|
}
|
|
71868
72303
|
/**
|
|
71869
|
-
* The only `executor` value Kimi Code accepts on a stdio
|
|
71870
|
-
* the never-implemented `kaos` option and narrowed the
|
|
71871
|
-
* `z.enum(['local'])`, so
|
|
71872
|
-
*
|
|
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.
|
|
71873
72310
|
* @see https://github.com/MoonshotAI/kimi-code/pull/4184
|
|
72311
|
+
* @see https://github.com/MoonshotAI/kimi-code/pull/4062
|
|
71874
72312
|
*/
|
|
71875
72313
|
const KIMI_CODE_MCP_EXECUTOR = "local";
|
|
71876
72314
|
/** Remove, with a warning, an `executor` value Kimi Code would reject. Mutates `server`. */
|
|
71877
72315
|
function dropUnsupportedKimiCodeExecutor({ name, server, logger }) {
|
|
71878
72316
|
if (server.executor === void 0 || server.executor === KIMI_CODE_MCP_EXECUTOR) return;
|
|
71879
|
-
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}".`);
|
|
71880
72318
|
delete server.executor;
|
|
71881
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
|
+
}
|
|
71882
72340
|
function toKimiCodeServer({ name, server, logger }) {
|
|
71883
72341
|
const transport = server.transport ?? server.type;
|
|
71884
72342
|
if (transport === "ws") {
|
|
@@ -71906,6 +72364,7 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71906
72364
|
for (const field of [
|
|
71907
72365
|
"env",
|
|
71908
72366
|
"cwd",
|
|
72367
|
+
"environment_id",
|
|
71909
72368
|
"executor",
|
|
71910
72369
|
"runtime_id",
|
|
71911
72370
|
"headers",
|
|
@@ -71922,6 +72381,12 @@ function toKimiCodeServer({ name, server, logger }) {
|
|
|
71922
72381
|
server: converted,
|
|
71923
72382
|
logger
|
|
71924
72383
|
});
|
|
72384
|
+
dropUnsupportedKimiCodeEnvironmentId({
|
|
72385
|
+
name,
|
|
72386
|
+
server: converted,
|
|
72387
|
+
isStdio: normalizedTransport === "stdio",
|
|
72388
|
+
logger
|
|
72389
|
+
});
|
|
71925
72390
|
if (server.disabled === true) converted.enabled = false;
|
|
71926
72391
|
return converted;
|
|
71927
72392
|
}
|
|
@@ -71946,6 +72411,15 @@ function fromKimiCodeServers({ servers, logger }) {
|
|
|
71946
72411
|
logger
|
|
71947
72412
|
});
|
|
71948
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
|
+
});
|
|
71949
72423
|
return [name, {
|
|
71950
72424
|
...rest,
|
|
71951
72425
|
...type && { type },
|
|
@@ -73114,7 +73588,12 @@ var OpenclawMcp = class OpenclawMcp extends ToolMcp {
|
|
|
73114
73588
|
const existingMcp = isRecord(existing["mcp"]) ? existing["mcp"] : {};
|
|
73115
73589
|
const existingServers = isRecord(existingMcp["servers"]) ? existingMcp[OPENCLAW_MCP_SERVERS_KEY] : {};
|
|
73116
73590
|
const converted = convertToOpenclawFormat({
|
|
73117
|
-
mcpServers:
|
|
73591
|
+
mcpServers: skipServersWithEmptyEnabledTools({
|
|
73592
|
+
mcpServers: rulesyncMcp.getMcpServers(),
|
|
73593
|
+
toolName: "OpenClaw",
|
|
73594
|
+
ignoreBlankNames: true,
|
|
73595
|
+
logger
|
|
73596
|
+
}),
|
|
73118
73597
|
logger
|
|
73119
73598
|
});
|
|
73120
73599
|
const servers = {
|
|
@@ -73742,7 +74221,11 @@ var PoolMcp = class PoolMcp extends ToolMcp {
|
|
|
73742
74221
|
const paths = this.getSettablePaths({ global });
|
|
73743
74222
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
73744
74223
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
73745
|
-
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]) => {
|
|
73746
74229
|
const rawServer = rulesyncMcp.getRawMcpServer(serverName);
|
|
73747
74230
|
const poolAllow = isRecord(rawServer) ? rawServer.poolAllow : void 0;
|
|
73748
74231
|
return [serverName, {
|
|
@@ -77268,7 +77751,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77268
77751
|
meta: {
|
|
77269
77752
|
supportsProject: true,
|
|
77270
77753
|
supportsGlobal: true,
|
|
77271
|
-
supportsEnabledTools:
|
|
77754
|
+
supportsEnabledTools: true,
|
|
77272
77755
|
supportsDisabledTools: true
|
|
77273
77756
|
}
|
|
77274
77757
|
}],
|
|
@@ -77277,7 +77760,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77277
77760
|
meta: {
|
|
77278
77761
|
supportsProject: true,
|
|
77279
77762
|
supportsGlobal: true,
|
|
77280
|
-
supportsEnabledTools:
|
|
77763
|
+
supportsEnabledTools: true,
|
|
77281
77764
|
supportsDisabledTools: true
|
|
77282
77765
|
}
|
|
77283
77766
|
}],
|
|
@@ -77286,7 +77769,7 @@ const toolMcpFactories = /* @__PURE__ */ new Map([
|
|
|
77286
77769
|
meta: {
|
|
77287
77770
|
supportsProject: true,
|
|
77288
77771
|
supportsGlobal: false,
|
|
77289
|
-
supportsEnabledTools:
|
|
77772
|
+
supportsEnabledTools: true,
|
|
77290
77773
|
supportsDisabledTools: true
|
|
77291
77774
|
}
|
|
77292
77775
|
}],
|
|
@@ -77853,6 +78336,9 @@ function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger })
|
|
|
77853
78336
|
var McpProcessor = class extends FeatureProcessor {
|
|
77854
78337
|
toolTarget;
|
|
77855
78338
|
global;
|
|
78339
|
+
sweepsThroughInRootLinks() {
|
|
78340
|
+
return this.global;
|
|
78341
|
+
}
|
|
77856
78342
|
getFactory;
|
|
77857
78343
|
featureOptions;
|
|
77858
78344
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, getFactory = defaultGetFactory, dryRun = false, logger, featureOptions }) {
|
|
@@ -79905,6 +80391,39 @@ const MATCH_ALL_PATH_GLOBS = /* @__PURE__ */ new Set([
|
|
|
79905
80391
|
]);
|
|
79906
80392
|
const DIRECTORY_CONTENTS_SUFFIX = /\/\*\*(?:\/\*)?$/;
|
|
79907
80393
|
const HOME_OR_VARIABLE_PATH = /^~(?:\/|$)|^\$[A-Za-z_]|\$\{[A-Za-z_]/;
|
|
80394
|
+
const LEADING_HOME = /^~(?:\/|$)/;
|
|
80395
|
+
/**
|
|
80396
|
+
* The home directory as an Antigravity path: forward slashes, no trailing
|
|
80397
|
+
* slash. agy accepts `C:/Users/...` on Windows, so one spelling serves both.
|
|
80398
|
+
*/
|
|
80399
|
+
function toAntigravityHomePath(homeDirectory) {
|
|
80400
|
+
return homeDirectory.replaceAll("\\", "/").replace(/\/+$/, "");
|
|
80401
|
+
}
|
|
80402
|
+
/**
|
|
80403
|
+
* Rewrite a leading `~` or `~/` of a path target to the absolute home
|
|
80404
|
+
* directory. agy 1.5.1 matches `~` literally, so a `~/...` rule would never
|
|
80405
|
+
* apply (rulesync#3324). Only a file the user's own home holds, such as the
|
|
80406
|
+
* global Antigravity CLI settings, may carry a machine-specific path, so the
|
|
80407
|
+
* caller passes `homeDirectory` only then. `$HOME`, `${HOME}`, `%VAR%` and
|
|
80408
|
+
* `~user/` are left as written (the variable forms are still warned about).
|
|
80409
|
+
*/
|
|
80410
|
+
function expandLeadingHome(target, homeDirectory) {
|
|
80411
|
+
if (homeDirectory === void 0 || !LEADING_HOME.test(target)) return target;
|
|
80412
|
+
const home = toAntigravityHomePath(homeDirectory);
|
|
80413
|
+
return target === "~" ? home || "/" : `${home}${target.slice(1)}`;
|
|
80414
|
+
}
|
|
80415
|
+
/**
|
|
80416
|
+
* The reverse of {@link expandLeadingHome} on import: an absolute path in the
|
|
80417
|
+
* home directory goes back to `~/...`, so `.rulesync/permissions.jsonc` stays
|
|
80418
|
+
* the same on every machine.
|
|
80419
|
+
*/
|
|
80420
|
+
function collapseLeadingHome(target, homeDirectory) {
|
|
80421
|
+
if (homeDirectory === void 0) return target;
|
|
80422
|
+
const home = toAntigravityHomePath(homeDirectory);
|
|
80423
|
+
if (home.length === 0) return target;
|
|
80424
|
+
if (target === home) return "~";
|
|
80425
|
+
return target.startsWith(`${home}/`) ? `~${target.slice(home.length)}` : target;
|
|
80426
|
+
}
|
|
79908
80427
|
const HOSTNAME = /^[a-z0-9-]+(?:\.[a-z0-9-]+)*$/;
|
|
79909
80428
|
const DENY_HINTS = {
|
|
79910
80429
|
read_file: "deny a directory or file path instead",
|
|
@@ -79929,12 +80448,30 @@ function toPathTarget(pattern) {
|
|
|
79929
80448
|
"**",
|
|
79930
80449
|
"**/*"
|
|
79931
80450
|
].includes(path)) return { target: "." };
|
|
79932
|
-
const
|
|
80451
|
+
const stripped = path.replace(DIRECTORY_CONTENTS_SUFFIX, "");
|
|
80452
|
+
const directory = stripped === "" && path.startsWith("/") ? "/" : /^[A-Za-z]:$/.test(stripped) && stripped !== path ? `${stripped}/` : stripped;
|
|
79933
80453
|
if (directory !== path && directory.length > 0 && !hasGlob(directory)) return { target: directory };
|
|
79934
80454
|
if (!hasGlob(path)) return { target: path };
|
|
79935
80455
|
return { skipReason: "Antigravity file targets are paths, not globs. Only a plain path, `dir/**` or `*` can be expressed" };
|
|
79936
80456
|
}
|
|
79937
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
|
+
/**
|
|
79938
80475
|
* A URL or domain pattern becomes its hostname. A leading `*.` is dropped,
|
|
79939
80476
|
* since a domain already covers its subdomains, and the URL path is ignored.
|
|
79940
80477
|
* Claude Code's `domain:` form is read as well.
|
|
@@ -80057,13 +80594,14 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
|
|
|
80057
80594
|
* nothing enforces it, and a widened one is warned about too, because it
|
|
80058
80595
|
* blocks more than it says.
|
|
80059
80596
|
*/
|
|
80060
|
-
function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
|
|
80597
|
+
function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel, homeDirectory }) {
|
|
80061
80598
|
const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
|
|
80062
80599
|
const action = isMcp ? "mcp" : toolAction;
|
|
80063
80600
|
const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
|
|
80064
80601
|
const rule = `${category}: { "${pattern}": "${decision}" }`;
|
|
80065
80602
|
if ("skipReason" in result) {
|
|
80066
|
-
const
|
|
80603
|
+
const lead = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
|
|
80604
|
+
const prefix = lead !== void 0 && PATH_ACTIONS.has(action) ? expandLeadingHome(lead, homeDirectory) : lead;
|
|
80067
80605
|
if (prefix !== void 0) {
|
|
80068
80606
|
const { entry, scope } = describeWidenedDeny({
|
|
80069
80607
|
action,
|
|
@@ -80089,22 +80627,23 @@ function buildAntigravityPermissionEntry({ action: toolAction, category, pattern
|
|
|
80089
80627
|
const consequence = decision === "deny" ? ", so the deny also overrides any allow or ask there" : "";
|
|
80090
80628
|
logger.warn(`${toolLabel} permissions: ${rule}: ${result.note}${consequence}.`);
|
|
80091
80629
|
}
|
|
80630
|
+
const target = PATH_ACTIONS.has(action) ? expandLeadingHome(result.target, homeDirectory) : result.target;
|
|
80092
80631
|
warnHomeOrVariablePath({
|
|
80093
80632
|
action,
|
|
80094
|
-
target
|
|
80633
|
+
target,
|
|
80095
80634
|
rule,
|
|
80096
80635
|
decision,
|
|
80097
80636
|
logger,
|
|
80098
80637
|
toolLabel
|
|
80099
80638
|
});
|
|
80100
|
-
return
|
|
80639
|
+
return target === "*" ? action : `${action}(${target})`;
|
|
80101
80640
|
}
|
|
80102
80641
|
/**
|
|
80103
80642
|
* Convert rulesync permissions config to Antigravity allow/ask/deny arrays,
|
|
80104
80643
|
* shared by the CLI and IDE generators; `toAction` maps a canonical category to
|
|
80105
80644
|
* the target's tool or action name.
|
|
80106
80645
|
*/
|
|
80107
|
-
function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel, logger }) {
|
|
80646
|
+
function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel, logger, homeDirectory }) {
|
|
80108
80647
|
const allow = [];
|
|
80109
80648
|
const ask = [];
|
|
80110
80649
|
const deny = [];
|
|
@@ -80117,7 +80656,8 @@ function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel,
|
|
|
80117
80656
|
pattern,
|
|
80118
80657
|
decision: permissionAction,
|
|
80119
80658
|
logger,
|
|
80120
|
-
toolLabel
|
|
80659
|
+
toolLabel,
|
|
80660
|
+
homeDirectory
|
|
80121
80661
|
});
|
|
80122
80662
|
if (entry === void 0) continue;
|
|
80123
80663
|
switch (permissionAction) {
|
|
@@ -80142,25 +80682,27 @@ function convertRulesyncToAntigravityPermissions({ config, toAction, toolLabel,
|
|
|
80142
80682
|
* shared by the CLI and IDE adapters; `toCategory` maps the target's tool or
|
|
80143
80683
|
* action name back to a canonical category.
|
|
80144
80684
|
*/
|
|
80145
|
-
function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory, toolLabel, logger }) {
|
|
80685
|
+
function convertAntigravityToRulesyncPermissions({ allow, ask, deny, toCategory, toolLabel, logger, homeDirectory }) {
|
|
80146
80686
|
const permission = {};
|
|
80147
80687
|
const processEntries = (entries, action) => {
|
|
80148
80688
|
for (const entry of entries) {
|
|
80149
80689
|
const { toolName, pattern: target } = parseToolPatternEntry(entry);
|
|
80150
80690
|
const canonical = toCategory(toolName);
|
|
80151
|
-
const
|
|
80691
|
+
const patterns = ANTIGRAVITY_COMMAND_ACTIONS.has(toolName) ? [importAntigravityCommandTarget({
|
|
80152
80692
|
target,
|
|
80153
80693
|
entry,
|
|
80154
80694
|
decision: action,
|
|
80155
80695
|
logger,
|
|
80156
80696
|
toolLabel
|
|
80157
|
-
}) : target;
|
|
80158
|
-
|
|
80159
|
-
|
|
80160
|
-
|
|
80161
|
-
|
|
80162
|
-
|
|
80163
|
-
|
|
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
|
+
}
|
|
80164
80706
|
}
|
|
80165
80707
|
};
|
|
80166
80708
|
processEntries(allow, "allow");
|
|
@@ -80293,7 +80835,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
80293
80835
|
config,
|
|
80294
80836
|
toAction: toAntigravityCliToolName,
|
|
80295
80837
|
toolLabel: "Antigravity CLI",
|
|
80296
|
-
logger
|
|
80838
|
+
logger,
|
|
80839
|
+
homeDirectory: outputRoot
|
|
80297
80840
|
});
|
|
80298
80841
|
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
|
|
80299
80842
|
const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
|
|
@@ -80344,7 +80887,8 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
80344
80887
|
deny: permissions.deny ?? [],
|
|
80345
80888
|
toCategory: toCanonicalToolName$9,
|
|
80346
80889
|
toolLabel: "Antigravity CLI",
|
|
80347
|
-
logger: moduleLogger$4
|
|
80890
|
+
logger: moduleLogger$4,
|
|
80891
|
+
homeDirectory: this.getOutputRoot()
|
|
80348
80892
|
});
|
|
80349
80893
|
const override = {};
|
|
80350
80894
|
const toolPermission = pickDocumentedValue({
|
|
@@ -80929,6 +81473,10 @@ var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermission
|
|
|
80929
81473
|
* in global mode it is dropped with a warning instead of written somewhere it
|
|
80930
81474
|
* does nothing.
|
|
80931
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
|
+
*
|
|
80932
81480
|
* Both make Auggie load code the repository did not ship — a recommended
|
|
80933
81481
|
* marketplace is one prompt away from being cloned, and an enabled plugin can
|
|
80934
81482
|
* carry its own hooks and MCP servers — so writing either is logged, unless
|
|
@@ -80948,7 +81496,7 @@ function buildPluginConsumptionPatch({ override, existing, global, logger }) {
|
|
|
80948
81496
|
else {
|
|
80949
81497
|
patch.recommendedMarketplaces = recommendedMarketplaces;
|
|
80950
81498
|
const existingMarketplaces = existingKeys.recommendedMarketplaces;
|
|
80951
|
-
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).`);
|
|
80952
81500
|
warnAboutRemovedPluginIds({
|
|
80953
81501
|
key: "recommendedMarketplaces",
|
|
80954
81502
|
removed: (existingMarketplaces ?? []).filter((marketplace) => !recommendedMarketplaces.includes(marketplace)),
|
|
@@ -81177,12 +81725,12 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
81177
81725
|
const regex = entry.shellInputRegex;
|
|
81178
81726
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
81179
81727
|
else {
|
|
81180
|
-
if (action
|
|
81181
|
-
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.`);
|
|
81182
81730
|
continue;
|
|
81183
81731
|
}
|
|
81184
|
-
if (action
|
|
81185
|
-
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.`);
|
|
81186
81734
|
pattern = "*";
|
|
81187
81735
|
} else {
|
|
81188
81736
|
pattern = shellRegexToGlob(regex);
|
|
@@ -81268,6 +81816,62 @@ function classifyPattern(pattern, bareStarIsWildcard) {
|
|
|
81268
81816
|
};
|
|
81269
81817
|
return { kind: "literal" };
|
|
81270
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
|
+
}
|
|
81271
81875
|
function pluralize(count, singular, plural) {
|
|
81272
81876
|
return count === 1 ? singular : plural;
|
|
81273
81877
|
}
|
|
@@ -81362,8 +81966,16 @@ function buildVscodeCommandLists({ rules, toolLabel, logger, prefixSemantics = "
|
|
|
81362
81966
|
const matcherAllows = [];
|
|
81363
81967
|
const widenedDenies = [];
|
|
81364
81968
|
const inertDenies = [];
|
|
81969
|
+
const literalDenyStems = collectLiteralDenyStems({
|
|
81970
|
+
rules,
|
|
81971
|
+
bareStarIsWildcard: isRooLineage
|
|
81972
|
+
});
|
|
81365
81973
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
81366
81974
|
if (action !== "allow" && action !== "deny") continue;
|
|
81975
|
+
if (action === "deny" && isCoveredByLiteralDeny({
|
|
81976
|
+
pattern,
|
|
81977
|
+
literalDenyStems
|
|
81978
|
+
})) continue;
|
|
81367
81979
|
const shape = classifyPattern(pattern, isRooLineage);
|
|
81368
81980
|
if (action === "allow") {
|
|
81369
81981
|
allowed.push(pattern);
|
|
@@ -81726,16 +82338,20 @@ var BobPermissions = class BobPermissions extends ToolPermissions {
|
|
|
81726
82338
|
validate: true
|
|
81727
82339
|
});
|
|
81728
82340
|
}
|
|
81729
|
-
toRulesyncPermissions() {
|
|
82341
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
81730
82342
|
const settings = parseBobSettings({
|
|
81731
82343
|
fileContent: this.getFileContent(),
|
|
81732
82344
|
filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())
|
|
81733
82345
|
});
|
|
81734
82346
|
const approval = settings[APPROVAL_KEY$1];
|
|
81735
82347
|
const entry = (isPlainObject(approval) && Array.isArray(approval.allowedExecutors) ? approval.allowedExecutors : []).find(isExecuteCommandEntry);
|
|
81736
|
-
const rules = {
|
|
81737
|
-
|
|
81738
|
-
|
|
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
|
+
});
|
|
81739
82355
|
const permission = {};
|
|
81740
82356
|
if (Object.keys(rules).length > 0) permission[SHELL_PERMISSION_CATEGORY] = rules;
|
|
81741
82357
|
const bob = readBobOverride(settings);
|
|
@@ -85499,7 +86115,7 @@ function computeCodexcliOverridePatch({ existing, override, logger }) {
|
|
|
85499
86115
|
const patch = {};
|
|
85500
86116
|
const allowed = new Set(CODEXCLI_OVERRIDE_KEYS);
|
|
85501
86117
|
for (const [key, value] of Object.entries(override ?? {})) {
|
|
85502
|
-
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;
|
|
85503
86119
|
if (!allowed.has(key)) {
|
|
85504
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.`);
|
|
85505
86121
|
continue;
|
|
@@ -85788,6 +86404,7 @@ function stripTrailingSlash(pattern) {
|
|
|
85788
86404
|
function buildCodexBashRulesContent({ config, logger }) {
|
|
85789
86405
|
const bashRules = bashRulesHonoringAllTools(config.permission);
|
|
85790
86406
|
const entries = Object.entries(bashRules);
|
|
86407
|
+
const canonicalMatching = config.codexcli?.bash_canonical_matching === true;
|
|
85791
86408
|
const header = ["# Generated by Rulesync from .rulesync/permissions.jsonc (permission.bash)", "# https://developers.openai.com/codex/rules"];
|
|
85792
86409
|
if (entries.length === 0) return [...header, "# No bash permission rules were configured."].join("\n");
|
|
85793
86410
|
const ruleBlocks = entries.map(([pattern, action]) => {
|
|
@@ -85806,6 +86423,7 @@ function buildCodexBashRulesContent({ config, logger }) {
|
|
|
85806
86423
|
` pattern = [${serializedTokens}],`,
|
|
85807
86424
|
` decision = ${JSON.stringify(decision)},`,
|
|
85808
86425
|
` justification = ${JSON.stringify(`Generated from Rulesync permission.bash: ${pattern}`)},`,
|
|
86426
|
+
...canonicalMatching && decision !== "allow" ? [" canonical_matching = True,"] : [],
|
|
85809
86427
|
")"
|
|
85810
86428
|
].join("\n");
|
|
85811
86429
|
}).filter((block) => block !== null);
|
|
@@ -87390,9 +88008,126 @@ function toCanonicalToolName$5(crushName) {
|
|
|
87390
88008
|
function isMcpToolName(crushName) {
|
|
87391
88009
|
return crushName.startsWith(MCP_CRUSH_PREFIX) && crushName.length > 4;
|
|
87392
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
|
+
}
|
|
87393
88122
|
function uniq$11(values) {
|
|
87394
88123
|
return [...new Set(values)];
|
|
87395
88124
|
}
|
|
88125
|
+
function stringList(value) {
|
|
88126
|
+
return isStringArray(value) ? value : [];
|
|
88127
|
+
}
|
|
88128
|
+
function listOrUndefined(values) {
|
|
88129
|
+
return values.length > 0 ? values : void 0;
|
|
88130
|
+
}
|
|
87396
88131
|
/**
|
|
87397
88132
|
* Permissions adapter for Crush.
|
|
87398
88133
|
*
|
|
@@ -87414,8 +88149,24 @@ function uniq$11(values) {
|
|
|
87414
88149
|
* `webfetch` -> `fetch`, `mcp__<server>__<tool>` -> `mcp_<server>_<tool>`;
|
|
87415
88150
|
* any other category passes through verbatim.
|
|
87416
88151
|
*
|
|
87417
|
-
*
|
|
87418
|
-
*
|
|
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`
|
|
87419
88170
|
* is not written while the same category carries a pattern-specific `deny` or
|
|
87420
88171
|
* `ask`: the tool keeps prompting (fail closed) rather than being widened to
|
|
87421
88172
|
* everything. The all-tools `*` category is mirrored onto `bash` by
|
|
@@ -87479,7 +88230,12 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87479
88230
|
const existingContent = location.fileContent ?? "";
|
|
87480
88231
|
warnCrushTwinLeftovers({
|
|
87481
88232
|
location,
|
|
87482
|
-
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
|
+
],
|
|
87483
88239
|
logger
|
|
87484
88240
|
});
|
|
87485
88241
|
const existing = parseCrushConfig(existingContent, location.filePath);
|
|
@@ -87491,12 +88247,22 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87491
88247
|
logger
|
|
87492
88248
|
});
|
|
87493
88249
|
const managedToolNames = managedCrushToolNames(config);
|
|
88250
|
+
const commands = rebuildCrushCommandLists({
|
|
88251
|
+
config,
|
|
88252
|
+
existingPermissions,
|
|
88253
|
+
logger
|
|
88254
|
+
});
|
|
87494
88255
|
const preservedAllowed = (isStringArray(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
|
|
87495
88256
|
const preservedDisabled = (isStringArray(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
|
|
87496
88257
|
const allowedList = uniq$11([...preservedAllowed, ...generated.allowed]);
|
|
87497
88258
|
const disabledList = uniq$11([...preservedDisabled, ...generated.disabled]);
|
|
88259
|
+
const permissionsHasContent = allowedList.length > 0 || commands.safe.length > 0 || commands.blocked.length > 0;
|
|
87498
88260
|
const patch = {};
|
|
87499
|
-
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
|
+
};
|
|
87500
88266
|
if (disabledList.length > 0 || existingOptions !== void 0) patch[CRUSH_OPTIONS_KEY] = { [CRUSH_DISABLED_TOOLS_KEY]: disabledList.length > 0 ? disabledList : void 0 };
|
|
87501
88267
|
return new CrushPermissions({
|
|
87502
88268
|
outputRoot,
|
|
@@ -87514,17 +88280,79 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
|
|
|
87514
88280
|
global
|
|
87515
88281
|
});
|
|
87516
88282
|
}
|
|
87517
|
-
toRulesyncPermissions() {
|
|
88283
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
87518
88284
|
const permissions = isRecord(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
|
|
87519
88285
|
const options = toRecord(this.json[CRUSH_OPTIONS_KEY]);
|
|
87520
88286
|
const rulesyncConfig = convertCrushListsToRulesync({
|
|
87521
|
-
allowed:
|
|
87522
|
-
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
|
|
87523
88292
|
});
|
|
87524
88293
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
87525
88294
|
}
|
|
87526
88295
|
};
|
|
87527
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
|
+
/**
|
|
87528
88356
|
* The Crush tool names the canonical config manages, i.e. the names its
|
|
87529
88357
|
* categories map to. Entries naming any other tool are the user's and survive
|
|
87530
88358
|
* a generate. The all-tools `*` category names no Crush tool of its own.
|
|
@@ -87549,7 +88377,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
87549
88377
|
catchAllAction = action;
|
|
87550
88378
|
continue;
|
|
87551
88379
|
}
|
|
87552
|
-
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.`);
|
|
87553
88381
|
if (action !== "allow") restricted = true;
|
|
87554
88382
|
}
|
|
87555
88383
|
if (catchAllAction === "allow" && restricted) {
|
|
@@ -87600,11 +88428,71 @@ function convertRulesyncToCrushLists({ config, logger }) {
|
|
|
87600
88428
|
};
|
|
87601
88429
|
}
|
|
87602
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
|
+
/**
|
|
87603
88491
|
* Convert Crush's two tool lists back into a rulesync config. A `tool:action`
|
|
87604
88492
|
* entry scopes an allow to one action of a tool, which the canonical model
|
|
87605
88493
|
* cannot express, so it is left out rather than widened to the whole tool.
|
|
87606
88494
|
*/
|
|
87607
|
-
function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
88495
|
+
function convertCrushListsToRulesync({ allowed, disabled, safe, blocked, logger }) {
|
|
87608
88496
|
const permission = {};
|
|
87609
88497
|
const bucketFor = (category) => {
|
|
87610
88498
|
const own = lookupOwn({
|
|
@@ -87620,6 +88508,29 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
87620
88508
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
87621
88509
|
bucketFor(toCanonicalToolName$5(entry))["*"] = "deny";
|
|
87622
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
|
+
}
|
|
87623
88534
|
for (const entry of allowed) {
|
|
87624
88535
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
87625
88536
|
bucketFor(toCanonicalToolName$5(entry))["*"] ??= "allow";
|
|
@@ -88999,8 +89910,8 @@ const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
|
88999
89910
|
* In global mode the config file is shared with the hooks (`hooks`) feature
|
|
89000
89911
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
89001
89912
|
* merge into the existing JSON and the file is never deleted; only the keys
|
|
89002
|
-
* this feature manages are rewritten — `permissions`, plus `sandbox`
|
|
89003
|
-
* 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` /
|
|
89004
89915
|
* `forbiddenPlugins`) in project mode when the `devin` override authors them.
|
|
89005
89916
|
*
|
|
89006
89917
|
* The sibling `sandbox` block — which decides what a permitted command may
|
|
@@ -89108,6 +90019,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89108
90019
|
});
|
|
89109
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.");
|
|
89110
90021
|
}
|
|
90022
|
+
applyPermissionModeOverride({
|
|
90023
|
+
permissionMode: config.devin?.permission_mode,
|
|
90024
|
+
patch,
|
|
90025
|
+
global,
|
|
90026
|
+
logger
|
|
90027
|
+
});
|
|
89111
90028
|
applyPluginGovernanceOverride({
|
|
89112
90029
|
override: config.devin,
|
|
89113
90030
|
settings,
|
|
@@ -89151,6 +90068,12 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
89151
90068
|
if (DEVIN_PLUGIN_GOVERNANCE_SCHEMAS[key].safeParse(value).success) devinOverride[key] = value;
|
|
89152
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)"}.`);
|
|
89153
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;
|
|
89154
90077
|
const result = { ...config };
|
|
89155
90078
|
if (Object.keys(devinOverride).length > 0) result.devin = devinOverride;
|
|
89156
90079
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
@@ -89167,6 +90090,48 @@ const DEVIN_PLUGIN_GOVERNANCE_KEYS = [
|
|
|
89167
90090
|
"forbiddenPlugins"
|
|
89168
90091
|
];
|
|
89169
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
|
+
/**
|
|
89170
90135
|
* Patch the plugin governance lists the `devin` override authors into the
|
|
89171
90136
|
* project `.devin/config.json`. Each authored list replaces the file's list
|
|
89172
90137
|
* whole; a list it does not author is left as the file holds it. Devin reads these
|
|
@@ -95156,7 +96121,8 @@ function convertQwenToRulesyncPermissions(params) {
|
|
|
95156
96121
|
*
|
|
95157
96122
|
* Reasonix matches a rule's tool exactly against its registered tools, which
|
|
95158
96123
|
* are snake_case (`read_file`, `write_file`, `grep`, `glob`, `web_fetch`,
|
|
95159
|
-
* `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
|
|
95160
96126
|
* writer: `write_file`, `edit_file`, `multi_edit`, `notebook_edit`, ...),
|
|
95161
96127
|
* `Browser` and `Computer` are Claude-Code-style family aliases
|
|
95162
96128
|
* (`canonicalRuleTool` / `groupOf`), so a `Read(...)` or `Write(...)` rule
|
|
@@ -95179,6 +96145,21 @@ const CANONICAL_TO_REASONIX_TOOL_NAMES = {
|
|
|
95179
96145
|
agent: "task"
|
|
95180
96146
|
};
|
|
95181
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
|
+
/**
|
|
95182
96163
|
* The Claude-Code-style spellings rulesync emitted before it followed the
|
|
95183
96164
|
* Reasonix tool vocabulary. None of them reaches a Reasonix tool, so they are
|
|
95184
96165
|
* read back on import (a file generated by an older rulesync still imports)
|
|
@@ -95201,7 +96182,8 @@ const CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES = {
|
|
|
95201
96182
|
*/
|
|
95202
96183
|
const REASONIX_TO_CANONICAL_TOOL_NAMES = {
|
|
95203
96184
|
...invertRecord(CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES),
|
|
95204
|
-
...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])))
|
|
95205
96187
|
};
|
|
95206
96188
|
/** Every Reasonix tool name a canonical category owns on generate. */
|
|
95207
96189
|
function reasonixToolNamesOf(canonical) {
|
|
@@ -95209,14 +96191,20 @@ function reasonixToolNamesOf(canonical) {
|
|
|
95209
96191
|
record: CANONICAL_TO_LEGACY_REASONIX_TOOL_NAMES,
|
|
95210
96192
|
key: canonical
|
|
95211
96193
|
});
|
|
95212
|
-
const current =
|
|
95213
|
-
return legacy === void 0 ?
|
|
96194
|
+
const current = toReasonixToolNames(canonical);
|
|
96195
|
+
return legacy === void 0 ? current : [...current, legacy];
|
|
95214
96196
|
}
|
|
95215
|
-
|
|
95216
|
-
|
|
96197
|
+
/** The Reasonix tool names a canonical category's rules are written under. */
|
|
96198
|
+
function toReasonixToolNames(canonical) {
|
|
96199
|
+
const primary = lookupOwn({
|
|
95217
96200
|
record: CANONICAL_TO_REASONIX_TOOL_NAMES,
|
|
95218
96201
|
key: canonical
|
|
95219
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];
|
|
95220
96208
|
}
|
|
95221
96209
|
function toCanonicalToolName$2(reasonixName) {
|
|
95222
96210
|
return lookupOwn({
|
|
@@ -95624,6 +96612,17 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
95624
96612
|
});
|
|
95625
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.`);
|
|
95626
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
|
+
});
|
|
95627
96626
|
const mergedPermissions = { ...existingPermissions };
|
|
95628
96627
|
setOrDeleteEntries(mergedPermissions, "allow", [
|
|
95629
96628
|
...preservedAllow,
|
|
@@ -95799,6 +96798,55 @@ function preserveExistingEntries({ existingPermissions, managedToolNames, rawOwn
|
|
|
95799
96798
|
deny
|
|
95800
96799
|
};
|
|
95801
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
|
+
}
|
|
95802
96850
|
/** Sort, dedupe and set a permissions array key, deleting it when empty. */
|
|
95803
96851
|
function setOrDeleteEntries(table, key, entries) {
|
|
95804
96852
|
const merged = (0, es_toolkit.uniq)(entries.toSorted());
|
|
@@ -95812,22 +96860,19 @@ function convertRulesyncToReasonixPermissions(config) {
|
|
|
95812
96860
|
const allow = [];
|
|
95813
96861
|
const ask = [];
|
|
95814
96862
|
const deny = [];
|
|
95815
|
-
for (const [category, rules] of Object.entries(config.permission)) {
|
|
95816
|
-
const
|
|
95817
|
-
|
|
95818
|
-
|
|
95819
|
-
|
|
95820
|
-
|
|
95821
|
-
|
|
95822
|
-
|
|
95823
|
-
|
|
95824
|
-
|
|
95825
|
-
|
|
95826
|
-
|
|
95827
|
-
|
|
95828
|
-
break;
|
|
95829
|
-
case "deny": deny.push(entry);
|
|
95830
|
-
}
|
|
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);
|
|
95831
96876
|
}
|
|
95832
96877
|
}
|
|
95833
96878
|
return {
|
|
@@ -96054,7 +97099,7 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96054
97099
|
validate: true
|
|
96055
97100
|
});
|
|
96056
97101
|
}
|
|
96057
|
-
toRulesyncPermissions() {
|
|
97102
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
96058
97103
|
const constructor = this.constructor;
|
|
96059
97104
|
let settings;
|
|
96060
97105
|
try {
|
|
@@ -96068,9 +97113,12 @@ var RooPermissions = class extends ToolPermissions {
|
|
|
96068
97113
|
} catch (error) {
|
|
96069
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 });
|
|
96070
97115
|
}
|
|
96071
|
-
const rules = {
|
|
96072
|
-
|
|
96073
|
-
|
|
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
|
+
});
|
|
96074
97122
|
const permission = {};
|
|
96075
97123
|
if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
|
|
96076
97124
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
@@ -101394,6 +102442,9 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
101394
102442
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
101395
102443
|
toolTarget;
|
|
101396
102444
|
global;
|
|
102445
|
+
sweepsThroughInRootLinks() {
|
|
102446
|
+
return this.global;
|
|
102447
|
+
}
|
|
101397
102448
|
constructor({ outputRoot = process.cwd(), inputRoots, toolTarget, global = false, dryRun = false, logger }) {
|
|
101398
102449
|
super({
|
|
101399
102450
|
outputRoot,
|
|
@@ -104637,6 +105688,12 @@ Object.defineProperty(exports, "assertDirectoryIfExists", {
|
|
|
104637
105688
|
return assertDirectoryIfExists;
|
|
104638
105689
|
}
|
|
104639
105690
|
});
|
|
105691
|
+
Object.defineProperty(exports, "assertLandingOutsideVcsMetadata", {
|
|
105692
|
+
enumerable: true,
|
|
105693
|
+
get: function() {
|
|
105694
|
+
return assertLandingOutsideVcsMetadata;
|
|
105695
|
+
}
|
|
105696
|
+
});
|
|
104640
105697
|
Object.defineProperty(exports, "assertNoSymlinkBelowRoot", {
|
|
104641
105698
|
enumerable: true,
|
|
104642
105699
|
get: function() {
|
|
@@ -104679,6 +105736,12 @@ Object.defineProperty(exports, "checkPathTraversal", {
|
|
|
104679
105736
|
return checkPathTraversal;
|
|
104680
105737
|
}
|
|
104681
105738
|
});
|
|
105739
|
+
Object.defineProperty(exports, "classifyRemoteName", {
|
|
105740
|
+
enumerable: true,
|
|
105741
|
+
get: function() {
|
|
105742
|
+
return classifyRemoteName;
|
|
105743
|
+
}
|
|
105744
|
+
});
|
|
104682
105745
|
Object.defineProperty(exports, "convertFromTool", {
|
|
104683
105746
|
enumerable: true,
|
|
104684
105747
|
get: function() {
|
|
@@ -104811,16 +105874,16 @@ Object.defineProperty(exports, "groupSpellingsByCaseFoldedIdentity", {
|
|
|
104811
105874
|
return groupSpellingsByCaseFoldedIdentity;
|
|
104812
105875
|
}
|
|
104813
105876
|
});
|
|
104814
|
-
Object.defineProperty(exports, "
|
|
105877
|
+
Object.defineProperty(exports, "hasEnclosingMarkOutsideKeycap", {
|
|
104815
105878
|
enumerable: true,
|
|
104816
105879
|
get: function() {
|
|
104817
|
-
return
|
|
105880
|
+
return hasEnclosingMarkOutsideKeycap;
|
|
104818
105881
|
}
|
|
104819
105882
|
});
|
|
104820
|
-
Object.defineProperty(exports, "
|
|
105883
|
+
Object.defineProperty(exports, "hasWindowsFoldableName", {
|
|
104821
105884
|
enumerable: true,
|
|
104822
105885
|
get: function() {
|
|
104823
|
-
return
|
|
105886
|
+
return hasWindowsFoldableName;
|
|
104824
105887
|
}
|
|
104825
105888
|
});
|
|
104826
105889
|
Object.defineProperty(exports, "importFromTool", {
|
|
@@ -105069,6 +106132,12 @@ Object.defineProperty(exports, "stripHiddenCharacters", {
|
|
|
105069
106132
|
return stripHiddenCharacters;
|
|
105070
106133
|
}
|
|
105071
106134
|
});
|
|
106135
|
+
Object.defineProperty(exports, "sweepRefusalReason", {
|
|
106136
|
+
enumerable: true,
|
|
106137
|
+
get: function() {
|
|
106138
|
+
return sweepRefusalReason;
|
|
106139
|
+
}
|
|
106140
|
+
});
|
|
105072
106141
|
Object.defineProperty(exports, "toInstructionBudgetFile", {
|
|
105073
106142
|
enumerable: true,
|
|
105074
106143
|
get: function() {
|