Releases: systeminit/swamp
swamp 20260227.010040.0-sha.9cd80fb4
What's Changed
- feat: add extension workflows support (
extensions/workflows/) (#500)
Summary
Adds support for discovering and executing workflow YAML files from extensions/workflows/, mirroring the existing extensions/models/ pattern. This means extensions installed from third-party sources can now ship workflow files that are automatically discoverable and executable without manually copying them into .swamp/workflows/.
- Extension workflows are read-only — they cannot be deleted or overwritten via CLI commands
- Extension workflows are transparently merged with user workflows —
workflow search,workflow run, and all other workflow commands see them alongside regular workflows - Name conflicts are resolved by giving priority to primary workflows in
.swamp/workflows/— extension workflows with the same name are hidden - The workflows directory is configurable via
workflowsDirin.swamp.yamlor theSWAMP_WORKFLOWS_DIRenvironment variable (default:extensions/workflows)
What this means for users
Previously, if you installed an extension that included workflow YAML files, you had to manually copy those files into your .swamp/workflows/ directory. Now, extensions can place their workflows in extensions/workflows/ and they'll just work:
# Extension workflows are discoverable
swamp workflow search --json
# Returns workflows from both .swamp/workflows/ AND extensions/workflows/
# Extension workflows are executable
swamp workflow run my-extension-workflow
# Runs exactly like a regular workflow, creating runs in .swamp/workflow-runs/
# Extension workflows are read-only
swamp workflow delete my-extension-workflow
# Error: "Cannot delete extension workflow 'my-extension-workflow'. Extension workflows are read-only."If you want to override an extension workflow, create one with the same name in .swamp/workflows/ — your version always takes priority.
Architecture: Composite Repository Pattern
Two new repository classes implement this transparently:
ExtensionWorkflowRepository— Read-only repo that recursively discovers*.yamlworkflows from the configured extensions directory. Broken YAML files are logged as warnings and skipped (resilient loading).CompositeWorkflowRepository— Wraps the existingYamlWorkflowRepository(primary/mutable) +ExtensionWorkflowRepository(secondary/read-only). All consumers use theWorkflowRepositoryinterface, so this is a transparent change.
The RepositoryContext.workflowRepo type was widened from YamlWorkflowRepository to WorkflowRepository (the interface). This required no changes to downstream consumers since WorkflowExecutionService, SymlinkRepoIndexService, and all CLI commands already work against the interface.
Changes
New files
src/cli/resolve_workflows_dir.ts—resolveWorkflowsDir()with priority: env var > config > defaultsrc/infrastructure/persistence/extension_workflow_repository.ts— Read-only workflow reposrc/infrastructure/persistence/composite_workflow_repository.ts— Merges primary + extension repos
Modified files
src/infrastructure/persistence/repo_marker_repository.ts— AddedworkflowsDir?: stringtoRepoMarkerDatasrc/infrastructure/persistence/repository_factory.ts— Wired composite repo, widenedworkflowRepotypesrc/cli/repo_context.ts— ResolvesworkflowsDirfrom marker and passes to factorysrc/cli/completion_types.ts— Shell completions now include extension workflowssrc/cli/mod.ts— Re-exportsresolveWorkflowsDirsrc/cli/commands/workflow_search.ts— UsesWorkflowRepositoryinterface typesrc/cli/commands/workflow_delete.ts— Guards against deleting extension-only workflowssrc/cli/commands/workflow_edit.ts— Falls back to actual source file for extension workflows
Test plan
Automated tests (46 new tests, all 2272 pass)
resolveWorkflowsDir (5 tests in src/cli/mod_test.ts)
- Returns default
extensions/workflowswith null marker - Returns default when marker has no
workflowsDir - Uses
marker.workflowsDirwhen set SWAMP_WORKFLOWS_DIRenv var takes priority over configSWAMP_WORKFLOWS_DIRenv var takes priority over default
ExtensionWorkflowRepository (10 tests in extension_workflow_repository_test.ts)
- Discovers YAML workflows from a directory
- Discovers workflows in subdirectories (recursive walk)
- Returns empty array for empty directory
- Returns empty array for non-existent directory
- Skips broken YAML files with warnings (resilient loading)
findByNamereturns matching workflowfindByNamereturns null for non-existent namefindByIdreturns matching workflowsave()rejects withUserError(read-only enforcement)delete()rejects withUserError(read-only enforcement)
CompositeWorkflowRepository (11 tests in composite_workflow_repository_test.ts)
findByNamechecks primary first (precedence)findByNamefalls back to extensionfindByNamereturns null when not found in eitherfindByIdchecks primary first (precedence)findByIdfalls back to extensionfindAlldeduplicates by name, primary winssavedelegates to primarydeletedelegates to primary- Works with null extension repo (backwards compatibility)
nextIddelegates to primarygetPathdelegates to primary
Manual end-to-end testing (verified with compiled binary)
- Created a fresh swamp repo with
swamp repo init - Created a
command/shellmodel namedhello - Created
extensions/workflows/greet.yamlwith a workflow that runsecho 'Hello from extension workflow!' swamp workflow search --json— confirmed thegreetworkflow appears in results with correct name, description, and job countswamp workflow search --json greet— confirmed exact-match returns full workflow detailsswamp workflow run greet— confirmed the workflow executes successfully, runs the shell command, prints "Hello from extension workflow!", saves data outputs, and completes with status"succeeded"
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.010040.0-sha.9cd80fb4/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.010040.0-sha.9cd80fb4/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.010040.0-sha.9cd80fb4/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.010040.0-sha.9cd80fb4/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260227.003433.0-sha.349ae436
What's Changed
- feat: add integrity verification to
swamp extension pull(#498)
Summary
Part 2 of extension integrity verification. Part 1 (swamp-club PR #105) added server-side SHA-256 checksum computation during extension push and a GET .../checksum API endpoint. This adds client-side verification to swamp extension pull so the CLI computes the SHA-256 of the downloaded archive, fetches the expected checksum from the server, and compares them — blocking extraction on mismatch.
Why this design
API client: getChecksum(name, version) returns string | null
The nullable return keeps backward compatibility with extensions published before Part 1 added checksum support. A 404 or a null checksum in the response both gracefully degrade to an "unverified" warning rather than failing the pull. This matches the existing getLatestVersion() and getExtension() 404→null pattern in the same client class.
Verification placement: after download, before extraction
The integrity check runs after the archive bytes are fully downloaded and before any extraction happens. This ensures a tampered archive is never written to disk. It also means verifyChecksum() throws a UserError that the existing command error flow already handles — no new error rendering needed.
Reuse of existing utilities
computeChecksum (domain/models) and verifyChecksum (domain/update/integrity) already exist and are tested. No new crypto code was introduced.
Output: verified vs unverified
Two states, not three — mismatch is a hard failure (thrown UserError), so it never reaches the render function. "verified" logs at info level, "unverified" logs at warn level to flag legacy extensions that lack checksums.
Verified output
$ swamp extension pull @stack72/system-extensions
00:23:44.618 info extension·pull Pulling "@stack72/system-extensions"@"2026.02.26.2"
00:23:44.623 info extension·pull Description: "Few system extensions for system and disk usage"
00:23:47.026 info extension·pull Identity verified: "@stack72/system-extensions"@"2026.02.26.2"
00:23:47.050 info extension·pull Pulled "@stack72/system-extensions"@"2026.02.26.2"
00:23:47.050 info extension·pull Extracted 4 files:
00:23:47.051 info extension·pull "extensions/models/system_usage.ts"
00:23:47.051 info extension·pull "extensions/models/disk_usage.ts"
00:23:47.051 info extension·pull ".swamp/bundles/system_usage.js"
00:23:47.051 info extension·pull ".swamp/bundles/disk_usage.js"
Changes
| File | Change |
|---|---|
src/infrastructure/http/extension_api_client.ts |
Add getChecksum() method |
src/presentation/output/extension_pull_output.ts |
Add renderExtensionPullIntegrity() render function |
src/cli/commands/extension_pull.ts |
Wire integrity verification into pull flow |
Test plan
-
deno check— type checking passes -
deno lint— linting passes -
deno fmt— formatting passes -
deno run test— all 2246 tests pass - Manual:
swamp extension pull @stack72/system-extensionsshows "Identity verified"
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.003433.0-sha.349ae436/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.003433.0-sha.349ae436/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.003433.0-sha.349ae436/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260227.003433.0-sha.349ae436/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.231547.0-sha.8c92b192
What's Changed
- feat: add
swamp extension pushcommand for publishing extensions (#494)
Summary
Add the swamp extension push command, which packages and publishes extension models and workflows to the swamp registry.
- Extension push command — manifest-driven three-phase push workflow (initiate, upload to S3, confirm) with
--dry-runand-yflags - Manifest validation — Zod-based schema validation with clear error messages for missing fields, bad CalVer versions, reserved namespaces, and missing models/workflows
- Safety analyzer — scans all files before push; blocks
eval(),new Function(), symlinks, hidden files, oversized files; warns onDeno.Command(), long lines, base64 blobs - Import/dependency resolution — auto-discovers local TypeScript imports and workflow-to-model dependencies
- Bundling — compiles each model entry point to standalone JS via deno bundle
- Version management — pre-flight CalVer version check with interactive bump on duplicates
ExtensionApiClient— HTTP client for the registry API with HTML error page detection (returns clean message instead of raw HTML dump)- Skill documentation — publishing guide added to
swamp-extension-modelskill with full manifest schema, examples, push workflow, safety rules, and common errors
Files changed
src/cli/commands/extension.ts— Extension command groupsrc/cli/commands/extension_push.ts— Push command implementationsrc/cli/mod.ts— Register extension commandsrc/cli/unknown_command_handler.ts— Extension subcommand suggestionssrc/domain/extensions/extension_manifest.ts— Manifest schema and parsersrc/domain/extensions/extension_safety_analyzer.ts— Safety analysis rulessrc/domain/extensions/extension_import_resolver.ts— Local import resolversrc/domain/extensions/extension_dependency_resolver.ts— Workflow dependency resolversrc/domain/models/calver.ts— CalVerbump()supportsrc/infrastructure/http/extension_api_client.ts— Registry HTTP clientsrc/presentation/output/extension_push_output.ts— Push output rendering.claude/skills/swamp-extension-model/SKILL.md— Publishing section added.claude/skills/swamp-extension-model/references/publishing.md— Detailed publishing reference
Test plan
- All 2229 tests pass (
deno run test) -
deno checkpasses -
deno lintpasses -
deno fmt --checkpasses - Integration tests: CLI help, manifest validation errors, auth errors, safety hard errors
- Unit tests: manifest parsing, safety analyzer, import resolver, dependency resolver, CalVer, API client, push output rendering
- Manifest validation runs before auth check so CI tests pass without credentials
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.231547.0-sha.8c92b192/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.231547.0-sha.8c92b192/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.231547.0-sha.8c92b192/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.231547.0-sha.8c92b192/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.230901.0-sha.756f7276
What's Changed
- feat: add
swamp extension pullcommand for downloading extensions (#496)
Summary
Adds the swamp extension pull command, the counterpart to swamp extension push, allowing users to download published extensions from the swamp.club registry into their local swamp repository.
Usage
swamp extension pull @namespace/name # pulls latest version
swamp extension pull @namespace/name@version # pulls specific version
swamp extension pull @namespace/name --force # overwrites existing files
Key Design Decisions
-
Unauthenticated endpoints: All pull operations (search, metadata, version info, download) are unauthenticated — no
swamp auth loginrequired. This usesgetExtension()(unauthenticated metadata endpoint that includeslatestVersion) instead ofgetLatestVersion()(the/latestendpoint requires auth). -
Server URL resolution: Uses
SWAMP_CLUB_URLenv var or defaults tohttps://swamp.club— does not depend on stored auth credentials for the server URL. -
Models directory resolution: Uses
resolveModelsDir()which respectsSWAMP_MODELS_DIRenv >.swamp.yamlmodelsDir> defaultextensions/models— not hardcoded. -
Circular import fix: Extracted
resolveModelsDir()tosrc/cli/resolve_models_dir.tsto break the circular import chain (extension_pull → mod → extension → extension_pull). The originalmod.tsnow imports and re-exports from the new file. -
Safety analysis: Runs
analyzeExtensionSafety()on all downloaded.tsfiles before extracting to disk. Hard errors (e.g.,Deno.run,eval) abort the pull; warnings are displayed but don't block. -
Recursive dependency pulling: If a manifest declares
dependencies, each missing dependency is automatically pulled. Includes circular dependency guard (Set<string>) and max depth (10). -
Concurrency-safe tracking:
upstream_extensions.jsontracks pulled extensions with lockfile-based read-modify-write protection andatomicWriteTextFile()for crash-safe writes. -
Archive extraction mapping:
extension/models/*→{modelsDir}/extension/bundles/*→.swamp/bundles/(pre-compiled, immediately usable)extension/workflows/*→workflows/extension/files/*→{modelsDir}/under extension subdirectory
New Files (6)
| File | Purpose |
|---|---|
src/cli/commands/extension_pull.ts |
Command definition + orchestration (~380 lines) |
src/cli/commands/extension_pull_test.ts |
Unit tests for parseExtensionRef() |
src/cli/resolve_models_dir.ts |
Extracted to break circular import |
src/presentation/output/extension_pull_output.ts |
Log + JSON rendering for all pull states |
src/presentation/output/extension_pull_output_test.ts |
Render function tests |
integration/extension_pull_test.ts |
CLI integration tests (help, invalid name, repo required, no auth required) |
Modified Files (6)
| File | Change |
|---|---|
src/cli/commands/extension.ts |
Wire up pull subcommand |
src/cli/mod.ts |
Import + re-export resolveModelsDir from new location |
src/cli/unknown_command_handler.ts |
Add pull to extension subcommand suggestions |
src/infrastructure/http/extension_api_client.ts |
Add downloadArchive(), make apiKey optional on getDownloadUrl/getExtension |
src/infrastructure/http/extension_api_client_test.ts |
Add downloadArchive connection failure test |
integration/ddd_layer_rules_test.ts |
Bump presentation→infra violation ratchet 27→28 |
Test Plan
-
deno checkpasses -
deno lintpasses -
deno fmt --checkpasses - All 2246 tests pass (
deno run test) -
deno run compilesucceeds -
swamp extension pull --helpshows usage -
swamp extension --helpshows pull subcommand - Invalid name format gives clear error
- Pull without initialized repo gives clear error
- Pull does not require authentication (connection error, not auth error)
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.230901.0-sha.756f7276/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.230901.0-sha.756f7276/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.230901.0-sha.756f7276/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.230901.0-sha.756f7276/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.174314.0-sha.0a12c9ff
What's Changed
- fix: prevent deno.lock creation during extension bundling (#491)
Summary
Fixes #490.
PR #452 introduced deno bundle for extension model transpilation. The subprocess inherits the user's CWD and Deno's default lockfile behavior creates/updates a deno.lock in the user's project root — polluting their repo with an unexpected file.
Why --no-lock is the right fix
The --no-lock flag tells Deno to skip lockfile auto-discovery entirely. This is correct for swamp's bundling use case because:
-
Bundling is a build step, not a dependency install. Swamp bundles extensions at startup as an internal transpilation step. The user didn't ask Deno to manage their dependencies — swamp is doing it behind the scenes. Creating a
deno.lockin their project is a side effect they never opted into. -
npm resolution still works without a lockfile. Packages are fetched from the registry and fully inlined into the bundle. The lockfile only pins versions across runs — it doesn't enable resolution.
-
Version pinning is handled at the source level instead. Since there's no lockfile, users should pin explicit versions in their import specifiers (e.g.,
npm:lodash-es@4.17.21). The skill docs and examples are updated to reflect this guidance. This is actually more explicit and portable than relying on a lockfile that lives outside the extension source. -
Alternative approaches are worse:
- Setting
cwdon the subprocess to a temp dir would break relative imports in extensions - Cleaning up
deno.lockafter bundling is fragile and races with other processes - Using
--lock=<temp-path>still creates a lockfile (just elsewhere) for no benefit
- Setting
Changes
src/domain/models/bundle.ts— Add--no-locktodeno bundleargssrc/domain/models/bundle_test.ts— Add test: bundles npm imports successfully and verifies nodeno.lockis created in the source directory.claude/skills/swamp-extension-model/SKILL.md— Add version pinning rule to Key Rules section.claude/skills/swamp-extension-model/references/examples.md— Pin versions in import examples table and text analyzer example
Test plan
- New test passes:
deno run test src/domain/models/bundle_test.ts(4/4 pass) - Full test suite passes: 2167 passed, 0 failed
-
deno check— type checking passes -
deno lint— linting passes -
deno fmt— formatting passes -
deno run compile— binary compiles successfully
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.174314.0-sha.0a12c9ff/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.174314.0-sha.0a12c9ff/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.174314.0-sha.0a12c9ff/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.174314.0-sha.0a12c9ff/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.152033.0-sha.bd7ab0fd
What's Changed
Summary
Fixes the race condition in refreshSecretsIndex that caused intermittent ENOTEMPTY errors when multiple vault put processes ran concurrently against the same vault.
Root cause
refreshSecretsIndex used a destructive remove+recreate pattern: it removed the entire vaults/{name}/secrets/ directory with Deno.remove({ recursive: true }) and rebuilt it from scratch on every vault put. When two processes collided, one would write symlinks into the directory while the other was trying to remove it, triggering ENOTEMPTY (os error 66).
This was the only index method using this pattern — indexModel and indexWorkflow both use idempotent ensureDir + atomic createSymlink without ever removing their parent directory.
Fix
Replaced the remove+recreate with an incremental sync that matches the existing model/workflow pattern:
- Migration guard — only remove
logicalSecretsDiriflstatshows it's an old-style symlink (not a directory), preserving backward compatibility ensureDir()— idempotent directory creation, safe for concurrent calls- Build desired set — read actual
.encfiles to determine which keys should have symlinks - Create/update symlinks — via existing atomic
createSymlink()(temp file + rename) - Remove stale entries — iterate logical dir and remove any symlinks not in the desired set, with
NotFoundtolerance for concurrent removals
This is convergent: any number of concurrent executions produce the same final state without interfering with each other.
Tests added
4 new test cases for refreshSecretsIndex:
- Basic secrets indexing —
.encfiles get symlinks created - Incremental add — adding a new secret preserves existing symlinks
- Stale cleanup — removing a
.encfile and re-indexing removes the stale symlink - Migration — old-style symlink gets replaced with a directory of individual symlinks
Manual verification
Ran the exact reproduction scenario from the issue — 5 rounds of 5 concurrent vault put operations against a fresh repo using the compiled binary. All 25 operations completed successfully with all keys present, zero ENOTEMPTY errors.
Closes #487
Test plan
-
deno check— type checking passes -
deno lint— linting passes -
deno fmt— formatting passes -
deno run test— all 2166 tests pass -
deno run compile— binary compiles - Manual concurrent
vault putstress test (5 rounds x 5 concurrent processes) — no errors
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.152033.0-sha.bd7ab0fd/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.152033.0-sha.bd7ab0fd/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.152033.0-sha.bd7ab0fd/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.152033.0-sha.bd7ab0fd/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.144740.0-sha.27ef5933
What's Changed
- feat: add contract, property-based, and architectural fitness tests (#486)
Summary
Adds three new categories of tests (13 files, 77 tests) that catch classes of bugs our existing unit and integration tests cannot:
- Architectural fitness tests enforce DDD boundary rules automatically — no more accidental layering violations or circular dependencies between bounded contexts
- Contract tests verify behavioral invariants that cross-context consumers depend on — catches drift in shared interfaces before it causes runtime failures
- Property-based tests verify domain aggregate invariants hold across randomly generated inputs — catches edge cases that hand-picked examples miss
Why these tests matter
Our existing 2,085 tests are all unit or integration tests with hand-crafted inputs. This leaves three gaps:
-
Architectural erosion is invisible. A developer can add a
domain → infrastructureimport and nothing fails. Over time, the clean DDD layering degrades. The architectural fitness tests catch this immediately with ratchets — the current violation count is locked in, and any new violation fails CI. -
Cross-context contracts drift silently. When
ModelResolver.buildContext()changes what fields it exposes, expression consumers break at runtime, not at compile time. Contract tests lock down the behavioral interface consumers depend on. -
Aggregate invariants aren't stress-tested. We test
Definition.create()with 3-4 names, but never with randomly generated strings containing path traversal characters, unicode, or empty segments. Property tests run 100 random inputs per property, catching edge cases we'd never think to write by hand.
Architectural fitness tests (2 files)
| Rule | Enforcement |
|---|---|
| No new circular dependencies between bounded contexts | Ratchet at 4 known mutual deps (data↔models, definitions↔models, expressions↔models, expressions↔workflows) |
| Domain must not import infrastructure | Ratchet at 13 known violations |
| Presentation must not import infrastructure | Ratchet at 26 known violations |
| Domain must not import CLI or presentation | Hard rule (0 violations) |
| Infrastructure must not import CLI | Hard rule (0 violations) |
| Production code must not import test files | Hard rule (0 violations) |
The ratchet pattern means: fixing an existing violation makes the count go down (test still passes). Adding a new violation makes the count go up (test fails). No allow-lists to maintain.
Contract tests (5 files, 42 tests)
Each test verifies a behavioral invariant that consumers across bounded contexts depend on, with zero overlap with existing unit tests:
- EventBus (
event_bus_contract_test.ts): Batch preserves publication order across event types, handler errors don't break subsequent handlers, type-specific and wildcard handlers both fire, batch error propagation cleans up state, post-batch events deliver immediately, unsubscribe is idempotent, selective unsubscribe only removes target handler - Definition (
definition_contract_test.ts):getMethodArguments()returns isolated copies (mutation-safe), nonexistent method returns empty object,withUpgradedGlobalArgumentspreserves identity fields, complex JSON Schema inputs survive serialization round-trip,globalArgumentsare immutable after create,setMethodArgumentsfully replaces - Data (
data_contract_test.ts): GC schema rejects zero/negative/float/zero-duration values, ownership schema enforces enum + non-empty ref, optional workflow fields behavior,withNewVersionpreserves all inherited fields,toData()returns tag copies (not shared references),isOwnedByignores optional fields - Workflow (
workflow_contract_test.ts): Job ordering preserved through serialization round-trip,addJobappends in call order,createallows empty jobs butfromDatarejects them, dependency structure survives round-trip, multi-step order preservation,Job.createrejects empty steps, tag isolation throughfromData - ModelResolver (
model_resolver_contract_test.ts):resolveModelthrowsModelNotFoundErrorfor invalid name/UUID (not null), finds models by name and UUID,buildContextalways includesenvnamespace, indexes models by both name and UUID,ModelData.inputhas stable interface (id, name, version, tags, globalArguments), self reference has correct fields,updateDefinitionInContextmutates context correctly
Property-based tests (5 files, 28 tests)
Uses fast-check to generate random inputs and verify invariants hold universally:
- ModelType (
model_type_property_test.ts): Normalization is idempotent, always lowercase, no consecutive separators, no leading/trailing separators, equality by normalized form, empty input rejected - Definition (
definition_property_test.ts): Path traversal characters always rejected, version always positive, serialization round-trips, ID is always UUID, hash is deterministic, hash is content-sensitive - Data (
data_property_test.ts): Path traversal rejected, version positive, tags always include 'type', ownership check correctness, serialization round-trips,withNewVersionpreserves identity - DataMetadata (
data_metadata_property_test.ts): Zero durations become "workflow", leading zeros become "workflow", non-zero durations pass through, named lifetimes pass through - Workflow (
workflow_property_test.ts): Empty jobs rejected by schema, duplicate job names rejected, version positive, serialization round-trips,getJoblookup works
Interesting findings during implementation
The contract tests revealed two real architectural facts worth documenting:
Data.tagsandWorkflow.tagsreturn direct references, not defensive copies. Mutating the returned object mutates the entity. This differs fromDefinition, which uses private fields + copy getters. The contract tests now document this actual behavior.Workflow.toData()passes tags by reference too, sofromData()is the isolation boundary (it goes throughWorkflowSchema.parsewhich copies).
Files changed (14)
| File | Type | Tests |
|---|---|---|
deno.json |
Config | Added fast-check dependency |
deno.lock |
Lockfile | Updated |
integration/architecture_boundary_test.ts |
Arch fitness | 5 |
integration/ddd_layer_rules_test.ts |
Arch fitness | 2 |
src/domain/events/event_bus_contract_test.ts |
Contract | 7 |
src/domain/definitions/definition_contract_test.ts |
Contract | 6 |
src/domain/data/data_contract_test.ts |
Contract | 13 |
src/domain/workflows/workflow_contract_test.ts |
Contract | 7 |
src/domain/expressions/model_resolver_contract_test.ts |
Contract | 9 |
src/domain/models/model_type_property_test.ts |
Property | 7 |
src/domain/definitions/definition_property_test.ts |
Property | 6 |
src/domain/data/data_property_test.ts |
Property | 6 |
src/domain/data/data_metadata_property_test.ts |
Property | 4 |
src/domain/workflows/workflow_property_test.ts |
Property | 5 |
Test plan
-
deno check— passes -
deno lint— passes -
deno fmt— passes -
deno run test— 2,162 tests pass (77 new), 0 failures -
deno run compile— binary compiles
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.144740.0-sha.27ef5933/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.144740.0-sha.27ef5933/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.144740.0-sha.27ef5933/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.144740.0-sha.27ef5933/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.140557.0-sha.017139c4
What's Changed
- feat: add dependency auditing CI gate with OSV-Scanner (#484)
Summary
Adds an automated dependency auditing job to the CI pipeline that catches
known vulnerabilities before they reach main. This is especially important
for a codebase that is entirely authored by AI agents, where no human is
manually reviewing changelogs or security advisories for each dependency
update.
Why this matters for an AI-authored codebase
AI agents are excellent at writing code but operate without awareness of the
broader security landscape of their dependencies. When an agent adds or
updates a dependency, it has no way to know whether that version has a
published CVE, has been deprecated due to a security incident, or pulls in
a transitive dependency with known vulnerabilities. This creates a blind spot
that compounds over time:
- Agents pick versions based on documentation and training data, not
real-time vulnerability databases - Automated dependency updates (from agents or Dependabot) can introduce
vulnerable transitive dependencies without anyone noticing - No human in the loop means no one is reading npm advisories, GitHub
security alerts, or package changelogs - Supply chain attacks targeting popular npm packages are increasingly
common — a compromised transitive dependency could be pulled in without
any agent or reviewer noticing
This CI gate acts as an automated security reviewer that fills the gap between
AI-generated code and the real-world vulnerability landscape.
Why a custom Deno script instead of existing tools
None of the established vulnerability scanners support deno.lock:
| Tool | Deno Support |
|---|---|
deno audit |
Does not exist (no native command) |
osv-scanner (Google) |
Does not recognize deno.lock format |
trivy (Aqua Security) |
Does not support deno.lock |
actions/dependency-review-action |
Requires GitHub dependency graph, which doesn't index deno.lock |
npm audit |
Only works with package-lock.json, requires npm setup |
So we wrote scripts/audit_deps.ts — a lightweight Deno script that:
- Reads
deno.lockand extracts all 240 npm packages with their resolved versions - Batch-queries the OSV.dev API (the same vulnerability database that osv-scanner, Deps.dev, and GitHub Advisory Database use)
- Reports any findings with advisory IDs, CVE references, and descriptions
- Exits non-zero if any vulnerabilities are found
This approach needs no external tools — it runs with deno run using only
--allow-read and --allow-net=api.osv.dev (minimal permissions).
What's included
New deps-audit CI job (runs in parallel with test for zero added
latency on the happy path):
-
Vulnerability scanning via the OSV.dev API — scans all npm packages in
deno.lockagainst the OSV database. This is a hard gate — any known
vulnerability fails the pipeline. -
Outdated dependency reporting via
deno outdated— reports which
dependencies have newer versions available as a GitHub Actions warning
annotation. Intentionally non-blocking since upstream releases would
otherwise break CI on unrelated PRs.
Pipeline integration:
- Runs in parallel with the
testjob (no sequential dependency) - Both
claude-reviewandauto-mergenow requiredeps-auditto pass - No PR can reach main with a known vulnerability in its dependency tree
Local development:
deno run auditruns the same vulnerability scan +deno outdatedlocally
Already finding real issues
Running the script against our current deno.lock immediately found:
Found vulnerabilities in 1 package(s):
jws@3.2.2
- GHSA-869p-cjfg-cm3x: No description available
This is a transitive dependency: @azure/identity → @azure/msal-node →
jsonwebtoken → jws@3.2.2. An upstream fix is needed from the Azure SDK.
Test Plan
-
deno fmt --checkpasses -
deno lintpasses -
deno run testpasses (2085 tests) - Script runs locally and correctly identifies known vulnerabilities
- CI runs the new
deps-auditjob successfully on this PR
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.140557.0-sha.017139c4/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.140557.0-sha.017139c4/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.140557.0-sha.017139c4/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.140557.0-sha.017139c4/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.020959.0-sha.ee058080
What's Changed
- fix: redact vault secrets from stdout/stderr output (#482)
Summary
Fixes #478 — when a model method references a vault secret via vault.get(...) and the command runs, the resolved secret value appeared in plaintext in:
- Console output — real-time streaming via
logger.info(line) - Data artifacts — stdout/stderr/command in result.json persisted to
.swamp/data/ - Log file artifacts — output log written to
.swamp/data/.../log/ - Run log files —
.swamp/outputs/*.logfiles - Error messages — catch block in shell_model.ts
The SecretRedactor existed and was populated during vault resolution, but was only wired to the RunFileSink (the .swamp/outputs/*.log files). It never reached the shell model or the process executor.
The fix
Thread the SecretRedactor through MethodContext so models can apply redaction to all output before persistence and streaming.
Changes:
- Add
redactor?: SecretRedactortoMethodContextinterface - Add
redactor?: SecretRedactortoProcessExecutorOptions— redacts lines before passing to logger in streaming mode - In
shell_model.ts— passcontext.redactortoexecuteProcess(), redactstdout,stderr, andcommandfields before storing in result attributes, redact error messages in catch block - In
model_method_run.ts— pass theredactorin the context object toexecutionService.executeWorkflow() - In
execution_service.ts— passctx.secretRedactorasredactorin the context for workflow step execution
What users see
Before this fix, running a model with a vault secret:
$ swamp model method run secret-test execute
... Executing method "execute"
... my-super-secret-password-12345 ← secret in plaintext
After this fix:
$ swamp model method run secret-test execute
... Executing method "execute"
... *** ← redacted
The redaction applies everywhere the secret could appear:
| Output channel | Before | After |
|---|---|---|
| Console streaming | my-super-secret-password-12345 |
*** |
result.json stdout |
my-super-secret-password-12345 |
*** |
result.json command |
echo 'my-super-secret-password-12345' |
echo '***' |
| Log data artifact | my-super-secret-password-12345 |
*** |
| Run log file | my-super-secret-password-12345 |
*** |
JSON mode (--json) |
my-super-secret-password-12345 |
*** |
| Error messages | my-super-secret-password-12345 |
*** |
User impact
- No breaking changes for normal usage. Commands that don't use vault secrets behave identically.
- The
commandfield in result data now shows the redacted command (e.g.,curl -H 'Authorization: Bearer ***' https://api.example.com) instead of the resolved secret. Users who need to see which vault/key was referenced can check the definition YAML, which preserves the originalvault.get()expression. - In practice, the
***replacement in the command field has minimal impact — the command structure remains visible and the secret is hidden, which is the desired behavior for audit logs.
End-to-end CLI verification
Tested with the compiled binary in a fresh swamp repo:
- Created a
local_encryptionvault with secretmy-super-secret-password-12345 - Created a
command/shellmodel withrun: "echo '${{ vault.get(test-vault, my-api-key) }}'" - Ran
swamp model method run secret-test execute - Verified console output shows
*** - Checked all persisted artifacts:
- result.json:
{"command":"echo '***'","stdout":"***","stderr":"", ...} - log artifact:
[stdout]\n*** - run log:
... ***
- result.json:
- Ran
grep -r "my-super-secret-password-12345" .swamp/— zero matches, no plaintext secret in any persisted artifact - Verified
--jsonmode also shows redacted values
Files changed (9)
Domain model:
src/domain/models/model.ts— addredactortoMethodContextsrc/domain/models/command/shell/shell_model.ts— apply redaction to stdout, stderr, command, and error messages
Infrastructure:
src/infrastructure/process/process_executor.ts— addredactorto options, redact streamed lines before logger
CLI / Workflow threading:
src/cli/commands/model_method_run.ts— passredactorin contextsrc/domain/workflows/execution_service.ts— passsecretRedactorasredactorin workflow step context
Tests (6 new):
src/domain/models/command/shell/shell_model_test.ts— 5 new tests (stdout, stderr, command, log file, error message redaction)src/infrastructure/process/process_executor_test.ts— 1 new test (streamed line redaction)
Design docs:
design/vaults.md— updated Expression Security sectiondesign/models.md— mentionredactorin MethodContext description
Test plan
-
deno check— passes -
deno lint— passes -
deno fmt— passes -
deno run test— 2085 tests pass -
deno run compile— binary compiles - End-to-end CLI test with compiled binary confirms no secret leakage
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.020959.0-sha.ee058080/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.020959.0-sha.ee058080/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.020959.0-sha.ee058080/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.020959.0-sha.ee058080/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/swamp 20260226.014830.0-sha.a97f0ed8
What's Changed
- fix: prevent symlink path traversal in file writes (#481)
Summary
Fixes #479 — symlink path traversal allowing writes outside the repository.
When .swamp/ subdirectories (e.g. outputs, data, secrets) are replaced with symlinks pointing outside the repository, swamp follows the symlink and writes sensitive data (resolved secrets, computation results) to the attacker-controlled location.
The vulnerability
The existing assertPathContained() uses resolve() which only normalizes paths lexically — it does not follow symlinks. A symlinked directory passes the check even when it points outside .swamp/.
The fix
A new shared assertSafePath() utility uses Deno.realPath() to resolve symlinks before verifying path containment. It's integrated at every write location across the codebase (17 files, 34 call sites).
Attack scenario: before vs after
Attack setup: .swamp/outputs is replaced with a symlink → /tmp/evil
Before the fix — no symlink-aware check exists:
write path: /repo/.swamp/outputs/aws-ec2/create/file.yaml
actual write: /tmp/evil/aws-ec2/create/file.yaml ← data exfiltrated
Naive fix (wrong boundary) — using the subdirectory as boundary:
boundary = realPath("/repo/.swamp/outputs") = /tmp/evil ← follows symlink!
path = realPath("/repo/.swamp/outputs/file.yaml") = /tmp/evil/file.yaml
check: "/tmp/evil/file.yaml".startsWith("/tmp/evil/") → true → PASSES ✗
Both sides resolve through the same symlink, making the check a no-op.
Correct fix (parent boundary) — using .swamp/ as boundary:
boundary = realPath("/repo/.swamp") = /repo/.swamp ← real directory
path = realPath("/repo/.swamp/outputs/file.yaml") = /tmp/evil/file.yaml
check: "/tmp/evil/file.yaml".startsWith("/repo/.swamp/") → false → PathTraversalError ✓
User impact
- No breaking changes for normal usage. All paths that stay within the repository work exactly as before.
- If a symlink-based escape is detected, a clear
PathTraversalErroris thrown with the path, boundary, and resolved target in the message. - The existing lexical
assertPathContained()checks inUnifiedDataRepositoryandYamlVaultConfigRepositoryare kept as defense-in-depth.
Plan vs implementation deviations
The original plan specified subdirectory-level boundaries (e.g. .swamp/outputs, .swamp/data, .swamp/secrets). During implementation review, this was identified as incorrect — using the potentially-symlinked directory as its own boundary makes the check ineffective. All boundaries were raised to the .swamp/ directory (or repo root for the index service).
| Component | Plan boundary | Actual boundary | Why |
|---|---|---|---|
| All persistence repos (7 files) | swampPath(repoDir, SWAMP_SUBDIRS.xxx) |
swampPath(repoDir) |
Subdirectory could be the symlink |
UnifiedDataRepository |
swampPath(repoDir, SWAMP_SUBDIRS.data) |
swampPath(repoDir) |
Same |
YamlOutputRepository |
swampPath(repoDir, SWAMP_SUBDIRS.outputs) |
swampPath(repoDir) |
Same |
LocalEncryptionVaultProvider |
swampPath(baseDir, SWAMP_SUBDIRS.secrets) |
swampPath(baseDir) |
Same |
UserModelLoader |
swampPath(repoDir, SWAMP_SUBDIRS.bundles) |
join(repoDir, SWAMP_DATA_DIR) |
Same |
RunFileSink callers |
Subdirectory boundaries | swampPath(repoDir) |
Same |
SymlinkRepoIndexService |
modelsBaseDir / workflowsBaseDir / vaultsBaseDir |
this.repoDir |
Parent dirs could be symlinks |
Additional deviation: the plan didn't mention the execution_service.ts call site for RunFileSink.register(), which was also protected.
Files changed (17)
New:
src/infrastructure/persistence/safe_path.ts—PathTraversalError+assertSafePath()src/infrastructure/persistence/safe_path_test.ts— 9 test cases
Modified (15):
src/infrastructure/persistence/unified_data_repository.ts— 5 checks (save, append, allocate, symlink)src/infrastructure/persistence/yaml_output_repository.tssrc/infrastructure/persistence/yaml_definition_repository.tssrc/infrastructure/persistence/yaml_evaluated_definition_repository.tssrc/infrastructure/persistence/yaml_workflow_repository.tssrc/infrastructure/persistence/yaml_evaluated_workflow_repository.tssrc/infrastructure/persistence/yaml_workflow_run_repository.tssrc/infrastructure/persistence/yaml_vault_config_repository.tssrc/infrastructure/persistence/json_telemetry_repository.tssrc/infrastructure/logging/run_file_sink.ts— optionalboundaryparamsrc/cli/commands/model_method_run.ts— passes boundary to sinksrc/domain/workflows/execution_service.ts— passes boundary to sinksrc/domain/vaults/local_encryption_vault_provider.ts— checks in put/ensureDirsrc/domain/models/user_model_loader.ts— checks in bundleWithCachesrc/infrastructure/repo/symlink_repo_index_service.ts— replaced private method with shared utility
Test Plan
-
deno fmt --check— passes -
deno lint— passes -
deno check— passes -
deno run test— 2079 tests pass (138 steps) -
deno run compile— binary compiles - New unit tests cover: path within boundary, symlink escape, non-existent paths, internal symlinks, boundary-as-symlink attack, error details
🤖 Generated with Claude Code
Installation
macOS (Apple Silicon):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.014830.0-sha.a97f0ed8/swamp-darwin-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/macOS (Intel):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.014830.0-sha.a97f0ed8/swamp-darwin-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (x86_64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.014830.0-sha.a97f0ed8/swamp-linux-x86_64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/Linux (aarch64):
curl -L https://github.com/systeminit/swamp/releases/download/v20260226.014830.0-sha.a97f0ed8/swamp-linux-aarch64 -o swamp
chmod +x swamp && sudo mv swamp /usr/local/bin/