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/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
- * ALWAYS routed to the human, never auto-allowed
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
- * ALWAYS routed to the human, never auto-allowed
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
- const user = JSON.stringify({
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
- const content = await withLlmRetry(() => chatCompletion(cfg, RISK_SYSTEM_PROMPT, user, nowFetch).then((r) => (r.ok ? r.content : undefined)));
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
- * Settle a learning candidate after the call finished (`tools/result`).
121
- * A result arriving for a registered pending candidate means the human
122
- * approved it and it actually executed — record one confirmation.
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
- settleExecution(exec: ToolExecutionLike): void;
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 {};