@vantio/cli
Advanced tools
| // Shared Optics/Gate wrap catalog — destinations the Node interceptor may | ||
| // observe or enforce. Keep in lockstep with | ||
| // vantio-open-core/packages/vantio-agent-sdk-py/vantio/_http_observe.py | ||
| // | ||
| // Exact DNS names plus regional patterns verified against provider docs | ||
| // (2026-08-14). Do not add company NEVER_BLOCK SaaS, Cursor, or whole-cloud | ||
| // suffixes such as amazonaws.com / googleapis.com. | ||
| "use strict"; | ||
| const LLM_HOSTS = [ | ||
| "api.openai.com", | ||
| "api.anthropic.com", | ||
| "generativelanguage.googleapis.com", | ||
| "api.cohere.ai", | ||
| "api.cohere.com", | ||
| "api.mistral.ai", | ||
| "api.groq.com", | ||
| "api.together.xyz", | ||
| "api.perplexity.ai", | ||
| "inference.ai.azure.com", | ||
| "openai.azure.com", | ||
| "api.x.ai", | ||
| "api.deepseek.com", | ||
| "api.fireworks.ai", | ||
| "openrouter.ai", | ||
| "api.cerebras.ai", | ||
| "api.voyageai.com", | ||
| "api.sambanova.ai", | ||
| "api.deepinfra.com", | ||
| "router.huggingface.co", | ||
| "api-inference.huggingface.co", | ||
| "api.replicate.com", | ||
| "ollama.com", | ||
| "integrate.api.nvidia.com", | ||
| ]; | ||
| function hostListed(hostname, items) { | ||
| const h = String(hostname || "").toLowerCase(); | ||
| if (!h) return false; | ||
| const arr = items && typeof items.has === "function" ? [...items] : Array.isArray(items) ? items : []; | ||
| for (const item of arr) { | ||
| const b = String(item || "").toLowerCase().trim(); | ||
| if (!b) continue; | ||
| if (h === b) return true; | ||
| // Suffix match only when the listed host looks like a DNS name (has a dot) | ||
| // so a bare token like "com" cannot sweep the internet. | ||
| if (b.includes(".") && h.endsWith("." + b)) return true; | ||
| } | ||
| return false; | ||
| } | ||
| /** Regional / infix API DNS that exact+suffix matching cannot cover honestly. */ | ||
| function hostMatchesRegional(hostname) { | ||
| const h = String(hostname || "").toLowerCase(); | ||
| if (!h) return false; | ||
| // Amazon Bedrock Runtime + Mantle + Agents Runtime (region is a DNS label). | ||
| if (/^bedrock-runtime(-fips)?\.[a-z0-9-]+\.amazonaws\.com$/.test(h)) return true; | ||
| if (/^bedrock-mantle\.[a-z0-9-]+\.api\.aws$/.test(h)) return true; | ||
| if (/^bedrock-agent-runtime(-fips)?\.[a-z0-9-]+\.amazonaws\.com$/.test(h)) return true; | ||
| // Google Vertex AI: global, {region}-aiplatform, multi-region REP. | ||
| if (h === "aiplatform.googleapis.com") return true; | ||
| if (h.endsWith("-aiplatform.googleapis.com") && h.includes(".")) return true; | ||
| if (/^aiplatform\.(us|eu)\.rep\.googleapis\.com$/.test(h)) return true; | ||
| // Hugging Face Inference Endpoints: {name}.{region}.endpoints.huggingface.cloud | ||
| if (h === "endpoints.huggingface.cloud" || h.endsWith(".endpoints.huggingface.cloud")) return true; | ||
| return false; | ||
| } | ||
| function isOllamaLocal(hostname, port) { | ||
| const h = String(hostname || "").toLowerCase(); | ||
| const p = String(port == null ? "" : port); | ||
| if (p !== "11434") return false; | ||
| return h === "localhost" || h === "127.0.0.1" || h === "::1" || h === "[::1]"; | ||
| } | ||
| function catalogInScope(hostname, port, hosts) { | ||
| return ( | ||
| hostListed(hostname, hosts) || | ||
| hostMatchesRegional(hostname) || | ||
| isOllamaLocal(hostname, port) | ||
| ); | ||
| } | ||
| function guessProvider(hostname, port) { | ||
| const h = String(hostname || "").toLowerCase(); | ||
| if (!h) return "unknown"; | ||
| if (isOllamaLocal(hostname, port) || h === "ollama.com" || h.endsWith(".ollama.com")) return "ollama"; | ||
| if (h.includes("openai") || h === "api.openai.com") return "openai"; | ||
| if (h.includes("anthropic")) return "anthropic"; | ||
| if (h.includes("aiplatform")) return "vertex"; | ||
| if (h.includes("googleapis") || h.includes("generativelanguage")) return "google"; | ||
| if (h.includes("cohere")) return "cohere"; | ||
| if (h.includes("mistral")) return "mistral"; | ||
| if (h.includes("groq")) return "groq"; | ||
| if (h.includes("together")) return "together"; | ||
| if (h.includes("perplexity")) return "perplexity"; | ||
| if (h.includes("azure") || h.includes("openai.azure")) return "azure_openai"; | ||
| if (h.includes("x.ai") || h.endsWith(".x.ai")) return "xai"; | ||
| if (h.includes("deepseek")) return "deepseek"; | ||
| if (h.includes("fireworks")) return "fireworks"; | ||
| if (h.includes("openrouter")) return "openrouter"; | ||
| if (h.includes("cerebras")) return "cerebras"; | ||
| if (h.includes("voyageai")) return "voyage"; | ||
| if (h.includes("sambanova")) return "sambanova"; | ||
| if (h.includes("deepinfra")) return "deepinfra"; | ||
| if (h.includes("bedrock")) return "bedrock"; | ||
| if (h.includes("huggingface") || h.endsWith(".huggingface.cloud")) return "huggingface"; | ||
| if (h.includes("replicate")) return "replicate"; | ||
| if (h.includes("nvidia")) return "nvidia"; | ||
| if (h.includes("localhost") || h.startsWith("127.")) return "local"; | ||
| return "other"; | ||
| } | ||
| module.exports = { | ||
| LLM_HOSTS, | ||
| hostListed, | ||
| hostMatchesRegional, | ||
| isOllamaLocal, | ||
| catalogInScope, | ||
| guessProvider, | ||
| }; |
+590
-47
| // [ ∅ VANTIO ] Open Core Interceptor — Observe Plane | ||
| // Injected at runtime by `vantio run node agent.js` via Node --require. | ||
| // Patches globalThis.fetch to intercept outbound LLM calls — zero code changes. | ||
| // Patches globalThis.fetch and Node http/https.request|get to intercept | ||
| // outbound LLM calls — zero code changes. Raw sockets, curl, and browser | ||
| // paths stay outside this wrap. | ||
| // | ||
@@ -22,2 +24,8 @@ // Layer identity in the Vantio suite: | ||
| const { join } = require("node:path"); | ||
| const { | ||
| LLM_HOSTS: BASE_LLM_HOSTS, | ||
| hostListed, | ||
| catalogInScope, | ||
| guessProvider, | ||
| } = require("./llm-hosts.cjs"); | ||
@@ -45,2 +53,4 @@ const USE_COLOR = process.stderr.isTTY === true; | ||
| const SOAK_LOCAL = process.env.VANTIO_SOAK_LOCAL === "1"; | ||
| // Local Gate control plane (Phantom-Box / dogfood) — never upsell Optics-only. | ||
| const LOCAL_GATE = SOAK_LOCAL || /:5001\/?$/.test(String(INGEST_URL || "")); | ||
@@ -65,15 +75,6 @@ // ── Lane 1 anonymous telemetry (optional, fire-and-forget) ─────────────────── | ||
| const LLM_HOSTS = new Set([ | ||
| "api.openai.com", | ||
| "api.anthropic.com", | ||
| "generativelanguage.googleapis.com", | ||
| "api.cohere.ai", | ||
| "api.mistral.ai", | ||
| "api.groq.com", | ||
| "api.together.xyz", | ||
| "api.perplexity.ai", | ||
| "inference.ai.azure.com", | ||
| ]); | ||
| // Local soak / self-hosted LLMs via env only — never hardcode 127.0.0.1 | ||
| // (would make every out-of-scope localhost call look like LLM traffic). | ||
| const LLM_HOSTS = new Set(BASE_LLM_HOSTS); | ||
| // Local / extra LLM hosts via env only — never hardcode 127.0.0.1 as a | ||
| // blanket catalog entry (would make every localhost call look like LLM traffic). | ||
| // Ollama on localhost:11434 is matched by catalogInScope, not this set. | ||
| for (const h of String(process.env.VANTIO_EXTRA_LLM_HOSTS || "").split(",")) { | ||
@@ -84,2 +85,58 @@ const t = h.trim(); | ||
| /** Safe URL metadata — path only, never query string (may contain keys). */ | ||
| function extractRequestMeta(input, init) { | ||
| let href = ""; | ||
| let method = (init && init.method) || "GET"; | ||
| try { | ||
| if (typeof input === "string") href = input; | ||
| else if (input instanceof URL) href = input.href; | ||
| else if (typeof Request !== "undefined" && input instanceof Request) { | ||
| href = input.url; | ||
| method = init?.method || input.method || method; | ||
| } else if (input && input.url) href = input.url; | ||
| } catch { | ||
| href = ""; | ||
| } | ||
| let path = "/"; | ||
| let scheme = "https"; | ||
| try { | ||
| const u = new URL(href); | ||
| path = u.pathname || "/"; | ||
| scheme = u.protocol.replace(":", "") || "https"; | ||
| } catch { | ||
| /* keep defaults */ | ||
| } | ||
| let request_bytes = null; | ||
| try { | ||
| const body = init && init.body; | ||
| if (typeof body === "string") request_bytes = Buffer.byteLength(body); | ||
| else if (Buffer.isBuffer(body)) request_bytes = body.length; | ||
| else if (body instanceof Uint8Array) request_bytes = body.byteLength; | ||
| } catch { | ||
| request_bytes = null; | ||
| } | ||
| return { | ||
| method: String(method || "GET").toUpperCase(), | ||
| path, | ||
| scheme, | ||
| request_bytes, | ||
| }; | ||
| } | ||
| function responseMeta(response) { | ||
| if (!response) { | ||
| return { status: null, ok: null, content_type: null, bytes: null }; | ||
| } | ||
| const cl = response.headers?.get?.("content-length"); | ||
| const bytes = cl != null && cl !== "" ? parseInt(cl, 10) || 0 : null; | ||
| const ctRaw = response.headers?.get?.("content-type") || ""; | ||
| const content_type = ctRaw.split(";")[0].trim() || null; | ||
| return { | ||
| status: typeof response.status === "number" ? response.status : null, | ||
| ok: typeof response.ok === "boolean" ? response.ok : null, | ||
| content_type, | ||
| bytes, | ||
| }; | ||
| } | ||
| // ── Default policy (fail-open until cloud policy loads) ────────────────────── | ||
@@ -187,5 +244,9 @@ const DEFAULT_POLICY = { | ||
| if (!cloudSyncActive) { | ||
| log(`${c.dim}[ ∅ VANTIO ] Free plan — calls observed locally only. Dashboard sync requires Pro or Enterprise (vantio.ai/pricing).${c.reset}`); | ||
| log( | ||
| LOCAL_GATE | ||
| ? `${c.dim}[ ∅ VANTIO ] Local Gate — events sync to the on-box control plane (${INGEST_URL}).${c.reset}` | ||
| : `${c.dim}[ ∅ VANTIO ] Free plan — calls observed locally only. Dashboard sync requires Pro or Enterprise (vantio.ai/pricing).${c.reset}` | ||
| ); | ||
| } else if (SOAK_LOCAL) { | ||
| log(`${c.dim}[ ∅ VANTIO ] Soak-local mode — syncing events to ${INGEST_URL}${c.reset}`); | ||
| log(`${c.dim}[ ∅ VANTIO ] Local Gate mode — syncing events to ${INGEST_URL}${c.reset}`); | ||
| } | ||
@@ -272,2 +333,7 @@ } | ||
| // Maximum bytes we will buffer from a ReadableStream to scan for PII. | ||
| // Requests larger than this threshold pass through unscanned rather than being | ||
| // held in memory, preserving back-pressure for true streaming workloads. | ||
| const MAX_STREAM_SCAN_BYTES = 2 * 1024 * 1024; // 2 MB ? max Latch (2026-08-09); was 64 KB | ||
| // Redact a concrete request body value, preserving its original type. | ||
@@ -278,5 +344,8 @@ // Returns { value, bytes, redactions, replaced, unscanned }: | ||
| // any redaction happened (so the caller knows to swap the body). | ||
| // - ReadableStream / FormData / Blob → not scanned; `unscanned` is its label. | ||
| // - ReadableStream ≤ 64 KB → tee'd, buffered, scanned; redacted copy returned | ||
| // as Uint8Array when PII found, pass-through branch returned unchanged when not. | ||
| // - ReadableStream > 64 KB / FormData / Blob → not scanned; `unscanned` is its label. | ||
| // Never throws — on any unexpected shape it returns the body unchanged. | ||
| function redactRequestBody(body) { | ||
| // This function is async because ReadableStream buffering requires awaiting reads. | ||
| async function redactRequestBody(body) { | ||
| const none = { value: body, bytes: 0, redactions: [], replaced: false, unscanned: null }; | ||
@@ -314,2 +383,50 @@ if (body == null) return none; | ||
| // ── ReadableStream: tee + buffer up to MAX_STREAM_SCAN_BYTES ───────────── | ||
| // We tee the stream so the pass-through branch (b) always carries the full | ||
| // original content. The scan branch (a) is read until we confirm the body | ||
| // fits within the scan window. If PII is found we return the redacted text as | ||
| // a Uint8Array (the stream was small enough that buffering is safe). If no PII | ||
| // is found we return the pass-through branch so the network call is unaffected. | ||
| // Streams larger than the threshold fall back to unscanned — no bytes consumed | ||
| // on the pass-through branch, preserving back-pressure. | ||
| if (typeof ReadableStream !== "undefined" && body instanceof ReadableStream) { | ||
| try { | ||
| const [scanBranch, passBranch] = body.tee(); | ||
| const reader = scanBranch.getReader(); | ||
| const chunks = []; | ||
| let total = 0; | ||
| let oversized = false; | ||
| for (;;) { | ||
| const { done, value } = await reader.read(); | ||
| if (done) break; | ||
| const n = value ? (value.byteLength != null ? value.byteLength : value.length || 0) : 0; | ||
| total += n; | ||
| if (total > MAX_STREAM_SCAN_BYTES) { oversized = true; break; } | ||
| if (value) chunks.push(value); | ||
| } | ||
| // Release our scan reader regardless of outcome so GC can clean up. | ||
| try { reader.cancel(); } catch { /* ignore */ } | ||
| if (!oversized) { | ||
| // Full body fits — scan and optionally redact. | ||
| const all = Buffer.concat(chunks.map((c) => Buffer.from(c))); | ||
| const text = all.toString("utf8"); | ||
| const r = redactBody(text); | ||
| if (r.redactions.length > 0) { | ||
| // PII found: return the redacted content as a Uint8Array so the | ||
| // caller can substitute it for the original stream. | ||
| const buf = Buffer.from(r.text, "utf8"); | ||
| return { value: new Uint8Array(buf), bytes: buf.length, redactions: r.redactions, replaced: true, unscanned: null }; | ||
| } | ||
| // No PII: use the pass-through branch (original content, no latency). | ||
| return { value: passBranch, bytes: total, redactions: [], replaced: false, unscanned: null }; | ||
| } | ||
| // Oversized — use pass-through branch unmodified; log as unscanned. | ||
| return { value: passBranch, bytes: 0, redactions: [], replaced: false, unscanned: "ReadableStream" }; | ||
| } catch { | ||
| // tee() / read failed (e.g. stream already locked) — fall through below. | ||
| } | ||
| return { value: body, bytes: 0, redactions: [], replaced: false, unscanned: "ReadableStream" }; | ||
| } | ||
| const label = unscannableBodyLabel(body); | ||
@@ -331,2 +448,10 @@ if (label) return { value: body, bytes: 0, redactions: [], replaced: false, unscanned: label }; | ||
| if (FREE_MODE || !INGEST_URL || !cloudSyncActive) return; | ||
| // Additive Optics Sight Loop fields — Gate stores opaque JSON; PE joins on traceId. | ||
| const host = metadata && metadata.target_host; | ||
| const eventPayload = { | ||
| ...metadata, | ||
| provider: metadata.provider || (host ? guessProvider(host) : undefined), | ||
| mediation: metadata.mediation || "sight_loop", | ||
| plane: metadata.plane || "optics_gate", | ||
| }; | ||
| void _originalFetch.call(globalThis, `${INGEST_URL}/api/v1/ingest`, { | ||
@@ -342,3 +467,3 @@ method: "POST", | ||
| auditMode: AUDIT_MODE, | ||
| eventPayload: metadata, | ||
| eventPayload, | ||
| }), | ||
@@ -354,7 +479,7 @@ signal: AbortSignal.timeout(5000), | ||
| // (OS / package-manager / unrelated) traffic merely because a policy exists. | ||
| function inScope(hostname) { | ||
| function inScope(hostname, port) { | ||
| return ( | ||
| LLM_HOSTS.has(hostname) || | ||
| policy.blocked_hosts.includes(hostname) || | ||
| policy.allowed_hosts.includes(hostname) | ||
| catalogInScope(hostname, port, LLM_HOSTS) || | ||
| hostListed(hostname, policy.blocked_hosts) || | ||
| hostListed(hostname, policy.allowed_hosts) | ||
| ); | ||
@@ -460,3 +585,3 @@ } | ||
| if (policy.enforce) { | ||
| if (policy.blocked_hosts.includes(hostname)) { | ||
| if (hostListed(hostname, policy.blocked_hosts)) { | ||
| if (policy.dry_run) { | ||
@@ -471,3 +596,3 @@ _calls.push({ hostname, action: "DRY_RUN_BLOCKED_HOST" }); | ||
| } | ||
| } else if (policy.allowed_hosts.length > 0 && !policy.allowed_hosts.includes(hostname)) { | ||
| } else if (policy.allowed_hosts.length > 0 && !hostListed(hostname, policy.allowed_hosts)) { | ||
| if (policy.dry_run) { | ||
@@ -491,3 +616,3 @@ _calls.push({ hostname, action: "DRY_RUN_BLOCKED_HOST" }); | ||
| if (init && init.body != null) { | ||
| const r = redactRequestBody(init.body); | ||
| const r = await redactRequestBody(init.body); | ||
| reqBytes = r.bytes; | ||
@@ -561,4 +686,11 @@ redactions = r.redactions; | ||
| function destFromHref(href) { | ||
| const u = new URL(href); | ||
| const port = u.port || (u.protocol === "https:" ? "443" : "80"); | ||
| return { hostname: u.hostname, port }; | ||
| } | ||
| globalThis.fetch = async function vantioFetch(input, init) { | ||
| let hostname; | ||
| let port; | ||
| try { | ||
@@ -569,3 +701,5 @@ const url = typeof input === "string" ? input | ||
| : input.url; | ||
| hostname = new URL(url).hostname; | ||
| const dest = destFromHref(url); | ||
| hostname = dest.hostname; | ||
| port = dest.port; | ||
| } catch { | ||
@@ -586,3 +720,3 @@ return _originalFetch.call(this, input, init); | ||
| // through, untouched. We never block/redact/meter unrelated traffic. | ||
| if (!inScope(hostname)) { | ||
| if (!inScope(hostname, port)) { | ||
| return _originalFetch.call(this, input, init); | ||
@@ -596,14 +730,73 @@ } | ||
| if (FREE_MODE) { | ||
| const response = await _originalFetch.call(this, input, init); | ||
| const bytes = parseInt(response.headers.get("content-length") || "0", 10) || null; | ||
| const reqMeta = extractRequestMeta(input, init); | ||
| const provider = guessProvider(hostname, port); | ||
| const t0 = Date.now(); | ||
| let response; | ||
| try { | ||
| response = await _originalFetch.call(this, input, init); | ||
| } catch (err) { | ||
| const ts = new Date().toISOString(); | ||
| const duration_ms = Date.now() - t0; | ||
| _calls.push({ | ||
| hostname, | ||
| provider, | ||
| method: reqMeta.method, | ||
| path: reqMeta.path, | ||
| scheme: reqMeta.scheme, | ||
| request_bytes: reqMeta.request_bytes, | ||
| bytes: null, | ||
| status: null, | ||
| ok: false, | ||
| content_type: null, | ||
| duration_ms, | ||
| ts, | ||
| action: "OBSERVED", | ||
| error_class: err && err.name ? String(err.name) : "Error", | ||
| error: "network_error", | ||
| }); | ||
| log([ | ||
| "", | ||
| `${c.dim}[ ∅ VANTIO ]${c.reset} ${c.red}Outbound LLM call failed${c.reset}`, | ||
| ` host: ${c.cyan}${hostname}${c.reset}`, | ||
| ` provider: ${provider}`, | ||
| ` method: ${reqMeta.method} ${reqMeta.path}`, | ||
| ` error: ${err && err.name ? err.name : "Error"}`, | ||
| ` duration: ${duration_ms}ms`, | ||
| ` pid: ${process.pid}`, | ||
| ` time: ${ts}`, | ||
| ].join("\n")); | ||
| throw err; | ||
| } | ||
| const resp = responseMeta(response); | ||
| const duration_ms = Date.now() - t0; | ||
| const ts = new Date().toISOString(); | ||
| _calls.push({ hostname, bytes, ts, action: "OBSERVED" }); | ||
| _calls.push({ | ||
| hostname, | ||
| provider, | ||
| method: reqMeta.method, | ||
| path: reqMeta.path, | ||
| scheme: reqMeta.scheme, | ||
| request_bytes: reqMeta.request_bytes, | ||
| bytes: resp.bytes, | ||
| status: resp.status, | ||
| ok: resp.ok, | ||
| content_type: resp.content_type, | ||
| duration_ms, | ||
| ts, | ||
| action: "OBSERVED", | ||
| }); | ||
| log([ | ||
| "", | ||
| `${c.dim}[ ∅ VANTIO ]${c.reset} ${c.yellow}Outbound LLM call intercepted${c.reset}`, | ||
| ` host: ${c.cyan}${hostname}${c.reset}`, | ||
| ` pid: ${process.pid}`, | ||
| ` bytes: ${bytes != null ? bytes.toLocaleString() : "unknown"}`, | ||
| ` time: ${ts}`, | ||
| ` ${c.dim}→ Optics observes only. See docs/observe-only.md · upgrade to Vantio Gate (Pro) to enforce.${c.reset}`, | ||
| ` host: ${c.cyan}${hostname}${c.reset}`, | ||
| ` provider: ${provider}`, | ||
| ` method: ${reqMeta.method} ${reqMeta.path}`, | ||
| ` status: ${resp.status != null ? resp.status : "unknown"}`, | ||
| ` duration: ${duration_ms}ms`, | ||
| ` bytes: ${resp.bytes != null ? resp.bytes.toLocaleString() : "unknown"}`, | ||
| ` pid: ${process.pid}`, | ||
| ` time: ${ts}`, | ||
| LOCAL_GATE | ||
| ? ` ${c.dim}→ Local Gate attached — observe now; run with VANTIO_API_KEY for Policy Latch enforce.${c.reset}` | ||
| : ` ${c.dim}→ Optics data log (your machine). See docs/sight-loop.md · Gate enforces on this path.${c.reset}`, | ||
| ].join("\n")); | ||
@@ -630,3 +823,5 @@ return response; | ||
| // network error and propagates unchanged. | ||
| const t0 = Date.now(); | ||
| const response = await _originalFetch.call(this, plan.input, plan.init); | ||
| const duration_ms = Math.max(0, Date.now() - t0); | ||
@@ -637,3 +832,20 @@ // Post-call accounting + reporting — guarded so a metering error never | ||
| const action = plan.redactions.length > 0 ? "REDACTED" : "ALLOWED"; | ||
| const callRec = { hostname, bytes: 0, action, redactions: plan.redactions.length }; | ||
| const reqMeta = extractRequestMeta(plan.input, plan.init); | ||
| const resp = responseMeta(response); | ||
| const callRec = { | ||
| hostname, | ||
| provider: guessProvider(hostname, port), | ||
| method: reqMeta.method, | ||
| path: reqMeta.path, | ||
| scheme: reqMeta.scheme, | ||
| request_bytes: plan.reqBytes || reqMeta.request_bytes, | ||
| bytes: 0, | ||
| status: resp.status, | ||
| ok: resp.ok, | ||
| content_type: resp.content_type, | ||
| duration_ms, | ||
| action, | ||
| redactions: plan.redactions.length, | ||
| ts: new Date().toISOString(), | ||
| }; | ||
| _calls.push(callRec); | ||
@@ -656,4 +868,17 @@ | ||
| } | ||
| report({ target_host: hostname, pid: process.pid, action_taken: action, | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: callRec.bytes }); | ||
| report({ | ||
| target_host: hostname, | ||
| pid: process.pid, | ||
| action_taken: action, | ||
| timestamp_ns: Date.now() * 1e6, | ||
| bytes_severed: callRec.bytes, | ||
| provider: callRec.provider, | ||
| method: callRec.method, | ||
| path: callRec.path, | ||
| status: callRec.status, | ||
| content_type: callRec.content_type, | ||
| request_bytes: callRec.request_bytes, | ||
| duration_ms: callRec.duration_ms, | ||
| ok: callRec.ok, | ||
| }); | ||
| } catch { | ||
@@ -667,2 +892,268 @@ // Accounting/reporting must never break the agent's call. | ||
| // ── Run summary ───────────────────────────────────────────────────────────── | ||
| // Node http/https — same Sight Loop / Gate rules as fetch, last-known policy | ||
| // (request() is sync; fail-open until policy loads). Out-of-scope hosts and | ||
| // the ingest control plane pass through untouched. Raw sockets / curl / browsers | ||
| // stay residual. | ||
| (function patchNodeHttpHttps() { | ||
| const { EventEmitter } = require("node:events"); | ||
| function isControlPlaneRequest(args) { | ||
| try { | ||
| const ingest = new URL(INGEST_URL); | ||
| const a0 = args && args[0]; | ||
| let u = null; | ||
| if (typeof a0 === "string" || (typeof URL !== "undefined" && a0 instanceof URL)) { | ||
| u = new URL(String(a0)); | ||
| } else if (a0 && typeof a0 === "object") { | ||
| const host = a0.hostname || (a0.host ? String(a0.host).split(":")[0] : ""); | ||
| if (!host) return false; | ||
| const port = String(a0.port || (a0.protocol === "https:" ? 443 : 80)); | ||
| const ingestPort = ingest.port || (ingest.protocol === "https:" ? "443" : "80"); | ||
| const path = String(a0.path || a0.pathname || ""); | ||
| return host.toLowerCase() === ingest.hostname.toLowerCase() | ||
| && port === String(ingestPort) | ||
| && path.startsWith("/api/v1/"); | ||
| } | ||
| if (!u) return false; | ||
| const ingestPort = ingest.port || (ingest.protocol === "https:" ? "443" : "80"); | ||
| const reqPort = u.port || (u.protocol === "https:" ? "443" : "80"); | ||
| return u.hostname.toLowerCase() === ingest.hostname.toLowerCase() | ||
| && reqPort === ingestPort | ||
| && u.pathname.startsWith("/api/v1/"); | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
| function destFromArgs(args) { | ||
| try { | ||
| if (!args || !args.length) return { hostname: null, port: null }; | ||
| const a0 = args[0]; | ||
| if (typeof a0 === "string" || (typeof URL !== "undefined" && a0 instanceof URL)) { | ||
| try { | ||
| return destFromHref(String(a0)); | ||
| } catch { return { hostname: null, port: null }; } | ||
| } | ||
| if (a0 && typeof a0 === "object") { | ||
| let hostname = null; | ||
| if (typeof a0.hostname === "string") hostname = a0.hostname; | ||
| else if (typeof a0.host === "string") hostname = a0.host.split(":")[0]; | ||
| else if (typeof a0.href === "string") { | ||
| try { hostname = new URL(a0.href).hostname; } catch { hostname = null; } | ||
| } | ||
| let port = a0.port; | ||
| if ((port == null || port === "") && a0.host && String(a0.host).includes(":")) { | ||
| port = String(a0.host).split(":").pop(); | ||
| } | ||
| if (port == null || port === "") { | ||
| port = a0.protocol === "https:" ? 443 : 80; | ||
| } | ||
| return { hostname, port: String(port) }; | ||
| } | ||
| } catch { /* ignore */ } | ||
| return { hostname: null, port: null }; | ||
| } | ||
| function blockedClientRequest(err) { | ||
| const fake = new EventEmitter(); | ||
| fake.end = () => fake; | ||
| fake.write = () => true; | ||
| fake.abort = () => {}; | ||
| fake.destroy = () => {}; | ||
| fake.setTimeout = () => fake; | ||
| fake.setHeader = () => {}; | ||
| fake.getHeader = () => undefined; | ||
| fake.removeHeader = () => {}; | ||
| process.nextTick(() => fake.emit("error", err)); | ||
| return fake; | ||
| } | ||
| let policySettled = FREE_MODE; | ||
| if (!FREE_MODE && policyReady && typeof policyReady.then === "function") { | ||
| policyReady.then(() => { policySettled = true; }).catch(() => { policySettled = true; }); | ||
| } | ||
| function decideHttp(hostname, port, args) { | ||
| if (!hostname || isControlPlaneRequest(args)) return "pass"; | ||
| if (!inScope(hostname, port)) return "pass"; | ||
| if (FREE_MODE) return "observe"; | ||
| if (policy.enforce) { | ||
| const blocked = hostListed(hostname, policy.blocked_hosts) || | ||
| (policy.allowed_hosts.length > 0 && !hostListed(hostname, policy.allowed_hosts)); | ||
| if (blocked) return policy.dry_run ? "dry_block" : "block"; | ||
| } | ||
| return "observe"; | ||
| } | ||
| function wrapModule(mod, scheme) { | ||
| if (!mod || typeof mod.request !== "function") return; | ||
| if (mod.__vantioPatched) return; | ||
| const origRequest = mod.request.bind(mod); | ||
| const origGet = typeof mod.get === "function" ? mod.get.bind(mod) : null; | ||
| function pendingRequest(args, launch) { | ||
| const pending = new EventEmitter(); | ||
| const buffer = []; | ||
| pending.write = (c, e, cb) => { buffer.push(["write", c, e, cb]); return true; }; | ||
| pending.end = (c, e, cb) => { buffer.push(["end", c, e, cb]); return pending; }; | ||
| pending.abort = () => {}; | ||
| pending.destroy = () => {}; | ||
| pending.setTimeout = () => pending; | ||
| pending.setHeader = () => {}; | ||
| pending.getHeader = () => undefined; | ||
| pending.removeHeader = () => {}; | ||
| policyReady.then(() => { | ||
| policySettled = true; | ||
| try { | ||
| const real = wrapLaunch(args, launch); | ||
| if (real && typeof real.on === "function") { | ||
| real.on("error", (err) => pending.emit("error", err)); | ||
| real.on("response", (res) => pending.emit("response", res)); | ||
| real.on("socket", (sock) => pending.emit("socket", sock)); | ||
| real.on("timeout", () => pending.emit("timeout")); | ||
| real.on("close", () => pending.emit("close")); | ||
| } | ||
| for (const [op, c, e, cb] of buffer) { | ||
| if (op === "write" && real && real.write) real.write(c, e, cb); | ||
| if (op === "end" && real && real.end) real.end(c, e, cb); | ||
| } | ||
| } catch (err) { | ||
| pending.emit("error", err); | ||
| } | ||
| }).catch((err) => pending.emit("error", err)); | ||
| return pending; | ||
| } | ||
| function wrapLaunch(args, launch) { | ||
| if (!FREE_MODE && !policySettled) { | ||
| return pendingRequest(args, launch); | ||
| } | ||
| const dest = destFromArgs(args); | ||
| const hostname = dest.hostname; | ||
| const port = dest.port; | ||
| const decision = decideHttp(hostname, port, args); | ||
| if (decision === "pass") return launch(); | ||
| sendRunTelemetryOnce(hostname); | ||
| const provider = guessProvider(hostname, port); | ||
| const ts = new Date().toISOString(); | ||
| const baseCall = { | ||
| hostname, provider, method: "REQUEST", path: null, scheme, | ||
| request_bytes: null, bytes: 0, status: null, ok: true, | ||
| content_type: null, duration_ms: 0, ts, optics_plane: "app_http", | ||
| }; | ||
| if (decision === "block") { | ||
| _calls.push({ ...baseCall, action: "BLOCKED_HOST", ok: false }); | ||
| report({ | ||
| target_host: hostname, pid: process.pid, action_taken: "BLOCKED_HOST", | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: 0, | ||
| mediation: "node_http", plane: "optics_gate", | ||
| }); | ||
| log(`${c.red}[ ∅ VANTIO ] BLOCKED${c.reset} ${hostname} — Node ${scheme}.request`); | ||
| const err = new Error(`Vantio Gate blocked host: ${hostname}`); | ||
| err.code = "VANTIO_GATE_BLOCKED"; | ||
| return blockedClientRequest(err); | ||
| } | ||
| if (decision === "dry_block") { | ||
| _calls.push({ ...baseCall, action: "DRY_RUN_BLOCKED_HOST" }); | ||
| report({ | ||
| target_host: hostname, pid: process.pid, action_taken: "DRY_RUN_BLOCKED_HOST", | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: 0, | ||
| mediation: "node_http", plane: "optics_gate", | ||
| }); | ||
| log(`${c.yellow}[ ∅ VANTIO ] DRY_RUN${c.reset} ${hostname} — would BLOCK Node ${scheme}.request; dry_run=true passes through`); | ||
| } else { | ||
| _calls.push({ ...baseCall, action: FREE_MODE ? "OBSERVED" : "ALLOWED" }); | ||
| report({ | ||
| target_host: hostname, pid: process.pid, | ||
| action_taken: FREE_MODE ? "OBSERVED" : "ALLOWED", | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: 0, | ||
| mediation: "node_http", plane: "optics_gate", | ||
| }); | ||
| if (FREE_MODE) { | ||
| log(`${c.cyan}[ ∅ VANTIO ] OBSERVED${c.reset} ${hostname} — Node ${scheme}.request`); | ||
| } | ||
| } | ||
| const req = launch(); | ||
| if (!req || typeof req.write !== "function") return req; | ||
| if (FREE_MODE || (!policy.redact_pii && !(policy.enforce && policy.max_request_bytes > 0))) { | ||
| return req; | ||
| } | ||
| const origWrite = req.write.bind(req); | ||
| let written = 0; | ||
| req.write = function vantioHttpWrite(chunk, encoding, cb) { | ||
| try { | ||
| const buf = chunk == null ? Buffer.alloc(0) | ||
| : Buffer.isBuffer(chunk) ? chunk | ||
| : Buffer.from(String(chunk), typeof encoding === "string" ? encoding : "utf8"); | ||
| written += buf.length; | ||
| if (policy.enforce && policy.max_request_bytes > 0 && written > policy.max_request_bytes) { | ||
| if (policy.dry_run) { | ||
| report({ | ||
| target_host: hostname, pid: process.pid, action_taken: "DRY_RUN_BLOCKED_SIZE", | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: written, | ||
| mediation: "node_http", | ||
| }); | ||
| return origWrite(chunk, encoding, cb); | ||
| } | ||
| report({ | ||
| target_host: hostname, pid: process.pid, action_taken: "BLOCKED_SIZE", | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: written, | ||
| mediation: "node_http", | ||
| }); | ||
| log(`${c.red}[ ∅ VANTIO ] BLOCKED${c.reset} ${hostname} — Node ${scheme} request ${written}B exceeds cap`); | ||
| const err = new Error("Vantio Gate blocked request: request_too_large"); | ||
| err.code = "VANTIO_GATE_BLOCKED"; | ||
| process.nextTick(() => req.emit("error", err)); | ||
| return false; | ||
| } | ||
| if (policy.redact_pii && buf.length) { | ||
| const r = redactBody(buf.toString("utf8")); | ||
| if (r.redactions.length) { | ||
| report({ | ||
| target_host: hostname, pid: process.pid, action_taken: "REDACTED", | ||
| timestamp_ns: Date.now() * 1e6, bytes_severed: 0, | ||
| mediation: "node_http", | ||
| }); | ||
| log(`${c.green}[ ∅ VANTIO ] REDACTED${c.reset} ${hostname} — Node ${scheme}.request stripped ${r.redactions.length} PII item(s)`); | ||
| return origWrite(Buffer.from(r.text, "utf8"), undefined, cb); | ||
| } | ||
| } | ||
| } catch { | ||
| // Fail open — never break the agent's write. | ||
| } | ||
| return origWrite(chunk, encoding, cb); | ||
| }; | ||
| return req; | ||
| } | ||
| mod.request = function (...args) { | ||
| try { | ||
| return wrapLaunch(args, () => origRequest(...args)); | ||
| } catch { | ||
| return origRequest(...args); | ||
| } | ||
| }; | ||
| if (origGet) { | ||
| mod.get = function (...args) { | ||
| try { | ||
| return wrapLaunch(args, () => origGet(...args)); | ||
| } catch { | ||
| return origGet(...args); | ||
| } | ||
| }; | ||
| } | ||
| mod.__vantioPatched = true; | ||
| } | ||
| try { wrapModule(require("node:http"), "http"); } catch { try { wrapModule(require("http"), "http"); } catch { /* ignore */ } } | ||
| try { wrapModule(require("node:https"), "https"); } catch { try { wrapModule(require("https"), "https"); } catch { /* ignore */ } } | ||
| })(); | ||
| process.on("exit", () => { | ||
@@ -686,9 +1177,34 @@ if (_calls.length === 0) return; | ||
| try { | ||
| const runsDir = join(homedir(), ".vantio", "runs"); | ||
| const vantioHome = process.env.VANTIO_HOME || join(homedir(), ".vantio"); | ||
| const runsDir = join(vantioHome, "runs"); | ||
| mkdirSync(runsDir, { recursive: true, mode: 0o700 }); | ||
| const machineHost = (() => { try { return osHostname(); } catch { return "unknown"; } })(); | ||
| const providers = [...new Set(_calls.map((x) => x.provider).filter(Boolean))]; | ||
| const errors = _calls.filter((x) => x.error || x.ok === false).length; | ||
| const by_host = {}; | ||
| const by_provider = {}; | ||
| for (const call of _calls) { | ||
| const h = call.hostname || "unknown"; | ||
| const p = call.provider || "unknown"; | ||
| by_host[h] = by_host[h] || { calls: 0, bytes: 0, errors: 0 }; | ||
| by_host[h].calls += 1; | ||
| by_host[h].bytes += call.bytes || 0; | ||
| if (call.error || call.ok === false) by_host[h].errors += 1; | ||
| by_provider[p] = by_provider[p] || { calls: 0, bytes: 0 }; | ||
| by_provider[p].calls += 1; | ||
| by_provider[p].bytes += call.bytes || 0; | ||
| } | ||
| const log = { | ||
| vantio_run_log: "1", | ||
| schema_version: 2, | ||
| plane: "optics", | ||
| workflow: "sight_loop", | ||
| data_note: "Developer egress data log — metadata only; never prompts or completions.", | ||
| trace_id: RUN_TRACE_ID, | ||
| pid: process.pid, | ||
| ppid: typeof process.ppid === "number" ? process.ppid : null, | ||
| node_version: process.version, | ||
| platform: process.platform, | ||
| arch: process.arch, | ||
| cwd: (() => { try { return process.cwd(); } catch { return null; } })(), | ||
| machine: machineHost, | ||
@@ -699,8 +1215,20 @@ started_at: new Date(_startMs).toISOString(), | ||
| cli_version: CLI_VERSION, | ||
| free_mode: FREE_MODE, | ||
| calls: _calls.map((call) => ({ | ||
| hostname: call.hostname, | ||
| action: call.action, | ||
| bytes: call.bytes || 0, | ||
| ts: call.ts || null, | ||
| redactions: call.redactions || 0, | ||
| hostname: call.hostname, | ||
| provider: call.provider || guessProvider(call.hostname), | ||
| method: call.method || null, | ||
| path: call.path || null, | ||
| scheme: call.scheme || null, | ||
| request_bytes: call.request_bytes != null ? call.request_bytes : null, | ||
| bytes: call.bytes || 0, | ||
| status: call.status != null ? call.status : null, | ||
| ok: call.ok != null ? call.ok : null, | ||
| content_type: call.content_type || null, | ||
| duration_ms: call.duration_ms != null ? call.duration_ms : null, | ||
| action: call.action, | ||
| ts: call.ts || null, | ||
| redactions: call.redactions || 0, | ||
| error: call.error || null, | ||
| error_class: call.error_class || null, | ||
| })), | ||
@@ -711,2 +1239,6 @@ summary: { | ||
| hosts: hosts, | ||
| providers, | ||
| errors, | ||
| by_host, | ||
| by_provider, | ||
| redacted: redacted, | ||
@@ -716,2 +1248,7 @@ blocked: blocked, | ||
| }, | ||
| residual: { | ||
| note: "App plane covers fetch + Node http/https. Host Sight covers host egress observe. curl/raw sockets without Host Sight still dark until PE.", | ||
| upgrade_gate: "https://vantio.ai/gate", | ||
| upgrade_enterprise: "https://vantio.ai/enterprise", | ||
| }, | ||
| }; | ||
@@ -744,7 +1281,13 @@ const safeid = RUN_TRACE_ID.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 80); | ||
| ? ` ${c.dim}→ Events routed to your Vantio dashboard.${c.reset}` | ||
| : ` ${c.dim}→ Free plan — observed locally only. Upgrade at vantio.ai/pricing to sync your dashboard.${c.reset}` | ||
| : (LOCAL_GATE | ||
| ? ` ${c.dim}→ Local Gate — events stay on this control plane (${INGEST_URL}).${c.reset}` | ||
| : ` ${c.dim}→ Free plan — observed locally only. Upgrade at vantio.ai/pricing to sync your dashboard.${c.reset}`) | ||
| ); | ||
| } else { | ||
| lines.push(` ${c.dim}→ Run \`vantio prove\` to export an auditor-ready artifact from this run.${c.reset}`); | ||
| lines.push(` ${c.dim}→ Optics observes only — upgrade to Vantio Gate (Pro) to enforce policy.${c.reset}`); | ||
| lines.push( | ||
| LOCAL_GATE | ||
| ? ` ${c.dim}→ Local Gate control plane detected — set VANTIO_API_KEY=soak-pro for enforce on this box.${c.reset}` | ||
| : ` ${c.dim}→ Optics observes only — upgrade to Vantio Gate (Pro) to enforce policy.${c.reset}` | ||
| ); | ||
| if (!telemetryDisabled()) { | ||
@@ -751,0 +1294,0 @@ lines.push(` ${c.dim}Anonymous usage telemetry helps improve Vantio. Opt out with VANTIO_TELEMETRY_DISABLED=1.${c.reset}`); |
+13
-9
| { | ||
| "name": "@vantio/cli", | ||
| "version": "0.3.1", | ||
| "version": "0.3.2", | ||
| "description": "Vantio Optics CLI — wrap any AI agent and observe LLM egress. Blind by design, not a proxy. Sight Loop: wrap → capture → inspect.", | ||
| "license": "MIT", | ||
| "author": "Vantio AI, Inc.", | ||
| "homepage": "https://vantio.ai/platform", | ||
| "homepage": "https://vantio.ai/optics", | ||
| "repository": { | ||
@@ -13,3 +13,5 @@ "type": "git", | ||
| }, | ||
| "bugs": { "url": "https://github.com/vantioai/vantio-open-core/issues" }, | ||
| "bugs": { | ||
| "url": "https://github.com/vantioai/vantio-open-core/issues" | ||
| }, | ||
| "keywords": [ | ||
@@ -33,10 +35,12 @@ "vantio", | ||
| }, | ||
| "files": ["bin"], | ||
| "files": [ | ||
| "bin" | ||
| ], | ||
| "engines": { | ||
| "node": ">=18.3.0" | ||
| }, | ||
| "scripts": { | ||
| "lint": "node --check bin/vantio.js && node --check bin/interceptor.cjs && node --check bin/telemetry.cjs", | ||
| "lint": "node --check bin/vantio.js && node --check bin/interceptor.cjs && node --check bin/telemetry.cjs && node --check bin/llm-hosts.cjs", | ||
| "test": "node --test" | ||
| }, | ||
| "engines": { | ||
| "node": ">=18.3.0" | ||
| } | ||
| } | ||
| } |
+7
-7
| # @vantio/cli | ||
| > Run any AI agent with full observability. Zero code changes. | ||
| > Wrap any AI agent with **Vantio Optics** — free visibility into what it sends. Zero code changes. | ||
@@ -16,7 +16,7 @@ ```bash | ||
| ```bash | ||
| vantio login <your-api-key> # validates + saves your key once | ||
| vantio run node agent.js # no env vars needed — the key is loaded for you | ||
| vantio run node agent.js # Free Observe — no key needed | ||
| vantio login <your-api-key> # optional — Gate / paid features | ||
| ``` | ||
| `vantio login` validates your key against `https://vantio.ai/api/v1/config` and, on success, stores it at `~/.vantio/config.json` (chmod `600`). After that, `vantio run` injects it automatically — no `VANTIO_API_KEY` juggling. Grab your key from your [dashboard](https://vantio.ai/dashboard). | ||
| `vantio login` validates your key against `https://vantio.ai/api/v1/config` and, on success, stores it at `~/.vantio/config.json` (chmod `600`). After that, `vantio run` injects it automatically — no `VANTIO_API_KEY` juggling. Free Optics needs no key. Paid Gate keys come from a trial (`hello@vantio.ai`) or Stripe once live — there is no public self-serve key dashboard yet (`/dashboard` redirects). | ||
@@ -48,3 +48,3 @@ --- | ||
| Wrap any process with `vantio run`. The CLI automatically intercepts every outbound call to a known LLM API — OpenAI, Anthropic, Gemini, Cohere, Mistral, and more — and streams the metadata to your dashboard. | ||
| Wrap any process with `vantio run`. The CLI automatically intercepts every outbound call to a known LLM API — OpenAI, Anthropic, Gemini, Cohere, Mistral, and more — and records connection metadata locally (and to Gate when a key is configured). | ||
@@ -109,3 +109,3 @@ Your code doesn't change. Your agent runs normally. If you've run `vantio login`, the stored key is injected into the child process; an explicit `VANTIO_API_KEY` in your environment always takes precedence. | ||
| - **Free (--local)** — reads local run logs from `~/.vantio/runs/`. No API key needed. Covers only processes started with `vantio run` on this machine. | ||
| - **Pro users** — see all SDK-monitored LLM calls with governance status (ALLOWED / REDACTED / BLOCKED / OBSERVED). | ||
| - **Pro users** — see all SDK-monitored LLM calls with governance status (`OBSERVED` / `ALLOWED` / `REDACTED` / `BLOCKED`). | ||
| - **Enterprise users (Phantom Engine)** — additionally surfaces processes that called LLM endpoints without a Vantio `trace_id` — the **Shadow AI** agents that have no governance coverage. | ||
@@ -197,2 +197,2 @@ | ||
| [vantio.ai](https://vantio.ai) · [Platform](https://vantio.ai/platform) · [Pricing](https://vantio.ai/pricing) · MIT License | ||
| [vantio.ai](https://vantio.ai) · [Optics](https://vantio.ai/optics) · [Pricing](https://vantio.ai/pricing) · MIT License |
Network access
Supply chain riskThis module accesses the network.
Found 4 instances
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
115108
30.38%6
20%2323
37.46%29
3.57%12
50%