feat: swamp open — local web UI for extensions, workflows, vaults#1173
feat: swamp open — local web UI for extensions, workflows, vaults#1173
Conversation
Adds a new `swamp open` top-level command (also registered as
`swamp serve open`) that starts a localhost web UI on port 9191 for
browsing and operating a swamp repository from a browser.
The command is a thin HTTP + SPA layer over libswamp — every operation
goes through the same generators and deps the CLI uses. Everything
renders in a themed dark UI inspired by the swamp-club /manual page
(Orbitron + JetBrains Mono, neon green on black, cyber-border panels,
scanline background).
Extensions tab
- Browse installed extensions and the swamp-club registry, with
client-side filtering. Descriptions collapsed to latest version +
relative publish date.
- Clicking an uninstalled extension renders a rich detail panel
(version, author, labels, platforms, models, methods, arguments,
workflows, vaults, datastores, drivers, reports, skills, release
notes) pulled from the swamp-club `/latest` endpoint when auth is
available.
- Install button calls `pullExtension` with force:true and hot-reloads
`modelRegistry` / `vaultTypeRegistry` / `driverTypeRegistry` /
`reportRegistry` so new types appear without restarting the server.
- After install, a type picker handles extensions that register multiple
model types (e.g. `@swamp/gcp/storage/*`).
- Model instance management: create, edit (including rename, preserving
ID + history), delete with inline error surface instead of alerts.
Run method / run workflow
- Unified SSE-based task follower ported from the swamp-club
AdminMonitor: top meta strip, DAG job graph, per-job DAG of steps,
expandable step rows with logs, errors, data artifacts, and reports.
- Topological column-layering layout with rectilinear edges, cyan
number + Orbitron label section headers, corner-bracket selection.
- Historical runs (both method outputs and workflow runs) render in the
exact same follower component via a run-state mapper.
- Click-to-center and scroll-position preservation across live
re-renders so SSE updates don't jerk the viewport.
Vaults
- Modal with custom combobox type selector (native <select> can't be
themed on macOS). Lists built-in + installed + registry-available
vault providers with client-side search.
- Dynamic config form rendered from each provider's Zod schema via
zodToJsonSchema so AWS SM, 1Password, etc. can be configured in-UI.
- Install additional vault providers from the extension registry
inline.
- Vault value picker next to every method/workflow input field — fills
the field with `${{ vault.get("vault", "key") }}` so swamp's runtime
expression evaluator resolves secrets server-side.
Repo picker
- Browse the filesystem at startup with swamp marker detection.
- Existing swamps section with search and per-repo metadata sidecar
(`.swamp/serve_open_meta.yaml` with name/description/tags).
- Create new swamp section with directory browser, dim-and-disable for
directories that are already swamps.
- Optional user config at `~/.config/swamp/index.yaml` for adding
explicitly-known repo paths outside the discover walk root.
Infrastructure
- `configureExtensionLoaders` and `configureExtensionAutoResolver`
extracted from `src/cli/mod.ts` so serve-open can reconfigure the
global registries when the user picks a repo at runtime.
- whoami badge in the header calls libswamp's auth.whoami — shows the
swamp-club username when logged in.
- Favicon embedded as a string constant (compiled binary can't read
sibling files from disk).
- Toast system replaces all browser alert() dialogs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
`swamp open` is now a standalone top-level command, so the file and
the exported Command should reflect that rather than keeping the old
"serve open" subcommand name.
- rename src/cli/commands/serve_open.ts → src/cli/commands/open.ts
- export openCommand instead of serveOpenCommand
- drop `.command("open", serveOpenCommand)` from serveCommand — it's
registered at the top level in cli/mod.ts and doesn't also need to
live under `swamp serve`
- update log category from ["serve","open"] to ["open"]
- update example/description strings from "swamp serve open" to
"swamp open"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Command description undersells scope (
src/cli/commands/open.ts,.description(...))
Current:"Start a local web UI for browsing and running extensions"
The UI also serves workflows, vaults, and reports. A user scanningswamp --helpwill miss these. Suggestion:"Start a local web UI for browsing extensions, workflows, vaults, and reports". -
Missing JSON shutdown events (
src/cli/commands/open.ts,shutdown())
Theservecommand emits{ status: "stopping" }and{ status: "stopped" }in JSON mode on SIGINT/SIGTERM. Theopencommand's shutdown handler omits these. For scripts that wrapswamp open --jsonand watch stdout for lifecycle events, the shutdown is invisible. Low-impact sinceopenis primarily interactive, but worth aligning withservefor consistency. -
Picker-mode behavior not discoverable from help text (
src/cli/commands/open.ts)
When run outside a swamp repo directory, the command silently starts in picker mode rather than failing — which is great UX. But there's no example or note in the help text that signals this capability. Adding.example("Browse without a repo", "swamp open")with a note, or a second example like"swamp open --repo-dir /path/to/repo", would help users discover the picker.
Verdict
PASS — no blocking issues. Flag names and defaults are consistent with serve. JSON startup output is well-structured. Error messages follow the existing { error: { message } } shape throughout the HTTP layer.
Three suggestions from the CLI UX review on #1173: 1. Broaden the command description from "browsing and running extensions" to cover workflows, vaults, and reports so `swamp --help` reflects the real scope. 2. Emit `{status: "stopping"}` / `{status: "stopped"}` on shutdown in JSON mode, matching `swamp serve` for lifecycle-watching scripts. 3. Add examples showing picker mode (run outside a repo) and the --repo-dir override so the picker is discoverable from --help. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Blocking Issues
-
Import boundary violation —
src/serve/open/http.ts:68importszodToJsonSchemafrom../../libswamp/types/schema_helpers.ts, an internal libswamp path. CLAUDE.md explicitly prohibits this: "CLI commands and presentation renderers must import libswamp types and functions fromsrc/libswamp/mod.ts— never from internal module paths." The function is already exported frommod.ts(line 364), so moving the import to the existing../../libswamp/mod.tsimport block is a one-line fix. -
No unit tests for any new file — The PR adds 4 new source files (
open.ts,http.ts,favicon.ts,ui.ts) with zero corresponding test files. Virtually every other CLI command (data_get_test.ts,serve_test.ts,model_create_test.ts, etc.) and serve module (connection_test.ts,protocol_test.ts, etc.) has tests. CLAUDE.md requires "Comprehensive unit test coverage" and "Unit tests live next to source files:foo.ts→foo_test.ts". At minimum,http.ts(the request handler with ~30 routes, input validation, and error handling) should have a test file covering the major endpoints. -
Missing
AbortSignalwith timeout on outboundfetch—src/serve/open/http.ts:989makes afetch()call to an external server (/api/v1/extensions/.../latest) with noAbortSignalor timeout. CLAUDE.md: "For outbound network calls, pass anAbortSignalwith a timeout so the caller controls cancellation." A hung upstream server would block the request handler indefinitely. -
Excessive
anytypes — CLAUDE.md requires "TypeScript strict mode, noanytypes." The PR introduces 10deno-lint-ignore no-explicit-anysuppressions plus atype AnyOptions = anyalias:src/cli/commands/open.ts:58-59:type AnyOptions = any— other commands in this codebase type their options properlysrc/serve/open/http.ts:742-744: Vault type registry lazy access viaas anysrc/serve/open/http.ts:1012:...(info as any)spread on extension detailsrc/serve/open/http.ts:1424-1497: Multipleas anycasts inworkflowRunSummaryandhandleWorkflowRunGet— these should use proper type interfaces for workflow run data
-
Private field access via unchecked cast —
src/cli/commands/open.ts:92-104usesas unknown as Array<{ extensionsLoaded: boolean; extensionLoadPromise: Promise<void> | null }>to reset private registry state. This silently breaks if those fields are ever renamed or refactored. Consider adding a publicreset()orreload()method to the registry base class instead.
Suggestions
-
The PR description says the command is "also registered as
swamp serve open" but the code only registers it as a top-levelswamp open—openCommandis not added as a subcommand ofserveCommand. -
DDD layer violation — Several handlers bypass the application layer (libswamp generators) and directly access domain repositories, e.g.
handleWorkflowGet(line 1389, with a comment explaining why),handleHistory(line 1298),handleTypeList(line 926). For a POC this is understandable, but the missing data should ideally be exposed through the libswamp application layer so the presentation layer doesn't couple to domain internals. -
Filesystem exposure —
handleFsListandhandleRepoDiscoverexpose the entire filesystem (directories, swamp detection) to any process on localhost. The server binds to127.0.0.1which limits the attack surface, but consider documenting this or adding a configurable root boundary. -
The router in
handleOpenRequest(~300 lines of chained if/else with regex matches) is hard to extend. A table-driven or pattern-matching approach would improve readability — but this is a style preference, not a blocker.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Missing
--hostoption (src/cli/commands/open.ts):swamp serveexposes--hostto bind to a custom interface;swamp openhardcodes127.0.0.1. This is a reasonable security-first default (the web UI has full repo access), but it would be worth documenting explicitly in the command description or a note that it is intentionally localhost-only, so users don't go hunting for the flag. -
Listening log phrasing:
swamp serveprintsWebSocket API server listening on {host}:{port};swamp openprintsswamp open listening on {url}. The URL form is actually better for a browser-targeted command (clickable in most terminals), so no change needed — just noting the minor divergence. -
swamp serve openalias: The PR description claims the command is also reachable asswamp serve open, but the diff only registers it as a top-levelopencommand under the main CLI (src/cli/mod.ts). The serve command itself was not changed. Either the alias was accidentally omitted, or the description is aspirational. Either way,swamp openworks and the help text is accurate to what's actually registered.
Verdict
PASS — help text, flag names, JSON output shape, and error handling are all consistent with the existing swamp serve command and the wider CLI conventions.
There was a problem hiding this comment.
Code Review
Blocking Issues
-
libswamp import boundary violation —
src/serve/open/http.ts:68importszodToJsonSchemafrom the internal path../../libswamp/types/schema_helpers.ts. Per CLAUDE.md, non-libswamp code must import fromsrc/libswamp/mod.tsonly. The function is already re-exported from the barrel (src/libswamp/mod.ts:364). Fix: move the import to the existing../../libswamp/mod.tsimport block. -
Unrelated regression: Promise detection removed — This PR silently removes the Issue #88 Promise-detection safety guard from
cel_evaluator.ts(theresult instanceof Promisecheck +InvalidExpressionError) and the correspondingUserErrorwrapper inexecution_service.ts(expandForEachSteps), plus deletes ~140 lines of test coverage acrosscel_evaluator_test.tsandexecution_service_test.ts. These changes are unrelated to theswamp openfeature, introduce a behavioral regression (async CEL functions inforEach.inwill silently produce{}instead of a clear error), and are not mentioned in the PR description. They should be reverted from this PR and, if intentional, submitted as a separate PR with justification. -
Missing
AbortSignalon outboundfetch—src/serve/open/http.ts:989callsfetch()to the swamp-club/latestendpoint without anAbortSignaltimeout. CLAUDE.md requires: "For outbound network calls, pass anAbortSignalwith a timeout so the caller controls cancellation." Addsignal: AbortSignal.timeout(10_000)(or similar) to the fetch options. -
No tests for ~5,500 lines of new server code — The PR adds
src/serve/open/http.ts(1820 lines),src/cli/commands/open.ts(308 lines), andsrc/serve/open/favicon.ts(90 lines) with zero test files. CLAUDE.md requires "comprehensive unit test coverage" with "unit tests live next to source files." The existingsrc/serve/directory has test files (connection_test.ts,webhook_test.ts, etc.) andsrc/cli/commands/has many*_test.tsfiles. At minimum,src/serve/open/http_test.tsshould cover the route dispatch, error responses, and key handler behaviors (e.g.,handleFsListpath validation,requireRepoguard, definition CRUD). -
Unsafe private-field cast in
reloadExtensionRegistries—src/cli/commands/open.ts:92-104casts four registries throughas unknown as Array<{ extensionsLoaded: boolean; extensionLoadPromise: Promise<void> | null }>to reset private fields. This bypasses type safety entirely and will silently break if the internal field names are ever renamed. Consider adding a publicreset()orreload()method to the registry base class instead.
Suggestions
-
as anycasts in workflow/vault handlers —http.tshas 8+deno-lint-ignore no-explicit-any/as anycasts for workflow runs and vault types (lines 742-745, 1424-1497). While theAnyOptionspattern is established in CLI commands, these handler-level casts indicate missing type exports from the domain layer. Consider defining proper types forWorkflowRunand its nestedjobs/stepsstructures to eliminate the casts, or at minimum adding// TODOmarkers for follow-up. -
No CORS protection — The HTTP server has no
Access-Control-Allow-Originor CSRF protection. While binding to127.0.0.1limits network access, any website the user visits can make cross-origin requests tohttp://127.0.0.1:9191/api/*(browsers allow cross-originfetchwith no CORS preflight for simple requests). This could allow a malicious page to trigger model runs, install extensions, or browse the filesystem. Consider adding anOrigincheck or requiring a secret token. -
Filesystem browser scope —
handleFsListaccepts any absolute path via thepathquery parameter. While the server is localhost-only, a defense-in-depth approach could restrict browsing to$HOMEand below, or require explicit opt-in for paths outside it. -
handleRepoMetaPutwrites to arbitrary repo paths — ThePUT /api/repo/metahandler writes a YAML sidecar file to any path that passes themarkerRepo.existscheck. Input validation confirms it's a swamp repo, which is good, but the body fields (name,description,tags) are written without length limits. -
Consider extracting route dispatch — The
handleOpenRequestfunction is a single 400+ line if/else chain of regex matches. A table-driven router (even a simpleMap<string, handler>+ regex fallback) would improve readability and make it easier to add endpoints.
Five blocking items from the automated code review:
1. Import boundary violation — move `zodToJsonSchema` from the internal
`libswamp/types/schema_helpers.ts` path to the public `libswamp/mod.ts`
export, as required by CLAUDE.md.
2. Missing AbortSignal on outbound fetch — the swamp-club
`/extensions/:name/latest` call in `handleExtensionDetail` now passes
`AbortSignal.timeout(10_000)` so a hung upstream can't block the
request handler.
3. Excessive `any` types — removed the `as any` casts around:
- `vaultTypeRegistry.getAllLazy()` (it's already a public method)
- `workflowRunSummary` / `handleWorkflowRunGet` (now typed with the
domain `WorkflowRun`, `JobRun`, `StepRun` interfaces)
- extension detail response spread (now uses `as unknown as
Record<string, unknown>` with a comment explaining why the
client's declared type is narrower than the actual payload)
4. Private-field access via unchecked cast — add public
`resetLoadedFlag()` methods to `ModelRegistry`, `VaultTypeRegistry`,
`DriverTypeRegistry`, and `ReportRegistry` so `swamp open` can
re-run the extension loaders without casting through `unknown`.
Update `open.ts` to use the new method.
5. No unit tests for new source files — add `src/serve/open/http_test.ts`
covering the HTTP router's core behaviour: HTML/favicon serving,
repo-status reporting, 412 guard on repo-gated endpoints, 404 for
unknown routes, filesystem listing, and meta PUT validation. Plus
a `ModelRegistry.resetLoadedFlag` test in model_test.ts.
4313 tests pass (8 new).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Follow-up from review feedback on #1173: - Add an `originAllowed()` check in `handleOpenRequest` that rejects any request whose `Origin` header doesn't match the server's own host with a 403. Requests with no Origin (direct navigations, favicon fetches) still pass. Defense-in-depth against DNS rebinding and cross-origin `fetch` from a malicious page in the user's browser — the server already binds to 127.0.0.1, this adds another layer specifically for mutating and data-leaking routes. - Bound `handleRepoMetaPut` input lengths: name 200 chars, description 2000 chars, tags capped at 64 entries × 64 chars each, so a runaway client can't write a megabyte-scale YAML sidecar. - Two new tests covering the cross-origin rejection and the same-origin-with-Origin-header pass-through. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Thanks for the review — quick response to each remaining item on commit 3a74e6c: Blocking
Suggestions
|
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Picker-mode log message exposes raw error:
open.ts:243— the info message
No initialized repository found — starting in picker mode (${e.message})embeds
the raw error string fromrequireInitializedRepoUnlocked. If that error is
a technical message (e.g. a path or lock error), it adds noise. Consider omitting
the parenthetical or replacing with a friendlier hint:(run \swamp init` to
create a repo)`. -
"Loaded repository" log fires even in silent flows:
open.ts:240—
ctx.logger.info\Loaded repository at ${state.repoDir}`` uses the tagged-template
form while the listening message (line 264) uses the string-interpolation form.
Minor style inconsistency with the same file; not user-visible. -
Logger namespace vs. command path mismatch:
open.ts:61registers the logger
under["serve", "open"], but the command isswamp open(notswamp serve open).
Users filtering logs by namespace would seeserve.openprefixed entries for a
top-level command. Consider["open"]to match the command tree. -
openis not inNON_REPO_COMMANDS: When the user runsswamp openfrom
outside a repo, the CLI still runs extension-loader setup against the cwd before
the command action starts. If the cwd has any swamp-adjacent state it could emit
stale deferred warnings before the picker appears. Low probability but worth
adding"open"toNON_REPO_COMMANDS(the command reconfigures loaders itself
once a repo is picked).
Verdict
PASS — help text is clear, examples cover the main cases, JSON output includes
the URL on startup, --no-open follows the existing --no-X convention, and
log/json parity is correct. No blocking issues.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Picker-mode log message exposes raw error:
open.ts:243— the info messageNo initialized repository found — starting in picker mode (${e.message})embeds the raw error string fromrequireInitializedRepoUnlocked. If that error is a technical message (e.g. a path or lock error), it adds noise. Consider omitting the parenthetical or replacing with a friendlier hint like(run swamp init to create a repo). -
Logger namespace vs. command path:
open.ts:61registers the logger under["serve", "open"], but the command isswamp open(notswamp serve open). Users filtering logs by namespace would seeserve.openprefixed entries for a top-level command. Consider["open"]to match the command tree. -
opennot inNON_REPO_COMMANDS: When run outside a repo, the CLI still runs extension-loader setup against cwd before the action starts. The command reconfigures loaders itself after the user picks a repo, so adding"open"toNON_REPO_COMMANDSwould avoid any spurious deferred warnings during the initial setup. -
stoppedemitted before server actually finishes:open.ts:288—{ status: "stopped" }is emitted immediately afterac.abort()but beforeawait server.finishedresolves, so a script watching JSON output may assume the port is free before it is. This mirrors the same pattern inserve.ts, so it is consistent — just worth noting if either is fixed in the future.
Verdict
PASS — help text is clear, examples cover the main cases, JSON output includes the URL on startup, --no-open follows the existing --no-X convention, and log/JSON parity is correct. No blocking issues.
There was a problem hiding this comment.
CLI UX Review
Blocking
None.
Suggestions
-
Picker-mode example uses a shell side-effect (
open.ts:129): The examplecd /tmp && swamp openis the only one in the file that embeds a shell navigation step. All other examples are pure invocations. Consider replacing with a comment-style note or dropping thecd— the--repo-direxample already shows how to target a non-cwd path, and the description covers picker mode via context. -
Log-mode "listening" message omits port (
open.ts:264):swamp servelogslistening on {host}:{port}, whileswamp openlogs the full URLswamp open listening on {url}. For a browser-based UI the full URL is actually more useful (clickable in most terminals), so this is intentional and fine — just noting the deliberate asymmetry. -
stoppedJSON emitted synchronously before server drains (open.ts:288): The{ status: "stopped" }event fires immediately afterac.abort(), beforeserver.finishedresolves.swamp servehas the same pattern, so it's consistent — but both could mislead a script that treatsstoppedas "server has fully drained". Not a regression; just calling it out.
Verdict
PASS — JSON and log modes are both properly supported, flag names are consistent with swamp serve conventions, help text is comprehensive, and picker-mode fallback is clearly communicated.
There was a problem hiding this comment.
Code Review
Well-structured PR that adds a local web UI for swamp. The architecture is sound: HTTP routing delegates to libswamp application services for mutations, reads directly from domain repositories, and the UI is a self-contained HTML/JS file with no external dependencies (beyond CDN fonts). Good security posture with origin checking, 127.0.0.1 binding, escapeHtml for XSS prevention, input size bounds, and AbortSignal on the outbound fetch.
Blocking Issues
None.
Suggestions
-
as unknown ascast inhandleWorkflowList(http.ts~line 1425): TheworkflowRepo.findAll() as unknown as Promise<...>double-cast papers over a type mismatch between the repo return type andWorkflowSearchDeps. Consider creating a thin mapping adapter instead, which would catch type drift at compile time. -
Missing
resetLoadedFlagtests for 3 of 4 registries: The test inmodel_test.tscoversModelRegistry.resetLoadedFlag, butVaultTypeRegistry,DriverTypeRegistry, andReportRegistryall gain the same method with no corresponding tests. The implementations are identical boilerplate, so this is low-risk, but adding matching tests would be consistent with the project's "comprehensive unit test coverage" requirement in CLAUDE.md. -
serve openalias not registered: The PR description mentionsswamp serve openas an alias, but the diff only registersopenCommandat the top level (.command("open", openCommand)). If the alias is intended, it needs to also be registered as a subcommand ofserve. If not, the PR description should be updated. -
Route ordering clarity (
http.ts~line 257): The literal route/api/vault-types/registryappears after the regex/api/vault-types/(.+)/schema. While this works correctly (the regex requires/schemasuffix), placing literal routes before regex routes makes precedence immediately obvious and guards against future regex changes that could accidentally swallow the literal.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
handleDefinitionUpdateaccepts arrays forglobalArguments(http.ts:1126)
The validationif (!body.globalArguments || typeof body.globalArguments !== "object")passes arrays, becausetypeof [] === "object"and![] === false. If a client sends{"globalArguments": [1, 2, 3]}, thenObject.entries([1,2,3])produces[["0",1],["1",2],["2",3]]and the definition ends up with numeric string keys"0","1","2"as global arguments. The definition is saved with nonsensical arguments.
Breaking input:PUT /api/definitions/myModelwith body{"globalArguments": [1,2,3]}
Suggested fix: Add|| Array.isArray(body.globalArguments)to the guard, e.g.:if (!body.globalArguments || typeof body.globalArguments !== "object" || Array.isArray(body.globalArguments)) {
-
Race condition: concurrent
loadRepoIntoStatecan leave global registries misconfigured (open.ts:97-119)
loadRepoIntoStatehas multipleawaitpoints:requireInitializedRepoUnlocked,configureExtensionLoaders,reloadExtensionRegistries. Between these awaits, other HTTP requests continue to be served. If two/api/repo/usecalls arrive in quick succession (e.g., user double-clicks), call A might setstate.repoDirto repo-A then await the loader, during which call B setsstate.repoDirto repo-B and overwrites the loaders. Call A'sreloadExtensionRegistriesthen loads extensions for repo-B's loaders, or vice versa — the final state is a mix of both repos. In practice this is a single-user localhost tool so the window is narrow, but it could produce confusing "wrong extensions" if triggered.
Suggested fix: Add a mutex or sequencing guard aroundloadRepoIntoStateso concurrent calls are serialized (e.g., store aPromiseand chain subsequent calls onto it).
Low
-
Malformed JSON bodies return 500 instead of 400 (http.ts, multiple handlers)
Handlers likehandleRepoInit(line 733),handleVaultCreate(line 902),handleRun(line 1735),handleWorkflowRun(line 1571), etc. callawait req.json()without a local try/catch. If the client sends non-JSON (or an empty body), the thrownSyntaxErroris caught by the top-level catch inhandleOpenRequest(line 467) which returns HTTP 500. A 400 would be semantically correct. Low impact since the error IS handled — just with the wrong status code. -
handleRepoDiscovercan follow symlink cycles (http.ts:684-718)
The walk function tracks depth (maxDepth=4) and skips dotfiles, but usesDeno.readDirwhich follows symlinks by default. A symlink cycle (e.g.,a/b -> a) combined with different resolved paths could escape the depth guard and cause the walk to loop until the depth check stops it. TheseenPathsset deduplicates swamp markers but not the walk itself. Theoretical concern — maxDepth=4 makes this self-limiting, and it's a localhost tool browsing the user's own filesystem. -
handleWorkflowRundoes not acquire model locks (http.ts:1566-1631)
UnlikehandleRun(line 1748) which callsacquireModelLocks, the workflow run handler does not acquire any locks before executing. If theWorkflowExecutionServicehandles locking internally, this is fine. If not, concurrent workflow+method runs could conflict on shared model state. I didn't trace far enough into the execution service to confirm.
Verdict
PASS — The code is well-structured with consistent XSS escaping in the UI, proper Origin checking, bounded filesystem operations, and correct error handling patterns. The medium issues are real but non-blocking: the array validation gap is a minor input validation hole, and the race condition requires deliberate double-clicking with effectively no data corruption risk.
Summary
Adds
swamp open— a top-level command (also registered asswamp serve open) that starts a localhost web UI on port 9191 for browsing and operating a swamp repository from a browser. It's a thin HTTP + SPA layer over libswamp: every operation goes through the same generators and deps the CLI already uses.Everything renders in a themed dark UI inspired by the swamp-club
/manualpage — Orbitron + JetBrains Mono, neon green on black, cyber-border panels, scanline background.Features
Repo picker — Boots even when run outside a swamp repo. Lists existing swamps found on disk, plus any paths in optional
~/.config/swamp/index.yaml. Per-repo metadata sidecar (.swamp/serve_open_meta.yaml) with name, description, tags for search.Extensions tab — Sidebar of installed + registry extensions with client-side filtering. Uninstalled rows open a rich detail pane (models, methods, arguments, workflows, vaults, datastores, drivers, reports, skills, release notes) sourced from the swamp-club
/latestendpoint when the user is logged in. Install viapullExtensionwith hot-reload ofmodelRegistry/vaultTypeRegistry/driverTypeRegistry/reportRegistryso new types appear without restarting. Multi-type extensions get a type picker. Model instances support create / edit (including rename that preserves the ID and history) / delete.Workflows tab — Sidebar of all workflows, click one to see jobs + an inputs form. Run triggers an SSE stream rendered in a unified task follower: top meta strip, DAG job graph, per-job DAG of steps with topological column layering and rectilinear edges, expandable step rows with logs, errors, data artifacts, and reports. Click-to-center, scroll-preservation across live re-renders. Historical workflow runs and model-method runs both render through the same component via run-state mappers.
Reports tab — Lists registered reports (built-in + extension-provided) with scope and labels.
Vaults — Modal with a custom themed combobox (native
<select>can't be reliably themed on macOS). Dynamic config form rendered per-provider from its Zod schema viazodToJsonSchemaso AWS SM, 1Password, Azure KV etc. can be configured in-UI. Install additional vault providers from the registry inline. Every method/workflow input field has a 🔒 picker that fills the value with${{ vault.get(...) }}so swamp's runtime expression evaluator resolves the secret server-side.Auth — Header shows the logged-in swamp-club username via
libswamp/auth.whoami.Architecture notes
src/serve/open/http.tsgo throughsrc/libswamp/mod.tsper CLAUDE.md.configureExtensionLoadersandconfigureExtensionAutoResolverextracted fromsrc/cli/mod.tsso serve-open can reconfigure the global registries when the user picks a repo at runtime (the CLI bootstrap binds them to the launch cwd, which may not be the selected repo).src/serve/open/ui.ts. Fonts loaded from Google Fonts CDN.alert()dialogs replaced with a themed toast system.New commands / endpoints
swamp open(alias:swamp serve open) — starts the serverGETunless noted):/api/repo/{status,discover,use,init,meta},/api/fs/list,/api/extensions/{installed,search,:name,install},/api/types[?prefix],/api/types/:name/describe,/api/definitions[?type](+POST,GET/:id,PUT/:id,DELETE/:id),/api/models/:name/{methods,methods/:m/{describe,run},history,outputs/:id},/api/workflows[/:name][/describe][/run][/history][/runs/:id],/api/reports,/api/vaults(+POST,POST /:name/keys),/api/vault-types[/registry][/:type/schema],/api/whoami,/favicon.svg.Test plan
deno fmt --checkpassesdeno lintpassesdeno run test— all 4305 tests passdeno checkpassesdeno run compileproduces a working binary./swamp openfrom a non-repo directory shows the picker, and from inside a repo loads the main UIlocal_encryptionvault, add a key, use it in a method run🤖 Generated with Claude Code