dsh-dlp 0.4.1 → 0.6.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/README.md +25 -11
- package/SECURITY.md +8 -6
- package/cordis.patch.yml +1 -0
- package/lib/approvals.js +177 -0
- package/lib/cli.js +4 -4
- package/lib/config-writes.js +58 -6
- package/lib/detectors.js +22 -0
- package/lib/index.js +15 -8
- package/lib/paths.js +7 -3
- package/lib/policy.js +3 -0
- package/lib/types/approvals.d.ts +100 -0
- package/lib/types/cli.d.ts +1 -1
- package/lib/types/config-writes.d.ts +5 -0
- package/lib/types/index.d.ts +5 -4
- package/lib/types/policy.d.ts +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -20,11 +20,18 @@ built as an out-of-repo plugin.
|
|
|
20
20
|
5. **Neutralises remote markdown images in assistant output** and detects a tool call another
|
|
21
21
|
plugin rewrote after the session log recorded it.
|
|
22
22
|
6. **Asks before the agent writes a file that changes future behaviour** — agent settings and
|
|
23
|
-
hooks, `CLAUDE.md`, `.
|
|
24
|
-
|
|
25
|
-
provider
|
|
26
|
-
|
|
27
|
-
|
|
23
|
+
hooks, `CLAUDE.md`, `.claude/rules/**` and the other agent rules directories, prompt
|
|
24
|
+
templates, `.vscode/tasks.json`, `.mcp.json`, git hooks, CI workflows, shell startup files,
|
|
25
|
+
`pnpm-workspace.yaml` — and before it writes a `*_BASE_URL` that would redirect a provider
|
|
26
|
+
credential.
|
|
27
|
+
7. **Asks before a call switches off its own confirmation** — `non_interactive: true`,
|
|
28
|
+
`approval_mode: auto`, an `apply` whose approval is still pending. Both `ask` tiers are
|
|
29
|
+
prompts rather than controls: they live at `tools/pre-execute` and can be neutralised.
|
|
30
|
+
8. **Writes an audit record for every decision.** A redaction or denial names the rule, its
|
|
31
|
+
version, the offsets and a keyed hash; the three kinds with no matched region to describe —
|
|
32
|
+
an ask, a rewritten call, a neutralised image — carry a rule id, the changed field names or
|
|
33
|
+
the destination hostname instead. Never the secret, never the path or command that matched.
|
|
34
|
+
`dsh-dlp report` reads it back.
|
|
28
35
|
|
|
29
36
|
## What this is not
|
|
30
37
|
|
|
@@ -40,9 +47,12 @@ Three limits worth knowing before you rely on it:
|
|
|
40
47
|
- **Only the guard floor is unconditional.** Every other seam can be neutralised by a listener
|
|
41
48
|
registered ahead of ours. `ctx.tools.guard()` is order-independent only because it has no allow
|
|
42
49
|
arm.
|
|
43
|
-
- **The shell-command arm is advisory pattern-matching.** It
|
|
44
|
-
|
|
45
|
-
`python3 -c`
|
|
50
|
+
- **The shell-command arm is advisory pattern-matching.** It tests the whole command line and
|
|
51
|
+
each of its tokens, so a credential path left *spelled* in the command is caught whatever
|
|
52
|
+
program would open it: `python3 -c "open('~/.ssh/id_rsa')"` is denied. Changing the spelling
|
|
53
|
+
defeats it — one glob character, quote-splitting, `find -exec`, a substitution that assembles
|
|
54
|
+
the path from pieces, a base64 round-trip, each verified. **Do not count this arm as a
|
|
55
|
+
control.**
|
|
46
56
|
- **Detection is pattern-based.** No entropy rule (measured, not assumed: at a false-positive-free
|
|
47
57
|
threshold the miss rate is 100% below 22 characters). Encoded forms pass. A homoglyph defeats
|
|
48
58
|
every rule in this package.
|
|
@@ -83,6 +93,7 @@ load.
|
|
|
83
93
|
resultRedaction: true
|
|
84
94
|
telemetryRedaction: true
|
|
85
95
|
configWriteAsk: true
|
|
96
|
+
approvalSuppressionAsk: true
|
|
86
97
|
```
|
|
87
98
|
|
|
88
99
|
`redactionKeyFile` is created on first mount with 32 random bytes at mode `0600`. Keep it out of
|
|
@@ -103,11 +114,14 @@ switch a pass on. Any downgrade makes the whole file invalid.
|
|
|
103
114
|
```sh
|
|
104
115
|
dsh-dlp report # everything in the audit sink
|
|
105
116
|
dsh-dlp report --since 24h
|
|
106
|
-
dsh-dlp report --
|
|
117
|
+
dsh-dlp report --session <id>
|
|
118
|
+
dsh-dlp report --would-have # everything except the denials
|
|
107
119
|
```
|
|
108
120
|
|
|
109
|
-
|
|
110
|
-
value.
|
|
121
|
+
A redaction or denial record carries a rule id, rule version, span offsets and a keyed hash —
|
|
122
|
+
never the matched value. An ask carries its rule id, a rewritten call the names of the fields
|
|
123
|
+
that changed, and a neutralised remote image the destination hostname in the clear; none of
|
|
124
|
+
those has a matched region to hash.
|
|
111
125
|
|
|
112
126
|
[Audit record format →](https://charlotten7.github.io/dsh-dlp/audit.html)
|
|
113
127
|
|
package/SECURITY.md
CHANGED
|
@@ -4,12 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
| Version | Supported |
|
|
6
6
|
|---|---|
|
|
7
|
-
| 0.
|
|
8
|
-
| < 0.
|
|
7
|
+
| 0.5.x | yes |
|
|
8
|
+
| < 0.5 | no |
|
|
9
9
|
|
|
10
|
-
Only the latest published `0.
|
|
10
|
+
Only the latest published `0.5.x` receives fixes. There is no long-term-support branch while
|
|
11
11
|
the package is pre-1.0: each minor supersedes the one before it, and a fix ships as the next
|
|
12
|
-
`0.
|
|
12
|
+
`0.5.x` patch or, if the minor has already moved on, as the next minor.
|
|
13
13
|
|
|
14
14
|
## Reporting a vulnerability
|
|
15
15
|
|
|
@@ -32,8 +32,10 @@ This plugin is **not a containment boundary**. It runs in-process at the agent's
|
|
|
32
32
|
anything the agent can execute can read the same files the guard denies. The following are
|
|
33
33
|
documented limits, not vulnerabilities — they are described in README.md:
|
|
34
34
|
|
|
35
|
-
- shell-command obfuscation defeating the `bash` path arm
|
|
36
|
-
|
|
35
|
+
- shell-command obfuscation defeating the `bash` path arm — anything that stops the path being
|
|
36
|
+
spelled in the command line: globbing, quote-splitting, `find -exec`, assembling the path
|
|
37
|
+
from pieces, a base64 round-trip. A command that spells the path is caught whatever program
|
|
38
|
+
it runs, so that is a gap worth reporting;
|
|
37
39
|
- encoded or split secrets passing both detection tiers;
|
|
38
40
|
- a secret with no recognisable structure going undetected;
|
|
39
41
|
- a secret reaching the provider because it was already in the conversation.
|
package/cordis.patch.yml
CHANGED
package/lib/approvals.js
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ask` tier's second rule class: a tool argument whose effect is to switch
|
|
3
|
+
* off the confirmation for the call carrying it.
|
|
4
|
+
*
|
|
5
|
+
* Three 2026 advisories are the same technique. CVE-2026-18733 is
|
|
6
|
+
* `non_interactive: true`; CVE-2026-53808 is `apply: true` while
|
|
7
|
+
* `approvalPolicy` is still `pending`; CVE-2026-56075 is
|
|
8
|
+
* `approval_mode: auto`. In each one the model composes an argument that
|
|
9
|
+
* removes the human from that call, and nothing about the file paths or the
|
|
10
|
+
* secrets involved says so.
|
|
11
|
+
*
|
|
12
|
+
* This is not the mutation check in `mutation.ts` and not the neutralizable
|
|
13
|
+
* seam of the guard floor. Those are about *this plugin's* decision being
|
|
14
|
+
* deleted by something else in the process. This is the model asking a tool to
|
|
15
|
+
* skip the tool's *own* prompt, which every one of those seams would let
|
|
16
|
+
* through as an ordinary, unmodified, secret-free call.
|
|
17
|
+
*
|
|
18
|
+
* It is an `ask` and deliberately not a denial, for the reason the write side
|
|
19
|
+
* is: `non_interactive` also means "no TTY" on plenty of ordinary programs, the
|
|
20
|
+
* tool registry is open, and an unoverridable denial on an argument name is a
|
|
21
|
+
* false denial nobody can work around. ADR.md records the whole weighing.
|
|
22
|
+
* @module dsh-dlp/approvals
|
|
23
|
+
*/
|
|
24
|
+
import { isReadOnlyTool } from "./paths.js";
|
|
25
|
+
/**
|
|
26
|
+
* Values that mean the confirmation does not happen.
|
|
27
|
+
*
|
|
28
|
+
* Only the affirmative spellings: `false`, `0` and `no` leave the prompt in
|
|
29
|
+
* place, and a rule that fired on them would prompt on the argument that asks
|
|
30
|
+
* for the prompt.
|
|
31
|
+
*/
|
|
32
|
+
const SUPPRESSING_TRUE = /^(?:true|yes|on|1)$/;
|
|
33
|
+
/**
|
|
34
|
+
* Values of an approval-mode argument that name the absence of a prompt.
|
|
35
|
+
*
|
|
36
|
+
* One spelling per mode, because the value is normalized the same way the key
|
|
37
|
+
* is: `full-auto` — which is what Codex writes — `full_auto` and `fullauto`
|
|
38
|
+
* are one entry rather than three.
|
|
39
|
+
*/
|
|
40
|
+
const SUPPRESSING_MODE = /^(?:auto|autoapprove|autoedit|never|none|bypass|fullauto|yolo)$/;
|
|
41
|
+
/**
|
|
42
|
+
* Arguments that turn off the human confirmation for the call carrying them.
|
|
43
|
+
*
|
|
44
|
+
* Matched by argument name and value, at any depth of the arguments object,
|
|
45
|
+
* never against the filesystem and never against the tool's name: the registry
|
|
46
|
+
* is open, so a table keyed on tool names would abstain on every MCP tool this
|
|
47
|
+
* build has never heard of — which is where these arguments live.
|
|
48
|
+
*/
|
|
49
|
+
export const APPROVAL_SUPPRESSION_RULES = [
|
|
50
|
+
// CVE-2026-18733.
|
|
51
|
+
{
|
|
52
|
+
id: 'dsh-dlp/approval-non-interactive',
|
|
53
|
+
version: 1,
|
|
54
|
+
condition: { key: /^noninteractive$/, value: SUPPRESSING_TRUE },
|
|
55
|
+
effect: 'a non-interactive flag, which runs the call without the confirmation it would otherwise ask for',
|
|
56
|
+
},
|
|
57
|
+
// CVE-2026-56075.
|
|
58
|
+
{
|
|
59
|
+
id: 'dsh-dlp/approval-mode-auto',
|
|
60
|
+
version: 2,
|
|
61
|
+
condition: { key: /^approval(?:mode|policy|setting)$/, value: SUPPRESSING_MODE },
|
|
62
|
+
effect: 'an approval mode that approves on the model\'s behalf instead of asking',
|
|
63
|
+
},
|
|
64
|
+
// CVE-2026-53808: applying while the approval for that change is still
|
|
65
|
+
// pending is what skips the decision, so neither half is a finding alone.
|
|
66
|
+
{
|
|
67
|
+
id: 'dsh-dlp/approval-apply-pending',
|
|
68
|
+
version: 1,
|
|
69
|
+
condition: { key: /^apply$/, value: SUPPRESSING_TRUE },
|
|
70
|
+
alongside: { key: /^approvalpolicy$/, value: /^pending$/ },
|
|
71
|
+
effect: 'an instruction to apply a change whose approval is still pending, which commits it before the answer',
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
/**
|
|
75
|
+
* The spelling one key or value is matched under: lowercase, with the
|
|
76
|
+
* separators that distinguish `non_interactive`, `nonInteractive` and
|
|
77
|
+
* `non-interactive` — and `full-auto` from `full_auto` — removed.
|
|
78
|
+
*
|
|
79
|
+
* Values take the same normalization as keys, and that is the whole reason
|
|
80
|
+
* `approval_mode: full-auto`, which is the spelling Codex writes, reaches the
|
|
81
|
+
* table: enumerating the separator variants one at a time only ever covers the
|
|
82
|
+
* spellings someone already thought of. Every value the rules name is a single
|
|
83
|
+
* word with no legitimate hyphenated or dotted form, so folding the separators
|
|
84
|
+
* away cannot pull an ordinary value in — `on-demand` and `ask-every-time`
|
|
85
|
+
* stay outside the table.
|
|
86
|
+
* @param text - a key as the tool declared it, or a scalar value it carried.
|
|
87
|
+
* @returns the normalized spelling.
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeArgumentToken(text) {
|
|
90
|
+
return text.toLowerCase().replace(/[_.-]/g, '');
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One argument value as a string, for the values a flag can take.
|
|
94
|
+
* @param node - the value under one argument key.
|
|
95
|
+
* @returns the normalized rendering, or `undefined` for an object or a list.
|
|
96
|
+
*/
|
|
97
|
+
function scalarValue(node) {
|
|
98
|
+
if (typeof node === 'boolean' || typeof node === 'number')
|
|
99
|
+
return String(node);
|
|
100
|
+
if (typeof node === 'string')
|
|
101
|
+
return normalizeArgumentToken(node.trim());
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
/** Whether one object carries a key and value the condition describes. */
|
|
105
|
+
function satisfies(entries, condition) {
|
|
106
|
+
for (const [key, value] of entries) {
|
|
107
|
+
if (condition.key.test(key) && condition.value.test(value))
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* The first rule any object inside the arguments satisfies.
|
|
114
|
+
*
|
|
115
|
+
* Both halves of a two-part rule must sit on the *same* object: an `apply` in
|
|
116
|
+
* one element of a batch and an `approvalPolicy` in another are two different
|
|
117
|
+
* requests, and pairing them across objects would report a call nobody made.
|
|
118
|
+
* @param args - the pending call's parsed arguments.
|
|
119
|
+
* @param rules - the rule table; defaults to {@link APPROVAL_SUPPRESSION_RULES}.
|
|
120
|
+
* @returns the first matching rule, or `undefined`.
|
|
121
|
+
*/
|
|
122
|
+
export function matchApprovalSuppression(args, rules = APPROVAL_SUPPRESSION_RULES) {
|
|
123
|
+
let found;
|
|
124
|
+
const walk = (node) => {
|
|
125
|
+
if (found !== undefined)
|
|
126
|
+
return;
|
|
127
|
+
if (Array.isArray(node)) {
|
|
128
|
+
for (const item of node)
|
|
129
|
+
walk(item);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (typeof node !== 'object' || node === null)
|
|
133
|
+
return;
|
|
134
|
+
const entries = new Map();
|
|
135
|
+
for (const [key, value] of Object.entries(node)) {
|
|
136
|
+
const scalar = scalarValue(value);
|
|
137
|
+
if (scalar !== undefined)
|
|
138
|
+
entries.set(normalizeArgumentToken(key), scalar);
|
|
139
|
+
}
|
|
140
|
+
found = rules.find(rule => satisfies(entries, rule.condition)
|
|
141
|
+
&& (rule.alongside === undefined || satisfies(entries, rule.alongside)));
|
|
142
|
+
if (found !== undefined)
|
|
143
|
+
return;
|
|
144
|
+
for (const value of Object.values(node))
|
|
145
|
+
walk(value);
|
|
146
|
+
};
|
|
147
|
+
walk(args);
|
|
148
|
+
return found;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Prompt text for one approval-suppressing argument.
|
|
152
|
+
*
|
|
153
|
+
* The argument's own value is not quoted, for the reason the write side does
|
|
154
|
+
* not quote a path: this string is model-visible, and what the user needs in
|
|
155
|
+
* order to answer is which tool, which rule, and what the argument does.
|
|
156
|
+
*/
|
|
157
|
+
function approvalSuppressionReason(toolName, rule) {
|
|
158
|
+
return `dsh-dlp is asking before ${JSON.stringify(toolName)} runs with ${rule.effect} (rule ${rule.id}). `
|
|
159
|
+
+ 'The call carries an argument that turns off the confirmation for this call, so the prompt you would '
|
|
160
|
+
+ 'normally see is this one. Approve it if you asked for an unattended run; decline it if you did not.';
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Decide whether one call suppresses its own confirmation.
|
|
164
|
+
*
|
|
165
|
+
* A tool {@link isReadOnlyTool} classifies as query-only is left alone: it has
|
|
166
|
+
* nothing to confirm, so an argument switching a confirmation off changes
|
|
167
|
+
* nothing there.
|
|
168
|
+
* @param exec - the pending call.
|
|
169
|
+
* @param rules - the rule table; defaults to {@link APPROVAL_SUPPRESSION_RULES}.
|
|
170
|
+
* @returns the finding, or `undefined` to leave the call alone.
|
|
171
|
+
*/
|
|
172
|
+
export function evaluateApprovalSuppression(exec, rules = APPROVAL_SUPPRESSION_RULES) {
|
|
173
|
+
if (isReadOnlyTool(exec.name))
|
|
174
|
+
return undefined;
|
|
175
|
+
const rule = matchApprovalSuppression(exec.arguments, rules);
|
|
176
|
+
return rule === undefined ? undefined : { rule, reason: approvalSuppressionReason(exec.name, rule) };
|
|
177
|
+
}
|
package/lib/cli.js
CHANGED
|
@@ -94,9 +94,9 @@ export const USAGE = [
|
|
|
94
94
|
' --since <when> only decisions at or after an ISO timestamp, or a span back',
|
|
95
95
|
' from now written as 30m, 24h or 7d',
|
|
96
96
|
' --session <id> only decisions from one session',
|
|
97
|
-
' --would-have
|
|
98
|
-
'
|
|
99
|
-
'
|
|
97
|
+
' --would-have leave out the decisions that stopped a call, keeping the',
|
|
98
|
+
' redactions, the invisible-character findings, the asks and',
|
|
99
|
+
' the neutralised images',
|
|
100
100
|
' -h, --help print this text',
|
|
101
101
|
].join('\n');
|
|
102
102
|
/** Milliseconds in one `--since` suffix; {@link parseSince} accepts no other. */
|
|
@@ -250,7 +250,7 @@ export function formatReport(records, unreadable, options) {
|
|
|
250
250
|
if (options.session !== undefined)
|
|
251
251
|
lines.push(` session ${options.session}`);
|
|
252
252
|
if (options.wouldHave)
|
|
253
|
-
lines.push('
|
|
253
|
+
lines.push(' decisions that stopped a call left out');
|
|
254
254
|
if (unreadable > 0)
|
|
255
255
|
lines.push(` ${unreadable} line(s) were not readable as records`);
|
|
256
256
|
if (selected.length === 0)
|
package/lib/config-writes.js
CHANGED
|
@@ -31,6 +31,11 @@ import { nestedStrings } from "./redaction.js";
|
|
|
31
31
|
* CVE-2026-25725, CVE-2026-33068, CVE-2026-48124, CVE-2026-26268 and
|
|
32
32
|
* CVE-2025-59041.
|
|
33
33
|
*
|
|
34
|
+
* The file is the whole payload, which is why this tier watches the write
|
|
35
|
+
* rather than a later execution: the keyv/cacheable compromise of 2026-08-04
|
|
36
|
+
* placed a `SessionStart` hook in `.claude/settings.json` and was reported to
|
|
37
|
+
* need no `npm install` to take effect.
|
|
38
|
+
*
|
|
34
39
|
* The rules match by name, never by what is on disk, so a file the call is
|
|
35
40
|
* about to *create* is matched exactly like one it would change:
|
|
36
41
|
* CVE-2026-25725 worked precisely because the path did not exist yet and was
|
|
@@ -46,25 +51,55 @@ export const CONFIG_WRITE_RULES = [
|
|
|
46
51
|
},
|
|
47
52
|
{
|
|
48
53
|
id: 'dsh-dlp/config-agent-hooks',
|
|
49
|
-
version:
|
|
54
|
+
version: 2,
|
|
50
55
|
match: 'path',
|
|
51
|
-
|
|
56
|
+
// `.cursor` is in the sibling settings rule above and was missing here,
|
|
57
|
+
// which left one of the six agent directories' hooks unguarded.
|
|
58
|
+
pattern: /(^|\/)\.(claude|gemini|codex|cursor|windsurf|continue)\/hooks(\/|$)/i,
|
|
52
59
|
effect: 'an agent hook, which runs on a session event without the model asking for it',
|
|
53
60
|
},
|
|
61
|
+
// Copilot reads these without any agent asking it to: VS Code documents
|
|
62
|
+
// `.github/copilot-instructions.md` and every `.github/instructions/**.md`
|
|
63
|
+
// as workspace instruction files it applies on its own. They are separate
|
|
64
|
+
// from `config-ci-workflow`, which governs what CI *runs* rather than what a
|
|
65
|
+
// model is told.
|
|
54
66
|
{
|
|
55
|
-
id: 'dsh-dlp/config-
|
|
67
|
+
id: 'dsh-dlp/config-copilot-instructions',
|
|
56
68
|
version: 1,
|
|
57
69
|
match: 'path',
|
|
58
|
-
pattern: /(^|\/)(
|
|
70
|
+
pattern: /(^|\/)\.github\/(copilot-instructions\.md$|instructions\/)/i,
|
|
71
|
+
effect: 'standing instructions the editor feeds to every future session',
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
id: 'dsh-dlp/config-agent-instructions',
|
|
75
|
+
version: 2,
|
|
76
|
+
// `CLAUDE.local.md` is the personal, git-ignored companion to `CLAUDE.md`
|
|
77
|
+
// and is read the same way, so covering one and not the other left the
|
|
78
|
+
// quieter of the two files unguarded.
|
|
79
|
+
pattern: /(^|\/)(CLAUDE(\.local)?|AGENTS|GEMINI|\.cursorrules|\.windsurfrules)(\.md)?$/i,
|
|
80
|
+
match: 'path',
|
|
59
81
|
effect: 'standing instructions every future session in this repository reads',
|
|
60
82
|
},
|
|
83
|
+
// `.claude/rules` belongs beside the other three: VS Code lists it as a
|
|
84
|
+
// workspace instruction location it detects and applies on its own
|
|
85
|
+
// ("Workspace (Claude format) `.claude/rules` folder"), so it is loaded by an
|
|
86
|
+
// editor a developer never configured for Claude.
|
|
61
87
|
{
|
|
62
88
|
id: 'dsh-dlp/config-agent-rules',
|
|
63
|
-
version:
|
|
89
|
+
version: 2,
|
|
64
90
|
match: 'path',
|
|
65
|
-
pattern: /(^|\/)\.(cursor|windsurf|continue)\/rules(\/|$)/i,
|
|
91
|
+
pattern: /(^|\/)\.(claude|cursor|windsurf|continue)\/rules(\/|$)/i,
|
|
66
92
|
effect: 'an always-apply rules file every future session in this repository reads',
|
|
67
93
|
},
|
|
94
|
+
// CVE-2026-46580. A prompt template is loaded into a later session the same
|
|
95
|
+
// way a rules file is, and the extension is what marks it for loading.
|
|
96
|
+
{
|
|
97
|
+
id: 'dsh-dlp/config-prompt-template',
|
|
98
|
+
version: 1,
|
|
99
|
+
match: 'path',
|
|
100
|
+
pattern: /(^|\/)\.prompts\/.*\.prompttemplate$/i,
|
|
101
|
+
effect: 'a prompt template a later session loads without the model asking for it',
|
|
102
|
+
},
|
|
68
103
|
{
|
|
69
104
|
id: 'dsh-dlp/config-mcp-manifest',
|
|
70
105
|
version: 1,
|
|
@@ -114,6 +149,23 @@ export const CONFIG_WRITE_RULES = [
|
|
|
114
149
|
pattern: /(^|\/)cordis[^/]*\.ya?ml$/i,
|
|
115
150
|
effect: 'a harness bundle manifest, which decides which plugins load',
|
|
116
151
|
},
|
|
152
|
+
// pnpm reads `registry`, `registries` and `namedRegistries` from
|
|
153
|
+
// `pnpm-workspace.yaml`, so the file decides which host the next install
|
|
154
|
+
// downloads packages from. pnpm's own documentation treats it as
|
|
155
|
+
// attacker-controlled for exactly that reason: since v11.5.3 it refuses to
|
|
156
|
+
// expand `${...}` in those settings, "Because `pnpm-workspace.yaml` is
|
|
157
|
+
// committed to the repository, expanding env variables in registry URLs could
|
|
158
|
+
// be exploited by a malicious repository to leak secrets from the environment
|
|
159
|
+
// to an attacker-controlled registry." A literal hostile URL is still obeyed.
|
|
160
|
+
// The `.npmrc` half of the same technique needs no rule here: it is on the
|
|
161
|
+
// guard floor as `dsh-dlp/path-npmrc`, where every call is denied.
|
|
162
|
+
{
|
|
163
|
+
id: 'dsh-dlp/config-pnpm-workspace',
|
|
164
|
+
version: 1,
|
|
165
|
+
match: 'path',
|
|
166
|
+
pattern: /(^|\/)pnpm-workspace\.ya?ml$/i,
|
|
167
|
+
effect: 'the pnpm workspace settings, which decide the registry the next install downloads packages from',
|
|
168
|
+
},
|
|
117
169
|
// CVE-2026-21852: a repo-local settings file setting `ANTHROPIC_BASE_URL`
|
|
118
170
|
// sends the user's own API key to whatever host it names. This is neither a
|
|
119
171
|
// path nor a secret — it is a key whose *value* redirects a credential — so
|
package/lib/detectors.js
CHANGED
|
@@ -64,7 +64,29 @@ export const SYNC_RULES = [
|
|
|
64
64
|
{ id: 'dsh-dlp/google-oauth-client-secret', version: 1, severity: 'critical', pattern: /\bGOCSPX-[A-Za-z0-9_-]{24,}/g },
|
|
65
65
|
{ id: 'dsh-dlp/databricks-token', version: 1, severity: 'critical', pattern: /\bdapi[0-9a-f]{32}(?:-\d+)?\b/g },
|
|
66
66
|
{ id: 'dsh-dlp/sendgrid-api-key', version: 1, severity: 'critical', pattern: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}/g },
|
|
67
|
+
// Supabase's `sbp_` format is superseded but not retired: the platform's own
|
|
68
|
+
// deprecation notice for the keys it replaces says they "will be deprecated
|
|
69
|
+
// by the end of 2026", so a credential in this format is still live and a
|
|
70
|
+
// rule matching it still fires on something. Removing it would also make
|
|
71
|
+
// every audit record already carrying this rule id uninterpretable.
|
|
67
72
|
{ id: 'dsh-dlp/supabase-service-key', version: 1, severity: 'critical', pattern: /\bsbp_[0-9a-f]{40}\b/g },
|
|
73
|
+
// The current format. Supabase documents the prefixes (`sb_publishable_...`,
|
|
74
|
+
// `sb_secret_...`) but not the suffix, so the rule anchors on the documented
|
|
75
|
+
// prefix and requires enough base64url characters to exclude prose: the keys
|
|
76
|
+
// shown in the announcement discussion carry a 22-character body, a `_`, and
|
|
77
|
+
// a checksum. `sb_publishable_` is deliberately absent — it is the
|
|
78
|
+
// browser-facing replacement for `anon` and is meant to be published, the
|
|
79
|
+
// same reason this table matches Stripe's `sk_live_` and not `pk_live_`.
|
|
80
|
+
{ id: 'dsh-dlp/supabase-secret-key', version: 1, severity: 'critical', pattern: /\bsb_secret_[A-Za-z0-9_-]{16,}/g },
|
|
81
|
+
// Cloudflare's scannable format, from the provider's own table: "Each
|
|
82
|
+
// credential type has a distinct prefix followed by 40 characters and a
|
|
83
|
+
// checksum" — `cfk_` for a Global API Key, `cfut_` for a User API Token,
|
|
84
|
+
// `cfat_` for an Account API Token. The checksum's length and character set
|
|
85
|
+
// are not published, so the rule requires the documented 40 and lets the
|
|
86
|
+
// match run to the end of the token. The legacy formats are a bare
|
|
87
|
+
// 40-character alphanumeric string and a 37-45 character hex string, neither
|
|
88
|
+
// of which is prefix-anchored and both of which are therefore tier 2's.
|
|
89
|
+
{ id: 'dsh-dlp/cloudflare-api-token', version: 1, severity: 'critical', pattern: /\bcf(?:ut|at|k)_[A-Za-z0-9_-]{40,}/g },
|
|
68
90
|
{ id: 'dsh-dlp/notion-token', version: 1, severity: 'critical', pattern: /\bntn_[A-Za-z0-9]{40,}/g },
|
|
69
91
|
{ id: 'dsh-dlp/private-key-block', version: 1, severity: 'critical', pattern: /-----BEGIN (?:[A-Z]+ )*PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z]+ )*PRIVATE KEY-----/g },
|
|
70
92
|
{ id: 'dsh-dlp/json-web-token', version: 1, severity: 'high', pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g },
|
package/lib/index.js
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* has no allow arm.
|
|
10
10
|
* 2. `tools/pre-execute` — the async breadth tier, which can await
|
|
11
11
|
* `@secretlint/core`. Neutralizable by any listener registered ahead of it.
|
|
12
|
-
* 2b. `tools/pre-execute` — the `ask` tier for writes to behaviour-changing
|
|
13
|
-
* config paths
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* 2b. `tools/pre-execute` — the `ask` tier, for writes to behaviour-changing
|
|
13
|
+
* config paths and for calls carrying an argument that switches their own
|
|
14
|
+
* confirmation off. Deliberately here rather than on the floor: its rules
|
|
15
|
+
* have a real false-positive rate and the floor cannot ask. Neutralizable,
|
|
16
|
+
* and it abstains entirely when no approval service is mounted.
|
|
16
17
|
* 3. `tools/post-execute` — result redaction, applied before the `tool/result`
|
|
17
18
|
* session event is appended, so the durable log records the redacted copy;
|
|
18
19
|
* a result that cannot be cleaned is withheld rather than accepted.
|
|
@@ -41,6 +42,7 @@ import { safeEvaluateGuard } from "./guard.js";
|
|
|
41
42
|
import { neutralizeImageStream } from "./images.js";
|
|
42
43
|
import { ExecutionSnapshots, mutationReason } from "./mutation.js";
|
|
43
44
|
import { evaluateConfigWrite } from "./config-writes.js";
|
|
45
|
+
import { evaluateApprovalSuppression } from "./approvals.js";
|
|
44
46
|
import { breadthTierDenial, evaluateBreadthTier, redactDecision } from "./results.js";
|
|
45
47
|
import { redactRecord, telemetrySeamNotice } from "./telemetry.js";
|
|
46
48
|
import { AuditSink, CallCorrelator, newDecisionId, RECORD_VERSION } from "./sink.js";
|
|
@@ -251,11 +253,12 @@ export function apply(ctx, config) {
|
|
|
251
253
|
if (approvalSeamReported)
|
|
252
254
|
return;
|
|
253
255
|
approvalSeamReported = true;
|
|
254
|
-
notice(ctx, 'dsh-dlp: configWriteAsk is enabled, but no approval service
|
|
255
|
-
+ ' to a denial. This tier abstains instead: a write to a
|
|
256
|
+
notice(ctx, 'dsh-dlp: the ask tier (configWriteAsk, approvalSuppressionAsk) is enabled, but no approval service'
|
|
257
|
+
+ ' is mounted, so an ask would degrade to a denial. This tier abstains instead: a write to a'
|
|
258
|
+
+ ' behaviour-changing config path, and a call that switches its own confirmation off, are allowed through'
|
|
256
259
|
+ ' with no prompt. The guard floor is unaffected.');
|
|
257
260
|
};
|
|
258
|
-
if (policy.configWriteAsk) {
|
|
261
|
+
if (policy.configWriteAsk || policy.approvalSuppressionAsk) {
|
|
259
262
|
// Registered ahead of the breadth tier, so a call that is both a config
|
|
260
263
|
// write and carries a secret is denied rather than merely asked about:
|
|
261
264
|
// this listener sees whatever the rest of the waterfall settled on and
|
|
@@ -264,7 +267,11 @@ export function apply(ctx, config) {
|
|
|
264
267
|
const decision = await next();
|
|
265
268
|
if (decision.kind !== 'allow')
|
|
266
269
|
return decision;
|
|
267
|
-
|
|
270
|
+
// The argument that switches a confirmation off is reported ahead of the
|
|
271
|
+
// file it would write: it describes the call itself rather than what the
|
|
272
|
+
// call touches, and it is the one the user has least reason to expect.
|
|
273
|
+
const finding = (policy.approvalSuppressionAsk ? evaluateApprovalSuppression(exec) : undefined)
|
|
274
|
+
?? (policy.configWriteAsk ? evaluateConfigWrite(exec) : undefined);
|
|
268
275
|
if (finding === undefined)
|
|
269
276
|
return decision;
|
|
270
277
|
// A call the floor will deny anyway is left to the floor. Any non-allow
|
package/lib/paths.js
CHANGED
|
@@ -40,7 +40,9 @@ export const CREDENTIAL_PATH_RULES = [
|
|
|
40
40
|
{ id: 'dsh-dlp/path-pypirc', version: 1, pattern: /(^|\/)\.pypirc$/i },
|
|
41
41
|
{ id: 'dsh-dlp/path-git-credentials', version: 1, pattern: /(^|\/)\.git-credentials$/i },
|
|
42
42
|
{ id: 'dsh-dlp/path-gh-config', version: 1, pattern: /(^|\/)\.config\/gh(\/|$)/i },
|
|
43
|
-
|
|
43
|
+
// `.kube` matches at any depth, like `.aws` and `.azure`: the cached OIDC and
|
|
44
|
+
// exec-plugin tokens sit under `~/.kube/cache/`, not beside the config file.
|
|
45
|
+
{ id: 'dsh-dlp/path-kubeconfig', version: 3, pattern: /(^|\/)(\.kube(\/|$)|kubeconfig[^/]*$)/i },
|
|
44
46
|
{ id: 'dsh-dlp/path-kubernetes-conf', version: 1, pattern: /(^|\/)kubernetes\/[^/]*\.conf$/i },
|
|
45
47
|
{ id: 'dsh-dlp/path-docker-config', version: 2, pattern: /(^|\/)(\.docker\/config\.json|\.dockercfg)$/i },
|
|
46
48
|
{ id: 'dsh-dlp/path-gcloud-credentials', version: 1, pattern: /(^|\/)\.config\/gcloud\/[^/]*credential[^/]*$/i },
|
|
@@ -53,8 +55,10 @@ export const CREDENTIAL_PATH_RULES = [
|
|
|
53
55
|
// token file whatever else the directory holds.
|
|
54
56
|
{ id: 'dsh-dlp/path-agent-auth', version: 1, pattern: /(^|\/)\.?(codex|cursor|composer|windsurf|continue|aider|claude|gemini)\/auth\.json$/i },
|
|
55
57
|
// An MCP manifest carries each server's `env`, which is where its API keys
|
|
56
|
-
// are written.
|
|
57
|
-
|
|
58
|
+
// are written. The directory alternation is the one above: an agent that
|
|
59
|
+
// keeps an `auth.json` keeps its manifest beside it, and Cursor's own
|
|
60
|
+
// directory is spelled without the dot under `~/.config`.
|
|
61
|
+
{ id: 'dsh-dlp/path-agent-mcp-config', version: 2, pattern: /(^|\/)\.?(codex|cursor|composer|windsurf|continue|aider|claude|gemini)\/mcp\.json$/i },
|
|
58
62
|
// Cursor keeps session tokens in a SQLite state database rather than a
|
|
59
63
|
// credential file.
|
|
60
64
|
{ id: 'dsh-dlp/path-editor-state-db', version: 1, pattern: /(^|\/)state\.vscdb(-journal|-wal|-shm)?$/i },
|
package/lib/policy.js
CHANGED
|
@@ -33,6 +33,7 @@ export const Config = z.object({
|
|
|
33
33
|
remoteImageNeutralization: z.boolean().default(true),
|
|
34
34
|
redactTelemetryWorkspacePaths: z.boolean().default(true),
|
|
35
35
|
configWriteAsk: z.boolean().default(true),
|
|
36
|
+
approvalSuppressionAsk: z.boolean().default(true),
|
|
36
37
|
});
|
|
37
38
|
/** Config toggles a repo-local policy may switch on, and never off. */
|
|
38
39
|
const ENABLEABLE = [
|
|
@@ -42,6 +43,7 @@ const ENABLEABLE = [
|
|
|
42
43
|
'remoteImageNeutralization',
|
|
43
44
|
'redactTelemetryWorkspacePaths',
|
|
44
45
|
'configWriteAsk',
|
|
46
|
+
'approvalSuppressionAsk',
|
|
45
47
|
];
|
|
46
48
|
/** Keys a repo-local policy file may carry; anything else fails the load. */
|
|
47
49
|
const POLICY_KEYS = ['v', 'addCredentialPaths', 'addEgressTools', 'raiseSeverity', 'enable'];
|
|
@@ -284,5 +286,6 @@ export function resolvePolicy(config, repo) {
|
|
|
284
286
|
remoteImageNeutralization: enabled('remoteImageNeutralization'),
|
|
285
287
|
redactTelemetryWorkspacePaths: enabled('redactTelemetryWorkspacePaths'),
|
|
286
288
|
configWriteAsk: enabled('configWriteAsk'),
|
|
289
|
+
approvalSuppressionAsk: enabled('approvalSuppressionAsk'),
|
|
287
290
|
};
|
|
288
291
|
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ask` tier's second rule class: a tool argument whose effect is to switch
|
|
3
|
+
* off the confirmation for the call carrying it.
|
|
4
|
+
*
|
|
5
|
+
* Three 2026 advisories are the same technique. CVE-2026-18733 is
|
|
6
|
+
* `non_interactive: true`; CVE-2026-53808 is `apply: true` while
|
|
7
|
+
* `approvalPolicy` is still `pending`; CVE-2026-56075 is
|
|
8
|
+
* `approval_mode: auto`. In each one the model composes an argument that
|
|
9
|
+
* removes the human from that call, and nothing about the file paths or the
|
|
10
|
+
* secrets involved says so.
|
|
11
|
+
*
|
|
12
|
+
* This is not the mutation check in `mutation.ts` and not the neutralizable
|
|
13
|
+
* seam of the guard floor. Those are about *this plugin's* decision being
|
|
14
|
+
* deleted by something else in the process. This is the model asking a tool to
|
|
15
|
+
* skip the tool's *own* prompt, which every one of those seams would let
|
|
16
|
+
* through as an ordinary, unmodified, secret-free call.
|
|
17
|
+
*
|
|
18
|
+
* It is an `ask` and deliberately not a denial, for the reason the write side
|
|
19
|
+
* is: `non_interactive` also means "no TTY" on plenty of ordinary programs, the
|
|
20
|
+
* tool registry is open, and an unoverridable denial on an argument name is a
|
|
21
|
+
* false denial nobody can work around. ADR.md records the whole weighing.
|
|
22
|
+
* @module dsh-dlp/approvals
|
|
23
|
+
*/
|
|
24
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
25
|
+
/** One argument key and the value that makes it a finding. */
|
|
26
|
+
export interface ArgumentCondition {
|
|
27
|
+
/** Matched against the key lowercased with `_`, `-` and `.` removed. */
|
|
28
|
+
readonly key: RegExp;
|
|
29
|
+
/** Matched against the value under the same normalization as the key. */
|
|
30
|
+
readonly value: RegExp;
|
|
31
|
+
}
|
|
32
|
+
/** One argument, or pair of arguments, that suppresses a confirmation. */
|
|
33
|
+
export interface ApprovalSuppressionRule {
|
|
34
|
+
readonly id: string;
|
|
35
|
+
readonly version: number;
|
|
36
|
+
readonly condition: ArgumentCondition;
|
|
37
|
+
/**
|
|
38
|
+
* A second pair that must be present on the same object for the rule to
|
|
39
|
+
* fire. `apply: true` on its own is how half the infrastructure tools in
|
|
40
|
+
* existence are driven; it is the pending approval beside it that makes the
|
|
41
|
+
* call skip a decision someone else had not made yet.
|
|
42
|
+
*/
|
|
43
|
+
readonly alongside?: ArgumentCondition;
|
|
44
|
+
/** What the argument does, quoted in the prompt the user answers. */
|
|
45
|
+
readonly effect: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Arguments that turn off the human confirmation for the call carrying them.
|
|
49
|
+
*
|
|
50
|
+
* Matched by argument name and value, at any depth of the arguments object,
|
|
51
|
+
* never against the filesystem and never against the tool's name: the registry
|
|
52
|
+
* is open, so a table keyed on tool names would abstain on every MCP tool this
|
|
53
|
+
* build has never heard of — which is where these arguments live.
|
|
54
|
+
*/
|
|
55
|
+
export declare const APPROVAL_SUPPRESSION_RULES: readonly ApprovalSuppressionRule[];
|
|
56
|
+
/**
|
|
57
|
+
* The spelling one key or value is matched under: lowercase, with the
|
|
58
|
+
* separators that distinguish `non_interactive`, `nonInteractive` and
|
|
59
|
+
* `non-interactive` — and `full-auto` from `full_auto` — removed.
|
|
60
|
+
*
|
|
61
|
+
* Values take the same normalization as keys, and that is the whole reason
|
|
62
|
+
* `approval_mode: full-auto`, which is the spelling Codex writes, reaches the
|
|
63
|
+
* table: enumerating the separator variants one at a time only ever covers the
|
|
64
|
+
* spellings someone already thought of. Every value the rules name is a single
|
|
65
|
+
* word with no legitimate hyphenated or dotted form, so folding the separators
|
|
66
|
+
* away cannot pull an ordinary value in — `on-demand` and `ask-every-time`
|
|
67
|
+
* stay outside the table.
|
|
68
|
+
* @param text - a key as the tool declared it, or a scalar value it carried.
|
|
69
|
+
* @returns the normalized spelling.
|
|
70
|
+
*/
|
|
71
|
+
export declare function normalizeArgumentToken(text: string): string;
|
|
72
|
+
/**
|
|
73
|
+
* The first rule any object inside the arguments satisfies.
|
|
74
|
+
*
|
|
75
|
+
* Both halves of a two-part rule must sit on the *same* object: an `apply` in
|
|
76
|
+
* one element of a batch and an `approvalPolicy` in another are two different
|
|
77
|
+
* requests, and pairing them across objects would report a call nobody made.
|
|
78
|
+
* @param args - the pending call's parsed arguments.
|
|
79
|
+
* @param rules - the rule table; defaults to {@link APPROVAL_SUPPRESSION_RULES}.
|
|
80
|
+
* @returns the first matching rule, or `undefined`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function matchApprovalSuppression(args: unknown, rules?: readonly ApprovalSuppressionRule[]): ApprovalSuppressionRule | undefined;
|
|
83
|
+
/** A call this tier wants a human to confirm, because the call asked not to be. */
|
|
84
|
+
export interface ApprovalSuppressionFinding {
|
|
85
|
+
readonly rule: ApprovalSuppressionRule;
|
|
86
|
+
/** Model- and user-facing text; names the tool, the rule and what the argument does. */
|
|
87
|
+
readonly reason: string;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Decide whether one call suppresses its own confirmation.
|
|
91
|
+
*
|
|
92
|
+
* A tool {@link isReadOnlyTool} classifies as query-only is left alone: it has
|
|
93
|
+
* nothing to confirm, so an argument switching a confirmation off changes
|
|
94
|
+
* nothing there.
|
|
95
|
+
* @param exec - the pending call.
|
|
96
|
+
* @param rules - the rule table; defaults to {@link APPROVAL_SUPPRESSION_RULES}.
|
|
97
|
+
* @returns the finding, or `undefined` to leave the call alone.
|
|
98
|
+
*/
|
|
99
|
+
export declare function evaluateApprovalSuppression(exec: Pick<ToolExecution, 'name' | 'arguments'>, rules?: readonly ApprovalSuppressionRule[]): ApprovalSuppressionFinding | undefined;
|
|
100
|
+
//# sourceMappingURL=approvals.d.ts.map
|
package/lib/types/cli.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface ReportOptions {
|
|
|
36
36
|
/** Epoch milliseconds; records before it are left out. */
|
|
37
37
|
readonly since?: number;
|
|
38
38
|
readonly session?: string;
|
|
39
|
-
/**
|
|
39
|
+
/** Leave out the decisions that stopped a call; see {@link DENYING_KINDS}. */
|
|
40
40
|
readonly wouldHave: boolean;
|
|
41
41
|
}
|
|
42
42
|
/** The outcome of reading the command line. */
|
|
@@ -45,6 +45,11 @@ export interface ConfigWriteRule {
|
|
|
45
45
|
* CVE-2026-25725, CVE-2026-33068, CVE-2026-48124, CVE-2026-26268 and
|
|
46
46
|
* CVE-2025-59041.
|
|
47
47
|
*
|
|
48
|
+
* The file is the whole payload, which is why this tier watches the write
|
|
49
|
+
* rather than a later execution: the keyv/cacheable compromise of 2026-08-04
|
|
50
|
+
* placed a `SessionStart` hook in `.claude/settings.json` and was reported to
|
|
51
|
+
* need no `npm install` to take effect.
|
|
52
|
+
*
|
|
48
53
|
* The rules match by name, never by what is on disk, so a file the call is
|
|
49
54
|
* about to *create* is matched exactly like one it would change:
|
|
50
55
|
* CVE-2026-25725 worked precisely because the path did not exist yet and was
|
package/lib/types/index.d.ts
CHANGED
|
@@ -9,10 +9,11 @@
|
|
|
9
9
|
* has no allow arm.
|
|
10
10
|
* 2. `tools/pre-execute` — the async breadth tier, which can await
|
|
11
11
|
* `@secretlint/core`. Neutralizable by any listener registered ahead of it.
|
|
12
|
-
* 2b. `tools/pre-execute` — the `ask` tier for writes to behaviour-changing
|
|
13
|
-
* config paths
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* 2b. `tools/pre-execute` — the `ask` tier, for writes to behaviour-changing
|
|
13
|
+
* config paths and for calls carrying an argument that switches their own
|
|
14
|
+
* confirmation off. Deliberately here rather than on the floor: its rules
|
|
15
|
+
* have a real false-positive rate and the floor cannot ask. Neutralizable,
|
|
16
|
+
* and it abstains entirely when no approval service is mounted.
|
|
16
17
|
* 3. `tools/post-execute` — result redaction, applied before the `tool/result`
|
|
17
18
|
* session event is appended, so the durable log records the redacted copy;
|
|
18
19
|
* a result that cannot be cleaned is withheld rather than accepted.
|
package/lib/types/policy.d.ts
CHANGED
|
@@ -43,10 +43,16 @@ export interface Config {
|
|
|
43
43
|
* letting an `ask` degrade into a denial.
|
|
44
44
|
*/
|
|
45
45
|
configWriteAsk: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Whether a call carrying an argument that switches its own confirmation off
|
|
48
|
+
* asks the user first. Shares the `ask` tier, and its approval service, with
|
|
49
|
+
* {@link Config.configWriteAsk}.
|
|
50
|
+
*/
|
|
51
|
+
approvalSuppressionAsk: boolean;
|
|
46
52
|
}
|
|
47
53
|
export declare const Config: z<Config>;
|
|
48
54
|
/** Config toggles a repo-local policy may switch on, and never off. */
|
|
49
|
-
declare const ENABLEABLE: readonly ["breadthTier", "resultRedaction", "telemetryRedaction", "remoteImageNeutralization", "redactTelemetryWorkspacePaths", "configWriteAsk"];
|
|
55
|
+
declare const ENABLEABLE: readonly ["breadthTier", "resultRedaction", "telemetryRedaction", "remoteImageNeutralization", "redactTelemetryWorkspacePaths", "configWriteAsk", "approvalSuppressionAsk"];
|
|
50
56
|
/** One toggle name a repo-local policy may name in `enable`. */
|
|
51
57
|
export type EnableableToggle = typeof ENABLEABLE[number];
|
|
52
58
|
/** Payload version this package writes and accepts for repo-local policy files. */
|
|
@@ -70,6 +76,7 @@ export interface ResolvedPolicy {
|
|
|
70
76
|
readonly remoteImageNeutralization: boolean;
|
|
71
77
|
readonly redactTelemetryWorkspacePaths: boolean;
|
|
72
78
|
readonly configWriteAsk: boolean;
|
|
79
|
+
readonly approvalSuppressionAsk: boolean;
|
|
73
80
|
}
|
|
74
81
|
/** Thrown when a policy file is malformed or attempts to loosen the policy. */
|
|
75
82
|
export declare class PolicyError extends Error {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-dlp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Data-loss-prevention plugin for DeepSeek Harness: a non-configurable tool guard floor, tool-result redaction, and fail-closed telemetry redaction",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Ivan Tyshchenko <nsof@protonmail.com>",
|