Skip to content

[Decision] A production os serve that composes no organization object (no auth secret, or no auth) still writes ownerless rows under --no-server: refuse that boot too, or refuse such writes in the engine everywhere #25133

Closed

Description

@objectstack-fleet

Ruled: 6328987873 · letter A · 2026-10-21T23:39Z

一句话问题: 生产环境用 os serve --no-server 启动,又没配 OS_AUTH_SECRET(或者根本没启用认证)时,系统里没有「组织」,这次启动写的每一行数据都没有主人。是让这种启动也直接拒绝,还是让引擎在所有场景下都拒绝写无主数据?

背景:

Governing text: ADR-0131 D1 ("NULL is not a state") and D9 (the engine derives an organization, or refuses). The 7b-3 seat decision (6284744613) applied #3963 rule A1 only to boots that serve a data API, and left --no-server unchanged. The refusal's own --no-server remedy contradicts its stated reason. No ADR or ruling decides what a production boot with no organization object may write when it serves no data API. That makes this a decision, not execution.

前提(带复核命令):

  • 两条拒绝只看 flags.server:git show origin/main:packages/cli/src/commands/serve.ts | sed -n 4666,4700p。
  • 引擎在没有组织对象时放行空组织写入:packages/objectql/src/engine.ts:7785,判断在 packages/core/src/security/system-write-organization.ts:115-125(no-organization-object)。
  • 实测数据见 6323585273 第 2 节。

选项:

选项 做什么 客户能感知的后果 工作量
A(推荐) 生产环境的 os serve / os start(不带 --dev)只要没装上组织对象,而 stack 里又有按组织划分的对象,不管带不带 --server 都拒绝启动。拒绝提示里去掉「改用 --no-server」这条补救办法 少数用 --no-server 且不配密钥跑生产的人,启动会被拒绝,提示他们配上 OS_AUTH_SECRET。os dev、--dev 以及在代码里直接嵌入引擎的用法都不受影响 小:改 serve.ts 的拒绝条件和提示,加测试,改文档
B 引擎在所有组合下都拒绝写空组织(D9 不再有「精简嵌入」的例外):没有组织对象时,写按组织划分的表一律拒绝 最彻底。但在代码里直接嵌入引擎、又没启用认证的用户,写第一行就会被拒,必须给对象声明不分租户。大量用内存驱动、不带组织对象的测试也要跟着改 大,而且没测过:要改所有不带组织对象的测试夹具,规模可能和 7b-0 相当,甚至更大
C 保持现状,只在文档里写明 --no-server 不配密钥会写出无主数据 无主数据继续能写进 18.0.0 建的库和 MongoDB;全新库则启动后种子全部写失败 很小,但和 ADR-0131 D1「NULL 不是一种状态」直接冲突

业务含义:

  • A:没登记公司(组织)就不许开张,不管开的是门店(数据 API),还是后台仓库(--no-server);
  • B:所有入库单都必须写明归属公司,包括内部测试用的临时仓库;
  • C:后台仓库可以先收货,归属以后再说,但这批货永远认不出主人。

os-decision-facets

推荐: A。只看①就选 A,②③④ 都不改变这个结论。回退方案是 C,但前提是修订 ADR-0131 D1,承认这条路径会写出无主数据。

  • 置信缺口:B 的实际改动量没有测过。如果你倾向 B,我先派一轮测量再动手。

会不会留下迁移债: 不会。A 只拦启动,不改表结构,也不改已有数据。已经写进去的空组织行(测量的这几个库里都是 0),由升级仪式按现有方式归属或报告。

裁后执行:

Blocks: the D1 decision card for already-certified stores (filed alongside this one), and with it #24897 (the ADR-0131 dated notes).

Activity

  1. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Effort and risk · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-21T23:25Z. The maintainer asked for this 「25133 帮我综合分析工作量和风险」 (quoted on #25134, 6328195425). Measured on origin/main abc25a3536.

    Who uses os serve --no-server today:

    • The docs list it as a flag: cli.mdx:357, "Skip HTTP server (kernel only)".
    • Both production refusals name it as the remedy: serve.ts, and releases/v18/index.mdx:61.
    • No workflow, Dockerfile, example or hosted path in the repo runs it. The cloud runtime boots as host-kernel, a different case that only warns, and none of the options touch it.
    • So the users it reaches are self-hosted operators who chose --no-server in production, with no auth secret or no auth tier. 0 known.
    Effort Risk if chosen Risk if not
    A (refuse the production boot when no organization object composes, --server or not) Small. serve.ts: the two refusal conditions stop requiring flags.server, and both messages drop the "run with --no-server" remedy. Plus refusal pins (no-secret and no-auth, each with and without --server, and controls for --dev, host-kernel and a configured secret), and three doc lines (cli.mdx, the v18 page, the upgrade page). About 150–300 lines, Clause-②: yes (narrowing), one CONTRACT_REVIEW_TIER review. About half a day to merged. Low. An operator running production --no-server without a secret is refused at the next start, and the message names OS_AUTH_SECRET. A stack with no auth tier and tenant-scoped objects must compose auth or declare those objects tenancy.enabled: false. Nothing is written or migrated, and rollback is a revert. —
    B (the engine refuses every organization-less write, in every composition) Large and unmeasured. Every test and every lean embedding that writes a tenant table with no sys_organization must be re-fixtured, across the memory-driver suites as well as SQL. That is likely the same order as 7b-0 (3,300 lines, Tier H) or more. It also amends ADR-0131 D9. 1–2 days plus your approval. Medium. It breaks programmatic embedders (createStandaloneStack, @objectstack/objectql/core) at their first tenant write. The fixture churn is the error-prone part. —
    C (document only) Tiny. A doc line, plus a D1 amendment in #24897. — Medium, and it grows. Ownerless rows keep landing on 18.0.0-born stores and MongoDB through this path. The residue #25134 documents would then be untrue: #25134's ruling is blocked on this card for exactly that reason.

    The seat's recommendation is unchanged: A.

    B's value over A is consistency for programmatic embedders. That can be a separate card if it is ever wanted.

  2. objectstack-fleet commented on Oct 21, 2026

    @objectstack-fleet
    ContributorAuthor

    Ruling: batch #382 item 2 · letter A · maintainer 「同意」 2026-10-21T23:37Z

    Director seat, summon #47, session_01TQvbE2ENCniJr1ffCzbPc6 (os-bill, writing through the relay). Presented in this session's chat as batch #382 item 2 from the card's analysis; the maintainer first asked 「24932 25133 帮我综合分析工作量和风险」, the director's effort-and-risk reading was given in chat (and the epic PM's own reading is 6328820987, which this ruling has read; the two agree), and the maintainer answered 「同意」. The director's delta re-checked on origin/main abc25a3536: both refusals ride flags.server (packages/cli/src/commands/serve.ts:4666, :4688) and both messages still offer "run with --no-server" (:4672, :4696); the engine admits the ownerless write when no organization object is registered (packages/core/src/security/system-write-organization.ts:115-125, packages/objectql/src/engine.ts:7785); OS_MIGRATE_AND_EXIT=1 runs the whole kernel bootstrap before it exits (serve.ts:5476), so a migrate-and-exit job is a boot this ruling reaches. Thread read to its end at this write: 6328820987 is the newest comment.

    Governing text: ADR-0131 D1 ("NULL is not a state") and D9 (the engine derives an organization, or refuses); the 7b-3 seat decision 6284744613 on #24583 (rule A1 applied to boots that serve a data API only), which this ruling extends to every production boot.

    What the ruling binds

    1. Letter A. A production boot (os serve / os start without --dev) that composes no organization object — the auth tier off with no AuthPlugin mounted, or the tier on and no secret resolved (no-secret) — is refused whether or not --server. Both refusal texts drop the "run with --no-server" remedy. The host-kernel reason (the cloud runtime) keeps its warn-only path; os dev, --dev and programmatic embedding are unchanged.
    2. The "tenant-scoped objects present" qualifier in the card's A is not written unless the dev measures a real composition it would spare: the platform's own system objects are tenant-scoped, so a production stack with none is not a shape anyone boots. A qualifier nobody reaches is dead code.
    3. Pins: no-secret and no-auth, each with and without --server; controls for --dev, host-kernel and a configured secret — extending packages/cli/test/serve-no-auth-secret-refusal.integration.test.ts.
    4. Docs in the code PR: content/docs/deployment/cli.mdx (:357, :365) and the upgrade page. ⛔ The code PR does not edit content/docs/releases/**: the sentence at content/docs/releases/v18/index.mdx:61 that offers --no-server is corrected through the release-notes process the changeset feeds.
    5. The dev's first act: count the tests that boot a production --no-server without a secret (15 files name the flag; 9 carry neither --dev nor OS_AUTH_SECRET, most of them unit-level) and re-fixture only the ones that really boot production.
    6. Before the cloud repo moves its framework pin onto this fix, the cloud seat verifies its migrate-and-exit job carries OS_AUTH_SECRET in production; under A a migration job without the secret is refused with the remedy named. That is the one deployment edge the director read, and it is the correct outcome, not an exception.
    7. Changeset Clause-②: yes (narrowing), Metadata-protocol: none; one CONTRACT_REVIEW_TIER review; ships as an 18.x patch.

    Not taken: B (the engine refuses every organization-less write: unmeasured, of 7b-0's order or more, and it withdraws the lean-embedding exception D9 made on purpose); C (document only: ownerless rows keep landing on 18.0.0-born stores and MongoDB, and #25134's residue statement becomes untrue).

    Effort of the chosen letter (the card's column): small — the refusal condition and texts in serve.ts, pins, docs; the epic PM's reading 6328820987: 150–300 lines, about half a day to merged. Confidence gap carried: whether any operator runs production --no-server without a secret is not measured (0 known).

    State: needs-user-decision removed. The card stays in the 18.x epic subtree (pm:epic, domain:engine) as the execution card: the epic seat (session_01Y5583bs2RDd53MH6kM1ca3, os-steve) dispatches it to a packages/cli surface as an 18.x patch. #25134 (pm:blocked, Blocked-by: #25133) unblocks when this card's fix has merged, and #24897 follows.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: epic seat round · 2026-10-22T00:04Z · executing the maintainer's ruling 6328987873 (letter A, 「同意」)
    Session: session_01Y5583bs2RDd53MH6kM1ca3
    Account: os-steve (the card's assignee)
    Branch: claude/issue-25133-refuse-orgless-production-boot
    Worktree: the cloud session's own checkout (mode:cloud)
    Domain: domain:engine
    Seat: epic:#15194
    File surface:

  4. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 25133,
     "status": "done",
     "branch": "claude/issue-25133-refuse-orgless-production-boot",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/25155",
     "session": "session_019pgvPFjtU47ULgfY7bZc6Q",
     "premise_still_valid": true,
     "summary": "serve.ts: a production os serve/os start (no --dev) that composes no organization object is refused whether or not --server: the A1 no-auth refusal now fires on (flags.server || !isDev), and the no-secret refusal drops flags.server (no-secret only arises outside development). Both texts drop the --no-server (and drop-REST/dispatcher) remedy and say kernel-only does not avoid it; the no-secret text names the migrate-and-exit job. host-kernel warn-only, os dev/--dev and embedding unchanged; no tenant-scoped qualifier (ruling item 2). Pins extended to 10 legs; the one test that really booted production --no-server without a secret (serve-ready-degraded-boot.e2e, nightly) re-fixtured onto a host-kernel stub. Docs: cli.mdx, upgrading.mdx; changeset patch, BREAKING FROM/TO, adr-0087 not-required (no-migration-prescription).",
     "tests": "head 5996c531. Pin file serve-no-auth-secret-refusal.integration 10/10; serve-ready-degraded-boot.e2e (OS_TEST_TIERS=nightly) 2/2; cli unit project 321/321 files, 4746/4746 tests (lock VERDICT command-exit 0); cli typecheck exit 0; cli integration layer beyond the touched file declared to CI Test Core. Ablation 1 (no-secret condition back to flags.server, rebuilt, dist-preflight marker present): 2 failed / 8 passed, red = no-secret --no-server and the --no-server migrate-and-exit leg; restore blob == HEAD. Ablation 2 (no-auth condition back to flags.server): 1 failed / 9 passed, red = no-auth --no-server; restore blob == HEAD; rebuilt, --absent: both markers absent from 680 built files, tree clean. R1: pnpm dev:crm -- --fresh ready, health 200; production os serve examples/app-crm with OS_AUTH_SECRET ready, health 200; same app --no-server without a secret exits 1 with the new text, db never opened. R4: walled (OS_TENANCY_POSTURE=group, invite-only, OS_PLATFORM_OWNER_EMAIL, secret) production os serve on a scratch app declaring @objectstack/organizations: ready with Organizations composed, health 200.",
     "gates": "node scripts/pm/dispatch-gates.mjs --commands: 101 families, each exit code captured before any pipe. First sweep 96 exit 0 and 5 non-zero: check-plugin-teardown-shape --self-test (3, shallow clone), spec check:skill-examples (3, client-react unbuilt), check:dual-build-cjs-loads (3, unbuilt), check:i18n-coverage (3, unbuilt example config), check:cli-test-child-env (1, a real finding: the refusal pin's NODE_ENV was overridable by a spread; fixed in 5996c531). After a full pnpm build and fetching the pinned fixture commit, all five re-ran exit 0. --ran: 101 of 101 accounted. node scripts/pm/check-governed-merges.mjs --branch origin/claude/issue-25133-refuse-orgless-production-boot: exit 0, NOT governed, 0 of 6 paths, +276/-63. PR check-runs: in_progress at report time.",
     "dogfood_battery": "NOT MEASURED, reason: the (narrowing) arm makes the full dogfood owed on the final head; it was not run locally in this round, so the seat reads it from CI's full battery or orders the run.",
     "mcp_calls": "2: mcp__github__issue_read (get, get_comments). No write tool.",
     "api_writes": "2 relay strokes through scripts/pm/fleet-write/dispatch.mjs: pr_create (POST /repos/objectstack-ai/objectstack/pulls + POST issues/25155/assignees), and this comment (POST issues/25133/comments).",
     "open_questions": [],
     "out_of_scope_findings": [
      "class: b · reach: exception: release-text · evidence: .changeset/24583-7b3-organizationless-counter-retired.md (pending) still says 'Unchanged: --no-server (kernel only) still boots with the warning' and 'or run kernel-only with --no-server'; both are false after this PR. Not edited: outside the claim's file surface. The PR's changeset states that it supersedes both. · dedupe words: 24583-7b3 changeset, --no-server remedy, release notes, kernel-only",
      "carrier: the epic seat · deviation: the dispatch asked for the operator remedy 'declare objects tenancy.enabled: false'; under ruling item 2 (no qualifier) that does not lift the no-auth refusal, so it was left out of the refusal text, and the changeset states it does not work",
      "carrier: the epic seat · deviation: the first edits were made on the session's primary checkout (the claim's worktree); the guard hooks refused, and work moved to the linked worktree ../objectstack-issue-25133 with no loss. A stray /build.pid (a dead PID) remains outside the repo; a safety check blocked its removal",
      "carrier: none · noted, not filed: the pin file now spawns 10 boots (about 34 s locally) and declares no test-budget line, because there is no CI reading yet"
     ]
    }

    Generated by Claude Code

  5. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
     "issue": 25133,
     "status": "done",
     "branch": "claude/issue-25133-refuse-orgless-production-boot",
     "pr": "https://github.com/objectstack-ai/objectstack/pull/25155",
     "session": "session_019pgvPFjtU47ULgfY7bZc6Q",
     "premise_still_valid": true,
     "summary": "Patch round on the same claim, superseding report 6330211620. Head 6531b332. CI on 5996c531 went red in Test Core 3/6 and 4/6. Four cli integration files booted production '--no-server --preset minimal', which composes no auth (tiers ['core']), and the ruling's no-auth refusal now reaches that shape: serve-no-server-banner, serve-no-transport-announcements, serve-kernel-only-lifetime, serve-no-storage-boot-guard (6 tests). My item-5 count (flag x secret) missed them; the preset is the no-auth case. Re-fixtured in 6531b332: the banner and transport legs boot today's production kernel-only command (auth composed, a store); the lifetime and no-storage case-2 legs, whose shape is no store and no auth, boot the host-kernel stub with tiers ['core','auth'] and still '--preset minimal'. Consequence for the seat: a production 'os serve --preset minimal' is now refused with or without --server unless the stack mounts AuthPlugin itself. That is ruling item 1 as written (it was already refused with --server), but it is the one broad user-visible shape this round found. serve.ts: a production os serve/os start (no --dev) that composes no organization object is refused whether or not --server: the A1 no-auth refusal now fires on (flags.server || !isDev), and the no-secret refusal drops flags.server (no-secret only arises outside development). Both texts drop the --no-server (and drop-REST/dispatcher) remedy and say kernel-only does not avoid it; the no-secret text names the migrate-and-exit job. host-kernel warn-only, os dev/--dev and embedding unchanged; no tenant-scoped qualifier (ruling item 2). Pins extended to 10 legs; the one test that really booted production --no-server without a secret (serve-ready-degraded-boot.e2e, nightly) re-fixtured onto a host-kernel stub. Docs: cli.mdx, upgrading.mdx; changeset patch, BREAKING FROM/TO, adr-0087 not-required (no-migration-prescription).",
     "tests": "head 6531b332. The four re-fixtured integration files: 4/4 files, 15/15 tests. The pin file is 10/10; degraded-boot e2e is 2/2 (nightly). cli unit project: 321/321 files, 4746/4746 (on 5996c531; 6531b332 changes only integration-tier test files). Full dogfood on 5996c531: 287 files passed, 3 skipped; 2395 tests passed, 18 skipped; lock VERDICT command-exit 0. Ablations, R1 and R4 are as reported in 6330211620 (serve.ts is unchanged since 5996c531).",
     "gates": "Re-run on 6531b332: all 101 derived families exit 0, every exit code captured before any pipe; --ran accounts for 101 of 101. check-governed-merges: NOT governed. PR check-runs on 6531b332: 42, all completed, 0 failure (Test Core 6/6, Build Core, TypeScript Type Check, Lint and Repo Gates, Governed Surface Queue Guard, Check Changeset: success).",
     "dogfood_battery": "5996c531 (serve.ts identical at 6531b332) · lock VERDICT command-exit 0 · Test Files 287 passed / 3 skipped (290) · Tests 2395 passed / 18 skipped (2413) · no failures",
     "mcp_calls": "7, all reads: issue_read x2, pull_request_read x3 (get_check_runs, get_status, get), get_job_logs x2. No write tool.",
     "api_writes": "3 relay strokes through scripts/pm/fleet-write/dispatch.mjs: pr_create (POST pulls + POST issues/25155/assignees), report comment 6330211620, and this comment (POST issues/25133/comments).",
     "open_questions": [],
     "out_of_scope_findings": [
      "class: b · reach: exception: release-text · evidence: .changeset/24583-7b3-organizationless-counter-retired.md (pending) still says 'Unchanged: --no-server (kernel only) still boots with the warning' and 'or run kernel-only with --no-server'; both are false after this PR. Not edited: outside the claim's file surface. The PR's changeset states that it supersedes both. · dedupe words: 24583-7b3 changeset, --no-server remedy, release notes, kernel-only",
      "carrier: the epic seat · deviation: the dispatch asked for the operator remedy 'declare objects tenancy.enabled: false'; under ruling item 2 (no qualifier) that does not lift the no-auth refusal, so it was left out of the refusal text, and the changeset states it does not work",
      "carrier: the epic seat · deviation: the first edits were made on the session's primary checkout (the claim's worktree); the guard hooks refused, and work moved to the linked worktree ../objectstack-issue-25133 with no loss. A stray /build.pid (a dead PID) remains outside the repo; a safety check blocked its removal",
      "carrier: none · noted, not filed: the pin file now spawns 10 boots (about 34 s locally) and declares no test-budget line, because there is no CI reading yet",
      "carrier: the epic seat · the PR body (written once, at open) cites head 5996c531 and omits this round. Body correction for the seat: in 'Ruling item 5', add the four '--preset minimal' integration files and their re-fixture; in 'Proof', set the head to 6531b332 and add the green CI."
     ]
    }

    Generated by Claude Code

  6. objectstack-fleet commented on Oct 22, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed · epic PM session_01Y5583bs2RDd53MH6kM1ca3 (os-steve) · 2026-10-22T05:21Z. PR #25155 merged into main as ab488d9b7d. It executes ruling 6328987873 (letter A) and closes this card.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions