rulesync 43.4.1 → 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",
@@ -22206,7 +22247,7 @@ var OpenCodeCommand = class extends ToolCommand {
22206
22247
  * and `~/.config/mimocode/commands/` (global), so this target reuses
22207
22248
  * {@link OpenCodeCommand} with that layout.
22208
22249
  *
22209
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/config/command.ts
22250
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/config/command.ts
22210
22251
  */
22211
22252
  var MimocodeCommand = class extends OpenCodeCommand {
22212
22253
  static layout = MIMOCODE_LAYOUT;
@@ -30129,7 +30170,7 @@ function normalizeDisableModelInvocation({ frontmatter, skillFilePath }) {
30129
30170
  * `~/.config/mimocode/` (global), so this target reuses {@link OpenCodeSkill}
30130
30171
  * with that layout.
30131
30172
  *
30132
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/skill/index.ts
30173
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/skill/index.ts
30133
30174
  */
30134
30175
  var MimocodeSkill = class extends OpenCodeSkill {
30135
30176
  static layout = MIMOCODE_LAYOUT;
@@ -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({
@@ -38602,7 +38644,7 @@ var OpenCodeSubagent = class extends OpenCodeStyleSubagent {
38602
38644
  * `~/.config/mimocode/agents/` (global), so this target reuses
38603
38645
  * {@link OpenCodeSubagent} with that layout.
38604
38646
  *
38605
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/config/agent.ts
38647
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/config/agent.ts
38606
38648
  */
38607
38649
  var MimocodeSubagent = class extends OpenCodeSubagent {
38608
38650
  static layout = MIMOCODE_LAYOUT;
@@ -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,10 +43068,50 @@ 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. */
43009
43108
  static layout = OPENCODE_LAYOUT;
43109
+ /**
43110
+ * Per-server keys a fork's MCP schema adds on top of OpenCode's. On generate
43111
+ * each one a canonical server carries is copied verbatim onto that server's
43112
+ * local or remote entry, as `timeout` and `oauth` are. OpenCode adds none.
43113
+ */
43114
+ static extraPassthroughServerFields = [];
43010
43115
  json;
43011
43116
  /**
43012
43117
  * With a `logger`, the V2 values that lowering the content drops are warned
@@ -43125,6 +43230,20 @@ var OpencodeMcp = class extends ToolMcp {
43125
43230
  if (containers.length > 0) logger?.warn(`OpenCode MCP: the whole "mcp" key is rewritten in the flat V1 shape, so ${containers.map(quoteValueForWarning).join(" and ")} ${containers.length === 1 ? "is" : "are"} not kept.`);
43126
43231
  }
43127
43232
  const { mcp: convertedMcp, tools: mcpTools } = convertToOpencodeFormat(transformedServers, (existingMcp.success ? existingMcp.data.mcp : void 0) ?? {}, configShape, logger);
43233
+ this.copyExtraPassthroughServerFields({
43234
+ convertedMcp,
43235
+ mcpServers: transformedServers
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
+ });
43128
43247
  if (this.layout.configVersion === "v2") warnAboutOpencodeV2SseServers({
43129
43248
  mcpServers: transformedServers,
43130
43249
  convertedMcp,
@@ -43174,6 +43293,21 @@ var OpencodeMcp = class extends ToolMcp {
43174
43293
  });
43175
43294
  }
43176
43295
  /**
43296
+ * Copies {@link OpencodeMcp.extraPassthroughServerFields} from each canonical
43297
+ * server onto its generated transport entry. A toggle entry is left alone:
43298
+ * it has no transport for these keys to configure.
43299
+ */
43300
+ static copyExtraPassthroughServerFields({ convertedMcp, mcpServers }) {
43301
+ if (this.extraPassthroughServerFields.length === 0) return;
43302
+ for (const [serverName, entry] of Object.entries(convertedMcp)) {
43303
+ const source = Object.hasOwn(mcpServers, serverName) ? mcpServers[serverName] : void 0;
43304
+ if (source === void 0 || !isOpencodeTransportServer(entry)) continue;
43305
+ const sourceRecord = source;
43306
+ const entryRecord = entry;
43307
+ for (const key of this.extraPassthroughServerFields) if (sourceRecord[key] !== void 0) entryRecord[key] = sourceRecord[key];
43308
+ }
43309
+ }
43310
+ /**
43177
43311
  * Builds the V2 `mcp` value: each server raised to the V2 spelling under the
43178
43312
  * `mcp.servers` envelope, keeping what the existing file has that Rulesync
43179
43313
  * cannot express (see {@link raiseOpencodeServerToV2}), plus the existing
@@ -43303,16 +43437,30 @@ var OpencodeMcp = class extends ToolMcp {
43303
43437
  * `.mimocode/mimocode.jsonc` (project) and `~/.config/mimocode/mimocode.jsonc`
43304
43438
  * (global), so this target reuses {@link OpencodeMcp} with that layout.
43305
43439
  *
43306
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/config/mcp.ts
43440
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/config/mcp.ts
43307
43441
  */
43308
43442
  var MimocodeMcp = class extends OpencodeMcp {
43309
43443
  static layout = MIMOCODE_LAYOUT;
43310
43444
  /**
43445
+ * MiMo Code's local and remote server schemas add two keys OpenCode does not
43446
+ * have:
43447
+ * - `sampling` (`"deny"` | `"ask"` | `"allow"`): the policy for the server's
43448
+ * client-side sampling requests (since v0.1.10).
43449
+ * - `alwaysDirectTools` (string array): tools exposed as top-level tools
43450
+ * instead of through `mcp_tool_search` / `mcp_tool_use` (on `main`, not yet
43451
+ * released). Releases without it ignore the key, since MiMo Code only
43452
+ * rejects unknown keys at the top level of the config.
43453
+ *
43454
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/config/mcp.ts
43455
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/v0.1.16/packages/cli/src/config/config.ts
43456
+ */
43457
+ static extraPassthroughServerFields = ["sampling", "alwaysDirectTools"];
43458
+ /**
43311
43459
  * MiMo Code resolves a relative `instructions` entry against the project
43312
43460
  * directory, not the config directory, so a global rule is written as a
43313
43461
  * `~/`-rooted path (which MiMo Code expands to the home directory).
43314
43462
  *
43315
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/session/instruction.ts
43463
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/session/instruction.ts
43316
43464
  */
43317
43465
  static toGlobalInstructionEntry({ path }) {
43318
43466
  return `~/${path}`;
@@ -48682,7 +48830,7 @@ var OpenCodeRule = class extends ToolRule {
48682
48830
  * `instructions` list of its config, so this target reuses {@link OpenCodeRule}
48683
48831
  * with the `.mimocode/` layout.
48684
48832
  *
48685
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/session/instruction.ts
48833
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/session/instruction.ts
48686
48834
  */
48687
48835
  var MimocodeRule = class extends OpenCodeRule {
48688
48836
  static layout = MIMOCODE_LAYOUT;
@@ -51650,6 +51798,7 @@ var RulesProcessor = class extends FeatureProcessor {
51650
51798
  factory
51651
51799
  });
51652
51800
  await this.warnForWarpLegacyRootFile(toolRules);
51801
+ this.warnForAugmentcodePluginRules(toolRules);
51653
51802
  await this.warnForInstructionBudget({
51654
51803
  toolRules,
51655
51804
  meta
@@ -52041,6 +52190,20 @@ var RulesProcessor = class extends FeatureProcessor {
52041
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.`);
52042
52191
  }
52043
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
+ /**
52044
52207
  * Warp still reads a legacy `WARP.md`, and when both `WARP.md` and
52045
52208
  * `AGENTS.md` sit in the same directory, `WARP.md` takes priority — so an
52046
52209
  * `AGENTS.md` this run writes next to one would never be read. That holds for
@@ -63884,7 +64047,7 @@ var OpencodeHooks = class extends ToolHooks {
63884
64047
  * `~/.config/mimocode/plugins/` (global) and keeps OpenCode's plugin event
63885
64048
  * names, so this target reuses {@link OpencodeHooks} with that layout.
63886
64049
  *
63887
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/config/plugin.ts
64050
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/config/plugin.ts
63888
64051
  */
63889
64052
  var MimocodeHooks = class extends OpencodeHooks {
63890
64053
  static layout = MIMOCODE_LAYOUT;
@@ -82283,6 +82446,57 @@ function matchesGlobStep(step, character) {
82283
82446
  const admitted = step.members.has(character) || step.ranges.some(([low, high]) => code >= low && code <= high);
82284
82447
  return step.negated ? !admitted : admitted;
82285
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
+ }
82286
82500
  /** Whether two single-character steps can both match one same character. */
82287
82501
  function stepsShareACharacter(left, right) {
82288
82502
  if (left.kind === "any" || right.kind === "any") return true;
@@ -83819,9 +84033,13 @@ const CANONICAL_TO_AUGMENT_TOOL_NAMES = {
83819
84033
  const AUGMENT_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_AUGMENT_TOOL_NAMES).map(([k, v]) => [v, k]));
83820
84034
  const CURRENT_TO_LEGACY_AUGMENT_TOOL_NAMES = {
83821
84035
  terminal: "launch-process",
84036
+ bash: "launch-process",
84037
+ shell: "launch-process",
83822
84038
  read: "view",
83823
84039
  edit: "str-replace-editor",
83824
- write: "save-file"
84040
+ str_replace_editor: "str-replace-editor",
84041
+ write: "save-file",
84042
+ save_file: "save-file"
83825
84043
  };
83826
84044
  function toAugmentToolName(canonical) {
83827
84045
  return lookupOwn({
@@ -83973,6 +84191,7 @@ function isShellRegexRoundtrippable(regex) {
83973
84191
  while (i < body.length) {
83974
84192
  const ch = body[i];
83975
84193
  if (ch === "\\" && i + 1 < body.length) {
84194
+ if (isGlobSpecial(body[i + 1])) return false;
83976
84195
  i += 2;
83977
84196
  continue;
83978
84197
  }
@@ -83989,6 +84208,37 @@ function isShellRegexRoundtrippable(regex) {
83989
84208
  }
83990
84209
  return true;
83991
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
+ }
83992
84242
  const MANAGED_AUGMENT_TOOL_NAMES = new Set(Object.values(CANONICAL_TO_AUGMENT_TOOL_NAMES));
83993
84243
  var AugmentcodePermissions = class AugmentcodePermissions extends ToolPermissions {
83994
84244
  constructor(params) {
@@ -84425,12 +84675,18 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
84425
84675
  if (legacyToolName === "launch-process" && entry.shellInputRegex) {
84426
84676
  const regex = entry.shellInputRegex;
84427
84677
  if (isShellRegexRoundtrippable(regex)) pattern = shellRegexToGlob(regex);
84428
- else if (action === "deny") {
84429
- 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.`);
84430
- pattern = "*";
84431
- } else {
84432
- pattern = shellRegexToGlob(regex);
84433
- 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
+ }
84434
84690
  }
84435
84691
  } else pattern = "*";
84436
84692
  if (isPrototypePollutionKey(pattern)) {
@@ -85923,15 +86179,19 @@ const CLAUDECODE_ENV_IGNORED_IN_EVERY_FILE = /* @__PURE__ */ new Set([
85923
86179
  * `env` variables a project or local settings file cannot set, because a
85924
86180
  * checked-out repository should not control them: where Claude Code stores its
85925
86181
  * own files, what session content it exports, where telemetry goes, how it
85926
- * starts or syncs, the timers on an unanswered dialog, and whether it processes
85927
- * 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`.
85928
86185
  *
85929
- * Only the names the settings reference lists are covered; its directory and
85930
- * 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
85931
86188
  * families is still written. Windows variable names are case-insensitive, so
85932
- * 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.
85933
86192
  *
85934
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
85935
86195
  */
85936
86196
  const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85937
86197
  "BETA_TRACING_ENDPOINT",
@@ -85941,6 +86201,9 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85941
86201
  "CLAUDE_CODE_DISABLE_FILE_MENTIONS",
85942
86202
  "CLAUDE_CODE_ENABLE_TELEMETRY",
85943
86203
  "CLAUDE_CODE_ENHANCED_TELEMETRY_BETA",
86204
+ "CLAUDE_CODE_OAUTH_REFRESH_TOKEN",
86205
+ "CLAUDE_CODE_OAUTH_SCOPES",
86206
+ "CLAUDE_CODE_OAUTH_TOKEN",
85944
86207
  "CLAUDE_CODE_PLUGIN_CACHE_DIR",
85945
86208
  "CLAUDE_CODE_PLUGIN_SEED_DIR",
85946
86209
  "CLAUDE_CODE_PROCESS_WRAPPER",
@@ -85961,14 +86224,23 @@ const CLAUDECODE_ENV_PROJECT_IGNORED_NAMES = /* @__PURE__ */ new Set([
85961
86224
  "TMPDIR"
85962
86225
  ]);
85963
86226
  const CLAUDECODE_ENV_PROJECT_IGNORED_WINDOWS_NAMES = /* @__PURE__ */ new Set([
86227
+ "ALLUSERSPROFILE",
86228
+ "COMMONPROGRAMW6432",
85964
86229
  "COMSPEC",
85965
86230
  "LOCALAPPDATA",
85966
86231
  "PATHEXT",
85967
86232
  "PROGRAMDATA",
85968
86233
  "PROGRAMW6432",
85969
86234
  "PSMODULEPATH",
86235
+ "SYSTEMDRIVE",
85970
86236
  "SYSTEMROOT"
85971
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"];
85972
86244
  const CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES = [
85973
86245
  "_ENDPOINT",
85974
86246
  "_HEADERS",
@@ -86013,7 +86285,7 @@ const CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES = {
86013
86285
  function isProjectIgnoredEnvVariable(name, value) {
86014
86286
  if (CLAUDECODE_ENV_PROJECT_IGNORED_NAMES.has(name) || name.startsWith("XDG_") || name.startsWith("CLAUDE_AX_")) return true;
86015
86287
  const upper = name.toUpperCase();
86016
- 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;
86017
86289
  if (name.startsWith("OTEL_EXPORTER_OTLP_") && CLAUDECODE_ENV_OTLP_IGNORED_SUFFIXES.some((suffix) => name.endsWith(suffix))) return true;
86018
86290
  if (Object.hasOwn(CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES, name)) {
86019
86291
  const offValues = CLAUDECODE_ENV_PROJECT_HONORED_OFF_VALUES[name];
@@ -86045,7 +86317,7 @@ function stripIgnoredEnvVariables({ env, global, relativeFilePath, logger }) {
86045
86317
  kept[name] = value;
86046
86318
  }
86047
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.`);
86048
- 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.`);
86049
86321
  return kept;
86050
86322
  }
86051
86323
  /**
@@ -86695,7 +86967,7 @@ const CODEBUDDY_RULE_LISTS = [
86695
86967
  "ask",
86696
86968
  "deny"
86697
86969
  ];
86698
- const CATCH_ALL_PATTERN$13 = "*";
86970
+ const CATCH_ALL_PATTERN$14 = "*";
86699
86971
  const MCP_CATEGORY = "mcp";
86700
86972
  const MCP_RULE_PREFIX = "mcp__";
86701
86973
  const ALL_MCP_RULE = "mcp__*";
@@ -86765,7 +87037,7 @@ function categoryOfRule$2(rule) {
86765
87037
  * and reach the `Bash` rules through `honorAllToolsOnBash`.
86766
87038
  */
86767
87039
  function buildCodebuddyRule({ category, pattern, action, logger }) {
86768
- const catchAll = pattern === CATCH_ALL_PATTERN$13;
87040
+ const catchAll = pattern === CATCH_ALL_PATTERN$14;
86769
87041
  if (category === MCP_CATEGORY) {
86770
87042
  if (catchAll && action === "allow") {
86771
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').`);
@@ -86774,7 +87046,7 @@ function buildCodebuddyRule({ category, pattern, action, logger }) {
86774
87046
  return catchAll ? ALL_MCP_RULE : `${MCP_RULE_PREFIX}${pattern}`;
86775
87047
  }
86776
87048
  if (category === "*") {
86777
- if (catchAll) return CATCH_ALL_PATTERN$13;
87049
+ if (catchAll) return CATCH_ALL_PATTERN$14;
86778
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').`);
86779
87051
  return null;
86780
87052
  }
@@ -86792,7 +87064,7 @@ function parseCodebuddyRule(rule) {
86792
87064
  const { specifier } = splitCodebuddyRule(rule);
86793
87065
  if (specifier === void 0) return {
86794
87066
  category,
86795
- pattern: CATCH_ALL_PATTERN$13
87067
+ pattern: CATCH_ALL_PATTERN$14
86796
87068
  };
86797
87069
  const trimmed = specifier.trim();
86798
87070
  return trimmed === "" ? null : {
@@ -89118,8 +89390,8 @@ function mapBashActionToDecision(action) {
89118
89390
  // src/features/permissions/commandcode-permissions.ts
89119
89391
  /** Top-level key of `.commandcode/settings.json` that rulesync owns. */
89120
89392
  const COMMANDCODE_PERMISSIONS_KEY = "permissions";
89121
- const CATCH_ALL_PATTERN$12 = "*";
89122
- const MCP_CANONICAL_PREFIX$7 = "mcp__";
89393
+ const CATCH_ALL_PATTERN$13 = "*";
89394
+ const MCP_CANONICAL_PREFIX$8 = "mcp__";
89123
89395
  const COMMANDCODE_ALL_MCP_RULE = "mcp__*";
89124
89396
  const CATEGORY_TO_COMMANDCODE_TOOL = {
89125
89397
  bash: "Shell",
@@ -89164,10 +89436,10 @@ const PADDED_SHELL_WILDCARD_REASON = "a pattern narrower than the bare 'Shell' (
89164
89436
  * restrictions and reach the `Shell` entries through `honorAllToolsOnBash`.
89165
89437
  */
89166
89438
  function buildCommandcodeRule(category, pattern) {
89167
- const catchAll = pattern === CATCH_ALL_PATTERN$12 || pattern === "";
89168
- if (category.startsWith(MCP_CANONICAL_PREFIX$7)) return catchAll ? category : `${category}(${pattern})`;
89169
- if (category === "mcp") return catchAll ? COMMANDCODE_ALL_MCP_RULE : `${MCP_CANONICAL_PREFIX$7}${pattern}`;
89170
- 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;
89171
89443
  const tool = Object.hasOwn(CATEGORY_TO_COMMANDCODE_TOOL, category) ? CATEGORY_TO_COMMANDCODE_TOOL[category] : void 0;
89172
89444
  if (tool === void 0) return null;
89173
89445
  return catchAll ? tool : `${tool}(${pattern})`;
@@ -89203,7 +89475,7 @@ function splitCommandcodeRule(rule) {
89203
89475
  }
89204
89476
  /** Whether a specifier means "no specifier" to Command Code: only `` and `*` do. */
89205
89477
  function isCatchAllSpecifier(inner) {
89206
- return inner === "" || inner === CATCH_ALL_PATTERN$12;
89478
+ return inner === "" || inner === CATCH_ALL_PATTERN$13;
89207
89479
  }
89208
89480
  /**
89209
89481
  * Read an MCP-shaped rule the way Command Code does (`command-code` 1.54.0,
@@ -89237,34 +89509,34 @@ function parseMcpCommandcodeRule({ tool, inner }) {
89237
89509
  if (server.length === 0) return null;
89238
89510
  const everyServer = {
89239
89511
  category: "mcp",
89240
- pattern: CATCH_ALL_PATTERN$12,
89512
+ pattern: CATCH_ALL_PATTERN$13,
89241
89513
  notInAllow: true
89242
89514
  };
89243
- if (tool.startsWith(MCP_CANONICAL_PREFIX$7)) {
89244
- if (server === CATCH_ALL_PATTERN$12) return everyServer;
89245
- 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;
89246
89518
  return {
89247
- category: rest === "" || rest === CATCH_ALL_PATTERN$12 ? `${MCP_CANONICAL_PREFIX$7}${server}` : `${MCP_CANONICAL_PREFIX$7}${remainder}`,
89248
- 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,
89249
89521
  notInAllow: false
89250
89522
  };
89251
89523
  }
89252
- const category = `${MCP_CANONICAL_PREFIX$7}${remainder}`;
89253
- 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);
89254
89526
  if (!isCatchAllSpecifier(inner)) return namesTool ? {
89255
89527
  category,
89256
89528
  pattern: inner,
89257
89529
  notInAllow: true
89258
89530
  } : null;
89259
- if (remainder === CATCH_ALL_PATTERN$12) return everyServer;
89260
- if (rest === CATCH_ALL_PATTERN$12 && !server.includes(CATCH_ALL_PATTERN$12)) return {
89261
- category: `${MCP_CANONICAL_PREFIX$7}${server}`,
89262
- 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,
89263
89535
  notInAllow: true
89264
89536
  };
89265
89537
  return namesTool ? {
89266
89538
  category,
89267
- pattern: CATCH_ALL_PATTERN$12,
89539
+ pattern: CATCH_ALL_PATTERN$13,
89268
89540
  notInAllow: false
89269
89541
  } : null;
89270
89542
  }
@@ -89288,14 +89560,14 @@ function friendlyToolRule({ category, inner }) {
89288
89560
  if (category === "bash") {
89289
89561
  if (isCatchAllSpecifier(inner)) return {
89290
89562
  category,
89291
- pattern: CATCH_ALL_PATTERN$12,
89563
+ pattern: CATCH_ALL_PATTERN$13,
89292
89564
  notInAllow: false
89293
89565
  };
89294
89566
  const normalized = inner.trim().replace(/\s+/g, " ");
89295
89567
  if (normalized === "" || normalized === ":*") return null;
89296
89568
  return isCatchAllSpecifier(normalized) ? {
89297
89569
  category,
89298
- pattern: CATCH_ALL_PATTERN$12,
89570
+ pattern: CATCH_ALL_PATTERN$13,
89299
89571
  notInAllow: true
89300
89572
  } : {
89301
89573
  category,
@@ -89305,7 +89577,7 @@ function friendlyToolRule({ category, inner }) {
89305
89577
  }
89306
89578
  if (isCatchAllSpecifier(inner)) return {
89307
89579
  category,
89308
- pattern: CATCH_ALL_PATTERN$12,
89580
+ pattern: CATCH_ALL_PATTERN$13,
89309
89581
  notInAllow: false
89310
89582
  };
89311
89583
  return isCatchAllSpecifier(inner.trim()) ? null : {
@@ -89324,13 +89596,13 @@ function friendlyToolRule({ category, inner }) {
89324
89596
  */
89325
89597
  function parseCommandcodeRule(rule) {
89326
89598
  const { tool, inner } = splitCommandcodeRule(rule);
89327
- if (tool === CATCH_ALL_PATTERN$12) return isCatchAllSpecifier(inner) ? {
89599
+ if (tool === CATCH_ALL_PATTERN$13) return isCatchAllSpecifier(inner) ? {
89328
89600
  category: "*",
89329
- pattern: CATCH_ALL_PATTERN$12,
89601
+ pattern: CATCH_ALL_PATTERN$13,
89330
89602
  notInAllow: true
89331
89603
  } : null;
89332
89604
  const lowered = tool.toLowerCase();
89333
- if (lowered.startsWith(MCP_CANONICAL_PREFIX$7)) return parseMcpCommandcodeRule({
89605
+ if (lowered.startsWith(MCP_CANONICAL_PREFIX$8)) return parseMcpCommandcodeRule({
89334
89606
  tool,
89335
89607
  inner
89336
89608
  });
@@ -89348,12 +89620,12 @@ function parseCommandcodeRule(rule) {
89348
89620
  */
89349
89621
  function isFoldedMcpToolNameRule({ rule, category }) {
89350
89622
  const { tool } = splitCommandcodeRule(rule);
89351
- 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);
89352
89624
  }
89353
89625
  /** The `Tool` half of `Tool(specifier)`, when the rule is an exact-prefix MCP rule with a specifier. */
89354
89626
  function scopedMcpRuleTool(rule) {
89355
89627
  const { tool, inner } = splitCommandcodeRule(rule);
89356
- return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$7) ? tool : null;
89628
+ return !isCatchAllSpecifier(inner) && tool.startsWith(MCP_CANONICAL_PREFIX$8) ? tool : null;
89357
89629
  }
89358
89630
  /**
89359
89631
  * The string entries of a Command Code permission list. Command Code reads
@@ -89396,7 +89668,7 @@ function preservedRules({ existingPermissions, key, managedCategories, written,
89396
89668
  }).filter((rule) => {
89397
89669
  const parsed = parseCommandcodeRule(rule);
89398
89670
  if (parsed === null || !managedCategories.has(parsed.category)) return true;
89399
- 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})`);
89400
89672
  if (key !== "allow" && isStricterAction({
89401
89673
  action: key,
89402
89674
  existing: replacement
@@ -89453,7 +89725,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89453
89725
  const ranked = /* @__PURE__ */ new Map();
89454
89726
  const managedCategories = /* @__PURE__ */ new Set();
89455
89727
  for (const [category, rules] of Object.entries(permission)) {
89456
- const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$12);
89728
+ const categoryRule = buildCommandcodeRule(category, CATCH_ALL_PATTERN$13);
89457
89729
  const managedCategory = categoryRule === null ? null : parseCommandcodeRule(categoryRule);
89458
89730
  if (managedCategory !== null) managedCategories.add(managedCategory.category);
89459
89731
  for (const [pattern, action] of Object.entries(rules)) {
@@ -89522,7 +89794,7 @@ function buildCommandcodeRuleLists({ config, existingPermissions, logger }) {
89522
89794
  function skippedAllowImportMessage({ rule, category }) {
89523
89795
  const { tool, inner } = splitCommandcodeRule(rule);
89524
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.`;
89525
- 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.`;
89526
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.`;
89527
89799
  }
89528
89800
  /**
@@ -89717,7 +89989,7 @@ function parseCommandcodeSettings({ fileContent, relativePath }) {
89717
89989
  }
89718
89990
  // src/features/permissions/continue-permissions.ts
89719
89991
  const CONTINUE_GLOBAL_ONLY_MESSAGE = "Continue permissions are global-only; use --global to sync ~/.continue/permissions.yaml";
89720
- const CATCH_ALL_PATTERN$11 = "*";
89992
+ const CATCH_ALL_PATTERN$12 = "*";
89721
89993
  /**
89722
89994
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Continue
89723
89995
  * would match `Tool(**)` against every argument just as the bare tool name
@@ -89725,7 +89997,7 @@ const CATCH_ALL_PATTERN$11 = "*";
89725
89997
  * the bare entry instead of being skipped as a pattern-specific rule.
89726
89998
  */
89727
89999
  function normalizeCatchAllPattern$1(pattern) {
89728
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90000
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$12 : pattern;
89729
90001
  }
89730
90002
  const CANONICAL_TO_CONTINUE_TOOL_NAMES = {
89731
90003
  bash: "Bash",
@@ -89769,7 +90041,7 @@ function parseContinuePermissionEntry(entry) {
89769
90041
  if (toolName === "") return;
89770
90042
  return {
89771
90043
  toolName,
89772
- pattern: pattern === "" ? CATCH_ALL_PATTERN$11 : pattern
90044
+ pattern: pattern === "" ? CATCH_ALL_PATTERN$12 : pattern
89773
90045
  };
89774
90046
  }
89775
90047
  /**
@@ -89934,7 +90206,7 @@ function convertRulesyncToContinuePermissions({ config, logger }) {
89934
90206
  logger?.warn(`Continue permissions.yaml cannot hold a parenthesis inside a pattern, so the "${action}" rule for "${category}" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89935
90207
  continue;
89936
90208
  }
89937
- if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$11) {
90209
+ if (toolName === "*" && pattern !== CATCH_ALL_PATTERN$12) {
89938
90210
  logger?.warn(`Continue permissions.yaml cannot scope a pattern to every tool, so the "${action}" rule for "*" (pattern ${quoteValueForWarning(pattern)}) was skipped.`);
89939
90211
  continue;
89940
90212
  }
@@ -89990,13 +90262,116 @@ const AUTO_APPROVE_KEYS = {
89990
90262
  webfetch: "chat.tools.urls.autoApprove"
89991
90263
  };
89992
90264
  /**
89993
- * The workspace setting holding the starting permission level of new chat
89994
- * 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}.
89995
90269
  *
89996
90270
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
89997
90271
  */
89998
90272
  const DEFAULT_PERMISSION_MODE_KEY$1 = "chat.permissions.default";
89999
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
+ /**
90000
90375
  * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
90001
90376
  * at path `a.b` is written to `chat.agent.sandbox.a.b`.
90002
90377
  */
@@ -90183,9 +90558,6 @@ function readSandboxFromSettings(settings) {
90183
90558
  }
90184
90559
  return Object.keys(sandbox).length > 0 ? sandbox : void 0;
90185
90560
  }
90186
- function isCopilotDefaultPermissionMode(value) {
90187
- return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
90188
- }
90189
90561
  function asAutoApproveMap(value) {
90190
90562
  if (!isPlainObject(value)) return {};
90191
90563
  const result = {};
@@ -90229,9 +90601,11 @@ function buildAutoApproveValue({ category, rules }) {
90229
90601
  * `false`. A key whose canonical category is absent entirely
90230
90602
  * is left untouched, so authoring only `bash` rules never disturbs a
90231
90603
  * hand-written edits or urls map.
90232
- * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
90233
- * (the starting permission level of new chat sessions); without it the key is
90234
- * 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
90235
90609
  * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
90236
90610
  * warns about every value it writes that loosens the sandbox.
90237
90611
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
@@ -90282,17 +90656,24 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90282
90656
  rules
90283
90657
  });
90284
90658
  }
90659
+ const existingSettings = () => parseSharedConfig({
90660
+ format: "jsonc",
90661
+ fileContent: existingContent,
90662
+ filePath
90663
+ });
90285
90664
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
90286
- 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
+ }
90287
90672
  const sandbox = config.copilot?.sandbox;
90288
90673
  if (sandbox !== void 0) {
90289
90674
  Object.assign(patch, buildSandboxPatch({
90290
90675
  sandbox,
90291
- existingSettings: parseSharedConfig({
90292
- format: "jsonc",
90293
- fileContent: existingContent,
90294
- filePath
90295
- })
90676
+ existingSettings: existingSettings()
90296
90677
  }));
90297
90678
  warnOnTrustAffectingEntries({
90298
90679
  toolLabel: COPILOT_TOOL_LABEL,
@@ -90339,10 +90720,10 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
90339
90720
  for (const [pattern, flag] of Object.entries(autoApprove)) rules[pattern] = flag ? "allow" : "deny";
90340
90721
  if (Object.keys(rules).length > 0) permission[category] = rules;
90341
90722
  }
90342
- const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY$1];
90723
+ const defaultPermissionMode = readDefaultPermissionMode(settings);
90343
90724
  const sandbox = readSandboxFromSettings(settings);
90344
90725
  const copilot = {
90345
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
90726
+ ...defaultPermissionMode !== void 0 && { defaultPermissionMode },
90346
90727
  ...sandbox !== void 0 && { sandbox }
90347
90728
  };
90348
90729
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
@@ -90540,14 +90921,14 @@ var CopilotcliPermissions = class CopilotcliPermissions extends ToolPermissions
90540
90921
  }
90541
90922
  };
90542
90923
  // src/features/permissions/crush-permissions.ts
90543
- const CATCH_ALL_PATTERN$10 = "*";
90924
+ const CATCH_ALL_PATTERN$11 = "*";
90544
90925
  /**
90545
90926
  * A run of `*` (`**`, `***`) is the catch-all spelled another way: Crush has no
90546
90927
  * argument patterns at all, so it is the only pattern a rule can carry and
90547
90928
  * still be written.
90548
90929
  */
90549
90930
  function normalizeCatchAllPattern(pattern) {
90550
- return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$10 : pattern;
90931
+ return /^\*+$/.test(pattern) ? CATCH_ALL_PATTERN$11 : pattern;
90551
90932
  }
90552
90933
  const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90553
90934
  bash: "bash",
@@ -90559,11 +90940,11 @@ const CANONICAL_TO_CRUSH_TOOL_NAMES = {
90559
90940
  webfetch: "fetch"
90560
90941
  };
90561
90942
  const CRUSH_TO_CANONICAL_TOOL_NAMES = Object.fromEntries(Object.entries(CANONICAL_TO_CRUSH_TOOL_NAMES).map(([k, v]) => [v, k]));
90562
- const MCP_CANONICAL_PREFIX$6 = "mcp__";
90943
+ const MCP_CANONICAL_PREFIX$7 = "mcp__";
90563
90944
  const MCP_CRUSH_PREFIX = "mcp_";
90564
90945
  function toCrushToolName(canonical) {
90565
90946
  if (Object.hasOwn(CANONICAL_TO_CRUSH_TOOL_NAMES, canonical)) return CANONICAL_TO_CRUSH_TOOL_NAMES[canonical] ?? canonical;
90566
- 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("__", "_")}`;
90567
90948
  return canonical;
90568
90949
  }
90569
90950
  function toCanonicalToolName$5(crushName) {
@@ -90737,7 +91118,7 @@ function resolveCategoryAction({ category, toolName, rules, logger }) {
90737
91118
  for (const [rawPattern, action] of Object.entries(rules)) {
90738
91119
  if (isPrototypePollutionKey(rawPattern)) continue;
90739
91120
  const pattern = normalizeCatchAllPattern(rawPattern);
90740
- if (pattern === CATCH_ALL_PATTERN$10) {
91121
+ if (pattern === CATCH_ALL_PATTERN$11) {
90741
91122
  catchAllAction = action;
90742
91123
  continue;
90743
91124
  }
@@ -90810,11 +91191,11 @@ function convertCrushListsToRulesync({ allowed, disabled }) {
90810
91191
  };
90811
91192
  for (const entry of disabled) {
90812
91193
  if (entry === "" || isPrototypePollutionKey(entry)) continue;
90813
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] = "deny";
91194
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] = "deny";
90814
91195
  }
90815
91196
  for (const entry of allowed) {
90816
91197
  if (entry === "" || entry.includes(":") || isPrototypePollutionKey(entry)) continue;
90817
- bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$10] ??= "allow";
91198
+ bucketFor(toCanonicalToolName$5(entry))[CATCH_ALL_PATTERN$11] ??= "allow";
90818
91199
  }
90819
91200
  return { permission };
90820
91201
  }
@@ -90860,13 +91241,13 @@ const CURSOR_TYPE_TO_CANONICAL = {
90860
91241
  WebFetch: "webfetch",
90861
91242
  Mcp: "mcp"
90862
91243
  };
90863
- const MCP_CANONICAL_PREFIX$5 = "mcp__";
91244
+ const MCP_CANONICAL_PREFIX$6 = "mcp__";
90864
91245
  /**
90865
91246
  * Returns true if the canonical category is the per-tool MCP form
90866
91247
  * `mcp__<server>__<tool>`.
90867
91248
  */
90868
91249
  function isMcpScopedCategory(canonical) {
90869
- return canonical.startsWith(MCP_CANONICAL_PREFIX$5) && canonical.length > 5;
91250
+ return canonical.startsWith(MCP_CANONICAL_PREFIX$6) && canonical.length > 5;
90870
91251
  }
90871
91252
  function toCursorType(canonical) {
90872
91253
  if (isMcpScopedCategory(canonical)) return "Mcp";
@@ -90900,7 +91281,7 @@ function toCanonicalCategory$1(cursorType, pattern) {
90900
91281
  if (match) {
90901
91282
  const server = match[1] ?? "*";
90902
91283
  const tool = match[2] ?? "*";
90903
- return `${MCP_CANONICAL_PREFIX$5}${server}__${tool}`;
91284
+ return `${MCP_CANONICAL_PREFIX$6}${server}__${tool}`;
90904
91285
  }
90905
91286
  return CURSOR_TYPE_TO_CANONICAL[cursorType] ?? cursorType.toLowerCase();
90906
91287
  }
@@ -91162,7 +91543,7 @@ function convertCursorToRulesyncPermissions(params) {
91162
91543
  const { toolName: type, pattern } = parseToolPatternEntry(entry);
91163
91544
  const canonical = toCanonicalCategory$1(type, pattern);
91164
91545
  if (!permission[canonical]) permission[canonical] = {};
91165
- const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$5) ? "*" : pattern;
91546
+ const canonicalPattern = type === "Mcp" && canonical.startsWith(MCP_CANONICAL_PREFIX$6) ? "*" : pattern;
91166
91547
  permission[canonical][canonicalPattern] = action;
91167
91548
  }
91168
91549
  };
@@ -92661,7 +93042,7 @@ function convertFactorydroidToRulesyncPermissions({ allow, deny }) {
92661
93042
  // src/features/permissions/goose-permissions.ts
92662
93043
  const GOOSE_GLOBAL_ONLY_MESSAGE = "Goose permissions are global-only; use --global to sync ~/.config/goose/permission.yaml";
92663
93044
  const GOOSE_USER_KEY = "user";
92664
- const CATCH_ALL_PATTERN$9 = "*";
93045
+ const CATCH_ALL_PATTERN$10 = "*";
92665
93046
  const RULESYNC_TO_GOOSE_TOOL_NAME = { bash: "shell" };
92666
93047
  const GOOSE_TO_RULESYNC_TOOL_NAMES = {
92667
93048
  shell: ["bash"],
@@ -92810,7 +93191,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
92810
93191
  const assigned = /* @__PURE__ */ new Map();
92811
93192
  for (const [category, rules] of Object.entries(honorAllToolsOnBash(config.permission))) {
92812
93193
  const toolName = RULESYNC_TO_GOOSE_TOOL_NAME[category] ?? category;
92813
- 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.`);
92814
93195
  const { action, downgraded } = failSafeCatchAllAction({
92815
93196
  rules,
92816
93197
  allToolsRules: category === "*" ? void 0 : config.permission["*"]
@@ -92843,7 +93224,7 @@ function convertGoosePermissionConfigToRulesync(userPermission) {
92843
93224
  const action = GOOSE_LIST_TO_ACTION[key];
92844
93225
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
92845
93226
  permission[category] ??= {};
92846
- permission[category][CATCH_ALL_PATTERN$9] = action;
93227
+ permission[category][CATCH_ALL_PATTERN$10] = action;
92847
93228
  }
92848
93229
  }
92849
93230
  return { permission };
@@ -92853,8 +93234,8 @@ const GROKCLI_UI_KEY = "ui";
92853
93234
  const GROKCLI_PERMISSION_MODE_KEY = "permission_mode";
92854
93235
  const GROKCLI_PERMISSION_KEY = "permission";
92855
93236
  const GROKCLI_AUTO_PERMISSION_MODE = "auto";
92856
- const CATCH_ALL_PATTERN$8 = "*";
92857
- const MCP_CANONICAL_PREFIX$4 = "mcp__";
93237
+ const CATCH_ALL_PATTERN$9 = "*";
93238
+ const MCP_CANONICAL_PREFIX$5 = "mcp__";
92858
93239
  const CATEGORY_TO_GROK_TOOL = {
92859
93240
  bash: "Bash",
92860
93241
  read: "Read",
@@ -92886,6 +93267,25 @@ const GROK_TOOL_TO_CATEGORY_LOWER = {
92886
93267
  };
92887
93268
  const GROK_MCP_TOOL_ALIASES = /* @__PURE__ */ new Set(["mcp", GROK_MCP_TOOL.toLowerCase()]);
92888
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
+ }
92889
93289
  /**
92890
93290
  * Build a Grok Claude-style permission entry (e.g. `Bash(git *)`, `Read`,
92891
93291
  * `MCPTool(server__tool)`) from a canonical category + pattern. Returns `null`
@@ -92896,18 +93296,20 @@ const GROKCLI_PERMISSION_RULES_KEY = "rules";
92896
93296
  * `MCPTool`. For every other tool, a `*` pattern emits the bare tool name and a
92897
93297
  * concrete pattern emits `Tool(pattern)`. The all-tools `*` category becomes
92898
93298
  * Grok's bare `*` rule for its catch-all pattern only; a narrower pattern
92899
- * 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`).
92900
93302
  */
92901
93303
  function buildGrokEntry(category, pattern) {
92902
- if (category === "*") return pattern === CATCH_ALL_PATTERN$8 ? GROK_ALL_TOOLS_ENTRY : null;
92903
- 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)) {
92904
93306
  const remainder = category.slice(5);
92905
93307
  return remainder.length > 0 ? `${GROK_MCP_TOOL}(${remainder})` : GROK_MCP_TOOL;
92906
93308
  }
92907
- 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})`;
92908
93310
  const tool = CATEGORY_TO_GROK_TOOL[category];
92909
93311
  if (tool === void 0) return null;
92910
- return pattern === CATCH_ALL_PATTERN$8 || pattern === "" ? tool : `${tool}(${pattern})`;
93312
+ return pattern === CATCH_ALL_PATTERN$9 || pattern === "" ? tool : `${tool}(${toGrokPathPattern(category, pattern)})`;
92911
93313
  }
92912
93314
  /**
92913
93315
  * Parse a Grok Claude-style permission entry back into a canonical category +
@@ -92929,21 +93331,21 @@ function parseGrokEntry(entry) {
92929
93331
  }
92930
93332
  if (parenIndex === -1 && tool === GROK_ALL_TOOLS_ENTRY) return {
92931
93333
  category: "*",
92932
- pattern: CATCH_ALL_PATTERN$8
93334
+ pattern: CATCH_ALL_PATTERN$9
92933
93335
  };
92934
- 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);
92935
93337
  if (tool === GROK_MCP_TOOL) return inner.length > 0 ? {
92936
- category: `${MCP_CANONICAL_PREFIX$4}${inner}`,
92937
- pattern: CATCH_ALL_PATTERN$8
93338
+ category: `${MCP_CANONICAL_PREFIX$5}${inner}`,
93339
+ pattern: CATCH_ALL_PATTERN$9
92938
93340
  } : {
92939
93341
  category: "mcp",
92940
- pattern: CATCH_ALL_PATTERN$8
93342
+ pattern: CATCH_ALL_PATTERN$9
92941
93343
  };
92942
93344
  const category = GROK_TOOL_TO_CATEGORY[tool];
92943
93345
  if (category === void 0) return null;
92944
93346
  return {
92945
93347
  category,
92946
- pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$8
93348
+ pattern: inner.length > 0 ? inner : CATCH_ALL_PATTERN$9
92947
93349
  };
92948
93350
  }
92949
93351
  /**
@@ -92960,14 +93362,14 @@ function parseGrokEntry(entry) {
92960
93362
  function parseClaudeStyleMcpEntry(entry) {
92961
93363
  const rest = entry.slice(5);
92962
93364
  if (rest.length === 0) return null;
92963
- if (rest === CATCH_ALL_PATTERN$8) return {
93365
+ if (rest === CATCH_ALL_PATTERN$9) return {
92964
93366
  category: "mcp",
92965
- pattern: CATCH_ALL_PATTERN$8
93367
+ pattern: CATCH_ALL_PATTERN$9
92966
93368
  };
92967
93369
  const address = rest.includes("__") ? rest : `${rest}__*`;
92968
93370
  return {
92969
- category: `${MCP_CANONICAL_PREFIX$4}${address}`,
92970
- pattern: CATCH_ALL_PATTERN$8
93371
+ category: `${MCP_CANONICAL_PREFIX$5}${address}`,
93372
+ pattern: CATCH_ALL_PATTERN$9
92971
93373
  };
92972
93374
  }
92973
93375
  /**
@@ -92988,20 +93390,20 @@ function parseGrokRule(rule) {
92988
93390
  if (typeof tool !== "string") return null;
92989
93391
  if (pattern !== void 0 && typeof pattern !== "string") return null;
92990
93392
  const trimmedPattern = pattern?.trim() ?? "";
92991
- const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$8;
93393
+ const resolvedPattern = trimmedPattern.length > 0 ? trimmedPattern : CATCH_ALL_PATTERN$9;
92992
93394
  const lowerTool = tool.trim().toLowerCase();
92993
- 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 ? {
92994
93396
  category: "*",
92995
- pattern: CATCH_ALL_PATTERN$8,
93397
+ pattern: CATCH_ALL_PATTERN$9,
92996
93398
  action
92997
93399
  } : null;
92998
- 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 ? {
92999
93401
  category: "mcp",
93000
- pattern: CATCH_ALL_PATTERN$8,
93402
+ pattern: CATCH_ALL_PATTERN$9,
93001
93403
  action
93002
93404
  } : {
93003
- category: `${MCP_CANONICAL_PREFIX$4}${resolvedPattern}`,
93004
- pattern: CATCH_ALL_PATTERN$8,
93405
+ category: `${MCP_CANONICAL_PREFIX$5}${resolvedPattern}`,
93406
+ pattern: CATCH_ALL_PATTERN$9,
93005
93407
  action
93006
93408
  };
93007
93409
  if (!Object.hasOwn(GROK_TOOL_TO_CATEGORY_LOWER, lowerTool)) return null;
@@ -93151,7 +93553,7 @@ var GrokcliPermissions = class GrokcliPermissions extends ToolPermissions {
93151
93553
  throw new Error(`Failed to parse Grok config.toml content in ${(0, node_path.join)(this.getRelativeDirPath(), this.getRelativeFilePath())}: ${formatError(error)}`, { cause: error });
93152
93554
  }
93153
93555
  const fineGrained = parseGrokPermissionArrays(isRecord(parsed[GROKCLI_PERMISSION_KEY]) ? parsed[GROKCLI_PERMISSION_KEY] : {});
93154
- 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) } } };
93155
93557
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
93156
93558
  }
93157
93559
  static forDeletion({ outputRoot = process.cwd(), relativeDirPath, relativeFilePath, global = false }) {
@@ -93336,6 +93738,195 @@ function ensureCategory(permission, category) {
93336
93738
  function removeEmptyCategories(permission) {
93337
93739
  for (const [category, rules] of Object.entries(permission)) if (Object.keys(rules).length === 0) delete permission[category];
93338
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
+ }
93339
93930
  /**
93340
93931
  * Make the native `command_allowlist` authoritative for the `bash` allow rules
93341
93932
  * it can speak for, and for those only. The allowlist is a list of shell-command
@@ -93353,8 +93944,9 @@ function reconcileCommandAllowlist({ permission, commandAllowlist }) {
93353
93944
  writesAllToolsDeny: false
93354
93945
  });
93355
93946
  const withheld = new Set(shadowedAllowPatterns);
93947
+ const isWithheld = (pattern) => withheld.has(pattern) || isMcpAllowlistEntry(pattern);
93356
93948
  const rules = ensureCategory(permission, SHELL_PERMISSION_CATEGORY);
93357
- 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];
93358
93950
  for (const pattern of commandAllowlist) rules[pattern] = "allow";
93359
93951
  }
93360
93952
  /**
@@ -93442,17 +94034,31 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93442
94034
  return true;
93443
94035
  }
93444
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
+ }
93445
94054
  this.fileContent = applySharedConfigPatch({
93446
94055
  fileKey: getHermesagentConfigSharedFileKey({ global: this.global }),
93447
94056
  feature: "permissions",
93448
94057
  existingContent: fileContent,
93449
- patch: parseSharedConfig({
93450
- format: "yaml",
93451
- fileContent: this.fileContent
93452
- })
94058
+ patch
93453
94059
  });
93454
94060
  }
93455
- toRulesyncPermissions() {
94061
+ toRulesyncPermissions({ logger } = {}) {
93456
94062
  const config = parseSharedConfig({
93457
94063
  format: "yaml",
93458
94064
  fileContent: this.getFileContent()
@@ -93464,9 +94070,15 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93464
94070
  }) : rawProvenance);
93465
94071
  const provenance = parsedProvenance.success ? parsedProvenance.data : { permission: {} };
93466
94072
  const permission = clonePermissionBlock(provenance.permission);
94073
+ const commandAllowlist = isStringArray(config.command_allowlist) ? config.command_allowlist : [];
93467
94074
  reconcileCommandAllowlist({
93468
94075
  permission,
93469
- 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
93470
94082
  });
93471
94083
  const approvals = isRecord(config.approvals) ? config.approvals : {};
93472
94084
  reconcileNativeDenies({
@@ -93524,8 +94136,16 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
93524
94136
  logger
93525
94137
  });
93526
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];
93527
94147
  let config = {};
93528
- if (commandAllowlist.length > 0) config.command_allowlist = commandAllowlist;
94148
+ if (allowlist.length > 0) config.command_allowlist = allowlist;
93529
94149
  if (bashDeny.length > 0) config.approvals = { deny: bashDeny };
93530
94150
  if (webfetchDeny.length > 0) config.security = { website_blocklist: {
93531
94151
  enabled: true,
@@ -96010,7 +96630,7 @@ var OpencodePermissions = class extends ToolPermissions {
96010
96630
  * `.mimocode/` layout. Tool-only categories round-trip through the `mimocode`
96011
96631
  * override block.
96012
96632
  *
96013
- * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/opencode/src/config/permission.ts
96633
+ * @see https://github.com/XiaomiMiMo/MiMo-Code/blob/main/packages/cli/src/config/permission.ts
96014
96634
  */
96015
96635
  var MimocodePermissions = class extends OpencodePermissions {
96016
96636
  static layout = MIMOCODE_LAYOUT;
@@ -100366,6 +100986,7 @@ const TAKT_SECURITY_POLICIES = {
100366
100986
  ],
100367
100987
  workflow_runtime_prepare: ["custom_scripts"],
100368
100988
  workflow_command_gates: ["custom_scripts"],
100989
+ workflow_system_commands: ["custom_scripts"],
100369
100990
  sync_conflict_resolver: ["auto_approve_tools"],
100370
100991
  allow_git_hooks: null,
100371
100992
  allow_git_filters: null