Finish RAILIANCE-WP-0017 consumption-mode enforcement
Read the resource-control open/restricted signal and refuse new orders that would exceed a restricted entity's published allowance. Open and missing signals stay unchanged. Safety paths admit with an exception.
This commit is contained in:
parent
bf8c26cd61
commit
34a3123799
8 changed files with 437 additions and 4 deletions
|
|
@ -11,6 +11,7 @@ make db-status # gitea-db / forgejo-db cnpg cluster health (read-only)
|
|||
make apps-pg-status # shared apps-pg cluster health (read-only)
|
||||
make pg-status # confirms retired Bitnami postgresql-ha is absent
|
||||
make valkey-status # confirms Valkey is absent
|
||||
make consumption-preflight CONSUMING_ENTITY=entity:<slug> ESTIMATE_EUR=<eur>
|
||||
make db-deploy | apps-pg-deploy # production-touching
|
||||
```
|
||||
|
||||
|
|
|
|||
20
Makefile
20
Makefile
|
|
@ -66,9 +66,27 @@ forgejo-db-status: ## Show forgejo-db cnpg cluster health
|
|||
forgejo-db-shell: ## Open psql shell on forgejo-db primary
|
||||
$(KUBECTL) cnpg psql forgejo-db -n databases -- -U forgejo forgejo
|
||||
|
||||
CONSUMING_ENTITY ?=
|
||||
ESTIMATE_EUR ?=
|
||||
CONSUMPTION_CLASS ?= new-order
|
||||
CONSUMPTION_MODE_FILE ?= data/consumption-mode/current.json
|
||||
|
||||
##@ Consumption mode (RAILIANCE-WP-0017)
|
||||
|
||||
consumption-preflight: ## Refuse a new order that exceeds a restricted entity's allowance
|
||||
@test -n "$(CONSUMING_ENTITY)" || { echo 'set CONSUMING_ENTITY=entity:<slug>' >&2; exit 2; }
|
||||
CONSUMPTION_MODE_FILE='$(CONSUMPTION_MODE_FILE)' python3 scripts/consumption_mode.py check \
|
||||
--entity '$(CONSUMING_ENTITY)' \
|
||||
--class '$(CONSUMPTION_CLASS)' \
|
||||
$(if $(ESTIMATE_EUR),--estimate-eur '$(ESTIMATE_EUR)')
|
||||
|
||||
##@ Shared apps-pg (S5 application databases)
|
||||
|
||||
apps-pg-deploy: ## Apply shared apps-pg cnpg Cluster + NetworkPolicies
|
||||
@if [ -n "$(CONSUMING_ENTITY)" ]; then \
|
||||
$(MAKE) consumption-preflight CONSUMING_ENTITY='$(CONSUMING_ENTITY)' \
|
||||
ESTIMATE_EUR='$(ESTIMATE_EUR)' CONSUMPTION_CLASS=new-order; \
|
||||
fi
|
||||
$(KUBECTL) apply -f helm/apps-pg-cluster.yaml
|
||||
$(KUBECTL) apply -f helm/apps-pg-networkpolicies.yaml
|
||||
|
||||
|
|
@ -387,4 +405,4 @@ help: ## Show this help
|
|||
/^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \
|
||||
/^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' $(MAKEFILE_LIST)
|
||||
|
||||
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence help
|
||||
.PHONY: db-deploy db-status db-shell db-logs forgejo-db-deploy forgejo-db-status forgejo-db-shell apps-pg-deploy apps-pg-status apps-pg-shell apps-pg-logs net-kingdom-pg-inter-hub-networkpolicy-deploy pg-deploy pg-status pg-pgpool-check valkey-deploy valkey-status openbao-repo openbao-dry-run openbao-overlay-apply openbao-verify-login-overlay openbao-deploy openbao-status openbao-verify openbao-verify-post-unseal openbao-configure-initial openbao-configure-ssh openbao-verify-ssh openbao-verify-authenticated openbao-configure-external-secrets-issue-core openbao-configure-external-secrets-activity-core openbao-configure-external-secrets-forgejo openbao-validate-restore-evidence openbao-validate-emergency-evidence credential-grants-validate credential-change-applier-dry-run credential-change-applier-apply-plan credential-change-applier-apply credential-change-runbook credential-change-record-evidence credential-change-lifecycle-plan credential-change-lifecycle-event credential-change-import-inventory openbao-credential-change-appliers-dry-run openbao-configure-credential-change-appliers openbao-token-grants-dry-run openbao-configure-token-grants openbao-verify-token-grants-dry-run openbao-verify-token-grants openbao-verify-token-grants-smoke credential-helper-dry-run credential-tests credential-exec-ops-warden-smoke argocd-bootstrap-dry-run argocd-bootstrap-deploy argocd-repo-apply argocd-status backup forgejo-backup forgejo-backup-dry-run forgejo-backup-status forgejo-package-prune forgejo-package-prune-dry-run reef-deployables apps-pg-evidence consumption-preflight help
|
||||
|
|
|
|||
1
data/consumption-mode/current.json
Normal file
1
data/consumption-mode/current.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
[]
|
||||
63
docs/consumption-mode-enforcement.md
Normal file
63
docs/consumption-mode-enforcement.md
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
# Consumption-mode enforcement
|
||||
|
||||
Work record: `RAILIANCE-WP-0017-T01`
|
||||
Signal owner: `resource-control` (`docs/resource-procurement-facility.md`)
|
||||
Terms: `resource-control/docs/TermsOfResourceProcurement.md` § 11.4
|
||||
|
||||
`resource-control` publishes `open` / `restricted` and the remaining
|
||||
transfer-price allowance. This repo refuses **new orders** and **elastic
|
||||
usage** that would exceed that allowance. It does not throttle a running
|
||||
cluster and does not cancel a committed provider contract (OQ-2).
|
||||
|
||||
## Signal
|
||||
|
||||
Default file: `data/consumption-mode/current.json`
|
||||
|
||||
Shape (one record or a list):
|
||||
|
||||
```json
|
||||
{
|
||||
"schema_version": "0.1",
|
||||
"record_type": "consumption_mode",
|
||||
"financial_entity_id": "entity:coulomb",
|
||||
"period": "2026-09",
|
||||
"consumption_mode": "restricted",
|
||||
"new_transfer_charges_allowed_eur": "45.83",
|
||||
"terms_version": "0.1"
|
||||
}
|
||||
```
|
||||
|
||||
Refresh after a settlement close by writing that file (or pointing
|
||||
`CONSUMPTION_MODE_FILE` at a settlement-statement JSON — the helper
|
||||
projects `next_month_allowance_eur`). An empty list means no entity is
|
||||
restricted. Missing mode is **not** `open` and **not** `restricted`; it
|
||||
does not refuse.
|
||||
|
||||
`entity:railiance` is the procuring entity and is never restricted.
|
||||
|
||||
## Check
|
||||
|
||||
```bash
|
||||
make consumption-preflight \
|
||||
CONSUMING_ENTITY=entity:coulomb \
|
||||
ESTIMATE_EUR=20 \
|
||||
CONSUMPTION_CLASS=new-order
|
||||
```
|
||||
|
||||
| Class | Restricted behaviour |
|
||||
| --- | --- |
|
||||
| `new-order` / `elastic` | Refuse if estimate is missing or greater than the allowance |
|
||||
| `safety` | Admit; print an exception line (backup, identity, ingress needed to avoid data loss) |
|
||||
| `committed` | Admit; flag for human financial authority (do not cancel) |
|
||||
|
||||
## Where it is wired
|
||||
|
||||
| Target | Class | Notes |
|
||||
| --- | --- | --- |
|
||||
| `make consumption-preflight` | caller-supplied | required on any new consuming order |
|
||||
| `make apps-pg-deploy` | `new-order` | only when `CONSUMING_ENTITY` is set (a new consumer, not a re-apply) |
|
||||
| `make valkey-deploy` | `new-order` | already gated; would still have to pass this check |
|
||||
| Backup / restore deploys | `safety` | continue under restriction; overage is an exception |
|
||||
|
||||
A restricted entity cannot place a new order that would exceed the
|
||||
published allowance. An `open` entity is unchanged.
|
||||
|
|
@ -15,6 +15,7 @@ unless the consumer-visible fields change.
|
|||
| Standing runtime secret | `ccr-workload-kv.v1` | Workloads that need a KV path via External Secrets | `schemas/credential-change-request.schema.yaml` |
|
||||
| Short-lived lease | `credential-grant.v1` | Operators/agents and dynamic DB roles | `credential-grants/catalog.yaml` |
|
||||
| Rapp credential bind | `rapp-lane.v1` | How a rapp names the two lanes above | `docs/rapp-credential-lane-binding.md` |
|
||||
| Consumption-mode gate | `consumption-mode.v1` | New orders for a restricted financial entity | `docs/consumption-mode-enforcement.md` |
|
||||
|
||||
## Compatibility rules
|
||||
|
||||
|
|
|
|||
199
scripts/consumption_mode.py
Executable file
199
scripts/consumption_mode.py
Executable file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Admit or refuse a platform order from the resource-control consumption-mode signal.
|
||||
|
||||
RAILIANCE-WP-0017-T01. resource-control publishes the signal; this repo
|
||||
enforces it. Missing or unknown mode is not restricted (facility runbook).
|
||||
Only restricted + a new-order/elastic charge that exceeds the published
|
||||
allowance is refused. Safety paths are admitted and recorded as exceptions.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_SIGNAL = REPO_DIR / "data" / "consumption-mode" / "current.json"
|
||||
ENTITY_RE_PREFIX = "entity:"
|
||||
MODES = {"open", "restricted"}
|
||||
CLASSES = {"new-order", "elastic", "safety", "committed"}
|
||||
RAILIANCE = "entity:railiance"
|
||||
|
||||
|
||||
def money(value: Any) -> Decimal | None:
|
||||
if value is None or value == "":
|
||||
return None
|
||||
try:
|
||||
amount = Decimal(str(value))
|
||||
except (InvalidOperation, ValueError):
|
||||
return None
|
||||
if amount < 0:
|
||||
return None
|
||||
return amount.quantize(Decimal("0.01"))
|
||||
|
||||
|
||||
def load_payload(path: Path) -> list[dict[str, Any]]:
|
||||
if not path.is_file():
|
||||
return []
|
||||
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||
if isinstance(raw, list):
|
||||
out: list[dict[str, Any]] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
if item.get("record_type") == "settlement_statement":
|
||||
out.append(signal_from_statement(item))
|
||||
else:
|
||||
out.append(item)
|
||||
return out
|
||||
if isinstance(raw, dict) and raw.get("record_type") == "consumption_mode":
|
||||
return [raw]
|
||||
if isinstance(raw, dict) and isinstance(raw.get("signals"), list):
|
||||
return [item for item in raw["signals"] if isinstance(item, dict)]
|
||||
if isinstance(raw, dict) and raw.get("record_type") == "settlement_statement":
|
||||
return [signal_from_statement(raw)]
|
||||
return []
|
||||
|
||||
|
||||
def signal_from_statement(statement: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
"schema_version": "0.1",
|
||||
"record_type": "consumption_mode",
|
||||
"financial_entity_id": statement.get("financial_entity_id"),
|
||||
"period": statement.get("period"),
|
||||
"consumption_mode": statement.get("consumption_mode"),
|
||||
"new_transfer_charges_allowed_eur": statement.get("next_month_allowance_eur"),
|
||||
"terms_version": statement.get("terms_version"),
|
||||
}
|
||||
|
||||
|
||||
def signal_for(signals: list[dict[str, Any]], entity_id: str) -> dict[str, Any] | None:
|
||||
matches = [
|
||||
item
|
||||
for item in signals
|
||||
if item.get("financial_entity_id") == entity_id
|
||||
and item.get("record_type", "consumption_mode") == "consumption_mode"
|
||||
]
|
||||
return matches[-1] if matches else None
|
||||
|
||||
|
||||
def decide(
|
||||
*,
|
||||
entity_id: str,
|
||||
order_class: str,
|
||||
estimate_eur: Decimal | None,
|
||||
signal: dict[str, Any] | None,
|
||||
) -> dict[str, Any]:
|
||||
if entity_id == RAILIANCE:
|
||||
return _result("admit", "railiance-self-use", "open", None, order_class)
|
||||
if signal is None:
|
||||
return _result("admit", "no-signal-not-restricted", None, None, order_class)
|
||||
mode = signal.get("consumption_mode")
|
||||
if mode not in MODES:
|
||||
return _result("admit", "unknown-mode-not-restricted", None, None, order_class)
|
||||
allowance = money(signal.get("new_transfer_charges_allowed_eur"))
|
||||
if mode == "open":
|
||||
return _result("admit", "open", mode, allowance, order_class)
|
||||
if order_class == "safety":
|
||||
return _result(
|
||||
"admit",
|
||||
"safety-exception",
|
||||
mode,
|
||||
allowance,
|
||||
order_class,
|
||||
exception=True,
|
||||
)
|
||||
if order_class == "committed":
|
||||
return _result(
|
||||
"admit",
|
||||
"committed-flagged",
|
||||
mode,
|
||||
allowance,
|
||||
order_class,
|
||||
exception=True,
|
||||
)
|
||||
if allowance is None:
|
||||
return _result("refuse", "restricted-without-allowance", mode, allowance, order_class)
|
||||
if estimate_eur is None:
|
||||
return _result("refuse", "restricted-estimate-required", mode, allowance, order_class)
|
||||
if estimate_eur > allowance:
|
||||
return _result("refuse", "exceeds-allowance", mode, allowance, order_class)
|
||||
return _result("admit", "within-allowance", mode, allowance, order_class)
|
||||
|
||||
|
||||
def _result(
|
||||
decision: str,
|
||||
reason: str,
|
||||
mode: str | None,
|
||||
allowance: Decimal | None,
|
||||
order_class: str,
|
||||
exception: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"decision": decision,
|
||||
"reason": reason,
|
||||
"consumption_mode": mode,
|
||||
"allowance_eur": None if allowance is None else f"{allowance:.2f}",
|
||||
"order_class": order_class,
|
||||
"exception": exception,
|
||||
}
|
||||
|
||||
|
||||
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description=__doc__.split("\n\n", 1)[0])
|
||||
parser.add_argument("command", choices=["check"], help="check an order against the signal")
|
||||
parser.add_argument("--entity", required=True, help="financial_entity_id, e.g. entity:coulomb")
|
||||
parser.add_argument(
|
||||
"--class",
|
||||
dest="order_class",
|
||||
choices=sorted(CLASSES),
|
||||
default="new-order",
|
||||
)
|
||||
parser.add_argument("--estimate-eur", help="transfer-price estimate for the new order")
|
||||
parser.add_argument(
|
||||
"--signal",
|
||||
type=Path,
|
||||
default=Path(os.environ.get("CONSUMPTION_MODE_FILE", DEFAULT_SIGNAL)),
|
||||
help="JSON signal file (list, one record, or a settlement statement)",
|
||||
)
|
||||
return parser.parse_args(argv)
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
args = parse_args(argv)
|
||||
if not args.entity.startswith(ENTITY_RE_PREFIX) or len(args.entity) < 8:
|
||||
print(f"[FAIL] --entity must be entity:<slug>: {args.entity}", file=sys.stderr)
|
||||
return 2
|
||||
estimate = money(args.estimate_eur)
|
||||
if args.estimate_eur and estimate is None:
|
||||
print(f"[FAIL] --estimate-eur is not a non-negative EUR amount", file=sys.stderr)
|
||||
return 2
|
||||
try:
|
||||
signals = load_payload(args.signal)
|
||||
except json.JSONDecodeError as exc:
|
||||
print(f"[FAIL] signal is not JSON: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
signal = signal_for(signals, args.entity)
|
||||
result = decide(
|
||||
entity_id=args.entity,
|
||||
order_class=args.order_class,
|
||||
estimate_eur=estimate,
|
||||
signal=signal,
|
||||
)
|
||||
tag = "OK" if result["decision"] == "admit" else "REFUSE"
|
||||
print(
|
||||
f"[{tag}] {args.entity} {result['order_class']} "
|
||||
f"mode={result['consumption_mode'] or 'unknown'} "
|
||||
f"allowance={result['allowance_eur']} reason={result['reason']}"
|
||||
)
|
||||
if result["exception"]:
|
||||
print("[EXCEPTION] restricted safety/committed path; record overage on the next statement")
|
||||
return 0 if result["decision"] == "admit" else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
139
tests/test_consumption_mode.py
Normal file
139
tests/test_consumption_mode.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from decimal import Decimal
|
||||
from pathlib import Path
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parents[1]
|
||||
SPEC = importlib.util.spec_from_file_location(
|
||||
"consumption_mode", REPO_DIR / "scripts/consumption_mode.py"
|
||||
)
|
||||
cm = importlib.util.module_from_spec(SPEC)
|
||||
assert SPEC.loader is not None
|
||||
sys.modules[SPEC.name] = cm
|
||||
SPEC.loader.exec_module(cm)
|
||||
|
||||
|
||||
def restricted(allowance: str = "45.83") -> dict:
|
||||
return {
|
||||
"schema_version": "0.1",
|
||||
"record_type": "consumption_mode",
|
||||
"financial_entity_id": "entity:coulomb",
|
||||
"period": "2026-09",
|
||||
"consumption_mode": "restricted",
|
||||
"new_transfer_charges_allowed_eur": allowance,
|
||||
"terms_version": "0.1",
|
||||
}
|
||||
|
||||
|
||||
class ConsumptionModeTests(unittest.TestCase):
|
||||
def test_open_entity_unchanged(self) -> None:
|
||||
signal = restricted()
|
||||
signal["consumption_mode"] = "open"
|
||||
signal["new_transfer_charges_allowed_eur"] = None
|
||||
result = cm.decide(
|
||||
entity_id="entity:coulomb",
|
||||
order_class="new-order",
|
||||
estimate_eur=Decimal("200.00"),
|
||||
signal=signal,
|
||||
)
|
||||
self.assertEqual(result["decision"], "admit")
|
||||
self.assertEqual(result["reason"], "open")
|
||||
|
||||
def test_restricted_cannot_exceed_allowance(self) -> None:
|
||||
result = cm.decide(
|
||||
entity_id="entity:coulomb",
|
||||
order_class="new-order",
|
||||
estimate_eur=Decimal("45.84"),
|
||||
signal=restricted(),
|
||||
)
|
||||
self.assertEqual(result["decision"], "refuse")
|
||||
self.assertEqual(result["reason"], "exceeds-allowance")
|
||||
|
||||
def test_restricted_within_allowance(self) -> None:
|
||||
result = cm.decide(
|
||||
entity_id="entity:coulomb",
|
||||
order_class="new-order",
|
||||
estimate_eur=Decimal("45.83"),
|
||||
signal=restricted(),
|
||||
)
|
||||
self.assertEqual(result["decision"], "admit")
|
||||
self.assertEqual(result["reason"], "within-allowance")
|
||||
|
||||
def test_restricted_without_estimate_refused(self) -> None:
|
||||
result = cm.decide(
|
||||
entity_id="entity:coulomb",
|
||||
order_class="elastic",
|
||||
estimate_eur=None,
|
||||
signal=restricted(),
|
||||
)
|
||||
self.assertEqual(result["decision"], "refuse")
|
||||
self.assertEqual(result["reason"], "restricted-estimate-required")
|
||||
|
||||
def test_safety_admitted_as_exception(self) -> None:
|
||||
result = cm.decide(
|
||||
entity_id="entity:coulomb",
|
||||
order_class="safety",
|
||||
estimate_eur=Decimal("200.00"),
|
||||
signal=restricted(),
|
||||
)
|
||||
self.assertEqual(result["decision"], "admit")
|
||||
self.assertTrue(result["exception"])
|
||||
|
||||
def test_no_signal_is_not_restricted(self) -> None:
|
||||
result = cm.decide(
|
||||
entity_id="entity:coulomb",
|
||||
order_class="new-order",
|
||||
estimate_eur=Decimal("999.00"),
|
||||
signal=None,
|
||||
)
|
||||
self.assertEqual(result["decision"], "admit")
|
||||
self.assertEqual(result["reason"], "no-signal-not-restricted")
|
||||
|
||||
def test_railiance_self_use_always_open(self) -> None:
|
||||
result = cm.decide(
|
||||
entity_id="entity:railiance",
|
||||
order_class="new-order",
|
||||
estimate_eur=Decimal("999.00"),
|
||||
signal=restricted(),
|
||||
)
|
||||
self.assertEqual(result["decision"], "admit")
|
||||
self.assertEqual(result["reason"], "railiance-self-use")
|
||||
|
||||
def test_cli_refuses_restricted_overage(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "signal.json"
|
||||
path.write_text(json.dumps([restricted()]), encoding="utf-8")
|
||||
code = cm.main(
|
||||
[
|
||||
"check",
|
||||
"--entity",
|
||||
"entity:coulomb",
|
||||
"--estimate-eur",
|
||||
"50",
|
||||
"--signal",
|
||||
str(path),
|
||||
]
|
||||
)
|
||||
self.assertEqual(code, 2)
|
||||
|
||||
def test_cli_admits_open(self) -> None:
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
path = Path(tmp) / "signal.json"
|
||||
path.write_text("[]", encoding="utf-8")
|
||||
code = cm.main(
|
||||
[
|
||||
"check",
|
||||
"--entity",
|
||||
"entity:coulomb",
|
||||
"--estimate-eur",
|
||||
"50",
|
||||
"--signal",
|
||||
str(path),
|
||||
]
|
||||
)
|
||||
self.assertEqual(code, 0)
|
||||
|
|
@ -4,11 +4,11 @@ type: workplan
|
|||
title: "Enforce resource-control consumption mode"
|
||||
domain: financials
|
||||
repo: railiance-platform
|
||||
status: proposed
|
||||
status: finished
|
||||
owner: codex
|
||||
topic_slug: railiance
|
||||
created: "2026-08-14"
|
||||
updated: "2026-08-14"
|
||||
updated: "2026-08-15"
|
||||
related:
|
||||
- RESOURCE-WP-0005
|
||||
origin: residual
|
||||
|
|
@ -42,7 +42,7 @@ Runbook: `resource-control/docs/resource-procurement-facility.md`.
|
|||
|
||||
```task
|
||||
id: RAILIANCE-WP-0017-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "1c872918-0872-4b2a-8596-89b106239e54"
|
||||
```
|
||||
|
|
@ -53,3 +53,14 @@ the current period.
|
|||
|
||||
Done when a restricted entity cannot place a new order that would
|
||||
exceed the published allowance, and an `open` entity is unchanged.
|
||||
|
||||
Delivered 2026-08-15: `scripts/consumption_mode.py`,
|
||||
`docs/consumption-mode-enforcement.md`, `make consumption-preflight`.
|
||||
Restricted + estimate above allowance exits 2. Open and missing-signal
|
||||
admit. Safety/committed paths admit with an exception line. `apps-pg-deploy`
|
||||
runs the gate when `CONSUMING_ENTITY` is set.
|
||||
|
||||
## Outcome
|
||||
|
||||
T01 done. Workplan finished. resource-control still owns the signal;
|
||||
this repo only enforces it.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue