Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 crates/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ These are hard rules with no exceptions:
- **`RequestPayload::PermissionRequest` is not the kernel permission system.** Kernel permissions route through `PermissionBridge`; the protocol payload is an unsupported legacy host-callback-shaped frame and should be dropped with the ACP core-removal work.
- **Rust permission globs are segment-scoped by default.** In kernel/sidecar permission rules, single `*` and `?` match within one path segment and stop at `/`; use `**` when a rule needs to authorize nested paths or resources across separators.
- **Inspection RPC permissions are separate from runtime setup permissions.** `FindListener` / `FindBoundUdp` require `network.inspect`, and `GetProcessSnapshot` requires `process.inspect`; test fixtures that exercise those handlers still need `child_process.spawn` and `network.listen` allowed so the guest process and listener can start before the inspection request runs.
- **Toolkit registration bootstrap and toolkit invocation use different permission paths.** In `crates/sidecar/src/tools.rs`, sidecar-owned `register_host_callbacks(...)` work should temporarily swap the bridge policy to `PermissionsPolicy::allow_all()` while it refreshes `/bin/agentos*` command stubs, then restore the VM policy; actual guest host-tool execution is separately gated by `tool.invoke` against `<toolkit>:<tool>` resources.
- **Toolkit registration bootstrap and toolkit invocation use different permission paths.** In `crates/sidecar/src/tools.rs`, sidecar-owned `register_host_callbacks(...)` work should temporarily swap the bridge policy to `PermissionsPolicy::allow_all()` while it refreshes `/bin/agentos*` command stubs, then restore the VM policy; actual guest host-tool execution is separately gated by `binding.invoke` against `<toolkit>:<tool>` resources.
- **Sidecar request handlers that need rollback must keep fallible mutations inside a `Result`-returning closure.** In handlers like `register_host_callbacks(...)`, a bare block with `?` returns from the whole request handler before rollback runs; wrap the mutation block in `(|| -> Result<_, SidecarError> { ... })()` so restore/fail-closed cleanup still executes on errors.
- **Native-sidecar VM bootstrap must keep a temporary static policy installed.** During `create_vm` and `configure_vm`, swap in `PermissionsPolicy::allow_all()` for sidecar-owned `/bin`/command-path reconciliation instead of clearing the stored policy entirely, or the `LocalBridge` fallback will deny internal filesystem checks before the guest-visible policy is restored.
- **Filesystem-side teardown races must fail closed too.** In `crates/sidecar/src/filesystem.rs`, guest filesystem and Python VFS handlers should treat missing VMs or active processes as stale teardown races: log and return a rejection/`Ok(())` instead of `expect(...)`, because dispose can win after a request is queued but before the response is emitted.
Expand Down
9 changes: 5 additions & 4 deletions crates/agentos-actor-plugin/src/actions/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,11 @@ pub async fn send_prompt(
// Canonical resume state-machine documentation lives on the sidecar handler
// in `crates/agentos-sidecar/src/acp_extension.rs` (spec §6); this is just
// the actor-side trigger that drives it.
if !vars.live_sessions.contains_key(session_id) && !is_session_live(vm, session_id) {
if session_is_persisted(ctx, session_id).await? {
resume_session(ctx, vm, vars, session_id).await?;
}
if !vars.live_sessions.contains_key(session_id)
&& !is_session_live(vm, session_id)
&& session_is_persisted(ctx, session_id).await?
{
resume_session(ctx, vm, vars, session_id).await?;
}

// Record the outbound prompt text as a synthetic `user_prompt` event BEFORE
Expand Down
1 change: 1 addition & 0 deletions crates/agentos-actor-plugin/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ impl Drop for RunGuard {
/// event stream closes (host cancel). The VM-dispatch layer (decode actions +
/// drive the sidecar via `agentos-client`) slots into `actor_loop` next.
#[no_mangle]
#[allow(clippy::not_unsafe_ptr_arg_deref)]
pub extern "C" fn rivet_actor_run(
factory: *mut c_void,
host: *const abi::HostVtable,
Expand Down
61 changes: 29 additions & 32 deletions crates/agentos-sidecar/src/acp_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -392,35 +392,33 @@ impl AcpExtension {
args.push(String::from("--append-developer-instructions"));
args.push(prompt);
}
"opencode" => {
if !env.contains_key("OPENCODE_CONTEXTPATHS") {
ctx.guest_filesystem_call_wire(GuestFilesystemCallRequest {
operation: GuestFilesystemOperation::WriteFile,
path: String::from(OPENCODE_SYSTEM_PROMPT_PATH),
destination_path: None,
target: None,
content: Some(prompt),
encoding: None,
recursive: false,
mode: None,
uid: None,
gid: None,
atime_ms: None,
mtime_ms: None,
len: None,
offset: None,
})
.await?;
let mut context_paths = OPENCODE_DEFAULT_CONTEXT_PATHS
.iter()
.map(|path| path.to_string())
.collect::<Vec<_>>();
context_paths.push(OPENCODE_SYSTEM_PROMPT_PATH.to_string());
env.insert(
String::from("OPENCODE_CONTEXTPATHS"),
serde_json::to_string(&context_paths).expect("serialize context paths"),
);
}
"opencode" if !env.contains_key("OPENCODE_CONTEXTPATHS") => {
ctx.guest_filesystem_call_wire(GuestFilesystemCallRequest {
operation: GuestFilesystemOperation::WriteFile,
path: String::from(OPENCODE_SYSTEM_PROMPT_PATH),
destination_path: None,
target: None,
content: Some(prompt),
encoding: None,
recursive: false,
mode: None,
uid: None,
gid: None,
atime_ms: None,
mtime_ms: None,
len: None,
offset: None,
})
.await?;
let mut context_paths = OPENCODE_DEFAULT_CONTEXT_PATHS
.iter()
.map(|path| path.to_string())
.collect::<Vec<_>>();
context_paths.push(OPENCODE_SYSTEM_PROMPT_PATH.to_string());
env.insert(
String::from("OPENCODE_CONTEXTPATHS"),
serde_json::to_string(&context_paths).expect("serialize context paths"),
);
}
_ => {}
}
Expand Down Expand Up @@ -1267,6 +1265,7 @@ impl AcpSessionRecord {
}
}

#[allow(clippy::too_many_arguments)]
async fn send_json_rpc_request(
ctx: &mut ExtensionContext<'_>,
process_id: &str,
Expand Down Expand Up @@ -1994,9 +1993,7 @@ async fn kill_process_best_effort(ctx: &mut ExtensionContext<'_>, process_id: &s
/// `agentCapabilities`. Prefer ACP `loadSession`/`session/load`; fall back to the
/// non-standard `resume`/`session/resume` capability some adapters expose.
fn native_resume_method(agent_capabilities: Option<&Value>) -> Option<&'static str> {
let Some(caps) = agent_capabilities.and_then(Value::as_object) else {
return None;
};
let caps = agent_capabilities.and_then(Value::as_object)?;
if caps
.get("loadSession")
.and_then(Value::as_bool)
Expand Down
2 changes: 1 addition & 1 deletion crates/agentos-sidecar/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ fn main() {

/// `1` => true, anything else => false. Mirrors rivet's `env_flag`.
fn env_flag(name: &str) -> bool {
std::env::var(name).map_or(false, |v| v == "1")
std::env::var(name).is_ok_and(|v| v == "1")
}

/// Initialize tracing for the sidecar.
Expand Down
8 changes: 6 additions & 2 deletions crates/agentos-sidecar/tests/acp_extension.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ fn acp_extension_creates_reports_and_closes_session_over_ext() {
};
assert_eq!(envelope.namespace, ACP_EXTENSION_NAMESPACE);
let event: AcpEvent = serde_bare::from_slice(&envelope.payload).expect("decode ACP event");
let AcpEvent::AcpSessionEvent(event) = event;
let AcpEvent::AcpSessionEvent(event) = event else {
panic!("unexpected ACP event: {event:?}");
};
assert_eq!(event.session_id, "adapter-session");
let notification: Value = serde_json::from_str(&event.notification).expect("notification json");
assert_eq!(notification["method"], "session/update");
Expand Down Expand Up @@ -769,7 +771,9 @@ fn decode_single_acp_session_event(events: &[EventFrame]) -> Value {
};
assert_eq!(envelope.namespace, ACP_EXTENSION_NAMESPACE);
let event: AcpEvent = serde_bare::from_slice(&envelope.payload).expect("decode ACP event");
let AcpEvent::AcpSessionEvent(event) = event;
let AcpEvent::AcpSessionEvent(event) = event else {
panic!("unexpected ACP event: {event:?}");
};
serde_json::from_str(&event.notification).expect("synthetic notification json")
}

Expand Down
8 changes: 4 additions & 4 deletions crates/client/src/agent_os.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1109,7 +1109,7 @@ fn permissions_policy_config(config: &AgentOsConfig) -> vm_config::PermissionsPo
),
binding: Some(
permissions
.tool
.binding
.as_ref()
.map(serialize_pattern_permissions_config)
.unwrap_or(vm_config::PatternPermissionScope::Mode(
Expand Down Expand Up @@ -2514,7 +2514,7 @@ async fn invoke_host_tool(

if tool_permission_mode(registry.permissions.as_ref(), &callback_key) != PermissionMode::Allow {
return Err(format!(
"EACCES: blocked by tool.invoke policy for {callback_key}"
"EACCES: blocked by binding.invoke policy for {callback_key}"
));
}

Expand Down Expand Up @@ -3245,7 +3245,7 @@ fn tool_permission_mode(permissions: Option<&Permissions>, callback_key: &str) -
let Some(permissions) = permissions else {
return PermissionMode::Allow;
};
let Some(scope) = permissions.tool.as_ref() else {
let Some(scope) = permissions.binding.as_ref() else {
return PermissionMode::Allow;
};
match scope {
Expand Down Expand Up @@ -3561,7 +3561,7 @@ fn permissions_policy(config: &AgentOsConfig) -> wire::PermissionsPolicy {
),
binding: Some(
permissions
.tool
.binding
.as_ref()
.map(serialize_pattern_permissions)
.unwrap_or(wire::PatternPermissionScope::PermissionMode(
Expand Down
4 changes: 2 additions & 2 deletions crates/client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -553,7 +553,7 @@ pub struct Permissions {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub env: Option<PatternPermissions>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tool: Option<PatternPermissions>,
pub binding: Option<PatternPermissions>,
}

/// `"allow"` or `"deny"`.
Expand All @@ -572,7 +572,7 @@ pub enum FsPermissions {
Rules(RulePermissions<FsPermissionRule>),
}

/// `PermissionMode | RulePermissions<PatternPermissionRule>` (network/childProcess/process/env/tool).
/// `PermissionMode | RulePermissions<PatternPermissionRule>` (network/childProcess/process/env/binding).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum PatternPermissions {
Expand Down
19 changes: 15 additions & 4 deletions crates/client/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
use std::path::PathBuf;
use std::sync::Once;

use agentos_client::config::{AgentOsConfig, AgentOsSidecarConfig, MountConfig, MountPlugin};
use agentos_client::config::{
AgentOsConfig, AgentOsSidecarConfig, MountConfig, MountPlugin, Permissions,
};
use agentos_client::AgentOs;

static INIT: Once = Once::new();
Expand Down Expand Up @@ -82,7 +84,7 @@ pub async fn new_vm_with_sidecar_pool(pool: impl Into<String>) -> AgentOs {
}

pub async fn new_vm_with_loopback_ports(loopback_exempt_ports: Vec<u16>) -> AgentOs {
new_vm_with_config(loopback_exempt_ports, Vec::new()).await
new_vm_with_config(loopback_exempt_ports, Vec::new(), None).await
}

pub async fn new_vm_with_wasm_commands() -> AgentOs {
Expand All @@ -92,10 +94,18 @@ pub async fn new_vm_with_wasm_commands() -> AgentOs {
pub async fn new_vm_with_wasm_commands_and_loopback_ports(
loopback_exempt_ports: Vec<u16>,
) -> AgentOs {
new_vm_with_config(loopback_exempt_ports, wasm_command_mounts()).await
new_vm_with_config(loopback_exempt_ports, wasm_command_mounts(), None).await
}

async fn new_vm_with_config(loopback_exempt_ports: Vec<u16>, mounts: Vec<MountConfig>) -> AgentOs {
pub async fn new_vm_with_wasm_commands_and_permissions(permissions: Permissions) -> AgentOs {
new_vm_with_config(Vec::new(), wasm_command_mounts(), Some(permissions)).await
}

async fn new_vm_with_config(
loopback_exempt_ports: Vec<u16>,
mounts: Vec<MountConfig>,
permissions: Option<Permissions>,
) -> AgentOs {
ensure_sidecar_env();
AgentOs::create(AgentOsConfig {
loopback_exempt_ports,
Expand All @@ -106,6 +116,7 @@ async fn new_vm_with_config(loopback_exempt_ports: Vec<u16>, mounts: Vec<MountCo
.into_owned(),
),
mounts,
permissions,
..Default::default()
})
.await
Expand Down
Loading
Loading