dsh-dlp 0.1.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/LICENSE +21 -0
- package/README.md +324 -0
- package/SECURITY.md +47 -0
- package/cordis.patch.yml +22 -0
- package/lib/detectors.js +148 -0
- package/lib/guard.js +133 -0
- package/lib/index.js +212 -0
- package/lib/paths.js +227 -0
- package/lib/policy.js +274 -0
- package/lib/redaction.js +216 -0
- package/lib/results.js +238 -0
- package/lib/sink.js +105 -0
- package/lib/telemetry.js +80 -0
- package/lib/types/detectors.d.ts +102 -0
- package/lib/types/guard.d.ts +68 -0
- package/lib/types/index.d.ts +47 -0
- package/lib/types/paths.d.ts +123 -0
- package/lib/types/policy.d.ts +128 -0
- package/lib/types/redaction.d.ts +113 -0
- package/lib/types/results.d.ts +71 -0
- package/lib/types/sink.d.ts +118 -0
- package/lib/types/telemetry.d.ts +51 -0
- package/package.json +84 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two classifications the guard floor runs on: which paths are credential
|
|
3
|
+
* material, and which tools can move data off the machine.
|
|
4
|
+
*
|
|
5
|
+
* Both tables are security invariants. Neither is settable from `cordis.yml`;
|
|
6
|
+
* the repo-local policy tier may only add to them.
|
|
7
|
+
* @module dsh-dlp/paths
|
|
8
|
+
*/
|
|
9
|
+
/** One credential-path pattern. */
|
|
10
|
+
export interface CredentialPathRule {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly version: number;
|
|
13
|
+
/** Matched against a normalized, forward-slash path. */
|
|
14
|
+
readonly pattern: RegExp;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Paths whose contents are credentials. Reading any of them through a tool is
|
|
18
|
+
* denied unconditionally.
|
|
19
|
+
*
|
|
20
|
+
* Order matters only for reporting: the first match wins, so the specific
|
|
21
|
+
* rules precede the filename heuristic and an operator sees the precise rule
|
|
22
|
+
* id in the audit record.
|
|
23
|
+
*
|
|
24
|
+
* `$DSH_HOME/.credentials.yaml` is here because core permits it: the harness
|
|
25
|
+
* has no file-read restriction in any mode ("Reads pass through untouched:
|
|
26
|
+
* every mode permits reading"), so the provider token the agent itself
|
|
27
|
+
* authenticates with is agent-readable. That is the gap this table closes.
|
|
28
|
+
*/
|
|
29
|
+
export declare const CREDENTIAL_PATH_RULES: readonly CredentialPathRule[];
|
|
30
|
+
/**
|
|
31
|
+
* Normalize one candidate path for matching: Windows separators become
|
|
32
|
+
* forward slashes, surrounding quotes come off, `~` expands to a
|
|
33
|
+
* root-anchored marker, `..` segments collapse so a traversal cannot hide a
|
|
34
|
+
* credential path from the table, and a trailing slash is dropped so `.env/`
|
|
35
|
+
* matches the same rule as `.env`.
|
|
36
|
+
* @param candidate - a raw string from tool arguments.
|
|
37
|
+
* @returns the normalized form the rules are matched against.
|
|
38
|
+
*/
|
|
39
|
+
export declare function normalizeCandidatePath(candidate: string): string;
|
|
40
|
+
/**
|
|
41
|
+
* Whether one string names credential material. Pure: it never touches the
|
|
42
|
+
* filesystem, so a symlink is matched by the name it was given. The guard
|
|
43
|
+
* calls {@link matchPathArgument} instead, which resolves first.
|
|
44
|
+
* @param candidate - a raw string from tool arguments.
|
|
45
|
+
* @param rules - the rule table; defaults to {@link CREDENTIAL_PATH_RULES}.
|
|
46
|
+
* @returns the first matching rule, or `undefined`.
|
|
47
|
+
*/
|
|
48
|
+
export declare function matchCredentialPath(candidate: string, rules?: readonly CredentialPathRule[]): CredentialPathRule | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve one candidate through the filesystem so a symlink cannot rename a
|
|
51
|
+
* credential file out of the table.
|
|
52
|
+
* @param candidate - a raw string from tool arguments.
|
|
53
|
+
* @returns the canonical path, or `undefined` when it does not resolve.
|
|
54
|
+
*/
|
|
55
|
+
export declare function resolveCandidatePath(candidate: string): string | undefined;
|
|
56
|
+
/**
|
|
57
|
+
* Match one path-typed argument against the credential table, by the name the
|
|
58
|
+
* caller used and by what that name resolves to.
|
|
59
|
+
* @param candidate - a raw string from a path-typed tool argument.
|
|
60
|
+
* @param rules - the rule table; defaults to {@link CREDENTIAL_PATH_RULES}.
|
|
61
|
+
* @returns the first matching rule, or `undefined`.
|
|
62
|
+
*/
|
|
63
|
+
export declare function matchPathArgument(candidate: string, rules?: readonly CredentialPathRule[]): CredentialPathRule | undefined;
|
|
64
|
+
/**
|
|
65
|
+
* Split one shell command into the substrings worth testing as paths: the
|
|
66
|
+
* whole string, plus each shell-ish token. A `bash` command is a single string
|
|
67
|
+
* argument, so without tokenization `cat ~/.ssh/id_rsa && echo ok` would not
|
|
68
|
+
* match a rule anchored at the end of a path.
|
|
69
|
+
*
|
|
70
|
+
* This is advisory only. A shell command is a program, not a path, and any
|
|
71
|
+
* quoting, globbing, substitution or encoding defeats the split — see the
|
|
72
|
+
* "what this is not" section of README.md.
|
|
73
|
+
* @param text - one shell command line taken from tool arguments.
|
|
74
|
+
* @returns the whole string followed by its tokens.
|
|
75
|
+
*/
|
|
76
|
+
export declare function pathCandidates(text: string): string[];
|
|
77
|
+
/**
|
|
78
|
+
* Argument keys whose values name filesystem paths.
|
|
79
|
+
*
|
|
80
|
+
* The guard tests these and nothing else. Running the credential-path table
|
|
81
|
+
* over every string argument matches file *content* — a `.gitignore` listing
|
|
82
|
+
* `.env`, an edit that mentions `id_rsa`, a grep pattern — and denies ordinary
|
|
83
|
+
* work with a message saying it cannot be overridden.
|
|
84
|
+
*/
|
|
85
|
+
export declare const PATH_ARGUMENT_KEYS: ReadonlySet<string>;
|
|
86
|
+
/**
|
|
87
|
+
* Argument keys whose values are shell command lines. Their tokens are tested
|
|
88
|
+
* as paths; see {@link pathCandidates} for why that is advisory.
|
|
89
|
+
*/
|
|
90
|
+
export declare const SHELL_ARGUMENT_KEYS: ReadonlySet<string>;
|
|
91
|
+
/** One string worth testing as a path, and how to split it. */
|
|
92
|
+
export interface PathArgument {
|
|
93
|
+
readonly text: string;
|
|
94
|
+
/** Whether the string is a shell command line rather than a single path. */
|
|
95
|
+
readonly shell: boolean;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Collect the path-typed strings inside one tool's arguments, at any depth.
|
|
99
|
+
*
|
|
100
|
+
* A key names paths for every tool that uses it: the tool registry is open, so
|
|
101
|
+
* a per-tool table would abstain on every plugin and MCP tool this build has
|
|
102
|
+
* never heard of.
|
|
103
|
+
* @param args - the pending call's parsed arguments.
|
|
104
|
+
* @returns each path-typed string, labelled with whether it is a command line.
|
|
105
|
+
*/
|
|
106
|
+
export declare function pathArguments(args: unknown): PathArgument[];
|
|
107
|
+
/**
|
|
108
|
+
* Tools that provably cannot move data off the machine: they read local
|
|
109
|
+
* state, edit local files, or talk only to the session itself.
|
|
110
|
+
*
|
|
111
|
+
* Anything absent from this set — every shell, `run_code`, the web tools,
|
|
112
|
+
* every `mcp__*` tool, and any plugin tool this build has never heard of — is
|
|
113
|
+
* treated as egress-capable. Unknown defaults to the safe side.
|
|
114
|
+
*/
|
|
115
|
+
export declare const LOCAL_TOOLS: ReadonlySet<string>;
|
|
116
|
+
/**
|
|
117
|
+
* Whether a tool can move data off the machine.
|
|
118
|
+
* @param toolName - the executing tool's registered name.
|
|
119
|
+
* @param extraEgressTools - names the repo-local policy tier added.
|
|
120
|
+
* @returns `true` when the tool is not a known local-only tool.
|
|
121
|
+
*/
|
|
122
|
+
export declare function isEgressCapable(toolName: string, extraEgressTools?: ReadonlySet<string>): boolean;
|
|
123
|
+
//# sourceMappingURL=paths.d.ts.map
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment configuration, the repo-local policy tier, and the tighten-only
|
|
3
|
+
* merge between them.
|
|
4
|
+
*
|
|
5
|
+
* Trust ranking, highest first:
|
|
6
|
+
*
|
|
7
|
+
* 1. invariants compiled into this package — the guard floor's tables; not configurable;
|
|
8
|
+
* 2. `cordis.yml` / bundle patch config — deployment-controlled; sets every field;
|
|
9
|
+
* 3. `policyFile` — a repo-local YAML file; **attacker-controlled**, may only tighten.
|
|
10
|
+
*
|
|
11
|
+
* Rank 3 is a file inside the workspace, so a hostile repository ships one and
|
|
12
|
+
* a prompt-injected agent can write one. It may add deny patterns, add egress
|
|
13
|
+
* tools, raise a severity, and switch a redaction pass on. Every other key,
|
|
14
|
+
* and every downgrade, is a load-time error rather than a silent ignore.
|
|
15
|
+
* @module dsh-dlp/policy
|
|
16
|
+
*/
|
|
17
|
+
import z from '@deepseek-ai/schemastery';
|
|
18
|
+
import { type Severity, type SyncRule } from './detectors.ts';
|
|
19
|
+
import { type CredentialPathRule } from './paths.ts';
|
|
20
|
+
/** Deployment configuration, validated from `cordis.yml`. */
|
|
21
|
+
export interface Config {
|
|
22
|
+
/** Absolute path of this plugin's own JSONL audit sink. Never the session log. */
|
|
23
|
+
auditLog: string;
|
|
24
|
+
/** Absolute path of the installation's redaction key; created with 32 random bytes if absent. */
|
|
25
|
+
redactionKeyFile: string;
|
|
26
|
+
/** Optional repo-local policy file; the lowest-trust source. */
|
|
27
|
+
policyFile?: string;
|
|
28
|
+
/** Cap on characters handed to a detector in one scan. */
|
|
29
|
+
maxScanBytes: number;
|
|
30
|
+
/** Whether the async `tools/pre-execute` secretlint pass runs. */
|
|
31
|
+
breadthTier: boolean;
|
|
32
|
+
/** Whether `tools/post-execute` redaction runs. */
|
|
33
|
+
resultRedaction: boolean;
|
|
34
|
+
/** Whether `session-telemetry/record` redaction runs. */
|
|
35
|
+
telemetryRedaction: boolean;
|
|
36
|
+
/** Whether telemetry's `session.cwd` attribute is replaced with a keyed hash. */
|
|
37
|
+
redactTelemetryWorkspacePaths: boolean;
|
|
38
|
+
}
|
|
39
|
+
export declare const Config: z<Config>;
|
|
40
|
+
/** Config toggles a repo-local policy may switch on, and never off. */
|
|
41
|
+
declare const ENABLEABLE: readonly ["breadthTier", "resultRedaction", "telemetryRedaction", "redactTelemetryWorkspacePaths"];
|
|
42
|
+
/** One toggle name a repo-local policy may name in `enable`. */
|
|
43
|
+
export type EnableableToggle = typeof ENABLEABLE[number];
|
|
44
|
+
/** Payload version this package writes and accepts for repo-local policy files. */
|
|
45
|
+
export declare const POLICY_VERSION = 1;
|
|
46
|
+
/** A repo-local policy file after parsing and validation. */
|
|
47
|
+
export interface RepoPolicy {
|
|
48
|
+
readonly addCredentialPaths: readonly CredentialPathRule[];
|
|
49
|
+
readonly addEgressTools: readonly string[];
|
|
50
|
+
readonly raiseSeverity: ReadonlyMap<string, Severity>;
|
|
51
|
+
readonly enable: readonly EnableableToggle[];
|
|
52
|
+
}
|
|
53
|
+
/** Everything the seams read after both tiers have been merged. */
|
|
54
|
+
export interface ResolvedPolicy {
|
|
55
|
+
readonly credentialPathRules: readonly CredentialPathRule[];
|
|
56
|
+
readonly extraEgressTools: ReadonlySet<string>;
|
|
57
|
+
readonly syncRules: readonly SyncRule[];
|
|
58
|
+
readonly maxScanBytes: number;
|
|
59
|
+
readonly breadthTier: boolean;
|
|
60
|
+
readonly resultRedaction: boolean;
|
|
61
|
+
readonly telemetryRedaction: boolean;
|
|
62
|
+
readonly redactTelemetryWorkspacePaths: boolean;
|
|
63
|
+
}
|
|
64
|
+
/** Thrown when a policy file is malformed or attempts to loosen the policy. */
|
|
65
|
+
export declare class PolicyError extends Error {
|
|
66
|
+
/**
|
|
67
|
+
* @param message - what the file did and why it is rejected.
|
|
68
|
+
*/
|
|
69
|
+
constructor(message: string);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Parse and validate one repo-local policy document.
|
|
73
|
+
*
|
|
74
|
+
* Loaded under `js-yaml`'s `JSON_SCHEMA`, so a `!!js/function` tag is a parse
|
|
75
|
+
* error rather than code execution. This path deliberately never touches the
|
|
76
|
+
* Cordis loader, whose `!!js` support is the whole reason it must not see
|
|
77
|
+
* workspace-authored files.
|
|
78
|
+
* @param text - the file's contents.
|
|
79
|
+
* @returns the validated policy.
|
|
80
|
+
* @throws PolicyError on an unknown key, a bad value, or any attempt to loosen.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseRepoPolicy(text: string): RepoPolicy;
|
|
83
|
+
/** Outcome of reading the file named by `policyFile`. */
|
|
84
|
+
export type RepoPolicyLoad =
|
|
85
|
+
/** No file at that path: the workspace ships no policy. */
|
|
86
|
+
{
|
|
87
|
+
readonly kind: 'absent';
|
|
88
|
+
} | {
|
|
89
|
+
readonly kind: 'loaded';
|
|
90
|
+
readonly policy: RepoPolicy;
|
|
91
|
+
}
|
|
92
|
+
/** Present but unreadable or invalid; `problem` is what to log. */
|
|
93
|
+
| {
|
|
94
|
+
readonly kind: 'invalid';
|
|
95
|
+
readonly problem: string;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Read a repo-local policy file from disk.
|
|
99
|
+
*
|
|
100
|
+
* Absence is not a misconfiguration here, which is the one place this package
|
|
101
|
+
* departs from "never silently skip a missing referent": `policyFile` names a
|
|
102
|
+
* path inside the *workspace*, and the recommended value is workspace-relative,
|
|
103
|
+
* so most repositories will not have one. Failing the mount would refuse to
|
|
104
|
+
* start `dsh` in every repository lacking the file — and would hand a hostile
|
|
105
|
+
* repository a way to remove the guard floor by deleting or breaking it.
|
|
106
|
+
* A malformed file is loud and ignored, never obeyed in part.
|
|
107
|
+
* @param path - the file to read.
|
|
108
|
+
* @returns the validated policy, its absence, or the problem to report.
|
|
109
|
+
*/
|
|
110
|
+
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
|
+
/**
|
|
121
|
+
* Merge the deployment config with an optional repo-local policy.
|
|
122
|
+
* @param config - the deployment-controlled configuration.
|
|
123
|
+
* @param repo - the repo-local policy, when one is mounted.
|
|
124
|
+
* @returns the effective policy every seam reads.
|
|
125
|
+
*/
|
|
126
|
+
export declare function resolvePolicy(config: Config, repo?: RepoPolicy): ResolvedPolicy;
|
|
127
|
+
export {};
|
|
128
|
+
//# sourceMappingURL=policy.d.ts.map
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turning {@link Detection}s into redacted text, and the keyed hash that lets
|
|
3
|
+
* an operator correlate two placeholders without the plugin ever writing the
|
|
4
|
+
* secret.
|
|
5
|
+
*
|
|
6
|
+
* Two properties this module exists to hold:
|
|
7
|
+
*
|
|
8
|
+
* - **Spans are expanded before they are spliced.** Tier-2 spans are advisory
|
|
9
|
+
* and verified to under-cover a secret, so every span grows outward to
|
|
10
|
+
* whitespace boundaries and overlapping spans merge. Over-redaction is the
|
|
11
|
+
* safe direction.
|
|
12
|
+
* - **The placeholder is deterministic.** `HMAC-SHA256(key, span)` truncated
|
|
13
|
+
* to 12 hex characters — stable for the same secret under the same
|
|
14
|
+
* installation key, useless to anyone without the key.
|
|
15
|
+
* @module dsh-dlp/redaction
|
|
16
|
+
*/
|
|
17
|
+
import type { ContentBlock } from '@deepseek-ai/dsh-llm';
|
|
18
|
+
import type { JsonValue } from '@deepseek-ai/dsh-session';
|
|
19
|
+
import { type Detection, type Severity } from './detectors.ts';
|
|
20
|
+
/** One replaced region, described without disclosing what it held. */
|
|
21
|
+
export interface RedactedSpan {
|
|
22
|
+
/** Rule that justified the replacement; the strictest one when spans merged. */
|
|
23
|
+
readonly ruleId: string;
|
|
24
|
+
readonly ruleVersion: number;
|
|
25
|
+
readonly severity: Severity;
|
|
26
|
+
readonly start: number;
|
|
27
|
+
readonly end: number;
|
|
28
|
+
/** Keyed hash of the replaced text; never the text itself. */
|
|
29
|
+
readonly hash: string;
|
|
30
|
+
/** JSON pointer to the string this span came from, when the scan walked a structure. */
|
|
31
|
+
readonly path?: string;
|
|
32
|
+
}
|
|
33
|
+
/** Result of redacting one string. */
|
|
34
|
+
export interface RedactedText {
|
|
35
|
+
readonly text: string;
|
|
36
|
+
readonly spans: readonly RedactedSpan[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Mints the keyed hashes that appear in placeholders and audit records. Holds
|
|
40
|
+
* the installation key; the key never leaves this object.
|
|
41
|
+
*/
|
|
42
|
+
export declare class SpanHasher {
|
|
43
|
+
#private;
|
|
44
|
+
/**
|
|
45
|
+
* @param key - installation secret, at least 16 bytes, read from `redactionKeyFile`.
|
|
46
|
+
*/
|
|
47
|
+
constructor(key: Buffer);
|
|
48
|
+
/**
|
|
49
|
+
* Keyed hash of one replaced region.
|
|
50
|
+
* @param text - the exact text being replaced.
|
|
51
|
+
* @returns 12 lowercase hex characters.
|
|
52
|
+
*/
|
|
53
|
+
hash(text: string): string;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Shorten a rule id for the placeholder. Secretlint's package-qualified ids
|
|
57
|
+
* are long enough to dominate the replacement text.
|
|
58
|
+
* @param ruleId - the full rule identity.
|
|
59
|
+
* @returns the readable tail of the id.
|
|
60
|
+
*/
|
|
61
|
+
export declare function shortRuleId(ruleId: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* The text substituted for one redacted span.
|
|
64
|
+
* @param span - the span being replaced.
|
|
65
|
+
* @returns a stable placeholder carrying the rule and the keyed hash.
|
|
66
|
+
*/
|
|
67
|
+
export declare function placeholderFor(span: Pick<RedactedSpan, 'ruleId' | 'hash'>): string;
|
|
68
|
+
/**
|
|
69
|
+
* Replace every detected region of one string with its placeholder.
|
|
70
|
+
* @param text - the string to redact.
|
|
71
|
+
* @param detections - matches reported by either detection tier.
|
|
72
|
+
* @param hasher - mints each span's keyed hash.
|
|
73
|
+
* @param path - JSON pointer recorded on every span, when scanning a structure.
|
|
74
|
+
* @returns the redacted string and the spans that were replaced.
|
|
75
|
+
*/
|
|
76
|
+
export declare function redactText(text: string, detections: readonly Detection[], hasher: SpanHasher, path?: string): RedactedText;
|
|
77
|
+
/**
|
|
78
|
+
* Every string reachable inside a value, at any depth. Object keys are not
|
|
79
|
+
* included: a key is structure, not payload.
|
|
80
|
+
* @param value - parsed tool arguments, a tool's canonical output, or any JSON value.
|
|
81
|
+
* @returns each string found, in traversal order.
|
|
82
|
+
*/
|
|
83
|
+
export declare function nestedStrings(value: unknown): string[];
|
|
84
|
+
/**
|
|
85
|
+
* Redact every string inside a JSON value, at any depth.
|
|
86
|
+
*
|
|
87
|
+
* Object *keys* are left alone: a key is structure, not payload, and renaming
|
|
88
|
+
* one would break the owning tool's `output.schema` on re-validation.
|
|
89
|
+
* @param value - the structure to redact.
|
|
90
|
+
* @param scan - synchronous detector applied to each string.
|
|
91
|
+
* @param hasher - mints each span's keyed hash.
|
|
92
|
+
* @returns the redacted structure, the spans replaced, and whether anything changed.
|
|
93
|
+
*/
|
|
94
|
+
export declare function redactJson(value: JsonValue, scan: (text: string) => readonly Detection[], hasher: SpanHasher): {
|
|
95
|
+
value: JsonValue;
|
|
96
|
+
spans: readonly RedactedSpan[];
|
|
97
|
+
changed: boolean;
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Redact the text blocks of one model-facing content array. Non-text blocks
|
|
101
|
+
* pass through: this plugin has no detector for image or audio payloads and
|
|
102
|
+
* silently dropping them would be worse than leaving them.
|
|
103
|
+
* @param blocks - the content blocks to redact.
|
|
104
|
+
* @param scan - synchronous detector applied to each block's text.
|
|
105
|
+
* @param hasher - mints each span's keyed hash.
|
|
106
|
+
* @returns the redacted blocks, the spans replaced, and whether anything changed.
|
|
107
|
+
*/
|
|
108
|
+
export declare function redactContent(blocks: readonly ContentBlock[], scan: (text: string) => readonly Detection[], hasher: SpanHasher): {
|
|
109
|
+
content: ContentBlock[];
|
|
110
|
+
spans: readonly RedactedSpan[];
|
|
111
|
+
changed: boolean;
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=redaction.d.ts.map
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two result-side seams: the async argument breadth tier at
|
|
3
|
+
* `tools/pre-execute`, and result redaction at `tools/post-execute`.
|
|
4
|
+
*
|
|
5
|
+
* Both are best-effort by construction. A `tools/pre-execute` listener
|
|
6
|
+
* registered ahead of ours can return without calling `next()` and neutralize
|
|
7
|
+
* the breadth tier; a `tools/post-execute` listener ahead of ours can replace a
|
|
8
|
+
* result after we redacted it. Only `ctx.tools.guard()` is order-independent.
|
|
9
|
+
* What these seams buy is breadth: they can await, so `@secretlint/core`'s
|
|
10
|
+
* whole rule set applies here and not in the guard.
|
|
11
|
+
* @module dsh-dlp/results
|
|
12
|
+
*/
|
|
13
|
+
import type { PostToolDecision, PreToolDecision, ToolExecution, ToolExecutionResult } from '@deepseek-ai/dsh-tools';
|
|
14
|
+
import type { ResolvedPolicy } from './policy.ts';
|
|
15
|
+
import { type RedactedSpan, type SpanHasher } from './redaction.ts';
|
|
16
|
+
/** What a redaction pass produced, before an arm is chosen. */
|
|
17
|
+
export interface ResultRedaction {
|
|
18
|
+
readonly decision: PostToolDecision;
|
|
19
|
+
readonly spans: readonly RedactedSpan[];
|
|
20
|
+
readonly truncatedScan: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Redact whatever the downstream decision settled on.
|
|
24
|
+
*
|
|
25
|
+
* Arm selection follows what each arm can actually clean:
|
|
26
|
+
*
|
|
27
|
+
* - `accept{value}` re-validates `output.schema`, re-runs `output.render()`
|
|
28
|
+
* and re-derives `presentationMeta()`, so one replacement redacts the
|
|
29
|
+
* canonical value, the model-facing content and the persisted meta together.
|
|
30
|
+
* It is the only arm that keeps a secret out of the durable log, so every
|
|
31
|
+
* successful result with anything to redact takes it.
|
|
32
|
+
* - `accept{content}` replaces presentation only. It is used when the
|
|
33
|
+
* persisted surfaces are already clean — a failed result, which has no
|
|
34
|
+
* value, or a success whose secret exists only in the rendered content.
|
|
35
|
+
* - `block` is the fallback when neither works: a failed result whose `meta`
|
|
36
|
+
* carries a secret, or a value that still scans dirty after redaction.
|
|
37
|
+
* Blocking replaces the whole result, which is the only way to drop `meta`.
|
|
38
|
+
*
|
|
39
|
+
* Replacing the value can fail: the placeholder is re-validated against the
|
|
40
|
+
* tool's `output.schema`, and a schema that constrains the string rejects it,
|
|
41
|
+
* which the registry reports as a `ToolOutputError`. A failed call is the
|
|
42
|
+
* intended outcome there — see README.md.
|
|
43
|
+
*
|
|
44
|
+
* A downstream `accept{content}` over a dirty value is overruled by the value
|
|
45
|
+
* arm, which discards that listener's presentation choice. Keeping it would
|
|
46
|
+
* put the value in the session log; the harness re-renders from the redacted
|
|
47
|
+
* value instead.
|
|
48
|
+
* @param decision - what the rest of the waterfall returned.
|
|
49
|
+
* @param result - the dispatch outcome the waterfall was called with.
|
|
50
|
+
* @param policy - the effective policy.
|
|
51
|
+
* @param hasher - mints each span's keyed hash.
|
|
52
|
+
* @returns the decision to return, the spans replaced, and scan completeness.
|
|
53
|
+
*/
|
|
54
|
+
export declare function redactDecision(decision: PostToolDecision, result: Readonly<ToolExecutionResult>, policy: ResolvedPolicy, hasher: SpanHasher): Promise<ResultRedaction>;
|
|
55
|
+
/**
|
|
56
|
+
* Decide whether the breadth tier denies one call before dispatch.
|
|
57
|
+
*
|
|
58
|
+
* Only ever narrows: the caller has already delegated, and a decision that is
|
|
59
|
+
* not `allow` is returned untouched.
|
|
60
|
+
* @param exec - the pending call.
|
|
61
|
+
* @param policy - the effective policy.
|
|
62
|
+
* @param hasher - mints the keyed hashes quoted in a denial reason.
|
|
63
|
+
* @returns the denial reason and its spans, or `undefined` to leave the call allowed.
|
|
64
|
+
*/
|
|
65
|
+
export declare function evaluateBreadthTier(exec: Pick<ToolExecution, 'name' | 'arguments'>, policy: ResolvedPolicy, hasher: SpanHasher): Promise<{
|
|
66
|
+
reason: string;
|
|
67
|
+
spans: readonly RedactedSpan[];
|
|
68
|
+
} | undefined>;
|
|
69
|
+
/** A `PreToolDecision` denial built from a breadth-tier finding. */
|
|
70
|
+
export declare function breadthTierDenial(reason: string): PreToolDecision;
|
|
71
|
+
//# sourceMappingURL=results.d.ts.map
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This plugin's own durable output, and the call correlation that makes a
|
|
3
|
+
* record locatable.
|
|
4
|
+
*
|
|
5
|
+
* Nothing here touches the session log. `Session.append()` offers no way to
|
|
6
|
+
* set the envelope's `ignorable` flag, so an out-of-repo event type is written
|
|
7
|
+
* without it and the user's next resume throws `SessionFormatUnsupportedError`
|
|
8
|
+
* and refuses the whole session. The plugin is therefore read-side with
|
|
9
|
+
* respect to the log, and every durable record goes to the JSONL file named by
|
|
10
|
+
* `auditLog`.
|
|
11
|
+
*
|
|
12
|
+
* Because the `SessionEvent` envelope carries only `type`, `seq`, `time` and
|
|
13
|
+
* `data`, each record carries its own identity: `sessionId`, `turn`, `step`,
|
|
14
|
+
* `callId`, and a producer-minted `decisionId`.
|
|
15
|
+
* @module dsh-dlp/sink
|
|
16
|
+
*/
|
|
17
|
+
import type { RedactedSpan } from './redaction.ts';
|
|
18
|
+
declare const decisionIdBrand: unique symbol;
|
|
19
|
+
/** Producer-minted id correlating one decision across records. */
|
|
20
|
+
export type DecisionId = string & {
|
|
21
|
+
readonly [decisionIdBrand]: true;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Mint a decision id.
|
|
25
|
+
* @returns an id unique to one guard verdict or redaction pass.
|
|
26
|
+
*/
|
|
27
|
+
export declare function newDecisionId(): DecisionId;
|
|
28
|
+
/** Payload version carried inside every record this plugin writes. */
|
|
29
|
+
export declare const RECORD_VERSION = 1;
|
|
30
|
+
/** What produced one audit record. */
|
|
31
|
+
export type AuditKind = 'guard-deny' | 'pre-execute-deny' | 'result-redaction' | 'telemetry-redaction' | 'audit-failure';
|
|
32
|
+
/** One durable record. Never carries matched secret text. */
|
|
33
|
+
export interface AuditRecord {
|
|
34
|
+
readonly v: number;
|
|
35
|
+
/** ISO-8601 capture time. */
|
|
36
|
+
readonly time: string;
|
|
37
|
+
readonly kind: AuditKind;
|
|
38
|
+
readonly decisionId: DecisionId;
|
|
39
|
+
readonly sessionId?: string;
|
|
40
|
+
readonly turn?: number;
|
|
41
|
+
readonly step?: number;
|
|
42
|
+
readonly callId?: string;
|
|
43
|
+
readonly rootCallId?: string;
|
|
44
|
+
readonly tool?: string;
|
|
45
|
+
/**
|
|
46
|
+
* Redacted or denied regions: rule identity, offsets, and a keyed hash only.
|
|
47
|
+
*
|
|
48
|
+
* This is the whole description of what matched. The model-facing denial
|
|
49
|
+
* text is deliberately not recorded: it names a tool and quotes nothing, but
|
|
50
|
+
* a reason built from a candidate would put the candidate — a shell command
|
|
51
|
+
* line, a tenant directory — into a durable file, which is exactly what this
|
|
52
|
+
* sink exists to avoid.
|
|
53
|
+
*/
|
|
54
|
+
readonly spans?: readonly RedactedSpan[];
|
|
55
|
+
/** Set when the scanned input exceeded the byte cap. */
|
|
56
|
+
readonly truncatedScan?: boolean;
|
|
57
|
+
/** Telemetry record channel, for `telemetry-redaction`. */
|
|
58
|
+
readonly channel?: string;
|
|
59
|
+
}
|
|
60
|
+
/** Append-only JSONL sink for this plugin's decisions. */
|
|
61
|
+
export declare class AuditSink {
|
|
62
|
+
#private;
|
|
63
|
+
/**
|
|
64
|
+
* @param path - absolute path of the JSONL file to append to.
|
|
65
|
+
* @param onFailure - notified when a write fails; a broken sink never changes a verdict.
|
|
66
|
+
*/
|
|
67
|
+
constructor(path: string, onFailure: (error: unknown) => void);
|
|
68
|
+
/**
|
|
69
|
+
* Append one record.
|
|
70
|
+
*
|
|
71
|
+
* A write failure is reported and swallowed on purpose: the sink is
|
|
72
|
+
* evidence, not enforcement, and letting a full disk turn every tool call
|
|
73
|
+
* into a denial trades a confidentiality control for an availability
|
|
74
|
+
* outage. A guard that throws would also skip `tools/post-execute` and so
|
|
75
|
+
* disable redaction for that call.
|
|
76
|
+
* @param record - the decision to record.
|
|
77
|
+
*/
|
|
78
|
+
write(record: AuditRecord): void;
|
|
79
|
+
}
|
|
80
|
+
/** Turn and step of one in-flight tool call. */
|
|
81
|
+
export interface CallPosition {
|
|
82
|
+
readonly turn: number;
|
|
83
|
+
readonly step: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Remembers where each in-flight tool call sits in the session.
|
|
87
|
+
*
|
|
88
|
+
* `Agent` exposes no turn or step, and the tool pipeline hands listeners only
|
|
89
|
+
* a `ToolExecution`. The `tool/call` session event carries `turn`, `step` and
|
|
90
|
+
* `callId` together, so following the session firehose is the only way to
|
|
91
|
+
* label a record with its position.
|
|
92
|
+
*/
|
|
93
|
+
export declare class CallCorrelator {
|
|
94
|
+
#private;
|
|
95
|
+
/**
|
|
96
|
+
* @param limit - maximum remembered calls; the oldest entry is dropped past it.
|
|
97
|
+
*/
|
|
98
|
+
constructor(limit?: number);
|
|
99
|
+
/**
|
|
100
|
+
* Record one call's position.
|
|
101
|
+
* @param callId - the call's id from the `tool/call` event.
|
|
102
|
+
* @param position - the turn and step that event reported.
|
|
103
|
+
*/
|
|
104
|
+
note(callId: string, position: CallPosition): void;
|
|
105
|
+
/**
|
|
106
|
+
* Forget one call.
|
|
107
|
+
* @param callId - the call whose result has been committed.
|
|
108
|
+
*/
|
|
109
|
+
forget(callId: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* Look one call's position up.
|
|
112
|
+
* @param callId - the call to locate.
|
|
113
|
+
* @returns its turn and step, or `undefined` when the call was never seen.
|
|
114
|
+
*/
|
|
115
|
+
lookup(callId: string): CallPosition | undefined;
|
|
116
|
+
}
|
|
117
|
+
export {};
|
|
118
|
+
//# sourceMappingURL=sink.d.ts.map
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fail-closed redaction for exported telemetry.
|
|
3
|
+
*
|
|
4
|
+
* `DSH_TELEMETRY_MODE=FULL` mounts the OTel backend with a live coordinator
|
|
5
|
+
* that exports a deep copy of every session event's `data` — user and
|
|
6
|
+
* assistant message text, tool arguments, tool results — plus identity
|
|
7
|
+
* attributes including `session.cwd`. The `session-telemetry/record` waterfall
|
|
8
|
+
* is the documented redaction seam and it ships **no rules of its own**: with
|
|
9
|
+
* nothing mounted, records reach the exporter exactly as captured. This
|
|
10
|
+
* listener is the missing rule set.
|
|
11
|
+
*
|
|
12
|
+
* The waterfall is synchronous (`next: () => SessionTelemetryRecord`), so only
|
|
13
|
+
* tier 1 is reachable here — there is no seam on this path that can await, and
|
|
14
|
+
* a secret only `@secretlint/core` recognises survives it. It is fail-closed
|
|
15
|
+
* by construction: the coordinator dispatches inside its own containment, and a
|
|
16
|
+
* throwing listener withholds that one record and never reaches the agent
|
|
17
|
+
* loop. This module therefore throws rather than returning a record it could
|
|
18
|
+
* not fully process.
|
|
19
|
+
*
|
|
20
|
+
* Records mirroring `tool/result` have already been through
|
|
21
|
+
* `tools/post-execute` redaction, which runs before the event is appended —
|
|
22
|
+
* but only for what that seam could reach, so this listener re-scans every
|
|
23
|
+
* record rather than trusting the event type. The value it adds beyond that is
|
|
24
|
+
* on `user/message`, `assistant/message`, `tool/call` arguments, and the
|
|
25
|
+
* workspace path.
|
|
26
|
+
* @module dsh-dlp/telemetry
|
|
27
|
+
*/
|
|
28
|
+
import type { SessionTelemetryRecord } from '@deepseek-ai/dsh-session-telemetry';
|
|
29
|
+
import type { ResolvedPolicy } from './policy.ts';
|
|
30
|
+
import { type RedactedSpan, type SpanHasher } from './redaction.ts';
|
|
31
|
+
/** Rule identity recorded when a workspace path is replaced. */
|
|
32
|
+
export declare const WORKSPACE_PATH_RULE = "dsh-dlp/telemetry-workspace-path";
|
|
33
|
+
/** One redacted telemetry record and what was replaced in it. */
|
|
34
|
+
export interface RedactedRecord {
|
|
35
|
+
readonly record: SessionTelemetryRecord;
|
|
36
|
+
readonly spans: readonly RedactedSpan[];
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Redact one outbound telemetry record.
|
|
40
|
+
*
|
|
41
|
+
* The record is treated as data, not as a typed structure: `body` is the
|
|
42
|
+
* event's own `data`, whose shape is owned by whichever package declared the
|
|
43
|
+
* event, and new event types appear without this plugin knowing them. Walking
|
|
44
|
+
* it as JSON is what makes the listener total.
|
|
45
|
+
* @param record - the candidate record, already the coordinator's own deep copy.
|
|
46
|
+
* @param policy - the effective policy.
|
|
47
|
+
* @param hasher - mints each span's keyed hash.
|
|
48
|
+
* @returns the record to hand onward and the spans replaced.
|
|
49
|
+
*/
|
|
50
|
+
export declare function redactRecord(record: SessionTelemetryRecord, policy: ResolvedPolicy, hasher: SpanHasher): RedactedRecord;
|
|
51
|
+
//# sourceMappingURL=telemetry.d.ts.map
|