@compr/opscontext-mcp 2.8.4 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +80 -0
- package/README.md +17 -17
- package/defaults/claude-code-hook.sh +11 -3
- package/dist/audit.d.ts +109 -1
- package/dist/audit.js +485 -41
- package/dist/cli-commands.js +2 -0
- package/dist/cli.js +80 -1
- package/dist/detector.js +7 -3
- package/dist/hooks.js +26 -13
- package/dist/http-server.d.ts +7 -0
- package/dist/http-server.js +38 -1
- package/dist/index.js +20 -1
- package/dist/rubric.js +1 -1
- package/dist/secret-shapes.d.ts +28 -0
- package/dist/secret-shapes.js +132 -0
- package/dist/server-registry.d.ts +11 -1
- package/dist/server-registry.js +29 -4
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,86 @@ All notable changes to OpsContext for AI Agents (previously ContextEngine — MC
|
|
|
4
4
|
|
|
5
5
|
> Entries for 2.2.0 through 2.4.0 were not backfilled here; see `docs/sessions/SESSION_19` through `SESSION_21` for those releases.
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [2.9.1] 2026-09-25: a staged file name is data, never shell
|
|
10
|
+
|
|
11
|
+
### Security
|
|
12
|
+
|
|
13
|
+
- **A staged file name is data, never shell** (`src/hooks.ts`, LOCK
|
|
14
|
+
`[STAGED-PATH-IS-AN-ARGUMENT-NEVER-A-SHELL-STRING]`). The pre-commit checkers ran
|
|
15
|
+
`git diff --cached -- "<name>"` and `git show :"<name>"` through a shell, escaping only the
|
|
16
|
+
double quote. Proven on 2026-09-25: a staged file named `note$(touch PROOF).md` ran `touch`
|
|
17
|
+
while `contextengine hook secret-scan` listed the staged files, so a cloned repo or a pull
|
|
18
|
+
request could run a command on the committer's machine. Names now reach git as arguments,
|
|
19
|
+
listed with `-z` and matched with a `:(literal)` pathspec; the rule-parity index read, whose
|
|
20
|
+
names come from `policy.json`, takes the same path.
|
|
21
|
+
- **Every staged file is scanned, whatever its name.** The same line dropped every non-ASCII
|
|
22
|
+
name (git prints `caf\303\251.md` quoted, the lookup failed, the file was skipped unscanned)
|
|
23
|
+
and a name containing `*` matched other files too. A planted key in `café.md` passed the
|
|
24
|
+
scanner before; it is caught now. A diff above 1 MB was skipped silently as well; the buffer
|
|
25
|
+
is 32 MB.
|
|
26
|
+
|
|
27
|
+
## [2.9.0] 2026-09-25: credentials and prompt words stay out of the audit log
|
|
28
|
+
|
|
29
|
+
### Security
|
|
30
|
+
|
|
31
|
+
- **The public repo is now a release copy** (`scripts/release-public.sh`, LOCK
|
|
32
|
+
`[PUBLIC_REPO_ONLY_FROM_RELEASE_SCRIPT]`). The working repo goes private; the public one
|
|
33
|
+
receives only the code, tests, README, licence and change list, checked for forbidden paths
|
|
34
|
+
and scanned for credential shapes before every push. The licence server code is no longer public.
|
|
35
|
+
|
|
36
|
+
- **Captured text is redacted before it reaches the audit log** (`src/secret-shapes.ts`,
|
|
37
|
+
`src/http-server.ts`, LOCK `[CAPTURE-IS-REDACTED-AT-THE-DOOR]`). The Claude Code hook sent the
|
|
38
|
+
first 4,000 characters of every prompt and 200 of every command unfiltered. Every capture event
|
|
39
|
+
now passes one set of credential shapes (vendor keys, passwords inside URLs, `sshpass -p`,
|
|
40
|
+
`mysql -p`, `Bearer`, `curl -u`, `--password`, `name = value`) at the one receiver, from every
|
|
41
|
+
surface.
|
|
42
|
+
- **`contextengine audit-scrub [--apply --reason "..."]`** removes the same shapes from records
|
|
43
|
+
written before, and acknowledges every rewrite on the chain, so `audit-verify` still passes
|
|
44
|
+
(LOCK `[SCRUB-IS-ACKNOWLEDGED-REDACTION]`). Dry run by default, idempotent.
|
|
45
|
+
- **The search index never holds a credential** (LOCK `[INDEX-NEVER-SERVES-A-CREDENTIAL]`): every
|
|
46
|
+
chunk is redacted as the index is built, including dotenv-derived chunks, whose masking had
|
|
47
|
+
skipped the password inside a database URL.
|
|
48
|
+
- **Prompts and AI responses are no longer stored as text** (LOCK `[PROMPT-TEXT-IS-NOT-KEPT]`):
|
|
49
|
+
the audit log keeps their length and a keyed fingerprint, so an exact repeat is still caught;
|
|
50
|
+
commands keep their redacted text. `OPSCONTEXT_KEEP_PROMPT_TEXT=1` restores the old behaviour.
|
|
51
|
+
- **The Claude Code hook keeps the prompt and the receiver's secret off curl's command line**,
|
|
52
|
+
where every program on the machine could read them (LOCK `[HOOK-KEEPS-PROMPT-AND-SECRET-OFF-ARGV]`).
|
|
53
|
+
Existing installs get it with `contextengine install-claude-hook`.
|
|
54
|
+
|
|
55
|
+
### Fixed
|
|
56
|
+
|
|
57
|
+
- **`contextengine servers` reported a server as current when only a module other than the entry
|
|
58
|
+
script had changed** (LOCK `[BUILD-HASH-COVERS-EVERY-MODULE]`): the build fingerprint now covers
|
|
59
|
+
every `.js` file of the build.
|
|
60
|
+
|
|
61
|
+
- **Two overlapping rotations could erase archived history** (`src/audit.ts`, LOCKs
|
|
62
|
+
`[ROTATION-HOLDS-THE-LOCK-BEFORE-IT-PLANS]` and `[SEGMENT-IS-NEVER-OVERWRITTEN]`). Only the
|
|
63
|
+
automatic rotation took the rotate lock; the manual `audit-rotate` did not. On 2026-09-15 two
|
|
64
|
+
rotations both wrote `audit-0020.jsonl`, the second replaced the first, and 51,174 records
|
|
65
|
+
vanished (`audit-verify`: one orphan). Every rotation now takes the lock before it plans,
|
|
66
|
+
numbers its segment from the highest existing one, puts it in place without ever replacing a
|
|
67
|
+
file, and stops if the live log changed under it. Replayed in two real processes: the old
|
|
68
|
+
build erased 200,000 of 300,000 records; the new one lost nothing at any timing, even with its
|
|
69
|
+
lock broken.
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- **`contextengine audit-restore <file> [--apply --reason "..."]`** (LOCK
|
|
74
|
+
`[RESTORE-ONLY-CLOSES-A-PROVEN-GAP]`). Puts a lost block of records back from a backup, only
|
|
75
|
+
into a gap `audit-verify` reports, only at a segment boundary, and only if the block is the
|
|
76
|
+
original: strictly linear, every hash valid, joined to both sides of the gap. Dry run by
|
|
77
|
+
default; `--apply` re-verifies, removes its own segment unless the chain improved by exactly
|
|
78
|
+
that gap, and records an `audit.restore` event. The 51,174 records above were restored from a
|
|
79
|
+
Time Machine snapshot with it, and the chain verifies again.
|
|
80
|
+
|
|
81
|
+
### Docs
|
|
82
|
+
|
|
83
|
+
- **The npm page matches the new behaviour**: Browser Capture and the Claude Code step no longer
|
|
84
|
+
say prompts are stored as text, the privacy table says what the package version is used for
|
|
85
|
+
now, and every link points to the public repo `FASTPROD/opscontext-mcp`.
|
|
86
|
+
|
|
7
87
|
## [2.8.4] 2026-09-17: a gate on complexity, a watch on doubled hooks
|
|
8
88
|
|
|
9
89
|
### Added
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@ Claude Code, Cursor, and Copilot write code without seeing your servers — so t
|
|
|
12
12
|
|
|
13
13
|
OpsContext is an [MCP](https://modelcontextprotocol.io) server. It runs locally, snapshots your live infra (PM2 processes, nginx config, Docker containers, git status, cron jobs, redacted env), and exposes it via tools your AI coding agents (Claude Code, Cursor, Copilot, Windsurf, OpenClaw) can call in real time. Everything stays on your machine — no telemetry, no code uploads.
|
|
14
14
|
|
|
15
|
-
> **🌐 Browser Capture (Phase 1, shipped 2026-06):** OpsContext now
|
|
15
|
+
> **🌐 Browser Capture (Phase 1, shipped 2026-06):** OpsContext now records prompts, assistant responses and tool calls from **Claude.ai**, **ChatGPT.com**, *and* your **Claude Code** terminal sessions in the same hash-chained audit log. Since 2.9.0 a prompt or a response is kept as its length and a keyed fingerprint, never its words; commands are kept with credentials redacted. Cross-surface drift detection becomes possible (e.g. catch when a model says one thing in the browser and another in the terminal). See [Step 3](#3-capture-browser--claude-code-events-optional) below.
|
|
16
16
|
|
|
17
17
|
## Why
|
|
18
18
|
|
|
@@ -35,8 +35,8 @@ Plus the persistent-memory + search features carried forward from the contexteng
|
|
|
35
35
|
- ⚡ **Instant startup** — keyword search ready immediately, embeddings load in background
|
|
36
36
|
- 💾 **Session Persistence** — AI agents can save/restore context across conversations
|
|
37
37
|
- 💡 **Learning Store** — permanent operational rules that auto-surface in search results
|
|
38
|
-
-
|
|
39
|
-
-
|
|
38
|
+
- 🛡️ **Protocol Firewall** — progressive enforcement that ensures agents commit, document, and save learnings
|
|
39
|
+
- 🔌 **Plugin Adapters** — extend with custom data sources (Notion, Jira, RSS, etc.)
|
|
40
40
|
- 🧩 **MCP native** — works with any MCP-compatible client (VS Code, Claude, Cursor, OpenClaw)
|
|
41
41
|
|
|
42
42
|
### What OpsContext is NOT
|
|
@@ -167,7 +167,7 @@ npm i -g @compr/opscontext-mcp && opscontext install-claude-hook
|
|
|
167
167
|
|
|
168
168
|
(Prefer the global install here: the hook scripts keep absolute paths to the CLI, and an `npx` cache copy can be pruned.)
|
|
169
169
|
|
|
170
|
-
Adds `UserPromptSubmit`, `PostToolUse`, and `SessionStart` hook entries to `~/.claude/settings.json` so every Claude Code prompt
|
|
170
|
+
Adds `UserPromptSubmit`, `PostToolUse`, and `SessionStart` hook entries to `~/.claude/settings.json` so every Claude Code prompt (kept as a length and a keyed fingerprint, not its words) and tool call (credentials redacted) lands in the same audit log as the browser events, plus a `Stop` entry: the **session gate** (2.7.0). A Claude Code turn cannot end while the repo's OpsContext session is older than the last commit; the agent is told which session to save, which session doc to update, and how far the agent docs are behind. No more "did you save the session?" at the end of a day. Details: `npx @compr/opscontext-mcp session-gate --help`.
|
|
171
171
|
|
|
172
172
|
Verify:
|
|
173
173
|
```bash
|
|
@@ -539,7 +539,7 @@ Everything happens locally — search, scoring, learnings, sessions, embeddings.
|
|
|
539
539
|
| License key (`CE-XXXX-...`) | Activation + daily heartbeat | Validate subscription |
|
|
540
540
|
| Machine ID (SHA-256 hash) | Activation + daily heartbeat | Enforce machine limit |
|
|
541
541
|
| Email | Activation only | Tie the licence to an account |
|
|
542
|
-
| Package version | Activation only |
|
|
542
|
+
| Package version | Activation only | Recorded with your activation, so support knows which version a machine runs |
|
|
543
543
|
| Platform/arch (e.g., `darwin/arm64`) | Activation only | Compatibility check |
|
|
544
544
|
| Licence bundle version | Daily heartbeat | Compatibility marker carried in the signed licence |
|
|
545
545
|
|
|
@@ -553,7 +553,7 @@ That is the complete list. The activation request sends exactly six fields and t
|
|
|
553
553
|
|
|
554
554
|
One file in the published package is deliberately unreadable: `dist/rubric.js`, which holds the scoring thresholds (what earns which points). Those values are commercial IP under [BSL-1.1](LICENSE), and knowing them exactly makes an AI-readiness score easy to game by padding files to hit a number rather than doing the work.
|
|
555
555
|
|
|
556
|
-
**What that hides: values. What it does not hide: behaviour.** No code path, network call, file access, or data flow is concealed anywhere in this package. The scoring logic itself, every collector, the search ranker, and both network calls above ship as readable JavaScript — and the full source is public at [FASTPROD/ContextEngine](https://github.com/FASTPROD/
|
|
556
|
+
**What that hides: values. What it does not hide: behaviour.** No code path, network call, file access, or data flow is concealed anywhere in this package. The scoring logic itself, every collector, the search ranker, and both network calls above ship as readable JavaScript — and the full source is public at [FASTPROD/ContextEngine](https://github.com/FASTPROD/opscontext-mcp). If a privacy claim on this page were false, the code that broke it would be right there to find.
|
|
557
557
|
|
|
558
558
|
### Why this matters
|
|
559
559
|
|
|
@@ -573,19 +573,19 @@ For commercial licensing: [yannick@compr.ch](mailto:yannick@compr.ch)
|
|
|
573
573
|
|
|
574
574
|
## Publisher
|
|
575
575
|
|
|
576
|
-
**OpsContext is built by PROD LLC**, an operating brand of **
|
|
576
|
+
**OpsContext is built by PROD LLC**, an operating brand of **CSS LLC** (Cross Stream Solutions Sàrl), a Swiss company incorporated in 2005. The engineering team works under the FASTPROD name, which is also the GitHub organisation hosting this repository.
|
|
577
577
|
|
|
578
578
|
The VS Code Marketplace lists the extension under the legal-parent publisher ID `css-llc`; the npm package is published under the `@compr` scope. Both belong to the same entity.
|
|
579
579
|
|
|
580
|
-
PROD LLC also operates these
|
|
580
|
+
PROD LLC also operates these products. The full, current list is on **[compr.fr](https://compr.fr)**.
|
|
581
581
|
|
|
582
|
-
|
|
|
582
|
+
| Product | What it does | Site |
|
|
583
583
|
|---|---|---|
|
|
584
|
-
| **
|
|
585
|
-
| **
|
|
586
|
-
| **
|
|
587
|
-
| **INVOC** |
|
|
588
|
-
| **PLANK** |
|
|
589
|
-
| **compR** |
|
|
590
|
-
|
|
591
|
-
Contact: [yannick@compr.ch](mailto:yannick@compr.ch). Full corporate disclosure at [docs/about.md](docs/about.md).
|
|
584
|
+
| **CROWLR** | Recruitment software: applicant tracking for companies, a job app for candidates, live event sensing | [admin.crowlr.com](https://admin.crowlr.com) · [app.crowlr.com](https://app.crowlr.com) · [crowlr.io](https://www.crowlr.io) |
|
|
585
|
+
| **KONIVE** | AI career agent: salary negotiation and job matching | [konive.com](https://konive.com) |
|
|
586
|
+
| **INVOC** | Grocery scanner app for shoppers, brand monitoring for food companies | [invoc.io](https://invoc.io) |
|
|
587
|
+
| **INVOC.me** | Demand forecasting shared by operations, sales and finance | [invoc.me](https://invoc.me) |
|
|
588
|
+
| **PLANK** | Hyperlocal social app for iOS and Android | [plank.io](https://plank.io) |
|
|
589
|
+
| **compR** | Company site, and candidate credibility scoring | [compr.fr](https://compr.fr) · [compr.app](https://compr.app) |
|
|
590
|
+
|
|
591
|
+
Contact: [yannick@compr.ch](mailto:yannick@compr.ch). Full corporate disclosure at [docs/about.md](https://github.com/FASTPROD/opscontext-mcp/blob/main/docs/about.md).
|
|
@@ -91,10 +91,18 @@ esac
|
|
|
91
91
|
[ -n "$PAYLOAD" ] || exit 0
|
|
92
92
|
|
|
93
93
|
# POST with 1s hard timeout. Any error → silent (curl >/dev/null 2>&1, exit 0).
|
|
94
|
-
|
|
94
|
+
# [LOCKED] [HOOK-KEEPS-PROMPT-AND-SECRET-OFF-ARGV] - 2026-09-25
|
|
95
|
+
# [NEVER] put $PAYLOAD or $SECRET in curl's arguments (`--data "..."`, `-H "...: $SECRET"`).
|
|
96
|
+
# WHY: a process's arguments are readable by every program on the machine (`ps`), for as long
|
|
97
|
+
# as curl runs, up to the 1 s timeout. Until 2.8.4 each prompt (up to 4,000 characters)
|
|
98
|
+
# and the receiver's shared secret rode there.
|
|
99
|
+
# FIX: printf is a shell builtin, so it starts no process: it pipes the body into curl's
|
|
100
|
+
# standard input (--data-binary @-), and writes the header into a process-substitution
|
|
101
|
+
# file (-H @file, curl 7.55 and later), which only curl reads.
|
|
102
|
+
printf '%s' "{\"events\":[$PAYLOAD]}" | curl -sS --max-time 1.0 \
|
|
95
103
|
-H "Content-Type: application/json" \
|
|
96
|
-
-H
|
|
97
|
-
--data
|
|
104
|
+
-H @<(printf 'X-OpsContext-Secret: %s\n' "$SECRET") \
|
|
105
|
+
--data-binary @- \
|
|
98
106
|
"$ENDPOINT" >/dev/null 2>&1
|
|
99
107
|
|
|
100
108
|
exit 0
|
package/dist/audit.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type AuditEvent = "learning.save" | "learning.delete" | "learning.store_unreadable" | "learning.store_shrink_refused" | "learning.store_growth_refused" | "server.start" | "server.role" | "index.write" | "learning.import" | "learning.export" | "session.save" | "session.delete" | "activation.activate" | "activation.deactivate" | "activation.heartbeat" | "activation.signature_reject" | "activation.legacy_signature" | "firewall.escalate" | "hook.block" | "hook.bypass" | "policy.skipped" | "browser.prompt" | "browser.response" | "browser.tool_call" | "browser.session_start" | "browser.session_end" | "browser.capture_miss" | "vscode.prompt_submit" | "vscode.tool_call" | "vscode.session_start" | "drift.detected" | "notification.fired" | "community.sync_ok" | "community.sync_error" | "audit.rotate" | "audit.redact";
|
|
1
|
+
export type AuditEvent = "learning.save" | "learning.delete" | "learning.store_unreadable" | "learning.store_shrink_refused" | "learning.store_growth_refused" | "server.start" | "server.role" | "index.write" | "learning.import" | "learning.export" | "session.save" | "session.delete" | "activation.activate" | "activation.deactivate" | "activation.heartbeat" | "activation.signature_reject" | "activation.legacy_signature" | "firewall.escalate" | "hook.block" | "hook.bypass" | "policy.skipped" | "browser.prompt" | "browser.response" | "browser.tool_call" | "browser.session_start" | "browser.session_end" | "browser.capture_miss" | "vscode.prompt_submit" | "vscode.tool_call" | "vscode.session_start" | "drift.detected" | "notification.fired" | "community.sync_ok" | "community.sync_error" | "audit.rotate" | "audit.redact" | "audit.restore";
|
|
2
2
|
export interface AuditRecord {
|
|
3
3
|
ts: string;
|
|
4
4
|
event: AuditEvent;
|
|
@@ -26,11 +26,17 @@ export interface RotationPlan {
|
|
|
26
26
|
segmentFile: string | null;
|
|
27
27
|
/** Set when the rotation must not run, with the reason. */
|
|
28
28
|
refusedReason: string | null;
|
|
29
|
+
/** Hash of the first live record and the live record count the plan was computed on. The
|
|
30
|
+
* rotation only slices a log that still starts there. [LOCK] [ROTATION-HOLDS-THE-LOCK-BEFORE-IT-PLANS] */
|
|
31
|
+
firstLiveHash?: string | null;
|
|
32
|
+
liveCount?: number;
|
|
29
33
|
}
|
|
30
34
|
export interface RotationResult extends RotationPlan {
|
|
31
35
|
rotated: boolean;
|
|
32
36
|
bytesArchived: number;
|
|
33
37
|
bytesRemaining: number;
|
|
38
|
+
/** True when another rotation held the rotate lock, so nothing was read or written. */
|
|
39
|
+
inProgress?: boolean;
|
|
34
40
|
}
|
|
35
41
|
export interface RotateOptions {
|
|
36
42
|
/** Archive records older than this many days. Minimum 1. */
|
|
@@ -64,6 +70,20 @@ export declare function planRotation(opts?: RotateOptions): RotationPlan;
|
|
|
64
70
|
* Refuses to run on a chain that does not currently verify: rotating a log with altered
|
|
65
71
|
* or orphaned records would bake the damage into an append-only segment and make the
|
|
66
72
|
* cause unrecoverable. Forks are fine — they are concurrency, not tampering.
|
|
73
|
+
*
|
|
74
|
+
* [LOCKED] [ROTATION-HOLDS-THE-LOCK-BEFORE-IT-PLANS] - 2026-09-24
|
|
75
|
+
* [NEVER] let any caller rotate without the rotate lock, and never plan (choose the segment
|
|
76
|
+
* name, count the slice) before holding it.
|
|
77
|
+
* WHY: on 2026-09-15 at 12:44:46Z and 12:44:51Z two rotations both wrote audit-0020.jsonl. The
|
|
78
|
+
* manual `audit-rotate` command called this function directly, and only autoRotateAuditLog()
|
|
79
|
+
* took the lock. The second run planned while the first was mid-write (same segment name),
|
|
80
|
+
* then applied its old count to the live log the first run had already cut: it archived the
|
|
81
|
+
* first run's remainder into the same file name and erased the first run's segment. 51,174
|
|
82
|
+
* records vanished; verifyChain() reported an orphan. They were put back on 2026-09-24 from a
|
|
83
|
+
* Time Machine local snapshot with restoreSegment().
|
|
84
|
+
* FIX: this function takes the lock first and plans under it, so the manual command, auto-rotation
|
|
85
|
+
* and anything added later all go through one lock. The slice is refused unless the live log
|
|
86
|
+
* still starts with the record the plan read. A held lock returns inProgress, untouched.
|
|
67
87
|
*/
|
|
68
88
|
export declare function rotateAuditLog(opts?: RotateOptions): RotationResult;
|
|
69
89
|
/**
|
|
@@ -81,6 +101,8 @@ export declare function rotateAuditLog(opts?: RotateOptions): RotationResult;
|
|
|
81
101
|
* rotation buys ~a day of quiet. A dedicated rotate lock (O_EXCL, stale after 10 min,
|
|
82
102
|
* long enough to verify a 500k-record chain) makes late starters return "in progress"
|
|
83
103
|
* without touching the log. Opt out with CONTEXTENGINE_AUTO_ROTATE=0.
|
|
104
|
+
* 2026-09-24: the lock is now taken inside rotateAuditLog(), not here, because the manual
|
|
105
|
+
* command bypassed it. [LOCK] [ROTATION-HOLDS-THE-LOCK-BEFORE-IT-PLANS]
|
|
84
106
|
*/
|
|
85
107
|
export declare const AUTO_ROTATE_TRIGGER: number;
|
|
86
108
|
/** Count newline-terminated lines without parsing. The live log is small by construction. */
|
|
@@ -162,8 +184,94 @@ export declare function acknowledgeRedaction(indices: number[], reason: string,
|
|
|
162
184
|
}>;
|
|
163
185
|
record: AuditRecord | null;
|
|
164
186
|
};
|
|
187
|
+
export interface RestorePlan {
|
|
188
|
+
/** Why the block cannot be restored; null when it fits. */
|
|
189
|
+
refusedReason: string | null;
|
|
190
|
+
records: number;
|
|
191
|
+
firstTs: string | null;
|
|
192
|
+
lastTs: string | null;
|
|
193
|
+
/** The segment the block goes right after. */
|
|
194
|
+
after: string | null;
|
|
195
|
+
/** The segment, or "audit.log", whose first record the block reconnects. */
|
|
196
|
+
before: string | null;
|
|
197
|
+
/** The name the restored segment gets. */
|
|
198
|
+
segmentFile: string | null;
|
|
199
|
+
/** History index, before the restore, of the orphan the block closes. */
|
|
200
|
+
orphanIndex: number | null;
|
|
201
|
+
}
|
|
202
|
+
export interface RestoreResult extends RestorePlan {
|
|
203
|
+
restored: boolean;
|
|
204
|
+
record: AuditRecord | null;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* Put a lost block of records back into the archive, from a backup.
|
|
208
|
+
*
|
|
209
|
+
* [LOCKED] [RESTORE-ONLY-CLOSES-A-PROVEN-GAP] - 2026-09-24
|
|
210
|
+
* [NEVER] put records into the archive unless every record hashes correctly, each names the one
|
|
211
|
+
* before it as its parent, the block's first parent is the last record of the segment it
|
|
212
|
+
* follows, the record after the gap names the block's last record as its parent, and none
|
|
213
|
+
* of the block's records is already in the log. Together these admit only the original
|
|
214
|
+
* records: a substitute block would need a SHA-256 preimage.
|
|
215
|
+
* WHY: the archive is evidence. A restore that accepted any block would be a sanctioned way to
|
|
216
|
+
* rewrite history, the one thing the chain exists to make visible. First real use: the
|
|
217
|
+
* 51,174 records erased on 2026-09-15 (see [SEGMENT-IS-NEVER-OVERWRITTEN]), found whole in a
|
|
218
|
+
* Time Machine local snapshot on 2026-09-24.
|
|
219
|
+
* FIX: only a hole the verifier already reports can be filled, and only at a segment boundary.
|
|
220
|
+
* Dry run by default. Apply takes the rotate lock before it plans, never overwrites a file,
|
|
221
|
+
* re-verifies, removes its own segment unless the chain has exactly one orphan fewer and no
|
|
222
|
+
* new damage, and records itself as an `audit.restore` event with a reason.
|
|
223
|
+
*/
|
|
224
|
+
export declare function restoreSegment(file: string, opts?: {
|
|
225
|
+
apply?: boolean;
|
|
226
|
+
reason?: string;
|
|
227
|
+
actor?: string;
|
|
228
|
+
}): RestoreResult;
|
|
229
|
+
export interface ScrubFileReport {
|
|
230
|
+
name: string;
|
|
231
|
+
records: number;
|
|
232
|
+
redacted: number;
|
|
233
|
+
}
|
|
234
|
+
export interface ScrubReport {
|
|
235
|
+
applied: boolean;
|
|
236
|
+
refusedReason: string | null;
|
|
237
|
+
files: ScrubFileReport[];
|
|
238
|
+
redactedRecords: number;
|
|
239
|
+
counts: Record<string, number>;
|
|
240
|
+
/** The audit.redact records appended to acknowledge the rewrite. */
|
|
241
|
+
acknowledgements: AuditRecord[];
|
|
242
|
+
}
|
|
243
|
+
type Redactor = (payload: Record<string, unknown>) => {
|
|
244
|
+
value: Record<string, unknown>;
|
|
245
|
+
counts: Record<string, number>;
|
|
246
|
+
changed: boolean;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* Remove credentials from records already written, and acknowledge each rewrite on the chain.
|
|
250
|
+
*
|
|
251
|
+
* [LOCKED] [SCRUB-IS-ACKNOWLEDGED-REDACTION] - 2026-09-25
|
|
252
|
+
* [NEVER] rewrite a record's content without appending the audit.redact acknowledgement that
|
|
253
|
+
* binds its original hash to its new content, and never touch a record that is not a
|
|
254
|
+
* capture record or a line the redactor left unchanged.
|
|
255
|
+
* WHY: the credentials found on 2026-09-24 sat in 45 archived segments and the live log. Segments
|
|
256
|
+
* are never overwritten ([SEGMENT-IS-NEVER-OVERWRITTEN]); this is the one sanctioned
|
|
257
|
+
* exception, because leaving a live Stripe key or a database password in an evidence
|
|
258
|
+
* archive forever is worse than an acknowledged edit. Without the acknowledgement the
|
|
259
|
+
* verifier would, truthfully, call every scrubbed record altered.
|
|
260
|
+
* FIX: dry run by default. With apply: under the rotate lock (no rotation moves records while we
|
|
261
|
+
* rewrite), each segment is rewritten via temp file and rename only where a line changed,
|
|
262
|
+
* the live log under the append lock (appends wait, none is lost), then one audit.redact
|
|
263
|
+
* record per 100 rewrites names each original hash and its new content hash
|
|
264
|
+
* ([REDACTION-IS-A-CHAINED-RECORD]). Running it again changes nothing.
|
|
265
|
+
*/
|
|
266
|
+
export declare function scrubAuditLog(opts: {
|
|
267
|
+
apply?: boolean;
|
|
268
|
+
reason?: string;
|
|
269
|
+
redact: Redactor;
|
|
270
|
+
actor?: string;
|
|
271
|
+
}): ScrubReport;
|
|
165
272
|
export declare function filterByRange(records: AuditRecord[], since?: string, until?: string): AuditRecord[];
|
|
166
273
|
export declare function toCsv(records: AuditRecord[]): string;
|
|
167
274
|
export declare function resetCacheForTest(): void;
|
|
168
275
|
export declare function safeAppend(event: AuditEvent, payload: Record<string, unknown>, actor?: string): void;
|
|
276
|
+
export {};
|
|
169
277
|
//# sourceMappingURL=audit.d.ts.map
|