const MaxUnion, MaxOps
Limits. Resolution costs one call per element per union member, and those calls may cross a realm boundary, so both are capped rather than trusted.
Package ns is a Plan 9 namespace: a private, mutable mount table over gno.land/p/moul/x/plan9/ninep file trees.
gno.land/p/moul/x/plan9/ns/v0A Plan 9 namespace: a private, mutable mount table over
ninep file trees, with bind(2)'s flags and union
directories.
1import ns "gno.land/p/moul/x/plan9/ns/v0"
2
3n := ns.New(fs.Root())
4n.Bind("/usr/glenda/bin", "/bin", ns.MAFTER|ns.MCREATE)
5n.ReadDir("/bin", false) // the concatenation of both directories
This is the idea the rest of the suite exists for. Plan 9's leverage does not
come from "everything is a file", it comes from every process owning its own
name-to-resource mapping, so a name can be replaced and any service can be
composed, shadowed, sandboxed or mocked without the program knowing. Gno has one
global tree of realm paths that looks the same to everybody; an Ns is a view
of it that belongs to you.
Flags follow bind(2): MREPL
replaces, MBEFORE splices the new directory in front of the old one, MAFTER
behind it, and MCREATE (OR'd onto any of them) marks the union member that new
files are created in.
Four Plan 9 properties are load-bearing and reproduced deliberately:
/a onto /b unions the two
directories at /b; /b/c resolves in whichever member won the walk and is
not itself a union unless something is bound there too.unique to collapse it first-wins instead.MCREATE on some member, and is refused
otherwise. A plain, unbound directory takes creates with no flag at all.bind(1) requires.MaxUnion (8), MaxOps (64) and ninep.MaxDepth (32) bound resolution,
because each element of each member can cost a cross-realm call.
String() prints the namespace in ns(1) format: one line per binding, in
application order, then the working directory.
Live demo: r/moul/x/plan9/ns gives
every account one of these. Design and analysis:
moul/gno-contracts#136.
Not affiliated with Plan 9. Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the Plan 9 Foundation. This package borrows the vocabulary and none of the code: it is an independent homage, asking what that ecosystem's spirit looks like on a chain. Full attribution: NOTICE.
Part of moul/gno-contracts — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage.
Dependency graph:

🧪 Highly experimental — potentially vibe-coded. Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: DISCLAIMER.
Package ns is a Plan 9 namespace: a private, mutable mount table over gno.land/p/moul/x/plan9/ninep file trees.
This is the idea the rest of the suite exists for. Plan 9's leverage does not come from "everything is a file", it comes from every process owning its own name-to-resource mapping, so a name can be REPLACED and any service can be composed, shadowed, sandboxed or mocked without the program knowing. Gno has one global tree and no way to hold a view of it; an Ns is that view.
Bind flags follow bind(2): MREPL replaces, MBEFORE splices the new directory in front of the old one, MAFTER behind it, and MCREATE (OR'd onto any of them) marks the union member that new files are created in.
Two Plan 9 properties are load-bearing and reproduced deliberately:
".." is removed lexically before resolution starts and never reaches a server, per "Lexical File Names in Plan 9, or Getting Dot-Dot Right".
NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the Plan 9 Foundation (https://p9f.org). This package is not affiliated with, endorsed by, or sponsored by them, and contains no Plan 9 code: it borrows the vocabulary so that the design reads without a glossary, and it is an homage, asking what that ecosystem's spirit looks like on a chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
Limits. Resolution costs one call per element per union member, and those calls may cross a realm boundary, so both are capped rather than trusted.
Mount flags, with Plan 9's values.
1var (
2 // ErrNoRoot means the namespace has nothing bound at "/".
3 ErrNoRoot = errors.New("no root in namespace")
4 // ErrWideUnion means a bind would exceed MaxUnion members.
5 ErrWideUnion = errors.New("union too wide")
6 // ErrTooManyOps means the namespace has reached MaxOps bindings.
7 ErrTooManyOps = errors.New("too many namespace operations")
8 // ErrNotBound means unmount was asked to remove something that is not
9 // bound at that name.
10 ErrNotBound = errors.New("not bound")
11)Flag is a bind(2) mount flag. The low two bits pick the mode; MCREATE is OR'd onto it.
Ns is one namespace: a mount table, a working directory, and the ordered list of operations that produced them.
Abs resolves p against the working directory.
Bind makes source visible at target, as bind(1) does. Both names are resolved in THIS namespace, and the source is resolved once: what is stored is the file it names today, not the name.
BindVerb is Bind with the verb that String should print. Plan 9 spells the same operation "bind" or "mount" depending on whether the source is a name or a channel, and ns(1) echoes back whichever was used.
Cd sets the working directory, which must resolve to a directory.
CreateTarget returns the directory that a create at p should happen in.
For a plain directory that is just the directory. For a union it is the first member carrying MCREATE, and if no member has it, creation is refused, which is bind(2)'s rule.
Cwd returns the working directory.
Mount grafts a file tree that has no name in this namespace yet, which is how a service posted by another realm gets in. source is a label, used only when printing the namespace.
Open returns the file a name resolves to: the first member of its union, which is the one a walk through this name would reach.
Ops returns the bindings in application order.
ReadDir lists p. A union directory is the CONCATENATION of its members' contents, as in Plan 9, so duplicate names can appear and shadowing is visible. Pass unique to collapse them first-wins instead, which is the set of names a walk can actually reach.
ReadFile reads the whole file at p.
Resolve returns every union member visible at p, in search order.
Stat returns the entry for p, with the name replaced by the last element of the path as asked for, so that a union member's own name never leaks.
String prints the namespace the way Plan 9's ns(1) does: one line per binding, in the order they were applied, then the working directory.
Unmount undoes bindings at target. With an empty source it removes every binding there, restoring the name to whatever it resolved to originally.
Op records one bind or mount, in application order, so that String can print the namespace the way Plan 9's ns(1) does.