fix: complete Runtime verification cutover

This commit is contained in:
2026-09-08 10:11:18 +09:00
parent 7b6a84a550
commit fae36d220d
4 changed files with 495 additions and 180 deletions
+27 -8
View File
@@ -3433,11 +3433,10 @@ fn workspace_runtime_operation(method: &str, path_and_query: &str) -> &'static s
{
return "workers:create";
}
if path.ends_with("/input")
|| path.ends_with("/restore")
|| path.ends_with("/workspace-api")
|| path.contains("/attachments")
{
if path.ends_with("/workspace-api") {
return "workers:create";
}
if path.ends_with("/input") || path.ends_with("/restore") || path.contains("/attachments") {
return "workers:input";
}
if path.ends_with("/stop") || path.ends_with("/cancel") {
@@ -4556,13 +4555,20 @@ impl WorkspaceWorkerRuntime for RemoteWorkerRuntime {
&self,
worker_id: &str,
) -> Option<crate::observation::RuntimeObservationSource> {
let path = format!("/v1/workers/{worker_id}/protocol/ws");
let bearer_token = match &self.workspace_authorization {
Some(authorization) => authorization
.issue("GET", &path, "workers:protocol", Some(worker_id), &[])
.ok(),
None => self
.runtime_capability_token(&path)
.or_else(|| self.bearer_token.clone()),
};
Some(crate::observation::RuntimeObservationSource::remote_ws(
crate::observation::RuntimeObservationSourceConfig {
worker: RuntimeWorkerRef::new(&self.runtime_id, worker_id),
endpoint: self.ws_endpoint(worker_id),
bearer_token: self
.runtime_capability_token(&format!("/v1/workers/{worker_id}/protocol"))
.or_else(|| self.bearer_token.clone()),
bearer_token,
},
))
}
@@ -6631,6 +6637,19 @@ mod tests {
assert!(!format!("{failure:?}").contains("secret-token"));
}
#[test]
fn workspace_runtime_operation_matches_runtime_permission_classifier() {
let worker_id = EmbeddedWorkerId::from_legacy_u64(1).to_string();
assert_eq!(
workspace_runtime_operation("POST", &format!("/v1/workers/{worker_id}/workspace-api")),
"workers:create"
);
assert_eq!(
workspace_runtime_operation("GET", &format!("/v1/workers/{worker_id}/protocol/ws")),
"workers:protocol"
);
}
#[test]
fn remote_runtime_registry_routes_commands_without_browser_secret_leaks() {
let worker_id = EmbeddedWorkerId::from_legacy_u64(1).to_string();
+300 -165
View File
@@ -2123,23 +2123,12 @@ impl WorkspaceApi {
))
})?;
let runtime_binding_store = store.clone();
let configured_runtime_endpoints = config
.remote_runtime_sources
.iter()
.filter_map(|source| {
(source.workspace_id.as_deref() == Some(config.workspace_id.as_str()))
.then(|| (source.runtime_id.clone(), source.base_url.clone()))
})
.collect::<HashMap<_, _>>();
let expected_runtime_bindings = store
.list_workspace_runtime_bindings(&config.workspace_id, false)
.await?
.into_iter()
.filter(|binding| binding.runtime_id != EMBEDDED_RUNTIME_ID)
.filter(|binding| binding.state == StoredRuntimeBindingState::Verified)
.filter(|binding| {
configured_runtime_endpoints.get(&binding.runtime_id) == Some(&binding.base_url)
})
.filter(|binding| binding.state != StoredRuntimeBindingState::Revoked)
.map(|binding| {
(
(binding.workspace_id.clone(), binding.runtime_id.clone()),
@@ -2171,19 +2160,34 @@ impl WorkspaceApi {
.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);
{
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);
for binding in api
.store
.list_workspace_runtime_bindings(&api.config.workspace_id, false)
.await?
.into_iter()
.filter(|binding| {
binding.authentication_mode
== crate::store::WorkspaceRuntimeAuthenticationMode::WorkspaceIdentity
})
.filter(|binding| binding.state != StoredRuntimeBindingState::Revoked)
{
let activate = binding.state == StoredRuntimeBindingState::Verified;
api.register_workspace_runtime_binding(binding, activate)?;
}
Ok(api)
}
@@ -2342,6 +2346,37 @@ impl WorkspaceApi {
self.config.workspace_id.as_str()
}
fn register_workspace_runtime_binding(
&self,
binding: WorkspaceRuntimeBinding,
activate: bool,
) -> Result<()> {
let backend_url = self
.config
.backend_base_url
.clone()
.unwrap_or_else(|| "http://127.0.0.1:8787".to_string());
let mut remote_config = remote_runtime_config_from_binding(&binding)
.map_err(|diagnostic| Error::Store(diagnostic.message))?;
remote_config.workspace_authorization = Some(WorkspaceRuntimeAuthorization::new(
self.store.clone(),
self.signing_identities.clone(),
backend_url.clone(),
activate.then_some(binding),
));
let remote_runtime = RemoteWorkerRuntime::new(
remote_config.clone(),
self.config.workspace_id.clone(),
backend_url,
)
.map(|runtime| runtime.with_resource_broker(self.resource_broker.clone()))
.map_err(|error| error.into_error())?;
self.runtime.register_or_replace(remote_runtime);
self.runtime_subscription_broker
.register_remote_runtime(remote_config);
Ok(())
}
pub fn runtime_subscription_broker(&self) -> &RuntimeSubscriptionBroker {
&self.runtime_subscription_broker
}
@@ -13629,16 +13664,20 @@ async fn create_remote_runtime(
updated_at: now,
revoked_at: None,
};
let (mutation, _) = api
let (mutation, binding) = api
.store
.put_workspace_runtime_binding_key(record, request.expected_revision, &actor.account_id)
.await?;
api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.remove(&(api.config.workspace_id.clone(), runtime_id.clone()));
.insert(
(api.config.workspace_id.clone(), runtime_id.clone()),
binding.clone(),
);
api.runtime_subscription_broker
.unregister_runtime(&runtime_id);
api.register_workspace_runtime_binding(binding, false)?;
let resource = workspace_runtime_resources_response(&api, &api.config.workspace_id)
.await?
.items
@@ -13778,122 +13817,138 @@ async fn perform_workspace_runtime_verification(
.await
.map_err(|error| error.to_string())?;
let challenge_body = serde_json::to_vec(&challenge).map_err(|error| error.to_string())?;
let challenge_claims = WorkspaceCapabilityClaims {
issuer: backend_url.to_string(),
issuer_workspace_id: binding.workspace_id.clone(),
issuer_key_id: identity.key_id.clone(),
issuer_identity_revision: identity.revision,
trust_generation: workspace_trust_generation,
binding_revision: binding.binding_revision,
runtime_id: binding.runtime_id.clone(),
worker_id: None,
operation: WORKSPACE_VERIFICATION_OPERATION.to_string(),
method: "POST".to_string(),
path_and_query: WORKSPACE_VERIFICATION_CHALLENGE_PATH.to_string(),
body_digest: workspace_request_body_digest(&challenge_body),
iat: now.timestamp(),
exp: expires_at,
jti: Uuid::now_v7().to_string(),
};
let challenge_token = api
.signing_identities
.issue_workspace_capability(&binding.workspace_id, &challenge_claims)
.map_err(|error| error.to_string())?;
let challenge_runtime = runtime.clone();
let challenge_runtime_id = binding.runtime_id.clone();
let challenge_request = challenge.clone();
let response = tokio::task::spawn_blocking(move || {
challenge_runtime.send_workspace_verification_challenge(
&challenge_runtime_id,
&challenge_request,
&challenge_token,
)
})
.await
.map_err(|_| "Runtime verification challenge task failed".to_string())?
.map_err(|failure| failure.diagnostic.message)?;
verify_runtime_verification_response(
&response,
&challenge,
&binding.public_key,
Utc::now().timestamp(),
)
.map_err(|error| error.to_string())?;
let response_bytes = serde_json::to_vec(&response).map_err(|error| error.to_string())?;
let acknowledgement = WorkspaceRuntimeVerificationAcknowledgement {
challenge_id: response.challenge_id.clone(),
workspace_id: response.workspace_id.clone(),
runtime_id: response.runtime_id.clone(),
binding_revision: response.binding_revision,
workspace_key_id: response.workspace_key_id.clone(),
workspace_identity_revision: response.workspace_identity_revision,
workspace_trust_generation: response.workspace_trust_generation,
runtime_public_key_fingerprint: response.runtime_public_key_fingerprint.clone(),
runtime_identity_revision: response.runtime_identity_revision,
workspace_nonce: response.workspace_nonce.clone(),
runtime_nonce: response.runtime_nonce.clone(),
response_digest: workspace_request_body_digest(&response_bytes),
response: response.clone(),
expires_at: response.expires_at,
};
let acknowledgement_body =
serde_json::to_vec(&acknowledgement).map_err(|error| error.to_string())?;
let acknowledgement_claims = WorkspaceCapabilityClaims {
issuer: backend_url.to_string(),
issuer_workspace_id: binding.workspace_id.clone(),
issuer_key_id: identity.key_id.clone(),
issuer_identity_revision: identity.revision,
trust_generation: workspace_trust_generation,
binding_revision: binding.binding_revision,
runtime_id: binding.runtime_id.clone(),
worker_id: None,
operation: WORKSPACE_VERIFICATION_OPERATION.to_string(),
method: "POST".to_string(),
path_and_query: WORKSPACE_VERIFICATION_ACK_PATH.to_string(),
body_digest: workspace_request_body_digest(&acknowledgement_body),
iat: Utc::now().timestamp(),
exp: expires_at,
jti: Uuid::now_v7().to_string(),
};
let acknowledgement_token = api
.signing_identities
.issue_workspace_capability(&binding.workspace_id, &acknowledgement_claims)
.map_err(|error| error.to_string())?;
let acknowledgement_runtime = runtime;
let acknowledgement_runtime_id = binding.runtime_id.clone();
let acknowledgement_request = acknowledgement.clone();
let receipt = tokio::task::spawn_blocking(move || {
acknowledgement_runtime.send_workspace_verification_acknowledgement(
&acknowledgement_runtime_id,
&acknowledgement_request,
&acknowledgement_token,
)
})
.await
.map_err(|_| "Runtime verification acknowledgement task failed".to_string())?
.map_err(|failure| failure.diagnostic.message)?;
if receipt.challenge_id != challenge.challenge_id
|| receipt.workspace_id != binding.workspace_id
|| receipt.runtime_id != binding.runtime_id
|| receipt.binding_revision != binding.binding_revision
{
return Err("Runtime verification acknowledgement receipt mismatched".to_string());
}
let verified_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let verified = crate::store::WorkspaceRuntimeVerificationEvidence {
state: "verified".to_string(),
last_outcome: "verified".to_string(),
verified_at: Some(verified_at.clone()),
checked_at: verified_at,
..pending
};
api.store
.complete_workspace_runtime_verification(&verified)
let result = async {
let challenge_body = serde_json::to_vec(&challenge).map_err(|error| error.to_string())?;
let challenge_claims = WorkspaceCapabilityClaims {
issuer: backend_url.to_string(),
issuer_workspace_id: binding.workspace_id.clone(),
issuer_key_id: identity.key_id.clone(),
issuer_identity_revision: identity.revision,
trust_generation: workspace_trust_generation,
binding_revision: binding.binding_revision,
runtime_id: binding.runtime_id.clone(),
worker_id: None,
operation: WORKSPACE_VERIFICATION_OPERATION.to_string(),
method: "POST".to_string(),
path_and_query: WORKSPACE_VERIFICATION_CHALLENGE_PATH.to_string(),
body_digest: workspace_request_body_digest(&challenge_body),
iat: now.timestamp(),
exp: expires_at,
jti: Uuid::now_v7().to_string(),
};
let challenge_token = api
.signing_identities
.issue_workspace_capability(&binding.workspace_id, &challenge_claims)
.map_err(|error| error.to_string())?;
let challenge_runtime = runtime.clone();
let challenge_runtime_id = binding.runtime_id.clone();
let challenge_request = challenge.clone();
let response = tokio::task::spawn_blocking(move || {
challenge_runtime.send_workspace_verification_challenge(
&challenge_runtime_id,
&challenge_request,
&challenge_token,
)
})
.await
.map_err(|error| error.to_string())
.map_err(|_| "Runtime verification challenge task failed".to_string())?
.map_err(|failure| failure.diagnostic.message)?;
verify_runtime_verification_response(
&response,
&challenge,
&binding.public_key,
Utc::now().timestamp(),
)
.map_err(|error| error.to_string())?;
let response_bytes = serde_json::to_vec(&response).map_err(|error| error.to_string())?;
let acknowledgement = WorkspaceRuntimeVerificationAcknowledgement {
challenge_id: response.challenge_id.clone(),
workspace_id: response.workspace_id.clone(),
runtime_id: response.runtime_id.clone(),
binding_revision: response.binding_revision,
workspace_key_id: response.workspace_key_id.clone(),
workspace_identity_revision: response.workspace_identity_revision,
workspace_trust_generation: response.workspace_trust_generation,
runtime_public_key_fingerprint: response.runtime_public_key_fingerprint.clone(),
runtime_identity_revision: response.runtime_identity_revision,
workspace_nonce: response.workspace_nonce.clone(),
runtime_nonce: response.runtime_nonce.clone(),
response_digest: workspace_request_body_digest(&response_bytes),
response: response.clone(),
expires_at: response.expires_at,
};
let acknowledgement_body =
serde_json::to_vec(&acknowledgement).map_err(|error| error.to_string())?;
let acknowledgement_claims = WorkspaceCapabilityClaims {
issuer: backend_url.to_string(),
issuer_workspace_id: binding.workspace_id.clone(),
issuer_key_id: identity.key_id.clone(),
issuer_identity_revision: identity.revision,
trust_generation: workspace_trust_generation,
binding_revision: binding.binding_revision,
runtime_id: binding.runtime_id.clone(),
worker_id: None,
operation: WORKSPACE_VERIFICATION_OPERATION.to_string(),
method: "POST".to_string(),
path_and_query: WORKSPACE_VERIFICATION_ACK_PATH.to_string(),
body_digest: workspace_request_body_digest(&acknowledgement_body),
iat: Utc::now().timestamp(),
exp: expires_at,
jti: Uuid::now_v7().to_string(),
};
let acknowledgement_token = api
.signing_identities
.issue_workspace_capability(&binding.workspace_id, &acknowledgement_claims)
.map_err(|error| error.to_string())?;
let acknowledgement_runtime = runtime;
let acknowledgement_runtime_id = binding.runtime_id.clone();
let acknowledgement_request = acknowledgement.clone();
let receipt = tokio::task::spawn_blocking(move || {
acknowledgement_runtime.send_workspace_verification_acknowledgement(
&acknowledgement_runtime_id,
&acknowledgement_request,
&acknowledgement_token,
)
})
.await
.map_err(|_| "Runtime verification acknowledgement task failed".to_string())?
.map_err(|failure| failure.diagnostic.message)?;
if receipt.challenge_id != challenge.challenge_id
|| receipt.workspace_id != binding.workspace_id
|| receipt.runtime_id != binding.runtime_id
|| receipt.binding_revision != binding.binding_revision
{
return Err("Runtime verification acknowledgement receipt mismatched".to_string());
}
let verified_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let verified = crate::store::WorkspaceRuntimeVerificationEvidence {
state: "verified".to_string(),
last_outcome: "verified".to_string(),
verified_at: Some(verified_at.clone()),
checked_at: verified_at,
..pending.clone()
};
api.store
.complete_workspace_runtime_verification(&verified)
.await
.map_err(|error| error.to_string())
}
.await;
if result.is_err() {
let checked_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
let _ = api
.store
.record_workspace_runtime_verification_outcome_if_current(
&pending,
"failed",
"verification_failed",
&checked_at,
)
.await;
}
result
}
async fn test_runtime_connection(
@@ -13918,6 +13973,7 @@ async fn test_runtime_connection(
if binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity {
match perform_workspace_runtime_verification(&api, api.runtime.clone(), &binding).await {
Ok(verified_binding) => {
api.register_workspace_runtime_binding(verified_binding.clone(), true)?;
api.runtime_binding_expectations
.write()
.unwrap_or_else(std::sync::PoisonError::into_inner)
@@ -13933,22 +13989,6 @@ async fn test_runtime_connection(
.map_err(|error| Error::Store(format!("{error:?}")))?;
}
Err(message) => {
if let Ok(Some(mut evidence)) = api
.store
.get_workspace_runtime_verification(api.workspace_id(), &runtime_id)
.await
{
if evidence.state != "verified" {
evidence.state = "failed".to_string();
evidence.verified_at = None;
}
evidence.last_outcome = "verification_failed".to_string();
evidence.checked_at = Utc::now().to_rfc3339();
let _ = api
.store
.record_workspace_runtime_verification_attempt(&evidence)
.await;
}
let mut result = runtime_connection_test_failure(
api.workspace_id(),
&runtime_id,
@@ -13981,15 +14021,19 @@ async fn test_runtime_connection(
if ping.is_err()
&& binding.authentication_mode == StoredRuntimeAuthenticationMode::WorkspaceIdentity
&& let Some(mut evidence) = api
&& let Some(evidence) = api
.store
.get_workspace_runtime_verification(api.workspace_id(), &runtime_id)
.await?
{
evidence.last_outcome = "connectivity_failed".to_string();
evidence.checked_at = Utc::now().to_rfc3339();
let checked_at = Utc::now().to_rfc3339();
api.store
.record_workspace_runtime_verification_attempt(&evidence)
.record_workspace_runtime_verification_outcome_if_current(
&evidence,
"failed",
"connectivity_failed",
&checked_at,
)
.await?;
}
let current_binding = api
@@ -16505,7 +16549,6 @@ fn validate_public_runtime_id(runtime_id: &str) -> ApiResult<()> {
Ok(())
}
#[cfg(test)]
fn remote_runtime_config_from_binding(
binding: &crate::store::WorkspaceRuntimeBinding,
) -> std::result::Result<RemoteRuntimeConfig, RuntimeDiagnostic> {
@@ -20349,6 +20392,98 @@ mod tests {
assert!(!serialized.contains("materialized_path"));
}
#[tokio::test]
async fn verified_workspace_runtime_binding_is_restored_into_live_registry() {
let dir = tempfile::tempdir().unwrap();
let root = dir.path().to_path_buf();
let api = test_api(&root).await;
let actor = test_owner_actor();
let identity = api
.signing_identities
.provision_existing(&api.config.workspace_id, &actor.account_id)
.unwrap();
let runtime_identity = RuntimeIdentityMaterial::generate("restored-runtime").unwrap();
api.store
.upsert_workspace_runtime_binding_record(
WorkspaceRuntimeBinding {
workspace_id: api.config.workspace_id.clone(),
runtime_id: "restored-runtime".to_string(),
display_name: "Restored Runtime".to_string(),
base_url: "https://8.8.8.8".to_string(),
public_key: runtime_identity.public_key,
public_key_fingerprint: String::new(),
binding_revision: 1,
state: StoredRuntimeBindingState::Configured,
authentication_mode: StoredRuntimeAuthenticationMode::WorkspaceIdentity,
workspace_key_id: Some(identity.key_id.clone()),
workspace_key_generation: Some(identity.revision),
created_at: "1".to_string(),
updated_at: "1".to_string(),
revoked_at: None,
},
false,
)
.await
.unwrap();
let configured = api
.store
.get_workspace_runtime_binding(&api.config.workspace_id, "restored-runtime")
.await
.unwrap()
.unwrap();
let evidence = crate::store::WorkspaceRuntimeVerificationEvidence {
workspace_id: configured.workspace_id.clone(),
runtime_id: configured.runtime_id.clone(),
binding_revision: configured.binding_revision,
workspace_key_id: identity.key_id,
workspace_identity_revision: identity.revision,
workspace_trust_generation: identity.revision,
runtime_public_key_fingerprint: configured.public_key_fingerprint.clone(),
runtime_identity_revision: 1,
challenge_id: "restart-challenge".to_string(),
state: "verified".to_string(),
last_outcome: "verified".to_string(),
verified_at: Some("2".to_string()),
checked_at: "2".to_string(),
};
api.store
.record_workspace_runtime_verification_attempt(&evidence)
.await
.unwrap();
api.store
.complete_workspace_runtime_verification(&evidence)
.await
.unwrap();
drop(api);
let restored_config = test_server_config(&root);
let restored_store =
SqliteWorkspaceStore::open(restored_config.database_path.clone()).unwrap();
let restored = WorkspaceApi::new(restored_config, Arc::new(restored_store))
.await
.unwrap();
assert!(
restored
.runtime
.list_runtimes(100)
.items
.iter()
.any(|runtime| runtime.runtime_id == "restored-runtime"),
"verified persisted Runtime binding must return to the live registry"
);
assert!(
restored
.runtime_binding_expectations
.read()
.unwrap()
.contains_key(&(
restored.config.workspace_id.clone(),
"restored-runtime".to_string(),
)),
"restored Runtime must retain its revision-fenced binding expectation"
);
}
#[tokio::test]
async fn remote_runtime_registration_is_workspace_scoped_revisioned_and_configured() {
let dir = tempfile::tempdir().unwrap();
@@ -20416,18 +20551,18 @@ mod tests {
.list_runtimes(100)
.items
.iter()
.all(|runtime| runtime.runtime_id != "configured-runtime"),
"configured binding must remove a stale active Runtime projection"
.any(|runtime| runtime.runtime_id == "configured-runtime"),
"configured binding must remain registered so verification can reach it"
);
assert!(
!api.runtime_binding_expectations
api.runtime_binding_expectations
.read()
.unwrap()
.contains_key(&(
api.config.workspace_id.clone(),
"configured-runtime".to_string(),
)),
"configured binding must not remain a control expectation"
"configured binding must remain fenced by its current binding revision"
);
let replacement_identity = RuntimeIdentityMaterial::generate("configured-runtime").unwrap();
let generic_put = scoped_put_runtime_trust_key(
+149 -1
View File
@@ -771,6 +771,13 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
&self,
evidence: &WorkspaceRuntimeVerificationEvidence,
) -> Result<()>;
async fn record_workspace_runtime_verification_outcome_if_current(
&self,
expected: &WorkspaceRuntimeVerificationEvidence,
state: &str,
outcome: &str,
checked_at: &str,
) -> Result<bool>;
async fn complete_workspace_runtime_verification(
&self,
evidence: &WorkspaceRuntimeVerificationEvidence,
@@ -2336,6 +2343,65 @@ impl SqliteWorkspaceStore {
})
}
pub fn record_workspace_runtime_verification_outcome_if_current(
&self,
expected: &WorkspaceRuntimeVerificationEvidence,
state: &str,
outcome: &str,
checked_at: &str,
) -> Result<bool> {
validate_workspace_runtime_verification(expected)?;
if !matches!(state, "pending" | "verified" | "failed") {
return Err(Error::Store(
"invalid Workspace Runtime verification state".to_string(),
));
}
if !matches!(
outcome,
"challenge_issued" | "verified" | "verification_failed" | "connectivity_failed"
) {
return Err(Error::Store(
"invalid Workspace Runtime verification outcome".to_string(),
));
}
if checked_at.is_empty() || checked_at.len() > 128 {
return Err(Error::Store(
"invalid Workspace Runtime verification checked_at".to_string(),
));
}
self.with_conn(|conn| {
let changed = conn.execute(
r#"UPDATE workspace_runtime_verifications
SET state = CASE WHEN state = 'verified' THEN state ELSE ?10 END,
last_outcome = ?11,
checked_at = ?12
WHERE workspace_id = ?1 AND runtime_id = ?2
AND binding_revision = ?3
AND workspace_key_id = ?4
AND workspace_identity_revision = ?5
AND workspace_trust_generation = ?6
AND runtime_public_key_fingerprint = ?7
AND runtime_identity_revision = ?8
AND challenge_id = ?9"#,
params![
expected.workspace_id,
expected.runtime_id,
expected.binding_revision,
expected.workspace_key_id,
expected.workspace_identity_revision,
expected.workspace_trust_generation,
expected.runtime_public_key_fingerprint,
expected.runtime_identity_revision,
expected.challenge_id,
state,
outcome,
checked_at,
],
)?;
Ok(changed == 1)
})
}
pub fn complete_workspace_runtime_verification(
&self,
evidence: &WorkspaceRuntimeVerificationEvidence,
@@ -2348,6 +2414,36 @@ impl SqliteWorkspaceStore {
}
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let current_attempt = tx.query_row(
r#"SELECT EXISTS(
SELECT 1 FROM workspace_runtime_verifications
WHERE workspace_id = ?1 AND runtime_id = ?2
AND binding_revision = ?3
AND workspace_key_id = ?4
AND workspace_identity_revision = ?5
AND workspace_trust_generation = ?6
AND runtime_public_key_fingerprint = ?7
AND runtime_identity_revision = ?8
AND challenge_id = ?9
)"#,
params![
evidence.workspace_id,
evidence.runtime_id,
evidence.binding_revision,
evidence.workspace_key_id,
evidence.workspace_identity_revision,
evidence.workspace_trust_generation,
evidence.runtime_public_key_fingerprint,
evidence.runtime_identity_revision,
evidence.challenge_id,
],
|row| row.get::<_, bool>(0),
)?;
if !current_attempt {
return Err(Error::RuntimeBindingConflict(
"Runtime verification attempt was superseded".to_string(),
));
}
let changed = tx.execute(
r#"UPDATE workspace_runtime_bindings
SET state = 'verified', updated_at = ?7
@@ -3193,6 +3289,18 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
SqliteWorkspaceStore::record_workspace_runtime_verification_attempt(self, evidence)
}
async fn record_workspace_runtime_verification_outcome_if_current(
&self,
expected: &WorkspaceRuntimeVerificationEvidence,
state: &str,
outcome: &str,
checked_at: &str,
) -> Result<bool> {
SqliteWorkspaceStore::record_workspace_runtime_verification_outcome_if_current(
self, expected, state, outcome, checked_at,
)
}
async fn complete_workspace_runtime_verification(
&self,
evidence: &WorkspaceRuntimeVerificationEvidence,
@@ -9857,6 +9965,9 @@ mod tests {
verified_at: Some("2".to_string()),
checked_at: "2".to_string(),
};
store
.record_workspace_runtime_verification_attempt(&evidence)
.unwrap();
let verified = store
.complete_workspace_runtime_verification(&evidence)
.unwrap();
@@ -9886,6 +9997,43 @@ mod tests {
store
.complete_workspace_runtime_verification(&evidence)
.unwrap();
let newer_pending = WorkspaceRuntimeVerificationEvidence {
challenge_id: "challenge-b".to_string(),
state: "pending".to_string(),
last_outcome: "challenge_issued".to_string(),
verified_at: None,
checked_at: "4".to_string(),
..evidence.clone()
};
store
.record_workspace_runtime_verification_attempt(&newer_pending)
.unwrap();
let newer_verified = WorkspaceRuntimeVerificationEvidence {
state: "verified".to_string(),
last_outcome: "verified".to_string(),
verified_at: Some("5".to_string()),
checked_at: "5".to_string(),
..newer_pending
};
store
.complete_workspace_runtime_verification(&newer_verified)
.unwrap();
assert!(
!store
.record_workspace_runtime_verification_outcome_if_current(
&pending_retry,
"failed",
"verification_failed",
"6",
)
.unwrap()
);
assert_eq!(
store
.get_workspace_runtime_verification("workspace-a", "runtime-a")
.unwrap(),
Some(newer_verified.clone())
);
drop(store);
let reopened = SqliteWorkspaceStore::open(&path).unwrap();
@@ -9893,7 +10041,7 @@ mod tests {
reopened
.get_workspace_runtime_verification("workspace-a", "runtime-a")
.unwrap(),
Some(evidence)
Some(newer_verified)
);
assert_eq!(
reopened