@tuent/sentinel 0.1.0 → 0.1.2

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/README.md CHANGED
@@ -1,19 +1,24 @@
1
1
  # @tuent/sentinel
2
2
 
3
- Runtime security for Claude Code. Sentinel evaluates a policy on every Claude Code tool call — before it runs — and records each decision to a hash-chained, signed audit trail.
3
+ **Runtime security for Claude Code.** Sentinel checks your policy on every Claude Code tool call — _before_ it runs — and writes every decision to a signed, tamper-evident audit trail. Install it, point it at your project, and Claude Code operates inside guardrails you control.
4
4
 
5
- It is deliberately narrow. This is not a general-purpose "agent security platform"; it is a Claude-Code-native enforcement layer that hooks Claude Code's own tool-call lifecycle.
5
+ Sentinel is purpose-built for Claude Code. Rather than loosely wrapping a general-purpose "agent platform," it hooks Claude Code's own tool-call lifecycle directly — enforcement happens at the exact point where the agent decides to act.
6
+
7
+ ## What you get
8
+
9
+ - **Enforcement before execution** — every tool call is evaluated against your policy and allowed or denied before it runs, not flagged after the fact.
10
+ - **A signed audit trail** — every decision appended to a hash-chained, Ed25519-signed trail anchored by a signed manifest, verifiable end to end.
11
+ - **Automatic escalation** — repeated violations move the agent normal → restricted → quarantined at thresholds you set, and one command restores it.
12
+ - **Behavioral baseline** — a per-workspace baseline surfaces deviation signals as advisory context.
6
13
 
7
14
  ## How it works
8
15
 
9
- `init` installs a hook into Claude Code's PreToolUse lifecycle. Each tool call is routed to a local gateway daemon, evaluated against your policy, and allowed or denied before execution. Every decision is appended to a signed, hash-chained audit trail.
16
+ `init` installs a hook into Claude Code's PreToolUse lifecycle. Each tool call is routed to a local gateway daemon, evaluated against your policy, and allowed or denied before execution then recorded to the signed trail.
10
17
 
11
18
  ```
12
19
  Claude Code tool call → PreToolUse hook → gateway daemon → policy decision → signed audit
13
20
  ```
14
21
 
15
- Enforcement is cooperative: it depends on Claude Code invoking the hook. Sentinel is not a sandbox and does not contain a hostile agent that bypasses the hook.
16
-
17
22
  ## Install
18
23
 
19
24
  ```sh
@@ -21,9 +26,7 @@ npm install @tuent/sentinel
21
26
  npx sentinel init claude-code
22
27
  ```
23
28
 
24
- `init` writes a `.sentinel.yaml` policy into your project, sets up the gateway hook, and merges a hook entry into `.claude/settings.local.json`. No `tsx` or build step is required — the gateway ships as a runnable daemon.
25
-
26
- Requires Node.js ≥ 20. ESM-only.
29
+ `init` writes a `.sentinel.yaml` policy into your project, sets up the gateway hook, and merges a hook entry into `.claude/settings.local.json`. No build step — the gateway ships as a runnable daemon. Requires Node.js ≥ 20 (ESM-only).
27
30
 
28
31
  ## Policy
29
32
 
@@ -60,36 +63,33 @@ enforcement:
60
63
  quarantineAfter: 5 # quarantine after this many
61
64
  ```
62
65
 
66
+ `forbid.targets` is a hard deny — a match blocks the action. `allow.targets` is **advisory for file and tool actions**: a read or write outside it is _logged_ as a `scope_violation` but still runs, because Sentinel governs the agent's own tool calls rather than sandboxing the filesystem. The exception is `networkHosts` — a `network_request` to a host not on the list is **denied** by default. That host allowlist governs **tool-level network requests** (WebFetch/WebSearch); it does not contain network access made from _inside_ a Bash command (`curl`, `wget`, etc.) — Bash commands are checked for forbidden file paths, not egress destinations.
67
+
68
+ Tool names Sentinel doesn't recognize — for example, tools added in a Claude Code release newer than your Sentinel build — are **allowed and logged** by default (`enforcement.unknownTools: warn`; every unknown call is recorded as an `unknown_tool` audit finding). Set `enforcement.unknownTools: deny` to block them instead, with `enforcement.allowUnknownTools: [<names>]` as the per-name escape hatch for legitimate new tools.
69
+
63
70
  As policy violations accumulate, the agent escalates through modes: normal → restricted → quarantined (at the `restrictAfter` / `quarantineAfter` thresholds). Release a restricted or quarantined agent through the API (`sentinel.release(...)`), which records the change as a signed entry in the audit trail. Editing the mode state file by hand changes the live state without recording it — leaving the trail and the actual state out of sync.
64
71
 
65
72
  ## Audit trail
66
73
 
67
- Every decision is appended to a hash-chained trail and signed with Ed25519, anchored by a signed manifest. A verify check validates the hash chain and the entry signatures; the signed manifest anchors the chain.
74
+ Every decision is appended to a hash-chained trail and signed with Ed25519, anchored by a signed manifest. Run `sentinel --verify-audit` to validate the chain and every entry signature.
68
75
 
69
- Scope and limits — please read:
76
+ ## Behavioral analytics
70
77
 
71
- - The trail is designed for a single writer (the gateway daemon). There is no inter-process write lock. Running concurrent writer processes against one trail can fork the chain, so a verify-audit failure can indicate benign concurrency rather than tampering.
72
- - Signature determinism currently relies on V8's JSON key ordering. Verifying on another engine (Bun, Deno) is not yet supported and may report false invalids.
73
- - The trail is stored in plaintext. It is tamper-evident, not tamper-proof: a compromised same-host process can alter it, and the signed manifest is designed to detect that, not prevent it.
78
+ Sentinel maintains a per-workspace behavioral baseline and surfaces session-level deviation signals as advisory context. These are observational and do not block tool calls; the richer signals require a matured workspace baseline.
74
79
 
75
- ## Behavioral analytics
80
+ ## If a tool call is unexpectedly blocked
76
81
 
77
- Sentinel maintains a per-workspace behavioral baseline and surfaces session-level, advisory deviation signals. These are observational; they do not block tool calls. Activity-absence can surface once a baseline exists; temporal, access-pattern, and the remaining deviation signals require a matured workspace baseline. None fire on a fresh install.
82
+ Sentinel matches forbidden targets conservatively, which can occasionally deny a benign command that only _references_ a sensitive filename for example, searching your code with `grep` for `.env`. Plain mentions under safe commands (`echo`, comments) pass through. If a false positive restricts or quarantines the agent, restore it in one step:
78
83
 
79
- ## What Sentinel does not do
84
+ ```sh
85
+ sentinel release
86
+ ```
80
87
 
81
- - It does not defend against prompt injection of the agent itself.
82
- - In log-adapter mode it does not block in real time (it observes, with latency).
83
- - It does not prevent log tampering by a compromised same-host process.
84
- - It does not detect encrypted or obfuscated exfiltration.
85
- - It does not defend against multi-agent coordinated activity.
86
- - It does not resolve symlinks in target paths.
87
- - Workspace identity is a non-cryptographic 32-bit hash. It distinguishes local workspaces; it is not collision-resistant.
88
+ This records the change in the audit trail. Don't edit the mode state file by hand — that desyncs the trail from the live state.
88
89
 
89
- ## Notes
90
+ ## Security model
90
91
 
91
- - Target and content matching is conservative and can produce false positives for example, a literal token such as `process.env` can match a `.env` forbid pattern. Tune your policy accordingly.
92
- - On a cold start, the first tool call of a fresh session waits up to ~5 seconds for the daemon to warm up. If it is not ready, Sentinel applies its tiered fallback: high-sensitivity tools are denied, lower-sensitivity tools are allowed through.
92
+ Sentinel's enforcement is **cooperative** — it works by intercepting Claude Code's tool-call hook and the audit trail is **tamper-evident**, not tamper-proof. Today Sentinel runs on the same host as the agent; running it off-host is the hardened posture on the roadmap. For the full threat model — what Sentinel defends against, what's out of scope by design, and current limitations — see [SECURITY_MODEL.md](./SECURITY_MODEL.md).
93
93
 
94
94
  ## License
95
95
 
@@ -0,0 +1,281 @@
1
+ # Sentinel Security Model
2
+
3
+ Sentinel's enforcement is **cooperative, not a sandbox**: it governs the agent's own tool calls at the hook layer — it does not intercept syscalls, contain the filesystem, or firewall the network. The audit trail is **tamper-evident, not tamper-proof**. Sentinel currently runs on the same host as the agent; the off-host posture described under "Trust Model" is the hardened, recommended direction. Read every claim below with those three frames in mind.
4
+
5
+ ## What Sentinel Protects Against
6
+
7
+ ### Pre-Execution Enforcement
8
+
9
+ When using `wrap()` or `wrapTool()`, Sentinel validates every action before the agent executes it. HIGH and CRITICAL severity actions are blocked -- the agent's execution function never runs. The dangerous file is never read, the unauthorized API is never called, the forbidden command is never executed.
10
+
11
+ This is the strongest integration mode. The agent code passes its intended action and an execute function to Sentinel. If the action violates the role definition or targets a high-sensitivity resource, Sentinel returns `{ blocked: true }` and the execute function is never invoked. LOW and MEDIUM findings are informational -- the action still executes, and the finding is returned alongside the result for logging. (One exception in the blocking direction: a MEDIUM out-of-scope `network_request` is denied by default -- see "Unauthorized Target Access" below.)
12
+
13
+ ### Role Violations
14
+
15
+ Agents are bound to a defined set of allowed actions. If an agent with `allowedActions: ["file_read", "file_write"]` attempts a `command_exec` or `database_query`, Sentinel produces a **HIGH** `role_violation` finding immediately.
16
+
17
+ ### Unauthorized Target Access
18
+
19
+ Targets are checked against glob patterns in two layers:
20
+
21
+ 1. **Forbidden patterns** (checked first) -- any match produces a **HIGH** `unauthorized_target`
22
+ 2. **Allowed patterns** (checked second) -- access outside allowed scope produces a **MEDIUM** `scope_violation`
23
+
24
+ A `scope_violation` is **advisory by default, not a block**: an out-of-scope `file_read`, `file_write`, or `tool_invocation` is **logged and still runs** (`DEFAULT_MEDIUM_DISPOSITION` leaves these actions at `allow`). The exception is `network_request` -- an out-of-scope or unlisted host is **denied** by default. So the allowlist _enforces_ egress **for `network_request` actions** (the WebFetch/WebSearch tool surface) but only _annotates_ out-of-scope file and tool activity: forbidden-pattern hits (HIGH) always block; allowed-scope misses (MEDIUM) do not, except for network. The host allowlist does **not** govern network access made from inside a Bash command — see "Bash-Mediated Network Egress" under the limitations below. Sentinel governs the agent's own tool calls; it is not a filesystem sandbox.
25
+
26
+ Pattern matching uses `**` (any path) and `*` (single segment). Examples:
27
+
28
+ - `**/.env` matches `.env` at any depth
29
+ - `**/.ssh/**` matches anything inside any `.ssh` directory
30
+ - `src/**` matches all files under `src/`
31
+
32
+ ### Behavioral Anomalies
33
+
34
+ After a baseline is computed from historical sessions, the DeviationDetector checks every new session for:
35
+
36
+ - **Volume spikes** -- event count exceeds 3x the baseline average
37
+ - **Unusual targets** -- more than 50% of accessed files are outside the agent's typical target set
38
+ - **Weight anomaly** -- session engagement exceeds baseline normal range + 0.3 threshold
39
+ - **Category shifts** -- activity type represents less than 5% of baseline behavior
40
+ - **Activity drops** -- session event count drops 75%+ below baseline average (possible log suppression)
41
+ - **Activity gaps** -- agent silence exceeds 2-3x the longest observed gap in the baseline period
42
+ - **Convergence escalation** -- when 3+ MEDIUM findings occur in one session, the first is escalated to HIGH
43
+
44
+ ### High-Sensitivity Target Access
45
+
46
+ The TargetSensitivityScorer assigns risk scores (0.0-1.0) to every target based on built-in pattern rules. Scores are multiplied by action type (writes are 1.3x, command execution is 1.5x). When the effective score reaches 0.9+, findings are escalated to **CRITICAL**.
47
+
48
+ Built-in sensitivity categories:
49
+
50
+ - **Credentials** (1.0): `.ssh/`, `.env`, `.aws/`, `secrets/`, `id_rsa*`, `.pem`, `shadow`
51
+ - **System** (0.7-0.9): `/etc/`, `passwd`
52
+ - **PII** (0.8-0.85): `users/`, `customers/`, `payments/`
53
+ - **Database** (0.75-0.8): `database/`, `.sqlite`, `.db`
54
+ - **Config** (0.5-0.75): `config/`, `config/production*`
55
+ - **Source** (0.1-0.45): `src/`, `tests/`, `docs/`, `.git/`
56
+ - **Default** (0.15): anything not matching a specific rule
57
+
58
+ Custom rules can be injected via the `TargetSensitivityScorer` constructor, overriding built-in patterns.
59
+
60
+ ### Temporal Anomalies
61
+
62
+ Agents can have an `expectedSchedule` with allowed days and hours (UTC). Activity outside the schedule produces a **MEDIUM** `temporal_anomaly`. The DeviationDetector independently checks against the baseline's typical active hours and days.
63
+
64
+ ### Path Traversal Attempts
65
+
66
+ All target paths are normalized with `path.normalize()` before pattern matching. This resolves `../` and `./` sequences, preventing bypass attempts like:
67
+
68
+ - `src/../.env` is normalized to `.env` before checking against `**/.env`
69
+ - `project/subdir/../../.ssh/id_rsa` is normalized to `.ssh/id_rsa`
70
+
71
+ ### Self-Protection: Policy and Hook-Wiring Files
72
+
73
+ The agent's own tool-writes to the live policy file (`.sentinel.yaml`) and to Claude Code's hook-wiring settings files (`.claude/settings.json` and `.claude/settings.local.json`, project- and user-level) are **denied by default**. Without this, an agent could rewrite its own policy or remove the hook entries that route its tool calls through the gateway with an ordinary file write. The corresponding forbid patterns ship in the defaults and the starter policy; the read carve-out is appended **code-side at ceiling-role construction** and is deliberately not authorable from the workspace yaml (workspace-authored exceptions are dropped by the ceiling merge, so a policy file can never widen its own protection away).
74
+
75
+ **Defaults are a floor, not a fallback.** The built-in forbidden patterns are unioned into every role at role construction, regardless of what the policy file supplies. A policy file can extend the built-in list but cannot remove entries from it — omitting a built-in pattern from a yaml's `forbid.targets` does not disable it. This is deliberate: a stale policy file (written before newer built-in protections existed) or a hostile one cannot weaken built-in protection by superseding the list. Legitimately relaxing a built-in pattern goes through the operator-ceiling policy's `exceptions` mechanism, which is an explicit, auditable act — never through omission.
76
+
77
+ Honest boundaries of this protection:
78
+
79
+ - **Reads are allowed via the Read tool only.** Bash-mediated reads that name these files (`cat`, `grep`, redirection sources) are denied — the gateway's bash layers have no exception plumbing. The agent inspects its policy and hook wiring with the Read tool.
80
+ - **Plain bash writes to the policy/settings files are denied, but encoding-obfuscated bash writes are not decoded for these patterns.** Their basenames are deliberately excluded from the content-scan list to avoid false positives on source files that mention them. This is an accepted limitation.
81
+ - **Operators edit these files directly.** Agent tool-writes are denied by design; a human editing `.sentinel.yaml` or the settings files in an editor or separate shell is outside the agent tool-call surface and unaffected.
82
+ - **`~/.claude.json` is not part of Claude Code's honored-hooks surface** (it holds legacy global state, not hook configuration) and is therefore not on the forbid list.
83
+ - **Managed settings and launch-flag settings files are admin surface**, controlled outside the agent's tool-call path, and out of tool-write scope for this protection.
84
+
85
+ ### Unknown Tool Names
86
+
87
+ Tool names outside Sentinel's recognized native set (and not MCP-shaped — `mcp__*` names have their own arg-scanning translation path) cannot be evaluated by name-aware policy checks. The gateway's disposition for them is the `enforcement.unknownTools` knob in the operator launch policy:
88
+
89
+ - **`warn` (shipped default):** the call is allowed and a persisted, observational `unknown_tool` finding is written to the audit trail — no unknown tool passes unlogged. This is the default because the recognized set is inventoried from live sessions and cannot be proven complete against capability-gated or newer Claude Code tools; deny-by-default on an unverifiable list risks breaking legitimate cc features. The default is intended to flip to deny once warn-mode telemetry reconciles the inventory against live traffic.
90
+ - **`deny` (hardened opt-in):** the call is blocked with a HIGH `unknown_tool` finding. The deny message names the blocked tool and the escape hatch: `enforcement.allowUnknownTools` (a list of names to translate as known) plus a daemon restart — so a newly shipped cc tool can be unbricked with a one-line policy edit, without waiting on a Sentinel release.
91
+
92
+ `unknown_tool` findings are deliberately **not escalation-eligible**: the dominant real-world cause is a recognized set gone stale after a Claude Code update, not agent misbehavior, and counting them would turn every cc update into a restriction ratchet.
93
+
94
+ **Gateway-down behavior is deliberately stricter:** the fail-closed hook hard-blocks unknown names regardless of the knob, because the hook cannot persist findings while the daemon is down — a warn-equivalent there would be allow-_unlogged_, which is exactly the gap this protection closes. `allowUnknownTools` names are baked into the hook at init so the operator escape hatch survives gateway-down; re-run init after editing the list.
95
+
96
+ ---
97
+
98
+ ## What Sentinel Does NOT Protect Against
99
+
100
+ ### Prompt Injection
101
+
102
+ Sentinel monitors agent **actions**, not agent **prompts**. It cannot detect or prevent prompt injection attacks directly. However, if a prompt injection causes an agent to perform a forbidden action (e.g., reading `.env`), that action **will** be caught by role validation.
103
+
104
+ ### Real-Time Blocking in Log Adapter Mode
105
+
106
+ When using the LogAdapter (file-tailing mode), events are detected after they occur with a polling delay (default ~2 seconds). This mode is observation-only -- it cannot block actions.
107
+
108
+ For real-time pre-execution enforcement, use one of:
109
+
110
+ - **`sentinel.wrap()` / `sentinel.wrapTool()`** (recommended) -- synchronous enforcement, actions blocked before execution
111
+ - **`sentinel.check()`** -- manual pre-execution gate in your agent orchestrator
112
+ - **WebhookReceiver** in synchronous mode (agent sends event before executing)
113
+
114
+ ### Log Tampering by Compromised Agents
115
+
116
+ If an AI agent and Sentinel run on the same host and the agent has write access to the filesystem, a compromised agent could theoretically modify its own log files or audit trail. **Mitigation:** Run Sentinel on a separate host and ingest events via the webhook adapter.
117
+
118
+ ### Bash-Mediated Network Egress
119
+
120
+ The `networkHosts` allowlist is enforced for `network_request` actions — the WebFetch/WebSearch tool surface. It does **not** contain network access initiated from inside a Bash command: `curl`, `wget`, a script that opens a socket, or a package manager's network activity all run under `command_exec`, where Sentinel's bash layers check the command for **forbidden file paths and basenames**, not egress destinations. There is no comprehensive network containment; treat the host allowlist as a control on the agent's tool-level fetches, not a firewall.
121
+
122
+ ### Encrypted or Obfuscated Data Exfiltration
123
+
124
+ Sentinel sees which targets an agent accesses and what actions it performs. It does not inspect the content of data being read or transmitted. An agent reading a sensitive file and encoding its contents into an innocuous-looking API call would be partially detected (the file read is logged) but the exfiltration channel may not be flagged.
125
+
126
+ ### Self-Hosting Development Boundary
127
+
128
+ In a repository that contains Sentinel's own source code, the agent can still edit the enforcement code itself (and the running daemon picks the change up on its next restart). The policy/hook-wiring write-deny does not — and cannot — close that vector; it is inherent to developing Sentinel with the agent it monitors. Treat enforcement-source changes in such a repo as requiring the same human review as any security-sensitive code.
129
+
130
+ ### Multi-Agent Coordinated Attacks
131
+
132
+ Each agent is monitored independently by default. The `CorrelationDetector` provides basic cross-agent pattern detection (e.g., read-then-exfiltrate chains), but sophisticated coordinated attacks where each agent stays within its own role may evade detection.
133
+
134
+ ### Symlink and Hardlink Resolution Limits
135
+
136
+ When Sentinel runs on the same host as the agent (the Claude Code gateway integration), validation does resolve links before matching: targets are passed through `realpath` (so a symlink named `src/safe-link` pointing at `/etc/shadow` is checked as `/etc/shadow` and hits the `/etc/**` forbid), and a forbidden-inode comparison catches **hardlinks** to forbidden files that pattern matching cannot see. The honest limits:
137
+
138
+ - **Off-host modes resolve nothing.** The WebhookReceiver / LogAdapter paths operate on path strings without filesystem access to the agent host — there, link-based bypasses are not detected.
139
+ - **Time-of-check vs time-of-use.** A link can be re-pointed between Sentinel's pre-execution check and the tool actually running. This window is inherent to the cooperative model.
140
+ - **Resolution requires the path to exist** at check time; a dangling link that is created after the check is not re-evaluated.
141
+
142
+ ### Concurrent Audit-Trail Writers (No Inter-Process Lock)
143
+
144
+ The audit trail is designed for a **single writer** -- the gateway daemon. There is no inter-process write lock. Running concurrent writer processes against one trail can fork the hash chain, so a `verify-audit` failure can indicate benign concurrency rather than tampering.
145
+
146
+ ### Cross-Engine Signature Verification
147
+
148
+ Ed25519 signature determinism currently relies on **V8's JSON key ordering** when serializing the signed payload. Verifying on another JavaScript engine (Bun, Deno) is not supported and may report false invalids.
149
+
150
+ ### Workspace Identity Collisions
151
+
152
+ Workspace identity is a **32-bit FNV-1a hash** of the workspace root. It is non-cryptographic and not collision-resistant; it distinguishes local workspaces but is not a security boundary.
153
+
154
+ ### Conservative Matching / False Positives
155
+
156
+ Target matching is deliberately conservative: a command that only _references_ a forbidden filename — for instance, a `grep` whose pattern or path is `.env` — can be denied even though it never opens the file. Because each denial counts toward the escalation ladder, a run of such false positives can move the agent to `restricted` or `quarantined`. Plain mentions under safe verbs (`echo`, `printf`) and in comments are not flagged. When a false positive does restrict or quarantine the agent, recover with `sentinel release`, which records the change in the audit trail. Tune policies to widen the allow set where this proves noisy.
157
+
158
+ ### Cold-Start Window
159
+
160
+ On a cold start, the first tool call of a fresh session waits up to **~5 seconds** for the gateway daemon to warm up. If the daemon is not ready, the hook applies its **tiered fallback** rather than failing closed uniformly: high-sensitivity tools (e.g. `Bash`, `Write`, `Edit`, `WebFetch`) are **denied**, while lower-sensitivity tools (e.g. `Read`, `Glob`, `Grep`, `WebSearch`) are **allowed** through. MCP tools default to the high (deny) tier; **unrecognized tool names are always high-tier** — floored, not configurable down (allowing them unlogged while the gateway is down would reopen the unknown-tool gap), with the operator's `allowUnknownTools` names (baked into the hook at init) as the only pass-through.
161
+
162
+ ---
163
+
164
+ ## Trust Model
165
+
166
+ ### Recommended Production Deployment
167
+
168
+ ```
169
+ +------------------+ HTTPS/webhook +-------------------+
170
+ | AI Agent Host | -----------------------------> | Sentinel Host |
171
+ | | POST /api/sentinel/events | |
172
+ | Agent runtime | | WebhookReceiver |
173
+ | sends events | | RoleValidator |
174
+ | before/after | | AuditTrail |
175
+ | each action | | AlertManager |
176
+ +------------------+ +-------------------+
177
+ ```
178
+
179
+ - Sentinel runs on a separate host the agent cannot access
180
+ - Events are pushed via authenticated webhook (API key in Authorization header)
181
+ - Audit trail is on the Sentinel host, inaccessible to the agent
182
+ - Alerts dispatch to external systems (Slack webhook, PagerDuty, etc.)
183
+
184
+ The off-host posture above is the **hardened, recommended direction**. Note that the **Claude Code gateway integration currently runs same-host** — the gateway daemon, policy file, and audit trail live on the machine the agent runs on, with the self-protection forbids (policy/hook-wiring write-denies, state-dir forbid) as the same-host mitigations. Moving the cc gateway's enforcement core off-host is the roadmap re-architecture; until then, treat same-host deployment with the honesty the "Log Tampering" section above implies.
185
+
186
+ ### Development Deployment
187
+
188
+ ```
189
+ +----------------------------------------------+
190
+ | Same Host |
191
+ | |
192
+ | Agent runtime ---> activity.log |
193
+ | | |
194
+ | LogAdapter (polls) |
195
+ | | |
196
+ | SentinelRunner |
197
+ | AuditTrail |
198
+ +----------------------------------------------+
199
+ ```
200
+
201
+ - Sentinel watches the agent's log file via LogAdapter
202
+ - Simpler setup but agent could tamper with its own logs
203
+ - Suitable for development and testing
204
+
205
+ ### Audit Trail Integrity
206
+
207
+ - Append-only: entries are only written, never modified or deleted programmatically
208
+ - Per-agent: each agent has its own `audit.log` file
209
+ - Rotation: files rotate at 10 MB, keeping 3 archives (audit.log.1, .2, .3)
210
+ - All entries include timestamps and are queryable by time range and severity
211
+
212
+ ### Data Storage
213
+
214
+ - All data is local to the Sentinel host filesystem
215
+ - No cloud services, no external databases, no network dependencies
216
+ - Storage location is configurable via `agentsDir` constructor parameter
217
+ - Default: `~/.dahlia/agents/`
218
+
219
+ ---
220
+
221
+ ## Severity Levels
222
+
223
+ | Severity | Meaning | Triggers Alert? | Response |
224
+ | ------------ | ------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
225
+ | **LOW** | Notable but expected variation | No (unless configured) | Log for review. Weight anomalies, off-day activity. |
226
+ | **MEDIUM** | Warrants monitoring | Configurable | Single deviation signal. Volume spike, unusual targets, category shift, off-hours. Escalates to HIGH when 3+ converge in one session. |
227
+ | **HIGH** | Requires investigation | Yes | Role violation, forbidden target access, escalated convergence. |
228
+ | **CRITICAL** | Immediate response required | Yes | Target sensitivity effective score >= 0.9. Credential access, system file writes. |
229
+
230
+ ### Escalation Rules
231
+
232
+ - MEDIUM findings escalate to HIGH when 3+ MEDIUM findings occur in a single session (convergence escalation)
233
+ - HIGH findings are escalated to CRITICAL by the TargetSensitivityScorer when the effective score (sensitivity x action multiplier) reaches 0.9+
234
+
235
+ ---
236
+
237
+ ## Detection Capabilities
238
+
239
+ | Threat | Detection Method | Component | Expected Severity |
240
+ | --------------------------------------------- | -------------------------------------------- | -------------------------- | ----------------------------------------------------------------- |
241
+ | SSH key access (`~/.ssh/id_rsa`) | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
242
+ | `.env` file read | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
243
+ | AWS credential access (`~/.aws/credentials`) | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
244
+ | Kubernetes config access (`~/.kube/config`) | Target sensitivity (0.95) | RoleValidator + Scorer | CRITICAL |
245
+ | Write to `~/.ssh/authorized_keys` (backdoor) | Target sensitivity (1.0 x 1.3 write) | RoleValidator + Scorer | CRITICAL |
246
+ | `/etc/passwd` read | Target sensitivity (0.9) + forbidden pattern | RoleValidator + Scorer | CRITICAL |
247
+ | System config write (`/etc/nginx/nginx.conf`) | Target sensitivity (0.7 x 1.3 write = 0.91) | RoleValidator + Scorer | CRITICAL |
248
+ | Payment data access (`payments/`) | Target sensitivity (0.85) | DeviationDetector + Scorer | HIGH |
249
+ | Database query by file-only agent | Action not in allowedActions | RoleValidator | HIGH |
250
+ | Shell command by code-writer agent | Action not in allowedActions | RoleValidator | HIGH |
251
+ | 3x baseline event volume | Event count exceeds threshold | DeviationDetector | MEDIUM (escalates to HIGH with other signals) |
252
+ | >50% novel target files in session | Target set comparison | DeviationDetector | MEDIUM |
253
+ | Activity outside expected hours | Schedule check (UTC) | RoleValidator | MEDIUM |
254
+ | Weekend activity when weekday-only | Schedule check (UTC) | RoleValidator | MEDIUM |
255
+ | Path traversal (`src/../.env`) | Path normalization + pattern match | RoleValidator | HIGH/CRITICAL (depends on target) |
256
+ | Category shift (rare action type) | Baseline action distribution | DeviationDetector | MEDIUM |
257
+ | Access outside allowed scope | Allowed target pattern miss | RoleValidator | MEDIUM — advisory (logged, not blocked); `network_request` denied |
258
+ | Multiple converging anomalies (3+) | Finding count in session | DeviationDetector | MEDIUM escalated to HIGH |
259
+ | Activity drop (75%+ fewer events) | Event count vs baseline average | DeviationDetector | MEDIUM (HIGH at 90%+ drop) |
260
+ | Extended agent silence (2-3x max gap) | Time since last event vs baseline gaps | DeviationDetector | LOW (MEDIUM at 3x+) |
261
+
262
+ ---
263
+
264
+ ## Finding Types
265
+
266
+ | Type | Source | Description |
267
+ | --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
268
+ | `role_violation` | RoleValidator | Agent performed an action not in its `allowedActions` list |
269
+ | `unauthorized_target` | RoleValidator, DeviationDetector | Agent accessed a target matching `forbiddenTargetPatterns` or scoring high on sensitivity |
270
+ | `scope_violation` | RoleValidator | Agent accessed a target outside its `allowedTargetPatterns` |
271
+ | `temporal_anomaly` | RoleValidator, DeviationDetector | Activity outside expected schedule or baseline typical hours/days |
272
+ | `volume_spike` | DeviationDetector | Session event count exceeds baseline average by 3x+ |
273
+ | `access_pattern` | DeviationDetector | Unusual target distribution, weight anomaly, or category shift |
274
+ | `behavioral_absence` | DeviationDetector | Significant activity drop (75%+ below baseline) or extended silence (2-3x max gap) |
275
+ | `intent_drift` | Similarity engine (active task) | Action poorly aligned with the declared active task — informational, never blocks |
276
+ | `bash_analysis` | Gateway L3 | Dangerous construct / unparseable command without forbidden indicators — informational |
277
+ | `hook_block` | HookEngine | A registered pre-execution hook vetoed the action |
278
+ | `workspace_mismatch` | Gateway routing | Request with no/unresolvable workspace — fail-closed routing refusal; not escalation-eligible |
279
+ | `agent_restricted` | Enforcement ladder | Action denied because the agent is in restricted mode |
280
+ | `agent_quarantined` | Enforcement ladder | Action denied because the agent is quarantined |
281
+ | `unknown_tool` | Gateway unknown-tool consumer | Tool name outside the recognized set — logged-allow by default (`warn`), deny opt-in; not escalation-eligible |
@@ -0,0 +1,10 @@
1
+ import {
2
+ Sentinel
3
+ } from "./chunk-GRN5P3H2.js";
4
+ import "./chunk-QIYQWOLO.js";
5
+ import "./chunk-WLIDSTS4.js";
6
+ import "./chunk-NUXSUSYY.js";
7
+ export {
8
+ Sentinel
9
+ };
10
+ //# sourceMappingURL=Sentinel-XMSJE4DZ.js.map
@@ -259,8 +259,13 @@ interface SecurityFinding {
259
259
  * routing error, not that agent's misbehavior. Same non-eligible posture as
260
260
  * `bash_analysis`. Escalation-ineligibility is a consequence of what the type
261
261
  * means, not a knob — see Sentinel.ESCALATION_ELIGIBLE_TYPES.
262
+ *
263
+ * `unknown_tool` (Sprint 26 Gate-A Item D / F-8) is likewise non-eligible:
264
+ * its dominant real-world cause is a recognized-tool set gone stale after a
265
+ * Claude Code update, not agent misbehavior — counting it would turn every
266
+ * cc update into a restriction ratchet.
262
267
  */
263
- type: "scope_violation" | "temporal_anomaly" | "access_pattern" | "volume_spike" | "unauthorized_target" | "role_violation" | "behavioral_absence" | "agent_quarantined" | "agent_restricted" | "intent_drift" | "hook_block" | "bash_analysis" | "workspace_mismatch";
268
+ type: "scope_violation" | "temporal_anomaly" | "access_pattern" | "volume_spike" | "unauthorized_target" | "role_violation" | "behavioral_absence" | "agent_quarantined" | "agent_restricted" | "intent_drift" | "hook_block" | "bash_analysis" | "workspace_mismatch" | "unknown_tool";
264
269
  agentId: string;
265
270
  agentName: string;
266
271
  description: string;
@@ -278,6 +283,22 @@ interface SecurityFinding {
278
283
  args: string[];
279
284
  };
280
285
  softSignal?: boolean;
286
+ /**
287
+ * True when the forbidden token was a confident command-string MENTION (a
288
+ * proper substring of an argv token, no L1 path-glob hit, no ambiguity) rather
289
+ * than a forbidden-file access. Excluded from getEffectiveBlockCount. Absent /
290
+ * false = counted (today's behavior). Strict-safe: a real file open resolves to
291
+ * an L1 path hit, so it can never be mentionOnly.
292
+ */
293
+ mentionOnly?: boolean;
294
+ /**
295
+ * Distinct-target dedup key for getEffectiveBlockCount — the COMMAND identity
296
+ * (event.primaryTarget). Distinct real accesses, including same-basename-family
297
+ * files (`cat .env` vs `cat .env.local`), get distinct keys and count
298
+ * separately; a true repeat of the same command dedups to one. Absent = the
299
+ * finding is its own distinct entry (never merged).
300
+ */
301
+ dedupKey?: string;
281
302
  }
282
303
  /** Computed behavioral baseline for an agent over a time window. */
283
304
  /**
@@ -1778,6 +1799,15 @@ declare class Sentinel {
1778
1799
  * Sprint 16 Prompt 3 — replaces in-memory blockCounts Map to survive
1779
1800
  * gateway restarts. Same pattern as Sprint 11 sessionCount Shape D fix.
1780
1801
  */
1802
+ /**
1803
+ * Shared escalation-eligibility predicate (Sprint 26 F-5). Single source of
1804
+ * truth for "this finding counts toward the block ladder", extracted from the
1805
+ * formerly-inline copies in getEffectiveBlockCount and maybeEscalate — behavior
1806
+ * of both is identical to before the extraction. Accepts either a
1807
+ * SecurityFinding-shaped object ({type}) or an audit entry ({findingType},
1808
+ * mapped by the caller).
1809
+ */
1810
+ private static isEscalationEligible;
1781
1811
  private getEffectiveBlockCount;
1782
1812
  private maybeEscalate;
1783
1813
  }
@@ -0,0 +1,32 @@
1
+ // src/workspaceIdentity.ts
2
+ var AGENT_PREFIX = "claude-code";
3
+ function fnv1a32Hex(s) {
4
+ let h = 2166136261;
5
+ for (let i = 0; i < s.length; i++) {
6
+ h ^= s.charCodeAt(i);
7
+ h = Math.imul(h, 16777619);
8
+ }
9
+ return (h >>> 0).toString(16).padStart(8, "0");
10
+ }
11
+ function lastSegment(path) {
12
+ const parts = path.split("/").filter(Boolean);
13
+ return parts.length > 0 ? parts[parts.length - 1] : "";
14
+ }
15
+ function slugify(s) {
16
+ return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
17
+ }
18
+ function normalizeRoot(root) {
19
+ if (root === "" || root === "/") return root;
20
+ return root.replace(/\/+$/, "") || "/";
21
+ }
22
+ function deriveAgentId(workspaceRoot) {
23
+ const root = normalizeRoot(workspaceRoot);
24
+ const slug = slugify(lastSegment(root)) || "root";
25
+ const hash = fnv1a32Hex(root);
26
+ return `${AGENT_PREFIX}@${slug}-${hash}`;
27
+ }
28
+
29
+ export {
30
+ deriveAgentId
31
+ };
32
+ //# sourceMappingURL=chunk-B5QKJHSV.js.map