feat: bind Workdir attachments to durable connection lifetimes
Add caller-scoped bounded connection inventory and conditional detach on the existing ledger/session lifecycle. Preserve lifetime identity during attenuation and compensation, migrate persisted rows, and bind self operations to verified Runtime/Worker proof. Validation: workspace-root cargo check; Backend 561 unit + 11 CLI tests; server-api 62 default + 69 typescript tests. Canonical OpenAPI and generated TypeScript source digests regenerated; browser declarations unchanged.
This commit is contained in:
@@ -192,6 +192,10 @@ impl_openapi_schema!(
|
||||
WorkingDirectoryCreateResponse,
|
||||
CurrentWorkerWorkdirAttachRequest,
|
||||
CurrentWorkerWorkdirAttachmentResponse,
|
||||
CurrentWorkerWorkdirAttachmentItem,
|
||||
CurrentWorkerWorkdirAttachmentListQuery,
|
||||
CurrentWorkerWorkdirAttachmentListResponse,
|
||||
CurrentWorkerWorkdirDetachQuery,
|
||||
CurrentWorkerWorkdirOperationRequest,
|
||||
CurrentWorkerWorkdirOperationResponse,
|
||||
ExternalWorkdirGrantCreateRequest,
|
||||
@@ -2549,6 +2553,19 @@ pub trait ServerApi {
|
||||
#[path] working_directory_id: String,
|
||||
#[body] request: WorkingDirectoryRemovalRequest,
|
||||
) -> Result<WorkingDirectoryRemovalResponse, RepositoryApiError>;
|
||||
#[get(
|
||||
"/api/w/{workspace_id}/workers/self/workdir-attachments",
|
||||
status = 200,
|
||||
error_status = 400,
|
||||
additional_error_statuses = [401, 403, 404, 500],
|
||||
openapi = false,
|
||||
)]
|
||||
async fn current_worker_workdir_attachment_list(
|
||||
&self,
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[query] query: CurrentWorkerWorkdirAttachmentListQuery,
|
||||
) -> Result<CurrentWorkerWorkdirAttachmentListResponse, RepositoryApiError>;
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/workers/self/workdir-attachments",
|
||||
status = 200,
|
||||
@@ -2574,6 +2591,7 @@ pub trait ServerApi {
|
||||
#[extension] context: ServerRequestContext,
|
||||
#[path] workspace_id: String,
|
||||
#[path] alias: String,
|
||||
#[query] query: CurrentWorkerWorkdirDetachQuery,
|
||||
) -> Result<CurrentWorkerWorkdirAttachmentResponse, RepositoryApiError>;
|
||||
#[post(
|
||||
"/api/w/{workspace_id}/workers/self/workdir-session/operations",
|
||||
@@ -6754,6 +6772,45 @@ pub struct CurrentWorkerWorkdirAttachRequest {
|
||||
pub working_directory_id: String,
|
||||
}
|
||||
|
||||
/// Bounded, alias-ordered paging over the current Worker's active attachments.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CurrentWorkerWorkdirAttachmentListQuery {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<u32>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub offset: Option<u32>,
|
||||
/// Exact caller-scoped connection lookup, applied before paging; 1..=128 bytes.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub connection_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CurrentWorkerWorkdirAttachmentItem {
|
||||
pub alias: String,
|
||||
pub working_directory_id: String,
|
||||
pub capabilities: workdir::WorkdirSessionCapabilities,
|
||||
/// Opaque durable identity of this attachment lifetime, not a Workdir ID.
|
||||
pub connection_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CurrentWorkerWorkdirAttachmentListResponse {
|
||||
pub workspace_id: String,
|
||||
pub items: Vec<CurrentWorkerWorkdirAttachmentItem>,
|
||||
pub next_offset: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CurrentWorkerWorkdirDetachQuery {
|
||||
/// When omitted, retain the normal Tool's unconditional detach contract.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub expected_connection_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CurrentWorkerWorkdirAttachmentResponse {
|
||||
@@ -11930,6 +11987,78 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_worker_attachment_contract_keeps_normal_tool_response_closed() {
|
||||
let lookup = CurrentWorkerWorkdirAttachmentListQuery {
|
||||
limit: Some(1),
|
||||
offset: None,
|
||||
connection_id: Some("opaque-connection".to_string()),
|
||||
};
|
||||
let value = serde_json::json!({"limit": 1, "connection_id": "opaque-connection"});
|
||||
assert_eq!(serde_json::to_value(&lookup).unwrap(), value);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<CurrentWorkerWorkdirAttachmentListQuery>(value).unwrap(),
|
||||
lookup
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_value(CurrentWorkerWorkdirAttachmentListQuery::default()).unwrap(),
|
||||
serde_json::json!({})
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_value::<CurrentWorkerWorkdirAttachmentListQuery>(
|
||||
serde_json::json!({"connection_id": 123})
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
serde_json::from_value::<CurrentWorkerWorkdirAttachmentListQuery>(
|
||||
serde_json::json!({"expected_connection_id": "wrong-field"})
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let query: CurrentWorkerWorkdirDetachQuery =
|
||||
serde_json::from_value(serde_json::json!({})).unwrap();
|
||||
assert_eq!(query.expected_connection_id, None);
|
||||
assert_eq!(serde_json::to_value(query).unwrap(), serde_json::json!({}));
|
||||
assert!(
|
||||
serde_json::from_value::<CurrentWorkerWorkdirDetachQuery>(
|
||||
serde_json::json!({"connection_id":"wrong-field"})
|
||||
)
|
||||
.is_err()
|
||||
);
|
||||
let response = CurrentWorkerWorkdirAttachmentResponse {
|
||||
workspace_id: "workspace".to_string(),
|
||||
alias: "checkout".to_string(),
|
||||
working_directory_id: "workdir".to_string(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::EMPTY,
|
||||
attached: false,
|
||||
};
|
||||
let value = serde_json::to_value(&response).unwrap();
|
||||
assert_eq!(value.as_object().unwrap().len(), 5);
|
||||
assert!(value.get("connection_id").is_none());
|
||||
assert_eq!(
|
||||
serde_json::from_value::<CurrentWorkerWorkdirAttachmentResponse>(value).unwrap(),
|
||||
response
|
||||
);
|
||||
let list = CurrentWorkerWorkdirAttachmentListResponse {
|
||||
workspace_id: "workspace".to_string(),
|
||||
next_offset: Some(1),
|
||||
items: vec![CurrentWorkerWorkdirAttachmentItem {
|
||||
alias: "checkout".to_string(),
|
||||
working_directory_id: "workdir".to_string(),
|
||||
capabilities: workdir::WorkdirSessionCapabilities::READ_ONLY,
|
||||
connection_id: "opaque".to_string(),
|
||||
}],
|
||||
};
|
||||
assert_eq!(
|
||||
serde_json::from_value::<CurrentWorkerWorkdirAttachmentListResponse>(
|
||||
serde_json::to_value(&list).unwrap()
|
||||
)
|
||||
.unwrap(),
|
||||
list
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_and_repository_response_shapes_round_trip() {
|
||||
let workspace = serde_json::json!({
|
||||
@@ -12895,6 +13024,7 @@ mod openapi_artifact_tests {
|
||||
// identity, or a one-use Reviewer capability. They remain generated Rust client/Axum
|
||||
// operations, but must not appear as unauthenticated operations in the public OpenAPI.
|
||||
const SIGNED_INTERNAL: &[&str] = &[
|
||||
"current_worker_workdir_attachment_list",
|
||||
"current_worker_workdir_attach",
|
||||
"current_worker_workdir_detach",
|
||||
"current_worker_workdir_operation",
|
||||
|
||||
Reference in New Issue
Block a user