Skip to content

Commit 03c8f58

Browse files
committed
feat(shrike-security): real before_tool_call plugin + image install path
Rebuild interception as a genuine OpenClaw before_tool_call plugin (the supported runtime interception path, matching NemoClaw's own in-tree plugin), replacing the PreToolUse settings.json hook that the OpenClaw runtime does not load. - plugin/: self-contained TS before_tool_call plugin; fail-closed on every unresolved verdict (malformed event, unreachable enforce, no decision) - providers/shrike.yaml: scope egress to the two enforce POST paths; curl-only - onboard.sh: attach the Shrike provider to the sandbox - build-image.sh: durable custom-image bake (nemoclaw onboard --from) as the recommended install; runtime install kept as a flagged best-effort dev path that never silently bypasses the managed config-integrity shield - verify.sh: drive real tool calls through the gateway (/tools/invoke) - README + docs/verify-functionality.md: reframe as in-sandbox defense-in-depth with the real-runtime allow/deny proof Signed-off-by: Habirua <41754124+Habirua@users.noreply.github.com>
1 parent c9ef556 commit 03c8f58

20 files changed

Lines changed: 1139 additions & 336 deletions

examples/recipes/partners/shrike/shrike-security/.env.example

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,19 @@ SHRIKE_API_KEY=
1414
NEMOCLAW_PROVIDER=build
1515
NVIDIA_INFERENCE_API_KEY=
1616

17+
# Install path for the governance plugin:
18+
# runtime (default, tested) — install into the live sandbox; not durable
19+
# across `nemoclaw <sb> rebuild`.
20+
# image — bake the plugin into a version-matched custom
21+
# sandbox image (scripts/build-image.sh); durable
22+
# across rebuild. Needs a matched NemoClaw source
23+
# checkout (NEMOCLAW_SOURCE_DIR) or GitHub access.
24+
# INSTALL_MODE=runtime
25+
1726
# Optional overrides:
1827
# NEMOCLAW_SANDBOX_NAME=shrike-security
1928
# SHRIKE_PROFILE_ID=nemoclaw-shrike # v2 provider-profile id (providers/shrike.yaml)
2029
# SHRIKE_PROVIDER_NAME=shrike-security-shrike # provider instance name
2130
# SHRIKE_FAIL_MODE=closed # closed (default) = deny on enforce error; open = allow
31+
# SHRIKE_VERIFY_TOOL=web_search # tool scripts/verify.sh drives (must exist in the agent)
32+
# NEMOCLAW_SOURCE_DIR=/path/to/NemoClaw # image mode: matched source checkout as build context
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
.env
2+
.image-build/

examples/recipes/partners/shrike/shrike-security/README.md

Lines changed: 140 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,23 @@
55
# Shrike Security — Agent Action Governance
66

77
Govern what an OpenClaw agent is allowed to **do**. This recipe installs a
8-
Shrike PreToolUse hook into a NemoClaw/OpenShell sandbox so every matched tool
9-
call is evaluated by Shrike's enforce plane before it runs, and denied when the
10-
verdict is `block` or `require_approval`.
8+
Shrike **`before_tool_call` plugin** into a NemoClaw/OpenShell sandbox so every
9+
tool call is evaluated by Shrike's enforce plane before it runs, and blocked
10+
when the verdict is `block` or `require_approval`.
1111

1212
OpenShell contains *where* an agent can act (network, filesystem, syscalls).
1313
Shrike governs *what* a specific action is — a benign shell command is allowed;
1414
a destructive command, a SQL injection, an injected instruction, or a secret
15-
exfiltration attempt is denied — with the decision made server-side against
15+
exfiltration attempt is blocked — with the decision made server-side against
1616
organizational policy and returned as `allow` / `warn` / `require_approval` /
1717
`block`. The two compose: the sandbox is the cage, Shrike is the judgment.
1818

19+
This is **in-sandbox defense-in-depth**, not an independent security boundary.
20+
The plugin runs inside the sandbox alongside the agent; a fully compromised
21+
sandbox could tamper with it. The independent controls remain the OpenShell
22+
egress policy + credential provider (`providers/shrike.yaml`), which hold even
23+
if the in-sandbox plugin is subverted.
24+
1925
## Intended users and support boundary
2026

2127
For operators running OpenClaw agents inside NemoClaw who want an action-level
@@ -29,12 +35,12 @@ agreement.
2935

3036
## Data-sharing boundary (read before enabling)
3137

32-
When the hook evaluates an action, the **action content** — the shell command,
33-
SQL text, file-write body, web-search target, or message content of the matched
34-
tool call — is transmitted over TLS to `api.shrikesecurity.com` for evaluation.
35-
A decision and a short reason are returned; no other sandbox data is sent. If no
36-
Shrike credential is configured, the hook cannot obtain a verdict and (by
37-
default) denies the action fail-closed — nothing is transmitted without a key.
38+
When the plugin evaluates an action, the **action content** — the shell command,
39+
SQL text, file-write body, web-search target, or message content of the tool
40+
call — is transmitted over TLS to `api.shrikesecurity.com` for evaluation. A
41+
decision and a short reason are returned; no other sandbox data is sent. If no
42+
Shrike credential is configured, the plugin cannot obtain a verdict and (by
43+
default) blocks the action fail-closed — nothing is transmitted without a key.
3844
Retention, GDPR/CCPA, and data-deletion posture: <https://shrikesecurity.com/privacy>.
3945

4046
Do not enable this recipe on a workload whose tool-call content must never leave
@@ -43,89 +49,151 @@ the sandbox.
4349
## Provenance
4450

4551
Contributed by **Shrike Security, Inc.** Attribution is preserved in the SPDX
46-
headers of every file. Shrike Security authored the hook, policy, and lifecycle
47-
scripts; the recipe targets the public NemoClaw/OpenShell CLIs.
52+
headers of every file. Shrike Security authored the plugin, policy, and
53+
lifecycle scripts; the recipe targets the public NemoClaw/OpenShell CLIs.
4854

4955
## Prerequisites and supported environments
5056

5157
- NemoClaw installed with a working OpenShell gateway (`nemoclaw`, `openshell`
5258
on `PATH`). Install: `curl -fsSL https://www.nvidia.com/nemoclaw.sh | NEMOCLAW_ACCEPT_THIRD_PARTY_SOFTWARE=1 bash`
5359
- An inference provider (NVIDIA-hosted `build` endpoints, or any OpenAI-compatible endpoint).
5460
- A Shrike API key (free tier available): <https://shrikesecurity.com/signup>
55-
- `bash`, `node`, and `curl` available in the sandbox (present in the OpenClaw runtime).
56-
- Verified on macOS (arm64) and Linux hosts with a CPU-only OpenShell gateway.
61+
- On the host: `node` + `npm` (to build the plugin) and `curl`.
62+
- Verified on macOS (arm64) and Linux hosts with a CPU-only OpenShell gateway,
63+
NemoClaw v0.0.97 / OpenClaw 2026.7.1.
5764

5865
## Architecture and major components
5966

6067
```
61-
OpenClaw agent --(tool call)--> PreToolUse hook --(action content)--> Shrike enforce
62-
| | |
63-
| | allow/warn -> allow | server-side
64-
| <---------- decision ------------+ block/require_approval -> deny | 9-layer policy
68+
OpenClaw agent --(tool call)--> before_tool_call plugin --(action content)--> Shrike enforce
69+
| | |
70+
| | allow/warn -> allow | server-side
71+
| <----------- decision -------------+ block/require_approval -> block | 9-layer policy
6572
```
6673

67-
- `hooks/shrike-preaction-hook.mjs` — classifies the action (sql / command /
68-
file_write / web_search / general), calls the enforce plane, maps the verdict
69-
to an OpenClaw permission decision. Fail-closed by default.
74+
- `plugin/` — the OpenClaw `before_tool_call` plugin (TypeScript → `dist/`). It
75+
classifies each action (sql / command / file_write / web_search / general),
76+
calls the enforce plane, and maps the verdict to an OpenClaw decision
77+
(`allow`/`warn` → allow; `block`/`require_approval` → block). Fail-closed by
78+
default. This is the supported OpenClaw interception path — the same contract
79+
NemoClaw's own secret-scanner uses — not a Claude-style PreToolUse hook.
7080
- `providers/shrike.yaml` — OpenShell v2 provider profile: declares the
71-
`SHRIKE_API_KEY` bearer credential and scopes egress to
72-
`api.shrikesecurity.com` with `enforcement: enforce`. Imported at onboard time.
73-
- `agents.yaml` — keeps the full toolset; governance is at the hook, not by
81+
`SHRIKE_API_KEY` bearer credential and scopes egress to the two enforce POST
82+
paths on `api.shrikesecurity.com` with `enforcement: enforce`.
83+
- `agents.yaml` — keeps the full toolset; governance is at the plugin, not by
7484
removing tools.
75-
- `scripts/``onboard.sh`, `install.sh`, `verify.sh`, `status.sh`, `teardown.sh`.
85+
- `scripts/``onboard.sh`, `install.sh`, `build-image.sh`, `verify.sh`,
86+
`status.sh`, `teardown.sh`.
87+
88+
## Two install paths
89+
90+
The plugin can be delivered two ways. **Image is the supported, reliable path;
91+
runtime is a best-effort local convenience.**
92+
93+
| | **`image` (recommended)** | **`runtime` (dev quick-try)** |
94+
| --- | --- | --- |
95+
| How | Baked into a version-matched custom sandbox image at onboard time (`nemoclaw onboard --from`) | Installed into the live sandbox (`openclaw plugins install`) |
96+
| Durable across `rebuild` | Yes | No |
97+
| Touches the managed config guard | No — config is sealed correctly at build time | Yes — see below |
98+
| Provenance / trusted-source check | Yes — NemoClaw records + re-validates plugin provenance on every rebuild (v0.0.76+) | No — see below |
99+
| Prerequisite | A matched NemoClaw source checkout (or GitHub access to clone the release tag) | None |
100+
101+
### Why image is the reliable path (the config-integrity shield)
102+
103+
The managed runtime protects `openclaw.json` with an integrity shield
104+
(`.config-hash`). Enabling a plugin **at runtime** rewrites `openclaw.json`
105+
out-of-band, and a background normalizer keeps rewriting it, so the gateway
106+
refuses to restart (`GATEWAY_UNSAFE_CONFIG_PATH`) until the change is re-blessed
107+
— and re-blessing races the normalizer nondeterministically. There is no
108+
supported runtime plugin-install into a managed sandbox yet (tracked upstream in
109+
[NemoClaw #5998](https://github.com/NVIDIA/NemoClaw/issues/5998)).
110+
111+
The **image path avoids this entirely**: the plugin is installed and the config
112+
is sealed *at trusted build time*, before the shield locks it, under NemoClaw's
113+
provenance guard. That guard is the real "this change is authentic" check — a
114+
property the runtime path cannot provide (a runtime re-bless proves *intent*,
115+
not *authenticity*: `.config-hash` is a sha256, carrying no signature). As a
116+
security recipe, this example uses NemoClaw's supported path and does **not**
117+
silently circumvent the integrity shield.
118+
119+
## Setup and configuration
120+
121+
```bash
122+
cp .env.example .env # set SHRIKE_API_KEY + your inference provider vars
123+
```
124+
125+
### Recommended — image install (durable, provenance-guarded)
126+
127+
```bash
128+
# Point at a NemoClaw source checkout matching your installed CLI (or let
129+
# build-image.sh clone the matching release tag from GitHub).
130+
export INSTALL_MODE=image
131+
export NEMOCLAW_SOURCE_DIR=/path/to/NemoClaw # matched to `nemoclaw --version`
132+
133+
bash scripts/onboard.sh # import provider + create provider + BAKE plugin image + attach provider
134+
bash scripts/install.sh # verifies the baked plugin loaded (no-op install)
135+
bash scripts/verify.sh # allowed/denied live validation
136+
```
137+
138+
### Dev quick-try — runtime install (not durable; best-effort)
139+
140+
```bash
141+
bash scripts/onboard.sh # import + create provider + onboard + attach provider
142+
SHRIKE_RUNTIME_REBLESS=1 bash scripts/install.sh # install plugin into the live sandbox
143+
bash scripts/verify.sh
144+
```
145+
146+
`install.sh` will not silently touch the managed config guard: without
147+
`SHRIKE_RUNTIME_REBLESS=1` it fails loud and points you here. With the opt-in it
148+
re-blesses the integrity hash (unsigned, operator-asserted) and retries; because
149+
that races the managed normalizer it may not settle — if it doesn't, use the
150+
image path.
151+
152+
Overridable knobs (in `.env`): `INSTALL_MODE`, `NEMOCLAW_SANDBOX_NAME`,
153+
`SHRIKE_PROFILE_ID`, `SHRIKE_PROVIDER_NAME`, `SHRIKE_FAIL_MODE` (`closed`
154+
default = block on enforce error; `open` = allow), `SHRIKE_VERIFY_TOOL`,
155+
`NEMOCLAW_SOURCE_DIR`.
76156

77157
## Credential and secret handling
78158

79159
`onboard.sh` imports the v2 provider profile (`providers/shrike.yaml`) and
80-
creates a provider instance with `openshell provider create`, supplying
81-
`SHRIKE_API_KEY` to the **gateway** once through a clean sub-environment. The
82-
sandboxed agent and hook reference only the placeholder
160+
creates a provider with `openshell provider create`, supplying `SHRIKE_API_KEY`
161+
to the **gateway** once through a clean sub-environment, then **attaches** that
162+
provider to the sandbox. The plugin references only the placeholder
83163
`openshell:resolve:env:SHRIKE_API_KEY`; the OpenShell L7 proxy substitutes the
84164
real value on egress to `api.shrikesecurity.com`. The raw key is never written
85165
into the sandbox environment, filesystem, or agent context. `.env` holds the key
86166
on the host only (for the one `provider create` call) and is git-ignored.
87167

88-
## Setup and configuration
89-
90-
```bash
91-
cp .env.example .env # set SHRIKE_API_KEY + your inference provider vars
92-
bash scripts/onboard.sh # import provider profile + create provider (key held gateway-side) + onboard sandbox
93-
bash scripts/install.sh # install the PreToolUse hook into the sandbox
94-
```
95-
96-
Overridable knobs (in `.env`): `NEMOCLAW_SANDBOX_NAME`, `SHRIKE_PROFILE_ID`,
97-
`SHRIKE_PROVIDER_NAME`, `SHRIKE_FAIL_MODE` (`closed` default = deny on enforce
98-
error; `open` = allow).
99-
100168
## Sandbox, network, and policy permissions
101169

102-
The provider profile (`providers/shrike.yaml`) scopes egress to
103-
`api.shrikesecurity.com:443` with `enforcement: enforce` and permits `curl` +
104-
`node` for the hook. The Shrike enforce API itself accepts only the
105-
`/agent/api/scan/enforce[/specialized]`, `/agent/api/session/status`, and
106-
`/health` paths. No other network destination is reachable from the sandbox.
170+
`providers/shrike.yaml` scopes egress to `api.shrikesecurity.com:443` with
171+
`enforcement: enforce`, and restricts it to exactly two POST paths
172+
(`/agent/api/scan/enforce` and `/agent/api/scan/enforce/specialized`) — no other
173+
method or path on the host is reachable — and permits only `curl` for the egress
174+
call. No other network destination is reachable from the sandbox.
107175

108176
## Startup behavior
109177

110-
Governance is active as soon as `install.sh` registers the PreToolUse hook —
111-
there is no long-running service to start. Every matched tool call is evaluated
112-
inline before it executes.
178+
Governance is active once the plugin is loaded (after the image onboard, or
179+
after a successful runtime install + gateway restart). There is no long-running
180+
service to start; every tool call is evaluated inline before it executes.
113181

114182
## Verification steps and expected results
115183

116184
```bash
117185
bash scripts/verify.sh
118186
```
119187

120-
Drives the installed hook with representative payloads and asserts:
188+
`verify.sh` drives **real tool calls through the sandbox gateway**
189+
(`/tools/invoke`); the `before_tool_call` plugin fires on each before the tool
190+
runs. A benign call must pass the plugin (not blocked); a malicious call must be
191+
blocked (`tool_call_blocked`).
121192

122-
| Action | Expected |
193+
| Tool call (`SHRIKE_VERIFY_TOOL`, default `web_search`) | Expected |
123194
| --- | --- |
124-
| Benign shell command (`ls -la`) | **allow** |
125-
| Destructive command (`rm -rf /`) | **deny** |
126-
| SQL injection (`... OR 1=1; DROP TABLE`) | **deny** |
127-
| Prompt injection (ignore-instructions + exfil) | **deny** |
128-
| Secret exfiltration (key in URL) | **deny** |
195+
| Benign technical query | **allowed** (passes the plugin) |
196+
| Prompt injection (ignore-instructions + exfil) | **blocked** |
129197

130198
`scripts/verify.sh` exits `0` only when every case matches. A full transcript of
131199
a real run is in [docs/verify-functionality.md](docs/verify-functionality.md).
@@ -134,25 +202,34 @@ Inspect wiring any time with `bash scripts/status.sh`.
134202
## Teardown and cleanup
135203

136204
```bash
137-
bash scripts/teardown.sh # remove hook + Shrike provider/profile + sandbox
205+
bash scripts/teardown.sh # disable/uninstall plugin + detach + remove provider/profile + sandbox
138206
KEEP_SANDBOX=1 bash scripts/teardown.sh # only un-wire Shrike; keep the sandbox
139207
```
140208

141-
Teardown-safe: leaves no service or credential active.
209+
Teardown-safe: leaves no service or credential active. For an `image` install
210+
the plugin is baked into the image, so removing the sandbox (the default) is the
211+
clean teardown.
142212

143213
## Known limitations
144214

145-
- The hook governs **matched tool calls**; content the agent never routes
146-
through a tool is out of scope (that is OpenShell's containment layer's job).
215+
- **In-sandbox defense-in-depth, not an independent boundary** — a fully
216+
compromised sandbox could tamper with the plugin; the OpenShell egress policy
217+
+ credential provider are the controls that hold regardless.
218+
- **Runtime install is best-effort** — it races the managed config-integrity
219+
shield and is not durable across `rebuild`; use `image` for anything real.
147220
- Enforce adds a network round-trip per governed action (typically well under a
148221
second warm). `SHRIKE_FAIL_MODE=open` trades containment for availability if
149222
the enforce plane is unreachable.
150223
- Action classification is heuristic by tool/field shape; unusual tool schemas
151224
fall back to the general enforce path.
225+
- `verify.sh` drives one tool (`SHRIKE_VERIFY_TOOL`, default `web_search`); set
226+
it to a tool your agent exposes.
152227

153228
## Third-party dependencies and license obligations
154229

155-
No new third-party libraries are added by this recipe. It uses the NemoClaw and
156-
OpenShell CLIs, `node`, and `curl`, all already present in the runtime. The
157-
recipe is licensed under Apache-2.0. The Shrike service it calls is operated by
158-
Shrike Security, Inc. under its own terms (<https://shrikesecurity.com>).
230+
The plugin builds with TypeScript (dev-only) and imports nothing from the
231+
`openclaw` runtime (structural types), so it ships no runtime third-party
232+
libraries. It uses the NemoClaw and OpenShell CLIs and `curl`, already present
233+
in the runtime. The recipe is licensed under Apache-2.0. The Shrike service it
234+
calls is operated by Shrike Security, Inc. under its own terms
235+
(<https://shrikesecurity.com>).

examples/recipes/partners/shrike/shrike-security/agents.yaml

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
#
55
# Agent manifest for the Shrike governance recipe.
66
#
7-
# Shrike governs at the ACTION boundary via the PreToolUse hook (see
8-
# install.sh), not by removing tools from the agent. The agent keeps its full
9-
# toolset; every matched call is evaluated by Shrike before it runs and denied
10-
# when the verdict is block / require_approval. This keeps the recipe a drop-in
11-
# governance layer over whatever workflow the operator runs.
7+
# Shrike governs at the ACTION boundary via an OpenClaw `before_tool_call`
8+
# plugin (plugin/, installed by install.sh or baked in by build-image.sh), not
9+
# by removing tools from the agent. The agent keeps its full toolset; every
10+
# tool call is evaluated by Shrike before it runs and blocked when the verdict
11+
# is block / require_approval. This keeps the recipe a drop-in governance layer
12+
# over whatever workflow the operator runs.
1213
main:
1314
tools: {}

0 commit comments

Comments
 (0)