Add browse files button and remember filled in paths in localstorage#4
Closed
Victor-De-Decker wants to merge 3 commits into
Closed
Add browse files button and remember filled in paths in localstorage#4Victor-De-Decker wants to merge 3 commits into
Victor-De-Decker wants to merge 3 commits into
Conversation
insuT0ver
added a commit
that referenced
this pull request
Jun 29, 2026
* feat: verwijder Sparky vLLM-integratie volledig (#4) Verwijdert alle Sparky-gerelateerde code en configuratie: - huddle.ps1: Start-SparkyProxy functie, variabelen en --add-host flag - .ai/opencode/opencode.json: sparky-provider en modelregistratie - .ai/opencode/AGENTS.md: alle Sparky-instructies - .ai/README.md: Sparky-vermeldingen Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: verbeter gateway block response met rijke JSON-foutinfo (#2) De proxy geeft nu een gestructureerde JSON-response bij geblokkeerde requests: - error: REQUEST_BLOCKED_BY_HUDDLE (machine-leesbaar) - blockedEndpoint, reason, actionRequired - devcontainerId voor traceerbaarheid - huddlePortal URL voor directe navigatie - x-huddle-blocked header voor snelle detectie door clients send403, rejectSocket en MITM-padblocking allemaal bijgewerkt. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: voeg resource caps toe voor devcontainers (#3) Nieuwe memory- en CPU-limieten kunnen worden ingesteld bij het starten van devcontainers of als globale standaard in de instellingen. - docker.ts: parseMemoryBytes/parseCpuQuota helpers; StartParams.memory/cpus; HostConfig.Memory/CpuQuota/CpuPeriod worden doorgegeven aan Docker - api.ts: POST /api/docker/start accepteert memory en cpus; GET/POST /api/settings beheert defaultMemory en defaultCpus - api.service.ts: HuddleSettings bijgewerkt naar defaultMemory/defaultCpus - settings.component.ts: UI voor globale resource limieten Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: vervang hardcoded AI-mounts door generieke foldermappings (#5 #6) Verwijdert de vaste Claude/Codex/OpenCode provider-mount logica en vervangt die door een configureerbare folder_mappings tabel. Operators kunnen via de Instellingen-pagina bindmounts of named volumes toevoegen, bewerken en verwijderen. Verwijdert ook de hardcoded AI_CONFIG_DIR en de COPY .ai/… stappen uit de Dockerfile. De .ai-map wordt niet meer ingebakken in het image maar als gewone runtime bindmount aangeboden (standaard host_path = /.ai). De .ai-map is uit git-tracking verwijderd. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: voeg Docker host port approval flow toe met poortbeheer UI (#7) Containers die een host-poort willen binden worden onderschept door de socket-proxy. Niet-goedgekeurde poorten worden geblokkeerd met HTTP 403. Operators keuren poorten goed via de Docker-tab op de containerdetailpagina of via de Docker Access-pagina. Backend: GET/POST/DELETE /api/containers/:name/ports. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * clean up * docs: herschrijf README met uitgebreide getting started en uitleg - Vervang PS1-script door npm run build/start/install commando's - Voeg sectie toe voor containers openen in JetBrains en VS Code - Leg AI-configuratie (.ai/) uit met Windows→container padmapping - Vervang Freshdesk-voorbeeld door Aikido Security extensie - Voeg firewall-beheer sectie toe Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat: standaard resource limieten 8g/2cpu, hernoem Audit Logs naar Network Logs, fix rogue-status voor gestopte containers - Nieuwe devcontainers krijgen standaard 8 GB RAM en 2 CPU tenzij de operator een andere waarde heeft ingesteld - "Audit Logs" hernoemd naar "Network Logs" in sidebar, paginatitel en container-detail - Dashboard markeert gestopte containers nu correct als "Stopped" i.p.v. "Rogue" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
insuT0ver
added a commit
that referenced
this pull request
Jul 15, 2026
…#10/#11/#13) Closes the "no operator auth" cluster from docs/SECURITY-REVIEW.md. Because huddle runs as a container published via -p 3000:3000, the operator and a LAN/sibling attacker both arrive with the docker bridge-gateway source IP, so the source-IP gate cannot separate them and a loopback bind would break the operator port-forward. The fix is a shared operator token, enforced on the whole operator surface. - auth.ts (new): operator token (env HUDDLE_OPERATOR_TOKEN -> persisted /data/operator-token -> generated+logged), timing-safe compare, cookie parse, Bearer/cookie token extraction, isAuthenticated, isAllowedOrigin, and an httpOnly + SameSite=Strict session cookie. - api.ts: onRequest hook now requires a valid token on every operator /api/* except /api/auth/{login,logout,status}; devcontainer whitelist unchanged. The WS upgrade requires an Origin allowlist match AND authentication, killing Cross-Site WebSocket Hijacking (#4) and the unauth terminal/exec shells (#5). Adds login/logout/status endpoints. Extension-upload + credentials endpoints are now behind auth (#10/#11). Token initialised+logged at startup. - db.ts: validateFolderMappingKeys column allowlist -> kills the JSON-key SQL injection (#9), fail-closed on unknown keys; PUT route returns 400. - frontend: auth.service + 401 interceptor + login component; App shell gates on the auth signal (defers StateService/WS until logged in); topbar logout. - cli: apiCall sends Authorization: Bearer from env/~/.huddle/config.json; init generates/reuses the token, passes -e HUDDLE_OPERATOR_TOKEN, persists it to the config, and prints it for the operator. Verified: gateway + CLI tsc clean; ng build clean; vitest 121 passed (new auth.test.ts 21/21). DB-backed suites (incl. new folder-mapping.test.ts) skip locally (better-sqlite3 native binding unbuildable behind the firewall), run in CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
insuT0ver
added a commit
that referenced
this pull request
Jul 15, 2026
* feat(podman): auto-detect container engine and support Podman
Detect Docker vs Podman across the CLI, gateway, and PowerShell manager,
and handle Podman's socket mapping and SELinux/bind-source differences so
`huddle init` and devcontainers work on Podman as well as Docker.
- runtime.ts: add isRemote (engine-in-VM) and securityOpts to ContainerRuntime
- init.ts: pre-create the socket dir in the machine VM for Podman (it won't
auto-create a bind source), pass --security-opt label=disable and
HUDDLE_RUNTIME to the huddle container
- gateway/docker.ts: add SecurityOpt label=disable to devcontainers when
HUDDLE_RUNTIME=podman so they can reach the SELinux-labeled proxy socket
- huddle.ps1: resolve the runtime once (mirrors the CLI) and route every
docker call through it; export HUDDLE_RUNTIME for the delegated `huddle init`
- restore the executable bit on the CLI bin entrypoint (cli/dist/index.js)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(podman): detect Podman even when exposed as the `docker` command
Many Podman setups symlink or shim `docker` to Podman (podman-docker), and
Podman emulates `docker --version`, so name-based detection wrongly picked
"docker" — misconfiguring the socket path, network, and skipping
label=disable, which made `huddle init` fail.
Probe the real engine instead: Podman's `info` exposes Host.ServiceIsRemote
while Docker's schema does not. Use it (via `<cmd> info --format
{{.Host.ServiceIsRemote}}`) in both the CLI runtime resolver and huddle.ps1
so the engine behind the `docker` command is identified correctly.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(podman): make egress network primary so operator reaches the web UI
Under rootless Podman (pasta), host port-forward traffic enters the huddle
container via its PRIMARY interface and is sourced from that subnet. With
devcontainer-net primary, the operator's browser appeared to originate from
the devcontainer subnet, so the source-IP gate rejected the UI with
"endpoint not allowed from devcontainer network".
Start the huddle container on the egress network (runtime.defaultNetwork)
as primary and attach devcontainer-net afterwards, so host traffic arrives
from a non-blocked subnet while devcontainers stay blocked. Podman-only;
Docker forwards via its own proxy (source = loopback/bridge-gw, independent
of the primary network) and keeps the existing ordering.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(podman): tolerate Podman's 'already connected' network error on start
Podman phrases an already-connected network differently than Docker
('network is already connected' vs 'already exists in network'), so the
guard in createAndStartContainer re-threw and aborted the devcontainer
start with a 403. Broaden the guard to match both.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(podman): drop NXDOMAIN internal-net resolvers so gateway egress DNS works
The gateway attaches to the --internal devcontainer networks; Podman then adds
those networks' aardvark-dns (which answers NXDOMAIN for external names) to the
gateway's /etc/resolv.conf. On Alpine/musl the resolver queries all nameservers
in parallel and uses the fastest reply, so the on-host aardvark's instant
NXDOMAIN wins the race and every external lookup fails with ENOTFOUND —
api.anthropic.com and other upstreams then return 502 through the proxy.
Add dns-egress.ts: probe each nameserver and remove the ones that cannot resolve
externally (reordering does not help with musl's parallel queries). It runs at
startup, over a short settling window (to catch the devcontainer-net connect
that `huddle init` performs after the gateway has started), and after every
gateway network connect/disconnect. Fail-safe: leaves resolv.conf untouched when
no resolver works.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(gateway): guard against creating a devcontainer with a name already in use
Ports the pre-flight existing-container check from PR #51 (podman-on-windows):
inspect before create and fail with a clear message instead of letting the
engine error surface later. Also carries over #51's local:prepare npm script
for building/testing the CLI against a local Podman-built gateway image.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* Update gateway/src/dns-egress.ts
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
* feat(gateway): operator authentication for the control plane (#4/#5/#9/#10/#11/#13)
Closes the "no operator auth" cluster from docs/SECURITY-REVIEW.md. Because
huddle runs as a container published via -p 3000:3000, the operator and a
LAN/sibling attacker both arrive with the docker bridge-gateway source IP, so
the source-IP gate cannot separate them and a loopback bind would break the
operator port-forward. The fix is a shared operator token, enforced on the
whole operator surface.
- auth.ts (new): operator token (env HUDDLE_OPERATOR_TOKEN -> persisted
/data/operator-token -> generated+logged), timing-safe compare, cookie parse,
Bearer/cookie token extraction, isAuthenticated, isAllowedOrigin, and an
httpOnly + SameSite=Strict session cookie.
- api.ts: onRequest hook now requires a valid token on every operator /api/*
except /api/auth/{login,logout,status}; devcontainer whitelist unchanged. The
WS upgrade requires an Origin allowlist match AND authentication, killing
Cross-Site WebSocket Hijacking (#4) and the unauth terminal/exec shells (#5).
Adds login/logout/status endpoints. Extension-upload + credentials endpoints
are now behind auth (#10/#11). Token initialised+logged at startup.
- db.ts: validateFolderMappingKeys column allowlist -> kills the JSON-key SQL
injection (#9), fail-closed on unknown keys; PUT route returns 400.
- frontend: auth.service + 401 interceptor + login component; App shell gates on
the auth signal (defers StateService/WS until logged in); topbar logout.
- cli: apiCall sends Authorization: Bearer from env/~/.huddle/config.json; init
generates/reuses the token, passes -e HUDDLE_OPERATOR_TOKEN, persists it to
the config, and prints it for the operator.
Verified: gateway + CLI tsc clean; ng build clean; vitest 121 passed (new
auth.test.ts 21/21). DB-backed suites (incl. new folder-mapping.test.ts) skip
locally (better-sqlite3 native binding unbuildable behind the firewall), run in
CI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli,portal): auto-login link from `huddle init`
Getting past the operator login required manually pasting the token, which is
error-prone. Now `huddle init` prints a full auto-login URL
(http://localhost:<port>/?token=<token>), and the portal reads the ?token=
query param on load, logs in automatically, and strips it from the address bar
(so it does not linger in history/bookmarks). Pasting the token by hand still
works as a fallback.
Verified: CLI tsc clean; ng build clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(gateway): replace the source-IP gate with operator-token auth
The source-IP gate was the root cause of the Podman 403 wmeints hit on the
portal (PR #64): rootless Podman forwards published ports via rootlessport,
which picks its target ("child") IP by iterating a Go map of the container's
networks (GetRootlessPortChildIP). After any gateway restart or dc-net
disconnect that pick is re-rolled, and when it lands on devcontainer-net or a
dc-net-* the operator's traffic arrives sourced from that subnet's bridge
gateway and the gate rejected it. Source IP is simply not a stable signal for
"who is calling" on either engine.
With the operator token (cherry-picked from experiment/48) enforcing auth on
every /api/* route and on the WebSocket upgrade, the gate adds nothing the
token does not already guarantee, so remove it entirely:
- api.ts: drop blockedSubnets/refreshBlockedSubnets/isFromDevcontainer and the
devcontainer-403 branch; the old devcontainer whitelist (POST /api/audit/sudo,
GET /api/tls/ca.crt) becomes the tokenless-public set, matched on path
without query string. Drop the source-IP check on the WS upgrade (origin +
token checks remain).
- net-gate.ts + source-ip-gate.test.ts: deleted, nothing uses them anymore.
- e2e: helpers authenticate with HUDDLE_OPERATOR_TOKEN as Bearer; the
devcontainer boundary test now expects 401 instead of 403; CI starts the
gateway with a fixed token and passes it to the e2e run (the wait-curl
authenticates too, otherwise it spins on 401).
- init.ts comment: network-attach ordering is still needed for Docker's
internal-network port-forward quirk (moby/moby#36174), but no longer for
source-IP reasons.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): fail the pre-flight with a targeted message on auth misconfig
With operator-token auth on the API, a missing/wrong HUDDLE_OPERATOR_TOKEN
made the e2e pre-flight fail with the generic "huddle-API niet bereikbaar —
draait de stack?" even though the stack was up. Split the pre-flight:
unreachable, 401 (token misconfig, names the env var to set), and other
non-2xx now each get their own error so the CI log points straight at the
cause.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* ci: generate the e2e operator token per run instead of hardcoding it
Aikido flagged the fixed Bearer token in the wait-curl. It was a dummy for the
throwaway CI gateway, but a per-run `openssl rand` token (shared with the
docker run, wait-curl and e2e step via GITHUB_ENV) removes the literal from
the repo entirely, which is both quieter and cleaner.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(e2e): split devcontainer→API self-traffic into its two real paths
The 401-expectation was wrong for the default path: devcontainer curl goes
through the egress proxy (curlrc/http_proxy → huddle:80), whose self-traffic
gate answers 403 before the request ever reaches the API — that was already
the 403 this test used to see, even when the source-IP gate existed. Restore
the 403 expectation there under an accurate name, and add the test that
actually covers the gate-removal: a direct call to :3000 (allowed by the
devcontainer iptables) must now bounce off the operator-token auth with 401.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Willem Meints <1550763+wmeints@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
TijsVK
added a commit
to TijsVK/huddle
that referenced
this pull request
Jul 18, 2026
…hz plugin
Adversarial review found the socket-proxy filter (dind-filter.ts) bypassable two
ways, both verified live:
1. the devcontainer mounted the dir holding BOTH the filter's docker.sock AND
the raw inner.sock — `curl --unix-socket inner.sock` skipped the filter.
2. even after splitting them, a nested container could symlink a shared
workspace path to the sidecar's socket dir and bind through it; dockerd
follows symlink bind sources, so a lexical bind guard can't stop it.
Root cause: putting a filter IN FRONT of an unfiltered daemon socket always
leaves that socket reachable. Fix: enforce the guard IN the daemon via
`--authorization-plugin=huddle-authz` (gateway/src/dind-authz.ts). dockerd calls
the plugin before every request on its single socket, so there is no unfiltered
path to reach (findings infosupport#1/infosupport#2 gone) and dockerd handles stream/hijack/chunk
framing natively (the old half-open/grpc/buffering complexity — findings infosupport#4/infosupport#5/infosupport#6
— dissolves).
Topology: sidecar dockerd listens on docker.sock (mounted into the devcontainer);
the gateway serves huddle-authz.sock, mounted into the sidecar at
/run/docker/plugins. No inner.sock. Under authz all binds are safe (docker.sock is
authz-guarded), so the DinD bind guard is dropped — Ryuk / docker-outside-of-
docker work.
Also closes review findings infosupport#3 (MaskedPaths/ReadonlyPaths unmask — deny when the
critical /proc/kcore mask or /proc/sysrq-trigger read-only path is removed; the
CLI sends these populated by default, so only an unmask is refused), infosupport#7
(privileged exec-create), infosupport#8 (any non-default seccomp/apparmor, not just literal
unconfined). validateDindEscape simplified to the kernel/device policy; the pure
host-config-policy.ts is reused by the plugin. The compat harness (lib.sh) now
runs the real plugin via authz-runner.mjs.
Verified live: no inner.sock; raw privileged create over docker.sock → 403;
symlink-bind bypass closed; privileged/device/exec denied; MaskedPaths unmask
denied; compose/buildx/exec/host-path-binds/Ryuk all work. 239 unit tests green
(dind-authz.test.ts replaces dind-filter.test.ts).
TijsVK
added a commit
to TijsVK/huddle
that referenced
this pull request
Jul 18, 2026
…pology - dind-authz: only dockerd should talk to the plugin socket, but a nested container can bind + flood it and all per-container authz servers share the gateway process. Cap maxConnections and set header/request/keepAlive timeouts so one devcontainer can't exhaust the gateway's fds (review finding infosupport#4 residual). - e2e-delete-cleanup: check outer/docker.sock (authz topology), not the old path.
TijsVK
added a commit
to TijsVK/huddle
that referenced
this pull request
Jul 18, 2026
…nfosupport#2 A second adversarial review of the authz plugin found four real host escapes, all verified live and now fixed: - infosupport#1 CRITICAL case-insensitive keys: dockerd's Go JSON decoder matches struct fields case-insensitively, so `{"hostconfig":{"privileged":true}}` slipped past the case-sensitive plugin (saw undefined → allow) while dockerd applied it → privileged container. Verified: create returned 201, container was Privileged. Fix: deep-lowercase all body keys before inspection, in both the validators and the HostConfig extraction. - infosupport#2 CRITICAL plugin-socket swap: the plugin dir was world-writable and bind- mountable, so a nested `-v /run/docker/plugins:/x` could delete/replace huddle-authz.sock with an allow-all plugin (dockerd re-dials on keepalive) → full bypass. Verified: rm succeeded. Fix: mount the plugin dir READ-ONLY into the sidecar (dockerd only connects; gateway writes host-side). Read-only survives parent/root binds (recursive-bind preserves ro). Verified: rm now "Read-only file system" via /run/docker/plugins, /run, /. - infosupport#3 CRITICAL host-kernel bind: the privileged sidecar's /proc,/sys,/dev are the HOST's writable kernel interfaces. `-v /proc/sys:/x; echo … > /x/kernel/core_pattern` set the HOST's core_pattern → host root. Verified live (host core_pattern changed). Fix: deny binds whose source is /proc, /sys, /dev or /. Ryuk (binds docker.sock) and workspace binds still work. Residual: a workspace-symlink variant remains (documented). - infosupport#4 MEDIUM MaskedPaths partial unmask: only /proc/kcore was pinned; drop the rest and host kernel state leaks. Fix: require any explicit MaskedPaths/ReadonlyPaths to be a superset of runc's defaults (null/omitted → daemon defaults, allowed). 256 unit tests green; escape e2e extended with case/infosupport#2/infosupport#3 assertions and verified live: all four escapes closed, compose/Ryuk/host-path binds still work.
TijsVK
added a commit
to TijsVK/huddle
that referenced
this pull request
Jul 18, 2026
TijsVK
added a commit
to TijsVK/huddle
that referenced
this pull request
Jul 18, 2026
…findings infosupport#1/infosupport#2) Review round 4 found two more live host-root escapes: - infosupport#2 HIGH/CRITICAL: the round-3 nosymfollow fix covered only the SHARED mounts, but a nested container can bind ANY sidecar dir (/tmp,/etc,…) — which are NOT nosymfollow'd — plant a symlink there (/tmp/x -> /proc) and bind it, reaching the host's rw /proc via dockerd's symlink resolution. Verified live (host core_pattern written via /tmp). Fix: switch the bind guard from a denylist to an ALLOWLIST — a bind/volume-device source is permitted only if it is a named volume/relative, the authz-guarded docker socket, or under a nosymfollow'd shared root (safeRoots, = the workspace + folder mappings, passed from the gateway to the plugin). The sidecar's own writable dirs are no longer bindable, so there is nowhere to plant a followed symlink. Verified live: /tmp bind refused, host core_pattern unchanged; workspace binds, named volumes and the Ryuk docker.sock bind still work. - infosupport#1 CRITICAL: `docker plugin install` POSTs a LOCAL tar (no egress, no operator prompt) whose config.json can request host bind mounts / caps / all-devices; the managed plugin then runs as root on the privileged sidecar — a full escape that never touches the container-create guard. Verified the endpoints were unguarded. Fix: deny /plugins/create|pull|{name}/enable|set|upgrade|push. Verified live. safeRoots threaded through createDindAuthz/authorize and the harness authz-runner. 263 unit tests green; escape e2e extended (now 22 assertions) and verified live.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.