Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/authority-app-auth-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@ permissions: {}

jobs:
authenticate:
strategy:
max-parallel: 1
matrix:
environment-permission: [read, write]
runs-on: ubuntu-24.04
timeout-minutes: 5
environment: binance-runtime
steps:
- name: Mint scoped read-only installation token
- name: Mint scoped maintenance installation token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
Expand All @@ -21,7 +25,7 @@ jobs:
owner: ${{ github.repository_owner }}
repositories: ${{ github.event.repository.name }}
permission-actions: read
permission-environments: read
permission-environments: ${{ matrix.environment-permission }}
permission-metadata: read
env:
# v3.2.0 parses this undeclared input into actions_variables.
Expand Down
65 changes: 0 additions & 65 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -622,16 +622,6 @@ jobs:
if: ${{ env.RUNTIME_TARGET_ENABLED == 'false' && github.event.inputs.full_cycle == 'true' }}
run: echo "Runtime target is disabled; full-cycle validation used read-only broker calls and suppressed all writes."

- name: 5. Stage execution report for isolated log publisher
continue-on-error: true
if: ${{ github.event.inputs.validate_only != 'true' && github.event.inputs.reconcile_only != 'true' && env.RUNTIME_TARGET_ENABLED == 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: binance-execution-report-${{ github.run_id }}
path: reports/execution_report.json
if-no-files-found: warn
retention-days: 1

- name: 5a. Stage redacted lifecycle recorder output for monitor
continue-on-error: true
if: ${{ always() && github.event.inputs.validate_only != 'true' && github.event.inputs.reconcile_only != 'true' && env.RUNTIME_TARGET_ENABLED == 'true' }}
Expand Down Expand Up @@ -714,58 +704,3 @@ jobs:
TG_TOKEN: ${{ secrets.TG_TOKEN }}
GLOBAL_TELEGRAM_CHAT_ID: ${{ vars.GLOBAL_TELEGRAM_CHAT_ID }}

publish-execution-log:
name: Publish execution log without broker credentials
needs: deploy
if: ${{ needs.deploy.result == 'success' && needs.deploy.outputs.runtime_target_enabled == 'true' && github.event.inputs.validate_only != 'true' && github.event.inputs.reconcile_only != 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: read
contents: write
steps:
- name: Download staged execution report
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: binance-execution-report-${{ github.run_id }}
path: reports

- name: Push execution log to logs branch
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
LOGS_BRANCH="logs"
TS="$(date -u +%Y-%m-%dT%H%M)"
MONTH="${TS:0:7}"
REPORT_FILE="${GITHUB_WORKSPACE}/reports/execution_report.json"

if [ ! -f "$REPORT_FILE" ]; then
echo "No execution report found, skipping log push."
exit 0
fi

TMP_DIR=$(mktemp -d)
trap 'rm -rf "$TMP_DIR"' EXIT
git clone --no-checkout "https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" "${TMP_DIR}/logs-repo"
cd "${TMP_DIR}/logs-repo"

if git ls-remote --exit-code --heads origin "${LOGS_BRANCH}" >/dev/null 2>&1; then
git fetch origin "${LOGS_BRANCH}:${LOGS_BRANCH}"
git checkout "${LOGS_BRANCH}"
else
git checkout --orphan "${LOGS_BRANCH}"
git rm -rf . >/dev/null 2>&1 || true
find . -mindepth 1 -maxdepth 1 ! -name '.git' -exec rm -rf {} +
fi

mkdir -p "hourly/${MONTH}"
cp "$REPORT_FILE" "hourly/${MONTH}/${TS}.json"

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add "hourly/${MONTH}/${TS}.json"
git commit -m "execution: ${TS}"
git push origin "HEAD:${LOGS_BRANCH}"
66 changes: 40 additions & 26 deletions application/cycle_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@

from quant_platform_kit.common.runtime_reports import persist_runtime_report
from quant_platform_kit.strategy_lifecycle.performance_monitor import (
PerformanceMonitor,
infer_strategy_domain,
logger as _performance_monitor_logger,
resolve_lifecycle_stream_id,
try_record_platform_execution,
)
from application.execution_receipt_adapter import attach_execution_receipt_from_report
from application.execution_kernel_adapter import consult_t2_unknown_new_cycle
Expand All @@ -26,6 +28,25 @@
)


def try_record_platform_execution(
profile_id, execution_result, *, domain="", decision=None, stream_id="",
):
"""Preserve the pinned recorder's best-effort behavior without raw errors."""
try:
if not execution_result:
return
monitor = PerformanceMonitor()
monitor.record_execution(
profile_id,
execution_result,
domain=infer_strategy_domain(profile_id, explicit_domain=domain),
decision=decision,
stream_id=stream_id,
)
except Exception: # noqa: BLE001 - preserve the recorder's best-effort boundary
_performance_monitor_logger.warning("platform_execution_record_failed")


def _record_risk_diagnostics(report, allocation):
assessment = allocation.get("risk_assessment")
if not isinstance(assessment, Mapping):
Expand Down Expand Up @@ -69,8 +90,12 @@ def _build_platform_execution_result(report, *, state_healthy, state_owner_relea
}


def _public_cycle_status(status):
return status if isinstance(status, str) and status in {"ok", "aborted", "error"} else "unknown"


def _write_lifecycle_export(profile_id, execution_result):
"""Best-effort, redacted export of the current recorder payload.
"""Best-effort public health metadata, without financial recorder fields.

The normal PerformanceStore path remains authoritative. This optional
file is a short-lived transport handoff for the monitor and must never
Expand All @@ -91,18 +116,13 @@ def _write_lifecycle_export(profile_id, execution_result):
profile = str(profile_id or "").strip()
if not profile:
return
stream_id = resolve_lifecycle_stream_id(execution_result=execution_result)
stream_id = resolve_lifecycle_stream_id(execution_result={"platform": "binance"})
safe_result = {
key: execution_result.get(key)
for key in (
"platform",
"status",
"total_equity_usdt",
"trend_equity_usdt",
"external_cash_flow",
"external_cash_flow_interval",
"degraded_mode_level",
)
"platform": "binance",
"status": _public_cycle_status(execution_result.get("status")),
# Legacy consumers treat an absent flow as zero. Explicit null
# preserves unknown performance evidence without exposing values.
"external_cash_flow": None,
}
if safe_result.get("status") != "ok":
safe_result["error_code"] = "cycle_failed"
Expand Down Expand Up @@ -654,10 +674,10 @@ def run_live_cycle(
printer=output_printer,
)
report = execute_cycle(runtime)
output_printer("\n".join(report.get("log_lines", [])))
# Free-form portfolio lines belong only in the existing private report.
output_printer("Strategy report details retained in the execution report.")
report_path = report_writer(report)
persisted_local_path = report_path
persisted_cloud_uri = None
try:
persisted = persist_runtime_report(
report,
Expand All @@ -666,16 +686,12 @@ def run_live_cycle(
project_id=os.getenv("CLOUD_PROJECT_ID") or os.getenv("GCP_PROJECT_ID") or os.getenv("GOOGLE_CLOUD_PROJECT"),
)
persisted_local_path = persisted.local_path or report_path
if hasattr(persisted, "cloud_uri"):
persisted_cloud_uri = persisted.cloud_uri
else:
persisted_cloud_uri = getattr(persisted, "gcs_uri", None)
except Exception:
report["status"] = "error"
append_report_error(report, "report_persistence_failed", stage="report_persistence")
output_printer("failed to persist archived execution report: report_persistence_failed")
persisted_local_path = report_writer(report)
report_status = str(report.get("status", "unknown"))
report_status = _public_cycle_status(report.get("status"))
status_event = {
"ok": "strategy_cycle_completed",
"aborted": "strategy_cycle_aborted",
Expand All @@ -687,12 +703,10 @@ def run_live_cycle(
severity="INFO" if report_status in {"ok", "aborted"} else "ERROR",
printer=output_printer,
status=report_status,
report_path=persisted_local_path,
report_cloud_uri=persisted_cloud_uri,
total_equity_usdt=report.get("total_equity_usdt"),
trend_equity_usdt=report.get("trend_equity_usdt"),
degraded_mode_level=report.get("degraded_mode_level"),
circuit_breaker_triggered=report.get("circuit_breaker_triggered"),
circuit_breaker_triggered=(
report.get("circuit_breaker_triggered")
if isinstance(report.get("circuit_breaker_triggered"), bool) else None
),
error_count=len(report.get("error_summary", {}).get("errors", [])),
)

Expand Down
10 changes: 9 additions & 1 deletion docs/authority_reissue.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,22 @@ After that adoption, dispatch `authority-app-auth-check.yml` on
`runtime-production` to verify installation-token creation, repository scope,
and configuration reads. This separate manual check does not read the authority
JSON, write configuration, or dispatch trading; it requires no authority-update
inputs and can run without stopping a trading runtime.
inputs and can run without stopping a trading runtime. It serially mints the
read and write Environment permission variants used by plan and apply, while
performing only read requests with both tokens.

Before adopting a maintenance change on `runtime-production`, compare its
current revision with `BINANCE_RUNTIME_WORKFLOW_SHA`. Updating the branch also
changes the runtime workflow revision. Do not advance the approved runtime pin
or include unrelated runtime changes merely to validate authentication; those
changes require their own approved release decision.

An authentication-only adoption must leave `.github/workflows/main.yml`,
runtime source, and the approved runtime pins unchanged. If the production
branch already differs from the approved workflow pin, that mismatch remains
a separate runtime release issue; token verification does not resolve it or
authorize execution.

## Explicit inputs (no main-tip selection)

All of the following must be supplied as full lowercase hex values:
Expand Down
73 changes: 73 additions & 0 deletions docs/operator_runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,41 @@ The monthly execution pool is locked to the accepted upstream `version` / `as_of
- Repository variable changes are consumed by the next externally scheduled dispatch; they do not reconfigure the VPS scheduler cadence.
- Avoid overlapping dispatches from multiple schedulers or from a second manual run while the current runtime job is still in progress.

### Optional native account facts: separate manual capability

The automatic `Runtime` workflow does not attach the optional native
account-facts collector. Its original workflow-wide concurrency lock, strategy
job, and isolated execution-log publisher remain unchanged. Waiting for optional
account-data approval must not become part of an automatic trading run.

`Binance Account Facts` remains a separate protected manual workflow. Its existing
manual inputs are `enabled` (default `false`), an exact `source_run_id`, and
`source_mode=legacy_terminal`. The repository gate, `runtime-production` ref,
`binance-runtime` environment approval, trusted reader revision, source checks,
and isolated publication token still apply. Retaining this entry does not grant
permission to invoke it or relax its checks.

Use it only during an explicitly established quiet maintenance window: Runtime
workflow admission remains disabled, no active or runnable Runtime attempt exists,
and no other operator or controller can re-enable, rerun, or dispatch trading.
Its separate concurrency group and point-in-time source checks do not provide an
atomic read-to-publication fence against a later Runtime. Actions-level inactivity
does not establish host-process or broker-order inactivity.

The reader also requires the latest qualifying Runtime source to be terminal
success with its exact approved revisions and report. At the 2026-10-06
maintenance checkpoint, the latest queued run and old waiting parent were
cancelled, so no eligible latest successful source was established. Do not create
one by enabling or dispatching a strategy merely to fill the account-data gap;
do not make the reader accept cancellation or stale sources.

Automatic native account facts are not yet a completed data path. Preserve
historical observation timestamps and unavailable/stale states rather than
presenting old data as fresh or missing data as zero. Reliable asynchronous
collection and receiver-side run/session fencing require separate review.
Source publication, workflow-pin adoption, and resuming trading are separate
decisions; this detachment itself authorizes none of them.

### Approved runtime release pin

`BINANCE_RUNTIME_RELEASE_SHA` is the single protected repository variable that
Expand Down Expand Up @@ -819,3 +854,41 @@ returns `blocked` and exit 2. All outcomes keep full account reconciliation and
execution authority false, including when out-of-scope Spot assets are present.
Stop on the first real failure; do not initialize a cursor or activate trading
to make this diagnostic pass. A successful preview is not durable accounting.


## Public diagnostic output privacy

The Runtime workflow must not upload the complete `execution_report.json` to
GitHub Actions or copy it into the public `logs` branch. Its former full-report
upload and isolated repository log-publisher job are removed. Existing local and
configured private runtime-report persistence remain unchanged; this change
creates no storage destination, identity or permission.

Public lifecycle export and console completion records contain bounded health
metadata only. They omit account valuation, cash-flow amounts, account-scope digests,
free-form portfolio lines and nested financial objects. The private report and
PerformanceStore recorder keep their existing content. Missing financial values
in public diagnostics mean unavailable, never zero or a substitute account NAV.
The public lifecycle payload explicitly keeps `external_cash_flow: null`, since
legacy consumers otherwise interpret an absent cash-flow field as zero.

The standalone protected account-facts workflow remains unchanged. Its exact
source-artifact requirement now makes new Runtime sources unavailable: the
preflight or required download must fail before native-account reading and
publication. Do not bypass that check, substitute an old report, or enable a
strategy to manufacture a source. Restoring this optional reader needs a
separately reviewed private handoff; automatic native account facts remain open.

This source change is not a deployment or permission to restore trading.
Runtime stays manually disabled. Both the workflow and application revisions
must be reviewed and adopted through the existing protected process before any
future operator recovery; deploying the workflow alone can still upload an old
application's unsanitized lifecycle file. Do not change pins or enable Runtime
as part of this patch.

Deleting existing Actions artifacts or job logs is separate from this source
fix and requires an exact operator decision because that evidence cannot be
restored normally. Public Git copies and history, clones and prior downloads
are not removed by artifact/log deletion. Keep only non-financial audit metadata
(run/artifact/job identifiers, timestamps, source revisions and field-presence
booleans) in the incident summary; do not copy amounts or account-scope hashes.
12 changes: 8 additions & 4 deletions scripts/runtime_isolation_host_probe.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,14 @@ def inspect_secret_source(workflow_path: Path) -> dict[str, Any]:
workflow = workflow_path.read_text(encoding="utf-8")
key_ref = "BINANCE_API_KEY: ${{ secrets.BINANCE_API_KEY }}"
secret_ref = "BINANCE_API_SECRET: ${{ secrets.BINANCE_API_SECRET }}"
strategy_marker = "- name: 4. Run trading strategy"
next_marker = "- name: 5. Stage execution report"
strategy_marker = " - name: 4. Run trading strategy"
next_marker = " - name: Remove protected LIVE risk authority source"
strategy_block = ""
if strategy_marker in workflow and next_marker in workflow:
strategy_block = workflow[workflow.index(strategy_marker) : workflow.index(next_marker)]
if workflow.count(strategy_marker) == 1 and workflow.count(next_marker) == 1:
candidate = workflow[workflow.index(strategy_marker) : workflow.index(next_marker)]
# The end marker must be the very next step, never a broader job slice.
if not any(line.startswith(" - ") for line in candidate.splitlines()[1:]):
strategy_block = candidate
references_present = key_ref in workflow and secret_ref in workflow
return {
"source": "github_actions_environment_secrets" if references_present else "UNVERIFIED",
Expand All @@ -68,6 +71,7 @@ def inspect_secret_source(workflow_path: Path) -> dict[str, Any]:
"broker_secret_scope": (
"strategy_step_only"
if key_ref in strategy_block and secret_ref in strategy_block
and workflow.count(key_ref) == 1 and workflow.count(secret_ref) == 1
else "UNVERIFIED"
),
"secret_values_read": False,
Expand Down
20 changes: 20 additions & 0 deletions tests/test_authority_reissue_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,23 @@ def test_manual_inputs_and_existing_stop_contract_remain() -> None:
assert f" {name}:\n description:" in workflow
assert "RUNTIME_TARGET_ENABLED` live value is exactly `false`" in docs
assert "first failure or uncertain outcome: stop" in docs


def test_authentication_check_covers_both_modes_without_authority_or_writes() -> None:
workflow = WORKFLOW.with_name("authority-app-auth-check.yml").read_text(encoding="utf-8")

assert "environment-permission: [read, write]" in workflow
assert "max-parallel: 1" in workflow
assert "permission-environments: ${{ matrix.environment-permission }}" in workflow
assert "repositories: ${{ github.event.repository.name }}" in workflow
assert "GH_TOKEN: ${{ steps.app-token.outputs.token }}" in workflow
assert "skip-token-revoke:" not in workflow
assert "BINANCE_RISK_AUTHORITY_JSON" not in workflow
assert "BINANCE_AUTHORITY_UPDATE_TOKEN" not in workflow
assert "actions/checkout" not in workflow
for method in ("POST", "PUT", "PATCH", "DELETE"):
assert f"--method {method}" not in workflow
assert f"-X {method}" not in workflow
assert "scripts/update_binance_authority_reissue.py" not in workflow
assert "main.py" not in workflow
assert "schedule:" not in workflow
Loading
Loading