Skip to content

fix(plugin-auth): a fresh os dev database opens on the shell — the dev seed gives the admin's workspace the host's time zone (#23175) - #23237

Merged
objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23175-dev-seed-workspace-timezone
Oct 13, 2026
Merged

objectstack-fleet[bot] merged 6 commits into
mainfrom
claude/issue-23175-dev-seed-workspace-timezone

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #23175
Clause-②: no — the dev seed completes the workspace it creates with the host's zone; no published schema, option, flag or export moves, and the prompt's own rule is unchanged.

What changes

On the boot that seeds the dev admin (NODE_ENV=development, OS_SEED_ADMIN on by default), plugin-auth's seed now writes the host's resolved zone to localization.timezone for each organization the seeded admin is a member of. A fresh os dev database then opens on the console shell, not on the "Set the workspace timezone" dialog.

  • Value: Intl.DateTimeFormat().resolvedOptions().timeZone, so TZ chooses it. No new CLI flag, and no new env var.
  • Door: the settings service's own set('localization', 'timezone', zone, { tenantId: orgId }). This is the same per-organization tenant row that the dialog's confirm writes through PUT /api/settings/localization. The key's iana_time_zone domain is judged there, and the write is audited there. It is not a raw row write.
  • Never over a set zone: the seed writes only when the key still resolves with source: 'default'. A value on the organization, global or env rung is left untouched.
  • Dialog not suppressed: a host that reports no zone, or a zone the door refuses, gets nothing written. The workspace keeps the default and the dialog still asks.
  • Only the seeding boot writes. Boots after it leave the database as they find it.

File surface: the seed path in packages/plugins/plugin-auth/src/auth-plugin.ts, one new test file, and one changeset. The import-deps region (about :2984), identity-objects-plugin.ts and default-organization-invariant.ts are untouched.

Mechanism hypotheses, measured

  • H1, partly falsified. The seed sits in auth-plugin.ts and dev.ts only passes env vars, as stated. But under the default single posture the seed does not create the workspace. AuthPlugin.start() creates the Default Organization at boot (ADR-0131 D3). The seed's signUpEmail then triggers the default-organization bootstrap, which binds the new admin as its owner inside the same call chain. So the admin's sys_member row exists by the time the seed's next step runs. Pinned by case 1 below and seen live.
  • H2, corrected. The zone the dialog reads is the settings key localization.timezone, scope tenant, one sys_setting row per organization. It is not the sys_organization.timezone column at sys-organization.object.ts about :284. That column's own comment says it is distinct from the settings door's key and is not read by it. The seed writes only the settings key.
  • H3, confirmed at the pin .objectui-sha 1fb1ced362. findTimezoneOffer in workspaceTimezonePromptLogic.ts reads GET /api/settings/localization and offers the dialog only when values.timezone.source === 'default', the key is not locked, and the session holds the manifest's writePermission (setup.write). The request goes to the session's active organization (the dialog needs activeOrganization.id). Every pin below asserts source for exactly that organization.
  • H4, decision: write UTC too. UTC is the host's resolved zone, and the console's creation path (console: send the creator's browser timezone when a workspace is created, so a new workspace never shows the timezone prompt (objectui half of cloud#2676) objectui#11908) sends whatever the browser reports, UTC included. It leaves the key out only when the browser reports nothing. For os dev, the browser opening the console usually runs on the same host, so the dialog would pre-fill that same zone. "No zone set" is the case where Intl reports none, or reports one the door refuses (ICU's Etc/Unknown). Nothing is written then, and the dialog asks, as the ruling requires.
  • H5. os dev sets neither OS_TENANCY_POSTURE nor OS_MULTI_ORG_ENABLED, so it boots single (the boot banner reads Tenancy: single). The write needs the organization id. A walled posture refuses an organization-less tenant write, and the dialog reads the active organization's row. The seed reads the admin's memberships rather than guessing an organization. Walled postures were not booted here: NOT MEASURED. An admin with no membership gets nothing written.

Pins: src/dev-admin-seed-workspace-timezone.test.ts

These run on a real ObjectKernel, ObjectQL on in-memory SQLite, the real AuthPlugin, SecurityPlugin and SettingsServicePlugin, and NODE_ENV=development. The host zone is set with TZ, and each case first asserts that the process really resolves it.

  1. Fresh dev database, host Asia/Shanghai: the seeded admin is in exactly the Default Organization, which resolves { value: 'Asia/Shanghai', source: 'tenant' }. SettingsService.set was called with ('localization', 'timezone', 'Asia/Shanghai', { tenantId }).
  2. A workspace with no zone still gets the dialog: the same boot with OS_SEED_ADMIN=0 resolves { value: 'UTC', source: 'default' }.
  3. A host that reports no zone (an unknown TZ, where Intl answers undefined): the workspace stays source: 'default', with no localization write.
  4. A zone the door refuses (Etc/Unknown): the workspace stays source: 'default'.
  5. Never over a set zone: a tenant row stored before the seed (Europe/Paris) is kept, with no localization write.

Negative-pin ablation. The fix was committed first. The call if (seededId) await this.seedDevWorkspaceTimezone(ctx, ql, seededId); was removed with scripts/ablation-replace.mjs (anchor x1 to x0, blob 11ebf1845458 to 2568e9af9cfc), with a trap restore armed. Case 1 went red and the four controls stayed green (Tests 1 failed | 4 passed (5)):

AssertionError: expected { value: 'UTC', …(4) } to match object { value: 'Asia/Shanghai', …(1) }
- Expected
+ Received
  {
-   "source": "tenant",
-   "value": "Asia/Shanghai",
+   "source": "default",
+   "value": "UTC",
  }
 ❯ src/dev-admin-seed-workspace-timezone.test.ts:206:36

Restore: blob after restore 11ebf1845458 equals the HEAD blob, and git diff HEAD is empty. The subject is imported from source, so this ablation needed no build.

Live boot: os dev --fresh on examples/app-crm

These are sign-in readings as admin@objectos.ai, read off the same route the dialog reads:

build host zone session activeOrganizationId GET /api/settings/localization → timezone
this branch TZ=Asia/Shanghai org_mv6hnzlwfvausmzc { value: 'Asia/Shanghai', source: 'tenant' }, so no dialog
this branch container (UTC) org_mv6hologvdf199rd { value: 'UTC', source: 'tenant' }, so no dialog
call removed (dist ablation) TZ=Asia/Shanghai org_mv6hpy140467la3x { value: 'UTC', source: 'default' }, so the dialog opens

How the dist ablation was checked:

  • ablation-dist-preflight --absent proved the call was gone from all 10 built files.
  • The ablated build's DTS step refused the now-unused private method (TS6133). The JS that os dev runs was emitted, and the preflight read that JS.
  • After the restore, the rebuild was green (check-dts-emitted 2/2). The preflight then found the call back in dist/index.js and dist/index.mjs, and found the working tree clean.

Tests and gates (head 68696c376b)

  • pnpm --filter @objectstack/plugin-auth typecheck: exit 0. It covers tsc for src and examples, plus check:test-typecheck over tsconfig.test.json. The new test file adds no debt.
  • pnpm --filter @objectstack/plugin-auth exec vitest run --maxWorkers=2: Test Files 137 passed (137), Tests 2785 passed | 10 skipped. That run was at 9db639634d. The only later commit is the comment in seedDevWorkspaceTimezone's doc block. On 68696c376b the three seed files were re-run: Tests 22 passed (22).
  • Gates: node scripts/pm/dispatch-gates.mjs --commands was derived after the diff with no paths: 68 families. Reconciled with --ran, each line carrying its exit code: 68 run, 68 exit 0, 0 NOT MEASURED, at 68696c376b.
    • On the first pass, at 9db639634d, check:dual-build-cjs-loads exited 3 (PREREQUISITE NOT MET: 9 packages had no dist/).
    • A gate later in that list built those 9 packages, and the re-run on the final head measured 108 require entry points loading across 67 packages.
  • Lint, narrowed and proven:
    • eslint --no-inline-config --format json on the two changed .ts files reports 2 files, 0 errors and 0 warnings. Neither file is ignored.
    • eslint.config.mjs never enables type-aware linting (no parserOptions.project, about :327). So this diff cannot move any other file's verdict. The full pnpm lint is CI's.

Acceptance notes

  • In-process test boots now get the host zone too (measured). @objectstack/verify's harness forces NODE_ENV=development to get the seeded admin. A bootStack(crm) under TZ=Asia/Shanghai now reads { value: 'Asia/Shanghai', source: 'tenant' } for that admin.
    • On a UTC host (CI), the resolved value is unchanged.
    • On a non-UTC host, harness boots now compute business dates in the host's zone, where they used UTC before.
    • Under TZ=Asia/Shanghai, analytics-rls.dogfood and analytics-timezone.dogfood stayed green (8 tests).
    • temporal-storage-e2e.dogfood is NOT MEASURED: its showcase boot could not resolve the unbuilt @objectstack/connector-slack here.
    • The changeset tells suites how to opt out (TZ=UTC, or a PUT /api/settings/localization). Whether the harness should pin its own zone is raised with the PM, not decided here.
  • Remote dev. When the browser runs in a different zone from the os dev host, the host's zone wins and the dialog does not ask. Setup → Localization, or TZ before os dev, changes it.
  • The console precedent this mirrors sends the creator's zone on a workspace create only. The single-posture first-run wizard renames the bootstrapped organization and sends no zone, so a non-dev first owner still gets the dialog. That is ruled behaviour, and it is untouched here.

Generated by Claude Code

… time zone

WIP: implementation; tests follow.

Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE
Co-authored-by: Claude <noreply@anthropic.com>
… dialog's own condition

Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE
Co-authored-by: Claude <noreply@anthropic.com>
… it was measured under

Claude-Session: https://claude.ai/code/session_01FzQy26KznSa5j2gnf3AzsE
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Oct 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/plugin-auth, touching 4 documentable anchor(s).

8 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/deployment/cli.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/deployment/self-hosting.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/deployment/tenancy-modes.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/kernel/services-checklist.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/permissions/authentication.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/permissions/record-view-auditing.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/plugins/index.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/plugins/packages.mdx (via AuthPlugin (symbol, a top-level class))

⛔ 4 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17/17-3.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-5.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-6.mdx (via AuthPlugin (symbol, a top-level class))
  • content/docs/releases/v17/17-7.mdx (via /api/settings/localization (route, a path literal in a comment in AuthPlugin))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0a2d326dc4e10a94575f474e9144da676d768ce8 — the merge of head 68696c376b842b7f58d065eb9f75ae0ba4d9623f into base 01a3421f42b15801dd6289f9a5fdf22fa469b8f2, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0a2d326dc4e10a94575f474e9144da676d768ce8 && git checkout 0a2d326dc4e10a94575f474e9144da676d768ce8
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 68696c376b842b7f58d065eb9f75ae0ba4d9623f && git checkout -B drift-repro 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 && git merge --no-ff 68696c376b842b7f58d065eb9f75ae0ba4d9623f

node scripts/docs-audit/affected-docs.mjs --json 01a3421f42b15801dd6289f9a5fdf22fa469b8f2

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 01a3421f42b15801dd6289f9a5fdf22fa469b8f2 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 13, 2026 10:58
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 13, 2026 10:58
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 13, 2026
Merged via the queue into main with commit 6f4c518 Oct 13, 2026
41 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-23175-dev-seed-workspace-timezone branch October 13, 2026 11:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

2 participants