@tuent/sentinel 0.1.1 → 0.1.3

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
@@ -63,6 +63,10 @@ enforcement:
63
63
  quarantineAfter: 5 # quarantine after this many
64
64
  ```
65
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
+
66
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.
67
71
 
68
72
  ## Audit trail
@@ -85,7 +89,7 @@ This records the change in the audit trail. Don't edit the mode state file by ha
85
89
 
86
90
  ## Security model
87
91
 
88
- 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. For the full threat model — what Sentinel defends against, what's out of scope by design, and current v0.1.0 limitations — see [SECURITY_MODEL.md](./SECURITY_MODEL.md).
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).
89
93
 
90
94
  ## License
91
95
 
package/SECURITY_MODEL.md CHANGED
@@ -1,12 +1,14 @@
1
1
  # Sentinel Security Model
2
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
+
3
5
  ## What Sentinel Protects Against
4
6
 
5
7
  ### Pre-Execution Enforcement
6
8
 
7
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.
8
10
 
9
- 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.
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.)
10
12
 
11
13
  ### Role Violations
12
14
 
@@ -19,6 +21,8 @@ Targets are checked against glob patterns in two layers:
19
21
  1. **Forbidden patterns** (checked first) -- any match produces a **HIGH** `unauthorized_target`
20
22
  2. **Allowed patterns** (checked second) -- access outside allowed scope produces a **MEDIUM** `scope_violation`
21
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
+
22
26
  Pattern matching uses `**` (any path) and `*` (single segment). Examples:
23
27
 
24
28
  - `**/.env` matches `.env` at any depth
@@ -64,6 +68,40 @@ All target paths are normalized with `path.normalize()` before pattern matching.
64
68
  - `src/../.env` is normalized to `.env` before checking against `**/.env`
65
69
  - `project/subdir/../../.ssh/id_rsa` is normalized to `.ssh/id_rsa`
66
70
 
71
+ Conversely, `command_exec` targets (whole shell command strings) are **never glob-matched as if they were a path**. They are screened by a scanner: the command is tokenized, path-shaped tokens are resolved and matched against the forbidden patterns, and every argv token's basename is matched against each pattern's basename component with **component-boundary** semantics (the basename must match as a whole, not as a substring). So `process.env` and similarly-shaped code constructs are not treated as file access (the basename `process.env` is not the basename `.env`), while a real operand like `payroll.csv` against a `payroll.csv` forbid pattern is denied — in any command position, not only at the end. This screen is the single source of truth for command screening on **both** the gateway and the embedded-SDK (`wrap()`/`check()`) paths; the gateway additionally keeps its inline L1/L2 scan as defense-in-depth. Correspondingly, `command_exec` is **exempt from the allowed-target allowlist** (and session `SCOPE:` narrowing): those are path globs, and a command string is not a path, so matching it there only produced false `scope_violation`s — which the sensitivity scorer could escalate to a hard block when the command merely named a credential token (e.g. `process.env`). Whether an agent may run commands at all is governed by `allowedActions`; whether a given command is forbidden is the command screen above.
72
+
73
+ Two residuals of letting operators forbid bare basenames in commands (both pre-existing, narrower than the prior whole-string glob, and operator-self-inflicted):
74
+
75
+ - **R1 — generic-word custom basenames.** A custom forbid pattern whose basename is a common word (e.g. a pattern ending in `build`) will also match a command that uses that word as a non-file argument (e.g. `npm run build`). This is intrinsic to forbidding a bare common word; prefer specific basenames for custom command-forbid patterns. The same property holds for path-target matching.
76
+ - **R2 — positional-safety scope.** The echo-class positional-safety suppression (`isPositionallySafeMention`) is scoped to the built-in basename list, not to operator-custom patterns, so a custom basename mentioned in an otherwise-safe position (e.g. as an `echo` argument) is still denied.
77
+
78
+ SDK-path residual: obfuscated/encoded command operands are de-obfuscated only on the gateway path (the translator's resolver pipeline); a direct `wrap()`/`check()` caller gets tokenized + basename + substring screening but not the decode layer.
79
+
80
+ ### Self-Protection: Policy and Hook-Wiring Files
81
+
82
+ 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).
83
+
84
+ **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.
85
+
86
+ Honest boundaries of this protection:
87
+
88
+ - **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.
89
+ - **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.
90
+ - **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.
91
+ - **`~/.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.
92
+ - **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.
93
+
94
+ ### Unknown Tool Names
95
+
96
+ 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:
97
+
98
+ - **`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.
99
+ - **`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.
100
+
101
+ `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.
102
+
103
+ **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.
104
+
67
105
  ---
68
106
 
69
107
  ## What Sentinel Does NOT Protect Against
@@ -86,17 +124,29 @@ For real-time pre-execution enforcement, use one of:
86
124
 
87
125
  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.
88
126
 
127
+ ### Bash-Mediated Network Egress
128
+
129
+ 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.
130
+
89
131
  ### Encrypted or Obfuscated Data Exfiltration
90
132
 
91
133
  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.
92
134
 
135
+ ### Self-Hosting Development Boundary
136
+
137
+ 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.
138
+
93
139
  ### Multi-Agent Coordinated Attacks
94
140
 
95
141
  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.
96
142
 
97
- ### Symlink Resolution
143
+ ### Symlink and Hardlink Resolution Limits
98
144
 
99
- RoleValidator operates on path strings, not the filesystem. A symlink named `src/safe-link` pointing to `/etc/shadow` would pass the `src/**` allowed pattern check. Sentinel cannot resolve symlinks without filesystem access on the target host.
145
+ 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:
146
+
147
+ - **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.
148
+ - **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.
149
+ - **Resolution requires the path to exist** at check time; a dangling link that is created after the check is not re-evaluated.
100
150
 
101
151
  ### Concurrent Audit-Trail Writers (No Inter-Process Lock)
102
152
 
@@ -116,7 +166,7 @@ Target matching is deliberately conservative: a command that only _references_ a
116
166
 
117
167
  ### Cold-Start Window
118
168
 
119
- 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 and unknown tools default to the high (deny) tier.
169
+ 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.
120
170
 
121
171
  ---
122
172
 
@@ -140,6 +190,8 @@ On a cold start, the first tool call of a fresh session waits up to **~5 seconds
140
190
  - Audit trail is on the Sentinel host, inaccessible to the agent
141
191
  - Alerts dispatch to external systems (Slack webhook, PagerDuty, etc.)
142
192
 
193
+ 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.
194
+
143
195
  ### Development Deployment
144
196
 
145
197
  ```
@@ -193,39 +245,46 @@ On a cold start, the first tool call of a fresh session waits up to **~5 seconds
193
245
 
194
246
  ## Detection Capabilities
195
247
 
196
- | Threat | Detection Method | Component | Expected Severity |
197
- | --------------------------------------------- | -------------------------------------------- | -------------------------- | --------------------------------------------- |
198
- | SSH key access (`~/.ssh/id_rsa`) | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
199
- | `.env` file read | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
200
- | AWS credential access (`~/.aws/credentials`) | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
201
- | Kubernetes config access (`~/.kube/config`) | Target sensitivity (0.95) | RoleValidator + Scorer | CRITICAL |
202
- | Write to `~/.ssh/authorized_keys` (backdoor) | Target sensitivity (1.0 x 1.3 write) | RoleValidator + Scorer | CRITICAL |
203
- | `/etc/passwd` read | Target sensitivity (0.9) + forbidden pattern | RoleValidator + Scorer | CRITICAL |
204
- | System config write (`/etc/nginx/nginx.conf`) | Target sensitivity (0.7 x 1.3 write = 0.91) | RoleValidator + Scorer | CRITICAL |
205
- | Payment data access (`payments/`) | Target sensitivity (0.85) | DeviationDetector + Scorer | HIGH |
206
- | Database query by file-only agent | Action not in allowedActions | RoleValidator | HIGH |
207
- | Shell command by code-writer agent | Action not in allowedActions | RoleValidator | HIGH |
208
- | 3x baseline event volume | Event count exceeds threshold | DeviationDetector | MEDIUM (escalates to HIGH with other signals) |
209
- | >50% novel target files in session | Target set comparison | DeviationDetector | MEDIUM |
210
- | Activity outside expected hours | Schedule check (UTC) | RoleValidator | MEDIUM |
211
- | Weekend activity when weekday-only | Schedule check (UTC) | RoleValidator | MEDIUM |
212
- | Path traversal (`src/../.env`) | Path normalization + pattern match | RoleValidator | HIGH/CRITICAL (depends on target) |
213
- | Category shift (rare action type) | Baseline action distribution | DeviationDetector | MEDIUM |
214
- | Access outside allowed scope | Allowed target pattern miss | RoleValidator | MEDIUM |
215
- | Multiple converging anomalies (3+) | Finding count in session | DeviationDetector | MEDIUM escalated to HIGH |
216
- | Activity drop (75%+ fewer events) | Event count vs baseline average | DeviationDetector | MEDIUM (HIGH at 90%+ drop) |
217
- | Extended agent silence (2-3x max gap) | Time since last event vs baseline gaps | DeviationDetector | LOW (MEDIUM at 3x+) |
248
+ | Threat | Detection Method | Component | Expected Severity |
249
+ | --------------------------------------------- | -------------------------------------------- | -------------------------- | ----------------------------------------------------------------- |
250
+ | SSH key access (`~/.ssh/id_rsa`) | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
251
+ | `.env` file read | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
252
+ | AWS credential access (`~/.aws/credentials`) | Target sensitivity (1.0) | RoleValidator + Scorer | CRITICAL |
253
+ | Kubernetes config access (`~/.kube/config`) | Target sensitivity (0.95) | RoleValidator + Scorer | CRITICAL |
254
+ | Write to `~/.ssh/authorized_keys` (backdoor) | Target sensitivity (1.0 x 1.3 write) | RoleValidator + Scorer | CRITICAL |
255
+ | `/etc/passwd` read | Target sensitivity (0.9) + forbidden pattern | RoleValidator + Scorer | CRITICAL |
256
+ | System config write (`/etc/nginx/nginx.conf`) | Target sensitivity (0.7 x 1.3 write = 0.91) | RoleValidator + Scorer | CRITICAL |
257
+ | Payment data access (`payments/`) | Target sensitivity (0.85) | DeviationDetector + Scorer | HIGH |
258
+ | Database query by file-only agent | Action not in allowedActions | RoleValidator | HIGH |
259
+ | Shell command by code-writer agent | Action not in allowedActions | RoleValidator | HIGH |
260
+ | 3x baseline event volume | Event count exceeds threshold | DeviationDetector | MEDIUM (escalates to HIGH with other signals) |
261
+ | >50% novel target files in session | Target set comparison | DeviationDetector | MEDIUM |
262
+ | Activity outside expected hours | Schedule check (UTC) | RoleValidator | MEDIUM |
263
+ | Weekend activity when weekday-only | Schedule check (UTC) | RoleValidator | MEDIUM |
264
+ | Path traversal (`src/../.env`) | Path normalization + pattern match | RoleValidator | HIGH/CRITICAL (depends on target) |
265
+ | Category shift (rare action type) | Baseline action distribution | DeviationDetector | MEDIUM |
266
+ | Access outside allowed scope | Allowed target pattern miss | RoleValidator | MEDIUM — advisory (logged, not blocked); `network_request` denied |
267
+ | Multiple converging anomalies (3+) | Finding count in session | DeviationDetector | MEDIUM escalated to HIGH |
268
+ | Activity drop (75%+ fewer events) | Event count vs baseline average | DeviationDetector | MEDIUM (HIGH at 90%+ drop) |
269
+ | Extended agent silence (2-3x max gap) | Time since last event vs baseline gaps | DeviationDetector | LOW (MEDIUM at 3x+) |
218
270
 
219
271
  ---
220
272
 
221
273
  ## Finding Types
222
274
 
223
- | Type | Source | Description |
224
- | --------------------- | -------------------------------- | ----------------------------------------------------------------------------------------- |
225
- | `role_violation` | RoleValidator | Agent performed an action not in its `allowedActions` list |
226
- | `unauthorized_target` | RoleValidator, DeviationDetector | Agent accessed a target matching `forbiddenTargetPatterns` or scoring high on sensitivity |
227
- | `scope_violation` | RoleValidator | Agent accessed a target outside its `allowedTargetPatterns` |
228
- | `temporal_anomaly` | RoleValidator, DeviationDetector | Activity outside expected schedule or baseline typical hours/days |
229
- | `volume_spike` | DeviationDetector | Session event count exceeds baseline average by 3x+ |
230
- | `access_pattern` | DeviationDetector | Unusual target distribution, weight anomaly, or category shift |
231
- | `behavioral_absence` | DeviationDetector | Significant activity drop (75%+ below baseline) or extended silence (2-3x max gap) |
275
+ | Type | Source | Description |
276
+ | --------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------- |
277
+ | `role_violation` | RoleValidator | Agent performed an action not in its `allowedActions` list |
278
+ | `unauthorized_target` | RoleValidator, DeviationDetector | Agent accessed a target matching `forbiddenTargetPatterns` or scoring high on sensitivity |
279
+ | `scope_violation` | RoleValidator | Agent accessed a target outside its `allowedTargetPatterns` |
280
+ | `temporal_anomaly` | RoleValidator, DeviationDetector | Activity outside expected schedule or baseline typical hours/days |
281
+ | `volume_spike` | DeviationDetector | Session event count exceeds baseline average by 3x+ |
282
+ | `access_pattern` | DeviationDetector | Unusual target distribution, weight anomaly, or category shift |
283
+ | `behavioral_absence` | DeviationDetector | Significant activity drop (75%+ below baseline) or extended silence (2-3x max gap) |
284
+ | `intent_drift` | Similarity engine (active task) | Action poorly aligned with the declared active task — informational, never blocks |
285
+ | `bash_analysis` | Gateway L3 | Dangerous construct / unparseable command without forbidden indicators — informational |
286
+ | `hook_block` | HookEngine | A registered pre-execution hook vetoed the action |
287
+ | `workspace_mismatch` | Gateway routing | Request with no/unresolvable workspace — fail-closed routing refusal; not escalation-eligible |
288
+ | `agent_restricted` | Enforcement ladder | Action denied because the agent is in restricted mode |
289
+ | `agent_quarantined` | Enforcement ladder | Action denied because the agent is quarantined |
290
+ | `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-SSDIBY52.js";
4
+ import "./chunk-JTR2E7RD.js";
5
+ import "./chunk-WLIDSTS4.js";
6
+ import "./chunk-NUXSUSYY.js";
7
+ export {
8
+ Sentinel
9
+ };
10
+ //# sourceMappingURL=Sentinel-5CQ6HKXS.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,37 @@ 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;
302
+ /**
303
+ * Sprint 26B F-5a (corroboration-by-distinct-target). Identity of the RESOLVED
304
+ * forbidden target the deny fired on: the L1-resolved path when one exists,
305
+ * else a basename-family fallback (`l2:<sorted basenames>`) for L2-only hits
306
+ * that never produced a resolved path (unparseable / construct ambiguity).
307
+ * When present, getEffectiveBlockCount keys distinct-counting on it INSTEAD of
308
+ * dedupKey, so repeated denials against the same forbidden target — e.g. many
309
+ * differently-shaped commands all naming one token during self-hosted
310
+ * development — contribute ONE count toward the escalation ladder, while
311
+ * distinct forbidden targets still accumulate (a real multi-file sweep
312
+ * escalates as before). Additive and conditional (soft-signal discipline):
313
+ * absent on pre-F-5a entries, which keep counting by dedupKey / keyless
314
+ * exactly as before. NEVER changes the per-command deny disposition.
315
+ */
316
+ targetKey?: string;
281
317
  }
282
318
  /** Computed behavioral baseline for an agent over a time window. */
283
319
  /**
@@ -1778,6 +1814,15 @@ declare class Sentinel {
1778
1814
  * Sprint 16 Prompt 3 — replaces in-memory blockCounts Map to survive
1779
1815
  * gateway restarts. Same pattern as Sprint 11 sessionCount Shape D fix.
1780
1816
  */
1817
+ /**
1818
+ * Shared escalation-eligibility predicate (Sprint 26 F-5). Single source of
1819
+ * truth for "this finding counts toward the block ladder", extracted from the
1820
+ * formerly-inline copies in getEffectiveBlockCount and maybeEscalate — behavior
1821
+ * of both is identical to before the extraction. Accepts either a
1822
+ * SecurityFinding-shaped object ({type}) or an audit entry ({findingType},
1823
+ * mapped by the caller).
1824
+ */
1825
+ private static isEscalationEligible;
1781
1826
  private getEffectiveBlockCount;
1782
1827
  private maybeEscalate;
1783
1828
  }