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
1 change: 1 addition & 0 deletions .config/nextest.toml
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ binary_id(cyber-account)
| binary_id(cyber-tools::messaging_tools)
| binary_id(cyber-tools::monitor)
| binary_id(cyber-tools::notebook)
| binary_id(cyber-tools::workspace_edits)
| binary_id(cyber-tools::notify)
| binary_id(cyber-tools::ordered_permissions)
| binary_id(cyber-tools::parent_modes)
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/binary-editor-proof.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Binary editor native proof
on:
pull_request:
paths:
- crates/cyber-core/src/binary/**
- crates/cyber-core/Cargo.toml
- Cargo.lock
- .github/workflows/binary-editor-proof.yml
workflow_dispatch:
permissions:
contents: read
concurrency:
group: binary-editor-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
native:
name: Binary storage (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, macos-15, windows-2025]
env:
CARGO_INCREMENTAL: '0'
CARGO_PROFILE_DEV_DEBUG: line-tables-only
CARGO_PROFILE_TEST_DEBUG: line-tables-only
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
key: binary-editor-proof
- name: Check native core
run: cargo clippy --locked -p cyber-core --lib --tests -- -D warnings
- name: Run native storage, draft and recovery proof
shell: bash
run: |
set -euo pipefail
cargo test --locked -p cyber-core --lib binary:: -- --nocapture 2>&1 | tee binary-editor-proof.log
- name: Prove cross-filesystem refusal on actual Linux mounts
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail
cargo test --locked -p cyber-core --lib binary::native::tests::native_cross_filesystem_replacement_refuses_without_copy_delete_fallback -- --ignored --exact --nocapture 2>&1 | tee -a binary-editor-proof.log
- name: Retain native evidence
if: always()
uses: actions/upload-artifact@v4
with:
name: binary-editor-proof-${{ matrix.os }}
path: binary-editor-proof.log
if-no-files-found: warn
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 15 additions & 0 deletions crates/cyber-app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,16 @@ impl App {
// Establish the private directory before Linux sandbox mount planning or
// any plugin, LSP, MCP, hook or model-driven subprocess can launch.
cyber_tools::plugin_host::hide_computer_artifacts(&opts.paths, &opts.home)?;
// Prepare the existing object before sandbox mount planning. Unsafe storage
// disables editing while retaining the live viewer and normal App startup.
let binary_storage = cyber_core::binary::storage::StorageRoot::prepare(&opts.paths.data)
.ok()
.map(Arc::new);
let binary_path = opts.paths.data.join("binary");
cyber_sandbox::hide_credential_file(&opts.home, binary_path.clone());
if let Ok(canonical) = binary_path.canonicalize() {
cyber_sandbox::hide_credential_file(&opts.home, canonical);
}
if let Some(password) = &opts.password {
cyber_sandbox::reserve_owner_credential(password);
}
Expand Down Expand Up @@ -348,6 +358,11 @@ impl App {
interactive: opts.interactive,
snapshots,
});
if let Some(storage) = binary_storage {
runtime
.configure_binary_storage(storage)
.map_err(|error| error.to_string())?;
}
host.attach(runtime.clone());
// One fair scheduler of the slots of every Workflow agent of this server
// (`workflows.max_concurrent`, reduced to the core count), fixed at startup like the
Expand Down
119 changes: 119 additions & 0 deletions crates/cyber-app/tests/files.rs
Original file line number Diff line number Diff line change
Expand Up @@ -872,3 +872,122 @@ async fn binary_close_requires_its_session_but_no_longer_needs_a_live_location()
json!({"data":true})
);
}

#[tokio::test]
async fn binary_private_root_is_hidden_before_drafts_exist_even_with_readable_path_grants() {
use cyber_server::runtime::{Asker, Invocation, ToolHost, ToolOutcome};
let f = Fixture::with(json!({"*": "allow"})).await;
let session = f.session().await;
let info = f._app.runtime.state(&session).await.unwrap().info;
let private = f._app.paths.data.join("binary");
assert!(private.is_dir());
// Created after App bootstrap, as actual draft files would be.
let secret = private.join("owner-draft.json");
std::fs::write(&secret, "PRIVATE UNSAVED OWNER BYTES").unwrap();
let public = f.project.join("public.txt");
std::fs::write(&public, "public positive control").unwrap();
let invoke = |path: &Path, call: &str| Invocation {
tool_output: Default::default(),
registration: None,
session_id: session.clone(),
directory: info.directory.clone(),
agent: info.agent.clone(),
mode: info.mode.clone(),
message_id: "msg_binary_privacy".into(),
call_id: call.into(),
name: "read".into(),
input: json!({"path": path}),
attempt: 1,
operation_key: call.into(),
asker: Asker::detached(),
rules: info.rules.clone(),
};
let result = f
._app
.host
.execute(
invoke(&secret, "call_private"),
tokio_util::sync::CancellationToken::new(),
)
.await;
let ToolOutcome::Failed(reason) = result else {
panic!("private draft was not refused: {result:?}")
};
assert!(reason.contains("Sandbox denied: read"), "{reason}");
assert!(!reason.contains("PRIVATE UNSAVED"));
let result = f
._app
.host
.execute(
invoke(&public, "call_public"),
tokio_util::sync::CancellationToken::new(),
)
.await;
let ToolOutcome::Ok(output) = result else {
panic!("positive read failed: {result:?}")
};
assert!(output.contains("public positive control"));
let mut sandbox = cyber_sandbox::SandboxConfig::resolve(
&Value::Null,
&Default::default(),
None,
&f.root.join("home"),
);
sandbox.readable_paths.push(private.clone());
assert!(sandbox.unreadable(&f.root.join("home")).contains(&private));
}

#[tokio::test]
async fn owner_binary_edit_permission_remains_manual_in_auto_mode_without_inference() {
let f = Fixture::with(json!({"read": "allow", "edit": "ask"})).await;
let (status, session) = f
.send(
"POST",
"/sessions",
Some(json!({"model": "test/main", "mode": "auto"})),
)
.await;
assert_eq!(status, 201, "{session}");
let id = session["data"]["id"].as_str().unwrap();
let turn = f
._app
.runtime
.file_turn(Some(id), &f.project)
.await
.unwrap();
let runtime = f._app.runtime.clone();
let path = f.project.join("blob.bin");
let authorize = tokio::spawn(async move { runtime.authorize_file(&turn, "edit", &path).await });
let mut asked = vec![];
for _ in 0..500 {
asked = pending(&f, id).await;
if !asked.is_empty() || authorize.is_finished() {
break;
}
tokio::time::sleep(Duration::from_millis(10)).await;
}
assert_eq!(asked.len(), 1, "manual permission was not requested");
assert_eq!(
asked[0]["metadata"]["requires_confirmation"], true,
"{asked:?}"
);
assert!(
f._model.requests().is_empty(),
"owner operation invoked a provider"
);
let request = asked[0]["id"].as_str().unwrap();
let (status, result) = f
.send(
"POST",
&format!("/sessions/{id}/permissions/{request}/reply"),
Some(json!({"reply": "once"})),
)
.await;
assert_eq!(status, 204, "{result}");
authorize.await.unwrap().unwrap();
assert!(f._model.requests().is_empty());
assert_eq!(
std::fs::read(f.project.join("blob.bin")).unwrap(),
[0, 159, 146, 150]
);
}
1 change: 1 addition & 0 deletions crates/cyber-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ulid.workspace = true

[target.'cfg(unix)'.dependencies]
libc.workspace = true
rustix = { version = "=1.1.5", default-features = false, features = ["fs", "std"] }

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61.2", features = ["Wdk_Foundation", "Wdk_Storage_FileSystem", "Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Com", "Win32_System_IO", "Win32_System_Threading", "Win32_UI_Shell"] }
Expand Down
Loading
Loading