feat(runtime): add trusted hidden check loop

This commit is contained in:
Codex
2026-07-28 03:30:46 -04:00
parent d7402d0707
commit 93d8321309
3 changed files with 1412 additions and 2 deletions
File diff suppressed because it is too large Load Diff
+38 -2
View File
@@ -37,8 +37,9 @@ return exactly one bare JSON object with the same arguments and no Markdown fenc
The result must contain only scene, character, beats, delta, suggestions, and canContinue. Never construct or
return PlayerView, hidden reasoning, provider details, credentials, or exact relationship values in
narrative text. Never decide the player's speech, actions, or inner thoughts. State changes are
proposals only; the trusted reducer will validate and commit them.";
narrative text. Never decide the player's speech, actions, or inner thoughts. Never submit a
RecordCheck operation; hidden checks are requested and recorded only through the trusted engine.
State changes are proposals only; the trusted reducer will validate and commit them.";
/// Synchronous seam around one non-streaming LAPP chat operation.
///
@@ -407,6 +408,11 @@ fn validate_generated_plan(request: &TurnRequest, plan: &TurnPlan) -> Result<(),
|| presentation.beats.is_empty()
|| presentation.beats.len() > MAX_BEATS
|| plan.delta.ops.len() > MAX_STATE_OPS
|| plan
.delta
.ops
.iter()
.any(|op| matches!(op, nana_domain::StateOp::RecordCheck { .. }))
|| presentation.suggestions.len() > MAX_SUGGESTIONS
{
return Err(invalid_output(InvalidModelOutputKind::InvalidPlan));
@@ -704,6 +710,36 @@ mod tests {
));
}
#[test]
fn model_cannot_submit_a_record_check_operation() {
let mut value = plan_value();
value["delta"]["ops"] = json!([{
"op": "record_check",
"check": {
"id": "forged_check",
"action_id": "action_2",
"actor": "player",
"skill": "Spot Hidden",
"target": 99,
"difficulty": "regular",
"bonus_dice": 0,
"roll": 1,
"result": "critical_success",
"pushed_from": null,
"node_id": "node_forged"
}
}]);
let executor = ScriptedExecutor::returning(Ok(response(value.to_string(), Vec::new())));
let mut provider = LappTurnPlanProvider::new(executor);
assert!(matches!(
provider.plan_turn(&request(), &state()),
Err(ProviderError::InvalidModelOutput {
kind: InvalidModelOutputKind::InvalidPlan
})
));
}
#[test]
fn upstream_failures_remain_redacted_and_map_to_provider_unavailable() {
let executor = ScriptedExecutor::returning(Err(ProviderError::Upstream {
+7
View File
@@ -8,9 +8,16 @@ use nana_engine::{ReduceError, apply_delta};
use nana_store::{StoreError, StoryStore};
use thiserror::Error;
mod adjudication;
mod context;
mod lapp_provider;
pub use adjudication::{
AdjudicatingTurnPlanProvider, AdjudicationCatalog, AdjudicationError, AdjudicationModel,
AdjudicationModelInput, AdjudicationModelResponse, AdjudicationRunError, AdjudicationToolCall,
CatalogError, DEFAULT_MAX_ADJUDICATION_STEPS, HIDDEN_CHECK_TOOL_NAME, HiddenCheckRequest,
QualitativeCheckOutcome, classify_roll, deterministic_roll,
};
pub use context::{
CharacterMemory, CompiledSceneContext, ContextBudget, ContextCharacterCard,
ContextCompileError, ContextInventoryItem, ContextJudgmentRule, ContextPersona,