chore: merge develop into companion integration

# Conflicts:
#	crates/workspace-server/src/server.rs
This commit is contained in:
2026-08-31 01:51:13 +09:00
35 changed files with 2751 additions and 629 deletions
+1
View File
@@ -17,6 +17,7 @@ const WORKER_MUTATION_SOURCE_SIGNING_INPUT_PREFIX: &str = "yoi-worker-source-v1.
pub const WORKER_REMOVE_PERMISSION: &str = "workspace:worker-remove";
pub const RUNTIME_REQUEST_SOURCE_PROOF_HEADER: &str = "x-yoi-runtime-request-proof";
pub const WORKSPACE_REQUEST_PERMISSION: &str = "workspace:request";
pub const WORKSPACE_WORKER_DISCOVERY_PERMISSION: &str = "workspace:worker-discovery";
pub const BACKEND_RESOURCE_FETCH_PERMISSION: &str = "workspace:resource-fetch";
const RUNTIME_REQUEST_SOURCE_PROOF_PREFIX: &str = "yoi-runtime-request-v1";
const RUNTIME_REQUEST_SOURCE_SIGNING_INPUT_PREFIX: &str = "yoi-runtime-request-v1.";
+38 -13
View File
@@ -1721,17 +1721,8 @@ impl RuntimeHttpWorkdirError {
impl From<workdir::WorkdirError> for RuntimeHttpWorkdirError {
fn from(error: workdir::WorkdirError) -> Self {
let payload = WorkdirTransportError::from_workdir_error(&error);
let status = match payload.code {
WorkdirTransportErrorCode::NotFound | WorkdirTransportErrorCode::UnknownCommand => {
StatusCode::NOT_FOUND
}
WorkdirTransportErrorCode::Conflict => StatusCode::CONFLICT,
WorkdirTransportErrorCode::Unsupported | WorkdirTransportErrorCode::InvalidRequest => {
StatusCode::BAD_REQUEST
}
WorkdirTransportErrorCode::Unavailable => StatusCode::SERVICE_UNAVAILABLE,
WorkdirTransportErrorCode::Internal => StatusCode::INTERNAL_SERVER_ERROR,
};
let status = StatusCode::from_u16(payload.code.http_status())
.expect("Workdir transport error status is valid");
Self { status, payload }
}
}
@@ -1886,8 +1877,8 @@ mod tests {
use manifest::{Scope, SharedScope};
use tower::ServiceExt;
use workdir::{
LocalWorkdirSession, ReadRequest, StatRequest, Workdir, WorkdirPath,
WorkdirSessionCapabilities,
GrepOutputMode, GrepRequest, LocalWorkdirSession, ReadRequest, StatRequest, Workdir,
WorkdirPath, WorkdirSessionCapabilities,
};
fn test_bundle(profile: ProfileSelector) -> ConfigBundle {
@@ -2348,6 +2339,40 @@ mod tests {
.expect("owned operation");
assert!(matches!(result, WorkdirSessionOperationResult::Stat(_)));
let grep = WorkdirSessionOperationRequest {
delegations: Vec::new(),
operation: WorkdirSessionOperation::Grep(GrepRequest {
pattern: "hello".into(),
path: WorkdirPath::new("hello.txt").unwrap(),
glob: Some("*.txt".into()),
file_type: Some("txt".into()),
case_insensitive: false,
before_context: 0,
after_context: 0,
multiline: false,
output_mode: GrepOutputMode::Content,
limit: 10,
offset: 0,
}),
};
let Json(result) = run_workdir_session_operation(
State(state.clone()),
Path("session-1".to_string()),
Some(Extension(auth.clone())),
Ok(Json(grep)),
)
.await
.expect("grep direct file through provider operation");
match result {
WorkdirSessionOperationResult::Grep(result) => {
assert_eq!(result.match_count, 1);
assert_eq!(result.matched_files, 1);
assert!(result.output.starts_with("hello.txt\n"));
assert!(result.output.contains("> 1 │ hello"));
}
other => panic!("unexpected workdir grep result: {other:?}"),
}
#[cfg(unix)]
{
let delegated_visible = WorkdirSessionOperationRequest {
+174 -38
View File
@@ -9,8 +9,8 @@ use worker::{
use crate::auth::{
RUNTIME_REQUEST_SOURCE_PROOF_HEADER, RuntimeAuthError, RuntimeIdentityMaterial,
RuntimeRequestSourceSigner, RuntimeWorkerMutationSourceSigner, WORKER_REMOVE_PERMISSION,
WORKSPACE_REQUEST_PERMISSION, WorkerMutationActorKind, WorkerMutationOperation,
WorkerMutationSourceClaims, new_token_id,
WORKSPACE_REQUEST_PERMISSION, WORKSPACE_WORKER_DISCOVERY_PERMISSION, WorkerMutationActorKind,
WorkerMutationOperation, WorkerMutationSourceClaims, new_token_id,
};
use crate::runtime::RuntimeWorkspaceScope;
use crate::worker_backend::WorkspacePromptProjectionCache;
@@ -343,6 +343,51 @@ impl RuntimeOwnedWorkspaceClient {
self.request_timeout = request_timeout;
self
}
fn execute_with_permission(
&self,
request: WorkspaceRequest,
permission: &'static str,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let base_url = self.base_url.clone();
let workspace_id = self.workspace_id.clone();
let runtime_id = self.runtime_id.clone();
let worker_id = self.worker_id.clone();
let request_source_signer = self.request_source_signer.clone();
let request_source_audience = self.request_source_audience.clone();
let request_timeout = self.request_timeout;
if tokio::runtime::Handle::try_current().is_ok() {
std::thread::spawn(move || {
execute_runtime_owned_workspace_http(
&base_url,
&workspace_id,
&runtime_id,
&worker_id,
request_source_signer.as_ref(),
request_source_audience.as_deref(),
request_timeout,
permission,
request,
)
})
.join()
.map_err(|_| {
WorkspaceClientError::Request("workspace request thread panicked".to_string())
})?
} else {
execute_runtime_owned_workspace_http(
&self.base_url,
&self.workspace_id,
&self.runtime_id,
&self.worker_id,
self.request_source_signer.as_ref(),
self.request_source_audience.as_deref(),
self.request_timeout,
permission,
request,
)
}
}
}
impl std::fmt::Debug for RuntimeOwnedWorkspaceClient {
@@ -377,42 +422,40 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
&self,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
let base_url = self.base_url.clone();
let workspace_id = self.workspace_id.clone();
let runtime_id = self.runtime_id.clone();
let worker_id = self.worker_id.clone();
let request_source_signer = self.request_source_signer.clone();
let request_source_audience = self.request_source_audience.clone();
let request_timeout = self.request_timeout;
if tokio::runtime::Handle::try_current().is_ok() {
std::thread::spawn(move || {
execute_runtime_owned_workspace_http(
&base_url,
&workspace_id,
&runtime_id,
&worker_id,
request_source_signer.as_ref(),
request_source_audience.as_deref(),
request_timeout,
request,
)
})
.join()
.map_err(|_| {
WorkspaceClientError::Request("workspace request thread panicked".to_string())
})?
} else {
execute_runtime_owned_workspace_http(
&self.base_url,
&self.workspace_id,
&self.runtime_id,
&self.worker_id,
self.request_source_signer.as_ref(),
self.request_source_audience.as_deref(),
self.request_timeout,
request,
)
self.execute_with_permission(request, WORKSPACE_REQUEST_PERMISSION)
}
fn list_workspace_workers(
&self,
request: worker::WorkspaceWorkerDiscoveryRequest,
) -> Result<workspace_api::WorkspaceWorkerDiscoveryPage, WorkspaceClientError> {
let mut path = format!(
"/api/w/{}/worker-discovery/workers?limit={}",
self.workspace_id, request.limit
);
if let Some(cursor) = request.cursor.as_deref() {
path.push_str("&cursor=");
path.push_str(&percent_encode_query(cursor));
}
if let Some(query) = request.query.as_deref() {
path.push_str("&query=");
path.push_str(&percent_encode_query(query));
}
let response = self.execute_with_permission(
WorkspaceRequest::get(path),
WORKSPACE_WORKER_DISCOVERY_PERMISSION,
)?;
if !(200..300).contains(&response.status) {
return Err(WorkspaceClientError::Request(format!(
"Workspace Worker discovery failed with HTTP {}: {}",
response.status, response.body
)));
}
serde_json::from_str(&response.body).map_err(|error| {
WorkspaceClientError::Request(format!(
"invalid Workspace Worker discovery response: {error}"
))
})
}
fn current_prompt_projection(
@@ -506,6 +549,19 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
}
}
fn percent_encode_query(value: &str) -> String {
let mut encoded = String::with_capacity(value.len());
for byte in value.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
encoded.push(char::from(byte));
} else {
use std::fmt::Write as _;
let _ = write!(encoded, "%{byte:02X}");
}
}
encoded
}
fn execute_runtime_owned_workspace_http(
base_url: &str,
workspace_id: &str,
@@ -514,6 +570,7 @@ fn execute_runtime_owned_workspace_http(
request_source_signer: Option<&RuntimeRequestSourceSigner>,
request_source_audience: Option<&str>,
request_timeout: Option<Duration>,
permission: &'static str,
request: WorkspaceRequest,
) -> Result<WorkspaceResponse, WorkspaceClientError> {
if !request.path.starts_with('/') || request.path.starts_with("//") {
@@ -553,7 +610,7 @@ fn execute_runtime_owned_workspace_http(
audience,
workspace_id,
Some(worker_id),
WORKSPACE_REQUEST_PERMISSION,
permission,
method.as_str(),
&request.path,
body.as_bytes(),
@@ -903,6 +960,85 @@ mod tests {
);
}
#[test]
fn workspace_worker_discovery_signs_dedicated_permission_and_encoded_query() {
use std::io::{Read, Write};
use std::net::TcpListener;
use std::sync::Mutex;
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let address = listener.local_addr().unwrap();
let received = Arc::new(Mutex::new(String::new()));
let received_for_server = received.clone();
let body = serde_json::json!({
"workers": [{
"subject": {
"kind": "runtime_worker",
"runtime_id": "runtime-b",
"worker_id": "worker-b"
},
"resource_key": "W-2",
"display_name": "coder two",
"profile": "builtin:coder",
"status": "idle"
}],
"next_cursor": "v1:1"
})
.to_string();
let server = std::thread::spawn(move || {
let (mut stream, _) = listener.accept().unwrap();
let mut bytes = [0_u8; 4096];
let count = stream.read(&mut bytes).unwrap();
*received_for_server.lock().unwrap() =
String::from_utf8_lossy(&bytes[..count]).into_owned();
write!(
stream,
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\n\r\n{}",
body.len(),
body
)
.unwrap();
});
let identity = RuntimeIdentityMaterial::generate("runtime-a").unwrap();
let client = RuntimeOwnedWorkspaceClient::new(
"workspace-a",
format!("http://{address}"),
"runtime-a",
"worker-a",
)
.with_runtime_request_source(&identity, "server-a");
let page = client
.list_workspace_workers(worker::WorkspaceWorkerDiscoveryRequest {
cursor: Some("v1:0".to_string()),
limit: 1,
query: Some("coder two".to_string()),
})
.unwrap();
assert_eq!(page.workers[0].resource_key, "W-2");
server.join().unwrap();
let request = received.lock().unwrap().clone();
assert!(request.contains(
"GET /api/w/workspace-a/worker-discovery/workers?limit=1&cursor=v1%3A0&query=coder%20two "
));
let token = request
.lines()
.find_map(|line| {
line.split_once(':').and_then(|(name, value)| {
name.eq_ignore_ascii_case(RUNTIME_REQUEST_SOURCE_PROOF_HEADER)
.then(|| value.trim())
})
})
.unwrap();
let claims = decode_runtime_request_source_claims(token).unwrap();
assert_eq!(claims.permission, WORKSPACE_WORKER_DISCOVERY_PERMISSION);
assert_eq!(
claims.path,
"/api/w/workspace-a/worker-discovery/workers?limit=1&cursor=v1%3A0&query=coder%20two"
);
}
#[test]
fn remote_authority_stamps_and_signs_worker_remove_without_caller_claim_choices() {
let identity = RuntimeIdentityMaterial::generate("runtime-a").unwrap();