cd /news/ai-agents/lyngvi-a-hardened-sandbox-for-ai · home › topics › ai-agents › article
[ARTICLE · art-162764] src=lyngvi.skaperiet.no ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

Lyngvi – A Hardened Sandbox for AI

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.

read11 min views1 publishedNov 2, 2026
Lyngvi – A Hardened Sandbox for AI
Image: source

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.

The 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.

Everything 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:

a first run, start to finishsh

./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

Named after the island in Norse mythology where the wolf Fenrir was bound: a place where great power is kept within defined boundaries.

Making 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.

Every 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.

Lyngvi 2.0.0 · dark theme · sandbox «shop»

The four premises

The product is the boundary

Everything else in Lyngvi is shell around these four. They are the reason an agent can run unattended in a repository you care about.

01

No direct network access

The 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.

squid · allowlist + blocklist

02

No access to your .git

The 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.

copied working tree

03

Secrets live in a gateway sidecar

Internal 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.

never a token in the container

04

Validated, human-approved delivery

Changes 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.

apply · hold · reject

Architecture

One core, two front-ends, three layers of containment

All 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.

Your machine

JavaFX GUIsetup.sh → lyngvi-cli

lyngvi-core git · podman · statestate/ and .sessions/

Shared stack (podman compose)

Squid proxy → InternetGateway sidecar → Internal systems

internal network · no default route

One container per conversation

/work/<repo>cap_drop ALL/work/<repo>cap_drop ALL/work/<repo>cap_drop ALL

Inside the sandbox

A complete workspace, and nothing more

The agent gets a real development environment — git, a toolchain, a package registry through the proxy — with a strictly bounded view of the world.

What the agent sees

Mounted into the container, and nothing else from the host:

/work/<repo>a working copy with a fresh .git, one mount per repository

/work/outputfiles meant for you, outside the repositories

/work/_attachmentsfiles you sent with a message, read-only

/work/AGENTS.mdthe house rules, generated from the sandbox config

HTTP_PROXY, HTTPS_PROXYthe only way out, to the allowlist

<SYSTEM>_GATEWAY_URLa hostname per configured internal system

What it never sees

Structurally out of reach, not merely discouraged:

Your .git — hooks, config, remotes, credential helpers

Anything else on your filesystem; no home directory, no SSH keys

The host machine or services on its localhost

A token for an internal system — the gateway holds those

sudo, new privileges or any Linux capability

A default route: unknown hosts fail at the proxy, not at your firewall

Agents are interchangeable

Claude 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.

Claude Codesubscription, token or API key · streaming · reasoning · attachments

OpenCodesubscription or key · streaming · local and hosted models

Your ownagent.json, or an adapter.sh in any language

Egress control

Every request the agent makes, answered by a list you own

The blocklist is evaluated first, then the allowlist; anything that matches neither is denied. Try a hostname and see what the proxy would answer.

In 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.

Delivery

Nothing leaves the sandbox unseen

A 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.

01

Baseline

Pick where the diff starts: session start, the last delivery, or an earlier one.

02

Patch

Built with a throwaway index, so uncommitted, unstaged and agent-committed work all come along.

03

Classify

Three buckets on the host, by rule: apply, hold, reject.

04

Review

File by file, as hunks or whole files, in the GUI or the CLI.

05

Branch

Commit or stage on a working branch. Protected branches are refused.

session review 7f3a2cClick a held file to include it — held files are only delivered when you name them.

File

Change

Bucket

src/auth.ts

modified+12 −3

apply

src/session/cookie.ts

modified+4 −1

apply

tests/auth.spec.ts

added+58 −0

apply

bin/run.shEXECUTABLE

addedmode 100755

hold

docs/screenshot.pngBINARY

addedbinary

hold

../secrets.envPATH_TRAVERSAL

added+1 −0

reject

apply

Ordinary text changes, applied when you approve them.

hold

Executables, symlinks, binaries and oversized files. Delivered only when named explicitly.

reject

Path traversal, absolute paths, .git segments, invalid UTF-8, unparseable patches. Never delivered, whatever you click.

Deliver changesgui

The 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.

The gateway

Reach the intranet without handing over the key

Some 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.

What it looks like from inside

agentGET https://wiki.gateway.local/page/48219

gateway200 · text/markdownallowed: space OPS is on the list

agentGET https://wiki.gateway.local/space/FINANCE

gateway403 · not allowednot allowed yet — the human must add it

the gateway answers before any upstream call

./setup.sh gateway instance add wiki --plugin confluence \
    --hostname wiki.gateway.local
printf '%s\n' "$TOKEN" | ./setup.sh gateway secret set wiki \
    --url https://wiki.example.com --token-stdin
./setup.sh gateway allow wiki add space:OPS

Every system type is a plugin

http 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.

http · built inconfluence.jarjira.jarGatewayPlugin · your own

What the runtime guarantees, whatever a plugin does

01Unknown hostname → 404, with no upstream call at all

02GET and HEAD only, unless the instance and the profile both allow writes

03The upstream object is pinned to one origin; .. and absolute URLs are refused

04The secret is injected by the runtime and exists as a string nowhere a plugin can read

05The item allowlist is re-read on every lookup and enforced before the handler runs

06An empty allowlist means deny everything

07no-store on every response; set-cookie and hop-by-hop headers stripped

08Masked logging: Authorization values and *_TOKEN variables are never written

09A broken instance fails alone; /healthz reports it as configured: false

The rest of it

Built to be lived in

The boundaries are the point, but a sandbox you avoid is a sandbox that protects nothing.

01

One container per conversation

Sessions cannot see or edit each other’s work, and resource limits mean something. Start, stop, shell in, garbage-collect the idle ones.

02

Profiles instead of hard-coding

Hostnames, allowlists, mirrors, CA names, protected branches, toolchains and language live in profile.json and a profile pack. No organisation is baked into the source.

03

Toolchains as fragments

A slim Debian base plus the fragments you pick: Java 17/21/25, Node 22, Python 3.12, Go, .NET 8, Maven, Gradle, Kotlin.

04

Resource presets

review (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.

05

--json on every command

One 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.

06

A desktop app, not a wrapper

Nav 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.

07

Pinned and reproducible

Base 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.

08

Attachments and local models

Drop 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.

09

English and Norwegian

Every user-visible string comes from a resource bundle, in the CLI and the GUI alike. --lang picks one per command.

Hardening, in one table

What is switched off, everywhere

Network

Internal network only; no default route from a session container

Capabilities

cap_drop ALL and no-new-privileges on every container, session containers included

User

Unprivileged agent user with your UID and GID, so mounted files keep their owner

Filesystem

Copied working tree; the gateway runs read-only; nothing else from the host is mounted

No system config, no terminal prompt, no signing, identity fixed by the profile

Credentials

Agent credentials under state/agents/<id>/, mounted only into that agent’s containers; no tokens in .env

Branches

Protected branches from the profile plus the repository default; delivery always lands on a working branch

Images

Base image, Squid and gateway base pinned by digest; CI verifies the pins

Install

From nothing to a running sandbox

The 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.

Requirements: macOS or Linux · Podman 5+ · Java 25 only if you build from source

In the app

Install 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.

1Folder and repositoriesWhere the sandbox lives, and which repositories a conversation may be given a copy of.

2Base imageThe toolchains the profile asks for and the agents it enables, built once.

3Shared stackThe proxy and the gateway, started and left running while the sandbox is in use.

4CheckThe same checks as doctor: the container runtime, the image, the stack, the repositories and the certificate.

5AgentLog in once, or point the sandbox at a model server of your own. Either can wait until later.

6ReadyThe sandbox is made, and the first conversation can open as the wizard closes.

New sandboxgui

Step 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.

macOS: 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.

── more in #ai-agents 4 stories · sorted by recency
── more on @lyngvi 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/lyngvi-a-hardened-sa…] indexed:0 read:11min 2026-11-02 · —