Skip to content

Commit d9e67a3

Browse files
Pigbibicodex
andauthored
feat(console): expose bounded recovery confirmation read (#335)
Co-authored-by: Codex <noreply@openai.com>
1 parent e865328 commit d9e67a3

6 files changed

Lines changed: 120 additions & 3 deletions

File tree

.github/workflows/deploy-strategy-switch-console.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ jobs:
4343
RESEARCH_TASK_SYNC_TOKEN: ${{ secrets.RESEARCH_TASK_SYNC_TOKEN }}
4444
EXECUTION_EVIDENCE_SYNC_TOKEN: ${{ secrets.EXECUTION_EVIDENCE_SYNC_TOKEN }}
4545
RECONCILIATION_RECOVERY_SYNC_TOKEN: ${{ secrets.RECONCILIATION_RECOVERY_SYNC_TOKEN }}
46+
RECONCILIATION_RECOVERY_CONTROLLER_TOKEN: ${{ secrets.RECONCILIATION_RECOVERY_CONTROLLER_TOKEN }}
4647
steps:
4748
- name: Checkout
4849
uses: actions/checkout@v6
@@ -173,6 +174,13 @@ jobs:
173174
set -euo pipefail
174175
printf '%s' "$RECONCILIATION_RECOVERY_SYNC_TOKEN" | npx wrangler@4.106.0 secret put RECONCILIATION_RECOVERY_SYNC_TOKEN --config wrangler.toml
175176
177+
- name: Sync optional reconciliation-recovery controller token
178+
if: env.RECONCILIATION_RECOVERY_CONTROLLER_TOKEN != ''
179+
working-directory: web/strategy-switch-console
180+
run: |
181+
set -euo pipefail
182+
printf '%s' "$RECONCILIATION_RECOVERY_CONTROLLER_TOKEN" | npx wrangler@4.106.0 secret put RECONCILIATION_RECOVERY_CONTROLLER_TOKEN --config wrangler.toml
183+
176184
- name: Sync M0 research-ledger ingress token
177185
env:
178186
M0_RESEARCH_SYNC_TOKEN: ${{ secrets.M0_RESEARCH_SYNC_TOKEN }}

tests/strategy_switch_worker_validation.mjs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2529,11 +2529,13 @@ const recoveryKv = {
25292529
},
25302530
};
25312531
const recoverySyncValue = ["reconciliation", "recovery", "sync"].join("-");
2532+
const recoveryControllerValue = ["reconciliation", "recovery", "controller"].join("-");
25322533
const recoveryEnv = {
25332534
SESSION_SECRET: "recovery-session-value",
25342535
ALLOWED_GITHUB_LOGINS: "recovery-admin,recovery-reader",
25352536
STRATEGY_SWITCH_ADMIN_LOGINS: "recovery-admin",
25362537
RECONCILIATION_RECOVERY_SYNC_TOKEN: recoverySyncValue,
2538+
RECONCILIATION_RECOVERY_CONTROLLER_TOKEN: recoveryControllerValue,
25372539
STRATEGY_SWITCH_CONFIG: recoveryKv,
25382540
};
25392541
const recoveryAdminCookie = await __test.makeSession("recovery-admin", [], recoveryEnv);
@@ -2631,6 +2633,27 @@ const recoveryConfirmationRequest = {
26312633
candidate_sha256: recoveryCandidateSha256,
26322634
dual_review_binding_sha256: recoveryCandidateSha256,
26332635
};
2636+
const wrongRecoveryControllerRead = await worker.fetch(
2637+
new Request("https://switch.example/api/internal/reconciliation-recovery-confirmation?recovery_id=ibkr_legacy_soxl_live", {
2638+
headers: { Authorization: "Bearer wrong-token" },
2639+
}),
2640+
recoveryEnv,
2641+
);
2642+
assert.equal(wrongRecoveryControllerRead.status, 401);
2643+
const unconfirmedRecoveryControllerRead = await worker.fetch(
2644+
new Request("https://switch.example/api/internal/reconciliation-recovery-confirmation?recovery_id=ibkr_legacy_soxl_live", {
2645+
headers: { Authorization: `Bearer ${recoveryControllerValue}` },
2646+
}),
2647+
recoveryEnv,
2648+
);
2649+
assert.equal(unconfirmedRecoveryControllerRead.status, 404);
2650+
const reusedRecoveryControllerToken = await worker.fetch(
2651+
new Request("https://switch.example/api/internal/reconciliation-recovery-confirmation?recovery_id=ibkr_legacy_soxl_live", {
2652+
headers: { Authorization: `Bearer ${recoverySyncValue}` },
2653+
}),
2654+
{ ...recoveryEnv, RECONCILIATION_RECOVERY_CONTROLLER_TOKEN: recoverySyncValue },
2655+
);
2656+
assert.equal(reusedRecoveryControllerToken.status, 500);
26342657
const staleRecoveryCandidateSha256 = "c".repeat(64);
26352658
const staleRecoverySourcePayload = {
26362659
...recoverySourcePayload,
@@ -2698,6 +2721,19 @@ assert.ok(recoveryStore.has("reconciliation_recovery_current:ibkr_legacy_soxl_li
26982721
assert.ok(recoveryStore.has(
26992722
`reconciliation_recovery_confirmation:ibkr_legacy_soxl_live:${recoveryConfirmationPayload.confirmation.confirmation_sha256}`,
27002723
));
2724+
const confirmedRecoveryControllerRead = await worker.fetch(
2725+
new Request("https://switch.example/api/internal/reconciliation-recovery-confirmation?recovery_id=ibkr_legacy_soxl_live", {
2726+
headers: { Authorization: `Bearer ${recoveryControllerValue}` },
2727+
}),
2728+
recoveryEnv,
2729+
);
2730+
assert.equal(confirmedRecoveryControllerRead.status, 200);
2731+
const confirmedRecoveryControllerPayload = await confirmedRecoveryControllerRead.json();
2732+
assert.equal(confirmedRecoveryControllerPayload.schema_version, "qsl_reconciliation_recovery_controller_read.v1");
2733+
assert.equal(confirmedRecoveryControllerPayload.recovery.candidate_sha256, recoveryCandidateSha256);
2734+
assert.equal(confirmedRecoveryControllerPayload.confirmation.confirmation_sha256, recoveryConfirmationPayload.confirmation.confirmation_sha256);
2735+
assert.equal(confirmedRecoveryControllerPayload.policy.no_order, true);
2736+
assert.equal(confirmedRecoveryControllerPayload.policy.execution_authority_granted, false);
27012737
const recordedRecoveryRead = await worker.fetch(
27022738
new Request("https://switch.example/api/reconciliation-recovery", { headers: recoveryAdminHeaders }),
27032739
recoveryEnv,

web/strategy-switch-console/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ The console only allows live-enabled profiles whose `domain` is included in the
171171

172172
## Existing Live Recovery Verification
173173

174-
An already authorised target that entered `RECONCILE_ONLY` uses a separate, non-executable recovery path rather than the P6 new-strategy queue. A private runtime publishes a redacted `qsl_reconciliation_recovery_source_snapshot.v1` to `POST /api/internal/sync-reconciliation-recovery-source` with a dedicated `RECONCILIATION_RECOVERY_SYNC_TOKEN`; allowlisted users read `GET /api/reconciliation-recovery`, and an administrator may record `POST /api/reconciliation-recovery-confirmations`.
174+
An already authorised target that entered `RECONCILE_ONLY` uses a separate, non-executable recovery path rather than the P6 new-strategy queue. A private runtime publishes a redacted `qsl_reconciliation_recovery_source_snapshot.v1` to `POST /api/internal/sync-reconciliation-recovery-source` with a dedicated `RECONCILIATION_RECOVERY_SYNC_TOKEN`; allowlisted users read `GET /api/reconciliation-recovery`, and an administrator may record `POST /api/reconciliation-recovery-confirmations`. A platform-owned controller reads only the current confirmation binding from `GET /api/internal/reconciliation-recovery-confirmation?recovery_id=...`, protected by a different `RECONCILIATION_RECOVERY_CONTROLLER_TOKEN`.
175175

176176
An item can await confirmation only when it remains `RECONCILE_ONLY`, has two or more read-only samples separated by 1–15 minutes, has a current candidate digest, two approved reviews bound to that same digest, and no blocker. Source and candidate evidence expire after 30 minutes by default. The confirmation is an immutable `no_order=true`, `execution_authority_granted=false` intent only: it cannot dispatch a workflow, read broker credentials, change an account, restore runtime, or place an order. A private platform controller must re-verify raw receipts and the dual-review binding before it may restore the pre-existing runtime; `manual-strategy-switch.yml` rejects all legacy continuity states so it cannot bypass this boundary.
177177

@@ -203,6 +203,7 @@ wrangler secret put RUNTIME_SETTINGS_DISPATCH_TOKEN
203203
wrangler secret put STRATEGY_SWITCH_SYNC_TOKEN # optional; defaults to RUNTIME_SETTINGS_DISPATCH_TOKEN
204204
wrangler secret put M0_RESEARCH_SYNC_TOKEN
205205
wrangler secret put RECONCILIATION_RECOVERY_SYNC_TOKEN
206+
wrangler secret put RECONCILIATION_RECOVERY_CONTROLLER_TOKEN
206207
wrangler secret put ALLOWED_GITHUB_LOGINS
207208
wrangler secret put ALLOWED_GITHUB_ORGS
208209
wrangler secret put STRATEGY_SWITCH_ADMIN_LOGINS
@@ -218,7 +219,7 @@ wrangler kv namespace create STRATEGY_SWITCH_CONFIG
218219

219220
Add the returned namespace id to `wrangler.toml`.
220221

221-
For GitHub Actions auto-deploy, configure `STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID`, `STRATEGY_SWITCH_CONSOLE_URL`, `STRATEGY_SWITCH_SYNC_TOKEN`, `M0_RESEARCH_SYNC_TOKEN`, and either `CLOUDFLARE_API_TOKEN` or `CLOUDFLARE_WRANGLER_CONFIG_TOML` in the `runtime-strategy-switch` environment (or reuse `RUNTIME_SETTINGS_GH_TOKEN` only if it matches the Worker sync secret). Add a separate `RECONCILIATION_RECOVERY_SYNC_TOKEN` before enabling any legacy-recovery publisher; the deploy synchronizes it only when it is present. `CLOUDFLARE_ACCOUNT_ID` is optional when Wrangler can infer it from the token. `M0_RESEARCH_SYNC_TOKEN` must match the separately protected `m0-research-publisher` environment secret; it is only copied to the Worker binding. A missing M0 token fails the deployment before it can retain a stale Worker secret. The workflow deploys the Worker and then syncs the bundled strategy profile catalog into KV so the website is not left with stale profile/plugin metadata.
222+
For GitHub Actions auto-deploy, configure `STRATEGY_SWITCH_CONFIG_KV_NAMESPACE_ID`, `STRATEGY_SWITCH_CONSOLE_URL`, `STRATEGY_SWITCH_SYNC_TOKEN`, `M0_RESEARCH_SYNC_TOKEN`, and either `CLOUDFLARE_API_TOKEN` or `CLOUDFLARE_WRANGLER_CONFIG_TOML` in the `runtime-strategy-switch` environment (or reuse `RUNTIME_SETTINGS_GH_TOKEN` only if it matches the Worker sync secret). Add separate `RECONCILIATION_RECOVERY_SYNC_TOKEN` and `RECONCILIATION_RECOVERY_CONTROLLER_TOKEN` values before enabling a recovery publisher/controller; the deploy synchronizes each only when present, and the Worker rejects an equal pair. `CLOUDFLARE_ACCOUNT_ID` is optional when Wrangler can infer it from the token. `M0_RESEARCH_SYNC_TOKEN` must match the separately protected `m0-research-publisher` environment secret; it is only copied to the Worker binding. A missing M0 token fails the deployment before it can retain a stale Worker secret. The workflow deploys the Worker and then syncs the bundled strategy profile catalog into KV so the website is not left with stale profile/plugin metadata.
222223

223224
An authenticated retry carrying the exact same immutable M0 source-artifact SHA is acknowledged with `200` and `replayed: true`, without another KV write. A different source/run replay or a ledger-time rollback remains rejected with `409`.
224225

web/strategy-switch-console/README.zh-CN.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ CONTROL_PLANE_SYNC_TOKEN
3939
RESEARCH_TASK_SYNC_TOKEN
4040
M0_RESEARCH_SYNC_TOKEN
4141
RECONCILIATION_RECOVERY_SYNC_TOKEN
42+
RECONCILIATION_RECOVERY_CONTROLLER_TOKEN
4243
```
4344

4445
可选:
@@ -263,11 +264,12 @@ GET /api/control-plane
263264
POST /api/internal/sync-reconciliation-recovery-source
264265
GET /api/reconciliation-recovery
265266
POST /api/reconciliation-recovery-confirmations
267+
GET /api/internal/reconciliation-recovery-confirmation?recovery_id=<opaque-id>
266268
```
267269

268270
来源契约是 `qsl_reconciliation_recovery_source_snapshot.v1`。每项只允许携带不含账户或 broker 状态的:不透明恢复 ID、平台/策略、`RECONCILE_ONLY`、QPK 候选 SHA-256、两次以上只读样本的时间窗与数量、双 AI 审计结果/绑定 SHA-256,以及稳定阻断码。`awaiting_human_confirmation` 只有在“两次样本、至少相隔 1 分钟且不超过 15 分钟、至少两位审计者、双审绑定同一候选、无阻断项”同时满足时才会被接受;来源和最后一次候选观测均默认 30 分钟后过期。任何已过期来源或候选都会使确认入口保持关闭。
269271

270-
控制台管理员确认后,Worker 只保存 `qsl_reconciliation_recovery_confirmation.v1` 的不可执行意图,固定 `no_order=true``execution_authority_granted=false`。它不会调用 workflow、读取券商凭证、改账户、下单或启用目标。未来的私有恢复控制器仍必须在同一目标上重新验证来源收据与双审绑定,原子写入五项预期状态摘要并切换到 `ACTIVE_LKG`;任一条件不成立就保持 `RECONCILE_ONLY`。旧 `manual-strategy-switch.yml` 明确拒绝任何 `live_continuity_state != NONE`,避免绕开这条链路。
272+
控制台管理员确认后,Worker 只保存 `qsl_reconciliation_recovery_confirmation.v1` 的不可执行意图,固定 `no_order=true``execution_authority_granted=false`。它不会调用 workflow、读取券商凭证、改账户、下单或启用目标。私有恢复控制器只能以**另一枚** `RECONCILIATION_RECOVERY_CONTROLLER_TOKEN` 调用内部只读路径,读取当前候选绑定与确认摘要;Worker 会拒绝该 token 与来源同步 token 相同。控制器仍必须在同一目标上重新验证来源收据与双审绑定,原子写入五项预期状态摘要并切换到 `ACTIVE_LKG`;任一条件不成立就保持 `RECONCILE_ONLY`。旧 `manual-strategy-switch.yml` 明确拒绝任何 `live_continuity_state != NONE`,避免绕开这条链路。
271273

272274
## 平台运行状态只读接口
273275

@@ -367,6 +369,7 @@ wrangler secret put STRATEGY_HEALTH_SYNC_TOKEN
367369
wrangler secret put CONTROL_PLANE_SYNC_TOKEN
368370
wrangler secret put M0_RESEARCH_SYNC_TOKEN
369371
wrangler secret put RECONCILIATION_RECOVERY_SYNC_TOKEN
372+
wrangler secret put RECONCILIATION_RECOVERY_CONTROLLER_TOKEN
370373
wrangler secret put ALLOWED_GITHUB_LOGINS
371374
wrangler secret put ALLOWED_GITHUB_ORGS
372375
wrangler secret put STRATEGY_SWITCH_ADMIN_LOGINS

web/strategy-switch-console/worker.js

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ const RECONCILIATION_RECOVERY_CURRENT_PREFIX = "reconciliation_recovery_current:
129129
const RECONCILIATION_RECOVERY_SOURCE_SCHEMA_VERSION = "qsl_reconciliation_recovery_source_snapshot.v1";
130130
const RECONCILIATION_RECOVERY_DASHBOARD_SCHEMA_VERSION = "qsl_reconciliation_recovery_dashboard.v1";
131131
const RECONCILIATION_RECOVERY_CONFIRMATION_SCHEMA_VERSION = "qsl_reconciliation_recovery_confirmation.v1";
132+
const RECONCILIATION_RECOVERY_CONTROLLER_READ_SCHEMA_VERSION = "qsl_reconciliation_recovery_controller_read.v1";
132133
const RECONCILIATION_RECOVERY_MAX_SOURCES = 100;
133134
const RECONCILIATION_RECOVERY_MAX_BODY_BYTES = 128 * 1024;
134135
const RECONCILIATION_RECOVERY_DEFAULT_STALE_TTL_SECONDS = 30 * 60;
@@ -358,6 +359,9 @@ export default {
358359
if (url.pathname === "/api/internal/sync-reconciliation-recovery-source" && request.method === "POST") {
359360
return await syncReconciliationRecoverySourceResponse(request, env);
360361
}
362+
if (url.pathname === "/api/internal/reconciliation-recovery-confirmation" && request.method === "GET") {
363+
return await reconciliationRecoveryControllerReadResponse(request, env, url);
364+
}
361365
if (url.pathname === "/api/reconciliation-recovery" && request.method === "GET") {
362366
return await reconciliationRecoveryResponse(request, env);
363367
}
@@ -1983,6 +1987,59 @@ async function reconciliationRecoveryResponse(request, env) {
19831987
return json(await aggregateReconciliationRecoverySources(env));
19841988
}
19851989

1990+
// This is a least-privilege service-to-service read for a platform-owned
1991+
// controller. It deliberately returns only the current, redacted digest
1992+
// binding and immutable human receipt; broker state remains in the platform.
1993+
async function reconciliationRecoveryControllerReadResponse(request, env, url) {
1994+
requireDedicatedReconciliationRecoveryControllerToken(request, env);
1995+
if (!hasConfigStore(env)) {
1996+
return json({ ok: false, error: "reconciliation recovery KV is not configured" }, 503);
1997+
}
1998+
let recoveryId;
1999+
try {
2000+
recoveryId = normalizeControlPlaneIdentifier(
2001+
url.searchParams.get("recovery_id") || "",
2002+
"reconciliation recovery controller recovery_id",
2003+
false,
2004+
);
2005+
} catch (error) {
2006+
return json({ ok: false, error: error.message || "invalid recovery_id" }, 400);
2007+
}
2008+
const dashboard = await aggregateReconciliationRecoverySources(env);
2009+
let recovery;
2010+
try {
2011+
recovery = currentReconciliationRecoveryRequest(dashboard, recoveryId);
2012+
} catch (error) {
2013+
return json({ ok: false, error: error.message || "reconciliation recovery is unavailable" }, error.status || 409);
2014+
}
2015+
const entry = dashboard.recoveries.find((item) => item.recovery?.recovery_id === recoveryId);
2016+
if (!entry?.confirmation) {
2017+
return json({ ok: false, error: "reconciliation recovery has no current confirmation" }, 404);
2018+
}
2019+
return json({
2020+
ok: true,
2021+
schema_version: RECONCILIATION_RECOVERY_CONTROLLER_READ_SCHEMA_VERSION,
2022+
recovery: {
2023+
recovery_id: recovery.recovery_id,
2024+
platform: recovery.platform,
2025+
strategy_profile: recovery.strategy_profile,
2026+
environment: recovery.environment,
2027+
reconciliation_state: recovery.reconciliation_state,
2028+
candidate_sha256: recovery.candidate_sha256,
2029+
dual_review_binding_sha256: recovery.dual_review.evidence_binding_sha256,
2030+
evidence_sample_count: recovery.evidence_sample_count,
2031+
first_observed_at: recovery.first_observed_at,
2032+
last_observed_at: recovery.last_observed_at,
2033+
},
2034+
confirmation: entry.confirmation,
2035+
policy: {
2036+
no_order: true,
2037+
execution_authority_granted: false,
2038+
controller_must_reverify: true,
2039+
},
2040+
});
2041+
}
2042+
19862043
function currentReconciliationRecoveryRequest(dashboard, recoveryId) {
19872044
if (dashboard?.data_status !== "ready") {
19882045
throw new HttpError("current reconciliation recovery evidence is not ready", 409);
@@ -3153,6 +3210,17 @@ function requireDedicatedReconciliationRecoverySyncToken(request, env) {
31533210
if (token !== expected) throw new HttpError("reconciliation recovery sync token is invalid", 401);
31543211
}
31553212

3213+
function requireDedicatedReconciliationRecoveryControllerToken(request, env) {
3214+
const expected = String(env.RECONCILIATION_RECOVERY_CONTROLLER_TOKEN || "");
3215+
if (!expected) throw new HttpError("reconciliation recovery controller token is not configured", 500);
3216+
if (expected === String(env.RECONCILIATION_RECOVERY_SYNC_TOKEN || "")) {
3217+
throw new HttpError("reconciliation recovery controller token must differ from the source sync token", 500);
3218+
}
3219+
const header = request.headers.get("Authorization") || "";
3220+
const token = header.match(/^Bearer\s+(.+)$/i)?.[1] || "";
3221+
if (token !== expected) throw new HttpError("reconciliation recovery controller token is invalid", 401);
3222+
}
3223+
31563224
function requireDedicatedResearchTaskSyncToken(request, env) {
31573225
const expected = String(env.RESEARCH_TASK_SYNC_TOKEN || "");
31583226
if (!expected) throw new HttpError("research task sync token is not configured", 500);

web/strategy-switch-console/wrangler.toml.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ workers_dev = true
1414
# - RESEARCH_TASK_SYNC_TOKEN # dedicated immutable research-task source token; never reuse another token
1515
# - M0_RESEARCH_SYNC_TOKEN # dedicated read-only M0 research-ledger ingress token; never reuse another token
1616
# - RECONCILIATION_RECOVERY_SYNC_TOKEN # dedicated legacy recovery evidence ingress token; never reuse another token
17+
# - RECONCILIATION_RECOVERY_CONTROLLER_TOKEN # dedicated private recovery-controller read token; never reuse another token
1718
# - ALLOWED_GITHUB_LOGINS
1819
# - ALLOWED_GITHUB_ORGS
1920
# - STRATEGY_SWITCH_ADMIN_LOGINS

0 commit comments

Comments
 (0)