Skip to main content
Advanced

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

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": "pi", "version": "0.60.0", "bin": { "pi-acp": "dist/acp.js" } }

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