Skip to main content
Advanced

Packages & Command Resolution

Understand how agentOS packages software, projects commands into VM paths, resolves executables, and selects the runtime for each file.

These internal architecture docs are mostly generated and maintained by LLMs, then reviewed by humans. They are intentionally verbose; use your preferred LLM to ask focused questions about the architecture as needed.

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

exec “pi”$PATH walk over the VFS/opt/agentos/bin/pia real symlink in the VFSread header (binfmt)#!…nodeJavaScript · V8#!…python3Python · Pyodide\0asmWebAssemblyELF / Mach-O / PEENOEXECspawn under the VM permission policy
  • Resolve — a real $PATH walk 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)
PathContents
package.jsonname, version, and a bin map (command → entry file).
agentos-package.jsonagentOS 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.
currentSymlink → <version>; switching versions is one atomic rename.
// package.json — commands come from "bin"; an agent's ACP entrypoint is just one of them
{ "name": "@agentos-software/pi", "version": "0.0.1", "bin": { "pi-acp": "dist/pi-acp.wasm" } }

A directory is a valid package when:

  • Commands come from package.json bin (command → a real entry file), and each entry dispatches by header — a magic number or #! shebang, no .wasm/.js extension or runtime/type field; a headerless entry is ENOEXEC. The package ships no symlinks (npm-safe); the runtime builds the bin/ farm under /opt/agentos itself.
  • 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 metadatapackage.json carries only the command set (bin) and version; there is no command list beyond bin, no permission tiers (the VM policy governs every command), and no dependency list. A small agentos-package.json alongside it holds the agentOS-specific fields the sidecar reads when it mounts the package — the name, an optional agent block, and any provides (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.

Searched left → right — first match wins (left shadows right)/usr/local/sbin/usr/local/bin/opt/agentos/bin/usr/sbin/usr/bin/sbin/binagentOS links into /opt/agentos/bin; the rest are ordinary writable Linux dirs — drop a binary in /usr/local/bin to shadow an agentOS tool.
SoftwareStoredLinked into
Base, mounted, and runtime-installed agentOS software/opt/agentos/<pkg>/<ver> (or the mount)/opt/agentos/bin
The user’s own fileswherever they put them/usr/local/bin, /usr/bin, … (normal)
  • Base & mounts link into /opt/agentos/bin in 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/bin in the writable layer via agentos-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 partpython3, node, and pi are runtimes only by virtue of their files’ headers.

HeaderResult
#! at bytes 0–1 (binfmt_script)the interpreter named on the line
\0asm (00 61 73 6d)WebAssembly runtime
\x7fELF / Mach-O / PEENOEXEC — foreign binary format, no native-arch handler
anything elseENOEXEC (no implicit /bin/sh fallback here)

Shebang handling matches binfmt_script:

  • The interpreter path is literal and absolute — not $PATH-searched. #!/usr/bin/env node works only because /usr/bin/env looks up its argument.
  • At most one argument follows, not whitespace-split (#!/usr/bin/env node --flag passes node --flag as 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 is ENOENT, not ENOEXEC.

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 $PATH and 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 yields EACCES). Left shadows right.
  • An empty $PATH element (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 (its package.json bin map 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/ls into a writable $PATH dir; trusted in-VM components defend by invoking tools via absolute paths (or a $PATH that 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, *PRELOAD are stripped, as glibc does under AT_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