fix: adapt T-769 recovery onto current restore lane checkpoint

UNCOMPILED TRANSPORT / PRESERVATION CHECKPOINT, NOT A VALIDATED CANDIDATE. One-time recovery authorization permits publication before validation so the parent can validate this exact source in a separate Workdir. Prior compilation attempts were interrupted by Workdir-session unavailability; cause and test outcome remain unknown. No Cargo build/check/test was started in this bounded checkpoint turn.

Merge current lane 4ef7b03651 into existing source b7a22146a0. Selectively adapt typed factory failure and same-operation cleanup/replay while preserving current saved Feature restore and operation-time authorization, logical/deferred Workdirs, T765 Host/schema and T768 replay-safe Subject grant. No T770/T771 implementation.

All 19 target-relative paths and added test bodies were source-audited. Workspace fmt check, scoped rustfmt, whitespace/diff checks passed; conflict markers zero. Excluded unrelated formatter changes and untracked compiler artifacts. Local audit: target/t769-checkpoint/AUDIT.md.

Handoff to parent: compile and run focused and package/API regressions on this exact source, fix as necessary, then obtain fresh independent review. Existing MR 01a15606-a6ba-7d23-8d60-0874607399e7 only; no new review requested here. No acceptance, approval, adoption, target/develop push, integration, Ticket closure or service update is asserted.
This commit is contained in:
2026-10-21 00:20:54 +09:00
238 changed files with 29879 additions and 5011 deletions
+366 -8
View File
@@ -91,11 +91,38 @@ pub fn parse_bearer(headers: &HeaderMap) -> Option<String> {
}
pub fn parse_cookie(headers: &HeaderMap, cookie_name: &str) -> Option<String> {
let cookie_header = headers.get(header::COOKIE)?.to_str().ok()?;
cookie_header.split(';').find_map(|pair| {
let (name, value) = pair.trim().split_once('=')?;
(name == cookie_name && !value.is_empty()).then(|| value.to_string())
})
unique_cookie(headers, cookie_name)
.ok()
.flatten()
.filter(|value| !value.is_empty())
}
// Cookie has no path/domain metadata on requests. Duplicates (including identical values
// and multiple Cookie headers) cannot be disambiguated safely after cookie tossing.
fn unique_cookie(
headers: &HeaderMap,
cookie_name: &str,
) -> std::result::Result<Option<String>, ()> {
let mut found = None;
for header in headers.get_all(header::COOKIE) {
let header = header.to_str().map_err(|_| ())?;
for pair in header.split(';') {
let pair = pair.trim();
let Some((name, value)) = pair.split_once('=') else {
if pair == cookie_name {
return Err(());
}
continue;
};
if name == cookie_name {
if found.is_some() {
return Err(());
}
found = Some(value.to_owned());
}
}
}
Ok(found)
}
pub async fn resolve_request_actor(
@@ -121,6 +148,14 @@ pub(crate) fn resolve_request_actor_sync(
bearer: Option<String>,
session: Option<String>,
) -> Result<Option<RequestActor>> {
Ok(resolve_credentials_sync(store, bearer, session)?.map(|(actor, _)| actor))
}
fn resolve_credentials_sync(
store: &dyn ControlPlaneStore,
bearer: Option<String>,
session: Option<String>,
) -> Result<Option<(RequestActor, Option<String>)>> {
if let Some(token) = bearer {
let token_hash = token_hash(&token);
if let Some(api_token) = store.resolve_api_token(&token_hash)? {
@@ -132,7 +167,10 @@ pub(crate) fn resolve_request_actor_sync(
return Ok(None);
}
store.mark_api_token_used(&token_hash, &now_rfc3339())?;
return actor_for_user(store, &api_token.user_id, ActorAuthMethod::ApiToken);
return Ok(
actor_for_user(store, &api_token.user_id, ActorAuthMethod::ApiToken)?
.map(|actor| (actor, None)),
);
}
}
@@ -141,13 +179,95 @@ pub(crate) fn resolve_request_actor_sync(
if is_expired(&session.expires_at) {
return Ok(None);
}
return actor_for_user(store, &session.user_id, ActorAuthMethod::BrowserSession);
return Ok(
actor_for_user(store, &session.user_id, ActorAuthMethod::BrowserSession)?
.map(|actor| (actor, Some(session.expires_at))),
);
}
}
Ok(None)
}
// This marker is a non-authenticating reference to the existing non-revoked session row.
// It outlives the auth cookie so Max-Age expiry remains distinguishable from a new browser.
// No second session store is introduced; clearing all cookies necessarily loses this evidence.
// Keep the established 14-day authentication lifetime. The additional week is only a bounded
// return-navigation window (including a week away); it never extends authentication. The store
// reclaims expired rows past this same boundary on subsequent session issuance. Neither whoami
// nor other reads refresh the marker or the row's absolute expiry.
pub const BROWSER_SESSION_SECONDS: i64 = 14 * 24 * 60 * 60;
pub const SESSION_EXPIRY_GRACE_SECONDS: i64 = 7 * 24 * 60 * 60;
pub fn session_marker_cookie_name(cookie_name: &str) -> String {
format!("{cookie_name}_expiry")
}
pub async fn resolve_whoami(
store: std::sync::Arc<dyn ControlPlaneStore>,
headers: &HeaderMap,
cookie_name: &str,
) -> Result<server_api::WhoamiResponse> {
let bearer = parse_bearer(headers);
// Even an empty/invalid auth cookie takes precedence over an older marker.
// An ambiguous primary is invalid, not absent; never fall back to an older marker.
let session = unique_cookie(headers, cookie_name).unwrap_or_else(|()| Some(String::new()));
let marker = parse_cookie(headers, &session_marker_cookie_name(cookie_name));
let executor = store.db_executor();
executor
.run(move || resolve_whoami_sync(store.as_ref(), bearer, session, marker))
.await
}
fn resolve_whoami_sync(
store: &dyn ControlPlaneStore,
bearer: Option<String>,
session_token: Option<String>,
marker: Option<String>,
) -> Result<server_api::WhoamiResponse> {
use server_api::{BrowserSessionState, WhoamiResponse};
let anonymous = || WhoamiResponse {
actor: None,
session: BrowserSessionState::Anonymous {},
};
// Preserve the existing API-token precedence and fallback rules, including expired tokens.
if let Some((actor, expires_at)) =
resolve_credentials_sync(store, bearer, session_token.clone())?
{
return Ok(WhoamiResponse {
actor: Some(actor),
session: BrowserSessionState::Authenticated { expires_at },
});
}
let hash = match session_token {
Some(token) => token_hash(&token),
None => match marker {
Some(hash) => hash,
None => return Ok(anonymous()),
},
};
let Some(session) = store.resolve_browser_session(&hash)? else {
return Ok(anonymous());
};
if store.get_user(&session.user_id)?.is_none() {
return Ok(anonymous());
}
if within_session_expiry_grace(&session.expires_at, Utc::now()) {
return Ok(WhoamiResponse {
actor: None,
session: BrowserSessionState::Expired {},
});
}
// A marker for a still-valid session cannot restore authentication after partial deletion.
Ok(anonymous())
}
fn within_session_expiry_grace(expires_at: &str, now: DateTime<Utc>) -> bool {
DateTime::parse_from_rfc3339(expires_at).is_ok_and(|expires_at| {
expires_at <= now && now < expires_at + Duration::seconds(SESSION_EXPIRY_GRACE_SECONDS)
})
}
fn actor_for_user<S: ControlPlaneStore + ?Sized>(
store: &S,
user_id: &str,
@@ -274,6 +394,13 @@ mod tests {
.unwrap();
assert_eq!(actor.user_id, "user-auth");
assert_eq!(actor.auth_method, ActorAuthMethod::ApiToken);
let whoami = resolve_whoami(store.clone(), &headers, "session")
.await
.unwrap();
assert_eq!(
whoami.session,
server_api::BrowserSessionState::Authenticated { expires_at: None }
);
assert!(
store
.resolve_api_token(&hash)
@@ -363,20 +490,119 @@ mod tests {
assert!(futures::poll!(queued.as_mut()).is_pending());
let mut headers = HeaderMap::new();
headers.insert(header::COOKIE, "session=session-secret".parse().unwrap());
let actor_result = tokio::time::timeout(
std::time::Duration::from_secs(1),
resolve_request_actor(store.clone(), &headers, "session"),
)
.await;
let result = tokio::time::timeout(
std::time::Duration::from_secs(1),
resolve_request_actor(store, &headers, "session"),
resolve_whoami(store, &headers, "session"),
)
.await;
release.send(()).unwrap();
blocker.await.unwrap().unwrap();
queued.await.unwrap();
assert!(matches!(
actor_result.expect("actor lookup rejects capacity"),
Err(Error::DbCapacityExceeded)
));
assert!(matches!(
result.expect("capacity rejection must not wait for queued SQL"),
Err(Error::DbCapacityExceeded)
));
}
#[tokio::test]
async fn whoami_classifies_only_live_store_backed_session_expiry() {
use server_api::BrowserSessionState;
let store = seeded_store();
for (secret, offset, revoked) in [
("recent", -60, false),
("old", -SESSION_EXPIRY_GRACE_SECONDS - 60, false),
("revoked", -60, true),
] {
store
.create_browser_session(&BrowserSessionRecord {
session_id: format!("session-{secret}"),
token_hash: token_hash(secret),
user_id: "user-auth".into(),
created_at: "1".into(),
expires_at: rfc3339_after(Duration::seconds(offset)),
revoked_at: revoked.then(|| now_rfc3339()),
})
.unwrap();
}
let cases = [
(String::new(), BrowserSessionState::Anonymous {}),
("session=unknown".into(), BrowserSessionState::Anonymous {}),
("session=recent".into(), BrowserSessionState::Expired {}),
(
format!("session_expiry={}", token_hash("recent")),
BrowserSessionState::Expired {},
),
(
format!("session_expiry={}", token_hash("session-secret")),
BrowserSessionState::Anonymous {},
),
(
"session_expiry=unknown".into(),
BrowserSessionState::Anonymous {},
),
(
"session_expiry=recent".into(),
BrowserSessionState::Anonymous {},
),
(
format!("session=unknown; session_expiry={}", token_hash("recent")),
BrowserSessionState::Anonymous {},
),
(
format!("session=; session_expiry={}", token_hash("recent")),
BrowserSessionState::Anonymous {},
),
("session=revoked".into(), BrowserSessionState::Anonymous {}),
(
format!("session_expiry={}", token_hash("revoked")),
BrowserSessionState::Anonymous {},
),
("session=old".into(), BrowserSessionState::Anonymous {}),
(
format!("session_expiry={}", token_hash("old")),
BrowserSessionState::Anonymous {},
),
(
format!(
"session=session-secret; session_expiry={}",
token_hash("recent")
),
BrowserSessionState::Authenticated {
expires_at: Some("9999-01-01T00:00:00Z".into()),
},
),
];
for (cookie, expected) in cases {
let mut headers = HeaderMap::new();
headers.insert(header::COOKIE, cookie.parse().unwrap());
let response = resolve_whoami(store.clone(), &headers, "session")
.await
.unwrap();
assert_eq!(response.session, expected, "cookie: {cookie}");
assert_eq!(
response.actor.is_some(),
matches!(expected, BrowserSessionState::Authenticated { .. })
);
if !matches!(expected, BrowserSessionState::Authenticated { .. }) {
assert!(
resolve_request_actor(store.clone(), &headers, "session")
.await
.unwrap()
.is_none()
);
}
}
}
#[test]
fn user_code_uses_the_uuid_random_tail() {
let id = Uuid::parse_str("01234567-89ab-7cde-8123-456789abcdef").expect("UUID");
@@ -395,4 +621,136 @@ mod tests {
);
assert_eq!(code, code.to_ascii_uppercase());
}
#[test]
fn expiry_grace_includes_expiry_but_excludes_its_absolute_end() {
let expiry = DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
for (offset, expected) in [
(-1, false),
(0, true),
(SESSION_EXPIRY_GRACE_SECONDS - 1, true),
(SESSION_EXPIRY_GRACE_SECONDS, false),
] {
assert_eq!(
within_session_expiry_grace(
&expiry.to_rfc3339(),
expiry + Duration::seconds(offset)
),
expected
);
}
assert!(!within_session_expiry_grace("invalid", expiry));
}
#[tokio::test]
async fn duplicate_cookies_never_select_an_actor_or_promote_an_ambiguous_marker() {
use server_api::BrowserSessionState;
let store = seeded_store();
let expired_hash = token_hash("expired-duplicate");
store
.create_browser_session(&BrowserSessionRecord {
session_id: "expired-duplicate".into(),
token_hash: expired_hash.clone(),
user_id: "user-auth".into(),
created_at: now_rfc3339(),
expires_at: rfc3339_after(Duration::seconds(-60)),
revoked_at: None,
})
.unwrap();
for cookies in [
vec!["session=session-secret; session=unknown".to_owned()],
vec!["session=session-secret; session=session-secret".to_owned()],
vec![
"session=session-secret".to_owned(),
"session=unknown".to_owned(),
],
vec![format!(
"session=; session=session-secret; session_expiry={expired_hash}"
)],
vec![format!(
"session_expiry={expired_hash}; session_expiry={expired_hash}"
)],
vec![format!("session; session_expiry={expired_hash}")],
] {
let mut headers = HeaderMap::new();
for cookie in cookies {
headers.append(header::COOKIE, cookie.parse().unwrap());
}
assert!(
resolve_request_actor(store.clone(), &headers, "session")
.await
.unwrap()
.is_none()
);
assert_eq!(
resolve_whoami(store.clone(), &headers, "session")
.await
.unwrap()
.session,
BrowserSessionState::Anonymous {}
);
}
let mut headers = HeaderMap::new();
headers.insert(
header::COOKIE,
format!("session=session-secret; session_expiry={expired_hash}")
.parse()
.unwrap(),
);
assert!(matches!(
resolve_whoami(store, &headers, "session")
.await
.unwrap()
.session,
BrowserSessionState::Authenticated { .. }
));
}
#[test]
fn session_issuance_reclaims_only_rows_beyond_recovery_grace() {
let store = seeded_store();
let record = |secret: &str, offset| BrowserSessionRecord {
session_id: secret.into(),
token_hash: token_hash(secret),
user_id: "user-auth".into(),
created_at: now_rfc3339(),
expires_at: rfc3339_after(Duration::seconds(offset)),
revoked_at: None,
};
store
.create_browser_session(&record("past-grace", -SESSION_EXPIRY_GRACE_SECONDS - 60))
.unwrap();
assert!(
store
.resolve_browser_session(&token_hash("past-grace"))
.unwrap()
.is_some()
);
store
.create_browser_session(&record("within-grace", -SESSION_EXPIRY_GRACE_SECONDS + 60))
.unwrap();
assert!(
store
.resolve_browser_session(&token_hash("past-grace"))
.unwrap()
.is_none()
);
store
.create_browser_session(&record("new-session", BROWSER_SESSION_SECONDS))
.unwrap();
assert!(
store
.resolve_browser_session(&token_hash("within-grace"))
.unwrap()
.is_some()
);
assert!(
store
.resolve_browser_session(&token_hash("new-session"))
.unwrap()
.is_some()
);
}
}
+8
View File
@@ -567,6 +567,10 @@ pub struct WorkerSpawnRequest {
/// Trusted Backend operation identity used to make Worker-owned spawns replay-safe.
#[serde(skip, default)]
pub resolved_control_operation: Option<WorkerControlOperation>,
/// Host-derived child execution ceiling, persisted against the ordinary
/// Worker reservation before Runtime activation. Not accepted from JSON.
#[serde(skip, default)]
pub resolved_execution_grant: Option<server_api::authorization::WorkerExecutionGrant>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
@@ -7892,6 +7896,7 @@ mod tests {
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_execution_grant: None,
resolved_workspace_api: Some(test_workspace_api()),
resolved_memory_settings: Some(test_memory_settings()),
resolved_subjektiv_attached: false,
@@ -8182,6 +8187,7 @@ mod tests {
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_execution_grant: None,
resolved_workspace_api: Some(test_workspace_api()),
resolved_memory_settings: Some(test_memory_settings()),
resolved_subjektiv_attached: false,
@@ -8294,6 +8300,7 @@ mod tests {
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_execution_grant: None,
resolved_workspace_api: Some(test_workspace_api()),
resolved_memory_settings: Some(test_memory_settings()),
resolved_subjektiv_attached: false,
@@ -8335,6 +8342,7 @@ mod tests {
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_execution_grant: None,
resolved_workspace_api: Some(test_workspace_api()),
resolved_memory_settings: Some(test_memory_settings()),
resolved_subjektiv_attached: false,
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,489 @@
//! Host binding for cooperative Feature authorization. Profile configuration requests
//! capabilities; it never issues them. Non-Subject Workers retain their existing
//! Backend/provider contracts. Subject bodies with no assignment deny by default.
//!
//! Bindings here are execution grants, not another Worker registry. Current body
//! ownership remains the singleton store. Replacing a permission assignment fences
//! all children issued from it; restoring a Worker cannot refresh that assignment.
//! Checks refuse new effects; they do not undo effects already committed. Trusted
//! Features must call the operation boundary themselves, including alternate surfaces.
//!
//! Subject delegation uses the normal Worker control lifecycle, which binds stable
//! operation identity to the authenticated origin and recovers control-grant writes.
//! Generic Worker creation rejects Subject delegation before reservation, even if
//! the payload supplies an operation ID; its manual allocations are not replay-safe.
use super::*;
use server_api::authorization::{
OperationAuthorizationRequest, PermissionResource, ScopedPermission, SubjectPermissions,
WorkerDelegationRequest, WorkerExecutionGrant,
};
use std::collections::BTreeSet;
fn denied(message: impl Into<String>) -> ApiError {
Error::WorkspacePermissionDenied(message.into()).into()
}
fn policy(api: &WorkspaceApi, subject_id: &str) -> ApiResult<Option<SubjectPermissions>> {
let store = open_subjektiv_store(api)?;
let subject = store
.subject(subject_id)
.map_err(subjektiv_store_error)?
.ok_or_else(|| denied("Subject permission source no longer exists"))?;
if subject.state != crate::subjektiv::SubjectState::Active {
return Err(denied("Subject permission source is retired"));
}
Ok(store
.subject_permissions(subject_id)
.map_err(subjektiv_store_error)?)
}
fn current_body_subject(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
) -> ApiResult<Option<String>> {
let retained = api
.store
.retained_worker_singleton_lease(&api.config.workspace_id, worker)?;
let Some(retained) = retained else {
return Ok(None);
};
let Some(subject) = retained.key.strip_prefix(SUBJEKTIV_SINGLETON_PREFIX) else {
return Ok(None);
};
// This also rejects a retained lease from a released/replaced body. Never
// fall through to non-Subject compatibility after ownership has ended.
let current = api
.store
.require_current_worker_singleton_owner(&api.config.workspace_id, worker)?
.ok_or_else(|| denied("Subject body no longer owns its singleton"))?;
if current.key != retained.key {
return Err(denied("Subject body ownership changed"));
}
Ok(Some(subject.to_owned()))
}
fn live_child_grant(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
) -> ApiResult<Option<WorkerExecutionGrant>> {
let store = open_subjektiv_store(api)?;
let Some(grant) = store
.worker_execution_grant(&worker.runtime_id, &worker.worker_id)
.map_err(subjektiv_store_error)?
else {
return Ok(None);
};
let source =
policy(api, &grant.subject_id)?.ok_or_else(|| denied("Subject permissions are unset"))?;
if source.change_id != grant.source_change_id {
return Err(denied(
"delegated execution grant was revoked by permission replacement",
));
}
let mut current = grant.clone();
let mut visited = HashSet::new();
for _ in 0..64 {
if !visited.insert((current.runtime_id.clone(), current.worker_id.clone())) {
return Err(denied("cyclic execution grant lineage"));
}
if current.subject_id != grant.subject_id
|| current.source_change_id != source.change_id
|| current
.permissions
.iter()
.chain(current.redelegation.iter())
.any(|permission| {
!source
.delegation
.iter()
.any(|ceiling| ceiling.covers(permission))
})
{
return Err(denied(
"delegated execution exceeds the current Subject ceiling",
));
}
let origin = RuntimeWorkerRef::new(&current.origin_runtime_id, &current.origin_worker_id);
match store
.worker_execution_grant(&origin.runtime_id, &origin.worker_id)
.map_err(subjektiv_store_error)?
{
Some(parent) => {
if current
.permissions
.iter()
.chain(current.redelegation.iter())
.any(|permission| {
!parent
.redelegation
.iter()
.any(|ceiling| ceiling.covers(permission))
})
{
return Err(denied(
"execution grant exceeds its immediate parent's redelegation ceiling",
));
}
current = parent;
}
None => {
if current_body_subject(api, &origin)?.as_deref() != Some(grant.subject_id.as_str())
{
return Err(denied(
"delegation origin is no longer the current Subject body",
));
}
return Ok(Some(grant));
}
}
}
Err(denied(
"execution grant lineage exceeds the bounded delegation depth",
))
}
/// None is explicitly the preexisting non-Subject contract, not an all-powerful
/// grant. Existing Workdir/provider/Ticket checks still apply to those Workers.
pub(super) fn worker_permissions(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
) -> ApiResult<Option<Vec<ScopedPermission>>> {
if let Some(grant) = live_child_grant(api, worker)? {
return Ok(Some(grant.permissions));
}
let Some(subject) = current_body_subject(api, worker)? else {
return Ok(None);
};
Ok(Some(
policy(api, &subject)?
.map(|policy| policy.execution)
.unwrap_or_default(),
))
}
pub(super) fn authorize(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
request: &OperationAuthorizationRequest,
) -> ApiResult<()> {
authorize_scope(api, worker, request).map(|_| ())
}
pub(super) fn authorize_scope(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
request: &OperationAuthorizationRequest,
) -> ApiResult<Option<PermissionResource>> {
request.validate().map_err(denied)?;
if let PermissionResource::Workspace { workspace_id } = &request.resource {
if workspace_id != &api.config.workspace_id {
return Err(denied("resource belongs to another Workspace"));
}
}
let needed = ScopedPermission {
operation: request.operation.clone(),
resource: request.resource.clone(),
};
let Some(permissions) = worker_permissions(api, worker)? else {
return Ok(None);
};
if let Some(permission) = permissions
.iter()
.find(|permission| permission.covers(&needed))
{
return Ok(Some(permission.resource.clone()));
}
Err(denied(format!(
"operation `{}` is not granted for the requested resource",
request.operation
)))
}
pub(super) fn source_from_context(
context: &server_api::ServerRequestContext,
) -> ApiResult<RuntimeWorkerRef> {
let source = context
.runtime_source
.as_ref()
.ok_or_else(|| denied("authorization requires a verified Runtime source"))?;
let worker_id = source
.worker_id
.as_deref()
.ok_or_else(|| denied("authorization requires Runtime-bound Worker identity"))?;
Ok(RuntimeWorkerRef::new(&source.runtime_id, worker_id))
}
/// Resolves authority from the authenticated origin, independently of its enabled
/// Feature/tool set. A model cannot nominate a Subject or inherit body identity.
pub(super) fn prepare_delegation(
api: &WorkspaceApi,
source: &RuntimeWorkerRef,
requested: Option<&WorkerDelegationRequest>,
runtime_id: &str,
profile: &str,
profile_digest: &str,
singleton_key: Option<&str>,
subject_connection: bool,
) -> ApiResult<Option<WorkerExecutionGrant>> {
let child = live_child_grant(api, source)?;
let body = if child.is_none() {
current_body_subject(api, source)?
} else {
None
};
if child.is_none() && body.is_none() {
if requested.is_some() {
return Err(denied(
"only an authenticated Subject delegation source can issue an execution grant",
));
}
return Ok(None);
}
if singleton_key.is_some() || subject_connection {
return Err(denied(
"delegated Workers cannot acquire a Subject body or singleton identity",
));
}
let requested = requested.ok_or_else(|| {
denied("Subject delegation requires explicit least-authority permissions")
})?;
requested.validate().map_err(denied)?;
let (subject_id, origin_runtime_id, origin_worker_id, source_change_id, ceiling) = match child {
Some(grant) => (
grant.subject_id,
source.runtime_id.clone(),
source.worker_id.clone(),
grant.source_change_id,
grant.redelegation,
),
None => {
let subject = body.expect("checked Subject body");
let source_policy = policy(api, &subject)?.ok_or_else(|| {
denied(
"Subject permissions are unset; an authorized administrator must assign them",
)
})?;
(
subject,
source.runtime_id.clone(),
source.worker_id.clone(),
source_policy.change_id,
source_policy.delegation,
)
}
};
if requested
.permissions
.iter()
.chain(requested.redelegation.iter())
.any(|permission| !ceiling.iter().any(|allowed| allowed.covers(permission)))
{
return Err(denied(
"requested delegation exceeds explicit operations, resources or redelegation ceiling",
));
}
if requested.redelegation.iter().any(|permission| {
!requested
.permissions
.iter()
.any(|allowed| allowed.covers(permission))
}) {
return Err(denied(
"redelegation must be within this Worker's effective grant",
));
}
Ok(Some(WorkerExecutionGrant {
subject_id,
origin_runtime_id,
origin_worker_id,
source_change_id,
runtime_id: runtime_id.to_owned(),
worker_id: String::new(),
profile: profile.to_owned(),
profile_digest: profile_digest.to_owned(),
permissions: requested.permissions.clone(),
redelegation: requested.redelegation.clone(),
}))
}
pub(super) fn bind_reserved_grant(
api: &WorkspaceApi,
grant: &WorkerExecutionGrant,
worker_id: &str,
) -> ApiResult<()> {
let mut bound = grant.clone();
bound.worker_id = worker_id.to_owned();
Ok(open_subjektiv_store(api)?
.bind_worker_execution_grant(&bound)
.map_err(subjektiv_store_error)?)
}
/// Host-owned binding for an already authenticated Backend operation. No model
/// input can choose the captured Worker or turn a shared client into a grant.
#[derive(Clone)]
struct BoundOperationClient {
api: WorkspaceApi,
worker: RuntimeWorkerRef,
}
impl std::fmt::Debug for BoundOperationClient {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("BoundOperationClient")
.field("worker", &self.worker)
.finish_non_exhaustive()
}
}
impl worker::WorkspaceClient for BoundOperationClient {
fn workspace_id(&self) -> Option<&str> {
Some(&self.api.config.workspace_id)
}
fn kind(&self) -> &str {
"backend-bound-operation"
}
fn is_available(&self) -> bool {
true
}
fn execute(
&self,
_: worker::WorkspaceRequest,
) -> std::result::Result<worker::WorkspaceResponse, worker::WorkspaceClientError> {
Err(worker::WorkspaceClientError::Unavailable(
"this binding provides authorization only".into(),
))
}
fn authorize_operation(
&self,
operation: &str,
resource: &PermissionResource,
) -> std::result::Result<(), worker::WorkspaceClientError> {
authorize(
&self.api,
&self.worker,
&OperationAuthorizationRequest {
operation: operation.into(),
resource: resource.clone(),
},
)
.map_err(|error| worker::WorkspaceClientError::Request(error.error.to_string()))
}
fn authorize_operation_scope(
&self,
operation: &str,
resource: &PermissionResource,
) -> std::result::Result<Option<PermissionResource>, worker::WorkspaceClientError> {
authorize_scope(
&self.api,
&self.worker,
&OperationAuthorizationRequest {
operation: operation.into(),
resource: resource.clone(),
},
)
.map_err(|error| worker::WorkspaceClientError::Request(error.error.to_string()))
}
}
pub(super) fn bind_operation_client(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
) -> Arc<dyn worker::WorkspaceClient> {
Arc::new(BoundOperationClient {
api: api.clone(),
worker: worker.clone(),
})
}
pub(super) async fn interrupt_subject_workers(
api: &WorkspaceApi,
subject_id: &str,
) -> Vec<server_api::SubjectPermissionInterruption> {
let mut workers = BTreeSet::new();
let collected = (|| -> ApiResult<()> {
for grant in open_subjektiv_store(api)?
.list_subject_worker_execution_grants(subject_id)
.map_err(subjektiv_store_error)?
{
workers.insert((grant.runtime_id, grant.worker_id));
}
let key = crate::subjektiv::subject_worker_singleton_key(subject_id)
.map_err(subjektiv_store_error)?;
if let Some(body) = api
.store
.current_worker_singleton_owner(&api.config.workspace_id, &key)?
{
workers.insert((body.worker.runtime_id, body.worker.worker_id));
}
Ok(())
})();
if let Err(error) = collected {
return vec![server_api::SubjectPermissionInterruption {
runtime_id: String::new(),
worker_id: String::new(),
cancellation_accepted: false,
backend_command_sessions_closed: false,
diagnostics: vec![format!(
"permissions committed; interruption targets could not be fully collected: {}",
error.error
)],
}];
}
let mut results = Vec::new();
for (runtime_id, worker_id) in workers {
let worker = RuntimeWorkerRef::new(&runtime_id, &worker_id);
let mut evidence = server_api::SubjectPermissionInterruption {
runtime_id,
worker_id,
cancellation_accepted: false,
backend_command_sessions_closed: false,
diagnostics: Vec::new(),
};
let cancellation_api = api.clone();
let cancellation_worker = worker.clone();
let cancellation = match api.host_executors.host() {
Ok(host) => host.run(move || -> ApiResult<_> {
Ok(cancellation_api.runtime.cancel_worker(&cancellation_worker, WorkerLifecycleRequest {
reason: Some("Subject permission assignment changed; new operations are fenced".into()), ticket_assignment: None,
}).map_err(RuntimeRegistryError::into_error)?)
}).await,
Err(error) => Err(error.into()),
};
match cancellation {
Ok(result) => {
evidence.cancellation_accepted =
result.state == InternalWorkerOperationState::Accepted;
evidence.diagnostics.extend(
result
.diagnostics
.into_iter()
.map(|diagnostic| diagnostic.message),
);
}
Err(error) => evidence.diagnostics.push(format!(
"Runtime cancellation acceptance unknown or rejected: {}",
error.error
)),
}
match close_worker_workdir_sessions(&api.workdir_sessions, &worker).await {
Ok(()) => evidence.backend_command_sessions_closed = true,
Err(error) => evidence
.diagnostics
.push(format!("Backend command cleanup not confirmed: {error}")),
}
results.push(evidence);
}
results
}
pub(super) fn administration_actor(
context: &server_api::ServerRequestContext,
) -> ApiResult<&RequestActor> {
if context.runtime_source.is_some() || context.worker_source.is_some() {
return Err(denied(
"Subject permission administration is not a Worker capability",
));
}
context
.actor
.as_ref()
.ok_or_else(|| denied("Subject permission administration requires an authenticated owner"))
}
@@ -105,6 +105,7 @@ async fn cleanup_retention_failures_preserve_cause_on_execution_and_recovery() {
resolved_memory_settings: None,
resolved_subjektiv_attached: false,
resolved_control_operation: None,
resolved_execution_grant: None,
},
)
.unwrap();
@@ -279,6 +280,7 @@ async fn generated_cancellation_errors_preserve_response_and_log_details() {
resolved_memory_settings: Some(test_worker_memory_settings()),
resolved_subjektiv_attached: false,
resolved_control_operation: None,
resolved_execution_grant: None,
},
)
.unwrap()
@@ -471,11 +473,10 @@ async fn generated_workdir_provider_errors_preserve_response_and_log_details() {
));
// Seed an already registered command, without launching a real process.
let provider_handle = CommandHandle("logging-command".to_string());
session
.active_commands
.lock()
.await
.insert(provider_handle.0.clone(), provider_handle.clone());
session.active_commands.lock().await.insert(
provider_handle.0.clone(),
Arc::new(ExternalProviderCommandState::new(provider_handle.clone())),
);
let handle = fixture
.api
.workdir_sessions
@@ -682,13 +683,24 @@ async fn self_workdir_provider_refusals_log_operation_identity_without_request_t
let mut fixture = manual_worker_assignment_fixture().await;
let root = tempfile::tempdir().unwrap();
let provider_root = root.path().to_path_buf();
// Use an attachment-relative path so Host resource validation succeeds;
// the provider's narrower scope still owns this read refusal.
let provider_scope = manifest::Scope::writable(&provider_root)
.unwrap()
.with_added_deny_rules([manifest::ScopeRule {
target: provider_root.join("private"),
permission: manifest::Permission::Read,
recursive: true,
symlink_policy: manifest::SymlinkPolicy::Resolved,
}])
.unwrap();
// Fresh sessions exercise the real opening/broker/dispatch path on each request.
fixture.runtime.workdir_session_factory = Some(Arc::new(move |id| {
Ok(Arc::new(workdir::LocalWorkdirSession::materialized_bound(
workdir::Workdir::new(id),
provider_root.clone(),
provider_root.clone(),
manifest::SharedScope::new(manifest::Scope::writable(&provider_root).unwrap()),
manifest::SharedScope::new(provider_scope.clone()),
Caps::READ_ONLY,
)) as WorkdirSessionHandle)
}));
@@ -708,7 +720,7 @@ async fn self_workdir_provider_refusals_log_operation_identity_without_request_t
);
let cases = [
(
json!({"operation": "read", "request": {"path": "/host/private/secret.txt", "offset": 0, "limit": 10, "max_bytes": 1024}}),
json!({"operation": "read", "request": {"path": "private/secret.txt", "offset": 0, "limit": 10, "max_bytes": 1024}}),
"read",
"out_of_scope",
Reason::PathOutOfScope,
@@ -720,7 +732,7 @@ async fn self_workdir_provider_refusals_log_operation_identity_without_request_t
Reason::ScopedCapabilityDenied,
),
(
json!({"operation": "command_start", "request": {"command": secret, "timeout_secs": 1, "output_limit": 1024, "cwd": "", "tool_call_id": secret}}),
json!({"operation": "command_start", "request": {"command": secret, "timeout_secs": 1, "transport_byte_budget": 1048576, "cwd": "", "tool_call_id": secret}}),
"command_start",
"denied",
Reason::ScopedCapabilityDenied,
@@ -942,8 +954,8 @@ async fn self_workdir_post_start_registration_failure_does_not_claim_pre_dispatc
serde_json::to_vec(&json!({
"target_workdir": "checkout",
"operation": {"operation": "command_start", "request": {
"command": "body-secret", "timeout_secs": 1, "output_limit": 1024,
"cwd": "", "spill_dir": null, "tool_call_id": "body-secret"
"command": "body-secret", "timeout_secs": 1, "transport_byte_budget": 1048576,
"cwd": "", "tool_call_id": "body-secret"
}}
}))
.unwrap(),
@@ -1071,8 +1083,8 @@ async fn saturated_host_before_command_start_returns_availability_without_a_prov
serde_json::to_vec(&json!({
"target_workdir": "checkout",
"operation": {"operation": "command_start", "request": {
"command": "not-dispatched", "timeout_secs": 1, "output_limit": 1024,
"cwd": "", "spill_dir": null, "tool_call_id": "not-dispatched"
"command": "not-dispatched", "timeout_secs": 1, "transport_byte_budget": 1048576,
"cwd": "", "tool_call_id": "not-dispatched"
}}
}))
.unwrap(),
@@ -0,0 +1,223 @@
//! Provider-bridge lifecycle tests. The production bridge runs against a controlled
//! provider boundary; the websocket/CLI-pump test in the parent module covers real byte transport.
use super::*;
use workdir::WorkdirSession;
fn command_fixture() -> (
Arc<ExternalProviderWorkdirSession>,
tokio::sync::mpsc::Receiver<ExternalProviderCommand>,
) {
let (sender, receiver) = tokio::sync::mpsc::channel(16);
let session = Arc::new(ExternalProviderWorkdirSession::new(
Arc::new(ExternalProviderConnection {
grant_id: "command-grant".into(),
workdir_id: "command-workdir".into(),
provider_instance_id: "command-provider".into(),
generation: 1,
expires_at: None,
capabilities: workdir::WorkdirSessionCapabilities::COMMAND_ONLY,
admission: Arc::new(tokio::sync::Semaphore::new(1)),
shutdown_confirmed: Arc::new(std::sync::atomic::AtomicBool::new(false)),
sender,
}),
None,
));
(session, receiver)
}
fn page(cursor: u64, bytes: &[u8], terminal: bool) -> workdir::CommandOutput {
workdir::CommandOutput {
chunks: if bytes.is_empty() {
Vec::new()
} else {
vec![workdir::CommandByteChunk {
sequence: cursor,
stream: workdir::CommandStream::Stdout,
offset: cursor,
bytes: bytes.to_vec(),
}]
},
next_cursor: cursor + bytes.len() as u64,
terminal: terminal.then_some(workdir::CommandTerminal {
status: workdir::CommandStatus::Cancelled,
exit_code: None,
stdout_bytes: cursor + bytes.len() as u64,
stderr_bytes: 0,
stdout_delivered_bytes: cursor + bytes.len() as u64,
stderr_delivered_bytes: 0,
truncation_reason: None,
}),
}
}
async fn receive(
receiver: &mut tokio::sync::mpsc::Receiver<ExternalProviderCommand>,
) -> (
WorkdirSessionOperation,
tokio::sync::oneshot::Sender<
std::result::Result<WorkdirSessionOperationResult, WorkdirTransportError>,
>,
) {
let command = tokio::time::timeout(std::time::Duration::from_secs(2), receiver.recv())
.await
.unwrap()
.unwrap();
let ExternalProviderCommand::Operation {
operation,
response,
..
} = command
else {
panic!("expected provider operation")
};
(operation, response)
}
#[tokio::test]
async fn close_cancels_waiting_reader_then_drains_from_latest_cursor() {
let (session, mut receiver) = command_fixture();
session.active_commands.lock().await.insert(
"external".into(),
Arc::new(ExternalProviderCommandState::new(CommandHandle(
"provider".into(),
))),
);
let reader = {
let session = session.clone();
tokio::spawn(async move {
session
.command_output(workdir::CommandOutputRequest {
handle: CommandHandle("external".into()),
cursor: 0,
limit: 3,
wait: true,
})
.await
})
};
let (_, response) = receive(&mut receiver).await;
response
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
0, b"one", false,
))))
.unwrap();
assert_eq!(reader.await.unwrap().unwrap().next_cursor, 3);
let reader = {
let session = session.clone();
tokio::spawn(async move {
session
.command_output(workdir::CommandOutputRequest {
handle: CommandHandle("external".into()),
cursor: 3,
limit: 3,
wait: true,
})
.await
})
};
let (_, pending_read) = receive(&mut receiver).await;
let closer = {
let session = session.clone();
tokio::spawn(async move { session.close().await })
};
let (operation, response) = receive(&mut receiver).await;
assert!(
matches!(operation, WorkdirSessionOperation::CommandCancel(ref handle) if handle.0 == "provider")
);
response
.send(Ok(WorkdirSessionOperationResult::CommandCancel))
.unwrap();
pending_read
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
3, b"two", false,
))))
.unwrap();
assert_eq!(reader.await.unwrap().unwrap().next_cursor, 6);
for (cursor, bytes, terminal) in [(6, &b"end"[..], false), (9, &b""[..], true)] {
let (operation, response) = receive(&mut receiver).await;
assert!(
matches!(operation, WorkdirSessionOperation::CommandOutput(ref request) if request.handle.0 == "provider" && request.cursor == cursor && request.wait)
);
response
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
cursor, bytes, terminal,
))))
.unwrap();
}
tokio::time::timeout(std::time::Duration::from_secs(2), closer)
.await
.unwrap()
.unwrap()
.unwrap();
}
#[tokio::test]
async fn terminal_output_retains_same_binding_for_current_page_replay() {
let (session, mut receiver) = command_fixture();
session.active_commands.lock().await.insert(
"external".into(),
Arc::new(ExternalProviderCommandState::new(CommandHandle(
"provider".into(),
))),
);
for _ in 0..2 {
let reader = {
let session = session.clone();
tokio::spawn(async move {
session
.command_output(workdir::CommandOutputRequest {
handle: CommandHandle("external".into()),
cursor: 0,
limit: 3,
wait: true,
})
.await
})
};
let (operation, response) = receive(&mut receiver).await;
assert!(
matches!(operation, WorkdirSessionOperation::CommandOutput(ref request) if request.handle.0 == "provider" && request.cursor == 0)
);
response
.send(Ok(WorkdirSessionOperationResult::CommandOutput(page(
0, b"end", true,
))))
.unwrap();
assert_eq!(reader.await.unwrap().unwrap(), page(0, b"end", true));
}
assert_eq!(session.active_commands.lock().await.len(), 1);
}
#[tokio::test]
async fn terminal_registry_retention_is_bounded_and_excludes_active_commands() {
let (source, _) = command_fixture();
let worker = RuntimeWorkerRef::new("runtime", "worker");
let mut registry = WorkdirSessionRegistry::default();
let active = registry.register_command(
worker.clone(),
"checkout".into(),
source.clone(),
CommandHandle("active".into()),
);
let mut last = None;
for index in 0..MAX_RETAINED_TERMINAL_COMMANDS + 4 {
let handle = registry.register_command(
worker.clone(),
"checkout".into(),
source.clone(),
CommandHandle(format!("terminal-{index}")),
);
registry.mark_terminal(&worker, &handle);
last = Some(handle);
}
assert_eq!(registry.active_command_count(&worker), 1);
assert_eq!(registry.commands.len(), MAX_RETAINED_TERMINAL_COMMANDS + 1);
assert!(registry.command(&worker, "checkout", &active).is_some());
assert!(
registry
.command(&worker, "checkout", &last.unwrap())
.is_some()
);
}
@@ -325,6 +325,7 @@ async fn workers(api: &WorkspaceApi) -> (WorkdirlessFixtureRuntime, Vec<RuntimeW
workdir_attachments: vec![],
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -777,6 +778,7 @@ async fn drive_read_entrypoints_use_current_grants_and_cannot_fallback_to_browse
configure_runtime_request_auth(&mut api, &identity, &workers[0].runtime_id);
let routes = [
format!("{}/root", base()),
format!("{}/capabilities", base()),
format!(
"{}/metadata?entry_workspace_id={TEST_WORKSPACE_ID}&id={id}",
base()
@@ -1007,3 +1009,106 @@ async fn drive_download_stream_expires_instead_of_mixing_last_mutation_ids() {
"next chunk must expire, not deliver generation b"
);
}
#[tokio::test]
async fn drive_capabilities_expose_only_authenticated_current_caller_ceiling() {
let temp = tempfile::tempdir().unwrap();
let mut api = test_api(temp.path()).await;
let owner = seed_test_api_token(api.store.as_ref(), TEST_WORKSPACE_ID);
let (_runtime, workers) = workers(&api).await;
let identity = RuntimeIdentityMaterial::generate(&workers[0].runtime_id).unwrap();
configure_runtime_request_auth(&mut api, &identity, &workers[0].runtime_id);
let path = format!("{}/capabilities", base());
let unsigned = build_router(api.clone())
.oneshot(Request::builder().uri(&path).body(Body::empty()).unwrap())
.await
.unwrap();
assert!(matches!(
unsigned.status(),
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN
));
signed(
&api,
&identity,
None,
"GET",
&path,
None,
StatusCode::FORBIDDEN,
)
.await;
let mut grants = vec![];
for (worker, access, write) in [
(&workers[0], "read_only", false),
(&workers[1], "read_write", true),
] {
grants.push(user_call(&api,&owner,"POST",&format!("{}/grants",base()),Some(json!({"runtime_id":worker.runtime_id,"worker_id":worker.worker_id,"access":access})),StatusCode::OK).await);
assert_eq!(
signed(
&api,
&identity,
Some(&worker.worker_id),
"GET",
&path,
None,
StatusCode::OK
)
.await,
json!({"write":write})
);
}
// Altering a signed source cannot acquire the other Worker's larger ceiling.
let mut forged =
runtime_source_request(&identity, Some(&workers[0].worker_id), "GET", &path, vec![]);
forged
.headers_mut()
.insert("x-yoi-worker-id", workers[1].worker_id.parse().unwrap());
let response = build_router(api.clone()).oneshot(forged).await.unwrap();
assert!(matches!(
response.status(),
StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN
));
let foreign = path.replace(&format!("/w/{TEST_WORKSPACE_ID}/"), "/w/other-workspace/");
let response = build_router(api.clone())
.oneshot(runtime_source_request(
&identity,
Some(&workers[1].worker_id),
"GET",
&foreign,
vec![],
))
.await
.unwrap();
assert!(!response.status().is_success());
signed(
&api,
&identity,
Some("nonexistent-worker"),
"GET",
&path,
None,
StatusCode::UNAUTHORIZED,
)
.await;
for (worker, grant) in workers.iter().zip(&grants) {
user_call(
&api,
&owner,
"DELETE",
&format!("{}/grants/{}", base(), grant["grant_id"].as_str().unwrap()),
None,
StatusCode::OK,
)
.await;
signed(
&api,
&identity,
Some(&worker.worker_id),
"GET",
&path,
None,
StatusCode::FORBIDDEN,
)
.await;
}
}
@@ -183,6 +183,7 @@ impl RealDriveFixture {
workdir_attachments: vec![],
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -0,0 +1,194 @@
//! Restore entry -> real SQLite attachment registry -> embedded Runtime restore
//! coordination. Only execution is deterministic; Workdir providers are absent.
//! This suite covers restore preparation, not HTTP authentication or model turns.
use super::*;
#[tokio::test]
async fn restore_keeps_offline_external_and_unmaterialized_repository_attachments() {
let temp = tempfile::tempdir().unwrap();
init_clean_git_workspace(temp.path());
let (api, execution) = test_api_with_recording_backend(temp.path()).await;
execution.accept_restores();
let Json(created) = create_workspace_worker(
State(api.clone()),
HeaderMap::new(),
Json(CreateWorkspaceWorkerRequest {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.into(),
display_name: "Restore without providers".into(),
singleton_key: None,
profile: Some("builtin:coder".into()),
ticket_assignment: None,
initial_submit: Vec::new(),
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
.unwrap();
let worker = RuntimeWorkerRef::new(&created.runtime_id, &created.worker_id);
let grant = ExternalWorkdirGrantRecord {
grant_id: "restore-external-grant".into(),
workspace_id: TEST_WORKSPACE_ID.into(),
workdir_id: "restore-external".into(),
provider_instance_id: "offline-provider".into(),
display_name: "Offline external".into(),
permissions: "read_only".into(),
created_by: test_browser_request_actor().account_id,
created_at: TEST_CREATED_AT.into(),
expires_at: None,
generation: 1,
status: "pending".into(),
updated_at: TEST_CREATED_AT.into(),
};
for (alias, workdir_id, source) in [
(
"external",
grant.workdir_id.as_str(),
WorkdirRegistrySource::ExternalGrant {
grant_id: grant.grant_id.clone(),
},
),
(
"repository",
"restore-missing-repository",
WorkdirRegistrySource::Repository {
runtime_id: EMBEDDED_WORKER_RUNTIME_ID.into(),
repository_id: test_repository_id(&api),
},
),
] {
let record = WorkdirRegistryRecord {
workspace_id: TEST_WORKSPACE_ID.into(),
workdir_id: workdir_id.into(),
display_name: Some(alias.into()),
source,
creation_selector: None,
creation_ref: None,
creation_tree: None,
current_selector: None,
current_ref: None,
current_tree: None,
observed_at_epoch_seconds: None,
materialization_status: "not_found".into(),
cleanliness: "unknown".into(),
created_at: TEST_CREATED_AT.into(),
updated_at: TEST_CREATED_AT.into(),
};
if alias == "external" {
api.store
.create_external_workdir_grant(&grant, &record)
.unwrap();
assert!(
api.store
.activate_external_workdir_grant(
TEST_WORKSPACE_ID,
&grant.grant_id,
&grant.provider_instance_id,
1,
TEST_CREATED_AT,
)
.unwrap()
);
} else {
api.store.upsert_workdir_registry(&record).unwrap();
}
api.store
.attach_worker_workdir(&WorkerWorkdirLinkRecord {
connection_id: String::new(),
workspace_id: TEST_WORKSPACE_ID.into(),
worker: worker.clone(),
workdir_id: workdir_id.into(),
alias: alias.into(),
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
linked_at: TEST_CREATED_AT.into(),
unlinked_at: None,
})
.unwrap();
}
let links = api
.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
.unwrap();
api.runtime
.stop_worker(
&worker,
WorkerLifecycleRequest {
reason: None,
ticket_assignment: None,
},
)
.unwrap();
let result = api
.restore_workspace_worker(
&worker,
server_api::WorkerRestoreRequest {
expected_observation_token: api
.runtime
.worker(&worker)
.unwrap()
.restore_observation_token
.unwrap(),
request_id: uuid::Uuid::now_v7().to_string(),
},
)
.unwrap();
assert_eq!(result.state, server_api::WorkerRestoreState::Accepted);
let attachments = restore_logical_workdir_attachments(&api, &worker).unwrap();
assert_eq!(attachments.len(), 2);
assert!(
attachments
.iter()
.any(|attachment| attachment.alias.as_str() == "external"
&& attachment.working_directory_id == grant.workdir_id)
);
assert!(
attachments
.iter()
.any(|attachment| attachment.alias.as_str() == "repository"
&& attachment.working_directory_id == "restore-missing-repository")
);
assert_eq!(
api.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
.unwrap(),
links
);
let lock = current_worker_session_lock(&api, &worker);
let guard = Arc::new(lock.lock_owned().await);
for link in &links {
let error = open_current_worker_workdir_session_locked(&api, &worker, link, &guard)
.await
.unwrap_err();
match link.alias.as_str() {
"external" => assert!(matches!(
error,
Error::RuntimeOperationFailed { code, .. }
if code == "external_workdir_provider_offline"
)),
"repository" => assert!(matches!(
error,
Error::WorkdirSessionOpenFailed { transport, .. }
if transport.code == workdir::http::WorkdirTransportErrorCode::Unavailable
)),
alias => panic!("unexpected attachment: {alias}"),
}
}
assert_eq!(
api.store
.list_worker_workdir_links(TEST_WORKSPACE_ID, &worker)
.unwrap(),
links
);
api.runtime
.stop_worker(
&worker,
WorkerLifecycleRequest {
reason: None,
ticket_assignment: None,
},
)
.unwrap();
}
@@ -156,6 +156,7 @@ async fn spawn_pair_member(api: &WorkspaceApi, name: &str) -> RuntimeWorkerRef {
workdir_attachments: vec![],
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
File diff suppressed because it is too large Load Diff
@@ -527,6 +527,7 @@ async fn subjektiv_job_legacy_cutover_waits_foreground_then_removes_only_owned_c
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: vec![],
resolved_control_operation: None,
resolved_execution_grant: None,
},
)
.unwrap()
@@ -260,7 +260,7 @@ async fn connected_create(api: &WorkspaceApi, subject: &str, name: &str) -> Runt
workdir_attachments: vec![],
feature_connections: server_api::WorkspaceWorkerFeatureConnectionsRequest {
subjektiv: Some(server_api::SubjektivWorkerConnectionRequest { subject_id: subject.into() }),
}, control_operation_id: None,
}, control_operation_id: None, execution_grant: None,
}),
).await.unwrap();
RuntimeWorkerRef::new(created.runtime_id, created.worker_id)
@@ -386,6 +386,56 @@ async fn backend_real_runtime_journal_release_and_next_body_persisted_startup()
let subject = store
.create_subject(crate::subjektiv::SubjectRole::new("connected-handoff").unwrap())
.unwrap();
let owner =
seed_test_workspace_owner_token(api.store.as_ref(), "connected-journal-owner").await;
// This fixture exercises Journal continuity, not unset-policy refusal. Assign
// the companion's exact activation keys through authenticated administration;
// no filesystem, shell or delegation permission is granted to its body.
let update = server_api::authorization::SubjectPermissionsUpdate {
operation_id: "connected-journal-activation".into(),
expected_change_id: None,
execution: [
"task",
"subjektiv",
"web",
"image",
"sub_worker",
"worker",
"workspace_worker_discovery",
"objective",
"manage_workdir",
"workdir_catalog",
"drive",
"ticket",
]
.into_iter()
.map(|name| server_api::authorization::ScopedPermission {
operation: name.into(),
resource: server_api::authorization::PermissionResource::Feature,
})
.collect(),
delegation: Vec::new(),
};
let response = build_router(api.clone())
.oneshot(
axum::http::Request::builder()
.method("POST")
.uri(format!(
"/api/w/{TEST_WORKSPACE_ID}/subjektiv/subjects/{}/permissions",
subject.id
))
.header("authorization", format!("Bearer {owner}"))
.header(CONTENT_TYPE, "application/json")
.body(Body::from(serde_json::to_vec(&update).unwrap()))
.unwrap(),
)
.await
.unwrap();
let status = response.status();
let body = to_bytes(response.into_body(), 2 * 1024 * 1024)
.await
.unwrap();
assert_eq!(status, StatusCode::OK, "{}", String::from_utf8_lossy(&body));
let old = connected_create(&api, &subject.id, "connected-old-body").await;
let old_attr = store
.subject_session_attributions(&subject.id)
@@ -547,30 +597,40 @@ async fn backend_real_runtime_journal_release_and_next_body_persisted_startup()
let segments = session.list_segments(session_id).unwrap();
assert_eq!(segments.len(), 1);
let entries = session.read_all_read_only(session_id, segments[0]).unwrap();
#[derive(serde::Deserialize)]
struct SavedJournal {
snapshot: serde_json::Value,
snapshot_sha256: String,
prompt_provenance: session_store::PromptRenderProvenance,
}
let startup: Vec<_> = entries
.iter()
.enumerate()
.filter_map(|(index, log)| match log {
session_store::LogEntry::AnnotatedSystemItem { entry, .. } => match &entry.item {
session_store::SystemItem::JournalStartup {
snapshot,
snapshot_sha256,
body,
prompt_provenance,
} => Some((index, snapshot, snapshot_sha256, body, prompt_provenance)),
_ => None,
},
_ => None,
.filter_map(|(index, log)| {
let session_store::LogEntry::AnnotatedSystemItem { entry, .. } = log else {
return None;
};
let payload = entry
.metadata
.input_metadata
.as_ref()?
.feature_payload()?
.decode::<SavedJournal>("journal:startup", 1)
.ok()?;
Some((index, payload, entry.item.history_text()))
})
.collect();
assert_eq!(startup.len(), 1, "startup must be durably committed once");
let (startup_index, snapshot, digest, body, provenance) = startup[0];
let (startup_index, payload, body) = &startup[0];
let snapshot = &payload.snapshot;
let digest = &payload.snapshot_sha256;
let provenance = &payload.prompt_provenance;
let first_input = entries
.iter()
.position(|entry| matches!(entry, session_store::LogEntry::AnnotatedUserInput { .. }))
.expect("new body persisted its initial human input");
assert!(
startup_index < first_input,
*startup_index < first_input,
"startup reference must be persisted before the first human input/model request"
);
assert_eq!(snapshot["subject_id"], subject.id);
@@ -479,6 +479,7 @@ fn seed_session(
vec![session_store::LoggedHistoryEntry {
item,
metadata: session_store::LoggedSessionHistoryMetadata {
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId("read-source-original".into()),
origin,
derivation: None,
@@ -1686,6 +1687,7 @@ impl SubjectRemovalFixture {
}),
},
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -1706,6 +1708,7 @@ impl SubjectRemovalFixture {
.map(|text| session_store::LoggedHistoryEntry {
item: message(text),
metadata: session_store::LoggedSessionHistoryMetadata {
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId(text.into()),
origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput,
derivation: None,
@@ -67,6 +67,7 @@ impl Fixture {
content: vec![session_store::LoggedContentPart::Text { text: text.into() }],
},
metadata: session_store::LoggedSessionHistoryMetadata {
input_metadata: None,
entry_id: session_store::LoggedSessionHistoryEntryId(format!("original-{index}")),
origin: session_store::LoggedSessionHistoryOrigin::LocalHumanInput,
derivation: None,
@@ -1032,6 +1032,7 @@ async fn read_only_ticket_targets_allow_extra_external_grants_without_widening_c
resolved_memory_settings: None,
resolved_subjektiv_attached: false,
resolved_control_operation: None,
resolved_execution_grant: None,
};
api.validate_worker_spawn_repository_scope(EMBEDDED_WORKER_RUNTIME_ID, &mut request)
.unwrap();
@@ -1580,6 +1581,7 @@ async fn mr_open_retains_source_before_first_review_and_observed_result() {
}],
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -200,6 +200,7 @@ impl Fixture {
workdir_attachments: vec![],
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -517,7 +518,7 @@ async fn user_uploaded_markdown_roundtrips_through_workdirless_tools_and_native_
read["entry"]["metadata"]["last_mutation_id"],
uploaded["entry"]["last_mutation_id"]
);
value(native_tool(&tools, "Invoke", json!({"path":path,"interface":reference,"operation":"write","arguments":{"content":updated_text}})).await.unwrap())
value(native_tool(&tools, "Invoke", json!({"path":path,"interface":file_write_reference(&inspected),"operation":"write","arguments":{"content":updated_text}})).await.unwrap())["metadata"].clone()
} else {
let read = value(
tool(&f.features[0], "DriveRead", json!({"entry":id}))
@@ -733,6 +734,19 @@ async fn worker_removal_preserves_drive_reference_for_explicitly_granted_replace
.await
.unwrap();
assert!(f.api.runtime.worker(old).is_err());
let client = f.clients[0].clone();
let capability = tokio::task::spawn_blocking(move || {
client.request(
WorkspaceRequestMethod::Get,
format!("{}/capabilities", base()),
vec![],
65536,
)
})
.await
.unwrap()
.unwrap();
assert!(!(200..300).contains(&capability.status));
assert!(
f.api
.store
@@ -754,6 +768,7 @@ async fn worker_removal_preserves_drive_reference_for_explicitly_granted_replace
workdir_attachments: vec![],
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -1571,11 +1586,30 @@ impl agen::llm_client::LlmClient for NoLlm {
panic!("native tool fixture must not call an LLM")
}
}
fn file_write_reference(observed: &Value) -> Value {
observed["interfaces"]
.as_array()
.unwrap()
.iter()
.find(|i| i["reference"]["name"] == "yoi.file/write-text/v1")
.expect("write capability")["reference"]
.clone()
}
fn native_tools(
feature: &DriveFeature,
) -> std::collections::BTreeMap<String, Arc<dyn agen::tool::Tool>> {
native_tools_with_checkouts(feature, None)
}
fn native_tools_with_checkouts(
feature: &DriveFeature,
router: Option<Arc<WorkdirSessionRouter>>,
) -> std::collections::BTreeMap<String, Arc<dyn agen::tool::Tool>> {
let mut mounts = worker::wip::WipMountRegistry::new();
worker::feature::builtin::drive::wip::mount_drive_wip(&mut mounts, feature).unwrap();
if let Some(router) = router {
worker::checkout::mount_checkouts(&mut mounts, router, tools::Tracker::new(), None)
.unwrap();
}
let mut engine = agen::Engine::<_, agen::state::Mutable, ()>::new_annotated(NoLlm);
engine.register_tools(
feature
@@ -1651,7 +1685,14 @@ async fn native_tree_does_not_index_entries_but_direct_inspect_and_invoke_use_cu
);
assert_eq!(read["text"], "native content");
let denied_write = native_tool(&tools, "Invoke", json!({"path":path,"interface":reference,"operation":"write","arguments":{"content":"lost"}})).await;
denied(denied_write);
assert!(denied_write.is_err());
assert!(
!inspected["interfaces"]
.as_array()
.unwrap()
.iter()
.any(|i| i["reference"]["name"] == "yoi.file/write-text/v1")
);
f.revoke(1).await;
assert!(
native_tool(
@@ -1705,12 +1746,12 @@ async fn native_create_and_write_use_client_managed_committed_requests_and_rejec
.await
.unwrap(),
);
let updated = value(native_tool(&first, "Invoke", json!({"path":path,"interface":observed_first["interfaces"][0]["reference"],"operation":"write","arguments":{"content":"committed"}})).await.unwrap());
let updated = value(native_tool(&first, "Invoke", json!({"path":path,"interface":file_write_reference(&observed_first),"operation":"write","arguments":{"content":"committed"}})).await.unwrap())["metadata"].clone();
assert_ne!(
updated["entry"]["metadata"]["last_mutation_id"],
created["entry"]["metadata"]["last_mutation_id"]
);
let error = native_tool(&second, "Invoke", json!({"path":path,"interface":observed_second["interfaces"][0]["reference"],"operation":"write","arguments":{"content":"lost"}})).await.unwrap_err();
let error = native_tool(&second, "Invoke", json!({"path":path,"interface":file_write_reference(&observed_second),"operation":"write","arguments":{"content":"lost"}})).await.unwrap_err();
assert!(
error.to_string().to_lowercase().contains("validator"),
"stale native mutation must report validator conflict: {error:?}"
@@ -1833,3 +1874,143 @@ async fn image_attachment_retains_original_bytes_after_drive_update_and_revocati
"history attachment must not fetch latest Drive content"
);
}
#[tokio::test]
async fn common_file_inspect_invoke_crosses_drive_and_checkout_without_transferring_authority() {
let f = Fixture::new(["read_write", "read_only"]).await;
let created = f.create("common.md", "first\n雪 needle\nlast\n").await;
let path = created["entry"]["path"].as_str().unwrap();
let checkout = f._temp.path().join("checkout");
fs::create_dir(&checkout).unwrap();
fs::write(checkout.join("file"), "first\n雪 needle\nlast\n").unwrap();
let router = Arc::new(WorkdirSessionRouter::new());
router
.attach(
workdir::WorkdirAttachmentAlias::new("main").unwrap(),
Arc::new(workdir::LocalWorkdirSession::materialized(
checkout.clone(),
checkout.clone(),
manifest::SharedScope::new(manifest::Scope::writable(&checkout).unwrap()),
workdir::WorkdirSessionCapabilities::READ_WRITE,
)),
)
.unwrap();
let tools = native_tools_with_checkouts(&f.features[0], Some(router));
let drive = value(
native_tool(&tools, "Inspect", json!({"path":path}))
.await
.unwrap(),
);
let local = value(
native_tool(&tools, "Inspect", json!({"path":"/checkouts/main/file"}))
.await
.unwrap(),
);
for name in [
"yoi.file/read-lines/v1",
"yoi.file/search-text/v1",
"yoi.file/edit-text/v1",
"yoi.file/write-text/v1",
] {
let selected = drive["interfaces"]
.as_array()
.unwrap()
.iter()
.find(|i| i["reference"]["name"] == name)
.unwrap();
let reference = &selected["reference"];
assert_eq!(reference["scope"], "/");
assert!(
local["interfaces"]
.as_array()
.unwrap()
.iter()
.any(|i| i["reference"] == *reference)
);
let signature = value(
native_tool(&tools, "Inspect", json!({"path":selected["path"]}))
.await
.unwrap(),
);
assert!(
signature["interface_signature"]
.as_str()
.unwrap()
.contains(name)
);
let (operation, arguments) = match name {
"yoi.file/read-lines/v1" => ("read", json!({"offset":1,"limit":1})),
"yoi.file/search-text/v1" => ("search", json!({"pattern":"needle","limit":2})),
"yoi.file/edit-text/v1" => {
("edit", json!({"old_string":"needle","new_string":"found"}))
}
_ => ("write", json!({"content":"saved\n"})),
};
let mut results = vec![];
for target in [path, "/checkouts/main/file"] {
results.push(value(native_tool(&tools,"Invoke",json!({"path":target,"interface":reference,"operation":operation,"arguments":arguments})).await.unwrap()));
}
if matches!(operation, "read" | "search") {
assert_eq!(results[0], results[1]);
} else {
assert!(results.iter().all(|v| v.get("metadata").is_some()));
}
}
assert_eq!(
fs::read_to_string(checkout.join("file")).unwrap(),
"saved\n"
);
assert_eq!(
value(
tool(
&f.features[1],
"DriveRead",
json!({"entry":entry(&created)})
)
.await
.unwrap()
)["text"],
"saved\n"
);
// A capability query is not a grant: drop the writer to readonly after the common write was cached.
f.revoke(0).await;
owner_call(&f.api,&f.owner,"POST",&format!("{}/grants",base()),Some(json!({"runtime_id":f.clients[0].worker.runtime_id,"worker_id":f.clients[0].worker.worker_id,"access":"read_only"}))).await;
assert!(native_tool(&tools,"Invoke",json!({"path":path,"interface":file_write_reference(&drive),"operation":"write","arguments":{"content":"lost"}})).await.is_err());
let refreshed = value(
native_tool(&tools, "Inspect", json!({"path":path,"refresh":true}))
.await
.unwrap(),
);
assert!(
!refreshed["interfaces"]
.as_array()
.unwrap()
.iter()
.any(|i| i["reference"]["name"] == "yoi.file/write-text/v1")
);
let common_read = json!({"scope":"/","name":"yoi.file/read-lines/v1"});
assert_eq!(
value(
native_tool(
&tools,
"Invoke",
json!({"path":path,"interface":common_read,"operation":"read","arguments":{}})
)
.await
.unwrap()
)["content"],
" 1\tsaved\n"
);
assert_eq!(
value(
tool(
&f.features[1],
"DriveRead",
json!({"entry":entry(&created)})
)
.await
.unwrap()
)["text"],
"saved\n"
);
}
@@ -1593,7 +1593,7 @@ async fn restore_guard_workspace_interrupted_preparation_recovers_admitted_snaps
}
#[tokio::test]
async fn restore_guard_cross_runtime_ssh_completion_retries_pinned_snapshot_at_workdir_owner() {
async fn restore_keeps_cross_runtime_ssh_attachment_without_issuing_repository_access() {
let workspace = tempfile::tempdir().unwrap();
let mut config = test_server_config(workspace.path());
let source = server_api::RepositorySource {
@@ -1619,39 +1619,6 @@ async fn restore_guard_cross_runtime_ssh_completion_retries_pinned_snapshot_at_w
.stop(&WorkerOperationContext::Backend, operation_lifecycle())
.await
.unwrap();
let original_key = api
.repository_secrets
.generate_credential(
api.workspace_id(),
GenerateRepositorySshCredentialRequest {
operation_id: "cross-runtime-default-key".into(),
credential_id:
crate::repository_access::WORKSPACE_DEFAULT_REPOSITORY_SSH_CREDENTIAL_ID.into(),
name: "Restore default".into(),
},
"owner-account",
)
.unwrap();
let public_key = api
.repository_secrets
.credential_public_key(api.workspace_id(), &original_key.credential_id)
.unwrap()
.unwrap()
.public_key;
api.repository_secrets
.put_host_trust(
api.workspace_id(),
PutRepositorySshHostTrustRequest {
operation_id: "cross-runtime-trust".into(),
host_trust_id: "cross-runtime-host".into(),
hostname: "example.invalid".into(),
port: 22,
host_key: public_key,
expected_fingerprint: None,
},
"owner-account",
)
.unwrap();
let record = restore_test_workdir(
&api,
"cross-runtime-restore-workdir",
@@ -1663,102 +1630,27 @@ async fn restore_guard_cross_runtime_ssh_completion_retries_pinned_snapshot_at_w
peer.set_workdir_repository(&record.workdir_id, &test_repository_id(&api));
api.store.upsert_workdir_registry(&record).unwrap();
attach_restore_test_workdir(&api, &identity, &record);
let links = api
.store
.list_worker_workdir_links(api.workspace_id(), &identity)
.unwrap();
let request = restore_request_for(&api, &identity, "cross-runtime-restore-request");
assert_eq!(
handle
.restore(&WorkerOperationContext::Backend, request.clone())
.await
.unwrap()
.state,
server_api::WorkerRestoreState::ReconciliationRequired
);
assert!(
execution.restore_operations.lock().unwrap().is_empty(),
"credential completion must precede execution"
);
let first = peer.repository_access_requests.lock().unwrap()[0].clone();
assert_eq!(
first.materialization.runtime_id,
WorkdirlessFixtureRuntime::RUNTIME_ID
);
assert_ne!(first.materialization.runtime_id, identity.runtime_id);
// Rotate current host trust while the accepted operation is uncertain. Its
// original credential/trust fingerprints, source and binding must remain pinned.
let alternate = api
.repository_secrets
.generate_credential(
api.workspace_id(),
GenerateRepositorySshCredentialRequest {
operation_id: "cross-runtime-alternate-key".into(),
credential_id: "cross-runtime-alternate".into(),
name: "Alternate".into(),
},
"owner-account",
)
.unwrap();
let alternate_public = api
.repository_secrets
.credential_public_key(api.workspace_id(), &alternate.credential_id)
.unwrap()
.unwrap()
.public_key;
let original_trust_fingerprint = first
.materialization
.ssh
.as_ref()
.unwrap()
.host_key_fingerprint
.clone();
api.repository_secrets
.put_host_trust(
api.workspace_id(),
PutRepositorySshHostTrustRequest {
operation_id: "cross-runtime-trust-rotate".into(),
host_trust_id: "cross-runtime-host".into(),
hostname: "example.invalid".into(),
port: 22,
host_key: alternate_public,
expected_fingerprint: Some(original_trust_fingerprint),
},
"owner-account",
)
.unwrap();
*peer.reject_repository_access.lock().unwrap() = false;
assert_eq!(
handle
.restore(&WorkerOperationContext::Backend, request.clone())
.await
.unwrap()
.state,
server_api::WorkerRestoreState::Accepted
);
let attempts = peer.repository_access_requests.lock().unwrap();
assert_eq!(attempts.len(), 2);
let second = &attempts[1];
assert_eq!(
second.materialization.operation_id,
first.materialization.operation_id
);
let mut normalized = second.clone();
let old_ssh = first.materialization.ssh.as_ref().unwrap();
let new_ssh = normalized.materialization.ssh.as_mut().unwrap();
assert_ne!(new_ssh.secret_resource.nonce, old_ssh.secret_resource.nonce);
new_ssh.secret_resource = old_ssh.secret_resource.clone();
new_ssh.expires_at_epoch_seconds = old_ssh.expires_at_epoch_seconds;
assert_eq!(normalized, first, "only opaque handle and expiry may renew");
drop(attempts);
assert_eq!(execution.restore_operations.lock().unwrap().len(), 1);
assert_eq!(
handle
.restore(&WorkerOperationContext::Backend, request)
.await
.unwrap()
.state,
server_api::WorkerRestoreState::Accepted
);
assert_eq!(
peer.repository_access_requests.lock().unwrap().len(),
2,
"terminal receipt must not renew access"
);
for _ in 0..2 {
assert_eq!(
handle
.restore(&WorkerOperationContext::Backend, request.clone())
.await
.unwrap()
.state,
server_api::WorkerRestoreState::Accepted
);
assert!(peer.repository_access_requests.lock().unwrap().is_empty());
assert_eq!(execution.restore_operations.lock().unwrap().len(), 1);
assert_eq!(
api.store
.list_worker_workdir_links(api.workspace_id(), &identity)
.unwrap(),
links
);
}
}
@@ -256,6 +256,7 @@ fn read_result() -> WorkdirSessionOperationResult {
total_lines: 1,
content_hash: sha2::Sha256::digest(b"visible\n").into(),
truncated: false,
byte_truncated: false,
})
}
@@ -0,0 +1,521 @@
//! Subject resource checks at the Workdir provider boundary, in addition to existing ceilings.
//!
//! Paths here are attachment-relative lexical resources. Providers still own resolved-path,
//! symlink, capability and checkout validation; this helper never substitutes for those checks.
use server_api::authorization::{PermissionResource, ScopedPermission};
use workdir::{
WorkdirPath, WorkdirSessionCapabilities, WorkdirToolScopePermission,
http::WorkdirSessionOperation,
};
use super::{ApiError, ApiResult, Error, RuntimeWorkerRef, WorkspaceApi};
use crate::store::WorkerWorkdirLinkRecord;
const READ: &str = "filesystem.read";
const WRITE: &str = "filesystem.write";
const COMMAND: &str = "bash.execute";
pub(super) fn authorize(
api: &WorkspaceApi,
worker: &RuntimeWorkerRef,
link: &WorkerWorkdirLinkRecord,
operation: &WorkdirSessionOperation,
) -> ApiResult<()> {
// These are reconciliation/termination of an already registered effect, not execution.
// Keep the existing worker/attachment/opaque-handle ownership checks on these routes.
if is_command_reconciliation(operation) {
return Ok(());
}
let permissions = super::subject_authorization::worker_permissions(api, worker)?;
authorize_permissions(permissions.as_deref(), &link.workdir_id, operation)
}
fn is_command_reconciliation(operation: &WorkdirSessionOperation) -> bool {
matches!(
operation,
WorkdirSessionOperation::CommandCancel(_) | WorkdirSessionOperation::CommandStatus(_)
)
}
fn denied(message: &str) -> ApiError {
Error::WorkspacePermissionDenied(format!("Workdir Subject authorization: {message}")).into()
}
fn resource(
working_directory_id: &str,
operation: &str,
path: &WorkdirPath,
recursive: bool,
) -> ApiResult<ScopedPermission> {
// FsPath also accepts explicitly scoped absolute paths. Subject resources here are
// attachment-relative only, even if the provider separately supports absolute grants.
let normalized = WorkdirPath::new(path.as_str())
.map_err(|_| denied("operation path must be attachment-relative without escape"))?;
let permission = ScopedPermission {
operation: operation.into(),
resource: PermissionResource::Workdir {
working_directory_id: working_directory_id.into(),
path: if normalized.is_root() {
".".into()
} else {
normalized.as_str().into()
},
recursive,
},
};
permission
.validate()
.map_err(|_| denied("invalid normalized operation resource"))?;
Ok(permission)
}
fn require(
permissions: &[ScopedPermission],
working_directory_id: &str,
operation: &str,
path: &WorkdirPath,
recursive: bool,
) -> ApiResult<()> {
let requested = resource(working_directory_id, operation, path, recursive)?;
if permissions.iter().any(|granted| granted.covers(&requested)) {
Ok(())
} else {
Err(denied("operation or resource is outside the current grant"))
}
}
fn authorize_permissions(
permissions: Option<&[ScopedPermission]>,
working_directory_id: &str,
operation: &WorkdirSessionOperation,
) -> ApiResult<()> {
if is_command_reconciliation(operation) {
return Ok(());
}
let Some(permissions) = permissions else {
// Non-Subject Workers retain the existing provider/Profile/attachment ceilings.
return Ok(());
};
let check = |operation, path: &WorkdirPath, recursive| {
require(
permissions,
working_directory_id,
operation,
path,
recursive,
)
};
match operation {
WorkdirSessionOperation::Stat(request) => check(READ, &request.path, false),
WorkdirSessionOperation::Read(request) => check(READ, &request.path, false),
WorkdirSessionOperation::ReadBytes(request) => check(READ, &request.path, false),
WorkdirSessionOperation::Write(request) => check(WRITE, &request.path, false),
WorkdirSessionOperation::Edit(request) => check(WRITE, &request.path, false),
WorkdirSessionOperation::List(request) => {
// Directory enumeration accesses children, not just the directory inode.
check(READ, &request.path, true)?;
if let Some(cursor) = &request.after {
check(READ, &cursor.path, false)?;
}
Ok(())
}
WorkdirSessionOperation::Glob(request) => check(READ, &request.path, true),
WorkdirSessionOperation::Grep(request) => check(READ, &request.path, true),
WorkdirSessionOperation::AuthorizeScope(request) => check(
match request.permission {
WorkdirToolScopePermission::Read => READ,
WorkdirToolScopePermission::Write => WRITE,
},
&request.path,
false,
),
WorkdirSessionOperation::CommandStart(request) => {
// Validating cwd is not a shell confinement mechanism. The command can cd elsewhere,
// so a prefix grant (even one covering cwd recursively) is never enough.
resource(working_directory_id, COMMAND, &request.cwd, false)?;
check(COMMAND, &WorkdirPath::root(), true)
}
WorkdirSessionOperation::CommandOutput(_) => {
// Output may contain anything the original shell read. Unlike status/cancel,
// retrieving its contents requires a current whole-attachment command grant.
check(COMMAND, &WorkdirPath::root(), true)
}
WorkdirSessionOperation::CommandCancel(_) | WorkdirSessionOperation::CommandStatus(_) => {
Ok(())
}
// These contracts have additional targets, scopes or provider-resolved metadata.
// Deny explicitly until all their accesses can be mapped to scoped resources.
WorkdirSessionOperation::ScopeRulesOverlap(_)
| WorkdirSessionOperation::CheckoutObserve(_)
| WorkdirSessionOperation::CheckoutExecute(_)
| WorkdirSessionOperation::CheckoutSearch(_) => {
Err(denied("operation has no scoped Subject resource mapping"))
}
}
}
/// Attenuate coarse attachment capabilities, without converting a path grant into root scope.
/// Per-operation authorization above remains required, including for recursive searches.
pub(super) fn intersect_capabilities(
permissions: &[ScopedPermission],
working_directory_id: &str,
ceiling: WorkdirSessionCapabilities,
) -> WorkdirSessionCapabilities {
let has_path_grant = |operation: &str| {
permissions.iter().any(|grant| {
grant.operation == operation
&& grant.validate().is_ok()
&& matches!(&grant.resource, PermissionResource::Workdir {
working_directory_id: id, ..
} if id == working_directory_id)
})
};
let mut allowed = WorkdirSessionCapabilities::EMPTY;
if has_path_grant(READ) {
allowed = allowed.union(WorkdirSessionCapabilities::READ_ONLY);
}
if has_path_grant(WRITE) {
allowed = allowed.union(WorkdirSessionCapabilities::WRITE_ONLY);
}
if require(
permissions,
working_directory_id,
COMMAND,
&WorkdirPath::root(),
true,
)
.is_ok()
{
allowed = allowed.union(WorkdirSessionCapabilities::COMMAND_ONLY);
}
ceiling.intersection(allowed)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use workdir::{
CommandHandle, CommandOutputRequest, CommandRequest, EditRequest, GlobRequest, GrepRequest,
ListRequest, ReadBytesRequest, ReadRequest, StatRequest, WriteRequest,
};
fn path(value: &str) -> WorkdirPath {
WorkdirPath::new(value).unwrap()
}
fn grant(operation: &str, target: &str, recursive: bool) -> ScopedPermission {
resource("wd", operation, &path(target), recursive).unwrap()
}
fn allowed(grants: &[ScopedPermission], operation: &WorkdirSessionOperation) -> bool {
authorize_permissions(Some(grants), "wd", operation).is_ok()
}
fn read(target: &str) -> WorkdirSessionOperation {
WorkdirSessionOperation::Read(ReadRequest {
path: path(target),
offset: 0,
limit: 10,
max_bytes: 1024,
})
}
fn command(cwd: &str) -> WorkdirSessionOperation {
WorkdirSessionOperation::CommandStart(CommandRequest {
command: "cd ..; cat secret".into(),
timeout_secs: 1,
transport_byte_budget: 1024,
cwd: path(cwd),
tool_call_id: None,
})
}
#[test]
fn real_filesystem_operations_require_exact_operation_and_attachment_scope() {
let read_grant = grant(READ, "src", true);
let write_grant = grant(WRITE, "src", true);
let reads = [
read("src/a"),
WorkdirSessionOperation::Stat(StatRequest {
path: path("src/a"),
}),
WorkdirSessionOperation::ReadBytes(ReadBytesRequest {
path: path("src/a"),
offset: 0,
max_bytes: 10,
expected_hash: None,
}),
WorkdirSessionOperation::List(ListRequest {
path: path("src"),
limit: 10,
after: None,
}),
WorkdirSessionOperation::Glob(GlobRequest {
pattern: "**/*".into(),
path: path("src"),
limit: 10,
}),
WorkdirSessionOperation::Grep(GrepRequest {
pattern: "needle".into(),
path: path("src"),
glob: None,
file_type: None,
case_insensitive: false,
before_context: 0,
after_context: 0,
multiline: false,
output_mode: Default::default(),
limit: 10,
offset: 0,
}),
];
for operation in reads {
assert!(
allowed(std::slice::from_ref(&read_grant), &operation),
"{operation:?}"
);
assert!(
!allowed(std::slice::from_ref(&write_grant), &operation),
"{operation:?}"
);
assert!(
authorize_permissions(
Some(std::slice::from_ref(&read_grant)),
"other-wd",
&operation
)
.is_err()
);
}
let writes = [
WorkdirSessionOperation::Write(WriteRequest {
path: path("src/a"),
content: vec![],
expected_hash: None,
}),
WorkdirSessionOperation::Edit(EditRequest {
path: path("src/a"),
old_string: "a".into(),
new_string: "b".into(),
replace_all: false,
expected_hash: [0; 32],
}),
];
for operation in writes {
assert!(allowed(std::slice::from_ref(&write_grant), &operation));
assert!(!allowed(std::slice::from_ref(&read_grant), &operation));
}
assert!(!allowed(&[read_grant], &read("src-other/a")));
}
#[test]
fn activation_workspace_and_parent_operation_names_do_not_grant_file_access() {
for permission in [
ScopedPermission {
operation: "filesystem".into(),
resource: PermissionResource::Feature,
},
grant("filesystem", ".", true),
grant("filesystem.read.extra", ".", true),
ScopedPermission {
operation: READ.into(),
resource: PermissionResource::Workspace {
workspace_id: "ws".into(),
},
},
] {
assert!(!allowed(&[permission], &read("src/a")));
}
assert!(!allowed(&[], &read("src/a")));
assert!(authorize_permissions(None, "wd", &read("src/a")).is_ok());
}
#[test]
fn nonrecursive_grant_cannot_enumerate_descendants_or_search() {
let grants = [grant(READ, "src", false)];
assert!(allowed(&grants, &read("src")));
assert!(!allowed(&grants, &read("src/a")));
assert!(!allowed(
&grants,
&WorkdirSessionOperation::List(ListRequest {
path: path("src"),
limit: 10,
after: None
})
));
assert!(!allowed(
&grants,
&WorkdirSessionOperation::Glob(GlobRequest {
pattern: "a".into(),
path: path("src"),
limit: 10
})
));
}
#[test]
fn wire_paths_are_normalized_and_escape_is_denied_before_provider_dispatch() {
let grants = [grant(READ, "src", true)];
for bad in [
"/src/a",
"../src/a",
"src/../secret",
"src\\a",
"C:/src/a",
"src/\u{0}a",
] {
let operation = serde_json::from_value::<WorkdirSessionOperation>(json!({
"operation": "stat", "request": {"path": bad}
}));
// Some escapes are already rejected by FsPath's wire boundary. Absolute
// provider paths and malformed resource names must also fail this boundary.
if let Ok(operation) = operation {
assert!(!allowed(&grants, &operation), "{bad:?}");
}
}
let operation: WorkdirSessionOperation = serde_json::from_value(json!({
"operation": "stat", "request": {"path": "./src//a"}
}))
.unwrap();
assert!(allowed(&grants, &operation));
}
#[test]
fn scope_probes_and_list_cursors_cannot_access_outside_the_granted_resource() {
let grants = [grant(READ, "src", true)];
for (target, permission, expected) in [
("src/a", WorkdirToolScopePermission::Read, true),
("private/a", WorkdirToolScopePermission::Read, false),
("src/a", WorkdirToolScopePermission::Write, false),
] {
let operation = WorkdirSessionOperation::AuthorizeScope(
workdir::WorkdirScopeAuthorizationRequest {
rules: vec![],
path: path(target),
permission,
},
);
assert_eq!(allowed(&grants, &operation), expected);
}
let list = |cursor| {
WorkdirSessionOperation::List(ListRequest {
path: path("src"),
limit: 10,
after: Some(workdir::ListCursor {
kind: workdir::EntryKind::File,
path: path(cursor),
}),
})
};
assert!(allowed(&grants, &list("src/a")));
assert!(!allowed(&grants, &list("private/a")));
}
#[test]
fn arbitrary_shell_requires_recursive_attachment_root_not_cwd_scope() {
assert!(!allowed(&[grant(COMMAND, "src", true)], &command("src")));
assert!(!allowed(&[grant(COMMAND, ".", false)], &command("src")));
assert!(!allowed(&[grant(WRITE, ".", true)], &command("src")));
assert!(allowed(&[grant(COMMAND, ".", true)], &command("src")));
}
#[test]
fn revocation_denies_new_execute_and_output_but_preserves_cancel_and_status() {
let handle = CommandHandle("owned-opaque-handle".into());
assert!(allowed(
&[],
&WorkdirSessionOperation::CommandCancel(handle.clone())
));
assert!(allowed(
&[],
&WorkdirSessionOperation::CommandStatus(handle.clone())
));
assert!(!allowed(&[], &command(".")));
let output = WorkdirSessionOperation::CommandOutput(CommandOutputRequest {
handle,
cursor: 0,
limit: 10,
wait: false,
});
assert!(!allowed(&[], &output));
assert!(allowed(&[grant(COMMAND, ".", true)], &output));
}
#[test]
fn unsupported_checkout_and_scope_comparison_fail_closed_for_scoped_workers() {
let grants = [
grant(READ, ".", true),
grant(WRITE, ".", true),
grant(COMMAND, ".", true),
];
let operations = [
serde_json::from_value::<WorkdirSessionOperation>(json!({
"operation": "scope_rules_overlap",
"request": {
"left": {"target": "src", "permission": "read", "recursive": true},
"right": {"target": "private", "permission": "read", "recursive": true}
}
}))
.unwrap(),
WorkdirSessionOperation::CheckoutObserve(path("src")),
WorkdirSessionOperation::CheckoutExecute(workdir::CheckoutRequest {
target: path("src"),
validator: vec![],
operation: workdir::CheckoutOperation::Create {
path: path("outside/a"),
content: vec![],
},
}),
WorkdirSessionOperation::CheckoutSearch(workdir::CheckoutSearchRequest::new(
workdir::CheckoutSearchOperation::List(ListRequest {
path: path("src"),
limit: 10,
after: None,
}),
)),
];
for operation in operations {
assert!(!allowed(&grants, &operation));
assert!(authorize_permissions(None, "wd", &operation).is_ok());
}
}
#[test]
fn readonly_delegation_attenuates_edit_capable_profile_without_losing_narrow_read_scope() {
let grants = [grant(READ, "src", true)];
assert_eq!(
intersect_capabilities(&grants, "wd", WorkdirSessionCapabilities::ALL),
WorkdirSessionCapabilities::READ_ONLY
);
assert_eq!(
intersect_capabilities(&grants, "other", WorkdirSessionCapabilities::ALL),
WorkdirSessionCapabilities::EMPTY
);
assert_eq!(
intersect_capabilities(
&[grant(WRITE, ".", true)],
"wd",
WorkdirSessionCapabilities::READ_ONLY
),
WorkdirSessionCapabilities::EMPTY
);
assert_eq!(
intersect_capabilities(
&[grant(COMMAND, "src", true)],
"wd",
WorkdirSessionCapabilities::ALL
),
WorkdirSessionCapabilities::EMPTY
);
assert_eq!(
intersect_capabilities(
&[grant(COMMAND, ".", true)],
"wd",
WorkdirSessionCapabilities::READ_WRITE
),
WorkdirSessionCapabilities::EMPTY
);
}
}
@@ -466,7 +466,6 @@ impl AttentionWorkerFactory {
worker.with_notification_coalesce_delay(std::time::Duration::from_millis(10)),
worker::WorkerBootstrapLayout::RuntimeManagedRun {
run_dir: root.join(uuid::Uuid::now_v7().to_string()),
bash_output_dir: root.join("bash-output"),
},
worker::WorkerControllerTransport::InProcess,
)
@@ -799,7 +799,6 @@ impl DeliveryWorkerFactory {
worker::WorkerFilesystemAuthority::None,
worker::WorkerBootstrapLayout::RuntimeManagedRun {
run_dir: root.join(uuid::Uuid::now_v7().to_string()),
bash_output_dir: root.join("bash-output"),
},
worker::WorkerControllerTransport::InProcess,
)
@@ -53,6 +53,7 @@ async fn start_source(api: &mut WorkspaceApi, runtime: &WorkdirlessFixtureRuntim
resolved_memory_settings: None,
resolved_subjektiv_attached: false,
resolved_control_operation: None,
resolved_execution_grant: None,
},
);
summary.tags = vec!["orchestrator".into(), "singleton".into()];
@@ -59,6 +59,7 @@ async fn runtime_and_account_assign_retries_return_saved_result_after_clear_and_
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
@@ -794,6 +795,7 @@ async fn tracking_fixture(
resolved_memory_settings: None,
resolved_subjektiv_attached: false,
resolved_control_operation: None,
resolved_execution_grant: None,
},
);
summary.tags = vec!["orchestrator".into(), "singleton".into()];
@@ -238,6 +238,25 @@ pub(super) async fn root(
let drive = drive_grants::authorized_drive(api, context, workspace, false).await?;
blocking(false, move || drive.root()).await.map(public_node)
}
/// Query only the authenticated caller's current ceiling using the operation authorizer.
/// This is admission/publication evidence, never authority retained for a later mutation.
pub(super) async fn capabilities(
api: &WorkspaceApi,
context: &ServerRequestContext,
workspace: &str,
) -> DriveResult<DriveCapabilities> {
let drive = drive_grants::authorized_drive(api, context, workspace, false).await?;
let write = blocking(false, move || match drive.check_authorized(true) {
Ok(()) => Ok(true),
Err(workspace_drive::Error::Denied) => {
drive.check_authorized(false)?;
Ok(false)
}
Err(error) => Err(error),
})
.await?;
Ok(DriveCapabilities { write })
}
pub(super) async fn metadata(
api: &WorkspaceApi,
context: &ServerRequestContext,
@@ -581,6 +600,9 @@ pub(super) fn router(service: Arc<ServerApiContractService>) -> Router {
.merge(streaming_router(service.clone()))
.merge(server_api::server_api_axum::drive_root(service.clone()))
.merge(server_api::server_api_axum::drive_metadata(service.clone()))
.merge(server_api::server_api_axum::drive_capabilities(
service.clone(),
))
.merge(server_api::server_api_axum::drive_list(service.clone()))
.merge(server_api::server_api_axum::drive_search(service.clone()))
.merge(server_api::server_api_axum::drive_read_text(
@@ -14,7 +14,7 @@ use worker::{
struct TicketRouterClient {
api: WorkspaceApi,
identity: Arc<RuntimeIdentityMaterial>,
source: RuntimeWorkerRef,
source: Mutex<RuntimeWorkerRef>,
runtime: tokio::runtime::Handle,
lose_next_response: AtomicBool,
requests: Mutex<Vec<(String, u16, String)>>,
@@ -45,9 +45,10 @@ impl WorkspaceClient for TicketRouterClient {
WorkspaceRequestMethod::Patch => "PATCH",
WorkspaceRequestMethod::Delete => "DELETE",
};
let source = self.source.lock().unwrap().clone();
let signed = runtime_source_request(
&self.identity,
Some(&self.source.worker_id),
Some(&source.worker_id),
method,
&request.path,
request.body.unwrap_or_default().into_bytes(),
@@ -225,7 +226,7 @@ fn ticket_client(
Arc::new(TicketRouterClient {
api: api.clone(),
identity: Arc::new(identity),
source: source.clone(),
source: Mutex::new(source.clone()),
runtime: tokio::runtime::Handle::current(),
lose_next_response: AtomicBool::new(false),
requests: Mutex::new(Vec::new()),
@@ -307,18 +308,16 @@ async fn ticket_public_roundtrip_observed_answer_replays_without_changing_waitin
assert!(error.to_string().contains("409"), "{error}");
// A valid Runtime signature is not a Worker membership grant. The
// Backend must reject this unregistered source through the same tools.
let denied = Arc::new(TicketRouterClient {
api: api.clone(),
identity: client.identity.clone(),
source: RuntimeWorkerRef::new(
source.runtime_id.clone(),
"00000000-0000-7000-8000-000000000099",
),
runtime: tokio::runtime::Handle::current(),
lose_next_response: AtomicBool::new(false),
requests: Mutex::new(Vec::new()),
});
let denied_surface = TicketSurface::new(&dir.path().join("denied"), denied, wip).await;
// Activate the real Feature while the source is registered, then lose
// membership before dispatch. Otherwise activation itself correctly rejects
// the fixture before it can exercise the operation's authentication check.
let denied = ticket_client(&api, (*client.identity).clone(), &source);
let denied_surface =
TicketSurface::new(&dir.path().join("denied"), denied.clone(), wip).await;
*denied.source.lock().unwrap() = RuntimeWorkerRef::new(
source.runtime_id.clone(),
"00000000-0000-7000-8000-000000000099",
);
let error = denied_surface
.call("TicketAssignmentAnswer", input.clone(), context("denied"))
.await
@@ -187,7 +187,7 @@ async fn config_call(
runtime
.invoke(
path.into(),
config_interface(path),
if matches!(operation,"edit"|"write") {wip_protocol::InterfaceReference {scope:"/".into(),name:format!("yoi.file/{operation}-text/v1")}} else {config_interface(path)},
operation.into(),
args,
agen::tool::ToolExecutionContext::direct(),
@@ -859,6 +859,7 @@ async fn workspace_config_actual_remote_and_embedded_clients_use_authenticated_w
workdir_attachments: Vec::new(),
feature_connections: Default::default(),
control_operation_id: None,
execution_grant: None,
}),
)
.await
+11
View File
@@ -7131,6 +7131,16 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
fn create_browser_session(&self, record: &BrowserSessionRecord) -> Result<()> {
self.with_immediate_transaction(|conn| {
activation::require_active(conn, &record.user_id)?;
// Retain the real row throughout the non-authenticating recovery window, not
// forever. Issuance amortizes reclamation without extending marker lifetime or
// introducing another store/scheduler. Inactive stores do not grow between issues.
let cutoff = (chrono::Utc::now()
- chrono::Duration::seconds(crate::auth::SESSION_EXPIRY_GRACE_SECONDS))
.to_rfc3339();
conn.execute(
"DELETE FROM browser_sessions WHERE julianday(expires_at) <= julianday(?1) OR julianday(expires_at) IS NULL",
params![cutoff],
)?;
conn.execute(
r#"INSERT INTO browser_sessions (session_id, token_hash, user_id, created_at, expires_at, revoked_at)
VALUES (?1, ?2, ?3, ?4, ?5, ?6)"#,
@@ -21014,6 +21024,7 @@ CREATE UNIQUE INDEX ticket_current_worker_role_idx
resolved_worker_observation_enabled: false,
resolved_worker_observation_grants: Vec::new(),
resolved_control_operation: None,
resolved_execution_grant: None,
resolved_workspace_api: None,
resolved_memory_settings: None,
resolved_subjektiv_attached: false,