@aiorouter/dsh-shield
v2.0.2
Published
AIOrouter Shield — privacy-restoration policy controls (GW-2 header, GW-1 markers) and Shield status/usage tooling for the DeepSeek Harness (dsh). Plugin-only: reads the public /v1/me/* whitelist, stores no API keys, writes no files.
Readme
@aiorouter/dsh-shield
AIOrouter Shield controls and status tooling for the DeepSeek Harness (dsh) plugin system.
Merged (2026-08-31): Shield is now part of the unified AIOrouter plugin
@aiorouter/dsh-aiorouter-mas— which adds the AIOrouter model route and the multi-agent workflow on top of the same privacy status window. New installs should use@aiorouter/dsh-aiorouter-mas; this package remains available for existing setups and receives security fixes.
This plugin is public (npm package). It only touches the public AIOrouter API contract:
account health (GET /v1/me/usage), session protection summary
(GET /v1/me/protection-summary?window=session), model discovery (GET /v1/models),
and the request header values the gateway already defines. It stores no API keys,
writes no files, and never embeds server-side privacy-taxonomy internals.
Features
- Dashboard-governed privacy (V2.0.0) — Restore/Redact/Disable are controlled
exclusively on the AIOrouter Dashboard (
https://dashboard.aiorouter.ca, per-key policy + Shield toggle). This plugin no longer writesx-aiorouter-privacy-policy/x-aiorouter-restoration-markersand no longer registers a settings section — there is intentionally no policy control surface in dsh (single control surface, no dual-control drift). aiorouter_shield_statustool — model-facing account card (balance, effective policy, Shield state, dashboard link) and the per-session protection mapping table (type × count × disposition), with metadata-only output (no protected values ever rendered). The effective policy shown is the dashboard-governed GW-3 value.- model discovery — on plugin load, when an API key is configured, fills the provider's
model list from the gateway's OpenAI-compatible
/v1/models(never clobbers user-maintained models; silent if the key is missing or the call fails). - PL-9 floating value-comparison window — a draggable, minimisable Shield window
in the Web UI (all-English UI). It defaults COLLAPSED to a compact vertical
{icon / counts / Detected}card (V1.0.2): three centre-aligned lines (shield icon, the live session detection count, and the label) in a narrow column, so the bottom-right anchor never overlaps the chat composer input. The collapsed card is itself draggable (drag moves it, click expands). Clicking expands the detailed list, which shows (a) the CURRENT Restore/Redact posture (W-5: the dashboard-governed effective policy and Shield state, read-only via theaiorouterShield/getStatusremote) and (b) what was replaced vs what the model actually saw (Type × Before (Original) × LLM Saw × Action, four equal-width columns, with aDetected {N} · Restore {R} · Redact {D} · 10-minute windowsummary line). Restored entries show the original value; redacted entries never show it. The window reads the ephemeral/v1/me/redaction-traceendpoint through host-side Typert remotes — the API key stays on the host and the payloads are held in pure client memory (never written to disk or localStorage, never sent to the model). - Client wire contract (dsh v0.1.0-rc.6, V2.6.4): the bundle declares
inject ["slots","connection","remote"]and resolves the namespace face withctx.get('remote.aiorouterShield')— the reflector-STORE lookup that deliberately has NO cordis inject guard (gateway.client.spec.tsresolvesctx.get('remote.probe')the same way). The two wrong shapes are documented regressions: declaringremote.aiorouterShieldininjectdeadlocks boot (the api-gateway installs that dotted child service only while this entry's own apply runsctx.remote.$mount, so cordis inject-wait — which has no timeout — leaves the entry PENDING →1 entry did not activate … (waiting for service: remote.aiorouterShield)), and accessing it as a plain property without the inject entry throwscannot get property "remote.aiorouterShield" without injectat apply time. The namespace face and its methods are bound INSIDEapply; the window never touches the ctx proxy at render time (React effects / 60s interval / event handlers call only the bound functions), then the bundle mounts a hand-written Typert remote contribution viactx.remote.$mount({ package, descriptors })(direct, zero-parameter,cancellation: { parameter: 'signal' }) exposinggetRedactionTraceandgetStatus; both resolve to theRemoteResultenvelope ({ ok: true, value } | { ok: false, error }). The contribution'sresultMUST be amode: 'strict'codec — the client api-gateway'srequireStrictCodecrejectssrc-jsonwithfield "result" has no strict codecand the whole plugin fails to load; the strict schema is a zod-free shape guard (src/redaction-trace-codec.ts) so zod never enters the client bundle.ctx.connection.api.*is the fixed legacy host-apiproxy face (sessions/goals/settings/…) and does not project plugin remotes — do not regress to it (V2.6.2 fix forCannot read properties of undefined (reading 'getRedactionTrace')). - Host wire contract (dsh v0.1.0-rc.6, V2.6.5): the gateway claims the
aiorouterShield/*endpoints only after TWO host-side steps. (1) A HOST strict contribution is registered viactx.typert.register(...)— the typert-loader auto-registers only packages that export a./typerthost-face artifact whose codecs are real zod v4 schemas; this package intentionally ships zero runtime deps, and the typert REGISTRY's own validation (validateInvocation/validateCodec) accepts the same zod-free shape-guard codecs the client uses (src/typert-contribution.ts). (2) The remote service is mounted withctx.root.plugin(ShieldHostRemote, { pluginCtx })—ctx.rootis the common ancestor of every cordis context, so the gateway's receiver resolution (ctx.get('aiorouterShield')) always succeeds; business seams come throughpluginCtxbecause root-scope staticinjectresolution is not guaranteed. Without BOTH steps the client seestransport failure for /api/aiorouterShield/getRedactionTrace: HTTP 404(V2.6.4 regression root cause). Both are guarded — pluginapplynever rejects, so a dsh web restart cannot regress into aFailed to load plugins web boot.
Installation
dsh plugin add @aiorouter/dsh-shieldThen configure the AIOrouter API key in Settings → Models (AIOROUTER_API_KEY).
V2.0.0 upgrade note: plugin versions ≤ 1.0.2 wrote
x-aiorouter-privacy-policy/x-aiorouter-restoration-markersinto the provider route and registered an AIOrouter Shield settings section — both are REMOVED. Restore/Redact/Disable are governed exclusively on the dashboard (https://dashboard.aiorouter.ca, per-key). If you upgraded from ≤1.0.2, remove the two stale header lines from your provider (~/.dsh/profiles/web/settings.yaml→llm-pi-ai.providers.aiorouter.headers) so requests stop carrying the old policy header; the dashboard settings then take effect for dsh traffic too.
Peer-only runtime (V1.0.1): since V1.0.1 the package declares its
@deepseek-ai/*runtime imports (cordis,dsh-tools,dsh-settings,dsh-credentials,dsh-typert-protocol,schemastery) as peerDependencies, provided by the dsh harness. Do not add them back todependencies: a nested copy in a profile'snode_modulesshadows the harness junction and breaks Symbol identity (Cannot read properties of undefined (reading 'prepare')— a harness-side peer-resolution bug, fixed in the harness during August 2026). Profiles that run pnpm installs must keepauto-install-peers=falsein their.npmrc(installed with the web profile since 2026-08-20).
Display contract (parity with the gateway)
The account card and summary table read the public /v1/me/* whitelist exactly as the
gateway serializes it (2026-08-15 audit):
effectiveRestorationPolicy/policyarrive as OBJECTS ({ default_mode, overrides? }), not preset-name strings. The plugin maps them to the SD-D9 display names (Restore All / Redact Secrets / Redact All / Custom).- The table's Action column prefers
policy_result(restored → Restored, redacted → Masked — emitted only when unanimous) and falls back to the raw DLP action semantics (pseudonymized/depseudonymized/blocked/flagged…) when there is no unanimous result.
Updating (V1.0.2)
New versions of this plugin are published to the npm registry. The DSH plugin
manager (dsh plugin) has no silent auto-update: after a release, update
with the one-command pnpm forwarder (installs the newer version; the DSH
reconcile step re-activates the bundle automatically):
dsh plugin --profile <profile> update @aiorouter/dsh-shield
# e.g. dsh plugin --profile web update @aiorouter/dsh-shield- The
aiorouter_shield_statustool shows a📦 Update available: X → Yline (checked against the public npm registry metadata at most once per day) when a newer version exists — the update command is printed right there. - Model-catalog auto-sync: the aiorouter route's model list refreshes from the
gateway's
/v1/modelseverymodelRefreshMinutes(default 360 = 6h,0= at boot only). New gateway models appear automatically; user-added models are never removed (append-only union-merge).
Privacy posture
- Zero-disk: API keys live only in the credentials seam, resolved per call.
- Zero-PII: the status tool renders counts, type labels, dispositions, and policy names only.
- Header values are the preset names the gateway accepts, or JSON the gateway validates.
Structurally invalid JSON (bad shape, empty
overrides, no recognized signal) falls back to the preset rather than poisoning the request. Per-type NAMES in the advanced JSON are validated by the gateway itself (this package never embeds the type taxonomy): a mistyped name is a loud, explicit HTTP 400 — never a silent privacy change. - This package holds no founder/personal identity and no contract pricing.
License
MIT
