modelos-secure 0.3.0 → 0.3.2
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/{dist/cli.js → cli.js} +55 -39
- package/package.json +10 -26
- package/dist/README.md +0 -60
- package/dist/package.json +0 -20
- /package/{dist/assets → assets}/login-CvPyhFJ7.js +0 -0
- /package/{dist/assets → assets}/login-DeAJGhiN.js +0 -0
- /package/{dist/hook → hook}/clients.mjs +0 -0
- /package/{dist/hook → hook}/edge.mjs +0 -0
- /package/{dist/hook → hook}/hook.mjs +0 -0
- /package/{dist/hook → hook}/hosts.d.mts +0 -0
- /package/{dist/hook → hook}/hosts.mjs +0 -0
- /package/{dist/hook → hook}/loader.mjs +0 -0
- /package/{dist/hook → hook}/rewrite.mjs +0 -0
- /package/{dist/python → python}/modelos_secure/__init__.py +0 -0
- /package/{dist/python → python}/modelos_secure/clients.py +0 -0
- /package/{dist/python → python}/modelos_secure/edge.py +0 -0
- /package/{dist/python → python}/modelos_secure/hook.py +0 -0
- /package/{dist/python → python}/requirements.txt +0 -0
- /package/{dist/python → python}/sitecustomize.py +0 -0
- /package/{dist/wasm → wasm}/tree-sitter-javascript.wasm +0 -0
- /package/{dist/wasm → wasm}/tree-sitter-python.wasm +0 -0
- /package/{dist/wasm → wasm}/tree-sitter-tsx.wasm +0 -0
- /package/{dist/wasm → wasm}/tree-sitter-typescript.wasm +0 -0
- /package/{dist/wasm → wasm}/web-tree-sitter.wasm +0 -0
package/{dist/cli.js → cli.js}
RENAMED
|
@@ -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.2";
|
|
27
27
|
var type = "module";
|
|
28
28
|
var bin = { "modelos-secure": "dist/cli.js" };
|
|
29
29
|
var exports$1 = { "./hook": "./dist/hook/hook.mjs" };
|
|
@@ -46,7 +46,7 @@ var repository = {
|
|
|
46
46
|
var package_default = {
|
|
47
47
|
name,
|
|
48
48
|
version: version$1,
|
|
49
|
-
"private":
|
|
49
|
+
"private": true,
|
|
50
50
|
type,
|
|
51
51
|
bin,
|
|
52
52
|
exports: exports$1,
|
|
@@ -21996,21 +21996,23 @@ function read(result, understood) {
|
|
|
21996
21996
|
facts: mergeFacts(result.facts, verified.facts).filter((f) => !(f.kind === "way-in" && f.adapter !== "model" && f.function && acts.has(f.function)))
|
|
21997
21997
|
};
|
|
21998
21998
|
}
|
|
21999
|
-
// src/
|
|
22000
|
-
var TOOL_POWERS = [
|
|
22001
|
-
"read",
|
|
22002
|
-
"read private data",
|
|
22003
|
-
"change",
|
|
22004
|
-
"send outside",
|
|
22005
|
-
"move money",
|
|
22006
|
-
"delete"
|
|
22007
|
-
];
|
|
22008
|
-
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.";
|
|
21999
|
+
// src/several-apps.ts
|
|
22009
22000
|
var AI_DEPENDENCIES = /"(ai|openai|@google\/genai|langchain|@langchain\/[\w-]+|@anthropic-ai\/sdk|@mastra\/core|@openai\/agents)"|^\s*"?(openai|google-genai|langgraph|langchain|anthropic|crewai|pydantic-ai|smolagents|openai-agents|llama-index|autogen-agentchat|litellm)\b/m;
|
|
22010
|
-
/**
|
|
22001
|
+
/** Whether a file imports an AI library. */
|
|
22011
22002
|
var AI_IMPORT = /^\s*(?:import|from)\b[^\n]*\b(openai|anthropic|genai|gemini|langchain|langgraph|llama_index|crewai|pydantic_ai|smolagents|autogen|mastra|ai-sdk|agents|vertexai|litellm|ollama)\b|require\(["'](openai|ai|@anthropic-ai\/sdk)["']\)/m;
|
|
22012
|
-
var
|
|
22013
|
-
|
|
22003
|
+
var MANIFESTS = [
|
|
22004
|
+
"package.json",
|
|
22005
|
+
"pyproject.toml",
|
|
22006
|
+
"requirements.txt"
|
|
22007
|
+
];
|
|
22008
|
+
async function dependsOnAi(folder) {
|
|
22009
|
+
for (const manifest of MANIFESTS) {
|
|
22010
|
+
const text = await readFile(join(folder, manifest), "utf8").catch(() => "");
|
|
22011
|
+
if (AI_DEPENDENCIES.test(text)) return true;
|
|
22012
|
+
}
|
|
22013
|
+
return false;
|
|
22014
|
+
}
|
|
22015
|
+
/** Folders below the root that are apps of their own. What is inside an app is part of it. */
|
|
22014
22016
|
async function appsIn(root) {
|
|
22015
22017
|
const found = [];
|
|
22016
22018
|
const visit = async (dir, depth) => {
|
|
@@ -22023,26 +22025,34 @@ async function appsIn(root) {
|
|
|
22023
22025
|
"venv"
|
|
22024
22026
|
].includes(entry.name)) continue;
|
|
22025
22027
|
const full = join(dir, entry.name);
|
|
22026
|
-
|
|
22027
|
-
|
|
22028
|
-
"pyproject.toml",
|
|
22029
|
-
"requirements.txt"
|
|
22030
|
-
]) {
|
|
22031
|
-
const text = await readFile(join(full, manifest), "utf8").catch(() => "");
|
|
22032
|
-
if (AI_DEPENDENCIES.test(text)) {
|
|
22033
|
-
found.push(inRepo(root, full));
|
|
22034
|
-
break;
|
|
22035
|
-
}
|
|
22036
|
-
}
|
|
22037
|
-
await visit(full, depth + 1);
|
|
22028
|
+
if (await dependsOnAi(full)) found.push(inRepo(root, full));
|
|
22029
|
+
else await visit(full, depth + 1);
|
|
22038
22030
|
}
|
|
22039
22031
|
};
|
|
22040
22032
|
await visit(root, 1);
|
|
22041
22033
|
return found;
|
|
22042
22034
|
}
|
|
22043
|
-
|
|
22035
|
+
/** Whether any code here, wherever it sits, builds on an AI library. */
|
|
22036
|
+
function usesAi(result) {
|
|
22044
22037
|
return result.facts.some((f) => f.kind === "tool" || f.kind === "agent") || result.project.files.some((f) => AI_IMPORT.test(f.text));
|
|
22045
22038
|
}
|
|
22039
|
+
/** Whether the folder is an app itself, beside any apps in its subfolders. */
|
|
22040
|
+
async function rootIsAnApp(root, result, apps) {
|
|
22041
|
+
if (await dependsOnAi(root)) return true;
|
|
22042
|
+
const outside = (path) => !apps.some((app) => path === app || path.startsWith(`${app}/`));
|
|
22043
|
+
return result.facts.some((f) => (f.kind === "tool" || f.kind === "agent") && outside(f.file)) || result.project.files.some((f) => outside(f.path) && AI_IMPORT.test(f.text));
|
|
22044
|
+
}
|
|
22045
|
+
// src/commands/scan.ts
|
|
22046
|
+
var TOOL_POWERS = [
|
|
22047
|
+
"read",
|
|
22048
|
+
"read private data",
|
|
22049
|
+
"change",
|
|
22050
|
+
"send outside",
|
|
22051
|
+
"move money",
|
|
22052
|
+
"delete"
|
|
22053
|
+
];
|
|
22054
|
+
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.";
|
|
22055
|
+
var plural = (n, one, many = `${one}s`) => `${n.toLocaleString("en-GB")} ${n === 1 ? one : many}`;
|
|
22046
22056
|
async function scanCommand(dir, options) {
|
|
22047
22057
|
const retry = {
|
|
22048
22058
|
yes: options.yes,
|
|
@@ -22062,21 +22072,27 @@ async function scanCommand(dir, options) {
|
|
|
22062
22072
|
const result = await scanFolder(dir);
|
|
22063
22073
|
say(`${bold("ModelOS Secure")} scanning ${result.look.name}\n`);
|
|
22064
22074
|
step("Look", `${[...result.look.languages, ...result.look.frameworks].join(", ") || "No source found"}, ${plural(result.look.fileCount, "file")}`);
|
|
22065
|
-
|
|
22066
|
-
|
|
22067
|
-
|
|
22068
|
-
|
|
22075
|
+
const apps = await appsIn(dir);
|
|
22076
|
+
if (apps.length > 1 && !await rootIsAnApp(dir, result, apps)) {
|
|
22077
|
+
const several = `This repository has ${apps.length.toString()} apps with model calls.`;
|
|
22078
|
+
if (options.yes) say(`\n ${several} --yes takes the first, ${apps[0]}.\n`);
|
|
22079
|
+
const chosen = options.yes ? [apps[0]] : await ask(several, void 0, [
|
|
22080
|
+
...apps.map((app) => ({
|
|
22069
22081
|
label: `Pick ${app}`,
|
|
22070
22082
|
value: [app]
|
|
22071
|
-
})),
|
|
22083
|
+
})),
|
|
22084
|
+
{
|
|
22072
22085
|
label: `Scan all ${apps.length.toString()} as separate systems`,
|
|
22073
22086
|
value: apps
|
|
22074
|
-
}
|
|
22075
|
-
|
|
22076
|
-
|
|
22077
|
-
|
|
22078
|
-
|
|
22079
|
-
|
|
22087
|
+
},
|
|
22088
|
+
{
|
|
22089
|
+
label: "They are one system: scan this whole folder",
|
|
22090
|
+
value: []
|
|
22091
|
+
}
|
|
22092
|
+
]);
|
|
22093
|
+
for (const app of chosen) await scanCommand(join(dir, app), options);
|
|
22094
|
+
if (chosen.length > 0) return;
|
|
22095
|
+
} else if (apps.length === 1 && !usesAi(result)) return scanCommand(join(dir, apps[0]), options);
|
|
22080
22096
|
step("Read", `${plural(result.project.files.length, "file")} parsed${result.adapters.some((a) => a !== "http") ? `, with the reader for ${result.adapters.filter((a) => a !== "http").join(" and ")}` : ""}`);
|
|
22081
22097
|
const outline = await buildOutline(result.look, result.facts, result.project);
|
|
22082
22098
|
if (options.explain) {
|
package/package.json
CHANGED
|
@@ -1,36 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "modelos-secure",
|
|
3
|
-
"version": "0.3.
|
|
4
|
-
"
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/mn-chetan/modelos-secure.git"
|
|
9
|
+
},
|
|
5
10
|
"type": "module",
|
|
6
11
|
"bin": {
|
|
7
|
-
"modelos-secure": "
|
|
12
|
+
"modelos-secure": "cli.js"
|
|
8
13
|
},
|
|
9
14
|
"exports": {
|
|
10
|
-
"./hook": "./
|
|
15
|
+
"./hook": "./hook/hook.mjs"
|
|
11
16
|
},
|
|
12
17
|
"engines": {
|
|
13
18
|
"node": ">=22"
|
|
14
|
-
},
|
|
15
|
-
"files": [
|
|
16
|
-
"dist"
|
|
17
|
-
],
|
|
18
|
-
"scripts": {
|
|
19
|
-
"build": "vite build"
|
|
20
|
-
},
|
|
21
|
-
"dependencies": {
|
|
22
|
-
"@modelos-secure/core": "workspace:*",
|
|
23
|
-
"@modelos-secure/scan": "workspace:*",
|
|
24
|
-
"@modelos-secure/schema": "workspace:*",
|
|
25
|
-
"yaml": "2.9.1"
|
|
26
|
-
},
|
|
27
|
-
"devDependencies": {
|
|
28
|
-
"vite": "8.3.6"
|
|
29
|
-
},
|
|
30
|
-
"description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
|
|
31
|
-
"license": "UNLICENSED",
|
|
32
|
-
"repository": {
|
|
33
|
-
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/mn-chetan/modelos-secure.git"
|
|
35
19
|
}
|
|
36
|
-
}
|
|
20
|
+
}
|
package/dist/README.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# modelos-secure
|
|
2
|
-
|
|
3
|
-
Tests an AI system for Security, Safety and Quality. One command in the system's repository connects it, with no code changes:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npx modelos-secure scan .
|
|
7
|
-
```
|
|
8
|
-
|
|
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
|
-
|
|
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.
|
|
12
|
-
|
|
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
|
-
|
|
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. |
|
|
26
|
-
|
|
27
|
-
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
|
-
|
|
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.
|
|
30
|
-
|
|
31
|
-
Every command says what it takes with `--help`, such as `modelos-secure scan --help`, and refuses a flag it does not take.
|
|
32
|
-
|
|
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. |
|
|
40
|
-
|
|
41
|
-
## Signing in
|
|
42
|
-
|
|
43
|
-
Copy the login command from the workspace tokens page in the app. It has the app's address and, when the app is behind Google sign-in, the Google client to sign in with:
|
|
44
|
-
|
|
45
|
-
```bash
|
|
46
|
-
npx modelos-secure login --url <address> --client-id <client ID> --client-secret <client secret>
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Paste the workspace token when it asks. The token is never typed on the command line. Your browser then opens for you to sign in with Google. The address, the token and the Google sign-in are kept in `~/.modelos-secure/credentials.json`, readable only by you, and the sign-in is renewed as it runs out. Without `--client-id`, as on a local stack, there is no Google sign-in.
|
|
50
|
-
|
|
51
|
-
When the app is reached at an IP address and its certificate is issued for a name, the login command also has `--tls-name` and that name. The CLI then connects to the address and checks the certificate against the name: its chain, its dates and the name are all still checked. `MODELOS_SECURE_TLS_NAME` does the same for one run. When something cannot be reached, the CLI says why in plain words.
|
|
52
|
-
|
|
53
|
-
`MODELOS_SECURE_URL` and `MODELOS_SECURE_TOKEN` take the place of the saved address and token. `modelos-secure doctor` says who is signed in to Google, and why a request was turned away.
|
|
54
|
-
|
|
55
|
-
## In CI
|
|
56
|
-
|
|
57
|
-
CI has no browser, so it signs in to Google as a service account. The service account needs IAP-secured Web App User on the app, and the client ID must be the one on the tokens page. Set `MODELOS_SECURE_URL` and `MODELOS_SECURE_TOKEN`, then one of these:
|
|
58
|
-
|
|
59
|
-
- `MODELOS_SECURE_CLIENT_ID`, the client ID from the tokens page, with gcloud signed in as the service account. The CLI runs `gcloud auth print-identity-token --audiences=<client ID>` and runs it again as the token runs out, so it suits reviews of any length. It does not work with a person's own gcloud sign-in.
|
|
60
|
-
- `MODELOS_SECURE_IAP_TOKEN`, an ID token for the service account with the client ID as its audience. It is sent as it is and never renewed, and it lasts about an hour. In GitHub Actions, `google-github-actions/auth` makes one with `token_format: id_token`, `id_token_audience` set to the client ID and `id_token_include_email: true`.
|
package/dist/package.json
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "modelos-secure",
|
|
3
|
-
"version": "0.3.0",
|
|
4
|
-
"description": "Tests AI systems for Security, Safety and Quality. One command connects a system from its code.",
|
|
5
|
-
"license": "UNLICENSED",
|
|
6
|
-
"repository": {
|
|
7
|
-
"type": "git",
|
|
8
|
-
"url": "git+https://github.com/mn-chetan/modelos-secure.git"
|
|
9
|
-
},
|
|
10
|
-
"type": "module",
|
|
11
|
-
"bin": {
|
|
12
|
-
"modelos-secure": "cli.js"
|
|
13
|
-
},
|
|
14
|
-
"exports": {
|
|
15
|
-
"./hook": "./hook/hook.mjs"
|
|
16
|
-
},
|
|
17
|
-
"engines": {
|
|
18
|
-
"node": ">=20"
|
|
19
|
-
}
|
|
20
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|