Skip to content

Commit 43097a2

Browse files
committed
feat(sandbox): guest-native /opt/agentos granular tar mounts
Serve packages directly from package.tar via a tar-backed read-only VFS (mmap + digest index, no extraction), compose /opt/agentos from granular leaf mounts (tar per pkg-version + single-symlink per bin/<cmd> and current), resolve symlinks across mounts, and move exec bits to pack time. Carries in-progress sandbox-multiline-prompt work (registry/software + doc cleanup).
1 parent 94f540b commit 43097a2

75 files changed

Lines changed: 3228 additions & 2874 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

BROWSER-CONVERGENCE-ARCHITECTURE.md

Lines changed: 0 additions & 961 deletions
This file was deleted.

BROWSER-CONVERGENCE-HARDENING.md

Lines changed: 0 additions & 354 deletions
This file was deleted.

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Every bound that protects a shared resource — memory/heap, CPU/wall-clock, fd/
4545
- **No expensive objects per-call.** Build once, reuse via a pool/persistent worker. Never construct per-operation: Tokio runtime, OS thread, V8 isolate/snapshot, DNS resolver, HTTP client, connection pool. Construct-then-teardown every call IS the bug.
4646
- **No serialize→deserialize in-process.** Pass the typed struct directly; wire encoding is for the wire only. Don't encode a frame to bytes only to re-parse it into a command.
4747
- **No whole-buffer copies per I/O.** Use chunked `Vec<u8>` + `extend_from_slice`, not byte-by-byte fills; move/`Arc`/slice payloads — never clone a record that carries its full buffer on each read/write.
48+
- **Zero host-disk writes on VM start (mount, don't extract).** Mounting read-only packaged content (the `agentos_packages` tar projection, or any archive/content mount) MUST NOT extract the archive to host disk or write physical symlinks at VM/session start. Back content by byte-range reads over the source archive (mmap/seek into the uncompressed tar, digest-keyed and shared across VMs), and serve derived symlinks (the `/opt/agentos/bin` farm, `<pkg>/current`) as **in-memory synthetic mounts**, never on-disk nodes. Extraction/staging is redundant work + state (full unpack, ~32k inodes, symlink farm, temp cleanup, double on-disk copy) for bytes the archive already holds at a known offset — reintroducing it is the bug. The only writes VM start may incur are cheap in-RAM overlay mountpoint dirs materialized by the mount table; host-disk materialization is zero. Any code that "helpfully" extracts must be reverted; the tar reader and projection carry doc comments saying why.
4849
- **No per-call allocs/locks/clones** on the sync hot path.
4950
- **Avoid polling**, prefer readiness/event-driven. But a read-probe can be load-bearing for protocol correctness — measure before removing one, and keep its semantic test.
5051
- **No baseline, no merge.** Capture native + unoptimized numbers BEFORE touching code, gate every change on a measured before/after delta, and keep it measure-gated.

Cargo.lock

Lines changed: 35 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

DOCS-GAPS.md

Lines changed: 0 additions & 482 deletions
This file was deleted.

REBASE-ONTO-MAIN.md

Lines changed: 0 additions & 162 deletions
This file was deleted.

crates/execution/src/javascript.rs

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ const NODE_IMPORT_CACHE_LOADER_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_LOADE
4646
const NODE_IMPORT_CACHE_PATH_ENV: &str = "AGENTOS_NODE_IMPORT_CACHE_PATH";
4747
const NODE_KEEP_STDIN_OPEN_ENV: &str = "SECURE_EXEC_KEEP_STDIN_OPEN";
4848
const NODE_GUEST_ENTRYPOINT_ENV: &str = "AGENTOS_GUEST_ENTRYPOINT";
49+
const NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV: &str = "AGENTOS_GUEST_ENTRYPOINT_MODULE_MODE";
4950
const NODE_GUEST_PATH_MAPPINGS_ENV: &str = "AGENTOS_GUEST_PATH_MAPPINGS";
5051
const NODE_VIRTUAL_PROCESS_EXEC_PATH_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_EXEC_PATH";
5152
const NODE_VIRTUAL_PROCESS_PID_ENV: &str = "AGENTOS_VIRTUAL_PROCESS_PID";
@@ -274,6 +275,7 @@ const RESERVED_NODE_ENV_KEYS: &[&str] = &[
274275
NODE_SANDBOX_ROOT_ENV,
275276
NODE_FROZEN_TIME_ENV,
276277
NODE_GUEST_ENTRYPOINT_ENV,
278+
NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV,
277279
NODE_GUEST_ARGV_ENV,
278280
NODE_GUEST_PATH_MAPPINGS_ENV,
279281
NODE_VIRTUAL_PROCESS_EXEC_PATH_ENV,
@@ -2278,6 +2280,22 @@ impl JavascriptExecutionEngine {
22782280
let host_entrypoint = translator.resolve_host_entrypoint(&request.cwd, &request.argv[0]);
22792281
let guest_entrypoint = if request.argv[0] == "-e" || request.argv[0] == "--eval" {
22802282
request.argv[0].clone()
2283+
} else if let Some(explicit_guest_entrypoint) = request
2284+
.env
2285+
.get(NODE_GUEST_ENTRYPOINT_ENV)
2286+
.filter(|value| value.starts_with('/'))
2287+
{
2288+
// Part B (guest-VFS adapter launch): the sidecar already resolved the
2289+
// GUEST entrypoint path (AGENTOS_GUEST_ENTRYPOINT). Use it directly as
2290+
// the sourceURL / module-resolution base instead of translating the
2291+
// host entrypoint — `host_to_guest_string` misses for guest-native
2292+
// mounts (`agentos_packages`, whose host staging dir is not in the
2293+
// translation map) and falls back to `/unknown/<cmd>`, which then
2294+
// poisons the adapter's own relative/bare imports. For host-backed
2295+
// mounts the two values are equal, so this is a no-op there. Applies
2296+
// to child launches too (they set AGENTOS_GUEST_ENTRYPOINT as well),
2297+
// which is the child-process `/unknown` case.
2298+
explicit_guest_entrypoint.clone()
22812299
} else {
22822300
translator.host_to_guest_string(&host_entrypoint)
22832301
};
@@ -2295,7 +2313,11 @@ impl JavascriptExecutionEngine {
22952313
.inline_code
22962314
.clone()
22972315
.map(|inline_code| strip_javascript_hashbang(&inline_code));
2298-
let use_module_mode = host_entrypoint_uses_module_mode(&host_entrypoint)
2316+
let use_module_mode = request
2317+
.env
2318+
.get(NODE_GUEST_ENTRYPOINT_MODULE_MODE_ENV)
2319+
.is_some_and(|value| value == "1" || value.eq_ignore_ascii_case("true"))
2320+
|| host_entrypoint_uses_module_mode(&host_entrypoint)
22992321
|| inline_code
23002322
.as_deref()
23012323
.is_some_and(inline_code_uses_module_mode);

crates/kernel/src/kernel.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3355,6 +3355,12 @@ impl<F: VirtualFileSystem + 'static> KernelVm<F> {
33553355
} else {
33563356
normalize_path(&format!("{cwd}/{command}"))
33573357
};
3358+
// exec(2) follows symlinks, and a symlink target may live in a different
3359+
// mount (e.g. `/opt/agentos/bin/<cmd>` is its own single-symlink mount
3360+
// pointing into a package tar mount). Resolve the real path before
3361+
// stat-ing / reading the executable so cross-mount symlinked commands
3362+
// exec their real target instead of failing to read the symlink node.
3363+
let path = self.filesystem.realpath(&path).unwrap_or(path);
33583364
let stat = self.filesystem.stat(&path)?;
33593365
if stat.is_directory {
33603366
return Err(KernelError::new(

0 commit comments

Comments
 (0)