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.
@@ -7895,14 +7895,53 @@ const COPILOT_DEFAULT_PERMISSION_MODES = [
7895
7895
  "autoApprove",
7896
7896
  "autopilot"
7897
7897
  ];
7898
+ /** `chat.agent.sandbox.enabled`: `"off"` or `"on"`. */
7899
+ const CopilotSandboxEnabledSchema = z.enum(["off", "on"]);
7900
+ /** `chat.agent.sandbox.fileSystem.userConfiguredPaths`. */
7901
+ const CopilotSandboxUserConfiguredPathsSchema = z.strictObject({
7902
+ readwritePaths: z.optional(z.array(z.string())),
7903
+ readonlyPaths: z.optional(z.array(z.string())),
7904
+ deniedPaths: z.optional(z.array(z.string()))
7905
+ });
7906
+ /**
7907
+ * The Agent Host sandbox settings VS Code 1.141 settled on, as a nested object
7908
+ * whose path mirrors the dotted setting key under `chat.agent.sandbox.` — e.g.
7909
+ * `network.allowNetwork` writes `chat.agent.sandbox.network.allowNetwork`.
7910
+ * Only the documented, registered keys are accepted; the deprecated 1.138
7911
+ * shapes (`chat.agent.sandbox.allowNetwork`, `.fileSystem.linux|mac|windows`)
7912
+ * are not modeled, since the Agent Host ignores them.
7913
+ *
7914
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
7915
+ * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
7916
+ */
7917
+ const CopilotSandboxSchema = z.strictObject({
7918
+ enabled: z.optional(CopilotSandboxEnabledSchema),
7919
+ network: z.optional(z.strictObject({
7920
+ allowNetwork: z.optional(z.boolean()),
7921
+ allowLocalNetwork: z.optional(z.boolean())
7922
+ })),
7923
+ allowUnsandboxedCommands: z.optional(z.boolean()),
7924
+ mcpServers: z.optional(z.boolean()),
7925
+ lspServers: z.optional(z.boolean()),
7926
+ credentials: z.optional(z.strictObject({
7927
+ authenticategit: z.optional(z.boolean()),
7928
+ authenticategh: z.optional(z.boolean())
7929
+ })),
7930
+ fileSystem: z.optional(z.strictObject({
7931
+ allowDevToolAccess: z.optional(z.boolean()),
7932
+ userConfiguredPaths: z.optional(CopilotSandboxUserConfiguredPathsSchema)
7933
+ }))
7934
+ });
7898
7935
  /**
7899
7936
  * Tool-scoped override block for GitHub Copilot in VS Code.
7900
7937
  * `defaultPermissionMode` writes the workspace `chat.permissions.default`
7901
- * setting, which has no canonical category.
7938
+ * setting, and `sandbox` writes the `chat.agent.sandbox.*` settings; neither
7939
+ * has a canonical category.
7902
7940
  */
7903
7941
  const CopilotPermissionsOverrideSchema = z.looseObject({
7904
7942
  permission: z.optional(ToolScopedPermissionSchema),
7905
- defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES))
7943
+ defaultPermissionMode: z.optional(z.enum(COPILOT_DEFAULT_PERMISSION_MODES)),
7944
+ sandbox: z.optional(CopilotSandboxSchema)
7906
7945
  });
7907
7946
  /**
7908
7947
  * oh-my-pi's `tools.approvalMode` values: the tool tiers it auto-approves
@@ -12874,6 +12913,16 @@ const SHARED_CONFIG_OWNERSHIP = {
12874
12913
  "chat.tools.edits.autoApprove",
12875
12914
  "chat.tools.urls.autoApprove",
12876
12915
  "chat.permissions.default",
12916
+ "chat.agent.sandbox.enabled",
12917
+ "chat.agent.sandbox.network.allowNetwork",
12918
+ "chat.agent.sandbox.network.allowLocalNetwork",
12919
+ "chat.agent.sandbox.allowUnsandboxedCommands",
12920
+ "chat.agent.sandbox.mcpServers",
12921
+ "chat.agent.sandbox.lspServers",
12922
+ "chat.agent.sandbox.credentials.authenticategit",
12923
+ "chat.agent.sandbox.credentials.authenticategh",
12924
+ "chat.agent.sandbox.fileSystem.allowDevToolAccess",
12925
+ "chat.agent.sandbox.fileSystem.userConfiguredPaths",
12877
12926
  "zoo-code.allowedCommands",
12878
12927
  "zoo-code.deniedCommands",
12879
12928
  "roo-cline.allowedCommands",
@@ -28036,8 +28085,34 @@ function collectOwnedRefs({ generatedHooks, shape }) {
28036
28085
  }
28037
28086
  return owned;
28038
28087
  }
28088
+ /**
28089
+ * The merge keeps every unowned existing handler where it is. Some tools key
28090
+ * state by position — Codex CLI persists a hook's trust review under its event,
28091
+ * matcher group index and handler index — so moving a handler the user already
28092
+ * reviewed would silently change which hook that review belongs to. Hence:
28093
+ * - an unowned existing handler stays at its index;
28094
+ * - a newly generated handler is appended to the first existing group with its
28095
+ * matcher and group-level keys, or in a new group after the existing ones;
28096
+ * - within a group, the slots rulesync's own handlers occupy (including the
28097
+ * appended ones) are filled in generated order, so reordering the sources
28098
+ * still reorders the generated handlers, which matters wherever handlers
28099
+ * run in order (a Qwen Code `sequential` group, a Cursor list).
28100
+ *
28101
+ * A retracted handler, or a group left empty by retractions, is still removed,
28102
+ * which shifts whatever follows it in the same list; that is unavoidable
28103
+ * without leaving a placeholder behind.
28104
+ */
28039
28105
  function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned, logger }) {
28040
- const merged = [...generated];
28106
+ const pending = createPendingGenerated();
28107
+ for (const group of generated) if (isMatcherGroup(group)) addPending({
28108
+ pending,
28109
+ handlers: group.hooks,
28110
+ event,
28111
+ matcher: matcherKey(group.matcher),
28112
+ attributes: groupAttributesKey(group)
28113
+ });
28114
+ const merged = [];
28115
+ const homes = /* @__PURE__ */ new Map();
28041
28116
  for (const group of existing) {
28042
28117
  if (!isMatcherGroup(group)) {
28043
28118
  warnSkip({
@@ -28048,27 +28123,57 @@ function mergeMatcherGroups({ existing, generated, event, seen, previouslyOwned,
28048
28123
  continue;
28049
28124
  }
28050
28125
  const matcher = matcherKey(group.matcher);
28051
- const preserved = group.hooks.filter((handler) => shouldPreserve({
28052
- handler,
28126
+ const hooks = keepExistingHandlers({
28127
+ handlers: group.hooks,
28053
28128
  event,
28054
28129
  matcher,
28130
+ attributes: groupAttributesKey(group),
28131
+ pending,
28055
28132
  seen,
28056
28133
  previouslyOwned,
28057
28134
  logger
28058
- })).map((handler) => cloneHandler(handler));
28059
- if (preserved.length === 0) continue;
28060
- const attributes = groupAttributesKey(group);
28061
- const target = merged.find((candidate) => isMatcherGroup(candidate) && matcherKey(candidate.matcher) === matcher && groupAttributesKey(candidate) === attributes);
28062
- if (target !== void 0 && isMatcherGroup(target)) target.hooks.push(...preserved);
28063
- else merged.push({
28135
+ });
28136
+ if (hooks.length === 0) continue;
28137
+ const kept = {
28064
28138
  ...omitPrototypePollutionKeys(group),
28065
- hooks: preserved
28139
+ hooks
28140
+ };
28141
+ merged.push(kept);
28142
+ const home = groupSlotKey(group);
28143
+ if (!homes.has(home)) homes.set(home, kept);
28144
+ }
28145
+ for (const group of generated) {
28146
+ if (!isMatcherGroup(group)) {
28147
+ merged.push(group);
28148
+ continue;
28149
+ }
28150
+ const remaining = group.hooks.filter((handler) => !pending.placed.has(handler));
28151
+ if (remaining.length === 0) continue;
28152
+ const home = homes.get(groupSlotKey(group));
28153
+ if (home === void 0) merged.push({
28154
+ ...group,
28155
+ hooks: remaining
28066
28156
  });
28157
+ else home.hooks.push(...remaining);
28067
28158
  }
28159
+ const order = generatedOrder(generated.flatMap((group) => isMatcherGroup(group) ? group.hooks : []));
28160
+ for (const group of merged) if (isMatcherGroup(group)) orderGeneratedSlots({
28161
+ handlers: group.hooks,
28162
+ order
28163
+ });
28068
28164
  return merged;
28069
28165
  }
28166
+ /** The flat-list counterpart of {@link mergeMatcherGroups}, with the same positional rules. */
28070
28167
  function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned, logger }) {
28071
- const preserved = [];
28168
+ const pending = createPendingGenerated();
28169
+ addPending({
28170
+ pending,
28171
+ handlers: generated,
28172
+ event,
28173
+ matcher: void 0,
28174
+ attributes: ""
28175
+ });
28176
+ const flat = [];
28072
28177
  for (const handler of existing) {
28073
28178
  if (isMatcherGroup(handler)) {
28074
28179
  warnSkip({
@@ -28078,17 +28183,93 @@ function mergeFlatHandlers({ existing, generated, event, seen, previouslyOwned,
28078
28183
  });
28079
28184
  continue;
28080
28185
  }
28081
- if (!shouldPreserve({
28186
+ flat.push(handler);
28187
+ }
28188
+ const merged = keepExistingHandlers({
28189
+ handlers: flat,
28190
+ event,
28191
+ matcher: void 0,
28192
+ attributes: "",
28193
+ pending,
28194
+ seen,
28195
+ previouslyOwned,
28196
+ logger
28197
+ });
28198
+ merged.push(...generated.filter((handler) => !pending.placed.has(handler)));
28199
+ orderGeneratedSlots({
28200
+ handlers: merged,
28201
+ order: generatedOrder(generated)
28202
+ });
28203
+ return merged;
28204
+ }
28205
+ /** Each generated handler's position in the generated list, by object identity. */
28206
+ function generatedOrder(handlers) {
28207
+ const order = /* @__PURE__ */ new Map();
28208
+ for (const [index, handler] of handlers.entries()) if (isPlainObject$1(handler)) order.set(handler, index);
28209
+ return order;
28210
+ }
28211
+ /**
28212
+ * Rewrite, in place, the slots of `handlers` that hold generated handlers so
28213
+ * they follow the generated order. Every other slot — an unowned handler — is
28214
+ * left at its index.
28215
+ */
28216
+ function orderGeneratedSlots({ handlers, order }) {
28217
+ const slots = [];
28218
+ for (const [index, handler] of handlers.entries()) if (order.has(handler)) slots.push(index);
28219
+ const sorted = slots.map((index) => handlers[index]).toSorted((left, right) => (order.get(left) ?? 0) - (order.get(right) ?? 0));
28220
+ for (const [position, index] of slots.entries()) handlers[index] = sorted[position];
28221
+ }
28222
+ function createPendingGenerated() {
28223
+ return {
28224
+ byKey: /* @__PURE__ */ new Map(),
28225
+ placed: /* @__PURE__ */ new Set()
28226
+ };
28227
+ }
28228
+ function addPending({ pending, handlers, event, matcher, attributes }) {
28229
+ for (const handler of handlers) {
28230
+ if (!isPlainObject$1(handler)) continue;
28231
+ const key = pendingKey({
28232
+ event,
28233
+ matcher,
28234
+ attributes,
28235
+ handler
28236
+ });
28237
+ const queue = pending.byKey.get(key);
28238
+ if (queue === void 0) pending.byKey.set(key, [handler]);
28239
+ else queue.push(handler);
28240
+ }
28241
+ }
28242
+ /**
28243
+ * Walk one existing list in order and decide each handler: replaced in place
28244
+ * by its generated copy, kept as unowned, or dropped (retracted or duplicate).
28245
+ */
28246
+ function keepExistingHandlers({ handlers, event, matcher, attributes, pending, seen, previouslyOwned, logger }) {
28247
+ const kept = [];
28248
+ for (const handler of handlers) {
28249
+ if (isPlainObject$1(handler)) {
28250
+ const key = pendingKey({
28251
+ event,
28252
+ matcher,
28253
+ attributes,
28254
+ handler
28255
+ });
28256
+ const generated = pending.byKey.get(key)?.shift();
28257
+ if (generated !== void 0) {
28258
+ pending.placed.add(generated);
28259
+ kept.push(generated);
28260
+ continue;
28261
+ }
28262
+ }
28263
+ if (shouldPreserve({
28082
28264
  handler,
28083
28265
  event,
28084
- matcher: void 0,
28266
+ matcher,
28085
28267
  seen,
28086
28268
  previouslyOwned,
28087
28269
  logger
28088
- })) continue;
28089
- preserved.push(cloneHandler(handler));
28270
+ })) kept.push(cloneHandler(handler));
28090
28271
  }
28091
- return [...generated, ...preserved];
28272
+ return kept;
28092
28273
  }
28093
28274
  function isMatcherGroup(value) {
28094
28275
  return isPlainObject$1(value) && Array.isArray(value.hooks);
@@ -28179,10 +28360,35 @@ function stableStringify$1(value) {
28179
28360
  function matcherKey(matcher) {
28180
28361
  return matcher === void 0 ? "" : stableStringify$1(matcher);
28181
28362
  }
28182
- /** The stable form of a matcher group's keys other than `matcher` and `hooks`. */
28363
+ function pendingKey({ event, matcher, attributes, handler }) {
28364
+ const key = ownedHookKey({
28365
+ event,
28366
+ matcher,
28367
+ identity: handlerIdentity(handler)
28368
+ });
28369
+ return JSON.stringify([key, attributes]);
28370
+ }
28371
+ /** Groups that share a matcher and attributes are interchangeable homes for a handler. */
28372
+ function groupSlotKey(group) {
28373
+ return JSON.stringify([matcherKey(group.matcher), groupAttributesKey(group)]);
28374
+ }
28375
+ /** Group-level keys whose default value is known, so an explicit default can be ignored. */
28376
+ const GROUP_ATTRIBUTE_DEFAULTS = Object.freeze({
28377
+ __proto__: null,
28378
+ sequential: false
28379
+ });
28380
+ /**
28381
+ * The stable form of a matcher group's keys other than `matcher` and `hooks`.
28382
+ *
28383
+ * A key set to its known default is the same group as one that omits it:
28384
+ * Qwen Code's `sequential` defaults to `false` and rulesync emits it only when
28385
+ * `true`, so an explicit `sequential: false` (or any `undefined` key) is
28386
+ * dropped before comparing. Other `false` values are kept, since their default
28387
+ * is not known here.
28388
+ */
28183
28389
  function groupAttributesKey(group) {
28184
28390
  const { matcher: _matcher, hooks: _hooks, ...attributes } = group;
28185
- return stableStringify$1(attributes);
28391
+ return stableStringify$1(Object.fromEntries(Object.entries(attributes).filter(([key, value]) => value !== void 0 && GROUP_ATTRIBUTE_DEFAULTS[key] !== value)));
28186
28392
  }
28187
28393
  function warnSkip({ logger, event, expected }) {
28188
28394
  logger?.warn(`Skipping existing hook entry on ${event}: expected ${expected}`);
@@ -42651,9 +42857,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42651
42857
  outputRoot,
42652
42858
  global
42653
42859
  });
42860
+ const existingDisabled = settings.document?.disabledMcpServers;
42861
+ 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;
42654
42862
  const copilotCliMcpServers = addTypeField(Object.fromEntries(Object.entries(rulesyncMcp.getMcpServers()).filter(([name, server]) => {
42655
- if (server.disabled !== true || settings.document !== null) return true;
42656
- logger?.warn(`[CopilotcliMcp] Skipping disabled MCP server "${name}": ${join(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} cannot be parsed, so disabledMcpServers cannot be written to switch it off.`);
42863
+ if (server.disabled !== true || offSwitchBlocker === void 0) return true;
42864
+ logger?.warn(`[CopilotcliMcp] Skipping disabled MCP server "${name}": ${join(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} ${offSwitchBlocker}, so disabledMcpServers cannot be written to switch it off.`);
42657
42865
  return false;
42658
42866
  })), logger);
42659
42867
  const mcpJson = {
@@ -42683,7 +42891,8 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42683
42891
  * Copilot CLI actually consults: an MCP file entry has no `disabled` field.
42684
42892
  * rulesync owns the toggle for the servers it writes; names it does not
42685
42893
  * manage keep their existing state. The file is not created or touched when
42686
- * there is nothing to disable and nothing to retract.
42894
+ * there is nothing to disable and nothing to retract, nor when the existing
42895
+ * `disabledMcpServers` is not a string array (left as is, with a warning).
42687
42896
  */
42688
42897
  static async getAuxiliaryFiles({ outputRoot = process.cwd(), global = false, rulesyncMcp, logger }) {
42689
42898
  const servers = rulesyncMcp.forTarget({
@@ -42701,7 +42910,11 @@ var CopilotcliMcp = class CopilotcliMcp extends ToolMcp {
42701
42910
  return [];
42702
42911
  }
42703
42912
  const existing = settings.document.disabledMcpServers;
42704
- const existingDisabled = isStringArray$1(existing) ? existing : [];
42913
+ if (existing !== void 0 && !isStringArray$1(existing)) {
42914
+ logger?.warn(`Skipping the Copilot CLI disabledMcpServers update: the existing value is not an array of server names. Fix it in ${join(settings.paths.relativeDirPath, settings.paths.relativeFilePath)} so rulesync can manage the toggle.`);
42915
+ return [];
42916
+ }
42917
+ const existingDisabled = existing ?? [];
42705
42918
  const managedNameSet = new Set(managedNames);
42706
42919
  const reEnabled = existingDisabled.filter((name) => managedNameSet.has(name) && !disabledNames.includes(name));
42707
42920
  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.`);
@@ -56707,7 +56920,7 @@ function convertClaudeToRulesyncPermissions(params) {
56707
56920
  * (https://docs.cline.bot/cli/cli-reference). The output is still generated for
56708
56921
  * compatibility, with a deprecation warning on every generation.
56709
56922
  */
56710
- 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.";
56923
+ 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.";
56711
56924
  const ClineCommandPermissionsSchema = z.looseObject({
56712
56925
  allow: z.optional(z.array(z.string())),
56713
56926
  deny: z.optional(z.array(z.string())),
@@ -60223,6 +60436,193 @@ const AUTO_APPROVE_KEYS = {
60223
60436
  * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60224
60437
  */
60225
60438
  const DEFAULT_PERMISSION_MODE_KEY = "chat.permissions.default";
60439
+ /**
60440
+ * The prefix every Agent Host sandbox setting shares. A `copilot.sandbox` leaf
60441
+ * at path `a.b` is written to `chat.agent.sandbox.a.b`.
60442
+ */
60443
+ const SANDBOX_KEY_PREFIX = "chat.agent.sandbox.";
60444
+ /**
60445
+ * The `copilot.sandbox` leaves rulesync manages, each a flat VS Code setting
60446
+ * (`SANDBOX_KEY_PREFIX` + the dotted path) with the value shape VS Code
60447
+ * registers for it. The shape is re-checked on import, so a hand-written value
60448
+ * VS Code would not accept is skipped instead of failing the whole import.
60449
+ *
60450
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
60451
+ * @see https://code.visualstudio.com/docs/agents/reference/ai-settings
60452
+ */
60453
+ const SANDBOX_LEAVES = [
60454
+ {
60455
+ path: ["enabled"],
60456
+ schema: CopilotSandboxEnabledSchema,
60457
+ normalize: (value) => value === true ? "on" : value === false ? "off" : value
60458
+ },
60459
+ {
60460
+ path: ["network", "allowNetwork"],
60461
+ schema: z.boolean()
60462
+ },
60463
+ {
60464
+ path: ["network", "allowLocalNetwork"],
60465
+ schema: z.boolean()
60466
+ },
60467
+ {
60468
+ path: ["allowUnsandboxedCommands"],
60469
+ schema: z.boolean()
60470
+ },
60471
+ {
60472
+ path: ["mcpServers"],
60473
+ schema: z.boolean()
60474
+ },
60475
+ {
60476
+ path: ["lspServers"],
60477
+ schema: z.boolean()
60478
+ },
60479
+ {
60480
+ path: ["credentials", "authenticategit"],
60481
+ schema: z.boolean()
60482
+ },
60483
+ {
60484
+ path: ["credentials", "authenticategh"],
60485
+ schema: z.boolean()
60486
+ },
60487
+ {
60488
+ path: ["fileSystem", "allowDevToolAccess"],
60489
+ schema: z.boolean()
60490
+ },
60491
+ {
60492
+ path: ["fileSystem", "userConfiguredPaths"],
60493
+ schema: CopilotSandboxUserConfiguredPathsSchema
60494
+ }
60495
+ ];
60496
+ const USER_CONFIGURED_PATHS_KEY = `${SANDBOX_KEY_PREFIX}fileSystem.userConfiguredPaths`;
60497
+ /**
60498
+ * `copilot.sandbox` values that loosen the Agent Host sandbox. They are still
60499
+ * written — a sandbox policy is the user's call — but never silently. Each
60500
+ * `widens` names the value that does *not* loosen anything and reports the
60501
+ * rest, so an unexpected value is reported rather than ignored.
60502
+ *
60503
+ * @see https://code.visualstudio.com/docs/agents/run/agent-sandboxing
60504
+ */
60505
+ const COPILOT_TRUST_AFFECTING_SANDBOX_PATHS = [
60506
+ {
60507
+ path: ["enabled"],
60508
+ reason: "turns the sandbox off, so agent terminal commands run unconfined",
60509
+ widens: (value) => value !== "on"
60510
+ },
60511
+ {
60512
+ path: ["network", "allowNetwork"],
60513
+ reason: "lets sandboxed commands reach the external network",
60514
+ widens: isNotFalse
60515
+ },
60516
+ {
60517
+ path: ["network", "allowLocalNetwork"],
60518
+ reason: "lets sandboxed commands reach local network resources",
60519
+ widens: isNotFalse
60520
+ },
60521
+ {
60522
+ path: ["allowUnsandboxedCommands"],
60523
+ reason: "lets the agent ask to run a blocked command outside the sandbox",
60524
+ widens: isNotFalse
60525
+ },
60526
+ {
60527
+ path: ["mcpServers"],
60528
+ reason: "runs locally launched MCP servers outside the sandbox",
60529
+ widens: isNotTrue
60530
+ },
60531
+ {
60532
+ path: ["lspServers"],
60533
+ reason: "runs language servers outside the sandbox",
60534
+ widens: isNotTrue
60535
+ },
60536
+ {
60537
+ path: ["credentials", "authenticategit"],
60538
+ reason: "hands your Git credentials to sandboxed commands",
60539
+ widens: isNotFalse
60540
+ },
60541
+ {
60542
+ path: ["credentials", "authenticategh"],
60543
+ reason: "hands your GitHub CLI authentication to sandboxed commands",
60544
+ widens: isNotFalse
60545
+ },
60546
+ {
60547
+ path: ["fileSystem", "allowDevToolAccess"],
60548
+ reason: "grants sandboxed commands developer-tool directories, which can hold credentials such as registry tokens",
60549
+ widens: isNotFalse
60550
+ },
60551
+ {
60552
+ path: [
60553
+ "fileSystem",
60554
+ "userConfiguredPaths",
60555
+ "readwritePaths"
60556
+ ],
60557
+ reason: "adds paths sandboxed commands may write to, outside the working directory",
60558
+ widens: isNonEmptyList
60559
+ },
60560
+ {
60561
+ path: [
60562
+ "fileSystem",
60563
+ "userConfiguredPaths",
60564
+ "readonlyPaths"
60565
+ ],
60566
+ reason: "adds paths sandboxed commands may read, outside the working directory",
60567
+ widens: isNonEmptyList
60568
+ }
60569
+ ];
60570
+ const COPILOT_TOOL_LABEL = "GitHub Copilot";
60571
+ function sandboxKey(path) {
60572
+ return `${SANDBOX_KEY_PREFIX}${path.join(".")}`;
60573
+ }
60574
+ /**
60575
+ * Patch entries for every `copilot.sandbox` leaf the canonical config states.
60576
+ * Leaves it does not state are left as the file has them, so authoring only
60577
+ * `network.allowNetwork` never disturbs a hand-written `enabled`. The
60578
+ * `userConfiguredPaths` object merges per list for the same reason: authoring
60579
+ * `readwritePaths` keeps a hand-written `deniedPaths`, which would otherwise be
60580
+ * a restriction lost without a word.
60581
+ */
60582
+ function buildSandboxPatch({ sandbox, existingSettings }) {
60583
+ const patch = {};
60584
+ for (const { path } of SANDBOX_LEAVES) {
60585
+ const value = readSandboxPath({
60586
+ sandbox,
60587
+ path
60588
+ });
60589
+ if (value === void 0 || value === UNREADABLE_SANDBOX_PATH) continue;
60590
+ const key = sandboxKey(path);
60591
+ if (key === USER_CONFIGURED_PATHS_KEY && isPlainObject$1(value)) {
60592
+ const existing = existingSettings[key];
60593
+ patch[key] = {
60594
+ ...isPlainObject$1(existing) ? existing : {},
60595
+ ...value
60596
+ };
60597
+ continue;
60598
+ }
60599
+ patch[key] = value;
60600
+ }
60601
+ return patch;
60602
+ }
60603
+ /**
60604
+ * Lift every managed `chat.agent.sandbox.*` key the file holds back into a
60605
+ * nested `copilot.sandbox` object. A value whose shape VS Code would reject is
60606
+ * skipped; the write path leaves it in place.
60607
+ */
60608
+ function readSandboxFromSettings(settings) {
60609
+ const sandbox = {};
60610
+ for (const { path, schema, normalize } of SANDBOX_LEAVES) {
60611
+ const value = settings[sandboxKey(path)];
60612
+ const parsed = schema.safeParse(normalize === void 0 ? value : normalize(value));
60613
+ if (!parsed.success) continue;
60614
+ let cursor = sandbox;
60615
+ for (const segment of path.slice(0, -1)) {
60616
+ const next = cursor[segment];
60617
+ const child = isPlainObject$1(next) ? next : {};
60618
+ cursor[segment] = child;
60619
+ cursor = child;
60620
+ }
60621
+ const leaf = path[path.length - 1];
60622
+ if (leaf !== void 0) cursor[leaf] = parsed.data;
60623
+ }
60624
+ return Object.keys(sandbox).length > 0 ? sandbox : void 0;
60625
+ }
60226
60626
  function isCopilotDefaultPermissionMode(value) {
60227
60627
  return COPILOT_DEFAULT_PERMISSION_MODES.some((mode) => mode === value);
60228
60628
  }
@@ -60267,7 +60667,9 @@ function buildAutoApproveValue(rules) {
60267
60667
  * hand-written edits or urls map.
60268
60668
  * The `copilot.defaultPermissionMode` override writes `chat.permissions.default`
60269
60669
  * (the starting permission level of new chat sessions); without it the key is
60270
- * left untouched.
60670
+ * left untouched. The `copilot.sandbox` override writes the Agent Host sandbox
60671
+ * settings (`chat.agent.sandbox.*`, see {@link SANDBOX_LEAVES}) per key, and
60672
+ * warns about every value it writes that loosens the sandbox.
60271
60673
  * Only project scope is modeled: VS Code's user-scope settings.json lives at a
60272
60674
  * platform-dependent path outside rulesync's home-relative global model.
60273
60675
  */
@@ -60315,6 +60717,26 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60315
60717
  }
60316
60718
  const defaultPermissionMode = config.copilot?.defaultPermissionMode;
60317
60719
  if (defaultPermissionMode !== void 0) patch[DEFAULT_PERMISSION_MODE_KEY] = defaultPermissionMode;
60720
+ const sandbox = config.copilot?.sandbox;
60721
+ if (sandbox !== void 0) {
60722
+ Object.assign(patch, buildSandboxPatch({
60723
+ sandbox,
60724
+ existingSettings: parseSharedConfig({
60725
+ format: "jsonc",
60726
+ fileContent: existingContent,
60727
+ filePath
60728
+ })
60729
+ }));
60730
+ warnOnTrustAffectingEntries({
60731
+ toolLabel: COPILOT_TOOL_LABEL,
60732
+ entries: collectTrustAffectingSandboxPaths({
60733
+ sandbox,
60734
+ paths: COPILOT_TRUST_AFFECTING_SANDBOX_PATHS
60735
+ }),
60736
+ relativeFilePath: join(paths.relativeDirPath, paths.relativeFilePath),
60737
+ logger
60738
+ });
60739
+ }
60318
60740
  return new CopilotPermissions({
60319
60741
  outputRoot,
60320
60742
  relativeDirPath: paths.relativeDirPath,
@@ -60351,9 +60773,14 @@ var CopilotPermissions = class CopilotPermissions extends ToolPermissions {
60351
60773
  if (Object.keys(rules).length > 0) permission[category] = rules;
60352
60774
  }
60353
60775
  const defaultPermissionMode = settings[DEFAULT_PERMISSION_MODE_KEY];
60776
+ const sandbox = readSandboxFromSettings(settings);
60777
+ const copilot = {
60778
+ ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { defaultPermissionMode },
60779
+ ...sandbox !== void 0 && { sandbox }
60780
+ };
60354
60781
  return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify({
60355
60782
  permission,
60356
- ...isCopilotDefaultPermissionMode(defaultPermissionMode) && { copilot: { defaultPermissionMode } }
60783
+ ...Object.keys(copilot).length > 0 && { copilot }
60357
60784
  }, null, 2) });
60358
60785
  }
60359
60786
  validate() {
@@ -99629,9 +100056,10 @@ const toolRuleFactories = /* @__PURE__ */ new Map([
99629
100056
  collisionPolicy: "fold",
99630
100057
  projectInstructionBudget: {
99631
100058
  bytes: CODEXCLI_PROJECT_DOC_MAX_BYTES,
99632
- 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`.",
100059
+ 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).",
99633
100060
  readConfiguredBytes: readCodexcliProjectDocMaxBytes,
99634
- configuredSource: "`project_doc_max_bytes` in `.codex/config.toml`"
100061
+ configuredSource: "`project_doc_max_bytes` in `.codex/config.toml` (honored only if the project is trusted)",
100062
+ defaultLimitHint: "Rulesync does not read `~/.codex/config.toml`; ignore this warning if you already raised `project_doc_max_bytes` there."
99635
100063
  }
99636
100064
  }
99637
100065
  }],
@@ -100210,6 +100638,7 @@ var RulesProcessor = class extends FeatureProcessor {
100210
100638
  const configuredBytes = await budget.readConfiguredBytes?.(this.outputRoot);
100211
100639
  const limitBytes = configuredBytes ?? budget.bytes;
100212
100640
  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"}`;
100641
+ const remedy = configuredBytes === void 0 && budget.defaultLimitHint !== void 0 ? `${budget.remedy} ${budget.defaultLimitHint}` : budget.remedy;
100213
100642
  const files = toolRules.map((rule) => ({
100214
100643
  dir: normalizeRelativeDir(rule.getRelativeDirPath()),
100215
100644
  path: toPosixPath(join(rule.getRelativeDirPath(), rule.getRelativeFilePath())),
@@ -100220,7 +100649,7 @@ var RulesProcessor = class extends FeatureProcessor {
100220
100649
  const rootFiles = files.filter(({ dir }) => dir === ".");
100221
100650
  const rootBytes = rootFiles.reduce((sum, { bytes }) => sum + bytes, 0);
100222
100651
  if (rootBytes > limitBytes) {
100223
- this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${budget.remedy}`);
100652
+ this.logger.warn(`The generated ${this.toolTarget} root instruction file${location} (${rootFiles.map(({ path }) => path).join(", ")}) is ${rootBytes} bytes, over the ${limitLabel}. ${remedy}`);
100224
100653
  return;
100225
100654
  }
100226
100655
  const reportedDirs = [];
@@ -100231,7 +100660,7 @@ var RulesProcessor = class extends FeatureProcessor {
100231
100660
  const chainBytes = chain.reduce((sum, { bytes }) => sum + bytes, 0);
100232
100661
  if (chainBytes > limitBytes) {
100233
100662
  reportedDirs.push(file.dir);
100234
- 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}`);
100663
+ 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}`);
100235
100664
  }
100236
100665
  }
100237
100666
  }
@@ -104136,4 +104565,4 @@ async function importChecksCore(params) {
104136
104565
  //#endregion
104137
104566
  export { RulesyncHooks as $, removeDirectoryStrict as $t, groupSpellingsByCaseFoldedIdentity as A, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as An, applyFileMode as At, QWENCODE_LOCAL_RULE_FILE_NAME as B, parseCommaSeparatedList as Bn, getFileSizeAndExecutableBit as Bt, COMMANDCODE_SETTINGS_LOCAL_FILE_NAME as C, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as Cn, fallbackLogger as Ct, FACTORYDROID_DIR as D, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as Dn, withWarnOnceScope as Dt, ChecksProcessor as E, RULESYNC_IGNORE_RELATIVE_FILE_PATH as En, resetRunWarningState as Et, CLAUDECODE_LOCAL_RULE_FILE_NAME as F, RULESYNC_SKILLS_RELATIVE_DIR_PATH as Fn, createTempDirectory as Ft, RulesyncSubagentFrontmatterSchema as G, truncateText as Gn, listDirectoryEntryNames as Gt, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as H, ALL_FEATURES_WITH_WILDCARD as Hn, isFileNotFoundError as Ht, CLAUDECODE_MEMORIES_DIR_NAME as I, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as In, directoryExists as It, RulesyncRule as J, quoteForLog as Jn, pathEscapesRoot as Jt, RulesyncSkill as K, hasDeceptiveHiddenCharacters as Kn, listFilePathsRecursively as Kt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as L, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as Ln, ensureDir as Lt, CODEXCLI_DIR as M, RULESYNC_PERMISSIONS_SCHEMA_URL as Mn, assertTreeContainsNoSymlinks as Mt, CLAUDECODE_AGENT_MEMORY_LOCAL_DIR_NAME as N, RULESYNC_RELATIVE_DIR_PATH as Nn, assertWritablePathInsideRoot as Nt, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_MCP_RELATIVE_FILE_PATH as On, CLIError as Ot, CLAUDECODE_DIR as P, RULESYNC_RULES_RELATIVE_DIR_PATH as Pn, checkPathTraversal as Pt, RulesyncIgnore as Q, removeDirectory as Qt, CLAUDECODE_SKILLS_DIR_PATH as R, RULESYNC_USER_CONFIG_DIR_NAME as Rn, fileExists as Rt, COMMANDCODE_DIR as S, RULESYNC_CONFIG_SCHEMA_URL as Sn, WarningCollectingLogger as St, CODEBUDDY_LOCAL_RULE_FILE_NAME as T, RULESYNC_HOOKS_RELATIVE_FILE_PATH as Tn, withFallbackLoggerTarget as Tt, getLocalSkillDirNames as U, DEPRECATED_FEATURE_REPLACEMENTS as Un, isFileSystemError as Ut, AUGMENTCODE_DIR as V, ALL_FEATURES as Vn, getHomeDirectory as Vt, RulesyncSubagent as W, formatError as Wn, isSymlink as Wt, RulesyncPermissions as X, stripControlCharactersKeepingLineFeeds as Xn, readFileContent as Xt, RulesyncRuleFrontmatterSchema as Y, stripControlCharacters as Yn, posixRelativePathEscapesRoot as Yt, RulesyncMcp as Z, stripHiddenCharacters as Zn, readFileContentOrNull as Zt, LETTACODE_DIR as _, MAX_FILE_SIZE as _n, SourceEntrySchema as _t, getProcessorRegistryEntry as a, runWithDirectoryRollback as an, RulesyncCheck as at, CommandsProcessor as b, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as bn, ConsoleLogger as bt, RulesProcessor as c, writeFileBuffer as cn, loadYaml as ct, ELLIPSIS_WIDTH as d, ALL_TOOL_TARGETS as dn, ConfigResolver as dt, removeFile as en, getRulesyncSourceCandidates as et, displayWidthOf as f, ALL_TOOL_TARGETS_WITH_WILDCARD as fn, mergeInputRootConfigs as ft, HooksProcessor as g, FEATURE_SOURCE_TREE_ENTRIES as gn, GITIGNORE_DESTINATION_KEY as gt, IgnoreProcessor as h, CURATED_RULES_FEATURE_SUBDIR as hn, ConfigFileSchema as ht, inspectInputRoots as i, resolvedRelativePath as in, RulesyncCommandFrontmatterSchema as it, CODEXCLI_BASH_RULES_FILE_NAME as j, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as jn, assertDirectoryIfExists as jt, caseFoldIdentity as k, RULESYNC_MCP_SCHEMA_URL as kn, ErrorCodes as kt, SubagentsProcessor as l, writeFileContent as ln, SHARED_USER_MANAGED_CONFIG_PATHS as lt, McpProcessor as m, ToolTargetSchema as mn, CONFLICTING_TARGET_PAIRS as mt, formatSourceLoadFailure as n, removeTempDirectory as nn, parseJsonc as nt, convertFromTool as o, splitPathSegments as on, RulesyncCheckFrontmatterSchema as ot, shortenToWidth as p, PACKAGING_TOOL_TARGETS as pn, resolveEffectiveInputRoots as pt, RulesyncSkillFrontmatterSchema as q, hasEnclosingMarkOutsideKeycap as qn, listSubdirectoryNames as qt, generate as r, resolvePath as rn, RulesyncCommand as rt, isPackagingToolTarget as s, toPosixPath as sn, stringifyFrontmatter as st, importFromTool as t, removeFileStrict as tn, resolveRulesyncSourceWritePath as tt, SkillsProcessor as u, mapWithConcurrency as un, SKILL_FILE_NAME as ut, LETTACODE_SETTINGS_LOCAL_FILE_NAME as v, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as vn, assertTargetsFeaturesExclusive as vt, CODEBUDDY_DIR as w, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as wn, warnOnConflictingFlags as wt, REASONIX_LOCAL_RULE_FILE_NAME as x, RULESYNC_CONFIG_RELATIVE_FILE_PATH as xn, JsonLogger as xt, CRUSH_LOCAL_RULE_FILE_NAME as y, RULESYNC_CHECKS_RELATIVE_DIR_PATH as yn, findControlCharacter as yt, QWENCODE_DIR as z, RULESYNC_XDG_CONFIG_HOME_DEFAULT_DIR_NAME as zn, findFilesByGlobs as zt };
104138
104567
 
104139
- //# sourceMappingURL=import-BPSwSahy.js.map
104568
+ //# sourceMappingURL=import-BsEFYJsg.js.map