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
5 changes: 5 additions & 0 deletions docs/m0_research_publisher_envelope_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,3 +159,8 @@ Environment 保护的 fallback。控制台部署会在部署前验证该 secret
URL、发布 token 和 QAR 读取 token 不会写进封套、`GITHUB_STEP_SUMMARY` 或 workflow 输出。该
workflow 不读取运行时、平台、selector、策略或券商配置;其唯一网络写入是构建器在
`--publish` 明确指定时,对上述研究接收地址发送经过校验的 no-order 封套。

若已存 current 与来件的 `source_artifact.sha256` 完全相同,接收端返回 `200` 和
`replayed: true`,且不再写 KV。这只确认同一不可变来源已经收到,供网络重试或重复人工触发
恢复;不同 source artifact 的重复 source run、不同来源的重复 ledger 或 ledger 时间回退仍返回
`409`。
19 changes: 18 additions & 1 deletion tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2818,7 +2818,9 @@ const m0ResearchSync = await worker.fetch(
m0ResearchEnv,
);
assert.equal(m0ResearchSync.status, 200);
assert.equal((await m0ResearchSync.json()).no_order, true);
const m0ResearchSyncPayload = await m0ResearchSync.json();
assert.equal(m0ResearchSyncPayload.replayed, false);
assert.equal(m0ResearchSyncPayload.no_order, true);
assert.ok(m0LedgerStore.has("m0_research_ledger_current"));
assert.ok(m0LedgerStore.has(`m0_research_ledger_archive:${m0ResearchLedgerSha}`));
const storedM0ResearchCurrent = JSON.parse(m0LedgerStore.get("m0_research_ledger_current"));
Expand All @@ -2831,6 +2833,21 @@ const m0ResearchLedgerWrites = m0LedgerPutOptions.filter((entry) => (
));
assert.equal(m0ResearchLedgerWrites.length, 2);
assert.ok(m0ResearchLedgerWrites.every((entry) => entry.options?.expirationTtl === 14 * 24 * 60 * 60));
const m0ExactSourceReplay = await worker.fetch(
new Request("https://switch.example/api/internal/sync-m0-research-ledger", {
method: "POST",
headers: { Authorization: `Bearer ${m0ResearchSyncValue}`, "Content-Type": "application/json" },
body: JSON.stringify(m0ResearchEnvelope),
}),
m0ResearchEnv,
);
assert.equal(m0ExactSourceReplay.status, 200);
const m0ExactSourceReplayPayload = await m0ExactSourceReplay.json();
assert.equal(m0ExactSourceReplayPayload.replayed, true);
assert.equal(m0ExactSourceReplayPayload.no_order, true);
assert.equal(m0LedgerPutOptions.filter((entry) => (
entry.key === "m0_research_ledger_current" || entry.key.startsWith("m0_research_ledger_archive:")
)).length, 2);
const m0ResearchRead = await worker.fetch(
new Request("https://switch.example/api/m0-research", { headers: m0ResearchCookieHeaders }),
m0ResearchEnv,
Expand Down
2 changes: 2 additions & 0 deletions web/strategy-switch-console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,8 @@ Add the returned namespace id to `wrangler.toml`.

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). `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.

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`.

Deploy:

```bash
Expand Down
6 changes: 4 additions & 2 deletions web/strategy-switch-console/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,8 +124,10 @@ JSON 计算并验证 `ledger_sha256`;`source_artifact.sha256` 是已认证的
不超过 262,144 bytes;Worker 保留同一请求体上限作为接收端边界,不另行转换或放宽该契约。

Worker 固定使用 `m0_research_ledger_current` 和由已校验 digest 派生的
`m0_research_ledger_archive:<ledger_sha256>`;调用方不能传入 KV key。current 记录检测到
重复 artifact/ledger、相同 source run ID 或 ledger 时间回退会返回 `409`。这是基于 KV
`m0_research_ledger_archive:<ledger_sha256>`;调用方不能传入 KV key。若来件的
`source_artifact.sha256` 与当前记录完全相同,Worker 返回 `200` 与 `replayed: true`,且不写入
KV,用于网络重试或重复人工触发。不同 source artifact 的相同 source run ID、不同来源的重复
ledger 或时间回退仍会返回 `409`。这是基于 KV
当前记录的 **best-effort** 重放/回退保护:Cloudflare KV 不是线性一致的比较并交换存储,
并发写入仍不能被表述为强原子顺序保证。本接口不为此新增 Durable Object 或其他绑定;它的
职责仍限于 no-order 研究资料接收。current 与 archive 均使用 Cloudflare KV 的 14 天物理
Expand Down
18 changes: 18 additions & 0 deletions web/strategy-switch-console/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -2259,6 +2259,23 @@ async function syncM0ResearchLedgerResponse(request, env) {

const current = await readCurrentM0ResearchLedgerRecord(env);
if (current) {
// A retry for the exact immutable source is not a new decision or a
// second write. Acknowledge it so a lost response cannot make a valid
// no-order publication look failed, while retaining the guards below.
if (envelope.source_artifact.sha256 === current.envelope.source_artifact.sha256) {
return json({
ok: true,
schema_version: current.envelope.schema_version,
source_repository: current.envelope.source_artifact.repository,
source_revision: current.envelope.source_artifact.revision,
source_run_id: current.envelope.source_artifact.run_id,
artifact_id: current.envelope.source_artifact.artifact_id,
ledger_sha256: current.envelope.ledger_sha256,
replayed: true,
no_order: true,
expires_at: current.expires_at,
});
}
const replayError = m0ResearchLedgerReplayError(current.envelope, envelope);
if (replayError) return json({ ok: false, error: replayError }, 409);
}
Expand Down Expand Up @@ -2308,6 +2325,7 @@ async function syncM0ResearchLedgerResponse(request, env) {
source_run_id: envelope.source_artifact.run_id,
artifact_id: envelope.source_artifact.artifact_id,
ledger_sha256: envelope.ledger_sha256,
replayed: false,
no_order: true,
expires_at: expiresAt,
});
Expand Down