fix(wip): page complete workdir catalogs and fence full connection sets

This commit is contained in:
2026-10-05 19:01:13 +09:00
parent 01156f4c78
commit 6feed0f22f
13 changed files with 1187 additions and 57 deletions
+465 -1
View File
@@ -5885,6 +5885,9 @@ fn generated_workspace_contract_router(service: ServerApiContractService) -> Rou
.merge(server_api::server_api_axum::runtime_workdir_cleanup(
service.clone(),
))
.merge(server_api::server_api_axum::current_worker_workdir_catalog(
service.clone(),
))
.merge(server_api::server_api_axum::current_worker_workdir_attachment_list(service.clone()))
.merge(server_api::server_api_axum::current_worker_workdir_attach(
service.clone(),
@@ -10417,6 +10420,28 @@ impl server_api::ServerApi for ServerApiContractService {
.map_err(ApiError::into_repository_api_error)
}
async fn current_worker_workdir_catalog(
&self,
context: server_api::ServerRequestContext,
workspace_id: String,
query: server_api::CurrentWorkerWorkdirCatalogQuery,
) -> std::result::Result<
server_api::CurrentWorkerWorkdirCatalogResponse,
server_api::RepositoryApiError,
> {
let api = self.workspace_api()?;
validate_workspace_scope(api, &workspace_id)
.map_err(ApiError::into_repository_api_error)?;
current_worker_identity(
api,
&workspace_id,
&current_worker_contract_headers(&context)?,
)
.map_err(ApiError::from)
.map_err(ApiError::into_repository_api_error)?;
list_current_worker_workdir_catalog(api, query).map_err(ApiError::into_repository_api_error)
}
async fn current_worker_workdir_attachment_list(
&self,
context: server_api::ServerRequestContext,
@@ -16486,6 +16511,45 @@ async fn scoped_attach_current_worker_workdir(
}))
}
fn list_current_worker_workdir_catalog(
api: &WorkspaceApi,
query: server_api::CurrentWorkerWorkdirCatalogQuery,
) -> ApiResult<server_api::CurrentWorkerWorkdirCatalogResponse> {
// No Runtime inventory refresh: both projection inputs and complete digest are authoritative
// Backend records captured by the store in one read transaction.
let page = api.store.workdir_catalog_page(
&api.config.workspace_id,
query.limit.unwrap_or(50),
query.cursor.as_deref(),
)?;
let items = page
.entries
.into_iter()
.filter_map(|entry| {
if matches!(
entry.record.source,
WorkdirRegistrySource::ExternalGrant { .. }
) && entry.external_grant_permissions.is_none()
{
return None;
}
let mut summary = workdir_summary_from_record(
&entry.record,
entry.repository_key.as_deref(),
entry.external_grant_permissions,
);
summary.occupied_by = entry.occupied_by;
Some(server_api::WorkingDirectorySummary::from(summary))
})
.collect();
Ok(server_api::CurrentWorkerWorkdirCatalogResponse {
workspace_id: api.config.workspace_id.clone(),
items,
next_cursor: page.next_cursor,
revision: page.revision,
})
}
fn list_current_worker_workdir_attachments(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
@@ -16498,7 +16562,7 @@ fn list_current_worker_workdir_attachments(
Error::InvalidInput("attachment page limit must be 1..=100".to_string()).into(),
);
}
let mut links = api.store.list_worker_workdir_links_page(
let (mut links, revision) = api.store.list_worker_workdir_links_page_with_revision(
&api.config.workspace_id,
worker,
limit + 1,
@@ -16526,6 +16590,7 @@ fn list_current_worker_workdir_attachments(
})
.collect(),
next_offset,
revision,
})
}
@@ -50672,6 +50737,260 @@ mod tests {
);
}
#[tokio::test]
async fn current_worker_workdir_catalog_enumerates_complete_stable_inventory() {
let mut fixture = manual_coder_assignment_fixture().await;
let identity =
worker_runtime::auth::RuntimeIdentityMaterial::generate(&fixture.worker.runtime_id)
.unwrap();
configure_runtime_request_auth(&mut fixture.api, &identity, &fixture.worker.runtime_id);
let api = &fixture.api;
let template = api
.store
.list_workdir_registry(TEST_WORKSPACE_ID, 10)
.unwrap()
.remove(0);
let mut expected = api
.store
.list_workdir_registry(TEST_WORKSPACE_ID, 10)
.unwrap()
.into_iter()
.map(|record| record.workdir_id)
.collect::<HashSet<_>>();
let mut oldest = None;
for index in 0..233 {
let legacy = index < 3;
let mut record = WorkdirRegistryRecord {
workdir_id: format!("000-catalog-{index:03}"),
display_name: Some(format!("Catalog {index}")),
// Timestamp order intentionally differs from stable ID order.
updated_at: format!("{index:05}"),
..template.clone()
};
if legacy || index % 2 == 1 {
let grant = ExternalWorkdirGrantRecord {
grant_id: format!("catalog-grant-{index}"),
workspace_id: TEST_WORKSPACE_ID.to_string(),
workdir_id: record.workdir_id.clone(),
provider_instance_id: "private-provider-instance".to_string(),
display_name: record.display_name.clone().unwrap(),
permissions: if legacy { "command_only" } else { "read_write" }.to_string(),
created_by: "test-account".to_string(),
created_at: "1".to_string(),
expires_at: None,
generation: 1,
status: "online".to_string(),
updated_at: "1".to_string(),
};
record.source = WorkdirRegistrySource::ExternalGrant {
grant_id: grant.grant_id.clone(),
};
api.store
.create_external_workdir_grant(&grant, &record)
.unwrap();
} else {
api.store.upsert_workdir_registry(&record).unwrap();
}
if !legacy {
expected.insert(record.workdir_id.clone());
if oldest.is_none() {
oldest = Some(record.clone());
}
}
}
let base = format!("/api/w/{TEST_WORKSPACE_ID}/workers/self/workdir-catalog");
let request = |path: &str| {
runtime_source_request(
&identity,
Some(&fixture.worker.worker_id),
"GET",
path,
Vec::new(),
)
};
let response = build_router(api.clone())
.oneshot(request(&format!("{base}?limit=3")))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let empty: server_api::CurrentWorkerWorkdirCatalogResponse =
serde_json::from_slice(&to_bytes(response.into_body(), usize::MAX).await.unwrap())
.unwrap();
assert!(empty.items.is_empty());
assert_eq!(empty.next_cursor.as_deref(), Some("000-catalog-002"));
let mut cursor = None;
let mut seen = Vec::new();
let mut pages = 0;
loop {
let path = match &cursor {
Some(cursor) => format!("{base}?limit=37&cursor={cursor}"),
None => format!("{base}?limit=37"),
};
let response = build_router(api.clone())
.oneshot(request(&path))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let bytes = to_bytes(response.into_body(), usize::MAX).await.unwrap();
assert!(!String::from_utf8_lossy(&bytes).contains("private-provider-instance"));
let page: server_api::CurrentWorkerWorkdirCatalogResponse =
serde_json::from_slice(&bytes).unwrap();
assert!(page.items.len() <= 37);
assert_eq!(page.revision, empty.revision);
seen.extend(page.items.into_iter().map(|item| item.working_directory_id));
pages += 1;
if let Some(next) = page.next_cursor {
assert!(cursor.as_ref().is_none_or(|previous| previous < &next));
cursor = Some(next);
} else {
break;
}
assert!(pages < 20);
}
assert!(seen.windows(2).all(|pair| pair[0] < pair[1]));
assert_eq!(seen.into_iter().collect::<HashSet<_>>(), expected);
assert!(pages > 6);
let oldest = oldest.unwrap();
// Existing direct lookup agrees for the oldest visible External Workdir.
let Json(detail) = scoped_working_directory_detail(
State(api.clone()),
AxumPath(ScopedWorkingDirectoryPath {
workspace_id: TEST_WORKSPACE_ID.to_string(),
working_directory_id: oldest.workdir_id.clone(),
}),
)
.await
.unwrap();
let oldest_page = list_current_worker_workdir_catalog(
api,
server_api::CurrentWorkerWorkdirCatalogQuery {
limit: Some(1),
cursor: Some("000-catalog-002".to_string()),
},
)
.unwrap();
assert_eq!(oldest_page.items, vec![detail.item]);
assert!(
!api.store
.list_workdir_registry(TEST_WORKSPACE_ID, 200)
.unwrap()
.iter()
.any(|record| record.workdir_id == oldest.workdir_id)
);
for query in ["limit=0", "limit=101", "limit=-1", "cursor=", "cursor=%0A"] {
assert_eq!(
build_router(api.clone())
.oneshot(request(&format!("{base}?{query}")))
.await
.unwrap()
.status(),
StatusCode::BAD_REQUEST
);
}
let default = list_current_worker_workdir_catalog(api, Default::default()).unwrap();
assert_eq!(default.items.len(), 47);
let before = list_current_worker_workdir_catalog(
api,
server_api::CurrentWorkerWorkdirCatalogQuery {
limit: Some(3),
cursor: None,
},
)
.unwrap();
let mut last = api
.store
.get_workdir_registry(TEST_WORKSPACE_ID, "000-catalog-232")
.unwrap()
.unwrap();
last.display_name = Some("Updated outside the first page".to_string());
api.store.upsert_workdir_registry(&last).unwrap();
let changed = list_current_worker_workdir_catalog(
api,
server_api::CurrentWorkerWorkdirCatalogQuery {
limit: Some(3),
cursor: None,
},
)
.unwrap();
assert_eq!(before.items, changed.items);
assert_ne!(before.revision, changed.revision);
let template_link = api
.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &fixture.worker)
.unwrap()
.remove(0);
let link = api
.store
.attach_worker_workdir(&WorkerWorkdirLinkRecord {
workdir_id: last.workdir_id.clone(),
alias: "catalog-occupancy".to_string(),
..template_link
})
.unwrap();
let occupied = list_current_worker_workdir_catalog(
api,
server_api::CurrentWorkerWorkdirCatalogQuery {
limit: Some(3),
cursor: None,
},
)
.unwrap();
assert_eq!(changed.items, occupied.items);
assert_ne!(changed.revision, occupied.revision);
api.store
.detach_worker_workdir_connection(
TEST_WORKSPACE_ID,
&fixture.worker,
&link.alias,
&link.connection_id,
"detach",
)
.unwrap();
assert_eq!(
changed.revision,
list_current_worker_workdir_catalog(api, Default::default())
.unwrap()
.revision
);
let restarted = SqliteWorkspaceStore::open(&api.config.database_path).unwrap();
assert_eq!(
changed.revision,
restarted
.workdir_catalog_page(TEST_WORKSPACE_ID, 100, None)
.unwrap()
.revision
);
let no_worker = runtime_source_request(&identity, None, "GET", &base, Vec::new());
assert_eq!(
build_router(api.clone())
.oneshot(no_worker)
.await
.unwrap()
.status(),
StatusCode::FORBIDDEN
);
let mut forged = request(&base);
forged
.headers_mut()
.insert("x-yoi-worker-id", "foreign-worker".parse().unwrap());
assert_eq!(
build_router(api.clone())
.oneshot(forged)
.await
.unwrap()
.status(),
StatusCode::FORBIDDEN
);
assert_eq!(
build_router(api.clone())
.oneshot(Request::builder().uri(&base).body(Body::empty()).unwrap())
.await
.unwrap()
.status(),
StatusCode::UNAUTHORIZED
);
}
#[tokio::test]
async fn current_worker_workdir_routes_require_a_live_runtime_worker_identity() {
let workspace = tempfile::tempdir().unwrap();
@@ -50995,6 +51314,10 @@ mod tests {
assert_eq!(page.items[0].working_directory_id, target.workdir_id);
assert_eq!(page.items[0].connection_id, target.connection_id);
assert_eq!(page.next_offset, None);
assert_eq!(
page.revision, first.revision,
"lookup does not narrow the collection revision"
);
let foreign_worker_id = seed_cleanup_worker(api, 902, "normal");
seed_cleanup_workdir(api, "foreign-lookup-workdir", "present", "clean");
@@ -51023,6 +51346,7 @@ mod tests {
.unwrap();
assert!(page.items.is_empty());
assert_eq!(page.next_offset, None);
assert_eq!(page.revision, first.revision);
}
let mut forged = request(&lookup_path);
forged
@@ -51049,6 +51373,13 @@ mod tests {
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
let unchanged =
list_current_worker_workdir_attachments(api, &fixture.worker, Default::default())
.unwrap();
assert_eq!(
unchanged, first,
"another caller's connection cannot change this revision"
);
api.store
.detach_worker_workdir_connection(
TEST_WORKSPACE_ID,
@@ -51060,6 +51391,139 @@ mod tests {
.unwrap();
let replacement = api.store.attach_worker_workdir(&target).unwrap();
assert_ne!(replacement.connection_id, target.connection_id);
let reattached =
list_current_worker_workdir_attachments(api, &fixture.worker, Default::default())
.unwrap();
assert_eq!(reattached.items, first.items);
assert_eq!(reattached.next_offset, first.next_offset);
assert_ne!(
reattached.revision, first.revision,
"same alias and Workdir, new lifetime beyond page zero"
);
let downgraded = api
.store
.attach_worker_workdir(&WorkerWorkdirLinkRecord {
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
..replacement.clone()
})
.unwrap();
assert_eq!(downgraded.connection_id, replacement.connection_id);
let capability_changed =
list_current_worker_workdir_attachments(api, &fixture.worker, Default::default())
.unwrap();
assert_eq!(capability_changed.items, reattached.items);
assert_ne!(capability_changed.revision, reattached.revision);
let restarted = SqliteWorkspaceStore::open(&api.config.database_path).unwrap();
let (restarted_page, restarted_revision) = restarted
.list_worker_workdir_links_page_with_revision(
TEST_WORKSPACE_ID,
&fixture.worker,
51,
0,
None,
)
.unwrap();
assert_eq!(restarted_page.len(), 51);
assert_eq!(restarted_revision, capability_changed.revision);
// A SQLite backup restores the same authoritative content/revision, without a sync store.
let source = rusqlite::Connection::open(&api.config.database_path).unwrap();
let mut snapshot = rusqlite::Connection::open_in_memory().unwrap();
rusqlite::backup::Backup::new(&source, &mut snapshot)
.unwrap()
.run_to_completion(128, std::time::Duration::from_millis(1), None)
.unwrap();
let restored = SqliteWorkspaceStore::from_connection(snapshot).unwrap();
assert_eq!(
restored
.list_worker_workdir_links_page_with_revision(
TEST_WORKSPACE_ID,
&fixture.worker,
51,
0,
None
)
.unwrap(),
(restarted_page, restarted_revision)
);
// Independent SQLite connections race a last-row capability update against exact lookup.
// The page and complete revision must always describe the same snapshot, not two reads.
let mut read_write_links = api
.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &fixture.worker)
.unwrap();
read_write_links
.iter_mut()
.find(|link| link.connection_id == replacement.connection_id)
.unwrap()
.capabilities = workdir::WorkdirSessionCapabilities::READ_WRITE;
let read_only_links = api
.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &fixture.worker)
.unwrap();
restarted
.replace_worker_workdir_link_capabilities(
TEST_WORKSPACE_ID,
&fixture.worker,
&read_write_links,
)
.unwrap();
let (_, read_write_revision) = restarted
.list_worker_workdir_links_page_with_revision(
TEST_WORKSPACE_ID,
&fixture.worker,
1,
0,
Some(&replacement.connection_id),
)
.unwrap();
assert_ne!(read_write_revision, capability_changed.revision);
let reader = SqliteWorkspaceStore::open(&api.config.database_path).unwrap();
let barrier = Arc::new(std::sync::Barrier::new(2));
std::thread::scope(|scope| {
let worker = &fixture.worker;
let writer_barrier = barrier.clone();
scope.spawn(move || {
writer_barrier.wait();
for index in 0..64 {
restarted
.replace_worker_workdir_link_capabilities(
TEST_WORKSPACE_ID,
worker,
if index % 2 == 0 {
&read_write_links
} else {
&read_only_links
},
)
.unwrap();
}
});
barrier.wait();
for _ in 0..100 {
let (page, revision) = reader
.list_worker_workdir_links_page_with_revision(
TEST_WORKSPACE_ID,
worker,
1,
0,
Some(&replacement.connection_id),
)
.unwrap();
assert_eq!(page.len(), 1);
if revision == read_write_revision {
assert_eq!(
page[0].capabilities,
workdir::WorkdirSessionCapabilities::READ_WRITE
);
} else {
assert_eq!(revision, capability_changed.revision);
assert_eq!(
page[0].capabilities,
workdir::WorkdirSessionCapabilities::READ_ONLY
);
}
}
});
let response = build_router(api.clone())
.oneshot(request(&lookup_path))
.await
+248 -3
View File
@@ -951,6 +951,23 @@ pub struct WorkdirRegistryRecord {
pub updated_at: String,
}
/// Projection inputs captured together with the catalog revision in one SQLite snapshot.
#[derive(Debug, Clone)]
pub struct WorkdirCatalogEntry {
pub record: WorkdirRegistryRecord,
pub repository_key: Option<String>,
/// None for Repository records and unsupported legacy External grants.
pub external_grant_permissions: Option<workdir::workspace::WorkdirPermissionSummary>,
pub occupied_by: Option<workdir::workspace::WorkingDirectoryOccupancy>,
}
#[derive(Debug, Clone)]
pub struct WorkdirCatalogPage {
pub entries: Vec<WorkdirCatalogEntry>,
pub next_cursor: Option<String>,
pub revision: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ExternalWorkdirGrantRecord {
pub grant_id: String,
@@ -1896,6 +1913,12 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
workspace_id: &str,
limit: usize,
) -> Result<Vec<WorkdirRegistryRecord>>;
fn workdir_catalog_page(
&self,
workspace_id: &str,
limit: u32,
cursor: Option<&str>,
) -> Result<WorkdirCatalogPage>;
fn delete_workdir_registry(&self, workspace_id: &str, workdir_id: &str) -> Result<bool>;
fn create_external_workdir_grant(
@@ -2008,6 +2031,14 @@ pub trait ControlPlaneStore: Send + Sync + WorkspaceDeletionStore {
offset: u32,
connection_id: Option<&str>,
) -> Result<Vec<WorkerWorkdirLinkRecord>>;
fn list_worker_workdir_links_page_with_revision(
&self,
workspace_id: &str,
worker: &RuntimeWorkerRef,
limit: u32,
offset: u32,
connection_id: Option<&str>,
) -> Result<(Vec<WorkerWorkdirLinkRecord>, String)>;
fn worker_workdir_link_history_exists(
&self,
workspace_id: &str,
@@ -8901,6 +8932,72 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
})
}
fn workdir_catalog_page(
&self,
workspace_id: &str,
limit: u32,
cursor: Option<&str>,
) -> Result<WorkdirCatalogPage> {
if limit == 0 || limit > 100 {
return Err(Error::InvalidInput(
"catalog page limit must be 1..=100".to_string(),
));
}
if cursor.is_some_and(|id| {
id.trim().is_empty() || id.len() > 128 || id.chars().any(char::is_control)
}) {
return Err(Error::InvalidInput(
"catalog cursor must contain 1..=128 bytes without control characters".to_string(),
));
}
self.with_conn(|conn| {
// Also fence writers using other SQLite connections, not only this mutex.
let tx = conn.unchecked_transaction()?;
let sql = workdir_catalog_select_sql();
let mut hasher = Sha256::new();
update_public_collection_digest(&mut hasher, &("workdir-catalog:v1", workspace_id))?;
{
let mut stmt = tx.prepare(&format!("{sql} WHERE wr.workspace_id = ?1 ORDER BY wr.workdir_id"))?;
let mut rows = stmt.query(params![workspace_id])?;
while let Some(row) = rows.next()? {
let (entry, connection_id) = read_workdir_catalog_entry(row)?;
let record = &entry.record;
// Safe projection fields and timestamps only: no paths, provider identity,
// credentials, Repository UUIDs, or raw internal records.
update_public_collection_digest(&mut hasher, &(
&record.workdir_id, &record.display_name, &entry.repository_key,
record.source.external_grant_id(), &entry.external_grant_permissions,
(&record.creation_selector, &record.creation_ref, &record.creation_tree),
(&record.current_selector, &record.current_ref, &record.current_tree),
record.observed_at_epoch_seconds,
(&record.materialization_status, &record.cleanliness),
(&record.created_at, &record.updated_at),
&entry.occupied_by, connection_id,
))?;
}
}
let mut stmt = tx.prepare(&format!(
"{sql} WHERE wr.workspace_id = ?1 AND (?2 IS NULL OR wr.workdir_id > ?2) ORDER BY wr.workdir_id ASC LIMIT ?3"
))?;
let mut rows = stmt.query(params![workspace_id, cursor, limit + 1])?;
let mut entries = Vec::new();
while let Some(row) = rows.next()? {
entries.push(read_workdir_catalog_entry(row)?.0);
}
let next_cursor = if entries.len() > limit as usize {
entries.truncate(limit as usize);
entries.last().map(|entry| entry.record.workdir_id.clone())
} else {
None
};
Ok(WorkdirCatalogPage {
entries,
next_cursor,
revision: hasher.finalize().iter().map(|byte| format!("{byte:02x}")).collect::<String>(),
})
})
}
fn delete_workdir_registry(&self, workspace_id: &str, workdir_id: &str) -> Result<bool> {
self.with_conn(|conn| {
let tx = rusqlite::Transaction::new_unchecked(
@@ -9815,6 +9912,24 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
offset: u32,
connection_id: Option<&str>,
) -> Result<Vec<WorkerWorkdirLinkRecord>> {
self.list_worker_workdir_links_page_with_revision(
workspace_id,
worker,
limit,
offset,
connection_id,
)
.map(|(links, _)| links)
}
fn list_worker_workdir_links_page_with_revision(
&self,
workspace_id: &str,
worker: &RuntimeWorkerRef,
limit: u32,
offset: u32,
connection_id: Option<&str>,
) -> Result<(Vec<WorkerWorkdirLinkRecord>, String)> {
if limit == 0 || limit > 101 {
return Err(Error::InvalidInput(
"attachment page limit must be 1..=101".to_string(),
@@ -9829,7 +9944,40 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
));
}
self.with_conn(|conn| {
let mut stmt = conn.prepare(
let tx = conn.unchecked_transaction()?;
let mut hasher = Sha256::new();
update_public_collection_digest(
&mut hasher,
&(
"workdir-connections:v1",
workspace_id,
&worker.runtime_id,
&worker.worker_id,
),
)?;
{
let mut stmt = tx.prepare(
"SELECT alias, connection_id, workdir_id, capabilities FROM worker_workdir_links
WHERE workspace_id = ?1 AND runtime_id = ?2 AND worker_id = ?3
AND unlinked_at IS NULL ORDER BY alias"
)?;
let mut rows =
stmt.query(params![workspace_id, worker.runtime_id, worker.worker_id])?;
while let Some(row) = rows.next()? {
let alias: String = row.get(0)?;
let id: String = row.get(1)?;
let workdir: String = row.get(2)?;
let capabilities =
decode_workdir_link_capabilities(&row.get::<_, String>(3)?, 3).map_err(
|_| Error::Store("invalid public connection capabilities".to_string()),
)?;
update_public_collection_digest(
&mut hasher,
&(alias, id, workdir, capabilities),
)?;
}
}
let mut stmt = tx.prepare(
r#"SELECT workspace_id, runtime_id, worker_id, workdir_id, alias, capabilities,
linked_at, unlinked_at, connection_id
FROM worker_workdir_links
@@ -9849,8 +9997,15 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
],
read_worker_workdir_link_record,
)?;
rows.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Error::from)
let links = rows.collect::<std::result::Result<Vec<_>, _>>()?;
Ok((
links,
hasher
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect::<String>(),
))
})
}
@@ -11614,6 +11769,96 @@ fn require_expected_ticket_assignment(
)))
}
// Length-framed canonical JSON prevents ambiguous concatenations and never hashes raw ledgers.
fn update_public_collection_digest(hasher: &mut Sha256, value: &impl Serialize) -> Result<()> {
let bytes = serde_json::to_vec(value)
.map_err(|_| Error::Store("unable to encode public collection metadata".to_string()))?;
hasher.update((bytes.len() as u64).to_be_bytes());
hasher.update(bytes);
Ok(())
}
fn workdir_catalog_select_sql() -> String {
format!(
"SELECT wr.*, r.repository_key, g.permissions,
l.runtime_id, l.worker_id, w.display_name, l.linked_at, l.connection_id
FROM ({}) wr
LEFT JOIN repositories r ON r.workspace_id = wr.workspace_id AND r.repository_id = wr.repository_id
LEFT JOIN external_workdir_grants g ON g.workspace_id = wr.workspace_id AND g.grant_id = wr.external_grant_id
LEFT JOIN worker_workdir_links l ON l.rowid = (
SELECT rowid FROM worker_workdir_links
WHERE workspace_id = wr.workspace_id AND workdir_id = wr.workdir_id AND unlinked_at IS NULL
ORDER BY linked_at DESC, rowid DESC LIMIT 1
)
LEFT JOIN worker_registry w ON w.workspace_id = l.workspace_id AND w.runtime_id = l.runtime_id AND w.worker_id = l.worker_id",
workdir_registry_select_sql("")
)
}
fn read_workdir_catalog_entry(
row: &rusqlite::Row<'_>,
) -> Result<(WorkdirCatalogEntry, Option<String>)> {
let record = read_workdir_registry_record(row)?;
let repository_key: Option<String> = row.get(18)?;
let external_grant_permissions = match &record.source {
WorkdirRegistrySource::Repository { .. } => {
if repository_key.is_none() {
return Err(Error::Store(
"Workdir catalog Repository is unavailable".to_string(),
));
}
None
}
WorkdirRegistrySource::ExternalGrant { .. } => {
let permissions: Option<String> = row.get(19)?;
match permissions.as_deref() {
Some("read_only") => Some(workdir::workspace::WorkdirPermissionSummary {
read: true,
write: false,
command: false,
}),
Some("read_write") => Some(workdir::workspace::WorkdirPermissionSummary {
read: true,
write: true,
command: false,
}),
Some("read_write_command") => Some(workdir::workspace::WorkdirPermissionSummary {
read: true,
write: true,
command: true,
}),
Some("command_only" | "read_command") => None,
_ => {
return Err(Error::Store(
"Workdir catalog External grant permissions are unavailable".to_string(),
));
}
}
}
};
let runtime_id: Option<String> = row.get(20)?;
let occupied_by = match runtime_id {
Some(runtime_id) => Some(workdir::workspace::WorkingDirectoryOccupancy {
runtime_id,
worker_id: row.get(21)?,
display_name: row.get::<_, Option<String>>(22)?.ok_or_else(|| {
Error::Store("Workdir catalog occupant is unavailable".to_string())
})?,
linked_at: row.get(23)?,
}),
None => None,
};
Ok((
WorkdirCatalogEntry {
record,
repository_key,
external_grant_permissions,
occupied_by,
},
row.get(24)?,
))
}
fn workdir_registry_select_sql(where_clause: &str) -> String {
format!(
"SELECT workspace_id, workdir_id, display_name, source_kind, runtime_id, repository_id, external_grant_id, \