Skip to content
dsh.fish
Bundle

dsh-audit-log

Runtime data-flow audit log for DeepSeek Harness: who mutated what, in what order — per-plugin, per-fiber attribution.

Source
ssdyg4444-sys
stars
1 stars
License
MIT
Updated
Updated 14 days ago

Readme

# dsh-audit-log

> **Who mutated my data flow?** — Runtime data-flow audit log for DeepSeek Harness plugins, with per-plugin, per-fiber attribution.

**English** | [简体中文](README.zh-CN.md)

`dsh-audit-log` answers the question every plugin ecosystem eventually asks: when a field in your session data
disappears or gets rewritten, **which plugin did it, in what order, and what exactly changed**?

It is a read-only observer: it never blocks, never rewrites, and never stores payload values — only structural
fingerprints (types, keys, lengths). Secrets never enter the log.

## Why

Cordis (the framework under DeepSeek Harness) shares the same `args` array across every listener of a
`waterfall` dispatch, and the `internal/get` / `internal/set` interception points are public. Any plugin can
silently rewrite data flowing through the system. With hundreds of plugins, "which plugin washed out my
field" is otherwise nearly impossible to answer.

`dsh-audit-log` makes it answerable:

- **Dispatch-level diff** — every audited dispatch snapshots argument shapes before and after, and records
  the mutations in between.
- **Per-listener window attribution** — each listener is wrapped at registration; mutations are attributed
  to the exact plugin + fiber that performed them (`confidence: "window"`), not a heuristic.

## Install

```bash
# into your web profile
cd ~/.dsh/profiles/web
pnpm add dsh-audit-log
```

Then add `dsh-audit-log` to `dsh.profile.bundles` in `package.json` (or use `dsh plugin --profile web add dsh-audit-log`),
and restart your dsh instance.

The plugin registers `ctx.auditLog` and starts recording immediately. No configuration required for basic use.

## Usage

Query the audit trail from any plugin:

```js
const records = await ctx.auditLog.query({
  events: ['message/send'],
  mutationsOnly: true,   // only dispatches that changed something
  fromSeq: 100,
  limit: 50,
})

// Each mutation carries exact attribution (when attribute_by_window is on):
//   { listenerIndex, package, fiber, confidence: 'window' }
const culprit = records[0].mutations[0].attribution?.package

// Raw per-listener windows:
const windows = ctx.auditLog.queryWindows({ event: 'message/send' })
```

### Record shape

```jsonc
{
  "v": 1, "ts": "2026-08-19T08:00:00.000Z", "seq": 42,
  "mode": "waterfall", "event": "message/send",
  "listeners": [{ "order": 0, "package": "my-plugin", "fiber": 3 }],
  "before": [ { "type": "object", "keys": ["content"] } ],
  "after":  [ { "type": "object", "keys": ["content"] } ],
  "mutations": [{
    "argIndex": 0, "path": "arg[0].content",
    "kind": "replace", "beforeLength": 56, "afterLength": 36,
    "attribution": { "listenerIndex": 1, "package": "spam-filter", "fiber": 7, "confidence": "window" }
  }]
}
```

## Configuration

Via the profile patch layer (`cordis.patch.yml`), or `$DSH_HOME/settings.yaml` namespace `audit-log`:

| Field | Default | Meaning |
|---|---|---|
| `enabled` | `true` | Global switch |
| `capacity` | `10000` | Ring-buffer size (oldest dropped first) |
| `mutations_only` | `false` | Only keep dispatches that mutated something |
| `attribute_by_window` | `true` | Per-listener attribution (wrap listeners) |
| `package_allowlist` | `[]` | Regex sources; empty audits all packages |
| `package_blocklist` | `[]` | Regex sources; excludes after allowlist |
| `events` | `[]` | Exact event names to audit; empty audits all |
| `event_allowlist` | `[]` | Regex sources for event names |

```yaml
- id: audit-log
  config:
    events: ["message/send", "before/*"]
    package_blocklist: ["my-noisy-plugin"]
```

## How it works

```
dispatch → internal/dispatch (prepend) → fingerprint(args) → listeners run → fingerprint(args) → diff → store
```

1. **Dispatch-level diff (P0/P1)** — Cordis emits `internal/dispatch` synchronously *before* public listeners
   run. The service mounts that one hook (`prepend: true, global: true`), snapshots argument shapes, lets the
   dispatch proceed, then diffs and stores. Shape-only fingerprints (`maxDepth 3`, `maxKeys 20`) keep the
   overhead negligible and payload values out of the log.
2. **Per-listener window attribution (P2)** — intercepts `internal/listener` (bail) at registration time and
   wraps every non-internal listener. Each wrapped call snapshots the shared args before and after *that
   listener*, so mutations are attributed to the exact plugin + fiber (`confidence: "window"`).
   Framework-internal events are never wrapped — no recursion, no self-noise.

## Limitations

- **Shape-only fingerprints**: equal-length string swaps (`'by-c'` → `'by-d'`) and in-place number edits are
  invisible to the diff. This is a deliberate privacy/cost trade-off; a value-aware `deep` mode is future work.
- **Sync-path timing**: the microtask resume is exact for synchronous `emit`; for async listeners in
  `serial`/`parallel`/`waterfall`, the per-listener window wrapper (P2) covers the gap precisely, while the
  dispatch-level diff remains a coarse overview.

## Relationship to other plugins

Observability tools in the ecosystem come in layers: some show *what plugins put into the model prompt*
(context layer), while `dsh-audit-log` shows *what plugins changed in the event data flow* (runtime data
layer). They are complementary — install both.

## Development

```bash
npx @dsh-io/dsh-dev check    # validate manifest, YAML, build
npx @dsh-io/dsh-dev dev      # run under dsh web with file watching
```

Tests (no framework needed):

```bash
node --test tests/
```

## License

MIT

Install

dsh plugin --profile web add github:ssdyg4444-sys/dsh-audit-log

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source