|
|
|
@@ -29,7 +29,7 @@ provider seam dsh-fs ctx.fs: text IO + ATOMIC mutation primitives who
|
|
|
|
|
provider dsh-fs-local local implementation of ctx.fs
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The model is **additive, not subtractive**: `ctx.fs` on its own is a complete, unconstrained text-storage seam — `read` reads, `write` unconditionally creates-or-overwrites, `edit` unconditionally replaces literal text in the current content. There is no "先读后写", no version check, nothing to remove; the bare provider just does the I/O atomically. `dsh-file-context` is a plugin that *adds* constraints on top: observed-state, read-before-edit, and "write/edit must be based on the version you read". So removing `dsh-file-context` does not break `dsh-tool-fs` at the service-injection boundary; it removes the policy gate and leaves the bare provider behavior. The product default still loads `dsh-file-context`, so the default user-facing behavior and prompt discipline remain read-before-write/edit. The bare-provider mode exists because the tool should not be method-coupled to the policy plugin, not because an unconstrained filesystem is the normal product stance.
|
|
|
|
|
The model is **additive, not subtractive**: `ctx.fs` on its own is a complete, unconstrained text-storage seam — `read` reads, `write` unconditionally creates-or-overwrites, `edit` unconditionally replaces literal text in the current content. There is no "先读后写", no version check, nothing to remove; the bare provider just does the I/O atomically. `dsh-file-context` is a plugin that *adds* constraints on top: observed-state, read-before-edit, and "write/edit must be based on the version you read". So removing `dsh-file-context` does not break `dsh-tool-fs` at the service-injection boundary; it removes the policy gate and leaves the bare provider behavior. The intended deployment stance is that a config loading the fs tools also loads `dsh-file-context`, so the user-facing behavior and prompt discipline are read-before-write/edit (no default/example config wires the fs tools yet — the demo agents do file ops through bash). The bare-provider mode exists because the tool should not be method-coupled to the policy plugin, not because an unconstrained filesystem is the normal product stance.
|
|
|
|
|
|
|
|
|
|
`dsh-tool-fs` no longer injects `fileContext`. It injects `fs` and `tools`/`systemPrompt`.
|
|
|
|
|
|
|
|
|
@@ -70,7 +70,7 @@ These events carry existing `dsh-fs` vocabulary (`FsTarget`, `FsVersion`, `FsWri
|
|
|
|
|
|
|
|
|
|
**The two `fs/*` decision events are single-slot decision points, NOT a composable interception chain.** A waterfall listener that does not call `next()` short-circuits the rest of the chain (verified in [vendor/cordis/src/events.ts](../../../../vendor/cordis/src/events.ts) — `waterfall` runs listeners around the final `next` thunk, and a listener that returns without calling `next()` reaches neither later listeners nor the tool's default thunk). `dsh-file-context` fully decides the write/edit expectation and does not call `next()`, so it occupies that one decision slot in the default deployment. This is deliberate: "what version basis does this mutation guard against" is a single decision, not an accumulation. The names (`fs/write-expectation`, `fs/edit-expectation`) say "produce the value", not "authorize", so they do not imply a stackable authorization chain. Genuinely composable interception (permission, audit, sandbox) belongs on the existing `tools/execute` waterfall, which every tool call already flows through — not on this fs version-decision slot.
|
|
|
|
|
|
|
|
|
|
**The occupant is decided by registration order — first-registered (or `prepend`ed) wins.** cordis dispatches waterfall listeners in registration order (`push`, or `unshift` for `prepend` — [vendor/cordis/src/events.ts](../../../../vendor/cordis/src/events.ts)), and the first non-`next()` decider short-circuits the rest. So the slot is **first-wins**, and `dsh-file-context` owning it rests on the default deployment convention: it is the decider registered for these events. The event shape does NOT itself guarantee "an unread edit is rejected" — a plugin that registers a looser `fs/edit-expectation` decider BEFORE `dsh-file-context` (or with `prepend`) would decide first and bypass the `FS_NOT_OBSERVED` gate. That is the inherent property of a first-wins single slot, stated here so it is not mistaken for an enforced invariant. This RFC does not add a multi-policy composition mechanism; the implementation requirement is that the shipped `dsh-tool-fs` dispatches these waterfalls on every write/edit path and the shipped default config loads `dsh-file-context` as the policy decider.
|
|
|
|
|
**The occupant is decided by registration order — first-registered (or `prepend`ed) wins.** cordis dispatches waterfall listeners in registration order (`push`, or `unshift` for `prepend` — [vendor/cordis/src/events.ts](../../../../vendor/cordis/src/events.ts)), and the first non-`next()` decider short-circuits the rest. So the slot is **first-wins**, and `dsh-file-context` owning it rests on the default deployment convention: it is the decider registered for these events. The event shape does NOT itself guarantee "an unread edit is rejected" — a plugin that registers a looser `fs/edit-expectation` decider BEFORE `dsh-file-context` (or with `prepend`) would decide first and bypass the `FS_NOT_OBSERVED` gate. That is the inherent property of a first-wins single slot, stated here so it is not mistaken for an enforced invariant. This RFC does not add a multi-policy composition mechanism; the implementation requirement is that `dsh-tool-fs` dispatches these waterfalls on every write/edit path and that a config wiring the fs tools loads `dsh-file-context` as the policy decider.
|
|
|
|
|
|
|
|
|
|
The actor is typed `object` in `dsh-fs` — a pure opaque carrier the provider seam never reads or narrows. The owner-derivation (`actor.agent?.session`) and the `{ agent?: { session? } }` structural shape stay entirely inside `dsh-file-context`, which narrows the `object` actor to that shape in its listeners. `dsh-fs` owns the event names and the fs vocabulary; it does NOT own the policy layer's runtime owner structure.
|
|
|
|
|
|
|
|
|
@@ -110,7 +110,7 @@ The `fs/*` decision events are **unbound waterfalls dispatched by the tool** (li
|
|
|
|
|
|
|
|
|
|
## Tool contract (`dsh-tool-fs`)
|
|
|
|
|
|
|
|
|
|
The tool keeps its model-facing schemas (`read`/`write`/`edit`, byte-for-byte unchanged) and prompt sections. The prompt guidance stays policy-first because the default product config loads `dsh-file-context`: the model is still told to read before overwriting or editing, and any wording that says the "backend" requires that should be corrected to say the default file-context policy requires it. The bare-provider fallback does not change the default prompt stance.
|
|
|
|
|
The tool keeps its model-facing schemas (`read`/`write`/`edit`, byte-for-byte unchanged) and prompt sections. The prompt guidance stays policy-first because a deployment loading the fs tools is expected to also load `dsh-file-context`: the model is still told to read before overwriting or editing, and any wording that says the "backend" requires that should be corrected to say the file-context policy requires it. The bare-provider fallback does not change the prompt stance.
|
|
|
|
|
|
|
|
|
|
`dsh-tool-fs` gains the executor responsibilities relocated from the old `fileContext` method service, including **read windowing** (`window.ts`, `READ_MAX_BYTES`, `READ_MAX_LINE_LENGTH`, `FileReadRequest`/`FileReadOutcome`/`FileTextLine`, `STREAM_MIN_SIZE`), which is the tool's rendering detail now that the tool owns the read. Those read-windowing types and helpers move into `dsh-tool-fs`; the policy plugin must not remain a type dependency for the tool.
|
|
|
|
|
|
|
|
|
@@ -140,7 +140,7 @@ An observed-state entry is the **prior-observation record**: a successful `read`
|
|
|
|
|
|
|
|
|
|
## Bare-provider behavior (no `dsh-file-context`)
|
|
|
|
|
|
|
|
|
|
This is not the default product mode — the default product config loads `dsh-file-context`. It is the unconstrained provider floor that exists once the tool is no longer coupled to a policy method service. With `dsh-file-context` absent, every `fs/*` waterfall falls through to its `undefined` default and `fs/observed` has no listener:
|
|
|
|
|
This is not the intended deployment stance — a config loading the fs tools is expected to also load `dsh-file-context`. It is the unconstrained provider floor that exists once the tool is no longer coupled to a policy method service. With `dsh-file-context` absent, every `fs/*` waterfall falls through to its `undefined` default and `fs/observed` has no listener:
|
|
|
|
|
|
|
|
|
|
- **read** is identical (it never needed policy; it only emits a now-unheard `fs/observed`).
|
|
|
|
|
- **write** unconditionally creates-or-overwrites: `expected` is `undefined`, so `writeText` writes whether or not the file exists and whatever its current version. No read-first requirement, no version check.
|
|
|
|
@@ -172,4 +172,4 @@ This amends — does not reverse — [the split-fs-seam RFC](../simplification/2
|
|
|
|
|
- **Policy events in the storage seam.** `dsh-fs` gains two version-decision events plus a recording event though it is "just storage". This is the price of decoupling (the emitter cannot depend on the policy plugin). The events carry only `dsh-fs` vocabulary plus an opaque `object` actor and no model-facing concepts, so the seam stays free of line-window/observation policy types and of the agent/session owner structure.
|
|
|
|
|
- **Single policy occupant, first-wins by convention.** The `fs/write-expectation`/`fs/edit-expectation` slots hold exactly one decider; the first-registered (or `prepend`ed) listener wins and the rest are short-circuited. `dsh-file-context` owning the slot is a deployment convention, not an event-enforced invariant — a second decider registered first would bypass it. This is acceptable because a second fs-version-policy decider is a misconfiguration, not a feature. If a future need for *layered* fs version policy appears, it is a new RFC (a composable value-passing seam), not a silent second listener on these events. Layered permission/audit/sandbox interception already has its home on `tools/execute`.
|
|
|
|
|
- **Dropping the post-read confirming stat** makes a follow-up *guarded* edit occasionally fail-closed (`FS_STALE_VERSION` → re-read) under a read/write race. This is a UX nicety lost, never a correctness hole; the provider lock still prevents wrong-version writes.
|
|
|
|
|
- **The bare provider does no read-before-write/edit and no version check.** A deployment without `dsh-file-context` lets the model overwrite or edit any existing file unconditionally. This is the deliberate meaning of keeping the tool independent of a policy service: the safety disciplines live in the default `dsh-file-context` plugin. A deployment that omits it is opting into an unconstrained filesystem on purpose; that is not the default product stance.
|
|
|
|
|
- **The bare provider does no read-before-write/edit and no version check.** A deployment without `dsh-file-context` lets the model overwrite or edit any existing file unconditionally. This is the deliberate meaning of keeping the tool independent of a policy service: the safety disciplines live in the `dsh-file-context` plugin. A deployment that omits it is opting into an unconstrained filesystem on purpose; that is not the intended stance for a config that ships the fs tools.
|