Bundle
@dsh-external/dsh-policy-strict-gate
Strict gate: turns strict_check and the failure journal from tools the model may call into policy the harness enforces — repeated-failure intervention, a glob-protected critical-path gate, and an automatic post-write check.
- Source
- catsenior507
- License
- MIT
- Updated
- Updated 21 hours ago
Readme
<div align="center">
# Strict Gate
**A tool the model must remember to call is a tool it will skip exactly when it matters most.**
A DeepSeek Harness host plugin that turns `strict_check` and the failure journal
from things the agent *may* use into policy the harness *enforces*.
[](LICENSE)
[](#install)
[](package.json)
[](package.json)
[](https://github.com/catsenior507/dsh-policy-strict-gate/stargazers)
[English](README.md) · [简体中文](README.zh.md)
</div>
---
## The problem this solves
The previous two plugins gave the agent abilities. This one takes the decision to
use them away from the agent.
That is not a criticism of the agent — it is a statement about when checks get
skipped. Self-discipline fails precisely at the moments that matter: when the
model is confident, when it is rushing, and when it is already looping. Those are
the moments a check is worth most and the moments it is least likely to be invoked
voluntarily.
There is a second reason, and it is the one that convinced me to build this: **a
check whose output lands in a tool result can be skimmed. A check whose refusal
blocks the next write cannot.**
## The three gates
Each fails independently, is configured independently, and can be switched off
without touching the others.
### 1. Repeated failure
The same failure signature `repeatThreshold` times in one session (default 3)
queues a notice naming the failure class, the shipped remediation, and the
instruction to change something. After that it speaks again every
`repeatCooldown` repeats, so a long loop does not flood the context.
The harness already does this for *byte-identical* calls. This extends it to calls
that differ in whitespace or paths but fail for the same reason.
Counted on `tools/result` rather than `tools/post-execute` deliberately: the former
fires for **every** settled outcome, including the pipeline failures that bypass
post-execute — a denied call, an unknown tool, a call refused by this very gate. A
model hammering a refused call is exactly the loop worth breaking.
### 2. Critical paths
A write to a glob-protected path is **refused** until a Lean specification
covering it has been accepted.
Two design choices make this a policy rather than an obstruction:
**A refusal carries the diagnostics.** The harness discards a denied call's
would-be result, so a refusal that only said "blocked" would destroy the
information needed to satisfy it. Instead the gate runs the check *before*
refusing and quotes what it found.
**The repair path is always open.** A write that is fixing a diagnostic this gate
just reported is allowed through. Without that rule the gate deadlocks — it would
refuse the fix to the very error it is reporting, and there would be no way out.
This is `repairPasses` in the counters, and it is the single most important
behaviour in the plugin.
Three ways out of a refusal are printed with every one: satisfy the gate, retry
the repair, or remove the path from `criticalPaths`.
### 3. Post-write syntax
Every accepted write is followed immediately by the language's own check
(`py_compile`, `node --check`, the PowerShell parser), and the diagnostics ride
along as context for the next step.
This is the cheap tier that pays for itself most often: it catches the edit that
broke parsing **at the step that broke it**, instead of three commands later via
an unrelated failure.
<a id="install"></a>
## Install
The plugin is installed as a package into a dsh **profile**. `dsh plugin` forwards
to `pnpm` inside the profile directory, so any spec pnpm accepts works.
```bash
# from GitHub (the published form)
dsh plugin --profile web add github:catsenior507/dsh-policy-strict-gate
# a local checkout, while developing
dsh plugin --profile web add /absolute/path/to/dsh-policy-strict-gate
```
`web` is the shipped GUI profile; substitute `headless`, `sdk`, `acp`, or your own
profile name. On Windows, use forward slashes in a path.
For the critical-path and post-write gates, also install the checker it reasons
with:
```bash
dsh plugin --profile web add github:catsenior507/dsh-tool-strict-check
```
The gate locates that package by walking the DSH profile directories, so it works
whether the two are linked or installed separately — and if it is absent, the gate
says so and stands the two checking gates down rather than guessing.
Then **restart the host** and confirm with:
```
strict_gate_status action=status
```
### What install does *not* do
- **No build step** — the published JavaScript is the source; no `prepare` script
runs.
- **No dependencies** — `dependencies` and `peerDependencies` are both empty.
- **`strict-check` is optional at load time.** Without it you still get the
repeated-failure gate; you lose the other two.
Node.js 20 or newer.
## Enable the critical-path gate
It ships **off**, because the glob list is a statement about *your* code that no
plugin can guess. Until you set it, the other two gates still run.
`dsh plugin add` already inserted the plugin row. Add the list to that row's
`config` in the profile's `cordis.patch.yml`:
```yaml
- insert:
- id: policy-strict-gate
name: '@dsh-external/dsh-policy-strict-gate'
config:
criticalPaths:
- 'src/core/**'
- 'src/**/*.spec.lean'
protectedTools: ['write', 'edit']
repeatThreshold: 3
repeatCooldown: 3
postWriteSyntax: true
maxDiagnostics: 5
```
Ask `strict_gate_status action=targets path=<a file>` to confirm a path really is
protected and by which rule — a glob that silently fails to match is the most
likely way this gate ends up doing nothing while looking enabled.
### How a spec is found
For a protected target `src/core/x.ts`, the gate looks for, in order:
1. `src/core/x.spec.lean`
2. `src/core/x.ts.lean`
3. `src/core/specs/x.lean`
A `.lean` file is **never** refused by the gate, whatever the globs say — a spec is
how the gate is satisfied, so refusing to let one be written would make the gate
unsatisfiable.
## Introspection
`strict_gate_status` reports which gates are active, the compiled glob rules, and
the counters:
| Counter | Meaning |
| --- | --- |
| `failures observed` | Settled abnormal calls seen |
| `repeat notices sent` | "You are repeating yourself" notices delivered |
| `critical-path refusals` | Writes refused by the glob gate |
| `repair writes allowed through` | Refusals bypassed because the write was the fix |
| `spec checks run by the gate` | Lean checks the gate performed itself |
A policy that runs silently is indistinguishable from a policy that is broken, so
this tool exists to make the difference visible.
## Development
```bash
npm test # 72 tests; the integration ones run the real Lean kernel
```
The integration tests drive the **real** collaborators rather than a stub, because
the failure they guard against is silent: two plugins that disagree about what
"verified" means would leave the gate refusing writes the model was told were fine.
`test/activation.test.js` goes further and drives the **real cordis waterfalls**,
because a listener registered on the wrong event name, or one that never calls
`next()`, throws nothing — it just makes the policy silently absent.
Four bugs worth remembering, all caught by tests rather than by review:
- **State keyed by object identity.** The state bag was a `WeakMap` keyed on the
agent object. The harness hands a *different object* for the same agent to
different hooks, so the gate learned a spec was accepted in one bag and looked
for it in another — refusing forever. Now keyed by session id, bounded.
- **The subject of a spec was derived by string surgery.** `x.spec.lean` has stem
`x`, but the real file is `x.ts`; recording `x` opened the repair window on a
path no write can match. The subject is now resolved against the directory.
- **An unhandled rejection on unload.** The hooks attach from an async
continuation, so a reload disposes the fiber before the collaborator import
resolves; registering an effect then threw `cannot create effect on inactive
context` **inside a floating promise**. That is a host-level fault caused by a
plugin being unloaded, now guarded by asking the framework whether the fiber is
still active.
- **A repeat notice cannot attach to the call that failed.** `tools/result` has no
decision channel, so the notice is queued and delivered on the *next* call's
`pre-execute`. Correct, but easy to mistake for a bug.
### Delivery, precisely
| Notice produced by | Reaches the model via | Delay |
| --- | --- | --- |
| `tools/result` (a failure) | queued → next call's `pre-execute` | one call |
| `tools/post-execute` (a write, a check) | that result's `additionalContexts` | none |
| a refusal | the `deny` reason text | immediate |
A denial has no `additionalContexts`, so anything queued for that step is appended
to the reason string instead — otherwise a refusal would silently swallow a repeat
notice produced by the very loop the refusal is part of.
| File | Role |
| --- | --- |
| `lib/index.js` | Mount, collaborator discovery, unload guard |
| `lib/gate.js` | The three gates and their decisions |
| `lib/matching.js` | Glob compilation, path extraction, spec-subject resolution |
| `lib/signature.js` | Failure identity and the remediation catalog |
| `lib/notify.js` | The one channel that reaches the model's next step |
| `lib/status.js` | `strict_gate_status` |
## License
MIT
Install
dsh plugin --profile web add github:catsenior507/dsh-policy-strict-gate
Profile: web
With the hub plugin installed, ask your agent to install it by name — it resolves the same plan shown here.
dsh plugin --profile web add github:stvlynn/dsh.fish#path:packages/dsh-plugin-hub
install dsh-external-dsh-policy-strict-gate from the hub
- This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.