wispy-cli 2.7.12 → 2.7.13
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/core/engine.mjs +36 -1
- package/core/harness.mjs +87 -0
- package/core/providers.mjs +20 -0
- package/core/tools.mjs +204 -1
- package/lib/commands/trust.mjs +57 -0
- package/package.json +1 -1
package/core/engine.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import os from "node:os";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { readFile, writeFile, mkdir, appendFile } from "node:fs/promises";
|
|
15
|
+
import { readFile, writeFile, mkdir, appendFile, stat as fsStat } from "node:fs/promises";
|
|
16
16
|
|
|
17
17
|
import { WISPY_DIR, CONVERSATIONS_DIR, MEMORY_DIR, MCP_CONFIG_PATH, detectProvider, PROVIDERS } from "./config.mjs";
|
|
18
18
|
import { NullEmitter } from "../lib/jsonl-emitter.mjs";
|
|
@@ -43,6 +43,7 @@ import { UserModel } from "./user-model.mjs";
|
|
|
43
43
|
import { routeTask, classifyTask, filterAvailableModels } from "./task-router.mjs";
|
|
44
44
|
import { decomposeTask, executeDecomposedPlan } from "./task-decomposer.mjs";
|
|
45
45
|
import { BrowserBridge } from "./browser.mjs";
|
|
46
|
+
import { LoopDetector } from "./loop-detector.mjs";
|
|
46
47
|
|
|
47
48
|
const MAX_TOOL_ROUNDS = 10;
|
|
48
49
|
const MAX_CONTEXT_CHARS = 40_000;
|
|
@@ -333,7 +334,38 @@ export class WispyEngine {
|
|
|
333
334
|
// Optimize context
|
|
334
335
|
messages = this._optimizeContext(messages);
|
|
335
336
|
|
|
337
|
+
// Create a fresh loop detector per agent turn
|
|
338
|
+
const loopDetector = new LoopDetector();
|
|
339
|
+
let loopWarned = false;
|
|
340
|
+
|
|
336
341
|
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
342
|
+
// ── Loop detection check before LLM call ─────────────────────────────
|
|
343
|
+
if (loopDetector.size >= 2) {
|
|
344
|
+
const loopCheck = loopDetector.check();
|
|
345
|
+
if (loopCheck.looping) {
|
|
346
|
+
if (opts.onLoopDetected) opts.onLoopDetected(loopCheck);
|
|
347
|
+
|
|
348
|
+
if (!loopWarned) {
|
|
349
|
+
// First warning: inject a system message and continue
|
|
350
|
+
loopWarned = true;
|
|
351
|
+
const warningMsg = loopCheck.suggestion ?? `Loop detected: agent called ${loopCheck.tool} multiple times without progress. Try a different approach.`;
|
|
352
|
+
messages.push({
|
|
353
|
+
role: "user",
|
|
354
|
+
content: `[SYSTEM WARNING] ${warningMsg}`,
|
|
355
|
+
});
|
|
356
|
+
if (process.env.WISPY_DEBUG) {
|
|
357
|
+
console.error(`[wispy] Loop detected: ${loopCheck.reason} — warning injected`);
|
|
358
|
+
}
|
|
359
|
+
} else {
|
|
360
|
+
// Second time loop detected after warning: force-break the agent turn
|
|
361
|
+
if (process.env.WISPY_DEBUG) {
|
|
362
|
+
console.error(`[wispy] Loop force-break: ${loopCheck.reason}`);
|
|
363
|
+
}
|
|
364
|
+
return `⚠️ Agent loop detected and stopped: ${loopCheck.suggestion ?? loopCheck.reason}. Please try rephrasing your request.`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
337
369
|
const result = await this.providers.chat(messages, this.tools.getDefinitions(), {
|
|
338
370
|
onChunk: opts.onChunk,
|
|
339
371
|
model: opts.model,
|
|
@@ -371,6 +403,9 @@ export class WispyEngine {
|
|
|
371
403
|
}
|
|
372
404
|
}
|
|
373
405
|
|
|
406
|
+
// Record into loop detector
|
|
407
|
+
loopDetector.record(call.name, call.args, toolResult);
|
|
408
|
+
|
|
374
409
|
const _toolDuration = Date.now() - _toolStartMs;
|
|
375
410
|
if (opts.onToolResult) opts.onToolResult(call.name, toolResult);
|
|
376
411
|
loopEmitter.toolResult(call.name, toolResult, _toolDuration);
|
package/core/harness.mjs
CHANGED
|
@@ -536,6 +536,56 @@ export class Harness extends EventEmitter {
|
|
|
536
536
|
receipt.approved = permResult.approved;
|
|
537
537
|
}
|
|
538
538
|
|
|
539
|
+
// ── 1b. Approval gate (security mode) ────────────────────────────────────
|
|
540
|
+
const securityMode = this.config.securityLevel ?? context.securityLevel ?? "balanced";
|
|
541
|
+
if (_needsApproval(toolName, args, securityMode) && permResult.allowed) {
|
|
542
|
+
// Check allowlist first
|
|
543
|
+
const allowlisted = await this.allowlist.matches(toolName, args);
|
|
544
|
+
if (!allowlisted) {
|
|
545
|
+
// Non-interactive (no TTY): auto-deny in careful, auto-allow in balanced/yolo
|
|
546
|
+
const isTTY = process.stdin.isTTY && process.stdout.isTTY;
|
|
547
|
+
if (!isTTY) {
|
|
548
|
+
if (securityMode === "careful") {
|
|
549
|
+
receipt.approved = false;
|
|
550
|
+
receipt.success = false;
|
|
551
|
+
receipt.error = `Auto-denied (careful mode, non-interactive): ${toolName}`;
|
|
552
|
+
receipt.duration = Date.now() - callStart;
|
|
553
|
+
this.emit("tool:denied", { toolName, args, receipt, context, reason: "non-interactive-careful" });
|
|
554
|
+
return new HarnessResult({ result: { success: false, error: receipt.error, denied: true }, receipt, denied: true });
|
|
555
|
+
}
|
|
556
|
+
// balanced/yolo non-interactive → auto-allow
|
|
557
|
+
receipt.approved = true;
|
|
558
|
+
} else {
|
|
559
|
+
// Emit approval_required event and wait for response
|
|
560
|
+
const approved = await this._requestApproval(toolName, args, receipt, context, securityMode);
|
|
561
|
+
receipt.approved = approved;
|
|
562
|
+
if (!approved) {
|
|
563
|
+
receipt.success = false;
|
|
564
|
+
receipt.error = `Approval denied for: ${toolName}`;
|
|
565
|
+
receipt.duration = Date.now() - callStart;
|
|
566
|
+
this.audit.log({
|
|
567
|
+
type: EVENT_TYPES.APPROVAL_DENIED,
|
|
568
|
+
sessionId: context.sessionId,
|
|
569
|
+
tool: toolName,
|
|
570
|
+
args,
|
|
571
|
+
}).catch(() => {});
|
|
572
|
+
this.emit("tool:denied", { toolName, args, receipt, context, reason: "approval-denied" });
|
|
573
|
+
return new HarnessResult({ result: { success: false, error: receipt.error, denied: true }, receipt, denied: true });
|
|
574
|
+
}
|
|
575
|
+
// User approved — add to allowlist if they want to remember
|
|
576
|
+
// (The auto-approve-and-remember logic is handled by the approval handler)
|
|
577
|
+
this.audit.log({
|
|
578
|
+
type: EVENT_TYPES.APPROVAL_GRANTED ?? "approval_granted",
|
|
579
|
+
sessionId: context.sessionId,
|
|
580
|
+
tool: toolName,
|
|
581
|
+
args,
|
|
582
|
+
}).catch(() => {});
|
|
583
|
+
}
|
|
584
|
+
} else {
|
|
585
|
+
receipt.approved = true; // allowlisted
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
539
589
|
// ── 2. Dry-run mode ──────────────────────────────────────────────────────
|
|
540
590
|
if (receipt.dryRun) {
|
|
541
591
|
const preview = simulateDryRun(toolName, args);
|
|
@@ -650,6 +700,43 @@ export class Harness extends EventEmitter {
|
|
|
650
700
|
return new HarnessResult({ result, receipt });
|
|
651
701
|
}
|
|
652
702
|
|
|
703
|
+
/**
|
|
704
|
+
* Emit 'approval_required' and wait for user response.
|
|
705
|
+
* Resolves to true (approved) or false (denied).
|
|
706
|
+
*/
|
|
707
|
+
async _requestApproval(toolName, args, receipt, context, mode) {
|
|
708
|
+
return new Promise((resolve) => {
|
|
709
|
+
// Timeout after 60s → auto-deny in careful, auto-allow otherwise
|
|
710
|
+
const timer = setTimeout(() => {
|
|
711
|
+
if (mode === "careful") {
|
|
712
|
+
resolve(false);
|
|
713
|
+
} else {
|
|
714
|
+
resolve(true);
|
|
715
|
+
}
|
|
716
|
+
}, 60_000);
|
|
717
|
+
|
|
718
|
+
const respond = (approved, remember = false) => {
|
|
719
|
+
clearTimeout(timer);
|
|
720
|
+
if (remember && approved) {
|
|
721
|
+
const pattern = _getArgString(toolName, args);
|
|
722
|
+
if (pattern) {
|
|
723
|
+
this.allowlist.add(toolName, pattern).catch(() => {});
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
resolve(approved);
|
|
727
|
+
};
|
|
728
|
+
|
|
729
|
+
this.emit("approval_required", {
|
|
730
|
+
tool: toolName,
|
|
731
|
+
args,
|
|
732
|
+
receipt,
|
|
733
|
+
context,
|
|
734
|
+
mode,
|
|
735
|
+
respond,
|
|
736
|
+
});
|
|
737
|
+
});
|
|
738
|
+
}
|
|
739
|
+
|
|
653
740
|
/**
|
|
654
741
|
* Set sandbox mode for a tool.
|
|
655
742
|
* @param {string} toolName
|
package/core/providers.mjs
CHANGED
|
@@ -290,6 +290,17 @@ export class ProviderRegistry {
|
|
|
290
290
|
type: "tool_use", id: tc.id ?? tc.name, name: tc.name, input: tc.args,
|
|
291
291
|
})),
|
|
292
292
|
});
|
|
293
|
+
} else if (m.images && m.images.length > 0) {
|
|
294
|
+
// Multimodal message with images (Anthropic format)
|
|
295
|
+
const contentParts = m.images.map(img => ({
|
|
296
|
+
type: "image",
|
|
297
|
+
source: { type: "base64", media_type: img.mimeType, data: img.data },
|
|
298
|
+
}));
|
|
299
|
+
if (m.content) contentParts.push({ type: "text", text: m.content });
|
|
300
|
+
anthropicMessages.push({
|
|
301
|
+
role: m.role === "assistant" ? "assistant" : "user",
|
|
302
|
+
content: contentParts,
|
|
303
|
+
});
|
|
293
304
|
} else {
|
|
294
305
|
anthropicMessages.push({
|
|
295
306
|
role: m.role === "assistant" ? "assistant" : "user",
|
|
@@ -400,6 +411,15 @@ export class ProviderRegistry {
|
|
|
400
411
|
})),
|
|
401
412
|
};
|
|
402
413
|
}
|
|
414
|
+
// Multimodal message with images (OpenAI format)
|
|
415
|
+
if (m.images && m.images.length > 0) {
|
|
416
|
+
const contentParts = m.images.map(img => ({
|
|
417
|
+
type: "image_url",
|
|
418
|
+
image_url: { url: `data:${img.mimeType};base64,${img.data}` },
|
|
419
|
+
}));
|
|
420
|
+
if (m.content) contentParts.push({ type: "text", text: m.content });
|
|
421
|
+
return { role: m.role === "assistant" ? "assistant" : "user", content: contentParts };
|
|
422
|
+
}
|
|
403
423
|
return { role: m.role, content: m.content };
|
|
404
424
|
});
|
|
405
425
|
|
package/core/tools.mjs
CHANGED
|
@@ -326,6 +326,21 @@ export class ToolRegistry {
|
|
|
326
326
|
},
|
|
327
327
|
},
|
|
328
328
|
},
|
|
329
|
+
// ── apply_patch (Part 3) ─────────────────────────────────────────────────
|
|
330
|
+
{
|
|
331
|
+
name: "apply_patch",
|
|
332
|
+
description: "Apply multi-file patches. Supports creating, editing, and deleting files in one atomic operation. All operations succeed or all rollback.",
|
|
333
|
+
parameters: {
|
|
334
|
+
type: "object",
|
|
335
|
+
properties: {
|
|
336
|
+
patch: {
|
|
337
|
+
type: "string",
|
|
338
|
+
description: "Patch in structured format:\n*** Begin Patch\n*** Add File: path\n+line1\n+line2\n*** Edit File: path\n@@@ context line @@@\n-old line\n+new line\n*** Delete File: path\n*** End Patch",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
required: ["patch"],
|
|
342
|
+
},
|
|
343
|
+
},
|
|
329
344
|
];
|
|
330
345
|
|
|
331
346
|
for (const def of builtins) {
|
|
@@ -631,6 +646,9 @@ export class ToolRegistry {
|
|
|
631
646
|
return { success: false, error: "action must be 'copy' or 'paste'" };
|
|
632
647
|
}
|
|
633
648
|
|
|
649
|
+
case "apply_patch":
|
|
650
|
+
return this._executeApplyPatch(args.patch);
|
|
651
|
+
|
|
634
652
|
// Agent tools — these are handled by the engine level
|
|
635
653
|
case "spawn_agent":
|
|
636
654
|
case "list_agents":
|
|
@@ -656,10 +674,195 @@ export class ToolRegistry {
|
|
|
656
674
|
return { success: false, error: `Tool "${name}" requires engine context. Call via WispyEngine.processMessage().` };
|
|
657
675
|
|
|
658
676
|
default:
|
|
659
|
-
return { success: false, error: `Unknown tool: ${name}. Available built-in tools: read_file, write_file, file_edit, file_search, run_command, list_directory, git, web_search, web_fetch, keychain, clipboard` };
|
|
677
|
+
return { success: false, error: `Unknown tool: ${name}. Available built-in tools: read_file, write_file, file_edit, file_search, run_command, list_directory, git, web_search, web_fetch, keychain, clipboard, apply_patch` };
|
|
660
678
|
}
|
|
661
679
|
} catch (err) {
|
|
662
680
|
return { success: false, error: err.message };
|
|
663
681
|
}
|
|
664
682
|
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Execute an apply_patch operation atomically.
|
|
686
|
+
* Supports: Add File, Edit File, Delete File
|
|
687
|
+
*
|
|
688
|
+
* Format:
|
|
689
|
+
* *** Begin Patch
|
|
690
|
+
* *** Add File: path/to/file.txt
|
|
691
|
+
* +line content
|
|
692
|
+
* *** Edit File: path/to/file.txt
|
|
693
|
+
* @@@ context @@@
|
|
694
|
+
* -old line
|
|
695
|
+
* +new line
|
|
696
|
+
* *** Delete File: path/to/file.txt
|
|
697
|
+
* *** End Patch
|
|
698
|
+
*/
|
|
699
|
+
async _executeApplyPatch(patchText) {
|
|
700
|
+
if (!patchText || typeof patchText !== "string") {
|
|
701
|
+
return { success: false, error: "patch parameter is required and must be a string" };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const { readFile: rf, writeFile: wf, unlink, mkdir: mkdirFs } = await import("node:fs/promises");
|
|
705
|
+
|
|
706
|
+
const lines = patchText.split("\n");
|
|
707
|
+
const operations = [];
|
|
708
|
+
|
|
709
|
+
// ── Parse operations ──────────────────────────────────────────────────────
|
|
710
|
+
let i = 0;
|
|
711
|
+
// Skip to Begin Patch
|
|
712
|
+
while (i < lines.length && !lines[i].startsWith("*** Begin Patch")) i++;
|
|
713
|
+
if (i >= lines.length) {
|
|
714
|
+
return { success: false, error: 'Patch must start with "*** Begin Patch"' };
|
|
715
|
+
}
|
|
716
|
+
i++;
|
|
717
|
+
|
|
718
|
+
while (i < lines.length) {
|
|
719
|
+
const line = lines[i];
|
|
720
|
+
|
|
721
|
+
if (line.startsWith("*** End Patch")) break;
|
|
722
|
+
|
|
723
|
+
if (line.startsWith("*** Add File:")) {
|
|
724
|
+
const filePath = line.slice("*** Add File:".length).trim();
|
|
725
|
+
const addLines = [];
|
|
726
|
+
i++;
|
|
727
|
+
while (i < lines.length && !lines[i].startsWith("***")) {
|
|
728
|
+
const l = lines[i];
|
|
729
|
+
if (l.startsWith("+")) addLines.push(l.slice(1));
|
|
730
|
+
else if (l.startsWith(" ")) addLines.push(l.slice(1));
|
|
731
|
+
// Lines starting with - are ignored for Add File
|
|
732
|
+
i++;
|
|
733
|
+
}
|
|
734
|
+
operations.push({ type: "add", path: filePath, content: addLines.join("\n") });
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
if (line.startsWith("*** Edit File:")) {
|
|
739
|
+
const filePath = line.slice("*** Edit File:".length).trim();
|
|
740
|
+
const hunks = [];
|
|
741
|
+
i++;
|
|
742
|
+
// Parse edit hunks
|
|
743
|
+
while (i < lines.length && !lines[i].startsWith("*** ")) {
|
|
744
|
+
const hunkLine = lines[i];
|
|
745
|
+
if (hunkLine.startsWith("@@@")) {
|
|
746
|
+
// Start of a hunk: @@@ context @@@
|
|
747
|
+
const contextText = hunkLine.replace(/^@@@\s*/, "").replace(/\s*@@@$/, "").trim();
|
|
748
|
+
const removals = [];
|
|
749
|
+
const additions = [];
|
|
750
|
+
i++;
|
|
751
|
+
while (i < lines.length && !lines[i].startsWith("@@@") && !lines[i].startsWith("***")) {
|
|
752
|
+
const hl = lines[i];
|
|
753
|
+
if (hl.startsWith("-")) removals.push(hl.slice(1));
|
|
754
|
+
else if (hl.startsWith("+")) additions.push(hl.slice(1));
|
|
755
|
+
i++;
|
|
756
|
+
}
|
|
757
|
+
hunks.push({ context: contextText, removals, additions });
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
i++;
|
|
761
|
+
}
|
|
762
|
+
operations.push({ type: "edit", path: filePath, hunks });
|
|
763
|
+
continue;
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
if (line.startsWith("*** Delete File:")) {
|
|
767
|
+
const filePath = line.slice("*** Delete File:".length).trim();
|
|
768
|
+
operations.push({ type: "delete", path: filePath });
|
|
769
|
+
i++;
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
i++;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
if (operations.length === 0) {
|
|
777
|
+
return { success: false, error: "No valid operations found in patch" };
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
// ── Resolve paths ─────────────────────────────────────────────────────────
|
|
781
|
+
const resolvePatchPath = (p) => {
|
|
782
|
+
let resolved = p.replace(/^~/, os.homedir());
|
|
783
|
+
if (!path.isAbsolute(resolved)) resolved = path.resolve(process.cwd(), resolved);
|
|
784
|
+
return resolved;
|
|
785
|
+
};
|
|
786
|
+
|
|
787
|
+
// ── Pre-flight: load original file contents for rollback ──────────────────
|
|
788
|
+
const originalContents = new Map(); // path → string | null (null = didn't exist)
|
|
789
|
+
for (const op of operations) {
|
|
790
|
+
const resolved = resolvePatchPath(op.path);
|
|
791
|
+
try {
|
|
792
|
+
originalContents.set(resolved, await rf(resolved, "utf8"));
|
|
793
|
+
} catch {
|
|
794
|
+
originalContents.set(resolved, null);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// ── Apply operations ──────────────────────────────────────────────────────
|
|
799
|
+
const applied = [];
|
|
800
|
+
const results = [];
|
|
801
|
+
|
|
802
|
+
try {
|
|
803
|
+
for (const op of operations) {
|
|
804
|
+
const resolved = resolvePatchPath(op.path);
|
|
805
|
+
|
|
806
|
+
if (op.type === "add") {
|
|
807
|
+
await mkdirFs(path.dirname(resolved), { recursive: true });
|
|
808
|
+
await wf(resolved, op.content, "utf8");
|
|
809
|
+
applied.push({ op, resolved });
|
|
810
|
+
results.push(`✅ Added: ${op.path}`);
|
|
811
|
+
|
|
812
|
+
} else if (op.type === "edit") {
|
|
813
|
+
const original = originalContents.get(resolved);
|
|
814
|
+
if (original === null) {
|
|
815
|
+
throw new Error(`Cannot edit non-existent file: ${op.path}`);
|
|
816
|
+
}
|
|
817
|
+
let current = original;
|
|
818
|
+
for (const hunk of op.hunks) {
|
|
819
|
+
const oldText = hunk.removals.join("\n");
|
|
820
|
+
const newText = hunk.additions.join("\n");
|
|
821
|
+
if (oldText && !current.includes(oldText)) {
|
|
822
|
+
throw new Error(`Edit hunk not found in ${op.path}: "${oldText.slice(0, 60)}"`);
|
|
823
|
+
}
|
|
824
|
+
current = oldText ? current.replace(oldText, newText) : current + "\n" + newText;
|
|
825
|
+
}
|
|
826
|
+
await wf(resolved, current, "utf8");
|
|
827
|
+
applied.push({ op, resolved });
|
|
828
|
+
results.push(`✅ Edited: ${op.path}`);
|
|
829
|
+
|
|
830
|
+
} else if (op.type === "delete") {
|
|
831
|
+
await unlink(resolved);
|
|
832
|
+
applied.push({ op, resolved });
|
|
833
|
+
results.push(`✅ Deleted: ${op.path}`);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} catch (err) {
|
|
837
|
+
// ── Rollback all applied operations ──────────────────────────────────────
|
|
838
|
+
for (const { op, resolved } of applied.reverse()) {
|
|
839
|
+
try {
|
|
840
|
+
const original = originalContents.get(resolved);
|
|
841
|
+
if (op.type === "delete" && original !== null) {
|
|
842
|
+
// Restore deleted file
|
|
843
|
+
await wf(resolved, original, "utf8");
|
|
844
|
+
} else if ((op.type === "add") && original === null) {
|
|
845
|
+
// Remove newly created file
|
|
846
|
+
await unlink(resolved).catch(() => {});
|
|
847
|
+
} else if (op.type === "edit" && original !== null) {
|
|
848
|
+
// Restore original content
|
|
849
|
+
await wf(resolved, original, "utf8");
|
|
850
|
+
}
|
|
851
|
+
} catch { /* best-effort rollback */ }
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
return {
|
|
855
|
+
success: false,
|
|
856
|
+
error: `Patch failed (rolled back): ${err.message}`,
|
|
857
|
+
applied: applied.length,
|
|
858
|
+
total: operations.length,
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
return {
|
|
863
|
+
success: true,
|
|
864
|
+
message: `Applied ${operations.length} operation(s)`,
|
|
865
|
+
results,
|
|
866
|
+
};
|
|
867
|
+
}
|
|
665
868
|
}
|
package/lib/commands/trust.mjs
CHANGED
|
@@ -319,6 +319,58 @@ export async function cmdTrustReceipt(id) {
|
|
|
319
319
|
console.log("");
|
|
320
320
|
}
|
|
321
321
|
|
|
322
|
+
export async function cmdTrustApprovals(subArgs = []) {
|
|
323
|
+
const { globalAllowlist } = await import("../../core/harness.mjs");
|
|
324
|
+
const action = subArgs[0];
|
|
325
|
+
|
|
326
|
+
if (!action || action === "list") {
|
|
327
|
+
const list = await globalAllowlist.getAll();
|
|
328
|
+
console.log(`\n${bold("🔐 Approval Allowlist")} ${dim("(auto-approved patterns)")}\n`);
|
|
329
|
+
let empty = true;
|
|
330
|
+
for (const [tool, patterns] of Object.entries(list)) {
|
|
331
|
+
if (patterns.length === 0) continue;
|
|
332
|
+
empty = false;
|
|
333
|
+
console.log(` ${cyan(tool)}:`);
|
|
334
|
+
for (const p of patterns) {
|
|
335
|
+
console.log(` ${dim("•")} ${p}`);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
if (empty) {
|
|
339
|
+
console.log(dim(" No patterns configured."));
|
|
340
|
+
}
|
|
341
|
+
console.log(dim("\n Manage: wispy trust approvals add <tool> <pattern>"));
|
|
342
|
+
console.log(dim(" wispy trust approvals clear"));
|
|
343
|
+
console.log(dim(" wispy trust approvals reset\n"));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (action === "add") {
|
|
348
|
+
const tool = subArgs[1];
|
|
349
|
+
const pattern = subArgs[2];
|
|
350
|
+
if (!tool || !pattern) {
|
|
351
|
+
console.log(yellow("Usage: wispy trust approvals add <tool> <pattern>"));
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
await globalAllowlist.add(tool, pattern);
|
|
355
|
+
console.log(`${green("✅")} Added pattern for ${cyan(tool)}: ${pattern}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (action === "clear") {
|
|
360
|
+
await globalAllowlist.clear();
|
|
361
|
+
console.log(green("✅ Allowlist cleared."));
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (action === "reset") {
|
|
366
|
+
await globalAllowlist.reset();
|
|
367
|
+
console.log(green("✅ Allowlist reset to defaults."));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
console.log(yellow(`Unknown approvals action: ${action}. Use: list, add, clear, reset`));
|
|
372
|
+
}
|
|
373
|
+
|
|
322
374
|
export async function handleTrustCommand(args) {
|
|
323
375
|
const sub = args[1];
|
|
324
376
|
|
|
@@ -405,6 +457,7 @@ export async function handleTrustCommand(args) {
|
|
|
405
457
|
if (sub === "log") return cmdTrustLog(args.slice(2));
|
|
406
458
|
if (sub === "replay") return cmdTrustReplay(args[2]);
|
|
407
459
|
if (sub === "receipt") return cmdTrustReceipt(args[2]);
|
|
460
|
+
if (sub === "approvals") return cmdTrustApprovals(args.slice(2));
|
|
408
461
|
|
|
409
462
|
console.log(`
|
|
410
463
|
${bold("🔐 Trust Commands")}
|
|
@@ -414,5 +467,9 @@ ${bold("🔐 Trust Commands")}
|
|
|
414
467
|
wispy trust log ${dim("show audit log")}
|
|
415
468
|
wispy trust replay <session-id> ${dim("replay session step by step")}
|
|
416
469
|
wispy trust receipt <session-id> ${dim("show execution receipt")}
|
|
470
|
+
wispy trust approvals ${dim("list approval allowlist")}
|
|
471
|
+
wispy trust approvals add <tool> <pat> ${dim("add an allowlist pattern")}
|
|
472
|
+
wispy trust approvals clear ${dim("clear all allowlist patterns")}
|
|
473
|
+
wispy trust approvals reset ${dim("reset allowlist to defaults")}
|
|
417
474
|
`);
|
|
418
475
|
}
|
package/package.json
CHANGED