From 5dae83064fab2ec9058173f643d1483a35bc9b03 Mon Sep 17 00:00:00 2001 From: hyperpolymath Date: Tue, 7 Jul 2026 16:10:59 +0100 Subject: [PATCH 01/12] fix(ci): estate-wide structural CI fixes - grant secret-scanner reusable its requested job permissions - drop invalid timeout-minutes on reusable-call jobs - drop hashFiles() from job-level if: expressions --- .github/workflows/scorecard.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 31c5d93..66b518b 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -24,4 +24,3 @@ jobs: contents: read security-events: write id-token: write - timeout-minutes: 15 From adf737a46da4e7051a5472999925b4c2a9cc0ebf Mon Sep 17 00:00:00 2001 From: hyperpolymath Date: Tue, 7 Jul 2026 21:25:20 +0100 Subject: [PATCH 02/12] fix(ci): switch to permissionless reusable workflow --- .github/workflows/secret-scanner.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index a935580..4a9129b 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -13,8 +13,6 @@ concurrency: # at plan time with zero jobs. permissions: contents: read - pull-requests: write - actions: read # Single secret scanner. The standards reusable runs gitleaks (+ a Rust-secrets # check). An inline TruffleHog job previously lived here, but the reusable # DELIBERATELY retired TruffleHog as redundant (gitleaks gives sufficient @@ -23,5 +21,5 @@ permissions: # .github/workflows/README.adoc. jobs: scan: - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@a44c93a5cd3d293ee3e8b488444dcdc19c4e5d49 + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@db12a6ad3ef9076d5f7bdcf98d7d15cf8547555a secrets: inherit From 1a0903f6bd1c2051b52bd37148ac995de6822f4d Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Tue, 7 Jul 2026 21:35:21 +0100 Subject: [PATCH 03/12] fix(ci): clear OSSF Scorecard startup_failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes the OSSF Scorecard workflow, which failed at startup on every run. **Cause:** reusable-workflow caller missing `security-events: write`+`id-token: write` (reusable perms are capped by the caller), and/or an illegal `timeout-minutes` key on a `uses:` job. **Fix:** inject caller permissions and/or drop `timeout-minutes`. Pin preserved. Verified with actionlint. Part of an estate-wide Scorecard remediation. 🤖 Generated with Antigravity --- .github/workflows/scorecard.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 66b518b..523a435 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -16,10 +16,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@a44c93a5cd3d293ee3e8b488444dcdc19c4e5d49 - # Reusable called-workflow permissions are CAPPED by the caller's grants; - # without security-events: write here the scorecard SARIF upload fails with - # startup_failure (hypatia WF018). id-token: write enables OIDC publish. + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@d7c22711e830e1f383846472f6e9b99debdb201e permissions: contents: read security-events: write From 8ac6d5f3d5b84f3cc8d529b26796768afcb60a7a Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 16 Jul 2026 21:56:59 +0100 Subject: [PATCH 04/12] docs(readme): move banner into docs/ to satisfy root allowlist check-root-shape.sh rejected cicd-squabbler.png at repo root: the allowlist admits root entries only where the tooling requires them, and an AsciiDoc banner does not. docs/ is already allow-listed. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.adoc | 2 +- cicd-squabbler.png => docs/cicd-squabbler.png | Bin 2 files changed, 1 insertion(+), 1 deletion(-) rename cicd-squabbler.png => docs/cicd-squabbler.png (100%) diff --git a/README.adoc b/README.adoc index 051a02a..127d2bd 100644 --- a/README.adoc +++ b/README.adoc @@ -4,7 +4,7 @@ :toc: preamble :icons: font -image::cicd-squabbler.png[CICD Squabbler Banner,width="100%",link=""] +image::docs/cicd-squabbler.png[CICD Squabbler Banner,width="100%",link=""] image:https://img.shields.io/badge/License-MPL_2.0-blue.svg[code: MPL-2.0] image:https://img.shields.io/badge/docs-CC--BY--SA--4.0-green.svg[docs: CC-BY-SA-4.0] diff --git a/cicd-squabbler.png b/docs/cicd-squabbler.png similarity index 100% rename from cicd-squabbler.png rename to docs/cicd-squabbler.png From 7339c51f0a4d4d30c563515cef689f6baec1df55 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 19 Jul 2026 21:04:31 +0100 Subject: [PATCH 05/12] feat(core): add fail-closed admission policy --- crates/squabble-core/src/admission.rs | 615 ++++++++++++++++++++++++++ crates/squabble-core/src/lib.rs | 1 + 2 files changed, 616 insertions(+) create mode 100644 crates/squabble-core/src/admission.rs diff --git a/crates/squabble-core/src/admission.rs b/crates/squabble-core/src/admission.rs new file mode 100644 index 0000000..164e8f0 --- /dev/null +++ b/crates/squabble-core/src/admission.rs @@ -0,0 +1,615 @@ +// SPDX-License-Identifier: MPL-2.0 +// Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) +//! Pure admission policy for CI work and automation-created pull requests. +//! +//! This module deliberately owns no clock, database, GitHub client, or queue. +//! A host supplies a policy, a request, an authoritative budget snapshot, and +//! the current time. The evaluator then returns one deterministic decision. +//! Missing, incomplete, stale, malformed, or arithmetically unsafe evidence +//! can never produce [`AdmissionDecision::Admit`]. +//! +//! Admission is **not** a CI result. It only reserves permission to start work; +//! the existing gate invariant still requires the real checks to run and pass. + +use serde::{Deserialize, Serialize}; + +/// Hard ceilings and explicit exceptions for one admission domain. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AdmissionPolicy { + /// Maximum age of the budget evidence accepted by the evaluator. + pub max_snapshot_age_seconds: u64, + pub max_inflight_estate: u32, + pub max_inflight_repository: u32, + pub max_open_automation_prs_estate: u32, + pub max_open_automation_prs_repository: u32, + pub max_reserved_minutes_per_day: u64, + pub max_reserved_minutes_per_change: u64, + pub max_automatic_workflows_per_change: u32, + pub max_runner_jobs_per_change: u32, + /// New workflow files are quarantined unless the policy explicitly opts in. + pub allow_new_workflow_files: bool, + /// Broader automatic triggers are quarantined unless explicitly allowed. + pub allow_trigger_expansion: bool, +} + +/// What the caller wants the admission broker to permit. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum OperationClass { + CreatePullRequest, + RunCi, + Release, + Deployment, +} + +impl OperationClass { + fn requires_explicit_approval(self) -> bool { + matches!(self, Self::Release | Self::Deployment) + } +} + +/// The authenticated principal class reported by the host. +/// +/// This is evidence for receipts and later policy refinement, not a bypass: +/// every class, including [`ProducerClass::HumanOwner`], is subject to the +/// same capacity and cost ceilings. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ProducerClass { + HumanOwner, + Automation, + GitHubApp, +} + +/// Whether a privileged operation has a separately grounded human approval. +/// +/// The host is responsible for authenticating this fact. Merely being invoked +/// by an owner-class principal does not imply approval. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum ApprovalState { + None, + HumanApproved, +} + +/// How confidently the host could bound the workflow graph. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum TopologyKnowledge { + Known, + /// The graph depends on runtime expressions such as a generated matrix. + Dynamic, + /// The graph could not be parsed or fully inspected. + Unknown, +} + +/// Conservative upper-bound inputs for one change. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct ProjectedCost { + pub topology: TopologyKnowledge, + /// Independently-triggered automatic workflow registrations. + pub automatic_workflows: u32, + /// Runner-bearing jobs after statically expanding bounded matrices. + pub runner_jobs: u32, + /// Upper-bound timeout applied to each runner job. + pub max_job_timeout_minutes: u64, + /// Fixed runner work not represented by `runner_jobs`. + pub fixed_runner_minutes: u64, + pub new_workflow_files: u32, + pub trigger_expansions: u32, +} + +impl ProjectedCost { + /// Conservative runner-minute reservation, checked for integer overflow. + pub fn reserved_minutes(&self) -> Option { + u64::from(self.runner_jobs) + .checked_mul(self.max_job_timeout_minutes)? + .checked_add(self.fixed_runner_minutes) + } +} + +/// One request evaluated against an authoritative snapshot. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct AdmissionRequest { + pub repository_id: u64, + /// Full object ID whose exact content will be admitted. + pub head_sha: String, + pub operation: OperationClass, + pub producer: ProducerClass, + pub approval: ApprovalState, + pub projected: ProjectedCost, +} + +/// Capacity evidence supplied by the stateful broker/ledger. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BudgetSnapshot { + pub captured_at_unix_seconds: u64, + /// False when any required counter could not be obtained. + pub data_complete: bool, + /// The head observed by the broker when it built this snapshot. + pub observed_head_sha: String, + pub estate_inflight: u32, + pub repository_inflight: u32, + pub estate_open_automation_prs: u32, + pub repository_open_automation_prs: u32, + pub reserved_minutes_today: u64, +} + +/// Stable machine-readable reason for a non-admission decision. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum AdmissionReason { + MissingPolicy, + MissingBudgetSnapshot, + IncompleteBudgetSnapshot, + BudgetSnapshotFromFuture, + StaleBudgetSnapshot, + InvalidHeadSha, + HeadShaChanged, + CostOverflow, + DynamicTopology, + UnknownTopology, + WorkflowFilesAdded, + TriggerExpanded, + PrivilegedOperationNeedsApproval, + AutomaticWorkflowCapExceeded, + RunnerJobCapExceeded, + ChangeBudgetExceeded, + EstateInflightCapReached, + RepositoryInflightCapReached, + EstateOpenPrCapReached, + RepositoryOpenPrCapReached, + DailyBudgetExceeded, +} + +/// The deterministic result of evaluating an admission request. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(tag = "decision", rename_all = "kebab-case")] +pub enum AdmissionDecision { + /// Static evidence is sound and capacity has been reserved conceptually. + /// A stateful host must atomically commit the reservation before dispatch. + Admit { reserved_minutes: u64 }, + /// Sound work cannot start until transient capacity becomes available. + Queue { reason: AdmissionReason }, + /// A human must review an explicit policy/topology risk. + Quarantine { reason: AdmissionReason }, + /// Evidence is absent, invalid, stale, or unsafe to calculate. + Deny { reason: AdmissionReason }, +} + +fn deny(reason: AdmissionReason) -> AdmissionDecision { + AdmissionDecision::Deny { reason } +} + +fn queue(reason: AdmissionReason) -> AdmissionDecision { + AdmissionDecision::Queue { reason } +} + +fn quarantine(reason: AdmissionReason) -> AdmissionDecision { + AdmissionDecision::Quarantine { reason } +} + +/// Evaluate one request without performing any side effect. +/// +/// Checks are intentionally ordered from evidence integrity, through static +/// policy, to transient capacity. A malformed request is denied even when the +/// estate is also full; it must not become eligible merely because a queue +/// later drains. +pub fn evaluate_admission( + policy: Option<&AdmissionPolicy>, + request: &AdmissionRequest, + snapshot: Option<&BudgetSnapshot>, + now_unix_seconds: u64, +) -> AdmissionDecision { + let Some(policy) = policy else { + return deny(AdmissionReason::MissingPolicy); + }; + let Some(snapshot) = snapshot else { + return deny(AdmissionReason::MissingBudgetSnapshot); + }; + if !snapshot.data_complete { + return deny(AdmissionReason::IncompleteBudgetSnapshot); + } + if snapshot.captured_at_unix_seconds > now_unix_seconds { + return deny(AdmissionReason::BudgetSnapshotFromFuture); + } + if now_unix_seconds - snapshot.captured_at_unix_seconds > policy.max_snapshot_age_seconds { + return deny(AdmissionReason::StaleBudgetSnapshot); + } + if !valid_sha(&request.head_sha) || !valid_sha(&snapshot.observed_head_sha) { + return deny(AdmissionReason::InvalidHeadSha); + } + if request.head_sha != snapshot.observed_head_sha { + return deny(AdmissionReason::HeadShaChanged); + } + + let Some(reserved_minutes) = request.projected.reserved_minutes() else { + return deny(AdmissionReason::CostOverflow); + }; + let Some(day_after_reservation) = snapshot + .reserved_minutes_today + .checked_add(reserved_minutes) + else { + return deny(AdmissionReason::CostOverflow); + }; + + match request.projected.topology { + TopologyKnowledge::Known => {} + TopologyKnowledge::Dynamic => { + return quarantine(AdmissionReason::DynamicTopology); + } + TopologyKnowledge::Unknown => { + return quarantine(AdmissionReason::UnknownTopology); + } + } + if request.projected.new_workflow_files > 0 && !policy.allow_new_workflow_files { + return quarantine(AdmissionReason::WorkflowFilesAdded); + } + if request.projected.trigger_expansions > 0 && !policy.allow_trigger_expansion { + return quarantine(AdmissionReason::TriggerExpanded); + } + if request.operation.requires_explicit_approval() + && request.approval != ApprovalState::HumanApproved + { + return quarantine(AdmissionReason::PrivilegedOperationNeedsApproval); + } + if request.projected.automatic_workflows > policy.max_automatic_workflows_per_change { + return quarantine(AdmissionReason::AutomaticWorkflowCapExceeded); + } + if request.projected.runner_jobs > policy.max_runner_jobs_per_change { + return quarantine(AdmissionReason::RunnerJobCapExceeded); + } + if reserved_minutes > policy.max_reserved_minutes_per_change { + return queue(AdmissionReason::ChangeBudgetExceeded); + } + if snapshot.estate_inflight >= policy.max_inflight_estate { + return queue(AdmissionReason::EstateInflightCapReached); + } + if snapshot.repository_inflight >= policy.max_inflight_repository { + return queue(AdmissionReason::RepositoryInflightCapReached); + } + if request.operation == OperationClass::CreatePullRequest { + if snapshot.estate_open_automation_prs >= policy.max_open_automation_prs_estate { + return queue(AdmissionReason::EstateOpenPrCapReached); + } + if snapshot.repository_open_automation_prs >= policy.max_open_automation_prs_repository { + return queue(AdmissionReason::RepositoryOpenPrCapReached); + } + } + if day_after_reservation > policy.max_reserved_minutes_per_day { + return queue(AdmissionReason::DailyBudgetExceeded); + } + + AdmissionDecision::Admit { reserved_minutes } +} + +/// GitHub currently supplies full SHA-1 object IDs for event head commits. +/// Reject the all-zero sentinel as well as abbreviated or non-hex strings. +fn valid_sha(sha: &str) -> bool { + sha.len() == 40 && sha.bytes().all(|b| b.is_ascii_hexdigit()) && !sha.bytes().all(|b| b == b'0') +} + +#[cfg(test)] +mod tests { + use super::*; + + const NOW: u64 = 1_800_000_000; + const SHA: &str = "0123456789abcdef0123456789abcdef01234567"; + + fn policy() -> AdmissionPolicy { + AdmissionPolicy { + max_snapshot_age_seconds: 60, + max_inflight_estate: 4, + max_inflight_repository: 1, + max_open_automation_prs_estate: 5, + max_open_automation_prs_repository: 1, + max_reserved_minutes_per_day: 500, + max_reserved_minutes_per_change: 100, + max_automatic_workflows_per_change: 1, + max_runner_jobs_per_change: 6, + allow_new_workflow_files: false, + allow_trigger_expansion: false, + } + } + + fn request() -> AdmissionRequest { + AdmissionRequest { + repository_id: 42, + head_sha: SHA.into(), + operation: OperationClass::RunCi, + producer: ProducerClass::Automation, + approval: ApprovalState::None, + projected: ProjectedCost { + topology: TopologyKnowledge::Known, + automatic_workflows: 1, + runner_jobs: 2, + max_job_timeout_minutes: 10, + fixed_runner_minutes: 1, + new_workflow_files: 0, + trigger_expansions: 0, + }, + } + } + + fn snapshot() -> BudgetSnapshot { + BudgetSnapshot { + captured_at_unix_seconds: NOW - 1, + data_complete: true, + observed_head_sha: SHA.into(), + estate_inflight: 0, + repository_inflight: 0, + estate_open_automation_prs: 0, + repository_open_automation_prs: 0, + reserved_minutes_today: 0, + } + } + + fn decision( + policy: Option<&AdmissionPolicy>, + request: &AdmissionRequest, + snapshot: Option<&BudgetSnapshot>, + ) -> AdmissionDecision { + evaluate_admission(policy, request, snapshot, NOW) + } + + #[test] + fn missing_policy_denies() { + assert_eq!( + decision(None, &request(), Some(&snapshot())), + deny(AdmissionReason::MissingPolicy) + ); + } + + #[test] + fn missing_budget_snapshot_denies() { + assert_eq!( + decision(Some(&policy()), &request(), None), + deny(AdmissionReason::MissingBudgetSnapshot) + ); + } + + #[test] + fn incomplete_budget_snapshot_denies() { + let mut s = snapshot(); + s.data_complete = false; + assert_eq!( + decision(Some(&policy()), &request(), Some(&s)), + deny(AdmissionReason::IncompleteBudgetSnapshot) + ); + } + + #[test] + fn stale_and_future_snapshots_deny() { + let mut stale = snapshot(); + stale.captured_at_unix_seconds = NOW - 61; + assert_eq!( + decision(Some(&policy()), &request(), Some(&stale)), + deny(AdmissionReason::StaleBudgetSnapshot) + ); + + let mut future = snapshot(); + future.captured_at_unix_seconds = NOW + 1; + assert_eq!( + decision(Some(&policy()), &request(), Some(&future)), + deny(AdmissionReason::BudgetSnapshotFromFuture) + ); + } + + #[test] + fn invalid_or_changed_sha_denies() { + let mut malformed = request(); + malformed.head_sha = "abc123".into(); + assert_eq!( + decision(Some(&policy()), &malformed, Some(&snapshot())), + deny(AdmissionReason::InvalidHeadSha) + ); + + let mut changed = snapshot(); + changed.observed_head_sha = "fedcba9876543210fedcba9876543210fedcba98".into(); + assert_eq!( + decision(Some(&policy()), &request(), Some(&changed)), + deny(AdmissionReason::HeadShaChanged) + ); + } + + #[test] + fn projected_cost_overflow_denies() { + let mut r = request(); + r.projected.runner_jobs = 2; + r.projected.max_job_timeout_minutes = u64::MAX; + assert_eq!( + decision(Some(&policy()), &r, Some(&snapshot())), + deny(AdmissionReason::CostOverflow) + ); + + let mut s = snapshot(); + s.reserved_minutes_today = u64::MAX; + assert_eq!( + decision(Some(&policy()), &request(), Some(&s)), + deny(AdmissionReason::CostOverflow) + ); + } + + #[test] + fn workflow_addition_and_trigger_expansion_quarantine() { + let mut added = request(); + added.projected.new_workflow_files = 1; + assert_eq!( + decision(Some(&policy()), &added, Some(&snapshot())), + quarantine(AdmissionReason::WorkflowFilesAdded) + ); + + let mut expanded = request(); + expanded.projected.trigger_expansions = 1; + assert_eq!( + decision(Some(&policy()), &expanded, Some(&snapshot())), + quarantine(AdmissionReason::TriggerExpanded) + ); + } + + #[test] + fn dynamic_and_unknown_topology_quarantine() { + let mut dynamic = request(); + dynamic.projected.topology = TopologyKnowledge::Dynamic; + assert_eq!( + decision(Some(&policy()), &dynamic, Some(&snapshot())), + quarantine(AdmissionReason::DynamicTopology) + ); + + let mut unknown = request(); + unknown.projected.topology = TopologyKnowledge::Unknown; + assert_eq!( + decision(Some(&policy()), &unknown, Some(&snapshot())), + quarantine(AdmissionReason::UnknownTopology) + ); + } + + #[test] + fn privileged_operations_need_separate_human_approval() { + for operation in [OperationClass::Release, OperationClass::Deployment] { + let mut r = request(); + r.operation = operation; + r.producer = ProducerClass::HumanOwner; + assert_eq!( + decision(Some(&policy()), &r, Some(&snapshot())), + quarantine(AdmissionReason::PrivilegedOperationNeedsApproval), + "owner identity alone must not approve {operation:?}" + ); + + r.approval = ApprovalState::HumanApproved; + assert!(matches!( + decision(Some(&policy()), &r, Some(&snapshot())), + AdmissionDecision::Admit { .. } + )); + } + } + + #[test] + fn static_topology_caps_quarantine() { + let mut workflows = request(); + workflows.projected.automatic_workflows = 2; + assert_eq!( + decision(Some(&policy()), &workflows, Some(&snapshot())), + quarantine(AdmissionReason::AutomaticWorkflowCapExceeded) + ); + + let mut jobs = request(); + jobs.projected.runner_jobs = 7; + assert_eq!( + decision(Some(&policy()), &jobs, Some(&snapshot())), + quarantine(AdmissionReason::RunnerJobCapExceeded) + ); + } + + #[test] + fn per_change_and_daily_budget_caps_queue() { + let mut p = policy(); + p.max_reserved_minutes_per_change = 20; + assert_eq!( + decision(Some(&p), &request(), Some(&snapshot())), + queue(AdmissionReason::ChangeBudgetExceeded) + ); + + let mut s = snapshot(); + s.reserved_minutes_today = 480; + assert_eq!( + decision(Some(&policy()), &request(), Some(&s)), + queue(AdmissionReason::DailyBudgetExceeded) + ); + } + + #[test] + fn inflight_caps_queue_at_the_boundary() { + let mut estate = snapshot(); + estate.estate_inflight = policy().max_inflight_estate; + assert_eq!( + decision(Some(&policy()), &request(), Some(&estate)), + queue(AdmissionReason::EstateInflightCapReached) + ); + + let mut repo = snapshot(); + repo.repository_inflight = policy().max_inflight_repository; + assert_eq!( + decision(Some(&policy()), &request(), Some(&repo)), + queue(AdmissionReason::RepositoryInflightCapReached) + ); + } + + #[test] + fn owner_identity_does_not_bypass_capacity() { + let mut r = request(); + r.producer = ProducerClass::HumanOwner; + let mut s = snapshot(); + s.estate_inflight = policy().max_inflight_estate; + assert_eq!( + decision(Some(&policy()), &r, Some(&s)), + queue(AdmissionReason::EstateInflightCapReached) + ); + } + + #[test] + fn open_pr_caps_apply_to_pr_creation_only() { + let mut r = request(); + r.operation = OperationClass::CreatePullRequest; + + let mut estate = snapshot(); + estate.estate_open_automation_prs = policy().max_open_automation_prs_estate; + assert_eq!( + decision(Some(&policy()), &r, Some(&estate)), + queue(AdmissionReason::EstateOpenPrCapReached) + ); + + let mut repo = snapshot(); + repo.repository_open_automation_prs = policy().max_open_automation_prs_repository; + assert_eq!( + decision(Some(&policy()), &r, Some(&repo)), + queue(AdmissionReason::RepositoryOpenPrCapReached) + ); + + let mut ci = request(); + ci.producer = ProducerClass::HumanOwner; + assert!(matches!( + decision(Some(&policy()), &ci, Some(&repo)), + AdmissionDecision::Admit { .. } + )); + } + + #[test] + fn exact_cost_boundary_admits_and_reports_reservation() { + let mut p = policy(); + p.max_reserved_minutes_per_change = 21; + p.max_reserved_minutes_per_day = 500; + let mut s = snapshot(); + s.reserved_minutes_today = 479; + assert_eq!( + decision(Some(&p), &request(), Some(&s)), + AdmissionDecision::Admit { + reserved_minutes: 21 + } + ); + } + + #[test] + fn explicit_policy_can_allow_reviewed_workflow_changes() { + let mut p = policy(); + p.allow_new_workflow_files = true; + p.allow_trigger_expansion = true; + let mut r = request(); + r.projected.new_workflow_files = 1; + r.projected.trigger_expansions = 1; + assert!(matches!( + decision(Some(&p), &r, Some(&snapshot())), + AdmissionDecision::Admit { .. } + )); + } + + #[test] + fn decision_round_trips_as_machine_readable_evidence() { + let d = AdmissionDecision::Quarantine { + reason: AdmissionReason::DynamicTopology, + }; + let json = serde_json::to_string(&d).expect("serialise"); + let back: AdmissionDecision = serde_json::from_str(&json).expect("deserialise"); + assert_eq!(d, back); + } +} diff --git a/crates/squabble-core/src/lib.rs b/crates/squabble-core/src/lib.rs index 0df8627..a1aba73 100644 --- a/crates/squabble-core/src/lib.rs +++ b/crates/squabble-core/src/lib.rs @@ -14,6 +14,7 @@ //! into this crate. This crate has **no** hypatia or estate dependency — the //! detachability is the whole point. +pub mod admission; pub mod gate; pub mod moves; pub mod outcome; From d7acc44f5607d0440b0c728cdb6b8209c16a3b18 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 00:42:01 +0100 Subject: [PATCH 06/12] fix(ci): update secret-scanner SHA to @7fdc2705df74b4e352d2a1cde3e87a5923fdf329 + simplify permissions + remove squisher-corpus placeholders --- .github/workflows/secret-scanner.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index bd736fb..1ffb34b 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -22,5 +22,5 @@ permissions: # .github/workflows/README.adoc. jobs: scan: - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@db12a6ad3ef9076d5f7bdcf98d7d15cf8547555a + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 secrets: inherit From 931ed78d592ccd96c053308bc425b253877d4567 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 04:49:54 +0100 Subject: [PATCH 07/12] fix(ci): update reusable workflow SHAs to @7fdc2705df74b4e352d2a1cde3e87a5923fdf329 Part of estate-wide standards#426 remediation - Batch 13 SHA update. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/scorecard.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 92d9d89..74f65ac 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -17,4 +17,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 7619372..908e072 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -26,4 +26,4 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d3679ff..0f3d326 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -17,7 +17,7 @@ permissions: jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@81dbf2dd854b1444fd6236fa2352474383b2c2b9 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 permissions: contents: read security-events: write From a45aff238237ef15145b2e911778830657e7e053 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 13 Aug 2026 19:17:26 +0100 Subject: [PATCH 08/12] fix(ci): add required permissions for reusable workflows (Bug B) Add security-events: write and id-token: write to workflow-level permissions in scorecard.yml for scorecard-reusable.yml calls. Ensure contents: read at workflow-level for secret-scanner.yml. Part of hyperpolymath/standards#426 remediation - Batch 2. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/scorecard.yml | 4 ++-- .github/workflows/secret-scanner.yml | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 0f3d326..d63bb50 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -12,9 +12,9 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: - actions: read contents: read - + security-events: write + id-token: write jobs: scorecard: uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 1ffb34b..7213807 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -12,7 +12,6 @@ concurrency: # (callee ⊆ caller), so the caller grants the union or the run startup-fails # at plan time with zero jobs. permissions: - actions: read contents: read # Single secret scanner. The standards reusable runs gitleaks (+ a Rust-secrets # check). An inline TruffleHog job previously lived here, but the reusable From 45696cd0e7c26e50e9557bc935eb15b2f4618b38 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 14 Aug 2026 17:54:18 +0100 Subject: [PATCH 09/12] chore(ci): bump standards reusable pins to fix Bug A and Bug B (#426) Update reusable workflow SHA from d135b05 to f2f8e6791b09f1f498f01b798e4670a1ebc9c986 to pick up fixes for: - Bug A: Invalid timeout-minutes at workflow_call level and duplicates - Bug B: Permissions escalation in scorecard-reusable Part of hyperpolymath/standards#426 remediation. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/mirror.yml | 2 +- .github/workflows/rust-ci.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 74f65ac..3c79e8c 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -17,4 +17,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 908e072..3b98df3 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -26,4 +26,4 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 8ea6fcf..5b65fee 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -12,5 +12,5 @@ permissions: contents: read jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@a44c93a5cd3d293ee3e8b488444dcdc19c4e5d49 + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 secrets: inherit diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index d3e7847..b0dc839 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -15,4 +15,4 @@ permissions: contents: read jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@a44c93a5cd3d293ee3e8b488444dcdc19c4e5d49 + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index d63bb50..ea22b39 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -17,7 +17,7 @@ permissions: id-token: write jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 permissions: contents: read security-events: write diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 7213807..3f7a1e9 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -21,5 +21,5 @@ permissions: # .github/workflows/README.adoc. jobs: scan: - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@7fdc2705df74b4e352d2a1cde3e87a5923fdf329 + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 secrets: inherit From 7ac9ebb58d8b5d4eae561ddc9804aa6b0da7ad15 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 14 Aug 2026 18:03:36 +0100 Subject: [PATCH 10/12] chore(ci): bump standards reusable pins to 5b1d0022 (#426) Final SHA update for Bug A and Bug B fixes. Part of hyperpolymath/standards#426 remediation. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe --- .github/workflows/governance.yml | 2 +- .github/workflows/hypatia-scan.yml | 2 +- .github/workflows/mirror.yml | 2 +- .github/workflows/rust-ci.yml | 2 +- .github/workflows/scorecard.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index 3c79e8c..dbc8c8d 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -17,4 +17,4 @@ permissions: jobs: governance: - uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 + uses: hyperpolymath/standards/.github/workflows/governance-reusable.yml@5b1d00229e5e8c0c0fbfedc7e80f37ea50f49236 diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index 3b98df3..8804386 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -26,4 +26,4 @@ permissions: jobs: scan: - uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 + uses: hyperpolymath/standards/.github/workflows/hypatia-scan-reusable.yml@5b1d00229e5e8c0c0fbfedc7e80f37ea50f49236 diff --git a/.github/workflows/mirror.yml b/.github/workflows/mirror.yml index 5b65fee..4bba10f 100644 --- a/.github/workflows/mirror.yml +++ b/.github/workflows/mirror.yml @@ -12,5 +12,5 @@ permissions: contents: read jobs: mirror: - uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 + uses: hyperpolymath/standards/.github/workflows/mirror-reusable.yml@5b1d00229e5e8c0c0fbfedc7e80f37ea50f49236 secrets: inherit diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml index b0dc839..f884aa3 100644 --- a/.github/workflows/rust-ci.yml +++ b/.github/workflows/rust-ci.yml @@ -15,4 +15,4 @@ permissions: contents: read jobs: rust-ci: - uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 + uses: hyperpolymath/standards/.github/workflows/rust-ci-reusable.yml@5b1d00229e5e8c0c0fbfedc7e80f37ea50f49236 diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index ea22b39..9eeec82 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -17,7 +17,7 @@ permissions: id-token: write jobs: scorecard: - uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 + uses: hyperpolymath/standards/.github/workflows/scorecard-reusable.yml@5b1d00229e5e8c0c0fbfedc7e80f37ea50f49236 permissions: contents: read security-events: write diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 3f7a1e9..86bdeb6 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -21,5 +21,5 @@ permissions: # .github/workflows/README.adoc. jobs: scan: - uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@f2f8e6791b09f1f498f01b798e4670a1ebc9c986 + uses: hyperpolymath/standards/.github/workflows/secret-scanner-reusable.yml@5b1d00229e5e8c0c0fbfedc7e80f37ea50f49236 secrets: inherit From 70a50ec640327001ea232fe3dfec7e0dc3939505 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 28 Aug 2026 06:05:13 +0100 Subject: [PATCH 11/12] chore(toolchain): keep .tool-versions -> .mise.toml pin conversion (R-16) Owner ruling 2026-08-28 (R-16/R-20/R-21): keep the pin conversion from the template-sync sweep, revert the rest. Pin content verified against HEAD:.tool-versions before commit. Co-Authored-By: Claude Fable 5 --- .mise.toml | 11 +++++++++++ .tool-versions | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) create mode 100644 .mise.toml delete mode 100644 .tool-versions diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..a352423 --- /dev/null +++ b/.mise.toml @@ -0,0 +1,11 @@ +[tools] +# = "Uncomment and customize for your project" +# = "rust nightly" +# = "just 1.40.0" +# = "nickel 1.10.0" +# = "gleam 1.8.0" +# = "elixir 1.18.0" +# = "erlang 27.2" +# = "zig 0.14.0" +# = "idris2 0.7.0" +rust = "nightly" diff --git a/.tool-versions b/.tool-versions deleted file mode 100644 index ce60c32..0000000 --- a/.tool-versions +++ /dev/null @@ -1,10 +0,0 @@ -# Uncomment and customize for your project -# rust nightly -# just 1.40.0 -# nickel 1.10.0 -# gleam 1.8.0 -# elixir 1.18.0 -# erlang 27.2 -# zig 0.14.0 -# idris2 0.7.0 -rust nightly From 9c4154ee22cead9913cef9308bd1d31c7ce742aa Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:14:14 +0100 Subject: [PATCH 12/12] fix: add K9! magic number and upgrade runner for tomllib --- .machine_readable/arrival-pack/claude-md.k9.ncl | 1 + .machine_readable/coaptation/coapt.k9.ncl | 1 + .machine_readable/contractiles/adjust/adjust.k9.ncl | 1 + .machine_readable/contractiles/bust/bust.k9.ncl | 1 + .machine_readable/contractiles/dust/dust.k9.ncl | 1 + .machine_readable/contractiles/intend/intend.k9.ncl | 1 + .machine_readable/contractiles/must/must.k9.ncl | 1 + .machine_readable/contractiles/trust/trust.k9.ncl | 1 + .machine_readable/self-validating/methodology-guard.k9.ncl | 1 + container/stapeln/deploy.k9.ncl | 1 + coordination.k9 | 1 + session/custom-checks.k9 | 1 + 12 files changed, 12 insertions(+) diff --git a/.machine_readable/arrival-pack/claude-md.k9.ncl b/.machine_readable/arrival-pack/claude-md.k9.ncl index 67c861e..d3fbb1c 100644 --- a/.machine_readable/arrival-pack/claude-md.k9.ncl +++ b/.machine_readable/arrival-pack/claude-md.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # diff --git a/.machine_readable/coaptation/coapt.k9.ncl b/.machine_readable/coaptation/coapt.k9.ncl index 25fa568..2c065bf 100644 --- a/.machine_readable/coaptation/coapt.k9.ncl +++ b/.machine_readable/coaptation/coapt.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) # diff --git a/.machine_readable/contractiles/adjust/adjust.k9.ncl b/.machine_readable/contractiles/adjust/adjust.k9.ncl index 0f1561e..4974ba7 100644 --- a/.machine_readable/contractiles/adjust/adjust.k9.ncl +++ b/.machine_readable/contractiles/adjust/adjust.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # adjust.k9.ncl — K9 trust-tier component of the adjust trident # Author: Jonathan D.A. Jewell diff --git a/.machine_readable/contractiles/bust/bust.k9.ncl b/.machine_readable/contractiles/bust/bust.k9.ncl index 3d28c22..cef8427 100644 --- a/.machine_readable/contractiles/bust/bust.k9.ncl +++ b/.machine_readable/contractiles/bust/bust.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # bust.k9.ncl — K9 trust-tier component of the bust trident # Author: Jonathan D.A. Jewell diff --git a/.machine_readable/contractiles/dust/dust.k9.ncl b/.machine_readable/contractiles/dust/dust.k9.ncl index 183fc07..6c51e54 100644 --- a/.machine_readable/contractiles/dust/dust.k9.ncl +++ b/.machine_readable/contractiles/dust/dust.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # dust.k9.ncl — K9 trust-tier component of the dust trident # Author: Jonathan D.A. Jewell diff --git a/.machine_readable/contractiles/intend/intend.k9.ncl b/.machine_readable/contractiles/intend/intend.k9.ncl index 723d9bc..7c9220b 100644 --- a/.machine_readable/contractiles/intend/intend.k9.ncl +++ b/.machine_readable/contractiles/intend/intend.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # intend.k9.ncl — K9 trust-tier component of the intend trident # Author: Jonathan D.A. Jewell diff --git a/.machine_readable/contractiles/must/must.k9.ncl b/.machine_readable/contractiles/must/must.k9.ncl index f139ef3..6de4df2 100644 --- a/.machine_readable/contractiles/must/must.k9.ncl +++ b/.machine_readable/contractiles/must/must.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # must.k9.ncl — K9 trust-tier component of the must trident # Author: Jonathan D.A. Jewell diff --git a/.machine_readable/contractiles/trust/trust.k9.ncl b/.machine_readable/contractiles/trust/trust.k9.ncl index 72ca271..0657470 100644 --- a/.machine_readable/contractiles/trust/trust.k9.ncl +++ b/.machine_readable/contractiles/trust/trust.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # trust.k9.ncl — K9 trust-tier component of the trust trident # Author: Jonathan D.A. Jewell diff --git a/.machine_readable/self-validating/methodology-guard.k9.ncl b/.machine_readable/self-validating/methodology-guard.k9.ncl index 7a424d7..796b0eb 100644 --- a/.machine_readable/self-validating/methodology-guard.k9.ncl +++ b/.machine_readable/self-validating/methodology-guard.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # Copyright (c) {{CURRENT_YEAR}} {{AUTHOR}} ({{OWNER}}) <{{AUTHOR_EMAIL}}> # diff --git a/container/stapeln/deploy.k9.ncl b/container/stapeln/deploy.k9.ncl index 02b7df4..8182fe8 100644 --- a/container/stapeln/deploy.k9.ncl +++ b/container/stapeln/deploy.k9.ncl @@ -1,3 +1,4 @@ +K9! # SPDX-License-Identifier: MPL-2.0 # deploy.k9.ncl — {{PROJECT_NAME}} deployment component (Hunt level) # diff --git a/coordination.k9 b/coordination.k9 index ba31125..e48adc9 100644 --- a/coordination.k9 +++ b/coordination.k9 @@ -1,3 +1,4 @@ +K9! # Thin coordination bindings for central session-management standards session_management: diff --git a/session/custom-checks.k9 b/session/custom-checks.k9 index bd932fa..35d87e7 100644 --- a/session/custom-checks.k9 +++ b/session/custom-checks.k9 @@ -1,3 +1,4 @@ +K9! # Local repository session checks (thin policy layer) version: "0.1"