feat: add Runtime Workspace issuer trust

This commit is contained in:
2026-09-08 01:54:10 +09:00
parent 3baf0b6358
commit e035df9e7b
4 changed files with 1410 additions and 24 deletions
+1
View File
@@ -28,6 +28,7 @@ mod runtime;
pub mod worker_backend; pub mod worker_backend;
pub mod worker_source; pub mod worker_source;
pub mod working_directory; pub mod working_directory;
pub mod workspace_issuer;
#[cfg(feature = "fs-store")] #[cfg(feature = "fs-store")]
pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions}; pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions};
+328 -23
View File
@@ -12,6 +12,7 @@ use std::net::SocketAddr;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::Arc; use std::sync::Arc;
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use worker_runtime::auth::{ use worker_runtime::auth::{
@@ -24,6 +25,11 @@ use worker_runtime::http_server::{
}; };
use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend}; use worker_runtime::worker_backend::{ProfileRuntimeWorkerFactory, WorkerRuntimeExecutionBackend};
use worker_runtime::working_directory::RuntimeGitCacheMaterializer; use worker_runtime::working_directory::RuntimeGitCacheMaterializer;
use worker_runtime::workspace_issuer::{
WorkspaceIssuerTrustMutation, WorkspaceIssuerTrustRecord, add_workspace_issuer_trust,
replace_workspace_issuer_trust, revoke_workspace_issuer_trust,
validate_workspace_issuer_trust_records,
};
use worker_runtime::{Runtime, RuntimeOptions}; use worker_runtime::{Runtime, RuntimeOptions};
fn main() -> ExitCode { fn main() -> ExitCode {
@@ -72,7 +78,7 @@ fn run() -> Result<(), ProcessError> {
} }
if matches!( if matches!(
args.first().map(String::as_str), args.first().map(String::as_str),
Some("identity" | "trust-server") Some("identity" | "trust-server" | "trust-workspace")
) { ) {
return run_auth_command(args); return run_auth_command(args);
} }
@@ -170,7 +176,7 @@ fn run_migration_command(mut args: Vec<String>) -> Result<(), ProcessError> {
println!( println!(
"{}", "{}",
serde_json::to_string_pretty(&plan) serde_json::to_string_pretty(&plan)
.map_err(|error| ProcessError::Auth(format!("encode migration plan: {error}")))? .map_err(|error| ProcessError::auth(format!("encode migration plan: {error}")))?
); );
Ok(()) Ok(())
} }
@@ -198,12 +204,12 @@ fn build_runtime(config: &ProcessConfig) -> Result<Runtime, ProcessError> {
> = None; > = None;
if let Some(endpoint) = config.backend_resource_endpoint.clone() { if let Some(endpoint) = config.backend_resource_endpoint.clone() {
let identity = runtime_auth.identity.as_ref().ok_or_else(|| { let identity = runtime_auth.identity.as_ref().ok_or_else(|| {
ProcessError::Auth( ProcessError::auth(
"--backend-resource-endpoint requires a configured Runtime identity".to_owned(), "--backend-resource-endpoint requires a configured Runtime identity".to_owned(),
) )
})?; })?;
let [trusted_server] = runtime_auth.trusted_servers.as_slice() else { let [trusted_server] = runtime_auth.trusted_servers.as_slice() else {
return Err(ProcessError::Auth( return Err(ProcessError::auth(
"--backend-resource-endpoint requires exactly one trusted Server identity" "--backend-resource-endpoint requires exactly one trusted Server identity"
.to_owned(), .to_owned(),
)); ));
@@ -468,6 +474,10 @@ impl ProcessError {
fn usage(message: String) -> Self { fn usage(message: String) -> Self {
Self::Usage(message) Self::Usage(message)
} }
fn auth(message: impl Into<String>) -> Self {
Self::Auth(message.into())
}
} }
impl fmt::Display for ProcessError { impl fmt::Display for ProcessError {
@@ -512,6 +522,8 @@ struct RuntimeAuthFile {
identity: Option<RuntimeIdentityMaterial>, identity: Option<RuntimeIdentityMaterial>,
#[serde(default)] #[serde(default)]
trusted_servers: Vec<TrustedServerKey>, trusted_servers: Vec<TrustedServerKey>,
#[serde(default)]
workspace_issuers: Vec<WorkspaceIssuerTrustRecord>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
@@ -527,6 +539,134 @@ fn runtime_public_identity_view(identity: &RuntimeIdentityMaterial) -> RuntimePu
} }
} }
fn run_trust_workspace_command(mut args: VecDeque<String>) -> Result<(), ProcessError> {
let subcommand = args.pop_front().ok_or_else(|| {
ProcessError::usage(
"trust-workspace requires add, list, show, replace, or revoke".to_string(),
)
})?;
match subcommand.as_str() {
"add" | "replace" => {
let bundle_path = take_required_auth_option(&mut args, "--bundle")?;
let config = parse_auth_storage_flags(&mut args)?;
let auth_path = runtime_auth_path(&config);
let mut auth = read_runtime_auth_file(&auth_path)?;
let bundle_bytes = std::fs::read(&bundle_path).map_err(|_| {
ProcessError::auth("Workspace signing public identity bundle is unavailable")
})?;
let bundle = serde_json::from_slice(&bundle_bytes).map_err(|_| {
ProcessError::auth("Workspace signing public identity bundle is invalid")
})?;
let now_unix = unix_now_i64()?;
let (mutation, record) = if subcommand == "add" {
add_workspace_issuer_trust(&mut auth.workspace_issuers, bundle, now_unix)
} else {
replace_workspace_issuer_trust(&mut auth.workspace_issuers, bundle, now_unix)
}
.map_err(|error| ProcessError::auth(error.to_string()))?;
if mutation != WorkspaceIssuerTrustMutation::Unchanged {
write_runtime_auth_file(&auth_path, &auth)?;
}
print_workspace_trust_mutation(mutation, &record);
Ok(())
}
"list" => {
let config = parse_auth_storage_flags(&mut args)?;
let auth_path = runtime_auth_path(&config);
let mut records = read_runtime_auth_file(&auth_path)?.workspace_issuers;
records.sort_by(|left, right| left.workspace_id.cmp(&right.workspace_id));
let output = serde_json::to_string_pretty(&records)
.map_err(|_| ProcessError::auth("Workspace issuer trust output failed"))?;
println!("{output}");
Ok(())
}
"show" => {
let workspace_id = take_required_auth_option(&mut args, "--workspace-id")?;
let config = parse_auth_storage_flags(&mut args)?;
let auth_path = runtime_auth_path(&config);
let auth = read_runtime_auth_file(&auth_path)?;
let record = auth
.workspace_issuers
.iter()
.find(|record| record.workspace_id == workspace_id)
.ok_or_else(|| {
ProcessError::auth(format!(
"Workspace issuer trust is not registered for `{workspace_id}`"
))
})?;
let output = serde_json::to_string_pretty(record)
.map_err(|_| ProcessError::auth("Workspace issuer trust output failed"))?;
println!("{output}");
Ok(())
}
"revoke" => {
let workspace_id = take_required_auth_option(&mut args, "--workspace-id")?;
let config = parse_auth_storage_flags(&mut args)?;
let auth_path = runtime_auth_path(&config);
let mut auth = read_runtime_auth_file(&auth_path)?;
let (mutation, record) = revoke_workspace_issuer_trust(
&mut auth.workspace_issuers,
&workspace_id,
unix_now_i64()?,
)
.map_err(|error| ProcessError::auth(error.to_string()))?;
if mutation != WorkspaceIssuerTrustMutation::Unchanged {
write_runtime_auth_file(&auth_path, &auth)?;
}
print_workspace_trust_mutation(mutation, &record);
Ok(())
}
_ => Err(ProcessError::usage(format!(
"unknown trust-workspace command `{subcommand}`"
))),
}
}
fn take_required_auth_option(
args: &mut VecDeque<String>,
expected: &str,
) -> Result<String, ProcessError> {
let argument = args
.pop_front()
.ok_or_else(|| ProcessError::usage(format!("missing required `{expected}`")))?;
let (flag, inline_value) = split_flag_value(argument)?;
if flag != expected {
return Err(ProcessError::usage(format!(
"expected `{expected}`, found `{flag}`"
)));
}
take_value(&flag, inline_value, args)
}
fn unix_now_i64() -> Result<i64, ProcessError> {
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| ProcessError::auth("system clock is before the Unix epoch"))?
.as_secs();
i64::try_from(now).map_err(|_| ProcessError::auth("system clock is out of range"))
}
fn print_workspace_trust_mutation(
mutation: WorkspaceIssuerTrustMutation,
record: &WorkspaceIssuerTrustRecord,
) {
let action = match mutation {
WorkspaceIssuerTrustMutation::Added => "added",
WorkspaceIssuerTrustMutation::Replaced => "replaced",
WorkspaceIssuerTrustMutation::Revoked => "revoked",
WorkspaceIssuerTrustMutation::Unchanged => "unchanged",
};
println!(
"Workspace issuer trust {action}: workspace={} key={} fingerprint={} identity_revision={} trust_generation={} state={:?}",
record.workspace_id,
record.key_id,
record.public_key_fingerprint,
record.identity_revision,
record.trust_generation,
record.state,
);
}
fn runtime_auth_path(config: &ProcessConfig) -> PathBuf { fn runtime_auth_path(config: &ProcessConfig) -> PathBuf {
config.resolved_fs_paths().runtime_dir.join("auth.toml") config.resolved_fs_paths().runtime_dir.join("auth.toml")
} }
@@ -535,34 +675,50 @@ fn read_runtime_auth_file(path: &Path) -> Result<RuntimeAuthFile, ProcessError>
if !path.exists() { if !path.exists() {
return Ok(RuntimeAuthFile::default()); return Ok(RuntimeAuthFile::default());
} }
let contents = std::fs::read_to_string(path)?; let contents = std::fs::read_to_string(path)
toml::from_str(&contents) .map_err(|_| ProcessError::auth("runtime auth store is unavailable"))?;
.map_err(|error| ProcessError::Auth(format!("failed to parse {}: {error}", path.display()))) let auth: RuntimeAuthFile = toml::from_str(&contents)
.map_err(|_| ProcessError::auth("runtime auth store is corrupt"))?;
validate_workspace_issuer_trust_records(&auth.workspace_issuers)
.map_err(|_| ProcessError::auth("runtime Workspace issuer trust store is corrupt"))?;
Ok(auth)
} }
fn write_runtime_auth_file(path: &Path, auth: &RuntimeAuthFile) -> Result<(), ProcessError> { fn write_runtime_auth_file(path: &Path, auth: &RuntimeAuthFile) -> Result<(), ProcessError> {
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?; std::fs::create_dir_all(parent)?;
} }
let contents = toml::to_string_pretty(auth).map_err(|error| { let contents = toml::to_string_pretty(auth)
ProcessError::Auth(format!("failed to serialize {}: {error}", path.display())) .map_err(|_| ProcessError::auth("runtime auth store serialization failed"))?;
})?;
write_secret_file(path, contents.as_bytes()) write_secret_file(path, contents.as_bytes())
} }
fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), ProcessError> { fn write_secret_file(path: &Path, contents: &[u8]) -> Result<(), ProcessError> {
#[cfg(unix)] use std::io::Write as _;
{
use std::os::unix::fs::OpenOptionsExt; let parent = path
.parent()
.ok_or_else(|| ProcessError::auth("runtime auth store path has no parent"))?;
let temporary_path = parent.join(format!(".runtime-auth-{}.tmp", uuid::Uuid::now_v7()));
let write_result = (|| -> Result<(), ProcessError> {
let mut options = std::fs::OpenOptions::new(); let mut options = std::fs::OpenOptions::new();
options.create(true).write(true).truncate(true).mode(0o600); options.create_new(true).write(true);
std::io::Write::write_all(&mut options.open(path)?, contents)?; #[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt as _;
options.mode(0o600);
}
let mut file = options.open(&temporary_path)?;
file.write_all(contents)?;
file.sync_all()?;
std::fs::rename(&temporary_path, path)?;
std::fs::File::open(parent)?.sync_all()?;
Ok(())
})();
if write_result.is_err() {
let _ = std::fs::remove_file(&temporary_path);
} }
#[cfg(not(unix))] write_result
{
std::fs::write(path, contents)?;
}
Ok(())
} }
fn load_runtime_http_auth( fn load_runtime_http_auth(
@@ -609,6 +765,7 @@ fn run_auth_command(args: Vec<String>) -> Result<(), ProcessError> {
match command.as_str() { match command.as_str() {
"identity" => run_identity_command(args), "identity" => run_identity_command(args),
"trust-server" => run_trust_server_command(args), "trust-server" => run_trust_server_command(args),
"trust-workspace" => run_trust_workspace_command(args),
_ => Err(ProcessError::usage(format!( _ => Err(ProcessError::usage(format!(
"unknown auth command `{command}`" "unknown auth command `{command}`"
))), ))),
@@ -668,7 +825,7 @@ fn run_identity_command(mut args: VecDeque<String>) -> Result<(), ProcessError>
})?; })?;
auth.identity = Some( auth.identity = Some(
RuntimeIdentityMaterial::generate(runtime_id) RuntimeIdentityMaterial::generate(runtime_id)
.map_err(|error| ProcessError::Auth(error.to_string()))?, .map_err(|error| ProcessError::auth(error.to_string()))?,
); );
write_runtime_auth_file(&path, &auth)?; write_runtime_auth_file(&path, &auth)?;
let identity = auth.identity.as_ref().unwrap(); let identity = auth.identity.as_ref().unwrap();
@@ -721,7 +878,7 @@ fn run_identity_command(mut args: VecDeque<String>) -> Result<(), ProcessError>
println!( println!(
"{}", "{}",
serde_json::to_string_pretty(&view) serde_json::to_string_pretty(&view)
.map_err(|error| ProcessError::Auth(error.to_string()))? .map_err(|error| ProcessError::auth(error.to_string()))?
); );
} else { } else {
println!("runtime_id={}", view.identity_id); println!("runtime_id={}", view.identity_id);
@@ -852,7 +1009,7 @@ fn run_trust_server_command(mut args: VecDeque<String>) -> Result<(), ProcessErr
println!( println!(
"{}", "{}",
serde_json::to_string_pretty(&auth.trusted_servers) serde_json::to_string_pretty(&auth.trusted_servers)
.map_err(|error| ProcessError::Auth(error.to_string()))? .map_err(|error| ProcessError::auth(error.to_string()))?
); );
} else { } else {
for server in auth.trusted_servers { for server in auth.trusted_servers {
@@ -944,6 +1101,11 @@ Auth commands:
identity show [--json] [--fs-root PATH] [--fs-runtime-dir PATH] identity show [--json] [--fs-root PATH] [--fs-runtime-dir PATH]
trust-server add --server-id ID --public-key KEY [--display-name NAME] [--replace] [--fs-root PATH] [--fs-runtime-dir PATH] trust-server add --server-id ID --public-key KEY [--display-name NAME] [--replace] [--fs-root PATH] [--fs-runtime-dir PATH]
trust-server list [--json] [--fs-root PATH] [--fs-runtime-dir PATH] trust-server list [--json] [--fs-root PATH] [--fs-runtime-dir PATH]
trust-workspace add --bundle PATH [--fs-root PATH] [--fs-runtime-dir PATH]
trust-workspace list [--fs-root PATH] [--fs-runtime-dir PATH]
trust-workspace show --workspace-id ID [--fs-root PATH] [--fs-runtime-dir PATH]
trust-workspace replace --bundle PATH [--fs-root PATH] [--fs-runtime-dir PATH]
trust-workspace revoke --workspace-id ID [--fs-root PATH] [--fs-runtime-dir PATH]
trust-server revoke --server-id ID [--fs-root PATH] [--fs-runtime-dir PATH]"# trust-server revoke --server-id ID [--fs-root PATH] [--fs-runtime-dir PATH]"#
} }
@@ -1121,6 +1283,149 @@ mod tests {
assert_eq!(std::fs::read(root.join("runtime.json")).unwrap(), before); assert_eq!(std::fs::read(root.join("runtime.json")).unwrap(), before);
} }
#[test]
fn workspace_issuer_trust_cli_persists_across_reload_and_writes_private_mode() {
use sha2::{Digest as _, Sha256};
use workspace_api::WorkspacePublicIdentityBundle;
let temp = tempfile::tempdir().unwrap();
let identity = RuntimeIdentityMaterial::generate("WK-1").unwrap();
let public_key = decode_public_key(&identity.public_key).unwrap();
let fingerprint = Sha256::digest(public_key);
let fingerprint = format!(
"sha256:{}",
fingerprint
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
);
let bundle_path = temp.path().join("workspace-public.json");
std::fs::write(
&bundle_path,
serde_json::to_vec(&WorkspacePublicIdentityBundle {
workspace_id: "workspace-1".to_string(),
backend_url: "https://backend.example.test".to_string(),
key_id: "WK-1".to_string(),
algorithm: "ed25519".to_string(),
public_key: identity.public_key,
public_key_fingerprint: fingerprint,
revision: 1,
})
.unwrap(),
)
.unwrap();
run_trust_workspace_command(VecDeque::from([
"add".to_string(),
"--bundle".to_string(),
bundle_path.to_string_lossy().into_owned(),
"--fs-root".to_string(),
temp.path().to_string_lossy().into_owned(),
]))
.unwrap();
let config = ProcessConfig {
fs_root: Some(temp.path().to_path_buf()),
..ProcessConfig::default().unwrap()
};
let path = runtime_auth_path(&config);
let first = read_runtime_auth_file(&path).unwrap();
assert_eq!(first.workspace_issuers.len(), 1);
assert_eq!(first.workspace_issuers[0].trust_generation, 1);
run_trust_workspace_command(VecDeque::from([
"add".to_string(),
"--bundle".to_string(),
bundle_path.to_string_lossy().into_owned(),
"--fs-root".to_string(),
temp.path().to_string_lossy().into_owned(),
]))
.unwrap();
let replay = read_runtime_auth_file(&path).unwrap();
assert_eq!(replay.workspace_issuers[0].trust_generation, 1);
let replacement = RuntimeIdentityMaterial::generate("WK-2").unwrap();
let public_key = decode_public_key(&replacement.public_key).unwrap();
let fingerprint = Sha256::digest(public_key);
let fingerprint = format!(
"sha256:{}",
fingerprint
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>()
);
std::fs::write(
&bundle_path,
serde_json::to_vec(&WorkspacePublicIdentityBundle {
workspace_id: "workspace-1".to_string(),
backend_url: "https://backend.example.test".to_string(),
key_id: "WK-2".to_string(),
algorithm: "ed25519".to_string(),
public_key: replacement.public_key,
public_key_fingerprint: fingerprint,
revision: 2,
})
.unwrap(),
)
.unwrap();
for command in ["replace", "show", "list"] {
let mut args = VecDeque::from([command.to_string()]);
match command {
"replace" => {
args.push_back("--bundle".to_string());
args.push_back(bundle_path.to_string_lossy().into_owned());
}
"show" => {
args.push_back("--workspace-id".to_string());
args.push_back("workspace-1".to_string());
}
"list" => {}
_ => unreachable!(),
}
args.push_back("--fs-root".to_string());
args.push_back(temp.path().to_string_lossy().into_owned());
run_trust_workspace_command(args).unwrap();
}
run_trust_workspace_command(VecDeque::from([
"revoke".to_string(),
"--workspace-id".to_string(),
"workspace-1".to_string(),
"--fs-root".to_string(),
temp.path().to_string_lossy().into_owned(),
]))
.unwrap();
let revoked = read_runtime_auth_file(&path).unwrap();
assert_eq!(revoked.workspace_issuers[0].trust_generation, 3);
assert_eq!(
revoked.workspace_issuers[0].state,
worker_runtime::workspace_issuer::WorkspaceIssuerTrustState::Revoked
);
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt as _;
assert_eq!(
std::fs::metadata(path).unwrap().permissions().mode() & 0o777,
0o600
);
}
}
#[test]
fn corrupt_workspace_issuer_trust_fails_closed_without_echoing_store_content() {
let temp = tempfile::tempdir().unwrap();
let path = temp.path().join("runtime-auth.toml");
let private_marker = "private-key-material-must-not-appear";
std::fs::write(
&path,
format!("identity = {{ private_key_pkcs8 = \"{private_marker}\" }}\n[[workspace_issuers]]\nworkspace_id = []\n"),
)
.unwrap();
let error = read_runtime_auth_file(&path).unwrap_err().to_string();
assert_eq!(error, "runtime auth store is corrupt");
assert!(!error.contains(private_marker));
assert!(!error.contains(&path.to_string_lossy().into_owned()));
}
#[test] #[test]
fn no_store_disables_runtime_catalog_persistence() { fn no_store_disables_runtime_catalog_persistence() {
let config = parse_args(["--no-store"]).unwrap().unwrap(); let config = parse_args(["--no-store"]).unwrap().unwrap();
File diff suppressed because it is too large Load Diff
+48 -1
View File
@@ -65,7 +65,7 @@ By default, Runtime auth state is stored at:
<data_dir>/runtime/auth.toml <data_dir>/runtime/auth.toml
``` ```
If the Runtime process is launched with `--fs-root` or `--fs-runtime-dir`, pass the same flags to every `identity` and `trust-server` command. Otherwise the setup command may write an auth file that the server process never reads. If the Runtime process is launched with `--fs-root` or `--fs-runtime-dir`, pass the same flags to every `identity`, `trust-server`, and `trust-workspace` command. Otherwise the setup command may write an auth file that the server process never reads.
Example with explicit Runtime storage: Example with explicit Runtime storage:
@@ -104,6 +104,53 @@ Verify:
yoi-runtime trust-server list --json yoi-runtime trust-server list --json
``` ```
## Workspace issuer trust foundation
Workspace signing identity is Workspace-scoped. Provision the identity through the authenticated owner-only Workspace Settings operation, then export the public bundle from:
```text
GET /api/w/<WORKSPACE_ID>/settings/workspace/signing-identity
```
Save the response's `identity` object—not the outer response wrapper—as `workspace-public-identity.json`, and transfer only that document to the Runtime host:
```bash
yoi-runtime trust-workspace add \
--bundle workspace-public-identity.json \
--fs-root /var/lib/yoi-runtime
```
The bundle contains `workspace_id`, `backend_url`, `key_id`, `algorithm`, `public_key`, `public_key_fingerprint`, and the Workspace signing identity `revision`. It never contains the Workspace private key. Do not transfer the Server-side private-material file or place private material in Runtime configuration.
Inspect the Runtime trust records without exposing private material:
```bash
yoi-runtime trust-workspace list --fs-root /var/lib/yoi-runtime
yoi-runtime trust-workspace show \
--workspace-id '<WORKSPACE_ID>' \
--fs-root /var/lib/yoi-runtime
```
An exact repeated `add` is idempotent. A different bundle for an existing Workspace is rejected; use the explicit `replace` operation after verifying the new public fingerprint out of band:
```bash
yoi-runtime trust-workspace replace \
--bundle workspace-public-identity-v2.json \
--fs-root /var/lib/yoi-runtime
```
Runtime increments a local `trust_generation` on replacement and revocation. Signed claims bind the issuer URL, Workspace signing identity revision, Runtime trust generation, live WorkspaceRuntime `binding_revision`, Runtime/Worker target, operation, request-body SHA-256 digest, expiry, and one-time `jti`. Claims are accepted only when the Workspace identity revision, Runtime trust generation, and caller-supplied current binding revision all match exactly. Revoke trust without deleting its generation fence:
```bash
yoi-runtime trust-workspace revoke \
--workspace-id '<WORKSPACE_ID>' \
--fs-root /var/lib/yoi-runtime
```
For rotation, create and export the new Workspace identity first, verify its fingerprint, replace Runtime trust, update the WorkspaceRuntime binding authority, and only then issue claims under the new key/generation. For emergency revocation, revoke Runtime trust first and stop issuing claims; reactivation requires an explicit `replace` with an active public bundle. Runtime auth state is written atomically with private file permissions and survives restart; malformed trust state fails closed.
This command establishes the Runtime-side trust and claim-verifier foundation only. The Runtime auth store is read during process startup; once signed verification is connected, a controlled Runtime restart will be required before a changed trust record affects verification. Do not restart a live Runtime until its active Worker lifecycle has been handled. Until the signed-verification cutover Ticket is integrated, existing remote control traffic continues to select the legacy trusted-Server verifier explicitly; Runtime never falls back from one verifier mode to the other.
## 4. Register the Runtime public key and endpoint on Server ## 4. Register the Runtime public key and endpoint on Server
On the Workspace Server host, register the Runtime public key copied from `yoi-runtime identity show --json`: On the Workspace Server host, register the Runtime public key copied from `yoi-runtime identity show --json`: