@wyattjoh/demur 0.5.0 → 0.7.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 +82 -25
- package/extensions/demur/index.ts +99 -15
- package/extensions/demur/training-store.ts +213 -35
- package/package.json +4 -2
- package/src/adapters/pi-worker.ts +21 -7
- package/src/cli.ts +554 -135
- package/src/guard.internal.ts +83 -33
- package/src/guard.ts +42 -2
- package/src/judge.ts +53 -7
- package/src/policy.ts +10 -1
- package/src/questions.ts +9 -1
- package/src/settings-model.ts +60 -0
- package/src/state.ts +14 -16
- package/src/training-evaluation.ts +408 -0
- package/src/training-review-model.ts +18 -2
- package/src/training-review-tui.tsx +408 -44
- package/src/types.ts +56 -0
package/src/guard.internal.ts
CHANGED
|
@@ -6,14 +6,91 @@ import {
|
|
|
6
6
|
Predicate,
|
|
7
7
|
Result,
|
|
8
8
|
} from "effect";
|
|
9
|
-
import { judgeEffect, Judgment } from "./judge.ts";
|
|
9
|
+
import { judgeEffect, Judgment, TYPESAFE_MODEL } from "./judge.ts";
|
|
10
10
|
import { Environment } from "./key.ts";
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
import {
|
|
12
|
+
applyStaticGate,
|
|
13
|
+
decide,
|
|
14
|
+
POLICY_VERSION,
|
|
15
|
+
THRESHOLDS,
|
|
16
|
+
} from "./policy.ts";
|
|
17
|
+
import { QUESTION_SET_VERSION } from "./questions.ts";
|
|
18
|
+
import { gatherStateEffect, GitCommand, renderState } from "./state.ts";
|
|
19
|
+
import type {
|
|
20
|
+
CommandState,
|
|
21
|
+
GuardEvaluation,
|
|
22
|
+
Host,
|
|
23
|
+
Verdict,
|
|
24
|
+
} from "./types.ts";
|
|
14
25
|
|
|
15
26
|
const PREFIX = "demur:";
|
|
16
27
|
|
|
28
|
+
/**
|
|
29
|
+
* Effect-native guard implementation with replayable evidence.
|
|
30
|
+
*
|
|
31
|
+
* @param command - The shell command the agent wants to run
|
|
32
|
+
* @param cwd - Absolute working directory for the command
|
|
33
|
+
* @param agent - Which coding agent is asking
|
|
34
|
+
* @returns A fail-closed verdict and the exact evidence behind it
|
|
35
|
+
*/
|
|
36
|
+
export const guardEvaluationEffect = Effect.fn("guardEvaluationEffect")(
|
|
37
|
+
function* (
|
|
38
|
+
command: string,
|
|
39
|
+
cwd: string,
|
|
40
|
+
agent: Host,
|
|
41
|
+
): Effect.fn.Return<
|
|
42
|
+
GuardEvaluation,
|
|
43
|
+
never,
|
|
44
|
+
Environment | GitCommand | Judgment
|
|
45
|
+
> {
|
|
46
|
+
const started = yield* Clock.monotonicTimeNanos;
|
|
47
|
+
const core = Effect.gen(function* () {
|
|
48
|
+
const environment = yield* Environment;
|
|
49
|
+
const disabled = isDisabled(yield* environment.get("DEMUR_DISABLE"));
|
|
50
|
+
|
|
51
|
+
if (disabled) {
|
|
52
|
+
const verdict = yield* completeVerdict(started, {
|
|
53
|
+
...emptyEvidence,
|
|
54
|
+
decision: "allow",
|
|
55
|
+
reason: `${PREFIX} disabled via DEMUR_DISABLE.`,
|
|
56
|
+
});
|
|
57
|
+
return { verdict, evidence: undefined } satisfies GuardEvaluation;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (command.trim() === "") {
|
|
61
|
+
const verdict = yield* completeVerdict(started, {
|
|
62
|
+
...emptyEvidence,
|
|
63
|
+
decision: "allow",
|
|
64
|
+
reason: `${PREFIX} empty command.`,
|
|
65
|
+
});
|
|
66
|
+
return { verdict, evidence: undefined } satisfies GuardEvaluation;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const state = yield* gatherStateEffect(command, cwd, agent);
|
|
70
|
+
const verdict = yield* judgeStateCore(state, started, 0);
|
|
71
|
+
return {
|
|
72
|
+
verdict,
|
|
73
|
+
evidence: {
|
|
74
|
+
modelState: renderState(state),
|
|
75
|
+
analysis: state.analysis,
|
|
76
|
+
model: TYPESAFE_MODEL,
|
|
77
|
+
questionSetVersion: QUESTION_SET_VERSION,
|
|
78
|
+
policyVersion: POLICY_VERSION,
|
|
79
|
+
policyThresholds: { ...THRESHOLDS },
|
|
80
|
+
},
|
|
81
|
+
} satisfies GuardEvaluation;
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const exit = yield* Effect.exit(core);
|
|
85
|
+
if (Exit.isSuccess(exit)) return exit.value;
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
verdict: yield* unexpectedVerdict(started, exit.cause, 0),
|
|
89
|
+
evidence: undefined,
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
|
|
17
94
|
/**
|
|
18
95
|
* Effect-native guard implementation used by the Promise boundary.
|
|
19
96
|
*
|
|
@@ -27,35 +104,8 @@ export const guardEffect = Effect.fn("guardEffect")(function* (
|
|
|
27
104
|
cwd: string,
|
|
28
105
|
agent: Host,
|
|
29
106
|
): Effect.fn.Return<Verdict, never, Environment | GitCommand | Judgment> {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
const environment = yield* Environment;
|
|
33
|
-
const disabled = isDisabled(yield* environment.get("DEMUR_DISABLE"));
|
|
34
|
-
|
|
35
|
-
if (disabled) {
|
|
36
|
-
return yield* completeVerdict(started, {
|
|
37
|
-
...emptyEvidence,
|
|
38
|
-
decision: "allow",
|
|
39
|
-
reason: `${PREFIX} disabled via DEMUR_DISABLE.`,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (command.trim() === "") {
|
|
44
|
-
return yield* completeVerdict(started, {
|
|
45
|
-
...emptyEvidence,
|
|
46
|
-
decision: "allow",
|
|
47
|
-
reason: `${PREFIX} empty command.`,
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const state = yield* gatherStateEffect(command, cwd, agent);
|
|
52
|
-
return yield* judgeStateCore(state, started, 0);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
const exit = yield* Effect.exit(core);
|
|
56
|
-
if (Exit.isSuccess(exit)) return exit.value;
|
|
57
|
-
|
|
58
|
-
return yield* unexpectedVerdict(started, exit.cause, 0);
|
|
107
|
+
const evaluation = yield* guardEvaluationEffect(command, cwd, agent);
|
|
108
|
+
return evaluation.verdict;
|
|
59
109
|
});
|
|
60
110
|
|
|
61
111
|
/**
|
package/src/guard.ts
CHANGED
|
@@ -1,9 +1,18 @@
|
|
|
1
1
|
import { Layer, ManagedRuntime, Predicate } from "effect";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
guardEffect,
|
|
4
|
+
guardEvaluationEffect,
|
|
5
|
+
judgeStateEffect,
|
|
6
|
+
} from "./guard.internal.ts";
|
|
3
7
|
import { Judgment } from "./judge.ts";
|
|
4
8
|
import { Environment } from "./key.ts";
|
|
5
9
|
import { GitCommand } from "./state.ts";
|
|
6
|
-
import type {
|
|
10
|
+
import type {
|
|
11
|
+
CommandState,
|
|
12
|
+
GuardEvaluation,
|
|
13
|
+
Host,
|
|
14
|
+
Verdict,
|
|
15
|
+
} from "./types.ts";
|
|
7
16
|
|
|
8
17
|
const PREFIX = "demur:";
|
|
9
18
|
|
|
@@ -41,6 +50,37 @@ export function guard(
|
|
|
41
50
|
.catch((error: unknown) => unexpectedVerdict(error, started));
|
|
42
51
|
}
|
|
43
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Judge one command while retaining the exact state and policy versions.
|
|
55
|
+
*
|
|
56
|
+
* This boundary is used only when a host has enabled local training capture;
|
|
57
|
+
* ordinary guard callers continue to receive the smaller {@link Verdict}.
|
|
58
|
+
*
|
|
59
|
+
* @param command - The shell command the agent wants to run
|
|
60
|
+
* @param cwd - Absolute working directory for the command
|
|
61
|
+
* @param agent - Which coding agent is asking
|
|
62
|
+
* @param signal - Optional cancellation signal from the host
|
|
63
|
+
* @returns The verdict and replayable evidence, when state collection occurred
|
|
64
|
+
*/
|
|
65
|
+
export function guardWithEvidence(
|
|
66
|
+
command: string,
|
|
67
|
+
cwd: string,
|
|
68
|
+
agent: Host,
|
|
69
|
+
signal: AbortSignal | undefined = undefined,
|
|
70
|
+
): Promise<GuardEvaluation> {
|
|
71
|
+
const started = performance.now();
|
|
72
|
+
|
|
73
|
+
return runtime
|
|
74
|
+
.runPromise(
|
|
75
|
+
guardEvaluationEffect(command, cwd, agent),
|
|
76
|
+
signal === undefined ? undefined : { signal },
|
|
77
|
+
)
|
|
78
|
+
.catch((error: unknown) => ({
|
|
79
|
+
verdict: unexpectedVerdict(error, started),
|
|
80
|
+
evidence: undefined,
|
|
81
|
+
}));
|
|
82
|
+
}
|
|
83
|
+
|
|
44
84
|
/**
|
|
45
85
|
* Judge a command from state that has already been collected.
|
|
46
86
|
*
|
package/src/judge.ts
CHANGED
|
@@ -19,7 +19,12 @@ import {
|
|
|
19
19
|
} from "./key.ts";
|
|
20
20
|
import { COMMAND_JUDGMENTS } from "./questions.ts";
|
|
21
21
|
import { renderState } from "./state.ts";
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
CommandState,
|
|
24
|
+
FailureKind,
|
|
25
|
+
Judgments,
|
|
26
|
+
RenderedCommandState,
|
|
27
|
+
} from "./types.ts";
|
|
23
28
|
|
|
24
29
|
/**
|
|
25
30
|
* Default per-attempt timeout for the judgment call.
|
|
@@ -29,7 +34,10 @@ import type { CommandState, FailureKind, Judgments } from "./types.ts";
|
|
|
29
34
|
*/
|
|
30
35
|
const DEFAULT_TIMEOUT_MS = 4000;
|
|
31
36
|
|
|
32
|
-
|
|
37
|
+
/**
|
|
38
|
+
* TypeSafe model used for command judgments.
|
|
39
|
+
*/
|
|
40
|
+
export const TYPESAFE_MODEL = "jev-latest";
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
43
|
* Build the TypeSafe-backed Effect decision model for one API key.
|
|
@@ -90,7 +98,9 @@ export class JudgmentError extends Schema.TaggedError<JudgmentError>()(
|
|
|
90
98
|
export class Judgment extends Context.Service<
|
|
91
99
|
Judgment,
|
|
92
100
|
{
|
|
93
|
-
judge(
|
|
101
|
+
judge(
|
|
102
|
+
state: RenderedCommandState,
|
|
103
|
+
): Effect.Effect<JudgeSuccess, JudgmentError>;
|
|
94
104
|
}
|
|
95
105
|
>()("demur/judge/Judgment") {
|
|
96
106
|
static readonly layerNoDeps = Layer.effect(
|
|
@@ -134,11 +144,11 @@ export class Judgment extends Context.Service<
|
|
|
134
144
|
);
|
|
135
145
|
|
|
136
146
|
const judge = Effect.fn("Judgment.judge")(function* (
|
|
137
|
-
state:
|
|
147
|
+
state: RenderedCommandState,
|
|
138
148
|
): Effect.fn.Return<JudgeSuccess, JudgmentError> {
|
|
139
149
|
const activeDecisionLayer = yield* getDecisionLayer();
|
|
140
150
|
const result = yield* DecisionModel.decide(COMMAND_JUDGMENTS, {
|
|
141
|
-
input:
|
|
151
|
+
input: state,
|
|
142
152
|
}).pipe(
|
|
143
153
|
Effect.provide(activeDecisionLayer),
|
|
144
154
|
Effect.timeout(timeoutMs),
|
|
@@ -188,9 +198,24 @@ export const judgeEffect = Effect.fn("judgeEffect")(function* (
|
|
|
188
198
|
state: CommandState,
|
|
189
199
|
): Effect.fn.Return<JudgeSuccess, JudgmentError, Judgment> {
|
|
190
200
|
const judgment = yield* Judgment;
|
|
191
|
-
return yield* judgment.judge(state);
|
|
201
|
+
return yield* judgment.judge(renderState(state));
|
|
192
202
|
});
|
|
193
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Ask the configured judgment service about already-rendered TypeSafe state.
|
|
206
|
+
*
|
|
207
|
+
* @param state - Exact JSON state captured from an earlier invocation
|
|
208
|
+
* @returns The current question set's judgments in the Effect error channel
|
|
209
|
+
*/
|
|
210
|
+
export const judgeRenderedStateEffect = Effect.fn("judgeRenderedStateEffect")(
|
|
211
|
+
function* (
|
|
212
|
+
state: RenderedCommandState,
|
|
213
|
+
): Effect.fn.Return<JudgeSuccess, JudgmentError, Judgment> {
|
|
214
|
+
const judgment = yield* Judgment;
|
|
215
|
+
return yield* judgment.judge(state);
|
|
216
|
+
},
|
|
217
|
+
);
|
|
218
|
+
|
|
194
219
|
const runtime = ManagedRuntime.make(Judgment.layer);
|
|
195
220
|
|
|
196
221
|
/**
|
|
@@ -206,9 +231,30 @@ const runtime = ManagedRuntime.make(Judgment.layer);
|
|
|
206
231
|
export function judge(
|
|
207
232
|
state: CommandState,
|
|
208
233
|
signal: AbortSignal | undefined = undefined,
|
|
234
|
+
): Promise<JudgeResult> {
|
|
235
|
+
return runJudgment(judgeEffect(state), signal);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Re-run the current question set against exact previously captured state.
|
|
240
|
+
*
|
|
241
|
+
* @param state - Exact model-visible state from a version-two training record
|
|
242
|
+
* @param signal - Optional cancellation signal
|
|
243
|
+
* @returns Current raw judgments or a typed failure
|
|
244
|
+
*/
|
|
245
|
+
export function judgeRenderedState(
|
|
246
|
+
state: RenderedCommandState,
|
|
247
|
+
signal: AbortSignal | undefined = undefined,
|
|
248
|
+
): Promise<JudgeResult> {
|
|
249
|
+
return runJudgment(judgeRenderedStateEffect(state), signal);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function runJudgment(
|
|
253
|
+
effect: Effect.Effect<JudgeSuccess, JudgmentError, Judgment>,
|
|
254
|
+
signal: AbortSignal | undefined,
|
|
209
255
|
): Promise<JudgeResult> {
|
|
210
256
|
const program = Effect.gen(function* () {
|
|
211
|
-
const result = yield* Effect.result(
|
|
257
|
+
const result = yield* Effect.result(effect);
|
|
212
258
|
if (Result.isFailure(result)) {
|
|
213
259
|
return {
|
|
214
260
|
ok: false,
|
package/src/policy.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import type { CommandAnalysis } from "./analyze.ts";
|
|
2
2
|
import type { Decision, Judgments } from "./types.ts";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Version of the policy ordering and threshold semantics.
|
|
6
|
+
*
|
|
7
|
+
* Increment this whenever composition behavior changes. Threshold values are
|
|
8
|
+
* captured separately by offline evaluation, so candidate sweeps do not need a
|
|
9
|
+
* version bump until a candidate is promoted.
|
|
10
|
+
*/
|
|
11
|
+
export const POLICY_VERSION = 2;
|
|
12
|
+
|
|
4
13
|
/**
|
|
5
14
|
* Decision thresholds applied to raw judgments.
|
|
6
15
|
*
|
|
@@ -81,7 +90,7 @@ export const THRESHOLDS: Thresholds = {
|
|
|
81
90
|
denyUnrecoverable: 0.45,
|
|
82
91
|
denySharedInfrastructure: 0.5,
|
|
83
92
|
denyBlastRadius: 2.0,
|
|
84
|
-
minBlastRadiusConfidence: 0.
|
|
93
|
+
minBlastRadiusConfidence: 0.95,
|
|
85
94
|
uncertaintyBand: 0.04,
|
|
86
95
|
uncertaintyBandScore: 0.12,
|
|
87
96
|
staticGateMinExecution: 0.8,
|
package/src/questions.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
2
|
import { Decision } from "effect/unstable/ai";
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Version of the complete TypeSafe question set.
|
|
6
|
+
*
|
|
7
|
+
* Increment this whenever question instructions, criteria, or primitives change
|
|
8
|
+
* so captured training evidence remains attributable to its exact semantics.
|
|
9
|
+
*/
|
|
10
|
+
export const QUESTION_SET_VERSION = 2;
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* The judgment set demur asks about every command.
|
|
6
14
|
*
|
|
@@ -35,7 +43,7 @@ export const QUESTIONS = {
|
|
|
35
43
|
criteria: {
|
|
36
44
|
true: "It reads a value from an environment variable, file, credential or secret store, database, or remote system and prints, logs, copies, or transmits it. Read-only output still counts.",
|
|
37
45
|
false:
|
|
38
|
-
"It reveals only names, metadata, schema, attributes, or redacted values; rotates or revokes without revealing; or quotes or prints secret-looking names or commands as documentation, comments, or other inert payload.",
|
|
46
|
+
"It reveals only names, metadata, schema, attributes, or redacted values; looks up or describes a credential item without requesting its secret value; rotates or revokes without revealing; or quotes or prints secret-looking names or commands as documentation, comments, or other inert payload. For example, `security find-generic-password -s NAME` without `-w` prints item attributes rather than the password value.",
|
|
39
47
|
},
|
|
40
48
|
}),
|
|
41
49
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEMUR_MODES,
|
|
3
|
+
FAILURE_POLICIES,
|
|
4
|
+
type DemurSettings,
|
|
5
|
+
} from "../extensions/demur/settings.ts";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Settings exposed by both the Pi extension menu and the central TUI.
|
|
9
|
+
*/
|
|
10
|
+
export type DemurSettingKey = "mode" | "training" | "failurePolicy";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Move one persisted demur setting to its next or previous supported value.
|
|
14
|
+
*
|
|
15
|
+
* Selecting disabled mode always turns training capture off. Training cannot be
|
|
16
|
+
* changed while disabled, matching the persisted configuration invariant.
|
|
17
|
+
*
|
|
18
|
+
* @param settings - Current persisted settings
|
|
19
|
+
* @param key - Setting to change
|
|
20
|
+
* @param direction - Positive for the next value, negative for the previous
|
|
21
|
+
* @returns The updated settings, or the original object when no change is valid
|
|
22
|
+
*/
|
|
23
|
+
export function changeDemurSetting(
|
|
24
|
+
settings: DemurSettings,
|
|
25
|
+
key: DemurSettingKey,
|
|
26
|
+
direction: number,
|
|
27
|
+
): DemurSettings {
|
|
28
|
+
if (key === "training") {
|
|
29
|
+
if (settings.mode === "disabled") return settings;
|
|
30
|
+
return { ...settings, training: !settings.training };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (key === "mode") {
|
|
34
|
+
const mode = cycleValue(DEMUR_MODES, settings.mode, direction);
|
|
35
|
+
return {
|
|
36
|
+
...settings,
|
|
37
|
+
mode,
|
|
38
|
+
training: mode === "disabled" ? false : settings.training,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...settings,
|
|
44
|
+
failurePolicy: cycleValue(
|
|
45
|
+
FAILURE_POLICIES,
|
|
46
|
+
settings.failurePolicy,
|
|
47
|
+
direction,
|
|
48
|
+
),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function cycleValue<Value>(
|
|
53
|
+
values: ReadonlyArray<Value>,
|
|
54
|
+
current: Value,
|
|
55
|
+
direction: number,
|
|
56
|
+
): Value {
|
|
57
|
+
const currentIndex = values.indexOf(current);
|
|
58
|
+
const offset = direction < 0 ? values.length - 1 : 1;
|
|
59
|
+
return values[(currentIndex + offset) % values.length] ?? current;
|
|
60
|
+
}
|
package/src/state.ts
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import { Context, Effect, Layer, Option, Schema } from "effect";
|
|
2
2
|
import { analyze, type CommandAnalysis } from "./analyze.ts";
|
|
3
3
|
import { Environment } from "./key.ts";
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Any JSON-compatible value.
|
|
13
|
-
*/
|
|
14
|
-
type JsonValue = string | number | boolean | null | JsonValue[] | JsonObject;
|
|
4
|
+
import type {
|
|
5
|
+
CommandState,
|
|
6
|
+
GitState,
|
|
7
|
+
Host,
|
|
8
|
+
JsonValue,
|
|
9
|
+
RenderedCommandState,
|
|
10
|
+
} from "./types.ts";
|
|
15
11
|
|
|
16
12
|
/**
|
|
17
13
|
* Milliseconds to wait for git before giving up and judging without it.
|
|
@@ -204,8 +200,8 @@ export function gatherState(
|
|
|
204
200
|
* @param state - Collected command state
|
|
205
201
|
* @returns A plain JSON object suitable for the `state` field
|
|
206
202
|
*/
|
|
207
|
-
export function renderState(state: CommandState):
|
|
208
|
-
const out:
|
|
203
|
+
export function renderState(state: CommandState): RenderedCommandState {
|
|
204
|
+
const out: { [key: string]: JsonValue } = {
|
|
209
205
|
command: state.command,
|
|
210
206
|
working_directory: state.cwd,
|
|
211
207
|
requesting_agent: state.agent,
|
|
@@ -245,11 +241,13 @@ export function renderState(state: CommandState): JsonObject {
|
|
|
245
241
|
* @param analysis - Analysis produced by {@link analyze}
|
|
246
242
|
* @returns A JSON object describing what code determined
|
|
247
243
|
*/
|
|
248
|
-
export function renderAnalysis(
|
|
249
|
-
|
|
244
|
+
export function renderAnalysis(
|
|
245
|
+
analysis: CommandAnalysis,
|
|
246
|
+
): RenderedCommandState {
|
|
247
|
+
const out: { [key: string]: JsonValue } = {
|
|
250
248
|
note: "A breakdown of what this command will do when it runs. `program` is the executable that will actually be invoked, after removing quotes and stripping wrappers such as env or sudo. `paths` are resolved for ~, $TMPDIR, and .. before being located. Every command listed here executes.",
|
|
251
249
|
commands: analysis.segments.map((segment) => {
|
|
252
|
-
const entry:
|
|
250
|
+
const entry: { [key: string]: JsonValue } = {
|
|
253
251
|
program: segment.argv0 ?? "(could not determine)",
|
|
254
252
|
arguments: segment.args,
|
|
255
253
|
};
|