feat(workspace-config): project grant-bound logical settings through WIP

This commit is contained in:
2026-10-06 06:56:51 +09:00
parent 7a96c42130
commit 0a471242cb
44 changed files with 8159 additions and 72 deletions
+94
View File
@@ -11,6 +11,7 @@ pub use api_macros::axum as server_support;
pub use api_macros::reqwest as client_support;
pub use api_macros::{ApiContract, BinaryBody, HttpMethod, TransportMetadata, WebSocketOperation};
pub mod repository_openapi_typescript;
mod workspace_config;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
@@ -18,6 +19,7 @@ use webauthn_rs_proto::{
CreationChallengeResponse, PublicKeyCredential, RegisterPublicKeyCredential,
RequestChallengeResponse,
};
pub use workspace_config::*;
#[allow(dead_code)]
#[derive(JsonSchema)]
@@ -200,6 +202,22 @@ impl_openapi_schema!(
CurrentWorkerWorkdirDetachQuery,
CurrentWorkerWorkdirOperationRequest,
CurrentWorkerWorkdirOperationResponse,
WorkspaceConfigAccess,
WorkspaceConfigNodeKind,
WorkspaceConfigFailureClassification,
WorkspaceConfigAttachRequest,
WorkspaceConfigAttachment,
WorkspaceConfigCurrentResponse,
WorkspaceConfigObserveRequest,
WorkspaceConfigNode,
WorkspaceConfigObserveResponse,
WorkspaceConfigReadRequest,
WorkspaceConfigReadResponse,
WorkspaceConfigCommitRequest,
WorkspaceConfigCommitResponse,
WorkspaceConfigApiError,
WorkspaceConfigGrantCreateRequest,
WorkspaceConfigGrantResponse,
ExternalWorkdirGrantCreateRequest,
ExternalWorkdirGrantResponse,
WorkspaceResponse,
@@ -2570,6 +2588,55 @@ pub trait ServerApi {
#[path] working_directory_id: String,
#[body] request: WorkingDirectoryRemovalRequest,
) -> Result<WorkingDirectoryRemovalResponse, RepositoryApiError>;
#[get("/api/w/{workspace_id}/workers/self/workspace-config", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,500,503], openapi = false)]
async fn current_worker_workspace_config_get(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
) -> Result<WorkspaceConfigCurrentResponse, WorkspaceConfigApiError>;
#[post("/api/w/{workspace_id}/workers/self/workspace-config", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,500,503], openapi = false)]
async fn current_worker_workspace_config_attach(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[body] request: WorkspaceConfigAttachRequest,
) -> Result<WorkspaceConfigAttachment, WorkspaceConfigApiError>;
#[post("/api/w/{workspace_id}/workers/self/workspace-config/observe", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,500,503], openapi = false)]
async fn current_worker_workspace_config_observe(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[body] request: WorkspaceConfigObserveRequest,
) -> Result<WorkspaceConfigObserveResponse, WorkspaceConfigApiError>;
#[post("/api/w/{workspace_id}/workers/self/workspace-config/read", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,500,503], openapi = false)]
async fn current_worker_workspace_config_read(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[body] request: WorkspaceConfigReadRequest,
) -> Result<WorkspaceConfigReadResponse, WorkspaceConfigApiError>;
#[post("/api/w/{workspace_id}/workers/self/workspace-config/commit", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,413,500,503], openapi = false)]
async fn current_worker_workspace_config_commit(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[body] request: WorkspaceConfigCommitRequest,
) -> Result<WorkspaceConfigCommitResponse, WorkspaceConfigApiError>;
#[post("/api/w/{workspace_id}/workspace-config-grants", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,500], bearer_auth = true, browser_auth = true)]
async fn workspace_config_grant_create(
&self,
#[extension] actor: RequestActor,
#[path] workspace_id: String,
#[body] request: WorkspaceConfigGrantCreateRequest,
) -> Result<WorkspaceConfigGrantResponse, WorkspaceConfigApiError>;
#[delete("/api/w/{workspace_id}/workspace-config-grants/{grant_id}", status = 200, error_status = 400, additional_error_statuses = [401,403,404,409,500], bearer_auth = true, browser_auth = true)]
async fn workspace_config_grant_revoke(
&self,
#[extension] actor: RequestActor,
#[path] workspace_id: String,
#[path] grant_id: String,
) -> Result<WorkspaceConfigGrantResponse, WorkspaceConfigApiError>;
#[get(
"/api/w/{workspace_id}/workers/self/workdir-catalog",
status = 200,
@@ -6323,6 +6390,7 @@ pub struct Diagnostic {
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum WorkingDirectoryMaterializerKind {
LogicalWorkspaceConfig,
#[default]
RuntimeGitClone,
ClientHostedExternal,
@@ -6462,6 +6530,11 @@ pub struct RuntimeWorkingDirectorySummary {
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
pub enum WorkingDirectorySource {
WorkspaceConfig {
access: WorkspaceConfigAccess,
content_path: String,
purpose: String,
},
Repository {
repository_key: String,
},
@@ -10723,6 +10796,22 @@ pub fn legacy_catalog_typescript() -> String {
ToolchainContract::decl(&config),
WorkspaceConfigTreeResponse::decl(&config),
ConfigCommitRequest::decl(&config),
WorkspaceConfigAccess::decl(&config),
WorkspaceConfigNodeKind::decl(&config),
WorkspaceConfigFailureClassification::decl(&config),
WorkspaceConfigAttachRequest::decl(&config),
WorkspaceConfigAttachment::decl(&config),
WorkspaceConfigCurrentResponse::decl(&config),
WorkspaceConfigObserveRequest::decl(&config),
WorkspaceConfigNode::decl(&config),
WorkspaceConfigObserveResponse::decl(&config),
WorkspaceConfigReadRequest::decl(&config),
WorkspaceConfigReadResponse::decl(&config),
WorkspaceConfigCommitRequest::decl(&config),
WorkspaceConfigCommitResponse::decl(&config),
WorkspaceConfigApiError::decl(&config),
WorkspaceConfigGrantCreateRequest::decl(&config),
WorkspaceConfigGrantResponse::decl(&config),
ProfileSettingsResponse::decl(&config),
WorkspaceProfileSummary::decl(&config),
WorkspaceProfileSourceSummary::decl(&config),
@@ -13214,6 +13303,11 @@ mod openapi_artifact_tests {
// identity, or a one-use Reviewer capability. They remain generated Rust client/Axum
// operations, but must not appear as unauthenticated operations in the public OpenAPI.
const SIGNED_INTERNAL: &[&str] = &[
"current_worker_workspace_config_get",
"current_worker_workspace_config_attach",
"current_worker_workspace_config_observe",
"current_worker_workspace_config_read",
"current_worker_workspace_config_commit",
"current_worker_workdir_catalog",
"current_worker_workdir_attachment_list",
"current_worker_workdir_attach",
+284
View File
@@ -0,0 +1,284 @@
//! Grant-bound logical Workspace configuration transport. No host filesystem contract.
use super::{ConfigCommitRequest, ConfigContentType};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceConfigAccess {
ReadOnly,
ReadWrite,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceConfigNodeKind {
Directory,
File,
Missing,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum WorkspaceConfigFailureClassification {
NotCommitted,
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigAttachRequest {
#[serde(default)]
pub alias: Option<String>,
#[serde(default)]
pub access: Option<WorkspaceConfigAccess>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigAttachment {
pub workspace_id: String,
pub connection_id: String,
pub alias: String,
pub working_directory_id: String,
pub access: WorkspaceConfigAccess,
pub name: String,
pub purpose: String,
pub content_path: String,
pub already_attached: bool,
}
/// Named transport wrapper required by the generated API; JSON remains attachment-or-null.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(transparent)]
pub struct WorkspaceConfigCurrentResponse(pub Option<WorkspaceConfigAttachment>);
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigObserveRequest {
pub connection_id: String,
pub paths: Vec<String>,
pub depth: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigNode {
pub path: String,
pub kind: WorkspaceConfigNodeKind,
pub validator: String,
pub digest: Option<String>,
pub content_type: Option<ConfigContentType>,
pub operations: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigObserveResponse {
pub connection_id: String,
pub validator: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub revision: u64,
pub digest: String,
/// Canonical commit entrypoints from this exact metadata snapshot.
pub entrypoints: Vec<String>,
pub nodes: Vec<WorkspaceConfigNode>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigReadRequest {
pub connection_id: String,
pub path: String,
pub validator: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigReadResponse {
pub path: String,
pub content: String,
pub content_type: ConfigContentType,
pub digest: String,
pub validator: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigCommitRequest {
pub connection_id: String,
pub validator: String,
pub request: ConfigCommitRequest,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigCommitResponse {
pub validator: String,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
#[schemars(range(min = 0, max = 9_007_199_254_740_991_u64))]
pub revision: u64,
pub digest: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigApiError {
pub status: u16,
pub code: String,
pub message: String,
pub classification: WorkspaceConfigFailureClassification,
}
impl api_macros::HttpError for WorkspaceConfigApiError {
fn status_code(&self) -> u16 {
self.status
}
}
impl api_macros::HttpRequestError for WorkspaceConfigApiError {
fn from_request_rejection(status: u16, _message: String) -> Self {
Self {
status,
code: "invalid_request".into(),
message: "Invalid configuration request".into(),
classification: WorkspaceConfigFailureClassification::NotCommitted,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigGrantCreateRequest {
pub runtime_id: String,
pub worker_id: String,
pub access: WorkspaceConfigAccess,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceConfigGrantResponse {
pub grant_id: String,
pub workspace_id: String,
pub runtime_id: String,
pub worker_id: String,
pub working_directory_id: String,
pub access: WorkspaceConfigAccess,
pub revoked: bool,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ApiContract, HttpMethod, ServerApiMetadata};
#[test]
fn workspace_config_observation_is_metadata_only_and_has_same_snapshot_cas_inputs() {
let value = serde_json::json!({"connection_id":"c", "validator":"v", "revision":7, "digest":"d", "entrypoints":["main.dcdl"], "nodes":[{"path":"", "kind":"directory", "validator":"v", "digest":null, "content_type":null, "operations":["apply_changes"]}]});
let response: WorkspaceConfigObserveResponse =
serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(response).unwrap(), value);
for field in ["revision", "digest", "entrypoints"] {
let mut missing = value.clone();
missing.as_object_mut().unwrap().remove(field);
assert!(serde_json::from_value::<WorkspaceConfigObserveResponse>(missing).is_err());
}
let mut with_body = value;
with_body["nodes"][0]["content"] = serde_json::json!("must never appear here");
assert!(serde_json::from_value::<WorkspaceConfigObserveResponse>(with_body).is_err());
assert!(
serde_json::from_value::<WorkspaceConfigGrantCreateRequest>(
serde_json::json!({"runtime_id":"r","worker_id":"w","access":"read_write_command"})
)
.is_err()
);
assert!(
serde_json::from_value::<WorkspaceConfigAttachRequest>(
serde_json::json!({"grant_id":"invented"})
)
.is_err()
);
assert_eq!(
serde_json::to_value(WorkspaceConfigCurrentResponse(None)).unwrap(),
serde_json::Value::Null
);
}
#[test]
fn workspace_config_failure_classification_roundtrips_and_rejections_are_pre_effects() {
for classification in [
WorkspaceConfigFailureClassification::NotCommitted,
WorkspaceConfigFailureClassification::Unknown,
] {
let error = WorkspaceConfigApiError {
status: 500,
code: "unavailable".into(),
message: "safe".into(),
classification,
};
assert_eq!(
serde_json::from_value::<WorkspaceConfigApiError>(
serde_json::to_value(&error).unwrap()
)
.unwrap(),
error
);
assert_eq!(api_macros::HttpError::status_code(&error), 500);
}
let error =
<WorkspaceConfigApiError as api_macros::HttpRequestError>::from_request_rejection(
413,
"/private/secret".into(),
);
assert_eq!(
error.classification,
WorkspaceConfigFailureClassification::NotCommitted
);
assert!(!error.message.contains("private"));
}
#[test]
fn workspace_config_generated_routes_are_exact_and_grants_are_separate() {
for (name, method, suffix) in [
("get", HttpMethod::Get, ""),
("attach", HttpMethod::Post, ""),
("observe", HttpMethod::Post, "/observe"),
("read", HttpMethod::Post, "/read"),
("commit", HttpMethod::Post, "/commit"),
] {
let name = format!("current_worker_workspace_config_{name}");
let operation = ServerApiMetadata::OPERATIONS
.iter()
.find(|operation| operation.operation_id == name)
.unwrap();
assert_eq!(operation.method, method);
assert_eq!(
operation.path,
format!("/api/w/{{workspace_id}}/workers/self/workspace-config{suffix}")
);
}
for (name, method, path) in [
(
"workspace_config_grant_create",
HttpMethod::Post,
"/api/w/{workspace_id}/workspace-config-grants",
),
(
"workspace_config_grant_revoke",
HttpMethod::Delete,
"/api/w/{workspace_id}/workspace-config-grants/{grant_id}",
),
] {
let operation = ServerApiMetadata::OPERATIONS
.iter()
.find(|operation| operation.operation_id == name)
.unwrap();
assert_eq!(operation.method, method);
assert_eq!(operation.path, path);
}
}
}