@wyattjoh/demur 0.1.1 → 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 +71 -26
- package/package.json +5 -3
- package/src/adapters/claude-code.ts +0 -0
- package/src/cli.ts +222 -21
- package/src/judge.ts +22 -6
- package/src/key.ts +210 -9
- package/src/policy.ts +48 -4
- package/src/questions.ts +23 -3
- package/src/types.ts +9 -0
package/README.md
CHANGED
|
@@ -1,22 +1,24 @@
|
|
|
1
1
|
# demur
|
|
2
2
|
|
|
3
|
-
A proof-of-concept
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
A proof-of-concept harmful-command guard for coding agents. demur sends a shell
|
|
4
|
+
command and limited execution context to TypeSafe System One, then turns six
|
|
5
|
+
model judgments into an `allow`, `ask`, or `deny` decision.
|
|
6
6
|
|
|
7
7
|
> [!WARNING]
|
|
8
8
|
> demur is experimental and is not a security boundary. A model can
|
|
9
9
|
> misclassify, behave nondeterministically, or be influenced by attacker-controlled
|
|
10
10
|
> command text. Use it as an additional confirmation layer, not as your only
|
|
11
|
-
> protection against
|
|
11
|
+
> protection against harmful commands.
|
|
12
12
|
|
|
13
13
|
## How it works
|
|
14
14
|
|
|
15
15
|
For each agent-initiated Bash tool call, demur:
|
|
16
16
|
|
|
17
17
|
1. Collects the command, working directory, host name, and bounded Git facts.
|
|
18
|
-
2. Requests
|
|
18
|
+
2. Requests six judgments in one TypeSafe System One call:
|
|
19
19
|
- whether the command executes a destructive operation;
|
|
20
|
+
- whether it exposes secrets, credentials, or personal data;
|
|
21
|
+
- whether it weakens a security boundary or grants elevated access;
|
|
20
22
|
- whether its effects are recoverable;
|
|
21
23
|
- whether it targets shared infrastructure; and
|
|
22
24
|
- its expected blast radius.
|
|
@@ -55,15 +57,28 @@ shell arguments when the guard is active.
|
|
|
55
57
|
|
|
56
58
|
## Install
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
+
Install demur's command-line tools and save your TypeSafe API key in the
|
|
61
|
+
operating system credential store:
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
bun add --global @wyattjoh/demur
|
|
65
|
+
demur auth login
|
|
66
|
+
demur auth status
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
`Bun.secrets` stores the credential in macOS Keychain, Linux Secret Service, or
|
|
70
|
+
Windows Credential Manager. The operating system may request access when the
|
|
71
|
+
credential is first used or while its credential store is locked.
|
|
72
|
+
|
|
73
|
+
For automation or a one-off override, set `TYPESAFE_API_KEY` before launching
|
|
74
|
+
the host agent. An environment value takes precedence over the stored key:
|
|
60
75
|
|
|
61
76
|
```sh
|
|
62
77
|
export TYPESAFE_API_KEY="..."
|
|
63
78
|
```
|
|
64
79
|
|
|
65
80
|
Never commit the key. [`.env.schema`](.env.schema) documents the accepted
|
|
66
|
-
configuration, and local environment files are ignored by Git.
|
|
81
|
+
environment configuration, and local environment files are ignored by Git.
|
|
67
82
|
|
|
68
83
|
### Pi
|
|
69
84
|
|
|
@@ -76,10 +91,10 @@ pi install npm:@wyattjoh/demur
|
|
|
76
91
|
Pin a specific release when reproducibility matters:
|
|
77
92
|
|
|
78
93
|
```sh
|
|
79
|
-
pi install npm:@wyattjoh/demur@0.1.
|
|
94
|
+
pi install npm:@wyattjoh/demur@0.1.1
|
|
80
95
|
```
|
|
81
96
|
|
|
82
|
-
Launch Pi
|
|
97
|
+
Launch Pi normally after configuring the credential:
|
|
83
98
|
|
|
84
99
|
```sh
|
|
85
100
|
pi
|
|
@@ -102,12 +117,7 @@ pi install "$PWD"
|
|
|
102
117
|
|
|
103
118
|
### Claude Code
|
|
104
119
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
```sh
|
|
108
|
-
bun add --global @wyattjoh/demur
|
|
109
|
-
```
|
|
110
|
-
|
|
120
|
+
The global package installation above also provides the Claude Code hook.
|
|
111
121
|
Register its executable in `~/.claude/settings.json`:
|
|
112
122
|
|
|
113
123
|
```json
|
|
@@ -128,37 +138,45 @@ Register its executable in `~/.claude/settings.json`:
|
|
|
128
138
|
}
|
|
129
139
|
```
|
|
130
140
|
|
|
131
|
-
Launch Claude Code
|
|
132
|
-
|
|
133
|
-
`hookSpecificOutput.permissionDecision` response.
|
|
141
|
+
Launch Claude Code normally after configuring the credential. The adapter emits
|
|
142
|
+
Claude Code's `hookSpecificOutput.permissionDecision` response.
|
|
134
143
|
|
|
135
144
|
### CLI
|
|
136
145
|
|
|
137
|
-
|
|
146
|
+
Manage the stored credential or judge a single command without a host
|
|
147
|
+
integration:
|
|
138
148
|
|
|
139
149
|
```sh
|
|
140
|
-
|
|
150
|
+
demur auth login
|
|
151
|
+
demur auth status
|
|
152
|
+
demur auth logout
|
|
153
|
+
demur judge "git reset --hard HEAD~3"
|
|
141
154
|
```
|
|
142
155
|
|
|
156
|
+
From a development checkout, `bun run judge "<command>"` remains available.
|
|
157
|
+
|
|
143
158
|
## Configuration
|
|
144
159
|
|
|
145
160
|
| Variable | Default | Purpose |
|
|
146
161
|
| --- | --- | --- |
|
|
147
|
-
| `TYPESAFE_API_KEY` |
|
|
162
|
+
| `TYPESAFE_API_KEY` | stored credential | Optional TypeSafe API credential override. Missing keys fail closed. |
|
|
148
163
|
| `DEMUR_TIMEOUT_MS` | `4000` | Per-attempt model timeout in milliseconds. |
|
|
149
164
|
| `DEMUR_DISABLE` | unset | Emergency bypass. `1` or `true` allows every command. |
|
|
150
165
|
|
|
151
166
|
## Failure posture
|
|
152
167
|
|
|
153
|
-
demur fails closed. A missing key,
|
|
154
|
-
unexpected guard error returns `deny` with a
|
|
155
|
-
failure rather than presenting it as a policy
|
|
168
|
+
demur fails closed. A missing key, credential-store failure, timeout, API
|
|
169
|
+
failure, malformed response, or unexpected guard error returns `deny` with a
|
|
170
|
+
reason that identifies the guard failure rather than presenting it as a policy
|
|
171
|
+
judgment.
|
|
156
172
|
|
|
157
173
|
`DEMUR_DISABLE=1` is an explicit emergency bypass. It disables all protection
|
|
158
174
|
and should remain unset during normal use.
|
|
159
175
|
|
|
160
176
|
## Known limitations
|
|
161
177
|
|
|
178
|
+
- `Bun.secrets` is experimental, and credential-store availability and prompts
|
|
179
|
+
vary by operating system configuration.
|
|
162
180
|
- Model decisions are probabilistic and may vary between identical requests.
|
|
163
181
|
- The hard-coded `jev-latest` model alias may change without a demur release.
|
|
164
182
|
- Attacker-controlled command text can influence the model.
|
|
@@ -176,14 +194,41 @@ and deterministic policy controls alongside demur.
|
|
|
176
194
|
|
|
177
195
|
## Project layout
|
|
178
196
|
|
|
179
|
-
- `src/questions.ts` — the
|
|
197
|
+
- `src/questions.ts` — the six model judgments
|
|
180
198
|
- `src/policy.ts` — thresholds and `allow` / `ask` / `deny` composition
|
|
181
199
|
- `src/analyze.ts` — deterministic shell analysis for the static uncertainty gate
|
|
182
200
|
- `src/state.ts` — bounded environment and Git context collection
|
|
201
|
+
- `src/key.ts` — environment precedence and operating-system credential storage
|
|
183
202
|
- `src/guard.internal.ts` — Effect-native orchestration and fail-closed recovery
|
|
184
203
|
- `src/guard.ts` — managed runtime and Promise boundary
|
|
185
204
|
- `extensions/demur/` — Pi `tool_call` integration
|
|
186
205
|
- `src/adapters/claude-code.ts` — Claude Code `PreToolUse` integration
|
|
206
|
+
- `eval/` — safe synthetic contrast cases and the live evaluation runner
|
|
207
|
+
|
|
208
|
+
## Synthetic evaluation
|
|
209
|
+
|
|
210
|
+
The synthetic evaluation measures whether the two policy-qualification questions
|
|
211
|
+
separate clear positive and negative cases, then verifies that active hazards
|
|
212
|
+
produce a policy denial. Its 60 commands are hand-authored fixture strings with
|
|
213
|
+
synthetic names and no secret values. **The runner never executes a candidate
|
|
214
|
+
command.** It only sends each string and fixed synthetic context to TypeSafe.
|
|
215
|
+
|
|
216
|
+
Run one sample per case:
|
|
217
|
+
|
|
218
|
+
```sh
|
|
219
|
+
bun run eval:synthetic
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Repeat each case (up to 10 samples) to expose model instability:
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
bun run eval:synthetic --runs=3
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
The command prints each case's expected and observed classification, exits
|
|
229
|
+
nonzero on a miss or provider failure, and writes full evidence to
|
|
230
|
+
`.scratch/synthetic-eval.json`. Repeated runs make additional provider calls and
|
|
231
|
+
may incur cost, so the live evaluation is deliberately not part of `bun run ci`.
|
|
187
232
|
|
|
188
233
|
## Development
|
|
189
234
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wyattjoh/demur",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "A proof-of-concept destructive-command guard for coding agents.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,8 @@
|
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://github.com/wyattjoh/demur#readme",
|
|
15
15
|
"bin": {
|
|
16
|
-
"demur
|
|
16
|
+
"demur": "src/cli.ts",
|
|
17
|
+
"demur-claude-hook": "src/adapters/claude-code.ts"
|
|
17
18
|
},
|
|
18
19
|
"keywords": [
|
|
19
20
|
"coding-agent",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"build:claude": "bun build src/adapters/claude-code.ts --target=bun --outfile=dist/demur-hook.js --format=esm",
|
|
49
50
|
"build": "bun run build:pi && bun run build:claude",
|
|
50
51
|
"ci": "bun run check && bun run test && bun run build",
|
|
52
|
+
"eval:synthetic": "bun run eval/run.ts",
|
|
51
53
|
"judge": "bun run src/cli.ts",
|
|
52
54
|
"prepublishOnly": "bun run ci"
|
|
53
55
|
},
|
|
@@ -64,7 +66,7 @@
|
|
|
64
66
|
"@earendil-works/pi-coding-agent": "0.85.1",
|
|
65
67
|
"@effect/vitest": "4.0.0-rc.116",
|
|
66
68
|
"@types/bun": "1.4.2",
|
|
67
|
-
"typescript": "
|
|
69
|
+
"typescript": "7.0.2",
|
|
68
70
|
"vitest": "5.0.1"
|
|
69
71
|
},
|
|
70
72
|
"peerDependencies": {
|
|
File without changes
|
package/src/cli.ts
CHANGED
|
@@ -1,41 +1,242 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
+
import { Predicate } from "effect";
|
|
2
3
|
import { guard } from "./guard.ts";
|
|
4
|
+
import {
|
|
5
|
+
deleteApiKey,
|
|
6
|
+
resolveApiKey,
|
|
7
|
+
storeApiKey,
|
|
8
|
+
type ResolvedApiKey,
|
|
9
|
+
} from "./key.ts";
|
|
10
|
+
import type { Verdict } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
const USAGE = `Usage:
|
|
13
|
+
demur auth login
|
|
14
|
+
demur auth status
|
|
15
|
+
demur auth logout
|
|
16
|
+
demur judge "<command>" [--cwd=<path>]`;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Injectable process boundaries used by the command-line interface.
|
|
20
|
+
*/
|
|
21
|
+
export type CliDependencies = {
|
|
22
|
+
judge(command: string, cwd: string): Promise<Verdict>;
|
|
23
|
+
resolveApiKey(): Promise<ResolvedApiKey | undefined>;
|
|
24
|
+
storeApiKey(value: string): Promise<void>;
|
|
25
|
+
deleteApiKey(): Promise<boolean>;
|
|
26
|
+
readSecret(prompt: string): Promise<string>;
|
|
27
|
+
cwd(): string;
|
|
28
|
+
stdout(message: string): void;
|
|
29
|
+
stderr(message: string): void;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const defaultDependencies: CliDependencies = {
|
|
33
|
+
judge: (command, cwd) => guard(command, cwd, "cli"),
|
|
34
|
+
resolveApiKey,
|
|
35
|
+
storeApiKey,
|
|
36
|
+
deleteApiKey,
|
|
37
|
+
readSecret,
|
|
38
|
+
cwd: () => process.cwd(),
|
|
39
|
+
stdout: (message) => console.log(message),
|
|
40
|
+
stderr: (message) => console.error(message),
|
|
41
|
+
};
|
|
3
42
|
|
|
4
43
|
/**
|
|
5
|
-
*
|
|
6
|
-
* judgments behind it.
|
|
44
|
+
* Run demur's command-line interface with explicit process dependencies.
|
|
7
45
|
*
|
|
8
|
-
*
|
|
46
|
+
* @param args - Command-line arguments after the executable name
|
|
47
|
+
* @param dependencies - Guard, credential, terminal, and process boundaries
|
|
48
|
+
* @returns The process exit code
|
|
9
49
|
*/
|
|
10
|
-
async function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
50
|
+
export async function runCli(
|
|
51
|
+
args: ReadonlyArray<string>,
|
|
52
|
+
dependencies: CliDependencies = defaultDependencies,
|
|
53
|
+
): Promise<number> {
|
|
54
|
+
try {
|
|
55
|
+
if (args[0] === "auth") {
|
|
56
|
+
return await runAuth(args.slice(1), dependencies);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (args[0] === "help" || args[0] === "--help" || args[0] === "-h") {
|
|
60
|
+
dependencies.stdout(USAGE);
|
|
61
|
+
return 0;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const judgeArgs = args[0] === "judge" ? args.slice(1) : args;
|
|
65
|
+
return await runJudge(judgeArgs, dependencies);
|
|
66
|
+
} catch (error: unknown) {
|
|
67
|
+
dependencies.stderr(`demur: ${errorDetail(error)}`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function runAuth(
|
|
73
|
+
args: ReadonlyArray<string>,
|
|
74
|
+
dependencies: CliDependencies,
|
|
75
|
+
): Promise<number> {
|
|
76
|
+
const command = args[0];
|
|
77
|
+
|
|
78
|
+
if (command === "login" && args.length === 1) {
|
|
79
|
+
const resolved = await dependencies.resolveApiKey();
|
|
80
|
+
const value =
|
|
81
|
+
resolved?.source === "environment"
|
|
82
|
+
? resolved.value
|
|
83
|
+
: await dependencies.readSecret("TypeSafe API key: ");
|
|
84
|
+
|
|
85
|
+
if (value.trim() === "") {
|
|
86
|
+
dependencies.stderr("demur: the TypeSafe API key cannot be empty.");
|
|
87
|
+
return 2;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
await dependencies.storeApiKey(value);
|
|
91
|
+
dependencies.stdout(
|
|
92
|
+
resolved?.source === "environment"
|
|
93
|
+
? "Stored TYPESAFE_API_KEY in the operating system credential store."
|
|
94
|
+
: "Stored the TypeSafe API key in the operating system credential store.",
|
|
95
|
+
);
|
|
96
|
+
if (resolved?.source === "environment") {
|
|
97
|
+
dependencies.stdout(
|
|
98
|
+
"TYPESAFE_API_KEY remains the active override until it is unset.",
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
return 0;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (command === "status" && args.length === 1) {
|
|
105
|
+
const resolved = await dependencies.resolveApiKey();
|
|
106
|
+
if (resolved === undefined) {
|
|
107
|
+
dependencies.stderr("No TypeSafe API key is configured.");
|
|
108
|
+
return 1;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dependencies.stdout(
|
|
112
|
+
resolved.source === "environment"
|
|
113
|
+
? "A TypeSafe API key is configured through TYPESAFE_API_KEY."
|
|
114
|
+
: "A TypeSafe API key is stored in the operating system credential store.",
|
|
115
|
+
);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (command === "logout" && args.length === 1) {
|
|
120
|
+
const resolved = await dependencies.resolveApiKey();
|
|
121
|
+
const deleted = await dependencies.deleteApiKey();
|
|
122
|
+
dependencies.stdout(
|
|
123
|
+
deleted
|
|
124
|
+
? "Deleted the stored TypeSafe API key."
|
|
125
|
+
: "No stored TypeSafe API key was found.",
|
|
126
|
+
);
|
|
127
|
+
if (resolved?.source === "environment") {
|
|
128
|
+
dependencies.stdout(
|
|
129
|
+
"TYPESAFE_API_KEY remains configured and must be unset separately.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
dependencies.stderr(USAGE);
|
|
136
|
+
return 2;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function runJudge(
|
|
140
|
+
args: ReadonlyArray<string>,
|
|
141
|
+
dependencies: CliDependencies,
|
|
142
|
+
): Promise<number> {
|
|
143
|
+
const cwdArg = args.find((arg) => arg.startsWith("--cwd="));
|
|
144
|
+
const command = args.filter((arg) => !arg.startsWith("--")).join(" ");
|
|
14
145
|
|
|
15
146
|
if (command.trim() === "") {
|
|
16
|
-
|
|
17
|
-
|
|
147
|
+
dependencies.stderr(USAGE);
|
|
148
|
+
return 2;
|
|
18
149
|
}
|
|
19
150
|
|
|
20
|
-
const cwd = cwdArg?.slice("--cwd=".length)
|
|
21
|
-
const verdict = await
|
|
151
|
+
const cwd = cwdArg?.slice("--cwd=".length) || dependencies.cwd();
|
|
152
|
+
const verdict = await dependencies.judge(command, cwd);
|
|
22
153
|
|
|
23
154
|
const mark = { allow: "✓", ask: "?", deny: "✗" }[verdict.decision];
|
|
24
|
-
|
|
155
|
+
dependencies.stdout(
|
|
156
|
+
`${mark} ${verdict.decision.toUpperCase()} ${verdict.reason}`,
|
|
157
|
+
);
|
|
25
158
|
|
|
26
159
|
if (verdict.judgments !== undefined) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
160
|
+
const judgments = verdict.judgments;
|
|
161
|
+
dependencies.stdout("");
|
|
162
|
+
dependencies.stdout(
|
|
163
|
+
` executes destruction ${judgments.executesDestruction.toFixed(3)}`,
|
|
164
|
+
);
|
|
165
|
+
dependencies.stdout(
|
|
166
|
+
` sensitive-data exposure ${judgments.exposesSensitiveData.toFixed(3)}`,
|
|
167
|
+
);
|
|
168
|
+
dependencies.stdout(
|
|
169
|
+
` weakens security ${judgments.weakensSecurityBoundary.toFixed(3)}`,
|
|
170
|
+
);
|
|
171
|
+
dependencies.stdout(
|
|
172
|
+
` unrecoverable ${judgments.unrecoverable.toFixed(3)}`,
|
|
173
|
+
);
|
|
174
|
+
dependencies.stdout(
|
|
175
|
+
` shared infrastructure ${judgments.targetsSharedInfrastructure.toFixed(3)}`,
|
|
176
|
+
);
|
|
177
|
+
dependencies.stdout(
|
|
178
|
+
` blast radius ${judgments.blastRadius.toFixed(2)}/3 (confidence ${judgments.blastRadiusConfidence.toFixed(2)})`,
|
|
179
|
+
);
|
|
33
180
|
}
|
|
34
181
|
|
|
35
|
-
|
|
36
|
-
|
|
182
|
+
dependencies.stdout("");
|
|
183
|
+
dependencies.stdout(
|
|
37
184
|
` ${verdict.latencyMs}ms${verdict.usage ? `, ${verdict.usage.inputTokens} in / ${verdict.usage.outputTokens} out tokens` : ""}`,
|
|
38
185
|
);
|
|
186
|
+
return 0;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function readSecret(prompt: string): Promise<string> {
|
|
190
|
+
if (!process.stdin.isTTY || typeof process.stdin.setRawMode !== "function") {
|
|
191
|
+
return (await Bun.stdin.text()).trim();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
process.stderr.write(prompt);
|
|
195
|
+
process.stdin.setEncoding("utf8");
|
|
196
|
+
process.stdin.setRawMode(true);
|
|
197
|
+
process.stdin.resume();
|
|
198
|
+
|
|
199
|
+
return await new Promise<string>((resolve, reject) => {
|
|
200
|
+
let value = "";
|
|
201
|
+
|
|
202
|
+
const cleanup = () => {
|
|
203
|
+
process.stdin.off("data", onData);
|
|
204
|
+
process.stdin.setRawMode(false);
|
|
205
|
+
process.stdin.pause();
|
|
206
|
+
process.stderr.write("\n");
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const onData = (chunk: string | Buffer) => {
|
|
210
|
+
for (const character of String(chunk)) {
|
|
211
|
+
if (character === "\u0003" || character === "\u0004") {
|
|
212
|
+
cleanup();
|
|
213
|
+
reject(new Error("credential entry cancelled"));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (character === "\r" || character === "\n") {
|
|
218
|
+
cleanup();
|
|
219
|
+
resolve(value.trim());
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
if (character === "\b" || character === "\u007f") {
|
|
224
|
+
value = Array.from(value).slice(0, -1).join("");
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (character >= " ") value += character;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
process.stdin.on("data", onData);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function errorDetail(error: unknown): string {
|
|
237
|
+
return Predicate.isError(error) ? error.message : String(error);
|
|
39
238
|
}
|
|
40
239
|
|
|
41
|
-
|
|
240
|
+
if (import.meta.main) {
|
|
241
|
+
process.exitCode = await runCli(Bun.argv.slice(2));
|
|
242
|
+
}
|
package/src/judge.ts
CHANGED
|
@@ -12,7 +12,11 @@ import {
|
|
|
12
12
|
} from "effect";
|
|
13
13
|
import { AiError, DecisionModel } from "effect/unstable/ai";
|
|
14
14
|
import { FetchHttpClient } from "effect/unstable/http";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
Environment,
|
|
17
|
+
MISSING_KEY_HELP,
|
|
18
|
+
TypeSafeApiKey,
|
|
19
|
+
} from "./key.ts";
|
|
16
20
|
import { COMMAND_JUDGMENTS } from "./questions.ts";
|
|
17
21
|
import { renderState } from "./state.ts";
|
|
18
22
|
import type { CommandState, FailureKind, Judgments } from "./types.ts";
|
|
@@ -71,6 +75,7 @@ export class JudgmentError extends Schema.TaggedError<JudgmentError>()(
|
|
|
71
75
|
{
|
|
72
76
|
failure: Schema.Literals([
|
|
73
77
|
"no-api-key",
|
|
78
|
+
"credential-error",
|
|
74
79
|
"timeout",
|
|
75
80
|
"api-error",
|
|
76
81
|
"unexpected",
|
|
@@ -92,6 +97,7 @@ export class Judgment extends Context.Service<
|
|
|
92
97
|
Judgment,
|
|
93
98
|
Effect.gen(function* () {
|
|
94
99
|
const environment = yield* Environment;
|
|
100
|
+
const apiKey = yield* TypeSafeApiKey;
|
|
95
101
|
let decisionLayer: ReturnType<typeof makeDecisionModelLayer> | undefined;
|
|
96
102
|
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
97
103
|
|
|
@@ -102,9 +108,16 @@ export class Judgment extends Context.Service<
|
|
|
102
108
|
> {
|
|
103
109
|
if (decisionLayer !== undefined) return decisionLayer;
|
|
104
110
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
111
|
+
const resolvedApiKey = yield* apiKey.resolve.pipe(
|
|
112
|
+
Effect.mapError(
|
|
113
|
+
(error) =>
|
|
114
|
+
new JudgmentError({
|
|
115
|
+
failure: "credential-error",
|
|
116
|
+
detail: `Unable to read the operating system credential store: ${error.detail}`,
|
|
117
|
+
}),
|
|
118
|
+
),
|
|
119
|
+
);
|
|
120
|
+
if (resolvedApiKey === undefined) {
|
|
108
121
|
return yield* new JudgmentError({
|
|
109
122
|
failure: "no-api-key",
|
|
110
123
|
detail: MISSING_KEY_HELP,
|
|
@@ -115,7 +128,7 @@ export class Judgment extends Context.Service<
|
|
|
115
128
|
yield* environment.get("DEMUR_TIMEOUT_MS"),
|
|
116
129
|
);
|
|
117
130
|
timeoutMs = configuredTimeout || DEFAULT_TIMEOUT_MS;
|
|
118
|
-
decisionLayer = makeDecisionModelLayer(
|
|
131
|
+
decisionLayer = makeDecisionModelLayer(resolvedApiKey.value);
|
|
119
132
|
return decisionLayer;
|
|
120
133
|
},
|
|
121
134
|
);
|
|
@@ -140,6 +153,9 @@ export class Judgment extends Context.Service<
|
|
|
140
153
|
ok: true,
|
|
141
154
|
judgments: {
|
|
142
155
|
executesDestruction: answers.executesDestruction.probability,
|
|
156
|
+
exposesSensitiveData: answers.exposesSensitiveData.probability,
|
|
157
|
+
weakensSecurityBoundary:
|
|
158
|
+
answers.weakensSecurityBoundary.probability,
|
|
143
159
|
unrecoverable: answers.unrecoverable.probability,
|
|
144
160
|
targetsSharedInfrastructure:
|
|
145
161
|
answers.targetsSharedInfrastructure.probability,
|
|
@@ -158,7 +174,7 @@ export class Judgment extends Context.Service<
|
|
|
158
174
|
);
|
|
159
175
|
|
|
160
176
|
static readonly layer = this.layerNoDeps.pipe(
|
|
161
|
-
Layer.provide(Environment.layer),
|
|
177
|
+
Layer.provide(Layer.merge(Environment.layer, TypeSafeApiKey.layer)),
|
|
162
178
|
);
|
|
163
179
|
}
|
|
164
180
|
|
package/src/key.ts
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Context,
|
|
3
|
+
Effect,
|
|
4
|
+
Layer,
|
|
5
|
+
ManagedRuntime,
|
|
6
|
+
Predicate,
|
|
7
|
+
Schema,
|
|
8
|
+
} from "effect";
|
|
9
|
+
|
|
10
|
+
const TYPESAFE_SECRET = {
|
|
11
|
+
service: "com.github.wyattjoh.demur",
|
|
12
|
+
name: "typesafe-api-key",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Where demur found the active TypeSafe API key.
|
|
17
|
+
*/
|
|
18
|
+
export type ApiKeySource = "environment" | "system";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A resolved TypeSafe API key and the source that supplied it.
|
|
22
|
+
*/
|
|
23
|
+
export type ResolvedApiKey = {
|
|
24
|
+
value: string;
|
|
25
|
+
source: ApiKeySource;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A failure while accessing the operating system credential store.
|
|
30
|
+
*/
|
|
31
|
+
export class SecretStoreError extends Schema.TaggedError<SecretStoreError>()(
|
|
32
|
+
"SecretStoreError",
|
|
33
|
+
{
|
|
34
|
+
operation: Schema.Literals(["read", "write", "delete"]),
|
|
35
|
+
detail: Schema.String,
|
|
36
|
+
},
|
|
37
|
+
) {}
|
|
2
38
|
|
|
3
39
|
/**
|
|
4
40
|
* Effect service for reading process environment variables.
|
|
@@ -23,16 +59,167 @@ export class Environment extends Context.Service<
|
|
|
23
59
|
}
|
|
24
60
|
|
|
25
61
|
/**
|
|
26
|
-
*
|
|
62
|
+
* Effect service for the operating system's credential storage.
|
|
63
|
+
*/
|
|
64
|
+
export class SystemSecrets extends Context.Service<
|
|
65
|
+
SystemSecrets,
|
|
66
|
+
{
|
|
67
|
+
get(service: string, name: string): Effect.Effect<string | undefined, SecretStoreError>;
|
|
68
|
+
set(service: string, name: string, value: string): Effect.Effect<void, SecretStoreError>;
|
|
69
|
+
delete(service: string, name: string): Effect.Effect<boolean, SecretStoreError>;
|
|
70
|
+
}
|
|
71
|
+
>()("demur/key/SystemSecrets") {
|
|
72
|
+
static readonly layer = Layer.succeed(
|
|
73
|
+
SystemSecrets,
|
|
74
|
+
SystemSecrets.of({
|
|
75
|
+
get: Effect.fn("SystemSecrets.get")(function* (
|
|
76
|
+
service: string,
|
|
77
|
+
name: string,
|
|
78
|
+
) {
|
|
79
|
+
const value = yield* Effect.tryPromise({
|
|
80
|
+
try: () => Bun.secrets.get({ service, name }),
|
|
81
|
+
catch: secretStoreFailure("read"),
|
|
82
|
+
});
|
|
83
|
+
return value ?? undefined;
|
|
84
|
+
}),
|
|
85
|
+
set: Effect.fn("SystemSecrets.set")(function* (
|
|
86
|
+
service: string,
|
|
87
|
+
name: string,
|
|
88
|
+
value: string,
|
|
89
|
+
) {
|
|
90
|
+
yield* Effect.tryPromise({
|
|
91
|
+
try: () => Bun.secrets.set({ service, name, value }),
|
|
92
|
+
catch: secretStoreFailure("write"),
|
|
93
|
+
});
|
|
94
|
+
}),
|
|
95
|
+
delete: Effect.fn("SystemSecrets.delete")(function* (
|
|
96
|
+
service: string,
|
|
97
|
+
name: string,
|
|
98
|
+
) {
|
|
99
|
+
return yield* Effect.tryPromise({
|
|
100
|
+
try: () => Bun.secrets.delete({ service, name }),
|
|
101
|
+
catch: secretStoreFailure("delete"),
|
|
102
|
+
});
|
|
103
|
+
}),
|
|
104
|
+
}),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Effect service that resolves and manages demur's TypeSafe API key.
|
|
110
|
+
*
|
|
111
|
+
* `TYPESAFE_API_KEY` remains the highest-priority source for automation and
|
|
112
|
+
* one-off overrides. Otherwise, demur reads the key from the operating system's
|
|
113
|
+
* credential store through {@link SystemSecrets}.
|
|
114
|
+
*/
|
|
115
|
+
export class TypeSafeApiKey extends Context.Service<
|
|
116
|
+
TypeSafeApiKey,
|
|
117
|
+
{
|
|
118
|
+
readonly resolve: Effect.Effect<ResolvedApiKey | undefined, SecretStoreError>;
|
|
119
|
+
store(value: string): Effect.Effect<void, SecretStoreError>;
|
|
120
|
+
readonly remove: Effect.Effect<boolean, SecretStoreError>;
|
|
121
|
+
}
|
|
122
|
+
>()("demur/key/TypeSafeApiKey") {
|
|
123
|
+
static readonly layerNoDeps = Layer.effect(
|
|
124
|
+
TypeSafeApiKey,
|
|
125
|
+
Effect.gen(function* () {
|
|
126
|
+
const environment = yield* Environment;
|
|
127
|
+
const secrets = yield* SystemSecrets;
|
|
128
|
+
|
|
129
|
+
const resolve = Effect.gen(function* () {
|
|
130
|
+
const environmentValue = normalizeApiKey(
|
|
131
|
+
yield* environment.get("TYPESAFE_API_KEY"),
|
|
132
|
+
);
|
|
133
|
+
if (environmentValue !== undefined) {
|
|
134
|
+
return {
|
|
135
|
+
value: environmentValue,
|
|
136
|
+
source: "environment",
|
|
137
|
+
} satisfies ResolvedApiKey;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const storedValue = normalizeApiKey(
|
|
141
|
+
yield* secrets.get(TYPESAFE_SECRET.service, TYPESAFE_SECRET.name),
|
|
142
|
+
);
|
|
143
|
+
if (storedValue === undefined) return undefined;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
value: storedValue,
|
|
147
|
+
source: "system",
|
|
148
|
+
} satisfies ResolvedApiKey;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const store = Effect.fn("TypeSafeApiKey.store")(function* (value: string) {
|
|
152
|
+
const normalized = normalizeApiKey(value);
|
|
153
|
+
if (normalized === undefined) {
|
|
154
|
+
return yield* new SecretStoreError({
|
|
155
|
+
operation: "write",
|
|
156
|
+
detail: "The TypeSafe API key cannot be empty.",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
yield* secrets.set(
|
|
161
|
+
TYPESAFE_SECRET.service,
|
|
162
|
+
TYPESAFE_SECRET.name,
|
|
163
|
+
normalized,
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return TypeSafeApiKey.of({
|
|
168
|
+
resolve,
|
|
169
|
+
store,
|
|
170
|
+
remove: secrets.delete(TYPESAFE_SECRET.service, TYPESAFE_SECRET.name),
|
|
171
|
+
});
|
|
172
|
+
}),
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
static readonly layer = this.layerNoDeps.pipe(
|
|
176
|
+
Layer.provide(Layer.merge(Environment.layer, SystemSecrets.layer)),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const runtime = ManagedRuntime.make(TypeSafeApiKey.layer);
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Resolve the TypeSafe API key for a Promise-based host integration.
|
|
184
|
+
*
|
|
185
|
+
* @returns The active key and its source, or `undefined` when none is configured
|
|
186
|
+
*/
|
|
187
|
+
export function resolveApiKey(): Promise<ResolvedApiKey | undefined> {
|
|
188
|
+
return runtime.runPromise(
|
|
189
|
+
Effect.gen(function* () {
|
|
190
|
+
const apiKey = yield* TypeSafeApiKey;
|
|
191
|
+
return yield* apiKey.resolve;
|
|
192
|
+
}),
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Store a TypeSafe API key in the operating system credential store.
|
|
27
198
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
|
|
199
|
+
* @param value - The API key to persist
|
|
200
|
+
* @returns A promise that completes after the credential is stored
|
|
201
|
+
*/
|
|
202
|
+
export function storeApiKey(value: string): Promise<void> {
|
|
203
|
+
return runtime.runPromise(
|
|
204
|
+
Effect.gen(function* () {
|
|
205
|
+
const apiKey = yield* TypeSafeApiKey;
|
|
206
|
+
return yield* apiKey.store(value);
|
|
207
|
+
}),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Delete demur's TypeSafe API key from the operating system credential store.
|
|
31
213
|
*
|
|
32
|
-
* @returns
|
|
214
|
+
* @returns Whether a stored credential existed
|
|
33
215
|
*/
|
|
34
|
-
export function
|
|
35
|
-
return
|
|
216
|
+
export function deleteApiKey(): Promise<boolean> {
|
|
217
|
+
return runtime.runPromise(
|
|
218
|
+
Effect.gen(function* () {
|
|
219
|
+
const apiKey = yield* TypeSafeApiKey;
|
|
220
|
+
return yield* apiKey.remove;
|
|
221
|
+
}),
|
|
222
|
+
);
|
|
36
223
|
}
|
|
37
224
|
|
|
38
225
|
/**
|
|
@@ -42,4 +229,18 @@ export function resolveApiKey(): string | undefined {
|
|
|
42
229
|
* name the fix, or the failure reads like a policy decision.
|
|
43
230
|
*/
|
|
44
231
|
export const MISSING_KEY_HELP =
|
|
45
|
-
"No
|
|
232
|
+
"No TypeSafe API key configured. Run `demur auth login`, set TYPESAFE_API_KEY, or inject it with a secret manager.";
|
|
233
|
+
|
|
234
|
+
function normalizeApiKey(value: string | undefined): string | undefined {
|
|
235
|
+
return value?.trim() || undefined;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function secretStoreFailure(
|
|
239
|
+
operation: SecretStoreError["operation"],
|
|
240
|
+
): (cause: unknown) => SecretStoreError {
|
|
241
|
+
return (cause) =>
|
|
242
|
+
new SecretStoreError({
|
|
243
|
+
operation,
|
|
244
|
+
detail: Predicate.isError(cause) ? cause.message : String(cause),
|
|
245
|
+
});
|
|
246
|
+
}
|
package/src/policy.ts
CHANGED
|
@@ -20,6 +20,14 @@ export type Thresholds = {
|
|
|
20
20
|
* merely mention a scary path.
|
|
21
21
|
*/
|
|
22
22
|
executesDestruction: number;
|
|
23
|
+
/**
|
|
24
|
+
* Probability of exposing sensitive values at which a command is denied.
|
|
25
|
+
*/
|
|
26
|
+
denySensitiveDataExposure: number;
|
|
27
|
+
/**
|
|
28
|
+
* Probability of weakening a security boundary at which a command is denied.
|
|
29
|
+
*/
|
|
30
|
+
denySecurityBoundaryWeakening: number;
|
|
23
31
|
/**
|
|
24
32
|
* Probability of unrecoverable loss at which a command is denied.
|
|
25
33
|
*/
|
|
@@ -68,6 +76,8 @@ export type Thresholds = {
|
|
|
68
76
|
*/
|
|
69
77
|
export const THRESHOLDS: Thresholds = {
|
|
70
78
|
executesDestruction: 0.3,
|
|
79
|
+
denySensitiveDataExposure: 0.45,
|
|
80
|
+
denySecurityBoundaryWeakening: 0.45,
|
|
71
81
|
denyUnrecoverable: 0.45,
|
|
72
82
|
denySharedInfrastructure: 0.5,
|
|
73
83
|
denyBlastRadius: 2.0,
|
|
@@ -94,10 +104,10 @@ export type PolicyOutcome = {
|
|
|
94
104
|
/**
|
|
95
105
|
* Turn raw judgments into an action.
|
|
96
106
|
*
|
|
97
|
-
* Order matters.
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
107
|
+
* Order matters. Sensitive-data exposure and security-boundary weakening run
|
|
108
|
+
* before the inert-text gate because a read-only command can still disclose a
|
|
109
|
+
* secret. Their uncertainty bands also run before that gate. The destructive
|
|
110
|
+
* severity signals are checked only after the command is known to change state.
|
|
101
111
|
*
|
|
102
112
|
* @param j - Judgments returned for the command
|
|
103
113
|
* @param t - Threshold set to apply; defaults to the shipped values
|
|
@@ -108,6 +118,40 @@ export function decide(j: Judgments, t: Thresholds = THRESHOLDS): PolicyOutcome
|
|
|
108
118
|
const band = t.uncertaintyBand;
|
|
109
119
|
const scoreBand = t.uncertaintyBandScore;
|
|
110
120
|
|
|
121
|
+
if (j.exposesSensitiveData >= t.denySensitiveDataExposure + band) {
|
|
122
|
+
return {
|
|
123
|
+
decision: "deny",
|
|
124
|
+
reason: `May expose sensitive data such as secrets, credentials, or personal information (${pct(j.exposesSensitiveData)} likelihood). Use a redacted or metadata-only command.`,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (
|
|
129
|
+
j.weakensSecurityBoundary >=
|
|
130
|
+
t.denySecurityBoundaryWeakening + band
|
|
131
|
+
) {
|
|
132
|
+
return {
|
|
133
|
+
decision: "deny",
|
|
134
|
+
reason: `May weaken or bypass a security boundary or grant elevated access (${pct(j.weakensSecurityBoundary)} likelihood). Run it yourself if that is intended.`,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (j.exposesSensitiveData >= t.denySensitiveDataExposure - band) {
|
|
139
|
+
return {
|
|
140
|
+
decision: "ask",
|
|
141
|
+
reason: `Borderline on whether this exposes sensitive values (${pct(j.exposesSensitiveData)}). Confirm that its output and destination are safe.`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (
|
|
146
|
+
j.weakensSecurityBoundary >=
|
|
147
|
+
t.denySecurityBoundaryWeakening - band
|
|
148
|
+
) {
|
|
149
|
+
return {
|
|
150
|
+
decision: "ask",
|
|
151
|
+
reason: `Borderline on whether this weakens a security boundary or grants elevated access (${pct(j.weakensSecurityBoundary)}).`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
111
155
|
if (j.executesDestruction < t.executesDestruction - band) {
|
|
112
156
|
return {
|
|
113
157
|
decision: "allow",
|
package/src/questions.ts
CHANGED
|
@@ -4,15 +4,15 @@ import { Decision } from "effect/unstable/ai";
|
|
|
4
4
|
/**
|
|
5
5
|
* The judgment set demur asks about every command.
|
|
6
6
|
*
|
|
7
|
-
* All
|
|
8
|
-
* another and are evaluated in parallel, so asking all
|
|
7
|
+
* All six are sent in a single System One call. They are independent of one
|
|
8
|
+
* another and are evaluated in parallel, so asking all six costs one round
|
|
9
9
|
* trip. Code — not the model — decides which ones matter for a given verdict.
|
|
10
10
|
*
|
|
11
11
|
* Every question is phrased so that a high value means *more* dangerous,
|
|
12
12
|
* except `executesDestruction`, where a high value means the command really is
|
|
13
13
|
* an operation rather than inert text.
|
|
14
14
|
*
|
|
15
|
-
* A
|
|
15
|
+
* A prior question asking whether the command reaches outside the working
|
|
16
16
|
* directory was removed: "outside the working directory" turned out not to be a
|
|
17
17
|
* risk signal at all. It scored ~0.97 on any absolute path, including a rename
|
|
18
18
|
* within one directory, and no threshold sweep ever found a setting where it
|
|
@@ -29,6 +29,26 @@ export const QUESTIONS = {
|
|
|
29
29
|
},
|
|
30
30
|
}),
|
|
31
31
|
|
|
32
|
+
exposesSensitiveData: Decision.probability({
|
|
33
|
+
instructions:
|
|
34
|
+
"Executing this command will reveal or transmit the value of a secret, credential, authentication material, or personal data, including by printing it into the coding agent's command output.",
|
|
35
|
+
criteria: {
|
|
36
|
+
true: "It reads sensitive values from environment variables, configuration files, credential stores, secret managers, databases, or remote systems and then prints, logs, copies, uploads, or otherwise discloses them. Output returned to the coding agent counts as disclosure, even when the command is read-only.",
|
|
37
|
+
false:
|
|
38
|
+
"It only names sensitive files, variables, or records; lists metadata; returns redacted values; rotates or revokes credentials without revealing them; or contains labels such as `secret` or `API_KEY` as inert search text, documentation, comments, or non-sensitive payload data rather than actual values.",
|
|
39
|
+
},
|
|
40
|
+
}),
|
|
41
|
+
|
|
42
|
+
weakensSecurityBoundary: Decision.probability({
|
|
43
|
+
instructions:
|
|
44
|
+
"Executing this command will actually disable, weaken, evade, or misrepresent a security boundary, or grant elevated access, rather than merely displaying or describing such an operation.",
|
|
45
|
+
criteria: {
|
|
46
|
+
true: "Running it defeats authentication, authorization, policy enforcement, auditing, MFA, SSO, a firewall, or another safeguard; grants administrator, root, sudo, or otherwise elevated access; hides that a control was bypassed; or makes a sensitive resource more permissive.",
|
|
47
|
+
false:
|
|
48
|
+
"It only inspects security state; enables or tightens a safeguard; revokes or narrows access; performs an ordinary scoped credential, role, permission, or membership change without elevating privileges; or changes non-security configuration. Dangerous-looking command text is inert when it is quoted, printed, searched, documented, commented, or sent as data rather than executed.",
|
|
49
|
+
},
|
|
50
|
+
}),
|
|
51
|
+
|
|
32
52
|
unrecoverable: Decision.probability({
|
|
33
53
|
instructions:
|
|
34
54
|
"Whatever this command destroys could not be recovered afterwards from the state described.",
|
package/src/types.ts
CHANGED
|
@@ -89,6 +89,14 @@ export type Judgments = {
|
|
|
89
89
|
* than merely containing destructive-looking text as data.
|
|
90
90
|
*/
|
|
91
91
|
executesDestruction: number;
|
|
92
|
+
/**
|
|
93
|
+
* Probability the command reveals or transmits sensitive values.
|
|
94
|
+
*/
|
|
95
|
+
exposesSensitiveData: number;
|
|
96
|
+
/**
|
|
97
|
+
* Probability the command weakens a security boundary or grants elevated access.
|
|
98
|
+
*/
|
|
99
|
+
weakensSecurityBoundary: number;
|
|
92
100
|
/**
|
|
93
101
|
* Probability that what it destroys cannot be recovered.
|
|
94
102
|
*/
|
|
@@ -117,6 +125,7 @@ export type Decision = "allow" | "ask" | "deny";
|
|
|
117
125
|
*/
|
|
118
126
|
export type FailureKind =
|
|
119
127
|
| "no-api-key"
|
|
128
|
+
| "credential-error"
|
|
120
129
|
| "timeout"
|
|
121
130
|
| "api-error"
|
|
122
131
|
| "unexpected";
|