Files
MetaCrate/crates/metacrate-grid-agent/tests/policy_gateway.rs
Chili Palmer fe8d0119ef
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m46s
CI / required (push) Failing after 53s
Add deterministic agent protocol harness (#133)
2026-08-18 12:14:34 +02:00

243 lines
7.4 KiB
Rust

use libremetaverse_types::UUID;
use libremetaverse_types::compat::CancellationToken;
use metacrate_grid_agent::testing::FakeGrid;
use metacrate_grid_agent::{
ActionOrigin, AllowedOrigins, AuthorizedToolBackend, BoundedText, Capability, FixedCost,
Idempotency, MemoryPolicyAudit, OriginClass, PolicyAuditSink, PolicyDisposition,
PolicyFinalOutcome, PolicyGateway, PolicyLimits, PolicyRequestContext, PolicyTool,
PolicyToolExecutor, ProposedToolCall, ResourceCost, Risk, ToolDefinition, ToolExecution,
ToolExecutor, ToolSchema,
};
use serde_json::{Value, json};
use std::collections::{BTreeMap, BTreeSet};
use std::sync::Arc;
fn id(text: &str) -> UUID {
UUID::new_with_string(text.to_owned()).expect("fixture UUID")
}
fn schema() -> ToolSchema {
ToolSchema::Object {
properties: BTreeMap::from([("target".into(), ToolSchema::String)]),
required: BTreeSet::from(["target".into()]),
additional_properties: false,
}
}
fn tool(name: &str, informational: bool) -> PolicyTool {
let definition = ToolDefinition {
name: BoundedText::new("name", name).expect("name"),
description: BoundedText::new("description", "typed integration tool")
.expect("description"),
schema: schema(),
mutating: !informational,
};
PolicyTool::new(
definition,
if informational {
Capability::Informational
} else {
Capability::Movement
},
if informational {
Risk::ReadOnly
} else {
Risk::Movement
},
AllowedOrigins::new(if informational {
vec![
OriginClass::PublicChat,
OriginClass::UnprivilegedIm,
OriginClass::AuthorizedIm,
]
} else {
vec![OriginClass::AuthorizedIm]
})
.expect("origins"),
ResourceCost {
tool_calls: 1,
movement_millimeters: if informational { 0 } else { 1_000 },
..ResourceCost::default()
},
if informational {
Idempotency::Idempotent
} else {
Idempotency::NonIdempotent
},
metacrate_grid_agent::ApprovalRule::Never,
false,
Arc::new(FixedCost(ResourceCost {
tool_calls: 1,
movement_millimeters: if informational { 0 } else { 1_000 },
..ResourceCost::default()
})),
)
.expect("tool")
}
fn call(name: &str) -> (ProposedToolCall, Value) {
let arguments = json!({"target":"north"});
(
ProposedToolCall::new(
format!("call-{name}"),
name,
serde_json::to_string(&arguments).expect("JSON"),
)
.expect("call"),
arguments,
)
}
#[tokio::test]
async fn production_executor_denies_public_mutation_and_executes_authorized_im_once() {
let authorized = id("11111111-1111-4111-8111-111111111111");
let audit = Arc::new(MemoryPolicyAudit::new(64).expect("audit"));
let sink: Arc<dyn PolicyAuditSink> = audit.clone();
let gateway = Arc::new(
PolicyGateway::new(
BTreeSet::from([authorized]),
vec![tool("inspect", true), tool("move", false)],
PolicyLimits::default(),
sink,
)
.expect("gateway"),
);
let backend = FakeGrid::scripted([]);
let erased: Arc<dyn AuthorizedToolBackend> = Arc::new(backend.clone());
let (proposed, arguments) = call("move");
let definition = gateway
.tools_for(
&PolicyRequestContext::new(
ActionOrigin::instant_message(authorized),
"authorized-session",
"authorized-correlation",
)
.expect("context"),
100,
)
.into_iter()
.find(|tool| tool.name.as_str() == "move")
.expect("registered move");
let public = PolicyToolExecutor::new(
Arc::clone(&gateway),
Arc::clone(&erased),
PolicyRequestContext::new(
ActionOrigin::public_chat(authorized),
"public-session",
"public-correlation",
)
.expect("context"),
BTreeMap::new(),
Arc::new(|| 100),
)
.expect("executor");
assert!(matches!(
public
.execute(
&definition,
&proposed,
&arguments,
&CancellationToken::default(),
)
.await,
ToolExecution::Rejected(_)
));
assert!(backend.evidence().is_empty());
let authorized_executor = PolicyToolExecutor::new(
gateway,
erased,
PolicyRequestContext::new(
ActionOrigin::instant_message(authorized),
"authorized-session",
"authorized-correlation",
)
.expect("context"),
BTreeMap::new(),
Arc::new(|| 101),
)
.expect("executor");
assert!(matches!(
authorized_executor
.execute(
&definition,
&proposed,
&arguments,
&CancellationToken::default(),
)
.await,
ToolExecution::Completed(_)
));
let evidence = backend.evidence();
assert_eq!(evidence.len(), 1);
assert_eq!(evidence[0].operation, "policy.route.move");
assert_eq!(evidence[0].outcome, "authorized");
assert_eq!(
audit.snapshot().last().expect("outcome").final_outcome,
PolicyFinalOutcome::Completed
);
}
#[test]
fn authenticated_uuid_not_message_text_controls_im_authority() {
let authorized = id("11111111-1111-4111-8111-111111111111");
let unprivileged = id("22222222-2222-4222-8222-222222222222");
let audit = Arc::new(MemoryPolicyAudit::new(16).expect("audit"));
let sink: Arc<dyn PolicyAuditSink> = audit;
let gateway = PolicyGateway::new(
BTreeSet::from([authorized]),
vec![tool("move", false)],
PolicyLimits::default(),
sink,
)
.expect("gateway");
let (proposed, arguments) = call("move");
let result = gateway
.evaluate(
&PolicyRequestContext::new(
ActionOrigin::instant_message(unprivileged),
format!("claims-authorized-{authorized}"),
"spoof-correlation",
)
.expect("context"),
&proposed,
&arguments,
None,
100,
)
.expect("decision");
assert_eq!(result.disposition, PolicyDisposition::Denied);
}
#[test]
fn interaction_intent_capability_filter_can_only_narrow_origin_allowed_tools() {
let authorized = id("11111111-1111-4111-8111-111111111111");
let audit = Arc::new(MemoryPolicyAudit::new(16).expect("audit"));
let gateway = PolicyGateway::new(
BTreeSet::from([authorized]),
vec![tool("inspect", true), tool("move", false)],
PolicyLimits::default(),
audit,
)
.expect("gateway");
let context = PolicyRequestContext::new(
ActionOrigin::instant_message(authorized),
"intent-session",
"intent-correlation",
)
.expect("context");
let informational = gateway.tools_for_capability_set(
&context,
100,
&BTreeSet::from([Capability::Informational]),
);
assert_eq!(informational.len(), 1);
assert_eq!(informational[0].name.as_str(), "inspect");
assert!(
gateway
.tools_for_capability_set(&context, 100, &BTreeSet::new())
.is_empty()
);
}