rulesync 37.0.0 → 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.
@@ -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
@@ -12896,6 +12935,16 @@ const SHARED_CONFIG_OWNERSHIP = {
12896
12935
  "chat.tools.edits.autoApprove",
12897
12936
  "chat.tools.urls.autoApprove",
12898
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",
12899
12948
  "zoo-code.allowedCommands",
12900
12949
  "zoo-code.deniedCommands",
12901
12950
  "roo-cline.allowedCommands",
@@ -28058,8 +28107,34 @@ function collectOwnedRefs({ generatedHooks, shape }) {
28058
28107
  }
28059
28108
  return owned;
28060
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
+ */
28061
28127
  function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned, logger }) {
28062
- 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();
28063
28138
  for (const group of existing) {
28064
28139
  if (!isMatcherGroup(group)) {
28065
28140
  warnSkip({
@@ -28070,27 +28145,57 @@ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned,
28070
28145
  continue;
28071
28146
  }
28072
28147
  const matcher = matcherKey(group.matcher);
28073
- const preserved = group.hooks.filter((handler) => shouldPreserve({
28074
- handler,
28148
+ const hooks = keepExistingHandlers({
28149
+ handlers: group.hooks,
28075
28150
  event,
28076
28151
  matcher,
28152
+ attributes: groupAttributesKey(group),
28153
+ pending,
28077
28154
  seen,
28078
28155
  previouslyOwned,
28079
28156
  logger
28080
- })).map((handler) => cloneHandler(handler));
28081
- if (preserved.length === 0) continue;
28082
- const attributes = groupAttributesKey(group);
28083
- const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher && groupAttributesKey(candidate) === attributes);
28084
- if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
28085
- else merged.push({
28157
+ });
28158
+ if (hooks.length === 0) continue;
28159
+ const kept = {
28086
28160
  ...omitPrototypePollutionKeys(group),
28087
- 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
28088
28178
  });
28179
+ else home.hooks.push(...remaining);
28089
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
+ });
28090
28186
  return merged;
28091
28187
  }
28188
+ /** The flat-list counterpart of {@link mergeMatcherGroups}, with the same positional rules. */
28092
28189
  function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned, logger }) {
28093
- 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 = [];
28094
28199
  for (const handler of existing) {
28095
28200
  if (isMatcherGroup(handler)) {
28096
28201
  warnSkip({
@@ -28100,17 +28205,93 @@ function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned,
28100
28205
  });
28101
28206
  continue;
28102
28207
  }
28103
- 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({
28104
28286
  handler,
28105
28287
  event,
28106
- matcher: void 0,
28288
+ matcher,
28107
28289
  seen,
28108
28290
  previouslyOwned,
28109
28291
  logger
28110
- })) continue;
28111
- preserved.push(cloneHandler(handler));
28292
+ })) kept.push(cloneHandler(handler));
28112
28293
  }
28113
- return [...generated, ...preserved];
28294
+ return kept;
28114
28295
  }
28115
28296
  function isMatcherGroup(value) {
28116
28297
  return isPlainObject$1(value) && Array.isArray(value.hooks);
@@ -28201,10 +28382,35 @@ function stableStringify$1(value) {
28201
28382
  function matcherKey(matcher) {
28202
28383
  return matcher === void 0 ? "" : stableStringify$1(matcher);
28203
28384
  }
28204
- /** The stable form of a matcher group's keys other than `matcher` and `hooks`. */
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
+ */
28205
28411
  function groupAttributesKey(group) {
28206
28412
  const { matcher: _matcher, hooks: _hooks, ...attributes } = group;
28207
- return stableStringify$1(attributes);
28413
+ return stableStringify$1(Object.fromEntries(Object.entries(attributes).filter(([key, value]) => value !== void 0 && GROUP_ATTRIBUTE_DEFAULTS[key] !== value)));
28208
28414
  }
28209
28415
  function warnSkip({ logger, event, expected }) {
28210
28416
  logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
@@ -42673,9 +42879,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42673
42879
  outputRoot,
42674
42880
  global
42675
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;
42676
42884
  const copilotCliMcpServers = addTypeField(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).filter(([name, server]) => {
42677
- if (server.disabled !== true || settings.document !== null) return true;
42678
- logger?.warn(`[CopilotcliMcp] Skipping disabled MCP server "${name}": ${(0, node_path.join)(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} cannot be parsed, so disabledMcpServers cannot be written to switch it off.`);
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.`);
42679
42887
  return false;
42680
42888
  })), logger);
42681
42889
  const mcpJson = {
@@ -42705,7 +42913,8 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42705
42913
  * Copilot CLI actually consults: an MCP file entry has no `disabled` field.
42706
42914
  * rulesync owns the toggle for the servers it writes; names it does not
42707
42915
  * manage keep their existing state. The file is not created or touched when
42708
- * there is nothing to disable and nothing to retract.
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).
42709
42918
  */
42710
42919
  static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
42711
42920
  const servers = rulesyncMcp.forTarget({
@@ -42723,7 +42932,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42723
42932
  return [];
42724
42933
  }
42725
42934
  const existing = settings.document.disabledMcpServers;
42726
- const existingDisabled = isStringArray$1(existing) ? existing : [];
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 ?? [];
42727
42940
  const managedNameSet = new Set(managedNames);
42728
42941
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
42729
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.`);
@@ -56729,7 +56942,7 @@ function convertClaudeToRulesyncPermissions(params) {
56729
56942
  * (https://docs.cline.bot/cli/cli-reference). The output is still generated for
56730
56943
  * compatibility, with a deprecation warning on every generation.
56731
56944
  */
56732
- const CLINE_PERMISSIONS_DEPRECATION_WARNING = "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. See https://docs.cline.bot/cli/cli-reference.";
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.";
56733
56946
  const ClineCommandPermissionsSchema = zod_mini.z.looseObject({
56734
56947
  allow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
56735
56948
  deny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
@@ -60245,6 +60458,193 @@ const AUTO_APPROVE_KEYS = {
60245
60458
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60246
60459
  */
60247
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
+ }
60248
60648
  function isCopilotDefaultPermissionMode(value) {
60249
60649
  return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
60250
60650
  }
@@ -60289,7 +60689,9 @@ function buildAutoApproveValue(rules) {
60289
60689
  * hand-written edits or urls map.
60290
60690
  * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
60291
60691
  * (the starting permission level of new chat sessions); without it the key is
60292
- * 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.
60293
60695
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
60294
60696
  * platform-dependent path outside rulesync's home-relative global model.
60295
60697
  */
@@ -60337,6 +60739,26 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60337
60739
  }
60338
60740
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
60339
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
+ }
60340
60762
  return new CopilotPermissions({
60341
60763
  outputRoot,
60342
60764
  relativeDirPath: paths.relativeDirPath,
@@ -60373,9 +60795,14 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60373
60795
  if (Object.keys(rules).length > 0) permission[category] = rules;
60374
60796
  }
60375
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
+ };
60376
60803
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
60377
60804
  permission,
60378
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { copilot: { defaultPermissionMode } }
60805
+ ...Object.keys(copilot).length > 0 && { copilot }
60379
60806
  }, null, 2) });
60380
60807
  }
60381
60808
  validate() {
@@ -99651,9 +100078,10 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
99651
100078
  collisionPolicy: "fold",
99652
100079
  projectInstructionBudget: {
99653
100080
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
99654
- 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`.",
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).",
99655
100082
  readConfiguredBytes: readCodexcliProjectDocMaxBytes,
99656
- configuredSource: "`project_doc_max_bytes` in `.codex/config.toml`"
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."
99657
100085
  }
99658
100086
  }
99659
100087
  }],
@@ -100232,6 +100660,7 @@ var RulesProcessor = class extends FeatureProcessor {
100232
100660
  const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
100233
100661
  const limitBytes = configuredBytes ?? budget.bytes;
100234
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;
100235
100664
  const files = toolRules.map((rule) => ({
100236
100665
  dir: normalizeRelativeDir(rule.getRelativeDirPath()),
100237
100666
  path: toPosixPath((0, node_path.join)(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
@@ -100242,7 +100671,7 @@ var RulesProcessor = class extends FeatureProcessor {
100242
100671
  const rootFiles = files.filter(({ dir }) => dir === ".");
100243
100672
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100244
100673
  if (rootBytes > limitBytes) {
100245
- this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${budget.remedy}`);
100674
+ this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${remedy}`);
100246
100675
  return;
100247
100676
  }
100248
100677
  const reportedDirs = [];
@@ -100253,7 +100682,7 @@ var RulesProcessor = class extends FeatureProcessor {
100253
100682
  const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
100254
100683
  if (chainBytes > limitBytes) {
100255
100684
  reportedDirs.push(file.dir);
100256
- 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). ${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}`);
100257
100686
  }
100258
100687
  }
100259
100688
  }
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_import = require("./import-S70XttcD.cjs");
2
+ const require_import = require("./import-DYYG88Rs.cjs");
3
3
  //#region src/index.ts
4
4
  async function generate(options = {}) {
5
5
  const { silent = true, verbose = false, ...rest } = options;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Vn as ALL_FEATURES, bt as ConsoleLogger, dn as ALL_TOOL_TARGETS, dt as ConfigResolver, i as inspectInputRoots, o as convertFromTool$1, r as generate$1, t as importFromTool$1 } from "./import-BPSwSahy.js";
1
+ import { Vn as ALL_FEATURES, bt as ConsoleLogger, dn as ALL_TOOL_TARGETS, dt as ConfigResolver, i as inspectInputRoots, o as convertFromTool$1, r as generate$1, t as importFromTool$1 } from "./import-BsEFYJsg.js";
2
2
  //#region src/index.ts
3
3
  async function generate(options = {}) {
4
4
  const { silent = true, verbose = false, ...rest } = options;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rulesync",
3
- "version": "37.0.0",
3
+ "version": "37.1.0",
4
4
  "description": "Unified AI rules management CLI tool that generates configuration files for various AI development tools",
5
5
  "keywords": [
6
6
  "ai",