feat: support permissioned multi-repository ticket targets

This commit is contained in:
2026-09-23 23:11:37 +09:00
parent 9c107eaf74
commit 9ccf2fbc3b
36 changed files with 4093 additions and 1360 deletions
+6 -1
View File
@@ -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)]
+37 -1
View File
@@ -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,
}]
);
}
+55 -12
View File
@@ -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(_))
+117 -3
View File
@@ -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();