modelos-secure 0.3.1 → 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.
Files changed (2) hide show
  1. package/cli.js +54 -38
  2. package/package.json +1 -1
package/cli.js CHANGED
@@ -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.1";
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" };
@@ -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/commands/scan.ts
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
- /** Only for telling a repository of several apps from one app: whether any file imports an AI library. */
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 plural = (n, one, many = `${one}s`) => `${n.toLocaleString("en-GB")} ${n === 1 ? one : many}`;
22013
- /** Folders below the root that are apps of their own. */
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
- for (const manifest of [
22027
- "package.json",
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
- function looksLikeAnApp(result) {
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
- if (!looksLikeAnApp(result)) {
22066
- const apps = await appsIn(dir);
22067
- if (apps.length > 1) {
22068
- const chosen = options.yes ? [apps[0]] : await ask(`This repository has ${apps.length.toString()} apps with model calls.`, void 0, [...apps.map((app) => ({
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
- for (const app of chosen) await scanCommand(join(dir, app), options);
22076
- return;
22077
- }
22078
- if (apps.length === 1) return scanCommand(join(dir, apps[0]), options);
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,6 +1,6 @@
1
1
  {
2
2
  "name": "modelos-secure",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
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": {