rulesync 16.22.1 → 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-B7o8So93.cjs → import-D420jbVB.cjs} +523 -90
- package/dist/{import-DUE1P1zV.js → import-DIDEUv63.js} +524 -91
- package/dist/{import-DUE1P1zV.js.map → import-DIDEUv63.js.map} +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
|
@@ -6674,6 +6674,8 @@ const QwencodePermissionsOverrideSchema = z.looseObject({
|
|
|
6674
6674
|
*
|
|
6675
6675
|
* @example
|
|
6676
6676
|
* { "sandbox": { "bash": "enforce", "network": false }, "agent": { "plan_mode_read_only_commands": ["gh pr diff"] } }
|
|
6677
|
+
* @example
|
|
6678
|
+
* { "allowDynamicBash": true, "rawAllow": ["Bash=pnpm test"] }
|
|
6677
6679
|
*/
|
|
6678
6680
|
const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
6679
6681
|
permission: z.optional(ToolScopedPermissionSchema),
|
|
@@ -6681,7 +6683,8 @@ const ReasonixPermissionsOverrideSchema = z.looseObject({
|
|
|
6681
6683
|
agent: z.optional(z.looseObject({})),
|
|
6682
6684
|
rawAllow: z.optional(z.array(z.string())),
|
|
6683
6685
|
rawAsk: z.optional(z.array(z.string())),
|
|
6684
|
-
rawDeny: z.optional(z.array(z.string()))
|
|
6686
|
+
rawDeny: z.optional(z.array(z.string())),
|
|
6687
|
+
allowDynamicBash: z.optional(z.boolean())
|
|
6685
6688
|
});
|
|
6686
6689
|
/**
|
|
6687
6690
|
* Tool-scoped override block for Factory Droid. Factory Droid's `settings.json`
|
|
@@ -7325,6 +7328,32 @@ const ZedPermissionsOverrideSchema = z.looseObject({
|
|
|
7325
7328
|
})))
|
|
7326
7329
|
});
|
|
7327
7330
|
/**
|
|
7331
|
+
* Tool-scoped override block for Devin Local. `sandbox` is the sibling
|
|
7332
|
+
* top-level `config.json` block that governs the sandbox Devin runs commands
|
|
7333
|
+
* in: `allowed_domains` / `denied_domains` (proxy domain patterns, deny beating
|
|
7334
|
+
* allow), `network_mode` (`full`, the upstream default, allows every HTTP
|
|
7335
|
+
* method; `limited` only GET/HEAD/OPTIONS) and `excluded` (`allow` / `ask` /
|
|
7336
|
+
* `deny` lists of `Exec(...)` matchers deciding which commands run *outside* the
|
|
7337
|
+
* sandbox — `deny` pins them inside it). It constrains how
|
|
7338
|
+
* a permitted command runs rather than which commands are permitted, so it has
|
|
7339
|
+
* no canonical category and is authored here.
|
|
7340
|
+
*
|
|
7341
|
+
* Upstream lists `sandbox` as a **User Config Only** key, so it is emitted at
|
|
7342
|
+
* global scope only; at project scope it is dropped with a warning rather than
|
|
7343
|
+
* written into a file Devin would ignore.
|
|
7344
|
+
*
|
|
7345
|
+
* @example
|
|
7346
|
+
* { "sandbox": { "allowed_domains": ["github.com"], "network_mode": "limited" } }
|
|
7347
|
+
* @example
|
|
7348
|
+
* { "sandbox": { "excluded": { "allow": ["Exec(git status *)"], "deny": ["Exec(git tag *)"] } } }
|
|
7349
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
7350
|
+
* @see https://docs.devin.ai/cli/reference/configuration/config-file
|
|
7351
|
+
*/
|
|
7352
|
+
const DevinPermissionsOverrideSchema = z.looseObject({
|
|
7353
|
+
permission: z.optional(ToolScopedPermissionSchema),
|
|
7354
|
+
sandbox: z.optional(z.looseObject({}))
|
|
7355
|
+
});
|
|
7356
|
+
/**
|
|
7328
7357
|
* Permissions configuration.
|
|
7329
7358
|
* Keys are tool category names (e.g., "bash", "edit", "read", "webfetch").
|
|
7330
7359
|
* Values are pattern-to-action mappings for that tool category.
|
|
@@ -7374,10 +7403,10 @@ const PermissionsConfigSchema = z.looseObject({
|
|
|
7374
7403
|
kiro: z.optional(KiroPermissionsOverrideSchema),
|
|
7375
7404
|
codexcli: z.optional(CodexcliPermissionsOverrideSchema),
|
|
7376
7405
|
zed: z.optional(ZedPermissionsOverrideSchema),
|
|
7406
|
+
devin: z.optional(DevinPermissionsOverrideSchema),
|
|
7377
7407
|
"antigravity-ide": z.optional(CanonicalPermissionsOverrideSchema),
|
|
7378
7408
|
copilot: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7379
7409
|
copilotcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7380
|
-
devin: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7381
7410
|
goose: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7382
7411
|
grokcli: z.optional(CanonicalPermissionsOverrideSchema),
|
|
7383
7412
|
"kimi-code": z.optional(KimiCodePermissionsOverrideSchema),
|
|
@@ -12603,7 +12632,7 @@ const SHARED_CONFIG_OWNERSHIP = {
|
|
|
12603
12632
|
},
|
|
12604
12633
|
permissions: {
|
|
12605
12634
|
kind: "replace-owned-keys",
|
|
12606
|
-
ownedKeys: ["permissions"]
|
|
12635
|
+
ownedKeys: ["permissions", "sandbox"]
|
|
12607
12636
|
}
|
|
12608
12637
|
}
|
|
12609
12638
|
},
|
|
@@ -36324,6 +36353,180 @@ function convertAugmentToRulesyncPermissions({ entries, logger }) {
|
|
|
36324
36353
|
return { permission };
|
|
36325
36354
|
}
|
|
36326
36355
|
//#endregion
|
|
36356
|
+
//#region src/features/permissions/sandbox-trust.ts
|
|
36357
|
+
/** A key whose quiet value is an explicit `false`. */
|
|
36358
|
+
const isNotFalse = (value) => value !== false;
|
|
36359
|
+
/** A key whose quiet value is an explicit `true`. */
|
|
36360
|
+
const isNotTrue = (value) => value !== true;
|
|
36361
|
+
/** A list-valued key whose quiet value is the empty list. */
|
|
36362
|
+
const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
|
|
36363
|
+
/** The map-valued counterpart of {@link isNonEmptyList}. */
|
|
36364
|
+
const isNonEmptyMap = (value) => !isRecord$1(value) || Object.keys(value).length > 0;
|
|
36365
|
+
/**
|
|
36366
|
+
* What {@link readSandboxPath} returns when a container on the way to the leaf
|
|
36367
|
+
* is present but is not an object, so the leaf cannot be read at all. It is not
|
|
36368
|
+
* `undefined`, because the two mean opposite things to a caller: `undefined` is
|
|
36369
|
+
* "this path is not being written", while this is "something is being written
|
|
36370
|
+
* here and its shape hides what". The same fail-safe rule the predicates follow
|
|
36371
|
+
* applies to the walk — silence must mean "this cannot loosen anything", not
|
|
36372
|
+
* "this is not the shape the table expected".
|
|
36373
|
+
*/
|
|
36374
|
+
const UNREADABLE_SANDBOX_PATH = Symbol("unreadable-sandbox-path");
|
|
36375
|
+
/**
|
|
36376
|
+
* Reads `sandbox` at `path`. Returns `undefined` when a segment is absent, and
|
|
36377
|
+
* {@link UNREADABLE_SANDBOX_PATH} when one is present but is not an object.
|
|
36378
|
+
* Shared by everything that addresses a `sandbox` path so a nested path added to
|
|
36379
|
+
* one of the tables is actually traversed rather than silently skipped, and so a
|
|
36380
|
+
* hostile shape (an array, a string, `null`) is reported rather than throwing.
|
|
36381
|
+
*/
|
|
36382
|
+
function readSandboxPath({ sandbox, path }) {
|
|
36383
|
+
let cursor = sandbox;
|
|
36384
|
+
for (const segment of path) {
|
|
36385
|
+
if (cursor === void 0) return void 0;
|
|
36386
|
+
if (!isRecord$1(cursor)) return UNREADABLE_SANDBOX_PATH;
|
|
36387
|
+
cursor = cursor[segment];
|
|
36388
|
+
}
|
|
36389
|
+
return cursor;
|
|
36390
|
+
}
|
|
36391
|
+
/**
|
|
36392
|
+
* Every path in `paths` whose value in `sandbox` loosens the policy. Nothing is
|
|
36393
|
+
* removed — the values are written, just not silently. Call it on the block this
|
|
36394
|
+
* generate authored, after any scope filter has run: a value the file already
|
|
36395
|
+
* held is the user's own, not something rulesync opened, and a path a filter
|
|
36396
|
+
* dropped is not being written at all.
|
|
36397
|
+
*/
|
|
36398
|
+
function collectTrustAffectingSandboxPaths({ sandbox, paths }) {
|
|
36399
|
+
const entries = [];
|
|
36400
|
+
const reportedContainers = /* @__PURE__ */ new Set();
|
|
36401
|
+
for (const { path, reason, widens } of paths) {
|
|
36402
|
+
const value = readSandboxPath({
|
|
36403
|
+
sandbox,
|
|
36404
|
+
path
|
|
36405
|
+
});
|
|
36406
|
+
if (value === void 0) continue;
|
|
36407
|
+
if (value === UNREADABLE_SANDBOX_PATH) {
|
|
36408
|
+
const label = findUnreadableContainer({
|
|
36409
|
+
sandbox,
|
|
36410
|
+
path
|
|
36411
|
+
});
|
|
36412
|
+
if (label === void 0 || reportedContainers.has(label)) continue;
|
|
36413
|
+
reportedContainers.add(label);
|
|
36414
|
+
entries.push({
|
|
36415
|
+
label,
|
|
36416
|
+
reason: UNREADABLE_CONTAINER_REASON
|
|
36417
|
+
});
|
|
36418
|
+
continue;
|
|
36419
|
+
}
|
|
36420
|
+
if (!widens(value)) continue;
|
|
36421
|
+
entries.push({
|
|
36422
|
+
label: `sandbox.${path.join(".")}`,
|
|
36423
|
+
reason
|
|
36424
|
+
});
|
|
36425
|
+
}
|
|
36426
|
+
return entries;
|
|
36427
|
+
}
|
|
36428
|
+
/** The reason printed for a container that hides the settings underneath it. */
|
|
36429
|
+
const UNREADABLE_CONTAINER_REASON = "is not the object it has to be, so nothing under it can be checked for what it opens";
|
|
36430
|
+
/**
|
|
36431
|
+
* The prefix of `path` that {@link readSandboxPath} could not walk past, as a
|
|
36432
|
+
* label. `undefined` when the walk was not blocked at all. Callers that report
|
|
36433
|
+
* an unreadable path name the container rather than the leaf, because the leaf
|
|
36434
|
+
* is not what the file actually holds.
|
|
36435
|
+
*/
|
|
36436
|
+
function findUnreadableContainer({ sandbox, path }) {
|
|
36437
|
+
let cursor = sandbox;
|
|
36438
|
+
const walked = [];
|
|
36439
|
+
for (const segment of path) {
|
|
36440
|
+
if (cursor === void 0) return void 0;
|
|
36441
|
+
if (!isRecord$1(cursor)) return walked.length === 0 ? "sandbox" : `sandbox.${walked.join(".")}`;
|
|
36442
|
+
walked.push(segment);
|
|
36443
|
+
cursor = cursor[segment];
|
|
36444
|
+
}
|
|
36445
|
+
}
|
|
36446
|
+
/**
|
|
36447
|
+
* The reason printed for a value the file held in a shape that cannot be read,
|
|
36448
|
+
* which this generate is about to replace. `shape` names what the tool documents
|
|
36449
|
+
* there, so the message says which expectation the file's value missed.
|
|
36450
|
+
*/
|
|
36451
|
+
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`;
|
|
36452
|
+
/**
|
|
36453
|
+
* The restrictions this generate would weaken, compared between the `sandbox`
|
|
36454
|
+
* already in the file and the one about to replace it. A `before` that is
|
|
36455
|
+
* present but not a list is reported outright: a shape the tool may still honor
|
|
36456
|
+
* is not something to go quiet about just because it cannot be diffed. Shared by
|
|
36457
|
+
* every tool whose override replaces a restricting list whole rather than
|
|
36458
|
+
* merging into it — Claude Code needs no equivalent, because it merges its lists
|
|
36459
|
+
* across settings scopes, so a file can only ever add to them.
|
|
36460
|
+
*/
|
|
36461
|
+
function collectRestrictionLosingSandboxEntries({ existing, merged, paths, toolLabel }) {
|
|
36462
|
+
const entries = [];
|
|
36463
|
+
const reportedContainers = /* @__PURE__ */ new Set();
|
|
36464
|
+
for (const { path, reason, loosens } of paths) {
|
|
36465
|
+
const before = readSandboxPath({
|
|
36466
|
+
sandbox: existing,
|
|
36467
|
+
path
|
|
36468
|
+
});
|
|
36469
|
+
if (before === void 0) continue;
|
|
36470
|
+
const [rootKey] = path;
|
|
36471
|
+
if (rootKey !== void 0 && existing[rootKey] === merged[rootKey]) continue;
|
|
36472
|
+
const after = readSandboxPath({
|
|
36473
|
+
sandbox: merged,
|
|
36474
|
+
path
|
|
36475
|
+
});
|
|
36476
|
+
const label = `sandbox.${path.join(".")}`;
|
|
36477
|
+
if (before === UNREADABLE_SANDBOX_PATH) {
|
|
36478
|
+
const container = findUnreadableContainer({
|
|
36479
|
+
sandbox: existing,
|
|
36480
|
+
path
|
|
36481
|
+
});
|
|
36482
|
+
if (container === void 0 || reportedContainers.has(container)) continue;
|
|
36483
|
+
reportedContainers.add(container);
|
|
36484
|
+
entries.push({
|
|
36485
|
+
label: container,
|
|
36486
|
+
reason: replacedUnreadableReason({
|
|
36487
|
+
shape: "object",
|
|
36488
|
+
toolLabel
|
|
36489
|
+
})
|
|
36490
|
+
});
|
|
36491
|
+
continue;
|
|
36492
|
+
}
|
|
36493
|
+
if (!Array.isArray(before)) {
|
|
36494
|
+
entries.push({
|
|
36495
|
+
label,
|
|
36496
|
+
reason: replacedUnreadableReason({
|
|
36497
|
+
shape: "list",
|
|
36498
|
+
toolLabel
|
|
36499
|
+
})
|
|
36500
|
+
});
|
|
36501
|
+
continue;
|
|
36502
|
+
}
|
|
36503
|
+
if (before.length === 0) continue;
|
|
36504
|
+
if (!loosens({
|
|
36505
|
+
before,
|
|
36506
|
+
after: Array.isArray(after) ? after : []
|
|
36507
|
+
})) continue;
|
|
36508
|
+
entries.push({
|
|
36509
|
+
label,
|
|
36510
|
+
reason
|
|
36511
|
+
});
|
|
36512
|
+
}
|
|
36513
|
+
return entries;
|
|
36514
|
+
}
|
|
36515
|
+
/**
|
|
36516
|
+
* The one warning that names every trust-affecting setting this generate wrote
|
|
36517
|
+
* to `relativeFilePath`. Emitted once per file: the individual reasons are what
|
|
36518
|
+
* matter, but the "review this as you would a hook" framing only needs saying
|
|
36519
|
+
* once, and repeating it per key buries the reasons in boilerplate. `noun` lets
|
|
36520
|
+
* a tool whose entries are not all additions call them something more accurate
|
|
36521
|
+
* than "setting".
|
|
36522
|
+
*/
|
|
36523
|
+
function warnOnTrustAffectingEntries({ toolLabel, noun = "setting", entries, relativeFilePath, logger }) {
|
|
36524
|
+
if (entries.length === 0) return;
|
|
36525
|
+
const one = entries.length === 1;
|
|
36526
|
+
const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
|
|
36527
|
+
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}.`);
|
|
36528
|
+
}
|
|
36529
|
+
//#endregion
|
|
36327
36530
|
//#region src/features/permissions/claudecode-permissions.ts
|
|
36328
36531
|
/**
|
|
36329
36532
|
* Mapping from rulesync canonical tool category names (lowercase) to Claude Code tool names (PascalCase).
|
|
@@ -36372,19 +36575,6 @@ function parseClaudePermissionEntry(entry) {
|
|
|
36372
36575
|
};
|
|
36373
36576
|
}
|
|
36374
36577
|
/**
|
|
36375
|
-
* Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
|
|
36376
|
-
* rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
|
|
36377
|
-
* but never matched by those checks, so Claude Code warns at startup for each
|
|
36378
|
-
* allow, deny, or ask rule in one of these unmatched forms" — so a canonical
|
|
36379
|
-
* `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
|
|
36380
|
-
* docs prescribe instead. A tool-name rule with no path is unaffected: it
|
|
36381
|
-
* matches the tool everywhere and produces no warning.
|
|
36382
|
-
* @see https://code.claude.com/docs/en/permissions
|
|
36383
|
-
*/
|
|
36384
|
-
function isPlainRecord(value) {
|
|
36385
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
36386
|
-
}
|
|
36387
|
-
/**
|
|
36388
36578
|
* Merge `patch` into `base`, recursing into plain objects so a sibling key at
|
|
36389
36579
|
* any depth survives. Arrays and scalars are replaced, since a list the author
|
|
36390
36580
|
* states is the list they mean.
|
|
@@ -36394,7 +36584,7 @@ function deepMergeRecords(base, patch) {
|
|
|
36394
36584
|
for (const [key, value] of Object.entries(patch)) {
|
|
36395
36585
|
if (PROTOTYPE_POLLUTION_KEYS.has(key)) continue;
|
|
36396
36586
|
const existing = merged[key];
|
|
36397
|
-
merged[key] =
|
|
36587
|
+
merged[key] = isRecord$1(existing) && isRecord$1(value) ? deepMergeRecords(existing, value) : value;
|
|
36398
36588
|
}
|
|
36399
36589
|
return merged;
|
|
36400
36590
|
}
|
|
@@ -36459,13 +36649,11 @@ const CLAUDECODE_MANAGED_ONLY_SANDBOX_PATHS = [["filesystem", "allowManagedReadP
|
|
|
36459
36649
|
* traversed rather than silently skipped.
|
|
36460
36650
|
*/
|
|
36461
36651
|
function resolveSandboxParent({ root, segments }) {
|
|
36462
|
-
|
|
36463
|
-
|
|
36464
|
-
|
|
36465
|
-
|
|
36466
|
-
|
|
36467
|
-
}
|
|
36468
|
-
return parent;
|
|
36652
|
+
const resolved = readSandboxPath({
|
|
36653
|
+
sandbox: root,
|
|
36654
|
+
path: segments
|
|
36655
|
+
});
|
|
36656
|
+
return isRecord$1(resolved) ? resolved : void 0;
|
|
36469
36657
|
}
|
|
36470
36658
|
/**
|
|
36471
36659
|
* Deletes `path` from `target` in place and reports whether anything was there,
|
|
@@ -36499,18 +36687,6 @@ function deleteSandboxPath({ target, path }) {
|
|
|
36499
36687
|
return true;
|
|
36500
36688
|
}
|
|
36501
36689
|
/**
|
|
36502
|
-
* The one warning that names every trust-affecting setting this generate wrote
|
|
36503
|
-
* to `relativeFilePath`. Emitted once per file: the individual reasons are what
|
|
36504
|
-
* matter, but the "review this as you would a hook" framing only needs saying
|
|
36505
|
-
* once, and repeating it per key buries the reasons in boilerplate.
|
|
36506
|
-
*/
|
|
36507
|
-
function warnOnTrustAffectingEntries({ entries, relativeFilePath, logger }) {
|
|
36508
|
-
if (entries.length === 0) return;
|
|
36509
|
-
const one = entries.length === 1;
|
|
36510
|
-
const details = entries.map(({ label, reason }) => `'${label}' — ${reason}`).join("; ");
|
|
36511
|
-
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}.`);
|
|
36512
|
-
}
|
|
36513
|
-
/**
|
|
36514
36690
|
* The `permissions.defaultMode` values that start a session with fewer prompts
|
|
36515
36691
|
* than the default. `plan` and `default` are absent because they do not widen
|
|
36516
36692
|
* anything.
|
|
@@ -36555,18 +36731,6 @@ const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS = [
|
|
|
36555
36731
|
["socatPath"]
|
|
36556
36732
|
];
|
|
36557
36733
|
/**
|
|
36558
|
-
* The predicates the "which value actually widens?" tables are built from.
|
|
36559
|
-
* Each names the value that does *not* widen and reports everything else, never
|
|
36560
|
-
* the reverse: the override is authored JSONC, so a key can carry any value at
|
|
36561
|
-
* all, and one Claude Code coerces is still honored. Reporting an off-type value
|
|
36562
|
-
* keeps the warning fail-safe — silence has to mean "this cannot loosen
|
|
36563
|
-
* anything", not "this is not the type the table expected".
|
|
36564
|
-
*/
|
|
36565
|
-
const isNotFalse = (value) => value !== false;
|
|
36566
|
-
const isNotTrue = (value) => value !== true;
|
|
36567
|
-
const isNonEmptyList = (value) => !Array.isArray(value) || value.length > 0;
|
|
36568
|
-
const isNonEmptyMap = (value) => !isPlainRecord(value) || Object.keys(value).length > 0;
|
|
36569
|
-
/**
|
|
36570
36734
|
* `sandbox` paths that loosen the sandbox rather than naming something to run:
|
|
36571
36735
|
* they let commands out of it, weaken the isolation it provides, or redirect
|
|
36572
36736
|
* where its traffic goes. They are written like `env` is — the ordinary uses are
|
|
@@ -36669,30 +36833,6 @@ const CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
|
36669
36833
|
widens: () => true
|
|
36670
36834
|
}
|
|
36671
36835
|
];
|
|
36672
|
-
/**
|
|
36673
|
-
* Every authored `sandbox` path that loosens the sandbox. Nothing is removed —
|
|
36674
|
-
* the values are written, just not silently. Called on the filtered `sandbox`
|
|
36675
|
-
* so it never claims to be writing a path the scope filters dropped.
|
|
36676
|
-
*/
|
|
36677
|
-
function collectTrustAffectingSandboxPaths({ sandbox }) {
|
|
36678
|
-
const entries = [];
|
|
36679
|
-
for (const { path, reason, widens } of CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS) {
|
|
36680
|
-
const leaf = path.at(-1);
|
|
36681
|
-
if (leaf === void 0) continue;
|
|
36682
|
-
const parent = resolveSandboxParent({
|
|
36683
|
-
root: sandbox,
|
|
36684
|
-
segments: path.slice(0, -1)
|
|
36685
|
-
});
|
|
36686
|
-
if (parent === void 0) continue;
|
|
36687
|
-
const value = parent[leaf];
|
|
36688
|
-
if (value === void 0 || !widens(value)) continue;
|
|
36689
|
-
entries.push({
|
|
36690
|
-
label: `sandbox.${path.join(".")}`,
|
|
36691
|
-
reason
|
|
36692
|
-
});
|
|
36693
|
-
}
|
|
36694
|
-
return entries;
|
|
36695
|
-
}
|
|
36696
36836
|
/** Paths that name an executable Claude Code runs. Refused in both scopes. */
|
|
36697
36837
|
const CLAUDECODE_COMMAND_EXECUTING_SANDBOX_REFUSAL = {
|
|
36698
36838
|
paths: CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS,
|
|
@@ -36763,13 +36903,13 @@ const CLAUDECODE_MASKABLE_CREDENTIAL_LISTS = ["envVars", "files"];
|
|
|
36763
36903
|
*/
|
|
36764
36904
|
function stripProjectIgnoredMaskEntries({ sandbox, relativeFilePath, logger }) {
|
|
36765
36905
|
const credentials = sandbox.credentials;
|
|
36766
|
-
if (!
|
|
36906
|
+
if (!isRecord$1(credentials)) return sandbox;
|
|
36767
36907
|
const filteredCredentials = { ...credentials };
|
|
36768
36908
|
let changed = false;
|
|
36769
36909
|
for (const listKey of CLAUDECODE_MASKABLE_CREDENTIAL_LISTS) {
|
|
36770
36910
|
const list = filteredCredentials[listKey];
|
|
36771
36911
|
if (!Array.isArray(list)) continue;
|
|
36772
|
-
const kept = list.filter((entry) => !(
|
|
36912
|
+
const kept = list.filter((entry) => !(isRecord$1(entry) && entry.mode === "mask"));
|
|
36773
36913
|
if (kept.length === list.length) continue;
|
|
36774
36914
|
changed = true;
|
|
36775
36915
|
const dropped = list.length - kept.length;
|
|
@@ -37048,6 +37188,16 @@ function stripUnhonoredTopLevelKeys({ overrides, global, relativeFilePath, logge
|
|
|
37048
37188
|
trustAffecting
|
|
37049
37189
|
};
|
|
37050
37190
|
}
|
|
37191
|
+
/**
|
|
37192
|
+
* Claude Code's file permission checks match only `Edit(path)` and `Read(path)`
|
|
37193
|
+
* rules. A `Write(path)`, `NotebookEdit(path)` or `Glob(path)` rule "is accepted
|
|
37194
|
+
* but never matched by those checks, so Claude Code warns at startup for each
|
|
37195
|
+
* allow, deny, or ask rule in one of these unmatched forms" — so a canonical
|
|
37196
|
+
* `write`/`notebookedit`/`glob` rule with a pattern is emitted in the form the
|
|
37197
|
+
* docs prescribe instead. A tool-name rule with no path is unaffected: it
|
|
37198
|
+
* matches the tool everywhere and produces no warning.
|
|
37199
|
+
* @see https://code.claude.com/docs/en/permissions
|
|
37200
|
+
*/
|
|
37051
37201
|
const CLAUDE_PATH_RULE_ALIASES = {
|
|
37052
37202
|
Write: "Edit",
|
|
37053
37203
|
NotebookEdit: "Edit",
|
|
@@ -37128,7 +37278,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37128
37278
|
};
|
|
37129
37279
|
}
|
|
37130
37280
|
const overrideSandbox = config.claudecode?.sandbox;
|
|
37131
|
-
if (
|
|
37281
|
+
if (isRecord$1(overrideSandbox)) {
|
|
37132
37282
|
const honorableSandbox = stripSandboxPaths({
|
|
37133
37283
|
sandbox: overrideSandbox,
|
|
37134
37284
|
refusals: [
|
|
@@ -37144,8 +37294,11 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37144
37294
|
relativeFilePath: paths.relativeFilePath,
|
|
37145
37295
|
logger
|
|
37146
37296
|
});
|
|
37147
|
-
trustAffecting.push(...collectTrustAffectingSandboxPaths({
|
|
37148
|
-
|
|
37297
|
+
trustAffecting.push(...collectTrustAffectingSandboxPaths({
|
|
37298
|
+
sandbox: scopedSandbox,
|
|
37299
|
+
paths: CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS
|
|
37300
|
+
}));
|
|
37301
|
+
if (Object.keys(scopedSandbox).length > 0) settings.sandbox = deepMergeRecords(isRecord$1(settings.sandbox) ? settings.sandbox : {}, scopedSandbox);
|
|
37149
37302
|
}
|
|
37150
37303
|
const overrideTopLevel = {};
|
|
37151
37304
|
for (const [key, value] of Object.entries(config.claudecode ?? {})) {
|
|
@@ -37163,8 +37316,9 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37163
37316
|
trustAffecting.push(...trustAffectingTopLevel);
|
|
37164
37317
|
if (Object.keys(scopedTopLevel).length > 0) settings = deepMergeRecords(settings, scopedTopLevel);
|
|
37165
37318
|
warnOnTrustAffectingEntries({
|
|
37319
|
+
toolLabel: "Claude Code",
|
|
37166
37320
|
entries: trustAffecting,
|
|
37167
|
-
relativeFilePath: paths.relativeFilePath,
|
|
37321
|
+
relativeFilePath: toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath)),
|
|
37168
37322
|
logger
|
|
37169
37323
|
});
|
|
37170
37324
|
const managedToolNames = managedClaudeToolNames(config);
|
|
@@ -37207,7 +37361,7 @@ var ClaudecodePermissions = class ClaudecodePermissions extends ToolPermissions
|
|
|
37207
37361
|
const nonListFields = Object.fromEntries(Object.entries(permissionsRest).filter(([key]) => !PROTOTYPE_POLLUTION_KEYS.has(key)));
|
|
37208
37362
|
if (Object.keys(nonListFields).length > 0) config.claudecode = { permissions: nonListFields };
|
|
37209
37363
|
const { sandbox } = settings;
|
|
37210
|
-
if (
|
|
37364
|
+
if (isRecord$1(sandbox)) {
|
|
37211
37365
|
const importedSandbox = structuredClone(sandbox);
|
|
37212
37366
|
for (const path of CLAUDECODE_COMMAND_EXECUTING_SANDBOX_PATHS) deleteSandboxPath({
|
|
37213
37367
|
target: importedSandbox,
|
|
@@ -39460,6 +39614,80 @@ function buildDevinPermissionEntry(scope, pattern) {
|
|
|
39460
39614
|
if (pattern === "*") return scope;
|
|
39461
39615
|
return `${scope}(${pattern})`;
|
|
39462
39616
|
}
|
|
39617
|
+
function asDevinRecord(value) {
|
|
39618
|
+
return isRecord$1(value) ? { ...value } : {};
|
|
39619
|
+
}
|
|
39620
|
+
/**
|
|
39621
|
+
* `sandbox` paths whose authored value loosens the sandbox on its own: they let
|
|
39622
|
+
* a command out of it, or widen what a command left inside it may reach. They
|
|
39623
|
+
* are written — the ordinary uses are far too common to refuse — but never
|
|
39624
|
+
* silently, because a permissions file is shareable (`rulesync fetch` copies one
|
|
39625
|
+
* into a project) and should not be able to open the sandbox without saying so.
|
|
39626
|
+
* This is the same stance `CLAUDECODE_TRUST_AFFECTING_SANDBOX_PATHS` takes for
|
|
39627
|
+
* the equivalent Claude Code keys, and `widens` follows the same convention of
|
|
39628
|
+
* naming the restrictive value rather than the permissive ones, so a spelling
|
|
39629
|
+
* Devin does not recognize is reported rather than waved through.
|
|
39630
|
+
*
|
|
39631
|
+
* The three keys that restrict — `allowed_domains` (an allowlist only while it
|
|
39632
|
+
* has entries), `denied_domains` and `excluded.deny` — are not here: they loosen
|
|
39633
|
+
* by losing entries, which `DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS` covers.
|
|
39634
|
+
*
|
|
39635
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
39636
|
+
*/
|
|
39637
|
+
const DEVIN_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
39638
|
+
{
|
|
39639
|
+
path: ["network_mode"],
|
|
39640
|
+
reason: "anything but 'limited' lets sandboxed requests use every HTTP method, not just GET/HEAD/OPTIONS",
|
|
39641
|
+
widens: (value) => value !== "limited"
|
|
39642
|
+
},
|
|
39643
|
+
{
|
|
39644
|
+
path: ["excluded", "allow"],
|
|
39645
|
+
reason: "names commands that run outside the sandbox with no prompt and no sandbox policy",
|
|
39646
|
+
widens: isNonEmptyList
|
|
39647
|
+
},
|
|
39648
|
+
{
|
|
39649
|
+
path: ["excluded", "ask"],
|
|
39650
|
+
reason: "names commands that run outside the sandbox once confirmed, with no sandbox policy",
|
|
39651
|
+
widens: isNonEmptyList
|
|
39652
|
+
}
|
|
39653
|
+
];
|
|
39654
|
+
/** How Devin is named in the warnings this file emits. */
|
|
39655
|
+
const DEVIN_TOOL_LABEL = "Devin";
|
|
39656
|
+
/**
|
|
39657
|
+
* `sandbox` paths that restrict, and that therefore loosen the policy by losing
|
|
39658
|
+
* entries rather than by holding a value. Devin's config is one file rather than
|
|
39659
|
+
* a stack of settings scopes, and the override is shallow-merged over the
|
|
39660
|
+
* existing `sandbox` at its top level: each of these lists is replaced whole,
|
|
39661
|
+
* and `excluded.deny` vanishes as soon as the override states any other
|
|
39662
|
+
* `excluded` key. Losing an entry has the same effect as adding one to the
|
|
39663
|
+
* permissive keys above, so it is announced the same way. Claude Code needs no
|
|
39664
|
+
* equivalent — it merges its lists across settings scopes, so a file can only
|
|
39665
|
+
* ever add to them.
|
|
39666
|
+
*
|
|
39667
|
+
* `loosens` is asked only about a `before` that actually restricted something,
|
|
39668
|
+
* and the two directions are not symmetric: `allowed_domains` restricts by
|
|
39669
|
+
* listing what is reachable, so it loosens by gaining entries or by emptying
|
|
39670
|
+
* out altogether, while the deny lists loosen by losing entries.
|
|
39671
|
+
*
|
|
39672
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
39673
|
+
*/
|
|
39674
|
+
const DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS = [
|
|
39675
|
+
{
|
|
39676
|
+
path: ["allowed_domains"],
|
|
39677
|
+
reason: "adds to the proxy allowlist already in the file, or empties it so every domain becomes reachable again",
|
|
39678
|
+
loosens: ({ before, after }) => after.length === 0 || after.some((entry) => !before.includes(entry))
|
|
39679
|
+
},
|
|
39680
|
+
{
|
|
39681
|
+
path: ["denied_domains"],
|
|
39682
|
+
reason: "drops domains the deny list already in the file kept out of reach",
|
|
39683
|
+
loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
|
|
39684
|
+
},
|
|
39685
|
+
{
|
|
39686
|
+
path: ["excluded", "deny"],
|
|
39687
|
+
reason: "drops commands the list already in the file pinned inside the sandbox",
|
|
39688
|
+
loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
|
|
39689
|
+
}
|
|
39690
|
+
];
|
|
39463
39691
|
/**
|
|
39464
39692
|
* Permissions generator for Devin Local (native `.devin/` configuration).
|
|
39465
39693
|
*
|
|
@@ -39475,10 +39703,18 @@ function buildDevinPermissionEntry(scope, pattern) {
|
|
|
39475
39703
|
*
|
|
39476
39704
|
* In global mode the config file is shared with the hooks (`hooks`) feature
|
|
39477
39705
|
* (MCP moved to the dedicated mcp_config.json in v3000.3), so reads and writes
|
|
39478
|
-
* merge into the existing JSON and the file is never deleted; only the
|
|
39479
|
-
* `permissions`
|
|
39706
|
+
* merge into the existing JSON and the file is never deleted; only the keys
|
|
39707
|
+
* this feature manages are rewritten — `permissions`, plus `sandbox` in global
|
|
39708
|
+
* mode when the `devin` override authors it.
|
|
39709
|
+
*
|
|
39710
|
+
* The sibling `sandbox` block — which decides what a permitted command may
|
|
39711
|
+
* reach rather than which commands are permitted — has no canonical category
|
|
39712
|
+
* and is authored through the `devin` override in `.rulesync/permissions.jsonc`.
|
|
39713
|
+
* Devin documents it as a user-config-only key, so it is written at global
|
|
39714
|
+
* scope only.
|
|
39480
39715
|
*
|
|
39481
39716
|
* @see https://docs.devin.ai/cli/reference/permissions
|
|
39717
|
+
* @see https://docs.devin.ai/cli/sandbox
|
|
39482
39718
|
*/
|
|
39483
39719
|
var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
39484
39720
|
constructor(params) {
|
|
@@ -39489,7 +39725,8 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39489
39725
|
}
|
|
39490
39726
|
/**
|
|
39491
39727
|
* config.json may carry the MCP/hooks features' keys, so it is never deleted;
|
|
39492
|
-
* only the
|
|
39728
|
+
* only the keys this feature manages are rewritten — `permissions`, plus
|
|
39729
|
+
* `sandbox` in global mode when the `devin` override authors it.
|
|
39493
39730
|
*/
|
|
39494
39731
|
isDeletable() {
|
|
39495
39732
|
return false;
|
|
@@ -39515,7 +39752,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39515
39752
|
validate
|
|
39516
39753
|
});
|
|
39517
39754
|
}
|
|
39518
|
-
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true }) {
|
|
39755
|
+
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, global = false, validate = true, logger }) {
|
|
39519
39756
|
const paths = DevinPermissions.getSettablePaths({ global });
|
|
39520
39757
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
39521
39758
|
const existingContent = await readFileContentOrNull(filePath) ?? JSON.stringify({}, null, 2);
|
|
@@ -39541,6 +39778,46 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39541
39778
|
else delete mergedPermissions.ask;
|
|
39542
39779
|
if (mergedDeny.length > 0) mergedPermissions.deny = mergedDeny;
|
|
39543
39780
|
else delete mergedPermissions.deny;
|
|
39781
|
+
const patch = { permissions: mergedPermissions };
|
|
39782
|
+
const authoredSandbox = config.devin?.sandbox;
|
|
39783
|
+
if (authoredSandbox !== void 0) {
|
|
39784
|
+
const authoredSandboxRecord = asDevinRecord(authoredSandbox);
|
|
39785
|
+
if (global) {
|
|
39786
|
+
const existingSandbox = asDevinRecord(settings.sandbox);
|
|
39787
|
+
const mergedSandbox = {
|
|
39788
|
+
...existingSandbox,
|
|
39789
|
+
...authoredSandboxRecord
|
|
39790
|
+
};
|
|
39791
|
+
const writesSandbox = Object.keys(mergedSandbox).length > 0;
|
|
39792
|
+
if (writesSandbox) patch.sandbox = mergedSandbox;
|
|
39793
|
+
const replacesUnreadableSandbox = writesSandbox && settings.sandbox !== void 0 && !isRecord$1(settings.sandbox);
|
|
39794
|
+
warnOnTrustAffectingEntries({
|
|
39795
|
+
toolLabel: DEVIN_TOOL_LABEL,
|
|
39796
|
+
noun: "sandbox change",
|
|
39797
|
+
entries: [
|
|
39798
|
+
...replacesUnreadableSandbox ? [{
|
|
39799
|
+
label: "sandbox",
|
|
39800
|
+
reason: replacedUnreadableReason({
|
|
39801
|
+
shape: "object",
|
|
39802
|
+
toolLabel: DEVIN_TOOL_LABEL
|
|
39803
|
+
})
|
|
39804
|
+
}] : [],
|
|
39805
|
+
...collectTrustAffectingSandboxPaths({
|
|
39806
|
+
sandbox: authoredSandboxRecord,
|
|
39807
|
+
paths: DEVIN_TRUST_AFFECTING_SANDBOX_PATHS
|
|
39808
|
+
}),
|
|
39809
|
+
...collectRestrictionLosingSandboxEntries({
|
|
39810
|
+
existing: existingSandbox,
|
|
39811
|
+
merged: mergedSandbox,
|
|
39812
|
+
paths: DEVIN_RESTRICTION_LOSING_SANDBOX_PATHS,
|
|
39813
|
+
toolLabel: DEVIN_TOOL_LABEL
|
|
39814
|
+
})
|
|
39815
|
+
],
|
|
39816
|
+
relativeFilePath: toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath)),
|
|
39817
|
+
logger
|
|
39818
|
+
});
|
|
39819
|
+
} 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.");
|
|
39820
|
+
}
|
|
39544
39821
|
return new DevinPermissions({
|
|
39545
39822
|
outputRoot,
|
|
39546
39823
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -39549,7 +39826,7 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39549
39826
|
fileKey: sharedConfigFileKey(paths),
|
|
39550
39827
|
feature: "permissions",
|
|
39551
39828
|
existingContent,
|
|
39552
|
-
patch
|
|
39829
|
+
patch,
|
|
39553
39830
|
filePath
|
|
39554
39831
|
}),
|
|
39555
39832
|
validate
|
|
@@ -39569,7 +39846,10 @@ var DevinPermissions = class DevinPermissions extends ToolPermissions {
|
|
|
39569
39846
|
ask: Array.isArray(permissions.ask) ? permissions.ask : [],
|
|
39570
39847
|
deny: Array.isArray(permissions.deny) ? permissions.deny : []
|
|
39571
39848
|
});
|
|
39572
|
-
|
|
39849
|
+
const sandbox = asDevinRecord(settings.sandbox);
|
|
39850
|
+
const result = { ...config };
|
|
39851
|
+
if (Object.keys(sandbox).length > 0) result.devin = { sandbox };
|
|
39852
|
+
return this.toRulesyncPermissionsDefault({ fileContent: JSON.stringify(result, null, 2) });
|
|
39573
39853
|
}
|
|
39574
39854
|
validate() {
|
|
39575
39855
|
return {
|
|
@@ -42517,6 +42797,137 @@ function buildReasonixPermissionEntry(toolName, pattern) {
|
|
|
42517
42797
|
if (pattern === "*") return toolName;
|
|
42518
42798
|
return `${toolName}(${pattern})`;
|
|
42519
42799
|
}
|
|
42800
|
+
/** How Reasonix is named in the warnings this file emits. */
|
|
42801
|
+
const REASONIX_TOOL_LABEL = "Reasonix";
|
|
42802
|
+
/** The `[permissions]` key the override's `allowDynamicBash` writes and reads. */
|
|
42803
|
+
const REASONIX_ALLOW_DYNAMIC_BASH_KEY = "allow_dynamic_bash";
|
|
42804
|
+
/**
|
|
42805
|
+
* `[sandbox]` keys whose authored value loosens the enforcement layer beneath
|
|
42806
|
+
* the permission policy: they take Bash out of its OS jail, open that jail to
|
|
42807
|
+
* the network, or widen where the file-writing built-ins may write. Written —
|
|
42808
|
+
* the ordinary uses are far too common to refuse — but never silently, the same
|
|
42809
|
+
* stance `DEVIN_TRUST_AFFECTING_SANDBOX_PATHS` takes, and `widens` likewise
|
|
42810
|
+
* names the restrictive value so a spelling Reasonix does not recognize is
|
|
42811
|
+
* reported rather than waved through.
|
|
42812
|
+
*
|
|
42813
|
+
* `forbid_read` is not here: it restricts, so it loosens by losing entries
|
|
42814
|
+
* rather than by holding one, which needs the before/after comparison
|
|
42815
|
+
* {@link REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS} below does instead.
|
|
42816
|
+
*
|
|
42817
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
42818
|
+
*/
|
|
42819
|
+
const REASONIX_TRUST_AFFECTING_SANDBOX_PATHS = [
|
|
42820
|
+
{
|
|
42821
|
+
path: ["bash"],
|
|
42822
|
+
reason: "anything but 'enforce' takes Bash out of the OS sandbox, so a command may write and read wherever the user can",
|
|
42823
|
+
widens: (value) => value !== "enforce"
|
|
42824
|
+
},
|
|
42825
|
+
{
|
|
42826
|
+
path: ["network"],
|
|
42827
|
+
reason: "lets sandboxed Bash reach the network",
|
|
42828
|
+
widens: isNotFalse
|
|
42829
|
+
},
|
|
42830
|
+
{
|
|
42831
|
+
path: ["allow_write"],
|
|
42832
|
+
reason: "adds directories the file-writing tools may modify outside the workspace root, which a headless run would otherwise refuse",
|
|
42833
|
+
widens: isNonEmptyList
|
|
42834
|
+
},
|
|
42835
|
+
{
|
|
42836
|
+
path: ["workspace_root"],
|
|
42837
|
+
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",
|
|
42838
|
+
widens: escapesTheProject
|
|
42839
|
+
}
|
|
42840
|
+
];
|
|
42841
|
+
/**
|
|
42842
|
+
* Whether a `workspace_root` points somewhere other than inside the project the
|
|
42843
|
+
* generate runs in. The key moves the write confinement rather than adding to
|
|
42844
|
+
* it, so the ordinary value — the project directory, spelled relatively — would
|
|
42845
|
+
* otherwise be announced on every generate; anything else is the case worth
|
|
42846
|
+
* naming, since it is how a fetched permissions file would put `~/.ssh` or
|
|
42847
|
+
* `C:\\Users\\<user>` inside the jail: an absolute path in either flavour, one
|
|
42848
|
+
* carrying a drive letter, a home-relative one, a shell or environment
|
|
42849
|
+
* expansion, and any path holding a `..` segment — even one that would land back
|
|
42850
|
+
* inside, since resolving it here would only be a guess at what Reasonix does.
|
|
42851
|
+
* Both path flavours are asked because the file is authored on one machine and
|
|
42852
|
+
* generated on another, so a Windows-shaped root reaching a POSIX check must not
|
|
42853
|
+
* read as relative. Anything that is not a string is reported, per the fail-safe
|
|
42854
|
+
* rule the predicates in `sandbox-trust.ts` follow.
|
|
42855
|
+
*/
|
|
42856
|
+
function escapesTheProject(value) {
|
|
42857
|
+
if (typeof value !== "string") return true;
|
|
42858
|
+
const trimmed = value.trim();
|
|
42859
|
+
if (trimmed === "") return false;
|
|
42860
|
+
if (trimmed.startsWith("~")) return true;
|
|
42861
|
+
if (posix.isAbsolute(trimmed) || win32.isAbsolute(trimmed)) return true;
|
|
42862
|
+
if (/^[A-Za-z]:/.test(trimmed)) return true;
|
|
42863
|
+
if (trimmed.includes("$") || /%[^%]+%/.test(trimmed)) return true;
|
|
42864
|
+
return trimmed.split(/[\\/]/).includes("..");
|
|
42865
|
+
}
|
|
42866
|
+
/**
|
|
42867
|
+
* The `[sandbox]` key that restricts, and so loosens by losing entries rather
|
|
42868
|
+
* than by holding a value. The override is shallow-merged over the existing
|
|
42869
|
+
* `[sandbox]` at its top level, so an authored `forbid_read` replaces the list
|
|
42870
|
+
* the file had whole — emptying it, or dropping the `${HOME}/.ssh` entry
|
|
42871
|
+
* Reasonix's own example recommends, opens exactly what the list kept closed.
|
|
42872
|
+
*
|
|
42873
|
+
* @see https://github.com/esengine/DeepSeek-Reasonix/blob/main-v2/docs/SPEC.md
|
|
42874
|
+
*/
|
|
42875
|
+
const REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS = [{
|
|
42876
|
+
path: ["forbid_read"],
|
|
42877
|
+
reason: "drops paths the list already in the file kept out of read, list and search",
|
|
42878
|
+
loosens: ({ before, after }) => before.some((entry) => !after.includes(entry))
|
|
42879
|
+
}];
|
|
42880
|
+
/**
|
|
42881
|
+
* Everything worth naming about the `[sandbox]` table this generate is about to
|
|
42882
|
+
* write: the authored values that loosen enforcement, the restrictions the
|
|
42883
|
+
* shallow merge would drop, and a `[sandbox]` the file holds in some shape other
|
|
42884
|
+
* than a table, which the write replaces wholesale. The widening check reads the
|
|
42885
|
+
* authored block alone — a loosening value the file already held is the user's
|
|
42886
|
+
* own, and re-announcing it on every generate would bury the values rulesync
|
|
42887
|
+
* actually wrote — while a loss can only be seen from both sides.
|
|
42888
|
+
*/
|
|
42889
|
+
function collectSandboxOverlayEntries({ existing, authored, merged }) {
|
|
42890
|
+
return [
|
|
42891
|
+
...existing !== void 0 && !isRecord$1(existing) ? [{
|
|
42892
|
+
label: "sandbox",
|
|
42893
|
+
reason: replacedUnreadableReason({
|
|
42894
|
+
shape: "object",
|
|
42895
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
42896
|
+
})
|
|
42897
|
+
}] : [],
|
|
42898
|
+
...collectTrustAffectingSandboxPaths({
|
|
42899
|
+
sandbox: asReasonixRecord(authored),
|
|
42900
|
+
paths: REASONIX_TRUST_AFFECTING_SANDBOX_PATHS
|
|
42901
|
+
}),
|
|
42902
|
+
...collectRestrictionLosingSandboxEntries({
|
|
42903
|
+
existing: asReasonixRecord(existing),
|
|
42904
|
+
merged,
|
|
42905
|
+
paths: REASONIX_RESTRICTION_LOSING_SANDBOX_PATHS,
|
|
42906
|
+
toolLabel: REASONIX_TOOL_LABEL
|
|
42907
|
+
})
|
|
42908
|
+
];
|
|
42909
|
+
}
|
|
42910
|
+
/**
|
|
42911
|
+
* Writes the override's `allow_dynamic_bash` into `[permissions]`, where it sits
|
|
42912
|
+
* beside allow/ask/deny rather than in a table of its own, and reports it when
|
|
42913
|
+
* it is being turned on: it widens what a shareable permissions file lets run
|
|
42914
|
+
* with no human in the loop. Only an authored value is written — leaving the key
|
|
42915
|
+
* out of the override keeps whatever the file already had — and turning it off
|
|
42916
|
+
* narrows, so that stays quiet. Only a literal `false` is quiet, not everything
|
|
42917
|
+
* falsy: `getJson()` casts rather than parses, so a `--no-validate` run can put
|
|
42918
|
+
* a value the schema forbids here, and a value Reasonix might still coerce is
|
|
42919
|
+
* not something to write in silence. The entries are returned rather than logged so
|
|
42920
|
+
* one generate still produces one warning naming everything it wrote.
|
|
42921
|
+
*/
|
|
42922
|
+
function applyAllowDynamicBash({ permissions, authored }) {
|
|
42923
|
+
if (authored === void 0) return [];
|
|
42924
|
+
permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY] = authored;
|
|
42925
|
+
if (!isNotFalse(authored)) return [];
|
|
42926
|
+
return [{
|
|
42927
|
+
label: `permissions.${REASONIX_ALLOW_DYNAMIC_BASH_KEY}`,
|
|
42928
|
+
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"
|
|
42929
|
+
}];
|
|
42930
|
+
}
|
|
42520
42931
|
function parseReasonixConfig(fileContent) {
|
|
42521
42932
|
const parsed = smolToml.parse(fileContent || smolToml.stringify({}));
|
|
42522
42933
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
@@ -42573,6 +42984,7 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42573
42984
|
static async fromRulesyncPermissions({ outputRoot = process.cwd(), rulesyncPermissions, validate = true, logger, global = false }) {
|
|
42574
42985
|
const paths = this.getSettablePaths({ global });
|
|
42575
42986
|
const filePath = join(outputRoot, paths.relativeDirPath, paths.relativeFilePath);
|
|
42987
|
+
const relativeFilePathForLog = toPosixPath(join(paths.relativeDirPath, paths.relativeFilePath));
|
|
42576
42988
|
const existingContent = await readFileContentOrNull(filePath) ?? "";
|
|
42577
42989
|
const parsed = parseReasonixConfig(existingContent);
|
|
42578
42990
|
const config = rulesyncPermissions.getJson();
|
|
@@ -42617,11 +43029,23 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42617
43029
|
...deny,
|
|
42618
43030
|
...rawDeny
|
|
42619
43031
|
]);
|
|
43032
|
+
const trustAffecting = applyAllowDynamicBash({
|
|
43033
|
+
permissions: mergedPermissions,
|
|
43034
|
+
authored: override?.allowDynamicBash
|
|
43035
|
+
});
|
|
42620
43036
|
const patch = { permissions: mergedPermissions };
|
|
42621
|
-
if (override?.sandbox !== void 0)
|
|
42622
|
-
|
|
42623
|
-
|
|
42624
|
-
|
|
43037
|
+
if (override?.sandbox !== void 0) {
|
|
43038
|
+
const mergedSandbox = {
|
|
43039
|
+
...asReasonixRecord(parsed.sandbox),
|
|
43040
|
+
...asReasonixRecord(override.sandbox)
|
|
43041
|
+
};
|
|
43042
|
+
patch.sandbox = mergedSandbox;
|
|
43043
|
+
trustAffecting.push(...collectSandboxOverlayEntries({
|
|
43044
|
+
existing: parsed.sandbox,
|
|
43045
|
+
authored: override.sandbox,
|
|
43046
|
+
merged: mergedSandbox
|
|
43047
|
+
}));
|
|
43048
|
+
}
|
|
42625
43049
|
if (override?.agent !== void 0) {
|
|
42626
43050
|
const mergedAgent = {
|
|
42627
43051
|
...asReasonixRecord(parsed.agent),
|
|
@@ -42629,9 +43053,16 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42629
43053
|
};
|
|
42630
43054
|
const retired = REASONIX_RETIRED_AGENT_KEYS.filter((key) => mergedAgent[key] !== void 0);
|
|
42631
43055
|
for (const key of retired) delete mergedAgent[key];
|
|
42632
|
-
if (retired.length > 0) logger?.warn(`Reasonix permissions: removing ${retired.map((key) => `"${key}"`).join(", ")} from [agent] in ${
|
|
43056
|
+
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.`);
|
|
42633
43057
|
patch.agent = mergedAgent;
|
|
42634
43058
|
}
|
|
43059
|
+
warnOnTrustAffectingEntries({
|
|
43060
|
+
toolLabel: REASONIX_TOOL_LABEL,
|
|
43061
|
+
noun: "change",
|
|
43062
|
+
entries: trustAffecting,
|
|
43063
|
+
relativeFilePath: relativeFilePathForLog,
|
|
43064
|
+
logger
|
|
43065
|
+
});
|
|
42635
43066
|
return new ReasonixPermissions({
|
|
42636
43067
|
outputRoot,
|
|
42637
43068
|
relativeDirPath: paths.relativeDirPath,
|
|
@@ -42666,6 +43097,8 @@ var ReasonixPermissions = class ReasonixPermissions extends ToolPermissions {
|
|
|
42666
43097
|
const sandbox = asReasonixRecord(this.toml.sandbox);
|
|
42667
43098
|
const agentPlanMode = pickReasonixKeys(this.toml.agent, [...REASONIX_OVERRIDE_AGENT_KEYS, ...REASONIX_RETIRED_AGENT_KEYS]);
|
|
42668
43099
|
const reasonixOverride = {};
|
|
43100
|
+
const allowDynamicBash = permissions[REASONIX_ALLOW_DYNAMIC_BASH_KEY];
|
|
43101
|
+
if (typeof allowDynamicBash === "boolean") reasonixOverride.allowDynamicBash = allowDynamicBash;
|
|
42669
43102
|
if (Object.keys(sandbox).length > 0) reasonixOverride.sandbox = sandbox;
|
|
42670
43103
|
if (Object.keys(agentPlanMode).length > 0) reasonixOverride.agent = agentPlanMode;
|
|
42671
43104
|
if (allowSplit.exact.length > 0) reasonixOverride.rawAllow = allowSplit.exact;
|
|
@@ -68704,4 +69137,4 @@ async function importChecksCore(params) {
|
|
|
68704
69137
|
//#endregion
|
|
68705
69138
|
export { RulesyncCheckFrontmatterSchema as $, ALL_TOOL_TARGETS_WITH_WILDCARD as $t, FACTORYDROID_DIR as A, RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH as An, fileExists as At, RulesyncSkillFrontmatterSchema as B, stripControlCharacters as Bn, readFileContent as Bt, CODEXCLI_BASH_RULES_FILE_NAME as C, RULESYNC_PERMISSIONS_LEGACY_FILE_NAME as Cn, assertDirectoryIfExists as Ct, CLAUDECODE_MEMORIES_DIR_NAME as D, RULESYNC_RULES_RELATIVE_DIR_PATH as Dn, createTempDirectory as Dt, CLAUDECODE_LOCAL_RULE_FILE_NAME as E, RULESYNC_RELATIVE_DIR_PATH as En, checkPathTraversal as Et, AUGMENTCODE_SETTINGS_LOCAL_FILE_NAME as F, formatError as Fn, isSymlink as Ft, RulesyncIgnore as G, removeFileStrict as Gt, RulesyncRuleFrontmatterSchema as H, stripHiddenCharacters as Hn, removeDirectory as Ht, getLocalSkillDirNames as I, truncateText as In, listDirectoryEntryNames as It, resolveRulesyncSourceWritePath as J, runWithDirectoryRollback as Jt, RulesyncHooks as K, removeTempDirectory as Kt, RulesyncSubagent as L, hasDeceptiveHiddenCharacters as Ln, listFilePathsRecursively as Lt, caseFoldIdentity as M, ALL_FEATURES as Mn, getHomeDirectory as Mt, groupSpellingsByCaseFoldedIdentity as N, ALL_FEATURES_WITH_WILDCARD as Nn, isFileNotFoundError as Nt, CLAUDECODE_SETTINGS_LOCAL_FILE_NAME as O, RULESYNC_SKILLS_RELATIVE_DIR_PATH as On, directoryExists as Ot, AUGMENTCODE_DIR as P, DEPRECATED_FEATURE_REPLACEMENTS as Pn, isFileSystemError as Pt, RulesyncCheck as Q, ALL_TOOL_TARGETS as Qt, RulesyncSubagentFrontmatterSchema as R, hasEnclosingMarkOutsideKeycap as Rn, listSubdirectoryNames as Rt, ChecksProcessor as S, RULESYNC_PERMISSIONS_FILE_NAME as Sn, applyFileMode as St, CLAUDECODE_DIR as T, RULESYNC_PERMISSIONS_SCHEMA_URL as Tn, assertWritablePathInsideRoot as Tt, RulesyncPermissions as U, removeDirectoryStrict as Ut, RulesyncRule as V, stripControlCharactersKeepingLineFeeds as Vn, readFileContentOrNull as Vt, RulesyncMcp as W, removeFile as Wt, RulesyncCommand as X, writeFileBuffer as Xt, parseJsonc as Y, toPosixPath as Yt, RulesyncCommandFrontmatterSchema as Z, writeFileContent as Zt, IgnoreProcessor as _, RULESYNC_MCP_FILE_NAME as _n, withFallbackLoggerTarget as _t, getProcessorRegistryEntry as a, RULESYNC_AIIGNORE_RELATIVE_FILE_PATH as an, mergeInputRootConfigs as at, QWENCODE_DIR as b, RULESYNC_MCP_SCHEMA_URL as bn, CLIError as bt, RulesProcessor as c, RULESYNC_CONFIG_RELATIVE_FILE_PATH as cn, ConfigFileSchema as ct, CODEBUDDY_DIR as d, RULESYNC_CURATED_SKILLS_RELATIVE_DIR_PATH as dn, findControlCharacter as dt, PACKAGING_TOOL_TARGETS as en, stringifyFrontmatter as et, CODEBUDDY_LOCAL_RULE_FILE_NAME as f, RULESYNC_HOOKS_FILE_NAME as fn, ConsoleLogger as ft, McpProcessor as g, RULESYNC_LOCAL_CONFIG_RELATIVE_FILE_PATH as gn, warnOnConflictingFlags as gt, shortenToWidth as h, RULESYNC_IGNORE_RELATIVE_FILE_PATH as hn, fallbackLogger as ht, inspectInputRoots as i, RULESYNC_AIIGNORE_FILE_NAME as in, ConfigResolver as it, FACTORYDROID_SETTINGS_LOCAL_FILE_NAME as j, parseCommaSeparatedList as jn, getFileSize as jt, CLAUDECODE_SKILLS_DIR_PATH as k, RULESYNC_SOURCES_LOCK_RELATIVE_FILE_PATH as kn, ensureDir as kt, SubagentsProcessor as l, RULESYNC_CONFIG_SCHEMA_URL as ln, GITIGNORE_DESTINATION_KEY as lt, displayWidthOf as m, RULESYNC_HOOKS_RELATIVE_FILE_PATH as mn, WarningCollectingLogger as mt, formatSourceLoadFailure as n, CURATED_RULES_FEATURE_SUBDIR as nn, SHARED_USER_MANAGED_CONFIG_PATHS as nt, convertFromTool as o, RULESYNC_CHECKS_RELATIVE_DIR_PATH as on, resolveEffectiveInputRoots as ot, ELLIPSIS_WIDTH as p, RULESYNC_HOOKS_LEGACY_FILE_NAME as pn, JsonLogger as pt, getRulesyncSourceCandidates as q, resolvePath as qt, generate as r, MAX_FILE_SIZE as rn, SKILL_FILE_NAME as rt, isPackagingToolTarget as s, RULESYNC_COMMANDS_RELATIVE_DIR_PATH as sn, CONFLICTING_TARGET_PAIRS as st, importFromTool as t, ToolTargetSchema as tn, loadYaml as tt, SkillsProcessor as u, RULESYNC_CURATED_RULES_RELATIVE_DIR_PATH as un, SourceEntrySchema as ut, HooksProcessor as v, RULESYNC_MCP_LEGACY_FILE_NAME as vn, resetRunWarningState as vt, CODEXCLI_DIR as w, RULESYNC_PERMISSIONS_RELATIVE_FILE_PATH as wn, assertTreeContainsNoSymlinks as wt, QWENCODE_LOCAL_RULE_FILE_NAME as x, RULESYNC_NPM_SOURCES_LOCK_RELATIVE_FILE_PATH as xn, ErrorCodes as xt, CommandsProcessor as y, RULESYNC_MCP_RELATIVE_FILE_PATH as yn, withWarnOnceScope as yt, RulesyncSkill as z, quoteForLog as zn, pathEscapesRoot as zt };
|
|
68706
69139
|
|
|
68707
|
-
//# sourceMappingURL=import-
|
|
69140
|
+
//# sourceMappingURL=import-DIDEUv63.js.map
|