Skip to content

Commit f6adb0b

Browse files
Pigbibicodex
andcommitted
feat: track enabled and disabled runtime targets
Co-Authored-By: Codex <noreply@openai.com>
1 parent fe69d55 commit f6adb0b

6 files changed

Lines changed: 681 additions & 0 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Publish runtime target lifecycle
2+
description: Build and sync one bounded, no-order runtime-target lifecycle snapshot.
3+
4+
inputs:
5+
source-id:
6+
description: Stable, non-sensitive source identity for this exact target.
7+
required: true
8+
target-id:
9+
description: Stable, non-sensitive target identity.
10+
required: true
11+
platform:
12+
description: Exact platform identifier.
13+
required: true
14+
configured-state:
15+
description: enabled or disabled; this action never changes it.
16+
required: true
17+
execution-mode:
18+
description: Intended dry_run, paper, or live lane.
19+
required: true
20+
runtime-guard:
21+
description: Sanitized runtime-guard result.
22+
required: true
23+
execution-heartbeat:
24+
description: Sanitized execution-heartbeat result.
25+
required: true
26+
sync-url:
27+
description: HTTPS Strategy Switch Console base URL.
28+
required: true
29+
30+
runs:
31+
using: composite
32+
steps:
33+
- name: Build and sync bounded target lifecycle
34+
shell: bash
35+
env:
36+
INPUT_SOURCE_ID: ${{ inputs.source-id }}
37+
INPUT_TARGET_ID: ${{ inputs.target-id }}
38+
INPUT_PLATFORM: ${{ inputs.platform }}
39+
INPUT_CONFIGURED_STATE: ${{ inputs.configured-state }}
40+
INPUT_EXECUTION_MODE: ${{ inputs.execution-mode }}
41+
INPUT_RUNTIME_GUARD: ${{ inputs.runtime-guard }}
42+
INPUT_EXECUTION_HEARTBEAT: ${{ inputs.execution-heartbeat }}
43+
INPUT_SYNC_URL: ${{ inputs.sync-url }}
44+
run: |
45+
set -euo pipefail
46+
if [[ -z "${EXECUTION_EVIDENCE_SYNC_TOKEN:-}" ]]; then
47+
echo "EXECUTION_EVIDENCE_SYNC_TOKEN is required." >&2
48+
exit 2
49+
fi
50+
if [[ ! "$INPUT_SYNC_URL" =~ ^https://[^/?#]+$ ]]; then
51+
echo "sync-url must be an HTTPS base URL without a path or query." >&2
52+
exit 2
53+
fi
54+
work_dir="$(mktemp -d)"
55+
cleanup() { rm -rf "$work_dir"; }
56+
trap cleanup EXIT
57+
snapshot_path="$work_dir/runtime-target-lifecycle.json"
58+
python3 "$GITHUB_ACTION_PATH/../../python/scripts/runtime_target_lifecycle.py" \
59+
--source-id "$INPUT_SOURCE_ID" \
60+
--target-id "$INPUT_TARGET_ID" \
61+
--platform "$INPUT_PLATFORM" \
62+
--configured-state "$INPUT_CONFIGURED_STATE" \
63+
--execution-mode "$INPUT_EXECUTION_MODE" \
64+
--runtime-guard "$INPUT_RUNTIME_GUARD" \
65+
--execution-heartbeat "$INPUT_EXECUTION_HEARTBEAT" \
66+
--output "$snapshot_path"
67+
curl --fail --silent --show-error \
68+
--request POST "${INPUT_SYNC_URL}/api/internal/sync-runtime-target-lifecycle-source" \
69+
--header "Authorization: Bearer ${EXECUTION_EVIDENCE_SYNC_TOKEN}" \
70+
--header "Content-Type: application/json" \
71+
--data-binary "@$snapshot_path" \
72+
>/dev/null

docs/runtime_target_lifecycle.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Runtime target lifecycle snapshot
2+
3+
`qsl_runtime_target_lifecycle_source_snapshot.v1` records the operational
4+
state of one exact platform target. It is deliberately separate from
5+
`qsl_execution_evidence_source_snapshot.v1`:
6+
7+
- an **enabled** target continues its runtime guard and execution-heartbeat
8+
monitoring;
9+
- a deliberately **disabled** target remains visible and continues no-order
10+
validation, rather than being misreported as an unhealthy execution target;
11+
- either monitoring failure returns `parked`; it never changes a target's
12+
enabled flag, execution mode, credentials, strategy, or order permissions.
13+
14+
Every target record has `no_order: true`. The Worker stores only platform,
15+
target identity, intended lane, sanitized monitor states, disposition, and
16+
bounded reason codes. It accepts the same protected
17+
`EXECUTION_EVIDENCE_SYNC_TOKEN` as the existing platform execution-evidence
18+
publisher, but stores these snapshots under a separate KV prefix and exposes
19+
them from `GET /api/runtime-target-lifecycle` only to an allowed signed-in
20+
user.
21+
22+
Platform workflows call the reusable
23+
`actions/publish-runtime-target-lifecycle` action after their existing checks.
24+
The action only constructs and posts a sanitized status object; it has no
25+
broker SDK, account material, or command to enable a runtime target.
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
#!/usr/bin/env python3
2+
"""Build a bounded, no-order lifecycle snapshot for one runtime target.
3+
4+
This contract intentionally tracks configured target state separately from
5+
execution evidence. A target disabled by policy is not an unavailable broker,
6+
and it must not be represented as a paper/live execution result.
7+
"""
8+
9+
from __future__ import annotations
10+
11+
import argparse
12+
import json
13+
import re
14+
from datetime import UTC, datetime
15+
from pathlib import Path
16+
from typing import Any, Mapping
17+
18+
19+
SOURCE_SCHEMA_VERSION = "qsl_runtime_target_lifecycle_source_snapshot.v1"
20+
PLATFORMS = frozenset({"alpaca", "binance", "firstrade", "ibkr", "longbridge", "qmt", "schwab"})
21+
CONFIGURED_STATES = frozenset({"enabled", "disabled"})
22+
EXECUTION_MODES = frozenset({"dry_run", "paper", "live"})
23+
CHECK_STATUSES = frozenset({"pass", "attention", "not_due", "not_applicable", "unavailable"})
24+
DISPOSITIONS = frozenset({"continue_enabled_monitoring", "continue_disabled_validation", "parked"})
25+
REASON_CODES = frozenset(
26+
{
27+
"none",
28+
"target_intentionally_disabled",
29+
"runtime_guard_attention",
30+
"execution_heartbeat_attention",
31+
"monitoring_unavailable",
32+
}
33+
)
34+
_IDENTIFIER = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._=-]{0,127}$")
35+
_TIMESTAMP = re.compile(r"^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$")
36+
37+
38+
class RuntimeTargetLifecycleError(ValueError):
39+
"""Raised when a lifecycle snapshot would be ambiguous or unsafe."""
40+
41+
42+
def _identifier(value: object, field: str) -> str:
43+
text = str(value or "").strip()
44+
if not _IDENTIFIER.fullmatch(text):
45+
raise RuntimeTargetLifecycleError(f"{field} must be a stable non-sensitive identifier")
46+
return text
47+
48+
49+
def _choice(value: object, choices: frozenset[str], field: str) -> str:
50+
text = str(value or "").strip()
51+
if text not in choices:
52+
raise RuntimeTargetLifecycleError(f"{field} is unsupported")
53+
return text
54+
55+
56+
def _timestamp(value: object | None) -> str:
57+
if value is None:
58+
return datetime.now(UTC).replace(microsecond=0).isoformat().replace("+00:00", "Z")
59+
text = str(value).strip()
60+
if not _TIMESTAMP.fullmatch(text):
61+
raise RuntimeTargetLifecycleError("observed_at must be an RFC3339 UTC timestamp")
62+
try:
63+
datetime.strptime(text, "%Y-%m-%dT%H:%M:%SZ")
64+
except ValueError as exc:
65+
raise RuntimeTargetLifecycleError("observed_at must be a valid calendar timestamp") from exc
66+
return text
67+
68+
69+
def _target_disposition(
70+
*,
71+
configured_state: str,
72+
runtime_guard: str,
73+
execution_heartbeat: str,
74+
) -> tuple[str, str]:
75+
if runtime_guard == "attention":
76+
return "parked", "runtime_guard_attention"
77+
if execution_heartbeat == "attention":
78+
return "parked", "execution_heartbeat_attention"
79+
if runtime_guard == "unavailable" or execution_heartbeat == "unavailable":
80+
return "parked", "monitoring_unavailable"
81+
if configured_state == "disabled":
82+
if execution_heartbeat != "not_applicable":
83+
raise RuntimeTargetLifecycleError(
84+
"disabled targets require a not_applicable execution heartbeat"
85+
)
86+
return "continue_disabled_validation", "target_intentionally_disabled"
87+
if runtime_guard not in {"pass", "not_due"} or execution_heartbeat not in {"pass", "not_due"}:
88+
raise RuntimeTargetLifecycleError("enabled target monitoring state is incomplete")
89+
return "continue_enabled_monitoring", "none"
90+
91+
92+
def build_runtime_target_lifecycle_source_snapshot(
93+
*,
94+
source_id: object,
95+
target_id: object,
96+
platform: object,
97+
configured_state: object,
98+
execution_mode: object,
99+
runtime_guard: object,
100+
execution_heartbeat: object,
101+
observed_at: object | None = None,
102+
) -> dict[str, Any]:
103+
"""Create one sanitized target state record without execution authority."""
104+
normalized_source_id = _identifier(source_id, "source_id")
105+
normalized_target_id = _identifier(target_id, "target_id")
106+
normalized_platform = _choice(platform, PLATFORMS, "platform")
107+
normalized_state = _choice(configured_state, CONFIGURED_STATES, "configured_state")
108+
normalized_mode = _choice(execution_mode, EXECUTION_MODES, "execution_mode")
109+
normalized_guard = _choice(runtime_guard, CHECK_STATUSES, "runtime_guard")
110+
normalized_heartbeat = _choice(execution_heartbeat, CHECK_STATUSES, "execution_heartbeat")
111+
disposition, reason_code = _target_disposition(
112+
configured_state=normalized_state,
113+
runtime_guard=normalized_guard,
114+
execution_heartbeat=normalized_heartbeat,
115+
)
116+
timestamp = _timestamp(observed_at)
117+
return {
118+
"schema_version": SOURCE_SCHEMA_VERSION,
119+
"source_id": normalized_source_id,
120+
"generated_at": timestamp,
121+
"computed_at": timestamp,
122+
"data_status": "ready",
123+
"targets": [
124+
{
125+
"target_id": normalized_target_id,
126+
"target": {
127+
"platform": normalized_platform,
128+
"configured_state": normalized_state,
129+
"execution_mode": normalized_mode,
130+
},
131+
"monitoring": {
132+
"runtime_guard": normalized_guard,
133+
"execution_heartbeat": normalized_heartbeat,
134+
},
135+
"disposition": {"code": disposition, "reason_code": reason_code},
136+
"no_order": True,
137+
}
138+
],
139+
"errors": [],
140+
}
141+
142+
143+
def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
144+
parser = argparse.ArgumentParser(description=__doc__)
145+
parser.add_argument("--source-id", required=True)
146+
parser.add_argument("--target-id", required=True)
147+
parser.add_argument("--platform", required=True)
148+
parser.add_argument("--configured-state", required=True, choices=sorted(CONFIGURED_STATES))
149+
parser.add_argument("--execution-mode", required=True, choices=sorted(EXECUTION_MODES))
150+
parser.add_argument("--runtime-guard", required=True, choices=sorted(CHECK_STATUSES))
151+
parser.add_argument("--execution-heartbeat", required=True, choices=sorted(CHECK_STATUSES))
152+
parser.add_argument("--observed-at")
153+
parser.add_argument("--output", required=True)
154+
return parser.parse_args(argv)
155+
156+
157+
def main(argv: list[str] | None = None) -> int:
158+
args = _parse_args(argv)
159+
snapshot = build_runtime_target_lifecycle_source_snapshot(
160+
source_id=args.source_id,
161+
target_id=args.target_id,
162+
platform=args.platform,
163+
configured_state=args.configured_state,
164+
execution_mode=args.execution_mode,
165+
runtime_guard=args.runtime_guard,
166+
execution_heartbeat=args.execution_heartbeat,
167+
observed_at=args.observed_at,
168+
)
169+
output = Path(args.output)
170+
output.parent.mkdir(parents=True, exist_ok=True)
171+
output.write_text(json.dumps(snapshot, ensure_ascii=False, indent=2, sort_keys=True) + "\n", encoding="utf-8")
172+
return 0
173+
174+
175+
if __name__ == "__main__":
176+
raise SystemExit(main())
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
from __future__ import annotations
2+
3+
import importlib.util
4+
import sys
5+
import unittest
6+
from pathlib import Path
7+
8+
9+
ROOT = Path(__file__).parents[2]
10+
MODULE_PATH = ROOT / "python" / "scripts" / "runtime_target_lifecycle.py"
11+
SPEC = importlib.util.spec_from_file_location("runtime_target_lifecycle", MODULE_PATH)
12+
assert SPEC and SPEC.loader
13+
lifecycle = importlib.util.module_from_spec(SPEC)
14+
sys.modules[SPEC.name] = lifecycle
15+
SPEC.loader.exec_module(lifecycle)
16+
17+
18+
def _snapshot(**overrides: object) -> dict[str, object]:
19+
values: dict[str, object] = {
20+
"source_id": "longbridge.sg",
21+
"target_id": "longbridge.sg",
22+
"platform": "longbridge",
23+
"configured_state": "disabled",
24+
"execution_mode": "dry_run",
25+
"runtime_guard": "pass",
26+
"execution_heartbeat": "not_applicable",
27+
"observed_at": "2026-08-30T00:00:00Z",
28+
}
29+
values.update(overrides)
30+
return lifecycle.build_runtime_target_lifecycle_source_snapshot(**values)
31+
32+
33+
class RuntimeTargetLifecycleTest(unittest.TestCase):
34+
def test_disabled_target_remains_in_no_order_validation_lane(self) -> None:
35+
snapshot = _snapshot()
36+
37+
target = snapshot["targets"][0]
38+
self.assertEqual(
39+
snapshot["schema_version"],
40+
"qsl_runtime_target_lifecycle_source_snapshot.v1",
41+
)
42+
self.assertEqual(target["target"]["configured_state"], "disabled")
43+
self.assertEqual(
44+
target["disposition"],
45+
{
46+
"code": "continue_disabled_validation",
47+
"reason_code": "target_intentionally_disabled",
48+
},
49+
)
50+
self.assertIs(target["no_order"], True)
51+
52+
53+
def test_enabled_target_continues_monitoring_when_checks_pass(self) -> None:
54+
snapshot = _snapshot(
55+
configured_state="enabled",
56+
execution_mode="paper",
57+
execution_heartbeat="pass",
58+
)
59+
60+
self.assertEqual(
61+
snapshot["targets"][0]["disposition"],
62+
{
63+
"code": "continue_enabled_monitoring",
64+
"reason_code": "none",
65+
},
66+
)
67+
68+
69+
def test_monitoring_failures_park_without_changing_target_state(self) -> None:
70+
cases = [
71+
("attention", "pass", "runtime_guard_attention"),
72+
("pass", "attention", "execution_heartbeat_attention"),
73+
("unavailable", "pass", "monitoring_unavailable"),
74+
]
75+
for runtime_guard, execution_heartbeat, reason_code in cases:
76+
with self.subTest(
77+
runtime_guard=runtime_guard,
78+
execution_heartbeat=execution_heartbeat,
79+
):
80+
snapshot = _snapshot(
81+
configured_state="enabled",
82+
execution_mode="paper",
83+
runtime_guard=runtime_guard,
84+
execution_heartbeat=execution_heartbeat,
85+
)
86+
87+
self.assertEqual(
88+
snapshot["targets"][0]["disposition"],
89+
{
90+
"code": "parked",
91+
"reason_code": reason_code,
92+
},
93+
)
94+
self.assertIs(snapshot["targets"][0]["no_order"], True)
95+
96+
97+
def test_disabled_target_rejects_an_execution_heartbeat_claim(self) -> None:
98+
with self.assertRaisesRegex(lifecycle.RuntimeTargetLifecycleError, "not_applicable"):
99+
_snapshot(execution_heartbeat="pass")

0 commit comments

Comments
 (0)