Redirection bypasses host control: > and >> write the daemon's disk without the files tier
- Dominant language
- TypeScript
- Stars
- 0
- Forks
- 0
- Avg merge
- 1h 35m
- Merged PRs (30d)
- 333
Description
`>` and `>>` write to the engine's host filesystem without consulting `engineFilesystem`, so a surface attached to a daemon writes files on the daemon's machine even when the operator who started it never granted the `files` tier.
## The declared boundary
`SurfaceCapabilities.engineFilesystem` exists to answer exactly this, and says so:
> The surface's operator sits at the same filesystem the engine runs on, so a path the engine resolves is a path they can open. True only for an in-process local shell. When false, a builtin that would write to disk must deliver through the surface instead, because the engine's disk is somebody else's machine.
`packages/calypso/src/daemon/launch.ts` sets it deliberately:
```ts
// The daemon's disk belongs to nobody attending this session — unless
// the operator who started it declared otherwise with the `files`
// tier of --host-control, in which case it is theirs by declaration.
engineFilesystem: policy.tiers.has('files'),
```
`upload` and `download` both honour it and refuse by name without it.
## What actually happens
`redirect_execute` in `packages/brasa/src/core/dispatch.ts` calls node's `writeFileSync` / `appendFileSync` on a path resolved by `redirectTarget_resolve` (`statSync`). Neither consults the capability. There is no `capability_require` on that path at all — where `pipe_execute`, one function away, does gate itself with `capability_require('pipeSegments', …)`.
Measured against a private daemon started as plain `chell --daemon`, with no `--host-control`, driven from a *remote* surface:
```
version >
→ 93 bytes written on the daemon's host, containing the version table
```
The daemon never declared the `files` tier. The write happened anyway.
## Why it matters
Host control is the whole mechanism by which an operator decides whether a daemon may touch its own machine, and it is refused by default. `!` is gated, pipe segments are gated, `upload` and `download` are gated. `>` is the hole in that fence, and it is the most ordinary shell gesture there is — an operator using it does not think of themselves as reaching for a host capability.
The exposure is bounded by who can attach: a token holder can already run commands. But that is precisely the boundary `--host-control` was built to draw *inside*, and a declared boundary that one common path ignores is worse than no boundary, because the daemon's banner says the tier is off.
## Also wrong: the help text
`packages/brasa/src/builtins/help.ts` describes redirection as:
```
Output Redirection Write/append to virtual files: ls -lh > output.txt
```
They are not virtual files. This is where I got the claim from before checking the code, and the operator corrected me; it will mislead anyone else the same way.
## What to do
Gate `redirect_execute` on `engineFilesystem`, refusing by name in the shape `upload` already uses, and fix the help text to say what redirection actually does. Whether a redirect should instead *deliver through the surface* when the capability is absent — the way `download` does — is a design question worth answering rather than assuming, and it bears directly on epic #400, where a cohort's `--csv` output has to land somewhere.
Found while grilling #400.
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with redirect_execute and redirectTarget_resolve in packages/brasa/src/core/dispatch.ts, then compare their capability handling with pipe_execute and the upload/download paths. Review packages/brasa/src/builtins/help.ts and determine the intended behavior when engineFilesystem is absent; done means redirection respects that boundary and the help text accurately describes it.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100