Scope the path-jail claim to the provider that enforces it, and test the /work contract everywhere - #127
Merged
Conversation
…hind Two failures that both ended in an error message about something other than the thing that went wrong. The local provider test's afterEach removed HUSK_HOME with no retries and lost a race with writes still landing under computers/, failing the macOS job with `ENOTEMPTY: directory not empty, rmdir` -- after every assertion in the file had already passed. #80 fixed this same family in the CLI smoke test; this mirrors it, including the part that matters most: teardown can warn, but it can no longer fail a run it has nothing to say about. `husk exec dev -- 'uname -sr && python3 -V'` is the documented form and a posix one. cmd.exe does not strip single quotes, so husk received `'uname` as an executable name and the failure surfaced as a raw OCI error naming neither husk nor the quote that caused it. An argument that opens a single quote and never closes it is not something a real argv produces, so exec now catches it before resolving a provider and prints the double-quoted form of what the caller meant. A token quoted on both sides is left alone; that one is ambiguous and could be deliberate. The docs taught only the form that breaks there, so the Windows-facing double-quoted form is now in both READMEs and in `husk help exec`. The README's test count was already stale and this adds a file; it now reports what a full local run measures.
…the /work contract everywhere Issue #120 says the path jail does not hold on docker. It does not. `read_file /etc/passwd` returns the container's copy, `list_dir /` returns twenty entries, and `/dev/shm` takes a write; what stops the other writes is the read-only root filesystem, not a jail. The design is right -- the container is the boundary, and a jail inside a kernel boundary is largely redundant -- so the sentence was the thing that was wrong. The issue attributes it to SECURITY-MODEL.md, which 0f664b6 had already corrected. The copies still saying it were the README, the trust-boundary diagram in all four of its forms -- source JSON, generated HTML and both exported PNGs -- and the diagram's own `path jail / realpath, rejects escapes` node, which sits in the main flow where every provider passes through it. All of them now say `local file tools only`. The other six controls in that sentence were not taken on trust either. Each was probed against a live local computer and a live docker computer: the deny list refuses `rm -rf /`, `mkfs` and a fork bomb on both; the scrub empties a canary and `AWS_SECRET_ACCESS_KEY` on both; 5 MB of output comes back `truncated` on both; a 2s timeout kills a process tree in about 2s on both; `redact()` hides an `sk-ant` key on both; `audited()` wraps one dispatch point. Those six stay. Two smaller claims in SECURITY-MODEL.md were scoped rather than deleted. `redact()` matches AWS access key IDs and cannot match a secret access key, which is forty characters of base64 with nothing to anchor on. And "never writes credentials to ~/.husk" is true of the environment and false of `husk import`, which caches your transcript verbatim -- everything derived from it is redacted first, but the cached copy of your own file is not rewritten. The local path-jail row described a realpath check that rejects links leaving the workspace. On a Windows host it rejects any symlink inside the workspace, because the file tools read through \wsl.localhost\ and cannot follow a Linux link (#113). A relative link and a guest-absolute link to a file that never leaves /work both fail; the same links work on docker. Said, rather than implied. Beyond the claims: - `packages/sdk/src/conformance.test.ts` is the acceptance test the QA roadmap asked for and the repository did not have. Four writers x four readers -- the runtime Computer, the MCP tools, the HTTP control plane and the shell -- over one computer, so a surface that keeps its own path space fails against the other three rather than passing its own round trip. Plus exec-by-/work-path through three surfaces, listing, the `/workshop` lookalike, the relative default workdir, and the CLI binary as a child process. It runs on local and, when a daemon answers, docker. Mutation-tested: force the WSL /work mount off and no-op `rewriteGuestPaths` and seven of them go red. - `browser_status` promised a 111 MB Chromium download on providers that cannot perform it -- the container root filesystem is read-only and the base image has neither curl nor python3, so `browser_goto` fails before a byte moves. It now says so, and names the three ways out (#122). - The README quickstart did not work as written. `up dev` picks docker when a daemon is running, `base` is debian:bookworm-slim, and the third line exited 127 with `python3: not found`. The prose only promised that output "with Docker stopped". Pinned to `--flavor python` and rewritten to say what each provider prints. - CHANGELOG records the /work fix, which was the largest user-facing correction since 0.1.3 and was in no changelog at all. - `server.json` declares `io.github.hotragn/husk` for the official MCP registry, with 512x512 light and dark icons rasterised from `brand/logo/mark.svg`. It validates against the 2025-12-11 schema. Submission waits for 0.1.4 to be on npm, since the package version it pins has to resolve. - The test-count line was measured rather than adjusted: 1,730 across 99 files, and it no longer claims they all pass with no Docker, because twenty-two of them are the container half of the conformance matrix and skip without a daemon.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The conformance suite asserted the `/work` contract unconditionally and went red on both Windows runners: no WSL there, so the local provider falls back to cmd.exe, where there is no `/work`, no `sh` and no heredoc. Twelve failures, all of them the shell-side pairs, and every one of them the documented behaviour rather than a defect -- the fallback reports its own raw workspace as `workdir` precisely so it is not claiming a path it cannot honour. `local.test.ts` guards the same contract with `skipIf(platform === 'win32')`, which would work and would also be wrong here: this repository is developed on a Windows host *with* WSL2, where the contract does hold and the suite has to run. Platform is a proxy for the question. The question is whether this computer got `/work`, and the provider already answers it in `info.workdir`. So the gate creates one throwaway local computer and reads that field. Creation is lazy -- nothing runs inside until an exec -- so the probe costs a directory. On a cmd.exe host the local matrix and the CLI case skip together and the docker half still runs; simulated here, 22 passed and 23 skipped with no failures.
The failure said "never opened its debugging port within 40 seconds". Forty is what 80 attempts x 0.5s comes to while every connect is refused instantly, which is the common case and not the ceiling: a listener that accepts and then wedges makes each curl pay its full --max-time, and the wait runs to the 90-second exec kill that is the real backstop. Someone who waited ninety seconds and was told forty has been given a number that does not match what they just watched.
The conformance suite gated its docker half on `docker version` exiting zero.
GitHub's windows-latest runner answers that yes -- and then cannot pull
debian:bookworm-slim, because the daemon it is running serves Windows
containers. The suite booted a provider that looked available and died on
`E_COMPUTER_FAILED could not obtain an image`.
Two questions were being asked as one: is a daemon up, and can it run the images
this suite needs. `{{.Server.Os}}` answers the second, and returning anything
other than `linux` now skips the docker half the same way an absent daemon does.
…talls them Two gaps that only show up from outside the repository. `husk doctor` answers "what can this machine do" and answers it well. It is not an onboarding: it hands you eleven model providers that are not configured and leaves you to pick. Between `npm i -g @husk-ai/cli` and a working first command sat read doctor, choose a provider, go and find a key, export it, come back, work out which of twenty commands to run. All of it documented, none of it in front of you. A bare `husk` printed the twenty-command wall. `husk onboard` walks the same ground and then does the next thing each time. It names the provider and how isolated it actually is, in the words the security docs use rather than softer ones -- this is the screen where someone forms that belief. It creates a computer, runs a command in it and destroys it, so you have seen the thing work before you configure anything. It names the cheapest route to a model when there is none, free and local first. It prints the MCP line for the editor you already use. Then one command to run next, chosen from what this machine can actually do. It never asks for an API key. Husk reads credentials from the environment and does not store them, and an onboarding that offered to hold one would make that sentence false on the very first run -- so it prints the export line and re-probes. There is a test for the absence of that prompt, because it is a contract and not a preference. With no terminal it creates nothing and prints the path as text: a CLI that blocks for input inside a Dockerfile is a CLI people stop installing. The one-time orientation was wired to `husk up` alone, so anyone whose first command was `husk mcp` -- a model about to be handed a shell -- was told nothing about the boundary at all. `run`, `serve` and `mcp` orient too now, on stderr, so the protocol channel stays clean. The second gap is that every existing check tests the workspace. `npm test` runs against source with symlinks between packages; `preflight` packs with `--dry-run` and runs the bins from inside the monorepo, where hoisting satisfies imports the published package never declared. Both are green on a tree that cannot be installed, which is how 0.1.1 shipped a bin that exited 0 and did nothing, and how 0.1.3 shipped three pins a version behind in the dependency group nobody reads. `npm run install-smoke` packs all eleven, installs them into a scratch directory outside the checkout, and drives the installed copies: CLI lifecycle, MCP handshake over stdio, SDK import. It also reads all twenty-eight cross-package pins out of the tarballs rather than the checkout. That last check is static, and the first version of it was not. Pointing `overrides` at the local files is what stops npm quietly satisfying a pin from the registry and testing code that is not in this checkout -- and it also masks a stale pin completely. Injecting `@husk-ai/sessions: 0.1.2` into the server manifest passed every check. Reading the pins out of the artifact catches it, and it has to be static because before a publish the correct version is not on the registry: an install that resolved transitively would 404 on a correct pin and sail through a stale one that happens to be published. `--registry` is the post-publish mode that resolves for real.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the gap to a publishable
0.1.4. Every HUSK finding from the QA roadmap was re-run rather than read, on a host with Docker, WSL2 and a real tool-calling model — an environment the first pass did not have.This branch includes #126 (rebased onto
main); merge that first or merge this and close it.P0-1 — the isolation claim
Issue #120 is correct and reproduced. On
docker:The last line is the read-only rootfs, not a jail. The design is fine; the sentence was not.
SECURITY-MODEL.mdhad already been corrected by0f664b6. The copies that still said it wereREADME.md:196, the trust-boundary diagram in all four of its forms (source JSON, generated HTML, both exported PNGs), and the diagram'spath jailnode itself — which sits in the main flow every provider passes through. The PNGs were re-exported through the diagram's own exporter and re-fitted to the repo's 2492×1116 geometry.The other six controls in that sentence were each probed on a live
localand a livedockercomputer before being left in:rm -rf /,mkfs, fork bomb)AWS_SECRET_ACCESS_KEY)truncated: truetruncated: trueredact()sk-ant…[redacted]sk-ant…[redacted]audited()Two further claims in
SECURITY-MODEL.mdwere scoped:redact()matches AWS access key IDs and cannot match a secret access key (40 chars of base64, nothing to anchor on — every other advertised format was tested and does redact); and "never writes credentials to~/.husk" is true of the environment but not ofhusk import, which caches your transcript verbatim. The local path-jail row now also records #113 — on a Windows host it refuses any symlink inside the workspace, because the file tools read through\wsl.localhost\and cannot follow a Linux link.HUSK-001..008 — re-run
/workmismatchlocal+docker+ the CLI binary. Mutation-tested: break the mount and no-oprewriteGuestPathsand 7 go red.local, and a second live instance found and fixed (above).husk runonollama/qwen2.5:1.5b: 2 steps, 45.1s, 982 tokens, correct tool call, grounded answer.--remote-debugging-port. Nit: the message says "40 seconds" where the ceiling is 90.CHANGELOG.mdgone. One residual (launch/twitter.md, whose**Tweet 1 (hook)**headers parse as roles).name: use-key-redacted-for-the-bot, no canary anywhere in the emitted spec.EBUSY … rmdirinsmoke.test.ts— a Windows teardown race, same family as #80/#63. Green for 12 consecutive runs.The conformance suite
packages/sdk/src/conformance.test.tsis the acceptance test the roadmap asked for and the repo did not have. Four writers × four readers over one computer, so a surface that keeps its own path space fails against the other three instead of passing its own round trip. Nothing mocked: real providers, a real Fastify socket, the real MCP dispatcher, the real CLI as a child process.Also
browser_statuspromised a 111 MB download on providers that cannot perform it. It now names the read-only rootfs and the three ways out (The Chromium linkability check runs after the 111 MB download, and browser_status does not mention that container providers cannot run a browser #122).up devpicksdockerwhen a daemon runs,baseisdebian:bookworm-slim, and line 3 exited 127 withpython3: not found. Pinned to--flavor python; prose now says what each provider prints. Re-run clean against a freshnpxcache.CHANGELOG.mdrecords the/workfix and everything else user-facing since 0.1.3.server.jsonfor the official MCP registry, validating against the2025-12-11schema, with 512×512 light/dark icons rasterised frombrand/logo/mark.svg.Onboarding, and testing the artifact
Added after review, because neither is visible from inside the repo.
husk onboard— the guided setup there wasn't one of.doctoris a diagnostic; it reports eleven unconfigured model providers and leaves you to choose.onboardwalks the same ground and does the next thing each time: names the provider and how isolated it really is, creates a computer and destroys it in front of you, names the cheapest route to a model, prints the MCP line for your editor, then one command to run. Five steps, each skippable, safe to re-run,--jsonclean.It never asks for an API key — husk reads credentials from the environment and does not store them, and there is a test asserting the prompt's absence, because that is a contract this release already had to scope twice.
The one-time orientation was wired to
husk upalone, so anyone whose first command washusk mcp— a model about to be handed a shell — was told nothing about the boundary.run,serveandmcporient too now, on stderr, verified not to disturb the JSON-RPC channel.npm run install-smoke— everything else in the gate tests the workspace.npm testruns against source with symlinks;preflightpacks--dry-runand runs the bins inside the monorepo, where hoisting satisfies imports the published package never declared. Both are green on a tree that cannot be installed. That is how 0.1.1 shipped a bin that exited 0 and did nothing, and how 0.1.3 shipped three pins a version behind.It packs all eleven, installs into a scratch dir outside the checkout, and drives the installed copies — CLI lifecycle on docker, MCP handshake (21 tools), SDK import — plus all 28 cross-package pins read out of the tarballs.
That last check is static on purpose, and the first version of it was wrong.
overridespointing at the local tarballs is what stops npm satisfying a pin from the registry and testing code that isn't here — and it masks a stale pin completely. Injecting@husk-ai/sessions: 0.1.2passed every check. Reading the pins from the artifact catches it, and it cannot be a resolution check pre-publish: the correct version isn't on the registry yet, so a transitive install would 404 on a correct pin and sail through a stale published one.--registryis the post-publish mode.Gate
npm run build:packages && npm run typecheck && npm test— green, 1,739 passed / 19 platform-skipped across 100 files.npm run install-smoke— clean.npm run drift51 ok.npm run links45/45.node scripts/secret-scan.mjsclean.Every command in
README.mdandlaunch/executed as written against a cleannpm_config_cacheand a cleanHUSK_HOMEon published@husk-ai/cli@0.1.3.Not in this diff
launch/andHusk QA and refinement roadmap.mdare gitignored on purpose (.gitignore:108-112). The 27@husk/→@husk-ai/fixes and the roadmap re-run are on disk but deliberately not committed.No version bump here — that is its own commit, and it waits for a go-ahead on publishing.