rulesync 16.22.0 → 16.23.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.
- package/dist/cli/index.cjs +3 -3
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/{import-CUyeYGxZ.cjs → import-D420jbVB.cjs} +651 -149
- package/dist/{import-BKqbq4Ut.js → import-DIDEUv63.js} +652 -150
- package/dist/import-DIDEUv63.js.map +1 -0
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/import-BKqbq4Ut.js.map +0 -1
|
@@ -3385,6 +3385,47 @@ var RulesyncFile = class extends AiFile {
|
|
|
3385
3385
|
}
|
|
3386
3386
|
};
|
|
3387
3387
|
//#endregion
|
|
3388
|
+
//#region src/utils/bounded-walk.ts
|
|
3389
|
+
const ALIAS_HINT = "(a chain of YAML aliases may be amplifying the document)";
|
|
3390
|
+
/**
|
|
3391
|
+
* Create the bookkeeping for one walk. `subject` names the document kind in
|
|
3392
|
+
* every error ("Frontmatter", "Shared config"); `root`, when given, is entered
|
|
3393
|
+
* up front so the root object counts as the first nesting level, matching the
|
|
3394
|
+
* +1 that `enter` applies to every container nested inside it.
|
|
3395
|
+
*/
|
|
3396
|
+
function createBoundedWalk({ subject, limits, root }) {
|
|
3397
|
+
const ancestors = /* @__PURE__ */ new WeakSet();
|
|
3398
|
+
let valuesRemaining = limits.maxValues;
|
|
3399
|
+
let stringCharsRemaining = limits.maxStringChars;
|
|
3400
|
+
let depth = 0;
|
|
3401
|
+
const chargeChars = (chars) => {
|
|
3402
|
+
stringCharsRemaining -= chars;
|
|
3403
|
+
if (stringCharsRemaining < 0) throw new Error(`${subject}'s string values expand to more than ${limits.maxStringChars} characters; refusing to process it ${ALIAS_HINT}`);
|
|
3404
|
+
};
|
|
3405
|
+
const chargeValue = (stringChars = 0) => {
|
|
3406
|
+
valuesRemaining -= 1;
|
|
3407
|
+
if (valuesRemaining < 0) throw new Error(`${subject} expands to more than ${limits.maxValues} values; refusing to process it ${ALIAS_HINT}`);
|
|
3408
|
+
chargeChars(stringChars);
|
|
3409
|
+
};
|
|
3410
|
+
const enter = (container) => {
|
|
3411
|
+
depth += 1;
|
|
3412
|
+
if (depth > limits.maxDepth) throw new Error(`${subject} nests more than ${limits.maxDepth} levels deep; refusing to process it ${ALIAS_HINT}`);
|
|
3413
|
+
ancestors.add(container);
|
|
3414
|
+
};
|
|
3415
|
+
const leave = (container) => {
|
|
3416
|
+
ancestors.delete(container);
|
|
3417
|
+
depth -= 1;
|
|
3418
|
+
};
|
|
3419
|
+
if (root !== void 0) enter(root);
|
|
3420
|
+
return {
|
|
3421
|
+
chargeValue,
|
|
3422
|
+
chargeChars,
|
|
3423
|
+
isAncestor: (container) => ancestors.has(container),
|
|
3424
|
+
enter,
|
|
3425
|
+
leave
|
|
3426
|
+
};
|
|
3427
|
+
}
|
|
3428
|
+
//#endregion
|
|
3388
3429
|
//#region src/utils/prototype-pollution.ts
|
|
3389
3430
|
/**
|
|
3390
3431
|
* Keys that, if walked into when constructing or merging objects from
|
|
@@ -3524,30 +3565,6 @@ const MAX_FRONTMATTER_STRING_CHARS = 4e6;
|
|
|
3524
3565
|
* manifest stays well under this.
|
|
3525
3566
|
*/
|
|
3526
3567
|
const MAX_FRONTMATTER_RAW_CHARS = 65536;
|
|
3527
|
-
/** Charge string content (a string leaf or an object key) against the character budget. */
|
|
3528
|
-
function chargeStringChars({ options, chars }) {
|
|
3529
|
-
options.budget.stringCharsRemaining -= chars;
|
|
3530
|
-
if (options.budget.stringCharsRemaining < 0) throw new Error(`Frontmatter's string values expand to more than ${MAX_FRONTMATTER_STRING_CHARS} characters; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3531
|
-
}
|
|
3532
|
-
function consumeBudget({ options, stringChars = 0 }) {
|
|
3533
|
-
options.budget.remaining -= 1;
|
|
3534
|
-
if (options.budget.remaining < 0) throw new Error(`Frontmatter expands to more than ${MAX_FRONTMATTER_VALUES} values; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3535
|
-
chargeStringChars({
|
|
3536
|
-
options,
|
|
3537
|
-
chars: stringChars
|
|
3538
|
-
});
|
|
3539
|
-
}
|
|
3540
|
-
/** Enter one more container level, throwing if the depth cap is exceeded. */
|
|
3541
|
-
function enterContainer({ options, container }) {
|
|
3542
|
-
options.depth += 1;
|
|
3543
|
-
if (options.depth > 64) throw new Error(`Frontmatter nests more than 64 levels deep; refusing to process it (a chain of YAML aliases may be amplifying the document)`);
|
|
3544
|
-
options.ancestors.add(container);
|
|
3545
|
-
}
|
|
3546
|
-
/** Leave a container level entered via {@link enterContainer}. */
|
|
3547
|
-
function leaveContainer({ options, container }) {
|
|
3548
|
-
options.ancestors.delete(container);
|
|
3549
|
-
options.depth -= 1;
|
|
3550
|
-
}
|
|
3551
3568
|
/**
|
|
3552
3569
|
* Estimate the serialized character cost of a leaf that is not a string (a
|
|
3553
3570
|
* string leaf is charged by its own length instead).
|
|
@@ -3570,40 +3587,26 @@ function estimateLeafChars(value) {
|
|
|
3570
3587
|
* bounded by {@link MAX_FRONTMATTER_VALUES} instead.
|
|
3571
3588
|
*/
|
|
3572
3589
|
function deepCleanValue(value, options) {
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
stringChars: typeof value === "string" ? value.length : estimateLeafChars(value)
|
|
3576
|
-
});
|
|
3590
|
+
const leafChars = typeof value === "string" ? value.length : estimateLeafChars(value);
|
|
3591
|
+
options.walk.chargeValue(leafChars);
|
|
3577
3592
|
if (value === null || value === void 0) return;
|
|
3578
3593
|
if (typeof value === "string") return options.transformString ? options.transformString(value) : value;
|
|
3579
3594
|
if (Array.isArray(value)) {
|
|
3580
|
-
if (options.
|
|
3581
|
-
|
|
3582
|
-
options,
|
|
3583
|
-
container: value
|
|
3584
|
-
});
|
|
3595
|
+
if (options.walk.isAncestor(value)) return;
|
|
3596
|
+
options.walk.enter(value);
|
|
3585
3597
|
const cleanedArray = [];
|
|
3586
3598
|
for (const item of value) {
|
|
3587
3599
|
const cleaned = deepCleanValue(item, options);
|
|
3588
3600
|
if (cleaned !== void 0) cleanedArray.push(cleaned);
|
|
3589
3601
|
}
|
|
3590
|
-
|
|
3591
|
-
options,
|
|
3592
|
-
container: value
|
|
3593
|
-
});
|
|
3602
|
+
options.walk.leave(value);
|
|
3594
3603
|
return cleanedArray;
|
|
3595
3604
|
}
|
|
3596
3605
|
if (isPlainObject$1(value)) {
|
|
3597
|
-
if (options.
|
|
3598
|
-
|
|
3599
|
-
options,
|
|
3600
|
-
container: value
|
|
3601
|
-
});
|
|
3606
|
+
if (options.walk.isAncestor(value)) return;
|
|
3607
|
+
options.walk.enter(value);
|
|
3602
3608
|
const result = cleanOwnEntries(value, options);
|
|
3603
|
-
|
|
3604
|
-
options,
|
|
3605
|
-
container: value
|
|
3606
|
-
});
|
|
3609
|
+
options.walk.leave(value);
|
|
3607
3610
|
return result;
|
|
3608
3611
|
}
|
|
3609
3612
|
return value;
|
|
@@ -3622,10 +3625,7 @@ function deepCleanValue(value, options) {
|
|
|
3622
3625
|
function cleanOwnEntries(obj, options) {
|
|
3623
3626
|
const result = {};
|
|
3624
3627
|
for (const [key, val] of Object.entries(obj)) {
|
|
3625
|
-
|
|
3626
|
-
options,
|
|
3627
|
-
chars: key.length
|
|
3628
|
-
});
|
|
3628
|
+
options.walk.chargeChars(key.length);
|
|
3629
3629
|
const cleaned = deepCleanValue(val, options);
|
|
3630
3630
|
if (isPrototypePollutionKey(key)) continue;
|
|
3631
3631
|
if (cleaned !== void 0) result[key] = cleaned;
|
|
@@ -3636,12 +3636,15 @@ function deepCleanObject(obj, options) {
|
|
|
3636
3636
|
if (!obj || typeof obj !== "object") return {};
|
|
3637
3637
|
return cleanOwnEntries(obj, {
|
|
3638
3638
|
...options,
|
|
3639
|
-
|
|
3640
|
-
|
|
3641
|
-
|
|
3642
|
-
|
|
3643
|
-
|
|
3644
|
-
|
|
3639
|
+
walk: createBoundedWalk({
|
|
3640
|
+
subject: "Frontmatter",
|
|
3641
|
+
limits: {
|
|
3642
|
+
maxValues: MAX_FRONTMATTER_VALUES,
|
|
3643
|
+
maxStringChars: MAX_FRONTMATTER_STRING_CHARS,
|
|
3644
|
+
maxDepth: 64
|
|
3645
|
+
},
|
|
3646
|
+
root: obj
|
|
3647
|
+
})
|
|
3645
3648
|
});
|
|
3646
3649
|
}
|
|
3647
3650
|
/** Drop null and undefined values, recursively. */
|
|
@@ -6696,6 +6699,8 @@ const QwencodePermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6696
6699
|
*
|
|
6697
6700
|
* @example
|
|
6698
6701
|
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
6702
|
+
* @example
|
|
6703
|
+
* { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
|
|
6699
6704
|
*/
|
|
6700
6705
|
const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
6701
6706
|
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
@@ -6703,7 +6708,8 @@ const ReasonixPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
6703
6708
|
agent: zod_mini.z.optional(zod_mini.z.looseObject({})),
|
|
6704
6709
|
rawAllow: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6705
6710
|
rawAsk: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6706
|
-
rawDeny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string()))
|
|
6711
|
+
rawDeny: zod_mini.z.optional(zod_mini.z.array(zod_mini.z.string())),
|
|
6712
|
+
allowDynamicBash: zod_mini.z.optional(zod_mini.z.boolean())
|
|
6707
6713
|
});
|
|
6708
6714
|
/**
|
|
6709
6715
|
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
@@ -7347,6 +7353,32 @@ const ZedPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
|
7347
7353
|
})))
|
|
7348
7354
|
});
|
|
7349
7355
|
/**
|
|
7356
|
+
* Tool-scoped override block for Devin Local. `sandbox` is the sibling
|
|
7357
|
+
* top-level `config.json` block that governs the sandbox Devin runs commands
|
|
7358
|
+
* in: `allowed_domains` / `denied_domains` (proxy domain patterns, deny beating
|
|
7359
|
+
* allow), `network_mode` (`full`, the upstream default, allows every HTTP
|
|
7360
|
+
* method; `limited` only GET/HEAD/OPTIONS) and `excluded` (`allow` / `ask` /
|
|
7361
|
+
* `deny` lists of `Exec(...)` matchers deciding which commands run *outside* the
|
|
7362
|
+
* sandbox — `deny` pins them inside it). It constrains how
|
|
7363
|
+
* a permitted command runs rather than which commands are permitted, so it has
|
|
7364
|
+
* no canonical category and is authored here.
|
|
7365
|
+
*
|
|
7366
|
+
* Upstream lists `sandbox` as a **User Config Only** key, so it is emitted at
|
|
7367
|
+
* global scope only; at project scope it is dropped with a warning rather than
|
|
7368
|
+
* written into a file Devin would ignore.
|
|
7369
|
+
*
|
|
7370
|
+
* @example
|
|
7371
|
+
* { "sandbox": { "allowed_domains": ["github.com"], "network_mode": "limited" } }
|
|
7372
|
+
* @example
|
|
7373
|
+
* { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
|
|
7374
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
7375
|
+
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
7376
|
+
*/
|
|
7377
|
+
const DevinPermissionsOverrideSchema = zod_mini.z.looseObject({
|
|
7378
|
+
permission: zod_mini.z.optional(ToolScopedPermissionSchema),
|
|
7379
|
+
sandbox: zod_mini.z.optional(zod_mini.z.looseObject({}))
|
|
7380
|
+
});
|
|
7381
|
+
/**
|
|
7350
7382
|
* Permissions configuration.
|
|
7351
7383
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
7352
7384
|
* Values are pattern-to-action mappings for that tool category.
|
|
@@ -7396,10 +7428,10 @@ const PermissionsConfigSchema = zod_mini.z.looseObject({
|
|
|
7396
7428
|
kiro: zod_mini.z.optional(KiroPermissionsOverrideSchema),
|
|
7397
7429
|
codexcli: zod_mini.z.optional(CodexcliPermissionsOverrideSchema),
|
|
7398
7430
|
zed: zod_mini.z.optional(ZedPermissionsOverrideSchema),
|
|
7431
|
+
devin: zod_mini.z.optional(DevinPermissionsOverrideSchema),
|
|
7399
7432
|
"antigravity-ide": zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7400
7433
|
copilot: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7401
7434
|
copilotcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7402
|
-
devin: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7403
7435
|
goose: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7404
7436
|
grokcli: zod_mini.z.optional(CanonicalPermissionsOverrideSchema),
|
|
7405
7437
|
"kimi-code": zod_mini.z.optional(KimiCodePermissionsOverrideSchema),
|
|
@@ -11483,6 +11515,21 @@ function stripStrings(_key, value) {
|
|
|
11483
11515
|
//#endregion
|
|
11484
11516
|
//#region src/features/shared/shared-config-gateway.ts
|
|
11485
11517
|
/**
|
|
11518
|
+
* Upper bound on the number of values a shared config document may expand
|
|
11519
|
+
* to once every YAML alias is written out. Real config files hold a few
|
|
11520
|
+
* hundred values at most; even a large MCP server catalog stays orders of
|
|
11521
|
+
* magnitude below the limit.
|
|
11522
|
+
*/
|
|
11523
|
+
const MAX_SHARED_CONFIG_VALUES = 1e5;
|
|
11524
|
+
/**
|
|
11525
|
+
* Upper bound on the total character count of the string leaves and keys a
|
|
11526
|
+
* shared config document may expand to. The value budget bounds how many
|
|
11527
|
+
* values are visited, but one long string aliased thousands of times fits
|
|
11528
|
+
* that budget while the duplicated output balloons; charging every visited
|
|
11529
|
+
* string's length separately bounds the output regardless of alias count.
|
|
11530
|
+
*/
|
|
11531
|
+
const MAX_SHARED_CONFIG_STRING_CHARS = 4e6;
|
|
11532
|
+
/**
|
|
11486
11533
|
* Rebuild a parsed document without its prototype-pollution keys.
|
|
11487
11534
|
*
|
|
11488
11535
|
* Every object is rebuilt, not just the ones that are already plain: a literal
|
|
@@ -11497,15 +11544,61 @@ function stripStrings(_key, value) {
|
|
|
11497
11544
|
*
|
|
11498
11545
|
* Dates are the one object the YAML and TOML parsers produce that is not a
|
|
11499
11546
|
* mapping, so they are passed through rather than flattened into `{}`.
|
|
11547
|
+
*
|
|
11548
|
+
* The rebuild is bounded, because a YAML alias makes one parsed container
|
|
11549
|
+
* reachable from many keys and every alias is copied out independently (the
|
|
11550
|
+
* writers dump with `noRefs: true`, so memoizing here would only move the
|
|
11551
|
+
* blowup into serialization). A small "alias bomb" of nested anchors would
|
|
11552
|
+
* otherwise cost exponential time and memory, and a self-referencing anchor
|
|
11553
|
+
* would recurse until the stack overflowed — both reachable from a config
|
|
11554
|
+
* file committed to a cloned repository. The walk therefore charges every
|
|
11555
|
+
* value against {@link MAX_SHARED_CONFIG_VALUES}, every string and key
|
|
11556
|
+
* against {@link MAX_SHARED_CONFIG_STRING_CHARS}, caps nesting at
|
|
11557
|
+
* {@link MAX_SHARED_CONFIG_DEPTH}, and refuses a reference back to an
|
|
11558
|
+
* ancestor outright, each with a clear error instead of a hang or a crash.
|
|
11500
11559
|
*/
|
|
11501
11560
|
function sanitizeSharedConfigValue(value) {
|
|
11502
|
-
|
|
11561
|
+
return sanitizeSharedConfigValueBounded(value, createBoundedWalk({
|
|
11562
|
+
subject: "Shared config",
|
|
11563
|
+
limits: {
|
|
11564
|
+
maxValues: MAX_SHARED_CONFIG_VALUES,
|
|
11565
|
+
maxStringChars: MAX_SHARED_CONFIG_STRING_CHARS,
|
|
11566
|
+
maxDepth: 64
|
|
11567
|
+
}
|
|
11568
|
+
}));
|
|
11569
|
+
}
|
|
11570
|
+
/**
|
|
11571
|
+
* Refuse a container that is already on the descent path. Unlike the
|
|
11572
|
+
* frontmatter cleaner, which drops such a cycle and keeps the rest of the
|
|
11573
|
+
* document, a shared config file is refused outright: silently dropping part
|
|
11574
|
+
* of a user's settings file would let a later write-back persist the loss.
|
|
11575
|
+
*/
|
|
11576
|
+
function enterSharedConfigContainer(walk, container) {
|
|
11577
|
+
if (walk.isAncestor(container)) throw new Error("Shared config contains a value that refers back to itself (a circular YAML alias); refusing to process it");
|
|
11578
|
+
walk.enter(container);
|
|
11579
|
+
}
|
|
11580
|
+
function sanitizeSharedConfigValueBounded(value, walk) {
|
|
11581
|
+
if (typeof value === "string") {
|
|
11582
|
+
walk.chargeValue(value.length);
|
|
11583
|
+
return value;
|
|
11584
|
+
}
|
|
11585
|
+
walk.chargeValue();
|
|
11586
|
+
if (Array.isArray(value)) {
|
|
11587
|
+
enterSharedConfigContainer(walk, value);
|
|
11588
|
+
const items = value.map((item) => sanitizeSharedConfigValueBounded(item, walk));
|
|
11589
|
+
walk.leave(value);
|
|
11590
|
+
return items;
|
|
11591
|
+
}
|
|
11503
11592
|
if (value === null || typeof value !== "object" || value instanceof Date) return value;
|
|
11593
|
+
enterSharedConfigContainer(walk, value);
|
|
11504
11594
|
const result = {};
|
|
11505
11595
|
for (const [key, nested] of Object.entries(value)) {
|
|
11596
|
+
walk.chargeChars(key.length);
|
|
11597
|
+
const sanitized = sanitizeSharedConfigValueBounded(nested, walk);
|
|
11506
11598
|
if (isPrototypePollutionKey(key)) continue;
|
|
11507
|
-
result[key] =
|
|
11599
|
+
result[key] = sanitized;
|
|
11508
11600
|
}
|
|
11601
|
+
walk.leave(value);
|
|
11509
11602
|
return result;
|
|
11510
11603
|
}
|
|
11511
11604
|
/**
|
|
@@ -11534,7 +11627,12 @@ function parseSharedConfig({ format, fileContent, filePath, invalidRootPolicy =
|
|
|
11534
11627
|
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11535
11628
|
}
|
|
11536
11629
|
if (parsed === void 0 || parsed === null) return {};
|
|
11537
|
-
|
|
11630
|
+
let sanitized;
|
|
11631
|
+
try {
|
|
11632
|
+
sanitized = sanitizeSharedConfigValue(parsed);
|
|
11633
|
+
} catch (error) {
|
|
11634
|
+
throw new Error(`Failed to parse shared config${at}: ${formatError(error)}`, { cause: error });
|
|
11635
|
+
}
|
|
11538
11636
|
if (!isPlainObject$1(sanitized)) {
|
|
11539
11637
|
if (invalidRootPolicy === "error") throw new Error(`Failed to parse shared config${at}: expected a mapping at the root`);
|
|
11540
11638
|
return {};
|
|
@@ -12559,7 +12657,7 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12559
12657
|
},
|
|
12560
12658
|
permissions: {
|
|
12561
12659
|
kind: "replace-owned-keys",
|
|
12562
|
-
ownedKeys: ["permissions"]
|
|
12660
|
+
ownedKeys: ["permissions", "sandbox"]
|
|
12563
12661
|
}
|
|
12564
12662
|
}
|
|
12565
12663
|
},
|
|
@@ -36280,6 +36378,180 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
36280
36378
|
return { permission };
|
|
36281
36379
|
}
|
|
36282
36380
|
//#endregion
|
|
36381
|
+
//#region src/features/permissions/sandbox-trust.ts
|
|
36382
|
+
/** A key whose quiet value is an explicit `false`. */
|
|
36383
|
+
const isNotFalse = (value) => value !== false;
|
|
36384
|
+
/** A key whose quiet value is an explicit `true`. */
|
|
36385
|
+
const isNotTrue = (value) => value !== true;
|
|
36386
|
+
/** A list-valued key whose quiet value is the empty list. */
|
|
36387
|
+
const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
|
|
36388
|
+
/** The map-valued counterpart of {@link isNonEmptyList}. */
|
|
36389
|
+
const isNonEmptyMap = (value) => !isRecord$1(value) || Object.keys(value).length > 0;
|
|
36390
|
+
/**
|
|
36391
|
+
* What {@link readSandboxPath} returns when a container on the way to the leaf
|
|
36392
|
+
* is present but is not an object, so the leaf cannot be read at all. It is not
|
|
36393
|
+
* `undefined`, because the two mean opposite things to a caller: `undefined` is
|
|
36394
|
+
* "this path is not being written", while this is "something is being written
|
|
36395
|
+
* here and its shape hides what". The same fail-safe rule the predicates follow
|
|
36396
|
+
* applies to the walk — silence must mean "this cannot loosen anything", not
|
|
36397
|
+
* "this is not the shape the table expected".
|
|
36398
|
+
*/
|
|
36399
|
+
const UNREADABLE_SANDBOX_PATH = Symbol("unreadable-sandbox-path");
|
|
36400
|
+
/**
|
|
36401
|
+
* Reads `sandbox` at `path`. Returns `undefined` when a segment is absent, and
|
|
36402
|
+
* {@link UNREADABLE_SANDBOX_PATH} when one is present but is not an object.
|
|
36403
|
+
* Shared by everything that addresses a `sandbox` path so a nested path added to
|
|
36404
|
+
* one of the tables is actually traversed rather than silently skipped, and so a
|
|
36405
|
+
* hostile shape (an array, a string, `null`) is reported rather than throwing.
|
|
36406
|
+
*/
|
|
36407
|
+
function readSandboxPath({ sandbox, path }) {
|
|
36408
|
+
let cursor = sandbox;
|
|
36409
|
+
for (const segment of path) {
|
|
36410
|
+
if (cursor === void 0) return void 0;
|
|
36411
|
+
if (!isRecord$1(cursor)) return UNREADABLE_SANDBOX_PATH;
|
|
36412
|
+
cursor = cursor[segment];
|
|
36413
|
+
}
|
|
36414
|
+
return cursor;
|
|
36415
|
+
}
|
|
36416
|
+
/**
|
|
36417
|
+
* Every path in `paths` whose value in `sandbox` loosens the policy. Nothing is
|
|
36418
|
+
* removed — the values are written, just not silently. Call it on the block this
|
|
36419
|
+
* generate authored, after any scope filter has run: a value the file already
|
|
36420
|
+
* held is the user's own, not something rulesync opened, and a path a filter
|
|
36421
|
+
* dropped is not being written at all.
|
|
36422
|
+
*/
|
|
36423
|
+
function collectTrustAffectingSandboxPaths({ sandbox, paths }) {
|
|
36424
|
+
const entries = [];
|
|
36425
|
+
const reportedContainers = /* @__PURE__ */ new Set();
|
|
36426
|
+
for (const { path, reason, widens } of paths) {
|
|
36427
|
+
const value = readSandboxPath({
|
|
36428
|
+
sandbox,
|
|
36429
|
+
path
|
|
36430
|
+
});
|
|
36431
|
+
if (value === void 0) continue;
|
|
36432
|
+
if (value === UNREADABLE_SANDBOX_PATH) {
|
|
36433
|
+
const label = findUnreadableContainer({
|
|
36434
|
+
sandbox,
|
|
36435
|
+
path
|
|
36436
|
+
});
|
|
36437
|
+
if (label === void 0 || reportedContainers.has(label)) continue;
|
|
36438
|
+
reportedContainers.add(label);
|
|
36439
|
+
entries.push({
|
|
36440
|
+
label,
|
|
36441
|
+
reason: UNREADABLE_CONTAINER_REASON
|
|
36442
|
+
});
|
|
36443
|
+
continue;
|
|
36444
|
+
}
|
|
36445
|
+
if (!widens(value)) continue;
|
|
36446
|
+
entries.push({
|
|
36447
|
+
label: `sandbox.${path.join(".")}`,
|
|
36448
|
+
reason
|
|
36449
|
+
});
|
|
36450
|
+
}
|
|
36451
|
+
return entries;
|
|
36452
|
+
}
|
|
36453
|
+
/** The reason printed for a container that hides the settings underneath it. */
|
|
36454
|
+
const UNREADABLE_CONTAINER_REASON = "is not the object it has to be, so nothing under it can be checked for what it opens";
|
|
36455
|
+
/**
|
|
36456
|
+
* The prefix of `path` that {@link readSandboxPath} could not walk past, as a
|
|
36457
|
+
* label. `undefined` when the walk was not blocked at all. Callers that report
|
|
36458
|
+
* an unreadable path name the container rather than the leaf, because the leaf
|
|
36459
|
+
* is not what the file actually holds.
|
|
36460
|
+
*/
|
|
36461
|
+
function findUnreadableContainer({ sandbox, path }) {
|
|
36462
|
+
let cursor = sandbox;
|
|
36463
|
+
const walked = [];
|
|
36464
|
+
for (const segment of path) {
|
|
36465
|
+
if (cursor === void 0) return void 0;
|
|
36466
|
+
if (!isRecord$1(cursor)) return walked.length === 0 ? "sandbox" : `sandbox.${walked.join(".")}`;
|
|
36467
|
+
walked.push(segment);
|
|
36468
|
+
cursor = cursor[segment];
|
|
36469
|
+
}
|
|
36470
|
+
}
|
|
36471
|
+
/**
|
|
36472
|
+
* The reason printed for a value the file held in a shape that cannot be read,
|
|
36473
|
+
* which this generate is about to replace. `shape` names what the tool documents
|
|
36474
|
+
* there, so the message says which expectation the file's value missed.
|
|
36475
|
+
*/
|
|
36476
|
+
const replacedUnreadableReason = ({ shape, toolLabel }) => `replaces a value already in the file that is not the ${shape} ${toolLabel} documents, so what it restricted cannot be read`;
|
|
36477
|
+
/**
|
|
36478
|
+
* The restrictions this generate would weaken, compared between the `sandbox`
|
|
36479
|
+
* already in the file and the one about to replace it. A `before` that is
|
|
36480
|
+
* present but not a list is reported outright: a shape the tool may still honor
|
|
36481
|
+
* is not something to go quiet about just because it cannot be diffed. Shared by
|
|
36482
|
+
* every tool whose override replaces a restricting list whole rather than
|
|
36483
|
+
* merging into it — Claude Code needs no equivalent, because it merges its lists
|
|
36484
|
+
* across settings scopes, so a file can only ever add to them.
|
|
36485
|
+
*/
|
|
36486
|
+
function collectRestrictionLosingSandboxEntries({ existing, merged, paths, toolLabel }) {
|
|
36487
|
+
const entries = [];
|
|
36488
|
+
const reportedContainers = /* @__PURE__ */ new Set();
|
|
36489
|
+
for (const { path, reason, loosens } of paths) {
|
|
36490
|
+
const before = readSandboxPath({
|
|
36491
|
+
sandbox: existing,
|
|
36492
|
+
path
|
|
36493
|
+
});
|
|
36494
|
+
if (before === void 0) continue;
|
|
36495
|
+
const [rootKey] = path;
|
|
36496
|
+
if (rootKey !== void 0 && existing[rootKey] === merged[rootKey]) continue;
|
|
36497
|
+
const after = readSandboxPath({
|
|
36498
|
+
sandbox: merged,
|
|
36499
|
+
path
|
|
36500
|
+
});
|
|
36501
|
+
const label = `sandbox.${path.join(".")}`;
|
|
36502
|
+
if (before === UNREADABLE_SANDBOX_PATH) {
|
|
36503
|
+
const container = findUnreadableContainer({
|
|
36504
|
+
sandbox: existing,
|
|
36505
|
+
path
|
|
36506
|
+
});
|
|
36507
|
+
if (container === void 0 || reportedContainers.has(container)) continue;
|
|
36508
|
+
reportedContainers.add(container);
|
|
36509
|
+
entries.push({
|
|
36510
|
+
label: container,
|
|
36511
|
+
reason: replacedUnreadableReason({
|
|
36512
|
+
shape: "object",
|
|
36513
|
+
toolLabel
|
|
36514
|
+
})
|
|
36515
|
+
});
|
|
36516
|
+
continue;
|
|
36517
|
+
}
|
|
36518
|
+
if (!Array.isArray(before)) {
|
|
36519
|
+
entries.push({
|
|
36520
|
+
label,
|
|
36521
|
+
reason: replacedUnreadableReason({
|
|
36522
|
+
shape: "list",
|
|
36523
|
+
toolLabel
|
|
36524
|
+
})
|
|
36525
|
+
});
|
|
36526
|
+
continue;
|
|
36527
|
+
}
|
|
36528
|
+
if (before.length === 0) continue;
|
|
36529
|
+
if (!loosens({
|
|
36530
|
+
before,
|
|
36531
|
+
after: Array.isArray(after) ? after : []
|
|
36532
|
+
})) continue;
|
|
36533
|
+
entries.push({
|
|
36534
|
+
label,
|
|
36535
|
+
reason
|
|
36536
|
+
});
|
|
36537
|
+
}
|
|
36538
|
+
return entries;
|
|
36539
|
+
}
|
|
36540
|
+
/**
|
|
36541
|
+
* The one warning that names every trust-affecting setting this generate wrote
|
|
36542
|
+
* to `relativeFilePath`. Emitted once per file: the individual reasons are what
|
|
36543
|
+
* matter, but the "review this as you would a hook" framing only needs saying
|
|
36544
|
+
* once, and repeating it per key buries the reasons in boilerplate. `noun` lets
|
|
36545
|
+
* a tool whose entries are not all additions call them something more accurate
|
|
36546
|
+
* than "setting".
|
|
36547
|
+
*/
|
|
36548
|
+
function warnOnTrustAffectingEntries({ toolLabel, noun = "setting", entries, relativeFilePath, logger }) {
|
|
36549
|
+
if (entries.length === 0) return;
|
|
36550
|
+
const one = entries.length === 1;
|
|
36551
|
+
const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
|
|
36552
|
+
logger?.warn(`${toolLabel} permissions: writing ${entries.length} trust-affecting ${noun}${one ? "" : "s"} to ${relativeFilePath}; review ${one ? "it" : "them"} as you would a hook, especially if this permissions file came from 'rulesync fetch'. ${details}.`);
|
|
36553
|
+
}
|
|
36554
|
+
//#endregion
|
|
36283
36555
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
36284
36556
|
/**
|
|
36285
36557
|
* Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
|
|
@@ -36328,19 +36600,6 @@ function parseClaudePermissionEntry(entry) {
|
|
|
36328
36600
|
};
|
|
36329
36601
|
}
|
|
36330
36602
|
/**
|
|
36331
|
-
* Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
|
|
36332
|
-
* rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
|
|
36333
|
-
* but never matched by those checks, so Claude Code warns at startup for each
|
|
36334
|
-
* allow, deny, or ask rule in one of these unmatched forms" — so a canonical
|
|
36335
|
-
* `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
|
|
36336
|
-
* docs prescribe instead. A tool-name rule with no path is unaffected: it
|
|
36337
|
-
* matches the tool everywhere and produces no warning.
|
|
36338
|
-
* @see https://code.claude.com/docs/en/permissions
|
|
36339
|
-
*/
|
|
36340
|
-
function isPlainRecord(value) {
|
|
36341
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36342
|
-
}
|
|
36343
|
-
/**
|
|
36344
36603
|
* Merge `patch` into `base`, recursing into plain objects so a sibling key at
|
|
36345
36604
|
* any depth survives. Arrays and scalars are replaced, since a list the author
|
|
36346
36605
|
* states is the list they mean.
|
|
@@ -36350,7 +36609,7 @@ function deepMergeRecords(base, patch) {
|
|
|
36350
36609
|
for (const [key, value] of Object.entries(patch)) {
|
|
36351
36610
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36352
36611
|
const existing = merged[key];
|
|
36353
|
-
merged[key] =
|
|
36612
|
+
merged[key] = isRecord$1(existing) && isRecord$1(value) ? deepMergeRecords(existing, value) : value;
|
|
36354
36613
|
}
|
|
36355
36614
|
return merged;
|
|
36356
36615
|
}
|
|
@@ -36415,13 +36674,11 @@ const CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS = [["filesystem", "allowManagedReadP
|
|
|
36415
36674
|
* traversed rather than silently skipped.
|
|
36416
36675
|
*/
|
|
36417
36676
|
function resolveSandboxParent({ root, segments }) {
|
|
36418
|
-
|
|
36419
|
-
|
|
36420
|
-
|
|
36421
|
-
|
|
36422
|
-
|
|
36423
|
-
}
|
|
36424
|
-
return parent;
|
|
36677
|
+
const resolved = readSandboxPath({
|
|
36678
|
+
sandbox: root,
|
|
36679
|
+
path: segments
|
|
36680
|
+
});
|
|
36681
|
+
return isRecord$1(resolved) ? resolved : void 0;
|
|
36425
36682
|
}
|
|
36426
36683
|
/**
|
|
36427
36684
|
* Deletes `path` from `target` in place and reports whether anything was there,
|
|
@@ -36455,18 +36712,6 @@ function deleteSandboxPath({ target, path }) {
|
|
|
36455
36712
|
return true;
|
|
36456
36713
|
}
|
|
36457
36714
|
/**
|
|
36458
|
-
* The one warning that names every trust-affecting setting this generate wrote
|
|
36459
|
-
* to `relativeFilePath`. Emitted once per file: the individual reasons are what
|
|
36460
|
-
* matter, but the "review this as you would a hook" framing only needs saying
|
|
36461
|
-
* once, and repeating it per key buries the reasons in boilerplate.
|
|
36462
|
-
*/
|
|
36463
|
-
function warnOnTrustAffectingEntries({ entries, relativeFilePath, logger }) {
|
|
36464
|
-
if (entries.length === 0) return;
|
|
36465
|
-
const one = entries.length === 1;
|
|
36466
|
-
const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
|
|
36467
|
-
logger?.warn(`Claude Code permissions: writing ${entries.length} trust-affecting ${one ? "setting" : "settings"} to ${relativeFilePath}; review ${one ? "it" : "them"} as you would a hook, especially if this permissions file came from 'rulesync fetch'. ${details}.`);
|
|
36468
|
-
}
|
|
36469
|
-
/**
|
|
36470
36715
|
* The `permissions.defaultMode` values that start a session with fewer prompts
|
|
36471
36716
|
* than the default. `plan` and `default` are absent because they do not widen
|
|
36472
36717
|
* anything.
|
|
@@ -36511,18 +36756,6 @@ const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
|
|
|
36511
36756
|
["socatPath"]
|
|
36512
36757
|
];
|
|
36513
36758
|
/**
|
|
36514
|
-
* The predicates the "which value actually widens?" tables are built from.
|
|
36515
|
-
* Each names the value that does *not* widen and reports everything else, never
|
|
36516
|
-
* the reverse: the override is authored JSONC, so a key can carry any value at
|
|
36517
|
-
* all, and one Claude Code coerces is still honored. Reporting an off-type value
|
|
36518
|
-
* keeps the warning fail-safe — silence has to mean "this cannot loosen
|
|
36519
|
-
* anything", not "this is not the type the table expected".
|
|
36520
|
-
*/
|
|
36521
|
-
const isNotFalse = (value) => value !== false;
|
|
36522
|
-
const isNotTrue = (value) => value !== true;
|
|
36523
|
-
const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
|
|
36524
|
-
const isNonEmptyMap = (value) => !isPlainRecord(value) || Object.keys(value).length > 0;
|
|
36525
|
-
/**
|
|
36526
36759
|
* `sandbox` paths that loosen the sandbox rather than naming something to run:
|
|
36527
36760
|
* they let commands out of it, weaken the isolation it provides, or redirect
|
|
36528
36761
|
* where its traffic goes. They are written like `env` is — the ordinary uses are
|
|
@@ -36625,30 +36858,6 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
36625
36858
|
widens: () => true
|
|
36626
36859
|
}
|
|
36627
36860
|
];
|
|
36628
|
-
/**
|
|
36629
|
-
* Every authored `sandbox` path that loosens the sandbox. Nothing is removed —
|
|
36630
|
-
* the values are written, just not silently. Called on the filtered `sandbox`
|
|
36631
|
-
* so it never claims to be writing a path the scope filters dropped.
|
|
36632
|
-
*/
|
|
36633
|
-
function collectTrustAffectingSandboxPaths({ sandbox }) {
|
|
36634
|
-
const entries = [];
|
|
36635
|
-
for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
|
|
36636
|
-
const leaf = path.at(-1);
|
|
36637
|
-
if (leaf === void 0) continue;
|
|
36638
|
-
const parent = resolveSandboxParent({
|
|
36639
|
-
root: sandbox,
|
|
36640
|
-
segments: path.slice(0, -1)
|
|
36641
|
-
});
|
|
36642
|
-
if (parent === void 0) continue;
|
|
36643
|
-
const value = parent[leaf];
|
|
36644
|
-
if (value === void 0 || !widens(value)) continue;
|
|
36645
|
-
entries.push({
|
|
36646
|
-
label: `sandbox.${path.join(".")}`,
|
|
36647
|
-
reason
|
|
36648
|
-
});
|
|
36649
|
-
}
|
|
36650
|
-
return entries;
|
|
36651
|
-
}
|
|
36652
36861
|
/** Paths that name an executable Claude Code runs. Refused in both scopes. */
|
|
36653
36862
|
const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL = {
|
|
36654
36863
|
paths: CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS,
|
|
@@ -36719,13 +36928,13 @@ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
|
|
|
36719
36928
|
*/
|
|
36720
36929
|
function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
|
|
36721
36930
|
const credentials = sandbox.credentials;
|
|
36722
|
-
if (!
|
|
36931
|
+
if (!isRecord$1(credentials)) return sandbox;
|
|
36723
36932
|
const filteredCredentials = { ...credentials };
|
|
36724
36933
|
let changed = false;
|
|
36725
36934
|
for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
|
|
36726
36935
|
const list = filteredCredentials[listKey];
|
|
36727
36936
|
if (!Array.isArray(list)) continue;
|
|
36728
|
-
const kept = list.filter((entry) => !(
|
|
36937
|
+
const kept = list.filter((entry) => !(isRecord$1(entry) && entry.mode === "mask"));
|
|
36729
36938
|
if (kept.length === list.length) continue;
|
|
36730
36939
|
changed = true;
|
|
36731
36940
|
const dropped = list.length - kept.length;
|
|
@@ -37004,6 +37213,16 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
|
|
|
37004
37213
|
trustAffecting
|
|
37005
37214
|
};
|
|
37006
37215
|
}
|
|
37216
|
+
/**
|
|
37217
|
+
* Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
|
|
37218
|
+
* rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
|
|
37219
|
+
* but never matched by those checks, so Claude Code warns at startup for each
|
|
37220
|
+
* allow, deny, or ask rule in one of these unmatched forms" — so a canonical
|
|
37221
|
+
* `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
|
|
37222
|
+
* docs prescribe instead. A tool-name rule with no path is unaffected: it
|
|
37223
|
+
* matches the tool everywhere and produces no warning.
|
|
37224
|
+
* @see https://code.claude.com/docs/en/permissions
|
|
37225
|
+
*/
|
|
37007
37226
|
const CLAUDE_PATH_RULE_ALIASES = {
|
|
37008
37227
|
Write: "Edit",
|
|
37009
37228
|
NotebookEdit: "Edit",
|
|
@@ -37084,7 +37303,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37084
37303
|
};
|
|
37085
37304
|
}
|
|
37086
37305
|
const overrideSandbox = config.claudecode?.sandbox;
|
|
37087
|
-
if (
|
|
37306
|
+
if (isRecord$1(overrideSandbox)) {
|
|
37088
37307
|
const honorableSandbox = stripSandboxPaths({
|
|
37089
37308
|
sandbox: overrideSandbox,
|
|
37090
37309
|
refusals: [
|
|
@@ -37100,8 +37319,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37100
37319
|
relativeFilePath: paths.relativeFilePath,
|
|
37101
37320
|
logger
|
|
37102
37321
|
});
|
|
37103
|
-
trustAffecting.push(...collectTrustAffectingSandboxPaths({
|
|
37104
|
-
|
|
37322
|
+
trustAffecting.push(...collectTrustAffectingSandboxPaths({
|
|
37323
|
+
sandbox: scopedSandbox,
|
|
37324
|
+
paths: CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS
|
|
37325
|
+
}));
|
|
37326
|
+
if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isRecord$1(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
|
|
37105
37327
|
}
|
|
37106
37328
|
const overrideTopLevel = {};
|
|
37107
37329
|
for (const [key, value] of Object.entries(config.claudecode ?? {})) {
|
|
@@ -37119,8 +37341,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37119
37341
|
trustAffecting.push(...trustAffectingTopLevel);
|
|
37120
37342
|
if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
|
|
37121
37343
|
warnOnTrustAffectingEntries({
|
|
37344
|
+
toolLabel: "Claude Code",
|
|
37122
37345
|
entries: trustAffecting,
|
|
37123
|
-
relativeFilePath: paths.relativeFilePath,
|
|
37346
|
+
relativeFilePath: toPosixPath((0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)),
|
|
37124
37347
|
logger
|
|
37125
37348
|
});
|
|
37126
37349
|
const managedToolNames = managedClaudeToolNames(config);
|
|
@@ -37163,7 +37386,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37163
37386
|
const nonListFields = Object.fromEntries(Object.entries(permissionsRest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
37164
37387
|
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
37165
37388
|
const { sandbox } = settings;
|
|
37166
|
-
if (
|
|
37389
|
+
if (isRecord$1(sandbox)) {
|
|
37167
37390
|
const importedSandbox = structuredClone(sandbox);
|
|
37168
37391
|
for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) deleteSandboxPath({
|
|
37169
37392
|
target: importedSandbox,
|
|
@@ -39416,6 +39639,80 @@ function buildDevinPermissionEntry(scope, pattern) {
|
|
|
39416
39639
|
if (pattern === "*") return scope;
|
|
39417
39640
|
return `${scope}(${pattern})`;
|
|
39418
39641
|
}
|
|
39642
|
+
function asDevinRecord(value) {
|
|
39643
|
+
return isRecord$1(value) ? { ...value } : {};
|
|
39644
|
+
}
|
|
39645
|
+
/**
|
|
39646
|
+
* `sandbox` paths whose authored value loosens the sandbox on its own: they let
|
|
39647
|
+
* a command out of it, or widen what a command left inside it may reach. They
|
|
39648
|
+
* are written — the ordinary uses are far too common to refuse — but never
|
|
39649
|
+
* silently, because a permissions file is shareable (`rulesync fetch` copies one
|
|
39650
|
+
* into a project) and should not be able to open the sandbox without saying so.
|
|
39651
|
+
* This is the same stance `CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS` takes for
|
|
39652
|
+
* the equivalent Claude Code keys, and `widens` follows the same convention of
|
|
39653
|
+
* naming the restrictive value rather than the permissive ones, so a spelling
|
|
39654
|
+
* Devin does not recognize is reported rather than waved through.
|
|
39655
|
+
*
|
|
39656
|
+
* The three keys that restrict — `allowed_domains` (an allowlist only while it
|
|
39657
|
+
* has entries), `denied_domains` and `excluded.deny` — are not here: they loosen
|
|
39658
|
+
* by losing entries, which `DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS` covers.
|
|
39659
|
+
*
|
|
39660
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
39661
|
+
*/
|
|
39662
|
+
const DEVIN_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
39663
|
+
{
|
|
39664
|
+
path: ["network_mode"],
|
|
39665
|
+
reason: "anything but 'limited' lets sandboxed requests use every HTTP method, not just GET/HEAD/OPTIONS",
|
|
39666
|
+
widens: (value) => value !== "limited"
|
|
39667
|
+
},
|
|
39668
|
+
{
|
|
39669
|
+
path: ["excluded", "allow"],
|
|
39670
|
+
reason: "names commands that run outside the sandbox with no prompt and no sandbox policy",
|
|
39671
|
+
widens: isNonEmptyList
|
|
39672
|
+
},
|
|
39673
|
+
{
|
|
39674
|
+
path: ["excluded", "ask"],
|
|
39675
|
+
reason: "names commands that run outside the sandbox once confirmed, with no sandbox policy",
|
|
39676
|
+
widens: isNonEmptyList
|
|
39677
|
+
}
|
|
39678
|
+
];
|
|
39679
|
+
/** How Devin is named in the warnings this file emits. */
|
|
39680
|
+
const DEVIN_TOOL_LABEL = "Devin";
|
|
39681
|
+
/**
|
|
39682
|
+
* `sandbox` paths that restrict, and that therefore loosen the policy by losing
|
|
39683
|
+
* entries rather than by holding a value. Devin's config is one file rather than
|
|
39684
|
+
* a stack of settings scopes, and the override is shallow-merged over the
|
|
39685
|
+
* existing `sandbox` at its top level: each of these lists is replaced whole,
|
|
39686
|
+
* and `excluded.deny` vanishes as soon as the override states any other
|
|
39687
|
+
* `excluded` key. Losing an entry has the same effect as adding one to the
|
|
39688
|
+
* permissive keys above, so it is announced the same way. Claude Code needs no
|
|
39689
|
+
* equivalent — it merges its lists across settings scopes, so a file can only
|
|
39690
|
+
* ever add to them.
|
|
39691
|
+
*
|
|
39692
|
+
* `loosens` is asked only about a `before` that actually restricted something,
|
|
39693
|
+
* and the two directions are not symmetric: `allowed_domains` restricts by
|
|
39694
|
+
* listing what is reachable, so it loosens by gaining entries or by emptying
|
|
39695
|
+
* out altogether, while the deny lists loosen by losing entries.
|
|
39696
|
+
*
|
|
39697
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
39698
|
+
*/
|
|
39699
|
+
const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
39700
|
+
{
|
|
39701
|
+
path: ["allowed_domains"],
|
|
39702
|
+
reason: "adds to the proxy allowlist already in the file, or empties it so every domain becomes reachable again",
|
|
39703
|
+
loosens: ({ before, after }) => after.length === 0 || after.some((entry) => !before.includes(entry))
|
|
39704
|
+
},
|
|
39705
|
+
{
|
|
39706
|
+
path: ["denied_domains"],
|
|
39707
|
+
reason: "drops domains the deny list already in the file kept out of reach",
|
|
39708
|
+
loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
|
|
39709
|
+
},
|
|
39710
|
+
{
|
|
39711
|
+
path: ["excluded", "deny"],
|
|
39712
|
+
reason: "drops commands the list already in the file pinned inside the sandbox",
|
|
39713
|
+
loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
|
|
39714
|
+
}
|
|
39715
|
+
];
|
|
39419
39716
|
/**
|
|
39420
39717
|
* Permissions generator for Devin Local (native `.devin/` configuration).
|
|
39421
39718
|
*
|
|
@@ -39431,10 +39728,18 @@ function buildDevinPermissionEntry(scope, pattern) {
|
|
|
39431
39728
|
*
|
|
39432
39729
|
* In global mode the config file is shared with the hooks (`hooks`) feature
|
|
39433
39730
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
39434
|
-
* merge into the existing JSON and the file is never deleted; only the
|
|
39435
|
-
* `permissions`
|
|
39731
|
+
* merge into the existing JSON and the file is never deleted; only the keys
|
|
39732
|
+
* this feature manages are rewritten — `permissions`, plus `sandbox` in global
|
|
39733
|
+
* mode when the `devin` override authors it.
|
|
39734
|
+
*
|
|
39735
|
+
* The sibling `sandbox` block — which decides what a permitted command may
|
|
39736
|
+
* reach rather than which commands are permitted — has no canonical category
|
|
39737
|
+
* and is authored through the `devin` override in `.rulesync/permissions.jsonc`.
|
|
39738
|
+
* Devin documents it as a user-config-only key, so it is written at global
|
|
39739
|
+
* scope only.
|
|
39436
39740
|
*
|
|
39437
39741
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
39742
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
39438
39743
|
*/
|
|
39439
39744
|
var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
39440
39745
|
constructor(params) {
|
|
@@ -39445,7 +39750,8 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39445
39750
|
}
|
|
39446
39751
|
/**
|
|
39447
39752
|
* config.json may carry the MCP/hooks features' keys, so it is never deleted;
|
|
39448
|
-
* only the
|
|
39753
|
+
* only the keys this feature manages are rewritten — `permissions`, plus
|
|
39754
|
+
* `sandbox` in global mode when the `devin` override authors it.
|
|
39449
39755
|
*/
|
|
39450
39756
|
isDeletable() {
|
|
39451
39757
|
return false;
|
|
@@ -39471,7 +39777,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39471
39777
|
validate
|
|
39472
39778
|
});
|
|
39473
39779
|
}
|
|
39474
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
|
|
39780
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true, logger }) {
|
|
39475
39781
|
const paths = DevinPermissions.getSettablePaths({ global });
|
|
39476
39782
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
39477
39783
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
@@ -39497,6 +39803,46 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39497
39803
|
else delete mergedPermissions.ask;
|
|
39498
39804
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
39499
39805
|
else delete mergedPermissions.deny;
|
|
39806
|
+
const patch = { permissions: mergedPermissions };
|
|
39807
|
+
const authoredSandbox = config.devin?.sandbox;
|
|
39808
|
+
if (authoredSandbox !== void 0) {
|
|
39809
|
+
const authoredSandboxRecord = asDevinRecord(authoredSandbox);
|
|
39810
|
+
if (global) {
|
|
39811
|
+
const existingSandbox = asDevinRecord(settings.sandbox);
|
|
39812
|
+
const mergedSandbox = {
|
|
39813
|
+
...existingSandbox,
|
|
39814
|
+
...authoredSandboxRecord
|
|
39815
|
+
};
|
|
39816
|
+
const writesSandbox = Object.keys(mergedSandbox).length > 0;
|
|
39817
|
+
if (writesSandbox) patch.sandbox = mergedSandbox;
|
|
39818
|
+
const replacesUnreadableSandbox = writesSandbox && settings.sandbox !== void 0 && !isRecord$1(settings.sandbox);
|
|
39819
|
+
warnOnTrustAffectingEntries({
|
|
39820
|
+
toolLabel: DEVIN_TOOL_LABEL,
|
|
39821
|
+
noun: "sandbox change",
|
|
39822
|
+
entries: [
|
|
39823
|
+
...replacesUnreadableSandbox ? [{
|
|
39824
|
+
label: "sandbox",
|
|
39825
|
+
reason: replacedUnreadableReason({
|
|
39826
|
+
shape: "object",
|
|
39827
|
+
toolLabel: DEVIN_TOOL_LABEL
|
|
39828
|
+
})
|
|
39829
|
+
}] : [],
|
|
39830
|
+
...collectTrustAffectingSandboxPaths({
|
|
39831
|
+
sandbox: authoredSandboxRecord,
|
|
39832
|
+
paths: DEVIN_TRUST_AFFECTING_SANDBOX_PATHS
|
|
39833
|
+
}),
|
|
39834
|
+
...collectRestrictionLosingSandboxEntries({
|
|
39835
|
+
existing: existingSandbox,
|
|
39836
|
+
merged: mergedSandbox,
|
|
39837
|
+
paths: DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS,
|
|
39838
|
+
toolLabel: DEVIN_TOOL_LABEL
|
|
39839
|
+
})
|
|
39840
|
+
],
|
|
39841
|
+
relativeFilePath: toPosixPath((0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath)),
|
|
39842
|
+
logger
|
|
39843
|
+
});
|
|
39844
|
+
} else if (Object.keys(authoredSandboxRecord).length > 0) logger?.warn("Devin reads 'sandbox' from the user config only, so the 'devin.sandbox' override was dropped from the project config. Generate with --global to author it.");
|
|
39845
|
+
}
|
|
39500
39846
|
return new DevinPermissions({
|
|
39501
39847
|
outputRoot,
|
|
39502
39848
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -39505,7 +39851,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39505
39851
|
fileKey: sharedConfigFileKey(paths),
|
|
39506
39852
|
feature: "permissions",
|
|
39507
39853
|
existingContent,
|
|
39508
|
-
patch
|
|
39854
|
+
patch,
|
|
39509
39855
|
filePath
|
|
39510
39856
|
}),
|
|
39511
39857
|
validate
|
|
@@ -39525,7 +39871,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39525
39871
|
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
39526
39872
|
deny: Array.isArray(permissions.deny) ? permissions.deny : []
|
|
39527
39873
|
});
|
|
39528
|
-
|
|
39874
|
+
const sandbox = asDevinRecord(settings.sandbox);
|
|
39875
|
+
const result = { ...config };
|
|
39876
|
+
if (Object.keys(sandbox).length > 0) result.devin = { sandbox };
|
|
39877
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
39529
39878
|
}
|
|
39530
39879
|
validate() {
|
|
39531
39880
|
return {
|
|
@@ -42473,6 +42822,137 @@ function buildReasonixPermissionEntry(toolName, pattern) {
|
|
|
42473
42822
|
if (pattern === "*") return toolName;
|
|
42474
42823
|
return `${toolName}(${pattern})`;
|
|
42475
42824
|
}
|
|
42825
|
+
/** How Reasonix is named in the warnings this file emits. */
|
|
42826
|
+
const REASONIX_TOOL_LABEL = "Reasonix";
|
|
42827
|
+
/** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
|
|
42828
|
+
const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
|
|
42829
|
+
/**
|
|
42830
|
+
* `[sandbox]` keys whose authored value loosens the enforcement layer beneath
|
|
42831
|
+
* the permission policy: they take Bash out of its OS jail, open that jail to
|
|
42832
|
+
* the network, or widen where the file-writing built-ins may write. Written —
|
|
42833
|
+
* the ordinary uses are far too common to refuse — but never silently, the same
|
|
42834
|
+
* stance `DEVIN_TRUST_AFFECTING_SANDBOX_PATHS` takes, and `widens` likewise
|
|
42835
|
+
* names the restrictive value so a spelling Reasonix does not recognize is
|
|
42836
|
+
* reported rather than waved through.
|
|
42837
|
+
*
|
|
42838
|
+
* `forbid_read` is not here: it restricts, so it loosens by losing entries
|
|
42839
|
+
* rather than by holding one, which needs the before/after comparison
|
|
42840
|
+
* {@link REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS} below does instead.
|
|
42841
|
+
*
|
|
42842
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
42843
|
+
*/
|
|
42844
|
+
const REASONIX_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
42845
|
+
{
|
|
42846
|
+
path: ["bash"],
|
|
42847
|
+
reason: "anything but 'enforce' takes Bash out of the OS sandbox, so a command may write and read wherever the user can",
|
|
42848
|
+
widens: (value) => value !== "enforce"
|
|
42849
|
+
},
|
|
42850
|
+
{
|
|
42851
|
+
path: ["network"],
|
|
42852
|
+
reason: "lets sandboxed Bash reach the network",
|
|
42853
|
+
widens: isNotFalse
|
|
42854
|
+
},
|
|
42855
|
+
{
|
|
42856
|
+
path: ["allow_write"],
|
|
42857
|
+
reason: "adds directories the file-writing tools may modify outside the workspace root, which a headless run would otherwise refuse",
|
|
42858
|
+
widens: isNonEmptyList
|
|
42859
|
+
},
|
|
42860
|
+
{
|
|
42861
|
+
path: ["workspace_root"],
|
|
42862
|
+
reason: "moves the root the file-writing tools and sandboxed Bash are confined to, so what they may reach is decided by this path rather than by the project directory",
|
|
42863
|
+
widens: escapesTheProject
|
|
42864
|
+
}
|
|
42865
|
+
];
|
|
42866
|
+
/**
|
|
42867
|
+
* Whether a `workspace_root` points somewhere other than inside the project the
|
|
42868
|
+
* generate runs in. The key moves the write confinement rather than adding to
|
|
42869
|
+
* it, so the ordinary value — the project directory, spelled relatively — would
|
|
42870
|
+
* otherwise be announced on every generate; anything else is the case worth
|
|
42871
|
+
* naming, since it is how a fetched permissions file would put `~/.ssh` or
|
|
42872
|
+
* `C:\\Users\\<user>` inside the jail: an absolute path in either flavour, one
|
|
42873
|
+
* carrying a drive letter, a home-relative one, a shell or environment
|
|
42874
|
+
* expansion, and any path holding a `..` segment — even one that would land back
|
|
42875
|
+
* inside, since resolving it here would only be a guess at what Reasonix does.
|
|
42876
|
+
* Both path flavours are asked because the file is authored on one machine and
|
|
42877
|
+
* generated on another, so a Windows-shaped root reaching a POSIX check must not
|
|
42878
|
+
* read as relative. Anything that is not a string is reported, per the fail-safe
|
|
42879
|
+
* rule the predicates in `sandbox-trust.ts` follow.
|
|
42880
|
+
*/
|
|
42881
|
+
function escapesTheProject(value) {
|
|
42882
|
+
if (typeof value !== "string") return true;
|
|
42883
|
+
const trimmed = value.trim();
|
|
42884
|
+
if (trimmed === "") return false;
|
|
42885
|
+
if (trimmed.startsWith("~")) return true;
|
|
42886
|
+
if (node_path.posix.isAbsolute(trimmed) || node_path.win32.isAbsolute(trimmed)) return true;
|
|
42887
|
+
if (/^[A-Za-z]:/.test(trimmed)) return true;
|
|
42888
|
+
if (trimmed.includes("$") || /%[^%]+%/.test(trimmed)) return true;
|
|
42889
|
+
return trimmed.split(/[\\/]/).includes("..");
|
|
42890
|
+
}
|
|
42891
|
+
/**
|
|
42892
|
+
* The `[sandbox]` key that restricts, and so loosens by losing entries rather
|
|
42893
|
+
* than by holding a value. The override is shallow-merged over the existing
|
|
42894
|
+
* `[sandbox]` at its top level, so an authored `forbid_read` replaces the list
|
|
42895
|
+
* the file had whole — emptying it, or dropping the `${HOME}/.ssh` entry
|
|
42896
|
+
* Reasonix's own example recommends, opens exactly what the list kept closed.
|
|
42897
|
+
*
|
|
42898
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
42899
|
+
*/
|
|
42900
|
+
const REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS = [{
|
|
42901
|
+
path: ["forbid_read"],
|
|
42902
|
+
reason: "drops paths the list already in the file kept out of read, list and search",
|
|
42903
|
+
loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
|
|
42904
|
+
}];
|
|
42905
|
+
/**
|
|
42906
|
+
* Everything worth naming about the `[sandbox]` table this generate is about to
|
|
42907
|
+
* write: the authored values that loosen enforcement, the restrictions the
|
|
42908
|
+
* shallow merge would drop, and a `[sandbox]` the file holds in some shape other
|
|
42909
|
+
* than a table, which the write replaces wholesale. The widening check reads the
|
|
42910
|
+
* authored block alone — a loosening value the file already held is the user's
|
|
42911
|
+
* own, and re-announcing it on every generate would bury the values rulesync
|
|
42912
|
+
* actually wrote — while a loss can only be seen from both sides.
|
|
42913
|
+
*/
|
|
42914
|
+
function collectSandboxOverlayEntries({ existing, authored, merged }) {
|
|
42915
|
+
return [
|
|
42916
|
+
...existing !== void 0 && !isRecord$1(existing) ? [{
|
|
42917
|
+
label: "sandbox",
|
|
42918
|
+
reason: replacedUnreadableReason({
|
|
42919
|
+
shape: "object",
|
|
42920
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
42921
|
+
})
|
|
42922
|
+
}] : [],
|
|
42923
|
+
...collectTrustAffectingSandboxPaths({
|
|
42924
|
+
sandbox: asReasonixRecord(authored),
|
|
42925
|
+
paths: REASONIX_TRUST_AFFECTING_SANDBOX_PATHS
|
|
42926
|
+
}),
|
|
42927
|
+
...collectRestrictionLosingSandboxEntries({
|
|
42928
|
+
existing: asReasonixRecord(existing),
|
|
42929
|
+
merged,
|
|
42930
|
+
paths: REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS,
|
|
42931
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
42932
|
+
})
|
|
42933
|
+
];
|
|
42934
|
+
}
|
|
42935
|
+
/**
|
|
42936
|
+
* Writes the override's `allow_dynamic_bash` into `[permissions]`, where it sits
|
|
42937
|
+
* beside allow/ask/deny rather than in a table of its own, and reports it when
|
|
42938
|
+
* it is being turned on: it widens what a shareable permissions file lets run
|
|
42939
|
+
* with no human in the loop. Only an authored value is written — leaving the key
|
|
42940
|
+
* out of the override keeps whatever the file already had — and turning it off
|
|
42941
|
+
* narrows, so that stays quiet. Only a literal `false` is quiet, not everything
|
|
42942
|
+
* falsy: `getJson()` casts rather than parses, so a `--no-validate` run can put
|
|
42943
|
+
* a value the schema forbids here, and a value Reasonix might still coerce is
|
|
42944
|
+
* not something to write in silence. The entries are returned rather than logged so
|
|
42945
|
+
* one generate still produces one warning naming everything it wrote.
|
|
42946
|
+
*/
|
|
42947
|
+
function applyAllowDynamicBash({ permissions, authored }) {
|
|
42948
|
+
if (authored === void 0) return [];
|
|
42949
|
+
permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY] = authored;
|
|
42950
|
+
if (!isNotFalse(authored)) return [];
|
|
42951
|
+
return [{
|
|
42952
|
+
label: `permissions.${REASONIX_ALLOW_DYNAMIC_BASH_KEY}`,
|
|
42953
|
+
reason: "lets an Allow fallback, Auto included, run the nested and indirect Bash that otherwise needs a human or an exact-literal grant — command and process substitution, a dynamic command name, 'eval', 'source', 'sh -c' and their kind"
|
|
42954
|
+
}];
|
|
42955
|
+
}
|
|
42476
42956
|
function parseReasonixConfig(fileContent) {
|
|
42477
42957
|
const parsed = smol_toml.parse(fileContent || smol_toml.stringify({}));
|
|
42478
42958
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -42529,6 +43009,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42529
43009
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
42530
43010
|
const paths = this.getSettablePaths({ global });
|
|
42531
43011
|
const filePath = (0, node_path.join)(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
43012
|
+
const relativeFilePathForLog = toPosixPath((0, node_path.join)(paths.relativeDirPath, paths.relativeFilePath));
|
|
42532
43013
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
42533
43014
|
const parsed = parseReasonixConfig(existingContent);
|
|
42534
43015
|
const config = rulesyncPermissions.getJson();
|
|
@@ -42573,11 +43054,23 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42573
43054
|
...deny,
|
|
42574
43055
|
...rawDeny
|
|
42575
43056
|
]);
|
|
43057
|
+
const trustAffecting = applyAllowDynamicBash({
|
|
43058
|
+
permissions: mergedPermissions,
|
|
43059
|
+
authored: override?.allowDynamicBash
|
|
43060
|
+
});
|
|
42576
43061
|
const patch = { permissions: mergedPermissions };
|
|
42577
|
-
if (override?.sandbox !== void 0)
|
|
42578
|
-
|
|
42579
|
-
|
|
42580
|
-
|
|
43062
|
+
if (override?.sandbox !== void 0) {
|
|
43063
|
+
const mergedSandbox = {
|
|
43064
|
+
...asReasonixRecord(parsed.sandbox),
|
|
43065
|
+
...asReasonixRecord(override.sandbox)
|
|
43066
|
+
};
|
|
43067
|
+
patch.sandbox = mergedSandbox;
|
|
43068
|
+
trustAffecting.push(...collectSandboxOverlayEntries({
|
|
43069
|
+
existing: parsed.sandbox,
|
|
43070
|
+
authored: override.sandbox,
|
|
43071
|
+
merged: mergedSandbox
|
|
43072
|
+
}));
|
|
43073
|
+
}
|
|
42581
43074
|
if (override?.agent !== void 0) {
|
|
42582
43075
|
const mergedAgent = {
|
|
42583
43076
|
...asReasonixRecord(parsed.agent),
|
|
@@ -42585,9 +43078,16 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42585
43078
|
};
|
|
42586
43079
|
const retired = REASONIX_RETIRED_AGENT_KEYS.filter((key) => mergedAgent[key] !== void 0);
|
|
42587
43080
|
for (const key of retired) delete mergedAgent[key];
|
|
42588
|
-
if (retired.length > 0) logger?.warn(`Reasonix permissions: removing ${retired.map((key) => `"${key}"`).join(", ")} from [agent] in ${
|
|
43081
|
+
if (retired.length > 0) logger?.warn(`Reasonix permissions: removing ${retired.map((key) => `"${key}"`).join(", ")} from [agent] in ${relativeFilePathForLog}; Reasonix took the key off its config surface in v1.17.18, so what it used to express now belongs in the shared \`permission\` block.`);
|
|
42589
43082
|
patch.agent = mergedAgent;
|
|
42590
43083
|
}
|
|
43084
|
+
warnOnTrustAffectingEntries({
|
|
43085
|
+
toolLabel: REASONIX_TOOL_LABEL,
|
|
43086
|
+
noun: "change",
|
|
43087
|
+
entries: trustAffecting,
|
|
43088
|
+
relativeFilePath: relativeFilePathForLog,
|
|
43089
|
+
logger
|
|
43090
|
+
});
|
|
42591
43091
|
return new ReasonixPermissions({
|
|
42592
43092
|
outputRoot,
|
|
42593
43093
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -42622,6 +43122,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42622
43122
|
const sandbox = asReasonixRecord(this.toml.sandbox);
|
|
42623
43123
|
const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
|
|
42624
43124
|
const reasonixOverride = {};
|
|
43125
|
+
const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
|
|
43126
|
+
if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
|
|
42625
43127
|
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
42626
43128
|
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
42627
43129
|
if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
|