Skip to content

First Light: kernel controls, live commentary, package-install control, Security history - #4

Closed
abhiabhijit wants to merge 41 commits into
mainfrom
demo-first-light
Closed

abhiabhijit wants to merge 41 commits into
mainfrom
demo-first-light

Conversation

@abhiabhijit

Copy link
Copy Markdown
Collaborator

What this branch adds

  • Kernel controls for agent trees:
    • admin tools, work outside the agent, instruction files
    • per-agent approved programs and hosts, with hostnames resolved to IPs by DNS learning
  • Agents can't install packages (new, on by default):
    • The kernel stops an agent's package manager before it runs. The daemon refuses installs (npm install, npx, pip install, uv add, cargo install, …) and resumes everything else (npm test, npm run).
    • The agent is told why on its stderr.
  • Package managers are never mistaken for agents: npm i openclaw is npm, not OpenClaw.
  • Discovery lists every agent, including Hermes Agent and OpenClaw.
  • Live commentary (text): what each agent is doing and what was refused, in plain words; per-session transcript in Sessions.
  • Security history (was Threats):
    • every refusal in the last 24 hours, none hidden, with the full path of refused files
    • actions: Change rule (password-gated) and Mark reviewed

Fixes

  • A directory rule no longer blocks a file name everywhere.
  • A worker thread exiting no longer untags its agent.
  • opencode is enforced and narrated.
  • Terminate works.
  • polkit errors are clear.

Testing

  • 347 daemon tests pass. The eBPF object loads cleanly under the verifier (Lima, kernel 6.8).
  • Checked by hand on the Ubuntu 26.04 test VM (kernel 7.0):
    • npm install, npx and python -m pip install from an agent are refused.
    • npm test from an agent runs.
    • A person's own npm install works.
    • Refused file opens record the full path.

…ernance, CIS-MCP map

Uncommitted research parked off main. NOT for main as-is — experimental.

- checks/src/registry.rs — pluggable model-provider registry: one option-scoring
  contract for every model, raise-only clamp enforced centrally (41 tests pass).
- models/schema.py, build_dataset.py — the 13 EnforcementCategories as option sets
  + a synthetic bootstrap dataset builder.
- models/ROSTER.md — base models: reflex = Laya (Apache-2.0 ModernBERT encoder,
  5/5 vs Tev1 3/5 zero-shot on our contract, calibrated, local); DLP = GLiNER;
  judgment = Span-1-style TBD; malware = YARA-labelled encoder.
- models/TRAINING-LOOP.md + trigger_retrain.py + misses/ — miss-driven flywheel:
  miss taxonomy (model_miss→train, evasion→fix detection first, false_positive→
  train), manual trigger, benchmark gate before promote, hackathon design.
- models/malware/ — bounded (agent file/exec only, not EDR) YARA-as-labels track,
  license-filtered, sandbox-guarded.
- models/bench/ — two-axis benchmark (enforcement outcome vs model detection) +
  attack scenarios.
- docs/data-governance-and-flywheel.md — consent tiers, redact-before-train, the
  red lines for a released-model data pipeline.
- docs/cis-mcp-coverage.md — CIS MCP Server Benchmark -> Ring Zero coverage map
  (kernel-enforce / detect / roadmap / server-author's-job).
…, seed the circumvention scenarios

Add models/THE-BRAIN.md as the single source of truth: the RLCD intent models
decide (off the hot path) and the kernel executes the precomputed bit
(direction A). Tighten-only is not advisory-only; add the calibrated-confidence
enforce / contain / review tiers and the anti-patterns that caused the drift.
Honest status kept per CONTRIBUTING: no weights ship yet, enforce bit is
pattern-set today, model->enforce is the next target.

Align the rest to it: ROSTER.md rewritten (13 categories = one reflex model,
~1 GB on-device, judgment tier self-hosted, sizing follows the task, caveats on
the 5-case smoke test and GPU-vs-CPU latency); registry.rs doc comments;
schema.py docstring; TRAINING-LOOP, bench/README, models/README, cis coverage
and the root README's one-rule line.

Seed the five multi_step_evasion scenarios (staged-for-subagent, side-channel
device, deferred job, laundered artifact, delegation) in bench/scenarios.jsonl
and matching human_seed rows in misses/misses.jsonl. Also commit the
enterprise-ai-threat-model.md doc that was untracked.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…smoke result

Add laya_feed.py: builds the upstream MPS trainer's item cache from our
oracle-labelled rows (misses/ + optional Laya-shape JSONL), one score question
per category with criteria = ordered options and one-hot gold on the rank, so
the gold score is the severity rank (tighten-only by construction).

Add laya_eval.py: accuracy, mean P(gold), and the bench's shuffled-context
control for any checkpoint dir, base vs fine-tuned.

Record the smoke result in ROSTER.md status: from typed-decisions on the 9 seed
rows (Apple M5/MPS, 2 epochs, loss 1.02 -> 0.37): training-set fit 4/9 -> 9/9,
mean P(gold) 0.42 -> 0.85, shuffled-context delta +0.22 -> +0.56. Labelled a
smoke test on the training rows, not a benchmark: no held-out or calibration
rows. Gitignore generated data/weights/ONNX (model IP, kept outside git).

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…th numbers

Measured 2026-10-03 on Apple M5: the fine-tuned int8 ONNX export is 571 MB and
self-contained (fp32 pair not needed to ship); single-row CPU latency via
onnxruntime is ~160 ms (min 152 ms) for a 68-token row; the int8 decision
matches fp32 (same argmax, P(gold) 0.56 vs 0.60). On-device total revised to
~0.9-1.2 GB. Remaining to measure: batch-of-10, MPS, DLP/malware encoders.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…1-P3)

Move the 17-chapter course out of the loose workspace folder into the private
fork on models-rnd, excluding the lab build artifacts (vmlinux.h, .o, the
compiled hello binary) from git. Add course/private/ — model IP, never in a
public zip: P1 the brain and where a decision model fits (System 1 vs 2, 13
categories = one model, where the model may act, tighten-only != advisory-only,
the calibrated-confidence tiers, shipped vs designed); P2 why the state space
does not hit the model (the standard doubter objection answered from the
mechanisms, where state persists, the real gaps, the eight-question drill); P3
research basis and the lab plan (SalesRLAgent -> Laya lineage, what transfers,
measured results with their honest label, the six-stage Inception compute plan,
why not pretrain now). private/README.md carries the rule and the public-zip
recipe with a zero-leak check; the public syllabus gets a Part VII pointer.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
… kernel bit)

Ten chapters reading the decision model's source end to end: the idea and
RLCD (L1), sequence layout (L2), forward pass (L3), the objective — proper
scoring rules and what RL does, deferred to the source via exercises (L4),
calibration/confidence/abstention (L5), the runtime (L6), training with our
feeder and the measured MPS run (L7), evaluation incl. the shuffled-context
control and held-out attack families (L8), ONNX int8 export + serving with
measured artifact size and latency (L9), and the bridge into the registry
contract, write-scan decide() and the BPF verdict — shipped vs designed as an
enumerated gap list (L10). README index updated.

agent/write_scan: reword the decide() doc comment so it no longer calls the
model layer "optional" — it is the reflex tier run off the hot path; the
enforce-from-patterns-alone behaviour is described as v1-as-shipped with a
pointer to THE-BRAIN.md §5/§7.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…ture

Captures the 2026-10-03 critique (Laya/SalesRLAgent too shallow for all
agents) and the resulting direction: the reflex tier as a Kahneman System 1
perception stack — kernel-event tower first, text tower, then SigLIP2 vision
and Whisper audio — fused into one state vector with Laya's typed heads,
abstain head and per-bucket calibration kept on top; System 2 (session judge)
triggered by System 1's abstention. States what survives from the Laya work,
footprint targets, the first five experiments, and the anti-patterns.

THE-BRAIN.md §6 and ROSTER.md now describe the reflex as System 1 with Laya
v1 as the text-only baseline; the private course index points to RESEARCH.md.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
Add gero_eval.py — runs a Gero-style branch scorer (Qwen3ForSequenceClassification,
num_labels=1) on our rows with the same metrics as laya_eval.py. Measured
zero-shot on 9 rows: 2/9, mean P(gold) 0.29, shuffled-context delta 0.00 —
a generic 4B decision model answers from option priors on security
situations; data, not recipe, is the moat. Recorded in RESEARCH.md §6 as E0
with caveats (n=9, label format, swap-bound latency).

RESEARCH.md §3 gains the design lessons worth keeping from Gero: the branch
readout (zero position bias by construction), the pre-train data checker, the
reward fixed-point test, the zero-gradient-at-truth rule, Murphy
reliability/resolution, and System 2 as a scorer head rather than a chatbot.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
… local models

New read-only endpoint GET /api/v1/discovery/inventory (scanner/inventory.rs)
reports, for every user on the host: installed AI agents with whether Ring
Zero's enforcement covers each one; MCP servers from Claude Code (user and
project scope), Cursor, Windsurf, Gemini, Codex (TOML), Claude Desktop,
VS Code and opencode configs, with transport and risk flags (remote,
credentials in config, unpinned package); AI extensions in VS Code, VS Code
Server, Cursor and Windsurf; and local model runtimes (Ollama, LM Studio,
llama.cpp, Hugging Face cache).

Discovery reads directory listings and config files only and never executes
a binary it finds. MCP environment variables are reported by name, never
value, URLs lose their query string, and credential-like arguments are
redacted before they leave the daemon.

The desktop app's "Agent Skills" page becomes "Discovery": the inventory at
the top, the existing skills and instructions audit below it.

agent_detect: the install-path test behind is_agent_by_binary is exposed as
is_agent_path so inventory can use it without a live pid.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
The agent's UserPromptSubmit hook already posts the prompt to the local
daemon. It is now scanned with the same secret patterns the file scanner uses
(new detector::find_spans returns value spans), and [dlp] prompt_guard
decides what happens: off, warn (record, let it through; the default), or
block (rz-hook returns Claude Code's {"decision":"block","reason":...} so the
prompt is not sent and the person sees why). No network call is made.

The trace now stores the REDACTED prompt. Before this, a prompt event stored
the raw text, which would have put the very secret into Ring Zero's own logs.
A finding is recorded as a denied (block) or flagged (warn) event with the
kinds found and masked previews; values never appear in the event, the reason,
or the hook response.

The on-device span model (GLiNER-small, DLP tier) is the next layer on the
same function and may only add findings.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…uard)

The opt-in Claude Code hook installer registered PreToolUse and PostToolUse
only, so prompts never reached the daemon and the prompt guard could not run.
UserPromptSubmit is added as a matcher-less group, merged into the user's
existing settings like the others.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
A profile, written as [[profiles]] in daemon.toml, attaches to one agent or
one MCP server and says which hosts it may connect to, whether it may start
other programs, and if so which ones.

Every connection and program start inside an agent's process tree is
attributed to the nearest profiled MCP server or agent (MCP servers first,
since they run inside an agent's tree), checked against its profile, and a
mismatch is recorded on the event as "Would block — <profile>: <detail>".
Per-profile counts (allowed / would block) and the latest violations are
served at GET /api/v1/policy/profiles together with the daemon mode and the
prompt-guard setting, for the Policy screen.

This is stage 1 and refuses nothing: it observes, which is also how a
profile is rolled out against real work before it is enforced. Host names
are resolved to IPs in the background every five minutes because connect
events carry addresses. Kernel enforcement is stage 2: the network half maps
onto the connect hook's existing IP allowlist, the program half needs a new
exec denial and its own review.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
Replaces the separate Enforcement and File Access pages with one Policy
page that answers four questions in order:

  1. Mode — enforcing or watching only.
  2. Protected data — the kernel-enforced file rules (the File Access
     editor, embedded).
  3. Agent & tool permissions — one card per agent and per MCP server:
     allowed hosts, whether it may start programs and which, and live
     "allowed / would block" counts with the latest violations
     (GET /api/v1/policy/profiles, refreshed every 5 s). Labelled
     "Watching" because stage 1 records and refuses nothing.
  4. Prompts — the prompt-guard setting in plain words.

The per-category settings move to a collapsed "Alert categories" section,
labelled as recording rather than refusing, and their "Block" option is
renamed "Violation": nothing in the event pipeline enforced it, and the old
label put a false claim on screen. Old page ids route to Policy.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
Policy page header said everything on it was enforced in the kernel and
editable with an admin password; only Protected data is both. It now says
each section states whether it is enforced or only watched, and the mode
card scopes "refused by the kernel" to protected files.

Overview showed "All 13 categories: BLOCK" for a setting that records a
violation and refuses nothing. Reworded to "record as violation" / "flag for
review".

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…LI and app

ProfileConfig gains network_mode and programs_mode (watch | enforce, default
watch) and CIDR ranges in allow_hosts. Every write is validated (name, agent
xor MCP match, host entries, program names) and saved atomically to
/etc/ringzero/profiles.json, which becomes the complete list once present;
daemon.toml [[profiles]] only seeds it. Changes apply live.

New: PUT /api/v1/policy/profiles, DELETE /api/v1/policy/profiles/:name (full
token only), rz profile show|set --json|remove, and the two write commands on
the app's polkit allow-list. The enforce mode is stored here; the kernel side
that acts on it follows.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
… agents

Policy changes are meant to have one road: the admin-password prompt from a
process that is not an agent. The API already needs the root-only token and
refuses writes from agent callers, but an agent running where the operator
recently used sudo could skip the API: read the token, or edit
profiles.json / daemon.toml / the service unit directly.

/etc/ringzero, /var/lib/ringzero and the ringzero-daemon unit (and its
drop-in directory) are now blocked for every agent process tree, by tree
membership rather than uid, so it holds for an agent running as root. Always
on, not removable from the app or CLI, and re-applied when the operator's
directory rules are cleared (clearing them used to drop the switch that
directory blocking hangs on).

Measured on Ubuntu 26.04 / kernel 7.0: an agent process running as root is
refused the token, daemon.toml, a write into /etc/ringzero and the unit
file; an ordinary file and a non-agent `sudo cat` are unaffected.

Not covered, stated in the code: the binaries (a file block also refuses
exec, and agents must run rz and rz-hook), and stopping the service through
systemd, which delivers the signal itself.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…nd agents cannot use bpf()

New kernel switch, tamper_protect (the last reserved config byte, so the
struct size and its Rust mirror are unchanged), separate from enforce_blocks
so it holds even when every other rule is only observing. Set from
[daemon] tamper_protection, on by default.

  task_kill            any non-zero signal to the daemon is refused unless it
                       comes from the daemon itself or pid 1 (systemd, which
                       delivers reload, shutdown and the operator's stop).
                       Root included.
  ptrace_access_check  nobody may ptrace the daemon or open its memory.
  bpf (new hook)       an agent process tree, root or not, may not make the
                       bpf() syscall, so it cannot detach a Ring Zero program
                       or rewrite one of its maps. The daemon and a human's
                       root shell outside any agent are unaffected.

Measured on Ubuntu 26.04 / kernel 7.0 / arm64: root kill -TERM, kill -9 and
pkill -STOP of the daemon are refused and it keeps running; root opening
/proc/<pid>/mem is refused; an agent running as root gets EACCES from
bpftool, while root bpftool outside an agent still works; systemctl restart
still works. Not refused, observed: reading /proc/<pid>/environ.

The previous observe-only lines in these hooks are left as they were; this
is a new, separately gated path.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
Discovery: an environment variable is treated as a credential only when a
whole word of its name is one (TOKEN, KEY, PAT, PASSWORD, ...). The old
substring match flagged PATH, so Codex's node_repl MCP server was wrongly
shown as "credentials in config". MCP variable names collapse into an
expandable count instead of a wall of text. Page width matches the others.

Threats: plain titles for a secret stopped in a prompt, a refused tamper
attempt, and an agent refused Ring Zero's own files; the line under each
threat names the agent and what happened instead of the raw event target.
Page width standardised.

Sessions: an ended session shows a neutral grey "Ended" instead of a red
"Terminated", which read as if Ring Zero had killed it; agents show by
product name (Claude Code, ChatGPT, Codex CLI, ...).

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
- Posture strip at the top: mode, tamper protection, secrets in prompts,
  agent permissions, each green/amber at a glance (the policy endpoint now
  reports tamper_protection). The separate Mode section it duplicated is gone.
- Protected data is a new panel instead of the old File Access page
  embedded whole: one list (File/Folder, Enforced / Not found on disk /
  Not saved), one-click common protections (SSH keys, cloud credentials,
  .env files), a File/Folder add field, and one "Save and enforce" bar that
  appears only when something changed. Saving uses the same rz file-access
  add/remove commands through polkit as before, so the password is asked
  once per save. The legacy [dir-block] marker is no longer shown in notes.
- Permission cards: hosts and programs as chips with "+N more", a large
  would-block count, allowed beneath it.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
…; OS-neutral text

Following how established security products lay out settings (one switch
and one sentence per protection, Detect vs Block, administrator-protected
controls marked, a banner when a protection is off, product security as its
own group):

- Product security group: a Tamper protection switch. Turning it on or off
  asks for the administrator password every time (rz settings set
  tamper_protection on|off through polkit) and applies live in the kernel.
- Prompts: Off / Detect / Block, same password path.
- Agent & tool permissions are a table of switches (agents: any host vs
  approved hosts only, any program vs approved only; MCP servers: network
  access, can start programs) with the lists behind "Edit". One Save sends
  each changed profile with rz profile set, one password prompt.
- "Watching" is now "Detect only — nothing is blocked yet".
- A banner lists any protection that is off.

Backend: settings.json beside daemon.toml overrides it for the switches
the app can change, so the operator's daemon.toml is never rewritten.
PUT /api/v1/policy/settings (full token, agent callers refused) saves and
applies; rz settings set; allow-listed in the app.

User-facing text no longer shows sudo, systemctl or Linux paths.

Signed-off-by: Ring Zero Security <abhijit@ringzerosecurity.com>
Theme colors are hex CSS variables, so Tailwind emitted no rule for classes
like bg-muted-foreground/30 or bg-primary/5. Off switches were invisible and
tinted save bars and focus rings were missing. Wrap each color in color-mix
with the alpha placeholder.
pkexec exits 127 with a terminal error when the app runs outside the desktop
session. Show what to do instead of polkit's text and a shell command; keep
polkit's words in the app log.
…ion files

Replace the 13 threat-category settings, which never refused anything, with
controls the kernel enforces on agent process trees:

- admin tools (sudo, su, pkexec, doas, run0), on by default
- escape tools (systemd-run, at, batch, crontab), on by default
- agent instruction files (CLAUDE.md, AGENTS.md, .cursorrules, ...): read
  allowed; write, create, rename, delete and hardlink refused. Off by default.
- quarantine of flagged agent-written files, now a switch

Each is a switch on Policy behind the administrator password (rz settings
set). Threat categories become labels from a narrow rule-based classifier on
recorded events, shown on Threats; trained classifiers replace the rules
per category. Overview shows the controls that are blocking.

Tamper protection now allows read-only process metadata to non-agent
processes; journald hit it on every log line. Attach (debug, memory) stays
refused to everyone.
The network switch meant 'restricted' while the programs switch meant
'allowed', so the same green switch read as opposite things. Now on always
limits the agent to its approved list (empty list = none), amber because the
limits are watched, not enforced. 'would block' becomes a count of times
outside the limits since start, which opens the recorded events.
Each process in an agent tree now carries its profile's slot (set when the
agent starts, by name or by command line for node/python agents and MCP
servers, copied at fork). A profile in Block mode is enforced at the call:

- programs: exec outside the approved list is refused; names behind
  symlinks (python3 -> python3.14, sh -> dash) are approved too
- network: connect and unconnected sendto outside the approved addresses are
  refused; v4-mapped IPv6 judged as IPv4; other IPv6 refused; loopback, DNS
  and model endpoints always allowed
- approved host names are looked up every minute; addresses from the agent
  tree's own DNS answers are added for the TTL, which is what makes
  *.domain work. DNS capture now covers the whole agent tree, not only the
  agent process. Sub-domains seen once are remembered across restarts.
- network is not enforced until names were looked up once after start

Policy: each limit has Watch or Block. Also fixes a 4-second rescan that
reset every agent's profile tag.
The daemon turns what it records into short spoken lines (narrator.rs):
the task, the agent's thinking (from its transcript), files read (grouped)
and changed, commands run, web and MCP use, and above all what Ring Zero
refused. Lines carry a level: alerts (a refusal) cut in, info may be
skipped when the agent is busy. Every line passes the secret detector, so a
key is never read aloud. GET /api/v1/commentary long-polls for new lines.

The app shows them as captions and speaks them on the machine with Piper
(one warm process, raw audio to the system player), falling back to
espeak-ng or captions only. Piper is installed separately by
packaging/voice/install-voice.sh with its licences (GPL-3.0 via espeak-ng;
voice dataset CC0) and is never linked into the app. Switch at the bottom
of the sidebar.
rz-kokoro keeps Kokoro loaded through the sherpa-onnx C API and turns each
stdin line into raw PCM, so the app drives it exactly like Piper. Measured
on the ARM64 demo VM: model loads in 0.6 s, each line is ready in about 1 s.
Full precision on purpose: the int8 build was about 5x slower on ARM64.
An agent's worker threads (Bun, Node) have their own names, and an agent
that was running before the daemon (re)started was never in the kernel's
agent map, so its worker threads could read protected files. Reproduced on
the VM: after a restart, a thread named 'Bun Pool 1' read ~/projects/flag.txt.

The kernel now checks the process's main thread name when the current
thread's is not an agent's, and tags the process on the spot. The daemon
also re-tags running agents and everything they started, with their profile
slot, at startup. Both cases are refused again; a non-agent is unaffected.
- The daemon dropped opencode's kernel events: its agent classifier had no
  opencode entry (so no session), and events were labelled with the thread
  name ("HTTP Client"). opencode is classified now, and an event whose
  thread name is not an agent's takes the process's name.
- The narrator speaks from kernel events for agents without hooks: "is at
  work", the programs it runs (shell plumbing stays quiet), files it saves.
  Claude Code keeps its richer hook narration and is not repeated.
- Refusals say "Ring Zero Security" and name the agent, not the helper.
- The app starts over when the daemon restarts and its numbering resets,
  instead of ignoring every new line.
The modal showed a hard-coded confidence (94/91/97%), template text styled
as AI analysis, buttons that did nothing (Allow, Block Once, Report False
Positive) and the helper's name (cat) as the source, and it covered the
screen on every refusal.

Now a corner notice that says only what the record says: one sentence (the
same one the commentary speaks), which control refused it, the target, the
time and the classifier's category. Refusals from one agent within a few
seconds are one notice. Buttons: View in Threats, Change rule. Compact
while live commentary is on.

The daemon also attributes a helper's event to its agent session, so it
reads "opencode tried to read .env", not "head".
The table listed only agents that already had a profile (opencode and four
others Discovery found were missing) and put two controls in every cell (a
limit switch and Watch/Block).

Now a list of every discovered agent, each with one status line, and a
detail view with Network and Programs cards. Each card is one choice: No
limit, Watch or Block. Things the agent reached outside its list are
offered under 'Seen outside the list' to approve with one click. Saving a
discovered agent creates its profile; the password is asked once per save.
Kernel name map: opencode also runs as opencode.exe.
- Commentary tab in each session: the session told in sentences, rebuilt
  from what was recorded (GET /api/v1/sessions/:id/commentary replays its
  events through the narrator with each event's real time, plus the agent's
  prompts and tool calls in the session's span and its thinking lines).
- Session events that were refused now show red and say Blocked. Every
  file event was drawn green before, so a refused read looked successful.
- Terminate goes through the administrator password (rz sessions
  terminate). The direct call used the app's read-only token, which the
  daemon refuses, so the button silently did nothing.
- Refused reads of protected files say read, not touch.
Captions while agents work and each session's Commentary tab stay. The
spoken voice (Piper, Kokoro via rz-kokoro, espeak fallback), its Tauri
commands, the voice switch and packaging/voice are removed; git history
keeps them.
Both were already enforced as agents (agent_detect) but missing from the
inventory, so Discovery showed only coding agents. Package description now
says AI agents, not AI coding agents.
Agents can't install packages (new control, on by default):
- Kernel: when a process in an agent's tree becomes a package manager
  (npm, npx, pnpm, yarn, bun, pip, uv, uvx, poetry, cargo, gem), the
  sched_process_exec hook stops it before it runs an instruction.
- Daemon: reads the full command line on the reader thread. Installs and
  fetches (npm install/ci/add/update/exec, npx, pnpm/yarn/bun add, dlx,
  pip install, uv add/sync/pip install/tool install, poetry add,
  cargo install/add, gem install) are killed; anything else (npm test,
  npm run, pip list) is resumed.
- `python -m pip install` runs as python, so it is caught just after it
  starts instead.
- The refused process is told why on its own stderr (via pidfd_getfd, so
  no DAC override is needed), and commentary names the agent and command.
- `rz settings set package_installs on|off`, password-gated like the
  other controls.

Package managers are never an agent by their own name or arguments. npm
puts its command line in its process name ("npm i openclaw@"), so
installing OpenClaw made npm look like OpenClaw and the kernel refused the
install. A package manager an agent starts is still in that agent's tree.
In the kernel, the claw-family match now only looks at the first word of
the name.

A directory rule no longer becomes a file-name block everywhere. A rule
for ~/rz-test/secrets refused every file or folder named "secrets" on the
machine. Known credential stores (~/.ssh/*, ~/.aws/*) still expand to their
key names.

A worker thread exiting no longer untags its agent: only the thread-group
leader's exit removes the process from agent_descendants.

A refused file open now records the file's full path (bpf_d_path, only on
refusals), so the record says which file.

GET /api/v1/events?blocked=true returns refusals across the whole 24-hour
window, so busy activity cannot crowd one out.
- Threats is now Security history. Everything on it was refused before it
  happened, so it is no longer described as "active threats".
- It lists every refusal from the daemon's blocked-only query. It no longer
  takes the newest 100 events of every kind, which lost refusals within
  seconds, and it no longer hides "low severity" ones behind a toggle. The
  notice that says "View in Security history" now always finds its item.
- Notices, the page and the Overview "Refused (24h)" tile share one list.
  The sidebar count is gone.
- Row titles say what happened (File access refused, Package install
  refused, Program refused, Connection refused).
- Actions:
  - Removed "Trust Process", which changed only what the page showed, and
    "Whitelist Path", which unblocked a file name everywhere.
  - Added "Change rule", which opens Policy (password-gated), and
    "Mark reviewed", which is remembered in this browser.
- Policy gains the "Agents can't install packages" control. Overview shows
  "Package installs" among the controls that are blocking.
@abhiabhijit abhiabhijit closed this Oct 9, 2026
@abhiabhijit
abhiabhijit deleted the demo-first-light branch October 9, 2026 10:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants