veefore 1.0.0 → 1.1.0

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