@@ -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