feat: replace Subject retirement with durable explicit deletion

This commit is contained in:
2026-10-28 11:18:43 +09:00
parent 618880d38d
commit a06f6bfb3d
71 changed files with 7448 additions and 315 deletions
+35 -6
View File
@@ -338,6 +338,8 @@ impl_openapi_schema!(
MemoryConsolidationResponse,
SubjektivSubjectCreateRequest,
SubjektivSubjectBehaviorUpdateRequest,
SubjektivSubjectDeleteRequest,
SubjektivSubjectDeleteResponse,
SubjektivSubjectListQuery,
SubjektivSubjectListResponse,
SubjektivSubjectResponse,
@@ -1527,19 +1529,21 @@ pub trait ServerApi {
#[path] memory_id: String,
#[query] query: SubjektivMemoryChangesQuery,
) -> Result<SubjektivMemoryListChangesResponse, RepositoryApiError>;
#[post(
"/api/w/{workspace_id}/subjektiv/subjects/{subject_id}/retire",
#[delete(
"/api/w/{workspace_id}/subjektiv/subjects/{subject_id}",
status = 200,
error_status = 400,
additional_error_statuses = [401, 403, 404, 409, 500],
bearer_auth = true,
browser_auth = true,
)]
async fn subjektiv_subject_retire(
async fn subjektiv_subject_delete(
&self,
#[extension] context: ServerRequestContext,
#[path] workspace_id: String,
#[path] subject_id: String,
) -> Result<SubjektivSubjectResponse, RepositoryApiError>;
#[body] request: SubjektivSubjectDeleteRequest,
) -> Result<SubjektivSubjectDeleteResponse, RepositoryApiError>;
#[post(
"/api/w/{workspace_id}/subjektiv/staging",
status = 200,
@@ -4955,6 +4959,8 @@ pub struct WorkspacePermissionSummary {
pub manage_secrets: bool,
pub manage_runtimes: bool,
pub delete_workspace: bool,
/// Explicit Subject deletion authority for the current authenticated actor.
pub delete_subjects: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -5350,7 +5356,25 @@ pub struct MemoryConsolidationResponse {
#[serde(rename_all = "snake_case")]
pub enum SubjektivSubjectState {
Active,
Retired,
/// Saved compatibility only; not a current lifecycle operation.
#[serde(rename = "retired")]
LegacyRetired,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SubjektivSubjectDeleteRequest {
/// Stable identity retained for every retry, including unknown outcomes.
pub operation_id: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct SubjektivSubjectDeleteResponse {
pub subject_id: String,
pub operation_id: String,
/// False confirms the deletion fence, not external stop or erasure completion.
pub completed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
@@ -10950,6 +10974,10 @@ pub fn memory_api_typescript() -> String {
export type SubjektivSubjectCreateRequest = { role: string, behavior_md?: string, };
export type SubjektivSubjectDeleteRequest = { operation_id: string, };
export type SubjektivSubjectDeleteResponse = { subject_id: string, operation_id: string, completed: boolean, };
export type SubjektivSubjectBehaviorUpdateRequest = { expected_behavior_md: string, behavior_md: string, };
export type SubjektivSubjectResponse = { id: string, role: string, behavior_md: string, state: SubjektivSubjectState, memory_fingerprint: string, created_at: string, updated_at: string, current_worker?: | import("./worker-launch-api").WorkerLaunchWorkerSummary | null, };
@@ -13483,7 +13511,8 @@ mod tests {
"manage_repositories": true,
"manage_secrets": true,
"manage_runtimes": true,
"delete_workspace": true
"delete_workspace": true,
"delete_subjects": true
},
"extension_points": {
"store": "sqlite",
@@ -0,0 +1,49 @@
use serde_json::json;
use server_api::{
SubjektivSubjectDeleteRequest, SubjektivSubjectDeleteResponse, SubjektivSubjectState,
};
#[test]
fn subject_delete_api_replaces_retire_and_requires_operation_identity() {
let document: serde_json::Value = serde_json::from_str(
&server_api::canonical_openapi_document()
.unwrap()
.to_json()
.unwrap(),
)
.unwrap();
let paths = &document["paths"];
assert!(
paths
.get("/api/w/{workspace_id}/subjektiv/subjects/{subject_id}/retire")
.is_none()
);
let operation = &paths["/api/w/{workspace_id}/subjektiv/subjects/{subject_id}"]["delete"];
assert_eq!(operation["operationId"], "subjektiv_subject_delete");
assert_eq!(operation["requestBody"]["required"], true);
assert!(serde_json::from_value::<SubjektivSubjectDeleteRequest>(json!({})).is_err());
assert!(
serde_json::from_value::<SubjektivSubjectDeleteRequest>(
json!({"operation_id":"op", "force":true})
)
.is_err()
);
for completed in [false, true] {
let response = SubjektivSubjectDeleteResponse {
subject_id: "subject".into(),
operation_id: "op".into(),
completed,
};
assert_eq!(
serde_json::to_value(response).unwrap(),
json!({"subject_id":"subject", "operation_id":"op", "completed":completed})
);
}
}
#[test]
fn old_subject_state_is_explicit_saved_compatibility_not_a_new_operation() {
let state: SubjektivSubjectState = serde_json::from_value(json!("retired")).unwrap();
assert_eq!(state, SubjektivSubjectState::LegacyRetired);
assert_eq!(serde_json::to_value(state).unwrap(), json!("retired"));
}