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.
@@ -8400,17 +8400,29 @@ const CodewhalePermissionsOverrideSchema = zod_mini.z.looseObject({
8400
8400
  rules: zod_mini.z.optional(zod_mini.z.array(CodewhalePermissionRuleSchema))
8401
8401
  });
8402
8402
  /**
8403
- * The starting permission level VS Code Copilot Chat gives new chat sessions
8404
- * (`chat.permissions.default`). Values are VS Code's own: `default` (Default
8403
+ * The values VS Code's `chat.permissions.default` setting accepts: the starting
8404
+ * permission level of new Local-harness chat sessions — `default` (Default
8405
8405
  * Permissions), `autoApprove` (Bypass Approvals) and `autopilot` (Autopilot).
8406
8406
  *
8407
8407
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
8408
8408
  */
8409
- const COPILOT_DEFAULT_PERMISSION_MODES = [
8409
+ const COPILOT_LOCAL_PERMISSION_LEVELS = [
8410
8410
  "default",
8411
8411
  "autoApprove",
8412
8412
  "autopilot"
8413
8413
  ];
8414
+ /**
8415
+ * The starting permission level the `copilot.defaultPermissionMode` override
8416
+ * sets for new chat sessions: the {@link COPILOT_LOCAL_PERMISSION_LEVELS}, plus
8417
+ * `assisted` (Assisted permissions, GA for Copilot-harness sessions since VS
8418
+ * Code 1.144 and unavailable to Local sessions). Each value is written to both
8419
+ * `chat.permissions.default` (Local sessions) and the `approvals` / `mode` of
8420
+ * `chat.defaultConfiguration` (Copilot-harness sessions, VS Code 1.144+).
8421
+ *
8422
+ * @see https://code.visualstudio.com/updates/v1_144
8423
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
8424
+ */
8425
+ const COPILOT_DEFAULT_PERMISSION_MODES = [...COPILOT_LOCAL_PERMISSION_LEVELS, "assisted"];
8414
8426
  /** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
8415
8427
  const CopilotSandboxEnabledSchema = zod_mini.z.enum(["off", "on"]);
8416
8428
  /** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
@@ -8451,7 +8463,7 @@ const CopilotSandboxSchema = zod_mini.z.strictObject({
8451
8463
  /**
8452
8464
  * Tool-scoped override block for GitHub Copilot in VS Code.
8453
8465
  * `defaultPermissionMode` writes the workspace `chat.permissions.default`
8454
- * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
8466
+ * and `chat.defaultConfiguration` settings, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
8455
8467
  * has a canonical category.
8456
8468
  */
8457
8469
  const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
@@ -9333,6 +9345,7 @@ const TaktPermissionsOverrideSchema = zod_mini.z.looseObject({
9333
9345
  })),
9334
9346
  workflow_runtime_prepare: zod_mini.z.optional(zod_mini.z.object({ custom_scripts: zod_mini.z.optional(zod_mini.z.boolean()) })),
9335
9347
  workflow_command_gates: zod_mini.z.optional(zod_mini.z.object({ custom_scripts: zod_mini.z.optional(zod_mini.z.boolean()) })),
9348
+ workflow_system_commands: zod_mini.z.optional(zod_mini.z.object({ custom_scripts: zod_mini.z.optional(zod_mini.z.boolean()) })),
9336
9349
  sync_conflict_resolver: zod_mini.z.optional(zod_mini.z.object({ auto_approve_tools: zod_mini.z.optional(zod_mini.z.boolean()) })),
9337
9350
  allow_git_hooks: zod_mini.z.optional(zod_mini.z.boolean()),
9338
9351
  allow_git_filters: zod_mini.z.optional(zod_mini.z.boolean())
@@ -9600,6 +9613,7 @@ const CodexSandboxModeSchema = zod_mini.z.enum([
9600
9613
  "workspace-write",
9601
9614
  "danger-full-access"
9602
9615
  ]);
9616
+ const CODEX_U32_MAX = 4294967295;
9603
9617
  /**
9604
9618
  * Codex CLI's reviewer for approval requests. `guardian_subagent` is a legacy
9605
9619
  * value Codex still accepts for backward compatibility, so it is included so
@@ -9668,6 +9682,9 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
9668
9682
  * | `guardian_subagent`), or a table for the richer reviewer config.
9669
9683
  * Defaults to `auto_review` when neither the override nor the existing
9670
9684
  * config sets it.
9685
+ * - `auto_review` — the `[auto_review]` table that configures the reviewer
9686
+ * `approvals_reviewer = "auto_review"` selects (`policy`, `extra_policy`,
9687
+ * `timeout_ms`, …). A loose table, shallow-merged like `apps` / `tui`.
9671
9688
  * - `git_write_rules` — whether the managed profile's `:workspace_roots` table
9672
9689
  * emits the default `.git` carve-out (`".git/**" = "write"`). Codex's
9673
9690
  * `:workspace` baseline makes `.git` read-only, which denies basic git
@@ -9681,7 +9698,8 @@ const CodexBasePermissionProfileSchema = zod_mini.z.enum(CODEX_BASE_PERMISSION_P
9681
9698
  * The keys written to `config.toml` are an **allowlist**, not verbatim
9682
9699
  * passthrough: only `CODEXCLI_OVERRIDE_KEYS`
9683
9700
  * (`src/constants/codexcli-paths.ts` — `approval_policy`, `sandbox_mode`,
9684
- * `sandbox_workspace_write`, `apps`, `approvals_reviewer`) are emitted, and
9701
+ * `sandbox_workspace_write`, `apps`, `approvals_reviewer`, `auto_review`,
9702
+ * `tui`) are emitted, and
9685
9703
  * `computeCodexcliOverridePatch` skips anything else with a warning.
9686
9704
  * `base_permission_profile` and `git_write_rules` are consumed by the profile
9687
9705
  * builder rather than written, as described above, and `permission` is the
@@ -9713,6 +9731,26 @@ const CodexcliPermissionsOverrideSchema = zod_mini.z.looseObject({
9713
9731
  apps: zod_mini.z.optional(zod_mini.z.looseObject({})),
9714
9732
  approvals_reviewer: zod_mini.z.optional(zod_mini.z.union([CodexApprovalsReviewerSchema, zod_mini.z.looseObject({})])),
9715
9733
  /**
9734
+ * The `[auto_review]` table of `config.toml`, which configures the reviewer
9735
+ * that `approvals_reviewer = "auto_review"` (Rulesync's default) selects:
9736
+ * `policy` / `extra_policy` (text inserted into the reviewer prompt),
9737
+ * `timeout_ms` (the review budget, Codex 0.165.0+; `0` waits without a
9738
+ * deadline), `circuit_break_action` (`default` | `strict`) and
9739
+ * `conversation_history_max_output_tokens` (a positive token cap). Codex
9740
+ * rejects the whole config file when one of these has the wrong type or is
9741
+ * out of range (`timeout_ms` is a `u32`, the token cap a `NonZeroUsize`), so
9742
+ * they are checked here; every other key passes through verbatim.
9743
+ *
9744
+ * @see https://github.com/openai/codex/blob/rust-v0.166.0/codex-rs/config/src/config_toml.rs (`AutoReviewToml`)
9745
+ */
9746
+ auto_review: zod_mini.z.optional(zod_mini.z.looseObject({
9747
+ policy: zod_mini.z.optional(zod_mini.z.string()),
9748
+ extra_policy: zod_mini.z.optional(zod_mini.z.string()),
9749
+ timeout_ms: zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.nonnegative(), zod_mini.z.lte(CODEX_U32_MAX))),
9750
+ circuit_break_action: zod_mini.z.optional(zod_mini.z.enum(["default", "strict"])),
9751
+ conversation_history_max_output_tokens: zod_mini.z.optional(zod_mini.z.int().check(zod_mini.z.positive()))
9752
+ })),
9753
+ /**
9716
9754
  * The `[tui]` table of `config.toml` (e.g. `vim_mode_default`, `keymap.*`).
9717
9755
  * Not a permission surface, but like `apps` it is a top-level table with no
9718
9756
  * canonical category, and Codex adds keys to it faster than an explicit model
@@ -12319,6 +12357,7 @@ const CODEXCLI_OVERRIDE_KEYS = [
12319
12357
  "sandbox_workspace_write",
12320
12358
  "apps",
12321
12359
  "approvals_reviewer",
12360
+ "auto_review",
12322
12361
  "tui"
12323
12362
  ];
12324
12363
  // src/constants/takt-paths.ts
@@ -13741,6 +13780,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13741
13780
  "workflow_arpeggio",
13742
13781
  "workflow_runtime_prepare",
13743
13782
  "workflow_command_gates",
13783
+ "workflow_system_commands",
13744
13784
  "sync_conflict_resolver",
13745
13785
  "allow_git_hooks",
13746
13786
  "allow_git_filters"
@@ -13882,6 +13922,7 @@ const SHARED_CONFIG_OWNERSHIP = {
13882
13922
  "chat.tools.edits.autoApprove",
13883
13923
  "chat.tools.urls.autoApprove",
13884
13924
  "chat.permissions.default",
13925
+ "chat.defaultConfiguration",
13885
13926
  "chat.agent.sandbox.enabled",
13886
13927
  "chat.agent.sandbox.network.allowNetwork",
13887
13928
  "chat.agent.sandbox.network.allowLocalNetwork",
@@ -34123,7 +34164,7 @@ function normalizeAugmentcodeToolList(value) {
34123
34164
  return tools.length > 0 ? tools : void 0;
34124
34165
  }
34125
34166
  const AugmentcodeSubagentFrontmatterSchema = zod_mini.z.looseObject({
34126
- name: zod_mini.z.string(),
34167
+ name: zod_mini.z.optional(zod_mini.z.string()),
34127
34168
  description: zod_mini.z.optional(zod_mini.z.string()),
34128
34169
  color: zod_mini.z.optional(zod_mini.z.string()),
34129
34170
  model: zod_mini.z.optional(zod_mini.z.string()),
@@ -34182,7 +34223,7 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
34182
34223
  outputRoot: ".",
34183
34224
  frontmatter: {
34184
34225
  targets: ["*"],
34185
- name,
34226
+ name: name ?? (0, node_path.basename)(this.getRelativeFilePath(), (0, node_path.extname)(this.getRelativeFilePath())),
34186
34227
  description,
34187
34228
  ...Object.keys(augmentcodeSection).length > 0 && { augmentcode: augmentcodeSection }
34188
34229
  },
@@ -34248,7 +34289,8 @@ var AugmentcodeSubagent = class AugmentcodeSubagent extends ToolSubagent {
34248
34289
  const dirPath = relativeDirPath ?? this.getSettablePaths({ global }).relativeDirPath;
34249
34290
  const filePath = (0, node_path.join)(outputRoot, dirPath, relativeFilePath);
34250
34291
  const fileContent = await readFileContent(filePath);
34251
- const { frontmatter, body: content } = parseFrontmatter(fileContent, filePath);
34292
+ const { frontmatter, body: content, hasFrontmatter } = parseFrontmatter(fileContent, filePath);
34293
+ if (!hasFrontmatter) throw new Error(`Missing frontmatter in ${filePath}`);
34252
34294
  const result = AugmentcodeSubagentFrontmatterSchema.safeParse(frontmatter);
34253
34295
  if (!result.success) throw new Error(`Invalid frontmatter in ${filePath}: ${formatError(result.error)}`);
34254
34296
  return new AugmentcodeSubagent({
@@ -41831,7 +41873,23 @@ function sanitizeOpencodeMcpNamePart(value) {
41831
41873
  * key that matches nothing.
41832
41874
  */
41833
41875
  function opencodeMcpToolPermissionKey(serverName, toolName) {
41834
- return `${sanitizeOpencodeMcpNamePart(serverName)}_${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
41876
+ return `${opencodeMcpServerKeyPrefix(serverName)}${toolName.replace(/[^a-zA-Z0-9_\-*?]/g, "_")}`;
41877
+ }
41878
+ /** The sanitized `<server>_` prefix every permission key of the server's tools starts with. */
41879
+ function opencodeMcpServerKeyPrefix(serverName) {
41880
+ return `${sanitizeOpencodeMcpNamePart(serverName)}_`;
41881
+ }
41882
+ /**
41883
+ * Whether `key` is exactly the sanitized `<server>_` prefix of one of
41884
+ * `serverNames` (e.g. `a_b_` for server `a.b` or `a_b`). No tool has an empty
41885
+ * name, so such a key filters nothing of that server, and
41886
+ * {@link findOpencodeMcpToolKeyOwner} gives it no owner. Read as an unlisted
41887
+ * server's filter instead, it would be split at the first `_` into a phantom
41888
+ * server (`a` with tool `b_`), so importers leave it out of the rulesync
41889
+ * config, and the OpenCode family's generate keeps it in the file as it was.
41890
+ */
41891
+ function isBareOpencodeMcpServerPrefix({ key, serverNames }) {
41892
+ return serverNames.some((serverName) => key === opencodeMcpServerKeyPrefix(serverName));
41835
41893
  }
41836
41894
  /**
41837
41895
  * The server among `serverNames` whose tool a permission key names, with the
@@ -41845,7 +41903,7 @@ function findOpencodeMcpToolKeyOwner({ key, serverNames, builtinKeys, acceptRawP
41845
41903
  if (builtinKeys.has(key)) return;
41846
41904
  let owner;
41847
41905
  for (const serverName of serverNames) {
41848
- const sanitized = `${sanitizeOpencodeMcpNamePart(serverName)}_`;
41906
+ const sanitized = opencodeMcpServerKeyPrefix(serverName);
41849
41907
  const prefixes = acceptRawPrefix ? (0, es_toolkit.uniq)([sanitized, `${serverName}_`]) : [sanitized];
41850
41908
  for (const prefix of prefixes) if (key.length > prefix.length && key.startsWith(prefix) && (owner === void 0 || prefix.length > owner.prefix.length)) owner = {
41851
41909
  serverName,
@@ -42306,7 +42364,10 @@ function convertFromKiloFormat(kiloMcp, tools, permission) {
42306
42364
  const serverNames = Object.keys(kiloMcp);
42307
42365
  return {
42308
42366
  ...Object.fromEntries(Object.entries(kiloMcp).map(([serverName, serverConfig]) => [serverName, kiloServerToRulesync(serverName, serverConfig, splitKiloServerTools(serverName, serverNames, filters))])),
42309
- ...orphanMcpToolFiltersToRulesync(kiloMcp, tools)
42367
+ ...orphanMcpToolFiltersToRulesync(kiloMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findKiloMcpToolKeyOwner(key, serverNames) && !isBareOpencodeMcpServerPrefix({
42368
+ key,
42369
+ serverNames
42370
+ }))))
42310
42371
  };
42311
42372
  }
42312
42373
  /**
@@ -42824,16 +42885,20 @@ function convertFromOpencodeFormat(opencodeMcp, tools, remoteType) {
42824
42885
  ...orphanMcpToolFiltersToRulesync(opencodeMcp, Object.fromEntries(Object.entries(tools ?? {}).filter(([key]) => !findOpencodeToolOwner({
42825
42886
  key,
42826
42887
  serverNames
42888
+ }) && !isBareOpencodeMcpServerPrefix({
42889
+ key,
42890
+ serverNames
42827
42891
  })))),
42828
42892
  ...convertOpencodeServers(opencodeMcp, tools, remoteType)
42829
42893
  };
42830
42894
  }
42895
+ const OPENCODE_MCP_BUILTIN_TOOL_KEYS = /* @__PURE__ */ new Set();
42831
42896
  /** Recognize both OpenCode's sanitized names and keys older rulesync versions wrote. */
42832
42897
  function findOpencodeToolOwner({ key, serverNames }) {
42833
42898
  return findOpencodeMcpToolKeyOwner({
42834
42899
  key,
42835
42900
  serverNames,
42836
- builtinKeys: /* @__PURE__ */ new Set(),
42901
+ builtinKeys: OPENCODE_MCP_BUILTIN_TOOL_KEYS,
42837
42902
  acceptRawPrefix: true
42838
42903
  });
42839
42904
  }
@@ -43003,6 +43068,40 @@ function convertToOpencodeFormat(mcpServers, existingMcp, configShape, logger) {
43003
43068
  tools
43004
43069
  };
43005
43070
  }
43071
+ /**
43072
+ * Copy each key of `existingTools` that is a bare sanitized `<server>_` prefix
43073
+ * of one of `serverNames` into `tools`, unless a generated filter already
43074
+ * writes the same key or another listed server owns it (`a_b_` is tool `b_` of
43075
+ * server `a` when `a` is listed too). An owned key is the rulesync config's to
43076
+ * write, so carrying it over would keep a filter the user removed. Mutates
43077
+ * `tools` in place.
43078
+ */
43079
+ function preserveBareOpencodeMcpServerPrefixKeys({ tools, existingTools, serverNames }) {
43080
+ for (const [key, enabled] of Object.entries(existingTools ?? {})) if (!Object.hasOwn(tools, key) && isBareOpencodeMcpServerPrefix({
43081
+ key,
43082
+ serverNames
43083
+ }) && !findOpencodeToolOwner({
43084
+ key,
43085
+ serverNames
43086
+ })) tools[key] = enabled;
43087
+ }
43088
+ /**
43089
+ * When the existing file does not pass the schema, its `tools` map cannot be
43090
+ * read, so a bare `<server>_` key in it is not carried over. Say so rather
43091
+ * than lift the deny it may hold without a word.
43092
+ */
43093
+ function warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({ existingJson, serverNames, logger }) {
43094
+ const rawTools = isRecord(existingJson) ? existingJson.tools : void 0;
43095
+ if (!isRecord(rawTools)) return;
43096
+ const bareKeys = Object.keys(rawTools).filter((key) => isBareOpencodeMcpServerPrefix({
43097
+ key,
43098
+ serverNames
43099
+ }) && !findOpencodeToolOwner({
43100
+ key,
43101
+ serverNames
43102
+ }));
43103
+ 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".`);
43104
+ }
43006
43105
  const OpencodeMcpFeatureOptionsSchema = zod_mini.z.looseObject({ configShape: zod_mini.z.optional(zod_mini.z.enum(["v1", "v2"])) });
43007
43106
  var OpencodeMcp = class extends ToolMcp {
43008
43107
  /** Directory layout; OpenCode forks (MiMo Code) override it. */
@@ -43135,6 +43234,16 @@ var OpencodeMcp = class extends ToolMcp {
43135
43234
  convertedMcp,
43136
43235
  mcpServers: transformedServers
43137
43236
  });
43237
+ if (existingMcp.success) preserveBareOpencodeMcpServerPrefixKeys({
43238
+ tools: mcpTools,
43239
+ existingTools: existingMcp.data.tools,
43240
+ serverNames: Object.keys(transformedServers)
43241
+ });
43242
+ else warnAboutUnreadableBareOpencodeMcpServerPrefixKeys({
43243
+ existingJson: this.lowerConfig(existingJson),
43244
+ serverNames: Object.keys(transformedServers),
43245
+ logger
43246
+ });
43138
43247
  if (this.layout.configVersion === "v2") warnAboutOpencodeV2SseServers({
43139
43248
  mcpServers: transformedServers,
43140
43249
  convertedMcp,
@@ -51689,6 +51798,7 @@ var RulesProcessor = class extends FeatureProcessor {
51689
51798
  factory
51690
51799
  });
51691
51800
  await this.warnForWarpLegacyRootFile(toolRules);
51801
+ this.warnForAugmentcodePluginRules(toolRules);
51692
51802
  await this.warnForInstructionBudget({
51693
51803
  toolRules,
51694
51804
  meta
@@ -52080,6 +52190,20 @@ var RulesProcessor = class extends FeatureProcessor {
52080
52190
  this.logger.warn(`Writing ${stripControlCharacters(rootFileRelativePath)} for ${this.toolTarget} means ${listedNames.join(", ")}${remainingCount > 0 ? ` and ${remainingCount} more` : ""} will no longer be read. Run \`rulesync import --targets ${this.toolTarget}\` first to carry that content into ${RULESYNC_RULES_RELATIVE_DIR_PATH}, or delete ${listedNames.length === 1 ? "it" : "them"} once you have checked the content is already in the root file.`);
52081
52191
  }
52082
52192
  /**
52193
+ * Auggie 1.x (still a prerelease on npm at the time of writing; the stable
52194
+ * line is 0.36.0) no longer loads `rules/` from plugins: "Rules shipped in
52195
+ * plugins (`rules/` in a plugin)" is listed among the removed features, while
52196
+ * commands, agents, skills, hooks and MCP servers still load. The rule files
52197
+ * are still written, because the stable 0.x CLI reads them, but under 1.x the
52198
+ * bundle silently loses them, so say so.
52199
+ * @see https://docs.augmentcode.com/cli/whats-new
52200
+ * @see https://docs.augmentcode.com/cli/v0/plugins
52201
+ */
52202
+ warnForAugmentcodePluginRules(toolRules) {
52203
+ if (this.toolTarget !== "augmentcode-plugin" || toolRules.length === 0) return;
52204
+ 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.`);
52205
+ }
52206
+ /**
52083
52207
  * Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
52084
52208
  * `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so an
52085
52209
  * `AGENTS.md` this run writes next to one would never be read. That holds for
@@ -82322,6 +82446,57 @@ function matchesGlobStep(step, character) {
82322
82446
  const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
82323
82447
  return step.negated ? !admitted : admitted;
82324
82448
  }
82449
+ /**
82450
+ * Whether `value` matches `glob` in full, with `*`, `?` and `[...]` carrying
82451
+ * their usual meaning. The glob comes first, unlike Node's own
82452
+ * `path.matchesGlob(path, pattern)`.
82453
+ *
82454
+ * The glob is walked step by step rather than translated to a regex, because
82455
+ * both sides of a comparison can come from a file a repository carries: `*a*a*a*a*b`
82456
+ * translated to `^.*a.*a.*a.*a.*b$` takes minutes against a forty-character
82457
+ * name, while walking the steps is linear in the name per `*`. Remembering only
82458
+ * the most recent `*` to fall back to is enough — an earlier one can always give
82459
+ * up whatever the later one needed.
82460
+ */
82461
+ function matchesGlob(glob, value) {
82462
+ return compileGlob(glob)(value);
82463
+ }
82464
+ /**
82465
+ * Parse `glob` once and return a predicate that walks it, for a caller that
82466
+ * tests the same glob against a whole list of names.
82467
+ */
82468
+ function compileGlob(glob) {
82469
+ const steps = parseGlob(glob);
82470
+ return (value) => matchesParsedGlob(steps, value);
82471
+ }
82472
+ function matchesParsedGlob(steps, value) {
82473
+ const characters = [...value];
82474
+ let stepIndex = 0;
82475
+ let characterIndex = 0;
82476
+ let starStepIndex = -1;
82477
+ let starCharacterIndex = 0;
82478
+ while (characterIndex < characters.length) {
82479
+ const step = steps[stepIndex];
82480
+ if (step?.kind === "star") {
82481
+ starStepIndex = stepIndex;
82482
+ starCharacterIndex = characterIndex;
82483
+ stepIndex += 1;
82484
+ continue;
82485
+ }
82486
+ if (step !== void 0 && matchesGlobStep(step, characters[characterIndex] ?? "")) {
82487
+ stepIndex += 1;
82488
+ characterIndex += 1;
82489
+ continue;
82490
+ }
82491
+ if (starStepIndex < 0) return false;
82492
+ starCharacterIndex += 1;
82493
+ stepIndex = starStepIndex + 1;
82494
+ characterIndex = starCharacterIndex;
82495
+ }
82496
+ let remaining = stepIndex;
82497
+ while (steps[remaining]?.kind === "star") remaining += 1;
82498
+ return remaining === steps.length;
82499
+ }
82325
82500
  /** Whether two single-character steps can both match one same character. */
82326
82501
  function stepsShareACharacter(left, right) {
82327
82502
  if (left.kind === "any" || right.kind === "any") return true;
@@ -83858,9 +84033,13 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
83858
84033
  const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
83859
84034
  const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
83860
84035
  terminal: "launch-process",
84036
+ bash: "launch-process",
84037
+ shell: "launch-process",
83861
84038
  read: "view",
83862
84039
  edit: "str-replace-editor",
83863
- write: "save-file"
84040
+ str_replace_editor: "str-replace-editor",
84041
+ write: "save-file",
84042
+ save_file: "save-file"
83864
84043
  };
83865
84044
  function toAugmentToolName(canonical) {
83866
84045
  return lookupOwn({
@@ -84012,6 +84191,7 @@ function isShellRegexRoundtrippable(regex) {
84012
84191
  while (i < body.length) {
84013
84192
  const ch = body[i];
84014
84193
  if (ch === "\\" && i + 1 < body.length) {
84194
+ if (isGlobSpecial(body[i + 1])) return false;
84015
84195
  i += 2;
84016
84196
  continue;
84017
84197
  }
@@ -84028,6 +84208,37 @@ function isShellRegexRoundtrippable(regex) {
84028
84208
  }
84029
84209
  return true;
84030
84210
  }
84211
+ /** Characters a glob reads as syntax (wildcards or the start of a character class). */
84212
+ function isGlobSpecial(ch) {
84213
+ return ch === "*" || ch === "?" || ch === "[";
84214
+ }
84215
+ /**
84216
+ * Whether `shellRegexToGlob` can approximate a `shellInputRegex` without widening it. The lossy
84217
+ * conversion keeps regex quantifiers as characters, and a `*` or `?` left in the glob becomes a
84218
+ * wildcard: `^rm -rf /tmp/a*$` (a run of `a`s) would turn into the glob `rm -rf /tmp/a*`, which
84219
+ * matches `rm -rf /tmp/a /home`. So a regex is unsafe when it has an escaped literal `\*` / `\?` /
84220
+ * `\[` (a `[` would open a character class in targets whose globs support one),
84221
+ * or a bare quantifier `*` / `?` / `+` / `{` that is not part of a `.*` run (which the glob `*`
84222
+ * reproduces faithfully).
84223
+ */
84224
+ function canApproximateShellRegexWithoutWidening(regex) {
84225
+ let i = 0;
84226
+ while (i < regex.length) {
84227
+ const ch = regex[i];
84228
+ if (ch === "\\" && i + 1 < regex.length) {
84229
+ if (isGlobSpecial(regex[i + 1])) return false;
84230
+ i += 2;
84231
+ continue;
84232
+ }
84233
+ if (ch === "." && regex[i + 1] === "*") {
84234
+ i += 2;
84235
+ continue;
84236
+ }
84237
+ if (ch === "*" || ch === "?" || ch === "+" || ch === "{") return false;
84238
+ i += 1;
84239
+ }
84240
+ return true;
84241
+ }
84031
84242
  const MANAGED_AUGMENT_TOOL_NAMES = new Set(Object.values(CANONICAL_TO_AUGMENT_TOOL_NAMES));
84032
84243
  var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermissions {
84033
84244
  constructor(params) {
@@ -84464,12 +84675,18 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
84464
84675
  if (legacyToolName === "launch-process" && entry.shellInputRegex) {
84465
84676
  const regex = entry.shellInputRegex;
84466
84677
  if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
84467
- else if (action === "deny") {
84468
- logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as the catch-all '*' pattern (fail-closed) so the deny rule cannot be silently narrowed on regenerate.`);
84469
- pattern = "*";
84470
- } else {
84471
- pattern = shellRegexToGlob(regex);
84472
- logger?.warn(`AugmentCode permissions: shellInputRegex '${regex}' on tool '${entry.toolName}' is not faithfully roundtrippable to a glob. Importing as glob '${pattern}'; the rule may match a different set of inputs after regenerate.`);
84678
+ else {
84679
+ if (action !== "deny" && !canApproximateShellRegexWithoutWidening(regex)) {
84680
+ 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.`);
84681
+ continue;
84682
+ }
84683
+ if (action === "deny") {
84684
+ 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.`);
84685
+ pattern = "*";
84686
+ } else {
84687
+ pattern = shellRegexToGlob(regex);
84688
+ 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.`);
84689
+ }
84473
84690
  }
84474
84691
  } else pattern = "*";
84475
84692
  if (isPrototypePollutionKey(pattern)) {
@@ -85962,15 +86179,19 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
85962
86179
  * `env` variables a project or local settings file cannot set, because a
85963
86180
  * checked-out repository should not control them: where Claude Code stores its
85964
86181
  * own files, what session content it exports, where telemetry goes, how it
85965
- * starts or syncs, the timers on an unanswered dialog, and whether it processes
85966
- * attachments or expands `@` file mentions, and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
86182
+ * starts or syncs, which claude.ai sign-in it uses, the timers on an unanswered
86183
+ * dialog, and whether it processes attachments or expands `@` file mentions,
86184
+ * and the screen reader (`CLAUDE_AX_*`) variables. Claude Code drops them there, so they are skipped at project scope and emitted only under `--global`.
85967
86185
  *
85968
- * Only the names the settings reference lists are covered; its directory and
85969
- * Windows groups are introduced with "such as", so an unlisted member of those
86186
+ * Only the names the settings reference lists are covered; its directory,
86187
+ * Windows and sign-in groups are introduced with "such as", so an unlisted member of those
85970
86188
  * families is still written. Windows variable names are case-insensitive, so
85971
- * that group is matched without regard to case.
86189
+ * that group is matched without regard to case. `ALLUSERSPROFILE`,
86190
+ * `SystemDrive` and the `CommonProgramFiles` family are ignored there since
86191
+ * Claude Code v2.1.285.
85972
86192
  *
85973
86193
  * @see https://code.claude.com/docs/en/settings-reference#variables-claude-code-ignores-in-env
86194
+ * @see https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md
85974
86195
  */
85975
86196
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85976
86197
  "BETA_TRACING_ENDPOINT",
@@ -85980,6 +86201,9 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85980
86201
  "CLAUDE_CODE_DISABLE_FILE_MENTIONS",
85981
86202
  "CLAUDE_CODE_ENABLE_TELEMETRY",
85982
86203
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
86204
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
86205
+ "CLAUDE_CODE_OAUTH_SCOPES",
86206
+ "CLAUDE_CODE_OAUTH_TOKEN",
85983
86207
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
85984
86208
  "CLAUDE_CODE_PLUGIN_SEED_DIR",
85985
86209
  "CLAUDE_CODE_PROCESS_WRAPPER",
@@ -86000,14 +86224,23 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
86000
86224
  "TMPDIR"
86001
86225
  ]);
86002
86226
  const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES = /* @__PURE__ */ new Set([
86227
+ "ALLUSERSPROFILE",
86228
+ "COMMONPROGRAMW6432",
86003
86229
  "COMSPEC",
86004
86230
  "LOCALAPPDATA",
86005
86231
  "PATHEXT",
86006
86232
  "PROGRAMDATA",
86007
86233
  "PROGRAMW6432",
86008
86234
  "PSMODULEPATH",
86235
+ "SYSTEMDRIVE",
86009
86236
  "SYSTEMROOT"
86010
86237
  ]);
86238
+ /**
86239
+ * Windows name prefixes matched as whole families: `ProgramFiles*` and
86240
+ * `CommonProgramFiles*`. `ProgramW6432` and `CommonProgramW6432` are exact
86241
+ * names in the set above.
86242
+ */
86243
+ const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES = ["PROGRAMFILES", "COMMONPROGRAMFILES"];
86011
86244
  const CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES = [
86012
86245
  "_ENDPOINT",
86013
86246
  "_HEADERS",
@@ -86052,7 +86285,7 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
86052
86285
  function isProjectIgnoredEnvVariable(name, value) {
86053
86286
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_") || name.startsWith("CLAUDE_AX_")) return true;
86054
86287
  const upper = name.toUpperCase();
86055
- if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || upper.startsWith("PROGRAMFILES")) return true;
86288
+ if (CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES.has(upper) || CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_PREFIXES.some((prefix) => upper.startsWith(prefix))) return true;
86056
86289
  if (name.startsWith("OTEL_EXPORTER_OTLP_") && CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return true;
86057
86290
  if (Object.hasOwn(CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES, name)) {
86058
86291
  const offValues = CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES[name];
@@ -86084,7 +86317,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
86084
86317
  kept[name] = value;
86085
86318
  }
86086
86319
  if (ignoredEverywhere.length > 0) logger?.warn(`Claude Code permissions: Claude Code ignores ${ignoredEverywhere.map((name) => `'env.${name}'`).join(", ")} in every settings file, so ${ignoredEverywhere.length === 1 ? "it is" : "they are"} not written to ${relativeFilePath}. Set ${ignoredEverywhere.length === 1 ? "it" : "them"} in the environment Claude Code starts from instead, and check ${relativeFilePath} for a stale value an earlier generate may have left there.`);
86087
- if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
86320
+ if (ignoredAtProjectScope.length > 0) logger?.warn(`Claude Code permissions: ${ignoredAtProjectScope.map((name) => `'env.${name}'`).join(", ")} ${ignoredAtProjectScope.length === 1 ? "is" : "are"} not honored in the project-scoped ${relativeFilePath}, so ${ignoredAtProjectScope.length === 1 ? "it is" : "they are"} not written there — Claude Code ignores variables there that choose where it writes its files, export session content or telemetry, change how it starts or syncs, set the claude.ai sign-in, set dialog timers, turn attachments or file mentions off, or turn Claude in Chrome on (a value that turns telemetry or Chrome off, such as "none" or "0", is still written). Author ${ignoredAtProjectScope.length === 1 ? "it" : "them"} in the global scope instead, and check that file for a stale value an earlier generate may have left there.`);
86088
86321
  return kept;
86089
86322
  }
86090
86323
  /**
@@ -86734,7 +86967,7 @@ const CODEBUDDY_RULE_LISTS = [
86734
86967
  "ask",
86735
86968
  "deny"
86736
86969
  ];
86737
- const CATCH_ALL_PATTERN$13 = "*";
86970
+ const CATCH_ALL_PATTERN$14 = "*";
86738
86971
  const MCP_CATEGORY = "mcp";
86739
86972
  const MCP_RULE_PREFIX = "mcp__";
86740
86973
  const ALL_MCP_RULE = "mcp__*";
@@ -86804,7 +87037,7 @@ function categoryOfRule$2(rule) {
86804
87037
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
86805
87038
  */
86806
87039
  function buildCodebuddyRule({ category, pattern, action, logger }) {
86807
- const catchAll = pattern === CATCH_ALL_PATTERN$13;
87040
+ const catchAll = pattern === CATCH_ALL_PATTERN$14;
86808
87041
  if (category === MCP_CATEGORY) {
86809
87042
  if (catchAll && action === "allow") {
86810
87043
  logger?.warn(`CodeBuddy permissions: skipping the 'mcp' allow rule '*' — CodeBuddy honors the all-MCP wildcard 'mcp__*' in deny and ask only. Allow servers by name instead (for example 'mcp__github').`);
@@ -86813,7 +87046,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
86813
87046
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
86814
87047
  }
86815
87048
  if (category === "*") {
86816
- if (catchAll) return CATCH_ALL_PATTERN$13;
87049
+ if (catchAll) return CATCH_ALL_PATTERN$14;
86817
87050
  logger?.warn(`CodeBuddy permissions: skipping the all-tools '*' rule '${pattern}' — CodeBuddy's standalone '*' rule takes no specifier. Write it under the category it is meant for (for example 'bash' or 'read').`);
86818
87051
  return null;
86819
87052
  }
@@ -86831,7 +87064,7 @@ function parseCodebuddyRule(rule) {
86831
87064
  const { specifier } = splitCodebuddyRule(rule);
86832
87065
  if (specifier === void 0) return {
86833
87066
  category,
86834
- pattern: CATCH_ALL_PATTERN$13
87067
+ pattern: CATCH_ALL_PATTERN$14
86835
87068
  };
86836
87069
  const trimmed = specifier.trim();
86837
87070
  return trimmed === "" ? null : {
@@ -89157,8 +89390,8 @@ function mapBashActionToDecision(action) {
89157
89390
  // src/features/permissions/commandcode-permissions.ts
89158
89391
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
89159
89392
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
89160
- const CATCH_ALL_PATTERN$12 = "*";
89161
- const MCP_CANONICAL_PREFIX$7 = "mcp__";
89393
+ const CATCH_ALL_PATTERN$13 = "*";
89394
+ const MCP_CANONICAL_PREFIX$8 = "mcp__";
89162
89395
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
89163
89396
  const CATEGORY_TO_COMMANDCODE_TOOL = {
89164
89397
  bash: "Shell",
@@ -89203,10 +89436,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
89203
89436
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
89204
89437
  */
89205
89438
  function buildCommandcodeRule(category, pattern) {
89206
- const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
89207
- if (category.startsWith(MCP_CANONICAL_PREFIX$7)) return catchAll ? category : `${category}(${pattern})`;
89208
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$7}${pattern}`;
89209
- if (category === "*") return catchAll ? CATCH_ALL_PATTERN$12 : null;
89439
+ const catchAll = pattern === CATCH_ALL_PATTERN$13 || pattern === "";
89440
+ if (category.startsWith(MCP_CANONICAL_PREFIX$8)) return catchAll ? category : `${category}(${pattern})`;
89441
+ if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$8}${pattern}`;
89442
+ if (category === "*") return catchAll ? CATCH_ALL_PATTERN$13 : null;
89210
89443
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
89211
89444
  if (tool === void 0) return null;
89212
89445
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -89242,7 +89475,7 @@ function splitCommandcodeRule(rule) {
89242
89475
  }
89243
89476
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
89244
89477
  function isCatchAllSpecifier(inner) {
89245
- return inner === "" || inner === CATCH_ALL_PATTERN$12;
89478
+ return inner === "" || inner === CATCH_ALL_PATTERN$13;
89246
89479
  }
89247
89480
  /**
89248
89481
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -89276,34 +89509,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
89276
89509
  if (server.length === 0) return null;
89277
89510
  const everyServer = {
89278
89511
  category: "mcp",
89279
- pattern: CATCH_ALL_PATTERN$12,
89512
+ pattern: CATCH_ALL_PATTERN$13,
89280
89513
  notInAllow: true
89281
89514
  };
89282
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7)) {
89283
- if (server === CATCH_ALL_PATTERN$12) return everyServer;
89284
- if (server.includes(CATCH_ALL_PATTERN$12)) return null;
89515
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$8)) {
89516
+ if (server === CATCH_ALL_PATTERN$13) return everyServer;
89517
+ if (server.includes(CATCH_ALL_PATTERN$13)) return null;
89285
89518
  return {
89286
- category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$7}${server}` : `${MCP_CANONICAL_PREFIX$7}${remainder}`,
89287
- pattern: CATCH_ALL_PATTERN$12,
89519
+ category: rest === "" || rest === CATCH_ALL_PATTERN$13 ? `${MCP_CANONICAL_PREFIX$8}${server}` : `${MCP_CANONICAL_PREFIX$8}${remainder}`,
89520
+ pattern: CATCH_ALL_PATTERN$13,
89288
89521
  notInAllow: false
89289
89522
  };
89290
89523
  }
89291
- const category = `${MCP_CANONICAL_PREFIX$7}${remainder}`;
89292
- const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$12);
89524
+ const category = `${MCP_CANONICAL_PREFIX$8}${remainder}`;
89525
+ const namesTool = separator !== -1 && rest.length > 0 && !remainder.includes(CATCH_ALL_PATTERN$13);
89293
89526
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
89294
89527
  category,
89295
89528
  pattern: inner,
89296
89529
  notInAllow: true
89297
89530
  } : null;
89298
- if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
89299
- if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
89300
- category: `${MCP_CANONICAL_PREFIX$7}${server}`,
89301
- pattern: CATCH_ALL_PATTERN$12,
89531
+ if (remainder === CATCH_ALL_PATTERN$13) return everyServer;
89532
+ if (rest === CATCH_ALL_PATTERN$13 && !server.includes(CATCH_ALL_PATTERN$13)) return {
89533
+ category: `${MCP_CANONICAL_PREFIX$8}${server}`,
89534
+ pattern: CATCH_ALL_PATTERN$13,
89302
89535
  notInAllow: true
89303
89536
  };
89304
89537
  return namesTool ? {
89305
89538
  category,
89306
- pattern: CATCH_ALL_PATTERN$12,
89539
+ pattern: CATCH_ALL_PATTERN$13,
89307
89540
  notInAllow: false
89308
89541
  } : null;
89309
89542
  }
@@ -89327,14 +89560,14 @@ function friendlyToolRule({ category, inner }) {
89327
89560
  if (category === "bash") {
89328
89561
  if (isCatchAllSpecifier(inner)) return {
89329
89562
  category,
89330
- pattern: CATCH_ALL_PATTERN$12,
89563
+ pattern: CATCH_ALL_PATTERN$13,
89331
89564
  notInAllow: false
89332
89565
  };
89333
89566
  const normalized = inner.trim().replace(/\s+/g, " ");
89334
89567
  if (normalized === "" || normalized === ":*") return null;
89335
89568
  return isCatchAllSpecifier(normalized) ? {
89336
89569
  category,
89337
- pattern: CATCH_ALL_PATTERN$12,
89570
+ pattern: CATCH_ALL_PATTERN$13,
89338
89571
  notInAllow: true
89339
89572
  } : {
89340
89573
  category,
@@ -89344,7 +89577,7 @@ function friendlyToolRule({ category, inner }) {
89344
89577
  }
89345
89578
  if (isCatchAllSpecifier(inner)) return {
89346
89579
  category,
89347
- pattern: CATCH_ALL_PATTERN$12,
89580
+ pattern: CATCH_ALL_PATTERN$13,
89348
89581
  notInAllow: false
89349
89582
  };
89350
89583
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -89363,13 +89596,13 @@ function friendlyToolRule({ category, inner }) {
89363
89596
  */
89364
89597
  function parseCommandcodeRule(rule) {
89365
89598
  const { tool, inner } = splitCommandcodeRule(rule);
89366
- if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
89599
+ if (tool === CATCH_ALL_PATTERN$13) return isCatchAllSpecifier(inner) ? {
89367
89600
  category: "*",
89368
- pattern: CATCH_ALL_PATTERN$12,
89601
+ pattern: CATCH_ALL_PATTERN$13,
89369
89602
  notInAllow: true
89370
89603
  } : null;
89371
89604
  const lowered = tool.toLowerCase();
89372
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$7)) return parseMcpCommandcodeRule({
89605
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$8)) return parseMcpCommandcodeRule({
89373
89606
  tool,
89374
89607
  inner
89375
89608
  });
@@ -89387,12 +89620,12 @@ function parseCommandcodeRule(rule) {
89387
89620
  */
89388
89621
  function isFoldedMcpToolNameRule({ rule, category }) {
89389
89622
  const { tool } = splitCommandcodeRule(rule);
89390
- return category.startsWith(MCP_CANONICAL_PREFIX$7) && !tool.startsWith(MCP_CANONICAL_PREFIX$7);
89623
+ return category.startsWith(MCP_CANONICAL_PREFIX$8) && !tool.startsWith(MCP_CANONICAL_PREFIX$8);
89391
89624
  }
89392
89625
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
89393
89626
  function scopedMcpRuleTool(rule) {
89394
89627
  const { tool, inner } = splitCommandcodeRule(rule);
89395
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$7) ? tool : null;
89628
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$8) ? tool : null;
89396
89629
  }
89397
89630
  /**
89398
89631
  * The string entries of a Command Code permission list. Command Code reads
@@ -89435,7 +89668,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
89435
89668
  }).filter((rule) => {
89436
89669
  const parsed = parseCommandcodeRule(rule);
89437
89670
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
89438
- const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$12})`);
89671
+ const replacement = written.get(`${parsed.category}(${parsed.pattern})`) ?? written.get(`${parsed.category}(${CATCH_ALL_PATTERN$13})`);
89439
89672
  if (key !== "allow" && isStricterAction({
89440
89673
  action: key,
89441
89674
  existing: replacement
@@ -89492,7 +89725,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89492
89725
  const ranked = /* @__PURE__ */ new Map();
89493
89726
  const managedCategories = /* @__PURE__ */ new Set();
89494
89727
  for (const [category, rules] of Object.entries(permission)) {
89495
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
89728
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$13);
89496
89729
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
89497
89730
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
89498
89731
  for (const [pattern, action] of Object.entries(rules)) {
@@ -89561,7 +89794,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89561
89794
  function skippedAllowImportMessage({ rule, category }) {
89562
89795
  const { tool, inner } = splitCommandcodeRule(rule);
89563
89796
  if (category === "bash") return `Command Code reads '${rule}' in "allow" as ${PADDED_SHELL_WILDCARD_REASON}, which rulesync cannot import without widening the grant to every command, so it was not imported.`;
89564
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
89797
+ if (tool.startsWith(MCP_CANONICAL_PREFIX$8) || isCatchAllSpecifier(inner)) return `Command Code ignores '${rule}' in "allow" (an allow rule must name what it grants), so it was not imported.`;
89565
89798
  return `Command Code globs the specifier of '${rule}' in "allow" against the call's arguments, which rulesync cannot import without widening the grant to the whole tool, so it was not imported.`;
89566
89799
  }
89567
89800
  /**
@@ -89756,7 +89989,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
89756
89989
  }
89757
89990
  // src/features/permissions/continue-permissions.ts
89758
89991
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
89759
- const CATCH_ALL_PATTERN$11 = "*";
89992
+ const CATCH_ALL_PATTERN$12 = "*";
89760
89993
  /**
89761
89994
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
89762
89995
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -89764,7 +89997,7 @@ const CATCH_ALL_PATTERN$11 = "*";
89764
89997
  * the bare entry instead of being skipped as a pattern-specific rule.
89765
89998
  */
89766
89999
  function normalizeCatchAllPattern$1(pattern) {
89767
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90000
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$12 : pattern;
89768
90001
  }
89769
90002
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
89770
90003
  bash: "Bash",
@@ -89808,7 +90041,7 @@ function parseContinuePermissionEntry(entry) {
89808
90041
  if (toolName === "") return;
89809
90042
  return {
89810
90043
  toolName,
89811
- pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
90044
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$12 : pattern
89812
90045
  };
89813
90046
  }
89814
90047
  /**
@@ -89973,7 +90206,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
89973
90206
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89974
90207
  continue;
89975
90208
  }
89976
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
90209
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$12) {
89977
90210
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89978
90211
  continue;
89979
90212
  }
@@ -90029,13 +90262,116 @@ const AUTO_APPROVE_KEYS = {
90029
90262
  webfetch: "chat.tools.urls.autoApprove"
90030
90263
  };
90031
90264
  /**
90032
- * The workspace setting holding the starting permission level of new chat
90033
- * sessions, authored through the `copilot.defaultPermissionMode` override.
90265
+ * The workspace setting holding the starting permission level of new Local
90266
+ * (`local` harness) chat sessions, authored through the
90267
+ * `copilot.defaultPermissionMode` override. VS Code 1.144 narrowed it to Local
90268
+ * sessions; the Copilot harness reads {@link DEFAULT_CONFIGURATION_KEY}.
90034
90269
  *
90035
90270
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
90036
90271
  */
90037
90272
  const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
90038
90273
  /**
90274
+ * The workspace setting holding the starting `mode` (`interactive` / `plan` /
90275
+ * `autopilot`) and `approvals` (`manual` / `assisted` / `allowAll`) of new
90276
+ * agent sessions such as the Copilot harness, the default since VS Code 1.143.
90277
+ * It shipped in stable VS Code 1.144 as an object setting with
90278
+ * `additionalProperties: false`; older releases ignore it.
90279
+ *
90280
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/browser/chat.shared.contribution.ts
90281
+ * @see https://github.com/microsoft/vscode/blob/1.144.0/src/vs/workbench/contrib/chat/common/constants.ts
90282
+ */
90283
+ const DEFAULT_CONFIGURATION_KEY = "chat.defaultConfiguration";
90284
+ /**
90285
+ * What each `copilot.defaultPermissionMode` value writes. `local` goes to
90286
+ * `chat.permissions.default`; `approvals` and `autopilot` go to
90287
+ * `chat.defaultConfiguration`. The Local picker has no Assisted level, so
90288
+ * `assisted` starts Local sessions with Default Permissions — the stricter
90289
+ * fallback. Autopilot is a permission level in the Local picker but a `mode`
90290
+ * in `chat.defaultConfiguration`, where it runs with every tool call approved.
90291
+ */
90292
+ const DEFAULT_PERMISSION_MODE_MAPPINGS = {
90293
+ default: {
90294
+ local: "default",
90295
+ approvals: "manual",
90296
+ autopilot: false
90297
+ },
90298
+ autoApprove: {
90299
+ local: "autoApprove",
90300
+ approvals: "allowAll",
90301
+ autopilot: false
90302
+ },
90303
+ autopilot: {
90304
+ local: "autopilot",
90305
+ approvals: "allowAll",
90306
+ autopilot: true
90307
+ },
90308
+ assisted: {
90309
+ local: "default",
90310
+ approvals: "assisted",
90311
+ autopilot: false
90312
+ }
90313
+ };
90314
+ /**
90315
+ * The `chat.defaultConfiguration` value for `mode`. Rulesync owns only whether
90316
+ * the mode is Autopilot: authoring Autopilot sets it, and any other value
90317
+ * removes any Autopilot mode in the file (whether an earlier run or a person
90318
+ * wrote it, since the two cannot be told apart), while an `interactive` or
90319
+ * `plan` mode is kept.
90320
+ */
90321
+ function buildDefaultConfigurationValue({ defaultPermissionMode, existingSettings }) {
90322
+ const { approvals, autopilot } = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode];
90323
+ const existing = existingSettings[DEFAULT_CONFIGURATION_KEY];
90324
+ const value = { ...isPlainObject(existing) ? existing : {} };
90325
+ if (autopilot) value.mode = "autopilot";
90326
+ else if (value.mode === "autopilot") delete value.mode;
90327
+ value.approvals = approvals;
90328
+ return value;
90329
+ }
90330
+ /**
90331
+ * The `copilot.defaultPermissionMode` values from least to most permissive,
90332
+ * used to settle a file whose two keys disagree.
90333
+ */
90334
+ const DEFAULT_PERMISSION_MODE_RANK = {
90335
+ default: 0,
90336
+ assisted: 1,
90337
+ autoApprove: 2,
90338
+ autopilot: 3
90339
+ };
90340
+ /**
90341
+ * The `copilot.defaultPermissionMode` a `chat.defaultConfiguration` value
90342
+ * stands for. Autopilot is recognized only together with `allowAll`, the pair
90343
+ * Rulesync writes for it: an Autopilot mode with stricter approvals is read
90344
+ * from `approvals` alone, so that regenerating never escalates it to Allow
90345
+ * All. An object without `approvals` is read as `default`, since VS Code then
90346
+ * starts sessions with Manual permissions.
90347
+ */
90348
+ function readDefaultConfigurationMode(configuration) {
90349
+ if (!isPlainObject(configuration)) return;
90350
+ switch (configuration.approvals) {
90351
+ case void 0:
90352
+ case "manual": return "default";
90353
+ case "assisted": return "assisted";
90354
+ case "allowAll": return configuration.mode === "autopilot" ? "autopilot" : "autoApprove";
90355
+ default: return;
90356
+ }
90357
+ }
90358
+ /**
90359
+ * Read `copilot.defaultPermissionMode` back from the settings. When the two
90360
+ * keys agree — `chat.permissions.default` holds the Local level Rulesync
90361
+ * writes for the `chat.defaultConfiguration` value — the latter is returned,
90362
+ * since only it can say `assisted`. When they disagree, the less permissive of
90363
+ * the two is returned, so regenerating never raises either harness's starting
90364
+ * permissions. Either key alone is read as it stands.
90365
+ */
90366
+ function readDefaultPermissionMode(settings) {
90367
+ const fromConfiguration = readDefaultConfigurationMode(settings[DEFAULT_CONFIGURATION_KEY]);
90368
+ const localValue = settings[DEFAULT_PERMISSION_MODE_KEY$1];
90369
+ const fromLocal = COPILOT_LOCAL_PERMISSION_LEVELS.find((level) => level === localValue);
90370
+ if (fromConfiguration === void 0 || fromLocal === void 0) return fromConfiguration ?? fromLocal;
90371
+ if (DEFAULT_PERMISSION_MODE_MAPPINGS[fromConfiguration].local === fromLocal) return fromConfiguration;
90372
+ return DEFAULT_PERMISSION_MODE_RANK[fromLocal] < DEFAULT_PERMISSION_MODE_RANK[fromConfiguration] ? fromLocal : fromConfiguration;
90373
+ }
90374
+ /**
90039
90375
  * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
90040
90376
  * at path `a.b` is written to `chat.agent.sandbox.a.b`.
90041
90377
  */
@@ -90222,9 +90558,6 @@ function readSandboxFromSettings(settings) {
90222
90558
  }
90223
90559
  return Object.keys(sandbox).length > 0 ? sandbox : void 0;
90224
90560
  }
90225
- function isCopilotDefaultPermissionMode(value) {
90226
- return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
90227
- }
90228
90561
  function asAutoApproveMap(value) {
90229
90562
  if (!isPlainObject(value)) return {};
90230
90563
  const result = {};
@@ -90268,9 +90601,11 @@ function buildAutoApproveValue({ category, rules }) {
90268
90601
  * `false`. A key whose canonical category is absent entirely
90269
90602
  * is left untouched, so authoring only `bash` rules never disturbs a
90270
90603
  * hand-written edits or urls map.
90271
- * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
90272
- * (the starting permission level of new chat sessions); without it the key is
90273
- * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
90604
+ * The `copilot.defaultPermissionMode` override writes both
90605
+ * `chat.permissions.default` (Local sessions) and `chat.defaultConfiguration`
90606
+ * (Copilot-harness sessions, VS Code 1.144+; see
90607
+ * {@link DEFAULT_PERMISSION_MODE_MAPPINGS}); without it both keys are left
90608
+ * untouched. The `copilot.sandbox` override writes the Agent Host sandbox
90274
90609
  * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
90275
90610
  * warns about every value it writes that loosens the sandbox.
90276
90611
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
@@ -90321,17 +90656,24 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90321
90656
  rules
90322
90657
  });
90323
90658
  }
90659
+ const existingSettings = () => parseSharedConfig({
90660
+ format: "jsonc",
90661
+ fileContent: existingContent,
90662
+ filePath
90663
+ });
90324
90664
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
90325
- if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY$1] = defaultPermissionMode;
90665
+ if (defaultPermissionMode !== void 0) {
90666
+ patch[DEFAULT_PERMISSION_MODE_KEY$1] = DEFAULT_PERMISSION_MODE_MAPPINGS[defaultPermissionMode].local;
90667
+ patch[DEFAULT_CONFIGURATION_KEY] = buildDefaultConfigurationValue({
90668
+ defaultPermissionMode,
90669
+ existingSettings: existingSettings()
90670
+ });
90671
+ }
90326
90672
  const sandbox = config.copilot?.sandbox;
90327
90673
  if (sandbox !== void 0) {
90328
90674
  Object.assign(patch, buildSandboxPatch({
90329
90675
  sandbox,
90330
- existingSettings: parseSharedConfig({
90331
- format: "jsonc",
90332
- fileContent: existingContent,
90333
- filePath
90334
- })
90676
+ existingSettings: existingSettings()
90335
90677
  }));
90336
90678
  warnOnTrustAffectingEntries({
90337
90679
  toolLabel: COPILOT_TOOL_LABEL,
@@ -90378,10 +90720,10 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90378
90720
  for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
90379
90721
  if (Object.keys(rules).length > 0) permission[category] = rules;
90380
90722
  }
90381
- const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY$1];
90723
+ const defaultPermissionMode = readDefaultPermissionMode(settings);
90382
90724
  const sandbox = readSandboxFromSettings(settings);
90383
90725
  const copilot = {
90384
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
90726
+ ...defaultPermissionMode !== void 0 && { defaultPermissionMode },
90385
90727
  ...sandbox !== void 0 && { sandbox }
90386
90728
  };
90387
90729
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -90579,14 +90921,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
90579
90921
  }
90580
90922
  };
90581
90923
  // src/features/permissions/crush-permissions.ts
90582
- const CATCH_ALL_PATTERN$10 = "*";
90924
+ const CATCH_ALL_PATTERN$11 = "*";
90583
90925
  /**
90584
90926
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
90585
90927
  * argument patterns at all, so it is the only pattern a rule can carry and
90586
90928
  * still be written.
90587
90929
  */
90588
90930
  function normalizeCatchAllPattern(pattern) {
90589
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
90931
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90590
90932
  }
90591
90933
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90592
90934
  bash: "bash",
@@ -90598,11 +90940,11 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90598
90940
  webfetch: "fetch"
90599
90941
  };
90600
90942
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
90601
- const MCP_CANONICAL_PREFIX$6 = "mcp__";
90943
+ const MCP_CANONICAL_PREFIX$7 = "mcp__";
90602
90944
  const MCP_CRUSH_PREFIX = "mcp_";
90603
90945
  function toCrushToolName(canonical) {
90604
90946
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
90605
- if (canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
90947
+ if (canonical.startsWith(MCP_CANONICAL_PREFIX$7) && canonical.length > 5) return `${MCP_CRUSH_PREFIX}${canonical.slice(5).replaceAll("__", "_")}`;
90606
90948
  return canonical;
90607
90949
  }
90608
90950
  function toCanonicalToolName$5(crushName) {
@@ -90776,7 +91118,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
90776
91118
  for (const [rawPattern, action] of Object.entries(rules)) {
90777
91119
  if (isPrototypePollutionKey(rawPattern)) continue;
90778
91120
  const pattern = normalizeCatchAllPattern(rawPattern);
90779
- if (pattern === CATCH_ALL_PATTERN$10) {
91121
+ if (pattern === CATCH_ALL_PATTERN$11) {
90780
91122
  catchAllAction = action;
90781
91123
  continue;
90782
91124
  }
@@ -90849,11 +91191,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
90849
91191
  };
90850
91192
  for (const entry of disabled) {
90851
91193
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
90852
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
91194
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] = "deny";
90853
91195
  }
90854
91196
  for (const entry of allowed) {
90855
91197
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
90856
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
91198
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] ??= "allow";
90857
91199
  }
90858
91200
  return { permission };
90859
91201
  }
@@ -90899,13 +91241,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
90899
91241
  WebFetch: "webfetch",
90900
91242
  Mcp: "mcp"
90901
91243
  };
90902
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
91244
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
90903
91245
  /**
90904
91246
  * Returns true if the canonical category is the per-tool MCP form
90905
91247
  * `mcp__<server>__<tool>`.
90906
91248
  */
90907
91249
  function isMcpScopedCategory(canonical) {
90908
- return canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5;
91250
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5;
90909
91251
  }
90910
91252
  function toCursorType(canonical) {
90911
91253
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -90939,7 +91281,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
90939
91281
  if (match) {
90940
91282
  const server = match[1] ?? "*";
90941
91283
  const tool = match[2] ?? "*";
90942
- return `${MCP_CANONICAL_PREFIX$5}${server}__${tool}`;
91284
+ return `${MCP_CANONICAL_PREFIX$6}${server}__${tool}`;
90943
91285
  }
90944
91286
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
90945
91287
  }
@@ -91201,7 +91543,7 @@ function convertCursorToRulesyncPermissions(params) {
91201
91543
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
91202
91544
  const canonical = toCanonicalCategory$1(type, pattern);
91203
91545
  if (!permission[canonical]) permission[canonical] = {};
91204
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$5) ? "*" : pattern;
91546
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$6) ? "*" : pattern;
91205
91547
  permission[canonical][canonicalPattern] = action;
91206
91548
  }
91207
91549
  };
@@ -92700,7 +93042,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
92700
93042
  // src/features/permissions/goose-permissions.ts
92701
93043
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
92702
93044
  const GOOSE_USER_KEY = "user";
92703
- const CATCH_ALL_PATTERN$9 = "*";
93045
+ const CATCH_ALL_PATTERN$10 = "*";
92704
93046
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
92705
93047
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
92706
93048
  shell: ["bash"],
@@ -92849,7 +93191,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
92849
93191
  const assigned = /* @__PURE__ */ new Map();
92850
93192
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
92851
93193
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
92852
- for (const [pattern, action] of Object.entries(rules)) if (pattern !== CATCH_ALL_PATTERN$9) logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
93194
+ for (const [pattern, action] of Object.entries(rules)) if (pattern !== CATCH_ALL_PATTERN$10) logger?.warn(`Goose permission.yaml lists whole tool names, so the per-pattern "${action}" rule for "${category}" (pattern "${pattern}") cannot be expressed and was skipped.`);
92853
93195
  const { action, downgraded } = failSafeCatchAllAction({
92854
93196
  rules,
92855
93197
  allToolsRules: category === "*" ? void 0 : config.permission["*"]
@@ -92882,7 +93224,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
92882
93224
  const action = GOOSE_LIST_TO_ACTION[key];
92883
93225
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
92884
93226
  permission[category] ??= {};
92885
- permission[category][CATCH_ALL_PATTERN$9] = action;
93227
+ permission[category][CATCH_ALL_PATTERN$10] = action;
92886
93228
  }
92887
93229
  }
92888
93230
  return { permission };
@@ -92892,8 +93234,8 @@ const GROKCLI_UI_KEY = "ui";
92892
93234
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
92893
93235
  const GROKCLI_PERMISSION_KEY = "permission";
92894
93236
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
92895
- const CATCH_ALL_PATTERN$8 = "*";
92896
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
93237
+ const CATCH_ALL_PATTERN$9 = "*";
93238
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
92897
93239
  const CATEGORY_TO_GROK_TOOL = {
92898
93240
  bash: "Bash",
92899
93241
  read: "Read",
@@ -92925,6 +93267,25 @@ const GROK_TOOL_TO_CATEGORY_LOWER = {
92925
93267
  };
92926
93268
  const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
92927
93269
  const GROKCLI_PERMISSION_RULES_KEY = "rules";
93270
+ const GROK_PATH_PATTERN_CATEGORIES = /* @__PURE__ */ new Set([
93271
+ "read",
93272
+ "edit",
93273
+ "write",
93274
+ "grep",
93275
+ "glob"
93276
+ ]);
93277
+ const CLAUDE_ABSOLUTE_PATH_PREFIX = "//";
93278
+ /**
93279
+ * Rewrite a canonical path pattern into the anchor Grok understands. A
93280
+ * Claude-style `//abs` absolute pattern becomes `/abs`; without this the rule
93281
+ * would match nothing on Grok, so a `deny` or `ask` rule would silently give
93282
+ * no protection. Every other pattern is returned unchanged.
93283
+ */
93284
+ function toGrokPathPattern(category, pattern) {
93285
+ if (!GROK_PATH_PATTERN_CATEGORIES.has(category)) return pattern;
93286
+ if (!pattern.startsWith(CLAUDE_ABSOLUTE_PATH_PREFIX)) return pattern;
93287
+ return `/${pattern.replace(/^\/+/, "")}`;
93288
+ }
92928
93289
  /**
92929
93290
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
92930
93291
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -92935,18 +93296,20 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
92935
93296
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
92936
93297
  * concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
92937
93298
  * Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
92938
- * under it has no Grok spelling and returns `null`.
93299
+ * under it has no Grok spelling and returns `null`. A Claude-style `//abs`
93300
+ * path pattern on a file tool is rewritten to Grok's `/abs` (see
93301
+ * `toGrokPathPattern`).
92939
93302
  */
92940
93303
  function buildGrokEntry(category, pattern) {
92941
- if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
92942
- if (category.startsWith(MCP_CANONICAL_PREFIX$4)) {
93304
+ if (category === "*") return pattern === CATCH_ALL_PATTERN$9 ? GROK_ALL_TOOLS_ENTRY : null;
93305
+ if (category.startsWith(MCP_CANONICAL_PREFIX$5)) {
92943
93306
  const remainder = category.slice(5);
92944
93307
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
92945
93308
  }
92946
- if (category === "mcp") return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
93309
+ if (category === "mcp") return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? GROK_MCP_TOOL : `${GROK_MCP_TOOL}(${pattern})`;
92947
93310
  const tool = CATEGORY_TO_GROK_TOOL[category];
92948
93311
  if (tool === void 0) return null;
92949
- return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
93312
+ return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
92950
93313
  }
92951
93314
  /**
92952
93315
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -92968,21 +93331,21 @@ function parseGrokEntry(entry) {
92968
93331
  }
92969
93332
  if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
92970
93333
  category: "*",
92971
- pattern: CATCH_ALL_PATTERN$8
93334
+ pattern: CATCH_ALL_PATTERN$9
92972
93335
  };
92973
- if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$4)) return parseClaudeStyleMcpEntry(tool);
93336
+ if (parenIndex === -1 && tool.startsWith(MCP_CANONICAL_PREFIX$5)) return parseClaudeStyleMcpEntry(tool);
92974
93337
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
92975
- category: `${MCP_CANONICAL_PREFIX$4}${inner}`,
92976
- pattern: CATCH_ALL_PATTERN$8
93338
+ category: `${MCP_CANONICAL_PREFIX$5}${inner}`,
93339
+ pattern: CATCH_ALL_PATTERN$9
92977
93340
  } : {
92978
93341
  category: "mcp",
92979
- pattern: CATCH_ALL_PATTERN$8
93342
+ pattern: CATCH_ALL_PATTERN$9
92980
93343
  };
92981
93344
  const category = GROK_TOOL_TO_CATEGORY[tool];
92982
93345
  if (category === void 0) return null;
92983
93346
  return {
92984
93347
  category,
92985
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
93348
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$9
92986
93349
  };
92987
93350
  }
92988
93351
  /**
@@ -92999,14 +93362,14 @@ function parseGrokEntry(entry) {
92999
93362
  function parseClaudeStyleMcpEntry(entry) {
93000
93363
  const rest = entry.slice(5);
93001
93364
  if (rest.length === 0) return null;
93002
- if (rest === CATCH_ALL_PATTERN$8) return {
93365
+ if (rest === CATCH_ALL_PATTERN$9) return {
93003
93366
  category: "mcp",
93004
- pattern: CATCH_ALL_PATTERN$8
93367
+ pattern: CATCH_ALL_PATTERN$9
93005
93368
  };
93006
93369
  const address = rest.includes("__") ? rest : `${rest}__*`;
93007
93370
  return {
93008
- category: `${MCP_CANONICAL_PREFIX$4}${address}`,
93009
- pattern: CATCH_ALL_PATTERN$8
93371
+ category: `${MCP_CANONICAL_PREFIX$5}${address}`,
93372
+ pattern: CATCH_ALL_PATTERN$9
93010
93373
  };
93011
93374
  }
93012
93375
  /**
@@ -93027,20 +93390,20 @@ function parseGrokRule(rule) {
93027
93390
  if (typeof tool !== "string") return null;
93028
93391
  if (pattern !== void 0 && typeof pattern !== "string") return null;
93029
93392
  const trimmedPattern = pattern?.trim() ?? "";
93030
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
93393
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$9;
93031
93394
  const lowerTool = tool.trim().toLowerCase();
93032
- if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
93395
+ if (lowerTool === GROK_VERBOSE_ANY_TOOL) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93033
93396
  category: "*",
93034
- pattern: CATCH_ALL_PATTERN$8,
93397
+ pattern: CATCH_ALL_PATTERN$9,
93035
93398
  action
93036
93399
  } : null;
93037
- if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$8 ? {
93400
+ if (GROK_MCP_TOOL_ALIASES.has(lowerTool)) return resolvedPattern === CATCH_ALL_PATTERN$9 ? {
93038
93401
  category: "mcp",
93039
- pattern: CATCH_ALL_PATTERN$8,
93402
+ pattern: CATCH_ALL_PATTERN$9,
93040
93403
  action
93041
93404
  } : {
93042
- category: `${MCP_CANONICAL_PREFIX$4}${resolvedPattern}`,
93043
- pattern: CATCH_ALL_PATTERN$8,
93405
+ category: `${MCP_CANONICAL_PREFIX$5}${resolvedPattern}`,
93406
+ pattern: CATCH_ALL_PATTERN$9,
93044
93407
  action
93045
93408
  };
93046
93409
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -93190,7 +93553,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
93190
93553
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
93191
93554
  }
93192
93555
  const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
93193
- const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$8]: legacyModeAction(parsed) } } };
93556
+ const rulesyncConfig = fineGrained ? { permission: fineGrained } : { permission: { bash: { [CATCH_ALL_PATTERN$9]: legacyModeAction(parsed) } } };
93194
93557
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
93195
93558
  }
93196
93559
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
@@ -93375,6 +93738,195 @@ function ensureCategory(permission, category) {
93375
93738
  function removeEmptyCategories(permission) {
93376
93739
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
93377
93740
  }
93741
+ /** The prefix Hermes's MCP trust gate keys its persisted "Always" answers with. */
93742
+ const MCP_ALLOWLIST_PREFIX = "mcp:";
93743
+ const MCP_CANONICAL_PREFIX$4 = "mcp__";
93744
+ const CATCH_ALL_PATTERN$8 = "*";
93745
+ /**
93746
+ * Whether a `command_allowlist` entry is an MCP trust-gate grant rather than a
93747
+ * shell-command pattern. Since Hermes Agent v0.22.6, answering **Always** to
93748
+ * the approval prompt of a `trust: untrusted` MCP server appends
93749
+ * `mcp:<server>/<tool>` to `command_allowlist` (`_trust_approval_key` in
93750
+ * `tools/mcp_tool_handlers.py`), and the gate looks the key up by exact set
93751
+ * membership, so no `mcp:` entry is ever a command.
93752
+ */
93753
+ function isMcpAllowlistEntry(entry) {
93754
+ return entry.startsWith(MCP_ALLOWLIST_PREFIX);
93755
+ }
93756
+ /**
93757
+ * The canonical category (`mcp__<server>__<tool>`) a Hermes `mcp:<server>/<tool>`
93758
+ * grant stands for, or `null` when it has none: a malformed entry, a server
93759
+ * name containing `__` (the canonical name would read back as another server),
93760
+ * or a `*` in either name (Hermes compares the key literally, while a canonical
93761
+ * `*` is a wildcard).
93762
+ */
93763
+ function mcpAllowlistEntryToCategory(entry) {
93764
+ const parsed = parseMcpAllowlistEntry(entry);
93765
+ if (parsed === null) return null;
93766
+ const { server, tool } = parsed;
93767
+ if (server.includes("__") || server.includes(CATCH_ALL_PATTERN$8)) return null;
93768
+ if (tool.includes(CATCH_ALL_PATTERN$8)) return null;
93769
+ return `${MCP_CANONICAL_PREFIX$4}${server}__${tool}`;
93770
+ }
93771
+ /** The server and tool an `mcp:<server>/<tool>` entry names, or `null` when it is malformed. */
93772
+ function parseMcpAllowlistEntry(entry) {
93773
+ if (!isMcpAllowlistEntry(entry)) return null;
93774
+ const rest = entry.slice(4);
93775
+ const slash = rest.indexOf("/");
93776
+ const server = slash === -1 ? "" : rest.slice(0, slash);
93777
+ const tool = slash === -1 ? "" : rest.slice(slash + 1);
93778
+ return server.length === 0 || tool.length === 0 ? null : {
93779
+ server,
93780
+ tool
93781
+ };
93782
+ }
93783
+ /**
93784
+ * The server and tool a canonical `mcp__<server>__<tool>` category names, or
93785
+ * `null` when it names no single tool (a whole server, a wildcard, or the bare
93786
+ * `mcp` category). Only such a category can become one Hermes grant.
93787
+ */
93788
+ function parseMcpToolCategory(category) {
93789
+ if (!category.startsWith(MCP_CANONICAL_PREFIX$4)) return null;
93790
+ const remainder = category.slice(5);
93791
+ const separator = remainder.indexOf("__");
93792
+ if (separator <= 0) return null;
93793
+ const server = remainder.slice(0, separator);
93794
+ const tool = remainder.slice(separator + 2);
93795
+ if (tool.length === 0 || server.includes("/") || server.includes(CATCH_ALL_PATTERN$8) || tool.includes(CATCH_ALL_PATTERN$8)) return null;
93796
+ return {
93797
+ server,
93798
+ tool
93799
+ };
93800
+ }
93801
+ function isMcpCategory$1(category) {
93802
+ return category === "mcp" || category.startsWith(MCP_CANONICAL_PREFIX$4);
93803
+ }
93804
+ function isRestricting(action) {
93805
+ return action === "deny" || action === "ask";
93806
+ }
93807
+ /** The first `deny` or `ask` rule of a category, as `<category>: <pattern> (<action>)`. */
93808
+ function firstRestrictingRule(category, rules, covers = () => true) {
93809
+ const found = Object.entries(rules ?? {}).find(([pattern, action]) => isRestricting(action) && covers(pattern));
93810
+ return found ? `${category}: ${found[0]} (${found[1]})` : null;
93811
+ }
93812
+ /**
93813
+ * Whether an MCP name glob (the remainder of an `mcp__...` category, or a
93814
+ * pattern of the bare `mcp` category) covers `server`'s tool `tool`: it names
93815
+ * the tool (`github__delete_repo`, `github__*`, `*__delete_repo`, `*`) or the
93816
+ * whole server (`github`).
93817
+ */
93818
+ function mcpNameGlobCovers(glob, server, tool) {
93819
+ return glob === `${server}__${tool}` || glob === server || matchesGlob(glob, `${server}__${tool}`) || matchesGlob(glob, server);
93820
+ }
93821
+ /**
93822
+ * The `deny` or `ask` rule that outranks an allow on `server`'s tool `tool`, or
93823
+ * `null` when none does: any one under the all-tools `*` category (a rule there
93824
+ * applies to every tool, so even a narrower pattern restricts some of the
93825
+ * tool's calls), one under the bare `mcp` category whose pattern covers the
93826
+ * tool, or any one in an `mcp__...` category whose name covers the tool — its
93827
+ * own category included, since a deny or ask scoped to some of its calls still
93828
+ * forbids approving all of them. A Hermes grant approves every call of the
93829
+ * tool and Hermes has no ask tier, so such an allow is withheld, the same way
93830
+ * a `bash` allow a stricter rule covers is.
93831
+ */
93832
+ function findMcpToolRestriction(permission, server, tool) {
93833
+ for (const [category, rules] of Object.entries(permission)) {
93834
+ let found = null;
93835
+ if (category === "*") found = firstRestrictingRule(category, rules);
93836
+ else if (category === "mcp") found = firstRestrictingRule(category, rules, (pattern) => mcpNameGlobCovers(pattern, server, tool));
93837
+ else if (category.startsWith(MCP_CANONICAL_PREFIX$4) && mcpNameGlobCovers(category.slice(5), server, tool)) found = firstRestrictingRule(category, rules);
93838
+ if (found !== null) return found;
93839
+ }
93840
+ return null;
93841
+ }
93842
+ function isMcpToolRestricted(permission, server, tool) {
93843
+ return findMcpToolRestriction(permission, server, tool) !== null;
93844
+ }
93845
+ /**
93846
+ * Translate the canonical MCP `allow` rules into Hermes trust-gate grants: a
93847
+ * `*` allow on `mcp__<server>__<tool>` becomes `mcp:<server>/<tool>`. Every
93848
+ * other MCP allow (a whole server, a wildcard, a narrower pattern) has no
93849
+ * Hermes spelling, and an allow a deny or ask covers is withheld.
93850
+ */
93851
+ function collectMcpAllowlistEntries(permission) {
93852
+ const entries = [];
93853
+ const withheldCategories = [];
93854
+ const unexpressedRules = [];
93855
+ for (const [category, rules] of Object.entries(permission)) {
93856
+ if (!isMcpCategory$1(category)) continue;
93857
+ const parsed = parseMcpToolCategory(category);
93858
+ for (const pattern of patternsByAction(rules, "allow")) if (parsed === null || pattern !== CATCH_ALL_PATTERN$8) unexpressedRules.push(`${category}: ${pattern}`);
93859
+ else {
93860
+ const restriction = findMcpToolRestriction(permission, parsed.server, parsed.tool);
93861
+ if (restriction === null) entries.push(`${MCP_ALLOWLIST_PREFIX}${parsed.server}/${parsed.tool}`);
93862
+ else withheldCategories.push(`${category} (covered by ${restriction})`);
93863
+ }
93864
+ }
93865
+ return {
93866
+ entries,
93867
+ withheldCategories,
93868
+ unexpressedRules
93869
+ };
93870
+ }
93871
+ function warnAboutUnwrittenMcpAllows({ withheldCategories, unexpressedRules, logger }) {
93872
+ 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.`);
93873
+ 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.`);
93874
+ }
93875
+ /**
93876
+ * Make the native `mcp:<server>/<tool>` grants authoritative for the canonical
93877
+ * single-tool MCP allows they can speak for. An allow the generator withheld
93878
+ * because a deny or ask covers it was never written, so its absence
93879
+ * is not a retraction and it is kept, as is every MCP allow with no Hermes
93880
+ * spelling. A grant with no canonical spelling stays only in Hermes, where
93881
+ * generation keeps it (see `setFileContent`).
93882
+ */
93883
+ function reconcileMcpAllowlist({ permission, mcpEntries, logger }) {
93884
+ for (const [category, rules] of Object.entries(permission)) {
93885
+ const parsed = parseMcpToolCategory(category);
93886
+ if (parsed !== null && rules[CATCH_ALL_PATTERN$8] === "allow" && !isMcpToolRestricted(permission, parsed.server, parsed.tool)) delete rules[CATCH_ALL_PATTERN$8];
93887
+ }
93888
+ const keptRestrictions = [];
93889
+ for (const entry of mcpEntries) {
93890
+ const category = mcpAllowlistEntryToCategory(entry);
93891
+ if (category === null) continue;
93892
+ const rules = ensureCategory(permission, category);
93893
+ if (isRestricting(rules[CATCH_ALL_PATTERN$8])) {
93894
+ keptRestrictions.push(entry);
93895
+ continue;
93896
+ }
93897
+ rules[CATCH_ALL_PATTERN$8] = "allow";
93898
+ }
93899
+ 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.`);
93900
+ }
93901
+ /**
93902
+ * The `mcp:` grants in an existing `command_allowlist` that rulesync does not
93903
+ * manage and must keep when it rewrites the list: Hermes appends one whenever
93904
+ * the user answers **Always** on an untrusted MCP server, so dropping it would
93905
+ * silently revoke the grant. A grant is managed — and so retracted with its
93906
+ * canonical rule — when the previous or the new canonical block has a rule
93907
+ * for its `mcp__<server>__<tool>` category, or when the new block restricts
93908
+ * the tool in any way.
93909
+ */
93910
+ function unmanagedMcpAllowlistEntries({ existing, previousPermission, nextPermission, nextAllowlist }) {
93911
+ return (isStringArray(existing.command_allowlist) ? existing.command_allowlist : []).filter((entry) => {
93912
+ if (!isMcpAllowlistEntry(entry) || nextAllowlist.includes(entry)) return false;
93913
+ const parsed = parseMcpAllowlistEntry(entry);
93914
+ if (parsed !== null && isMcpToolRestricted(nextPermission, parsed.server, parsed.tool)) return false;
93915
+ const category = mcpAllowlistEntryToCategory(entry);
93916
+ return category === null || !Object.hasOwn(previousPermission, category) && !Object.hasOwn(nextPermission, category);
93917
+ });
93918
+ }
93919
+ /** The validated canonical `permission` block of a config's `permissions.rulesync` blob. */
93920
+ function canonicalPermissionOf(config) {
93921
+ const parsed = RulesyncPermissionsFileSchema.safeParse({ permission: provenancePermissionOf(config) });
93922
+ return parsed.success ? parsed.data.permission : {};
93923
+ }
93924
+ /** The canonical `permission` block of a config's `permissions.rulesync` blob, if any. */
93925
+ function provenancePermissionOf(config) {
93926
+ const permissionsRoot = isRecord(config.permissions) ? config.permissions : {};
93927
+ const provenance = isRecord(permissionsRoot.rulesync) ? permissionsRoot.rulesync : {};
93928
+ return isRecord(provenance.permission) ? provenance.permission : {};
93929
+ }
93378
93930
  /**
93379
93931
  * Make the native `command_allowlist` authoritative for the `bash` allow rules
93380
93932
  * it can speak for, and for those only. The allowlist is a list of shell-command
@@ -93392,8 +93944,9 @@ function reconcileCommandAllowlist({ permission, commandAllowlist }) {
93392
93944
  writesAllToolsDeny: false
93393
93945
  });
93394
93946
  const withheld = new Set(shadowedAllowPatterns);
93947
+ const isWithheld = (pattern) => withheld.has(pattern) || isMcpAllowlistEntry(pattern);
93395
93948
  const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
93396
- for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !withheld.has(pattern)) delete rules[pattern];
93949
+ for (const [pattern, action] of Object.entries(rules)) if (action === "allow" && !isWithheld(pattern)) delete rules[pattern];
93397
93950
  for (const pattern of commandAllowlist) rules[pattern] = "allow";
93398
93951
  }
93399
93952
  /**
@@ -93481,17 +94034,31 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93481
94034
  return true;
93482
94035
  }
93483
94036
  setFileContent(fileContent) {
94037
+ const patch = parseSharedConfig({
94038
+ format: "yaml",
94039
+ fileContent: this.fileContent
94040
+ });
94041
+ if (isStringArray(patch.command_allowlist)) {
94042
+ const existing = parseSharedConfig({
94043
+ format: "yaml",
94044
+ fileContent
94045
+ });
94046
+ const kept = unmanagedMcpAllowlistEntries({
94047
+ existing,
94048
+ previousPermission: provenancePermissionOf(existing),
94049
+ nextPermission: canonicalPermissionOf(patch),
94050
+ nextAllowlist: patch.command_allowlist
94051
+ });
94052
+ if (kept.length > 0) patch.command_allowlist = [...patch.command_allowlist, ...kept];
94053
+ }
93484
94054
  this.fileContent = applySharedConfigPatch({
93485
94055
  fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
93486
94056
  feature: "permissions",
93487
94057
  existingContent: fileContent,
93488
- patch: parseSharedConfig({
93489
- format: "yaml",
93490
- fileContent: this.fileContent
93491
- })
94058
+ patch
93492
94059
  });
93493
94060
  }
93494
- toRulesyncPermissions() {
94061
+ toRulesyncPermissions({ logger } = {}) {
93495
94062
  const config = parseSharedConfig({
93496
94063
  format: "yaml",
93497
94064
  fileContent: this.getFileContent()
@@ -93503,9 +94070,15 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93503
94070
  }) : rawProvenance);
93504
94071
  const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
93505
94072
  const permission = clonePermissionBlock(provenance.permission);
94073
+ const commandAllowlist = isStringArray(config.command_allowlist) ? config.command_allowlist : [];
93506
94074
  reconcileCommandAllowlist({
93507
94075
  permission,
93508
- commandAllowlist: isStringArray(config.command_allowlist) ? config.command_allowlist : []
94076
+ commandAllowlist: commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry))
94077
+ });
94078
+ reconcileMcpAllowlist({
94079
+ permission,
94080
+ mcpEntries: commandAllowlist.filter(isMcpAllowlistEntry),
94081
+ logger
93509
94082
  });
93510
94083
  const approvals = isRecord(config.approvals) ? config.approvals : {};
93511
94084
  reconcileNativeDenies({
@@ -93563,8 +94136,16 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93563
94136
  logger
93564
94137
  });
93565
94138
  const webfetchDeny = patternsByAction(permissionBlock[WEBFETCH_PERMISSION_CATEGORY], "deny");
94139
+ const mcpAllows = collectMcpAllowlistEntries(permissionBlock);
94140
+ warnAboutUnwrittenMcpAllows({
94141
+ ...mcpAllows,
94142
+ logger
94143
+ });
94144
+ const bashMcpAllows = commandAllowlist.filter(isMcpAllowlistEntry);
94145
+ 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.`);
94146
+ const allowlist = [...commandAllowlist.filter((entry) => !isMcpAllowlistEntry(entry)), ...mcpAllows.entries];
93566
94147
  let config = {};
93567
- if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
94148
+ if (allowlist.length > 0) config.command_allowlist = allowlist;
93568
94149
  if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
93569
94150
  if (webfetchDeny.length > 0) config.security = { website_blocklist: {
93570
94151
  enabled: true,
@@ -100405,6 +100986,7 @@ const TAKT_SECURITY_POLICIES = {
100405
100986
  ],
100406
100987
  workflow_runtime_prepare: ["custom_scripts"],
100407
100988
  workflow_command_gates: ["custom_scripts"],
100989
+ workflow_system_commands: ["custom_scripts"],
100408
100990
  sync_conflict_resolver: ["auto_approve_tools"],
100409
100991
  allow_git_hooks: null,
100410
100992
  allow_git_filters: null