v0.2.0 — sandy import: URL/file-driven MCP server configuration
The first feature release beyond the 0.1.x line: sandy import — URL- and file-driven MCP server configuration (PRs #47 / #48). Point Sandy at a URL, a file, or stdin that describes an MCP server and it produces a validated, content-hash-pinned, reviewable candidate config — no more hand-copying endpoints, tool names, and auth shapes into mcp-servers.json from remote docs.
Usage
sandy import https://registry.internal/servers/jira.json # one-shot confirmed fetch → stage
sandy import ./jira.json # air-gap: no network
curl -s <url> | sandy import - # host-fetched path
sandy import <src> --tools jira=read_sprints,read_issues --apply # explicit promote- Staged by default — nothing touches live config until
--apply(or a manual edit). --applymerges intomcp-servers.json+ appends the computedsandbox.allowed_networklines, refuses name collisions, and re-runs the full config load as the final gate (fail-closed — never ships a broken config).
What landed
- The import pipeline (
src/import.ts) — fetch → sniff → validate → stage → (apply), one flow, one trust gate:- The fetch is the one bounded, human-confirmed, audited exception to zero-egress. Sandy prints the exact
GET <url>and the operator confirms (default: no — a cancelled fetch dials nothing). The dial is bounded (30s timeout, 1MB body cap, 3-redirect cap; non-http(s) refused) and recorded as animport_fetchevent (url, finalUrl, sha256, bytes). This is the first change to the egress surface in Sandy's history. - The schema is the law. Fetched bytes run through the existing
mcpServersManifestSchema(fail-closed, exit 3) — nothing is legal because a URL said so. There is no new validation surface. - Content-hash-pinned staging under
.sandy-import/<sha256>.json— the URL is a source at time T, not a live channel: a later change to the URL is a different hash, so it produces a fresh reviewable diff, never a silent mutation (the VCS-reviewed-registry model holds). The review package prints the exactallowed_networklines to add, the env-var names (never values), and the per-server allowlist — which is never auto-expanded (confirm it or pass--tools, validated againstcapabilities). - Explicit apply only.
--applyis the sole path that touches live config: it requires a loadable config, refuses to overwrite an existing server name, preserves existing entries' order, and re-runs the fullloadSandyConfigas the final gate. - Air-gap safe. The file/stdin path needs no network at all.
- v1 is deterministic-only. Prose URLs are rejected fail-closed; the LLM-transcription
--autopath is a documented follow-up (the core has no LLM client in v1 — seedocs/IMPORT_DESIGN.md).
- The fetch is the one bounded, human-confirmed, audited exception to zero-egress. Sandy prints the exact
- Audit events —
import_fetch+import_stagedjoin the forensic record (2 new types insrc/audit/logger.ts). - CLI — the
importverb with--yes/--apply/--tools <s=a,b>; exit codes follow the stable contract (usage-class 2, fail-closed 3). - Docs —
docs/IMPORT_DESIGN.md(design) +docs/PLAN_IMPORT.md(plan); the consumer guide audited so every "zero egress" claim honestly names the exception (security threat-model note, config guide, troubleshooting, dev layout);plugin.jsondescription qualified. - Community health (PR #46) — Contributor Covenant CoC, contributing guide, security policy, issue/PR templates; ASCII diagrams → Mermaid across the docs.
Verification
npm run typecheck && npm test && npm run build→ 346/346 tests (was 323, +23), green.- Conformance green (in-process + the Docker/Firejail
boundary × modematrix + byte-identical-signature identity check): the import dial is a separate, confirmed operator action, not a runtime egress path — the matrix runssandy run/sandy ask(neversandy import), so the no-egress / cross-sandbox guarantees are unaffected. - No new dependencies — the fetch uses global
fetch+node:crypto; the install stays at 3 runtime deps.
Security note
The import dial is a documented, bounded, human-confirmed, audited exception — not a new vulnerability surface. The threat-model write-up (why the exception stays narrow, and its accepted residual risk) is in guide/security.md. No new security advisories — a feature release.