Read and change a machine's files from the backend
The first half of EXPLORER.md: server/src/files.rs, which lists a directory, reads a file, writes one, and creates a file or a directory on whichever machine a setup names. Each operation is one small POSIX script run through `Transport`, the way the import listing and the usage fetch already ask a machine a question, so the local and the ssh case are one implementation rather than two that drift. The path crosses as a positional argument and never as script text; `PATH_PRELUDE` is the one line that gives a leading `~` its meaning, because a shell expands a tilde in text and not in an argument, and it is the far machine's home that has to answer. A read has four answers -- text, binary, tooBig, or the machine's own error -- because a binary file drawn as text and a big one cut off silently are both wrong in ways the reader cannot see. A write carries the sha256 the read reported and is refused with a 409 when the file has moved on, which is what happens whenever an agent is editing the file somebody is reading. `Transport::capture_with_input` is the one description of "run this there, with this on stdin", and `ship_attachment` moves onto it rather than assembling a second ssh invocation of its own. It is also the only capture that hands back the exit status, which is how the write says "this is not the file you read" without that answer looking like a failure. Exercised on both transports against the sandbox -- ssh to this VM with a throwaway key, since the quoting and the stdin path are what that proves -- including a filename with an apostrophe, one with a tab, an unreadable file, a binary one, one over the limit, and the 409. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f6bee1b8a5
commit
cc7e4f63ef
8 files changed
+1008
-46
No files matched your search
@@ -790,6 +790,11 @@ POST /sessions/:id/attachments multipart upload → id (referenced by /me
|
||||
GET /sessions/:id/files/:ref images the session produced or was sent
|
||||
DELETE /sessions/:id kill process, release llama-server, delete transcript+files
|
||||
GET /usage cached usage windows
|
||||
GET /setups/:id/dir?path=P entries of directory P, and P resolved
|
||||
GET /setups/:id/file?path=P content of file P, or why not
|
||||
PUT /setups/:id/file {path, content, ifSha256}; 409 if it moved on
|
||||
POST /setups/:id/file {path} create empty; refused if it exists
|
||||
POST /setups/:id/dir {path} create; refused if it exists
|
||||
GET/PUT /hosts, /models config editing from the phone
|
||||
```
|
||||
|
||||
@@ -798,6 +803,17 @@ directory under `$XDG_DATA_HOME/ai-app/sessions/` (transcript.jsonl,
|
||||
attachments, produced images), owner-only. Deleting a session is the
|
||||
complete path out of everything spawning one created.
|
||||
|
||||
### The file explorer (decided 2026-09-03)
|
||||
|
||||
**`EXPLORER.md` holds this design**, decision by decision with what was
|
||||
rejected, the same way this file does — it is long enough to be its own
|
||||
document and it is where a change to it belongs. The one-line version: a
|
||||
machine's filesystem, seen from the phone through the backend, keyed on
|
||||
the **setup** rather than on a session (a session only says where to
|
||||
start), with every operation one fixed shell script run through
|
||||
`Transport` so the local and the ssh case are one implementation. The
|
||||
security consequence is in the token paragraph below.
|
||||
|
||||
### Security
|
||||
|
||||
- TLS with a self-signed CA, pinned in the app — same
|
||||
@@ -853,6 +869,20 @@ complete path out of everything spawning one created.
|
||||
gates LAN-reachable RCE; it does not (and cannot) defend a compromised
|
||||
backend host or phone — those are inside the trust boundary, and a
|
||||
compromised phone is handled by rotation.
|
||||
- **The explorer's routes take a path, and that is deliberate**
|
||||
(2026-09-03; see EXPLORER.md's decision 3). Elsewhere the rule is that
|
||||
the phone picks an **id** and the server resolves which file it names —
|
||||
the import listing is written that way so an enrolled token cannot
|
||||
become "read me an arbitrary file". `/setups/{id}/dir` and
|
||||
`/setups/{id}/file` take the path, because the path is the whole
|
||||
feature. It grants nothing new: the same token already spawns a
|
||||
bypass-permissions agent in any directory on any machine a setup names,
|
||||
and that agent already reads and writes every file its user can, so
|
||||
this is a shorter path to authority the token holds either way. The
|
||||
import rule stands where it is, because there a path was unnecessary
|
||||
and refusing one cost nothing. What is unchanged is the harder line:
|
||||
**no route accepts a command.** Listing, reading and writing are fixed
|
||||
scripts in `files.rs`; the phone chooses only the path and the bytes.
|
||||
- **Generation**: 256 bits from the OS CSPRNG on first run, base64url. A
|
||||
machine credential, never typed twice, so unguessable costs nothing; at
|
||||
this entropy no key stretching is needed.
|
||||
|
||||
Reference in new issue
Block a user