From 588dbf50784943466df57c103174ea22e997e345 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:42:15 +0800 Subject: [PATCH 1/2] feat: add Alpaca paper admission contract Co-Authored-By: Codex --- README.md | 11 ++ src/alpaca_platform/__init__.py | 18 +++ src/alpaca_platform/paper_admission.py | 179 +++++++++++++++++++++++++ tests/test_paper_admission.py | 112 ++++++++++++++++ 4 files changed, 320 insertions(+) create mode 100644 src/alpaca_platform/paper_admission.py create mode 100644 tests/test_paper_admission.py diff --git a/README.md b/README.md index 5fb10e2..cdfe78f 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,17 @@ P5 receipt 不是 P4/P6 权限。shadow input 会复核上游 policy-gate receip 此前的 v1 仅为未启用的纯账本原型,未签发任何真实 receipt;v2 因此不保留可绕过 policy-gate receipt 的兼容入口。 +## P4 paper admission(本地准入契约) + +`paper_admission` 只生成和校验 `qsl.alpaca_paper_admission.v1`,不连接 Alpaca、不读取凭据、 +不访问网络、不排程、不提交订单。契约必须显式使用 `PAPER_DRY_RUN`,并只包含 paper endpoint、 +配置、策略、部署和风险的 SHA-256 摘要,以及不可变 `cycle_id` 和 UTC 有效窗口;不会保存原始 +URL、账户、订单或任何凭据字段。 + +相同 `cycle_id` 与相同内容可通过 `reconcile_paper_admission` 返回 `RECONCILED`;同一 cycle +出现不同内容会 fail-closed 为冲突。`LIVE`、`SHADOW`、shadow receipt 提权和未知/敏感字段均被拒绝。 +该契约不代表 paper 账户已连接,也不授予 P4/P6 执行权限。 + UESP 只负责产生 `qsl.tqqq-forward-observation.v1`。本仓的适配器再把它与独立的 policy-gate receipt、风险摘要和 deployment bundle 摘要组成 v2 shadow input;因此研究层 不需要导入或理解券商、政策签名或运行环境。适配器本身也不会排程或写入账本: diff --git a/src/alpaca_platform/__init__.py b/src/alpaca_platform/__init__.py index 5609f5f..f55b591 100644 --- a/src/alpaca_platform/__init__.py +++ b/src/alpaca_platform/__init__.py @@ -1,5 +1,15 @@ """Bounded P4/P5 execution gateway primitives.""" +from .paper_admission import ( + PAPER_ADMISSION_SCHEMA, + PAPER_ENVIRONMENT, + PaperAdmissionConflictError, + PaperAdmissionError, + build_paper_admission, + calculate_paper_admission_sha256, + reconcile_paper_admission, + validate_paper_admission, +) from .gcs_p5_artifacts import ( GcsCreateOnlyShadowReceiptStore, GcsP5ArtifactError, @@ -66,6 +76,8 @@ "ADMISSION_SCHEMA", "DETERMINISTIC_RISK_GATE_DECISION_SCHEMA", "FORWARD_OBSERVATION_SCHEMA", + "PAPER_ADMISSION_SCHEMA", + "PAPER_ENVIRONMENT", "P5_DEFAULT_PARKED_SCHEDULER_STATUS_SCHEMA", "P5_DEFAULT_PARKED_SCHEDULER_SUMMARY_SCHEMA", "P5_RISK_GATE_DECISION_ENVELOPE_SCHEMA", @@ -82,6 +94,8 @@ "P5DefaultParkedSchedulerError", "P5DefaultParkedSchedulerOutcome", "P5ShadowArtifactSnapshot", + "PaperAdmissionConflictError", + "PaperAdmissionError", "RestrictedP5ArtifactReadError", "RestrictedP5ShadowArtifactReader", "ShadowCycleOutcome", @@ -92,21 +106,25 @@ "ShadowRiskGateDecisionProhibitedError", "ShadowSchedulerError", "build_p5_risk_gate_decision_reference", + "build_paper_admission", "build_shadow_ledger_receipt", "build_shadow_receipt_admission", "build_tqqq_shadow_cycle_input", "calculate_forward_observation_sha256", "calculate_p5_default_parked_scheduler_deduplication_sha256", "calculate_p5_risk_gate_decision_envelope_sha256", + "calculate_paper_admission_sha256", "calculate_policy_gate_receipt_sha256", "calculate_shadow_receipt_admission_sha256", "persist_shadow_cycle_outcome", "run_p5_default_parked_shadow_cycle", "run_tqqq_shadow_cycle", + "reconcile_paper_admission", "summarize_p5_default_parked_scheduler_status", "validate_p5_default_parked_scheduler_status", "validate_p5_default_parked_scheduler_summary", "validate_p5_risk_gate_decision_reference", + "validate_paper_admission", "validate_shadow_cycle_input", "validate_shadow_ledger_receipt", "validate_shadow_receipt_admission", diff --git a/src/alpaca_platform/paper_admission.py b/src/alpaca_platform/paper_admission.py new file mode 100644 index 0000000..ebf8eec --- /dev/null +++ b/src/alpaca_platform/paper_admission.py @@ -0,0 +1,179 @@ +"""Local, fail-closed admission contract for Alpaca paper dry-runs.""" + +from __future__ import annotations + +import hashlib +import json +import re +from datetime import datetime, timezone +from typing import Any, Mapping + +PAPER_ADMISSION_SCHEMA = "qsl.alpaca_paper_admission.v1" +PAPER_ENVIRONMENT = "PAPER_DRY_RUN" + +_DIGEST = re.compile(r"[0-9a-f]{64}") +_IDENTITY = re.compile(r"[a-z0-9][a-z0-9._:-]{0,127}") +_TIMESTAMP = re.compile(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z") +_FORBIDDEN_KEY = re.compile( + r"(?:account|order|credential|secret|token|password|authorization|shadow)", + re.IGNORECASE, +) +_FIELDS = { + "schema", + "environment", + "cycle_id", + "valid_from", + "valid_until", + "paper_endpoint_sha256", + "config_sha256", + "strategy_sha256", + "deployment_sha256", + "risk_sha256", + "admission_sha256", +} +_BODY_FIELDS = _FIELDS - {"admission_sha256"} + + +class PaperAdmissionError(ValueError): + """Raised when a paper admission is invalid or unsafe.""" + + +class PaperAdmissionConflictError(PaperAdmissionError): + """Raised when one cycle id is associated with different admission data.""" + + +def _fail(message: str) -> None: + raise PaperAdmissionError(message) + + +def _object(value: Any, label: str) -> Mapping[str, Any]: + if not isinstance(value, Mapping): + _fail(f"{label} must be an object") + return value + + +def _check_keys(value: Mapping[str, Any], required: set[str], allowed: set[str], label: str) -> None: + keys = set(value) + if any(not isinstance(key, str) for key in keys): + _fail(f"{label} contains a non-string field name") + missing = sorted(required - keys) + unknown = sorted(keys - allowed) + if missing: + _fail(f"{label} missing required field(s): {', '.join(missing)}") + if unknown: + if any(_FORBIDDEN_KEY.search(key) for key in unknown): + _fail(f"{label} contains a forbidden field") + _fail(f"{label} contains an unknown field") + + +def _exact_keys(value: Mapping[str, Any], expected: set[str], label: str) -> None: + _check_keys(value, expected, expected, label) + + +def _digest(value: Any, label: str) -> str: + if not isinstance(value, str) or not _DIGEST.fullmatch(value): + _fail(f"{label} must be a lowercase SHA-256 digest") + return value + + +def _timestamp(value: Any, label: str) -> datetime: + if not isinstance(value, str) or not _TIMESTAMP.fullmatch(value): + _fail(f"{label} must be an RFC3339 UTC timestamp with whole seconds") + try: + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + except ValueError as exc: + raise PaperAdmissionError(f"{label} must be a valid calendar timestamp") from exc + + +def _canonical_json(value: Mapping[str, Any]) -> str: + content = dict(value) + content.pop("admission_sha256", None) + try: + return json.dumps(content, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False) + except (TypeError, ValueError) as exc: + raise PaperAdmissionError("paper admission cannot be represented as canonical JSON") from exc + + +def calculate_paper_admission_sha256(value: Mapping[str, Any]) -> str: + """Calculate the digest over all admission fields except its self-digest.""" + admission = _object(value, "paper admission") + _check_keys(admission, _BODY_FIELDS, _BODY_FIELDS | {"admission_sha256"}, "paper admission") + return hashlib.sha256(_canonical_json(admission).encode("utf-8")).hexdigest() + + +def _validate_body(value: Mapping[str, Any]) -> None: + if value["schema"] != PAPER_ADMISSION_SCHEMA: + _fail("paper admission schema is unsupported") + if value["environment"] != PAPER_ENVIRONMENT: + _fail("paper admission environment is not PAPER_DRY_RUN") + if not isinstance(value["cycle_id"], str) or not _IDENTITY.fullmatch(value["cycle_id"]): + _fail("paper admission cycle_id must be an immutable lowercase identity") + + valid_from = _timestamp(value["valid_from"], "paper admission valid_from") + valid_until = _timestamp(value["valid_until"], "paper admission valid_until") + if valid_from >= valid_until: + _fail("paper admission validity window must be increasing") + + for field in ( + "paper_endpoint_sha256", + "config_sha256", + "strategy_sha256", + "deployment_sha256", + "risk_sha256", + "admission_sha256", + ): + _digest(value[field], f"paper admission {field}") + + +def validate_paper_admission(value: Any) -> dict[str, str]: + """Validate and return a copy of a complete local paper admission.""" + admission = _object(value, "paper admission") + _exact_keys(admission, _FIELDS, "paper admission") + _validate_body(admission) + if admission["admission_sha256"] != calculate_paper_admission_sha256(admission): + _fail("paper admission digest does not match its content") + return dict(admission) + + +def build_paper_admission( + *, + environment: str, + cycle_id: str, + valid_from: str, + valid_until: str, + paper_endpoint_sha256: str, + config_sha256: str, + strategy_sha256: str, + deployment_sha256: str, + risk_sha256: str, +) -> dict[str, str]: + """Build a deterministic, non-executable paper admission.""" + admission: dict[str, str] = { + "schema": PAPER_ADMISSION_SCHEMA, + "environment": environment, + "cycle_id": cycle_id, + "valid_from": valid_from, + "valid_until": valid_until, + "paper_endpoint_sha256": paper_endpoint_sha256, + "config_sha256": config_sha256, + "strategy_sha256": strategy_sha256, + "deployment_sha256": deployment_sha256, + "risk_sha256": risk_sha256, + "admission_sha256": "0" * 64, + } + _exact_keys(admission, _FIELDS, "paper admission") + _validate_body(admission) + admission["admission_sha256"] = calculate_paper_admission_sha256(admission) + return admission + + +def reconcile_paper_admission(existing: Any, candidate: Any) -> str: + """Reconcile two local admissions for one idempotency key.""" + current = validate_paper_admission(existing) + proposed = validate_paper_admission(candidate) + if ( + current["cycle_id"] != proposed["cycle_id"] + or current["admission_sha256"] != proposed["admission_sha256"] + ): + raise PaperAdmissionConflictError("paper admission cycle id already has different content") + return "RECONCILED" diff --git a/tests/test_paper_admission.py b/tests/test_paper_admission.py new file mode 100644 index 0000000..e520ee9 --- /dev/null +++ b/tests/test_paper_admission.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import copy + +import pytest + +from alpaca_platform import paper_admission + + +def sha(character: str) -> str: + return character * 64 + + +def valid_admission() -> dict[str, str]: + return paper_admission.build_paper_admission( + environment="PAPER_DRY_RUN", + cycle_id="alpaca_paper_20260904_001", + valid_from="2026-09-04T00:00:00Z", + valid_until="2026-09-05T00:00:00Z", + paper_endpoint_sha256=sha("1"), + config_sha256=sha("2"), + strategy_sha256=sha("3"), + deployment_sha256=sha("4"), + risk_sha256=sha("5"), + ) + + +def test_build_and_validate_paper_admission(): + admission = valid_admission() + + assert admission["schema"] == paper_admission.PAPER_ADMISSION_SCHEMA + assert admission["environment"] == "PAPER_DRY_RUN" + assert paper_admission.validate_paper_admission(admission) == admission + + +def test_digest_calculation_allows_omitting_self_digest(): + admission = valid_admission() + admission_without_digest = dict(admission) + del admission_without_digest["admission_sha256"] + + assert paper_admission.calculate_paper_admission_sha256(admission_without_digest) == admission["admission_sha256"] + + +def test_same_cycle_and_content_is_idempotently_reconciled(): + admission = valid_admission() + replay = copy.deepcopy(admission) + + assert paper_admission.reconcile_paper_admission(admission, replay) == "RECONCILED" + + +def test_same_cycle_with_changed_digest_is_a_conflict(): + admission = valid_admission() + conflicting = copy.deepcopy(admission) + conflicting["risk_sha256"] = sha("6") + conflicting["admission_sha256"] = paper_admission.calculate_paper_admission_sha256(conflicting) + + with pytest.raises(paper_admission.PaperAdmissionConflictError): + paper_admission.reconcile_paper_admission(admission, conflicting) + + +@pytest.mark.parametrize("environment", ["LIVE", "live", "SHADOW", "PAPER"]) +def test_non_paper_environment_is_rejected(environment: str): + admission = valid_admission() + admission["environment"] = environment + admission["admission_sha256"] = paper_admission.calculate_paper_admission_sha256(admission) + + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) + + +@pytest.mark.parametrize("forbidden_field", ["shadow_receipt", "account_id", "order_id", "credential"]) +def test_shadow_and_sensitive_material_cannot_be_added(forbidden_field: str): + admission = valid_admission() + admission[forbidden_field] = "forbidden" + + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) + + +def test_raw_endpoint_and_missing_digest_are_rejected(): + admission = valid_admission() + admission["endpoint"] = "https://paper.example.invalid" + + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) + + admission = valid_admission() + del admission["risk_sha256"] + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) + + +def test_window_and_cycle_id_are_fail_closed(): + admission = valid_admission() + admission["valid_until"] = admission["valid_from"] + admission["admission_sha256"] = paper_admission.calculate_paper_admission_sha256(admission) + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) + + admission = valid_admission() + admission["cycle_id"] = "contains whitespace" + admission["admission_sha256"] = paper_admission.calculate_paper_admission_sha256(admission) + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) + + +def test_tampered_admission_digest_is_rejected(): + admission = valid_admission() + admission["admission_sha256"] = sha("f") + + with pytest.raises(paper_admission.PaperAdmissionError): + paper_admission.validate_paper_admission(admission) From cf657a4c4609fa41a69f01b38e4d863212dadc91 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 4 Sep 2026 20:45:42 +0800 Subject: [PATCH 2/2] fix: satisfy Alpaca admission lint Co-Authored-By: Codex --- src/alpaca_platform/__init__.py | 26 +++++++++++++------------- src/alpaca_platform/paper_admission.py | 7 ++++--- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/alpaca_platform/__init__.py b/src/alpaca_platform/__init__.py index f55b591..07ee3d8 100644 --- a/src/alpaca_platform/__init__.py +++ b/src/alpaca_platform/__init__.py @@ -1,15 +1,5 @@ """Bounded P4/P5 execution gateway primitives.""" -from .paper_admission import ( - PAPER_ADMISSION_SCHEMA, - PAPER_ENVIRONMENT, - PaperAdmissionConflictError, - PaperAdmissionError, - build_paper_admission, - calculate_paper_admission_sha256, - reconcile_paper_admission, - validate_paper_admission, -) from .gcs_p5_artifacts import ( GcsCreateOnlyShadowReceiptStore, GcsP5ArtifactError, @@ -30,6 +20,16 @@ validate_p5_default_parked_scheduler_status, validate_p5_default_parked_scheduler_summary, ) +from .paper_admission import ( + PAPER_ADMISSION_SCHEMA, + PAPER_ENVIRONMENT, + PaperAdmissionConflictError, + PaperAdmissionError, + build_paper_admission, + calculate_paper_admission_sha256, + reconcile_paper_admission, + validate_paper_admission, +) from .shadow_ledger import ( FORWARD_OBSERVATION_SCHEMA, POLICY_GATE_RECEIPT_SCHEMA, @@ -76,12 +76,12 @@ "ADMISSION_SCHEMA", "DETERMINISTIC_RISK_GATE_DECISION_SCHEMA", "FORWARD_OBSERVATION_SCHEMA", - "PAPER_ADMISSION_SCHEMA", - "PAPER_ENVIRONMENT", "P5_DEFAULT_PARKED_SCHEDULER_STATUS_SCHEMA", "P5_DEFAULT_PARKED_SCHEDULER_SUMMARY_SCHEMA", "P5_RISK_GATE_DECISION_ENVELOPE_SCHEMA", "P5_RISK_GATE_DECISION_REFERENCE_SCHEMA", + "PAPER_ADMISSION_SCHEMA", + "PAPER_ENVIRONMENT", "PERSISTENCE_RESULT_SCHEMA", "POLICY_GATE_RECEIPT_SCHEMA", "SCHEDULER_RESULT_SCHEMA", @@ -117,9 +117,9 @@ "calculate_policy_gate_receipt_sha256", "calculate_shadow_receipt_admission_sha256", "persist_shadow_cycle_outcome", + "reconcile_paper_admission", "run_p5_default_parked_shadow_cycle", "run_tqqq_shadow_cycle", - "reconcile_paper_admission", "summarize_p5_default_parked_scheduler_status", "validate_p5_default_parked_scheduler_status", "validate_p5_default_parked_scheduler_summary", diff --git a/src/alpaca_platform/paper_admission.py b/src/alpaca_platform/paper_admission.py index ebf8eec..aeb1370 100644 --- a/src/alpaca_platform/paper_admission.py +++ b/src/alpaca_platform/paper_admission.py @@ -5,8 +5,9 @@ import hashlib import json import re -from datetime import datetime, timezone -from typing import Any, Mapping +from collections.abc import Mapping +from datetime import UTC, datetime +from typing import Any PAPER_ADMISSION_SCHEMA = "qsl.alpaca_paper_admission.v1" PAPER_ENVIRONMENT = "PAPER_DRY_RUN" @@ -80,7 +81,7 @@ def _timestamp(value: Any, label: str) -> datetime: if not isinstance(value, str) or not _TIMESTAMP.fullmatch(value): _fail(f"{label} must be an RFC3339 UTC timestamp with whole seconds") try: - return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc) + return datetime.strptime(value, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=UTC) except ValueError as exc: raise PaperAdmissionError(f"{label} must be a valid calendar timestamp") from exc