feat: enforce arbitrary worker singleton ownership

This commit is contained in:
2026-10-01 15:27:05 +09:00
parent f742362b03
commit 1afddc148d
20 changed files with 1463 additions and 109 deletions
+1
View File
@@ -878,6 +878,7 @@ mod tests {
let create = BackendCreateWorkerRequest {
runtime_id: "embedded".to_string(),
display_name: "Coder one".to_string(),
singleton_key: None,
profile: Some("builtin:coder".to_string()),
ticket_assignment: None,
initial_submit: Vec::new(),
+1
View File
@@ -431,6 +431,7 @@ impl BackendWorkspaceProductClient {
let request = CreateWorkspaceWorkerRequest {
runtime_id: runtime.runtime_id.clone(),
display_name: format!("intake-{ticket_id}"),
singleton_key: None,
profile: Some("builtin:intake".to_string()),
ticket_assignment: None,
initial_submit: vec![protocol::Segment::Text {
+8
View File
@@ -6859,6 +6859,9 @@ pub struct RuntimeWorkerSpawnRequest {
pub intent: RuntimeWorkerSpawnIntent,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub requested_worker_name: Option<String>,
/// Opaque Workspace-scoped singleton identity. The Backend, not the Runtime, owns this lease.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub singleton_key: Option<String>,
pub acceptance: RuntimeWorkerSpawnAcceptanceRequirement,
pub profile: RuntimeProfileSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -8230,6 +8233,9 @@ pub struct CreateWorkspaceWorkerTicketAssignmentRequest {
pub struct CreateWorkspaceWorkerRequest {
pub runtime_id: String,
pub display_name: String,
/// Opaque Workspace-scoped singleton identity. Possession does not grant Worker access.
#[serde(default)]
pub singleton_key: Option<String>,
#[serde(default)]
pub profile: Option<String>,
#[serde(default)]
@@ -10372,6 +10378,7 @@ mod tests {
let request = serde_json::to_value(CreateWorkspaceWorkerRequest {
runtime_id: "runtime-a".to_string(),
display_name: "Worker A".to_string(),
singleton_key: Some("subjektiv:subject-a".to_string()),
profile: None,
ticket_assignment: None,
initial_submit: Vec::new(),
@@ -10384,6 +10391,7 @@ mod tests {
serde_json::json!({
"runtime_id": "runtime-a",
"display_name": "Worker A",
"singleton_key": "subjektiv:subject-a",
"profile": null,
"ticket_assignment": null,
"initial_submit": [],
+1
View File
@@ -162,6 +162,7 @@ fn request_from_selection(selection: Selection) -> BackendCreateWorkerRequest {
BackendCreateWorkerRequest {
runtime_id: selection.runtime_id,
display_name: selection.display_name,
singleton_key: None,
profile: Some(selection.profile),
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
@@ -474,6 +474,7 @@ pub struct WorkerLifecycleSpawnRequest {
/// Ordered alias-keyed selections forwarded as canonical Workdir attachments.
pub workdir_attachments: Vec<WorkerLifecycleWorkdirAttachment>,
pub profile: String,
pub singleton_key: Option<String>,
pub ticket_id: Option<String>,
pub operation_id: Option<String>,
pub display_name: String,
@@ -508,6 +509,7 @@ fn workspace_worker_create_request(
Ok(CreateWorkspaceWorkerRequest {
runtime_id: request.runtime_id,
display_name: request.display_name,
singleton_key: request.singleton_key,
profile: Some(request.profile),
ticket_assignment,
initial_submit: request.initial_submit,
@@ -716,6 +718,9 @@ struct WorkerSpawnInput {
/// under the stable `workdir` alias.
working_directory_id: String,
profile: String,
/// Optional opaque Workspace singleton key. Prefixes are caller-owned and uninterpreted.
#[serde(default)]
singleton_key: Option<String>,
/// Optional inprogress Ticket already accepted by the Orchestrator. Set
/// this with one Flow segment to assign the new Coder atomically.
#[serde(default)]
@@ -910,6 +915,7 @@ impl Tool for WorkspaceWorkerTool {
.transpose()?,
}],
profile: non_empty(input.profile, "profile")?,
singleton_key: input.singleton_key,
ticket_id,
operation_id,
display_name: input
@@ -1751,6 +1757,7 @@ mod tests {
},
],
profile: "builtin:coder".to_string(),
singleton_key: Some("subjektiv:subject-1".to_string()),
ticket_id: None,
operation_id: None,
display_name: "Coder".to_string(),
@@ -127,6 +127,7 @@ impl Tool for SpawnTicketCoderTool {
runtime_id,
workdir_attachments,
profile: CODER_PROFILE.to_string(),
singleton_key: None,
ticket_id: Some(ticket.id.clone()),
operation_id: Some(format!("spawn-ticket-coder:{}:{call_id}", ticket.id)),
display_name: format!("Coder · {}", ticket.resource_key),
+7
View File
@@ -507,6 +507,9 @@ pub struct WorkerSpawnRequest {
pub intent: WorkerSpawnIntent,
#[serde(skip_serializing_if = "Option::is_none")]
pub requested_worker_name: Option<String>,
/// Opaque Backend-owned singleton key. Runtime providers must not infer semantics from it.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub singleton_key: Option<String>,
pub acceptance: WorkerSpawnAcceptanceRequirement,
pub profile: ProfileSelector,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -6911,6 +6914,7 @@ mod tests {
role: TicketWorkerRole::Coder,
},
requested_worker_name: None,
singleton_key: None,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
@@ -7198,6 +7202,7 @@ mod tests {
role: TicketWorkerRole::Coder,
},
requested_worker_name: Some("friendly-name-is-not-authority".to_string()),
singleton_key: None,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
@@ -7299,6 +7304,7 @@ mod tests {
role: TicketWorkerRole::Coder,
},
requested_worker_name: None,
singleton_key: None,
acceptance: WorkerSpawnAcceptanceRequirement::RunAccepted {
expected_segments: 0,
},
@@ -7340,6 +7346,7 @@ mod tests {
WorkerSpawnRequest {
intent: WorkerSpawnIntent::WorkspaceCompanion,
requested_worker_name: None,
singleton_key: None,
acceptance: WorkerSpawnAcceptanceRequirement::SocketReady,
profile: ProfileSelector::Builtin("builtin:companion".to_string()),
ticket_assignment: None,
@@ -768,8 +768,37 @@ CREATE TABLE worker_create_reservations (
state TEXT NOT NULL CHECK (state IN ('reserved', 'created', 'removed')),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL, request_fingerprint TEXT, memory_settings_revision INTEGER, memory_language TEXT,
singleton_key TEXT,
singleton_generation INTEGER CHECK (singleton_generation IS NULL OR singleton_generation > 0),
PRIMARY KEY (workspace_id, allocation_key),
UNIQUE (workspace_id, worker_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
CHECK ((singleton_key IS NULL) = (singleton_generation IS NULL))
);
CREATE TRIGGER worker_create_reservation_singleton_lease_insert
BEFORE INSERT ON worker_create_reservations
WHEN (NEW.singleton_key IS NULL AND NEW.singleton_generation IS NOT NULL)
OR (NEW.singleton_key IS NOT NULL AND NEW.singleton_generation IS NULL)
BEGIN
SELECT RAISE(ABORT, 'worker_singleton_lease_incomplete');
END;
CREATE TRIGGER worker_create_reservation_singleton_lease_update
BEFORE UPDATE OF singleton_key, singleton_generation ON worker_create_reservations
WHEN (NEW.singleton_key IS NULL AND NEW.singleton_generation IS NOT NULL)
OR (NEW.singleton_key IS NOT NULL AND NEW.singleton_generation IS NULL)
BEGIN
SELECT RAISE(ABORT, 'worker_singleton_lease_incomplete');
END;
CREATE TABLE worker_singleton_owners (
workspace_id TEXT NOT NULL,
singleton_key TEXT NOT NULL,
runtime_id TEXT NOT NULL,
worker_id TEXT NOT NULL,
generation INTEGER NOT NULL CHECK (generation > 0),
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
PRIMARY KEY (workspace_id, singleton_key),
UNIQUE (workspace_id, worker_id),
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
);
CREATE TABLE worker_diagnostics_archives (
+1 -1
View File
@@ -471,7 +471,7 @@ impl SqliteWorkspaceStore {
}
tx.execute("UPDATE worker_workdir_links SET unlinked_at=?4 WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND unlinked_at IS NULL",params![workspace_id,plan.worker.runtime_id,plan.worker.worker_id,now])?;
let deleted=tx.execute("DELETE FROM worker_registry WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND updated_at=?4",params![workspace_id,plan.worker.runtime_id,plan.worker.worker_id,plan.worker_revision])?;
tx.execute("UPDATE worker_create_reservations SET state='removed',updated_at=?4 WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND state='created'",params![workspace_id,plan.worker.runtime_id,plan.worker.worker_id,now])?;
tx.execute("UPDATE worker_create_reservations SET state='removed',updated_at=?4 WHERE workspace_id=?1 AND runtime_id=?2 AND worker_id=?3 AND state IN ('reserved','created')",params![workspace_id,plan.worker.runtime_id,plan.worker.worker_id,now])?;
if deleted!=1{return Err(StoreError::InvalidInput(format!("stale:{}:removal fence changed",plan.plan_id)));}
let catalog=commit_worker_catalog_removal(&tx,workspace_id,&plan.worker)?;
tx.execute("UPDATE worker_removal_operations SET state='succeeded',failure_category=NULL,updated_at=?1 WHERE operation_id=?2",params![now,operation_id])?;
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -108,6 +108,9 @@ pub async fn verify_runtime_request_source_proof_with_store(
if member.is_none() && !reserved {
return Err(WorkerMutationSourceProofError::WorkerCatalogMembership);
}
store
.require_current_worker_singleton_owner(workspace_id, &worker)
.map_err(|error| WorkerMutationSourceProofError::Authority(error.to_string()))?;
}
Ok(VerifiedRuntimeRequestSource {
runtime_id: claims.iss,
@@ -254,6 +257,9 @@ async fn verify_worker_remove_source_with(
if member.is_none() {
return Err(WorkerMutationSourceProofError::WorkerCatalogMembership);
}
store
.require_current_worker_singleton_owner(&config.workspace_id, &worker)
.map_err(|error| WorkerMutationSourceProofError::Authority(error.to_string()))?;
let consumed_at = chrono::Utc::now().to_rfc3339();
let consumed = store
@@ -80,6 +80,7 @@ const WORKSPACE_DELETION_PURGE_TABLES: &[&str] = &[
"worker_removal_operations",
"worker_retention_audit_events",
"worker_session_archives",
"worker_singleton_owners",
"worker_tombstones",
"worker_workdir_attachment_reservations",
"worker_workdir_links",
+16 -1
View File
@@ -1545,6 +1545,14 @@
"runtime_id": {
"type": "string"
},
"singleton_key": {
"default": null,
"description": "Opaque Workspace-scoped singleton identity. Possession does not grant Worker access.",
"type": [
"string",
"null"
]
},
"ticket_assignment": {
"anyOf": [
{
@@ -7690,6 +7698,13 @@
"null"
]
},
"singleton_key": {
"description": "Opaque Workspace-scoped singleton identity. The Backend, not the Runtime, owns this lease.",
"type": [
"string",
"null"
]
},
"ticket_assignment": {
"anyOf": [
{
@@ -13989,7 +14004,7 @@
"info": {
"title": "Yoi Server API",
"version": "0.1.0",
"x-yoi-source-digest": "sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043"
"x-yoi-source-digest": "sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a"
},
"openapi": "3.1.0",
"paths": {
@@ -1,6 +1,6 @@
// @generated by yoi-companion-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
// Options: companion-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/companion-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_companion_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-repository-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
// Options: repository-operation-schema-closure,openapi-3.1,format:uint64=safe-number,strict-safe-integers,sorted-output
// Output: web/workspace/src/lib/generated/repository-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_repository_openapi_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-runtime-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
// Options: runtime-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/runtime-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_runtime_api_types -- --check
@@ -115,7 +115,7 @@ export type RuntimeWorkerSpawnAcceptanceRequirement = { kind: "socket_ready"; }
export type RuntimeWorkerSpawnIntent = { kind: "workspace_companion"; } | { kind: "workspace_orchestrator"; } | { kind: "workspace_coding"; } | { kind: "ticket_role"; role: RuntimeTicketWorkerRole; ticket_id: string; };
export type RuntimeWorkerSpawnRequest = { acceptance: RuntimeWorkerSpawnAcceptanceRequirement; initial_submit?: Array<unknown>; intent: RuntimeWorkerSpawnIntent; profile: RuntimeProfileSelector; requested_worker_name?: string | null; ticket_assignment?: RuntimeWorkerTicketAssignmentRequest | null; workdir_attachment_requests?: Array<RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest>; };
export type RuntimeWorkerSpawnRequest = { acceptance: RuntimeWorkerSpawnAcceptanceRequirement; initial_submit?: Array<unknown>; intent: RuntimeWorkerSpawnIntent; profile: RuntimeProfileSelector; requested_worker_name?: string | null; singleton_key?: string | null; ticket_assignment?: RuntimeWorkerTicketAssignmentRequest | null; workdir_attachment_requests?: Array<RuntimeWorkerSpawnWorkingDirectoryAttachmentRequest>; };
export type RuntimeWorkerSpawnResponse = { acceptance_evidence?: Array<RuntimeWorkerSpawnAcceptanceEvidence>; diagnostics?: Array<Diagnostic>; state: WorkerOperationState; worker?: RuntimeWorkerSummary | null; };
@@ -1,6 +1,6 @@
// @generated by yoi-ticket-browser-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
// Options: ticket-objective-merge-request-browser-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/ticket-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_ticket_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-workdir-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:7b06d005d3436758c96fc4cfd462df76460f89620fd1b58fdf05009d96bb9052)
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
// Options: workdir-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/workdir-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_workdir_api_types -- --check
@@ -1,6 +1,6 @@
// @generated by yoi-worker-openapi-typescript@1. Do not edit.
// Input: openapi/server-api.json
// Input contract: 0.1.0 (sha256:ae559492220ec118f04c2c0c9593e8b3cb44c3cb857be3ae85715ace26da8043)
// Input contract: 0.1.0 (sha256:a0956070b29f49aafac3b19a0fa54626f285bdf01337f26afc4389ff6d23495a)
// Options: worker-operation-schema-closure,openapi-3.1,strict-integer-ranges,sorted-output
// Output: web/workspace/src/lib/generated/worker-launch-api.ts
// Regenerate/check: cargo run -q -p server-api --example generate_worker_launch_api_types -- --check
@@ -11,7 +11,7 @@ export type BrowserWorkerWorkingDirectorySelection = { alias: string; relative_c
export type BrowserWorkspaceOrchestratorResponse = { diagnostics: Array<Diagnostic>; disposition: string; online: boolean; worker?: WorkerLaunchWorkerSummary | null; workspace_id: string; };
export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array<Segment>; profile?: string | null; runtime_id: string; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array<BrowserWorkerWorkingDirectorySelection>; };
export type CreateWorkspaceWorkerRequest = { control_operation_id?: string | null; display_name: string; initial_submit?: Array<Segment>; profile?: string | null; runtime_id: string; singleton_key?: string | null; ticket_assignment?: CreateWorkspaceWorkerTicketAssignmentRequest | null; workdir_attachments?: Array<BrowserWorkerWorkingDirectorySelection>; };
export type CreateWorkspaceWorkerTicketAssignmentRequest = { operation_id: string; ticket_id: string; };