Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
c091c08
fix(client): migrate agentos-client to secure-exec 0.3.4-rc.1 wire API
NathanFlurry Jul 4, 2026
60b3a7b
fix(ci): sparse-index crate verify, wasm-pack for agentos-browser, dr…
NathanFlurry Jul 4, 2026
a40834b
refactor(core,client): remove deprecated moduleAccessCwd in favor of …
NathanFlurry Jul 4, 2026
ca39cf5
refactor(core,client): remove hardcoded agent registry for dynamic ma…
NathanFlurry Jul 4, 2026
ea0500f
refactor(core,client,sidecar): npm-agnostic client, sidecar-owned age…
NathanFlurry Jul 4, 2026
2820e84
refactor(core): run exec chmod/stat/exit in the guest instead of clie…
NathanFlurry Jul 4, 2026
c34fbb8
refactor(core,client): consume sidecar projected commands instead of …
NathanFlurry Jul 4, 2026
59ee957
refactor(core,client): drop client-side snapshot-bundle read; sidecar…
NathanFlurry Jul 4, 2026
a7c7e22
refactor(core,client): forward bootstrap/toolkit command names; drop …
NathanFlurry Jul 4, 2026
b33e1f3
refactor(core,client): forward recursive fs ops as one sidecar reques…
NathanFlurry Jul 4, 2026
97f14c9
refactor(core): thin runtime-compat NativeKernel to use sidecar-repor…
NathanFlurry Jul 4, 2026
1a8ee22
test(core): rework agent-session tests to projected mock packages
NathanFlurry Jul 4, 2026
cf07894
fix(runtime): wire nodeModulesMount into createNodeDriver; drop lefto…
NathanFlurry Jul 4, 2026
578261b
refactor(core): remove dead legacyPackageManifest / legacyManifest
NathanFlurry Jul 4, 2026
ac887c9
chore(deps): bump secure-exec ref to merged #267 and align crate pins…
NathanFlurry Jul 5, 2026
2679e0e
Merge remote-tracking branch 'origin/main' into refactor/remove-hardc…
NathanFlurry Jul 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/refs/secure-exec
Original file line number Diff line number Diff line change
@@ -1 +1 @@
abf0d0172b05e59dfc8bbb698ae73904b2a076b7
78efd873e92b917b2bf2dc08cf1f347a44f2ca10
7 changes: 5 additions & 2 deletions .github/workflows/publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ jobs:
fi
case "$v" in 0.0.0-*) echo "::error::secure_exec_version must be a real release, not a preview"; exit 1;; esac
npm view "@secure-exec/core@$v" version >/dev/null
curl -sf "https://crates.io/api/v1/crates/secure-exec-sidecar/$v" >/dev/null || { echo "::error::secure-exec-sidecar@$v is not on crates.io"; exit 1; }
curl -sf "https://index.crates.io/se/cu/secure-exec-sidecar" | grep -q "\"vers\":\"$v\"" || { echo "::error::secure-exec-sidecar@$v is not on crates.io"; exit 1; }
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "registry_tag=latest" >> "$GITHUB_OUTPUT"
exit 0
Expand Down Expand Up @@ -404,11 +404,14 @@ jobs:
pnpm --filter=publish exec tsx src/ci/bin.ts bump-versions \
--version ${{ needs.context.outputs.version }} \
--version-only
- name: Install wasm-pack (for @rivet-dev/agentos-browser wasm build)
run: |
rustup target add wasm32-unknown-unknown
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
- name: Build TypeScript packages
run: |
npx turbo build \
--filter='!@rivet-dev/agentos-playground' \
--filter='!@agentos/website' \
--filter='!./examples/*' \
--filter='!./examples/quickstart/*'
- name: Finalize package versions for publish (inject optionalDeps)
Expand Down
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ Every limit, timeout, bounded queue/buffer, and per-entity collection MUST be bo
- Subscription methods are delivered through actor events; lifecycle behavior belongs in actor sleep/destroy hooks.
- Agent adapters must use real upstream agent SDKs. Do not replace SDK adapters with direct API-call stubs.
- Host-native agent wrappers are not allowed; agents run through the VM runtime supplied by secure-exec.
- **The client (TS `packages/core` AND Rust `crates/client`) is npm-agnostic.** It only ever knows the tar/dir paths handed to it via the API (`software`/`linkSoftware`/the actor's `packages`). It MUST NOT scan `node_modules`, call `require.resolve`/`createRequire`, read its own `package.json` dependencies, or compute/parse an agent's adapter entrypoint. npm is a *delivery* mechanism only: each `@agentos-software/*` package self-resolves its own tar/dir path (e.g. `import common from "@agentos-software/common"`), and the client forwards `{dir}` on the wire. Default software is a plain import (`resolveDefaultSoftware()` returns `[common].flat()`), not a dependency scan.
- **Agent resolution AND enumeration are OWNED BY THE SIDECAR.** `createSession(name)`/`resumeSession(name)` send only the agent `agentType` (no `adapterEntrypoint` on the wire); the sidecar reads the projected `/opt/agentos/<name>/current/agentos-package.json` and resolves `entrypoint = /opt/agentos/bin/<agent.acpEntrypoint>` plus the manifest's `env` (applied as defaults; caller env wins) and `launchArgs` (prepended before caller args), then spawns. `listAgents()` is a sidecar ACP RPC (`AcpListAgentsRequest`) — the sidecar enumerates the projected `/opt/agentos` packages; the client parses NO manifests for discovery. There is no hardcoded `AGENT_CONFIGS`/`BUILTIN_AGENT_IDS` table and no `/root/node_modules/<adapter>` npm lookup.
- **The Rust (`crates/client`) and TypeScript (`packages/core`) clients MUST behave IDENTICALLY.** They are two implementations of the same client surface; any change to one must be mirrored in the other in the same change. Both send only `agentType`; there is NO client divergence (the old TS-only lazy-link/`resolveDependencyAgents()` Node-module path is removed). The public method surface and wire behavior must match exactly.

## Extension Authoring

Expand Down
3 changes: 0 additions & 3 deletions crates/agentos-actor-plugin/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,6 @@ pub(crate) struct AgentOsConfigJson {
#[serde(default)]
additional_instructions: Option<String>,
#[serde(default)]
module_access_cwd: Option<String>,
#[serde(default)]
loopback_exempt_ports: Vec<u16>,
#[serde(default)]
allowed_node_builtins: Option<Vec<String>>,
Expand Down Expand Up @@ -160,7 +158,6 @@ impl AgentOsConfigJson {
packages_mount_at: self.packages_mount_at.clone(),
loopback_exempt_ports: self.loopback_exempt_ports.clone(),
allowed_node_builtins: self.allowed_node_builtins.clone(),
module_access_cwd: self.module_access_cwd.clone(),
additional_instructions: self.additional_instructions.clone(),
permissions: self.permissions.clone(),
mounts: self
Expand Down
22 changes: 19 additions & 3 deletions crates/agentos-protocol/protocol/agent_os_acp_v1.bare
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ type AcpRuntimeKind enum {
type AcpCreateSessionRequest struct {
agentType: str
runtime: AcpRuntimeKind
adapterEntrypoint: str
cwd: str
args: list<str>
env: map<str><str>
Expand All @@ -29,6 +28,21 @@ type AcpSessionRequest struct {
params: optional<JsonUtf8>
}

# Enumerate the agents available in this VM. The sidecar answers from the already
# projected `/opt/agentos` packages (client parses no manifests).
type AcpListAgentsRequest struct {
reserved: bool
}

type AcpAgentEntry struct {
id: str
installed: bool
}

type AcpListAgentsResponse struct {
agents: list<AcpAgentEntry>
}

type AcpGetSessionStateRequest struct {
sessionId: str
}
Expand Down Expand Up @@ -69,7 +83,8 @@ type AcpRequest union {
AcpGetSessionStateRequest |
AcpCloseSessionRequest |
AcpResumeSessionRequest |
AcpDeliverAgentOutputRequest
AcpDeliverAgentOutputRequest |
AcpListAgentsRequest
}

type AcpSessionCreatedResponse struct {
Expand Down Expand Up @@ -134,7 +149,8 @@ type AcpResponse union {
AcpSessionClosedResponse |
AcpSessionResumedResponse |
AcpErrorResponse |
AcpPendingResponse
AcpPendingResponse |
AcpListAgentsResponse
}

type AcpSessionEvent struct {
Expand Down
1 change: 0 additions & 1 deletion crates/agentos-protocol/tests/roundtrip.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ fn acp_protocol_round_trips_create_session() {
let request = AcpRequest::AcpCreateSessionRequest(AcpCreateSessionRequest {
agent_type: String::from("codex"),
runtime: AcpRuntimeKind::JavaScript,
adapter_entrypoint: String::from("/root/node_modules/agent/adapter.mjs"),
cwd: String::from("/home/agentos"),
args: vec![String::from("--model"), String::from("gpt-5")],
env: [(
Expand Down
131 changes: 98 additions & 33 deletions crates/agentos-sidecar-core/src/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ const SESSION_CLOSE_TIMEOUT_MS: u64 = 5_000;
const INITIALIZE_TIMEOUT_MS: u64 = 10_000;
const SESSION_NEW_TIMEOUT_MS: u64 = 30_000;

/// Reserved `env` key on `AcpResumeSessionRequest` carrying the adapter entrypoint
/// (the resume wire request stays minimal; the caller forwards it through env).
/// Matches the native `RESUME_ADAPTER_ENTRYPOINT_ENV`.
const RESUME_ADAPTER_ENTRYPOINT_ENV: &str = "AGENT_OS_RESUME_ADAPTER_ENTRYPOINT";
/// Matches the native `ACP_RESUME_PROTOCOL_VERSION`.
const ACP_RESUME_PROTOCOL_VERSION: i32 = 1;
/// Matches the native `DEFAULT_RESUME_CLIENT_CAPABILITIES`.
Expand All @@ -43,6 +39,62 @@ const DEFAULT_RESUME_CLIENT_CAPABILITIES: &str = "{}";
/// native `CONTINUATION_PREAMBLE`.
const CONTINUATION_PREAMBLE: &str = "You are continuing an earlier session. The full prior transcript is at `{path}`. Read it with your file tools if you need context before answering.";

/// Agent launch parameters resolved from a projected `/opt/agentos` package
/// manifest. The npm-agnostic client sends only the agent name; the sidecar owns
/// the name -> package -> entrypoint/env/launchArgs resolution. Mirrors the native
/// sidecar's `ResolvedAgent`.
struct ResolvedAgent {
entrypoint: String,
env: BTreeMap<String, String>,
launch_args: Vec<String>,
}

/// The subset of `agentos-package.json` the sidecar needs to launch an agent.
#[derive(serde::Deserialize)]
struct AgentPackageManifest {
#[serde(default)]
agent: Option<AgentPackageAgentBlock>,
}

#[derive(serde::Deserialize)]
struct AgentPackageAgentBlock {
#[serde(rename = "acpEntrypoint", default)]
acp_entrypoint: String,
#[serde(default)]
env: BTreeMap<String, String>,
#[serde(rename = "launchArgs", default)]
launch_args: Vec<String>,
}

/// Resolve an agent name to its launch parameters by reading the projected
/// manifest at `/opt/agentos/<name>/current/agentos-package.json` over the host
/// filesystem seam. A missing file, a missing `agent` block, or an empty
/// `agent.acpEntrypoint` all map to a single typed "unknown agent" error. Mirrors
/// the native sidecar `resolve_agent`.
fn resolve_agent<H: AcpHost>(
host: &mut H,
agent_type: &str,
) -> Result<ResolvedAgent, AcpCoreError> {
let unknown = || {
AcpCoreError::InvalidState(format!(
"unknown agent type \"{agent_type}\": no projected /opt/agentos/{agent_type} package \
with an agent.acpEntrypoint — pass its package to AgentOs software"
))
};
let path = format!("/opt/agentos/{agent_type}/current/agentos-package.json");
let bytes = host.read_file(&path).map_err(|_| unknown())?;
let manifest: AgentPackageManifest = serde_json::from_slice(&bytes).map_err(|_| unknown())?;
let agent = manifest.agent.ok_or_else(|| unknown())?;
if agent.acp_entrypoint.is_empty() {
return Err(unknown());
}
Ok(ResolvedAgent {
entrypoint: format!("/opt/agentos/bin/{}", agent.acp_entrypoint),
env: agent.env,
launch_args: agent.launch_args,
})
}

/// Host-free ACP session engine. The native and browser sidecars each hold one of
/// these and feed it decoded requests plus the caller's connection id (for the
/// per-connection ownership checks) and an [`AcpHost`] for the host-coupled steps.
Expand Down Expand Up @@ -200,16 +252,24 @@ impl AcpCore {
caller_connection_id: &str,
request: &AcpCreateSessionRequest,
) -> Result<AcpResponse, AcpCoreError> {
let resolved = resolve_agent(host, &request.agent_type)?;
let process_id = self.allocate_process_id("acp-agent");
let mut env: BTreeMap<String, String> = request.env.clone().into_iter().collect();
env.insert(String::from("SECURE_EXEC_KEEP_STDIN_OPEN"), String::from("1"));
// Manifest env applies as DEFAULTS; caller/base env wins on conflicts.
for (key, value) in &resolved.env {
env.entry(key.clone()).or_insert_with(|| value.clone());
}
// Manifest launch args first, then any caller-supplied args.
let mut args = resolved.launch_args.clone();
args.extend(request.args.iter().cloned());

let spawned = host.spawn_agent(SpawnAgentRequest {
process_id: process_id.clone(),
runtime: request.runtime.clone(),
entrypoint: Some(request.adapter_entrypoint.clone()),
entrypoint: Some(resolved.entrypoint.clone()),
command: None,
args: request.args.clone(),
args,
env,
cwd: Some(request.cwd.clone()),
})?;
Expand Down Expand Up @@ -257,16 +317,24 @@ impl AcpCore {
caller_connection_id: &str,
request: &AcpCreateSessionRequest,
) -> Result<String, AcpCoreError> {
let resolved = resolve_agent(host, &request.agent_type)?;
let process_id = self.allocate_process_id("acp-agent");
let mut env: BTreeMap<String, String> = request.env.clone().into_iter().collect();
env.insert(String::from("SECURE_EXEC_KEEP_STDIN_OPEN"), String::from("1"));
// Manifest env applies as DEFAULTS; caller/base env wins on conflicts.
for (key, value) in &resolved.env {
env.entry(key.clone()).or_insert_with(|| value.clone());
}
// Manifest launch args first, then any caller-supplied args.
let mut args = resolved.launch_args.clone();
args.extend(request.args.iter().cloned());

let spawned = host.spawn_agent(SpawnAgentRequest {
process_id: process_id.clone(),
runtime: request.runtime.clone(),
entrypoint: Some(request.adapter_entrypoint.clone()),
entrypoint: Some(resolved.entrypoint.clone()),
command: None,
args: request.args.clone(),
args,
env,
cwd: Some(request.cwd.clone()),
})?;
Expand Down Expand Up @@ -671,27 +739,22 @@ impl AcpCore {
caller_connection_id: &str,
request: &AcpResumeSessionRequest,
) -> Result<AcpResponse, AcpCoreError> {
let adapter_entrypoint = request
.env
.get(RESUME_ADAPTER_ENTRYPOINT_ENV)
.cloned()
.ok_or_else(|| {
AcpCoreError::InvalidState(format!(
"resume request missing reserved env `{RESUME_ADAPTER_ENTRYPOINT_ENV}` (adapter entrypoint)"
))
})?;
let resolved = resolve_agent(host, &request.agent_type)?;

let process_id = self.allocate_process_id("acp-agent");
let mut env: BTreeMap<String, String> = request.env.clone().into_iter().collect();
env.remove(RESUME_ADAPTER_ENTRYPOINT_ENV);
env.insert(String::from("SECURE_EXEC_KEEP_STDIN_OPEN"), String::from("1"));
// Manifest env applies as DEFAULTS; caller/base env wins on conflicts.
for (key, value) in &resolved.env {
env.entry(key.clone()).or_insert_with(|| value.clone());
}

let spawned = host.spawn_agent(SpawnAgentRequest {
process_id: process_id.clone(),
runtime: AcpRuntimeKind::JavaScript,
entrypoint: Some(adapter_entrypoint),
entrypoint: Some(resolved.entrypoint.clone()),
command: None,
args: Vec::new(),
args: resolved.launch_args.clone(),
env,
cwd: Some(request.cwd.clone()),
})?;
Expand Down Expand Up @@ -874,6 +937,15 @@ impl AcpCore {
AcpRequest::AcpDeliverAgentOutputRequest(request) => {
self.deliver_agent_output(host, &request)
}
// Agent enumeration needs a directory listing of `/opt/agentos`, which
// the host-free `AcpHost` seam does not expose (it has `read_file`, not
// `read_dir`). The native sidecar answers `list_agents` directly from the
// projected packages; the browser resumable path does not support it yet.
AcpRequest::AcpListAgentsRequest(_) => Err(AcpCoreError::InvalidState(
"list_agents is not handled by the host-free ACP core; the native sidecar answers \
it from the projected /opt/agentos packages"
.to_string(),
)),
}
}

Expand Down Expand Up @@ -1157,7 +1229,7 @@ mod tests {
Ok(())
}
fn read_file(&mut self, _: &str) -> Result<Vec<u8>, AcpCoreError> {
Ok(Vec::new())
Ok(br#"{"name":"echo","agent":{"acpEntrypoint":"echo-agent"}}"#.to_vec())
}
fn now_ms(&self) -> u64 {
0
Expand Down Expand Up @@ -1284,7 +1356,7 @@ mod tests {
Ok(())
}
fn read_file(&mut self, _: &str) -> Result<Vec<u8>, AcpCoreError> {
Ok(Vec::new())
Ok(br#"{"name":"echo","agent":{"acpEntrypoint":"echo-agent"}}"#.to_vec())
}
fn now_ms(&self) -> u64 {
self.clock
Expand Down Expand Up @@ -1375,7 +1447,7 @@ mod tests {
Ok(())
}
fn read_file(&mut self, _: &str) -> Result<Vec<u8>, AcpCoreError> {
Ok(Vec::new())
Ok(br#"{"name":"echo","agent":{"acpEntrypoint":"echo-agent"}}"#.to_vec())
}
fn now_ms(&self) -> u64 {
self.clock
Expand All @@ -1384,17 +1456,12 @@ mod tests {

let mut core = AcpCore::new();
let mut host = ResumeHost::default();
let mut env = HashMap::new();
env.insert(
RESUME_ADAPTER_ENTRYPOINT_ENV.to_string(),
"/bin/echo-agent".to_string(),
);
let request = AcpResumeSessionRequest {
session_id: "old-session".into(),
agent_type: "echo".into(),
transcript_path: Some("/transcripts/old.jsonl".into()),
cwd: "/workspace".into(),
env,
env: HashMap::new(),
};

let response = core
Expand Down Expand Up @@ -1477,7 +1544,7 @@ mod tests {
Ok(())
}
fn read_file(&mut self, _: &str) -> Result<Vec<u8>, AcpCoreError> {
Ok(Vec::new())
Ok(br#"{"name":"echo","agent":{"acpEntrypoint":"echo-agent"}}"#.to_vec())
}
fn now_ms(&self) -> u64 {
self.clock
Expand All @@ -1488,7 +1555,6 @@ mod tests {
let mut host = CreateHost::default();
let request = AcpCreateSessionRequest {
agent_type: "echo".into(),
adapter_entrypoint: "/bin/echo-agent".into(),
runtime: AcpRuntimeKind::JavaScript,
protocol_version: 1,
cwd: "/workspace".into(),
Expand Down Expand Up @@ -1570,7 +1636,7 @@ mod tests {
Ok(())
}
fn read_file(&mut self, _: &str) -> Result<Vec<u8>, AcpCoreError> {
Ok(Vec::new())
Ok(br#"{"name":"echo","agent":{"acpEntrypoint":"echo-agent"}}"#.to_vec())
}
fn now_ms(&self) -> u64 {
0
Expand All @@ -1580,7 +1646,6 @@ mod tests {
fn echo_create_request() -> AcpCreateSessionRequest {
AcpCreateSessionRequest {
agent_type: "echo".into(),
adapter_entrypoint: "/bin/echo-agent".into(),
runtime: AcpRuntimeKind::JavaScript,
protocol_version: 1,
cwd: "/workspace".into(),
Expand Down
Loading
Loading