rulesync 36.0.2 → 37.1.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.
@@ -457,36 +457,36 @@ const isFeatureValueEnabled = (value) => {
457
457
  const parseCommaSeparatedList = (value) => value.split(",").map((s) => s.trim()).filter(Boolean);
458
458
  //#endregion
459
459
  //#region src/constants/rulesync-paths.ts
460
- const { join: join$422 } = node_path.posix;
460
+ const { join: join$423 } = node_path.posix;
461
461
  const RULESYNC_CONFIG_RELATIVE_FILE_PATH = "rulesync.jsonc";
462
462
  const RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH = "rulesync.local.jsonc";
463
463
  const RULESYNC_USER_CONFIG_DIR_NAME = "rulesync";
464
464
  const RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME = ".config";
465
465
  const RULESYNC_RELATIVE_DIR_PATH = ".rulesync";
466
466
  const RULES_FEATURE_SUBDIR = "rules";
467
- const CURATED_RULES_FEATURE_SUBDIR = join$422(RULES_FEATURE_SUBDIR, ".curated");
467
+ const CURATED_RULES_FEATURE_SUBDIR = join$423(RULES_FEATURE_SUBDIR, ".curated");
468
468
  const COMMANDS_FEATURE_SUBDIR = "commands";
469
469
  const SUBAGENTS_FEATURE_SUBDIR = "subagents";
470
470
  const CHECKS_FEATURE_SUBDIR = "checks";
471
471
  const SKILLS_FEATURE_SUBDIR = "skills";
472
- const CURATED_SKILLS_FEATURE_SUBDIR = join$422(SKILLS_FEATURE_SUBDIR, ".curated");
473
- const RULESYNC_RULES_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
- const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
- const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
- const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
- const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
- const RULESYNC_MCP_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
- const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
- const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
- join$422(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
- const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
- const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
472
+ const CURATED_SKILLS_FEATURE_SUBDIR = join$423(SKILLS_FEATURE_SUBDIR, ".curated");
473
+ const RULESYNC_RULES_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, RULES_FEATURE_SUBDIR);
474
+ const RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CURATED_RULES_FEATURE_SUBDIR);
475
+ const RULESYNC_COMMANDS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, COMMANDS_FEATURE_SUBDIR);
476
+ const RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, SUBAGENTS_FEATURE_SUBDIR);
477
+ const RULESYNC_CHECKS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CHECKS_FEATURE_SUBDIR);
478
+ const RULESYNC_MCP_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "mcp.jsonc");
479
+ const RULESYNC_HOOKS_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "hooks.jsonc");
480
+ const RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "permissions.jsonc");
481
+ join$423(RULESYNC_RELATIVE_DIR_PATH, "mcp.json");
482
+ const RULESYNC_HOOKS_LEGACY_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "hooks.json");
483
+ const RULESYNC_PERMISSIONS_LEGACY_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, "permissions.json");
484
484
  const RULESYNC_AIIGNORE_FILE_NAME = ".aiignore";
485
- const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
485
+ const RULESYNC_AIIGNORE_RELATIVE_FILE_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, ".aiignore");
486
486
  const RULESYNC_IGNORE_RELATIVE_FILE_PATH = ".rulesyncignore";
487
487
  const RULESYNC_OVERVIEW_FILE_NAME = "overview.md";
488
- const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
- const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$422(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
488
+ const RULESYNC_SKILLS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, SKILLS_FEATURE_SUBDIR);
489
+ const RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH = join$423(RULESYNC_RELATIVE_DIR_PATH, CURATED_SKILLS_FEATURE_SUBDIR);
490
490
  const RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync.lock";
491
491
  const RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH = "rulesync-npm.lock.json";
492
492
  const RULESYNC_MCP_FILE_NAME = "mcp.jsonc";
@@ -4062,7 +4062,7 @@ function isPlainObject$1(value) {
4062
4062
  /**
4063
4063
  * Type guard to check if a value is an array of strings.
4064
4064
  */
4065
- function isStringArray$2(value) {
4065
+ function isStringArray$1(value) {
4066
4066
  return Array.isArray(value) && value.every((item) => typeof item === "string");
4067
4067
  }
4068
4068
  //#endregion
@@ -7917,14 +7917,53 @@ const COPILOT_DEFAULT_PERMISSION_MODES = [
7917
7917
  "autoApprove",
7918
7918
  "autopilot"
7919
7919
  ];
7920
+ /** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
7921
+ const CopilotSandboxEnabledSchema = zod_mini.z.enum(["off", "on"]);
7922
+ /** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
7923
+ const CopilotSandboxUserConfiguredPathsSchema = zod_mini.z.strictObject({
7924
+ readwritePaths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
7925
+ readonlyPaths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
7926
+ deniedPaths: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
7927
+ });
7928
+ /**
7929
+ * The Agent Host sandbox settings VS Code 1.141 settled on, as a nested object
7930
+ * whose path mirrors the dotted setting key under `chat.agent.sandbox.` — e.g.
7931
+ * `network.allowNetwork` writes `chat.agent.sandbox.network.allowNetwork`.
7932
+ * Only the documented, registered keys are accepted; the deprecated 1.138
7933
+ * shapes (`chat.agent.sandbox.allowNetwork`, `.fileSystem.linux|mac|windows`)
7934
+ * are not modeled, since the Agent Host ignores them.
7935
+ *
7936
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
7937
+ * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
7938
+ */
7939
+ const CopilotSandboxSchema = zod_mini.z.strictObject({
7940
+ enabled: zod_mini.z.optional(CopilotSandboxEnabledSchema),
7941
+ network: zod_mini.z.optional(zod_mini.z.strictObject({
7942
+ allowNetwork: zod_mini.z.optional(zod_mini.z.boolean()),
7943
+ allowLocalNetwork: zod_mini.z.optional(zod_mini.z.boolean())
7944
+ })),
7945
+ allowUnsandboxedCommands: zod_mini.z.optional(zod_mini.z.boolean()),
7946
+ mcpServers: zod_mini.z.optional(zod_mini.z.boolean()),
7947
+ lspServers: zod_mini.z.optional(zod_mini.z.boolean()),
7948
+ credentials: zod_mini.z.optional(zod_mini.z.strictObject({
7949
+ authenticategit: zod_mini.z.optional(zod_mini.z.boolean()),
7950
+ authenticategh: zod_mini.z.optional(zod_mini.z.boolean())
7951
+ })),
7952
+ fileSystem: zod_mini.z.optional(zod_mini.z.strictObject({
7953
+ allowDevToolAccess: zod_mini.z.optional(zod_mini.z.boolean()),
7954
+ userConfiguredPaths: zod_mini.z.optional(CopilotSandboxUserConfiguredPathsSchema)
7955
+ }))
7956
+ });
7920
7957
  /**
7921
7958
  * Tool-scoped override block for GitHub Copilot in VS Code.
7922
7959
  * `defaultPermissionMode` writes the workspace `chat.permissions.default`
7923
- * setting, which has no canonical category.
7960
+ * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
7961
+ * has a canonical category.
7924
7962
  */
7925
7963
  const CopilotPermissionsOverrideSchema = zod_mini.z.looseObject({
7926
7964
  permission: zod_mini.z.optional(ToolScopedPermissionSchema),
7927
- defaultPermissionMode: zod_mini.z.optional(zod_mini.z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
7965
+ defaultPermissionMode: zod_mini.z.optional(zod_mini.z.enum(COPILOT_DEFAULT_PERMISSION_MODES)),
7966
+ sandbox: zod_mini.z.optional(CopilotSandboxSchema)
7928
7967
  });
7929
7968
  /**
7930
7969
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
@@ -12857,6 +12896,10 @@ const SHARED_CONFIG_OWNERSHIP = {
12857
12896
  kind: "replace-owned-keys",
12858
12897
  ownedKeys: ["disableAllHooks"]
12859
12898
  },
12899
+ mcp: {
12900
+ kind: "replace-owned-keys",
12901
+ ownedKeys: ["disabledMcpServers"]
12902
+ },
12860
12903
  permissions: {
12861
12904
  kind: "replace-owned-keys",
12862
12905
  ownedKeys: ["deniedUrls"]
@@ -12871,6 +12914,10 @@ const SHARED_CONFIG_OWNERSHIP = {
12871
12914
  kind: "replace-owned-keys",
12872
12915
  ownedKeys: ["disableAllHooks"]
12873
12916
  },
12917
+ mcp: {
12918
+ kind: "replace-owned-keys",
12919
+ ownedKeys: ["disabledMcpServers"]
12920
+ },
12874
12921
  permissions: {
12875
12922
  kind: "replace-owned-keys",
12876
12923
  ownedKeys: ["allowedUrls", "deniedUrls"]
@@ -12888,6 +12935,16 @@ const SHARED_CONFIG_OWNERSHIP = {
12888
12935
  "chat.tools.edits.autoApprove",
12889
12936
  "chat.tools.urls.autoApprove",
12890
12937
  "chat.permissions.default",
12938
+ "chat.agent.sandbox.enabled",
12939
+ "chat.agent.sandbox.network.allowNetwork",
12940
+ "chat.agent.sandbox.network.allowLocalNetwork",
12941
+ "chat.agent.sandbox.allowUnsandboxedCommands",
12942
+ "chat.agent.sandbox.mcpServers",
12943
+ "chat.agent.sandbox.lspServers",
12944
+ "chat.agent.sandbox.credentials.authenticategit",
12945
+ "chat.agent.sandbox.credentials.authenticategh",
12946
+ "chat.agent.sandbox.fileSystem.allowDevToolAccess",
12947
+ "chat.agent.sandbox.fileSystem.userConfiguredPaths",
12891
12948
  "zoo-code.allowedCommands",
12892
12949
  "zoo-code.deniedCommands",
12893
12950
  "roo-cline.allowedCommands",
@@ -22135,7 +22192,7 @@ function getOpencodeCommandEntries({ config, layout = OPENCODE_LAYOUT }) {
22135
22192
  */
22136
22193
  function getOpencodeSkillPaths({ config, layout = OPENCODE_LAYOUT }) {
22137
22194
  if (Array.isArray(config.skills)) {
22138
- if (!layout.readsV2ConfigSpellings || !isStringArray$2(config.skills)) return [];
22195
+ if (!layout.readsV2ConfigSpellings || !isStringArray$1(config.skills)) return [];
22139
22196
  return config.skills.filter((value) => !/^https?:\/\//i.test(value));
22140
22197
  }
22141
22198
  const skills = asOpencodeEntries(config.skills);
@@ -26381,7 +26438,7 @@ const isBooleanValue = ({ value }) => typeof value === "boolean";
26381
26438
  const isFiniteNumber = ({ value }) => Number.isFinite(value);
26382
26439
  const isNonEmptyString = (value) => typeof value === "string" && value !== "";
26383
26440
  const isEmittableString = ({ value }) => isNonEmptyString(value);
26384
- const isEmittableArray = ({ value }) => isStringArray$1(value);
26441
+ const isEmittableArray = ({ value }) => isStringArray(value);
26385
26442
  const isEmittableRecord = ({ value }) => isSafeStringRecord(value);
26386
26443
  const isImportableArray = ({ value }) => isSafeStringArray(value);
26387
26444
  const isEmittableObject = ({ value }) => isPlainObject$1(value);
@@ -26823,7 +26880,7 @@ function isStringRecord(value) {
26823
26880
  if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
26824
26881
  return Object.values(value).every((v) => typeof v === "string");
26825
26882
  }
26826
- function isStringArray$1(value) {
26883
+ function isStringArray(value) {
26827
26884
  return Array.isArray(value) && value.every((v) => typeof v === "string");
26828
26885
  }
26829
26886
  /** Compare object values without letting key order decide the answer. */
@@ -26868,7 +26925,7 @@ function isSafeEnvEntry(entry) {
26868
26925
  * skipped wholesale when that read fails.
26869
26926
  */
26870
26927
  function isSafeStringArray(value) {
26871
- return isStringArray$1(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
26928
+ return isStringArray(value) && value.every((entry) => !CONTROL_CHARS.some((char) => entry.includes(char)));
26872
26929
  }
26873
26930
  /**
26874
26931
  * A raw string kept only if the canonical field it would land in accepts it,
@@ -26910,7 +26967,7 @@ function importTypePayloadFields({ h, hookType, warn }) {
26910
26967
  return {
26911
26968
  ...url !== void 0 && { url },
26912
26969
  ...isStringRecord(headers) && { headers },
26913
- ...isStringArray$1(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
26970
+ ...isStringArray(h.allowedEnvVars) && { allowedEnvVars: h.allowedEnvVars }
26914
26971
  };
26915
26972
  }
26916
26973
  if (hookType === "mcp_tool") {
@@ -28050,8 +28107,34 @@ function collectOwnedRefs({ generatedHooks, shape }) {
28050
28107
  }
28051
28108
  return owned;
28052
28109
  }
28110
+ /**
28111
+ * The merge keeps every unowned existing handler where it is. Some tools key
28112
+ * state by position — Codex CLI persists a hook's trust review under its event,
28113
+ * matcher group index and handler index — so moving a handler the user already
28114
+ * reviewed would silently change which hook that review belongs to. Hence:
28115
+ * - an unowned existing handler stays at its index;
28116
+ * - a newly generated handler is appended to the first existing group with its
28117
+ * matcher and group-level keys, or in a new group after the existing ones;
28118
+ * - within a group, the slots rulesync's own handlers occupy (including the
28119
+ * appended ones) are filled in generated order, so reordering the sources
28120
+ * still reorders the generated handlers, which matters wherever handlers
28121
+ * run in order (a Qwen Code `sequential` group, a Cursor list).
28122
+ *
28123
+ * A retracted handler, or a group left empty by retractions, is still removed,
28124
+ * which shifts whatever follows it in the same list; that is unavoidable
28125
+ * without leaving a placeholder behind.
28126
+ */
28053
28127
  function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned, logger }) {
28054
- const merged = [...generated];
28128
+ const pending = createPendingGenerated();
28129
+ for (const group of generated) if (isMatcherGroup(group)) addPending({
28130
+ pending,
28131
+ handlers: group.hooks,
28132
+ event,
28133
+ matcher: matcherKey(group.matcher),
28134
+ attributes: groupAttributesKey(group)
28135
+ });
28136
+ const merged = [];
28137
+ const homes = /* @__PURE__ */ new Map();
28055
28138
  for (const group of existing) {
28056
28139
  if (!isMatcherGroup(group)) {
28057
28140
  warnSkip({
@@ -28062,26 +28145,57 @@ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned,
28062
28145
  continue;
28063
28146
  }
28064
28147
  const matcher = matcherKey(group.matcher);
28065
- const preserved = group.hooks.filter((handler) => shouldPreserve({
28066
- handler,
28148
+ const hooks = keepExistingHandlers({
28149
+ handlers: group.hooks,
28067
28150
  event,
28068
28151
  matcher,
28152
+ attributes: groupAttributesKey(group),
28153
+ pending,
28069
28154
  seen,
28070
28155
  previouslyOwned,
28071
28156
  logger
28072
- })).map((handler) => cloneHandler(handler));
28073
- if (preserved.length === 0) continue;
28074
- const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher);
28075
- if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
28076
- else merged.push({
28157
+ });
28158
+ if (hooks.length === 0) continue;
28159
+ const kept = {
28077
28160
  ...omitPrototypePollutionKeys(group),
28078
- hooks: preserved
28161
+ hooks
28162
+ };
28163
+ merged.push(kept);
28164
+ const home = groupSlotKey(group);
28165
+ if (!homes.has(home)) homes.set(home, kept);
28166
+ }
28167
+ for (const group of generated) {
28168
+ if (!isMatcherGroup(group)) {
28169
+ merged.push(group);
28170
+ continue;
28171
+ }
28172
+ const remaining = group.hooks.filter((handler) => !pending.placed.has(handler));
28173
+ if (remaining.length === 0) continue;
28174
+ const home = homes.get(groupSlotKey(group));
28175
+ if (home === void 0) merged.push({
28176
+ ...group,
28177
+ hooks: remaining
28079
28178
  });
28179
+ else home.hooks.push(...remaining);
28080
28180
  }
28181
+ const order = generatedOrder(generated.flatMap((group) => isMatcherGroup(group) ? group.hooks : []));
28182
+ for (const group of merged) if (isMatcherGroup(group)) orderGeneratedSlots({
28183
+ handlers: group.hooks,
28184
+ order
28185
+ });
28081
28186
  return merged;
28082
28187
  }
28188
+ /** The flat-list counterpart of {@link mergeMatcherGroups}, with the same positional rules. */
28083
28189
  function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned, logger }) {
28084
- const preserved = [];
28190
+ const pending = createPendingGenerated();
28191
+ addPending({
28192
+ pending,
28193
+ handlers: generated,
28194
+ event,
28195
+ matcher: void 0,
28196
+ attributes: ""
28197
+ });
28198
+ const flat = [];
28085
28199
  for (const handler of existing) {
28086
28200
  if (isMatcherGroup(handler)) {
28087
28201
  warnSkip({
@@ -28091,17 +28205,93 @@ function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned,
28091
28205
  });
28092
28206
  continue;
28093
28207
  }
28094
- if (!shouldPreserve({
28208
+ flat.push(handler);
28209
+ }
28210
+ const merged = keepExistingHandlers({
28211
+ handlers: flat,
28212
+ event,
28213
+ matcher: void 0,
28214
+ attributes: "",
28215
+ pending,
28216
+ seen,
28217
+ previouslyOwned,
28218
+ logger
28219
+ });
28220
+ merged.push(...generated.filter((handler) => !pending.placed.has(handler)));
28221
+ orderGeneratedSlots({
28222
+ handlers: merged,
28223
+ order: generatedOrder(generated)
28224
+ });
28225
+ return merged;
28226
+ }
28227
+ /** Each generated handler's position in the generated list, by object identity. */
28228
+ function generatedOrder(handlers) {
28229
+ const order = /* @__PURE__ */ new Map();
28230
+ for (const [index, handler] of handlers.entries()) if (isPlainObject$1(handler)) order.set(handler, index);
28231
+ return order;
28232
+ }
28233
+ /**
28234
+ * Rewrite, in place, the slots of `handlers` that hold generated handlers so
28235
+ * they follow the generated order. Every other slot — an unowned handler — is
28236
+ * left at its index.
28237
+ */
28238
+ function orderGeneratedSlots({ handlers, order }) {
28239
+ const slots = [];
28240
+ for (const [index, handler] of handlers.entries()) if (order.has(handler)) slots.push(index);
28241
+ const sorted = slots.map((index) => handlers[index]).toSorted((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
28242
+ for (const [position, index] of slots.entries()) handlers[index] = sorted[position];
28243
+ }
28244
+ function createPendingGenerated() {
28245
+ return {
28246
+ byKey: /* @__PURE__ */ new Map(),
28247
+ placed: /* @__PURE__ */ new Set()
28248
+ };
28249
+ }
28250
+ function addPending({ pending, handlers, event, matcher, attributes }) {
28251
+ for (const handler of handlers) {
28252
+ if (!isPlainObject$1(handler)) continue;
28253
+ const key = pendingKey({
28254
+ event,
28255
+ matcher,
28256
+ attributes,
28257
+ handler
28258
+ });
28259
+ const queue = pending.byKey.get(key);
28260
+ if (queue === void 0) pending.byKey.set(key, [handler]);
28261
+ else queue.push(handler);
28262
+ }
28263
+ }
28264
+ /**
28265
+ * Walk one existing list in order and decide each handler: replaced in place
28266
+ * by its generated copy, kept as unowned, or dropped (retracted or duplicate).
28267
+ */
28268
+ function keepExistingHandlers({ handlers, event, matcher, attributes, pending, seen, previouslyOwned, logger }) {
28269
+ const kept = [];
28270
+ for (const handler of handlers) {
28271
+ if (isPlainObject$1(handler)) {
28272
+ const key = pendingKey({
28273
+ event,
28274
+ matcher,
28275
+ attributes,
28276
+ handler
28277
+ });
28278
+ const generated = pending.byKey.get(key)?.shift();
28279
+ if (generated !== void 0) {
28280
+ pending.placed.add(generated);
28281
+ kept.push(generated);
28282
+ continue;
28283
+ }
28284
+ }
28285
+ if (shouldPreserve({
28095
28286
  handler,
28096
28287
  event,
28097
- matcher: void 0,
28288
+ matcher,
28098
28289
  seen,
28099
28290
  previouslyOwned,
28100
28291
  logger
28101
- })) continue;
28102
- preserved.push(cloneHandler(handler));
28292
+ })) kept.push(cloneHandler(handler));
28103
28293
  }
28104
- return [...generated, ...preserved];
28294
+ return kept;
28105
28295
  }
28106
28296
  function isMatcherGroup(value) {
28107
28297
  return isPlainObject$1(value) && Array.isArray(value.hooks);
@@ -28192,6 +28382,36 @@ function stableStringify$1(value) {
28192
28382
  function matcherKey(matcher) {
28193
28383
  return matcher === void 0 ? "" : stableStringify$1(matcher);
28194
28384
  }
28385
+ function pendingKey({ event, matcher, attributes, handler }) {
28386
+ const key = ownedHookKey({
28387
+ event,
28388
+ matcher,
28389
+ identity: handlerIdentity(handler)
28390
+ });
28391
+ return JSON.stringify([key, attributes]);
28392
+ }
28393
+ /** Groups that share a matcher and attributes are interchangeable homes for a handler. */
28394
+ function groupSlotKey(group) {
28395
+ return JSON.stringify([matcherKey(group.matcher), groupAttributesKey(group)]);
28396
+ }
28397
+ /** Group-level keys whose default value is known, so an explicit default can be ignored. */
28398
+ const GROUP_ATTRIBUTE_DEFAULTS = Object.freeze({
28399
+ __proto__: null,
28400
+ sequential: false
28401
+ });
28402
+ /**
28403
+ * The stable form of a matcher group's keys other than `matcher` and `hooks`.
28404
+ *
28405
+ * A key set to its known default is the same group as one that omits it:
28406
+ * Qwen Code's `sequential` defaults to `false` and rulesync emits it only when
28407
+ * `true`, so an explicit `sequential: false` (or any `undefined` key) is
28408
+ * dropped before comparing. Other `false` values are kept, since their default
28409
+ * is not known here.
28410
+ */
28411
+ function groupAttributesKey(group) {
28412
+ const { matcher: _matcher, hooks: _hooks, ...attributes } = group;
28413
+ return stableStringify$1(Object.fromEntries(Object.entries(attributes).filter(([key, value]) => value !== void 0 && GROUP_ATTRIBUTE_DEFAULTS[key] !== value)));
28414
+ }
28195
28415
  function warnSkip({ logger, event, expected }) {
28196
28416
  logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
28197
28417
  }
@@ -28687,7 +28907,7 @@ const MANAGED_EVENT_NAMES = new Set(Object.values(CANONICAL_TO_CLINE_EVENT_NAMES
28687
28907
  function parseManifest(fileContent) {
28688
28908
  try {
28689
28909
  const parsed = JSON.parse(fileContent);
28690
- if (!isRecord$1(parsed) || !isStringArray$2(parsed.events)) return null;
28910
+ if (!isRecord$1(parsed) || !isStringArray$1(parsed.events)) return null;
28691
28911
  return {
28692
28912
  generatedBy: "rulesync",
28693
28913
  events: parsed.events.filter((event) => MANAGED_EVENT_NAMES.has(event))
@@ -30225,7 +30445,7 @@ function copilotCliHooksToCanonical(rawHooks, logger) {
30225
30445
  * see the CLI config dir reference:
30226
30446
  * https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
30227
30447
  */
30228
- function copilotCliSettingsPaths({ global }) {
30448
+ function copilotCliSettingsPaths$1({ global }) {
30229
30449
  return global ? {
30230
30450
  relativeDirPath: COPILOT_DIR,
30231
30451
  relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
@@ -30240,7 +30460,7 @@ function copilotCliSettingsPaths({ global }) {
30240
30460
  * subject, and the permissions feature reports a broken settings file itself.
30241
30461
  */
30242
30462
  async function readDisableAllHooks({ outputRoot, global }) {
30243
- const paths = copilotCliSettingsPaths({ global });
30463
+ const paths = copilotCliSettingsPaths$1({ global });
30244
30464
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath));
30245
30465
  if (fileContent === null) return;
30246
30466
  try {
@@ -30270,7 +30490,7 @@ var CopilotcliHooksSettings = class CopilotcliHooksSettings extends ToolFile {
30270
30490
  return false;
30271
30491
  }
30272
30492
  static async fromDisableAllHooks({ outputRoot = process.cwd(), global = false, disableAllHooks }) {
30273
- const paths = copilotCliSettingsPaths({ global });
30493
+ const paths = copilotCliSettingsPaths$1({ global });
30274
30494
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
30275
30495
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
30276
30496
  return new CopilotcliHooksSettings({
@@ -30300,7 +30520,7 @@ var CopilotcliHooks = class CopilotcliHooks extends ToolHooks {
30300
30520
  this.disableAllHooks = disableAllHooks;
30301
30521
  }
30302
30522
  static getExtraSharedWritePaths({ global = false } = {}) {
30303
- return [copilotCliSettingsPaths({ global })];
30523
+ return [copilotCliSettingsPaths$1({ global })];
30304
30524
  }
30305
30525
  static async getAuxiliaryFiles({ toolHooks } = {}) {
30306
30526
  return toolHooks instanceof CopilotcliHooks && toolHooks.settingsFile ? [toolHooks.settingsFile] : [];
@@ -34744,12 +34964,21 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
34744
34964
  validate
34745
34965
  });
34746
34966
  }
34747
- static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, logger }) {
34967
+ static async fromRulesyncHooks({ outputRoot = process.cwd(), rulesyncHooks, validate = true, global = false, preserveUnowned = false, logger }) {
34748
34968
  const paths = QwencodeHooks.getSettablePaths({ global });
34749
34969
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
34750
34970
  const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
34751
34971
  const config = rulesyncHooks.getJson();
34752
- const patch = { hooks: canonicalToQwencodeHooks(config, logger) };
34972
+ const previouslyOwned = preserveUnowned ? parseHooksOwnershipLock(await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, HOOKS_OWNERSHIP_LOCK_FILE_NAME))) : void 0;
34973
+ const merged = mergeGeneratedHookLists({
34974
+ existingContent,
34975
+ generatedHooks: canonicalToQwencodeHooks(config, logger),
34976
+ shape: "matcher-groups",
34977
+ preserveUnowned,
34978
+ previouslyOwned,
34979
+ logger
34980
+ });
34981
+ const patch = { hooks: merged.hooks };
34753
34982
  const disableAllHooks = config.qwencode?.disableAllHooks;
34754
34983
  if (typeof disableAllHooks === "boolean") patch.disableAllHooks = disableAllHooks;
34755
34984
  const fileContent = applySharedConfigPatch({
@@ -34764,9 +34993,16 @@ var QwencodeHooks = class QwencodeHooks extends ToolHooks {
34764
34993
  relativeDirPath: paths.relativeDirPath,
34765
34994
  relativeFilePath: paths.relativeFilePath,
34766
34995
  fileContent,
34996
+ ownedHookRefs: preserveUnowned ? merged.owned : void 0,
34767
34997
  validate
34768
34998
  });
34769
34999
  }
35000
+ static supportsPreserveUnowned() {
35001
+ return true;
35002
+ }
35003
+ static async getAuxiliaryFiles({ toolHooks } = {}) {
35004
+ return toolHooks instanceof QwencodeHooks ? toolHooks.getOwnershipLockFiles() : [];
35005
+ }
34770
35006
  toRulesyncHooks() {
34771
35007
  let settings;
34772
35008
  try {
@@ -41166,7 +41402,7 @@ function isCodexStdioServer(config) {
41166
41402
  * @see https://learn.chatgpt.com/docs/extend/mcp
41167
41403
  */
41168
41404
  function isValidRenamedArray(key, value) {
41169
- return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$2(value);
41405
+ return key === "env_vars" || key === "envVars" ? isEnvVarEntryArray(value) : isStringArray$1(value);
41170
41406
  }
41171
41407
  /**
41172
41408
  * The keys of Codex's `McpServerOAuthConfig`. Anything else in a server's
@@ -42385,18 +42621,6 @@ function renameEnabledToolsToTools({ server, serverName, logger }) {
42385
42621
  };
42386
42622
  }
42387
42623
  /**
42388
- * Copilot CLI's `tools` accepts more than the canonical `enabledTools` shape:
42389
- * `--tools` documents `*`, a comma-separated list, or `""`, so a hand-written or
42390
- * CLI-written entry can be a bare string. Canonical `enabledTools` is
42391
- * `string[]`, and `RulesyncMcp` throws on a schema failure — carrying a string
42392
- * through would reject the WHOLE imported MCP file rather than one key. Anything
42393
- * that is not a string array is therefore left under `tools` untouched, matching
42394
- * `codexcli-mcp.ts`'s `isValidRenamedArray` guard on the same class of rename.
42395
- */
42396
- function isStringArray(value) {
42397
- return Array.isArray(value) && value.every((entry) => typeof entry === "string");
42398
- }
42399
- /**
42400
42624
  * Resolves and sets the transport type for each MCP server config.
42401
42625
  * GitHub Copilot CLI requires the "type" field for each server. A server it
42402
42626
  * cannot express — one that names no transport, or names one it carries no way
@@ -42438,7 +42662,7 @@ function addTypeField(mcpServers, logger) {
42438
42662
  });
42439
42663
  continue;
42440
42664
  }
42441
- const { httpUrl: _httpUrl, ...rest } = parsed;
42665
+ const { httpUrl: _httpUrl, disabled: _disabled, ...rest } = parsed;
42442
42666
  result[name] = {
42443
42667
  ...renameEnabledToolsToTools({
42444
42668
  server: rest,
@@ -42450,7 +42674,7 @@ function addTypeField(mcpServers, logger) {
42450
42674
  };
42451
42675
  continue;
42452
42676
  }
42453
- const { httpUrl: _localHttpUrl, ...local } = parsed;
42677
+ const { httpUrl: _localHttpUrl, disabled: _localDisabled, ...local } = parsed;
42454
42678
  const [head, ...tail] = resolveLocalMcpCommand(parsed);
42455
42679
  if (head === void 0) {
42456
42680
  warnAndSkipMcpServer({
@@ -42481,7 +42705,7 @@ function removeTypeField(config) {
42481
42705
  const result = {};
42482
42706
  for (const [name, server] of Object.entries(config.mcpServers ?? {})) {
42483
42707
  const { tools, ...withoutTools } = server;
42484
- const restored = isStringArray(tools) ? {
42708
+ const restored = isStringArray$1(tools) ? {
42485
42709
  ...withoutTools,
42486
42710
  enabledTools: tools
42487
42711
  } : {
@@ -42497,6 +42721,99 @@ function removeTypeField(config) {
42497
42721
  }
42498
42722
  return result;
42499
42723
  }
42724
+ /**
42725
+ * The Copilot CLI settings file carrying `disabledMcpServers`:
42726
+ * `.github/copilot/settings.json` (project) / `~/.copilot/settings.json`
42727
+ * (global). Both scopes document the key ("MCP server names to disable. Listed
42728
+ * servers are configured but not started"); at repository scope it is
42729
+ * union-merged with the user value, so a repository can add names but never
42730
+ * remove them.
42731
+ *
42732
+ * @see https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
42733
+ */
42734
+ function copilotCliSettingsPaths({ global }) {
42735
+ return global ? {
42736
+ relativeDirPath: COPILOT_DIR,
42737
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
42738
+ } : {
42739
+ relativeDirPath: COPILOTCLI_PROJECT_SETTINGS_DIR_PATH,
42740
+ relativeFilePath: COPILOTCLI_SETTINGS_FILE_NAME
42741
+ };
42742
+ }
42743
+ async function readCopilotCliSettings({ outputRoot, global }) {
42744
+ const paths = copilotCliSettingsPaths({ global });
42745
+ const existingContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? "";
42746
+ try {
42747
+ return {
42748
+ paths,
42749
+ existingContent,
42750
+ document: parseSharedConfig({
42751
+ format: "json",
42752
+ fileContent: existingContent,
42753
+ filePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath),
42754
+ invalidRootPolicy: "error"
42755
+ })
42756
+ };
42757
+ } catch (error) {
42758
+ return {
42759
+ paths,
42760
+ existingContent,
42761
+ document: null,
42762
+ error
42763
+ };
42764
+ }
42765
+ }
42766
+ /**
42767
+ * The names of the servers rulesync actually writes for this target: one
42768
+ * `addTypeField` skips (no transport, WebSocket, missing url or command) is
42769
+ * not managed, so its name neither gains a toggle nor strips a same-named
42770
+ * user-authored entry from the settings file.
42771
+ */
42772
+ function managedServerNames(mcpServers) {
42773
+ return Object.keys(addTypeField(mcpServers) ?? {});
42774
+ }
42775
+ /**
42776
+ * Overlays `disabled: true` on the servers the scope's settings file lists in
42777
+ * `disabledMcpServers`, so import round-trips the toggle into the canonical
42778
+ * config. A settings file that exists but cannot be parsed throws
42779
+ * (fail-closed): importing past it would re-enable every disabled server.
42780
+ */
42781
+ async function overlayDisabledServers({ mcpServers, outputRoot, global }) {
42782
+ const settings = await readCopilotCliSettings({
42783
+ outputRoot,
42784
+ global
42785
+ });
42786
+ if (settings.document === null) throw new Error(`Cannot import Copilot CLI MCP servers: ${formatError(settings.error)}. Fix the file so disabledMcpServers can be read.`, { cause: settings.error });
42787
+ const disabled = settings.document.disabledMcpServers;
42788
+ if (!isStringArray$1(disabled) || disabled.length === 0) return mcpServers;
42789
+ const servers = { ...mcpServers };
42790
+ for (const name of disabled) {
42791
+ if (!Object.hasOwn(servers, name)) continue;
42792
+ const server = servers[name];
42793
+ if (isPlainObject$1(server)) servers[name] = {
42794
+ ...server,
42795
+ disabled: true
42796
+ };
42797
+ }
42798
+ return servers;
42799
+ }
42800
+ /**
42801
+ * Auxiliary writer for `disabledMcpServers` in the Copilot CLI settings file.
42802
+ * Only that key is owned (through the shared-config gateway), so `deniedUrls`,
42803
+ * `disableAllHooks`, `model` and every other setting are preserved. It is a
42804
+ * shared, user-owned file and is never deleted.
42805
+ */
42806
+ var CopilotcliMcpSettings = class extends ToolFile {
42807
+ validate() {
42808
+ return {
42809
+ success: true,
42810
+ error: null
42811
+ };
42812
+ }
42813
+ isDeletable() {
42814
+ return false;
42815
+ }
42816
+ };
42500
42817
  var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42501
42818
  json;
42502
42819
  constructor(params) {
@@ -42539,7 +42856,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42539
42856
  const json = JSON.parse(fileContent);
42540
42857
  const newJson = {
42541
42858
  ...json,
42542
- mcpServers: json.mcpServers ?? {}
42859
+ mcpServers: await overlayDisabledServers({
42860
+ mcpServers: json.mcpServers ?? {},
42861
+ outputRoot,
42862
+ global
42863
+ })
42543
42864
  };
42544
42865
  return new CopilotcliMcp({
42545
42866
  outputRoot,
@@ -42554,7 +42875,17 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42554
42875
  const paths = this.getSettablePaths({ global });
42555
42876
  const fileContent = await readFileContentOrNull((0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath)) ?? JSON.stringify({ mcpServers: {} }, null, 2);
42556
42877
  const json = JSON.parse(fileContent);
42557
- const copilotCliMcpServers = addTypeField(rulesyncMcp.getMcpServers(), logger);
42878
+ const settings = await readCopilotCliSettings({
42879
+ outputRoot,
42880
+ global
42881
+ });
42882
+ const existingDisabled = settings.document?.disabledMcpServers;
42883
+ const offSwitchBlocker = settings.document === null ? "cannot be parsed" : existingDisabled !== void 0 && !isStringArray$1(existingDisabled) ? "has a disabledMcpServers value that is not an array of server names" : void 0;
42884
+ const copilotCliMcpServers = addTypeField(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).filter(([name, server]) => {
42885
+ if (server.disabled !== true || offSwitchBlocker === void 0) return true;
42886
+ logger?.warn(`[CopilotcliMcp] Skipping disabled MCP server "${name}": ${(0, node_path.join)(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} ${offSwitchBlocker}, so disabledMcpServers cannot be written to switch it off.`);
42887
+ return false;
42888
+ })), logger);
42558
42889
  const mcpJson = {
42559
42890
  ...json,
42560
42891
  mcpServers: copilotCliMcpServers
@@ -42568,6 +42899,66 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42568
42899
  global
42569
42900
  });
42570
42901
  }
42902
+ /**
42903
+ * `disabledMcpServers` lives in the Copilot CLI settings file the hooks and
42904
+ * permissions features also write. Declared here so the write-order
42905
+ * derivation sees this feature as one of that file's writers — it is not a
42906
+ * settable path, since the servers themselves live in the MCP file.
42907
+ */
42908
+ static getExtraSharedWritePaths({ global = false } = {}) {
42909
+ return [copilotCliSettingsPaths({ global })];
42910
+ }
42911
+ /**
42912
+ * Routes the canonical `disabled: true` to `disabledMcpServers`, the key
42913
+ * Copilot CLI actually consults: an MCP file entry has no `disabled` field.
42914
+ * rulesync owns the toggle for the servers it writes; names it does not
42915
+ * manage keep their existing state. The file is not created or touched when
42916
+ * there is nothing to disable and nothing to retract, nor when the existing
42917
+ * `disabledMcpServers` is not a string array (left as is, with a warning).
42918
+ */
42919
+ static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
42920
+ const servers = rulesyncMcp.forTarget({
42921
+ toolTarget: "copilotcli",
42922
+ global
42923
+ }).getMcpServers();
42924
+ const managedNames = managedServerNames(servers);
42925
+ const disabledNames = managedNames.filter((name) => servers[name]?.disabled === true);
42926
+ const settings = await readCopilotCliSettings({
42927
+ outputRoot,
42928
+ global
42929
+ });
42930
+ if (settings.document === null) {
42931
+ logger?.warn(`Skipping the Copilot CLI disabledMcpServers update: ${formatError(settings.error)}`);
42932
+ return [];
42933
+ }
42934
+ const existing = settings.document.disabledMcpServers;
42935
+ if (existing !== void 0 && !isStringArray$1(existing)) {
42936
+ logger?.warn(`Skipping the Copilot CLI disabledMcpServers update: the existing value is not an array of server names. Fix it in ${(0, node_path.join)(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} so rulesync can manage the toggle.`);
42937
+ return [];
42938
+ }
42939
+ const existingDisabled = existing ?? [];
42940
+ const managedNameSet = new Set(managedNames);
42941
+ const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
42942
+ if (reEnabled.length > 0) logger?.warn(`Copilot CLI MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
42943
+ const mergedDisabled = [...existingDisabled.filter((name) => !managedNameSet.has(name)), ...disabledNames].toSorted();
42944
+ if (mergedDisabled.length === 0 && existing === void 0) return [];
42945
+ if (isStringArray$1(existing) && existing.length === mergedDisabled.length && existing.toSorted().every((name, index) => name === mergedDisabled[index])) return [];
42946
+ const { paths } = settings;
42947
+ const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
42948
+ return [new CopilotcliMcpSettings({
42949
+ outputRoot,
42950
+ relativeDirPath: paths.relativeDirPath,
42951
+ relativeFilePath: paths.relativeFilePath,
42952
+ fileContent: applySharedConfigPatch({
42953
+ fileKey: sharedConfigFileKey(paths),
42954
+ feature: "mcp",
42955
+ existingContent: settings.existingContent,
42956
+ patch: { disabledMcpServers: mergedDisabled.length > 0 ? mergedDisabled : void 0 },
42957
+ filePath
42958
+ }),
42959
+ global
42960
+ })];
42961
+ }
42571
42962
  toRulesyncMcp() {
42572
42963
  const mcpServers = removeTypeField(this.json);
42573
42964
  return this.toRulesyncMcpDefault({ fileContent: JSON.stringify({ mcpServers }, null, 2) });
@@ -42999,10 +43390,10 @@ function convertFromCrushFormat(crushServers) {
42999
43390
  if (value === "http" || value === "sse") converted.type = value;
43000
43391
  break;
43001
43392
  case "disabled_tools":
43002
- if (isStringArray$2(value)) converted.disabledTools = value;
43393
+ if (isStringArray$1(value)) converted.disabledTools = value;
43003
43394
  break;
43004
43395
  case "enabled_tools":
43005
- if (isStringArray$2(value)) converted.enabledTools = value;
43396
+ if (isStringArray$1(value)) converted.enabledTools = value;
43006
43397
  break;
43007
43398
  case "oauth_token": break;
43008
43399
  default: {
@@ -43989,7 +44380,7 @@ function convertRowsFromDsh(rows, logger) {
43989
44380
  } else {
43990
44381
  server.type = "stdio";
43991
44382
  if (typeof config.command === "string") server.command = config.command;
43992
- if (isStringArray$2(config.args)) server.args = config.args;
44383
+ if (isStringArray$1(config.args)) server.args = config.args;
43993
44384
  const env = copyStringRecord(config.env);
43994
44385
  if (env !== void 0) server.env = env;
43995
44386
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -44420,11 +44811,11 @@ function applyGooseStdioFields(ext, config) {
44420
44811
  if (Array.isArray(command)) {
44421
44812
  if (typeof command[0] === "string") ext.cmd = command[0];
44422
44813
  const rest = command.slice(1).filter((c) => typeof c === "string");
44423
- const args = isStringArray$2(config.args) ? config.args : [];
44814
+ const args = isStringArray$1(config.args) ? config.args : [];
44424
44815
  if (rest.length > 0 || args.length > 0) ext.args = [...rest, ...args];
44425
44816
  } else if (typeof command === "string") {
44426
44817
  ext.cmd = command;
44427
- if (isStringArray$2(config.args)) ext.args = config.args;
44818
+ if (isStringArray$1(config.args)) ext.args = config.args;
44428
44819
  }
44429
44820
  if (isPlainObject$1(config.env)) ext.envs = omitPrototypePollutionKeys(config.env);
44430
44821
  if (typeof config.cwd === "string") ext.cwd = config.cwd;
@@ -44527,7 +44918,7 @@ function convertFromGooseFormat(extensions) {
44527
44918
  else if (type === "streamable_http") server.type = "http";
44528
44919
  else server.type = "stdio";
44529
44920
  if (typeof ext.cmd === "string") server.command = ext.cmd;
44530
- if (isStringArray$2(ext.args)) server.args = ext.args;
44921
+ if (isStringArray$1(ext.args)) server.args = ext.args;
44531
44922
  if (isPlainObject$1(ext.envs)) server.env = omitPrototypePollutionKeys(ext.envs);
44532
44923
  if (typeof ext.cwd === "string") server.cwd = ext.cwd;
44533
44924
  if (typeof ext.uri === "string") server.url = ext.uri;
@@ -44549,11 +44940,11 @@ function buildGoosePluginStdioServer(config) {
44549
44940
  if (Array.isArray(command)) {
44550
44941
  if (typeof command[0] === "string") server.command = command[0];
44551
44942
  const rest = command.slice(1).filter((c) => typeof c === "string");
44552
- const args = isStringArray$2(config.args) ? config.args : [];
44943
+ const args = isStringArray$1(config.args) ? config.args : [];
44553
44944
  if (rest.length > 0 || args.length > 0) server.args = [...rest, ...args];
44554
44945
  } else if (typeof command === "string") {
44555
44946
  server.command = command;
44556
- if (isStringArray$2(config.args)) server.args = config.args;
44947
+ if (isStringArray$1(config.args)) server.args = config.args;
44557
44948
  }
44558
44949
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
44559
44950
  if (typeof config.cwd === "string") server.cwd = config.cwd;
@@ -44961,7 +45352,7 @@ function copyHermesOauth(source) {
44961
45352
  if (!isRecord$1(source)) return;
44962
45353
  const oauth = {};
44963
45354
  for (const key of HERMES_OAUTH_STRING_KEYS) if (typeof source[key] === "string") oauth[key] = source[key];
44964
- if (typeof oauth.scope !== "string" && isStringArray$2(source.scopes)) oauth.scope = source.scopes.join(" ");
45355
+ if (typeof oauth.scope !== "string" && isStringArray$1(source.scopes)) oauth.scope = source.scopes.join(" ");
44965
45356
  for (const key of ["redirect_port", "timeout"]) if (typeof source[key] === "number") oauth[key] = source[key];
44966
45357
  if (typeof source.cimd === "boolean") oauth.cimd = source.cimd;
44967
45358
  return Object.keys(oauth).length > 0 ? oauth : void 0;
@@ -44988,7 +45379,7 @@ function copyHermesAdvancedFields(source, target) {
44988
45379
  target.auth = source.auth;
44989
45380
  copied = true;
44990
45381
  }
44991
- if (typeof source.client_cert === "string" || isStringArray$2(source.client_cert)) {
45382
+ if (typeof source.client_cert === "string" || isStringArray$1(source.client_cert)) {
44992
45383
  target.client_cert = source.client_cert;
44993
45384
  copied = true;
44994
45385
  }
@@ -45060,8 +45451,8 @@ function copyHermesAdvancedFields(source, target) {
45060
45451
  */
45061
45452
  function buildHermesToolsBlock(config) {
45062
45453
  const tools = {};
45063
- if (isStringArray$2(config.enabledTools)) tools.include = config.enabledTools;
45064
- if (isStringArray$2(config.disabledTools)) tools.exclude = config.disabledTools;
45454
+ if (isStringArray$1(config.enabledTools)) tools.include = config.enabledTools;
45455
+ if (isStringArray$1(config.disabledTools)) tools.exclude = config.disabledTools;
45065
45456
  if (typeof config.promptsEnabled === "boolean") tools.prompts = config.promptsEnabled;
45066
45457
  if (typeof config.resourcesEnabled === "boolean") tools.resources = config.resourcesEnabled;
45067
45458
  return tools;
@@ -45073,8 +45464,8 @@ function buildHermesToolsBlock(config) {
45073
45464
  * `promptsEnabled`/`resourcesEnabled` top-level toggles.
45074
45465
  */
45075
45466
  function applyHermesToolsBlock(hermesTools, server) {
45076
- if (isStringArray$2(hermesTools.include)) server.enabledTools = hermesTools.include;
45077
- if (isStringArray$2(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
45467
+ if (isStringArray$1(hermesTools.include)) server.enabledTools = hermesTools.include;
45468
+ if (isStringArray$1(hermesTools.exclude)) server.disabledTools = hermesTools.exclude;
45078
45469
  if (typeof hermesTools.prompts === "boolean") server.promptsEnabled = hermesTools.prompts;
45079
45470
  if (typeof hermesTools.resources === "boolean") server.resourcesEnabled = hermesTools.resources;
45080
45471
  }
@@ -45100,11 +45491,11 @@ function convertServerToHermes(config) {
45100
45491
  if (Array.isArray(command)) {
45101
45492
  if (typeof command[0] === "string") out.command = command[0];
45102
45493
  const rest = command.slice(1).filter((c) => typeof c === "string");
45103
- const args = isStringArray$2(config.args) ? config.args : [];
45494
+ const args = isStringArray$1(config.args) ? config.args : [];
45104
45495
  if (rest.length > 0 || args.length > 0) out.args = [...rest, ...args];
45105
45496
  } else if (typeof command === "string") {
45106
45497
  out.command = command;
45107
- if (isStringArray$2(config.args)) out.args = config.args;
45498
+ if (isStringArray$1(config.args)) out.args = config.args;
45108
45499
  }
45109
45500
  if (isPlainObject$1(config.env)) out.env = omitPrototypePollutionKeys(config.env);
45110
45501
  } else if (url !== void 0) {
@@ -45154,7 +45545,7 @@ function convertFromHermesFormat(mcpServers) {
45154
45545
  if (PROTOTYPE_POLLUTION_KEYS.has(name) || !isRecord$1(config)) continue;
45155
45546
  const server = {};
45156
45547
  if (typeof config.command === "string") server.command = config.command;
45157
- if (isStringArray$2(config.args)) server.args = config.args;
45548
+ if (isStringArray$1(config.args)) server.args = config.args;
45158
45549
  if (isPlainObject$1(config.env)) server.env = omitPrototypePollutionKeys(config.env);
45159
45550
  if (typeof config.url === "string") server.url = config.url;
45160
45551
  if (isPlainObject$1(config.headers)) server.headers = omitPrototypePollutionKeys(config.headers);
@@ -46346,7 +46737,7 @@ function mergeToolLists(...lists) {
46346
46737
  function toKiroMcpServers(servers) {
46347
46738
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46348
46739
  const { kiroAutoApprove, kiroAutoBlock, disabledTools, ...rest } = server;
46349
- const autoApprove = mergeToolLists(isStringArray$2(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
46740
+ const autoApprove = mergeToolLists(isStringArray$1(rest.autoApprove) ? rest.autoApprove : void 0, kiroAutoApprove);
46350
46741
  const disabled = mergeToolLists(disabledTools, kiroAutoBlock);
46351
46742
  return [name, {
46352
46743
  ...rest,
@@ -46370,7 +46761,7 @@ function fromKiroMcpServers(servers) {
46370
46761
  return Object.fromEntries(Object.entries(servers).map(([name, server]) => {
46371
46762
  if (server === null || typeof server !== "object" || Array.isArray(server)) return [name, server];
46372
46763
  const { autoApprove, ...rest } = server;
46373
- if (!isStringArray$2(autoApprove)) return [name, server];
46764
+ if (!isStringArray$1(autoApprove)) return [name, server];
46374
46765
  return [name, {
46375
46766
  ...rest,
46376
46767
  kiroAutoApprove: autoApprove
@@ -47799,7 +48190,7 @@ function headersRecordToPoolList(headers) {
47799
48190
  return Object.entries(headers).map(([name, value]) => `${name}: ${value}`);
47800
48191
  }
47801
48192
  function poolHeadersListToRecord(serverName, headers) {
47802
- if (!isStringArray$2(headers)) return void 0;
48193
+ if (!isStringArray$1(headers)) return void 0;
47803
48194
  const result = {};
47804
48195
  for (const entry of headers) {
47805
48196
  const separator = entry.indexOf(":");
@@ -47825,7 +48216,7 @@ const GLOB_METACHARACTERS = /[*?[]/;
47825
48216
  /** Patterns named in the glob warning before the rest collapse to `(+N more)`. */
47826
48217
  const MAX_LISTED_GLOB_PATTERNS = 10;
47827
48218
  function importPoolToolList({ serverName, poolKey, value, warnOnGlobs }) {
47828
- if (!isStringArray$2(value)) {
48219
+ if (!isStringArray$1(value)) {
47829
48220
  warnWithFallback(void 0, `Ignored malformed value for ${poolKey} in Pool MCP server ${quoteValueForWarning(serverName)}: expected a list of strings`);
47830
48221
  return;
47831
48222
  }
@@ -48035,7 +48426,7 @@ var PoolMcp = class PoolMcp extends ToolMcp {
48035
48426
  const poolAllow = isRecord$1(rawServer) ? rawServer.poolAllow : void 0;
48036
48427
  return [serverName, {
48037
48428
  ...serverConfig,
48038
- ...isStringArray$2(poolAllow) && { poolAllow }
48429
+ ...isStringArray$1(poolAllow) && { poolAllow }
48039
48430
  }];
48040
48431
  })), logger);
48041
48432
  return new PoolMcp({
@@ -48266,7 +48657,7 @@ function pickServerLists(source) {
48266
48657
  if (!isPlainObject$1(source)) return {};
48267
48658
  return Object.fromEntries(QWENCODE_MCP_SERVER_LIST_KEYS.flatMap((key) => {
48268
48659
  const value = source[key];
48269
- return isStringArray$2(value) ? [[key, value]] : [];
48660
+ return isStringArray$1(value) ? [[key, value]] : [];
48270
48661
  }));
48271
48662
  }
48272
48663
  /**
@@ -48959,7 +49350,7 @@ async function resolveRovodevMcpImportPath({ outputRoot, global, config, logger
48959
49350
  }
48960
49351
  function disabledNamesOf(config) {
48961
49352
  const mcpBlock = config && isRecord$1(config.mcp) ? config.mcp : {};
48962
- return isStringArray$2(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
49353
+ return isStringArray$1(mcpBlock.disabledMcpServers) ? mcpBlock.disabledMcpServers : [];
48963
49354
  }
48964
49355
  /**
48965
49356
  * The value `mcp.mcpConfigPath` needs so Rovo Dev reads the `mcp.json` rulesync
@@ -49375,7 +49766,7 @@ var RovodevMcp = class RovodevMcp extends ToolMcp {
49375
49766
  return [];
49376
49767
  }
49377
49768
  const existingMcp = isRecord$1(existingParsed.mcp) ? { ...existingParsed.mcp } : {};
49378
- const existingDisabled = isStringArray$2(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
49769
+ const existingDisabled = isStringArray$1(existingMcp.disabledMcpServers) ? existingMcp.disabledMcpServers : [];
49379
49770
  const managedNameSet = new Set(managedNames);
49380
49771
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
49381
49772
  if (reEnabled.length > 0) logger?.warn(`Rovo Dev MCP: re-enabling ${reEnabled.join(", ")} — the canonical config does not mark ${reEnabled.length === 1 ? "it" : "them"} disabled. Set "disabled": true in .rulesync/mcp.jsonc to keep a managed server off.`);
@@ -56534,8 +56925,9 @@ function convertClaudeToRulesyncPermissions(params) {
56534
56925
  //#endregion
56535
56926
  //#region src/features/permissions/cline-permissions.ts
56536
56927
  /**
56537
- * Cline CLI loads command permissions from the `CLINE_COMMAND_PERMISSIONS` environment variable.
56538
- * Cline supports only `allow` and `deny` (no `ask`), and only for shell commands.
56928
+ * The legacy Cline CLI loaded command permissions from the `CLINE_COMMAND_PERMISSIONS`
56929
+ * environment variable. It supported only `allow` and `deny` (no `ask`), and only for
56930
+ * shell commands.
56539
56931
  * Schema:
56540
56932
  * ```json
56541
56933
  * { "allow": ["pattern1", ...], "deny": ["pattern2", ...], "allowRedirects": false }
@@ -56543,7 +56935,14 @@ function convertClaudeToRulesyncPermissions(params) {
56543
56935
  *
56544
56936
  * Rulesync writes the JSON to `.cline/command-permissions.json` so users can do:
56545
56937
  * `export CLINE_COMMAND_PERMISSIONS=$(cat .cline/command-permissions.json)`
56938
+ *
56939
+ * Deprecated: no Cline CLI 3 release reads `CLINE_COMMAND_PERMISSIONS`. Upstream removed
56940
+ * it from the docs in cline/cline@6354d209, and the CLI reference now says the CLI has no
56941
+ * allow or deny list for shell commands and recommends a `PreToolUse` hook instead
56942
+ * (https://docs.cline.bot/cli/cli-reference). The output is still generated for
56943
+ * compatibility, with a deprecation warning on every generation.
56546
56944
  */
56945
+ const CLINE_PERMISSIONS_DEPRECATION_WARNING = "The Cline permissions output (.cline/command-permissions.json) is deprecated. No Cline CLI 3 release reads CLINE_COMMAND_PERMISSIONS, so the generated allow/deny lists are not enforced by current Cline releases. To block shell commands in Cline, use a preToolUse hook (rulesync hooks feature, cline target) that cancels the run_commands calls you do not allow. Note that a hook returning `cancel: true` stops the whole Cline task, not just that one tool call (https://github.com/cline/cline/issues/14697). See https://docs.cline.bot/cli/cli-reference.";
56547
56946
  const ClineCommandPermissionsSchema = zod_mini.z.looseObject({
56548
56947
  allow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
56549
56948
  deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -56661,6 +57060,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
56661
57060
  });
56662
57061
  }
56663
57062
  static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, logger }) {
57063
+ logger?.warn(CLINE_PERMISSIONS_DEPRECATION_WARNING);
56664
57064
  const paths = ClinePermissions.getSettablePaths({ global });
56665
57065
  const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
56666
57066
  const existingContent = await readFileContentOrNull(filePath) ?? "{}";
@@ -56690,7 +57090,7 @@ var ClinePermissions = class ClinePermissions extends ToolPermissions {
56690
57090
  const mergedDeny = (0, es_toolkit.uniq)([...existing.deny ?? [], ...dedupedDeny]).toSorted();
56691
57091
  const denySet = new Set(mergedDeny);
56692
57092
  const collisions = dedupedAllow.filter((p) => denySet.has(p));
56693
- if (collisions.length > 0) logger?.warn(`Cline command permissions: pattern(s) ${collisions.map((p) => `'${p}'`).join(", ")} appear in both 'allow' and 'deny'. Cline documents that deny rules always take precedence, so the 'allow' entry has no effect. Consider removing the duplicate rule.`);
57093
+ if (collisions.length > 0) logger?.warn(`Cline command permissions: pattern(s) ${collisions.map((p) => `'${p}'`).join(", ")} appear in both 'allow' and 'deny'. The legacy Cline CLI gave deny rules precedence, so the 'allow' entry has no effect. Consider removing the duplicate rule.`);
56694
57094
  const next = {
56695
57095
  ...existing,
56696
57096
  allow: dedupedAllow,
@@ -59973,9 +60373,9 @@ function managedContinueToolNames(config) {
59973
60373
  */
59974
60374
  function parseContinuePermissionLists(parsed) {
59975
60375
  return {
59976
- exclude: isStringArray$2(parsed.exclude) ? parsed.exclude : [],
59977
- ask: isStringArray$2(parsed.ask) ? parsed.ask : [],
59978
- allow: isStringArray$2(parsed.allow) ? parsed.allow : []
60376
+ exclude: isStringArray$1(parsed.exclude) ? parsed.exclude : [],
60377
+ ask: isStringArray$1(parsed.ask) ? parsed.ask : [],
60378
+ allow: isStringArray$1(parsed.allow) ? parsed.allow : []
59979
60379
  };
59980
60380
  }
59981
60381
  /**
@@ -60058,6 +60458,193 @@ const AUTO_APPROVE_KEYS = {
60058
60458
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60059
60459
  */
60060
60460
  const DEFAULT_PERMISSION_MODE_KEY = "chat.permissions.default";
60461
+ /**
60462
+ * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
60463
+ * at path `a.b` is written to `chat.agent.sandbox.a.b`.
60464
+ */
60465
+ const SANDBOX_KEY_PREFIX = "chat.agent.sandbox.";
60466
+ /**
60467
+ * The `copilot.sandbox` leaves rulesync manages, each a flat VS Code setting
60468
+ * (`SANDBOX_KEY_PREFIX` + the dotted path) with the value shape VS Code
60469
+ * registers for it. The shape is re-checked on import, so a hand-written value
60470
+ * VS Code would not accept is skipped instead of failing the whole import.
60471
+ *
60472
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
60473
+ * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60474
+ */
60475
+ const SANDBOX_LEAVES = [
60476
+ {
60477
+ path: ["enabled"],
60478
+ schema: CopilotSandboxEnabledSchema,
60479
+ normalize: (value) => value === true ? "on" : value === false ? "off" : value
60480
+ },
60481
+ {
60482
+ path: ["network", "allowNetwork"],
60483
+ schema: zod_mini.z.boolean()
60484
+ },
60485
+ {
60486
+ path: ["network", "allowLocalNetwork"],
60487
+ schema: zod_mini.z.boolean()
60488
+ },
60489
+ {
60490
+ path: ["allowUnsandboxedCommands"],
60491
+ schema: zod_mini.z.boolean()
60492
+ },
60493
+ {
60494
+ path: ["mcpServers"],
60495
+ schema: zod_mini.z.boolean()
60496
+ },
60497
+ {
60498
+ path: ["lspServers"],
60499
+ schema: zod_mini.z.boolean()
60500
+ },
60501
+ {
60502
+ path: ["credentials", "authenticategit"],
60503
+ schema: zod_mini.z.boolean()
60504
+ },
60505
+ {
60506
+ path: ["credentials", "authenticategh"],
60507
+ schema: zod_mini.z.boolean()
60508
+ },
60509
+ {
60510
+ path: ["fileSystem", "allowDevToolAccess"],
60511
+ schema: zod_mini.z.boolean()
60512
+ },
60513
+ {
60514
+ path: ["fileSystem", "userConfiguredPaths"],
60515
+ schema: CopilotSandboxUserConfiguredPathsSchema
60516
+ }
60517
+ ];
60518
+ const USER_CONFIGURED_PATHS_KEY = `${SANDBOX_KEY_PREFIX}fileSystem.userConfiguredPaths`;
60519
+ /**
60520
+ * `copilot.sandbox` values that loosen the Agent Host sandbox. They are still
60521
+ * written — a sandbox policy is the user's call — but never silently. Each
60522
+ * `widens` names the value that does *not* loosen anything and reports the
60523
+ * rest, so an unexpected value is reported rather than ignored.
60524
+ *
60525
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
60526
+ */
60527
+ const COPILOT_TRUST_AFFECTING_SANDBOX_PATHS = [
60528
+ {
60529
+ path: ["enabled"],
60530
+ reason: "turns the sandbox off, so agent terminal commands run unconfined",
60531
+ widens: (value) => value !== "on"
60532
+ },
60533
+ {
60534
+ path: ["network", "allowNetwork"],
60535
+ reason: "lets sandboxed commands reach the external network",
60536
+ widens: isNotFalse
60537
+ },
60538
+ {
60539
+ path: ["network", "allowLocalNetwork"],
60540
+ reason: "lets sandboxed commands reach local network resources",
60541
+ widens: isNotFalse
60542
+ },
60543
+ {
60544
+ path: ["allowUnsandboxedCommands"],
60545
+ reason: "lets the agent ask to run a blocked command outside the sandbox",
60546
+ widens: isNotFalse
60547
+ },
60548
+ {
60549
+ path: ["mcpServers"],
60550
+ reason: "runs locally launched MCP servers outside the sandbox",
60551
+ widens: isNotTrue
60552
+ },
60553
+ {
60554
+ path: ["lspServers"],
60555
+ reason: "runs language servers outside the sandbox",
60556
+ widens: isNotTrue
60557
+ },
60558
+ {
60559
+ path: ["credentials", "authenticategit"],
60560
+ reason: "hands your Git credentials to sandboxed commands",
60561
+ widens: isNotFalse
60562
+ },
60563
+ {
60564
+ path: ["credentials", "authenticategh"],
60565
+ reason: "hands your GitHub CLI authentication to sandboxed commands",
60566
+ widens: isNotFalse
60567
+ },
60568
+ {
60569
+ path: ["fileSystem", "allowDevToolAccess"],
60570
+ reason: "grants sandboxed commands developer-tool directories, which can hold credentials such as registry tokens",
60571
+ widens: isNotFalse
60572
+ },
60573
+ {
60574
+ path: [
60575
+ "fileSystem",
60576
+ "userConfiguredPaths",
60577
+ "readwritePaths"
60578
+ ],
60579
+ reason: "adds paths sandboxed commands may write to, outside the working directory",
60580
+ widens: isNonEmptyList
60581
+ },
60582
+ {
60583
+ path: [
60584
+ "fileSystem",
60585
+ "userConfiguredPaths",
60586
+ "readonlyPaths"
60587
+ ],
60588
+ reason: "adds paths sandboxed commands may read, outside the working directory",
60589
+ widens: isNonEmptyList
60590
+ }
60591
+ ];
60592
+ const COPILOT_TOOL_LABEL = "GitHub Copilot";
60593
+ function sandboxKey(path) {
60594
+ return `${SANDBOX_KEY_PREFIX}${path.join(".")}`;
60595
+ }
60596
+ /**
60597
+ * Patch entries for every `copilot.sandbox` leaf the canonical config states.
60598
+ * Leaves it does not state are left as the file has them, so authoring only
60599
+ * `network.allowNetwork` never disturbs a hand-written `enabled`. The
60600
+ * `userConfiguredPaths` object merges per list for the same reason: authoring
60601
+ * `readwritePaths` keeps a hand-written `deniedPaths`, which would otherwise be
60602
+ * a restriction lost without a word.
60603
+ */
60604
+ function buildSandboxPatch({ sandbox, existingSettings }) {
60605
+ const patch = {};
60606
+ for (const { path } of SANDBOX_LEAVES) {
60607
+ const value = readSandboxPath({
60608
+ sandbox,
60609
+ path
60610
+ });
60611
+ if (value === void 0 || value === UNREADABLE_SANDBOX_PATH) continue;
60612
+ const key = sandboxKey(path);
60613
+ if (key === USER_CONFIGURED_PATHS_KEY && isPlainObject$1(value)) {
60614
+ const existing = existingSettings[key];
60615
+ patch[key] = {
60616
+ ...isPlainObject$1(existing) ? existing : {},
60617
+ ...value
60618
+ };
60619
+ continue;
60620
+ }
60621
+ patch[key] = value;
60622
+ }
60623
+ return patch;
60624
+ }
60625
+ /**
60626
+ * Lift every managed `chat.agent.sandbox.*` key the file holds back into a
60627
+ * nested `copilot.sandbox` object. A value whose shape VS Code would reject is
60628
+ * skipped; the write path leaves it in place.
60629
+ */
60630
+ function readSandboxFromSettings(settings) {
60631
+ const sandbox = {};
60632
+ for (const { path, schema, normalize } of SANDBOX_LEAVES) {
60633
+ const value = settings[sandboxKey(path)];
60634
+ const parsed = schema.safeParse(normalize === void 0 ? value : normalize(value));
60635
+ if (!parsed.success) continue;
60636
+ let cursor = sandbox;
60637
+ for (const segment of path.slice(0, -1)) {
60638
+ const next = cursor[segment];
60639
+ const child = isPlainObject$1(next) ? next : {};
60640
+ cursor[segment] = child;
60641
+ cursor = child;
60642
+ }
60643
+ const leaf = path[path.length - 1];
60644
+ if (leaf !== void 0) cursor[leaf] = parsed.data;
60645
+ }
60646
+ return Object.keys(sandbox).length > 0 ? sandbox : void 0;
60647
+ }
60061
60648
  function isCopilotDefaultPermissionMode(value) {
60062
60649
  return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
60063
60650
  }
@@ -60102,7 +60689,9 @@ function buildAutoApproveValue(rules) {
60102
60689
  * hand-written edits or urls map.
60103
60690
  * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
60104
60691
  * (the starting permission level of new chat sessions); without it the key is
60105
- * left untouched.
60692
+ * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
60693
+ * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
60694
+ * warns about every value it writes that loosens the sandbox.
60106
60695
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
60107
60696
  * platform-dependent path outside rulesync's home-relative global model.
60108
60697
  */
@@ -60150,6 +60739,26 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60150
60739
  }
60151
60740
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
60152
60741
  if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
60742
+ const sandbox = config.copilot?.sandbox;
60743
+ if (sandbox !== void 0) {
60744
+ Object.assign(patch, buildSandboxPatch({
60745
+ sandbox,
60746
+ existingSettings: parseSharedConfig({
60747
+ format: "jsonc",
60748
+ fileContent: existingContent,
60749
+ filePath
60750
+ })
60751
+ }));
60752
+ warnOnTrustAffectingEntries({
60753
+ toolLabel: COPILOT_TOOL_LABEL,
60754
+ entries: collectTrustAffectingSandboxPaths({
60755
+ sandbox,
60756
+ paths: COPILOT_TRUST_AFFECTING_SANDBOX_PATHS
60757
+ }),
60758
+ relativeFilePath: (0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath),
60759
+ logger
60760
+ });
60761
+ }
60153
60762
  return new CopilotPermissions({
60154
60763
  outputRoot,
60155
60764
  relativeDirPath: paths.relativeDirPath,
@@ -60186,9 +60795,14 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60186
60795
  if (Object.keys(rules).length > 0) permission[category] = rules;
60187
60796
  }
60188
60797
  const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY];
60798
+ const sandbox = readSandboxFromSettings(settings);
60799
+ const copilot = {
60800
+ ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
60801
+ ...sandbox !== void 0 && { sandbox }
60802
+ };
60189
60803
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
60190
60804
  permission,
60191
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { copilot: { defaultPermissionMode } }
60805
+ ...Object.keys(copilot).length > 0 && { copilot }
60192
60806
  }, null, 2) });
60193
60807
  }
60194
60808
  validate() {
@@ -60502,8 +61116,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
60502
61116
  logger
60503
61117
  });
60504
61118
  const managedToolNames = managedCrushToolNames(config);
60505
- const preservedAllowed = (isStringArray$2(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
60506
- const preservedDisabled = (isStringArray$2(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
61119
+ const preservedAllowed = (isStringArray$1(existingPermissions?.["allowed_tools"]) ? existingPermissions[CRUSH_ALLOWED_TOOLS_KEY] : []).filter((entry) => entry.includes(":") || !managedToolNames.has(entry));
61120
+ const preservedDisabled = (isStringArray$1(existingOptions?.["disabled_tools"]) ? existingOptions[CRUSH_DISABLED_TOOLS_KEY] : []).filter((entry) => !managedToolNames.has(entry));
60507
61121
  const allowedList = uniq$11([...preservedAllowed, ...generated.allowed]);
60508
61122
  const disabledList = uniq$11([...preservedDisabled, ...generated.disabled]);
60509
61123
  const patch = {};
@@ -60529,8 +61143,8 @@ var CrushPermissions = class CrushPermissions extends ToolPermissions {
60529
61143
  const permissions = isRecord$1(this.json["permissions"]) ? this.json[CRUSH_PERMISSIONS_KEY] : {};
60530
61144
  const options = isRecord$1(this.json["options"]) ? this.json[CRUSH_OPTIONS_KEY] : {};
60531
61145
  const rulesyncConfig = convertCrushListsToRulesync({
60532
- allowed: isStringArray$2(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
60533
- disabled: isStringArray$2(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
61146
+ allowed: isStringArray$1(permissions["allowed_tools"]) ? permissions[CRUSH_ALLOWED_TOOLS_KEY] : [],
61147
+ disabled: isStringArray$1(options["disabled_tools"]) ? options[CRUSH_DISABLED_TOOLS_KEY] : []
60534
61148
  });
60535
61149
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(rulesyncConfig, null, 2) });
60536
61150
  }
@@ -62705,7 +63319,7 @@ function convertRulesyncToGoosePermissionConfig({ config, logger }) {
62705
63319
  function convertGoosePermissionConfigToRulesync(userPermission) {
62706
63320
  const permission = {};
62707
63321
  for (const key of GOOSE_PERMISSION_LIST_KEYS) {
62708
- const toolNames = isStringArray$2(userPermission[key]) ? userPermission[key] : [];
63322
+ const toolNames = isStringArray$1(userPermission[key]) ? userPermission[key] : [];
62709
63323
  const action = GOOSE_LIST_TO_ACTION[key];
62710
63324
  for (const toolName of toolNames) for (const category of GOOSE_TO_RULESYNC_TOOL_NAMES[toolName] ?? [toolName]) {
62711
63325
  permission[category] ??= {};
@@ -63053,7 +63667,7 @@ const ACTION_RANK$3 = {
63053
63667
  * (mirrors the Cursor adapter's preservation of unmanaged types).
63054
63668
  */
63055
63669
  function unmanagedEntries(existingPermission, key) {
63056
- return (isStringArray$2(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
63670
+ return (isStringArray$1(existingPermission[key]) ? existingPermission[key] : []).filter((entry) => parseGrokEntry(entry) === null);
63057
63671
  }
63058
63672
  /**
63059
63673
  * Bucket canonical rules into Grok's `[permission]` allow/deny/ask arrays of
@@ -63110,7 +63724,7 @@ function warnRemovedAllToolsRestrictions({ existingPermission, ranked, logger })
63110
63724
  const generated = ranked.get(GROK_ALL_TOOLS_ENTRY);
63111
63725
  for (const action of ["deny", "ask"]) {
63112
63726
  const entries = existingPermission[action];
63113
- if (!isStringArray$2(entries)) continue;
63727
+ if (!isStringArray$1(entries)) continue;
63114
63728
  if (!entries.some((entry) => parseGrokEntry(entry)?.category === "*")) continue;
63115
63729
  if (generated !== void 0 && ACTION_RANK$3[generated] >= ACTION_RANK$3[action]) continue;
63116
63730
  logger.warn(`Grok CLI permissions: removing the existing bare '*' entry from the '${action}' array — Rulesync now manages it through the all-tools '*' category, and the canonical config has no '*' '${action}' rule. Add "*": { "*": "${action}" } to keep restricting every tool.`);
@@ -63141,9 +63755,9 @@ function warnEntriesShadowedByAllTools({ ranked, logger }) {
63141
63755
  * described more than once resolves to the strictest action.
63142
63756
  */
63143
63757
  function parseGrokPermissionArrays(permission) {
63144
- const allow = isStringArray$2(permission.allow) ? permission.allow : void 0;
63145
- const deny = isStringArray$2(permission.deny) ? permission.deny : void 0;
63146
- const ask = isStringArray$2(permission.ask) ? permission.ask : void 0;
63758
+ const allow = isStringArray$1(permission.allow) ? permission.allow : void 0;
63759
+ const deny = isStringArray$1(permission.deny) ? permission.deny : void 0;
63760
+ const ask = isStringArray$1(permission.ask) ? permission.ask : void 0;
63147
63761
  const rules = Array.isArray(permission[GROKCLI_PERMISSION_RULES_KEY]) ? permission[GROKCLI_PERMISSION_RULES_KEY] : void 0;
63148
63762
  if ((allow?.length ?? 0) + (deny?.length ?? 0) + (ask?.length ?? 0) + (rules?.length ?? 0) === 0) return null;
63149
63763
  const result = {};
@@ -63351,20 +63965,20 @@ var HermesagentPermissions = class HermesagentPermissions extends ToolPermission
63351
63965
  const permission = clonePermissionBlock(provenance.permission);
63352
63966
  reconcileCommandAllowlist({
63353
63967
  permission,
63354
- commandAllowlist: isStringArray$2(config.command_allowlist) ? config.command_allowlist : []
63968
+ commandAllowlist: isStringArray$1(config.command_allowlist) ? config.command_allowlist : []
63355
63969
  });
63356
63970
  const approvals = isRecord$1(config.approvals) ? config.approvals : {};
63357
63971
  reconcileNativeDenies({
63358
63972
  permission,
63359
63973
  category: SHELL_PERMISSION_CATEGORY,
63360
- patterns: isStringArray$2(approvals.deny) ? approvals.deny : []
63974
+ patterns: isStringArray$1(approvals.deny) ? approvals.deny : []
63361
63975
  });
63362
63976
  const security = isRecord$1(config.security) ? config.security : {};
63363
63977
  const websiteBlocklist = isRecord$1(security.website_blocklist) ? security.website_blocklist : {};
63364
63978
  reconcileNativeDenies({
63365
63979
  permission,
63366
63980
  category: WEBFETCH_PERMISSION_CATEGORY,
63367
- patterns: websiteBlocklist.enabled === true && isStringArray$2(websiteBlocklist.domains) ? websiteBlocklist.domains : []
63981
+ patterns: websiteBlocklist.enabled === true && isStringArray$1(websiteBlocklist.domains) ? websiteBlocklist.domains : []
63368
63982
  });
63369
63983
  removeEmptyCategories(permission);
63370
63984
  const { permission: _permission, hermes: _hermes, ...otherProvenance } = provenance;
@@ -64758,13 +65372,13 @@ function classifyExistingEntry({ emitted, capability, pattern, effect }) {
64758
65372
  * how many entries rulesync does not generate.
64759
65373
  */
64760
65374
  function mergeExistingRule({ rule, emitted, logger }) {
64761
- if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$2(rule.match)) return {
65375
+ if (!isRecord$1(rule) || typeof rule.capability !== "string" || rule.exclude !== void 0 || !MANAGED_CAPABILITIES.has(rule.capability) || rule.match !== void 0 && !isStringArray$1(rule.match)) return {
64762
65376
  rule,
64763
65377
  unrelatedEntries: 0
64764
65378
  };
64765
65379
  const capability = rule.capability;
64766
65380
  const isCatchAll = rule.match === void 0;
64767
- const patterns = isStringArray$2(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
65381
+ const patterns = isStringArray$1(rule.match) ? rule.match : [CATCH_ALL_PATTERN$6];
64768
65382
  if (patterns.length === 0) return {
64769
65383
  rule,
64770
65384
  unrelatedEntries: 0
@@ -64948,7 +65562,7 @@ var KiroCapabilityPermissions = class KiroCapabilityPermissions extends ToolPerm
64948
65562
  for (const rule of rules) {
64949
65563
  if (!isImportableRule(rule) || !isRecord$1(rule) || !isPermissionAction$1(rule.effect)) continue;
64950
65564
  const category = KIRO_CAPABILITY_TO_CANONICAL[rule.capability];
64951
- const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$2(rule.match) ? rule.match : [];
65565
+ const patterns = rule.match === void 0 ? [CATCH_ALL_PATTERN$6] : isStringArray$1(rule.match) ? rule.match : [];
64952
65566
  const categoryRules = permission[category] ??= {};
64953
65567
  for (const pattern of patterns) {
64954
65568
  const previous = categoryRules[pattern];
@@ -66666,8 +67280,8 @@ function importPoolTool({ toolConfig, bucket }) {
66666
67280
  bucket[CATCH_ALL_PATTERN$3] = "deny";
66667
67281
  return;
66668
67282
  }
66669
- const denied = isStringArray$2(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
66670
- const allowed = isStringArray$2(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
67283
+ const denied = isStringArray$1(toolConfig[POOL_DENY_KEY]) ? toolConfig[POOL_DENY_KEY] : [];
67284
+ const allowed = isStringArray$1(toolConfig[POOL_ALLOW_KEY]) ? toolConfig[POOL_ALLOW_KEY] : [];
66671
67285
  for (const pattern of denied.filter(isUsablePattern)) bucket[pattern] = "deny";
66672
67286
  for (const pattern of allowed.filter(isUsablePattern)) bucket[pattern] ??= "allow";
66673
67287
  }
@@ -68872,7 +69486,7 @@ function convertRovodevToolPermissionsToRulesync(toolPermissions) {
68872
69486
  permission[category] ??= {};
68873
69487
  permission[category][CATCH_ALL_PATTERN$1] = levels.reduce((strictest, level) => strictestAction(strictest, level ?? implicitLevel), permission[category][CATCH_ALL_PATTERN$1]);
68874
69488
  }
68875
- if (isStringArray$2(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
69489
+ if (isStringArray$1(toolPermissions.allowedExternalPaths)) for (const path of toolPermissions.allowedExternalPaths) {
68876
69490
  permission.read ??= {};
68877
69491
  permission.read[path] ??= "allow";
68878
69492
  }
@@ -69128,7 +69742,7 @@ function stripRefusedOverridePaths(override) {
69128
69742
  */
69129
69743
  function collectTrustAffectingOverrideEntries({ tools, general }) {
69130
69744
  const entries = [];
69131
- const allowed = isStringArray$2(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
69745
+ const allowed = isStringArray$1(tools[ALLOWED_KEY]) ? tools[ALLOWED_KEY] : [];
69132
69746
  if (allowed.length > 0) entries.push({
69133
69747
  label: `tools.allowed (${allowed.map(quoteValueForWarning).join(", ")})`,
69134
69748
  reason: "auto-approves what it names as the override spells it; a non-shell entry is only checked against a whole-tool deny of its tool"
@@ -69279,7 +69893,7 @@ function withholdDeniedOverrideAllowed({ overrideVerbatimAllowed, exclude, logge
69279
69893
  * hand-written entry went.
69280
69894
  */
69281
69895
  function reportDroppedManagedEntries({ existingTools, allowedList, excludeList, logger }) {
69282
- const dropped = (key, list) => (0, es_toolkit.uniq)(isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69896
+ const dropped = (key, list) => (0, es_toolkit.uniq)(isStringArray$1(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69283
69897
  if (list.includes(entry)) return false;
69284
69898
  const toolName = parseTabnineEntry(entry)?.toolName;
69285
69899
  return toolName === void 0 || !list.includes(toolName);
@@ -69361,7 +69975,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69361
69975
  }
69362
69976
  const overrideList = (key) => {
69363
69977
  const value = overrideTools[key];
69364
- if (value === void 0 || isStringArray$2(value)) return value ?? [];
69978
+ if (value === void 0 || isStringArray$1(value)) return value ?? [];
69365
69979
  warnWithFallback(logger, `Tabnine CLI permissions: ignored tools.${key} of the tabnine override (${quoteValueForWarning(value)}); it must be a list of tool names.`);
69366
69980
  return [];
69367
69981
  };
@@ -69399,7 +70013,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69399
70013
  if (ignoredOverrideKeys.length > 0) warnWithFallback(logger, `Tabnine CLI permissions: ignored ${ignoredOverrideKeys.length} key(s) of the tabnine override (${ignoredOverrideKeys.map(quoteValueForWarning).join(", ")}); only 'tools' and 'general' are written through the permissions feature.`);
69400
70014
  const existingTools = this.existingToolsGroup(existingContent);
69401
70015
  const managedToolNames = new Set(Object.keys(config.permission).map((category) => category === "bash" || category === "*" ? SHELL_TOOL_NAME : toTabnineToolName(category)));
69402
- const preservedEntries = (key) => (isStringArray$2(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
70016
+ const preservedEntries = (key) => (isStringArray$1(existingTools?.[key]) ? existingTools[key] : []).filter((entry) => {
69403
70017
  const toolName = parseTabnineEntry(entry)?.toolName;
69404
70018
  return toolName === void 0 || !managedToolNames.has(toolName);
69405
70019
  });
@@ -69477,7 +70091,7 @@ var TabninePermissions = class TabninePermissions extends ToolPermissions {
69477
70091
  if (rules !== void 0 && !Object.hasOwn(rules, pattern)) rules[pattern] = action;
69478
70092
  };
69479
70093
  for (const [key, action] of [[EXCLUDE_KEY, "deny"], [ALLOWED_KEY, "allow"]]) {
69480
- const entries = isStringArray$2(tools[key]) ? tools[key] : [];
70094
+ const entries = isStringArray$1(tools[key]) ? tools[key] : [];
69481
70095
  for (const entry of entries) {
69482
70096
  const parsed = parseTabnineEntry(entry);
69483
70097
  if (parsed === void 0) {
@@ -70345,12 +70959,12 @@ const WIDE_CHARACTERS_PATTERN = /\p{Emoji_Presentation}|\p{Emoji_Modifier_Base}|
70345
70959
  * The ranges are the `East_Asian_Width=A` property of Unicode 17.0, taken from
70346
70960
  * the table `get-east-asian-width` 1.6.0 carries and merged where two are
70347
70961
  * adjacent. The table is written out here rather than read from the package so
70348
- * that the one lookup adds no dependency of its own, and the drift test in
70349
- * `display-width.test.ts` is what keeps it honest: it loads the package through
70350
- * the prompt renderer that already depends on it, walks every code point, and
70351
- * fails when this table and the property disagree in either direction. When
70352
- * the renderer's copy moves to a newer Unicode, that test is what fails, and
70353
- * the table is regenerated from the new property — its ranges, marks aside,
70962
+ * that the one lookup adds no runtime dependency of its own, and the drift test
70963
+ * in `display-width.test.ts` is what keeps it honest: it loads the package,
70964
+ * pinned as a dev dependency, walks every code point, and fails when this
70965
+ * table and the property disagree in either direction. When that pin moves to
70966
+ * a newer Unicode, that test is what fails, and the table is regenerated from
70967
+ * the new property — its ranges, marks aside,
70354
70968
  * merged where adjacent and written as escapes — with the version named here.
70355
70969
  *
70356
70970
  * Three stretches of the property are left out because they never reach this
@@ -72099,8 +72713,8 @@ var WarpPermissions = class WarpPermissions extends ToolPermissions {
72099
72713
  const executionProfiles = isRecord$1(agents["execution_profiles"]) ? agents[EXECUTION_PROFILES_KEY] : void 0;
72100
72714
  const defaultProfile = executionProfiles && isRecord$1(executionProfiles["default"]) ? executionProfiles[DEFAULT_PROFILE_KEY] : void 0;
72101
72715
  const config = convertWarpToRulesyncPermissions({
72102
- allow: defaultProfile ? isStringArray$2(defaultProfile["command_allowlist"]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$2(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
72103
- deny: defaultProfile ? isStringArray$2(defaultProfile["command_denylist"]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$2(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
72716
+ allow: defaultProfile ? isStringArray$1(defaultProfile["command_allowlist"]) ? defaultProfile[PROFILE_ALLOWLIST_KEY] : [] : isStringArray$1(profiles[ALLOWLIST_KEY]) ? profiles[ALLOWLIST_KEY] : [],
72717
+ deny: defaultProfile ? isStringArray$1(defaultProfile["command_denylist"]) ? defaultProfile[PROFILE_DENYLIST_KEY] : [] : isStringArray$1(profiles[DENYLIST_KEY]) ? profiles[DENYLIST_KEY] : []
72104
72718
  });
72105
72719
  const warpOverride = {};
72106
72720
  for (const key of WARP_OVERRIDE_KEYS) if (profiles[key] !== void 0) warpOverride[key] = profiles[key];
@@ -72630,8 +73244,8 @@ var WarpcliPermissions = class WarpcliPermissions extends ToolPermissions {
72630
73244
  const allowlist = defaultProfile[PROFILE_ALLOWLIST_KEY];
72631
73245
  const denylist = defaultProfile[PROFILE_DENYLIST_KEY];
72632
73246
  const config = convertWarpToRulesyncPermissions({
72633
- allow: isStringArray$2(allowlist) ? allowlist : [],
72634
- deny: isStringArray$2(denylist) ? denylist : []
73247
+ allow: isStringArray$1(allowlist) ? allowlist : [],
73248
+ deny: isStringArray$1(denylist) ? denylist : []
72635
73249
  });
72636
73250
  const executionProfileOverride = liftExecutionProfileOverride(defaultProfile);
72637
73251
  const warpcliOverride = {};
@@ -84646,7 +85260,7 @@ var BobSubagent = class extends RooSubagent {
84646
85260
  const mode = super.toRooMode(rulesyncSubagent);
84647
85261
  const frontmatter = rulesyncSubagent.getFrontmatter();
84648
85262
  const bobSection = isRecord$1(frontmatter.bob) ? frontmatter.bob : {};
84649
- if (isStringArray$2(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
85263
+ if (isStringArray$1(bobSection.allowedSubagents)) mode.allowedSubagents = bobSection.allowedSubagents;
84650
85264
  return mode;
84651
85265
  }
84652
85266
  };
@@ -90101,7 +90715,7 @@ var ZoocodeSubagent = class extends RooSubagent {
90101
90715
  const mode = super.toRooMode(rulesyncSubagent);
90102
90716
  const frontmatter = rulesyncSubagent.getFrontmatter();
90103
90717
  const zoocodeSection = isRecord$1(frontmatter.zoocode) ? frontmatter.zoocode : {};
90104
- if (isStringArray$2(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
90718
+ if (isStringArray$1(zoocodeSection.allowedMcpServers)) mode.allowedMcpServers = zoocodeSection.allowedMcpServers;
90105
90719
  return mode;
90106
90720
  }
90107
90721
  toRulesyncSubagents() {
@@ -90113,7 +90727,7 @@ var ZoocodeSubagent = class extends RooSubagent {
90113
90727
  targets: ["zoocode"],
90114
90728
  roo: restRooSection
90115
90729
  };
90116
- if (isStringArray$2(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
90730
+ if (isStringArray$1(allowedMcpServers)) rebuilt.zoocode = { allowedMcpServers };
90117
90731
  return new RulesyncSubagent({
90118
90732
  outputRoot: ".",
90119
90733
  frontmatter: rebuilt,
@@ -94054,7 +94668,8 @@ var NestedAgentsmdRule = class extends ToolRule {
94054
94668
  * `<subprojectPath>/AGENTS.md` (project scope only); every other non-root rule
94055
94669
  * folds into the root `AGENTS.md`, since Codex has no modular rules directory.
94056
94670
  * The project files on the root-to-cwd chain share one `project_doc_max_bytes`
94057
- * budget (32 KiB by default), which the RulesProcessor warns about.
94671
+ * budget (32 KiB by default, or the value set in the project's
94672
+ * `.codex/config.toml`), which the RulesProcessor warns about.
94058
94673
  *
94059
94674
  * @see https://learn.chatgpt.com/docs/agent-configuration/agents-md
94060
94675
  */
@@ -94066,6 +94681,29 @@ var CodexcliRule = class extends NestedAgentsmdRule {
94066
94681
  };
94067
94682
  }
94068
94683
  };
94684
+ /**
94685
+ * Read the `project_doc_max_bytes` value set in the project's
94686
+ * `.codex/config.toml`, which Codex loads for trusted projects and which
94687
+ * overrides the 32 KiB default budget for project `AGENTS.md` files.
94688
+ *
94689
+ * Answers `undefined` when the file is missing, is not valid TOML, or does not
94690
+ * set the key to a positive integer, so the caller falls back to the default.
94691
+ *
94692
+ * @see https://learn.chatgpt.com/docs/config-file/config-reference
94693
+ */
94694
+ async function readCodexcliProjectDocMaxBytes(outputRoot) {
94695
+ const content = await readFileContentOrNull((0, node_path.join)(outputRoot, CODEXCLI_DIR, CODEXCLI_MCP_FILE_NAME));
94696
+ if (content === null) return;
94697
+ let parsed;
94698
+ try {
94699
+ parsed = smol_toml.parse(content);
94700
+ } catch {
94701
+ return;
94702
+ }
94703
+ if (!isPlainObject$1(parsed)) return;
94704
+ const value = parsed.project_doc_max_bytes;
94705
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
94706
+ }
94069
94707
  //#endregion
94070
94708
  //#region src/features/rules/commandcode-rule.ts
94071
94709
  var CommandcodeRule = class CommandcodeRule extends ToolRule {
@@ -99440,7 +100078,10 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
99440
100078
  collisionPolicy: "fold",
99441
100079
  projectInstructionBudget: {
99442
100080
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
99443
- remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (ignore this warning if you already have)."
100081
+ remedy: "Codex CLI stops reading project `AGENTS.md` files once their combined size on the root-to-working-directory chain reaches `project_doc_max_bytes` (32 KiB by default) and silently drops the rest. Move directory-scoped rules into nested `AGENTS.md` files with `agentsmd.subprojectPath` (or `deriveSubprojectPathFromGlobs: true` in rulesync.jsonc), trim the rules, or raise `project_doc_max_bytes` in `.codex/config.toml` (Codex reads that file only if the project is trusted).",
100082
+ readConfiguredBytes: readCodexcliProjectDocMaxBytes,
100083
+ configuredSource: "`project_doc_max_bytes` in `.codex/config.toml` (honored only if the project is trusted)",
100084
+ defaultLimitHint: "Rulesync does not read `~/.codex/config.toml`; ignore this warning if you already raised `project_doc_max_bytes` there."
99444
100085
  }
99445
100086
  }
99446
100087
  }],
@@ -99985,7 +100626,7 @@ var RulesProcessor = class extends FeatureProcessor {
99985
100626
  factory
99986
100627
  });
99987
100628
  await this.warnForWarpLegacyRootFile(toolRules);
99988
- this.warnForInstructionBudget({
100629
+ await this.warnForInstructionBudget({
99989
100630
  toolRules,
99990
100631
  meta
99991
100632
  });
@@ -100006,14 +100647,20 @@ var RulesProcessor = class extends FeatureProcessor {
100006
100647
  * once, at its shallowest directory. With several output roots the warning
100007
100648
  * names the root it refers to. An output root below the git root is measured
100008
100649
  * without the parent directories' files Codex would also load, so the
100009
- * estimate can come out low there. The budget checked is the tool's default: a value
100010
- * raised by hand in the tool's own config is not read. Global scope is not
100650
+ * estimate can come out low there. The budget checked is the value set in the
100651
+ * output root's tool config when the tool supports one (Codex's
100652
+ * `project_doc_max_bytes` in `.codex/config.toml`), else the tool's default;
100653
+ * the warning names which one it used. Global scope is not
100011
100654
  * checked because Codex loads the personal `~/.codex/AGENTS.md` outside the
100012
100655
  * project budget.
100013
100656
  */
100014
- warnForInstructionBudget({ toolRules, meta }) {
100657
+ async warnForInstructionBudget({ toolRules, meta }) {
100015
100658
  const budget = meta.projectInstructionBudget;
100016
100659
  if (budget === void 0 || this.global) return;
100660
+ const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
100661
+ const limitBytes = configuredBytes ?? budget.bytes;
100662
+ const limitLabel = budget.readConfiguredBytes === void 0 ? `${limitBytes}-byte limit` : configuredBytes === void 0 ? `${limitBytes}-byte default limit` : `${limitBytes}-byte limit set by ${budget.configuredSource ?? "the tool config"}`;
100663
+ const remedy = configuredBytes === void 0 && budget.defaultLimitHint !== void 0 ? `${budget.remedy} ${budget.defaultLimitHint}` : budget.remedy;
100017
100664
  const files = toolRules.map((rule) => ({
100018
100665
  dir: normalizeRelativeDir(rule.getRelativeDirPath()),
100019
100666
  path: toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
@@ -100023,8 +100670,8 @@ var RulesProcessor = class extends FeatureProcessor {
100023
100670
  const location = this.outputRoot === process.cwd() ? "" : ` under '${this.outputRoot}'`;
100024
100671
  const rootFiles = files.filter(({ dir }) => dir === ".");
100025
100672
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100026
- if (rootBytes > budget.bytes) {
100027
- this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${budget.bytes}-byte limit. ${budget.remedy}`);
100673
+ if (rootBytes > limitBytes) {
100674
+ this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${remedy}`);
100028
100675
  return;
100029
100676
  }
100030
100677
  const reportedDirs = [];
@@ -100033,9 +100680,9 @@ var RulesProcessor = class extends FeatureProcessor {
100033
100680
  if (reportedDirs.some((dir) => isAncestorOrSelf(dir, file.dir))) continue;
100034
100681
  const chain = files.filter(({ dir }) => isAncestorOrSelf(dir, file.dir));
100035
100682
  const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
100036
- if (chainBytes > budget.bytes) {
100683
+ if (chainBytes > limitBytes) {
100037
100684
  reportedDirs.push(file.dir);
100038
- this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${budget.bytes}-byte limit (as is every directory below it). ${budget.remedy}`);
100685
+ this.logger.warn(`The generated ${this.toolTarget} instruction files${location} loaded while working in '${file.dir}' (${chain.map(({ path }) => path).join(", ")}) total ${chainBytes} bytes, over the ${limitLabel} (as is every directory below it). ${remedy}`);
100039
100686
  }
100040
100687
  }
100041
100688
  }