use meerkat::{
AddEvidenceRequest, AttentionListRequest, AttentionPauseRequest, AttentionProjectionRequest,
AttentionPruneRequest, AttentionReassignRequest, AttentionResumeRequest,
BreakGlassAttentionReassignRequest, ClaimWorkItemRequest, CloseWorkItemRequest,
CreateWorkItemRequest, GoalAttentionTarget, GoalConfirmRequest, GoalCreateRequest,
GoalRequestCloseRequest, GoalStatusRequest, LinkWorkItemsRequest, PolicyEscalateRequest,
ReadyWorkFilter, ReleaseWorkItemRequest, UpdateWorkItemRequest, WorkAttentionBindingId,
WorkGraphError, WorkGraphEventFilter, WorkGraphIdParams, WorkGraphService,
WorkGraphSnapshotFilter, WorkItemFilter, WorkItemId, WorkNamespace, WorkOwnerKey, WorkStatus,
};
use serde_json::Map;
use crate::workgraph_admission::{WorkGraphAdmission, WorkGraphAdmissionError};
use super::*;
pub(crate) const WORKGRAPH_READ_METHODS: &[&str] = &[
"mobkit/workgraph/snapshot",
"mobkit/workgraph/list",
"mobkit/workgraph/get",
"mobkit/workgraph/ready",
"mobkit/workgraph/events",
"mobkit/workgraph/attention/list",
"mobkit/workgraph/goal/status",
];
pub(crate) const WORKGRAPH_MUTATE_METHODS: &[&str] = &[
"mobkit/workgraph/create",
"mobkit/workgraph/update",
"mobkit/workgraph/claim",
"mobkit/workgraph/release",
"mobkit/workgraph/close",
"mobkit/workgraph/block",
"mobkit/workgraph/link",
"mobkit/workgraph/evidence/add",
"mobkit/workgraph/policy/escalate",
"mobkit/workgraph/goal/create",
"mobkit/workgraph/goal/confirm",
"mobkit/workgraph/goal/request_close",
"mobkit/workgraph/attention/pause",
"mobkit/workgraph/attention/resume",
"mobkit/workgraph/attention/reassign",
"mobkit/workgraph/attention/prune",
];
pub(crate) const WORKGRAPH_CONSOLE_MUTATE_METHODS: &[&str] =
&["mobkit/workgraph/attention/break_glass_reassign"];
pub(crate) fn is_workgraph_method(method: &str) -> bool {
method.starts_with("mobkit/workgraph/")
}
pub(crate) fn is_workgraph_read_method(method: &str) -> bool {
WORKGRAPH_READ_METHODS.contains(&method)
}
pub(crate) fn is_workgraph_mutating_method(method: &str) -> bool {
WORKGRAPH_MUTATE_METHODS.contains(&method) || WORKGRAPH_CONSOLE_MUTATE_METHODS.contains(&method)
}
fn invalid_params(message: impl std::fmt::Display) -> JsonRpcError {
JsonRpcError {
code: -32602,
message: format!("Invalid params: {message}"),
data: None,
}
}
fn method_not_found() -> JsonRpcError {
JsonRpcError {
code: -32601,
message: "Method not found".to_string(),
data: None,
}
}
pub(crate) fn workgraph_unavailable_error() -> JsonRpcError {
JsonRpcError {
code: WORKGRAPH_UNAVAILABLE_CODE,
message: "workgraph is not configured on this runtime".to_string(),
data: Some(serde_json::json!({ "kind": "workgraph_unavailable" })),
}
}
const STALE_ATTENTION_WITNESS_PREFIX: &str = "stale WorkGraph attention projection";
fn workgraph_conflict(detail: String) -> JsonRpcError {
JsonRpcError {
code: WORKGRAPH_CONFLICT_CODE,
message: format!("workgraph conflict: {detail}"),
data: Some(serde_json::json!({
"kind": "workgraph_conflict",
"detail": detail,
})),
}
}
fn workgraph_error_to_rpc(error: WorkGraphError) -> JsonRpcError {
let detail = error.to_string();
match error {
WorkGraphError::StaleRevision { .. } | WorkGraphError::Conflict(_) => {
workgraph_conflict(detail)
}
WorkGraphError::InvalidTransition(ref message)
if message.starts_with(STALE_ATTENTION_WITNESS_PREFIX) =>
{
workgraph_conflict(detail)
}
WorkGraphError::InvalidInput(_) => invalid_params(detail),
_ => JsonRpcError {
code: WORKGRAPH_ERROR_CODE,
message: detail.clone(),
data: Some(serde_json::json!({
"kind": "workgraph_error",
"detail": detail,
})),
},
}
}
fn to_result_value<T: serde::Serialize>(value: &T) -> Value {
serde_json::to_value(value).unwrap_or(Value::Null)
}
fn params_object(params: &Value) -> Result<Map<String, Value>, JsonRpcError> {
match params {
Value::Null => Ok(Map::new()),
Value::Object(map) => Ok(map.clone()),
_ => Err(invalid_params("params must be a JSON object")),
}
}
fn parse_request<T: serde::de::DeserializeOwned>(
object: Map<String, Value>,
) -> Result<T, JsonRpcError> {
serde_json::from_value(Value::Object(object)).map_err(invalid_params)
}
fn parse_binding_id(object: &Map<String, Value>) -> Result<WorkAttentionBindingId, JsonRpcError> {
let raw = object
.get("binding_id")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| invalid_params("binding_id must be a non-empty string"))?;
WorkAttentionBindingId::new(raw).map_err(invalid_params)
}
fn parse_expected_revision(object: &Map<String, Value>) -> Result<u64, JsonRpcError> {
object
.get("expected_revision")
.and_then(Value::as_u64)
.ok_or_else(|| invalid_params("expected_revision must be a non-negative integer"))
}
fn parse_namespace(object: &Map<String, Value>) -> Result<Option<WorkNamespace>, JsonRpcError> {
match object.get("namespace") {
None | Some(Value::Null) => Ok(None),
Some(value) => serde_json::from_value(value.clone())
.map(Some)
.map_err(|error| invalid_params(format!("namespace is invalid: {error}"))),
}
}
fn normalize_attention_status_param(object: &mut Map<String, Value>) -> Result<(), JsonRpcError> {
let Some(value) = object.get("status") else {
return Ok(());
};
let Some(text) = value.as_str() else {
return Ok(());
};
match text {
"active" | "paused" | "superseded" | "stopped" => {
let tagged = serde_json::json!({ "state": text });
object.insert("status".to_string(), tagged);
Ok(())
}
other => Err(invalid_params(format!(
"status '{other}' is unknown (allowed: active, paused, superseded, stopped)"
))),
}
}
fn reject_non_default_namespace(
service: &WorkGraphService,
object: &Map<String, Value>,
) -> Result<(), JsonRpcError> {
let Some(namespace) = parse_namespace(object)? else {
return Ok(());
};
if namespace == *service.default_namespace() {
return Ok(());
}
Err(invalid_params(format!(
"namespace '{}' is not accepted on goal/attention methods: turn attention overlays \
resolve only in the default namespace '{}', so a goal filed elsewhere would never \
reach its member",
namespace.as_str(),
service.default_namespace().as_str(),
)))
}
fn resolve_goal_target(
mob_id: &meerkat_mob::MobId,
value: &Value,
) -> Result<GoalAttentionTarget, JsonRpcError> {
let object = value
.as_object()
.ok_or_else(|| invalid_params("target must be a JSON object"))?;
let kind = object
.get("kind")
.and_then(Value::as_str)
.unwrap_or_default();
match kind {
"identity" => {
let identity = object
.get("identity")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| invalid_params("target.identity must be a non-empty string"))?;
meerkat_mob::lower_agent_identity_attention_target(
mob_id,
&meerkat_mob::AgentIdentity::from(identity),
)
.map_err(invalid_params)
}
"session" | "owner" => serde_json::from_value(value.clone())
.map_err(|error| invalid_params(format!("target is invalid: {error}"))),
"lowered_owner" => {
let mut object = object.clone();
object.insert("kind".to_string(), Value::String("owner".to_string()));
serde_json::from_value(Value::Object(object))
.map_err(|error| invalid_params(format!("target is invalid: {error}")))
}
other => Err(invalid_params(format!(
"target.kind '{other}' is unsupported (allowed: session, owner, lowered_owner, \
identity)"
))),
}
}
fn default_confirm_evidence(
policy: &meerkat::WorkCompletionPolicy,
binding_id: &WorkAttentionBindingId,
) -> meerkat::WorkEvidenceRef {
let kind = match policy {
meerkat::WorkCompletionPolicy::SelfAttest => "self_attest",
meerkat::WorkCompletionPolicy::HostConfirmed => "host_confirmation",
meerkat::WorkCompletionPolicy::PrincipalConfirmed => "principal_confirmation",
meerkat::WorkCompletionPolicy::Supervisor { .. } => "supervisor_confirmation",
meerkat::WorkCompletionPolicy::ReviewerQuorum { .. } => "reviewer_confirmation",
};
meerkat::WorkEvidenceRef {
kind: kind.to_string(),
id: binding_id.as_str().to_string(),
label: None,
summary: None,
confirmation_kind: None,
confirming_owner_key: None,
}
}
fn parse_confirm_evidence(value: &Value) -> Result<meerkat::WorkEvidenceRef, JsonRpcError> {
let object = value
.as_object()
.ok_or_else(|| invalid_params("evidence must be a JSON object"))?;
for key in object.keys() {
if !matches!(key.as_str(), "kind" | "id" | "label" | "summary") {
return Err(invalid_params(format!(
"evidence.{key} is not accepted (allowed: kind, id, label, summary; \
confirmation classification is stamped server-side)"
)));
}
}
let field = |name: &str| -> Result<String, JsonRpcError> {
object
.get(name)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
.ok_or_else(|| invalid_params(format!("evidence.{name} must be a non-empty string")))
};
let optional = |name: &str| -> Result<Option<String>, JsonRpcError> {
match object.get(name) {
None | Some(Value::Null) => Ok(None),
Some(value) => value
.as_str()
.map(|value| Some(value.to_string()))
.ok_or_else(|| invalid_params(format!("evidence.{name} must be a string"))),
}
};
Ok(meerkat::WorkEvidenceRef {
kind: field("kind")?,
id: field("id")?,
label: optional("label")?,
summary: optional("summary")?,
confirmation_kind: None,
confirming_owner_key: None,
})
}
async fn fetch_authority_projection(
service: &WorkGraphService,
binding_id: WorkAttentionBindingId,
namespace: Option<WorkNamespace>,
) -> Result<meerkat::AttentionContextProjection, JsonRpcError> {
let result = service
.attention_projection(AttentionProjectionRequest {
binding_id,
realm_id: None,
namespace,
})
.await
.map_err(workgraph_error_to_rpc)?;
Ok(result.projection)
}
fn reassign_error_to_rpc(
error: WorkGraphError,
binding_id: &WorkAttentionBindingId,
mode: meerkat::WorkAttentionMode,
) -> JsonRpcError {
match &error {
WorkGraphError::InvalidInput(message)
if message.contains("requires derived_from link authority") =>
{
let mode = serde_json::to_value(mode)
.ok()
.and_then(|value| value.as_str().map(str::to_string))
.unwrap_or_else(|| format!("{mode:?}"));
let detail = format!(
"attention binding {binding_id} is in '{mode}' mode; meerkat 0.7.23 derives the \
derived_from link authority reassignment requires only for coordinate-mode \
bindings — pause the binding or recreate the goal with mode 'coordinate'",
);
JsonRpcError {
code: WORKGRAPH_ERROR_CODE,
message: format!("workgraph attention reassignment denied: {detail}"),
data: Some(serde_json::json!({
"kind": "workgraph_error",
"detail": detail,
})),
}
}
_ => workgraph_error_to_rpc(error),
}
}
fn admission_error_to_rpc(error: WorkGraphAdmissionError) -> JsonRpcError {
match error {
WorkGraphAdmissionError::Occupied { detail } => workgraph_conflict(detail),
WorkGraphAdmissionError::Service(error) => workgraph_error_to_rpc(error),
WorkGraphAdmissionError::Lock(detail) => {
let detail = format!("workgraph admission lock failed: {detail}");
JsonRpcError {
code: WORKGRAPH_ERROR_CODE,
message: detail.clone(),
data: Some(serde_json::json!({
"kind": "workgraph_error",
"detail": detail,
})),
}
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum WorkgraphSurface<'a> {
HostStdin,
Console {
authenticated_principal: Option<&'a str>,
},
}
pub(crate) async fn handle_workgraph_method(
service: Option<&WorkGraphService>,
admission: &WorkGraphAdmission,
surface: WorkgraphSurface<'_>,
method: &str,
params: &Value,
) -> Result<Value, JsonRpcError> {
if !is_workgraph_read_method(method) && !is_workgraph_mutating_method(method) {
return Err(method_not_found());
}
if WORKGRAPH_CONSOLE_MUTATE_METHODS.contains(&method)
&& matches!(surface, WorkgraphSurface::HostStdin)
{
return Err(method_not_found());
}
let trusted_principal = match surface {
WorkgraphSurface::HostStdin => None,
WorkgraphSurface::Console {
authenticated_principal,
} => console_trusted_principal(authenticated_principal),
};
let Some(service) = service else {
return Err(workgraph_unavailable_error());
};
let mob_id = &admission.mob_handle().definition().id;
let object = params_object(params)?;
if object.contains_key("realm_id") {
return Err(invalid_params(
"realm_id is not accepted; workgraph is scoped to this runtime's realm",
));
}
match method {
"mobkit/workgraph/snapshot" => {
let filter: WorkGraphSnapshotFilter = parse_request(object)?;
let snapshot = service
.snapshot(filter)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&snapshot))
}
"mobkit/workgraph/list" => {
let filter: WorkItemFilter = parse_request(object)?;
let items = service.list(filter).await.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "items": items }))
}
"mobkit/workgraph/get" => {
let request: WorkGraphIdParams = parse_request(object)?;
let item = service
.get(None, request.namespace, request.id)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/ready" => {
let filter: ReadyWorkFilter = parse_request(object)?;
let items = service
.ready(filter)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "items": items }))
}
"mobkit/workgraph/events" => {
let filter: WorkGraphEventFilter = parse_request(object)?;
let events = service
.events(filter)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "events": events }))
}
"mobkit/workgraph/attention/list" => {
reject_non_default_namespace(service, &object)?;
let mut object = object;
normalize_attention_status_param(&mut object)?;
let request: AttentionListRequest = parse_request(object)?;
let result = service
.list_attention(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/goal/status" => {
let request: GoalStatusRequest = parse_request(object)?;
let result = service
.goal_status(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/create" => {
let request: CreateWorkItemRequest = parse_request(object)?;
let item = service
.create(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/update" => {
let request: UpdateWorkItemRequest = parse_request(object)?;
let item = service
.update(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/claim" => {
let mut object = object;
if let Some(Value::Object(owner)) = object.get("owner")
&& !owner.contains_key("key")
&& owner.contains_key("kind")
{
let mut normalized = Map::new();
normalized.insert(
"key".to_string(),
serde_json::json!({
"kind": owner.get("kind"),
"id": owner.get("id"),
}),
);
if let Some(display_name) = owner.get("display_name") {
normalized.insert("display_name".to_string(), display_name.clone());
}
object.insert("owner".to_string(), Value::Object(normalized));
}
let request: ClaimWorkItemRequest = parse_request(object)?;
let item = service
.claim(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/release" => {
let request: ReleaseWorkItemRequest = parse_request(object)?;
let item = service
.release(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/close" => {
let request: CloseWorkItemRequest = parse_request(object)?;
if !matches!(
request.status,
WorkStatus::Completed | WorkStatus::Cancelled | WorkStatus::Failed
) {
return Err(invalid_params(
"status must be one of: completed, cancelled, failed",
));
}
let item = service
.close(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/block" => {
#[derive(serde::Deserialize)]
struct BlockParams {
id: WorkItemId,
expected_revision: u64,
#[serde(default)]
namespace: Option<WorkNamespace>,
}
let request: BlockParams = parse_request(object)?;
let item = service
.block(
None,
request.namespace,
request.id,
request.expected_revision,
)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/link" => {
let request: LinkWorkItemsRequest = parse_request(object)?;
let edge = service
.link(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "edge": edge }))
}
"mobkit/workgraph/evidence/add" => {
let request: AddEvidenceRequest = parse_request(object)?;
let item = service
.add_evidence(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/policy/escalate" => {
reject_non_default_namespace(service, &object)?;
if object.contains_key("authority_projection") {
return Err(invalid_params(
"authority_projection is not accepted; the authority witness is fetched \
server-side from binding_id",
));
}
#[derive(serde::Deserialize)]
struct PolicyEscalateParams {
id: WorkItemId,
expected_revision: u64,
completion_policy: meerkat::WorkCompletionPolicy,
#[serde(default)]
namespace: Option<WorkNamespace>,
}
let binding_id = parse_binding_id(&object)?;
let mut object = object;
object.remove("binding_id");
let params: PolicyEscalateParams = parse_request(object)?;
let projection =
fetch_authority_projection(service, binding_id, params.namespace.clone()).await?;
let item = service
.escalate_policy(PolicyEscalateRequest {
id: params.id,
realm_id: None,
namespace: params.namespace,
expected_revision: params.expected_revision,
authority_projection: projection,
completion_policy: params.completion_policy,
})
.await
.map_err(workgraph_error_to_rpc)?;
Ok(serde_json::json!({ "item": item }))
}
"mobkit/workgraph/goal/create" => {
reject_non_default_namespace(service, &object)?;
let mut object = object;
let target_value = object
.remove("target")
.ok_or_else(|| invalid_params("target is required"))?;
let target = resolve_goal_target(mob_id, &target_value)?;
let target = admission
.lower_member_session_target(target)
.await
.map_err(admission_error_to_rpc)?;
object.insert("target".to_string(), to_result_value(&target));
let request: GoalCreateRequest = parse_request(object)?;
let _permit = admission.acquire().await.map_err(admission_error_to_rpc)?;
admission
.check_target_free(
service,
request.namespace.clone(),
&request.target.to_attention_target(),
None,
"creating another goal for the same target",
)
.await
.map_err(admission_error_to_rpc)?;
let result = service
.create_goal(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/goal/confirm" => {
let binding_id = parse_binding_id(&object)?;
let expected_revision = parse_expected_revision(&object)?;
let namespace = parse_namespace(&object)?;
let evidence = match object.get("evidence") {
None | Some(Value::Null) => {
let status = service
.goal_status(GoalStatusRequest {
binding_id: binding_id.clone(),
realm_id: None,
namespace: namespace.clone(),
})
.await
.map_err(workgraph_error_to_rpc)?;
default_confirm_evidence(&status.item.completion_policy, &binding_id)
}
Some(value) => parse_confirm_evidence(value)?,
};
let request = GoalConfirmRequest {
binding_id,
realm_id: None,
namespace,
expected_revision,
evidence,
principal: None,
trusted_principal: None,
}
.with_trusted_principal(trusted_principal);
let result = service
.goal_confirm(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/goal/request_close" => {
let request: GoalRequestCloseRequest = parse_request(object)?;
let result = service
.goal_request_close(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/attention/pause" => {
reject_non_default_namespace(service, &object)?;
let request: AttentionPauseRequest = parse_request(object)?;
let result = service
.pause_attention(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/attention/resume" => {
reject_non_default_namespace(service, &object)?;
let request: AttentionResumeRequest = parse_request(object)?;
let _permit = admission.acquire().await.map_err(admission_error_to_rpc)?;
admission
.check_resume_target_free(service, request.namespace.clone(), &request.binding_id)
.await
.map_err(admission_error_to_rpc)?;
let result = service
.resume_attention(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/attention/reassign" => {
reject_non_default_namespace(service, &object)?;
if object.contains_key("authority_projection") {
return Err(invalid_params(
"authority_projection is not accepted; the authority witness is fetched \
server-side from binding_id",
));
}
let binding_id = parse_binding_id(&object)?;
let expected_revision = parse_expected_revision(&object)?;
let namespace = parse_namespace(&object)?;
let target_value = object
.get("target")
.ok_or_else(|| invalid_params("target is required"))?;
let target = resolve_goal_target(mob_id, target_value)?;
let target = admission
.lower_member_session_target(target)
.await
.map_err(admission_error_to_rpc)?;
let _permit = admission.acquire().await.map_err(admission_error_to_rpc)?;
admission
.check_target_free(
service,
namespace.clone(),
&target.to_attention_target(),
Some(&binding_id),
"reassigning this binding onto the same target",
)
.await
.map_err(admission_error_to_rpc)?;
let projection =
fetch_authority_projection(service, binding_id.clone(), namespace.clone()).await?;
let binding_mode = projection.mode;
let result = service
.reassign_attention(AttentionReassignRequest {
binding_id: binding_id.clone(),
realm_id: None,
namespace,
expected_revision,
authority_projection: projection,
target,
})
.await
.map_err(|error| reassign_error_to_rpc(error, &binding_id, binding_mode))?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/attention/prune" => {
reject_non_default_namespace(service, &object)?;
let request: AttentionPruneRequest = parse_request(object)?;
let result = service
.prune_terminal_attention(request)
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
"mobkit/workgraph/attention/break_glass_reassign" => {
reject_non_default_namespace(service, &object)?;
let WorkgraphSurface::Console {
authenticated_principal,
} = surface
else {
return Err(method_not_found());
};
let Some(principal) = authenticated_principal
.map(str::trim)
.filter(|value| !value.is_empty())
else {
return Err(JsonRpcError {
code: -32030,
message: "break-glass reassignment requires an authenticated console \
principal for audit attribution"
.to_string(),
data: Some(serde_json::json!({ "kind": "access_denied" })),
});
};
if object.contains_key("principal") {
return Err(invalid_params(
"principal is not accepted; the authenticated console principal is recorded",
));
}
if object.contains_key("authority_projection") {
return Err(invalid_params(
"authority_projection is not accepted; break-glass bypasses the witness \
by design and is audited instead",
));
}
let binding_id = parse_binding_id(&object)?;
let expected_revision = parse_expected_revision(&object)?;
let namespace = parse_namespace(&object)?;
let reason = object
.get("reason")
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.ok_or_else(|| invalid_params("reason is required and must be non-empty"))?
.to_string();
let target_value = object
.get("target")
.ok_or_else(|| invalid_params("target is required"))?;
let target = resolve_goal_target(mob_id, target_value)?;
let target = admission
.lower_member_session_target(target)
.await
.map_err(admission_error_to_rpc)?;
let _permit = admission.acquire().await.map_err(admission_error_to_rpc)?;
admission
.check_target_free(
service,
namespace.clone(),
&target.to_attention_target(),
Some(&binding_id),
"break-glass reassigning this binding onto the same target",
)
.await
.map_err(admission_error_to_rpc)?;
let result = service
.break_glass_reassign_attention(BreakGlassAttentionReassignRequest {
binding_id,
realm_id: None,
namespace,
expected_revision,
target,
principal: principal.to_string(),
reason,
})
.await
.map_err(workgraph_error_to_rpc)?;
Ok(to_result_value(&result))
}
_ => Err(method_not_found()),
}
}
pub(crate) fn console_trusted_principal(
authenticated_principal: Option<&str>,
) -> Option<WorkOwnerKey> {
let principal = authenticated_principal
.map(str::trim)
.filter(|value| !value.is_empty())?;
match WorkOwnerKey::principal(principal) {
Ok(key) => Some(key),
Err(error) => {
tracing::warn!(
target: "mobkit::workgraph",
error = %error,
"console principal could not be lowered to a workgraph owner key",
);
None
}
}
}
#[cfg(test)]
#[allow(clippy::expect_used, clippy::unwrap_used)]
mod tests {
use meerkat::WorkAttentionStatus;
use super::*;
#[test]
fn method_predicates_partition_the_namespace() {
for method in WORKGRAPH_READ_METHODS {
assert!(is_workgraph_method(method));
assert!(is_workgraph_read_method(method));
assert!(!is_workgraph_mutating_method(method));
}
for method in WORKGRAPH_MUTATE_METHODS {
assert!(is_workgraph_method(method));
assert!(is_workgraph_mutating_method(method));
assert!(!is_workgraph_read_method(method));
}
assert!(is_workgraph_method("mobkit/workgraph/bogus"));
assert!(!is_workgraph_method("mobkit/memory/query"));
}
#[test]
fn error_taxonomy_matches_wire_contract() {
let conflict = workgraph_error_to_rpc(WorkGraphError::StaleRevision {
id: WorkItemId::new("work_1").expect("id"),
expected: 1,
actual: 2,
});
assert_eq!(conflict.code, WORKGRAPH_CONFLICT_CODE);
assert_eq!(
conflict.data.as_ref().unwrap()["kind"],
serde_json::json!("workgraph_conflict")
);
let conflict = workgraph_error_to_rpc(WorkGraphError::Conflict("busy".to_string()));
assert_eq!(conflict.code, WORKGRAPH_CONFLICT_CODE);
let params = workgraph_error_to_rpc(WorkGraphError::InvalidInput("bad".to_string()));
assert_eq!(params.code, -32602);
let other = workgraph_error_to_rpc(WorkGraphError::Store("io".to_string()));
assert_eq!(other.code, WORKGRAPH_ERROR_CODE);
assert_eq!(
other.data.as_ref().unwrap()["kind"],
serde_json::json!("workgraph_error")
);
assert!(
other.data.as_ref().unwrap()["detail"]
.as_str()
.unwrap()
.contains("io"),
"full detail is disclosed (K2 posture)"
);
assert_eq!(
workgraph_unavailable_error().code,
WORKGRAPH_UNAVAILABLE_CODE
);
}
#[test]
fn resolve_goal_target_accepts_the_lowered_owner_result_spelling() {
let mob_id = meerkat_mob::MobId::from("round-trip-mob");
let owner_key = WorkOwnerKey::principal("operator@example.test").expect("owner key");
let owner_form = serde_json::json!({ "kind": "owner", "owner_key": owner_key });
let lowered_form = serde_json::json!({ "kind": "lowered_owner", "owner_key": owner_key });
let from_owner = resolve_goal_target(&mob_id, &owner_form).expect("owner form parses");
let from_lowered =
resolve_goal_target(&mob_id, &lowered_form).expect("lowered_owner form parses");
assert_eq!(from_owner, from_lowered);
assert_eq!(
to_result_value(&from_lowered.to_attention_target()),
lowered_form,
);
let error = resolve_goal_target(&mob_id, &serde_json::json!({ "kind": "mob" }))
.expect_err("unknown kinds stay rejected");
assert_eq!(error.code, -32602);
assert!(error.message.contains("lowered_owner"), "{}", error.message);
}
#[test]
fn console_principal_lowering_is_tolerant() {
assert!(console_trusted_principal(None).is_none());
assert!(console_trusted_principal(Some(" ")).is_none());
let key = console_trusted_principal(Some("alice@example.test")).expect("principal key");
assert_eq!(key.canonical(), "principal:alice@example.test");
}
#[test]
fn attention_status_param_accepts_both_wire_spellings() {
for state in ["active", "paused", "superseded", "stopped"] {
let mut object = Map::new();
object.insert("status".to_string(), Value::String(state.to_string()));
normalize_attention_status_param(&mut object).expect("bare string accepted");
assert_eq!(object["status"], serde_json::json!({ "state": state }));
let request: AttentionListRequest =
parse_request(object).expect("normalized status parses upstream");
assert!(request.status.is_some(), "{state}");
}
let mut object = Map::new();
object.insert(
"status".to_string(),
serde_json::json!({ "state": "paused" }),
);
normalize_attention_status_param(&mut object).expect("tagged form passes through");
let request: AttentionListRequest = parse_request(object).expect("tagged form parses");
assert!(matches!(
request.status,
Some(WorkAttentionStatus::Paused { until: None })
));
let mut object = Map::new();
object.insert("status".to_string(), Value::String("bogus".to_string()));
let error =
normalize_attention_status_param(&mut object).expect_err("unknown string rejected");
assert_eq!(error.code, -32602);
assert!(error.message.contains("active"), "{}", error.message);
}
#[tokio::test]
async fn stale_attention_witness_maps_to_conflict() {
use meerkat::{
AttentionProjectionRequest, GoalAttentionTarget, GoalCreateRequest,
PolicyEscalateRequest, UpdateWorkItemRequest, WorkCompletionPolicy,
};
let service = crate::workgraph_wiring::ephemeral_workgraph_service("stale-realm");
let goal = service
.create_goal(GoalCreateRequest {
realm_id: None,
namespace: None,
title: "goes stale".to_string(),
description: None,
target: GoalAttentionTarget::Owner {
owner_key: WorkOwnerKey::principal("operator@example.test").expect("key"),
},
mode: Default::default(),
completion_policy: Default::default(),
delegated_authority: Default::default(),
projection_policy: Default::default(),
})
.await
.expect("create goal");
let stale_witness = service
.attention_projection(AttentionProjectionRequest {
binding_id: goal.attention.binding_id.clone(),
realm_id: None,
namespace: None,
})
.await
.expect("attention projection")
.projection;
let bumped = service
.update(UpdateWorkItemRequest {
id: goal.item.id.clone(),
realm_id: None,
namespace: None,
expected_revision: goal.item.revision,
title: Some("moved underneath".to_string()),
description: None,
priority: None,
completion_policy: None,
labels: None,
due_at: None,
not_before: None,
snoozed_until: None,
external_refs: Vec::new(),
})
.await
.expect("bump item revision");
let error = service
.escalate_policy(PolicyEscalateRequest {
id: goal.item.id.clone(),
realm_id: None,
namespace: None,
expected_revision: bumped.revision,
authority_projection: stale_witness,
completion_policy: WorkCompletionPolicy::HostConfirmed,
})
.await
.expect_err("a stale witness must be rejected");
assert!(
matches!(&error, WorkGraphError::InvalidTransition(message)
if message.starts_with(STALE_ATTENTION_WITNESS_PREFIX)),
"pins the upstream stale-witness spelling: {error}"
);
let rpc_error = workgraph_error_to_rpc(error);
assert_eq!(rpc_error.code, WORKGRAPH_CONFLICT_CODE, "{rpc_error:?}");
assert_eq!(
rpc_error.data.as_ref().unwrap()["kind"],
serde_json::json!("workgraph_conflict")
);
assert!(
rpc_error.data.as_ref().unwrap()["detail"]
.as_str()
.unwrap()
.contains(STALE_ATTENTION_WITNESS_PREFIX),
"{rpc_error:?}"
);
}
}