dsh-perm-gate 0.2.0 → 0.2.1-beta.2
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/CHANGELOG.ja.md +97 -0
- package/CHANGELOG.ko.md +96 -0
- package/CHANGELOG.md +146 -0
- package/INSTALL.ja.md +2 -2
- package/INSTALL.ko.md +2 -2
- package/INSTALL.md +3 -2
- package/INSTALL.zh.md +2 -2
- package/README.ja.md +29 -6
- package/README.ko.md +37 -8
- package/README.md +73 -11
- package/README.zh.md +44 -9
- package/cordis.patch.yml +19 -12
- package/lib/audit.d.ts +1 -1
- package/lib/classifier.d.ts +2 -0
- package/lib/classifier.js +7 -4
- package/lib/client.js +1786 -53
- package/lib/client.js.map +1 -1
- package/lib/config.d.ts +63 -4
- package/lib/config.js +50 -2
- package/lib/deny-defaults.d.ts +12 -0
- package/lib/deny-defaults.js +19 -0
- package/lib/engine.d.ts +13 -1
- package/lib/engine.js +85 -8
- package/lib/evaluate.d.ts +10 -0
- package/lib/evaluate.js +101 -11
- package/lib/events.d.ts +140 -4
- package/lib/events.js +578 -13
- package/lib/host-llm.d.ts +61 -0
- package/lib/host-llm.js +52 -0
- package/lib/index.d.ts +15 -1
- package/lib/index.js +266 -23
- package/lib/learning.d.ts +19 -2
- package/lib/learning.js +47 -5
- package/lib/llm-presets.d.ts +16 -0
- package/lib/llm-presets.js +11 -0
- package/lib/preset.d.ts +30 -0
- package/lib/preset.js +41 -0
- package/lib/receiver-info.d.ts +76 -0
- package/lib/receiver-info.js +70 -0
- package/lib/risk.d.ts +16 -1
- package/lib/risk.js +15 -3
- package/lib/runtime.d.ts +210 -4
- package/lib/runtime.js +589 -33
- package/package.json +6 -5
package/lib/preset.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session permission-preset awareness for dsh-perm-gate.
|
|
3
|
+
*
|
|
4
|
+
* `dsh-permission-presets` folds three durable knob events per session
|
|
5
|
+
* (`permission/preset`, `sandbox/mode`, `approval/policy`) and pins an initial
|
|
6
|
+
* `permission/preset` at session creation, so the last `permission/preset`
|
|
7
|
+
* event names the tier the user selected. The gate reads that fold to decide
|
|
8
|
+
* whether its own approval tier is the active one.
|
|
9
|
+
*
|
|
10
|
+
* Reading the session log directly (instead of requiring the
|
|
11
|
+
* `permissionPresets` service) keeps the gate usable in a bare embedding and
|
|
12
|
+
* avoids a hard dependency on a service that may not be composed. The fold
|
|
13
|
+
* matches `effectivePermissionPreset` in dsh-permission-presets: last event
|
|
14
|
+
* wins.
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Fold the last selected permission preset out of one session's event log.
|
|
18
|
+
* @param events - the session's events in log order.
|
|
19
|
+
* @returns the preset name, or `undefined` when the log records none.
|
|
20
|
+
*/
|
|
21
|
+
export function permissionPresetOf(events) {
|
|
22
|
+
if (events === undefined)
|
|
23
|
+
return undefined;
|
|
24
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
25
|
+
const event = events[index];
|
|
26
|
+
if (event?.type !== 'permission/preset')
|
|
27
|
+
continue;
|
|
28
|
+
const preset = event.data?.preset;
|
|
29
|
+
if (typeof preset === 'string' && preset !== '')
|
|
30
|
+
return preset;
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/** Whether one preset name is covered by a configured scope list. */
|
|
35
|
+
export function presetInScope(preset, scope) {
|
|
36
|
+
if (scope.includes('*'))
|
|
37
|
+
return true;
|
|
38
|
+
if (preset === undefined)
|
|
39
|
+
return false;
|
|
40
|
+
return scope.includes(preset);
|
|
41
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receiver introspection for the settings card: which LLM the gate will
|
|
3
|
+
* actually use right now, plus (in host mode) the live provider/model-group
|
|
4
|
+
* catalog from the DSH `llm` service — including user-configured custom
|
|
5
|
+
* groups (e.g. a self-hosted provider), the same projection the harness's
|
|
6
|
+
* model catalog builds via `listProviders` / `listModels`.
|
|
7
|
+
*
|
|
8
|
+
* Pure projection: never throws, per-provider failures are reported inline so
|
|
9
|
+
* one broken group never blanks the whole list.
|
|
10
|
+
*/
|
|
11
|
+
/** One model entry as offered by a provider group. */
|
|
12
|
+
export interface ReceiverModel {
|
|
13
|
+
readonly id: string;
|
|
14
|
+
readonly name: string;
|
|
15
|
+
}
|
|
16
|
+
/** One provider group and its models (or the reason it failed to enumerate). */
|
|
17
|
+
export interface ReceiverProvider {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly models: readonly ReceiverModel[];
|
|
21
|
+
readonly error?: string;
|
|
22
|
+
}
|
|
23
|
+
/** What the settings card needs to render the receiver UI. */
|
|
24
|
+
export interface ReceiverInfo {
|
|
25
|
+
readonly source: 'custom' | 'host';
|
|
26
|
+
/** The provider/model the gate will use right now (null when undetermined). */
|
|
27
|
+
readonly selection: {
|
|
28
|
+
readonly provider: string;
|
|
29
|
+
readonly model: string;
|
|
30
|
+
} | null;
|
|
31
|
+
/** Live provider groups; non-empty only in host mode. */
|
|
32
|
+
readonly providers: readonly ReceiverProvider[];
|
|
33
|
+
}
|
|
34
|
+
/** The minimal faces of the host services this projection reads. */
|
|
35
|
+
export interface ReceiverDeps {
|
|
36
|
+
source: 'custom' | 'host';
|
|
37
|
+
/** Host `llm` service (host mode only). */
|
|
38
|
+
llm?: {
|
|
39
|
+
listProviders?: () => readonly {
|
|
40
|
+
id: string;
|
|
41
|
+
name: string;
|
|
42
|
+
}[];
|
|
43
|
+
listModels?: (providerId: string) => Promise<readonly {
|
|
44
|
+
id: string;
|
|
45
|
+
name: string;
|
|
46
|
+
}[]>;
|
|
47
|
+
listConfigurableProviders?: () => readonly {
|
|
48
|
+
provider: string;
|
|
49
|
+
settingsNs: string;
|
|
50
|
+
}[];
|
|
51
|
+
/** Discovery for a configured route answers from the adapter's own knowledge — no network call. */
|
|
52
|
+
discoverModels?: (settingsNs: string, request: {
|
|
53
|
+
provider?: string;
|
|
54
|
+
}) => Promise<readonly {
|
|
55
|
+
id: string;
|
|
56
|
+
name?: string;
|
|
57
|
+
}[]>;
|
|
58
|
+
};
|
|
59
|
+
/** Live session selection (agentDefaultModel.currentSelection). */
|
|
60
|
+
currentSelection?: () => {
|
|
61
|
+
provider?: unknown;
|
|
62
|
+
model?: unknown;
|
|
63
|
+
} | undefined;
|
|
64
|
+
/** Namespace overrides (empty string = follow the session). */
|
|
65
|
+
overrideProvider?: string;
|
|
66
|
+
overrideModel?: string;
|
|
67
|
+
/** The model field as configured for the custom receiver. */
|
|
68
|
+
customModel?: string;
|
|
69
|
+
}
|
|
70
|
+
/** Resolve the effective host model selection (override → session → default). */
|
|
71
|
+
export declare function resolveHostSelection(deps: ReceiverDeps): {
|
|
72
|
+
provider: string;
|
|
73
|
+
model: string;
|
|
74
|
+
};
|
|
75
|
+
/** Build the receiver projection. Never throws. */
|
|
76
|
+
export declare function buildReceiverInfo(deps: ReceiverDeps): Promise<ReceiverInfo>;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Receiver introspection for the settings card: which LLM the gate will
|
|
3
|
+
* actually use right now, plus (in host mode) the live provider/model-group
|
|
4
|
+
* catalog from the DSH `llm` service — including user-configured custom
|
|
5
|
+
* groups (e.g. a self-hosted provider), the same projection the harness's
|
|
6
|
+
* model catalog builds via `listProviders` / `listModels`.
|
|
7
|
+
*
|
|
8
|
+
* Pure projection: never throws, per-provider failures are reported inline so
|
|
9
|
+
* one broken group never blanks the whole list.
|
|
10
|
+
*/
|
|
11
|
+
function str(v) {
|
|
12
|
+
return typeof v === 'string' ? v : '';
|
|
13
|
+
}
|
|
14
|
+
/** Resolve the effective host model selection (override → session → default). */
|
|
15
|
+
export function resolveHostSelection(deps) {
|
|
16
|
+
const sel = deps.currentSelection?.();
|
|
17
|
+
const provider = deps.overrideProvider !== '' && deps.overrideProvider !== undefined
|
|
18
|
+
? deps.overrideProvider
|
|
19
|
+
: (str(sel?.provider) !== '' ? str(sel?.provider) : 'deepseek-official');
|
|
20
|
+
const model = deps.overrideModel !== '' && deps.overrideModel !== undefined
|
|
21
|
+
? deps.overrideModel
|
|
22
|
+
: (str(sel?.model) !== '' ? str(sel?.model) : 'deepseek-v4-flash');
|
|
23
|
+
return { provider, model };
|
|
24
|
+
}
|
|
25
|
+
/** Build the receiver projection. Never throws. */
|
|
26
|
+
export async function buildReceiverInfo(deps) {
|
|
27
|
+
if (deps.source !== 'host' || deps.llm?.listProviders === undefined) {
|
|
28
|
+
return {
|
|
29
|
+
source: deps.source,
|
|
30
|
+
selection: deps.source === 'custom' && str(deps.customModel) !== ''
|
|
31
|
+
? { provider: 'custom', model: str(deps.customModel) }
|
|
32
|
+
: null,
|
|
33
|
+
providers: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const selection = resolveHostSelection(deps);
|
|
37
|
+
const raw = deps.llm.listProviders();
|
|
38
|
+
const providers = await Promise.all(raw.map(async (p) => {
|
|
39
|
+
let models = [];
|
|
40
|
+
let error;
|
|
41
|
+
try {
|
|
42
|
+
models = (await deps.llm?.listModels?.(p.id) ?? []).map((m) => ({ id: m.id, name: m.name }));
|
|
43
|
+
}
|
|
44
|
+
catch (e) {
|
|
45
|
+
error = String(e?.message ?? e);
|
|
46
|
+
}
|
|
47
|
+
if (models.length === 0) {
|
|
48
|
+
// Fallback: route discovery. For an already-configured route the
|
|
49
|
+
// adapter answers from its own stored knowledge (no network call) —
|
|
50
|
+
// this also covers builds whose `listModels` is absent or dormant.
|
|
51
|
+
try {
|
|
52
|
+
const entry = deps.llm?.listConfigurableProviders?.().find((c) => c.provider === p.id);
|
|
53
|
+
if (entry !== undefined && deps.llm?.discoverModels !== undefined) {
|
|
54
|
+
const discovered = await deps.llm.discoverModels(entry.settingsNs, { provider: p.id });
|
|
55
|
+
if (discovered.length > 0) {
|
|
56
|
+
models = discovered.map((m) => ({ id: m.id, name: m.name ?? m.id }));
|
|
57
|
+
error = undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// keep the primary error / empty state
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (models.length === 0 && error === undefined)
|
|
66
|
+
error = 'no models advertised';
|
|
67
|
+
return { id: p.id, name: p.name, models, ...(error === undefined ? {} : { error }) };
|
|
68
|
+
}));
|
|
69
|
+
return { source: 'host', selection, providers };
|
|
70
|
+
}
|
package/lib/risk.d.ts
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* safe → the gate may auto-allow the ask
|
|
8
8
|
* risky:<hard category> → deletion / credential / remote / system / bulk —
|
|
9
|
-
*
|
|
9
|
+
* auto-deny: the operation is clearly dangerous,
|
|
10
|
+
* no popup or human review needed
|
|
10
11
|
* risky:neutral → no hard-risk signal but not clearly safe; the
|
|
11
12
|
* verdict-learning path may auto-allow after
|
|
12
13
|
* enough human confirmations
|
|
@@ -56,3 +57,17 @@ export declare function parseRiskVerdict(content: string): RiskVerdict | undefin
|
|
|
56
57
|
* any failure, timeout, or protocol violation resolves `unresolved` — never throws.
|
|
57
58
|
*/
|
|
58
59
|
export declare function classifyRisk(cfg: ClassifierConfig, req: RiskRequest, nowFetch?: typeof fetch): Promise<RiskVerdict>;
|
|
60
|
+
/** A transport-agnostic completion: system+user prompt → assistant text. */
|
|
61
|
+
export type RiskSend = (system: string, user: string) => Promise<{
|
|
62
|
+
ok: true;
|
|
63
|
+
content: string;
|
|
64
|
+
} | {
|
|
65
|
+
ok: false;
|
|
66
|
+
}>;
|
|
67
|
+
/** The bounded user prompt for one risk grading (shared by every transport). */
|
|
68
|
+
export declare function riskUserText(req: RiskRequest): string;
|
|
69
|
+
/**
|
|
70
|
+
* Grade one tool call through any transport. Up to two attempts (one retry);
|
|
71
|
+
* any failure, timeout, or protocol violation resolves `unresolved` — never throws.
|
|
72
|
+
*/
|
|
73
|
+
export declare function classifyRiskWith(send: RiskSend, req: RiskRequest): Promise<RiskVerdict>;
|
package/lib/risk.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* safe → the gate may auto-allow the ask
|
|
8
8
|
* risky:<hard category> → deletion / credential / remote / system / bulk —
|
|
9
|
-
*
|
|
9
|
+
* auto-deny: the operation is clearly dangerous,
|
|
10
|
+
* no popup or human review needed
|
|
10
11
|
* risky:neutral → no hard-risk signal but not clearly safe; the
|
|
11
12
|
* verdict-learning path may auto-allow after
|
|
12
13
|
* enough human confirmations
|
|
@@ -87,13 +88,24 @@ export function parseRiskVerdict(content) {
|
|
|
87
88
|
* any failure, timeout, or protocol violation resolves `unresolved` — never throws.
|
|
88
89
|
*/
|
|
89
90
|
export async function classifyRisk(cfg, req, nowFetch = fetch) {
|
|
90
|
-
|
|
91
|
+
return classifyRiskWith((system, user) => chatCompletion(cfg, system, user, nowFetch), req);
|
|
92
|
+
}
|
|
93
|
+
/** The bounded user prompt for one risk grading (shared by every transport). */
|
|
94
|
+
export function riskUserText(req) {
|
|
95
|
+
return JSON.stringify({
|
|
91
96
|
tool: req.tool,
|
|
92
97
|
args: req.args ?? {},
|
|
93
98
|
reason: req.reason,
|
|
94
99
|
instruction: 'Reply strictly as one JSON object: {"risk":"safe"} or {"risk":"risky","category":"..."} plus a short "reason".',
|
|
95
100
|
});
|
|
96
|
-
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Grade one tool call through any transport. Up to two attempts (one retry);
|
|
104
|
+
* any failure, timeout, or protocol violation resolves `unresolved` — never throws.
|
|
105
|
+
*/
|
|
106
|
+
export async function classifyRiskWith(send, req) {
|
|
107
|
+
const user = riskUserText(req);
|
|
108
|
+
const content = await withLlmRetry(() => send(RISK_SYSTEM_PROMPT, user).then((r) => (r.ok ? r.content : undefined)));
|
|
97
109
|
if (content === undefined)
|
|
98
110
|
return { kind: 'unresolved' };
|
|
99
111
|
return parseRiskVerdict(content) ?? { kind: 'unresolved' };
|
package/lib/runtime.d.ts
CHANGED
|
@@ -4,7 +4,9 @@ import type { ClassifierConfig } from './classifier.js';
|
|
|
4
4
|
import { EventLog } from './events.js';
|
|
5
5
|
import { RiskLearning } from './learning.js';
|
|
6
6
|
import { type RiskRequest, type RiskVerdict } from './risk.js';
|
|
7
|
+
import { type HostLlmLike, type HostModelSelection } from './host-llm.js';
|
|
7
8
|
import { type CompiledRuleset } from './rule.js';
|
|
9
|
+
import { type SessionEventLike } from './preset.js';
|
|
8
10
|
export interface PreToolDecisionLike {
|
|
9
11
|
kind: 'deny' | 'ask';
|
|
10
12
|
reason: string;
|
|
@@ -17,6 +19,50 @@ export interface ToolExecutionLike {
|
|
|
17
19
|
readonly sessionId?: string;
|
|
18
20
|
readonly parentAuthorized?: boolean;
|
|
19
21
|
readonly signal?: AbortSignal;
|
|
22
|
+
/** Host-minted execution id; the correlation key for an ask's terminal answer. */
|
|
23
|
+
readonly callId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* The host agent face. DSH carries the session here — `exec.sessionId` and
|
|
26
|
+
* `exec.cwd` are usually absent — so the session id and workspace cwd must be
|
|
27
|
+
* read from `agent.session.id` / `agent.session.header.cwd`.
|
|
28
|
+
*/
|
|
29
|
+
readonly agent?: {
|
|
30
|
+
readonly session?: {
|
|
31
|
+
readonly id?: string;
|
|
32
|
+
readonly header?: {
|
|
33
|
+
readonly cwd?: string;
|
|
34
|
+
};
|
|
35
|
+
/** The durable event log; the permission-preset fold reads it. */
|
|
36
|
+
readonly events?: readonly SessionEventLike[];
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The face of a `tools/result` payload this plugin inspects: a human rejection
|
|
42
|
+
* surfaces as `isError` plus the approval seam's reason message (see findings E3).
|
|
43
|
+
*/
|
|
44
|
+
export interface ToolResultLike {
|
|
45
|
+
readonly isError?: boolean;
|
|
46
|
+
readonly error?: {
|
|
47
|
+
readonly message?: string;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* One ask awaiting the human's answer. The event context is snapshotted when
|
|
52
|
+
* the ask is recorded, because the `approval/request` observer has no execution
|
|
53
|
+
* object of its own — the terminal event must still carry the right session and
|
|
54
|
+
* file chips.
|
|
55
|
+
*/
|
|
56
|
+
interface PendingAsk {
|
|
57
|
+
readonly reason: string;
|
|
58
|
+
readonly tool: string;
|
|
59
|
+
readonly sessionId: string;
|
|
60
|
+
readonly files: readonly string[];
|
|
61
|
+
readonly baseDir?: string;
|
|
62
|
+
/** Snapshot file key: used to load pre-change snapshots for review. */
|
|
63
|
+
readonly snapshotId: number;
|
|
64
|
+
/** Set when this ask also registered a learning candidate (drives the progress chip). */
|
|
65
|
+
readonly learningKey?: string;
|
|
20
66
|
}
|
|
21
67
|
export interface PermGateRuntimeOptions extends PermGateConfig {
|
|
22
68
|
readonly onRulesChanged?: (ruleset: CompiledRuleset) => void;
|
|
@@ -30,15 +76,54 @@ export interface PermGateRuntimeOptions extends PermGateConfig {
|
|
|
30
76
|
readonly readPermissive?: () => PermissiveState;
|
|
31
77
|
/** Optional live source of the llmAssist classifier setup. */
|
|
32
78
|
readonly readClassifyConfig?: () => ClassifierConfig;
|
|
79
|
+
/**
|
|
80
|
+
* Optional live receiver source: `'custom'` (an OpenAI-compatible endpoint,
|
|
81
|
+
* default) or `'host'` (the DSH host `llm` service — the model-group setup
|
|
82
|
+
* the user already configured, via {@link hostLlm}).
|
|
83
|
+
*/
|
|
84
|
+
readonly readClassifySource?: () => 'custom' | 'host';
|
|
85
|
+
/** The host `llm` service (typed minimally); `undefined` disables the host receiver. */
|
|
86
|
+
readonly hostLlm?: HostLlmLike;
|
|
87
|
+
/**
|
|
88
|
+
* Live model-group selection for the host receiver (e.g. DSH's
|
|
89
|
+
* agentDefaultModel.currentSelection, possibly overridden from settings).
|
|
90
|
+
*/
|
|
91
|
+
readonly readHostModel?: () => HostModelSelection | undefined;
|
|
33
92
|
/**
|
|
34
93
|
* Optional live source of the verdict-learning state. When unset, the static
|
|
35
94
|
* `riskLearning` / `riskThreshold` options apply.
|
|
36
95
|
*/
|
|
37
96
|
readonly readRiskLearning?: () => RiskLearningState;
|
|
97
|
+
/**
|
|
98
|
+
* Optional live switch for learning sedimentation (default on while verdict
|
|
99
|
+
* learning is enabled): a threshold-reached key's confirmed fingerprint
|
|
100
|
+
* becomes a deterministic auto-allow — no LLM round-trip, and it survives
|
|
101
|
+
* the llmAssist switch because the human confirmations already happened.
|
|
102
|
+
*/
|
|
103
|
+
readonly readRiskSediment?: () => boolean;
|
|
104
|
+
/**
|
|
105
|
+
* Optional live source of the deny-keyword blacklist. `undefined` or an
|
|
106
|
+
* empty array applies the preset `DEFAULT_DENY_KEYWORDS` (the blacklist is
|
|
107
|
+
* protective and never silently off); a non-empty array replaces the preset.
|
|
108
|
+
*/
|
|
109
|
+
readonly readDenyKeywords?: () => readonly string[] | undefined;
|
|
110
|
+
/**
|
|
111
|
+
* Optional live source of the session's effective approval policy (`'ask'` /
|
|
112
|
+
* `'never'`), normally `approval.effectivePolicy(agent.session)`. Under
|
|
113
|
+
* `'never'` the DSH approval seam rejects every request before any answerer
|
|
114
|
+
* runs, so an ask the gate cannot answer degrades to passthrough instead of
|
|
115
|
+
* becoming a denial that reads as "the user rejected tool ...".
|
|
116
|
+
*/
|
|
117
|
+
readonly readApprovalPolicy?: (exec: ToolExecutionLike) => string | undefined;
|
|
38
118
|
/** Persistence path for verdict learning; `undefined` keeps it in memory. */
|
|
39
119
|
readonly learningFile?: string;
|
|
40
120
|
/** Persistence path for the decision-event feed; `undefined` disables it. */
|
|
41
121
|
readonly eventsFile?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Directory for per-event pre-change file snapshots; `undefined` disables the
|
|
124
|
+
* review page's diff/revert data plane (events are still recorded).
|
|
125
|
+
*/
|
|
126
|
+
readonly snapshotsDir?: string;
|
|
42
127
|
}
|
|
43
128
|
export type CallDecision = 'allow' | 'deny' | 'ask';
|
|
44
129
|
/** The effective Permissive tier state read per tool call. */
|
|
@@ -60,10 +145,23 @@ export declare class PermGateRuntime {
|
|
|
60
145
|
private readonly cache;
|
|
61
146
|
private readonly permissiveEnabled;
|
|
62
147
|
private readonly strategies;
|
|
148
|
+
/** Presets in which this gate is active at all; outside them it stands down. */
|
|
149
|
+
private readonly gatePresets;
|
|
150
|
+
/** Extra tool names the user classified as safe (config `autoAllowTools`). */
|
|
151
|
+
private readonly autoAllowExtra;
|
|
152
|
+
/** Cache of the permission-preset fold, keyed by log identity + length. */
|
|
153
|
+
private presetCache;
|
|
63
154
|
private readonly learning;
|
|
64
155
|
private readonly events?;
|
|
65
156
|
/** Learning candidates awaiting human approval + execution (call fingerprint → candidate). */
|
|
66
157
|
private readonly pending;
|
|
158
|
+
/**
|
|
159
|
+
* Asks awaiting the human's terminal answer, keyed by {@link askKey}. Both the
|
|
160
|
+
* approval observer (primary) and `tools/result` (fallback) settle an entry by
|
|
161
|
+
* deleting it — "delete on settle" is the whole de-duplication rule, so a
|
|
162
|
+
* missing `callId` or an untriggered observer can never lose a record.
|
|
163
|
+
*/
|
|
164
|
+
private readonly pendingAsks;
|
|
67
165
|
constructor(options?: PermGateRuntimeOptions);
|
|
68
166
|
private compileInline;
|
|
69
167
|
/** Reload rules from disk (HMR); on failure keeps the previous rules. */
|
|
@@ -79,14 +177,77 @@ export declare class PermGateRuntime {
|
|
|
79
177
|
artifactCount(): number;
|
|
80
178
|
/** The decision-event feed (undefined when event recording is disabled). */
|
|
81
179
|
get eventLog(): EventLog | undefined;
|
|
180
|
+
/** The snapshot directory backing diff/revert (undefined when disabled). */
|
|
181
|
+
get snapshotDir(): string | undefined;
|
|
82
182
|
/** Learning candidates currently awaiting settlement (diagnostics/tests). */
|
|
83
183
|
pendingCount(): number;
|
|
84
184
|
/** Deep read-only view of the learning state (diagnostics/tests). */
|
|
85
185
|
learningSnapshot(): ReturnType<RiskLearning['snapshot']>;
|
|
86
186
|
private ctxFor;
|
|
87
187
|
private liveRiskLearning;
|
|
188
|
+
private liveRiskSediment;
|
|
189
|
+
private liveClassifySource;
|
|
190
|
+
/** The host model-group selection, or undefined when unusable. */
|
|
191
|
+
private hostModel;
|
|
88
192
|
private livePermissive;
|
|
193
|
+
/**
|
|
194
|
+
* The session's selected permission preset, folded from its event log. The
|
|
195
|
+
* fold is cached by the log array identity plus its length: a session's log
|
|
196
|
+
* is a stable, append-only array, so a changed reference or a grown log
|
|
197
|
+
* invalidates the cache while repeated calls on the same log stay cheap.
|
|
198
|
+
*/
|
|
199
|
+
private presetOf;
|
|
200
|
+
/**
|
|
201
|
+
* Whether this gate is active for one call at all.
|
|
202
|
+
*
|
|
203
|
+
* The gate owns an independent permission tier, so it only acts while that
|
|
204
|
+
* tier (or another preset the user listed in `gatePresets`) is selected.
|
|
205
|
+
* Outside it the gate stands down completely — no allow, no ask, no deny, no
|
|
206
|
+
* P0 hard-deny — because the selected tier's own policy governs the session:
|
|
207
|
+
* `danger-full-access` means "full access without approval prompts", where a
|
|
208
|
+
* forwarded ask can only fail (the approval seam rejects before any answerer
|
|
209
|
+
* runs) and a hard-deny would silently overrule the tier the user chose.
|
|
210
|
+
*/
|
|
211
|
+
private gateActive;
|
|
212
|
+
/**
|
|
213
|
+
* Whether an `ask` produced while the gate is active can actually reach a
|
|
214
|
+
* human. Under `approval: never` the DSH approval seam returns `rejected`
|
|
215
|
+
* before any answerer runs, so such an ask degrades to passthrough.
|
|
216
|
+
*
|
|
217
|
+
* The reader is an optional duck-typed seam (the host half probes a PRIVATE
|
|
218
|
+
* approval-service method, identical in DSH 0.1.1-rc.2 and 0.1.2-rc.1). A
|
|
219
|
+
* throwing reader must not kill the decision path: "unknown" is treated as
|
|
220
|
+
* answerable, so the ask stands and the approval seam decides.
|
|
221
|
+
*/
|
|
222
|
+
private askAnswerable;
|
|
223
|
+
/** Effective deny-keyword blacklist: the live namespace override or the preset.
|
|
224
|
+
* An empty override is treated as "no meaningful override" (the blacklist is
|
|
225
|
+
* protective and stays on); schemastery materializes unset arrays as `[]`. */
|
|
226
|
+
private liveDenyKeywords;
|
|
227
|
+
/**
|
|
228
|
+
* The preset deny-keyword layer (inherited from dsh-approval-gate): a keyword
|
|
229
|
+
* hit over the call's command/argument text vetoes it with `deny` before any
|
|
230
|
+
* allow path (deny wins over allow). Purely additive to P0 — it can only ever
|
|
231
|
+
* deny, never widen. Matching is boundary-aware and skips document bodies, so
|
|
232
|
+
* a keyword can no longer veto an unrelated identifier or a file's text.
|
|
233
|
+
*/
|
|
234
|
+
private denyKeywordHit;
|
|
235
|
+
/**
|
|
236
|
+
* Record one decision event on the feed. `verdict` labels the decision path
|
|
237
|
+
* for the history view; `files` (defaulted from the call itself) drives the
|
|
238
|
+
* snapshot/diff/revert chain, and `baseDir` resolves relative paths.
|
|
239
|
+
*/
|
|
89
240
|
private recordEvent;
|
|
241
|
+
/** The correlation key of one ask: its call id, or a canonical call fingerprint. */
|
|
242
|
+
private askKey;
|
|
243
|
+
/** Remember one ask so either settlement channel can record its terminal answer. */
|
|
244
|
+
private trackAsk;
|
|
245
|
+
/** Drop one tracked ask without recording it (the call never needed a human). */
|
|
246
|
+
private untrackAsk;
|
|
247
|
+
/** Asks currently awaiting a terminal answer (diagnostics/tests). */
|
|
248
|
+
pendingAskCount(): number;
|
|
249
|
+
/** Get one pending ask by its key (diagnostics/tests only). */
|
|
250
|
+
getPendingAsk(key: string): PendingAsk | undefined;
|
|
90
251
|
/**
|
|
91
252
|
* Decide one tool call. Returns a decision to veto (`deny`/`ask`) or
|
|
92
253
|
* undefined to delegate via `next()` (allow / passthrough). Always records an
|
|
@@ -117,11 +278,55 @@ export declare class PermGateRuntime {
|
|
|
117
278
|
*/
|
|
118
279
|
refineAsk(exec: ToolExecutionLike, decision: PreToolDecisionLike): Promise<PreToolDecisionLike | undefined>;
|
|
119
280
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
281
|
+
* Record the human's terminal answer for one tracked ask. This is the primary
|
|
282
|
+
* channel, driven by the `approval/request` observer, which sees the closed
|
|
283
|
+
* outcome (`allowed-once` / `rejected` / `cancelled` / `unavailable`) without
|
|
284
|
+
* having to parse any reason text.
|
|
285
|
+
*
|
|
286
|
+
* Consuming the entry is the entire de-duplication rule: a second call for the
|
|
287
|
+
* same ask — or a later `tools/result` — finds nothing and records nothing.
|
|
288
|
+
* @returns whether an ask was settled.
|
|
289
|
+
*/
|
|
290
|
+
settleAskOutcome(callId: string, outcome: string): boolean;
|
|
291
|
+
/**
|
|
292
|
+
* Settle after the call finished (`tools/result`): the fallback channel for an
|
|
293
|
+
* ask whose approval never surfaced to the observer (no approval service, a
|
|
294
|
+
* missing `callId`, or an earlier listener short-circuiting the waterfall).
|
|
295
|
+
* Also settles a pending learning candidate: a result for one means the human
|
|
296
|
+
* approved it and it actually executed — one confirmation.
|
|
297
|
+
*/
|
|
298
|
+
settleExecution(exec: ToolExecutionLike, result?: ToolResultLike): void;
|
|
299
|
+
/**
|
|
300
|
+
* Promote a pending learning candidate to a session-level grant: the same
|
|
301
|
+
* operation auto-passes for the rest of the session (bounded by TTL and
|
|
302
|
+
* max-uses) without waiting for human confirmations. This is the "one-click
|
|
303
|
+
* allow in session" shortcut the settings card can expose.
|
|
304
|
+
*
|
|
305
|
+
* @returns `true` when a pending candidate was promoted.
|
|
306
|
+
*/
|
|
307
|
+
approveSessionGrant(exec: ToolExecutionLike): boolean;
|
|
308
|
+
/** Classify one `tools/result` payload into the ask's terminal outcome. */
|
|
309
|
+
private settleAskFromResult;
|
|
310
|
+
/**
|
|
311
|
+
* Write one terminal event from the ask's snapshotted context. The observer
|
|
312
|
+
* has no execution object of its own, so everything here comes from what
|
|
313
|
+
* {@link trackAsk} captured when the ask was recorded.
|
|
314
|
+
*/
|
|
315
|
+
private recordAskOutcome;
|
|
316
|
+
/** The live confirmation threshold the sediment view compares counts against. */
|
|
317
|
+
learningThreshold(): number;
|
|
318
|
+
/** Terminate one key's learning, or drop a single sedimented sample. */
|
|
319
|
+
learningReset(key: string, fp?: string): void;
|
|
320
|
+
/**
|
|
321
|
+
* One minimal completion through the currently configured receiver (host
|
|
322
|
+
* model group or custom endpoint) with latency — the settings card's
|
|
323
|
+
* "health test". Never throws; failures come back as `ok: false` + detail.
|
|
123
324
|
*/
|
|
124
|
-
|
|
325
|
+
healthCheck(): Promise<{
|
|
326
|
+
ok: boolean;
|
|
327
|
+
ms: number;
|
|
328
|
+
detail: string;
|
|
329
|
+
}>;
|
|
125
330
|
/** Mint a precise session grant bound to a canonical call fingerprint. */
|
|
126
331
|
grant(exec: ToolExecutionLike, maxUses: number, ttlMs: number): void;
|
|
127
332
|
/**
|
|
@@ -144,3 +349,4 @@ export declare class PermGateRuntime {
|
|
|
144
349
|
*/
|
|
145
350
|
setAllowlist(patterns: readonly string[]): boolean;
|
|
146
351
|
}
|
|
352
|
+
export {};
|