From b0e78ffaad7c37e579998c178a4214fc3e2049c6 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 02:26:24 -0400 Subject: [PATCH 01/10] Add inactive risk-gate evaluator --- control/v1/evaluate-risk-gates.sh | 370 +++++++++++++++++ control/v1/risk-gates-decision.json | 1 + control/v1/risk-gates-policy.json | 1 + control/v1/risk-gates.jq | 226 +++++++++++ scripts/test/control-risk-gates.test.sh | 518 ++++++++++++++++++++++++ 5 files changed, 1116 insertions(+) create mode 100755 control/v1/evaluate-risk-gates.sh create mode 100644 control/v1/risk-gates-decision.json create mode 100644 control/v1/risk-gates-policy.json create mode 100644 control/v1/risk-gates.jq create mode 100755 scripts/test/control-risk-gates.test.sh diff --git a/control/v1/evaluate-risk-gates.sh b/control/v1/evaluate-risk-gates.sh new file mode 100755 index 0000000..502076f --- /dev/null +++ b/control/v1/evaluate-risk-gates.sh @@ -0,0 +1,370 @@ +#!/bin/bash +# shellcheck disable=SC2016 +set -uo pipefail +export LC_ALL=C +umask 077 + +emit_error() { + case "${1:-}" in + E_USAGE|E_RUNTIME|E_LIMIT|E_RELATION|E_DUTY) + /usr/bin/printf '%s\n' "$1" >&2 + ;; + *) /usr/bin/printf '%s\n' E_RUNTIME >&2 ;; + esac + exit 1 +} + +[ "$#" -eq 7 ] && [ "$1" = evaluate ] || emit_error E_USAGE +shift +source_path=${BASH_SOURCE[0]} +case "$source_path" in /*) ;; *) source_path="$(pwd -P)/$source_path" ;; esac +source_dir=$(CDPATH='' cd -P -- "${source_path%/*}" 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +source_path="$source_dir/${source_path##*/}" +[ "$source_path" = "$source_dir/evaluate-risk-gates.sh" ] || emit_error E_RUNTIME +repo=$(CDPATH='' cd -P -- "$source_dir/../.." 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +policy="$source_dir/risk-gates-policy.json" +decision="$source_dir/risk-gates-decision.json" +program="$source_dir/risk-gates.jq" +duty_policy="$source_dir/duty-separation-policy.json" +duty_decision="$source_dir/duty-separation-decision.json" +duty_program="$source_dir/duty-separation.jq" +duty_driver="$source_dir/evaluate-duty.sh" +policy_validator="$source_dir/validate.sh" +validator_program="$source_dir/policy-set.jq" +core_driver="$repo/scripts/core-contract.sh" +for required in "$source_path" "$policy" "$decision" "$program" "$duty_policy" \ + "$duty_decision" "$duty_program" "$duty_driver" "$policy_validator" \ + "$validator_program" "$core_driver"; do + [ -f "$required" ] && [ ! -L "$required" ] || emit_error E_RUNTIME +done +for input in "$@"; do + [ -f "$input" ] && [ ! -L "$input" ] || emit_error E_RUNTIME +done +jq_bin=$(command -v jq 2>/dev/null) || emit_error E_RUNTIME +case "$jq_bin" in /*) ;; *) emit_error E_RUNTIME ;; esac +[ -f "$jq_bin" ] && [ -x "$jq_bin" ] && [ ! -L "$jq_bin" ] && + [ "$($jq_bin --version 2>/dev/null)" = jq-1.6 ] || emit_error E_RUNTIME + +sha256_path() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +sha256_text() { + /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}' +} +selected_core_generation() { + local wrapper=$1 selected assignment_count + assignment_count=$(/usr/bin/grep -Ec \ + '^[[:space:]]*PORTABLE_CORE_GENERATION=' "$wrapper") || return 1 + [ "$assignment_count" -eq 1 ] || return 1 + selected=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_GENERATION='\(g-[0-9a-f]\{64\}\)'$/\1/p" \ + "$wrapper") || return 1 + [[ "$selected" =~ ^g-[0-9a-f]{64}$ ]] || return 1 + /usr/bin/printf '%s\n' "$selected" +} + +scratch=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-risk.XXXXXX" 2>/dev/null) || + emit_error E_RUNTIME +scratch=$(CDPATH='' cd -P -- "$scratch" 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +cleanup() { /bin/rm -rf -- "$scratch" >/dev/null 2>&1 || :; } +signal_exit() { trap - EXIT HUP INT TERM; cleanup; exit 1; } +trap cleanup EXIT +trap signal_exit HUP INT TERM + +snapshot_fixed() { + local source=$1 target=$2 size + /bin/dd if="$source" of="$target" bs=1048577 count=1 2>/dev/null || + emit_error E_RUNTIME + size=$(/usr/bin/wc -c <"$target" | /usr/bin/tr -d ' ') || emit_error E_RUNTIME + [ "$size" -le 1048576 ] || emit_error E_LIMIT +} +canonical_json() { + local input=$1 canonical=$2 + "$jq_bin" -s -S -c 'if length==1 then .[0] else error("root-count") end' \ + "$input" >"$canonical" 2>/dev/null || return 1 + /usr/bin/cmp -s "$input" "$canonical" +} +runtime_paths() { + local selected=$1 + /usr/bin/printf '%s\n' \ + control/v1/duty-separation-policy.json \ + control/v1/duty-separation-decision.json \ + control/v1/duty-separation.jq \ + control/v1/evaluate-duty.sh \ + control/v1/policy-set.jq \ + control/v1/validate.sh \ + scripts/core-contract.sh \ + core/v2/generation-registry.json \ + "core/v2/generations/$selected/contracts.jq" \ + "core/v2/generations/$selected/core-ingress.sh" \ + "core/v2/generations/$selected/modules/profile_graph.jq" \ + "core/v2/generations/$selected/modules/result_facts.jq" \ + "core/v2/generations/$selected/modules/result_truth.jq" \ + "core/v2/generations/$selected/modules/schema.jq" \ + "core/v2/generations/$selected/modules/stage_request.jq" +} +runtime_closure_sha() { + local root=$1 selected=$2 tag=$3 relative file digest descriptor members + local required_dir physical selected_at_root + for required_dir in "$root" "$root/control" "$root/control/v1" "$root/scripts" \ + "$root/core" "$root/core/v2" "$root/core/v2/generations" \ + "$root/core/v2/generations/$selected" \ + "$root/core/v2/generations/$selected/modules"; do + [ -d "$required_dir" ] && [ ! -L "$required_dir" ] || return 1 + physical=$(CDPATH='' cd -P -- "$required_dir" 2>/dev/null && pwd -P) || return 1 + [ "$physical" = "$required_dir" ] || return 1 + done + selected_at_root=$(selected_core_generation "$root/scripts/core-contract.sh") || + return 1 + [ "$selected_at_root" = "$selected" ] || return 1 + members="$scratch/runtime-$tag.tsv" + : >"$members" || return 1 + while IFS= read -r relative; do + file="$root/$relative" + [ -f "$file" ] && [ ! -L "$file" ] || return 1 + digest=$(sha256_path "$file") || return 1 + /usr/bin/printf '%s\t%s\n' "$relative" "$digest" >>"$members" || return 1 + done < <(runtime_paths "$selected") + descriptor=$("$jq_bin" -Rn -S -c \ + --arg generation_sha "$(sha256_text "$selected")" ' + [inputs|split("\t")|{path:.[0],sha256:.[1]}] as $members | + {schema_version:1,kind:"duty_separation_runtime_closure", + selected_generation_id_sha256:$generation_sha,members:$members} + ' <"$members") || return 1 + sha256_text "$descriptor" +} +build_runtime_mirror() { + local selected=$1 mirror="$scratch/runtime" relative source target size + /bin/mkdir -p "$mirror/control/v1" "$mirror/scripts" \ + "$mirror/core/v2/generations/$selected/modules" || return 1 + while IFS= read -r relative; do + source="$repo/$relative" + target="$mirror/$relative" + /bin/dd if="$source" of="$target" bs=1048577 count=1 2>/dev/null || return 1 + size=$(/usr/bin/wc -c <"$target" | /usr/bin/tr -d ' ') || return 1 + [ "$size" -le 1048576 ] || return 1 + done < <(runtime_paths "$selected") + /bin/chmod 0500 "$mirror/control/v1/evaluate-duty.sh" \ + "$mirror/control/v1/validate.sh" "$mirror/scripts/core-contract.sh" || return 1 + /usr/bin/printf '%s\n' "$mirror" +} +fixed_files_ok() { + [ "$(sha256_path "$source_path")" = "$driver_sha" ] && + [ "$(sha256_path "$program")" = "$program_sha" ] && + [ "$(sha256_path "$policy")" = "$policy_sha" ] && + [ "$(sha256_path "$decision")" = "$decision_sha" ] +} + +names=(policy-set request resolved result duty-evaluation claim) +index=0 +for input in "$@"; do + snapshot_fixed "$input" "$scratch/${names[$index]}.json" + canonical_json "$scratch/${names[$index]}.json" \ + "$scratch/${names[$index]}.canonical" || emit_error E_RELATION + index=$((index + 1)) +done +snapshot_fixed "$policy" "$scratch/policy.json" +snapshot_fixed "$decision" "$scratch/decision.json" +snapshot_fixed "$program" "$scratch/program.jq" +canonical_json "$scratch/policy.json" "$scratch/policy.canonical" || + emit_error E_RELATION +canonical_json "$scratch/decision.json" "$scratch/decision.canonical" || + emit_error E_RELATION +for control_dir in "$repo/control" "$source_dir"; do + [ -d "$control_dir" ] && [ ! -L "$control_dir" ] || emit_error E_RELATION +done +[ "$source_dir" = "$repo/control/v1" ] || emit_error E_RELATION + +driver_sha=$(sha256_path "$source_path") || emit_error E_RUNTIME +program_sha=$(sha256_path "$scratch/program.jq") || emit_error E_RUNTIME +policy_sha=$(sha256_path "$scratch/policy.json") || emit_error E_RUNTIME +decision_sha=$(sha256_path "$scratch/decision.json") || emit_error E_RUNTIME +duty_policy_sha=$(sha256_path "$duty_policy") || emit_error E_RUNTIME +duty_decision_sha=$(sha256_path "$duty_decision") || emit_error E_RUNTIME +duty_driver_sha=$(sha256_path "$duty_driver") || emit_error E_RUNTIME +duty_program_sha=$(sha256_path "$duty_program") || emit_error E_RUNTIME +validator_driver_sha=$(sha256_path "$policy_validator") || emit_error E_RUNTIME +validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RUNTIME + +"$jq_bin" -e --arg policy_sha "$policy_sha" --arg driver_sha "$driver_sha" \ + --arg program_sha "$program_sha" --arg duty_policy_sha "$duty_policy_sha" \ + --arg duty_decision_sha "$duty_decision_sha" \ + --slurpfile policy "$scratch/policy.json" \ + --slurpfile definition "$scratch/decision.json" ' + $definition[0] == { + schema_version:1,kind:"risk_gates_decision", + id:"control-decision.risk-gates", + body:{activation_state:"inactive",decision:"allow-observation-only-evaluation", + dependencies:{duty_separation:{ + policy_ref:{content_id:"control-policy.duty-separation", + media_type:"application/vnd.ystack.control-policy+json",sha256:$duty_policy_sha}, + decision_ref:{content_id:"control-decision.duty-separation", + media_type:"application/vnd.ystack.control-decision+json",sha256:$duty_decision_sha}}}, + evaluator:{ + driver_ref:{content_id:"control-evaluator-driver.risk-gates.v1", + media_type:"text/x-shellscript",sha256:$driver_sha}, + program_ref:{content_id:"control-evaluator-program.risk-gates.v1", + media_type:"text/x-jq",sha256:$program_sha}}, + fail_mode:"closed", + policy_ref:{content_id:$policy[0].id, + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + semantics:{authority_effect:"none", + decision_claim_semantics:"immutable-input-claim-only", + input_contract:"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1", + output_kind:"risk_gate_evaluation",output_schema_version:1, + reference_semantics:"identity-only", + verdicts:["inconclusive","satisfied","violated"]}} + } +' >/dev/null 2>&1 || emit_error E_RELATION + +"$jq_bin" -e --arg duty_driver_sha "$duty_driver_sha" \ + --arg duty_program_sha "$duty_program_sha" \ + --arg validator_driver_sha "$validator_driver_sha" \ + --arg validator_program_sha "$validator_program_sha" ' + .body.evaluator == { + driver_ref:{content_id:"control-evaluator-driver.duty-separation.v1", + media_type:"text/x-shellscript",sha256:$duty_driver_sha}, + policy_set_validator:{ + driver_ref:{content_id:"control-policy-set-validator-driver.v1", + media_type:"text/x-shellscript",sha256:$validator_driver_sha}, + program_ref:{content_id:"control-policy-set-validator-program.v1", + media_type:"text/x-jq",sha256:$validator_program_sha}}, + program_ref:{content_id:"control-evaluator-program.duty-separation.v1", + media_type:"text/x-jq",sha256:$duty_program_sha}} +' "$duty_decision" >/dev/null 2>&1 || emit_error E_RELATION + +selected=$(selected_core_generation "$core_driver") || emit_error E_RELATION +live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-pre) || + emit_error E_RELATION +mirror_root=$(build_runtime_mirror "$selected") || emit_error E_RELATION +mirror_runtime_sha=$(runtime_closure_sha "$mirror_root" "$selected" mirror-pre) || + emit_error E_RELATION +[ "$live_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION + +: >"$scratch/duty-ready" +duty_status=0 +PATH="${jq_bin%/*}:/usr/bin:/bin" "$mirror_root/control/v1/evaluate-duty.sh" evaluate \ + "$scratch/policy-set.json" "$scratch/request.json" "$scratch/resolved.json" \ + "$scratch/result.json" >"$scratch/generated-duty.json" \ + 2>"$scratch/generated-duty.err" || duty_status=$? +: >"$scratch/duty-complete" +post_live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-post-duty) || + emit_error E_RELATION +post_mirror_runtime_sha=$(runtime_closure_sha \ + "$mirror_root" "$selected" mirror-post-duty) || emit_error E_RELATION +[ "$post_live_runtime_sha" = "$live_runtime_sha" ] && + [ "$post_mirror_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION +[ "$duty_status" -eq 0 ] || emit_error E_DUTY +/usr/bin/cmp -s "$scratch/generated-duty.json" "$scratch/duty-evaluation.json" || + emit_error E_DUTY + +policy_set_sha=$(sha256_path "$scratch/policy-set.json") || emit_error E_RUNTIME +request_sha=$(sha256_path "$scratch/request.json") || emit_error E_RUNTIME +resolved_sha=$(sha256_path "$scratch/resolved.json") || emit_error E_RUNTIME +result_sha=$(sha256_path "$scratch/result.json") || emit_error E_RUNTIME +duty_sha=$(sha256_path "$scratch/duty-evaluation.json") || emit_error E_RUNTIME +claim_sha=$(sha256_path "$scratch/claim.json") || emit_error E_RUNTIME +request_basis=$("$jq_bin" -S -c \ + '{schema_version,kind,id,body:(.body|del(.gate_decision_refs))}' \ + "$scratch/request.json") || emit_error E_RELATION +request_basis_sha=$(sha256_text "$request_basis") || emit_error E_RUNTIME +policy_scope_descriptor=$("$jq_bin" -S -c -n --arg policy_sha "$policy_sha" \ + --slurpfile request "$scratch/request.json" ' + {schema_version:1,kind:"risk_policy_scope", + policy_ref:{content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + subject_ref:{type:"artifact",value:$request[0].body.source.value}} +') || emit_error E_RELATION +policy_scope_sha=$(sha256_text "$policy_scope_descriptor") || emit_error E_RUNTIME +declared_tier=$("$jq_bin" -er '.body.risk.tier.name' "$scratch/request.json") || + emit_error E_RELATION +requirement_descriptor=$("$jq_bin" -S -c -n --arg policy_sha "$policy_sha" \ + --arg tier "$declared_tier" --slurpfile request "$scratch/request.json" ' + {schema_version:1,kind:"risk_gate_requirement",declared_tier:$tier, + policy_ref:{content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + subject_ref:{type:"artifact",value:$request[0].body.source.value}} +') || emit_error E_RELATION +requirement_scope_sha=$(sha256_text "$requirement_descriptor") || emit_error E_RUNTIME + +: >"$scratch/risk-ready" +"$jq_bin" -S -c -n -f "$scratch/program.jq" \ + --slurpfile policy "$scratch/policy.json" \ + --slurpfile decision "$scratch/decision.json" \ + --slurpfile policy_set "$scratch/policy-set.json" \ + --slurpfile request "$scratch/request.json" \ + --slurpfile resolved "$scratch/resolved.json" \ + --slurpfile result "$scratch/result.json" \ + --slurpfile duty_evaluation "$scratch/duty-evaluation.json" \ + --slurpfile claim "$scratch/claim.json" \ + --arg policy_sha "$policy_sha" --arg decision_sha "$decision_sha" \ + --arg policy_set_sha "$policy_set_sha" --arg request_sha "$request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$result_sha" \ + --arg duty_sha "$duty_sha" --arg claim_sha "$claim_sha" \ + --arg request_basis_sha "$request_basis_sha" \ + --arg policy_scope_sha "$policy_scope_sha" \ + --arg requirement_scope_sha "$requirement_scope_sha" \ + >"$scratch/evaluation.json" 2>/dev/null || emit_error E_RUNTIME +: >"$scratch/risk-complete" +fixed_files_ok || emit_error E_RELATION +final_live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-post-risk) || + emit_error E_RELATION +final_mirror_runtime_sha=$(runtime_closure_sha \ + "$mirror_root" "$selected" mirror-post-risk) || emit_error E_RELATION +[ "$final_live_runtime_sha" = "$live_runtime_sha" ] && + [ "$final_mirror_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION +canonical_json "$scratch/evaluation.json" "$scratch/evaluation.canonical" || + emit_error E_RUNTIME + +"$jq_bin" -e --arg policy_sha "$policy_sha" --arg decision_sha "$decision_sha" \ + --arg policy_set_sha "$policy_set_sha" --arg request_sha "$request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$result_sha" \ + --arg duty_sha "$duty_sha" --arg claim_sha "$claim_sha" \ + --slurpfile request "$scratch/request.json" \ + --slurpfile resolved "$scratch/resolved.json" \ + --slurpfile result "$scratch/result.json" \ + --slurpfile policy_set "$scratch/policy-set.json" \ + --slurpfile claim "$scratch/claim.json" \ + --slurpfile duty "$scratch/duty-evaluation.json" ' + (keys|sort)==["body","id","kind","schema_version"] and + .schema_version==1 and .kind=="risk_gate_evaluation" and .id==$result[0].id and + (.body|keys|sort)==["activation_state","authority_effect","classification", + "core_contract","decision_claim_ref","decision_ref","duty_evaluation_ref", + "evaluation_mode","policy_ref","policy_set","reason_ids", + "reference_semantics","stage","verdict"] and + .body.activation_state=="inactive" and .body.authority_effect=="none" and + .body.evaluation_mode=="observation-only" and + .body.reference_semantics=="identity-only" and + .body.policy_set=={id:$policy_set[0].id,sha256:$policy_set_sha} and + .body.policy_ref=={content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha} and + .body.decision_ref=={content_id:"control-decision.risk-gates", + media_type:"application/vnd.ystack.control-decision+json",sha256:$decision_sha} and + .body.decision_claim_ref=={content_id:$claim[0].id, + media_type:"application/vnd.ystack.risk-gate-decision-claim+json",sha256:$claim_sha} and + .body.duty_evaluation_ref=={content_id:$duty[0].id, + media_type:"application/vnd.ystack.duty-separation-evaluation+json",sha256:$duty_sha} and + .body.core_contract==$policy_set[0].body.core_contract and + .body.stage=={ + request_ref:{schema_version:$request[0].schema_version,kind:$request[0].kind, + id:$request[0].id,sha256:$request_sha}, + resolved_profile_ref:{schema_version:$resolved[0].schema_version, + kind:$resolved[0].kind,id:$resolved[0].id,sha256:$resolved_sha}, + result_ref:{schema_version:$result[0].schema_version,kind:$result[0].kind, + id:$result[0].id,sha256:$result_sha}} and + (.body.classification|keys|sort)==["declared_tier","minimum_tier"] and + (.body.classification.minimum_tier as $minimum | + ["routine","high","bootstrap"]|index($minimum)!=null) and + (.body.verdict=="satisfied" or .body.verdict=="violated" or + .body.verdict=="inconclusive") and + (.body.reason_ids|type=="array" and length>=1 and all(.[];type=="string") and + .==(sort|unique)) and + (if .body.verdict=="satisfied" then .body.reason_ids==["risk-gates.satisfied"] + else (.body.reason_ids|index("risk-gates.satisfied")==null) end) and + ((.body|has("grant_ref") or has("qualification_ref") or has("activation"))|not) +' "$scratch/evaluation.json" >/dev/null 2>&1 || emit_error E_RUNTIME + +/bin/cat "$scratch/evaluation.json" || emit_error E_RUNTIME +trap - EXIT HUP INT TERM +cleanup diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json new file mode 100644 index 0000000..7323b6f --- /dev/null +++ b/control/v1/risk-gates-decision.json @@ -0,0 +1 @@ +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"54f912d801bde9ca084d4944eb431dc7afdc38c369e689dfdbae86c56ecd52ef"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"49be84411033bb3bee54cdb194cf099206a6a34623a5e16d33b24c5dff65fead"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"095fff49a29a48f1701e2e0031278bfdb5b219573ddbe42526ffcd995d278e0e"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","satisfied","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/control/v1/risk-gates-policy.json b/control/v1/risk-gates-policy.json new file mode 100644 index 0000000..fd75560 --- /dev/null +++ b/control/v1/risk-gates-policy.json @@ -0,0 +1 @@ +{"body":{"activation_state":"inactive","core_contract":{"generation_id_sha256":"6f6acbbd0cf40ab3c913328d6c0070635424ffe920bcdb900fbd0718345d7137","package_ref":{"content_id":"core-contract-package.v2","media_type":"application/vnd.ystack.core-contract+json","sha256":"005431c5c7e3a39dc3ab75dfcafd0f09359331667fdcacb140514a4384592716"},"semantic_identity":"core.contracts.v2"},"decision_claim_semantics":"immutable-input-claim-only","duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"evaluation_kind":"duty_separation_evaluation","policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"},"required_verdict":"satisfied"},"evaluation_mode":"observation-only","fail_mode":"closed","forced_high_reason_ids":["risk.broad-architecture","risk.constitution","risk.deployment","risk.identity-auth","risk.migration","risk.production-infrastructure","risk.security-control","risk.workflow"],"policy_version":"v1","tier_rules":[{"classification":"bootstrap","decision_kind":"operator-bootstrap-approval","decision_role":"operator","declared_tier":"bootstrap"},{"classification":"high","decision_kind":"operator-plan-approval","decision_role":"operator","declared_tier":"high"},{"classification":"routine","decision_kind":"operator-plan-approval","decision_role":"operator","declared_tier":"high"},{"classification":"routine","decision_kind":"independent-plan-check","decision_role":"reviewer","declared_tier":"routine"}]},"id":"control-policy.risk-gates","kind":"risk_gates_policy","schema_version":1} diff --git a/control/v1/risk-gates.jq b/control/v1/risk-gates.jq new file mode 100644 index 0000000..197b3d2 --- /dev/null +++ b/control/v1/risk-gates.jq @@ -0,0 +1,226 @@ +def exact($fields): + type == "object" and (keys | sort) == ($fields | sort); + +def id_ok: + type == "string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z"); + +def sha256_ok: + type == "string" and test("\\A[0-9a-f]{64}\\z"); + +def time_ok: + type == "string" and + test("\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\z"); + +def actor_ok: + exact(["adapter_instance_id","execution_boundary_id","implementation_id", + "implementation_version","principal_id","role"]) and + (.role | id_ok) and (.implementation_id | id_ok) and + (.implementation_version | id_ok) and (.adapter_instance_id | id_ok) and + (.principal_id | id_ok) and (.execution_boundary_id | id_ok); + +def reason_set_ok: + type == "array" and length >= 1 and length <= 256 and + all(.[];id_ok) and length == (unique | length) and . == sort; + +def content_ref($id;$media;$sha): + {content_id:$id,media_type:$media,sha256:$sha}; + +def document_ref($document;$digest): + {schema_version:$document.schema_version,kind:$document.kind, + id:$document.id,sha256:$digest}; + +def expected_core: + {semantic_identity:"core.contracts.v2", + generation_id_sha256: + "6f6acbbd0cf40ab3c913328d6c0070635424ffe920bcdb900fbd0718345d7137", + package_ref:{content_id:"core-contract-package.v2", + media_type:"application/vnd.ystack.core-contract+json", + sha256:"005431c5c7e3a39dc3ab75dfcafd0f09359331667fdcacb140514a4384592716"}}; + +def expected_tier_rules: + [ + {classification:"bootstrap",declared_tier:"bootstrap", + decision_kind:"operator-bootstrap-approval",decision_role:"operator"}, + {classification:"high",declared_tier:"high", + decision_kind:"operator-plan-approval",decision_role:"operator"}, + {classification:"routine",declared_tier:"high", + decision_kind:"operator-plan-approval",decision_role:"operator"}, + {classification:"routine",declared_tier:"routine", + decision_kind:"independent-plan-check",decision_role:"reviewer"} + ]; + +def expected_high_reasons: + ["risk.broad-architecture","risk.constitution","risk.deployment", + "risk.identity-auth","risk.migration","risk.production-infrastructure", + "risk.security-control","risk.workflow"]; + +def policy_ok: + exact(["body","id","kind","schema_version"]) and .schema_version == 1 and + .kind == "risk_gates_policy" and .id == "control-policy.risk-gates" and + (.body | + exact(["activation_state","core_contract","decision_claim_semantics", + "duty_separation","evaluation_mode","fail_mode","forced_high_reason_ids", + "policy_version","tier_rules"]) and + .activation_state == "inactive" and + .core_contract == expected_core and + .decision_claim_semantics == "immutable-input-claim-only" and + .evaluation_mode == "observation-only" and .fail_mode == "closed" and + .policy_version == "v1" and .forced_high_reason_ids == expected_high_reasons and + .tier_rules == expected_tier_rules and + .duty_separation == { + decision_ref:{content_id:"control-decision.duty-separation", + media_type:"application/vnd.ystack.control-decision+json", + sha256:"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"}, + evaluation_kind:"duty_separation_evaluation", + policy_ref:{content_id:"control-policy.duty-separation", + media_type:"application/vnd.ystack.control-policy+json", + sha256:"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}, + required_verdict:"satisfied"}); + +def claim_shape_ok: + exact(["body","id","kind","schema_version"]) and .schema_version == 1 and + .kind == "risk_gate_decision_claim" and (.id | id_ok) and + (.body | + exact(["activation_state","classification","decision","policy_ref", + "request_basis_sha256","required_gate_refs"]) and + .activation_state == "inactive" and + (.request_basis_sha256 | sha256_ok) and + (.classification | + exact(["reason_ids","tier"]) and + (.tier == "routine" or .tier == "high" or .tier == "bootstrap") and + (.reason_ids | reason_set_ok)) and + (.policy_ref | type == "object") and + (.required_gate_refs | type == "array") and + (.decision | + (exact(["state"]) and .state == "absent") or + (exact(["state","value"]) and .state == "present" and + (.value | + exact(["asserted_decision","decided_by","decision_kind","recorded_at"]) and + (.asserted_decision == "accept" or .asserted_decision == "reject") and + (.decided_by | actor_ok) and (.decision_kind | id_ok) and + (.recorded_at | time_ok))))); + +($policy[0]) as $p | +($decision[0]) as $definition | +($policy_set[0]) as $set | +($request[0]) as $request_doc | +($resolved[0]) as $resolved_doc | +($result[0]) as $result_doc | +($duty_evaluation[0]) as $duty | +($claim[0]) as $claim_doc | +(if ($p | policy_ok) then true else error("invalid shipped risk policy") end) | +(if ($claim_doc | claim_shape_ok) then true else error("invalid risk decision claim") end) | + +content_ref($p.id;"application/vnd.ystack.control-policy+json";$policy_sha) as + $risk_policy_ref | +content_ref($definition.id;"application/vnd.ystack.control-decision+json"; + $decision_sha) as $risk_definition_ref | +{type:"artifact",value:$request_doc.body.source.value} as $subject_ref | +{purpose:"policy",decision_record_ref:$risk_policy_ref,subject_ref:$subject_ref, + scope_sha256:$policy_scope_sha} as $expected_policy_scope | +{purpose:"gate-requirement",decision_record_ref:$risk_policy_ref, + subject_ref:$subject_ref,scope_sha256:$requirement_scope_sha} as + $expected_requirement_scope | +content_ref($claim_doc.id; + "application/vnd.ystack.risk-gate-decision-claim+json";$claim_sha) as + $claim_content_ref | +{purpose:"gate-decision",decision_record_ref:$claim_content_ref, + subject_ref:$subject_ref,scope_sha256:$request_basis_sha} as + $expected_gate_decision | + +([$set.body.sections[] | select(.section_id == "risk-gates")]) as $risk_sections | +([$set.body.sections[] | select(.section_id == "duty-separation")]) as $duty_sections | +(if ($risk_sections | length) == 1 and + $risk_sections[0].policy_ref == $risk_policy_ref and + $risk_sections[0].decision_ref == $risk_definition_ref and + ($duty_sections | length) == 1 and + $duty_sections[0].policy_ref == $p.body.duty_separation.policy_ref and + $duty_sections[0].decision_ref == $p.body.duty_separation.decision_ref + then true else error("invalid policy-set section bindings") end) | + +($request_doc.body.risk.tier.namespace == "core" and + ($request_doc.body.risk.tier.name == "routine" or + $request_doc.body.risk.tier.name == "high" or + $request_doc.body.risk.tier.name == "bootstrap")) as $core_tier_supported | +($request_doc.body.risk.tier.name) as $declared_tier | +(if $request_doc.body.target_revision.state == "absent" or + $request_doc.body.base.state == "absent" then "bootstrap" + elif any($request_doc.body.risk.reason_ids[]; + . as $reason | $p.body.forced_high_reason_ids | index($reason) != null) + then "high" + else $claim_doc.body.classification.tier + end) as $minimum_tier | +([$p.body.tier_rules[] | + select(.classification == $minimum_tier and .declared_tier == $declared_tier)]) as + $matching_rules | +($matching_rules[0] // null) as $matching_rule | +([$request_doc.body.gate_decision_refs[] | + select(.decision_record_ref.media_type == + "application/vnd.ystack.risk-gate-decision-claim+json")]) as + $risk_decision_refs | + +((if $request_doc.body.risk.policy_ref == $expected_policy_scope then [] + else ["risk.policy-ref-mismatch"] end) + + (if $request_doc.body.risk.required_gate_refs == [$expected_requirement_scope] then [] + else ["risk.requirement-ref-mismatch"] end) + + (if $claim_doc.body.policy_ref == $request_doc.body.risk.policy_ref and + $claim_doc.body.required_gate_refs == $request_doc.body.risk.required_gate_refs + then [] else ["decision.claim-context-mismatch"] end) + + (if $claim_doc.body.request_basis_sha256 == $request_basis_sha then [] + else ["decision.stale"] end) + + (if ($risk_decision_refs | length) == 1 and + $risk_decision_refs[0] == $expected_gate_decision then [] + elif ($risk_decision_refs | length) > 1 then ["decision.ambiguous"] + else ["decision.unbound"] end) + + (if $claim_doc.body.classification.reason_ids == $request_doc.body.risk.reason_ids + then [] else ["classification.reasons-mismatch"] end) + + (if $minimum_tier == $claim_doc.body.classification.tier then [] + else ["classification.forced-tier-mismatch"] end) + + (if $core_tier_supported and ($matching_rules | length) == 1 then [] + elif $core_tier_supported then ["risk.tier-downgrade"] + else [] end) + + (if $duty.body.verdict == "violated" then ["duty.violated"] else [] end) + + (if $claim_doc.body.decision.state == "absent" then ["decision.missing"] + elif $claim_doc.body.decision.value.asserted_decision == "reject" + then ["decision.rejected"] + else [] end) + + (if $claim_doc.body.decision.state == "present" and $matching_rule != null then + (if $claim_doc.body.decision.value.decision_kind == $matching_rule.decision_kind + then [] else ["decision.kind-denied"] end) + + (if $claim_doc.body.decision.value.decided_by.role == $matching_rule.decision_role + then [] else ["decision.role-denied"] end) + + (if $claim_doc.body.decision.value.recorded_at <= $request_doc.body.requested_at + then [] else ["decision.after-request"] end) + else [] end) | sort | unique) as $violations | + +((if $core_tier_supported then [] else ["risk.tier-unsupported"] end) + + (if $duty.body.verdict == "inconclusive" then ["duty.inconclusive"] else [] end) | + sort | unique) as $unknowns | +(if ($violations | length) > 0 then {verdict:"violated",reasons:$violations} + elif ($unknowns | length) > 0 then {verdict:"inconclusive",reasons:$unknowns} + else {verdict:"satisfied",reasons:["risk-gates.satisfied"]} end) as $result | + +{ + schema_version:1, + kind:"risk_gate_evaluation", + id:$result_doc.id, + body:{ + activation_state:"inactive", + authority_effect:"none", + classification:{declared_tier:$declared_tier,minimum_tier:$minimum_tier}, + core_contract:$set.body.core_contract, + decision_claim_ref:$claim_content_ref, + decision_ref:$risk_definition_ref, + duty_evaluation_ref:content_ref($duty.id; + "application/vnd.ystack.duty-separation-evaluation+json";$duty_sha), + evaluation_mode:"observation-only", + policy_ref:$risk_policy_ref, + policy_set:{id:$set.id,sha256:$policy_set_sha}, + reason_ids:$result.reasons, + reference_semantics:"identity-only", + stage:{request_ref:document_ref($request_doc;$request_sha), + resolved_profile_ref:document_ref($resolved_doc;$resolved_sha), + result_ref:document_ref($result_doc;$result_sha)}, + verdict:$result.verdict + } +} diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh new file mode 100755 index 0000000..2b36913 --- /dev/null +++ b/scripts/test/control-risk-gates.test.sh @@ -0,0 +1,518 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +if [ "${YSTACK_RISK_TEST_BOUNDED:-0}" != 1 ]; then + YSTACK_RISK_TEST_BOUNDED=1 exec /usr/bin/perl -e 'alarm 240; exec @ARGV' "$0" +fi + +root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +evaluator="$root/control/v1/evaluate-risk-gates.sh" +duty_evaluator="$root/control/v1/evaluate-duty.sh" +policy="$root/control/v1/risk-gates-policy.json" +definition="$root/control/v1/risk-gates-decision.json" +duty_policy="$root/control/v1/duty-separation-policy.json" +duty_definition="$root/control/v1/duty-separation-decision.json" +core_wrapper="$root/scripts/core-contract.sh" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-risk-test.XXXXXX") +cleanup() { /bin/rm -rf -- "$tmp"; } +trap cleanup EXIT HUP INT TERM +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +passes=0 +pass() { passes=$((passes + 1)); /usr/bin/printf 'ok %s - %s\n' "$passes" "$1"; } +sha256_path() { /usr/bin/shasum -a 256 "$1" | /usr/bin/awk '{print $1}'; } +sha256_text() { + /usr/bin/printf '%s' "$1" | /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}' +} + +platform=$(/usr/bin/uname -s):$(/usr/bin/uname -m) +case "$platform" in + Darwin:*) + jq_asset=jq-osx-amd64 + jq_sha=5c0a0a3ea600f302ee458b30317425dd9632d1ad8882259fcaf4e9b868b2b1ef + ;; + Linux:x86_64) + jq_asset=jq-linux64 + jq_sha=af986793a515d500ab2d35f8d2aecd656e764504b789b66d7e1a0b727a124c44 + ;; + *) fail "unsupported host $platform" ;; +esac +jq_cache_dir="${TMPDIR:-/tmp}/ystack-portable-core-jq16" +/bin/mkdir -p "$jq_cache_dir" +jq_cache="$jq_cache_dir/$jq_asset" +if [ ! -f "$jq_cache" ] || [ "$(sha256_path "$jq_cache")" != "$jq_sha" ]; then + download=$(/usr/bin/mktemp "$jq_cache_dir/.jq-1.6.XXXXXX") + /usr/bin/curl --proto '=https' --tlsv1.2 -fsSL \ + "https://github.com/jqlang/jq/releases/download/jq-1.6/$jq_asset" -o "$download" + [ "$(sha256_path "$download")" = "$jq_sha" ] || fail 'jq release digest' + /bin/chmod 0555 "$download" + /bin/mv "$download" "$jq_cache" +fi +bin="$tmp/bin" +/bin/mkdir -m 700 "$bin" +/bin/cp "$jq_cache" "$bin/jq" +/bin/chmod 0555 "$bin/jq" +jq_bin="$bin/jq" +[ "$($jq_bin --version)" = jq-1.6 ] || fail 'jq identity' + +generation=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_GENERATION='\(g-[0-9a-f]\{64\}\)'$/\1/p" "$core_wrapper") || + fail 'selected generation' +[[ "$generation" =~ ^g-[0-9a-f]{64}$ ]] || fail 'selected generation shape' +policy_sha=$(sha256_path "$policy") +definition_sha=$(sha256_path "$definition") +duty_policy_sha=$(sha256_path "$duty_policy") +duty_definition_sha=$(sha256_path "$duty_definition") +core_package_sha=$("$jq_bin" -er '.body.core_contract.package_ref.sha256' "$policy") + +for canonical_source in "$policy" "$definition"; do + "$jq_bin" -S -c . "$canonical_source" >"$tmp/canonical" + /usr/bin/cmp -s "$canonical_source" "$tmp/canonical" || + fail "canonical ${canonical_source##*/}" +done +for source_path in control/v1/risk-gates-policy.json \ + control/v1/risk-gates-decision.json control/v1/risk-gates.jq \ + control/v1/evaluate-risk-gates.sh scripts/test/control-risk-gates.test.sh; do + ! /usr/bin/grep -Fq "$generation" "$root/$source_path" || + fail "raw generation $source_path" +done +pass 'canonical shipped definitions and opaque generation identity' + +policy_set="$tmp/policy-set.json" +"$jq_bin" -S -c -n --arg risk_policy_sha "$policy_sha" \ + --arg risk_definition_sha "$definition_sha" --arg duty_policy_sha "$duty_policy_sha" \ + --arg duty_definition_sha "$duty_definition_sha" --arg generation "$generation" \ + --arg core_package_sha "$core_package_sha" ' + def ref($id;$media;$sha): {content_id:$id,media_type:$media,sha256:$sha}; + def section($id;$policy_sha;$decision_sha): + {section_id:$id, + policy_ref:ref("control-policy."+$id; + "application/vnd.ystack.control-policy+json";$policy_sha), + decision_ref:ref("control-decision."+$id; + "application/vnd.ystack.control-decision+json";$decision_sha)}; + {schema_version:1,kind:"control_policy_set",id:"control-policy-set.test", + body:{activation_state:"inactive",fail_mode:"closed",policy_version:"v1", + core_contract:{semantic_identity:"core.contracts.v2",generation_id:$generation, + package_ref:ref("core-contract-package.v2"; + "application/vnd.ystack.core-contract+json";$core_package_sha)}, + sections:[section("credential-policy";("1"*64);("a"*64)), + section("duty-separation";$duty_policy_sha;$duty_definition_sha), + section("evidence-integrity";("3"*64);("c"*64)), + section("kill-switch";("4"*64);("d"*64)), + section("risk-gates";$risk_policy_sha;$risk_definition_sha), + section("sandbox";("6"*64);("f"*64))]}} +' >"$policy_set" + +resolved="$tmp/resolved.json" +"$jq_bin" -L "$root/scripts/test" -S -c -n ' + import "portable-core-profile-graph-fixtures" as profile; + def v2: walk(if type == "object" and has("schema_version") + then .schema_version=2 else . end); + def forge_binding($sha): + {binding_id:"binding.forge",role:"forge", + manifest_ref:{schema_version:2,kind:"adapter_manifest",id:"manifest.forge",sha256:$sha}, + execution_kind:"deterministic",adapter_instance_id:"instance.forge", + principal_id:"principal.forge",execution_boundary_id:"boundary.forge", + authority_ref:profile::scope("authority";"authority-forge";profile::sha("5")), + package_ref:profile::blob("packages/forge.bin";"6"),skill_refs:[],requested_tools:[], + requested_capabilities:["core.forge.materialize-candidate.v2"], + requested_permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"]}; + {forge:("1"*64),producer:("2"*64),publisher:("3"*64), + reviewer:("4"*64),verifier:("5"*64)} as $shas | + (profile::profile_doc($shas) | v2 | .body.profile_version="v2" | + .body.bindings += [forge_binding($shas.forge)] | + .body.bindings |= sort_by(.binding_id)) as $profile | + profile::resolved_profile_doc($profile;("0"*64);$shas) | v2 | + .body.bindings |= map(if .binding.role == "forge" then + .adapter_implementation.version="v2" | + .manifest_source=profile::source_value( + profile::blob("manifests/forge.json";"a");"canonical-json";$shas.forge) + else . end) +' >"$resolved" +resolved_sha=$(sha256_path "$resolved") + +policy_scope() { + local request=$1 output=$2 descriptor digest + descriptor=$("$jq_bin" -S -c -n --arg policy_sha "$policy_sha" \ + --slurpfile request "$request" ' + {schema_version:1,kind:"risk_policy_scope", + policy_ref:{content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + subject_ref:{type:"artifact",value:$request[0].body.source.value}} + ') + digest=$(sha256_text "$descriptor") + "$jq_bin" -S -c -n --arg policy_sha "$policy_sha" --arg digest "$digest" \ + --slurpfile request "$request" ' + {purpose:"policy", + decision_record_ref:{content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + subject_ref:{type:"artifact",value:$request[0].body.source.value}, + scope_sha256:$digest} + ' >"$output" +} +requirement_scope() { + local request=$1 tier=$2 output=$3 descriptor digest + descriptor=$("$jq_bin" -S -c -n --arg policy_sha "$policy_sha" --arg tier "$tier" \ + --slurpfile request "$request" ' + {schema_version:1,kind:"risk_gate_requirement",declared_tier:$tier, + policy_ref:{content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + subject_ref:{type:"artifact",value:$request[0].body.source.value}} + ') + digest=$(sha256_text "$descriptor") + "$jq_bin" -S -c -n --arg policy_sha "$policy_sha" --arg digest "$digest" \ + --slurpfile request "$request" ' + {purpose:"gate-requirement", + decision_record_ref:{content_id:"control-policy.risk-gates", + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + subject_ref:{type:"artifact",value:$request[0].body.source.value}, + scope_sha256:$digest} + ' >"$output" +} + +build_case() { + local name=$1 declared=$2 classification=$3 decision_state=$4 decision_kind=$5 + local decision_role=$6 asserted=$7 reason=$8 mode=${9:-normal} + local namespace=${10:-core} + local dir="$tmp/$name" base_request="$tmp/$name.base-request" + local request_basis request_basis_sha claim_sha request_sha + /bin/mkdir -p "$dir" + "$jq_bin" -L "$root/scripts/test" -S -c -n --arg resolved_sha "$resolved_sha" ' + import "portable-core-stage-request-fixtures" as request; + request::request_doc("producer";$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) + ' >"$base_request" + if [ "$mode" = bootstrap ]; then + "$jq_bin" -S -c '.body.target_revision={state:"absent"} | + .body.base={state:"absent"}' "$base_request" >"$dir/request.seed" + elif [ "$mode" = duty-collision ]; then + "$jq_bin" -S -c '.body.requested_by.principal_id="principal.producer"' \ + "$base_request" >"$dir/request.seed" + else + /bin/cp "$base_request" "$dir/request.seed" + fi + "$jq_bin" -S -c --arg declared "$declared" --arg namespace "$namespace" \ + --arg reason "$reason" ' + .body.risk.tier={namespace:$namespace,name:$declared} | + .body.risk.reason_ids=[$reason] | + .body.gate_decision_refs=[] + ' "$dir/request.seed" >"$dir/request.typed" + policy_scope "$dir/request.typed" "$dir/policy-scope.json" + requirement_scope "$dir/request.typed" "$declared" "$dir/requirement-scope.json" + "$jq_bin" -S -c --slurpfile policy_ref "$dir/policy-scope.json" \ + --slurpfile requirement "$dir/requirement-scope.json" ' + .body.risk.policy_ref=$policy_ref[0] | + .body.risk.required_gate_refs=[$requirement[0]] + ' "$dir/request.typed" >"$dir/request.basis" + request_basis=$("$jq_bin" -S -c \ + '{schema_version,kind,id,body:(.body|del(.gate_decision_refs))}' "$dir/request.basis") + request_basis_sha=$(sha256_text "$request_basis") + if [ "$decision_state" = absent ]; then + decision_json='{"state":"absent"}' + else + decision_json=$("$jq_bin" -S -c -n --arg kind "$decision_kind" \ + --arg role "$decision_role" --arg asserted "$asserted" ' + {state:"present",value:{asserted_decision:$asserted,decision_kind:$kind, + decided_by:{role:$role,implementation_id:("implementation."+$role), + implementation_version:"v1",adapter_instance_id:("instance."+$role), + principal_id:("principal."+$role),execution_boundary_id:("boundary."+$role)}, + recorded_at:"2026-08-29T23:59:59Z"}} + ') + fi + "$jq_bin" -S -c -n --arg id "risk.claim.$name" \ + --arg classification "$classification" --arg reason "$reason" \ + --arg request_basis_sha "$request_basis_sha" --argjson decision "$decision_json" \ + --slurpfile policy_ref "$dir/policy-scope.json" \ + --slurpfile requirement "$dir/requirement-scope.json" ' + {schema_version:1,kind:"risk_gate_decision_claim",id:$id, + body:{activation_state:"inactive", + classification:{tier:$classification,reason_ids:[$reason]}, + decision:$decision,policy_ref:$policy_ref[0], + request_basis_sha256:$request_basis_sha, + required_gate_refs:[$requirement[0]]}} + ' >"$dir/claim.json" + claim_sha=$(sha256_path "$dir/claim.json") + "$jq_bin" -S -c --arg id "risk.claim.$name" --arg claim_sha "$claim_sha" \ + --arg request_basis_sha "$request_basis_sha" ' + .body.gate_decision_refs=[{purpose:"gate-decision", + decision_record_ref:{content_id:$id, + media_type:"application/vnd.ystack.risk-gate-decision-claim+json",sha256:$claim_sha}, + subject_ref:{type:"artifact",value:.body.source.value}, + scope_sha256:$request_basis_sha}] + ' "$dir/request.basis" >"$dir/request.json" + request_sha=$(sha256_path "$dir/request.json") + "$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$dir/request.json" --slurpfile resolved "$resolved" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) + ' >"$dir/result.json" + PATH="$bin:/usr/bin:/bin" "$duty_evaluator" evaluate "$policy_set" \ + "$dir/request.json" "$resolved" "$dir/result.json" >"$dir/duty.json" \ + 2>"$dir/duty.err" || { + /bin/cat "$dir/duty.err" >&2 + fail "build duty $name" + } +} + +expect_eval() { + local name=$1 verdict=$2 reason=$3 + local dir="$tmp/$name" out="$tmp/$name/risk.out" + PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$dir/request.json" \ + "$resolved" "$dir/result.json" "$dir/duty.json" "$dir/claim.json" \ + >"$out" 2>"$dir/risk.err" || { + /bin/cat "$dir/risk.err" >&2 + fail "$name status" + } + [ ! -s "$dir/risk.err" ] || fail "$name stderr" + "$jq_bin" -e --arg verdict "$verdict" --arg reason "$reason" ' + .kind=="risk_gate_evaluation" and .body.activation_state=="inactive" and + .body.authority_effect=="none" and .body.evaluation_mode=="observation-only" and + .body.reference_semantics=="identity-only" and .body.verdict==$verdict and + (.body.reason_ids|index($reason)!=null) and .body.reason_ids==(.body.reason_ids|sort|unique) and + ((.body|has("grant_ref") or has("qualification_ref") or has("activation"))|not) + ' "$out" >/dev/null || fail "$name verdict" + "$jq_bin" -S -c . "$out" >"$dir/repeat" + /usr/bin/cmp -s "$out" "$dir/repeat" || fail "$name canonical output" + pass "$name" +} + +expect_error() { + local name=$1 expected=$2 runtime=${3:-$root} + local dir="$tmp/$name" status=0 + PATH="$bin:/usr/bin:/bin" "$runtime/control/v1/evaluate-risk-gates.sh" evaluate \ + "$policy_set" "$dir/request.json" "$resolved" "$dir/result.json" \ + "$dir/duty.json" "$dir/claim.json" >"$dir/error.out" 2>"$dir/error.err" || + status=$? + [ "$status" -ne 0 ] && [ ! -s "$dir/error.out" ] && + [ "$(/bin/cat "$dir/error.err")" = "$expected" ] || fail "$name error" + pass "$name" +} + +build_case routine routine routine present independent-plan-check reviewer accept \ + risk.routine +expect_eval routine satisfied risk-gates.satisfied + +build_case routine-escalated high routine present operator-plan-approval operator accept \ + risk.routine +expect_eval routine-escalated satisfied risk-gates.satisfied + +build_case high high high present operator-plan-approval operator accept \ + risk.security-control +expect_eval high satisfied risk-gates.satisfied + +build_case bootstrap bootstrap bootstrap present operator-bootstrap-approval operator accept \ + risk.bootstrap bootstrap +expect_eval bootstrap satisfied risk-gates.satisfied + +build_case missing high high absent ignored operator accept risk.security-control +expect_eval missing violated decision.missing + +build_case rejected high high present operator-plan-approval operator reject \ + risk.security-control +expect_eval rejected violated decision.rejected + +build_case downgrade routine high present independent-plan-check reviewer accept \ + risk.security-control +expect_eval downgrade violated risk.tier-downgrade + +build_case invented-role high high present operator-plan-approval reviewer accept \ + risk.security-control +expect_eval invented-role violated decision.role-denied + +build_case invented-kind high high present independent-plan-check operator accept \ + risk.security-control +expect_eval invented-kind violated decision.kind-denied + +build_case after-request routine routine present independent-plan-check reviewer accept \ + risk.routine +"$jq_bin" -S -c '.body.decision.value.recorded_at="2026-09-01T00:00:01Z"' \ + "$tmp/after-request/claim.json" >"$tmp/after-request/claim.changed" +/bin/mv "$tmp/after-request/claim.changed" "$tmp/after-request/claim.json" +expect_eval after-request violated decision.after-request + +build_case stale routine routine present independent-plan-check reviewer accept risk.routine +"$jq_bin" -S -c '.body.request_basis_sha256=("0"*64)' "$tmp/stale/claim.json" \ + >"$tmp/stale/claim.changed" +/bin/mv "$tmp/stale/claim.changed" "$tmp/stale/claim.json" +expect_eval stale violated decision.stale + +build_case unbound routine routine present independent-plan-check reviewer accept risk.routine +"$jq_bin" -S -c '.body.gate_decision_refs[0].decision_record_ref.sha256=("0"*64)' \ + "$tmp/unbound/request.json" >"$tmp/unbound/request.changed" +/bin/mv "$tmp/unbound/request.changed" "$tmp/unbound/request.json" +unbound_request_sha=$(sha256_path "$tmp/unbound/request.json") +"$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$tmp/unbound/request.json" --slurpfile resolved "$resolved" \ + --arg request_sha "$unbound_request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) +' >"$tmp/unbound/result.json" +PATH="$bin:/usr/bin:/bin" "$duty_evaluator" evaluate "$policy_set" \ + "$tmp/unbound/request.json" "$resolved" "$tmp/unbound/result.json" \ + >"$tmp/unbound/duty.json" 2>"$tmp/unbound/duty.err" || fail 'unbound duty rebuild' +expect_eval unbound violated decision.unbound + +build_case ambiguous routine routine present independent-plan-check reviewer accept risk.routine +"$jq_bin" -S -c '.body.gate_decision_refs += [{purpose:"gate-decision", + decision_record_ref:{content_id:"risk.claim.other", + media_type:"application/vnd.ystack.risk-gate-decision-claim+json",sha256:("1"*64)}, + subject_ref:{type:"artifact",value:.body.source.value},scope_sha256:("0"*64)}] | + .body.gate_decision_refs |= sort_by(.scope_sha256)' "$tmp/ambiguous/request.json" \ + >"$tmp/ambiguous/request.changed" +/bin/mv "$tmp/ambiguous/request.changed" "$tmp/ambiguous/request.json" +ambiguous_request_sha=$(sha256_path "$tmp/ambiguous/request.json") +"$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$tmp/ambiguous/request.json" --slurpfile resolved "$resolved" \ + --arg request_sha "$ambiguous_request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) +' >"$tmp/ambiguous/result.json" +PATH="$bin:/usr/bin:/bin" "$duty_evaluator" evaluate "$policy_set" \ + "$tmp/ambiguous/request.json" "$resolved" "$tmp/ambiguous/result.json" \ + >"$tmp/ambiguous/duty.json" 2>"$tmp/ambiguous/duty.err" || + fail 'ambiguous duty rebuild' +expect_eval ambiguous violated decision.ambiguous + +build_case unsupported custom routine present independent-plan-check reviewer accept \ + risk.routine normal example.test +expect_eval unsupported inconclusive risk.tier-unsupported + +build_case duty-violated routine routine present independent-plan-check reviewer accept \ + risk.routine duty-collision +expect_eval duty-violated violated duty.violated + +build_case forged-duty routine routine present independent-plan-check reviewer accept risk.routine +"$jq_bin" -S -c '.body.verdict="violated" | .body.reason_ids=["duty.violated"]' \ + "$tmp/forged-duty/duty.json" >"$tmp/forged-duty/duty.changed" +/bin/mv "$tmp/forged-duty/duty.changed" "$tmp/forged-duty/duty.json" +expect_error forged-duty E_DUTY + +link="$tmp/request-link.json" +/bin/ln -s "$tmp/routine/request.json" "$link" +status=0 +PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$link" "$resolved" \ + "$tmp/routine/result.json" "$tmp/routine/duty.json" "$tmp/routine/claim.json" \ + >"$tmp/link.out" 2>"$tmp/link.err" || status=$? +[ "$status" -ne 0 ] && [ ! -s "$tmp/link.out" ] && + [ "$(/bin/cat "$tmp/link.err")" = E_RUNTIME ] || fail 'symlink input rejection' +pass 'symlink input rejection' + +relative_bin="$tmp/relative-bin" +/bin/mkdir "$relative_bin" +/bin/cp "$jq_bin" "$relative_bin/jq" +status=0 +(cd "$relative_bin" && PATH=".:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ + "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ + "$tmp/routine/duty.json" "$tmp/routine/claim.json") \ + >"$tmp/relative.out" 2>"$tmp/relative.err" || status=$? +[ "$status" -ne 0 ] && [ ! -s "$tmp/relative.out" ] && + [ "$(/bin/cat "$tmp/relative.err")" = E_RUNTIME ] || fail 'relative jq rejection' +pass 'relative jq rejection' + +copy_runtime() { + local destination=$1 copy_path + /bin/mkdir -p "$destination/control/v1" "$destination/scripts" "$destination/core" + for copy_path in risk-gates-policy.json risk-gates-decision.json risk-gates.jq \ + evaluate-risk-gates.sh duty-separation-policy.json duty-separation-decision.json \ + duty-separation.jq evaluate-duty.sh validate.sh policy-set.jq; do + /bin/cp "$root/control/v1/$copy_path" "$destination/control/v1/$copy_path" + done + /bin/cp "$root/scripts/core-contract.sh" "$destination/scripts/core-contract.sh" + /bin/cp -R "$root/core/v2" "$destination/core/v2" +} + +dependency_runtime="$tmp/dependency-runtime" +copy_runtime "$dependency_runtime" +/usr/bin/printf '\n' >>"$dependency_runtime/control/v1/duty-separation-decision.json" +/bin/cp -R "$tmp/routine" "$tmp/dependency-binding" +expect_error dependency-binding E_DUTY "$dependency_runtime" + +race_runtime="$tmp/race-runtime" +copy_runtime "$race_runtime" +race_bin="$tmp/race-bin" +race_scratch="$tmp/race-scratch" +race_trigger="$tmp/race-trigger" +/bin/mkdir -p "$race_bin" "$race_scratch" +/usr/bin/printf '%s\n' '#!/bin/bash' "real_jq='$jq_bin'" \ + 'if [ "${1:-}" = --version ]; then exec "$real_jq" "$@"; fi' \ + 'for arg in "$@"; do' \ + ' case "$arg" in' \ + ' */program.jq)' \ + ' marker=$(/usr/bin/find "${TMPDIR:-/tmp}" -type f -name risk-ready -print -quit 2>/dev/null)' \ + ' if [ -n "$marker" ] && [ ! -e "$RISK_TRIGGER" ]; then' \ + ' : >"$RISK_TRIGGER"' \ + ' /bin/sleep 1' \ + ' fi' \ + ' ;;' \ + ' esac' \ + 'done' \ + 'exec "$real_jq" "$@"' >"$race_bin/jq" +/bin/chmod 0555 "$race_bin/jq" +( + status=0 + TMPDIR="$race_scratch" RISK_TRIGGER="$race_trigger" \ + PATH="$race_bin:/usr/bin:/bin" \ + "$race_runtime/control/v1/evaluate-risk-gates.sh" evaluate "$policy_set" \ + "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ + "$tmp/routine/duty.json" "$tmp/routine/claim.json" \ + >"$tmp/race.out" 2>"$tmp/race.err" || status=$? + /usr/bin/printf '%s\n' "$status" >"$tmp/race.status" +) & +race_pid=$! +attempt=0 +while [ ! -e "$race_trigger" ] && kill -0 "$race_pid" 2>/dev/null && + [ "$attempt" -lt 400 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 +done +if [ ! -e "$race_trigger" ]; then + kill -TERM "$race_pid" 2>/dev/null || : + wait "$race_pid" 2>/dev/null || : + fail 'risk program race marker' +fi +/usr/bin/printf '\n' >>"$race_runtime/control/v1/risk-gates.jq" +wait "$race_pid" +[ "$(/bin/cat "$tmp/race.status")" -ne 0 ] && [ ! -s "$tmp/race.out" ] && + [ "$(/bin/cat "$tmp/race.err")" = E_RELATION ] || fail 'risk program TOCTOU' +pass 'risk program TOCTOU closes after mirrored execution' + +pure_dir="$tmp/routine" +pure_policy_set_sha=$(sha256_path "$policy_set") +pure_request_sha=$(sha256_path "$pure_dir/request.json") +pure_result_sha=$(sha256_path "$pure_dir/result.json") +pure_duty_sha=$(sha256_path "$pure_dir/duty.json") +pure_claim_sha=$(sha256_path "$pure_dir/claim.json") +pure_basis=$("$jq_bin" -S -c \ + '{schema_version,kind,id,body:(.body|del(.gate_decision_refs))}' \ + "$pure_dir/request.json") +pure_basis_sha=$(sha256_text "$pure_basis") +policy_scope "$pure_dir/request.json" "$tmp/pure-policy-scope" +pure_policy_scope_sha=$("$jq_bin" -r '.scope_sha256' "$tmp/pure-policy-scope") +requirement_scope "$pure_dir/request.json" routine "$tmp/pure-requirement-scope" +pure_requirement_sha=$("$jq_bin" -r '.scope_sha256' "$tmp/pure-requirement-scope") +"$jq_bin" -S -c -n -f "$root/control/v1/risk-gates.jq" \ + --slurpfile policy "$policy" --slurpfile decision "$definition" \ + --slurpfile policy_set "$policy_set" --slurpfile request "$pure_dir/request.json" \ + --slurpfile resolved "$resolved" --slurpfile result "$pure_dir/result.json" \ + --slurpfile duty_evaluation "$pure_dir/duty.json" \ + --slurpfile claim "$pure_dir/claim.json" --arg policy_sha "$policy_sha" \ + --arg decision_sha "$definition_sha" --arg policy_set_sha "$pure_policy_set_sha" \ + --arg request_sha "$pure_request_sha" --arg resolved_sha "$resolved_sha" \ + --arg result_sha "$pure_result_sha" --arg duty_sha "$pure_duty_sha" \ + --arg claim_sha "$pure_claim_sha" --arg request_basis_sha "$pure_basis_sha" \ + --arg policy_scope_sha "$pure_policy_scope_sha" \ + --arg requirement_scope_sha "$pure_requirement_sha" >"$tmp/pure.out" +"$jq_bin" -e '.body.verdict=="satisfied" and + .body.reason_ids==["risk-gates.satisfied"]' "$tmp/pure.out" >/dev/null || + fail 'pure evaluator' +pass 'pure evaluator canonical satisfied path' + +/usr/bin/printf 'control risk gates: %s passed\n' "$passes" From 89ea765532710c5b40dd67c415d5d0c293f861bb Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 03:33:04 -0400 Subject: [PATCH 02/10] Keep risk decision claims unqualified --- control/v1/evaluate-risk-gates.sh | 18 +++-- control/v1/risk-gates-decision.json | 2 +- control/v1/risk-gates-policy.json | 2 +- control/v1/risk-gates.jq | 72 +++++++++++++++---- scripts/test/control-risk-gates.test.sh | 94 ++++++++++++++++++++----- 5 files changed, 146 insertions(+), 42 deletions(-) diff --git a/control/v1/evaluate-risk-gates.sh b/control/v1/evaluate-risk-gates.sh index 502076f..2c27d6f 100755 --- a/control/v1/evaluate-risk-gates.sh +++ b/control/v1/evaluate-risk-gates.sh @@ -164,6 +164,12 @@ for input in "$@"; do "$scratch/${names[$index]}.canonical" || emit_error E_RELATION index=$((index + 1)) done +"$jq_bin" -e ' + (keys|sort)==["body","id","kind","schema_version"] and + .schema_version==1 and .kind=="risk_gate_decision_claim" and + (.id|type=="string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z")) and + (.body|type=="object") +' "$scratch/claim.json" >/dev/null 2>&1 || emit_error E_RELATION snapshot_fixed "$policy" "$scratch/policy.json" snapshot_fixed "$decision" "$scratch/decision.json" snapshot_fixed "$program" "$scratch/program.jq" @@ -211,10 +217,11 @@ validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RUNTIM media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, semantics:{authority_effect:"none", decision_claim_semantics:"immutable-input-claim-only", + decision_provenance:"unqualified-input-claim", input_contract:"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1", output_kind:"risk_gate_evaluation",output_schema_version:1, reference_semantics:"identity-only", - verdicts:["inconclusive","satisfied","violated"]}} + verdicts:["inconclusive","violated"]}} } ' >/dev/null 2>&1 || emit_error E_RELATION @@ -355,13 +362,12 @@ canonical_json "$scratch/evaluation.json" "$scratch/evaluation.canonical" || id:$result[0].id,sha256:$result_sha}} and (.body.classification|keys|sort)==["declared_tier","minimum_tier"] and (.body.classification.minimum_tier as $minimum | - ["routine","high","bootstrap"]|index($minimum)!=null) and - (.body.verdict=="satisfied" or .body.verdict=="violated" or - .body.verdict=="inconclusive") and + (["routine","high","bootstrap"]|index($minimum)!=null) or + ($minimum=="unknown" and .body.verdict=="violated")) and + (.body.verdict=="violated" or .body.verdict=="inconclusive") and (.body.reason_ids|type=="array" and length>=1 and all(.[];type=="string") and .==(sort|unique)) and - (if .body.verdict=="satisfied" then .body.reason_ids==["risk-gates.satisfied"] - else (.body.reason_ids|index("risk-gates.satisfied")==null) end) and + (.body.reason_ids|index("risk-gates.satisfied")==null) and ((.body|has("grant_ref") or has("qualification_ref") or has("activation"))|not) ' "$scratch/evaluation.json" >/dev/null 2>&1 || emit_error E_RUNTIME diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json index 7323b6f..2b1dbde 100644 --- a/control/v1/risk-gates-decision.json +++ b/control/v1/risk-gates-decision.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"54f912d801bde9ca084d4944eb431dc7afdc38c369e689dfdbae86c56ecd52ef"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"49be84411033bb3bee54cdb194cf099206a6a34623a5e16d33b24c5dff65fead"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"095fff49a29a48f1701e2e0031278bfdb5b219573ddbe42526ffcd995d278e0e"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","satisfied","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"6d5ee1ec23b8821766e7ae977147bf03d440d2c34cf0f7dc49ef88488b8d4959"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/control/v1/risk-gates-policy.json b/control/v1/risk-gates-policy.json index fd75560..f7d8563 100644 --- a/control/v1/risk-gates-policy.json +++ b/control/v1/risk-gates-policy.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","core_contract":{"generation_id_sha256":"6f6acbbd0cf40ab3c913328d6c0070635424ffe920bcdb900fbd0718345d7137","package_ref":{"content_id":"core-contract-package.v2","media_type":"application/vnd.ystack.core-contract+json","sha256":"005431c5c7e3a39dc3ab75dfcafd0f09359331667fdcacb140514a4384592716"},"semantic_identity":"core.contracts.v2"},"decision_claim_semantics":"immutable-input-claim-only","duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"evaluation_kind":"duty_separation_evaluation","policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"},"required_verdict":"satisfied"},"evaluation_mode":"observation-only","fail_mode":"closed","forced_high_reason_ids":["risk.broad-architecture","risk.constitution","risk.deployment","risk.identity-auth","risk.migration","risk.production-infrastructure","risk.security-control","risk.workflow"],"policy_version":"v1","tier_rules":[{"classification":"bootstrap","decision_kind":"operator-bootstrap-approval","decision_role":"operator","declared_tier":"bootstrap"},{"classification":"high","decision_kind":"operator-plan-approval","decision_role":"operator","declared_tier":"high"},{"classification":"routine","decision_kind":"operator-plan-approval","decision_role":"operator","declared_tier":"high"},{"classification":"routine","decision_kind":"independent-plan-check","decision_role":"reviewer","declared_tier":"routine"}]},"id":"control-policy.risk-gates","kind":"risk_gates_policy","schema_version":1} +{"body":{"activation_state":"inactive","core_contract":{"generation_id_sha256":"6f6acbbd0cf40ab3c913328d6c0070635424ffe920bcdb900fbd0718345d7137","package_ref":{"content_id":"core-contract-package.v2","media_type":"application/vnd.ystack.core-contract+json","sha256":"005431c5c7e3a39dc3ab75dfcafd0f09359331667fdcacb140514a4384592716"},"semantic_identity":"core.contracts.v2"},"decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"evaluation_kind":"duty_separation_evaluation","policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"},"required_verdict":"satisfied"},"evaluation_mode":"observation-only","fail_mode":"closed","forced_high_reason_ids":["risk.broad-architecture","risk.constitution","risk.deployment","risk.identity-auth","risk.migration","risk.production-infrastructure","risk.security-control","risk.workflow"],"policy_version":"v1","tier_rules":[{"classification":"bootstrap","decision_kind":"operator-bootstrap-approval","decision_role":"operator","declared_tier":"bootstrap"},{"classification":"high","decision_kind":"operator-plan-approval","decision_role":"operator","declared_tier":"high"},{"classification":"routine","decision_kind":"operator-plan-approval","decision_role":"operator","declared_tier":"high"},{"classification":"routine","decision_kind":"independent-plan-check","decision_role":"reviewer","declared_tier":"routine"}]},"id":"control-policy.risk-gates","kind":"risk_gates_policy","schema_version":1} diff --git a/control/v1/risk-gates.jq b/control/v1/risk-gates.jq index 197b3d2..4cb8dde 100644 --- a/control/v1/risk-gates.jq +++ b/control/v1/risk-gates.jq @@ -9,7 +9,19 @@ def sha256_ok: def time_ok: type == "string" and - test("\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\z"); + test("\\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z\\z") and + (capture("\\A(?[0-9]{4})-(?[0-9]{2})-(?[0-9]{2})T(?[0-9]{2}):(?[0-9]{2}):(?[0-9]{2})Z\\z") as $parts | + ($parts.year | tonumber) as $year | + ($parts.month | tonumber) as $month | + ($parts.day | tonumber) as $day | + ($parts.hour | tonumber) as $hour | + ($parts.minute | tonumber) as $minute | + ($parts.second | tonumber) as $second | + ($year % 4 == 0 and ($year % 100 != 0 or $year % 400 == 0)) as $leap | + [31,(if $leap then 29 else 28 end),31,30,31,30,31,31,30,31,30,31] as $days | + $month >= 1 and $month <= 12 and $day >= 1 and $day <= $days[$month - 1] and + $hour >= 0 and $hour <= 23 and $minute >= 0 and $minute <= 59 and + $second >= 0 and $second <= 59); def actor_ok: exact(["adapter_instance_id","execution_boundary_id","implementation_id", @@ -29,6 +41,24 @@ def document_ref($document;$digest): {schema_version:$document.schema_version,kind:$document.kind, id:$document.id,sha256:$digest}; +def actor_from_binding($entry): + {role:$entry.binding.role, + implementation_id:$entry.adapter_implementation.id, + implementation_version:$entry.adapter_implementation.version, + adapter_instance_id:$entry.binding.adapter_instance_id, + principal_id:$entry.binding.principal_id, + execution_boundary_id:$entry.binding.execution_boundary_id}; + +def decision_actor_bound($rule;$request_doc;$resolved_doc;$actor): + if $rule.decision_role == "reviewer" then + [$resolved_doc.body.bindings[] | select(.binding.role == "reviewer")] as $matches | + ($matches | length) == 1 and $actor == actor_from_binding($matches[0]) + elif $rule.decision_role == "operator" then + $request_doc.body.requested_by.role == "operator" and + $actor == $request_doc.body.requested_by + else false + end; + def expected_core: {semantic_identity:"core.contracts.v2", generation_id_sha256: @@ -59,11 +89,12 @@ def policy_ok: .kind == "risk_gates_policy" and .id == "control-policy.risk-gates" and (.body | exact(["activation_state","core_contract","decision_claim_semantics", - "duty_separation","evaluation_mode","fail_mode","forced_high_reason_ids", - "policy_version","tier_rules"]) and + "decision_provenance","duty_separation","evaluation_mode","fail_mode", + "forced_high_reason_ids","policy_version","tier_rules"]) and .activation_state == "inactive" and .core_contract == expected_core and .decision_claim_semantics == "immutable-input-claim-only" and + .decision_provenance == "unqualified-input-claim" and .evaluation_mode == "observation-only" and .fail_mode == "closed" and .policy_version == "v1" and .forced_high_reason_ids == expected_high_reasons and .tier_rules == expected_tier_rules and @@ -109,7 +140,10 @@ def claim_shape_ok: ($duty_evaluation[0]) as $duty | ($claim[0]) as $claim_doc | (if ($p | policy_ok) then true else error("invalid shipped risk policy") end) | -(if ($claim_doc | claim_shape_ok) then true else error("invalid risk decision claim") end) | +($claim_doc | claim_shape_ok) as $claim_valid | +($claim_doc.body.classification.tier? // "unknown") as $claimed_tier | +($claim_doc.body.decision.state? // "invalid") as $claim_decision_state | +($claim_doc.body.decision.value.asserted_decision? // "invalid") as $claim_assertion | content_ref($p.id;"application/vnd.ystack.control-policy+json";$policy_sha) as $risk_policy_ref | @@ -148,7 +182,7 @@ content_ref($claim_doc.id; elif any($request_doc.body.risk.reason_ids[]; . as $reason | $p.body.forced_high_reason_ids | index($reason) != null) then "high" - else $claim_doc.body.classification.tier + else $claimed_tier end) as $minimum_tier | ([$p.body.tier_rules[] | select(.classification == $minimum_tier and .declared_tier == $declared_tier)]) as @@ -159,7 +193,8 @@ content_ref($claim_doc.id; "application/vnd.ystack.risk-gate-decision-claim+json")]) as $risk_decision_refs | -((if $request_doc.body.risk.policy_ref == $expected_policy_scope then [] +((if $claim_valid then [] else ["decision.claim-malformed"] end) + + (if $request_doc.body.risk.policy_ref == $expected_policy_scope then [] else ["risk.policy-ref-mismatch"] end) + (if $request_doc.body.risk.required_gate_refs == [$expected_requirement_scope] then [] else ["risk.requirement-ref-mismatch"] end) + @@ -172,33 +207,40 @@ content_ref($claim_doc.id; $risk_decision_refs[0] == $expected_gate_decision then [] elif ($risk_decision_refs | length) > 1 then ["decision.ambiguous"] else ["decision.unbound"] end) + - (if $claim_doc.body.classification.reason_ids == $request_doc.body.risk.reason_ids + (if $claim_valid and + $claim_doc.body.classification.reason_ids == $request_doc.body.risk.reason_ids then [] else ["classification.reasons-mismatch"] end) + - (if $minimum_tier == $claim_doc.body.classification.tier then [] + (if $claim_valid and $minimum_tier == $claimed_tier then [] else ["classification.forced-tier-mismatch"] end) + (if $core_tier_supported and ($matching_rules | length) == 1 then [] elif $core_tier_supported then ["risk.tier-downgrade"] - else [] end) + + else ["risk.tier-unsupported"] end) + (if $duty.body.verdict == "violated" then ["duty.violated"] else [] end) + - (if $claim_doc.body.decision.state == "absent" then ["decision.missing"] - elif $claim_doc.body.decision.value.asserted_decision == "reject" + (if $claim_decision_state == "absent" then ["decision.missing"] + elif $claim_assertion == "reject" then ["decision.rejected"] else [] end) + - (if $claim_doc.body.decision.state == "present" and $matching_rule != null then + (if $claim_valid and $claim_decision_state == "present" and + $matching_rule != null then (if $claim_doc.body.decision.value.decision_kind == $matching_rule.decision_kind then [] else ["decision.kind-denied"] end) + (if $claim_doc.body.decision.value.decided_by.role == $matching_rule.decision_role then [] else ["decision.role-denied"] end) + + (if decision_actor_bound($matching_rule;$request_doc;$resolved_doc; + $claim_doc.body.decision.value.decided_by) + then [] else ["decision.actor-unbound"] end) + (if $claim_doc.body.decision.value.recorded_at <= $request_doc.body.requested_at then [] else ["decision.after-request"] end) else [] end) | sort | unique) as $violations | -((if $core_tier_supported then [] else ["risk.tier-unsupported"] end) + - (if $duty.body.verdict == "inconclusive" then ["duty.inconclusive"] else [] end) | +((if $duty.body.verdict == "inconclusive" then ["duty.inconclusive"] else [] end) + + (if $claim_valid and $claim_decision_state == "present" and + $claim_assertion == "accept" + then ["decision.provenance-unqualified"] else [] end) | sort | unique) as $unknowns | (if ($violations | length) > 0 then {verdict:"violated",reasons:$violations} elif ($unknowns | length) > 0 then {verdict:"inconclusive",reasons:$unknowns} - else {verdict:"satisfied",reasons:["risk-gates.satisfied"]} end) as $result | + else {verdict:"inconclusive",reasons:["decision.provenance-unqualified"]} end) as $result | { schema_version:1, diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index 2b36913..e1189dc 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -178,8 +178,9 @@ build_case() { local name=$1 declared=$2 classification=$3 decision_state=$4 decision_kind=$5 local decision_role=$6 asserted=$7 reason=$8 mode=${9:-normal} local namespace=${10:-core} + local decision_time=${11:-2026-08-29T23:59:59Z} local dir="$tmp/$name" base_request="$tmp/$name.base-request" - local request_basis request_basis_sha claim_sha request_sha + local request_basis request_basis_sha claim_sha request_sha actor_json /bin/mkdir -p "$dir" "$jq_bin" -L "$root/scripts/test" -S -c -n --arg resolved_sha "$resolved_sha" ' import "portable-core-stage-request-fixtures" as request; @@ -196,7 +197,12 @@ build_case() { /bin/cp "$base_request" "$dir/request.seed" fi "$jq_bin" -S -c --arg declared "$declared" --arg namespace "$namespace" \ - --arg reason "$reason" ' + --arg reason "$reason" --arg decision_role "$decision_role" ' + (if $decision_role == "operator" then + .body.requested_by={role:"operator",implementation_id:"implementation.operator", + implementation_version:"v1",adapter_instance_id:"instance.operator", + principal_id:"principal.operator",execution_boundary_id:"boundary.operator"} + else . end) | .body.risk.tier={namespace:$namespace,name:$declared} | .body.risk.reason_ids=[$reason] | .body.gate_decision_refs=[] @@ -214,14 +220,35 @@ build_case() { if [ "$decision_state" = absent ]; then decision_json='{"state":"absent"}' else + actor_json=$("$jq_bin" -S -c -n --arg role "$decision_role" \ + --slurpfile request "$dir/request.basis" --slurpfile resolved "$resolved" ' + if $role == "reviewer" then + [$resolved[0].body.bindings[] | select(.binding.role == "reviewer")][0] as $entry | + {role:$entry.binding.role,implementation_id:$entry.adapter_implementation.id, + implementation_version:$entry.adapter_implementation.version, + adapter_instance_id:$entry.binding.adapter_instance_id, + principal_id:$entry.binding.principal_id, + execution_boundary_id:$entry.binding.execution_boundary_id} + elif $role == "operator" then $request[0].body.requested_by + else + {role:$role,implementation_id:("implementation."+$role), + implementation_version:"v1",adapter_instance_id:("instance."+$role), + principal_id:("principal."+$role),execution_boundary_id:("boundary."+$role)} + end + ') + if [ "$mode" = actor-mismatch ]; then + actor_json=$("$jq_bin" -S -c '.principal_id="principal.unbound"' <<<"$actor_json") + fi decision_json=$("$jq_bin" -S -c -n --arg kind "$decision_kind" \ - --arg role "$decision_role" --arg asserted "$asserted" ' + --arg asserted "$asserted" --arg recorded_at "$decision_time" \ + --argjson actor "$actor_json" ' {state:"present",value:{asserted_decision:$asserted,decision_kind:$kind, - decided_by:{role:$role,implementation_id:("implementation."+$role), - implementation_version:"v1",adapter_instance_id:("instance."+$role), - principal_id:("principal."+$role),execution_boundary_id:("boundary."+$role)}, - recorded_at:"2026-08-29T23:59:59Z"}} + decided_by:$actor, + recorded_at:$recorded_at}} ') + if [ "$mode" = malformed-claim ]; then + decision_json=$("$jq_bin" -S -c 'del(.value.decision_kind)' <<<"$decision_json") + fi fi "$jq_bin" -S -c -n --arg id "risk.claim.$name" \ --arg classification "$classification" --arg reason "$reason" \ @@ -235,6 +262,11 @@ build_case() { request_basis_sha256:$request_basis_sha, required_gate_refs:[$requirement[0]]}} ' >"$dir/claim.json" + if [ "$mode" = malformed-classification ]; then + "$jq_bin" -S -c 'del(.body.classification.tier)' "$dir/claim.json" \ + >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + fi claim_sha=$(sha256_path "$dir/claim.json") "$jq_bin" -S -c --arg id "risk.claim.$name" --arg claim_sha "$claim_sha" \ --arg request_basis_sha "$request_basis_sha" ' @@ -296,19 +328,23 @@ expect_error() { build_case routine routine routine present independent-plan-check reviewer accept \ risk.routine -expect_eval routine satisfied risk-gates.satisfied +expect_eval routine inconclusive decision.provenance-unqualified build_case routine-escalated high routine present operator-plan-approval operator accept \ risk.routine -expect_eval routine-escalated satisfied risk-gates.satisfied +expect_eval routine-escalated inconclusive decision.provenance-unqualified build_case high high high present operator-plan-approval operator accept \ risk.security-control -expect_eval high satisfied risk-gates.satisfied +expect_eval high inconclusive decision.provenance-unqualified + +build_case backdated-high high high present operator-plan-approval operator accept \ + risk.security-control normal core 2000-01-01T00:00:00Z +expect_eval backdated-high inconclusive decision.provenance-unqualified build_case bootstrap bootstrap bootstrap present operator-bootstrap-approval operator accept \ risk.bootstrap bootstrap -expect_eval bootstrap satisfied risk-gates.satisfied +expect_eval bootstrap inconclusive decision.provenance-unqualified build_case missing high high absent ignored operator accept risk.security-control expect_eval missing violated decision.missing @@ -325,15 +361,28 @@ build_case invented-role high high present operator-plan-approval reviewer accep risk.security-control expect_eval invented-role violated decision.role-denied +build_case unbound-actor routine routine present independent-plan-check reviewer accept \ + risk.routine actor-mismatch +expect_eval unbound-actor violated decision.actor-unbound + build_case invented-kind high high present independent-plan-check operator accept \ risk.security-control expect_eval invented-kind violated decision.kind-denied +build_case malformed-claim routine routine present independent-plan-check reviewer accept \ + risk.routine malformed-claim +expect_eval malformed-claim violated decision.claim-malformed + +build_case malformed-classification routine routine present independent-plan-check reviewer \ + accept risk.routine malformed-classification +expect_eval malformed-classification violated decision.claim-malformed + +build_case malformed-time routine routine present independent-plan-check reviewer accept \ + risk.routine normal core 2026-99-99T99:99:99Z +expect_eval malformed-time violated decision.claim-malformed + build_case after-request routine routine present independent-plan-check reviewer accept \ - risk.routine -"$jq_bin" -S -c '.body.decision.value.recorded_at="2026-09-01T00:00:01Z"' \ - "$tmp/after-request/claim.json" >"$tmp/after-request/claim.changed" -/bin/mv "$tmp/after-request/claim.changed" "$tmp/after-request/claim.json" + risk.routine normal core 2026-09-01T00:00:01Z expect_eval after-request violated decision.after-request build_case stale routine routine present independent-plan-check reviewer accept risk.routine @@ -383,7 +432,7 @@ expect_eval ambiguous violated decision.ambiguous build_case unsupported custom routine present independent-plan-check reviewer accept \ risk.routine normal example.test -expect_eval unsupported inconclusive risk.tier-unsupported +expect_eval unsupported violated risk.tier-unsupported build_case duty-violated routine routine present independent-plan-check reviewer accept \ risk.routine duty-collision @@ -510,9 +559,16 @@ pure_requirement_sha=$("$jq_bin" -r '.scope_sha256' "$tmp/pure-requirement-scope --arg claim_sha "$pure_claim_sha" --arg request_basis_sha "$pure_basis_sha" \ --arg policy_scope_sha "$pure_policy_scope_sha" \ --arg requirement_scope_sha "$pure_requirement_sha" >"$tmp/pure.out" -"$jq_bin" -e '.body.verdict=="satisfied" and - .body.reason_ids==["risk-gates.satisfied"]' "$tmp/pure.out" >/dev/null || +"$jq_bin" -e '.body.verdict=="inconclusive" and + .body.reason_ids==["decision.provenance-unqualified"]' "$tmp/pure.out" >/dev/null || fail 'pure evaluator' -pass 'pure evaluator canonical satisfied path' +pass 'pure evaluator keeps identity-only claim unqualified' + +while IFS= read -r risk_output; do + "$jq_bin" -e '.body.verdict != "satisfied" and + (.body.reason_ids | index("risk-gates.satisfied") == null)' "$risk_output" \ + >/dev/null || fail "caller-synthesized satisfied ${risk_output##*/}" +done < <(/usr/bin/find "$tmp" -type f \( -name risk.out -o -name pure.out \) -print) +pass 'no caller-authored claim can synthesize satisfied' /usr/bin/printf 'control risk gates: %s passed\n' "$passes" From 313250e82f6cade51c3f0cfad0f3bfda3078e446 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 04:26:29 -0400 Subject: [PATCH 03/10] Normalize malformed risk tiers --- control/v1/risk-gates-decision.json | 2 +- control/v1/risk-gates.jq | 6 ++- scripts/test/control-risk-gates.test.sh | 54 +++++++++++++++++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json index 2b1dbde..afec99b 100644 --- a/control/v1/risk-gates-decision.json +++ b/control/v1/risk-gates-decision.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"6d5ee1ec23b8821766e7ae977147bf03d440d2c34cf0f7dc49ef88488b8d4959"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"827139e3d11777424236c1f3b60a2a4d818a6684753150b43f906d2b382f3caf"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/control/v1/risk-gates.jq b/control/v1/risk-gates.jq index 4cb8dde..e30c460 100644 --- a/control/v1/risk-gates.jq +++ b/control/v1/risk-gates.jq @@ -141,7 +141,11 @@ def claim_shape_ok: ($claim[0]) as $claim_doc | (if ($p | policy_ok) then true else error("invalid shipped risk policy") end) | ($claim_doc | claim_shape_ok) as $claim_valid | -($claim_doc.body.classification.tier? // "unknown") as $claimed_tier | +(($claim_doc.body.classification.tier? // null) as $candidate_tier | + if ($candidate_tier | type) == "string" and + ($candidate_tier == "routine" or $candidate_tier == "high" or + $candidate_tier == "bootstrap") + then $candidate_tier else "unknown" end) as $claimed_tier | ($claim_doc.body.decision.state? // "invalid") as $claim_decision_state | ($claim_doc.body.decision.value.asserted_decision? // "invalid") as $claim_assertion | diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index e1189dc..0a71850 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -266,6 +266,26 @@ build_case() { "$jq_bin" -S -c 'del(.body.classification.tier)' "$dir/claim.json" \ >"$dir/claim.changed" /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = tier-custom ]; then + "$jq_bin" -S -c '.body.classification.tier="custom"' "$dir/claim.json" \ + >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = tier-empty ]; then + "$jq_bin" -S -c '.body.classification.tier=""' "$dir/claim.json" \ + >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = tier-number ]; then + "$jq_bin" -S -c '.body.classification.tier=1' "$dir/claim.json" \ + >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = tier-object ]; then + "$jq_bin" -S -c '.body.classification.tier={value:"routine"}' "$dir/claim.json" \ + >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = tier-null ]; then + "$jq_bin" -S -c '.body.classification.tier=null' "$dir/claim.json" \ + >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" fi claim_sha=$(sha256_path "$dir/claim.json") "$jq_bin" -S -c --arg id "risk.claim.$name" --arg claim_sha "$claim_sha" \ @@ -314,6 +334,14 @@ expect_eval() { pass "$name" } +expect_minimum() { + local name=$1 expected=$2 + "$jq_bin" -e --arg expected "$expected" ' + .body.verdict=="violated" and .body.classification.minimum_tier==$expected and + (.body.reason_ids|index("decision.claim-malformed")!=null) + ' "$tmp/$name/risk.out" >/dev/null || fail "$name normalized minimum" +} + expect_error() { local name=$1 expected=$2 runtime=${3:-$root} local dir="$tmp/$name" status=0 @@ -376,6 +404,32 @@ expect_eval malformed-claim violated decision.claim-malformed build_case malformed-classification routine routine present independent-plan-check reviewer \ accept risk.routine malformed-classification expect_eval malformed-classification violated decision.claim-malformed +expect_minimum malformed-classification unknown + +build_case tier-wrong-string routine routine present independent-plan-check reviewer accept \ + risk.routine tier-custom +expect_eval tier-wrong-string violated decision.claim-malformed +expect_minimum tier-wrong-string unknown + +build_case tier-empty routine routine present independent-plan-check reviewer accept \ + risk.routine tier-empty +expect_eval tier-empty violated decision.claim-malformed +expect_minimum tier-empty unknown + +build_case tier-number routine routine present independent-plan-check reviewer accept \ + risk.routine tier-number +expect_eval tier-number violated decision.claim-malformed +expect_minimum tier-number unknown + +build_case tier-object routine routine present independent-plan-check reviewer accept \ + risk.routine tier-object +expect_eval tier-object violated decision.claim-malformed +expect_minimum tier-object unknown + +build_case tier-null routine routine present independent-plan-check reviewer accept \ + risk.routine tier-null +expect_eval tier-null violated decision.claim-malformed +expect_minimum tier-null unknown build_case malformed-time routine routine present independent-plan-check reviewer accept \ risk.routine normal core 2026-99-99T99:99:99Z From 533979ad390a442e04b8919b438ffb8dd206bcf2 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 05:18:20 -0400 Subject: [PATCH 04/10] Normalize unsupported request tiers --- control/v1/risk-gates-decision.json | 2 +- control/v1/risk-gates.jq | 3 ++- scripts/test/control-risk-gates.test.sh | 9 +++++++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json index afec99b..bf48a08 100644 --- a/control/v1/risk-gates-decision.json +++ b/control/v1/risk-gates-decision.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"827139e3d11777424236c1f3b60a2a4d818a6684753150b43f906d2b382f3caf"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"e75b887d83a0d6ac2a63bb6933c48fb6acc221d7235a57b91474c9c70da76386"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/control/v1/risk-gates.jq b/control/v1/risk-gates.jq index e30c460..964218c 100644 --- a/control/v1/risk-gates.jq +++ b/control/v1/risk-gates.jq @@ -181,7 +181,8 @@ content_ref($claim_doc.id; $request_doc.body.risk.tier.name == "high" or $request_doc.body.risk.tier.name == "bootstrap")) as $core_tier_supported | ($request_doc.body.risk.tier.name) as $declared_tier | -(if $request_doc.body.target_revision.state == "absent" or +(if ($core_tier_supported | not) then "unknown" + elif $request_doc.body.target_revision.state == "absent" or $request_doc.body.base.state == "absent" then "bootstrap" elif any($request_doc.body.risk.reason_ids[]; . as $reason | $p.body.forced_high_reason_ids | index($reason) != null) diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index 0a71850..4d82d49 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -337,8 +337,7 @@ expect_eval() { expect_minimum() { local name=$1 expected=$2 "$jq_bin" -e --arg expected "$expected" ' - .body.verdict=="violated" and .body.classification.minimum_tier==$expected and - (.body.reason_ids|index("decision.claim-malformed")!=null) + .body.verdict=="violated" and .body.classification.minimum_tier==$expected ' "$tmp/$name/risk.out" >/dev/null || fail "$name normalized minimum" } @@ -487,6 +486,12 @@ expect_eval ambiguous violated decision.ambiguous build_case unsupported custom routine present independent-plan-check reviewer accept \ risk.routine normal example.test expect_eval unsupported violated risk.tier-unsupported +expect_minimum unsupported unknown + +build_case unsupported-foreign-high high high present operator-plan-approval operator accept \ + risk.security-control normal example.test +expect_eval unsupported-foreign-high violated risk.tier-unsupported +expect_minimum unsupported-foreign-high unknown build_case duty-violated routine routine present independent-plan-check reviewer accept \ risk.routine duty-collision From 206572bb3c39f2e8fedb6fb0d361ec6aca65ad6a Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 06:11:39 -0400 Subject: [PATCH 05/10] Normalize malformed risk claim objects --- control/v1/risk-gates-decision.json | 2 +- control/v1/risk-gates.jq | 34 +++++++--- scripts/test/control-risk-gates.test.sh | 86 +++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 16 deletions(-) diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json index bf48a08..e7b1101 100644 --- a/control/v1/risk-gates-decision.json +++ b/control/v1/risk-gates-decision.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"e75b887d83a0d6ac2a63bb6933c48fb6acc221d7235a57b91474c9c70da76386"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"b6b31e39276e49b95f8a41c75364be1b0470db36bae9e26a40f67a9cf11828ff"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/control/v1/risk-gates.jq b/control/v1/risk-gates.jq index 964218c..5568eca 100644 --- a/control/v1/risk-gates.jq +++ b/control/v1/risk-gates.jq @@ -141,13 +141,26 @@ def claim_shape_ok: ($claim[0]) as $claim_doc | (if ($p | policy_ok) then true else error("invalid shipped risk policy") end) | ($claim_doc | claim_shape_ok) as $claim_valid | -(($claim_doc.body.classification.tier? // null) as $candidate_tier | +(($claim_doc.body.classification? // null) as $raw_classification | + if ($raw_classification | type) == "object" then $raw_classification else {} end) as + $classification | +(($classification.tier? // null) as $candidate_tier | if ($candidate_tier | type) == "string" and ($candidate_tier == "routine" or $candidate_tier == "high" or $candidate_tier == "bootstrap") then $candidate_tier else "unknown" end) as $claimed_tier | -($claim_doc.body.decision.state? // "invalid") as $claim_decision_state | -($claim_doc.body.decision.value.asserted_decision? // "invalid") as $claim_assertion | +(($claim_doc.body.decision? // null) as $raw_decision | + if ($raw_decision | type) == "object" then $raw_decision else {} end) as + $claim_decision | +(($claim_decision.value? // null) as $raw_decision_value | + if ($raw_decision_value | type) == "object" then $raw_decision_value else {} end) as + $claim_decision_value | +(($claim_decision_value.decided_by? // null) as $raw_actor | + if ($raw_actor | type) == "object" then $raw_actor else {} end) as $claim_actor | +($claim_decision.state? // "invalid") as $claim_decision_state | +($claim_decision_value.asserted_decision? // "invalid") as $claim_assertion | +($claim_decision_value.decision_kind? // "invalid") as $claim_decision_kind | +($claim_decision_value.recorded_at? // "invalid") as $claim_decision_time | content_ref($p.id;"application/vnd.ystack.control-policy+json";$policy_sha) as $risk_policy_ref | @@ -181,7 +194,8 @@ content_ref($claim_doc.id; $request_doc.body.risk.tier.name == "high" or $request_doc.body.risk.tier.name == "bootstrap")) as $core_tier_supported | ($request_doc.body.risk.tier.name) as $declared_tier | -(if ($core_tier_supported | not) then "unknown" +(if $claimed_tier == "unknown" then "unknown" + elif ($core_tier_supported | not) then "unknown" elif $request_doc.body.target_revision.state == "absent" or $request_doc.body.base.state == "absent" then "bootstrap" elif any($request_doc.body.risk.reason_ids[]; @@ -211,9 +225,9 @@ content_ref($claim_doc.id; (if ($risk_decision_refs | length) == 1 and $risk_decision_refs[0] == $expected_gate_decision then [] elif ($risk_decision_refs | length) > 1 then ["decision.ambiguous"] - else ["decision.unbound"] end) + + else ["decision.unbound"] end) + (if $claim_valid and - $claim_doc.body.classification.reason_ids == $request_doc.body.risk.reason_ids + ($classification.reason_ids? // null) == $request_doc.body.risk.reason_ids then [] else ["classification.reasons-mismatch"] end) + (if $claim_valid and $minimum_tier == $claimed_tier then [] else ["classification.forced-tier-mismatch"] end) + @@ -227,14 +241,14 @@ content_ref($claim_doc.id; else [] end) + (if $claim_valid and $claim_decision_state == "present" and $matching_rule != null then - (if $claim_doc.body.decision.value.decision_kind == $matching_rule.decision_kind + (if $claim_decision_kind == $matching_rule.decision_kind then [] else ["decision.kind-denied"] end) + - (if $claim_doc.body.decision.value.decided_by.role == $matching_rule.decision_role + (if ($claim_actor.role? // "invalid") == $matching_rule.decision_role then [] else ["decision.role-denied"] end) + (if decision_actor_bound($matching_rule;$request_doc;$resolved_doc; - $claim_doc.body.decision.value.decided_by) + $claim_actor) then [] else ["decision.actor-unbound"] end) + - (if $claim_doc.body.decision.value.recorded_at <= $request_doc.body.requested_at + (if $claim_decision_time <= $request_doc.body.requested_at then [] else ["decision.after-request"] end) else [] end) | sort | unique) as $violations | diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index 4d82d49..192cbfe 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -5,7 +5,7 @@ export LC_ALL=C umask 077 if [ "${YSTACK_RISK_TEST_BOUNDED:-0}" != 1 ]; then - YSTACK_RISK_TEST_BOUNDED=1 exec /usr/bin/perl -e 'alarm 240; exec @ARGV' "$0" + YSTACK_RISK_TEST_BOUNDED=1 exec /usr/bin/perl -e 'alarm 420; exec @ARGV' "$0" fi root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) @@ -187,7 +187,8 @@ build_case() { request::request_doc("producer";$resolved_sha) | walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) ' >"$base_request" - if [ "$mode" = bootstrap ]; then + if [ "$mode" = bootstrap ] || [ "$mode" = tier-number-bootstrap ] || + [ "$mode" = tier-object-bootstrap ] || [ "$mode" = tier-null-bootstrap ]; then "$jq_bin" -S -c '.body.target_revision={state:"absent"} | .body.base={state:"absent"}' "$base_request" >"$dir/request.seed" elif [ "$mode" = duty-collision ]; then @@ -274,18 +275,39 @@ build_case() { "$jq_bin" -S -c '.body.classification.tier=""' "$dir/claim.json" \ >"$dir/claim.changed" /bin/mv "$dir/claim.changed" "$dir/claim.json" - elif [ "$mode" = tier-number ]; then + elif [ "$mode" = tier-number ] || [ "$mode" = tier-number-forced ] || + [ "$mode" = tier-number-bootstrap ]; then "$jq_bin" -S -c '.body.classification.tier=1' "$dir/claim.json" \ >"$dir/claim.changed" /bin/mv "$dir/claim.changed" "$dir/claim.json" - elif [ "$mode" = tier-object ]; then + elif [ "$mode" = tier-object ] || [ "$mode" = tier-object-forced ] || + [ "$mode" = tier-object-bootstrap ]; then "$jq_bin" -S -c '.body.classification.tier={value:"routine"}' "$dir/claim.json" \ >"$dir/claim.changed" /bin/mv "$dir/claim.changed" "$dir/claim.json" - elif [ "$mode" = tier-null ]; then + elif [ "$mode" = tier-null ] || [ "$mode" = tier-null-forced ] || + [ "$mode" = tier-null-bootstrap ]; then "$jq_bin" -S -c '.body.classification.tier=null' "$dir/claim.json" \ >"$dir/claim.changed" /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = decision-scalar ]; then + "$jq_bin" -S -c '.body.decision=1' "$dir/claim.json" >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = decision-array ]; then + "$jq_bin" -S -c '.body.decision=[]' "$dir/claim.json" >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = decision-null ]; then + "$jq_bin" -S -c '.body.decision=null' "$dir/claim.json" >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = value-scalar ]; then + "$jq_bin" -S -c '.body.decision.value=1' "$dir/claim.json" >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = value-array ]; then + "$jq_bin" -S -c '.body.decision.value=[]' "$dir/claim.json" >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" + elif [ "$mode" = value-null ]; then + "$jq_bin" -S -c '.body.decision.value=null' "$dir/claim.json" >"$dir/claim.changed" + /bin/mv "$dir/claim.changed" "$dir/claim.json" fi claim_sha=$(sha256_path "$dir/claim.json") "$jq_bin" -S -c --arg id "risk.claim.$name" --arg claim_sha "$claim_sha" \ @@ -400,6 +422,30 @@ build_case malformed-claim routine routine present independent-plan-check review risk.routine malformed-claim expect_eval malformed-claim violated decision.claim-malformed +build_case decision-scalar routine routine present independent-plan-check reviewer accept \ + risk.routine decision-scalar +expect_eval decision-scalar violated decision.claim-malformed + +build_case decision-array routine routine present independent-plan-check reviewer accept \ + risk.routine decision-array +expect_eval decision-array violated decision.claim-malformed + +build_case decision-null routine routine present independent-plan-check reviewer accept \ + risk.routine decision-null +expect_eval decision-null violated decision.claim-malformed + +build_case value-scalar routine routine present independent-plan-check reviewer accept \ + risk.routine value-scalar +expect_eval value-scalar violated decision.claim-malformed + +build_case value-array routine routine present independent-plan-check reviewer accept \ + risk.routine value-array +expect_eval value-array violated decision.claim-malformed + +build_case value-null routine routine present independent-plan-check reviewer accept \ + risk.routine value-null +expect_eval value-null violated decision.claim-malformed + build_case malformed-classification routine routine present independent-plan-check reviewer \ accept risk.routine malformed-classification expect_eval malformed-classification violated decision.claim-malformed @@ -430,6 +476,36 @@ build_case tier-null routine routine present independent-plan-check reviewer acc expect_eval tier-null violated decision.claim-malformed expect_minimum tier-null unknown +build_case forced-high-number high high present operator-plan-approval operator accept \ + risk.security-control tier-number-forced +expect_eval forced-high-number violated decision.claim-malformed +expect_minimum forced-high-number unknown + +build_case forced-high-object high high present operator-plan-approval operator accept \ + risk.security-control tier-object-forced +expect_eval forced-high-object violated decision.claim-malformed +expect_minimum forced-high-object unknown + +build_case forced-high-null high high present operator-plan-approval operator accept \ + risk.security-control tier-null-forced +expect_eval forced-high-null violated decision.claim-malformed +expect_minimum forced-high-null unknown + +build_case bootstrap-number bootstrap bootstrap present operator-bootstrap-approval operator \ + accept risk.bootstrap tier-number-bootstrap +expect_eval bootstrap-number violated decision.claim-malformed +expect_minimum bootstrap-number unknown + +build_case bootstrap-object bootstrap bootstrap present operator-bootstrap-approval operator \ + accept risk.bootstrap tier-object-bootstrap +expect_eval bootstrap-object violated decision.claim-malformed +expect_minimum bootstrap-object unknown + +build_case bootstrap-null bootstrap bootstrap present operator-bootstrap-approval operator \ + accept risk.bootstrap tier-null-bootstrap +expect_eval bootstrap-null violated decision.claim-malformed +expect_minimum bootstrap-null unknown + build_case malformed-time routine routine present independent-plan-check reviewer accept \ risk.routine normal core 2026-99-99T99:99:99Z expect_eval malformed-time violated decision.claim-malformed From b620564de301ea45c0b76c434db4f37bbb6afb29 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 07:01:13 -0400 Subject: [PATCH 06/10] Document inactive risk-gates evaluator --- README.md | 16 ++++++++++++++++ RESTORE.md | 12 ++++++++++++ ci/required-files.txt | 7 +++++++ 3 files changed, 35 insertions(+) diff --git a/README.md b/README.md index 1629c4d..f240680 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,22 @@ credential, runs no candidate, and performs no network or external write. Sandbo credential, risk, kill-switch, evidence, and publisher enforcement remain later Control foundation units. +## Inactive risk-gates evaluator + +`control/v1/evaluate-risk-gates.sh` checks one public core v2 stage tuple, its +regenerated duty-separation result, and one caller-supplied decision claim against +the shipped risk-gates policy. It binds the policy-set, policy, decision, +evaluator, duty-separation, and selected public-core identities before producing a +canonical observation. Malformed, stale, ambiguous, rejected, downgraded, and +unsupported claims are `violated`. + +The decision input is only an immutable, identity-bound claim. No qualified +decision-provenance adapter exists yet, so even an internally matching accept +claim is `inconclusive` with `decision.provenance-unqualified`; this evaluator has +no `satisfied` result. It grants no approval, authority, qualification, or +permission, activates nothing, and performs no candidate, credential, network, +publish, deploy, or external-write action. + ## The current default team You talk **only** to yshifu, in a Claude Code session. yshifu orchestrates the other roles diff --git a/RESTORE.md b/RESTORE.md index 7a55bbe..3a72147 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -342,6 +342,18 @@ ceilings, all three identity-separation dimensions, dormant publisher behavior, canonical observation results. It does not enforce effective sandbox or credential permissions and grants no authority or external write. +Restore the five paths in the manifest's inactive risk-gates block, then run: + +```sh +bash scripts/test/control-risk-gates.test.sh +``` + +This checks the exact policy, decision, evaluator, duty-separation, policy-set, and +public-core identity closure; tier downgrade and malformed-input handling; and +canonical `violated` or `inconclusive` observations. Decision input is an +unqualified immutable claim, so an accept claim cannot produce `satisfied` or grant +approval, authority, qualification, activation, permission, or an external effect. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/ci/required-files.txt b/ci/required-files.txt index a8c8a2b..7e68a9b 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -178,3 +178,10 @@ control/v1/duty-separation-decision.json control/v1/duty-separation.jq control/v1/evaluate-duty.sh scripts/test/control-duty-separation.test.sh + +# Inactive risk-gates policy and evaluator +control/v1/risk-gates-policy.json +control/v1/risk-gates-decision.json +control/v1/risk-gates.jq +control/v1/evaluate-risk-gates.sh +scripts/test/control-risk-gates.test.sh From fa8451784ab6a60473f8e80f0650d2562d543f99 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 07:31:56 -0400 Subject: [PATCH 07/10] Add sanitized risk relation diagnostics --- control/v1/evaluate-risk-gates.sh | 26 ++++++++++++++++++++++++- control/v1/risk-gates-decision.json | 2 +- scripts/test/control-risk-gates.test.sh | 5 +++-- 3 files changed, 29 insertions(+), 4 deletions(-) diff --git a/control/v1/evaluate-risk-gates.sh b/control/v1/evaluate-risk-gates.sh index 2c27d6f..a46df77 100755 --- a/control/v1/evaluate-risk-gates.sh +++ b/control/v1/evaluate-risk-gates.sh @@ -3,12 +3,19 @@ set -uo pipefail export LC_ALL=C umask 077 +relation_stage="initial" emit_error() { case "${1:-}" in - E_USAGE|E_RUNTIME|E_LIMIT|E_RELATION|E_DUTY) + E_USAGE|E_RUNTIME|E_LIMIT|E_DUTY) /usr/bin/printf '%s\n' "$1" >&2 ;; + E_RELATION) + /usr/bin/printf '%s\n' E_RELATION >&2 + if [ "${YSTACK_RISK_TEST_RELATION_STAGE:-0}" = 1 ]; then + /usr/bin/printf 'relation-stage:%s\n' "$relation_stage" >&2 + fi + ;; *) /usr/bin/printf '%s\n' E_RUNTIME >&2 ;; esac exit 1 @@ -158,6 +165,7 @@ fixed_files_ok() { names=(policy-set request resolved result duty-evaluation claim) index=0 +relation_stage="input-snapshot" for input in "$@"; do snapshot_fixed "$input" "$scratch/${names[$index]}.json" canonical_json "$scratch/${names[$index]}.json" \ @@ -170,6 +178,7 @@ done (.id|type=="string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z")) and (.body|type=="object") ' "$scratch/claim.json" >/dev/null 2>&1 || emit_error E_RELATION +relation_stage="shipped-snapshot" snapshot_fixed "$policy" "$scratch/policy.json" snapshot_fixed "$decision" "$scratch/decision.json" snapshot_fixed "$program" "$scratch/program.jq" @@ -182,6 +191,7 @@ for control_dir in "$repo/control" "$source_dir"; do done [ "$source_dir" = "$repo/control/v1" ] || emit_error E_RELATION +relation_stage="shipped-identity" driver_sha=$(sha256_path "$source_path") || emit_error E_RUNTIME program_sha=$(sha256_path "$scratch/program.jq") || emit_error E_RUNTIME policy_sha=$(sha256_path "$scratch/policy.json") || emit_error E_RUNTIME @@ -241,12 +251,17 @@ validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RUNTIM media_type:"text/x-jq",sha256:$duty_program_sha}} ' "$duty_decision" >/dev/null 2>&1 || emit_error E_RELATION +relation_stage="runtime-select" selected=$(selected_core_generation "$core_driver") || emit_error E_RELATION +relation_stage="runtime-live-pre" live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-pre) || emit_error E_RELATION +relation_stage="runtime-mirror-build" mirror_root=$(build_runtime_mirror "$selected") || emit_error E_RELATION +relation_stage="runtime-mirror-pre" mirror_runtime_sha=$(runtime_closure_sha "$mirror_root" "$selected" mirror-pre) || emit_error E_RELATION +relation_stage="runtime-pre-match" [ "$live_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION : >"$scratch/duty-ready" @@ -256,16 +271,20 @@ PATH="${jq_bin%/*}:/usr/bin:/bin" "$mirror_root/control/v1/evaluate-duty.sh" eva "$scratch/result.json" >"$scratch/generated-duty.json" \ 2>"$scratch/generated-duty.err" || duty_status=$? : >"$scratch/duty-complete" +relation_stage="runtime-live-post-duty" post_live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-post-duty) || emit_error E_RELATION +relation_stage="runtime-mirror-post-duty" post_mirror_runtime_sha=$(runtime_closure_sha \ "$mirror_root" "$selected" mirror-post-duty) || emit_error E_RELATION +relation_stage="runtime-post-duty-match" [ "$post_live_runtime_sha" = "$live_runtime_sha" ] && [ "$post_mirror_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION [ "$duty_status" -eq 0 ] || emit_error E_DUTY /usr/bin/cmp -s "$scratch/generated-duty.json" "$scratch/duty-evaluation.json" || emit_error E_DUTY +relation_stage="stage-identity" policy_set_sha=$(sha256_path "$scratch/policy-set.json") || emit_error E_RUNTIME request_sha=$(sha256_path "$scratch/request.json") || emit_error E_RUNTIME resolved_sha=$(sha256_path "$scratch/resolved.json") || emit_error E_RUNTIME @@ -295,6 +314,7 @@ requirement_descriptor=$("$jq_bin" -S -c -n --arg policy_sha "$policy_sha" \ ') || emit_error E_RELATION requirement_scope_sha=$(sha256_text "$requirement_descriptor") || emit_error E_RUNTIME +relation_stage="risk-evaluation" : >"$scratch/risk-ready" "$jq_bin" -S -c -n -f "$scratch/program.jq" \ --slurpfile policy "$scratch/policy.json" \ @@ -314,11 +334,15 @@ requirement_scope_sha=$(sha256_text "$requirement_descriptor") || emit_error E_R --arg requirement_scope_sha "$requirement_scope_sha" \ >"$scratch/evaluation.json" 2>/dev/null || emit_error E_RUNTIME : >"$scratch/risk-complete" +relation_stage="runtime-post" fixed_files_ok || emit_error E_RELATION +relation_stage="runtime-live-post-risk" final_live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-post-risk) || emit_error E_RELATION +relation_stage="runtime-mirror-post-risk" final_mirror_runtime_sha=$(runtime_closure_sha \ "$mirror_root" "$selected" mirror-post-risk) || emit_error E_RELATION +relation_stage="runtime-post-risk-match" [ "$final_live_runtime_sha" = "$live_runtime_sha" ] && [ "$final_mirror_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION canonical_json "$scratch/evaluation.json" "$scratch/evaluation.canonical" || diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json index e7b1101..a085b7a 100644 --- a/control/v1/risk-gates-decision.json +++ b/control/v1/risk-gates-decision.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"f06c0a4135d6f12867032f2b368265d5184c2ff6d14279df6a7d1b533bcb2dc2"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"b6b31e39276e49b95f8a41c75364be1b0470db36bae9e26a40f67a9cf11828ff"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"276657b3955b4e2c661178375324435ba8894e9e8e531ace1f11eedb7255166d"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"b6b31e39276e49b95f8a41c75364be1b0470db36bae9e26a40f67a9cf11828ff"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index 192cbfe..3ce8971 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -337,8 +337,9 @@ build_case() { expect_eval() { local name=$1 verdict=$2 reason=$3 local dir="$tmp/$name" out="$tmp/$name/risk.out" - PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$dir/request.json" \ - "$resolved" "$dir/result.json" "$dir/duty.json" "$dir/claim.json" \ + YSTACK_RISK_TEST_RELATION_STAGE=1 PATH="$bin:/usr/bin:/bin" \ + "$evaluator" evaluate "$policy_set" "$dir/request.json" "$resolved" \ + "$dir/result.json" "$dir/duty.json" "$dir/claim.json" \ >"$out" 2>"$dir/risk.err" || { /bin/cat "$dir/risk.err" >&2 fail "$name status" From ff25b9db3b0d92ae243f6695b22b9ff2b59d4a1a Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 07:50:35 -0400 Subject: [PATCH 08/10] Fix risk identity check on empty input --- control/v1/evaluate-risk-gates.sh | 28 ++------------------- control/v1/risk-gates-decision.json | 2 +- scripts/test/control-risk-gates.test.sh | 33 ++++++++++++++++++++++--- 3 files changed, 32 insertions(+), 31 deletions(-) diff --git a/control/v1/evaluate-risk-gates.sh b/control/v1/evaluate-risk-gates.sh index a46df77..f95802d 100755 --- a/control/v1/evaluate-risk-gates.sh +++ b/control/v1/evaluate-risk-gates.sh @@ -3,19 +3,12 @@ set -uo pipefail export LC_ALL=C umask 077 -relation_stage="initial" emit_error() { case "${1:-}" in - E_USAGE|E_RUNTIME|E_LIMIT|E_DUTY) + E_USAGE|E_RUNTIME|E_LIMIT|E_RELATION|E_DUTY) /usr/bin/printf '%s\n' "$1" >&2 ;; - E_RELATION) - /usr/bin/printf '%s\n' E_RELATION >&2 - if [ "${YSTACK_RISK_TEST_RELATION_STAGE:-0}" = 1 ]; then - /usr/bin/printf 'relation-stage:%s\n' "$relation_stage" >&2 - fi - ;; *) /usr/bin/printf '%s\n' E_RUNTIME >&2 ;; esac exit 1 @@ -165,7 +158,6 @@ fixed_files_ok() { names=(policy-set request resolved result duty-evaluation claim) index=0 -relation_stage="input-snapshot" for input in "$@"; do snapshot_fixed "$input" "$scratch/${names[$index]}.json" canonical_json "$scratch/${names[$index]}.json" \ @@ -178,7 +170,6 @@ done (.id|type=="string" and test("\\A[a-z0-9][a-z0-9._:-]{0,127}\\z")) and (.body|type=="object") ' "$scratch/claim.json" >/dev/null 2>&1 || emit_error E_RELATION -relation_stage="shipped-snapshot" snapshot_fixed "$policy" "$scratch/policy.json" snapshot_fixed "$decision" "$scratch/decision.json" snapshot_fixed "$program" "$scratch/program.jq" @@ -191,7 +182,6 @@ for control_dir in "$repo/control" "$source_dir"; do done [ "$source_dir" = "$repo/control/v1" ] || emit_error E_RELATION -relation_stage="shipped-identity" driver_sha=$(sha256_path "$source_path") || emit_error E_RUNTIME program_sha=$(sha256_path "$scratch/program.jq") || emit_error E_RUNTIME policy_sha=$(sha256_path "$scratch/policy.json") || emit_error E_RUNTIME @@ -203,7 +193,7 @@ duty_program_sha=$(sha256_path "$duty_program") || emit_error E_RUNTIME validator_driver_sha=$(sha256_path "$policy_validator") || emit_error E_RUNTIME validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RUNTIME -"$jq_bin" -e --arg policy_sha "$policy_sha" --arg driver_sha "$driver_sha" \ +"$jq_bin" -n -e --arg policy_sha "$policy_sha" --arg driver_sha "$driver_sha" \ --arg program_sha "$program_sha" --arg duty_policy_sha "$duty_policy_sha" \ --arg duty_decision_sha "$duty_decision_sha" \ --slurpfile policy "$scratch/policy.json" \ @@ -251,17 +241,12 @@ validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RUNTIM media_type:"text/x-jq",sha256:$duty_program_sha}} ' "$duty_decision" >/dev/null 2>&1 || emit_error E_RELATION -relation_stage="runtime-select" selected=$(selected_core_generation "$core_driver") || emit_error E_RELATION -relation_stage="runtime-live-pre" live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-pre) || emit_error E_RELATION -relation_stage="runtime-mirror-build" mirror_root=$(build_runtime_mirror "$selected") || emit_error E_RELATION -relation_stage="runtime-mirror-pre" mirror_runtime_sha=$(runtime_closure_sha "$mirror_root" "$selected" mirror-pre) || emit_error E_RELATION -relation_stage="runtime-pre-match" [ "$live_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION : >"$scratch/duty-ready" @@ -271,20 +256,16 @@ PATH="${jq_bin%/*}:/usr/bin:/bin" "$mirror_root/control/v1/evaluate-duty.sh" eva "$scratch/result.json" >"$scratch/generated-duty.json" \ 2>"$scratch/generated-duty.err" || duty_status=$? : >"$scratch/duty-complete" -relation_stage="runtime-live-post-duty" post_live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-post-duty) || emit_error E_RELATION -relation_stage="runtime-mirror-post-duty" post_mirror_runtime_sha=$(runtime_closure_sha \ "$mirror_root" "$selected" mirror-post-duty) || emit_error E_RELATION -relation_stage="runtime-post-duty-match" [ "$post_live_runtime_sha" = "$live_runtime_sha" ] && [ "$post_mirror_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION [ "$duty_status" -eq 0 ] || emit_error E_DUTY /usr/bin/cmp -s "$scratch/generated-duty.json" "$scratch/duty-evaluation.json" || emit_error E_DUTY -relation_stage="stage-identity" policy_set_sha=$(sha256_path "$scratch/policy-set.json") || emit_error E_RUNTIME request_sha=$(sha256_path "$scratch/request.json") || emit_error E_RUNTIME resolved_sha=$(sha256_path "$scratch/resolved.json") || emit_error E_RUNTIME @@ -314,7 +295,6 @@ requirement_descriptor=$("$jq_bin" -S -c -n --arg policy_sha "$policy_sha" \ ') || emit_error E_RELATION requirement_scope_sha=$(sha256_text "$requirement_descriptor") || emit_error E_RUNTIME -relation_stage="risk-evaluation" : >"$scratch/risk-ready" "$jq_bin" -S -c -n -f "$scratch/program.jq" \ --slurpfile policy "$scratch/policy.json" \ @@ -334,15 +314,11 @@ relation_stage="risk-evaluation" --arg requirement_scope_sha "$requirement_scope_sha" \ >"$scratch/evaluation.json" 2>/dev/null || emit_error E_RUNTIME : >"$scratch/risk-complete" -relation_stage="runtime-post" fixed_files_ok || emit_error E_RELATION -relation_stage="runtime-live-post-risk" final_live_runtime_sha=$(runtime_closure_sha "$repo" "$selected" live-post-risk) || emit_error E_RELATION -relation_stage="runtime-mirror-post-risk" final_mirror_runtime_sha=$(runtime_closure_sha \ "$mirror_root" "$selected" mirror-post-risk) || emit_error E_RELATION -relation_stage="runtime-post-risk-match" [ "$final_live_runtime_sha" = "$live_runtime_sha" ] && [ "$final_mirror_runtime_sha" = "$mirror_runtime_sha" ] || emit_error E_RELATION canonical_json "$scratch/evaluation.json" "$scratch/evaluation.canonical" || diff --git a/control/v1/risk-gates-decision.json b/control/v1/risk-gates-decision.json index a085b7a..c00ebb6 100644 --- a/control/v1/risk-gates-decision.json +++ b/control/v1/risk-gates-decision.json @@ -1 +1 @@ -{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"276657b3955b4e2c661178375324435ba8894e9e8e531ace1f11eedb7255166d"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"b6b31e39276e49b95f8a41c75364be1b0470db36bae9e26a40f67a9cf11828ff"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","dependencies":{"duty_separation":{"decision_ref":{"content_id":"control-decision.duty-separation","media_type":"application/vnd.ystack.control-decision+json","sha256":"08f8b496a689ab6fefa976de495fa13e1f9d954cd2b33ee44acd3c70022b4697"},"policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"}}},"evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.risk-gates.v1","media_type":"text/x-shellscript","sha256":"0df2094a1a86901d5db8bd463cdeb295f455585b345096719bdc6dcd0b8852e8"},"program_ref":{"content_id":"control-evaluator-program.risk-gates.v1","media_type":"text/x-jq","sha256":"b6b31e39276e49b95f8a41c75364be1b0470db36bae9e26a40f67a9cf11828ff"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.risk-gates","media_type":"application/vnd.ystack.control-policy+json","sha256":"0286be22ec3d3a31be8e2c00c5e57a5b674c3f6b0e6d03c87a670e51e3141bed"},"semantics":{"authority_effect":"none","decision_claim_semantics":"immutable-input-claim-only","decision_provenance":"unqualified-input-claim","input_contract":"control-policy-set+public-core-stage-run+duty-evaluation+risk-decision-claim.v1","output_kind":"risk_gate_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","violated"]}},"id":"control-decision.risk-gates","kind":"risk_gates_decision","schema_version":1} diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index 3ce8971..6d217b2 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -337,9 +337,8 @@ build_case() { expect_eval() { local name=$1 verdict=$2 reason=$3 local dir="$tmp/$name" out="$tmp/$name/risk.out" - YSTACK_RISK_TEST_RELATION_STAGE=1 PATH="$bin:/usr/bin:/bin" \ - "$evaluator" evaluate "$policy_set" "$dir/request.json" "$resolved" \ - "$dir/result.json" "$dir/duty.json" "$dir/claim.json" \ + PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$dir/request.json" \ + "$resolved" "$dir/result.json" "$dir/duty.json" "$dir/claim.json" \ >"$out" 2>"$dir/risk.err" || { /bin/cat "$dir/risk.err" >&2 fail "$name status" @@ -602,6 +601,32 @@ status=0 [ "$(/bin/cat "$tmp/relative.err")" = E_RUNTIME ] || fail 'relative jq rejection' pass 'relative jq rejection' +strict_bin="$tmp/strict-empty-input-bin" +/bin/mkdir "$strict_bin" +/usr/bin/printf '%s\n' '#!/bin/bash' "real_jq='$jq_bin'" \ + 'if [ "${1:-}" = --version ]; then exec "$real_jq" "$@"; fi' \ + 'has_definition=0' \ + 'has_null_input=0' \ + 'for arg in "$@"; do' \ + ' [ "$arg" = definition ] && has_definition=1' \ + ' [ "$arg" = -n ] && has_null_input=1' \ + 'done' \ + 'if [ "$has_definition" -eq 1 ] && [ "$has_null_input" -ne 1 ]; then exit 4; fi' \ + 'exec "$real_jq" "$@"' >"$strict_bin/jq" +/bin/chmod 0555 "$strict_bin/jq" +PATH="$strict_bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ + "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ + "$tmp/routine/duty.json" "$tmp/routine/claim.json" >"$tmp/strict.out" \ + 2>"$tmp/strict.err" || { + /bin/cat "$tmp/strict.err" >&2 + fail 'strict empty-input jq status' + } +[ ! -s "$tmp/strict.err" ] || fail 'strict empty-input jq stderr' +"$jq_bin" -e '.body.verdict=="inconclusive" and + .body.reason_ids==["decision.provenance-unqualified"]' "$tmp/strict.out" \ + >/dev/null || fail 'strict empty-input jq result' +pass 'risk definition check uses explicit null input on strict jq' + copy_runtime() { local destination=$1 copy_path /bin/mkdir -p "$destination/control/v1" "$destination/scripts" "$destination/core" @@ -618,7 +643,7 @@ dependency_runtime="$tmp/dependency-runtime" copy_runtime "$dependency_runtime" /usr/bin/printf '\n' >>"$dependency_runtime/control/v1/duty-separation-decision.json" /bin/cp -R "$tmp/routine" "$tmp/dependency-binding" -expect_error dependency-binding E_DUTY "$dependency_runtime" +expect_error dependency-binding E_RELATION "$dependency_runtime" race_runtime="$tmp/race-runtime" copy_runtime "$race_runtime" From 1689d9de692796f620af46ddc36f668fd634b2c3 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 08:25:41 -0400 Subject: [PATCH 09/10] Bound and optimize risk evaluator tests --- scripts/test/control-risk-gates.test.sh | 484 +++++++++++++++--------- 1 file changed, 315 insertions(+), 169 deletions(-) diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index 6d217b2..ca03122 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -5,7 +5,7 @@ export LC_ALL=C umask 077 if [ "${YSTACK_RISK_TEST_BOUNDED:-0}" != 1 ]; then - YSTACK_RISK_TEST_BOUNDED=1 exec /usr/bin/perl -e 'alarm 420; exec @ARGV' "$0" + YSTACK_RISK_TEST_BOUNDED=1 exec /usr/bin/perl -e 'alarm 240; exec @ARGV' "$0" fi root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) @@ -17,8 +17,117 @@ duty_policy="$root/control/v1/duty-separation-policy.json" duty_definition="$root/control/v1/duty-separation-decision.json" core_wrapper="$root/scripts/core-contract.sh" tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-risk-test.XXXXXX") -cleanup() { /bin/rm -rf -- "$tmp"; } -trap cleanup EXIT HUP INT TERM +eval_tmp="$tmp/evaluator-scratch" +/bin/mkdir -p "$eval_tmp" +ACTIVE_EVAL_PID='' +ACTIVE_EVAL_PGID='' +LAUNCH_COUNTER=0 + +group_alive() { kill -0 -- "-$1" 2>/dev/null; } +clear_active() { ACTIVE_EVAL_PID=''; ACTIVE_EVAL_PGID=''; } +terminate_active() { + local attempt=0 + [ -n "$ACTIVE_EVAL_PID" ] && [ -n "$ACTIVE_EVAL_PGID" ] || return 0 + [[ "$ACTIVE_EVAL_PID" =~ ^[1-9][0-9]*$ ]] && + [ "$ACTIVE_EVAL_PID" = "$ACTIVE_EVAL_PGID" ] || return 1 + if kill -0 "$ACTIVE_EVAL_PID" 2>/dev/null; then + kill -TERM "$ACTIVE_EVAL_PID" 2>/dev/null || : + fi + if group_alive "$ACTIVE_EVAL_PGID"; then + kill -TERM -- "-$ACTIVE_EVAL_PGID" 2>/dev/null || : + fi + while group_alive "$ACTIVE_EVAL_PGID" && [ "$attempt" -lt 40 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.025 + done + if group_alive "$ACTIVE_EVAL_PGID"; then + kill -KILL -- "-$ACTIVE_EVAL_PGID" 2>/dev/null || : + fi + if kill -0 "$ACTIVE_EVAL_PID" 2>/dev/null; then + kill -KILL "$ACTIVE_EVAL_PID" 2>/dev/null || : + fi + attempt=0 + while group_alive "$ACTIVE_EVAL_PGID" && [ "$attempt" -lt 80 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.025 + done + wait "$ACTIVE_EVAL_PID" 2>/dev/null || : + group_alive "$ACTIVE_EVAL_PGID" && return 1 + clear_active +} +start_group() { + local out=$1 err=$2 attempt=0 ready + shift 2 + LAUNCH_COUNTER=$((LAUNCH_COUNTER + 1)) + ready="$tmp/launcher-ready-$LAUNCH_COUNTER" + /usr/bin/perl -e ' + use POSIX (); + my $ready = shift @ARGV; + POSIX::setpgid(0,0) == 0 or die "setpgid"; + open my $fh, ">", $ready or die "ready"; + print {$fh} "ready\n"; + close $fh or die "ready"; + exec @ARGV; + ' "$ready" "$@" >"$out" 2>"$err" & + ACTIVE_EVAL_PID=$! + ACTIVE_EVAL_PGID=$ACTIVE_EVAL_PID + while [ ! -s "$ready" ] && [ "$attempt" -lt 100 ]; do + kill -0 "$ACTIVE_EVAL_PID" 2>/dev/null || break + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + if [ ! -s "$ready" ]; then + terminate_active || : + return 126 + fi + /bin/rm -f -- "$ready" +} +wait_group() { + local timeout_seconds=$1 ticks=0 max_ticks status=0 + max_ticks=$((timeout_seconds * 20)) + while group_alive "$ACTIVE_EVAL_PGID" && [ "$ticks" -lt "$max_ticks" ]; do + ticks=$((ticks + 1)) + /bin/sleep 0.05 + done + if group_alive "$ACTIVE_EVAL_PGID"; then + terminate_active || return 125 + return 124 + fi + wait "$ACTIVE_EVAL_PID" 2>/dev/null || status=$? + clear_active + return "$status" +} +run_bounded() { + local timeout_seconds=$1 out=$2 err=$3 + shift 3 + start_group "$out" "$err" "$@" || return $? + wait_group "$timeout_seconds" +} +cleanup() { + local status=0 + terminate_active || status=1 + /bin/rm -rf -- "$tmp" + return "$status" +} +on_exit() { + local status=$? + trap - EXIT HUP INT TERM ALRM + cleanup || status=1 + exit "$status" +} +on_alarm() { + trap - ALRM + terminate_active || : + exit 124 +} +on_signal() { + trap - HUP INT TERM + terminate_active || : + exit 124 +} +trap on_exit EXIT +trap on_signal HUP INT TERM +trap on_alarm ALRM fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } passes=0 pass() { passes=$((passes + 1)); /usr/bin/printf 'ok %s - %s\n' "$passes" "$1"; } @@ -319,30 +428,39 @@ build_case() { scope_sha256:$request_basis_sha}] ' "$dir/request.basis" >"$dir/request.json" request_sha=$(sha256_path "$dir/request.json") - "$jq_bin" -L "$root/scripts/test" -S -c -n \ - --slurpfile request "$dir/request.json" --slurpfile resolved "$resolved" \ - --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" ' - import "portable-core-result-truth-fixtures" as result; - result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | - walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) - ' >"$dir/result.json" - PATH="$bin:/usr/bin:/bin" "$duty_evaluator" evaluate "$policy_set" \ - "$dir/request.json" "$resolved" "$dir/result.json" >"$dir/duty.json" \ - 2>"$dir/duty.err" || { + if [ "${PURE_CASE_BUILD:-0}" = 1 ]; then + /bin/cp "$tmp/routine/result.json" "$dir/result.json" + /bin/cp "$tmp/routine/duty.json" "$dir/duty.json" + else + "$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$dir/request.json" --slurpfile resolved "$resolved" \ + --arg request_sha "$request_sha" --arg resolved_sha "$resolved_sha" ' + import "portable-core-result-truth-fixtures" as result; + result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) + ' >"$dir/result.json" + if ! run_bounded 45 "$dir/duty.json" "$dir/duty.err" /usr/bin/env \ + "TMPDIR=$eval_tmp" "PATH=$bin:/usr/bin:/bin" \ + "$duty_evaluator" evaluate "$policy_set" \ + "$dir/request.json" "$resolved" "$dir/result.json"; then /bin/cat "$dir/duty.err" >&2 fail "build duty $name" - } + fi + fi } +build_pure_case() { PURE_CASE_BUILD=1 build_case "$@"; } + expect_eval() { local name=$1 verdict=$2 reason=$3 local dir="$tmp/$name" out="$tmp/$name/risk.out" - PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$dir/request.json" \ - "$resolved" "$dir/result.json" "$dir/duty.json" "$dir/claim.json" \ - >"$out" 2>"$dir/risk.err" || { + if ! run_bounded 45 "$out" "$dir/risk.err" /usr/bin/env \ + "TMPDIR=$eval_tmp" "PATH=$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ + "$dir/request.json" "$resolved" "$dir/result.json" "$dir/duty.json" \ + "$dir/claim.json"; then /bin/cat "$dir/risk.err" >&2 fail "$name status" - } + fi [ ! -s "$dir/risk.err" ] || fail "$name stderr" "$jq_bin" -e --arg verdict "$verdict" --arg reason "$reason" ' .kind=="risk_gate_evaluation" and .body.activation_state=="inactive" and @@ -356,6 +474,55 @@ expect_eval() { pass "$name" } +expect_pure_eval() { + local name=$1 verdict=$2 reason=$3 expected_minimum=${4:-} + local dir="$tmp/$name" out="$tmp/$name/risk.out" request_basis + local policy_set_digest request_digest result_digest duty_digest claim_digest + local request_basis_digest policy_scope_digest requirement_scope_digest + policy_set_digest=$(sha256_path "$policy_set") + request_digest=$(sha256_path "$dir/request.json") + result_digest=$(sha256_path "$dir/result.json") + duty_digest=$(sha256_path "$dir/duty.json") + claim_digest=$(sha256_path "$dir/claim.json") + request_basis=$("$jq_bin" -S -c \ + '{schema_version,kind,id,body:(.body|del(.gate_decision_refs))}' \ + "$dir/request.json") + request_basis_digest=$(sha256_text "$request_basis") + policy_scope_digest=$("$jq_bin" -er '.body.risk.policy_ref.scope_sha256' \ + "$dir/request.json") + requirement_scope_digest=$("$jq_bin" -er \ + '.body.risk.required_gate_refs[0].scope_sha256' "$dir/request.json") + if ! run_bounded 15 "$out" "$dir/risk.err" "$jq_bin" -S -c -n \ + -f "$root/control/v1/risk-gates.jq" --slurpfile policy "$policy" \ + --slurpfile decision "$definition" --slurpfile policy_set "$policy_set" \ + --slurpfile request "$dir/request.json" --slurpfile resolved "$resolved" \ + --slurpfile result "$dir/result.json" --slurpfile duty_evaluation "$dir/duty.json" \ + --slurpfile claim "$dir/claim.json" --arg policy_sha "$policy_sha" \ + --arg decision_sha "$definition_sha" --arg policy_set_sha "$policy_set_digest" \ + --arg request_sha "$request_digest" --arg resolved_sha "$resolved_sha" \ + --arg result_sha "$result_digest" --arg duty_sha "$duty_digest" \ + --arg claim_sha "$claim_digest" --arg request_basis_sha "$request_basis_digest" \ + --arg policy_scope_sha "$policy_scope_digest" \ + --arg requirement_scope_sha "$requirement_scope_digest"; then + /bin/cat "$dir/risk.err" >&2 + fail "$name pure status" + fi + [ ! -s "$dir/risk.err" ] || fail "$name pure stderr" + "$jq_bin" -e --arg verdict "$verdict" --arg reason "$reason" \ + --arg minimum "$expected_minimum" ' + .kind=="risk_gate_evaluation" and .body.activation_state=="inactive" and + .body.authority_effect=="none" and .body.evaluation_mode=="observation-only" and + .body.reference_semantics=="identity-only" and .body.verdict==$verdict and + (.body.reason_ids|index($reason)!=null) and + .body.reason_ids==(.body.reason_ids|sort|unique) and + ($minimum=="" or .body.classification.minimum_tier==$minimum) and + ((.body|has("grant_ref") or has("qualification_ref") or has("activation"))|not) + ' "$out" >/dev/null || fail "$name pure verdict" + "$jq_bin" -S -c . "$out" >"$dir/repeat" + /usr/bin/cmp -s "$out" "$dir/repeat" || fail "$name pure canonical output" + pass "$name" +} + expect_minimum() { local name=$1 expected=$2 "$jq_bin" -e --arg expected "$expected" ' @@ -366,10 +533,11 @@ expect_minimum() { expect_error() { local name=$1 expected=$2 runtime=${3:-$root} local dir="$tmp/$name" status=0 - PATH="$bin:/usr/bin:/bin" "$runtime/control/v1/evaluate-risk-gates.sh" evaluate \ + run_bounded 45 "$dir/error.out" "$dir/error.err" /usr/bin/env \ + "TMPDIR=$eval_tmp" "PATH=$bin:/usr/bin:/bin" \ + "$runtime/control/v1/evaluate-risk-gates.sh" evaluate \ "$policy_set" "$dir/request.json" "$resolved" "$dir/result.json" \ - "$dir/duty.json" "$dir/claim.json" >"$dir/error.out" 2>"$dir/error.err" || - status=$? + "$dir/duty.json" "$dir/claim.json" || status=$? [ "$status" -ne 0 ] && [ ! -s "$dir/error.out" ] && [ "$(/bin/cat "$dir/error.err")" = "$expected" ] || fail "$name error" pass "$name" @@ -379,165 +547,154 @@ build_case routine routine routine present independent-plan-check reviewer accep risk.routine expect_eval routine inconclusive decision.provenance-unqualified -build_case routine-escalated high routine present operator-plan-approval operator accept \ +build_pure_case routine-escalated high routine present operator-plan-approval operator accept \ risk.routine -expect_eval routine-escalated inconclusive decision.provenance-unqualified +expect_pure_eval routine-escalated inconclusive decision.provenance-unqualified build_case high high high present operator-plan-approval operator accept \ risk.security-control expect_eval high inconclusive decision.provenance-unqualified -build_case backdated-high high high present operator-plan-approval operator accept \ +build_pure_case backdated-high high high present operator-plan-approval operator accept \ risk.security-control normal core 2000-01-01T00:00:00Z -expect_eval backdated-high inconclusive decision.provenance-unqualified +expect_pure_eval backdated-high inconclusive decision.provenance-unqualified build_case bootstrap bootstrap bootstrap present operator-bootstrap-approval operator accept \ risk.bootstrap bootstrap expect_eval bootstrap inconclusive decision.provenance-unqualified -build_case missing high high absent ignored operator accept risk.security-control -expect_eval missing violated decision.missing +build_pure_case missing high high absent ignored operator accept risk.security-control +expect_pure_eval missing violated decision.missing build_case rejected high high present operator-plan-approval operator reject \ risk.security-control expect_eval rejected violated decision.rejected -build_case downgrade routine high present independent-plan-check reviewer accept \ +build_pure_case downgrade routine high present independent-plan-check reviewer accept \ risk.security-control -expect_eval downgrade violated risk.tier-downgrade +expect_pure_eval downgrade violated risk.tier-downgrade -build_case invented-role high high present operator-plan-approval reviewer accept \ +build_pure_case invented-role high high present operator-plan-approval reviewer accept \ risk.security-control -expect_eval invented-role violated decision.role-denied +expect_pure_eval invented-role violated decision.role-denied -build_case unbound-actor routine routine present independent-plan-check reviewer accept \ +build_pure_case unbound-actor routine routine present independent-plan-check reviewer accept \ risk.routine actor-mismatch -expect_eval unbound-actor violated decision.actor-unbound +expect_pure_eval unbound-actor violated decision.actor-unbound -build_case invented-kind high high present independent-plan-check operator accept \ +build_pure_case invented-kind high high present independent-plan-check operator accept \ risk.security-control -expect_eval invented-kind violated decision.kind-denied +expect_pure_eval invented-kind violated decision.kind-denied -build_case malformed-claim routine routine present independent-plan-check reviewer accept \ +build_pure_case malformed-claim routine routine present independent-plan-check reviewer accept \ risk.routine malformed-claim -expect_eval malformed-claim violated decision.claim-malformed +expect_pure_eval malformed-claim violated decision.claim-malformed -build_case decision-scalar routine routine present independent-plan-check reviewer accept \ +build_pure_case decision-scalar routine routine present independent-plan-check reviewer accept \ risk.routine decision-scalar -expect_eval decision-scalar violated decision.claim-malformed +expect_pure_eval decision-scalar violated decision.claim-malformed -build_case decision-array routine routine present independent-plan-check reviewer accept \ +build_pure_case decision-array routine routine present independent-plan-check reviewer accept \ risk.routine decision-array -expect_eval decision-array violated decision.claim-malformed +expect_pure_eval decision-array violated decision.claim-malformed -build_case decision-null routine routine present independent-plan-check reviewer accept \ +build_pure_case decision-null routine routine present independent-plan-check reviewer accept \ risk.routine decision-null -expect_eval decision-null violated decision.claim-malformed +expect_pure_eval decision-null violated decision.claim-malformed -build_case value-scalar routine routine present independent-plan-check reviewer accept \ +build_pure_case value-scalar routine routine present independent-plan-check reviewer accept \ risk.routine value-scalar -expect_eval value-scalar violated decision.claim-malformed +expect_pure_eval value-scalar violated decision.claim-malformed -build_case value-array routine routine present independent-plan-check reviewer accept \ +build_pure_case value-array routine routine present independent-plan-check reviewer accept \ risk.routine value-array -expect_eval value-array violated decision.claim-malformed +expect_pure_eval value-array violated decision.claim-malformed -build_case value-null routine routine present independent-plan-check reviewer accept \ +build_pure_case value-null routine routine present independent-plan-check reviewer accept \ risk.routine value-null -expect_eval value-null violated decision.claim-malformed +expect_pure_eval value-null violated decision.claim-malformed -build_case malformed-classification routine routine present independent-plan-check reviewer \ +build_pure_case malformed-classification routine routine present independent-plan-check reviewer \ accept risk.routine malformed-classification -expect_eval malformed-classification violated decision.claim-malformed +expect_pure_eval malformed-classification violated decision.claim-malformed unknown expect_minimum malformed-classification unknown -build_case tier-wrong-string routine routine present independent-plan-check reviewer accept \ +build_pure_case tier-wrong-string routine routine present independent-plan-check reviewer accept \ risk.routine tier-custom -expect_eval tier-wrong-string violated decision.claim-malformed +expect_pure_eval tier-wrong-string violated decision.claim-malformed unknown expect_minimum tier-wrong-string unknown -build_case tier-empty routine routine present independent-plan-check reviewer accept \ +build_pure_case tier-empty routine routine present independent-plan-check reviewer accept \ risk.routine tier-empty -expect_eval tier-empty violated decision.claim-malformed +expect_pure_eval tier-empty violated decision.claim-malformed unknown expect_minimum tier-empty unknown -build_case tier-number routine routine present independent-plan-check reviewer accept \ +build_pure_case tier-number routine routine present independent-plan-check reviewer accept \ risk.routine tier-number -expect_eval tier-number violated decision.claim-malformed +expect_pure_eval tier-number violated decision.claim-malformed unknown expect_minimum tier-number unknown -build_case tier-object routine routine present independent-plan-check reviewer accept \ +build_pure_case tier-object routine routine present independent-plan-check reviewer accept \ risk.routine tier-object -expect_eval tier-object violated decision.claim-malformed +expect_pure_eval tier-object violated decision.claim-malformed unknown expect_minimum tier-object unknown -build_case tier-null routine routine present independent-plan-check reviewer accept \ +build_pure_case tier-null routine routine present independent-plan-check reviewer accept \ risk.routine tier-null -expect_eval tier-null violated decision.claim-malformed +expect_pure_eval tier-null violated decision.claim-malformed unknown expect_minimum tier-null unknown -build_case forced-high-number high high present operator-plan-approval operator accept \ +build_pure_case forced-high-number high high present operator-plan-approval operator accept \ risk.security-control tier-number-forced -expect_eval forced-high-number violated decision.claim-malformed +expect_pure_eval forced-high-number violated decision.claim-malformed unknown expect_minimum forced-high-number unknown -build_case forced-high-object high high present operator-plan-approval operator accept \ +build_pure_case forced-high-object high high present operator-plan-approval operator accept \ risk.security-control tier-object-forced -expect_eval forced-high-object violated decision.claim-malformed +expect_pure_eval forced-high-object violated decision.claim-malformed unknown expect_minimum forced-high-object unknown -build_case forced-high-null high high present operator-plan-approval operator accept \ +build_pure_case forced-high-null high high present operator-plan-approval operator accept \ risk.security-control tier-null-forced -expect_eval forced-high-null violated decision.claim-malformed +expect_pure_eval forced-high-null violated decision.claim-malformed unknown expect_minimum forced-high-null unknown -build_case bootstrap-number bootstrap bootstrap present operator-bootstrap-approval operator \ +build_pure_case bootstrap-number bootstrap bootstrap present operator-bootstrap-approval operator \ accept risk.bootstrap tier-number-bootstrap -expect_eval bootstrap-number violated decision.claim-malformed +expect_pure_eval bootstrap-number violated decision.claim-malformed unknown expect_minimum bootstrap-number unknown -build_case bootstrap-object bootstrap bootstrap present operator-bootstrap-approval operator \ +build_pure_case bootstrap-object bootstrap bootstrap present operator-bootstrap-approval operator \ accept risk.bootstrap tier-object-bootstrap -expect_eval bootstrap-object violated decision.claim-malformed +expect_pure_eval bootstrap-object violated decision.claim-malformed unknown expect_minimum bootstrap-object unknown -build_case bootstrap-null bootstrap bootstrap present operator-bootstrap-approval operator \ +build_pure_case bootstrap-null bootstrap bootstrap present operator-bootstrap-approval operator \ accept risk.bootstrap tier-null-bootstrap -expect_eval bootstrap-null violated decision.claim-malformed +expect_pure_eval bootstrap-null violated decision.claim-malformed unknown expect_minimum bootstrap-null unknown -build_case malformed-time routine routine present independent-plan-check reviewer accept \ +build_pure_case malformed-time routine routine present independent-plan-check reviewer accept \ risk.routine normal core 2026-99-99T99:99:99Z -expect_eval malformed-time violated decision.claim-malformed +expect_pure_eval malformed-time violated decision.claim-malformed -build_case after-request routine routine present independent-plan-check reviewer accept \ +build_pure_case after-request routine routine present independent-plan-check reviewer accept \ risk.routine normal core 2026-09-01T00:00:01Z -expect_eval after-request violated decision.after-request +expect_pure_eval after-request violated decision.after-request -build_case stale routine routine present independent-plan-check reviewer accept risk.routine +build_pure_case stale routine routine present independent-plan-check reviewer accept risk.routine "$jq_bin" -S -c '.body.request_basis_sha256=("0"*64)' "$tmp/stale/claim.json" \ >"$tmp/stale/claim.changed" /bin/mv "$tmp/stale/claim.changed" "$tmp/stale/claim.json" -expect_eval stale violated decision.stale +expect_pure_eval stale violated decision.stale -build_case unbound routine routine present independent-plan-check reviewer accept risk.routine +build_pure_case unbound routine routine present independent-plan-check reviewer accept risk.routine "$jq_bin" -S -c '.body.gate_decision_refs[0].decision_record_ref.sha256=("0"*64)' \ "$tmp/unbound/request.json" >"$tmp/unbound/request.changed" /bin/mv "$tmp/unbound/request.changed" "$tmp/unbound/request.json" -unbound_request_sha=$(sha256_path "$tmp/unbound/request.json") -"$jq_bin" -L "$root/scripts/test" -S -c -n \ - --slurpfile request "$tmp/unbound/request.json" --slurpfile resolved "$resolved" \ - --arg request_sha "$unbound_request_sha" --arg resolved_sha "$resolved_sha" ' - import "portable-core-result-truth-fixtures" as result; - result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | - walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) -' >"$tmp/unbound/result.json" -PATH="$bin:/usr/bin:/bin" "$duty_evaluator" evaluate "$policy_set" \ - "$tmp/unbound/request.json" "$resolved" "$tmp/unbound/result.json" \ - >"$tmp/unbound/duty.json" 2>"$tmp/unbound/duty.err" || fail 'unbound duty rebuild' -expect_eval unbound violated decision.unbound - -build_case ambiguous routine routine present independent-plan-check reviewer accept risk.routine +expect_pure_eval unbound violated decision.unbound + +build_pure_case ambiguous routine routine present independent-plan-check reviewer accept risk.routine "$jq_bin" -S -c '.body.gate_decision_refs += [{purpose:"gate-decision", decision_record_ref:{content_id:"risk.claim.other", media_type:"application/vnd.ystack.risk-gate-decision-claim+json",sha256:("1"*64)}, @@ -545,28 +702,16 @@ build_case ambiguous routine routine present independent-plan-check reviewer acc .body.gate_decision_refs |= sort_by(.scope_sha256)' "$tmp/ambiguous/request.json" \ >"$tmp/ambiguous/request.changed" /bin/mv "$tmp/ambiguous/request.changed" "$tmp/ambiguous/request.json" -ambiguous_request_sha=$(sha256_path "$tmp/ambiguous/request.json") -"$jq_bin" -L "$root/scripts/test" -S -c -n \ - --slurpfile request "$tmp/ambiguous/request.json" --slurpfile resolved "$resolved" \ - --arg request_sha "$ambiguous_request_sha" --arg resolved_sha "$resolved_sha" ' - import "portable-core-result-truth-fixtures" as result; - result::completed_result_doc($request[0];$request_sha;$resolved[0];$resolved_sha) | - walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) -' >"$tmp/ambiguous/result.json" -PATH="$bin:/usr/bin:/bin" "$duty_evaluator" evaluate "$policy_set" \ - "$tmp/ambiguous/request.json" "$resolved" "$tmp/ambiguous/result.json" \ - >"$tmp/ambiguous/duty.json" 2>"$tmp/ambiguous/duty.err" || - fail 'ambiguous duty rebuild' -expect_eval ambiguous violated decision.ambiguous - -build_case unsupported custom routine present independent-plan-check reviewer accept \ +expect_pure_eval ambiguous violated decision.ambiguous + +build_pure_case unsupported custom routine present independent-plan-check reviewer accept \ risk.routine normal example.test -expect_eval unsupported violated risk.tier-unsupported +expect_pure_eval unsupported violated risk.tier-unsupported unknown expect_minimum unsupported unknown -build_case unsupported-foreign-high high high present operator-plan-approval operator accept \ +build_pure_case unsupported-foreign-high high high present operator-plan-approval operator accept \ risk.security-control normal example.test -expect_eval unsupported-foreign-high violated risk.tier-unsupported +expect_pure_eval unsupported-foreign-high violated risk.tier-unsupported unknown expect_minimum unsupported-foreign-high unknown build_case duty-violated routine routine present independent-plan-check reviewer accept \ @@ -582,9 +727,11 @@ expect_error forged-duty E_DUTY link="$tmp/request-link.json" /bin/ln -s "$tmp/routine/request.json" "$link" status=0 -PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" "$link" "$resolved" \ - "$tmp/routine/result.json" "$tmp/routine/duty.json" "$tmp/routine/claim.json" \ - >"$tmp/link.out" 2>"$tmp/link.err" || status=$? +run_bounded 10 "$tmp/link.out" "$tmp/link.err" /usr/bin/env \ + "TMPDIR=$eval_tmp" "PATH=$bin:/usr/bin:/bin" \ + "$evaluator" evaluate "$policy_set" "$link" "$resolved" \ + "$tmp/routine/result.json" "$tmp/routine/duty.json" "$tmp/routine/claim.json" || + status=$? [ "$status" -ne 0 ] && [ ! -s "$tmp/link.out" ] && [ "$(/bin/cat "$tmp/link.err")" = E_RUNTIME ] || fail 'symlink input rejection' pass 'symlink input rejection' @@ -593,10 +740,12 @@ relative_bin="$tmp/relative-bin" /bin/mkdir "$relative_bin" /bin/cp "$jq_bin" "$relative_bin/jq" status=0 -(cd "$relative_bin" && PATH=".:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ - "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ - "$tmp/routine/duty.json" "$tmp/routine/claim.json") \ - >"$tmp/relative.out" 2>"$tmp/relative.err" || status=$? +run_bounded 10 "$tmp/relative.out" "$tmp/relative.err" /usr/bin/env \ + "TMPDIR=$eval_tmp" /bin/bash -c ' + cd "$1" && PATH=".:/usr/bin:/bin" exec "$2" evaluate "$3" "$4" "$5" "$6" "$7" "$8" +' _ "$relative_bin" "$evaluator" "$policy_set" "$tmp/routine/request.json" \ + "$resolved" "$tmp/routine/result.json" "$tmp/routine/duty.json" \ + "$tmp/routine/claim.json" || status=$? [ "$status" -ne 0 ] && [ ! -s "$tmp/relative.out" ] && [ "$(/bin/cat "$tmp/relative.err")" = E_RUNTIME ] || fail 'relative jq rejection' pass 'relative jq rejection' @@ -614,19 +763,46 @@ strict_bin="$tmp/strict-empty-input-bin" 'if [ "$has_definition" -eq 1 ] && [ "$has_null_input" -ne 1 ]; then exit 4; fi' \ 'exec "$real_jq" "$@"' >"$strict_bin/jq" /bin/chmod 0555 "$strict_bin/jq" -PATH="$strict_bin:/usr/bin:/bin" "$evaluator" evaluate "$policy_set" \ +if ! run_bounded 45 "$tmp/strict.out" "$tmp/strict.err" /usr/bin/env \ + "TMPDIR=$eval_tmp" "PATH=$strict_bin:/usr/bin:/bin" \ + "$evaluator" evaluate "$policy_set" \ "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ - "$tmp/routine/duty.json" "$tmp/routine/claim.json" >"$tmp/strict.out" \ - 2>"$tmp/strict.err" || { + "$tmp/routine/duty.json" "$tmp/routine/claim.json"; then /bin/cat "$tmp/strict.err" >&2 fail 'strict empty-input jq status' - } +fi [ ! -s "$tmp/strict.err" ] || fail 'strict empty-input jq stderr' "$jq_bin" -e '.body.verdict=="inconclusive" and .body.reason_ids==["decision.provenance-unqualified"]' "$tmp/strict.out" \ >/dev/null || fail 'strict empty-input jq result' pass 'risk definition check uses explicit null input on strict jq' +timeout_helper="$tmp/timeout-helper.sh" +timeout_scratch="$tmp/forced-timeout-scratch" +timeout_child_file="$tmp/forced-timeout-child" +/usr/bin/printf '%s\n' '#!/bin/bash' \ + 'scratch=$1' \ + 'child_file=$2' \ + '/bin/mkdir -p "$scratch"' \ + '/bin/bash -c '\''trap "" TERM; /bin/sleep 30'\'' &' \ + 'child=$!' \ + '/usr/bin/printf "%s\n" "$child" >"$child_file"' \ + 'trap '\''/bin/rm -rf -- "$scratch"; exit 143'\'' TERM' \ + 'wait "$child"' >"$timeout_helper" +/bin/chmod 0555 "$timeout_helper" +timeout_status=0 +run_bounded 1 "$tmp/timeout.out" "$tmp/timeout.err" "$timeout_helper" \ + "$timeout_scratch" "$timeout_child_file" || timeout_status=$? +[ "$timeout_status" -eq 124 ] && [ -s "$timeout_child_file" ] && + [ ! -e "$timeout_scratch" ] && [ -z "$ACTIVE_EVAL_PID" ] && + [ -z "$ACTIVE_EVAL_PGID" ] || fail 'forced timeout cleanup state' +timeout_child=$(/bin/cat "$timeout_child_file") +if [[ ! "$timeout_child" =~ ^[1-9][0-9]*$ ]] || + kill -0 "$timeout_child" 2>/dev/null; then + fail 'forced timeout nested survivor' +fi +pass 'forced timeout reaps nested process group and scratch' + copy_runtime() { local destination=$1 copy_path /bin/mkdir -p "$destination/control/v1" "$destination/scripts" "$destination/core" @@ -666,64 +842,34 @@ race_trigger="$tmp/race-trigger" 'done' \ 'exec "$real_jq" "$@"' >"$race_bin/jq" /bin/chmod 0555 "$race_bin/jq" -( - status=0 - TMPDIR="$race_scratch" RISK_TRIGGER="$race_trigger" \ - PATH="$race_bin:/usr/bin:/bin" \ - "$race_runtime/control/v1/evaluate-risk-gates.sh" evaluate "$policy_set" \ - "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ - "$tmp/routine/duty.json" "$tmp/routine/claim.json" \ - >"$tmp/race.out" 2>"$tmp/race.err" || status=$? - /usr/bin/printf '%s\n' "$status" >"$tmp/race.status" -) & -race_pid=$! +start_group "$tmp/race.out" "$tmp/race.err" /usr/bin/env \ + "TMPDIR=$race_scratch" "RISK_TRIGGER=$race_trigger" \ + "PATH=$race_bin:/usr/bin:/bin" \ + "$race_runtime/control/v1/evaluate-risk-gates.sh" evaluate "$policy_set" \ + "$tmp/routine/request.json" "$resolved" "$tmp/routine/result.json" \ + "$tmp/routine/duty.json" "$tmp/routine/claim.json" || fail 'risk race launch' attempt=0 -while [ ! -e "$race_trigger" ] && kill -0 "$race_pid" 2>/dev/null && +while [ ! -e "$race_trigger" ] && group_alive "$ACTIVE_EVAL_PGID" && [ "$attempt" -lt 400 ]; do attempt=$((attempt + 1)) /bin/sleep 0.01 done if [ ! -e "$race_trigger" ]; then - kill -TERM "$race_pid" 2>/dev/null || : - wait "$race_pid" 2>/dev/null || : + terminate_active || : fail 'risk program race marker' fi /usr/bin/printf '\n' >>"$race_runtime/control/v1/risk-gates.jq" -wait "$race_pid" -[ "$(/bin/cat "$tmp/race.status")" -ne 0 ] && [ ! -s "$tmp/race.out" ] && +race_status=0 +wait_group 45 || race_status=$? +[ "$race_status" -ne 0 ] && [ ! -s "$tmp/race.out" ] && [ "$(/bin/cat "$tmp/race.err")" = E_RELATION ] || fail 'risk program TOCTOU' pass 'risk program TOCTOU closes after mirrored execution' -pure_dir="$tmp/routine" -pure_policy_set_sha=$(sha256_path "$policy_set") -pure_request_sha=$(sha256_path "$pure_dir/request.json") -pure_result_sha=$(sha256_path "$pure_dir/result.json") -pure_duty_sha=$(sha256_path "$pure_dir/duty.json") -pure_claim_sha=$(sha256_path "$pure_dir/claim.json") -pure_basis=$("$jq_bin" -S -c \ - '{schema_version,kind,id,body:(.body|del(.gate_decision_refs))}' \ - "$pure_dir/request.json") -pure_basis_sha=$(sha256_text "$pure_basis") -policy_scope "$pure_dir/request.json" "$tmp/pure-policy-scope" -pure_policy_scope_sha=$("$jq_bin" -r '.scope_sha256' "$tmp/pure-policy-scope") -requirement_scope "$pure_dir/request.json" routine "$tmp/pure-requirement-scope" -pure_requirement_sha=$("$jq_bin" -r '.scope_sha256' "$tmp/pure-requirement-scope") -"$jq_bin" -S -c -n -f "$root/control/v1/risk-gates.jq" \ - --slurpfile policy "$policy" --slurpfile decision "$definition" \ - --slurpfile policy_set "$policy_set" --slurpfile request "$pure_dir/request.json" \ - --slurpfile resolved "$resolved" --slurpfile result "$pure_dir/result.json" \ - --slurpfile duty_evaluation "$pure_dir/duty.json" \ - --slurpfile claim "$pure_dir/claim.json" --arg policy_sha "$policy_sha" \ - --arg decision_sha "$definition_sha" --arg policy_set_sha "$pure_policy_set_sha" \ - --arg request_sha "$pure_request_sha" --arg resolved_sha "$resolved_sha" \ - --arg result_sha "$pure_result_sha" --arg duty_sha "$pure_duty_sha" \ - --arg claim_sha "$pure_claim_sha" --arg request_basis_sha "$pure_basis_sha" \ - --arg policy_scope_sha "$pure_policy_scope_sha" \ - --arg requirement_scope_sha "$pure_requirement_sha" >"$tmp/pure.out" -"$jq_bin" -e '.body.verdict=="inconclusive" and - .body.reason_ids==["decision.provenance-unqualified"]' "$tmp/pure.out" >/dev/null || - fail 'pure evaluator' -pass 'pure evaluator keeps identity-only claim unqualified' +[ -z "$ACTIVE_EVAL_PID" ] && [ -z "$ACTIVE_EVAL_PGID" ] && + [ -z "$(/usr/bin/find "$eval_tmp" -mindepth 1 -print -quit)" ] && + [ -z "$(/usr/bin/find "$race_scratch" -mindepth 1 -print -quit)" ] || + fail 'evaluator process or scratch cleanup' +pass 'all evaluator groups and scratch are gone' while IFS= read -r risk_output; do "$jq_bin" -e '.body.verdict != "satisfied" and From 007eb84905b5e3f3853fadf44ac5831006dd4b24 Mon Sep 17 00:00:00 2001 From: ci Date: Tue, 1 Sep 2026 08:33:16 -0400 Subject: [PATCH 10/10] Allow Linux TOCTOU setup margin --- scripts/test/control-risk-gates.test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/test/control-risk-gates.test.sh b/scripts/test/control-risk-gates.test.sh index ca03122..bb865af 100755 --- a/scripts/test/control-risk-gates.test.sh +++ b/scripts/test/control-risk-gates.test.sh @@ -850,7 +850,7 @@ start_group "$tmp/race.out" "$tmp/race.err" /usr/bin/env \ "$tmp/routine/duty.json" "$tmp/routine/claim.json" || fail 'risk race launch' attempt=0 while [ ! -e "$race_trigger" ] && group_alive "$ACTIVE_EVAL_PGID" && - [ "$attempt" -lt 400 ]; do + [ "$attempt" -lt 2000 ]; do attempt=$((attempt + 1)) /bin/sleep 0.01 done