feat: enforce arbitrary worker singleton ownership
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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": [],
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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
@@ -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; };
|
||||
|
||||
|
||||
Reference in New Issue
Block a user