Skip to content

Commit 2350277

Browse files
Pigbibicodex
andcommitted
feat(shadow): require deterministic risk decision for receipt admission
Co-Authored-By: Codex <noreply@openai.com>
1 parent 308d2e3 commit 2350277

5 files changed

Lines changed: 609 additions & 28 deletions

File tree

README.md

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,15 +44,34 @@ policy-gate receipt、风险摘要、deployment bundle 或前一账本回执缺
4444
Alpaca,也不代表 P5 已启用或 P4/P6 已获许可。后续部署会为该纯控制步骤单独接入受限的
4545
工件读取、create-only 回执写入与状态发布。
4646

47-
现已定义 P5 后置的 `qsl.tqqq_shadow_receipt_admission.v1`:它只能封装 controller 已返回的
48-
`RECEIPT_READY` 结果和已复核的虚拟账本回执。`CreateOnlyShadowReceiptStore` 是未来受限工件
49-
存储的最小接口:按 `cycle_id` 原子地 create-if-absent,已存在时读取后按 admission digest 对账;
50-
digest 不同只返回 `PARKED/receipt_conflict`,绝不覆盖。仓内 `InMemoryShadowReceiptStore` 仅用于
51-
确定性测试与本地回放,不连接文件系统、GCS、GitHub Actions、券商或任何凭据。
52-
53-
未来每个 P5 周期必须先提供完整 forward observation、独立 policy-gate receipt、风险摘要和
54-
deployment bundle,且(如果存在)上一账本回执必须有效;任一缺失/无效仍由 controller 返回
55-
`PARKED`。只有 `RECEIPT_READY` 才可调用该 create-only port;本次没有添加 cron、环境变量或部署。
47+
P5 后置 admission 现为 `qsl.tqqq_shadow_receipt_admission.v2`。它除了 controller 已返回的
48+
`RECEIPT_READY` 结果和已复核的虚拟账本回执,还必须消费一个
49+
`qsl.tqqq_shadow_risk_gate_decision_envelope.v1`。该 envelope 包含 QSL
50+
`qsl.deterministic_risk_gate_decision.v1` 的完整、可重算摘要,以及与本 P5 cycle 的
51+
`cycle_id``computed_at``qsl.forward_observation_risk_control.v1` 三元组绑定。
52+
53+
适配器不导入、不复制或重新执行 `QuantRuntimeSettings` 的风险内核;它只严格检查 QSL decision
54+
的闭合字段、canonical SHA-256、`ALLOW_NEW_RISK``CLOSED` breaker 建议、
55+
`manual_reset_required=true` 及上述 P5 绑定。写入的 v2 admission 只保留脱敏的 source
56+
risk-control、risk-gate policy id/version/SHA-256 和 decision SHA-256;不会保留账户、价格、
57+
notional、投影金额或订单材料。
58+
59+
缺少 envelope、结构/摘要无效、cycle/时间/source risk-control 不匹配,或 decision 为
60+
`NEW_RISK_PROHIBITED` 时,`persist_shadow_cycle_outcome` 都在读取或调用存储前返回 `PARKED`
61+
不会自动 reset breaker。`CreateOnlyShadowReceiptStore` 仍是未来受限工件存储的最小接口:按
62+
`cycle_id` 原子地 create-if-absent,已存在时读取后按 admission digest 对账;digest 不同只返回
63+
`PARKED/receipt_conflict`,绝不覆盖。仓内 `InMemoryShadowReceiptStore` 仅用于确定性测试与本地
64+
回放,不连接文件系统、GCS、GitHub Actions、券商或任何凭据。
65+
66+
未来每个 P5 周期必须先提供完整 forward observation、独立 policy-gate receipt、风险摘要、
67+
deployment bundle 和通过上述 adapter 的 deterministic risk decision,且(如果存在)上一账本回执
68+
必须有效;任一缺失/无效仍由 controller 或 admission 返回 `PARKED`。只有同时满足
69+
`RECEIPT_READY` 与风险决定的 create-only port 才可写入;本次没有添加 cron、环境变量或部署。
70+
71+
这仍不是账户级 shadow 启动:真正启用前,独立 gateway 必须从受限、已对账的快照运行 QSL risk
72+
kernel,生成 per-cycle envelope,并与 policy-gate 身份、工件读取和持久化 adapter 一起部署;AI、
73+
网页、GitHub Actions、策略代码都不能伪造输入或 reset breaker。此仓没有任何此类身份、账户、
74+
券商或网络能力。
5675

5776
```bash
5877
python -m alpaca_platform.shadow_ledger --input cycle.json --output receipt.json

src/alpaca_platform/__init__.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,17 @@
2323
validate_shadow_receipt_admission,
2424
validate_shadow_receipt_persistence_result,
2525
)
26+
from .shadow_risk_gate_decision import (
27+
DETERMINISTIC_RISK_GATE_DECISION_SCHEMA,
28+
P5_RISK_GATE_DECISION_ENVELOPE_SCHEMA,
29+
P5_RISK_GATE_DECISION_REFERENCE_SCHEMA,
30+
ShadowRiskGateDecisionError,
31+
ShadowRiskGateDecisionMismatchError,
32+
ShadowRiskGateDecisionProhibitedError,
33+
build_p5_risk_gate_decision_reference,
34+
calculate_p5_risk_gate_decision_envelope_sha256,
35+
validate_p5_risk_gate_decision_reference,
36+
)
2637
from .shadow_scheduler import (
2738
SCHEDULER_RESULT_SCHEMA,
2839
ShadowCycleOutcome,
@@ -33,7 +44,10 @@
3344

3445
__all__ = [
3546
"ADMISSION_SCHEMA",
47+
"DETERMINISTIC_RISK_GATE_DECISION_SCHEMA",
3648
"FORWARD_OBSERVATION_SCHEMA",
49+
"P5_RISK_GATE_DECISION_ENVELOPE_SCHEMA",
50+
"P5_RISK_GATE_DECISION_REFERENCE_SCHEMA",
3751
"PERSISTENCE_RESULT_SCHEMA",
3852
"POLICY_GATE_RECEIPT_SCHEMA",
3953
"SCHEDULER_RESULT_SCHEMA",
@@ -42,15 +56,21 @@
4256
"ShadowCycleOutcome",
4357
"ShadowLedgerError",
4458
"ShadowReceiptStoreError",
59+
"ShadowRiskGateDecisionError",
60+
"ShadowRiskGateDecisionMismatchError",
61+
"ShadowRiskGateDecisionProhibitedError",
4562
"ShadowSchedulerError",
63+
"build_p5_risk_gate_decision_reference",
4664
"build_shadow_ledger_receipt",
4765
"build_shadow_receipt_admission",
4866
"build_tqqq_shadow_cycle_input",
4967
"calculate_forward_observation_sha256",
68+
"calculate_p5_risk_gate_decision_envelope_sha256",
5069
"calculate_policy_gate_receipt_sha256",
5170
"calculate_shadow_receipt_admission_sha256",
5271
"persist_shadow_cycle_outcome",
5372
"run_tqqq_shadow_cycle",
73+
"validate_p5_risk_gate_decision_reference",
5474
"validate_shadow_cycle_input",
5575
"validate_shadow_ledger_receipt",
5676
"validate_shadow_receipt_admission",

src/alpaca_platform/shadow_receipt_store.py

Lines changed: 93 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
"""Create-only, no-broker persistence seam for bounded P5 shadow receipts.
22
33
This module runs only after ``shadow_scheduler``. It wraps an already
4-
validated ``RECEIPT_READY`` result in a closed admission artifact and exposes a
5-
small create-only storage port. The included in-memory implementation is for
6-
tests/local deterministic replay only: it has no filesystem, network,
7-
credential, market-data, broker, or scheduler dependency.
4+
validated ``RECEIPT_READY`` result in a closed admission artifact only after a
5+
separately produced deterministic risk decision passes its bounded adapter.
6+
It exposes a small create-only storage port. The included in-memory
7+
implementation is for tests/local deterministic replay only: it has no
8+
filesystem, network, credential, market-data, broker, or scheduler dependency.
89
"""
910

1011
from __future__ import annotations
@@ -16,13 +17,20 @@
1617
from typing import Any, Protocol
1718

1819
from .shadow_ledger import ShadowLedgerError, validate_shadow_ledger_receipt
20+
from .shadow_risk_gate_decision import (
21+
ShadowRiskGateDecisionError,
22+
ShadowRiskGateDecisionMismatchError,
23+
ShadowRiskGateDecisionProhibitedError,
24+
build_p5_risk_gate_decision_reference,
25+
validate_p5_risk_gate_decision_reference,
26+
)
1927
from .shadow_scheduler import (
2028
ShadowCycleOutcome,
2129
ShadowSchedulerError,
2230
validate_shadow_scheduler_result,
2331
)
2432

25-
ADMISSION_SCHEMA = "qsl.tqqq_shadow_receipt_admission.v1"
33+
ADMISSION_SCHEMA = "qsl.tqqq_shadow_receipt_admission.v2"
2634
PERSISTENCE_RESULT_SCHEMA = "qsl.tqqq_shadow_receipt_persistence_result.v1"
2735

2836
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
@@ -32,6 +40,7 @@
3240
"computed_at",
3341
"scheduler_result",
3442
"shadow_receipt",
43+
"risk_gate_decision",
3544
"admission_sha256",
3645
}
3746
_PERSISTENCE_RESULT_FIELDS = {
@@ -51,6 +60,10 @@
5160
"prior_receipt_invalid",
5261
"cycle_input_invalid",
5362
"ledger_receipt_invalid",
63+
"risk_gate_decision_missing",
64+
"risk_gate_decision_invalid",
65+
"risk_gate_decision_mismatch",
66+
"risk_gate_decision_prohibited",
5467
"receipt_conflict",
5568
}
5669

@@ -139,24 +152,40 @@ def validate_shadow_receipt_admission(value: Any) -> dict[str, Any]:
139152
_fail("shadow receipt admission computed_at must match controller result")
140153
if result["shadow_receipt_sha256"] != receipt["receipt_sha256"]:
141154
_fail("shadow receipt admission scheduler result must bind ledger receipt digest")
155+
try:
156+
risk_gate_decision = validate_p5_risk_gate_decision_reference(
157+
value["risk_gate_decision"],
158+
expected_source_risk_control=receipt["risk_control"],
159+
expected_computed_at=result["computed_at"],
160+
)
161+
except ShadowRiskGateDecisionMismatchError as exc:
162+
raise ShadowReceiptStoreError("shadow receipt admission risk-gate decision does not match this P5 receipt") from exc
163+
except ShadowRiskGateDecisionError as exc:
164+
raise ShadowReceiptStoreError("shadow receipt admission risk-gate decision is invalid") from exc
142165
normalized = {
143166
"schema": ADMISSION_SCHEMA,
144167
"cycle_id": result["cycle_id"],
145168
"computed_at": result["computed_at"],
146169
"scheduler_result": result,
147170
"shadow_receipt": receipt,
171+
"risk_gate_decision": risk_gate_decision,
148172
"admission_sha256": _digest(value["admission_sha256"], "shadow receipt admission.admission_sha256"),
149173
}
150174
if normalized["admission_sha256"] != calculate_shadow_receipt_admission_sha256(normalized):
151175
_fail("shadow receipt admission.admission_sha256 mismatch")
152176
return normalized
153177

154178

155-
def build_shadow_receipt_admission(outcome: ShadowCycleOutcome) -> dict[str, Any]:
179+
def build_shadow_receipt_admission(
180+
outcome: ShadowCycleOutcome,
181+
*,
182+
risk_gate_decision: Any,
183+
) -> dict[str, Any]:
156184
"""Convert one controller ``RECEIPT_READY`` outcome into an admission.
157185
158-
This function cannot create a ledger receipt. A parked result, a missing
159-
receipt, or an inconsistent outcome is rejected before a store is called.
186+
This function cannot create a ledger receipt or a risk decision. A parked
187+
result, a missing receipt, a prohibited decision, or an inconsistent
188+
binding is rejected before a store is called.
160189
"""
161190
try:
162191
result = validate_shadow_scheduler_result(outcome.result)
@@ -166,12 +195,23 @@ def build_shadow_receipt_admission(outcome: ShadowCycleOutcome) -> dict[str, Any
166195
_fail("only a RECEIPT_READY controller outcome can be admitted")
167196
if outcome.receipt is None:
168197
_fail("RECEIPT_READY controller outcome must include a ledger receipt")
198+
try:
199+
receipt = validate_shadow_ledger_receipt(outcome.receipt)
200+
except ShadowLedgerError as exc:
201+
raise ShadowReceiptStoreError("RECEIPT_READY controller outcome ledger receipt is invalid") from exc
202+
risk_gate_reference = build_p5_risk_gate_decision_reference(
203+
risk_gate_decision,
204+
expected_cycle_id=result["cycle_id"],
205+
expected_computed_at=result["computed_at"],
206+
expected_source_risk_control=receipt["risk_control"],
207+
)
169208
admission: dict[str, Any] = {
170209
"schema": ADMISSION_SCHEMA,
171210
"cycle_id": result["cycle_id"],
172211
"computed_at": result["computed_at"],
173212
"scheduler_result": result,
174-
"shadow_receipt": outcome.receipt,
213+
"shadow_receipt": receipt,
214+
"risk_gate_decision": risk_gate_reference,
175215
"admission_sha256": "",
176216
}
177217
admission["admission_sha256"] = calculate_shadow_receipt_admission_sha256(admission)
@@ -269,11 +309,15 @@ def create_if_absent(self, admission: Mapping[str, Any]) -> bool:
269309
def persist_shadow_cycle_outcome(
270310
outcome: ShadowCycleOutcome,
271311
store: CreateOnlyShadowReceiptStore,
312+
*,
313+
risk_gate_decision: Any | None = None,
272314
) -> dict[str, Any]:
273315
"""Persist a ready P5 receipt once, or return a closed parked/reconciled result.
274316
275-
It never promotes P5, retries missing inputs, or writes a parked result. A
276-
conflicting immutable cycle returns ``PARKED`` and remains untouched.
317+
It never promotes P5, resets a breaker, retries missing inputs, or writes a
318+
parked result. A missing, prohibited, or mismatched deterministic risk
319+
decision returns ``PARKED`` before the store is read or called. A
320+
conflicting immutable cycle also returns ``PARKED`` and remains untouched.
277321
"""
278322
try:
279323
controller_result = validate_shadow_scheduler_result(outcome.result)
@@ -291,7 +335,44 @@ def persist_shadow_cycle_outcome(
291335
admission_sha256=None,
292336
)
293337

294-
admission = build_shadow_receipt_admission(outcome)
338+
if risk_gate_decision is None:
339+
return _persistence_result(
340+
cycle_id=controller_result["cycle_id"],
341+
computed_at=controller_result["computed_at"],
342+
status="PARKED",
343+
reason_code="risk_gate_decision_missing",
344+
shadow_receipt_sha256=None,
345+
admission_sha256=None,
346+
)
347+
try:
348+
admission = build_shadow_receipt_admission(outcome, risk_gate_decision=risk_gate_decision)
349+
except ShadowRiskGateDecisionProhibitedError:
350+
return _persistence_result(
351+
cycle_id=controller_result["cycle_id"],
352+
computed_at=controller_result["computed_at"],
353+
status="PARKED",
354+
reason_code="risk_gate_decision_prohibited",
355+
shadow_receipt_sha256=None,
356+
admission_sha256=None,
357+
)
358+
except ShadowRiskGateDecisionMismatchError:
359+
return _persistence_result(
360+
cycle_id=controller_result["cycle_id"],
361+
computed_at=controller_result["computed_at"],
362+
status="PARKED",
363+
reason_code="risk_gate_decision_mismatch",
364+
shadow_receipt_sha256=None,
365+
admission_sha256=None,
366+
)
367+
except ShadowRiskGateDecisionError:
368+
return _persistence_result(
369+
cycle_id=controller_result["cycle_id"],
370+
computed_at=controller_result["computed_at"],
371+
status="PARKED",
372+
reason_code="risk_gate_decision_invalid",
373+
shadow_receipt_sha256=None,
374+
admission_sha256=None,
375+
)
295376
cycle_id = admission["cycle_id"]
296377
existing = store.read(cycle_id)
297378
created = existing is None and store.create_if_absent(admission)

0 commit comments

Comments
 (0)