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.
@@ -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$384 } = node_path.posix;
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$384(RULES_FEATURE_SUBDIR, ".curated");
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$384(SKILLS_FEATURE_SUBDIR, ".curated");
398
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
399
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
400
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
401
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
402
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
403
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
404
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
405
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
406
- join$384(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
407
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
408
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
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$384(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
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$384(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
414
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$384(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
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 starting permission level VS Code Copilot Chat gives new chat sessions
8404
- * (`chat.permissions.default`). Values are VS Code's own: `default` (Default
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 COPILOT_DEFAULT_PERMISSION_MODES = [
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
- * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
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`) are emitted, and
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
- return (await Promise.all(rulesyncSkills.map(async (rulesyncSkill) => {
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 `${sanitizeOpencodeMcpNamePart(serverName)}_${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
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 = `${sanitizeOpencodeMcpNamePart(serverName)}_`;
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: /* @__PURE__ */ new Set(),
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 relativeDirPath === this.getOutputStylesDirPath() && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
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 !frontmatter.root && frontmatter.factorydroid?.channel === "output-style";
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
- if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(`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}.`);
47653
- const stem = (0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath));
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
- ...name !== void 0 && { name },
47659
- ...frontmatter.description !== void 0 && { description: frontmatter.description }
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 relativeDirPath === QODER_OUTPUT_STYLES_DIR_PATH && (0, node_path.extname)(relativeFilePath) === ".md" && (0, node_path.basename)(relativeFilePath) === relativeFilePath;
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 !frontmatter.root && frontmatter.qoder?.channel === "output-style";
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
- if ((0, node_path.basename)(sourcePath) !== sourcePath) throw new Error(`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}.`);
49413
- const name = qoder?.name;
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: `${(0, node_path.basename)(sourcePath, (0, node_path.extname)(sourcePath))}.md`,
49418
- frontmatter: {
49419
- ...name !== void 0 && { name },
49420
- ...description !== void 0 && { description }
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
- const { command: stripped, unrestored, retained } = importProjectDirVariable({
56694
- command: cmd,
56695
- projectDirVar,
56696
- firstWordOnly: isExecFormHook({
56697
- args,
56698
- converterConfig
56699
- }) || !runsInPosixShell({
56700
- shell,
56701
- converterConfig
56702
- })
56703
- });
56704
- if (warn && unrestored) warn(`Hook command ${quoteValueForWarning(cmd)} was imported as ${quoteValueForWarning(stripped)}: a ${projectDirVar} 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.`);
56705
- if (warn && retained) warn(`Hook command ${quoteValueForWarning(cmd)} keeps ${projectDirVar} 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.`);
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 === "string") return JSON.stringify(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 = Object.fromEntries(Object.entries(CANONICAL_TO_CORTEXCODE_TOOL_NAMES).map(([canonical, tool]) => [tool, canonical]));
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 canonicalTransport(config) === "sse" ? "sse" : "streamable_http";
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. Goose also documents
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 === "sse" || gooseType === "streamable_http") {
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
- * | `sse` | `builtin`), `cmd`/`args`/`envs` for stdio, `uri`/`headers` for
74337
- * remote, plus `enabled` and `timeout`.
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 (http/sse) servers.
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: [...mcpServers, ...unmanagedServers] },
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 posix = /^\[:\^?([\s\S]*?):\]/.exec(source.slice(index));
82601
- if (posix !== null) {
82602
- if (!POSIX_CLASSES.has(posix[1] ?? "")) return;
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 += posix[0].length;
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 = source.startsWith("[:", index) ? source.indexOf(":]", index + 2) : -1;
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 result = pattern === "*" ? { target: "*" } : toTarget(action, pattern);
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 === "deny" ? toWidenedDenyTarget(action, pattern) : void 0;
83886
+ const prefix = decision !== "deny" ? void 0 : isMcp ? result.lead : toWidenedDenyTarget(action, pattern);
83387
83887
  if (prefix !== void 0) {
83388
- const scope = PATH_ACTIONS.has(action) ? `every path under \`${prefix}\`` : `every command that starts with \`${prefix}\``;
83389
- logger.warn(`${toolLabel} permissions: widening ${rule} to ${action}(${prefix}). ${result.skipReason}. The deny now blocks ${scope}, overriding any allow or ask for them.`);
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 `${action}(${prefix})`;
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) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83606
- const preservedAsk = (existingPermissions.ask ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
83607
- const preservedDeny = (existingPermissions.deny ?? []).filter((entry) => !managedToolNames.has(parseToolPatternEntry(entry).toolName));
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) => !managedActions.has(parseToolPatternEntry(entry).toolName));
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
- write: "save-file"
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 if (action === "deny") {
84468
- 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.`);
84469
- pattern = "*";
84470
- } else {
84471
- pattern = shellRegexToGlob(regex);
84472
- 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.`);
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 dialog, and whether it processes
85966
- * attachments or expands `@` file mentions, and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
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 and
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("PROGRAMFILES")) return true;
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$13 = "*";
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$13;
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$13;
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$13
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$12 = "*";
89161
- const MCP_CANONICAL_PREFIX$7 = "mcp__";
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$12 || pattern === "";
89207
- if (category.startsWith(MCP_CANONICAL_PREFIX$7)) return catchAll ? category : `${category}(${pattern})`;
89208
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$7}${pattern}`;
89209
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
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$12;
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$12,
89845
+ pattern: CATCH_ALL_PATTERN$13,
89280
89846
  notInAllow: true
89281
89847
  };
89282
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7)) {
89283
- if (server === CATCH_ALL_PATTERN$12) return everyServer;
89284
- if (server.includes(CATCH_ALL_PATTERN$12)) return null;
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$12 ? `${MCP_CANONICAL_PREFIX$7}${server}` : `${MCP_CANONICAL_PREFIX$7}${remainder}`,
89287
- pattern: CATCH_ALL_PATTERN$12,
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$7}${remainder}`;
89292
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
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$12) return everyServer;
89299
- if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
89300
- category: `${MCP_CANONICAL_PREFIX$7}${server}`,
89301
- pattern: CATCH_ALL_PATTERN$12,
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$12,
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$12,
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$12,
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$12,
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$12) return isCatchAllSpecifier(inner) ? {
89932
+ if (tool === CATCH_ALL_PATTERN$13) return isCatchAllSpecifier(inner) ? {
89367
89933
  category: "*",
89368
- pattern: CATCH_ALL_PATTERN$12,
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$7)) return parseMcpCommandcodeRule({
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$7) && !tool.startsWith(MCP_CANONICAL_PREFIX$7);
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$7) ? tool : null;
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$12})`);
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$12);
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$7) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
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$11 = "*";
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$11 : 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$11 : 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$11) {
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 chat
90033
- * sessions, authored through the `copilot.defaultPermissionMode` override.
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 `chat.permissions.default`
90272
- * (the starting permission level of new chat sessions); without it the key is
90273
- * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
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) patch[DEFAULT_PERMISSION_MODE_KEY$1] = defaultPermissionMode;
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: parseSharedConfig({
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[DEFAULT_PERMISSION_MODE_KEY$1];
91056
+ const defaultPermissionMode = readDefaultPermissionMode(settings);
90382
91057
  const sandbox = readSandboxFromSettings(settings);
90383
91058
  const copilot = {
90384
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
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$10 = "*";
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$10 : 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$6 = "mcp__";
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$6) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
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$10) {
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$10] = "deny";
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$10] ??= "allow";
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$5 = "mcp__";
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$5) && canonical.length > 5;
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$5}${server}__${tool}`;
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$5) ? "*" : pattern;
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$9 = "*";
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$9) 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.`);
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$9] = action;
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$8 = "*";
92896
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
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$8 ? GROK_ALL_TOOLS_ENTRY : null;
92942
- if (category.startsWith(MCP_CANONICAL_PREFIX$4)) {
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$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${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$8 || pattern === "" ? tool : `${tool}(${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$8
93667
+ pattern: CATCH_ALL_PATTERN$9
92972
93668
  };
92973
- if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$4)) return parseClaudeStyleMcpEntry(tool);
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$4}${inner}`,
92976
- pattern: CATCH_ALL_PATTERN$8
93671
+ category: `${MCP_CANONICAL_PREFIX$5}${inner}`,
93672
+ pattern: CATCH_ALL_PATTERN$9
92977
93673
  } : {
92978
93674
  category: "mcp",
92979
- pattern: CATCH_ALL_PATTERN$8
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$8
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$8) return {
93698
+ if (rest === CATCH_ALL_PATTERN$9) return {
93003
93699
  category: "mcp",
93004
- pattern: CATCH_ALL_PATTERN$8
93700
+ pattern: CATCH_ALL_PATTERN$9
93005
93701
  };
93006
93702
  const address = rest.includes("__") ? rest : `${rest}__*`;
93007
93703
  return {
93008
- category: `${MCP_CANONICAL_PREFIX$4}${address}`,
93009
- pattern: CATCH_ALL_PATTERN$8
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$8;
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$8 ? {
93728
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93033
93729
  category: "*",
93034
- pattern: CATCH_ALL_PATTERN$8,
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$8 ? {
93733
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93038
93734
  category: "mcp",
93039
- pattern: CATCH_ALL_PATTERN$8,
93735
+ pattern: CATCH_ALL_PATTERN$9,
93040
93736
  action
93041
93737
  } : {
93042
- category: `${MCP_CANONICAL_PREFIX$4}${resolvedPattern}`,
93043
- pattern: CATCH_ALL_PATTERN$8,
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$8]: legacyModeAction(parsed) } } };
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" && !withheld.has(pattern)) delete rules[pattern];
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: parseSharedConfig({
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: isStringArray(config.command_allowlist) ? config.command_allowlist : []
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 (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
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