@pmcollab/coworkstream-audit
v0.1.0
Published
Append-only event log for @pmcollab/coworkstream. Captures every action and engine event for compliance, debugging, and agent training.
Readme
@pmcollab/coworkstream-audit
Tamper-evident, append-only event log for @pmcollab/coworkstream. Each entry is hash-chained to the previous via SHA-256 over a canonical serialization of the entry, so any post-hoc mutation is detectable provided the adapter satisfies the contract below.
Install
npm install @pmcollab/coworkstream-auditUse
import { createAuditLog, createMemoryAdapter, createAuditAdapter } from '@pmcollab/coworkstream-audit'
const log = createAuditLog({ adapter: createMemoryAdapter() })
await log.append({ type: 'workstream.action_fired', itemId: 'i1', action: 'approve', userId: 'u1' })
const history = await log.read({ itemId: 'i1' })
const verification = await log.verify() // { ok: true } or { ok: false, brokenAt, reason }
// Periodically checkpoint the head outside this trust domain (recommended):
const head = await log.getHead()
await fetch('https://checkpoint.example.com/anchors', {
method: 'POST',
body: JSON.stringify({ logId: 'workstream', head, at: new Date().toISOString() }),
})Wiring into the engine and HTTP API
Both @pmcollab/coworkstream-engine and @pmcollab/coworkstream-server accept an optional audit callback. Bridge them in:
const engine = createEngine({
...,
store: { ...myStore, audit: createAuditAdapter(log) },
})Now every dispatch, escalation, auto-resolve, and human resolution is captured.
Adapter contract — REQUIRED behaviors
Adapters that violate this contract will produce a chain that cannot be verified. This is a security contract, not a soft expectation.
interface AuditAdapter {
/** Persist an entry exactly as written. Field set MUST be preserved. */
append(entry: AuditEntry): Promise<void>
/**
* Return entries IN APPEND ORDER (oldest first), unless `reverse: true`.
* Adapters that cannot guarantee order must store and return a monotonic
* `seq` field and order by it.
*/
readAll(filter?: AuditFilter): Promise<AuditEntry[]>
/** Optional. */
count?(): Promise<number>
}Concretely:
- Insertion-order preservation.
verify()walksreadAll({})linearly and validatesentry[i].prevHash === entry[i-1].hash. If your storage is a SQL table without aseqcolumn, add one andORDER BY seqinreadAll. - Field-set preservation. The canonical hash includes every field on the entry. An adapter that drops
undefinedfields is fine (the hasher already does that), but one that adds extra fields at read time will break verification. - Append-only at the storage layer. The chain detects post-hoc mutation, but only if the adapter does not silently overwrite entries. For Postgres, grant only INSERT to the writer role and lock the audit table against UPDATE/DELETE via a trigger. For object stores (S3 + JSONL), use bucket policies that deny
PutObjectoverwrites. - Single-process append serialization is provided by this package; multi-process serialization is your responsibility. The recommended pattern for Postgres is:
BEGIN; SELECT hash FROM workstream_audit ORDER BY id DESC LIMIT 1 FOR UPDATE; INSERT INTO workstream_audit (...) VALUES (...); COMMIT;
Threat model — what the chain detects, and what it doesn't
Detects:
- Post-hoc mutation of any field on a stored entry (changes the hash;
verify()fails at that entry). - Reordering of entries (changes which entry's
prevHashmatches whichhash). - Insertion of a fabricated entry between two real ones, if the attacker cannot rewrite all subsequent entries.
Does NOT detect:
- An attacker who has BOTH write access to the storage backend AND the ability to call
append(). Such an attacker can rebuild the entire chain from any point and the result will verify cleanly. - Truncation of the tail (the chain stays internally consistent if the most-recent N entries are deleted).
Recommended mitigation for higher-assurance environments:
Periodically read log.getHead() and store the head hash in a separate trust domain (an HSM/KMS-signed checkpoint, a customer's S3 bucket with versioning, a third-party WORM log). At audit time, re-fetch the head and compare to the latest checkpoint. This converts the chain from "tamper-evident against the application" to "tamper-evident against everyone except parties with access to BOTH the application AND the checkpoint store."
License
Commercial. See LICENSE in the repository root.
