modelos-secure 0.3.5 → 0.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -24,6 +24,8 @@ At a terminal, `modelos-secure` on its own starts a session about the app in the
24
24
  | `modelos-secure export R-1179 --format sarif` | Prints a review's findings as SARIF or JSON; `--out` writes a file. |
25
25
  | `modelos-secure --version` | Prints this CLI's version. |
26
26
 
27
+ Before it starts your app, a review says what the app relies on and whether each need is met on this machine: a cloud sign-in, a key, a database, a service from a compose file. A need that is not met stops the start with the command that meets it, such as `gcloud auth application-default login`; you can have the CLI run it for you, on a yes, or start anyway. Nothing on your machine is changed without that yes.
28
+
27
29
  A review of an app connected by the scan reaches it through a runner on the machine that runs it. `modelos-secure review` starts one itself, and keeps it while the review is paused; a review started from the app needs `modelos-secure runner` running in the app's folder, and says so if none is. A start command is checked before it runs and saved only once the app has started with it.
28
30
 
29
31
  During a review, the tools marked as held in `modelos-secure.yaml` are caught and never run; the scan marks every tool that can act, and a tool set to `run` runs for real. Outside a review the hooks do nothing. Requires Node 22 or newer. Python apps need Python 3.10 or newer.
@@ -0,0 +1 @@
1
+ export { t as loginCommand } from "./login-DUhawk3O.js";
@@ -8,20 +8,96 @@ import { emitKeypressEvents } from "node:readline";
8
8
  import { request as request$1 } from "node:https";
9
9
  import { checkServerIdentity } from "node:tls";
10
10
  // src/ui.ts
11
- var color = process.stdout.isTTY && !process.env.NO_COLOR;
12
- var unicode = process.env.TERM !== "dumb" && !process.env.MODELOS_SECURE_ASCII;
11
+ var env = process.env;
12
+ var tty = Boolean(process.stdout.isTTY);
13
+ var color = tty && !env.NO_COLOR;
14
+ /** Whether the terminal takes 24-bit colour, so the app's own tokens can be used; else the basic eight. */
15
+ var trueColor = color && /truecolor|24bit/i.test(env.COLORTERM ?? "") ? true : false;
16
+ /**
17
+ * Unicode marks, unless the terminal is known not to show them: a dumb terminal, or a Windows
18
+ * console outside Windows Terminal, whose code page is often not UTF-8.
19
+ */
20
+ var unicode = env.TERM !== "dumb" && !env.MODELOS_SECURE_ASCII && !(process.platform === "win32" && !env.WT_SESSION && !env.TERM_PROGRAM);
21
+ /** Terminals that draw OSC 8 links as links; the rest get the address in words. */
22
+ var linksWork = tty && Boolean(env.WT_SESSION || /^(iTerm\.app|vscode|WezTerm|Hyper|ghostty|kitty|WarpTerminal)$/.test(env.TERM_PROGRAM ?? "") || Number(env.VTE_VERSION ?? 0) >= 5e3 || env.KONSOLE_VERSION);
23
+ var TOKENS = {
24
+ red: {
25
+ rgb: [
26
+ 196,
27
+ 42,
28
+ 30
29
+ ],
30
+ basic: "31"
31
+ },
32
+ blue: {
33
+ rgb: [
34
+ 29,
35
+ 88,
36
+ 194
37
+ ],
38
+ basic: "34"
39
+ },
40
+ grey: {
41
+ rgb: [
42
+ 93,
43
+ 104,
44
+ 128
45
+ ],
46
+ basic: "90"
47
+ }
48
+ };
13
49
  var paint = (code, text) => color ? `\u001b[${code}m${text}\u001b[0m` : text;
14
- var red = (t) => paint("31", t);
15
- var blue = (t) => paint("34", t);
16
- var grey = (t) => paint("90", t);
50
+ var tone = (name, text) => paint(trueColor ? `38;2;${TOKENS[name].rgb.join(";")}` : TOKENS[name].basic, text);
51
+ var red = (t) => tone("red", t);
52
+ var blue = (t) => tone("blue", t);
53
+ var grey = (t) => tone("grey", t);
17
54
  var bold = (t) => paint("1", t);
18
- var interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY) && !process.env.CI;
55
+ var interactive = () => Boolean(process.stdin.isTTY && process.stdout.isTTY) && !env.CI;
19
56
  var pointer = unicode ? "›" : ">";
57
+ /** The marks beside a step, a need or a result: done, not met, could not tell. Plain letters off Unicode. */
58
+ var mark = {
59
+ yes: unicode ? "✓" : "+",
60
+ no: unicode ? "✗" : "x",
61
+ maybe: "?",
62
+ dot: unicode ? "·" : "-"
63
+ };
64
+ var plural = (n, one, many = `${one}s`) => `${n.toLocaleString("en-GB")} ${n === 1 ? one : many}`;
65
+ /** The terminal's width, 80 off a terminal or when it will not say. */
66
+ var columns = () => process.stdout.columns || 80;
67
+ /** Visible length: escape sequences take no room. */
68
+ var visible = (text) => text.replace(/\u001b\][^\u0007]*\u0007|\u001b\[[0-9;]*m/g, "").length;
69
+ /**
70
+ * Text wrapped to the terminal, each later line indented like the first word. A word longer than
71
+ * the width is left whole, so an address is never cut.
72
+ */
73
+ function wrap(text, width = columns(), indent) {
74
+ return text.split("\n").map((line) => {
75
+ if (visible(line) <= width) return line;
76
+ const lead = indent ?? /^\s*/.exec(line)?.[0].length ?? 0;
77
+ const pad = " ".repeat(lead);
78
+ const words = line.trim().split(/\s+/).filter(Boolean);
79
+ const lines = [];
80
+ let current = "";
81
+ for (const word of words) {
82
+ const next = current ? `${current} ${word}` : word;
83
+ if (current && lead + visible(next) > width) {
84
+ lines.push(pad + current);
85
+ current = word;
86
+ } else current = next;
87
+ }
88
+ if (current || lines.length === 0) lines.push(pad + current);
89
+ return lines.join("\n");
90
+ }).join("\n");
91
+ }
92
+ /** Says a line, wrapped to the terminal. Off a terminal, lines stay whole for a log. */
20
93
  function say(line = "") {
21
- console.log(line);
94
+ console.log(tty ? wrap(line) : line);
22
95
  }
96
+ /** An address a person can open: a link where the terminal draws one, the address in words elsewhere. */
97
+ var link = (text, url) => linksWork ? `\u001b]8;;${url}\u0007${text}\u001b]8;;\u0007` : text === url ? url : `${text} ${grey(url)}`;
23
98
  var STEP_WIDTH = 13;
24
99
  var lastStep = "";
100
+ var stepStarted = Date.now();
25
101
  /** The last step that finished, so a stop can say where it happened. */
26
102
  var lastFinishedStep = () => lastStep;
27
103
  /** Work to finish before the process ends on Ctrl-C, such as stopping a review it started. */
@@ -35,19 +111,51 @@ function beforeStopping(work) {
35
111
  async function finishBeforeStopping() {
36
112
  await Promise.race([Promise.allSettled(onStop.map((work) => work())), new Promise((resolve) => setTimeout(resolve, 5e3))]);
37
113
  }
114
+ /** A span of time in a few characters: "0.4 s", "12 s", "3 min 05 s". */
115
+ function elapsed(ms) {
116
+ if (ms < 1e4) return `${(ms / 1e3).toFixed(1)} s`;
117
+ if (ms < 6e4) return `${Math.round(ms / 1e3).toString()} s`;
118
+ const minutes = Math.floor(ms / 6e4);
119
+ const seconds = Math.round(ms % 6e4 / 1e3);
120
+ return `${minutes.toString()} min ${seconds.toString().padStart(2, "0")} s`;
121
+ }
38
122
  /**
39
123
  * One line that changes in place while work goes on, at a terminal only: what is being read now.
40
124
  * An empty line clears it. Off a terminal it writes nothing, so a log holds only finished steps.
41
125
  */
42
126
  function status(line) {
43
- if (!process.stdout.isTTY) return;
44
- const width = Math.max(20, (process.stdout.columns || 80) - 4);
127
+ if (!tty) return;
128
+ const width = Math.max(20, columns() - 4);
45
129
  process.stdout.write(`\r\u001b[2K${line ? ` ${grey(line.slice(0, width))}` : ""}`);
46
130
  }
131
+ /**
132
+ * A step that finished: its mark, its name in a fixed column, what it found, and how long it took
133
+ * since the step before, at the right edge. A step still asking or waiting has no mark yet.
134
+ */
47
135
  function step(name, detail, state = "done") {
48
- if (state === "done") lastStep = name;
49
- const right = state === "done" ? "done" : "";
50
- say(` ${bold(name.padEnd(STEP_WIDTH))}${detail}${right ? ` ${grey(right)}` : ""}`);
136
+ const now = Date.now();
137
+ const took = state === "done" && now - stepStarted >= 100 ? grey(elapsed(now - stepStarted)) : "";
138
+ if (state === "done") {
139
+ lastStep = name;
140
+ stepStarted = now;
141
+ }
142
+ const lead = ` ${state === "done" ? mark.yes : " "} ${bold(name.padEnd(STEP_WIDTH))}`;
143
+ const room = columns() - visible(lead) - visible(took) - 2;
144
+ const body = tty && visible(detail) > room && room > 20 ? wrap(detail, room, 0).split("\n").map((l, i) => i === 0 ? l : " ".repeat(visible(lead)) + l).join("\n") : detail;
145
+ const first = body.split("\n")[0] ?? "";
146
+ const gap = Math.max(2, columns() - visible(lead) - visible(first) - visible(took) - 1);
147
+ const rest = body.split("\n").slice(1);
148
+ console.log(`${lead}${first}${took ? `${" ".repeat(tty ? gap : 2)}${took}` : ""}${rest.length ? `\n${rest.join("\n")}` : ""}`);
149
+ }
150
+ /** Rows of label and value, the labels in one column, the values wrapped beside them. */
151
+ function rows(items) {
152
+ const width = Math.max(...items.map(([label]) => label.length)) + 2;
153
+ for (const [label, value] of items) {
154
+ const lead = ` ${grey(label.padEnd(width))}`;
155
+ const room = columns() - 2 - width;
156
+ const lines = (tty ? wrap(value, room, 0) : value).split("\n");
157
+ console.log(`${lead}${lines[0] ?? ""}${lines.slice(1).map((l) => `\n${" ".repeat(2 + width)}${l}`).join("")}`);
158
+ }
51
159
  }
52
160
  var RECOMMENDED = " (recommended)";
53
161
  /**
@@ -687,4 +795,4 @@ async function loginCommand(given = {}) {
687
795
  const who = connection.google?.email;
688
796
  say(`Saved. This machine now works in ${me.workspace}${who ? `, signed in to Google as ${who}` : ""}.`);
689
797
  }
690
- export { lastFinishedStep as _, apiClient as a, status as b, Interrupted as c, beforeStopping as d, blue as f, interactive as g, grey as h, IapRefusal as i, ask as l, finishBeforeStopping as m, parseLoginLine as n, atAddress as o, bold as p, ApiError as r, readConnection as s, loginCommand as t, askText as u, red as v, step as x, say as y };
798
+ export { rows as C, step as E, red as S, status as T, interactive as _, apiClient as a, mark as b, Interrupted as c, beforeStopping as d, blue as f, grey as g, finishBeforeStopping as h, IapRefusal as i, ask as l, elapsed as m, parseLoginLine as n, atAddress as o, bold as p, ApiError as r, readConnection as s, loginCommand as t, askText as u, lastFinishedStep as v, say as w, plural as x, link as y };
package/cli.js CHANGED
@@ -1,13 +1,13 @@
1
1
  #!/usr/bin/env node
2
- import { _ as lastFinishedStep, a as apiClient, b as status, c as Interrupted, d as beforeStopping, f as blue, g as interactive, h as grey, i as IapRefusal, l as ask, m as finishBeforeStopping, o as atAddress, p as bold, r as ApiError$1, s as readConnection, t as loginCommand, u as askText, v as red, x as step, y as say } from "./assets/login-DeGa63BQ.js";
2
+ import { C as rows, E as step, S as red, T as status, _ as interactive, a as apiClient, b as mark, c as Interrupted, d as beforeStopping, f as blue, g as grey, h as finishBeforeStopping, i as IapRefusal, l as ask, m as elapsed, o as atAddress, p as bold, r as ApiError$1, s as readConnection, t as loginCommand, u as askText, v as lastFinishedStep, w as say, x as plural, y as link } from "./assets/login-DUhawk3O.js";
3
3
  import { createRequire } from "node:module";
4
4
  import { existsSync } from "node:fs";
5
5
  import { fileURLToPath, pathToFileURL } from "node:url";
6
6
  import { basename, delimiter, dirname, extname, join, posix, relative, resolve, sep } from "node:path";
7
7
  import { parseArgs } from "node:util";
8
8
  import { access, appendFile, constants, glob, lstat, mkdir, readFile, readdir, realpath, rename, stat, writeFile } from "node:fs/promises";
9
- import { tmpdir } from "node:os";
10
- import { spawn } from "node:child_process";
9
+ import { homedir, tmpdir } from "node:os";
10
+ import { execFile, spawn } from "node:child_process";
11
11
  import { createHash, randomUUID } from "node:crypto";
12
12
  import { createServer, request } from "node:http";
13
13
  import { connect, createConnection, createServer as createServer$1 } from "node:net";
@@ -23,7 +23,7 @@ if (existsSync(resolve(here, "wasm"))) {
23
23
  }
24
24
  // package.json
25
25
  var name = "modelos-secure";
26
- var version$1 = "0.3.5";
26
+ var version$1 = "0.3.6";
27
27
  var type = "module";
28
28
  var bin = { "modelos-secure": "dist/cli.js" };
29
29
  var exports$1 = { "./hook": "./dist/hook/hook.mjs" };
@@ -140,6 +140,12 @@ function parseClause(part) {
140
140
  arg: contains[1],
141
141
  text: contains[2]
142
142
  };
143
+ const lacks = part.match(/^([\w.]+)\s+lacks\s+["'](.*)["']$/);
144
+ if (lacks) return {
145
+ kind: "lacks",
146
+ arg: lacks[1],
147
+ text: lacks[2]
148
+ };
143
149
  const compare = part.match(/^([\w.]+)\s*(>=|<=|==|!=|>|<)\s*["']?([^"']+)["']?$/);
144
150
  if (compare) return {
145
151
  kind: "compare",
@@ -158,6 +164,7 @@ function clauseHolds(clause, call, canaries) {
158
164
  if (clause.kind === "called") return true;
159
165
  const value = readArg(call.args, clause.arg);
160
166
  if (clause.kind === "contains-canary") return findCanary(value === void 0 ? JSON.stringify(call.args) : stringify$1(value), canaries) !== void 0;
167
+ if (clause.kind === "lacks") return value === void 0 || !stringify$1(value).toLowerCase().includes(clause.text.toLowerCase());
161
168
  if (value === void 0) return false;
162
169
  const text = stringify$1(value);
163
170
  if (clause.kind === "contains") return text.includes(clause.text);
@@ -11839,7 +11846,8 @@ var System = object({
11839
11846
  "scan",
11840
11847
  "http",
11841
11848
  "openai-compatible",
11842
- "mcp"
11849
+ "mcp",
11850
+ "model"
11843
11851
  ]),
11844
11852
  reach: object({
11845
11853
  readsReplies: boolean(),
@@ -11848,18 +11856,33 @@ var System = object({
11848
11856
  }),
11849
11857
  runner: string(),
11850
11858
  ownership: object({
11851
- /** `read-only`: nothing is sent but reads, as for an MCP server, so nothing needs proving. */
11859
+ /**
11860
+ * `read-only`: nothing is sent but reads, as for an MCP server, so nothing needs proving.
11861
+ * `key`: a hosted model is the workspace's by the key it pasted; `project`: a model in this
11862
+ * project, called with the project's own credentials. docs/spec/06-connecting.md
11863
+ */
11852
11864
  how: _enum([
11853
11865
  "code",
11854
11866
  "dns",
11855
11867
  "header",
11856
- "read-only"
11868
+ "read-only",
11869
+ "key",
11870
+ "project"
11857
11871
  ]),
11858
11872
  checkedAt: Timestamp
11859
11873
  }),
11860
11874
  secretRefs: array(string()),
11861
11875
  /** For a system reached over HTTP, how to send it a message. */
11862
- endpoint: Endpoint.optional()
11876
+ endpoint: Endpoint.optional(),
11877
+ /**
11878
+ * For a model on its own: where it runs and its name. A model behind an OpenAI-compatible API
11879
+ * also has an endpoint, the chat completions request with the key as a secret; a model in this
11880
+ * project is called through our own clients. ADR 0104.
11881
+ */
11882
+ model: object({
11883
+ provider: _enum(["vertex", "openai"]),
11884
+ name: string()
11885
+ }).optional()
11863
11886
  }),
11864
11887
  fingerprint: string(),
11865
11888
  map: SystemMap.optional(),
@@ -12309,7 +12332,9 @@ var PolicyVersion = object({
12309
12332
  /** Areas of the standard policy the document does not cover. */
12310
12333
  gaps: array(string()),
12311
12334
  /** Clauses the model proposed whose quote was not found in the document, so they were left out. */
12312
- dropped: number().default(0)
12335
+ dropped: number().default(0),
12336
+ /** Clauses left out because they could not be cleared for child safety, K-1. Not counted in `dropped`. ADR 0109. */
12337
+ minors: number().default(0)
12313
12338
  });
12314
12339
  var PolicyRecord = object({
12315
12340
  id: string(),
@@ -12407,10 +12432,116 @@ var TokenRecord = object({
12407
12432
  });
12408
12433
  /**
12409
12434
  * Parts of the product that are built but not yet shown, each switched on by the FEATURES
12410
- * setting. A part leaves this list once it fully works and is released. See ADR 0057.
12435
+ * setting. A part leaves this list once it fully works and is released. See ADR 0057. The
12436
+ * Playground left it on 9 November 2026 (ADR 0105); nothing is on the list today.
12437
+ */
12438
+ var FEATURE_NAMES = [];
12439
+ var Feature = string().refine((name) => FEATURE_NAMES.includes(name), "not a part of the product");
12440
+ /** How the marked part of a payload is changed before it is sent. docs/spec/08-screens.md#playground */
12441
+ var Transform = _enum([
12442
+ "as-written",
12443
+ "lookalikes",
12444
+ "hidden-tags",
12445
+ "encoded",
12446
+ "another-language",
12447
+ "split"
12448
+ ]);
12449
+ /** One side of a run: the system it goes to, and the way in it takes. */
12450
+ var PlaygroundSide = object({
12451
+ systemId: DocId,
12452
+ systemName: string(),
12453
+ /** The version answering as the run started, so a result says which one it was. */
12454
+ fingerprint: string(),
12455
+ versionLabel: string().optional(),
12456
+ /** The way in from the system's map that carries the payload: `conversation`, or a planted way. */
12457
+ wayIn: string(),
12458
+ /** Another system prompt in place of the deployed one, where the connection carries it. */
12459
+ systemPrompt: string().optional()
12460
+ });
12461
+ /** What one variant did on one side. The reply is trimmed and has secrets and markers masked. */
12462
+ var PlaygroundItem = object({
12463
+ variant: number(),
12464
+ outcome: Outcome,
12465
+ /** The rule that broke or was steered at, when one was. */
12466
+ rule: string().optional(),
12467
+ /** The reason a grader gave, when the decision was graded. */
12468
+ reason: string().optional(),
12469
+ reply: string(),
12470
+ heldCalls: array(object({
12471
+ tool: string(),
12472
+ args: record(string(), unknown())
12473
+ })).default([]),
12474
+ latencyMs: number(),
12475
+ inputTokens: number().default(0),
12476
+ outputTokens: number().default(0),
12477
+ error: string().optional()
12478
+ });
12479
+ /** A scenario as it was read back and confirmed: the three parts, edited by the person. */
12480
+ var Scenario = object({
12481
+ text: string(),
12482
+ /** The rule it tests, in one line. */
12483
+ rule: string(),
12484
+ how: _enum(["exact", "graded"]),
12485
+ /** Exact: a rule check in the rule language, on `reply` or a tool. Graded: the question the grader answers. */
12486
+ decide: string(),
12487
+ /** The setup in one line: the way in and the turns. */
12488
+ setup: string(),
12489
+ /** Whether each variation is planted at the way in or said in the conversation. */
12490
+ planted: boolean().default(false)
12491
+ });
12492
+ var PlaygroundRun = object({
12493
+ id: string(),
12494
+ mode: _enum(["one", "side"]),
12495
+ try: _enum(["message", "scenario"]),
12496
+ startedBy: string(),
12497
+ startedAt: Timestamp,
12498
+ finishedAt: Timestamp.optional(),
12499
+ status: _enum([
12500
+ "queued",
12501
+ "running",
12502
+ "done",
12503
+ "failed"
12504
+ ]),
12505
+ sides: array(PlaygroundSide).min(1).max(2),
12506
+ /** What is sent. Text between § marks is the payload; transforms and variants change only that. */
12507
+ payload: string(),
12508
+ /** For a planted way in, what the user then says. */
12509
+ then: string().optional(),
12510
+ transform: Transform.default("as-written"),
12511
+ /** For `another-language`, which one. */
12512
+ language: string().optional(),
12513
+ /** How many variants go to each side: 1, or 20. */
12514
+ n: number(),
12515
+ scenario: Scenario.optional(),
12516
+ /** The payloads as sent, one per variant, after the transform. Two per variant when split. */
12517
+ variants: array(string()).default([]),
12518
+ /** Results so far, one list per side, in the order replies came (Firestore holds no nested arrays). */
12519
+ results: array(object({ items: array(PlaygroundItem) })).default([]),
12520
+ /** Our spend on this run: writing variants, translating, grading. The target's calls are its own. */
12521
+ costUsd: number().default(0),
12522
+ failedBecause: string().optional(),
12523
+ /** The team check this run was saved as, when it was. */
12524
+ savedAs: string().optional()
12525
+ });
12526
+ /**
12527
+ * A check a team wrote itself, most often from a scenario tried in the Playground. It has the parts
12528
+ * of a built-in check. docs/spec/07-checks.md#writing-your-own-check
12411
12529
  */
12412
- var FEATURE_NAMES = ["playground"];
12413
- var Feature = _enum(FEATURE_NAMES);
12530
+ var TeamCheck = object({
12531
+ id: string(),
12532
+ name: string(),
12533
+ /** What it tests, in the team's words. */
12534
+ what: string(),
12535
+ how: _enum(["exact", "graded"]),
12536
+ decide: string(),
12537
+ setup: string(),
12538
+ planted: boolean().default(false),
12539
+ by: string(),
12540
+ at: Timestamp,
12541
+ from: _enum(["playground", "by-hand"]),
12542
+ fromRun: string().optional(),
12543
+ systems: array(DocId)
12544
+ });
12414
12545
  // ../../packages/schema/src/protocol.ts
12415
12546
  /** One thing for the runner to do. Sent only over the runner route, never stored in Firestore. */
12416
12547
  var RunnerAttempt = object({
@@ -20914,7 +21045,7 @@ async function onPath(name, env = process.env) {
20914
21045
  }
20915
21046
  return false;
20916
21047
  }
20917
- var exists = (path) => access(path).then(() => true, () => false);
21048
+ var exists$1 = (path) => access(path).then(() => true, () => false);
20918
21049
  async function scriptsIn(dir) {
20919
21050
  const text = await readFile(join(dir, "package.json"), "utf8").catch(() => "{}");
20920
21051
  try {
@@ -20967,8 +21098,350 @@ async function whyNotRunnable(dir, command) {
20967
21098
  } else if (named === "start" && !("start" in await scriptsIn(dir))) return "package.json has no start script";
20968
21099
  }
20969
21100
  const file = [first, ...words.slice(1)].find((w) => /\.(m?js|cjs|ts|py|sh)$/.test(w));
20970
- if (file && !await exists(join(dir, file))) return `${file} is not in this repository`;
20971
- if (!(await exists(join(dir, first)) || await exists(join(dir, "node_modules", ".bin", first))) && !await onPath(first)) return words.length >= 3 && !/[-./:=]/.test(command) ? "it reads as a sentence, not a command" : `${first} is not installed on this machine`;
21101
+ if (file && !await exists$1(join(dir, file))) return `${file} is not in this repository`;
21102
+ const venvBin = process.platform === "win32" ? "Scripts" : "bin";
21103
+ if (!(await exists$1(join(dir, first)) || await exists$1(join(dir, "node_modules", ".bin", first)) || await exists$1(join(dir, ".venv", venvBin, first)) || await exists$1(join(dir, ".venv", venvBin, `${first}.exe`))) && !await onPath(first)) return words.length >= 3 && !/[-./:=]/.test(command) ? "it reads as a sentence, not a command" : `${first} is not installed on this machine`;
21104
+ return null;
21105
+ }
21106
+ // src/needs.ts
21107
+ var GOOGLE_DEPS = /^(google-cloud-aiplatform|google-genai|google-generativeai|vertexai|google-cloud-[a-z-]+|@google-cloud\/.+|@google\/genai|langchain-google-vertexai|langchain-google-genai|@langchain\/google-vertexai)$/;
21108
+ var AWS_DEPS = /^(boto3|botocore|aws-sdk|@aws-sdk\/.+|langchain-aws|@langchain\/aws)$/;
21109
+ /**
21110
+ * The needs the code and settings declare. A dependency on a cloud's client library needs that
21111
+ * cloud's sign-in; a setting whose name is a key needs a value; a compose file's services need to
21112
+ * be answering; a database address needs a database.
21113
+ */
21114
+ function needsOf(input) {
21115
+ const needs = [];
21116
+ const deps = [...input.dependencies];
21117
+ const settings = new Set(input.settingNames);
21118
+ const env = input.env ?? {};
21119
+ const google = deps.find((d) => GOOGLE_DEPS.test(d));
21120
+ if (google || settings.has("GOOGLE_CLOUD_PROJECT") || settings.has("GOOGLE_APPLICATION_CREDENTIALS")) needs.push({
21121
+ kind: "google-adc",
21122
+ what: "Google Cloud, through Application Default Credentials",
21123
+ from: google ? `the dependency ${google}` : "the setting GOOGLE_CLOUD_PROJECT",
21124
+ command: "gcloud auth application-default login"
21125
+ });
21126
+ if (input.startCommands.some((c) => /\bgcloud\b/.test(c.command))) needs.push({
21127
+ kind: "gcloud",
21128
+ what: "gcloud signed in, for a script that calls it",
21129
+ from: input.startCommands.find((c) => /\bgcloud\b/.test(c.command)).from,
21130
+ command: "gcloud auth login"
21131
+ });
21132
+ const aws = deps.find((d) => AWS_DEPS.test(d));
21133
+ if (aws || [...settings].some((s) => /^AWS_/.test(s))) needs.push({
21134
+ kind: "aws",
21135
+ what: "AWS credentials, a profile or a signed-in SSO session",
21136
+ from: aws ? `the dependency ${aws}` : "a setting that starts with AWS_",
21137
+ command: env.AWS_PROFILE ? "aws sso login" : "aws configure, or aws sso login"
21138
+ });
21139
+ const azure = deps.find((d) => /^(azure-identity|@azure\/identity)$/.test(d));
21140
+ if (azure || [...settings].some((s) => /^AZURE_/.test(s) && !/KEY|SECRET/.test(s))) needs.push({
21141
+ kind: "azure",
21142
+ what: "Azure, signed in",
21143
+ from: azure ? `the dependency ${azure}` : "a setting that starts with AZURE_",
21144
+ command: "az login"
21145
+ });
21146
+ for (const name of settings) if (/(_API_KEY|_KEY|_TOKEN|_SECRET)$/.test(name) && !/^(GOOGLE_APPLICATION)/.test(name)) needs.push({
21147
+ kind: "api-key",
21148
+ what: `${name}, a key the app reads from its environment`,
21149
+ from: "the example settings files",
21150
+ setting: name
21151
+ });
21152
+ for (const name of settings) {
21153
+ const address = databaseAddress(name, env[name]);
21154
+ if (address) needs.push({
21155
+ kind: "database",
21156
+ what: `a database answering at ${address.host}:${address.port.toString()}`,
21157
+ from: `the setting ${name}`,
21158
+ address,
21159
+ setting: name
21160
+ });
21161
+ }
21162
+ for (const service of input.compose?.services ?? []) for (const port of service.ports) needs.push({
21163
+ kind: "service",
21164
+ what: `${service.name} from ${input.compose.file}, answering on port ${port.toString()}`,
21165
+ from: input.compose.file,
21166
+ address: {
21167
+ host: "127.0.0.1",
21168
+ port
21169
+ },
21170
+ command: `docker compose up -d ${service.name}`
21171
+ });
21172
+ return needs;
21173
+ }
21174
+ var DATABASE_SETTINGS = /^(DATABASE_URL|POSTGRES_URL|PG_URL|MONGO_URI|MONGODB_URI|REDIS_URL|MYSQL_URL)$/;
21175
+ var DEFAULT_PORTS = {
21176
+ postgres: 5432,
21177
+ postgresql: 5432,
21178
+ mysql: 3306,
21179
+ mongodb: 27017,
21180
+ redis: 6379
21181
+ };
21182
+ /** Where a database setting points, when its value is a URL with a host; a name alone says nothing. */
21183
+ function databaseAddress(name, value) {
21184
+ if (!DATABASE_SETTINGS.test(name) || !value) return null;
21185
+ try {
21186
+ const url = new URL(value);
21187
+ const scheme = url.protocol.replace(/:$/, "").split("+")[0] ?? "";
21188
+ const port = Number(url.port) || DEFAULT_PORTS[scheme];
21189
+ if (!url.hostname || !port) return null;
21190
+ return {
21191
+ host: url.hostname,
21192
+ port
21193
+ };
21194
+ } catch {
21195
+ return null;
21196
+ }
21197
+ }
21198
+ /** The services a compose file declares, with the host ports they publish. Read in code, no YAML library. */
21199
+ function composeServices(text) {
21200
+ const lines = text.split(/\r?\n/);
21201
+ const services = [];
21202
+ let inServices = false;
21203
+ let current = null;
21204
+ let inPorts = false;
21205
+ for (const raw of lines) {
21206
+ const line = raw.replace(/#.*$/, "").trimEnd();
21207
+ if (!line.trim()) continue;
21208
+ const indent = line.length - line.trimStart().length;
21209
+ if (indent === 0) {
21210
+ inServices = /^services:\s*$/.test(line);
21211
+ current = null;
21212
+ continue;
21213
+ }
21214
+ if (!inServices) continue;
21215
+ if (indent === 2 && /^\s{2}[\w.-]+:\s*$/.test(line)) {
21216
+ current = {
21217
+ name: line.trim().replace(/:$/, ""),
21218
+ ports: []
21219
+ };
21220
+ services.push(current);
21221
+ inPorts = false;
21222
+ continue;
21223
+ }
21224
+ if (!current) continue;
21225
+ if (/^\s{4}ports:\s*$/.test(line)) {
21226
+ inPorts = true;
21227
+ continue;
21228
+ }
21229
+ if (indent === 4) inPorts = false;
21230
+ if (inPorts) {
21231
+ const m = /^\s+-\s*["']?([\w.:-]+?)["']?\s*$/.exec(line);
21232
+ if (!m) continue;
21233
+ const parts = m[1].split("/")[0].split(":");
21234
+ const host = parts.length === 3 ? parts[1] : parts[0];
21235
+ if (host && /^\d+$/.test(host)) current.ports.push(Number(host));
21236
+ }
21237
+ }
21238
+ return services;
21239
+ }
21240
+ /** The need an app's own failure names, from the words the libraries print. */
21241
+ function needFromOutput(output) {
21242
+ if (/DefaultCredentialsError|could not automatically determine credentials|Could not load the default credentials|Your default credentials were not found|GOOGLE_APPLICATION_CREDENTIALS/i.test(output)) return {
21243
+ kind: "google-adc",
21244
+ what: "Google Cloud, through Application Default Credentials",
21245
+ from: "the app's own error",
21246
+ command: "gcloud auth application-default login"
21247
+ };
21248
+ if (/NoCredentialsError|Unable to locate credentials|CredentialsProviderError|Could not load credentials from any providers|ExpiredToken|The SSO session associated with this profile has expired/i.test(output)) return {
21249
+ kind: "aws",
21250
+ what: "AWS credentials, a profile or a signed-in SSO session",
21251
+ from: "the app's own error",
21252
+ command: /SSO session|sso/i.test(output) ? "aws sso login" : "aws configure, or aws sso login"
21253
+ };
21254
+ if (/AADSTS\d+|DefaultAzureCredential failed|Please run 'az login'|CredentialUnavailableError/i.test(output)) return {
21255
+ kind: "azure",
21256
+ what: "Azure, signed in",
21257
+ from: "the app's own error",
21258
+ command: "az login"
21259
+ };
21260
+ const refused = /ECONNREFUSED\s+(?:\[?([\w.:-]+?)\]?):(\d+)|connection refused.*?(?:at|to)\s+([\w.-]+):(\d+)|could not connect to server:.*?"?([\w.-]+)"?.*?port (\d+)/i.exec(output);
21261
+ if (refused) {
21262
+ const host = refused[1] ?? refused[3] ?? refused[5] ?? "127.0.0.1";
21263
+ const port = Number(refused[2] ?? refused[4] ?? refused[6]);
21264
+ return {
21265
+ kind: "service",
21266
+ what: `something answering at ${host}:${port.toString()}`,
21267
+ from: "the app's own error",
21268
+ address: {
21269
+ host,
21270
+ port
21271
+ }
21272
+ };
21273
+ }
21274
+ return null;
21275
+ }
21276
+ /** Whether a need is met on this machine, without changing anything. */
21277
+ async function checkNeed(need, env = process.env) {
21278
+ switch (need.kind) {
21279
+ case "google-adc": {
21280
+ const file = env.GOOGLE_APPLICATION_CREDENTIALS;
21281
+ if (file && await exists(file)) return {
21282
+ need,
21283
+ met: true,
21284
+ detail: `GOOGLE_APPLICATION_CREDENTIALS points at ${file}`
21285
+ };
21286
+ const adc = join(adcDir(env), "application_default_credentials.json");
21287
+ return await exists(adc) ? {
21288
+ need,
21289
+ met: true,
21290
+ detail: "Application Default Credentials are on this machine"
21291
+ } : {
21292
+ need,
21293
+ met: false,
21294
+ detail: `no credentials at ${adc}`
21295
+ };
21296
+ }
21297
+ case "gcloud": {
21298
+ const account = await run("gcloud", [
21299
+ "auth",
21300
+ "list",
21301
+ "--filter=status:ACTIVE",
21302
+ "--format=value(account)"
21303
+ ]);
21304
+ if (account === null) return {
21305
+ need,
21306
+ met: "unknown",
21307
+ detail: "gcloud is not installed"
21308
+ };
21309
+ return account ? {
21310
+ need,
21311
+ met: true,
21312
+ detail: `gcloud is signed in as ${account}`
21313
+ } : {
21314
+ need,
21315
+ met: false,
21316
+ detail: "gcloud is not signed in"
21317
+ };
21318
+ }
21319
+ case "aws": {
21320
+ if (env.AWS_ACCESS_KEY_ID) return {
21321
+ need,
21322
+ met: true,
21323
+ detail: "AWS keys are in the environment"
21324
+ };
21325
+ const home = env.HOME ?? env.USERPROFILE ?? homedir();
21326
+ const credentials = await exists(join(home, ".aws", "credentials"));
21327
+ const config = await exists(join(home, ".aws", "config"));
21328
+ if (!credentials && !config) return {
21329
+ need,
21330
+ met: false,
21331
+ detail: "no AWS profile on this machine"
21332
+ };
21333
+ const who = await run("aws", [
21334
+ "sts",
21335
+ "get-caller-identity",
21336
+ "--query",
21337
+ "Arn",
21338
+ "--output",
21339
+ "text"
21340
+ ]);
21341
+ if (who === null) return {
21342
+ need,
21343
+ met: "unknown",
21344
+ detail: "a profile is on this machine; the aws command is not"
21345
+ };
21346
+ return who ? {
21347
+ need,
21348
+ met: true,
21349
+ detail: `AWS answers as ${who}`
21350
+ } : {
21351
+ need,
21352
+ met: false,
21353
+ detail: "the AWS profile is there, but its session is not signed in"
21354
+ };
21355
+ }
21356
+ case "azure": {
21357
+ const who = await run("az", [
21358
+ "account",
21359
+ "show",
21360
+ "--query",
21361
+ "user.name",
21362
+ "-o",
21363
+ "tsv"
21364
+ ]);
21365
+ if (who === null) return {
21366
+ need,
21367
+ met: "unknown",
21368
+ detail: "the az command is not installed"
21369
+ };
21370
+ return who ? {
21371
+ need,
21372
+ met: true,
21373
+ detail: `Azure is signed in as ${who}`
21374
+ } : {
21375
+ need,
21376
+ met: false,
21377
+ detail: "Azure is not signed in"
21378
+ };
21379
+ }
21380
+ case "api-key": return env[need.setting] ? {
21381
+ need,
21382
+ met: true,
21383
+ detail: `${need.setting} is set`
21384
+ } : {
21385
+ need,
21386
+ met: false,
21387
+ detail: `${need.setting} is not set here; a settings file can hold a staging value`
21388
+ };
21389
+ case "database":
21390
+ case "service": return await portOpen(need.address.host, need.address.port) ? {
21391
+ need,
21392
+ met: true,
21393
+ detail: `${need.address.host}:${need.address.port.toString()} answers`
21394
+ } : {
21395
+ need,
21396
+ met: false,
21397
+ detail: `nothing answers at ${need.address.host}:${need.address.port.toString()}`
21398
+ };
21399
+ }
21400
+ }
21401
+ var adcDir = (env) => env.CLOUDSDK_CONFIG ?? (process.platform === "win32" ? join(env.APPDATA ?? join(env.USERPROFILE ?? homedir(), "AppData", "Roaming"), "gcloud") : join(env.HOME ?? homedir(), ".config", "gcloud"));
21402
+ var exists = (path) => access(path).then(() => true, () => false);
21403
+ /** A command's trimmed output, "" when it fails, null when it is not installed. */
21404
+ function run(command, args) {
21405
+ return new Promise((resolve) => {
21406
+ execFile(command, args, {
21407
+ timeout: 15e3,
21408
+ windowsHide: true
21409
+ }, (error, stdout) => {
21410
+ if (error && error.code === "ENOENT") return resolve(null);
21411
+ resolve(error ? "" : stdout.trim());
21412
+ });
21413
+ });
21414
+ }
21415
+ var portOpen = (host, port) => new Promise((resolve) => {
21416
+ const socket = createConnection({
21417
+ host,
21418
+ port,
21419
+ timeout: 1500
21420
+ });
21421
+ socket.once("connect", () => {
21422
+ socket.destroy();
21423
+ resolve(true);
21424
+ });
21425
+ socket.once("error", () => resolve(false));
21426
+ socket.once("timeout", () => {
21427
+ socket.destroy();
21428
+ resolve(false);
21429
+ });
21430
+ });
21431
+ /** The compose file in a folder, read as the services it declares, or null. */
21432
+ async function composeIn(dir) {
21433
+ for (const file of [
21434
+ "compose.yaml",
21435
+ "compose.yml",
21436
+ "docker-compose.yml",
21437
+ "docker-compose.yaml"
21438
+ ]) {
21439
+ const text = await readFile(join(dir, file), "utf8").catch(() => null);
21440
+ if (text !== null) return {
21441
+ file,
21442
+ services: composeServices(text)
21443
+ };
21444
+ }
20972
21445
  return null;
20973
21446
  }
20974
21447
  // ../../hooks/node/src/hosts.mjs
@@ -21478,7 +21951,8 @@ async function knownIn(ctx) {
21478
21951
  settings: found.settingNames,
21479
21952
  settingsFiles: found.settingsFiles,
21480
21953
  files: [...found.sourceFiles, ...found.documents].slice(0, 400),
21481
- tools: ctx.config.tools.map((t) => t.name)
21954
+ tools: ctx.config.tools.map((t) => t.name),
21955
+ dependencies: [...found.dependencies]
21482
21956
  };
21483
21957
  }
21484
21958
  /** Commands a fix might start with, kept when this machine has them on its PATH. */
@@ -21638,6 +22112,60 @@ var notKnown = {
21638
22112
  throw new Failure("Then the app cannot be started yet. Your other answers are kept. When you know the command, put it under start: in modelos-secure.yaml, or run the same command again and type it.", []);
21639
22113
  }
21640
22114
  };
22115
+ /**
22116
+ * What the app needs, from its dependencies, settings and compose file, each checked here. A need
22117
+ * that is not met and has a command stops the start with that command named; the person may have
22118
+ * it run, on a plain yes, or start anyway.
22119
+ */
22120
+ async function sayNeeds(ctx) {
22121
+ const known = await knownIn(ctx);
22122
+ const env = {
22123
+ ...process.env,
22124
+ ...ctx.extraEnv
22125
+ };
22126
+ const needs = needsOf({
22127
+ dependencies: known.dependencies,
22128
+ settingNames: known.settings,
22129
+ startCommands: ctx.startCommands,
22130
+ compose: await composeIn(ctx.dir) ?? void 0,
22131
+ env
22132
+ });
22133
+ if (needs.length === 0) return;
22134
+ const states = await Promise.all(needs.map((n) => checkNeed(n, env)));
22135
+ step("Needs", plural(needs.length, "thing") + " the app relies on");
22136
+ for (const state of states) say(` ${state.met === true ? mark.yes : state.met === false ? mark.no : mark.maybe} ${state.need.what}${grey(`, ${state.detail}`)}`);
22137
+ const unmet = states.find((s) => s.met === false && s.need.kind !== "api-key");
22138
+ if (unmet) throw new Failure(`The app needs ${unmet.need.what}, and that is not met: ${unmet.detail}.`, needFixes(ctx, unmet.need), unmet.need.command ? `Run ${unmet.need.command}.` : void 0);
22139
+ }
22140
+ /** The ways past a need that is not met: a login run for the person on a plain yes, or on regardless. */
22141
+ function needFixes(ctx, need) {
22142
+ const fixes = [];
22143
+ if (need.command && !need.command.includes(", or ")) fixes.push({
22144
+ label: `Run ${need.command} for me now`,
22145
+ recommended: true,
22146
+ needsPerson: true,
22147
+ apply: () => runForPerson(need.command)
22148
+ });
22149
+ fixes.push({
22150
+ label: "Start anyway",
22151
+ apply: () => {
22152
+ ctx.startAnyway = true;
22153
+ }
22154
+ });
22155
+ return fixes;
22156
+ }
22157
+ /** Runs a login command the person asked for, in their terminal, and waits for it. */
22158
+ function runForPerson(command) {
22159
+ say(`\n Running ${bold(command)}. It may open your browser.\n`);
22160
+ return new Promise((resolve, reject) => {
22161
+ const child = spawn(command, {
22162
+ shell: true,
22163
+ stdio: "inherit"
22164
+ });
22165
+ child.once("exit", (code) => code === 0 ? resolve() : reject(new Failure(`${command} ended with code ${String(code)}.`, [])));
22166
+ child.once("error", reject);
22167
+ });
22168
+ }
21641
22169
  /** Starts the pool, or fails with what happened and how to fix it. */
21642
22170
  async function startApp(ctx, instances) {
21643
22171
  const { config } = ctx;
@@ -21714,6 +22242,7 @@ async function startApp(ctx, instances) {
21714
22242
  }]);
21715
22243
  const settings = await loadSettings(ctx.dir, config.start?.settings);
21716
22244
  Object.assign(ctx.extraEnv, settings, ctx.extraEnv);
22245
+ if (!ctx.startAnyway) await sayNeeds(ctx);
21717
22246
  const basePort = await freePort();
21718
22247
  const pool = await startAppPool({
21719
22248
  config,
@@ -21735,6 +22264,8 @@ async function startApp(ctx, instances) {
21735
22264
  notKnown
21736
22265
  ];
21737
22266
  if (explained) throw new Failure(explained.what, [...await runnableFixes(ctx, explained.fixes), ...others], explained.cause);
22267
+ const needed = output ? needFromOutput(output) : null;
22268
+ if (needed) throw new Failure(`The app needs ${needed.what}, and it is not met: its own error says so.`, [...needFixes(ctx, needed), ...others], needed.command ? `Run ${needed.command}.` : void 0);
21738
22269
  const missing = output ? missingSetting(output) : void 0;
21739
22270
  if (missing && /(_API_KEY|_KEY|_TOKEN)$/.test(missing)) throw new Failure(`The app calls its model with ${missing}.`, settingsFixes(ctx, missing, Boolean(process.env[missing])));
21740
22271
  if (missing) throw new Failure(`The app stopped: ${missing} is not set.`, settingsFixes(ctx, missing, false));
@@ -21994,6 +22525,7 @@ function toRegister(config, parts, extra) {
21994
22525
  }
21995
22526
  // src/commands/review.ts
21996
22527
  async function reviewCommand(dir, options) {
22528
+ const startedAt = Date.now();
21997
22529
  const config = await readConfig(dir);
21998
22530
  if (!config) throw new Error("There is no modelos-secure.yaml here. Run modelos-secure scan . first.");
21999
22531
  if (options.ci && (config.skipped?.length ?? 0) > 0) {
@@ -22098,7 +22630,7 @@ async function reviewCommand(dir, options) {
22098
22630
  for (const f of relevant) say(` ${f.status === "probable" ? "Probable" : red("Broke")} ${f.id} ${f.title} (${f.reproduced.k.toString()} of ${f.reproduced.n.toString()} replays)`);
22099
22631
  if (relevant.length === 0 && last.review.status === "done") say(` ${blue("No break")} on ${Object.keys(results?.paths ?? {}).length.toString()} paths${standIn ? ", against the stand-in attacker" : ""}.`);
22100
22632
  else if (relevant.length > 0 && standIn) say(" The stand-in attacker wrote the attempts that broke it.");
22101
- say(`\n Review: ${connection.url}/reviews/${last.review.id}`);
22633
+ rows([["Review", link(`${connection.url}/reviews/${last.review.id}`, `${connection.url}/reviews/${last.review.id}`)], ["Took", elapsed(Date.now() - startedAt)]]);
22102
22634
  const verdict = last.review.verdict;
22103
22635
  if (verdict) {
22104
22636
  say(`\n New ${verdict.newBreaks.length.toString()}, fixed ${verdict.fixed.length.toString()}, holding so far ${verdict.holdingSoFar.length.toString()}, in both ${verdict.inBoth.length.toString()}, came back ${verdict.returning.length.toString()}.`);
@@ -22312,8 +22844,8 @@ var TOOL_POWERS = [
22312
22844
  "delete"
22313
22845
  ];
22314
22846
  var CONSENT_TEXT = "To understand your app, the scan lets a model read your code the way an engineer would, on our own Google Cloud project. It starts from an outline: file paths, the names and signatures of functions and classes, your prompts and the names of your settings. Then it asks for the files and lines it needs, and only those are sent, each through a filter that takes secrets out. It never sends settings files, keys, certificates or credential files, the values of your settings, or files over 1 MB. Every answer it sends is listed in .modelos-secure/sent.jsonl. Run with --explain to see what may be read, or --offline to keep everything here.";
22315
- var plural = (n, one, many = `${one}s`) => `${n.toLocaleString("en-GB")} ${n === 1 ? one : many}`;
22316
22847
  async function scanCommand(dir, options) {
22848
+ const startedAt = Date.now();
22317
22849
  const retry = {
22318
22850
  yes: options.yes,
22319
22851
  ci: options.ci
@@ -22418,7 +22950,7 @@ async function scanCommand(dir, options) {
22418
22950
  label: "Enter a token now",
22419
22951
  recommended: true,
22420
22952
  apply: async () => {
22421
- const { loginCommand } = await import("./assets/login-Dz4o-1cM.js");
22953
+ const { loginCommand } = await import("./assets/login-CFKpL6IN.js");
22422
22954
  await loginCommand();
22423
22955
  const fresh = await readConnection();
22424
22956
  api = fresh ? apiClient(fresh) : null;
@@ -22587,12 +23119,16 @@ async function scanCommand(dir, options) {
22587
23119
  step("Register", `The map is saved to the workspace`);
22588
23120
  const paths = (config.ways_in.length || 1) * (config.rules.length || 1);
22589
23121
  say(`\n ${bold(`${config.system} is connected.`)}\n`);
22590
- say(` ${`${plural(config.ways_in.length, "way")} in`.padEnd(14)} ${config.ways_in.map((w) => w.name).join(", ")}`);
22591
23122
  const heldNow = held(config);
22592
- say(` ${plural(config.tools.length, "tool").padEnd(14)} ${config.tools.length === 0 ? "none to hold" : heldNow.length === 0 ? "none held during reviews" : `${heldNow.length.toString()} held during reviews: ${heldNow.join(", ")}`}`);
22593
- say(` ${plural(paths, "path").padEnd(14)} none tested yet\n`);
22594
23123
  const url = connection ? `${connection.url}/systems/${system.id}` : "";
22595
- say(` Map: ${url}`);
23124
+ rows([
23125
+ ["Ways in", `${config.ways_in.length.toString()}: ${config.ways_in.map((w) => w.name).join(", ")}`],
23126
+ ["Tools", config.tools.length === 0 ? "none to hold" : heldNow.length === 0 ? `${config.tools.length.toString()}, none held during reviews` : `${config.tools.length.toString()}, ${heldNow.length.toString()} held during reviews: ${heldNow.join(", ")}`],
23127
+ ["Paths", `${paths.toString()}, none tested yet`],
23128
+ ["Map", link(url, url)],
23129
+ ["Took", elapsed(Date.now() - startedAt)]
23130
+ ]);
23131
+ say();
22596
23132
  if (options.noReview || options.ci || options.yes || !interactive()) return;
22597
23133
  if (await ask("Start a quick first review now? About 15 minutes.", "It starts the app on this machine with the hooks attached, checks they hold what they should, then reviews it.", [{
22598
23134
  label: "Yes, start it",
@@ -22740,7 +23276,6 @@ var { values, positionals } = parseArgs({
22740
23276
  format: { type: "string" },
22741
23277
  out: { type: "string" },
22742
23278
  system: { type: "string" },
22743
- against: { type: "string" },
22744
23279
  url: { type: "string" },
22745
23280
  "client-id": { type: "string" },
22746
23281
  "client-secret": { type: "string" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "modelos-secure",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -1 +0,0 @@
1
- export { t as loginCommand } from "./login-DeGa63BQ.js";