init
This commit is contained in:
commit
0858862cda
17 files changed
+2189
No files matched your search
@@ -0,0 +1,207 @@
|
||||
# dsh-facet
|
||||
|
||||
Scope an agent session to **one facet of a system** — the folder the
|
||||
conversation was started in, plus whatever you explicitly grant it.
|
||||
|
||||
A DeepSeek Harness bundle: one host row (the policy) and one agent preset (the
|
||||
enforcement).
|
||||
|
||||
---
|
||||
|
||||
## Why this exists
|
||||
|
||||
A multi-faceted system is built from modules that look alike. In the project this
|
||||
was written against there are fourteen controllers — the same idioms, the same
|
||||
naming, the same shape — and a conversation about one of them that can see all
|
||||
fourteen spends its first several steps working out which one it is looking at.
|
||||
That is a quality problem before it is a cost problem, and the cost is real too:
|
||||
in a long session the dominant expense is re-reading accumulated history, and
|
||||
most of that history is file content the agent did not need.
|
||||
|
||||
So this narrows the world to one module rather than trying to steer through all
|
||||
of them. It is the same idea as the `focus` argument in
|
||||
[`dsh-recall`](https://github.com/green-dalii/dsh-recall) — which confines a
|
||||
*search* to one module — applied to the whole agent: what it may read, where it
|
||||
may search, and what its prompt says it is.
|
||||
|
||||
## What it does
|
||||
|
||||
Three things, and they are deliberately in three different places:
|
||||
|
||||
| | where | why there |
|
||||
|---|---|---|
|
||||
| **the rule** ("work inside the facet; ask to go outside") | system prompt | invariant for the session, so it does not invalidate the prompt cache |
|
||||
| **the binding** (which folder, what is granted) | runtime-context snapshot | it changes when you grant something, and a changed system prompt costs a full-context cache miss |
|
||||
| **the guard** | `tools/pre-execute` | the part that does not depend on the model agreeing |
|
||||
|
||||
**The scope is not configured — it is where you already are.** A session's
|
||||
working directory is recorded immutably when the session is created, so starting
|
||||
a conversation in `src/controllers/room/occupancy` makes that controller the
|
||||
facet, and starting one at the project root simply leaves the session unscoped
|
||||
(which is the honest answer for a conversation that is genuinely about
|
||||
everything).
|
||||
|
||||
**Going outside is a question, not a refusal.** In that same project 41.6% of the
|
||||
code — shared types, services, infrastructure — sits outside every controller. A
|
||||
wall that cannot be opened would not confine the agent, it would only make it
|
||||
wrong, and an agent that cannot reach a shared type does not stop needing it.
|
||||
So an out-of-scope file call is put to you, and saying yes records the grant for
|
||||
the rest of the conversation.
|
||||
|
||||
### The `facet` tool
|
||||
|
||||
| action | effect |
|
||||
|---|---|
|
||||
| `status` | the root, the mode, the standing grants and this session's grants |
|
||||
| `add` | asks you to approve an extra path, then records it for this session |
|
||||
| `remove` | drops a session grant |
|
||||
| `reset` | returns to the session's own folder |
|
||||
|
||||
`add` is the only one that needs approval. It is also asked in `deny` mode,
|
||||
because otherwise there would be no way to grant anything at all.
|
||||
|
||||
## Install
|
||||
|
||||
```sh
|
||||
dsh plugin --profile web add "/path/to/dsh-facet"
|
||||
dsh --profile web --dump-config | grep preset-facet # expect the row
|
||||
```
|
||||
|
||||
The bundle contributes two layers:
|
||||
|
||||
- **`cordis.patch.yml`** — the host row `facet-policy`, which owns the mode and
|
||||
the standing grants and publishes them as `ctx.facetPolicy`.
|
||||
- **`presets/facet.patch.yml`** — the agent preset `facet`, which is the shipped
|
||||
`standard` composition plus the guard and a configured compaction row.
|
||||
|
||||
Then pick **Facet** as the preset for a session — in the Web UI that is the
|
||||
preset picker in the new-session surface, or *Settings → General → Agent preset*
|
||||
to make it the default for new sessions. The preset is fixed once a session's
|
||||
first turn has run, so choose it when you start the conversation.
|
||||
|
||||
### Configure it in your profile, not in the package
|
||||
|
||||
The host row's config is where this deployment's facts belong — the profile's own
|
||||
`cordis.patch.yml`:
|
||||
|
||||
```yaml
|
||||
- id: facet-policy
|
||||
name: dsh-facet
|
||||
config:
|
||||
mode: ask # ask | deny | off
|
||||
extraRoots: # always in scope, in every session
|
||||
- ../../shared # relative to each session's own directory
|
||||
```
|
||||
|
||||
`extraRoots` is for code a module legitimately always depends on. Anything you
|
||||
grant by hand is per-conversation and is never persisted — "this session may
|
||||
also see `src/shared`" is a statement about one conversation, and writing it down
|
||||
would quietly widen every future one, which is the failure this plugin exists to
|
||||
prevent.
|
||||
|
||||
## The compaction row
|
||||
|
||||
`presets/facet.patch.yml` also configures `compaction-basic`, because a scoped
|
||||
session and a budgeted context are two halves of one idea — scoping reduces
|
||||
context at the source, compaction is the ceiling:
|
||||
|
||||
```yaml
|
||||
thresholdRatio: 0.35 # a 350k ceiling, not the shipped 800k
|
||||
retainTokens: 80000 # an absolute working set, not 16% of a 1M window
|
||||
maxTokens: 16384 # the summarisation cap, stated rather than inherited
|
||||
```
|
||||
|
||||
The shipped ratio is `0.80` of a 1,000,000-token window, i.e. 800k. Measured
|
||||
across this deployment's 40 recorded sessions, the deepest ever reached 787,668
|
||||
pressure tokens — **the trigger had never once fired**, so every token below it
|
||||
was re-read on every subsequent step. `maxTokens` is worth stating explicitly:
|
||||
its default is not 8192 but `headroomTokens`, 65536.
|
||||
|
||||
If you want that budget on *every* session rather than only scoped ones,
|
||||
`presets/standard-compaction.patch.yml` is an opt-in overlay that applies the
|
||||
same row to the shipped `standard` preset. It is not in the bundle's default
|
||||
patch list, and the header of that file explains why: a preset declaration
|
||||
replaces the whole plugin list rather than merging into it, so the file is a
|
||||
frozen copy of `standard` that will not follow the harness when `standard`
|
||||
changes.
|
||||
|
||||
## What it does not cover
|
||||
|
||||
Honest boundaries, because each one is a decision rather than an oversight:
|
||||
|
||||
- **`bash` is not guarded.** A shell command cannot be judged reliably from
|
||||
argv, so the facet is enforced on the file tools and `bash` remains a
|
||||
legitimate way out. The prompt tells the model this rather than pretending
|
||||
otherwise.
|
||||
- **Tools that name a path in a shape the table cannot read are not guessed at** —
|
||||
including MCP tools. See `PATH_ARGUMENTS` in `lib/scope.js`; it is a list of
|
||||
tools whose path argument is a plain top-level string.
|
||||
- **This guards the tool pipeline, not the filesystem.** There is no read seam in
|
||||
the harness — `dsh-fs-sandbox` states plainly that "reads pass through
|
||||
untouched: every mode permits reading" — so anything that reads through
|
||||
`ctx.fs` without going through tool dispatch (another plugin, the HTTP
|
||||
workspace-files endpoint) is unaffected. Fencing *all* readers would mean
|
||||
replacing `ctx.fs` with a stricter `FileSystem` subclass.
|
||||
- **A grant is not persisted**, so it does not survive a process restart.
|
||||
- **Sessions at the project root are not scoped at all**, which is deliberate.
|
||||
|
||||
## What has been verified, and what has not
|
||||
|
||||
Verified:
|
||||
|
||||
- 42 tests (`npm test`) over the scope algebra, the policy state, and the guard —
|
||||
including that containment is decided on canonical targets (a symlink inside
|
||||
the facet pointing outside it is refused), that a prefix is not a parent
|
||||
(`/a/room` does not contain `/a/roommate`), that a malformed tool call never
|
||||
makes the guard throw, and that every `facet` result satisfies the tool's own
|
||||
declared output schema.
|
||||
- The bundle's patch layers parse, merge and land correctly against the real
|
||||
profile: `dsh --profile web --patch ./cordis.patch.yml --patch
|
||||
./presets/facet.patch.yml --dump-config` shows `facet-policy`, `preset-facet`,
|
||||
the `facet-guard` row and the compaction config, with `preset-standard`
|
||||
untouched. The optional `standard-compaction` overlay was checked the same way.
|
||||
|
||||
**Not yet verified: a live session.** The plugin has not been loaded by a running
|
||||
harness, because that needs the bundle installed. The first thing to check after
|
||||
installing is that `preset-facet` is not `broken` — a preset whose child row
|
||||
fails to resolve still appears on the roster, with its diagnostic, and cannot
|
||||
compose a session:
|
||||
|
||||
```sh
|
||||
dsh --profile web --dump-config | grep -A 3 preset-facet
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```sh
|
||||
node scripts/dev-links.mjs # link the harness's cordis/schemastery/dsh-tools
|
||||
npm test
|
||||
```
|
||||
|
||||
`dev-links.mjs` links rather than installs those three packages, and for
|
||||
`@deepseek-ai/cordis` that is a correctness requirement rather than tidiness: a
|
||||
second copy of Cordis means a second `Context` class, two service registries in
|
||||
one process, and a plugin whose services are invisible to the harness that loaded
|
||||
it.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
index.js host row: the policy, published as ctx.facetPolicy
|
||||
lib/policy.js the stateful half — scope resolution, grants
|
||||
lib/scope.js the pure half — argument extraction, text, reasons
|
||||
lib/guard.js preset row: rule, binding, tool, pre-execute guard
|
||||
presets/facet.patch.yml the `facet` agent preset
|
||||
presets/standard-compaction.patch.yml optional: the same budget for `standard`
|
||||
test/ 42 tests
|
||||
```
|
||||
|
||||
## Also in this repository
|
||||
|
||||
- [`facet-preset.md`](../facet-preset.md) — the design note this was built from,
|
||||
including the harness mechanisms it rests on.
|
||||
- [`compaction-tuning.md`](../compaction-tuning.md) — the investigation into why
|
||||
compaction had never fired. Its measurements reproduce exactly; note that its
|
||||
"The change" section prescribes `$DSH_HOME/.agent-presets/`, a directory
|
||||
**nothing reads any more** on the installed harness. Presets are bundle patch
|
||||
declarations now, which is what `presets/facet.patch.yml` is.
|
||||
Reference in new issue
Block a user