@tuent/sentinel 0.1.1 → 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 +5 -1
- package/SECURITY_MODEL.md +85 -35
- package/dist/Sentinel-XMSJE4DZ.js +10 -0
- package/dist/{Sentinel-B_sv8Kiy.d.ts → Sentinel-xFCyXH45.d.ts} +31 -1
- package/dist/{chunk-WPTJBRX5.js → chunk-FWIISAZZ.js} +118 -7
- package/dist/{chunk-NS6ZLMDK.js → chunk-GRN5P3H2.js} +67 -23
- package/dist/{chunk-IYC5E7RL.js → chunk-L4R3LPJS.js} +148 -31
- package/dist/{chunk-QHE56MEO.js → chunk-QIYQWOLO.js} +82 -4
- package/dist/{chunk-2FFMYSVC.js → chunk-WLIDSTS4.js} +18 -2
- package/dist/cli.js +1 -1
- package/dist/gateway/index.d.ts +23 -1
- package/dist/gateway/index.js +3 -3
- package/dist/gatewayDaemon.js +3 -3
- package/dist/index.d.ts +11 -2
- package/dist/index.js +4 -4
- package/dist/{policyLoader-6KR5VFVV.js → policyLoader-KZL2U4M2.js} +2 -2
- package/package.json +1 -1
- package/dist/Sentinel-QHMQ67W3.js +0 -10
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
|
|
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,31 @@ 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
|
+
### 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
|
+
|
|
67
96
|
---
|
|
68
97
|
|
|
69
98
|
## What Sentinel Does NOT Protect Against
|
|
@@ -86,17 +115,29 @@ For real-time pre-execution enforcement, use one of:
|
|
|
86
115
|
|
|
87
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.
|
|
88
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
|
+
|
|
89
122
|
### Encrypted or Obfuscated Data Exfiltration
|
|
90
123
|
|
|
91
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.
|
|
92
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
|
+
|
|
93
130
|
### Multi-Agent Coordinated Attacks
|
|
94
131
|
|
|
95
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.
|
|
96
133
|
|
|
97
|
-
### Symlink Resolution
|
|
134
|
+
### Symlink and Hardlink Resolution Limits
|
|
98
135
|
|
|
99
|
-
|
|
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.
|
|
100
141
|
|
|
101
142
|
### Concurrent Audit-Trail Writers (No Inter-Process Lock)
|
|
102
143
|
|
|
@@ -116,7 +157,7 @@ Target matching is deliberately conservative: a command that only _references_ a
|
|
|
116
157
|
|
|
117
158
|
### Cold-Start Window
|
|
118
159
|
|
|
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
|
|
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.
|
|
120
161
|
|
|
121
162
|
---
|
|
122
163
|
|
|
@@ -140,6 +181,8 @@ On a cold start, the first tool call of a fresh session waits up to **~5 seconds
|
|
|
140
181
|
- Audit trail is on the Sentinel host, inaccessible to the agent
|
|
141
182
|
- Alerts dispatch to external systems (Slack webhook, PagerDuty, etc.)
|
|
142
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
|
+
|
|
143
186
|
### Development Deployment
|
|
144
187
|
|
|
145
188
|
```
|
|
@@ -193,39 +236,46 @@ On a cold start, the first tool call of a fresh session waits up to **~5 seconds
|
|
|
193
236
|
|
|
194
237
|
## Detection Capabilities
|
|
195
238
|
|
|
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+)
|
|
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+) |
|
|
218
261
|
|
|
219
262
|
---
|
|
220
263
|
|
|
221
264
|
## Finding Types
|
|
222
265
|
|
|
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)
|
|
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 |
|
|
@@ -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
|
}
|
|
@@ -6,8 +6,12 @@ import {
|
|
|
6
6
|
discoverPolicy
|
|
7
7
|
} from "./chunk-FMZWHT4M.js";
|
|
8
8
|
import {
|
|
9
|
+
FORBIDDEN_BASENAMES
|
|
10
|
+
} from "./chunk-QIYQWOLO.js";
|
|
11
|
+
import {
|
|
12
|
+
loadPolicy,
|
|
9
13
|
loadPolicyFromString
|
|
10
|
-
} from "./chunk-
|
|
14
|
+
} from "./chunk-WLIDSTS4.js";
|
|
11
15
|
|
|
12
16
|
// src/setup/initClaudeCode.ts
|
|
13
17
|
import { access, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
|
|
@@ -17,12 +21,16 @@ import { createServer } from "http";
|
|
|
17
21
|
import { fileURLToPath } from "url";
|
|
18
22
|
|
|
19
23
|
// src/gateway/hookScriptSource.ts
|
|
24
|
+
var SCORER_CRITICAL_EXTRAS = ["shadow", "passwd"];
|
|
25
|
+
var HOOK_SENSITIVE_BASENAMES = [
|
|
26
|
+
.../* @__PURE__ */ new Set([...FORBIDDEN_BASENAMES, ...SCORER_CRITICAL_EXTRAS])
|
|
27
|
+
];
|
|
20
28
|
var HOOK_SCRIPT_SOURCE = `#!/usr/bin/env node
|
|
21
29
|
// Sentinel cc hook bridge \u2014 generated by sentinel init claude-code
|
|
22
30
|
// Do not edit manually; regenerate with: sentinel init claude-code --force
|
|
23
31
|
|
|
24
32
|
import { readFileSync, appendFileSync, existsSync } from "node:fs";
|
|
25
|
-
import { join } from "node:path";
|
|
33
|
+
import { join, resolve, sep } from "node:path";
|
|
26
34
|
import { spawn } from "node:child_process";
|
|
27
35
|
|
|
28
36
|
const GATEWAY_ENTRY_POINT = "__GATEWAY_ENTRY_POINT__";
|
|
@@ -74,6 +82,17 @@ function logFallback(entry) {
|
|
|
74
82
|
try { appendFileSync(FALLBACK_LOG, line + "\\n"); } catch { /* best effort */ }
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
// Sprint 26 Fix-2 Part 1 \u2014 hardcoded fail-closed FLOOR. tiers.json may ADD
|
|
86
|
+
// high-tier tools but can never DOWNGRADE a floor tool to low (defeats the
|
|
87
|
+
// two-step tier-config rewrite). Checked BEFORE the editable tiers below.
|
|
88
|
+
const FLOOR_HIGH = ["Bash", "Write", "Edit", "WebFetch", "NotebookEdit", "Task", "Skill"];
|
|
89
|
+
|
|
90
|
+
// Item D \u2014 operator allowUnknownTools escape hatch, baked at init from the
|
|
91
|
+
// launch policy so it survives gateway-down (the daemon allows these names
|
|
92
|
+
// unconditionally at the name level, so allowing them here keeps down \u2287 up).
|
|
93
|
+
// The marker default is [] \u2014 an un-substituted script stays strictest.
|
|
94
|
+
const ALLOW_UNKNOWN_TOOLS = /* __ALLOW_UNKNOWN_TOOLS__ */ [];
|
|
95
|
+
|
|
77
96
|
// Tier config uses flat format: { high, low, mcpDefault, unknownDefault } (plan v3.1 spec'd nested but simplified during 5a)
|
|
78
97
|
function loadTiers() {
|
|
79
98
|
try {
|
|
@@ -90,10 +109,52 @@ function loadTiers() {
|
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
function isHighSensitivity(toolName, tiers) {
|
|
112
|
+
if (FLOOR_HIGH.includes(toolName)) return true; // floor: tiers may add high, never downgrade
|
|
93
113
|
if (tiers.high && tiers.high.includes(toolName)) return true;
|
|
94
114
|
if (tiers.low && tiers.low.includes(toolName)) return false;
|
|
95
115
|
if (toolName.startsWith("mcp__")) return tiers.mcpDefault === "high";
|
|
96
|
-
|
|
116
|
+
// Item D \u2014 operator-allowlisted names pass: gateway-up they are translated
|
|
117
|
+
// as known and allowed at the name level, so the escape hatch must survive
|
|
118
|
+
// gateway-down too.
|
|
119
|
+
if (ALLOW_UNKNOWN_TOOLS.includes(toolName)) return false;
|
|
120
|
+
// Item D floor: all other unknown names are always high-tier gateway-down.
|
|
121
|
+
// Deliberately stricter than gateway-up's warn default: the hook cannot
|
|
122
|
+
// persist findings while the daemon is down, so a warn-equivalent here
|
|
123
|
+
// would be allow-UNLOGGED \u2014 the exact F-8 hole. Hard-deny keeps down \u2287 up
|
|
124
|
+
// in both knob modes (same fail-closed stance as Fix-2's read handling),
|
|
125
|
+
// and a tiers.json edit (unknownDefault: "low") cannot reopen it. Named
|
|
126
|
+
// tools can still be tiered low explicitly via tiers.low above.
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Sprint 26 Fix-2 Part 2 \u2014 gateway-down safe-read exception.
|
|
131
|
+
// INVARIANT: on gateway-down a read is NEVER more permissive than gateway-up.
|
|
132
|
+
// Low-tier file reads (Read/Glob/Grep) fail CLOSED by default; only a
|
|
133
|
+
// structurally-provably-safe target is allowed. The guard is GENERATED from the
|
|
134
|
+
// canonical FORBIDDEN_BASENAMES (\u222A scorer extras), so it can't drift open.
|
|
135
|
+
const SENSITIVE_BASENAMES = ${JSON.stringify(HOOK_SENSITIVE_BASENAMES)};
|
|
136
|
+
const READ_TOOL_PATH_KEYS = { Read: "file_path", Glob: "pattern", Grep: "path" };
|
|
137
|
+
|
|
138
|
+
function isProvablySafeRead(toolName, toolInput, payloadCwd) {
|
|
139
|
+
const key = READ_TOOL_PATH_KEYS[toolName];
|
|
140
|
+
if (!key) return false; // not a path-bearing read tool \u2014 not provably safe
|
|
141
|
+
const raw = toolInput && toolInput[key];
|
|
142
|
+
if (typeof raw !== "string" || raw.length === 0) return false; // no target \u2192 not provable
|
|
143
|
+
if (raw[0] === "/" || raw[0] === "~") return false; // absolute / home \u2192 fail closed
|
|
144
|
+
const cwd = typeof payloadCwd === "string" && payloadCwd ? payloadCwd : process.cwd();
|
|
145
|
+
const cwdNorm = resolve(cwd);
|
|
146
|
+
const norm = resolve(cwd, raw);
|
|
147
|
+
// Must resolve strictly within cwd (rejects any .. escape).
|
|
148
|
+
if (norm !== cwdNorm && !norm.startsWith(cwdNorm + sep)) return false;
|
|
149
|
+
const segments = norm.slice(cwdNorm.length).split(sep).filter(Boolean);
|
|
150
|
+
for (const seg of segments) {
|
|
151
|
+
const low = seg.toLowerCase();
|
|
152
|
+
if (low[0] === ".") return false; // dotfile / dot-dir
|
|
153
|
+
for (const s of SENSITIVE_BASENAMES) {
|
|
154
|
+
if (low.includes(s.toLowerCase())) return false; // conservative sensitive-token match
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return true;
|
|
97
158
|
}
|
|
98
159
|
|
|
99
160
|
// ---------------------------------------------------------------------------
|
|
@@ -111,15 +172,29 @@ if (mode === "pre") {
|
|
|
111
172
|
const result = JSON.parse(resp.body);
|
|
112
173
|
process.stdout.write(JSON.stringify(result), () => process.exit(0));
|
|
113
174
|
} catch {
|
|
114
|
-
// Gateway unreachable \u2014 tiered fail-closed
|
|
175
|
+
// Gateway unreachable \u2014 tiered fail-closed (Sprint 26 Fix-2).
|
|
115
176
|
const tiers = loadTiers();
|
|
177
|
+
const restore = "Restart your Claude Code session to relaunch the gateway; see ~/.dahlia/gateway-fallback.log.";
|
|
116
178
|
if (isHighSensitivity(toolName, tiers)) {
|
|
117
179
|
logFallback({ event: "fail-closed-block", tool: toolName, tier: "high" });
|
|
118
180
|
process.stderr.write(
|
|
119
|
-
\`Sentinel gateway unreachable; high-sensitivity tool "\${toolName}" blocked per fail-closed policy\`,
|
|
181
|
+
\`Sentinel gateway unreachable; high-sensitivity tool "\${toolName}" blocked per fail-closed policy. \${restore}\`,
|
|
182
|
+
() => process.exit(2),
|
|
183
|
+
);
|
|
184
|
+
} else if (
|
|
185
|
+
READ_TOOL_PATH_KEYS[toolName] &&
|
|
186
|
+
!isProvablySafeRead(toolName, payload.tool_input || {}, payload.cwd)
|
|
187
|
+
) {
|
|
188
|
+
// Low-tier read whose target is not provably safe \u2192 fail CLOSED. The hook
|
|
189
|
+
// carries no policy, so it cannot evaluate the forbid surface; a read it
|
|
190
|
+
// can't prove safe is treated as if the gateway would deny it.
|
|
191
|
+
logFallback({ event: "fail-closed-block", tool: toolName, tier: "low", reason: "read-not-provably-safe" });
|
|
192
|
+
process.stderr.write(
|
|
193
|
+
\`Sentinel gateway unreachable; read by "\${toolName}" blocked (fail-closed: target not provably safe). \${restore}\`,
|
|
120
194
|
() => process.exit(2),
|
|
121
195
|
);
|
|
122
196
|
} else {
|
|
197
|
+
// Provably-safe read, or a non-read low-tier tool (e.g. WebSearch) \u2192 allow.
|
|
123
198
|
logFallback({ event: "fail-closed-allow", tool: toolName, tier: "low" });
|
|
124
199
|
process.stdout.write(JSON.stringify({
|
|
125
200
|
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "allow" },
|
|
@@ -322,6 +397,11 @@ agent:
|
|
|
322
397
|
policy:
|
|
323
398
|
allow:
|
|
324
399
|
actions: [file_read, file_write, tool_invocation, network_request, command_exec]
|
|
400
|
+
# allow.targets is ADVISORY for file_read / file_write / tool_invocation: an
|
|
401
|
+
# access outside this list is logged as a MEDIUM scope_violation but still
|
|
402
|
+
# runs \u2014 Sentinel governs the agent's own tool calls, it is not a filesystem
|
|
403
|
+
# sandbox. forbid.targets below is the hard deny. network_request is the
|
|
404
|
+
# exception: unlisted hosts are DENIED by default (see the networkHosts note).
|
|
325
405
|
targets:
|
|
326
406
|
- "src/**"
|
|
327
407
|
- "test/**"
|
|
@@ -378,6 +458,28 @@ policy:
|
|
|
378
458
|
- "**/*.pem"
|
|
379
459
|
- "**/*.key"
|
|
380
460
|
- "/etc/**"
|
|
461
|
+
# Sprint 26 FIX 1 (A) \u2014 common credential stores. DRIFT: keep in sync with
|
|
462
|
+
# DEFAULT_FORBIDDEN_PATTERNS in defaults.ts (unification tracked separately).
|
|
463
|
+
- "**/.netrc"
|
|
464
|
+
- "**/.npmrc"
|
|
465
|
+
- "**/.git-credentials"
|
|
466
|
+
- "**/.pgpass"
|
|
467
|
+
- "**/.zsh_history"
|
|
468
|
+
- "**/.config/gh/**"
|
|
469
|
+
- "**/.docker/config.json"
|
|
470
|
+
- "**/.gnupg/**"
|
|
471
|
+
- "**/.config/gcloud/**"
|
|
472
|
+
- "**/.kube/**"
|
|
473
|
+
- "**/Library/Keychains/**"
|
|
474
|
+
# Sprint 26 FIX 1 (B) \u2014 Sentinel's own state dir (current path only).
|
|
475
|
+
- "**/.dahlia/**"
|
|
476
|
+
# Sprint 26 FIX 3 \u2014 this policy file and cc's hook-wiring settings files.
|
|
477
|
+
# Agent tool-writes are denied; reads stay allowed via a code-side ceiling
|
|
478
|
+
# exception (not authorable here \u2014 workspace-authored exceptions are
|
|
479
|
+
# dropped by the ceiling merge, by design). DRIFT: keep in sync with
|
|
480
|
+
# DEFAULT_FORBIDDEN_PATTERNS in defaults.ts.
|
|
481
|
+
- "**/.sentinel.yaml"
|
|
482
|
+
- "**/.claude/settings*.json"
|
|
381
483
|
enforcement:
|
|
382
484
|
restrictAfter: 3
|
|
383
485
|
quarantineAfter: 5
|
|
@@ -418,7 +520,16 @@ async function runInitClaudeCode(options) {
|
|
|
418
520
|
);
|
|
419
521
|
const hookPath = join(dahliaDir, "cc-hook.mjs");
|
|
420
522
|
const gatewayEntryPoint = resolveGatewayEntryPoint();
|
|
421
|
-
|
|
523
|
+
let allowUnknownTools = [];
|
|
524
|
+
try {
|
|
525
|
+
const effectivePolicy = await loadPolicy(policyPath);
|
|
526
|
+
allowUnknownTools = effectivePolicy.enforcement?.allowUnknownTools ?? [];
|
|
527
|
+
} catch {
|
|
528
|
+
}
|
|
529
|
+
const hookContent = HOOK_SCRIPT_SOURCE.replace(
|
|
530
|
+
/__GATEWAY_ENTRY_POINT__/g,
|
|
531
|
+
gatewayEntryPoint
|
|
532
|
+
).replace("/* __ALLOW_UNKNOWN_TOOLS__ */ []", JSON.stringify(allowUnknownTools));
|
|
422
533
|
if (hookContent.includes("__GATEWAY_ENTRY_POINT__")) {
|
|
423
534
|
throw new Error("Failed to substitute all __GATEWAY_ENTRY_POINT__ placeholders");
|
|
424
535
|
}
|
|
@@ -536,4 +647,4 @@ export {
|
|
|
536
647
|
runInitClaudeCode,
|
|
537
648
|
runSessionStart
|
|
538
649
|
};
|
|
539
|
-
//# sourceMappingURL=chunk-
|
|
650
|
+
//# sourceMappingURL=chunk-FWIISAZZ.js.map
|