modelos-secure 0.3.4 → 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
@@ -8,35 +8,38 @@ npx modelos-secure scan .
8
8
 
9
9
  It reads the code the way an engineer would, starting where the app starts, draws the system's map and asks only what it cannot work out. Everything it decides goes into `modelos-secure.yaml`.
10
10
 
11
- Before it sends anything it asks once whether a model may read the code it needs, or only an outline of it, or nothing. Every answer it sends passes a filter that takes secrets out, and settings files, keys and credentials are never read. `.modelos-secure/sent.jsonl` lists what was sent, `--explain` shows the outline and what may and may not be read without sending anything, and `--offline` keeps everything on your machine.
11
+ Before it sends anything it asks once whether a model may read the code it needs, or only an outline of it, or nothing. Every answer it sends passes a filter that takes secrets out, and settings files, keys and credentials are never sent. What leaves: the outline, the reader's answers, the map it registers, your answer key if you add one, and the last lines of the app's own output when a start fails. `.modelos-secure/sent.jsonl` lists every answer the reader sent, `--explain` shows the outline and what may and may not be read without sending anything, and `--offline` keeps everything on your machine.
12
12
 
13
13
  At a terminal, `modelos-secure` on its own starts a session about the app in the folder: ask about it in your own words, and each answer cites the files and lines it rests on, checked against the code.
14
14
 
15
- | Command | Does |
16
- | --------------------------------------------- | ------------------------------------------------------------------ |
17
- | `modelos-secure` | At a terminal, a session about the app in this folder. |
18
- | `modelos-secure login` | Saves a workspace token, and signs in with Google if needed. |
19
- | `modelos-secure scan .` | Connects this repository. `--offline`, `--explain`, `--yes`. |
20
- | `modelos-secure review` | Starts a review of this system. `--quick`, `--ci`, `--budget`. |
21
- | `modelos-secure replay F-031` | Replays one finding. |
22
- | `modelos-secure doctor` | Checks the setup and fixes what it can. |
23
- | `modelos-secure runner` | Serves a review's attempts from this machine, in the app's folder. |
24
- | `modelos-secure export R-1179 --format sarif` | Writes a review's findings as SARIF or JSON. |
25
- | `modelos-secure --version` | Prints this CLI's version. |
15
+ | Command | Does |
16
+ | --------------------------------------------- | --------------------------------------------------------------------------------------------- |
17
+ | `modelos-secure` | At a terminal, a session about the app in this folder. |
18
+ | `modelos-secure login` | Saves a workspace token, and signs in with Google if needed. |
19
+ | `modelos-secure scan .` | Connects this repository. `--offline`, `--explain`, `--yes`, `--ci`, `--no-review`. |
20
+ | `modelos-secure review` | Starts a review of this system. `--quick`, `--ci`, `--yes`, `--budget`, `--instances`. |
21
+ | `modelos-secure replay F-031` | Replays one finding of an app the scan connected. |
22
+ | `modelos-secure doctor` | Checks the setup and fixes what it can. |
23
+ | `modelos-secure runner` | Serves a review's attempts from this machine, in the app's folder. `--system`, `--instances`. |
24
+ | `modelos-secure export R-1179 --format sarif` | Prints a review's findings as SARIF or JSON; `--out` writes a file. |
25
+ | `modelos-secure --version` | Prints this CLI's version. |
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.
26
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
- During a review, actions are held and never run. Outside a review the hooks do nothing. Requires Node 22 or newer. Python apps need Python 3.10 or newer.
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.
30
32
 
31
33
  Every command says what it takes with `--help`, such as `modelos-secure scan --help`, and refuses a flag it does not take.
32
34
 
33
- | Exit code | Means |
34
- | --------- | -------------------------------------------------------------- |
35
- | 0 | Done. |
36
- | 1 | Something failed, or a fix check found a break. |
37
- | 2 | The command was used wrongly. |
38
- | 3 | There are questions only a person can answer, as `--ci` lists. |
39
- | 130 | Stopped with Ctrl-C. What was done is kept. |
35
+ | Exit code | Means |
36
+ | --------- | ------------------------------------------------------------------------------- |
37
+ | 0 | Done. |
38
+ | 1 | Something failed, or a fix check found a break. |
39
+ | 2 | The command was used wrongly. |
40
+ | 3 | There are questions only a person can answer, as `--ci` lists. |
41
+ | 130 | Stopped with Ctrl-C. What was done is kept. |
42
+ | 143, 129 | Stopped by a signal, or the terminal closed. The app it started is stopped too. |
40
43
 
41
44
  ## Signing in
42
45
 
@@ -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 };