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 +2 -0
- package/assets/login-CFKpL6IN.js +1 -0
- package/assets/{login-DeGa63BQ.js → login-DUhawk3O.js} +121 -13
- package/cli.js +559 -24
- package/package.json +1 -1
- package/assets/login-Dz4o-1cM.js +0 -1
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
|
|
12
|
-
var
|
|
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
|
|
15
|
-
var
|
|
16
|
-
var
|
|
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) && !
|
|
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 (!
|
|
44
|
-
const width = Math.max(20, (
|
|
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
|
-
|
|
49
|
-
const
|
|
50
|
-
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
-
/**
|
|
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
|
|
12413
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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-
|
|
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
|
-
|
|
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
package/assets/login-Dz4o-1cM.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { t as loginCommand } from "./login-DeGa63BQ.js";
|