Packages & Command Resolution
How software is packaged, linked, resolved, and executed in an agentOS VM: a package is a directory, resolution is a $PATH walk, and a file's header picks its runtime.
How a command name becomes a running program, and how the software that provides it
is packaged and linked. Everything is real files under
/opt/agentos — there is no command registry; the
filesystem and $PATH are the only source of truth. For the host API that produces
packages, see Software Definition.
Overview
- Resolve — a real
$PATHwalk over the VFS; the first executable match wins. - Dispatch — by the file’s header (
binfmt): a#!shebang or a magic number. Never the name, never the extension. - Run — on one of three runtimes: JavaScript (V8), WebAssembly, Python (Pyodide). See Processes.
- Confine — every process runs under the VM’s single permission policy. No per-command tiers.
Packages
A package is a directory; its metadata is a normal package.json (name, version,
and a bin command map) plus a small agentos-package.json (the agentOS-specific
name/agent/provides). The shipped package contains real files — it’s a plain npm
dependency. The /opt/agentos/<name>/<version>/ tree below, with its bin/ symlink farm,
is what the runtime projects from that package when it mounts it:
/opt/agentos/<name>/<version>/
├── package.json # name, version, and the "bin" map (command → entry file)
├── agentos-package.json # agentOS metadata: name, optional agent block, provides
├── bin/ # symlinks the PROJECTION builds from package.json "bin"
│ ├── ls → ../libexec/coreutils # → multicall blob
│ └── vdir → ../libexec/coreutils # an "alias" is just another symlink
├── libexec/coreutils # helpers run by other programs, never on $PATH
├── node_modules/ | lib/ # support payload (a JS CLI's flat, self-contained closure)
└── share/man/man1/ls.1 # man pages and other FHS content
/opt/agentos/<name>/current → <version> # version pointer; upgrade re-points it (atomic rename)
| Path | Contents |
|---|---|
package.json | name, version, and a bin map (command → entry file). |
agentos-package.json | agentOS metadata the sidecar reads on mount: name, an optional agent block, and any provides (files/env). Generated for command/WASM packages; carries the agent block for agents. |
bin/ | Command symlinks the projection builds from package.json bin; each basename is the command name. (Not part of the shipped package — npm can’t carry symlinks.) |
libexec/ | Helpers invoked by other programs, never on $PATH (e.g. a multicall blob). |
node_modules/, lib/ | Non-executable payload — bundled deps and assets. |
share/ | FHS data — share/man/man<n>/*, etc. |
current | Symlink → <version>; switching versions is one atomic rename. |
// package.json — commands come from "bin"; an agent's ACP entrypoint is just one of them
{ "name": "pi", "version": "0.60.0", "bin": { "pi-acp": "dist/acp.js" } }
A directory is a valid package when:
- Commands come from
package.jsonbin(command → a real entry file), and each entry dispatches by header — a magic number or#!shebang, no.wasm/.jsextension orruntime/typefield; a headerless entry isENOEXEC. The package ships no symlinks (npm-safe); the runtime builds thebin/farm under/opt/agentositself. - Aliases are symlinks in the projected
bin/farm — several names for one program (or a multicall blob);argv[0]is the invoked name. - It is self-contained — every import/require/asset resolves inside the package; nothing
comes from a host
node_modules, pnpm store, or workspace at runtime (packaging flattens/bundles deps in). - Minimal metadata —
package.jsoncarries only the command set (bin) andversion; there is no command list beyondbin, no permission tiers (the VM policy governs every command), and no dependency list. A smallagentos-package.jsonalongside it holds the agentOS-specific fields the sidecar reads when it mounts the package — thename, an optionalagentblock, and anyprovides(files/env). The client never carries this on the wire; it forwards only the package directory.
Linking
Linking is creating the bin/ symlinks in a $PATH directory. agentOS follows Homebrew:
/opt/agentos/<name> is the cellar, and every command is symlinked into one managed prefix,
/opt/agentos/bin, which is on $PATH. The standard dirs (/usr/bin, /usr/local/bin,
/bin) stay ordinary writable Linux dirs — agentOS never writes to them.
| Software | Stored | Linked into |
|---|---|---|
| Base, mounted, and runtime-installed agentOS software | /opt/agentos/<pkg>/<ver> (or the mount) | /opt/agentos/bin |
| The user’s own files | wherever they put them | /usr/local/bin, /usr/bin, … (normal) |
- Base & mounts link into
/opt/agentos/binin a read-only layer projected from the host and shared across VMs — the symlinks are real but cost nothing per boot. A mounted host directory is linked the same way, with no copy. - Runtime installs add symlinks to
/opt/agentos/binin the writable layer viaagentos-software link— ordinary symlinks, found by the normal walk.
Persistence
Links and installed files are filesystem entries, so they persist exactly when their
filesystem layer does — the same rule as VFS-persistent
pip. A snapshotted/persistent volume keeps runtime installs and links across restart; an
ephemeral one drops them on teardown. There is no package-specific persistence mechanism.
Persisting a layer an untrusted guest can write to also persists whatever the guest linked
there. Treat a guest-writable /usr/local/bin as guest-controlled on restore (see
Confinement & trust).
Execution dispatch (binfmt)
A resolved file’s leading bytes are read into a fixed buffer and dispatched like the Linux
kernel’s binary-format handlers. The command’s name plays no part — python3, node,
and pi are runtimes only by virtue of their files’ headers.
| Header | Result |
|---|---|
#! at bytes 0–1 (binfmt_script) | the interpreter named on the line |
\0asm (00 61 73 6d) | WebAssembly runtime |
\x7fELF / Mach-O / PE | ENOEXEC — foreign binary format, no native-arch handler |
| anything else | ENOEXEC (no implicit /bin/sh fallback here) |
Shebang handling matches binfmt_script:
- The interpreter path is literal and absolute — not
$PATH-searched.#!/usr/bin/env nodeworks only because/usr/bin/envlooks up its argument. - At most one argument follows, not whitespace-split (
#!/usr/bin/env node --flagpassesnode --flagas a single arg). - The header read is bounded to a fixed buffer (
BINPRM_BUF_SIZE); a longer line truncates. Interpreter chaining is depth-bounded (ELOOP); a missing interpreter isENOENT, notENOEXEC.
Shell fallback. On ENOEXEC, a POSIX shell re-runs a headerless script via /bin/sh. That
retry lives in the shell (agentos-shell), not the dispatcher,
which stays strictly binfmt-faithful.
Multicall (busybox-style)
bin/ls → ../libexec/coreutils resolves at open to the shared coreutils blob. argv[0] is
the caller’s value verbatim ("ls") — never derived from the symlink — and the blob selects
its applet with basename(argv[0]), like busybox. Always invoke via the bin/ name; calling the
blob by its own path yields an argv[0] that selects no applet.
Command resolution
A $PATH walk over the VFS, full Linux semantics:
- A name containing
/bypasses$PATHand resolves directly (relative to cwd, or absolute). - Otherwise each
:-separated dir is searched in order; the first executable regular file wins (execute bit required — a non-executable match yieldsEACCES). Left shadows right. - An empty
$PATHelement (leading/trailing/::) means the current working directory — the POSIX footgun, kept for fidelity. - Matches are real VFS files/symlinks —
ls -l-able,stat-able, removable, replaceable. The filesystem is authoritative; there is no resolution cache to grow stale.
The agentos-software CLI
agentos-software link <path>
<path>is a package directory or a node module directory (itspackage.jsonbinmap is the command list).- It brokers a request to the sidecar, which owns the filesystem; the CLI has no privilege of its own.
- Linked names are validated (no
/,.., control chars, overlong names), and for a guest-supplied package each symlink target must resolve inside the package root.
Confinement & trust
Every process runs under the VM’s single permission policy — like a Linux process running with its user/namespace/container privileges, not privileges declared by the binary. A package cannot grant itself permissions. The trust boundary is the sidecar (trusted) vs. the guest (untrusted):
- Linking changes discoverability, not privilege — the policy is enforced at spawn, regardless of how a command was found.
- Shadowing is allowed, Linux-style — a guest may drop a
node/lsinto a writable$PATHdir; trusted in-VM components defend by invoking tools via absolute paths (or a$PATHthat excludes guest-writable dirs). The shadowing binary still runs only under the VM policy. - Guest env is sanitized like a privileged exec —
LD_*,DYLD_*,NODE_OPTIONS,PATH,BASH_ENV,*PRELOADare stripped, as glibc does underAT_SECURE. - Trusted vs. guest packages — symlink-escape checks apply only to guest-writable runtime packages.
- Bounded — the runtime link count is bounded; it warns on approach and fails with a typed error naming the limit (see Limits & Observability).
See also
- Software Definition — the host API that produces these packages.
- Processes — the JavaScript, WebAssembly, and Python runtimes.
- Filesystem — the VFS, layers, and persistence.
- Security Model — the trust boundary and VM permission policy.