Internals
How the pieces actually fit together, and which rules are enforced where. Written for people who will extend this, not just run it.
The whole flow
checkCapabilities(caps, granted, root) every declared capability must be granted
↓
skill.plan(input, ctx) → Action[] trusted skill code constructs a plan
↓
checkAction(action, root, ids, caps) per action: path escapes, ui scope, undoability
↓
preview ─ print and stop
apply ─ for each action: pre-assertions → execute → post-assertions → journal
undo ─ read journal → replay undos in reverse → mark rolled-back
Everything above is synchronous filesystem work plus driver calls. There is no scheduler, no retry loop, and no model.
Planning is intended to be read-only, but skills currently run in the host process with filesystem access and the driver context; that convention is not a sandbox. Preview and apply each call plan() independently. Fixed plans and a trusted approval boundary are proposed in RFC 0001.
What the policy layer checks
| Check | Where | What it prevents | | --- | --- | --- | | Capability granted | checkCapabilities | running with permissions the user never gave | | fs.* scope inside root | checkCapabilities | a skill claiming C:\ as its scope | | Action covered by a declared capability | checkAction | declaring ui.read and then clicking | | Path inside root (lexical) | checkAction → isInside | .. traversal, absolute paths | | Path inside root (real) | checkAction → isInsideReal | a junction or symlink inside the root pointing outside | | Window inside declared scope | checkAction | declaring "Notepad" and driving "Calculator" | | Action undoable | checkAction | fs.delete with undo: null |
The real-path check resolves the nearest existing ancestor, so a target that does not exist yet is still checked through its parent. For fs.move sources it only checks the parent — moving a shortcut itself is legitimate.
Failure semantics
When an action fails, the kernel:
- records the failure in the journal (
status: "failed"+detail); - un-does the failing action's own effect, but only if execution actually started and the effect is verifiably present (
selfUndoSafe) — this avoids the classic "pre-assertion failed, then undo moved someone else's file away" bug; - rolls back every completed step in reverse order;
- marks exactly the steps that were successfully undone as
rolled-back.
If an undo itself fails (for example the file changed), that step stays un-rolled-back in the journal. The journal never claims a rollback that did not happen.
The journal is the source of truth
<root>/.deskshell/journal.jsonl, one JSON object per line:
{"ts":1793281142815,"taskId":"task-1793281142815","skill":"photo-sort","actionId":"move:IMG_0001.jpg",
"kind":"fs.move","summary":"move IMG_0001.jpg → …/2026-03/IMG_0001.jpg","status":"ok",
"undo":{"kind":"fs.move","from":"…/2026-03/IMG_0001.jpg","to":"…/IMG_0001.jpg"}}
taskIdgroups onerun()invocation;skillmakesdeskshell historyreadable;hashappears onfs.writesteps: the sha256 of what was written. Undo refuses to overwrite a file whose current hash differs, so a human edit after the fact is never silently destroyed.statusisok,failed, orrolled-back.
deskshell history summarises tasks; deskshell show <taskId> prints the timeline of one.
Parallel runs
deskshell run a b c --parallel executes the skills concurrently in one process. Each skill keeps its own plan and context; they share the journal. Journal writes are synchronous, so appends cannot interleave in a way that loses lines, and markRolledBack does its read-modify-write in a single tick.
One skill failing exits non-zero for the CLI but does not stop the others — verified by a test that runs a deliberately failing skill next to photo-sort.
Where skills live
packages/skills/— built in, ships with the package;~/.deskshell/skills/(or$DESKSHELL_SKILLS_DIR) — installed viadeskshell add.
The user directory wins on name collisions, and it survives upgrades because it is outside the package.
Extension points
- A new skill — one directory,
skill.json+index.ts,plan()only. - A new driver — implement
UiDriver, pass the 13 contract tests; the kernel stays unchanged. - A model — sits above
plan(). It may produce actions through the same policy checks; nothing in the kernel imports a model.
What remains to be built
The current implementation has no durable task queue, resident task host, transactional task database, or plugin sandbox. Its JSONL journal records completed/failed actions, but cannot resolve a process crash between a system effect and the subsequent journal write. The roadmap proposes these capabilities in stages while preserving the deterministic execution kernel.