Skip to content

Commit 89c19a0

Browse files
authored
feat: require bounded policy receipt for P5 shadow
Bind P5 shadow receipts to a bounded upstream policy-gate receipt; no execution activation.
1 parent 3c32fdb commit 89c19a0

4 files changed

Lines changed: 307 additions & 16 deletions

File tree

README.md

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ QuantStrategyLab 的 Alpaca 执行边界。它分为两个互不混用的阶段
55
- P5 `SHADOW`:只生成虚拟账本回执,不连接券商、不读取凭据、不提交订单。
66
- P4 `PAPER_DRY_RUN`:未来只会使用独立 paper endpoint 与独立 paper 凭据;不能从 P5 或市场数据配置推断资格。
77

8-
当前实现仅包含 P5 的 `qsl.tqqq_shadow_cycle_input.v1`
9-
`qsl.tqqq_shadow_ledger_receipt.v1`。每个输入固定候选、策略 revision、P1/P2/P3
10-
摘要、风险控制摘要与目标权重(basis points);输出记录相对上一回执的**虚拟**权重变化。
8+
当前实现仅包含 P5 的 `qsl.tqqq_shadow_cycle_input.v2`
9+
`qsl.tqqq_shadow_ledger_receipt.v2`。每个输入固定 deployment bundle、候选、策略 revision、P1/P2/P3
10+
摘要、风险控制摘要、目标权重(basis points)以及上游
11+
`qsl.gcp_kms_policy_gate_receipt.v1`;输出记录相对上一回执的**虚拟**权重变化。
1112
它不保存市场数据、价格、金额、账户、订单、broker URL 或任何凭据。
1213

13-
P5 receipt 不是 P4/P6 权限。实际调度器只能消费已验签且未过期的独立 P5 policy;P6
14-
仍由所有者明确决定。
14+
P5 receipt 不是 P4/P6 权限。shadow input 会复核上游 policy-gate receipt 的闭合字段、
15+
自校验摘要、`SHADOW` stage、有效窗口以及与风险控制的精确绑定;缺一即拒绝。该 receipt
16+
必须由隔离的 KMS 验签 gate 生成并交付给调度器,本模块不会验签、签发 policy 或把自校验
17+
摘要误作独立授权。P6 仍由所有者明确决定。
18+
19+
此前的 v1 仅为未启用的纯账本原型,未签发任何真实 receipt;v2 因此不保留可绕过
20+
policy-gate receipt 的兼容入口。
1521

1622
```bash
1723
python -m alpaca_platform.shadow_ledger --input cycle.json --output receipt.json

src/alpaca_platform/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
"""Bounded P4/P5 execution gateway primitives."""
22

33
from .shadow_ledger import (
4+
POLICY_GATE_RECEIPT_SCHEMA,
45
ShadowLedgerError,
56
build_shadow_ledger_receipt,
7+
calculate_policy_gate_receipt_sha256,
68
validate_shadow_cycle_input,
79
validate_shadow_ledger_receipt,
810
)
911

1012
__all__ = [
13+
"POLICY_GATE_RECEIPT_SCHEMA",
1114
"ShadowLedgerError",
1215
"build_shadow_ledger_receipt",
16+
"calculate_policy_gate_receipt_sha256",
1317
"validate_shadow_cycle_input",
1418
"validate_shadow_ledger_receipt",
1519
]

src/alpaca_platform/shadow_ledger.py

Lines changed: 208 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
"""Pure, no-broker P5 shadow-ledger receipt construction for frozen TQQQ input.
22
33
There is intentionally no Alpaca client in this module. The caller supplies a
4-
validated forward decision and writes the resulting receipt to its own
5-
create-only storage. The module neither reads credentials nor makes network
6-
requests nor produces broker order payloads.
4+
validated forward decision and a bounded receipt from the independently
5+
protected policy gate, then writes the resulting receipt to its own create-only
6+
storage. The module neither reads credentials nor makes network requests nor
7+
produces broker order payloads.
78
"""
89

910
from __future__ import annotations
@@ -19,8 +20,9 @@
1920
from pathlib import Path
2021
from typing import Any
2122

22-
INPUT_SCHEMA = "qsl.tqqq_shadow_cycle_input.v1"
23-
RECEIPT_SCHEMA = "qsl.tqqq_shadow_ledger_receipt.v1"
23+
INPUT_SCHEMA = "qsl.tqqq_shadow_cycle_input.v2"
24+
RECEIPT_SCHEMA = "qsl.tqqq_shadow_ledger_receipt.v2"
25+
POLICY_GATE_RECEIPT_SCHEMA = "qsl.gcp_kms_policy_gate_receipt.v1"
2426
CANDIDATE_ID = "tqqq_core_only_p2_v5"
2527
_DIGEST = re.compile(r"^[0-9a-f]{64}$")
2628
_REVISION = re.compile(r"^[0-9a-f]{40}$")
@@ -38,18 +40,46 @@
3840
"schema",
3941
"cycle_id",
4042
"produced_at",
43+
"deployment_bundle_sha256",
4144
"candidate",
4245
"source_evidence",
4346
"forward_decision",
4447
"risk_control",
48+
"policy_gate_receipt",
4549
"input_sha256",
4650
}
4751
_CANDIDATE_FIELDS = {"candidate_id", "config_sha256", "strategy_repository", "strategy_revision"}
4852
_EVIDENCE_FIELDS = {"p1_manifest_sha256", "p2_config_sha256", "p3_evidence_sha256", "producer_revision"}
4953
_DECISION_FIELDS = {"decision_id", "effective_session", "producer_revision", "allocation_bps", "decision_sha256"}
5054
_RISK_CONTROL_FIELDS = {"stage", "execution_lane", "risk_policy_id", "risk_policy_version", "risk_policy_sha256"}
55+
_POLICY_GATE_RECEIPT_FIELDS = {
56+
"schema",
57+
"verified_at",
58+
"deployment_bundle",
59+
"policy",
60+
"activation",
61+
"target",
62+
"risk_control",
63+
"trusted_policy_root",
64+
"signature_sha256",
65+
"receipt_sha256",
66+
}
67+
_POLICY_GATE_BUNDLE_FIELDS = {"schema", "bundle_id", "bundle_sha256"}
68+
_POLICY_GATE_POLICY_FIELDS = {
69+
"policy_id",
70+
"policy_version",
71+
"policy_sha256",
72+
"stage",
73+
"effective_at",
74+
"expires_at",
75+
}
76+
_POLICY_GATE_ACTIVATION_FIELDS = {"activation_id", "activation_sha256", "effective_at", "expires_at"}
77+
_POLICY_GATE_TARGET_FIELDS = {"platform", "repository", "revision", "environment", "target_sha256"}
78+
_POLICY_GATE_RISK_FIELDS = {"risk_policy_id", "risk_policy_version", "risk_policy_sha256"}
79+
_POLICY_GATE_ROOT_FIELDS = {"root_id", "trusted_policy_root_sha256", "expires_at"}
5180
_RECEIPT_FIELDS = {
52-
"schema", "cycle_id", "produced_at", "candidate", "source_evidence", "forward_decision", "risk_control",
81+
"schema", "cycle_id", "produced_at", "deployment_bundle_sha256", "candidate", "source_evidence", "forward_decision", "risk_control",
82+
"policy_gate_receipt",
5383
"ledger_parent_sha256", "shadow_adjustments_bps", "receipt_sha256",
5484
}
5585

@@ -153,6 +183,13 @@ def calculate_receipt_sha256(value: Mapping[str, Any]) -> str:
153183
return hashlib.sha256(_canonical_json(value, "receipt_sha256", "shadow receipt").encode("utf-8")).hexdigest()
154184

155185

186+
def calculate_policy_gate_receipt_sha256(value: Mapping[str, Any]) -> str:
187+
"""Return the digest of the non-secret receipt emitted by the P0 KMS gate."""
188+
return hashlib.sha256(
189+
_canonical_json(value, "receipt_sha256", "policy-gate receipt").encode("utf-8")
190+
).hexdigest()
191+
192+
156193
def _allocation(value: Any, path: str) -> dict[str, int]:
157194
allocation = _object(value, path)
158195
_exact_keys(allocation, set(_ALLOCATION_SYMBOLS), path)
@@ -242,25 +279,174 @@ def _risk_control(value: Any) -> dict[str, str]:
242279
}
243280

244281

282+
def _repository(value: Any, path: str) -> str:
283+
if not isinstance(value, str) or not re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9_.-]*/[A-Za-z0-9][A-Za-z0-9_.-]*", value):
284+
_fail(f"{path} must be an owner/repository identity")
285+
return value
286+
287+
288+
def _policy_gate_receipt(
289+
value: Any,
290+
*,
291+
observed_at: datetime,
292+
expected_bundle_sha256: str,
293+
risk_control: Mapping[str, str],
294+
) -> dict[str, Any]:
295+
"""Validate a bounded upstream policy-gate receipt for the P5 shadow lane.
296+
297+
The receipt must arrive from the independently protected KMS gate. This
298+
consumer checks its closed structure, digest, control window, and exact
299+
risk-policy binding; it neither verifies a KMS signature nor issues a
300+
policy itself.
301+
"""
302+
_reject_unsafe_material(value, "policy_gate_receipt")
303+
receipt = _object(value, "policy_gate_receipt")
304+
_exact_keys(receipt, _POLICY_GATE_RECEIPT_FIELDS, "policy_gate_receipt")
305+
if receipt["schema"] != POLICY_GATE_RECEIPT_SCHEMA:
306+
_fail(f"policy_gate_receipt.schema must be {POLICY_GATE_RECEIPT_SCHEMA}")
307+
verified_at_text = receipt["verified_at"]
308+
verified_at = _timestamp(verified_at_text, "policy_gate_receipt.verified_at")
309+
310+
bundle = _object(receipt["deployment_bundle"], "policy_gate_receipt.deployment_bundle")
311+
_exact_keys(bundle, _POLICY_GATE_BUNDLE_FIELDS, "policy_gate_receipt.deployment_bundle")
312+
if bundle["schema"] != "qsl.deployment_bundle.v1":
313+
_fail("policy_gate_receipt.deployment_bundle.schema must be qsl.deployment_bundle.v1")
314+
normalized_bundle = {
315+
"schema": "qsl.deployment_bundle.v1",
316+
"bundle_id": _identity(bundle["bundle_id"], "policy_gate_receipt.deployment_bundle.bundle_id"),
317+
"bundle_sha256": _digest(bundle["bundle_sha256"], "policy_gate_receipt.deployment_bundle.bundle_sha256"),
318+
}
319+
if normalized_bundle["bundle_sha256"] != expected_bundle_sha256:
320+
_fail("policy_gate_receipt deployment bundle does not match shadow input")
321+
322+
policy = _object(receipt["policy"], "policy_gate_receipt.policy")
323+
_exact_keys(policy, _POLICY_GATE_POLICY_FIELDS, "policy_gate_receipt.policy")
324+
if policy["stage"] != "SHADOW":
325+
_fail("policy_gate_receipt.policy.stage must be SHADOW")
326+
policy_effective_text = policy["effective_at"]
327+
policy_expires_text = policy["expires_at"]
328+
policy_effective = _timestamp(policy_effective_text, "policy_gate_receipt.policy.effective_at")
329+
policy_expires = _timestamp(policy_expires_text, "policy_gate_receipt.policy.expires_at")
330+
if policy_expires <= policy_effective:
331+
_fail("policy_gate_receipt policy window is invalid")
332+
normalized_policy = {
333+
"policy_id": _identity(policy["policy_id"], "policy_gate_receipt.policy.policy_id"),
334+
"policy_version": _identity(policy["policy_version"], "policy_gate_receipt.policy.policy_version"),
335+
"policy_sha256": _digest(policy["policy_sha256"], "policy_gate_receipt.policy.policy_sha256"),
336+
"stage": "SHADOW",
337+
"effective_at": policy_effective_text,
338+
"expires_at": policy_expires_text,
339+
}
340+
341+
activation = _object(receipt["activation"], "policy_gate_receipt.activation")
342+
_exact_keys(activation, _POLICY_GATE_ACTIVATION_FIELDS, "policy_gate_receipt.activation")
343+
activation_effective_text = activation["effective_at"]
344+
activation_expires_text = activation["expires_at"]
345+
activation_effective = _timestamp(activation_effective_text, "policy_gate_receipt.activation.effective_at")
346+
activation_expires = _timestamp(activation_expires_text, "policy_gate_receipt.activation.expires_at")
347+
if activation_expires <= activation_effective:
348+
_fail("policy_gate_receipt activation window is invalid")
349+
if activation_effective < policy_effective or activation_expires > policy_expires:
350+
_fail("policy_gate_receipt activation window is not contained in policy window")
351+
normalized_activation = {
352+
"activation_id": _identity(activation["activation_id"], "policy_gate_receipt.activation.activation_id"),
353+
"activation_sha256": _digest(activation["activation_sha256"], "policy_gate_receipt.activation.activation_sha256"),
354+
"effective_at": activation_effective_text,
355+
"expires_at": activation_expires_text,
356+
}
357+
358+
target = _object(receipt["target"], "policy_gate_receipt.target")
359+
_exact_keys(target, _POLICY_GATE_TARGET_FIELDS, "policy_gate_receipt.target")
360+
normalized_target = {
361+
"platform": _identity(target["platform"], "policy_gate_receipt.target.platform"),
362+
"repository": _repository(target["repository"], "policy_gate_receipt.target.repository"),
363+
"revision": _revision(target["revision"], "policy_gate_receipt.target.revision"),
364+
"environment": _identity(target["environment"], "policy_gate_receipt.target.environment"),
365+
"target_sha256": _digest(target["target_sha256"], "policy_gate_receipt.target.target_sha256"),
366+
}
367+
if normalized_target["platform"] != "alpaca" or normalized_target["repository"] != "QuantStrategyLab/AlpacaPlatform":
368+
_fail("policy_gate_receipt target must bind this Alpaca P5 gateway")
369+
if normalized_target["environment"] != "alpaca-shadow":
370+
_fail("policy_gate_receipt target.environment must be alpaca-shadow")
371+
372+
receipt_risk = _object(receipt["risk_control"], "policy_gate_receipt.risk_control")
373+
_exact_keys(receipt_risk, _POLICY_GATE_RISK_FIELDS, "policy_gate_receipt.risk_control")
374+
normalized_risk = {
375+
"risk_policy_id": _identity(receipt_risk["risk_policy_id"], "policy_gate_receipt.risk_control.risk_policy_id"),
376+
"risk_policy_version": _identity(
377+
receipt_risk["risk_policy_version"], "policy_gate_receipt.risk_control.risk_policy_version"
378+
),
379+
"risk_policy_sha256": _digest(
380+
receipt_risk["risk_policy_sha256"], "policy_gate_receipt.risk_control.risk_policy_sha256"
381+
),
382+
}
383+
if normalized_risk != {
384+
"risk_policy_id": risk_control["risk_policy_id"],
385+
"risk_policy_version": risk_control["risk_policy_version"],
386+
"risk_policy_sha256": risk_control["risk_policy_sha256"],
387+
}:
388+
_fail("policy_gate_receipt risk control does not match shadow input")
389+
390+
trusted_root = _object(receipt["trusted_policy_root"], "policy_gate_receipt.trusted_policy_root")
391+
_exact_keys(trusted_root, _POLICY_GATE_ROOT_FIELDS, "policy_gate_receipt.trusted_policy_root")
392+
root_expires_text = trusted_root["expires_at"]
393+
root_expires = _timestamp(root_expires_text, "policy_gate_receipt.trusted_policy_root.expires_at")
394+
normalized_root = {
395+
"root_id": _identity(trusted_root["root_id"], "policy_gate_receipt.trusted_policy_root.root_id"),
396+
"trusted_policy_root_sha256": _digest(
397+
trusted_root["trusted_policy_root_sha256"], "policy_gate_receipt.trusted_policy_root.trusted_policy_root_sha256"
398+
),
399+
"expires_at": root_expires_text,
400+
}
401+
normalized: dict[str, Any] = {
402+
"schema": POLICY_GATE_RECEIPT_SCHEMA,
403+
"verified_at": verified_at_text,
404+
"deployment_bundle": normalized_bundle,
405+
"policy": normalized_policy,
406+
"activation": normalized_activation,
407+
"target": normalized_target,
408+
"risk_control": normalized_risk,
409+
"trusted_policy_root": normalized_root,
410+
"signature_sha256": _digest(receipt["signature_sha256"], "policy_gate_receipt.signature_sha256"),
411+
"receipt_sha256": _digest(receipt["receipt_sha256"], "policy_gate_receipt.receipt_sha256"),
412+
}
413+
if normalized["receipt_sha256"] != calculate_policy_gate_receipt_sha256(normalized):
414+
_fail("policy_gate_receipt.receipt_sha256 mismatch")
415+
if observed_at < verified_at or observed_at < activation_effective:
416+
_fail("policy_gate_receipt is not yet effective for this shadow cycle")
417+
if observed_at >= min(policy_expires, activation_expires, root_expires):
418+
_fail("policy_gate_receipt is expired for this shadow cycle")
419+
return normalized
420+
421+
245422
def validate_shadow_cycle_input(value: Any) -> dict[str, Any]:
246-
"""Validate one bounded P5 input; this does not verify or issue its policy."""
423+
"""Validate one bounded P5 input; this does not verify or issue a KMS policy."""
247424
_reject_unsafe_material(value, "shadow_input")
248425
root = _object(value, "shadow_input")
249426
_exact_keys(root, _INPUT_FIELDS, "shadow_input")
250427
if root["schema"] != INPUT_SCHEMA:
251428
_fail(f"shadow_input.schema must be {INPUT_SCHEMA}")
252429
candidate = _candidate(root["candidate"])
430+
produced_at = _timestamp(root["produced_at"], "shadow_input.produced_at")
431+
deployment_bundle_sha256 = _digest(root["deployment_bundle_sha256"], "shadow_input.deployment_bundle_sha256")
432+
risk_control = _risk_control(root["risk_control"])
253433
normalized = {
254434
"schema": INPUT_SCHEMA,
255435
"cycle_id": _identity(root["cycle_id"], "shadow_input.cycle_id"),
256436
"produced_at": root["produced_at"],
437+
"deployment_bundle_sha256": deployment_bundle_sha256,
257438
"candidate": candidate,
258439
"source_evidence": _source_evidence(root["source_evidence"], candidate),
259440
"forward_decision": _forward_decision(root["forward_decision"], candidate),
260-
"risk_control": _risk_control(root["risk_control"]),
441+
"risk_control": risk_control,
442+
"policy_gate_receipt": _policy_gate_receipt(
443+
root["policy_gate_receipt"],
444+
observed_at=produced_at,
445+
expected_bundle_sha256=deployment_bundle_sha256,
446+
risk_control=risk_control,
447+
),
261448
"input_sha256": _digest(root["input_sha256"], "shadow_input.input_sha256"),
262449
}
263-
_timestamp(normalized["produced_at"], "shadow_input.produced_at")
264450
if normalized["input_sha256"] != calculate_input_sha256(normalized):
265451
_fail("shadow_input.input_sha256 mismatch")
266452
return normalized
@@ -274,21 +460,30 @@ def validate_shadow_ledger_receipt(value: Any) -> dict[str, Any]:
274460
if root["schema"] != RECEIPT_SCHEMA:
275461
_fail(f"shadow_receipt.schema must be {RECEIPT_SCHEMA}")
276462
candidate = _candidate(root["candidate"])
463+
produced_at = _timestamp(root["produced_at"], "shadow_receipt.produced_at")
464+
deployment_bundle_sha256 = _digest(root["deployment_bundle_sha256"], "shadow_receipt.deployment_bundle_sha256")
465+
risk_control = _risk_control(root["risk_control"])
277466
normalized = {
278467
"schema": RECEIPT_SCHEMA,
279468
"cycle_id": _identity(root["cycle_id"], "shadow_receipt.cycle_id"),
280469
"produced_at": root["produced_at"],
470+
"deployment_bundle_sha256": deployment_bundle_sha256,
281471
"candidate": candidate,
282472
"source_evidence": _source_evidence(root["source_evidence"], candidate),
283473
"forward_decision": _forward_decision(root["forward_decision"], candidate),
284-
"risk_control": _risk_control(root["risk_control"]),
474+
"risk_control": risk_control,
475+
"policy_gate_receipt": _policy_gate_receipt(
476+
root["policy_gate_receipt"],
477+
observed_at=produced_at,
478+
expected_bundle_sha256=deployment_bundle_sha256,
479+
risk_control=risk_control,
480+
),
285481
"ledger_parent_sha256": _digest(root["ledger_parent_sha256"], "shadow_receipt.ledger_parent_sha256"),
286482
"shadow_adjustments_bps": _adjustment_bps(
287483
root["shadow_adjustments_bps"], "shadow_receipt.shadow_adjustments_bps"
288484
),
289485
"receipt_sha256": _digest(root["receipt_sha256"], "shadow_receipt.receipt_sha256"),
290486
}
291-
_timestamp(normalized["produced_at"], "shadow_receipt.produced_at")
292487
if normalized["receipt_sha256"] != calculate_receipt_sha256(normalized):
293488
_fail("shadow_receipt.receipt_sha256 mismatch")
294489
return normalized
@@ -320,10 +515,12 @@ def build_shadow_ledger_receipt(cycle_input: Any, *, prior_receipt: Any | None =
320515
"schema": RECEIPT_SCHEMA,
321516
"cycle_id": cycle["cycle_id"],
322517
"produced_at": cycle["produced_at"],
518+
"deployment_bundle_sha256": cycle["deployment_bundle_sha256"],
323519
"candidate": cycle["candidate"],
324520
"source_evidence": cycle["source_evidence"],
325521
"forward_decision": cycle["forward_decision"],
326522
"risk_control": cycle["risk_control"],
523+
"policy_gate_receipt": cycle["policy_gate_receipt"],
327524
"ledger_parent_sha256": parent_sha256,
328525
"shadow_adjustments_bps": _adjustments(cycle["forward_decision"]["allocation_bps"], previous_allocation),
329526
"receipt_sha256": "",

0 commit comments

Comments
 (0)