fix: align Workdir session cwd and model environment
Project alias, cwd, scope and command ceilings from actual sessions. Bind initial attachment cwd once in the shared broker/router while keeping root-relative delegation and detach generations. Preserve caller coordinates across reads, searches, checked checkout and commands; keep provider authorization and symlink boundaries. Connect real SubWorkers, all filesystem Tools, scoped aliases, HTTP providers and limited Host grants in permanent regressions. Validated related normal suites (workdir http-client, tools, worker, worker-runtime, server-api typescript, fs-operation), workspace-server normal suite (1272 passed, 3 existing ignores), root check, product binary builds, rustdoc/doctests, generated contract checks, format and state-contract names. Test roots are checkout-local; Git parent discovery requires GIT_CEILING_DIRECTORIES at TMPDIR, confirmed against exact b860 baseline.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
//! hashes alone do not accept same-content inode replacements. Callers own Read line numbering
|
||||
//! and per-attachment read history.
|
||||
//!
|
||||
//! Create paths are checkout-root-relative and must be strict descendants of the observed target
|
||||
//! Create paths use the target's session coordinates and must be strict descendants of the observed target
|
||||
//! directory. Existing destinations are never overwritten; results return the updated observation
|
||||
//! of the retained target directory, not the created file. Read results also return the validator
|
||||
//! of the state actually read, never a replacement from a later independent stat.
|
||||
@@ -18,7 +18,8 @@
|
||||
//!
|
||||
//! Search delegates to shared List/Glob/Grep engines. Scope rules form unions within layers and
|
||||
//! intersections across layers, provider authority, and output roots. Wrappers transform coordinates
|
||||
//! once; returned paths and Grep content are relative to the final session's logical root.
|
||||
//! once; returned paths and Grep content are relative to the composed output root (the caller's
|
||||
//! cwd by default).
|
||||
//! Paths are not reverse-parsed from rendered filenames; non-UTF-8 names are not lossily aliased.
|
||||
//! Unsupported providers reject search rather than fall back to broader trait searches. Search is
|
||||
//! live traversal, not an atomic snapshot of all descendants or exclusion of external editors.
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
//! Model-facing session coordinates, separate from provider paths and authority.
|
||||
|
||||
use crate::{WorkdirPath, WorkdirSessionCapabilities, WorkdirToolScopeRule};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// A snapshot of the environment actually used by a session's operations.
|
||||
///
|
||||
/// Cwd is a logical Workdir-root coordinate. Scope targets use that root or
|
||||
/// explicit absolute provider paths. Scope is an additional allow ceiling, not
|
||||
/// a replacement for provider checks. Providers
|
||||
/// may retain further restrictions (including deny rules); this projection
|
||||
/// never authorizes an operation.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkdirSessionEnvironment {
|
||||
#[serde(deserialize_with = "deserialize_cwd")]
|
||||
pub cwd: WorkdirPath,
|
||||
pub scope: Option<Vec<WorkdirToolScopeRule>>,
|
||||
pub capabilities: WorkdirSessionCapabilities,
|
||||
pub restrictions: Vec<String>,
|
||||
}
|
||||
|
||||
fn deserialize_cwd<'de, D: serde::Deserializer<'de>>(
|
||||
deserializer: D,
|
||||
) -> Result<WorkdirPath, D::Error> {
|
||||
let path = String::deserialize(deserializer)?;
|
||||
WorkdirPath::new(path).map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
impl WorkdirSessionEnvironment {
|
||||
pub fn root(capabilities: WorkdirSessionCapabilities) -> Self {
|
||||
Self {
|
||||
cwd: WorkdirPath::root(),
|
||||
scope: None,
|
||||
capabilities,
|
||||
restrictions: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn wire_cwd_is_normalized_and_cannot_claim_an_absolute_or_parent_base() {
|
||||
let mut wire = serde_json::to_value(WorkdirSessionEnvironment::root(
|
||||
WorkdirSessionCapabilities::READ_ONLY,
|
||||
))
|
||||
.unwrap();
|
||||
for invalid in [
|
||||
"/absolute",
|
||||
"../parent",
|
||||
"nested/../parent",
|
||||
"nested\\other",
|
||||
] {
|
||||
wire["cwd"] = invalid.into();
|
||||
assert!(
|
||||
serde_json::from_value::<WorkdirSessionEnvironment>(wire.clone()).is_err(),
|
||||
"{invalid}"
|
||||
);
|
||||
}
|
||||
wire["cwd"] = "./nested//child".into();
|
||||
let decoded: WorkdirSessionEnvironment = serde_json::from_value(wire).unwrap();
|
||||
assert_eq!(decoded.cwd.as_str(), "nested/child");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
//! Real Local/scoped/read-only/router sessions across loopback HTTP. Only the
|
||||
//! HTTP authentication boundary is a fixture; coordinate and scope checks are
|
||||
//! production implementations. Runs in the normal http-client library suite.
|
||||
#![cfg(all(feature = "http-client", target_os = "linux"))]
|
||||
|
||||
use crate::http::*;
|
||||
use crate::*;
|
||||
use axum::{
|
||||
Json, Router,
|
||||
extract::State,
|
||||
routing::{delete, post},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
struct Server(tokio::task::JoinHandle<()>);
|
||||
impl Drop for Server {
|
||||
fn drop(&mut self) {
|
||||
self.0.abort();
|
||||
}
|
||||
}
|
||||
|
||||
async fn open(State(session): State<WorkdirSessionHandle>) -> Json<OpenWorkdirSessionResponse> {
|
||||
Json(OpenWorkdirSessionResponse {
|
||||
session_id: WorkdirSessionId::new("environment-session").unwrap(),
|
||||
workdir_id: session.workdir().id().clone(),
|
||||
capabilities: session.capabilities(),
|
||||
environment: session.environment(),
|
||||
})
|
||||
}
|
||||
async fn operation(
|
||||
State(session): State<WorkdirSessionHandle>,
|
||||
Json(request): Json<WorkdirSessionOperationRequest>,
|
||||
) -> Result<
|
||||
Json<WorkdirSessionOperationResult>,
|
||||
(axum::http::StatusCode, Json<WorkdirTransportError>),
|
||||
> {
|
||||
dispatch_workdir_session_operation(session.as_ref(), request.operation)
|
||||
.await
|
||||
.map(Json)
|
||||
.map_err(|error| {
|
||||
(
|
||||
axum::http::StatusCode::FORBIDDEN,
|
||||
Json(WorkdirTransportError::from_workdir_error(&error)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn remote_environment_matches_scoped_readonly_operations_without_rebasing_twice() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
std::fs::create_dir(root.path().join("nested")).unwrap();
|
||||
std::fs::write(root.path().join("nested/file.txt"), "nested marker").unwrap();
|
||||
std::fs::write(root.path().join("file.txt"), "root decoy").unwrap();
|
||||
let broker = WorkdirToolBroker::new(Arc::new(LocalWorkdirSession::new(
|
||||
manifest::Scope::writable(root.path()).unwrap(),
|
||||
root.path().to_path_buf(),
|
||||
)));
|
||||
let lease = broker
|
||||
.scope(WorkdirToolScope {
|
||||
cwd: WorkdirPath::new("nested").unwrap(),
|
||||
rules: vec![WorkdirToolScopeRule {
|
||||
target: WorkdirPath::new("nested").unwrap(),
|
||||
permission: WorkdirToolScopePermission::Write,
|
||||
recursive: true,
|
||||
symlink_policy: Default::default(),
|
||||
}],
|
||||
command: true,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let session: WorkdirSessionHandle = Arc::new(ReadOnlyWorkdirSession::new(lease.tool_session()));
|
||||
let expected = session.environment();
|
||||
let app = Router::new()
|
||||
.route("/v1/working-directories/{id}/sessions", post(open))
|
||||
.route("/v1/workdir-sessions/{id}/operations", post(operation))
|
||||
.route(
|
||||
"/v1/workdir-sessions/{id}",
|
||||
delete(|| async { axum::http::StatusCode::NO_CONTENT }),
|
||||
)
|
||||
.with_state(session.clone());
|
||||
let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let address = listener.local_addr().unwrap();
|
||||
let _server = Server(tokio::spawn(async move {
|
||||
axum::serve(listener, app).await.unwrap();
|
||||
}));
|
||||
let remote: WorkdirSessionHandle = Arc::new(
|
||||
RemoteWorkdirSession::open(
|
||||
reqwest::Client::builder().no_proxy().build().unwrap(),
|
||||
format!("http://{address}").parse().unwrap(),
|
||||
"fixture",
|
||||
session.workdir().id().clone(),
|
||||
OpenWorkdirSessionRequest::default(),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
let router = WorkdirSessionRouter::new();
|
||||
router
|
||||
.attach(WorkdirAttachmentAlias::new("remote").unwrap(), remote)
|
||||
.unwrap();
|
||||
let selected = router.resolve(Some("remote")).unwrap().session;
|
||||
assert_eq!(selected.environment(), expected);
|
||||
assert_eq!(expected.cwd, WorkdirPath::new("nested").unwrap());
|
||||
assert!(
|
||||
!expected
|
||||
.capabilities
|
||||
.supports(WorkdirSessionCapability::Command)
|
||||
);
|
||||
assert!(
|
||||
expected
|
||||
.scope
|
||||
.unwrap()
|
||||
.iter()
|
||||
.all(|rule| rule.permission == WorkdirToolScopePermission::Read)
|
||||
);
|
||||
let found = selected
|
||||
.glob(GlobRequest {
|
||||
path: WorkdirPath::root(),
|
||||
pattern: "*.txt".into(),
|
||||
limit: 10,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(found.paths, vec![WorkdirPath::new("file.txt").unwrap()]);
|
||||
let path = found.paths[0].clone();
|
||||
let read = selected
|
||||
.read(ReadRequest {
|
||||
path: path.clone(),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
max_bytes: 100,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(read.path, path);
|
||||
assert_eq!(read.bytes, b"nested marker");
|
||||
assert!(
|
||||
selected
|
||||
.write(WriteRequest {
|
||||
path,
|
||||
content: b"denied".to_vec(),
|
||||
expected_hash: Some(read.content_hash)
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
selected
|
||||
.read(ReadRequest {
|
||||
path: WorkdirPath::new_scoped(root.path().join("file.txt").to_str().unwrap())
|
||||
.unwrap(),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
max_bytes: 100
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
selected
|
||||
.start_command(CommandRequest {
|
||||
command: "touch escaped".into(),
|
||||
cwd: WorkdirPath::root(),
|
||||
timeout_secs: 5,
|
||||
transport_byte_budget: 100,
|
||||
tool_call_id: None
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
assert!(!root.path().join("nested/escaped").exists());
|
||||
router.close_all().await.unwrap();
|
||||
lease.close().await.unwrap();
|
||||
broker.close().await.unwrap();
|
||||
}
|
||||
@@ -54,6 +54,7 @@ pub struct OpenWorkdirSessionResponse {
|
||||
pub session_id: WorkdirSessionId,
|
||||
pub workdir_id: WorkdirId,
|
||||
pub capabilities: WorkdirSessionCapabilities,
|
||||
pub environment: crate::WorkdirSessionEnvironment,
|
||||
}
|
||||
|
||||
/// One provider-side Workdir operation.
|
||||
@@ -553,6 +554,7 @@ mod client {
|
||||
workdir: Workdir,
|
||||
session_id: WorkdirSessionId,
|
||||
capabilities: WorkdirSessionCapabilities,
|
||||
environment: crate::WorkdirSessionEnvironment,
|
||||
closed: AtomicBool,
|
||||
}
|
||||
|
||||
@@ -612,6 +614,7 @@ mod client {
|
||||
workdir: Workdir::new(opened.workdir_id.as_str()),
|
||||
session_id: opened.session_id,
|
||||
capabilities: opened.capabilities,
|
||||
environment: opened.environment,
|
||||
closed: AtomicBool::new(false),
|
||||
})
|
||||
}
|
||||
@@ -676,6 +679,12 @@ mod client {
|
||||
self.capabilities
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::WorkdirSessionEnvironment {
|
||||
let mut environment = self.environment.clone();
|
||||
environment.capabilities = self.capabilities();
|
||||
environment
|
||||
}
|
||||
|
||||
async fn authorize_scope_path(
|
||||
&self,
|
||||
request: WorkdirScopeAuthorizationRequest,
|
||||
@@ -1209,6 +1218,9 @@ mod client {
|
||||
workdir,
|
||||
session_id: WorkdirSessionId::new("s").unwrap(),
|
||||
capabilities: WorkdirSessionCapabilities::EMPTY,
|
||||
environment: crate::WorkdirSessionEnvironment::root(
|
||||
WorkdirSessionCapabilities::EMPTY,
|
||||
),
|
||||
closed: AtomicBool::new(false),
|
||||
};
|
||||
let stat = StatRequest {
|
||||
|
||||
@@ -5,6 +5,8 @@ pub use checkout::{
|
||||
CheckoutObservation, CheckoutOperation, CheckoutOutput, CheckoutRequest, CheckoutResult,
|
||||
CheckoutSearchOperation, CheckoutSearchRequest, CheckoutSearchResult,
|
||||
};
|
||||
mod environment;
|
||||
pub use environment::WorkdirSessionEnvironment;
|
||||
pub mod external;
|
||||
pub mod http;
|
||||
mod local;
|
||||
@@ -16,6 +18,8 @@ pub mod workspace;
|
||||
#[cfg(test)]
|
||||
mod checkout_search_tests;
|
||||
#[cfg(test)]
|
||||
mod environment_tests;
|
||||
#[cfg(test)]
|
||||
mod external_local_tests;
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
@@ -185,6 +189,9 @@ pub type WriteOutcome = WriteResult;
|
||||
/// Worker-bound operation session for a persistent [`Workdir`].
|
||||
///
|
||||
/// Filesystem search and commands run on the host owning materialization.
|
||||
/// Relative operation paths and reusable search results use the session's cwd;
|
||||
/// root-coordinate providers are adapted once at attachment, not by each Tool.
|
||||
/// Scope declarations retain their own root-coordinate authority contract.
|
||||
/// Session closure is terminal but does not delete the Workdir or files.
|
||||
/// Unsupported checkout/search implementations explicitly reject requests, with no ambient
|
||||
/// filesystem fallback. Providers own identity validators; callers own read hash history
|
||||
@@ -194,6 +201,13 @@ pub trait WorkdirSession: std::fmt::Debug + Send + Sync {
|
||||
fn workdir(&self) -> &Workdir;
|
||||
fn capabilities(&self) -> WorkdirSessionCapabilities;
|
||||
|
||||
/// Describe the coordinates used by this session, not a caller's Profile
|
||||
/// or filesystem authority. Root-coordinate providers use the default;
|
||||
/// coordinate and authority wrappers must project their effective view.
|
||||
fn environment(&self) -> WorkdirSessionEnvironment {
|
||||
WorkdirSessionEnvironment::root(self.capabilities())
|
||||
}
|
||||
|
||||
/// Validate an attenuated filesystem rule at the provider boundary without
|
||||
/// exposing the resolved host path. Providers that cannot resolve symbolic
|
||||
/// links must reject resolved-policy checks rather than downgrade them.
|
||||
|
||||
@@ -1340,6 +1340,52 @@ impl WorkdirSession for LocalWorkdirSession {
|
||||
self.inner.capabilities
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::WorkdirSessionEnvironment {
|
||||
let scope = self.inner.scope.snapshot();
|
||||
let project = |rule: manifest::ScopeRule| -> Option<crate::WorkdirToolScopeRule> {
|
||||
let target = if self.inner.root.starts_with(&rule.target) && rule.recursive {
|
||||
WorkdirPath::root()
|
||||
} else {
|
||||
let path = rule
|
||||
.target
|
||||
.strip_prefix(&self.inner.root)
|
||||
.unwrap_or(&rule.target);
|
||||
WorkdirPath::new_scoped(path.to_str()?).ok()?
|
||||
};
|
||||
Some(crate::WorkdirToolScopeRule {
|
||||
target,
|
||||
permission: match rule.permission {
|
||||
Permission::Read => WorkdirToolScopePermission::Read,
|
||||
Permission::Write => WorkdirToolScopePermission::Write,
|
||||
},
|
||||
recursive: rule.recursive,
|
||||
symlink_policy: rule.symlink_policy,
|
||||
})
|
||||
};
|
||||
crate::WorkdirSessionEnvironment {
|
||||
cwd: WorkdirPath::root(),
|
||||
scope: Some(
|
||||
scope
|
||||
.allow_rules()
|
||||
.into_iter()
|
||||
.filter_map(project)
|
||||
.collect(),
|
||||
),
|
||||
capabilities: self.capabilities(),
|
||||
restrictions: scope
|
||||
.deny_rules()
|
||||
.into_iter()
|
||||
.map(|rule| {
|
||||
project(rule)
|
||||
.map(|rule| format!("Deny: {rule:?}"))
|
||||
.unwrap_or_else(|| {
|
||||
"Unrepresentable provider deny rule remains enforced".into()
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn authorize_scope_path(
|
||||
&self,
|
||||
request: WorkdirScopeAuthorizationRequest,
|
||||
|
||||
@@ -134,6 +134,9 @@ async fn pipe_and_wait_faults_cross_real_http_with_prefix_and_terminal_replay()
|
||||
session_id: WorkdirSessionId::new("fault-session").unwrap(),
|
||||
workdir_id: crate::Workdir::new("fault-workdir").id().clone(),
|
||||
capabilities: crate::WorkdirSessionCapabilities::ALL,
|
||||
environment: crate::WorkdirSessionEnvironment::root(
|
||||
crate::WorkdirSessionCapabilities::ALL,
|
||||
),
|
||||
})
|
||||
}),
|
||||
)
|
||||
|
||||
+236
-31
@@ -225,16 +225,27 @@ impl Drop for ActiveOperation<'_> {
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RoutedWorkdirSession {
|
||||
state: Arc<RoutedSessionState>,
|
||||
view: Option<WorkdirSessionHandle>,
|
||||
}
|
||||
|
||||
impl RoutedWorkdirSession {
|
||||
fn inner(&self) -> &WorkdirSessionHandle {
|
||||
self.view.as_ref().unwrap_or(&self.state.session)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl WorkdirSession for RoutedWorkdirSession {
|
||||
fn workdir(&self) -> &Workdir {
|
||||
self.state.session.workdir()
|
||||
self.inner().workdir()
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> WorkdirSessionCapabilities {
|
||||
self.state.session.capabilities()
|
||||
self.inner().capabilities()
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::WorkdirSessionEnvironment {
|
||||
self.inner().environment()
|
||||
}
|
||||
|
||||
async fn authorize_scope_path(
|
||||
@@ -242,7 +253,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
request: WorkdirScopeAuthorizationRequest,
|
||||
) -> Result<(), WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.authorize_scope_path(request).await
|
||||
self.inner().authorize_scope_path(request).await
|
||||
}
|
||||
|
||||
async fn scope_rules_overlap(
|
||||
@@ -250,7 +261,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
request: WorkdirScopeOverlapRequest,
|
||||
) -> Result<bool, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.scope_rules_overlap(request).await
|
||||
self.inner().scope_rules_overlap(request).await
|
||||
}
|
||||
|
||||
async fn checkout_search(
|
||||
@@ -258,7 +269,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
request: crate::CheckoutSearchRequest,
|
||||
) -> Result<crate::CheckoutSearchResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.checkout_search(request).await
|
||||
self.inner().checkout_search(request).await
|
||||
}
|
||||
|
||||
async fn checkout_observe(
|
||||
@@ -266,59 +277,59 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
path: crate::WorkdirPath,
|
||||
) -> Result<crate::CheckoutObservation, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.checkout_observe(path).await
|
||||
self.inner().checkout_observe(path).await
|
||||
}
|
||||
async fn checkout_execute(
|
||||
&self,
|
||||
request: crate::CheckoutRequest,
|
||||
) -> Result<crate::CheckoutResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.checkout_execute(request).await
|
||||
self.inner().checkout_execute(request).await
|
||||
}
|
||||
|
||||
async fn stat(&self, request: StatRequest) -> Result<StatResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.stat(request).await
|
||||
self.inner().stat(request).await
|
||||
}
|
||||
|
||||
async fn read(&self, request: ReadRequest) -> Result<ReadResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.read(request).await
|
||||
self.inner().read(request).await
|
||||
}
|
||||
|
||||
async fn read_bytes(&self, request: ReadBytesRequest) -> Result<ReadBytesResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.read_bytes(request).await
|
||||
self.inner().read_bytes(request).await
|
||||
}
|
||||
|
||||
async fn write(&self, request: WriteRequest) -> Result<WriteResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.write(request).await
|
||||
self.inner().write(request).await
|
||||
}
|
||||
|
||||
async fn edit(&self, request: EditRequest) -> Result<EditResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.edit(request).await
|
||||
self.inner().edit(request).await
|
||||
}
|
||||
|
||||
async fn list(&self, request: ListRequest) -> Result<ListResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.list(request).await
|
||||
self.inner().list(request).await
|
||||
}
|
||||
|
||||
async fn glob(&self, request: GlobRequest) -> Result<GlobResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.glob(request).await
|
||||
self.inner().glob(request).await
|
||||
}
|
||||
|
||||
async fn grep(&self, request: GrepRequest) -> Result<GrepResult, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.grep(request).await
|
||||
self.inner().grep(request).await
|
||||
}
|
||||
|
||||
async fn start_command(&self, request: CommandRequest) -> Result<CommandHandle, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
let handle = self.state.session.start_command(request).await?;
|
||||
let handle = self.inner().start_command(request).await?;
|
||||
self.state
|
||||
.active_commands
|
||||
.lock()
|
||||
@@ -329,7 +340,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
|
||||
async fn command_status(&self, handle: CommandHandle) -> Result<CommandStatus, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
let status = self.state.session.command_status(handle.clone()).await?;
|
||||
let status = self.inner().command_status(handle.clone()).await?;
|
||||
self.state.mark_terminal(&handle, status);
|
||||
Ok(status)
|
||||
}
|
||||
@@ -340,7 +351,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
) -> Result<CommandOutput, WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
let handle = request.handle.clone();
|
||||
let output = self.state.session.command_output(request).await?;
|
||||
let output = self.inner().command_output(request).await?;
|
||||
if let Some(terminal) = &output.terminal {
|
||||
self.state.mark_terminal(&handle, terminal.status);
|
||||
}
|
||||
@@ -349,7 +360,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
|
||||
async fn cancel_command(&self, handle: CommandHandle) -> Result<(), WorkdirError> {
|
||||
let _active = self.state.enter()?;
|
||||
self.state.session.cancel_command(handle.clone()).await?;
|
||||
self.inner().cancel_command(handle.clone()).await?;
|
||||
self.state
|
||||
.active_commands
|
||||
.lock()
|
||||
@@ -359,11 +370,11 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
}
|
||||
|
||||
fn subscribe_command_events(&self) -> Option<broadcast::Receiver<CommandEvent>> {
|
||||
self.state.session.subscribe_command_events()
|
||||
self.inner().subscribe_command_events()
|
||||
}
|
||||
|
||||
fn command_snapshot(&self) -> Vec<CommandSnapshot> {
|
||||
let snapshots = self.state.session.command_snapshot();
|
||||
let snapshots = self.inner().command_snapshot();
|
||||
self.state.reconcile_terminal_commands(&snapshots);
|
||||
snapshots
|
||||
}
|
||||
@@ -374,7 +385,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
"Workdir attachment has active operations or commands".to_string(),
|
||||
));
|
||||
}
|
||||
self.state.session.close().await
|
||||
self.inner().close().await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +398,7 @@ impl WorkdirSession for RoutedWorkdirSession {
|
||||
pub struct WorkdirSessionRouter {
|
||||
sessions: RwLock<BTreeMap<WorkdirAttachmentAlias, Arc<RoutedSessionState>>>,
|
||||
next_generation: AtomicU64,
|
||||
coordinate_brokers: RwLock<BTreeMap<WorkdirAttachmentAlias, (u64, crate::WorkdirToolBroker)>>,
|
||||
}
|
||||
|
||||
impl WorkdirSessionRouter {
|
||||
@@ -473,9 +485,9 @@ impl WorkdirSessionRouter {
|
||||
}
|
||||
};
|
||||
Ok(ResolvedWorkdirSession {
|
||||
session: self.routed(&selected.0, selected.1.clone()),
|
||||
alias: selected.0,
|
||||
generation: selected.1.generation,
|
||||
session: Arc::new(RoutedWorkdirSession { state: selected.1 }),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -483,13 +495,22 @@ impl WorkdirSessionRouter {
|
||||
&self,
|
||||
alias: WorkdirAttachmentAlias,
|
||||
session: WorkdirSessionHandle,
|
||||
) -> Result<WorkdirSessionHandle, WorkdirError> {
|
||||
self.attach_with_cwd(alias, session, None)
|
||||
}
|
||||
|
||||
fn attach_with_cwd(
|
||||
&self,
|
||||
alias: WorkdirAttachmentAlias,
|
||||
session: WorkdirSessionHandle,
|
||||
cwd: Option<crate::WorkdirPath>,
|
||||
) -> Result<WorkdirSessionHandle, WorkdirError> {
|
||||
let mut sessions = self.sessions.write().map_err(|_| {
|
||||
WorkdirError::Unavailable("Workdir attachment router is poisoned".to_string())
|
||||
WorkdirError::Unavailable("Workdir attachment router is poisoned".into())
|
||||
})?;
|
||||
if sessions.contains_key(&alias) {
|
||||
return Err(WorkdirError::Conflict(format!(
|
||||
"Workdir attachment alias `{alias}` already exists"
|
||||
"Workdir attachment alias \u{0060}{alias}\u{0060} already exists"
|
||||
)));
|
||||
}
|
||||
let state = Arc::new(RoutedSessionState {
|
||||
@@ -499,8 +520,61 @@ impl WorkdirSessionRouter {
|
||||
active_operations: AtomicUsize::new(0),
|
||||
active_commands: Mutex::new(HashSet::new()),
|
||||
});
|
||||
let view = if let Some(cwd) = cwd {
|
||||
let source = Arc::new(RoutedWorkdirSession {
|
||||
state: state.clone(),
|
||||
view: None,
|
||||
});
|
||||
let broker = crate::WorkdirToolBroker::at_cwd(source, cwd)?;
|
||||
let view = broker.tool_session();
|
||||
self.coordinate_brokers
|
||||
.write()
|
||||
.map_err(|_| {
|
||||
WorkdirError::Unavailable("Workdir coordinate broker map is poisoned".into())
|
||||
})?
|
||||
.insert(alias.clone(), (state.generation, broker));
|
||||
Some(view)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
sessions.insert(alias, state.clone());
|
||||
Ok(Arc::new(RoutedWorkdirSession { state }))
|
||||
Ok(Arc::new(RoutedWorkdirSession { state, view }))
|
||||
}
|
||||
|
||||
fn routed(
|
||||
&self,
|
||||
alias: &WorkdirAttachmentAlias,
|
||||
state: Arc<RoutedSessionState>,
|
||||
) -> Arc<RoutedWorkdirSession> {
|
||||
let view = self
|
||||
.coordinate_broker(alias, state.generation)
|
||||
.map(|broker| broker.tool_session());
|
||||
Arc::new(RoutedWorkdirSession { state, view })
|
||||
}
|
||||
|
||||
pub(crate) fn coordinate_broker(
|
||||
&self,
|
||||
alias: &WorkdirAttachmentAlias,
|
||||
generation: u64,
|
||||
) -> Option<crate::WorkdirToolBroker> {
|
||||
self.coordinate_brokers
|
||||
.read()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.get(alias)
|
||||
.filter(|(current, _)| *current == generation)
|
||||
.map(|(_, broker)| broker.clone())
|
||||
}
|
||||
|
||||
/// Bind tool coordinates once while retaining root-coordinate delegation
|
||||
/// and provider authorization. Both views share the route's detach fence.
|
||||
pub fn attach_at_cwd(
|
||||
&self,
|
||||
alias: WorkdirAttachmentAlias,
|
||||
source: WorkdirSessionHandle,
|
||||
cwd: crate::WorkdirPath,
|
||||
) -> Result<WorkdirSessionHandle, WorkdirError> {
|
||||
let cwd = crate::WorkdirPath::new(cwd.as_str())?;
|
||||
self.attach_with_cwd(alias, source, Some(cwd))
|
||||
}
|
||||
|
||||
pub fn session(&self, alias: &WorkdirAttachmentAlias) -> Option<WorkdirSessionHandle> {
|
||||
@@ -509,7 +583,7 @@ impl WorkdirSessionRouter {
|
||||
.ok()?
|
||||
.get(alias)
|
||||
.cloned()
|
||||
.map(|state| Arc::new(RoutedWorkdirSession { state }) as WorkdirSessionHandle)
|
||||
.map(|state| self.routed(alias, state) as WorkdirSessionHandle)
|
||||
}
|
||||
|
||||
/// Compatibility projection used only while filesystem tool schemas have no
|
||||
@@ -521,10 +595,8 @@ impl WorkdirSessionRouter {
|
||||
return None;
|
||||
}
|
||||
sessions
|
||||
.values()
|
||||
.next()
|
||||
.cloned()
|
||||
.map(|state| Arc::new(RoutedWorkdirSession { state }) as WorkdirSessionHandle)
|
||||
.first_key_value()
|
||||
.map(|(alias, state)| self.routed(alias, state.clone()) as WorkdirSessionHandle)
|
||||
}
|
||||
|
||||
pub fn can_detach(&self, alias: &WorkdirAttachmentAlias) -> Result<(), WorkdirError> {
|
||||
@@ -620,6 +692,10 @@ impl WorkdirSessionRouter {
|
||||
.is_some_and(|current| Arc::ptr_eq(current, &state))
|
||||
{
|
||||
sessions.remove(alias);
|
||||
self.coordinate_brokers
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(alias);
|
||||
}
|
||||
Ok(state.session.clone())
|
||||
}
|
||||
@@ -661,6 +737,10 @@ impl WorkdirSessionRouter {
|
||||
.is_some_and(|current| Arc::ptr_eq(current, &state))
|
||||
{
|
||||
sessions.remove(&alias);
|
||||
self.coordinate_brokers
|
||||
.write()
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner)
|
||||
.remove(&alias);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -684,6 +764,131 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn cwd_views_share_root_delegation_leases_and_attachment_fences() {
|
||||
use crate::{
|
||||
WorkdirPath as P, WorkdirToolBrokerRouter, WorkdirToolScope,
|
||||
WorkdirToolScopePermission as Permission, WorkdirToolScopeRule,
|
||||
};
|
||||
let root = TempDir::new().unwrap();
|
||||
for directory in ["work", "other", "other/inside"] {
|
||||
std::fs::create_dir_all(root.path().join(directory)).unwrap();
|
||||
std::fs::write(root.path().join(directory).join("marker"), directory).unwrap();
|
||||
}
|
||||
let router = Arc::new(WorkdirSessionRouter::new());
|
||||
let alias = WorkdirAttachmentAlias::new("source").unwrap();
|
||||
let parent = router
|
||||
.attach_at_cwd(alias.clone(), session("wd", &root), P::new("work").unwrap())
|
||||
.unwrap();
|
||||
assert_eq!(parent.environment().cwd, P::new("work").unwrap());
|
||||
let request = |cwd: &str| WorkdirToolScope {
|
||||
rules: vec![WorkdirToolScopeRule {
|
||||
target: P::new(cwd).unwrap(),
|
||||
permission: Permission::Write,
|
||||
recursive: true,
|
||||
symlink_policy: Default::default(),
|
||||
}],
|
||||
cwd: P::new(cwd).unwrap(),
|
||||
command: false,
|
||||
};
|
||||
let brokers = WorkdirToolBrokerRouter::new(router.clone());
|
||||
// Declarations stay root-relative, not work/other.
|
||||
let child = brokers
|
||||
.scope(vec![(alias.clone(), request("other"))])
|
||||
.await
|
||||
.unwrap();
|
||||
let grandchild = child
|
||||
.broker_router()
|
||||
.scope(vec![(alias.clone(), request("other/inside"))])
|
||||
.await
|
||||
.unwrap();
|
||||
for (session, cwd) in [
|
||||
(parent.clone(), "work"),
|
||||
(
|
||||
child.session_router().resolve(None).unwrap().session,
|
||||
"other",
|
||||
),
|
||||
(
|
||||
grandchild.session_router().resolve(None).unwrap().session,
|
||||
"other/inside",
|
||||
),
|
||||
] {
|
||||
assert_eq!(session.environment().cwd, P::new(cwd).unwrap());
|
||||
let result = session
|
||||
.read(ReadRequest {
|
||||
path: P::new("marker").unwrap(),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
max_bytes: 100,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(result.bytes, cwd.as_bytes());
|
||||
assert_eq!(result.path, P::new("marker").unwrap());
|
||||
assert_eq!(
|
||||
session
|
||||
.stat(StatRequest {
|
||||
path: P::new("marker").unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap()
|
||||
.path,
|
||||
P::new("marker").unwrap()
|
||||
);
|
||||
}
|
||||
// The model's tool view and delegation view share write-lease exclusion.
|
||||
let absolute = P::new_scoped(root.path().join("other/marker").to_str().unwrap()).unwrap();
|
||||
let denied = parent
|
||||
.write(WriteRequest {
|
||||
path: absolute,
|
||||
content: b"forbidden".to_vec(),
|
||||
expected_hash: None,
|
||||
})
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(
|
||||
denied.denial_reason(),
|
||||
Some(crate::WorkdirDenialReason::ChildWriteLeaseConflict)
|
||||
);
|
||||
let old_child = child.session_router().resolve(None).unwrap().session;
|
||||
router.begin_detach(&alias).await.unwrap();
|
||||
assert!(
|
||||
old_child
|
||||
.read(ReadRequest {
|
||||
path: P::new("marker").unwrap(),
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
max_bytes: 100
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
router.cancel_detach(&alias);
|
||||
grandchild.close().await.unwrap();
|
||||
child.close().await.unwrap();
|
||||
router.detach(&alias).await.unwrap();
|
||||
router
|
||||
.attach_at_cwd(
|
||||
alias,
|
||||
session("replacement", &root),
|
||||
P::new("other").unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
router.resolve(None).unwrap().session.environment().cwd,
|
||||
P::new("other").unwrap()
|
||||
);
|
||||
assert!(
|
||||
old_child
|
||||
.stat(StatRequest {
|
||||
path: P::new("marker").unwrap()
|
||||
})
|
||||
.await
|
||||
.is_err()
|
||||
);
|
||||
router.close_all().await.unwrap();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn checkout_routes_use_active_operation_detach_fence_and_old_handles_stay_closed() {
|
||||
let root = TempDir::new().unwrap();
|
||||
|
||||
@@ -108,17 +108,34 @@ impl WorkdirToolBroker {
|
||||
Self::with_capabilities(source, capabilities)
|
||||
}
|
||||
|
||||
/// Set the tool coordinate base without changing provider authority.
|
||||
/// The input is relative to the source session's coordinates; delegation
|
||||
/// declarations retain their existing coordinate contract.
|
||||
pub fn at_cwd(source: WorkdirSessionHandle, cwd: FsPath) -> Result<Self, WorkdirError> {
|
||||
let cwd = FsPath::new(cwd.as_str())?;
|
||||
let capabilities = source.capabilities();
|
||||
Ok(Self::with_capabilities_and_cwd(source, capabilities, cwd))
|
||||
}
|
||||
|
||||
/// Own a session behind an additional Backend-authored capability ceiling.
|
||||
/// The ceiling can only attenuate provider capabilities; it can never add one.
|
||||
pub fn with_capabilities(
|
||||
source: WorkdirSessionHandle,
|
||||
capabilities: WorkdirSessionCapabilities,
|
||||
) -> Self {
|
||||
Self::with_capabilities_and_cwd(source, capabilities, FsPath::root())
|
||||
}
|
||||
|
||||
fn with_capabilities_and_cwd(
|
||||
source: WorkdirSessionHandle,
|
||||
capabilities: WorkdirSessionCapabilities,
|
||||
cwd: FsPath,
|
||||
) -> Self {
|
||||
let capabilities = source.capabilities().intersection(capabilities);
|
||||
let (command_events, _) = broadcast::channel(64);
|
||||
let authority = Arc::new(ScopedWorkdirSession {
|
||||
source,
|
||||
cwd: FsPath::new("").expect("empty Workdir path is valid"),
|
||||
cwd,
|
||||
scope: None,
|
||||
capabilities,
|
||||
validity: SessionValidity::root(),
|
||||
@@ -416,6 +433,9 @@ impl WorkdirToolBrokerRouter {
|
||||
.sessions
|
||||
.resolve(Some(alias.as_str()))
|
||||
.map_err(|error| WorkdirError::InvalidArgument(error.to_string()))?;
|
||||
if let Some(broker) = self.sessions.coordinate_broker(alias, selected.generation) {
|
||||
return Ok(broker);
|
||||
}
|
||||
let mut brokers = self.brokers.lock().map_err(|_| {
|
||||
WorkdirError::Unavailable("Workdir tool broker router is poisoned".to_string())
|
||||
})?;
|
||||
@@ -684,7 +704,7 @@ impl ScopedWorkdirSession {
|
||||
}
|
||||
|
||||
fn resolve_path(&self, path: &FsPath) -> Result<FsPath, WorkdirError> {
|
||||
if self.cwd.as_str().is_empty() {
|
||||
if self.cwd.as_str().is_empty() || Path::new(path.as_str()).is_absolute() {
|
||||
return Ok(path.clone());
|
||||
}
|
||||
let joined = Path::new(self.cwd.as_str()).join(path.as_str());
|
||||
@@ -1085,6 +1105,43 @@ impl WorkdirSession for ScopedWorkdirSession {
|
||||
self.capabilities
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::WorkdirSessionEnvironment {
|
||||
let mut environment = self.source.environment();
|
||||
let source_cwd = environment.cwd.clone();
|
||||
environment.cwd = FsPath::new(
|
||||
Path::new(source_cwd.as_str())
|
||||
.join(self.cwd.as_str())
|
||||
.to_str()
|
||||
.expect("validated UTF-8 paths"),
|
||||
)
|
||||
.expect("validated session cwd");
|
||||
if let Some(scope) = &self.scope {
|
||||
if let Some(inherited) = environment.scope.take() {
|
||||
environment
|
||||
.restrictions
|
||||
.push(format!("Provider allow ceiling: {inherited:?}"));
|
||||
}
|
||||
environment.scope = Some(
|
||||
scope
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|mut rule| {
|
||||
rule.target = FsPath::new(
|
||||
Path::new(source_cwd.as_str())
|
||||
.join(rule.target.as_str())
|
||||
.to_str()
|
||||
.expect("validated UTF-8 paths"),
|
||||
)
|
||||
.expect("validated scope target");
|
||||
rule
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
environment.capabilities = self.capabilities();
|
||||
environment
|
||||
}
|
||||
|
||||
async fn authorize_scope_path(
|
||||
&self,
|
||||
mut request: WorkdirScopeAuthorizationRequest,
|
||||
@@ -1273,8 +1330,11 @@ impl WorkdirSession for ScopedWorkdirSession {
|
||||
.resolve_operation_path(&request.path, WorkdirToolScopePermission::Read)
|
||||
.await?;
|
||||
self.ensure_read(&path, WorkdirSessionCapability::Read)?;
|
||||
let caller_path = request.path.clone();
|
||||
request.path = path;
|
||||
self.source.stat(request).await
|
||||
let mut result = self.source.stat(request).await?;
|
||||
result.path = caller_path;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn read(&self, mut request: ReadRequest) -> Result<ReadResult, WorkdirError> {
|
||||
@@ -1282,8 +1342,11 @@ impl WorkdirSession for ScopedWorkdirSession {
|
||||
.resolve_operation_path(&request.path, WorkdirToolScopePermission::Read)
|
||||
.await?;
|
||||
self.ensure_read(&path, WorkdirSessionCapability::Read)?;
|
||||
let caller_path = request.path.clone();
|
||||
request.path = path;
|
||||
self.source.read(request).await
|
||||
let mut result = self.source.read(request).await?;
|
||||
result.path = caller_path;
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn read_bytes(
|
||||
@@ -1422,9 +1485,10 @@ impl WorkdirSession for ScopedWorkdirSession {
|
||||
)));
|
||||
}
|
||||
let tool_call_id = request.tool_call_id.clone();
|
||||
if self.scope.is_some() {
|
||||
request.cwd = self.resolve_path(&request.cwd)?;
|
||||
}
|
||||
request.cwd = self
|
||||
.resolve_operation_path(&request.cwd, WorkdirToolScopePermission::Read)
|
||||
.await?;
|
||||
self.ensure_path(&request.cwd, WorkdirToolScopePermission::Read)?;
|
||||
if let Some(tool_call_id) = &tool_call_id {
|
||||
self.starting_tool_calls
|
||||
.lock()
|
||||
@@ -1601,7 +1665,20 @@ impl WorkdirSession for ReadOnlyWorkdirSession {
|
||||
}
|
||||
|
||||
fn capabilities(&self) -> WorkdirSessionCapabilities {
|
||||
WorkdirSessionCapabilities::READ_ONLY
|
||||
self.inner
|
||||
.capabilities()
|
||||
.intersection(WorkdirSessionCapabilities::READ_ONLY)
|
||||
}
|
||||
|
||||
fn environment(&self) -> crate::WorkdirSessionEnvironment {
|
||||
let mut environment = self.inner.environment();
|
||||
environment.capabilities = self.capabilities();
|
||||
if let Some(scope) = &mut environment.scope {
|
||||
for rule in scope {
|
||||
rule.permission = WorkdirToolScopePermission::Read;
|
||||
}
|
||||
}
|
||||
environment
|
||||
}
|
||||
|
||||
async fn authorize_scope_path(
|
||||
|
||||
@@ -14,6 +14,18 @@ use workdir::{
|
||||
},
|
||||
};
|
||||
|
||||
fn opened(workdir_id: &str) -> String {
|
||||
serde_json::to_string(&workdir::http::OpenWorkdirSessionResponse {
|
||||
session_id: workdir::http::WorkdirSessionId::new("s").unwrap(),
|
||||
workdir_id: Workdir::new(workdir_id).id().clone(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::ALL,
|
||||
environment: workdir::WorkdirSessionEnvironment::root(
|
||||
workdir::WorkdirSessionCapabilities::ALL,
|
||||
),
|
||||
})
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn reply(listener: &TcpListener, status: &str, body: &str) {
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
@@ -73,11 +85,7 @@ async fn remote_http_denials_forward_only_typed_reasons_and_old_missing_reason()
|
||||
error["denial_reason"] = serde_json::to_value(reason).unwrap();
|
||||
}
|
||||
let server = thread::spawn(move || {
|
||||
reply(
|
||||
&listener,
|
||||
"200 OK",
|
||||
r#"{"session_id":"s","workdir_id":"w","capabilities":{"bits":63}}"#,
|
||||
);
|
||||
reply(&listener, "200 OK", &opened("w"));
|
||||
reply(&listener, "403 Forbidden", &error.to_string());
|
||||
});
|
||||
let remote = RemoteWorkdirSession::open(
|
||||
@@ -163,11 +171,7 @@ async fn remote_checkout_diagnostics_preserve_mutation_outcome_and_read_failures
|
||||
let address = listener.local_addr().unwrap();
|
||||
let error = serde_json::json!({"code":code, "message":"/secret/host provider-secret", "denial_reason":reason});
|
||||
let server = thread::spawn(move || {
|
||||
reply(
|
||||
&listener,
|
||||
"200 OK",
|
||||
r#"{"session_id":"s","workdir_id":"w","capabilities":{"bits":63}}"#,
|
||||
);
|
||||
reply(&listener, "200 OK", &opened("w"));
|
||||
reply(
|
||||
&listener,
|
||||
&format!("{} Fixture failure", code.http_status()),
|
||||
@@ -230,11 +234,11 @@ async fn mismatched_http_results_keep_cause_and_mutation_uncertainty() {
|
||||
reply(
|
||||
&listener,
|
||||
"200 OK",
|
||||
if case == "identity" {
|
||||
r#"{"session_id":"s","workdir_id":"wrong-secret","capabilities":{"bits":63}}"#
|
||||
&opened(if case == "identity" {
|
||||
"wrong-secret"
|
||||
} else {
|
||||
r#"{"session_id":"s","workdir_id":"w","capabilities":{"bits":63}}"#
|
||||
},
|
||||
"w"
|
||||
}),
|
||||
);
|
||||
if case != "identity" {
|
||||
let result = if case == "bytes" {
|
||||
|
||||
Reference in New Issue
Block a user