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 diff --git a/control/v1/evaluate-risk-gates.sh b/control/v1/evaluate-risk-gates.sh new file mode 100755 index 0000000..f95802d --- /dev/null +++ b/control/v1/evaluate-risk-gates.sh @@ -0,0 +1,376 @@ +#!/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 +"$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" +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" -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" \ + --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", + 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"]}} + } +' >/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) 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 + (.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 + +/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..c00ebb6 --- /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":"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/control/v1/risk-gates-policy.json b/control/v1/risk-gates-policy.json new file mode 100644 index 0000000..f7d8563 --- /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","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 new file mode 100644 index 0000000..5568eca --- /dev/null +++ b/control/v1/risk-gates.jq @@ -0,0 +1,287 @@ +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") 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", + "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 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: + "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", + "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 + .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) | +($claim_doc | claim_shape_ok) as $claim_valid | +(($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? // 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 | +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 $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[]; + . as $reason | $p.body.forced_high_reason_ids | index($reason) != null) + then "high" + else $claimed_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 $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) + + (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_valid and + ($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) + + (if $core_tier_supported and ($matching_rules | length) == 1 then [] + elif $core_tier_supported then ["risk.tier-downgrade"] + else ["risk.tier-unsupported"] end) + + (if $duty.body.verdict == "violated" then ["duty.violated"] else [] end) + + (if $claim_decision_state == "absent" then ["decision.missing"] + elif $claim_assertion == "reject" + then ["decision.rejected"] + else [] end) + + (if $claim_valid and $claim_decision_state == "present" and + $matching_rule != null then + (if $claim_decision_kind == $matching_rule.decision_kind + then [] else ["decision.kind-denied"] end) + + (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_actor) + then [] else ["decision.actor-unbound"] end) + + (if $claim_decision_time <= $request_doc.body.requested_at + then [] else ["decision.after-request"] end) + else [] end) | sort | unique) as $violations | + +((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:"inconclusive",reasons:["decision.provenance-unqualified"]} 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..bb865af --- /dev/null +++ b/scripts/test/control-risk-gates.test.sh @@ -0,0 +1,881 @@ +#!/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") +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"; } +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 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 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; + 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 ] || [ "$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 + "$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" --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=[] + ' "$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 + 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 asserted "$asserted" --arg recorded_at "$decision_time" \ + --argjson actor "$actor_json" ' + {state:"present",value:{asserted_decision:$asserted,decision_kind:$kind, + 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" \ + --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" + 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" + 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 ] || [ "$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 ] || [ "$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 ] || [ "$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" \ + --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") + 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" + 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 + .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_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" ' + .body.verdict=="violated" and .body.classification.minimum_tier==$expected + ' "$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 + 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" || 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 inconclusive decision.provenance-unqualified + +build_pure_case routine-escalated high routine present operator-plan-approval operator accept \ + risk.routine +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_pure_case backdated-high high high present operator-plan-approval operator accept \ + risk.security-control normal core 2000-01-01T00:00:00Z +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_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_pure_case downgrade routine high present independent-plan-check reviewer accept \ + risk.security-control +expect_pure_eval downgrade violated risk.tier-downgrade + +build_pure_case invented-role high high present operator-plan-approval reviewer accept \ + risk.security-control +expect_pure_eval invented-role violated decision.role-denied + +build_pure_case unbound-actor routine routine present independent-plan-check reviewer accept \ + risk.routine actor-mismatch +expect_pure_eval unbound-actor violated decision.actor-unbound + +build_pure_case invented-kind high high present independent-plan-check operator accept \ + risk.security-control +expect_pure_eval invented-kind violated decision.kind-denied + +build_pure_case malformed-claim routine routine present independent-plan-check reviewer accept \ + risk.routine malformed-claim +expect_pure_eval malformed-claim violated decision.claim-malformed + +build_pure_case decision-scalar routine routine present independent-plan-check reviewer accept \ + risk.routine decision-scalar +expect_pure_eval decision-scalar violated decision.claim-malformed + +build_pure_case decision-array routine routine present independent-plan-check reviewer accept \ + risk.routine decision-array +expect_pure_eval decision-array violated decision.claim-malformed + +build_pure_case decision-null routine routine present independent-plan-check reviewer accept \ + risk.routine decision-null +expect_pure_eval decision-null violated decision.claim-malformed + +build_pure_case value-scalar routine routine present independent-plan-check reviewer accept \ + risk.routine value-scalar +expect_pure_eval value-scalar violated decision.claim-malformed + +build_pure_case value-array routine routine present independent-plan-check reviewer accept \ + risk.routine value-array +expect_pure_eval value-array violated decision.claim-malformed + +build_pure_case value-null routine routine present independent-plan-check reviewer accept \ + risk.routine value-null +expect_pure_eval value-null violated decision.claim-malformed + +build_pure_case malformed-classification routine routine present independent-plan-check reviewer \ + accept risk.routine malformed-classification +expect_pure_eval malformed-classification violated decision.claim-malformed unknown +expect_minimum malformed-classification unknown + +build_pure_case tier-wrong-string routine routine present independent-plan-check reviewer accept \ + risk.routine tier-custom +expect_pure_eval tier-wrong-string violated decision.claim-malformed unknown +expect_minimum tier-wrong-string unknown + +build_pure_case tier-empty routine routine present independent-plan-check reviewer accept \ + risk.routine tier-empty +expect_pure_eval tier-empty violated decision.claim-malformed unknown +expect_minimum tier-empty unknown + +build_pure_case tier-number routine routine present independent-plan-check reviewer accept \ + risk.routine tier-number +expect_pure_eval tier-number violated decision.claim-malformed unknown +expect_minimum tier-number unknown + +build_pure_case tier-object routine routine present independent-plan-check reviewer accept \ + risk.routine tier-object +expect_pure_eval tier-object violated decision.claim-malformed unknown +expect_minimum tier-object unknown + +build_pure_case tier-null routine routine present independent-plan-check reviewer accept \ + risk.routine tier-null +expect_pure_eval tier-null violated decision.claim-malformed unknown +expect_minimum tier-null unknown + +build_pure_case forced-high-number high high present operator-plan-approval operator accept \ + risk.security-control tier-number-forced +expect_pure_eval forced-high-number violated decision.claim-malformed unknown +expect_minimum forced-high-number unknown + +build_pure_case forced-high-object high high present operator-plan-approval operator accept \ + risk.security-control tier-object-forced +expect_pure_eval forced-high-object violated decision.claim-malformed unknown +expect_minimum forced-high-object unknown + +build_pure_case forced-high-null high high present operator-plan-approval operator accept \ + risk.security-control tier-null-forced +expect_pure_eval forced-high-null violated decision.claim-malformed unknown +expect_minimum forced-high-null unknown + +build_pure_case bootstrap-number bootstrap bootstrap present operator-bootstrap-approval operator \ + accept risk.bootstrap tier-number-bootstrap +expect_pure_eval bootstrap-number violated decision.claim-malformed unknown +expect_minimum bootstrap-number unknown + +build_pure_case bootstrap-object bootstrap bootstrap present operator-bootstrap-approval operator \ + accept risk.bootstrap tier-object-bootstrap +expect_pure_eval bootstrap-object violated decision.claim-malformed unknown +expect_minimum bootstrap-object unknown + +build_pure_case bootstrap-null bootstrap bootstrap present operator-bootstrap-approval operator \ + accept risk.bootstrap tier-null-bootstrap +expect_pure_eval bootstrap-null violated decision.claim-malformed unknown +expect_minimum bootstrap-null unknown + +build_pure_case malformed-time routine routine present independent-plan-check reviewer accept \ + risk.routine normal core 2026-99-99T99:99:99Z +expect_pure_eval malformed-time violated decision.claim-malformed + +build_pure_case after-request routine routine present independent-plan-check reviewer accept \ + risk.routine normal core 2026-09-01T00:00:01Z +expect_pure_eval after-request violated decision.after-request + +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_pure_eval stale violated decision.stale + +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" +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)}, + 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" +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_pure_eval unsupported violated risk.tier-unsupported unknown +expect_minimum unsupported unknown + +build_pure_case unsupported-foreign-high high high present operator-plan-approval operator accept \ + risk.security-control normal example.test +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 \ + 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 +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' + +relative_bin="$tmp/relative-bin" +/bin/mkdir "$relative_bin" +/bin/cp "$jq_bin" "$relative_bin/jq" +status=0 +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' + +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" +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"; 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" + 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_RELATION "$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" +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" ] && group_alive "$ACTIVE_EVAL_PGID" && + [ "$attempt" -lt 2000 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 +done +if [ ! -e "$race_trigger" ]; then + terminate_active || : + fail 'risk program race marker' +fi +/usr/bin/printf '\n' >>"$race_runtime/control/v1/risk-gates.jq" +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' + +[ -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 + (.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"