rulesync 16.13.0 → 16.15.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/README.md +1 -1
- package/dist/cli/index.cjs +7 -20
- package/dist/cli/index.js +5 -18
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-Dswkc7Ub.js → import-BPTCMtUS.js} +841 -49
- package/dist/import-BPTCMtUS.js.map +1 -0
- package/dist/{import-ChnrtxiJ.cjs → import-nfDFI5X1.cjs} +860 -62
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/package.json +15 -15
- package/dist/import-Dswkc7Ub.js.map +0 -1
|
@@ -134,27 +134,27 @@ const isFeatureValueEnabled = (value) => {
|
|
|
134
134
|
};
|
|
135
135
|
//#endregion
|
|
136
136
|
//#region src/constants/rulesync-paths.ts
|
|
137
|
-
const { join: join$
|
|
137
|
+
const { join: join$290 } = node_path.posix;
|
|
138
138
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
139
139
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
140
140
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
141
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
142
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
143
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
144
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
145
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
146
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
147
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
148
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
149
|
-
join$
|
|
150
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
151
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
141
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "rules");
|
|
142
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$290(RULESYNC_RULES_RELATIVE_DIR_PATH, ".curated");
|
|
143
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "commands");
|
|
144
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "subagents");
|
|
145
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "checks");
|
|
146
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
147
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
148
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
149
|
+
join$290(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
150
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
151
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
152
152
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
153
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
153
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
154
154
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
155
155
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
156
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
157
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
156
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$290(RULESYNC_RELATIVE_DIR_PATH, "skills");
|
|
157
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$290(RULESYNC_SKILLS_RELATIVE_DIR_PATH, ".curated");
|
|
158
158
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
159
159
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
160
160
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -444,7 +444,8 @@ const permissionsProcessorToolTargetTuple = [
|
|
|
444
444
|
"takt",
|
|
445
445
|
"vibe",
|
|
446
446
|
"warp",
|
|
447
|
-
"zed"
|
|
447
|
+
"zed",
|
|
448
|
+
"zoocode"
|
|
448
449
|
];
|
|
449
450
|
const checksProcessorToolTargetTuple = [
|
|
450
451
|
"amp",
|
|
@@ -4421,11 +4422,26 @@ const KiloPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
4421
4422
|
* rather than which commands are permitted — so it is a loose passthrough on
|
|
4422
4423
|
* the same terms, merged into the top level of `.claude/settings.json`.
|
|
4423
4424
|
*
|
|
4425
|
+
* Any other key is a plain top-level `settings.json` key (`editorMode`, `env`,
|
|
4426
|
+
* `model`, ...), deep-merged into the generated file verbatim so settings
|
|
4427
|
+
* Claude Code adds faster than an allowlist can track stay authorable.
|
|
4428
|
+
* The exceptions are the keys another feature owns (`hooks`) and `$schema`.
|
|
4429
|
+
* Keys the target file cannot honor — `Managed`-only and `~/.claude.json`-only
|
|
4430
|
+
* keys in either scope, plus user-scope keys at project scope — are dropped
|
|
4431
|
+
* with a warning rather than written where they would never apply. So are the
|
|
4432
|
+
* keys whose value is a command Claude Code executes (`apiKeyHelper`,
|
|
4433
|
+
* `statusLine`, ...): this file is shareable via `rulesync fetch`, and a file
|
|
4434
|
+
* named for restricting things is not where a command belongs — author those
|
|
4435
|
+
* in `.rulesync/hooks.jsonc` instead.
|
|
4436
|
+
*
|
|
4424
4437
|
* @example
|
|
4425
4438
|
* { "permissions": { "defaultMode": "acceptEdits", "additionalDirectories": ["../shared"] } }
|
|
4426
4439
|
* @example
|
|
4427
4440
|
* { "sandbox": { "network": { "allowedDomains": ["example.com"], "strictAllowlist": true } } }
|
|
4441
|
+
* @example
|
|
4442
|
+
* { "editorMode": "vim", "env": { "MY_VAR": "1" } }
|
|
4428
4443
|
* @see https://code.claude.com/docs/en/sandboxing
|
|
4444
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
4429
4445
|
*/
|
|
4430
4446
|
const ClaudecodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
4431
4447
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
@@ -5100,6 +5116,15 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
5100
5116
|
sandbox_workspace_write: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
5101
5117
|
apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
5102
5118
|
approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})])),
|
|
5119
|
+
/**
|
|
5120
|
+
* The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
|
|
5121
|
+
* Not a permission surface, but like `apps` it is a top-level table with no
|
|
5122
|
+
* canonical category, and Codex adds keys to it faster than an explicit model
|
|
5123
|
+
* could track — so it is a loose passthrough written verbatim.
|
|
5124
|
+
*
|
|
5125
|
+
* @see https://developers.openai.com/codex/config-reference
|
|
5126
|
+
*/
|
|
5127
|
+
tui: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
5103
5128
|
git_write_rules: zod_mini.z.optional(zod_mini.z.boolean())
|
|
5104
5129
|
});
|
|
5105
5130
|
/**
|
|
@@ -5370,6 +5395,7 @@ const RulesyncRuleFrontmatterSchema = zod_mini.z.object({
|
|
|
5370
5395
|
systemPrompt: zod_mini.z.optional(zod_mini.z.enum(["append"])),
|
|
5371
5396
|
contextFile: zod_mini.z.optional(zod_mini.z.enum(["override"]))
|
|
5372
5397
|
})),
|
|
5398
|
+
roo: zod_mini.z.optional(zod_mini.z.looseObject({ mode: zod_mini.z.optional(zod_mini.z.string()) })),
|
|
5373
5399
|
takt: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
5374
5400
|
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
5375
5401
|
extends: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -7486,7 +7512,8 @@ const CODEXCLI_OVERRIDE_KEYS = [
|
|
|
7486
7512
|
"sandbox_mode",
|
|
7487
7513
|
"sandbox_workspace_write",
|
|
7488
7514
|
"apps",
|
|
7489
|
-
"approvals_reviewer"
|
|
7515
|
+
"approvals_reviewer",
|
|
7516
|
+
"tui"
|
|
7490
7517
|
];
|
|
7491
7518
|
//#endregion
|
|
7492
7519
|
//#region src/features/shared/shared-config-gateway.ts
|
|
@@ -7811,7 +7838,9 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
7811
7838
|
ownedKeys: [
|
|
7812
7839
|
"chat.tools.terminal.autoApprove",
|
|
7813
7840
|
"chat.tools.edits.autoApprove",
|
|
7814
|
-
"chat.tools.urls.autoApprove"
|
|
7841
|
+
"chat.tools.urls.autoApprove",
|
|
7842
|
+
"zoo-code.allowedCommands",
|
|
7843
|
+
"zoo-code.deniedCommands"
|
|
7815
7844
|
]
|
|
7816
7845
|
} }
|
|
7817
7846
|
},
|
|
@@ -12775,6 +12804,21 @@ const ROO_DIR = ".roo";
|
|
|
12775
12804
|
const ROO_COMMANDS_DIR_PATH = (0, node_path.join)(ROO_DIR, "commands");
|
|
12776
12805
|
const ROO_SKILLS_DIR_PATH = (0, node_path.join)(ROO_DIR, "skills");
|
|
12777
12806
|
const ROO_MCP_FILE_NAME = "mcp.json";
|
|
12807
|
+
/**
|
|
12808
|
+
* Mode slugs Roo/Zoo Code themselves accept for a `rules-{mode}` directory:
|
|
12809
|
+
* the loader builds the directory name by interpolating the active mode slug,
|
|
12810
|
+
* and custom-mode slugs are restricted to this alphabet. Validating against it
|
|
12811
|
+
* also keeps an authored value from escaping `.roo/` through path separators or
|
|
12812
|
+
* `..` segments.
|
|
12813
|
+
*/
|
|
12814
|
+
const ROO_MODE_SLUG_PATTERN = /^[a-zA-Z0-9-]+$/;
|
|
12815
|
+
/**
|
|
12816
|
+
* `.roo/rules-{mode}/` — the mode-specific rule directory Roo/Zoo Code load
|
|
12817
|
+
* INSTEAD of `.roo/rules/` while that mode is active. The relative path is the
|
|
12818
|
+
* same in global scope, where it resolves under `~/.roo/`.
|
|
12819
|
+
* @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/prompts/sections/custom-instructions.ts
|
|
12820
|
+
*/
|
|
12821
|
+
const rooModeRulesDirName = (mode) => `rules-${mode}`;
|
|
12778
12822
|
const ROO_IGNORE_FILE_NAME = ".rooignore";
|
|
12779
12823
|
/**
|
|
12780
12824
|
* Roo Code reads project-level custom modes from a single aggregated
|
|
@@ -27800,6 +27844,27 @@ const defaultGetFactory$3 = (target) => {
|
|
|
27800
27844
|
if (!factory) throw new Error(`Unsupported tool target: ${target}`);
|
|
27801
27845
|
return factory;
|
|
27802
27846
|
};
|
|
27847
|
+
/**
|
|
27848
|
+
* Warn about per-server tool filters the target tool cannot express.
|
|
27849
|
+
*
|
|
27850
|
+
* `enabledTools`/`disabledTools` are canonical fields, so a single
|
|
27851
|
+
* `.rulesync/.mcp.json` can carry a filter that only some targets read. The
|
|
27852
|
+
* unsupported ones are stripped before generation (writing them would produce a
|
|
27853
|
+
* key the tool discards on load), which used to happen silently — the filter
|
|
27854
|
+
* simply did not apply and nothing said so. Warning names the servers whose
|
|
27855
|
+
* filter is being dropped for this target so the gap is visible at generate
|
|
27856
|
+
* time rather than in the tool's behavior.
|
|
27857
|
+
*
|
|
27858
|
+
* Only fields actually present are reported, so a config that never authored
|
|
27859
|
+
* the filter stays quiet.
|
|
27860
|
+
*/
|
|
27861
|
+
function warnStrippedMcpServerFields({ mcpServers, fields, toolTarget, logger }) {
|
|
27862
|
+
for (const field of fields) {
|
|
27863
|
+
const serverNames = Object.entries(mcpServers ?? {}).filter(([, serverConfig]) => serverConfig[field] !== void 0).map(([serverName]) => serverName);
|
|
27864
|
+
if (serverNames.length === 0) continue;
|
|
27865
|
+
logger.warn(`${toolTarget} does not read the per-server \`${field}\` MCP tool filter; dropping it from ${serverNames.join(", ")}.`);
|
|
27866
|
+
}
|
|
27867
|
+
}
|
|
27803
27868
|
var McpProcessor = class extends FeatureProcessor {
|
|
27804
27869
|
toolTarget;
|
|
27805
27870
|
global;
|
|
@@ -27879,6 +27944,12 @@ var McpProcessor = class extends FeatureProcessor {
|
|
|
27879
27944
|
const fieldsToStrip = [];
|
|
27880
27945
|
if (!factory.meta.supportsEnabledTools) fieldsToStrip.push("enabledTools");
|
|
27881
27946
|
if (!factory.meta.supportsDisabledTools) fieldsToStrip.push("disabledTools");
|
|
27947
|
+
warnStrippedMcpServerFields({
|
|
27948
|
+
mcpServers: targetedRulesyncMcp.getJson().mcpServers,
|
|
27949
|
+
fields: fieldsToStrip,
|
|
27950
|
+
toolTarget: this.toolTarget,
|
|
27951
|
+
logger: this.logger
|
|
27952
|
+
});
|
|
27882
27953
|
const filteredRulesyncMcp = targetedRulesyncMcp.stripMcpServerFields(fieldsToStrip);
|
|
27883
27954
|
return await factory.class.fromRulesyncMcp({
|
|
27884
27955
|
outputRoot: this.outputRoot,
|
|
@@ -29359,6 +29430,27 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
29359
29430
|
return { permission };
|
|
29360
29431
|
}
|
|
29361
29432
|
//#endregion
|
|
29433
|
+
//#region src/utils/control-characters.ts
|
|
29434
|
+
/**
|
|
29435
|
+
* Matches C0 controls, DEL, the C1 range (which includes the 8-bit CSI
|
|
29436
|
+
* introducer U+009B), the bidirectional overrides and isolates, and the Unicode
|
|
29437
|
+
* line and paragraph separators, and the plain LRM/RLM marks. A name or value
|
|
29438
|
+
* copied out of an untrusted config file, a fetched repository, or a tool's own
|
|
29439
|
+
* settings file must never reach the terminal with these intact: they let the
|
|
29440
|
+
* text forge log lines, reorder what is printed around them, or inject escape
|
|
29441
|
+
* sequences. LRM/RLM open no bidi scope of their own, but they still reorder the
|
|
29442
|
+
* neutral characters beside them, so they go too — a diagnostic line is not the
|
|
29443
|
+
* place to preserve the typography of a right-to-left name.
|
|
29444
|
+
*/
|
|
29445
|
+
const CONTROL_CHARACTERS_PATTERN = /[\u0000-\u001f\u007f-\u009f\u200e\u200f\u202a-\u202e\u2066-\u2069\u2028\u2029]/g;
|
|
29446
|
+
/**
|
|
29447
|
+
* Removes every control character from `text` so it is safe to splice into a
|
|
29448
|
+
* log line or other terminal output.
|
|
29449
|
+
*/
|
|
29450
|
+
function stripControlCharacters(text) {
|
|
29451
|
+
return text.replace(CONTROL_CHARACTERS_PATTERN, "");
|
|
29452
|
+
}
|
|
29453
|
+
//#endregion
|
|
29362
29454
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
29363
29455
|
/**
|
|
29364
29456
|
* Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
|
|
@@ -29443,8 +29535,9 @@ function deepMergeRecords(base, patch) {
|
|
|
29443
29535
|
* emits them only under `--global`.
|
|
29444
29536
|
*
|
|
29445
29537
|
* Deliberately NOT listed:
|
|
29446
|
-
* - `bwrapPath` / `socatPath`:
|
|
29447
|
-
*
|
|
29538
|
+
* - `ripgrep` / `bwrapPath` / `socatPath`: each names an executable, so
|
|
29539
|
+
* `stripCommandExecutingSandboxPaths` refuses them in both scopes rather than
|
|
29540
|
+
* emitting them under `--global`.
|
|
29448
29541
|
* - `credentials.envVars` / `credentials.files`: the ignored-at-project-scope
|
|
29449
29542
|
* unit is the individual entry's mode, not the settings key, and the same
|
|
29450
29543
|
* lists carry `deny` entries that project settings *do* honor — dropping a
|
|
@@ -29452,7 +29545,6 @@ function deepMergeRecords(base, patch) {
|
|
|
29452
29545
|
* filters those lists per entry instead.
|
|
29453
29546
|
*
|
|
29454
29547
|
* @see https://code.claude.com/docs/en/sandboxing
|
|
29455
|
-
* @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md — v2.1.232 scoped `sandbox.ripgrep`
|
|
29456
29548
|
*/
|
|
29457
29549
|
const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
29458
29550
|
["filesystem", "disabled"],
|
|
@@ -29461,10 +29553,228 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
|
29461
29553
|
["credentials", "allowPlaintextInject"],
|
|
29462
29554
|
["credentials", "awsPairs"],
|
|
29463
29555
|
["credentials", "sigv4"],
|
|
29464
|
-
["allowAppleEvents"]
|
|
29465
|
-
|
|
29556
|
+
["allowAppleEvents"]
|
|
29557
|
+
];
|
|
29558
|
+
/**
|
|
29559
|
+
* Walks `segments` from `root`, returning the record they name or `undefined` if
|
|
29560
|
+
* any step is missing or not a record. Shared by everything below that addresses
|
|
29561
|
+
* a `sandbox` path, so a nested path added to one of the tables is actually
|
|
29562
|
+
* traversed rather than silently skipped.
|
|
29563
|
+
*/
|
|
29564
|
+
function resolveSandboxParent({ root, segments }) {
|
|
29565
|
+
let parent = root;
|
|
29566
|
+
for (const segment of segments) {
|
|
29567
|
+
const next = parent[segment];
|
|
29568
|
+
if (!isPlainRecord(next)) return void 0;
|
|
29569
|
+
parent = next;
|
|
29570
|
+
}
|
|
29571
|
+
return parent;
|
|
29572
|
+
}
|
|
29573
|
+
/**
|
|
29574
|
+
* Deletes `path` from `target` in place and reports whether anything was there,
|
|
29575
|
+
* dropping a container the removal emptied so no `"network": {}` noise is left
|
|
29576
|
+
* behind.
|
|
29577
|
+
*/
|
|
29578
|
+
function deleteSandboxPath({ target, path }) {
|
|
29579
|
+
const leaf = path.at(-1);
|
|
29580
|
+
if (leaf === void 0) return false;
|
|
29581
|
+
const parentPath = path.slice(0, -1);
|
|
29582
|
+
const parent = resolveSandboxParent({
|
|
29583
|
+
root: target,
|
|
29584
|
+
segments: parentPath
|
|
29585
|
+
});
|
|
29586
|
+
if (parent === void 0 || parent[leaf] === void 0) return false;
|
|
29587
|
+
delete parent[leaf];
|
|
29588
|
+
for (let depth = parentPath.length; depth > 0; depth--) {
|
|
29589
|
+
const container = resolveSandboxParent({
|
|
29590
|
+
root: target,
|
|
29591
|
+
segments: parentPath.slice(0, depth)
|
|
29592
|
+
});
|
|
29593
|
+
if (container === void 0 || Object.keys(container).length > 0) break;
|
|
29594
|
+
const holder = resolveSandboxParent({
|
|
29595
|
+
root: target,
|
|
29596
|
+
segments: parentPath.slice(0, depth - 1)
|
|
29597
|
+
});
|
|
29598
|
+
const name = parentPath[depth - 1];
|
|
29599
|
+
if (holder === void 0 || name === void 0) break;
|
|
29600
|
+
delete holder[name];
|
|
29601
|
+
}
|
|
29602
|
+
return true;
|
|
29603
|
+
}
|
|
29604
|
+
/**
|
|
29605
|
+
* The `permissions.defaultMode` values that start a session with fewer prompts
|
|
29606
|
+
* than the default. `plan` and `default` are absent because they do not widen
|
|
29607
|
+
* anything.
|
|
29608
|
+
*/
|
|
29609
|
+
const CLAUDECODE_WIDENING_DEFAULT_MODES = {
|
|
29610
|
+
acceptEdits: "every file edit is then applied without a prompt",
|
|
29611
|
+
auto: "shell commands are then auto-approved by a classifier rather than by you",
|
|
29612
|
+
bypassPermissions: "every session then starts with no permission prompts at all"
|
|
29613
|
+
};
|
|
29614
|
+
/**
|
|
29615
|
+
* The `permissions` fields that widen rather than restrict: a `defaultMode` that
|
|
29616
|
+
* removes prompts, and `additionalDirectories`, which moves the
|
|
29617
|
+
* working-directory boundary. Warned for the same reason `disableAllHooks` is: a
|
|
29618
|
+
* shareable permissions file should not loosen the permission system quietly.
|
|
29619
|
+
*/
|
|
29620
|
+
function warnOnWideningPermissionFields({ fields, relativeFilePath, logger }) {
|
|
29621
|
+
const defaultMode = fields.defaultMode;
|
|
29622
|
+
if (typeof defaultMode === "string" && Object.hasOwn(CLAUDECODE_WIDENING_DEFAULT_MODES, defaultMode)) logger?.warn(`Claude Code permissions: writing 'permissions.defaultMode: "${defaultMode}"' to ${relativeFilePath}; ${CLAUDECODE_WIDENING_DEFAULT_MODES[defaultMode]}. Review it as you would a hook, especially if this permissions file came from 'rulesync fetch'.`);
|
|
29623
|
+
const additionalDirectories = fields.additionalDirectories;
|
|
29624
|
+
if (additionalDirectories !== void 0 && !(Array.isArray(additionalDirectories) && additionalDirectories.length === 0)) logger?.warn(`Claude Code permissions: writing 'permissions.additionalDirectories' to ${relativeFilePath}; it moves the boundary of what Claude Code may read and edit outside the project. Review the paths, especially if this permissions file came from 'rulesync fetch'.`);
|
|
29625
|
+
}
|
|
29626
|
+
/**
|
|
29627
|
+
* `sandbox` paths whose value names a binary Claude Code runs. `sandbox` has its
|
|
29628
|
+
* own merge branch, so the top-level refusal in `stripUnhonoredTopLevelKeys`
|
|
29629
|
+
* never sees them — they are refused here on the same grounds, in both scopes:
|
|
29630
|
+
* a fetched `.rulesync/permissions.jsonc` must not be able to point Claude Code
|
|
29631
|
+
* at an executable of its choosing.
|
|
29632
|
+
*
|
|
29633
|
+
* @see https://code.claude.com/docs/en/sandboxing
|
|
29634
|
+
*/
|
|
29635
|
+
const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
|
|
29636
|
+
["ripgrep"],
|
|
29637
|
+
["bwrapPath"],
|
|
29638
|
+
["socatPath"]
|
|
29639
|
+
];
|
|
29640
|
+
/**
|
|
29641
|
+
* `sandbox` paths that loosen the sandbox rather than naming something to run:
|
|
29642
|
+
* they let commands out of it, weaken the isolation it provides, or redirect
|
|
29643
|
+
* where its traffic goes. They are written like `env` is — the ordinary uses are
|
|
29644
|
+
* too common to refuse — but never silently, because a fetched override should
|
|
29645
|
+
* not be able to open the sandbox without saying so. `widens` keeps the warning
|
|
29646
|
+
* to the value that actually loosens the policy, so authoring the restrictive
|
|
29647
|
+
* value (`allowUnsandboxedCommands: false`, an empty `excludedCommands`) stays
|
|
29648
|
+
* quiet. The `allow*` lists are here for a structural reason: Claude Code merges
|
|
29649
|
+
* a list across every settings scope rather than replacing it, so a project file
|
|
29650
|
+
* can only ever add to them. Their counterparts — `denyRead`, `denyWrite`,
|
|
29651
|
+
* `deniedDomains` — merge the same way, but adding to a deny list only ever
|
|
29652
|
+
* narrows the policy, so they are absent.
|
|
29653
|
+
*
|
|
29654
|
+
* @see https://code.claude.com/docs/en/sandboxing
|
|
29655
|
+
*/
|
|
29656
|
+
const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
29657
|
+
{
|
|
29658
|
+
path: ["allowAppleEvents"],
|
|
29659
|
+
reason: "lets sandboxed commands send Apple Events, which removes code-execution isolation",
|
|
29660
|
+
widens: (value) => value === true
|
|
29661
|
+
},
|
|
29662
|
+
{
|
|
29663
|
+
path: ["allowUnsandboxedCommands"],
|
|
29664
|
+
reason: "controls whether Claude may retry a blocked command outside the sandbox",
|
|
29665
|
+
widens: (value) => value !== false
|
|
29666
|
+
},
|
|
29667
|
+
{
|
|
29668
|
+
path: ["autoAllowBashIfSandboxed"],
|
|
29669
|
+
reason: "controls whether every Bash command the sandbox accepts runs without a prompt",
|
|
29670
|
+
widens: (value) => value !== false
|
|
29671
|
+
},
|
|
29672
|
+
{
|
|
29673
|
+
path: ["enableWeakerNestedSandbox"],
|
|
29674
|
+
reason: "runs the Linux sandbox inside an unprivileged container, which weakens it",
|
|
29675
|
+
widens: (value) => value === true
|
|
29676
|
+
},
|
|
29677
|
+
{
|
|
29678
|
+
path: ["enableWeakerNetworkIsolation"],
|
|
29679
|
+
reason: "weakens the sandbox's network isolation on macOS",
|
|
29680
|
+
widens: (value) => value === true
|
|
29681
|
+
},
|
|
29682
|
+
{
|
|
29683
|
+
path: ["enabled"],
|
|
29684
|
+
reason: "turns the sandbox on, and sandboxed Bash commands then run without a permission prompt unless `autoAllowBashIfSandboxed` is false",
|
|
29685
|
+
widens: (value) => value === true
|
|
29686
|
+
},
|
|
29687
|
+
{
|
|
29688
|
+
path: ["excludedCommands"],
|
|
29689
|
+
reason: "names commands that always run outside the sandbox, with no sandbox policy applied",
|
|
29690
|
+
widens: (value) => !Array.isArray(value) || value.length > 0
|
|
29691
|
+
},
|
|
29692
|
+
{
|
|
29693
|
+
path: ["filesystem", "allowRead"],
|
|
29694
|
+
reason: "re-opens reading inside a region the sandbox's `denyRead` blocks",
|
|
29695
|
+
widens: (value) => !Array.isArray(value) || value.length > 0
|
|
29696
|
+
},
|
|
29697
|
+
{
|
|
29698
|
+
path: ["filesystem", "allowWrite"],
|
|
29699
|
+
reason: "adds paths sandboxed commands may write to, outside the working directory",
|
|
29700
|
+
widens: (value) => !Array.isArray(value) || value.length > 0
|
|
29701
|
+
},
|
|
29702
|
+
{
|
|
29703
|
+
path: ["ignoreViolations"],
|
|
29704
|
+
reason: "hides the sandbox violations it names, so a blocked access stops being reported",
|
|
29705
|
+
widens: (value) => isPlainRecord(value) ? Object.keys(value).length > 0 : value !== false
|
|
29706
|
+
},
|
|
29707
|
+
{
|
|
29708
|
+
path: ["network", "allowAllUnixSockets"],
|
|
29709
|
+
reason: "lets sandboxed commands connect to every Unix socket",
|
|
29710
|
+
widens: (value) => value === true
|
|
29711
|
+
},
|
|
29712
|
+
{
|
|
29713
|
+
path: ["network", "allowedDomains"],
|
|
29714
|
+
reason: "pre-allows domains sandboxed commands may reach without a prompt",
|
|
29715
|
+
widens: (value) => !Array.isArray(value) || value.length > 0
|
|
29716
|
+
},
|
|
29717
|
+
{
|
|
29718
|
+
path: ["network", "allowLocalBinding"],
|
|
29719
|
+
reason: "lets sandboxed commands bind local ports",
|
|
29720
|
+
widens: (value) => value === true
|
|
29721
|
+
},
|
|
29722
|
+
{
|
|
29723
|
+
path: ["network", "allowMachLookup"],
|
|
29724
|
+
reason: "names the macOS services sandboxed commands may reach, and `*` means every service",
|
|
29725
|
+
widens: (value) => !Array.isArray(value) || value.length > 0
|
|
29726
|
+
},
|
|
29727
|
+
{
|
|
29728
|
+
path: ["network", "allowUnixSockets"],
|
|
29729
|
+
reason: "names Unix sockets sandboxed commands may reach, and one such as `/var/run/docker.sock` is host access",
|
|
29730
|
+
widens: (value) => !Array.isArray(value) || value.length > 0
|
|
29731
|
+
},
|
|
29732
|
+
{
|
|
29733
|
+
path: ["network", "httpProxyPort"],
|
|
29734
|
+
reason: "routes the sandbox's HTTP traffic through the port it names",
|
|
29735
|
+
widens: () => true
|
|
29736
|
+
},
|
|
29737
|
+
{
|
|
29738
|
+
path: ["network", "socksProxyPort"],
|
|
29739
|
+
reason: "routes the sandbox's SOCKS traffic through the port it names",
|
|
29740
|
+
widens: () => true
|
|
29741
|
+
}
|
|
29466
29742
|
];
|
|
29467
29743
|
/**
|
|
29744
|
+
* Warns once per authored `sandbox` path that loosens the sandbox. Nothing is
|
|
29745
|
+
* removed — the value is written, just not silently. Called on the filtered
|
|
29746
|
+
* `sandbox` so it never claims to be writing a path the scope filters dropped.
|
|
29747
|
+
*/
|
|
29748
|
+
function warnOnTrustAffectingSandboxPaths({ sandbox, relativeFilePath, logger }) {
|
|
29749
|
+
for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
|
|
29750
|
+
const leaf = path.at(-1);
|
|
29751
|
+
if (leaf === void 0) continue;
|
|
29752
|
+
const parent = resolveSandboxParent({
|
|
29753
|
+
root: sandbox,
|
|
29754
|
+
segments: path.slice(0, -1)
|
|
29755
|
+
});
|
|
29756
|
+
if (parent === void 0) continue;
|
|
29757
|
+
const value = parent[leaf];
|
|
29758
|
+
if (value === void 0 || !widens(value)) continue;
|
|
29759
|
+
logger?.warn(`Claude Code permissions: writing 'sandbox.${path.join(".")}' to ${relativeFilePath}; it ${reason}. Review the value as you would a hook, especially if this permissions file came from 'rulesync fetch'.`);
|
|
29760
|
+
}
|
|
29761
|
+
}
|
|
29762
|
+
/**
|
|
29763
|
+
* Copy of the authored `sandbox` override with the paths that name an
|
|
29764
|
+
* executable removed, warning once per dropped path.
|
|
29765
|
+
*/
|
|
29766
|
+
function stripCommandExecutingSandboxPaths({ sandbox, relativeFilePath, logger }) {
|
|
29767
|
+
const filtered = structuredClone(sandbox);
|
|
29768
|
+
for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) {
|
|
29769
|
+
if (!deleteSandboxPath({
|
|
29770
|
+
target: filtered,
|
|
29771
|
+
path
|
|
29772
|
+
})) continue;
|
|
29773
|
+
logger?.warn(`Claude Code permissions: 'sandbox.${path.join(".")}' names an executable Claude Code runs, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; set this path in ${relativeFilePath} by hand.`);
|
|
29774
|
+
}
|
|
29775
|
+
return filtered;
|
|
29776
|
+
}
|
|
29777
|
+
/**
|
|
29468
29778
|
* Copy of the authored `sandbox` override with the user/managed-only paths
|
|
29469
29779
|
* removed, warning once per dropped path. Only the override copy is filtered —
|
|
29470
29780
|
* a value already hand-written in the target file is left untouched, matching
|
|
@@ -29473,24 +29783,10 @@ const CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS = [
|
|
|
29473
29783
|
function stripGlobalOnlySandboxPaths({ sandbox, relativeFilePath, logger }) {
|
|
29474
29784
|
const filtered = structuredClone(sandbox);
|
|
29475
29785
|
for (const path of CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS) {
|
|
29476
|
-
|
|
29477
|
-
|
|
29478
|
-
|
|
29479
|
-
|
|
29480
|
-
for (const segment of parentPath) {
|
|
29481
|
-
const next = parent[segment];
|
|
29482
|
-
if (!isPlainRecord(next)) {
|
|
29483
|
-
parent = {};
|
|
29484
|
-
break;
|
|
29485
|
-
}
|
|
29486
|
-
parent = next;
|
|
29487
|
-
}
|
|
29488
|
-
if (parent[leaf] === void 0) continue;
|
|
29489
|
-
delete parent[leaf];
|
|
29490
|
-
const [container] = parentPath;
|
|
29491
|
-
if (container !== void 0 && isPlainRecord(filtered[container])) {
|
|
29492
|
-
if (Object.keys(filtered[container]).length === 0) delete filtered[container];
|
|
29493
|
-
}
|
|
29786
|
+
if (!deleteSandboxPath({
|
|
29787
|
+
target: filtered,
|
|
29788
|
+
path
|
|
29789
|
+
})) continue;
|
|
29494
29790
|
logger?.warn(`Claude Code permissions: 'sandbox.${path.join(".")}' is only honored in user/managed/--settings settings, so it is not written to the project-scoped ${relativeFilePath}. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
|
|
29495
29791
|
}
|
|
29496
29792
|
return filtered;
|
|
@@ -29546,6 +29842,215 @@ function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
|
|
|
29546
29842
|
else filtered.credentials = filteredCredentials;
|
|
29547
29843
|
return filtered;
|
|
29548
29844
|
}
|
|
29845
|
+
/**
|
|
29846
|
+
* Top-level `.claude/settings.json` keys another feature owns, derived from
|
|
29847
|
+
* {@link SHARED_CONFIG_OWNERSHIP} rather than restated here so a feature that
|
|
29848
|
+
* starts owning a new key is excluded from the passthrough automatically
|
|
29849
|
+
* (today: `hooks`, from the hooks feature). Only `replace-owned-keys` entries
|
|
29850
|
+
* name keys; the `custom` policies on this file (`ignore`, `permissions`) own
|
|
29851
|
+
* entries *inside* `permissions`, which the passthrough excludes wholesale.
|
|
29852
|
+
*/
|
|
29853
|
+
const CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS = Object.entries(SHARED_CONFIG_OWNERSHIP[".claude/settings.json"]?.features ?? {}).flatMap(([feature, policy]) => feature !== "permissions" && policy.kind === "replace-owned-keys" ? policy.ownedKeys : []);
|
|
29854
|
+
/**
|
|
29855
|
+
* Top-level `.claude/settings.json` keys the generic `claudecode` override
|
|
29856
|
+
* passthrough must not carry. `permissions` and `sandbox` have their own merge
|
|
29857
|
+
* branches (the managed `allow`/`ask`/`deny` arrays and the scope filtering
|
|
29858
|
+
* respectively), `permission` is rulesync's own canonical tool-scoped block
|
|
29859
|
+
* rather than a settings key, `$schema` is an editor pointer rather than a
|
|
29860
|
+
* Claude Code setting, and the rest belong to the other features writing this
|
|
29861
|
+
* shared file.
|
|
29862
|
+
*/
|
|
29863
|
+
const CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS = /* @__PURE__ */ new Set([
|
|
29864
|
+
"permission",
|
|
29865
|
+
"permissions",
|
|
29866
|
+
"sandbox",
|
|
29867
|
+
"$schema",
|
|
29868
|
+
...CLAUDECODE_FEATURE_OWNED_SETTINGS_KEYS
|
|
29869
|
+
]);
|
|
29870
|
+
/**
|
|
29871
|
+
* Top-level settings keys Claude Code reads only from user settings, managed
|
|
29872
|
+
* settings and the `--settings` CLI flag — the same restriction
|
|
29873
|
+
* `CLAUDECODE_GLOBAL_ONLY_SANDBOX_PATHS` records for the `sandbox` subtree, and
|
|
29874
|
+
* the reason the passthrough drops them at project scope instead of committing
|
|
29875
|
+
* a setting that never applies. `rulesync generate --global` writes the user
|
|
29876
|
+
* settings file, so they are emitted there.
|
|
29877
|
+
*
|
|
29878
|
+
* Derived from the per-key **Scope** column of the settings reference: every
|
|
29879
|
+
* top-level key documented as `User or managed` or `User, local, or managed`.
|
|
29880
|
+
*
|
|
29881
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
29882
|
+
*/
|
|
29883
|
+
const CLAUDECODE_USER_SCOPE_ONLY_KEYS = /* @__PURE__ */ new Set([
|
|
29884
|
+
"askUserQuestionTimeout",
|
|
29885
|
+
"autoMode",
|
|
29886
|
+
"dialogExpiry",
|
|
29887
|
+
"enableArtifact",
|
|
29888
|
+
"footerLinksRegexes",
|
|
29889
|
+
"pluginConfigs",
|
|
29890
|
+
"skipAutoPermissionPrompt",
|
|
29891
|
+
"skipDangerousModePermissionPrompt",
|
|
29892
|
+
"spellcheck",
|
|
29893
|
+
"sshConfigs",
|
|
29894
|
+
"syncClaudeAiSkills",
|
|
29895
|
+
"useAutoModeDuringPlan",
|
|
29896
|
+
"vimInsertModeRemaps"
|
|
29897
|
+
]);
|
|
29898
|
+
/**
|
|
29899
|
+
* Top-level settings keys neither file rulesync writes can honor, with the file
|
|
29900
|
+
* that does. `Managed` keys are read only from the settings file an organization
|
|
29901
|
+
* deploys, and `Global config` keys only from `~/.claude.json` — rulesync writes
|
|
29902
|
+
* `.claude/settings.json` and `~/.claude/settings.json`, so authoring either
|
|
29903
|
+
* kind through the override would produce a policy that silently never applies.
|
|
29904
|
+
*
|
|
29905
|
+
* Derived from the per-key **Scope** column of the settings reference.
|
|
29906
|
+
*
|
|
29907
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
29908
|
+
*/
|
|
29909
|
+
const CLAUDECODE_UNHONORED_KEY_SOURCES = {
|
|
29910
|
+
allowAllClaudeAiMcps: "managed settings",
|
|
29911
|
+
allowedChannelPlugins: "managed settings",
|
|
29912
|
+
allowManagedHooksOnly: "managed settings",
|
|
29913
|
+
allowManagedMcpServersOnly: "managed settings",
|
|
29914
|
+
allowManagedPermissionRulesOnly: "managed settings",
|
|
29915
|
+
autoConnectIde: "~/.claude.json",
|
|
29916
|
+
autoInstallIdeExtension: "~/.claude.json",
|
|
29917
|
+
blockedMarketplaces: "managed settings",
|
|
29918
|
+
browserExternalPageTools: "managed settings",
|
|
29919
|
+
channelsEnabled: "managed settings",
|
|
29920
|
+
claudeMd: "managed settings",
|
|
29921
|
+
diffTool: "~/.claude.json",
|
|
29922
|
+
disableBrowserExternalNavigation: "managed settings",
|
|
29923
|
+
disableCommandPluginSources: "managed settings",
|
|
29924
|
+
disableMobileSimulatorTools: "managed settings",
|
|
29925
|
+
disableSideloadFlags: "managed settings",
|
|
29926
|
+
externalEditorContext: "~/.claude.json",
|
|
29927
|
+
forceLoginGatewayUrl: "managed settings",
|
|
29928
|
+
forceRemoteSettingsRefresh: "managed settings",
|
|
29929
|
+
parentSettingsBehavior: "managed settings",
|
|
29930
|
+
permissionExplainerEnabled: "~/.claude.json",
|
|
29931
|
+
pluginSuggestionMarketplaces: "managed settings",
|
|
29932
|
+
pluginTrustMessage: "managed settings",
|
|
29933
|
+
requiredMaximumVersion: "managed settings",
|
|
29934
|
+
requiredMinimumVersion: "managed settings",
|
|
29935
|
+
sshHostAllowlist: "managed settings",
|
|
29936
|
+
strictKnownMarketplaces: "managed settings",
|
|
29937
|
+
strictPluginOnlyCustomization: "managed settings",
|
|
29938
|
+
teammateDefaultModel: "~/.claude.json",
|
|
29939
|
+
wslInheritsWindowsSettings: "managed settings"
|
|
29940
|
+
};
|
|
29941
|
+
/**
|
|
29942
|
+
* Top-level settings keys whose value Claude Code **executes**. The generic
|
|
29943
|
+
* passthrough refuses them outright rather than warning: `.rulesync/*` files
|
|
29944
|
+
* are shareable — `rulesync fetch` copies a third party's `permissions.jsonc`
|
|
29945
|
+
* straight into a project — and a file named for *restricting* what an agent
|
|
29946
|
+
* may do is not somewhere a reviewer looks for a command to run. Commands
|
|
29947
|
+
* belong in `.rulesync/hooks.jsonc` and `.rulesync/.mcp.json`, which are read
|
|
29948
|
+
* as executable by anyone reviewing them. Set these by hand in the settings
|
|
29949
|
+
* file if you need them.
|
|
29950
|
+
*
|
|
29951
|
+
* The value is the reason, spliced into the warning.
|
|
29952
|
+
*
|
|
29953
|
+
* @see https://code.claude.com/docs/en/settings-reference
|
|
29954
|
+
*/
|
|
29955
|
+
const CLAUDECODE_COMMAND_EXECUTING_KEYS = {
|
|
29956
|
+
apiKeyHelper: "runs the script it names to mint an API key",
|
|
29957
|
+
awsAuthRefresh: "runs the command it names to refresh AWS credentials",
|
|
29958
|
+
awsCredentialExport: "runs the command it names to export AWS credentials",
|
|
29959
|
+
fileSuggestion: "runs its `command` on every `@` file completion",
|
|
29960
|
+
gcpAuthRefresh: "runs the command it names to refresh Google Cloud credentials",
|
|
29961
|
+
otelHeadersHelper: "runs the script it names to build OpenTelemetry headers",
|
|
29962
|
+
policyHelper: "runs the executable it names to compute the managed settings",
|
|
29963
|
+
processWrapper: "wraps every process Claude Code spawns",
|
|
29964
|
+
statusLine: "runs its `command` on every status-line render",
|
|
29965
|
+
subagentStatusLine: "runs its `command` on every subagent status row"
|
|
29966
|
+
};
|
|
29967
|
+
/**
|
|
29968
|
+
* Top-level settings keys the passthrough does write, but never silently: each
|
|
29969
|
+
* one widens what Claude Code trusts or where it sends data, so a value that
|
|
29970
|
+
* arrived with a fetched `.rulesync/permissions.jsonc` should be looked at
|
|
29971
|
+
* deliberately. Warning on write follows the precedent set for Warp's
|
|
29972
|
+
* `command_denylist`, which also replaces a protection when rulesync writes it.
|
|
29973
|
+
*
|
|
29974
|
+
* The value is the reason, spliced into the warning.
|
|
29975
|
+
*/
|
|
29976
|
+
const CLAUDECODE_TRUST_AFFECTING_KEYS = {
|
|
29977
|
+
agent: "starts every session as the named subagent, with that subagent's prompt, tools and model",
|
|
29978
|
+
allowedHttpHookUrls: "limits which URLs an HTTP hook may target, and an empty list means every URL",
|
|
29979
|
+
allowedMcpServers: "allowlists the MCP servers that may be used, and entries from every settings file merge into one list, so an entry here widens an allowlist deployed elsewhere",
|
|
29980
|
+
autoMode: "auto-approves shell commands with a classifier rather than with a prompt",
|
|
29981
|
+
disableAllHooks: "controls whether hooks run at all",
|
|
29982
|
+
disableSkillShellExecution: "re-opens the inline shell commands in a skill or custom command that a user setting had turned off",
|
|
29983
|
+
enableAllProjectMcpServers: "auto-approves every server in the project `.mcp.json`",
|
|
29984
|
+
enabledMcpjsonServers: "auto-approves the named servers in the project `.mcp.json`",
|
|
29985
|
+
enabledPlugins: "enables plugins, which can ship their own hooks",
|
|
29986
|
+
env: "sets environment variables for every process Claude Code spawns, so a value such as `NODE_OPTIONS` or `PATH` runs code and `ANTHROPIC_BASE_URL` redirects every prompt",
|
|
29987
|
+
extraKnownMarketplaces: "registers plugin marketplace sources",
|
|
29988
|
+
httpHookAllowedEnvVars: "controls which environment variables an HTTP hook may put in a request header, credentials included",
|
|
29989
|
+
outputStyle: "replaces the system prompt every session runs with",
|
|
29990
|
+
skipAutoPermissionPrompt: "removes the confirmation shown before auto-approval mode starts",
|
|
29991
|
+
skipDangerousModePermissionPrompt: "removes the confirmation shown before the mode that skips every permission check starts"
|
|
29992
|
+
};
|
|
29993
|
+
/**
|
|
29994
|
+
* The keys from the table above that only widen at one particular value.
|
|
29995
|
+
* `disableSkillShellExecution: true` turns inline shell execution off, which
|
|
29996
|
+
* restricts; the `false` that turns it back on is what a fetched override could
|
|
29997
|
+
* use to undo a user setting, so only that value is warned about.
|
|
29998
|
+
*/
|
|
29999
|
+
const CLAUDECODE_TRUST_KEY_WIDENING_VALUES = { disableSkillShellExecution: (value) => value === false };
|
|
30000
|
+
/**
|
|
30001
|
+
* A key name is authored data that ends up in a log line, so strip the control
|
|
30002
|
+
* characters that would let it forge a line or hide the warnings beside it, and
|
|
30003
|
+
* cap the length.
|
|
30004
|
+
*/
|
|
30005
|
+
function displayKey(key) {
|
|
30006
|
+
const stripped = stripControlCharacters(key);
|
|
30007
|
+
return stripped.length > 80 ? `${stripped.slice(0, 80)}…` : stripped;
|
|
30008
|
+
}
|
|
30009
|
+
/**
|
|
30010
|
+
* Alternate spellings Claude Code accepts for a top-level settings key, mapped
|
|
30011
|
+
* to the canonical key whose **Scope** the alias inherits: "In any settings
|
|
30012
|
+
* file that accepts the canonical key, Claude Code reads the alias exactly as
|
|
30013
|
+
* it reads the canonical key." Resolving through this map before the scope
|
|
30014
|
+
* check keeps an alias from slipping past a restriction its canonical spelling
|
|
30015
|
+
* is caught by — `allowedMarketplaces` is `Managed`, like
|
|
30016
|
+
* `strictKnownMarketplaces`. Both aliases require Claude Code v2.1.232+.
|
|
30017
|
+
*
|
|
30018
|
+
* @see https://code.claude.com/docs/en/settings-reference#marketplace-key-aliases
|
|
30019
|
+
*/
|
|
30020
|
+
const CLAUDECODE_SETTINGS_KEY_ALIASES = {
|
|
30021
|
+
additionalMarketplaces: "extraKnownMarketplaces",
|
|
30022
|
+
allowedMarketplaces: "strictKnownMarketplaces"
|
|
30023
|
+
};
|
|
30024
|
+
/**
|
|
30025
|
+
* Copy of the authored top-level passthrough with the keys the target file
|
|
30026
|
+
* cannot honor removed, warning once per dropped key. Like
|
|
30027
|
+
* `stripGlobalOnlySandboxPaths`, only the override copy is filtered — a value
|
|
30028
|
+
* already hand-written in the target file is left untouched, which is why the
|
|
30029
|
+
* warning points at it.
|
|
30030
|
+
*/
|
|
30031
|
+
function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logger }) {
|
|
30032
|
+
const filtered = {};
|
|
30033
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
30034
|
+
const shown = displayKey(key);
|
|
30035
|
+
const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
|
|
30036
|
+
if (Object.hasOwn(CLAUDECODE_COMMAND_EXECUTING_KEYS, canonicalKey)) {
|
|
30037
|
+
logger?.warn(`Claude Code permissions: '${shown}' ${CLAUDECODE_COMMAND_EXECUTING_KEYS[canonicalKey]}, so rulesync does not write it to ${relativeFilePath}. A permissions file is shareable — 'rulesync fetch' copies one into a project — and is not where a reviewer looks for a command to run; author commands in .rulesync/hooks.jsonc, or set this key in ${relativeFilePath} by hand.`);
|
|
30038
|
+
continue;
|
|
30039
|
+
}
|
|
30040
|
+
if (Object.hasOwn(CLAUDECODE_UNHONORED_KEY_SOURCES, canonicalKey)) {
|
|
30041
|
+
logger?.warn(`Claude Code permissions: '${shown}' is only honored in ${CLAUDECODE_UNHONORED_KEY_SOURCES[canonicalKey]}, which rulesync does not generate, so it is not written to ${relativeFilePath}. Set it in that file by hand, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
|
|
30042
|
+
continue;
|
|
30043
|
+
}
|
|
30044
|
+
if (!global && CLAUDECODE_USER_SCOPE_ONLY_KEYS.has(canonicalKey)) {
|
|
30045
|
+
logger?.warn(`Claude Code permissions: '${shown}' is not honored in the project-scoped ${relativeFilePath}, so it is not written there — Claude Code reads it from user, local or managed settings. Author it in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
|
|
30046
|
+
continue;
|
|
30047
|
+
}
|
|
30048
|
+
const widensAtValue = CLAUDECODE_TRUST_KEY_WIDENING_VALUES[canonicalKey];
|
|
30049
|
+
if (Object.hasOwn(CLAUDECODE_TRUST_AFFECTING_KEYS, canonicalKey) && (widensAtValue === void 0 || widensAtValue(value))) logger?.warn(`Claude Code permissions: writing '${shown}' to ${relativeFilePath}; it ${CLAUDECODE_TRUST_AFFECTING_KEYS[canonicalKey]}. Review the value as you would a hook, especially if this permissions file came from 'rulesync fetch'.`);
|
|
30050
|
+
filtered[key] = value;
|
|
30051
|
+
}
|
|
30052
|
+
return filtered;
|
|
30053
|
+
}
|
|
29549
30054
|
const CLAUDE_PATH_RULE_ALIASES = {
|
|
29550
30055
|
Write: "Edit",
|
|
29551
30056
|
NotebookEdit: "Edit",
|
|
@@ -29616,7 +30121,13 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
29616
30121
|
});
|
|
29617
30122
|
const overridePermissions = config.claudecode?.permissions;
|
|
29618
30123
|
if (overridePermissions && typeof overridePermissions === "object") {
|
|
29619
|
-
const { allow: _a, ask: _k, deny: _d, ...
|
|
30124
|
+
const { allow: _a, ask: _k, deny: _d, ...rest } = overridePermissions;
|
|
30125
|
+
const nonListFields = Object.fromEntries(Object.entries(rest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
30126
|
+
warnOnWideningPermissionFields({
|
|
30127
|
+
fields: nonListFields,
|
|
30128
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30129
|
+
logger
|
|
30130
|
+
});
|
|
29620
30131
|
settings.permissions = {
|
|
29621
30132
|
...settings.permissions,
|
|
29622
30133
|
...nonListFields
|
|
@@ -29624,17 +30135,41 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
29624
30135
|
}
|
|
29625
30136
|
const overrideSandbox = config.claudecode?.sandbox;
|
|
29626
30137
|
if (isPlainRecord(overrideSandbox)) {
|
|
29627
|
-
const
|
|
30138
|
+
const executableFreeSandbox = stripCommandExecutingSandboxPaths({
|
|
30139
|
+
sandbox: overrideSandbox,
|
|
30140
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30141
|
+
logger
|
|
30142
|
+
});
|
|
30143
|
+
const scopedSandbox = global ? executableFreeSandbox : stripProjectIgnoredMaskEntries({
|
|
29628
30144
|
sandbox: stripGlobalOnlySandboxPaths({
|
|
29629
|
-
sandbox:
|
|
30145
|
+
sandbox: executableFreeSandbox,
|
|
29630
30146
|
relativeFilePath: paths.relativeFilePath,
|
|
29631
30147
|
logger
|
|
29632
30148
|
}),
|
|
29633
30149
|
relativeFilePath: paths.relativeFilePath,
|
|
29634
30150
|
logger
|
|
29635
30151
|
});
|
|
30152
|
+
warnOnTrustAffectingSandboxPaths({
|
|
30153
|
+
sandbox: scopedSandbox,
|
|
30154
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30155
|
+
logger
|
|
30156
|
+
});
|
|
29636
30157
|
if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isPlainRecord(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
|
|
29637
30158
|
}
|
|
30159
|
+
const overrideTopLevel = {};
|
|
30160
|
+
for (const [key, value] of Object.entries(config.claudecode ?? {})) {
|
|
30161
|
+
if (CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS.has(key)) continue;
|
|
30162
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30163
|
+
if (value === void 0) continue;
|
|
30164
|
+
overrideTopLevel[key] = value;
|
|
30165
|
+
}
|
|
30166
|
+
const scopedTopLevel = stripUnhonoredTopLevelKeys({
|
|
30167
|
+
overrides: overrideTopLevel,
|
|
30168
|
+
global,
|
|
30169
|
+
relativeFilePath: paths.relativeFilePath,
|
|
30170
|
+
logger
|
|
30171
|
+
});
|
|
30172
|
+
if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
|
|
29638
30173
|
const managedToolNames = managedClaudeToolNames(config);
|
|
29639
30174
|
const merged = applyPermissions({
|
|
29640
30175
|
settings,
|
|
@@ -29667,12 +30202,33 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
29667
30202
|
ask: permissions.ask ?? [],
|
|
29668
30203
|
deny: permissions.deny ?? []
|
|
29669
30204
|
});
|
|
29670
|
-
const { allow: _a, ask: _k, deny: _d, ...
|
|
30205
|
+
const { allow: _a, ask: _k, deny: _d, ...permissionsRest } = permissions;
|
|
30206
|
+
const nonListFields = Object.fromEntries(Object.entries(permissionsRest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
29671
30207
|
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
29672
30208
|
const { sandbox } = settings;
|
|
29673
|
-
if (isPlainRecord(sandbox)
|
|
30209
|
+
if (isPlainRecord(sandbox)) {
|
|
30210
|
+
const importedSandbox = structuredClone(sandbox);
|
|
30211
|
+
for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) deleteSandboxPath({
|
|
30212
|
+
target: importedSandbox,
|
|
30213
|
+
path
|
|
30214
|
+
});
|
|
30215
|
+
if (Object.keys(importedSandbox).length > 0) config.claudecode = {
|
|
30216
|
+
...config.claudecode,
|
|
30217
|
+
sandbox: importedSandbox
|
|
30218
|
+
};
|
|
30219
|
+
}
|
|
30220
|
+
const topLevelPassthrough = {};
|
|
30221
|
+
for (const [key, value] of Object.entries(settings)) {
|
|
30222
|
+
if (CLAUDECODE_NON_PASSTHROUGH_OVERRIDE_KEYS.has(key)) continue;
|
|
30223
|
+
const canonicalKey = Object.hasOwn(CLAUDECODE_SETTINGS_KEY_ALIASES, key) ? CLAUDECODE_SETTINGS_KEY_ALIASES[key] : key;
|
|
30224
|
+
if (Object.hasOwn(CLAUDECODE_COMMAND_EXECUTING_KEYS, canonicalKey)) continue;
|
|
30225
|
+
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
30226
|
+
if (value === void 0) continue;
|
|
30227
|
+
topLevelPassthrough[key] = value;
|
|
30228
|
+
}
|
|
30229
|
+
if (Object.keys(topLevelPassthrough).length > 0) config.claudecode = {
|
|
29674
30230
|
...config.claudecode,
|
|
29675
|
-
|
|
30231
|
+
...topLevelPassthrough
|
|
29676
30232
|
};
|
|
29677
30233
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(config, null, 2) });
|
|
29678
30234
|
}
|
|
@@ -33532,7 +34088,7 @@ function extractKiroOverride(toolsSettings) {
|
|
|
33532
34088
|
if (Object.keys(overrideToolsSettings).length === 0) return void 0;
|
|
33533
34089
|
return { toolsSettings: overrideToolsSettings };
|
|
33534
34090
|
}
|
|
33535
|
-
function asStringArray(value) {
|
|
34091
|
+
function asStringArray$1(value) {
|
|
33536
34092
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
33537
34093
|
}
|
|
33538
34094
|
/**
|
|
@@ -33542,8 +34098,8 @@ function asStringArray(value) {
|
|
|
33542
34098
|
*/
|
|
33543
34099
|
function rulesFromArrays(settings, allowKey, denyKey) {
|
|
33544
34100
|
const rules = {};
|
|
33545
|
-
for (const pattern of asStringArray(settings[allowKey])) rules[pattern] = "allow";
|
|
33546
|
-
for (const pattern of asStringArray(settings[denyKey])) rules[pattern] = "deny";
|
|
34101
|
+
for (const pattern of asStringArray$1(settings[allowKey])) rules[pattern] = "allow";
|
|
34102
|
+
for (const pattern of asStringArray$1(settings[denyKey])) rules[pattern] = "deny";
|
|
33547
34103
|
return rules;
|
|
33548
34104
|
}
|
|
33549
34105
|
//#endregion
|
|
@@ -36455,6 +37011,181 @@ var ZedPermissions = class ZedPermissions extends ToolPermissions {
|
|
|
36455
37011
|
}
|
|
36456
37012
|
};
|
|
36457
37013
|
//#endregion
|
|
37014
|
+
//#region src/constants/zoocode-paths.ts
|
|
37015
|
+
/**
|
|
37016
|
+
* Zoo Code is a VS Code extension, so its committable command allow/deny lists
|
|
37017
|
+
* are workspace settings rather than files in the `.roo/` agent-asset tree that
|
|
37018
|
+
* the other Zoo Code features write.
|
|
37019
|
+
*
|
|
37020
|
+
* `zoo-code.allowedCommands` / `zoo-code.deniedCommands` are contributed with no
|
|
37021
|
+
* `scope`, which in VS Code means `window` scope — settable in a workspace's
|
|
37022
|
+
* `.vscode/settings.json` — and `ClineProvider.mergeCommandLists()` unions the
|
|
37023
|
+
* workspace values into the effective auto-approval lists.
|
|
37024
|
+
*
|
|
37025
|
+
* The `zoo-code.*` namespace is Zoo-era (the v3.74.0 rebrand); the archived Roo
|
|
37026
|
+
* Code lineage spelled the same settings `roo-cline.*`, so this surface is
|
|
37027
|
+
* deliberately not shared with the `roo` target.
|
|
37028
|
+
*
|
|
37029
|
+
* @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
|
|
37030
|
+
* @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/webview/ClineProvider.ts
|
|
37031
|
+
*/
|
|
37032
|
+
const ZOOCODE_VSCODE_SETTINGS_DIR = ".vscode";
|
|
37033
|
+
const ZOOCODE_VSCODE_SETTINGS_FILE_NAME = "settings.json";
|
|
37034
|
+
const ZOOCODE_ALLOWED_COMMANDS_KEY = "zoo-code.allowedCommands";
|
|
37035
|
+
const ZOOCODE_DENIED_COMMANDS_KEY = "zoo-code.deniedCommands";
|
|
37036
|
+
//#endregion
|
|
37037
|
+
//#region src/features/permissions/zoocode-permissions.ts
|
|
37038
|
+
/**
|
|
37039
|
+
* The canonical category Zoo Code's command lists correspond to. Zoo Code gates
|
|
37040
|
+
* terminal command execution and nothing else through these settings, so only
|
|
37041
|
+
* `bash` maps; `read`/`write`/`edit`/`webfetch` have no workspace-settable
|
|
37042
|
+
* counterpart in the extension's contributions.
|
|
37043
|
+
*/
|
|
37044
|
+
const COMMAND_CATEGORY = "bash";
|
|
37045
|
+
function asStringArray(value) {
|
|
37046
|
+
if (!Array.isArray(value)) return [];
|
|
37047
|
+
return value.filter((entry) => typeof entry === "string");
|
|
37048
|
+
}
|
|
37049
|
+
/**
|
|
37050
|
+
* Split one canonical category's rules into Zoo Code's two command lists.
|
|
37051
|
+
*
|
|
37052
|
+
* Zoo Code matches these entries as command **prefixes**: a command runs
|
|
37053
|
+
* without a confirmation prompt when it starts with an `allowedCommands` entry,
|
|
37054
|
+
* and is refused outright when it starts with a `deniedCommands` entry (deny
|
|
37055
|
+
* wins). `ask` is represented by listing the pattern in neither list, which
|
|
37056
|
+
* leaves Zoo Code's default approval prompt in charge.
|
|
37057
|
+
*
|
|
37058
|
+
* Each list is `undefined` when it would be empty, so the key is retracted from
|
|
37059
|
+
* the settings file rather than written as an empty array — an empty
|
|
37060
|
+
* `allowedCommands` and an absent one mean the same thing to Zoo Code, and the
|
|
37061
|
+
* absent form leaves no rulesync residue behind.
|
|
37062
|
+
*/
|
|
37063
|
+
function buildCommandLists(rules) {
|
|
37064
|
+
const allowed = [];
|
|
37065
|
+
const denied = [];
|
|
37066
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow") allowed.push(pattern);
|
|
37067
|
+
else if (action === "deny") denied.push(pattern);
|
|
37068
|
+
return {
|
|
37069
|
+
allowed: allowed.length > 0 ? allowed : void 0,
|
|
37070
|
+
denied: denied.length > 0 ? denied : void 0
|
|
37071
|
+
};
|
|
37072
|
+
}
|
|
37073
|
+
/**
|
|
37074
|
+
* Permissions generator for Zoo Code.
|
|
37075
|
+
*
|
|
37076
|
+
* Zoo Code has no policy file in its `.roo/` tree: the committable command
|
|
37077
|
+
* allow/deny lists are VS Code workspace settings
|
|
37078
|
+
* (`zoo-code.allowedCommands` / `zoo-code.deniedCommands` in
|
|
37079
|
+
* `.vscode/settings.json`), which `ClineProvider.mergeCommandLists()` unions
|
|
37080
|
+
* into the lists the auto-approval decision reads. That file is a
|
|
37081
|
+
* general-purpose workspace settings file with many unrelated keys, so reads
|
|
37082
|
+
* and writes merge into the existing JSONC (touching only the two managed keys)
|
|
37083
|
+
* and the file is never deleted.
|
|
37084
|
+
*
|
|
37085
|
+
* Only project scope is modeled: VS Code's user-scope `settings.json` lives at
|
|
37086
|
+
* a platform-dependent path outside rulesync's home-relative global model.
|
|
37087
|
+
*
|
|
37088
|
+
* The `roo` target deliberately does not get this adapter. The settings
|
|
37089
|
+
* namespace is Zoo-era (`roo-cline.*` before the v3.74.0 rebrand), and Roo Code
|
|
37090
|
+
* is EOL with its repository archived, so emitting `zoo-code.*` keys for a
|
|
37091
|
+
* `--targets roo` generate would write settings that Roo itself never reads.
|
|
37092
|
+
*
|
|
37093
|
+
* @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/package.json
|
|
37094
|
+
* @see https://github.com/Zoo-Code-Org/Zoo-Code/blob/main/src/core/auto-approval/commands.ts
|
|
37095
|
+
*/
|
|
37096
|
+
var ZoocodePermissions = class ZoocodePermissions extends ToolPermissions {
|
|
37097
|
+
constructor(params) {
|
|
37098
|
+
super({
|
|
37099
|
+
...params,
|
|
37100
|
+
fileContent: params.fileContent ?? "{}"
|
|
37101
|
+
});
|
|
37102
|
+
}
|
|
37103
|
+
/**
|
|
37104
|
+
* `.vscode/settings.json` is a user-managed workspace file with unrelated
|
|
37105
|
+
* settings, so it must not be deleted.
|
|
37106
|
+
*/
|
|
37107
|
+
isDeletable() {
|
|
37108
|
+
return false;
|
|
37109
|
+
}
|
|
37110
|
+
static getSettablePaths(_options = {}) {
|
|
37111
|
+
return {
|
|
37112
|
+
relativeDirPath: ZOOCODE_VSCODE_SETTINGS_DIR,
|
|
37113
|
+
relativeFilePath: ZOOCODE_VSCODE_SETTINGS_FILE_NAME
|
|
37114
|
+
};
|
|
37115
|
+
}
|
|
37116
|
+
static async fromFile({ outputRoot = process.cwd(), validate = true }) {
|
|
37117
|
+
const paths = ZoocodePermissions.getSettablePaths();
|
|
37118
|
+
const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "{}";
|
|
37119
|
+
return new ZoocodePermissions({
|
|
37120
|
+
outputRoot,
|
|
37121
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37122
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37123
|
+
fileContent,
|
|
37124
|
+
validate
|
|
37125
|
+
});
|
|
37126
|
+
}
|
|
37127
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions }) {
|
|
37128
|
+
const paths = ZoocodePermissions.getSettablePaths();
|
|
37129
|
+
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
37130
|
+
const existingContent = await readFileContentOrNull(filePath) ?? "{}";
|
|
37131
|
+
const rules = rulesyncPermissions.getJson().permission[COMMAND_CATEGORY];
|
|
37132
|
+
const patch = {};
|
|
37133
|
+
if (rules !== void 0) {
|
|
37134
|
+
const { allowed, denied } = buildCommandLists(rules);
|
|
37135
|
+
patch[ZOOCODE_ALLOWED_COMMANDS_KEY] = allowed;
|
|
37136
|
+
patch[ZOOCODE_DENIED_COMMANDS_KEY] = denied;
|
|
37137
|
+
}
|
|
37138
|
+
return new ZoocodePermissions({
|
|
37139
|
+
outputRoot,
|
|
37140
|
+
relativeDirPath: paths.relativeDirPath,
|
|
37141
|
+
relativeFilePath: paths.relativeFilePath,
|
|
37142
|
+
fileContent: applySharedConfigPatch({
|
|
37143
|
+
fileKey: sharedConfigFileKey(paths),
|
|
37144
|
+
feature: "permissions",
|
|
37145
|
+
existingContent,
|
|
37146
|
+
patch,
|
|
37147
|
+
filePath
|
|
37148
|
+
}),
|
|
37149
|
+
validate: true
|
|
37150
|
+
});
|
|
37151
|
+
}
|
|
37152
|
+
toRulesyncPermissions() {
|
|
37153
|
+
let settings;
|
|
37154
|
+
try {
|
|
37155
|
+
settings = parseSharedConfig({
|
|
37156
|
+
format: "jsonc",
|
|
37157
|
+
fileContent: this.getFileContent() || "{}",
|
|
37158
|
+
filePath: (0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath()),
|
|
37159
|
+
invalidRootPolicy: "error",
|
|
37160
|
+
jsoncParseErrors: "error"
|
|
37161
|
+
});
|
|
37162
|
+
} catch (error) {
|
|
37163
|
+
throw new Error(`Failed to parse Zoo Code VS Code settings in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
37164
|
+
}
|
|
37165
|
+
const rules = {};
|
|
37166
|
+
for (const pattern of asStringArray(settings[ZOOCODE_ALLOWED_COMMANDS_KEY])) rules[pattern] = "allow";
|
|
37167
|
+
for (const pattern of asStringArray(settings[ZOOCODE_DENIED_COMMANDS_KEY])) rules[pattern] = "deny";
|
|
37168
|
+
const permission = {};
|
|
37169
|
+
if (Object.keys(rules).length > 0) permission[COMMAND_CATEGORY] = rules;
|
|
37170
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({ permission }, null, 2) });
|
|
37171
|
+
}
|
|
37172
|
+
validate() {
|
|
37173
|
+
return {
|
|
37174
|
+
success: true,
|
|
37175
|
+
error: null
|
|
37176
|
+
};
|
|
37177
|
+
}
|
|
37178
|
+
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath }) {
|
|
37179
|
+
return new ZoocodePermissions({
|
|
37180
|
+
outputRoot,
|
|
37181
|
+
relativeDirPath,
|
|
37182
|
+
relativeFilePath,
|
|
37183
|
+
fileContent: "{}",
|
|
37184
|
+
validate: false
|
|
37185
|
+
});
|
|
37186
|
+
}
|
|
37187
|
+
};
|
|
37188
|
+
//#endregion
|
|
36458
37189
|
//#region src/features/permissions/permissions-processor.ts
|
|
36459
37190
|
const PermissionsProcessorToolTargetSchema = zod_mini.z.enum(permissionsProcessorToolTargetTuple);
|
|
36460
37191
|
const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
@@ -36697,6 +37428,14 @@ const toolPermissionsFactories = /* @__PURE__ */ new Map([
|
|
|
36697
37428
|
supportsGlobal: true,
|
|
36698
37429
|
supportsImport: true
|
|
36699
37430
|
}
|
|
37431
|
+
}],
|
|
37432
|
+
["zoocode", {
|
|
37433
|
+
class: ZoocodePermissions,
|
|
37434
|
+
meta: {
|
|
37435
|
+
supportsProject: true,
|
|
37436
|
+
supportsGlobal: false,
|
|
37437
|
+
supportsImport: true
|
|
37438
|
+
}
|
|
36700
37439
|
}]
|
|
36701
37440
|
]);
|
|
36702
37441
|
var PermissionsProcessor = class extends FeatureProcessor {
|
|
@@ -52076,11 +52815,12 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
52076
52815
|
static getSettablePaths(_options = {}) {
|
|
52077
52816
|
return { nonRoot: { relativeDirPath: buildToolPath(ROO_DIR, "rules", _options.excludeToolDir) } };
|
|
52078
52817
|
}
|
|
52079
|
-
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, validate = true }) {
|
|
52080
|
-
const
|
|
52818
|
+
static async fromFile({ outputRoot = process.cwd(), relativeFilePath, relativeDirPath: overrideDirPath, validate = true }) {
|
|
52819
|
+
const relativeDirPath = overrideDirPath !== void 0 && RooRule.extractModeFromDirPath(overrideDirPath) !== void 0 ? overrideDirPath : this.getSettablePaths().nonRoot.relativeDirPath;
|
|
52820
|
+
const fileContent = await readFileContent((0, node_path.join)(outputRoot, relativeDirPath, relativeFilePath));
|
|
52081
52821
|
return new RooRule({
|
|
52082
52822
|
outputRoot,
|
|
52083
|
-
relativeDirPath
|
|
52823
|
+
relativeDirPath,
|
|
52084
52824
|
relativeFilePath,
|
|
52085
52825
|
fileContent,
|
|
52086
52826
|
validate,
|
|
@@ -52088,12 +52828,16 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
52088
52828
|
});
|
|
52089
52829
|
}
|
|
52090
52830
|
static fromRulesyncRule({ outputRoot = process.cwd(), rulesyncRule, validate = true }) {
|
|
52091
|
-
|
|
52831
|
+
const params = this.buildToolRuleParamsDefault({
|
|
52092
52832
|
outputRoot,
|
|
52093
52833
|
rulesyncRule,
|
|
52094
52834
|
validate,
|
|
52095
52835
|
nonRootPath: this.getSettablePaths().nonRoot
|
|
52096
|
-
})
|
|
52836
|
+
});
|
|
52837
|
+
const mode = rulesyncRule.getFrontmatter().roo?.mode;
|
|
52838
|
+
if (!params.root && mode !== void 0 && mode !== "") if (!ROO_MODE_SLUG_PATTERN.test(mode)) warnWithFallback(void 0, `Ignoring roo.mode "${mode}" on ${rulesyncRule.getRelativeFilePath()}: a mode slug may contain only letters, digits and hyphens. Writing the rule to ${params.relativeDirPath} instead.`);
|
|
52839
|
+
else params.relativeDirPath = (0, node_path.join)((0, node_path.dirname)(params.relativeDirPath), rooModeRulesDirName(mode));
|
|
52840
|
+
return new RooRule(params);
|
|
52097
52841
|
}
|
|
52098
52842
|
/**
|
|
52099
52843
|
* Extract mode slug from file path for mode-specific rules
|
|
@@ -52105,8 +52849,51 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
52105
52849
|
const singleFileMatch = filePath.match(/\.(roo|cline)rules-([a-zA-Z0-9-]+)$/);
|
|
52106
52850
|
if (singleFileMatch) return singleFileMatch[2];
|
|
52107
52851
|
}
|
|
52852
|
+
/**
|
|
52853
|
+
* The mode slug of a `.roo/rules-{mode}/` directory, or `undefined` for the
|
|
52854
|
+
* generic `.roo/rules/` directory and anything else. Path-shaped input is
|
|
52855
|
+
* rejected by the slug pattern, so a crafted directory name cannot be lifted
|
|
52856
|
+
* back into frontmatter.
|
|
52857
|
+
*/
|
|
52858
|
+
static extractModeFromDirPath(relativeDirPath) {
|
|
52859
|
+
for (const segment of toPosixPath(relativeDirPath).split("/")) {
|
|
52860
|
+
if (!segment.startsWith("rules-")) continue;
|
|
52861
|
+
const mode = segment.slice(6);
|
|
52862
|
+
if (ROO_MODE_SLUG_PATTERN.test(mode)) return mode;
|
|
52863
|
+
}
|
|
52864
|
+
}
|
|
52108
52865
|
toRulesyncRule() {
|
|
52109
|
-
|
|
52866
|
+
const mode = RooRule.extractModeFromDirPath(this.getRelativeDirPath());
|
|
52867
|
+
if (mode === void 0) return this.toRulesyncRuleDefault();
|
|
52868
|
+
const baseName = this.getRelativeFilePath().replace(/\.md$/, "");
|
|
52869
|
+
const suffix = `-${mode}`;
|
|
52870
|
+
const importedName = baseName.endsWith(suffix) ? baseName : `${baseName}${suffix}`;
|
|
52871
|
+
return new RulesyncRule({
|
|
52872
|
+
outputRoot: this.getOutputRoot(),
|
|
52873
|
+
relativeDirPath: RulesyncRule.getSettablePaths().recommended.relativeDirPath,
|
|
52874
|
+
relativeFilePath: `${importedName}.md`,
|
|
52875
|
+
frontmatter: {
|
|
52876
|
+
root: false,
|
|
52877
|
+
targets: [this.constructor.getToolTargetName()],
|
|
52878
|
+
description: this.description,
|
|
52879
|
+
globs: this.globs ?? [],
|
|
52880
|
+
roo: { mode }
|
|
52881
|
+
},
|
|
52882
|
+
body: this.getFileContent(),
|
|
52883
|
+
validate: true
|
|
52884
|
+
});
|
|
52885
|
+
}
|
|
52886
|
+
/**
|
|
52887
|
+
* Mode-specific rule directories (`.roo/rules-{mode}/`), which Roo/Zoo Code
|
|
52888
|
+
* load instead of `.roo/rules/` while that mode is active. Import-only: the
|
|
52889
|
+
* generic directory is the only one the deletion sweep enumerates, because a
|
|
52890
|
+
* `rules-*` glob would also match mode rules a user wrote by hand.
|
|
52891
|
+
*/
|
|
52892
|
+
static getNestedFilePatterns({ outputRoot }) {
|
|
52893
|
+
return {
|
|
52894
|
+
include: [`${toPosixPath(outputRoot)}/${toPosixPath(ROO_DIR)}/rules-*/**/*.md`],
|
|
52895
|
+
ignore: []
|
|
52896
|
+
};
|
|
52110
52897
|
}
|
|
52111
52898
|
validate() {
|
|
52112
52899
|
return {
|
|
@@ -52127,10 +52914,18 @@ var RooRule = class RooRule extends ToolRule {
|
|
|
52127
52914
|
static isTargetedByRulesyncRule(rulesyncRule) {
|
|
52128
52915
|
return this.isTargetedByRulesyncRuleDefault({
|
|
52129
52916
|
rulesyncRule,
|
|
52130
|
-
toolTarget:
|
|
52917
|
+
toolTarget: this.getToolTargetName()
|
|
52131
52918
|
});
|
|
52132
52919
|
}
|
|
52133
52920
|
/**
|
|
52921
|
+
* The tool target this class imports as. `ZoocodeRule` narrows the same
|
|
52922
|
+
* `.roo/` adapters to the `zoocode` target, so the name has to come from the
|
|
52923
|
+
* class rather than a literal.
|
|
52924
|
+
*/
|
|
52925
|
+
static getToolTargetName() {
|
|
52926
|
+
return "roo";
|
|
52927
|
+
}
|
|
52928
|
+
/**
|
|
52134
52929
|
* Glob for the `separate-local-file` deletion; Roo reads `AGENTS.local.md`
|
|
52135
52930
|
* at the project root, not under `.roo/` (mirrors rovodev).
|
|
52136
52931
|
*/
|
|
@@ -52663,11 +53458,8 @@ var ZedRule = class ZedRule extends ToolRule {
|
|
|
52663
53458
|
* @see https://docs.zoocode.dev
|
|
52664
53459
|
*/
|
|
52665
53460
|
var ZoocodeRule = class extends RooRule {
|
|
52666
|
-
static
|
|
52667
|
-
return
|
|
52668
|
-
rulesyncRule,
|
|
52669
|
-
toolTarget: "zoocode"
|
|
52670
|
-
});
|
|
53461
|
+
static getToolTargetName() {
|
|
53462
|
+
return "zoocode";
|
|
52671
53463
|
}
|
|
52672
53464
|
};
|
|
52673
53465
|
//#endregion
|
|
@@ -56324,6 +57116,12 @@ Object.defineProperty(exports, "stringifyFrontmatter", {
|
|
|
56324
57116
|
return stringifyFrontmatter;
|
|
56325
57117
|
}
|
|
56326
57118
|
});
|
|
57119
|
+
Object.defineProperty(exports, "stripControlCharacters", {
|
|
57120
|
+
enumerable: true,
|
|
57121
|
+
get: function() {
|
|
57122
|
+
return stripControlCharacters;
|
|
57123
|
+
}
|
|
57124
|
+
});
|
|
56327
57125
|
Object.defineProperty(exports, "toPosixPath", {
|
|
56328
57126
|
enumerable: true,
|
|
56329
57127
|
get: function() {
|