pi-warden 0.5.5 → 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 +17 -10
- package/dist/action-guard.d.ts +50 -0
- package/dist/action-guard.js +63 -0
- package/dist/config.d.ts +8 -0
- package/dist/config.js +8 -1
- package/dist/done.d.ts +3 -1
- package/dist/done.js +22 -25
- package/dist/excerpt.d.ts +25 -0
- package/dist/excerpt.js +171 -0
- package/dist/extension.d.ts +3 -6
- package/dist/extension.js +63 -65
- package/dist/guard.d.ts +3 -3
- package/dist/guard.js +73 -80
- package/dist/index.d.ts +11 -5
- package/dist/index.js +6 -3
- package/dist/jev.d.ts +23 -0
- package/dist/jev.js +19 -0
- package/dist/output.d.ts +28 -5
- package/dist/output.js +54 -30
- package/dist/prose.d.ts +3 -1
- package/dist/prose.js +8 -11
- package/dist/recall.d.ts +13 -0
- package/dist/recall.js +68 -0
- package/dist/saver.d.ts +23 -6
- package/dist/saver.js +44 -6
- package/dist/shape.js +6 -1
- package/dist/stuck.d.ts +3 -1
- package/dist/stuck.js +17 -20
- package/package.json +1 -1
package/dist/extension.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { MouseRegion, Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { createTypeSafe, resolveApiKey } from "pi-typesafe";
|
|
3
3
|
import { ensureApiKey } from "pi-typesafe/ui";
|
|
4
|
+
import { ActionGuard } from "./action-guard.js";
|
|
4
5
|
import * as configModule from "./config.js";
|
|
5
6
|
import { applyUserOverrides, defaultConfig, isMode, loadConfig, PACKAGE_NAME, projectConfigPath, readUserConfig, setUserSetting, userConfigPath, writeUserConfig } from "./config.js";
|
|
6
7
|
import { classifyToolResult, doneNudge, emptyEvidence, evaluateDone, finalAssistantText, formatDone, needsDoneCheck, recordOutcome } from "./done.js";
|
|
7
|
-
import { evaluateAction, formatVerdict, SLOP_LABELS, steerReason
|
|
8
|
+
import { evaluateAction, formatVerdict, SLOP_LABELS, steerReason } from "./guard.js";
|
|
8
9
|
import { evaluateProse, proseNudge, ProseTrend } from "./prose.js";
|
|
9
|
-
import { compressOutput, evaluateOutput, saveOutput, securityNotice } from "./output.js";
|
|
10
|
+
import { compressOutput, duplicateNote, evaluateOutput, outputKey, saveOutput, securityNotice } from "./output.js";
|
|
11
|
+
import { classifyRecall, detectSearchTool, recallInstruction } from "./recall.js";
|
|
10
12
|
import { redact } from "./redact.js";
|
|
11
13
|
import { AttemptWindow, evaluateStuck, formatStuck, makeAttempt, resultFailed, stuckNudge } from "./stuck.js";
|
|
12
14
|
import { openTracePanel } from "./panel.js";
|
|
@@ -14,7 +16,7 @@ import { completeConfig, shapeWarning } from "./shape.js";
|
|
|
14
16
|
import { ContextLedger, formatLedger } from "./saver.js";
|
|
15
17
|
import { actionDetails, doneDetails, proseDetails, stuckDetails, Trace } from "./trace.js";
|
|
16
18
|
import { proseTokens, renderTemplate, TOKEN_NAMES } from "./widget.js";
|
|
17
|
-
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and
|
|
19
|
+
export const disclosure = "With TypeSafe judgments enabled, pi-warden sends to api.typesafe.ai: your latest request and up to eight redacted prior user/assistant text messages for task context, plus a redacted, truncated summary of each guarded bash, write, or edit call before it runs; the last few tool calls and output tails when the agent keeps failing; the agent's final message when it reports completion without running checks; and redacted tool-output samples for security and context saving (retention and output format). Compression and duplicate notes store an exact, owner-only copy in a temporary file on this machine. Requests may incur charges. Secret redaction is best-effort. Results are model judgments, not proof or authorization; offline pattern checks stay active either way.";
|
|
18
20
|
const WIDGET = PACKAGE_NAME;
|
|
19
21
|
const CONFIRM_TEXT_LIMIT = 500;
|
|
20
22
|
const freshStats = () => ({ inspected: 0, judged: 0, warned: 0, held: 0, approved: 0, slop: 0, stuckChecks: 0, stuck: 0, doneChecks: 0, unverified: 0, proseChecks: 0, proseNudges: 0, errors: 0 });
|
|
@@ -69,7 +71,7 @@ export function siblingToolCalls(ctx) {
|
|
|
69
71
|
if (!Array.isArray(content))
|
|
70
72
|
return [];
|
|
71
73
|
return content.flatMap(part => part.type === "toolCall" && typeof part.id === "string" && typeof part.name === "string"
|
|
72
|
-
? [{ id: part.id,
|
|
74
|
+
? [{ id: part.id, tool: part.name, input: (part.arguments ?? {}) }]
|
|
73
75
|
: []);
|
|
74
76
|
}
|
|
75
77
|
return [];
|
|
@@ -119,21 +121,15 @@ export default function wardenExtension(pi) {
|
|
|
119
121
|
const trace = new Trace();
|
|
120
122
|
let panel;
|
|
121
123
|
let lastUi;
|
|
122
|
-
|
|
123
|
-
const prejudged = new Map();
|
|
124
|
-
/**
|
|
125
|
-
* Steer-mode hold state. After a hold, the next guarded call that runs under a *new* user prompt asks Jev whether that
|
|
126
|
-
* prompt approves it. The retry rarely repeats the held string byte for byte (a `command -v` dropped, a different
|
|
127
|
-
* timeout), so approval is judged against the action itself, not matched against the earlier command text.
|
|
128
|
-
*/
|
|
129
|
-
let lastHoldPrompt;
|
|
130
|
-
let holdPending = false;
|
|
124
|
+
const actionGuard = new ActionGuard();
|
|
131
125
|
let attempts = new AttemptWindow(defaultConfig().stuck.window);
|
|
132
126
|
let evidence = emptyEvidence();
|
|
133
127
|
let doneNudged = false;
|
|
134
128
|
const prose = new ProseTrend();
|
|
135
129
|
const slopCounts = { stub: 0, comments: 0, dead: 0, hedging: 0 };
|
|
136
130
|
const ledger = new ContextLedger();
|
|
131
|
+
// Probed once per session, outside any tool_result handler; the footer under excerpts names this command.
|
|
132
|
+
let searchTool;
|
|
137
133
|
// A partially updated module graph can hand this build a config without the sections it expects; see shape.ts.
|
|
138
134
|
let shapeReported = false;
|
|
139
135
|
const configFor = (ctx) => {
|
|
@@ -206,13 +202,13 @@ export default function wardenExtension(pi) {
|
|
|
206
202
|
widget.clear();
|
|
207
203
|
trace.clear();
|
|
208
204
|
panel?.close();
|
|
209
|
-
|
|
210
|
-
holdPending = false;
|
|
205
|
+
actionGuard.reset();
|
|
211
206
|
attempts.reset();
|
|
212
207
|
evidence = emptyEvidence();
|
|
213
208
|
doneNudged = false;
|
|
214
209
|
prose.reset();
|
|
215
210
|
ledger.reset();
|
|
211
|
+
searchTool = undefined;
|
|
216
212
|
for (const symptom of Object.keys(slopCounts))
|
|
217
213
|
slopCounts[symptom] = 0;
|
|
218
214
|
if (ctx.hasUI)
|
|
@@ -222,7 +218,7 @@ export default function wardenExtension(pi) {
|
|
|
222
218
|
pi.on("before_agent_start", async (_event, ctx) => {
|
|
223
219
|
attempts = new AttemptWindow(configFor(ctx).stuck.window);
|
|
224
220
|
doneNudged = false;
|
|
225
|
-
|
|
221
|
+
actionGuard.turnEnd();
|
|
226
222
|
});
|
|
227
223
|
// Each low-level run collects its own evidence of changes and checks.
|
|
228
224
|
pi.on("agent_start", async () => {
|
|
@@ -231,44 +227,28 @@ export default function wardenExtension(pi) {
|
|
|
231
227
|
// Every turn that runs after a compression is a turn that did not carry the removed text.
|
|
232
228
|
pi.on("turn_end", async () => {
|
|
233
229
|
ledger.turnEnd();
|
|
234
|
-
|
|
235
|
-
prejudged.clear();
|
|
230
|
+
actionGuard.turnEnd();
|
|
236
231
|
});
|
|
237
232
|
pi.on("tool_call", async (event, ctx) => {
|
|
238
233
|
const config = configFor(ctx);
|
|
239
234
|
if (!config.enabled)
|
|
240
235
|
return;
|
|
241
236
|
// A read of a stored full output means the excerpt was not enough; that is the number that tunes context.confidence.
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
237
|
+
// A whole-file read also undoes the saving, so the kind of access is kept apart.
|
|
238
|
+
const serializedInput = JSON.stringify(event.input);
|
|
239
|
+
const storedPath = ledger.storedPathIn(serializedInput);
|
|
240
|
+
if (storedPath) {
|
|
241
|
+
const kind = classifyRecall(event.toolName, event.input, storedPath);
|
|
242
|
+
const recalled = ledger.noteAccess(serializedInput, kind);
|
|
243
|
+
if (recalled)
|
|
244
|
+
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: `full output recalled (${kind})` }), [`the agent went back to ${recalled} (${kind === "full" ? "whole-file read" : "scoped access"})`, formatLedger(ledger.snapshot())]);
|
|
245
|
+
}
|
|
245
246
|
if (!config.action.enabled || !config.action.tools.includes(event.toolName))
|
|
246
247
|
return;
|
|
247
248
|
stats.inspected++;
|
|
248
249
|
const task = latestUserPrompt(ctx);
|
|
249
|
-
// A hold happened under an earlier prompt and the user has since replied: ask whether the reply approves this action.
|
|
250
|
-
const retryAfterHold = holdPending && lastHoldPrompt !== task;
|
|
251
250
|
const judge = judgeFor(config);
|
|
252
|
-
const
|
|
253
|
-
const options = { config: config.action, judge, signal: ctx.signal, slop: config.slop, security: config.security, retryAfterHold };
|
|
254
|
-
const judgeCall = (tool, input) => evaluateAction({ tool, input, cwd: ctx.cwd, task, context }, options);
|
|
255
|
-
// Sibling calls in the same assistant message are preflighted one after another; their requests go out together.
|
|
256
|
-
// A retry after a hold stays sequential because an approval consumed by one sibling changes the question for the next.
|
|
257
|
-
if (judge && !retryAfterHold) {
|
|
258
|
-
for (const sibling of siblingToolCalls(ctx)) {
|
|
259
|
-
if (sibling.id === event.toolCallId || prejudged.has(sibling.id) || !config.action.tools.includes(sibling.name))
|
|
260
|
-
continue;
|
|
261
|
-
const verdict = judgeCall(sibling.name, sibling.input);
|
|
262
|
-
verdict.catch(() => undefined);
|
|
263
|
-
prejudged.set(sibling.id, { key: JSON.stringify(sibling.input), verdict, used: false });
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
// An earlier hook may have changed this call's input; a stale judgment is discarded, not reused.
|
|
267
|
-
const key = JSON.stringify(event.input);
|
|
268
|
-
const ready = prejudged.get(event.toolCallId);
|
|
269
|
-
const pending = ready && !ready.used && ready.key === key && !retryAfterHold ? ready.verdict : judgeCall(event.toolName, event.input);
|
|
270
|
-
prejudged.set(event.toolCallId, { key, verdict: pending, used: true });
|
|
271
|
-
const verdict = await pending;
|
|
251
|
+
const verdict = await actionGuard.inspect({ id: event.toolCallId, tool: event.toolName, input: event.input }, { task, context: recentTaskContext(ctx), siblings: siblingToolCalls(ctx) }, { config: config.action, cwd: ctx.cwd, judge, signal: ctx.signal, slop: config.slop, security: config.security });
|
|
272
252
|
if (verdict.source === "skipped")
|
|
273
253
|
return;
|
|
274
254
|
if (verdict.judgment?.securityRisk !== undefined && verdict.judgment.securityRisk >= config.security.threshold) {
|
|
@@ -278,16 +258,8 @@ export default function wardenExtension(pi) {
|
|
|
278
258
|
stats.judged++;
|
|
279
259
|
if (verdict.source === "error")
|
|
280
260
|
noteError(ctx, verdict.error ?? "TypeSafe request failed.", verdict.errorCode);
|
|
281
|
-
|
|
282
|
-
if (retryAfterHold && !judge && verdict.level === "confirm" && textApproves(task)) {
|
|
283
|
-
verdict.level = "allow";
|
|
284
|
-
verdict.approvedByUser = true;
|
|
285
|
-
verdict.reasons = ["user approved in the latest message", ...verdict.reasons];
|
|
286
|
-
}
|
|
287
|
-
if (verdict.approvedByUser) {
|
|
261
|
+
if (verdict.approvedByUser)
|
|
288
262
|
stats.approved++;
|
|
289
|
-
holdPending = false;
|
|
290
|
-
}
|
|
291
263
|
const mode = activeMode(config, ctx.hasUI);
|
|
292
264
|
const told = verdict.level === "confirm" && mode === "steer" ? steerReason(verdict, { canApprove: judge !== undefined }) : undefined;
|
|
293
265
|
if (verdict.source !== "read-only")
|
|
@@ -324,10 +296,7 @@ export default function wardenExtension(pi) {
|
|
|
324
296
|
return { block: true, reason: `pi-warden: the user declined this ${event.toolName} call (${reasons}). Do not retry it unchanged; ask the user how to proceed.` };
|
|
325
297
|
}
|
|
326
298
|
stats.held++;
|
|
327
|
-
|
|
328
|
-
if (!holdPending)
|
|
329
|
-
lastHoldPrompt = task;
|
|
330
|
-
holdPending = true;
|
|
299
|
+
actionGuard.hold(task);
|
|
331
300
|
if (ctx.hasUI)
|
|
332
301
|
ctx.ui.notify(`warden · held ${event.toolName}: ${reasons}. The agent was told why and asked to re-plan or ask you.`, "warning");
|
|
333
302
|
return { block: true, reason: told ?? steerReason(verdict, { canApprove: judge !== undefined }) };
|
|
@@ -349,7 +318,12 @@ export default function wardenExtension(pi) {
|
|
|
349
318
|
stats.stuckChecks++;
|
|
350
319
|
return evaluateStuck(attempts, latestUserPrompt(ctx), { config: config.stuck, judge: judgeFor(config), timeoutMs: config.timeoutMs, signal: ctx.signal });
|
|
351
320
|
})();
|
|
352
|
-
|
|
321
|
+
// A recall brings stored text back on purpose: it is not judged, compressed, or dropped again.
|
|
322
|
+
const recallRead = ledger.storedPathIn(JSON.stringify(event.input)) !== undefined;
|
|
323
|
+
// Duplicate detection is code only: an identical result adds nothing, whatever Jev would say about it.
|
|
324
|
+
const key = config.context.enabled && !recallRead && textBlocks.length === 1 && text.length >= config.context.duplicateMinChars ? outputKey(text) : undefined;
|
|
325
|
+
const earlier = key ? ledger.duplicateOf(key) : undefined;
|
|
326
|
+
const output = earlier || recallRead ? { secret: false, suspicious: false, retention: "all" } : await evaluateOutput(event.toolName, text, latestUserPrompt(ctx), {
|
|
353
327
|
security: config.security, context: config.context, judge: judgeFor(config), timeoutMs: config.timeoutMs,
|
|
354
328
|
signal: ctx.signal, compressible: textBlocks.length === 1, taskContext: recentTaskContext(ctx),
|
|
355
329
|
});
|
|
@@ -359,19 +333,41 @@ export default function wardenExtension(pi) {
|
|
|
359
333
|
noteError(ctx, output.error, output.errorCode);
|
|
360
334
|
let content = event.content;
|
|
361
335
|
const notice = securityNotice(output);
|
|
362
|
-
|
|
336
|
+
const recallTool = await (searchTool ??= detectSearchTool(config.context.recallTool));
|
|
337
|
+
let storedPath;
|
|
338
|
+
if (earlier && key) {
|
|
339
|
+
try {
|
|
340
|
+
storedPath = earlier.path ?? await saveOutput(text);
|
|
341
|
+
const replacement = `${duplicateNote(text, earlier.tool)}\n\n${recallInstruction(recallTool, storedPath)}`;
|
|
342
|
+
const bytesSaved = Buffer.byteLength(text) - Buffer.byteLength(replacement);
|
|
343
|
+
if (bytesSaved > 0) {
|
|
344
|
+
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
345
|
+
ledger.duplicate(bytesSaved);
|
|
346
|
+
ledger.remember(key, earlier.tool, storedPath);
|
|
347
|
+
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: "duplicate", bytesSaved: String(bytesSaved) }), [
|
|
348
|
+
`identical to an earlier ${earlier.tool} result; saved ${bytesSaved} bytes; full output: ${storedPath}`,
|
|
349
|
+
formatLedger(ledger.snapshot()),
|
|
350
|
+
]);
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
catch {
|
|
354
|
+
noteError(ctx, "Could not store full output; keeping it unchanged.", undefined);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (config.context.enabled && !earlier && !recallRead && textBlocks.length === 1 && text.length >= config.context.tailMinChars)
|
|
363
358
|
ledger.candidate();
|
|
364
|
-
const excerpt = compressOutput(text, output.retention);
|
|
359
|
+
const excerpt = earlier ? undefined : compressOutput(text, output.retention, output.format);
|
|
365
360
|
if (excerpt && !ctx.signal?.aborted) {
|
|
366
361
|
try {
|
|
367
362
|
const path = await saveOutput(text);
|
|
368
|
-
const replacement = `${excerpt}\n\
|
|
363
|
+
const replacement = `${excerpt}\n\n${recallInstruction(recallTool, path)}`;
|
|
369
364
|
const bytesSaved = Buffer.byteLength(text) - Buffer.byteLength(replacement) - (notice ? Buffer.byteLength(notice) * 2 + 4 : 0);
|
|
370
365
|
if (bytesSaved > 0) {
|
|
371
366
|
content = content.map(part => part.type === "text" ? { ...part, text: replacement } : part);
|
|
372
367
|
ledger.record(path, bytesSaved);
|
|
368
|
+
storedPath = path;
|
|
373
369
|
record(ctx, config, "context", renderTemplate(config.widget.context, { tool: event.toolName, retention: output.retention, bytesSaved: String(bytesSaved) }), [
|
|
374
|
-
`retention: ${output.retention}; confidence ${output.confidence?.toFixed(2)}; ${output.model}; ${output.elapsedMs} ms`,
|
|
370
|
+
`retention: ${output.retention}; confidence ${output.confidence?.toFixed(2)}; format ${output.format ?? "generic"}${output.formatConfidence === undefined ? "" : ` (${output.formatConfidence.toFixed(2)})`}; ${output.model}; ${output.elapsedMs} ms`,
|
|
375
371
|
`saved ${bytesSaved} bytes; full output: ${path}`,
|
|
376
372
|
formatLedger(ledger.snapshot()),
|
|
377
373
|
]);
|
|
@@ -381,6 +377,8 @@ export default function wardenExtension(pi) {
|
|
|
381
377
|
noteError(ctx, "Could not store full output; keeping it unchanged.", undefined);
|
|
382
378
|
}
|
|
383
379
|
}
|
|
380
|
+
if (key && !earlier)
|
|
381
|
+
ledger.remember(key, event.toolName, storedPath);
|
|
384
382
|
if (notice && textBlocks.length) {
|
|
385
383
|
let index = 0;
|
|
386
384
|
content = content.map(part => {
|
|
@@ -406,7 +404,7 @@ export default function wardenExtension(pi) {
|
|
|
406
404
|
if (!verdict)
|
|
407
405
|
return patch;
|
|
408
406
|
if (verdict.error)
|
|
409
|
-
noteError(ctx, verdict.error,
|
|
407
|
+
noteError(ctx, verdict.error, verdict.errorCode);
|
|
410
408
|
if (verdict.source === "repeat" && !verdict.stuck)
|
|
411
409
|
return patch;
|
|
412
410
|
const nudge = verdict.stuck && config.stuck.nudge ? stuckNudge(verdict) : undefined;
|
|
@@ -440,7 +438,7 @@ export default function wardenExtension(pi) {
|
|
|
440
438
|
stats.proseChecks++;
|
|
441
439
|
const verdict = await proseCheck;
|
|
442
440
|
if (verdict.error)
|
|
443
|
-
noteError(ctx, verdict.error,
|
|
441
|
+
noteError(ctx, verdict.error, verdict.errorCode);
|
|
444
442
|
else
|
|
445
443
|
prose.record(verdict.flagged);
|
|
446
444
|
const due = verdict.error ? [] : prose.due(config.slop.prose.trend);
|
|
@@ -462,7 +460,7 @@ export default function wardenExtension(pi) {
|
|
|
462
460
|
stats.doneChecks++;
|
|
463
461
|
const verdict = await doneCheck;
|
|
464
462
|
if (verdict.error)
|
|
465
|
-
noteError(ctx, verdict.error,
|
|
463
|
+
noteError(ctx, verdict.error, verdict.errorCode);
|
|
466
464
|
const nudge = verdict.unverified && config.done.nudge ? doneNudge(verdict) : undefined;
|
|
467
465
|
record(ctx, config, "done", formatDone(verdict, config.widget.done), doneDetails(verdict, finalMessage, nudge));
|
|
468
466
|
if (!verdict.unverified)
|
|
@@ -505,7 +503,7 @@ export default function wardenExtension(pi) {
|
|
|
505
503
|
const key = resolveApiKey();
|
|
506
504
|
const source = consentSource(config);
|
|
507
505
|
const usage = client?.getUsage();
|
|
508
|
-
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`, config.security.enabled && "security", config.context.enabled && "context
|
|
506
|
+
const guards = [config.action.enabled && "action", config.stuck.enabled && "stuck", config.done.enabled && "done-check", config.slop.enabled && "slop", config.slop.enabled && config.slop.prose.enabled && `prose (${config.slop.prose.audience})`, config.security.enabled && "security", config.context.enabled && "context"].filter(Boolean).join(", ");
|
|
509
507
|
report([
|
|
510
508
|
`pi-warden: ${config.enabled ? `guarding ${config.action.tools.join(", ")} (${guards})` : "off"}; mode ${activeMode(config, ctx.hasUI)}; TypeSafe judgments ${source ? `enabled via ${source}` : "disabled (run /warden enable)"}; key ${key ? key.source === "stored" ? "stored (shared with pi-typesafe)" : "from TYPESAFE_API_KEY" : "missing (run /warden enable)"}.`,
|
|
511
509
|
`Session: ${stats.inspected} inspected, ${stats.judged} judged, ${stats.warned} warned, ${stats.held} held, ${stats.approved} approved on retry, ${stats.slop} slop notes, ${stats.stuck}/${stats.stuckChecks} stuck, ${stats.unverified}/${stats.doneChecks} unverified done, ${stats.proseNudges}/${stats.proseChecks} prose nudges, ${stats.errors} TypeSafe errors; ${usage?.requestsStarted ?? 0}/${config.maxRequests} requests. Steers are ${config.steerVisible ? "shown in the transcript" : "hidden from the transcript (trace panel shows them)"}.`,
|
package/dist/guard.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type { IntegrationErrorCode
|
|
1
|
+
import type { IntegrationErrorCode } from "pi-typesafe";
|
|
2
2
|
import type { ActionGuardConfig, SecurityConfig, SlopGuardConfig } from "./config.js";
|
|
3
|
+
import type { Judge } from "./jev.js";
|
|
3
4
|
export type Level = "allow" | "warn" | "confirm";
|
|
4
5
|
export type Severity = "destructive" | "risky" | "sensitive";
|
|
5
6
|
export interface PatternHit {
|
|
@@ -78,8 +79,7 @@ export interface Verdict {
|
|
|
78
79
|
error?: string;
|
|
79
80
|
errorCode?: IntegrationErrorCode;
|
|
80
81
|
}
|
|
81
|
-
|
|
82
|
-
export type Judge = Pick<TypeSafe, "evaluate">;
|
|
82
|
+
export type { Judge } from "./jev.js";
|
|
83
83
|
export interface EvaluateOptions {
|
|
84
84
|
config: ActionGuardConfig;
|
|
85
85
|
/** Omit to run offline pattern checks only (no consent, no network). */
|
package/dist/guard.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
|
-
import { choice, noul, score
|
|
4
|
+
import { choice, noul, score } from "pi-typesafe";
|
|
5
|
+
import { askJev } from "./jev.js";
|
|
5
6
|
import { redact } from "./redact.js";
|
|
6
7
|
import { commandOf } from "./tools.js";
|
|
7
8
|
import { actionTokens, DEFAULT_TEMPLATES, renderTemplate } from "./widget.js";
|
|
@@ -271,10 +272,6 @@ export function buildRequest(summary, task, extras = {}) {
|
|
|
271
272
|
questions: { ...questions, ...(wantSlop ? slopQuestions : {}), ...(extras.approval ? approvalQuestion : {}), ...(extras.security && (summary.tool === "write" || summary.tool === "edit") && hasContent(summary) ? securityQuestion : {}) },
|
|
272
273
|
};
|
|
273
274
|
}
|
|
274
|
-
function combineSignals(timeoutMs, signal) {
|
|
275
|
-
const timeout = AbortSignal.timeout(timeoutMs);
|
|
276
|
-
return signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
277
|
-
}
|
|
278
275
|
const percent = (value) => value.toFixed(2);
|
|
279
276
|
const APPROVAL_THRESHOLD = 0.7;
|
|
280
277
|
// ---------------------------------------------------------------------------
|
|
@@ -312,91 +309,87 @@ export async function evaluateAction(action, options) {
|
|
|
312
309
|
}
|
|
313
310
|
if (!judge)
|
|
314
311
|
return { level, source: "pattern", summary, patterns, reasons };
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
const judgment = {
|
|
320
|
-
irreversible: answers.irreversible.noul,
|
|
321
|
-
offTask: answers.off_task.noul,
|
|
322
|
-
scope: answers.scope.choice,
|
|
323
|
-
scopeConfidence: answers.scope.confidence,
|
|
324
|
-
model: result.model,
|
|
325
|
-
elapsedMs: result.elapsedMs,
|
|
326
|
-
};
|
|
327
|
-
if (typeof answers.approved?.noul === "number")
|
|
328
|
-
judgment.approved = answers.approved.noul;
|
|
329
|
-
if (typeof answers.mutates?.noul === "number")
|
|
330
|
-
judgment.mutates = answers.mutates.noul;
|
|
331
|
-
if (deferSensitive) {
|
|
332
|
-
for (const hit of patterns) {
|
|
333
|
-
if (hit.severity !== "sensitive")
|
|
334
|
-
continue;
|
|
335
|
-
if ((judgment.mutates ?? 1) >= 0.5) {
|
|
336
|
-
level = higher(level, "warn");
|
|
337
|
-
reasons.push(`${hit.severity}: ${hit.label}`);
|
|
338
|
-
}
|
|
339
|
-
else
|
|
340
|
-
reasons.push(`${hit.label} (read-only, not warned)`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
// write/edit always change something; a command that Jev judges read-only is warned about, never held, for scope alone.
|
|
344
|
-
const canChange = summary.tool === "write" || summary.tool === "edit" || (judgment.mutates ?? 1) >= 0.5;
|
|
345
|
-
if (judgment.irreversible >= config.irreversible.confirm) {
|
|
346
|
-
level = higher(level, "confirm");
|
|
347
|
-
reasons.push(`irreversible ${percent(judgment.irreversible)}`);
|
|
348
|
-
}
|
|
349
|
-
else if (judgment.irreversible >= config.irreversible.warn) {
|
|
350
|
-
level = higher(level, "warn");
|
|
351
|
-
reasons.push(`possibly irreversible ${percent(judgment.irreversible)}`);
|
|
352
|
-
}
|
|
353
|
-
if (judgment.offTask >= config.offTask.confirm && judgment.scope === "unrelated" && canChange) {
|
|
312
|
+
const request = buildRequest(summary, action.task, { slop: options.slop?.enabled ?? false, approval: options.retryAfterHold ?? false, security: options.security?.enabled ?? false, context: action.context });
|
|
313
|
+
const result = await askJev(judge, request, { timeoutMs: config.timeoutMs, signal: options.signal });
|
|
314
|
+
if (!result.ok) {
|
|
315
|
+
if (!config.failOpen) {
|
|
354
316
|
level = higher(level, "confirm");
|
|
355
|
-
reasons.push(
|
|
356
|
-
}
|
|
357
|
-
else if (judgment.offTask >= config.offTask.confirm && judgment.scope === "unrelated") {
|
|
358
|
-
level = higher(level, "warn");
|
|
359
|
-
reasons.push(`off-task ${percent(judgment.offTask)} (unrelated, but read-only: not held)`);
|
|
317
|
+
reasons.push("TypeSafe unavailable and failOpen is false");
|
|
360
318
|
}
|
|
361
|
-
else
|
|
362
|
-
|
|
363
|
-
reasons.push(`off-task ${percent(judgment.offTask)} (${judgment.scope.replace(/_/g, " ")})`);
|
|
319
|
+
else {
|
|
320
|
+
reasons.push("TypeSafe unavailable; allowed by failOpen");
|
|
364
321
|
}
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
322
|
+
return { level, source: "error", summary, patterns, reasons, error: result.error, ...(result.errorCode ? { errorCode: result.errorCode } : {}) };
|
|
323
|
+
}
|
|
324
|
+
const answers = result.answers;
|
|
325
|
+
const judgment = {
|
|
326
|
+
irreversible: answers.irreversible.noul,
|
|
327
|
+
offTask: answers.off_task.noul,
|
|
328
|
+
scope: answers.scope.choice,
|
|
329
|
+
scopeConfidence: answers.scope.confidence,
|
|
330
|
+
model: result.model,
|
|
331
|
+
elapsedMs: result.elapsedMs,
|
|
332
|
+
};
|
|
333
|
+
if (typeof answers.approved?.noul === "number")
|
|
334
|
+
judgment.approved = answers.approved.noul;
|
|
335
|
+
if (typeof answers.mutates?.noul === "number")
|
|
336
|
+
judgment.mutates = answers.mutates.noul;
|
|
337
|
+
if (deferSensitive) {
|
|
338
|
+
for (const hit of patterns) {
|
|
339
|
+
if (hit.severity !== "sensitive")
|
|
340
|
+
continue;
|
|
341
|
+
if ((judgment.mutates ?? 1) >= 0.5) {
|
|
368
342
|
level = higher(level, "warn");
|
|
369
|
-
reasons.push(
|
|
343
|
+
reasons.push(`${hit.severity}: ${hit.label}`);
|
|
370
344
|
}
|
|
345
|
+
else
|
|
346
|
+
reasons.push(`${hit.label} (read-only, not warned)`);
|
|
371
347
|
}
|
|
372
|
-
const verdict = { level, source: "typesafe", summary, patterns, reasons, judgment };
|
|
373
|
-
if (options.slop?.enabled && SLOP_SYMPTOMS.every(symptom => typeof answers[`slop_${symptom}`]?.noul === "number")) {
|
|
374
|
-
verdict.slop = { stub: answers.slop_stub.noul, comments: answers.slop_comments.noul, dead: answers.slop_dead.noul, hedging: answers.slop_hedging.noul };
|
|
375
|
-
const flagged = SLOP_SYMPTOMS.filter(symptom => verdict.slop[symptom] >= options.slop.threshold).sort((a, b) => verdict.slop[b] - verdict.slop[a]);
|
|
376
|
-
if (flagged.length) {
|
|
377
|
-
verdict.slopSymptoms = flagged;
|
|
378
|
-
verdict.slopReasons = flagged.map(symptom => `${SLOP_LABELS[symptom]} (${percent(verdict.slop[symptom])})`);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
if (level === "confirm" && judgment.approved !== undefined && judgment.approved >= APPROVAL_THRESHOLD) {
|
|
382
|
-
verdict.level = "allow";
|
|
383
|
-
verdict.approvedByUser = true;
|
|
384
|
-
verdict.reasons = [`user approved in the latest message (${percent(judgment.approved)})`, ...reasons];
|
|
385
|
-
}
|
|
386
|
-
return verdict;
|
|
387
348
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
349
|
+
// write/edit always change something; a command that Jev judges read-only is warned about, never held, for scope alone.
|
|
350
|
+
const canChange = summary.tool === "write" || summary.tool === "edit" || (judgment.mutates ?? 1) >= 0.5;
|
|
351
|
+
if (judgment.irreversible >= config.irreversible.confirm) {
|
|
352
|
+
level = higher(level, "confirm");
|
|
353
|
+
reasons.push(`irreversible ${percent(judgment.irreversible)}`);
|
|
354
|
+
}
|
|
355
|
+
else if (judgment.irreversible >= config.irreversible.warn) {
|
|
356
|
+
level = higher(level, "warn");
|
|
357
|
+
reasons.push(`possibly irreversible ${percent(judgment.irreversible)}`);
|
|
358
|
+
}
|
|
359
|
+
if (judgment.offTask >= config.offTask.confirm && judgment.scope === "unrelated" && canChange) {
|
|
360
|
+
level = higher(level, "confirm");
|
|
361
|
+
reasons.push(`off-task ${percent(judgment.offTask)} (unrelated to the request)`);
|
|
362
|
+
}
|
|
363
|
+
else if (judgment.offTask >= config.offTask.confirm && judgment.scope === "unrelated") {
|
|
364
|
+
level = higher(level, "warn");
|
|
365
|
+
reasons.push(`off-task ${percent(judgment.offTask)} (unrelated, but read-only: not held)`);
|
|
366
|
+
}
|
|
367
|
+
else if (judgment.offTask >= config.offTask.warn && judgment.scope !== "unclear") {
|
|
368
|
+
level = higher(level, "warn");
|
|
369
|
+
reasons.push(`off-task ${percent(judgment.offTask)} (${judgment.scope.replace(/_/g, " ")})`);
|
|
370
|
+
}
|
|
371
|
+
if (options.security?.enabled && typeof answers.security_risk?.noul === "number") {
|
|
372
|
+
judgment.securityRisk = answers.security_risk.noul;
|
|
373
|
+
if (judgment.securityRisk >= options.security.threshold) {
|
|
374
|
+
level = higher(level, "warn");
|
|
375
|
+
reasons.push(`possible security weakness ${percent(judgment.securityRisk)} in written content`);
|
|
394
376
|
}
|
|
395
|
-
|
|
396
|
-
|
|
377
|
+
}
|
|
378
|
+
const verdict = { level, source: "typesafe", summary, patterns, reasons, judgment };
|
|
379
|
+
if (options.slop?.enabled && SLOP_SYMPTOMS.every(symptom => typeof answers[`slop_${symptom}`]?.noul === "number")) {
|
|
380
|
+
verdict.slop = { stub: answers.slop_stub.noul, comments: answers.slop_comments.noul, dead: answers.slop_dead.noul, hedging: answers.slop_hedging.noul };
|
|
381
|
+
const flagged = SLOP_SYMPTOMS.filter(symptom => verdict.slop[symptom] >= options.slop.threshold).sort((a, b) => verdict.slop[b] - verdict.slop[a]);
|
|
382
|
+
if (flagged.length) {
|
|
383
|
+
verdict.slopSymptoms = flagged;
|
|
384
|
+
verdict.slopReasons = flagged.map(symptom => `${SLOP_LABELS[symptom]} (${percent(verdict.slop[symptom])})`);
|
|
397
385
|
}
|
|
398
|
-
return { level, source: "error", summary, patterns, reasons, error: message, ...(known ? { errorCode: known.code } : {}) };
|
|
399
386
|
}
|
|
387
|
+
if (level === "confirm" && judgment.approved !== undefined && judgment.approved >= APPROVAL_THRESHOLD) {
|
|
388
|
+
verdict.level = "allow";
|
|
389
|
+
verdict.approvedByUser = true;
|
|
390
|
+
verdict.reasons = [`user approved in the latest message (${percent(judgment.approved)})`, ...reasons];
|
|
391
|
+
}
|
|
392
|
+
return verdict;
|
|
400
393
|
}
|
|
401
394
|
/** Offline stand-in for the approval question when TypeSafe is not available. */
|
|
402
395
|
export function textApproves(task) {
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
|
-
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason,
|
|
1
|
+
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason, questions, slopQuestions, approvalQuestion, securityQuestion } from "./guard.js";
|
|
2
2
|
export type { ActionInput, ActionSummary, EvaluateOptions, Judge, Judgment, SlopJudgment, SlopSymptom, Level, PatternHit, ScopeLabel, Severity, Verdict } from "./guard.js";
|
|
3
3
|
export { SLOP_SYMPTOMS, SLOP_LABELS } from "./guard.js";
|
|
4
|
+
export { ActionGuard } from "./action-guard.js";
|
|
5
|
+
export type { Conversation, InspectOptions, ToolCallRef } from "./action-guard.js";
|
|
4
6
|
export { evaluateProse, buildProseRequest, proseQuestions, proseNudge, describeAudience, ProseTrend, PROSE_SYMPTOMS, PROSE_LABELS } from "./prose.js";
|
|
5
7
|
export type { ProseSymptom, ProseVerdict, ProseOptions } from "./prose.js";
|
|
6
8
|
export { AttemptWindow, makeAttempt, resultFailed, resultText, evaluateStuck, buildStuckRequest, stuckQuestions, stuckNudge, formatStuck } from "./stuck.js";
|
|
7
9
|
export type { Attempt, StuckJudgment, StuckVerdict, StuckOptions } from "./stuck.js";
|
|
8
10
|
export { classifyToolResult, recordOutcome, emptyEvidence, needsDoneCheck, finalAssistantText, evaluateDone, buildDoneRequest, doneQuestions, doneNudge, formatDone } from "./done.js";
|
|
9
11
|
export type { ToolOutcome, RunEvidence, DoneJudgment, DoneVerdict, DoneOptions } from "./done.js";
|
|
10
|
-
export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, saveOutput, securityNotice } from "./output.js";
|
|
12
|
+
export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, saveOutput, securityNotice, outputKey, duplicateNote } from "./output.js";
|
|
11
13
|
export type { Retention, OutputVerdict, OutputOptions } from "./output.js";
|
|
14
|
+
export { formatExcerpt, formatQuestion } from "./excerpt.js";
|
|
15
|
+
export type { OutputFormat } from "./excerpt.js";
|
|
16
|
+
export { detectSearchTool, recallInstruction, classifyRecall } from "./recall.js";
|
|
17
|
+
export type { SearchTool } from "./recall.js";
|
|
12
18
|
export { redact } from "./redact.js";
|
|
13
19
|
export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
14
20
|
export { ContextLedger, formatLedger } from "./saver.js";
|
|
15
|
-
export type { ContextLedgerSnapshot } from "./saver.js";
|
|
21
|
+
export type { ContextLedgerSnapshot, RecallKind } from "./saver.js";
|
|
16
22
|
export type { ShapeResult } from "./shape.js";
|
|
17
|
-
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
18
|
-
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, SecurityConfig, ContextConfig, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
|
23
|
+
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, isRecallTool, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
24
|
+
export type { ActionGuardConfig, StuckGuardConfig, DoneGuardConfig, SlopGuardConfig, ProseConfig, SecurityConfig, ContextConfig, RecallTool, Threshold, WardenConfig, WardenMode, LoadOptions } from "./config.js";
|
|
19
25
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
20
26
|
export type { CommandView } from "./tools.js";
|
|
21
27
|
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
package/dist/index.js
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason,
|
|
1
|
+
export { evaluateAction, describeAction, matchPatterns, isReadOnlyCommand, buildRequest, formatVerdict, steerReason, questions, slopQuestions, approvalQuestion, securityQuestion } from "./guard.js";
|
|
2
2
|
export { SLOP_SYMPTOMS, SLOP_LABELS } from "./guard.js";
|
|
3
|
+
export { ActionGuard } from "./action-guard.js";
|
|
3
4
|
export { evaluateProse, buildProseRequest, proseQuestions, proseNudge, describeAudience, ProseTrend, PROSE_SYMPTOMS, PROSE_LABELS } from "./prose.js";
|
|
4
5
|
export { AttemptWindow, makeAttempt, resultFailed, resultText, evaluateStuck, buildStuckRequest, stuckQuestions, stuckNudge, formatStuck } from "./stuck.js";
|
|
5
6
|
export { classifyToolResult, recordOutcome, emptyEvidence, needsDoneCheck, finalAssistantText, evaluateDone, buildDoneRequest, doneQuestions, doneNudge, formatDone } from "./done.js";
|
|
6
|
-
export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, saveOutput, securityNotice } from "./output.js";
|
|
7
|
+
export { evaluateOutput, buildOutputRequest, outputQuestions, compressOutput, saveOutput, securityNotice, outputKey, duplicateNote } from "./output.js";
|
|
8
|
+
export { formatExcerpt, formatQuestion } from "./excerpt.js";
|
|
9
|
+
export { detectSearchTool, recallInstruction, classifyRecall } from "./recall.js";
|
|
7
10
|
export { redact } from "./redact.js";
|
|
8
11
|
export { completeConfig, shapeWarning, EXPECTED_SCHEMA } from "./shape.js";
|
|
9
12
|
export { ContextLedger, formatLedger } from "./saver.js";
|
|
10
|
-
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
13
|
+
export { CONFIG_SCHEMA, defaultConfig, loadConfig, applyUserOverrides, applyProjectOverrides, isMode, isRecallTool, userConfigPath, projectConfigPath, readUserConfig, writeUserConfig, setUserSetting, PACKAGE_NAME } from "./config.js";
|
|
11
14
|
export { commandOf, outputReportsFailure, COMMAND_TOOLS } from "./tools.js";
|
|
12
15
|
export { renderTemplate, actionTokens, stuckTokens, doneTokens, proseTokens, defaultWidgetConfig, DEFAULT_TEMPLATES, TOKEN_NAMES } from "./widget.js";
|
|
13
16
|
export { Trace, actionDetails, stuckDetails, doneDetails, proseDetails } from "./trace.js";
|
package/dist/jev.d.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { Evaluation, IntegrationErrorCode, Questions, SystemOneRequest, TypeSafe } from "pi-typesafe";
|
|
2
|
+
/** Anything with pi-typesafe's `evaluate`: the real client in a session, a stub in tests. */
|
|
3
|
+
export type Judge = Pick<TypeSafe, "evaluate">;
|
|
4
|
+
export interface AskOptions {
|
|
5
|
+
timeoutMs: number;
|
|
6
|
+
signal?: AbortSignal | undefined;
|
|
7
|
+
}
|
|
8
|
+
export type JevAnswer<Q extends Questions> = {
|
|
9
|
+
ok: true;
|
|
10
|
+
answers: Evaluation<Q>["answers"];
|
|
11
|
+
model: string;
|
|
12
|
+
elapsedMs: number;
|
|
13
|
+
} | {
|
|
14
|
+
ok: false;
|
|
15
|
+
error: string;
|
|
16
|
+
errorCode?: IntegrationErrorCode;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* One Jev request with the per-guard timeout merged into the caller's abort signal. Never throws: a failure comes back as
|
|
20
|
+
* `{ ok: false }` with pi-typesafe's own message (which carries no upstream body) and its code, so the caller can stop
|
|
21
|
+
* asking after a `budget` error. Unknown errors get a fixed message so nothing from the transport reaches the user.
|
|
22
|
+
*/
|
|
23
|
+
export declare function askJev<Q extends Questions>(judge: Judge, request: SystemOneRequest<Q>, options: AskOptions): Promise<JevAnswer<Q>>;
|
package/dist/jev.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { TypeSafeIntegrationError } from "pi-typesafe";
|
|
2
|
+
const FALLBACK_MESSAGE = "TypeSafe request failed.";
|
|
3
|
+
/**
|
|
4
|
+
* One Jev request with the per-guard timeout merged into the caller's abort signal. Never throws: a failure comes back as
|
|
5
|
+
* `{ ok: false }` with pi-typesafe's own message (which carries no upstream body) and its code, so the caller can stop
|
|
6
|
+
* asking after a `budget` error. Unknown errors get a fixed message so nothing from the transport reaches the user.
|
|
7
|
+
*/
|
|
8
|
+
export async function askJev(judge, request, options) {
|
|
9
|
+
const timeout = AbortSignal.timeout(options.timeoutMs);
|
|
10
|
+
try {
|
|
11
|
+
const result = await judge.evaluate(request, { signal: options.signal ? AbortSignal.any([options.signal, timeout]) : timeout });
|
|
12
|
+
return { ok: true, answers: result.answers, model: result.model, elapsedMs: result.elapsedMs };
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (error instanceof TypeSafeIntegrationError)
|
|
16
|
+
return { ok: false, error: error.message, errorCode: error.code };
|
|
17
|
+
return { ok: false, error: FALLBACK_MESSAGE };
|
|
18
|
+
}
|
|
19
|
+
}
|