dsh-dlp 0.1.0 → 0.3.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 +297 -21
- package/SECURITY.md +1 -1
- package/cordis.patch.yml +1 -0
- package/lib/cli.js +307 -0
- package/lib/detectors.js +87 -0
- package/lib/guard.js +9 -6
- package/lib/home.js +37 -0
- package/lib/images.js +183 -0
- package/lib/index.js +112 -7
- package/lib/mutation.js +102 -0
- package/lib/paths.js +38 -0
- package/lib/policy.js +24 -17
- package/lib/redaction.js +9 -1
- package/lib/results.js +25 -9
- package/lib/schema.js +99 -0
- package/lib/telemetry.js +29 -0
- package/lib/types/cli.d.ts +107 -0
- package/lib/types/detectors.d.ts +66 -0
- package/lib/types/guard.d.ts +6 -4
- package/lib/types/home.d.ts +31 -0
- package/lib/types/images.d.ts +81 -0
- package/lib/types/index.d.ts +9 -0
- package/lib/types/mutation.d.ts +83 -0
- package/lib/types/paths.d.ts +38 -0
- package/lib/types/policy.d.ts +4 -10
- package/lib/types/results.d.ts +15 -6
- package/lib/types/schema.d.ts +41 -0
- package/lib/types/sink.d.ts +18 -1
- package/lib/types/telemetry.d.ts +16 -1
- package/package.json +4 -1
package/lib/telemetry.js
CHANGED
|
@@ -27,6 +27,35 @@
|
|
|
27
27
|
*/
|
|
28
28
|
import { scanSync } from "./detectors.js";
|
|
29
29
|
import { placeholderFor, redactJson, redactText } from "./redaction.js";
|
|
30
|
+
/**
|
|
31
|
+
* What to tell the operator when the redaction seam will never dispatch.
|
|
32
|
+
*
|
|
33
|
+
* A `session-telemetry/record` listener mounts successfully and never runs
|
|
34
|
+
* unless a backend built a coordinator, and the shipped default builds none:
|
|
35
|
+
* the mode is `DISABLED`, so nothing is exported and nothing is dispatched.
|
|
36
|
+
* That is the safe posture, not a leak — but an operator who mounts a redactor
|
|
37
|
+
* under it sees every signal of success and has verified nothing. The
|
|
38
|
+
* backend's own `sharing` disclosure is the resolved answer, so this never
|
|
39
|
+
* guesses at `DSH_TELEMETRY_MODE`, which is only the base patch's default
|
|
40
|
+
* expression for a `mode` a deployment can also set directly.
|
|
41
|
+
* @param sharing - the mounted backend's disclosure, or `undefined` when no backend is mounted.
|
|
42
|
+
* @returns the line to report, or `undefined` when the seam does dispatch.
|
|
43
|
+
*/
|
|
44
|
+
export function telemetrySeamNotice(sharing) {
|
|
45
|
+
const consequence = 'nothing dispatches the session-telemetry/record waterfall and this plugin\'s telemetry'
|
|
46
|
+
+ ' redaction never runs. Nothing is exported in this state, so this is not a leak — it means the redaction'
|
|
47
|
+
+ ' rules are unverified, and they begin running the moment telemetry is turned on. Informational only: the'
|
|
48
|
+
+ ' plugin\'s other seams are unaffected.';
|
|
49
|
+
switch (sharing) {
|
|
50
|
+
case 'disabled':
|
|
51
|
+
return 'dsh-dlp: telemetryRedaction is enabled, but the mounted session-telemetry backend reports sharing'
|
|
52
|
+
+ ` "disabled", so ${consequence}`;
|
|
53
|
+
case undefined:
|
|
54
|
+
return `dsh-dlp: telemetryRedaction is enabled, but no session-telemetry backend is mounted, so ${consequence}`;
|
|
55
|
+
default:
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
30
59
|
/** Attribute keys whose values are filesystem paths rather than payload text. */
|
|
31
60
|
const PATH_ATTRIBUTES = ['session.cwd'];
|
|
32
61
|
/** Rule identity recorded when a workspace path is replaced. */
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `dsh-dlp report` — read this plugin's audit JSONL and say what it decided.
|
|
4
|
+
*
|
|
5
|
+
* The sink is the only evidence a decision happened, and nothing read it: a
|
|
6
|
+
* user could not answer "what did this block today?". This command reads the
|
|
7
|
+
* file directly and imports nothing from the harness, so it runs wherever the
|
|
8
|
+
* package is installed, with no profile and no `dsh` on the path.
|
|
9
|
+
*
|
|
10
|
+
* The file is a durable boundary — written by an older version of this
|
|
11
|
+
* package, appended to under crash — so every line is parsed defensively and a
|
|
12
|
+
* line that is not a record is counted rather than trusted.
|
|
13
|
+
* @module dsh-dlp/cli
|
|
14
|
+
*/
|
|
15
|
+
/** One line of the audit file, after the fields this command reads are checked. */
|
|
16
|
+
export interface ReportRecord {
|
|
17
|
+
/** Epoch milliseconds parsed from the record's ISO time. */
|
|
18
|
+
readonly time: number;
|
|
19
|
+
readonly kind: string;
|
|
20
|
+
readonly tool?: string;
|
|
21
|
+
readonly sessionId?: string;
|
|
22
|
+
/** Rule ids named by the record's spans, without repeats. */
|
|
23
|
+
readonly ruleIds: readonly string[];
|
|
24
|
+
/** Invisible-character runs by rule id. */
|
|
25
|
+
readonly unicode: Readonly<Record<string, number>>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse one JSONL line into the fields this command reports on.
|
|
29
|
+
* @param line - one line of the audit file.
|
|
30
|
+
* @returns the record, or `undefined` when the line is not one.
|
|
31
|
+
*/
|
|
32
|
+
export declare function parseRecord(line: string): ReportRecord | undefined;
|
|
33
|
+
/** What `report` was asked for. */
|
|
34
|
+
export interface ReportOptions {
|
|
35
|
+
readonly log: string;
|
|
36
|
+
/** Epoch milliseconds; records before it are left out. */
|
|
37
|
+
readonly since?: number;
|
|
38
|
+
readonly session?: string;
|
|
39
|
+
/** Keep only the decisions that let the call through. */
|
|
40
|
+
readonly wouldHave: boolean;
|
|
41
|
+
}
|
|
42
|
+
/** The outcome of reading the command line. */
|
|
43
|
+
export type Invocation = {
|
|
44
|
+
readonly kind: 'report';
|
|
45
|
+
readonly options: ReportOptions;
|
|
46
|
+
} | {
|
|
47
|
+
readonly kind: 'help';
|
|
48
|
+
} | {
|
|
49
|
+
readonly kind: 'error';
|
|
50
|
+
readonly message: string;
|
|
51
|
+
};
|
|
52
|
+
/** Text printed for `--help` and alongside a usage error. */
|
|
53
|
+
export declare const USAGE: string;
|
|
54
|
+
/**
|
|
55
|
+
* Read `--since`: an ISO timestamp, or a span back from now.
|
|
56
|
+
* @param value - the argument as written.
|
|
57
|
+
* @param now - epoch milliseconds a relative span counts back from.
|
|
58
|
+
* @returns epoch milliseconds, or `undefined` when the value is neither.
|
|
59
|
+
*/
|
|
60
|
+
export declare function parseSince(value: string, now: number): number | undefined;
|
|
61
|
+
/**
|
|
62
|
+
* Read the command line.
|
|
63
|
+
* @param argv - arguments after the program name.
|
|
64
|
+
* @param env - environment used for the default sink path.
|
|
65
|
+
* @param now - epoch milliseconds a relative `--since` counts back from.
|
|
66
|
+
* @returns what to run, or the usage error to print.
|
|
67
|
+
*/
|
|
68
|
+
export declare function parseArguments(argv: readonly string[], env: NodeJS.ProcessEnv, now: number): Invocation;
|
|
69
|
+
/** Outcome of reading the audit file. */
|
|
70
|
+
export type AuditFileRead =
|
|
71
|
+
/** No file at that path: nothing has been recorded, or the sink is elsewhere. */
|
|
72
|
+
{
|
|
73
|
+
readonly kind: 'absent';
|
|
74
|
+
} | {
|
|
75
|
+
readonly kind: 'unreadable';
|
|
76
|
+
readonly problem: string;
|
|
77
|
+
} | {
|
|
78
|
+
readonly kind: 'read';
|
|
79
|
+
readonly records: readonly ReportRecord[];
|
|
80
|
+
/** Lines that were not records; a torn final append is the expected cause. */
|
|
81
|
+
readonly unreadable: number;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Read and parse the audit file.
|
|
85
|
+
* @param path - the file to read.
|
|
86
|
+
* @returns its records, its absence, or the problem to print.
|
|
87
|
+
*/
|
|
88
|
+
export declare function readAuditFile(path: string): AuditFileRead;
|
|
89
|
+
/**
|
|
90
|
+
* Render the report.
|
|
91
|
+
* @param records - every record the file yielded.
|
|
92
|
+
* @param unreadable - how many of its lines were not records.
|
|
93
|
+
* @param options - the filters the invocation asked for.
|
|
94
|
+
* @returns the lines to print.
|
|
95
|
+
*/
|
|
96
|
+
export declare function formatReport(records: readonly ReportRecord[], unreadable: number, options: ReportOptions): string[];
|
|
97
|
+
/**
|
|
98
|
+
* Run one invocation.
|
|
99
|
+
* @param argv - arguments after the program name.
|
|
100
|
+
* @param write - receives each line of output.
|
|
101
|
+
* @param fail - receives each line of error output.
|
|
102
|
+
* @param env - environment used for the default sink path.
|
|
103
|
+
* @param now - epoch milliseconds a relative `--since` counts back from.
|
|
104
|
+
* @returns the process exit code.
|
|
105
|
+
*/
|
|
106
|
+
export declare function main(argv: readonly string[], write: (line: string) => void, fail: (line: string) => void, env?: NodeJS.ProcessEnv, now?: number): number;
|
|
107
|
+
//# sourceMappingURL=cli.d.ts.map
|
package/lib/types/detectors.d.ts
CHANGED
|
@@ -37,6 +37,13 @@ export interface Detection {
|
|
|
37
37
|
readonly start: number;
|
|
38
38
|
/** Exclusive end offset into the scanned string. */
|
|
39
39
|
readonly end: number;
|
|
40
|
+
/**
|
|
41
|
+
* Set when the offsets cover exactly what must be replaced, so redaction
|
|
42
|
+
* must not widen them to the surrounding delimiters. Only the Unicode
|
|
43
|
+
* indicators set it: their matches are the characters themselves, while a
|
|
44
|
+
* secret's reported span is advisory and verified to under-cover (ADR §4).
|
|
45
|
+
*/
|
|
46
|
+
readonly exact?: true;
|
|
40
47
|
}
|
|
41
48
|
/** Outcome of one scan. */
|
|
42
49
|
export interface ScanResult {
|
|
@@ -59,11 +66,70 @@ export interface SyncRule {
|
|
|
59
66
|
* tier 2, where a false positive costs a redaction rather than a denial.
|
|
60
67
|
*/
|
|
61
68
|
export declare const SYNC_RULES: readonly SyncRule[];
|
|
69
|
+
/**
|
|
70
|
+
* What the scan does with one class of invisible or direction-changing
|
|
71
|
+
* characters.
|
|
72
|
+
*
|
|
73
|
+
* `strip` classes have no legitimate use in tool output, so they are replaced
|
|
74
|
+
* like any other detection. `report` classes do: `U+200D` joins an emoji
|
|
75
|
+
* sequence and a variation selector chooses a glyph, so replacing them would
|
|
76
|
+
* corrupt ordinary text. They are counted and never rewritten.
|
|
77
|
+
*/
|
|
78
|
+
export type UnicodeAction = 'strip' | 'report';
|
|
79
|
+
/** One class of invisible or direction-changing characters. */
|
|
80
|
+
export interface UnicodeRule {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly version: number;
|
|
83
|
+
readonly severity: Severity;
|
|
84
|
+
readonly action: UnicodeAction;
|
|
85
|
+
/** Global, unicode-flagged, matching one run of this class. */
|
|
86
|
+
readonly pattern: RegExp;
|
|
87
|
+
/** The class's ranges as a character-class body, for the combined run pattern. */
|
|
88
|
+
readonly ranges: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Character classes that hide text from the reader while the model still reads
|
|
92
|
+
* it, verified against the Unicode character database.
|
|
93
|
+
*
|
|
94
|
+
* Every class is `medium`. These are injection *indicators*, not credentials:
|
|
95
|
+
* the guard floor denies at `high` and above, so an argument carrying one is
|
|
96
|
+
* never denied on that basis. What they buy is a redaction and an audit record
|
|
97
|
+
* on a path the harness does not cover — it strips directional controls in
|
|
98
|
+
* exactly one place, session titles, and never on the tool-result path.
|
|
99
|
+
*
|
|
100
|
+
* Not attempted here: UTS #39 confusables. A Cyrillic `а` needs a data table
|
|
101
|
+
* to detect and is a different cost class, and it defeats every rule in this
|
|
102
|
+
* file. README.md says so rather than implying coverage.
|
|
103
|
+
*/
|
|
104
|
+
export declare const UNICODE_RULES: readonly UnicodeRule[];
|
|
105
|
+
/** One indicator match and what the caller should do with it. */
|
|
106
|
+
export interface UnicodeFinding extends Detection {
|
|
107
|
+
readonly action: UnicodeAction;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Find every invisible or direction-changing character in one string.
|
|
111
|
+
*
|
|
112
|
+
* Offsets are UTF-16 indices into `text`, so a caller can splice them
|
|
113
|
+
* directly; they are exact rather than advisory, and {@link Detection.exact}
|
|
114
|
+
* says so.
|
|
115
|
+
* @param text - the string to scan.
|
|
116
|
+
* @returns every indicator run, ordered by start offset.
|
|
117
|
+
*/
|
|
118
|
+
export declare function scanUnicode(text: string): UnicodeFinding[];
|
|
119
|
+
/**
|
|
120
|
+
* How many runs of each indicator class one string carries.
|
|
121
|
+
* @param text - the string to scan.
|
|
122
|
+
* @returns a count per rule id; absent means none were found.
|
|
123
|
+
*/
|
|
124
|
+
export declare function countUnicodeIndicators(text: string): Record<string, number>;
|
|
62
125
|
/**
|
|
63
126
|
* Scan text with tier 1. Pure, synchronous, no I/O, and never capped: a table
|
|
64
127
|
* of anchored regular expressions costs a linear pass, so there is no reason
|
|
65
128
|
* to stop scanning where tier 2 has to. `truncated` is therefore always
|
|
66
129
|
* `false` here and only tier 2 can set it.
|
|
130
|
+
*
|
|
131
|
+
* The `strip` half of {@link UNICODE_RULES} is included, so every seam reading
|
|
132
|
+
* tier 1 — including the synchronous telemetry waterfall — gets it.
|
|
67
133
|
* @param text - the string to scan.
|
|
68
134
|
* @param rules - the rule table to apply; defaults to {@link SYNC_RULES}.
|
|
69
135
|
* @returns every match, ordered by start offset.
|
package/lib/types/guard.d.ts
CHANGED
|
@@ -43,10 +43,12 @@ export interface GuardVerdict {
|
|
|
43
43
|
* Credential paths are denied for every tool, not only readers: a shell that
|
|
44
44
|
* can `cat` a key can also copy it. Only path-typed arguments are tested —
|
|
45
45
|
* running the table over every string matches file content and denies writing
|
|
46
|
-
* a `.gitignore` that mentions `.env`.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
46
|
+
* a `.gitignore` that mentions `.env`. The one exception is a `writes-only`
|
|
47
|
+
* rule, which a tool classified read-only is exempt from; that is how
|
|
48
|
+
* `$DSH_HOME` stays readable while every write to it is denied. Argument
|
|
49
|
+
* secrets are denied only for egress-capable tools, because denying a local
|
|
50
|
+
* editor for holding the text it was asked to write would break ordinary work
|
|
51
|
+
* without closing an exfiltration path.
|
|
50
52
|
* @param exec - the pending call as the guard stage sees it.
|
|
51
53
|
* @param policy - the effective policy after the tighten-only merge.
|
|
52
54
|
* @param hasher - mints the keyed hashes quoted in a denial reason.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the harness keeps its state, and where this plugin's audit sink lands
|
|
3
|
+
* by default.
|
|
4
|
+
*
|
|
5
|
+
* Its own module so the `dsh-dlp report` command can resolve the sink without
|
|
6
|
+
* importing the plugin: `policy.ts` pulls in `@secretlint/core`, `js-yaml` and
|
|
7
|
+
* the schema library, none of which a reader of a JSONL file needs.
|
|
8
|
+
* @module dsh-dlp/home
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* File name the bundle patch gives the audit sink under the harness home.
|
|
12
|
+
* `cordis.patch.yml` spells the same name; a deployment that sets `auditLog`
|
|
13
|
+
* itself must tell `dsh-dlp report` where it put it.
|
|
14
|
+
*/
|
|
15
|
+
export declare const DEFAULT_AUDIT_LOG_NAME = "dsh-dlp.audit.jsonl";
|
|
16
|
+
/**
|
|
17
|
+
* Resolve the harness home the same way the harness does: `$DSH_HOME` when it
|
|
18
|
+
* is set to something other than whitespace, otherwise `~/.dsh`. Read here
|
|
19
|
+
* rather than through `@deepseek-ai/dsh-home-paths` to keep the plugin's
|
|
20
|
+
* runtime imports to the ones a profile is guaranteed to resolve.
|
|
21
|
+
* @param env - environment consulted for `DSH_HOME`; defaults to `process.env`.
|
|
22
|
+
* @returns the absolute harness home.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resolveDshHome(env?: NodeJS.ProcessEnv): string;
|
|
25
|
+
/**
|
|
26
|
+
* Where the audit sink sits when the deployment did not name one.
|
|
27
|
+
* @param env - environment consulted for `DSH_HOME`; defaults to `process.env`.
|
|
28
|
+
* @returns the absolute path the bundle patch configures.
|
|
29
|
+
*/
|
|
30
|
+
export declare function defaultAuditLog(env?: NodeJS.ProcessEnv): string;
|
|
31
|
+
//# sourceMappingURL=home.d.ts.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Neutralising remote markdown images in assistant output, on the `llm/stream`
|
|
3
|
+
* waterfall.
|
|
4
|
+
*
|
|
5
|
+
* The web UI renders any absolute `http:`/`https:` markdown image a model
|
|
6
|
+
* emits as a real `<img src>`, and the harness sets no Content-Security-Policy,
|
|
7
|
+
* so the fetch happens in the user's browser where no host-side listener can
|
|
8
|
+
* see it. This module rewrites the destination out of the assistant's text
|
|
9
|
+
* before it becomes an `assistant/chunk` or `assistant/message` session event,
|
|
10
|
+
* so the log and the rendered answer stay in agreement.
|
|
11
|
+
*
|
|
12
|
+
* Two properties this module exists to hold:
|
|
13
|
+
*
|
|
14
|
+
* - **A destination split across chunks is still caught.** The mock and real
|
|
15
|
+
* adapters both emit text in small deltas, so `` is
|
|
16
|
+
* routinely spread over several of them and the browser renders the
|
|
17
|
+
* accumulation. Text that could still be the start of an image is held back
|
|
18
|
+
* until it either completes or exceeds {@link MAX_HELD_CHARACTERS}.
|
|
19
|
+
* - **Only the destination is replaced.** The alt text survives, so the
|
|
20
|
+
* sentence the model wrote still reads, and the renderer's own
|
|
21
|
+
* non-absolute-URL arm shows that alt text instead of fetching anything.
|
|
22
|
+
*
|
|
23
|
+
* This does not close the channel. It matches inline image syntax only:
|
|
24
|
+
* reference-style images, an alt text carrying a `]`, and any destination form
|
|
25
|
+
* the pattern does not model still reach the renderer. Raw HTML needs no
|
|
26
|
+
* handling — the renderer keeps it as literal text and no HTML enters the DOM
|
|
27
|
+
* (`packages/client/ui-primitives/src/markdown/render.tsx:261-263`). The
|
|
28
|
+
* upstream fix is one `img-src` directive.
|
|
29
|
+
* @module dsh-dlp/images
|
|
30
|
+
*/
|
|
31
|
+
import type { StreamChunk } from '@deepseek-ai/dsh-llm';
|
|
32
|
+
/**
|
|
33
|
+
* Destination substituted for a remote image URL.
|
|
34
|
+
*
|
|
35
|
+
* It is deliberately not a URL: `new URL()` throws on it, which is the
|
|
36
|
+
* renderer's own "not an absolute destination" arm, and that arm renders the
|
|
37
|
+
* alt text as a `<span>` instead of emitting an `<img>`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const BLOCKED_IMAGE_DESTINATION = "dsh-dlp-blocked-remote-image";
|
|
40
|
+
/**
|
|
41
|
+
* Longest suffix held back waiting for an image to complete.
|
|
42
|
+
*
|
|
43
|
+
* Held text is text the user cannot see yet, so the wait is bounded: past this
|
|
44
|
+
* many characters the suffix is emitted as it stands and a destination that
|
|
45
|
+
* completes later is caught only by the assembled block. A protocol bound on
|
|
46
|
+
* this module's own buffering, not a deployment choice.
|
|
47
|
+
*/
|
|
48
|
+
export declare const MAX_HELD_CHARACTERS = 4096;
|
|
49
|
+
/** One string after its remote image destinations were replaced. */
|
|
50
|
+
export interface NeutralizedText {
|
|
51
|
+
readonly text: string;
|
|
52
|
+
/** Hostnames of the replaced destinations, in match order; never the full URL. */
|
|
53
|
+
readonly hosts: readonly string[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Replace every absolute HTTP(S) inline image destination in one string.
|
|
57
|
+
* @param text - assistant text, whole or partial.
|
|
58
|
+
* @returns the rewritten text and the hosts whose destinations were replaced.
|
|
59
|
+
*/
|
|
60
|
+
export declare function neutralizeRemoteImages(text: string): NeutralizedText;
|
|
61
|
+
/**
|
|
62
|
+
* Where the held suffix of a partially streamed string starts.
|
|
63
|
+
* @param text - everything accumulated for one block and not yet emitted.
|
|
64
|
+
* @returns the offset to emit up to; the string's length when nothing is held.
|
|
65
|
+
*/
|
|
66
|
+
export declare function heldSuffixStart(text: string): number;
|
|
67
|
+
/**
|
|
68
|
+
* Wrap one model stream, replacing remote image destinations in its text.
|
|
69
|
+
*
|
|
70
|
+
* Text deltas are rewritten as they pass, with a possible image start held
|
|
71
|
+
* back until it resolves, and the assembled block on `block-end` — which is
|
|
72
|
+
* what the agent loop turns into the assistant message — is rewritten too. A
|
|
73
|
+
* held suffix is always flushed as a delta before the block closes and before
|
|
74
|
+
* the terminal finish, so no text is lost and the emitted chunks still satisfy
|
|
75
|
+
* the stream grammar.
|
|
76
|
+
* @param source - the stream from the rest of the waterfall.
|
|
77
|
+
* @param onNeutralized - notified once per host per text block.
|
|
78
|
+
* @returns the rewritten stream.
|
|
79
|
+
*/
|
|
80
|
+
export declare function neutralizeImageStream(source: AsyncIterable<StreamChunk>, onNeutralized: (host: string) => void): AsyncIterable<StreamChunk>;
|
|
81
|
+
//# sourceMappingURL=images.d.ts.map
|
package/lib/types/index.d.ts
CHANGED
|
@@ -14,6 +14,15 @@
|
|
|
14
14
|
* a result that cannot be cleaned is withheld rather than accepted.
|
|
15
15
|
* 4. `session-telemetry/record` — fail-closed redaction of exported telemetry,
|
|
16
16
|
* reaching tier 1 only because the waterfall is synchronous.
|
|
17
|
+
* 5. `llm/stream` — neutralising remote markdown image destinations in
|
|
18
|
+
* assistant output, before the text becomes a session event.
|
|
19
|
+
*
|
|
20
|
+
* Three of those registrations mitigate defects in the harness rather than in
|
|
21
|
+
* a deployment's own configuration: the missing Content-Security-Policy behind
|
|
22
|
+
* (5), the mutable execution object behind the guard's mutation check, and the
|
|
23
|
+
* silently inert telemetry seam behind the notice reported at mount. Each one
|
|
24
|
+
* is partial, none closes its channel, and README.md says so beside the
|
|
25
|
+
* feature.
|
|
17
26
|
*
|
|
18
27
|
* This plugin is not a containment boundary. It runs in-process at the agent's
|
|
19
28
|
* own uid; anything the agent can execute can read the same files the guard
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detecting a tool call that was rewritten after it was logged.
|
|
3
|
+
*
|
|
4
|
+
* The registry deep-freezes `exec.arguments` but does not freeze the execution
|
|
5
|
+
* object until results are notified, so a `tools/pre-execute` listener can
|
|
6
|
+
* reassign `exec.arguments` or `exec.name` — and reassigning `exec.name`
|
|
7
|
+
* changes which tool body runs. The agent loop appended `tool/call` from the
|
|
8
|
+
* model's own response block before the waterfall ran, so nothing in the
|
|
9
|
+
* session log records the change: the durable record then describes a
|
|
10
|
+
* different call than the one about to execute.
|
|
11
|
+
*
|
|
12
|
+
* This module snapshots the call as early in the waterfall as it can and
|
|
13
|
+
* compares in the guard, which runs after the whole waterfall and cannot be
|
|
14
|
+
* out-ordered. It is detection, not prevention: preventing the rewrite would
|
|
15
|
+
* mean freezing an object this plugin does not own, and the snapshot itself is
|
|
16
|
+
* best-effort — a later `{ prepend: true }` registration runs ahead of ours and
|
|
17
|
+
* would be snapshotted after its own rewrite.
|
|
18
|
+
* @module dsh-dlp/mutation
|
|
19
|
+
*/
|
|
20
|
+
import type { ToolExecution } from '@deepseek-ai/dsh-tools';
|
|
21
|
+
import type { SpanHasher } from './redaction.ts';
|
|
22
|
+
/** The parts of an execution this module compares. */
|
|
23
|
+
type Comparable = Pick<ToolExecution, 'name' | 'arguments'>;
|
|
24
|
+
/** One rewritten field. */
|
|
25
|
+
export type MutatedField = 'name' | 'arguments';
|
|
26
|
+
/** A call whose identity changed between the snapshot and the guard. */
|
|
27
|
+
export interface ExecutionMutation {
|
|
28
|
+
/** Which fields differ, in a stable order. */
|
|
29
|
+
readonly fields: readonly MutatedField[];
|
|
30
|
+
/** The tool name the session log recorded. */
|
|
31
|
+
readonly originalTool: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Render a JSON value with object keys in a fixed order, so two equal argument
|
|
35
|
+
* sets hash equally whatever order a listener rebuilt them in.
|
|
36
|
+
* @param value - the argument value; JSON-serializable by the registry's own snapshot step.
|
|
37
|
+
* @returns a canonical string for hashing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function canonicalJson(value: unknown): string;
|
|
40
|
+
/**
|
|
41
|
+
* Remembers what each pending call looked like before the rest of the
|
|
42
|
+
* `tools/pre-execute` waterfall ran.
|
|
43
|
+
*
|
|
44
|
+
* Keyed by the execution object's identity in a `WeakMap`, the way the
|
|
45
|
+
* registry keys its own per-execution state, so the entry is found again in
|
|
46
|
+
* the guard and released with the execution.
|
|
47
|
+
*/
|
|
48
|
+
export declare class ExecutionSnapshots {
|
|
49
|
+
#private;
|
|
50
|
+
/**
|
|
51
|
+
* @param hasher - mints the keyed digest of the arguments; the values themselves are never stored.
|
|
52
|
+
*/
|
|
53
|
+
constructor(hasher: SpanHasher);
|
|
54
|
+
/**
|
|
55
|
+
* Snapshot one pending call.
|
|
56
|
+
* @param exec - the execution as the earliest listener sees it.
|
|
57
|
+
*/
|
|
58
|
+
record(exec: Comparable): void;
|
|
59
|
+
/**
|
|
60
|
+
* Compare one call against its snapshot.
|
|
61
|
+
*
|
|
62
|
+
* A call with no snapshot is not a finding: the listener may never have run
|
|
63
|
+
* for it, and reporting absence as mutation would deny calls this plugin
|
|
64
|
+
* simply did not observe.
|
|
65
|
+
* @param exec - the execution as the guard stage sees it.
|
|
66
|
+
* @returns what changed, or `undefined` when nothing did.
|
|
67
|
+
*/
|
|
68
|
+
detect(exec: Comparable): ExecutionMutation | undefined;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Denial text for a rewritten call.
|
|
72
|
+
*
|
|
73
|
+
* Both tool names are named: a tool name is already in the session log and in
|
|
74
|
+
* every other denial this plugin writes, and naming them is the whole point —
|
|
75
|
+
* the operator needs to know which call the log describes and which one was
|
|
76
|
+
* about to run. No argument value appears.
|
|
77
|
+
* @param exec - the call as the guard sees it, after the rewrite.
|
|
78
|
+
* @param mutation - the fields that changed and the recorded tool name.
|
|
79
|
+
* @returns the model-facing reason.
|
|
80
|
+
*/
|
|
81
|
+
export declare function mutationReason(exec: Comparable, mutation: ExecutionMutation): string;
|
|
82
|
+
export {};
|
|
83
|
+
//# sourceMappingURL=mutation.d.ts.map
|
package/lib/types/paths.d.ts
CHANGED
|
@@ -6,12 +6,25 @@
|
|
|
6
6
|
* the repo-local policy tier may only add to them.
|
|
7
7
|
* @module dsh-dlp/paths
|
|
8
8
|
*/
|
|
9
|
+
/**
|
|
10
|
+
* Which calls a credential-path rule is enforced for.
|
|
11
|
+
*
|
|
12
|
+
* `every-call` is the default and the only setting a rule protecting
|
|
13
|
+
* credential *contents* may use. `writes-only` exempts the tools
|
|
14
|
+
* {@link READ_ONLY_TOOLS} classifies as unable to change anything: it exists
|
|
15
|
+
* for a directory whose contents are ordinary work to read and dangerous to
|
|
16
|
+
* modify, which is `$DSH_HOME` — it holds the installed plugin tree and every
|
|
17
|
+
* profile's `cordis.yml`.
|
|
18
|
+
*/
|
|
19
|
+
export type RuleEnforcement = 'every-call' | 'writes-only';
|
|
9
20
|
/** One credential-path pattern. */
|
|
10
21
|
export interface CredentialPathRule {
|
|
11
22
|
readonly id: string;
|
|
12
23
|
readonly version: number;
|
|
13
24
|
/** Matched against a normalized, forward-slash path. */
|
|
14
25
|
readonly pattern: RegExp;
|
|
26
|
+
/** Defaults to `every-call`; the repo-local tier cannot set it. */
|
|
27
|
+
readonly enforcement?: RuleEnforcement;
|
|
15
28
|
}
|
|
16
29
|
/**
|
|
17
30
|
* Paths whose contents are credentials. Reading any of them through a tool is
|
|
@@ -120,4 +133,29 @@ export declare const LOCAL_TOOLS: ReadonlySet<string>;
|
|
|
120
133
|
* @returns `true` when the tool is not a known local-only tool.
|
|
121
134
|
*/
|
|
122
135
|
export declare function isEgressCapable(toolName: string, extraEgressTools?: ReadonlySet<string>): boolean;
|
|
136
|
+
/**
|
|
137
|
+
* Tools that can only look: they query the filesystem, the language server,
|
|
138
|
+
* the session store or a running job, and have no operation that changes
|
|
139
|
+
* anything.
|
|
140
|
+
*
|
|
141
|
+
* A `writes-only` rule is lifted for these names and for no others. Every
|
|
142
|
+
* shell, `run_code`, every editor, every `mcp__*` tool and any tool this build
|
|
143
|
+
* has never heard of stays on the deny side, so a new tool is denied until it
|
|
144
|
+
* is classified — the same default as {@link LOCAL_TOOLS}, in the same
|
|
145
|
+
* direction.
|
|
146
|
+
*/
|
|
147
|
+
export declare const READ_ONLY_TOOLS: ReadonlySet<string>;
|
|
148
|
+
/**
|
|
149
|
+
* Whether a tool is known to be incapable of changing anything.
|
|
150
|
+
* @param toolName - the executing tool's registered name.
|
|
151
|
+
* @returns `true` only for a name in {@link READ_ONLY_TOOLS}.
|
|
152
|
+
*/
|
|
153
|
+
export declare function isReadOnlyTool(toolName: string): boolean;
|
|
154
|
+
/**
|
|
155
|
+
* The credential-path rules one tool is judged against.
|
|
156
|
+
* @param toolName - the executing tool's registered name.
|
|
157
|
+
* @param rules - the effective rule table.
|
|
158
|
+
* @returns every rule, minus the `writes-only` ones for a read-only tool.
|
|
159
|
+
*/
|
|
160
|
+
export declare function rulesForTool(toolName: string, rules: readonly CredentialPathRule[]): readonly CredentialPathRule[];
|
|
123
161
|
//# sourceMappingURL=paths.d.ts.map
|
package/lib/types/policy.d.ts
CHANGED
|
@@ -33,12 +33,14 @@ export interface Config {
|
|
|
33
33
|
resultRedaction: boolean;
|
|
34
34
|
/** Whether `session-telemetry/record` redaction runs. */
|
|
35
35
|
telemetryRedaction: boolean;
|
|
36
|
+
/** Whether remote markdown image destinations are neutralised in assistant output. */
|
|
37
|
+
remoteImageNeutralization: boolean;
|
|
36
38
|
/** Whether telemetry's `session.cwd` attribute is replaced with a keyed hash. */
|
|
37
39
|
redactTelemetryWorkspacePaths: boolean;
|
|
38
40
|
}
|
|
39
41
|
export declare const Config: z<Config>;
|
|
40
42
|
/** Config toggles a repo-local policy may switch on, and never off. */
|
|
41
|
-
declare const ENABLEABLE: readonly ["breadthTier", "resultRedaction", "telemetryRedaction", "redactTelemetryWorkspacePaths"];
|
|
43
|
+
declare const ENABLEABLE: readonly ["breadthTier", "resultRedaction", "telemetryRedaction", "remoteImageNeutralization", "redactTelemetryWorkspacePaths"];
|
|
42
44
|
/** One toggle name a repo-local policy may name in `enable`. */
|
|
43
45
|
export type EnableableToggle = typeof ENABLEABLE[number];
|
|
44
46
|
/** Payload version this package writes and accepts for repo-local policy files. */
|
|
@@ -59,6 +61,7 @@ export interface ResolvedPolicy {
|
|
|
59
61
|
readonly breadthTier: boolean;
|
|
60
62
|
readonly resultRedaction: boolean;
|
|
61
63
|
readonly telemetryRedaction: boolean;
|
|
64
|
+
readonly remoteImageNeutralization: boolean;
|
|
62
65
|
readonly redactTelemetryWorkspacePaths: boolean;
|
|
63
66
|
}
|
|
64
67
|
/** Thrown when a policy file is malformed or attempts to loosen the policy. */
|
|
@@ -108,15 +111,6 @@ export type RepoPolicyLoad =
|
|
|
108
111
|
* @returns the validated policy, its absence, or the problem to report.
|
|
109
112
|
*/
|
|
110
113
|
export declare function loadRepoPolicy(path: string): RepoPolicyLoad;
|
|
111
|
-
/**
|
|
112
|
-
* Resolve the harness home the same way the harness does: `$DSH_HOME` when it
|
|
113
|
-
* is set to something other than whitespace, otherwise `~/.dsh`. Read here
|
|
114
|
-
* rather than through `@deepseek-ai/dsh-home-paths` to keep the plugin's
|
|
115
|
-
* runtime imports to the ones a profile is guaranteed to resolve.
|
|
116
|
-
* @param env - environment consulted for `DSH_HOME`; defaults to `process.env`.
|
|
117
|
-
* @returns the absolute harness home.
|
|
118
|
-
*/
|
|
119
|
-
export declare function resolveDshHome(env?: NodeJS.ProcessEnv): string;
|
|
120
114
|
/**
|
|
121
115
|
* Merge the deployment config with an optional repo-local policy.
|
|
122
116
|
* @param config - the deployment-controlled configuration.
|
package/lib/types/results.d.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* whole rule set applies here and not in the guard.
|
|
11
11
|
* @module dsh-dlp/results
|
|
12
12
|
*/
|
|
13
|
-
import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';
|
|
13
|
+
import type { JsonSchemaNode, PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';
|
|
14
14
|
import type { ResolvedPolicy } from './policy.ts';
|
|
15
15
|
import { type RedactedSpan, type SpanHasher } from './redaction.ts';
|
|
16
16
|
/** What a redaction pass produced, before an arm is chosen. */
|
|
@@ -18,6 +18,13 @@ export interface ResultRedaction {
|
|
|
18
18
|
readonly decision: PostToolDecision;
|
|
19
19
|
readonly spans: readonly RedactedSpan[];
|
|
20
20
|
readonly truncatedScan: boolean;
|
|
21
|
+
/**
|
|
22
|
+
* Runs of each invisible-character class the result carried, by rule id.
|
|
23
|
+
* The `strip` classes also appear in `spans`; the `report` classes appear
|
|
24
|
+
* only here, because rewriting them would corrupt legitimate emoji and
|
|
25
|
+
* right-to-left text.
|
|
26
|
+
*/
|
|
27
|
+
readonly indicators: Readonly<Record<string, number>>;
|
|
21
28
|
}
|
|
22
29
|
/**
|
|
23
30
|
* Redact whatever the downstream decision settled on.
|
|
@@ -36,10 +43,11 @@ export interface ResultRedaction {
|
|
|
36
43
|
* carries a secret, or a value that still scans dirty after redaction.
|
|
37
44
|
* Blocking replaces the whole result, which is the only way to drop `meta`.
|
|
38
45
|
*
|
|
39
|
-
* Replacing the value
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
46
|
+
* Replacing the value is re-validated by the registry against the tool's
|
|
47
|
+
* `output.schema`, and a schema that pins the redacted string rejects it. That
|
|
48
|
+
* surfaces as a `ToolOutputError` naming a validation failure, which tells the
|
|
49
|
+
* model nothing it can act on, so the schema is checked here first and the
|
|
50
|
+
* result is withheld with this plugin's own explanation instead.
|
|
43
51
|
*
|
|
44
52
|
* A downstream `accept{content}` over a dirty value is overruled by the value
|
|
45
53
|
* arm, which discards that listener's presentation choice. Keeping it would
|
|
@@ -49,9 +57,10 @@ export interface ResultRedaction {
|
|
|
49
57
|
* @param result - the dispatch outcome the waterfall was called with.
|
|
50
58
|
* @param policy - the effective policy.
|
|
51
59
|
* @param hasher - mints each span's keyed hash.
|
|
60
|
+
* @param outputSchema - the executing tool's declared output schema, when one could be resolved.
|
|
52
61
|
* @returns the decision to return, the spans replaced, and scan completeness.
|
|
53
62
|
*/
|
|
54
|
-
export declare function redactDecision(decision: PostToolDecision, result: Readonly<ToolExecutionResult>, policy: ResolvedPolicy, hasher: SpanHasher): Promise<ResultRedaction>;
|
|
63
|
+
export declare function redactDecision(decision: PostToolDecision, result: Readonly<ToolExecutionResult>, policy: ResolvedPolicy, hasher: SpanHasher, outputSchema?: JsonSchemaNode): Promise<ResultRedaction>;
|
|
55
64
|
/**
|
|
56
65
|
* Decide whether the breadth tier denies one call before dispatch.
|
|
57
66
|
*
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a redacted value still satisfies the tool's declared `output.schema`.
|
|
3
|
+
*
|
|
4
|
+
* Replacing a canonical value makes the registry re-validate it, so a schema
|
|
5
|
+
* that pins the redacted string turns a redaction into a `ToolOutputError` the
|
|
6
|
+
* model cannot act on. Asking the question first lets the listener withhold
|
|
7
|
+
* the result with its own explanation instead.
|
|
8
|
+
*
|
|
9
|
+
* This is a re-implementation of the harness's own check rather than a call
|
|
10
|
+
* into it: every harness type this package uses is imported with `import
|
|
11
|
+
* type`, so nothing from `@deepseek-ai/dsh-*` is emitted as a runtime import
|
|
12
|
+
* and the plugin resolves from a profile directory that has none of them
|
|
13
|
+
* installed. The enforced subset is small — `type`, `oneOf`, `properties`,
|
|
14
|
+
* `required`, `additionalProperties`, `items`, `enum`, `const` — and the
|
|
15
|
+
* caller guards against any disagreement by checking the *original* value
|
|
16
|
+
* first: a value this module rejects before redaction means the answer cannot
|
|
17
|
+
* be trusted, and the redaction proceeds as it did before.
|
|
18
|
+
* @module dsh-dlp/schema
|
|
19
|
+
*/
|
|
20
|
+
import type { JsonSchemaNode } from '@deepseek-ai/dsh-tools';
|
|
21
|
+
/**
|
|
22
|
+
* Whether one value satisfies one schema node.
|
|
23
|
+
* @param node - a node of the tool's declared output schema.
|
|
24
|
+
* @param value - the candidate value.
|
|
25
|
+
* @returns `true` when the registry's own validation would accept it.
|
|
26
|
+
*/
|
|
27
|
+
export declare function satisfiesJsonSchema(node: JsonSchemaNode, value: unknown): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Whether replacing a value with its redacted copy would fail the tool's
|
|
30
|
+
* output validation.
|
|
31
|
+
*
|
|
32
|
+
* A schema this module already rejects for the original value is one it does
|
|
33
|
+
* not model correctly, so the answer is `false` and the registry decides — the
|
|
34
|
+
* check can withhold a result, and it must never do so on its own confusion.
|
|
35
|
+
* @param schema - the tool's declared output schema, when one could be resolved.
|
|
36
|
+
* @param original - the value the tool produced.
|
|
37
|
+
* @param redacted - the value the redaction pass produced.
|
|
38
|
+
* @returns `true` only when the original validates and the redacted one does not.
|
|
39
|
+
*/
|
|
40
|
+
export declare function redactionBreaksSchema(schema: JsonSchemaNode | undefined, original: unknown, redacted: unknown): boolean;
|
|
41
|
+
//# sourceMappingURL=schema.d.ts.map
|