{"slug": "lyngvi-a-hardened-sandbox-for-ai", "title": "Lyngvi – A Hardened Sandbox for AI", "summary": "Lyngvi 2.0.0 has launched as a hardened, agent-agnostic sandbox that runs autonomous coding agents against a user's own repositories, with the agent working in a copied tree with a fresh .git, no default network route, and secrets held in a gateway sidecar. The tool ships as a JavaFX desktop app and a CLI that both call the same Java core, which is the only component that talks to git and Podman; outbound connections pass through a Squid proxy with an allowlist and an explicit blocklist of known exfiltration endpoints evaluated first. Changes return as a patch classified file by file on the host and committed only to a working branch after human approval — never to main — with the sample session showing 2 files committed on agent/fix-login while bin/run.sh was held as EXECUTABLE and ../secrets.env rejected for PATH_TRAVERSAL.", "body_md": "Lyngvi is a hardened, agent-agnostic sandbox for running autonomous coding agents against your own repositories. You drive it from a desktop app — the conversation, the diff and the delivery in one window — or from the command line, which is the same product without the window. The agent gets a working copy, a toolchain and a controlled way out. It never gets your credentials, your git history or a route to the internet.\n\nThe app: a conversation per task, the agent’s work as it happens, and the bar above the composer saying what is waiting to be delivered — three files, one of them held back.\n\nEverything the app does is also a command. The CLI ships inside the app and in a package of its own, and both call the same core:\n\na first run, start to finishsh\n\n```\n./setup.sh --sandbox ~/sandboxes/shop init --repo ~/src/shop✔ sandbox created · profile \"default\" · 1 repository./setup.sh build && ./setup.sh up✔ localhost/lyngvi-base:2.0.0✔ proxy and gateway up on 10.89.3.0/24 (internal)./setup.sh session new --title \"Fix the login redirect\" --preset dev✔ session 7f3a2c · container lyngvi-shop-7f3a2c · 8g / 4.0 cpusecho \"Find and fix the login redirect bug\" | ./setup.sh session ask 7f3a2c  … reading src/auth.ts, running the test suite  ✗ DENIED api.telemetry.example → not on the allowlist  3 files changed, tests green./setup.sh session review 7f3a2c  apply    src/auth.ts            +12 −3  apply    tests/auth.spec.ts     +58 −0  hold     bin/run.sh             EXECUTABLE  reject   ../secrets.env         PATH_TRAVERSAL./setup.sh session apply 7f3a2c --branch agent/fix-login✔ 2 files committed on agent/fix-login\n```\n\nNamed after the island in Norse mythology where the wolf Fenrir was bound: a place where great power is kept within defined boundaries.\n\nMaking a sandbox, talking to the agent, reading the diff and delivering what you approve are all in the app, and none of it needs the terminal. Every screen below is the real JavaFX app, run against a sandbox with one repository, three conversations and a change waiting to be reviewed. No mock-ups, no retouching.\n\nEvery reply shows what the agent read, searched, ran and edited — with +/− counts on each edit — and what the turn cost in tokens and dollars. The strip above the composer says what is waiting for delivery, and that one file is held back.\n\nLyngvi 2.0.0 · dark theme · sandbox «shop»\n\nThe four premises\n\nThe product is the boundary\n\nEverything else in Lyngvi is shell around these four. They are the reason an agent can run unattended in a repository you care about.\n\n01\n\nNo direct network access\n\nThe session container sits on an internal network with no default route. Every outbound connection goes through a Squid proxy with an allowlist, and an explicit blocklist of known exfiltration endpoints is evaluated first.\n\nsquid · allowlist + blocklist\n\n02\n\nNo access to your .git\n\nThe agent works in a copy with a fresh .git — no hooks, no config, no remotes, no credential helper. The whole git attack surface is removed structurally, not by policy.\n\ncopied working tree\n\n03\n\nSecrets live in a gateway sidecar\n\nInternal systems are reached through a read-only, allowlisted API that holds the token. The agent only ever sees a hostname. A plugin cannot read the secret: the runtime injects it into a locked upstream object.\n\nnever a token in the container\n\n04\n\nValidated, human-approved delivery\n\nChanges come back as a patch, classified on the host file by file, reviewed by you, and committed to a working branch. Never to main, never silently.\n\napply · hold · reject\n\nArchitecture\n\nOne core, two front-ends, three layers of containment\n\nAll logic lives in a Java core that both the CLI and the JavaFX GUI call in-process. The core is the only thing that talks to git and Podman. Select a part of the diagram to see what it guarantees.\n\nYour machine\n\nJavaFX GUIsetup.sh → lyngvi-cli\n\nlyngvi-core git · podman · statestate/ and .sessions/\n\nShared stack (podman compose)\n\nSquid proxy → InternetGateway sidecar → Internal systems\n\ninternal network · no default route\n\nOne container per conversation\n\n/work/<repo>cap_drop ALL/work/<repo>cap_drop ALL/work/<repo>cap_drop ALL\n\nInside the sandbox\n\nA complete workspace, and nothing more\n\nThe agent gets a real development environment — git, a toolchain, a package registry through the proxy — with a strictly bounded view of the world.\n\nWhat the agent sees\n\nMounted into the container, and nothing else from the host:\n\n/work/<repo>a working copy with a fresh .git, one mount per repository\n\n/work/outputfiles meant for you, outside the repositories\n\n/work/_attachmentsfiles you sent with a message, read-only\n\n/work/AGENTS.mdthe house rules, generated from the sandbox config\n\nHTTP_PROXY, HTTPS_PROXYthe only way out, to the allowlist\n\n<SYSTEM>_GATEWAY_URLa hostname per configured internal system\n\nWhat it never sees\n\nStructurally out of reach, not merely discouraged:\n\nYour .git — hooks, config, remotes, credential helpers\n\nAnything else on your filesystem; no home directory, no SSH keys\n\nThe host machine or services on its localhost\n\nA token for an internal system — the gateway holds those\n\nsudo, new privileges or any Linux capability\n\nA default route: unknown hosts fail at the proxy, not at your firewall\n\nAgents are interchangeable\n\nClaude Code and OpenCode ship as reference adapters, with subscription login or a token. An adapter is an agent.json plus a command contract, so a third agent is a config file, not a fork. Models running on your own machine (Ollama, LM Studio, MLX) are reached through the proxy on the ports your profile names.\n\nClaude Codesubscription, token or API key · streaming · reasoning · attachments\n\nOpenCodesubscription or key · streaming · local and hosted models\n\nYour ownagent.json, or an adapter.sh in any language\n\nEgress control\n\nEvery request the agent makes, answered by a list you own\n\nThe blocklist is evaluated first, then the allowlist; anything that matches neither is denied. Try a hostname and see what the proxy would answer.\n\nIn the GUI the same list is a table: every host the proxy denied, with a count, and an Allow button that adds it to your section of the allowlist and reloads the proxy.\n\nDelivery\n\nNothing leaves the sandbox unseen\n\nA delivery is measured from a baseline — the session start, the last delivery, or any delivery in between — built into a patch inside the container, and then classified on the host. Classification is yours, not the agent’s.\n\n01\n\nBaseline\n\nPick where the diff starts: session start, the last delivery, or an earlier one.\n\n02\n\nPatch\n\nBuilt with a throwaway index, so uncommitted, unstaged and agent-committed work all come along.\n\n03\n\nClassify\n\nThree buckets on the host, by rule: apply, hold, reject.\n\n04\n\nReview\n\nFile by file, as hunks or whole files, in the GUI or the CLI.\n\n05\n\nBranch\n\nCommit or stage on a working branch. Protected branches are refused.\n\nsession review 7f3a2cClick a held file to include it — held files are only delivered when you name them.\n\nFile\n\nChange\n\nBucket\n\nsrc/auth.ts\n\nmodified+12 −3\n\napply\n\nsrc/session/cookie.ts\n\nmodified+4 −1\n\napply\n\ntests/auth.spec.ts\n\nadded+58 −0\n\napply\n\nbin/run.shEXECUTABLE\n\naddedmode 100755\n\nhold\n\ndocs/screenshot.pngBINARY\n\naddedbinary\n\nhold\n\n../secrets.envPATH_TRAVERSAL\n\nadded+1 −0\n\nreject\n\napply\n\nOrdinary text changes, applied when you approve them.\n\nhold\n\nExecutables, symlinks, binaries and oversized files. Delivered only when named explicitly.\n\nreject\n\nPath traversal, absolute paths, .git segments, invalid UTF-8, unparseable patches. Never delivered, whatever you click.\n\nDeliver changesgui\n\nThe same review in the GUI: what will be applied, what is held back until you tick it, what is rejected — and the diff of the selected file beside it.\n\nThe gateway\n\nReach the intranet without handing over the key\n\nSome work needs the wiki, the issue tracker or an internal API. The gateway is a sidecar that holds the credential and exposes a narrow, read-only, allowlisted view of the system. The agent gets a hostname and a 403 when it asks for something you have not allowed.\n\nWhat it looks like from inside\n\nagentGET https://wiki.gateway.local/page/48219\n\ngateway200 · text/markdownallowed: space OPS is on the list\n\nagentGET https://wiki.gateway.local/space/FINANCE\n\ngateway403 · not allowednot allowed yet — the human must add it\n\nthe gateway answers before any upstream call\n\n```\n./setup.sh gateway instance add wiki --plugin confluence \\\n    --hostname wiki.gateway.local\nprintf '%s\\n' \"$TOKEN\" | ./setup.sh gateway secret set wiki \\\n    --url https://wiki.example.com --token-stdin\n./setup.sh gateway allow wiki add space:OPS\n```\n\nEvery system type is a plugin\n\nhttp is built in and covers any REST API behind a static secret. confluence and jira ship as jars. Your own is a class implementing GatewayPlugin plus a service entry, dropped in with gateway plugin install. The codebase ships with no systems configured — a profile pack adds what your organisation needs.\n\nhttp · built inconfluence.jarjira.jarGatewayPlugin · your own\n\nWhat the runtime guarantees, whatever a plugin does\n\n01Unknown hostname → 404, with no upstream call at all\n\n02GET and HEAD only, unless the instance and the profile both allow writes\n\n03The upstream object is pinned to one origin; .. and absolute URLs are refused\n\n04The secret is injected by the runtime and exists as a string nowhere a plugin can read\n\n05The item allowlist is re-read on every lookup and enforced before the handler runs\n\n06An empty allowlist means deny everything\n\n07no-store on every response; set-cookie and hop-by-hop headers stripped\n\n08Masked logging: Authorization values and *_TOKEN variables are never written\n\n09A broken instance fails alone; /healthz reports it as configured: false\n\nThe rest of it\n\nBuilt to be lived in\n\nThe boundaries are the point, but a sandbox you avoid is a sandbox that protects nothing.\n\n01\n\nOne container per conversation\n\nSessions cannot see or edit each other’s work, and resource limits mean something. Start, stop, shell in, garbage-collect the idle ones.\n\n02\n\nProfiles instead of hard-coding\n\nHostnames, allowlists, mirrors, CA names, protected branches, toolchains and language live in profile.json and a profile pack. No organisation is baked into the source.\n\n03\n\nToolchains as fragments\n\nA slim Debian base plus the fragments you pick: Java 17/21/25, Node 22, Python 3.12, Go, .NET 8, Maven, Gradle, Kotlin.\n\n04\n\nResource presets\n\nreview (2g, 1 cpu), dev (8g, 4), build (16g, 6) — or one derived from your machine. Chosen per session, by what the session is about to do.\n\n05\n\n--json on every command\n\nOne JSON document on stdout, documented exit codes, and a line-delimited event stream from session ask. The CLI contract is the public, language-independent API.\n\n06\n\nA desktop app, not a wrapper\n\nNav rail, streaming chat with tool calls and usage, a file pane, a diff viewer with whole-file mode, and the same review and apply flow as the CLI — the flow itself living in the core both of them call.\n\n07\n\nPinned and reproducible\n\nBase image, Squid and the gateway base are pinned by digest; the agent CLI version is pinned in its fragment. doctor --deep proves the egress rules from a throwaway container.\n\n08\n\nAttachments and local models\n\nDrop files or paste an image into the composer. Point the sandbox at Ollama, LM Studio or an MLX server on your own machine; the proxy opens just those ports.\n\n09\n\nEnglish and Norwegian\n\nEvery user-visible string comes from a resource bundle, in the CLI and the GUI alike. --lang picks one per command.\n\nHardening, in one table\n\nWhat is switched off, everywhere\n\nNetwork\n\nInternal network only; no default route from a session container\n\nCapabilities\n\ncap_drop ALL and no-new-privileges on every container, session containers included\n\nUser\n\nUnprivileged agent user with your UID and GID, so mounted files keep their owner\n\nFilesystem\n\nCopied working tree; the gateway runs read-only; nothing else from the host is mounted\n\nNo system config, no terminal prompt, no signing, identity fixed by the profile\n\nCredentials\n\nAgent credentials under state/agents/<id>/, mounted only into that agent’s containers; no tokens in .env\n\nBranches\n\nProtected branches from the profile plus the repository default; delivery always lands on a working branch\n\nImages\n\nBase image, Squid and gateway base pinned by digest; CI verifies the pins\n\nInstall\n\nFrom nothing to a running sandbox\n\nThe app ships as a jpackage build with the CLI inside it, and the CLI on its own as a jlink image with its own runtime. You do not install Java.\n\nRequirements: macOS or Linux · Podman 5+ · Java 25 only if you build from source\n\nIn the app\n\nInstall the app, and the «New sandbox» wizard takes you from nothing to a sandbox you can start a conversation in. There is nothing else to install: the CLI ships inside the app, and a sandbox made here links its own cli/ to it — so the same folder is yours to work in from the terminal whenever you want to.\n\n1Folder and repositoriesWhere the sandbox lives, and which repositories a conversation may be given a copy of.\n\n2Base imageThe toolchains the profile asks for and the agents it enables, built once.\n\n3Shared stackThe proxy and the gateway, started and left running while the sandbox is in use.\n\n4CheckThe same checks as doctor: the container runtime, the image, the stack, the repositories and the certificate.\n\n5AgentLog in once, or point the sandbox at a model server of your own. Either can wait until later.\n\n6ReadyThe sandbox is made, and the first conversation can open as the wizard closes.\n\nNew sandboxgui\n\nStep one of six. Nothing has been made yet — «Create» writes the sandbox folder and renders its configuration, and the wizard moves on to the image.\n\nmacOS: a .dmg, opened the first time with right-click → Open, since the app is not notarised yet. Linux: a .deb or the unpacked app image. Then the conversation, the diff and the delivery are the windows above.", "url": "https://wpnews.pro/news/lyngvi-a-hardened-sandbox-for-ai", "canonical_source": "https://lyngvi.skaperiet.no", "published_at": "2026-11-02 21:31:49+00:00", "updated_at": "2026-11-02 21:51:19.625672+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools"], "entities": ["Lyngvi", "Lyngvi 2.0.0", "Squid", "Podman", "JavaFX", "lyngvi-core", "lyngvi-cli"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/lyngvi-a-hardened-sandbox-for-ai", "markdown": "https://wpnews.pro/news/lyngvi-a-hardened-sandbox-for-ai.md", "text": "https://wpnews.pro/news/lyngvi-a-hardened-sandbox-for-ai.txt", "jsonld": "https://wpnews.pro/news/lyngvi-a-hardened-sandbox-for-ai.jsonld"}}