feat: support permissioned multi-repository ticket targets
This commit is contained in:
@@ -209,7 +209,9 @@ pub struct LogicalWorkdirAttachment {
|
||||
}
|
||||
|
||||
fn default_workdir_session_capabilities() -> workdir::WorkdirSessionCapabilities {
|
||||
workdir::WorkdirSessionCapabilities::ALL
|
||||
// Capability-less wire and persisted records fail closed instead of
|
||||
// silently regaining write or command authority.
|
||||
workdir::WorkdirSessionCapabilities::READ_ONLY
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -218,6 +220,9 @@ pub struct WorkingDirectoryAttachmentClaim {
|
||||
pub working_directory_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub relative_cwd: Option<String>,
|
||||
/// Backend-authoritative maximum capabilities for the attachment session.
|
||||
#[serde(default = "default_workdir_session_capabilities")]
|
||||
pub capabilities: workdir::WorkdirSessionCapabilities,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1801,6 +1801,11 @@ impl<'de> Deserialize<'de> for WorkerExecutionRecord {
|
||||
alias,
|
||||
working_directory_id: claim.working_directory_id,
|
||||
relative_cwd: claim.relative_cwd,
|
||||
// The removed singular field represented a full local
|
||||
// Workdir binding. Preserve that known legacy contract
|
||||
// explicitly; missing capabilities on the current claim
|
||||
// shape default to read-only instead.
|
||||
capabilities: workdir::WorkdirSessionCapabilities::ALL,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -2474,10 +2479,41 @@ mod tests {
|
||||
assert_eq!(attachment.alias.as_str(), "workdir");
|
||||
assert_eq!(attachment.working_directory_id, "wd-legacy");
|
||||
assert_eq!(attachment.relative_cwd.as_deref(), Some("crates/yoi"));
|
||||
assert_eq!(
|
||||
attachment.capabilities,
|
||||
workdir::WorkdirSessionCapabilities::ALL
|
||||
);
|
||||
let serialized = serde_json::to_value(execution).unwrap();
|
||||
assert!(serialized["request"].get("working_directory").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_attachment_claim_without_capabilities_defaults_to_read_only() {
|
||||
let worker_id = WorkerId::now_v7();
|
||||
let mut request = schema_v7_worker_document(worker_id)["request"].clone();
|
||||
request["workdir_attachments"] = serde_json::json!([{
|
||||
"alias": "docs",
|
||||
"working_directory_id": "wd-docs"
|
||||
}]);
|
||||
let execution: WorkerExecutionRecord = serde_json::from_value(serde_json::json!({
|
||||
"schema_version": SCHEMA_VERSION,
|
||||
"request": request,
|
||||
"binding": {},
|
||||
"restore_intent": "automatic"
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
execution.request.workdir_attachments[0].capabilities,
|
||||
workdir::WorkdirSessionCapabilities::READ_ONLY
|
||||
);
|
||||
let serialized = serde_json::to_value(execution).unwrap();
|
||||
assert_eq!(
|
||||
serialized["request"]["workdir_attachments"][0]["capabilities"],
|
||||
serde_json::to_value(workdir::WorkdirSessionCapabilities::READ_ONLY).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_identity_round_trips_logical_workdir_attachments() {
|
||||
let worker_id = WorkerId::now_v7();
|
||||
@@ -2512,7 +2548,7 @@ mod tests {
|
||||
[LogicalWorkdirAttachment {
|
||||
alias: workdir::WorkdirAttachmentAlias::new("checkout").unwrap(),
|
||||
working_directory_id: "remote-workdir".to_string(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::ALL,
|
||||
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2215,8 +2215,13 @@ impl Runtime {
|
||||
let _ = worker.apply_worker_state(snapshot);
|
||||
}
|
||||
worker.restore_intent = restore_intent_for_status(worker.status);
|
||||
let claims = worker
|
||||
.request
|
||||
.as_ref()
|
||||
.map(|request| request.workdir_attachments.as_slice())
|
||||
.unwrap_or_default();
|
||||
worker.logical_workdir_attachments =
|
||||
logical_workdir_attachments_from_statuses(&workdir_attachments);
|
||||
logical_workdir_attachments_from_statuses(&workdir_attachments, claims);
|
||||
worker.workdir_attachments = workdir_attachments;
|
||||
worker.detail()
|
||||
};
|
||||
@@ -2867,8 +2872,13 @@ impl Runtime {
|
||||
candidate.execution_bound = true;
|
||||
candidate.status = WorkerStatus::Idle;
|
||||
candidate.restore_intent = WorkerRestoreIntent::Explicit;
|
||||
let claims = candidate
|
||||
.request
|
||||
.as_ref()
|
||||
.map(|request| request.workdir_attachments.as_slice())
|
||||
.unwrap_or_default();
|
||||
candidate.logical_workdir_attachments =
|
||||
logical_workdir_attachments_from_statuses(&workdir_attachments);
|
||||
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)?;
|
||||
@@ -3213,7 +3223,11 @@ impl RuntimeState {
|
||||
}
|
||||
};
|
||||
let logical_workdir_attachments = if worker.logical_workdir_attachments.is_empty() {
|
||||
logical_workdir_attachments_from_statuses(&worker.workdir_attachments)
|
||||
let claims = request
|
||||
.as_ref()
|
||||
.map(|request| request.workdir_attachments.as_slice())
|
||||
.unwrap_or_default();
|
||||
logical_workdir_attachments_from_statuses(&worker.workdir_attachments, claims)
|
||||
} else {
|
||||
worker.logical_workdir_attachments
|
||||
};
|
||||
@@ -4283,17 +4297,29 @@ fn requested_workdir_ids(request: &CreateWorkerRequest) -> Vec<&str> {
|
||||
|
||||
fn logical_workdir_attachments_from_statuses(
|
||||
attachments: &[crate::catalog::WorkingDirectoryAttachmentStatus],
|
||||
claims: &[crate::catalog::WorkingDirectoryAttachmentClaim],
|
||||
) -> Vec<LogicalWorkdirAttachment> {
|
||||
attachments
|
||||
.iter()
|
||||
.map(|attachment| LogicalWorkdirAttachment {
|
||||
alias: attachment.alias.clone(),
|
||||
working_directory_id: attachment
|
||||
.working_directory
|
||||
.summary
|
||||
.working_directory_id
|
||||
.clone(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::ALL,
|
||||
.map(|attachment| {
|
||||
let working_directory_id = &attachment.working_directory.summary.working_directory_id;
|
||||
let capabilities = match claims.iter().find(|claim| claim.alias == attachment.alias) {
|
||||
Some(claim) if claim.working_directory_id == *working_directory_id => {
|
||||
claim.capabilities
|
||||
}
|
||||
// An alias/id mismatch is invalid at the operation boundary. If
|
||||
// corrupted persisted state reaches this compatibility path,
|
||||
// fail closed rather than silently restoring write authority.
|
||||
Some(_) => workdir::WorkdirSessionCapabilities::READ_ONLY,
|
||||
// Runtime-materialized attachment requests predate capability
|
||||
// claims and retain their existing full local-session behavior.
|
||||
None => workdir::WorkdirSessionCapabilities::ALL,
|
||||
};
|
||||
LogicalWorkdirAttachment {
|
||||
alias: attachment.alias.clone(),
|
||||
working_directory_id: working_directory_id.clone(),
|
||||
capabilities,
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -6584,6 +6610,7 @@ mod tests {
|
||||
alias: workdir::WorkdirAttachmentAlias::new("workdir").unwrap(),
|
||||
working_directory_id: "workdir-idempotent".to_string(),
|
||||
relative_cwd: None,
|
||||
capabilities: workdir::WorkdirSessionCapabilities::ALL,
|
||||
}];
|
||||
|
||||
let first = runtime.create_worker(request.clone()).unwrap();
|
||||
@@ -6974,7 +7001,13 @@ mod tests {
|
||||
.unwrap(),
|
||||
}],
|
||||
});
|
||||
let request = task_request("uncertain create cleanup");
|
||||
let mut request = task_request("uncertain create cleanup");
|
||||
request.workdir_attachments = vec![WorkingDirectoryAttachmentClaim {
|
||||
alias: workdir::WorkdirAttachmentAlias::new("checkout").unwrap(),
|
||||
working_directory_id: "wd-uncertain".to_string(),
|
||||
relative_cwd: None,
|
||||
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
|
||||
}];
|
||||
let worker_ref = WorkerRef::new(request.worker_id);
|
||||
|
||||
let error = runtime.create_worker(request).unwrap_err();
|
||||
@@ -6986,6 +7019,16 @@ mod tests {
|
||||
let retained = runtime.worker_detail(&worker_ref).unwrap();
|
||||
assert_eq!(retained.status, WorkerStatus::Idle);
|
||||
assert!(!retained.execution_metadata_available);
|
||||
assert_eq!(
|
||||
runtime
|
||||
.lock()
|
||||
.unwrap()
|
||||
.worker(&worker_ref)
|
||||
.unwrap()
|
||||
.logical_workdir_attachments[0]
|
||||
.capabilities,
|
||||
workdir::WorkdirSessionCapabilities::READ_ONLY
|
||||
);
|
||||
assert!(matches!(
|
||||
runtime.delete_worker(&worker_ref),
|
||||
Err(RuntimeError::InvalidRequest(_))
|
||||
|
||||
@@ -755,11 +755,25 @@ async fn fetch_workspace_config_http<T>(
|
||||
Err("Workspace Config fetch requires the worker-runtime http-server feature".to_string())
|
||||
}
|
||||
|
||||
fn initial_workdir_session_capabilities(
|
||||
alias: &WorkdirAttachmentAlias,
|
||||
claims: &[crate::catalog::WorkingDirectoryAttachmentClaim],
|
||||
) -> WorkdirSessionCapabilities {
|
||||
claims
|
||||
.iter()
|
||||
.find(|claim| &claim.alias == alias)
|
||||
.map(|claim| claim.capabilities)
|
||||
// New Runtime materialization requests have no claim and retain their
|
||||
// established full local-session behavior.
|
||||
.unwrap_or(WorkdirSessionCapabilities::ALL)
|
||||
}
|
||||
|
||||
fn runtime_local_workdir_session(
|
||||
workdir_id: &str,
|
||||
root: &Path,
|
||||
cwd: &Path,
|
||||
scope: manifest::SharedScope,
|
||||
capabilities: WorkdirSessionCapabilities,
|
||||
command_environment: std::collections::BTreeMap<String, String>,
|
||||
resources: Vec<Arc<dyn workdir::WorkdirSessionResource>>,
|
||||
) -> WorkdirSessionHandle {
|
||||
@@ -768,7 +782,7 @@ fn runtime_local_workdir_session(
|
||||
root.to_path_buf(),
|
||||
cwd.to_path_buf(),
|
||||
scope,
|
||||
WorkdirSessionCapabilities::ALL,
|
||||
capabilities,
|
||||
command_environment,
|
||||
resources,
|
||||
))
|
||||
@@ -776,6 +790,7 @@ fn runtime_local_workdir_session(
|
||||
|
||||
fn runtime_local_workdir_router(
|
||||
attachments: &BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>,
|
||||
capabilities: &BTreeMap<WorkdirAttachmentAlias, WorkdirSessionCapabilities>,
|
||||
scope: manifest::SharedScope,
|
||||
) -> Result<Arc<WorkdirSessionRouter>, String> {
|
||||
let router = Arc::new(WorkdirSessionRouter::new());
|
||||
@@ -788,6 +803,10 @@ fn runtime_local_workdir_router(
|
||||
binding.root(),
|
||||
binding.cwd(),
|
||||
scope.clone(),
|
||||
capabilities
|
||||
.get(alias)
|
||||
.copied()
|
||||
.unwrap_or(WorkdirSessionCapabilities::READ_ONLY),
|
||||
binding.command_environment(),
|
||||
binding.session_resources(),
|
||||
),
|
||||
@@ -800,10 +819,25 @@ fn runtime_local_workdir_router(
|
||||
fn restored_workdir_router(
|
||||
local_attachments: &BTreeMap<WorkdirAttachmentAlias, WorkingDirectoryBinding>,
|
||||
logical_attachments: &[crate::catalog::LogicalWorkdirAttachment],
|
||||
claims: &[crate::catalog::WorkingDirectoryAttachmentClaim],
|
||||
scope: manifest::SharedScope,
|
||||
workspace_client: Arc<dyn WorkspaceClient>,
|
||||
) -> Result<Arc<WorkdirSessionRouter>, String> {
|
||||
let router = runtime_local_workdir_router(local_attachments, scope)?;
|
||||
let mut local_capabilities = local_attachments
|
||||
.keys()
|
||||
.map(|alias| {
|
||||
(
|
||||
alias.clone(),
|
||||
initial_workdir_session_capabilities(alias, claims),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
for attachment in logical_attachments {
|
||||
if local_attachments.contains_key(&attachment.alias) {
|
||||
local_capabilities.insert(attachment.alias.clone(), attachment.capabilities);
|
||||
}
|
||||
}
|
||||
let router = runtime_local_workdir_router(local_attachments, &local_capabilities, scope)?;
|
||||
for attachment in logical_attachments {
|
||||
if let Some(local) = local_attachments.get(&attachment.alias) {
|
||||
if local.working_directory.id != attachment.working_directory_id {
|
||||
@@ -1068,8 +1102,22 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
})?;
|
||||
let worker = prepared.worker_mut();
|
||||
validate_worker_memory_settings(worker.manifest(), &request.request)?;
|
||||
let workdir_capabilities = request
|
||||
.workdir_attachments
|
||||
.keys()
|
||||
.map(|alias| {
|
||||
(
|
||||
alias.clone(),
|
||||
initial_workdir_session_capabilities(
|
||||
alias,
|
||||
&request.request.workdir_attachments,
|
||||
),
|
||||
)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
worker.bind_workdir_sessions(runtime_local_workdir_router(
|
||||
&request.workdir_attachments,
|
||||
&workdir_capabilities,
|
||||
worker.scope().clone(),
|
||||
)?);
|
||||
if let (Some(runtime_id), Some(workspace_id)) =
|
||||
@@ -1300,6 +1348,7 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
||||
let workdir_sessions = restored_workdir_router(
|
||||
&request.workdir_attachments,
|
||||
&request.logical_workdir_attachments,
|
||||
&request.request.workdir_attachments,
|
||||
worker.scope().clone(),
|
||||
worker.workspace_client_handle(),
|
||||
)?;
|
||||
@@ -2177,6 +2226,7 @@ where
|
||||
binding.root(),
|
||||
binding.cwd(),
|
||||
manifest::SharedScope::new(scope),
|
||||
WorkdirSessionCapabilities::ALL,
|
||||
binding.command_environment(),
|
||||
binding.session_resources(),
|
||||
))
|
||||
@@ -2979,6 +3029,7 @@ mod tests {
|
||||
working_directory_id: "remote-workdir".to_string(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
|
||||
}],
|
||||
&[],
|
||||
scope,
|
||||
client,
|
||||
)
|
||||
@@ -2992,6 +3043,37 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restored_router_preserves_read_only_capabilities_for_local_attachment() {
|
||||
let runtime_base = tempfile::tempdir().unwrap();
|
||||
let repo = create_clean_repo();
|
||||
let materializer = RuntimeGitMaterializer::new(runtime_base.path());
|
||||
let worker_ref = WorkerRef::new(crate::identity::WorkerId::now_v7());
|
||||
let binding = materializer
|
||||
.materialize(&worker_ref, &working_directory_request(repo.path()))
|
||||
.unwrap();
|
||||
let alias = WorkdirAttachmentAlias::new("docs").unwrap();
|
||||
let workdir_id = binding.working_directory.id.clone();
|
||||
let scope = manifest::SharedScope::new(manifest::Scope::writable(binding.root()).unwrap());
|
||||
let attachments = BTreeMap::from([(alias.clone(), binding)]);
|
||||
let logical = [LogicalWorkdirAttachment {
|
||||
alias: alias.clone(),
|
||||
working_directory_id: workdir_id,
|
||||
capabilities: WorkdirSessionCapabilities::READ_ONLY,
|
||||
}];
|
||||
let client = WorkerWorkspaceContext::local_filesystem(Some(
|
||||
WorkspaceId::new("workspace-a").unwrap(),
|
||||
))
|
||||
.client_handle();
|
||||
|
||||
let router = restored_workdir_router(&attachments, &logical, &[], scope, client).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
router.session(&alias).unwrap().capabilities(),
|
||||
WorkdirSessionCapabilities::READ_ONLY
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn profile_factory_routes_workspace_requests_by_workspace_id() {
|
||||
let profiles = tempfile::tempdir().unwrap();
|
||||
@@ -3949,13 +4031,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_opens_a_fresh_session_for_the_same_workdir_identity() {
|
||||
fn initial_claim_capabilities_override_full_materialization_default() {
|
||||
let checkout = WorkdirAttachmentAlias::new("checkout").unwrap();
|
||||
let docs = WorkdirAttachmentAlias::new("docs").unwrap();
|
||||
let claims = [WorkingDirectoryAttachmentClaim {
|
||||
alias: docs.clone(),
|
||||
working_directory_id: "workdir-docs".to_string(),
|
||||
relative_cwd: None,
|
||||
capabilities: WorkdirSessionCapabilities::READ_ONLY,
|
||||
}];
|
||||
|
||||
assert_eq!(
|
||||
initial_workdir_session_capabilities(&docs, &claims),
|
||||
WorkdirSessionCapabilities::READ_ONLY
|
||||
);
|
||||
assert_eq!(
|
||||
initial_workdir_session_capabilities(&checkout, &claims),
|
||||
WorkdirSessionCapabilities::ALL
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn restore_opens_a_fresh_session_with_the_same_read_only_capabilities() {
|
||||
let root = tempfile::tempdir().unwrap();
|
||||
let spawned = runtime_local_workdir_session(
|
||||
"working-directory-42",
|
||||
root.path(),
|
||||
root.path(),
|
||||
manifest::SharedScope::new(Scope::writable(root.path()).unwrap()),
|
||||
WorkdirSessionCapabilities::READ_ONLY,
|
||||
Default::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
@@ -3964,12 +4068,21 @@ mod tests {
|
||||
root.path(),
|
||||
root.path(),
|
||||
manifest::SharedScope::new(Scope::writable(root.path()).unwrap()),
|
||||
WorkdirSessionCapabilities::READ_ONLY,
|
||||
Default::default(),
|
||||
Vec::new(),
|
||||
);
|
||||
|
||||
assert_eq!(spawned.workdir().id().as_str(), "working-directory-42");
|
||||
assert_eq!(restored.workdir().id().as_str(), "working-directory-42");
|
||||
assert_eq!(
|
||||
spawned.capabilities(),
|
||||
WorkdirSessionCapabilities::READ_ONLY
|
||||
);
|
||||
assert_eq!(
|
||||
restored.capabilities(),
|
||||
WorkdirSessionCapabilities::READ_ONLY
|
||||
);
|
||||
assert!(!Arc::ptr_eq(&spawned, &restored));
|
||||
}
|
||||
|
||||
@@ -4924,6 +5037,7 @@ mod tests {
|
||||
alias: workdir::WorkdirAttachmentAlias::new("workdir").unwrap(),
|
||||
working_directory_id: workdir_id.clone(),
|
||||
relative_cwd: None,
|
||||
capabilities: workdir::WorkdirSessionCapabilities::ALL,
|
||||
}];
|
||||
|
||||
let error = runtime.create_worker(request).unwrap_err();
|
||||
|
||||
Reference in New Issue
Block a user