diff --git a/README.md b/README.md index d1cd4e7..1629c4d 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,22 @@ The package stays inactive and fail-closed. It grants no authority, activates no profile, reads no credential, launches no adapter, and performs no external write. Later bounded units own each policy body and its enforcement. +## Inactive duty-separation evaluator + +`control/v1/evaluate-duty.sh` checks one public core v2 stage tuple against the +shipped duty-separation ceiling. It binds that policy to the validated policy set +by exact content identity. Its shipped decision binds the policy, evaluator driver, +evaluator program, policy-set validator driver/program, and complete selected +public-core package closure. Only private mirrored validator/core packages execute. +It keeps publisher dormant and compares producer, forge, verifier, reviewer, +requester, performer, and reporter identities. + +The canonical result is observation only: `satisfied`, `violated`, or +`inconclusive`. The evaluator grants no authority, activates nothing, reads no +credential, runs no candidate, and performs no network or external write. Sandbox, +credential, risk, kill-switch, evidence, and publisher enforcement remain later +Control foundation units. + ## 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 7bbcf2e..7a55bbe 100644 --- a/RESTORE.md +++ b/RESTORE.md @@ -330,6 +330,18 @@ The proof validates only the canonical six-section identity bundle. It does not evaluate a policy, grant authority, activate a profile, or enforce sandbox, credential, risk, kill-switch, or evidence behavior. +Restore the five paths in the manifest's inactive duty-separation block, then run: + +```sh +bash scripts/test/control-duty-separation.test.sh +``` + +This checks the exact policy and decision links, mirrored policy-set validator, +full selected public-core package closure, evaluator identities, role and permission +ceilings, all three identity-separation dimensions, dormant publisher behavior, and +canonical observation results. It does not enforce effective sandbox or credential +permissions and grants no authority or external write. + --- ## 5. Smoke test — prove the rebuilt team is alive diff --git a/ci/required-files.txt b/ci/required-files.txt index c099a43..a8c8a2b 100644 --- a/ci/required-files.txt +++ b/ci/required-files.txt @@ -171,3 +171,10 @@ scripts/test/portable-adapter-contracts.test.sh control/v1/policy-set.jq control/v1/validate.sh scripts/test/control-policy-set.test.sh + +# Inactive duty-separation policy and evaluator +control/v1/duty-separation-policy.json +control/v1/duty-separation-decision.json +control/v1/duty-separation.jq +control/v1/evaluate-duty.sh +scripts/test/control-duty-separation.test.sh diff --git a/control/v1/duty-separation-decision.json b/control/v1/duty-separation-decision.json new file mode 100644 index 0000000..f263902 --- /dev/null +++ b/control/v1/duty-separation-decision.json @@ -0,0 +1 @@ +{"body":{"activation_state":"inactive","decision":"allow-observation-only-evaluation","evaluator":{"driver_ref":{"content_id":"control-evaluator-driver.duty-separation.v1","media_type":"text/x-shellscript","sha256":"146e73dc880d363e889f32140ac375997fb709e3101de32b8d9603f1f38ca0fa"},"policy_set_validator":{"driver_ref":{"content_id":"control-policy-set-validator-driver.v1","media_type":"text/x-shellscript","sha256":"cf173ad0eaa08244bf636e3937845e894b21f14291fc5e66753e8673bdd2bd2a"},"program_ref":{"content_id":"control-policy-set-validator-program.v1","media_type":"text/x-jq","sha256":"2be97550574ee4522fc0bd14780c92dee3c1b455f2c04b7763b0e437665a8d58"}},"program_ref":{"content_id":"control-evaluator-program.duty-separation.v1","media_type":"text/x-jq","sha256":"ea977f36619a199d965c8f525c428dbc2f4d3a58418373529b8a79bc9488b076"}},"fail_mode":"closed","policy_ref":{"content_id":"control-policy.duty-separation","media_type":"application/vnd.ystack.control-policy+json","sha256":"b33a4022c74c8a1ccb06674c080adf2fc106561c426a7d1dd30fdb9865531dbb"},"semantics":{"authority_effect":"none","input_contract":"control-policy-set+public-core-stage-run.v1","output_kind":"duty_separation_evaluation","output_schema_version":1,"reference_semantics":"identity-only","verdicts":["inconclusive","satisfied","violated"]}},"id":"control-decision.duty-separation","kind":"duty_separation_decision","schema_version":1} diff --git a/control/v1/duty-separation-policy.json b/control/v1/duty-separation-policy.json new file mode 100644 index 0000000..a79d177 --- /dev/null +++ b/control/v1/duty-separation-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"},"dormant_roles":["ci","execution","identity","publisher"],"evaluation_mode":"observation-only","fail_mode":"closed","identity_dimensions":["adapter_instance_id","execution_boundary_id","principal_id"],"operation_ceilings":[{"capabilities":["core.forge.materialize-candidate.v2"],"execution_kind":"deterministic","permissions":["core.perm.candidate-repository.write.v2","core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"role":"forge"},{"capabilities":["core.harness.produce.v1"],"execution_kind":"deterministic","permissions":["core.perm.evidence.write.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"role":"producer"},{"capabilities":["core.harness.produce.v1"],"execution_kind":"model","permissions":["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.scratch.write.v1","core.perm.target.read.v1"],"role":"producer"},{"capabilities":["core.review.change.v1"],"execution_kind":"deterministic","permissions":["core.perm.evidence.write.v1","core.perm.target.read.v1"],"role":"reviewer"},{"capabilities":["core.review.change.v1"],"execution_kind":"model","permissions":["core.perm.evidence.write.v1","core.perm.model.invoke.v1","core.perm.target.read.v1"],"role":"reviewer"},{"capabilities":["core.verify.run.v1"],"execution_kind":"deterministic","permissions":["core.perm.candidate.execute.v1","core.perm.evidence.write.v1","core.perm.target.read.v1"],"role":"verifier"}],"policy_version":"v1","protected_roles":["forge","producer","publisher","reviewer","verifier"],"reporter_relation":"performer-else-selected","requester_roles":["manager","operator","orchestrator"]},"id":"control-policy.duty-separation","kind":"duty_separation_policy","schema_version":1} diff --git a/control/v1/duty-separation.jq b/control/v1/duty-separation.jq new file mode 100644 index 0000000..56540f8 --- /dev/null +++ b/control/v1/duty-separation.jq @@ -0,0 +1,200 @@ +def exact($fields): + type == "object" and (keys | sort) == ($fields | sort); + +def expected_roles: ["forge","producer","publisher","reviewer","verifier"]; +def expected_dormant: ["ci","execution","identity","publisher"]; +def expected_dimensions: + ["adapter_instance_id","execution_boundary_id","principal_id"]; +def expected_ceilings: + [ + {role:"forge",execution_kind:"deterministic", + capabilities:["core.forge.materialize-candidate.v2"], + permissions:["core.perm.candidate-repository.write.v2", + "core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"]}, + {role:"producer",execution_kind:"deterministic", + capabilities:["core.harness.produce.v1"], + permissions:["core.perm.evidence.write.v1","core.perm.scratch.write.v1", + "core.perm.target.read.v1"]}, + {role:"producer",execution_kind:"model", + capabilities:["core.harness.produce.v1"], + permissions:["core.perm.evidence.write.v1","core.perm.model.invoke.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"]}, + {role:"reviewer",execution_kind:"deterministic", + capabilities:["core.review.change.v1"], + permissions:["core.perm.evidence.write.v1","core.perm.target.read.v1"]}, + {role:"reviewer",execution_kind:"model", + capabilities:["core.review.change.v1"], + permissions:["core.perm.evidence.write.v1","core.perm.model.invoke.v1", + "core.perm.target.read.v1"]}, + {role:"verifier",execution_kind:"deterministic", + capabilities:["core.verify.run.v1"], + permissions:["core.perm.candidate.execute.v1","core.perm.evidence.write.v1", + "core.perm.target.read.v1"]} + ]; + +def policy_ok: + exact(["body","id","kind","schema_version"]) and + .schema_version == 1 and .kind == "duty_separation_policy" and + .id == "control-policy.duty-separation" and + (.body | + exact(["activation_state","core_contract","dormant_roles","evaluation_mode", + "fail_mode","identity_dimensions","operation_ceilings","policy_version", + "protected_roles","reporter_relation","requester_roles"]) and + .activation_state == "inactive" and .evaluation_mode == "observation-only" and + .fail_mode == "closed" and .policy_version == "v1" and + .core_contract == { + 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"} + } and + .protected_roles == expected_roles and .dormant_roles == expected_dormant and + .identity_dimensions == expected_dimensions and + .requester_roles == ["manager","operator","orchestrator"] and + .reporter_relation == "performer-else-selected" and + .operation_ceilings == expected_ceilings); + +def identity: + {role,adapter_instance_id,execution_boundary_id,principal_id}; + +def ceiling_for($policy; $role; $execution): + [$policy.body.operation_ceilings[] | + select(.role == $role and .execution_kind == $execution)]; + +def binding_within_ceiling($policy): + .binding as $binding | + ceiling_for($policy;$binding.role;$binding.execution_kind) as $ceilings | + ($ceilings | length) == 1 and + $binding.requested_capabilities == $ceilings[0].capabilities and + $binding.requested_permissions == $ceilings[0].permissions; + +def dormant_ok($policy): + .binding as $binding | + ($policy.body.dormant_roles | index($binding.role)) != null and + $binding.execution_kind == "deterministic" and + $binding.requested_capabilities == [] and $binding.requested_permissions == []; + +def unique_dimension($bindings; $dimension): + ($bindings | map(.binding[$dimension])) as $values | + ($values | length) == ($values | unique | length); + +def document_ref($document; $digest): + {schema_version:$document.schema_version,kind:$document.kind, + id:$document.id,sha256:$digest}; + +($policy[0]) as $p | +($decision[0]) as $decision_doc | +($policy_set[0]) as $set | +($request[0]) as $request_doc | +($resolved[0]) as $resolved_doc | +($result[0]) as $result_doc | +(if ($p | policy_ok) then true else error("invalid shipped policy") end) | +($resolved_doc.body.bindings) as $bindings | +([$bindings[] | select(.binding.role as $role | + $p.body.protected_roles | index($role) != null)]) as $protected | +([$bindings[] | select( + .binding.binding_id == $request_doc.body.operation.binding_id and + .binding.role == $request_doc.body.operation.role)]) as $selected | +($selected[0] // null) as $selected_binding | +([$bindings[] | select(.binding.role == "publisher")][0] // null) as $publisher | +($request_doc.body.requested_by | identity) as $requester_identity | + +((if ($protected | map(.binding.role) | sort) == expected_roles + then [] else ["protected.roles-invalid"] end) + + (expected_dimensions | map(. as $dimension | + if unique_dimension($protected;$dimension) then empty + else "protected." + $dimension + "-collision" end)) + + [$bindings[] | select( + (.binding.role as $role | ($p.body.protected_roles + $p.body.dormant_roles) | + index($role) == null)) | "profile.role-denied"] + + [$bindings[] | select(.binding.role != "publisher" and + (.binding.role as $role | $p.body.dormant_roles | index($role) == null) and + (binding_within_ceiling($p) | not)) | "operation.ceiling-denied"] + + (if $publisher != null and ($publisher | dormant_ok($p)) + then [] else ["publisher.not-dormant"] end) + + [$bindings[] | select(.binding.role != "publisher" and + (.binding.role as $role | $p.body.dormant_roles | index($role) != null) and + (dormant_ok($p) | not)) | "dormant.not-dormant"] + + (if $p.body.requester_roles | index($request_doc.body.requested_by.role) + then [] else ["requester.role-denied"] end) + + (expected_dimensions | map(. as $dimension | + if all($protected[];.binding[$dimension] != $requester_identity[$dimension]) + then empty else "requester." + $dimension + "-collision" end)) + + (if ($selected | length) != 1 then ["operation.role-denied"] + elif $request_doc.body.operation.role == "publisher" then ["publisher.requested"] + else + ceiling_for($p;$request_doc.body.operation.role; + $selected_binding.binding.execution_kind) as $ceilings | + (if ($ceilings | length) == 1 then [] else ["operation.execution-kind-denied"] end) + + (if ($ceilings | length) == 1 and + $ceilings[0].capabilities == [$request_doc.body.operation.capability_id] + then [] else ["operation.capability-denied"] end) + + (if ($ceilings | length) == 1 and + $ceilings[0].permissions == $request_doc.body.operation.permissions + then [] else ["operation.permissions-denied"] end) + end) + + (if $selected_binding == null then [] + elif $result_doc.body | has("execution") then + ($selected_binding.binding | identity) as $selected_identity | + ($result_doc.body.execution.actual_binding | identity) as $actual | + ($result_doc.body.execution.performer | identity) as $performer | + ($result_doc.body.reported_by | identity) as $reporter | + (ceiling_for($p;$actual.role; + $result_doc.body.execution.actual_binding.execution_kind)) as $actual_ceilings | + (expected_dimensions + ["role"] | map(. as $dimension | + if $actual[$dimension] == $selected_identity[$dimension] then empty + else "actual." + $dimension + "-mismatch" end)) + + (if $result_doc.body.execution.actual_binding.execution_kind == + $selected_binding.binding.execution_kind + then [] else ["actual.execution-kind-mismatch"] end) + + (expected_dimensions + ["role"] | map(. as $dimension | + if $performer[$dimension] == $actual[$dimension] then empty + else "performer." + $dimension + "-mismatch" end)) + + (expected_dimensions + ["role"] | map(. as $dimension | + if $reporter[$dimension] == $performer[$dimension] then empty + else "reporter." + $dimension + "-mismatch" end)) + + (if $result_doc.body.execution.used_capability.kind == "unclassified" then [] + elif ($actual_ceilings | length) == 1 and + $result_doc.body.execution.used_capability == + {kind:"registered",id:$request_doc.body.operation.capability_id} and + $actual_ceilings[0].capabilities == + [$result_doc.body.execution.used_capability.id] + then [] else ["actual.capability-mismatch"] end) + + (if ($actual_ceilings | length) == 1 and + $actual_ceilings[0].permissions == $request_doc.body.operation.permissions + then [] else ["actual.permissions-mismatch"] end) + else + ($selected_binding.binding | identity) as $selected_identity | + ($result_doc.body.reported_by | identity) as $reporter | + (expected_dimensions + ["role"] | map(. as $dimension | + if $reporter[$dimension] == $selected_identity[$dimension] then empty + else "reporter." + $dimension + "-mismatch" end)) + end) | sort | unique) as $violations | +(if ($result_doc.body | has("execution")) and + $result_doc.body.execution.used_capability.kind == "unclassified" + then ["actual.capability-unclassified"] else [] end) as $unknowns | +(if ($violations | length) > 0 then {verdict:"violated",reasons:$violations} + elif ($unknowns | length) > 0 then {verdict:"inconclusive",reasons:$unknowns} + else {verdict:"satisfied",reasons:["duty.satisfied"]} end) as $decision | +{ + schema_version:1, + kind:"duty_separation_evaluation", + id:$result_doc.id, + body:{ + activation_state:"inactive", + evaluation_mode:"observation-only", + reference_semantics:"identity-only", + policy_set:{id:$set.id,sha256:$policy_set_sha}, + policy_ref:$decision_doc.body.policy_ref, + decision_ref:{content_id:$decision_doc.id, + media_type:"application/vnd.ystack.control-decision+json",sha256:$decision_sha}, + core_contract:$set.body.core_contract, + 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:$decision.verdict, + reason_ids:$decision.reasons + } +} diff --git a/control/v1/evaluate-duty.sh b/control/v1/evaluate-duty.sh new file mode 100755 index 0000000..8554236 --- /dev/null +++ b/control/v1/evaluate-duty.sh @@ -0,0 +1,424 @@ +#!/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_POLICY_SET|E_CORE|E_RELATION) + /usr/bin/printf '%s\n' "$1" >&2 + ;; + *) /usr/bin/printf '%s\n' E_RUNTIME >&2 ;; + esac + exit 1 +} + +[ "$#" -eq 5 ] && [ "$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-duty.sh" ] || emit_error E_RUNTIME +repo=$(CDPATH='' cd -P -- "$source_dir/../.." 2>/dev/null && pwd -P) || + emit_error E_RUNTIME +policy="$source_dir/duty-separation-policy.json" +decision="$source_dir/duty-separation-decision.json" +program="$source_dir/duty-separation.jq" +policy_validator="$source_dir/validate.sh" +validator_program="$source_dir/policy-set.jq" +core_validator="$repo/scripts/core-contract.sh" +for required in "$source_path" "$policy" "$decision" "$program" "$core_validator"; 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}' +} +validator_pair_ok() { + local pair_dir=$1 driver=$2 validator_jq=$3 expected_driver=$4 expected_program=$5 + local physical_dir + [ -d "$pair_dir" ] && [ ! -L "$pair_dir" ] || return 1 + physical_dir=$(CDPATH='' cd -P -- "$pair_dir" 2>/dev/null && pwd -P) || return 1 + [ "$physical_dir" = "$pair_dir" ] && + [ "$driver" = "$pair_dir/validate.sh" ] && + [ "$validator_jq" = "$pair_dir/policy-set.jq" ] && + [ -f "$driver" ] && [ ! -L "$driver" ] && + [ -f "$validator_jq" ] && [ ! -L "$validator_jq" ] && + [ "$(sha256_path "$driver")" = "$expected_driver" ] && + [ "$(sha256_path "$validator_jq")" = "$expected_program" ] +} +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" +} +core_closure_sha() { + local closure_root=$1 wrapper=$2 selected=$3 tag=$4 + local major registry_rel registry generation_rel generation_root physical_scripts wrapper_parent + local canonical members descriptor relative digest required_dir required_file + local root_count module_count major_count selected_sha + local -a closure_paths + major_count=$(/usr/bin/grep -Ec \ + '^[[:space:]]*PORTABLE_CORE_SCHEMA_MAJOR=' "$wrapper") || return 1 + [ "$major_count" -eq 1 ] || return 1 + major=$(/usr/bin/sed -n \ + "s/^PORTABLE_CORE_SCHEMA_MAJOR='\([12]\)'$/\1/p" "$wrapper") || return 1 + [ "$major" = 2 ] || return 1 + registry_rel="core/v$major/generation-registry.json" + registry="$closure_root/$registry_rel" + generation_rel="core/v$major/generations/$selected" + generation_root="$closure_root/$generation_rel" + for required_dir in "$closure_root" "$closure_root/scripts" "$closure_root/core" \ + "$closure_root/core/v$major" "$closure_root/core/v$major/generations" \ + "$generation_root" "$generation_root/modules"; do + [ -d "$required_dir" ] && [ ! -L "$required_dir" ] || return 1 + done + physical_scripts=$(CDPATH='' cd -P -- "$closure_root/scripts" 2>/dev/null && pwd -P) || + return 1 + wrapper_parent=$(CDPATH='' cd -P -- "${wrapper%/*}" 2>/dev/null && pwd -P) || return 1 + [ "$physical_scripts" = "$closure_root/scripts" ] && + [ "$wrapper_parent" = "$physical_scripts" ] && + [ "$wrapper" = "$closure_root/scripts/core-contract.sh" ] || return 1 + [ -f "$registry" ] && [ ! -L "$registry" ] || return 1 + root_count=$(/usr/bin/find "$generation_root" -mindepth 1 -maxdepth 1 \ + -print 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') || return 1 + module_count=$(/usr/bin/find "$generation_root/modules" -mindepth 1 -maxdepth 1 \ + -print 2>/dev/null | /usr/bin/wc -l | /usr/bin/tr -d ' ') || return 1 + [ "$root_count" -eq 3 ] && [ "$module_count" -eq 5 ] || return 1 + canonical="$scratch/core-registry-$tag.canonical" + "$jq_bin" -s -S -c 'if length==1 then .[0] else error("root-count") end' \ + "$registry" >"$canonical" 2>/dev/null || return 1 + /usr/bin/cmp -s "$registry" "$canonical" || return 1 + "$jq_bin" -e --arg selected "$selected" ' + type=="array" and length>=1 and + all(.[];type=="object" and + (keys|sort)==["authorization_comment_id","concern","generation_id", + "parent_generation_id","semantic_identity"] and + (.authorization_comment_id|type)=="number" and + (.concern|type)=="string" and + (.generation_id|type=="string" and test("\\Ag-[0-9a-f]{64}\\z")) and + (.parent_generation_id|type=="string" and test("\\Ag-[0-9a-f]{64}\\z")) and + (.semantic_identity|type=="string" and test("\\Acore\\.contracts\\.v[1-9][0-9]*\\z"))) and + (map(.generation_id)|length)==(map(.generation_id)|unique|length) and + ([.[]|select(.generation_id==$selected and .semantic_identity=="core.contracts.v2")]|length)==1 + ' "$registry" >/dev/null 2>&1 || return 1 + closure_paths=( + scripts/core-contract.sh + "$registry_rel" + "$generation_rel/contracts.jq" + "$generation_rel/core-ingress.sh" + "$generation_rel/modules/profile_graph.jq" + "$generation_rel/modules/result_facts.jq" + "$generation_rel/modules/result_truth.jq" + "$generation_rel/modules/schema.jq" + "$generation_rel/modules/stage_request.jq" + ) + members="$scratch/core-members-$tag.tsv" + : >"$members" || return 1 + for relative in "${closure_paths[@]}"; do + required_file="$closure_root/$relative" + [ -f "$required_file" ] && [ ! -L "$required_file" ] || return 1 + digest=$(sha256_path "$required_file") || return 1 + /usr/bin/printf '%s\t%s\n' "$relative" "$digest" >>"$members" || return 1 + done + selected_sha=$(sha256_text "$selected") || return 1 + descriptor=$("$jq_bin" -Rn -S -c --arg selected_sha "$selected_sha" ' + [inputs|split("\t")|{path:.[0],sha256:.[1]}] as $members | + {schema_version:1,kind:"core_contract_package_closure", + semantic_identity:"core.contracts.v2", + selected_generation_id_sha256:$selected_sha,members:$members} + ' <"$members") || return 1 + /usr/bin/printf '%s\n' "$descriptor" >"$scratch/core-closure-$tag.json" || return 1 + sha256_text "$descriptor" +} + +scratch=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-duty.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; } +snapshot_fixed() { + local source=$1 target=$2 fixed_size + /bin/dd if="$source" of="$target" bs=1048577 count=1 2>/dev/null || + emit_error E_RUNTIME + fixed_size=$(/usr/bin/wc -c <"$target" | /usr/bin/tr -d ' ') || + emit_error E_RUNTIME + [ "$fixed_size" -le 1048576 ] || emit_error E_LIMIT +} +build_core_mirror() { + local selected=$1 mirror="$scratch/core-package" relative source target mirror_size + local -a mirror_paths + /bin/mkdir -p "$mirror/scripts" \ + "$mirror/core/v2/generations/$selected/modules" || return 1 + mirror_paths=( + 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" + ) + for relative in "${mirror_paths[@]}"; do + source="$repo/$relative" + target="$mirror/$relative" + /bin/dd if="$source" of="$target" bs=1048577 count=1 2>/dev/null || return 1 + mirror_size=$(/usr/bin/wc -c <"$target" | /usr/bin/tr -d ' ') || return 1 + [ "$mirror_size" -le 1048576 ] || return 1 + done + /bin/chmod 0500 "$mirror/scripts/core-contract.sh" || return 1 + /usr/bin/printf '%s\n' "$mirror" +} +build_validator_mirror() { + local mirror="$scratch/policy-validator/control/v1" source target mirror_size + /bin/mkdir -p "$mirror" || return 1 + for source in "$policy_validator" "$validator_program"; do + target="$mirror/${source##*/}" + /bin/dd if="$source" of="$target" bs=1048577 count=1 2>/dev/null || return 1 + mirror_size=$(/usr/bin/wc -c <"$target" | /usr/bin/tr -d ' ') || return 1 + [ "$mirror_size" -le 1048576 ] || return 1 + done + /bin/chmod 0500 "$mirror/validate.sh" || return 1 + /usr/bin/printf '%s\n' "$mirror" +} +trap cleanup EXIT +trap signal_exit HUP INT TERM +names=(policy-set request resolved result) +index=0 +for input in "$@"; do + snapshot="$scratch/${names[$index]}.json" + /bin/dd if="$input" of="$snapshot" bs=1048577 count=1 2>/dev/null || + emit_error E_RUNTIME + size=$(/usr/bin/wc -c <"$snapshot" | /usr/bin/tr -d ' ') || emit_error E_RUNTIME + [ "$size" -le 1048576 ] || emit_error E_LIMIT + index=$((index + 1)) +done +snapshot_fixed "$policy" "$scratch/policy.json" +snapshot_fixed "$decision" "$scratch/decision.json" +snapshot_fixed "$program" "$scratch/program.jq" + +for static_name in policy decision; do + "$jq_bin" -s -S -c 'if length==1 then .[0] else error("root-count") end' \ + "$scratch/$static_name.json" >"$scratch/$static_name.canonical" 2>/dev/null || + emit_error E_RELATION + /usr/bin/cmp -s "$scratch/$static_name.json" "$scratch/$static_name.canonical" || + emit_error E_RELATION +done +for control_dir in "$repo/control" "$repo/control/v1"; do + [ -d "$control_dir" ] && [ ! -L "$control_dir" ] || emit_error E_RELATION +done +[ "$source_dir" = "$repo/control/v1" ] || emit_error E_RELATION +policy_sha=$(sha256_path "$scratch/policy.json") || emit_error E_RUNTIME +decision_sha=$(sha256_path "$scratch/decision.json") || emit_error E_RUNTIME +program_sha=$(sha256_path "$scratch/program.jq") || emit_error E_RUNTIME +driver_sha=$(sha256_path "$source_path") || emit_error E_RUNTIME +validator_driver_sha=$(sha256_path "$policy_validator") || emit_error E_RELATION +validator_program_sha=$(sha256_path "$validator_program") || emit_error E_RELATION +validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || emit_error E_RELATION +mirror_validator_dir=$(build_validator_mirror) || emit_error E_RELATION +mirror_policy_validator="$mirror_validator_dir/validate.sh" +mirror_validator_program="$mirror_validator_dir/policy-set.jq" +validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha" || + emit_error E_RELATION +"$jq_bin" -n -e --arg policy_sha "$policy_sha" --arg driver_sha "$driver_sha" \ + --arg program_sha "$program_sha" --arg validator_driver_sha "$validator_driver_sha" \ + --arg validator_program_sha "$validator_program_sha" \ + --slurpfile policy "$scratch/policy.json" \ + --slurpfile decision "$scratch/decision.json" ' + $decision[0] == { + schema_version:1,kind:"duty_separation_decision", + id:"control-decision.duty-separation", + body:{activation_state:"inactive",decision:"allow-observation-only-evaluation", + fail_mode:"closed", + policy_ref:{content_id:$policy[0].id, + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha}, + evaluator:{ + driver_ref:{content_id:"control-evaluator-driver.duty-separation.v1", + media_type:"text/x-shellscript",sha256:$driver_sha}, + program_ref:{content_id:"control-evaluator-program.duty-separation.v1", + media_type:"text/x-jq",sha256:$program_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}}}, + semantics:{authority_effect:"none", + input_contract:"control-policy-set+public-core-stage-run.v1", + output_kind:"duty_separation_evaluation",output_schema_version:1, + reference_semantics:"identity-only", + verdicts:["inconclusive","satisfied","violated"]}} + } +' >/dev/null 2>&1 || emit_error E_RELATION +: >"$scratch/policy-validator-ready" +policy_status=0 +PATH="${jq_bin%/*}:/usr/bin:/bin" "$mirror_policy_validator" validate \ + "$scratch/policy-set.json" >"$scratch/policy.out" 2>"$scratch/policy.err" || + policy_status=$? +: >"$scratch/policy-validator-complete" +if ! validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || + ! validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha"; then + emit_error E_RELATION +fi +[ "$policy_status" -eq 0 ] || emit_error E_POLICY_SET +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 +selected_generation=$(selected_core_generation "$core_validator") || emit_error E_RELATION +generation_id=$("$jq_bin" -er '.body.core_contract.generation_id' \ + "$scratch/policy-set.json" 2>/dev/null) || emit_error E_RELATION +[ "$generation_id" = "$selected_generation" ] || emit_error E_RELATION +generation_id_sha=$(sha256_text "$generation_id") || emit_error E_RUNTIME +live_core_package_sha=$(core_closure_sha \ + "$repo" "$core_validator" "$selected_generation" live-pre) || emit_error E_RELATION +mirror_root=$(build_core_mirror "$selected_generation") || emit_error E_RELATION +mirror_validator="$mirror_root/scripts/core-contract.sh" +mirror_generation=$(selected_core_generation "$mirror_validator") || emit_error E_RELATION +[ "$mirror_generation" = "$selected_generation" ] || emit_error E_RELATION +core_package_sha=$(core_closure_sha \ + "$mirror_root" "$mirror_validator" "$mirror_generation" mirror) || emit_error E_RELATION +[ "$core_package_sha" = "$live_core_package_sha" ] || emit_error E_RELATION + +"$jq_bin" -e --arg policy_sha "$policy_sha" \ + --arg decision_sha "$decision_sha" \ + --arg generation_id_sha "$generation_id_sha" \ + --arg core_package_sha "$core_package_sha" \ + --slurpfile policy "$scratch/policy.json" \ + --slurpfile decision "$scratch/decision.json" ' + .body.core_contract.semantic_identity == $policy[0].body.core_contract.semantic_identity and + $generation_id_sha == $policy[0].body.core_contract.generation_id_sha256 and + .body.core_contract.package_ref == $policy[0].body.core_contract.package_ref and + $core_package_sha == $policy[0].body.core_contract.package_ref.sha256 and + ([.body.sections[] | select(.section_id == "duty-separation")] | length) == 1 and + ([.body.sections[] | select(.section_id == "duty-separation")][0]) as $section | + ($section.policy_ref == { + content_id:$policy[0].id, + media_type:"application/vnd.ystack.control-policy+json", + sha256:$policy_sha + }) and + ($section.decision_ref == { + content_id:$decision[0].id, + media_type:"application/vnd.ystack.control-decision+json", + sha256:$decision_sha + }) +' "$scratch/policy-set.json" >/dev/null 2>&1 || emit_error E_RELATION + +core_status=0 +run_status=0 +PATH="${jq_bin%/*}:/usr/bin:/bin" "$mirror_validator" validate-document \ + "$scratch/resolved.json" >"$scratch/core.out" 2>"$scratch/core.err" || core_status=$? +if [ "$core_status" -eq 0 ]; then + PATH="${jq_bin%/*}:/usr/bin:/bin" "$mirror_validator" validate-stage-run \ + "$scratch/request.json" "$scratch/resolved.json" "$scratch/result.json" \ + >"$scratch/run.out" 2>"$scratch/run.err" || run_status=$? +else + run_status=1 +fi +post_selected_generation=$(selected_core_generation "$core_validator") || emit_error E_RELATION +[ "$post_selected_generation" = "$selected_generation" ] || emit_error E_RELATION +post_core_package_sha=$(core_closure_sha \ + "$repo" "$core_validator" "$post_selected_generation" live-post) || emit_error E_RELATION +post_mirror_generation=$(selected_core_generation "$mirror_validator") || emit_error E_RELATION +[ "$post_mirror_generation" = "$mirror_generation" ] || emit_error E_RELATION +post_mirror_package_sha=$(core_closure_sha \ + "$mirror_root" "$mirror_validator" "$post_mirror_generation" mirror-post) || + emit_error E_RELATION +[ "$post_core_package_sha" = "$live_core_package_sha" ] && + [ "$post_mirror_package_sha" = "$core_package_sha" ] || emit_error E_RELATION +if ! validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || + ! validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha"; then + emit_error E_RELATION +fi +[ "$core_status" -eq 0 ] && [ "$run_status" -eq 0 ] || emit_error E_CORE +[ "$(sha256_path "$policy")" = "$policy_sha" ] && + [ "$(sha256_path "$decision")" = "$decision_sha" ] && + [ "$(sha256_path "$program")" = "$program_sha" ] && + [ "$(sha256_path "$source_path")" = "$driver_sha" ] || emit_error E_RELATION + +"$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" \ + --arg policy_set_sha "$policy_set_sha" --arg decision_sha "$decision_sha" \ + --arg resolved_sha "$resolved_sha" --arg request_sha "$request_sha" \ + --arg result_sha "$result_sha" >"$scratch/evaluation.json" || emit_error E_RUNTIME +[ "$(sha256_path "$policy")" = "$policy_sha" ] && + [ "$(sha256_path "$decision")" = "$decision_sha" ] && + [ "$(sha256_path "$program")" = "$program_sha" ] && + [ "$(sha256_path "$source_path")" = "$driver_sha" ] || emit_error E_RELATION +if ! validator_pair_ok "$source_dir" "$policy_validator" "$validator_program" \ + "$validator_driver_sha" "$validator_program_sha" || + ! validator_pair_ok "$mirror_validator_dir" "$mirror_policy_validator" \ + "$mirror_validator_program" "$validator_driver_sha" "$validator_program_sha"; then + emit_error E_RELATION +fi +"$jq_bin" -s -S -c 'if length==1 then .[0] else error("root-count") end' \ + "$scratch/evaluation.json" >"$scratch/evaluation.canonical" 2>/dev/null || + emit_error E_RUNTIME +/usr/bin/cmp -s "$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" \ + --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" ' + (keys|sort)==["body","id","kind","schema_version"] and + .schema_version==1 and .kind=="duty_separation_evaluation" and .id==$result[0].id and + (.body|keys|sort)==["activation_state","core_contract","decision_ref", + "evaluation_mode","policy_ref","policy_set","reason_ids", + "reference_semantics","stage","verdict"] and + .body.activation_state=="inactive" 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:$policy[0].id, + media_type:"application/vnd.ystack.control-policy+json",sha256:$policy_sha} and + .body.decision_ref=={content_id:$decision[0].id, + media_type:"application/vnd.ystack.control-decision+json",sha256:$decision_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.verdict as $verdict | $decision[0].body.semantics.verdicts|index($verdict)!=null) 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==["duty.satisfied"] + elif .body.verdict=="inconclusive" then + .body.reason_ids==["actual.capability-unclassified"] + else (.body.reason_ids|index("duty.satisfied")==null and + index("actual.capability-unclassified")==null) end) +' "$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/scripts/test/control-duty-separation.test.sh b/scripts/test/control-duty-separation.test.sh new file mode 100755 index 0000000..bc8e7fa --- /dev/null +++ b/scripts/test/control-duty-separation.test.sh @@ -0,0 +1,971 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2016 +set -euo pipefail +export LC_ALL=C +umask 077 + +root=$(CDPATH='' cd -P -- "${BASH_SOURCE[0]%/*}/../.." && pwd -P) +evaluator="$root/control/v1/evaluate-duty.sh" +policy="$root/control/v1/duty-separation-policy.json" +decision="$root/control/v1/duty-separation-decision.json" +core_wrapper="$root/scripts/core-contract.sh" +core_registry="$root/core/v2/generation-registry.json" +tmp=$(/usr/bin/mktemp -d "${TMPDIR:-/tmp}/ystack-duty-test.XXXXXX") +RACE_PID='' +RACE_PGID='' +TEST_PGID='' +group_alive() { kill -0 -- "-$1" 2>/dev/null; } +terminate_reap() { + local evaluator_pid=$1 evaluator_pgid=$2 mode=${3:-} attempt=0 + [[ "$evaluator_pgid" =~ ^[1-9][0-9]*$ ]] && + [ "$evaluator_pgid" = "$evaluator_pid" ] && + [ "$evaluator_pgid" != "$TEST_PGID" ] || return 1 + if group_alive "$evaluator_pgid"; then kill -TERM -- "-$evaluator_pgid" 2>/dev/null || :; fi + while group_alive "$evaluator_pgid" && [ "$attempt" -lt 50 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + if group_alive "$evaluator_pgid"; then kill -KILL -- "-$evaluator_pgid" 2>/dev/null || :; fi + attempt=0 + while group_alive "$evaluator_pgid" && [ "$attempt" -lt 100 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + [ "$mode" != force-post-kill-live ] || return 1 + group_alive "$evaluator_pgid" && return 1 + wait "$evaluator_pid" 2>/dev/null || : + attempt=0 + while group_alive "$evaluator_pgid" && [ "$attempt" -lt 50 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + ! group_alive "$evaluator_pgid" +} +cleanup() { + if [ -n "${RACE_PID:-}" ] && [ -n "${RACE_PGID:-}" ]; then + if terminate_reap "$RACE_PID" "$RACE_PGID"; then + RACE_PID='' + RACE_PGID='' + else + /usr/bin/printf 'FAIL: EXIT could not reap race process group %s\n' "$RACE_PGID" >&2 + return 1 + fi + fi + /bin/rm -rf -- "$tmp" +} +on_exit() { + local status=$? + trap - EXIT + cleanup || status=1 + exit "$status" +} +trap on_exit EXIT +fail() { /usr/bin/printf 'FAIL: %s\n' "$1" >&2; exit 1; } +if ! TEST_PGID=$(/bin/ps -o pgid= -p $$ 2>/dev/null | /usr/bin/tr -d ' '); then + fail 'test process group inspection' +fi +[[ "$TEST_PGID" =~ ^[1-9][0-9]*$ ]] || fail 'test process group identity' +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' +"$jq_bin" -e --arg generation "$generation" \ + '[.[]|select(.generation_id==$generation and .semantic_identity=="core.contracts.v2")]|length==1' \ + "$core_registry" >/dev/null || fail 'selected registry entry' +core_package_sha=$("$jq_bin" -er '.body.core_contract.package_ref.sha256' "$policy") + +canonical="$tmp/policy-canonical.json" +"$jq_bin" -S -c . "$policy" >"$canonical" +/usr/bin/cmp -s "$policy" "$canonical" || fail 'canonical shipped policy' +policy_sha=$(sha256_path "$policy") +"$jq_bin" -S -c . "$decision" >"$tmp/decision-canonical.json" +/usr/bin/cmp -s "$decision" "$tmp/decision-canonical.json" || fail 'canonical decision' +decision_sha=$(sha256_path "$decision") +validator_driver_sha=$(sha256_path "$root/control/v1/validate.sh") +validator_program_sha=$(sha256_path "$root/control/v1/policy-set.jq") +"$jq_bin" -e --arg driver_sha "$validator_driver_sha" \ + --arg program_sha "$validator_program_sha" ' + .body.evaluator.policy_set_validator=={ + driver_ref:{content_id:"control-policy-set-validator-driver.v1", + media_type:"text/x-shellscript",sha256:$driver_sha}, + program_ref:{content_id:"control-policy-set-validator-program.v1", + media_type:"text/x-jq",sha256:$program_sha}} +' "$decision" >/dev/null || fail 'decision validator identities' + +closure_members="$tmp/core-closure-members.tsv" +closure_paths=( + scripts/core-contract.sh core/v2/generation-registry.json + "core/v2/generations/$generation/contracts.jq" + "core/v2/generations/$generation/core-ingress.sh" + "core/v2/generations/$generation/modules/profile_graph.jq" + "core/v2/generations/$generation/modules/result_facts.jq" + "core/v2/generations/$generation/modules/result_truth.jq" + "core/v2/generations/$generation/modules/schema.jq" + "core/v2/generations/$generation/modules/stage_request.jq" +) +: >"$closure_members" +for closure_path in "${closure_paths[@]}"; do + /usr/bin/printf '%s\t%s\n' "$closure_path" "$(sha256_path "$root/$closure_path")" \ + >>"$closure_members" +done +generation_sha=$(sha256_text "$generation") +closure_descriptor=$("$jq_bin" -Rn -S -c --arg generation_sha "$generation_sha" ' + [inputs|split("\t")|{path:.[0],sha256:.[1]}] as $members | + {schema_version:1,kind:"core_contract_package_closure", + semantic_identity:"core.contracts.v2",selected_generation_id_sha256:$generation_sha, + members:$members} +' <"$closure_members") +[ "$(sha256_text "$closure_descriptor")" = "$core_package_sha" ] || fail 'core closure identity' +[ "$generation_sha" = "$("$jq_bin" -r '.body.core_contract.generation_id_sha256' "$policy")" ] || + fail 'generation identity' +generation_newline_sha=$(/usr/bin/printf '%s\n' "$generation" | + /usr/bin/shasum -a 256 | /usr/bin/awk '{print $1}') +[ "$generation_newline_sha" != "$generation_sha" ] || fail 'generation newline distinction' +for source_path in control/v1/duty-separation-policy.json \ + control/v1/duty-separation-decision.json control/v1/duty-separation.jq \ + control/v1/evaluate-duty.sh scripts/test/control-duty-separation.test.sh; do + ! /usr/bin/grep -Fq "$generation" "$root/$source_path" || fail "raw generation $source_path" +done +pass 'opaque generation and full package closure identities' + +policy_set="$tmp/policy-set.json" +"$jq_bin" -S -c -n --arg duty_sha "$policy_sha" --arg decision_sha "$decision_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.example", + 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_sha;$decision_sha), + section("evidence-integrity";("3"*64);("c"*64)), + section("kill-switch";("4"*64);("d"*64)), + section("risk-gates";("5"*64);("e"*64)), + section("sandbox";("6"*64);("f"*64))]}} +' >"$policy_set" +policy_set_sha=$(sha256_path "$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") + +request="$tmp/request.json" +"$jq_bin" -L "$root/scripts/test" -S -c -n --arg sha "$resolved_sha" ' + import "portable-core-stage-request-fixtures" as request; + request::request_doc("producer";$sha) | + walk(if type == "object" and has("schema_version") then .schema_version=2 else . end) +' >"$request" +request_sha=$(sha256_path "$request") +result="$tmp/result.json" +"$jq_bin" -L "$root/scripts/test" -S -c -n \ + --slurpfile request "$request" --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) +' >"$result" + +expect_eval() { + local name=$1 expected=$2 reason=$3 set=$4 req=$5 resolved_input=$6 result_input=$7 + local out="$tmp/$name.out" err="$tmp/$name.err" repeat="$tmp/$name.repeat" + PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate \ + "$set" "$req" "$resolved_input" "$result_input" >"$out" 2>"$err" || { + /bin/cat "$err" >&2 + fail "$name status" + } + [ ! -s "$err" ] || fail "$name stderr" + "$jq_bin" -e --arg verdict "$expected" --arg reason "$reason" ' + .kind == "duty_separation_evaluation" and + .body.activation_state == "inactive" 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) + ' "$out" >/dev/null || fail "$name verdict" + "$jq_bin" -S -c . "$out" >"$repeat" + /usr/bin/cmp -s "$out" "$repeat" || fail "$name canonical" + pass "$name" +} +expect_error() { + local name=$1 expected=$2 set=$3 req=$4 resolved_input=$5 result_input=$6 + local out="$tmp/$name.out" err="$tmp/$name.err" status=0 + PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate \ + "$set" "$req" "$resolved_input" "$result_input" >"$out" 2>"$err" || status=$? + [ "$status" -ne 0 ] && [ ! -s "$out" ] && + [ "$(/bin/cat "$err")" = "$expected" ] || fail "$name" + pass "$name" +} +expect_pure() { + local name=$1 expected=$2 reason=$3 req=$4 resolved_input=$5 result_input=$6 + local out="$tmp/pure-$name.out" req_sha resolved_input_sha result_input_sha + req_sha=$(sha256_path "$req") + resolved_input_sha=$(sha256_path "$resolved_input") + result_input_sha=$(sha256_path "$result_input") + "$jq_bin" -S -c -n -f "$root/control/v1/duty-separation.jq" \ + --slurpfile policy "$policy" --slurpfile decision "$decision" \ + --slurpfile policy_set "$policy_set" \ + --slurpfile request "$req" --slurpfile resolved "$resolved_input" \ + --slurpfile result "$result_input" --arg policy_set_sha "$policy_set_sha" \ + --arg decision_sha "$decision_sha" \ + --arg request_sha "$req_sha" --arg resolved_sha "$resolved_input_sha" \ + --arg result_sha "$result_input_sha" >"$out" || fail "pure $name status" + "$jq_bin" -e --arg expected "$expected" --arg reason "$reason" ' + .body.verdict==$expected and (.body.reason_ids | index($reason) != null) and + .body.reason_ids == (.body.reason_ids | sort | unique) + ' "$out" >/dev/null || fail "pure $name verdict" + pass "pure $name" +} +sync_result() { + local changed_request=$1 output=$2 digest + digest=$(sha256_path "$changed_request") + "$jq_bin" -S -c --arg digest "$digest" '.body.request_ref.sha256=$digest' \ + "$result" >"$output" +} +copy_runtime() { + local destination=$1 copy_path + /bin/mkdir -p "$destination/control/v1" "$destination/scripts" "$destination/core" + for copy_path in evaluate-duty.sh duty-separation.jq duty-separation-policy.json \ + duty-separation-decision.json 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" +} +record_race_group() { + local leader_pid=$2 evaluator_pgid + if ! evaluator_pgid=$(/bin/ps -o pgid= -p "$leader_pid" 2>/dev/null | + /usr/bin/tr -d ' '); then + return 1 + fi + if [[ ! "$TEST_PGID" =~ ^[1-9][0-9]*$ ]] || + [[ ! "$evaluator_pgid" =~ ^[1-9][0-9]*$ ]] || + [ "$evaluator_pgid" != "$leader_pid" ] || [ "$evaluator_pgid" = "$TEST_PGID" ]; then + return 1 + fi + RACE_PID=$leader_pid + RACE_PGID=$evaluator_pgid +} +terminate_unowned_leader() { + local leader_pid=$1 attempt=0 + if kill -0 "$leader_pid" 2>/dev/null; then kill -TERM "$leader_pid" 2>/dev/null || :; fi + while kill -0 "$leader_pid" 2>/dev/null && [ "$attempt" -lt 50 ]; do + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + if kill -0 "$leader_pid" 2>/dev/null; then kill -KILL "$leader_pid" 2>/dev/null || :; fi + wait "$leader_pid" 2>/dev/null || : +} +start_gated_group() { + local name=$1 group_out=$2 group_err=$3 leader_pid gate + shift 3 + gate="$tmp/$name.race.gate" + /usr/bin/mkfifo "$gate" || fail "$name gate creation" + exec 9<>"$gate" || fail "$name gate open" + set -m + /bin/bash -c ' + gate=$1; race_path=$2; shift 2 + IFS= read -r token <"$gate" || exit 125 + [ "$token" = go ] || exit 125 + PATH=$race_path; export PATH + exec "$@" + ' race-supervisor "$gate" "$bin:/usr/bin:/bin" "$@" \ + 9>&- >"$group_out" 2>"$group_err" & + leader_pid=$! + set +m + if ! record_race_group "$name" "$leader_pid"; then + exec 9>&- + /bin/rm -f "$gate" + terminate_unowned_leader "$leader_pid" + fail "$name unsafe evaluator process group" + fi + if ! /usr/bin/printf 'go\n' >&9; then + exec 9>&- + /bin/rm -f "$gate" + if terminate_reap "$RACE_PID" "$RACE_PGID"; then + RACE_PID=''; RACE_PGID='' + fi + fail "$name gate release" + fi + exec 9>&- + /bin/rm -f "$gate" +} +start_race() { + local name=$1 set=$2 req=$3 resolved_input=$4 result_input=$5 + RACE_OUT="$tmp/$name.race.out" + RACE_ERR="$tmp/$name.race.err" + start_gated_group "$name" "$RACE_OUT" "$RACE_ERR" "$evaluator" evaluate \ + "$set" "$req" "$resolved_input" "$result_input" +} +wait_for_marker() { + local name=$1 search_root=$2 marker=$3 evaluator_pid=$4 attempt=0 + MARKER_PATH='' + MARKER_ERROR='' + while [ "$attempt" -lt 500 ]; do + MARKER_PATH=$(/usr/bin/find "$search_root" -name "$marker" \ + -type f -print -quit 2>/dev/null) + [ -n "$MARKER_PATH" ] && return 0 + if ! kill -0 "$evaluator_pid" 2>/dev/null; then + if group_alive "$RACE_PGID"; then + if terminate_reap "$evaluator_pid" "$RACE_PGID"; then + RACE_PID=''; RACE_PGID='' + else + MARKER_ERROR="$name process-group cleanup failed before marker $marker" + return 1 + fi + else + wait "$evaluator_pid" 2>/dev/null || : + RACE_PID=''; RACE_PGID='' + fi + MARKER_ERROR="$name evaluator exited before marker $marker" + return 1 + fi + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + if terminate_reap "$evaluator_pid" "$RACE_PGID"; then + RACE_PID=''; RACE_PGID='' + MARKER_ERROR="$name marker timeout: $marker" + else + MARKER_ERROR="$name marker timeout cleanup failed: $marker" + fi + return 1 +} +require_marker() { wait_for_marker "$@" || fail "$MARKER_ERROR"; } +wait_for_evaluator() { + local name=$1 evaluator_pid=$2 attempt=0 status=0 + while [ "$attempt" -lt 1000 ]; do + if ! kill -0 "$evaluator_pid" 2>/dev/null; then + wait "$evaluator_pid" || status=$? + if group_alive "$RACE_PGID"; then + if terminate_reap "$evaluator_pid" "$RACE_PGID"; then + RACE_PID=''; RACE_PGID='' + fail "$name evaluator left child processes" + else + fail "$name evaluator child cleanup failed" + fi + fi + RACE_STATUS=$status + RACE_PID='' + RACE_PGID='' + return 0 + fi + attempt=$((attempt + 1)) + /bin/sleep 0.01 + done + if terminate_reap "$evaluator_pid" "$RACE_PGID"; then + RACE_PID=''; RACE_PGID='' + fail "$name evaluator completion timeout" + else + fail "$name evaluator completion cleanup failed" + fi +} +finish_race_error() { + local name=$1 expected=$2 evaluator_pid=$3 + wait_for_evaluator "$name" "$evaluator_pid" + [ "$RACE_STATUS" -ne 0 ] && [ ! -s "$RACE_OUT" ] && + [ "$(/bin/cat "$RACE_ERR")" = "$expected" ] || fail "$name" + pass "$name" +} +finish_race_eval() { + local name=$1 expected=$2 reason=$3 evaluator_pid=$4 canonical + wait_for_evaluator "$name" "$evaluator_pid" + [ "$RACE_STATUS" -eq 0 ] && [ ! -s "$RACE_ERR" ] || fail "$name status" + "$jq_bin" -e --arg verdict "$expected" --arg reason "$reason" ' + .kind=="duty_separation_evaluation" and .body.verdict==$verdict and + (.body.reason_ids|index($reason)!=null) and + .body.reason_ids==(.body.reason_ids|sort|unique) + ' "$RACE_OUT" >/dev/null || fail "$name verdict" + canonical="$tmp/$name.race.canonical" + "$jq_bin" -S -c . "$RACE_OUT" >"$canonical" + /usr/bin/cmp -s "$RACE_OUT" "$canonical" || fail "$name canonical" + pass "$name" +} + +/bin/mkdir -p "$tmp/no-markers" +set -m +/usr/bin/false & +setup_pid=$! +set +m +wait "$setup_pid" 2>/dev/null || : +if record_race_group setup-early-exit "$setup_pid"; then + fail 'setup inspection accepted an exited group leader' +fi +[ -z "$RACE_PID" ] && [ -z "$RACE_PGID" ] || fail 'setup failure claimed ownership' +pass 'gated setup handles early supervisor exit without ownership' + +start_gated_group marker-early-exit "$tmp/marker-early-group.out" \ + "$tmp/marker-early-group.err" \ + /bin/sleep 0.2 +marker_pid=$RACE_PID +if wait_for_marker marker-early-exit "$tmp/no-markers" absent.marker "$marker_pid" \ + 2>"$tmp/marker-early.err"; then + fail 'marker early-exit helper accepted a missing marker' +fi +if [ "$MARKER_ERROR" != 'marker-early-exit evaluator exited before marker absent.marker' ] || + [ -n "$RACE_PID" ] || [ -n "$RACE_PGID" ]; then + fail 'marker early-exit helper result' +fi +pass 'marker helper bounds evaluator early exit' + +child_marker="$tmp/nested-child.pid" +start_gated_group marker-timeout "$tmp/marker-timeout-group.out" \ + "$tmp/marker-timeout-group.err" \ + /bin/bash -c '/bin/sleep 30 & child=$!; /usr/bin/printf "%s\n" "$child" >"$1"; wait "$child"' \ + nested-race "$child_marker" +marker_pid=$RACE_PID +require_marker marker-timeout-child "$tmp" nested-child.pid "$marker_pid" +nested_child_pid=$(/bin/cat "$child_marker") +if wait_for_marker marker-timeout "$tmp/no-markers" absent.marker "$marker_pid" \ + 2>"$tmp/marker-timeout.err"; then + fail 'marker timeout helper accepted a missing marker' +fi +if [ "$MARKER_ERROR" != 'marker-timeout marker timeout: absent.marker' ] || + [ -n "$RACE_PID" ] || [ -n "$RACE_PGID" ] || + kill -0 "$marker_pid" 2>/dev/null || kill -0 "$nested_child_pid" 2>/dev/null; then + fail 'marker timeout helper cleanup' +fi +pass 'marker helper bounds timeout and reaps process group' + +start_gated_group forced-post-kill "$tmp/forced-post-kill.out" \ + "$tmp/forced-post-kill.err" /bin/sleep 30 +forced_pid=$RACE_PID +forced_pgid=$RACE_PGID +if terminate_reap "$forced_pid" "$forced_pgid" force-post-kill-live \ + 2>"$tmp/forced-post-kill.cleanup"; then + fail 'post-KILL live-group branch reported cleanup success' +fi +if [ "$RACE_PID" != "$forced_pid" ] || [ "$RACE_PGID" != "$forced_pgid" ]; then + fail 'post-KILL cleanup failure discarded ownership' +fi +terminate_reap "$forced_pid" "$forced_pgid" 2>>"$tmp/forced-post-kill.cleanup" || + fail 'post-KILL cleanup retry' +RACE_PID='' +RACE_PGID='' +pass 'post-KILL live-group branch returns bounded with ownership' + +expect_eval satisfied satisfied duty.satisfied "$policy_set" "$request" "$resolved" "$result" +result_sha=$(sha256_path "$result") +"$jq_bin" -e --arg policy_set_sha "$policy_set_sha" --arg request_sha "$request_sha" \ + --arg resolved_sha "$resolved_sha" --arg result_sha "$result_sha" \ + --arg policy_sha "$policy_sha" --arg decision_sha "$decision_sha" ' + .body.policy_set.sha256==$policy_set_sha and + .body.policy_ref.sha256==$policy_sha and .body.decision_ref=={ + content_id:"control-decision.duty-separation", + media_type:"application/vnd.ystack.control-decision+json",sha256:$decision_sha} and + .body.stage.request_ref.sha256==$request_sha and + .body.stage.resolved_profile_ref.sha256==$resolved_sha and + .body.stage.result_ref.sha256==$result_sha +' "$tmp/satisfied.out" >/dev/null || fail 'output input identities' +pass 'output input identities' +repeat_out="$tmp/repeat.out" +PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate \ + "$policy_set" "$request" "$resolved" "$result" >"$repeat_out" +PATH="$bin:/usr/bin:/bin" "$evaluator" evaluate \ + "$policy_set" "$request" "$resolved" "$result" | /usr/bin/cmp -s - "$repeat_out" || + fail 'repeat determinism' +pass 'repeat determinism' + +skipped="$tmp/skipped.json" +"$jq_bin" -S -c '.body.status="skipped" | .body.reason={reason_id:"stage.skipped"} | + .body.outputs=[] | .body.diagnostics=[] | .body.evidence=[] | + del(.body.outcome,.body.execution,.body.started_at,.body.finished_at)' "$result" >"$skipped" +expect_eval skipped-satisfied satisfied duty.satisfied \ + "$policy_set" "$request" "$resolved" "$skipped" + +observer_request="$tmp/observer-request.json" +observer_result="$tmp/observer-result.json" +"$jq_bin" -S -c '.body.requested_by.role="observer"' "$request" >"$observer_request" +sync_result "$observer_request" "$observer_result" +expect_eval requester-role violated requester.role-denied \ + "$policy_set" "$observer_request" "$resolved" "$observer_result" + +unclassified="$tmp/unclassified.json" +"$jq_bin" -S -c ' + .body.status="failed" | .body.outcome={family:"change",value:"inconclusive"} | + .body.reason={reason_id:"stage.failed"} | .body.outputs=[] | + .body.diagnostics=[{content_id:"diagnostic.failed",media_type:"text/plain",sha256:("d"*64)}] | + .body.evidence |= map(.verdict="failed") | + .body.execution.used_capability={kind:"unclassified",id:"capability.unknown"} +' "$result" >"$unclassified" +expect_eval unclassified inconclusive actual.capability-unclassified \ + "$policy_set" "$request" "$resolved" "$unclassified" + +kind_incident="$tmp/kind-incident.json" +"$jq_bin" -S -c ' + .body.status="failed" | .body.outcome={family:"change",value:"inconclusive"} | + .body.reason={reason_id:"stage.failed"} | .body.outputs=[] | + .body.diagnostics=[{content_id:"diagnostic.failed",media_type:"text/plain",sha256:("d"*64)}] | + .body.evidence |= map(.verdict="failed") | + .body.execution.actual_binding.execution_kind="deterministic" | + .body.execution.metadata.kind="deterministic" | + .body.execution.metadata |= (.provider={state:"not-applicable"} | + .model={state:"not-applicable"} | .snapshot={state:"not-applicable"} | + .effort={state:"not-applicable"} | .prompt={state:"not-applicable"} | + .skills={state:"not-applicable"}) +' "$result" >"$kind_incident" +expect_eval actual-kind violated actual.execution-kind-mismatch \ + "$policy_set" "$request" "$resolved" "$kind_incident" + +deterministic_resolved="$tmp/deterministic-resolved.json" +deterministic_request="$tmp/deterministic-request.json" +deterministic_result="$tmp/deterministic-result.json" +"$jq_bin" -S -c ' + (.body.bindings[] | select(.binding.role=="producer" or .binding.role=="reviewer") | + .binding.execution_kind)="deterministic" | + (.body.bindings[] | select(.binding.role=="producer") | + .binding.requested_permissions)=["core.perm.evidence.write.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"] | + (.body.bindings[] | select(.binding.role=="reviewer") | + .binding.requested_permissions)=["core.perm.evidence.write.v1","core.perm.target.read.v1"] +' "$resolved" >"$deterministic_resolved" +"$jq_bin" -S -c '.body.operation.permissions=["core.perm.evidence.write.v1", + "core.perm.scratch.write.v1","core.perm.target.read.v1"]' \ + "$request" >"$deterministic_request" +"$jq_bin" -S -c '.body.execution.actual_binding.execution_kind="deterministic"' \ + "$result" >"$deterministic_result" +expect_pure deterministic-ceilings satisfied duty.satisfied \ + "$deterministic_request" "$deterministic_resolved" "$deterministic_result" + +for dimension in adapter_instance_id execution_boundary_id principal_id; do + requester_case="$tmp/requester-$dimension.json" + "$jq_bin" -S -c --arg dimension "$dimension" --slurpfile resolved "$resolved" ' + .body.requested_by[$dimension]=([$resolved[0].body.bindings[] | + select(.binding.role=="producer") | .binding[$dimension]][0]) + ' "$request" >"$requester_case" + requester_result="$tmp/requester-$dimension-result.json" + sync_result "$requester_case" "$requester_result" + expect_eval "requester-$dimension" violated "requester.$dimension-collision" \ + "$policy_set" "$requester_case" "$resolved" "$requester_result" +done +publisher_case="$tmp/publisher-active.json" +"$jq_bin" -S -c '(.body.bindings[] | select(.binding.role=="publisher") | + .binding.requested_permissions)=["core.perm.evidence.write.v1"]' \ + "$resolved" >"$publisher_case" +expect_pure publisher-not-dormant violated publisher.not-dormant \ + "$request" "$publisher_case" "$result" +precedence_case="$tmp/precedence.json" +"$jq_bin" -S -c '.body.reported_by.role="reviewer"' "$unclassified" >"$precedence_case" +expect_eval violation-precedence violated reporter.role-mismatch \ + "$policy_set" "$request" "$resolved" "$precedence_case" + +incident="$tmp/incident.json" +"$jq_bin" -S -c ' + .body.status="failed" | .body.outcome={family:"change",value:"inconclusive"} | + .body.reason={reason_id:"stage.failed"} | .body.outputs=[] | + .body.diagnostics=[{content_id:"diagnostic.failed",media_type:"text/plain",sha256:("d"*64)}] | + .body.evidence |= map(.verdict="failed") +' "$result" >"$incident" +for hop in actual_binding performer; do + hop_case="$tmp/incident-$hop.json" + "$jq_bin" -S -c --arg hop "$hop" \ + '.body.execution[$hop].principal_id="different.principal"' \ + "$incident" >"$hop_case" + reason_hop=${hop%_binding} + expect_eval "incident-$reason_hop" violated "$reason_hop.principal_id-mismatch" \ + "$policy_set" "$request" "$resolved" "$hop_case" +done +incident_capability="$tmp/incident-capability.json" +"$jq_bin" -S -c '.body.execution.used_capability= + {kind:"registered",id:"core.review.change.v1"}' "$incident" >"$incident_capability" +expect_eval incident-capability violated actual.capability-mismatch \ + "$policy_set" "$request" "$resolved" "$incident_capability" + +bad_policy_set="$tmp/bad-policy-set.json" +"$jq_bin" -S -c '(.body.sections[] | select(.section_id=="duty-separation") | + .policy_ref.sha256)=("7"*64)' "$policy_set" >"$bad_policy_set" +expect_error policy-identity E_RELATION "$bad_policy_set" "$request" "$resolved" "$result" +bad_decision_ref="$tmp/bad-decision-ref.json" +"$jq_bin" -S -c '(.body.sections[] | select(.section_id=="duty-separation") | + .decision_ref.sha256)=("7"*64)' "$policy_set" >"$bad_decision_ref" +expect_error decision-identity E_RELATION "$bad_decision_ref" "$request" "$resolved" "$result" +bad_generation="$tmp/bad-generation.json" +"$jq_bin" -S -c '.body.core_contract.generation_id=("g-"+("8"*64))' \ + "$policy_set" >"$bad_generation" +expect_error core-generation E_RELATION "$bad_generation" "$request" "$resolved" "$result" +bad_package="$tmp/bad-package.json" +"$jq_bin" -S -c '.body.core_contract.package_ref.sha256=("7"*64)' \ + "$policy_set" >"$bad_package" +expect_error core-package E_RELATION "$bad_package" "$request" "$resolved" "$result" + +bad_request="$tmp/bad-permission-request.json" +bad_request_result="$tmp/bad-permission-result.json" +"$jq_bin" -S -c '.body.operation.permissions=["core.perm.candidate.execute.v1"]' \ + "$request" >"$bad_request" +sync_result "$bad_request" "$bad_request_result" +expect_error producer-ceiling E_CORE "$policy_set" "$bad_request" "$resolved" "$bad_request_result" + +collision_resolved="$tmp/collision-resolved.json" +"$jq_bin" -S -c '(.body.bindings[] | select(.binding.role=="reviewer") | + .binding.principal_id)="principal.producer"' "$resolved" >"$collision_resolved" +expect_error protected-collision E_CORE "$policy_set" "$request" "$collision_resolved" "$result" + +pretty_set="$tmp/pretty-policy-set.json" +"$jq_bin" . "$policy_set" >"$pretty_set" +expect_error noncanonical-policy-set E_POLICY_SET "$pretty_set" "$request" "$resolved" "$result" +/bin/ln -s "$request" "$tmp/request-link.json" +expect_error symlink-input E_RUNTIME "$policy_set" "$tmp/request-link.json" "$resolved" "$result" +large="$tmp/large.json" +/usr/bin/awk 'BEGIN { for (i=0;i<1048577;i++) printf "x" }' >"$large" +expect_error input-limit E_LIMIT "$policy_set" "$large" "$resolved" "$result" +copy_root="$tmp/policy-limit-root" +/bin/mkdir -p "$copy_root/control/v1" "$copy_root/scripts" +for copy_path in evaluate-duty.sh duty-separation.jq duty-separation-policy.json \ + duty-separation-decision.json validate.sh; do + /bin/cp "$root/control/v1/$copy_path" "$copy_root/control/v1/$copy_path" +done +/bin/cp "$root/scripts/core-contract.sh" "$copy_root/scripts/core-contract.sh" +/usr/bin/awk 'BEGIN { for (i=0;i<1048577;i++) printf " " }' \ + >>"$copy_root/control/v1/duty-separation-policy.json" +original_evaluator=$evaluator +evaluator="$copy_root/control/v1/evaluate-duty.sh" +expect_error policy-limit E_LIMIT "$policy_set" "$request" "$resolved" "$result" +evaluator=$original_evaluator + +decision_limit_root="$tmp/decision-limit-root" +/bin/cp -R "$copy_root" "$decision_limit_root" +/bin/cp "$decision" "$decision_limit_root/control/v1/duty-separation-decision.json" +/usr/bin/awk 'BEGIN { for (i=0;i<1048577;i++) printf " " }' \ + >>"$decision_limit_root/control/v1/duty-separation-decision.json" +evaluator="$decision_limit_root/control/v1/evaluate-duty.sh" +expect_error decision-limit E_LIMIT "$policy_set" "$request" "$resolved" "$result" +evaluator=$original_evaluator + +runtime_root="$tmp/runtime-happy" +copy_runtime "$runtime_root" +evaluator="$runtime_root/control/v1/evaluate-duty.sh" +expect_eval copied-runtime satisfied duty.satisfied "$policy_set" "$request" "$resolved" "$result" +evaluator=$original_evaluator + +for stale_kind in wrapper ingress module registry; do + stale_root="$tmp/stale-$stale_kind" + stale_sentinel="$tmp/stale-$stale_kind-ran" + copy_runtime "$stale_root" + case "$stale_kind" in + wrapper) + /usr/bin/printf '\n: > "%s"\n' "$stale_sentinel" \ + >>"$stale_root/scripts/core-contract.sh" + ;; + ingress) + /usr/bin/printf '\n: > "%s"\n' "$stale_sentinel" \ + >>"$stale_root/core/v2/generations/$generation/core-ingress.sh" + ;; + module) + /usr/bin/printf '\n' \ + >>"$stale_root/core/v2/generations/$generation/modules/schema.jq" + ;; + registry) + "$jq_bin" -S -c '.[0].concern += ".stale"' \ + "$stale_root/core/v2/generation-registry.json" >"$stale_root/registry.next" + /bin/mv "$stale_root/registry.next" "$stale_root/core/v2/generation-registry.json" + ;; + esac + evaluator="$stale_root/control/v1/evaluate-duty.sh" + expect_error "stale-core-$stale_kind" E_RELATION \ + "$policy_set" "$request" "$resolved" "$result" + [ ! -e "$stale_sentinel" ] || fail "stale $stale_kind executed" +done +evaluator=$original_evaluator +pass 'stale core closure is rejected before execution' + +moving_root="$tmp/moving-core" +moving_scratch="$tmp/moving-scratch" +copy_runtime "$moving_root" +/bin/mkdir -p "$moving_scratch" +moving_sentinel="$tmp/moving-live-ingress-ran" +moving_target="$moving_root/core/v2/generations/$generation/core-ingress.sh" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$moving_scratch" +evaluator="$moving_root/control/v1/evaluate-duty.sh" +start_race moving-core-postflight "$policy_set" "$request" "$resolved" "$result" +require_marker moving-core-postflight "$moving_scratch" \ + core-closure-mirror.json "$RACE_PID" +/usr/bin/printf '\n: > "%s"\n' "$moving_sentinel" >>"$moving_target" +finish_race_error moving-core-postflight E_RELATION "$RACE_PID" +[ ! -e "$moving_sentinel" ] || fail 'moving live ingress executed' +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +failure_root="$tmp/moving-core-failure" +failure_scratch="$tmp/moving-failure-scratch" +copy_runtime "$failure_root" +/bin/mkdir -p "$failure_scratch" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$failure_scratch" +evaluator="$failure_root/control/v1/evaluate-duty.sh" +start_race moving-core-beats-core-error \ + "$policy_set" "$bad_request" "$resolved" "$bad_request_result" +require_marker moving-core-beats-core-error "$failure_scratch" \ + core-closure-mirror.json "$RACE_PID" +/usr/bin/printf '\n' \ + >>"$failure_root/core/v2/generations/$generation/modules/schema.jq" +finish_race_error moving-core-beats-core-error E_RELATION "$RACE_PID" +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +swap_root="$tmp/swap-restore-core" +swap_scratch="$tmp/swap-restore-scratch" +swap_sentinel="$tmp/swap-live-ingress-ran" +copy_runtime "$swap_root" +/bin/mkdir -p "$swap_scratch" +swap_target="$swap_root/core/v2/generations/$generation/core-ingress.sh" +/bin/cp "$swap_target" "$tmp/swap-ingress.saved" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$swap_scratch" +evaluator="$swap_root/control/v1/evaluate-duty.sh" +start_race live-swap-restore \ + "$policy_set" "$request" "$resolved" "$result" +require_marker live-swap-restore "$swap_scratch" core-closure-mirror.json "$RACE_PID" +/usr/bin/printf '\n: > "%s"\n' "$swap_sentinel" >>"$swap_target" +require_marker live-swap-restore "$swap_scratch" run.out "$RACE_PID" +/bin/cp "$tmp/swap-ingress.saved" "$swap_target" +finish_race_eval live-swap-restore satisfied duty.satisfied "$RACE_PID" +[ ! -e "$swap_sentinel" ] || fail 'swap-restore live ingress executed' +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +scripts_victim="$tmp/scripts-link-victim" +scripts_sibling="$tmp/scripts-link-sibling" +scripts_sentinel="$tmp/scripts-link-ingress-ran" +copy_runtime "$scripts_victim" +copy_runtime "$scripts_sibling" +/usr/bin/printf '\n: > "%s"\n' "$scripts_sentinel" \ + >>"$scripts_sibling/core/v2/generations/$generation/core-ingress.sh" +/bin/rm -rf "$scripts_victim/scripts" +/bin/ln -s "$scripts_sibling/scripts" "$scripts_victim/scripts" +evaluator="$scripts_victim/control/v1/evaluate-duty.sh" +expect_error symlinked-scripts-root E_RELATION "$policy_set" "$request" "$resolved" "$result" +[ ! -e "$scripts_sentinel" ] || fail 'symlinked scripts core executed' +evaluator=$original_evaluator + +for unsafe_kind in extra symlink; do + unsafe_root="$tmp/unsafe-$unsafe_kind" + copy_runtime "$unsafe_root" + if [ "$unsafe_kind" = extra ]; then + : >"$unsafe_root/core/v2/generations/$generation/unknown" + else + /bin/rm -f "$unsafe_root/core/v2/generations/$generation/modules/schema.jq" + /bin/ln -s result_truth.jq \ + "$unsafe_root/core/v2/generations/$generation/modules/schema.jq" + fi + evaluator="$unsafe_root/control/v1/evaluate-duty.sh" + expect_error "unsafe-core-$unsafe_kind" E_RELATION \ + "$policy_set" "$request" "$resolved" "$result" +done +evaluator=$original_evaluator + +rebound_decision_root="$tmp/rebound-validator-decision" +copy_runtime "$rebound_decision_root" +"$jq_bin" -S -c '.body.evaluator.policy_set_validator.driver_ref.sha256=("7"*64)' \ + "$rebound_decision_root/control/v1/duty-separation-decision.json" \ + >"$rebound_decision_root/decision.next" +/bin/mv "$rebound_decision_root/decision.next" \ + "$rebound_decision_root/control/v1/duty-separation-decision.json" +rebound_decision_sha=$(sha256_path \ + "$rebound_decision_root/control/v1/duty-separation-decision.json") +"$jq_bin" -S -c --arg sha "$rebound_decision_sha" \ + '(.body.sections[] | select(.section_id=="duty-separation") | + .decision_ref.sha256)=$sha' "$policy_set" >"$tmp/rebound-decision-policy-set.json" +evaluator="$rebound_decision_root/control/v1/evaluate-duty.sh" +expect_error rebound-validator-decision E_RELATION \ + "$tmp/rebound-decision-policy-set.json" "$request" "$resolved" "$result" +evaluator=$original_evaluator + +for stale_kind in validator-driver validator-program; do + stale_root="$tmp/stale-$stale_kind" + stale_sentinel="$tmp/stale-$stale_kind-ran" + copy_runtime "$stale_root" + if [ "$stale_kind" = validator-driver ]; then + /usr/bin/printf '\n: > "%s"\n' "$stale_sentinel" \ + >>"$stale_root/control/v1/validate.sh" + else + /usr/bin/printf '\n' >>"$stale_root/control/v1/policy-set.jq" + fi + evaluator="$stale_root/control/v1/evaluate-duty.sh" + expect_error "stale-$stale_kind" E_RELATION \ + "$policy_set" "$request" "$resolved" "$result" + [ ! -e "$stale_sentinel" ] || fail "stale $stale_kind executed" +done +evaluator=$original_evaluator + +validator_link_root="$tmp/validator-link" +copy_runtime "$validator_link_root" +/bin/rm -f "$validator_link_root/control/v1/validate.sh" +/bin/ln -s "$root/control/v1/validate.sh" "$validator_link_root/control/v1/validate.sh" +evaluator="$validator_link_root/control/v1/evaluate-duty.sh" +expect_error symlinked-validator E_RELATION "$policy_set" "$request" "$resolved" "$result" +evaluator=$original_evaluator + +validator_move_root="$tmp/moving-validator" +validator_move_scratch="$tmp/moving-validator-scratch" +validator_move_sentinel="$tmp/moving-validator-ran" +copy_runtime "$validator_move_root" +/bin/mkdir -p "$validator_move_scratch" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$validator_move_scratch" +evaluator="$validator_move_root/control/v1/evaluate-duty.sh" +start_race moving-validator "$policy_set" "$request" "$resolved" "$result" +require_marker moving-validator "$validator_move_scratch" \ + policy-validator-ready "$RACE_PID" +/usr/bin/printf '\n: > "%s"\n' "$validator_move_sentinel" \ + >>"$validator_move_root/control/v1/validate.sh" +finish_race_error moving-validator E_RELATION "$RACE_PID" +[ ! -e "$validator_move_sentinel" ] || fail 'moving live validator executed' +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +validator_failure_root="$tmp/moving-validator-failure" +validator_failure_scratch="$tmp/moving-validator-failure-scratch" +copy_runtime "$validator_failure_root" +/bin/mkdir -p "$validator_failure_scratch" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$validator_failure_scratch" +evaluator="$validator_failure_root/control/v1/evaluate-duty.sh" +start_race validator-movement-beats-policy-error \ + "$pretty_set" "$request" "$resolved" "$result" +require_marker validator-movement-beats-policy-error "$validator_failure_scratch" \ + policy-validator-ready "$RACE_PID" +/usr/bin/printf '\n' >>"$validator_failure_root/control/v1/policy-set.jq" +finish_race_error validator-movement-beats-policy-error E_RELATION "$RACE_PID" +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +validator_swap_root="$tmp/validator-swap" +validator_swap_scratch="$tmp/validator-swap-scratch" +validator_swap_sentinel="$tmp/validator-swap-ran" +copy_runtime "$validator_swap_root" +/bin/mkdir -p "$validator_swap_scratch" +/bin/cp "$validator_swap_root/control/v1/validate.sh" "$tmp/validator.saved" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$validator_swap_scratch" +evaluator="$validator_swap_root/control/v1/evaluate-duty.sh" +start_race validator-swap-restore \ + "$policy_set" "$request" "$resolved" "$result" +require_marker validator-swap-restore "$validator_swap_scratch" \ + policy-validator-ready "$RACE_PID" +/usr/bin/printf '\n: > "%s"\n' "$validator_swap_sentinel" \ + >>"$validator_swap_root/control/v1/validate.sh" +/bin/cp "$tmp/validator.saved" "$validator_swap_root/control/v1/validate.sh" +finish_race_eval validator-swap-restore satisfied duty.satisfied "$RACE_PID" +[ ! -e "$validator_swap_sentinel" ] || fail 'swap-restore live validator executed' +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +mirror_move_root="$tmp/moving-validator-mirror" +mirror_move_scratch="$tmp/moving-validator-mirror-scratch" +copy_runtime "$mirror_move_root" +/bin/mkdir -p "$mirror_move_scratch" +saved_tmpdir=${TMPDIR-} +export TMPDIR="$mirror_move_scratch" +evaluator="$mirror_move_root/control/v1/evaluate-duty.sh" +start_race moving-validator-mirror "$policy_set" "$request" "$resolved" "$result" +require_marker moving-validator-mirror "$mirror_move_scratch" \ + policy-validator-ready "$RACE_PID" +mirror_program=$(/usr/bin/find "$mirror_move_scratch" \ + -path '*/policy-validator/control/v1/policy-set.jq' -type f -print -quit) +[ -n "$mirror_program" ] || { + if terminate_reap "$RACE_PID" "$RACE_PGID"; then + RACE_PID=''; RACE_PGID='' + fail 'moving-validator-mirror missing mirrored program' + else + fail 'moving-validator-mirror cleanup failed after missing program' + fi +} +/usr/bin/printf '\n' >>"$mirror_program" +finish_race_error moving-validator-mirror E_RELATION "$RACE_PID" +if [ -n "$saved_tmpdir" ]; then export TMPDIR="$saved_tmpdir"; else unset TMPDIR; fi +evaluator=$original_evaluator + +for stale_kind in decision program driver; do + stale_root="$tmp/stale-$stale_kind" + copy_runtime "$stale_root" + case "$stale_kind" in + decision) + "$jq_bin" -S -c '.body.extra=true' \ + "$stale_root/control/v1/duty-separation-decision.json" >"$stale_root/decision.next" + /bin/mv "$stale_root/decision.next" \ + "$stale_root/control/v1/duty-separation-decision.json" + ;; + program) /usr/bin/printf '\n' >>"$stale_root/control/v1/duty-separation.jq" ;; + driver) /usr/bin/printf '\n' >>"$stale_root/control/v1/evaluate-duty.sh" ;; + esac + evaluator="$stale_root/control/v1/evaluate-duty.sh" + expect_error "stale-$stale_kind" E_RELATION \ + "$policy_set" "$request" "$resolved" "$result" +done +evaluator=$original_evaluator + +for required in control/v1/duty-separation-policy.json \ + control/v1/duty-separation-decision.json control/v1/duty-separation.jq \ + control/v1/evaluate-duty.sh scripts/test/control-duty-separation.test.sh; do + [ "$(/usr/bin/grep -Fxc "$required" "$root/ci/required-files.txt")" -eq 1 ] || + fail "manifest $required" +done +/usr/bin/grep -Fq 'Inactive duty-separation evaluator' "$root/README.md" || fail 'README docs' +/usr/bin/grep -Fq 'control-duty-separation.test.sh' "$root/RESTORE.md" || fail 'RESTORE docs' +pass 'restore manifest and docs' +/usr/bin/printf 'control duty separation: %s focused checks passed\n' "$passes"