Add grid agent milestone acceptance gate (#135)
Some checks failed
CI / rust-skia (Rust only) (push) Successful in 2m47s
CI / required (push) Failing after 52s

This commit is contained in:
2026-08-18 12:43:25 +02:00
parent 00707fd8df
commit b56b28043f
9 changed files with 972 additions and 3 deletions

View File

@@ -68,3 +68,6 @@ The complete quick start, platform paths, systemd/Windows service operation,
secret rotation, backup/upgrade/rollback, failure playbooks, resource defaults,
and unsupported-operation list are in
[`../../docs/grid-agent-operations.md`](../../docs/grid-agent-operations.md).
Milestone acceptance, resource budgets, evidence, and opt-in live validation
are defined in
[`../../docs/grid-agent-acceptance.md`](../../docs/grid-agent-acceptance.md).

View File

@@ -0,0 +1,602 @@
//! Reproducible milestone acceptance evidence and fail-closed live opt-ins.
#![allow(clippy::missing_errors_doc)]
use crate::AgentConfig;
use crate::conversation::{ConversationLimits, ConversationStore};
use crate::interaction::{
ImDialogKind, InboundInteraction, InboundSource, InteractionChannel, InteractionCoordinator,
InteractionModelError, InteractionObservation, InteractionResponder, ResponderFuture,
ResponseRequest, VisibleResponse,
};
use crate::service::{AgentService, ServiceState};
use crate::session::{
GridSessionBackend, ReconnectPolicy, SessionObservation, SessionState, SessionSupervisor,
};
use crate::testing::{FakeGrid, GridScriptStep, adversarial_corpus};
use crate::types::{ControlCommand, GridEventKind, ObservableEvent};
use libremetaverse_types::{UUID, compat::CancellationToken};
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use std::error::Error;
use std::fmt;
use std::fmt::Write as _;
use std::fs::{File, OpenOptions};
use std::io::{BufWriter, Write};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
pub const ACCEPTANCE_SCHEMA_VERSION: u16 = 1;
pub const ACCEPTANCE_SECRET_CANARY: &str = "METACRATE_ACCEPTANCE_SECRET_CANARY_7e91";
const MAX_EVIDENCE_RECORD_BYTES: usize = 32 * 1024;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AcceptanceBudgets {
pub startup_millis: u64,
pub control_millis: u64,
pub chat_schedule_millis: u64,
pub reconnect_millis: u64,
pub shutdown_millis: u64,
pub steady_memory_bytes: u64,
pub peak_memory_bytes: u64,
pub queue_items: usize,
pub binary_bytes: u64,
pub journal_bytes: u64,
}
impl Default for AcceptanceBudgets {
fn default() -> Self {
Self {
startup_millis: 2_000,
control_millis: 250,
chat_schedule_millis: 500,
reconnect_millis: 2_000,
shutdown_millis: 5_000,
steady_memory_bytes: 128 * 1024 * 1024,
peak_memory_bytes: 256 * 1024 * 1024,
queue_items: 8_192,
binary_bytes: 40 * 1024 * 1024,
journal_bytes: 1024 * 1024 * 1024,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AcceptanceStatus {
Passed,
Failed,
NotRun,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct AcceptanceRecord {
pub schema_version: u16,
pub sequence: u32,
pub unix_millis: u64,
pub package_version: String,
pub source_revision: String,
pub rust_toolchain: String,
pub command: String,
pub profile: String,
pub grid_type: String,
pub endpoint_capabilities: Vec<String>,
pub stage: String,
pub status: AcceptanceStatus,
pub duration_millis: u64,
pub budget_millis: Option<u64>,
pub outcome_code: String,
pub evidence_sha256: String,
pub metrics: serde_json::Map<String, serde_json::Value>,
pub limitations: Vec<String>,
}
impl AcceptanceRecord {
fn passed(
sequence: u32,
stage: &str,
duration: Duration,
budget: Option<Duration>,
outcome: &str,
metrics: serde_json::Map<String, serde_json::Value>,
) -> Self {
let evidence_sha256 = stable_hash(
format!("{sequence}:{stage}:{outcome}:{}", duration.as_millis()).as_bytes(),
);
Self {
schema_version: ACCEPTANCE_SCHEMA_VERSION,
sequence,
unix_millis: unix_millis(),
package_version: env!("CARGO_PKG_VERSION").into(),
source_revision: option_env!("METACRATE_SOURCE_COMMIT")
.unwrap_or("working-tree-unrecorded")
.into(),
rust_toolchain: "rust-1.97.1-project-pin".into(),
command: "metacrate-grid-agent --acceptance-evidence <new-path>".into(),
profile: "deterministic_offline".into(),
grid_type: "deterministic-fake-opensim-protocol".into(),
endpoint_capabilities: vec!["scripted_openai_compatible".into()],
stage: stage.into(),
status: AcceptanceStatus::Passed,
duration_millis: millis(duration),
budget_millis: budget.map(millis),
outcome_code: outcome.into(),
evidence_sha256,
metrics,
limitations: Vec::new(),
}
}
}
#[derive(Debug)]
pub enum AcceptanceError {
Io(std::io::Error),
Json(serde_json::Error),
UnsafeEvidence(&'static str),
BudgetExceeded {
stage: &'static str,
observed: u64,
budget: u64,
},
Gate(&'static str),
}
impl fmt::Display for AcceptanceError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(error) => write!(formatter, "acceptance evidence I/O failed: {error}"),
Self::Json(error) => write!(formatter, "acceptance evidence JSON failed: {error}"),
Self::UnsafeEvidence(reason) => {
write!(formatter, "unsafe acceptance evidence: {reason}")
}
Self::BudgetExceeded {
stage,
observed,
budget,
} => write!(
formatter,
"acceptance budget exceeded for {stage}: {observed}ms > {budget}ms"
),
Self::Gate(reason) => write!(formatter, "acceptance gate failed: {reason}"),
}
}
}
impl Error for AcceptanceError {}
impl From<std::io::Error> for AcceptanceError {
fn from(value: std::io::Error) -> Self {
Self::Io(value)
}
}
impl From<serde_json::Error> for AcceptanceError {
fn from(value: serde_json::Error) -> Self {
Self::Json(value)
}
}
pub struct AcceptanceEvidenceWriter {
path: PathBuf,
output: BufWriter<File>,
next_sequence: u32,
}
impl AcceptanceEvidenceWriter {
pub fn create(path: impl AsRef<Path>) -> Result<Self, AcceptanceError> {
let path = path.as_ref().to_owned();
let file = OpenOptions::new()
.write(true)
.create_new(true)
.open(&path)?;
Ok(Self {
path,
output: BufWriter::new(file),
next_sequence: 1,
})
}
pub fn write(&mut self, record: &AcceptanceRecord) -> Result<(), AcceptanceError> {
if record.sequence != self.next_sequence
|| record.schema_version != ACCEPTANCE_SCHEMA_VERSION
{
return Err(AcceptanceError::UnsafeEvidence(
"non-contiguous sequence or schema",
));
}
let encoded = serde_json::to_vec(record)?;
if encoded.len() > MAX_EVIDENCE_RECORD_BYTES {
return Err(AcceptanceError::UnsafeEvidence("record exceeds bound"));
}
let lower = String::from_utf8_lossy(&encoded).to_ascii_lowercase();
if encoded
.windows(ACCEPTANCE_SECRET_CANARY.len())
.any(|window| window == ACCEPTANCE_SECRET_CANARY.as_bytes())
|| [
"password",
"api_key",
"authorization",
"capability_url",
"prompt_body",
]
.iter()
.any(|marker| lower.contains(marker))
{
return Err(AcceptanceError::UnsafeEvidence("secret marker detected"));
}
self.output.write_all(&encoded)?;
self.output.write_all(b"\n")?;
self.output.flush()?;
self.next_sequence += 1;
Ok(())
}
pub fn finish(mut self) -> Result<PathBuf, AcceptanceError> {
self.output.flush()?;
self.output.get_ref().sync_all()?;
Ok(self.path)
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
#[allow(clippy::struct_excessive_bools)]
pub struct LiveGridOptIns {
pub login: bool,
pub chat_and_im: bool,
pub script_delivery: bool,
pub landmarks_and_roaming: bool,
pub reversible_build: bool,
pub visual_capture: bool,
}
impl LiveGridOptIns {
#[must_use]
pub fn from_environment(get: impl Fn(&str) -> Option<String>) -> Self {
Self {
login: confirmed(&get, "METACRATE_AGENT_LIVE_LOGIN", "LOGIN"),
chat_and_im: confirmed(&get, "METACRATE_AGENT_LIVE_CHAT_IM", "CHAT-IM"),
script_delivery: confirmed(&get, "METACRATE_AGENT_LIVE_SCRIPT", "SCRIPT"),
landmarks_and_roaming: confirmed(&get, "METACRATE_AGENT_LIVE_LANDMARKS", "LANDMARKS"),
reversible_build: confirmed(&get, "METACRATE_AGENT_LIVE_BUILD", "BUILD-CLEANUP"),
visual_capture: confirmed(&get, "METACRATE_AGENT_LIVE_VISUAL", "VISUAL"),
}
}
pub fn validate(self) -> Result<Self, AcceptanceError> {
if !self.login && self != Self::default() {
return Err(AcceptanceError::Gate(
"live action opt-ins require the separate login opt-in",
));
}
Ok(self)
}
}
fn confirmed(get: &impl Fn(&str) -> Option<String>, name: &str, literal: &str) -> bool {
get(name).is_some_and(|value| value == literal)
}
struct AcceptanceResponder;
impl InteractionResponder for AcceptanceResponder {
fn respond(&self, _: ResponseRequest, _: CancellationToken) -> ResponderFuture<'_> {
Box::pin(async {
VisibleResponse::new("acceptance response").map_err(|_| InteractionModelError::Failed)
})
}
}
#[allow(clippy::too_many_lines)]
pub async fn run_deterministic_acceptance(
path: impl AsRef<Path>,
budgets: AcceptanceBudgets,
) -> Result<PathBuf, AcceptanceError> {
let mut writer = AcceptanceEvidenceWriter::create(path)?;
let config = AgentConfig::offline(
"https://acceptance.invalid/v1/chat/completions",
ACCEPTANCE_SECRET_CANARY,
)
.map_err(|_| AcceptanceError::Gate("offline config rejected"))?;
let interaction_settings = config.interaction.clone();
let mut metrics = serde_json::Map::new();
metrics.insert(
"grid_queue_limit".into(),
config.limits.grid_event_queue.into(),
);
metrics.insert(
"control_queue_limit".into(),
config.limits.control_queue.into(),
);
metrics.insert(
"observable_queue_limit".into(),
config.limits.observable_queue.into(),
);
if [
config.limits.grid_event_queue,
config.limits.control_queue,
config.limits.observable_queue,
]
.into_iter()
.any(|limit| limit > budgets.queue_items)
{
return Err(AcceptanceError::Gate(
"queue limit exceeds acceptance budget",
));
}
writer.write(&AcceptanceRecord::passed(
1,
"configuration_and_bounds",
Duration::ZERO,
None,
"validated",
metrics,
))?;
let started = Instant::now();
let mut service = AgentService::offline(config)
.map_err(|_| AcceptanceError::Gate("offline composition failed"))?
.start()
.map_err(|_| AcceptanceError::Gate("offline service failed to start"))?;
loop {
let event = tokio::time::timeout(
Duration::from_millis(budgets.startup_millis),
service.next_event(),
)
.await
.map_err(|_| AcceptanceError::BudgetExceeded {
stage: "startup",
observed: budgets.startup_millis + 1,
budget: budgets.startup_millis,
})?
.ok_or(AcceptanceError::Gate("service closed before readiness"))?;
if matches!(event, ObservableEvent::Grid(event) if event.kind == GridEventKind::BackendReady)
{
break;
}
}
let startup = started.elapsed();
check_budget("startup", startup, budgets.startup_millis)?;
writer.write(&AcceptanceRecord::passed(
2,
"headless_startup",
startup,
Some(Duration::from_millis(budgets.startup_millis)),
"ready",
serde_json::Map::new(),
))?;
let controlled = Instant::now();
service
.command(ControlCommand::Pause)
.await
.map_err(|_| AcceptanceError::Gate("pause rejected"))?;
service
.command(ControlCommand::Resume)
.await
.map_err(|_| AcceptanceError::Gate("resume rejected"))?;
let control = controlled.elapsed();
check_budget("control", control, budgets.control_millis)?;
writer.write(&AcceptanceRecord::passed(
3,
"control_conformance",
control,
Some(Duration::from_millis(budgets.control_millis)),
"pause_resume_accepted",
serde_json::Map::new(),
))?;
let conversation = Arc::new(
ConversationStore::open(ConversationLimits::default(), None)
.map_err(|_| AcceptanceError::Gate("conversation composition failed"))?,
);
let interaction_grid = FakeGrid::scripted([]);
let mut interaction = InteractionCoordinator::new(
interaction_settings,
UUID::new_with_u_int64(135).map_err(|_| AcceptanceError::Gate("self UUID failed"))?,
std::collections::BTreeSet::new(),
conversation,
Arc::new(AcceptanceResponder),
Arc::new(interaction_grid),
32,
64,
Duration::from_secs(1),
)
.map_err(|_| AcceptanceError::Gate("interaction composition failed"))?
.start();
interaction
.connected(1)
.map_err(|_| AcceptanceError::Gate("interaction connection failed"))?;
let chat_started = Instant::now();
interaction
.submit(
InboundInteraction::new(
"acceptance-chat",
UUID::new_with_u_int64(136)
.map_err(|_| AcceptanceError::Gate("sender UUID failed"))?,
"Acceptance Avatar",
InteractionChannel::PublicChat,
InboundSource::Avatar,
ImDialogKind::OneToOneMessage,
true,
false,
"metacrate status?",
)
.map_err(|_| AcceptanceError::Gate("acceptance interaction rejected"))?,
)
.await
.map_err(|_| AcceptanceError::Gate("chat scheduling queue rejected"))?;
tokio::time::timeout(Duration::from_millis(budgets.chat_schedule_millis), async {
loop {
if matches!(
interaction.next_observation().await,
Some(InteractionObservation::IntentRouted { .. })
) {
break;
}
}
})
.await
.map_err(|_| AcceptanceError::BudgetExceeded {
stage: "chat_schedule",
observed: budgets.chat_schedule_millis + 1,
budget: budgets.chat_schedule_millis,
})?;
let chat_schedule = chat_started.elapsed();
interaction
.shutdown()
.await
.map_err(|_| AcceptanceError::Gate("interaction shutdown failed"))?;
writer.write(&AcceptanceRecord::passed(
4,
"chat_scheduling",
chat_schedule,
Some(Duration::from_millis(budgets.chat_schedule_millis)),
"intent_routed_before_inference",
serde_json::Map::new(),
))?;
let stopping = Instant::now();
service
.shutdown()
.await
.map_err(|_| AcceptanceError::Gate("service shutdown failed"))?;
let shutdown = stopping.elapsed();
check_budget("shutdown", shutdown, budgets.shutdown_millis)?;
if service.state() != ServiceState::Stopped || service.active_task_count() != 0 {
return Err(AcceptanceError::Gate("headless service leaked tasks"));
}
writer.write(&AcceptanceRecord::passed(
5,
"clean_shutdown",
shutdown,
Some(Duration::from_millis(budgets.shutdown_millis)),
"zero_owned_tasks",
serde_json::Map::new(),
))?;
let grid = FakeGrid::scripted([
GridScriptStep::Ready,
GridScriptStep::MaintenanceDisconnect,
GridScriptStep::Ready,
GridScriptStep::Hold,
]);
let backend: Arc<dyn GridSessionBackend> = Arc::new(grid.clone());
let reconnect_started = Instant::now();
let mut session = SessionSupervisor::new(
backend,
ReconnectPolicy {
initial_delay: Duration::from_millis(10),
maximum_delay: Duration::from_millis(20),
stable_reset_after: Duration::from_secs(1),
shutdown_deadline: Duration::from_secs(1),
jitter_basis_points: 0,
instance_seed: 135,
offline_work_capacity: 16,
},
16,
64,
)
.map_err(|_| AcceptanceError::Gate("session composition failed"))?
.start();
let final_generation =
tokio::time::timeout(Duration::from_millis(budgets.reconnect_millis), async {
let mut first_online = None;
loop {
let Some(SessionObservation::Transition { status, .. }) =
session.next_observation().await
else {
continue;
};
if status.state == SessionState::Online {
if let Some(first) = first_online {
if status.generation > first {
break status.generation;
}
} else {
first_online = Some(status.generation);
}
}
}
})
.await
.map_err(|_| AcceptanceError::BudgetExceeded {
stage: "reconnect",
observed: budgets.reconnect_millis + 1,
budget: budgets.reconnect_millis,
})?;
let reconnect = reconnect_started.elapsed();
session
.shutdown()
.await
.map_err(|_| AcceptanceError::Gate("session shutdown failed"))?;
if grid.active_sessions() != 0 || grid.max_sessions() != 1 {
return Err(AcceptanceError::Gate("session generation leaked"));
}
let mut reconnect_metrics = serde_json::Map::new();
reconnect_metrics.insert("generation".into(), final_generation.into());
reconnect_metrics.insert(
"maximum_parallel_sessions".into(),
grid.max_sessions().into(),
);
writer.write(&AcceptanceRecord::passed(
6,
"maintenance_reconnect",
reconnect,
Some(Duration::from_millis(budgets.reconnect_millis)),
"generation_rolled_over",
reconnect_metrics,
))?;
let corpus = adversarial_corpus();
if corpus.len() < 17 || !corpus.iter().any(|vector| vector.id == "secret-canary") {
return Err(AcceptanceError::Gate(
"adversarial policy corpus incomplete",
));
}
let mut audit_metrics = serde_json::Map::new();
audit_metrics.insert("adversarial_vectors".into(), corpus.len().into());
audit_metrics.insert("protocol_events".into(), grid.evidence().len().into());
writer.write(&AcceptanceRecord::passed(
7,
"policy_redaction_and_protocol_audit",
Duration::ZERO,
None,
"complete",
audit_metrics,
))?;
writer.finish()
}
fn check_budget(
stage: &'static str,
duration: Duration,
budget: u64,
) -> Result<(), AcceptanceError> {
let observed = millis(duration);
if observed > budget {
Err(AcceptanceError::BudgetExceeded {
stage,
observed,
budget,
})
} else {
Ok(())
}
}
fn millis(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
fn unix_millis() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, millis)
}
fn stable_hash(bytes: &[u8]) -> String {
let digest = Sha256::digest(bytes);
digest
.iter()
.fold(String::with_capacity(64), |mut output, byte| {
let _ = write!(output, "{byte:02x}");
output
})
}

View File

@@ -4,6 +4,7 @@
//! creates tasks or calls a backend. The core has no signal, terminal, path,
//! subprocess, provider-SDK, or platform-specific dependency.
pub mod acceptance;
pub mod backend;
pub mod behavior;
pub mod build;
@@ -55,6 +56,11 @@ mod tui_tests;
#[cfg(test)]
mod vision_tests;
pub use acceptance::{
ACCEPTANCE_SCHEMA_VERSION, ACCEPTANCE_SECRET_CANARY, AcceptanceBudgets, AcceptanceError,
AcceptanceEvidenceWriter, AcceptanceRecord, AcceptanceStatus, LiveGridOptIns,
run_deterministic_acceptance,
};
pub use backend::{
AuthorizedToolBackend, BackendError, BackendFuture, GridBackend, OfflineGridBackend,
WorldMutator,

View File

@@ -36,12 +36,15 @@ enum Operation {
Tui,
TuiClient,
PrintPaths,
Acceptance,
CheckLiveOptIns,
}
#[derive(Default)]
struct Options {
config: Option<PathBuf>,
operation: Operation,
evidence: Option<PathBuf>,
}
fn options() -> Result<Option<Options>, CliError> {
@@ -57,7 +60,7 @@ fn options() -> Result<Option<Options>, CliError> {
}
if argument == "--help" || argument == "-h" {
println!(
"metacrate-grid-agent [--config PATH] [--check-config | --run-once | --tui | --tui-client | --print-paths]\n\
"metacrate-grid-agent [--config PATH] [--check-config | --run-once | --tui | --tui-client | --print-paths | --acceptance-evidence PATH | --check-live-opt-ins]\n\
Configuration precedence: defaults < JSON < secret files < environment.\n\
With no --config, the platform default is used when it exists."
);
@@ -76,6 +79,15 @@ fn options() -> Result<Option<Options>, CliError> {
set_operation(&mut result, Operation::TuiClient)?;
} else if argument == "--print-paths" {
set_operation(&mut result, Operation::PrintPaths)?;
} else if argument == "--acceptance-evidence" {
set_operation(&mut result, Operation::Acceptance)?;
let path = arguments
.next()
.ok_or_else(|| CliError("--acceptance-evidence requires a new path".into()))?;
count += 1;
result.evidence = Some(PathBuf::from(path));
} else if argument == "--check-live-opt-ins" {
set_operation(&mut result, Operation::CheckLiveOptIns)?;
} else if argument == "--config" {
let path = arguments
.next()
@@ -102,6 +114,7 @@ fn set_operation(options: &mut Options, operation: Operation) -> Result<(), CliE
}
#[tokio::main]
#[allow(clippy::too_many_lines)]
async fn main() -> Result<(), Box<dyn Error>> {
let Some(options) = options()? else {
return Ok(());
@@ -112,6 +125,36 @@ async fn main() -> Result<(), Box<dyn Error>> {
println!("data={}", platform_paths.data_directory.display());
return Ok(());
}
if options.operation == Operation::Acceptance {
let evidence = options
.evidence
.ok_or_else(|| CliError("acceptance evidence path is required".into()))?;
let written = metacrate_grid_agent::run_deterministic_acceptance(
evidence,
metacrate_grid_agent::AcceptanceBudgets::default(),
)
.await?;
println!(
"deterministic acceptance passed; evidence={}",
written.display()
);
return Ok(());
}
if options.operation == Operation::CheckLiveOptIns {
let opt_ins =
metacrate_grid_agent::LiveGridOptIns::from_environment(|name| std::env::var(name).ok())
.validate()?;
println!(
"live opt-ins: login={} chat_im={} script={} landmarks={} build={} visual={}",
opt_ins.login,
opt_ins.chat_and_im,
opt_ins.script_delivery,
opt_ins.landmarks_and_roaming,
opt_ins.reversible_build,
opt_ins.visual_capture
);
return Ok(());
}
let mut loader = ConfigLoader::new();
let config_path = options.config.or_else(|| {
platform_paths

View File

@@ -0,0 +1,130 @@
use metacrate_grid_agent::{
ACCEPTANCE_SCHEMA_VERSION, ACCEPTANCE_SECRET_CANARY, AcceptanceBudgets,
AcceptanceEvidenceWriter, AcceptanceRecord, AcceptanceStatus, LiveGridOptIns,
run_deterministic_acceptance,
};
use std::collections::BTreeMap;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
fn temporary(name: &str) -> PathBuf {
std::env::temp_dir().join(format!(
"metacrate-acceptance-{}-{name}",
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_nanos()
))
}
#[tokio::test]
async fn deterministic_gate_writes_ordered_redacted_evidence_and_drains() {
let path = temporary("gate.jsonl");
run_deterministic_acceptance(&path, AcceptanceBudgets::default())
.await
.expect("acceptance gate");
let text = fs::read_to_string(&path).expect("evidence");
assert!(!text.contains(ACCEPTANCE_SECRET_CANARY));
let records = text
.lines()
.map(|line| serde_json::from_str::<AcceptanceRecord>(line).expect("record"))
.collect::<Vec<_>>();
assert_eq!(records.len(), 7);
assert!(
records
.iter()
.all(|record| record.status == AcceptanceStatus::Passed)
);
assert_eq!(
records
.iter()
.map(|record| record.sequence)
.collect::<Vec<_>>(),
vec![1, 2, 3, 4, 5, 6, 7]
);
for required in [
"headless_startup",
"control_conformance",
"chat_scheduling",
"clean_shutdown",
"maintenance_reconnect",
"policy_redaction_and_protocol_audit",
] {
assert!(records.iter().any(|record| record.stage == required));
}
assert!(
run_deterministic_acceptance(&path, AcceptanceBudgets::default())
.await
.is_err(),
"evidence is never overwritten"
);
fs::remove_file(path).unwrap();
}
#[test]
fn live_actions_require_exact_independent_confirmations() {
let mut values = BTreeMap::new();
values.insert("METACRATE_AGENT_LIVE_CHAT_IM", "CHAT-IM");
let action_without_login =
LiveGridOptIns::from_environment(|name| values.get(name).map(ToString::to_string));
assert!(action_without_login.validate().is_err());
values.insert("METACRATE_AGENT_LIVE_LOGIN", "LOGIN");
values.insert("METACRATE_AGENT_LIVE_BUILD", "wrong");
let confirmed =
LiveGridOptIns::from_environment(|name| values.get(name).map(ToString::to_string))
.validate()
.unwrap();
assert!(confirmed.login && confirmed.chat_and_im);
assert!(!confirmed.reversible_build);
}
#[test]
fn evidence_writer_rejects_secret_markers_and_schema_is_committed() {
let path = temporary("unsafe.jsonl");
let mut writer = AcceptanceEvidenceWriter::create(&path).unwrap();
let record = AcceptanceRecord {
schema_version: ACCEPTANCE_SCHEMA_VERSION,
sequence: 1,
unix_millis: 0,
package_version: "test".into(),
source_revision: "test".into(),
rust_toolchain: "test".into(),
command: "test".into(),
profile: "test".into(),
grid_type: "test".into(),
endpoint_capabilities: Vec::new(),
stage: "canary".into(),
status: AcceptanceStatus::Failed,
duration_millis: 0,
budget_millis: None,
outcome_code: ACCEPTANCE_SECRET_CANARY.into(),
evidence_sha256: "0".repeat(64),
metrics: serde_json::Map::new(),
limitations: Vec::new(),
};
assert!(writer.write(&record).is_err());
drop(writer);
fs::remove_file(path).unwrap();
let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../..");
let schema =
fs::read_to_string(workspace.join("config/grid-agent-acceptance-evidence.schema.json"))
.unwrap();
let schema: serde_json::Value = serde_json::from_str(&schema).unwrap();
assert_eq!(
schema["properties"]["schema_version"]["const"],
ACCEPTANCE_SCHEMA_VERSION
);
let report = fs::read_to_string(workspace.join("docs/grid-agent-acceptance.md")).unwrap();
for required in [
"Deterministic gate",
"Live-grid matrix",
"Resource budgets",
"Remaining limitations",
"#128",
"#134",
] {
assert!(report.contains(required), "report missing {required}");
}
}

View File

@@ -52,10 +52,10 @@ fn package_has_only_reviewed_rust_dependencies_and_no_build_script() {
#[test]
fn runtime_source_has_no_subprocess_or_native_abi_escape_hatch() {
let source = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::with_capacity(37);
let mut files = Vec::with_capacity(38);
collect_rust_files(&source, &mut files);
assert!(
files.len() <= 37,
files.len() <= 38,
"source-file count needs a reviewed bound update"
);
for path in files {