examples: add agent-action-monitor (behavioural gate for AI agent actions)#209
examples: add agent-action-monitor (behavioural gate for AI agent actions)#209ritiksah141 wants to merge 6 commits into
Conversation
…ions) Adds a new example at examples/agent-action-monitor/. A behavioural gate for AI agent actions, built on DUSK, an open source behavioural threat detection project. A hijacked AI agent still holds valid credentials, so a credential check waves it through. This gate asks a different question, does this agent normally do this, by scoring a proposed control-plane action against that agent's own learned baseline before the action reaches a downstream system. All three SIE primitives are wired into the live /v1/gate request path: extract (GLiNER) for privileged terms, encode (bge-m3) for a bounded fleet-wide similar-decision lookup, and score (bge-reranker-v2-m3) for semantic novelty against the agent's own baseline history. SIE is optional and degrades gracefully throughout the request path. Also adds a row to examples/README.md's gallery table. Built by Ritik Sah and Tanvir Farhad.
|
Hi @svonava,Quick update load testing is done, the latency table is in the README, and CI is green. This is ready for review whenever you get a chance. Thanks again for the support. |
…on-monitor-dusk # Conflicts: # examples/README.md
fm1320
left a comment
There was a problem hiding this comment.
Thanks for this contribution, and for the care that went into it: the pins all check out, the test suite is real, and the additive-only SIE design holds in the code. A few things before we can merge.
Main blocker: the example needs to show SIE earning its place. The benchmark can't show SIE lift by construction: the negatives are the training set and the 3 attacks are built to trip the static rules, so precision/recall are 1.0/1.0 with SIE on or off. You already built the mechanism to fix this (test_sie_extract_flags_terms_missed_by_the_static_frozenset): please add held-out negatives and at least one attack that evades the deterministic rules and is caught only via extract or rerank novelty, so disabling SIE visibly drops recall. Until then, please remove "evidence that SIE is load-bearing" (sie-primitives.md) and "disabling SIE degrades detection quality" (README).
Out-of-the-box fixes:
.envis dead config: compose has no${VAR}interpolation orenv_file, andSIE_API_KEYnever reaches the gate container.- The real-Bedrock path can't work (hardcoded
USE_REAL_BEDROCK: "false", no AWS creds plumbed, model ID needs theus.inference-profile prefix). Wire it or document it as mock-only. - Document cold start: ~5.7 GB weight download against a ~150s healthcheck window and ~7 GB RSS for the sie container. On slow connections or default Docker Desktop RAM,
compose upaborts with no hint why. Note the Apple Silicon emulation penalty too. - The without-Docker flow is broken: gate defaults to port 5000, harness targets 8000, and
requestsis only in the dev extra. - n8n:
N8N_USER_MANAGEMENT_DISABLEDis a no-op since 1.0 (UI hits an owner-setup screen); please setN8N_DIAGNOSTICS_ENABLED: "false"since the compose header claims nothing phones home.
Engine fixes:
- Repeat-offense refusals caused only by the repeat boost are re-recorded with fresh timestamps, so the documented decay never happens and watch mode alters future scoring. Record based on the score excluding the repeat contribution.
OffenseRecord.from_dictaccepts naive timestamps;_decaythen raises on every evaluation for that agent. Validate likeAgentActiondoes._load_gate_enginemissesOSError(e.g. a mis-mounted volume), turning intended 503 degradation into per-request 500s.- A half-up SIE can stall one gate call ~50s (five sequential calls at 10s timeout). A short per-call timeout or circuit breaker on the inline path would fix it.
agent_idaccepts newlines, allowing forged lines in the gate's own audit log. Reject control characters.- Caps bound counts, not bytes: large unique-token targets and rotating agent IDs blow past the advertised bounds and can LRU-evict a real agent's offense history.
Scope: at 71 files this is several times larger than our biggest existing example, and most of it is DUSK product code rather than SIE integration. We'd genuinely consider a version where the engine lives in your repo and this example is the thin integration layer. Open to discussing either shape.
Also, the branch conflicts with main, so please rebase so CI can run. Happy to re-review quickly once these land: with a benchmark where SIE visibly carries signal, this becomes a strong example.
…on-monitor-dusk # Conflicts: # examples/README.md
Rebuilds the live SIE benchmark so it actually shows SIE is load bearing: adds held out negatives the baseline never trained on, and an attack built to evade every deterministic check, then scores it with SIE live versus forced off to show the score lands on opposite sides of the block threshold, not just that a reason string mentions SIE. Also fixes: - SIE_API_KEY and AWS credentials are wired through compose.yml instead of being dead config - the real Bedrock model id now uses the region-prefixed inference profile id Bedrock requires for on-demand invocation - documented the actual cold start cost (weight download size, healthcheck window, RSS, Apple Silicon emulation) - the without-Docker flow now installs agent-demo/mock-prod's own requirements and the gate's default port matches everywhere else - swapped the no-op N8N_USER_MANAGEMENT_DISABLED for N8N_DIAGNOSTICS_ENABLED - a refusal driven only by the repeat-offense boost no longer re-records itself with a fresh timestamp and defeats its own decay - OffenseRecord rejects naive timestamps instead of crashing decay later - the gate engine loader catches OSError broadly, not just FileNotFoundError - an in-process circuit breaker bounds how long a half-up SIE can stall a single gate request - agent_id rejects control characters - offense memory bounds bytes, not just record and agent counts Also merges upstream/main to resolve the gallery table conflict in examples/README.md.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThe pull request adds a runnable DUSK agent-action-monitor example with canonical action ingestion, behavioral baseline analysis, SIE-assisted detection, offense persistence, an HTTP gate API, Bedrock scenarios, downstream mocks, n8n webhooks, container orchestration, and extensive tests and documentation. ChangesAgent action behavioral gate
Sequence Diagram(s)sequenceDiagram
participant AgentDemo
participant Bedrock
participant DuskGate
participant N8n
participant MockProd
AgentDemo->>Bedrock: converse tool-use request
Bedrock-->>AgentDemo: proposed action
AgentDemo->>DuskGate: POST /v1/gate
DuskGate->>N8n: decision/report/alert webhook
DuskGate-->>AgentDemo: ALLOW, WOULD-BLOCK, or BLOCK
AgentDemo->>MockProd: POST /apply for non-blocked verdicts
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (4)
examples/agent-action-monitor/tests/test_actions_offense_memory.py (1)
144-167: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winSleep-based scheduling assumptions risk CI flakiness. Both tests use a fixed
time.sleep()duration to assume a background thread has reached a particular point before asserting on shared state, which can intermittently fail under CI contention.
examples/agent-action-monitor/tests/test_actions_offense_memory.py#L144-L167: theelapsed < 0.2bound intest_record_does_not_block_on_a_slow_disk_writehas only the margin between "instant" and the 0.2s patched sleep; consider widening the patched sleep (e.g. 1s+) relative to the assertion threshold, or asserting via a signal (e.g. an event set just before the sleep) rather than wall-clock elapsed time.examples/agent-action-monitor/tests/test_n8n_client.py#L125-L163:time.sleep(0.1)"to let the single worker pick up the first send" is a guess at scheduling latency; consider polling_pending_countwith a timeout, or have_blocking_sendsignal an event once entered, instead of a fixed sleep before asserting the backlog bound.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/agent-action-monitor/tests/test_actions_offense_memory.py` around lines 144 - 167, The tests rely on fixed sleep durations for background-thread scheduling and can be flaky under CI contention. In examples/agent-action-monitor/tests/test_actions_offense_memory.py:144-167, update test_record_does_not_block_on_a_slow_disk_write to use a synchronization event before the delayed Path.replace call, or widen the delay substantially relative to the elapsed-time assertion. In examples/agent-action-monitor/tests/test_n8n_client.py:125-163, update the test around _blocking_send to signal when the worker enters the first send and wait for that signal, or poll _pending_count with a timeout, instead of sleeping for a fixed duration before asserting the backlog bound.examples/agent-action-monitor/tests/test_actions_gate.py (1)
267-268: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueDuplicate assertion.
Line 268 repeats line 267 verbatim; safe to drop the duplicate.
🧹 Proposed fix
assert not any("SIE extract" in r for r in extracted.reasons) - assert not any("SIE extract" in r for r in extracted.reasons)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/agent-action-monitor/tests/test_actions_gate.py` around lines 267 - 268, Remove the duplicate identical assertion in the test around extracted.reasons, leaving a single check that no reason contains “SIE extract.”examples/agent-action-monitor/contracts/gate.openapi.yaml (1)
1-99: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winNo authentication scheme defined in the contract.
The spec defines no
securityrequirement forPOST /v1/gate, consistent with the implementation inapi.pyhaving no auth check. See the consolidated comment onapi.pyfor the shared concern.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/agent-action-monitor/contracts/gate.openapi.yaml` around lines 1 - 99, The OpenAPI contract leaves POST /v1/gate unauthenticated, matching the current api.py implementation. Preserve this behavior by keeping the operation without a security requirement and do not add an authentication scheme or auth enforcement to the contract.examples/agent-action-monitor/src/dusk/actions/ingest.py (1)
56-63: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winWiden the per-record exception guard.
Only
AdapterErroris caught per record. If an adapter has a latent bug and raises something else (KeyError,AttributeError, etc.) for one malformed record, it escapesingest_fileentirely and aborts the whole batch — undermining the file's own "one bad record is the caller's to skip" design (as documented inadapters/base.py)._load_gate_engine()inapi.pyonly catches(OSError, ValueError), so this would also surface unhandled up through the lazy gate-engine loader.🛡️ Proposed fix
try: actions.append(normalise_record(source, record)) - except AdapterError as exc: + except (AdapterError, Exception) as exc: # noqa: BLE001 logger.warning("Skipping record %d in %s: %s", index, path, exc)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/agent-action-monitor/src/dusk/actions/ingest.py` around lines 56 - 63, Widen the per-record exception handler in ingest_file around normalise_record so any unexpected Exception is logged and skipped for that record, while preserving the existing AdapterError warning context and batch processing behavior. Do not catch BaseException subclasses such as interrupts or system exits.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@examples/agent-action-monitor/agent-demo/bedrock_client.py`:
- Around line 17-20: Update the verdict reason handling in the exception class
__init__ to treat an explicitly null reasons value like a missing value before
joining it. Preserve the existing comma-joined message for provided reason lists
and the “no reason given” fallback when reasons is absent or null.
In `@examples/agent-action-monitor/agent-demo/harness.py`:
- Around line 84-93: Update run_scenario_or_raise to raise DuskBlockedError only
for a real BLOCK verdict, while returning the result for WOULD-BLOCK so callers
preserve its applied action outcome. Keep ALLOW and NO_ACTION behavior
unchanged, and add coverage in test_harness.py for the WOULD-BLOCK
exception-based flow.
In `@examples/agent-action-monitor/compose.yml`:
- Around line 1-2: Update the compose configuration’s DUSK_ENFORCE environment
handling so values from .env can enable enforce mode instead of being overridden
by a hard-coded value. Preserve the documented default behavior when
DUSK_ENFORCE is unset, while forwarding the resolved setting to the container.
In `@examples/agent-action-monitor/docs/gate-latency-notes.md`:
- Around line 97-100: Update the watch-mode delivery statement in the
correctness summary to reflect that WOULD-BLOCK actions are forwarded, including
to /apply, and may be applied. Remove the inaccurate claim that poisoned actions
were never applied while preserving the accurate ALLOW delivery details.
In `@examples/agent-action-monitor/docs/sie-primitives.md`:
- Around line 21-27: The SIE request-path documentation incorrectly excludes
sie_encode from /v1/gate. Update
examples/agent-action-monitor/docs/gate-latency-notes.md lines 73-75 to describe
the _find_similar_decisions() and _record_decision() flow, including
embed_text()/sie_encode, and keep
examples/agent-action-monitor/docs/sie-primitives.md lines 21-27 aligned with
that behavior; both sites require documentation updates.
In `@examples/agent-action-monitor/scripts/vulture_whitelist.py`:
- Around line 25-29: Update the SimilarDecision.similarity entry in
vulture_whitelist.py to resolve similarity through an instance rather than a
class-level attribute, using a construct that is safe to evaluate during module
import and preserves the whitelist’s purpose.
In `@examples/agent-action-monitor/src/dusk/actions/baseline.py`:
- Around line 26-42: Update target_tokens and _flatten_scalars to enforce a
request-driven width limit in addition to the existing recursion depth limit.
Track the number of scanned nodes or materialized tokens with a shared cap, stop
traversal before the cap is exceeded, and preserve the current lowercase scalar
extraction behavior for inputs within the limit.
In `@examples/agent-action-monitor/src/dusk/actions/event.py`:
- Around line 74-75: Update the target validation in the event action’s
validation method to reject control characters using the same check already
applied to agent_id. Preserve the existing non-empty-string validation and raise
the established validation error for invalid target values.
In `@examples/agent-action-monitor/src/dusk/api.py`:
- Around line 18-21: Restrict CORS in the Flask app initialization so it is not
wildcard-enabled across every route, and add authentication checks to the
/v1/gate endpoint, preserving /health only if it is intentionally public. Update
the gate API contract alongside the implementation to document the required
security scheme, using the existing app configuration and route definitions as
the integration points.
In `@examples/agent-action-monitor/src/dusk/config.py`:
- Line 102: Constrain the score-like configuration thresholds, including
gate_block_threshold and the related settings in the configuration definitions
around them, to the inclusive range [0, 1]. Ensure values above 1.0 or below 0.0
raise the existing ConfigError during configuration validation rather than
allowing the gate to silently degrade.
In `@examples/agent-action-monitor/src/dusk/trace/n8n_client.py`:
- Around line 24-30: Synchronize lazy initialization in _get_executor by adding
the module’s existing locking pattern around the _executor check-and-set,
matching the double-checked locking used for _gate_engine. Ensure concurrent
first calls create and retain only one ThreadPoolExecutor while subsequent calls
continue returning the shared instance.
In `@examples/agent-action-monitor/src/dusk/trace/vector.py`:
- Around line 181-190: Move _record_sie_success() in both sie_score and
sie_extract so it executes only after response entries are fully parsed and the
result is successfully constructed. Remove the current success calls immediately
after fetching the raw payload, preserving the existing exception path where
malformed entries call _record_sie_failure() and return None.
- Around line 107-123: The SIE integration creates an unclosed client for every
call. Update _sie_client, sie_encode, sie_score, and sie_extract to reuse one
cached SIEClient instance, invalidate that cache when configuration reloads, and
close the client during reset/shutdown using its documented
cleanup/context-manager behavior.
---
Nitpick comments:
In `@examples/agent-action-monitor/contracts/gate.openapi.yaml`:
- Around line 1-99: The OpenAPI contract leaves POST /v1/gate unauthenticated,
matching the current api.py implementation. Preserve this behavior by keeping
the operation without a security requirement and do not add an authentication
scheme or auth enforcement to the contract.
In `@examples/agent-action-monitor/src/dusk/actions/ingest.py`:
- Around line 56-63: Widen the per-record exception handler in ingest_file
around normalise_record so any unexpected Exception is logged and skipped for
that record, while preserving the existing AdapterError warning context and
batch processing behavior. Do not catch BaseException subclasses such as
interrupts or system exits.
In `@examples/agent-action-monitor/tests/test_actions_gate.py`:
- Around line 267-268: Remove the duplicate identical assertion in the test
around extracted.reasons, leaving a single check that no reason contains “SIE
extract.”
In `@examples/agent-action-monitor/tests/test_actions_offense_memory.py`:
- Around line 144-167: The tests rely on fixed sleep durations for
background-thread scheduling and can be flaky under CI contention. In
examples/agent-action-monitor/tests/test_actions_offense_memory.py:144-167,
update test_record_does_not_block_on_a_slow_disk_write to use a synchronization
event before the delayed Path.replace call, or widen the delay substantially
relative to the elapsed-time assertion. In
examples/agent-action-monitor/tests/test_n8n_client.py:125-163, update the test
around _blocking_send to signal when the worker enters the first send and wait
for that signal, or poll _pending_count with a timeout, instead of sleeping for
a fixed duration before asserting the backlog bound.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: c7df84b7-d5a2-4178-b54d-855f0e458ead
⛔ Files ignored due to path filters (1)
examples/agent-action-monitor/docs/architecture.svgis excluded by!**/*.svg
📒 Files selected for processing (70)
examples/README.mdexamples/agent-action-monitor/.dockerignoreexamples/agent-action-monitor/.env.exampleexamples/agent-action-monitor/.gitignoreexamples/agent-action-monitor/Dockerfileexamples/agent-action-monitor/README.mdexamples/agent-action-monitor/agent-demo/Dockerfileexamples/agent-action-monitor/agent-demo/bedrock_client.pyexamples/agent-action-monitor/agent-demo/harness.pyexamples/agent-action-monitor/agent-demo/load_driver.pyexamples/agent-action-monitor/agent-demo/mock_bedrock.pyexamples/agent-action-monitor/agent-demo/requirements.txtexamples/agent-action-monitor/agent-demo/run_scenario.pyexamples/agent-action-monitor/agent-demo/stub_gate.pyexamples/agent-action-monitor/agent-demo/test_bedrock_client.pyexamples/agent-action-monitor/agent-demo/test_harness.pyexamples/agent-action-monitor/agent-demo/test_load_driver.pyexamples/agent-action-monitor/agent-demo/test_mock_bedrock.pyexamples/agent-action-monitor/agent-demo/test_stub_gate.pyexamples/agent-action-monitor/compose.ymlexamples/agent-action-monitor/contracts/gate.openapi.yamlexamples/agent-action-monitor/docs/action-schema.mdexamples/agent-action-monitor/docs/gate-latency-notes.mdexamples/agent-action-monitor/docs/sie-primitives.mdexamples/agent-action-monitor/lab/actions/generate_actions.pyexamples/agent-action-monitor/mock-prod/Dockerfileexamples/agent-action-monitor/mock-prod/app.pyexamples/agent-action-monitor/mock-prod/requirements.txtexamples/agent-action-monitor/mock-prod/test_app.pyexamples/agent-action-monitor/n8n/Dockerfileexamples/agent-action-monitor/n8n/docker-entrypoint.shexamples/agent-action-monitor/n8n/dusk-webhooks.jsonexamples/agent-action-monitor/pyproject.tomlexamples/agent-action-monitor/sample-data/baseline.jsonexamples/agent-action-monitor/sample-data/check-mixed.jsonexamples/agent-action-monitor/scripts/vulture_whitelist.pyexamples/agent-action-monitor/src/dusk/__init__.pyexamples/agent-action-monitor/src/dusk/actions/__init__.pyexamples/agent-action-monitor/src/dusk/actions/adapters/__init__.pyexamples/agent-action-monitor/src/dusk/actions/adapters/azure.pyexamples/agent-action-monitor/src/dusk/actions/adapters/base.pyexamples/agent-action-monitor/src/dusk/actions/adapters/bedrock.pyexamples/agent-action-monitor/src/dusk/actions/adapters/generic.pyexamples/agent-action-monitor/src/dusk/actions/analyse.pyexamples/agent-action-monitor/src/dusk/actions/baseline.pyexamples/agent-action-monitor/src/dusk/actions/event.pyexamples/agent-action-monitor/src/dusk/actions/ingest.pyexamples/agent-action-monitor/src/dusk/actions/normaliser.pyexamples/agent-action-monitor/src/dusk/actions/offense_memory.pyexamples/agent-action-monitor/src/dusk/actions/verdict.pyexamples/agent-action-monitor/src/dusk/api.pyexamples/agent-action-monitor/src/dusk/config.pyexamples/agent-action-monitor/src/dusk/trace/__init__.pyexamples/agent-action-monitor/src/dusk/trace/models.pyexamples/agent-action-monitor/src/dusk/trace/n8n_client.pyexamples/agent-action-monitor/src/dusk/trace/vector.pyexamples/agent-action-monitor/tests/conftest.pyexamples/agent-action-monitor/tests/integration/__init__.pyexamples/agent-action-monitor/tests/integration/test_gate_api.pyexamples/agent-action-monitor/tests/test_actions_azure.pyexamples/agent-action-monitor/tests/test_actions_bedrock.pyexamples/agent-action-monitor/tests/test_actions_event.pyexamples/agent-action-monitor/tests/test_actions_gate.pyexamples/agent-action-monitor/tests/test_actions_generic.pyexamples/agent-action-monitor/tests/test_actions_ingest.pyexamples/agent-action-monitor/tests/test_actions_offense_memory.pyexamples/agent-action-monitor/tests/test_config.pyexamples/agent-action-monitor/tests/test_n8n_client.pyexamples/agent-action-monitor/tests/test_sie_live_benchmark.pyexamples/agent-action-monitor/tests/test_trace_vector.py
| Correctness held throughout: every `ALLOW` reached `mock-prod` (confirmed | ||
| via its `/log`, 46 applied actions across this run and earlier manual | ||
| checks) and every poisoned action was `WOULD-BLOCK` in watch mode, never | ||
| applied. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Correct the watch-mode delivery claim.
WOULD-BLOCK is intentionally forwarded in watch mode, including to /apply; therefore poisoned actions were not “never applied” in this run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/agent-action-monitor/docs/gate-latency-notes.md` around lines 97 -
100, Update the watch-mode delivery statement in the correctness summary to
reflect that WOULD-BLOCK actions are forwarded, including to /apply, and may be
applied. Remove the inaccurate claim that poisoned actions were never applied
while preserving the accurate ALLOW delivery details.
| - **encode** -- `src/dusk/trace/vector.py`'s `sie_encode()` (wrapped by | ||
| `embed_text()`), called live by `/v1/gate` (`src/dusk/api.py`): once per | ||
| request to embed the incoming action, and once per verdict to record it | ||
| for future lookups. `find_similar_cached()` compares the fresh query | ||
| embedding against a bounded, pre-embedded history (capped at 200 entries) | ||
| to populate the response's `similar_decision_ids`, without re-embedding | ||
| that history on every call. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
ast-grep outline examples/agent-action-monitor/src/dusk/api.py --items all
ast-grep outline examples/agent-action-monitor/src/dusk/trace/vector.py --items all
rg -n -C3 'sie_encode|embed_text|sie_score|sie_extract|find_similar_cached' \
examples/agent-action-monitor/src/dusk/api.py \
examples/agent-action-monitor/src/dusk/trace/vector.pyRepository: superlinked/sie
Length of output: 13541
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== api.py relevant sections =="
sed -n '90,185p' examples/agent-action-monitor/src/dusk/api.py
sed -n '185,215p' examples/agent-action-monitor/src/dusk/api.py
echo
echo "== vector.py relevant sections =="
sed -n '125,152p' examples/agent-action-monitor/src/dusk/trace/vector.py
sed -n '340,385p' examples/agent-action-monitor/src/dusk/trace/vector.py
echo
echo "== doc relevant sections =="
sed -n '1,35p' examples/agent-action-monitor/docs/sie-primitives.md
sed -n '60,82p' examples/agent-action-monitor/docs/gate-latency-notes.md
echo
echo "== call-path probe =="
python3 - <<'PY'
from pathlib import Path
import ast
api_path = Path("examples/agent-action-monitor/src/dusk/api.py")
vec_path = Path("examples/agent-action-monitor/src/dusk/trace/vector.py")
api_tree = ast.parse(api_path.read_text())
vec_tree = ast.parse(vec_path.read_text())
imported_by = {}
for node in ast.walk(vec_tree):
if isinstance(node, ast.FunctionDef) and node.name == "embed_text":
for stmt in node.body:
if isinstance(stmt, ast.Return) and isinstance(stmt.value, ast.BinOp):
imported_by["embed_text"] = stmt.value.left
for node in ast.walk(api_tree):
if isinstance(node, ast.FunctionDef):
body = ast.unparse(node)
for sym in ["evaluate_gate_action", "_find_similar_decisions", "_record_decision"]:
if node.name == sym:
print(f"{node.name}:")
print(" calls _find_similar_decisions:", "_find_similar_decisions" in body)
print(" calls _record_decision:", "_record_decision(" in body)
print(" imports embed_text:", "from dusk.trace.vector import embed_text" in body)
print(" imports find_similar_cached:", "find_similar_cached" in body)
print(" calls embed_text:", "embed_text(" in body)
print(" calls find_similar_cached:", "find_similar_cached(" in body)
print("embed_text implementation calls:", ast.unparse(imported_by.get("embed_text")))
PYRepository: superlinked/sie
Length of output: 11392
Align the SIE request-path notes with the encode call.
/v1/gate routes through _find_similar_decisions() and _record_decision(), and _record_decision() calls embed_text() before appending the pre-embedded decision. Update examples/agent-action-monitor/docs/gate-latency-notes.md#L73-L75 so it no longer says /v1/dusk/gate only calls sie_score/sie_extract, and that sie_encode is “not on this request path.”
📍 Affects 2 files
examples/agent-action-monitor/docs/sie-primitives.md#L21-L27(this comment)examples/agent-action-monitor/docs/gate-latency-notes.md#L73-L75
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/agent-action-monitor/docs/sie-primitives.md` around lines 21 - 27,
The SIE request-path documentation incorrectly excludes sie_encode from
/v1/gate. Update examples/agent-action-monitor/docs/gate-latency-notes.md lines
73-75 to describe the _find_similar_decisions() and _record_decision() flow,
including embed_text()/sie_encode, and keep
examples/agent-action-monitor/docs/sie-primitives.md lines 21-27 aligned with
that behavior; both sites require documentation updates.
| app = Flask(__name__) | ||
| CORS(app) | ||
| # Bound public input without constraining normal AgentAction payloads. | ||
| app.config["MAX_CONTENT_LENGTH"] = 1 * 1024 * 1024 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
No authentication and permissive default CORS on the gate endpoint.
CORS(app) with no resources=/origins= restricts nothing — flask-cors applies a wildcard-origin policy to every route by default. Combined with no auth check anywhere on /v1/gate or /health, any reachable client (including cross-origin browser requests) can query verdicts or flood the gate. For a service whose stated purpose is gating privileged control-plane actions, this is worth locking down before any non-local deployment. See the consolidated note (shared with contracts/gate.openapi.yaml, which also documents no security scheme).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/agent-action-monitor/src/dusk/api.py` around lines 18 - 21, Restrict
CORS in the Flask app initialization so it is not wildcard-enabled across every
route, and add authentication checks to the /v1/gate endpoint, preserving
/health only if it is intentionally public. Update the gate API contract
alongside the implementation to document the required security scheme, using the
existing app configuration and route definitions as the integration points.
| entries = result["scores"] if isinstance(result, dict) else getattr(result, "scores", None) | ||
| _record_sie_success() | ||
| if not entries: | ||
| return None | ||
| score_by_id = {str(e["item_id"]): _sigmoid(float(e["score"])) for e in entries} | ||
| return [score_by_id.get(str(i), 0.0) for i in range(len(candidates))] | ||
| except Exception as exc: # noqa: BLE001 | ||
| _record_sie_failure() | ||
| logger.warning("SIE score failed: %s", exc) | ||
| return None |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
Success recorded before response parsing finishes.
In both sie_score and sie_extract, _record_sie_success() fires right after the raw payload is fetched but before its entries are parsed. If a subsequent malformed entry (missing "item_id"/"text" key) raises, it's caught by the same except Exception block and calls _record_sie_failure() — recording both a success and a failure for the one call, which skews the circuit breaker's failure count.
Move _record_sie_success() to after parsing completes successfully.
Also applies to: 220-246
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@examples/agent-action-monitor/src/dusk/trace/vector.py` around lines 181 -
190, Move _record_sie_success() in both sie_score and sie_extract so it executes
only after response entries are fully parsed and the result is successfully
constructed. Remove the current success calls immediately after fetching the raw
payload, preserving the existing exception path where malformed entries call
_record_sie_failure() and return None.
…xample Fixes bugs introduced by the previous commit: - event.py's target field was missing the control-character check its own docstring claimed it had - vector.py recorded a SIE call as successful before its response finished parsing, so a malformed entry double counted as both a success and a failure and skewed the circuit breaker - vulture_whitelist.py referenced a dataclass field with no defaults through the class instead of an instance, which raises AttributeError if the file is ever actually executed Also addresses pre-existing findings surfaced by the same review pass: - run_scenario_or_raise no longer raises for WOULD-BLOCK, since watch mode already applied the action and raising hid that from the caller - DuskBlockedError falls back correctly when reasons is explicitly None, not only when the key is missing - DUSK_ENFORCE is now overridable through .env like the other compose vars - gate_block_threshold is now bounded to <= 1.0, since analyse.py always caps the score to 1.0 and a threshold above it would silently make the gate unable to refuse anything - the n8n webhook executor's lazy singleton now uses the same double-checked locking as the gate engine's, closing a race where two concurrent first calls could each build their own executor - feature extraction now bounds traversal width, not just recursion depth - ingest_file skips any malformed record, not only ones raising AdapterError, matching its own documented one-bad-record contract - the SIE client is now cached per (endpoint, api key, timeout) instead of rebuilt on every single call, and closed when the cache is invalidated - a duplicate assertion and two sleep-based flaky test patterns are cleaned up in favour of explicit synchronization Corrects gate-latency-notes.md, which claimed sie_encode was not on the /v1/gate request path; it is, via _find_similar_decisions/_record_decision.
There was a problem hiding this comment.
♻️ Duplicate comments (1)
examples/agent-action-monitor/src/dusk/actions/baseline.py (1)
35-36: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick winMake the extraction bounds limit work, not only retained output.
examples/agent-action-monitor/src/dusk/actions/baseline.py#L35-L36:findall()builds every match before slicing to_MAX_TOKENS, so repetitive targets can still allocate an unbounded match list. Use lazyfinditer()withislice()or an explicitly capped loop.examples/agent-action-monitor/src/dusk/actions/baseline.py#L52-L60: oncebudgetreaches zero, the enclosingdict/listloops still enumerate remaining items. Also, depth-rejected children do not consume budget, allowing wide containers at the depth boundary to be fully scanned. Decrement before the depth check and break parent loops when the budget is exhausted.Proposed fix
+from itertools import islice + - return set(_TOKEN_RE.findall(target.lower())[:_MAX_TOKENS]) + return { + match.group(0) + for match in islice(_TOKEN_RE.finditer(target.lower()), _MAX_TOKENS) + } - if _depth > 10 or budget[0] <= 0: + if budget[0] <= 0: return budget[0] -= 1 + if _depth > 10: + return if isinstance(payload, dict): for value in payload.values(): + if budget[0] <= 0: + break _flatten_scalars(value, values, budget, _depth=_depth + 1) elif isinstance(payload, list): for value in payload: + if budget[0] <= 0: + break _flatten_scalars(value, values, budget, _depth=_depth + 1)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@examples/agent-action-monitor/src/dusk/actions/baseline.py` around lines 35 - 36, The token extraction near baseline.py lines 35-36 must cap regex work, not just the resulting set: replace findall slicing with lazy finditer consumption limited to _MAX_TOKENS. In the container traversal near lines 52-60, decrement budget before checking depth, and break enclosing dict/list loops as soon as budget reaches zero; apply these changes at both cited sites in the same file.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Duplicate comments:
In `@examples/agent-action-monitor/src/dusk/actions/baseline.py`:
- Around line 35-36: The token extraction near baseline.py lines 35-36 must cap
regex work, not just the resulting set: replace findall slicing with lazy
finditer consumption limited to _MAX_TOKENS. In the container traversal near
lines 52-60, decrement budget before checking depth, and break enclosing
dict/list loops as soon as budget reaches zero; apply these changes at both
cited sites in the same file.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: b7ff88d0-9b25-4f9e-a736-14119b8290d1
📒 Files selected for processing (17)
examples/agent-action-monitor/agent-demo/bedrock_client.pyexamples/agent-action-monitor/agent-demo/harness.pyexamples/agent-action-monitor/agent-demo/test_harness.pyexamples/agent-action-monitor/compose.ymlexamples/agent-action-monitor/docs/gate-latency-notes.mdexamples/agent-action-monitor/scripts/vulture_whitelist.pyexamples/agent-action-monitor/src/dusk/actions/baseline.pyexamples/agent-action-monitor/src/dusk/actions/event.pyexamples/agent-action-monitor/src/dusk/actions/ingest.pyexamples/agent-action-monitor/src/dusk/config.pyexamples/agent-action-monitor/src/dusk/trace/n8n_client.pyexamples/agent-action-monitor/src/dusk/trace/vector.pyexamples/agent-action-monitor/tests/conftest.pyexamples/agent-action-monitor/tests/test_actions_gate.pyexamples/agent-action-monitor/tests/test_actions_offense_memory.pyexamples/agent-action-monitor/tests/test_config.pyexamples/agent-action-monitor/tests/test_n8n_client.py
💤 Files with no reviewable changes (1)
- examples/agent-action-monitor/tests/test_actions_gate.py
🚧 Files skipped from review as they are similar to previous changes (14)
- examples/agent-action-monitor/scripts/vulture_whitelist.py
- examples/agent-action-monitor/src/dusk/actions/event.py
- examples/agent-action-monitor/compose.yml
- examples/agent-action-monitor/src/dusk/trace/n8n_client.py
- examples/agent-action-monitor/agent-demo/harness.py
- examples/agent-action-monitor/src/dusk/actions/ingest.py
- examples/agent-action-monitor/tests/test_config.py
- examples/agent-action-monitor/agent-demo/bedrock_client.py
- examples/agent-action-monitor/tests/test_actions_offense_memory.py
- examples/agent-action-monitor/tests/test_n8n_client.py
- examples/agent-action-monitor/docs/gate-latency-notes.md
- examples/agent-action-monitor/agent-demo/test_harness.py
- examples/agent-action-monitor/src/dusk/config.py
- examples/agent-action-monitor/src/dusk/trace/vector.py
…eline.py target_tokens() used findall()[:_MAX_TOKENS], which still builds the full match list before the slice runs, so a repetitive target could force an unbounded match list even though only the first _MAX_TOKENS ever reached the returned set. Switched to finditer() + islice() so the regex work itself is bounded. _flatten_scalars() checked its budget before decrementing for depth, so a depth-rejected node never consumed budget, and the enclosing dict/list loops kept enumerating every remaining item as a wasted no-op call once the budget hit zero instead of stopping. Now decrements before the depth check and breaks out of the loops as soon as the budget is exhausted.
|
Updating this comment now that CodeRabbit has also weighed in and its findings are addressed too. Three commits pushed since the review: rebased onto main, fixed everything below, and cleared CodeRabbit's own two follow-up passes (the second push introduced two small regressions of its own, which its next pass caught and are now fixed too). Main blocker (benchmark). Rebuilt it so it actually demonstrates SIE is load bearing instead of being unable to show a difference by construction. Added held out negatives (legitimate actions the baseline never trained on, not the training set replayed as its own test) and a new attack built to evade every deterministic check: it reuses a known action type and target class for its agent, and its new tokens/values are kept deliberately outside the static sensitive frozenset. A new test scores that one attack twice, once with real SIE calls and once with them forced off, and asserts the score lands below Out-of-the-box fixes:
Engine fixes:
CodeRabbit's findings, in short (two passes, both now clean):
Scope. Left the example self-contained rather than splitting the engine into its own repo. The engine at DUSK's own repo root has already diverged from what lives in this example, and Full test suite passes (189 passed, 3 skipped, the skips being the live SIE benchmark with no cluster reachable in that run), verified both with and without |
|
Hi @fm1320 Following up. All of the feedback above has been addressed: Rebuilt the SIE benchmark with held out negatives and an attack that evades the deterministic rules, caught only via extract or rerank, so the score now visibly lands on opposite sides of the block threshold with SIE on versus off. Fixed all six engine issues (repeat offense decay, timestamp validation, OSError handling, SIE stall circuit breaker, agent_id control characters, byte bounded caps). Fixed the five out of the box items (.env wiring, Bedrock model ID, cold start docs, without Docker flow, n8n diagnostics flag). Rebased and resolved the conflict with main. Also worked through CodeRabbit's automated findings across two follow up passes. Still owe you a proper answer on the scope question, thin integration layer versus engine in our repo. Will follow up here separately rather than bundle it with this fix pass. Ready for another look whenever you have a chance. |
Summary
Adds a new example at
examples/agent-action-monitor/. A behavioural gate for AI agent actions, built on DUSK, an open source behavioural threat detection project. The core idea: a hijacked AI agent still holds valid credentials, so a credential check waves it through. This gate asks a different question, does this agent normally do this, by scoring a proposed control-plane action (a firewall rule change, a route change, a role grant) against that agent's own learned baseline before the action reaches a downstream system.SIE is optional and degrades gracefully throughout the request path. Semantic novelty and qualifying extraction results can increase, but never reduce, the deterministic anomaly score.
extractuses GLiNER with the labels role, privilege, resource, segment, and port. Confident, previously unknown role or privilege entities contribute an additional bounded anomaly signal; neutral resource, segment, and port entities do not add score by themselves.encode(BAAI/bge-m3) embeds each recorded verdict once. When sufficient history exists, it also embeds the incoming action for the bounded fleet-wide similar-decision lookup. It does not contribute directly to the anomaly score.score(BAAI/bge-reranker-v2-m3) reranks the acting agent's learned baseline history to detect semantic novelty and separately reranks the fleet-wide similar-decision shortlist.Repeat-offense memory persists refused verdicts per agent. A matching later attempt receives a bounded, decaying contribution and cites the earlier trace ID. Storage is capped at 50 offenses per agent and 500 tracked agents.
Also adds a row to
examples/README.md's gallery table.Architecture
Five services on one Docker Compose stack, no API key required by default:
dusk-gate(the Flask/v1/gateservice),sie(self-hosted, CPU default image),n8n(three webhooks, decision, report, alert, baked in and active from container start),mock-prod(a dummy downstream target), andagent-demo(a Bedrock or mock agent harness that runs a clean and a poisoned scenario against the gate, then exits).Watch mode is the default: a refused action is logged as
WOULD-BLOCKbut still reachesmock-prod, since an inline gate that wrongly blocks a legitimate action can disrupt a network. Enforce mode upgradesWOULD-BLOCKtoBLOCK, which stops the action before it reachesmock-prod.Run it
cd examples/agent-action-monitor docker compose upBrings up the full stack.
agent-demoruns a clean and a poisoned scenario against the gate automatically, then exits. See the example's own README for the full walkthrough, watch versus enforce mode, and the n8n webhook views.Validated end to end
Documented in the example's own
docs/sie-primitives.md, from a run against Superlinked's hosted tester cluster:sie_encodereturns a real 1024-dimensional dense vector fromBAAI/bge-m3, confirming the model actually loaded and servedtest_offense_memory_persists_across_a_simulated_restart, which restarts the gate engine and confirms a repeat offense is still cited afterwardTest plan
docker compose upstarts the full stack; health-gated services become healthy andagent-democompletes both scenarios successfullyagent-demo's clean scenario returnsALLOWand reachesmock-prodagent-demo's poisoned scenario returnsWOULD-BLOCKin watch mode and still reachesmock-prodDUSK_ENFORCE=trueondusk-gateturns the same poisoned scenario intoBLOCK, and it no longer reachesmock-prodpytestpasses standalone from a freshpip install -e ".[dev]"of this directory alone, 186 passed, 2 skippedruff check,mypy, andbanditcleanNotes and caveats
/v1/gateis unauthenticated with CORS open to all origins by default, appropriate for a local example, not a production security boundaryBuilt by Ritik Sah and Tanvir Farhad.
Summary by CodeRabbit