chore: merge develop into hare/develop

This commit is contained in:
2026-10-16 05:52:36 +09:00
85 changed files with 21016 additions and 329 deletions
+12 -5
View File
@@ -256,15 +256,22 @@ impl FsRuntimeStore {
}
pub(crate) fn delete_worker_record(&self, worker_id: &WorkerId) -> Result<(), RuntimeError> {
self.delete_worker_record_with_intent(
worker_id,
session_store::retention::SessionDeletionIntent::Ordinary,
)
}
pub(crate) fn delete_worker_record_with_intent(
&self,
worker_id: &WorkerId,
intent: session_store::retention::SessionDeletionIntent,
) -> Result<(), RuntimeError> {
let worker_dir = self.worker_dir(worker_id);
if !worker_dir.exists() {
return Ok(());
}
fs::remove_dir_all(&worker_dir).map_err(|source| RuntimeError::StoreIo {
operation: "delete worker store",
path: worker_dir,
source,
})
crate::retention::delete_worker_aggregate(&self.root, &worker_dir, intent)
}
pub(crate) fn load_runtime_state(&self) -> Result<PersistedRuntimeState, RuntimeError> {
+3
View File
@@ -2420,6 +2420,9 @@ pub enum RuntimeHttpServerError {
#[cfg(test)]
mod tests {
#[cfg(feature = "fs-store")]
mod session_material_tests;
use super::*;
use crate::auth::RuntimeIdentityMaterial;
use crate::catalog::{ConfigBundleRef, ProfileSelector, WorkspaceApiRef};
@@ -35,6 +35,86 @@ fn required_auth() -> Result<Extension<RuntimeAuthContext>, runtime_api::Runtime
})
}
fn material_scope(
state: &RuntimeHttpState,
workspace_id: &str,
) -> Result<RuntimeWorkspaceScope, runtime_api::RuntimeApiError> {
let scope = auth_workspace_scope(state, auth_extension().as_ref())
.map_err(api_error)?
.ok_or_else(|| {
runtime_api::RuntimeApiError::new(
403,
"runtime_session_material_scope_required",
"Canonical Session material requires a Workspace-scoped capability",
)
})?;
if scope.workspace_id != workspace_id {
return Err(runtime_api::RuntimeApiError::new(
403,
"runtime_session_material_scope_mismatch",
"Canonical Session Workspace does not match the authenticated capability",
));
}
Ok(scope)
}
fn material_pin(
state: &RuntimeHttpState,
request: runtime_api::SessionMaterialPinRequest,
release: bool,
) -> Result<runtime_api::SessionMaterialPinResponse, runtime_api::RuntimeApiError> {
let scope = material_scope(state, &request.workspace_id)?;
#[cfg(feature = "fs-store")]
{
let pin = request
.pin_id
.parse::<uuid::Uuid>()
.map(session_store::retention::SessionRetentionPin)
.map_err(|_| {
runtime_api::RuntimeApiError::new(
400,
"invalid_session_retention_pin",
"Invalid Session retention pin identity",
)
})?;
if release {
state.runtime.release_session_public_source_pin_scoped(
&scope,
&request.source,
&request.expected_session_id,
pin,
)
} else {
state.runtime.pin_session_public_source_scoped(
&scope,
&request.source,
&request.expected_session_id,
pin,
)
}
.map_err(|_| {
runtime_api::RuntimeApiError::new(
409,
"session_retention_rejected",
"Session retention operation could not be verified",
)
})?;
Ok(runtime_api::SessionMaterialPinResponse {
session_id: request.expected_session_id,
pin_id: request.pin_id,
})
}
#[cfg(not(feature = "fs-store"))]
{
let _ = (scope, release);
Err(runtime_api::RuntimeApiError::new(
503,
"session_retention_unavailable",
"Session retention requires filesystem storage",
))
}
}
#[derive(Clone)]
struct RuntimeManagementApi {
state: RuntimeHttpState,
@@ -289,6 +369,103 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
.map_err(api_error)
}
async fn session_material_erase(
&self,
request: runtime_api::SessionMaterialEraseRequest,
) -> Result<runtime_api::SessionMaterialEraseResponse, runtime_api::RuntimeApiError> {
let scope = material_scope(&self.state, &request.workspace_id)?;
#[cfg(feature = "fs-store")]
{
self.state
.runtime
.delete_session_public_source_scoped(
&scope,
&request.source,
&request.expected_session_id,
session_store::retention::SessionDeletionIntent::ErasePermanently,
)
.map_err(|_| {
runtime_api::RuntimeApiError::new(
409,
"session_erasure_incomplete",
"Session erasure could not complete; retain erasure intent and retry",
)
})?;
Ok(runtime_api::SessionMaterialEraseResponse {
session_id: request.expected_session_id,
})
}
#[cfg(not(feature = "fs-store"))]
{
let _ = scope;
Err(runtime_api::RuntimeApiError::new(
503,
"session_retention_unavailable",
"Session erasure requires filesystem storage",
))
}
}
async fn session_material_pin(
&self,
request: runtime_api::SessionMaterialPinRequest,
) -> Result<runtime_api::SessionMaterialPinResponse, runtime_api::RuntimeApiError> {
material_pin(&self.state, request, false)
}
async fn session_material_release(
&self,
request: runtime_api::SessionMaterialPinRequest,
) -> Result<runtime_api::SessionMaterialPinResponse, runtime_api::RuntimeApiError> {
material_pin(&self.state, request, true)
}
async fn session_material_catalog(
&self,
request: runtime_api::SessionMaterialCatalogRequest,
) -> Result<runtime_api::SessionMaterialCatalogAvailability, runtime_api::RuntimeApiError> {
let scope = material_scope(&self.state, &request.workspace_id)?;
#[cfg(feature = "fs-store")]
{
Ok(self
.state
.runtime
.session_material_catalog_scoped(&scope, &request))
}
#[cfg(not(feature = "fs-store"))]
{
let _ = scope;
Ok(
runtime_api::SessionMaterialCatalogAvailability::Unavailable {
reason: runtime_api::SessionPublicUnavailableReason::StorageUnavailable,
message: "Canonical Session storage unavailable".into(),
},
)
}
}
async fn session_material_read(
&self,
request: runtime_api::SessionMaterialReadRequest,
) -> Result<runtime_api::SessionMaterialReadAvailability, runtime_api::RuntimeApiError> {
let scope = material_scope(&self.state, &request.workspace_id)?;
#[cfg(feature = "fs-store")]
{
Ok(self
.state
.runtime
.session_material_read_scoped(&scope, &request))
}
#[cfg(not(feature = "fs-store"))]
{
let _ = scope;
Ok(runtime_api::SessionMaterialReadAvailability::Unavailable {
reason: runtime_api::SessionPublicUnavailableReason::StorageUnavailable,
message: "Canonical Session storage unavailable".into(),
})
}
}
async fn session_public_search(
&self,
request: runtime_api::SessionPublicSearchRequest,
File diff suppressed because it is too large Load Diff
+3
View File
@@ -20,7 +20,10 @@ pub mod resource;
pub mod retention;
mod runtime;
#[cfg(feature = "fs-store")]
mod session_material;
#[cfg(feature = "fs-store")]
mod session_public;
mod session_retention;
pub mod ssh_host_key_probe;
pub mod worker_backend;
pub mod worker_source;
+410 -46
View File
@@ -133,6 +133,9 @@ pub struct WorkerSessionArchiveManifest {
pub source_runtime_id: String,
pub source_worker_id: WorkerId,
pub source_session_id: String,
/// Older archives have no attested active lineage and cannot serve canonical scans.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source_active_segment_id: Option<session_store::SegmentId>,
pub segment_ids: Vec<String>,
pub source_created_at: String,
pub removed_at: String,
@@ -173,6 +176,285 @@ pub(crate) trait WorkerRetentionProvider: Send + Sync {
) -> Result<WorkerRetentionExecutionResult, RuntimeError>;
}
pub use crate::session_retention::runtime_session_retention;
/// Prove the legitimate pre-publication state, rather than interpreting a missing
/// source as empty. A committed active pointer or any source file defeats this proof.
pub(crate) fn unmaterialized_session(
worker_dir: &Path,
workspace_id: &str,
) -> Result<Option<session_store::SessionId>, RuntimeError> {
let metadata_path = worker_dir.join("metadata.json");
let bytes = match fs::read(&metadata_path) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(RuntimeError::StoreIo {
operation: "inspect pending Session",
path: metadata_path,
source,
});
}
};
let metadata: session_store::WorkerMetadata = serde_json::from_slice(&bytes)
.map_err(|_| RuntimeError::InvalidRequest("Pending Session metadata is invalid".into()))?;
if metadata.workspace_id.as_deref() != Some(workspace_id) {
return Ok(None);
}
let Some(active) = metadata.active.filter(|active| active.segment_id.is_none()) else {
return Ok(None);
};
let root = worker_dir.join("session");
if collect_files(&root, "inspect pending Session")?.is_empty() {
Ok(Some(active.session_id))
} else {
Ok(None)
}
}
pub(crate) fn aggregate_session_id(
worker_dir: &Path,
) -> Result<Option<session_store::SessionId>, RuntimeError> {
let session_path = worker_dir.join("session/session.json");
let saved = match fs::read(&session_path) {
Ok(bytes) => {
let manifest: CanonicalSessionManifest =
serde_json::from_slice(&bytes).map_err(|_| {
RuntimeError::InvalidRequest("Retained Session manifest is invalid".into())
})?;
Some(
manifest
.session_id
.parse::<session_store::SessionId>()
.map_err(|_| {
RuntimeError::InvalidRequest("Retained Session identity is invalid".into())
})?,
)
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(source) => {
return Err(RuntimeError::StoreIo {
operation: "read retained Session identity",
path: session_path,
source,
});
}
};
let metadata_path = worker_dir.join("metadata.json");
let active = match fs::read(&metadata_path) {
Ok(bytes) => {
let metadata: session_store::WorkerMetadata =
serde_json::from_slice(&bytes).map_err(|_| {
RuntimeError::InvalidRequest("Retained Worker metadata is invalid".into())
})?;
metadata.active.map(|active| active.session_id)
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(source) => {
return Err(RuntimeError::StoreIo {
operation: "read retained Session pointer",
path: metadata_path,
source,
});
}
};
if let (Some(saved), Some(active)) = (saved, active) {
if saved != active {
return Err(RuntimeError::InvalidRequest(
"Retained Session manifest and active pointer disagree".into(),
));
}
}
if saved.is_none() && active.is_none() {
let session_dir = worker_dir.join("session");
if session_dir.is_dir()
&& !collect_files(&session_dir, "inspect uninitialized Session")?.is_empty()
{
return Err(RuntimeError::InvalidRequest(
"Session material has no retained identity".into(),
));
}
}
Ok(saved.or(active))
}
/// Called only while the common Session deletion lock is held. Include staged
/// archives from interrupted transfers: erasure must not leave a second original.
pub(crate) fn remove_archived_session_copies(
runtime_root: &Path,
session: session_store::SessionId,
) -> Result<(), session_store::StoreError> {
let archives = runtime_root.join("archives/workers");
let entries = match fs::read_dir(archives) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error.into()),
};
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_dir() {
continue;
}
let session_root = entry.path().join("session");
let owner = match fs::read(entry.path().join("manifest.json")) {
Ok(bytes) => Some(serde_json::from_slice::<WorkerSessionArchiveManifest>(
&bytes,
)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
Err(error) => return Err(error.into()),
};
if let Some(owner) = owner {
if owner.source_session_id == session.to_string() {
erase_archived_diagnostics(runtime_root, &owner)?;
match fs::remove_dir_all(&session_root) {
Ok(()) => (),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => (),
Err(error) => return Err(error.into()),
}
}
continue;
}
let bytes = match fs::read(session_root.join("session.json")) {
Ok(bytes) => bytes,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => continue,
Err(error) => return Err(error.into()),
};
let manifest: CanonicalSessionManifest = serde_json::from_slice(&bytes)?;
if manifest.session_id == session.to_string() {
fs::remove_dir_all(&session_root)?;
}
}
Ok(())
}
/// Erasure includes diagnostic originals of the same archived removal operation.
/// The immutable removal receipt is historical evidence and is never rewritten.
/// Staging names are bound to that exact operation; unrelated archives are untouched.
fn erase_archived_diagnostics(
runtime_root: &Path,
archive: &WorkerSessionArchiveManifest,
) -> Result<(), session_store::StoreError> {
let invalid = || {
session_store::StoreError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Archived diagnostics erasure identity mismatch",
))
};
if archive.schema_version != ARCHIVE_SCHEMA_VERSION {
return Err(invalid());
}
validate_id("operation_id", &archive.operation_id).map_err(|_| invalid())?;
let receipt_path = runtime_root
.join("retention/operations")
.join(format!("{}.json", archive.operation_id));
if let Some(receipt) = read_operation_receipt(&receipt_path).map_err(|_| invalid())? {
if receipt.request.workspace_id != archive.workspace_id
|| receipt.request.source_runtime_id != archive.source_runtime_id
|| receipt.request.worker_id != archive.source_worker_id
|| receipt.request.operation_id != archive.operation_id
|| receipt.request.input_fingerprint != archive.input_fingerprint
|| receipt.request.archive_id.as_ref() != Some(&archive.archive_id)
|| receipt.result.archive.as_ref() != Some(archive)
{
return Err(invalid());
}
}
let parent = runtime_root.join("archives/diagnostics");
let entries = match fs::read_dir(&parent) {
Ok(entries) => entries,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(()),
Err(error) => return Err(error.into()),
};
let staging_prefix = format!(".{}.tmp-", archive.operation_id);
for entry in entries {
let entry = entry?;
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let staged = name.strip_prefix(&staging_prefix).is_some_and(|suffix| {
suffix.split_once('-').is_some_and(|(pid, sequence)| {
!pid.is_empty()
&& !sequence.is_empty()
&& pid.bytes().all(|b| b.is_ascii_digit())
&& sequence.bytes().all(|b| b.is_ascii_digit())
})
});
if name != archive.operation_id && !staged {
continue;
}
if !entry.file_type()?.is_dir() {
return Err(invalid());
}
let manifest = match fs::read(entry.path().join("manifest.json")) {
Ok(bytes) => Some(serde_json::from_slice::<DiagnosticsArchiveManifest>(
&bytes,
)?),
Err(error) if error.kind() == std::io::ErrorKind::NotFound && staged => None,
Err(error) => return Err(error.into()),
};
if let Some(manifest) = manifest {
if manifest.schema_version != ARCHIVE_SCHEMA_VERSION
|| manifest.workspace_id != archive.workspace_id
|| manifest.source_runtime_id != archive.source_runtime_id
|| manifest.source_worker_id != archive.source_worker_id
|| manifest.operation_id != archive.operation_id
|| manifest.input_fingerprint != archive.input_fingerprint
{
return Err(invalid());
}
}
fs::remove_dir_all(entry.path())?;
}
sync_directory(&parent, "erase archived Worker diagnostics").map_err(|_| invalid())?;
Ok(())
}
pub(crate) fn retention_error(error: session_store::StoreError) -> RuntimeError {
RuntimeError::InvalidRequest(format!("Session retention rejected operation: {error}"))
}
/// Direct Worker deletion cannot bypass pins. Purge policy is not erase intent.
pub(crate) fn delete_worker_aggregate(
runtime_root: &Path,
worker_dir: &Path,
intent: session_store::retention::SessionDeletionIntent,
) -> Result<(), RuntimeError> {
let session = aggregate_session_id(worker_dir)?;
let retention = runtime_session_retention(runtime_root);
// Ordinary policy never creates erase authority. A pre-existing permanent
// tombstone is an irrevocable decision whose physical cleanup may be resumed.
let intent = match session
.map(|session| retention.ensure_live(session))
.transpose()
{
Err(session_store::StoreError::SessionErased(_)) => {
session_store::retention::SessionDeletionIntent::ErasePermanently
}
Err(error) => return Err(retention_error(error)),
Ok(_) => intent,
};
let remove = || -> Result<(), session_store::StoreError> {
if intent == session_store::retention::SessionDeletionIntent::ErasePermanently {
if let Some(session) = session {
remove_archived_session_copies(runtime_root, session)?;
}
}
match fs::remove_dir_all(worker_dir) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
};
if let Some(session) = session {
runtime_session_retention(runtime_root)
.delete(session, intent, remove)
.map_err(retention_error)
} else {
remove().map_err(retention_error)
}
}
/// Filesystem provider for the canonical Runtime Worker aggregate.
#[derive(Clone, Debug)]
pub(crate) struct FsWorkerRetentionProvider {
@@ -355,6 +637,16 @@ impl FsWorkerRetentionProvider {
pub(crate) fn session_archive(
&self,
archive_id: &str,
) -> Result<(WorkerSessionArchiveManifest, PathBuf), RuntimeError> {
let (manifest, session_root) = self.session_archive_identity(archive_id)?;
let archive_dir = self.archive_dir(archive_id)?;
let manifest = validate_existing_archive(&archive_dir, &manifest)?;
Ok((manifest, session_root))
}
pub(crate) fn session_archive_identity(
&self,
archive_id: &str,
) -> Result<(WorkerSessionArchiveManifest, PathBuf), RuntimeError> {
let archive_dir = self.archive_dir(archive_id)?;
let manifest: WorkerSessionArchiveManifest = read_json(
@@ -368,7 +660,6 @@ impl FsWorkerRetentionProvider {
message: "archive manifest identity mismatch".to_string(),
});
}
let manifest = validate_existing_archive(&archive_dir, &manifest)?;
Ok((manifest, archive_dir.join("session")))
}
@@ -401,33 +692,36 @@ impl WorkerRetentionProvider for FsWorkerRetentionProvider {
return Err(RuntimeError::WorkerNotFound { worker_id });
}
let session_dir = worker_dir.join("session");
let (session_id, segment_ids, session_bytes) = if session_dir.is_dir() {
let manifest: CanonicalSessionManifest = read_json(
&session_dir.join("session.json"),
"inventory Worker retention",
)?;
let files = collect_files(&session_dir, "inventory Worker retention")?;
let mut segment_ids = BTreeSet::new();
let mut bytes = 0_u64;
for (relative, path) in files {
bytes = bytes.saturating_add(file_len(&path, "inventory Worker retention")?);
if let Some(name) = relative.file_name().and_then(|value| value.to_str()) {
let segment = name
.strip_suffix(".trace.jsonl")
.or_else(|| name.strip_suffix(".jsonl"));
if let Some(segment) = segment {
segment_ids.insert(segment.to_string());
let (session_id, segment_ids, session_bytes) =
if unmaterialized_session(&worker_dir, workspace_id)?.is_some() {
(None, Vec::new(), 0)
} else if session_dir.is_dir() {
let manifest: CanonicalSessionManifest = read_json(
&session_dir.join("session.json"),
"inventory Worker retention",
)?;
let files = collect_files(&session_dir, "inventory Worker retention")?;
let mut segment_ids = BTreeSet::new();
let mut bytes = 0_u64;
for (relative, path) in files {
bytes = bytes.saturating_add(file_len(&path, "inventory Worker retention")?);
if let Some(name) = relative.file_name().and_then(|value| value.to_str()) {
let segment = name
.strip_suffix(".trace.jsonl")
.or_else(|| name.strip_suffix(".jsonl"));
if let Some(segment) = segment {
segment_ids.insert(segment.to_string());
}
}
}
}
(
Some(manifest.session_id),
segment_ids.into_iter().collect(),
bytes,
)
} else {
(None, Vec::new(), 0)
};
(
Some(manifest.session_id),
segment_ids.into_iter().collect(),
bytes,
)
} else {
(None, Vec::new(), 0)
};
let diagnostics_bytes = diagnostics_files(&worker_dir, "inventory Worker retention")?
.into_iter()
.try_fold(0_u64, |total, path| {
@@ -503,12 +797,46 @@ impl WorkerRetentionProvider for FsWorkerRetentionProvider {
worker_id: request.worker_id,
});
}
// Transfer keeps the same Session pin/fence namespace. Hold its lock
// across verified archive publication and aggregate removal. A policy
// purge instead uses ordinary deletion and must reject outstanding pins.
let retention = runtime_session_retention(&self.runtime_root);
let transfer_guard = if request.session_disposition == SessionDisposition::Archive {
aggregate_session_id(&worker_dir)?
.map(|session| retention.lock_live(session))
.transpose()
.map_err(retention_error)?
} else {
None
};
let archive = match request.session_disposition {
SessionDisposition::Archive => {
Some(commit_session_archive(self, request, &worker_dir)?)
}
SessionDisposition::Purge => None,
};
if let Some(manifest) = &archive {
if let Some(active_segment) = manifest.source_active_segment_id {
// We already hold the Session transfer lock. Do not reacquire it
// through the external-retention constructor during verification.
let reader =
session_store::public_material::SessionPublicMaterialReader::open_worker(
&self.archive_dir(&manifest.archive_id)?.join("session"),
active_segment,
Default::default(),
)
.map_err(|error| {
RuntimeError::InvalidRequest(format!(
"Archived canonical source is unreadable: {error}"
))
})?;
if reader.session_id().to_string() != manifest.source_session_id {
return Err(RuntimeError::InvalidRequest(
"Archived canonical source identity mismatch".into(),
));
}
}
}
let diagnostics_retained = match request.diagnostics_disposition {
DiagnosticsDisposition::Purge => false,
DiagnosticsDisposition::Retain => {
@@ -540,11 +868,23 @@ impl WorkerRetentionProvider for FsWorkerRetentionProvider {
"commit pending Worker retention receipt",
)?;
fs::remove_dir_all(&worker_dir).map_err(|source| RuntimeError::StoreIo {
operation: "remove retained Worker aggregate",
path: worker_dir.clone(),
source,
})?;
match request.session_disposition {
SessionDisposition::Archive => {
// commit_session_archive already compared all original bytes.
// The transfer lock remains held until removal has completed.
fs::remove_dir_all(&worker_dir).map_err(|source| RuntimeError::StoreIo {
operation: "remove retained Worker aggregate",
path: worker_dir.clone(),
source,
})?;
}
SessionDisposition::Purge => delete_worker_aggregate(
&self.runtime_root,
&worker_dir,
session_store::retention::SessionDeletionIntent::Ordinary,
)?,
}
drop(transfer_guard);
sync_directory(
worker_dir.parent().unwrap_or(&self.runtime_root),
"remove retained Worker aggregate",
@@ -703,14 +1043,14 @@ fn commit_session_archive(
RuntimeError::InvalidRequest("archive disposition requires archive_id".to_string())
})?;
let session_dir = worker_dir.join("session");
if !session_dir.is_dir() {
return Err(RuntimeError::StoreMissing {
operation: "archive Worker Session",
path: session_dir,
});
}
let session: CanonicalSessionManifest =
read_json(&session_dir.join("session.json"), "archive Worker Session")?;
let pending = unmaterialized_session(worker_dir, &request.workspace_id)?;
let session: CanonicalSessionManifest = if let Some(session) = pending {
CanonicalSessionManifest {
session_id: session.to_string(),
}
} else {
read_json(&session_dir.join("session.json"), "archive Worker Session")?
};
let source_files = collect_files(&session_dir, "archive Worker Session")?;
let (checksum, bytes, count) = checksum_files(&source_files, "archive Worker Session")?;
let mut segment_ids = BTreeSet::new();
@@ -730,6 +1070,25 @@ fn commit_session_archive(
workspace_id: request.workspace_id.clone(),
source_runtime_id: request.source_runtime_id.clone(),
source_worker_id: request.worker_id,
source_active_segment_id: if worker_dir.join("metadata.json").is_file() {
let metadata: session_store::WorkerMetadata = read_json(
&worker_dir.join("metadata.json"),
"archive active Session pointer",
)?;
let active = metadata.active.ok_or_else(|| {
RuntimeError::InvalidRequest("Worker has no active Session pointer".into())
})?;
if active.session_id.to_string() != session.session_id
|| metadata.workspace_id.as_deref() != Some(&request.workspace_id)
{
return Err(RuntimeError::InvalidRequest(
"Archive Session pointer scope mismatch".into(),
));
}
active.segment_id
} else {
None
},
source_session_id: session.session_id,
segment_ids: segment_ids.into_iter().collect(),
source_created_at: request.source_created_at.clone(),
@@ -770,6 +1129,13 @@ fn commit_session_archive(
path: staging.clone(),
source,
})?;
// Persist source ownership before copying any original bytes. An
// interrupted staging tree remains attributable to explicit erasure.
atomic_write_json(
&staging.join("manifest.json"),
&manifest,
"archive Worker Session",
)?;
let target_session = staging.join("session");
fs::create_dir(&target_session).map_err(|source| RuntimeError::StoreIo {
operation: "archive Worker Session",
@@ -777,11 +1143,6 @@ fn commit_session_archive(
source,
})?;
copy_files(&source_files, &target_session, "archive Worker Session")?;
atomic_write_json(
&staging.join("manifest.json"),
&manifest,
"archive Worker Session",
)?;
sync_tree(&staging, "archive Worker Session")?;
fs::rename(&staging, &archive_dir).map_err(|source| RuntimeError::StoreIo {
operation: "archive Worker Session",
@@ -1264,7 +1625,7 @@ mod tests {
);
write_json(
&worker.join("session/session.json"),
&serde_json::json!({"schema_version": 1, "session_id": "session-a"}),
&serde_json::json!({"schema_version": 1, "session_id": "00000000-0000-4000-8000-000000000001"}),
);
fs::create_dir_all(worker.join("session/segments")).unwrap();
fs::write(worker.join("session/segments/segment-a.jsonl"), b"one\n").unwrap();
@@ -1310,7 +1671,10 @@ mod tests {
let first = provider.execute(&request).unwrap();
assert!(first.source_removed);
let archive = first.archive.as_ref().unwrap();
assert_eq!(archive.source_session_id, "session-a");
assert_eq!(
archive.source_session_id,
"00000000-0000-4000-8000-000000000001"
);
assert_eq!(archive.segment_ids, vec!["segment-a"]);
assert!(!temp.path().join(format!("workers/{worker_id}")).exists());
assert!(
+334 -5
View File
@@ -3932,7 +3932,11 @@ impl Runtime {
scope: &RuntimeWorkspaceScope,
worker_ref: &WorkerRef,
) -> Result<WorkerDeleteResult, RuntimeError> {
self.delete_worker_with_scope(worker_ref, Some(scope))
self.delete_worker_with_scope(
worker_ref,
Some(scope),
session_store::retention::SessionDeletionIntent::Ordinary,
)
}
/// Delete a non-running Worker from Runtime state and persisted Worker storage.
@@ -3940,13 +3944,30 @@ impl Runtime {
&self,
worker_ref: &WorkerRef,
) -> Result<WorkerDeleteResult, RuntimeError> {
self.delete_worker_with_scope(worker_ref, None)
self.delete_worker_with_scope(
worker_ref,
None,
session_store::retention::SessionDeletionIntent::Ordinary,
)
}
/// A trusted Host must choose explicit erase intent separately from policy
/// Purge. This also permits finishing aggregate removal after source erasure.
#[cfg(feature = "fs-store")]
pub fn delete_worker_with_intent_scoped(
&self,
scope: &RuntimeWorkspaceScope,
worker_ref: &WorkerRef,
intent: session_store::retention::SessionDeletionIntent,
) -> Result<WorkerDeleteResult, RuntimeError> {
self.delete_worker_with_scope(worker_ref, Some(scope), intent)
}
fn delete_worker_with_scope(
&self,
worker_ref: &WorkerRef,
scope: Option<&RuntimeWorkspaceScope>,
intent: session_store::retention::SessionDeletionIntent,
) -> Result<WorkerDeleteResult, RuntimeError> {
let operation_lock = self.worker_operation_lock(worker_ref.worker_id)?;
let _operation_guard = operation_lock
@@ -4002,7 +4023,17 @@ impl Runtime {
worker_ref.worker_id
)));
}
if state.delete_worker_record(&worker_ref.worker_id).is_err() {
#[cfg(feature = "fs-store")]
let deletion = match state.fs_store() {
Some(store) => store.delete_worker_record_with_intent(&worker_ref.worker_id, intent),
None => Ok(()),
};
#[cfg(not(feature = "fs-store"))]
let deletion = {
let _ = intent;
state.delete_worker_record(&worker_ref.worker_id)
};
if deletion.is_err() {
state.record_worker_delete_persistence_failure(worker_ref);
let _diagnostic_persistence = state.persist_runtime_snapshot();
return Err(RuntimeError::WorkerDeletePersistenceFailed {
@@ -4773,6 +4804,277 @@ impl Runtime {
)
}
/// Retain only an authenticated source whose saved Session identity matches.
/// The existence check and pin publication share the physical deletion lock.
#[cfg(feature = "fs-store")]
pub fn pin_session_public_source_scoped(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
expected_session_id: &str,
pin: session_store::retention::SessionRetentionPin,
) -> Result<(), RuntimeError> {
let (root, _) = self.resolve_session_public_source(scope, source)?;
let session = expected_session_id
.parse::<session_store::SessionId>()
.map_err(|_| RuntimeError::InvalidRequest("Invalid Session identity".into()))?;
self.session_retention_store()?
.pin(session, pin, || {
let store = match session_store::WorkerSessionStore::open_read_only(&root) {
Ok(store) => store,
Err(session_store::StoreError::Io(error))
if error.kind() == std::io::ErrorKind::NotFound =>
{
return Ok(false);
}
Err(error) => return Err(error),
};
Ok(store.session_id()? == Some(session))
})
.map_err(crate::retention::retention_error)
}
#[cfg(feature = "fs-store")]
pub fn release_session_public_source_pin_scoped(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
expected_session_id: &str,
pin: session_store::retention::SessionRetentionPin,
) -> Result<(), RuntimeError> {
let (root, archive) =
self.resolve_session_public_source_with_validation(scope, source, false)?;
let session = expected_session_id
.parse::<session_store::SessionId>()
.map_err(|_| RuntimeError::InvalidRequest("Invalid Session identity".into()))?;
let owned_session = match archive {
Some(manifest) => manifest.source_session_id.parse().ok(),
None => {
crate::retention::aggregate_session_id(root.parent().ok_or_else(|| {
RuntimeError::InvalidRequest("Session root unavailable".into())
})?)?
}
};
if owned_session != Some(session) {
return Err(RuntimeError::InvalidRequest(
"Session source identity mismatch".into(),
));
}
self.session_retention_store()?
.release(session, pin)
.map_err(crate::retention::retention_error)
}
/// Physical source deletion is separate from Worker registration/lifecycle.
/// Only this explicit intent may invalidate pins and persist an erase fence;
/// a retention policy's Purge remains ordinary deletion.
#[cfg(feature = "fs-store")]
pub fn delete_session_public_source_scoped(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
expected_session_id: &str,
intent: session_store::retention::SessionDeletionIntent,
) -> Result<(), RuntimeError> {
let (root, archive) =
self.resolve_session_public_source_with_validation(scope, source, false)?;
let owner = match source {
runtime_api::SessionPublicSource::Retained { worker_id } => *worker_id,
runtime_api::SessionPublicSource::Archived { .. } => {
archive
.as_ref()
.ok_or_else(|| {
RuntimeError::InvalidRequest("Archive identity unavailable".into())
})?
.source_worker_id
}
};
let operation_lock = self.worker_operation_lock(owner)?;
let _operation_guard = operation_lock
.lock()
.map_err(|_| RuntimeError::StatePoisoned)?;
let state = self.lock()?;
if let Some(worker) = state.workers.get(&owner) {
if worker.workspace_id.as_deref() != Some(scope.workspace_id.as_str())
|| worker.status != WorkerStatus::Stopped
|| worker.pending_lifecycle_operation_name().is_some()
{
return Err(RuntimeError::InvalidRequest(
"Session deletion requires a stopped, settled source Worker".into(),
));
}
}
let store = state.fs_store().ok_or_else(|| {
RuntimeError::InvalidRequest("Session deletion requires filesystem storage".into())
})?;
let session = expected_session_id
.parse::<session_store::SessionId>()
.map_err(|_| RuntimeError::InvalidRequest("Invalid Session identity".into()))?;
let source_session = match archive.as_ref() {
Some(manifest) => manifest
.source_session_id
.parse::<session_store::SessionId>()
.ok(),
None => {
crate::retention::aggregate_session_id(root.parent().ok_or_else(|| {
RuntimeError::InvalidRequest("Source root unavailable".into())
})?)?
}
};
if source_session != Some(session) {
return Err(RuntimeError::InvalidRequest(
"Session source identity mismatch".into(),
));
}
crate::retention::runtime_session_retention(store.runtime_dir())
.delete(session, intent, || {
let saved = match archive.as_ref() {
Some(manifest) => manifest
.source_session_id
.parse::<session_store::SessionId>()
.ok(),
None => crate::retention::aggregate_session_id(
root.parent()
.ok_or(session_store::StoreError::SessionSourceMissing(session))?,
)
.map_err(|error| {
session_store::StoreError::Io(std::io::Error::other(error.to_string()))
})?,
};
if saved != Some(session) {
return Err(session_store::StoreError::SessionSourceMissing(session));
}
if intent == session_store::retention::SessionDeletionIntent::ErasePermanently {
crate::retention::remove_archived_session_copies(store.runtime_dir(), session)?;
let original = store
.runtime_dir()
.join("workers")
.join(owner.to_string())
.join("session");
if original != root
&& original.is_dir()
&& session_store::WorkerSessionStore::open_read_only(&original)?
.session_id()?
== Some(session)
{
std::fs::remove_dir_all(original)?;
}
}
match std::fs::remove_dir_all(&root) {
Ok(()) => Ok(()),
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(error) => Err(error.into()),
}
})
.map_err(crate::retention::retention_error)
}
#[cfg(feature = "fs-store")]
pub fn session_material_catalog_scoped(
&self,
scope: &RuntimeWorkspaceScope,
request: &runtime_api::SessionMaterialCatalogRequest,
) -> runtime_api::SessionMaterialCatalogAvailability {
crate::session_material::catalog(self, scope, request)
}
#[cfg(feature = "fs-store")]
pub fn session_material_read_scoped(
&self,
scope: &RuntimeWorkspaceScope,
request: &runtime_api::SessionMaterialReadRequest,
) -> runtime_api::SessionMaterialReadAvailability {
crate::session_material::read(self, scope, request)
}
#[cfg(feature = "fs-store")]
fn session_retention_store(
&self,
) -> Result<session_store::retention::SessionRetentionStore, RuntimeError> {
let state = self.lock()?;
let store = state.fs_store().ok_or_else(|| {
RuntimeError::InvalidRequest("Session retention requires an fs-backed Runtime".into())
})?;
Ok(crate::retention::runtime_session_retention(
store.runtime_dir(),
))
}
/// Prove an attributed but not yet materialized Session without treating a
/// missing committed log as empty. The source/Workspace binding is resolved first.
#[cfg(feature = "fs-store")]
pub(crate) fn session_not_materialized_scoped(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
expected_session_id: &str,
) -> Result<bool, RuntimeError> {
let (root, archive) = self.resolve_session_public_source(scope, source)?;
if archive.is_some() {
return Ok(false);
}
let session = expected_session_id
.parse::<session_store::SessionId>()
.map_err(|_| RuntimeError::InvalidRequest("Invalid Session identity".into()))?;
let retention = self.session_retention_store()?;
let _guard = retention
.lock_live(session)
.map_err(crate::retention::retention_error)?;
Ok(crate::retention::unmaterialized_session(
root.parent()
.ok_or_else(|| RuntimeError::InvalidRequest("Session root unavailable".into()))?,
&scope.workspace_id,
)? == Some(session))
}
/// Resolve the Backend-selected source without exposing local storage paths.
/// The saved active pointer, never a caller-provided Segment, selects lineage.
#[cfg(feature = "fs-store")]
pub fn session_public_material_reader_scoped(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
expected_session_id: &str,
) -> Result<
session_store::public_material::SessionPublicMaterialReader,
crate::worker_backend::SessionMaterialSourceError,
> {
use crate::worker_backend::SessionMaterialSourceError::Source;
use session_store::public_material::{PublicMaterialLimits, SessionPublicMaterialReader};
let (session_root, archive) = self
.resolve_session_public_source(scope, source)
.map_err(|error| Source(error.to_string()))?;
let reader = match (source, archive) {
(runtime_api::SessionPublicSource::Retained { worker_id }, None) => {
crate::worker_backend::open_session_public_material_reader(
session_root.parent().ok_or("Session root is unavailable")?,
&scope.workspace_id,
&WorkerRef::new(*worker_id),
expected_session_id,
)?
}
(runtime_api::SessionPublicSource::Archived { .. }, Some(manifest)) => {
if manifest.source_session_id != expected_session_id {
return Err(Source("Archive Session identity mismatch".into()));
}
SessionPublicMaterialReader::open_worker_with_retention(
&session_root,
manifest
.source_active_segment_id
.ok_or("Archive has no canonical active Segment")?,
PublicMaterialLimits::default(),
self.session_retention_store()
.map_err(|error| Source(error.to_string()))?,
)?
}
_ => return Err(Source("Session source is unavailable".into())),
};
if reader.session_id().to_string() != expected_session_id {
return Err(Source("Session identity mismatch".into()));
}
Ok(reader)
}
/// Search the committed public projection of one Backend-selected retained
/// Session or verified archive without restoring a Worker.
#[cfg(feature = "fs-store")]
@@ -4844,6 +5146,22 @@ impl Runtime {
Option<crate::retention::WorkerSessionArchiveManifest>,
),
RuntimeError,
> {
self.resolve_session_public_source_with_validation(scope, source, true)
}
#[cfg(feature = "fs-store")]
fn resolve_session_public_source_with_validation(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
verify_archive: bool,
) -> Result<
(
std::path::PathBuf,
Option<crate::retention::WorkerSessionArchiveManifest>,
),
RuntimeError,
> {
let state = self.lock()?;
let store = state.fs_store().ok_or_else(|| {
@@ -4876,7 +5194,11 @@ impl Runtime {
)
})?;
let provider = FsWorkerRetentionProvider::new(store.runtime_dir());
let (manifest, session_root) = provider.session_archive(archive_id)?;
let (manifest, session_root) = if verify_archive {
provider.session_archive(archive_id)?
} else {
provider.session_archive_identity(archive_id)?
};
if manifest.workspace_id != scope.workspace_id
|| manifest.source_runtime_id != runtime_id
{
@@ -4921,6 +5243,10 @@ impl Runtime {
&self,
request: &WorkerRetentionExecutionRequest,
) -> Result<WorkerRetentionExecutionResult, RuntimeError> {
let operation_lock = self.worker_operation_lock(request.worker_id)?;
let _operation_guard = operation_lock
.lock()
.map_err(|_| RuntimeError::StatePoisoned)?;
let mut state = self.lock()?;
let runtime_id = state.runtime_identity.clone().ok_or_else(|| {
RuntimeError::InvalidRequest(
@@ -6451,7 +6777,8 @@ fn validate_create_worker_request(request: &CreateWorkerRequest) -> Result<(), R
.validate()
.map_err(|message| RuntimeError::InvalidRequest(message.into()))?;
if request.workspace_api.is_none()
|| (binding.subjektiv_consolidation && !request.subjektiv_attached)
|| ((binding.subjektiv_consolidation || binding.subjektiv_journal)
&& !request.subjektiv_attached)
{
return Err(RuntimeError::InvalidRequest("Backend Job capability requires Workspace authority and consolidation requires trusted subject attachment".into()));
}
@@ -7302,6 +7629,7 @@ mod tests {
attempt_id: "attempt-1".into(),
input_digest: Some(format!("sha256:{}", "a".repeat(64))),
subjektiv_consolidation: false,
subjektiv_journal: false,
});
let owner = scope("local", "server-job-test");
let created = runtime
@@ -7351,6 +7679,7 @@ mod tests {
attempt_id: "attempt-1".into(),
input_digest: None,
subjektiv_consolidation: true,
subjektiv_journal: false,
});
assert!(validate_create_worker_request(&request).is_err());
request.workspace_api = Some(WorkspaceApiRef {
@@ -0,0 +1,166 @@
//! Bounded wire projection of canonical public material, never Memory previews.
use runtime_api::*;
use session_store::public_material::{
PublicMaterialDescriptor, PublicMaterialError, SessionPublicMaterialReader,
};
use sha2::{Digest, Sha256};
use crate::{Runtime, RuntimeWorkspaceScope, worker_backend::SessionMaterialSourceError};
fn descriptor(
value: &PublicMaterialDescriptor,
) -> Result<SessionMaterialDescriptor, PublicMaterialError> {
let result = SessionMaterialDescriptor {
segment_id: value.identity.segment_id.to_string(),
entry_ref: value.identity.entry_ref.clone(),
canonical_order: value.canonical_order,
origin_json: serde_json::to_string(&value.origin)?,
sha256: value.sha256.clone(),
total_bytes: value.total_bytes,
};
if serde_json::to_vec(&result)?.len() > 16 * 1024 {
return Err(PublicMaterialError::ResourceLimit);
}
Ok(result)
}
fn catalog_digest(reader: &SessionPublicMaterialReader) -> Result<String, PublicMaterialError> {
let mut digest = Sha256::new();
digest.update(reader.session_id().to_string());
digest.update(reader.active_segment_id().to_string());
for value in reader.descriptors() {
digest.update(serde_json::to_vec(&descriptor(value)?)?);
digest.update(b"\n");
}
Ok(digest
.finalize()
.iter()
.map(|byte| format!("{byte:02x}"))
.collect())
}
fn unavailable(error: SessionMaterialSourceError) -> SessionPublicUnavailableReason {
use PublicMaterialError as E;
use SessionPublicUnavailableReason as R;
match error {
SessionMaterialSourceError::Material(E::ResourceLimit) => R::ResourceLimit,
SessionMaterialSourceError::Material(E::Changed | E::InvalidOffset) => R::InvalidCursor,
SessionMaterialSourceError::Material(E::Missing | E::Erased) => R::RetentionMissing,
SessionMaterialSourceError::Material(E::Inconsistent | E::Record(_)) => R::CorruptLog,
_ => R::StorageUnavailable,
}
}
pub(crate) fn catalog(
runtime: &Runtime,
scope: &RuntimeWorkspaceScope,
request: &SessionMaterialCatalogRequest,
) -> SessionMaterialCatalogAvailability {
if scope.workspace_id == request.workspace_id
&& (1..=100).contains(&request.limit)
&& request.offset == 0
&& request.expected_catalog_sha256.is_none()
&& runtime
.session_not_materialized_scoped(scope, &request.source, &request.expected_session_id)
.is_ok_and(|empty| empty)
{
return SessionMaterialCatalogAvailability::NotMaterialized {
session_id: request.expected_session_id.clone(),
};
}
let result = (|| -> Result<_, SessionMaterialSourceError> {
if scope.workspace_id != request.workspace_id {
return Err("Session Workspace scope mismatch".into());
}
if request.limit == 0 || request.limit > 100 {
return Err(PublicMaterialError::ResourceLimit.into());
}
let reader = runtime.session_public_material_reader_scoped(
scope,
&request.source,
&request.expected_session_id,
)?;
let digest = catalog_digest(&reader)?;
if request
.expected_catalog_sha256
.as_ref()
.is_some_and(|expected| expected != &digest)
{
return Err(PublicMaterialError::Changed.into());
}
let offset =
usize::try_from(request.offset).map_err(|_| PublicMaterialError::InvalidOffset)?;
let count = reader.descriptors().len();
if offset > count {
return Err(PublicMaterialError::InvalidOffset.into());
}
let descriptors = reader
.descriptors()
.skip(offset)
.take(request.limit)
.map(descriptor)
.collect::<Result<Vec<_>, _>>()?;
let end = offset + descriptors.len();
Ok(SessionMaterialCatalogPage {
session_id: reader.session_id().to_string(),
active_segment_id: reader.active_segment_id().to_string(),
catalog_sha256: digest,
descriptors,
next_offset: (end < count).then_some(end as u64),
})
})();
match result {
Ok(page) => SessionMaterialCatalogAvailability::Page { page },
Err(error) => SessionMaterialCatalogAvailability::Unavailable {
reason: unavailable(error),
message: "Canonical Session catalog unavailable".into(),
},
}
}
pub(crate) fn read(
runtime: &Runtime,
scope: &RuntimeWorkspaceScope,
request: &SessionMaterialReadRequest,
) -> SessionMaterialReadAvailability {
let result = (|| -> Result<_, SessionMaterialSourceError> {
if scope.workspace_id != request.workspace_id {
return Err("Session Workspace scope mismatch".into());
}
if !(4..=16 * 1024).contains(&request.max_content_bytes) {
return Err(PublicMaterialError::ResourceLimit.into());
}
let reader = runtime.session_public_material_reader_scoped(
scope,
&request.source,
&request.expected_session_id,
)?;
// Resolve saved identity/order/digest here; no caller-supplied descriptor is adopted.
// New tail entries may arrive during a Job without changing its frozen originals.
let selected = reader
.descriptors()
.find(|value| {
value.identity.segment_id.to_string() == request.expected_descriptor.segment_id
&& value.identity.entry_ref == request.expected_descriptor.entry_ref
})
.ok_or(PublicMaterialError::Missing)?;
if descriptor(selected)? != request.expected_descriptor {
return Err(PublicMaterialError::Changed.into());
}
let content =
reader.read_content(selected, request.byte_offset, request.max_content_bytes)?;
Ok(SessionMaterialReadPage {
descriptor: descriptor(selected)?,
content: content.content,
next_offset: content.next_offset,
})
})();
match result {
Ok(page) => SessionMaterialReadAvailability::Page { page },
Err(error) => SessionMaterialReadAvailability::Unavailable {
reason: unavailable(error),
message: "Canonical Session material unavailable".into(),
},
}
}
@@ -364,6 +364,7 @@ fn api_archive_manifest(manifest: &DomainArchiveManifest) -> WorkerSessionArchiv
source_runtime_id: manifest.source_runtime_id.clone(),
source_worker_id: manifest.source_worker_id,
source_session_id: manifest.source_session_id.clone(),
source_active_segment_id: manifest.source_active_segment_id.map(|id| id.to_string()),
segment_ids: manifest.segment_ids.clone(),
source_created_at: manifest.source_created_at.clone(),
removed_at: manifest.removed_at.clone(),
@@ -0,0 +1,9 @@
//! One control namespace for live Worker sources and their verified archives.
//! It outlives aggregate removal and is also used by publishers and readers.
use session_store::retention::SessionRetentionStore;
use std::path::Path;
pub fn runtime_session_retention(runtime_root: &Path) -> SessionRetentionStore {
SessionRetentionStore::new(runtime_root.join("retention/session-sources"))
}
+118 -18
View File
@@ -432,6 +432,66 @@ impl WorkspacePromptProjectionCache {
}
}
#[derive(Debug, thiserror::Error)]
pub enum SessionMaterialSourceError {
#[error("{0}")]
Source(String),
#[error(transparent)]
Material(#[from] session_store::public_material::PublicMaterialError),
}
impl From<String> for SessionMaterialSourceError {
fn from(message: String) -> Self {
Self::Source(message)
}
}
impl From<&str> for SessionMaterialSourceError {
fn from(message: &str) -> Self {
Self::Source(message.into())
}
}
pub(crate) fn open_session_public_material_reader(
aggregate: &Path,
workspace_id: &str,
worker_ref: &WorkerRef,
expected_session_id: &str,
) -> Result<session_store::public_material::SessionPublicMaterialReader, SessionMaterialSourceError>
{
use session_store::public_material::{PublicMaterialLimits, SessionPublicMaterialReader};
let metadata = WorkerAggregateStore::open_read_only(
aggregate,
ProfileRuntimeWorkerFactory::runtime_worker_name_for_ref(worker_ref),
)
.and_then(|store| store.read_read_only())
.map_err(|error| format!("Session metadata unavailable: {error}"))?
.ok_or("Session metadata missing")?;
if metadata.workspace_id.as_deref() != Some(workspace_id) {
return Err("Session Workspace does not match the authorized source".into());
}
let active = metadata.active.ok_or("Session active pointer is missing")?;
if active.session_id.to_string() != expected_session_id {
return Err("Session identity does not match the authorized current Session".into());
}
let reader = SessionPublicMaterialReader::open_worker_with_retention(
&aggregate.join("session"),
active
.segment_id
.ok_or("Session active Segment is missing")?,
PublicMaterialLimits::default(),
crate::session_retention::runtime_session_retention(
aggregate
.parent()
.and_then(Path::parent)
.ok_or("Worker aggregate root is unavailable")?,
),
)?;
if reader.session_id() != active.session_id {
return Err("Session storage identity does not match its active pointer".into());
}
Ok(reader)
}
#[derive(Clone)]
pub struct ProfileRuntimeWorkerFactory {
observation_hub: Arc<RuntimeWorkerObservationHub>,
@@ -536,6 +596,34 @@ impl ProfileRuntimeWorkerFactory {
})
}
/// Open only the saved current Session of this Workspace's Worker. Paths and
/// restoration state remain inside the Runtime adapter.
pub fn session_public_material_reader(
&self,
workspace_id: &str,
worker_ref: &WorkerRef,
expected_session_id: &str,
) -> Result<
session_store::public_material::SessionPublicMaterialReader,
SessionMaterialSourceError,
> {
open_session_public_material_reader(
&self.worker_aggregate_dir(worker_ref)?,
workspace_id,
worker_ref,
expected_session_id,
)
}
fn session_retention(&self) -> Result<session_store::retention::SessionRetentionStore, String> {
let root = self
.worker_aggregate_root
.as_ref()
.and_then(|root| root.parent())
.ok_or("Runtime Worker aggregate root is not configured")?;
Ok(crate::session_retention::runtime_session_retention(root))
}
fn worker_run_dir(&self, worker_ref: &WorkerRef) -> Result<PathBuf, String> {
Ok(self
.worker_aggregate_dir(worker_ref)?
@@ -1097,9 +1185,10 @@ fn validate_backend_job_profile(
if let Some(binding) = &request.backend_job {
let policy = &manifest.feature.subjektiv.profile;
if policy.consolidation_tools != binding.subjektiv_consolidation
|| (binding.subjektiv_consolidation && policy.extraction.enabled)
|| ((binding.subjektiv_consolidation || binding.subjektiv_journal)
&& policy.extraction.enabled)
{
return Err("Backend Job consolidation_tools must match its trusted consolidation grant and extraction must be disabled".into());
return Err("Backend Job tools must match their independent trusted grants and extraction must be disabled".into());
}
}
Ok(())
@@ -1392,12 +1481,14 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
let session_dir = worker_aggregate_dir.join("session");
let session_store = WorkerSessionStore::new(&session_dir).map_err(|err| {
format!(
"failed to initialize canonical Worker Session store at {}: {err}",
session_dir.display()
)
})?;
let session_store =
WorkerSessionStore::new_with_retention(&session_dir, self.session_retention()?)
.map_err(|err| {
format!(
"failed to initialize canonical Worker Session store at {}: {err}",
session_dir.display()
)
})?;
let worker_metadata_store =
WorkerAggregateStore::new(&worker_aggregate_dir, worker_name.clone()).map_err(
|err| {
@@ -1545,8 +1636,14 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
if let Some(active) = metadata.active.as_ref()
&& let Some(segment_id) = active.segment_id
{
let session_store = WorkerSessionStore::new(worker_aggregate_dir.join("session"))
.map_err(|error| format!("failed to read Worker Session store: {error}"))?;
// Preflight must not create lock files or migrate retained sources.
// Actual restore reacquires the live guard before any publication.
self.session_retention()?
.ensure_live(active.session_id)
.map_err(|error| format!("Worker Session source is unavailable: {error}"))?;
let session_store =
WorkerSessionStore::open_read_only(worker_aggregate_dir.join("session"))
.map_err(|error| format!("failed to read Worker Session store: {error}"))?;
if !session_store
.exists(active.session_id, segment_id)
.map_err(|error| format!("failed to inspect Worker Session segment: {error}"))?
@@ -1635,12 +1732,14 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
let worker_aggregate_dir = self.worker_aggregate_dir(&request.worker_ref)?;
let session_dir = worker_aggregate_dir.join("session");
let session_store = WorkerSessionStore::new(&session_dir).map_err(|err| {
format!(
"failed to initialize canonical Worker Session store at {}: {err}",
session_dir.display()
)
})?;
let session_store =
WorkerSessionStore::new_with_retention(&session_dir, self.session_retention()?)
.map_err(|err| {
format!(
"failed to initialize canonical Worker Session store at {}: {err}",
session_dir.display()
)
})?;
let worker_metadata_store =
WorkerAggregateStore::new(&worker_aggregate_dir, worker_name.clone()).map_err(
|err| {
@@ -7360,18 +7459,19 @@ mod tests {
)
.unwrap();
let mut request = create_request("policy");
for granted in [false, true] {
for (granted, journal) in [(false, false), (true, false), (false, true)] {
request.backend_job = Some(crate::catalog::BackendJobExecutionBinding {
job_id: "job-1".into(),
attempt_id: "attempt-1".into(),
input_digest: None,
subjektiv_consolidation: granted,
subjektiv_journal: journal,
});
for tools in [false, true] {
for extraction in [false, true] {
manifest.feature.subjektiv.profile.consolidation_tools = tools;
manifest.feature.subjektiv.profile.extraction.enabled = extraction;
let expected = tools == granted && (!granted || !extraction);
let expected = tools == granted && (!(granted || journal) || !extraction);
let before = serde_json::to_value(&manifest).unwrap();
assert_eq!(
validate_backend_job_profile(&manifest, &request).is_ok(),