Skip to content

Commit 0e0a52f

Browse files
authored
refactor(agent): unify local and SSH workspace execution (#2707)
* refactor(agent): share workspace file execution across local and SSH * fix(workspace): close review gaps in search, identity and SSH lifecycle * fix(session): keep inherited snapshot capabilities scoped to their source * chore(capabilities): regenerate remote workspace command policies * test(workspace): respect native filesystem semantics across platforms
1 parent c150f6c commit 0e0a52f

78 files changed

Lines changed: 12832 additions & 2413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -370,13 +370,33 @@ jobs:
370370
- name: Run file watch contract tests
371371
run: cargo test --locked -p bitfun-services-integrations --no-default-features --features file-watch --test file_watch_contracts
372372

373-
# Search tools resolve paths and symlinks directly, which also differs
374-
# across platforms. Scoped to the search module: the glob tests in this
375-
# crate fail on Windows independently of this branch (walk-root
376-
# derivation treats separators differently) and need their own fix.
373+
# Search tools share matching but retain provider-specific path and
374+
# symlink handling. Keep the search module in the full OS matrix.
377375
- name: Run search tool tests
378376
run: "cargo test --locked -p tool-runtime --lib search::"
379377

378+
# Workspace providers own filesystem/link semantics and SSH cleanup;
379+
# compiling them as Desktop dependencies does not execute their tests.
380+
- name: Run workspace provider contracts
381+
shell: bash
382+
run: |
383+
cargo test --locked -p bitfun-services-core --no-default-features --features workspace-runtime --lib workspace::tests::
384+
cargo test --locked -p bitfun-services-integrations --no-default-features --features remote-ssh-concrete --lib remote_ssh::manager::tests::workspace_
385+
cargo test --locked -p tool-runtime --no-default-features --lib fs::read_file::tests::
386+
cargo test --locked -p tool-runtime --no-default-features --test tool_io_contracts
387+
388+
# Linux's product-full suite above already covers these tools. The
389+
# feature-free Core suites on macOS/Windows do not exercise workspace IO.
390+
- name: Run workspace Agent contracts
391+
if: runner.os != 'Linux'
392+
run: >-
393+
cargo test --locked -p bitfun-core --no-default-features
394+
--features agent-runtime,git,remote-workspace,document-read --lib --
395+
file_read_tool::tests file_write_tool::tests file_edit_tool::tests
396+
delete_file_tool::tests grep_tool::tests glob_tool::tests ls_tool::tests
397+
tool_context_runtime::path_resolution_tests file_read_state_runtime::tests
398+
service::snapshot:: product_runtime::tests::session_fork_
399+
380400
# Call the standalone Linux/Relay workflow directly. Calling the Nightly
381401
# artifact orchestrator would register a misleading skipped Desktop package
382402
# check on every producer-selected PR even when that matrix is disabled.

docs/architecture/agent-runtime-deployment-design.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ Agent Runtime 的模块职责见 [`agent-runtime-services-design.md`](agent-runt
88
和 Shared transport 提案见 [`app-server-architecture.md`](app-server-architecture.md)。Embedded interactive TUI 已完成 direct-runtime
99
迁移;Shared transport 未通过独立评审前继续使用 v17。
1010

11+
SSH/容器工作区保留轻量 SSH 部署:同一 Agent Runtime 和会话状态仍由 BitFun 主机持有,
12+
只在工作区文件与进程 I/O 边界选择本地或 SSH provider,不要求目标安装 BitFun CLI 或 daemon。
13+
具体收敛范围和验收见
14+
[`remote-workspace-transport.md`](remote-workspace-transport.md#agent-runtime-convergence)
15+
共用主循环仍不代表所有工具、快照与本地行为已经等价,完成状态以实际接线和测试为准。
16+
1117
## 1. 决策与当前状态
1218

1319
BitFun 只有一套 Agent Runtime 行为。`Embedded``Shared` 只描述同一套 Runtime 的物理部署方式,不是两套实现。

docs/architecture/remote-workspace-transport.md

Lines changed: 227 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Remote workspace transport
22

3-
This document defines how BitFun turns SSH hosts and Docker containers into one
4-
workspace runtime without leaking transport-specific behavior into Agent,
5-
search, terminal, or file-service callers.
3+
This document defines the transport boundary for SSH and Docker workspaces.
4+
The Agent Runtime stays on the BitFun host. Local and remote workspaces share
5+
file-tool algorithms through Session-bound IO providers; search retains native
6+
acceleration with shared matching and reduction. The convergence section below
7+
describes this boundary and the remaining capability limits.
68

79
## Goals
810

@@ -115,11 +117,15 @@ local Docker uses the existing local PTY service with `docker exec -it`.
115117
SSH workspaces continue to use SFTP. Docker workspaces use binary stdio streams,
116118
not text or base64 envelopes.
117119

118-
Reads stream chunks and report real byte progress. Writes stream to a unique
119-
temporary file in the destination directory and rename it only after the input
120-
has completed successfully. Cancellation kills the process and the shell trap
121-
removes the temporary file, so an interrupted upload does not replace a valid
122-
destination with partial content.
120+
Reads stream chunks and report real byte progress. File transfers stream to a
121+
unique temporary file in the destination directory and rename it only after the
122+
input has completed successfully. Cancellation kills the process and the shell
123+
trap removes the temporary file, so an interrupted upload does not replace a
124+
valid destination with partial content. Workspace tool writes retain ordinary
125+
filesystem semantics instead: after validating the staged bytes, they write
126+
through the destination to preserve existing links and permissions. The final
127+
write is not an atomic transaction against other writers; interruption during
128+
that phase can have a partial or unknown outcome.
123129

124130
Directory and stat records use NUL-separated fields. File names containing
125131
newlines or the delimiters used by older implementations remain round-trippable.
@@ -203,3 +209,216 @@ Contract tests cover legacy Agent/profile deserialization, defaulted connection
203209
options, remote-workspace retention, stdio round trips, cancellation, and
204210
delimiter-safe Docker metadata parsing. A Docker-backed ignored integration test
205211
is available through `BITFUN_TEST_DOCKER_CONTAINER`.
212+
213+
## Agent Runtime convergence
214+
215+
Decision: keep remote workspaces lightweight. Do not require BitFun CLI,
216+
a remote Agent daemon, a shared service, or a new remote installation. The
217+
BitFun host keeps the existing Agent Runtime, model credentials, Session and
218+
permission ownership. Only workspace filesystem and process IO crosses SSH.
219+
Read, Write, Edit, Delete and LS use the bound filesystem provider. Grep and
220+
Glob share matching/result algorithms while retaining native acceleration.
221+
Snapshot file IO uses that same boundary; complete remote Session Undo remains
222+
gated because individual recorded operations do not prove historical coverage.
223+
224+
### One Runtime, one IO boundary
225+
226+
```mermaid
227+
flowchart TB
228+
Surface["Driving surface"] --> Runtime["Existing Agent Runtime on BitFun host"]
229+
Runtime --> Owners["Session · tools · permission · hooks · snapshots"]
230+
Owners --> IO["Session-bound workspace IO"]
231+
IO --> Local["Local filesystem and process provider"]
232+
IO --> Remote["SSH / SFTP / Docker provider"]
233+
Remote --> Target["Ordinary target files and processes"]
234+
```
235+
236+
Runtime ownership and execution location are different concerns. Sharing the
237+
Runtime does not require deploying it beside the workspace. Session lifecycle,
238+
read-before-write checks, edit matching, result rendering, snapshot history and
239+
revert transitions have one implementation on the BitFun host. Providers only
240+
perform typed filesystem/process operations; they do not implement Read, Edit,
241+
Grep or fork as separate product features.
242+
243+
Target installation, process supervision, a multi-user daemon, model proxying
244+
and Session migration are not prerequisites for this SSH workspace feature.
245+
Existing App Server, Shared TUI IPC, Peer Device and Detached Dispatch contracts
246+
are unchanged.
247+
248+
### Shared owners and provider contracts
249+
250+
| Responsibility | Shared owner | Provider boundary |
251+
|---|---|---|
252+
| Agent loop, context, permissions, fork, Session persistence | Existing Runtime / Coordinator / SessionManager | None; preserve existing host ownership |
253+
| Read windows, tail, text encoding and output budgets | `tool-execution` reading algorithm | Open a byte stream; metadata when needed |
254+
| Edit/Write matching, freshness and result construction | Existing tool pipeline and read-state owner | Read bytes, inspect metadata, write bytes |
255+
| LS/Glob filtering, order and presentation | Shared listing and search helpers | Enumerate typed entries with metadata |
256+
| Grep pattern/type/ignore policy and result reduction | Shared search helpers | Native scanning, remote bytes, optional compatible search accelerator |
257+
| Snapshot hashes, compression, history and revert phases | Existing Snapshot owners on the BitFun host | Read/restore/remove actual workspace files |
258+
| Exec lifecycle and output | Existing execution owner | Local process or existing SSH process transport |
259+
| Hook contract, permission decisions and source trust | Existing hook owner | Explicitly selected execution domain and process provider |
260+
261+
`WorkspaceFileSystem` remains the typed boundary. It must distinguish missing
262+
paths from permission/transport errors, preserve byte content and symlink
263+
semantics, expose real metadata, and provide streaming reads and the mutation
264+
operations actually consumed by tools and snapshots. File length or timestamp
265+
not supplied by a backend is unknown, not zero. Remote modification times come
266+
from the target, not the controller clock.
267+
268+
Local and SFTP readers can seek. A Docker stream may reject seek explicitly
269+
while supporting the shared forward-only parser and tail ring buffer; it must
270+
not implement seek by silently downloading an entire file to a temporary local
271+
copy. Cancellation drops/cancels the transport, and successful EOF must include
272+
successful command completion. Output and memory bounds do not imply a bound
273+
on network transfer.
274+
275+
The path resolver and Runtime context select the provider once from the
276+
Session's verified workspace binding. A remote request with an unavailable
277+
provider fails; it never acquires a local provider as a fallback. Explicit
278+
`bitfun://` artifacts remain host-owned and use local storage even in a remote
279+
Session. Tools must not consult whichever workspace is currently selected in
280+
the UI. POSIX remote paths must not acquire controller OS path semantics.
281+
282+
### Search and large-file performance
283+
284+
Preserve the local native parallel scanner and its ignore behavior. Do not
285+
route all local search through a POSIX shell just to make the call sites look
286+
the same. Share pattern/type expansion, matching semantics, sorting, pagination
287+
and output construction; retain provider-specific data access optimizations.
288+
289+
An already available compatible target search executable can reduce network
290+
traffic. It is an optional accelerator, not a requirement to install BitFun.
291+
Its results must satisfy the same contract, including file type definitions,
292+
ignore rules, Unicode, filenames containing newlines, context and truncation.
293+
Do not equate different system `rg` type catalogs or approximate a Rust regex
294+
with system grep and report an empty result as success.
295+
296+
The built-in `Grep` API accepts structured search arguments; it is separate from
297+
model-authored `ExecCommand` shell text. ExecCommand preserves the submitted
298+
command and reports the target environment's actual result. The model can then
299+
choose another available command; the runtime does not silently rewrite it.
300+
301+
Current target prefilters accept nonempty case-sensitive literals or unions of
302+
literal alternatives. They never compile the full user regex in a second engine:
303+
even an installed `rg` can have different Unicode tables. Other expressions use
304+
the shared scanner. A behavior-probed `rg` returns framed NUL-delimited candidate
305+
paths. If unavailable, a behavior-probed system `grep -a -F -q` checks authorized
306+
regular files in batches of at most 128 paths / 16 KiB of command text. These are
307+
transport batch sizes, not search limits; oversized arguments use file streams.
308+
Grep additionally retains UTF-16 BOM files so native BOM decoding cannot create
309+
a match that the byte prefilter excluded. Per-file statuses and a completion
310+
marker distinguish no match from errors, startup banners and truncated output.
311+
Only the shared Rust matcher computes results, counts, context and pagination.
312+
This fallback saves file transfer but still starts one grep process per file;
313+
it is not a claim of equal performance on small-file trees or slow connections.
314+
315+
Without a compatible accelerator, identical matching may require reading
316+
remote file bytes into the shared scanner. That preserves semantics but costs
317+
bandwidth and SSH round trips. Exact total line counts and complete content
318+
hashes also require examining complete input. Make that cost observable,
319+
support cancellation/backpressure, and measure it with large-file and slow-link
320+
fixtures. Do not hide it behind arbitrary file-count, depth or size cutoffs.
321+
An existing optimized path must not be replaced until the shared path has
322+
behavior and performance evidence; incomplete migrations remain explicit.
323+
324+
The current WorkspaceFS scanner applies `.gitignore`/`.ignore` inside the
325+
requested search scope; it does not import parent, global Git or
326+
`.git/info/exclude` rules and reports that boundary in its result. File symlinks
327+
can be read, but directory-link roots and hard-link restrictions without a
328+
provider identity proof fail explicitly. Content/count searches scan candidates
329+
to compute accurate totals; multiline matching uses the native searcher's
330+
buffering. The remote query retains its cancellable 30-second deadline.
331+
Output retention is bounded by pagination independently of exact match counting;
332+
one extra retained line preserves the truncation indicator. Search results expose
333+
the selected backend, scanned-file count and stream bytes. Cancellation covers
334+
metadata, enumeration and opening as well as reading. Ordinary SSH command tasks
335+
retain transport ownership after caller drop to interrupt, drain and close their
336+
channel without cancelling sibling commands. Callers return after a bounded
337+
cleanup grace period; the transport owner retains a pending channel-open request
338+
until confirmation or disconnect, then closes a late channel without executing
339+
the cancelled command. A request that never receives confirmation cannot be
340+
individually closed with the current SSH library, so its owner remains until the
341+
transport ends rather than disconnecting the shared connection.
342+
These are current limits, not evidence of complete backend parity.
343+
344+
### Snapshot storage and safe mutations
345+
346+
Keep snapshot blobs, metadata, operation history and revert state in the
347+
existing local workspace runtime or remote-workspace mirror directories.
348+
Only the current workspace file bytes and metadata use the selected filesystem
349+
provider. Hashing, diff calculation, operation history and the staged revert
350+
state machine remain shared. No remote database, Session service, credential
351+
copy or model-network migration is introduced.
352+
353+
Snapshot manager caches, locks and persistence scope must identify the exact
354+
workspace binding, including the SSH connection. A POSIX path alone cannot
355+
identify a workspace when two users/hosts expose the same path. Existing data
356+
must remain readable; new isolation must not delete or reinterpret old records.
357+
Historical remote edits without snapshots cannot acquire a fabricated baseline
358+
from current file contents. Enable rollback only when its real evidence exists.
359+
360+
New remote snapshot directories are scoped by the complete Session connection
361+
identity inside the existing local mirror. Unattributed legacy records remain
362+
untouched and are not treated as proven history for that connection. Only
363+
successful snapshot completion adds `snapshot_recorded: true` to the persisted
364+
tool result. Operation cards use that fact to query immutable summary/diff
365+
history with the same Session scope, including while disconnected. This does
366+
not enable current-file comparison, full-Session rollback or Session-wide
367+
snapshot refresh for older remote history.
368+
369+
Prepare tracking, invoke the tool once, then complete tracking. A bookkeeping
370+
failure after a mutation cannot cause the wrapper to invoke the tool again.
371+
Retain the real tool result and an explicit snapshot warning. A transport loss
372+
after submitting a mutation can have an unknown outcome; it must not be
373+
reported as definitely unapplied or automatically replayed.
374+
375+
Local/remote read-before-write checks must use the same algorithm, but neither
376+
ordinary local filesystem writes nor SFTP provide a universal compare-and-swap
377+
against arbitrary external editors. Recheck freshness and preserve conflict
378+
errors. Do not describe an in-process lock, preflight timestamp or rename as a
379+
cross-user transaction guarantee.
380+
381+
### Multiple users and capability differences
382+
383+
Each BitFun host retains its own Session, model credentials, permissions and
384+
connection state. SSH authenticates an ordinary target OS user; no shared
385+
BitFun daemon or global target configuration is introduced. Connection routing
386+
and local mirrors must not mix profiles or identities. If users intentionally
387+
share a target OS account or directory, normal filesystem permissions and
388+
concurrent-edit conflicts still apply; BitFun cannot manufacture isolation
389+
between identical OS credentials.
390+
391+
Discover capabilities at the provider/assembly boundary, not with scattered
392+
`is_remote` gates in individual tools. Missing target programs, an unavailable
393+
interactive desktop, network loss and platform-specific process behavior can
394+
require explicit differences. A local hook executable or MCP command is not
395+
silently relocated to the target. Keep its execution domain and source trust
396+
explicit; reuse the existing engine and approval mechanism.
397+
398+
Remote workspaces do not acquire Detached Dispatch semantics through this
399+
change: closing the BitFun host does not promise a durable remote Agent run.
400+
Keep existing cancellation/reconnection behavior and report unknown command
401+
outcomes honestly.
402+
403+
### Completion evidence
404+
405+
Run the same real tool and snapshot fixtures with local and SSH-backed IO,
406+
comparing results, file bytes, error categories, ordering, read-state, fork,
407+
rollback and interaction behavior. Test direct SSH, ProxyJump, local Docker
408+
and remote Docker separately. Local fake-provider tests are useful owner
409+
coverage but are not evidence of actual SSH behavior.
410+
411+
Include binary/invalid UTF-8 input, CRLF, empty and very large files, tail and
412+
pagination, Unicode/newline names, symlinks, permission errors, offline targets,
413+
external changes, same-path different identities, snapshot start/completion
414+
failure and cancellation during transfer. Controller sentinel files must stay
415+
unread and unchanged for remote workspace requests. Preserve old profile and
416+
Session payload round trips.
417+
418+
The structural gate removes duplicate tool algorithms and routes workspace IO
419+
through the bound provider. Raw filesystem/process calls that remain must have
420+
an explicit owner: local Session/artifact storage, a concrete provider, or a
421+
measured compatible accelerator. Moving a remote shell builder into a wrapper
422+
without sharing its semantics does not satisfy the gate. Remote Control, Peer
423+
Device and Detached Dispatch remain separate scenarios requiring their own
424+
regression evidence.

0 commit comments

Comments
 (0)