Skip to content

Commit de1bb45

Browse files
committed
test(publish): native platform optional deps + lockstep-adapt to secure-exec 117d645
Native-platform packaging (Darwin + Linux arm64 sidecar + actor-plugin) plus the agent-os<->secure-exec lockstep changes the pinned 117d645 preview requires: - permissions: rename the 'tool' permission domain to 'binding' (tool.invoke -> binding.invoke) across the public Permissions API, the TS + Rust clients, the strict zod options schema (options-schema.ts), tests, examples, and docs. - e2e node_modules mount: build a flat (hoisted, symlink-free) install fixture via 'pnpm deploy --node-linker=hoisted' so the host_dir mount does not reject pnpm store symlinks under RESOLVE_BENEATH (transitive deps like undici present). - s3 tests: supply the now-required chunked_s3 metadataPath. - rebased onto main; clippy unnecessary_map_or fix.
1 parent 4c4c5ae commit de1bb45

52 files changed

Lines changed: 989 additions & 644 deletions

Some content is hidden

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

‎crates/CLAUDE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ These are hard rules with no exceptions:
100100
- **`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.
101101
- **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.
102102
- **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.
103-
- **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.
103+
- **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.
104104
- **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.
105105
- **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.
106106
- **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.

‎crates/agentos-actor-plugin/src/actions/session.rs‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,10 +176,11 @@ pub async fn send_prompt(
176176
// Canonical resume state-machine documentation lives on the sidecar handler
177177
// in `crates/agentos-sidecar/src/acp_extension.rs` (spec §6); this is just
178178
// the actor-side trigger that drives it.
179-
if !vars.live_sessions.contains_key(session_id) && !is_session_live(vm, session_id) {
180-
if session_is_persisted(ctx, session_id).await? {
181-
resume_session(ctx, vm, vars, session_id).await?;
182-
}
179+
if !vars.live_sessions.contains_key(session_id)
180+
&& !is_session_live(vm, session_id)
181+
&& session_is_persisted(ctx, session_id).await?
182+
{
183+
resume_session(ctx, vm, vars, session_id).await?;
183184
}
184185

185186
// Record the outbound prompt text as a synthetic `user_prompt` event BEFORE

‎crates/agentos-actor-plugin/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ impl Drop for RunGuard {
157157
/// event stream closes (host cancel). The VM-dispatch layer (decode actions +
158158
/// drive the sidecar via `agentos-client`) slots into `actor_loop` next.
159159
#[no_mangle]
160+
#[allow(clippy::not_unsafe_ptr_arg_deref)]
160161
pub extern "C" fn rivet_actor_run(
161162
factory: *mut c_void,
162163
host: *const abi::HostVtable,

‎crates/agentos-sidecar/src/acp_extension.rs‎

Lines changed: 29 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -392,35 +392,33 @@ impl AcpExtension {
392392
args.push(String::from("--append-developer-instructions"));
393393
args.push(prompt);
394394
}
395-
"opencode" => {
396-
if !env.contains_key("OPENCODE_CONTEXTPATHS") {
397-
ctx.guest_filesystem_call_wire(GuestFilesystemCallRequest {
398-
operation: GuestFilesystemOperation::WriteFile,
399-
path: String::from(OPENCODE_SYSTEM_PROMPT_PATH),
400-
destination_path: None,
401-
target: None,
402-
content: Some(prompt),
403-
encoding: None,
404-
recursive: false,
405-
mode: None,
406-
uid: None,
407-
gid: None,
408-
atime_ms: None,
409-
mtime_ms: None,
410-
len: None,
411-
offset: None,
412-
})
413-
.await?;
414-
let mut context_paths = OPENCODE_DEFAULT_CONTEXT_PATHS
415-
.iter()
416-
.map(|path| path.to_string())
417-
.collect::<Vec<_>>();
418-
context_paths.push(OPENCODE_SYSTEM_PROMPT_PATH.to_string());
419-
env.insert(
420-
String::from("OPENCODE_CONTEXTPATHS"),
421-
serde_json::to_string(&context_paths).expect("serialize context paths"),
422-
);
423-
}
395+
"opencode" if !env.contains_key("OPENCODE_CONTEXTPATHS") => {
396+
ctx.guest_filesystem_call_wire(GuestFilesystemCallRequest {
397+
operation: GuestFilesystemOperation::WriteFile,
398+
path: String::from(OPENCODE_SYSTEM_PROMPT_PATH),
399+
destination_path: None,
400+
target: None,
401+
content: Some(prompt),
402+
encoding: None,
403+
recursive: false,
404+
mode: None,
405+
uid: None,
406+
gid: None,
407+
atime_ms: None,
408+
mtime_ms: None,
409+
len: None,
410+
offset: None,
411+
})
412+
.await?;
413+
let mut context_paths = OPENCODE_DEFAULT_CONTEXT_PATHS
414+
.iter()
415+
.map(|path| path.to_string())
416+
.collect::<Vec<_>>();
417+
context_paths.push(OPENCODE_SYSTEM_PROMPT_PATH.to_string());
418+
env.insert(
419+
String::from("OPENCODE_CONTEXTPATHS"),
420+
serde_json::to_string(&context_paths).expect("serialize context paths"),
421+
);
424422
}
425423
_ => {}
426424
}
@@ -1267,6 +1265,7 @@ impl AcpSessionRecord {
12671265
}
12681266
}
12691267

1268+
#[allow(clippy::too_many_arguments)]
12701269
async fn send_json_rpc_request(
12711270
ctx: &mut ExtensionContext<'_>,
12721271
process_id: &str,
@@ -1994,9 +1993,7 @@ async fn kill_process_best_effort(ctx: &mut ExtensionContext<'_>, process_id: &s
19941993
/// `agentCapabilities`. Prefer ACP `loadSession`/`session/load`; fall back to the
19951994
/// non-standard `resume`/`session/resume` capability some adapters expose.
19961995
fn native_resume_method(agent_capabilities: Option<&Value>) -> Option<&'static str> {
1997-
let Some(caps) = agent_capabilities.and_then(Value::as_object) else {
1998-
return None;
1999-
};
1996+
let caps = agent_capabilities.and_then(Value::as_object)?;
20001997
if caps
20011998
.get("loadSession")
20021999
.and_then(Value::as_bool)

‎crates/agentos-sidecar/src/main.rs‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ fn main() {
1313

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

1919
/// Initialize tracing for the sidecar.

‎crates/agentos-sidecar/tests/acp_extension.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,9 @@ fn acp_extension_creates_reports_and_closes_session_over_ext() {
180180
};
181181
assert_eq!(envelope.namespace, ACP_EXTENSION_NAMESPACE);
182182
let event: AcpEvent = serde_bare::from_slice(&envelope.payload).expect("decode ACP event");
183-
let AcpEvent::AcpSessionEvent(event) = event;
183+
let AcpEvent::AcpSessionEvent(event) = event else {
184+
panic!("unexpected ACP event: {event:?}");
185+
};
184186
assert_eq!(event.session_id, "adapter-session");
185187
let notification: Value = serde_json::from_str(&event.notification).expect("notification json");
186188
assert_eq!(notification["method"], "session/update");
@@ -769,7 +771,9 @@ fn decode_single_acp_session_event(events: &[EventFrame]) -> Value {
769771
};
770772
assert_eq!(envelope.namespace, ACP_EXTENSION_NAMESPACE);
771773
let event: AcpEvent = serde_bare::from_slice(&envelope.payload).expect("decode ACP event");
772-
let AcpEvent::AcpSessionEvent(event) = event;
774+
let AcpEvent::AcpSessionEvent(event) = event else {
775+
panic!("unexpected ACP event: {event:?}");
776+
};
773777
serde_json::from_str(&event.notification).expect("synthetic notification json")
774778
}
775779

‎crates/client/src/agent_os.rs‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1109,7 +1109,7 @@ fn permissions_policy_config(config: &AgentOsConfig) -> vm_config::PermissionsPo
11091109
),
11101110
binding: Some(
11111111
permissions
1112-
.tool
1112+
.binding
11131113
.as_ref()
11141114
.map(serialize_pattern_permissions_config)
11151115
.unwrap_or(vm_config::PatternPermissionScope::Mode(
@@ -2514,7 +2514,7 @@ async fn invoke_host_tool(
25142514

25152515
if tool_permission_mode(registry.permissions.as_ref(), &callback_key) != PermissionMode::Allow {
25162516
return Err(format!(
2517-
"EACCES: blocked by tool.invoke policy for {callback_key}"
2517+
"EACCES: blocked by binding.invoke policy for {callback_key}"
25182518
));
25192519
}
25202520

@@ -3245,7 +3245,7 @@ fn tool_permission_mode(permissions: Option<&Permissions>, callback_key: &str) -
32453245
let Some(permissions) = permissions else {
32463246
return PermissionMode::Allow;
32473247
};
3248-
let Some(scope) = permissions.tool.as_ref() else {
3248+
let Some(scope) = permissions.binding.as_ref() else {
32493249
return PermissionMode::Allow;
32503250
};
32513251
match scope {
@@ -3561,7 +3561,7 @@ fn permissions_policy(config: &AgentOsConfig) -> wire::PermissionsPolicy {
35613561
),
35623562
binding: Some(
35633563
permissions
3564-
.tool
3564+
.binding
35653565
.as_ref()
35663566
.map(serialize_pattern_permissions)
35673567
.unwrap_or(wire::PatternPermissionScope::PermissionMode(

‎crates/client/src/config.rs‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,7 @@ pub struct Permissions {
553553
#[serde(default, skip_serializing_if = "Option::is_none")]
554554
pub env: Option<PatternPermissions>,
555555
#[serde(default, skip_serializing_if = "Option::is_none")]
556-
pub tool: Option<PatternPermissions>,
556+
pub binding: Option<PatternPermissions>,
557557
}
558558

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

575-
/// `PermissionMode | RulePermissions<PatternPermissionRule>` (network/childProcess/process/env/tool).
575+
/// `PermissionMode | RulePermissions<PatternPermissionRule>` (network/childProcess/process/env/binding).
576576
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
577577
#[serde(untagged)]
578578
pub enum PatternPermissions {

‎crates/client/tests/common/mod.rs‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
use std::path::PathBuf;
99
use std::sync::Once;
1010

11-
use agentos_client::config::{AgentOsConfig, AgentOsSidecarConfig, MountConfig, MountPlugin};
11+
use agentos_client::config::{
12+
AgentOsConfig, AgentOsSidecarConfig, MountConfig, MountPlugin, Permissions,
13+
};
1214
use agentos_client::AgentOs;
1315

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

8486
pub async fn new_vm_with_loopback_ports(loopback_exempt_ports: Vec<u16>) -> AgentOs {
85-
new_vm_with_config(loopback_exempt_ports, Vec::new()).await
87+
new_vm_with_config(loopback_exempt_ports, Vec::new(), None).await
8688
}
8789

8890
pub async fn new_vm_with_wasm_commands() -> AgentOs {
@@ -92,10 +94,18 @@ pub async fn new_vm_with_wasm_commands() -> AgentOs {
9294
pub async fn new_vm_with_wasm_commands_and_loopback_ports(
9395
loopback_exempt_ports: Vec<u16>,
9496
) -> AgentOs {
95-
new_vm_with_config(loopback_exempt_ports, wasm_command_mounts()).await
97+
new_vm_with_config(loopback_exempt_ports, wasm_command_mounts(), None).await
9698
}
9799

98-
async fn new_vm_with_config(loopback_exempt_ports: Vec<u16>, mounts: Vec<MountConfig>) -> AgentOs {
100+
pub async fn new_vm_with_wasm_commands_and_permissions(permissions: Permissions) -> AgentOs {
101+
new_vm_with_config(Vec::new(), wasm_command_mounts(), Some(permissions)).await
102+
}
103+
104+
async fn new_vm_with_config(
105+
loopback_exempt_ports: Vec<u16>,
106+
mounts: Vec<MountConfig>,
107+
permissions: Option<Permissions>,
108+
) -> AgentOs {
99109
ensure_sidecar_env();
100110
AgentOs::create(AgentOsConfig {
101111
loopback_exempt_ports,
@@ -106,6 +116,7 @@ async fn new_vm_with_config(loopback_exempt_ports: Vec<u16>, mounts: Vec<MountCo
106116
.into_owned(),
107117
),
108118
mounts,
119+
permissions,
109120
..Default::default()
110121
})
111122
.await

0 commit comments

Comments
 (0)