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
+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!({