veefore 1.0.0 → 1.2.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.
Files changed (2) hide show
  1. package/dist/veefore.mjs +1405 -851
  2. 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(path11, query = {}) {
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", `${path11}${suffix}`);
245
+ return this.send("GET", `${path12}${suffix}`);
246
246
  }
247
- async post(path11, body) {
248
- return this.send("POST", path11, body);
247
+ async post(path12, body) {
248
+ return this.send("POST", path12, body);
249
249
  }
250
- async patch(path11, body) {
251
- return this.send("PATCH", path11, body);
250
+ async patch(path12, body) {
251
+ return this.send("PATCH", path12, body);
252
252
  }
253
- async delete(path11) {
254
- return this.send("DELETE", path11);
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, path11, body) {
258
- return this.fetchWithTimeout(method, path11, body);
257
+ async raw(method, path12, body) {
258
+ return this.fetchWithTimeout(method, path12, body);
259
259
  }
260
- async send(method, path11, body) {
261
- const response = await this.fetchWithTimeout(method, path11, body);
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} ${path11}.`,
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} ${path11} failed with ${response.status} ${response.statusText}.`,
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, path11, body) {
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}${path11}`, {
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,332 @@ 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, options = {}) {
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 = (options.animate ?? true) && 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
+ function clearScreen() {
708
+ if (!isInteractive()) return;
709
+ process.stdout.write("\x1B[H\x1B[2J");
710
+ }
711
+ function compactHeader(context) {
712
+ const facts = Object.fromEntries(context.facts);
713
+ const parts = [
714
+ brand(style.bold("Veefore")),
715
+ style.grey(`v${context.version}`),
716
+ ...[facts.Folder, facts["Signed in"], facts.Veefore].filter((v) => Boolean(v))
717
+ ];
718
+ out();
719
+ out(fit(` ${parts.join(style.grey(" \xB7 "))}`, columns() - 1));
720
+ out(` ${style.grey("\xB7".repeat(Math.max(10, Math.min(columns() - 2, 72))))}`);
721
+ }
722
+ async function beginScreen(source, options = {}) {
723
+ if (!isInteractive()) return;
724
+ clearScreen();
725
+ if (options.compact) compactHeader(source());
726
+ else await showWelcome(source(), { animate: options.animate ?? false });
727
+ }
728
+ async function pressBack(label = "Back") {
729
+ if (!isInteractive()) return;
730
+ const stdin = process.stdin;
731
+ const stdout = process.stdout;
732
+ out();
733
+ stdout.write(`${brand("\u2190")} ${style.bold(label)} ${style.grey("\xB7 enter or esc")}`);
734
+ await new Promise((resolve) => {
735
+ const wasRaw = stdin.isRaw;
736
+ const done = () => {
737
+ stdin.off("keypress", onKey);
738
+ stdin.setRawMode(wasRaw);
739
+ stdin.pause();
740
+ stdout.write("\r\x1B[K");
741
+ resolve();
742
+ };
743
+ const onKey = (input, key = {}) => {
744
+ if (key.name === "return" || key.name === "enter" || key.name === "escape" || key.name === "left" || key.name === "backspace" || input === "b" || key.ctrl && key.name === "c") {
745
+ done();
746
+ }
747
+ };
748
+ emitKeypressEvents(stdin);
749
+ stdin.setRawMode(true);
750
+ stdin.resume();
751
+ stdin.on("keypress", onKey);
752
+ });
753
+ }
754
+ var BACK = /* @__PURE__ */ Symbol("veefore.back");
755
+ var MenuCancelled = class extends CliError {
756
+ constructor() {
757
+ super("Cancelled.", void 0, 130);
758
+ this.name = "MenuCancelled";
759
+ }
760
+ };
761
+ async function select(question, real, settings = {}) {
762
+ const options = settings.back ? [...real, { label: settings.backLabel ?? "Back", value: BACK, hint: "\u2190" }] : real;
763
+ const enabled = options.map((option, index) => option.disabled ? -1 : index).filter((i) => i >= 0);
764
+ if (enabled.length === 0) throw new CliError(`Nothing can be chosen for "${question}".`);
765
+ const choosable = enabled.filter((index) => options[index]?.value !== BACK);
766
+ const fallback = choosable.includes(settings.initial ?? -1) ? settings.initial : choosable[0] ?? enabled[0];
767
+ if (!isInteractive()) return options[fallback].value;
768
+ let cursor = fallback;
769
+ let rendered = 0;
770
+ const stdin = process.stdin;
771
+ const stdout = process.stdout;
772
+ const labelWidth = Math.max(...options.map((option) => [...option.label].length));
773
+ const draw = () => {
774
+ const width = columns();
775
+ const lines = [];
776
+ lines.push(`${brand("?")} ${style.bold(question)}`);
777
+ if (settings.help) lines.push(` ${style.grey(settings.help)}`);
778
+ options.forEach((option, index) => {
779
+ const active = index === cursor;
780
+ const pointer = active ? brand("\u276F") : " ";
781
+ const number = index < 9 ? `${index + 1}.` : " ";
782
+ const text = option.hint ? option.label.padEnd(labelWidth) : option.label;
783
+ const hint = option.hint ? ` ${style.grey(option.hint)}` : "";
784
+ const label = option.disabled ? style.grey(`${number} ${text}`) : active ? brand(`${number} ${text}`) : `${number} ${text}`;
785
+ lines.push(fit(`${pointer} ${label}${hint}`, width - 1));
786
+ });
787
+ lines.push(
788
+ style.grey(
789
+ settings.back ? " \u2191\u2193 move \xB7 enter choose \xB7 \u2190 back \xB7 esc cancel" : " \u2191\u2193 move \xB7 enter choose \xB7 esc cancel"
790
+ )
791
+ );
792
+ if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
793
+ stdout.write(`${lines.join("\n")}
794
+ `);
795
+ rendered = lines.length;
796
+ };
797
+ const move = (direction) => {
798
+ const position = enabled.indexOf(cursor);
799
+ const next = (position + direction + enabled.length) % enabled.length;
800
+ cursor = enabled[next];
801
+ };
802
+ return new Promise((resolve, reject) => {
803
+ const wasRaw = stdin.isRaw;
804
+ const restoreCursor = () => {
805
+ stdout.write("\x1B[?25h");
806
+ };
807
+ const finish = (result) => {
808
+ stdin.off("keypress", onKey);
809
+ stdin.setRawMode(wasRaw);
810
+ stdin.pause();
811
+ process.off("exit", restoreCursor);
812
+ if (rendered > 0) stdout.write(`\x1B[${rendered}A\r\x1B[J`);
813
+ restoreCursor();
814
+ if (result) {
815
+ if (!settings.quiet && result.value !== BACK) {
816
+ stdout.write(`${style.green("\u2713")} ${question} ${brand(result.label)}
817
+ `);
818
+ }
819
+ resolve(result.value);
820
+ } else {
821
+ stdout.write(`${style.grey("\u2717")} ${question} ${style.grey("cancelled")}
822
+ `);
823
+ reject(new MenuCancelled());
824
+ }
825
+ };
826
+ const choose = (index) => {
827
+ const option = options[index];
828
+ if (!option || option.disabled) return;
829
+ finish({ value: option.value, label: option.label });
830
+ };
831
+ const onKey = (input, key = {}) => {
832
+ if (key.ctrl && key.name === "c" || key.name === "escape") return finish(null);
833
+ if (key.name === "return" || key.name === "enter") return choose(cursor);
834
+ if (settings.back && (key.name === "left" || key.name === "backspace")) {
835
+ return finish({ value: BACK, label: settings.backLabel ?? "Back" });
836
+ }
837
+ if (key.name === "up" || key.name === "k") move(-1);
838
+ else if (key.name === "down" || key.name === "j" || key.name === "tab") move(1);
839
+ else if (input && /^[1-9]$/.test(input)) {
840
+ const index = Number(input) - 1;
841
+ if (options[index] && !options[index]?.disabled) return choose(index);
842
+ return;
843
+ } else if (input) {
844
+ const match = options.findIndex(
845
+ (option) => !option.disabled && option.key?.toLowerCase() === input.toLowerCase()
846
+ );
847
+ if (match >= 0) return choose(match);
848
+ return;
849
+ } else {
850
+ return;
851
+ }
852
+ draw();
853
+ };
854
+ process.once("exit", restoreCursor);
855
+ emitKeypressEvents(stdin);
856
+ stdin.setRawMode(true);
857
+ stdin.resume();
858
+ stdout.write("\x1B[?25l");
859
+ stdin.on("keypress", onKey);
860
+ draw();
861
+ });
862
+ }
863
+ async function selectYesNo(question, defaultYes = false) {
864
+ return select(
865
+ question,
866
+ [
867
+ { label: "Yes", value: true, key: "y" },
868
+ { label: "No", value: false, key: "n" }
869
+ ],
870
+ { initial: defaultYes ? 0 : 1 }
871
+ );
872
+ }
873
+ function fit(text, width) {
874
+ if (visibleLength(text) <= width) return text;
875
+ let shown = 0;
876
+ let result = "";
877
+ for (let index = 0; index < text.length; ) {
878
+ ANSI.lastIndex = index;
879
+ const escape = ANSI.exec(text);
880
+ if (escape && escape.index === index) {
881
+ result += escape[0];
882
+ index += escape[0].length;
883
+ continue;
884
+ }
885
+ if (shown >= width - 1) break;
886
+ const char = String.fromCodePoint(text.codePointAt(index));
887
+ result += char;
888
+ shown += 1;
889
+ index += char.length;
890
+ }
891
+ return `${result}\u2026\x1B[0m`;
892
+ }
893
+
894
+ // src/terminal.ts
569
895
  var COLOR_ENABLED = process.env.NO_COLOR === void 0 && process.env.TERM !== "dumb" && process.stdout.isTTY === true;
570
896
  function wrap(code, text) {
571
897
  return COLOR_ENABLED ? `\x1B[${code}m${text}\x1B[0m` : text;
@@ -599,12 +925,12 @@ function heading(text) {
599
925
  out(style.bold(text));
600
926
  out(style.grey("\u2500".repeat(Math.min(text.length, 72))));
601
927
  }
602
- function table(columns, rows) {
928
+ function table(columns2, rows) {
603
929
  if (rows.length === 0) {
604
930
  out(style.grey(" (none)"));
605
931
  return;
606
932
  }
607
- const widths = columns.map((column) => {
933
+ const widths = columns2.map((column) => {
608
934
  const longest = Math.max(
609
935
  column.header.length,
610
936
  ...rows.map((row) => (row[column.key] ?? "").length)
@@ -615,17 +941,17 @@ function table(columns, rows) {
615
941
  const parts = cells.map((cell, index) => {
616
942
  const width = widths[index] ?? cell.length;
617
943
  const text = cell.length > width ? `${cell.slice(0, Math.max(0, width - 1))}\u2026` : cell;
618
- return columns[index]?.align === "right" ? text.padStart(width) : text.padEnd(width);
944
+ return columns2[index]?.align === "right" ? text.padStart(width) : text.padEnd(width);
619
945
  });
620
946
  out(` ${decorate(parts.join(" "))}`);
621
947
  };
622
948
  renderRow(
623
- columns.map((column) => column.header),
949
+ columns2.map((column) => column.header),
624
950
  style.grey
625
951
  );
626
952
  for (const row of rows) {
627
953
  renderRow(
628
- columns.map((column) => row[column.key] ?? ""),
954
+ columns2.map((column) => row[column.key] ?? ""),
629
955
  (text) => text
630
956
  );
631
957
  }
@@ -696,6 +1022,14 @@ async function confirm(question, options = {}) {
696
1022
  out(`${question} ${style.grey("[y/N]")} ${style.amber("y (--yes)")}`);
697
1023
  return true;
698
1024
  }
1025
+ if (isInteractive()) {
1026
+ try {
1027
+ return await selectYesNo(question);
1028
+ } catch (error) {
1029
+ if (error instanceof MenuCancelled) return false;
1030
+ throw error;
1031
+ }
1032
+ }
699
1033
  if (!process.stdin.isTTY) {
700
1034
  err(
701
1035
  "This command requires confirmation but the terminal is not interactive. Re-run with --yes to confirm explicitly."
@@ -1405,11 +1739,11 @@ function renderRevokeConfirmation(agent, environment, preview) {
1405
1739
  out(` ${style.red("This action is irreversible for this identity.")}`);
1406
1740
  out();
1407
1741
  }
1408
- async function lifecycle(args, flags, path11, resultingStatus) {
1742
+ async function lifecycle(args, flags, path12, resultingStatus) {
1409
1743
  const context = await resolveContext(flags);
1410
1744
  const agentId = positional(args) ?? context.config.agentId;
1411
1745
  const agent = await context.api.get(`/v1/agents/${agentId}`);
1412
- const reasonRequired = path11 === "suspend";
1746
+ const reasonRequired = path12 === "suspend";
1413
1747
  const reason = valueFor(args, "--reason");
1414
1748
  if (reasonRequired && (!reason || reason.trim().length < 10)) {
1415
1749
  throw new CliError(
@@ -1419,24 +1753,24 @@ async function lifecycle(args, flags, path11, resultingStatus) {
1419
1753
  }
1420
1754
  out();
1421
1755
  out(
1422
- `${symbol.info} ${path11} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1756
+ `${symbol.info} ${path12} ${style.bold(agent.name)} ${style.grey(`(${agent.status} \u2192 ${resultingStatus})`)}`
1423
1757
  );
1424
- if (path11 !== "resume") {
1758
+ if (path12 !== "resume") {
1425
1759
  const confirmed = await confirm("Continue?", { yes: flags.yes });
1426
1760
  if (!confirmed) {
1427
1761
  out(style.grey("Cancelled."));
1428
1762
  return 1;
1429
1763
  }
1430
1764
  }
1431
- const actionSpinner = spinner(`Applying ${path11}`);
1765
+ const actionSpinner = spinner(`Applying ${path12}`);
1432
1766
  try {
1433
1767
  await withStepUp(
1434
1768
  context.api,
1435
- () => context.api.post(`/v1/agents/${agentId}/${path11}`, reason ? { reason } : {})
1769
+ () => context.api.post(`/v1/agents/${agentId}/${path12}`, reason ? { reason } : {})
1436
1770
  );
1437
1771
  actionSpinner.succeed(`${agent.name} is ${resultingStatus}`);
1438
1772
  } catch (error) {
1439
- actionSpinner.fail(`Could not ${path11} the agent`);
1773
+ actionSpinner.fail(`Could not ${path12} the agent`);
1440
1774
  throw error;
1441
1775
  }
1442
1776
  out();
@@ -2760,7 +3094,7 @@ function isLoosening(from, to) {
2760
3094
 
2761
3095
  // src/commands/init.ts
2762
3096
  import { readFileSync as readFileSync9 } from "node:fs";
2763
- import path10 from "node:path";
3097
+ import path11 from "node:path";
2764
3098
 
2765
3099
  // src/integrate.ts
2766
3100
  import { readFileSync as readFileSync8 } from "node:fs";
@@ -3556,32 +3890,30 @@ async function chooseMethod(items, options) {
3556
3890
  out(` ${style.bold(item.key.padEnd(24))} ${style.grey(`${item.file}:${item.line}`)}`);
3557
3891
  }
3558
3892
  out();
3559
- out("How would you like to handle them?");
3560
- out(
3561
- ` ${style.bold("1")} I will change them myself ${style.grey("(show me exactly what to change)")}`
3562
- );
3563
- out(
3564
- ` ${style.bold("2")} Use AI with my own Anthropic or OpenAI key ${style.grey("(code goes only to that provider)")}`
3565
- );
3566
- out(
3567
- ` ${style.bold("3")} Use Veefore AI ${style.grey(
3568
- options.hostedAvailable ? "(code is sent to Veefore, then to its model provider)" : "(not enabled on this deployment)"
3569
- )}`
3570
- );
3571
- out(` ${style.bold("4")} Skip for now`);
3572
- for (let attempt = 0; attempt < 3; attempt += 1) {
3573
- const answer = (await ask("Choice [1-4]:", "1")).trim();
3574
- if (answer === "1") return "manual";
3575
- if (answer === "2") return "own";
3576
- if (answer === "3") {
3577
- if (options.hostedAvailable) return "veefore";
3578
- out(style.amber(" Veefore AI is not enabled here. Choose 1, 2 or 4."));
3579
- continue;
3580
- }
3581
- if (answer === "4") return "skip";
3582
- out(style.amber(" Enter 1, 2, 3 or 4."));
3893
+ try {
3894
+ return await select("How would you like to handle them?", [
3895
+ {
3896
+ label: "I will change them myself",
3897
+ hint: "show me exactly what to change",
3898
+ value: "manual"
3899
+ },
3900
+ {
3901
+ label: "Use AI with my own Anthropic or OpenAI key",
3902
+ hint: "code goes only to that provider",
3903
+ value: "own"
3904
+ },
3905
+ {
3906
+ label: "Use Veefore AI",
3907
+ hint: options.hostedAvailable ? "code is sent to Veefore, then to its model provider" : "not enabled on this deployment",
3908
+ value: "veefore",
3909
+ disabled: !options.hostedAvailable
3910
+ },
3911
+ { label: "Skip for now", value: "skip" }
3912
+ ]);
3913
+ } catch (error) {
3914
+ if (error instanceof MenuCancelled) return "skip";
3915
+ throw error;
3583
3916
  }
3584
- return "manual";
3585
3917
  }
3586
3918
  function printManualInstructions(items) {
3587
3919
  heading("Make these changes yourself");
@@ -3666,9 +3998,7 @@ function printDispatchInstructions(items, where) {
3666
3998
  out(` ${style.cyan("import { guard } from '@veefore/sdk';")}`);
3667
3999
  out();
3668
4000
  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
- );
4001
+ out(` ${style.cyan("const result = await runTool(call);")}`);
3672
4002
  }
3673
4003
  out();
3674
4004
  out(
@@ -3793,16 +4123,21 @@ async function chooseModel(method, context) {
3793
4123
  async function pick(label, choices, preferred, interactive) {
3794
4124
  if (preferred && choices.includes(preferred)) return preferred;
3795
4125
  if (!interactive) return choices[0] ?? null;
3796
- const shown = choices.slice(0, 20);
4126
+ const OTHER = "\0other";
3797
4127
  out();
3798
- out(`${label}:`);
3799
- shown.forEach(
3800
- (choice, index2) => out(` ${style.bold(String(index2 + 1).padStart(2))} ${choice}`)
3801
- );
3802
- const answer = (await ask(`Choice [1-${shown.length}]:`, "1")).trim();
3803
- const index = Number(answer) - 1;
3804
- if (Number.isInteger(index) && shown[index]) return shown[index];
3805
- return choices.includes(answer) ? answer : null;
4128
+ let picked;
4129
+ try {
4130
+ picked = await select(label, [
4131
+ ...choices.slice(0, 20).map((choice) => ({ label: choice, value: choice })),
4132
+ ...choices.length > 20 ? [{ label: "Another\u2026", hint: `${choices.length - 20} more`, value: OTHER }] : []
4133
+ ]);
4134
+ } catch (error) {
4135
+ if (error instanceof MenuCancelled) return null;
4136
+ throw error;
4137
+ }
4138
+ if (picked !== OTHER) return picked;
4139
+ const typed = (await ask(`${label} id:`)).trim();
4140
+ return choices.includes(typed) ? typed : null;
3806
4141
  }
3807
4142
  async function runAssist(files, choice, context) {
3808
4143
  const accepted = [];
@@ -6551,120 +6886,570 @@ async function undoCommand(args, _flags) {
6551
6886
  return undoLatest(process.cwd(), { force: args.includes("--force") });
6552
6887
  }
6553
6888
 
6554
- // src/setup.ts
6555
- import { generateKeyPairSync } from "node:crypto";
6556
- function buildPlan(scan, options = {}) {
6557
- return {
6558
- agentName: (options.agentName ?? scan.projectName ?? "my-agent").slice(0, 200),
6559
- tools: scan.tools.map(planTool),
6560
- agentHeldSecrets: scan.secrets.filter((s) => s.kind === "service").map((s) => s.name),
6561
- modelSecrets: scan.secrets.filter((s) => s.kind === "model").map((s) => s.name),
6562
- extraCapabilities: options.extraCapabilities ?? []
6563
- };
6564
- }
6565
- function planTool(tool) {
6566
- return {
6567
- key: tool.key,
6568
- name: tool.name,
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
- }
6889
+ // src/commands/logs.ts
6890
+ async function logsCommand(args, flags) {
6891
+ const context = await resolveContext(flags);
6892
+ const follow = args.includes("--follow") || args.includes("-f");
6893
+ const limit = Number(valueFor(args, "--limit") ?? 25);
6894
+ const decisionFilter = valueFor(args, "--decision");
6895
+ const agentFilter = valueFor(args, "--agent") ?? (args.includes("--all") ? void 0 : context.config.agentId);
6896
+ const initial = await fetchActions(context, {
6897
+ limit,
6898
+ decision: decisionFilter,
6899
+ agentId: agentFilter
6900
+ });
6901
+ if (flags.json && !follow) {
6902
+ printJson(initial);
6903
+ return 0;
6608
6904
  }
6609
- const service = scan.secrets.filter((s) => s.kind === "service");
6610
- if (service.length > 0) {
6611
- out(
6612
- `${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)")}`
6613
- );
6905
+ heading(`Decisions \u2014 ${context.config.environmentName} (${context.config.environmentType})`);
6906
+ renderActions(initial, flags.verbose);
6907
+ if (!follow) {
6908
+ out();
6909
+ out(style.grey(" Use --follow to stream new decisions as they happen."));
6910
+ out();
6911
+ return 0;
6614
6912
  }
6913
+ return streamActions(context, {
6914
+ decision: decisionFilter,
6915
+ agentId: agentFilter,
6916
+ seen: new Set(initial.map((action) => action.id)),
6917
+ verbose: flags.verbose,
6918
+ json: flags.json
6919
+ });
6615
6920
  }
6616
- function printPlan2(plan, environment) {
6921
+ async function devCommand(args, flags) {
6922
+ const context = await resolveContext(flags);
6923
+ const environment = await context.api.get(`/v1/environments/${context.config.environmentId}`);
6924
+ const gateway = await probeGateway(context.config.gatewayUrl);
6925
+ const agent = await context.api.get(
6926
+ `/v1/agents/${context.config.agentId}`
6927
+ );
6928
+ heading(`veefore dev \u2014 ${context.config.organizationName} \u203A ${environment.name}`);
6617
6929
  out();
6618
- out(style.bold("Veefore will set up"));
6619
6930
  out(
6620
- ` ${symbol.bullet} Agent ${style.bold(plan.agentName)} in ${environment.name} (${environment.type})`
6931
+ ` ${gateway.reachable ? symbol.pass : symbol.fail} Gateway ${gateway.reachable ? style.green("connected") : style.red("unreachable")} ${style.grey(context.config.gatewayUrl)}`
6621
6932
  );
6622
6933
  out(
6623
- ` ${style.grey("with its own key pair, generated here. The private key never leaves this machine.")}`
6934
+ ` ${agent.status === "ACTIVE" ? symbol.pass : symbol.warn} Agent ${style.bold(agent.name)} ${decisionStyle(agent.status)}`
6624
6935
  );
6625
- if (plan.tools.length > 0) {
6626
- out(
6627
- ` ${symbol.bullet} ${plan.tools.length} tool${plan.tools.length === 1 ? "" : "s"}, each with its own capability, authorized for this agent only`
6628
- );
6629
- const dangerous = plan.tools.filter((tool) => tool.dangerous).map((tool) => tool.key);
6630
- if (dangerous.length > 0) {
6631
- out(
6632
- ` ${style.grey(`flagged high impact (approval under the starter policy): ${dangerous.join(", ")}`)}`
6633
- );
6634
- }
6635
- }
6636
6936
  out(
6637
- ` ${symbol.bullet} A starter policy, in ${style.bold("OBSERVE")} mode: decisions are recorded, nothing is blocked yet`
6937
+ ` ${environment.gatewayMode === "ENFORCE" ? symbol.pass : symbol.warn} Mode ${decisionStyle(environment.status === "PAUSED" ? "PAUSED" : environment.gatewayMode)}`
6638
6938
  );
6639
- if (plan.agentHeldSecrets.length > 0) {
6939
+ if (!gateway.reachable) {
6640
6940
  out();
6641
6941
  out(
6642
- style.grey(
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
- )
6942
+ ` ${symbol.fail} ${style.red("The gateway is not reachable, so no action can be permitted.")}`
6647
6943
  );
6944
+ out(style.grey(" Start it with `npm run dev:gateway`, then re-run `veefore dev`."));
6945
+ out();
6946
+ return 1;
6648
6947
  }
6649
- }
6650
- async function applyPlan(api, plan, scope, report = (line) => out(line), options = {}) {
6651
- const capabilityKeys = [
6652
- .../* @__PURE__ */ new Set([...plan.tools.map((t) => t.capability), ...plan.extraCapabilities])
6653
- ];
6654
- for (const tool of plan.tools) {
6655
- await createIfAbsent(
6656
- () => api.post("/v1/capabilities", {
6657
- key: tool.capability,
6658
- name: `Use ${tool.name}`.slice(0, 200),
6659
- description: tool.description.slice(0, 1e3),
6660
- dangerous: tool.dangerous
6661
- })
6948
+ if (environment.gatewayMode === "OBSERVE") {
6949
+ out();
6950
+ out(
6951
+ ` ${symbol.info} ${style.cyan("OBSERVE mode:")} every decision is evaluated and recorded, but nothing is blocked.`
6662
6952
  );
6953
+ out(style.grey(" This is the safe way to learn what your agent actually does."));
6663
6954
  }
6664
- const keys = generateKeyPairSync("ed25519");
6665
- const publicKey = keys.publicKey.export({ type: "spki", format: "der" }).toString(
6666
- "base64"
6667
- );
6955
+ if (agent.isolated) {
6956
+ out();
6957
+ out(` ${symbol.fail} ${style.red("This agent is isolated by an emergency control.")}`);
6958
+ }
6959
+ out();
6960
+ out(style.grey(" Watching for decisions. Press Ctrl+C to stop."));
6961
+ out();
6962
+ const existing = await fetchActions(context, { limit: 5, agentId: context.config.agentId });
6963
+ if (existing.length === 0) {
6964
+ out(
6965
+ ` ${symbol.info} No actions recorded yet. Call ${style.bold("agent.act({ tool: \u2026 })")} from your agent to see a decision here.`
6966
+ );
6967
+ out();
6968
+ } else {
6969
+ renderActions(existing, flags.verbose);
6970
+ }
6971
+ return streamActions(context, {
6972
+ agentId: args.includes("--all") ? void 0 : context.config.agentId,
6973
+ seen: new Set(existing.map((action) => action.id)),
6974
+ verbose: true,
6975
+ json: false,
6976
+ coach: true
6977
+ });
6978
+ }
6979
+ async function fetchActions(context, options) {
6980
+ const response = await context.api.get("/v1/actions", {
6981
+ limit: options.limit ?? 25,
6982
+ decision: options.decision,
6983
+ agent_id: options.agentId,
6984
+ environment_id: context.config.environmentId
6985
+ });
6986
+ return [...response.data].reverse();
6987
+ }
6988
+ function renderActions(actions, verbose) {
6989
+ if (actions.length === 0) {
6990
+ out(style.grey(" (no decisions yet)"));
6991
+ return;
6992
+ }
6993
+ if (!verbose) {
6994
+ table(
6995
+ [
6996
+ { header: "WHEN", key: "when", width: 12 },
6997
+ { header: "TOOL", key: "tool", width: 24 },
6998
+ { header: "DECISION", key: "decision", width: 20 },
6999
+ { header: "OUTCOME", key: "outcome", width: 24 },
7000
+ { header: "MS", key: "latency", width: 6, align: "right" }
7001
+ ],
7002
+ actions.map((action) => ({
7003
+ when: formatWhen(action.startedAt),
7004
+ tool: action.toolKey ?? action.action,
7005
+ decision: decisionStyle(effectiveDecision(action)),
7006
+ outcome: decisionStyle(action.outcome),
7007
+ latency: String(Math.round(action.latencyMs))
7008
+ }))
7009
+ );
7010
+ return;
7011
+ }
7012
+ for (const action of actions) renderAction(action);
7013
+ }
7014
+ function effectiveDecision(action) {
7015
+ return action.shadowDecision ?? action.decision ?? "UNKNOWN";
7016
+ }
7017
+ function renderAction(action, coach = false) {
7018
+ const decision = effectiveDecision(action);
7019
+ const icon = decision === "ALLOW" ? symbol.pass : decision === "BLOCK" ? symbol.fail : symbol.warn;
7020
+ const amount = action.amount === null ? "" : style.grey(` ${formatAmount(action.amount, action.currency)}`);
7021
+ out(
7022
+ `${icon} ${style.grey(new Date(action.startedAt).toISOString().slice(11, 19))} ${style.bold(action.toolKey ?? action.action)}${amount} ${decisionStyle(decision)} ${style.grey(`(${action.outcome})`)}`
7023
+ );
7024
+ if (action.denyReason) out(` ${action.denyReason}`);
7025
+ const facts = [];
7026
+ if (action.policyVersionNumber !== null) facts.push(`policy v${action.policyVersionNumber}`);
7027
+ if (action.matchedRuleIds.length > 0)
7028
+ facts.push(`${action.matchedRuleIds.length} rule(s) matched`);
7029
+ if (action.riskLevel) facts.push(`risk ${action.riskLevel}`);
7030
+ if (action.protocol !== "HTTP") facts.push(action.protocol);
7031
+ if (action.gatewayMode !== "ENFORCE") facts.push(`mode ${action.gatewayMode}`);
7032
+ facts.push(`${Math.round(action.latencyMs)}ms`);
7033
+ out(` ${style.grey(facts.join(" \xB7 "))}`);
7034
+ if (coach) {
7035
+ const hint = coachingFor(action);
7036
+ if (hint) out(` ${style.cyan("\u2192")} ${style.grey(hint)}`);
7037
+ }
7038
+ }
7039
+ function coachingFor(action) {
7040
+ const decision = effectiveDecision(action);
7041
+ if (action.gatewayMode === "OBSERVE" && decision !== "ALLOW") {
7042
+ return `In ENFORCE mode this would have been ${decision}. Simulate before switching.`;
7043
+ }
7044
+ switch (decision) {
7045
+ case "BLOCK":
7046
+ 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.";
7047
+ case "REQUIRE_APPROVAL":
7048
+ return "Call actWithApproval() so your agent waits for the reviewer instead of failing.";
7049
+ case "LIMIT":
7050
+ return "The amount was capped by policy. Read limits.appliedAmount from the decision.";
7051
+ case "MASK":
7052
+ return "Some fields were masked before reaching the tool. Check limits.maskedFields.";
7053
+ case "STEP_UP_AUTH":
7054
+ return "A human must re-authenticate. Agents cannot satisfy step-up on their own.";
7055
+ default:
7056
+ return null;
7057
+ }
7058
+ }
7059
+ function formatAmount(minorUnits, currency) {
7060
+ const major = (minorUnits / 100).toLocaleString("en-IN", {
7061
+ minimumFractionDigits: 2,
7062
+ maximumFractionDigits: 2
7063
+ });
7064
+ return currency ? `${major} ${currency}` : major;
7065
+ }
7066
+ async function streamActions(context, options) {
7067
+ let interval = 1e3;
7068
+ let stopped = false;
7069
+ const stop = () => {
7070
+ stopped = true;
7071
+ };
7072
+ process.once("SIGINT", stop);
7073
+ process.once("SIGTERM", stop);
7074
+ let blocked = 0;
7075
+ let approvals = 0;
7076
+ let allowed = 0;
7077
+ while (!stopped) {
7078
+ let fresh = [];
7079
+ try {
7080
+ const recent = await fetchActions(context, {
7081
+ limit: 50,
7082
+ decision: options.decision,
7083
+ agentId: options.agentId
7084
+ });
7085
+ fresh = recent.filter((action) => !options.seen.has(action.id));
7086
+ for (const action of fresh) options.seen.add(action.id);
7087
+ } catch (error) {
7088
+ out(
7089
+ `${symbol.warn} ${style.amber("Lost contact with the control plane;")} ${style.grey(error instanceof Error ? error.message : String(error))}`
7090
+ );
7091
+ interval = Math.min(interval * 2, 15e3);
7092
+ await delay(interval);
7093
+ continue;
7094
+ }
7095
+ for (const action of fresh) {
7096
+ if (options.json) {
7097
+ out(JSON.stringify(action));
7098
+ } else {
7099
+ renderAction(action, options.coach);
7100
+ }
7101
+ const decision = effectiveDecision(action);
7102
+ if (decision === "BLOCK") blocked += 1;
7103
+ else if (decision === "REQUIRE_APPROVAL") approvals += 1;
7104
+ else allowed += 1;
7105
+ }
7106
+ interval = fresh.length > 0 ? 1e3 : Math.min(interval * 1.5, 5e3);
7107
+ await delay(interval);
7108
+ }
7109
+ if (!options.json) {
7110
+ out();
7111
+ out(
7112
+ ` ${style.green(`${allowed} allowed`)} ${style.amber(`${approvals} needing approval`)} ${style.red(`${blocked} blocked`)}`
7113
+ );
7114
+ out();
7115
+ }
7116
+ return 0;
7117
+ }
7118
+ function delay(ms) {
7119
+ return new Promise((resolve) => setTimeout(resolve, ms));
7120
+ }
7121
+
7122
+ // src/commands/test.ts
7123
+ async function testCommand(args, flags) {
7124
+ const context = await resolveContext(flags);
7125
+ const toolFilter = valueFor(args, "--tool");
7126
+ heading(
7127
+ `Enforcement test \u2014 ${context.config.environmentName} (${context.config.environmentType})`
7128
+ );
7129
+ const results = [];
7130
+ const record = (check, passed, detail) => {
7131
+ results.push({ check, passed, detail });
7132
+ out(`${passed ? symbol.pass : symbol.fail} ${check} ${style.grey(detail)}`);
7133
+ };
7134
+ const gateway = await probeGateway(context.config.gatewayUrl);
7135
+ record(
7136
+ "Gateway reachable",
7137
+ gateway.reachable,
7138
+ gateway.reachable ? `${context.config.gatewayUrl} \u2014 posture ${gateway.posture ?? "unknown"}` : gateway.error ?? "unreachable"
7139
+ );
7140
+ const identities = await context.api.get(`/v1/agents/${context.config.agentId}/identities`).catch(() => []);
7141
+ const activeIdentity = identities.find((identity) => identity.status === "ACTIVE");
7142
+ record(
7143
+ "Agent identity active",
7144
+ Boolean(activeIdentity),
7145
+ activeIdentity ? `generation ${activeIdentity.generation}, expires ${activeIdentity.expiresAt.slice(0, 10)}` : "no ACTIVE identity \u2014 the agent cannot authenticate"
7146
+ );
7147
+ const policies = await context.api.get("/v1/policies", { workspace_id: context.config.workspaceId });
7148
+ const effective = policies.filter(
7149
+ (policy) => policy.status === "ACTIVE" && policy.bindings.length > 0
7150
+ );
7151
+ record(
7152
+ "Active, bound policy exists",
7153
+ effective.length > 0,
7154
+ effective.length > 0 ? effective.map((policy) => policy.name).join(", ") : "no active policy is bound to a scope, so no rule can match"
7155
+ );
7156
+ let caseTotal = 0;
7157
+ let caseFailed = 0;
7158
+ for (const policy of policies) {
7159
+ const result = await context.api.post(`/v1/policies/${policy.id}/tests/run`, {}).catch(() => null);
7160
+ if (!result) continue;
7161
+ caseTotal += result.passed + result.failed;
7162
+ caseFailed += result.failed;
7163
+ }
7164
+ record(
7165
+ "Policy test cases",
7166
+ caseFailed === 0,
7167
+ caseTotal === 0 ? "no test cases recorded \u2014 add cases so rule changes are provable" : `${caseTotal - caseFailed}/${caseTotal} passing`
7168
+ );
7169
+ const tools = await resolveAuthorizedTools(context, toolFilter);
7170
+ if (tools.length === 0) {
7171
+ record(
7172
+ "Authorized tools",
7173
+ false,
7174
+ "this agent is authorized for no tools, so it cannot do anything"
7175
+ );
7176
+ } else {
7177
+ record("Authorized tools", true, tools.map((tool) => tool.key).join(", "));
7178
+ }
7179
+ heading("Decision probe (dry run \u2014 nothing is executed)");
7180
+ const probes = [];
7181
+ for (const tool of tools) {
7182
+ const probeSpinner = spinner(`Evaluating ${tool.key}`);
7183
+ try {
7184
+ const outcome = await context.api.post("/v1/policies/simulate", {
7185
+ agent_id: context.config.agentId,
7186
+ tool: tool.key,
7187
+ capability: tool.requiredCapability,
7188
+ environment_id: context.config.environmentId,
7189
+ parameters: {}
7190
+ });
7191
+ probeSpinner.succeed(`${tool.key} \u2192 ${outcome.decision}`);
7192
+ probes.push({ tool, outcome });
7193
+ } catch (error) {
7194
+ probeSpinner.fail(`${tool.key} could not be evaluated`);
7195
+ probes.push({
7196
+ tool,
7197
+ outcome: null,
7198
+ error: error instanceof Error ? error.message : String(error)
7199
+ });
7200
+ }
7201
+ }
7202
+ out();
7203
+ table(
7204
+ [
7205
+ { header: "TOOL", key: "tool", width: 24 },
7206
+ { header: "RISK", key: "risk", width: 18 },
7207
+ { header: "DECISION", key: "decision", width: 20 },
7208
+ { header: "MATCHED RULE", key: "rule", width: 32 }
7209
+ ],
7210
+ probes.map((probe) => ({
7211
+ tool: probe.tool.dangerous ? `${probe.tool.key} ${style.red("(dangerous)")}` : probe.tool.key,
7212
+ risk: decisionStyle(probe.tool.riskLevel),
7213
+ decision: probe.outcome ? decisionStyle(probe.outcome.decision) : style.red("ERROR"),
7214
+ rule: probe.outcome?.matchedRules[0]?.name ?? style.grey(probe.error ?? "default decision")
7215
+ }))
7216
+ );
7217
+ const unguarded = probes.filter(
7218
+ (probe) => probe.tool.dangerous && probe.outcome?.decision === "ALLOW" && probe.outcome.matchedRules.length === 0
7219
+ );
7220
+ out();
7221
+ record(
7222
+ "Dangerous tools are guarded",
7223
+ unguarded.length === 0,
7224
+ 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`
7225
+ );
7226
+ const errored = probes.filter((probe) => probe.outcome === null);
7227
+ record(
7228
+ "All evaluations completed",
7229
+ errored.length === 0,
7230
+ errored.length === 0 ? `${probes.length} evaluated` : `${errored.length} failed to evaluate`
7231
+ );
7232
+ const impure = probes.filter((probe) => probe.outcome && probe.outcome.sideEffectFree !== true);
7233
+ record(
7234
+ "Simulation was side-effect free",
7235
+ impure.length === 0,
7236
+ impure.length === 0 ? "no action records were written" : "the server reported side effects"
7237
+ );
7238
+ const failed = results.filter((result) => !result.passed);
7239
+ out();
7240
+ out(
7241
+ failed.length === 0 ? ` ${symbol.pass} ${style.green(`${results.length} check(s) passing.`)}` : ` ${symbol.fail} ${style.red(`${failed.length} of ${results.length} check(s) failing.`)}`
7242
+ );
7243
+ if (failed.length > 0) {
7244
+ out();
7245
+ for (const failure of failed) out(` ${symbol.fail} ${failure.check}: ${failure.detail}`);
7246
+ }
7247
+ out();
7248
+ if (flags.json) printJson({ results, probes });
7249
+ return failed.length > 0 ? 1 : 0;
7250
+ }
7251
+ async function resolveAuthorizedTools(context, filter) {
7252
+ const tools = await context.api.get("/v1/tools", {
7253
+ workspace_id: context.config.workspaceId,
7254
+ agent_id: context.config.agentId,
7255
+ limit: 100
7256
+ });
7257
+ const candidates = tools.items.filter(
7258
+ (tool) => tool.status !== "DISABLED" && (tool.environmentId === null || tool.environmentId === context.config.environmentId)
7259
+ );
7260
+ if (!filter) return candidates;
7261
+ const match = candidates.filter((tool) => tool.key === filter);
7262
+ if (match.length === 0) {
7263
+ throw new CliError(
7264
+ `Tool "${filter}" is not available to this agent in ${context.config.environmentName}.`,
7265
+ `Available: ${candidates.map((tool) => tool.key).join(", ") || "none"}`
7266
+ );
7267
+ }
7268
+ return match;
7269
+ }
7270
+
7271
+ // src/panel.ts
7272
+ import { homedir as homedir2 } from "node:os";
7273
+ import path10 from "node:path";
7274
+
7275
+ // src/version.ts
7276
+ var CLI_VERSION = "1.2.0";
7277
+
7278
+ // src/panel.ts
7279
+ function welcomeSource(flags = {}) {
7280
+ return () => {
7281
+ const project = readProjectConfig();
7282
+ const controlPlaneUrl = project?.controlPlaneUrl ?? resolveControlPlaneUrl(flags.controlPlaneUrl);
7283
+ const session = loadSession(controlPlaneUrl);
7284
+ return {
7285
+ version: CLI_VERSION,
7286
+ facts: [
7287
+ ["Folder", shorten(tilde(process.cwd()), 34)],
7288
+ ["Signed in", session ? session.email : style.grey("not yet")],
7289
+ [
7290
+ "Project",
7291
+ project ? `${project.agentName} \xB7 ${project.environmentName}` : style.grey("not set up here")
7292
+ ],
7293
+ ["Veefore", hostOf(controlPlaneUrl)]
7294
+ ]
7295
+ };
7296
+ };
7297
+ }
7298
+ function tilde(dir) {
7299
+ const home = homedir2();
7300
+ return dir === home || dir.startsWith(`${home}${path10.sep}`) ? `~${dir.slice(home.length)}` : dir;
7301
+ }
7302
+ function hostOf(url) {
7303
+ try {
7304
+ return new URL(url).host;
7305
+ } catch {
7306
+ return url;
7307
+ }
7308
+ }
7309
+ function shorten(text, width) {
7310
+ return text.length <= width ? text : `\u2026${text.slice(text.length - width + 1)}`;
7311
+ }
7312
+
7313
+ // src/report.ts
7314
+ function reportError(error, flags) {
7315
+ if (error instanceof CliError) {
7316
+ err();
7317
+ err(`${style.red("\u2717")} ${error.message}`);
7318
+ if (error.hint) err(style.grey(` ${error.hint}`));
7319
+ err();
7320
+ return error.exitCode;
7321
+ }
7322
+ if (error instanceof CliApiError) {
7323
+ err();
7324
+ reportApiError(error);
7325
+ err();
7326
+ return 1;
7327
+ }
7328
+ err();
7329
+ err(`${style.red("\u2717")} ${error instanceof Error ? error.message : String(error)}`);
7330
+ if (flags.verbose && error instanceof Error && error.stack) {
7331
+ err(style.grey(error.stack));
7332
+ } else {
7333
+ err(style.grey(" Re-run with --verbose for the stack trace."));
7334
+ }
7335
+ err();
7336
+ return 1;
7337
+ }
7338
+
7339
+ // src/setup.ts
7340
+ import { generateKeyPairSync } from "node:crypto";
7341
+ function buildPlan(scan, options = {}) {
7342
+ return {
7343
+ agentName: (options.agentName ?? scan.projectName ?? "my-agent").slice(0, 200),
7344
+ tools: scan.tools.map(planTool),
7345
+ agentHeldSecrets: scan.secrets.filter((s) => s.kind === "service").map((s) => s.name),
7346
+ modelSecrets: scan.secrets.filter((s) => s.kind === "model").map((s) => s.name),
7347
+ extraCapabilities: options.extraCapabilities ?? []
7348
+ };
7349
+ }
7350
+ function planTool(tool) {
7351
+ return {
7352
+ key: tool.key,
7353
+ name: tool.name,
7354
+ description: tool.description ?? `Detected ${sourceLabel(tool.source)} tool.`,
7355
+ // One capability per tool keeps authority explicit: granting the agent this
7356
+ // tool grants nothing else. `tool.<key>` satisfies `resource.action`.
7357
+ capability: `tool.${tool.key}`,
7358
+ riskLevel: tool.riskLevel,
7359
+ dangerous: tool.dangerous,
7360
+ destinations: tool.hosts,
7361
+ location: `${tool.location.file}:${tool.location.line}`,
7362
+ money: tool.money ?? null
7363
+ };
7364
+ }
7365
+ function printScan(scan) {
7366
+ out(
7367
+ `${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")
7368
+ );
7369
+ if (scan.tools.length === 0) {
7370
+ out(`${symbol.warn} No tools found, so there is nothing for Veefore to decide on yet.`);
7371
+ out(style.grey(" Tools are found where they are declared for the model to call, not from"));
7372
+ out(
7373
+ style.grey(
7374
+ scan.framework.language === "python" ? " plain functions: @tool, @function_tool, @mcp.tool, Tool(name=\u2026), or a dict" : ' plain functions: tool({\u2026}), DynamicStructuredTool, { type: "function" },'
7375
+ )
7376
+ );
7377
+ out(
7378
+ style.grey(
7379
+ scan.framework.language === "python" ? " with name, description and an input schema." : " server.tool(\u2026), or an object with name, description and parameters."
7380
+ )
7381
+ );
7382
+ out(style.grey(" Register them in the console, or add them and run `veefore scan` again."));
7383
+ } else {
7384
+ out(`${symbol.pass} Found ${scan.tools.length} tool${scan.tools.length === 1 ? "" : "s"}:`);
7385
+ for (const tool of scan.tools) {
7386
+ const label = tool.riskLevel.padEnd(7);
7387
+ const risk = tool.riskLevel === "HIGH" ? style.red(label) : tool.riskLevel === "MEDIUM" ? style.amber(label) : style.green(label);
7388
+ out(
7389
+ ` ${style.bold(tool.key.padEnd(24))} ${risk} ${style.grey(`${tool.location.file}:${tool.location.line}`)}`
7390
+ );
7391
+ if (tool.hosts.length > 0) out(style.grey(` calls ${tool.hosts.join(", ")}`));
7392
+ }
7393
+ }
7394
+ const service = scan.secrets.filter((s) => s.kind === "service");
7395
+ if (service.length > 0) {
7396
+ out(
7397
+ `${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)")}`
7398
+ );
7399
+ }
7400
+ }
7401
+ function printPlan2(plan, environment) {
7402
+ out();
7403
+ out(style.bold("Veefore will set up"));
7404
+ out(
7405
+ ` ${symbol.bullet} Agent ${style.bold(plan.agentName)} in ${environment.name} (${environment.type})`
7406
+ );
7407
+ out(
7408
+ ` ${style.grey("with its own key pair, generated here. The private key never leaves this machine.")}`
7409
+ );
7410
+ if (plan.tools.length > 0) {
7411
+ out(
7412
+ ` ${symbol.bullet} ${plan.tools.length} tool${plan.tools.length === 1 ? "" : "s"}, each with its own capability, authorized for this agent only`
7413
+ );
7414
+ const dangerous = plan.tools.filter((tool) => tool.dangerous).map((tool) => tool.key);
7415
+ if (dangerous.length > 0) {
7416
+ out(
7417
+ ` ${style.grey(`flagged high impact (approval under the starter policy): ${dangerous.join(", ")}`)}`
7418
+ );
7419
+ }
7420
+ }
7421
+ out(
7422
+ ` ${symbol.bullet} A starter policy, in ${style.bold("OBSERVE")} mode: decisions are recorded, nothing is blocked yet`
7423
+ );
7424
+ if (plan.agentHeldSecrets.length > 0) {
7425
+ out();
7426
+ out(
7427
+ style.grey(
7428
+ ` Your agent keeps holding ${plan.agentHeldSecrets.join(", ")} for now. Veefore will decide every call
7429
+ and record it, but a call that skips the SDK is not stopped until those keys move into
7430
+ the Veefore credential broker. Setup does not read or move any secret.`
7431
+ )
7432
+ );
7433
+ }
7434
+ }
7435
+ async function applyPlan(api, plan, scope, report = (line) => out(line), options = {}) {
7436
+ const capabilityKeys = [
7437
+ .../* @__PURE__ */ new Set([...plan.tools.map((t) => t.capability), ...plan.extraCapabilities])
7438
+ ];
7439
+ for (const tool of plan.tools) {
7440
+ await createIfAbsent(
7441
+ () => api.post("/v1/capabilities", {
7442
+ key: tool.capability,
7443
+ name: `Use ${tool.name}`.slice(0, 200),
7444
+ description: tool.description.slice(0, 1e3),
7445
+ dangerous: tool.dangerous
7446
+ })
7447
+ );
7448
+ }
7449
+ const keys = generateKeyPairSync("ed25519");
7450
+ const publicKey = keys.publicKey.export({ type: "spki", format: "der" }).toString(
7451
+ "base64"
7452
+ );
6668
7453
  const privateKey = keys.privateKey.export({ type: "pkcs8", format: "der" }).toString(
6669
7454
  "base64"
6670
7455
  );
@@ -6857,7 +7642,7 @@ var DEFAULT_GATEWAY = "http://localhost:4010";
6857
7642
  async function initCommand(args, flags) {
6858
7643
  const options = parseInitArgs(args);
6859
7644
  const existing = readProjectConfig();
6860
- if (existing && !options.force && process.stdin.isTTY && process.stdout.isTTY && !flags.json) {
7645
+ if (existing && !options.force && isInteractive() && !flags.json) {
6861
7646
  return projectHome(existing, flags);
6862
7647
  }
6863
7648
  if (existing && !options.force) {
@@ -6898,20 +7683,34 @@ async function initCommand(args, flags) {
6898
7683
  }
6899
7684
  if (!session) throw new CliError("Sign-in did not produce a usable session.");
6900
7685
  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();
6907
- out(
6908
- `${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.`
6909
- );
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;
7686
+ let me = await api.get("/v1/auth/session");
7687
+ const STEPS = ["organization", "workspace", "environment"];
7688
+ const asked = /* @__PURE__ */ new Map();
7689
+ let organization;
7690
+ let workspace;
7691
+ let environment;
7692
+ for (let at = 0; at < STEPS.length; ) {
7693
+ const step = STEPS[at];
7694
+ const answer = step === "organization" ? await selectOrganization(api, me, options, flags) : step === "workspace" ? await selectWorkspace(api, me, options, flags) : await selectEnvironment(api, me, workspace.id, options, flags);
7695
+ if (answer === BACK) {
7696
+ let previous = at - 1;
7697
+ while (previous >= 0 && !asked.get(STEPS[previous])) previous -= 1;
7698
+ if (previous < 0) {
7699
+ throw new MenuCancelled();
7700
+ }
7701
+ at = previous;
7702
+ continue;
7703
+ }
7704
+ asked.set(step, answer.asked);
7705
+ if (step === "organization") {
7706
+ organization = answer.value;
7707
+ if (answer.asked) me = await api.get("/v1/auth/session");
7708
+ } else if (step === "workspace") {
7709
+ workspace = answer.value;
7710
+ } else {
7711
+ environment = answer.value;
6914
7712
  }
7713
+ at += 1;
6915
7714
  }
6916
7715
  heading("Looking at your project");
6917
7716
  const scan = scanProject(process.cwd());
@@ -6920,15 +7719,41 @@ async function initCommand(args, flags) {
6920
7719
  agentName: options.agentName,
6921
7720
  extraCapabilities: options.capabilities
6922
7721
  });
6923
- if (!options.agentName && !flags.yes) {
6924
- plan.agentName = await ask(`Agent name ${style.grey(`[${plan.agentName}]`)}:`, plan.agentName);
6925
- }
6926
- printPlan2(plan, environment);
6927
- out();
6928
- const proceed = await confirm("Set this up?", { yes: flags.yes });
6929
- if (!proceed) {
6930
- out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
6931
- return 1;
7722
+ for (; ; ) {
7723
+ if (!options.agentName && !flags.yes) {
7724
+ const name = await askAgentName(plan.agentName);
7725
+ if (name === BACK) {
7726
+ const again = await selectEnvironment(api, me, workspace.id, options, flags);
7727
+ if (again !== BACK) environment = again.value;
7728
+ continue;
7729
+ }
7730
+ plan.agentName = name;
7731
+ }
7732
+ printPlan2(plan, environment);
7733
+ out();
7734
+ const proceed = await confirm("Set this up?", { yes: flags.yes });
7735
+ if (proceed) break;
7736
+ if (flags.yes || options.agentName) {
7737
+ out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
7738
+ return 1;
7739
+ }
7740
+ const next = await select(
7741
+ "What would you like to do?",
7742
+ [
7743
+ { label: "Change the agent name", value: "name" },
7744
+ { label: "Choose a different environment", value: "environment" },
7745
+ { label: "Leave setup", value: "leave" }
7746
+ ],
7747
+ { initial: 0 }
7748
+ );
7749
+ if (next === "leave") {
7750
+ out(style.grey("Nothing was created. Run `veefore init` again when you are ready."));
7751
+ return 1;
7752
+ }
7753
+ if (next === "environment") {
7754
+ const again = await selectEnvironment(api, me, workspace.id, options, flags);
7755
+ if (again !== BACK) environment = again.value;
7756
+ }
6932
7757
  }
6933
7758
  heading("Setting up");
6934
7759
  let created;
@@ -7096,7 +7921,7 @@ async function initCommand(args, flags) {
7096
7921
  }
7097
7922
  function previousApprovalMode(dir = process.cwd()) {
7098
7923
  try {
7099
- const text = readFileSync9(path10.join(dir, ".env.local"), "utf8");
7924
+ const text = readFileSync9(path11.join(dir, ".env.local"), "utf8");
7100
7925
  const value = /^\s*VEEFORE_APPROVAL_MODE\s*=\s*['"]?(\w+)/m.exec(text)?.[1]?.toLowerCase();
7101
7926
  return value === "wait" || value === "throw" ? value : null;
7102
7927
  } catch {
@@ -7142,35 +7967,28 @@ async function selectOrganization(api, me, options, flags) {
7142
7967
  `Available: ${organizations.map((org) => org.id).join(", ")}`
7143
7968
  );
7144
7969
  }
7145
- return match;
7970
+ return { value: match, asked: false };
7146
7971
  }
7147
7972
  if (organizations.length === 1 || flags.yes) {
7148
7973
  const chosen2 = organizations[0];
7149
7974
  if (!chosen2) throw new CliError("Your account is not a member of any organization.");
7150
7975
  out(`${symbol.pass} Organization: ${style.bold(chosen2.name)}`);
7151
- return chosen2;
7976
+ return { value: chosen2, asked: false };
7152
7977
  }
7153
7978
  out();
7154
- out(style.bold("Select an organization"));
7155
- table(
7156
- [
7157
- { header: "#", key: "index", align: "right" },
7158
- { header: "NAME", key: "name" },
7159
- { header: "ROLE", key: "role" },
7160
- { header: "ID", key: "id" }
7161
- ],
7162
- organizations.map((org, index) => ({
7163
- index: String(index + 1),
7164
- name: org.name,
7165
- role: org.roleKey,
7166
- id: org.id
7167
- }))
7979
+ const chosen = await select(
7980
+ "Which organization?",
7981
+ organizations.map((org) => ({
7982
+ label: org.name,
7983
+ hint: `${org.roleKey} \xB7 ${org.id}`,
7984
+ value: org
7985
+ })),
7986
+ { quiet: true, back: true, backLabel: "Back" }
7168
7987
  );
7169
- const chosen = organizations[await pickIndex(organizations.length)];
7170
- if (!chosen) throw new CliError("No organization selected.");
7988
+ if (chosen === BACK) return BACK;
7171
7989
  await api.post("/v1/auth/scope", { organization_id: chosen.id });
7172
7990
  out(`${symbol.pass} Organization: ${style.bold(chosen.name)}`);
7173
- return chosen;
7991
+ return { value: chosen, asked: true };
7174
7992
  }
7175
7993
  async function selectWorkspace(api, me, options, flags) {
7176
7994
  const workspaces = me.workspaces.length > 0 ? me.workspaces : await api.get("/v1/workspaces");
@@ -7183,31 +8001,26 @@ async function selectWorkspace(api, me, options, flags) {
7183
8001
  if (options.workspaceId) {
7184
8002
  const match = workspaces.find((workspace) => workspace.id === options.workspaceId);
7185
8003
  if (!match) throw new CliError(`Workspace ${options.workspaceId} was not found.`);
7186
- return match;
8004
+ return { value: match, asked: false };
7187
8005
  }
7188
8006
  if (workspaces.length === 1 || flags.yes) {
7189
8007
  const chosen2 = workspaces[0];
7190
8008
  out(`${symbol.pass} Workspace: ${style.bold(chosen2.name)}`);
7191
- return chosen2;
8009
+ return { value: chosen2, asked: false };
7192
8010
  }
7193
8011
  out();
7194
- out(style.bold("Select a workspace"));
7195
- table(
7196
- [
7197
- { header: "#", key: "index", align: "right" },
7198
- { header: "NAME", key: "name" },
7199
- { header: "ID", key: "id" }
7200
- ],
7201
- workspaces.map((workspace, index) => ({
7202
- index: String(index + 1),
7203
- name: workspace.name,
7204
- id: workspace.id
7205
- }))
8012
+ const chosen = await select(
8013
+ "Which workspace?",
8014
+ workspaces.map((workspace) => ({
8015
+ label: workspace.name,
8016
+ hint: workspace.id,
8017
+ value: workspace
8018
+ })),
8019
+ { quiet: true, back: true }
7206
8020
  );
7207
- const chosen = workspaces[await pickIndex(workspaces.length)];
7208
- if (!chosen) throw new CliError("No workspace selected.");
8021
+ if (chosen === BACK) return BACK;
7209
8022
  out(`${symbol.pass} Workspace: ${style.bold(chosen.name)}`);
7210
- return chosen;
8023
+ return { value: chosen, asked: true };
7211
8024
  }
7212
8025
  async function selectEnvironment(api, me, workspaceId, options, flags) {
7213
8026
  const all = me.environments.length > 0 ? me.environments : await api.get("/v1/environments", { workspace_id: workspaceId });
@@ -7221,35 +8034,61 @@ async function selectEnvironment(api, me, workspaceId, options, flags) {
7221
8034
  if (options.environmentId) {
7222
8035
  const match = environments.find((environment) => environment.id === options.environmentId);
7223
8036
  if (!match) throw new CliError(`Environment ${options.environmentId} was not found.`);
7224
- return match;
8037
+ return { value: match, asked: false };
8038
+ }
8039
+ if (flags.yes) {
8040
+ const chosen = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
8041
+ out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
8042
+ return { value: chosen, asked: false };
8043
+ }
8044
+ const development = environments.findIndex((environment) => environment.type === "DEVELOPMENT");
8045
+ for (; ; ) {
8046
+ out();
8047
+ const chosen = await select(
8048
+ "Which environment?",
8049
+ environments.map((environment) => ({
8050
+ label: environment.name,
8051
+ hint: `${environment.type} \xB7 ${environment.gatewayMode ?? "\u2014"} \xB7 ${environment.id}`,
8052
+ value: environment
8053
+ })),
8054
+ { quiet: true, initial: development >= 0 ? development : 0, back: true }
8055
+ );
8056
+ if (chosen === BACK) return BACK;
8057
+ if (chosen.type === "PRODUCTION" && !options.force) {
8058
+ out();
8059
+ out(
8060
+ `${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.`
8061
+ );
8062
+ if (!await confirm("Continue with the production environment?", { yes: flags.yes })) {
8063
+ out(style.grey(" Choose another environment."));
8064
+ continue;
8065
+ }
8066
+ }
8067
+ out(`${symbol.pass} Environment: ${style.bold(chosen.name)} (${chosen.type})`);
8068
+ return { value: chosen, asked: true };
7225
8069
  }
7226
- if (flags.yes) {
7227
- const chosen2 = environments.find((environment) => environment.type === "DEVELOPMENT") ?? environments[0];
7228
- out(`${symbol.pass} Environment: ${style.bold(chosen2.name)} (${chosen2.type})`);
7229
- return chosen2;
8070
+ }
8071
+ async function askAgentName(suggested) {
8072
+ for (; ; ) {
8073
+ out();
8074
+ const choice = await select(
8075
+ "Agent name",
8076
+ [
8077
+ { label: suggested, hint: "suggested", value: "keep" },
8078
+ { label: "Type a different name\u2026", value: "type" }
8079
+ ],
8080
+ { quiet: true, back: true }
8081
+ );
8082
+ if (choice === BACK) return BACK;
8083
+ if (choice === "keep") {
8084
+ out(`${symbol.pass} Agent name: ${style.bold(suggested)}`);
8085
+ return suggested;
8086
+ }
8087
+ const typed = (await ask(`Agent name ${style.grey(`[${suggested}]`)}:`, suggested)).trim();
8088
+ if (typed === "") continue;
8089
+ out(`${symbol.pass} Agent name: ${style.bold(typed)}`);
8090
+ return typed;
7230
8091
  }
7231
- out();
7232
- out(style.bold("Select an environment"));
7233
- table(
7234
- [
7235
- { header: "#", key: "index", align: "right" },
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
8092
  }
7254
8093
  async function discoverAudience(gatewayUrl) {
7255
8094
  try {
@@ -7264,15 +8103,6 @@ async function discoverAudience(gatewayUrl) {
7264
8103
  return gatewayUrl;
7265
8104
  }
7266
8105
  }
7267
- async function pickIndex(count) {
7268
- for (let attempt = 0; attempt < 3; attempt += 1) {
7269
- const answer = await ask(`Choice [1-${count}]:`, "1");
7270
- const index = Number(answer) - 1;
7271
- if (Number.isInteger(index) && index >= 0 && index < count) return index;
7272
- out(style.amber(` Enter a number between 1 and ${count}.`));
7273
- }
7274
- throw new CliError("No valid selection was made.");
7275
- }
7276
8106
  async function configureStarterPolicy(api, workspaceId, flags, options) {
7277
8107
  if (options.skipPolicy) {
7278
8108
  out(`${symbol.info} Skipped starter policy (--skip-policy).`);
@@ -7348,443 +8178,266 @@ async function validateConnection(api, gatewayUrl, agentId, environmentId) {
7348
8178
  checks.push({
7349
8179
  name: "Gateway reachable",
7350
8180
  status: "fail",
7351
- message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
7352
- });
7353
- }
7354
- try {
7355
- const identities = await api.get(
7356
- `/v1/agents/${agentId}/identities`
7357
- );
7358
- const active = identities.find((identity) => identity.status === "ACTIVE");
7359
- checks.push({
7360
- name: "Agent identity active",
7361
- status: active ? "pass" : "fail",
7362
- message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
7363
- });
7364
- } catch (error) {
7365
- checks.push({
7366
- name: "Agent identity active",
7367
- status: "warn",
7368
- message: error instanceof Error ? error.message : String(error)
7369
- });
7370
- }
7371
- try {
7372
- const passport = await api.get(
7373
- `/v1/passports/${agentId}`
7374
- );
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
- }
7383
- try {
7384
- const environment = await api.get(
7385
- `/v1/environments/${environmentId}`
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
- });
7392
- } catch {
7393
- checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
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
- }
7456
- }
7457
- return options;
7458
- }
7459
- async function projectHome(existing, flags) {
7460
- const state = await agentState(existing);
7461
- out();
7462
- out(`${style.bold("Veefore")} ${style.grey(existing.controlPlaneUrl)}`);
7463
- details([
7464
- ["Agent", `${existing.agentName} (${existing.agentId})`],
7465
- ["Environment", `${existing.environmentName} (${existing.environmentType})`],
7466
- [
7467
- "Status",
7468
- 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"
7469
- ]
7470
- ]);
7471
- if (state.kind === "session-ended") {
7472
- out(`${symbol.warn} ${state.message}`);
7473
- }
7474
- const choices = [];
7475
- const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
7476
- if (ended) {
7477
- out();
7478
- out(
7479
- `${symbol.warn} ${style.amber(
7480
- state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
7481
- )} Every tool call wrapped in guard() is now refused.`
7482
- );
7483
- choices.push(
7484
- {
7485
- label: "Remove Veefore from this project (take guard() out of the code, disconnect)",
7486
- run: () => removeCommand([], flags, state)
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
- });
7502
- }
7503
- choices.push(
7504
- { label: "Check status", run: () => statusCommand([], flags) },
7505
- {
7506
- label: "Protect tools in the code (veefore integrate)",
7507
- run: () => integrateCommand([], flags)
7508
- },
7509
- { label: "Re-run setup for this project", run: () => initCommand(["--force"], flags) },
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
- });
7523
- }
7524
- }
7525
- choices.push({ label: "Exit", run: async () => 0 });
7526
- out();
7527
- out("What would you like to do?");
7528
- choices.forEach((choice, index) => out(` ${style.bold(String(index + 1))} ${choice.label}`));
7529
- const answer = Number(
7530
- (await ask(`Choice [1-${choices.length}]:`, String(choices.length))).trim()
7531
- );
7532
- const picked = choices[answer - 1];
7533
- if (!picked) {
7534
- out(style.grey("Nothing changed."));
7535
- return 0;
7536
- }
7537
- return picked.run();
7538
- }
7539
-
7540
- // src/commands/scan.ts
7541
- async function scanCommand(_args, flags) {
7542
- const scan = scanProject(process.cwd());
7543
- if (flags.json) {
7544
- out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
7545
- return 0;
7546
- }
7547
- heading("Veefore scan");
7548
- out(
7549
- `${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
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;
7572
- }
7573
- heading(`Decisions \u2014 ${context.config.environmentName} (${context.config.environmentType})`);
7574
- renderActions(initial, flags.verbose);
7575
- if (!follow) {
7576
- out();
7577
- out(style.grey(" Use --follow to stream new decisions as they happen."));
7578
- out();
7579
- return 0;
7580
- }
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
- out();
7598
- out(
7599
- ` ${gateway.reachable ? symbol.pass : symbol.fail} Gateway ${gateway.reachable ? style.green("connected") : style.red("unreachable")} ${style.grey(context.config.gatewayUrl)}`
7600
- );
7601
- out(
7602
- ` ${agent.status === "ACTIVE" ? symbol.pass : symbol.warn} Agent ${style.bold(agent.name)} ${decisionStyle(agent.status)}`
7603
- );
7604
- out(
7605
- ` ${environment.gatewayMode === "ENFORCE" ? symbol.pass : symbol.warn} Mode ${decisionStyle(environment.status === "PAUSED" ? "PAUSED" : environment.gatewayMode)}`
7606
- );
7607
- if (!gateway.reachable) {
7608
- out();
7609
- out(
7610
- ` ${symbol.fail} ${style.red("The gateway is not reachable, so no action can be permitted.")}`
7611
- );
7612
- out(style.grey(" Start it with `npm run dev:gateway`, then re-run `veefore dev`."));
7613
- out();
7614
- return 1;
8181
+ message: `${gatewayUrl} \u2014 ${error instanceof Error ? error.message : String(error)}. Start it with \`npm run dev:gateway\`.`
8182
+ });
7615
8183
  }
7616
- if (environment.gatewayMode === "OBSERVE") {
7617
- out();
7618
- out(
7619
- ` ${symbol.info} ${style.cyan("OBSERVE mode:")} every decision is evaluated and recorded, but nothing is blocked.`
8184
+ try {
8185
+ const identities = await api.get(
8186
+ `/v1/agents/${agentId}/identities`
7620
8187
  );
7621
- out(style.grey(" This is the safe way to learn what your agent actually does."));
7622
- }
7623
- if (agent.isolated) {
7624
- out();
7625
- out(` ${symbol.fail} ${style.red("This agent is isolated by an emergency control.")}`);
8188
+ const active = identities.find((identity) => identity.status === "ACTIVE");
8189
+ checks.push({
8190
+ name: "Agent identity active",
8191
+ status: active ? "pass" : "fail",
8192
+ message: active ? `expires ${active.expiresAt.slice(0, 10)}` : "no ACTIVE identity"
8193
+ });
8194
+ } catch (error) {
8195
+ checks.push({
8196
+ name: "Agent identity active",
8197
+ status: "warn",
8198
+ message: error instanceof Error ? error.message : String(error)
8199
+ });
7626
8200
  }
7627
- out();
7628
- out(style.grey(" Watching for decisions. Press Ctrl+C to stop."));
7629
- out();
7630
- const existing = await fetchActions(context, { limit: 5, agentId: context.config.agentId });
7631
- if (existing.length === 0) {
7632
- out(
7633
- ` ${symbol.info} No actions recorded yet. Call ${style.bold("agent.act({ tool: \u2026 })")} from your agent to see a decision here.`
8201
+ try {
8202
+ const passport = await api.get(
8203
+ `/v1/passports/${agentId}`
7634
8204
  );
7635
- out();
7636
- } else {
7637
- renderActions(existing, flags.verbose);
7638
- }
7639
- return streamActions(context, {
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
- });
7646
- }
7647
- async function fetchActions(context, options) {
7648
- const response = await context.api.get("/v1/actions", {
7649
- limit: options.limit ?? 25,
7650
- decision: options.decision,
7651
- agent_id: options.agentId,
7652
- environment_id: context.config.environmentId
7653
- });
7654
- return [...response.data].reverse();
7655
- }
7656
- function renderActions(actions, verbose) {
7657
- if (actions.length === 0) {
7658
- out(style.grey(" (no decisions yet)"));
7659
- return;
8205
+ checks.push({
8206
+ name: "Agent passport issued",
8207
+ status: passport ? "pass" : "warn",
8208
+ message: passport ? `version ${passport.version}` : "not issued yet"
8209
+ });
8210
+ } catch {
8211
+ checks.push({ name: "Agent passport issued", status: "warn", message: "could not read" });
7660
8212
  }
7661
- if (!verbose) {
7662
- table(
7663
- [
7664
- { header: "WHEN", key: "when", width: 12 },
7665
- { header: "TOOL", key: "tool", width: 24 },
7666
- { header: "DECISION", key: "decision", width: 20 },
7667
- { header: "OUTCOME", key: "outcome", width: 24 },
7668
- { header: "MS", key: "latency", width: 6, align: "right" }
7669
- ],
7670
- actions.map((action) => ({
7671
- when: formatWhen(action.startedAt),
7672
- tool: action.toolKey ?? action.action,
7673
- decision: decisionStyle(effectiveDecision(action)),
7674
- outcome: decisionStyle(action.outcome),
7675
- latency: String(Math.round(action.latencyMs))
7676
- }))
8213
+ try {
8214
+ const environment = await api.get(
8215
+ `/v1/environments/${environmentId}`
7677
8216
  );
7678
- return;
7679
- }
7680
- for (const action of actions) renderAction(action);
7681
- }
7682
- function effectiveDecision(action) {
7683
- return action.shadowDecision ?? action.decision ?? "UNKNOWN";
7684
- }
7685
- function renderAction(action, coach = false) {
7686
- const decision = effectiveDecision(action);
7687
- const icon = decision === "ALLOW" ? symbol.pass : decision === "BLOCK" ? symbol.fail : symbol.warn;
7688
- const amount = action.amount === null ? "" : style.grey(` ${formatAmount(action.amount, action.currency)}`);
7689
- out(
7690
- `${icon} ${style.grey(new Date(action.startedAt).toISOString().slice(11, 19))} ${style.bold(action.toolKey ?? action.action)}${amount} ${decisionStyle(decision)} ${style.grey(`(${action.outcome})`)}`
7691
- );
7692
- if (action.denyReason) out(` ${action.denyReason}`);
7693
- const facts = [];
7694
- if (action.policyVersionNumber !== null) facts.push(`policy v${action.policyVersionNumber}`);
7695
- if (action.matchedRuleIds.length > 0)
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)}`);
8217
+ checks.push({
8218
+ name: "Enforcement mode",
8219
+ status: "pass",
8220
+ message: environment.gatewayMode === "ENFORCE" ? "ENFORCE \u2014 decisions are enforced" : `${environment.gatewayMode} \u2014 decisions are recorded but not enforced`
8221
+ });
8222
+ } catch {
8223
+ checks.push({ name: "Enforcement mode", status: "warn", message: "could not read" });
7705
8224
  }
8225
+ return checks;
7706
8226
  }
7707
- function coachingFor(action) {
7708
- const decision = effectiveDecision(action);
7709
- if (action.gatewayMode === "OBSERVE" && decision !== "ALLOW") {
7710
- return `In ENFORCE mode this would have been ${decision}. Simulate before switching.`;
7711
- }
7712
- switch (decision) {
7713
- case "BLOCK":
7714
- 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.";
7715
- case "REQUIRE_APPROVAL":
7716
- return "Call actWithApproval() so your agent waits for the reviewer instead of failing.";
7717
- case "LIMIT":
7718
- return "The amount was capped by policy. Read limits.appliedAmount from the decision.";
7719
- case "MASK":
7720
- return "Some fields were masked before reaching the tool. Check limits.maskedFields.";
7721
- case "STEP_UP_AUTH":
7722
- return "A human must re-authenticate. Agents cannot satisfy step-up on their own.";
7723
- default:
7724
- return null;
8227
+ function parseInitArgs(args) {
8228
+ const options = { force: false, skipPolicy: false, skipCode: false };
8229
+ for (let index = 0; index < args.length; index += 1) {
8230
+ const arg = args[index];
8231
+ const next = () => {
8232
+ const value = args[index + 1];
8233
+ if (value === void 0 || value.startsWith("-")) {
8234
+ throw new CliError(`Option ${String(arg)} requires a value.`);
8235
+ }
8236
+ index += 1;
8237
+ return value;
8238
+ };
8239
+ switch (arg) {
8240
+ case "--force":
8241
+ options.force = true;
8242
+ break;
8243
+ case "--skip-policy":
8244
+ options.skipPolicy = true;
8245
+ break;
8246
+ case "--skip-code":
8247
+ options.skipCode = true;
8248
+ break;
8249
+ case "--new-agent":
8250
+ options.newAgent = true;
8251
+ break;
8252
+ case "--control-plane":
8253
+ options.controlPlaneUrl = next();
8254
+ break;
8255
+ case "--gateway":
8256
+ options.gatewayUrl = next();
8257
+ break;
8258
+ case "--organization":
8259
+ options.organizationId = next();
8260
+ break;
8261
+ case "--workspace":
8262
+ options.workspaceId = next();
8263
+ break;
8264
+ case "--environment":
8265
+ options.environmentId = next();
8266
+ break;
8267
+ case "--name":
8268
+ options.agentName = next();
8269
+ break;
8270
+ case "--capability":
8271
+ options.capabilities = [...options.capabilities ?? [], next()];
8272
+ break;
8273
+ case "--approval-mode": {
8274
+ const value = next().toLowerCase();
8275
+ if (value !== "wait" && value !== "throw") {
8276
+ throw new CliError('--approval-mode must be "wait" or "throw".');
8277
+ }
8278
+ options.approvalMode = value;
8279
+ break;
8280
+ }
8281
+ default:
8282
+ if (arg?.startsWith("-")) {
8283
+ throw new CliError(`Unknown option for \`veefore init\`: ${arg}`);
8284
+ }
8285
+ }
7725
8286
  }
8287
+ return options;
7726
8288
  }
7727
- function formatAmount(minorUnits, currency) {
7728
- const major = (minorUnits / 100).toLocaleString("en-IN", {
7729
- minimumFractionDigits: 2,
7730
- maximumFractionDigits: 2
7731
- });
7732
- return currency ? `${major} ${currency}` : major;
7733
- }
7734
- async function streamActions(context, options) {
7735
- let interval = 1e3;
7736
- let stopped = false;
7737
- const stop = () => {
7738
- stopped = true;
7739
- };
7740
- process.once("SIGINT", stop);
7741
- process.once("SIGTERM", stop);
7742
- let blocked = 0;
7743
- let approvals = 0;
7744
- let allowed = 0;
7745
- while (!stopped) {
7746
- let fresh = [];
8289
+ async function projectHome(existing, flags) {
8290
+ const source = welcomeSource(flags);
8291
+ let last = 0;
8292
+ for (let round = 0; ; round += 1) {
8293
+ await beginScreen(source, { animate: round === 0 });
8294
+ const state = await agentState(existing);
8295
+ printProjectSummary(existing, state);
8296
+ const choices = homeChoices(existing, state, flags);
8297
+ choices.push({ label: "Exit", hint: "esc", run: async () => last, leaves: true });
8298
+ out();
8299
+ let picked;
7747
8300
  try {
7748
- const recent = await fetchActions(context, {
7749
- limit: 50,
7750
- decision: options.decision,
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))}`
8301
+ picked = await select(
8302
+ "What would you like to do?",
8303
+ choices.map((choice) => ({ label: choice.label, hint: choice.hint, value: choice }))
7758
8304
  );
7759
- interval = Math.min(interval * 2, 15e3);
7760
- await delay(interval);
7761
- continue;
8305
+ } catch (error) {
8306
+ if (error instanceof MenuCancelled) return last;
8307
+ throw error;
7762
8308
  }
7763
- for (const action of fresh) {
7764
- if (options.json) {
7765
- out(JSON.stringify(action));
7766
- } else {
7767
- renderAction(action, options.coach);
8309
+ if (picked.leaves) {
8310
+ await beginScreen(source, { compact: true });
8311
+ try {
8312
+ return await picked.run();
8313
+ } catch (error) {
8314
+ return error instanceof MenuCancelled ? last : reportError(error, flags);
7768
8315
  }
7769
- const decision = effectiveDecision(action);
7770
- if (decision === "BLOCK") blocked += 1;
7771
- else if (decision === "REQUIRE_APPROVAL") approvals += 1;
7772
- else allowed += 1;
7773
8316
  }
7774
- interval = fresh.length > 0 ? 1e3 : Math.min(interval * 1.5, 5e3);
7775
- await delay(interval);
8317
+ await beginScreen(source, { compact: true });
8318
+ try {
8319
+ last = await picked.run();
8320
+ } catch (error) {
8321
+ last = error instanceof MenuCancelled ? last : reportError(error, flags);
8322
+ }
8323
+ if (!isInteractive()) return last;
8324
+ await pressBack("Back to menu");
8325
+ }
8326
+ }
8327
+ function printProjectSummary(existing, state) {
8328
+ out();
8329
+ details([
8330
+ ["Agent", `${existing.agentName} (${existing.agentId})`],
8331
+ ["Environment", `${existing.environmentName} (${existing.environmentType})`],
8332
+ [
8333
+ "Status",
8334
+ state.kind === "ok" ? decisionStyle(state.status) : state.kind === "signed-out" ? style.amber("not signed in") : state.kind === "session-ended" ? style.amber("signed out") : state.kind === "missing" ? style.red("agent not found") : style.amber("Veefore not reachable")
8335
+ ]
8336
+ ]);
8337
+ if (state.kind === "session-ended") {
8338
+ out(`${symbol.warn} ${state.message}`);
7776
8339
  }
7777
- if (!options.json) {
8340
+ }
8341
+ function homeChoices(existing, state, flags) {
8342
+ const ended = state.kind === "missing" || state.kind === "ok" && ENDED_STATUSES.includes(state.status);
8343
+ if (ended) {
7778
8344
  out();
7779
8345
  out(
7780
- ` ${style.green(`${allowed} allowed`)} ${style.amber(`${approvals} needing approval`)} ${style.red(`${blocked} blocked`)}`
8346
+ `${symbol.warn} ${style.amber(
8347
+ state.kind === "missing" ? "This agent no longer exists in Veefore." : `This agent was ${state.kind === "ok" ? state.status.toLowerCase() : "removed"} in Veefore.`
8348
+ )} Every tool call wrapped in guard() is now refused.`
7781
8349
  );
7782
- out();
8350
+ return [
8351
+ {
8352
+ label: "Remove Veefore from this project",
8353
+ hint: "take guard() out of the code, disconnect",
8354
+ run: () => removeCommand([], flags, state),
8355
+ leaves: true
8356
+ },
8357
+ {
8358
+ label: "Set it up again with a new agent",
8359
+ hint: "the guarded code keeps working",
8360
+ run: () => initCommand(["--force"], flags),
8361
+ leaves: true
8362
+ }
8363
+ ];
7783
8364
  }
7784
- return 0;
8365
+ const choices = [];
8366
+ if (state.kind === "signed-out" || state.kind === "session-ended") {
8367
+ choices.push({
8368
+ label: "Sign in",
8369
+ hint: "veefore login",
8370
+ run: async () => {
8371
+ await login(existing.controlPlaneUrl);
8372
+ return 0;
8373
+ }
8374
+ });
8375
+ }
8376
+ choices.push(
8377
+ { label: "Watch decisions live", hint: "veefore dev", run: () => devCommand([], flags) },
8378
+ { label: "Check status", hint: "veefore status", run: () => statusCommand([], flags) },
8379
+ {
8380
+ label: "Protect tools in the code",
8381
+ hint: "veefore integrate",
8382
+ run: () => integrateCommand([], flags)
8383
+ },
8384
+ {
8385
+ label: "Check every tool is guarded",
8386
+ hint: "veefore integrate --check",
8387
+ run: () => integrateCommand(["--check"], flags)
8388
+ },
8389
+ { label: "Test enforcement", hint: "veefore test", run: () => testCommand([], flags) },
8390
+ {
8391
+ label: "Audit the security posture",
8392
+ hint: "veefore doctor",
8393
+ run: () => doctorCommand([], flags)
8394
+ },
8395
+ { label: "Turn on enforcement", hint: "veefore deploy", run: () => deployCommand([], flags) },
8396
+ {
8397
+ label: "Re-run setup for this project",
8398
+ hint: "veefore init --force",
8399
+ run: () => initCommand(["--force"], flags),
8400
+ leaves: true
8401
+ },
8402
+ {
8403
+ label: "Remove Veefore from this project",
8404
+ hint: "veefore remove",
8405
+ run: () => removeCommand([], flags, state),
8406
+ leaves: true
8407
+ }
8408
+ );
8409
+ if (state.kind === "ok" || state.kind === "unreachable") {
8410
+ choices.push({
8411
+ label: "Sign out",
8412
+ hint: "veefore logout",
8413
+ run: async () => {
8414
+ await logout(existing.controlPlaneUrl);
8415
+ return 0;
8416
+ }
8417
+ // Deliberately not `leaves`: signing out comes back to this menu, so the
8418
+ // panel and the status redraw as signed out and the next step on offer
8419
+ // is Sign in. Leaving instead would quit on the one action whose result
8420
+ // a person most wants to see confirmed.
8421
+ });
8422
+ }
8423
+ return choices;
7785
8424
  }
7786
- function delay(ms) {
7787
- return new Promise((resolve) => setTimeout(resolve, ms));
8425
+
8426
+ // src/commands/scan.ts
8427
+ async function scanCommand(_args, flags) {
8428
+ const scan = scanProject(process.cwd());
8429
+ if (flags.json) {
8430
+ out(JSON.stringify({ scan, plan: buildPlan(scan) }, null, 2));
8431
+ return 0;
8432
+ }
8433
+ heading("Veefore scan");
8434
+ out(
8435
+ `${symbol.pass} Framework: ${style.bold(scan.framework.name)} ${style.grey(`(${scan.framework.language}, ${scan.framework.evidence})`)}`
8436
+ );
8437
+ printScan(scan);
8438
+ out();
8439
+ out(style.grey("Nothing was changed. Run `veefore` to set this project up."));
8440
+ return 0;
7788
8441
  }
7789
8442
 
7790
8443
  // src/commands/policies.ts
@@ -8172,155 +8825,6 @@ async function diffPolicy(args, flags) {
8172
8825
  return 0;
8173
8826
  }
8174
8827
 
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
8828
  // src/help.ts
8325
8829
  var COMMANDS = [
8326
8830
  {
@@ -8600,8 +9104,78 @@ function wrap2(text, width) {
8600
9104
  return lines;
8601
9105
  }
8602
9106
 
9107
+ // src/home.ts
9108
+ async function homeCommand(args, flags) {
9109
+ const source = welcomeSource(flags);
9110
+ let first = true;
9111
+ for (; ; ) {
9112
+ if (readProjectConfig()) return initCommand(args, flags);
9113
+ await beginScreen(source, { animate: first });
9114
+ first = false;
9115
+ out();
9116
+ out(style.bold("Let's get started."));
9117
+ out();
9118
+ let action;
9119
+ try {
9120
+ action = await select("What would you like to do?", [
9121
+ { label: "Set up this project", hint: "recommended \xB7 veefore init", value: "setup" },
9122
+ {
9123
+ label: "Preview what setup would find",
9124
+ hint: "veefore scan \xB7 changes nothing",
9125
+ value: "scan"
9126
+ },
9127
+ { label: "Sign in", hint: "veefore login", value: "login" },
9128
+ { label: "Show every command", hint: "veefore help", value: "help" },
9129
+ { label: "Exit", hint: "esc", value: "exit" }
9130
+ ]);
9131
+ } catch (error) {
9132
+ if (error instanceof MenuCancelled) return 0;
9133
+ throw error;
9134
+ }
9135
+ if (action === "exit") return 0;
9136
+ if (action === "setup") {
9137
+ await runOnScreen(source, flags, () => initCommand(args, flags), {
9138
+ // Setup done: "Continue", and the next pass opens the project home
9139
+ // with the panel filled in. Otherwise "Back to menu", to read why and
9140
+ // choose again.
9141
+ label: () => readProjectConfig() ? "Continue" : "Back to menu"
9142
+ });
9143
+ continue;
9144
+ }
9145
+ const code = await runOnScreen(source, flags, async () => {
9146
+ switch (action) {
9147
+ case "scan":
9148
+ return scanCommand([], flags);
9149
+ case "login":
9150
+ await login(resolveControlPlaneUrl(flags.controlPlaneUrl));
9151
+ return 0;
9152
+ case "help":
9153
+ printRootHelp(source().version);
9154
+ return 0;
9155
+ }
9156
+ return 0;
9157
+ });
9158
+ if (!isInteractive()) return code;
9159
+ }
9160
+ }
9161
+ async function initWithWelcome(args, flags) {
9162
+ await beginScreen(welcomeSource(flags), { animate: true });
9163
+ return initCommand(args, flags);
9164
+ }
9165
+ async function runOnScreen(source, flags, run2, options = {}) {
9166
+ await beginScreen(source, { compact: true });
9167
+ let code = 0;
9168
+ try {
9169
+ code = await run2();
9170
+ } catch (error) {
9171
+ code = error instanceof MenuCancelled ? 0 : reportError(error, flags);
9172
+ }
9173
+ const label = options.label ?? "Back";
9174
+ await pressBack(typeof label === "function" ? label() : label);
9175
+ return code;
9176
+ }
9177
+
8603
9178
  // src/index.ts
8604
- var CLI_VERSION = "1.0.0";
8605
9179
  async function run(argv) {
8606
9180
  const { flags, rest } = parseGlobalFlags(argv);
8607
9181
  if (rest.includes("--version") || rest.includes("-V")) {
@@ -8610,9 +9184,9 @@ async function run(argv) {
8610
9184
  }
8611
9185
  const [command, ...args] = rest.filter((arg) => arg !== "--help" && arg !== "-h");
8612
9186
  const wantsHelp = rest.includes("--help") || rest.includes("-h");
8613
- if (command === void 0 && !wantsHelp && process.stdin.isTTY && process.stdout.isTTY) {
9187
+ if (command === void 0 && !wantsHelp && isInteractive()) {
8614
9188
  try {
8615
- return await initCommand(args, flags);
9189
+ return flags.json ? await initCommand(args, flags) : await homeCommand(args, flags);
8616
9190
  } catch (error) {
8617
9191
  return handleError(error, flags);
8618
9192
  }
@@ -8632,7 +9206,7 @@ async function run(argv) {
8632
9206
  switch (command) {
8633
9207
  case "init":
8634
9208
  case "setup":
8635
- return await initCommand(args, flags);
9209
+ return flags.json || !isInteractive() ? await initCommand(args, flags) : await initWithWelcome(args, flags);
8636
9210
  case "scan":
8637
9211
  return await scanCommand(args, flags);
8638
9212
  case "integrate":
@@ -8690,28 +9264,8 @@ async function run(argv) {
8690
9264
  }
8691
9265
  }
8692
9266
  function handleError(error, flags) {
8693
- if (error instanceof CliError) {
8694
- err();
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;
9267
+ if (error instanceof MenuCancelled) return error.exitCode;
9268
+ return reportError(error, flags);
8715
9269
  }
8716
9270
  function parseGlobalFlags(argv) {
8717
9271
  const flags = { json: false, yes: false, verbose: false };