Skip to content

Add browse files button and remember filled in paths in localstorage#4

Closed
Victor-De-Decker wants to merge 3 commits into
mainfrom
add-browse-files-button
Closed

Add browse files button and remember filled in paths in localstorage#4
Victor-De-Decker wants to merge 3 commits into
mainfrom
add-browse-files-button

Conversation

@Victor-De-Decker

Copy link
Copy Markdown
Contributor

No description provided.

@insuT0ver insuT0ver closed this Jun 23, 2026
@insuT0ver
insuT0ver deleted the add-browse-files-button branch June 25, 2026 14:01
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants