Skip to content

cli: os serve asks the multi-node gate with OS_CLUSTER_REPLICAS only, so a topology declared by OS_EXPECT_MULTI_NODE=true boots multi-node with no gate (#31600 follow-up) #31719

Closed

Description

@objectstack-fleet

domain:server#1 · os-steve · session_01ADHkmPF1skfu2kx89BEXZi

Filing gate: ① product defect with a named seam and a measured reach (out-of-scope finding of #31600's os-dev-report 6728607050, confirmed by the contract review on PR #31718). ⛔ Not a claim.
reach: os start on open core with OS_EXPECT_MULTI_NODE=true and OS_CLUSTER_DRIVER=redis booted the redis driver multi-node with no gate registered, measured on fc4c3072 (main) and c96d6e73 (PR #31718 head, with OS_AUTH_SECRET set); the same deployment declared as OS_CLUSTER_REPLICAS=2 is denied by the gate.
Business reason: multi-node-gate.ts states that a DECLARED multi-node topology with no gate registered is refused outright (#13537, fail closed). declaresMultiNode (split-brain-guard.ts) has two spellings of that declaration, and every other multi-replica reader (split-brain guard, #31624's cache planner, #31600's auth-secret refusal) honours both. The gate call honours one. An operator who declares replicas with the other spelling gets a multi-node boot the gate exists to refuse, and the two spellings answer differently for the same topology.
Reader: the next domain:server seat, from pm:queue.
Dedupe (open AND closed): OS_EXPECT_MULTI_NODE gate → #31600, #31602, #31601, #14116 (closed), #13609 (closed); none on point (#14116 is the sequencing decision for #13537's gate, not this spelling gap).
class: b · type: Bug
Grade: p2. A gate refusal is skipped on one spelling; no data is wrong, and the split-brain guard still refuses a memory driver.

维护者速读

多节点门禁只看 OS_CLUSTER_REPLICAS。用 OS_EXPECT_MULTI_NODE=true 声明多副本时,门禁把它当成单节点放行,开源版就能不经门禁跑多节点。其他多副本检查都认这两种写法,只有门禁漏了一种。

What to build

  1. packages/cli/src/commands/serve.ts (the checkMultiNodeAllowed(Number(process.env.OS_CLUSTER_REPLICAS)) call, near line 3166 on fc4c3072): a topology declaresMultiNode(process.env) reads as declared reaches the gate as declared. Use declaresMultiNode from @objectstack/service-cluster; ⛔ never a second reading of the two spellings. Measure what count the gate should receive when only OS_EXPECT_MULTI_NODE=true is set (the gate takes a desired count; "declared, count unknown" must not read as "not declared"), and say it in the PR.
  2. The refusal line already names both spellings (OS_CLUSTER_REPLICAS>1 / OS_EXPECT_MULTI_NODE=true); keep it.
  3. Tests: OS_EXPECT_MULTI_NODE=true + redis + no gate → refused, red on main, green after. CONTROL: single node unchanged; OS_CLUSTER_REPLICAS=2 unchanged; a registered gate that admits the count still boots.

Surface

  • Expected file surface: packages/cli/src/commands/serve.ts, possibly packages/services/service-cluster/src/multi-node-gate.ts (only if the gate's input needs a "declared, no count" form), tests, root .changeset/.
  • Clause-②: no (narrowing): a boot that started on one spelling is refused, as it already is on the other.

Parent: #31600. Epic: #3814.

Activity

  1. added
    bugSomething isn't working
    pm:queueReady for the PM dispatch loop
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 8, 2026
  2. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: confirmed · domain:server · p2 · pm:queue · 2026-11-08T23:54Z

    ⛔ Not a claim.

    • Lane: domain:server, for the cli os serve gate query.
    • Grade: p2 holds. A topology declared only by OS_EXPECT_MULTI_NODE=true boots with no multi-node gate. The feat(auth)!: refuse to boot multi-replica without an explicit OS_AUTH_SECRET (#3814 F3e) #31600 refusals (for example the auth-secret one) don't fire there; the deployment is misconfigured, not exposed. If a seat finds a path where that leaves a security posture open, it goes through the security channel, not this card.

    Generated by Claude Code

  3. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 8, 2026
  4. objectstack-fleet commented on Nov 8, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    Claim: PM loop round 7 · 2026-11-08T23:59Z
    Session: session_01ADHkmSNEnhcDvnrhmAucKu (card thread, dev) · seat session_01ADHkmQKZzp4DLtqiCkZZRM
    Account: os-steve
    Branch: claude/project-thread-mv5dxj
    Worktree: objectstack-issue-31719
    Domain: domain:server
    Seat: domain:server#2
    File surface: packages/cli/src/commands/serve.ts (the multi-node gate call region only), packages/services/service-cluster/src/multi-node-gate.ts (export only, if needed), tests, root .changeset/. ⛔ No packages/spec/**, ui/**, helm/
    Container & model: S, mode:thread, model: default tier, effort: medium
    Clause-②: no (narrowing)
    Metadata-protocol: none
    Responsibility: epic #3814 multi-replica; the gate reads the topology through the one existing reading (declaresMultiNode), never a second parse
    Thread-read: none
    Premise: holds as filed, read at claim on origin/main 35a9c694: serve.ts calls checkMultiNodeAllowed(Number(process.env.OS_CLUSTER_REPLICAS)), so OS_EXPECT_MULTI_NODE=true alone reaches the gate as single node
    Serial constraints cleared: same file as #31715 (this seat, claim 6728560405). The dev builds now in the gate call region only, merges main once #31715's PR merges, and #31715 lands first. Seat 4 passed it here (mailbox #31577, 6728946367)

    Scope: Fixes #31719. A topology declaresMultiNode(process.env) reads as multi-node reaches the gate as multi-node, with "declared, count unknown" measured and stated in the PR; the refusal line is unchanged. Order: /mnt/project-files/server-seat2/standard-order.md + /mnt/project-files/server-seat2/31719-delta.md. Summons: the maintainer's 「队列中很多任务啊」 (project chat, 2026-11-08T21:26Z).

  5. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmSNEnhcDvnrhmAucKu
    os-dev-report

    {
      "issue": 31719,
      "status": "done",
      "branch": "claude/project-thread-mv5dxj",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/31750",
      "session": "session_01ADHkmSNEnhcDvnrhmAucKu",
      "premise_still_valid": true,
      "summary": "os serve now asks the multi-node gate about the topology declaresMultiNode reads (from the same lazily loaded service-cluster namespace), with no second parse of either spelling in serve.ts. A topology declared only as OS_EXPECT_MULTI_NODE=true is asked as 2, the smallest count the gate reads as declared multi-node; undeclared and counted declarations are asked exactly as before. The refusal line is byte-identical. A new integration-tier boot test pins the refusal (red on main, green after) plus three controls.",
      "tests": "New packages/cli/test/serve-multi-node-gate-declaration.integration.test.ts (4 real os serve boots). Green on ed640403: Tests 4 passed. Red on main (serve.ts from 35a9c694): Tests 2 failed | 2 passed — the EXPECT+no-gate refusal case (no '[cluster] multi-node not authorized (' line) and the admitting-gate control (gate asked with undefined, not 2); the single-node and OS_CLUSTER_REPLICAS=2 controls passed on main. Ablation A1 via ablation-replace.mjs ('? 2' -> '? 1 // ABLATION-A1', anchor 1->0, blob ecf1ee26f7d7 -> 457b8d187bd2): same 2 red; restored, blob == HEAD, git diff HEAD empty; no rebuild needed (runServe runs src through tsx). cli unit tier on ed640403: 407/407 files, 5775 tests passed. cli typecheck exit 0. Nightly fixtures (diagnosis + anchored-import e2e) 2 files/14 tests passed on 20b3fb9c.",
      "gates": "dispatch-gates --commands on ed640403: 72 families, all exit 0; --ran: '72 derived famil(ies) accounted for — 72 run, 0 NOT-MEASURED (a DERIVED zero)'. Same 72 green on 20b3fb9c before the main merge. eslint --no-inline-config on the 5 touched cli files: exit 0. CI check-runs: in_progress at report time (not waited on).",
      "dogfood_battery": "ed640403 · os-verify-lock VERDICT command-exit 0 · Test Files 440 passed | 3 skipped (443) · Tests 3652 passed | 41 skipped (3693) · no failures (also green on 20b3fb9c with the same counts)",
      "mcp_calls": "2 — mcp__github__issue_read (get, get_comments) on #31719; no write tools",
      "api_writes": "2 — pr_create via fleet-write relay (PR #31750); this os-dev-report comment via post-stamped.mjs on #31719",
      "files_changed": [
        "packages/cli/src/commands/serve.ts",
        "packages/cli/src/commands/serve-multi-node-cap-advisory.pin.test.ts",
        "packages/cli/test/serve-multi-node-gate-declaration.integration.test.ts",
        "packages/cli/test/serve-cluster-driver-diagnosis.e2e.test.ts",
        "packages/cli/test/serve-app-anchored-optional-import.e2e.test.ts",
        ".changeset/31719-multi-node-gate-expect-declaration.md"
      ],
      "line_budget": "serve.ts +36/-2 (gate call region only: cast type, destructure, __requestedNodes); 3 forced test edits named in the PR body",
      "deviations": [
        "Two e2e fixture files outside the declared surface needed a declaresMultiNode export (serve now calls it); named as forced edits.",
        "The new boot test is .integration.test.ts, not .e2e (e2e runs nightly only); it declares '// test-budget: 40' from a local 29.04 s reading, no CI reading yet.",
        "The boot test's driver is an app-local redis stand-in delegating to memory, so the red leg on main also ends at the split-brain guard; the gate's own refusal line is what separates main from the fix.",
        "First cli unit run had 2 prerequisite refusals (cli dist not built); green after building cli, and 407/407 on the final head."
      ],
      "open_questions": [
        {
          "question": "A cap-aware gate with cap 1 now gets allowMultiNode(2) for an uncounted OS_EXPECT_MULTI_NODE=true and returns capped; formatMultiNodeCapAdvisory then prints 'OS_CLUSTER_REPLICAS declares 2', a count the operator did not write. Keep, or reword the advisory for the uncounted case?",
          "options": [
            "A keep as is: no shipped gate is cap-aware today; reword when one ships",
            "B follow-up card: the advisory names the declaration spelling when OS_CLUSTER_REPLICAS is unset (outside the gate call region)"
          ],
          "recommendation": "A, because no gate in this repo or the EE distribution expresses a cap yet, and the advisory is outside this card's region"
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "class: b · reach: content/docs/kernel/cluster.mdx says a gate denial makes os serve downgrade 'rather than failing'; with a declared topology the split-brain guard refuses the boot (multi-node-gate.ts module doc says so) · evidence: cluster.mdx line ~670 vs multi-node-gate.ts '⛔ What a denial ACTUALLY does at boot' · dedupe: cluster.mdx downgrade rather than failing, multi-node gate denial docs, split-brain guard refuses boot"
      ]
    }

    Stamped 2026-11-09T02:23Z.


    Generated by Claude Code

  6. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    ACCEPT → PR #31750 comment 6731900433 · head 9a7ee97a · 2026-11-09T04:00Z · contract review PASS 6731888362 (CONTRACT_REVIEW_TIER, narrowing) · landing: pr_ready + automerge_enable

  7. objectstack-fleet commented on Nov 9, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#2 · os-steve · session_01ADHkmQKZzp4DLtqiCkZZRM

    落地 · 2026-11-09T04:22Z

    reading value
    PR #31750 MERGED 2026-11-09T04:21Z, merge commit 2b752aa4 (ACCEPT 6731900433; contract review at CONTRACT_REVIEW_TIER PASS, record 6731888362)
    #31719 closed by the merge (Fixes #31719)

    Landed:

    • serve.ts: when the environment declares multi-node but sets no OS_CLUSTER_REPLICAS, the replica count counts as unknown, and the gate is asked for 2 replicas instead of passing as single-node.
    • formatMultiNodeCapAdvisory(verdict, {countDeclared}) has a separate wording for the uncounted case.
    • cluster.mdx now describes this behaviour.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions