0.4.0 PTC callers use Outcome contracts / PTC 结果契约.
The historical worker/recovery details below remain applicable; SDK results now wrap canonical facts in data.
Issues: #3, #8. The file and PTC components use an independent Rust worker. The FastCtx-backed background component has been removed.
The current release ships native workers for win32-x64, linux-x64, darwin-arm64 and darwin-x64, each with a version/checksum receipt, plus the Windows Bash IPC proxies. 0.3.1 shipped the first three; current support adds Intel macOS on macOS 13+. Formal verify:release requires all four target pairs and validates every PE/ELF/Mach-O worker and the Windows IPC binary/source closure. Local check --staged verifies only local development outputs. Windows/Linux ARM64 are not supported. macOS 12 lacks CLONE_ACL; its write-path fallback is a separate follow-up, not part of this support claim. The Windows Bash payload remains a Windows-only optional dependency; PowerShell is host-managed. POSIX uses host Bash. There is no postinstall download/build. Source builds use npm run build:ptc, plus npm run build:bash-ipc on Windows.
PTC/Bash use original per-call sandboxPolicy and sandbox.confine, reporting the host's full/partial enforcement honestly. Read-only permits existing result pages but rejects mutations/new audit records. Absolute operation roots are selections, not new authority; full access permits external roots without confinement. No-follow/protection/no-overwrite remain operation integrity semantics. Windows workspaces that already grant WRITE_OWNER are accepted as-is whatever their owner; otherwise an owned Modify-only root receives a backed-up, directory-only WRITE_OWNER prerequisite automatically before outer run_code/pwsh launch (#651). This is not recursive repair, ownership change, self-escalation or unconfined fallback. Real Windows provider and pure PTC SDK acceptance is covered in the release gates.
The fixed file component follows DSH fs-sandbox's trusted-operation policy fence, not arbitrary-code isolation; shell and PTC code still require confinement. Runtime binaries target Windows x64, Linux x64 and macOS arm64/x64; IPC proxies remain Windows x64 only.
In workspace-write, native batch/result operations and their audit records are
permitted under the selected workspace root through DSH confinement. Read-only
permits existing result pages only, not new records or mutations. Windows roots that
already grant WRITE_OWNER are used as-is; otherwise an owned Modify-only root is
prepared automatically with directory-only WRITE_OWNER, not FullControl, before
the outer run_code transport starts. When an ACE is added, the existing DACL is
backed up under DSH_HOME/dsh-ops/acl-backups; descendants, owners and explicit
denials are never rewritten. Matching Windows x64 Bash payloads use a private
IPC-compatible MSYS copy, with the original DSH token/confinement unchanged.
Only new MSYS signal/anonymous/data pipes and pagefile-backed process-tracker
sections get compatible object ACLs/labels. No disk or token APIs are hooked.
The runtime pins original DLL and proxy hashes; unsupported versions fail closed.
Build proxies with npm run build:bash-ipc (MSVC, DSH_OPS_VCVARS64 when needed).
Real pure-PTC SDK command/script/pipeline and permission downgrade tests pass;
GUI acceptance still requires a Desktop restart.
npm run build:ptc
npm run test:ptc
npm run verifyRust >=1.88 is required. Build uses cargo build --release --locked against
native/Cargo.toml. No install scripts download binaries. On Windows, enable the
session toolchain before building; do not add multi-version toolchains to global PATH.
The dsh-ops/mode-ptc row is enabled by default; existing explicit profile
disablement is preserved. Config accepts required (default false), an optional
absolute binaryPath, allowedTools, and alwaysLoaded; see SDK loading. DSH_OPS_WORKER_BIN can
select a build; otherwise the component uses runtime/<platform>-<arch>/dsh-ops-worker[.exe] before native/target/release.
Published 0.3.1 packages include the runtime bytes
of every supported platform; source checkouts stage them explicitly with
node bin/native-runtime.mjs stage --platform <target>. There is no automatic
compilation or download during installation.
The session must already use DSH's PTC-only presentation. The component does
not force presentation, replace the registry, or create a REPL. It recognizes the
host's public prompt assembly (tools:ptc-only section plus the reserved-only wire
schema), not private modeFor, run_code existence alone or a guessed session key.
This PTC row publishes its independent SDK only in PTC-only scopes, with inspect and the
loader initially resident. Load result on demand through ops_load_sdk, even with allowedTools: [].
Other APIs require next-step activation or alwaysLoaded.
It adds no bindings or extra prompt in Native scopes.
PTC 初始常驻 inspect 与 loader;result 通过 ops_load_sdk 按需加载,即使 allowedTools 为空也可选择。 The file row independently supplies Native ops_result page/release; see result budgets. 文件组件独立提供 Native 引用分页/释放,详见结果预算。
The host generates SDK declarations from the same tool schemas; no duplicate guide
or dynamic plan IDs are added to the system prefix.
The migrated ops_glob, ops_grep, ops_inspect_local_file and ops_replace
capabilities use one path resolved against the current DSH session cwd. .
means that cwd. Reads are permitted wherever the OS permits them, matching DSH
fs-sandbox. Replacement is denied in read-only; workspace-write fences writes to
the workspace and DSH temporary areas; other writes require danger-full-access.
These fixed trusted operations run through the host subprocess with native
no-follow path checks, not an arbitrary-code confinement runner. Shell and PTC
code continue to require OS confinement.
ops_glob.pattern and ops_grep.glob are arrays of include/exclude patterns;
! entries exclude matches. ops_grep.pattern and ops_replace.pattern are
single strings. Directory traversal skips symlink/junction/reparse entries,
hidden entries, .git, .dsh-ops, node_modules, target and .venv without
turning skipped children into a whole-operation failure. Root access failures
return ACCESS_DENIED; outside-workspace failures return POLICY_DENIED with
the active workspace root in the message. Results are bounded and UTF-8-first.
Every mutating file operation serialises cooperating writers on a hidden sidecar
next to the target: .dsh-ops-update-<sha256 of the lowercased basename>.lock,
created mode 0600 when the lock is first needed and removed when the writer that
holds it is done (#319). Single-file operations validate a snapshot before
locking: stale revisions, edit conflicts and existing destinations are refused
without a sidecar. write_file and edit_file dry runs never acquire this lock.
Removing the artifact on release used to be considered unsafe (#240 P5): a waiter that had already opened the old inode could lock it after the unlink, while the next writer created and locked a fresh file under the same name — two writers, one target. Two rules close that window without leaving anything in the operator's directory:
- Bind before publishing. After taking the exclusive lock, the holder reopens
the name and compares the
(device, inode)identity of that handle with the inode it locked. A name that no longer leads to the locked inode means the previous holder finished and removed the sidecar while this writer was opening it: the handle is dropped and the acquisition starts over. Only a bound holder publishes. - Unlink while the lock is still held, and only the bound inode. The same identity check runs first, so a sidecar that someone removed out of band and another writer has since re-created is left alone.
What this means for a directory: a published write leaves nothing behind, and a
request refused after the lock leaves nothing behind either — including
ops_replace, which locks every file its glob selects before it counts a match,
so a fenced directory-wide replace no longer leaves a sidecar beside each
candidate. A writer killed between locking and release (SIGKILL, power loss)
cannot run a cleanup path, so it can leave a zero-byte sidecar; it is not a stuck
lock — the next writer of that name binds to it, publishes under it and removes it.
test/lock-sidecar.test.mjs checks refusals, dry runs, published writes and that
reuse; native/crates/ops-files/src/file_update_lock.rs is the protocol and its
unit tests.
Because the name is derived from the target's basename rather than its bytes, the
walker's hidden-component rule keeps it out of ops_glob, ops_grep and a
directory-wide ops_replace, and ops_batch resolves its operations from the
request rather than from a walk — the artifact is never a result, never a
replacement target and never a batch item.
outputMode: "details" reports bytes (the on-disk size) and modified, a real
modification time in milliseconds since the Unix epoch — the same quantity
and unit ops_stat reports as modifiedMs, and null when the filesystem
cannot report one. sort: "modified" orders newest first and is stable, so equal
timestamps (a coarse clock, a preserved copy, two writes inside one second) keep
the walk's path order; sort: "path" and an omitted sort keep that order. The
timestamp is read without following links, and a link, junction or reparse entry
is refused before it can become a candidate, so no ops_glob item is a link;
ops_stat reports the opened file's time for a path reached through one.
The native schemas deliberately do not expose root, workspace, cwd, or
file_path aliases. ops_bash requires description, uses the host foreground/background/promoted result contract, and exposes run_in_background when host jobs are available. See the bilingual shell contract in component-reference.md / 组件参考.md.
Their important fields are (paths resolve relative to the DSH session cwd):
ops_glob: requiredpattern: string[], optionalpath,sort(path|modified) andoutputMode: "details"(on-diskbytes, epoch-millisecondmodified); at least one positive glob is required.ops_grep: required regexpattern, optionalpathand arrayglobfilter. Usesummaryfor aggregate file/match totals,countfor per-file counts,contentfor matching text (default), ordefinitionsfor only the lines where the matched identifier is declared (lexical classification, nomultiline/context flags; on a complete scanmatchedcounts every matching line andtotalthe returned declaration lines). Item pages default to 100, with 0-basedoffsetandnextOffset. Every page scans candidates to count totals, but only materializes the requested window.complete:falsemeans more results or an incomplete scan;truncated/failedscans are not exhaustive. Totals exclude non-UTF-8 and oversized files (skippedFiles); read errors incrementfailed. Bounds and traversal filters still apply; this is not a persistent scan cursor.ops_inspect_local_file: one optionalpath(session cwd by default; a directory is rejected),offset(1-based line or 16-byte hex row, default 1; legacy 0 also reads the first unit),limit(1-10000, default 200),encoding: "utf-8",view: "text" | "hex". No multiple-file, PDF, arbitrary-encoding orpagesinterface is exposed.ops_replace: required literalpatternandreplacement, optionalpath,glob,maxReplacements(1-100000, default 100) anddryRun.
File tools, ops_batch and ops_result return failures as values, not thrown
exceptions: check status and error.code/error.nextAction. A try/catch
alone misses these failures. For example:
const result = await tools.ops_grep({ path: "src", pattern: "TODO", outputMode: "summary" });
if (result.status === "completed") {
console.log({ files: result.data.files, matches: result.data.matches });
} else if (result.status === "partial") {
console.log({ coverage: result.coverage, continuation: result.continuation });
} else {
console.log({ status: result.status, error: result.error });
}File parameter bounds are described in the host-compatible SDK schema and enforced
by the native worker, even for direct PTC calls. Required fields, enums, nested
edit shapes and strict unknown-field rejection remain discoverable.
Native file rendering uses === FILE: path (Lines a-b of n) === and N | text,
with concise mutation receipts and explicit paging/effect/retry notices. PTC wraps the unchanged canonical
execution data in a typed Outcome envelope. Content blocks are capped at
64 KiB after JSON escaping; clipped displays instruct the caller to recover
undisplayed content without blindly advancing a cursor or repeating a write.
Physical LF defines line counts across projections; bare CR/control separators
are escaped for display. Invalid text encoding returns ERR_INVALID_ENCODING
with document/binary-reader guidance. Use hex byte rows to read beyond an in-line
cut. See usage / 使用指南.
ops_batch({ action: "plan", operations }) performs preflight, persists a
no-effect manifest and returns a detailsRef/job id. It does not convert into
an executable run request. Execute that exact planned manifest with:
if (plan.status === "completed") {
const execution = await tools.ops_batch({ action: "resume", id: plan.data.id });
console.log(execution); // inspect its status before reading execution.data
}action: "run" is a separate immediate path and requires the operations again;
run without operations and an id is intentionally rejected. A plan is therefore
plan → resume, not plan → run. This preserves the observed revision and
protection set instead of silently replacing the plan with new input.
const result = await tools.ops_batch({
action: "run", // plan journals without effects; resume accepts only id
protect: ["project/main"],
operations: [
{ op: "copy", path: "notes/source.md", to: "archive/source.md" },
{ op: "move", path: "reviews/pr-old", to: "archive/pr-old" },
{ op: "remove", path: "cache/explicitly-approved-target" }
]
});
console.log({ status: result.status, receipt: result.data,
error: result.status === "completed" || result.status === "partial" ? undefined : result.error });Workspace authority comes only from sandboxPolicy.resolve({session}).
Optional model root selects an absolute operation root, not new authority. Paths are root-relative forward-slash paths on all platforms,
including Windows (Unicode/spaces supported); absolute paths, .., device/stream
names and ambiguous Windows spellings are rejected rather than shell-escaped.
Destination parent directories must already exist. No overwrite, cyclic moves,
overlapping or dependent targets within a batch. No automatic selection of junk.
All items are preflighted before effects. SHA-256 content/tree snapshots guard
observations and verify copies. Linked ancestors are never traversed. Remove
unlinks leaf symlinks/junctions, not their targets. Copy/move of links or trees
containing links is intentionally rejected in v1. Linked Git worktrees (.git
files), special objects and nested audit stores require their owning manager.
Ordinary repositories with a .git directory may be explicitly operated on.
Windows readonly regular files may have their attribute cleared through a no-follow
file handle only when singly linked; no chmod of links or shared hardlinks.
Unix readonly file unlink uses native directory permissions. File mode bits are
preserved when copying; ownership, ACLs, xattrs, resource forks and directory
metadata are not promised. Move is verified copy then delete, not an atomic
rename: it works across filesystems but needs temporary space and additional I/O.
operationsRef can replace operations with a same-session result containing
operation rows. Reading a reference does not authorize anything: all filesystem
preflight and current DSH policy checks run again.
// Small data is better processed directly in JavaScript.
const stored = await tools.ops_result({ action: "put", items: operationRows });
if (stored.status !== "completed") return stored;
const batch = await tools.ops_batch({ action: "run", operationsRef: stored.data.id });
if (batch.data?.detailsRef) {
const page = await tools.ops_result({ action: "page", id: batch.data.detailsRef,
offset: 0, limit: 64, maxBytes: 16384, fields: ["operation", "state", "error"] });
console.log(page); // partial pages include an explicit continuation
}
const released = await tools.ops_result({ action: "release", id: stored.data.id });
console.log({ batch, released });put stores object rows; page supports top-level equality and field selection,
not a general query language. nextOffset, total, complete, omitted are
explicit. Pages obey serialized JSON bytes (including escape/envelope allowance),
not raw text length alone. A row that cannot fit produces OUTPUT_LIMIT, not a
silent empty page. Complete data stays accessible through subsequent pages.
Every invocation requires a calling session identity, DSH sandboxPolicy,
systemPrompt, tools and subprocess. In workspace-write, exact native argv
is wrapped through sandbox.confine. The original host's enforcement level
(full or partial, including Windows ACL partial) is accepted and reported.
Missing/broken confinement fails closed. There is no unconfined fallback.
danger-full-access intentionally bypasses confinement as the host policy defines,
and permits explicit roots outside the workspace. Policy is resolved again after
async sandbox preparation. read-only supports existing result pages through a
read-only shared lock and denies mutations or new records.
Approval/escalation stays with DSH; the component does not self-grant permissions.
DSH subprocess owns credential scrub, process termination and cancellation. Native input is bounded stdin JSON, never a shell command or argv-sized path list. Unload aborts and waits for in-flight work. Authority downgrade aborts active workers. If host jobs/controller admission is available, batch foreground work uses that registry's owner, cancel and progress surfaces, waits once, then removes its own collected terminal record. Otherwise it remains a bounded foreground call; the shared host ceiling is 30 seconds. No second background scheduler, detached-job polling tool or cloned REPL is created. Progress is currently preflight/settlement only, not per-file live reporting.
The host allocates an audit reference before spawn, so cancellation/output
failure still returns detailsRef. A reference may be absent if interruption
preceded the initial plan commit: NOT_FOUND is not evidence that later effects
were completed. Once execution starts, each item has a durable write-ahead
unknown state before any filesystem effect, and a verified settlement after it.
pending: not begun;completed: verified, never executed again;failed: refused before effects; resume rechecks the original revision;unknown: may have effects; resume checks postconditions only, never retries it.
An unresolved partial copy/deletion remains unknown for manual inspection/repair. Recovery never substitutes new targets or protections. It binds normalized root and opened-root device/inode identity, session, original operations and observations; restart in the same session can recover, another session is denied. Preflight is not a rollback transaction. A missing post-delete source proves the postcondition, not who deleted it.
Records live in <workspace>/.dsh-ops/, outside model output. Add .dsh-ops/ to
workspace Git ignores; do not commit sensitive audit paths/data. Access control is
the local workspace's permissions plus same-session tool fencing; the store is
not an encrypted or tamper-proof secret vault. A workspace owner can tamper
with records; references are not capability tokens. Capability-relative handles
and no-follow operations prevent ordinary symlink escape, but a malicious process
actively renaming mounted/open directories or changing content during an effect
is outside the stronger protected-content guarantee: the operation is not atomic.
Do not run destructive cleanup concurrently with untrusted filesystem writers.
Result records expire after 24 hours and can be released, including after expiry. Job audits do not auto-expire or permit release through the result tool. Archive completed job IDs only while no worker is active; never discard unresolved records. Latest snapshots preserve all item states; older committed generations are compacted while retaining a predecessor. File sync + generation rename persist records; Unix also syncs the store directory. Windows power-loss durability of directory renames is not claimed (process interruption is covered).
Budgets: 512 operations, 128 protections, 100000 scanned entries, depth 128, 10000 stored rows, 2 MiB request/record, 64 MiB store/4096 files, pages 1..512 rows and 512..65536 bytes. Quota overflow fails explicitly. Logical bytes count paths and can double-count hardlinks; they are not physical reclaimed space.
One isolated test/ptc.test.mjs contains three compact scenarios plus a native
host-provider acceptance block, separate from
all native/src and lib/ptc business code:
- Mixed temp workspace: protected main, link cleanup, readonly Git file, copy/move verification, collision, changed observation and interrupted audit recovery without repeating completed/unknown effects.
- 1000-row lossless pagination, budgets/projection, reference-to-batch and output limit recovery; schema/summary byte measurements (not invented model tokens).
- Real Cordis/tools/prompt scopes + real worker, simulated confinement/jobs: SDK publication, native/hybrid isolation, authority denial, interruption facts and unload. The simulated provider is not evidence of OS sandbox isolation.
Windows x64: release build, clippy -D warnings, full npm run verify and original
DSH process sandbox acceptance executed.
Linux x64 (x86_64-unknown-linux-gnu): release build, cargo test, full
npm run verify (every suite that does not need a FastCtx runtime, i.e. 28 passed
and 2 skipped) and the native worker scenarios executed on the platform, including
the DSH local sandbox's workspace/temporary-area policy fence. The released Linux
x64 worker is built for x86_64-unknown-linux-musl and statically linked, so the
shipped artifact no longer requires a GLIBC_* floor; reproduce it with
npm run build:ptc -- --target x86_64-unknown-linux-musl.
macOS arm64 (aarch64-apple-darwin) and macOS Intel (x86_64-apple-darwin) run the
full gate in CI: macos-latest (arm64) and macos-15-intel (x64) are both blocking
legs. Both build and stage
their Mach-O worker, run the suite, and verify the receipt. The earlier 0.3.1
cargo check --locked-only record is superseded. A local Intel macOS 12.7.6 run
still has one expected CLONE_ACL failure, so macOS 12 remains a follow-up.
No fixed token/time saving percentage, actual model-turn benchmark,
live Desktop upgrade, npm release or GitHub push is claimed.
Architecture: native/src/{contract,paths,batch,store,results}.rs is the shared
Rust operation core; main.rs is bounded single-call JSON transport;
lib/ptc/{contract,worker,index}.js is only DSH registration/authority/transport.
Tests do not enter published business code.