veefore 1.0.0 → 1.1.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/dist/veefore.mjs +1713 -1330
- package/package.json +1 -1
package/dist/veefore.mjs
CHANGED
|
@@ -236,29 +236,29 @@ var ApiClient = class {
|
|
|
236
236
|
setToken(token) {
|
|
237
237
|
this.token = token;
|
|
238
238
|
}
|
|
239
|
-
async get(
|
|
239
|
+
async get(path12, query = {}) {
|
|
240
240
|
const search = new URLSearchParams();
|
|
241
241
|
for (const [key, value] of Object.entries(query)) {
|
|
242
242
|
if (value !== void 0) search.set(key, String(value));
|
|
243
243
|
}
|
|
244
244
|
const suffix = search.toString() === "" ? "" : `?${search.toString()}`;
|
|
245
|
-
return this.send("GET", `${
|
|
245
|
+
return this.send("GET", `${path12}${suffix}`);
|
|
246
246
|
}
|
|
247
|
-
async post(
|
|
248
|
-
return this.send("POST",
|
|
247
|
+
async post(path12, body) {
|
|
248
|
+
return this.send("POST", path12, body);
|
|
249
249
|
}
|
|
250
|
-
async patch(
|
|
251
|
-
return this.send("PATCH",
|
|
250
|
+
async patch(path12, body) {
|
|
251
|
+
return this.send("PATCH", path12, body);
|
|
252
252
|
}
|
|
253
|
-
async delete(
|
|
254
|
-
return this.send("DELETE",
|
|
253
|
+
async delete(path12) {
|
|
254
|
+
return this.send("DELETE", path12);
|
|
255
255
|
}
|
|
256
256
|
/** Raw request for endpoints that return a non-JSON body (exports). */
|
|
257
|
-
async raw(method,
|
|
258
|
-
return this.fetchWithTimeout(method,
|
|
257
|
+
async raw(method, path12, body) {
|
|
258
|
+
return this.fetchWithTimeout(method, path12, body);
|
|
259
259
|
}
|
|
260
|
-
async send(method,
|
|
261
|
-
const response = await this.fetchWithTimeout(method,
|
|
260
|
+
async send(method, path12, body) {
|
|
261
|
+
const response = await this.fetchWithTimeout(method, path12, body);
|
|
262
262
|
const text = await response.text();
|
|
263
263
|
const parsed = parseJson(text);
|
|
264
264
|
if (response.ok) {
|
|
@@ -266,14 +266,14 @@ var ApiClient = class {
|
|
|
266
266
|
if (parsed && "data" in parsed) return parsed.data;
|
|
267
267
|
throw new CliApiError(
|
|
268
268
|
"UNEXPECTED_RESPONSE",
|
|
269
|
-
`The control plane returned an unexpected body for ${method} ${
|
|
269
|
+
`The control plane returned an unexpected body for ${method} ${path12}.`,
|
|
270
270
|
response.status,
|
|
271
271
|
response.headers.get("x-request-id")
|
|
272
272
|
);
|
|
273
273
|
}
|
|
274
274
|
const error = new CliApiError(
|
|
275
275
|
parsed?.error?.code ?? `HTTP_${response.status}`,
|
|
276
|
-
parsed?.error?.message ?? `${method} ${
|
|
276
|
+
parsed?.error?.message ?? `${method} ${path12} failed with ${response.status} ${response.statusText}.`,
|
|
277
277
|
response.status,
|
|
278
278
|
parsed?.error?.request_id ?? response.headers.get("x-request-id"),
|
|
279
279
|
parsed?.error?.details ?? {}
|
|
@@ -291,7 +291,7 @@ var ApiClient = class {
|
|
|
291
291
|
}
|
|
292
292
|
throw error;
|
|
293
293
|
}
|
|
294
|
-
async fetchWithTimeout(method,
|
|
294
|
+
async fetchWithTimeout(method, path12, body) {
|
|
295
295
|
const controller = new AbortController();
|
|
296
296
|
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
297
297
|
const headers = { accept: "application/json" };
|
|
@@ -300,7 +300,7 @@ var ApiClient = class {
|
|
|
300
300
|
if (this.workspaceId) headers["x-veefore-workspace"] = this.workspaceId;
|
|
301
301
|
if (this.environmentId) headers["x-veefore-environment"] = this.environmentId;
|
|
302
302
|
try {
|
|
303
|
-
return await fetch(`${this.controlPlaneUrl}${
|
|
303
|
+
return await fetch(`${this.controlPlaneUrl}${path12}`, {
|
|
304
304
|
method,
|
|
305
305
|
headers,
|
|
306
306
|
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
@@ -566,6 +566,275 @@ function sleep(ms) {
|
|
|
566
566
|
|
|
567
567
|
// src/terminal.ts
|
|
568
568
|
import { createInterface } from "node:readline/promises";
|
|
569
|
+
|
|
570
|
+
// src/tui.ts
|
|
571
|
+
import { emitKeypressEvents } from "node:readline";
|
|
572
|
+
|
|
573
|
+
// src/logo.ts
|
|
574
|
+
var LOGO = [
|
|
575
|
+
"\u2580\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2584 \u2584\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2580",
|
|
576
|
+
" \u2588\u2588 \u2580\u2588 \u2588\u2580 \u2588\u2588",
|
|
577
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
578
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
579
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
580
|
+
" \u2580\u2588\u2584 \u2588\u2584 \u2584\u2588 \u2584\u2588 \u2584\u2588\u2580",
|
|
581
|
+
" \u2580\u2588\u2584 \u2580\u2588\u2588\u2588\u2580 \u2584\u2588\u2580 \u2584\u2588\u2580",
|
|
582
|
+
" \u2580\u2588\u2584 \u2580\u2588\u2580 \u2584\u2588\u2580 \u2584\u2588\u2580",
|
|
583
|
+
" \u2580\u2588 \u2580 \u2588\u2580 \u2588\u2580",
|
|
584
|
+
" \u2588\u2588 \u2588\u2588 \u2588\u2588",
|
|
585
|
+
" \u2588\u2588\u2588\u2588\u2588\u2588\u2588 \u2588\u2588"
|
|
586
|
+
];
|
|
587
|
+
|
|
588
|
+
// src/tui.ts
|
|
589
|
+
function isInteractive() {
|
|
590
|
+
return process.stdin.isTTY === true && process.stdout.isTTY === true && process.env.TERM !== "dumb" && process.env.CI === void 0;
|
|
591
|
+
}
|
|
592
|
+
var COLOR = process.env.NO_COLOR === void 0 && process.stdout.isTTY === true;
|
|
593
|
+
function colorDepth() {
|
|
594
|
+
const colorterm = (process.env.COLORTERM ?? "").toLowerCase();
|
|
595
|
+
if (colorterm === "truecolor" || colorterm === "24bit") return 24;
|
|
596
|
+
if (process.env.TERM_PROGRAM === "vscode" || process.env.TERM_PROGRAM === "iTerm.app") return 24;
|
|
597
|
+
if ((process.env.TERM ?? "").includes("256")) return 8;
|
|
598
|
+
return 4;
|
|
599
|
+
}
|
|
600
|
+
function paint(rgb, text) {
|
|
601
|
+
if (!COLOR) return text;
|
|
602
|
+
const depth = colorDepth();
|
|
603
|
+
if (depth === 24) return `\x1B[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${text}\x1B[0m`;
|
|
604
|
+
if (depth === 8) {
|
|
605
|
+
const level = (value) => Math.round(value / 255 * 5);
|
|
606
|
+
const code = 16 + 36 * level(rgb[0]) + 6 * level(rgb[1]) + level(rgb[2]);
|
|
607
|
+
return `\x1B[38;5;${code}m${text}\x1B[0m`;
|
|
608
|
+
}
|
|
609
|
+
return `\x1B[94m${text}\x1B[0m`;
|
|
610
|
+
}
|
|
611
|
+
var BRAND_TOP = [150, 172, 236];
|
|
612
|
+
var BRAND_BOTTOM = [84, 108, 186];
|
|
613
|
+
var BRAND = [118, 140, 214];
|
|
614
|
+
var brand = (text) => paint(BRAND, text);
|
|
615
|
+
function gradient(row, rows) {
|
|
616
|
+
const t = rows <= 1 ? 0 : row / (rows - 1);
|
|
617
|
+
const mix = (a, b) => Math.round(a + (b - a) * t);
|
|
618
|
+
return [
|
|
619
|
+
mix(BRAND_TOP[0], BRAND_BOTTOM[0]),
|
|
620
|
+
mix(BRAND_TOP[1], BRAND_BOTTOM[1]),
|
|
621
|
+
mix(BRAND_TOP[2], BRAND_BOTTOM[2])
|
|
622
|
+
];
|
|
623
|
+
}
|
|
624
|
+
var ANSI = /\u001B\[[0-9;?]*[A-Za-z]/g;
|
|
625
|
+
function visibleLength(text) {
|
|
626
|
+
return [...text.replace(ANSI, "")].length;
|
|
627
|
+
}
|
|
628
|
+
function columns() {
|
|
629
|
+
return process.stdout.columns && process.stdout.columns > 0 ? process.stdout.columns : 80;
|
|
630
|
+
}
|
|
631
|
+
var sleep2 = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
632
|
+
var STARS = [
|
|
633
|
+
[0, -3, "*"],
|
|
634
|
+
[3, -2, "\xB7"],
|
|
635
|
+
[7, -3, "*"],
|
|
636
|
+
[10, -1, "\xB7"],
|
|
637
|
+
[1, 33, "\xB7"],
|
|
638
|
+
[5, 34, "*"],
|
|
639
|
+
[9, 32, "\xB7"]
|
|
640
|
+
];
|
|
641
|
+
async function showWelcome(context) {
|
|
642
|
+
if (!isInteractive()) return;
|
|
643
|
+
const width = Math.min(columns(), 100);
|
|
644
|
+
const logoWidth = Math.max(...LOGO.map((line) => [...line].length));
|
|
645
|
+
const indent = 5;
|
|
646
|
+
const gap = 6;
|
|
647
|
+
const panelWidth = Math.max(
|
|
648
|
+
24,
|
|
649
|
+
...context.facts.map(([label, value]) => label.length + 2 + value.length)
|
|
650
|
+
);
|
|
651
|
+
const sideBySide = width >= indent + logoWidth + gap + panelWidth + 2;
|
|
652
|
+
const showLogo = width >= indent + logoWidth + 2;
|
|
653
|
+
const rule = style.grey("\xB7".repeat(Math.max(10, Math.min(width - 2, 72))));
|
|
654
|
+
const animate = process.env.VEEFORE_NO_ANIMATION === void 0;
|
|
655
|
+
out();
|
|
656
|
+
out(` ${brand(style.bold("Welcome to Veefore"))} ${style.grey(`v${context.version}`)}`);
|
|
657
|
+
out(` ${rule}`);
|
|
658
|
+
out();
|
|
659
|
+
const panel = renderPanel(context.facts);
|
|
660
|
+
if (showLogo) {
|
|
661
|
+
for (let row = 0; row < LOGO.length; row += 1) {
|
|
662
|
+
const line = LOGO[row] ?? "";
|
|
663
|
+
const left = starsFor(row, indent, (column) => column < 0, -indent);
|
|
664
|
+
const right = starsFor(row, 0, (column) => column >= logoWidth, logoWidth);
|
|
665
|
+
const art = paint(gradient(row, LOGO.length), line.padEnd(logoWidth));
|
|
666
|
+
let text = `${left}${art}`;
|
|
667
|
+
if (sideBySide) {
|
|
668
|
+
const top = Math.max(0, Math.floor((LOGO.length - panel.length) / 2));
|
|
669
|
+
const side = panel[row - top];
|
|
670
|
+
text += side === void 0 ? right : `${" ".repeat(gap)}${side}`;
|
|
671
|
+
} else {
|
|
672
|
+
text += right;
|
|
673
|
+
}
|
|
674
|
+
out(text.replace(/\s+$/, ""));
|
|
675
|
+
if (animate) await sleep2(14);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
if (!sideBySide) {
|
|
679
|
+
out();
|
|
680
|
+
for (const line of panel) out(fit(` ${line}`, width - 1));
|
|
681
|
+
}
|
|
682
|
+
out();
|
|
683
|
+
out(` ${rule}`);
|
|
684
|
+
}
|
|
685
|
+
function starsFor(row, width, inBand, offset) {
|
|
686
|
+
const cells = Array.from({ length: Math.max(width, 6) }, () => " ");
|
|
687
|
+
for (const [starRow, column, glyph] of STARS) {
|
|
688
|
+
if (starRow !== row || !inBand(column)) continue;
|
|
689
|
+
const index = column - offset;
|
|
690
|
+
if (index >= 0 && index < cells.length) cells[index] = style.grey(glyph);
|
|
691
|
+
}
|
|
692
|
+
return width === 0 ? cells.join("").replace(/\s+$/, "") : cells.slice(0, width).join("");
|
|
693
|
+
}
|
|
694
|
+
function renderPanel(facts) {
|
|
695
|
+
const lines = [
|
|
696
|
+
style.bold("Veefore"),
|
|
697
|
+
style.grey("Identity, authority and policy"),
|
|
698
|
+
style.grey("for AI agents"),
|
|
699
|
+
""
|
|
700
|
+
];
|
|
701
|
+
const labelWidth = Math.max(0, ...facts.map(([label]) => label.length));
|
|
702
|
+
for (const [label, value] of facts) {
|
|
703
|
+
lines.push(`${style.grey(label.padEnd(labelWidth))} ${value}`);
|
|
704
|
+
}
|
|
705
|
+
return lines;
|
|
706
|
+
}
|
|
707
|
+
var MenuCancelled = class extends CliError {
|
|
708
|
+
constructor() {
|
|
709
|
+
super("Cancelled.", void 0, 130);
|
|
710
|
+
this.name = "MenuCancelled";
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
async function select(question, options, settings = {}) {
|
|
714
|
+
const enabled = options.map((option, index) => option.disabled ? -1 : index).filter((i) => i >= 0);
|
|
715
|
+
if (enabled.length === 0) throw new CliError(`Nothing can be chosen for "${question}".`);
|
|
716
|
+
const fallback = enabled.includes(settings.initial ?? -1) ? settings.initial : enabled[0];
|
|
717
|
+
if (!isInteractive()) return options[fallback].value;
|
|
718
|
+
let cursor = fallback;
|
|
719
|
+
let rendered = 0;
|
|
720
|
+
const stdin = process.stdin;
|
|
721
|
+
const stdout = process.stdout;
|
|
722
|
+
const labelWidth = Math.max(...options.map((option) => [...option.label].length));
|
|
723
|
+
const draw = () => {
|
|
724
|
+
const width = columns();
|
|
725
|
+
const lines = [];
|
|
726
|
+
lines.push(`${brand("?")} ${style.bold(question)}`);
|
|
727
|
+
if (settings.help) lines.push(` ${style.grey(settings.help)}`);
|
|
728
|
+
options.forEach((option, index) => {
|
|
729
|
+
const active = index === cursor;
|
|
730
|
+
const pointer = active ? brand("\u276F") : " ";
|
|
731
|
+
const number = index < 9 ? `${index + 1}.` : " ";
|
|
732
|
+
const text = option.hint ? option.label.padEnd(labelWidth) : option.label;
|
|
733
|
+
const hint = option.hint ? ` ${style.grey(option.hint)}` : "";
|
|
734
|
+
const label = option.disabled ? style.grey(`${number} ${text}`) : active ? brand(`${number} ${text}`) : `${number} ${text}`;
|
|
735
|
+
lines.push(fit(`${pointer} ${label}${hint}`, width - 1));
|
|
736
|
+
});
|
|
737
|
+
lines.push(style.grey(" \u2191\u2193 move \xB7 enter choose \xB7 esc cancel"));
|
|
738
|
+
if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
|
|
739
|
+
stdout.write(`${lines.join("\n")}
|
|
740
|
+
`);
|
|
741
|
+
rendered = lines.length;
|
|
742
|
+
};
|
|
743
|
+
const move = (direction) => {
|
|
744
|
+
const position = enabled.indexOf(cursor);
|
|
745
|
+
const next = (position + direction + enabled.length) % enabled.length;
|
|
746
|
+
cursor = enabled[next];
|
|
747
|
+
};
|
|
748
|
+
return new Promise((resolve, reject) => {
|
|
749
|
+
const wasRaw = stdin.isRaw;
|
|
750
|
+
const restoreCursor = () => {
|
|
751
|
+
stdout.write("\x1B[?25h");
|
|
752
|
+
};
|
|
753
|
+
const finish = (result) => {
|
|
754
|
+
stdin.off("keypress", onKey);
|
|
755
|
+
stdin.setRawMode(wasRaw);
|
|
756
|
+
stdin.pause();
|
|
757
|
+
process.off("exit", restoreCursor);
|
|
758
|
+
if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
|
|
759
|
+
restoreCursor();
|
|
760
|
+
if (result) {
|
|
761
|
+
if (!settings.quiet) {
|
|
762
|
+
stdout.write(`${style.green("\u2713")} ${question} ${brand(result.label)}
|
|
763
|
+
`);
|
|
764
|
+
}
|
|
765
|
+
resolve(result.value);
|
|
766
|
+
} else {
|
|
767
|
+
stdout.write(`${style.grey("\u2717")} ${question} ${style.grey("cancelled")}
|
|
768
|
+
`);
|
|
769
|
+
reject(new MenuCancelled());
|
|
770
|
+
}
|
|
771
|
+
};
|
|
772
|
+
const choose = (index) => {
|
|
773
|
+
const option = options[index];
|
|
774
|
+
if (!option || option.disabled) return;
|
|
775
|
+
finish({ value: option.value, label: option.label });
|
|
776
|
+
};
|
|
777
|
+
const onKey = (input, key = {}) => {
|
|
778
|
+
if (key.ctrl && key.name === "c" || key.name === "escape") return finish(null);
|
|
779
|
+
if (key.name === "return" || key.name === "enter") return choose(cursor);
|
|
780
|
+
if (key.name === "up" || key.name === "k") move(-1);
|
|
781
|
+
else if (key.name === "down" || key.name === "j" || key.name === "tab") move(1);
|
|
782
|
+
else if (input && /^[1-9]$/.test(input)) {
|
|
783
|
+
const index = Number(input) - 1;
|
|
784
|
+
if (options[index] && !options[index]?.disabled) return choose(index);
|
|
785
|
+
return;
|
|
786
|
+
} else if (input) {
|
|
787
|
+
const match = options.findIndex(
|
|
788
|
+
(option) => !option.disabled && option.key?.toLowerCase() === input.toLowerCase()
|
|
789
|
+
);
|
|
790
|
+
if (match >= 0) return choose(match);
|
|
791
|
+
return;
|
|
792
|
+
} else {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
draw();
|
|
796
|
+
};
|
|
797
|
+
process.once("exit", restoreCursor);
|
|
798
|
+
emitKeypressEvents(stdin);
|
|
799
|
+
stdin.setRawMode(true);
|
|
800
|
+
stdin.resume();
|
|
801
|
+
stdout.write("\x1B[?25l");
|
|
802
|
+
stdin.on("keypress", onKey);
|
|
803
|
+
draw();
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
async function selectYesNo(question, defaultYes = false) {
|
|
807
|
+
return select(
|
|
808
|
+
question,
|
|
809
|
+
[
|
|
810
|
+
{ label: "Yes", value: true, key: "y" },
|
|
811
|
+
{ label: "No", value: false, key: "n" }
|
|
812
|
+
],
|
|
813
|
+
{ initial: defaultYes ? 0 : 1 }
|
|
814
|
+
);
|
|
815
|
+
}
|
|
816
|
+
function fit(text, width) {
|
|
817
|
+
if (visibleLength(text) <= width) return text;
|
|
818
|
+
let shown = 0;
|
|
819
|
+
let result = "";
|
|
820
|
+
for (let index = 0; index < text.length; ) {
|
|
821
|
+
ANSI.lastIndex = index;
|
|
822
|
+
const escape = ANSI.exec(text);
|
|
823
|
+
if (escape && escape.index === index) {
|
|
824
|
+
result += escape[0];
|
|
825
|
+
index += escape[0].length;
|
|
826
|
+
continue;
|
|
827
|
+
}
|
|
828
|
+
if (shown >= width - 1) break;
|
|
829
|
+
const char = String.fromCodePoint(text.codePointAt(index));
|
|
830
|
+
result += char;
|
|
831
|
+
shown += 1;
|
|
832
|
+
index += char.length;
|
|
833
|
+
}
|
|
834
|
+
return `${result}\u2026\x1B[0m`;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
// src/terminal.ts
|
|
569
838
|
var COLOR_ENABLED = process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb" && process.stdout.isTTY === true;
|
|
570
839
|
function wrap(code, text) {
|
|
571
840
|
return COLOR_ENABLED ? `\x1B[${code}m${text}\x1B[0m` : text;
|
|
@@ -599,12 +868,12 @@ function heading(text) {
|
|
|
599
868
|
out(style.bold(text));
|
|
600
869
|
out(style.grey("\u2500".repeat(Math.min(text.length, 72))));
|
|
601
870
|
}
|
|
602
|
-
function table(
|
|
871
|
+
function table(columns2, rows) {
|
|
603
872
|
if (rows.length === 0) {
|
|
604
873
|
out(style.grey(" (none)"));
|
|
605
874
|
return;
|
|
606
875
|
}
|
|
607
|
-
const widths =
|
|
876
|
+
const widths = columns2.map((column) => {
|
|
608
877
|
const longest = Math.max(
|
|
609
878
|
column.header.length,
|
|
610
879
|
...rows.map((row) => (row[column.key] ?? "").length)
|
|
@@ -615,17 +884,17 @@ function table(columns, rows) {
|
|
|
615
884
|
const parts = cells.map((cell, index) => {
|
|
616
885
|
const width = widths[index] ?? cell.length;
|
|
617
886
|
const text = cell.length > width ? `${cell.slice(0, Math.max(0, width - 1))}\u2026` : cell;
|
|
618
|
-
return
|
|
887
|
+
return columns2[index]?.align === "right" ? text.padStart(width) : text.padEnd(width);
|
|
619
888
|
});
|
|
620
889
|
out(` ${decorate(parts.join(" "))}`);
|
|
621
890
|
};
|
|
622
891
|
renderRow(
|
|
623
|
-
|
|
892
|
+
columns2.map((column) => column.header),
|
|
624
893
|
style.grey
|
|
625
894
|
);
|
|
626
895
|
for (const row of rows) {
|
|
627
896
|
renderRow(
|
|
628
|
-
|
|
897
|
+
columns2.map((column) => row[column.key] ?? ""),
|
|
629
898
|
(text) => text
|
|
630
899
|
);
|
|
631
900
|
}
|
|
@@ -696,6 +965,14 @@ async function confirm(question, options = {}) {
|
|
|
696
965
|
out(`${question} ${style.grey("[y/N]")} ${style.amber("y (--yes)")}`);
|
|
697
966
|
return true;
|
|
698
967
|
}
|
|
968
|
+
if (isInteractive()) {
|
|
969
|
+
try {
|
|
970
|
+
return await selectYesNo(question);
|
|
971
|
+
} catch (error) {
|
|
972
|
+
if (error instanceof MenuCancelled) return false;
|
|
973
|
+
throw error;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
699
976
|
if (!process.stdin.isTTY) {
|
|
700
977
|
err(
|
|
701
978
|
"This command requires confirmation but the terminal is not interactive. Re-run with --yes to confirm explicitly."
|
|
@@ -1405,11 +1682,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
|
|
|
1405
1682
|
out(` ${style.red("This action is irreversible for this identity.")}`);
|
|
1406
1683
|
out();
|
|
1407
1684
|
}
|
|
1408
|
-
async function lifecycle(args, flags,
|
|
1685
|
+
async function lifecycle(args, flags, path12, resultingStatus) {
|
|
1409
1686
|
const context = await resolveContext(flags);
|
|
1410
1687
|
const agentId = positional(args) ?? context.config.agentId;
|
|
1411
1688
|
const agent = await context.api.get(`/v1/agents/${agentId}`);
|
|
1412
|
-
const reasonRequired =
|
|
1689
|
+
const reasonRequired = path12 === "suspend";
|
|
1413
1690
|
const reason = valueFor(args, "--reason");
|
|
1414
1691
|
if (reasonRequired && (!reason || reason.trim().length < 10)) {
|
|
1415
1692
|
throw new CliError(
|
|
@@ -1419,24 +1696,24 @@ async function lifecycle(args, flags, path11, resultingStatus) {
|
|
|
1419
1696
|
}
|
|
1420
1697
|
out();
|
|
1421
1698
|
out(
|
|
1422
|
-
`${symbol.info} ${
|
|
1699
|
+
`${symbol.info} ${path12} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
|
|
1423
1700
|
);
|
|
1424
|
-
if (
|
|
1701
|
+
if (path12 !== "resume") {
|
|
1425
1702
|
const confirmed = await confirm("Continue?", { yes: flags.yes });
|
|
1426
1703
|
if (!confirmed) {
|
|
1427
1704
|
out(style.grey("Cancelled."));
|
|
1428
1705
|
return 1;
|
|
1429
1706
|
}
|
|
1430
1707
|
}
|
|
1431
|
-
const actionSpinner = spinner(`Applying ${
|
|
1708
|
+
const actionSpinner = spinner(`Applying ${path12}`);
|
|
1432
1709
|
try {
|
|
1433
1710
|
await withStepUp(
|
|
1434
1711
|
context.api,
|
|
1435
|
-
() => context.api.post(`/v1/agents/${agentId}/${
|
|
1712
|
+
() => context.api.post(`/v1/agents/${agentId}/${path12}`, reason ? { reason } : {})
|
|
1436
1713
|
);
|
|
1437
1714
|
actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
|
|
1438
1715
|
} catch (error) {
|
|
1439
|
-
actionSpinner.fail(`Could not ${
|
|
1716
|
+
actionSpinner.fail(`Could not ${path12} the agent`);
|
|
1440
1717
|
throw error;
|
|
1441
1718
|
}
|
|
1442
1719
|
out();
|
|
@@ -3556,32 +3833,30 @@ async function chooseMethod(items, options) {
|
|
|
3556
3833
|
out(` ${style.bold(item.key.padEnd(24))} ${style.grey(`${item.file}:${item.line}`)}`);
|
|
3557
3834
|
}
|
|
3558
3835
|
out();
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
out(style.amber(" Enter 1, 2, 3 or 4."));
|
|
3836
|
+
try {
|
|
3837
|
+
return await select("How would you like to handle them?", [
|
|
3838
|
+
{
|
|
3839
|
+
label: "I will change them myself",
|
|
3840
|
+
hint: "show me exactly what to change",
|
|
3841
|
+
value: "manual"
|
|
3842
|
+
},
|
|
3843
|
+
{
|
|
3844
|
+
label: "Use AI with my own Anthropic or OpenAI key",
|
|
3845
|
+
hint: "code goes only to that provider",
|
|
3846
|
+
value: "own"
|
|
3847
|
+
},
|
|
3848
|
+
{
|
|
3849
|
+
label: "Use Veefore AI",
|
|
3850
|
+
hint: options.hostedAvailable ? "code is sent to Veefore, then to its model provider" : "not enabled on this deployment",
|
|
3851
|
+
value: "veefore",
|
|
3852
|
+
disabled: !options.hostedAvailable
|
|
3853
|
+
},
|
|
3854
|
+
{ label: "Skip for now", value: "skip" }
|
|
3855
|
+
]);
|
|
3856
|
+
} catch (error) {
|
|
3857
|
+
if (error instanceof MenuCancelled) return "skip";
|
|
3858
|
+
throw error;
|
|
3583
3859
|
}
|
|
3584
|
-
return "manual";
|
|
3585
3860
|
}
|
|
3586
3861
|
function printManualInstructions(items) {
|
|
3587
3862
|
heading("Make these changes yourself");
|
|
@@ -3666,9 +3941,7 @@ function printDispatchInstructions(items, where) {
|
|
|
3666
3941
|
out(` ${style.cyan("import { guard } from '@veefore/sdk';")}`);
|
|
3667
3942
|
out();
|
|
3668
3943
|
out(` ${style.grey("// before: const result = await runTool(call);")}`);
|
|
3669
|
-
out(
|
|
3670
|
-
` ${style.cyan("const result = await guard(call.name, (_args: typeof call.args) => runTool(call))(call.args);")}`
|
|
3671
|
-
);
|
|
3944
|
+
out(` ${style.cyan("const result = await runTool(call);")}`);
|
|
3672
3945
|
}
|
|
3673
3946
|
out();
|
|
3674
3947
|
out(
|
|
@@ -3793,16 +4066,21 @@ async function chooseModel(method, context) {
|
|
|
3793
4066
|
async function pick(label, choices, preferred, interactive) {
|
|
3794
4067
|
if (preferred && choices.includes(preferred)) return preferred;
|
|
3795
4068
|
if (!interactive) return choices[0] ?? null;
|
|
3796
|
-
const
|
|
4069
|
+
const OTHER = "\0other";
|
|
3797
4070
|
out();
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
4071
|
+
let picked;
|
|
4072
|
+
try {
|
|
4073
|
+
picked = await select(label, [
|
|
4074
|
+
...choices.slice(0, 20).map((choice) => ({ label: choice, value: choice })),
|
|
4075
|
+
...choices.length > 20 ? [{ label: "Another\u2026", hint: `${choices.length - 20} more`, value: OTHER }] : []
|
|
4076
|
+
]);
|
|
4077
|
+
} catch (error) {
|
|
4078
|
+
if (error instanceof MenuCancelled) return null;
|
|
4079
|
+
throw error;
|
|
4080
|
+
}
|
|
4081
|
+
if (picked !== OTHER) return picked;
|
|
4082
|
+
const typed = (await ask(`${label} id:`)).trim();
|
|
4083
|
+
return choices.includes(typed) ? typed : null;
|
|
3806
4084
|
}
|
|
3807
4085
|
async function runAssist(files, choice, context) {
|
|
3808
4086
|
const accepted = [];
|
|
@@ -6551,1240 +6829,1430 @@ async function undoCommand(args, _flags) {
|
|
|
6551
6829
|
return undoLatest(process.cwd(), { force: args.includes("--force") });
|
|
6552
6830
|
}
|
|
6553
6831
|
|
|
6554
|
-
// src/
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
description: tool.description ?? `Detected ${sourceLabel(tool.source)} tool.`,
|
|
6570
|
-
// One capability per tool keeps authority explicit: granting the agent this
|
|
6571
|
-
// tool grants nothing else. `tool.<key>` satisfies `resource.action`.
|
|
6572
|
-
capability: `tool.${tool.key}`,
|
|
6573
|
-
riskLevel: tool.riskLevel,
|
|
6574
|
-
dangerous: tool.dangerous,
|
|
6575
|
-
destinations: tool.hosts,
|
|
6576
|
-
location: `${tool.location.file}:${tool.location.line}`,
|
|
6577
|
-
money: tool.money ?? null
|
|
6578
|
-
};
|
|
6579
|
-
}
|
|
6580
|
-
function printScan(scan) {
|
|
6581
|
-
out(
|
|
6582
|
-
`${symbol.pass} Scanned ${scan.filesScanned} file${scan.filesScanned === 1 ? "" : "s"}` + (scan.truncated ? style.amber(" (limit reached; findings may be incomplete)") : "") + style.grey(" \u2014 locally, nothing was uploaded")
|
|
6583
|
-
);
|
|
6584
|
-
if (scan.tools.length === 0) {
|
|
6585
|
-
out(`${symbol.warn} No tools found, so there is nothing for Veefore to decide on yet.`);
|
|
6586
|
-
out(style.grey(" Tools are found where they are declared for the model to call, not from"));
|
|
6587
|
-
out(
|
|
6588
|
-
style.grey(
|
|
6589
|
-
scan.framework.language === "python" ? " plain functions: @tool, @function_tool, @mcp.tool, Tool(name=\u2026), or a dict" : ' plain functions: tool({\u2026}), DynamicStructuredTool, { type: "function" },'
|
|
6590
|
-
)
|
|
6591
|
-
);
|
|
6592
|
-
out(
|
|
6593
|
-
style.grey(
|
|
6594
|
-
scan.framework.language === "python" ? " with name, description and an input schema." : " server.tool(\u2026), or an object with name, description and parameters."
|
|
6595
|
-
)
|
|
6596
|
-
);
|
|
6597
|
-
out(style.grey(" Register them in the console, or add them and run `veefore scan` again."));
|
|
6598
|
-
} else {
|
|
6599
|
-
out(`${symbol.pass} Found ${scan.tools.length} tool${scan.tools.length === 1 ? "" : "s"}:`);
|
|
6600
|
-
for (const tool of scan.tools) {
|
|
6601
|
-
const label = tool.riskLevel.padEnd(7);
|
|
6602
|
-
const risk = tool.riskLevel === "HIGH" ? style.red(label) : tool.riskLevel === "MEDIUM" ? style.amber(label) : style.green(label);
|
|
6603
|
-
out(
|
|
6604
|
-
` ${style.bold(tool.key.padEnd(24))} ${risk} ${style.grey(`${tool.location.file}:${tool.location.line}`)}`
|
|
6605
|
-
);
|
|
6606
|
-
if (tool.hosts.length > 0) out(style.grey(` calls ${tool.hosts.join(", ")}`));
|
|
6607
|
-
}
|
|
6832
|
+
// src/commands/logs.ts
|
|
6833
|
+
async function logsCommand(args, flags) {
|
|
6834
|
+
const context = await resolveContext(flags);
|
|
6835
|
+
const follow = args.includes("--follow") || args.includes("-f");
|
|
6836
|
+
const limit = Number(valueFor(args, "--limit") ?? 25);
|
|
6837
|
+
const decisionFilter = valueFor(args, "--decision");
|
|
6838
|
+
const agentFilter = valueFor(args, "--agent") ?? (args.includes("--all") ? void 0 : context.config.agentId);
|
|
6839
|
+
const initial = await fetchActions(context, {
|
|
6840
|
+
limit,
|
|
6841
|
+
decision: decisionFilter,
|
|
6842
|
+
agentId: agentFilter
|
|
6843
|
+
});
|
|
6844
|
+
if (flags.json && !follow) {
|
|
6845
|
+
printJson(initial);
|
|
6846
|
+
return 0;
|
|
6608
6847
|
}
|
|
6609
|
-
|
|
6610
|
-
|
|
6611
|
-
|
|
6612
|
-
|
|
6613
|
-
);
|
|
6848
|
+
heading(`Decisions \u2014 ${context.config.environmentName} (${context.config.environmentType})`);
|
|
6849
|
+
renderActions(initial, flags.verbose);
|
|
6850
|
+
if (!follow) {
|
|
6851
|
+
out();
|
|
6852
|
+
out(style.grey(" Use --follow to stream new decisions as they happen."));
|
|
6853
|
+
out();
|
|
6854
|
+
return 0;
|
|
6614
6855
|
}
|
|
6856
|
+
return streamActions(context, {
|
|
6857
|
+
decision: decisionFilter,
|
|
6858
|
+
agentId: agentFilter,
|
|
6859
|
+
seen: new Set(initial.map((action) => action.id)),
|
|
6860
|
+
verbose: flags.verbose,
|
|
6861
|
+
json: flags.json
|
|
6862
|
+
});
|
|
6615
6863
|
}
|
|
6616
|
-
function
|
|
6864
|
+
async function devCommand(args, flags) {
|
|
6865
|
+
const context = await resolveContext(flags);
|
|
6866
|
+
const environment = await context.api.get(`/v1/environments/${context.config.environmentId}`);
|
|
6867
|
+
const gateway = await probeGateway(context.config.gatewayUrl);
|
|
6868
|
+
const agent = await context.api.get(
|
|
6869
|
+
`/v1/agents/${context.config.agentId}`
|
|
6870
|
+
);
|
|
6871
|
+
heading(`veefore dev \u2014 ${context.config.organizationName} \u203A ${environment.name}`);
|
|
6617
6872
|
out();
|
|
6618
|
-
out(style.bold("Veefore will set up"));
|
|
6619
6873
|
out(
|
|
6620
|
-
` ${symbol.
|
|
6874
|
+
` ${gateway.reachable ? symbol.pass : symbol.fail} Gateway ${gateway.reachable ? style.green("connected") : style.red("unreachable")} ${style.grey(context.config.gatewayUrl)}`
|
|
6621
6875
|
);
|
|
6622
6876
|
out(
|
|
6623
|
-
`
|
|
6877
|
+
` ${agent.status === "ACTIVE" ? symbol.pass : symbol.warn} Agent ${style.bold(agent.name)} ${decisionStyle(agent.status)}`
|
|
6624
6878
|
);
|
|
6625
|
-
|
|
6879
|
+
out(
|
|
6880
|
+
` ${environment.gatewayMode === "ENFORCE" ? symbol.pass : symbol.warn} Mode ${decisionStyle(environment.status === "PAUSED" ? "PAUSED" : environment.gatewayMode)}`
|
|
6881
|
+
);
|
|
6882
|
+
if (!gateway.reachable) {
|
|
6883
|
+
out();
|
|
6626
6884
|
out(
|
|
6627
|
-
` ${symbol.
|
|
6885
|
+
` ${symbol.fail} ${style.red("The gateway is not reachable, so no action can be permitted.")}`
|
|
6628
6886
|
);
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
6632
|
-
` ${style.grey(`flagged high impact (approval under the starter policy): ${dangerous.join(", ")}`)}`
|
|
6633
|
-
);
|
|
6634
|
-
}
|
|
6887
|
+
out(style.grey(" Start it with `npm run dev:gateway`, then re-run `veefore dev`."));
|
|
6888
|
+
out();
|
|
6889
|
+
return 1;
|
|
6635
6890
|
}
|
|
6636
|
-
|
|
6637
|
-
` ${symbol.bullet} A starter policy, in ${style.bold("OBSERVE")} mode: decisions are recorded, nothing is blocked yet`
|
|
6638
|
-
);
|
|
6639
|
-
if (plan.agentHeldSecrets.length > 0) {
|
|
6891
|
+
if (environment.gatewayMode === "OBSERVE") {
|
|
6640
6892
|
out();
|
|
6641
6893
|
out(
|
|
6642
|
-
style.
|
|
6643
|
-
` Your agent keeps holding ${plan.agentHeldSecrets.join(", ")} for now. Veefore will decide every call
|
|
6644
|
-
and record it, but a call that skips the SDK is not stopped until those keys move into
|
|
6645
|
-
the Veefore credential broker. Setup does not read or move any secret.`
|
|
6646
|
-
)
|
|
6894
|
+
` ${symbol.info} ${style.cyan("OBSERVE mode:")} every decision is evaluated and recorded, but nothing is blocked.`
|
|
6647
6895
|
);
|
|
6896
|
+
out(style.grey(" This is the safe way to learn what your agent actually does."));
|
|
6648
6897
|
}
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6653
|
-
|
|
6654
|
-
for
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
dangerous: tool.dangerous
|
|
6661
|
-
})
|
|
6898
|
+
if (agent.isolated) {
|
|
6899
|
+
out();
|
|
6900
|
+
out(` ${symbol.fail} ${style.red("This agent is isolated by an emergency control.")}`);
|
|
6901
|
+
}
|
|
6902
|
+
out();
|
|
6903
|
+
out(style.grey(" Watching for decisions. Press Ctrl+C to stop."));
|
|
6904
|
+
out();
|
|
6905
|
+
const existing = await fetchActions(context, { limit: 5, agentId: context.config.agentId });
|
|
6906
|
+
if (existing.length === 0) {
|
|
6907
|
+
out(
|
|
6908
|
+
` ${symbol.info} No actions recorded yet. Call ${style.bold("agent.act({ tool: \u2026 })")} from your agent to see a decision here.`
|
|
6662
6909
|
);
|
|
6910
|
+
out();
|
|
6911
|
+
} else {
|
|
6912
|
+
renderActions(existing, flags.verbose);
|
|
6663
6913
|
}
|
|
6664
|
-
|
|
6665
|
-
|
|
6666
|
-
|
|
6667
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
);
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
6676
|
-
|
|
6677
|
-
|
|
6678
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
|
-
|
|
6692
|
-
|
|
6693
|
-
|
|
6694
|
-
|
|
6695
|
-
)
|
|
6696
|
-
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
} else {
|
|
6703
|
-
created = await api.post("/v1/agents", {
|
|
6704
|
-
name: plan.agentName,
|
|
6705
|
-
workspace_id: scope.workspaceId,
|
|
6706
|
-
environment_id: scope.environmentId,
|
|
6707
|
-
framework: scope.framework ?? void 0,
|
|
6708
|
-
capabilities: capabilityKeys,
|
|
6709
|
-
public_key: publicKey
|
|
6710
|
-
});
|
|
6711
|
-
report(
|
|
6712
|
-
`${symbol.pass} Agent ${style.bold(created.agent.name)} created ${style.grey(created.agent.id)}`
|
|
6914
|
+
return streamActions(context, {
|
|
6915
|
+
agentId: args.includes("--all") ? void 0 : context.config.agentId,
|
|
6916
|
+
seen: new Set(existing.map((action) => action.id)),
|
|
6917
|
+
verbose: true,
|
|
6918
|
+
json: false,
|
|
6919
|
+
coach: true
|
|
6920
|
+
});
|
|
6921
|
+
}
|
|
6922
|
+
async function fetchActions(context, options) {
|
|
6923
|
+
const response = await context.api.get("/v1/actions", {
|
|
6924
|
+
limit: options.limit ?? 25,
|
|
6925
|
+
decision: options.decision,
|
|
6926
|
+
agent_id: options.agentId,
|
|
6927
|
+
environment_id: context.config.environmentId
|
|
6928
|
+
});
|
|
6929
|
+
return [...response.data].reverse();
|
|
6930
|
+
}
|
|
6931
|
+
function renderActions(actions, verbose) {
|
|
6932
|
+
if (actions.length === 0) {
|
|
6933
|
+
out(style.grey(" (no decisions yet)"));
|
|
6934
|
+
return;
|
|
6935
|
+
}
|
|
6936
|
+
if (!verbose) {
|
|
6937
|
+
table(
|
|
6938
|
+
[
|
|
6939
|
+
{ header: "WHEN", key: "when", width: 12 },
|
|
6940
|
+
{ header: "TOOL", key: "tool", width: 24 },
|
|
6941
|
+
{ header: "DECISION", key: "decision", width: 20 },
|
|
6942
|
+
{ header: "OUTCOME", key: "outcome", width: 24 },
|
|
6943
|
+
{ header: "MS", key: "latency", width: 6, align: "right" }
|
|
6944
|
+
],
|
|
6945
|
+
actions.map((action) => ({
|
|
6946
|
+
when: formatWhen(action.startedAt),
|
|
6947
|
+
tool: action.toolKey ?? action.action,
|
|
6948
|
+
decision: decisionStyle(effectiveDecision(action)),
|
|
6949
|
+
outcome: decisionStyle(action.outcome),
|
|
6950
|
+
latency: String(Math.round(action.latencyMs))
|
|
6951
|
+
}))
|
|
6713
6952
|
);
|
|
6714
|
-
|
|
6715
|
-
report(
|
|
6716
|
-
`${symbol.warn} This is a new agent: delegations and amount limits set on any earlier "${created.agent.name}" do not apply to it. Requests with an amount wait for approval until you add a delegation.`
|
|
6717
|
-
);
|
|
6718
|
-
}
|
|
6953
|
+
return;
|
|
6719
6954
|
}
|
|
6720
|
-
|
|
6721
|
-
|
|
6955
|
+
for (const action of actions) renderAction(action);
|
|
6956
|
+
}
|
|
6957
|
+
function effectiveDecision(action) {
|
|
6958
|
+
return action.shadowDecision ?? action.decision ?? "UNKNOWN";
|
|
6959
|
+
}
|
|
6960
|
+
function renderAction(action, coach = false) {
|
|
6961
|
+
const decision = effectiveDecision(action);
|
|
6962
|
+
const icon = decision === "ALLOW" ? symbol.pass : decision === "BLOCK" ? symbol.fail : symbol.warn;
|
|
6963
|
+
const amount = action.amount === null ? "" : style.grey(` ${formatAmount(action.amount, action.currency)}`);
|
|
6964
|
+
out(
|
|
6965
|
+
`${icon} ${style.grey(new Date(action.startedAt).toISOString().slice(11, 19))} ${style.bold(action.toolKey ?? action.action)}${amount} ${decisionStyle(decision)} ${style.grey(`(${action.outcome})`)}`
|
|
6966
|
+
);
|
|
6967
|
+
if (action.denyReason) out(` ${action.denyReason}`);
|
|
6968
|
+
const facts = [];
|
|
6969
|
+
if (action.policyVersionNumber !== null) facts.push(`policy v${action.policyVersionNumber}`);
|
|
6970
|
+
if (action.matchedRuleIds.length > 0)
|
|
6971
|
+
facts.push(`${action.matchedRuleIds.length} rule(s) matched`);
|
|
6972
|
+
if (action.riskLevel) facts.push(`risk ${action.riskLevel}`);
|
|
6973
|
+
if (action.protocol !== "HTTP") facts.push(action.protocol);
|
|
6974
|
+
if (action.gatewayMode !== "ENFORCE") facts.push(`mode ${action.gatewayMode}`);
|
|
6975
|
+
facts.push(`${Math.round(action.latencyMs)}ms`);
|
|
6976
|
+
out(` ${style.grey(facts.join(" \xB7 "))}`);
|
|
6977
|
+
if (coach) {
|
|
6978
|
+
const hint = coachingFor(action);
|
|
6979
|
+
if (hint) out(` ${style.cyan("\u2192")} ${style.grey(hint)}`);
|
|
6722
6980
|
}
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6981
|
+
}
|
|
6982
|
+
function coachingFor(action) {
|
|
6983
|
+
const decision = effectiveDecision(action);
|
|
6984
|
+
if (action.gatewayMode === "OBSERVE" && decision !== "ALLOW") {
|
|
6985
|
+
return `In ENFORCE mode this would have been ${decision}. Simulate before switching.`;
|
|
6986
|
+
}
|
|
6987
|
+
switch (decision) {
|
|
6988
|
+
case "BLOCK":
|
|
6989
|
+
return action.matchedRuleIds.length > 0 ? "Blocked by a policy rule. Adjust the rule, or grant the capability if this is legitimate." : "Blocked before policy evaluation. Check the tool is registered and the agent is authorized for it.";
|
|
6990
|
+
case "REQUIRE_APPROVAL":
|
|
6991
|
+
return "Call actWithApproval() so your agent waits for the reviewer instead of failing.";
|
|
6992
|
+
case "LIMIT":
|
|
6993
|
+
return "The amount was capped by policy. Read limits.appliedAmount from the decision.";
|
|
6994
|
+
case "MASK":
|
|
6995
|
+
return "Some fields were masked before reaching the tool. Check limits.maskedFields.";
|
|
6996
|
+
case "STEP_UP_AUTH":
|
|
6997
|
+
return "A human must re-authenticate. Agents cannot satisfy step-up on their own.";
|
|
6998
|
+
default:
|
|
6999
|
+
return null;
|
|
7000
|
+
}
|
|
7001
|
+
}
|
|
7002
|
+
function formatAmount(minorUnits, currency) {
|
|
7003
|
+
const major = (minorUnits / 100).toLocaleString("en-IN", {
|
|
7004
|
+
minimumFractionDigits: 2,
|
|
7005
|
+
maximumFractionDigits: 2
|
|
7006
|
+
});
|
|
7007
|
+
return currency ? `${major} ${currency}` : major;
|
|
7008
|
+
}
|
|
7009
|
+
async function streamActions(context, options) {
|
|
7010
|
+
let interval = 1e3;
|
|
7011
|
+
let stopped = false;
|
|
7012
|
+
const stop = () => {
|
|
7013
|
+
stopped = true;
|
|
7014
|
+
};
|
|
7015
|
+
process.once("SIGINT", stop);
|
|
7016
|
+
process.once("SIGTERM", stop);
|
|
7017
|
+
let blocked = 0;
|
|
7018
|
+
let approvals = 0;
|
|
7019
|
+
let allowed = 0;
|
|
7020
|
+
while (!stopped) {
|
|
7021
|
+
let fresh = [];
|
|
6727
7022
|
try {
|
|
6728
|
-
const
|
|
6729
|
-
|
|
6730
|
-
|
|
6731
|
-
|
|
6732
|
-
name: tool.name.slice(0, 200),
|
|
6733
|
-
description: `${tool.description} (found at ${tool.location})`.slice(0, 2e3),
|
|
6734
|
-
kind: "CLIENT",
|
|
6735
|
-
required_capability: tool.capability,
|
|
6736
|
-
destination_allowlist: tool.destinations,
|
|
6737
|
-
risk_level: tool.riskLevel,
|
|
6738
|
-
dangerous: tool.dangerous,
|
|
6739
|
-
// Tells the gateway where the amount and currency are, so it neither
|
|
6740
|
-
// guesses the unit nor assumes a currency.
|
|
6741
|
-
...tool.money?.amountParameter ? { amount_parameter: tool.money.amountParameter, amount_unit: tool.money.amountUnit } : {},
|
|
6742
|
-
...tool.money?.currencyParameter ? { currency_parameter: tool.money.currencyParameter } : {}
|
|
7023
|
+
const recent = await fetchActions(context, {
|
|
7024
|
+
limit: 50,
|
|
7025
|
+
decision: options.decision,
|
|
7026
|
+
agentId: options.agentId
|
|
6743
7027
|
});
|
|
6744
|
-
|
|
7028
|
+
fresh = recent.filter((action) => !options.seen.has(action.id));
|
|
7029
|
+
for (const action of fresh) options.seen.add(action.id);
|
|
6745
7030
|
} catch (error) {
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
7031
|
+
out(
|
|
7032
|
+
`${symbol.warn} ${style.amber("Lost contact with the control plane;")} ${style.grey(error instanceof Error ? error.message : String(error))}`
|
|
7033
|
+
);
|
|
7034
|
+
interval = Math.min(interval * 2, 15e3);
|
|
7035
|
+
await delay(interval);
|
|
7036
|
+
continue;
|
|
6751
7037
|
}
|
|
6752
|
-
|
|
6753
|
-
()
|
|
6754
|
-
|
|
6755
|
-
|
|
7038
|
+
for (const action of fresh) {
|
|
7039
|
+
if (options.json) {
|
|
7040
|
+
out(JSON.stringify(action));
|
|
7041
|
+
} else {
|
|
7042
|
+
renderAction(action, options.coach);
|
|
7043
|
+
}
|
|
7044
|
+
const decision = effectiveDecision(action);
|
|
7045
|
+
if (decision === "BLOCK") blocked += 1;
|
|
7046
|
+
else if (decision === "REQUIRE_APPROVAL") approvals += 1;
|
|
7047
|
+
else allowed += 1;
|
|
7048
|
+
}
|
|
7049
|
+
interval = fresh.length > 0 ? 1e3 : Math.min(interval * 1.5, 5e3);
|
|
7050
|
+
await delay(interval);
|
|
6756
7051
|
}
|
|
6757
|
-
if (
|
|
6758
|
-
|
|
6759
|
-
|
|
6760
|
-
`${
|
|
7052
|
+
if (!options.json) {
|
|
7053
|
+
out();
|
|
7054
|
+
out(
|
|
7055
|
+
` ${style.green(`${allowed} allowed`)} ${style.amber(`${approvals} needing approval`)} ${style.red(`${blocked} blocked`)}`
|
|
6761
7056
|
);
|
|
7057
|
+
out();
|
|
6762
7058
|
}
|
|
6763
|
-
return
|
|
6764
|
-
agentId: created.agent.id,
|
|
6765
|
-
agentName: created.agent.name,
|
|
6766
|
-
keyId: created.identity.key_id,
|
|
6767
|
-
fingerprint: created.identity.fingerprint,
|
|
6768
|
-
privateKey,
|
|
6769
|
-
tools
|
|
6770
|
-
};
|
|
7059
|
+
return 0;
|
|
6771
7060
|
}
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
7061
|
+
function delay(ms) {
|
|
7062
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7063
|
+
}
|
|
7064
|
+
|
|
7065
|
+
// src/commands/test.ts
|
|
7066
|
+
async function testCommand(args, flags) {
|
|
7067
|
+
const context = await resolveContext(flags);
|
|
7068
|
+
const toolFilter = valueFor(args, "--tool");
|
|
7069
|
+
heading(
|
|
7070
|
+
`Enforcement test \u2014 ${context.config.environmentName} (${context.config.environmentType})`
|
|
6782
7071
|
);
|
|
6783
|
-
|
|
6784
|
-
const
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
|
|
6793
|
-
};
|
|
6794
|
-
})
|
|
7072
|
+
const results = [];
|
|
7073
|
+
const record = (check, passed, detail) => {
|
|
7074
|
+
results.push({ check, passed, detail });
|
|
7075
|
+
out(`${passed ? symbol.pass : symbol.fail} ${check} ${style.grey(detail)}`);
|
|
7076
|
+
};
|
|
7077
|
+
const gateway = await probeGateway(context.config.gatewayUrl);
|
|
7078
|
+
record(
|
|
7079
|
+
"Gateway reachable",
|
|
7080
|
+
gateway.reachable,
|
|
7081
|
+
gateway.reachable ? `${context.config.gatewayUrl} \u2014 posture ${gateway.posture ?? "unknown"}` : gateway.error ?? "unreachable"
|
|
6795
7082
|
);
|
|
6796
|
-
|
|
6797
|
-
|
|
7083
|
+
const identities = await context.api.get(`/v1/agents/${context.config.agentId}/identities`).catch(() => []);
|
|
7084
|
+
const activeIdentity = identities.find((identity) => identity.status === "ACTIVE");
|
|
7085
|
+
record(
|
|
7086
|
+
"Agent identity active",
|
|
7087
|
+
Boolean(activeIdentity),
|
|
7088
|
+
activeIdentity ? `generation ${activeIdentity.generation}, expires ${activeIdentity.expiresAt.slice(0, 10)}` : "no ACTIVE identity \u2014 the agent cannot authenticate"
|
|
6798
7089
|
);
|
|
6799
|
-
const
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
6812
|
-
|
|
7090
|
+
const policies = await context.api.get("/v1/policies", { workspace_id: context.config.workspaceId });
|
|
7091
|
+
const effective = policies.filter(
|
|
7092
|
+
(policy) => policy.status === "ACTIVE" && policy.bindings.length > 0
|
|
7093
|
+
);
|
|
7094
|
+
record(
|
|
7095
|
+
"Active, bound policy exists",
|
|
7096
|
+
effective.length > 0,
|
|
7097
|
+
effective.length > 0 ? effective.map((policy) => policy.name).join(", ") : "no active policy is bound to a scope, so no rule can match"
|
|
7098
|
+
);
|
|
7099
|
+
let caseTotal = 0;
|
|
7100
|
+
let caseFailed = 0;
|
|
7101
|
+
for (const policy of policies) {
|
|
7102
|
+
const result = await context.api.post(`/v1/policies/${policy.id}/tests/run`, {}).catch(() => null);
|
|
7103
|
+
if (!result) continue;
|
|
7104
|
+
caseTotal += result.passed + result.failed;
|
|
7105
|
+
caseFailed += result.failed;
|
|
7106
|
+
}
|
|
7107
|
+
record(
|
|
7108
|
+
"Policy test cases",
|
|
7109
|
+
caseFailed === 0,
|
|
7110
|
+
caseTotal === 0 ? "no test cases recorded \u2014 add cases so rule changes are provable" : `${caseTotal - caseFailed}/${caseTotal} passing`
|
|
7111
|
+
);
|
|
7112
|
+
const tools = await resolveAuthorizedTools(context, toolFilter);
|
|
7113
|
+
if (tools.length === 0) {
|
|
7114
|
+
record(
|
|
7115
|
+
"Authorized tools",
|
|
7116
|
+
false,
|
|
7117
|
+
"this agent is authorized for no tools, so it cannot do anything"
|
|
7118
|
+
);
|
|
7119
|
+
} else {
|
|
7120
|
+
record("Authorized tools", true, tools.map((tool) => tool.key).join(", "));
|
|
7121
|
+
}
|
|
7122
|
+
heading("Decision probe (dry run \u2014 nothing is executed)");
|
|
7123
|
+
const probes = [];
|
|
7124
|
+
for (const tool of tools) {
|
|
7125
|
+
const probeSpinner = spinner(`Evaluating ${tool.key}`);
|
|
7126
|
+
try {
|
|
7127
|
+
const outcome = await context.api.post("/v1/policies/simulate", {
|
|
7128
|
+
agent_id: context.config.agentId,
|
|
7129
|
+
tool: tool.key,
|
|
7130
|
+
capability: tool.requiredCapability,
|
|
7131
|
+
environment_id: context.config.environmentId,
|
|
7132
|
+
parameters: {}
|
|
7133
|
+
});
|
|
7134
|
+
probeSpinner.succeed(`${tool.key} \u2192 ${outcome.decision}`);
|
|
7135
|
+
probes.push({ tool, outcome });
|
|
7136
|
+
} catch (error) {
|
|
7137
|
+
probeSpinner.fail(`${tool.key} could not be evaluated`);
|
|
7138
|
+
probes.push({
|
|
7139
|
+
tool,
|
|
7140
|
+
outcome: null,
|
|
7141
|
+
error: error instanceof Error ? error.message : String(error)
|
|
7142
|
+
});
|
|
6813
7143
|
}
|
|
6814
|
-
throw error;
|
|
6815
7144
|
}
|
|
7145
|
+
out();
|
|
7146
|
+
table(
|
|
7147
|
+
[
|
|
7148
|
+
{ header: "TOOL", key: "tool", width: 24 },
|
|
7149
|
+
{ header: "RISK", key: "risk", width: 18 },
|
|
7150
|
+
{ header: "DECISION", key: "decision", width: 20 },
|
|
7151
|
+
{ header: "MATCHED RULE", key: "rule", width: 32 }
|
|
7152
|
+
],
|
|
7153
|
+
probes.map((probe) => ({
|
|
7154
|
+
tool: probe.tool.dangerous ? `${probe.tool.key} ${style.red("(dangerous)")}` : probe.tool.key,
|
|
7155
|
+
risk: decisionStyle(probe.tool.riskLevel),
|
|
7156
|
+
decision: probe.outcome ? decisionStyle(probe.outcome.decision) : style.red("ERROR"),
|
|
7157
|
+
rule: probe.outcome?.matchedRules[0]?.name ?? style.grey(probe.error ?? "default decision")
|
|
7158
|
+
}))
|
|
7159
|
+
);
|
|
7160
|
+
const unguarded = probes.filter(
|
|
7161
|
+
(probe) => probe.tool.dangerous && probe.outcome?.decision === "ALLOW" && probe.outcome.matchedRules.length === 0
|
|
7162
|
+
);
|
|
7163
|
+
out();
|
|
7164
|
+
record(
|
|
7165
|
+
"Dangerous tools are guarded",
|
|
7166
|
+
unguarded.length === 0,
|
|
7167
|
+
unguarded.length === 0 ? "every dangerous tool is either refused or matched by an explicit rule" : `${unguarded.map((probe) => probe.tool.key).join(", ")} allowed by the default decision with no rule`
|
|
7168
|
+
);
|
|
7169
|
+
const errored = probes.filter((probe) => probe.outcome === null);
|
|
7170
|
+
record(
|
|
7171
|
+
"All evaluations completed",
|
|
7172
|
+
errored.length === 0,
|
|
7173
|
+
errored.length === 0 ? `${probes.length} evaluated` : `${errored.length} failed to evaluate`
|
|
7174
|
+
);
|
|
7175
|
+
const impure = probes.filter((probe) => probe.outcome && probe.outcome.sideEffectFree !== true);
|
|
7176
|
+
record(
|
|
7177
|
+
"Simulation was side-effect free",
|
|
7178
|
+
impure.length === 0,
|
|
7179
|
+
impure.length === 0 ? "no action records were written" : "the server reported side effects"
|
|
7180
|
+
);
|
|
7181
|
+
const failed = results.filter((result) => !result.passed);
|
|
7182
|
+
out();
|
|
7183
|
+
out(
|
|
7184
|
+
failed.length === 0 ? ` ${symbol.pass} ${style.green(`${results.length} check(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${failed.length} of ${results.length} check(s) failing.`)}`
|
|
7185
|
+
);
|
|
7186
|
+
if (failed.length > 0) {
|
|
7187
|
+
out();
|
|
7188
|
+
for (const failure of failed) out(` ${symbol.fail} ${failure.check}: ${failure.detail}`);
|
|
7189
|
+
}
|
|
7190
|
+
out();
|
|
7191
|
+
if (flags.json) printJson({ results, probes });
|
|
7192
|
+
return failed.length > 0 ? 1 : 0;
|
|
6816
7193
|
}
|
|
6817
|
-
async function
|
|
6818
|
-
const
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
6824
|
-
|
|
6825
|
-
|
|
6826
|
-
|
|
6827
|
-
|
|
7194
|
+
async function resolveAuthorizedTools(context, filter) {
|
|
7195
|
+
const tools = await context.api.get("/v1/tools", {
|
|
7196
|
+
workspace_id: context.config.workspaceId,
|
|
7197
|
+
agent_id: context.config.agentId,
|
|
7198
|
+
limit: 100
|
|
7199
|
+
});
|
|
7200
|
+
const candidates = tools.items.filter(
|
|
7201
|
+
(tool) => tool.status !== "DISABLED" && (tool.environmentId === null || tool.environmentId === context.config.environmentId)
|
|
7202
|
+
);
|
|
7203
|
+
if (!filter) return candidates;
|
|
7204
|
+
const match = candidates.filter((tool) => tool.key === filter);
|
|
7205
|
+
if (match.length === 0) {
|
|
7206
|
+
throw new CliError(
|
|
7207
|
+
`Tool "${filter}" is not available to this agent in ${context.config.environmentName}.`,
|
|
7208
|
+
`Available: ${candidates.map((tool) => tool.key).join(", ") || "none"}`
|
|
6828
7209
|
);
|
|
6829
|
-
if (match) return match.id;
|
|
6830
|
-
if (page2.items.length < pageSize || offset + pageSize >= page2.total) return null;
|
|
6831
7210
|
}
|
|
6832
|
-
return
|
|
7211
|
+
return match;
|
|
6833
7212
|
}
|
|
6834
|
-
|
|
6835
|
-
|
|
6836
|
-
|
|
6837
|
-
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
return "CrewAI";
|
|
6850
|
-
case "tool-schema":
|
|
6851
|
-
return "tool definition";
|
|
7213
|
+
|
|
7214
|
+
// src/report.ts
|
|
7215
|
+
function reportError(error, flags) {
|
|
7216
|
+
if (error instanceof CliError) {
|
|
7217
|
+
err();
|
|
7218
|
+
err(`${style.red("\u2717")} ${error.message}`);
|
|
7219
|
+
if (error.hint) err(style.grey(` ${error.hint}`));
|
|
7220
|
+
err();
|
|
7221
|
+
return error.exitCode;
|
|
7222
|
+
}
|
|
7223
|
+
if (error instanceof CliApiError) {
|
|
7224
|
+
err();
|
|
7225
|
+
reportApiError(error);
|
|
7226
|
+
err();
|
|
7227
|
+
return 1;
|
|
6852
7228
|
}
|
|
7229
|
+
err();
|
|
7230
|
+
err(`${style.red("\u2717")} ${error instanceof Error ? error.message : String(error)}`);
|
|
7231
|
+
if (flags.verbose && error instanceof Error && error.stack) {
|
|
7232
|
+
err(style.grey(error.stack));
|
|
7233
|
+
} else {
|
|
7234
|
+
err(style.grey(" Re-run with --verbose for the stack trace."));
|
|
7235
|
+
}
|
|
7236
|
+
err();
|
|
7237
|
+
return 1;
|
|
6853
7238
|
}
|
|
6854
7239
|
|
|
6855
|
-
// src/
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6875
|
-
|
|
6876
|
-
|
|
7240
|
+
// src/setup.ts
|
|
7241
|
+
import { generateKeyPairSync } from "node:crypto";
|
|
7242
|
+
function buildPlan(scan, options = {}) {
|
|
7243
|
+
return {
|
|
7244
|
+
agentName: (options.agentName ?? scan.projectName ?? "my-agent").slice(0, 200),
|
|
7245
|
+
tools: scan.tools.map(planTool),
|
|
7246
|
+
agentHeldSecrets: scan.secrets.filter((s) => s.kind === "service").map((s) => s.name),
|
|
7247
|
+
modelSecrets: scan.secrets.filter((s) => s.kind === "model").map((s) => s.name),
|
|
7248
|
+
extraCapabilities: options.extraCapabilities ?? []
|
|
7249
|
+
};
|
|
7250
|
+
}
|
|
7251
|
+
function planTool(tool) {
|
|
7252
|
+
return {
|
|
7253
|
+
key: tool.key,
|
|
7254
|
+
name: tool.name,
|
|
7255
|
+
description: tool.description ?? `Detected ${sourceLabel(tool.source)} tool.`,
|
|
7256
|
+
// One capability per tool keeps authority explicit: granting the agent this
|
|
7257
|
+
// tool grants nothing else. `tool.<key>` satisfies `resource.action`.
|
|
7258
|
+
capability: `tool.${tool.key}`,
|
|
7259
|
+
riskLevel: tool.riskLevel,
|
|
7260
|
+
dangerous: tool.dangerous,
|
|
7261
|
+
destinations: tool.hosts,
|
|
7262
|
+
location: `${tool.location.file}:${tool.location.line}`,
|
|
7263
|
+
money: tool.money ?? null
|
|
7264
|
+
};
|
|
7265
|
+
}
|
|
7266
|
+
function printScan(scan) {
|
|
6877
7267
|
out(
|
|
6878
|
-
`${symbol.pass}
|
|
7268
|
+
`${symbol.pass} Scanned ${scan.filesScanned} file${scan.filesScanned === 1 ? "" : "s"}` + (scan.truncated ? style.amber(" (limit reached; findings may be incomplete)") : "") + style.grey(" \u2014 locally, nothing was uploaded")
|
|
6879
7269
|
);
|
|
6880
|
-
if (
|
|
7270
|
+
if (scan.tools.length === 0) {
|
|
7271
|
+
out(`${symbol.warn} No tools found, so there is nothing for Veefore to decide on yet.`);
|
|
7272
|
+
out(style.grey(" Tools are found where they are declared for the model to call, not from"));
|
|
6881
7273
|
out(
|
|
6882
7274
|
style.grey(
|
|
6883
|
-
"
|
|
7275
|
+
scan.framework.language === "python" ? " plain functions: @tool, @function_tool, @mcp.tool, Tool(name=\u2026), or a dict" : ' plain functions: tool({\u2026}), DynamicStructuredTool, { type: "function" },'
|
|
6884
7276
|
)
|
|
6885
7277
|
);
|
|
6886
|
-
}
|
|
6887
|
-
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
6888
|
-
const controlPlaneUrl = found.controlPlaneUrl;
|
|
6889
|
-
if (found.source !== "entered") {
|
|
6890
7278
|
out(
|
|
6891
|
-
|
|
7279
|
+
style.grey(
|
|
7280
|
+
scan.framework.language === "python" ? " with name, description and an input schema." : " server.tool(\u2026), or an object with name, description and parameters."
|
|
7281
|
+
)
|
|
6892
7282
|
);
|
|
7283
|
+
out(style.grey(" Register them in the console, or add them and run `veefore scan` again."));
|
|
7284
|
+
} else {
|
|
7285
|
+
out(`${symbol.pass} Found ${scan.tools.length} tool${scan.tools.length === 1 ? "" : "s"}:`);
|
|
7286
|
+
for (const tool of scan.tools) {
|
|
7287
|
+
const label = tool.riskLevel.padEnd(7);
|
|
7288
|
+
const risk = tool.riskLevel === "HIGH" ? style.red(label) : tool.riskLevel === "MEDIUM" ? style.amber(label) : style.green(label);
|
|
7289
|
+
out(
|
|
7290
|
+
` ${style.bold(tool.key.padEnd(24))} ${risk} ${style.grey(`${tool.location.file}:${tool.location.line}`)}`
|
|
7291
|
+
);
|
|
7292
|
+
if (tool.hosts.length > 0) out(style.grey(` calls ${tool.hosts.join(", ")}`));
|
|
7293
|
+
}
|
|
6893
7294
|
}
|
|
6894
|
-
|
|
6895
|
-
if (
|
|
6896
|
-
await login(controlPlaneUrl);
|
|
6897
|
-
session = loadSession(controlPlaneUrl);
|
|
6898
|
-
}
|
|
6899
|
-
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
6900
|
-
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
6901
|
-
const me = await api.get("/v1/auth/session");
|
|
6902
|
-
const organization = await selectOrganization(api, me, options, flags);
|
|
6903
|
-
const workspace = await selectWorkspace(api, me, options, flags);
|
|
6904
|
-
const environment = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
6905
|
-
if (environment.type === "PRODUCTION" && !options.force) {
|
|
6906
|
-
out();
|
|
7295
|
+
const service = scan.secrets.filter((s) => s.kind === "service");
|
|
7296
|
+
if (service.length > 0) {
|
|
6907
7297
|
out(
|
|
6908
|
-
`${symbol.warn} ${
|
|
7298
|
+
`${symbol.warn} Your code reads ${service.length} credential${service.length === 1 ? "" : "s"}: ${service.map((s) => s.name).join(", ")} ${style.grey("(names only; values were not read)")}`
|
|
6909
7299
|
);
|
|
6910
|
-
const proceed2 = await confirm("Continue with the production environment?", { yes: flags.yes });
|
|
6911
|
-
if (!proceed2) {
|
|
6912
|
-
out(style.grey("Cancelled. Re-run and choose a development environment."));
|
|
6913
|
-
return 1;
|
|
6914
|
-
}
|
|
6915
|
-
}
|
|
6916
|
-
heading("Looking at your project");
|
|
6917
|
-
const scan = scanProject(process.cwd());
|
|
6918
|
-
printScan(scan);
|
|
6919
|
-
const plan = buildPlan(scan, {
|
|
6920
|
-
agentName: options.agentName,
|
|
6921
|
-
extraCapabilities: options.capabilities
|
|
6922
|
-
});
|
|
6923
|
-
if (!options.agentName && !flags.yes) {
|
|
6924
|
-
plan.agentName = await ask(`Agent name ${style.grey(`[${plan.agentName}]`)}:`, plan.agentName);
|
|
6925
7300
|
}
|
|
6926
|
-
|
|
7301
|
+
}
|
|
7302
|
+
function printPlan2(plan, environment) {
|
|
6927
7303
|
out();
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
|
|
6937
|
-
|
|
6938
|
-
plan,
|
|
6939
|
-
{
|
|
6940
|
-
workspaceId: workspace.id,
|
|
6941
|
-
environmentId: environment.id,
|
|
6942
|
-
framework: framework.key === "unknown" ? null : framework.key
|
|
6943
|
-
},
|
|
6944
|
-
void 0,
|
|
6945
|
-
{ newAgent: options.newAgent === true }
|
|
7304
|
+
out(style.bold("Veefore will set up"));
|
|
7305
|
+
out(
|
|
7306
|
+
` ${symbol.bullet} Agent ${style.bold(plan.agentName)} in ${environment.name} (${environment.type})`
|
|
7307
|
+
);
|
|
7308
|
+
out(
|
|
7309
|
+
` ${style.grey("with its own key pair, generated here. The private key never leaves this machine.")}`
|
|
7310
|
+
);
|
|
7311
|
+
if (plan.tools.length > 0) {
|
|
7312
|
+
out(
|
|
7313
|
+
` ${symbol.bullet} ${plan.tools.length} tool${plan.tools.length === 1 ? "" : "s"}, each with its own capability, authorized for this agent only`
|
|
6946
7314
|
);
|
|
6947
|
-
|
|
6948
|
-
if (
|
|
6949
|
-
throw new CliError(
|
|
6950
|
-
"Your role cannot set up agents in this workspace.",
|
|
6951
|
-
'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
|
|
6952
|
-
);
|
|
6953
|
-
}
|
|
6954
|
-
throw error;
|
|
6955
|
-
}
|
|
6956
|
-
let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
|
|
6957
|
-
if (!gatewayUrl) {
|
|
6958
|
-
const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
|
|
6959
|
-
if (published && await gatewayReachable(published)) {
|
|
6960
|
-
gatewayUrl = published;
|
|
6961
|
-
out(
|
|
6962
|
-
`${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
|
|
6963
|
-
);
|
|
6964
|
-
} else {
|
|
6965
|
-
const suggestion = published ?? DEFAULT_GATEWAY;
|
|
7315
|
+
const dangerous = plan.tools.filter((tool) => tool.dangerous).map((tool) => tool.key);
|
|
7316
|
+
if (dangerous.length > 0) {
|
|
6966
7317
|
out(
|
|
6967
|
-
|
|
7318
|
+
` ${style.grey(`flagged high impact (approval under the starter policy): ${dangerous.join(", ")}`)}`
|
|
6968
7319
|
);
|
|
6969
|
-
gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
|
|
6970
7320
|
}
|
|
6971
7321
|
}
|
|
6972
|
-
gatewayUrl = normalizeUrl(gatewayUrl);
|
|
6973
|
-
const config = {
|
|
6974
|
-
version: 1,
|
|
6975
|
-
controlPlaneUrl,
|
|
6976
|
-
gatewayUrl,
|
|
6977
|
-
organizationId: organization.id,
|
|
6978
|
-
organizationName: organization.name,
|
|
6979
|
-
workspaceId: workspace.id,
|
|
6980
|
-
workspaceName: workspace.name,
|
|
6981
|
-
environmentId: environment.id,
|
|
6982
|
-
environmentName: environment.name,
|
|
6983
|
-
environmentType: environment.type,
|
|
6984
|
-
agentId: created.agentId,
|
|
6985
|
-
agentName: created.agentName,
|
|
6986
|
-
keyId: created.keyId,
|
|
6987
|
-
framework: framework.key === "unknown" ? null : framework.key,
|
|
6988
|
-
declaredTools: created.tools.map((tool) => tool.key),
|
|
6989
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
6990
|
-
};
|
|
6991
|
-
const configFile = writeProjectConfig(config);
|
|
6992
7322
|
out(
|
|
6993
|
-
|
|
7323
|
+
` ${symbol.bullet} A starter policy, in ${style.bold("OBSERVE")} mode: decisions are recorded, nothing is blocked yet`
|
|
6994
7324
|
);
|
|
6995
|
-
|
|
6996
|
-
|
|
6997
|
-
const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
|
|
6998
|
-
const envFile = writeLocalEnv({
|
|
6999
|
-
VEEFORE_GATEWAY_URL: gatewayUrl,
|
|
7000
|
-
VEEFORE_GATEWAY_AUDIENCE: audience,
|
|
7001
|
-
VEEFORE_AGENT_ID: created.agentId,
|
|
7002
|
-
VEEFORE_AGENT_KEY_ID: created.keyId,
|
|
7003
|
-
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
7004
|
-
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
7005
|
-
// Written explicitly so the behaviour is visible and editable in one place.
|
|
7006
|
-
VEEFORE_APPROVAL_MODE: approvalMode
|
|
7007
|
-
});
|
|
7008
|
-
if (audience !== gatewayUrl) {
|
|
7325
|
+
if (plan.agentHeldSecrets.length > 0) {
|
|
7326
|
+
out();
|
|
7009
7327
|
out(
|
|
7010
|
-
|
|
7011
|
-
`
|
|
7012
|
-
|
|
7328
|
+
style.grey(
|
|
7329
|
+
` Your agent keeps holding ${plan.agentHeldSecrets.join(", ")} for now. Veefore will decide every call
|
|
7330
|
+
and record it, but a call that skips the SDK is not stopped until those keys move into
|
|
7331
|
+
the Veefore credential broker. Setup does not read or move any secret.`
|
|
7332
|
+
)
|
|
7013
7333
|
);
|
|
7014
7334
|
}
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
|
|
7019
|
-
|
|
7020
|
-
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
|
|
7024
|
-
|
|
7025
|
-
|
|
7026
|
-
|
|
7027
|
-
|
|
7028
|
-
if (created.tools.length > 0 && !options.skipCode) {
|
|
7029
|
-
const outcome = await integrate(
|
|
7030
|
-
process.cwd(),
|
|
7031
|
-
created.tools.map((tool) => tool.key),
|
|
7032
|
-
{ yes: flags.yes, quiet: flags.json, api }
|
|
7335
|
+
}
|
|
7336
|
+
async function applyPlan(api, plan, scope, report = (line) => out(line), options = {}) {
|
|
7337
|
+
const capabilityKeys = [
|
|
7338
|
+
.../* @__PURE__ */ new Set([...plan.tools.map((t) => t.capability), ...plan.extraCapabilities])
|
|
7339
|
+
];
|
|
7340
|
+
for (const tool of plan.tools) {
|
|
7341
|
+
await createIfAbsent(
|
|
7342
|
+
() => api.post("/v1/capabilities", {
|
|
7343
|
+
key: tool.capability,
|
|
7344
|
+
name: `Use ${tool.name}`.slice(0, 200),
|
|
7345
|
+
description: tool.description.slice(0, 1e3),
|
|
7346
|
+
dangerous: tool.dangerous
|
|
7347
|
+
})
|
|
7033
7348
|
);
|
|
7034
|
-
integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
|
|
7035
|
-
}
|
|
7036
|
-
heading("Validating the connection");
|
|
7037
|
-
const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
|
|
7038
|
-
for (const check of checks) {
|
|
7039
|
-
const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
|
|
7040
|
-
out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
|
|
7041
7349
|
}
|
|
7042
|
-
const
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7350
|
+
const keys = generateKeyPairSync("ed25519");
|
|
7351
|
+
const publicKey = keys.publicKey.export({ type: "spki", format: "der" }).toString(
|
|
7352
|
+
"base64"
|
|
7353
|
+
);
|
|
7354
|
+
const privateKey = keys.privateKey.export({ type: "pkcs8", format: "der" }).toString(
|
|
7355
|
+
"base64"
|
|
7356
|
+
);
|
|
7357
|
+
const existing = options.newAgent ? null : await findExistingAgent(api, scope, plan.agentName);
|
|
7358
|
+
let created;
|
|
7359
|
+
if (existing) {
|
|
7360
|
+
const rotated = await api.post(
|
|
7361
|
+
`/v1/agents/${existing.id}/rotate-credentials`,
|
|
7362
|
+
{ public_key: publicKey, reason: "veefore setup re-run: new key generated on this machine" }
|
|
7363
|
+
);
|
|
7364
|
+
created = {
|
|
7365
|
+
agent: { id: existing.id, name: existing.name },
|
|
7366
|
+
identity: { key_id: rotated.key_id, fingerprint: rotated.fingerprint }
|
|
7367
|
+
};
|
|
7368
|
+
const held = new Set(
|
|
7369
|
+
(await api.get(
|
|
7370
|
+
`/v1/agents/${existing.id}/capabilities`
|
|
7371
|
+
)).filter((grant) => !grant.revokedAt).map((grant) => grant.capabilityKey)
|
|
7372
|
+
);
|
|
7373
|
+
for (const key of capabilityKeys.filter((key2) => !held.has(key2))) {
|
|
7374
|
+
await createIfAbsent(
|
|
7375
|
+
() => api.post(`/v1/agents/${existing.id}/capabilities`, { capability_key: key })
|
|
7376
|
+
);
|
|
7048
7377
|
}
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7054
|
-
|
|
7055
|
-
|
|
7378
|
+
report(
|
|
7379
|
+
`${symbol.pass} Agent ${style.bold(existing.name)} reused ${style.grey(existing.id)}` + style.grey(
|
|
7380
|
+
` \u2014 new key issued; its delegations (${existing.delegations}) and grants stay in place`
|
|
7381
|
+
)
|
|
7382
|
+
);
|
|
7383
|
+
if (existing.duplicates > 0) {
|
|
7384
|
+
report(
|
|
7385
|
+
`${symbol.warn} ${existing.duplicates} other agent${existing.duplicates === 1 ? "" : "s"} named "${existing.name}" exist${existing.duplicates === 1 ? "s" : ""} here. Suspend the ones you no longer use in the console (Agents).`
|
|
7056
7386
|
);
|
|
7057
7387
|
}
|
|
7058
|
-
|
|
7059
|
-
|
|
7388
|
+
} else {
|
|
7389
|
+
created = await api.post("/v1/agents", {
|
|
7390
|
+
name: plan.agentName,
|
|
7391
|
+
workspace_id: scope.workspaceId,
|
|
7392
|
+
environment_id: scope.environmentId,
|
|
7393
|
+
framework: scope.framework ?? void 0,
|
|
7394
|
+
capabilities: capabilityKeys,
|
|
7395
|
+
public_key: publicKey
|
|
7396
|
+
});
|
|
7397
|
+
report(
|
|
7398
|
+
`${symbol.pass} Agent ${style.bold(created.agent.name)} created ${style.grey(created.agent.id)}`
|
|
7399
|
+
);
|
|
7400
|
+
if (options.newAgent) {
|
|
7401
|
+
report(
|
|
7402
|
+
`${symbol.warn} This is a new agent: delegations and amount limits set on any earlier "${created.agent.name}" do not apply to it. Requests with an amount wait for approval until you add a delegation.`
|
|
7403
|
+
);
|
|
7060
7404
|
}
|
|
7061
7405
|
}
|
|
7062
|
-
|
|
7063
|
-
|
|
7064
|
-
|
|
7065
|
-
|
|
7066
|
-
|
|
7067
|
-
|
|
7068
|
-
|
|
7069
|
-
|
|
7070
|
-
|
|
7071
|
-
|
|
7072
|
-
|
|
7073
|
-
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
|
|
7086
|
-
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7406
|
+
if (!created.identity) {
|
|
7407
|
+
throw new Error("The agent was created without an identity.");
|
|
7408
|
+
}
|
|
7409
|
+
const tools = [];
|
|
7410
|
+
for (const tool of plan.tools) {
|
|
7411
|
+
let toolId;
|
|
7412
|
+
let reused = false;
|
|
7413
|
+
try {
|
|
7414
|
+
const registered = await api.post("/v1/tools", {
|
|
7415
|
+
workspace_id: scope.workspaceId,
|
|
7416
|
+
environment_id: scope.environmentId,
|
|
7417
|
+
key: tool.key,
|
|
7418
|
+
name: tool.name.slice(0, 200),
|
|
7419
|
+
description: `${tool.description} (found at ${tool.location})`.slice(0, 2e3),
|
|
7420
|
+
kind: "CLIENT",
|
|
7421
|
+
required_capability: tool.capability,
|
|
7422
|
+
destination_allowlist: tool.destinations,
|
|
7423
|
+
risk_level: tool.riskLevel,
|
|
7424
|
+
dangerous: tool.dangerous,
|
|
7425
|
+
// Tells the gateway where the amount and currency are, so it neither
|
|
7426
|
+
// guesses the unit nor assumes a currency.
|
|
7427
|
+
...tool.money?.amountParameter ? { amount_parameter: tool.money.amountParameter, amount_unit: tool.money.amountUnit } : {},
|
|
7428
|
+
...tool.money?.currencyParameter ? { currency_parameter: tool.money.currencyParameter } : {}
|
|
7429
|
+
});
|
|
7430
|
+
toolId = registered.id;
|
|
7431
|
+
} catch (error) {
|
|
7432
|
+
if (!(error instanceof CliApiError && error.code === "ALREADY_EXISTS")) throw error;
|
|
7433
|
+
const existing2 = await findTool(api, scope, tool.key);
|
|
7434
|
+
if (!existing2) throw error;
|
|
7435
|
+
toolId = existing2;
|
|
7436
|
+
reused = true;
|
|
7437
|
+
}
|
|
7438
|
+
await createIfAbsent(
|
|
7439
|
+
() => api.post(`/v1/tools/${toolId}/agents`, { agent_id: created.agent.id })
|
|
7093
7440
|
);
|
|
7441
|
+
tools.push({ key: tool.key, id: toolId, reused });
|
|
7094
7442
|
}
|
|
7095
|
-
|
|
7443
|
+
if (tools.length > 0) {
|
|
7444
|
+
const reusedCount = tools.filter((tool) => tool.reused).length;
|
|
7445
|
+
report(
|
|
7446
|
+
`${symbol.pass} ${tools.length} tool${tools.length === 1 ? "" : "s"} authorized for this agent` + (reusedCount > 0 ? style.grey(` (${reusedCount} already registered, reused)`) : "")
|
|
7447
|
+
);
|
|
7448
|
+
}
|
|
7449
|
+
return {
|
|
7450
|
+
agentId: created.agent.id,
|
|
7451
|
+
agentName: created.agent.name,
|
|
7452
|
+
keyId: created.identity.key_id,
|
|
7453
|
+
fingerprint: created.identity.fingerprint,
|
|
7454
|
+
privateKey,
|
|
7455
|
+
tools
|
|
7456
|
+
};
|
|
7096
7457
|
}
|
|
7097
|
-
function
|
|
7458
|
+
async function findExistingAgent(api, scope, name) {
|
|
7459
|
+
const wanted = name.trim().toLowerCase();
|
|
7460
|
+
const page2 = await api.get("/v1/agents", {
|
|
7461
|
+
workspace_id: scope.workspaceId,
|
|
7462
|
+
environment_id: scope.environmentId,
|
|
7463
|
+
q: name.trim(),
|
|
7464
|
+
limit: 100
|
|
7465
|
+
});
|
|
7466
|
+
const matches = page2.items.filter(
|
|
7467
|
+
(agent) => agent.name.trim().toLowerCase() === wanted && agent.status !== "REVOKED"
|
|
7468
|
+
);
|
|
7469
|
+
if (matches.length === 0) return null;
|
|
7470
|
+
const ranked = await Promise.all(
|
|
7471
|
+
matches.map(async (agent) => {
|
|
7472
|
+
const delegations = await api.get("/v1/delegations", {
|
|
7473
|
+
subject_id: agent.id,
|
|
7474
|
+
environment_id: scope.environmentId
|
|
7475
|
+
}).then((result) => Array.isArray(result) ? result : result.items).catch(() => []);
|
|
7476
|
+
return {
|
|
7477
|
+
agent,
|
|
7478
|
+
active: delegations.filter((delegation) => delegation.status === "ACTIVE").length
|
|
7479
|
+
};
|
|
7480
|
+
})
|
|
7481
|
+
);
|
|
7482
|
+
ranked.sort(
|
|
7483
|
+
(a, b) => b.active - a.active || (b.agent.createdAt ?? "").localeCompare(a.agent.createdAt ?? "")
|
|
7484
|
+
);
|
|
7485
|
+
const best = ranked[0];
|
|
7486
|
+
return {
|
|
7487
|
+
id: best.agent.id,
|
|
7488
|
+
name: best.agent.name,
|
|
7489
|
+
delegations: best.active,
|
|
7490
|
+
duplicates: matches.length - 1
|
|
7491
|
+
};
|
|
7492
|
+
}
|
|
7493
|
+
async function createIfAbsent(call) {
|
|
7098
7494
|
try {
|
|
7099
|
-
|
|
7100
|
-
|
|
7101
|
-
|
|
7102
|
-
|
|
7103
|
-
|
|
7495
|
+
await call();
|
|
7496
|
+
} catch (error) {
|
|
7497
|
+
if (error instanceof CliApiError && (error.code === "ALREADY_EXISTS" || error.status === 409)) {
|
|
7498
|
+
return;
|
|
7499
|
+
}
|
|
7500
|
+
throw error;
|
|
7104
7501
|
}
|
|
7105
7502
|
}
|
|
7106
|
-
function
|
|
7107
|
-
|
|
7108
|
-
|
|
7109
|
-
|
|
7110
|
-
|
|
7111
|
-
|
|
7112
|
-
|
|
7113
|
-
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
|
|
7118
|
-
|
|
7119
|
-
|
|
7503
|
+
async function findTool(api, scope, key) {
|
|
7504
|
+
const pageSize = 100;
|
|
7505
|
+
for (let offset = 0; offset < 1e4; offset += pageSize) {
|
|
7506
|
+
const page2 = await api.get("/v1/tools", {
|
|
7507
|
+
workspace_id: scope.workspaceId,
|
|
7508
|
+
environment_id: scope.environmentId,
|
|
7509
|
+
limit: pageSize,
|
|
7510
|
+
offset
|
|
7511
|
+
});
|
|
7512
|
+
const match = page2.items.find(
|
|
7513
|
+
(tool) => tool.key === key && tool.environmentId === scope.environmentId
|
|
7514
|
+
);
|
|
7515
|
+
if (match) return match.id;
|
|
7516
|
+
if (page2.items.length < pageSize || offset + pageSize >= page2.total) return null;
|
|
7120
7517
|
}
|
|
7121
|
-
return
|
|
7122
|
-
` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
|
|
7123
|
-
" carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
|
|
7124
|
-
" or the action never happens.",
|
|
7125
|
-
style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
|
|
7126
|
-
];
|
|
7518
|
+
return null;
|
|
7127
7519
|
}
|
|
7128
|
-
|
|
7129
|
-
|
|
7130
|
-
|
|
7131
|
-
|
|
7132
|
-
|
|
7133
|
-
|
|
7134
|
-
|
|
7135
|
-
|
|
7136
|
-
|
|
7137
|
-
|
|
7138
|
-
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7520
|
+
function sourceLabel(source) {
|
|
7521
|
+
switch (source) {
|
|
7522
|
+
case "vercel-ai":
|
|
7523
|
+
return "Vercel AI SDK";
|
|
7524
|
+
case "langchain-js":
|
|
7525
|
+
case "langchain-py":
|
|
7526
|
+
return "LangChain";
|
|
7527
|
+
case "openai-function":
|
|
7528
|
+
return "OpenAI function";
|
|
7529
|
+
case "openai-agents":
|
|
7530
|
+
case "openai-agents-py":
|
|
7531
|
+
return "OpenAI Agents SDK";
|
|
7532
|
+
case "mcp":
|
|
7533
|
+
return "MCP";
|
|
7534
|
+
case "crewai":
|
|
7535
|
+
return "CrewAI";
|
|
7536
|
+
case "tool-schema":
|
|
7537
|
+
return "tool definition";
|
|
7146
7538
|
}
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7539
|
+
}
|
|
7540
|
+
|
|
7541
|
+
// src/commands/init.ts
|
|
7542
|
+
var DEFAULT_GATEWAY = "http://localhost:4010";
|
|
7543
|
+
async function initCommand(args, flags) {
|
|
7544
|
+
const options = parseInitArgs(args);
|
|
7545
|
+
const existing = readProjectConfig();
|
|
7546
|
+
if (existing && !options.force && process.stdin.isTTY && process.stdout.isTTY && !flags.json) {
|
|
7547
|
+
return projectHome(existing, flags);
|
|
7152
7548
|
}
|
|
7153
|
-
|
|
7154
|
-
|
|
7155
|
-
|
|
7156
|
-
[
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7549
|
+
if (existing && !options.force) {
|
|
7550
|
+
out();
|
|
7551
|
+
out(`${symbol.warn} This project is already connected to Veefore.`);
|
|
7552
|
+
details([
|
|
7553
|
+
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
7554
|
+
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
7555
|
+
["Control plane", existing.controlPlaneUrl]
|
|
7556
|
+
]);
|
|
7557
|
+
out();
|
|
7558
|
+
out(style.grey("Re-run with --force to replace the configuration, or use `veefore status`."));
|
|
7559
|
+
return 0;
|
|
7560
|
+
}
|
|
7561
|
+
heading("Veefore init");
|
|
7562
|
+
const framework = detectFramework();
|
|
7563
|
+
out(
|
|
7564
|
+
`${symbol.pass} Framework: ${style.bold(framework.name)} ${style.grey(`(${framework.language}, matched ${framework.evidence})`)}`
|
|
7168
7565
|
);
|
|
7169
|
-
|
|
7170
|
-
|
|
7171
|
-
|
|
7172
|
-
|
|
7173
|
-
|
|
7174
|
-
}
|
|
7175
|
-
async function selectWorkspace(api, me, options, flags) {
|
|
7176
|
-
const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
|
|
7177
|
-
if (workspaces.length === 0) {
|
|
7178
|
-
throw new CliError(
|
|
7179
|
-
"This organization has no workspaces.",
|
|
7180
|
-
"Create one in the console, or ask an admin to."
|
|
7566
|
+
if (framework.key === "unknown") {
|
|
7567
|
+
out(
|
|
7568
|
+
style.grey(
|
|
7569
|
+
" No manifest was found. Veefore will still work; the integration snippet defaults to TypeScript."
|
|
7570
|
+
)
|
|
7181
7571
|
);
|
|
7182
7572
|
}
|
|
7183
|
-
|
|
7184
|
-
|
|
7185
|
-
|
|
7186
|
-
|
|
7573
|
+
const found = await locateControlPlane(options.controlPlaneUrl ?? flags.controlPlaneUrl);
|
|
7574
|
+
const controlPlaneUrl = found.controlPlaneUrl;
|
|
7575
|
+
if (found.source !== "entered") {
|
|
7576
|
+
out(
|
|
7577
|
+
`${symbol.pass} Veefore: ${style.bold(controlPlaneUrl)} ${style.grey(`(${found.source})`)}`
|
|
7578
|
+
);
|
|
7187
7579
|
}
|
|
7188
|
-
|
|
7189
|
-
|
|
7190
|
-
|
|
7191
|
-
|
|
7580
|
+
let session = loadSession(controlPlaneUrl);
|
|
7581
|
+
if (!session) {
|
|
7582
|
+
await login(controlPlaneUrl);
|
|
7583
|
+
session = loadSession(controlPlaneUrl);
|
|
7192
7584
|
}
|
|
7193
|
-
|
|
7194
|
-
|
|
7195
|
-
|
|
7196
|
-
|
|
7197
|
-
|
|
7198
|
-
|
|
7199
|
-
|
|
7200
|
-
|
|
7201
|
-
|
|
7202
|
-
|
|
7203
|
-
name: workspace.name,
|
|
7204
|
-
id: workspace.id
|
|
7205
|
-
}))
|
|
7206
|
-
);
|
|
7207
|
-
const chosen = workspaces[await pickIndex(workspaces.length)];
|
|
7208
|
-
if (!chosen) throw new CliError("No workspace selected.");
|
|
7209
|
-
out(`${symbol.pass} Workspace: ${style.bold(chosen.name)}`);
|
|
7210
|
-
return chosen;
|
|
7211
|
-
}
|
|
7212
|
-
async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
7213
|
-
const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
|
|
7214
|
-
const environments = all.filter((environment) => environment.workspaceId === workspaceId);
|
|
7215
|
-
if (environments.length === 0) {
|
|
7216
|
-
throw new CliError(
|
|
7217
|
-
"That workspace has no environments.",
|
|
7218
|
-
"Create one in the console, or ask an admin to."
|
|
7585
|
+
if (!session) throw new CliError("Sign-in did not produce a usable session.");
|
|
7586
|
+
const api = new ApiClient({ controlPlaneUrl, token: session.token });
|
|
7587
|
+
const me = await api.get("/v1/auth/session");
|
|
7588
|
+
const organization = await selectOrganization(api, me, options, flags);
|
|
7589
|
+
const workspace = await selectWorkspace(api, me, options, flags);
|
|
7590
|
+
const environment = await selectEnvironment(api, me, workspace.id, options, flags);
|
|
7591
|
+
if (environment.type === "PRODUCTION" && !options.force) {
|
|
7592
|
+
out();
|
|
7593
|
+
out(
|
|
7594
|
+
`${symbol.warn} ${style.amber("You selected a PRODUCTION environment.")} A new agent here starts in ${style.bold("OBSERVE")} mode and performs no enforcement until you switch it.`
|
|
7219
7595
|
);
|
|
7596
|
+
const proceed2 = await confirm("Continue with the production environment?", { yes: flags.yes });
|
|
7597
|
+
if (!proceed2) {
|
|
7598
|
+
out(style.grey("Cancelled. Re-run and choose a development environment."));
|
|
7599
|
+
return 1;
|
|
7600
|
+
}
|
|
7220
7601
|
}
|
|
7221
|
-
|
|
7222
|
-
|
|
7223
|
-
|
|
7224
|
-
|
|
7225
|
-
|
|
7226
|
-
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7602
|
+
heading("Looking at your project");
|
|
7603
|
+
const scan = scanProject(process.cwd());
|
|
7604
|
+
printScan(scan);
|
|
7605
|
+
const plan = buildPlan(scan, {
|
|
7606
|
+
agentName: options.agentName,
|
|
7607
|
+
extraCapabilities: options.capabilities
|
|
7608
|
+
});
|
|
7609
|
+
if (!options.agentName && !flags.yes) {
|
|
7610
|
+
plan.agentName = await ask(`Agent name ${style.grey(`[${plan.agentName}]`)}:`, plan.agentName);
|
|
7230
7611
|
}
|
|
7612
|
+
printPlan2(plan, environment);
|
|
7231
7613
|
out();
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
{ header: "NAME", key: "name" },
|
|
7237
|
-
{ header: "TYPE", key: "type" },
|
|
7238
|
-
{ header: "MODE", key: "mode" },
|
|
7239
|
-
{ header: "ID", key: "id" }
|
|
7240
|
-
],
|
|
7241
|
-
environments.map((environment, index) => ({
|
|
7242
|
-
index: String(index + 1),
|
|
7243
|
-
name: environment.name,
|
|
7244
|
-
type: environment.type,
|
|
7245
|
-
mode: environment.gatewayMode ?? "\u2014",
|
|
7246
|
-
id: environment.id
|
|
7247
|
-
}))
|
|
7248
|
-
);
|
|
7249
|
-
const chosen = environments[await pickIndex(environments.length)];
|
|
7250
|
-
if (!chosen) throw new CliError("No environment selected.");
|
|
7251
|
-
out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
|
|
7252
|
-
return chosen;
|
|
7253
|
-
}
|
|
7254
|
-
async function discoverAudience(gatewayUrl) {
|
|
7255
|
-
try {
|
|
7256
|
-
const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
|
|
7257
|
-
headers: { accept: "application/json" }
|
|
7258
|
-
});
|
|
7259
|
-
if (!response.ok) return gatewayUrl;
|
|
7260
|
-
const body = await response.json();
|
|
7261
|
-
const audience = body.data?.audience;
|
|
7262
|
-
return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
|
|
7263
|
-
} catch {
|
|
7264
|
-
return gatewayUrl;
|
|
7614
|
+
const proceed = await confirm("Set this up?", { yes: flags.yes });
|
|
7615
|
+
if (!proceed) {
|
|
7616
|
+
out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
|
|
7617
|
+
return 1;
|
|
7265
7618
|
}
|
|
7266
|
-
|
|
7267
|
-
|
|
7268
|
-
|
|
7269
|
-
|
|
7270
|
-
|
|
7271
|
-
|
|
7272
|
-
|
|
7619
|
+
heading("Setting up");
|
|
7620
|
+
let created;
|
|
7621
|
+
try {
|
|
7622
|
+
created = await applyPlan(
|
|
7623
|
+
api,
|
|
7624
|
+
plan,
|
|
7625
|
+
{
|
|
7626
|
+
workspaceId: workspace.id,
|
|
7627
|
+
environmentId: environment.id,
|
|
7628
|
+
framework: framework.key === "unknown" ? null : framework.key
|
|
7629
|
+
},
|
|
7630
|
+
void 0,
|
|
7631
|
+
{ newAgent: options.newAgent === true }
|
|
7632
|
+
);
|
|
7633
|
+
} catch (error) {
|
|
7634
|
+
if (error instanceof CliApiError && error.code === "PERMISSION_DENIED") {
|
|
7635
|
+
throw new CliError(
|
|
7636
|
+
"Your role cannot set up agents in this workspace.",
|
|
7637
|
+
'Ask an owner or admin for the "agent:create" and "tool:create" permissions.'
|
|
7638
|
+
);
|
|
7639
|
+
}
|
|
7640
|
+
throw error;
|
|
7273
7641
|
}
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
|
|
7277
|
-
|
|
7278
|
-
|
|
7279
|
-
|
|
7642
|
+
let gatewayUrl = options.gatewayUrl ?? flags.gatewayUrl ?? process.env.VEEFORE_GATEWAY_URL;
|
|
7643
|
+
if (!gatewayUrl) {
|
|
7644
|
+
const published = (await controlPlaneInfo(controlPlaneUrl)).gatewayUrl;
|
|
7645
|
+
if (published && await gatewayReachable(published)) {
|
|
7646
|
+
gatewayUrl = published;
|
|
7647
|
+
out(
|
|
7648
|
+
`${symbol.pass} Gateway: ${style.bold(published)} ${style.grey("(published by the control plane)")}`
|
|
7649
|
+
);
|
|
7650
|
+
} else {
|
|
7651
|
+
const suggestion = published ?? DEFAULT_GATEWAY;
|
|
7652
|
+
out(
|
|
7653
|
+
`${symbol.warn} ${published ? `The gateway at ${published} is not answering.` : "The control plane did not publish a gateway address."}`
|
|
7654
|
+
);
|
|
7655
|
+
gatewayUrl = await ask(`Gateway URL ${style.grey(`[${suggestion}]`)}:`, suggestion);
|
|
7656
|
+
}
|
|
7280
7657
|
}
|
|
7281
|
-
|
|
7282
|
-
|
|
7283
|
-
|
|
7658
|
+
gatewayUrl = normalizeUrl(gatewayUrl);
|
|
7659
|
+
const config = {
|
|
7660
|
+
version: 1,
|
|
7661
|
+
controlPlaneUrl,
|
|
7662
|
+
gatewayUrl,
|
|
7663
|
+
organizationId: organization.id,
|
|
7664
|
+
organizationName: organization.name,
|
|
7665
|
+
workspaceId: workspace.id,
|
|
7666
|
+
workspaceName: workspace.name,
|
|
7667
|
+
environmentId: environment.id,
|
|
7668
|
+
environmentName: environment.name,
|
|
7669
|
+
environmentType: environment.type,
|
|
7670
|
+
agentId: created.agentId,
|
|
7671
|
+
agentName: created.agentName,
|
|
7672
|
+
keyId: created.keyId,
|
|
7673
|
+
framework: framework.key === "unknown" ? null : framework.key,
|
|
7674
|
+
declaredTools: created.tools.map((tool) => tool.key),
|
|
7675
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
7676
|
+
};
|
|
7677
|
+
const configFile = writeProjectConfig(config);
|
|
7678
|
+
out(
|
|
7679
|
+
`${symbol.pass} Wrote ${style.bold("veefore.config.json")} ${style.grey("(safe to commit)")}`
|
|
7284
7680
|
);
|
|
7285
|
-
const
|
|
7286
|
-
|
|
7287
|
-
|
|
7288
|
-
|
|
7289
|
-
|
|
7290
|
-
|
|
7291
|
-
|
|
7292
|
-
|
|
7293
|
-
|
|
7294
|
-
|
|
7295
|
-
|
|
7296
|
-
|
|
7297
|
-
|
|
7298
|
-
|
|
7299
|
-
name: "Starter \u2014 safe defaults",
|
|
7300
|
-
template_key: "starter-observe-safe-defaults"
|
|
7301
|
-
});
|
|
7302
|
-
await api.post(`/v1/policies/${result.policy.id}/bindings`, {
|
|
7303
|
-
scope: "WORKSPACE",
|
|
7304
|
-
scope_id: workspaceId
|
|
7305
|
-
});
|
|
7306
|
-
policySpinner.succeed(
|
|
7307
|
-
`Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
|
|
7308
|
-
);
|
|
7309
|
-
keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
|
|
7681
|
+
const audience = await discoverAudience(gatewayUrl);
|
|
7682
|
+
ensureGitignore(process.cwd(), [".env.local", ".veefore/"]);
|
|
7683
|
+
const approvalMode = options.approvalMode ?? previousApprovalMode() ?? "wait";
|
|
7684
|
+
const envFile = writeLocalEnv({
|
|
7685
|
+
VEEFORE_GATEWAY_URL: gatewayUrl,
|
|
7686
|
+
VEEFORE_GATEWAY_AUDIENCE: audience,
|
|
7687
|
+
VEEFORE_AGENT_ID: created.agentId,
|
|
7688
|
+
VEEFORE_AGENT_KEY_ID: created.keyId,
|
|
7689
|
+
VEEFORE_AGENT_PRIVATE_KEY: created.privateKey,
|
|
7690
|
+
...created.tools.length > 0 ? { VEEFORE_DECLARED_TOOLS: created.tools.map((tool) => tool.key).join(",") } : {},
|
|
7691
|
+
// Written explicitly so the behaviour is visible and editable in one place.
|
|
7692
|
+
VEEFORE_APPROVAL_MODE: approvalMode
|
|
7693
|
+
});
|
|
7694
|
+
if (audience !== gatewayUrl) {
|
|
7310
7695
|
out(
|
|
7311
|
-
style.grey(
|
|
7312
|
-
|
|
7313
|
-
)
|
|
7696
|
+
`${symbol.info} Gateway audience is ${style.bold(audience)} ${style.grey(
|
|
7697
|
+
`(it reports this, rather than the ${gatewayUrl} you dialled)`
|
|
7698
|
+
)}`
|
|
7314
7699
|
);
|
|
7315
|
-
} catch (error) {
|
|
7316
|
-
policySpinner.fail("Could not create the starter policy");
|
|
7317
|
-
if (error instanceof CliApiError) {
|
|
7318
|
-
out(style.grey(` ${error.message}`));
|
|
7319
|
-
out(style.grey(" You can create one from a template in the console instead."));
|
|
7320
|
-
return;
|
|
7321
|
-
}
|
|
7322
|
-
throw error;
|
|
7323
7700
|
}
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7701
|
+
out(
|
|
7702
|
+
`${symbol.pass} Wrote agent credentials to ${style.bold(".env.local")} ${style.grey("(mode 0600, gitignored)")}`
|
|
7703
|
+
);
|
|
7704
|
+
out();
|
|
7705
|
+
out(
|
|
7706
|
+
` ${style.amber("The private key was generated here and written to that file only.")} Veefore`
|
|
7707
|
+
);
|
|
7708
|
+
out(
|
|
7709
|
+
` never received it and cannot recover it. Move it into your secret manager before deploying.`
|
|
7710
|
+
);
|
|
7711
|
+
out(` ${style.grey(`Fingerprint: ${created.fingerprint}`)}`);
|
|
7712
|
+
await configureStarterPolicy(api, workspace.id, flags, options);
|
|
7713
|
+
let integrated = false;
|
|
7714
|
+
if (created.tools.length > 0 && !options.skipCode) {
|
|
7715
|
+
const outcome = await integrate(
|
|
7716
|
+
process.cwd(),
|
|
7717
|
+
created.tools.map((tool) => tool.key),
|
|
7718
|
+
{ yes: flags.yes, quiet: flags.json, api }
|
|
7719
|
+
);
|
|
7720
|
+
integrated = outcome.status === "applied" || outcome.status === "nothing-to-do";
|
|
7336
7721
|
}
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
checks.push({
|
|
7343
|
-
name: "Gateway reachable",
|
|
7344
|
-
status: response.ok ? "pass" : "fail",
|
|
7345
|
-
message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
|
|
7346
|
-
});
|
|
7347
|
-
} catch (error) {
|
|
7348
|
-
checks.push({
|
|
7349
|
-
name: "Gateway reachable",
|
|
7350
|
-
status: "fail",
|
|
7351
|
-
message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
|
|
7352
|
-
});
|
|
7722
|
+
heading("Validating the connection");
|
|
7723
|
+
const checks = await validateConnection(api, gatewayUrl, created.agentId, environment.id);
|
|
7724
|
+
for (const check of checks) {
|
|
7725
|
+
const icon = check.status === "pass" ? symbol.pass : check.status === "warn" ? symbol.warn : symbol.fail;
|
|
7726
|
+
out(`${icon} ${check.name}${check.message ? style.grey(` \u2014 ${check.message}`) : ""}`);
|
|
7353
7727
|
}
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
);
|
|
7358
|
-
const
|
|
7359
|
-
|
|
7360
|
-
|
|
7361
|
-
|
|
7362
|
-
|
|
7363
|
-
|
|
7364
|
-
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
}
|
|
7728
|
+
const failed = checks.filter((check) => check.status === "fail");
|
|
7729
|
+
if (!integrated) {
|
|
7730
|
+
heading("Protect each tool");
|
|
7731
|
+
out();
|
|
7732
|
+
for (const line of integrationSnippet(framework).split("\n")) {
|
|
7733
|
+
out(` ${style.cyan(line)}`);
|
|
7734
|
+
}
|
|
7735
|
+
const entryPoints = findEntryPoints();
|
|
7736
|
+
if (entryPoints.length > 0) {
|
|
7737
|
+
out();
|
|
7738
|
+
out(
|
|
7739
|
+
style.grey(
|
|
7740
|
+
` Likely entry point${entryPoints.length > 1 ? "s" : ""}: ${entryPoints.join(", ")}`
|
|
7741
|
+
)
|
|
7742
|
+
);
|
|
7743
|
+
}
|
|
7744
|
+
if (created.tools.length > 0) {
|
|
7745
|
+
out(style.grey(" Or let Veefore wrap your tools for you: `veefore integrate`."));
|
|
7746
|
+
}
|
|
7370
7747
|
}
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7748
|
+
heading("When a human must approve");
|
|
7749
|
+
for (const line of approvalModeExplanation(approvalMode)) out(line);
|
|
7750
|
+
heading("Next");
|
|
7751
|
+
out(` ${symbol.bullet} ${style.bold("veefore dev")} watch decisions live while you build`);
|
|
7752
|
+
out(
|
|
7753
|
+
` ${symbol.bullet} ${style.bold("veefore doctor")} check the security posture of this setup`
|
|
7754
|
+
);
|
|
7755
|
+
out(` ${symbol.bullet} ${style.bold("veefore status")} see the current mode and agent health`);
|
|
7756
|
+
out();
|
|
7757
|
+
out(
|
|
7758
|
+
style.grey(
|
|
7759
|
+
` This environment is in ${environment.gatewayMode ?? "OBSERVE"} mode. Nothing is blocked yet.`
|
|
7760
|
+
)
|
|
7761
|
+
);
|
|
7762
|
+
out(style.grey(" Review the recommended policy, simulate it, then switch to ENFORCE."));
|
|
7763
|
+
out();
|
|
7764
|
+
if (flags.json) {
|
|
7765
|
+
out(
|
|
7766
|
+
JSON.stringify(
|
|
7767
|
+
{
|
|
7768
|
+
config_file: configFile,
|
|
7769
|
+
env_file: envFile,
|
|
7770
|
+
agent_id: created.agentId,
|
|
7771
|
+
key_id: created.keyId,
|
|
7772
|
+
fingerprint: created.fingerprint,
|
|
7773
|
+
tools: created.tools,
|
|
7774
|
+
checks
|
|
7775
|
+
},
|
|
7776
|
+
null,
|
|
7777
|
+
2
|
|
7778
|
+
)
|
|
7374
7779
|
);
|
|
7375
|
-
checks.push({
|
|
7376
|
-
name: "Agent passport issued",
|
|
7377
|
-
status: passport ? "pass" : "warn",
|
|
7378
|
-
message: passport ? `version ${passport.version}` : "not issued yet"
|
|
7379
|
-
});
|
|
7380
|
-
} catch {
|
|
7381
|
-
checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
|
|
7382
7780
|
}
|
|
7781
|
+
return failed.length > 0 ? 1 : 0;
|
|
7782
|
+
}
|
|
7783
|
+
function previousApprovalMode(dir = process.cwd()) {
|
|
7383
7784
|
try {
|
|
7384
|
-
const
|
|
7385
|
-
|
|
7386
|
-
|
|
7387
|
-
checks.push({
|
|
7388
|
-
name: "Enforcement mode",
|
|
7389
|
-
status: "pass",
|
|
7390
|
-
message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
|
|
7391
|
-
});
|
|
7785
|
+
const text = readFileSync9(path10.join(dir, ".env.local"), "utf8");
|
|
7786
|
+
const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
|
|
7787
|
+
return value === "wait" || value === "throw" ? value : null;
|
|
7392
7788
|
} catch {
|
|
7393
|
-
|
|
7394
|
-
}
|
|
7395
|
-
return checks;
|
|
7396
|
-
}
|
|
7397
|
-
function parseInitArgs(args) {
|
|
7398
|
-
const options = { force: false, skipPolicy: false, skipCode: false };
|
|
7399
|
-
for (let index = 0; index < args.length; index += 1) {
|
|
7400
|
-
const arg = args[index];
|
|
7401
|
-
const next = () => {
|
|
7402
|
-
const value = args[index + 1];
|
|
7403
|
-
if (value === void 0 || value.startsWith("-")) {
|
|
7404
|
-
throw new CliError(`Option ${String(arg)} requires a value.`);
|
|
7405
|
-
}
|
|
7406
|
-
index += 1;
|
|
7407
|
-
return value;
|
|
7408
|
-
};
|
|
7409
|
-
switch (arg) {
|
|
7410
|
-
case "--force":
|
|
7411
|
-
options.force = true;
|
|
7412
|
-
break;
|
|
7413
|
-
case "--skip-policy":
|
|
7414
|
-
options.skipPolicy = true;
|
|
7415
|
-
break;
|
|
7416
|
-
case "--skip-code":
|
|
7417
|
-
options.skipCode = true;
|
|
7418
|
-
break;
|
|
7419
|
-
case "--new-agent":
|
|
7420
|
-
options.newAgent = true;
|
|
7421
|
-
break;
|
|
7422
|
-
case "--control-plane":
|
|
7423
|
-
options.controlPlaneUrl = next();
|
|
7424
|
-
break;
|
|
7425
|
-
case "--gateway":
|
|
7426
|
-
options.gatewayUrl = next();
|
|
7427
|
-
break;
|
|
7428
|
-
case "--organization":
|
|
7429
|
-
options.organizationId = next();
|
|
7430
|
-
break;
|
|
7431
|
-
case "--workspace":
|
|
7432
|
-
options.workspaceId = next();
|
|
7433
|
-
break;
|
|
7434
|
-
case "--environment":
|
|
7435
|
-
options.environmentId = next();
|
|
7436
|
-
break;
|
|
7437
|
-
case "--name":
|
|
7438
|
-
options.agentName = next();
|
|
7439
|
-
break;
|
|
7440
|
-
case "--capability":
|
|
7441
|
-
options.capabilities = [...options.capabilities ?? [], next()];
|
|
7442
|
-
break;
|
|
7443
|
-
case "--approval-mode": {
|
|
7444
|
-
const value = next().toLowerCase();
|
|
7445
|
-
if (value !== "wait" && value !== "throw") {
|
|
7446
|
-
throw new CliError('--approval-mode must be "wait" or "throw".');
|
|
7447
|
-
}
|
|
7448
|
-
options.approvalMode = value;
|
|
7449
|
-
break;
|
|
7450
|
-
}
|
|
7451
|
-
default:
|
|
7452
|
-
if (arg?.startsWith("-")) {
|
|
7453
|
-
throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
|
|
7454
|
-
}
|
|
7455
|
-
}
|
|
7789
|
+
return null;
|
|
7456
7790
|
}
|
|
7457
|
-
return options;
|
|
7458
7791
|
}
|
|
7459
|
-
|
|
7460
|
-
|
|
7461
|
-
|
|
7462
|
-
|
|
7463
|
-
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
}
|
|
7474
|
-
|
|
7475
|
-
|
|
7476
|
-
|
|
7477
|
-
|
|
7478
|
-
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7487
|
-
|
|
7488
|
-
{
|
|
7489
|
-
label: "Set it up again with a new agent (the guarded code keeps working)",
|
|
7490
|
-
run: () => initCommand(["--force"], flags)
|
|
7491
|
-
}
|
|
7492
|
-
);
|
|
7493
|
-
} else {
|
|
7494
|
-
if (state.kind === "signed-out" || state.kind === "session-ended") {
|
|
7495
|
-
choices.push({
|
|
7496
|
-
label: "Sign in",
|
|
7497
|
-
run: async () => {
|
|
7498
|
-
await login(existing.controlPlaneUrl);
|
|
7499
|
-
return 0;
|
|
7500
|
-
}
|
|
7501
|
-
});
|
|
7792
|
+
function approvalModeExplanation(mode) {
|
|
7793
|
+
if (mode === "wait") {
|
|
7794
|
+
return [
|
|
7795
|
+
` Guarded tools ${style.bold("wait")} for the reviewer's decision, then:`,
|
|
7796
|
+
` ${symbol.bullet} approved: the tool runs once and returns its real result`,
|
|
7797
|
+
` ${symbol.bullet} denied, cancelled or expired: it fails with that reason, and nothing runs`,
|
|
7798
|
+
style.grey(
|
|
7799
|
+
" The agent process must stay running while it waits (up to the approval's expiry)."
|
|
7800
|
+
),
|
|
7801
|
+
style.grey(
|
|
7802
|
+
" For chat agents that should hand the question back instead, set VEEFORE_APPROVAL_MODE=throw"
|
|
7803
|
+
),
|
|
7804
|
+
style.grey(" in .env.local, or pass { waitForApproval: false } to a single guard().")
|
|
7805
|
+
];
|
|
7806
|
+
}
|
|
7807
|
+
return [
|
|
7808
|
+
` Guarded tools ${style.bold("return immediately")} with "approval required" (VeeforeApprovalRequiredError),`,
|
|
7809
|
+
" carrying the approval id. Your agent must resubmit the same call after a reviewer approves,",
|
|
7810
|
+
" or the action never happens.",
|
|
7811
|
+
style.grey(" To wait for the decision instead, set VEEFORE_APPROVAL_MODE=wait in .env.local.")
|
|
7812
|
+
];
|
|
7813
|
+
}
|
|
7814
|
+
async function selectOrganization(api, me, options, flags) {
|
|
7815
|
+
const organizations = me.organizations.length > 0 ? me.organizations : [
|
|
7816
|
+
{
|
|
7817
|
+
id: me.organization.id,
|
|
7818
|
+
name: me.organization.name,
|
|
7819
|
+
slug: me.organization.slug,
|
|
7820
|
+
roleKey: me.roleKey
|
|
7502
7821
|
}
|
|
7503
|
-
|
|
7504
|
-
|
|
7505
|
-
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
|
|
7509
|
-
|
|
7510
|
-
|
|
7511
|
-
label: "Remove Veefore from this project",
|
|
7512
|
-
run: () => removeCommand([], flags, state)
|
|
7513
|
-
}
|
|
7514
|
-
);
|
|
7515
|
-
if (state.kind === "ok" || state.kind === "unreachable") {
|
|
7516
|
-
choices.push({
|
|
7517
|
-
label: "Sign out",
|
|
7518
|
-
run: async () => {
|
|
7519
|
-
await logout(existing.controlPlaneUrl);
|
|
7520
|
-
return 0;
|
|
7521
|
-
}
|
|
7522
|
-
});
|
|
7822
|
+
];
|
|
7823
|
+
if (options.organizationId) {
|
|
7824
|
+
const match = organizations.find((org) => org.id === options.organizationId);
|
|
7825
|
+
if (!match) {
|
|
7826
|
+
throw new CliError(
|
|
7827
|
+
`You are not a member of organization ${options.organizationId}.`,
|
|
7828
|
+
`Available: ${organizations.map((org) => org.id).join(", ")}`
|
|
7829
|
+
);
|
|
7523
7830
|
}
|
|
7831
|
+
return match;
|
|
7832
|
+
}
|
|
7833
|
+
if (organizations.length === 1 || flags.yes) {
|
|
7834
|
+
const chosen2 = organizations[0];
|
|
7835
|
+
if (!chosen2) throw new CliError("Your account is not a member of any organization.");
|
|
7836
|
+
out(`${symbol.pass} Organization: ${style.bold(chosen2.name)}`);
|
|
7837
|
+
return chosen2;
|
|
7524
7838
|
}
|
|
7525
|
-
choices.push({ label: "Exit", run: async () => 0 });
|
|
7526
7839
|
out();
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7840
|
+
const chosen = await select(
|
|
7841
|
+
"Which organization?",
|
|
7842
|
+
organizations.map((org) => ({
|
|
7843
|
+
label: org.name,
|
|
7844
|
+
hint: `${org.roleKey} \xB7 ${org.id}`,
|
|
7845
|
+
value: org
|
|
7846
|
+
})),
|
|
7847
|
+
{ quiet: true }
|
|
7531
7848
|
);
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
return 0;
|
|
7536
|
-
}
|
|
7537
|
-
return picked.run();
|
|
7849
|
+
await api.post("/v1/auth/scope", { organization_id: chosen.id });
|
|
7850
|
+
out(`${symbol.pass} Organization: ${style.bold(chosen.name)}`);
|
|
7851
|
+
return chosen;
|
|
7538
7852
|
}
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7544
|
-
|
|
7545
|
-
|
|
7853
|
+
async function selectWorkspace(api, me, options, flags) {
|
|
7854
|
+
const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
|
|
7855
|
+
if (workspaces.length === 0) {
|
|
7856
|
+
throw new CliError(
|
|
7857
|
+
"This organization has no workspaces.",
|
|
7858
|
+
"Create one in the console, or ask an admin to."
|
|
7859
|
+
);
|
|
7546
7860
|
}
|
|
7547
|
-
|
|
7548
|
-
|
|
7549
|
-
|
|
7550
|
-
|
|
7551
|
-
printScan(scan);
|
|
7552
|
-
out();
|
|
7553
|
-
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
7554
|
-
return 0;
|
|
7555
|
-
}
|
|
7556
|
-
|
|
7557
|
-
// src/commands/logs.ts
|
|
7558
|
-
async function logsCommand(args, flags) {
|
|
7559
|
-
const context = await resolveContext(flags);
|
|
7560
|
-
const follow = args.includes("--follow") || args.includes("-f");
|
|
7561
|
-
const limit = Number(valueFor(args, "--limit") ?? 25);
|
|
7562
|
-
const decisionFilter = valueFor(args, "--decision");
|
|
7563
|
-
const agentFilter = valueFor(args, "--agent") ?? (args.includes("--all") ? void 0 : context.config.agentId);
|
|
7564
|
-
const initial = await fetchActions(context, {
|
|
7565
|
-
limit,
|
|
7566
|
-
decision: decisionFilter,
|
|
7567
|
-
agentId: agentFilter
|
|
7568
|
-
});
|
|
7569
|
-
if (flags.json && !follow) {
|
|
7570
|
-
printJson(initial);
|
|
7571
|
-
return 0;
|
|
7861
|
+
if (options.workspaceId) {
|
|
7862
|
+
const match = workspaces.find((workspace) => workspace.id === options.workspaceId);
|
|
7863
|
+
if (!match) throw new CliError(`Workspace ${options.workspaceId} was not found.`);
|
|
7864
|
+
return match;
|
|
7572
7865
|
}
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
out(style.grey(" Use --follow to stream new decisions as they happen."));
|
|
7578
|
-
out();
|
|
7579
|
-
return 0;
|
|
7866
|
+
if (workspaces.length === 1 || flags.yes) {
|
|
7867
|
+
const chosen2 = workspaces[0];
|
|
7868
|
+
out(`${symbol.pass} Workspace: ${style.bold(chosen2.name)}`);
|
|
7869
|
+
return chosen2;
|
|
7580
7870
|
}
|
|
7581
|
-
return streamActions(context, {
|
|
7582
|
-
decision: decisionFilter,
|
|
7583
|
-
agentId: agentFilter,
|
|
7584
|
-
seen: new Set(initial.map((action) => action.id)),
|
|
7585
|
-
verbose: flags.verbose,
|
|
7586
|
-
json: flags.json
|
|
7587
|
-
});
|
|
7588
|
-
}
|
|
7589
|
-
async function devCommand(args, flags) {
|
|
7590
|
-
const context = await resolveContext(flags);
|
|
7591
|
-
const environment = await context.api.get(`/v1/environments/${context.config.environmentId}`);
|
|
7592
|
-
const gateway = await probeGateway(context.config.gatewayUrl);
|
|
7593
|
-
const agent = await context.api.get(
|
|
7594
|
-
`/v1/agents/${context.config.agentId}`
|
|
7595
|
-
);
|
|
7596
|
-
heading(`veefore dev \u2014 ${context.config.organizationName} \u203A ${environment.name}`);
|
|
7597
7871
|
out();
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7872
|
+
const chosen = await select(
|
|
7873
|
+
"Which workspace?",
|
|
7874
|
+
workspaces.map((workspace) => ({
|
|
7875
|
+
label: workspace.name,
|
|
7876
|
+
hint: workspace.id,
|
|
7877
|
+
value: workspace
|
|
7878
|
+
})),
|
|
7879
|
+
{ quiet: true }
|
|
7603
7880
|
);
|
|
7604
|
-
out(
|
|
7605
|
-
|
|
7606
|
-
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7881
|
+
out(`${symbol.pass} Workspace: ${style.bold(chosen.name)}`);
|
|
7882
|
+
return chosen;
|
|
7883
|
+
}
|
|
7884
|
+
async function selectEnvironment(api, me, workspaceId, options, flags) {
|
|
7885
|
+
const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
|
|
7886
|
+
const environments = all.filter((environment) => environment.workspaceId === workspaceId);
|
|
7887
|
+
if (environments.length === 0) {
|
|
7888
|
+
throw new CliError(
|
|
7889
|
+
"That workspace has no environments.",
|
|
7890
|
+
"Create one in the console, or ask an admin to."
|
|
7611
7891
|
);
|
|
7612
|
-
out(style.grey(" Start it with `npm run dev:gateway`, then re-run `veefore dev`."));
|
|
7613
|
-
out();
|
|
7614
|
-
return 1;
|
|
7615
7892
|
}
|
|
7616
|
-
if (
|
|
7617
|
-
|
|
7618
|
-
|
|
7619
|
-
|
|
7620
|
-
);
|
|
7621
|
-
out(style.grey(" This is the safe way to learn what your agent actually does."));
|
|
7893
|
+
if (options.environmentId) {
|
|
7894
|
+
const match = environments.find((environment) => environment.id === options.environmentId);
|
|
7895
|
+
if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
|
|
7896
|
+
return match;
|
|
7622
7897
|
}
|
|
7623
|
-
if (
|
|
7624
|
-
|
|
7625
|
-
out(
|
|
7898
|
+
if (flags.yes) {
|
|
7899
|
+
const chosen2 = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
|
|
7900
|
+
out(`${symbol.pass} Environment: ${style.bold(chosen2.name)} (${chosen2.type})`);
|
|
7901
|
+
return chosen2;
|
|
7626
7902
|
}
|
|
7627
7903
|
out();
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
}
|
|
7639
|
-
return
|
|
7640
|
-
agentId: args.includes("--all") ? void 0 : context.config.agentId,
|
|
7641
|
-
seen: new Set(existing.map((action) => action.id)),
|
|
7642
|
-
verbose: true,
|
|
7643
|
-
json: false,
|
|
7644
|
-
coach: true
|
|
7645
|
-
});
|
|
7904
|
+
const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
|
|
7905
|
+
const chosen = await select(
|
|
7906
|
+
"Which environment?",
|
|
7907
|
+
environments.map((environment) => ({
|
|
7908
|
+
label: environment.name,
|
|
7909
|
+
hint: `${environment.type} \xB7 ${environment.gatewayMode ?? "\u2014"} \xB7 ${environment.id}`,
|
|
7910
|
+
value: environment
|
|
7911
|
+
})),
|
|
7912
|
+
{ quiet: true, initial: development >= 0 ? development : 0 }
|
|
7913
|
+
);
|
|
7914
|
+
out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
|
|
7915
|
+
return chosen;
|
|
7646
7916
|
}
|
|
7647
|
-
async function
|
|
7648
|
-
|
|
7649
|
-
|
|
7650
|
-
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
|
|
7654
|
-
|
|
7917
|
+
async function discoverAudience(gatewayUrl) {
|
|
7918
|
+
try {
|
|
7919
|
+
const response = await fetch(`${gatewayUrl}/.well-known/veefore-platform-key`, {
|
|
7920
|
+
headers: { accept: "application/json" }
|
|
7921
|
+
});
|
|
7922
|
+
if (!response.ok) return gatewayUrl;
|
|
7923
|
+
const body = await response.json();
|
|
7924
|
+
const audience = body.data?.audience;
|
|
7925
|
+
return typeof audience === "string" && audience !== "" ? audience : gatewayUrl;
|
|
7926
|
+
} catch {
|
|
7927
|
+
return gatewayUrl;
|
|
7928
|
+
}
|
|
7655
7929
|
}
|
|
7656
|
-
function
|
|
7657
|
-
if (
|
|
7658
|
-
out(
|
|
7930
|
+
async function configureStarterPolicy(api, workspaceId, flags, options) {
|
|
7931
|
+
if (options.skipPolicy) {
|
|
7932
|
+
out(`${symbol.info} Skipped starter policy (--skip-policy).`);
|
|
7659
7933
|
return;
|
|
7660
7934
|
}
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
|
|
7676
|
-
|
|
7935
|
+
const existing = await api.get(
|
|
7936
|
+
"/v1/policies",
|
|
7937
|
+
{ workspace_id: workspaceId }
|
|
7938
|
+
);
|
|
7939
|
+
const count = Array.isArray(existing) ? existing.length : existing.items?.length ?? 0;
|
|
7940
|
+
if (count > 0) {
|
|
7941
|
+
out(`${symbol.pass} Workspace already has ${count} policy/policies. Leaving them alone.`);
|
|
7942
|
+
return;
|
|
7943
|
+
}
|
|
7944
|
+
const create = flags.yes ? true : await confirm("Create a starter policy with safe defaults?", { yes: flags.yes });
|
|
7945
|
+
if (!create) {
|
|
7946
|
+
out(style.grey(" Skipped. Create one later from a template in the console."));
|
|
7947
|
+
return;
|
|
7948
|
+
}
|
|
7949
|
+
const policySpinner = spinner("Creating starter policy from the safe-defaults template");
|
|
7950
|
+
try {
|
|
7951
|
+
const result = await api.post("/v1/policies", {
|
|
7952
|
+
workspace_id: workspaceId,
|
|
7953
|
+
name: "Starter \u2014 safe defaults",
|
|
7954
|
+
template_key: "starter-observe-safe-defaults"
|
|
7955
|
+
});
|
|
7956
|
+
await api.post(`/v1/policies/${result.policy.id}/bindings`, {
|
|
7957
|
+
scope: "WORKSPACE",
|
|
7958
|
+
scope_id: workspaceId
|
|
7959
|
+
});
|
|
7960
|
+
policySpinner.succeed(
|
|
7961
|
+
`Starter policy created as ${style.bold("DRAFT")} and bound to the workspace`
|
|
7677
7962
|
);
|
|
7678
|
-
|
|
7963
|
+
keyValue("Policy", `${result.policy.name} (${result.policy.id})`);
|
|
7964
|
+
out(
|
|
7965
|
+
style.grey(
|
|
7966
|
+
" Review it, run `veefore policies test`, then `veefore policies deploy` when you are ready."
|
|
7967
|
+
)
|
|
7968
|
+
);
|
|
7969
|
+
} catch (error) {
|
|
7970
|
+
policySpinner.fail("Could not create the starter policy");
|
|
7971
|
+
if (error instanceof CliApiError) {
|
|
7972
|
+
out(style.grey(` ${error.message}`));
|
|
7973
|
+
out(style.grey(" You can create one from a template in the console instead."));
|
|
7974
|
+
return;
|
|
7975
|
+
}
|
|
7976
|
+
throw error;
|
|
7679
7977
|
}
|
|
7680
|
-
for (const action of actions) renderAction(action);
|
|
7681
|
-
}
|
|
7682
|
-
function effectiveDecision(action) {
|
|
7683
|
-
return action.shadowDecision ?? action.decision ?? "UNKNOWN";
|
|
7684
7978
|
}
|
|
7685
|
-
function
|
|
7686
|
-
const
|
|
7687
|
-
|
|
7688
|
-
|
|
7689
|
-
|
|
7690
|
-
|
|
7691
|
-
|
|
7692
|
-
|
|
7693
|
-
|
|
7694
|
-
|
|
7695
|
-
|
|
7696
|
-
facts.push(`${action.matchedRuleIds.length} rule(s) matched`);
|
|
7697
|
-
if (action.riskLevel) facts.push(`risk ${action.riskLevel}`);
|
|
7698
|
-
if (action.protocol !== "HTTP") facts.push(action.protocol);
|
|
7699
|
-
if (action.gatewayMode !== "ENFORCE") facts.push(`mode ${action.gatewayMode}`);
|
|
7700
|
-
facts.push(`${Math.round(action.latencyMs)}ms`);
|
|
7701
|
-
out(` ${style.grey(facts.join(" \xB7 "))}`);
|
|
7702
|
-
if (coach) {
|
|
7703
|
-
const hint = coachingFor(action);
|
|
7704
|
-
if (hint) out(` ${style.cyan("\u2192")} ${style.grey(hint)}`);
|
|
7979
|
+
async function validateConnection(api, gatewayUrl, agentId, environmentId) {
|
|
7980
|
+
const checks = [];
|
|
7981
|
+
try {
|
|
7982
|
+
await api.get("/v1/auth/session");
|
|
7983
|
+
checks.push({ name: "Control plane reachable", status: "pass", message: api.controlPlaneUrl });
|
|
7984
|
+
} catch (error) {
|
|
7985
|
+
checks.push({
|
|
7986
|
+
name: "Control plane reachable",
|
|
7987
|
+
status: "fail",
|
|
7988
|
+
message: error instanceof Error ? error.message : String(error)
|
|
7989
|
+
});
|
|
7705
7990
|
}
|
|
7706
|
-
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7991
|
+
try {
|
|
7992
|
+
const response = await fetch(`${gatewayUrl}/health`, {
|
|
7993
|
+
signal: AbortSignal.timeout(5e3)
|
|
7994
|
+
});
|
|
7995
|
+
const body = await response.json();
|
|
7996
|
+
checks.push({
|
|
7997
|
+
name: "Gateway reachable",
|
|
7998
|
+
status: response.ok ? "pass" : "fail",
|
|
7999
|
+
message: `${gatewayUrl} \u2014 ${body.status ?? response.status} (${body.enforcement_posture ?? "unknown posture"})`
|
|
8000
|
+
});
|
|
8001
|
+
} catch (error) {
|
|
8002
|
+
checks.push({
|
|
8003
|
+
name: "Gateway reachable",
|
|
8004
|
+
status: "fail",
|
|
8005
|
+
message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
|
|
8006
|
+
});
|
|
7711
8007
|
}
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
8008
|
+
try {
|
|
8009
|
+
const identities = await api.get(
|
|
8010
|
+
`/v1/agents/${agentId}/identities`
|
|
8011
|
+
);
|
|
8012
|
+
const active = identities.find((identity) => identity.status === "ACTIVE");
|
|
8013
|
+
checks.push({
|
|
8014
|
+
name: "Agent identity active",
|
|
8015
|
+
status: active ? "pass" : "fail",
|
|
8016
|
+
message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
|
|
8017
|
+
});
|
|
8018
|
+
} catch (error) {
|
|
8019
|
+
checks.push({
|
|
8020
|
+
name: "Agent identity active",
|
|
8021
|
+
status: "warn",
|
|
8022
|
+
message: error instanceof Error ? error.message : String(error)
|
|
8023
|
+
});
|
|
8024
|
+
}
|
|
8025
|
+
try {
|
|
8026
|
+
const passport = await api.get(
|
|
8027
|
+
`/v1/passports/${agentId}`
|
|
8028
|
+
);
|
|
8029
|
+
checks.push({
|
|
8030
|
+
name: "Agent passport issued",
|
|
8031
|
+
status: passport ? "pass" : "warn",
|
|
8032
|
+
message: passport ? `version ${passport.version}` : "not issued yet"
|
|
8033
|
+
});
|
|
8034
|
+
} catch {
|
|
8035
|
+
checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
|
|
8036
|
+
}
|
|
8037
|
+
try {
|
|
8038
|
+
const environment = await api.get(
|
|
8039
|
+
`/v1/environments/${environmentId}`
|
|
8040
|
+
);
|
|
8041
|
+
checks.push({
|
|
8042
|
+
name: "Enforcement mode",
|
|
8043
|
+
status: "pass",
|
|
8044
|
+
message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
|
|
8045
|
+
});
|
|
8046
|
+
} catch {
|
|
8047
|
+
checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
|
|
7725
8048
|
}
|
|
8049
|
+
return checks;
|
|
7726
8050
|
}
|
|
7727
|
-
function
|
|
7728
|
-
const
|
|
7729
|
-
|
|
7730
|
-
|
|
7731
|
-
|
|
7732
|
-
|
|
8051
|
+
function parseInitArgs(args) {
|
|
8052
|
+
const options = { force: false, skipPolicy: false, skipCode: false };
|
|
8053
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
8054
|
+
const arg = args[index];
|
|
8055
|
+
const next = () => {
|
|
8056
|
+
const value = args[index + 1];
|
|
8057
|
+
if (value === void 0 || value.startsWith("-")) {
|
|
8058
|
+
throw new CliError(`Option ${String(arg)} requires a value.`);
|
|
8059
|
+
}
|
|
8060
|
+
index += 1;
|
|
8061
|
+
return value;
|
|
8062
|
+
};
|
|
8063
|
+
switch (arg) {
|
|
8064
|
+
case "--force":
|
|
8065
|
+
options.force = true;
|
|
8066
|
+
break;
|
|
8067
|
+
case "--skip-policy":
|
|
8068
|
+
options.skipPolicy = true;
|
|
8069
|
+
break;
|
|
8070
|
+
case "--skip-code":
|
|
8071
|
+
options.skipCode = true;
|
|
8072
|
+
break;
|
|
8073
|
+
case "--new-agent":
|
|
8074
|
+
options.newAgent = true;
|
|
8075
|
+
break;
|
|
8076
|
+
case "--control-plane":
|
|
8077
|
+
options.controlPlaneUrl = next();
|
|
8078
|
+
break;
|
|
8079
|
+
case "--gateway":
|
|
8080
|
+
options.gatewayUrl = next();
|
|
8081
|
+
break;
|
|
8082
|
+
case "--organization":
|
|
8083
|
+
options.organizationId = next();
|
|
8084
|
+
break;
|
|
8085
|
+
case "--workspace":
|
|
8086
|
+
options.workspaceId = next();
|
|
8087
|
+
break;
|
|
8088
|
+
case "--environment":
|
|
8089
|
+
options.environmentId = next();
|
|
8090
|
+
break;
|
|
8091
|
+
case "--name":
|
|
8092
|
+
options.agentName = next();
|
|
8093
|
+
break;
|
|
8094
|
+
case "--capability":
|
|
8095
|
+
options.capabilities = [...options.capabilities ?? [], next()];
|
|
8096
|
+
break;
|
|
8097
|
+
case "--approval-mode": {
|
|
8098
|
+
const value = next().toLowerCase();
|
|
8099
|
+
if (value !== "wait" && value !== "throw") {
|
|
8100
|
+
throw new CliError('--approval-mode must be "wait" or "throw".');
|
|
8101
|
+
}
|
|
8102
|
+
options.approvalMode = value;
|
|
8103
|
+
break;
|
|
8104
|
+
}
|
|
8105
|
+
default:
|
|
8106
|
+
if (arg?.startsWith("-")) {
|
|
8107
|
+
throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
|
|
8108
|
+
}
|
|
8109
|
+
}
|
|
8110
|
+
}
|
|
8111
|
+
return options;
|
|
7733
8112
|
}
|
|
7734
|
-
async function
|
|
7735
|
-
let
|
|
7736
|
-
let
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
|
|
7742
|
-
|
|
7743
|
-
|
|
7744
|
-
|
|
7745
|
-
|
|
7746
|
-
let
|
|
8113
|
+
async function projectHome(existing, flags) {
|
|
8114
|
+
let last = 0;
|
|
8115
|
+
for (let round = 0; ; round += 1) {
|
|
8116
|
+
const state = await agentState(existing);
|
|
8117
|
+
if (round > 0) {
|
|
8118
|
+
out();
|
|
8119
|
+
out(style.grey("\u2500".repeat(Math.min(process.stdout.columns ?? 72, 72))));
|
|
8120
|
+
}
|
|
8121
|
+
printProjectSummary(existing, state);
|
|
8122
|
+
const choices = homeChoices(existing, state, flags);
|
|
8123
|
+
choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
|
|
8124
|
+
out();
|
|
8125
|
+
let picked;
|
|
7747
8126
|
try {
|
|
7748
|
-
|
|
7749
|
-
|
|
7750
|
-
|
|
7751
|
-
agentId: options.agentId
|
|
7752
|
-
});
|
|
7753
|
-
fresh = recent.filter((action) => !options.seen.has(action.id));
|
|
7754
|
-
for (const action of fresh) options.seen.add(action.id);
|
|
7755
|
-
} catch (error) {
|
|
7756
|
-
out(
|
|
7757
|
-
`${symbol.warn} ${style.amber("Lost contact with the control plane;")} ${style.grey(error instanceof Error ? error.message : String(error))}`
|
|
8127
|
+
picked = await select(
|
|
8128
|
+
"What would you like to do?",
|
|
8129
|
+
choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice }))
|
|
7758
8130
|
);
|
|
7759
|
-
|
|
7760
|
-
|
|
7761
|
-
|
|
8131
|
+
} catch (error) {
|
|
8132
|
+
if (error instanceof MenuCancelled) return last;
|
|
8133
|
+
throw error;
|
|
7762
8134
|
}
|
|
7763
|
-
|
|
7764
|
-
|
|
7765
|
-
|
|
7766
|
-
|
|
7767
|
-
|
|
7768
|
-
}
|
|
7769
|
-
const decision = effectiveDecision(action);
|
|
7770
|
-
if (decision === "BLOCK") blocked += 1;
|
|
7771
|
-
else if (decision === "REQUIRE_APPROVAL") approvals += 1;
|
|
7772
|
-
else allowed += 1;
|
|
8135
|
+
out();
|
|
8136
|
+
try {
|
|
8137
|
+
last = await picked.run();
|
|
8138
|
+
} catch (error) {
|
|
8139
|
+
last = error instanceof MenuCancelled ? last : reportError(error, flags);
|
|
7773
8140
|
}
|
|
7774
|
-
|
|
7775
|
-
await delay(interval);
|
|
8141
|
+
if (picked.leaves) return last;
|
|
7776
8142
|
}
|
|
7777
|
-
|
|
8143
|
+
}
|
|
8144
|
+
function printProjectSummary(existing, state) {
|
|
8145
|
+
out();
|
|
8146
|
+
out(`${style.bold(existing.agentName)} ${style.grey(`\xB7 ${existing.controlPlaneUrl}`)}`);
|
|
8147
|
+
details([
|
|
8148
|
+
["Agent", `${existing.agentName} (${existing.agentId})`],
|
|
8149
|
+
["Environment", `${existing.environmentName} (${existing.environmentType})`],
|
|
8150
|
+
[
|
|
8151
|
+
"Status",
|
|
8152
|
+
state.kind === "ok" ? state.status : state.kind === "signed-out" ? "not signed in" : state.kind === "session-ended" ? "signed out" : state.kind === "missing" ? "agent not found" : "Veefore not reachable"
|
|
8153
|
+
]
|
|
8154
|
+
]);
|
|
8155
|
+
if (state.kind === "session-ended") {
|
|
8156
|
+
out(`${symbol.warn} ${state.message}`);
|
|
8157
|
+
}
|
|
8158
|
+
}
|
|
8159
|
+
function homeChoices(existing, state, flags) {
|
|
8160
|
+
const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
|
|
8161
|
+
if (ended) {
|
|
7778
8162
|
out();
|
|
7779
8163
|
out(
|
|
7780
|
-
|
|
8164
|
+
`${symbol.warn} ${style.amber(
|
|
8165
|
+
state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
|
|
8166
|
+
)} Every tool call wrapped in guard() is now refused.`
|
|
7781
8167
|
);
|
|
7782
|
-
|
|
8168
|
+
return [
|
|
8169
|
+
{
|
|
8170
|
+
label: "Remove Veefore from this project",
|
|
8171
|
+
hint: "take guard() out of the code, disconnect",
|
|
8172
|
+
run: () => removeCommand([], flags, state),
|
|
8173
|
+
leaves: true
|
|
8174
|
+
},
|
|
8175
|
+
{
|
|
8176
|
+
label: "Set it up again with a new agent",
|
|
8177
|
+
hint: "the guarded code keeps working",
|
|
8178
|
+
run: () => initCommand(["--force"], flags),
|
|
8179
|
+
leaves: true
|
|
8180
|
+
}
|
|
8181
|
+
];
|
|
7783
8182
|
}
|
|
7784
|
-
|
|
8183
|
+
const choices = [];
|
|
8184
|
+
if (state.kind === "signed-out" || state.kind === "session-ended") {
|
|
8185
|
+
choices.push({
|
|
8186
|
+
label: "Sign in",
|
|
8187
|
+
hint: "veefore login",
|
|
8188
|
+
run: async () => {
|
|
8189
|
+
await login(existing.controlPlaneUrl);
|
|
8190
|
+
return 0;
|
|
8191
|
+
}
|
|
8192
|
+
});
|
|
8193
|
+
}
|
|
8194
|
+
choices.push(
|
|
8195
|
+
{ label: "Watch decisions live", hint: "veefore dev", run: () => devCommand([], flags) },
|
|
8196
|
+
{ label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
|
|
8197
|
+
{
|
|
8198
|
+
label: "Protect tools in the code",
|
|
8199
|
+
hint: "veefore integrate",
|
|
8200
|
+
run: () => integrateCommand([], flags)
|
|
8201
|
+
},
|
|
8202
|
+
{
|
|
8203
|
+
label: "Check every tool is guarded",
|
|
8204
|
+
hint: "veefore integrate --check",
|
|
8205
|
+
run: () => integrateCommand(["--check"], flags)
|
|
8206
|
+
},
|
|
8207
|
+
{ label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
|
|
8208
|
+
{
|
|
8209
|
+
label: "Audit the security posture",
|
|
8210
|
+
hint: "veefore doctor",
|
|
8211
|
+
run: () => doctorCommand([], flags)
|
|
8212
|
+
},
|
|
8213
|
+
{ label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
|
|
8214
|
+
{
|
|
8215
|
+
label: "Re-run setup for this project",
|
|
8216
|
+
hint: "veefore init --force",
|
|
8217
|
+
run: () => initCommand(["--force"], flags),
|
|
8218
|
+
leaves: true
|
|
8219
|
+
},
|
|
8220
|
+
{
|
|
8221
|
+
label: "Remove Veefore from this project",
|
|
8222
|
+
hint: "veefore remove",
|
|
8223
|
+
run: () => removeCommand([], flags, state),
|
|
8224
|
+
leaves: true
|
|
8225
|
+
}
|
|
8226
|
+
);
|
|
8227
|
+
if (state.kind === "ok" || state.kind === "unreachable") {
|
|
8228
|
+
choices.push({
|
|
8229
|
+
label: "Sign out",
|
|
8230
|
+
hint: "veefore logout",
|
|
8231
|
+
run: async () => {
|
|
8232
|
+
await logout(existing.controlPlaneUrl);
|
|
8233
|
+
return 0;
|
|
8234
|
+
},
|
|
8235
|
+
leaves: true
|
|
8236
|
+
});
|
|
8237
|
+
}
|
|
8238
|
+
return choices;
|
|
7785
8239
|
}
|
|
7786
|
-
|
|
7787
|
-
|
|
8240
|
+
|
|
8241
|
+
// src/commands/scan.ts
|
|
8242
|
+
async function scanCommand(_args, flags) {
|
|
8243
|
+
const scan = scanProject(process.cwd());
|
|
8244
|
+
if (flags.json) {
|
|
8245
|
+
out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
|
|
8246
|
+
return 0;
|
|
8247
|
+
}
|
|
8248
|
+
heading("Veefore scan");
|
|
8249
|
+
out(
|
|
8250
|
+
`${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
|
|
8251
|
+
);
|
|
8252
|
+
printScan(scan);
|
|
8253
|
+
out();
|
|
8254
|
+
out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
|
|
8255
|
+
return 0;
|
|
7788
8256
|
}
|
|
7789
8257
|
|
|
7790
8258
|
// src/commands/policies.ts
|
|
@@ -8172,155 +8640,6 @@ async function diffPolicy(args, flags) {
|
|
|
8172
8640
|
return 0;
|
|
8173
8641
|
}
|
|
8174
8642
|
|
|
8175
|
-
// src/commands/test.ts
|
|
8176
|
-
async function testCommand(args, flags) {
|
|
8177
|
-
const context = await resolveContext(flags);
|
|
8178
|
-
const toolFilter = valueFor(args, "--tool");
|
|
8179
|
-
heading(
|
|
8180
|
-
`Enforcement test \u2014 ${context.config.environmentName} (${context.config.environmentType})`
|
|
8181
|
-
);
|
|
8182
|
-
const results = [];
|
|
8183
|
-
const record = (check, passed, detail) => {
|
|
8184
|
-
results.push({ check, passed, detail });
|
|
8185
|
-
out(`${passed ? symbol.pass : symbol.fail} ${check} ${style.grey(detail)}`);
|
|
8186
|
-
};
|
|
8187
|
-
const gateway = await probeGateway(context.config.gatewayUrl);
|
|
8188
|
-
record(
|
|
8189
|
-
"Gateway reachable",
|
|
8190
|
-
gateway.reachable,
|
|
8191
|
-
gateway.reachable ? `${context.config.gatewayUrl} \u2014 posture ${gateway.posture ?? "unknown"}` : gateway.error ?? "unreachable"
|
|
8192
|
-
);
|
|
8193
|
-
const identities = await context.api.get(`/v1/agents/${context.config.agentId}/identities`).catch(() => []);
|
|
8194
|
-
const activeIdentity = identities.find((identity) => identity.status === "ACTIVE");
|
|
8195
|
-
record(
|
|
8196
|
-
"Agent identity active",
|
|
8197
|
-
Boolean(activeIdentity),
|
|
8198
|
-
activeIdentity ? `generation ${activeIdentity.generation}, expires ${activeIdentity.expiresAt.slice(0, 10)}` : "no ACTIVE identity \u2014 the agent cannot authenticate"
|
|
8199
|
-
);
|
|
8200
|
-
const policies = await context.api.get("/v1/policies", { workspace_id: context.config.workspaceId });
|
|
8201
|
-
const effective = policies.filter(
|
|
8202
|
-
(policy) => policy.status === "ACTIVE" && policy.bindings.length > 0
|
|
8203
|
-
);
|
|
8204
|
-
record(
|
|
8205
|
-
"Active, bound policy exists",
|
|
8206
|
-
effective.length > 0,
|
|
8207
|
-
effective.length > 0 ? effective.map((policy) => policy.name).join(", ") : "no active policy is bound to a scope, so no rule can match"
|
|
8208
|
-
);
|
|
8209
|
-
let caseTotal = 0;
|
|
8210
|
-
let caseFailed = 0;
|
|
8211
|
-
for (const policy of policies) {
|
|
8212
|
-
const result = await context.api.post(`/v1/policies/${policy.id}/tests/run`, {}).catch(() => null);
|
|
8213
|
-
if (!result) continue;
|
|
8214
|
-
caseTotal += result.passed + result.failed;
|
|
8215
|
-
caseFailed += result.failed;
|
|
8216
|
-
}
|
|
8217
|
-
record(
|
|
8218
|
-
"Policy test cases",
|
|
8219
|
-
caseFailed === 0,
|
|
8220
|
-
caseTotal === 0 ? "no test cases recorded \u2014 add cases so rule changes are provable" : `${caseTotal - caseFailed}/${caseTotal} passing`
|
|
8221
|
-
);
|
|
8222
|
-
const tools = await resolveAuthorizedTools(context, toolFilter);
|
|
8223
|
-
if (tools.length === 0) {
|
|
8224
|
-
record(
|
|
8225
|
-
"Authorized tools",
|
|
8226
|
-
false,
|
|
8227
|
-
"this agent is authorized for no tools, so it cannot do anything"
|
|
8228
|
-
);
|
|
8229
|
-
} else {
|
|
8230
|
-
record("Authorized tools", true, tools.map((tool) => tool.key).join(", "));
|
|
8231
|
-
}
|
|
8232
|
-
heading("Decision probe (dry run \u2014 nothing is executed)");
|
|
8233
|
-
const probes = [];
|
|
8234
|
-
for (const tool of tools) {
|
|
8235
|
-
const probeSpinner = spinner(`Evaluating ${tool.key}`);
|
|
8236
|
-
try {
|
|
8237
|
-
const outcome = await context.api.post("/v1/policies/simulate", {
|
|
8238
|
-
agent_id: context.config.agentId,
|
|
8239
|
-
tool: tool.key,
|
|
8240
|
-
capability: tool.requiredCapability,
|
|
8241
|
-
environment_id: context.config.environmentId,
|
|
8242
|
-
parameters: {}
|
|
8243
|
-
});
|
|
8244
|
-
probeSpinner.succeed(`${tool.key} \u2192 ${outcome.decision}`);
|
|
8245
|
-
probes.push({ tool, outcome });
|
|
8246
|
-
} catch (error) {
|
|
8247
|
-
probeSpinner.fail(`${tool.key} could not be evaluated`);
|
|
8248
|
-
probes.push({
|
|
8249
|
-
tool,
|
|
8250
|
-
outcome: null,
|
|
8251
|
-
error: error instanceof Error ? error.message : String(error)
|
|
8252
|
-
});
|
|
8253
|
-
}
|
|
8254
|
-
}
|
|
8255
|
-
out();
|
|
8256
|
-
table(
|
|
8257
|
-
[
|
|
8258
|
-
{ header: "TOOL", key: "tool", width: 24 },
|
|
8259
|
-
{ header: "RISK", key: "risk", width: 18 },
|
|
8260
|
-
{ header: "DECISION", key: "decision", width: 20 },
|
|
8261
|
-
{ header: "MATCHED RULE", key: "rule", width: 32 }
|
|
8262
|
-
],
|
|
8263
|
-
probes.map((probe) => ({
|
|
8264
|
-
tool: probe.tool.dangerous ? `${probe.tool.key} ${style.red("(dangerous)")}` : probe.tool.key,
|
|
8265
|
-
risk: decisionStyle(probe.tool.riskLevel),
|
|
8266
|
-
decision: probe.outcome ? decisionStyle(probe.outcome.decision) : style.red("ERROR"),
|
|
8267
|
-
rule: probe.outcome?.matchedRules[0]?.name ?? style.grey(probe.error ?? "default decision")
|
|
8268
|
-
}))
|
|
8269
|
-
);
|
|
8270
|
-
const unguarded = probes.filter(
|
|
8271
|
-
(probe) => probe.tool.dangerous && probe.outcome?.decision === "ALLOW" && probe.outcome.matchedRules.length === 0
|
|
8272
|
-
);
|
|
8273
|
-
out();
|
|
8274
|
-
record(
|
|
8275
|
-
"Dangerous tools are guarded",
|
|
8276
|
-
unguarded.length === 0,
|
|
8277
|
-
unguarded.length === 0 ? "every dangerous tool is either refused or matched by an explicit rule" : `${unguarded.map((probe) => probe.tool.key).join(", ")} allowed by the default decision with no rule`
|
|
8278
|
-
);
|
|
8279
|
-
const errored = probes.filter((probe) => probe.outcome === null);
|
|
8280
|
-
record(
|
|
8281
|
-
"All evaluations completed",
|
|
8282
|
-
errored.length === 0,
|
|
8283
|
-
errored.length === 0 ? `${probes.length} evaluated` : `${errored.length} failed to evaluate`
|
|
8284
|
-
);
|
|
8285
|
-
const impure = probes.filter((probe) => probe.outcome && probe.outcome.sideEffectFree !== true);
|
|
8286
|
-
record(
|
|
8287
|
-
"Simulation was side-effect free",
|
|
8288
|
-
impure.length === 0,
|
|
8289
|
-
impure.length === 0 ? "no action records were written" : "the server reported side effects"
|
|
8290
|
-
);
|
|
8291
|
-
const failed = results.filter((result) => !result.passed);
|
|
8292
|
-
out();
|
|
8293
|
-
out(
|
|
8294
|
-
failed.length === 0 ? ` ${symbol.pass} ${style.green(`${results.length} check(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${failed.length} of ${results.length} check(s) failing.`)}`
|
|
8295
|
-
);
|
|
8296
|
-
if (failed.length > 0) {
|
|
8297
|
-
out();
|
|
8298
|
-
for (const failure of failed) out(` ${symbol.fail} ${failure.check}: ${failure.detail}`);
|
|
8299
|
-
}
|
|
8300
|
-
out();
|
|
8301
|
-
if (flags.json) printJson({ results, probes });
|
|
8302
|
-
return failed.length > 0 ? 1 : 0;
|
|
8303
|
-
}
|
|
8304
|
-
async function resolveAuthorizedTools(context, filter) {
|
|
8305
|
-
const tools = await context.api.get("/v1/tools", {
|
|
8306
|
-
workspace_id: context.config.workspaceId,
|
|
8307
|
-
agent_id: context.config.agentId,
|
|
8308
|
-
limit: 100
|
|
8309
|
-
});
|
|
8310
|
-
const candidates = tools.items.filter(
|
|
8311
|
-
(tool) => tool.status !== "DISABLED" && (tool.environmentId === null || tool.environmentId === context.config.environmentId)
|
|
8312
|
-
);
|
|
8313
|
-
if (!filter) return candidates;
|
|
8314
|
-
const match = candidates.filter((tool) => tool.key === filter);
|
|
8315
|
-
if (match.length === 0) {
|
|
8316
|
-
throw new CliError(
|
|
8317
|
-
`Tool "${filter}" is not available to this agent in ${context.config.environmentName}.`,
|
|
8318
|
-
`Available: ${candidates.map((tool) => tool.key).join(", ") || "none"}`
|
|
8319
|
-
);
|
|
8320
|
-
}
|
|
8321
|
-
return match;
|
|
8322
|
-
}
|
|
8323
|
-
|
|
8324
8643
|
// src/help.ts
|
|
8325
8644
|
var COMMANDS = [
|
|
8326
8645
|
{
|
|
@@ -8600,8 +8919,92 @@ function wrap2(text, width) {
|
|
|
8600
8919
|
return lines;
|
|
8601
8920
|
}
|
|
8602
8921
|
|
|
8922
|
+
// src/home.ts
|
|
8923
|
+
import { homedir as homedir2 } from "node:os";
|
|
8924
|
+
import path11 from "node:path";
|
|
8925
|
+
async function homeCommand(args, flags, version) {
|
|
8926
|
+
await showWelcome(welcomeContext(flags, version));
|
|
8927
|
+
if (readProjectConfig()) return initCommand(args, flags);
|
|
8928
|
+
out();
|
|
8929
|
+
out(style.bold("Let's get started."));
|
|
8930
|
+
for (; ; ) {
|
|
8931
|
+
out();
|
|
8932
|
+
let action;
|
|
8933
|
+
try {
|
|
8934
|
+
action = await select("What would you like to do?", [
|
|
8935
|
+
{ label: "Set up this project", hint: "recommended \xB7 veefore init", value: "setup" },
|
|
8936
|
+
{
|
|
8937
|
+
label: "Preview what setup would find",
|
|
8938
|
+
hint: "veefore scan \xB7 changes nothing",
|
|
8939
|
+
value: "scan"
|
|
8940
|
+
},
|
|
8941
|
+
{ label: "Sign in", hint: "veefore login", value: "login" },
|
|
8942
|
+
{ label: "Show every command", hint: "veefore help", value: "help" },
|
|
8943
|
+
{ label: "Exit", hint: "esc", value: "exit" }
|
|
8944
|
+
]);
|
|
8945
|
+
} catch (error) {
|
|
8946
|
+
if (error instanceof MenuCancelled) return 0;
|
|
8947
|
+
throw error;
|
|
8948
|
+
}
|
|
8949
|
+
out();
|
|
8950
|
+
try {
|
|
8951
|
+
switch (action) {
|
|
8952
|
+
case "setup":
|
|
8953
|
+
return await initCommand(args, flags);
|
|
8954
|
+
case "scan":
|
|
8955
|
+
await scanCommand([], flags);
|
|
8956
|
+
break;
|
|
8957
|
+
case "login":
|
|
8958
|
+
await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
|
|
8959
|
+
break;
|
|
8960
|
+
case "help":
|
|
8961
|
+
printRootHelp(version);
|
|
8962
|
+
break;
|
|
8963
|
+
case "exit":
|
|
8964
|
+
return 0;
|
|
8965
|
+
}
|
|
8966
|
+
} catch (error) {
|
|
8967
|
+
if (!(error instanceof MenuCancelled)) reportError(error, flags);
|
|
8968
|
+
}
|
|
8969
|
+
}
|
|
8970
|
+
}
|
|
8971
|
+
async function initWithWelcome(args, flags, version) {
|
|
8972
|
+
await showWelcome(welcomeContext(flags, version));
|
|
8973
|
+
return initCommand(args, flags);
|
|
8974
|
+
}
|
|
8975
|
+
function welcomeContext(flags, version) {
|
|
8976
|
+
const project = readProjectConfig();
|
|
8977
|
+
const controlPlaneUrl = project?.controlPlaneUrl ?? resolveControlPlaneUrl(flags.controlPlaneUrl);
|
|
8978
|
+
const session = loadSession(controlPlaneUrl);
|
|
8979
|
+
const home = homedir2();
|
|
8980
|
+
const cwd = process.cwd();
|
|
8981
|
+
const folder = cwd === home || cwd.startsWith(`${home}${path11.sep}`) ? `~${cwd.slice(home.length)}` : cwd;
|
|
8982
|
+
return {
|
|
8983
|
+
version,
|
|
8984
|
+
facts: [
|
|
8985
|
+
["Folder", shorten(folder, 34)],
|
|
8986
|
+
["Signed in", session ? session.email : style.grey("not yet")],
|
|
8987
|
+
[
|
|
8988
|
+
"Project",
|
|
8989
|
+
project ? `${project.agentName} \xB7 ${project.environmentName}` : style.grey("not set up here")
|
|
8990
|
+
],
|
|
8991
|
+
["Veefore", hostOf(controlPlaneUrl)]
|
|
8992
|
+
]
|
|
8993
|
+
};
|
|
8994
|
+
}
|
|
8995
|
+
function hostOf(url) {
|
|
8996
|
+
try {
|
|
8997
|
+
return new URL(url).host;
|
|
8998
|
+
} catch {
|
|
8999
|
+
return url;
|
|
9000
|
+
}
|
|
9001
|
+
}
|
|
9002
|
+
function shorten(text, width) {
|
|
9003
|
+
return text.length <= width ? text : `\u2026${text.slice(text.length - width + 1)}`;
|
|
9004
|
+
}
|
|
9005
|
+
|
|
8603
9006
|
// src/index.ts
|
|
8604
|
-
var CLI_VERSION = "1.
|
|
9007
|
+
var CLI_VERSION = "1.1.0";
|
|
8605
9008
|
async function run(argv) {
|
|
8606
9009
|
const { flags, rest } = parseGlobalFlags(argv);
|
|
8607
9010
|
if (rest.includes("--version") || rest.includes("-V")) {
|
|
@@ -8612,7 +9015,7 @@ async function run(argv) {
|
|
|
8612
9015
|
const wantsHelp = rest.includes("--help") || rest.includes("-h");
|
|
8613
9016
|
if (command === void 0 && !wantsHelp && process.stdin.isTTY && process.stdout.isTTY) {
|
|
8614
9017
|
try {
|
|
8615
|
-
return await initCommand(args, flags);
|
|
9018
|
+
return flags.json ? await initCommand(args, flags) : await homeCommand(args, flags, CLI_VERSION);
|
|
8616
9019
|
} catch (error) {
|
|
8617
9020
|
return handleError(error, flags);
|
|
8618
9021
|
}
|
|
@@ -8632,7 +9035,7 @@ async function run(argv) {
|
|
|
8632
9035
|
switch (command) {
|
|
8633
9036
|
case "init":
|
|
8634
9037
|
case "setup":
|
|
8635
|
-
return await initCommand(args, flags);
|
|
9038
|
+
return flags.json || !isInteractive() ? await initCommand(args, flags) : await initWithWelcome(args, flags, CLI_VERSION);
|
|
8636
9039
|
case "scan":
|
|
8637
9040
|
return await scanCommand(args, flags);
|
|
8638
9041
|
case "integrate":
|
|
@@ -8690,28 +9093,8 @@ async function run(argv) {
|
|
|
8690
9093
|
}
|
|
8691
9094
|
}
|
|
8692
9095
|
function handleError(error, flags) {
|
|
8693
|
-
if (error instanceof
|
|
8694
|
-
|
|
8695
|
-
err(`${style.red("\u2717")} ${error.message}`);
|
|
8696
|
-
if (error.hint) err(style.grey(` ${error.hint}`));
|
|
8697
|
-
err();
|
|
8698
|
-
return error.exitCode;
|
|
8699
|
-
}
|
|
8700
|
-
if (error instanceof CliApiError) {
|
|
8701
|
-
err();
|
|
8702
|
-
reportApiError(error);
|
|
8703
|
-
err();
|
|
8704
|
-
return 1;
|
|
8705
|
-
}
|
|
8706
|
-
err();
|
|
8707
|
-
err(`${style.red("\u2717")} ${error instanceof Error ? error.message : String(error)}`);
|
|
8708
|
-
if (flags.verbose && error instanceof Error && error.stack) {
|
|
8709
|
-
err(style.grey(error.stack));
|
|
8710
|
-
} else {
|
|
8711
|
-
err(style.grey(" Re-run with --verbose for the stack trace."));
|
|
8712
|
-
}
|
|
8713
|
-
err();
|
|
8714
|
-
return 1;
|
|
9096
|
+
if (error instanceof MenuCancelled) return error.exitCode;
|
|
9097
|
+
return reportError(error, flags);
|
|
8715
9098
|
}
|
|
8716
9099
|
function parseGlobalFlags(argv) {
|
|
8717
9100
|
const flags = { json: false, yes: false, verbose: false };
|