loki-mode 9.41.0 → 9.42.0

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/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Autonomous spec-driven build system with a built-in trust layer. It does not call work done until it is verified (RARV-C closure loop, 8 quality gates, completion council, verified-completion evidence gate). Triggers on "Loki Mode". Takes a spec (PRD, GitHub issue, OpenAPI doc, etc.) to deployed product with minimal human intervention. Provider-agnostic. Requires --dangerously-skip-permissions flag.
4
4
  ---
5
5
 
6
- # Loki Mode v9.41.0
6
+ # Loki Mode v9.42.0
7
7
 
8
8
  **You are an autonomous agent. You make decisions. You do not ask questions. You do not stop.**
9
9
 
@@ -470,4 +470,4 @@ See `CHANGELOG.md` entries [7.5.7], [7.5.8], [7.5.13] for the per-fix list and r
470
470
 
471
471
  ---
472
472
 
473
- **v9.41.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
473
+ **v9.42.0 | [Autonomi](https://www.autonomi.dev/) flagship product | ~410 lines core**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 9.41.0
1
+ 9.42.0
package/autonomy/run.sh CHANGED
@@ -507,8 +507,27 @@ RESOURCE_CHECK_INTERVAL=${LOKI_RESOURCE_CHECK_INTERVAL:-300} # Check every 5 mi
507
507
  RESOURCE_CPU_THRESHOLD=${LOKI_RESOURCE_CPU_THRESHOLD:-80} # CPU % threshold
508
508
  RESOURCE_MEM_THRESHOLD=${LOKI_RESOURCE_MEM_THRESHOLD:-80} # Memory % threshold
509
509
 
510
- # Budget / Cost Limit (opt-in, empty = unlimited)
511
- BUDGET_LIMIT=${LOKI_BUDGET_LIMIT:-""} # USD amount, e.g., "50.00"
510
+ # Budget / Cost Limit.
511
+ #
512
+ # DEFAULT-ON as of v9.42.0. This shipped as `""` (unlimited), and run.sh's own
513
+ # F4 analysis names it as one of three runaway valves that all shipped
514
+ # DISABLED: LOKI_BUDGET_LIMIT="" returns immediately, LOKI_MAX_DURATION=0 never
515
+ # stops, leaving LOKI_MAX_ITERATIONS=1000 (a measured 8.3-DAY ceiling) as the
516
+ # only backstop. A user who mistypes a spec, or hits a loop, had no cost
517
+ # backstop at all.
518
+ #
519
+ # 100 USD, not a tight number. Measured envelope from 79 recorded benchmark
520
+ # trials: median 0.48 USD/trial, max 3.06. A real build is far under this, so
521
+ # the cap catches runaways, not ordinary work.
522
+ #
523
+ # SAFE BY CONSTRUCTION: breaching PAUSES (writes .loki/PAUSE and saves state);
524
+ # it never kills work or discards a deliverable. The user removes .loki/PAUSE
525
+ # to resume, or raises LOKI_BUDGET_LIMIT. Set LOKI_BUDGET_LIMIT="" to restore
526
+ # the old unlimited behavior explicitly.
527
+ BUDGET_LIMIT=${LOKI_BUDGET_LIMIT-"100.00"} # USD. Note `-` not `:-`: an
528
+ # explicit empty value means the
529
+ # operator chose unlimited, and is
530
+ # honored.
512
531
 
513
532
  # Background Mode
514
533
  BACKGROUND_MODE=${LOKI_BACKGROUND:-false} # Run in background
@@ -7,7 +7,7 @@ Modules:
7
7
  control: Session control API (start/stop/pause/resume)
8
8
  """
9
9
 
10
- __version__ = "9.41.0"
10
+ __version__ = "9.42.0"
11
11
 
12
12
  # Expose the control app for easy import
13
13
  try:
@@ -2,7 +2,7 @@
2
2
 
3
3
  The flagship product of [Autonomi](https://www.autonomi.dev/). Loki Mode is a spec-driven autonomous builder with a built-in trust layer that takes any spec to a deployed product and verifies completion with evidence (quality gates plus a completion council), not just a "done" claim. Complete installation instructions for all platforms and use cases.
4
4
 
5
- **Version:** v9.41.0
5
+ **Version:** v9.42.0
6
6
 
7
7
  ---
8
8
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var _t=Object.create;var{getPrototypeOf:vt,defineProperty:jG,getOwnPropertyNames:ht}=Object;var gt=Object.prototype.hasOwnProperty;function mt($){return this[$]}var ut,dt,pt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?ut??=new WeakMap:dt??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?_t(vt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of ht($))if(!gt.call(J,q))jG(J,q,{get:mt.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var ct=($)=>$;function lt($,X){this[$]=ct.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:lt.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var UR={};B1(UR,{lokiDir:()=>h0,homeLokiDir:()=>WQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as it}from"url";import{existsSync as Zq}from"fs";import{homedir as at}from"os";function ot(){let $=YR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(YR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function WQ(){return h2(at(),".loki")}var YR,L1;var k1=s(()=>{YR=LG(it(import.meta.url));L1=ot()});import{readFileSync as st}from"fs";import{resolve as nt,dirname as rt}from"path";import{fileURLToPath as tt}from"url";function j9(){if(n3!==null)return n3;let $="9.41.0";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=rt(tt(import.meta.url)),Q=AG(X);n3=st(nt(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var OR={};B1(OR,{runOrThrow:()=>Be,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>Me,commandExists:()=>g5,ShellError:()=>TG,MAX_STDOUT_BYTES:()=>MR});async function Jq($,X=MR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Be($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new TG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=Ne($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Ne($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function Me($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var MR=16777216,TG;var y8=s(()=>{TG=class TG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Oe?"":$}var Oe,p0,$5,q1,D61,A1,f1,m5,r;var t7=s(()=>{Oe=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),D61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as we}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(we($))return GQ=$,$;let X=await g5("python3.12");if(X)return GQ=X,X;let Q=await g5("python3");return GQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var GQ;var m2=s(()=>{y8()});var vR={};B1(vR,{runStatus:()=>te});import{existsSync as u5,readFileSync as C9,readdirSync as ER,statSync as xR}from"fs";import{resolve as A5,basename as pe}from"path";import{homedir as ce}from"os";function kR($){let X=Math.trunc($);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function SR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=kR($),Y=kR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function ie(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
2
+ var _t=Object.create;var{getPrototypeOf:vt,defineProperty:jG,getOwnPropertyNames:ht}=Object;var gt=Object.prototype.hasOwnProperty;function mt($){return this[$]}var ut,dt,pt=($,X,Q)=>{var z=$!=null&&typeof $==="object";if(z){var Z=X?ut??=new WeakMap:dt??=new WeakMap,K=Z.get($);if(K)return K}Q=$!=null?_t(vt($)):{};let J=X||!$||!$.__esModule?jG(Q,"default",{value:$,enumerable:!0}):Q;for(let q of ht($))if(!gt.call(J,q))jG(J,q,{get:mt.bind($,q),enumerable:!0});if(z)Z.set($,J);return J};var zq=($,X)=>()=>(X||$((X={exports:{}}).exports,X),X.exports);var ct=($)=>$;function lt($,X){this[$]=ct.bind(null,X)}var B1=($,X)=>{for(var Q in X)jG($,Q,{get:X[Q],enumerable:!0,configurable:!0,set:lt.bind(X,Q)})};var s=($,X)=>()=>($&&(X=$($=0)),X);var w5=import.meta.require;var UR={};B1(UR,{lokiDir:()=>h0,homeLokiDir:()=>WQ,findRepoRootForVersion:()=>AG,REPO_ROOT:()=>L1});import{resolve as h2,dirname as LG}from"path";import{fileURLToPath as it}from"url";import{existsSync as Zq}from"fs";import{homedir as at}from"os";function ot(){let $=YR;for(let X=0;X<6;X++){if(Zq(h2($,"VERSION"))&&Zq(h2($,"autonomy/run.sh")))return $;let Q=LG($);if(Q===$)break;$=Q}return h2(YR,"..","..","..")}function AG($){let X=$;for(let Q=0;Q<6;Q++){if(Zq(h2(X,"VERSION"))&&Zq(h2(X,"autonomy/run.sh")))return X;let z=LG(X);if(z===X)break;X=z}return h2($,"..","..","..")}function h0(){return process.env.LOKI_DIR??h2(process.cwd(),".loki")}function WQ(){return h2(at(),".loki")}var YR,L1;var k1=s(()=>{YR=LG(it(import.meta.url));L1=ot()});import{readFileSync as st}from"fs";import{resolve as nt,dirname as rt}from"path";import{fileURLToPath as tt}from"url";function j9(){if(n3!==null)return n3;let $="9.42.0";if(typeof $==="string"&&$.length>0)return n3=$,n3;try{let X=rt(tt(import.meta.url)),Q=AG(X);n3=st(nt(Q,"VERSION"),"utf-8").trim()}catch{n3="unknown"}return n3}var n3=null;var Kq=s(()=>{k1()});var OR={};B1(OR,{runOrThrow:()=>Be,run:()=>$1,readStreamCapped:()=>Jq,commandVersion:()=>Me,commandExists:()=>g5,ShellError:()=>TG,MAX_STDOUT_BYTES:()=>MR});async function Jq($,X=MR){let Q=$.getReader(),z=new TextDecoder,Z="",K=0;try{while(K<X){let{done:J,value:q}=await Q.read();if(J)break;if(!q)continue;if(K+=q.byteLength,K>X){let V=q.byteLength-(K-X);Z+=z.decode(q.subarray(0,V),{stream:!0});break}Z+=z.decode(q,{stream:!0})}Z+=z.decode()}finally{try{await Q.cancel()}catch{}Q.releaseLock()}return Z}async function $1($,X={}){let Q=Bun.spawn({cmd:[...$],stdout:"pipe",stderr:"pipe",env:X.env?{...process.env,...X.env}:process.env,cwd:X.cwd}),z,Z;if(X.timeoutMs&&X.timeoutMs>0)z=setTimeout(()=>{try{Q.kill("SIGTERM")}catch{}Z=setTimeout(()=>{try{Q.kill("SIGKILL")}catch{}},2000)},X.timeoutMs);try{let[K,J,q]=await Promise.all([Jq(Q.stdout),new Response(Q.stderr).text(),Q.exited]);return{stdout:K,stderr:J,exitCode:q}}finally{if(z)clearTimeout(z);if(Z)clearTimeout(Z)}}async function Be($,X={}){let Q=await $1($,X);if(Q.exitCode!==0)throw new TG(`command failed (${Q.exitCode}): ${$.join(" ")}`,Q.exitCode,Q.stdout,Q.stderr);return Q}async function g5($){let X=Ne($),Q=await $1(["sh","-c",`command -v ${X}`],{timeoutMs:5000});if(Q.exitCode===0)return Q.stdout.trim()||null;return null}function Ne($){if(!/^[A-Za-z0-9._/-]+$/.test($))throw Error(`refused to shell-escape suspect token: ${$}`);return $}async function Me($,X="--version"){if(!await g5($))return null;let z=await $1([$,X],{timeoutMs:5000});if(z.exitCode!==0)return null;return((z.stdout||z.stderr).split(/\r?\n/)[0]?.trim()??"")||null}var MR=16777216,TG;var y8=s(()=>{TG=class TG extends Error{message;exitCode;stdout;stderr;constructor($,X,Q,z){super($);this.message=$;this.exitCode=X;this.stdout=Q;this.stderr=z;this.name="ShellError"}}});function g2($){return Oe?"":$}var Oe,p0,$5,q1,D61,A1,f1,m5,r;var t7=s(()=>{Oe=(process.env.NO_COLOR??"").length>0;p0=g2("\x1B[0;31m"),$5=g2("\x1B[0;32m"),q1=g2("\x1B[1;33m"),D61=g2("\x1B[0;34m"),A1=g2("\x1B[0;36m"),f1=g2("\x1B[1m"),m5=g2("\x1B[2m"),r=g2("\x1B[0m")});import{existsSync as we}from"fs";async function Z2(){if(GQ!==void 0)return GQ;let $="/opt/homebrew/bin/python3.12";if(we($))return GQ=$,$;let X=await g5("python3.12");if(X)return GQ=X,X;let Q=await g5("python3");return GQ=Q,Q}async function I4($,X={}){let Q=await Z2();if(!Q)return{stdout:"",stderr:"python3 not found",exitCode:127};return $1([Q,"-c",$],X)}var GQ;var m2=s(()=>{y8()});var vR={};B1(vR,{runStatus:()=>te});import{existsSync as u5,readFileSync as C9,readdirSync as ER,statSync as xR}from"fs";import{resolve as A5,basename as pe}from"path";import{homedir as ce}from"os";function kR($){let X=Math.trunc($);if(X>=1e6)return`${(Math.trunc(X/1e6*10)/10).toFixed(1)}M`;if(X>=1000)return`${(Math.trunc(X/1000*10)/10).toFixed(1)}K`;return String(X)}function SR($,X,Q){if(X===0)return null;let z=Math.trunc($*100/X),Z=Math.trunc($*Vq/X);if(Z>Vq)Z=Vq;let K=Vq-Z,J=$5;if(z>=80)J=p0;else if(z>=50)J=q1;let q="=".repeat(Math.max(0,Z))+" ".repeat(Math.max(0,K)),V=kR($),Y=kR(X);return` ${f1}${Q}${r} ${J}[${q}]${r} ${z}% (${V} / ${Y})`}async function ie(){if(await g5("jq"))return!0;return process.stdout.write(`${p0}Error: jq is required but not installed.${r}
3
3
  `),process.stdout.write(`Install with:
4
4
  `),process.stdout.write(` brew install jq (macOS)
5
5
  `),process.stdout.write(` apt install jq (Debian/Ubuntu)
@@ -1337,4 +1337,4 @@ Set LOKI_LEGACY_BASH=1 to force the bash CLI for every command.
1337
1337
  `),2}case"start":{let{runStart:z}=await Promise.resolve().then(() => (yt(),St));return z(Q)}default:return process.stderr.write(`Unknown command: ${X}
1338
1338
  `),process.stderr.write(bt),2}}wR();process.on("SIGINT",()=>process.exit(130));process.on("SIGTERM",()=>process.exit(143));var K61=await Z61(Bun.argv.slice(2));process.exit(K61);
1339
1339
 
1340
- //# debugId=2D91561BF1C8026CBF5C04C1871B78CF
1340
+ //# debugId=1D0DDCEB1E91F296B6BCC5A878B18C97
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '9.41.0'
78
+ __version__ = '9.42.0'
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "9.41.0",
4
+ "version": "9.42.0",
5
5
  "description": "Loki Mode by Autonomi. Autonomous spec-to-product system: takes a PRD, GitHub issue, OpenAPI/JSON/YAML, or one-line brief to a deployed app via the RARV-C closure loop with 8 quality gates. Provider-agnostic (Claude Code, OpenAI Codex, Cline, Aider, opencode).",
6
6
  "keywords": [
7
7
  "agent",
@@ -111,7 +111,8 @@
111
111
  "tests/detect-mock-problems.sh",
112
112
  "tests/detect-test-mutations.sh",
113
113
  "tests/detect-semantic-test-problems.sh",
114
- "tests/detect-invariant-violations.sh"
114
+ "tests/detect-invariant-violations.sh",
115
+ "tests/detect-hallucinated-deps.sh"
115
116
  ],
116
117
  "scripts": {
117
118
  "prepack": "find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; find . -name '*.pyc' -delete 2>/dev/null; if command -v bun >/dev/null 2>&1; then (cd loki-ts && bun install --production && bun run build) || echo 'WARN: loki-ts build failed, using existing dist if present'; else echo 'WARN: bun not on PATH, skipping loki-ts build (using committed dist if present)'; fi; find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null; find . -name '*.pyc' -delete 2>/dev/null; node tools/normalize-package-permissions.mjs .",
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "9.41.0",
5
+ "version": "9.42.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env bash
2
+ # Hallucinated-dependency detector (slopsquatting guard)
3
+ #
4
+ # Usage: ./tests/detect-hallucinated-deps.sh [DIR] [--strict]
5
+ # --strict: exit non-zero on any unresolvable package (for a CI/quality gate)
6
+ #
7
+ # THE RISK. A model asked to add a dependency can emit a package name that does
8
+ # not exist. An attacker who registers that name owns code execution in every
9
+ # install that follows. This is documented as "slopsquatting"; see
10
+ # arxiv.org/pdf/2606.13918 (2026-06-15) on detecting hallucinated package
11
+ # imports in AI-assisted code.
12
+ #
13
+ # WHAT THIS DOES. Parses dependency manifests, resolves each declared package
14
+ # against its registry, and reports names that do not resolve.
15
+ #
16
+ # FAIL OPEN, DELIBERATELY. No network, no curl, or a registry 5xx means the
17
+ # package was NOT CHECKED -- which is reported as `unchecked`, never as a pass
18
+ # and never as a failure. Blocking a build because a registry was briefly
19
+ # unreachable would be a worse failure mode than the one being prevented, and
20
+ # silently passing would make the gate theatre. Three states, as everywhere
21
+ # else in this repo: resolved / MISSING / unchecked.
22
+ set -uo pipefail
23
+
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ TARGET="${1:-$(cd "$SCRIPT_DIR/.." && pwd)}"
26
+ case "${1:-}" in --strict) TARGET="$(cd "$SCRIPT_DIR/.." && pwd)" ;; esac
27
+ STRICT=0
28
+ for a in "$@"; do [ "$a" = "--strict" ] && STRICT=1; done
29
+
30
+ TIMEOUT="${LOKI_DEP_CHECK_TIMEOUT:-3}"
31
+ MISSING=0; RESOLVED=0; UNCHECKED=0
32
+ MISSING_LIST=""
33
+
34
+ if ! command -v curl >/dev/null 2>&1; then
35
+ echo "detect-hallucinated-deps: curl unavailable; every package is UNCHECKED (not a pass)"
36
+ exit 0
37
+ fi
38
+
39
+ # npm: one HEAD per package against the registry.
40
+ _check_npm() {
41
+ local pkg="$1" code
42
+ case "$pkg" in ""|.*|*" "*) return 0 ;; esac
43
+ code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time "$TIMEOUT" \
44
+ "https://registry.npmjs.org/$(printf '%s' "$pkg" | sed 's|/|%2f|g')" 2>/dev/null)" || code=""
45
+ # NOTE: -f is deliberately ABSENT. With -f curl exits nonzero on 404, the
46
+ # `|| code=""` fires, and a genuinely missing package is misreported as
47
+ # "unchecked" -- turning the detector into theatre. Verified: with -f the
48
+ # capture is "404CURL FAILED"; without it, "404".
49
+ case "$code" in
50
+ 200) RESOLVED=$((RESOLVED+1)) ;;
51
+ 404) MISSING=$((MISSING+1)); MISSING_LIST="${MISSING_LIST}
52
+ npm: $pkg" ;;
53
+ *) UNCHECKED=$((UNCHECKED+1)) ;; # network/5xx: NOT a verdict
54
+ esac
55
+ }
56
+
57
+ _check_pypi() {
58
+ local pkg="$1" code
59
+ case "$pkg" in ""|-*|*" "*) return 0 ;; esac
60
+ code="$(curl -sS -o /dev/null -w '%{http_code}' --max-time "$TIMEOUT" \
61
+ "https://pypi.org/pypi/${pkg}/json" 2>/dev/null)" || code=""
62
+ case "$code" in
63
+ 200) RESOLVED=$((RESOLVED+1)) ;;
64
+ 404) MISSING=$((MISSING+1)); MISSING_LIST="${MISSING_LIST}
65
+ pypi: $pkg" ;;
66
+ *) UNCHECKED=$((UNCHECKED+1)) ;;
67
+ esac
68
+ }
69
+
70
+ # package.json: dependencies + devDependencies, skipping local/git/workspace specs.
71
+ while IFS= read -r manifest; do
72
+ [ -f "$manifest" ] || continue
73
+ while IFS= read -r pkg; do
74
+ [ -n "$pkg" ] && _check_npm "$pkg"
75
+ done < <(python3 - "$manifest" <<'PY' 2>/dev/null
76
+ import json, sys
77
+ try:
78
+ d = json.load(open(sys.argv[1]))
79
+ except Exception:
80
+ sys.exit(0)
81
+ for key in ("dependencies", "devDependencies"):
82
+ for name, spec in (d.get(key) or {}).items():
83
+ s = str(spec)
84
+ # a local path, git url, or workspace protocol is not a registry name
85
+ if s.startswith(("file:", "link:", "git", "workspace:", "portal:", "http")):
86
+ continue
87
+ print(name)
88
+ PY
89
+ )
90
+ done < <(find "$TARGET" -name package.json -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -40)
91
+
92
+ # requirements.txt
93
+ while IFS= read -r manifest; do
94
+ [ -f "$manifest" ] || continue
95
+ while IFS= read -r pkg; do
96
+ [ -n "$pkg" ] && _check_pypi "$pkg"
97
+ done < <(python3 - "$manifest" <<'PY' 2>/dev/null
98
+ import re, sys
99
+ try:
100
+ lines = open(sys.argv[1], encoding="utf-8", errors="replace").read().splitlines()
101
+ except Exception:
102
+ sys.exit(0)
103
+ for l in lines:
104
+ l = l.split("#", 1)[0].strip()
105
+ if not l or l.startswith("-"): # -r, -e, --index-url
106
+ continue
107
+ if l.startswith(("git+", "http")) or "://" in l:
108
+ continue
109
+ m = re.match(r"^([A-Za-z0-9][A-Za-z0-9._-]*)", l)
110
+ if m:
111
+ print(m.group(1))
112
+ PY
113
+ )
114
+ done < <(find "$TARGET" -name requirements.txt -not -path '*/node_modules/*' -not -path '*/.git/*' 2>/dev/null | head -40)
115
+
116
+ TOTAL=$((RESOLVED + MISSING + UNCHECKED))
117
+ echo "detect-hallucinated-deps: $TOTAL declared packages"
118
+ echo " resolved : $RESOLVED"
119
+ echo " MISSING : $MISSING"
120
+ echo " unchecked: $UNCHECKED (network unavailable; NOT a pass and NOT a failure)"
121
+
122
+ if [ "$MISSING" -gt 0 ]; then
123
+ echo ""
124
+ echo "Packages that do not exist on their registry:${MISSING_LIST}"
125
+ echo ""
126
+ echo "A name that does not resolve is how a hallucinated dependency becomes a"
127
+ echo "supply-chain compromise: an attacker registers it and owns every install."
128
+ [ "$STRICT" -eq 1 ] && exit 1
129
+ fi
130
+ exit 0