rulesync 43.4.2 → 43.5.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.
@@ -8378,17 +8378,29 @@ const CodewhalePermissionsOverrideSchema = z.looseObject({
8378
8378
  rules: z.optional(z.array(CodewhalePermissionRuleSchema))
8379
8379
  });
8380
8380
  /**
8381
- * The starting permission level VS Code Copilot Chat gives new chat sessions
8382
- * (`chat.permissions.default`). Values are VS Code's own: `default` (Default
8381
+ * The values VS Code's `chat.permissions.default` setting accepts: the starting
8382
+ * permission level of new Local-harness chat sessions — `default` (Default
8383
8383
  * Permissions), `autoApprove` (Bypass Approvals) and `autopilot` (Autopilot).
8384
8384
  *
8385
8385
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
8386
8386
  */
8387
- const COPILOT_DEFAULT_PERMISSION_MODES = [
8387
+ const COPILOT_LOCAL_PERMISSION_LEVELS = [
8388
8388
  "default",
8389
8389
  "autoApprove",
8390
8390
  "autopilot"
8391
8391
  ];
8392
+ /**
8393
+ * The starting permission level the `copilot.defaultPermissionMode` override
8394
+ * sets for new chat sessions: the {@link COPILOT_LOCAL_PERMISSION_LEVELS}, plus
8395
+ * `assisted` (Assisted permissions, GA for Copilot-harness sessions since VS
8396
+ * Code 1.144 and unavailable to Local sessions). Each value is written to both
8397
+ * `chat.permissions.default` (Local sessions) and the `approvals` / `mode` of
8398
+ * `chat.defaultConfiguration` (Copilot-harness sessions, VS Code 1.144+).
8399
+ *
8400
+ * @see https://code.visualstudio.com/updates/v1_144
8401
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
8402
+ */
8403
+ const COPILOT_DEFAULT_PERMISSION_MODES = [...COPILOT_LOCAL_PERMISSION_LEVELS, "assisted"];
8392
8404
  /** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
8393
8405
  const CopilotSandboxEnabledSchema = z.enum(["off", "on"]);
8394
8406
  /** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
@@ -8429,7 +8441,7 @@ const CopilotSandboxSchema = z.strictObject({
8429
8441
  /**
8430
8442
  * Tool-scoped override block for GitHub Copilot in VS Code.
8431
8443
  * `defaultPermissionMode` writes the workspace `chat.permissions.default`
8432
- * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
8444
+ * and `chat.defaultConfiguration` settings, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
8433
8445
  * has a canonical category.
8434
8446
  */
8435
8447
  const CopilotPermissionsOverrideSchema = z.looseObject({
@@ -9311,6 +9323,7 @@ const TaktPermissionsOverrideSchema = z.looseObject({
9311
9323
  })),
9312
9324
  workflow_runtime_prepare: z.optional(z.object({ custom_scripts: z.optional(z.boolean()) })),
9313
9325
  workflow_command_gates: z.optional(z.object({ custom_scripts: z.optional(z.boolean()) })),
9326
+ workflow_system_commands: z.optional(z.object({ custom_scripts: z.optional(z.boolean()) })),
9314
9327
  sync_conflict_resolver: z.optional(z.object({ auto_approve_tools: z.optional(z.boolean()) })),
9315
9328
  allow_git_hooks: z.optional(z.boolean()),
9316
9329
  allow_git_filters: z.optional(z.boolean())
@@ -9578,6 +9591,7 @@ const CodexSandboxModeSchema = z.enum([
9578
9591
  "workspace-write",
9579
9592
  "danger-full-access"
9580
9593
  ]);
9594
+ const CODEX_U32_MAX = 4294967295;
9581
9595
  /**
9582
9596
  * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
9583
9597
  * value Codex still accepts for backward compatibility, so it is included so
@@ -9646,6 +9660,9 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
9646
9660
  * | `guardian_subagent`), or a table for the richer reviewer config.
9647
9661
  * Defaults to `auto_review` when neither the override nor the existing
9648
9662
  * config sets it.
9663
+ * - `auto_review` — the `[auto_review]` table that configures the reviewer
9664
+ * `approvals_reviewer = "auto_review"` selects (`policy`, `extra_policy`,
9665
+ * `timeout_ms`, …). A loose table, shallow-merged like `apps` / `tui`.
9649
9666
  * - `git_write_rules` — whether the managed profile's `:workspace_roots` table
9650
9667
  * emits the default `.git` carve-out (`".git/**" = "write"`). Codex's
9651
9668
  * `:workspace` baseline makes `.git` read-only, which denies basic git
@@ -9659,7 +9676,8 @@ const CodexBasePermissionProfileSchema = z.enum(CODEX_BASE_PERMISSION_PROFILES);
9659
9676
  * The keys written to `config.toml` are an **allowlist**, not verbatim
9660
9677
  * passthrough: only `CODEXCLI_OVERRIDE_KEYS`
9661
9678
  * (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
9662
- * `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
9679
+ * `sandbox_workspace_write`, `apps`, `approvals_reviewer`, `auto_review`,
9680
+ * `tui`) are emitted, and
9663
9681
  * `computeCodexcliOverridePatch` skips anything else with a warning.
9664
9682
  * `base_permission_profile` and `git_write_rules` are consumed by the profile
9665
9683
  * builder rather than written, as described above, and `permission` is the
@@ -9691,6 +9709,26 @@ const CodexcliPermissionsOverrideSchema = z.looseObject({
9691
9709
  apps: z.optional(z.looseObject({})),
9692
9710
  approvals_reviewer: z.optional(z.union([CodexApprovalsReviewerSchema, z.looseObject({})])),
9693
9711
  /**
9712
+ * The `[auto_review]` table of `config.toml`, which configures the reviewer
9713
+ * that `approvals_reviewer = "auto_review"` (Rulesync's default) selects:
9714
+ * `policy` / `extra_policy` (text inserted into the reviewer prompt),
9715
+ * `timeout_ms` (the review budget, Codex 0.165.0+; `0` waits without a
9716
+ * deadline), `circuit_break_action` (`default` | `strict`) and
9717
+ * `conversation_history_max_output_tokens` (a positive token cap). Codex
9718
+ * rejects the whole config file when one of these has the wrong type or is
9719
+ * out of range (`timeout_ms` is a `u32`, the token cap a `NonZeroUsize`), so
9720
+ * they are checked here; every other key passes through verbatim.
9721
+ *
9722
+ * @see https://github.com/openai/codex/blob/rust-v0.166.0/codex-rs/config/src/config_toml.rs (`AutoReviewToml`)
9723
+ */
9724
+ auto_review: z.optional(z.looseObject({
9725
+ policy: z.optional(z.string()),
9726
+ extra_policy: z.optional(z.string()),
9727
+ timeout_ms: z.optional(z.int().check(z.nonnegative(), z.lte(CODEX_U32_MAX))),
9728
+ circuit_break_action: z.optional(z.enum(["default", "strict"])),
9729
+ conversation_history_max_output_tokens: z.optional(z.int().check(z.positive()))
9730
+ })),
9731
+ /**
9694
9732
  * The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
9695
9733
  * Not a permission surface, but like `apps` it is a top-level table with no
9696
9734
  * canonical category, and Codex adds keys to it faster than an explicit model
@@ -12297,6 +12335,7 @@ const CODEXCLI_OVERRIDE_KEYS = [
12297
12335
  "sandbox_workspace_write",
12298
12336
  "apps",
12299
12337
  "approvals_reviewer",
12338
+ "auto_review",
12300
12339
  "tui"
12301
12340
  ];
12302
12341
  // src/constants/takt-paths.ts
@@ -13719,6 +13758,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13719
13758
  "workflow_arpeggio",
13720
13759
  "workflow_runtime_prepare",
13721
13760
  "workflow_command_gates",
13761
+ "workflow_system_commands",
13722
13762
  "sync_conflict_resolver",
13723
13763
  "allow_git_hooks",
13724
13764
  "allow_git_filters"
@@ -13860,6 +13900,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13860
13900
  "chat.tools.edits.autoApprove",
13861
13901
  "chat.tools.urls.autoApprove",
13862
13902
  "chat.permissions.default",
13903
+ "chat.defaultConfiguration",
13863
13904
  "chat.agent.sandbox.enabled",
13864
13905
  "chat.agent.sandbox.network.allowNetwork",
13865
13906
  "chat.agent.sandbox.network.allowLocalNetwork",
@@ -34101,7 +34142,7 @@ function normalizeAugmentcodeToolList(value) {
34101
34142
  return tools.length > 0 ? tools : void 0;
34102
34143
  }
34103
34144
  const AugmentcodeSubagentFrontmatterSchema = z.looseObject({
34104
- name: z.string(),
34145
+ name: z.optional(z.string()),
34105
34146
  description: z.optional(z.string()),
34106
34147
  color: z.optional(z.string()),
34107
34148
  model: z.optional(z.string()),
@@ -34160,7 +34201,7 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
34160
34201
  outputRoot: ".",
34161
34202
  frontmatter: {
34162
34203
  targets: ["*"],
34163
- name,
34204
+ name: name ?? basename(this.getRelativeFilePath(), extname(this.getRelativeFilePath())),
34164
34205
  description,
34165
34206
  ...Object.keys(augmentcodeSection).length > 0 && { augmentcode: augmentcodeSection }
34166
34207
  },
@@ -34226,7 +34267,8 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
34226
34267
  const dirPath = relativeDirPath ?? this.getSettablePaths({ global }).relativeDirPath;
34227
34268
  const filePath = join(outputRoot, dirPath, relativeFilePath);
34228
34269
  const fileContent = await readFileContent(filePath);
34229
- const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
34270
+ const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
34271
+ if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}`);
34230
34272
  const result = AugmentcodeSubagentFrontmatterSchema.safeParse(frontmatter);
34231
34273
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
34232
34274
  return new AugmentcodeSubagent({
@@ -41809,7 +41851,23 @@ function sanitizeOpencodeMcpNamePart(value) {
41809
41851
  * key that matches nothing.
41810
41852
  */
41811
41853
  function opencodeMcpToolPermissionKey(serverName, toolName) {
41812
- return `${sanitizeOpencodeMcpNamePart(serverName)}_${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
41854
+ return `${opencodeMcpServerKeyPrefix(serverName)}${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
41855
+ }
41856
+ /** The sanitized `<server>_` prefix every permission key of the server's tools starts with. */
41857
+ function opencodeMcpServerKeyPrefix(serverName) {
41858
+ return `${sanitizeOpencodeMcpNamePart(serverName)}_`;
41859
+ }
41860
+ /**
41861
+ * Whether `key` is exactly the sanitized `<server>_` prefix of one of
41862
+ * `serverNames` (e.g. `a_b_` for server `a.b` or `a_b`). No tool has an empty
41863
+ * name, so such a key filters nothing of that server, and
41864
+ * {@link findOpencodeMcpToolKeyOwner} gives it no owner. Read as an unlisted
41865
+ * server's filter instead, it would be split at the first `_` into a phantom
41866
+ * server (`a` with tool `b_`), so importers leave it out of the rulesync
41867
+ * config, and the OpenCode family's generate keeps it in the file as it was.
41868
+ */
41869
+ function isBareOpencodeMcpServerPrefix({ key, serverNames }) {
41870
+ return serverNames.some((serverName) => key === opencodeMcpServerKeyPrefix(serverName));
41813
41871
  }
41814
41872
  /**
41815
41873
  * The server among `serverNames` whose tool a permission key names, with the
@@ -41823,7 +41881,7 @@ function findOpencodeMcpToolKeyOwner({ key, serverNames, builtinKeys, acceptRawP
41823
41881
  if (builtinKeys.has(key)) return;
41824
41882
  let owner;
41825
41883
  for (const serverName of serverNames) {
41826
- const sanitized = `${sanitizeOpencodeMcpNamePart(serverName)}_`;
41884
+ const sanitized = opencodeMcpServerKeyPrefix(serverName);
41827
41885
  const prefixes = acceptRawPrefix ? uniq([sanitized, `${serverName}_`]) : [sanitized];
41828
41886
  for (const prefix of prefixes) if (key.length > prefix.length && key.startsWith(prefix) && (owner === void 0 || prefix.length > owner.prefix.length)) owner = {
41829
41887
  serverName,
@@ -42284,7 +42342,10 @@ function convertFromKiloFormat(kiloMcp, tools, permission) {
42284
42342
  const serverNames = Object.keys(kiloMcp);
42285
42343
  return {
42286
42344
  ...Object.fromEntries(Object.entries(kiloMcp).map(([serverName, serverConfig]) => [serverName, kiloServerToRulesync(serverName, serverConfig, splitKiloServerTools(serverName, serverNames, filters))])),
42287
- ...orphanMcpToolFiltersToRulesync(kiloMcp, tools)
42345
+ ...orphanMcpToolFiltersToRulesync(kiloMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findKiloMcpToolKeyOwner(key, serverNames) && !isBareOpencodeMcpServerPrefix({
42346
+ key,
42347
+ serverNames
42348
+ }))))
42288
42349
  };
42289
42350
  }
42290
42351
  /**
@@ -42802,16 +42863,20 @@ function convertFromOpencodeFormat(opencodeMcp, tools, remoteType) {
42802
42863
  ...orphanMcpToolFiltersToRulesync(opencodeMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findOpencodeToolOwner({
42803
42864
  key,
42804
42865
  serverNames
42866
+ }) && !isBareOpencodeMcpServerPrefix({
42867
+ key,
42868
+ serverNames
42805
42869
  })))),
42806
42870
  ...convertOpencodeServers(opencodeMcp, tools, remoteType)
42807
42871
  };
42808
42872
  }
42873
+ const OPENCODE_MCP_BUILTIN_TOOL_KEYS = /* @__PURE__ */ new Set();
42809
42874
  /** Recognize both OpenCode's sanitized names and keys older rulesync versions wrote. */
42810
42875
  function findOpencodeToolOwner({ key, serverNames }) {
42811
42876
  return findOpencodeMcpToolKeyOwner({
42812
42877
  key,
42813
42878
  serverNames,
42814
- builtinKeys: /* @__PURE__ */ new Set(),
42879
+ builtinKeys: OPENCODE_MCP_BUILTIN_TOOL_KEYS,
42815
42880
  acceptRawPrefix: true
42816
42881
  });
42817
42882
  }
@@ -42981,6 +43046,40 @@ function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
42981
43046
  tools
42982
43047
  };
42983
43048
  }
43049
+ /**
43050
+ * Copy each key of `existingTools` that is a bare sanitized `<server>_` prefix
43051
+ * of one of `serverNames` into `tools`, unless a generated filter already
43052
+ * writes the same key or another listed server owns it (`a_b_` is tool `b_` of
43053
+ * server `a` when `a` is listed too). An owned key is the rulesync config's to
43054
+ * write, so carrying it over would keep a filter the user removed. Mutates
43055
+ * `tools` in place.
43056
+ */
43057
+ function preserveBareOpencodeMcpServerPrefixKeys({ tools, existingTools, serverNames }) {
43058
+ for (const [key, enabled] of Object.entries(existingTools ?? {})) if (!Object.hasOwn(tools, key) && isBareOpencodeMcpServerPrefix({
43059
+ key,
43060
+ serverNames
43061
+ }) && !findOpencodeToolOwner({
43062
+ key,
43063
+ serverNames
43064
+ })) tools[key] = enabled;
43065
+ }
43066
+ /**
43067
+ * When the existing file does not pass the schema, its `tools` map cannot be
43068
+ * read, so a bare `<server>_` key in it is not carried over. Say so rather
43069
+ * than lift the deny it may hold without a word.
43070
+ */
43071
+ function warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({ existingJson, serverNames, logger }) {
43072
+ const rawTools = isRecord(existingJson) ? existingJson.tools : void 0;
43073
+ if (!isRecord(rawTools)) return;
43074
+ const bareKeys = Object.keys(rawTools).filter((key) => isBareOpencodeMcpServerPrefix({
43075
+ key,
43076
+ serverNames
43077
+ }) && !findOpencodeToolOwner({
43078
+ key,
43079
+ serverNames
43080
+ }));
43081
+ 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".`);
43082
+ }
42984
43083
  const OpencodeMcpFeatureOptionsSchema = z.looseObject({ configShape: z.optional(z.enum(["v1", "v2"])) });
42985
43084
  var OpencodeMcp = class extends ToolMcp {
42986
43085
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
@@ -43113,6 +43212,16 @@ var OpencodeMcp = class extends ToolMcp {
43113
43212
  convertedMcp,
43114
43213
  mcpServers: transformedServers
43115
43214
  });
43215
+ if (existingMcp.success) preserveBareOpencodeMcpServerPrefixKeys({
43216
+ tools: mcpTools,
43217
+ existingTools: existingMcp.data.tools,
43218
+ serverNames: Object.keys(transformedServers)
43219
+ });
43220
+ else warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({
43221
+ existingJson: this.lowerConfig(existingJson),
43222
+ serverNames: Object.keys(transformedServers),
43223
+ logger
43224
+ });
43116
43225
  if (this.layout.configVersion === "v2") warnAboutOpencodeV2SseServers({
43117
43226
  mcpServers: transformedServers,
43118
43227
  convertedMcp,
@@ -51667,6 +51776,7 @@ var RulesProcessor = class extends FeatureProcessor {
51667
51776
  factory
51668
51777
  });
51669
51778
  await this.warnForWarpLegacyRootFile(toolRules);
51779
+ this.warnForAugmentcodePluginRules(toolRules);
51670
51780
  await this.warnForInstructionBudget({
51671
51781
  toolRules,
51672
51782
  meta
@@ -52058,6 +52168,20 @@ var RulesProcessor = class extends FeatureProcessor {
52058
52168
  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.`);
52059
52169
  }
52060
52170
  /**
52171
+ * Auggie 1.x (still a prerelease on npm at the time of writing; the stable
52172
+ * line is 0.36.0) no longer loads `rules/` from plugins: "Rules shipped in
52173
+ * plugins (`rules/` in a plugin)" is listed among the removed features, while
52174
+ * commands, agents, skills, hooks and MCP servers still load. The rule files
52175
+ * are still written, because the stable 0.x CLI reads them, but under 1.x the
52176
+ * bundle silently loses them, so say so.
52177
+ * @see https://docs.augmentcode.com/cli/whats-new
52178
+ * @see https://docs.augmentcode.com/cli/v0/plugins
52179
+ */
52180
+ warnForAugmentcodePluginRules(toolRules) {
52181
+ if (this.toolTarget !== "augmentcode-plugin" || toolRules.length === 0) return;
52182
+ 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.`);
52183
+ }
52184
+ /**
52061
52185
  * Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
52062
52186
  * `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so an
52063
52187
  * `AGENTS.md` this run writes next to one would never be read. That holds for
@@ -82300,6 +82424,57 @@ function matchesGlobStep(step, character) {
82300
82424
  const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
82301
82425
  return step.negated ? !admitted : admitted;
82302
82426
  }
82427
+ /**
82428
+ * Whether `value` matches `glob` in full, with `*`, `?` and `[...]` carrying
82429
+ * their usual meaning. The glob comes first, unlike Node's own
82430
+ * `path.matchesGlob(path, pattern)`.
82431
+ *
82432
+ * The glob is walked step by step rather than translated to a regex, because
82433
+ * both sides of a comparison can come from a file a repository carries: `*a*a*a*a*b`
82434
+ * translated to `^.*a.*a.*a.*a.*b$` takes minutes against a forty-character
82435
+ * name, while walking the steps is linear in the name per `*`. Remembering only
82436
+ * the most recent `*` to fall back to is enough — an earlier one can always give
82437
+ * up whatever the later one needed.
82438
+ */
82439
+ function matchesGlob(glob, value) {
82440
+ return compileGlob(glob)(value);
82441
+ }
82442
+ /**
82443
+ * Parse `glob` once and return a predicate that walks it, for a caller that
82444
+ * tests the same glob against a whole list of names.
82445
+ */
82446
+ function compileGlob(glob) {
82447
+ const steps = parseGlob(glob);
82448
+ return (value) => matchesParsedGlob(steps, value);
82449
+ }
82450
+ function matchesParsedGlob(steps, value) {
82451
+ const characters = [...value];
82452
+ let stepIndex = 0;
82453
+ let characterIndex = 0;
82454
+ let starStepIndex = -1;
82455
+ let starCharacterIndex = 0;
82456
+ while (characterIndex < characters.length) {
82457
+ const step = steps[stepIndex];
82458
+ if (step?.kind === "star") {
82459
+ starStepIndex = stepIndex;
82460
+ starCharacterIndex = characterIndex;
82461
+ stepIndex += 1;
82462
+ continue;
82463
+ }
82464
+ if (step !== void 0 && matchesGlobStep(step, characters[characterIndex] ?? "")) {
82465
+ stepIndex += 1;
82466
+ characterIndex += 1;
82467
+ continue;
82468
+ }
82469
+ if (starStepIndex < 0) return false;
82470
+ starCharacterIndex += 1;
82471
+ stepIndex = starStepIndex + 1;
82472
+ characterIndex = starCharacterIndex;
82473
+ }
82474
+ let remaining = stepIndex;
82475
+ while (steps[remaining]?.kind === "star") remaining += 1;
82476
+ return remaining === steps.length;
82477
+ }
82303
82478
  /** Whether two single-character steps can both match one same character. */
82304
82479
  function stepsShareACharacter(left, right) {
82305
82480
  if (left.kind === "any" || right.kind === "any") return true;
@@ -83836,9 +84011,13 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
83836
84011
  const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
83837
84012
  const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
83838
84013
  terminal: "launch-process",
84014
+ bash: "launch-process",
84015
+ shell: "launch-process",
83839
84016
  read: "view",
83840
84017
  edit: "str-replace-editor",
83841
- write: "save-file"
84018
+ str_replace_editor: "str-replace-editor",
84019
+ write: "save-file",
84020
+ save_file: "save-file"
83842
84021
  };
83843
84022
  function toAugmentToolName(canonical) {
83844
84023
  return lookupOwn({
@@ -83990,6 +84169,7 @@ function isShellRegexRoundtrippable(regex) {
83990
84169
  while (i < body.length) {
83991
84170
  const ch = body[i];
83992
84171
  if (ch === "\\" && i + 1 < body.length) {
84172
+ if (isGlobSpecial(body[i + 1])) return false;
83993
84173
  i += 2;
83994
84174
  continue;
83995
84175
  }
@@ -84006,6 +84186,37 @@ function isShellRegexRoundtrippable(regex) {
84006
84186
  }
84007
84187
  return true;
84008
84188
  }
84189
+ /** Characters a glob reads as syntax (wildcards or the start of a character class). */
84190
+ function isGlobSpecial(ch) {
84191
+ return ch === "*" || ch === "?" || ch === "[";
84192
+ }
84193
+ /**
84194
+ * Whether `shellRegexToGlob` can approximate a `shellInputRegex` without widening it. The lossy
84195
+ * conversion keeps regex quantifiers as characters, and a `*` or `?` left in the glob becomes a
84196
+ * wildcard: `^rm -rf /tmp/a*$` (a run of `a`s) would turn into the glob `rm -rf /tmp/a*`, which
84197
+ * matches `rm -rf /tmp/a /home`. So a regex is unsafe when it has an escaped literal `\*` / `\?` /
84198
+ * `\[` (a `[` would open a character class in targets whose globs support one),
84199
+ * or a bare quantifier `*` / `?` / `+` / `{` that is not part of a `.*` run (which the glob `*`
84200
+ * reproduces faithfully).
84201
+ */
84202
+ function canApproximateShellRegexWithoutWidening(regex) {
84203
+ let i = 0;
84204
+ while (i < regex.length) {
84205
+ const ch = regex[i];
84206
+ if (ch === "\\" && i + 1 < regex.length) {
84207
+ if (isGlobSpecial(regex[i + 1])) return false;
84208
+ i += 2;
84209
+ continue;
84210
+ }
84211
+ if (ch === "." && regex[i + 1] === "*") {
84212
+ i += 2;
84213
+ continue;
84214
+ }
84215
+ if (ch === "*" || ch === "?" || ch === "+" || ch === "{") return false;
84216
+ i += 1;
84217
+ }
84218
+ return true;
84219
+ }
84009
84220
  const MANAGED_AUGMENT_TOOL_NAMES = new Set(Object.values(CANONICAL_TO_AUGMENT_TOOL_NAMES));
84010
84221
  var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermissions {
84011
84222
  constructor(params) {
@@ -84442,12 +84653,18 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
84442
84653
  if (legacyToolName === "launch-process" && entry.shellInputRegex) {
84443
84654
  const regex = entry.shellInputRegex;
84444
84655
  if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
84445
- else if (action === "deny") {
84446
- 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.`);
84447
- pattern = "*";
84448
- } else {
84449
- pattern = shellRegexToGlob(regex);
84450
- 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.`);
84656
+ else {
84657
+ if (action !== "deny" && !canApproximateShellRegexWithoutWidening(regex)) {
84658
+ 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.`);
84659
+ continue;
84660
+ }
84661
+ if (action === "deny") {
84662
+ 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.`);
84663
+ pattern = "*";
84664
+ } else {
84665
+ pattern = shellRegexToGlob(regex);
84666
+ 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.`);
84667
+ }
84451
84668
  }
84452
84669
  } else pattern = "*";
84453
84670
  if (isPrototypePollutionKey(pattern)) {
@@ -85940,15 +86157,19 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
85940
86157
  * `env` variables a project or local settings file cannot set, because a
85941
86158
  * checked-out repository should not control them: where Claude Code stores its
85942
86159
  * own files, what session content it exports, where telemetry goes, how it
85943
- * starts or syncs, the timers on an unanswered dialog, and whether it processes
85944
- * 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`.
86160
+ * starts or syncs, which claude.ai sign-in it uses, the timers on an unanswered
86161
+ * dialog, and whether it processes attachments or expands `@` file mentions,
86162
+ * and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
85945
86163
  *
85946
- * Only the names the settings reference lists are covered; its directory and
85947
- * Windows groups are introduced with "such as", so an unlisted member of those
86164
+ * Only the names the settings reference lists are covered; its directory,
86165
+ * Windows and sign-in groups are introduced with "such as", so an unlisted member of those
85948
86166
  * families is still written. Windows variable names are case-insensitive, so
85949
- * that group is matched without regard to case.
86167
+ * that group is matched without regard to case. `ALLUSERSPROFILE`,
86168
+ * `SystemDrive` and the `CommonProgramFiles` family are ignored there since
86169
+ * Claude Code v2.1.285.
85950
86170
  *
85951
86171
  * @see https://code.claude.com/docs/en/settings-reference#variables-claude-code-ignores-in-env
86172
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
85952
86173
  */
85953
86174
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85954
86175
  "BETA_TRACING_ENDPOINT",
@@ -85958,6 +86179,9 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85958
86179
  "CLAUDE_CODE_DISABLE_FILE_MENTIONS",
85959
86180
  "CLAUDE_CODE_ENABLE_TELEMETRY",
85960
86181
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
86182
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
86183
+ "CLAUDE_CODE_OAUTH_SCOPES",
86184
+ "CLAUDE_CODE_OAUTH_TOKEN",
85961
86185
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
85962
86186
  "CLAUDE_CODE_PLUGIN_SEED_DIR",
85963
86187
  "CLAUDE_CODE_PROCESS_WRAPPER",
@@ -85978,14 +86202,23 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85978
86202
  "TMPDIR"
85979
86203
  ]);
85980
86204
  const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES = /* @__PURE__ */ new Set([
86205
+ "ALLUSERSPROFILE",
86206
+ "COMMONPROGRAMW6432",
85981
86207
  "COMSPEC",
85982
86208
  "LOCALAPPDATA",
85983
86209
  "PATHEXT",
85984
86210
  "PROGRAMDATA",
85985
86211
  "PROGRAMW6432",
85986
86212
  "PSMODULEPATH",
86213
+ "SYSTEMDRIVE",
85987
86214
  "SYSTEMROOT"
85988
86215
  ]);
86216
+ /**
86217
+ * Windows name prefixes matched as whole families: `ProgramFiles*` and
86218
+ * `CommonProgramFiles*`. `ProgramW6432` and `CommonProgramW6432` are exact
86219
+ * names in the set above.
86220
+ */
86221
+ const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES = ["PROGRAMFILES", "COMMONPROGRAMFILES"];
85989
86222
  const CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES = [
85990
86223
  "_ENDPOINT",
85991
86224
  "_HEADERS",
@@ -86030,7 +86263,7 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
86030
86263
  function isProjectIgnoredEnvVariable(name, value) {
86031
86264
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_") || name.startsWith("CLAUDE_AX_")) return true;
86032
86265
  const upper = name.toUpperCase();
86033
- if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || upper.startsWith("PROGRAMFILES")) return true;
86266
+ if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES.some((prefix) => upper.startsWith(prefix))) return true;
86034
86267
  if (name.startsWith("OTEL_EXPORTER_OTLP_") && CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return true;
86035
86268
  if (Object.hasOwn(CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES, name)) {
86036
86269
  const offValues = CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES[name];
@@ -86062,7 +86295,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
86062
86295
  kept[name] = value;
86063
86296
  }
86064
86297
  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.`);
86065
- 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.`);
86298
+ 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.`);
86066
86299
  return kept;
86067
86300
  }
86068
86301
  /**
@@ -86712,7 +86945,7 @@ const CODEBUDDY_RULE_LISTS = [
86712
86945
  "ask",
86713
86946
  "deny"
86714
86947
  ];
86715
- const CATCH_ALL_PATTERN$13 = "*";
86948
+ const CATCH_ALL_PATTERN$14 = "*";
86716
86949
  const MCP_CATEGORY = "mcp";
86717
86950
  const MCP_RULE_PREFIX = "mcp__";
86718
86951
  const ALL_MCP_RULE = "mcp__*";
@@ -86782,7 +87015,7 @@ function categoryOfRule$2(rule) {
86782
87015
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
86783
87016
  */
86784
87017
  function buildCodebuddyRule({ category, pattern, action, logger }) {
86785
- const catchAll = pattern === CATCH_ALL_PATTERN$13;
87018
+ const catchAll = pattern === CATCH_ALL_PATTERN$14;
86786
87019
  if (category === MCP_CATEGORY) {
86787
87020
  if (catchAll && action === "allow") {
86788
87021
  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').`);
@@ -86791,7 +87024,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
86791
87024
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
86792
87025
  }
86793
87026
  if (category === "*") {
86794
- if (catchAll) return CATCH_ALL_PATTERN$13;
87027
+ if (catchAll) return CATCH_ALL_PATTERN$14;
86795
87028
  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').`);
86796
87029
  return null;
86797
87030
  }
@@ -86809,7 +87042,7 @@ function parseCodebuddyRule(rule) {
86809
87042
  const { specifier } = splitCodebuddyRule(rule);
86810
87043
  if (specifier === void 0) return {
86811
87044
  category,
86812
- pattern: CATCH_ALL_PATTERN$13
87045
+ pattern: CATCH_ALL_PATTERN$14
86813
87046
  };
86814
87047
  const trimmed = specifier.trim();
86815
87048
  return trimmed === "" ? null : {
@@ -89135,8 +89368,8 @@ function mapBashActionToDecision(action) {
89135
89368
  // src/features/permissions/commandcode-permissions.ts
89136
89369
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
89137
89370
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
89138
- const CATCH_ALL_PATTERN$12 = "*";
89139
- const MCP_CANONICAL_PREFIX$7 = "mcp__";
89371
+ const CATCH_ALL_PATTERN$13 = "*";
89372
+ const MCP_CANONICAL_PREFIX$8 = "mcp__";
89140
89373
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
89141
89374
  const CATEGORY_TO_COMMANDCODE_TOOL = {
89142
89375
  bash: "Shell",
@@ -89181,10 +89414,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
89181
89414
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
89182
89415
  */
89183
89416
  function buildCommandcodeRule(category, pattern) {
89184
- const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
89185
- if (category.startsWith(MCP_CANONICAL_PREFIX$7)) return catchAll ? category : `${category}(${pattern})`;
89186
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$7}${pattern}`;
89187
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
89417
+ const catchAll = pattern === CATCH_ALL_PATTERN$13 || pattern === "";
89418
+ if (category.startsWith(MCP_CANONICAL_PREFIX$8)) return catchAll ? category : `${category}(${pattern})`;
89419
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$8}${pattern}`;
89420
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$13 : null;
89188
89421
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
89189
89422
  if (tool === void 0) return null;
89190
89423
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -89220,7 +89453,7 @@ function splitCommandcodeRule(rule) {
89220
89453
  }
89221
89454
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
89222
89455
  function isCatchAllSpecifier(inner) {
89223
- return inner === "" || inner === CATCH_ALL_PATTERN$12;
89456
+ return inner === "" || inner === CATCH_ALL_PATTERN$13;
89224
89457
  }
89225
89458
  /**
89226
89459
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -89254,34 +89487,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
89254
89487
  if (server.length === 0) return null;
89255
89488
  const everyServer = {
89256
89489
  category: "mcp",
89257
- pattern: CATCH_ALL_PATTERN$12,
89490
+ pattern: CATCH_ALL_PATTERN$13,
89258
89491
  notInAllow: true
89259
89492
  };
89260
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7)) {
89261
- if (server === CATCH_ALL_PATTERN$12) return everyServer;
89262
- if (server.includes(CATCH_ALL_PATTERN$12)) return null;
89493
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$8)) {
89494
+ if (server === CATCH_ALL_PATTERN$13) return everyServer;
89495
+ if (server.includes(CATCH_ALL_PATTERN$13)) return null;
89263
89496
  return {
89264
- category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$7}${server}` : `${MCP_CANONICAL_PREFIX$7}${remainder}`,
89265
- pattern: CATCH_ALL_PATTERN$12,
89497
+ category: rest === "" || rest === CATCH_ALL_PATTERN$13 ? `${MCP_CANONICAL_PREFIX$8}${server}` : `${MCP_CANONICAL_PREFIX$8}${remainder}`,
89498
+ pattern: CATCH_ALL_PATTERN$13,
89266
89499
  notInAllow: false
89267
89500
  };
89268
89501
  }
89269
- const category = `${MCP_CANONICAL_PREFIX$7}${remainder}`;
89270
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
89502
+ const category = `${MCP_CANONICAL_PREFIX$8}${remainder}`;
89503
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$13);
89271
89504
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
89272
89505
  category,
89273
89506
  pattern: inner,
89274
89507
  notInAllow: true
89275
89508
  } : null;
89276
- if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
89277
- if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
89278
- category: `${MCP_CANONICAL_PREFIX$7}${server}`,
89279
- pattern: CATCH_ALL_PATTERN$12,
89509
+ if (remainder === CATCH_ALL_PATTERN$13) return everyServer;
89510
+ if (rest === CATCH_ALL_PATTERN$13 && !server.includes(CATCH_ALL_PATTERN$13)) return {
89511
+ category: `${MCP_CANONICAL_PREFIX$8}${server}`,
89512
+ pattern: CATCH_ALL_PATTERN$13,
89280
89513
  notInAllow: true
89281
89514
  };
89282
89515
  return namesTool ? {
89283
89516
  category,
89284
- pattern: CATCH_ALL_PATTERN$12,
89517
+ pattern: CATCH_ALL_PATTERN$13,
89285
89518
  notInAllow: false
89286
89519
  } : null;
89287
89520
  }
@@ -89305,14 +89538,14 @@ function friendlyToolRule({ category, inner }) {
89305
89538
  if (category === "bash") {
89306
89539
  if (isCatchAllSpecifier(inner)) return {
89307
89540
  category,
89308
- pattern: CATCH_ALL_PATTERN$12,
89541
+ pattern: CATCH_ALL_PATTERN$13,
89309
89542
  notInAllow: false
89310
89543
  };
89311
89544
  const normalized = inner.trim().replace(/\s+/g, " ");
89312
89545
  if (normalized === "" || normalized === ":*") return null;
89313
89546
  return isCatchAllSpecifier(normalized) ? {
89314
89547
  category,
89315
- pattern: CATCH_ALL_PATTERN$12,
89548
+ pattern: CATCH_ALL_PATTERN$13,
89316
89549
  notInAllow: true
89317
89550
  } : {
89318
89551
  category,
@@ -89322,7 +89555,7 @@ function friendlyToolRule({ category, inner }) {
89322
89555
  }
89323
89556
  if (isCatchAllSpecifier(inner)) return {
89324
89557
  category,
89325
- pattern: CATCH_ALL_PATTERN$12,
89558
+ pattern: CATCH_ALL_PATTERN$13,
89326
89559
  notInAllow: false
89327
89560
  };
89328
89561
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -89341,13 +89574,13 @@ function friendlyToolRule({ category, inner }) {
89341
89574
  */
89342
89575
  function parseCommandcodeRule(rule) {
89343
89576
  const { tool, inner } = splitCommandcodeRule(rule);
89344
- if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
89577
+ if (tool === CATCH_ALL_PATTERN$13) return isCatchAllSpecifier(inner) ? {
89345
89578
  category: "*",
89346
- pattern: CATCH_ALL_PATTERN$12,
89579
+ pattern: CATCH_ALL_PATTERN$13,
89347
89580
  notInAllow: true
89348
89581
  } : null;
89349
89582
  const lowered = tool.toLowerCase();
89350
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$7)) return parseMcpCommandcodeRule({
89583
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$8)) return parseMcpCommandcodeRule({
89351
89584
  tool,
89352
89585
  inner
89353
89586
  });
@@ -89365,12 +89598,12 @@ function parseCommandcodeRule(rule) {
89365
89598
  */
89366
89599
  function isFoldedMcpToolNameRule({ rule, category }) {
89367
89600
  const { tool } = splitCommandcodeRule(rule);
89368
- return category.startsWith(MCP_CANONICAL_PREFIX$7) && !tool.startsWith(MCP_CANONICAL_PREFIX$7);
89601
+ return category.startsWith(MCP_CANONICAL_PREFIX$8) && !tool.startsWith(MCP_CANONICAL_PREFIX$8);
89369
89602
  }
89370
89603
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
89371
89604
  function scopedMcpRuleTool(rule) {
89372
89605
  const { tool, inner } = splitCommandcodeRule(rule);
89373
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$7) ? tool : null;
89606
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$8) ? tool : null;
89374
89607
  }
89375
89608
  /**
89376
89609
  * The string entries of a Command Code permission list. Command Code reads
@@ -89413,7 +89646,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
89413
89646
  }).filter((rule) => {
89414
89647
  const parsed = parseCommandcodeRule(rule);
89415
89648
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
89416
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$12})`);
89649
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$13})`);
89417
89650
  if (key !== "allow" && isStricterAction({
89418
89651
  action: key,
89419
89652
  existing: replacement
@@ -89470,7 +89703,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89470
89703
  const ranked = /* @__PURE__ */ new Map();
89471
89704
  const managedCategories = /* @__PURE__ */ new Set();
89472
89705
  for (const [category, rules] of Object.entries(permission)) {
89473
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
89706
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$13);
89474
89707
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
89475
89708
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
89476
89709
  for (const [pattern, action] of Object.entries(rules)) {
@@ -89539,7 +89772,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89539
89772
  function skippedAllowImportMessage({ rule, category }) {
89540
89773
  const { tool, inner } = splitCommandcodeRule(rule);
89541
89774
  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.`;
89542
- 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.`;
89775
+ 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.`;
89543
89776
  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.`;
89544
89777
  }
89545
89778
  /**
@@ -89734,7 +89967,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
89734
89967
  }
89735
89968
  // src/features/permissions/continue-permissions.ts
89736
89969
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
89737
- const CATCH_ALL_PATTERN$11 = "*";
89970
+ const CATCH_ALL_PATTERN$12 = "*";
89738
89971
  /**
89739
89972
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
89740
89973
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -89742,7 +89975,7 @@ const CATCH_ALL_PATTERN$11 = "*";
89742
89975
  * the bare entry instead of being skipped as a pattern-specific rule.
89743
89976
  */
89744
89977
  function normalizeCatchAllPattern$1(pattern) {
89745
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
89978
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$12 : pattern;
89746
89979
  }
89747
89980
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
89748
89981
  bash: "Bash",
@@ -89786,7 +90019,7 @@ function parseContinuePermissionEntry(entry) {
89786
90019
  if (toolName === "") return;
89787
90020
  return {
89788
90021
  toolName,
89789
- pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
90022
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$12 : pattern
89790
90023
  };
89791
90024
  }
89792
90025
  /**
@@ -89951,7 +90184,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
89951
90184
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89952
90185
  continue;
89953
90186
  }
89954
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
90187
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$12) {
89955
90188
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89956
90189
  continue;
89957
90190
  }
@@ -90007,13 +90240,116 @@ const AUTO_APPROVE_KEYS = {
90007
90240
  webfetch: "chat.tools.urls.autoApprove"
90008
90241
  };
90009
90242
  /**
90010
- * The workspace setting holding the starting permission level of new chat
90011
- * sessions, authored through the `copilot.defaultPermissionMode` override.
90243
+ * The workspace setting holding the starting permission level of new Local
90244
+ * (`local` harness) chat sessions, authored through the
90245
+ * `copilot.defaultPermissionMode` override. VS Code 1.144 narrowed it to Local
90246
+ * sessions; the Copilot harness reads {@link DEFAULT_CONFIGURATION_KEY}.
90012
90247
  *
90013
90248
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
90014
90249
  */
90015
90250
  const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
90016
90251
  /**
90252
+ * The workspace setting holding the starting `mode` (`interactive` / `plan` /
90253
+ * `autopilot`) and `approvals` (`manual` / `assisted` / `allowAll`) of new
90254
+ * agent sessions such as the Copilot harness, the default since VS Code 1.143.
90255
+ * It shipped in stable VS Code 1.144 as an object setting with
90256
+ * `additionalProperties: false`; older releases ignore it.
90257
+ *
90258
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
90259
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/common/constants.ts
90260
+ */
90261
+ const DEFAULT_CONFIGURATION_KEY = "chat.defaultConfiguration";
90262
+ /**
90263
+ * What each `copilot.defaultPermissionMode` value writes. `local` goes to
90264
+ * `chat.permissions.default`; `approvals` and `autopilot` go to
90265
+ * `chat.defaultConfiguration`. The Local picker has no Assisted level, so
90266
+ * `assisted` starts Local sessions with Default Permissions — the stricter
90267
+ * fallback. Autopilot is a permission level in the Local picker but a `mode`
90268
+ * in `chat.defaultConfiguration`, where it runs with every tool call approved.
90269
+ */
90270
+ const DEFAULT_PERMISSION_MODE_MAPPINGS = {
90271
+ default: {
90272
+ local: "default",
90273
+ approvals: "manual",
90274
+ autopilot: false
90275
+ },
90276
+ autoApprove: {
90277
+ local: "autoApprove",
90278
+ approvals: "allowAll",
90279
+ autopilot: false
90280
+ },
90281
+ autopilot: {
90282
+ local: "autopilot",
90283
+ approvals: "allowAll",
90284
+ autopilot: true
90285
+ },
90286
+ assisted: {
90287
+ local: "default",
90288
+ approvals: "assisted",
90289
+ autopilot: false
90290
+ }
90291
+ };
90292
+ /**
90293
+ * The `chat.defaultConfiguration` value for `mode`. Rulesync owns only whether
90294
+ * the mode is Autopilot: authoring Autopilot sets it, and any other value
90295
+ * removes any Autopilot mode in the file (whether an earlier run or a person
90296
+ * wrote it, since the two cannot be told apart), while an `interactive` or
90297
+ * `plan` mode is kept.
90298
+ */
90299
+ function buildDefaultConfigurationValue({ defaultPermissionMode, existingSettings }) {
90300
+ const { approvals, autopilot } = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode];
90301
+ const existing = existingSettings[DEFAULT_CONFIGURATION_KEY];
90302
+ const value = { ...isPlainObject(existing) ? existing : {} };
90303
+ if (autopilot) value.mode = "autopilot";
90304
+ else if (value.mode === "autopilot") delete value.mode;
90305
+ value.approvals = approvals;
90306
+ return value;
90307
+ }
90308
+ /**
90309
+ * The `copilot.defaultPermissionMode` values from least to most permissive,
90310
+ * used to settle a file whose two keys disagree.
90311
+ */
90312
+ const DEFAULT_PERMISSION_MODE_RANK = {
90313
+ default: 0,
90314
+ assisted: 1,
90315
+ autoApprove: 2,
90316
+ autopilot: 3
90317
+ };
90318
+ /**
90319
+ * The `copilot.defaultPermissionMode` a `chat.defaultConfiguration` value
90320
+ * stands for. Autopilot is recognized only together with `allowAll`, the pair
90321
+ * Rulesync writes for it: an Autopilot mode with stricter approvals is read
90322
+ * from `approvals` alone, so that regenerating never escalates it to Allow
90323
+ * All. An object without `approvals` is read as `default`, since VS Code then
90324
+ * starts sessions with Manual permissions.
90325
+ */
90326
+ function readDefaultConfigurationMode(configuration) {
90327
+ if (!isPlainObject(configuration)) return;
90328
+ switch (configuration.approvals) {
90329
+ case void 0:
90330
+ case "manual": return "default";
90331
+ case "assisted": return "assisted";
90332
+ case "allowAll": return configuration.mode === "autopilot" ? "autopilot" : "autoApprove";
90333
+ default: return;
90334
+ }
90335
+ }
90336
+ /**
90337
+ * Read `copilot.defaultPermissionMode` back from the settings. When the two
90338
+ * keys agree — `chat.permissions.default` holds the Local level Rulesync
90339
+ * writes for the `chat.defaultConfiguration` value — the latter is returned,
90340
+ * since only it can say `assisted`. When they disagree, the less permissive of
90341
+ * the two is returned, so regenerating never raises either harness's starting
90342
+ * permissions. Either key alone is read as it stands.
90343
+ */
90344
+ function readDefaultPermissionMode(settings) {
90345
+ const fromConfiguration = readDefaultConfigurationMode(settings[DEFAULT_CONFIGURATION_KEY]);
90346
+ const localValue = settings[DEFAULT_PERMISSION_MODE_KEY$1];
90347
+ const fromLocal = COPILOT_LOCAL_PERMISSION_LEVELS.find((level) => level === localValue);
90348
+ if (fromConfiguration === void 0 || fromLocal === void 0) return fromConfiguration ?? fromLocal;
90349
+ if (DEFAULT_PERMISSION_MODE_MAPPINGS[fromConfiguration].local === fromLocal) return fromConfiguration;
90350
+ return DEFAULT_PERMISSION_MODE_RANK[fromLocal] < DEFAULT_PERMISSION_MODE_RANK[fromConfiguration] ? fromLocal : fromConfiguration;
90351
+ }
90352
+ /**
90017
90353
  * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
90018
90354
  * at path `a.b` is written to `chat.agent.sandbox.a.b`.
90019
90355
  */
@@ -90200,9 +90536,6 @@ function readSandboxFromSettings(settings) {
90200
90536
  }
90201
90537
  return Object.keys(sandbox).length > 0 ? sandbox : void 0;
90202
90538
  }
90203
- function isCopilotDefaultPermissionMode(value) {
90204
- return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
90205
- }
90206
90539
  function asAutoApproveMap(value) {
90207
90540
  if (!isPlainObject(value)) return {};
90208
90541
  const result = {};
@@ -90246,9 +90579,11 @@ function buildAutoApproveValue({ category, rules }) {
90246
90579
  * `false`. A key whose canonical category is absent entirely
90247
90580
  * is left untouched, so authoring only `bash` rules never disturbs a
90248
90581
  * hand-written edits or urls map.
90249
- * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
90250
- * (the starting permission level of new chat sessions); without it the key is
90251
- * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
90582
+ * The `copilot.defaultPermissionMode` override writes both
90583
+ * `chat.permissions.default` (Local sessions) and `chat.defaultConfiguration`
90584
+ * (Copilot-harness sessions, VS Code 1.144+; see
90585
+ * {@link DEFAULT_PERMISSION_MODE_MAPPINGS}); without it both keys are left
90586
+ * untouched. The `copilot.sandbox` override writes the Agent Host sandbox
90252
90587
  * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
90253
90588
  * warns about every value it writes that loosens the sandbox.
90254
90589
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
@@ -90299,17 +90634,24 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90299
90634
  rules
90300
90635
  });
90301
90636
  }
90637
+ const existingSettings = () => parseSharedConfig({
90638
+ format: "jsonc",
90639
+ fileContent: existingContent,
90640
+ filePath
90641
+ });
90302
90642
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
90303
- if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY$1] = defaultPermissionMode;
90643
+ if (defaultPermissionMode !== void 0) {
90644
+ patch[DEFAULT_PERMISSION_MODE_KEY$1] = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode].local;
90645
+ patch[DEFAULT_CONFIGURATION_KEY] = buildDefaultConfigurationValue({
90646
+ defaultPermissionMode,
90647
+ existingSettings: existingSettings()
90648
+ });
90649
+ }
90304
90650
  const sandbox = config.copilot?.sandbox;
90305
90651
  if (sandbox !== void 0) {
90306
90652
  Object.assign(patch, buildSandboxPatch({
90307
90653
  sandbox,
90308
- existingSettings: parseSharedConfig({
90309
- format: "jsonc",
90310
- fileContent: existingContent,
90311
- filePath
90312
- })
90654
+ existingSettings: existingSettings()
90313
90655
  }));
90314
90656
  warnOnTrustAffectingEntries({
90315
90657
  toolLabel: COPILOT_TOOL_LABEL,
@@ -90356,10 +90698,10 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90356
90698
  for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
90357
90699
  if (Object.keys(rules).length > 0) permission[category] = rules;
90358
90700
  }
90359
- const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY$1];
90701
+ const defaultPermissionMode = readDefaultPermissionMode(settings);
90360
90702
  const sandbox = readSandboxFromSettings(settings);
90361
90703
  const copilot = {
90362
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
90704
+ ...defaultPermissionMode !== void 0 && { defaultPermissionMode },
90363
90705
  ...sandbox !== void 0 && { sandbox }
90364
90706
  };
90365
90707
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -90557,14 +90899,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
90557
90899
  }
90558
90900
  };
90559
90901
  // src/features/permissions/crush-permissions.ts
90560
- const CATCH_ALL_PATTERN$10 = "*";
90902
+ const CATCH_ALL_PATTERN$11 = "*";
90561
90903
  /**
90562
90904
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
90563
90905
  * argument patterns at all, so it is the only pattern a rule can carry and
90564
90906
  * still be written.
90565
90907
  */
90566
90908
  function normalizeCatchAllPattern(pattern) {
90567
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
90909
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90568
90910
  }
90569
90911
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90570
90912
  bash: "bash",
@@ -90576,11 +90918,11 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90576
90918
  webfetch: "fetch"
90577
90919
  };
90578
90920
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
90579
- const MCP_CANONICAL_PREFIX$6 = "mcp__";
90921
+ const MCP_CANONICAL_PREFIX$7 = "mcp__";
90580
90922
  const MCP_CRUSH_PREFIX = "mcp_";
90581
90923
  function toCrushToolName(canonical) {
90582
90924
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
90583
- if (canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
90925
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX$7) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
90584
90926
  return canonical;
90585
90927
  }
90586
90928
  function toCanonicalToolName$5(crushName) {
@@ -90754,7 +91096,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
90754
91096
  for (const [rawPattern, action] of Object.entries(rules)) {
90755
91097
  if (isPrototypePollutionKey(rawPattern)) continue;
90756
91098
  const pattern = normalizeCatchAllPattern(rawPattern);
90757
- if (pattern === CATCH_ALL_PATTERN$10) {
91099
+ if (pattern === CATCH_ALL_PATTERN$11) {
90758
91100
  catchAllAction = action;
90759
91101
  continue;
90760
91102
  }
@@ -90827,11 +91169,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
90827
91169
  };
90828
91170
  for (const entry of disabled) {
90829
91171
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
90830
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
91172
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] = "deny";
90831
91173
  }
90832
91174
  for (const entry of allowed) {
90833
91175
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
90834
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
91176
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] ??= "allow";
90835
91177
  }
90836
91178
  return { permission };
90837
91179
  }
@@ -90877,13 +91219,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
90877
91219
  WebFetch: "webfetch",
90878
91220
  Mcp: "mcp"
90879
91221
  };
90880
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
91222
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
90881
91223
  /**
90882
91224
  * Returns true if the canonical category is the per-tool MCP form
90883
91225
  * `mcp__<server>__<tool>`.
90884
91226
  */
90885
91227
  function isMcpScopedCategory(canonical) {
90886
- return canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5;
91228
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5;
90887
91229
  }
90888
91230
  function toCursorType(canonical) {
90889
91231
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -90917,7 +91259,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
90917
91259
  if (match) {
90918
91260
  const server = match[1] ?? "*";
90919
91261
  const tool = match[2] ?? "*";
90920
- return `${MCP_CANONICAL_PREFIX$5}${server}__${tool}`;
91262
+ return `${MCP_CANONICAL_PREFIX$6}${server}__${tool}`;
90921
91263
  }
90922
91264
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
90923
91265
  }
@@ -91179,7 +91521,7 @@ function convertCursorToRulesyncPermissions(params) {
91179
91521
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
91180
91522
  const canonical = toCanonicalCategory$1(type, pattern);
91181
91523
  if (!permission[canonical]) permission[canonical] = {};
91182
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$5) ? "*" : pattern;
91524
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$6) ? "*" : pattern;
91183
91525
  permission[canonical][canonicalPattern] = action;
91184
91526
  }
91185
91527
  };
@@ -92678,7 +93020,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
92678
93020
  // src/features/permissions/goose-permissions.ts
92679
93021
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
92680
93022
  const GOOSE_USER_KEY = "user";
92681
- const CATCH_ALL_PATTERN$9 = "*";
93023
+ const CATCH_ALL_PATTERN$10 = "*";
92682
93024
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
92683
93025
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
92684
93026
  shell: ["bash"],
@@ -92827,7 +93169,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
92827
93169
  const assigned = /* @__PURE__ */ new Map();
92828
93170
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
92829
93171
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
92830
- 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.`);
93172
+ 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.`);
92831
93173
  const { action, downgraded } = failSafeCatchAllAction({
92832
93174
  rules,
92833
93175
  allToolsRules: category === "*" ? void 0 : config.permission["*"]
@@ -92860,7 +93202,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
92860
93202
  const action = GOOSE_LIST_TO_ACTION[key];
92861
93203
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
92862
93204
  permission[category] ??= {};
92863
- permission[category][CATCH_ALL_PATTERN$9] = action;
93205
+ permission[category][CATCH_ALL_PATTERN$10] = action;
92864
93206
  }
92865
93207
  }
92866
93208
  return { permission };
@@ -92870,8 +93212,8 @@ const GROKCLI_UI_KEY = "ui";
92870
93212
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
92871
93213
  const GROKCLI_PERMISSION_KEY = "permission";
92872
93214
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
92873
- const CATCH_ALL_PATTERN$8 = "*";
92874
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
93215
+ const CATCH_ALL_PATTERN$9 = "*";
93216
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
92875
93217
  const CATEGORY_TO_GROK_TOOL = {
92876
93218
  bash: "Bash",
92877
93219
  read: "Read",
@@ -92903,6 +93245,25 @@ const GROK_TOOL_TO_CATEGORY_LOWER = {
92903
93245
  };
92904
93246
  const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
92905
93247
  const GROKCLI_PERMISSION_RULES_KEY = "rules";
93248
+ const GROK_PATH_PATTERN_CATEGORIES = /* @__PURE__ */ new Set([
93249
+ "read",
93250
+ "edit",
93251
+ "write",
93252
+ "grep",
93253
+ "glob"
93254
+ ]);
93255
+ const CLAUDE_ABSOLUTE_PATH_PREFIX = "//";
93256
+ /**
93257
+ * Rewrite a canonical path pattern into the anchor Grok understands. A
93258
+ * Claude-style `//abs` absolute pattern becomes `/abs`; without this the rule
93259
+ * would match nothing on Grok, so a `deny` or `ask` rule would silently give
93260
+ * no protection. Every other pattern is returned unchanged.
93261
+ */
93262
+ function toGrokPathPattern(category, pattern) {
93263
+ if (!GROK_PATH_PATTERN_CATEGORIES.has(category)) return pattern;
93264
+ if (!pattern.startsWith(CLAUDE_ABSOLUTE_PATH_PREFIX)) return pattern;
93265
+ return `/${pattern.replace(/^\/+/, "")}`;
93266
+ }
92906
93267
  /**
92907
93268
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
92908
93269
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -92913,18 +93274,20 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
92913
93274
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
92914
93275
  * concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
92915
93276
  * Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
92916
- * under it has no Grok spelling and returns `null`.
93277
+ * under it has no Grok spelling and returns `null`. A Claude-style `//abs`
93278
+ * path pattern on a file tool is rewritten to Grok's `/abs` (see
93279
+ * `toGrokPathPattern`).
92917
93280
  */
92918
93281
  function buildGrokEntry(category, pattern) {
92919
- if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
92920
- if (category.startsWith(MCP_CANONICAL_PREFIX$4)) {
93282
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$9 ? GROK_ALL_TOOLS_ENTRY : null;
93283
+ if (category.startsWith(MCP_CANONICAL_PREFIX$5)) {
92921
93284
  const remainder = category.slice(5);
92922
93285
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
92923
93286
  }
92924
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
93287
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
92925
93288
  const tool = CATEGORY_TO_GROK_TOOL[category];
92926
93289
  if (tool === void 0) return null;
92927
- return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
93290
+ return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
92928
93291
  }
92929
93292
  /**
92930
93293
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -92946,21 +93309,21 @@ function parseGrokEntry(entry) {
92946
93309
  }
92947
93310
  if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
92948
93311
  category: "*",
92949
- pattern: CATCH_ALL_PATTERN$8
93312
+ pattern: CATCH_ALL_PATTERN$9
92950
93313
  };
92951
- if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$4)) return parseClaudeStyleMcpEntry(tool);
93314
+ if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$5)) return parseClaudeStyleMcpEntry(tool);
92952
93315
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
92953
- category: `${MCP_CANONICAL_PREFIX$4}${inner}`,
92954
- pattern: CATCH_ALL_PATTERN$8
93316
+ category: `${MCP_CANONICAL_PREFIX$5}${inner}`,
93317
+ pattern: CATCH_ALL_PATTERN$9
92955
93318
  } : {
92956
93319
  category: "mcp",
92957
- pattern: CATCH_ALL_PATTERN$8
93320
+ pattern: CATCH_ALL_PATTERN$9
92958
93321
  };
92959
93322
  const category = GROK_TOOL_TO_CATEGORY[tool];
92960
93323
  if (category === void 0) return null;
92961
93324
  return {
92962
93325
  category,
92963
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
93326
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$9
92964
93327
  };
92965
93328
  }
92966
93329
  /**
@@ -92977,14 +93340,14 @@ function parseGrokEntry(entry) {
92977
93340
  function parseClaudeStyleMcpEntry(entry) {
92978
93341
  const rest = entry.slice(5);
92979
93342
  if (rest.length === 0) return null;
92980
- if (rest === CATCH_ALL_PATTERN$8) return {
93343
+ if (rest === CATCH_ALL_PATTERN$9) return {
92981
93344
  category: "mcp",
92982
- pattern: CATCH_ALL_PATTERN$8
93345
+ pattern: CATCH_ALL_PATTERN$9
92983
93346
  };
92984
93347
  const address = rest.includes("__") ? rest : `${rest}__*`;
92985
93348
  return {
92986
- category: `${MCP_CANONICAL_PREFIX$4}${address}`,
92987
- pattern: CATCH_ALL_PATTERN$8
93349
+ category: `${MCP_CANONICAL_PREFIX$5}${address}`,
93350
+ pattern: CATCH_ALL_PATTERN$9
92988
93351
  };
92989
93352
  }
92990
93353
  /**
@@ -93005,20 +93368,20 @@ function parseGrokRule(rule) {
93005
93368
  if (typeof tool !== "string") return null;
93006
93369
  if (pattern !== void 0 && typeof pattern !== "string") return null;
93007
93370
  const trimmedPattern = pattern?.trim() ?? "";
93008
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
93371
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$9;
93009
93372
  const lowerTool = tool.trim().toLowerCase();
93010
- if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
93373
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93011
93374
  category: "*",
93012
- pattern: CATCH_ALL_PATTERN$8,
93375
+ pattern: CATCH_ALL_PATTERN$9,
93013
93376
  action
93014
93377
  } : null;
93015
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
93378
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93016
93379
  category: "mcp",
93017
- pattern: CATCH_ALL_PATTERN$8,
93380
+ pattern: CATCH_ALL_PATTERN$9,
93018
93381
  action
93019
93382
  } : {
93020
- category: `${MCP_CANONICAL_PREFIX$4}${resolvedPattern}`,
93021
- pattern: CATCH_ALL_PATTERN$8,
93383
+ category: `${MCP_CANONICAL_PREFIX$5}${resolvedPattern}`,
93384
+ pattern: CATCH_ALL_PATTERN$9,
93022
93385
  action
93023
93386
  };
93024
93387
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -93168,7 +93531,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
93168
93531
  throw new Error(`Failed to parse Grok config.toml content in ${join(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
93169
93532
  }
93170
93533
  const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
93171
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$8]: legacyModeAction(parsed) } } };
93534
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$9]: legacyModeAction(parsed) } } };
93172
93535
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
93173
93536
  }
93174
93537
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
@@ -93353,6 +93716,195 @@ function ensureCategory(permission, category) {
93353
93716
  function removeEmptyCategories(permission) {
93354
93717
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
93355
93718
  }
93719
+ /** The prefix Hermes's MCP trust gate keys its persisted "Always" answers with. */
93720
+ const MCP_ALLOWLIST_PREFIX = "mcp:";
93721
+ const MCP_CANONICAL_PREFIX$4 = "mcp__";
93722
+ const CATCH_ALL_PATTERN$8 = "*";
93723
+ /**
93724
+ * Whether a `command_allowlist` entry is an MCP trust-gate grant rather than a
93725
+ * shell-command pattern. Since Hermes Agent v0.22.6, answering **Always** to
93726
+ * the approval prompt of a `trust: untrusted` MCP server appends
93727
+ * `mcp:<server>/<tool>` to `command_allowlist` (`_trust_approval_key` in
93728
+ * `tools/mcp_tool_handlers.py`), and the gate looks the key up by exact set
93729
+ * membership, so no `mcp:` entry is ever a command.
93730
+ */
93731
+ function isMcpAllowlistEntry(entry) {
93732
+ return entry.startsWith(MCP_ALLOWLIST_PREFIX);
93733
+ }
93734
+ /**
93735
+ * The canonical category (`mcp__<server>__<tool>`) a Hermes `mcp:<server>/<tool>`
93736
+ * grant stands for, or `null` when it has none: a malformed entry, a server
93737
+ * name containing `__` (the canonical name would read back as another server),
93738
+ * or a `*` in either name (Hermes compares the key literally, while a canonical
93739
+ * `*` is a wildcard).
93740
+ */
93741
+ function mcpAllowlistEntryToCategory(entry) {
93742
+ const parsed = parseMcpAllowlistEntry(entry);
93743
+ if (parsed === null) return null;
93744
+ const { server, tool } = parsed;
93745
+ if (server.includes("__") || server.includes(CATCH_ALL_PATTERN$8)) return null;
93746
+ if (tool.includes(CATCH_ALL_PATTERN$8)) return null;
93747
+ return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
93748
+ }
93749
+ /** The server and tool an `mcp:<server>/<tool>` entry names, or `null` when it is malformed. */
93750
+ function parseMcpAllowlistEntry(entry) {
93751
+ if (!isMcpAllowlistEntry(entry)) return null;
93752
+ const rest = entry.slice(4);
93753
+ const slash = rest.indexOf("/");
93754
+ const server = slash === -1 ? "" : rest.slice(0, slash);
93755
+ const tool = slash === -1 ? "" : rest.slice(slash + 1);
93756
+ return server.length === 0 || tool.length === 0 ? null : {
93757
+ server,
93758
+ tool
93759
+ };
93760
+ }
93761
+ /**
93762
+ * The server and tool a canonical `mcp__<server>__<tool>` category names, or
93763
+ * `null` when it names no single tool (a whole server, a wildcard, or the bare
93764
+ * `mcp` category). Only such a category can become one Hermes grant.
93765
+ */
93766
+ function parseMcpToolCategory(category) {
93767
+ if (!category.startsWith(MCP_CANONICAL_PREFIX$4)) return null;
93768
+ const remainder = category.slice(5);
93769
+ const separator = remainder.indexOf("__");
93770
+ if (separator <= 0) return null;
93771
+ const server = remainder.slice(0, separator);
93772
+ const tool = remainder.slice(separator + 2);
93773
+ if (tool.length === 0 || server.includes("/") || server.includes(CATCH_ALL_PATTERN$8) || tool.includes(CATCH_ALL_PATTERN$8)) return null;
93774
+ return {
93775
+ server,
93776
+ tool
93777
+ };
93778
+ }
93779
+ function isMcpCategory$1(category) {
93780
+ return category === "mcp" || category.startsWith(MCP_CANONICAL_PREFIX$4);
93781
+ }
93782
+ function isRestricting(action) {
93783
+ return action === "deny" || action === "ask";
93784
+ }
93785
+ /** The first `deny` or `ask` rule of a category, as `<category>: <pattern> (<action>)`. */
93786
+ function firstRestrictingRule(category, rules, covers = () => true) {
93787
+ const found = Object.entries(rules ?? {}).find(([pattern, action]) => isRestricting(action) && covers(pattern));
93788
+ return found ? `${category}: ${found[0]} (${found[1]})` : null;
93789
+ }
93790
+ /**
93791
+ * Whether an MCP name glob (the remainder of an `mcp__...` category, or a
93792
+ * pattern of the bare `mcp` category) covers `server`'s tool `tool`: it names
93793
+ * the tool (`github__delete_repo`, `github__*`, `*__delete_repo`, `*`) or the
93794
+ * whole server (`github`).
93795
+ */
93796
+ function mcpNameGlobCovers(glob, server, tool) {
93797
+ return glob === `${server}__${tool}` || glob === server || matchesGlob(glob, `${server}__${tool}`) || matchesGlob(glob, server);
93798
+ }
93799
+ /**
93800
+ * The `deny` or `ask` rule that outranks an allow on `server`'s tool `tool`, or
93801
+ * `null` when none does: any one under the all-tools `*` category (a rule there
93802
+ * applies to every tool, so even a narrower pattern restricts some of the
93803
+ * tool's calls), one under the bare `mcp` category whose pattern covers the
93804
+ * tool, or any one in an `mcp__...` category whose name covers the tool — its
93805
+ * own category included, since a deny or ask scoped to some of its calls still
93806
+ * forbids approving all of them. A Hermes grant approves every call of the
93807
+ * tool and Hermes has no ask tier, so such an allow is withheld, the same way
93808
+ * a `bash` allow a stricter rule covers is.
93809
+ */
93810
+ function findMcpToolRestriction(permission, server, tool) {
93811
+ for (const [category, rules] of Object.entries(permission)) {
93812
+ let found = null;
93813
+ if (category === "*") found = firstRestrictingRule(category, rules);
93814
+ else if (category === "mcp") found = firstRestrictingRule(category, rules, (pattern) => mcpNameGlobCovers(pattern, server, tool));
93815
+ else if (category.startsWith(MCP_CANONICAL_PREFIX$4) && mcpNameGlobCovers(category.slice(5), server, tool)) found = firstRestrictingRule(category, rules);
93816
+ if (found !== null) return found;
93817
+ }
93818
+ return null;
93819
+ }
93820
+ function isMcpToolRestricted(permission, server, tool) {
93821
+ return findMcpToolRestriction(permission, server, tool) !== null;
93822
+ }
93823
+ /**
93824
+ * Translate the canonical MCP `allow` rules into Hermes trust-gate grants: a
93825
+ * `*` allow on `mcp__<server>__<tool>` becomes `mcp:<server>/<tool>`. Every
93826
+ * other MCP allow (a whole server, a wildcard, a narrower pattern) has no
93827
+ * Hermes spelling, and an allow a deny or ask covers is withheld.
93828
+ */
93829
+ function collectMcpAllowlistEntries(permission) {
93830
+ const entries = [];
93831
+ const withheldCategories = [];
93832
+ const unexpressedRules = [];
93833
+ for (const [category, rules] of Object.entries(permission)) {
93834
+ if (!isMcpCategory$1(category)) continue;
93835
+ const parsed = parseMcpToolCategory(category);
93836
+ for (const pattern of patternsByAction(rules, "allow")) if (parsed === null || pattern !== CATCH_ALL_PATTERN$8) unexpressedRules.push(`${category}: ${pattern}`);
93837
+ else {
93838
+ const restriction = findMcpToolRestriction(permission, parsed.server, parsed.tool);
93839
+ if (restriction === null) entries.push(`${MCP_ALLOWLIST_PREFIX}${parsed.server}/${parsed.tool}`);
93840
+ else withheldCategories.push(`${category} (covered by ${restriction})`);
93841
+ }
93842
+ }
93843
+ return {
93844
+ entries,
93845
+ withheldCategories,
93846
+ unexpressedRules
93847
+ };
93848
+ }
93849
+ function warnAboutUnwrittenMcpAllows({ withheldCategories, unexpressedRules, logger }) {
93850
+ 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.`);
93851
+ 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.`);
93852
+ }
93853
+ /**
93854
+ * Make the native `mcp:<server>/<tool>` grants authoritative for the canonical
93855
+ * single-tool MCP allows they can speak for. An allow the generator withheld
93856
+ * because a deny or ask covers it was never written, so its absence
93857
+ * is not a retraction and it is kept, as is every MCP allow with no Hermes
93858
+ * spelling. A grant with no canonical spelling stays only in Hermes, where
93859
+ * generation keeps it (see `setFileContent`).
93860
+ */
93861
+ function reconcileMcpAllowlist({ permission, mcpEntries, logger }) {
93862
+ for (const [category, rules] of Object.entries(permission)) {
93863
+ const parsed = parseMcpToolCategory(category);
93864
+ if (parsed !== null && rules[CATCH_ALL_PATTERN$8] === "allow" && !isMcpToolRestricted(permission, parsed.server, parsed.tool)) delete rules[CATCH_ALL_PATTERN$8];
93865
+ }
93866
+ const keptRestrictions = [];
93867
+ for (const entry of mcpEntries) {
93868
+ const category = mcpAllowlistEntryToCategory(entry);
93869
+ if (category === null) continue;
93870
+ const rules = ensureCategory(permission, category);
93871
+ if (isRestricting(rules[CATCH_ALL_PATTERN$8])) {
93872
+ keptRestrictions.push(entry);
93873
+ continue;
93874
+ }
93875
+ rules[CATCH_ALL_PATTERN$8] = "allow";
93876
+ }
93877
+ 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.`);
93878
+ }
93879
+ /**
93880
+ * The `mcp:` grants in an existing `command_allowlist` that rulesync does not
93881
+ * manage and must keep when it rewrites the list: Hermes appends one whenever
93882
+ * the user answers **Always** on an untrusted MCP server, so dropping it would
93883
+ * silently revoke the grant. A grant is managed — and so retracted with its
93884
+ * canonical rule — when the previous or the new canonical block has a rule
93885
+ * for its `mcp__<server>__<tool>` category, or when the new block restricts
93886
+ * the tool in any way.
93887
+ */
93888
+ function unmanagedMcpAllowlistEntries({ existing, previousPermission, nextPermission, nextAllowlist }) {
93889
+ return (isStringArray(existing.command_allowlist) ? existing.command_allowlist : []).filter((entry) => {
93890
+ if (!isMcpAllowlistEntry(entry) || nextAllowlist.includes(entry)) return false;
93891
+ const parsed = parseMcpAllowlistEntry(entry);
93892
+ if (parsed !== null && isMcpToolRestricted(nextPermission, parsed.server, parsed.tool)) return false;
93893
+ const category = mcpAllowlistEntryToCategory(entry);
93894
+ return category === null || !Object.hasOwn(previousPermission, category) && !Object.hasOwn(nextPermission, category);
93895
+ });
93896
+ }
93897
+ /** The validated canonical `permission` block of a config's `permissions.rulesync` blob. */
93898
+ function canonicalPermissionOf(config) {
93899
+ const parsed = RulesyncPermissionsFileSchema.safeParse({ permission: provenancePermissionOf(config) });
93900
+ return parsed.success ? parsed.data.permission : {};
93901
+ }
93902
+ /** The canonical `permission` block of a config's `permissions.rulesync` blob, if any. */
93903
+ function provenancePermissionOf(config) {
93904
+ const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
93905
+ const provenance = isRecord(permissionsRoot.rulesync) ? permissionsRoot.rulesync : {};
93906
+ return isRecord(provenance.permission) ? provenance.permission : {};
93907
+ }
93356
93908
  /**
93357
93909
  * Make the native `command_allowlist` authoritative for the `bash` allow rules
93358
93910
  * it can speak for, and for those only. The allowlist is a list of shell-command
@@ -93370,8 +93922,9 @@ function reconcileCommandAllowlist({ permission, commandAllowlist }) {
93370
93922
  writesAllToolsDeny: false
93371
93923
  });
93372
93924
  const withheld = new Set(shadowedAllowPatterns);
93925
+ const isWithheld = (pattern) => withheld.has(pattern) || isMcpAllowlistEntry(pattern);
93373
93926
  const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
93374
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
93927
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !isWithheld(pattern)) delete rules[pattern];
93375
93928
  for (const pattern of commandAllowlist) rules[pattern] = "allow";
93376
93929
  }
93377
93930
  /**
@@ -93459,17 +94012,31 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93459
94012
  return true;
93460
94013
  }
93461
94014
  setFileContent(fileContent) {
94015
+ const patch = parseSharedConfig({
94016
+ format: "yaml",
94017
+ fileContent: this.fileContent
94018
+ });
94019
+ if (isStringArray(patch.command_allowlist)) {
94020
+ const existing = parseSharedConfig({
94021
+ format: "yaml",
94022
+ fileContent
94023
+ });
94024
+ const kept = unmanagedMcpAllowlistEntries({
94025
+ existing,
94026
+ previousPermission: provenancePermissionOf(existing),
94027
+ nextPermission: canonicalPermissionOf(patch),
94028
+ nextAllowlist: patch.command_allowlist
94029
+ });
94030
+ if (kept.length > 0) patch.command_allowlist = [...patch.command_allowlist, ...kept];
94031
+ }
93462
94032
  this.fileContent = applySharedConfigPatch({
93463
94033
  fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
93464
94034
  feature: "permissions",
93465
94035
  existingContent: fileContent,
93466
- patch: parseSharedConfig({
93467
- format: "yaml",
93468
- fileContent: this.fileContent
93469
- })
94036
+ patch
93470
94037
  });
93471
94038
  }
93472
- toRulesyncPermissions() {
94039
+ toRulesyncPermissions({ logger } = {}) {
93473
94040
  const config = parseSharedConfig({
93474
94041
  format: "yaml",
93475
94042
  fileContent: this.getFileContent()
@@ -93481,9 +94048,15 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93481
94048
  }) : rawProvenance);
93482
94049
  const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
93483
94050
  const permission = clonePermissionBlock(provenance.permission);
94051
+ const commandAllowlist = isStringArray(config.command_allowlist) ? config.command_allowlist : [];
93484
94052
  reconcileCommandAllowlist({
93485
94053
  permission,
93486
- commandAllowlist: isStringArray(config.command_allowlist) ? config.command_allowlist : []
94054
+ commandAllowlist: commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry))
94055
+ });
94056
+ reconcileMcpAllowlist({
94057
+ permission,
94058
+ mcpEntries: commandAllowlist.filter(isMcpAllowlistEntry),
94059
+ logger
93487
94060
  });
93488
94061
  const approvals = isRecord(config.approvals) ? config.approvals : {};
93489
94062
  reconcileNativeDenies({
@@ -93541,8 +94114,16 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93541
94114
  logger
93542
94115
  });
93543
94116
  const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
94117
+ const mcpAllows = collectMcpAllowlistEntries(permissionBlock);
94118
+ warnAboutUnwrittenMcpAllows({
94119
+ ...mcpAllows,
94120
+ logger
94121
+ });
94122
+ const bashMcpAllows = commandAllowlist.filter(isMcpAllowlistEntry);
94123
+ 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.`);
94124
+ const allowlist = [...commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry)), ...mcpAllows.entries];
93544
94125
  let config = {};
93545
- if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
94126
+ if (allowlist.length > 0) config.command_allowlist = allowlist;
93546
94127
  if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
93547
94128
  if (webfetchDeny.length > 0) config.security = { website_blocklist: {
93548
94129
  enabled: true,
@@ -100383,6 +100964,7 @@ const TAKT_SECURITY_POLICIES = {
100383
100964
  ],
100384
100965
  workflow_runtime_prepare: ["custom_scripts"],
100385
100966
  workflow_command_gates: ["custom_scripts"],
100967
+ workflow_system_commands: ["custom_scripts"],
100386
100968
  sync_conflict_resolver: ["auto_approve_tools"],
100387
100969
  allow_git_hooks: null,
100388
100970
  allow_git_filters: null
@@ -107352,4 +107934,4 @@ async function importChecksCore(params) {
107352
107934
  }
107353
107935
  export { RulesyncPermissions as $, formatError as $n, isSymlink as $t, CODEBUDDY_DIR as A, MAX_FILE_SIZE as An, withFallbackLoggerTarget as At, CLAUDECODE_LOCAL_RULE_FILE_NAME as B, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Bn, assertTreeContainsNoSymlinks as Bt, QWENCODE_LOCAL_RULE_FILE_NAME as C, PACKAGING_TOOL_TARGETS as Cn, getSourceTransport as Ct, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as D, DEPRECATED_FEATURE_REPLACEMENTS as Dn, WarningCollectingLogger as Dt, FACTORYDROID_DIR as E, ALL_FEATURES_WITH_WILDCARD as En, JsonLogger as Et, groupSpellingsByCaseFoldedIdentity as F, RULESYNC_CONFIG_SCHEMA_URL as Fn, PathOutsideRootError as Ft, toInstructionBudgetFile as G, RULESYNC_PERMISSIONS_SCHEMA_URL as Gn, ensureDir as Gt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as H, RULESYNC_MCP_SCHEMA_URL as Hn, checkPathTraversal as Ht, CODEXCLI_BASH_RULES_FILE_NAME as I, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as In, applyFileMode as It, RulesyncSubagentFrontmatterSchema as J, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Jn, findFilesByGlobs as Jt, getLocalSkillDirNames as K, RULESYNC_RELATIVE_DIR_PATH as Kn, fileExists as Kt, CODEXCLI_DIR as L, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as Ln, assertDirectoryIfExists as Lt, AUGMENTCODE_DIR as M, RULESYNC_CHECKS_RELATIVE_DIR_PATH as Mn, withWarnOnceScope as Mt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as N, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as Nn, CLIError as Nt, COMMANDCODE_DIR as O, CURATED_RULES_FEATURE_SUBDIR as On, fallbackLogger as Ot, caseFoldIdentity as P, RULESYNC_CONFIG_RELATIVE_FILE_PATH as Pn, ErrorCodes as Pt, RulesyncRuleFrontmatterSchema as Q, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as Qn, isFileSystemError as Qt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as R, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Rn, assertNoSymlinkBelowRoot as Rt, QWENCODE_DIR as S, ALL_TOOL_TARGETS_WITH_WILDCARD as Sn, assertTargetsFeaturesExclusive as St, LETTACODE_SETTINGS_LOCAL_FILE_NAME as T, ALL_FEATURES as Tn, ConsoleLogger as Tt, CLAUDECODE_SKILLS_DIR_PATH as U, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as Un, createTempDirectory as Ut, CLAUDECODE_MEMORIES_DIR_NAME as V, RULESYNC_MCP_RELATIVE_FILE_PATH as Vn, assertWritablePathInsideRoot as Vt, findProjectInstructionBudgetFindings as W, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as Wn, directoryExists as Wt, RulesyncSkillFrontmatterSchema as X, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Xn, getHomeDirectory as Xt, RulesyncSkill as Y, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as Yn, getFileSizeAndExecutableBit as Yt, RulesyncRule as Z, RULESYNC_USER_CONFIG_DIR_NAME as Zn, isFileNotFoundError as Zt, SubagentsProcessor as _, toPosixPath as _n, resolveEffectiveInputRoots as _t, convertFromTool as a, readFileContent as an, stripControlCharactersKeepingLineFeeds as ar, parseJsonc as at, CommandsProcessor as b, mapWithConcurrency as bn, GITIGNORE_DESTINATION_KEY as bt, parseCommaSeparatedList as c, removeDirectoryStrict as cn, RulesyncCheck as ct, shortenToWidth as d, removeFileWithoutFollowingSymlinks as dn, loadYaml as dt, listDirectoryEntryNames as en, truncateText as er, RulesyncMcp as et, McpProcessor as f, removeTempDirectory as fn, isRecord as ft, RulesProcessor as g, splitPathSegments as gn, mergeInputRootConfigs as gt, ChecksProcessor as h, runWithDirectoryRollback as hn, ConfigResolver as ht, inspectInputRoots as i, posixRelativePathEscapesRoot as in, stripControlCharacters as ir, resolveRulesyncSourceWritePath as it, CODEBUDDY_LOCAL_RULE_FILE_NAME as j, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as jn, resetRunWarningState as jt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as k, FEATURE_SOURCE_TREE_ENTRIES as kn, warnOnConflictingFlags as kt, ELLIPSIS_WIDTH as l, removeFile as ln, RulesyncCheckFrontmatterSchema as lt, HooksProcessor as m, resolvedRelativePath as mn, SKILL_FILE_NAME as mt, formatSourceLoadFailure as n, listSubdirectoryNames as nn, hasEnclosingMarkOutsideKeycap as nr, RulesyncHooks as nt, isPackagingToolTarget as o, readFileContentOrNull as on, stripControlCharactersKeepingLineFeedsAndTabs as or, RulesyncCommand as ot, IgnoreProcessor as p, resolvePath as pn, SHARED_USER_MANAGED_CONFIG_PATHS as pt, RulesyncSubagent as q, RULESYNC_RULES_RELATIVE_DIR_PATH as qn, fileExistsStrict as qt, generate as r, pathEscapesRoot as rn, quoteForLog as rr, getRulesyncSourceCandidates as rt, getProcessorRegistryEntry as s, removeDirectory as sn, stripHiddenCharacters as sr, RulesyncCommandFrontmatterSchema as st, importFromTool as t, listFilePathsRecursively as tn, hasDeceptiveHiddenCharacters as tr, RulesyncIgnore as tt, displayWidthOf as u, removeFileStrict as un, stringifyFrontmatter as ut, SkillsProcessor as v, writeFileBuffer as vn, CONFLICTING_TARGET_PAIRS as vt, LETTACODE_DIR as w, ToolTargetSchema as wn, findControlCharacter as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, ALL_TOOL_TARGETS as xn, SourceEntrySchema as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, writeFileContent as yn, ConfigFileSchema as yt, CLAUDECODE_DIR as z, RULESYNC_IGNORE_RELATIVE_FILE_PATH as zn, assertPathResolvesInsideRoot as zt };
107354
107936
 
107355
- //# sourceMappingURL=import-BWOC-Tig.js.map
107937
+ //# sourceMappingURL=import-C_XK2L62.js.map