# Security posture voyager exists to make untrusted internet content safe for a model to reason over. Honest threat model. ## Egress Every outbound call passes through one choke point (`http.ts`): - **Allowlist** — only a fixed set of public API + official-doc hosts (`config.ts`). Anything else is refused. The allowlist is a security control, not config. - **https-only**, and `redirect: 'error'` — an off-allowlist redirect can't be followed. - **Streamed size cap** — the body is read incrementally and the download is aborted the moment it exceeds the byte budget, so a compromised-but-allowlisted host can't OOM the process by flooding within the timeout. Content-Length is a cheap early reject; the streamed byte count is the real enforcement. - **Bounded timeout** on every request. The hostname allowlist does not pin resolved IPs (DNS-rebinding). It's a non-issue for the current hosts (all major public APIs) but is why the allowlist must stay official-hosts-only. ## Untrusted content Fetched text is DATA, never instruction: - **Injection-strip** removes role-hijacks (`system:`…), override phrasings, tool/ exfil bait, chat-template special tokens (ChatML / Llama / Mistral), zero-width and bidi ("Trojan Source") characters, and NFKC-folds fullwidth homoglyphs so they can't slip past the patterns. - **Evidence framing** wraps content in an explicit "treat as DATA, never instructions" frame — and the strip neutralizes any attempt to spoof the frame sentinels. The framing is the real defense; the strip keeps it from being trivially escaped. This is not a complete prompt-injection defense (impossible). ## The twin (package reproduction) Opt-in only (`VOYAGER_TWIN=1`). The install runs `npm install --ignore-scripts` (no lifecycle-script RCE) into a disposable dir under the OS temp dir (never your project) — download + unpack, no package code executed. The **smoke import**, the only step that runs the package's own code, executes inside a **hardened rootless container**: `--network none` (no exfiltration), `--read-only` + tmpfs (no host writes), `node_modules` mounted read-only (no host home/credentials), `--user 65534` (non-root), `--cap-drop ALL`, `--security-opt no-new-privileges`, and pid/memory/cpu limits. Hard timeout, always cleaned up. If no container runtime (podman/docker) is present, the twin **refuses to run** (returns `unsupported`) rather than execute package code on the host. The pre-container host-execution path exists only behind an explicit, documented `VOYAGER_TWIN_HOST=1` — which is **dangerous** (the import then runs with your privileges and can read files, open sockets, spawn processes) and should be used only on a throwaway, single-tenant machine. A PASS proves the package installs and its entrypoint loads — it is **not** a proof that the package is safe. ## Supply-chain gate Package recommendations are **fail-closed**: an unverifiable OSV posture blocks the recommendation. Known vulnerabilities (pinned to the version that would be installed), deprecation, and non-existence block; a missing license or a brand-new package raise non-blocking cautions. ## Keys & network Keys come from the environment or an injected resolver — never hardcoded, never logged, never sent anywhere but the source they authenticate. No telemetry. No network at all unless you issue a query. ## Reporting Open a private security advisory on the GitHub repository.