The dashboard NetworkPolicy blocked egress to the kube-apiserver (kube-proxy
DNATs kubernetes.default.svc 10.32.0.1:443 -> 172.16.0.11:6443, and both
10.32/16 and 100.64/15 were excepted). On a FRESH sidecar start (e.g. after
the CI deploy's scale-0->1) tailscale must read its ts-state Secret via the
apiserver -> without the allow it crashlooped 'context deadline exceeded' ->
bad gateway on dashboard.fxhnt.ai. Adds the 6443->172.16.0.11/32 egress rule,
mirroring the dagster NP fix (06d12fd). Pre-existing gap, surfaced by the
first pipeline deploy that recreated the dashboard pod.
The dashboard/dagster manifests bundled the tailscale-sidecar SA+Role+RoleBinding,
which forced the webhook-triggered fxhnt-ci-deployer SA to hold create/update/patch
on roles/rolebindings in its own namespace — a privilege-escalation primitive. Move
those 6 objects to infra/k8s/rbac/tailscale-sidecars.yaml, applied once out-of-band
(not by CI); trim the deployer Role to drop rbac.authorization.k8s.io entirely, and
drop serviceaccounts too since neither CI-applied manifest creates one anymore.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
C1 (Critical, CI review): dagster webserver/daemon + K8sRunLauncher job_image
were pinned to a hardcoded tag (dagster-k8s-25e3d7c), so a mode=build dep bump
updated the dashboard (:latest) but left dagster + its run Jobs on stale deps
-> version skew in the nightly ETL. Now :latest with image_pull_policy Always.
I1 (Important): a trap scales the dashboard back to replicas=1 on ANY exit
path, so a failure between scale-0 and scale-1 can't strand the cockpit at 0.
Superseded by fxhnt-cockpit-workflowtemplate.yaml + fxhnt-build-sensor.yaml
+ fxhnt-ci-rbac.yaml (all validated end-to-end, both modes). The old files
targeted decommissioned infra (ci-compile-cpu pool, gitea-sshd.foxhunt,
MinIO artifacts) and no longer worked.
The {{=sprig.trunc(...)}} expression wasn't evaluated by this Argo (v4.0.6) —
Kaniko received it literally and rejected it as an invalid tag. Use the plain
{{workflow.parameters.commit-sha}} substitution (which works everywhere else
in this template); the full 40-char sha is a valid Docker tag, no trunc needed.
/busybox/sh -c parsed the {{=sprig.trunc(...)}} destination and errored on
the '(' -> syntax error, exit 2. Switched to Kaniko's entrypoint + a plain
YAML args list (the proven msp build-image pattern): Argo evaluates the
expression and passes it straight to Kaniko, no shell. Also pin executor
v1.23.2 (matches msp; no busybox needed).
Requested cpu:2/mem:4Gi was sized for the removed dedicated ci-compile-cpu
pool; the shared large nodes (3800m, ~1.3-1.6 CPU free) couldn't schedule it
(Insufficient cpu, autoscaler didn't add nodes). cpu:1/mem:3Gi request
(limit 3/10Gi) fits and still builds fine (Kaniko is I/O-bound here).
The dashboard+dagster manifests bundle a ConfigMap, ServiceAccount, and the
tailscale-sidecar Role/RoleBinding alongside the Deployment, so kubectl apply
of the whole file needs those types. RBAC-management is scoped to foxhunt
only (documented as a deliberate, namespace-local trade-off).
archiveLogs=true was inherited from the old MinIO-backed pipeline; this
Argo has no artifact repository configured, so log archival failed the
step. We don't need archived logs -> false.
The cp-key-to-~/.ssh idiom failed under Argo's executor because emissary
doesn't set HOME=/root, so git couldn't find the key -> clone exit 128.
GIT_SSH_COMMAND with the absolute key path is HOME-independent (verified
cloning under HOME=/nonexistent). Applied to all three git-clone steps.
The reanimated template used the OLD foxhunt-cluster gitea SSH service
(gitea-sshd.foxhunt), which doesn't exist on bizworx -> git clone failed
with exit 128. The real service is gitea-ssh.gitea.svc:22 (verified: the
argo-git-ssh-key deploy key resolves gitadmin/fxhnt HEAD there).
Argo's executor reads a valueFrom.path output parameter from a mounted
volume, not the container rootfs -> /tmp/mode gave 'no such file or
directory'. /workspace is the shared emptyDir the step already mounts.
The reanimated template inherited topology.kubernetes.io/zone=fr-par-2 from
the old ci-compile-cpu pool, but all nodes (and every fxhnt workload)
are in fr-par-1 -> pods were Unschedulable. pool-name=large alone is correct.
First real workflow run surfaced two gaps: (1) the WorkflowTemplate's SA
argo-workflow didn't exist in foxhunt (only in argo where msp builds);
(2) argo-events-sa couldn't submit workflows cross-ns into foxhunt. Adds
the SA, a least-privilege deployer Role (apply/scale/annotate/rollout the
dashboard+dagster deployments, exactly what kubectl-deploy runs), the Argo
workflowtaskresults role, and a foxhunt-scoped workflow-submitter binding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
commit-sha (from the untrusted Gitea webhook body.after) was substituted
INLINE into shell scripts via {{workflow.parameters.commit-sha}} in all
three git-clone/decide-mode steps -> a malicious payload could inject shell
on a cluster-privileged pod. Now passed via ENV + hex-validated before use
(the build-image template's pattern), and mode is allowlist-checked. Closes
the background security review's Orchestrator Template Injection finding.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Verified live: fxhnt-dashboard has its own dedicated tailscale sidecar
(TS_HOSTNAME=fxhnt-dashboard) and there is no Ingress/Gateway/VirtualService
in ns foxhunt routing dashboard.fxhnt.ai anywhere. fxhnt-ingress is an
unrelated nginx+tailscale pod (its own Deployment/Service, own tailscale
identity) for git/ssh routes, not the dashboard. The tailscale-session race
on `rollout restart` is real, so keep scale 0->1. Added the
fxhnt.io/deployed-sha annotation on both fxhnt-dashboard and dagster before
the rollouts, per the CI/CD plan Task 2.
Verified the Argo Events CRD has an open schema -> trigger-level path-filter
negation can't be validated. Redesigned the mode-split to live in the
workflow (git diff --name-only) with a single msp-style sensor (lowest risk).
5 tasks: WorkflowTemplate build side, deploy step (verify tailscale route),
sensor + decide-mode, apply+webhook, e2e verify both modes + retire old file.
Every cluster apply gated behind server dry-run.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Operator's principle: fxhnt and bizworx are different projects — use the
shared bizworx platform infra (Gitea, Argo engine, registry, node-pools)
but keep fxhnt-owned objects (WorkflowTemplate, build/deploy pods, secrets,
RBAC) in the foxhunt ns. Only the Sensor lives in argo-events (unavoidable,
kept minimal). No cross-ns RBAC, no fxhnt secrets in platform ns. Adds a
deployed-sha annotation for git-sync reproducibility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Reanimate the retired cockpit-build-deploy WorkflowTemplate with the mode
split (build vs deploy) driven by a path-filter sensor: dep/Dockerfile
changes -> Kaniko rebuild, code-only changes -> fast rollout (git-sync
re-pulls src). Reuses the existing bizworx Argo Events/Workflows/Kaniko
stack (one Gitea, verified). No ArgoCD app, no lock file. Deploy-step
route (tailscale scale-0->1 vs rollout-restart) flagged to verify.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Removes all VRP (volatility-risk-premium) code — 7 modules, DVOL pipeline,
vrp_nav Dagster asset + OPRA-freeze helpers, XspOptionBarsRepo class, CLI
commands, CronJob, registry entries, 13 test files. KEEPS the OPRA data
(xsp_option_bars table + XspOptionBarRow model + 13yr .dbn) per constraint.
Full suite green (2019 passed, 2 skipped); whole-branch review clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The CronJob invoked `fxhnt backfill-xsp-opra` (removed in Task 3); with the
command gone it would crash-loop. Its only purpose was freezing OPRA marks
for VRP recompute. OPRA data already frozen in xsp_option_bars (KEPT).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Task 6+7 (shipped together per plan so the key is never gone while a literal
still references it). Registry: 12->10 keys (vrp/vrp_exec removed). Literals:
_IBKR_ACCOUNT_SIDS + SIM_BOOKS + _REF_ALIAS drop vrp. Tests that asserted
vrp-present updated to assert vrp-ABSENT. Covering tests (forward_health,
dashboard_service, forward_definition, sim_curves, display_names) 39 green:
surviving strategies still WAIT-by-default (Constraint 2 upheld).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Research brief (6-agent workflow): NL tax/fund/accounting landscape for
going live. Resolved structure given operator's existing tech-BV + holding
+ DGA-salary: arbeid taxed in the BV, dividend (cleanly taxed) to private,
passive fund capital in Box 3 -> removes the Box-1 reclassification risk.
EUR-functional fund-ledger design spec: append-only double-entry ledger in
the existing Postgres warehouse, IBKR Flex ingestion, FIFO, fund-only scope.
No conversion pipeline (EUR end-to-end). NOT tax advice.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- fix(ucits): key the NLV stray-check on IBKR's reported symbol — the IEF UCITS
line (ticker CBU0) reports holdings as canonical CSBGU0, so the legitimate
Treasury book position was flagged stray + dropped from NLV. Adds
UcitsListing.ibkr_symbol (probe-verified: only CBU0 diverges) + probe-ucits-symbols CLI.
- fix(ib-gateway): zero-pad AUTO_RESTART_TIME (8:00->08:00 AM) — IBC rejected the
malformed time, leaving the daily auto-restart unconfigured -> ~16 restarts/day.
Fixed + verified live (pod RESTARTS=0). Liveness failureThreshold 5->6.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
IBC's parser rejected '8:00 AM' ('Auto restart time setting must be hh:mm AM/PM'),
leaving the daily auto-restart UNCONFIGURED, so the gateway churned ~35 restarts over
2 days instead of one clean daily cycle. Zero-padded to '08:00 AM' — IBC now logs
'Auto restart time set to 08:00 AM' (verified live 2026-07-20, fresh pod RESTARTS=0).
08:00 UTC is before the 09:00 UTC UCITS rebalance window so the daily restart won't
collide with execution. Also bumped liveness failureThreshold 5->6 (180s) for the
reauth window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Probe (probe-ucits-symbols, live 2026-07-20) confirmed only the IEF line diverges:
config ticker CBU0 resolves to IBKR canonical symbol CSBGU0, which is what
positions() reports the holding under. _expected_book_symbols keyed on the display
ticker, so the legitimate 55-share IEF/Treasury book position (reported CSBGU0) was
flagged 'outside the book' and dropped from NLV.
Add UcitsListing.ibkr_symbol (defaults to ticker; set CSBGU0 for IEF) and include it
in _expected_book_symbols so the stray-check matches what IBKR actually reports. A
genuine stray (IBIT) is still flagged. Also fixed 4 lint items on the touched lines.
11 dashboard tests green (new CSBGU0-not-stray test + IBIT-still-stray + no regression).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The IEF UCITS line's config ticker CBU0 resolves to IBKR canonical symbol CSBGU0
(the Error-478 root). After Monday's conId fix, CBU0 fills and IBKR reports the
holding as CSBGU0 — but _expected_book_symbols keys on the display ticker CBU0, so
the legitimate IEF/Treasury book position is flagged 'stray' and dropped from NLV
(live 2026-07-20: positions_json shows CSBGU0:55).
This adds IbkrBroker.resolve_symbol_info (read-only qualify-by-ISIN -> symbol/
localSymbol/conId) + the probe-ucits-symbols CLI to enumerate every line's real
reported symbol, so the config can carry an ibkr_symbol field and the NLV
stray-check keys on the REPORTED symbol. Probe first (this commit), fix config next.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Forensics found the positioning sleeve's 2026-07 -15% drawdown was ~entirely one
coin (LABUSDT, 10.3% long leg, -59%/-79.5% over 2 days). A 1295-day sweep validated
an ex-ante per-coin gross cap of 0.07: Sharpe 2.60->2.69, maxDD -32.3%->-29.9%,
per-year Sharpe non-decreasing; cap=0.04 falsified (2.43). Single-pass clip+renormalize
on the forming weights (causal), validated ~= iterative.
Threaded through EVERY live sink via opt-in (default None = uncapped = fail-safe):
modeled return, reconciliation ref, forward NAV, paper-book positions, testnet exec
sizing, live backfill, capital allocation. Research/eval paths left uncapped by design
(honest falsification). A source-level coverage-audit test (mutation-verified) guards
against a future uncapped live path.
positioning / bybit_4edge / bybit_4edge_levered re-inception v3, inception_t0 backdated
to 2026-07-06 so the forward gate recomputes the window (not reset).
10 tasks, whole-branch review: MERGE. 1126 tests green (the 1 skip-worthy failure is a
pre-existing torch/poc-extra env artifact, identical on master).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Final whole-branch review Minor: the audit checked the token appeared anywhere in
the function body, so a partial regression (param kept in signature but dropped
from the inner seam call) passed silently. Now require as a
forwarded kwarg. Mutation-verified: dropping the forwarding from run_bybit_testnet's
call (while keeping the import) now FAILS the audit, where the substring check passed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An exhaustive sweep of every caller in the positioning-cap dependency chain found two more live paths
still uncapped: the Bybit paper-book BACKFILL (bybit_paper_backfill.py, sizes/persists live paper
positions across history) and the honest-reference allocation inputs (allocation_honest_inputs.py,
feeds record_allocation, the LIVE strategy allocation engine's ex-ante capital sizing).
Threads positioning_coin_gross_cap (opt-in, default None) through both, matching the existing pattern:
- bybit_paper_backfill.py: _extract_per_sleeve + backfill_bybit_paper_book now accept and forward the
cap; cli.py's bybit-paper-backfill command passes POSITIONING_COIN_GROSS_CAP.
- allocation_honest_inputs.py: _sleeve_inputs + _book_curve + _deploy_curve + honest_allocation_inputs
now accept and forward the cap; allocation_ingest.py's record_allocation (called from the
cockpit_forward Dagster asset) passes POSITIONING_COIN_GROSS_CAP into honest_allocation_inputs.
Adds tests/unit/test_positioning_cap_coverage_audit.py — a source-level regression guard asserting every
LIVE-book/position/allocation callsite passes positioning_coin_gross_cap. Fixed a boundary bug in the
brief's own _func_body helper regex (bare ^\S with re.MULTILINE always matches position 0 of the sliced
remainder, truncating every function body to its signature line) so the audit actually inspects each
function's body instead of false-failing on already-capped callsites.
Research/eval callers (verify_positioning_edge, walk_forward_positioning, look_ahead_audit,
_variant_weights, _drop_top_n, _liquidity_sweep, positioning_metalabel, bybit_overlay_ab, vrp_eval,
onchain_fundamental_eval, spread_overlay, honest_report) are untouched — they stay uncapped by design.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
An exhaustive grep of every build_bybit_paper_book / combined_symbol_weights* /
latest_raw_sleeve_weights caller found bybit_testnet_run.py sized the executed
testnet positions UNCAPPED (latest_raw_sleeve_weights + combined_symbol_weights
called without positioning_coin_gross_cap). The cron is suspended (no creds) but
it is a real order-placing path — the moment it arms it would size concentrated
positions. Thread POSITIONING_COIN_GROSS_CAP the same as every other exec path.
35 testnet tests green, import + lint clean (no circular import).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The review of fb4cb49 found one more silently-uncapped call into
build_bybit_paper_book: the `bybit-paper-book` CLI command (cli.py) used
to manually seed/re-seed the SAME live Bybit paper book the nightly
Dagster asset populates. It mirrored the nightly asset's pre-fix call
exactly and was missed because it wasn't in that task's file list.
Pass positioning_coin_gross_cap=POSITIONING_COIN_GROSS_CAP, matching the
nightly bybit_paper_book asset call site. Default-None behavior elsewhere
is unaffected.
Threads positioning_coin_gross_cap through the two remaining uncapped
paths found by the Task-7 whole-branch review:
- Critical: combined_symbol_weights / combined_symbol_weights_and_returns /
latest_raw_sleeve_weights / derive_and_persist_bybit_paper_book /
build_bybit_paper_book now accept positioning_coin_gross_cap and forward
it into sleeve_weights_and_contributions, so the live Bybit paper-book's
ACTUAL persisted positions honor the same per-coin cap as the reconciled
return. The nightly bybit_paper_book asset now passes
POSITIONING_COIN_GROSS_CAP.
- Important: bybit_4edge_backtest_summary gained a positioning_coin_gross_cap
sibling next to positioning_tail_cap_k, forwarded into
BybitFourEdgeStrategy; report_backtest_summary now passes
POSITIONING_COIN_GROSS_CAP alongside POSITIONING_TAIL_CAP_K.
Scope: coin_gross_cap only (the EX-ANTE weight cap) — capacity_capital and
tail_cap_k are gains-only return haircuts and do not belong on the weight
path. Default None everywhere (OFF = byte-identical).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The per-coin gross cap (0.07) reached the reconciliation REF but not the live
forward NAV (BybitFourEdgeStrategy), so the gate would compare a capped ref
against an uncapped forward and misread a healthy book as diverging. Threads
positioning_coin_gross_cap through BybitFourEdgeStrategy, the four assets.py
builders + three nightly call sites, and the three migration_builders.py
builders, exactly parallel to the existing positioning_tail_cap_k. Also fixes
positioning_weights_and_contributions + sleeve_weights_and_contributions so
the paper-book per-symbol attribution reconciles against the capped sleeve
return (Sigma weight*return == sleeve_returns_from_store), preserving the SSOT
invariant. Default None everywhere (off = byte-identical).
Every minor is a latent bug, pre-existing is no exemption:
- bybit_book_eval __getattr__ missing -> Any return annotation
- two unused type: ignore[assignment] comments (mypy: unused) removed
- positioning_returns_from_store: bind the dict[int,float] 'net' to a typed local
instead of returning Any from _book_breakdown(...)['net']
mypy clean on both files; 26 positioning tests still green (behavior-neutral).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>