Skip to content

Commit ad8a6ae

Browse files
pdfinnclaude
andcommitted
docs(veltro): reframe SECURITY.md future work around nsaudit
Replace the earlier "static namespace reachability analysis" framing with a cleaner account of the actual problem and tool. Reachability over files is the wrong layer: files in Plan 9/Inferno may be network dials, payments, LLM round-trips, or process kills, and reachability is a read question in a system where reads leak through any egress path. The useful question is closer to "what irreversible effects on the outside world can this agent cause?" -- which Infernode's existing mechanisms (cowfs, wallet9p, NEWENV) already lean toward. The new section documents nsaudit explicitly as syntactic analysis, not formal verification -- complementary to the kernel-level TLA+ / SPIN / CBMC work in formal-verification/. Covers the closed authority axis set, the initial rule catalog, the CI gate architecture (fixtures + snapshots + suppressions with expiry), the Meta Agent and Lucifer GUI as first real targets, what the tool cannot answer (prompt injection, semantic reversibility, manifest truthfulness), and the sequencing from CLI skeleton through lucictx integration. Also retains the NODEVS short-term fix independent of nsaudit: add pctl(NODEVS) to veltro.b:168, repl.b:169, tools9p.b:644. The kernel gate at emu/port/chan.c:1041-1051 is strictly stronger than the path-based restriction for the device-attach class of bypass. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0864cfc commit ad8a6ae

1 file changed

Lines changed: 310 additions & 0 deletions

File tree

appl/veltro/SECURITY.md

Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,3 +353,313 @@ Concurrency tests in `tests/veltro_concurrent_test.b`:
353353
- Concurrent init
354354
- Concurrent restrictdir
355355
- Concurrent restrictns
356+
357+
## Future Investigation: nsaudit
358+
359+
### Not verification — syntactic analysis
360+
361+
The existing `formal-verification/` tree proves properties of the kernel's
362+
namespace primitives: 3.17 billion TLA+ states, SPIN model checks, CBMC
363+
harnesses over `pgrpcpy` and friends. Those proofs are about the mechanism.
364+
365+
`nsaudit` is about the *configuration* that feeds the mechanism. It parses a
366+
capability configuration, looks up each tool in a per-tool authority
367+
manifest, and applies pattern-match rules over the resulting authority set.
368+
No symbolic execution, no theorem, no proof. This is syntactic analysis —
369+
closer to a linter or a type checker than to TLA+. The two efforts are
370+
complementary: `formal-verification/` proves the kernel does what it's
371+
told; `nsaudit` checks that what you're telling it is what you meant.
372+
373+
### The question it answers
374+
375+
A single question, asked daily and asked under different urgencies:
376+
377+
> **"What does this namespace configuration actually allow the agent to do?"**
378+
379+
- *Daily debugging (high volume, low stakes)*: "My agent can't see
380+
`/n/local/foo/bar` and I don't know why. What did I miss?" — `nsaudit
381+
-reach /n/local/foo/bar` answers it.
382+
- *Shipping defaults (low volume, very high stakes)*: "The config we ship to
383+
every InferNode install — does it grant an agent escape valve we didn't
384+
intend?" — `nsaudit` run on a committed fixture, diffed against a
385+
committed snapshot, gated in CI.
386+
- *Tool development (per tool)*: "The new tool I'm writing — what authority
387+
does it actually add to an agent's caps?" — tool author runs `nsaudit`
388+
against a test fixture that includes their tool and reviews the report.
389+
- *Security review (per release, per incident)*: "The Meta Agent has
390+
capabilities we've never formally scoped. What can it reach and cause?" —
391+
`nsaudit` run against the meta-agent fixture, violations section reviewed
392+
by hand.
393+
394+
Same tool, same engine. Different modes emphasize different parts of the
395+
same underlying analysis.
396+
397+
**`nsaudit` is advisory, not enforcing.** The namespace is still what
398+
enforces. `restrictns()`, `FORKNS`, `NODEVS`, cowfs overlays, and
399+
`wallet9p`'s per-transaction gating are the runtime gate; `nsaudit` is the
400+
pre-flight review. If you ship a misconfigured caps, `nsaudit`'s warning
401+
does not help you at runtime — only the correctness of the caps themselves
402+
does. The value `nsaudit` adds is making misconfiguration visible before
403+
it ships.
404+
405+
### Data model
406+
407+
All inputs and outputs are files. No serialization format is invented; no
408+
in-tree JSON; everything is either a directory of scalar files (like
409+
`tools9p` already uses) or an ndb(6) attribute file (like factotum and cs
410+
already use). Inferno has `attrdb(2)` in `module/attrdb.m` and the ndb
411+
parser in `appl/cmd/ndb/`.
412+
413+
**Caps input — a directory of scalar files** (the format `tools9p` already
414+
exposes at `/tool/`):
415+
416+
/tool/tools one tool name per line
417+
/tool/paths one path grant per line
418+
/tool/meta/role "toplevel" or "child"
419+
/tool/meta/xenith "1" or "0"
420+
/tool/meta/actid integer or "-1"
421+
/tool/meta/nodevs "set" or "unset"
422+
423+
Live audit: `nsaudit /tool`. Hypothetical analysis: construct a mock
424+
directory of the same shape. Fixtures for CI are directories of the same
425+
shape.
426+
427+
A small addition to `tools9p` is required: expose `role`, `xenith`, `actid`,
428+
`nodevs` as scalar files under `/tool/meta/`. Without this, `nsaudit` only
429+
sees `tools` and `paths`.
430+
431+
**Tool authority manifest — ndb files at `lib/veltro/nsaudit/authorities/<tool>`:**
432+
433+
; cat lib/veltro/nsaudit/authorities/exec
434+
description Execute a shell command via sh.dis
435+
authorities spawns_proc execs_code reads_fs writes_fs dials_net
436+
irreversible spawns_proc writes_fs dials_net
437+
notes Force multiplier. Grants anything the spawned shell can
438+
reach within the agent's namespace. Scope limited by
439+
caps.shellcmds if set, otherwise unrestricted.
440+
441+
One file per tool. Adding a tool means adding a file. Reviewed at every
442+
new tool.
443+
444+
**Rules — ndb files at `lib/veltro/nsaudit/rules/<name>`:**
445+
446+
; cat lib/veltro/nsaudit/rules/device-gate-bypass
447+
name DEVICE_GATE_BYPASS
448+
severity high
449+
condition role=toplevel nodevs=unset
450+
message top-level caps grants kernel device attach without NODEVS.
451+
Any sys->bind on an #x device will succeed, reaching
452+
#sfactotum, #U (host fs), or other kernel services
453+
regardless of path-based restriction.
454+
fix add sys->pctl(Sys->NODEVS, nil) after the FORKNS site
455+
456+
One file per rule. Adding a rule means adding a file (and a test).
457+
458+
**Suppressions — ndb files at `lib/veltro/nsaudit/suppressions/<fixture>.<rule>`
459+
with expiry:**
460+
461+
; cat lib/veltro/nsaudit/suppressions/shipping-default-full.exec-force-multiplier
462+
rule EXEC_FORCE_MULTIPLIER
463+
scope fixture=shipping-default-full
464+
reason Interactive REPL exposes exec so power users can run
465+
commands. Scoped by caps.shellcmds and user consent at
466+
first launch.
467+
reviewed 2026-04-11
468+
by pdfinn
469+
expires 2026-10-11
470+
471+
Expired suppressions fail CI. Every rule suppression is a named, dated
472+
file — no hidden exceptions.
473+
474+
### Authority axes (closed set)
475+
476+
The soundness of syntactic analysis depends on the axis set being closed
477+
and enumerable. Adding a new axis is a deliberate act, not a derivation:
478+
479+
| Category | Axis | Source |
480+
|---|---|---|
481+
| Filesystem | `reads_fs` | tool manifest, `caps.paths` |
482+
| Filesystem | `writes_fs` | tool manifest, `caps.paths` |
483+
| Filesystem | `writes_fs_durable` | `writes_fs` ∧ not `/tmp/veltro``actid < 0` |
484+
| Network | `dials_net` | tool manifest, `caps.mcproviders` |
485+
| Network | `listens_net` | tool manifest |
486+
| Process | `spawns_proc` | tool manifest (e.g. exec, spawn, launch) |
487+
| Process | `signals_proc` | tool manifest |
488+
| Process | `execs_code` | tool manifest |
489+
| Kernel | `attaches_device` | `role=toplevel``nodevs=unset` |
490+
| Secrets | `reads_secrets_factotum` | `/mnt/factotum` in reads_fs |
491+
| Secrets | `reads_env` | `NEWENV` unset |
492+
| Economic | `spends` | tool manifest (wallet, pay) |
493+
| Comms | `sends_llm` | tool manifest, `caps.llmconfig` |
494+
| Comms | `sends_ui` | `caps.xenith``/n/ui` in writes_fs |
495+
| Comms | `receives_input` | `/dev/cons` in reads_fs |
496+
| Windows | `reads_windows` | `caps.xenith` |
497+
| Windows | `modifies_windows` | `caps.xenith` |
498+
| Memory | `persists_memory` | `caps.memory` |
499+
500+
### Initial rule set
501+
502+
Each rule is a file at `lib/veltro/nsaudit/rules/`, each with a test
503+
under `tests/nsaudit-rules/`. New rules land as (file, test) pairs.
504+
505+
| Rule | Condition | Severity |
506+
|---|---|---|
507+
| `DEVICE_GATE_BYPASS` | `role=toplevel``nodevs=unset` | high |
508+
| `EXFIL_RISK_EGRESS` | `reads_fs ∩ (dials_net ∨ sends_llm ∨ spawns_proc)` | high |
509+
| `EXEC_FORCE_MULTIPLIER` | `exec` in tools | info |
510+
| `UNCONSTRAINED_SHELL` | `exec` in tools ∧ `shellcmds` empty | high |
511+
| `SPAWN_INHERITANCE` | `spawn` in tools ∧ `writes_fs_durable` | medium |
512+
| `DURABLE_HOST_MUTATION` | `writes_fs_durable` non-empty | medium |
513+
| `UNBOUNDED_SPEND` | `spends` without per-call gating metadata | high |
514+
| `LLM_AS_EGRESS_FOR_SECRETS` | `sends_llm` ∧ reads_fs contains secrets path | high |
515+
| `NET_EGRESS_IMPLICIT` | `dials_net` without matching `mcproviders` entry | medium |
516+
| `SUBAGENT_MISSING_NODEVS` | `role=child``nodevs=unset` | high |
517+
518+
`SUBAGENT_MISSING_NODEVS` is worth highlighting: it turns the
519+
`spawn.b:576` `pctl(NODEVS)` call from an implementation detail into a
520+
checked property. If someone edits `spawn.b` and removes the NODEVS call,
521+
the subagent fixture snapshot regenerates without `nodevs=set`, the rule
522+
fires, CI fails.
523+
524+
### CI gate
525+
526+
The gate is not runtime enforcement. It is a build-time check that
527+
shipping configurations match committed expectations.
528+
529+
**Fixtures at `tests/nsaudit-fixtures/<name>/`:** directories of the same
530+
shape as a live `/tool`, one per shipping configuration.
531+
532+
- `shipping-default-full` — full GUI (`lib/lucifer/boot.sh` line 48)
533+
- `shipping-default-headless` — REPL without GUI
534+
- `shipping-default-subagent` — spawned child config (must have
535+
`nodevs=set`)
536+
- `meta-agent` — the Meta Agent / Chief of Staff config used by
537+
`lucibridge` at activity 0 (first high-value target)
538+
- `lucifer-gui` — lucifer's own context-zone namespace (second
539+
high-value target)
540+
- `shipping-default-minimal` — smallest viable agent
541+
542+
**Snapshots at `tests/nsaudit-fixtures/<name>/expected.ns`:** committed
543+
ndb file containing the authority inventory, violations list, and
544+
suppression references. The source of truth for "what this config grants."
545+
546+
**The gate itself:** a `mk nsaudit-check` target in `tests/mkfile` that
547+
runs `nsaudit` against every fixture and diffs the output against the
548+
snapshot. Exit nonzero on any difference. Wire into
549+
`.github/workflows/` alongside the existing security checks.
550+
551+
A companion script, `tests/nsaudit-fixtures/verify-matches-boot.sh`,
552+
diffs fixture contents against the tool lists in `lib/lucifer/boot.sh`,
553+
`dis/lucifer-start.sh`, `run-lucia.sh`, and friends — so the fixture
554+
cannot silently drift from the real shipping commands.
555+
556+
Updating a fixture or snapshot requires a deliberate edit in the PR,
557+
visible to reviewers. Adding a suppression requires a named, dated file
558+
with an expiry. Both force conscious decisions about what authorities
559+
ship by default.
560+
561+
### What nsaudit cannot answer
562+
563+
Stated plainly, because a tool that over-claims is worse than no tool:
564+
565+
- **Prompt injection propagation.** If the agent reads attacker-controlled
566+
data and the LLM chooses to act on it, effective authority becomes
567+
whatever the model decides. Not statically decidable.
568+
- **Semantic reversibility.** "Agent overwrote `notes.txt`" is
569+
reversible with backups, not without. Context-dependent.
570+
- **Manifest truthfulness.** A lying manifest entry is undetectable to
571+
`nsaudit`. The runtime ground-truth check (below) is the safety net.
572+
- **Tool-internal composition.** A tool that invokes sub-tools not named
573+
in its manifest entry is as good as its manifest, no better.
574+
575+
A caps that passes `nsaudit` is *not* "safe." It is "free of the
576+
authority compositions `nsaudit` knows to check for." That ceiling is the
577+
right one to advertise.
578+
579+
### Runtime ground-truth check
580+
581+
The one place runtime code is needed is the cross-check: does `nsaudit`'s
582+
static model of `reads_fs` agree with what `restrictns()` actually
583+
produces at runtime?
584+
585+
A test (`tests/nsaudit_groundtruth_test.b`) forks, applies real
586+
`restrictns(caps)` for each fixture, walks the resulting namespace with a
587+
bounded BFS, and asserts the walked set equals the `reads_fs` set the
588+
linter computed for the same caps. Any disagreement means either the
589+
linter's model or the implementation has drifted — both are real bugs.
590+
591+
This is where `nswalk` lives: not as a user-facing tool, but as a
592+
subroutine of the ground-truth check. Once it exists as a subroutine,
593+
exposing it as a user tool is cheap.
594+
595+
### NODEVS short-term fix, independent of nsaudit
596+
597+
`pctl(NODEVS)` is applied only in the spawned-child path
598+
(`spawn.b:576`). Top-level agents (`veltro.b:168`, `repl.b:169`,
599+
`tools9p.b:644`) call `pctl(FORKNS)` and `restrictns()` but leave
600+
`pgrp->nodevs == 0`. The kernel device gate is at
601+
`emu/port/chan.c:1041-1051`; with `nodevs` unset, `sys->bind("#sfactotum",
602+
"/tmp/veltro/x", MREPL)` succeeds and reaches factotum regardless of
603+
path-based restriction.
604+
605+
Today this is latent — top-level agents do not invoke `bind` on `#x`
606+
paths from model-driven code. It becomes exploitable the moment any tool
607+
or exec invocation does.
608+
609+
Fix: add `sys->pctl(Sys->NODEVS, nil)` to the three top-level FORKNS
610+
sites. The kernel gate is strictly stronger than the path gate for
611+
device-attach, and none of the top-level agents has a documented need for
612+
`#x` devices outside the `nodevs` allowlist (`|esDa`). Once fixed,
613+
`SUBAGENT_MISSING_NODEVS` plus an analogous `TOPLEVEL_MISSING_NODEVS`
614+
rule keep it fixed.
615+
616+
### Sequencing
617+
618+
1. **CLI skeleton** (`appl/cmd/nsaudit.b`): ndb parsing via
619+
`Attrdb`, three modes — full report, `-reach PATH`, `-d before after`.
620+
~300 lines. Runs inside emu, no namespace manipulation.
621+
2. **Per-tool manifest** for existing tools in `appl/veltro/tools/`.
622+
One file per tool, ndb format. Each entry is a small act of honest
623+
assessment — read the tool's source, decide what authorities it grants.
624+
3. **Initial rule set** — the ten rules above, one file each,
625+
test-per-rule.
626+
4. **Initial fixtures**`meta-agent` first (the user's stated priority),
627+
then `lucifer-gui`, then the three shipping defaults, then `minimal`.
628+
5. **CI gate**`mk nsaudit-check` + snapshot diff + boot-script drift
629+
detection.
630+
6. **Runtime ground-truth check** (`tests/nsaudit_groundtruth_test.b`)
631+
using a bounded `nswalk` subroutine. Catches manifest/implementation
632+
drift.
633+
7. **`tools9p` metadata exposure** — scalar files under `/tool/meta/`.
634+
8. **lucictx integration** — new collapsible Authority section;
635+
re-run `nsaudit` on every `/tool/ctl` write; inline `-reach PATH` on
636+
hover in the file browser.
637+
9. **Staging/preview** — right-click "What would change?" in lucictx
638+
runs `nsaudit -d` between current `/tool` and a staged mock.
639+
640+
Steps 1–5 are the shipping-gate MVP. That is what protects the defaults.
641+
Steps 6–9 add the debug UX and the live GUI. Every step is additive; the
642+
earlier steps keep working as the later ones land.
643+
644+
### Prior art, or lack thereof
645+
646+
No tool exists in the Plan 9 / Inferno / 9front ecosystem for namespace
647+
safety analysis (verified 2026-04). The closest things are `ns(1)`
648+
(inspection only) and ANTS's per-process `/srv` (mitigation, not
649+
analysis). No tool exists in the broader capability-OS literature for
650+
authority inventory over a running agent's caps either —
651+
seL4/EROS/KeyKOS verify confinement at the kernel level but do not
652+
produce human-readable authority reports for application-level capability
653+
sets. `nsaudit` fills unclaimed ground.
654+
655+
### Current state
656+
657+
- `appl/cmd/nsaudit.b` — CLI skeleton (in progress).
658+
- `lib/veltro/nsaudit/authorities/` — one entry (in progress).
659+
- `lib/veltro/nsaudit/rules/` — one entry (in progress).
660+
- `tests/nsaudit-fixtures/minimal/` — first fixture (in progress).
661+
- No `tools9p` metadata exposure yet.
662+
- No runtime ground-truth check yet.
663+
- No lucictx integration yet.
664+
- No `meta-agent` or `lucifer-gui` fixture yet — those are the first real
665+
targets after the CLI and one fixture work end-to-end.

0 commit comments

Comments
 (0)