dsh-perm-gate 5.3.4 → 5.4.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/CHANGELOG.ja.md +6 -0
- package/CHANGELOG.ko.md +6 -0
- package/CHANGELOG.md +8 -0
- package/README.ja.md +24 -0
- package/README.ko.md +24 -0
- package/README.md +42 -0
- package/README.zh.md +30 -0
- package/dsh.plugin.json +3 -3
- package/lib/client.js +319 -0
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +40 -1
- package/lib/config.js +7 -1
- package/lib/cred-families.d.ts +22 -0
- package/lib/cred-families.js +63 -0
- package/lib/engine.js +36 -3
- package/lib/evaluate.d.ts +8 -0
- package/lib/events.d.ts +19 -0
- package/lib/events.js +99 -4
- package/lib/index.js +39 -4
- package/lib/preset-rules.d.ts +33 -1
- package/lib/preset-rules.js +137 -0
- package/lib/runtime.d.ts +22 -0
- package/lib/runtime.js +23 -1
- package/package.json +1 -1
package/lib/config.d.ts
CHANGED
|
@@ -95,6 +95,33 @@ export interface PermGateConfig {
|
|
|
95
95
|
* rather answer than auto-run. Uncommon by design.
|
|
96
96
|
*/
|
|
97
97
|
readonly safenetAsk?: boolean;
|
|
98
|
+
/**
|
|
99
|
+
* Protected-branch push floor — **off by default means the floor stands**.
|
|
100
|
+
* The P0 layer hard-denies `git push --force` / `--force-with-lease` / `--delete` aimed at
|
|
101
|
+
* the protected branch list (main/master/production/release/stable). This switch is the
|
|
102
|
+
* deliberate opt-out for developers who own their remotes: when true, the floor steps
|
|
103
|
+
* aside and the call falls to the seeded `git.push-force` rule, which still **asks a
|
|
104
|
+
* person** every time unless the user revokes that entry or allowlists the form in their
|
|
105
|
+
* own rules. It cannot widen anything else: reads, wipes and credential floors are untouched.
|
|
106
|
+
*/
|
|
107
|
+
readonly allowProtectedBranchPush?: boolean;
|
|
108
|
+
/**
|
|
109
|
+
* Credential-family ladder — the **readwrite** rung per family. A listed family's
|
|
110
|
+
* built-in credential paths are exempted from exactly one P0 clause (credential-store
|
|
111
|
+
* **writes** — `npm login` legitimately rewrites `~/.npmrc`). Everything else is
|
|
112
|
+
* untouched: read floors, root/drive screens, dshHome protection, neverClear and the
|
|
113
|
+
* push floor stay armed at every rung; unlisted families are fully protected.
|
|
114
|
+
* Values are family ids (`ssh|cloud|package|gitcreds|env|cli|db`); anything outside
|
|
115
|
+
* the roster is dropped at read time, never guessed at.
|
|
116
|
+
*/
|
|
117
|
+
readonly credWriteExemptFamilies?: string[];
|
|
118
|
+
/**
|
|
119
|
+
* Seed watermark for the preset deny table. Activation re-runs the (per-id idempotent)
|
|
120
|
+
* merge whenever this lags the shipped table version, so entries added by a plugin
|
|
121
|
+
* update (the `cred.*` family rows) land on existing installs — and entries the user
|
|
122
|
+
* revoked stay revoked, because the merge only ever appends missing ids.
|
|
123
|
+
*/
|
|
124
|
+
readonly presetRulesVersion?: number;
|
|
98
125
|
/**
|
|
99
126
|
* Degraded review — fall back to a **rule-only gate when there is no reviewer**.
|
|
100
127
|
*
|
|
@@ -483,6 +510,12 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
483
510
|
safenetPass: z<boolean, boolean, "defined">;
|
|
484
511
|
/** Switch: the `permissive-snet` implementation exists (mechanical screen, still asks). */
|
|
485
512
|
safenetAsk: z<boolean, boolean, "defined">;
|
|
513
|
+
/** Opt-out of the P0 protected-branch push floor; default false keeps the floor. */
|
|
514
|
+
allowProtectedBranchPush: z<boolean, boolean, "defined">;
|
|
515
|
+
/** readwrite rung per credential family; unknown ids dropped at consumption. */
|
|
516
|
+
credWriteExemptFamilies: z<string[], string[], "defined">;
|
|
517
|
+
/** Preset-table seed watermark; activation backfills when it lags the shipped version. */
|
|
518
|
+
presetRulesVersion: z<number, number, "defined">;
|
|
486
519
|
scriptReview: z<boolean, boolean, "defined">;
|
|
487
520
|
gatePresets: z<string[], string[], "plain">;
|
|
488
521
|
permissiveStrategies: z<Schemastery.ObjectS<NoInfer<{
|
|
@@ -562,6 +595,12 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
562
595
|
safenetPass: z<boolean, boolean, "defined">;
|
|
563
596
|
/** Switch: the `permissive-snet` implementation exists (mechanical screen, still asks). */
|
|
564
597
|
safenetAsk: z<boolean, boolean, "defined">;
|
|
598
|
+
/** Opt-out of the P0 protected-branch push floor; default false keeps the floor. */
|
|
599
|
+
allowProtectedBranchPush: z<boolean, boolean, "defined">;
|
|
600
|
+
/** readwrite rung per credential family; unknown ids dropped at consumption. */
|
|
601
|
+
credWriteExemptFamilies: z<string[], string[], "defined">;
|
|
602
|
+
/** Preset-table seed watermark; activation backfills when it lags the shipped version. */
|
|
603
|
+
presetRulesVersion: z<number, number, "defined">;
|
|
565
604
|
scriptReview: z<boolean, boolean, "defined">;
|
|
566
605
|
gatePresets: z<string[], string[], "plain">;
|
|
567
606
|
permissiveStrategies: z<Schemastery.ObjectS<NoInfer<{
|
|
@@ -611,7 +650,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
|
611
650
|
/** Live reference the 0.1.7 loader hands `apply` for `.volatile()` config fields. */
|
|
612
651
|
export type VolatileRef<T> = Volatile<T>;
|
|
613
652
|
/** The config fields marked `.volatile()` — live refs inside `apply`'s config. */
|
|
614
|
-
export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "yolo", "rulesFallback", "unattendedTiers", "safenetPass", "safenetAsk", "scriptReview", "permissiveStrategies", "allowlist", "denyKeywords", "denyKeywordsExtra", "credentialPaths", "extraContentArgKeys", "extraOperationArgKeys", "presetRulesSeeded", "autoAllowTools", "sessionSweep", "searchUp", "badFilePolicy", "maxChainLength", "networkEnabled", "networkMode", "networkUnlisted", "networkUnattributed", "networkLoopback", "networkBind", "networkPort", "networkNoProxy", "networkInjectEnv", "networkAskTimeoutMs", "networkGrantTtlMs", "rules", "sandboxEnabled", "sandboxMode", "sandboxProxy", "sandboxLoopback"];
|
|
653
|
+
export declare const VOLATILE_CONFIG_KEYS: readonly ["defaultAction", "caseInsensitivePaths", "classifierEnabled", "classifierEndpoint", "classifierModel", "classifierApiKey", "riskTimeoutMs", "riskLearning", "riskSediment", "classifierSource", "classifierProvider", "riskThreshold", "grantTtlMs", "grantMaxUses", "permissive", "yolo", "rulesFallback", "unattendedTiers", "safenetPass", "safenetAsk", "allowProtectedBranchPush", "credWriteExemptFamilies", "presetRulesVersion", "scriptReview", "permissiveStrategies", "allowlist", "denyKeywords", "denyKeywordsExtra", "credentialPaths", "extraContentArgKeys", "extraOperationArgKeys", "presetRulesSeeded", "autoAllowTools", "sessionSweep", "searchUp", "badFilePolicy", "maxChainLength", "networkEnabled", "networkMode", "networkUnlisted", "networkUnattributed", "networkLoopback", "networkBind", "networkPort", "networkNoProxy", "networkInjectEnv", "networkAskTimeoutMs", "networkGrantTtlMs", "rules", "sandboxEnabled", "sandboxMode", "sandboxProxy", "sandboxLoopback"];
|
|
615
654
|
/** Resolve one possibly-volatile field: a live ref on 0.1.7+, a plain value otherwise. */
|
|
616
655
|
export declare function readVolatileValue<T>(value: T | Volatile<T> | undefined): VolatileSnapshot<T> | undefined;
|
|
617
656
|
/** Shallow-resolve every volatile field into a plain snapshot (one per read). */
|
package/lib/config.js
CHANGED
|
@@ -207,6 +207,12 @@ export const Config = z.object({
|
|
|
207
207
|
safenetPass: vol(z.boolean().default(true)),
|
|
208
208
|
/** Switch: the `permissive-snet` implementation exists (mechanical screen, still asks). */
|
|
209
209
|
safenetAsk: vol(z.boolean().default(false)),
|
|
210
|
+
/** Opt-out of the P0 protected-branch push floor; default false keeps the floor. */
|
|
211
|
+
allowProtectedBranchPush: vol(z.boolean().default(false)),
|
|
212
|
+
/** readwrite rung per credential family; unknown ids dropped at consumption. */
|
|
213
|
+
credWriteExemptFamilies: vol(z.array(z.string()).default([])),
|
|
214
|
+
/** Preset-table seed watermark; activation backfills when it lags the shipped version. */
|
|
215
|
+
presetRulesVersion: vol(z.number().min(0).default(0)),
|
|
210
216
|
scriptReview: vol(z.boolean().default(true)),
|
|
211
217
|
gatePresets: z.array(z.string()),
|
|
212
218
|
permissiveStrategies: vol(z.object({
|
|
@@ -260,7 +266,7 @@ export const VOLATILE_CONFIG_KEYS = [
|
|
|
260
266
|
'defaultAction', 'caseInsensitivePaths', 'classifierEnabled', 'classifierEndpoint',
|
|
261
267
|
'classifierModel', 'classifierApiKey', 'riskTimeoutMs', 'riskLearning', 'riskSediment',
|
|
262
268
|
'classifierSource', 'classifierProvider', 'riskThreshold', 'grantTtlMs', 'grantMaxUses',
|
|
263
|
-
'permissive', 'yolo', 'rulesFallback', 'unattendedTiers', 'safenetPass', 'safenetAsk', 'scriptReview', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'denyKeywordsExtra', 'credentialPaths', 'extraContentArgKeys', 'extraOperationArgKeys', 'presetRulesSeeded', 'autoAllowTools',
|
|
269
|
+
'permissive', 'yolo', 'rulesFallback', 'unattendedTiers', 'safenetPass', 'safenetAsk', 'allowProtectedBranchPush', 'credWriteExemptFamilies', 'presetRulesVersion', 'scriptReview', 'permissiveStrategies', 'allowlist', 'denyKeywords', 'denyKeywordsExtra', 'credentialPaths', 'extraContentArgKeys', 'extraOperationArgKeys', 'presetRulesSeeded', 'autoAllowTools',
|
|
264
270
|
'sessionSweep', 'searchUp', 'badFilePolicy', 'maxChainLength',
|
|
265
271
|
'networkEnabled', 'networkMode', 'networkUnlisted', 'networkUnattributed', 'networkLoopback',
|
|
266
272
|
'networkBind', 'networkPort', 'networkNoProxy', 'networkInjectEnv', 'networkAskTimeoutMs',
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export type CredFamily = 'ssh' | 'cloud' | 'package' | 'gitcreds' | 'env' | 'cli' | 'db';
|
|
2
|
+
/**
|
|
3
|
+
* Every family id, in the order the settings face should offer them. Lowercase +
|
|
4
|
+
* dots/dashes only: the ids ride inside the `[deny-rule:<id>]` tag, whose pattern
|
|
5
|
+
* does not accept uppercase (a `gitCreds` tag would never parse and the row could
|
|
6
|
+
* never be located for revocation).
|
|
7
|
+
*/
|
|
8
|
+
export declare const CRED_FAMILY_IDS: readonly CredFamily[];
|
|
9
|
+
/**
|
|
10
|
+
* Built-in path patterns per family, aligned with the P0 sensitive list (`path.ts`)
|
|
11
|
+
* and the cc-safety-net secret shapes the deny table was ported from. `~` forms are
|
|
12
|
+
* home-relative (the matcher strips or expands them); a bare dotted name is a
|
|
13
|
+
* segment-tail fragment matched at any depth.
|
|
14
|
+
*/
|
|
15
|
+
export declare const CRED_FAMILIES: Readonly<Record<CredFamily, readonly string[]>>;
|
|
16
|
+
/**
|
|
17
|
+
* Which credential family, if any, claims this path. First family in
|
|
18
|
+
* {@link CRED_FAMILY_IDS} order wins (ssh before package etc.), so a path cannot
|
|
19
|
+
* flip families between calls. `undefined` = not a built-in credential path —
|
|
20
|
+
* custom `credentialPaths` entries have no family and can never be write-exempt.
|
|
21
|
+
*/
|
|
22
|
+
export declare function familyOfPath(path: string | undefined): CredFamily | undefined;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential families — one shared answer to "which kind of secret is this path".
|
|
3
|
+
*
|
|
4
|
+
* The settings face exposes a four-step ladder per family (deny / ask / readonly /
|
|
5
|
+
* readwrite); the ladder's write side is enforced by `engine.hardDenyReason`, which
|
|
6
|
+
* consults `familyOfPath` against the `credWriteExemptFamilies` opt-out list. The
|
|
7
|
+
* exemption is deliberately coarse: family-level, binary, and it steps aside for
|
|
8
|
+
* exactly one P0 clause (the credential-store mutation floor). Root/drive wipes,
|
|
9
|
+
* protected destructive targets and the neverClear family are NOT family-scoped and
|
|
10
|
+
* stay armed whatever a user selects.
|
|
11
|
+
*
|
|
12
|
+
* Pure string work (no fs, no execution), so the client bundle can render the family
|
|
13
|
+
* roster from the same table. Matching reuses `compileCredentialPaths`, keeping glob
|
|
14
|
+
* semantics identical to the `credentialPaths` setting — one vocabulary, two uses.
|
|
15
|
+
*/
|
|
16
|
+
import { compileCredentialPaths } from './credential-paths.js';
|
|
17
|
+
/**
|
|
18
|
+
* Every family id, in the order the settings face should offer them. Lowercase +
|
|
19
|
+
* dots/dashes only: the ids ride inside the `[deny-rule:<id>]` tag, whose pattern
|
|
20
|
+
* does not accept uppercase (a `gitCreds` tag would never parse and the row could
|
|
21
|
+
* never be located for revocation).
|
|
22
|
+
*/
|
|
23
|
+
export const CRED_FAMILY_IDS = ['ssh', 'cloud', 'package', 'gitcreds', 'env', 'cli', 'db'];
|
|
24
|
+
/**
|
|
25
|
+
* Built-in path patterns per family, aligned with the P0 sensitive list (`path.ts`)
|
|
26
|
+
* and the cc-safety-net secret shapes the deny table was ported from. `~` forms are
|
|
27
|
+
* home-relative (the matcher strips or expands them); a bare dotted name is a
|
|
28
|
+
* segment-tail fragment matched at any depth.
|
|
29
|
+
*/
|
|
30
|
+
export const CRED_FAMILIES = {
|
|
31
|
+
ssh: ['~/.ssh', 'id_rsa', 'id_ed25519', 'id_ecdsa', 'id_dsa', 'authorized_keys'],
|
|
32
|
+
cloud: ['~/.aws', '~/.gcloud', '~/.config/gcloud', '~/.azure', '~/.azure/**', 'credentials.yaml', 'credentials.json'],
|
|
33
|
+
package: ['~/.npmrc', '.npmrc', '~/.pypirc', '~/.netrc', '.netrc', '.pypirc', '_netrc', '~/.pip/pip.conf'],
|
|
34
|
+
gitcreds: ['~/.git-credentials', '.git-credentials'],
|
|
35
|
+
env: ['.env', '.env.*', '*.env'],
|
|
36
|
+
cli: ['~/.claude', '~/.claude/**', '~/.codex', '~/.codex/**', '~/.config/gh', '~/.config/gh/**'],
|
|
37
|
+
db: ['~/.pgpass', 'pgpass', '~/.my.cnf', '.my.cnf'],
|
|
38
|
+
};
|
|
39
|
+
/** One compiled matcher per family (lazy; compile is pure and cheap, memoize anyway). */
|
|
40
|
+
const matcherCache = new Map();
|
|
41
|
+
function familyMatcher(family) {
|
|
42
|
+
let m = matcherCache.get(family);
|
|
43
|
+
if (m === undefined) {
|
|
44
|
+
m = compileCredentialPaths(CRED_FAMILIES[family], { caseInsensitive: true });
|
|
45
|
+
matcherCache.set(family, m);
|
|
46
|
+
}
|
|
47
|
+
return m;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Which credential family, if any, claims this path. First family in
|
|
51
|
+
* {@link CRED_FAMILY_IDS} order wins (ssh before package etc.), so a path cannot
|
|
52
|
+
* flip families between calls. `undefined` = not a built-in credential path —
|
|
53
|
+
* custom `credentialPaths` entries have no family and can never be write-exempt.
|
|
54
|
+
*/
|
|
55
|
+
export function familyOfPath(path) {
|
|
56
|
+
if (path === undefined || path.trim() === '')
|
|
57
|
+
return undefined;
|
|
58
|
+
for (const family of CRED_FAMILY_IDS) {
|
|
59
|
+
if (familyMatcher(family)(path))
|
|
60
|
+
return family;
|
|
61
|
+
}
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
package/lib/engine.js
CHANGED
|
@@ -16,6 +16,7 @@ import { executableVariants, normalizeCommandText } from './command-normalize.js
|
|
|
16
16
|
// 它看 `executableCommandOf`——已知非执行器(read/write/内部工具)的 `command` 参数是数据,
|
|
17
17
|
// 其余任何带命令串的调用都按命令对待,所以工具叫什么(包括模型打错的 `pash`)都不是绕过面。
|
|
18
18
|
import { INTERNAL_TOOLS, READ_TOOLS, executableCommandOf } from './tool-names.js';
|
|
19
|
+
import { familyOfPath } from './cred-families.js';
|
|
19
20
|
import { decomposeShellCommand } from './shell.js';
|
|
20
21
|
import { dispatchCommand } from './command-dispatcher.js';
|
|
21
22
|
const DESTRUCTIVE_TOOL = /(?:^|[_-])(?:delete|remove|rm|destroy|erase|purge|wipe|unlink|rmdir|reset)(?:$|[_-]|recursive)/i;
|
|
@@ -182,7 +183,11 @@ export function hardDenyReason(ctx) {
|
|
|
182
183
|
}
|
|
183
184
|
if (decodedReason !== undefined)
|
|
184
185
|
return decodedReason;
|
|
185
|
-
|
|
186
|
+
// The one floor a human may step aside: `allowProtectedBranchPush` (volatile setting,
|
|
187
|
+
// default false = floor stands). It is an explicit operator opt-out, never a model or
|
|
188
|
+
// grant path — and stepping aside still lands on the seeded `git.push-force` ask, so
|
|
189
|
+
// the call is named for a person unless the user separately revokes/allowlists it.
|
|
190
|
+
const rewrite = ctx.allowProtectedBranchPush === true ? undefined : gitRemoteRewriteReason(commands);
|
|
186
191
|
if (rewrite !== undefined)
|
|
187
192
|
return rewrite;
|
|
188
193
|
}
|
|
@@ -218,7 +223,16 @@ export function hardDenyReason(ctx) {
|
|
|
218
223
|
...(ctx.userHome === undefined ? {} : { home: ctx.userHome }),
|
|
219
224
|
});
|
|
220
225
|
if (path !== undefined && (ctx.tool === 'write' || ctx.tool === 'edit' || DESTRUCTIVE_TOOL.test(ctx.tool)) && isProtectedCredentialPath(path, declared)) {
|
|
221
|
-
|
|
226
|
+
// The one credential-floor a human may step aside per family: `credWriteExempt`
|
|
227
|
+
// (the readwrite rung of the credential-family ladder, fed by the volatile
|
|
228
|
+
// `credWriteExemptFamilies` setting). Family-level and binary — and it steps aside
|
|
229
|
+
// for **this clause only**: the protected-destructive-target screen above, the
|
|
230
|
+
// neverClear family and the push floor stay armed whatever a family is set to,
|
|
231
|
+
// which is why `readwrite` never means "write anything freely".
|
|
232
|
+
const exemptFamily = familyOfPath(path);
|
|
233
|
+
if (exemptFamily === undefined || !(ctx.credWriteExempt ?? []).includes(exemptFamily)) {
|
|
234
|
+
return `mutation targets a credential store: ${path}`;
|
|
235
|
+
}
|
|
222
236
|
}
|
|
223
237
|
if (READ_TOOLS.has(ctx.tool) && path !== undefined) {
|
|
224
238
|
const normalized = path.startsWith('/') || /^[A-Za-z]:/.test(path) ? path : path;
|
|
@@ -239,7 +253,26 @@ autoAllowExtra = NO_EXTRA_AUTO_ALLOW) {
|
|
|
239
253
|
// they cannot modify the workspace or execute a command, so asking a human
|
|
240
254
|
// about each one is pure noise. P0 hard-deny already protects sensitive-path
|
|
241
255
|
// reads and credential material, and the deny-keyword layer runs even earlier.
|
|
242
|
-
|
|
256
|
+
//
|
|
257
|
+
// READ tools first **consult the rule layer** (the credential-family ladder's
|
|
258
|
+
// read rung): a seeded `cred.*` entry (or any user rule naming a path) can route a
|
|
259
|
+
// read to ask/deny. Only a **named hit** counts — the ruleset's own default action
|
|
260
|
+
// (ruleIndex undefined) is not an opinion about this call, and treating it as one
|
|
261
|
+
// would turn every read into an ask under `defaultAction: ask` (measured: it did).
|
|
262
|
+
// A miss still auto-allows silently, so the popup budget is unchanged. INTERNAL
|
|
263
|
+
// tools are deliberately NOT pre-queried: session/memory tools reading their own
|
|
264
|
+
// stores is how the host works; a path rule blocking them would be host self-harm.
|
|
265
|
+
if (READ_TOOLS.has(ctx.tool) || autoAllowExtra.has(ctx.tool)) {
|
|
266
|
+
const pre = ruleset.decide(ctx);
|
|
267
|
+
if (pre.ruleIndex !== undefined) {
|
|
268
|
+
if (pre.action === 'deny')
|
|
269
|
+
return { ...pre, stage: 'rule' };
|
|
270
|
+
if (pre.action === 'ask')
|
|
271
|
+
return { ...pre, stage: 'default' };
|
|
272
|
+
}
|
|
273
|
+
return { action: 'allow', reason: 'read-only internal tool', stage: 'grant', ruleIndex: undefined };
|
|
274
|
+
}
|
|
275
|
+
if (INTERNAL_TOOLS.has(ctx.tool)) {
|
|
243
276
|
return { action: 'allow', reason: 'read-only internal tool', stage: 'grant', ruleIndex: undefined };
|
|
244
277
|
}
|
|
245
278
|
if (grants(ctx.tool, ctx.args) === 'allow') {
|
package/lib/evaluate.d.ts
CHANGED
|
@@ -31,6 +31,14 @@ export interface ToolCallContext {
|
|
|
31
31
|
readonly scanKeys?: ScanKeyPolicy;
|
|
32
32
|
/** Agent identity candidates from session context. */
|
|
33
33
|
readonly agentCandidates?: readonly string[];
|
|
34
|
+
/** Opt-out of the P0 protected-branch push floor (setting `allowProtectedBranchPush`). */
|
|
35
|
+
readonly allowProtectedBranchPush?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Credential families whose P0 credential-store **write** floor is exempted
|
|
38
|
+
* (setting `credWriteExemptFamilies`, the readwrite rung of the family ladder).
|
|
39
|
+
* Consulted by exactly one hardDeny clause; read floors never look at this.
|
|
40
|
+
*/
|
|
41
|
+
readonly credWriteExempt?: readonly string[];
|
|
34
42
|
/** Network request info (for network dimension matching). */
|
|
35
43
|
readonly network?: {
|
|
36
44
|
readonly domain?: string;
|
package/lib/events.d.ts
CHANGED
|
@@ -318,3 +318,22 @@ export declare function registerRulesRoute(server: unknown, provider: RulesRoute
|
|
|
318
318
|
* learning or drops one sedimented sample. Returns whether registered.
|
|
319
319
|
*/
|
|
320
320
|
export declare function registerLearningRoute(server: unknown, provider: LearningRouteProvider): (() => void) | undefined;
|
|
321
|
+
export declare const CRED_ROUTE = "/api/dsh-perm-gate/cred";
|
|
322
|
+
export interface CredRouteProvider {
|
|
323
|
+
/** Current raw rules document + the stored write-exemption list (no mutation). */
|
|
324
|
+
read(): {
|
|
325
|
+
doc: unknown;
|
|
326
|
+
exempt: readonly string[];
|
|
327
|
+
};
|
|
328
|
+
/** Persist the pair in ONE settings update (doc + exempt travel together). */
|
|
329
|
+
persist(doc: Record<string, unknown>, exempt: readonly string[]): Promise<void>;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* `GET /api/dsh-perm-gate/cred` → `{ ok, rungs: {family: deny|ask|readonly|readwrite}, exempt }`
|
|
333
|
+
* `POST /api/dsh-perm-gate/cred` → `{ family, rung }` applies one rung: rewrites the
|
|
334
|
+
* family's `cred.<id>` row in the rules document and the paired `credWriteExemptFamilies`
|
|
335
|
+
* list, persisted in a single settings update. The two stores are written only through
|
|
336
|
+
* here, so the ladder state cannot half-apply. Registration is optional like every
|
|
337
|
+
* other route (no webServer ⇒ the settings face degrades to read-only, never errors).
|
|
338
|
+
*/
|
|
339
|
+
export declare function registerCredRoute(server: unknown, provider: CredRouteProvider): (() => void) | undefined;
|
package/lib/events.js
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
|
+
import { credRungsOf, setCredFamilyAction } from './preset-rules.js';
|
|
18
|
+
import { CRED_FAMILY_IDS } from './cred-families.js';
|
|
17
19
|
const REASON_MAX = 200;
|
|
18
20
|
const JUSTIFICATION_MAX = 400;
|
|
19
21
|
const FILES_MAX = 8;
|
|
@@ -367,14 +369,49 @@ function json(res, code, body) {
|
|
|
367
369
|
}
|
|
368
370
|
function readBody(req, limit = 1024 * 1024) {
|
|
369
371
|
return new Promise((resolve, reject) => {
|
|
372
|
+
// Some embedded servers (the desktop webServer among them) hand the handler a
|
|
373
|
+
// pre-parsed body object instead of a readable stream — take it when present.
|
|
374
|
+
const pre = req.body ?? req.payload;
|
|
375
|
+
if (typeof pre === 'object' && pre !== null) {
|
|
376
|
+
resolve(pre);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
// A stream-less, body-less req must fail loud, not hang: with `req.on?.()` only,
|
|
380
|
+
// a missing `on` attached no listener and the promise never settled — the POST
|
|
381
|
+
// response never came, and the settings face sat on a silent spinner (the exact
|
|
382
|
+
// "only readwrite reacts" symptom reported on the desktop tier).
|
|
383
|
+
if (typeof req.on !== 'function') {
|
|
384
|
+
reject(new Error('request body stream unavailable'));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
370
387
|
let body = '';
|
|
371
|
-
|
|
388
|
+
let settled = false;
|
|
389
|
+
const timer = setTimeout(() => {
|
|
390
|
+
if (settled)
|
|
391
|
+
return;
|
|
392
|
+
settled = true;
|
|
393
|
+
reject(new Error('body read timeout'));
|
|
394
|
+
}, 8000);
|
|
395
|
+
req.on('data', (chunk) => {
|
|
372
396
|
body += String(chunk);
|
|
373
|
-
if (body.length > limit)
|
|
397
|
+
if (body.length > limit && !settled) {
|
|
398
|
+
settled = true;
|
|
399
|
+
clearTimeout(timer);
|
|
374
400
|
reject(new Error('payload too large'));
|
|
401
|
+
}
|
|
375
402
|
});
|
|
376
|
-
req.on
|
|
377
|
-
|
|
403
|
+
req.on('error', (err) => {
|
|
404
|
+
if (settled)
|
|
405
|
+
return;
|
|
406
|
+
settled = true;
|
|
407
|
+
clearTimeout(timer);
|
|
408
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
409
|
+
});
|
|
410
|
+
req.on('end', () => {
|
|
411
|
+
if (settled)
|
|
412
|
+
return;
|
|
413
|
+
settled = true;
|
|
414
|
+
clearTimeout(timer);
|
|
378
415
|
try {
|
|
379
416
|
resolve(body === '' ? {} : JSON.parse(body));
|
|
380
417
|
}
|
|
@@ -950,3 +987,61 @@ export function registerLearningRoute(server, provider) {
|
|
|
950
987
|
},
|
|
951
988
|
});
|
|
952
989
|
}
|
|
990
|
+
// ─── cred-family ladder (GET state / POST rung) ────────────────────────────
|
|
991
|
+
export const CRED_ROUTE = '/api/dsh-perm-gate/cred';
|
|
992
|
+
/**
|
|
993
|
+
* `GET /api/dsh-perm-gate/cred` → `{ ok, rungs: {family: deny|ask|readonly|readwrite}, exempt }`
|
|
994
|
+
* `POST /api/dsh-perm-gate/cred` → `{ family, rung }` applies one rung: rewrites the
|
|
995
|
+
* family's `cred.<id>` row in the rules document and the paired `credWriteExemptFamilies`
|
|
996
|
+
* list, persisted in a single settings update. The two stores are written only through
|
|
997
|
+
* here, so the ladder state cannot half-apply. Registration is optional like every
|
|
998
|
+
* other route (no webServer ⇒ the settings face degrades to read-only, never errors).
|
|
999
|
+
*/
|
|
1000
|
+
export function registerCredRoute(server, provider) {
|
|
1001
|
+
const ws = routeServer(server);
|
|
1002
|
+
if (ws === undefined)
|
|
1003
|
+
return undefined;
|
|
1004
|
+
return ws.register({
|
|
1005
|
+
kind: 'exact',
|
|
1006
|
+
path: CRED_ROUTE,
|
|
1007
|
+
handler: (rawReq, rawRes) => {
|
|
1008
|
+
void (async () => {
|
|
1009
|
+
const req = rawReq;
|
|
1010
|
+
const res = rawRes;
|
|
1011
|
+
try {
|
|
1012
|
+
if (req.method === 'GET' || req.method === 'HEAD') {
|
|
1013
|
+
const cur = provider.read();
|
|
1014
|
+
json(res, 200, { ok: true, rungs: credRungsOf(cur.doc, cur.exempt), exempt: cur.exempt });
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
if (req.method !== 'POST') {
|
|
1018
|
+
json(res, 405, { ok: false, error: 'method not allowed' });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const body = await readBody(req);
|
|
1022
|
+
const family = String(body.family ?? '');
|
|
1023
|
+
const rung = String(body.rung ?? '');
|
|
1024
|
+
if (!CRED_FAMILY_IDS.includes(family)) {
|
|
1025
|
+
json(res, 400, { ok: false, error: `unknown credential family: ${family}` });
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
1028
|
+
const rungs = ['deny', 'ask', 'readonly', 'readwrite'];
|
|
1029
|
+
if (!rungs.includes(rung)) {
|
|
1030
|
+
json(res, 400, { ok: false, error: `unknown rung: ${rung}` });
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const cur = provider.read();
|
|
1034
|
+
const next = setCredFamilyAction(cur.doc, cur.exempt, family, rung);
|
|
1035
|
+
console.log(`[dsh-perm-gate] cred POST apply family=${family} rung=${rung}`);
|
|
1036
|
+
await provider.persist(next.doc, next.exempt);
|
|
1037
|
+
console.log(`[dsh-perm-gate] cred POST applied (exempt: ${next.exempt.join(',') || '-'})`);
|
|
1038
|
+
json(res, 200, { ok: true, rungs: credRungsOf(next.doc, next.exempt), exempt: next.exempt });
|
|
1039
|
+
}
|
|
1040
|
+
catch (e) {
|
|
1041
|
+
console.warn('[dsh-perm-gate] cred POST failed:', String(e?.message ?? e));
|
|
1042
|
+
json(res, 400, { ok: false, error: String(e?.message ?? e) });
|
|
1043
|
+
}
|
|
1044
|
+
})();
|
|
1045
|
+
},
|
|
1046
|
+
});
|
|
1047
|
+
}
|
package/lib/index.js
CHANGED
|
@@ -3,9 +3,10 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { SandboxController } from './appcontainer/index.js';
|
|
4
4
|
import { Config, ensureDataDir, readRulesFromSettings, readVolatileValue, resolveVolatileConfig, resolveConfig, resolveDshHome, resolveDataDir, resolveGatePresets, resolvePermissiveStrategies, resolveRulesFile } from './config.js';
|
|
5
5
|
import { appendAllowToSettings, readRulesFileDoc, replaceAllowInSettings } from './allowlist.js';
|
|
6
|
-
import { mergePresetIntoRaw } from './preset-rules.js';
|
|
6
|
+
import { mergePresetIntoRaw, PRESET_TABLE_VERSION } from './preset-rules.js';
|
|
7
|
+
import { CRED_FAMILY_IDS } from './cred-families.js';
|
|
7
8
|
import { runDryRun } from './dry-run.js';
|
|
8
|
-
import { registerDryRunRoute, registerEventsRoute, registerHealthRoute, registerLearningRoute, registerNetworkRoute, registerReceiverRoute, registerReviewRoutes, registerRulesRoute } from './events.js';
|
|
9
|
+
import { registerDryRunRoute, registerEventsRoute, registerHealthRoute, registerLearningRoute, registerNetworkRoute, registerReceiverRoute, registerReviewRoutes, registerRulesRoute, registerCredRoute } from './events.js';
|
|
9
10
|
// T12 设置数据桥(describe/mutate;client BridgeDocHandle 消费)
|
|
10
11
|
import { registerSettingsBridgeRoutes } from './bridge.js';
|
|
11
12
|
import { buildReceiverInfo } from './receiver-info.js';
|
|
@@ -256,10 +257,12 @@ export function apply(ctx, config = {}) {
|
|
|
256
257
|
// 写失败不阻断启动也不置幂等位 ⇒ 下次激活重试。
|
|
257
258
|
const seedPresetDenyTable = (svc) => {
|
|
258
259
|
void (async () => {
|
|
259
|
-
|
|
260
|
+
// 水位线代替布尔位:首装(未播)全量注入;升版后只补新增 id(merge 按 tag 幂等,
|
|
261
|
+
// 用户撤销过的条目永不复活),补完把 shipped 版本写回。
|
|
262
|
+
if (current().presetRulesSeeded === true && Number(current().presetRulesVersion ?? 0) >= PRESET_TABLE_VERSION)
|
|
260
263
|
return;
|
|
261
264
|
const merged = mergePresetIntoRaw(readVolatileValue(config.rules));
|
|
262
|
-
await svc.update(ENTRY_ID, { rules: merged.doc, presetRulesSeeded: true });
|
|
265
|
+
await svc.update(ENTRY_ID, { rules: merged.doc, presetRulesSeeded: true, presetRulesVersion: PRESET_TABLE_VERSION });
|
|
263
266
|
console.log(`[dsh-perm-gate] preset deny table seeded via settings (+${merged.added} rules)`);
|
|
264
267
|
})().catch((e) => {
|
|
265
268
|
console.warn('[dsh-perm-gate] preset deny table 播种失败(不阻断启动,下次激活重试):', e);
|
|
@@ -492,6 +495,17 @@ export function apply(ctx, config = {}) {
|
|
|
492
495
|
// exists, and each implementation owns one preset, so presets never cross.
|
|
493
496
|
readSafenetPass: () => current().safenetPass ?? true,
|
|
494
497
|
readSafenetAsk: () => current().safenetAsk ?? false,
|
|
498
|
+
// Protected-branch push floor opt-out, read live so the settings-card toggle takes effect
|
|
499
|
+
// on the next tool call. Default false keeps the floor; true steps aside to the seeded
|
|
500
|
+
// git.push-force ask (and exempts the flat push keywords). See runtime.ts liveAllowProtectedBranchPush.
|
|
501
|
+
readAllowProtectedBranchPush: () => current().allowProtectedBranchPush ?? false,
|
|
502
|
+
// Credential-family ladder, readwrite rung: the exempted family ids, filtered to the
|
|
503
|
+
// roster (a typo in the stored list must not read as "exempt everything guessed").
|
|
504
|
+
readCredWriteExemptFamilies: () => {
|
|
505
|
+
const c = current();
|
|
506
|
+
const raw = Array.isArray(c.credWriteExemptFamilies) ? c.credWriteExemptFamilies : [];
|
|
507
|
+
return raw.filter((f) => typeof f === 'string' && CRED_FAMILY_IDS.includes(f));
|
|
508
|
+
},
|
|
495
509
|
// Degraded review: when no reviewer can grade an ask (none configured, or the
|
|
496
510
|
// endpoint gave up), the deterministic nets decide instead of a popup nobody
|
|
497
511
|
// answers — a cc-safety-net-class gate that blocks named danger and runs the rest.
|
|
@@ -783,6 +797,27 @@ export function apply(ctx, config = {}) {
|
|
|
783
797
|
});
|
|
784
798
|
if (offRules !== undefined)
|
|
785
799
|
ctx.effect(() => () => { offRules(); }, 'dsh-perm-gate: rules route');
|
|
800
|
+
// cred-family ladder: GET renders the per-family rungs, POST applies one rung
|
|
801
|
+
// (rules row + credWriteExemptFamilies travel together — one update, no half-state).
|
|
802
|
+
const offCred = registerCredRoute(webServer, {
|
|
803
|
+
read: () => {
|
|
804
|
+
const c = current();
|
|
805
|
+
return {
|
|
806
|
+
doc: readRulesDocument() ?? readVolatileValue(config.rules),
|
|
807
|
+
exempt: Array.isArray(c.credWriteExemptFamilies)
|
|
808
|
+
? c.credWriteExemptFamilies.filter((f) => typeof f === 'string')
|
|
809
|
+
: [],
|
|
810
|
+
};
|
|
811
|
+
},
|
|
812
|
+
persist: async (doc, exempt) => {
|
|
813
|
+
const svc = settingsRef.svc;
|
|
814
|
+
if (svc === undefined)
|
|
815
|
+
throw new Error('settings service unavailable');
|
|
816
|
+
await svc.update(ENTRY_ID, { rules: doc, credWriteExemptFamilies: [...exempt] });
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
if (offCred !== undefined)
|
|
820
|
+
ctx.effect(() => () => { offCred(); }, 'dsh-perm-gate: cred route');
|
|
786
821
|
// Receiver projection for the settings card (provider/model catalog is
|
|
787
822
|
// potentially slow to enumerate — cached briefly).
|
|
788
823
|
let receiverCache = null;
|
package/lib/preset-rules.d.ts
CHANGED
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
* table from `presetEntriesForView()` without pulling in the node filesystem.
|
|
29
29
|
*/
|
|
30
30
|
import { type PermissionsDoc } from './rule.js';
|
|
31
|
+
import { type CredFamily } from './cred-families.js';
|
|
32
|
+
/**
|
|
33
|
+
* Shipped version of the preset table. Activation backfills (idempotent, revocation-
|
|
34
|
+
* preserving) whenever the stored `presetRulesVersion` lags this number — bump it
|
|
35
|
+
* whenever `PRESET_RULES_YAML` gains entries that existing installs must receive
|
|
36
|
+
* (v1 added the seven `cred.*` family rows).
|
|
37
|
+
*/
|
|
38
|
+
export declare const PRESET_TABLE_VERSION = 1;
|
|
31
39
|
/** Tag prefix carried by every preset entry's reason; locates an entry for revocation. */
|
|
32
40
|
export declare const PRESET_RULE_MARKER = "[deny-rule:";
|
|
33
41
|
/** Extract the `[deny-rule:<id>]` tag from one reason string (undefined when untagged). */
|
|
@@ -40,7 +48,7 @@ export declare function presetRuleId(reason: unknown): string | undefined;
|
|
|
40
48
|
* the joined pipeline — see evaluate.ts), token co-occurrence NEVER through
|
|
41
49
|
* args (args is any-token OR).
|
|
42
50
|
*/
|
|
43
|
-
export declare const PRESET_RULES_YAML = "defaultAction: ask\n\ndeny:\n # \u2500\u2500 Git \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n - command: [git]\n argv: { pipeline: [\"git reset --hard*\", \"git reset *--hard*\"] }\n reason: \"[deny-rule:git.reset-hard] Hard reset discards uncommitted changes. Use git reset --soft/--mixed, or git stash first.\"\n - command: [git]\n argv: { pipeline: [\"git reset --merge*\", \"git reset --abort*\"] }\n reason: \"[deny-rule:git.reset-merge] Merge reset discards in-progress merge work. Stash first, then reset --soft/--mixed.\"\n - command: [git]\n argv: { pipeline: [\"git clean *-f*\", \"git clean *--force*\"] }\n reason: \"[deny-rule:git.clean-force] git clean permanently deletes untracked files. Dry-run first: git clean -nd.\"\n - command: [git]\n argv: { pipeline: [\"git push origin *--delete*\", \"git push * --delete *\", \"git push --delete*\"] }\n reason: \"[deny-rule:git.push-delete] Deleting remote refs is visible to collaborators and needs explicit consent.\"\n - command: [git]\n argv: { pipeline: [\"git push --mirror*\", \"git push * --mirror\"] }\n reason: \"[deny-rule:git.push-mirror] Mirror push force-updates and deletes remote refs wholesale.\"\n - command: [git]\n argv: { pipeline: [\"git branch -D*\", \"git branch * -D\", \"git branch *--force-delete*\"] }\n reason: \"[deny-rule:git.branch-force-delete] git branch -D drops unmerged commits. Prefer merged-safe git branch -d.\"\n - command: [git]\n argv: { pipeline: [\"git stash drop*\"] }\n reason: \"[deny-rule:git.stash-drop] Dropping a stash entry is unrecoverable after GC. Verify with git stash list.\"\n - command: [git]\n argv: { pipeline: [\"git stash clear*\"] }\n reason: \"[deny-rule:git.stash-clear] Clearing all stashes destroys every saved worktree state.\"\n - command: [git]\n argv: { pipeline: [\"git tag -d*\", \"git tag --delete*\"] }\n reason: \"[deny-rule:git.tag-delete] Tag deletion removes release anchors.\"\n - command: [git]\n argv: { pipeline: [\"git reflog delete*\", \"git reflog expire*\"] }\n reason: \"[deny-rule:git.reflog-delete] Reflog deletion removes the last recovery path for lost commits.\"\n - command: [git]\n argv: { pipeline: [\"git rebase --abort*\"] }\n reason: \"[deny-rule:git.rebase-abort] Aborting a rebase discards resolution work. Prefer --continue/--skip.\"\n - command: [git]\n argv: { pipeline: [\"git merge --abort*\"] }\n reason: \"[deny-rule:git.merge-abort] Aborting a merge discards conflict resolution.\"\n - command: [git]\n argv: { pipeline: [\"git rm -rf *\", \"git rm *-rf *\", \"git rm * -rf\", \"git rm -fr *\", \"git rm --force *\", \"git rm * --force\", \"git rm -f *\", \"git rm * -f\"] }\n reason: \"[deny-rule:git.rm-force] git rm -f deletes working-tree files even when modified. Stash them first.\"\n - command: [git]\n argv: { pipeline: [\"git checkout --force *\", \"git checkout * --force\", \"git checkout -f *\", \"git checkout * -f\", \"git checkout -f\", \"git checkout * --force *\"] }\n reason: \"[deny-rule:git.checkout-force] Forced checkout overwrites local changes. git stash, then checkout.\"\n - command: [git]\n argv: { pipeline: [\"git checkout -- *\"] }\n reason: \"[deny-rule:git.checkout-double-dash] Path restore from checkout discards local edits. Use git stash push -- <path>.\"\n - command: [git]\n argv: { pipeline: [\"git checkout * -- *\"] }\n reason: \"[deny-rule:git.checkout-ref-path] Checking out a ref with a path overwrites working-tree files. Stash the paths first.\"\n - command: [git]\n argv: { pipeline: [\"git checkout *--pathspec-from-file*\"] }\n reason: \"[deny-rule:git.checkout-pathspec-from-file] Bulk path restore from a file can silently overwrite edits. List the pathspec, stash, then restore.\"\n - command: [git]\n argv: { pipeline: [\"git switch *--discard-changes*\"] }\n reason: \"[deny-rule:git.switch-discard-changes] --discard-changes throws away edits. git stash first.\"\n - command: [git]\n argv: { pipeline: [\"git switch --force *\", \"git switch * --force\", \"git switch -f *\", \"git switch * -f\"] }\n reason: \"[deny-rule:git.switch-force] Forced switch overwrites local changes. git stash first.\"\n - command: [git]\n # git restore <bare path> (the plain unstaged discard) is NOT ported here:\n # any glob broad enough to catch it also catches the harmless\n # git restore --staged <path> (G6) \u2014 only the explicit discard forms are\n # denied, the bare-path case stays in the prompt layer.\n argv: { pipeline: [\"git restore --worktree *\", \"git restore * --worktree *\", \"git restore * --worktree\", \"git restore -W *\"] }\n reason: \"[deny-rule:git.restore-worktree] Worktree restore discards local edits. git stash push -- <path> first.\"\n - command: [git]\n argv: { pipeline: [\"git worktree remove --force *\", \"git worktree remove * --force\", \"git worktree remove -f *\", \"git worktree remove * -f\"] }\n reason: \"[deny-rule:git.worktree-remove-force] Forced worktree removal discards uncommitted work in that tree.\"\n - command: [git]\n args: [\"alias.*\"]\n reason: \"[deny-rule:git.alias-config] Command-line Git aliases run arbitrary shell and cannot be analyzed.\"\n # params matches the RAW command string: the env-assignment word lands as the\n # simple command's lowercased word, so a command:[git] dimension would never\n # see this form \u2014 match on params alone.\n - params: { command: [\"*GIT_SSH_COMMAND=*\", \"*git_ssh_command=*\", \"*GIT_SSH=*\", \"*git_ssh=*\"] }\n reason: \"[deny-rule:git.ssh-env] Git network ops with an SSH env override execute unanalyzable code.\"\n\n # \u2500\u2500 Filesystem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n - command: [\"rm#recursive\", \"rm#force\"]\n args: [\".git\", \".git/*\", \"*/.git\"]\n reason: \"[deny-rule:rm.git-metadata] Removing Git metadata destroys history and the gate's audit anchors.\"\n - command: [find]\n argv: { pipeline: [\"find .git*\"] }\n reason: \"[deny-rule:find.delete-git-metadata] Selecting .git for deletion destroys repository state.\"\n - command: [find]\n argv: { pipeline: [\"find * -delete\"] }\n reason: \"[deny-rule:find.delete] find -delete removes everything selected. Enumerate with plain find first.\"\n - command: [find]\n argv: { pipeline: [\"find *-exec rm*\", \"find *-delete*\"] }\n reason: \"[deny-rule:find.exec-rm-recursive-force] find -exec rm removes the whole tree per match. Dry-run plain find, delete explicit paths.\"\n - command: [dd]\n argv: { pipeline: [\"dd * of=/dev/sd*\", \"dd * of=/dev/nvme*\", \"dd * of=/dev/mmcblk*\"] }\n reason: \"[deny-rule:dd.device-write] dd writing to a block device destroys the disk.\"\n - command: [mkfs, mke2fs, newfs, \"mkfs.*\"]\n reason: \"[deny-rule:mkfs.device] Formatting a device erases it.\"\n - command: [diskutil, wipefs, sgdisk, parted]\n reason: \"[deny-rule:disk.erase] Disk erase/partition/wipe operations destroy data.\"\n - command: [shred]\n reason: \"[deny-rule:shred.target] shred irreversibly destroys files (even --help/--version reach it).\"\n - command: [cat, head, tail, dd]\n args: [\"*/dev/sd*\", \"*/dev/nvme*\", \"*/dev/disk*\", \"*/dev/mmcblk*\"]\n reason: \"[deny-rule:redirect.block-device] Redirect target resolving to a disk device path.\"\n\n # \u2500\u2500 PowerShell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n # Command words are lowercased by the decomposer \u2014 command dimensions must\n # carry the lowercased verb (Remove-Item \u2192 remove-item).\n - command: [\"remove-item#recursive\"]\n args: [\".git\", \".git/*\", \"*/.git\"]\n reason: \"[deny-rule:powershell.remove-item-git-metadata] Never target .git.\"\n - command: [powershell, pwsh]\n argv: { pipeline: [\"pwsh *Remove-Item*\", \"powershell *Remove-Item*\", \"pwsh *remove-item*\", \"powershell *remove-item*\"] }\n reason: \"[deny-rule:powershell.nested-recursive-delete-unread] A delete handed to pwsh/powershell in a non-literal form cannot be verified before it runs.\"\n - command: [\"remove-item#recursive\"]\n args: [\".\", \"./\"]\n reason: \"[deny-rule:powershell.remove-item-recursive-force-cwd-self] Deleting the current directory removes the project.\"\n - command: [\"remove-item#recursive\"]\n args: [\"../*\", \"..\\\\*\"]\n reason: \"[deny-rule:powershell.remove-item-recursive-force-outside-cwd] Deleting above the cwd escapes task scope.\"\n\n # \u2500\u2500 Command Prompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n # (The cc escaped-quote-collapse rule cannot be expressed faithfully with\n # these globs \u2014 every cmd /c recursion is nested behind a second parser,\n # so its true target is undecidable: it moves to the ask section instead.)\n - command: [cmd, rmdir, del]\n args: [\".git\", \".git/*\", \"*/.git\"]\n reason: \"[deny-rule:cmd.recursive-delete-git-metadata] Never target .git.\"\n - command: [rmdir, del]\n args: [\".\", \"./\"]\n reason: \"[deny-rule:cmd.recursive-delete-cwd-self] Deleting the current directory removes the project.\"\n - command: [rmdir, del]\n args: [\"../*\", \"..\\\\*\"]\n reason: \"[deny-rule:cmd.recursive-delete-outside-cwd] Delete above cwd escapes task scope.\"\n\n # \u2500\u2500 Secret targets (write/delete deny; reads are the ask section below) \u2500\u2500\n - command: [\"rm#recursive\", \"remove-item#recursive\", rmdir, del]\n args: [\"*/.ssh\", \"*/.aws\", \"*/.gnupg\", \".ssh\", \".aws\"]\n reason: \"[deny-rule:secret.delete-credential-path] Deleting credential stores destroys keys you cannot regenerate.\"\n - command: [echo, \"set-content\", \"out-file\"]\n args: [\".env\", \".env.*\"]\n reason: \"[deny-rule:secret.write-overwrite-env] Overwriting .env destroys existing secrets. Edit specific lines.\"\n - command: [\"rm#recursive\", \"remove-item#recursive\", rmdir, del]\n args: [\"*/.git-credentials\", \".git-credentials\"]\n reason: \"[deny-rule:secret.delete-git-credentials] Deleting the git credential store breaks push/pull auth.\"\n\nask:\n # Force push: ask, not deny (revoked from cc's deny ruling 2026-10-13).\n - command: [git]\n argv: { pipeline: [\"git push *--force*\", \"git push *-f*\"] }\n reason: \"[deny-rule:git.push-force] Force push rewrites shared history. Prefer git push --force-with-lease.\"\n\n # cmd /c recursion hides the real target behind a second parser (and quoting):\n # undecidable \u2192 ask, where cc denied only the drive-root-collapse variant.\n - command: [cmd]\n argv: { pipeline: [\"cmd /c *rmdir /s*\", \"cmd /c *del /s*\"] }\n reason: \"[deny-rule:cmd.recursive-delete-nested] cmd /c recursion nests a second parser; the real target cannot be resolved. Run rmdir/del directly so the gate can see the target.\"\n\n # Dynamic targets: this gate's ruling is \"undecidable \u2192 ask\", not fail-closed deny.\n - command: [\"rm#recursive\", \"remove-item#recursive\"]\n args: [\"$*\"]\n reason: \"[deny-rule:rm.recursive-force-dynamic-target] Recursive delete of a substitution-assembled target \u2014 verify the variable yourself.\"\n # The wrapper word xargs is stripped by the decomposer, so pipeline patterns\n # never see it \u2014 match the raw command string through the params dimension.\n - params: { command: [\"*xargs rm*\", \"*xargs del*\"] }\n reason: \"[deny-rule:xargs.rm-recursive-force-dynamic] xargs-fed removals have unverifiable target sets.\"\n - command: [parallel]\n argv: { pipeline: [\"parallel *rm*\", \"*| parallel*\"] }\n reason: \"[deny-rule:parallel.rm-recursive-force-dynamic] parallel-fed rm has unresolvable targets.\"\n - command: [awk]\n args: [\"*system(*\"]\n reason: \"[deny-rule:awk.system-dynamic] awk program text containing system() cannot be analyzed.\"\n # NOTE: cc's git checkout <ref> <path> ambiguity (G5) is NOT ported \u2014 a glob\n # cannot tell ref candidates from paths, and a broad pattern would prompt on\n # every git checkout -b new-branch. Documented residual gap.\n\n # Credential reads: ask (A2 ruling kept; egress combos stay in the prompt layer).\n - command: [cat, head, tail, type, \"get-content\", ls, dir, \"get-childitem\", \"select-string\"]\n args: [\".env\", \".env.*\", \"*.env\", \".npmrc\", \".pypirc\", \".netrc\", \".git-credentials\", \"id_rsa\", \"id_ed25519\", \"id_ecdsa\"]\n reason: \"[deny-rule:secret.read-credential-file] Reading a credential file requires confirmation. Echo only the keys you need via a filtered command.\"\n - command: [cat, head, tail, type, \"get-content\", ls, dir, \"get-childitem\"]\n args: [\"*/.ssh\", \"*/.ssh/*\", \"*/.aws\", \"*/.aws/*\", \"*/.gnupg\", \"*/.gnupg/*\", \"*/.config/gcloud/*\", \"*/.claude/*\", \"*/.codex/*\"]\n reason: \"[deny-rule:secret.read-home-credential-dirs] Browsing ssh/cloud/coding-CLI credential stores requires confirmation.\"\n\n # Self-protection: editing the gate's own rule sources needs confirmation \u2014\n # revocation must be a human act, not something an agent can slide through.\n # (Settings-namespace edits go through the host UI, not a tool call, and are\n # out of reach of this entry; documented gap.)\n - args: [\"*rules.yml\", \"*rules.yaml\", \"*deny-defaults*\"]\n reason: \"[deny-rule:gate.self-protection] This call touches the permission gate's own rule sources; changes here alter what the gate enforces.\"\n";
|
|
51
|
+
export declare const PRESET_RULES_YAML = "defaultAction: ask\n\ndeny:\n # \u2500\u2500 Git \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n - command: [git]\n argv: { pipeline: [\"git reset --hard*\", \"git reset *--hard*\"] }\n reason: \"[deny-rule:git.reset-hard] Hard reset discards uncommitted changes. Use git reset --soft/--mixed, or git stash first.\"\n - command: [git]\n argv: { pipeline: [\"git reset --merge*\", \"git reset --abort*\"] }\n reason: \"[deny-rule:git.reset-merge] Merge reset discards in-progress merge work. Stash first, then reset --soft/--mixed.\"\n - command: [git]\n argv: { pipeline: [\"git clean *-f*\", \"git clean *--force*\"] }\n reason: \"[deny-rule:git.clean-force] git clean permanently deletes untracked files. Dry-run first: git clean -nd.\"\n - command: [git]\n argv: { pipeline: [\"git push origin *--delete*\", \"git push * --delete *\", \"git push --delete*\"] }\n reason: \"[deny-rule:git.push-delete] Deleting remote refs is visible to collaborators and needs explicit consent.\"\n - command: [git]\n argv: { pipeline: [\"git push --mirror*\", \"git push * --mirror\"] }\n reason: \"[deny-rule:git.push-mirror] Mirror push force-updates and deletes remote refs wholesale.\"\n - command: [git]\n argv: { pipeline: [\"git branch -D*\", \"git branch * -D\", \"git branch *--force-delete*\"] }\n reason: \"[deny-rule:git.branch-force-delete] git branch -D drops unmerged commits. Prefer merged-safe git branch -d.\"\n - command: [git]\n argv: { pipeline: [\"git stash drop*\"] }\n reason: \"[deny-rule:git.stash-drop] Dropping a stash entry is unrecoverable after GC. Verify with git stash list.\"\n - command: [git]\n argv: { pipeline: [\"git stash clear*\"] }\n reason: \"[deny-rule:git.stash-clear] Clearing all stashes destroys every saved worktree state.\"\n - command: [git]\n argv: { pipeline: [\"git tag -d*\", \"git tag --delete*\"] }\n reason: \"[deny-rule:git.tag-delete] Tag deletion removes release anchors.\"\n - command: [git]\n argv: { pipeline: [\"git reflog delete*\", \"git reflog expire*\"] }\n reason: \"[deny-rule:git.reflog-delete] Reflog deletion removes the last recovery path for lost commits.\"\n - command: [git]\n argv: { pipeline: [\"git rebase --abort*\"] }\n reason: \"[deny-rule:git.rebase-abort] Aborting a rebase discards resolution work. Prefer --continue/--skip.\"\n - command: [git]\n argv: { pipeline: [\"git merge --abort*\"] }\n reason: \"[deny-rule:git.merge-abort] Aborting a merge discards conflict resolution.\"\n - command: [git]\n argv: { pipeline: [\"git rm -rf *\", \"git rm *-rf *\", \"git rm * -rf\", \"git rm -fr *\", \"git rm --force *\", \"git rm * --force\", \"git rm -f *\", \"git rm * -f\"] }\n reason: \"[deny-rule:git.rm-force] git rm -f deletes working-tree files even when modified. Stash them first.\"\n - command: [git]\n argv: { pipeline: [\"git checkout --force *\", \"git checkout * --force\", \"git checkout -f *\", \"git checkout * -f\", \"git checkout -f\", \"git checkout * --force *\"] }\n reason: \"[deny-rule:git.checkout-force] Forced checkout overwrites local changes. git stash, then checkout.\"\n - command: [git]\n argv: { pipeline: [\"git checkout -- *\"] }\n reason: \"[deny-rule:git.checkout-double-dash] Path restore from checkout discards local edits. Use git stash push -- <path>.\"\n - command: [git]\n argv: { pipeline: [\"git checkout * -- *\"] }\n reason: \"[deny-rule:git.checkout-ref-path] Checking out a ref with a path overwrites working-tree files. Stash the paths first.\"\n - command: [git]\n argv: { pipeline: [\"git checkout *--pathspec-from-file*\"] }\n reason: \"[deny-rule:git.checkout-pathspec-from-file] Bulk path restore from a file can silently overwrite edits. List the pathspec, stash, then restore.\"\n - command: [git]\n argv: { pipeline: [\"git switch *--discard-changes*\"] }\n reason: \"[deny-rule:git.switch-discard-changes] --discard-changes throws away edits. git stash first.\"\n - command: [git]\n argv: { pipeline: [\"git switch --force *\", \"git switch * --force\", \"git switch -f *\", \"git switch * -f\"] }\n reason: \"[deny-rule:git.switch-force] Forced switch overwrites local changes. git stash first.\"\n - command: [git]\n # git restore <bare path> (the plain unstaged discard) is NOT ported here:\n # any glob broad enough to catch it also catches the harmless\n # git restore --staged <path> (G6) \u2014 only the explicit discard forms are\n # denied, the bare-path case stays in the prompt layer.\n argv: { pipeline: [\"git restore --worktree *\", \"git restore * --worktree *\", \"git restore * --worktree\", \"git restore -W *\"] }\n reason: \"[deny-rule:git.restore-worktree] Worktree restore discards local edits. git stash push -- <path> first.\"\n - command: [git]\n argv: { pipeline: [\"git worktree remove --force *\", \"git worktree remove * --force\", \"git worktree remove -f *\", \"git worktree remove * -f\"] }\n reason: \"[deny-rule:git.worktree-remove-force] Forced worktree removal discards uncommitted work in that tree.\"\n - command: [git]\n args: [\"alias.*\"]\n reason: \"[deny-rule:git.alias-config] Command-line Git aliases run arbitrary shell and cannot be analyzed.\"\n # params matches the RAW command string: the env-assignment word lands as the\n # simple command's lowercased word, so a command:[git] dimension would never\n # see this form \u2014 match on params alone.\n - params: { command: [\"*GIT_SSH_COMMAND=*\", \"*git_ssh_command=*\", \"*GIT_SSH=*\", \"*git_ssh=*\"] }\n reason: \"[deny-rule:git.ssh-env] Git network ops with an SSH env override execute unanalyzable code.\"\n\n # \u2500\u2500 Filesystem \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n - command: [\"rm#recursive\", \"rm#force\"]\n args: [\".git\", \".git/*\", \"*/.git\"]\n reason: \"[deny-rule:rm.git-metadata] Removing Git metadata destroys history and the gate's audit anchors.\"\n - command: [find]\n argv: { pipeline: [\"find .git*\"] }\n reason: \"[deny-rule:find.delete-git-metadata] Selecting .git for deletion destroys repository state.\"\n - command: [find]\n argv: { pipeline: [\"find * -delete\"] }\n reason: \"[deny-rule:find.delete] find -delete removes everything selected. Enumerate with plain find first.\"\n - command: [find]\n argv: { pipeline: [\"find *-exec rm*\", \"find *-delete*\"] }\n reason: \"[deny-rule:find.exec-rm-recursive-force] find -exec rm removes the whole tree per match. Dry-run plain find, delete explicit paths.\"\n - command: [rsync]\n argv: { pipeline: [\"rsync * --delete*\", \"rsync --delete*\"] }\n reason: \"[deny-rule:rsync.delete] rsync --delete erases destination files absent from the source. Dry-run with rsync -n --delete first.\"\n - command: [dd]\n argv: { pipeline: [\"dd * of=/dev/sd*\", \"dd * of=/dev/nvme*\", \"dd * of=/dev/mmcblk*\"] }\n reason: \"[deny-rule:dd.device-write] dd writing to a block device destroys the disk.\"\n - command: [mkfs, mke2fs, newfs, \"mkfs.*\"]\n reason: \"[deny-rule:mkfs.device] Formatting a device erases it.\"\n - command: [diskutil, wipefs, sgdisk, parted]\n reason: \"[deny-rule:disk.erase] Disk erase/partition/wipe operations destroy data.\"\n - command: [shred]\n reason: \"[deny-rule:shred.target] shred irreversibly destroys files (even --help/--version reach it).\"\n - command: [cat, head, tail, dd]\n args: [\"*/dev/sd*\", \"*/dev/nvme*\", \"*/dev/disk*\", \"*/dev/mmcblk*\"]\n reason: \"[deny-rule:redirect.block-device] Redirect target resolving to a disk device path.\"\n\n # \u2500\u2500 PowerShell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n # Command words are lowercased by the decomposer \u2014 command dimensions must\n # carry the lowercased verb (Remove-Item \u2192 remove-item).\n - command: [\"remove-item#recursive\"]\n args: [\".git\", \".git/*\", \"*/.git\"]\n reason: \"[deny-rule:powershell.remove-item-git-metadata] Never target .git.\"\n - command: [powershell, pwsh]\n argv: { pipeline: [\"pwsh *Remove-Item*\", \"powershell *Remove-Item*\", \"pwsh *remove-item*\", \"powershell *remove-item*\"] }\n reason: \"[deny-rule:powershell.nested-recursive-delete-unread] A delete handed to pwsh/powershell in a non-literal form cannot be verified before it runs.\"\n - command: [\"remove-item#recursive\"]\n args: [\".\", \"./\"]\n reason: \"[deny-rule:powershell.remove-item-recursive-force-cwd-self] Deleting the current directory removes the project.\"\n - command: [\"remove-item#recursive\"]\n args: [\"../*\", \"..\\\\*\"]\n reason: \"[deny-rule:powershell.remove-item-recursive-force-outside-cwd] Deleting above the cwd escapes task scope.\"\n\n # \u2500\u2500 Command Prompt \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n # (The cc escaped-quote-collapse rule cannot be expressed faithfully with\n # these globs \u2014 every cmd /c recursion is nested behind a second parser,\n # so its true target is undecidable: it moves to the ask section instead.)\n - command: [cmd, rmdir, del]\n args: [\".git\", \".git/*\", \"*/.git\"]\n reason: \"[deny-rule:cmd.recursive-delete-git-metadata] Never target .git.\"\n - command: [rmdir, del]\n args: [\".\", \"./\"]\n reason: \"[deny-rule:cmd.recursive-delete-cwd-self] Deleting the current directory removes the project.\"\n - command: [rmdir, del]\n args: [\"../*\", \"..\\\\*\"]\n reason: \"[deny-rule:cmd.recursive-delete-outside-cwd] Delete above cwd escapes task scope.\"\n\n # \u2500\u2500 Secret targets (write/delete deny; reads are the ask section below) \u2500\u2500\n - command: [\"rm#recursive\", \"remove-item#recursive\", rmdir, del]\n args: [\"*/.ssh\", \"*/.aws\", \"*/.gnupg\", \".ssh\", \".aws\"]\n reason: \"[deny-rule:secret.delete-credential-path] Deleting credential stores destroys keys you cannot regenerate.\"\n - command: [echo, \"set-content\", \"out-file\"]\n args: [\".env\", \".env.*\"]\n reason: \"[deny-rule:secret.write-overwrite-env] Overwriting .env destroys existing secrets. Edit specific lines.\"\n - command: [\"rm#recursive\", \"remove-item#recursive\", rmdir, del]\n args: [\"*/.git-credentials\", \".git-credentials\"]\n reason: \"[deny-rule:secret.delete-git-credentials] Deleting the git credential store breaks push/pull auth.\"\n\nask:\n # Force push: ask, not deny (revoked from cc's deny ruling 2026-10-13).\n - command: [git]\n argv: { pipeline: [\"git push *--force*\", \"git push *-f*\"] }\n reason: \"[deny-rule:git.push-force] Force push rewrites shared history. Prefer git push --force-with-lease.\"\n\n # cmd /c recursion hides the real target behind a second parser (and quoting):\n # undecidable \u2192 ask, where cc denied only the drive-root-collapse variant.\n - command: [cmd]\n argv: { pipeline: [\"cmd /c *rmdir /s*\", \"cmd /c *del /s*\"] }\n reason: \"[deny-rule:cmd.recursive-delete-nested] cmd /c recursion nests a second parser; the real target cannot be resolved. Run rmdir/del directly so the gate can see the target.\"\n\n # Dynamic targets: this gate's ruling is \"undecidable \u2192 ask\", not fail-closed deny.\n - command: [\"rm#recursive\", \"remove-item#recursive\"]\n args: [\"$*\"]\n reason: \"[deny-rule:rm.recursive-force-dynamic-target] Recursive delete of a substitution-assembled target \u2014 verify the variable yourself.\"\n # The wrapper word xargs is stripped by the decomposer, so pipeline patterns\n # never see it \u2014 match the raw command string through the params dimension.\n - params: { command: [\"*xargs rm*\", \"*xargs del*\"] }\n reason: \"[deny-rule:xargs.rm-recursive-force-dynamic] xargs-fed removals have unverifiable target sets.\"\n - command: [parallel]\n argv: { pipeline: [\"parallel *rm*\", \"*| parallel*\"] }\n reason: \"[deny-rule:parallel.rm-recursive-force-dynamic] parallel-fed rm has unresolvable targets.\"\n - command: [awk]\n args: [\"*system(*\"]\n reason: \"[deny-rule:awk.system-dynamic] awk program text containing system() cannot be analyzed.\"\n # NOTE: cc's git checkout <ref> <path> ambiguity (G5) is NOT ported \u2014 a glob\n # cannot tell ref candidates from paths, and a broad pattern would prompt on\n # every git checkout -b new-branch. Documented residual gap.\n\n # Credential reads: ask (A2 ruling kept; egress combos stay in the prompt layer).\n - command: [cat, head, tail, type, \"get-content\", ls, dir, \"get-childitem\", \"select-string\"]\n args: [\".env\", \".env.*\", \"*.env\", \".npmrc\", \".pypirc\", \".netrc\", \".git-credentials\", \"id_rsa\", \"id_ed25519\", \"id_ecdsa\"]\n reason: \"[deny-rule:secret.read-credential-file] Reading a credential file requires confirmation. Echo only the keys you need via a filtered command.\"\n - command: [cat, head, tail, type, \"get-content\", ls, dir, \"get-childitem\"]\n args: [\"*/.ssh\", \"*/.ssh/*\", \"*/.aws\", \"*/.aws/*\", \"*/.gnupg\", \"*/.gnupg/*\", \"*/.config/gcloud/*\", \"*/.claude/*\", \"*/.codex/*\"]\n reason: \"[deny-rule:secret.read-home-credential-dirs] Browsing ssh/cloud/coding-CLI credential stores requires confirmation.\"\n\n # \u2500\u2500 Credential family ladder (read side) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n # One row per family, restricted to the file-READ tools: decide() pre-queries the\n # rule layer for READ calls, so these rows are what a read hits (shell reads stay\n # under the secret.* rows above, which match first in this section). The settings\n # face rewrites each row as the family's rung: deny - moved to the deny section;\n # ask - stays here; readonly/readwrite - enabled:false (readwrite additionally\n # lists the family in credWriteExemptFamilies for the engine's write clause).\n # Custom credentialPaths have no family and are never write-exempt.\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.ssh\", \"*/.ssh/*\", \"*id_rsa\", \"*id_ed25519\", \"*id_ecdsa\", \"*id_dsa\", \"*authorized_keys\"]\n reason: \"[deny-rule:cred.ssh] Reading SSH keys or authorized_keys needs confirmation (family rung: ask).\"\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.aws\", \"*/.aws/*\", \"*/.config/gcloud\", \"*/.config/gcloud/*\", \"*/.azure\", \"*/.azure/*\", \"*credentials.yaml\", \"*credentials.json\"]\n reason: \"[deny-rule:cred.cloud] Reading cloud-provider credential stores needs confirmation (family rung: ask).\"\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.npmrc\", \"*.npmrc\", \"*/.pypirc\", \"*/.netrc\", \"*_netrc\", \"*/pip.conf\"]\n reason: \"[deny-rule:cred.package] Reading package-registry auth files needs confirmation (family rung: ask).\"\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.git-credentials\", \"*.git-credentials\"]\n reason: \"[deny-rule:cred.gitcreds] Reading the git credential store needs confirmation (family rung: ask).\"\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.env\", \"*/.env.*\", \"*.env\"]\n reason: \"[deny-rule:cred.env] Reading project env files needs confirmation (family rung: ask; readwrite also lets edits, npm-login-style).\"\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.claude\", \"*/.claude/*\", \"*/.codex\", \"*/.codex/*\", \"*/.config/gh\", \"*/.config/gh/*\"]\n reason: \"[deny-rule:cred.cli] Reading coding-CLI credential stores needs confirmation (family rung: ask).\"\n - tools: [read, read_image, grep, glob, ls, lsp]\n args: [\"*/.pgpass\", \"*pgpass\", \"*/.my.cnf\"]\n reason: \"[deny-rule:cred.db] Reading database credential files needs confirmation (family rung: ask).\"\n\n # Self-protection: editing the gate's own rule sources needs confirmation \u2014\n # revocation must be a human act, not something an agent can slide through.\n # (Settings-namespace edits go through the host UI, not a tool call, and are\n # out of reach of this entry; documented gap.)\n - args: [\"*rules.yml\", \"*rules.yaml\", \"*deny-defaults*\"]\n reason: \"[deny-rule:gate.self-protection] This call touches the permission gate's own rule sources; changes here alter what the gate enforces.\"\n";
|
|
44
52
|
export declare function parsePresetDoc(): PermissionsDoc;
|
|
45
53
|
/** Every preset id, in table order. Client view + tests use this as the row key. */
|
|
46
54
|
export declare function presetRuleIds(): string[];
|
|
@@ -68,3 +76,27 @@ export declare function ensurePresetRulesFile(rulesFile: string, markerFile?: st
|
|
|
68
76
|
action: 'created' | 'merged' | 'skipped' | 'already-seeded';
|
|
69
77
|
added: number;
|
|
70
78
|
};
|
|
79
|
+
/** One rung of the credential-family ladder (settings face; see the cred block above). */
|
|
80
|
+
export type CredRung = 'deny' | 'ask' | 'readonly' | 'readwrite';
|
|
81
|
+
/**
|
|
82
|
+
* The current rung per family, read out of a RAW rules document plus the write-exemption
|
|
83
|
+
* list (GET side of the settings face; the card renders straight from this map).
|
|
84
|
+
* A missing row reports 'ask' — the shipped default every fresh seed writes.
|
|
85
|
+
*/
|
|
86
|
+
export declare function credRungsOf(rawDoc: unknown, exempt?: readonly string[]): Record<CredFamily, CredRung>;
|
|
87
|
+
/**
|
|
88
|
+
* Rewrite one `cred.<family>` row of a RAW rules document to the chosen rung, and
|
|
89
|
+
* maintain the paired write-exemption list. Both stores are returned together; the
|
|
90
|
+
* caller persists them in one settings update. Idempotent: applying the same rung
|
|
91
|
+
* twice is a no-op. Revocation-preserving elsewhere untouched — this function only
|
|
92
|
+
* ever moves or enables/disables the family's own row.
|
|
93
|
+
*
|
|
94
|
+
* `readonly` and `readwrite` both disable the read row (reads then fall through to
|
|
95
|
+
* the READ auto-allow); they differ only in `exempt`: readwrite additionally lists
|
|
96
|
+
* the family, which steps the engine's credential-store WRITE clause aside for that
|
|
97
|
+
* family's built-in paths — and nothing else.
|
|
98
|
+
*/
|
|
99
|
+
export declare function setCredFamilyAction(rawDoc: unknown, exempt: readonly string[], family: CredFamily, rung: CredRung): {
|
|
100
|
+
doc: Record<string, unknown>;
|
|
101
|
+
exempt: string[];
|
|
102
|
+
};
|