Skip to content

fix(ui): confine agent/UI media paths to the workspace - #32

Merged
yultyyev merged 1 commit into
feat/timeline-engine-phase0from
fix/confine-ingest-paths
Jun 18, 2026
Merged

fix(ui): confine agent/UI media paths to the workspace#32
yultyyev merged 1 commit into
feat/timeline-engine-phase0from
fix/confine-ingest-paths

Conversation

@yultyyev

Copy link
Copy Markdown
Collaborator

What

Confines media file paths supplied by untrusted surfaces — the chat agent and the localhost clip ui HTTP routes — to the workspace, enforcing AGENTS.md non-negotiable #3 ("File paths from MCP/agent tools are resolved against a workspace directory. No reads/writes outside it without explicit user consent"). The trusted CLI stays unconfined: a user typing a path is consent.

Why (the hole)

resolveInput returns absolute paths as-is, and the agent/UI feed model- or HTTP-supplied paths straight into FFmpeg through:

  • every tool in the registryPOST /api/tools/:name (unauthenticated localhost) and the agent's tool calls, and
  • the add_media verbPOST /api/timeline/verbs.

So any of these could read an arbitrary file off disk. Concretely, POST /api/tools/render {"input":"/etc/passwd"} re-encodes the file into a workspace output the UI then serves back via GET /api/output/:opId — an arbitrary-file-read + exfiltration channel.

Fix

  • resolveInWorkspace(path) (new, in src/workspace.ts): relative paths resolve against the workspace; an absolute path must already sit inside it; ..-traversal and out-of-tree absolutes throw WorkspaceBoundaryError.
  • Every path-bearing registry tool is routed through it — not just ingest. tool-registry.ts now declares each tool's source-path field(s) (input / inputs[] / inputA,inputB / foreground,background / audio / overlay / path) and a single confining wrapper applies resolveInWorkspace at the dispatch boundary.
  • The add_media verb (makeVerbContext().ingest) is confined the same way.
  • Both server routes map WorkspaceBoundaryError → HTTP 403.
  • The CLI keeps resolveInput and never dispatches through the registry, so editing files anywhere on disk still works.

Adversarial review

A 2-lens review (containment-bypass + gating-completeness) → verify-each was run before pushing. It confirmed the prefix-containment check is correct (no startsWith-prefix bug; rejects traversal, out-of-tree absolutes, the workspace dir itself, cross-drive) and caught that an earlier draft gated only ingest while the other ~16 registry tools stayed open — hence the comprehensive confinement here. The agent-unfriendly error message (advising a CLI the agent can't run) was also flagged and reworded to be audience-neutral.

Residual (documented, not closed here)

Containment is by resolved-path prefix and does not follow symlinks — a symlink already present inside the workspace can still point out. Disclosed in the resolveInWorkspace JSDoc; hardening via realpath is a follow-up.

Tests

tests/workspace.test.ts: accepts in-workspace absolute/relative paths and a file literally named ..foo; rejects ..-traversal, out-of-tree absolutes (/etc/passwd), and the workspace dir itself; the CLI resolver stays unconfined. Registry coverage now asserts render/trim reject out-of-workspace input, multi-path tools confine every field (add_audio), and array inputs are confined element-wise (concat).

Gate green: pnpm type-check, pnpm test (533 pass), pnpm lint. CLI smoke confirmed clip timeline add-media <out-of-workspace file> still succeeds.

🤖 Generated with Claude Code

The chat agent and the localhost `clip ui` routes dispatch model/HTTP-
supplied file paths straight into FFmpeg — via every tool in the registry
(POST /api/tools/:name + the agent's tool calls) and via the add_media
verb (POST /api/timeline/verbs). resolveInput returns absolute paths as-is,
so any of these untrusted surfaces could read an arbitrary file off disk:
POST /api/tools/render {"input":"/etc/passwd"} re-encodes it into a
workspace file the UI then serves back — an exfiltration channel. Violates
AGENTS.md non-negotiable #3.

Add resolveInWorkspace (relative resolves against the workspace; an absolute
path must already sit inside it; ..-traversal and out-of-tree absolutes throw
WorkspaceBoundaryError) and route EVERY path-bearing registry tool plus the
add_media verb through it — not just ingest. Both server routes map the error
to HTTP 403. The trusted CLI keeps resolveInput (a user-typed path is consent)
and never dispatches through the registry, so editing files anywhere on disk
still works.

Containment is by resolved-path prefix and does not yet follow symlinks
(documented residual: an in-workspace symlink can still point out — hardening
via realpath is a follow-up). The boundary message is audience-neutral so it
reads sensibly both as agent tool-result data and in the 403 body.
@yultyyev
yultyyev merged commit f5a7c4e into feat/timeline-engine-phase0 Jun 18, 2026
1 check passed
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.2.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant