feat: add workspace runtime trust key management

This commit is contained in:
2026-09-06 05:06:37 +09:00
parent e7803d1aba
commit 78d571ed14
18 changed files with 3258 additions and 459 deletions
+41 -2
View File
@@ -12,8 +12,10 @@ use workspace_api::{
BrowserCreateWorkerResponse, BrowserWorkspaceOrchestratorResponse,
CreateWorkspaceWorkerRequest, ListResponse, MemoryDocumentResponse, MemoryStagingListResponse,
ObjectiveCreateRequest, ObjectiveDetail, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
ObjectiveStateRequest, ObjectiveSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
TICKET_RELATIONS_QUERY_PATH, WorkerLaunchOptionsResponse,
ObjectiveStateRequest, ObjectiveSummary, PutRuntimeTrustKeyRequest,
RevokeRuntimeTrustKeyRequest, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
TICKET_RELATIONS_QUERY_PATH, WorkerLaunchOptionsResponse, WorkspaceRuntimeDetail,
WorkspaceRuntimeResource,
};
use crate::{BackendApiClient, BackendWorkspaceClientError};
@@ -241,6 +243,43 @@ impl BackendWorkspaceProductClient {
)
}
pub fn list_runtimes(
&self,
) -> Result<ListResponse<WorkspaceRuntimeResource>, BackendWorkspaceClientError> {
self.get_json("/runtimes")
}
pub fn runtime_detail(
&self,
runtime_id: &str,
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
self.get_json(&format!("/runtimes/{}", encode_path_segment(runtime_id)))
}
pub fn put_runtime_trust_key(
&self,
runtime_id: &str,
request: &PutRuntimeTrustKeyRequest,
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
self.send_json(
Method::PUT,
&format!("/runtimes/{}/trust-key", encode_path_segment(runtime_id)),
Some(request),
)
}
pub fn revoke_runtime_trust_key(
&self,
runtime_id: &str,
request: &RevokeRuntimeTrustKeyRequest,
) -> Result<WorkspaceRuntimeDetail, BackendWorkspaceClientError> {
self.send_json(
Method::DELETE,
&format!("/runtimes/{}/trust-key", encode_path_segment(runtime_id)),
Some(request),
)
}
pub fn memory_document(&self) -> Result<MemoryDocumentResponse, BackendWorkspaceClientError> {
self.get_json("/memory")
}
+203 -1
View File
@@ -539,6 +539,7 @@ pub enum WorkspaceAuthConfig {
pub struct WorkspacePermissionSummary {
pub manage_repositories: bool,
pub manage_secrets: bool,
pub manage_runtimes: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -1135,6 +1136,7 @@ pub struct ObjectiveLinkTicketRequest {
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeSourceKind {
EmbeddedWorkerRuntime,
@@ -1142,6 +1144,7 @@ pub enum RuntimeSourceKind {
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeSourceStatus {
Active,
@@ -1149,6 +1152,7 @@ pub enum RuntimeSourceStatus {
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeIdentityAuthority {
RuntimeRegistryProjection,
@@ -1156,6 +1160,8 @@ pub enum RuntimeIdentityAuthority {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RuntimeSourceSummary {
pub kind: RuntimeSourceKind,
pub status: RuntimeSourceStatus,
@@ -1164,6 +1170,7 @@ pub struct RuntimeSourceSummary {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct RuntimeSummary {
pub runtime_id: String,
pub label: String,
@@ -1180,6 +1187,8 @@ pub struct RuntimeSummary {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RuntimeManagementSummary {
pub built_in: bool,
pub config_managed: bool,
@@ -1189,12 +1198,119 @@ pub struct RuntimeManagementSummary {
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
pub struct WorkspaceRuntimeResource {
#[serde(flatten)]
pub runtime: RuntimeSummary,
pub management: RuntimeManagementSummary,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeTrustKeyStatus {
Unconfigured,
Active,
Revoked,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RuntimeTrustKeyState {
pub status: RuntimeTrustKeyStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub public_key: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub fingerprint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(type = "number | null"))]
pub revision: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revoked_at: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeTrustAuditAction {
Created,
Replaced,
Reactivated,
Revoked,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RuntimeTrustAuditEntry {
pub action: RuntimeTrustAuditAction,
pub actor_account_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub old_fingerprint: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub new_fingerprint: Option<String>,
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub revision: u64,
pub at: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct WorkspaceRuntimeDetail {
pub workspace_id: String,
pub runtime: WorkspaceRuntimeResource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub endpoint: Option<String>,
pub trust_key: RuntimeTrustKeyState,
#[serde(default)]
pub recent_audit: Vec<RuntimeTrustAuditEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct PutRuntimeTrustKeyRequest {
pub public_key: String,
#[serde(default)]
#[cfg_attr(feature = "typescript", ts(type = "number | null"))]
pub expected_revision: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RevokeRuntimeTrustKeyRequest {
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub expected_revision: u64,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(rename_all = "snake_case")]
pub enum RuntimeTrustConflictKind {
StaleRevision,
FingerprintInUse,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
#[serde(deny_unknown_fields)]
pub struct RuntimeTrustConflictResponse {
pub error: RuntimeTrustConflictKind,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "typescript", ts(type = "number"))]
pub current_revision: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub current_fingerprint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct CreateRemoteRuntimeRequest {
@@ -2394,6 +2510,22 @@ pub fn catalog_typescript() -> String {
RepositoryListResponse::decl(&config),
RepositoryDetailResponse::decl(&config),
RepositoryLogResponse::decl(&config),
RuntimeSourceKind::decl(&config),
RuntimeSourceStatus::decl(&config),
RuntimeIdentityAuthority::decl(&config),
RuntimeSourceSummary::decl(&config),
RuntimeSummary::decl(&config),
RuntimeManagementSummary::decl(&config),
WorkspaceRuntimeResource::decl(&config),
RuntimeTrustKeyStatus::decl(&config),
RuntimeTrustKeyState::decl(&config),
RuntimeTrustAuditAction::decl(&config),
RuntimeTrustAuditEntry::decl(&config),
WorkspaceRuntimeDetail::decl(&config),
PutRuntimeTrustKeyRequest::decl(&config),
RevokeRuntimeTrustKeyRequest::decl(&config),
RuntimeTrustConflictKind::decl(&config),
RuntimeTrustConflictResponse::decl(&config),
RuntimeConnectionTestStatus::decl(&config),
RuntimeConnectionTestFailureKind::decl(&config),
RuntimeConnectionTestResponse::decl(&config),
@@ -3022,7 +3154,8 @@ mod tests {
}},
"permissions": {
"manage_repositories": true,
"manage_secrets": true
"manage_secrets": true,
"manage_runtimes": true
},
"extension_points": {
"store": "sqlite",
@@ -3086,6 +3219,75 @@ mod tests {
assert!(serde_json::from_value::<RepositoryListResponse>(stale).is_err());
}
#[test]
fn runtime_detail_and_trust_mutations_are_closed_and_typed() {
let detail = serde_json::json!({
"workspace_id": "workspace-test",
"runtime": {
"runtime_id": "runtime-test",
"label": "Runtime Test",
"kind": "remote_http",
"status": "active",
"source": {
"kind": "remote_http",
"status": "active",
"identity_authority": "runtime_registry_projection",
"note": "active"
},
"host_ids": [],
"worker_creation_available": true,
"os": "linux",
"arch": "x86_64",
"diagnostics": [],
"management": {
"built_in": false,
"config_managed": true,
"removable": true,
"endpoint_configured": true,
"token_ref_configured": false
}
},
"endpoint": "https://runtime.example",
"trust_key": {
"status": "active",
"public_key": "ssh-ed25519 AAAA runtime-test",
"fingerprint": "SHA256:test",
"revision": 2,
"created_at": "2026-09-01T12:00:00Z",
"updated_at": "2026-09-01T13:00:00Z"
},
"recent_audit": [{
"action": "replaced",
"actor_account_id": "account-owner",
"old_fingerprint": "SHA256:old",
"new_fingerprint": "SHA256:test",
"revision": 2,
"at": "2026-09-01T13:00:00Z"
}]
});
let parsed: WorkspaceRuntimeDetail = serde_json::from_value(detail.clone()).unwrap();
assert_eq!(serde_json::to_value(parsed).unwrap(), detail);
let mut unknown = detail;
unknown["trust_key"]["private_key"] = serde_json::json!("forbidden");
assert!(serde_json::from_value::<WorkspaceRuntimeDetail>(unknown).is_err());
assert!(
serde_json::from_value::<PutRuntimeTrustKeyRequest>(serde_json::json!({
"public_key": "key",
"expected_revision": 1,
"replace": true
}))
.is_err()
);
assert!(
serde_json::from_value::<RevokeRuntimeTrustKeyRequest>(serde_json::json!({
"expected_revision": 1,
"delete_runtime": true
}))
.is_err()
);
}
#[test]
fn runtime_connection_test_response_is_closed_and_typed() {
let compatible = serde_json::json!({
@@ -440,6 +440,7 @@ CREATE TABLE workspace_runtime_bindings (
base_url TEXT NOT NULL,
public_key TEXT NOT NULL,
public_key_fingerprint TEXT NOT NULL,
binding_revision INTEGER NOT NULL CHECK (binding_revision > 0),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
revoked_at TEXT,
@@ -447,6 +448,22 @@ CREATE TABLE workspace_runtime_bindings (
UNIQUE (workspace_id, public_key_fingerprint),
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE RESTRICT
);
CREATE TABLE workspace_runtime_binding_audit (
workspace_id TEXT NOT NULL,
runtime_id TEXT NOT NULL,
actor_account_id TEXT NOT NULL,
action TEXT NOT NULL CHECK (action IN ('created', 'replaced', 'reactivated', 'revoked')),
old_fingerprint TEXT,
new_fingerprint TEXT,
binding_revision INTEGER NOT NULL CHECK (binding_revision > 0),
at TEXT NOT NULL,
PRIMARY KEY (workspace_id, runtime_id, binding_revision),
FOREIGN KEY(workspace_id, runtime_id)
REFERENCES workspace_runtime_bindings(workspace_id, runtime_id) ON DELETE RESTRICT,
FOREIGN KEY(actor_account_id) REFERENCES accounts(account_id) ON DELETE RESTRICT
);
CREATE INDEX idx_workspace_runtime_binding_audit_recent
ON workspace_runtime_binding_audit(workspace_id, runtime_id, binding_revision DESC);
CREATE TABLE typed_ticket_artifacts (
workspace_id TEXT NOT NULL, ticket_id TEXT NOT NULL, relative_path TEXT NOT NULL, content BLOB NOT NULL,
PRIMARY KEY (workspace_id, ticket_id, relative_path),
+9
View File
@@ -120,6 +120,15 @@ pub enum Error {
WorkspaceConfigConflict(String),
#[error("Runtime binding conflict: {0}")]
RuntimeBindingConflict(String),
#[error("Runtime binding revision conflict: expected {expected:?}, current {actual:?}")]
RuntimeBindingRevisionConflict {
expected: Option<u64>,
actual: Option<u64>,
},
#[error("Runtime public key fingerprint is already bound in this Workspace: {fingerprint}")]
RuntimeBindingFingerprintConflict { fingerprint: String },
#[error("Runtime binding was not found for {runtime_id}")]
RuntimeBindingNotFound { runtime_id: String },
#[error("Repository conflict: {0}")]
RepositoryConflict(String),
#[error("Registry inconsistency: {0}")]
+2
View File
@@ -324,6 +324,7 @@ fn run_trust_runtime_command(args: Vec<String>) -> Result<(), Box<dyn std::error
base_url,
public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: now.clone(),
updated_at: now,
revoked_at: None,
@@ -859,6 +860,7 @@ mod tests {
base_url: "http://127.0.0.1:18080".to_string(),
public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: "2026-07-26T00:00:00Z".to_string(),
updated_at: "2026-07-26T00:00:00Z".to_string(),
revoked_at: None,
+598 -29
View File
@@ -1,7 +1,7 @@
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::{Component, Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex, Weak};
use std::sync::{Arc, Mutex, RwLock, Weak};
use axum::body::Bytes;
use axum::extract::ws::{Message as WsMessage, WebSocket, WebSocketUpgrade};
@@ -74,11 +74,13 @@ use workspace_api::{
PasskeyLoginCompleteRequest, PasskeyLoginOptionsRequest, PasskeyLoginOptionsResponse,
PasskeyRegistrationCompleteRequest, PasskeyRegistrationOptionsRequest,
PasskeyRegistrationOptionsResponse, ProfileSettingsResponse, PutRepositorySshHostTrustRequest,
RepositoryAccessProjection, RepositoryDetailResponse, RepositoryListResponse,
RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, RequestActor,
RotateRepositorySshCredentialRequest, RuntimeConnectionTestFailureKind,
RuntimeConnectionTestResponse, RuntimeConnectionTestStatus, RuntimeManagementSummary,
TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
PutRuntimeTrustKeyRequest, RepositoryAccessProjection, RepositoryDetailResponse,
RepositoryListResponse, RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust,
RequestActor, RevokeRuntimeTrustKeyRequest, RotateRepositorySshCredentialRequest,
RuntimeConnectionTestFailureKind, RuntimeConnectionTestResponse, RuntimeConnectionTestStatus,
RuntimeManagementSummary, RuntimeTrustAuditAction, RuntimeTrustAuditEntry,
RuntimeTrustConflictKind, RuntimeTrustConflictResponse, RuntimeTrustKeyState,
RuntimeTrustKeyStatus, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, TICKET_RELATIONS_QUERY_PATH,
UpdateWorkspaceMetadataRequest, WhoamiResponse, WorkerLaunchOptionsResponse,
WorkerLaunchProfileCandidate, WorkerLaunchRuntimeOption, WorkerLaunchWorkerSummary,
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
@@ -90,8 +92,8 @@ use workspace_api::{
WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceExtensionPointState,
WorkspaceExtensionPoints, WorkspaceMetadataMutationResponse, WorkspaceMetadataSettingsResponse,
WorkspacePermissionSummary, WorkspaceRepositoryRecord, WorkspaceResponse,
WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem,
WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
WorkspaceRuntimeDetail, WorkspaceRuntimeResource, WorkspaceSummary,
WorkspaceWorkerDiscoveryItem, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
};
use crate::auth::{
@@ -153,7 +155,7 @@ use crate::store::{
RepositoryRecord, TicketAssignmentPrincipal, TicketAssignmentRole, TicketCoderAssignmentRecord,
TicketRoleAssignmentRecord, UserRecord, WorkdirCreateOperationRecord, WorkdirRegistryRecord,
WorkerControlGrantRecord, WorkerRegistryRecord, WorkerWorkdirLinkRecord, WorkspaceRecord,
WorkspaceResourceKind,
WorkspaceResourceKind, WorkspaceRuntimeBinding, WorkspaceRuntimeBindingAuditRecord,
};
use crate::workdir_removal::{
WorkdirRemovalAttemptOwner, WorkdirRemovalDisposition, WorkdirRemovalOperation,
@@ -585,6 +587,7 @@ pub struct WorkspaceApi {
prompt_projection_cache: crate::prompt_settings::WorkspacePromptProjectionCache,
authority: SqliteWorkspaceAuthority,
runtime: Arc<RuntimeRegistry>,
runtime_binding_expectations: Arc<RwLock<HashMap<(String, String), WorkspaceRuntimeBinding>>>,
companion: Arc<CompanionConsole>,
orchestrator_spawn_lock: Arc<std::sync::Mutex<()>>,
orchestrator_attention_fingerprint: Arc<Mutex<Option<String>>>,
@@ -1575,6 +1578,7 @@ impl WorkspaceApi {
base_url: "in-process://embedded".to_owned(),
public_key: embedded_identity.public_key.clone(),
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: config.workspace_created_at.clone(),
updated_at: config.workspace_created_at.clone(),
revoked_at: None,
@@ -1614,8 +1618,7 @@ impl WorkspaceApi {
.then(|| (source.runtime_id.clone(), source.base_url.clone()))
})
.collect::<HashMap<_, _>>();
let expected_runtime_bindings = Arc::new(
store
let expected_runtime_bindings = store
.list_workspace_runtime_bindings(&config.workspace_id, false)
.await?
.into_iter()
@@ -1623,9 +1626,14 @@ impl WorkspaceApi {
.filter(|binding| {
configured_runtime_endpoints.get(&binding.runtime_id) == Some(&binding.base_url)
})
.map(|binding| (binding.runtime_id.clone(), binding))
.collect::<HashMap<_, _>>(),
);
.map(|binding| {
(
(binding.workspace_id.clone(), binding.runtime_id.clone()),
binding,
)
})
.collect::<HashMap<_, _>>();
let workspace_id = config.workspace_id.clone();
let api = Self::new_with_execution_backend_and_broker(
config,
store,
@@ -1634,15 +1642,34 @@ impl WorkspaceApi {
Some(worker_remove_dispatcher),
)
.await?;
*api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner) = expected_runtime_bindings;
let runtime_binding_expectations = Arc::clone(&api.runtime_binding_expectations);
api.runtime.set_runtime_binding_gate(move |runtime_id| {
expected_runtime_bindings
.get(runtime_id)
runtime_binding_expectations
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.get(&(workspace_id.clone(), runtime_id.to_string()))
.is_some_and(|expected| {
runtime_binding_store
.workspace_runtime_binding_matches(expected)
.unwrap_or(false)
})
});
let active_expectations = api
.runtime_binding_expectations
.read()
.unwrap_or_else(std::sync::PoisonError::into_inner);
for source in &api.config.remote_runtime_sources {
if !active_expectations
.contains_key(&(api.config.workspace_id.clone(), source.runtime_id.clone()))
{
api.runtime_subscription_broker
.unregister_runtime(&source.runtime_id);
}
}
drop(active_expectations);
Ok(api)
}
@@ -1748,6 +1775,7 @@ impl WorkspaceApi {
config,
store,
runtime,
runtime_binding_expectations: Arc::new(RwLock::new(HashMap::new())),
companion,
orchestrator_spawn_lock: Arc::new(std::sync::Mutex::new(())),
orchestrator_attention_fingerprint: Arc::new(Mutex::new(None)),
@@ -2648,7 +2676,11 @@ fn build_inner_router(api: WorkspaceApi) -> Router {
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}",
delete(scoped_delete_remote_runtime),
get(scoped_get_runtime_detail).delete(scoped_delete_remote_runtime),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/trust-key",
put(scoped_put_runtime_trust_key).delete(scoped_revoke_runtime_trust_key),
)
.route(
"/api/w/{workspace_id}/runtimes/{runtime_id}/connection-tests",
@@ -10956,11 +10988,225 @@ async fn scoped_create_remote_runtime(
create_remote_runtime(State(api), Json(request)).await
}
async fn scoped_get_runtime_detail(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Extension(actor): Extension<RequestActor>,
) -> ApiResult<Json<WorkspaceRuntimeDetail>> {
validate_workspace_scope(&api, &path.workspace_id)?;
let workspace = api
.store
.get_workspace(&path.workspace_id)
.await?
.ok_or(Error::WorkspaceIdMismatch)?;
let is_owner = workspace.owner_account_id == actor.account_id;
Ok(Json(
workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id, is_owner).await?,
))
}
async fn scoped_put_runtime_trust_key(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<PutRuntimeTrustKeyRequest>,
) -> std::result::Result<Response, ApiError> {
validate_workspace_scope(&api, &path.workspace_id)?;
require_workspace_owner(&api, &path.workspace_id, &actor, "Runtime trust changes").await?;
let actor_account_id = actor.account_id.clone();
if path.runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
return Err(settings_bad_request(
"embedded_runtime_trust_managed_internally",
"the embedded Runtime trust key is managed by Server identity authority",
));
}
if request.expected_revision == Some(0) {
return Err(settings_bad_request(
"invalid_runtime_binding_revision",
"expected_revision must be greater than zero when provided",
));
}
if request.public_key.len() > 16 * 1024 {
return Err(settings_bad_request(
"runtime_public_key_too_large",
"public_key must be at most 16384 bytes",
));
}
let existing = api
.store
.get_workspace_runtime_binding(&path.workspace_id, &path.runtime_id)
.await?;
let source = api
.config
.remote_runtime_sources
.iter()
.find(|source| {
source.runtime_id == path.runtime_id
&& source.workspace_id.as_deref() == Some(path.workspace_id.as_str())
})
.cloned();
if let (Some(binding), Some(source)) = (&existing, &source)
&& binding.base_url != source.base_url
{
return Err(settings_bad_request(
"runtime_endpoint_mismatch",
"the persisted Runtime endpoint no longer matches Server Runtime configuration; reconcile the endpoint before changing trust",
));
}
let (display_name, base_url) = if let Some(binding) = &existing {
(binding.display_name.clone(), binding.base_url.clone())
} else if let Some(source) = &source {
(source.display_name.clone(), source.base_url.clone())
} else {
return Err(Error::UnknownRuntime(path.runtime_id.clone()).into());
};
let now = Utc::now().to_rfc3339();
let record = WorkspaceRuntimeBinding {
workspace_id: path.workspace_id.clone(),
runtime_id: path.runtime_id.clone(),
display_name,
base_url,
public_key: request.public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: existing
.as_ref()
.map_or_else(|| now.clone(), |binding| binding.created_at.clone()),
updated_at: now,
revoked_at: None,
};
let mutation = api
.store
.put_workspace_runtime_binding_key(record, request.expected_revision, &actor_account_id)
.await;
let (_, binding) = match mutation {
Ok(result) => result,
Err(error) => {
if let Some(response) = runtime_trust_conflict_response(&api, &path, &error).await {
return Ok(response);
}
return Err(error.into());
}
};
api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(
(path.workspace_id.clone(), path.runtime_id.clone()),
binding,
);
if let Some(source) = source {
api.runtime_subscription_broker
.register_remote_runtime(source);
}
Ok(
Json(workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id, true).await?)
.into_response(),
)
}
async fn scoped_revoke_runtime_trust_key(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Extension(actor): Extension<RequestActor>,
Json(request): Json<RevokeRuntimeTrustKeyRequest>,
) -> std::result::Result<Response, ApiError> {
validate_workspace_scope(&api, &path.workspace_id)?;
require_workspace_owner(&api, &path.workspace_id, &actor, "Runtime trust changes").await?;
let actor_account_id = actor.account_id.clone();
if path.runtime_id == EMBEDDED_WORKER_RUNTIME_ID {
return Err(settings_bad_request(
"embedded_runtime_trust_managed_internally",
"the embedded Runtime trust key is managed by Server identity authority",
));
}
if request.expected_revision == 0 {
return Err(settings_bad_request(
"invalid_runtime_binding_revision",
"expected_revision must be greater than zero",
));
}
let now = Utc::now().to_rfc3339();
let mutation = api
.store
.revoke_workspace_runtime_binding_key(
&path.workspace_id,
&path.runtime_id,
request.expected_revision,
&actor_account_id,
&now,
)
.await;
let _ = match mutation {
Ok(result) => result,
Err(error) => {
if let Some(response) = runtime_trust_conflict_response(&api, &path, &error).await {
return Ok(response);
}
return Err(error.into());
}
};
api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&(path.workspace_id.clone(), path.runtime_id.clone()));
api.runtime_subscription_broker
.unregister_runtime(&path.runtime_id);
Ok(
Json(workspace_runtime_detail(&api, &path.workspace_id, &path.runtime_id, true).await?)
.into_response(),
)
}
async fn runtime_trust_conflict_response(
api: &WorkspaceApi,
path: &ScopedRuntimePath,
error: &Error,
) -> Option<Response> {
let kind = match error {
Error::RuntimeBindingRevisionConflict { .. } => RuntimeTrustConflictKind::StaleRevision,
Error::RuntimeBindingFingerprintConflict { .. } => {
RuntimeTrustConflictKind::FingerprintInUse
}
_ => return None,
};
let current = api
.store
.get_workspace_runtime_binding(&path.workspace_id, &path.runtime_id)
.await
.ok()
.flatten();
Some(
(
StatusCode::CONFLICT,
Json(RuntimeTrustConflictResponse {
error: kind,
message: match kind {
RuntimeTrustConflictKind::StaleRevision => {
"the Runtime trust binding changed; reload before retrying".to_string()
}
RuntimeTrustConflictKind::FingerprintInUse => {
"the public key is already bound to another Runtime in this Workspace"
.to_string()
}
},
current_revision: current.as_ref().map(|binding| binding.binding_revision),
current_fingerprint: current
.as_ref()
.map(|binding| binding.public_key_fingerprint.clone()),
}),
)
.into_response(),
)
}
async fn scoped_delete_remote_runtime(
State(api): State<WorkspaceApi>,
AxumPath(path): AxumPath<ScopedRuntimePath>,
Extension(actor): Extension<RequestActor>,
) -> ApiResult<StatusCode> {
validate_workspace_scope(&api, &path.workspace_id)?;
require_workspace_owner(&api, &path.workspace_id, &actor, "Runtime removal").await?;
delete_remote_runtime(State(api), AxumPath(path.runtime_id)).await
}
@@ -12211,6 +12457,7 @@ async fn get_workspace(
permissions: WorkspacePermissionSummary {
manage_repositories: is_owner,
manage_secrets: is_owner,
manage_runtimes: is_owner,
},
extension_points: WorkspaceExtensionPoints {
store: "sqlite".to_string(),
@@ -12451,7 +12698,7 @@ async fn create_remote_runtime(
}
Err(settings_bad_request(
"runtime_public_key_required",
"remote Runtime registration requires an authenticated public key; use `yoi-server trust-runtime add` until the Workspace Runtime key API is available",
"remote Runtime registration requires an authenticated public key; configure it from the Runtime detail page after the Runtime endpoint is registered",
))
}
@@ -12469,8 +12716,13 @@ async fn delete_remote_runtime(
.store
.get_workspace_runtime_binding(&api.config.workspace_id, &runtime_id)
.await?
.filter(|binding| binding.revoked_at.is_none())
.ok_or_else(|| Error::UnknownRuntime(runtime_id.clone()))?;
if binding.revoked_at.is_none() {
return Err(Error::RuntimeBindingConflict(
"runtime trust is still active; revoke this Workspace's trust key with an expected revision before removing the inactive registration".to_string(),
)
.into());
}
match api
.runtime
.unregister_if_idle(&runtime_id, api.config.max_records.min(200))
@@ -12499,14 +12751,6 @@ async fn delete_remote_runtime(
));
}
}
let now = Utc::now().to_rfc3339();
if !api
.store
.revoke_workspace_runtime_binding_record(&binding.workspace_id, &binding.runtime_id, &now)
.await?
{
return Err(Error::UnknownRuntime(runtime_id).into());
}
Ok(StatusCode::NO_CONTENT)
}
@@ -14488,6 +14732,133 @@ async fn workspace_runtime_resources_response(
})
}
async fn workspace_runtime_detail(
api: &WorkspaceApi,
workspace_id: &str,
runtime_id: &str,
include_public_key: bool,
) -> ApiResult<WorkspaceRuntimeDetail> {
let binding = api
.store
.get_workspace_runtime_binding(workspace_id, runtime_id)
.await?;
let mut resource = workspace_runtime_resources_response(api, workspace_id)
.await?
.items
.into_iter()
.find(|resource| resource.runtime.runtime_id == runtime_id);
if resource.is_none() {
resource = binding.as_ref().map(|binding| WorkspaceRuntimeResource {
runtime: workspace_api::RuntimeSummary {
runtime_id: binding.runtime_id.clone(),
label: binding.display_name.clone(),
kind: "remote_http".to_string(),
status: "unavailable".to_string(),
source: workspace_api::RuntimeSourceSummary {
kind: workspace_api::RuntimeSourceKind::RemoteHttp,
status: workspace_api::RuntimeSourceStatus::Reserved,
identity_authority:
workspace_api::RuntimeIdentityAuthority::ServerRuntimeConfiguration,
note: "The Runtime trust binding is not active in the Runtime registry."
.to_string(),
},
host_ids: Vec::new(),
worker_creation_available: false,
os: String::new(),
arch: String::new(),
diagnostics: Vec::new(),
},
management: RuntimeManagementSummary {
built_in: false,
config_managed: true,
removable: false,
endpoint_configured: !binding.base_url.trim().is_empty(),
token_ref_configured: false,
},
});
}
let mut resource = resource.ok_or_else(|| Error::UnknownRuntime(runtime_id.to_string()))?;
if let Some(binding) = &binding {
resource.management.config_managed = true;
resource.management.endpoint_configured = !binding.base_url.trim().is_empty();
}
let endpoint = binding
.as_ref()
.map(|binding| binding.base_url.clone())
.or_else(|| {
api.config
.remote_runtime_sources
.iter()
.find(|source| {
source.runtime_id == runtime_id
&& source.workspace_id.as_deref() == Some(workspace_id)
})
.map(|source| source.base_url.clone())
});
let trust_key = binding.as_ref().map_or(
RuntimeTrustKeyState {
status: RuntimeTrustKeyStatus::Unconfigured,
public_key: None,
fingerprint: None,
revision: None,
created_at: None,
updated_at: None,
revoked_at: None,
},
|binding| RuntimeTrustKeyState {
status: if binding.revoked_at.is_some() {
RuntimeTrustKeyStatus::Revoked
} else {
RuntimeTrustKeyStatus::Active
},
public_key: include_public_key.then(|| binding.public_key.clone()),
fingerprint: Some(binding.public_key_fingerprint.clone()),
revision: Some(binding.binding_revision),
created_at: Some(binding.created_at.clone()),
updated_at: Some(binding.updated_at.clone()),
revoked_at: binding.revoked_at.clone(),
},
);
let recent_audit = api
.store
.list_workspace_runtime_binding_audit(workspace_id, runtime_id, 20)
.await?
.into_iter()
.map(project_runtime_trust_audit)
.collect::<Result<Vec<_>>>()?;
Ok(WorkspaceRuntimeDetail {
workspace_id: workspace_id.to_string(),
runtime: resource,
endpoint,
trust_key,
recent_audit,
})
}
fn project_runtime_trust_audit(
record: WorkspaceRuntimeBindingAuditRecord,
) -> Result<RuntimeTrustAuditEntry> {
let action = match record.action.as_str() {
"created" => RuntimeTrustAuditAction::Created,
"replaced" => RuntimeTrustAuditAction::Replaced,
"reactivated" => RuntimeTrustAuditAction::Reactivated,
"revoked" => RuntimeTrustAuditAction::Revoked,
other => {
return Err(Error::Store(format!(
"unsupported Runtime trust audit action {other}"
)));
}
};
Ok(RuntimeTrustAuditEntry {
action,
actor_account_id: record.actor_account_id,
old_fingerprint: record.old_fingerprint,
new_fingerprint: record.new_fingerprint,
revision: record.binding_revision,
at: record.at,
})
}
fn validate_runtime_connection_request(request: &CreateRemoteRuntimeRequest) -> ApiResult<()> {
validate_public_runtime_id(request.runtime_id.trim())?;
let endpoint = request.endpoint.trim();
@@ -16040,6 +16411,8 @@ impl IntoResponse for ApiError {
| Error::WorkdirAttachmentConflict(_)
| Error::WorkspaceConfigConflict(_)
| Error::RuntimeBindingConflict(_)
| Error::RuntimeBindingRevisionConflict { .. }
| Error::RuntimeBindingFingerprintConflict { .. }
| Error::RepositoryConflict(_) => StatusCode::CONFLICT,
Error::WorkerSourceIdentity(_) | Error::InvalidInput(_) => StatusCode::BAD_REQUEST,
Error::InvalidRuntimeIdentifier { .. } | Error::ReservedWorkerName(_) => {
@@ -16068,6 +16441,7 @@ impl IntoResponse for ApiError {
| Error::UnknownRuntime(_)
| Error::UnknownWorker { .. }
| Error::UnknownRepository(_)
| Error::RuntimeBindingNotFound { .. }
| Error::WorkspaceIdMismatch => StatusCode::NOT_FOUND,
Error::RuntimeOperationFailed { code, .. } if code == "skill_not_found" => {
StatusCode::NOT_FOUND
@@ -16573,6 +16947,7 @@ mod tests {
base_url: "https://runtime.test".to_owned(),
public_key: identity.public_key.clone(),
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: "2026-01-01T00:00:00Z".to_owned(),
updated_at: "2026-01-01T00:00:00Z".to_owned(),
revoked_at: None,
@@ -22068,6 +22443,166 @@ mod tests {
assert_eq!(detail.provenance.id, "workspace:triage-errors");
}
#[tokio::test]
async fn runtime_trust_management_is_owner_only_revisioned_and_redacted() {
let temp = tempfile::tempdir().unwrap();
let api = test_api(temp.path()).await;
let owner_account_id = format!("account-{TEST_WORKSPACE_ID}");
let owner = RequestActor {
user_id: "owner-user".to_string(),
account_id: owner_account_id.clone(),
handle: "owner".to_string(),
display_name: "Owner".to_string(),
auth_method: ActorAuthMethod::BrowserSession,
};
let non_owner = RequestActor {
user_id: "other-user".to_string(),
account_id: "other-account".to_string(),
handle: "other".to_string(),
display_name: "Other".to_string(),
auth_method: ActorAuthMethod::ApiToken,
};
let first = worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-a").unwrap();
let second = worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-a").unwrap();
let third = worker_runtime::auth::RuntimeIdentityMaterial::generate("runtime-a").unwrap();
let now = Utc::now().to_rfc3339();
api.store
.put_workspace_runtime_binding_key(
WorkspaceRuntimeBinding {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
display_name: "Runtime A".to_string(),
base_url: "https://runtime.example".to_string(),
public_key: first.public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: now.clone(),
updated_at: now,
revoked_at: None,
},
None,
&owner_account_id,
)
.await
.unwrap();
let Json(owner_detail) = scoped_get_runtime_detail(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
}),
Extension(owner.clone()),
)
.await
.unwrap();
assert!(owner_detail.trust_key.public_key.is_some());
assert_eq!(owner_detail.trust_key.revision, Some(1));
let Json(reader_detail) = scoped_get_runtime_detail(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
}),
Extension(non_owner.clone()),
)
.await
.unwrap();
assert!(reader_detail.trust_key.public_key.is_none());
assert!(reader_detail.trust_key.fingerprint.is_some());
let response = scoped_put_runtime_trust_key(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
}),
Extension(owner.clone()),
Json(PutRuntimeTrustKeyRequest {
public_key: second.public_key,
expected_revision: Some(1),
}),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let detail: WorkspaceRuntimeDetail = serde_json::from_slice(&body).unwrap();
assert_eq!(detail.trust_key.revision, Some(2));
assert_eq!(
detail.recent_audit[0].action,
RuntimeTrustAuditAction::Replaced
);
let stale = scoped_put_runtime_trust_key(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
}),
Extension(owner.clone()),
Json(PutRuntimeTrustKeyRequest {
public_key: third.public_key,
expected_revision: Some(1),
}),
)
.await
.unwrap();
assert_eq!(stale.status(), StatusCode::CONFLICT);
let body = axum::body::to_bytes(stale.into_body(), usize::MAX)
.await
.unwrap();
let conflict: RuntimeTrustConflictResponse = serde_json::from_slice(&body).unwrap();
assert_eq!(conflict.error, RuntimeTrustConflictKind::StaleRevision);
assert_eq!(conflict.current_revision, Some(2));
let denied = scoped_revoke_runtime_trust_key(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
}),
Extension(non_owner),
Json(RevokeRuntimeTrustKeyRequest {
expected_revision: 2,
}),
)
.await
.unwrap_err();
assert_eq!(denied.into_response().status(), StatusCode::FORBIDDEN);
let revoked = scoped_revoke_runtime_trust_key(
State(api.clone()),
AxumPath(ScopedRuntimePath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
runtime_id: "runtime-a".to_string(),
}),
Extension(owner),
Json(RevokeRuntimeTrustKeyRequest {
expected_revision: 2,
}),
)
.await
.unwrap();
assert_eq!(revoked.status(), StatusCode::OK);
let binding = api
.store
.get_workspace_runtime_binding(TEST_WORKSPACE_ID, "runtime-a")
.await
.unwrap()
.unwrap();
assert_eq!(binding.binding_revision, 3);
assert!(binding.revoked_at.is_some());
assert!(
!api.runtime_binding_expectations
.read()
.unwrap()
.contains_key(&(TEST_WORKSPACE_ID.to_string(), "runtime-a".to_string()))
);
}
#[tokio::test]
async fn repository_secret_management_is_owner_only() {
let temp = tempfile::tempdir().unwrap();
@@ -22115,6 +22650,16 @@ mod tests {
test_api_with_recording_backend(workspace_root).await.0
}
fn test_owner_actor() -> RequestActor {
RequestActor {
user_id: "owner-user".to_string(),
account_id: format!("account-{TEST_WORKSPACE_ID}"),
handle: "owner".to_string(),
display_name: "Owner".to_string(),
auth_method: ActorAuthMethod::BrowserSession,
}
}
fn test_repository_id(api: &WorkspaceApi) -> String {
api.store
.get_repository_by_key(TEST_WORKSPACE_ID, "test-repository")
@@ -22602,6 +23147,7 @@ mod tests {
base_url: "https://runtime.invalid".to_string(),
public_key: identity.public_key.clone(),
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: "2026-08-11T00:00:00Z".to_string(),
updated_at: "2026-08-11T00:00:00Z".to_string(),
revoked_at: None,
@@ -24088,6 +24634,7 @@ mod tests {
.unwrap()
.public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: "1".to_string(),
updated_at: "1".to_string(),
revoked_at: None,
@@ -25205,6 +25752,7 @@ mod tests {
base_url: "https://runtime.example.invalid".to_string(),
public_key: identity.public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: "1".to_string(),
updated_at: "1".to_string(),
revoked_at: None,
@@ -25222,7 +25770,7 @@ mod tests {
)
.unwrap(),
);
let app = build_inner_router(api);
let app = build_inner_router(api.clone()).layer(Extension(test_owner_actor()));
let runtimes_uri = format!("/api/w/{TEST_WORKSPACE_ID}/runtimes");
let initial = get_json(app.clone(), &runtimes_uri).await;
@@ -25303,6 +25851,16 @@ mod tests {
.expect("team runtime launch option");
assert_eq!(team_runtime["working_directory_required"], true);
api.store
.revoke_workspace_runtime_binding_key(
TEST_WORKSPACE_ID,
"team-runtime",
1,
&format!("account-{TEST_WORKSPACE_ID}"),
&Utc::now().to_rfc3339(),
)
.await
.unwrap();
let deleted = request_json(
app.clone(),
"DELETE",
@@ -25354,6 +25912,7 @@ mod tests {
.unwrap()
.public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
created_at: "1".to_string(),
updated_at: "1".to_string(),
revoked_at: None,
@@ -25370,7 +25929,17 @@ mod tests {
)
.unwrap(),
);
let app = build_inner_router(api);
api.store
.revoke_workspace_runtime_binding_key(
TEST_WORKSPACE_ID,
"busy-runtime",
1,
&format!("account-{TEST_WORKSPACE_ID}"),
&Utc::now().to_rfc3339(),
)
.await
.unwrap();
let app = build_inner_router(api).layer(Extension(test_owner_actor()));
let workers = get_json(app.clone(), "/api/workers").await;
assert!(
workers["items"]
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -6,7 +6,7 @@
"dev": "deno run -A npm:vite@7.2.7 dev",
"dev:backend": "cd ../.. && cargo run -p yoi-workspace-server --bin yoi-server -- serve --listen 127.0.0.1:8787",
"check": "deno run -A npm:@sveltejs/kit@2.49.4 sync && deno run -A npm:svelte-check@4.3.4 --tsconfig ./tsconfig.json",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts",
"test": "deno test --allow-read=src,test,tests --allow-env=LOG,VSCODE_TEXTMATE_DEBUG,NODE_ENV tests/workspace-model.test.ts tests/workspace-catalog.test.ts tests/profile-api.test.ts tests/skill-api.test.ts src/lib/workspace/auth/model.test.ts tests/auth-api.test.ts src/lib/workspace/api/http.test.ts src/lib/workspace/api/workers.test.ts src/lib/workspace/header/breadcrumb-model.test.ts src/lib/workspace/console/chat-submit.test.ts test/composer-history.test.ts tests/composer-paste.test.ts src/lib/workspace/console/composer-command.test.ts src/lib/workspace/console/composer-draft.test.ts src/lib/workspace/console/composer-completion.test.ts src/lib/workspace/console/markdown.test.ts test/console/ansi.test.ts src/lib/workspace/console/model.test.ts src/lib/workspace/companion/api.test.ts tests/workdir-api.test.ts src/lib/workspace/console/tasks.test.ts test/ticket-detail-route-reuse.test.ts test/repositories/ui.test.ts src/lib/workspace/console/worker-console.ui.test.ts src/lib/workspace/settings/model.test.ts tests/runtime-connection.test.ts tests/runtime-management.test.ts tests/runtime-management-source.test.ts src/lib/workspace/sidebar/override-stack.test.ts src/lib/workspace/sidebar/workers.test.ts src/lib/workspace/sidebar/workspace-switcher.test.ts src/lib/workspace/sidebar/worker-subscription.test.ts src/lib/workspace/sidebar/worker-launch.test.ts test/sidebar/worker-actions.test.ts src/lib/workspace/tickets/merge-request-resources.test.ts src/lib/workspace/tickets/ticket-panel.test.ts test/merge-request-status.test.ts test/config-source/decodal-grammar.test.ts test/config-source/editor-state.test.ts test/config-source/fixed-schema-wrapper.test.ts test/config-source/toolchain.test.ts test/config-source/wasm-parity.test.ts test/repository-access/api.test.ts test/repository-access/loader.test.ts test/repository-access/ui.test.ts",
"build": "deno run -A npm:vite@7.2.7 build",
"preview": "deno run -A npm:vite@7.2.7 preview"
},
@@ -47,6 +47,7 @@ export type WorkspaceAuthConfig = {
export type WorkspacePermissionSummary = {
manage_repositories: boolean;
manage_secrets: boolean;
manage_runtimes: boolean;
};
export type DiagnosticSeverity = "info" | "warning" | "error";
@@ -221,6 +222,107 @@ export type RepositoryLogResponse = {
diagnostics: Array<Diagnostic>;
};
export type RuntimeSourceKind = "embedded_worker_runtime" | "remote_http";
export type RuntimeSourceStatus = "active" | "reserved";
export type RuntimeIdentityAuthority =
| "runtime_registry_projection"
| "server_runtime_configuration";
export type RuntimeSourceSummary = {
kind: RuntimeSourceKind;
status: RuntimeSourceStatus;
identity_authority: RuntimeIdentityAuthority;
note: string;
};
export type RuntimeSummary = {
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type RuntimeManagementSummary = {
built_in: boolean;
config_managed: boolean;
removable: boolean;
endpoint_configured: boolean;
token_ref_configured: boolean;
};
export type WorkspaceRuntimeResource = {
management: RuntimeManagementSummary;
runtime_id: string;
label: string;
kind: string;
status: string;
source: RuntimeSourceSummary;
host_ids: Array<string>;
worker_creation_available: boolean;
os: string;
arch: string;
diagnostics: Array<Diagnostic>;
};
export type RuntimeTrustKeyStatus = "unconfigured" | "active" | "revoked";
export type RuntimeTrustKeyState = {
status: RuntimeTrustKeyStatus;
public_key?: string | null;
fingerprint?: string | null;
revision?: number | null;
created_at?: string | null;
updated_at?: string | null;
revoked_at?: string | null;
};
export type RuntimeTrustAuditAction =
| "created"
| "replaced"
| "reactivated"
| "revoked";
export type RuntimeTrustAuditEntry = {
action: RuntimeTrustAuditAction;
actor_account_id: string;
old_fingerprint?: string | null;
new_fingerprint?: string | null;
revision: number;
at: string;
};
export type WorkspaceRuntimeDetail = {
workspace_id: string;
runtime: WorkspaceRuntimeResource;
endpoint?: string | null;
trust_key: RuntimeTrustKeyState;
recent_audit: Array<RuntimeTrustAuditEntry>;
};
export type PutRuntimeTrustKeyRequest = {
public_key: string;
expected_revision: number | null;
};
export type RevokeRuntimeTrustKeyRequest = { expected_revision: number };
export type RuntimeTrustConflictKind = "stale_revision" | "fingerprint_in_use";
export type RuntimeTrustConflictResponse = {
error: RuntimeTrustConflictKind;
message: string;
current_revision?: number;
current_fingerprint?: string | null;
};
export type RuntimeConnectionTestStatus = "compatible" | "failed";
export type RuntimeConnectionTestFailureKind =
@@ -0,0 +1,723 @@
import type {
Diagnostic,
PutRuntimeTrustKeyRequest,
RevokeRuntimeTrustKeyRequest,
RuntimeIdentityAuthority,
RuntimeManagementSummary,
RuntimeSourceKind,
RuntimeSourceStatus,
RuntimeSourceSummary,
RuntimeTrustAuditAction,
RuntimeTrustAuditEntry,
RuntimeTrustConflictKind,
RuntimeTrustConflictResponse,
RuntimeTrustKeyState,
RuntimeTrustKeyStatus,
WorkspaceRuntimeDetail,
WorkspaceRuntimeResource,
} from "$lib/generated/workspace-api.ts";
import type { ListResponse } from "$lib/workspace/sidebar/types";
import { workspaceApiPath } from "./http.ts";
export type WorkspaceRuntimeList = ListResponse<WorkspaceRuntimeResource>;
const LIMITS = {
runtimeItems: 200,
auditEntries: 20,
hostIds: 128,
diagnostics: 64,
idBytes: 256,
labelBytes: 512,
kindBytes: 128,
statusBytes: 128,
noteBytes: 2_048,
endpointBytes: 4_096,
publicKeyBytes: 16 * 1_024,
fingerprintBytes: 512,
timestampBytes: 128,
diagnosticCodeBytes: 128,
diagnosticMessageBytes: 2_048,
conflictMessageBytes: 1_024,
responseBytes: 512 * 1_024,
} as const;
const SOURCE_KINDS = new Set<RuntimeSourceKind>([
"embedded_worker_runtime",
"remote_http",
]);
const SOURCE_STATUSES = new Set<RuntimeSourceStatus>(["active", "reserved"]);
const IDENTITY_AUTHORITIES = new Set<RuntimeIdentityAuthority>([
"runtime_registry_projection",
"server_runtime_configuration",
]);
const DIAGNOSTIC_SEVERITIES = new Set(["info", "warning", "error"]);
const TRUST_STATUSES = new Set<RuntimeTrustKeyStatus>([
"unconfigured",
"active",
"revoked",
]);
const AUDIT_ACTIONS = new Set<RuntimeTrustAuditAction>([
"created",
"replaced",
"reactivated",
"revoked",
]);
const CONFLICT_KINDS = new Set<RuntimeTrustConflictKind>([
"stale_revision",
"fingerprint_in_use",
]);
const encoder = new TextEncoder();
type JsonObject = Record<string, unknown>;
export class RuntimeManagementValidationError extends Error {
constructor(message: string) {
super(message.slice(0, 256));
this.name = "RuntimeManagementValidationError";
}
}
export class RuntimeTrustConflictError extends Error {
readonly conflict: RuntimeTrustConflictResponse;
constructor(conflict: RuntimeTrustConflictResponse) {
super(conflict.message);
this.name = "RuntimeTrustConflictError";
this.conflict = conflict;
}
}
export class RuntimeTrustRequestError extends Error {
readonly field: "public_key" | null;
constructor(message: string, field: "public_key" | null = null) {
super(message.slice(0, 256));
this.name = "RuntimeTrustRequestError";
this.field = field;
}
}
function fail(path: string, message: string): never {
throw new RuntimeManagementValidationError(`${path} ${message}`);
}
function object(value: unknown, path: string): JsonObject {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return fail(path, "must be an object");
}
return value as JsonObject;
}
function exactKeys(
value: JsonObject,
required: readonly string[],
optional: readonly string[],
path: string,
): void {
const allowed = new Set([...required, ...optional]);
for (const key of Object.keys(value)) {
if (!allowed.has(key)) {
fail(`${path}.${key}`, "is not part of the wire contract");
}
}
for (const key of required) {
if (!Object.hasOwn(value, key)) fail(`${path}.${key}`, "is required");
}
}
function array(value: unknown, path: string, max: number): unknown[] {
if (!Array.isArray(value)) return fail(path, "must be an array");
if (value.length > max) {
return fail(path, `must contain at most ${max} items`);
}
return value;
}
function boundedString(
value: unknown,
path: string,
maxBytes: number,
allowEmpty = false,
): string {
if (typeof value !== "string") return fail(path, "must be a string");
if (!allowEmpty && value.length === 0) return fail(path, "must not be empty");
if (encoder.encode(value).byteLength > maxBytes) {
return fail(path, `must be at most ${maxBytes} UTF-8 bytes`);
}
return value;
}
function boolean(value: unknown, path: string): boolean {
if (typeof value !== "boolean") return fail(path, "must be a boolean");
return value;
}
function safeInteger(value: unknown, path: string, minimum = 0): number {
if (
typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum
) {
return fail(path, `must be a safe integer of at least ${minimum}`);
}
return value;
}
function safeRevision(value: unknown, path: string): number {
return safeInteger(value, path, 1);
}
function optionalNullableString(
value: unknown,
path: string,
maxBytes: number,
allowEmpty = false,
): string | null | undefined {
if (value === undefined || value === null) return value;
return boundedString(value, path, maxBytes, allowEmpty);
}
function optionalRevision(
value: unknown,
path: string,
): number | undefined {
if (value === undefined || value === null) return undefined;
return safeRevision(value, path);
}
function optionalNullableRevision(
value: unknown,
path: string,
): number | null | undefined {
if (value === undefined || value === null) return value;
return safeRevision(value, path);
}
function timestamp(value: unknown, path: string): string {
const result = boundedString(value, path, LIMITS.timestampBytes);
if (
!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
.test(result)
) {
return fail(path, "must be an RFC 3339 timestamp");
}
return result;
}
function optionalNullableTimestamp(
value: unknown,
path: string,
): string | null | undefined {
if (value === undefined || value === null) return value;
return timestamp(value, path);
}
function enumValue<T extends string>(
value: unknown,
path: string,
variants: ReadonlySet<T>,
): T {
const result = boundedString(value, path, LIMITS.kindBytes);
if (!variants.has(result as T)) {
return fail(path, "contains an unknown enum value");
}
return result as T;
}
function diagnostic(value: unknown, path: string): Diagnostic {
const item = object(value, path);
exactKeys(item, ["code", "severity", "message"], [], path);
const severity = enumValue(
item.severity,
`${path}.severity`,
DIAGNOSTIC_SEVERITIES,
) as Diagnostic["severity"];
return {
code: boundedString(item.code, `${path}.code`, LIMITS.diagnosticCodeBytes),
severity,
message: boundedString(
item.message,
`${path}.message`,
LIMITS.diagnosticMessageBytes,
true,
),
};
}
function runtimeSource(value: unknown, path: string): RuntimeSourceSummary {
const item = object(value, path);
exactKeys(item, ["kind", "status", "identity_authority", "note"], [], path);
return {
kind: enumValue(item.kind, `${path}.kind`, SOURCE_KINDS),
status: enumValue(item.status, `${path}.status`, SOURCE_STATUSES),
identity_authority: enumValue(
item.identity_authority,
`${path}.identity_authority`,
IDENTITY_AUTHORITIES,
),
note: boundedString(item.note, `${path}.note`, LIMITS.noteBytes, true),
};
}
function runtimeManagement(
value: unknown,
path: string,
): RuntimeManagementSummary {
const item = object(value, path);
exactKeys(
item,
[
"built_in",
"config_managed",
"removable",
"endpoint_configured",
"token_ref_configured",
],
[],
path,
);
return {
built_in: boolean(item.built_in, `${path}.built_in`),
config_managed: boolean(item.config_managed, `${path}.config_managed`),
removable: boolean(item.removable, `${path}.removable`),
endpoint_configured: boolean(
item.endpoint_configured,
`${path}.endpoint_configured`,
),
token_ref_configured: boolean(
item.token_ref_configured,
`${path}.token_ref_configured`,
),
};
}
function runtimeResource(
value: unknown,
path: string,
): WorkspaceRuntimeResource {
const item = object(value, path);
exactKeys(
item,
[
"management",
"runtime_id",
"label",
"kind",
"status",
"source",
"host_ids",
"worker_creation_available",
"os",
"arch",
"diagnostics",
],
[],
path,
);
const hostIds = array(item.host_ids, `${path}.host_ids`, LIMITS.hostIds).map(
(entry, index) =>
boundedString(
entry,
`${path}.host_ids[${index}]`,
LIMITS.idBytes,
),
);
if (new Set(hostIds).size !== hostIds.length) {
fail(`${path}.host_ids`, "must not contain duplicate IDs");
}
return {
management: runtimeManagement(item.management, `${path}.management`),
runtime_id: boundedString(
item.runtime_id,
`${path}.runtime_id`,
LIMITS.idBytes,
),
label: boundedString(item.label, `${path}.label`, LIMITS.labelBytes),
kind: boundedString(item.kind, `${path}.kind`, LIMITS.kindBytes),
status: boundedString(item.status, `${path}.status`, LIMITS.statusBytes),
source: runtimeSource(item.source, `${path}.source`),
host_ids: hostIds,
worker_creation_available: boolean(
item.worker_creation_available,
`${path}.worker_creation_available`,
),
os: boundedString(item.os, `${path}.os`, LIMITS.kindBytes, true),
arch: boundedString(item.arch, `${path}.arch`, LIMITS.kindBytes, true),
diagnostics: array(
item.diagnostics,
`${path}.diagnostics`,
LIMITS.diagnostics,
).map((entry, index) => diagnostic(entry, `${path}.diagnostics[${index}]`)),
};
}
function trustKey(value: unknown, path: string): RuntimeTrustKeyState {
const item = object(value, path);
exactKeys(
item,
["status"],
[
"public_key",
"fingerprint",
"revision",
"created_at",
"updated_at",
"revoked_at",
],
path,
);
const result: RuntimeTrustKeyState = {
status: enumValue(item.status, `${path}.status`, TRUST_STATUSES),
public_key: optionalNullableString(
item.public_key,
`${path}.public_key`,
LIMITS.publicKeyBytes,
),
fingerprint: optionalNullableString(
item.fingerprint,
`${path}.fingerprint`,
LIMITS.fingerprintBytes,
),
revision: optionalNullableRevision(item.revision, `${path}.revision`),
created_at: optionalNullableTimestamp(
item.created_at,
`${path}.created_at`,
),
updated_at: optionalNullableTimestamp(
item.updated_at,
`${path}.updated_at`,
),
revoked_at: optionalNullableTimestamp(
item.revoked_at,
`${path}.revoked_at`,
),
};
const hasBinding = result.status !== "unconfigured";
if (
hasBinding &&
(result.fingerprint == null || result.revision == null ||
result.created_at == null || result.updated_at == null)
) {
fail(
path,
"must include fingerprint, revision, created_at, and updated_at",
);
}
if (
!hasBinding &&
Object.entries(result).some(([key, entry]) =>
key !== "status" && entry != null
)
) {
fail(path, "must not include binding values while unconfigured");
}
if (result.status === "revoked" && result.revoked_at == null) {
fail(`${path}.revoked_at`, "is required for a revoked key");
}
if (result.status === "active" && result.revoked_at != null) {
fail(`${path}.revoked_at`, "must be absent for an active key");
}
return result;
}
function auditEntry(value: unknown, path: string): RuntimeTrustAuditEntry {
const item = object(value, path);
exactKeys(
item,
["action", "actor_account_id", "revision", "at"],
["old_fingerprint", "new_fingerprint"],
path,
);
return {
action: enumValue(item.action, `${path}.action`, AUDIT_ACTIONS),
actor_account_id: boundedString(
item.actor_account_id,
`${path}.actor_account_id`,
LIMITS.idBytes,
),
old_fingerprint: optionalNullableString(
item.old_fingerprint,
`${path}.old_fingerprint`,
LIMITS.fingerprintBytes,
),
new_fingerprint: optionalNullableString(
item.new_fingerprint,
`${path}.new_fingerprint`,
LIMITS.fingerprintBytes,
),
revision: safeRevision(item.revision, `${path}.revision`),
at: timestamp(item.at, `${path}.at`),
};
}
export function parseWorkspaceRuntimeList(
value: unknown,
): WorkspaceRuntimeList {
const response = object(value, "Runtime list response");
exactKeys(
response,
["workspace_id", "limit", "items", "source", "diagnostics"],
[],
"Runtime list response",
);
const limit = safeInteger(response.limit, "Runtime list response.limit", 0);
if (limit > LIMITS.runtimeItems) {
fail(
"Runtime list response.limit",
`must not exceed ${LIMITS.runtimeItems}`,
);
}
const items = array(
response.items,
"Runtime list response.items",
LIMITS.runtimeItems,
).map((entry, index) =>
runtimeResource(entry, `Runtime list response.items[${index}]`)
);
if (items.length > limit) {
fail("Runtime list response.items", "must not exceed the declared limit");
}
return {
workspace_id: boundedString(
response.workspace_id,
"Runtime list response.workspace_id",
LIMITS.idBytes,
),
limit,
items,
source: boundedString(
response.source,
"Runtime list response.source",
LIMITS.kindBytes,
),
diagnostics: array(
response.diagnostics,
"Runtime list response.diagnostics",
LIMITS.diagnostics,
).map((entry, index) =>
diagnostic(entry, `Runtime list response.diagnostics[${index}]`)
),
};
}
export function parseWorkspaceRuntimeDetail(
value: unknown,
): WorkspaceRuntimeDetail {
const response = object(value, "Runtime detail response");
exactKeys(
response,
["workspace_id", "runtime", "trust_key", "recent_audit"],
["endpoint"],
"Runtime detail response",
);
return {
workspace_id: boundedString(
response.workspace_id,
"Runtime detail response.workspace_id",
LIMITS.idBytes,
),
runtime: runtimeResource(
response.runtime,
"Runtime detail response.runtime",
),
endpoint: optionalNullableString(
response.endpoint,
"Runtime detail response.endpoint",
LIMITS.endpointBytes,
),
trust_key: trustKey(
response.trust_key,
"Runtime detail response.trust_key",
),
recent_audit: array(
response.recent_audit,
"Runtime detail response.recent_audit",
LIMITS.auditEntries,
).map((entry, index) =>
auditEntry(entry, `Runtime detail response.recent_audit[${index}]`)
),
};
}
export function parseRuntimeTrustConflict(
value: unknown,
): RuntimeTrustConflictResponse {
const response = object(value, "Runtime trust conflict");
exactKeys(
response,
["error", "message"],
["current_revision", "current_fingerprint"],
"Runtime trust conflict",
);
return {
error: enumValue(
response.error,
"Runtime trust conflict.error",
CONFLICT_KINDS,
),
message: boundedString(
response.message,
"Runtime trust conflict.message",
LIMITS.conflictMessageBytes,
),
current_revision: optionalRevision(
response.current_revision,
"Runtime trust conflict.current_revision",
),
current_fingerprint: optionalNullableString(
response.current_fingerprint,
"Runtime trust conflict.current_fingerprint",
LIMITS.fingerprintBytes,
),
};
}
function revisionForJson(revision: number | null): number | null {
if (revision === null) return null;
if (!Number.isSafeInteger(revision) || revision < 1) {
throw new RuntimeTrustRequestError(
"Runtime trust revision is not a safe integer",
);
}
return revision;
}
async function readBoundedJson(response: Response): Promise<unknown> {
const contentLength = response.headers.get("content-length");
if (contentLength !== null) {
const parsed = Number(contentLength);
if (Number.isFinite(parsed) && parsed > LIMITS.responseBytes) {
throw new RuntimeTrustRequestError(
"Runtime trust response exceeds its byte limit",
);
}
}
const text = await response.text();
if (encoder.encode(text).byteLength > LIMITS.responseBytes) {
throw new RuntimeTrustRequestError(
"Runtime trust response exceeds its byte limit",
);
}
try {
return JSON.parse(text) as unknown;
} catch {
throw new RuntimeTrustRequestError(
"Runtime trust response is not valid JSON",
);
}
}
function requestErrorFrom(
value: unknown,
status: number,
): RuntimeTrustRequestError {
try {
const response = object(value, "Runtime trust error");
exactKeys(
response,
["error", "message", "diagnostics"],
[],
"Runtime trust error",
);
const diagnostics = array(
response.diagnostics,
"Runtime trust error.diagnostics",
LIMITS.diagnostics,
).map((entry, index) =>
diagnostic(entry, `Runtime trust error.diagnostics[${index}]`)
);
const message = boundedString(
response.message,
"Runtime trust error.message",
LIMITS.conflictMessageBytes,
);
const field = diagnostics.some((entry) =>
entry.code.startsWith("runtime_public_key_")
)
? "public_key"
: null;
return new RuntimeTrustRequestError(message, field);
} catch {
return new RuntimeTrustRequestError(
`Runtime trust request failed (${status})`,
);
}
}
async function finishMutation(
response: Response,
workspaceId: string,
runtimeId: string,
): Promise<WorkspaceRuntimeDetail> {
const payload = await readBoundedJson(response);
if (response.status === 409) {
try {
throw new RuntimeTrustConflictError(parseRuntimeTrustConflict(payload));
} catch (error) {
if (error instanceof RuntimeTrustConflictError) throw error;
throw new RuntimeTrustRequestError(
"Runtime trust conflict response was invalid",
);
}
}
if (!response.ok) throw requestErrorFrom(payload, response.status);
let detail: WorkspaceRuntimeDetail;
try {
detail = parseWorkspaceRuntimeDetail(payload);
} catch {
throw new RuntimeTrustRequestError("Runtime trust response was invalid");
}
if (
detail.workspace_id !== workspaceId ||
detail.runtime.runtime_id !== runtimeId
) {
throw new RuntimeTrustRequestError(
"Runtime trust response did not match the selected Runtime",
);
}
return detail;
}
export async function putRuntimeTrustKey(
workspaceId: string,
runtimeId: string,
request: PutRuntimeTrustKeyRequest,
fetchImpl: typeof fetch = fetch,
): Promise<WorkspaceRuntimeDetail> {
const response = await fetchImpl(
workspaceApiPath(
workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}/trust-key`,
),
{
method: "PUT",
headers: { "content-type": "application/json" },
body: JSON.stringify({
public_key: request.public_key,
expected_revision: revisionForJson(request.expected_revision),
}),
},
);
return await finishMutation(response, workspaceId, runtimeId);
}
export async function revokeRuntimeTrustKey(
workspaceId: string,
runtimeId: string,
request: RevokeRuntimeTrustKeyRequest,
fetchImpl: typeof fetch = fetch,
): Promise<WorkspaceRuntimeDetail> {
const response = await fetchImpl(
workspaceApiPath(
workspaceId,
`/runtimes/${encodeURIComponent(runtimeId)}/trust-key`,
),
{
method: "DELETE",
headers: { "content-type": "application/json" },
body: JSON.stringify({
expected_revision: revisionForJson(request.expected_revision),
}),
},
);
return await finishMutation(response, workspaceId, runtimeId);
}
@@ -367,13 +367,18 @@ function authConfig(value: unknown, path: string): WorkspaceAuthConfig {
function permissions(value: unknown, path: string): WorkspacePermissionSummary {
const item = object(value, path);
exactKeys(item, ["manage_repositories", "manage_secrets"], path);
exactKeys(
item,
["manage_repositories", "manage_secrets", "manage_runtimes"],
path,
);
return {
manage_repositories: boolean(
item.manage_repositories,
`${path}.manage_repositories`,
),
manage_secrets: boolean(item.manage_secrets, `${path}.manage_secrets`),
manage_runtimes: boolean(item.manage_runtimes, `${path}.manage_runtimes`),
};
}
@@ -342,6 +342,196 @@
.settings-test-result.failed {
border-inline-start: 3px solid var(--danger);
}
.runtime-detail-page {
display: grid;
gap: var(--space-5);
}
.runtime-detail-section {
display: grid;
gap: var(--space-3);
padding-top: var(--space-4);
border-top: 1px solid var(--line);
}
.runtime-detail-section h2,
.runtime-detail-section p {
margin: 0;
}
.runtime-detail-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
gap: var(--space-3) var(--space-5);
margin: 0;
}
.runtime-detail-grid div {
min-width: 0;
}
.runtime-detail-grid dt {
margin-bottom: var(--space-1);
color: var(--text-muted);
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.runtime-detail-grid dd {
margin: 0;
color: var(--text-strong);
overflow-wrap: anywhere;
}
.runtime-public-key-actions,
.runtime-revoke-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--space-3);
flex-wrap: wrap;
}
.runtime-public-key-actions {
justify-content: flex-start;
}
.runtime-public-key-actions button,
.runtime-revoke-row button,
.runtime-trust-form button {
border: 0;
border-radius: 0.6rem;
padding: 0.5rem 0.75rem;
background: var(--accent);
color: var(--bg);
font-weight: 700;
cursor: pointer;
}
.runtime-public-key-actions button.secondary {
border: 1px solid var(--line);
background: transparent;
color: var(--text-strong);
}
.runtime-public-key-actions button:disabled,
.runtime-revoke-row button:disabled,
.runtime-trust-form button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.runtime-public-key,
.runtime-trust-form textarea,
.runtime-trust-form input {
border: 1px solid var(--line);
border-radius: 0.5rem;
background: var(--bg-raised);
color: var(--text-strong);
font-family: var(--font-mono);
font-size: 0.78rem;
}
.runtime-public-key {
max-height: 14rem;
margin: 0;
padding: var(--space-3);
overflow: auto;
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.runtime-trust-form {
display: grid;
gap: var(--space-2);
max-width: 56rem;
}
.runtime-trust-form label {
color: var(--text-muted);
font-size: 0.78rem;
font-weight: 700;
}
.runtime-trust-form textarea,
.runtime-trust-form input {
width: 100%;
padding: 0.65rem 0.75rem;
}
.runtime-trust-form textarea {
resize: vertical;
}
.runtime-trust-form small {
color: var(--text-muted);
}
.runtime-trust-form .field-error,
.runtime-detail-page .section-state.error {
color: var(--danger);
}
.runtime-detail-page .section-state.success {
color: var(--success);
}
.runtime-revoke-row {
padding-top: var(--space-3);
border-top: 1px solid var(--line);
}
.runtime-revoke-row div {
display: grid;
gap: var(--space-1);
}
.runtime-revoke-row p {
color: var(--text-muted);
}
.runtime-revoke-row button.danger {
background: var(--danger);
}
.runtime-audit-table-wrap {
overflow-x: auto;
}
.runtime-audit-table {
width: 100%;
min-width: 48rem;
border-collapse: collapse;
}
.runtime-audit-table th,
.runtime-audit-table td {
padding: 0.7rem 0.5rem;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
}
.runtime-audit-table th {
color: var(--text-muted);
font-size: 0.72rem;
letter-spacing: 0.05em;
text-transform: uppercase;
}
.runtime-audit-table code {
overflow-wrap: anywhere;
}
@media (max-width: 760px) {
.runtime-revoke-row {
align-items: stretch;
}
}
.settings-page {
display: grid;
gap: var(--space-5);
@@ -1,9 +1,11 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import type { RuntimeConnectionTestResponse } from '$lib/generated/workspace-api';
import type {
RuntimeConnectionTestResponse,
WorkspaceRuntimeResource,
} from '$lib/generated/workspace-api';
import { testRuntimeConnection } from '$lib/workspace/api/runtime-connection';
import { workspaceApiPath } from '$lib/workspace/api/http';
import type { Runtime } from '$lib/workspace/sidebar/types';
import type { PageProps } from './$types';
let { data }: PageProps = $props();
@@ -15,7 +17,7 @@
let requestError = $state<string | null>(null);
let testResults = $state<Record<string, RuntimeConnectionTestResponse>>({});
function runtimePlatform(runtime: Runtime): string {
function runtimePlatform(runtime: WorkspaceRuntimeResource): string {
return runtime.os && runtime.arch ? `${runtime.os} / ${runtime.arch}` : 'Unknown';
}
@@ -38,7 +40,7 @@
}
}
function managementLabel(runtime: Runtime): string {
function managementLabel(runtime: WorkspaceRuntimeResource): string {
if (runtime.management?.built_in) return 'Built-in';
if (runtime.management?.config_managed) return 'Managed remote';
return 'Observed';
@@ -78,27 +80,7 @@
}
}
async function deleteRuntime(runtime: Runtime): Promise<void> {
requestError = null;
busyRuntimeId = runtime.runtime_id;
try {
const response = await fetch(
workspaceApiPath(data.workspaceId, `/runtimes/${encodeURIComponent(runtime.runtime_id)}`),
{ method: 'DELETE' },
);
if (!response.ok) throw new Error(await responseError(response));
const nextResults = { ...testResults };
delete nextResults[runtime.runtime_id];
testResults = nextResults;
await invalidateAll();
} catch (error) {
requestError = error instanceof Error ? error.message : String(error);
} finally {
busyRuntimeId = null;
}
}
async function testRuntime(runtime: Runtime): Promise<void> {
async function testRuntime(runtime: WorkspaceRuntimeResource): Promise<void> {
requestError = null;
busyRuntimeId = runtime.runtime_id;
try {
@@ -123,12 +105,14 @@
<h1 id="runtimes-heading">Runtimes</h1>
<p>Register and inspect the execution backends available to this Workspace.</p>
</div>
{#if data.workspace.permissions.manage_runtimes}
<button type="button" onclick={() => showAddRuntime = !showAddRuntime}>
{showAddRuntime ? 'Close' : 'Add Runtime'}
</button>
{/if}
</header>
{#if showAddRuntime}
{#if showAddRuntime && data.workspace.permissions.manage_runtimes}
<form class="settings-runtime-form" onsubmit={addRuntime}>
<h2>Add remote Runtime</h2>
<div class="settings-form-grid">
@@ -182,7 +166,11 @@
{#each data.runtimes.items as runtime}
<tr class:inactive={runtime.status !== 'active'}>
<td>
<strong>{runtime.label}</strong>
<strong>
<a class="inline-link" href={`/w/${encodeURIComponent(data.workspaceId)}/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}`}>
{runtime.label}
</a>
</strong>
<small><code>{runtime.runtime_id}</code></small>
</td>
<td>{runtime.kind}</td>
@@ -203,15 +191,8 @@
onclick={() => testRuntime(runtime)}
>Test</button>
{/if}
{#if runtime.management?.removable}
<button
class="danger"
type="button"
disabled={busyRuntimeId !== null}
onclick={() => deleteRuntime(runtime)}
>Delete</button>
{:else}
<span class="settings-muted-action">Not removable</span>
{#if !runtime.management?.config_managed}
<span class="settings-muted-action">Test unavailable</span>
{/if}
</div>
</td>
@@ -1,11 +1,19 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import type { ListResponse, Runtime } from "$lib/workspace/sidebar/types";
import { parseWorkspaceRuntimeList } from "$lib/workspace/api/runtime-management";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const runtimes = await loadJson<ListResponse<Runtime>>(
const runtimes = await loadJson(
fetch,
workspaceApiPath(params.workspaceId, "/runtimes"),
undefined,
(value) => {
const response = parseWorkspaceRuntimeList(value);
if (response.workspace_id !== params.workspaceId) {
throw new Error("Runtime list Workspace did not match the route");
}
return response;
},
);
return {
@@ -0,0 +1,325 @@
<script lang="ts">
import { invalidateAll } from '$app/navigation';
import type {
PutRuntimeTrustKeyRequest,
RevokeRuntimeTrustKeyRequest,
RuntimeTrustKeyStatus,
} from '$lib/generated/workspace-api';
import {
putRuntimeTrustKey,
revokeRuntimeTrustKey,
RuntimeTrustConflictError,
RuntimeTrustRequestError,
} from '$lib/workspace/api/runtime-management';
import type { PageProps } from './$types';
type TrustAction = 'create' | 'replace' | 'reactivate';
let { data }: PageProps = $props();
let revealPublicKey = $state(false);
let publicKey = $state('');
let fingerprintConfirmation = $state('');
let busyAction = $state<'save' | 'revoke' | 'copy' | null>(null);
let fieldError = $state<string | null>(null);
let requestError = $state<string | null>(null);
let successMessage = $state<string | null>(null);
function trustAction(status: RuntimeTrustKeyStatus): TrustAction {
if (status === 'unconfigured') return 'create';
if (status === 'revoked') return 'reactivate';
return 'replace';
}
function actionLabel(action: TrustAction): string {
switch (action) {
case 'create': return 'Create Workspace trust';
case 'replace': return 'Replace trusted key';
case 'reactivate': return 'Reactivate with this key';
}
}
function formatTimestamp(value: string | null | undefined): string {
if (!value) return '—';
const parsed = new Date(value);
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString();
}
function utf8Bytes(value: string): number {
return new TextEncoder().encode(value).byteLength;
}
async function reloadAuthority(): Promise<void> {
await invalidateAll();
}
async function saveTrustKey(event: SubmitEvent): Promise<void> {
event.preventDefault();
if (busyAction !== null || !data.runtimeDetail) return;
fieldError = null;
requestError = null;
successMessage = null;
const key = publicKey.trim();
if (!key) {
fieldError = 'Enter the Runtime public key.';
return;
}
if (utf8Bytes(key) > 16 * 1024) {
fieldError = 'Public key must be at most 16 KiB of UTF-8 text.';
return;
}
const trust = data.runtimeDetail.trust_key;
const action = trustAction(trust.status);
if (action !== 'create') {
if (!trust.fingerprint) {
requestError = 'The authoritative fingerprint is unavailable. Reload before changing trust.';
return;
}
if (fingerprintConfirmation.trim() !== trust.fingerprint) {
fieldError = 'Enter the current fingerprint exactly to confirm this change.';
return;
}
}
const request: PutRuntimeTrustKeyRequest = {
public_key: key,
expected_revision: trust.revision ?? null,
};
busyAction = 'save';
try {
await putRuntimeTrustKey(data.workspaceId, data.runtimeId, request);
publicKey = '';
fingerprintConfirmation = '';
revealPublicKey = false;
successMessage = action === 'create'
? 'Workspace trust was created.'
: action === 'replace'
? 'The trusted Runtime key was replaced.'
: 'Workspace trust was reactivated.';
await reloadAuthority();
} catch (error) {
fingerprintConfirmation = '';
if (error instanceof RuntimeTrustConflictError) {
requestError = `${error.message} Authoritative Runtime trust has been reloaded.`;
await reloadAuthority();
} else if (error instanceof RuntimeTrustRequestError && error.field === 'public_key') {
fieldError = error.message;
} else {
requestError = error instanceof Error ? error.message : 'Runtime trust update failed.';
}
} finally {
busyAction = null;
}
}
async function revokeTrust(): Promise<void> {
if (busyAction !== null || !data.runtimeDetail) return;
const trust = data.runtimeDetail.trust_key;
if (trust.revision == null || trust.status !== 'active') {
requestError = 'Only active Workspace trust can be revoked.';
return;
}
fieldError = null;
requestError = null;
successMessage = null;
busyAction = 'revoke';
const request: RevokeRuntimeTrustKeyRequest = {
expected_revision: trust.revision,
};
try {
await revokeRuntimeTrustKey(data.workspaceId, data.runtimeId, request);
publicKey = '';
fingerprintConfirmation = '';
revealPublicKey = false;
successMessage = 'Workspace trust was revoked.';
await reloadAuthority();
} catch (error) {
if (error instanceof RuntimeTrustConflictError) {
requestError = `${error.message} Authoritative Runtime trust has been reloaded.`;
await reloadAuthority();
} else {
requestError = error instanceof Error ? error.message : 'Runtime trust revoke failed.';
}
} finally {
busyAction = null;
}
}
async function copyPublicKey(): Promise<void> {
const key = data.runtimeDetail?.trust_key.public_key;
if (!key || busyAction !== null) return;
busyAction = 'copy';
requestError = null;
try {
await navigator.clipboard.writeText(key);
successMessage = 'Public key copied.';
} catch {
requestError = 'The browser could not copy the public key.';
} finally {
busyAction = null;
}
}
</script>
<svelte:head>
<title>{data.runtimeDetail?.runtime.label ?? data.runtimeId} · Runtime Settings · Yoi Workspace</title>
<meta name="description" content="Runtime identity and Workspace trust settings" />
</svelte:head>
<section class="runtime-detail-page" aria-labelledby="runtime-detail-heading">
<header class="page-header-row">
<div>
<a class="inline-link" href={`/w/${encodeURIComponent(data.workspaceId)}/settings/runtimes`}>Runtimes</a>
<h1 id="runtime-detail-heading">{data.runtimeDetail?.runtime.label ?? data.runtimeId}</h1>
<p><code>{data.runtimeId}</code></p>
</div>
<a class="button-link" href={`/w/${encodeURIComponent(data.workspaceId)}/settings/runtimes/${encodeURIComponent(data.runtimeId)}/workdirs`}>
Workdirs
</a>
</header>
{#if data.runtimeDetailError}
<p class="section-state error">{data.runtimeDetailError}</p>
{:else if !data.runtimeDetail}
<p class="section-state">Loading Runtime…</p>
{:else}
{@const detail = data.runtimeDetail}
{@const runtime = detail.runtime}
{@const trust = detail.trust_key}
{@const currentAction = trustAction(trust.status)}
<section class="runtime-detail-section" aria-labelledby="runtime-identity-heading">
<h2 id="runtime-identity-heading">Identity and binding</h2>
<dl class="runtime-detail-grid">
<div><dt>Runtime ID</dt><dd><code>{runtime.runtime_id}</code></dd></div>
<div><dt>Kind</dt><dd>{runtime.kind}</dd></div>
<div><dt>Endpoint</dt><dd>{detail.endpoint ?? 'Not configured'}</dd></div>
<div><dt>Status</dt><dd>{runtime.status}</dd></div>
<div><dt>Binding status</dt><dd>{trust.status}</dd></div>
<div><dt>Fingerprint</dt><dd><code>{trust.fingerprint ?? '—'}</code></dd></div>
<div><dt>Revision</dt><dd>{trust.revision?.toString() ?? '—'}</dd></div>
<div><dt>Created</dt><dd>{formatTimestamp(trust.created_at)}</dd></div>
<div><dt>Updated</dt><dd>{formatTimestamp(trust.updated_at)}</dd></div>
<div><dt>Revoked</dt><dd>{formatTimestamp(trust.revoked_at)}</dd></div>
</dl>
{#if runtime.diagnostics.length > 0}
<ul class="settings-diagnostics-list">
{#each runtime.diagnostics as diagnostic}
<li class:error={diagnostic.severity === 'error'} class:warning={diagnostic.severity === 'warning'}>
<strong>{diagnostic.code}</strong>
<span>{diagnostic.message}</span>
</li>
{/each}
</ul>
{/if}
</section>
{#if data.workspace.permissions.manage_runtimes}
<section class="runtime-detail-section" aria-labelledby="runtime-trust-heading">
<h2 id="runtime-trust-heading">Workspace trust</h2>
{#if trust.public_key}
<div class="runtime-public-key-actions">
<button type="button" class="secondary" onclick={() => revealPublicKey = !revealPublicKey}>
{revealPublicKey ? 'Hide public key' : 'Reveal public key'}
</button>
<button type="button" class="secondary" disabled={busyAction !== null} onclick={copyPublicKey}>
{busyAction === 'copy' ? 'Copying…' : 'Copy public key'}
</button>
</div>
{#if revealPublicKey}
<pre class="runtime-public-key"><code>{trust.public_key}</code></pre>
{/if}
{:else if trust.status !== 'unconfigured'}
<p class="section-state">The public key was not included in this authorized response.</p>
{/if}
<form class="runtime-trust-form" onsubmit={saveTrustKey}>
<label for="runtime-public-key-input">Runtime public key</label>
<textarea
id="runtime-public-key-input"
bind:value={publicKey}
rows="5"
autocomplete="off"
spellcheck="false"
aria-describedby={fieldError ? 'runtime-public-key-error' : undefined}
aria-invalid={fieldError ? 'true' : undefined}
placeholder="ssh-ed25519 …"
></textarea>
{#if currentAction !== 'create'}
<label for="runtime-fingerprint-confirmation">Confirm current fingerprint</label>
<input
id="runtime-fingerprint-confirmation"
bind:value={fingerprintConfirmation}
autocomplete="off"
spellcheck="false"
placeholder={trust.fingerprint ?? ''}
/>
<small>Enter <code>{trust.fingerprint ?? 'the current fingerprint'}</code> exactly.</small>
{/if}
{#if fieldError}
<p id="runtime-public-key-error" class="field-error">{fieldError}</p>
{/if}
<div class="settings-action-row">
<button type="submit" disabled={busyAction !== null}>
{busyAction === 'save' ? 'Saving…' : actionLabel(currentAction)}
</button>
</div>
</form>
<div class="runtime-revoke-row">
<div>
<strong>Revoke Workspace trust</strong>
<p>Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.</p>
</div>
<button
type="button"
class="danger"
disabled={busyAction !== null || trust.status !== 'active'}
onclick={revokeTrust}
>{busyAction === 'revoke' ? 'Revoking…' : 'Revoke trust'}</button>
</div>
{#if requestError}
<p class="section-state error" role="alert">{requestError}</p>
{/if}
{#if successMessage}
<p class="section-state success" role="status">{successMessage}</p>
{/if}
</section>
{/if}
<section class="runtime-detail-section" aria-labelledby="runtime-audit-heading">
<h2 id="runtime-audit-heading">Recent trust audit</h2>
{#if detail.recent_audit.length === 0}
<p class="section-state">No trust changes are recorded.</p>
{:else}
<div class="runtime-audit-table-wrap">
<table class="runtime-audit-table">
<thead>
<tr><th>Action</th><th>Revision</th><th>Fingerprint</th><th>Actor</th><th>Time</th></tr>
</thead>
<tbody>
{#each detail.recent_audit as entry}
<tr>
<td>{entry.action}</td>
<td>{entry.revision.toString()}</td>
<td><code>{entry.new_fingerprint ?? entry.old_fingerprint ?? '—'}</code></td>
<td><code>{entry.actor_account_id}</code></td>
<td>{formatTimestamp(entry.at)}</td>
</tr>
{/each}
</tbody>
</table>
</div>
{/if}
</section>
{/if}
</section>
@@ -0,0 +1,31 @@
import { loadJson, workspaceApiPath } from "$lib/workspace/api/http";
import { parseWorkspaceRuntimeDetail } from "$lib/workspace/api/runtime-management";
import type { PageLoad } from "./$types";
export const load: PageLoad = async ({ fetch, params }) => {
const detail = await loadJson(
fetch,
workspaceApiPath(
params.workspaceId,
`/runtimes/${encodeURIComponent(params.runtimeId)}`,
),
undefined,
(value) => {
const response = parseWorkspaceRuntimeDetail(value);
if (
response.workspace_id !== params.workspaceId ||
response.runtime.runtime_id !== params.runtimeId
) {
throw new Error("Runtime detail did not match the route");
}
return response;
},
);
return {
workspaceId: params.workspaceId,
runtimeId: params.runtimeId,
runtimeDetail: detail.data,
runtimeDetailError: detail.error,
};
};
@@ -0,0 +1,133 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
readTextFile(path: URL): Promise<string>;
};
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
Deno.test("Runtime Settings routes validate unknown JSON through the shared Runtime parser", async () => {
const [listLoader, detailLoader] = await Promise.all([
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/runtimes/+page.ts",
import.meta.url,
),
),
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.ts",
import.meta.url,
),
),
]);
assert(
listLoader.includes("parseWorkspaceRuntimeList(value)"),
"Runtime list loader should validate unknown JSON",
);
assert(
detailLoader.includes("parseWorkspaceRuntimeDetail(value)"),
"Runtime detail loader should validate unknown JSON",
);
for (const source of [listLoader, detailLoader]) {
assert(
!source.includes("loadJson<"),
"Runtime loaders must not cast response JSON to a handwritten DTO",
);
}
});
Deno.test("Runtime list links to canonical detail and has no inline delete action", async () => {
const page = await Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/runtimes/+page.svelte",
import.meta.url,
),
);
assert(
page.includes(
"/settings/runtimes/${encodeURIComponent(runtime.runtime_id)}",
),
"Runtime name should link to canonical detail",
);
assert(
page.includes("testRuntime(runtime)"),
"connection Test should remain available",
);
assert(page.includes("Add Runtime"), "Add Runtime should remain available");
assert(
page.includes("data.workspace.permissions.manage_runtimes"),
"Add Runtime should be hidden from non-owners",
);
assert(
!page.includes("deleteRuntime"),
"inline Runtime delete logic must be removed",
);
assert(
!page.includes(">Delete</button>"),
"inline Runtime delete control must be removed",
);
});
Deno.test("Runtime detail keeps trust controls owner-only and conflict-safe", async () => {
const page = await Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte",
import.meta.url,
),
);
const ownerGate = page.indexOf("data.workspace.permissions.manage_runtimes");
const reveal = page.indexOf("Reveal public key");
const mutation = page.indexOf('id="runtime-public-key-input"');
assert(ownerGate >= 0, "Runtime trust controls should use manage_runtimes");
assert(
ownerGate < reveal && ownerGate < mutation,
"owner gate should wrap key controls",
);
for (
const token of [
"Create Workspace trust",
"Replace trusted key",
"Reactivate with this key",
"Confirm current fingerprint",
"Revoke Workspace trust",
"Workspace trust only; this does not delete the Runtime process, Workers, or Workdirs.",
"RuntimeTrustConflictError",
"await reloadAuthority()",
"busyAction !== null",
"Workdirs",
"Recent trust audit",
]
) {
assert(page.includes(token), `Runtime detail should include ${token}`);
}
});
Deno.test("Runtime detail uses flat sections instead of nested cards", async () => {
const [page, css] = await Promise.all([
Deno.readTextFile(
new URL(
"../src/routes/w/[workspaceId]/settings/runtimes/[runtimeId]/+page.svelte",
import.meta.url,
),
),
Deno.readTextFile(
new URL("../src/lib/workspace/styles/settings.css", import.meta.url),
),
]);
assert(
!page.includes('class="card"') && !page.includes("settings-card"),
"Runtime detail should not add card nesting",
);
assert(
css.includes(".runtime-detail-section") &&
css.includes("border-top: 1px solid var(--line)"),
"Runtime detail hierarchy should use flat section separators",
);
});
@@ -0,0 +1,227 @@
declare const Deno: {
test(name: string, fn: () => void | Promise<void>): void;
};
import {
parseRuntimeTrustConflict,
parseWorkspaceRuntimeDetail,
parseWorkspaceRuntimeList,
putRuntimeTrustKey,
RuntimeTrustConflictError,
} from "../src/lib/workspace/api/runtime-management.ts";
function assert(condition: unknown, message: string): asserts condition {
if (!condition) throw new Error(message);
}
function assertThrows(operation: () => unknown, expected: string): void {
try {
operation();
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
if (message.includes(expected)) return;
throw new Error(
`expected error containing ${expected}, received ${message}`,
);
}
throw new Error("expected operation to throw");
}
function runtime() {
return {
management: {
built_in: false,
config_managed: true,
removable: false,
endpoint_configured: true,
token_ref_configured: false,
},
runtime_id: "arcadia",
label: "Arcadia",
kind: "remote",
status: "started",
source: {
kind: "remote_http",
status: "active",
identity_authority: "server_runtime_configuration",
note: "Configured by Server authority",
},
host_ids: ["host-a"],
worker_creation_available: true,
os: "linux",
arch: "x86_64",
diagnostics: [],
};
}
function detail() {
return {
workspace_id: "workspace-a",
runtime: runtime(),
endpoint: "https://runtime.example.test",
trust_key: {
status: "active",
public_key: "ssh-ed25519 AAAA-test",
fingerprint: "SHA256:current",
revision: 3,
created_at: "2026-09-01T12:00:00Z",
updated_at: "2026-09-01T13:00:00Z",
revoked_at: null,
},
recent_audit: [{
action: "created",
actor_account_id: "account-a",
old_fingerprint: null,
new_fingerprint: "SHA256:current",
revision: 3,
at: "2026-09-01T13:00:00Z",
}],
};
}
Deno.test("Runtime list and detail parsers return generated Runtime DTO shapes", () => {
const list = parseWorkspaceRuntimeList({
workspace_id: "workspace-a",
limit: 200,
items: [runtime()],
source: "workspace-control-plane",
diagnostics: [],
});
assert(
list.items[0]?.runtime_id === "arcadia",
"Runtime ID was not preserved",
);
const parsed = parseWorkspaceRuntimeDetail(detail());
assert(
parsed.trust_key.revision === 3,
"revision was not preserved as a safe integer",
);
assert(
parsed.recent_audit[0]?.revision === 3,
"audit revision was not normalized",
);
});
Deno.test("Runtime validators reject unknown object keys and enum variants", () => {
assertThrows(
() => parseWorkspaceRuntimeDetail({ ...detail(), head_tree: "stale" }),
"head_tree is not part",
);
const futureSource = structuredClone(detail());
futureSource.runtime.source.kind = "future_transport";
assertThrows(
() => parseWorkspaceRuntimeDetail(futureSource),
"contains an unknown enum value",
);
assertThrows(
() =>
parseRuntimeTrustConflict({
error: "future_conflict",
message: "conflict",
current_revision: 4,
current_fingerprint: "SHA256:new",
}),
"contains an unknown enum value",
);
});
Deno.test("Runtime validators reject unsafe revisions and bounded collection overflow", () => {
const unsafeRevision = structuredClone(detail());
unsafeRevision.trust_key.revision = Number.MAX_SAFE_INTEGER + 1;
assertThrows(
() => parseWorkspaceRuntimeDetail(unsafeRevision),
"must be a safe integer",
);
const tooMuchAudit = structuredClone(detail());
tooMuchAudit.recent_audit = Array.from(
{ length: 21 },
() => structuredClone(detail().recent_audit[0]),
);
assertThrows(
() => parseWorkspaceRuntimeDetail(tooMuchAudit),
"must contain at most 20 items",
);
const tooManyItems = Array.from({ length: 201 }, () => runtime());
assertThrows(
() =>
parseWorkspaceRuntimeList({
workspace_id: "workspace-a",
limit: 200,
items: tooManyItems,
source: "workspace-control-plane",
diagnostics: [],
}),
"must contain at most 200 items",
);
});
Deno.test("Runtime detail rejects unbounded strings and incoherent trust state", () => {
const largeKey = structuredClone(detail());
largeKey.trust_key.public_key = "x".repeat(16 * 1024 + 1);
assertThrows(
() => parseWorkspaceRuntimeDetail(largeKey),
"must be at most 16384 UTF-8 bytes",
);
const activeWithoutFingerprint = structuredClone(detail()) as Record<
string,
unknown
>;
(activeWithoutFingerprint.trust_key as Record<string, unknown>).fingerprint =
null;
assertThrows(
() => parseWorkspaceRuntimeDetail(activeWithoutFingerprint),
"must include fingerprint",
);
});
Deno.test("typed trust conflict is validated and preserves authoritative revision", async () => {
let sentBody: unknown = null;
const fetchImpl = ((_: RequestInfo | URL, init?: RequestInit) => {
sentBody = JSON.parse(String(init?.body)) as unknown;
return Promise.resolve(
new Response(
JSON.stringify({
error: "stale_revision",
message: "Runtime trust changed",
current_revision: 4,
current_fingerprint: "SHA256:new",
}),
{ status: 409, headers: { "content-type": "application/json" } },
),
);
}) as typeof fetch;
try {
await putRuntimeTrustKey(
"workspace-a",
"arcadia",
{ public_key: "ssh-ed25519 AAAA-new", expected_revision: 3 },
fetchImpl,
);
throw new Error("expected mutation to reject");
} catch (error) {
assert(
error instanceof RuntimeTrustConflictError,
"expected typed conflict",
);
assert(
error.conflict.current_revision === 4,
"authoritative revision was lost",
);
}
assert(
JSON.stringify(sentBody) ===
JSON.stringify({
public_key: "ssh-ed25519 AAAA-new",
expected_revision: 3,
}),
"request should serialize the generated bigint revision as a safe JSON integer",
);
});