Skip to content

Fix local workspace contract and complete QA roadmap hardening - #124

Merged
Hotragn merged 1 commit into
mainfrom
fix/local-work-contract-and-honest-claims
Sep 18, 2026
Merged

Hotragn merged 1 commit into
mainfrom
fix/local-work-contract-and-honest-claims

Conversation

@Hotragn

@Hotragn Hotragn commented Sep 18, 2026

Copy link
Copy Markdown
Owner

One commit on top of f8ffbbb. The headline is the first item; the rest are the
remaining QA findings, grouped because they are all small and none of them
touches a shared path.

The local /work contract

The posix shell had no /work. File tools translated guest paths and the shell
did not, so "write a script to /work, then run it" — the most basic agent
workflow there is — failed on the provider everyone gets by default. The WSL
branch bind-mounts a real /work per exec; a plain posix shell cannot do that
portably, because unprivileged user namespaces are not enabled everywhere and
macOS has none at all.

rewriteGuestPaths maps guest-absolute paths onto the workspace the shell
already runs in. It is lexical and quote-aware:

  • recognised only at a word boundary, so https://x/work and /workshop are
    left alone
  • literal inside single quotes, backslash-escaped inside double quotes,
    shell-quoted elsewhere
  • trailing slashes preserved, so /work/$f still concatenates and the expansion
    stays outside the replacement
  • a token that would escape the jail (/work/../../etc/passwd) is passed through
    untouched rather than helpfully repointed at the host file it names — /work
    does not exist in this mode, so it fails harmlessly

$PWD is also set to the workspace, so it agrees with what pwd prints.

What local claims to protect

"/work is jailed" was true of the file tools and false of the shell, and it was
in the MCP note the model reads first. A model that believes the shell is
confined takes risks it otherwise would not, which is the whole argument for
saying isolated: false out loud in the first place.

The note, SECURITY.md, docs/SECURITY-MODEL.md, README.md, the MCP package
README and the MCP docs page now say which half is confined: file tools are
confined to /work, shell commands reach whatever the user reaches. The
mount-namespace row also stops implying isolation between computers and says
what it actually gives — the /work an agent sees is always its own workspace.

The rest

  • Model context budgeting. A run that cannot fit is refused before the first
    call, with the numbers, instead of being truncated into nonsense by the server
    after a long local load. Local servers still default to tiny contexts, and
    Husk's own system prompt plus the computer tool schemas is ~2.5k tokens alone.
  • Bounded browser launch. A Chromium that never opens its debugging port is
    now reaped instead of left running, and the poll is bounded with the exec kill
    as the hard backstop. Every failed launch used to leak one browser, each
    holding its workspace open.
  • Transcript discovery. Markdown is content-sniffed. A repository's
    CHANGELOG is not a pasted chat, and offering it as one made husk import
    useless in the place people try it first.
  • Secret-derived metadata. Secrets are stripped before a title is slugged, so
    a credential in a transcript title cannot survive into the bot's name or its
    filename. Core keeps six characters of a matched secret so an operator can tell
    two keys apart in a log; everything leaving @husk-ai/sessions is an artifact,
    and there a kept prefix is only a smaller leak — sk-ant…[redacted] slugged
    into sk-antredacted.
  • Distiller consistency. It no longer reports "configured without tools"
    while the spec it just wrote lists them.
  • Windows smoke test. The exit-code test gets the timeout its subprocess
    needs.

Verification

Run on Windows 11 ARM64, Node 24.14.0, against f8ffbbb.

gate result
npm install clean
npm run build:packages exit 0
npm run typecheck exit 0
npm test 1,652 passed, 0 failed, 19 skipped
npm run drift 50 ok, 0 drifted
npm run links 45 of 45 resolve
scripts/secret-scan.mjs clean
git diff --check clean

No UI change: nothing under apps/web or apps/console, and no .tsx, .jsx
or .css in the diff. The single apps/ edit is prose inside a fenced code
block on the MCP docs page.

Two notes on the tests

The /work contract tests are posix-only, and the rewriteGuestPaths unit tests
are too. toHostPath resolves through node:path, so a posix JailMap becomes
a drive-qualified Windows path — /host/root/scrape.py arrives as
C:\host\root\scrape.py — and every expectation would have to be written twice
for no gain. The function is only reachable on the /bin/sh branch, which a
Windows host never takes: it gets WSL or cmd.exe. Both blocks are guarded with
describe.skipIf(process.platform === 'win32').

The isolation-honesty and Windows-messaging assertions are deliberately outside
that guard. They are literals and pure functions, they run on every platform, and
windowsHint/windowsReason are the only thing standing between a Windows user
and being told to install the WSL they already have. Skipping the posix contract
block must not take them with it.

@vercel

vercel Bot commented Sep 18, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
husk-dev Ready Ready Preview Sep 18, 2026 9:55pm UTC
husk-dev-docs Ready Ready Preview Sep 18, 2026 9:55pm UTC

@github-actions github-actions Bot added documentation Improvements or additions to documentation pkg: runtime Touches packages/runtime pkg: browser Touches packages/browser pkg: sessions Touches packages/sessions pkg: agent Touches packages/agent pkg: mcp Touches packages/mcp pkg: cli Touches packages/cli apps Touches apps/ (console, docs, web) labels Sep 18, 2026
…laims to protect

The posix shell had no /work. File tools translated guest paths and the shell did
not, so "write a script to /work, then run it" -- the most basic agent workflow --
failed on the provider everyone gets by default. `rewriteGuestPaths` maps
guest-absolute paths onto the workspace the shell already runs in: quote-aware,
lexical, preserving trailing slashes so `/work/$f` still concatenates, and leaving
lookalikes like `/workshop` and jail escapes alone rather than helpfully
repointing them at a host file.

Heredoc bodies are copied through untouched. `cat > /work/notes.md <<EOF` is how
an agent writes a file, and the thing it most often writes about is the
workspace, so rewriting inside the body changed the bytes that landed on disk.
Worse, the body is prose: an apostrophe in `don't` put the scanner in
single-quote state for the rest of the script, and the single-quote branch adds
no quoting, so a later host path containing a space came out bare and split into
two shell words. The terminator is tracked instead, the body never reaches the
path logic, and quote state resets after it. `<<-` and a quoted delimiter are
both understood; `<<<` is left alone.

The safety language followed from the first paragraph. "/work is jailed" was true
of the file tools and false of the shell, in the MCP note the model reads first
and in four documents. They now say which half is confined, because a model that
believes the shell is contained takes risks it otherwise would not.

Also:

- A run that cannot fit the model's context is refused before the first call,
  with the numbers, rather than truncated into nonsense by the server. Local
  servers still default to tiny contexts.
- A failed Chromium launch is bounded and the process reaped, so a launch that
  never opens its port stops leaking a browser per attempt.
- Markdown discovery is content-sniffed. A repository's CHANGELOG is not a pasted
  chat, and offering it as one made discovery useless where people try it first.
- Secrets are stripped from title-derived metadata before it becomes a name, a
  slug or a filename, where a kept prefix is only a smaller leak.
- The distiller no longer reports "configured without tools" while the spec it
  wrote lists them.
- The Windows exit-code smoke test gets the timeout its subprocess needs.

The /work contract tests are posix-only, and so are the `rewriteGuestPaths`
string expectations: `toHostPath` resolves through `node:path`, so a posix
JailMap becomes a drive-qualified Windows path and every expectation would have
to be written twice. That code is only reached on the `/bin/sh` branch, which a
Windows host never takes. The heredoc tests derive their expectations instead, so
they run everywhere, and the isolation-honesty and Windows-messaging assertions
stay outside the guard -- skipping the posix block must not take them with it.
@Hotragn
Hotragn force-pushed the fix/local-work-contract-and-honest-claims branch from 280934b to 0f664b6 Compare September 18, 2026 20:23
@Hotragn
Hotragn merged commit 25f6235 into main Sep 18, 2026
10 of 13 checks passed
@Hotragn
Hotragn deleted the fix/local-work-contract-and-honest-claims branch September 18, 2026 20:43

This branch was successfully deployed

2 active deployments
Preview – husk-dev 0f664b63 Deployed Sep 18, 2026 by vercel[bot]
Preview – husk-dev-docs 0f664b63 Deployed Sep 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

apps Touches apps/ (console, docs, web) documentation Improvements or additions to documentation pkg: agent Touches packages/agent pkg: browser Touches packages/browser pkg: cli Touches packages/cli pkg: mcp Touches packages/mcp pkg: runtime Touches packages/runtime pkg: sessions Touches packages/sessions

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant