rulesync 43.4.2 → 43.6.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 +6 -6
- package/dist/cli/index.js +6 -6
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-BWOC-Tig.js → import--Y_A56qx.js} +1179 -264
- package/dist/{import-BWOC-Tig.js.map → import--Y_A56qx.js.map} +1 -1
- package/dist/{import-ClFasGQ6.cjs → import-C-oT_Hdp.cjs} +1195 -280
- 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 +1 -1
|
@@ -382,36 +382,36 @@ function formatError(error) {
|
|
|
382
382
|
return boundErrorMessage(String(error));
|
|
383
383
|
}
|
|
384
384
|
// src/constants/rulesync-paths.ts
|
|
385
|
-
const { join: join$
|
|
385
|
+
const { join: join$385 } = node_path.posix;
|
|
386
386
|
const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
|
|
387
387
|
const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
|
|
388
388
|
const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
|
|
389
389
|
const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
|
|
390
390
|
const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
|
|
391
391
|
const RULES_FEATURE_SUBDIR = "rules";
|
|
392
|
-
const CURATED_RULES_FEATURE_SUBDIR = join$
|
|
392
|
+
const CURATED_RULES_FEATURE_SUBDIR = join$385(RULES_FEATURE_SUBDIR, ".curated");
|
|
393
393
|
const COMMANDS_FEATURE_SUBDIR = "commands";
|
|
394
394
|
const SUBAGENTS_FEATURE_SUBDIR = "subagents";
|
|
395
395
|
const CHECKS_FEATURE_SUBDIR = "checks";
|
|
396
396
|
const SKILLS_FEATURE_SUBDIR = "skills";
|
|
397
|
-
const CURATED_SKILLS_FEATURE_SUBDIR = join$
|
|
398
|
-
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$
|
|
399
|
-
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$
|
|
400
|
-
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$
|
|
401
|
-
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$
|
|
402
|
-
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$
|
|
403
|
-
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$
|
|
404
|
-
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$
|
|
405
|
-
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$
|
|
406
|
-
join$
|
|
407
|
-
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
408
|
-
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$
|
|
397
|
+
const CURATED_SKILLS_FEATURE_SUBDIR = join$385(SKILLS_FEATURE_SUBDIR, ".curated");
|
|
398
|
+
const RULESYNC_RULES_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
|
|
399
|
+
const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
|
|
400
|
+
const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
|
|
401
|
+
const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
|
|
402
|
+
const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
|
|
403
|
+
const RULESYNC_MCP_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
|
|
404
|
+
const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
|
|
405
|
+
const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
|
|
406
|
+
join$385(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
|
|
407
|
+
const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
|
|
408
|
+
const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
|
|
409
409
|
const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
|
|
410
|
-
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$
|
|
410
|
+
const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
|
|
411
411
|
const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
|
|
412
412
|
const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
|
|
413
|
-
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$
|
|
414
|
-
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$
|
|
413
|
+
const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
|
|
414
|
+
const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$385(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
|
|
415
415
|
const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
|
|
416
416
|
const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
|
|
417
417
|
const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
|
|
@@ -895,6 +895,7 @@ const hooksProcessorToolTargetTuple = [
|
|
|
895
895
|
"qwencode",
|
|
896
896
|
"reasonix",
|
|
897
897
|
"tabnine",
|
|
898
|
+
"trae",
|
|
898
899
|
"grokcli",
|
|
899
900
|
"gitlabduo",
|
|
900
901
|
"lettacode",
|
|
@@ -3945,6 +3946,7 @@ const SHARED_USER_MANAGED_CONFIG_PATHS = [
|
|
|
3945
3946
|
".poolside/settings.yaml",
|
|
3946
3947
|
".config/poolside/settings.yaml",
|
|
3947
3948
|
".tabnine/agent/settings.json",
|
|
3949
|
+
".trae/hooks.json",
|
|
3948
3950
|
".vibe/config.toml",
|
|
3949
3951
|
".zcode/config.json",
|
|
3950
3952
|
".zcode/cli/config.json",
|
|
@@ -6027,6 +6029,34 @@ const CANONICAL_TO_CORTEXCODE_EVENT_NAMES = {
|
|
|
6027
6029
|
};
|
|
6028
6030
|
const CORTEXCODE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_CORTEXCODE_EVENT_NAMES);
|
|
6029
6031
|
/**
|
|
6032
|
+
* Hook events supported by TRAE.
|
|
6033
|
+
*
|
|
6034
|
+
* TRAE reads `.trae/hooks.json` (project) and `~/.trae/hooks.json` (user):
|
|
6035
|
+
* `{ "version"?: 1, "hooks": { "<Event>": [{ "matcher"?, "hooks": [{ "type":
|
|
6036
|
+
* "command", "command", "timeout"? }] }] } }` with Claude-Code-style
|
|
6037
|
+
* PascalCase event names. `$TRAE_PROJECT_DIR` resolves to the workspace root
|
|
6038
|
+
* inside commands.
|
|
6039
|
+
*
|
|
6040
|
+
* @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
|
|
6041
|
+
*/
|
|
6042
|
+
const TRAE_HOOK_EVENTS = [
|
|
6043
|
+
"sessionStart",
|
|
6044
|
+
"beforeSubmitPrompt",
|
|
6045
|
+
"preToolUse",
|
|
6046
|
+
"postToolUse",
|
|
6047
|
+
"stop",
|
|
6048
|
+
"notification"
|
|
6049
|
+
];
|
|
6050
|
+
const CANONICAL_TO_TRAE_EVENT_NAMES = {
|
|
6051
|
+
sessionStart: "SessionStart",
|
|
6052
|
+
beforeSubmitPrompt: "UserPromptSubmit",
|
|
6053
|
+
preToolUse: "PreToolUse",
|
|
6054
|
+
postToolUse: "PostToolUse",
|
|
6055
|
+
stop: "Stop",
|
|
6056
|
+
notification: "Notification"
|
|
6057
|
+
};
|
|
6058
|
+
const TRAE_TO_CANONICAL_EVENT_NAMES = invertEventNames(CANONICAL_TO_TRAE_EVENT_NAMES);
|
|
6059
|
+
/**
|
|
6030
6060
|
* Hook events supported by CodeBuddy Code.
|
|
6031
6061
|
*
|
|
6032
6062
|
* CodeBuddy Code reads hooks from the `hooks` key of `.codebuddy/settings.json`
|
|
@@ -6499,6 +6529,7 @@ const HooksConfigSchema = zod_mini.z.looseObject({
|
|
|
6499
6529
|
bob: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6500
6530
|
codebuddy: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6501
6531
|
cortexcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6532
|
+
trae: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6502
6533
|
commandcode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6503
6534
|
lettacode: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
6504
6535
|
continue: zod_mini.z.optional(zod_mini.z.looseObject({ hooks: zod_mini.z.optional(hooksRecordSchema) })),
|
|
@@ -8400,17 +8431,29 @@ const CodewhalePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
8400
8431
|
rules: zod_mini.z.optional(zod_mini.z.array(CodewhalePermissionRuleSchema))
|
|
8401
8432
|
});
|
|
8402
8433
|
/**
|
|
8403
|
-
* The
|
|
8404
|
-
*
|
|
8434
|
+
* The values VS Code's `chat.permissions.default` setting accepts: the starting
|
|
8435
|
+
* permission level of new Local-harness chat sessions — `default` (Default
|
|
8405
8436
|
* Permissions), `autoApprove` (Bypass Approvals) and `autopilot` (Autopilot).
|
|
8406
8437
|
*
|
|
8407
8438
|
* @see https://code.visualstudio.com/docs/agents/reference/ai-settings
|
|
8408
8439
|
*/
|
|
8409
|
-
const
|
|
8440
|
+
const COPILOT_LOCAL_PERMISSION_LEVELS = [
|
|
8410
8441
|
"default",
|
|
8411
8442
|
"autoApprove",
|
|
8412
8443
|
"autopilot"
|
|
8413
8444
|
];
|
|
8445
|
+
/**
|
|
8446
|
+
* The starting permission level the `copilot.defaultPermissionMode` override
|
|
8447
|
+
* sets for new chat sessions: the {@link COPILOT_LOCAL_PERMISSION_LEVELS}, plus
|
|
8448
|
+
* `assisted` (Assisted permissions, GA for Copilot-harness sessions since VS
|
|
8449
|
+
* Code 1.144 and unavailable to Local sessions). Each value is written to both
|
|
8450
|
+
* `chat.permissions.default` (Local sessions) and the `approvals` / `mode` of
|
|
8451
|
+
* `chat.defaultConfiguration` (Copilot-harness sessions, VS Code 1.144+).
|
|
8452
|
+
*
|
|
8453
|
+
* @see https://code.visualstudio.com/updates/v1_144
|
|
8454
|
+
* @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
|
|
8455
|
+
*/
|
|
8456
|
+
const COPILOT_DEFAULT_PERMISSION_MODES = [...COPILOT_LOCAL_PERMISSION_LEVELS, "assisted"];
|
|
8414
8457
|
/** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
|
|
8415
8458
|
const CopilotSandboxEnabledSchema = zod_mini.z.enum(["off", "on"]);
|
|
8416
8459
|
/** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
|
|
@@ -8451,7 +8494,7 @@ const CopilotSandboxSchema = zod_mini.z.strictObject({
|
|
|
8451
8494
|
/**
|
|
8452
8495
|
* Tool-scoped override block for GitHub Copilot in VS Code.
|
|
8453
8496
|
* `defaultPermissionMode` writes the workspace `chat.permissions.default`
|
|
8454
|
-
*
|
|
8497
|
+
* and `chat.defaultConfiguration` settings, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
|
|
8455
8498
|
* has a canonical category.
|
|
8456
8499
|
*/
|
|
8457
8500
|
const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
@@ -9333,6 +9376,7 @@ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
9333
9376
|
})),
|
|
9334
9377
|
workflow_runtime_prepare: zod_mini.z.optional(zod_mini.z.object({ custom_scripts: zod_mini.z.optional(zod_mini.z.boolean()) })),
|
|
9335
9378
|
workflow_command_gates: zod_mini.z.optional(zod_mini.z.object({ custom_scripts: zod_mini.z.optional(zod_mini.z.boolean()) })),
|
|
9379
|
+
workflow_system_commands: zod_mini.z.optional(zod_mini.z.object({ custom_scripts: zod_mini.z.optional(zod_mini.z.boolean()) })),
|
|
9336
9380
|
sync_conflict_resolver: zod_mini.z.optional(zod_mini.z.object({ auto_approve_tools: zod_mini.z.optional(zod_mini.z.boolean()) })),
|
|
9337
9381
|
allow_git_hooks: zod_mini.z.optional(zod_mini.z.boolean()),
|
|
9338
9382
|
allow_git_filters: zod_mini.z.optional(zod_mini.z.boolean())
|
|
@@ -9600,6 +9644,7 @@ const CodexSandboxModeSchema = zod_mini.z.enum([
|
|
|
9600
9644
|
"workspace-write",
|
|
9601
9645
|
"danger-full-access"
|
|
9602
9646
|
]);
|
|
9647
|
+
const CODEX_U32_MAX = 4294967295;
|
|
9603
9648
|
/**
|
|
9604
9649
|
* Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
|
|
9605
9650
|
* value Codex still accepts for backward compatibility, so it is included so
|
|
@@ -9668,6 +9713,9 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
|
|
|
9668
9713
|
* | `guardian_subagent`), or a table for the richer reviewer config.
|
|
9669
9714
|
* Defaults to `auto_review` when neither the override nor the existing
|
|
9670
9715
|
* config sets it.
|
|
9716
|
+
* - `auto_review` — the `[auto_review]` table that configures the reviewer
|
|
9717
|
+
* `approvals_reviewer = "auto_review"` selects (`policy`, `extra_policy`,
|
|
9718
|
+
* `timeout_ms`, …). A loose table, shallow-merged like `apps` / `tui`.
|
|
9671
9719
|
* - `git_write_rules` — whether the managed profile's `:workspace_roots` table
|
|
9672
9720
|
* emits the default `.git` carve-out (`".git/**" = "write"`). Codex's
|
|
9673
9721
|
* `:workspace` baseline makes `.git` read-only, which denies basic git
|
|
@@ -9681,7 +9729,8 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
|
|
|
9681
9729
|
* The keys written to `config.toml` are an **allowlist**, not verbatim
|
|
9682
9730
|
* passthrough: only `CODEXCLI_OVERRIDE_KEYS`
|
|
9683
9731
|
* (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
|
|
9684
|
-
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`
|
|
9732
|
+
* `sandbox_workspace_write`, `apps`, `approvals_reviewer`, `auto_review`,
|
|
9733
|
+
* `tui`) are emitted, and
|
|
9685
9734
|
* `computeCodexcliOverridePatch` skips anything else with a warning.
|
|
9686
9735
|
* `base_permission_profile` and `git_write_rules` are consumed by the profile
|
|
9687
9736
|
* builder rather than written, as described above, and `permission` is the
|
|
@@ -9713,6 +9762,26 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
9713
9762
|
apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
9714
9763
|
approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})])),
|
|
9715
9764
|
/**
|
|
9765
|
+
* The `[auto_review]` table of `config.toml`, which configures the reviewer
|
|
9766
|
+
* that `approvals_reviewer = "auto_review"` (Rulesync's default) selects:
|
|
9767
|
+
* `policy` / `extra_policy` (text inserted into the reviewer prompt),
|
|
9768
|
+
* `timeout_ms` (the review budget, Codex 0.165.0+; `0` waits without a
|
|
9769
|
+
* deadline), `circuit_break_action` (`default` | `strict`) and
|
|
9770
|
+
* `conversation_history_max_output_tokens` (a positive token cap). Codex
|
|
9771
|
+
* rejects the whole config file when one of these has the wrong type or is
|
|
9772
|
+
* out of range (`timeout_ms` is a `u32`, the token cap a `NonZeroUsize`), so
|
|
9773
|
+
* they are checked here; every other key passes through verbatim.
|
|
9774
|
+
*
|
|
9775
|
+
* @see https://github.com/openai/codex/blob/rust-v0.166.0/codex-rs/config/src/config_toml.rs (`AutoReviewToml`)
|
|
9776
|
+
*/
|
|
9777
|
+
auto_review: zod_mini.z.optional(zod_mini.z.looseObject({
|
|
9778
|
+
policy: zod_mini.z.optional(zod_mini.z.string()),
|
|
9779
|
+
extra_policy: zod_mini.z.optional(zod_mini.z.string()),
|
|
9780
|
+
timeout_ms: zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.nonnegative(), zod_mini.z.lte(CODEX_U32_MAX))),
|
|
9781
|
+
circuit_break_action: zod_mini.z.optional(zod_mini.z.enum(["default", "strict"])),
|
|
9782
|
+
conversation_history_max_output_tokens: zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.positive()))
|
|
9783
|
+
})),
|
|
9784
|
+
/**
|
|
9716
9785
|
* The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
|
|
9717
9786
|
* Not a permission surface, but like `apps` it is a top-level table with no
|
|
9718
9787
|
* canonical category, and Codex adds keys to it faster than an explicit model
|
|
@@ -12319,6 +12388,7 @@ const CODEXCLI_OVERRIDE_KEYS = [
|
|
|
12319
12388
|
"sandbox_workspace_write",
|
|
12320
12389
|
"apps",
|
|
12321
12390
|
"approvals_reviewer",
|
|
12391
|
+
"auto_review",
|
|
12322
12392
|
"tui"
|
|
12323
12393
|
];
|
|
12324
12394
|
// src/constants/takt-paths.ts
|
|
@@ -13741,6 +13811,7 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13741
13811
|
"workflow_arpeggio",
|
|
13742
13812
|
"workflow_runtime_prepare",
|
|
13743
13813
|
"workflow_command_gates",
|
|
13814
|
+
"workflow_system_commands",
|
|
13744
13815
|
"sync_conflict_resolver",
|
|
13745
13816
|
"allow_git_hooks",
|
|
13746
13817
|
"allow_git_filters"
|
|
@@ -13882,6 +13953,7 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
13882
13953
|
"chat.tools.edits.autoApprove",
|
|
13883
13954
|
"chat.tools.urls.autoApprove",
|
|
13884
13955
|
"chat.permissions.default",
|
|
13956
|
+
"chat.defaultConfiguration",
|
|
13885
13957
|
"chat.agent.sandbox.enabled",
|
|
13886
13958
|
"chat.agent.sandbox.network.allowNetwork",
|
|
13887
13959
|
"chat.agent.sandbox.network.allowLocalNetwork",
|
|
@@ -14140,6 +14212,14 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
14140
14212
|
}
|
|
14141
14213
|
}
|
|
14142
14214
|
},
|
|
14215
|
+
".trae/hooks.json": {
|
|
14216
|
+
format: "json",
|
|
14217
|
+
invalidRootPolicy: "error",
|
|
14218
|
+
features: { hooks: {
|
|
14219
|
+
kind: "replace-owned-keys",
|
|
14220
|
+
ownedKeys: ["hooks"]
|
|
14221
|
+
} }
|
|
14222
|
+
},
|
|
14143
14223
|
".trae/permission/global.json": {
|
|
14144
14224
|
format: "json",
|
|
14145
14225
|
invalidRootPolicy: "error",
|
|
@@ -23265,10 +23345,13 @@ var TaktCommand = class TaktCommand extends ToolCommand {
|
|
|
23265
23345
|
* @see https://docs.trae.ai/ide/slash-commands?_lang=en
|
|
23266
23346
|
* @see https://docs.trae.ai/ide/ignore-files?_lang=en
|
|
23267
23347
|
* @see https://docs.trae.ai/ide/permission-and-approval?_lang=en
|
|
23348
|
+
* @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
|
|
23268
23349
|
*/
|
|
23269
23350
|
const TRAE_DIR = ".trae";
|
|
23270
23351
|
/** Project MCP servers (`mcpServers` object), project scope only. */
|
|
23271
23352
|
const TRAE_MCP_FILE_NAME = "mcp.json";
|
|
23353
|
+
/** Hooks: `.trae/hooks.json` (project) and `~/.trae/hooks.json` (global). */
|
|
23354
|
+
const TRAE_HOOKS_FILE_NAME = "hooks.json";
|
|
23272
23355
|
/** Skills: `.trae/skills/<name>/SKILL.md` (project) and `~/.trae/skills/` (global). */
|
|
23273
23356
|
const TRAE_SKILLS_DIR_PATH = (0, node_path.join)(TRAE_DIR, "skills");
|
|
23274
23357
|
/**
|
|
@@ -25193,12 +25276,80 @@ var AiassistantSkill = class AiassistantSkill extends ToolSkill {
|
|
|
25193
25276
|
});
|
|
25194
25277
|
}
|
|
25195
25278
|
};
|
|
25279
|
+
// src/features/shared/amp-settings.ts
|
|
25280
|
+
/**
|
|
25281
|
+
* Parse an Amp `settings.json` / `settings.jsonc` document.
|
|
25282
|
+
*
|
|
25283
|
+
* Shared by the MCP and permissions adapters because both read the very same
|
|
25284
|
+
* file. Two separately maintained copies drifted once already: the permissions
|
|
25285
|
+
* side was hardened while the MCP side kept calling `jsonc-parser` directly,
|
|
25286
|
+
* which left it importing MCP servers reachable only through an injected
|
|
25287
|
+
* `__proto__`.
|
|
25288
|
+
*
|
|
25289
|
+
* {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
|
|
25290
|
+
* error instead of returning `jsonc-parser`'s best-effort value, and it
|
|
25291
|
+
* rebuilds every object from its own enumerable entries, so a root `__proto__`
|
|
25292
|
+
* cannot swap the returned object's prototype and `constructor` / `prototype`
|
|
25293
|
+
* never survive as own keys, at any depth.
|
|
25294
|
+
*
|
|
25295
|
+
* Removal is silent, matching every other tool-side adapter (opencode, kilo,
|
|
25296
|
+
* copilot). Reporting the removed keys through `droppedPollutionKeysError` is
|
|
25297
|
+
* reserved for the three files a user authors under `.rulesync/`, whose whole
|
|
25298
|
+
* purpose is to be turned into tool config — a key that vanishes there needs
|
|
25299
|
+
* explaining, whereas here the surrounding settings are the user's own file
|
|
25300
|
+
* and are left alone.
|
|
25301
|
+
*
|
|
25302
|
+
* Note the consequence for a root `__proto__`: sanitizing runs before the
|
|
25303
|
+
* plain-object check, so such a document parses successfully with the key
|
|
25304
|
+
* stripped rather than failing that check. That is deliberate — dropping the
|
|
25305
|
+
* one poisoned key keeps the user's unrelated settings — and matches the
|
|
25306
|
+
* sanitize-before-the-root-check design the shared config gateway documents.
|
|
25307
|
+
*/
|
|
25308
|
+
function parseAmpSettings({ fileContent }) {
|
|
25309
|
+
let parsed;
|
|
25310
|
+
try {
|
|
25311
|
+
parsed = parseJsonc$6(fileContent || "{}");
|
|
25312
|
+
} catch (error) {
|
|
25313
|
+
throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
|
|
25314
|
+
}
|
|
25315
|
+
if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
25316
|
+
return parsed;
|
|
25317
|
+
}
|
|
25318
|
+
/**
|
|
25319
|
+
* Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
|
|
25320
|
+
* an existing user file is read-modified-written in place instead of a fresh
|
|
25321
|
+
* `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
|
|
25322
|
+
* `settings.json` when neither file exists: Amp's canonically documented
|
|
25323
|
+
* settings file (`.amp/settings.json`), shared by the MCP and permissions
|
|
25324
|
+
* adapters, so generating both on a clean repo writes a single settings file.
|
|
25325
|
+
*/
|
|
25326
|
+
async function resolveAmpSettingsFile(jsonDir) {
|
|
25327
|
+
const jsoncContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
|
|
25328
|
+
if (jsoncContent !== null) return {
|
|
25329
|
+
fileContent: jsoncContent,
|
|
25330
|
+
relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
|
|
25331
|
+
};
|
|
25332
|
+
const jsonContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_FILE_NAME));
|
|
25333
|
+
if (jsonContent !== null) return {
|
|
25334
|
+
fileContent: jsonContent,
|
|
25335
|
+
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
25336
|
+
};
|
|
25337
|
+
return {
|
|
25338
|
+
fileContent: null,
|
|
25339
|
+
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
25340
|
+
};
|
|
25341
|
+
}
|
|
25196
25342
|
// src/features/skills/amp-skill.ts
|
|
25197
25343
|
const AmpSkillFrontmatterSchema = zod_mini.z.looseObject({
|
|
25198
25344
|
name: zod_mini.z.string(),
|
|
25199
25345
|
description: zod_mini.z.string()
|
|
25200
25346
|
});
|
|
25201
25347
|
/**
|
|
25348
|
+
* Amp setting that makes it skip `~/.config/agents/skills/` and
|
|
25349
|
+
* `~/.agents/skills/`. https://ampcode.com/docs/cli/settings
|
|
25350
|
+
*/
|
|
25351
|
+
const AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY = "amp.skills.disableGlobalAgentsSkills";
|
|
25352
|
+
/**
|
|
25202
25353
|
* Represents an Amp Agent Skill directory.
|
|
25203
25354
|
*
|
|
25204
25355
|
* Amp natively reads Agent Skills following the open standard: a directory with a
|
|
@@ -25233,6 +25384,27 @@ var AmpSkill = class AmpSkill extends ToolSkill {
|
|
|
25233
25384
|
...global && { importOnlySkillRoots: [AGENTSMD_SKILLS_DIR_PATH, AMP_SKILLS_NATIVE_GLOBAL_DIR] }
|
|
25234
25385
|
};
|
|
25235
25386
|
}
|
|
25387
|
+
/**
|
|
25388
|
+
* Warn when the user's Amp settings disable the global agent-agnostic skill
|
|
25389
|
+
* roots, because the global skills rulesync writes to
|
|
25390
|
+
* `~/.config/agents/skills/` would then never load. Only the user settings
|
|
25391
|
+
* file is checked: a workspace `.amp/settings.json(c)` depends on where Amp
|
|
25392
|
+
* is started, which global generation cannot know.
|
|
25393
|
+
*/
|
|
25394
|
+
static async getGenerationWarnings({ outputRoot, global, logger }) {
|
|
25395
|
+
if (!global) return [];
|
|
25396
|
+
const { fileContent, relativeFilePath } = await resolveAmpSettingsFile((0, node_path.join)(outputRoot, AMP_GLOBAL_DIR));
|
|
25397
|
+
if (fileContent === null) return [];
|
|
25398
|
+
let settings;
|
|
25399
|
+
try {
|
|
25400
|
+
settings = parseAmpSettings({ fileContent });
|
|
25401
|
+
} catch (error) {
|
|
25402
|
+
logger.debug(`Skipping the Amp skills settings check for ${(0, node_path.join)(AMP_GLOBAL_DIR, relativeFilePath)}: ${formatError(error)}`);
|
|
25403
|
+
return [];
|
|
25404
|
+
}
|
|
25405
|
+
if (settings[AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY] !== true) return [];
|
|
25406
|
+
return [`Amp will not load the global skills rulesync writes to ~/${AMP_SKILLS_GLOBAL_DIR}/: '${AMP_DISABLE_GLOBAL_AGENTS_SKILLS_KEY}' is true in ~/${(0, node_path.join)(AMP_GLOBAL_DIR, relativeFilePath)}.`];
|
|
25407
|
+
}
|
|
25236
25408
|
getFrontmatter() {
|
|
25237
25409
|
return AmpSkillFrontmatterSchema.parse(this.requireMainFileFrontmatter());
|
|
25238
25410
|
}
|
|
@@ -33209,7 +33381,7 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
33209
33381
|
async convertRulesyncDirsToToolDirs(rulesyncDirs) {
|
|
33210
33382
|
const rulesyncSkills = rulesyncDirs.filter((dir) => dir instanceof RulesyncSkill);
|
|
33211
33383
|
const factory = this.getFactory(this.toolTarget);
|
|
33212
|
-
|
|
33384
|
+
const toolSkills = (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
|
|
33213
33385
|
const isClaudecodeScheduledTask = rulesyncSkill.getFrontmatter().claudecode?.["scheduled-task"] === true;
|
|
33214
33386
|
if (isClaudecodeScheduledTask && this.toolTarget !== "claudecode" && this.toolTarget !== "claudecode-legacy") return null;
|
|
33215
33387
|
if (!factory.class.isTargetedByRulesyncSkill(rulesyncSkill)) return null;
|
|
@@ -33236,6 +33408,15 @@ var SkillsProcessor = class extends DirFeatureProcessor {
|
|
|
33236
33408
|
logger: this.logger
|
|
33237
33409
|
});
|
|
33238
33410
|
}))).filter((skill) => skill !== null);
|
|
33411
|
+
if (toolSkills.length > 0 && factory.class.getGenerationWarnings) {
|
|
33412
|
+
const warnings = await factory.class.getGenerationWarnings({
|
|
33413
|
+
outputRoot: this.outputRoot,
|
|
33414
|
+
global: this.global,
|
|
33415
|
+
logger: this.logger
|
|
33416
|
+
});
|
|
33417
|
+
for (const warning of warnings) this.logger.warn(warning);
|
|
33418
|
+
}
|
|
33419
|
+
return toolSkills;
|
|
33239
33420
|
}
|
|
33240
33421
|
async convertToolDirsToRulesyncDirs(toolDirs) {
|
|
33241
33422
|
const toolSkills = toolDirs.filter((dir) => dir instanceof ToolSkill);
|
|
@@ -34123,7 +34304,7 @@ function normalizeAugmentcodeToolList(value) {
|
|
|
34123
34304
|
return tools.length > 0 ? tools : void 0;
|
|
34124
34305
|
}
|
|
34125
34306
|
const AugmentcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
|
|
34126
|
-
name: zod_mini.z.string(),
|
|
34307
|
+
name: zod_mini.z.optional(zod_mini.z.string()),
|
|
34127
34308
|
description: zod_mini.z.optional(zod_mini.z.string()),
|
|
34128
34309
|
color: zod_mini.z.optional(zod_mini.z.string()),
|
|
34129
34310
|
model: zod_mini.z.optional(zod_mini.z.string()),
|
|
@@ -34182,7 +34363,7 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
|
|
|
34182
34363
|
outputRoot: ".",
|
|
34183
34364
|
frontmatter: {
|
|
34184
34365
|
targets: ["*"],
|
|
34185
|
-
name,
|
|
34366
|
+
name: name ?? (0, node_path.basename)(this.getRelativeFilePath(), (0, node_path.extname)(this.getRelativeFilePath())),
|
|
34186
34367
|
description,
|
|
34187
34368
|
...Object.keys(augmentcodeSection).length > 0 && { augmentcode: augmentcodeSection }
|
|
34188
34369
|
},
|
|
@@ -34248,7 +34429,8 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
|
|
|
34248
34429
|
const dirPath = relativeDirPath ?? this.getSettablePaths({ global }).relativeDirPath;
|
|
34249
34430
|
const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
|
|
34250
34431
|
const fileContent = await readFileContent(filePath);
|
|
34251
|
-
const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
|
|
34432
|
+
const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
|
|
34433
|
+
if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}`);
|
|
34252
34434
|
const result = AugmentcodeSubagentFrontmatterSchema.safeParse(frontmatter);
|
|
34253
34435
|
if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
|
|
34254
34436
|
return new AugmentcodeSubagent({
|
|
@@ -41831,7 +42013,23 @@ function sanitizeOpencodeMcpNamePart(value) {
|
|
|
41831
42013
|
* key that matches nothing.
|
|
41832
42014
|
*/
|
|
41833
42015
|
function opencodeMcpToolPermissionKey(serverName, toolName) {
|
|
41834
|
-
return `${
|
|
42016
|
+
return `${opencodeMcpServerKeyPrefix(serverName)}${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
|
|
42017
|
+
}
|
|
42018
|
+
/** The sanitized `<server>_` prefix every permission key of the server's tools starts with. */
|
|
42019
|
+
function opencodeMcpServerKeyPrefix(serverName) {
|
|
42020
|
+
return `${sanitizeOpencodeMcpNamePart(serverName)}_`;
|
|
42021
|
+
}
|
|
42022
|
+
/**
|
|
42023
|
+
* Whether `key` is exactly the sanitized `<server>_` prefix of one of
|
|
42024
|
+
* `serverNames` (e.g. `a_b_` for server `a.b` or `a_b`). No tool has an empty
|
|
42025
|
+
* name, so such a key filters nothing of that server, and
|
|
42026
|
+
* {@link findOpencodeMcpToolKeyOwner} gives it no owner. Read as an unlisted
|
|
42027
|
+
* server's filter instead, it would be split at the first `_` into a phantom
|
|
42028
|
+
* server (`a` with tool `b_`), so importers leave it out of the rulesync
|
|
42029
|
+
* config, and the OpenCode family's generate keeps it in the file as it was.
|
|
42030
|
+
*/
|
|
42031
|
+
function isBareOpencodeMcpServerPrefix({ key, serverNames }) {
|
|
42032
|
+
return serverNames.some((serverName) => key === opencodeMcpServerKeyPrefix(serverName));
|
|
41835
42033
|
}
|
|
41836
42034
|
/**
|
|
41837
42035
|
* The server among `serverNames` whose tool a permission key names, with the
|
|
@@ -41845,7 +42043,7 @@ function findOpencodeMcpToolKeyOwner({ key, serverNames, builtinKeys, acceptRawP
|
|
|
41845
42043
|
if (builtinKeys.has(key)) return;
|
|
41846
42044
|
let owner;
|
|
41847
42045
|
for (const serverName of serverNames) {
|
|
41848
|
-
const sanitized =
|
|
42046
|
+
const sanitized = opencodeMcpServerKeyPrefix(serverName);
|
|
41849
42047
|
const prefixes = acceptRawPrefix ? (0, es_toolkit.uniq)([sanitized, `${serverName}_`]) : [sanitized];
|
|
41850
42048
|
for (const prefix of prefixes) if (key.length > prefix.length && key.startsWith(prefix) && (owner === void 0 || prefix.length > owner.prefix.length)) owner = {
|
|
41851
42049
|
serverName,
|
|
@@ -42306,7 +42504,10 @@ function convertFromKiloFormat(kiloMcp, tools, permission) {
|
|
|
42306
42504
|
const serverNames = Object.keys(kiloMcp);
|
|
42307
42505
|
return {
|
|
42308
42506
|
...Object.fromEntries(Object.entries(kiloMcp).map(([serverName, serverConfig]) => [serverName, kiloServerToRulesync(serverName, serverConfig, splitKiloServerTools(serverName, serverNames, filters))])),
|
|
42309
|
-
...orphanMcpToolFiltersToRulesync(kiloMcp, tools)
|
|
42507
|
+
...orphanMcpToolFiltersToRulesync(kiloMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findKiloMcpToolKeyOwner(key, serverNames) && !isBareOpencodeMcpServerPrefix({
|
|
42508
|
+
key,
|
|
42509
|
+
serverNames
|
|
42510
|
+
}))))
|
|
42310
42511
|
};
|
|
42311
42512
|
}
|
|
42312
42513
|
/**
|
|
@@ -42824,16 +43025,20 @@ function convertFromOpencodeFormat(opencodeMcp, tools, remoteType) {
|
|
|
42824
43025
|
...orphanMcpToolFiltersToRulesync(opencodeMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findOpencodeToolOwner({
|
|
42825
43026
|
key,
|
|
42826
43027
|
serverNames
|
|
43028
|
+
}) && !isBareOpencodeMcpServerPrefix({
|
|
43029
|
+
key,
|
|
43030
|
+
serverNames
|
|
42827
43031
|
})))),
|
|
42828
43032
|
...convertOpencodeServers(opencodeMcp, tools, remoteType)
|
|
42829
43033
|
};
|
|
42830
43034
|
}
|
|
43035
|
+
const OPENCODE_MCP_BUILTIN_TOOL_KEYS = /* @__PURE__ */ new Set();
|
|
42831
43036
|
/** Recognize both OpenCode's sanitized names and keys older rulesync versions wrote. */
|
|
42832
43037
|
function findOpencodeToolOwner({ key, serverNames }) {
|
|
42833
43038
|
return findOpencodeMcpToolKeyOwner({
|
|
42834
43039
|
key,
|
|
42835
43040
|
serverNames,
|
|
42836
|
-
builtinKeys:
|
|
43041
|
+
builtinKeys: OPENCODE_MCP_BUILTIN_TOOL_KEYS,
|
|
42837
43042
|
acceptRawPrefix: true
|
|
42838
43043
|
});
|
|
42839
43044
|
}
|
|
@@ -43003,6 +43208,40 @@ function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
|
|
|
43003
43208
|
tools
|
|
43004
43209
|
};
|
|
43005
43210
|
}
|
|
43211
|
+
/**
|
|
43212
|
+
* Copy each key of `existingTools` that is a bare sanitized `<server>_` prefix
|
|
43213
|
+
* of one of `serverNames` into `tools`, unless a generated filter already
|
|
43214
|
+
* writes the same key or another listed server owns it (`a_b_` is tool `b_` of
|
|
43215
|
+
* server `a` when `a` is listed too). An owned key is the rulesync config's to
|
|
43216
|
+
* write, so carrying it over would keep a filter the user removed. Mutates
|
|
43217
|
+
* `tools` in place.
|
|
43218
|
+
*/
|
|
43219
|
+
function preserveBareOpencodeMcpServerPrefixKeys({ tools, existingTools, serverNames }) {
|
|
43220
|
+
for (const [key, enabled] of Object.entries(existingTools ?? {})) if (!Object.hasOwn(tools, key) && isBareOpencodeMcpServerPrefix({
|
|
43221
|
+
key,
|
|
43222
|
+
serverNames
|
|
43223
|
+
}) && !findOpencodeToolOwner({
|
|
43224
|
+
key,
|
|
43225
|
+
serverNames
|
|
43226
|
+
})) tools[key] = enabled;
|
|
43227
|
+
}
|
|
43228
|
+
/**
|
|
43229
|
+
* When the existing file does not pass the schema, its `tools` map cannot be
|
|
43230
|
+
* read, so a bare `<server>_` key in it is not carried over. Say so rather
|
|
43231
|
+
* than lift the deny it may hold without a word.
|
|
43232
|
+
*/
|
|
43233
|
+
function warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({ existingJson, serverNames, logger }) {
|
|
43234
|
+
const rawTools = isRecord(existingJson) ? existingJson.tools : void 0;
|
|
43235
|
+
if (!isRecord(rawTools)) return;
|
|
43236
|
+
const bareKeys = Object.keys(rawTools).filter((key) => isBareOpencodeMcpServerPrefix({
|
|
43237
|
+
key,
|
|
43238
|
+
serverNames
|
|
43239
|
+
}) && !findOpencodeToolOwner({
|
|
43240
|
+
key,
|
|
43241
|
+
serverNames
|
|
43242
|
+
}));
|
|
43243
|
+
if (bareKeys.length > 0) logger?.warn(`OpenCode MCP: the existing config does not match the expected schema, so the bare server-prefix tool ${bareKeys.length === 1 ? "key" : "keys"} ${bareKeys.map(quoteValueForWarning).join(", ")} ${bareKeys.length === 1 ? "is" : "are"} not kept in "tools".`);
|
|
43244
|
+
}
|
|
43006
43245
|
const OpencodeMcpFeatureOptionsSchema = zod_mini.z.looseObject({ configShape: zod_mini.z.optional(zod_mini.z.enum(["v1", "v2"])) });
|
|
43007
43246
|
var OpencodeMcp = class extends ToolMcp {
|
|
43008
43247
|
/** Directory layout; OpenCode forks (MiMo Code) override it. */
|
|
@@ -43135,6 +43374,16 @@ var OpencodeMcp = class extends ToolMcp {
|
|
|
43135
43374
|
convertedMcp,
|
|
43136
43375
|
mcpServers: transformedServers
|
|
43137
43376
|
});
|
|
43377
|
+
if (existingMcp.success) preserveBareOpencodeMcpServerPrefixKeys({
|
|
43378
|
+
tools: mcpTools,
|
|
43379
|
+
existingTools: existingMcp.data.tools,
|
|
43380
|
+
serverNames: Object.keys(transformedServers)
|
|
43381
|
+
});
|
|
43382
|
+
else warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({
|
|
43383
|
+
existingJson: this.lowerConfig(existingJson),
|
|
43384
|
+
serverNames: Object.keys(transformedServers),
|
|
43385
|
+
logger
|
|
43386
|
+
});
|
|
43138
43387
|
if (this.layout.configVersion === "v2") warnAboutOpencodeV2SseServers({
|
|
43139
43388
|
mcpServers: transformedServers,
|
|
43140
43389
|
convertedMcp,
|
|
@@ -47373,6 +47622,45 @@ var DshRule = class DshRule extends NestedAgentsmdRule {
|
|
|
47373
47622
|
};
|
|
47374
47623
|
}
|
|
47375
47624
|
};
|
|
47625
|
+
// src/features/rules/output-style-paths.ts
|
|
47626
|
+
/**
|
|
47627
|
+
* Shared helpers for tool rule adapters that write opted-in rules as custom
|
|
47628
|
+
* output styles (`<tool dir>/output-styles/<name>.md`), e.g. Qoder and
|
|
47629
|
+
* Factory Droid. Both tools read only the direct `.md` children of their
|
|
47630
|
+
* output-styles directory, with optional `name` / `description` frontmatter.
|
|
47631
|
+
*/
|
|
47632
|
+
/**
|
|
47633
|
+
* Whether the given path is a direct `.md` child of `outputStylesDirPath`.
|
|
47634
|
+
*/
|
|
47635
|
+
function isDirectMarkdownChild({ outputStylesDirPath, relativeDirPath, relativeFilePath }) {
|
|
47636
|
+
return relativeDirPath === outputStylesDirPath && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
|
|
47637
|
+
}
|
|
47638
|
+
/**
|
|
47639
|
+
* Whether a rule opts into the `output-style` channel: only a non-root rule
|
|
47640
|
+
* can, since an output style is a file of its own.
|
|
47641
|
+
*/
|
|
47642
|
+
function isOutputStyleChannel({ root, channel }) {
|
|
47643
|
+
return !root && channel === "output-style";
|
|
47644
|
+
}
|
|
47645
|
+
/**
|
|
47646
|
+
* Return the file stem of an output-style source rule, throwing `message`
|
|
47647
|
+
* when the rule is nested in a subdirectory of the rules directory: the
|
|
47648
|
+
* output file is named after the rule, and the tools read only the top level
|
|
47649
|
+
* of their output-styles directory.
|
|
47650
|
+
*/
|
|
47651
|
+
function resolveTopLevelOutputStyleStem({ sourcePath, message }) {
|
|
47652
|
+
if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(message);
|
|
47653
|
+
return (0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath));
|
|
47654
|
+
}
|
|
47655
|
+
/**
|
|
47656
|
+
* The output style's frontmatter: only the `name` and `description` that are set.
|
|
47657
|
+
*/
|
|
47658
|
+
function buildOutputStyleFrontmatter({ name, description }) {
|
|
47659
|
+
return {
|
|
47660
|
+
...name !== void 0 && { name },
|
|
47661
|
+
...description !== void 0 && { description }
|
|
47662
|
+
};
|
|
47663
|
+
}
|
|
47376
47664
|
// src/features/rules/factorydroid-rule.ts
|
|
47377
47665
|
/**
|
|
47378
47666
|
* The file-name glob `getExtraFixedFiles` returns for the output-styles
|
|
@@ -47477,7 +47765,11 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
47477
47765
|
* child of the output-styles directory, at either scope.
|
|
47478
47766
|
*/
|
|
47479
47767
|
static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
|
|
47480
|
-
return
|
|
47768
|
+
return isDirectMarkdownChild({
|
|
47769
|
+
outputStylesDirPath: this.getOutputStylesDirPath(),
|
|
47770
|
+
relativeDirPath,
|
|
47771
|
+
relativeFilePath
|
|
47772
|
+
});
|
|
47481
47773
|
}
|
|
47482
47774
|
/**
|
|
47483
47775
|
* Which fixed-file channel, if any, owns the given output path. Matching on
|
|
@@ -47517,7 +47809,10 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
47517
47809
|
/** Whether a rule opts into the `output-style` channel (non-root only). */
|
|
47518
47810
|
static isOutputStyleRule(rulesyncRule) {
|
|
47519
47811
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
47520
|
-
return
|
|
47812
|
+
return isOutputStyleChannel({
|
|
47813
|
+
root: frontmatter.root,
|
|
47814
|
+
channel: frontmatter.factorydroid?.channel
|
|
47815
|
+
});
|
|
47521
47816
|
}
|
|
47522
47817
|
/**
|
|
47523
47818
|
* Factory Droid loads the channel files itself, so listing one in the root
|
|
@@ -47649,15 +47944,17 @@ var FactorydroidRule = class FactorydroidRule extends ToolRule {
|
|
|
47649
47944
|
static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
|
|
47650
47945
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
47651
47946
|
const sourcePath = rulesyncRule.getRelativeFilePath();
|
|
47652
|
-
|
|
47653
|
-
|
|
47947
|
+
const stem = resolveTopLevelOutputStyleStem({
|
|
47948
|
+
sourcePath,
|
|
47949
|
+
message: `Rule '${sourcePath}' sets factorydroid.channel: output-style but is nested in a subdirectory: Factory Droid loads only direct .md children of .factory/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
|
|
47950
|
+
});
|
|
47654
47951
|
const name = frontmatter.factorydroid?.name;
|
|
47655
47952
|
const styleName = name ?? stem;
|
|
47656
47953
|
if (FACTORYDROID_RESERVED_OUTPUT_STYLE_NAMES.includes(styleName.trim().toLowerCase())) throw new Error(`Rule '${sourcePath}' sets factorydroid.channel: output-style with the style name '${styleName}', which is reserved: Factory Droid's built-in Default and Concise styles cannot be replaced. Rename the rule file or set factorydroid.name.`);
|
|
47657
|
-
const styleFrontmatter = {
|
|
47658
|
-
|
|
47659
|
-
|
|
47660
|
-
};
|
|
47954
|
+
const styleFrontmatter = buildOutputStyleFrontmatter({
|
|
47955
|
+
name,
|
|
47956
|
+
description: frontmatter.description
|
|
47957
|
+
});
|
|
47661
47958
|
const body = rulesyncRule.getBody();
|
|
47662
47959
|
return new FactorydroidRule({
|
|
47663
47960
|
outputRoot,
|
|
@@ -49293,12 +49590,19 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
49293
49590
|
* `.qoder/output-styles/`, at either scope.
|
|
49294
49591
|
*/
|
|
49295
49592
|
static isOutputStylePath({ relativeDirPath, relativeFilePath }) {
|
|
49296
|
-
return
|
|
49593
|
+
return isDirectMarkdownChild({
|
|
49594
|
+
outputStylesDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
49595
|
+
relativeDirPath,
|
|
49596
|
+
relativeFilePath
|
|
49597
|
+
});
|
|
49297
49598
|
}
|
|
49298
49599
|
/** Whether a rule opts into the `output-style` channel (non-root only). */
|
|
49299
49600
|
static isOutputStyleRule(rulesyncRule) {
|
|
49300
49601
|
const frontmatter = rulesyncRule.getFrontmatter();
|
|
49301
|
-
return
|
|
49602
|
+
return isOutputStyleChannel({
|
|
49603
|
+
root: frontmatter.root,
|
|
49604
|
+
channel: frontmatter.qoder?.channel
|
|
49605
|
+
});
|
|
49302
49606
|
}
|
|
49303
49607
|
/**
|
|
49304
49608
|
* The output-styles directory, enumerated for import only: it also holds
|
|
@@ -49409,16 +49713,18 @@ var QoderRule = class QoderRule extends ToolRule {
|
|
|
49409
49713
|
static buildOutputStyle({ outputRoot, rulesyncRule, validate }) {
|
|
49410
49714
|
const { description, qoder } = rulesyncRule.getFrontmatter();
|
|
49411
49715
|
const sourcePath = rulesyncRule.getRelativeFilePath();
|
|
49412
|
-
|
|
49413
|
-
|
|
49716
|
+
const stem = resolveTopLevelOutputStyleStem({
|
|
49717
|
+
sourcePath,
|
|
49718
|
+
message: `Rule '${sourcePath}' sets qoder.channel: output-style but is nested in a subdirectory: Qoder reads only the files directly in .qoder/output-styles/, so move it to the top of ${RULESYNC_RULES_RELATIVE_DIR_PATH}.`
|
|
49719
|
+
});
|
|
49414
49720
|
return new QoderRule({
|
|
49415
49721
|
outputRoot,
|
|
49416
49722
|
relativeDirPath: QODER_OUTPUT_STYLES_DIR_PATH,
|
|
49417
|
-
relativeFilePath: `${
|
|
49418
|
-
frontmatter: {
|
|
49419
|
-
|
|
49420
|
-
|
|
49421
|
-
},
|
|
49723
|
+
relativeFilePath: `${stem}.md`,
|
|
49724
|
+
frontmatter: buildOutputStyleFrontmatter({
|
|
49725
|
+
name: qoder?.name,
|
|
49726
|
+
description
|
|
49727
|
+
}),
|
|
49422
49728
|
body: rulesyncRule.getBody(),
|
|
49423
49729
|
validate,
|
|
49424
49730
|
root: false,
|
|
@@ -51689,6 +51995,7 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
51689
51995
|
factory
|
|
51690
51996
|
});
|
|
51691
51997
|
await this.warnForWarpLegacyRootFile(toolRules);
|
|
51998
|
+
this.warnForAugmentcodePluginRules(toolRules);
|
|
51692
51999
|
await this.warnForInstructionBudget({
|
|
51693
52000
|
toolRules,
|
|
51694
52001
|
meta
|
|
@@ -52080,6 +52387,20 @@ var RulesProcessor = class extends FeatureProcessor {
|
|
|
52080
52387
|
this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
|
|
52081
52388
|
}
|
|
52082
52389
|
/**
|
|
52390
|
+
* Auggie 1.x (still a prerelease on npm at the time of writing; the stable
|
|
52391
|
+
* line is 0.36.0) no longer loads `rules/` from plugins: "Rules shipped in
|
|
52392
|
+
* plugins (`rules/` in a plugin)" is listed among the removed features, while
|
|
52393
|
+
* commands, agents, skills, hooks and MCP servers still load. The rule files
|
|
52394
|
+
* are still written, because the stable 0.x CLI reads them, but under 1.x the
|
|
52395
|
+
* bundle silently loses them, so say so.
|
|
52396
|
+
* @see https://docs.augmentcode.com/cli/whats-new
|
|
52397
|
+
* @see https://docs.augmentcode.com/cli/v0/plugins
|
|
52398
|
+
*/
|
|
52399
|
+
warnForAugmentcodePluginRules(toolRules) {
|
|
52400
|
+
if (this.toolTarget !== "augmentcode-plugin" || toolRules.length === 0) return;
|
|
52401
|
+
this.logger.warn(`augmentcode-plugin: plugin rules (rules/*.md) are loaded by Auggie 0.x only; Auggie 1.x no longer reads rules/ from plugins, so these ${toolRules.length} rule file(s) have no effect there. Ship instructions that must reach Auggie 1.x users as a skill, or have them generate the augmentcode target in their project.`);
|
|
52402
|
+
}
|
|
52403
|
+
/**
|
|
52083
52404
|
* Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
|
|
52084
52405
|
* `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so an
|
|
52085
52406
|
* `AGENTS.md` this run writes next to one would never be read. That holds for
|
|
@@ -56081,9 +56402,9 @@ function groupDefinitionsByMatcher({ definitions, eventName, converterConfig, wa
|
|
|
56081
56402
|
});
|
|
56082
56403
|
translatedMatchers.set(normalizedMatcher, matcher);
|
|
56083
56404
|
}
|
|
56084
|
-
const key = [matcher, ...subdividingFields.map(({ canonical, valueType }) => {
|
|
56405
|
+
const key = [matcher, ...subdividingFields.map(({ canonical, valueType, events }) => {
|
|
56085
56406
|
const value = def[canonical];
|
|
56086
|
-
return isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
56407
|
+
return isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(value, valueType) ? stableJson(value) : "";
|
|
56087
56408
|
})].join("\0");
|
|
56088
56409
|
const group = byMatcher.get(key);
|
|
56089
56410
|
if (group) group.defs.push(def);
|
|
@@ -56124,6 +56445,7 @@ function runsInPosixShell({ shell, converterConfig }) {
|
|
|
56124
56445
|
* Apply the optional project directory variable prefix to a command string.
|
|
56125
56446
|
*/
|
|
56126
56447
|
function applyCommandPrefix({ def, converterConfig }) {
|
|
56448
|
+
if (converterConfig.stripsProjectDirVarOnImportOnly) return def.command;
|
|
56127
56449
|
const commandText = def.command;
|
|
56128
56450
|
const trimmedCommand = typeof commandText === "string" ? commandText.trimStart() : void 0;
|
|
56129
56451
|
const unquotedCommand = trimmedCommand?.replace(/^["']/, "");
|
|
@@ -56332,12 +56654,16 @@ const describeInvalidObject = ({ tool, canonical, value }) => isPlainObject(valu
|
|
|
56332
56654
|
value
|
|
56333
56655
|
})} Importing it would fail validation on the next run.` : `Dropping "${tool}" while importing a hook: it must be an object.`;
|
|
56334
56656
|
const describeInvalidRecord = ({ tool }) => `Dropping "${tool}" while importing a hook: it must be a map of strings whose keys are non-empty and free of "=", and whose keys and values carry no newline, carriage return or NUL characters.`;
|
|
56657
|
+
function isGroupFieldOnEvent(events, eventName) {
|
|
56658
|
+
return events === void 0 || events.has(eventName);
|
|
56659
|
+
}
|
|
56335
56660
|
/**
|
|
56336
56661
|
* Check a value against the shape its field documents. A string field also
|
|
56337
56662
|
* rejects control characters, matching the canonical `safeString` so an
|
|
56338
56663
|
* imported value cannot fail validation on the next generate.
|
|
56339
56664
|
*/
|
|
56340
56665
|
function isGroupPassthroughValue(value, valueType = "object") {
|
|
56666
|
+
if (valueType === "number") return typeof value === "number" && Number.isFinite(value);
|
|
56341
56667
|
if (valueType === "string") return typeof value === "string" && !CONTROL_CHARS.some((char) => value.includes(char));
|
|
56342
56668
|
return isPlainObject(value);
|
|
56343
56669
|
}
|
|
@@ -56347,10 +56673,14 @@ function isGroupPassthroughValue(value, valueType = "object") {
|
|
|
56347
56673
|
*/
|
|
56348
56674
|
function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }) {
|
|
56349
56675
|
const emitted = {};
|
|
56350
|
-
for (const { canonical, tool, valueType } of converterConfig.groupPassthroughFields ?? []) {
|
|
56676
|
+
for (const { canonical, tool, valueType, events } of converterConfig.groupPassthroughFields ?? []) {
|
|
56351
56677
|
const carried = defs.map((def) => def[canonical]);
|
|
56352
56678
|
const first = carried.find((value) => isGroupPassthroughValue(value, valueType));
|
|
56353
56679
|
if (first === void 0) continue;
|
|
56680
|
+
if (!isGroupFieldOnEvent(events, eventName)) {
|
|
56681
|
+
logger?.warn(`"${canonical}" on "${eventName}" hooks is ignored — the tool supports "${tool}" only on ${[...events ?? []].map((event) => `"${event}"`).join(", ")} hooks.`);
|
|
56682
|
+
continue;
|
|
56683
|
+
}
|
|
56354
56684
|
const firstStable = stableJson(first);
|
|
56355
56685
|
const agrees = (value) => isGroupPassthroughValue(value, valueType) && stableJson(value) === firstStable;
|
|
56356
56686
|
if (!carried.every(agrees)) logger?.warn(`"${tool}" belongs to the whole matcher group on "${eventName}" hooks, so every hook in this group gets ${quoteValueForWarning(first)} — including any that asked for something else, or for nothing.`);
|
|
@@ -56362,9 +56692,9 @@ function emitGroupPassthroughFields({ defs, eventName, converterConfig, logger }
|
|
|
56362
56692
|
* Import the configured group-level passthrough fields onto a definition,
|
|
56363
56693
|
* reversing {@link emitGroupPassthroughFields}.
|
|
56364
56694
|
*/
|
|
56365
|
-
function importGroupPassthroughFields({ rawEntry, converterConfig }) {
|
|
56695
|
+
function importGroupPassthroughFields({ rawEntry, eventName, converterConfig }) {
|
|
56366
56696
|
const entry = rawEntry;
|
|
56367
|
-
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType }) => isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
56697
|
+
return Object.fromEntries((converterConfig.groupPassthroughFields ?? []).filter(({ tool, valueType, events }) => isGroupFieldOnEvent(events, eventName) && isGroupPassthroughValue(entry[tool], valueType)).map(({ canonical, tool }) => [canonical, entry[tool]]));
|
|
56368
56698
|
}
|
|
56369
56699
|
/**
|
|
56370
56700
|
* Emit the payload fields specific to a hook type — `url`/`headers`/
|
|
@@ -56690,19 +57020,23 @@ function stripCommandPrefix({ command, args, shell, converterConfig, warn }) {
|
|
|
56690
57020
|
const cmd = typeof command === "string" ? command : void 0;
|
|
56691
57021
|
const { projectDirVar } = converterConfig;
|
|
56692
57022
|
if (projectDirVar === "" || typeof cmd !== "string") return cmd;
|
|
56693
|
-
|
|
56694
|
-
|
|
56695
|
-
|
|
56696
|
-
|
|
56697
|
-
|
|
56698
|
-
|
|
56699
|
-
|
|
56700
|
-
|
|
56701
|
-
|
|
56702
|
-
|
|
56703
|
-
|
|
56704
|
-
|
|
56705
|
-
|
|
57023
|
+
let stripped = cmd;
|
|
57024
|
+
for (const variable of [projectDirVar, ...converterConfig.importedProjectDirVarAliases ?? []]) {
|
|
57025
|
+
const result = importProjectDirVariable({
|
|
57026
|
+
command: stripped,
|
|
57027
|
+
projectDirVar: variable,
|
|
57028
|
+
firstWordOnly: isExecFormHook({
|
|
57029
|
+
args,
|
|
57030
|
+
converterConfig
|
|
57031
|
+
}) || !runsInPosixShell({
|
|
57032
|
+
shell,
|
|
57033
|
+
converterConfig
|
|
57034
|
+
})
|
|
57035
|
+
});
|
|
57036
|
+
stripped = result.command;
|
|
57037
|
+
if (warn && result.unrestored && !converterConfig.stripsProjectDirVarOnImportOnly) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${variable} path that is not a script the command runs becomes relative to the hook's working directory and is not restored on generate. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") to keep the variable.`);
|
|
57038
|
+
if (warn && result.retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${variable} in an assignment or option value, or in a later word of a command whose later words are not normalized, so the imported command still refers to it and other tools that do not define it receive it verbatim. Put the exact command in a tool-specific hooks override (such as "claudecode.hooks") if other tools should not see it.`);
|
|
57039
|
+
}
|
|
56706
57040
|
return stripped;
|
|
56707
57041
|
}
|
|
56708
57042
|
/**
|
|
@@ -56743,7 +57077,7 @@ function isStringRecord(value) {
|
|
|
56743
57077
|
}
|
|
56744
57078
|
/** Compare object values without letting key order decide the answer. */
|
|
56745
57079
|
function stableJson(value) {
|
|
56746
|
-
if (typeof value
|
|
57080
|
+
if (typeof value !== "object") return JSON.stringify(value);
|
|
56747
57081
|
return JSON.stringify(Object.fromEntries(Object.entries(value).toSorted(([a], [b]) => a.localeCompare(b))));
|
|
56748
57082
|
}
|
|
56749
57083
|
/**
|
|
@@ -56983,6 +57317,7 @@ function toolHookToCanonical({ h, rawEntry, eventName, converterConfig, warn })
|
|
|
56983
57317
|
}),
|
|
56984
57318
|
...importGroupPassthroughFields({
|
|
56985
57319
|
rawEntry,
|
|
57320
|
+
eventName,
|
|
56986
57321
|
converterConfig
|
|
56987
57322
|
}),
|
|
56988
57323
|
...matcher !== void 0 && matcher !== null && matcher !== "" && { matcher }
|
|
@@ -57035,15 +57370,26 @@ function definingFields({ h, rawEntry, hookType, converterConfig }) {
|
|
|
57035
57370
|
* is the group-level twin of `matcher`: importing the group without it would
|
|
57036
57371
|
* widen every hook in it, so the group is skipped instead.
|
|
57037
57372
|
*/
|
|
57038
|
-
function describeGroupSkipReason({ rawEntry, converterConfig }) {
|
|
57373
|
+
function describeGroupSkipReason({ rawEntry, eventName, converterConfig }) {
|
|
57039
57374
|
const entry = rawEntry;
|
|
57040
|
-
for (const { tool, valueType, subdividesGroup } of converterConfig.groupPassthroughFields ?? []) {
|
|
57375
|
+
for (const { tool, valueType, subdividesGroup, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
|
|
57041
57376
|
const value = entry[tool];
|
|
57042
|
-
if (subdividesGroup !== true || value === void 0) continue;
|
|
57377
|
+
if (subdividesGroup !== true || onInvalidImport === "drop-field" || value === void 0 || !isGroupFieldOnEvent(events, eventName)) continue;
|
|
57043
57378
|
if (!isGroupPassthroughValue(value, valueType)) return `Skipping the hooks of a matcher group while importing: its "${tool}" (${quoteValueForWarning(value)}) is unusable, and these hooks run only where it matches. Importing them without it would widen when they fire, so they are skipped.`;
|
|
57044
57379
|
}
|
|
57045
57380
|
}
|
|
57046
57381
|
/**
|
|
57382
|
+
* Warn about each `"drop-field"` group value that is present but unusable;
|
|
57383
|
+
* {@link importGroupPassthroughFields} leaves it off the imported hooks.
|
|
57384
|
+
*/
|
|
57385
|
+
function warnDroppedGroupFields({ rawEntry, eventName, converterConfig, warn }) {
|
|
57386
|
+
const entry = rawEntry;
|
|
57387
|
+
for (const { tool, valueType, events, onInvalidImport } of converterConfig.groupPassthroughFields ?? []) {
|
|
57388
|
+
const value = entry[tool];
|
|
57389
|
+
if (onInvalidImport === "drop-field" && value !== void 0 && isGroupFieldOnEvent(events, eventName) && !isGroupPassthroughValue(value, valueType)) warn?.(`Dropping the unusable "${tool}" (${quoteValueForWarning(value)}) of a "${eventName}" matcher group while importing; its hooks are imported without it.`);
|
|
57390
|
+
}
|
|
57391
|
+
}
|
|
57392
|
+
/**
|
|
57047
57393
|
* Why this hook cannot be imported, or `undefined` when it can.
|
|
57048
57394
|
*/
|
|
57049
57395
|
function describeHookSkipReason({ h, rawEntry, hookType, converterConfig }) {
|
|
@@ -57070,12 +57416,19 @@ function toolMatcherEntryToCanonical({ rawEntry, eventName, converterConfig, war
|
|
|
57070
57416
|
const hookDefs = rawEntry.hooks ?? [];
|
|
57071
57417
|
const groupSkipReason = describeGroupSkipReason({
|
|
57072
57418
|
rawEntry,
|
|
57419
|
+
eventName,
|
|
57073
57420
|
converterConfig
|
|
57074
57421
|
});
|
|
57075
57422
|
if (groupSkipReason !== void 0) {
|
|
57076
57423
|
warn?.(groupSkipReason);
|
|
57077
57424
|
return [];
|
|
57078
57425
|
}
|
|
57426
|
+
warnDroppedGroupFields({
|
|
57427
|
+
rawEntry,
|
|
57428
|
+
eventName,
|
|
57429
|
+
converterConfig,
|
|
57430
|
+
warn
|
|
57431
|
+
});
|
|
57079
57432
|
const definitions = [];
|
|
57080
57433
|
for (const h of hookDefs) {
|
|
57081
57434
|
const skipReason = describeHookSkipReason({
|
|
@@ -60414,6 +60767,23 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
|
|
|
60414
60767
|
});
|
|
60415
60768
|
}
|
|
60416
60769
|
};
|
|
60770
|
+
// src/features/hooks/hook-matcher-tool-names.ts
|
|
60771
|
+
const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
|
|
60772
|
+
function isMatcherName(token) {
|
|
60773
|
+
return !token.startsWith("\\") && !token.startsWith("[");
|
|
60774
|
+
}
|
|
60775
|
+
/**
|
|
60776
|
+
* Rewrites the whole tool names inside a hook `matcher` regex through `names`,
|
|
60777
|
+
* leaving escapes, character classes and unknown names as written. Used by
|
|
60778
|
+
* adapters whose runtime tool names differ from the canonical (Claude Code)
|
|
60779
|
+
* spelling.
|
|
60780
|
+
*/
|
|
60781
|
+
function replaceMatcherNames(matcher, names) {
|
|
60782
|
+
return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
|
|
60783
|
+
}
|
|
60784
|
+
function invertToolNames(names) {
|
|
60785
|
+
return Object.fromEntries(Object.entries(names).map(([canonical, tool]) => [tool, canonical]));
|
|
60786
|
+
}
|
|
60417
60787
|
// src/features/hooks/cortexcode-hooks.ts
|
|
60418
60788
|
const CORTEXCODE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set(["beforeSubmitPrompt", "stop"]);
|
|
60419
60789
|
const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
|
|
@@ -60424,14 +60794,7 @@ const CANONICAL_TO_CORTEXCODE_TOOL_NAMES = {
|
|
|
60424
60794
|
Grep: "grep",
|
|
60425
60795
|
Glob: "glob"
|
|
60426
60796
|
};
|
|
60427
|
-
const CORTEXCODE_TO_CANONICAL_TOOL_NAMES =
|
|
60428
|
-
const MATCHER_TOKEN_PATTERN = /\\.|\[(?:\\.|[^\]\\])*\]|(?<=^|[^\w\\]|\\\w)[A-Za-z_]\w*(?!\w)/gs;
|
|
60429
|
-
function isMatcherName(token) {
|
|
60430
|
-
return !token.startsWith("\\") && !token.startsWith("[");
|
|
60431
|
-
}
|
|
60432
|
-
function replaceMatcherNames(matcher, names) {
|
|
60433
|
-
return matcher.replace(MATCHER_TOKEN_PATTERN, (token) => isMatcherName(token) && Object.hasOwn(names, token) ? names[token] ?? token : token);
|
|
60434
|
-
}
|
|
60797
|
+
const CORTEXCODE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
|
|
60435
60798
|
function canonicalToCortexcodeMatcher({ matcher, eventName, warn }) {
|
|
60436
60799
|
const translated = replaceMatcherNames(matcher, CANONICAL_TO_CORTEXCODE_TOOL_NAMES);
|
|
60437
60800
|
const unmatched = [...translated.matchAll(MATCHER_TOKEN_PATTERN)].map(([token]) => token).filter((token) => isMatcherName(token) && /^[A-Z]/.test(token));
|
|
@@ -65592,6 +65955,61 @@ var TabnineHooks = class TabnineHooks extends ToolHooks {
|
|
|
65592
65955
|
});
|
|
65593
65956
|
}
|
|
65594
65957
|
};
|
|
65958
|
+
// src/features/hooks/trae-hooks.ts
|
|
65959
|
+
const TRAE_NO_MATCHER_EVENTS = /* @__PURE__ */ new Set([
|
|
65960
|
+
"sessionStart",
|
|
65961
|
+
"beforeSubmitPrompt",
|
|
65962
|
+
"stop"
|
|
65963
|
+
]);
|
|
65964
|
+
const CANONICAL_TO_TRAE_TOOL_NAMES = { Bash: "RunCommand" };
|
|
65965
|
+
const TRAE_TO_CANONICAL_TOOL_NAMES = invertToolNames(CANONICAL_TO_TRAE_TOOL_NAMES);
|
|
65966
|
+
const TRAE_SPEC = {
|
|
65967
|
+
displayName: "TRAE",
|
|
65968
|
+
overrideKey: "trae",
|
|
65969
|
+
converterConfig: {
|
|
65970
|
+
supportedEvents: TRAE_HOOK_EVENTS,
|
|
65971
|
+
canonicalToToolEventNames: CANONICAL_TO_TRAE_EVENT_NAMES,
|
|
65972
|
+
toolToCanonicalEventNames: TRAE_TO_CANONICAL_EVENT_NAMES,
|
|
65973
|
+
projectDirVar: "$TRAE_PROJECT_DIR",
|
|
65974
|
+
stripsProjectDirVarOnImportOnly: true,
|
|
65975
|
+
importedProjectDirVarAliases: ["$CLAUDE_PROJECT_DIR"],
|
|
65976
|
+
noMatcherEvents: TRAE_NO_MATCHER_EVENTS,
|
|
65977
|
+
supportedHookTypes: /* @__PURE__ */ new Set(["command"]),
|
|
65978
|
+
groupPassthroughFields: [{
|
|
65979
|
+
canonical: "loop_limit",
|
|
65980
|
+
tool: "loop_limit",
|
|
65981
|
+
valueType: "number",
|
|
65982
|
+
events: /* @__PURE__ */ new Set(["stop"]),
|
|
65983
|
+
subdividesGroup: true,
|
|
65984
|
+
onInvalidImport: "drop-field"
|
|
65985
|
+
}],
|
|
65986
|
+
matcherTranslation: {
|
|
65987
|
+
toTool: ({ matcher }) => replaceMatcherNames(matcher, CANONICAL_TO_TRAE_TOOL_NAMES),
|
|
65988
|
+
toCanonical: (matcher) => replaceMatcherNames(matcher, TRAE_TO_CANONICAL_TOOL_NAMES)
|
|
65989
|
+
}
|
|
65990
|
+
}
|
|
65991
|
+
};
|
|
65992
|
+
/**
|
|
65993
|
+
* TRAE hooks.
|
|
65994
|
+
*
|
|
65995
|
+
* Hooks live under the top-level `hooks` key of the dedicated `.trae/hooks.json`
|
|
65996
|
+
* (project) and `~/.trae/hooks.json` (user scope). The IDE also writes a
|
|
65997
|
+
* `version` key there, so generation merges the `hooks` key into the file (see
|
|
65998
|
+
* `SHARED_CONFIG_OWNERSHIP`) instead of overwriting it.
|
|
65999
|
+
*
|
|
66000
|
+
* @see https://docs.trae.ai/ide/hook-configuration-reference?_lang=en
|
|
66001
|
+
*/
|
|
66002
|
+
var TraeHooks = class extends SettingsJsonHooks {
|
|
66003
|
+
static getSpec() {
|
|
66004
|
+
return TRAE_SPEC;
|
|
66005
|
+
}
|
|
66006
|
+
static getSettablePaths(_options = {}) {
|
|
66007
|
+
return {
|
|
66008
|
+
relativeDirPath: TRAE_DIR,
|
|
66009
|
+
relativeFilePath: TRAE_HOOKS_FILE_NAME
|
|
66010
|
+
};
|
|
66011
|
+
}
|
|
66012
|
+
};
|
|
65595
66013
|
// src/features/hooks/vibe-hooks.ts
|
|
65596
66014
|
const VIBE_DIR = ".vibe";
|
|
65597
66015
|
const VIBE_HOOKS_FILE_NAME = "hooks.toml";
|
|
@@ -66776,6 +67194,21 @@ const toolHooksFactories = /* @__PURE__ */ new Map([
|
|
|
66776
67194
|
supportedHookTypes: ["command"],
|
|
66777
67195
|
supportsMatcher: true
|
|
66778
67196
|
}],
|
|
67197
|
+
["trae", {
|
|
67198
|
+
class: TraeHooks,
|
|
67199
|
+
meta: {
|
|
67200
|
+
supportsProject: true,
|
|
67201
|
+
supportsGlobal: true,
|
|
67202
|
+
supportsImport: true
|
|
67203
|
+
},
|
|
67204
|
+
...converterCapabilities(TraeHooks.getConverterConfig()),
|
|
67205
|
+
supportsMatcher: true,
|
|
67206
|
+
matcherEvents: [
|
|
67207
|
+
"preToolUse",
|
|
67208
|
+
"postToolUse",
|
|
67209
|
+
"notification"
|
|
67210
|
+
]
|
|
67211
|
+
}],
|
|
66779
67212
|
["vibe", {
|
|
66780
67213
|
class: VibeHooks,
|
|
66781
67214
|
meta: {
|
|
@@ -68884,69 +69317,6 @@ var AiassistantMcp = class extends ToolMcp {
|
|
|
68884
69317
|
});
|
|
68885
69318
|
}
|
|
68886
69319
|
};
|
|
68887
|
-
// src/features/shared/amp-settings.ts
|
|
68888
|
-
/**
|
|
68889
|
-
* Parse an Amp `settings.json` / `settings.jsonc` document.
|
|
68890
|
-
*
|
|
68891
|
-
* Shared by the MCP and permissions adapters because both read the very same
|
|
68892
|
-
* file. Two separately maintained copies drifted once already: the permissions
|
|
68893
|
-
* side was hardened while the MCP side kept calling `jsonc-parser` directly,
|
|
68894
|
-
* which left it importing MCP servers reachable only through an injected
|
|
68895
|
-
* `__proto__`.
|
|
68896
|
-
*
|
|
68897
|
-
* {@link parseJsonc} is the strict, sanitizing parser: it throws on any syntax
|
|
68898
|
-
* error instead of returning `jsonc-parser`'s best-effort value, and it
|
|
68899
|
-
* rebuilds every object from its own enumerable entries, so a root `__proto__`
|
|
68900
|
-
* cannot swap the returned object's prototype and `constructor` / `prototype`
|
|
68901
|
-
* never survive as own keys, at any depth.
|
|
68902
|
-
*
|
|
68903
|
-
* Removal is silent, matching every other tool-side adapter (opencode, kilo,
|
|
68904
|
-
* copilot). Reporting the removed keys through `droppedPollutionKeysError` is
|
|
68905
|
-
* reserved for the three files a user authors under `.rulesync/`, whose whole
|
|
68906
|
-
* purpose is to be turned into tool config — a key that vanishes there needs
|
|
68907
|
-
* explaining, whereas here the surrounding settings are the user's own file
|
|
68908
|
-
* and are left alone.
|
|
68909
|
-
*
|
|
68910
|
-
* Note the consequence for a root `__proto__`: sanitizing runs before the
|
|
68911
|
-
* plain-object check, so such a document parses successfully with the key
|
|
68912
|
-
* stripped rather than failing that check. That is deliberate — dropping the
|
|
68913
|
-
* one poisoned key keeps the user's unrelated settings — and matches the
|
|
68914
|
-
* sanitize-before-the-root-check design the shared config gateway documents.
|
|
68915
|
-
*/
|
|
68916
|
-
function parseAmpSettings({ fileContent }) {
|
|
68917
|
-
let parsed;
|
|
68918
|
-
try {
|
|
68919
|
-
parsed = parseJsonc$6(fileContent || "{}");
|
|
68920
|
-
} catch (error) {
|
|
68921
|
-
throw new Error(`Failed to parse Amp settings: ${formatError(error)}`, { cause: error });
|
|
68922
|
-
}
|
|
68923
|
-
if (!isPlainObject(parsed)) throw new Error("Amp settings must be a JSON object");
|
|
68924
|
-
return parsed;
|
|
68925
|
-
}
|
|
68926
|
-
/**
|
|
68927
|
-
* Probe `<jsonDir>/settings.jsonc` first, falling back to `settings.json`, so
|
|
68928
|
-
* an existing user file is read-modified-written in place instead of a fresh
|
|
68929
|
-
* `.json` sibling being created next to a hand-authored `.jsonc`. Defaults to
|
|
68930
|
-
* `settings.json` when neither file exists: Amp's canonically documented
|
|
68931
|
-
* settings file (`.amp/settings.json`), shared by the MCP and permissions
|
|
68932
|
-
* adapters, so generating both on a clean repo writes a single settings file.
|
|
68933
|
-
*/
|
|
68934
|
-
async function resolveAmpSettingsFile(jsonDir) {
|
|
68935
|
-
const jsoncContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_JSONC_FILE_NAME));
|
|
68936
|
-
if (jsoncContent !== null) return {
|
|
68937
|
-
fileContent: jsoncContent,
|
|
68938
|
-
relativeFilePath: AMP_SETTINGS_JSONC_FILE_NAME
|
|
68939
|
-
};
|
|
68940
|
-
const jsonContent = await readFileContentOrNull((0, node_path.join)(jsonDir, AMP_SETTINGS_FILE_NAME));
|
|
68941
|
-
if (jsonContent !== null) return {
|
|
68942
|
-
fileContent: jsonContent,
|
|
68943
|
-
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
68944
|
-
};
|
|
68945
|
-
return {
|
|
68946
|
-
fileContent: null,
|
|
68947
|
-
relativeFilePath: AMP_SETTINGS_FILE_NAME
|
|
68948
|
-
};
|
|
68949
|
-
}
|
|
68950
69320
|
// src/features/mcp/amp-mcp.ts
|
|
68951
69321
|
const AMP_MCP_SERVERS_KEY = "amp.mcpServers";
|
|
68952
69322
|
function filterMcpServers(mcpServers) {
|
|
@@ -71932,7 +72302,7 @@ var CopilotMcp = class CopilotMcp extends ToolMcp {
|
|
|
71932
72302
|
fileKey: sharedConfigFileKey(paths),
|
|
71933
72303
|
feature: "mcp",
|
|
71934
72304
|
existingContent,
|
|
71935
|
-
patch: { servers: convertToCopilotFormat(mcpServers) },
|
|
72305
|
+
patch: { servers: undefinedWhenEmpty(convertToCopilotFormat(mcpServers)) },
|
|
71936
72306
|
filePath,
|
|
71937
72307
|
logger
|
|
71938
72308
|
}),
|
|
@@ -74253,14 +74623,20 @@ function resolveGooseUrl(config) {
|
|
|
74253
74623
|
}
|
|
74254
74624
|
/**
|
|
74255
74625
|
* Determines the Goose `type` for a server based on its command/url/transport.
|
|
74626
|
+
*
|
|
74627
|
+
* Goose no longer has an SSE extension type (it skips a `type: sse` entry as
|
|
74628
|
+
* unsupported), so every remote server is written as `streamable_http`.
|
|
74256
74629
|
*/
|
|
74257
74630
|
function resolveGooseType(config, url) {
|
|
74258
74631
|
if (config.command !== void 0) return "stdio";
|
|
74259
|
-
if (url !== void 0) return
|
|
74632
|
+
if (url !== void 0) return "streamable_http";
|
|
74260
74633
|
return canonicalTransport(config) === "builtin" ? "builtin" : "stdio";
|
|
74261
74634
|
}
|
|
74262
74635
|
/**
|
|
74263
|
-
* The Goose extension types that carry an MCP server.
|
|
74636
|
+
* The Goose extension types that carry an MCP server. `sse` is no longer one
|
|
74637
|
+
* Goose reads, but it stays here so a stale `type: sse` entry is still
|
|
74638
|
+
* recognized: imported as canonical `sse` and replaced by the regenerated
|
|
74639
|
+
* same-name `streamable_http` entry. Goose also documents
|
|
74264
74640
|
* `builtin`, `platform`, `frontend` and `inline_python` extensions, which have
|
|
74265
74641
|
* no canonical MCP counterpart: they name capabilities Goose provides itself
|
|
74266
74642
|
* rather than a server rulesync could describe.
|
|
@@ -74320,7 +74696,8 @@ function convertServerToGooseExtension(name, config, logger) {
|
|
|
74320
74696
|
warnWithFallback(logger, `Goose extension "${name}" has no command to run; skipping it rather than writing a stdio extension Goose cannot start to ~/.config/goose/config.yaml.`);
|
|
74321
74697
|
return;
|
|
74322
74698
|
}
|
|
74323
|
-
} else if (gooseType === "
|
|
74699
|
+
} else if (gooseType === "streamable_http") {
|
|
74700
|
+
if (canonicalTransport(config) === "sse") warnWithFallback(logger, `Goose extension "${name}" uses SSE, which Goose no longer supports; writing it as streamable_http, which only works if the server also speaks Streamable HTTP.`);
|
|
74324
74701
|
if (url !== void 0) ext.uri = url;
|
|
74325
74702
|
if (isPlainObject(config.headers)) ext.headers = omitPrototypePollutionKeys(config.headers);
|
|
74326
74703
|
}
|
|
@@ -74333,8 +74710,9 @@ function convertServerToGooseExtension(name, config, logger) {
|
|
|
74333
74710
|
* Converts rulesync canonical MCP servers into Goose `extensions:` entries.
|
|
74334
74711
|
*
|
|
74335
74712
|
* Goose uses a non-standard schema: `name`, `type` (`stdio` | `streamable_http`
|
|
74336
|
-
* | `
|
|
74337
|
-
*
|
|
74713
|
+
* | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for remote, plus
|
|
74714
|
+
* `enabled` and `timeout`. A canonical `sse` server is written as
|
|
74715
|
+
* `streamable_http` with a warning, since Goose dropped SSE support.
|
|
74338
74716
|
*
|
|
74339
74717
|
* `extensions:` is co-owned: alongside the MCP servers rulesync manages it also
|
|
74340
74718
|
* holds Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions
|
|
@@ -74465,7 +74843,8 @@ function convertToGoosePluginMcpServers(mcpServers, logger) {
|
|
|
74465
74843
|
* `~/.config/goose/config.yaml`. That file also holds other Goose settings
|
|
74466
74844
|
* (model, provider, ...), so generation merges the `extensions:` block into the
|
|
74467
74845
|
* existing config instead of overwriting it, and the file is never deleted.
|
|
74468
|
-
* This location supports both stdio and remote (
|
|
74846
|
+
* This location supports both stdio and remote servers (SSE is written as
|
|
74847
|
+
* `streamable_http`, since Goose no longer reads `type: sse`).
|
|
74469
74848
|
* - **Project**: a stdio-only open-plugin manifest at
|
|
74470
74849
|
* `.agents/plugins/rulesync/.mcp.json` (Goose v1.39.0+), reusing the same
|
|
74471
74850
|
* `.agents/plugins/rulesync/` tree as Goose hooks. The manifest uses the
|
|
@@ -79183,6 +79562,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
|
|
|
79183
79562
|
const managedNames = new Set(Object.keys(rulesyncServers));
|
|
79184
79563
|
const mcpServers = Object.entries(rulesyncServers).map(([name, server]) => rulesyncMcpServerToVibe(name, server, existingByName.get(name)));
|
|
79185
79564
|
const unmanagedServers = existingServers.filter((server) => !managedNames.has(server.name));
|
|
79565
|
+
const servers = [...mcpServers, ...unmanagedServers];
|
|
79186
79566
|
return new VibeMcp({
|
|
79187
79567
|
outputRoot,
|
|
79188
79568
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -79191,7 +79571,7 @@ var VibeMcp = class VibeMcp extends ToolMcp {
|
|
|
79191
79571
|
fileKey: sharedConfigFileKey(paths),
|
|
79192
79572
|
feature: "mcp",
|
|
79193
79573
|
existingContent,
|
|
79194
|
-
patch: { mcp_servers:
|
|
79574
|
+
patch: { mcp_servers: servers.length > 0 ? servers : void 0 },
|
|
79195
79575
|
filePath
|
|
79196
79576
|
}),
|
|
79197
79577
|
validate,
|
|
@@ -82322,6 +82702,57 @@ function matchesGlobStep(step, character) {
|
|
|
82322
82702
|
const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
|
|
82323
82703
|
return step.negated ? !admitted : admitted;
|
|
82324
82704
|
}
|
|
82705
|
+
/**
|
|
82706
|
+
* Whether `value` matches `glob` in full, with `*`, `?` and `[...]` carrying
|
|
82707
|
+
* their usual meaning. The glob comes first, unlike Node's own
|
|
82708
|
+
* `path.matchesGlob(path, pattern)`.
|
|
82709
|
+
*
|
|
82710
|
+
* The glob is walked step by step rather than translated to a regex, because
|
|
82711
|
+
* both sides of a comparison can come from a file a repository carries: `*a*a*a*a*b`
|
|
82712
|
+
* translated to `^.*a.*a.*a.*a.*b$` takes minutes against a forty-character
|
|
82713
|
+
* name, while walking the steps is linear in the name per `*`. Remembering only
|
|
82714
|
+
* the most recent `*` to fall back to is enough — an earlier one can always give
|
|
82715
|
+
* up whatever the later one needed.
|
|
82716
|
+
*/
|
|
82717
|
+
function matchesGlob(glob, value) {
|
|
82718
|
+
return compileGlob(glob)(value);
|
|
82719
|
+
}
|
|
82720
|
+
/**
|
|
82721
|
+
* Parse `glob` once and return a predicate that walks it, for a caller that
|
|
82722
|
+
* tests the same glob against a whole list of names.
|
|
82723
|
+
*/
|
|
82724
|
+
function compileGlob(glob) {
|
|
82725
|
+
const steps = parseGlob(glob);
|
|
82726
|
+
return (value) => matchesParsedGlob(steps, value);
|
|
82727
|
+
}
|
|
82728
|
+
function matchesParsedGlob(steps, value) {
|
|
82729
|
+
const characters = [...value];
|
|
82730
|
+
let stepIndex = 0;
|
|
82731
|
+
let characterIndex = 0;
|
|
82732
|
+
let starStepIndex = -1;
|
|
82733
|
+
let starCharacterIndex = 0;
|
|
82734
|
+
while (characterIndex < characters.length) {
|
|
82735
|
+
const step = steps[stepIndex];
|
|
82736
|
+
if (step?.kind === "star") {
|
|
82737
|
+
starStepIndex = stepIndex;
|
|
82738
|
+
starCharacterIndex = characterIndex;
|
|
82739
|
+
stepIndex += 1;
|
|
82740
|
+
continue;
|
|
82741
|
+
}
|
|
82742
|
+
if (step !== void 0 && matchesGlobStep(step, characters[characterIndex] ?? "")) {
|
|
82743
|
+
stepIndex += 1;
|
|
82744
|
+
characterIndex += 1;
|
|
82745
|
+
continue;
|
|
82746
|
+
}
|
|
82747
|
+
if (starStepIndex < 0) return false;
|
|
82748
|
+
starCharacterIndex += 1;
|
|
82749
|
+
stepIndex = starStepIndex + 1;
|
|
82750
|
+
characterIndex = starCharacterIndex;
|
|
82751
|
+
}
|
|
82752
|
+
let remaining = stepIndex;
|
|
82753
|
+
while (steps[remaining]?.kind === "star") remaining += 1;
|
|
82754
|
+
return remaining === steps.length;
|
|
82755
|
+
}
|
|
82325
82756
|
/** Whether two single-character steps can both match one same character. */
|
|
82326
82757
|
function stepsShareACharacter(left, right) {
|
|
82327
82758
|
if (left.kind === "any" || right.kind === "any") return true;
|
|
@@ -82564,6 +82995,20 @@ function isUnicodeClass(rest) {
|
|
|
82564
82995
|
}));
|
|
82565
82996
|
}
|
|
82566
82997
|
/**
|
|
82998
|
+
* Return a function that, for a `[:` at `index`, gives the position of the
|
|
82999
|
+
* next `:]` (Go reads everything between as the class name), or -1. The
|
|
83000
|
+
* position is cached and `index` only grows, so a long run of `[:` with no
|
|
83001
|
+
* `:]` costs linear time rather than a scan to the end at each one.
|
|
83002
|
+
*/
|
|
83003
|
+
function posixEndFinder(source) {
|
|
83004
|
+
let next = -2;
|
|
83005
|
+
return (index) => {
|
|
83006
|
+
if (!source.startsWith("[:", index)) return -1;
|
|
83007
|
+
if (next !== -1 && next < index + 2) next = source.indexOf(":]", index + 2);
|
|
83008
|
+
return next;
|
|
83009
|
+
};
|
|
83010
|
+
}
|
|
83011
|
+
/**
|
|
82567
83012
|
* Replace each POSIX class (`[:alpha:]`) inside a bracket class with a plain
|
|
82568
83013
|
* character, so JavaScript can compile the rest, or return `undefined` for an
|
|
82569
83014
|
* unknown one. Only inside a bracket is `[:name:]` a POSIX class; a bare
|
|
@@ -82573,6 +83018,7 @@ function replacePosixClasses(source) {
|
|
|
82573
83018
|
let result = "";
|
|
82574
83019
|
let inClass = false;
|
|
82575
83020
|
let index = 0;
|
|
83021
|
+
const findPosixEnd = posixEndFinder(source);
|
|
82576
83022
|
while (index < source.length) {
|
|
82577
83023
|
const character = source[index] ?? "";
|
|
82578
83024
|
if (character === "\\") {
|
|
@@ -82597,11 +83043,11 @@ function replacePosixClasses(source) {
|
|
|
82597
83043
|
}
|
|
82598
83044
|
continue;
|
|
82599
83045
|
}
|
|
82600
|
-
const
|
|
82601
|
-
if (
|
|
82602
|
-
if (!POSIX_CLASSES.has(
|
|
83046
|
+
const posixEnd = findPosixEnd(index);
|
|
83047
|
+
if (posixEnd !== -1) {
|
|
83048
|
+
if (!POSIX_CLASSES.has(source.slice(index + 2, posixEnd).replace(/^\^/, ""))) return;
|
|
82603
83049
|
result += "a";
|
|
82604
|
-
index
|
|
83050
|
+
index = posixEnd + 2;
|
|
82605
83051
|
continue;
|
|
82606
83052
|
}
|
|
82607
83053
|
if (character === "]") inClass = false;
|
|
@@ -82680,6 +83126,7 @@ function hasClassEscapeRangeEnd(source) {
|
|
|
82680
83126
|
let inClass = false;
|
|
82681
83127
|
let afterCharacter = false;
|
|
82682
83128
|
let index = 0;
|
|
83129
|
+
const findPosixEnd = posixEndFinder(source);
|
|
82683
83130
|
while (index < source.length) {
|
|
82684
83131
|
const character = source[index] ?? "";
|
|
82685
83132
|
if (character === "\\") {
|
|
@@ -82695,7 +83142,7 @@ function hasClassEscapeRangeEnd(source) {
|
|
|
82695
83142
|
});
|
|
82696
83143
|
continue;
|
|
82697
83144
|
}
|
|
82698
|
-
const posixEnd =
|
|
83145
|
+
const posixEnd = findPosixEnd(index);
|
|
82699
83146
|
if (character === "]" || posixEnd !== -1) {
|
|
82700
83147
|
inClass = character !== "]";
|
|
82701
83148
|
afterCharacter = false;
|
|
@@ -83257,6 +83704,8 @@ function buildToolPatternEntry({ toolName, pattern }) {
|
|
|
83257
83704
|
* - `execute_url` takes a domain, like `read_url`.
|
|
83258
83705
|
* - `command` and `unsandboxed` take literal words or a per-word `regex:`,
|
|
83259
83706
|
* matched word by word (see `antigravity-command-patterns.ts`).
|
|
83707
|
+
* - `mcp` takes `server/tool` or `server/*`, matched exactly. A canonical
|
|
83708
|
+
* `mcp__<server>__<tool>` category is written as that target.
|
|
83260
83709
|
*
|
|
83261
83710
|
* A glob copied verbatim (`read_file(**\/*.env)`) is a literal path that
|
|
83262
83711
|
* matches nothing, so a deny written that way blocks nothing. A pattern with
|
|
@@ -83280,7 +83729,8 @@ const DENY_HINTS = {
|
|
|
83280
83729
|
read_file: "deny a directory or file path instead",
|
|
83281
83730
|
write_file: "deny a directory or file path instead",
|
|
83282
83731
|
command: "deny the first words of the command instead",
|
|
83283
|
-
unsandboxed: "deny the first words of the command instead"
|
|
83732
|
+
unsandboxed: "deny the first words of the command instead",
|
|
83733
|
+
mcp: "deny the whole MCP server or tool instead"
|
|
83284
83734
|
};
|
|
83285
83735
|
function hasGlob(pattern) {
|
|
83286
83736
|
return BRACE_ALTERNATIVES.test(pattern) || parseGlobPattern(pattern).steps.some((step) => step.kind !== "literal");
|
|
@@ -83336,6 +83786,40 @@ function toTarget(action, pattern) {
|
|
|
83336
83786
|
if (URL_ACTIONS.has(action)) return toUrlTarget(pattern);
|
|
83337
83787
|
return { target: pattern };
|
|
83338
83788
|
}
|
|
83789
|
+
const MCP_CATEGORY_PREFIX = "mcp__";
|
|
83790
|
+
const MCP_TARGET_SEPARATORS = /[/()]/;
|
|
83791
|
+
/**
|
|
83792
|
+
* The `mcp` target of a canonical `mcp__<server>__<tool>` category:
|
|
83793
|
+
* `<server>/<tool>`, `<server>/*` for a whole server (`mcp__<server>`,
|
|
83794
|
+
* `mcp__<server>__*`), or `*` for every MCP tool (`mcp__*`, `mcp__*__*`). The
|
|
83795
|
+
* server ends at the first `__`, as elsewhere in rulesync. Antigravity matches
|
|
83796
|
+
* names exactly, so a glob, an empty name, or a `/` or parenthesis in a name
|
|
83797
|
+
* has no target; when only the tool part is at fault, its server is the `lead`
|
|
83798
|
+
* a deny widens to.
|
|
83799
|
+
*/
|
|
83800
|
+
function toMcpTarget(category) {
|
|
83801
|
+
const remainder = category.slice(5);
|
|
83802
|
+
const separator = remainder.indexOf("__");
|
|
83803
|
+
const server = separator === -1 ? remainder : remainder.slice(0, separator);
|
|
83804
|
+
const tool = separator === -1 ? "*" : remainder.slice(separator + 2);
|
|
83805
|
+
if (server === "*" && tool === "*") return { target: "*" };
|
|
83806
|
+
const skipReason = "Antigravity MCP targets name one server or one tool exactly. Only `mcp__<server>`, `mcp__<server>__*`, `mcp__<server>__<tool>` or `mcp__*` can be expressed";
|
|
83807
|
+
if (server.length === 0 || MCP_TARGET_SEPARATORS.test(server) || hasGlob(server)) return { skipReason };
|
|
83808
|
+
if (tool === "*") return { target: `${server}/*` };
|
|
83809
|
+
return tool.length === 0 || MCP_TARGET_SEPARATORS.test(tool) || hasGlob(tool) ? {
|
|
83810
|
+
skipReason,
|
|
83811
|
+
lead: `${server}/*`
|
|
83812
|
+
} : { target: `${server}/${tool}` };
|
|
83813
|
+
}
|
|
83814
|
+
/** The target of a canonical `mcp__...` rule, with the `lead` a deny widens to. */
|
|
83815
|
+
function toMcpRuleTarget(category, pattern) {
|
|
83816
|
+
const target = toMcpTarget(category);
|
|
83817
|
+
if (pattern === "*" || !("target" in target)) return target;
|
|
83818
|
+
return {
|
|
83819
|
+
skipReason: "Antigravity MCP targets name a server or a tool, not the tool's arguments",
|
|
83820
|
+
lead: target.target
|
|
83821
|
+
};
|
|
83822
|
+
}
|
|
83339
83823
|
/**
|
|
83340
83824
|
* The leading directories of a path glob (`src` for `src/*.ts`), or
|
|
83341
83825
|
* `undefined` when it has none. A directory target covers everything inside
|
|
@@ -83358,6 +83842,20 @@ function toWidenedDenyTarget(action, pattern) {
|
|
|
83358
83842
|
if (ANTIGRAVITY_COMMAND_ACTIONS.has(action)) return toAntigravityCommandLiteralPrefix(pattern);
|
|
83359
83843
|
if (PATH_ACTIONS.has(action)) return toPathLiteralPrefix(pattern);
|
|
83360
83844
|
}
|
|
83845
|
+
/** The entry a deny is widened to, and what it now blocks. */
|
|
83846
|
+
function describeWidenedDeny({ action, prefix, isMcp }) {
|
|
83847
|
+
if (isMcp) return prefix === "*" ? {
|
|
83848
|
+
entry: action,
|
|
83849
|
+
scope: "every MCP tool"
|
|
83850
|
+
} : {
|
|
83851
|
+
entry: `${action}(${prefix})`,
|
|
83852
|
+
scope: `every call of \`${action}(${prefix})\``
|
|
83853
|
+
};
|
|
83854
|
+
return {
|
|
83855
|
+
entry: `${action}(${prefix})`,
|
|
83856
|
+
scope: PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``
|
|
83857
|
+
};
|
|
83858
|
+
}
|
|
83361
83859
|
/**
|
|
83362
83860
|
* Warn about a path target that is a home or variable reference. Antigravity
|
|
83363
83861
|
* documents absolute and workspace-relative paths only, so such a reference
|
|
@@ -83379,14 +83877,20 @@ function warnHomeOrVariablePath({ action, target, rule, decision, logger, toolLa
|
|
|
83379
83877
|
* nothing enforces it, and a widened one is warned about too, because it
|
|
83380
83878
|
* blocks more than it says.
|
|
83381
83879
|
*/
|
|
83382
|
-
function buildAntigravityPermissionEntry({ action, category, pattern, decision, logger, toolLabel }) {
|
|
83383
|
-
const
|
|
83880
|
+
function buildAntigravityPermissionEntry({ action: toolAction, category, pattern, decision, logger, toolLabel }) {
|
|
83881
|
+
const isMcp = category.startsWith(MCP_CATEGORY_PREFIX);
|
|
83882
|
+
const action = isMcp ? "mcp" : toolAction;
|
|
83883
|
+
const result = isMcp ? toMcpRuleTarget(category, pattern) : pattern === "*" ? { target: "*" } : toTarget(action, pattern);
|
|
83384
83884
|
const rule = `${category}: { "${pattern}": "${decision}" }`;
|
|
83385
83885
|
if ("skipReason" in result) {
|
|
83386
|
-
const prefix = decision
|
|
83886
|
+
const prefix = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
|
|
83387
83887
|
if (prefix !== void 0) {
|
|
83388
|
-
const
|
|
83389
|
-
|
|
83888
|
+
const { entry, scope } = describeWidenedDeny({
|
|
83889
|
+
action,
|
|
83890
|
+
prefix,
|
|
83891
|
+
isMcp
|
|
83892
|
+
});
|
|
83893
|
+
logger.warn(`${toolLabel} permissions: widening ${rule} to ${entry}. ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
|
|
83390
83894
|
warnHomeOrVariablePath({
|
|
83391
83895
|
action,
|
|
83392
83896
|
target: prefix,
|
|
@@ -83395,7 +83899,7 @@ function buildAntigravityPermissionEntry({ action, category, pattern, decision,
|
|
|
83395
83899
|
logger,
|
|
83396
83900
|
toolLabel
|
|
83397
83901
|
});
|
|
83398
|
-
return
|
|
83902
|
+
return entry;
|
|
83399
83903
|
}
|
|
83400
83904
|
const consequence = decision === "deny" ? `This deny is NOT enforced in ${toolLabel}; ${DENY_HINTS[action] ?? "write it in a form Antigravity can match"}` : decision === "ask" ? `${toolLabel} falls back to its default for it, so it runs without a prompt when ${toolLabel} is set to always proceed` : `${toolLabel} falls back to its default for it`;
|
|
83401
83905
|
logger.warn(`${toolLabel} permissions: skipping ${rule}. ${result.skipReason}. ${consequence}.`);
|
|
@@ -83539,6 +84043,7 @@ const ANTIGRAVITY_CLI_TO_CANONICAL_TOOL_NAMES = {
|
|
|
83539
84043
|
mcp: "mcp"
|
|
83540
84044
|
};
|
|
83541
84045
|
function toAntigravityCliToolName(canonical) {
|
|
84046
|
+
if (canonical.startsWith("mcp__")) return "mcp";
|
|
83542
84047
|
return CANONICAL_TO_ANTIGRAVITY_CLI_TOOL_NAMES[canonical] ?? canonical;
|
|
83543
84048
|
}
|
|
83544
84049
|
function toCanonicalToolName$9(cliName) {
|
|
@@ -83601,10 +84106,11 @@ var AntigravityCliPermissions = class AntigravityCliPermissions extends ToolPerm
|
|
|
83601
84106
|
logger
|
|
83602
84107
|
});
|
|
83603
84108
|
const managedToolNames = new Set(Object.keys(config.permission).map((category) => toAntigravityCliToolName(category)));
|
|
84109
|
+
const isManagedToolName = (toolName) => managedToolNames.has(toolName) || managedToolNames.has("mcp") && toolName.startsWith("mcp__");
|
|
83604
84110
|
const existingPermissions = settings.permissions ?? {};
|
|
83605
|
-
const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !
|
|
83606
|
-
const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !
|
|
83607
|
-
const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !
|
|
84111
|
+
const preservedAllow = (existingPermissions.allow ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
|
|
84112
|
+
const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
|
|
84113
|
+
const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !isManagedToolName(parseToolPatternEntry(entry).toolName));
|
|
83608
84114
|
const mergedPermissions = { ...existingPermissions };
|
|
83609
84115
|
const mergedAllow = (0, es_toolkit.uniq)([...preservedAllow, ...allow].toSorted());
|
|
83610
84116
|
const mergedAsk = (0, es_toolkit.uniq)([...preservedAsk, ...ask].toSorted());
|
|
@@ -83713,6 +84219,7 @@ const IDE_ACTION_TO_CANONICAL = {
|
|
|
83713
84219
|
mcp: "mcp"
|
|
83714
84220
|
};
|
|
83715
84221
|
function toIdeAction(canonical) {
|
|
84222
|
+
if (canonical.startsWith("mcp__")) return "mcp";
|
|
83716
84223
|
return CANONICAL_TO_IDE_ACTION[canonical] ?? canonical;
|
|
83717
84224
|
}
|
|
83718
84225
|
function toCanonicalCategory$2(ideAction) {
|
|
@@ -83769,8 +84276,9 @@ var AntigravityIdePermissions = class AntigravityIdePermissions extends ToolPerm
|
|
|
83769
84276
|
logger
|
|
83770
84277
|
});
|
|
83771
84278
|
const managedActions = new Set(Object.keys(config.permission).map((category) => toIdeAction(category)));
|
|
84279
|
+
const isManagedAction = (action) => managedActions.has(action) || managedActions.has("mcp") && action.startsWith("mcp__");
|
|
83772
84280
|
const existingPermissions = settings.permissions ?? {};
|
|
83773
|
-
const preserve = (entries) => (entries ?? []).filter((entry) => !
|
|
84281
|
+
const preserve = (entries) => (entries ?? []).filter((entry) => !isManagedAction(parseToolPatternEntry(entry).toolName));
|
|
83774
84282
|
const mergedPermissions = { ...existingPermissions };
|
|
83775
84283
|
const mergedAllow = (0, es_toolkit.uniq)([...preserve(existingPermissions.allow), ...allow].toSorted());
|
|
83776
84284
|
const mergedAsk = (0, es_toolkit.uniq)([...preserve(existingPermissions.ask), ...ask].toSorted());
|
|
@@ -83858,9 +84366,13 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
|
|
|
83858
84366
|
const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
83859
84367
|
const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
|
|
83860
84368
|
terminal: "launch-process",
|
|
84369
|
+
bash: "launch-process",
|
|
84370
|
+
shell: "launch-process",
|
|
83861
84371
|
read: "view",
|
|
83862
84372
|
edit: "str-replace-editor",
|
|
83863
|
-
|
|
84373
|
+
str_replace_editor: "str-replace-editor",
|
|
84374
|
+
write: "save-file",
|
|
84375
|
+
save_file: "save-file"
|
|
83864
84376
|
};
|
|
83865
84377
|
function toAugmentToolName(canonical) {
|
|
83866
84378
|
return lookupOwn({
|
|
@@ -84012,6 +84524,7 @@ function isShellRegexRoundtrippable(regex) {
|
|
|
84012
84524
|
while (i < body.length) {
|
|
84013
84525
|
const ch = body[i];
|
|
84014
84526
|
if (ch === "\\" && i + 1 < body.length) {
|
|
84527
|
+
if (isGlobSpecial(body[i + 1])) return false;
|
|
84015
84528
|
i += 2;
|
|
84016
84529
|
continue;
|
|
84017
84530
|
}
|
|
@@ -84028,6 +84541,37 @@ function isShellRegexRoundtrippable(regex) {
|
|
|
84028
84541
|
}
|
|
84029
84542
|
return true;
|
|
84030
84543
|
}
|
|
84544
|
+
/** Characters a glob reads as syntax (wildcards or the start of a character class). */
|
|
84545
|
+
function isGlobSpecial(ch) {
|
|
84546
|
+
return ch === "*" || ch === "?" || ch === "[";
|
|
84547
|
+
}
|
|
84548
|
+
/**
|
|
84549
|
+
* Whether `shellRegexToGlob` can approximate a `shellInputRegex` without widening it. The lossy
|
|
84550
|
+
* conversion keeps regex quantifiers as characters, and a `*` or `?` left in the glob becomes a
|
|
84551
|
+
* wildcard: `^rm -rf /tmp/a*$` (a run of `a`s) would turn into the glob `rm -rf /tmp/a*`, which
|
|
84552
|
+
* matches `rm -rf /tmp/a /home`. So a regex is unsafe when it has an escaped literal `\*` / `\?` /
|
|
84553
|
+
* `\[` (a `[` would open a character class in targets whose globs support one),
|
|
84554
|
+
* or a bare quantifier `*` / `?` / `+` / `{` that is not part of a `.*` run (which the glob `*`
|
|
84555
|
+
* reproduces faithfully).
|
|
84556
|
+
*/
|
|
84557
|
+
function canApproximateShellRegexWithoutWidening(regex) {
|
|
84558
|
+
let i = 0;
|
|
84559
|
+
while (i < regex.length) {
|
|
84560
|
+
const ch = regex[i];
|
|
84561
|
+
if (ch === "\\" && i + 1 < regex.length) {
|
|
84562
|
+
if (isGlobSpecial(regex[i + 1])) return false;
|
|
84563
|
+
i += 2;
|
|
84564
|
+
continue;
|
|
84565
|
+
}
|
|
84566
|
+
if (ch === "." && regex[i + 1] === "*") {
|
|
84567
|
+
i += 2;
|
|
84568
|
+
continue;
|
|
84569
|
+
}
|
|
84570
|
+
if (ch === "*" || ch === "?" || ch === "+" || ch === "{") return false;
|
|
84571
|
+
i += 1;
|
|
84572
|
+
}
|
|
84573
|
+
return true;
|
|
84574
|
+
}
|
|
84031
84575
|
const MANAGED_AUGMENT_TOOL_NAMES = new Set(Object.values(CANONICAL_TO_AUGMENT_TOOL_NAMES));
|
|
84032
84576
|
var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermissions {
|
|
84033
84577
|
constructor(params) {
|
|
@@ -84464,12 +85008,18 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
84464
85008
|
if (legacyToolName === "launch-process" && entry.shellInputRegex) {
|
|
84465
85009
|
const regex = entry.shellInputRegex;
|
|
84466
85010
|
if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
|
|
84467
|
-
else
|
|
84468
|
-
|
|
84469
|
-
|
|
84470
|
-
|
|
84471
|
-
|
|
84472
|
-
|
|
85011
|
+
else {
|
|
85012
|
+
if (action !== "deny" && !canApproximateShellRegexWithoutWidening(regex)) {
|
|
85013
|
+
logger?.warn(`AugmentCode permissions: skipping ${action} 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.`);
|
|
85014
|
+
continue;
|
|
85015
|
+
}
|
|
85016
|
+
if (action === "deny") {
|
|
85017
|
+
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 the deny rule cannot be silently narrowed on regenerate.`);
|
|
85018
|
+
pattern = "*";
|
|
85019
|
+
} else {
|
|
85020
|
+
pattern = shellRegexToGlob(regex);
|
|
85021
|
+
logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as glob '${pattern}'; the rule may match a different set of inputs after regenerate.`);
|
|
85022
|
+
}
|
|
84473
85023
|
}
|
|
84474
85024
|
} else pattern = "*";
|
|
84475
85025
|
if (isPrototypePollutionKey(pattern)) {
|
|
@@ -85962,15 +86512,19 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
|
|
|
85962
86512
|
* `env` variables a project or local settings file cannot set, because a
|
|
85963
86513
|
* checked-out repository should not control them: where Claude Code stores its
|
|
85964
86514
|
* own files, what session content it exports, where telemetry goes, how it
|
|
85965
|
-
* starts or syncs, the timers on an unanswered
|
|
85966
|
-
*
|
|
86515
|
+
* starts or syncs, which claude.ai sign-in it uses, the timers on an unanswered
|
|
86516
|
+
* dialog, and whether it processes attachments or expands `@` file mentions,
|
|
86517
|
+
* and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
|
|
85967
86518
|
*
|
|
85968
|
-
* Only the names the settings reference lists are covered; its directory
|
|
85969
|
-
* Windows groups are introduced with "such as", so an unlisted member of those
|
|
86519
|
+
* Only the names the settings reference lists are covered; its directory,
|
|
86520
|
+
* Windows and sign-in groups are introduced with "such as", so an unlisted member of those
|
|
85970
86521
|
* families is still written. Windows variable names are case-insensitive, so
|
|
85971
|
-
* that group is matched without regard to case.
|
|
86522
|
+
* that group is matched without regard to case. `ALLUSERSPROFILE`,
|
|
86523
|
+
* `SystemDrive` and the `CommonProgramFiles` family are ignored there since
|
|
86524
|
+
* Claude Code v2.1.285.
|
|
85972
86525
|
*
|
|
85973
86526
|
* @see https://code.claude.com/docs/en/settings-reference#variables-claude-code-ignores-in-env
|
|
86527
|
+
* @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
|
|
85974
86528
|
*/
|
|
85975
86529
|
const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
|
|
85976
86530
|
"BETA_TRACING_ENDPOINT",
|
|
@@ -85980,6 +86534,9 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
|
|
|
85980
86534
|
"CLAUDE_CODE_DISABLE_FILE_MENTIONS",
|
|
85981
86535
|
"CLAUDE_CODE_ENABLE_TELEMETRY",
|
|
85982
86536
|
"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
|
|
86537
|
+
"CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
|
|
86538
|
+
"CLAUDE_CODE_OAUTH_SCOPES",
|
|
86539
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
85983
86540
|
"CLAUDE_CODE_PLUGIN_CACHE_DIR",
|
|
85984
86541
|
"CLAUDE_CODE_PLUGIN_SEED_DIR",
|
|
85985
86542
|
"CLAUDE_CODE_PROCESS_WRAPPER",
|
|
@@ -86000,14 +86557,23 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
|
|
|
86000
86557
|
"TMPDIR"
|
|
86001
86558
|
]);
|
|
86002
86559
|
const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES = /* @__PURE__ */ new Set([
|
|
86560
|
+
"ALLUSERSPROFILE",
|
|
86561
|
+
"COMMONPROGRAMW6432",
|
|
86003
86562
|
"COMSPEC",
|
|
86004
86563
|
"LOCALAPPDATA",
|
|
86005
86564
|
"PATHEXT",
|
|
86006
86565
|
"PROGRAMDATA",
|
|
86007
86566
|
"PROGRAMW6432",
|
|
86008
86567
|
"PSMODULEPATH",
|
|
86568
|
+
"SYSTEMDRIVE",
|
|
86009
86569
|
"SYSTEMROOT"
|
|
86010
86570
|
]);
|
|
86571
|
+
/**
|
|
86572
|
+
* Windows name prefixes matched as whole families: `ProgramFiles*` and
|
|
86573
|
+
* `CommonProgramFiles*`. `ProgramW6432` and `CommonProgramW6432` are exact
|
|
86574
|
+
* names in the set above.
|
|
86575
|
+
*/
|
|
86576
|
+
const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES = ["PROGRAMFILES", "COMMONPROGRAMFILES"];
|
|
86011
86577
|
const CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES = [
|
|
86012
86578
|
"_ENDPOINT",
|
|
86013
86579
|
"_HEADERS",
|
|
@@ -86052,7 +86618,7 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
|
|
|
86052
86618
|
function isProjectIgnoredEnvVariable(name, value) {
|
|
86053
86619
|
if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_") || name.startsWith("CLAUDE_AX_")) return true;
|
|
86054
86620
|
const upper = name.toUpperCase();
|
|
86055
|
-
if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || upper.startsWith(
|
|
86621
|
+
if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES.some((prefix) => upper.startsWith(prefix))) return true;
|
|
86056
86622
|
if (name.startsWith("OTEL_EXPORTER_OTLP_") && CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return true;
|
|
86057
86623
|
if (Object.hasOwn(CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES, name)) {
|
|
86058
86624
|
const offValues = CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES[name];
|
|
@@ -86084,7 +86650,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
|
|
|
86084
86650
|
kept[name] = value;
|
|
86085
86651
|
}
|
|
86086
86652
|
if (ignoredEverywhere.length > 0) logger?.warn(`Claude Code permissions: Claude Code ignores ${ignoredEverywhere.map((name) => `'env.${name}'`).join(", ")} in every settings file, so ${ignoredEverywhere.length === 1 ? "it is" : "they are"} not written to ${relativeFilePath}. Set ${ignoredEverywhere.length === 1 ? "it" : "them"} in the environment Claude Code starts from instead, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
|
|
86087
|
-
if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
|
|
86653
|
+
if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set the claude.ai sign-in, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
|
|
86088
86654
|
return kept;
|
|
86089
86655
|
}
|
|
86090
86656
|
/**
|
|
@@ -86734,7 +87300,7 @@ const CODEBUDDY_RULE_LISTS = [
|
|
|
86734
87300
|
"ask",
|
|
86735
87301
|
"deny"
|
|
86736
87302
|
];
|
|
86737
|
-
const CATCH_ALL_PATTERN$
|
|
87303
|
+
const CATCH_ALL_PATTERN$14 = "*";
|
|
86738
87304
|
const MCP_CATEGORY = "mcp";
|
|
86739
87305
|
const MCP_RULE_PREFIX = "mcp__";
|
|
86740
87306
|
const ALL_MCP_RULE = "mcp__*";
|
|
@@ -86804,7 +87370,7 @@ function categoryOfRule$2(rule) {
|
|
|
86804
87370
|
* and reach the `Bash` rules through `honorAllToolsOnBash`.
|
|
86805
87371
|
*/
|
|
86806
87372
|
function buildCodebuddyRule({ category, pattern, action, logger }) {
|
|
86807
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
87373
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$14;
|
|
86808
87374
|
if (category === MCP_CATEGORY) {
|
|
86809
87375
|
if (catchAll && action === "allow") {
|
|
86810
87376
|
logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
|
|
@@ -86813,7 +87379,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
|
|
|
86813
87379
|
return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
|
|
86814
87380
|
}
|
|
86815
87381
|
if (category === "*") {
|
|
86816
|
-
if (catchAll) return CATCH_ALL_PATTERN$
|
|
87382
|
+
if (catchAll) return CATCH_ALL_PATTERN$14;
|
|
86817
87383
|
logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
|
|
86818
87384
|
return null;
|
|
86819
87385
|
}
|
|
@@ -86831,7 +87397,7 @@ function parseCodebuddyRule(rule) {
|
|
|
86831
87397
|
const { specifier } = splitCodebuddyRule(rule);
|
|
86832
87398
|
if (specifier === void 0) return {
|
|
86833
87399
|
category,
|
|
86834
|
-
pattern: CATCH_ALL_PATTERN$
|
|
87400
|
+
pattern: CATCH_ALL_PATTERN$14
|
|
86835
87401
|
};
|
|
86836
87402
|
const trimmed = specifier.trim();
|
|
86837
87403
|
return trimmed === "" ? null : {
|
|
@@ -89157,8 +89723,8 @@ function mapBashActionToDecision(action) {
|
|
|
89157
89723
|
// src/features/permissions/commandcode-permissions.ts
|
|
89158
89724
|
/** Top-level key of `.commandcode/settings.json` that rulesync owns. */
|
|
89159
89725
|
const COMMANDCODE_PERMISSIONS_KEY = "permissions";
|
|
89160
|
-
const CATCH_ALL_PATTERN$
|
|
89161
|
-
const MCP_CANONICAL_PREFIX$
|
|
89726
|
+
const CATCH_ALL_PATTERN$13 = "*";
|
|
89727
|
+
const MCP_CANONICAL_PREFIX$8 = "mcp__";
|
|
89162
89728
|
const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
|
|
89163
89729
|
const CATEGORY_TO_COMMANDCODE_TOOL = {
|
|
89164
89730
|
bash: "Shell",
|
|
@@ -89203,10 +89769,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
|
|
|
89203
89769
|
* restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
|
|
89204
89770
|
*/
|
|
89205
89771
|
function buildCommandcodeRule(category, pattern) {
|
|
89206
|
-
const catchAll = pattern === CATCH_ALL_PATTERN$
|
|
89207
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
89208
|
-
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$
|
|
89209
|
-
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$
|
|
89772
|
+
const catchAll = pattern === CATCH_ALL_PATTERN$13 || pattern === "";
|
|
89773
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$8)) return catchAll ? category : `${category}(${pattern})`;
|
|
89774
|
+
if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$8}${pattern}`;
|
|
89775
|
+
if (category === "*") return catchAll ? CATCH_ALL_PATTERN$13 : null;
|
|
89210
89776
|
const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
|
|
89211
89777
|
if (tool === void 0) return null;
|
|
89212
89778
|
return catchAll ? tool : `${tool}(${pattern})`;
|
|
@@ -89242,7 +89808,7 @@ function splitCommandcodeRule(rule) {
|
|
|
89242
89808
|
}
|
|
89243
89809
|
/** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
|
|
89244
89810
|
function isCatchAllSpecifier(inner) {
|
|
89245
|
-
return inner === "" || inner === CATCH_ALL_PATTERN$
|
|
89811
|
+
return inner === "" || inner === CATCH_ALL_PATTERN$13;
|
|
89246
89812
|
}
|
|
89247
89813
|
/**
|
|
89248
89814
|
* Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
|
|
@@ -89276,34 +89842,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
|
|
|
89276
89842
|
if (server.length === 0) return null;
|
|
89277
89843
|
const everyServer = {
|
|
89278
89844
|
category: "mcp",
|
|
89279
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89845
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89280
89846
|
notInAllow: true
|
|
89281
89847
|
};
|
|
89282
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
89283
|
-
if (server === CATCH_ALL_PATTERN$
|
|
89284
|
-
if (server.includes(CATCH_ALL_PATTERN$
|
|
89848
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$8)) {
|
|
89849
|
+
if (server === CATCH_ALL_PATTERN$13) return everyServer;
|
|
89850
|
+
if (server.includes(CATCH_ALL_PATTERN$13)) return null;
|
|
89285
89851
|
return {
|
|
89286
|
-
category: rest === "" || rest === CATCH_ALL_PATTERN$
|
|
89287
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89852
|
+
category: rest === "" || rest === CATCH_ALL_PATTERN$13 ? `${MCP_CANONICAL_PREFIX$8}${server}` : `${MCP_CANONICAL_PREFIX$8}${remainder}`,
|
|
89853
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89288
89854
|
notInAllow: false
|
|
89289
89855
|
};
|
|
89290
89856
|
}
|
|
89291
|
-
const category = `${MCP_CANONICAL_PREFIX$
|
|
89292
|
-
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$
|
|
89857
|
+
const category = `${MCP_CANONICAL_PREFIX$8}${remainder}`;
|
|
89858
|
+
const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$13);
|
|
89293
89859
|
if (!isCatchAllSpecifier(inner)) return namesTool ? {
|
|
89294
89860
|
category,
|
|
89295
89861
|
pattern: inner,
|
|
89296
89862
|
notInAllow: true
|
|
89297
89863
|
} : null;
|
|
89298
|
-
if (remainder === CATCH_ALL_PATTERN$
|
|
89299
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
89300
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
89301
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89864
|
+
if (remainder === CATCH_ALL_PATTERN$13) return everyServer;
|
|
89865
|
+
if (rest === CATCH_ALL_PATTERN$13 && !server.includes(CATCH_ALL_PATTERN$13)) return {
|
|
89866
|
+
category: `${MCP_CANONICAL_PREFIX$8}${server}`,
|
|
89867
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89302
89868
|
notInAllow: true
|
|
89303
89869
|
};
|
|
89304
89870
|
return namesTool ? {
|
|
89305
89871
|
category,
|
|
89306
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89872
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89307
89873
|
notInAllow: false
|
|
89308
89874
|
} : null;
|
|
89309
89875
|
}
|
|
@@ -89327,14 +89893,14 @@ function friendlyToolRule({ category, inner }) {
|
|
|
89327
89893
|
if (category === "bash") {
|
|
89328
89894
|
if (isCatchAllSpecifier(inner)) return {
|
|
89329
89895
|
category,
|
|
89330
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89896
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89331
89897
|
notInAllow: false
|
|
89332
89898
|
};
|
|
89333
89899
|
const normalized = inner.trim().replace(/\s+/g, " ");
|
|
89334
89900
|
if (normalized === "" || normalized === ":*") return null;
|
|
89335
89901
|
return isCatchAllSpecifier(normalized) ? {
|
|
89336
89902
|
category,
|
|
89337
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89903
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89338
89904
|
notInAllow: true
|
|
89339
89905
|
} : {
|
|
89340
89906
|
category,
|
|
@@ -89344,7 +89910,7 @@ function friendlyToolRule({ category, inner }) {
|
|
|
89344
89910
|
}
|
|
89345
89911
|
if (isCatchAllSpecifier(inner)) return {
|
|
89346
89912
|
category,
|
|
89347
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89913
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89348
89914
|
notInAllow: false
|
|
89349
89915
|
};
|
|
89350
89916
|
return isCatchAllSpecifier(inner.trim()) ? null : {
|
|
@@ -89363,13 +89929,13 @@ function friendlyToolRule({ category, inner }) {
|
|
|
89363
89929
|
*/
|
|
89364
89930
|
function parseCommandcodeRule(rule) {
|
|
89365
89931
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
89366
|
-
if (tool === CATCH_ALL_PATTERN$
|
|
89932
|
+
if (tool === CATCH_ALL_PATTERN$13) return isCatchAllSpecifier(inner) ? {
|
|
89367
89933
|
category: "*",
|
|
89368
|
-
pattern: CATCH_ALL_PATTERN$
|
|
89934
|
+
pattern: CATCH_ALL_PATTERN$13,
|
|
89369
89935
|
notInAllow: true
|
|
89370
89936
|
} : null;
|
|
89371
89937
|
const lowered = tool.toLowerCase();
|
|
89372
|
-
if (lowered.startsWith(MCP_CANONICAL_PREFIX$
|
|
89938
|
+
if (lowered.startsWith(MCP_CANONICAL_PREFIX$8)) return parseMcpCommandcodeRule({
|
|
89373
89939
|
tool,
|
|
89374
89940
|
inner
|
|
89375
89941
|
});
|
|
@@ -89387,12 +89953,12 @@ function parseCommandcodeRule(rule) {
|
|
|
89387
89953
|
*/
|
|
89388
89954
|
function isFoldedMcpToolNameRule({ rule, category }) {
|
|
89389
89955
|
const { tool } = splitCommandcodeRule(rule);
|
|
89390
|
-
return category.startsWith(MCP_CANONICAL_PREFIX$
|
|
89956
|
+
return category.startsWith(MCP_CANONICAL_PREFIX$8) && !tool.startsWith(MCP_CANONICAL_PREFIX$8);
|
|
89391
89957
|
}
|
|
89392
89958
|
/** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
|
|
89393
89959
|
function scopedMcpRuleTool(rule) {
|
|
89394
89960
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
89395
|
-
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
89961
|
+
return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$8) ? tool : null;
|
|
89396
89962
|
}
|
|
89397
89963
|
/**
|
|
89398
89964
|
* The string entries of a Command Code permission list. Command Code reads
|
|
@@ -89435,7 +90001,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
|
|
|
89435
90001
|
}).filter((rule) => {
|
|
89436
90002
|
const parsed = parseCommandcodeRule(rule);
|
|
89437
90003
|
if (parsed === null || !managedCategories.has(parsed.category)) return true;
|
|
89438
|
-
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$
|
|
90004
|
+
const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$13})`);
|
|
89439
90005
|
if (key !== "allow" && isStricterAction({
|
|
89440
90006
|
action: key,
|
|
89441
90007
|
existing: replacement
|
|
@@ -89492,7 +90058,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
89492
90058
|
const ranked = /* @__PURE__ */ new Map();
|
|
89493
90059
|
const managedCategories = /* @__PURE__ */ new Set();
|
|
89494
90060
|
for (const [category, rules] of Object.entries(permission)) {
|
|
89495
|
-
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$
|
|
90061
|
+
const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$13);
|
|
89496
90062
|
const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
|
|
89497
90063
|
if (managedCategory !== null) managedCategories.add(managedCategory.category);
|
|
89498
90064
|
for (const [pattern, action] of Object.entries(rules)) {
|
|
@@ -89561,7 +90127,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
|
|
|
89561
90127
|
function skippedAllowImportMessage({ rule, category }) {
|
|
89562
90128
|
const { tool, inner } = splitCommandcodeRule(rule);
|
|
89563
90129
|
if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
|
|
89564
|
-
if (tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
90130
|
+
if (tool.startsWith(MCP_CANONICAL_PREFIX$8) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
|
|
89565
90131
|
return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
|
|
89566
90132
|
}
|
|
89567
90133
|
/**
|
|
@@ -89756,7 +90322,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
|
|
|
89756
90322
|
}
|
|
89757
90323
|
// src/features/permissions/continue-permissions.ts
|
|
89758
90324
|
const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
|
|
89759
|
-
const CATCH_ALL_PATTERN$
|
|
90325
|
+
const CATCH_ALL_PATTERN$12 = "*";
|
|
89760
90326
|
/**
|
|
89761
90327
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
|
|
89762
90328
|
* would match `Tool(**)` against every argument just as the bare tool name
|
|
@@ -89764,7 +90330,7 @@ const CATCH_ALL_PATTERN$11 = "*";
|
|
|
89764
90330
|
* the bare entry instead of being skipped as a pattern-specific rule.
|
|
89765
90331
|
*/
|
|
89766
90332
|
function normalizeCatchAllPattern$1(pattern) {
|
|
89767
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
90333
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$12 : pattern;
|
|
89768
90334
|
}
|
|
89769
90335
|
const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
|
|
89770
90336
|
bash: "Bash",
|
|
@@ -89808,7 +90374,7 @@ function parseContinuePermissionEntry(entry) {
|
|
|
89808
90374
|
if (toolName === "") return;
|
|
89809
90375
|
return {
|
|
89810
90376
|
toolName,
|
|
89811
|
-
pattern: pattern === "" ? CATCH_ALL_PATTERN$
|
|
90377
|
+
pattern: pattern === "" ? CATCH_ALL_PATTERN$12 : pattern
|
|
89812
90378
|
};
|
|
89813
90379
|
}
|
|
89814
90380
|
/**
|
|
@@ -89973,7 +90539,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
|
|
|
89973
90539
|
logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
89974
90540
|
continue;
|
|
89975
90541
|
}
|
|
89976
|
-
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$
|
|
90542
|
+
if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$12) {
|
|
89977
90543
|
logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
|
|
89978
90544
|
continue;
|
|
89979
90545
|
}
|
|
@@ -90029,13 +90595,116 @@ const AUTO_APPROVE_KEYS = {
|
|
|
90029
90595
|
webfetch: "chat.tools.urls.autoApprove"
|
|
90030
90596
|
};
|
|
90031
90597
|
/**
|
|
90032
|
-
* The workspace setting holding the starting permission level of new
|
|
90033
|
-
* sessions, authored through the
|
|
90598
|
+
* The workspace setting holding the starting permission level of new Local
|
|
90599
|
+
* (`local` harness) chat sessions, authored through the
|
|
90600
|
+
* `copilot.defaultPermissionMode` override. VS Code 1.144 narrowed it to Local
|
|
90601
|
+
* sessions; the Copilot harness reads {@link DEFAULT_CONFIGURATION_KEY}.
|
|
90034
90602
|
*
|
|
90035
90603
|
* @see https://code.visualstudio.com/docs/agents/reference/ai-settings
|
|
90036
90604
|
*/
|
|
90037
90605
|
const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
|
|
90038
90606
|
/**
|
|
90607
|
+
* The workspace setting holding the starting `mode` (`interactive` / `plan` /
|
|
90608
|
+
* `autopilot`) and `approvals` (`manual` / `assisted` / `allowAll`) of new
|
|
90609
|
+
* agent sessions such as the Copilot harness, the default since VS Code 1.143.
|
|
90610
|
+
* It shipped in stable VS Code 1.144 as an object setting with
|
|
90611
|
+
* `additionalProperties: false`; older releases ignore it.
|
|
90612
|
+
*
|
|
90613
|
+
* @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
|
|
90614
|
+
* @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/common/constants.ts
|
|
90615
|
+
*/
|
|
90616
|
+
const DEFAULT_CONFIGURATION_KEY = "chat.defaultConfiguration";
|
|
90617
|
+
/**
|
|
90618
|
+
* What each `copilot.defaultPermissionMode` value writes. `local` goes to
|
|
90619
|
+
* `chat.permissions.default`; `approvals` and `autopilot` go to
|
|
90620
|
+
* `chat.defaultConfiguration`. The Local picker has no Assisted level, so
|
|
90621
|
+
* `assisted` starts Local sessions with Default Permissions — the stricter
|
|
90622
|
+
* fallback. Autopilot is a permission level in the Local picker but a `mode`
|
|
90623
|
+
* in `chat.defaultConfiguration`, where it runs with every tool call approved.
|
|
90624
|
+
*/
|
|
90625
|
+
const DEFAULT_PERMISSION_MODE_MAPPINGS = {
|
|
90626
|
+
default: {
|
|
90627
|
+
local: "default",
|
|
90628
|
+
approvals: "manual",
|
|
90629
|
+
autopilot: false
|
|
90630
|
+
},
|
|
90631
|
+
autoApprove: {
|
|
90632
|
+
local: "autoApprove",
|
|
90633
|
+
approvals: "allowAll",
|
|
90634
|
+
autopilot: false
|
|
90635
|
+
},
|
|
90636
|
+
autopilot: {
|
|
90637
|
+
local: "autopilot",
|
|
90638
|
+
approvals: "allowAll",
|
|
90639
|
+
autopilot: true
|
|
90640
|
+
},
|
|
90641
|
+
assisted: {
|
|
90642
|
+
local: "default",
|
|
90643
|
+
approvals: "assisted",
|
|
90644
|
+
autopilot: false
|
|
90645
|
+
}
|
|
90646
|
+
};
|
|
90647
|
+
/**
|
|
90648
|
+
* The `chat.defaultConfiguration` value for `mode`. Rulesync owns only whether
|
|
90649
|
+
* the mode is Autopilot: authoring Autopilot sets it, and any other value
|
|
90650
|
+
* removes any Autopilot mode in the file (whether an earlier run or a person
|
|
90651
|
+
* wrote it, since the two cannot be told apart), while an `interactive` or
|
|
90652
|
+
* `plan` mode is kept.
|
|
90653
|
+
*/
|
|
90654
|
+
function buildDefaultConfigurationValue({ defaultPermissionMode, existingSettings }) {
|
|
90655
|
+
const { approvals, autopilot } = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode];
|
|
90656
|
+
const existing = existingSettings[DEFAULT_CONFIGURATION_KEY];
|
|
90657
|
+
const value = { ...isPlainObject(existing) ? existing : {} };
|
|
90658
|
+
if (autopilot) value.mode = "autopilot";
|
|
90659
|
+
else if (value.mode === "autopilot") delete value.mode;
|
|
90660
|
+
value.approvals = approvals;
|
|
90661
|
+
return value;
|
|
90662
|
+
}
|
|
90663
|
+
/**
|
|
90664
|
+
* The `copilot.defaultPermissionMode` values from least to most permissive,
|
|
90665
|
+
* used to settle a file whose two keys disagree.
|
|
90666
|
+
*/
|
|
90667
|
+
const DEFAULT_PERMISSION_MODE_RANK = {
|
|
90668
|
+
default: 0,
|
|
90669
|
+
assisted: 1,
|
|
90670
|
+
autoApprove: 2,
|
|
90671
|
+
autopilot: 3
|
|
90672
|
+
};
|
|
90673
|
+
/**
|
|
90674
|
+
* The `copilot.defaultPermissionMode` a `chat.defaultConfiguration` value
|
|
90675
|
+
* stands for. Autopilot is recognized only together with `allowAll`, the pair
|
|
90676
|
+
* Rulesync writes for it: an Autopilot mode with stricter approvals is read
|
|
90677
|
+
* from `approvals` alone, so that regenerating never escalates it to Allow
|
|
90678
|
+
* All. An object without `approvals` is read as `default`, since VS Code then
|
|
90679
|
+
* starts sessions with Manual permissions.
|
|
90680
|
+
*/
|
|
90681
|
+
function readDefaultConfigurationMode(configuration) {
|
|
90682
|
+
if (!isPlainObject(configuration)) return;
|
|
90683
|
+
switch (configuration.approvals) {
|
|
90684
|
+
case void 0:
|
|
90685
|
+
case "manual": return "default";
|
|
90686
|
+
case "assisted": return "assisted";
|
|
90687
|
+
case "allowAll": return configuration.mode === "autopilot" ? "autopilot" : "autoApprove";
|
|
90688
|
+
default: return;
|
|
90689
|
+
}
|
|
90690
|
+
}
|
|
90691
|
+
/**
|
|
90692
|
+
* Read `copilot.defaultPermissionMode` back from the settings. When the two
|
|
90693
|
+
* keys agree — `chat.permissions.default` holds the Local level Rulesync
|
|
90694
|
+
* writes for the `chat.defaultConfiguration` value — the latter is returned,
|
|
90695
|
+
* since only it can say `assisted`. When they disagree, the less permissive of
|
|
90696
|
+
* the two is returned, so regenerating never raises either harness's starting
|
|
90697
|
+
* permissions. Either key alone is read as it stands.
|
|
90698
|
+
*/
|
|
90699
|
+
function readDefaultPermissionMode(settings) {
|
|
90700
|
+
const fromConfiguration = readDefaultConfigurationMode(settings[DEFAULT_CONFIGURATION_KEY]);
|
|
90701
|
+
const localValue = settings[DEFAULT_PERMISSION_MODE_KEY$1];
|
|
90702
|
+
const fromLocal = COPILOT_LOCAL_PERMISSION_LEVELS.find((level) => level === localValue);
|
|
90703
|
+
if (fromConfiguration === void 0 || fromLocal === void 0) return fromConfiguration ?? fromLocal;
|
|
90704
|
+
if (DEFAULT_PERMISSION_MODE_MAPPINGS[fromConfiguration].local === fromLocal) return fromConfiguration;
|
|
90705
|
+
return DEFAULT_PERMISSION_MODE_RANK[fromLocal] < DEFAULT_PERMISSION_MODE_RANK[fromConfiguration] ? fromLocal : fromConfiguration;
|
|
90706
|
+
}
|
|
90707
|
+
/**
|
|
90039
90708
|
* The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
|
|
90040
90709
|
* at path `a.b` is written to `chat.agent.sandbox.a.b`.
|
|
90041
90710
|
*/
|
|
@@ -90222,9 +90891,6 @@ function readSandboxFromSettings(settings) {
|
|
|
90222
90891
|
}
|
|
90223
90892
|
return Object.keys(sandbox).length > 0 ? sandbox : void 0;
|
|
90224
90893
|
}
|
|
90225
|
-
function isCopilotDefaultPermissionMode(value) {
|
|
90226
|
-
return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
|
|
90227
|
-
}
|
|
90228
90894
|
function asAutoApproveMap(value) {
|
|
90229
90895
|
if (!isPlainObject(value)) return {};
|
|
90230
90896
|
const result = {};
|
|
@@ -90268,9 +90934,11 @@ function buildAutoApproveValue({ category, rules }) {
|
|
|
90268
90934
|
* `false`. A key whose canonical category is absent entirely
|
|
90269
90935
|
* is left untouched, so authoring only `bash` rules never disturbs a
|
|
90270
90936
|
* hand-written edits or urls map.
|
|
90271
|
-
* The `copilot.defaultPermissionMode` override writes
|
|
90272
|
-
* (
|
|
90273
|
-
*
|
|
90937
|
+
* The `copilot.defaultPermissionMode` override writes both
|
|
90938
|
+
* `chat.permissions.default` (Local sessions) and `chat.defaultConfiguration`
|
|
90939
|
+
* (Copilot-harness sessions, VS Code 1.144+; see
|
|
90940
|
+
* {@link DEFAULT_PERMISSION_MODE_MAPPINGS}); without it both keys are left
|
|
90941
|
+
* untouched. The `copilot.sandbox` override writes the Agent Host sandbox
|
|
90274
90942
|
* settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
|
|
90275
90943
|
* warns about every value it writes that loosens the sandbox.
|
|
90276
90944
|
* Only project scope is modeled: VS Code's user-scope settings.json lives at a
|
|
@@ -90321,17 +90989,24 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
90321
90989
|
rules
|
|
90322
90990
|
});
|
|
90323
90991
|
}
|
|
90992
|
+
const existingSettings = () => parseSharedConfig({
|
|
90993
|
+
format: "jsonc",
|
|
90994
|
+
fileContent: existingContent,
|
|
90995
|
+
filePath
|
|
90996
|
+
});
|
|
90324
90997
|
const defaultPermissionMode = config.copilot?.defaultPermissionMode;
|
|
90325
|
-
if (defaultPermissionMode !== void 0)
|
|
90998
|
+
if (defaultPermissionMode !== void 0) {
|
|
90999
|
+
patch[DEFAULT_PERMISSION_MODE_KEY$1] = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode].local;
|
|
91000
|
+
patch[DEFAULT_CONFIGURATION_KEY] = buildDefaultConfigurationValue({
|
|
91001
|
+
defaultPermissionMode,
|
|
91002
|
+
existingSettings: existingSettings()
|
|
91003
|
+
});
|
|
91004
|
+
}
|
|
90326
91005
|
const sandbox = config.copilot?.sandbox;
|
|
90327
91006
|
if (sandbox !== void 0) {
|
|
90328
91007
|
Object.assign(patch, buildSandboxPatch({
|
|
90329
91008
|
sandbox,
|
|
90330
|
-
existingSettings:
|
|
90331
|
-
format: "jsonc",
|
|
90332
|
-
fileContent: existingContent,
|
|
90333
|
-
filePath
|
|
90334
|
-
})
|
|
91009
|
+
existingSettings: existingSettings()
|
|
90335
91010
|
}));
|
|
90336
91011
|
warnOnTrustAffectingEntries({
|
|
90337
91012
|
toolLabel: COPILOT_TOOL_LABEL,
|
|
@@ -90378,10 +91053,10 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
|
|
|
90378
91053
|
for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
|
|
90379
91054
|
if (Object.keys(rules).length > 0) permission[category] = rules;
|
|
90380
91055
|
}
|
|
90381
|
-
const defaultPermissionMode = settings
|
|
91056
|
+
const defaultPermissionMode = readDefaultPermissionMode(settings);
|
|
90382
91057
|
const sandbox = readSandboxFromSettings(settings);
|
|
90383
91058
|
const copilot = {
|
|
90384
|
-
...
|
|
91059
|
+
...defaultPermissionMode !== void 0 && { defaultPermissionMode },
|
|
90385
91060
|
...sandbox !== void 0 && { sandbox }
|
|
90386
91061
|
};
|
|
90387
91062
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
|
|
@@ -90579,14 +91254,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
|
|
|
90579
91254
|
}
|
|
90580
91255
|
};
|
|
90581
91256
|
// src/features/permissions/crush-permissions.ts
|
|
90582
|
-
const CATCH_ALL_PATTERN$
|
|
91257
|
+
const CATCH_ALL_PATTERN$11 = "*";
|
|
90583
91258
|
/**
|
|
90584
91259
|
* A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
|
|
90585
91260
|
* argument patterns at all, so it is the only pattern a rule can carry and
|
|
90586
91261
|
* still be written.
|
|
90587
91262
|
*/
|
|
90588
91263
|
function normalizeCatchAllPattern(pattern) {
|
|
90589
|
-
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$
|
|
91264
|
+
return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
|
|
90590
91265
|
}
|
|
90591
91266
|
const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
90592
91267
|
bash: "bash",
|
|
@@ -90598,11 +91273,11 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
|
|
|
90598
91273
|
webfetch: "fetch"
|
|
90599
91274
|
};
|
|
90600
91275
|
const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
|
|
90601
|
-
const MCP_CANONICAL_PREFIX$
|
|
91276
|
+
const MCP_CANONICAL_PREFIX$7 = "mcp__";
|
|
90602
91277
|
const MCP_CRUSH_PREFIX = "mcp_";
|
|
90603
91278
|
function toCrushToolName(canonical) {
|
|
90604
91279
|
if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
|
|
90605
|
-
if (canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
91280
|
+
if (canonical.startsWith(MCP_CANONICAL_PREFIX$7) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
|
|
90606
91281
|
return canonical;
|
|
90607
91282
|
}
|
|
90608
91283
|
function toCanonicalToolName$5(crushName) {
|
|
@@ -90776,7 +91451,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
|
|
|
90776
91451
|
for (const [rawPattern, action] of Object.entries(rules)) {
|
|
90777
91452
|
if (isPrototypePollutionKey(rawPattern)) continue;
|
|
90778
91453
|
const pattern = normalizeCatchAllPattern(rawPattern);
|
|
90779
|
-
if (pattern === CATCH_ALL_PATTERN$
|
|
91454
|
+
if (pattern === CATCH_ALL_PATTERN$11) {
|
|
90780
91455
|
catchAllAction = action;
|
|
90781
91456
|
continue;
|
|
90782
91457
|
}
|
|
@@ -90849,11 +91524,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
|
|
|
90849
91524
|
};
|
|
90850
91525
|
for (const entry of disabled) {
|
|
90851
91526
|
if (entry === "" || isPrototypePollutionKey(entry)) continue;
|
|
90852
|
-
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$
|
|
91527
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] = "deny";
|
|
90853
91528
|
}
|
|
90854
91529
|
for (const entry of allowed) {
|
|
90855
91530
|
if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
|
|
90856
|
-
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$
|
|
91531
|
+
bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] ??= "allow";
|
|
90857
91532
|
}
|
|
90858
91533
|
return { permission };
|
|
90859
91534
|
}
|
|
@@ -90899,13 +91574,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
|
|
|
90899
91574
|
WebFetch: "webfetch",
|
|
90900
91575
|
Mcp: "mcp"
|
|
90901
91576
|
};
|
|
90902
|
-
const MCP_CANONICAL_PREFIX$
|
|
91577
|
+
const MCP_CANONICAL_PREFIX$6 = "mcp__";
|
|
90903
91578
|
/**
|
|
90904
91579
|
* Returns true if the canonical category is the per-tool MCP form
|
|
90905
91580
|
* `mcp__<server>__<tool>`.
|
|
90906
91581
|
*/
|
|
90907
91582
|
function isMcpScopedCategory(canonical) {
|
|
90908
|
-
return canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
91583
|
+
return canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5;
|
|
90909
91584
|
}
|
|
90910
91585
|
function toCursorType(canonical) {
|
|
90911
91586
|
if (isMcpScopedCategory(canonical)) return "Mcp";
|
|
@@ -90939,7 +91614,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
|
|
|
90939
91614
|
if (match) {
|
|
90940
91615
|
const server = match[1] ?? "*";
|
|
90941
91616
|
const tool = match[2] ?? "*";
|
|
90942
|
-
return `${MCP_CANONICAL_PREFIX$
|
|
91617
|
+
return `${MCP_CANONICAL_PREFIX$6}${server}__${tool}`;
|
|
90943
91618
|
}
|
|
90944
91619
|
return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
|
|
90945
91620
|
}
|
|
@@ -91201,7 +91876,7 @@ function convertCursorToRulesyncPermissions(params) {
|
|
|
91201
91876
|
const { toolName: type, pattern } = parseToolPatternEntry(entry);
|
|
91202
91877
|
const canonical = toCanonicalCategory$1(type, pattern);
|
|
91203
91878
|
if (!permission[canonical]) permission[canonical] = {};
|
|
91204
|
-
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$
|
|
91879
|
+
const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$6) ? "*" : pattern;
|
|
91205
91880
|
permission[canonical][canonicalPattern] = action;
|
|
91206
91881
|
}
|
|
91207
91882
|
};
|
|
@@ -92700,7 +93375,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
|
|
|
92700
93375
|
// src/features/permissions/goose-permissions.ts
|
|
92701
93376
|
const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
|
|
92702
93377
|
const GOOSE_USER_KEY = "user";
|
|
92703
|
-
const CATCH_ALL_PATTERN$
|
|
93378
|
+
const CATCH_ALL_PATTERN$10 = "*";
|
|
92704
93379
|
const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
|
|
92705
93380
|
const GOOSE_TO_RULESYNC_TOOL_NAMES = {
|
|
92706
93381
|
shell: ["bash"],
|
|
@@ -92849,7 +93524,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
|
|
|
92849
93524
|
const assigned = /* @__PURE__ */ new Map();
|
|
92850
93525
|
for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
|
|
92851
93526
|
const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
|
|
92852
|
-
for (const [pattern, action] of Object.entries(rules)) if (pattern !== CATCH_ALL_PATTERN$
|
|
93527
|
+
for (const [pattern, action] of Object.entries(rules)) if (pattern !== CATCH_ALL_PATTERN$10) logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
|
|
92853
93528
|
const { action, downgraded } = failSafeCatchAllAction({
|
|
92854
93529
|
rules,
|
|
92855
93530
|
allToolsRules: category === "*" ? void 0 : config.permission["*"]
|
|
@@ -92882,7 +93557,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
|
|
|
92882
93557
|
const action = GOOSE_LIST_TO_ACTION[key];
|
|
92883
93558
|
for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
|
|
92884
93559
|
permission[category] ??= {};
|
|
92885
|
-
permission[category][CATCH_ALL_PATTERN$
|
|
93560
|
+
permission[category][CATCH_ALL_PATTERN$10] = action;
|
|
92886
93561
|
}
|
|
92887
93562
|
}
|
|
92888
93563
|
return { permission };
|
|
@@ -92892,8 +93567,8 @@ const GROKCLI_UI_KEY = "ui";
|
|
|
92892
93567
|
const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
|
|
92893
93568
|
const GROKCLI_PERMISSION_KEY = "permission";
|
|
92894
93569
|
const GROKCLI_AUTO_PERMISSION_MODE = "auto";
|
|
92895
|
-
const CATCH_ALL_PATTERN$
|
|
92896
|
-
const MCP_CANONICAL_PREFIX$
|
|
93570
|
+
const CATCH_ALL_PATTERN$9 = "*";
|
|
93571
|
+
const MCP_CANONICAL_PREFIX$5 = "mcp__";
|
|
92897
93572
|
const CATEGORY_TO_GROK_TOOL = {
|
|
92898
93573
|
bash: "Bash",
|
|
92899
93574
|
read: "Read",
|
|
@@ -92925,6 +93600,25 @@ const GROK_TOOL_TO_CATEGORY_LOWER = {
|
|
|
92925
93600
|
};
|
|
92926
93601
|
const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
|
|
92927
93602
|
const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
93603
|
+
const GROK_PATH_PATTERN_CATEGORIES = /* @__PURE__ */ new Set([
|
|
93604
|
+
"read",
|
|
93605
|
+
"edit",
|
|
93606
|
+
"write",
|
|
93607
|
+
"grep",
|
|
93608
|
+
"glob"
|
|
93609
|
+
]);
|
|
93610
|
+
const CLAUDE_ABSOLUTE_PATH_PREFIX = "//";
|
|
93611
|
+
/**
|
|
93612
|
+
* Rewrite a canonical path pattern into the anchor Grok understands. A
|
|
93613
|
+
* Claude-style `//abs` absolute pattern becomes `/abs`; without this the rule
|
|
93614
|
+
* would match nothing on Grok, so a `deny` or `ask` rule would silently give
|
|
93615
|
+
* no protection. Every other pattern is returned unchanged.
|
|
93616
|
+
*/
|
|
93617
|
+
function toGrokPathPattern(category, pattern) {
|
|
93618
|
+
if (!GROK_PATH_PATTERN_CATEGORIES.has(category)) return pattern;
|
|
93619
|
+
if (!pattern.startsWith(CLAUDE_ABSOLUTE_PATH_PREFIX)) return pattern;
|
|
93620
|
+
return `/${pattern.replace(/^\/+/, "")}`;
|
|
93621
|
+
}
|
|
92928
93622
|
/**
|
|
92929
93623
|
* Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
|
|
92930
93624
|
* `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
|
|
@@ -92935,18 +93629,20 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
|
|
|
92935
93629
|
* `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
|
|
92936
93630
|
* concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
|
|
92937
93631
|
* Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
|
|
92938
|
-
* under it has no Grok spelling and returns `null`.
|
|
93632
|
+
* under it has no Grok spelling and returns `null`. A Claude-style `//abs`
|
|
93633
|
+
* path pattern on a file tool is rewritten to Grok's `/abs` (see
|
|
93634
|
+
* `toGrokPathPattern`).
|
|
92939
93635
|
*/
|
|
92940
93636
|
function buildGrokEntry(category, pattern) {
|
|
92941
|
-
if (category === "*") return pattern === CATCH_ALL_PATTERN$
|
|
92942
|
-
if (category.startsWith(MCP_CANONICAL_PREFIX$
|
|
93637
|
+
if (category === "*") return pattern === CATCH_ALL_PATTERN$9 ? GROK_ALL_TOOLS_ENTRY : null;
|
|
93638
|
+
if (category.startsWith(MCP_CANONICAL_PREFIX$5)) {
|
|
92943
93639
|
const remainder = category.slice(5);
|
|
92944
93640
|
return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
|
|
92945
93641
|
}
|
|
92946
|
-
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$
|
|
93642
|
+
if (category === "mcp") return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
|
|
92947
93643
|
const tool = CATEGORY_TO_GROK_TOOL[category];
|
|
92948
93644
|
if (tool === void 0) return null;
|
|
92949
|
-
return pattern === CATCH_ALL_PATTERN$
|
|
93645
|
+
return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
|
|
92950
93646
|
}
|
|
92951
93647
|
/**
|
|
92952
93648
|
* Parse a Grok Claude-style permission entry back into a canonical category +
|
|
@@ -92968,21 +93664,21 @@ function parseGrokEntry(entry) {
|
|
|
92968
93664
|
}
|
|
92969
93665
|
if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
|
|
92970
93666
|
category: "*",
|
|
92971
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93667
|
+
pattern: CATCH_ALL_PATTERN$9
|
|
92972
93668
|
};
|
|
92973
|
-
if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$
|
|
93669
|
+
if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$5)) return parseClaudeStyleMcpEntry(tool);
|
|
92974
93670
|
if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
|
|
92975
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
92976
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93671
|
+
category: `${MCP_CANONICAL_PREFIX$5}${inner}`,
|
|
93672
|
+
pattern: CATCH_ALL_PATTERN$9
|
|
92977
93673
|
} : {
|
|
92978
93674
|
category: "mcp",
|
|
92979
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93675
|
+
pattern: CATCH_ALL_PATTERN$9
|
|
92980
93676
|
};
|
|
92981
93677
|
const category = GROK_TOOL_TO_CATEGORY[tool];
|
|
92982
93678
|
if (category === void 0) return null;
|
|
92983
93679
|
return {
|
|
92984
93680
|
category,
|
|
92985
|
-
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$
|
|
93681
|
+
pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$9
|
|
92986
93682
|
};
|
|
92987
93683
|
}
|
|
92988
93684
|
/**
|
|
@@ -92999,14 +93695,14 @@ function parseGrokEntry(entry) {
|
|
|
92999
93695
|
function parseClaudeStyleMcpEntry(entry) {
|
|
93000
93696
|
const rest = entry.slice(5);
|
|
93001
93697
|
if (rest.length === 0) return null;
|
|
93002
|
-
if (rest === CATCH_ALL_PATTERN$
|
|
93698
|
+
if (rest === CATCH_ALL_PATTERN$9) return {
|
|
93003
93699
|
category: "mcp",
|
|
93004
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93700
|
+
pattern: CATCH_ALL_PATTERN$9
|
|
93005
93701
|
};
|
|
93006
93702
|
const address = rest.includes("__") ? rest : `${rest}__*`;
|
|
93007
93703
|
return {
|
|
93008
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
93009
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93704
|
+
category: `${MCP_CANONICAL_PREFIX$5}${address}`,
|
|
93705
|
+
pattern: CATCH_ALL_PATTERN$9
|
|
93010
93706
|
};
|
|
93011
93707
|
}
|
|
93012
93708
|
/**
|
|
@@ -93027,20 +93723,20 @@ function parseGrokRule(rule) {
|
|
|
93027
93723
|
if (typeof tool !== "string") return null;
|
|
93028
93724
|
if (pattern !== void 0 && typeof pattern !== "string") return null;
|
|
93029
93725
|
const trimmedPattern = pattern?.trim() ?? "";
|
|
93030
|
-
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$
|
|
93726
|
+
const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$9;
|
|
93031
93727
|
const lowerTool = tool.trim().toLowerCase();
|
|
93032
|
-
if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
93728
|
+
if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
|
|
93033
93729
|
category: "*",
|
|
93034
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93730
|
+
pattern: CATCH_ALL_PATTERN$9,
|
|
93035
93731
|
action
|
|
93036
93732
|
} : null;
|
|
93037
|
-
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$
|
|
93733
|
+
if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
|
|
93038
93734
|
category: "mcp",
|
|
93039
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93735
|
+
pattern: CATCH_ALL_PATTERN$9,
|
|
93040
93736
|
action
|
|
93041
93737
|
} : {
|
|
93042
|
-
category: `${MCP_CANONICAL_PREFIX$
|
|
93043
|
-
pattern: CATCH_ALL_PATTERN$
|
|
93738
|
+
category: `${MCP_CANONICAL_PREFIX$5}${resolvedPattern}`,
|
|
93739
|
+
pattern: CATCH_ALL_PATTERN$9,
|
|
93044
93740
|
action
|
|
93045
93741
|
};
|
|
93046
93742
|
if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
|
|
@@ -93190,7 +93886,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
|
|
|
93190
93886
|
throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
|
|
93191
93887
|
}
|
|
93192
93888
|
const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
|
|
93193
|
-
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$
|
|
93889
|
+
const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$9]: legacyModeAction(parsed) } } };
|
|
93194
93890
|
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
|
|
93195
93891
|
}
|
|
93196
93892
|
static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
|
|
@@ -93375,6 +94071,195 @@ function ensureCategory(permission, category) {
|
|
|
93375
94071
|
function removeEmptyCategories(permission) {
|
|
93376
94072
|
for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
|
|
93377
94073
|
}
|
|
94074
|
+
/** The prefix Hermes's MCP trust gate keys its persisted "Always" answers with. */
|
|
94075
|
+
const MCP_ALLOWLIST_PREFIX = "mcp:";
|
|
94076
|
+
const MCP_CANONICAL_PREFIX$4 = "mcp__";
|
|
94077
|
+
const CATCH_ALL_PATTERN$8 = "*";
|
|
94078
|
+
/**
|
|
94079
|
+
* Whether a `command_allowlist` entry is an MCP trust-gate grant rather than a
|
|
94080
|
+
* shell-command pattern. Since Hermes Agent v0.22.6, answering **Always** to
|
|
94081
|
+
* the approval prompt of a `trust: untrusted` MCP server appends
|
|
94082
|
+
* `mcp:<server>/<tool>` to `command_allowlist` (`_trust_approval_key` in
|
|
94083
|
+
* `tools/mcp_tool_handlers.py`), and the gate looks the key up by exact set
|
|
94084
|
+
* membership, so no `mcp:` entry is ever a command.
|
|
94085
|
+
*/
|
|
94086
|
+
function isMcpAllowlistEntry(entry) {
|
|
94087
|
+
return entry.startsWith(MCP_ALLOWLIST_PREFIX);
|
|
94088
|
+
}
|
|
94089
|
+
/**
|
|
94090
|
+
* The canonical category (`mcp__<server>__<tool>`) a Hermes `mcp:<server>/<tool>`
|
|
94091
|
+
* grant stands for, or `null` when it has none: a malformed entry, a server
|
|
94092
|
+
* name containing `__` (the canonical name would read back as another server),
|
|
94093
|
+
* or a `*` in either name (Hermes compares the key literally, while a canonical
|
|
94094
|
+
* `*` is a wildcard).
|
|
94095
|
+
*/
|
|
94096
|
+
function mcpAllowlistEntryToCategory(entry) {
|
|
94097
|
+
const parsed = parseMcpAllowlistEntry(entry);
|
|
94098
|
+
if (parsed === null) return null;
|
|
94099
|
+
const { server, tool } = parsed;
|
|
94100
|
+
if (server.includes("__") || server.includes(CATCH_ALL_PATTERN$8)) return null;
|
|
94101
|
+
if (tool.includes(CATCH_ALL_PATTERN$8)) return null;
|
|
94102
|
+
return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
|
|
94103
|
+
}
|
|
94104
|
+
/** The server and tool an `mcp:<server>/<tool>` entry names, or `null` when it is malformed. */
|
|
94105
|
+
function parseMcpAllowlistEntry(entry) {
|
|
94106
|
+
if (!isMcpAllowlistEntry(entry)) return null;
|
|
94107
|
+
const rest = entry.slice(4);
|
|
94108
|
+
const slash = rest.indexOf("/");
|
|
94109
|
+
const server = slash === -1 ? "" : rest.slice(0, slash);
|
|
94110
|
+
const tool = slash === -1 ? "" : rest.slice(slash + 1);
|
|
94111
|
+
return server.length === 0 || tool.length === 0 ? null : {
|
|
94112
|
+
server,
|
|
94113
|
+
tool
|
|
94114
|
+
};
|
|
94115
|
+
}
|
|
94116
|
+
/**
|
|
94117
|
+
* The server and tool a canonical `mcp__<server>__<tool>` category names, or
|
|
94118
|
+
* `null` when it names no single tool (a whole server, a wildcard, or the bare
|
|
94119
|
+
* `mcp` category). Only such a category can become one Hermes grant.
|
|
94120
|
+
*/
|
|
94121
|
+
function parseMcpToolCategory(category) {
|
|
94122
|
+
if (!category.startsWith(MCP_CANONICAL_PREFIX$4)) return null;
|
|
94123
|
+
const remainder = category.slice(5);
|
|
94124
|
+
const separator = remainder.indexOf("__");
|
|
94125
|
+
if (separator <= 0) return null;
|
|
94126
|
+
const server = remainder.slice(0, separator);
|
|
94127
|
+
const tool = remainder.slice(separator + 2);
|
|
94128
|
+
if (tool.length === 0 || server.includes("/") || server.includes(CATCH_ALL_PATTERN$8) || tool.includes(CATCH_ALL_PATTERN$8)) return null;
|
|
94129
|
+
return {
|
|
94130
|
+
server,
|
|
94131
|
+
tool
|
|
94132
|
+
};
|
|
94133
|
+
}
|
|
94134
|
+
function isMcpCategory$1(category) {
|
|
94135
|
+
return category === "mcp" || category.startsWith(MCP_CANONICAL_PREFIX$4);
|
|
94136
|
+
}
|
|
94137
|
+
function isRestricting(action) {
|
|
94138
|
+
return action === "deny" || action === "ask";
|
|
94139
|
+
}
|
|
94140
|
+
/** The first `deny` or `ask` rule of a category, as `<category>: <pattern> (<action>)`. */
|
|
94141
|
+
function firstRestrictingRule(category, rules, covers = () => true) {
|
|
94142
|
+
const found = Object.entries(rules ?? {}).find(([pattern, action]) => isRestricting(action) && covers(pattern));
|
|
94143
|
+
return found ? `${category}: ${found[0]} (${found[1]})` : null;
|
|
94144
|
+
}
|
|
94145
|
+
/**
|
|
94146
|
+
* Whether an MCP name glob (the remainder of an `mcp__...` category, or a
|
|
94147
|
+
* pattern of the bare `mcp` category) covers `server`'s tool `tool`: it names
|
|
94148
|
+
* the tool (`github__delete_repo`, `github__*`, `*__delete_repo`, `*`) or the
|
|
94149
|
+
* whole server (`github`).
|
|
94150
|
+
*/
|
|
94151
|
+
function mcpNameGlobCovers(glob, server, tool) {
|
|
94152
|
+
return glob === `${server}__${tool}` || glob === server || matchesGlob(glob, `${server}__${tool}`) || matchesGlob(glob, server);
|
|
94153
|
+
}
|
|
94154
|
+
/**
|
|
94155
|
+
* The `deny` or `ask` rule that outranks an allow on `server`'s tool `tool`, or
|
|
94156
|
+
* `null` when none does: any one under the all-tools `*` category (a rule there
|
|
94157
|
+
* applies to every tool, so even a narrower pattern restricts some of the
|
|
94158
|
+
* tool's calls), one under the bare `mcp` category whose pattern covers the
|
|
94159
|
+
* tool, or any one in an `mcp__...` category whose name covers the tool — its
|
|
94160
|
+
* own category included, since a deny or ask scoped to some of its calls still
|
|
94161
|
+
* forbids approving all of them. A Hermes grant approves every call of the
|
|
94162
|
+
* tool and Hermes has no ask tier, so such an allow is withheld, the same way
|
|
94163
|
+
* a `bash` allow a stricter rule covers is.
|
|
94164
|
+
*/
|
|
94165
|
+
function findMcpToolRestriction(permission, server, tool) {
|
|
94166
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
94167
|
+
let found = null;
|
|
94168
|
+
if (category === "*") found = firstRestrictingRule(category, rules);
|
|
94169
|
+
else if (category === "mcp") found = firstRestrictingRule(category, rules, (pattern) => mcpNameGlobCovers(pattern, server, tool));
|
|
94170
|
+
else if (category.startsWith(MCP_CANONICAL_PREFIX$4) && mcpNameGlobCovers(category.slice(5), server, tool)) found = firstRestrictingRule(category, rules);
|
|
94171
|
+
if (found !== null) return found;
|
|
94172
|
+
}
|
|
94173
|
+
return null;
|
|
94174
|
+
}
|
|
94175
|
+
function isMcpToolRestricted(permission, server, tool) {
|
|
94176
|
+
return findMcpToolRestriction(permission, server, tool) !== null;
|
|
94177
|
+
}
|
|
94178
|
+
/**
|
|
94179
|
+
* Translate the canonical MCP `allow` rules into Hermes trust-gate grants: a
|
|
94180
|
+
* `*` allow on `mcp__<server>__<tool>` becomes `mcp:<server>/<tool>`. Every
|
|
94181
|
+
* other MCP allow (a whole server, a wildcard, a narrower pattern) has no
|
|
94182
|
+
* Hermes spelling, and an allow a deny or ask covers is withheld.
|
|
94183
|
+
*/
|
|
94184
|
+
function collectMcpAllowlistEntries(permission) {
|
|
94185
|
+
const entries = [];
|
|
94186
|
+
const withheldCategories = [];
|
|
94187
|
+
const unexpressedRules = [];
|
|
94188
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
94189
|
+
if (!isMcpCategory$1(category)) continue;
|
|
94190
|
+
const parsed = parseMcpToolCategory(category);
|
|
94191
|
+
for (const pattern of patternsByAction(rules, "allow")) if (parsed === null || pattern !== CATCH_ALL_PATTERN$8) unexpressedRules.push(`${category}: ${pattern}`);
|
|
94192
|
+
else {
|
|
94193
|
+
const restriction = findMcpToolRestriction(permission, parsed.server, parsed.tool);
|
|
94194
|
+
if (restriction === null) entries.push(`${MCP_ALLOWLIST_PREFIX}${parsed.server}/${parsed.tool}`);
|
|
94195
|
+
else withheldCategories.push(`${category} (covered by ${restriction})`);
|
|
94196
|
+
}
|
|
94197
|
+
}
|
|
94198
|
+
return {
|
|
94199
|
+
entries,
|
|
94200
|
+
withheldCategories,
|
|
94201
|
+
unexpressedRules
|
|
94202
|
+
};
|
|
94203
|
+
}
|
|
94204
|
+
function warnAboutUnwrittenMcpAllows({ withheldCategories, unexpressedRules, logger }) {
|
|
94205
|
+
if (withheldCategories.length > 0) warnWithFallback(logger, `Hermes Agent's command_allowlist was not given the MCP allow rule(s) for ${withheldCategories.join(", ")}: a deny or ask rule covers each, a Hermes grant approves every call of the tool, and Hermes has no ask tier.`);
|
|
94206
|
+
if (unexpressedRules.length > 0) warnWithFallback(logger, `Hermes Agent grants MCP tools one at a time (command_allowlist entries 'mcp:<server>/<tool>'), so only a '*' allow on an 'mcp__<server>__<tool>' category can be written; the MCP allow rule(s) ${unexpressedRules.join(", ")} were skipped and survive only in the permissions.rulesync round-trip block.`);
|
|
94207
|
+
}
|
|
94208
|
+
/**
|
|
94209
|
+
* Make the native `mcp:<server>/<tool>` grants authoritative for the canonical
|
|
94210
|
+
* single-tool MCP allows they can speak for. An allow the generator withheld
|
|
94211
|
+
* because a deny or ask covers it was never written, so its absence
|
|
94212
|
+
* is not a retraction and it is kept, as is every MCP allow with no Hermes
|
|
94213
|
+
* spelling. A grant with no canonical spelling stays only in Hermes, where
|
|
94214
|
+
* generation keeps it (see `setFileContent`).
|
|
94215
|
+
*/
|
|
94216
|
+
function reconcileMcpAllowlist({ permission, mcpEntries, logger }) {
|
|
94217
|
+
for (const [category, rules] of Object.entries(permission)) {
|
|
94218
|
+
const parsed = parseMcpToolCategory(category);
|
|
94219
|
+
if (parsed !== null && rules[CATCH_ALL_PATTERN$8] === "allow" && !isMcpToolRestricted(permission, parsed.server, parsed.tool)) delete rules[CATCH_ALL_PATTERN$8];
|
|
94220
|
+
}
|
|
94221
|
+
const keptRestrictions = [];
|
|
94222
|
+
for (const entry of mcpEntries) {
|
|
94223
|
+
const category = mcpAllowlistEntryToCategory(entry);
|
|
94224
|
+
if (category === null) continue;
|
|
94225
|
+
const rules = ensureCategory(permission, category);
|
|
94226
|
+
if (isRestricting(rules[CATCH_ALL_PATTERN$8])) {
|
|
94227
|
+
keptRestrictions.push(entry);
|
|
94228
|
+
continue;
|
|
94229
|
+
}
|
|
94230
|
+
rules[CATCH_ALL_PATTERN$8] = "allow";
|
|
94231
|
+
}
|
|
94232
|
+
if (keptRestrictions.length > 0) warnWithFallback(logger, `Hermes Agent's command_allowlist grants ${keptRestrictions.join(", ")}, but the permissions.rulesync block denies or asks for those MCP tools; the canonical rule was kept and the grant was not imported.`);
|
|
94233
|
+
}
|
|
94234
|
+
/**
|
|
94235
|
+
* The `mcp:` grants in an existing `command_allowlist` that rulesync does not
|
|
94236
|
+
* manage and must keep when it rewrites the list: Hermes appends one whenever
|
|
94237
|
+
* the user answers **Always** on an untrusted MCP server, so dropping it would
|
|
94238
|
+
* silently revoke the grant. A grant is managed — and so retracted with its
|
|
94239
|
+
* canonical rule — when the previous or the new canonical block has a rule
|
|
94240
|
+
* for its `mcp__<server>__<tool>` category, or when the new block restricts
|
|
94241
|
+
* the tool in any way.
|
|
94242
|
+
*/
|
|
94243
|
+
function unmanagedMcpAllowlistEntries({ existing, previousPermission, nextPermission, nextAllowlist }) {
|
|
94244
|
+
return (isStringArray(existing.command_allowlist) ? existing.command_allowlist : []).filter((entry) => {
|
|
94245
|
+
if (!isMcpAllowlistEntry(entry) || nextAllowlist.includes(entry)) return false;
|
|
94246
|
+
const parsed = parseMcpAllowlistEntry(entry);
|
|
94247
|
+
if (parsed !== null && isMcpToolRestricted(nextPermission, parsed.server, parsed.tool)) return false;
|
|
94248
|
+
const category = mcpAllowlistEntryToCategory(entry);
|
|
94249
|
+
return category === null || !Object.hasOwn(previousPermission, category) && !Object.hasOwn(nextPermission, category);
|
|
94250
|
+
});
|
|
94251
|
+
}
|
|
94252
|
+
/** The validated canonical `permission` block of a config's `permissions.rulesync` blob. */
|
|
94253
|
+
function canonicalPermissionOf(config) {
|
|
94254
|
+
const parsed = RulesyncPermissionsFileSchema.safeParse({ permission: provenancePermissionOf(config) });
|
|
94255
|
+
return parsed.success ? parsed.data.permission : {};
|
|
94256
|
+
}
|
|
94257
|
+
/** The canonical `permission` block of a config's `permissions.rulesync` blob, if any. */
|
|
94258
|
+
function provenancePermissionOf(config) {
|
|
94259
|
+
const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
|
|
94260
|
+
const provenance = isRecord(permissionsRoot.rulesync) ? permissionsRoot.rulesync : {};
|
|
94261
|
+
return isRecord(provenance.permission) ? provenance.permission : {};
|
|
94262
|
+
}
|
|
93378
94263
|
/**
|
|
93379
94264
|
* Make the native `command_allowlist` authoritative for the `bash` allow rules
|
|
93380
94265
|
* it can speak for, and for those only. The allowlist is a list of shell-command
|
|
@@ -93392,8 +94277,9 @@ function reconcileCommandAllowlist({ permission, commandAllowlist }) {
|
|
|
93392
94277
|
writesAllToolsDeny: false
|
|
93393
94278
|
});
|
|
93394
94279
|
const withheld = new Set(shadowedAllowPatterns);
|
|
94280
|
+
const isWithheld = (pattern) => withheld.has(pattern) || isMcpAllowlistEntry(pattern);
|
|
93395
94281
|
const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
|
|
93396
|
-
for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !
|
|
94282
|
+
for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !isWithheld(pattern)) delete rules[pattern];
|
|
93397
94283
|
for (const pattern of commandAllowlist) rules[pattern] = "allow";
|
|
93398
94284
|
}
|
|
93399
94285
|
/**
|
|
@@ -93481,17 +94367,31 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
93481
94367
|
return true;
|
|
93482
94368
|
}
|
|
93483
94369
|
setFileContent(fileContent) {
|
|
94370
|
+
const patch = parseSharedConfig({
|
|
94371
|
+
format: "yaml",
|
|
94372
|
+
fileContent: this.fileContent
|
|
94373
|
+
});
|
|
94374
|
+
if (isStringArray(patch.command_allowlist)) {
|
|
94375
|
+
const existing = parseSharedConfig({
|
|
94376
|
+
format: "yaml",
|
|
94377
|
+
fileContent
|
|
94378
|
+
});
|
|
94379
|
+
const kept = unmanagedMcpAllowlistEntries({
|
|
94380
|
+
existing,
|
|
94381
|
+
previousPermission: provenancePermissionOf(existing),
|
|
94382
|
+
nextPermission: canonicalPermissionOf(patch),
|
|
94383
|
+
nextAllowlist: patch.command_allowlist
|
|
94384
|
+
});
|
|
94385
|
+
if (kept.length > 0) patch.command_allowlist = [...patch.command_allowlist, ...kept];
|
|
94386
|
+
}
|
|
93484
94387
|
this.fileContent = applySharedConfigPatch({
|
|
93485
94388
|
fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
|
|
93486
94389
|
feature: "permissions",
|
|
93487
94390
|
existingContent: fileContent,
|
|
93488
|
-
patch
|
|
93489
|
-
format: "yaml",
|
|
93490
|
-
fileContent: this.fileContent
|
|
93491
|
-
})
|
|
94391
|
+
patch
|
|
93492
94392
|
});
|
|
93493
94393
|
}
|
|
93494
|
-
toRulesyncPermissions() {
|
|
94394
|
+
toRulesyncPermissions({ logger } = {}) {
|
|
93495
94395
|
const config = parseSharedConfig({
|
|
93496
94396
|
format: "yaml",
|
|
93497
94397
|
fileContent: this.getFileContent()
|
|
@@ -93503,9 +94403,15 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
93503
94403
|
}) : rawProvenance);
|
|
93504
94404
|
const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
|
|
93505
94405
|
const permission = clonePermissionBlock(provenance.permission);
|
|
94406
|
+
const commandAllowlist = isStringArray(config.command_allowlist) ? config.command_allowlist : [];
|
|
93506
94407
|
reconcileCommandAllowlist({
|
|
93507
94408
|
permission,
|
|
93508
|
-
commandAllowlist:
|
|
94409
|
+
commandAllowlist: commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry))
|
|
94410
|
+
});
|
|
94411
|
+
reconcileMcpAllowlist({
|
|
94412
|
+
permission,
|
|
94413
|
+
mcpEntries: commandAllowlist.filter(isMcpAllowlistEntry),
|
|
94414
|
+
logger
|
|
93509
94415
|
});
|
|
93510
94416
|
const approvals = isRecord(config.approvals) ? config.approvals : {};
|
|
93511
94417
|
reconcileNativeDenies({
|
|
@@ -93563,8 +94469,16 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
|
|
|
93563
94469
|
logger
|
|
93564
94470
|
});
|
|
93565
94471
|
const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
|
|
94472
|
+
const mcpAllows = collectMcpAllowlistEntries(permissionBlock);
|
|
94473
|
+
warnAboutUnwrittenMcpAllows({
|
|
94474
|
+
...mcpAllows,
|
|
94475
|
+
logger
|
|
94476
|
+
});
|
|
94477
|
+
const bashMcpAllows = commandAllowlist.filter(isMcpAllowlistEntry);
|
|
94478
|
+
if (bashMcpAllows.length > 0) warnWithFallback(logger, `Hermes Agent reads 'mcp:' entries in command_allowlist as MCP tool grants, so the bash allow rule(s) ${bashMcpAllows.join(", ")} were skipped; write them as a '*' allow on an 'mcp__<server>__<tool>' category instead.`);
|
|
94479
|
+
const allowlist = [...commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry)), ...mcpAllows.entries];
|
|
93566
94480
|
let config = {};
|
|
93567
|
-
if (
|
|
94481
|
+
if (allowlist.length > 0) config.command_allowlist = allowlist;
|
|
93568
94482
|
if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
|
|
93569
94483
|
if (webfetchDeny.length > 0) config.security = { website_blocklist: {
|
|
93570
94484
|
enabled: true,
|
|
@@ -100405,6 +101319,7 @@ const TAKT_SECURITY_POLICIES = {
|
|
|
100405
101319
|
],
|
|
100406
101320
|
workflow_runtime_prepare: ["custom_scripts"],
|
|
100407
101321
|
workflow_command_gates: ["custom_scripts"],
|
|
101322
|
+
workflow_system_commands: ["custom_scripts"],
|
|
100408
101323
|
sync_conflict_resolver: ["auto_approve_tools"],
|
|
100409
101324
|
allow_git_hooks: null,
|
|
100410
101325
|
allow_git_filters: null
|