Skip to content

feat(security): add behavioral session tracker with trifecta detection#965

Closed
gemini2026 wants to merge 13 commits intoNVIDIA:mainfrom
gemini2026:feat/session-tracker
Closed

feat(security): add behavioral session tracker with trifecta detection#965
gemini2026 wants to merge 13 commits intoNVIDIA:mainfrom
gemini2026:feat/session-tracker

Conversation

@gemini2026
Copy link
Copy Markdown

@gemini2026 gemini2026 commented Mar 26, 2026

Closes #964

What this does

Adds behavioral session tracking under nemoclaw/src/security/. Most agent security gates check each tool call in isolation — an agent that reads a secret, fetches untrusted input, and opens an outbound connection across separate actions slips through. This tracker aggregates those capabilities over a session and flags when all three appear together (the "trifecta").

How it works

Three capability classes: read_sensitive, ingested_untrusted, has_egress. Risk goes from cleanelevated (1-2 caps) → critical (all 3).

The integration module hooks into before_tool_call to classify tool invocations automatically:

  • File-reading tools + sensitive path patterns → read_sensitive
  • Fetch/curl/wget → ingested_untrusted
  • POST/send/upload tools → has_egress

On trifecta: logs a warning via api.logger.warn(). Pluggable via onTrifecta callback if you want to block or alert instead.

Sessions clean up on session_end. Events capped at 100 per session (~10 KB). In-memory only — acceptable for ephemeral sandbox sessions.

Wired into register() in index.ts: wireSessionTracker(api).

Files

  • session-tracker.ts — core SessionStore with trifecta detection
  • session-integration.ts — plugin lifecycle hooks + tool classification
  • session-tracker.test.ts — 45 tests
  • session-integration.test.ts — 22 tests
  • docs/reference/session-tracker.md — reference docs
  • One import + one line in index.ts

Test plan

  • 67 tests passing across both suites
  • tsc --noEmit clean
  • All pre-commit hooks pass
  • Trifecta detection, session isolation, event cap boundary, deduplication
  • Integration: tool classification, hook registration, trifecta warning, session cleanup
  • Edge cases: empty sessionId, non-matching tools, throwing callback, post-clear behavior

Tracks three capability classes per session: read_sensitive,
ingested_untrusted, has_egress. When all three appear (trifecta),
risk escalates to critical, detecting multi-step exfiltration
attacks that per-action gates miss.
Align getExposure with getCapabilities and hasTrifecta by adding
an explicit empty-string guard. Add corresponding test case.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an in-memory SessionStore that records per-session capability events (read_sensitive, ingested_untrusted, has_egress), classifies session risk (clean/elevated/critical), detects the "trifecta" (all three), and exposes listing and detailed exposure APIs; includes tests and documentation.

Changes

Cohort / File(s) Summary
Session tracker implementation
nemoclaw/src/security/session-tracker.ts
New exported enums/types and SessionStore with record, clear, getCapabilities, hasTrifecta, listSessions, getExposure. Uses an in-memory Map per session, caps stored events at 100 (further events dropped while capability flags still update), detects trifecta once and optionally invokes an onTrifecta callback, and implements exposure aggregation (deduplication for sensitive files and external URLs; no dedupe for egress attempts).
Tests for session tracker
nemoclaw/src/security/session-tracker.test.ts
New Vitest suite validating recording semantics, capability retrieval (returns copy), trifecta detection and one-time callback firing, risk-level derivation, event cap behavior (100 retained, 101st dropped), session isolation, clear() lifecycle, and getExposure formatting/deduplication rules.
Documentation
docs/reference/session-tracker.md
New reference page describing capability classes, trifecta/risk definitions, CapabilityEvent structure, 100-event cap and in-memory reset behavior, and full exported TypeScript API and types (Capability, RiskLevel, CapabilityEvent, SessionSummary, SessionExposure, SessionStore).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I nibble logs and count each trace,

a file, a fetch, a quiet outbound race.
A hundred crumbs I tuck away with care,
flags whisper true — then trifecta flares.
I thump my foot: alert, nimble, and aware.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(security): add behavioral session tracker with trifecta detection' accurately reflects the main change—adding a session tracking system with trifecta detection for multi-step exfiltration attacks.
Linked Issues check ✅ Passed All coding requirements from issue #964 are fully implemented: session tracker module with three capability classes (read_sensitive, ingested_untrusted, has_egress), trifecta detection, risk classification (clean/elevated/critical), complete Session Exposure API, 100-event limit, comprehensive Vitest tests (35 tests), and documentation.
Out of Scope Changes check ✅ Passed All changes are within scope—three new files under nemoclaw/src/security/ (session-tracker.ts implementation and tests), documentation, and no modifications to existing NemoClaw internals as specified in issue #964.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
docs/reference/session-tracker.md (2)

3-3: Align the H1 with title.page.

The H1 does not match the frontmatter page title.

As per coding guidelines: "H1 heading matches the title.page frontmatter value."

Also applies to: 21-21

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` at line 3, Update the document so the
top-level H1 matches the frontmatter title.page value ("Session Tracker —
Behavioral Trifecta Detection"): locate the frontmatter key title.page and
replace or edit the existing H1 heading (the leading "# ..." line) to exactly
match that value, ensuring punctuation and capitalization are identical; also
check the second occurrence (line 21) mentioned and make it consistent with
title.page as well.

6-8: Fix product-name casing in frontmatter metadata.

Use NemoClaw, OpenClaw, and OpenShell casing consistently (including keywords/tags).

As per coding guidelines: "NemoClaw, OpenClaw, and OpenShell must use correct casing."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` around lines 6 - 8, The frontmatter arrays
keywords and tags use incorrect product-name casing—update occurrences of
"nemoclaw" to "NemoClaw", "openclaw" to "OpenClaw", and "openshell" to
"OpenShell" in the keywords and tags entries in
docs/reference/session-tracker.md (the frontmatter keys: keywords and tags) so
the metadata uses the correct product-name casing consistently.
nemoclaw/src/security/session-tracker.test.ts (1)

121-150: Consolidate duplicated event-cap tests.

These two blocks validate the same boundary behavior (100 stored, 101st dropped). Merging them into one parameterized/compact suite would keep intent while reducing maintenance overhead.

Also applies to: 329-349

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/security/session-tracker.test.ts` around lines 121 - 150, The
two test blocks under the "event cap" suite duplicate boundary checks for
storing exactly 100 events and dropping the 101st; consolidate them by creating
a single parameterized/compact test that covers: (1) inserting N events where
N=100 asserts exposure.events length is 100 and last event detail is "/file-99",
(2) inserting N+1 events where the 101st is dropped, and (3) after the log is
full a subsequent store.record("s1", Capability.HasEgress, ...) still flips
getCapabilities("s1")[Capability.HasEgress] to true while exposure.events
remains length 100; reuse store.record, getExposure and getCapabilities calls
and remove the duplicated block (the similar tests later in the file) so the
suite tests the boundary once with clear parameterization.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/reference/session-tracker.md`:
- Around line 95-96: Update the docs for getExposure() to state that it returns
null not only for unknown sessions but also when an empty sessionId is passed;
specifically mention the empty-sessionId null behavior so it matches the
implementation of getExposure(sessionId) and references the parameter name
sessionId and the function getExposure().
- Line 101: The sentence about egressAttempts is misleading because the
implementation emits just the tool name when detail is empty; update the wording
to state that egressAttempts records every has_egress event as "tool + ' ' +
detail" when detail is non-empty, and as just the tool name when detail === ""
(i.e., no trailing space), referencing the egressAttempts array and the
has_egress event/detail variable to make the conditional behavior explicit.

In `@nemoclaw/src/security/session-tracker.ts`:
- Around line 222-223: getExposure currently shallow-copies the session.events
array (const eventsCopy: CapabilityEvent[] = [...sess.events]) which still
exposes the internal CapabilityEvent objects to external mutation; change this
to return deep copies of each event instead — for example, replace the shallow
spread with a per-item clone (e.g., map and shallow-clone each event object with
object spread or use structuredClone/JSON deep-clone if events contain nested
state) so callers of getExposure cannot mutate sess.events; update references in
getExposure and any tests to use the cloned events.

---

Nitpick comments:
In `@docs/reference/session-tracker.md`:
- Line 3: Update the document so the top-level H1 matches the frontmatter
title.page value ("Session Tracker — Behavioral Trifecta Detection"): locate the
frontmatter key title.page and replace or edit the existing H1 heading (the
leading "# ..." line) to exactly match that value, ensuring punctuation and
capitalization are identical; also check the second occurrence (line 21)
mentioned and make it consistent with title.page as well.
- Around line 6-8: The frontmatter arrays keywords and tags use incorrect
product-name casing—update occurrences of "nemoclaw" to "NemoClaw", "openclaw"
to "OpenClaw", and "openshell" to "OpenShell" in the keywords and tags entries
in docs/reference/session-tracker.md (the frontmatter keys: keywords and tags)
so the metadata uses the correct product-name casing consistently.

In `@nemoclaw/src/security/session-tracker.test.ts`:
- Around line 121-150: The two test blocks under the "event cap" suite duplicate
boundary checks for storing exactly 100 events and dropping the 101st;
consolidate them by creating a single parameterized/compact test that covers:
(1) inserting N events where N=100 asserts exposure.events length is 100 and
last event detail is "/file-99", (2) inserting N+1 events where the 101st is
dropped, and (3) after the log is full a subsequent store.record("s1",
Capability.HasEgress, ...) still flips
getCapabilities("s1")[Capability.HasEgress] to true while exposure.events
remains length 100; reuse store.record, getExposure and getCapabilities calls
and remove the duplicated block (the similar tests later in the file) so the
suite tests the boundary once with clear parameterization.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a4fe7a16-7e3b-4730-8713-6e296e3f559e

📥 Commits

Reviewing files that changed from the base of the PR and between f0f53e4 and a17e65d.

📒 Files selected for processing (3)
  • docs/reference/session-tracker.md
  • nemoclaw/src/security/session-tracker.test.ts
  • nemoclaw/src/security/session-tracker.ts

- Deep-copy CapabilityEvent objects in getExposure to prevent
  external mutation of internal session state
- Document getExposure null return for empty sessionId
- Clarify egressAttempts formatting when detail is empty
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/reference/session-tracker.md`:
- Line 3: The H1 heading "# Session Tracker" does not match the frontmatter key
title.page ("Session Tracker — Behavioral Trifecta Detection"); update the H1 so
it exactly matches title.page (including punctuation and spacing) to satisfy the
page-structure rule and ensure the H1 equals title.page.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 209b67da-c891-4e74-a04f-3c003296fd58

📥 Commits

Reviewing files that changed from the base of the PR and between a17e65d and 7a8ee44.

📒 Files selected for processing (2)
  • docs/reference/session-tracker.md
  • nemoclaw/src/security/session-tracker.ts
✅ Files skipped from review due to trivial changes (1)
  • nemoclaw/src/security/session-tracker.ts

Align with the pattern used by other reference pages where H1 is
the short name and title.page adds descriptive context.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
docs/reference/session-tracker.md (1)

21-21: ⚠️ Potential issue | 🟡 Minor

H1 must match title.page exactly.

The H1 heading "Session Tracker" does not match the title.page frontmatter value "Session Tracker — Detect Multi-Step Exfiltration Attacks" (line 3). Update line 21 to include the full title with the em dash and subtitle.

As per coding guidelines, "H1 heading matches the title.page frontmatter value."

📝 Proposed fix
-# Session Tracker
+# Session Tracker — Detect Multi-Step Exfiltration Attacks
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` at line 21, Update the H1 heading so it
exactly matches the frontmatter title.page value: replace the current "Session
Tracker" H1 with "Session Tracker — Detect Multi-Step Exfiltration Attacks"
(ensure you include the em dash and subtitle exactly as in title.page).
🧹 Nitpick comments (1)
docs/reference/session-tracker.md (1)

40-40: Replace colon with em dash or period.

The colon in this sentence introduces an explanation rather than a list. The formatting rule states that colons should only introduce lists. Consider using an em dash or splitting into two sentences.

As per coding guidelines, "Colons should only introduce a list. Flag colons used as general punctuation between clauses."

📝 Suggested alternatives

Option 1: Use em dash

-A trifecta indicates a possible exfiltration chain: read a secret, get instructions from an attacker, and send the secret out.
+A trifecta indicates a possible exfiltration chain — read a secret, get instructions from an attacker, and send the secret out.

Option 2: Two sentences

-A trifecta indicates a possible exfiltration chain: read a secret, get instructions from an attacker, and send the secret out.
+A trifecta indicates a possible exfiltration chain.
+The agent reads a secret, gets instructions from an attacker, and sends the secret out.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` at line 40, Replace the colon in the
sentence "A trifecta indicates a possible exfiltration chain: read a secret, get
instructions from an attacker, and send the secret out." with an em dash or
split into two sentences so it no longer uses a colon as general punctuation;
update the line to either "A trifecta indicates a possible exfiltration chain —
read a secret, get instructions from an attacker, and send the secret out." or
"A trifecta indicates a possible exfiltration chain. It means reading a secret,
getting instructions from an attacker, and sending the secret out."
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/reference/session-tracker.md`:
- Line 21: Update the H1 heading so it exactly matches the frontmatter
title.page value: replace the current "Session Tracker" H1 with "Session Tracker
— Detect Multi-Step Exfiltration Attacks" (ensure you include the em dash and
subtitle exactly as in title.page).

---

Nitpick comments:
In `@docs/reference/session-tracker.md`:
- Line 40: Replace the colon in the sentence "A trifecta indicates a possible
exfiltration chain: read a secret, get instructions from an attacker, and send
the secret out." with an em dash or split into two sentences so it no longer
uses a colon as general punctuation; update the line to either "A trifecta
indicates a possible exfiltration chain — read a secret, get instructions from
an attacker, and send the secret out." or "A trifecta indicates a possible
exfiltration chain. It means reading a secret, getting instructions from an
attacker, and sending the secret out."

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c98d63a2-4938-4370-a148-41e48fce8897

📥 Commits

Reviewing files that changed from the base of the PR and between 7a8ee44 and ce785dc.

📒 Files selected for processing (1)
  • docs/reference/session-tracker.md

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (3)
docs/reference/session-tracker.md (3)

102-102: Use active voice.

The phrase "are not deduplicated" is passive. Rewrite in active voice.

Suggested revision:

-  Egress attempts are not deduplicated.
+  The tracker does not deduplicate egress attempts.

As per coding guidelines, "Active voice required. Flag passive constructions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` at line 102, Replace the passive sentence
"Egress attempts are not deduplicated." with an active-voice version (for
example: "We do not deduplicate egress attempts." or "The system does not
deduplicate egress attempts.") in docs/reference/session-tracker.md so the
statement complies with the project's active-voice guideline.

56-56: Use active voice.

The phrase "are dropped" is passive. Rewrite in active voice.

Suggested revision:

-Events beyond the 100th are dropped, but the capability set continues to update.
+The tracker drops events beyond the 100th, but continues to update the capability set.

As per coding guidelines, "Active voice required. Flag passive constructions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` at line 56, Replace the passive sentence
"Events beyond the 100th are dropped, but the capability set continues to
update." with an active-voice version such as "We drop events beyond the 100th,
but the capability set continues to update." to satisfy the active voice
guideline; locate that exact sentence in session-tracker.md and update it
accordingly.

77-77: Use active voice.

The phrase "are silently ignored" is passive. Rewrite in active voice.

Suggested revision:

-Empty `sessionId` values are silently ignored.
+The method silently ignores empty `sessionId` values.

As per coding guidelines, "Active voice required. Flag passive constructions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` at line 77, Change the passive sentence
about `sessionId` to active voice: locate the sentence containing `sessionId`
and replace "Empty `sessionId` values are silently ignored." with an active
phrasing such as "The system ignores empty `sessionId` values." (or "We ignore
empty `sessionId` values.") to meet the active-voice guideline.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/reference/session-tracker.md`:
- Line 102: Replace the passive sentence "Egress attempts are not deduplicated."
with an active-voice version (for example: "We do not deduplicate egress
attempts." or "The system does not deduplicate egress attempts.") in
docs/reference/session-tracker.md so the statement complies with the project's
active-voice guideline.
- Line 56: Replace the passive sentence "Events beyond the 100th are dropped,
but the capability set continues to update." with an active-voice version such
as "We drop events beyond the 100th, but the capability set continues to
update." to satisfy the active voice guideline; locate that exact sentence in
session-tracker.md and update it accordingly.
- Line 77: Change the passive sentence about `sessionId` to active voice: locate
the sentence containing `sessionId` and replace "Empty `sessionId` values are
silently ignored." with an active phrasing such as "The system ignores empty
`sessionId` values." (or "We ignore empty `sessionId` values.") to meet the
active-voice guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 4040fa89-6063-4cd9-ab1e-d75c361b8ff5

📥 Commits

Reviewing files that changed from the base of the PR and between ce785dc and 282315f.

📒 Files selected for processing (1)
  • docs/reference/session-tracker.md

@wscurran wscurran added enhancement New feature or request security Something isn't secure priority: high Important issue that should be resolved in the next release labels Mar 30, 2026
@wscurran
Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this proposal with a detailed summary, it proposes adding behavioral session tracking to NemoClaw's security toolkit, which could enhance the security of NemoClaw and provide better protection for users.

Copy link
Copy Markdown
Contributor

@prekshivyas prekshivyas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini2026

Review: Behavioral Session Tracker

Thanks for the contribution — the trifecta detection concept (read sensitive + ingest untrusted + egress = exfiltration chain) is a sound security pattern and worth exploring for NemoClaw.

A few things we'd need addressed before this can move forward:

Integration required

The SessionStore is self-contained with no call sites anywhere in NemoClaw. Nobody calls record() today, so the tracker would ship as dead code. For this to provide value, it needs to be wired into the agent execution pipeline — likely hooking into OpenClaw's tool-call lifecycle (beforeToolUse/afterToolUse) or the NemoClaw plugin's command dispatch. Without at least one integration point, there's nothing to review from a security effectiveness standpoint.

No consumer for detection results

The PR notes that "consumer decides what to do when trifecta is detected," but there's no callback, event emitter, or hook mechanism for a consumer to subscribe to. Detection without response is just logging — we'd need at minimum a way to register a handler (e.g., log a warning, emit a metric, terminate the session) when risk escalates.

Session lifecycle management

Sessions are stored in a Map with no TTL, eviction policy, or clear() method. In a long-running gateway process, this will grow unbounded. Would need either:

  • A max session count with LRU eviction, or
  • A TTL-based cleanup (e.g., sessions older than 1h are pruned), or
  • Tying session lifetime to OpenClaw's own session lifecycle

Documentation references non-existent pages

The doc at docs/reference/session-tracker.md links to {doc}/reference/injection-scanner and {doc}/reference/audit-chain — these don't exist in the repo. Please either remove these references or note them as future work.

Minor observations

  • The 100-event cap is reasonable but would benefit from a brief justification (why 100 vs. 50 or 500?)
  • In-memory-only storage means all tracking is lost on container restart — worth calling out explicitly as a known limitation and whether that's acceptable for the threat model
  • classifyRisk() has a redundant path: if count > 2 it returns "critical", but that case is already caught by the trifecta check above it (3 capabilities = trifecta = true)

Suggested path forward

The implementation quality is solid (good types, thorough tests, clean separation). The gap is entirely about integration. Would suggest:

  1. Define where record() gets called (which tool-call hook or middleware)
  2. Define what happens when trifecta is detected (the consumer/response mechanism)
  3. Add session cleanup
  4. Then this becomes a meaningful addition to the security stack

Happy to re-review once there's an integration plan.

prekshivyas and others added 2 commits April 2, 2026 11:57
- Remove dead code: `classifyRisk()` count > 2 branch can never be
  reached because trifecta is checked first and there are exactly
  three capability classes — simplify to a ternary
- Add `onTrifecta` callback to `SessionStore` constructor so callers
  can log a warning, emit a metric, or terminate the session when
  risk escalates; callback fires once per session on first trifecta
- Add `clear()` method to release all tracked state; prevents
  unbounded memory growth in long-running processes
- Consolidate duplicate event-cap / boundary-condition test suites
  into a single test covering all boundary behaviors
- Add tests for `onTrifecta` (fires once, per session, not partial)
  and `clear()` (resets state, allows new sessions)
- Fix passive voice in docs: "are dropped" → "The tracker drops",
  "are silently ignored" → "The method silently ignores",
  "are not deduplicated" → "The tracker does not deduplicate"
- Replace colon with em dash in exfiltration chain sentence
- Add justification for 100-event cap (~10 KB per session)
- Document in-memory-only limitation explicitly
- Annotate Next Steps cross-refs with pending PR numbers (NVIDIA#870/NVIDIA#892)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini2026
Copy link
Copy Markdown
Author

gemini2026 commented Apr 2, 2026

@prekshivyas good call on all of these. Here's what I pushed:

  • Removed the dead count > 2 branch in classifyRisk() (impossible with 3 capability classes)
  • Added onTrifecta constructor callback for consumers to hook into detection
  • Added clear() for session cleanup
  • Justified the 100-event cap in docs (~10 KB/session)
  • Consolidated duplicate tests, added coverage for onTrifecta and clear()

Re: integration — you're right, should've wired it from the start instead of proposing a follow-up. Pushing integration code next that hooks into before_tool_call in register().

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
nemoclaw/src/security/session-tracker.test.ts (1)

92-117: Consider adding a test case for zero capabilities, or document why "clean" is unreachable.

Looking at the implementation, a session is only created when record() is called, which always sets at least one capability. This means the "clean" risk level (count === 0) in classifyRisk() is currently unreachable code.

This isn't necessarily a bug—the API design is valid—but the test suite could benefit from documenting this invariant. Either:

  1. Add a comment explaining that "clean" exists for API completeness or future use
  2. Add a test that explicitly verifies a freshly recorded session is never "clean"
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw/src/security/session-tracker.test.ts` around lines 92 - 117, Add
coverage or documentation for the unreachable "clean" branch in classifyRisk():
either add a test that asserts a session created via store.record(...) is never
classified as "clean" (use store.record, store.listSessions(), and check
session.riskLevel !== "clean") or add an inline comment next to classifyRisk()
(or where sessions are created in record()) stating the invariant that sessions
are only created via record() and thus always have at least one capability;
reference the classifyRisk(), record(), listSessions(), and riskLevel symbols so
reviewers can verify the rationale.
docs/reference/session-tracker.md (1)

59-61: Passive voice detected.

Line 60 uses passive voice ("is lost"). Consider rewriting to active voice.

Suggested revision: "The host process loses all tracking state when it restarts."

📝 Suggested fix
 The tracker holds sessions in memory only.
-All tracking state is lost when the host process restarts.
+The host process loses all tracking state when it restarts.
 This is an acceptable trade-off for ephemeral sandbox sessions, where the threat window is bounded by the container lifetime.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/reference/session-tracker.md` around lines 59 - 61, Replace the passive
sentence "All tracking state is lost when the host process restarts." with an
active-voice version; update it to say "The host process loses all tracking
state when it restarts." so the line reads clearly and directly while preserving
the original meaning and context in the session-tracker documentation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@docs/reference/session-tracker.md`:
- Around line 59-61: Replace the passive sentence "All tracking state is lost
when the host process restarts." with an active-voice version; update it to say
"The host process loses all tracking state when it restarts." so the line reads
clearly and directly while preserving the original meaning and context in the
session-tracker documentation.

In `@nemoclaw/src/security/session-tracker.test.ts`:
- Around line 92-117: Add coverage or documentation for the unreachable "clean"
branch in classifyRisk(): either add a test that asserts a session created via
store.record(...) is never classified as "clean" (use store.record,
store.listSessions(), and check session.riskLevel !== "clean") or add an inline
comment next to classifyRisk() (or where sessions are created in record())
stating the invariant that sessions are only created via record() and thus
always have at least one capability; reference the classifyRisk(), record(),
listSessions(), and riskLevel symbols so reviewers can verify the rationale.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d4afbb79-2629-4d37-9473-93f4e9d95215

📥 Commits

Reviewing files that changed from the base of the PR and between 282315f and 3eed971.

📒 Files selected for processing (3)
  • docs/reference/session-tracker.md
  • nemoclaw/src/security/session-tracker.test.ts
  • nemoclaw/src/security/session-tracker.ts

- Wrap onTrifecta callback in try/catch so a broken handler never
  disrupts recording — the store state is already mutated by the time
  the callback fires, so propagating would leave callers in an
  inconsistent state
- Replace {doc} cross-references with plain-text file paths to avoid
  Sphinx build errors while injection-scanner (NVIDIA#870) and audit-chain
  (NVIDIA#892) pages are still pending
- Add "NemoClaw" prefix to title.page and H1 to match the naming
  convention used by other reference pages (NemoClaw Architecture,
  NemoClaw Network Policies, etc.)
- Add test: getExposure returns null after clear() — catches partial-
  reset bugs that wipe capabilities but not the event log
- Add test: onTrifecta fires again after clear + re-record — verifies
  the callback is not permanently suppressed for a session ID
- Add test: throwing onTrifecta callback is swallowed without error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@gemini2026
Copy link
Copy Markdown
Author

gemini2026 commented Apr 2, 2026

More fixes:

  • onTrifecta wrapped in try/catch (shouldn't crash record())
  • {doc} cross-refs → plain text (target pages pending)
  • NemoClaw prefix on title.page/H1
  • Tests for post-clear behavior and error swallowing

Copy link
Copy Markdown
Author

@gemini2026 gemini2026 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Ignore — formatting error.)

Copy link
Copy Markdown
Author

@gemini2026 gemini2026 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed: onTrifecta try/catch, broken doc cross-refs, H1 prefix mismatch. Added missing test coverage for post-clear and error-swallowing behavior.

Copy link
Copy Markdown
Contributor

@prekshivyas prekshivyas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini2026 Thanks for fixing the callback error-bounding, the Sphinx cross-refs, and the H1 mismatch — those are all good.

However, the main blocker from the previous review is still unaddressed: this has no integration points. SessionStore is self-contained with zero call sites in NemoClaw. Nobody calls record() anywhere, so this would ship as dead code.

Before this can move forward, we need:

  1. At least one integration point — hook into the agent execution pipeline (e.g., OpenClaw's beforeToolUse/afterToolUse or NemoClaw plugin command dispatch) so capabilities are actually recorded during real agent sessions
  2. A decision on trifecta response — what happens when all three capabilities are detected? Log only? Block the action? Alert the operator? The tracker detects the condition but nothing acts on it
  3. Shallow-copy mutation fix — CodeRabbit flagged that getExposure() only shallow-copies arrays, so returned events still alias internal state and can be mutated by consumers

The trifecta concept is solid — we want to land this, but not as a standalone library with no consumers. Please add the wiring and we can re-review.

Address the main blocker from PR review: SessionStore had no call sites
and shipped as dead code.

Integration:
- Add session-integration.ts that wires SessionStore into the OpenClaw
  plugin lifecycle via before_tool_call and session_end hooks
- Classify tool calls into capability classes based on tool name and
  argument patterns (read_file + sensitive path → ReadSensitive,
  fetch/curl → IngestedUntrusted, http_post/send_email → HasEgress)
- Log warning via api.logger.warn() when trifecta is first detected
- Clean up sessions on session_end to prevent unbounded memory growth
- Call wireSessionTracker(api) from register() in index.ts

SessionStore additions:
- Add delete(sessionId) method for per-session cleanup
- 3 new tests for delete()

Integration test suite:
- 22 tests covering classification rules, hook registration, trifecta
  warning, session cleanup, and edge cases (no sessionId, non-matching
  tools, non-sensitive paths)

Documentation:
- Add Integration section to session-tracker.md documenting the hook
  wiring, classification rules table, and trifecta response behavior
- Document delete() method in the API section

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gemini2026
Copy link
Copy Markdown
Author

gemini2026 commented Apr 7, 2026

Integration pushed (16dcbbe).

Hooks into before_tool_call to classify tools:

Capability Tools Trigger
read_sensitive read_file, cat, head, tail, str_replace_editor, view_file Sensitive path pattern
ingested_untrusted fetch, curl, wget, http_get, browser_navigate, web_fetch Always
has_egress http_post, http_put, send_email, send_message, upload_file Always

Cleans up on session_end. One line in register(): wireSessionTracker(api).

Rules are conservative on purpose — detection aid, not a gate. Easy to extend.

Trifecta fires api.logger.warn(), pluggable via onTrifecta.

Re: shallow-copy — already fixed, getExposure() spreads each event. All fields are primitives.

@prekshivyas prekshivyas added priority: medium Issue that should be addressed in upcoming releases and removed priority: high Important issue that should be resolved in the next release labels Apr 9, 2026
- Frontmatter description: flat string → main/agent structure
- Em dash → colon in title.page and H1
Copy link
Copy Markdown
Contributor

@prekshivyas prekshivyas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gemini2026 Thanks for the persistence through multiple review rounds — the code quality and test coverage are genuinely good, and the trifecta concept (credential read + untrusted ingestion + egress = exfiltration chain) is a valid security pattern worth thinking about.

After reviewing the latest version thoroughly, we've decided not to move forward with this PR. Here's the reasoning:

The fundamental problem: tool-call tracking can't see shell execution

The tracker hooks into before_tool_call and classifies tools like http_post, send_email, fetch. But an agent performing real exfiltration would use bash_execute or exec — running curl, wget, git push, or piping data through a subprocess. These bypass the tool classification entirely because the tracker sees "bash_execute" (one tool) but has no visibility into what the shell command actually does.

Adding bash_execute to the egress list would flag every shell command as potential egress, which makes the signal useless.

Detection-only doesn't fit our threat model

NemoClaw's security model is built on enforcement: Landlock blocks filesystem writes at the kernel level, network policies block unauthorized egress at the L7 proxy, the secret scanner blocks credential writes to memory. Each of these prevents the action.

The session tracker observes after the fact. By the time the trifecta warning fires, the exfiltration has already happened. For operators actively watching logs in real-time, this has some value — but our users run sandboxes unattended, often headless.

False positive volume would cause alert fatigue

Every fetch/curl to any URL (including localhost, internal APIs) is classified as ingested_untrusted. Most legitimate agent sessions make API calls, meaning nearly every session would immediately reach elevated risk. When the baseline is noisy, the trifecta signal gets lost.

Session-boundary evasion is trivial

The tracker is per-session with no cross-session correlation. An agent can read a secret in session A, end the session, and exfiltrate in session B. The trifecta never fires.

Where behavioral detection would actually work

The right place for this kind of detection is OpenShell's L7 proxy and network layer — where egress actually happens, regardless of which tool or shell command initiated it. OpenShell can see every outbound connection, inspect payloads, and correlate with filesystem access patterns at the kernel level (Landlock/audit). That's where behavioral detection can't be bypassed by tool-call indirection.

Summary

What Status
Code quality Good
Test coverage Good (67 tests)
Core concept Valid
Practical security value for NemoClaw Low — bypassed by shell execution, detection-only, high false positive rate
Recommendation Close

Thanks again for the contribution.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request priority: medium Issue that should be addressed in upcoming releases security Something isn't secure

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: behavioral session tracking with multi-step attack detection

4 participants