chore: merge develop into hare/develop

This commit is contained in:
2026-10-03 19:14:17 +09:00
147 changed files with 33175 additions and 2270 deletions
+4
View File
@@ -298,6 +298,10 @@ pub struct CreateWorkerRequest {
/// Backend-authored immutable Workspace Memory settings snapshot.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub memory_settings: Option<manifest::WorkspaceMemorySettingsSnapshot>,
/// Backend-attested subject or consolidation attachment. A reusable Profile
/// may enable subjektiv policy, but only this host-owned bit activates it.
#[serde(default, skip_serializing_if = "is_false")]
pub subjektiv_attached: bool,
}
/// Last persisted Worker lifecycle status.
+7 -2
View File
@@ -1334,7 +1334,6 @@ async fn runtime_protocol_ws_session(
request_id,
subscription_id: subscription_id.clone(),
selector: selector.clone(),
snapshot_revision: subscription.snapshot_revision(),
snapshot: subscription.snapshot().clone(),
},
));
@@ -1354,7 +1353,6 @@ async fn runtime_protocol_ws_session(
SubscriptionFramePayload::Event(
SubscriptionEvent::Event {
subscription_id: event_subscription_id.clone(),
subject_revision: update.subject_revision,
payload: update.payload,
},
),
@@ -2314,6 +2312,9 @@ fn required_runtime_permission(method: &Method, path: &str) -> Option<&'static s
if path.starts_with("/v1/workers/") && *method == Method::DELETE {
return Some("workers:delete");
}
if path.starts_with("/v1/session-public/") && *method == Method::POST {
return Some("workers:read");
}
if path.starts_with("/v1/workers/") && *method == Method::GET {
return Some("workers:read");
}
@@ -3101,6 +3102,7 @@ mod tests {
settings_revision: 1,
language: "English".to_string(),
}),
subjektiv_attached: false,
}
}
@@ -3994,6 +3996,8 @@ mod ws_tests {
tools: Vec::new(),
context_window: 0,
context_tokens: 0,
reasoning: None,
context_usage: None,
},
state: protocol::WorkerStateSnapshot::initial(),
in_flight: protocol::InFlightSnapshot {
@@ -4078,6 +4082,7 @@ mod ws_tests {
settings_revision: 1,
language: "English".to_string(),
}),
subjektiv_attached: false,
}
}
@@ -289,6 +289,80 @@ impl runtime_api::RuntimeApi for RuntimeManagementApi {
.map_err(api_error)
}
async fn session_public_search(
&self,
request: runtime_api::SessionPublicSearchRequest,
) -> Result<runtime_api::SessionPublicSearchAvailability, runtime_api::RuntimeApiError> {
let scope = auth_workspace_scope(&self.state, auth_extension().as_ref())
.map_err(api_error)?
.ok_or_else(|| {
runtime_api::RuntimeApiError::new(
StatusCode::FORBIDDEN.as_u16(),
"runtime_session_public_scope_required",
"Session public search requires a Workspace-scoped capability",
)
})?;
if scope.workspace_id != request.workspace_id {
return Err(runtime_api::RuntimeApiError::new(
StatusCode::FORBIDDEN.as_u16(),
"runtime_session_public_workspace_scope_mismatch",
"Session public search Workspace scope does not match the authenticated capability",
));
}
#[cfg(feature = "fs-store")]
{
self.state
.runtime
.session_public_search_scoped(&scope, &request)
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)
}
#[cfg(not(feature = "fs-store"))]
{
Ok(runtime_api::SessionPublicSearchAvailability::Unavailable {
reason: runtime_api::SessionPublicUnavailableReason::StorageUnavailable,
message: "Session public storage is unavailable".to_string(),
})
}
}
async fn session_public_read(
&self,
request: runtime_api::SessionPublicReadRequest,
) -> Result<runtime_api::SessionPublicReadAvailability, runtime_api::RuntimeApiError> {
let scope = auth_workspace_scope(&self.state, auth_extension().as_ref())
.map_err(api_error)?
.ok_or_else(|| {
runtime_api::RuntimeApiError::new(
StatusCode::FORBIDDEN.as_u16(),
"runtime_session_public_scope_required",
"Session public read requires a Workspace-scoped capability",
)
})?;
if scope.workspace_id != request.workspace_id {
return Err(runtime_api::RuntimeApiError::new(
StatusCode::FORBIDDEN.as_u16(),
"runtime_session_public_workspace_scope_mismatch",
"Session public read Workspace scope does not match the authenticated capability",
));
}
#[cfg(feature = "fs-store")]
{
self.state
.runtime
.session_public_read_scoped(&scope, &request)
.map_err(RuntimeHttpRestError::runtime)
.map_err(api_error)
}
#[cfg(not(feature = "fs-store"))]
{
Ok(runtime_api::SessionPublicReadAvailability::Unavailable {
reason: runtime_api::SessionPublicUnavailableReason::StorageUnavailable,
message: "Session public storage is unavailable".to_string(),
})
}
}
async fn create_worker(
&self,
value: runtime_api::CreateWorkerRequest,
+6 -1
View File
@@ -25,6 +25,8 @@ pub mod resource;
#[cfg(feature = "fs-store")]
pub mod retention;
mod runtime;
#[cfg(feature = "fs-store")]
mod session_public;
pub mod ssh_host_key_probe;
pub mod worker_backend;
pub mod worker_source;
@@ -36,5 +38,8 @@ pub mod workspace_request;
#[cfg(feature = "fs-store")]
pub use fs_store::{FsRuntimeStore, FsRuntimeStoreOptions};
pub use management::RuntimeOptions;
pub use runtime::{Runtime, RuntimeWorkspaceScope};
pub use runtime::{
Runtime, RuntimeEventSelectorSubscription, RuntimeSubscriptionRecvError,
RuntimeSubscriptionUpdate, RuntimeWorkspaceScope,
};
pub use session_store::UploadedFileUploadContext;
+20
View File
@@ -356,6 +356,26 @@ impl FsWorkerRetentionProvider {
.join(archive_id))
}
pub(crate) fn session_archive(
&self,
archive_id: &str,
) -> Result<(WorkerSessionArchiveManifest, PathBuf), RuntimeError> {
let archive_dir = self.archive_dir(archive_id)?;
let manifest: WorkerSessionArchiveManifest = read_json(
&archive_dir.join("manifest.json"),
"read Worker Session archive",
)?;
if manifest.schema_version != ARCHIVE_SCHEMA_VERSION || manifest.archive_id != archive_id {
return Err(RuntimeError::StoreCorrupt {
operation: "read Worker Session archive",
path: archive_dir.join("manifest.json"),
message: "archive manifest identity mismatch".to_string(),
});
}
let manifest = validate_existing_archive(&archive_dir, &manifest)?;
Ok((manifest, archive_dir.join("session")))
}
fn diagnostics_dir(&self, operation_id: &str) -> Result<PathBuf, RuntimeError> {
validate_id("operation_id", operation_id)?;
Ok(self
+256 -60
View File
@@ -77,6 +77,41 @@ impl RuntimeWorkspaceScope {
}
const SUBSCRIPTION_QUEUE_CAPACITY: usize = 256;
#[cfg(feature = "fs-store")]
fn session_public_source_unavailable(
source: &runtime_api::SessionPublicSource,
error: &RuntimeError,
) -> Option<(runtime_api::SessionPublicUnavailableReason, &'static str)> {
use runtime_api::SessionPublicUnavailableReason as Reason;
match (source, error) {
(
runtime_api::SessionPublicSource::Retained { .. },
RuntimeError::WorkerNotFound { .. },
) => Some((
Reason::RetentionMissing,
"retained Session storage is missing",
)),
(runtime_api::SessionPublicSource::Archived { .. }, RuntimeError::StoreMissing { .. }) => {
Some((
Reason::ArchiveIncomplete,
"committed Session archive is incomplete",
))
}
(runtime_api::SessionPublicSource::Archived { .. }, RuntimeError::StoreCorrupt { .. }) => {
Some((
Reason::ArchiveIncomplete,
"committed Session archive failed integrity validation",
))
}
(_, RuntimeError::StoreIo { .. } | RuntimeError::StoreCommitOutcomeUnknown { .. }) => {
Some((Reason::StorageUnavailable, "Session storage is unavailable"))
}
_ => None,
}
}
const WORKER_DELETE_FAILURE_DIAGNOSTIC_CODE: &str = "worker_delete_persistence_failed";
const WORKER_DELETE_FAILURE_DIAGNOSTIC_MESSAGE: &str =
"Worker metadata deletion failed; the persisted Worker identity was retained for retry";
@@ -151,7 +186,6 @@ impl RuntimeWorkerRestoreResult {
#[derive(Clone, Debug)]
pub struct RuntimeSubscriptionUpdate {
pub subject_revision: u64,
pub payload: SubscriptionEventPayload,
}
@@ -168,7 +202,6 @@ pub enum RuntimeSubscriptionRecvError {
pub struct RuntimeEventSelectorSubscription {
subscription_id: u64,
selector: EventSubscriptionSelector,
snapshot_revision: u64,
snapshot: SubscriptionSnapshot,
receiver: mpsc::Receiver<RuntimeSubscriptionUpdate>,
lagged: Arc<AtomicBool>,
@@ -180,10 +213,6 @@ impl RuntimeEventSelectorSubscription {
&self.selector
}
pub fn snapshot_revision(&self) -> u64 {
self.snapshot_revision
}
pub fn snapshot(&self) -> &SubscriptionSnapshot {
&self.snapshot
}
@@ -1280,7 +1309,6 @@ impl Runtime {
state.persist_runtime_snapshot()?;
}
let snapshot = state.subscription_snapshot(scope, &selector)?;
let snapshot_revision = state.subscription_revision;
let subscription_id = state.next_event_subscription_id;
state.next_event_subscription_id = state.next_event_subscription_id.saturating_add(1);
let (sender, receiver) = mpsc::channel(SUBSCRIPTION_QUEUE_CAPACITY);
@@ -1298,7 +1326,6 @@ impl Runtime {
Ok(RuntimeEventSelectorSubscription {
subscription_id,
selector,
snapshot_revision,
snapshot,
receiver,
lagged,
@@ -2217,9 +2244,9 @@ impl Runtime {
if let Some(payload) = input_protocol_event(&input) {
state.push_worker_observation_event(worker_ref.clone(), payload);
}
state.publish_worker_upsert(worker_ref.worker_id)?;
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
state.publish_worker_upsert(worker_ref.worker_id)?;
Ok(WorkerInteractionAck {
worker_ref: worker_ref.clone(),
@@ -2500,9 +2527,9 @@ impl Runtime {
worker.workdir_attachments = workdir_attachments;
worker.detail()
};
state.publish_worker_upsert(worker_ref.worker_id)?;
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
state.publish_worker_upsert(worker_ref.worker_id)?;
Ok(detail)
}
@@ -2541,8 +2568,8 @@ impl Runtime {
state
.observation_events
.retain(|event| event.worker_ref != *worker_ref);
state.publish_worker_removed(worker_ref.worker_id, workspace_id.as_deref())?;
state.persist_runtime_snapshot()?;
state.publish_worker_removed(worker_ref.worker_id, workspace_id.as_deref())?;
}
Ok(())
}
@@ -2564,9 +2591,9 @@ impl Runtime {
let mut state = self.lock()?;
let worker = state.worker_mut(worker_ref)?;
worker.apply_worker_state(&snapshot);
state.publish_worker_upsert(worker_ref.worker_id)?;
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
state.publish_worker_upsert(worker_ref.worker_id)?;
Ok(())
}
@@ -2870,8 +2897,8 @@ impl Runtime {
state
.observation_events
.retain(|event| event.worker_ref != *worker_ref);
state.publish_worker_removed(worker_ref.worker_id, removed_workspace_id.as_deref())?;
state.persist_runtime_snapshot()?;
state.publish_worker_removed(worker_ref.worker_id, removed_workspace_id.as_deref())?;
Ok(WorkerDeleteResult {
worker_id: removed.worker_id,
deleted: true,
@@ -2959,13 +2986,13 @@ impl Runtime {
let worker_state_changed =
state.project_protocol_event_to_worker_state(worker_ref, &payload);
let activity_changed = state.project_internal_worker_activity(worker_ref, &payload);
if worker_state_changed || activity_changed {
state.publish_worker_upsert(worker_ref.worker_id)?;
}
if worker_state_changed {
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
}
if worker_state_changed || activity_changed {
state.publish_worker_upsert(worker_ref.worker_id)?;
}
let event = state.push_worker_observation_event(worker_ref.clone(), payload);
Ok(event)
}
@@ -3021,13 +3048,13 @@ impl Runtime {
state.project_protocol_event_to_worker_state(&worker_ref, &payload);
let activity_changed =
state.project_internal_worker_activity(&worker_ref, &payload);
if worker_state_changed || activity_changed {
state.publish_worker_upsert(worker_ref.worker_id)?;
}
if worker_state_changed {
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
}
if worker_state_changed || activity_changed {
state.publish_worker_upsert(worker_ref.worker_id)?;
}
Ok(state.push_worker_observation_event(worker_ref, payload))
}),
)
@@ -3054,13 +3081,13 @@ impl Runtime {
state.project_protocol_event_to_worker_state(&worker_ref, &payload);
let activity_changed =
state.project_internal_worker_activity(&worker_ref, &payload);
if worker_state_changed || activity_changed {
state.publish_worker_upsert(worker_ref.worker_id)?;
}
if worker_state_changed {
state.persist_runtime_snapshot()?;
state.persist_worker(&worker_ref.worker_id)?;
}
if worker_state_changed || activity_changed {
state.publish_worker_upsert(worker_ref.worker_id)?;
}
Ok(state.push_worker_observation_event(worker_ref, payload))
}),
)
@@ -3454,8 +3481,8 @@ impl Runtime {
logical_workdir_attachments_from_statuses(&workdir_attachments, claims);
candidate.workdir_attachments = workdir_attachments;
state.workers.insert(worker_ref.worker_id, candidate);
state.publish_worker_upsert(worker_ref.worker_id)?;
state.persist_worker(&worker_ref.worker_id)?;
state.publish_worker_upsert(worker_ref.worker_id)?;
Ok(())
}
@@ -3551,6 +3578,123 @@ impl Runtime {
)
}
/// Search the committed public projection of one Backend-selected retained
/// Session or verified archive without restoring a Worker.
#[cfg(feature = "fs-store")]
pub fn session_public_search_scoped(
&self,
scope: &RuntimeWorkspaceScope,
request: &runtime_api::SessionPublicSearchRequest,
) -> Result<runtime_api::SessionPublicSearchAvailability, RuntimeError> {
let (session_root, archive) =
match self.resolve_session_public_source(scope, &request.source) {
Ok(source) => source,
Err(error) => {
if let Some((reason, message)) =
session_public_source_unavailable(&request.source, &error)
{
return Ok(runtime_api::SessionPublicSearchAvailability::Unavailable {
reason,
message: message.to_string(),
});
}
return Err(error);
}
};
Ok(crate::session_public::search(
&session_root,
request,
archive.as_ref(),
))
}
/// Read one exact public entry from a Backend-selected retained Session or
/// verified archive. No fallback Worker, Segment, or entry is selected.
#[cfg(feature = "fs-store")]
pub fn session_public_read_scoped(
&self,
scope: &RuntimeWorkspaceScope,
request: &runtime_api::SessionPublicReadRequest,
) -> Result<runtime_api::SessionPublicReadAvailability, RuntimeError> {
let (session_root, archive) =
match self.resolve_session_public_source(scope, &request.source) {
Ok(source) => source,
Err(error) => {
if let Some((reason, message)) =
session_public_source_unavailable(&request.source, &error)
{
return Ok(runtime_api::SessionPublicReadAvailability::Unavailable {
reason,
message: message.to_string(),
});
}
return Err(error);
}
};
Ok(crate::session_public::read(
&session_root,
request,
archive.as_ref(),
))
}
#[cfg(feature = "fs-store")]
fn resolve_session_public_source(
&self,
scope: &RuntimeWorkspaceScope,
source: &runtime_api::SessionPublicSource,
) -> Result<
(
std::path::PathBuf,
Option<crate::retention::WorkerSessionArchiveManifest>,
),
RuntimeError,
> {
let state = self.lock()?;
let store = state.fs_store().ok_or_else(|| {
RuntimeError::InvalidRequest(
"Session public reads require an fs-backed Runtime".to_string(),
)
})?;
match source {
runtime_api::SessionPublicSource::Retained { worker_id } => {
let worker_ref = WorkerRef::new(*worker_id);
let worker = state.worker(&worker_ref)?;
if worker.workspace_id.as_deref() != Some(scope.workspace_id.as_str()) {
return Err(RuntimeError::WorkerNotFound {
worker_id: *worker_id,
});
}
Ok((
store
.runtime_dir()
.join("workers")
.join(worker_id.to_string())
.join("session"),
None,
))
}
runtime_api::SessionPublicSource::Archived { archive_id } => {
let runtime_id = state.runtime_identity.as_deref().ok_or_else(|| {
RuntimeError::InvalidRequest(
"Runtime identity is not bound for Session archive reads".to_string(),
)
})?;
let provider = FsWorkerRetentionProvider::new(store.runtime_dir());
let (manifest, session_root) = provider.session_archive(archive_id)?;
if manifest.workspace_id != scope.workspace_id
|| manifest.source_runtime_id != runtime_id
{
return Err(RuntimeError::InvalidRequest(
"Session archive scope does not match the authenticated Runtime capability"
.to_string(),
));
}
Ok((session_root, Some(manifest)))
}
}
}
/// Enumerate host-authoritative Runtime inventory for Backend orphan
/// reconciliation. Runtime identity and Workspace scope are derived here,
/// not accepted in a diagnostic payload.
@@ -3702,8 +3846,6 @@ struct RuntimeState {
workspace_config_latest: BTreeMap<String, ConfigBundleRef>,
workspace_config_fetch_gates: BTreeMap<String, Arc<Mutex<()>>>,
diagnostics: Vec<RuntimeDiagnostic>,
subscription_revision: u64,
worker_subject_revisions: BTreeMap<WorkerId, u64>,
next_event_subscription_id: u64,
subscriptions: BTreeMap<u64, SubscriptionSink>,
#[cfg(feature = "ws-server")]
@@ -3733,8 +3875,6 @@ impl RuntimeState {
workspace_config_latest: BTreeMap::new(),
workspace_config_fetch_gates: BTreeMap::new(),
diagnostics: Vec::new(),
subscription_revision: 0,
worker_subject_revisions: BTreeMap::new(),
next_event_subscription_id: 1,
subscriptions: BTreeMap::new(),
#[cfg(feature = "ws-server")]
@@ -3765,8 +3905,6 @@ impl RuntimeState {
workspace_config_latest: BTreeMap::new(),
workspace_config_fetch_gates: BTreeMap::new(),
diagnostics: Vec::new(),
subscription_revision: 0,
worker_subject_revisions: BTreeMap::new(),
next_event_subscription_id: 1,
subscriptions: BTreeMap::new(),
#[cfg(feature = "ws-server")]
@@ -3895,8 +4033,6 @@ impl RuntimeState {
workspace_config_fetch_gates: BTreeMap::new(),
workspace_owners: persisted.workspace_owners,
diagnostics,
subscription_revision: 0,
worker_subject_revisions: BTreeMap::new(),
next_event_subscription_id: 1,
subscriptions: BTreeMap::new(),
#[cfg(feature = "ws-server")]
@@ -4215,11 +4351,6 @@ impl RuntimeState {
runtime_id: None,
resource_key: None,
availability: protocol::subscription::SubscriptionWorkerAvailability::Observed,
subject_revision: self
.worker_subject_revisions
.get(&worker.worker_id)
.copied()
.unwrap_or(0),
worker_state: worker.worker_state.clone(),
state: subscription_worker_state(worker.status),
has_running_internal_workers: worker.internal_workers.has_running_worker(),
@@ -4232,25 +4363,17 @@ impl RuntimeState {
}
fn publish_worker_upsert(&mut self, worker_id: WorkerId) -> Result<(), RuntimeError> {
self.subscription_revision = self.subscription_revision.saturating_add(1);
let subject_revision = {
let revision = self.worker_subject_revisions.entry(worker_id).or_insert(0);
*revision = revision.saturating_add(1);
*revision
};
let worker = self
.workers
.get(&worker_id)
.ok_or(RuntimeError::WorkerNotFound { worker_id })?;
let workspace_id = worker.workspace_id.clone();
let mut projected = self.subscription_worker(worker)?;
projected.subject_revision = subject_revision;
let projected = self.subscription_worker(worker)?;
let projected_worker_id = projected.worker_id.clone();
self.deliver_worker_subscription_update(
&projected_worker_id,
workspace_id.as_deref(),
RuntimeSubscriptionUpdate {
subject_revision,
payload: SubscriptionEventPayload::WorkerUpserted { worker: projected },
},
);
@@ -4262,19 +4385,12 @@ impl RuntimeState {
worker_id: WorkerId,
workspace_id: Option<&str>,
) -> Result<(), RuntimeError> {
self.subscription_revision = self.subscription_revision.saturating_add(1);
let subject_revision = {
let revision = self.worker_subject_revisions.entry(worker_id).or_insert(0);
*revision = revision.saturating_add(1);
*revision
};
let worker_id = SubscriptionWorkerId::new(worker_id.to_string())
.map_err(subscription_validation_error)?;
self.deliver_worker_subscription_update(
&worker_id,
workspace_id,
RuntimeSubscriptionUpdate {
subject_revision,
payload: SubscriptionEventPayload::WorkerRemoved {
worker_id: worker_id.clone(),
runtime_id: None,
@@ -4417,8 +4533,8 @@ impl RuntimeState {
worker_ref: Some(worker_ref.clone()),
});
self.workers.insert(worker_ref.worker_id, candidate);
self.publish_worker_upsert(worker_ref.worker_id)?;
self.persist_runtime_snapshot()?;
self.publish_worker_upsert(worker_ref.worker_id)?;
Ok(())
}
@@ -5520,6 +5636,7 @@ mod tests {
worker,
revision,
status,
greeting: None,
session: protocol::SessionSnapshot {
pending_submissions: protocol::PendingSubmissionsSnapshot::default(),
entries: Vec::new(),
@@ -5545,6 +5662,8 @@ mod tests {
tools: Vec::new(),
context_window: 0,
context_tokens: 0,
reasoning: None,
context_usage: None,
},
state: protocol::WorkerStatus::Idle.into(),
in_flight: protocol::InFlightSnapshot::default(),
@@ -5883,6 +6002,7 @@ mod tests {
settings_revision: 1,
language: "English".to_string(),
}),
subjektiv_attached: false,
}
}
@@ -6647,7 +6767,6 @@ mod tests {
let mut subscription = runtime
.subscribe_event_selector(EventSubscriptionSelector::RuntimeWorkers)
.unwrap();
assert_eq!(subscription.snapshot_revision(), 0);
let SubscriptionSnapshot::Workers { workers } = subscription.snapshot() else {
panic!("runtime_workers must return a Worker snapshot");
};
@@ -6655,11 +6774,9 @@ mod tests {
let created = runtime.create_worker(task_request("live")).unwrap();
let update = receive_subscription_update(&mut subscription).unwrap();
assert_eq!(update.subject_revision, 1);
match update.payload {
SubscriptionEventPayload::WorkerUpserted { worker } => {
assert_eq!(worker.worker_id.as_str(), created.worker_id.to_string());
assert_eq!(worker.subject_revision, 1);
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
}
payload => panic!("unexpected subscription payload: {payload:?}"),
@@ -6676,7 +6793,6 @@ mod tests {
)
.unwrap();
let update = receive_subscription_update(&mut subscription).unwrap();
assert_eq!(update.subject_revision, 2);
match update.payload {
SubscriptionEventPayload::WorkerUpserted { worker } => {
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
@@ -6695,7 +6811,6 @@ mod tests {
)
.unwrap();
let update = receive_subscription_update(&mut subscription).unwrap();
assert_eq!(update.subject_revision, 3);
match update.payload {
SubscriptionEventPayload::WorkerUpserted { worker } => {
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
@@ -6737,7 +6852,6 @@ mod tests {
)
.unwrap();
let update = receive_subscription_update(&mut subscription).unwrap();
assert_eq!(update.subject_revision, 4);
match update.payload {
SubscriptionEventPayload::WorkerUpserted { worker } => {
assert_eq!(worker.state, SubscriptionWorkerState::Idle);
@@ -6748,7 +6862,6 @@ mod tests {
runtime.stop_worker(&created.worker_ref, None).unwrap();
let update = receive_subscription_update(&mut subscription).unwrap();
assert_eq!(update.subject_revision, 5);
match update.payload {
SubscriptionEventPayload::WorkerUpserted { worker } => {
assert_eq!(worker.worker_id.as_str(), created.worker_id.to_string());
@@ -6760,6 +6873,87 @@ mod tests {
runtime.stop_runtime().unwrap();
}
#[test]
fn concurrent_snapshot_and_worker_creation_cross_the_boundary_exactly_once() {
let runtime = runtime_with_backend();
let barrier = Arc::new(Barrier::new(2));
let subscribing_runtime = runtime.clone();
let subscribing_barrier = barrier.clone();
let subscriber = std::thread::spawn(move || {
subscribing_barrier.wait();
subscribing_runtime
.subscribe_event_selector(EventSubscriptionSelector::RuntimeWorkers)
.unwrap()
});
let creating_runtime = runtime.clone();
let creating_barrier = barrier.clone();
let creator = std::thread::spawn(move || {
creating_barrier.wait();
creating_runtime
.create_worker(task_request("concurrent-boundary"))
.unwrap()
});
let mut subscription = subscriber.join().unwrap();
let created = creator.join().unwrap();
let SubscriptionSnapshot::Workers { workers } = subscription.snapshot() else {
panic!("runtime_workers must return a Worker snapshot");
};
let in_snapshot = workers
.iter()
.any(|worker| worker.worker_id.as_str() == created.worker_id.to_string());
let in_event = match subscription.receiver.try_recv() {
Ok(RuntimeSubscriptionUpdate {
payload: SubscriptionEventPayload::WorkerUpserted { worker },
}) => worker.worker_id.as_str() == created.worker_id.to_string(),
Ok(update) => panic!("unexpected subscription update: {update:?}"),
Err(mpsc::error::TryRecvError::Empty) => false,
Err(error) => panic!("unexpected subscription receive error: {error}"),
};
assert_ne!(
in_snapshot, in_event,
"the concurrent creation must appear in the snapshot or its ordered suffix, never both"
);
}
#[cfg(feature = "fs-store")]
#[test]
fn restarted_runtime_subscription_starts_from_persisted_authoritative_snapshot() {
let root = tempfile::tempdir().unwrap();
let options = crate::fs_store::FsRuntimeStoreOptions {
root: root.path().join("runtime"),
runtime_id: "subscription-restart".to_string(),
display_name: None,
};
let runtime = Runtime::with_fs_store_and_execution_backend(
options.clone(),
Arc::new(TestExecutionBackend::default()),
)
.unwrap();
runtime.store_config_bundle(test_bundle()).unwrap();
let worker = runtime
.create_worker(task_request("persisted-subscription-worker"))
.unwrap();
drop(runtime);
let restarted = Runtime::with_fs_store_and_execution_backend(
options,
Arc::new(TestExecutionBackend::default()),
)
.unwrap();
let subscription = restarted
.subscribe_event_selector(EventSubscriptionSelector::RuntimeWorkers)
.unwrap();
let SubscriptionSnapshot::Workers { workers } = subscription.snapshot() else {
panic!("runtime_workers must return a Worker snapshot");
};
assert!(workers.iter().any(|candidate| {
candidate.worker_id.as_str() == worker.worker_id.to_string()
&& candidate.availability
== protocol::subscription::SubscriptionWorkerAvailability::Observed
}));
}
#[test]
fn worker_lifecycle_subscription_delivers_only_selected_workers() {
let runtime = runtime_with_backend();
@@ -8108,6 +8302,8 @@ mod tests {
tools: Vec::new(),
context_window: 128,
context_tokens: 64,
reasoning: None,
context_usage: None,
},
state: protocol::WorkerStatus::Running.into(),
in_flight: protocol::InFlightSnapshot {
+622
View File
@@ -0,0 +1,622 @@
use std::path::Path;
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
use runtime_api::{
SessionPublicEntryKind, SessionPublicLineage, SessionPublicLineageKind,
SessionPublicReadAvailability, SessionPublicReadMode, SessionPublicReadPage,
SessionPublicReadRequest, SessionPublicSearchAvailability, SessionPublicSearchItem,
SessionPublicSearchPage, SessionPublicSearchRequest, SessionPublicToolPart,
SessionPublicUnavailableReason, WorkerSessionArchiveManifest,
};
use serde::{Deserialize, Serialize};
use session_store::{
SessionPublicIndexEntry, SessionPublicIndexEntryKind, SessionPublicIndexLimits,
SessionPublicIndexLineage, SessionPublicIndexOriginKind, SessionPublicIndexReadError,
SessionPublicIndexScanPosition, SessionPublicIndexToolPart, read_session_public_index_entry,
read_session_public_index_page,
};
use crate::retention::WorkerSessionArchiveManifest as DomainArchiveManifest;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
struct SessionPublicRuntimeCursor {
generation: String,
position: SessionPublicIndexScanPosition,
}
pub(crate) fn search(
session_root: &Path,
request: &SessionPublicSearchRequest,
archive_manifest: Option<&DomainArchiveManifest>,
) -> SessionPublicSearchAvailability {
if request.limit == 0 || request.limit > 100 {
return unavailable(
SessionPublicUnavailableReason::ResourceLimit,
"Session public search limit must be between 1 and 100",
);
}
let cursor = match request.scan_cursor.as_deref().map(decode_scan_cursor) {
Some(Ok(cursor)) => Some(cursor),
Some(Err(())) => {
return unavailable(
SessionPublicUnavailableReason::InvalidCursor,
"Session public search cursor is invalid",
);
}
None => None,
};
if let (Some(expected), Some(cursor)) =
(request.expected_generation.as_deref(), cursor.as_ref())
&& expected != cursor.generation
{
return unavailable(
SessionPublicUnavailableReason::InvalidCursor,
"Session public search cursor generation changed",
);
}
let query = request
.query
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_lowercase);
let expected_generation = request
.expected_generation
.as_deref()
.or_else(|| cursor.as_ref().map(|cursor| cursor.generation.as_str()));
let index = match read_session_public_index_page(
session_root,
SessionPublicIndexLimits {
max_bytes: request.max_scan_bytes,
max_segments: request.max_segments,
max_entries: request.max_entries,
},
expected_generation,
cursor.as_ref().map(|cursor| cursor.position.clone()),
request.limit,
|entry| matches_entry(entry, request, query.as_deref()),
) {
Ok(index) => index,
Err(error) => {
let (reason, message) = map_index_error(error);
return unavailable(reason, message);
}
};
if index.session_id != request.expected_session_id {
return unavailable(
SessionPublicUnavailableReason::NotFound,
"Session identity does not match the authorized source",
);
}
let mut items = Vec::with_capacity(index.entries.len());
for item in index.entries {
let scan_cursor = match encode_scan_cursor(&SessionPublicRuntimeCursor {
generation: index.generation.clone(),
position: item.scan_from,
}) {
Ok(cursor) => cursor,
Err(()) => {
return unavailable(
SessionPublicUnavailableReason::StorageUnavailable,
"Session public search cursor could not be encoded",
);
}
};
let entry = item.entry;
items.push(SessionPublicSearchItem {
segment_id: item.segment_id,
entry_ref: entry.entry_ref,
kind: entry_kind(entry.kind),
origin: entry.provenance,
tool_name: entry.tool_name,
tool_part: entry.tool_part.map(tool_part),
compact: entry.compact_text,
compact_truncated: entry.compact_text_truncated,
lineage: lineage(&item.lineage),
scan_cursor,
});
}
let next_scan_cursor = match index.next_position {
Some(position) => match encode_scan_cursor(&SessionPublicRuntimeCursor {
generation: index.generation.clone(),
position,
}) {
Ok(cursor) => Some(cursor),
Err(()) => {
return unavailable(
SessionPublicUnavailableReason::StorageUnavailable,
"Session public search cursor could not be encoded",
);
}
},
None => None,
};
SessionPublicSearchAvailability::Page {
page: SessionPublicSearchPage {
session_id: index.session_id,
generation: index.generation,
items,
next_scan_cursor,
has_more: index.has_more,
scanned_bytes: index.scanned_bytes,
scanned_segments: index.scanned_segments,
scanned_entries: index.scanned_entries,
archive_manifest: archive_manifest.map(api_archive_manifest),
},
}
}
fn encode_scan_cursor(cursor: &SessionPublicRuntimeCursor) -> Result<String, ()> {
serde_json::to_vec(cursor)
.map(|bytes| URL_SAFE_NO_PAD.encode(bytes))
.map_err(|_| ())
}
fn decode_scan_cursor(cursor: &str) -> Result<SessionPublicRuntimeCursor, ()> {
let bytes = URL_SAFE_NO_PAD.decode(cursor).map_err(|_| ())?;
serde_json::from_slice(&bytes).map_err(|_| ())
}
pub(crate) fn read(
session_root: &Path,
request: &SessionPublicReadRequest,
archive_manifest: Option<&DomainArchiveManifest>,
) -> SessionPublicReadAvailability {
if request.max_content_bytes == 0 {
return read_unavailable(
SessionPublicUnavailableReason::ResourceLimit,
"Session public read content limit must be positive",
);
}
let selected = match read_session_public_index_entry(
session_root,
SessionPublicIndexLimits {
max_bytes: request.max_scan_bytes,
max_segments: request.max_segments,
max_entries: request.max_entries,
},
request.expected_generation.as_deref(),
&request.segment_id,
&request.entry_ref,
) {
Ok(Some(selected)) => selected,
Ok(None) => {
return read_unavailable(
SessionPublicUnavailableReason::NotFound,
"Session entry was not found in the authorized source",
);
}
Err(error) => {
let (reason, message) = map_index_error(error);
return read_unavailable(reason, message);
}
};
if selected.session_id != request.expected_session_id {
return read_unavailable(
SessionPublicUnavailableReason::NotFound,
"Session identity does not match the authorized source",
);
}
let item = selected.item;
let entry = item.entry;
let content = match request.mode {
SessionPublicReadMode::Compact => &entry.compact_text,
SessionPublicReadMode::Full => &entry.full_text,
};
if request.byte_offset > content.len() || !content.is_char_boundary(request.byte_offset) {
return read_unavailable(
SessionPublicUnavailableReason::InvalidCursor,
"Session public read position is stale",
);
}
let mut end = request
.byte_offset
.saturating_add(request.max_content_bytes)
.min(content.len());
while end > request.byte_offset && !content.is_char_boundary(end) {
end -= 1;
}
if end == request.byte_offset && request.byte_offset < content.len() {
return read_unavailable(
SessionPublicUnavailableReason::ResourceLimit,
"Session public read cannot fit one UTF-8 scalar in the content limit",
);
}
let has_more = end < content.len();
SessionPublicReadAvailability::Page {
page: SessionPublicReadPage {
session_id: selected.session_id,
generation: selected.generation,
segment_id: item.segment_id,
entry_ref: entry.entry_ref.clone(),
kind: entry_kind(entry.kind),
origin: entry.provenance.clone(),
lineage: lineage(&item.lineage),
mode: request.mode,
content: content[request.byte_offset..end].to_string(),
next_byte_offset: has_more.then_some(end),
has_more,
scanned_bytes: selected.scanned_bytes,
scanned_segments: selected.scanned_segments,
scanned_entries: selected.scanned_entries,
archive_manifest: archive_manifest.map(api_archive_manifest),
},
}
}
fn matches_entry(
entry: &SessionPublicIndexEntry,
request: &SessionPublicSearchRequest,
query: Option<&str>,
) -> bool {
if request
.kind
.is_some_and(|kind| kind != entry_kind(entry.kind))
{
return false;
}
if request.tool_name.is_some() || request.tool_part != SessionPublicToolPart::Both {
if entry.kind != SessionPublicIndexEntryKind::Tool {
return false;
}
}
if request
.tool_name
.as_deref()
.is_some_and(|name| entry.tool_name.as_deref() != Some(name))
{
return false;
}
if request.tool_part != SessionPublicToolPart::Both
&& entry.tool_part.map(tool_part) != Some(request.tool_part)
{
return false;
}
query.is_none_or(|query| entry.full_text.to_lowercase().contains(query))
}
fn entry_kind(kind: SessionPublicIndexEntryKind) -> SessionPublicEntryKind {
match kind {
SessionPublicIndexEntryKind::User => SessionPublicEntryKind::User,
SessionPublicIndexEntryKind::Assistant => SessionPublicEntryKind::Assistant,
SessionPublicIndexEntryKind::Tool => SessionPublicEntryKind::Tool,
}
}
fn tool_part(part: SessionPublicIndexToolPart) -> SessionPublicToolPart {
match part {
SessionPublicIndexToolPart::Input => SessionPublicToolPart::Input,
SessionPublicIndexToolPart::Output => SessionPublicToolPart::Output,
}
}
fn lineage(value: &SessionPublicIndexLineage) -> SessionPublicLineage {
SessionPublicLineage {
kind: match value.origin_kind {
SessionPublicIndexOriginKind::Root => SessionPublicLineageKind::Root,
SessionPublicIndexOriginKind::Fork => SessionPublicLineageKind::Fork,
SessionPublicIndexOriginKind::Compact => SessionPublicLineageKind::Compact,
},
parent_segment_id: value.parent_segment_id.clone(),
at_turn_index: value.parent_turn_index,
}
}
fn map_index_error(
error: SessionPublicIndexReadError,
) -> (SessionPublicUnavailableReason, &'static str) {
match error {
SessionPublicIndexReadError::MigrationRequired => (
SessionPublicUnavailableReason::MigrationRequired,
"Session storage requires migration",
),
SessionPublicIndexReadError::Missing => (
SessionPublicUnavailableReason::RetentionMissing,
"Session storage is missing",
),
SessionPublicIndexReadError::Corrupt => (
SessionPublicUnavailableReason::CorruptLog,
"Session log is corrupt",
),
SessionPublicIndexReadError::Storage => (
SessionPublicUnavailableReason::StorageUnavailable,
"Session storage is unavailable",
),
SessionPublicIndexReadError::ResourceLimit => (
SessionPublicUnavailableReason::ResourceLimit,
"Session scan exceeds a resource limit",
),
SessionPublicIndexReadError::StaleCursor => (
SessionPublicUnavailableReason::InvalidCursor,
"Session public search cursor is stale",
),
}
}
fn unavailable(
reason: SessionPublicUnavailableReason,
message: impl Into<String>,
) -> SessionPublicSearchAvailability {
SessionPublicSearchAvailability::Unavailable {
reason,
message: message.into(),
}
}
fn read_unavailable(
reason: SessionPublicUnavailableReason,
message: impl Into<String>,
) -> SessionPublicReadAvailability {
SessionPublicReadAvailability::Unavailable {
reason,
message: message.into(),
}
}
fn api_archive_manifest(manifest: &DomainArchiveManifest) -> WorkerSessionArchiveManifest {
WorkerSessionArchiveManifest {
schema_version: manifest.schema_version,
archive_id: manifest.archive_id.clone(),
workspace_id: manifest.workspace_id.clone(),
source_runtime_id: manifest.source_runtime_id.clone(),
source_worker_id: manifest.source_worker_id,
source_session_id: manifest.source_session_id.clone(),
segment_ids: manifest.segment_ids.clone(),
source_created_at: manifest.source_created_at.clone(),
removed_at: manifest.removed_at.clone(),
archived_at_unix_seconds: manifest.archived_at_unix_seconds,
effective_profile: manifest.effective_profile.clone(),
retention_class: manifest.retention_class.clone(),
content_checksum_sha256: manifest.content_checksum_sha256.clone(),
content_bytes: manifest.content_bytes,
content_file_count: manifest.content_file_count,
policy_id: manifest.policy_id.clone(),
policy_revision: manifest.policy_revision,
operation_id: manifest.operation_id.clone(),
input_fingerprint: manifest.input_fingerprint.clone(),
}
}
#[cfg(test)]
mod tests {
use agen::llm_client::RequestConfig;
use session_store::{
LogEntry, LoggedContentPart, LoggedHistoryEntry, LoggedItem, LoggedRole,
LoggedSessionHistoryEntryId, LoggedSessionHistoryMetadata, LoggedSessionHistoryOrigin,
Store, WorkerSessionStore,
};
use super::*;
fn message(id: &str, text: &str) -> LoggedHistoryEntry {
LoggedHistoryEntry {
item: LoggedItem::Message {
role: LoggedRole::User,
content: vec![LoggedContentPart::Text { text: text.into() }],
},
metadata: LoggedSessionHistoryMetadata {
entry_id: LoggedSessionHistoryEntryId(id.to_string()),
origin: LoggedSessionHistoryOrigin::HumanInput {
account_id: "account-1".to_string(),
},
derivation: None,
},
}
}
fn request(
session_id: session_store::SessionId,
generation: Option<String>,
scan_cursor: Option<String>,
) -> SessionPublicSearchRequest {
SessionPublicSearchRequest {
workspace_id: "workspace-1".to_string(),
source: runtime_api::SessionPublicSource::Retained {
worker_id: crate::identity::WorkerId::now_v7(),
},
expected_session_id: session_id.to_string(),
expected_generation: generation,
query: Some("needle".to_string()),
kind: None,
tool_name: None,
tool_part: SessionPublicToolPart::Both,
scan_cursor,
limit: 20,
max_scan_bytes: 64 * 1024 * 1024,
max_segments: 64,
max_entries: 100_000,
}
}
#[test]
fn search_continues_beyond_segment_budget_and_stales_after_mutation() {
let temp = tempfile::TempDir::new().unwrap();
let root = temp.path().join("session");
let store = WorkerSessionStore::new(&root).unwrap();
let session_id = session_store::new_session_id();
let mut last_segment = None;
for index in 0..65_u128 {
let segment_id = uuid::Uuid::from_u128(10_000 + index);
last_segment = Some(segment_id);
store
.create_segment(
session_id,
segment_id,
&[LogEntry::AnnotatedSegmentStart {
ts: 1,
session_id,
system_prompt: None,
config: RequestConfig::default(),
history: vec![message(
&format!("entry-{index}"),
if index == 64 { "needle" } else { "ordinary" },
)],
forked_from: None,
compacted_from: None,
}],
)
.unwrap();
}
let first = match search(&root, &request(session_id, None, None), None) {
SessionPublicSearchAvailability::Page { page } => page,
other => panic!("unexpected first search result: {other:?}"),
};
assert!(first.items.is_empty());
assert!(first.has_more);
let cursor = first.next_scan_cursor.clone().unwrap();
let second = match search(
&root,
&request(
session_id,
Some(first.generation.clone()),
Some(cursor.clone()),
),
None,
) {
SessionPublicSearchAvailability::Page { page } => page,
other => panic!("unexpected continued search result: {other:?}"),
};
assert_eq!(second.items.len(), 1);
assert_eq!(second.items[0].compact, "needle");
assert!(!second.has_more);
let read_page = match read(
&root,
&SessionPublicReadRequest {
workspace_id: "workspace-1".into(),
source: runtime_api::SessionPublicSource::Retained {
worker_id: crate::identity::WorkerId::now_v7(),
},
expected_session_id: session_id.to_string(),
expected_generation: Some(second.generation.clone()),
segment_id: last_segment.unwrap().to_string(),
entry_ref: "Eentry-64".into(),
mode: SessionPublicReadMode::Full,
byte_offset: 0,
max_content_bytes: 16 * 1024,
max_scan_bytes: 64 * 1024 * 1024,
max_segments: 64,
max_entries: 100_000,
},
None,
) {
SessionPublicReadAvailability::Page { page } => page,
other => panic!("unexpected direct read result: {other:?}"),
};
assert_eq!(read_page.content, "needle");
assert_eq!(read_page.segment_id, last_segment.unwrap().to_string());
assert!(!read_page.has_more);
store
.append(
session_id,
last_segment.unwrap(),
&LogEntry::AnnotatedAssistantItem {
ts: 2,
entry: message("mutation", "changed"),
},
)
.unwrap();
assert!(matches!(
search(
&root,
&request(session_id, Some(first.generation), Some(cursor)),
None,
),
SessionPublicSearchAvailability::Unavailable {
reason: SessionPublicUnavailableReason::InvalidCursor,
..
}
));
}
#[test]
fn search_then_read_crosses_entry_budget_within_one_segment() {
let temp = tempfile::TempDir::new().unwrap();
let root = temp.path().join("session");
let store = WorkerSessionStore::new(&root).unwrap();
let session_id = session_store::new_session_id();
let segment_id = uuid::Uuid::from_u128(20_000);
store
.create_segment(
session_id,
segment_id,
&[LogEntry::AnnotatedSegmentStart {
ts: 1,
session_id,
system_prompt: None,
config: RequestConfig::default(),
history: vec![message("seed", "ordinary")],
forked_from: None,
compacted_from: None,
}],
)
.unwrap();
for index in 0..4 {
let id = if index == 3 {
"target".to_string()
} else {
format!("ordinary-{index}")
};
store
.append(
session_id,
segment_id,
&LogEntry::AnnotatedAssistantItem {
ts: 2 + index,
entry: message(&id, if index == 3 { "needle" } else { "ordinary" }),
},
)
.unwrap();
}
let mut generation = None;
let mut scan_cursor = None;
let found = loop {
let mut search_request = request(session_id, generation.clone(), scan_cursor.clone());
search_request.max_entries = 2;
let page = match search(&root, &search_request, None) {
SessionPublicSearchAvailability::Page { page } => page,
other => panic!("unexpected search result: {other:?}"),
};
if let Some(item) = page.items.into_iter().next() {
break (page.generation, item);
}
assert!(page.has_more);
generation = Some(page.generation);
scan_cursor = page.next_scan_cursor;
};
assert_eq!(found.1.entry_ref, "Etarget");
let read_page = match read(
&root,
&SessionPublicReadRequest {
workspace_id: "workspace-1".into(),
source: runtime_api::SessionPublicSource::Retained {
worker_id: crate::identity::WorkerId::now_v7(),
},
expected_session_id: session_id.to_string(),
expected_generation: Some(found.0),
segment_id: segment_id.to_string(),
entry_ref: found.1.entry_ref,
mode: SessionPublicReadMode::Full,
byte_offset: 0,
max_content_bytes: 16 * 1024,
max_scan_bytes: 64 * 1024 * 1024,
max_segments: 64,
max_entries: 2,
},
None,
) {
SessionPublicReadAvailability::Page { page } => page,
other => panic!("unexpected direct read result: {other:?}"),
};
assert_eq!(read_page.content, "needle");
assert!(read_page.scanned_entries > 2);
}
}
+132 -17
View File
@@ -944,7 +944,7 @@ fn bind_workspace_memory_settings(
request: &CreateWorkerRequest,
) -> Result<(), String> {
let Some(snapshot) = request.memory_settings.as_ref() else {
if request.workspace_api.is_some() {
if request.workspace_api.is_some() || request.subjektiv_attached {
return Err(
"Workspace Worker request is missing its bound Memory settings snapshot"
.to_string(),
@@ -960,14 +960,33 @@ fn bind_workspace_memory_settings(
snapshot.workspace_id, workspace_api.workspace_id
));
}
if manifest.feature.memory.profile.enabled {
manifest
.feature
.memory
.bind_workspace_settings(snapshot.clone())
.map_err(str::to_string)?;
}
if request.subjektiv_attached {
if !manifest.feature.subjektiv.profile.enabled {
return Err(
"subject-attached Worker profile does not enable subjektiv policy".to_string(),
);
}
manifest
.feature
.subjektiv
.bind_workspace_settings(snapshot.clone())
.map_err(str::to_string)?;
}
manifest
.feature
.memory
.bind_workspace_settings(snapshot.clone())
.validate_execution()
.map_err(str::to_string)?;
manifest
.feature
.memory
.subjektiv
.validate_execution()
.map_err(str::to_string)?;
Ok(())
@@ -978,6 +997,12 @@ fn validate_worker_memory_settings(
request: &CreateWorkerRequest,
) -> Result<(), String> {
let Some(expected) = request.memory_settings.as_ref() else {
if request.subjektiv_attached {
return Err(
"subject-attached Worker restore is missing its trusted settings snapshot"
.to_string(),
);
}
return Ok(());
};
manifest
@@ -985,17 +1010,21 @@ fn validate_worker_memory_settings(
.memory
.validate_execution()
.map_err(str::to_string)?;
if !manifest.feature.memory.profile.enabled {
return Ok(());
}
let actual = manifest
manifest
.feature
.memory
.workspace_settings()
.ok_or_else(|| {
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
})?;
if &actual != expected {
.subjektiv
.validate_execution()
.map_err(str::to_string)?;
if manifest.feature.memory.profile.enabled
&& manifest.feature.memory.workspace_settings().as_ref() != Some(expected)
{
let actual = manifest
.feature
.memory
.workspace_settings()
.ok_or_else(|| {
"Workspace Worker restored without its bound Memory settings snapshot".to_string()
})?;
return Err(format!(
"Workspace Worker Memory settings snapshot mismatch: expected {} revision {}, restored {} revision {}",
expected.workspace_id,
@@ -1004,6 +1033,33 @@ fn validate_worker_memory_settings(
actual.settings_revision
));
}
let subjektiv_settings = manifest.feature.subjektiv.workspace_settings();
if request.subjektiv_attached {
if !manifest.feature.subjektiv.profile.enabled {
return Err(
"subject-attached Worker restored with a Profile that disables subjektiv policy"
.to_string(),
);
}
if subjektiv_settings.as_ref() != Some(expected) {
let actual = subjektiv_settings.ok_or_else(|| {
"subject-attached Worker restored without its bound subjektiv settings snapshot"
.to_string()
})?;
return Err(format!(
"Workspace Worker subjektiv settings snapshot mismatch: expected {} revision {}, restored {} revision {}",
expected.workspace_id,
expected.settings_revision,
actual.workspace_id,
actual.settings_revision
));
}
} else if subjektiv_settings.is_some() {
return Err(
"ordinary Workspace Worker restored with an unauthorized subjektiv attachment"
.to_string(),
);
}
Ok(())
}
@@ -1241,10 +1297,6 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
}
}
worker
.materialize_durable_session_head()
.await
.map_err(|error| format!("materialize durable Worker session head: {error}"))?;
let workspace_client = worker.workspace_client_handle();
let started = prepared.start().await.map_err(|error| match error {
WorkerBootstrapError::Worker(source) => {
@@ -4391,9 +4443,70 @@ mod tests {
worker_observation_grants: Vec::new(),
workspace_api: None,
memory_settings: None,
subjektiv_attached: false,
}
}
#[test]
fn host_attachment_alone_activates_subjektiv_profile_policy() {
let manifest = || {
let mut manifest = WorkerManifest::from_toml(
r#"
[worker]
name = "subjektiv-attachment-test"
pwd = "./"
[model]
scheme = "anthropic"
model_id = "test-model"
auth = { kind = "none" }
[engine]
max_tokens = 100
[[scope.allow]]
target = "./"
permission = "read"
"#,
)
.unwrap();
manifest.feature.subjektiv.profile.enabled = true;
manifest.feature.subjektiv.profile.extraction.enabled = true;
manifest
};
let settings = manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace-subject".to_string(),
settings_revision: 3,
language: "English".to_string(),
};
let mut ordinary_request = create_request("ordinary");
ordinary_request.workspace_api = Some(WorkspaceApiRef {
workspace_id: settings.workspace_id.clone(),
base_url: "http://workspace.invalid".to_string(),
});
ordinary_request.memory_settings = Some(settings.clone());
let mut ordinary = manifest();
bind_workspace_memory_settings(&mut ordinary, &ordinary_request).unwrap();
assert!(ordinary.feature.subjektiv.profile.enabled);
assert!(!ordinary.feature.subjektiv.execution_enabled());
validate_worker_memory_settings(&ordinary, &ordinary_request).unwrap();
let mut attached_request = ordinary_request.clone();
attached_request.subjektiv_attached = true;
let mut attached = manifest();
bind_workspace_memory_settings(&mut attached, &attached_request).unwrap();
assert!(attached.feature.subjektiv.execution_enabled());
assert_eq!(
attached.feature.subjektiv.workspace_settings(),
Some(settings.clone())
);
validate_worker_memory_settings(&attached, &attached_request).unwrap();
assert!(validate_worker_memory_settings(&attached, &ordinary_request).is_err());
attached_request.memory_settings = None;
assert!(bind_workspace_memory_settings(&mut manifest(), &attached_request).is_err());
}
fn git(path: &std::path::Path, args: &[&str]) {
let status = Command::new("git")
.arg("-C")
@@ -4499,6 +4612,8 @@ mod tests {
tools: Vec::new(),
context_window: 1_000,
context_tokens: 0,
reasoning: None,
context_usage: None,
},
));
hub.workers.lock().unwrap().insert(
@@ -564,6 +564,30 @@ impl WorkspaceClient for RuntimeOwnedWorkspaceClient {
.await,
)
}
WorkspaceServerOperation::SubjektivStageCandidate(request) => {
generated_workspace_response(
client
.subjektiv_stage_candidate(workspace_id, request)
.await,
)
}
WorkspaceServerOperation::SubjektivRecordSession(request) => {
generated_workspace_response(
client.subjektiv_record_session(workspace_id, request).await,
)
}
WorkspaceServerOperation::SubjektivMemory(request) => {
generated_workspace_response(
client.subjektiv_memory_backend(workspace_id, request).await,
)
}
WorkspaceServerOperation::SubjektivSession(request) => {
generated_workspace_response(
client
.subjektiv_session_backend(workspace_id, request)
.await,
)
}
}
})
})