Skip to content

fix(app-shell): refuse a provision payload whose data carries no environment row - #6821

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-6707-provision-envelope-shape
Aug 30, 2026
Merged

fix(app-shell): refuse a provision payload whose data carries no environment row#6821
os-sam merged 1 commit into
mainfrom
claude/issue-6707-provision-envelope-shape

Conversation

@claude

@claudeclaudeBot commented Aug 29, 2026

Copy link
Copy Markdown
Contributor

Fixes#6707

Implements the ruled option B (2026-08-29, maintainer, batch #8): provisionProductionEnvironment refuses a 2xx whose data carries no environment row, instead of resolving best-effort.

The hard precondition, and how it was met

The ruling made this conditional: "the wire shape must be CONFIRMED against the cloud producer before the throw condition is written" — because the only in-repo artifact that ever pinned this payload before #6629 was a hand-written mock pinning the bug shape, and a tightening written against a wrong belief is worse than the leniency it replaces.

This lane cannot read objectstack-ai/cloud. It did not infer the shape from this consumer. The confirmation comes from three producer-side sources in objectstack-ai/objectstack, all naming the same handler:

  1. A transcribed cloud-side reading, landed in merged code.packages/client/src/index.ts (environments.create) on origin/main records, in a code comment: "both measured against the cloud repo's main on 2026-08-28 (the handler is packages/service-cloud/src/routes/environment-lifecycle.ts, POST /cloud/environments, which builds its body key by key)" — and states "the single-row key is environment. This route has NEVER emitted a project key", plus "The keys the route DOES send beside environment (warnings, durationMs, and a conditional hostnameAssignment)". That declaration is unwrapResponse of { environment }, changed away fromproject by objectstack#12866 precisely because the route never sent project.
  2. The published protocol contract.@objectstack/spec, packages/spec/src/cloud/environment.zod.tsProvisionEnvironmentResponseSchema declares environmentrequired (not optional).
  3. The verbatim handler body, quoted in objectstack#12883: ok({ environment: result.environment, warnings: result.warnings, durationMs: result.durationMs, ...(result.hostnameAssignment ? { hostnameAssignment: result.hostnameAssignment } : {}) }).

⭐ And the maintainer independently ruled on that shape ten minutes later: objectstack#12883, batch #11, adopts the four-key inline wire shape (environment + warnings + durationMs + optional hostnameAssignment) as what the route sends. Triage had proposed resolving both cards from one reading; that is what happened.

⚠️One known divergence, deliberately not resolved here. The spec schema also declares credentialrequired, and the handler quote does not send it (recorded on objectstack#12883 by the domain:cli lane, 2026-08-29). That divergence is real and unresolved, but it concerns credential only — every source agrees environment is present and unconditional, which is the single fact this throw condition rests on. Nothing here reads credential.

The change

The envelope check catches a missingdata and says nothing about data's shape. So after #6629 a producer that regressed to a flat payload would once again resolve successfully with id and hostname both undefined — the same silent outcome #6629 had just fixed, reachable again by a producer change alone. A flat payload is a producer contract violation, not a second dialect; tolerating it is how the original defect stayed invisible.

The refusal carries its own diagnostic, distinct from the missing-envelope one, so a logged warning still separates "the control plane did not wrap the payload" from "it did not put the row where it says it does". The guard also rejects a non-object environment (a string there produces the identical silent outcome), mirroring the envelope check one line above.

Blast radius, re-measured in this PR rather than inherited

  • Sole caller: CreateWorkspaceDialog.tsx:151. Already inside try { … } catch (provisionErr) { console.warn(…) }. The inner catch does not re-throw, so onCreated?.(org) still fires — workspace creation is unaffected; only the eager provision degrades to the lazy onboarding gate.
  • Return value has no consumer: the call site discards the result (await provisionProductionEnvironment(…)), which independently confirms the ruling's reason for leaving option C not-taken.
  • The two other test files referencing this function (CreateWorkspaceDialog.test.tsx, org-i18n-holdouts-4474.test.tsx) both vi.mock the module, so neither is affected.
  • Hot-file check: this change lands in packages/app-shell/src/console/organizations/not in hooks/ or layout/, so it does not overlap the unmerged PR fix(app-shell): one reading for sys_activity.type, and the unrecognised case stops claiming update #6814.

The anti-alias pin, updated in the same PR as ruled

does not fall back to a flat data shape… became rejects a flat data shape instead of falling back to it. The new assertion is strictly stronger than the one it replaces: a reintroduced data.environment ?? data alias would resolve { id: 'flat-1' } there and fail it either way. A second pin covers the producer-regression case (envelope intact, environment row absent) and asserts the diagnostic is not the envelope one, so the two conditions cannot be collapsed into one message.

Verification — all readings on 2e21f69d3

  • pnpm exec vitest run packages/app-shell/src/console/organizations/Test Files 12 passed (12), Tests 103 passed (103).
  • Reverse verification (ablation). The implementation alone was reverted to origin/main (tests kept), confirmed on disk by blob hash — on-disk 25a3c615… equals the origin/main blob and differs from the HEAD blob, and the throw string went to 0 occurrences. Result: Tests 2 failed | 6 passed (8) — exactly the two new/updated pins go red, the other six stay green, so neither pin is vacuous. Restore proven the same way: on-disk hash back to e0b61b1e…, git diff HEAD empty, throw string back to 1. No rebuild leg was needed — the test imports ../provisionEnvironment by relative path, so it resolves to source, never through a package exports field into dist/.
  • pnpm --filter @object-ui/app-shell run type-check → exit 0. It runs two configs, and both edited files are genuinely covered: --listFiles shows the base tsconfig.json excludes test files (0 hits) but tsconfig.test.json includes the edited test file (1 hit), so this is a measurement, not a green that skipped the new code.
  • pnpm --filter @object-ui/app-shell run lint0 errors, 2789 warnings, exit 0. (A stricter exploratory run with --no-inline-config surfaced 15 errors; all 15 are in files this PR never touches and are suppressed by inline disables the real gate honours. Both edited files are 0 errors / 0 warnings under the stricter run too.)
  • Gates implicated by the changed paths: check:control-bytes, check:vi-mock-specifiers, check:spec-symbols, check-changeset-presence.mjs, check-changeset-no-major.mjs — all exit 0. Plus a control-character self-scan of the three changed files: clean.
  • Declared narrowing: lint was run on the touched package rather than the whole repo. eslint itself chose the population (1016 files in packages/app-shell, counted from --format json, not estimated), and eslint.config.js declares no projectService and no parserOptions.project — type-aware linting is off, so each file's verdict is a function of that file plus the shared config. This diff changes neither the config nor any other file, so no untouched file's verdict can move. CI runs the full farm regardless.

Generated by Claude Code


Generated by Claude Code

…nvironment` row
`provisionProductionEnvironment`'s envelope check catches a MISSING `data` and
says nothing about `data`'s shape, so a producer that regressed to a flat
payload would once again resolve successfully with `id` and `hostname` both
`undefined` — the same silent outcome objectui#6629 fixed, reachable again by a
producer change alone.
A flat payload is a producer contract violation, not a second dialect, so it is
now refused. That routes a producer regression to this call's already documented
failure path: the sole caller wraps it in try/catch, logs a warning, and the
onboarding gate re-provisions lazily on first navigation. The refusal carries
its own diagnostic, distinct from the missing-envelope one.
The anti-alias pin updates in step, from "resolves with nothing" to "rejects" —
a strictly stronger assertion, since a reintroduced `data.environment ?? data`
alias resolves there and fails it either way.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRJge11jso9TpXRWFt1Z49
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

MetricValueBudget
Eager closure (gzip, 45 chunks)3174.0 KB3222.7 KB
Main entry chunk (gzip)148.2 KB350 KB
Entry fileindex-rlv8Wx7C.js
StatusPASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

PackageSizeGzipped
app-shell (consoleActionDispatch.js)0.20KB0.19KB
app-shell (index.js)11.89KB4.50KB
app-shell (runtime-config.js)20.61KB7.35KB
app-shell (types.js)0.01KB0.04KB
app-shell (urlParams.js)10.06KB3.86KB
auth (ActiveOrganizationStorage.js)25.05KB9.16KB
auth (AuthContext.js)0.31KB0.24KB
auth (AuthGuard.js)2.07KB1.00KB
auth (AuthProvider.js)40.18KB10.59KB
auth (AuthShell.js)3.49KB1.40KB
auth (ForgotPasswordForm.js)12.21KB3.45KB
auth (LoginForm.js)18.15KB5.39KB
auth (PreviewBanner.js)0.90KB0.50KB
auth (RegisterForm.js)6.65KB2.22KB
auth (SocialSignInButtons.js)9.61KB3.89KB
auth (UserMenu.js)3.41KB1.23KB
auth (auth-gate-events.js)1.29KB0.66KB
auth (authStyles.js)5.04KB1.72KB
auth (createAuthClient.js)40.21KB10.80KB
auth (createAuthenticatedFetch.js)8.46KB3.43KB
auth (index.js)3.19KB1.44KB
auth (invitation-status.js)1.22KB0.70KB
auth (org-roles.js)6.66KB2.78KB
auth (phone-identifier.js)1.11KB0.66KB
auth (types.js)0.59KB0.35KB
auth (useAuth.js)5.30KB1.02KB
auth (useWorkspaceAdminStatus.js)5.13KB2.35KB
collaboration (CommentThread.js)26.08KB7.56KB
collaboration (LiveCursors.js)3.17KB1.27KB
collaboration (PresenceAvatars.js)6.49KB2.64KB
collaboration (PresenceProvider.js)2.79KB1.13KB
collaboration (index.js)1.68KB0.73KB
collaboration (useCollaborationTranslation.js)6.05KB2.52KB
collaboration (useCommentSearch.js)1.98KB0.88KB
collaboration (useConflictResolution.js)7.75KB1.86KB
collaboration (useMentionNotifications.js)1.81KB0.68KB
collaboration (usePresence.js)6.33KB1.84KB
collaboration (useRealtimeSubscription.js)7.91KB2.01KB
components (index.js)511.50KB116.32KB
core (index.js)5.30KB2.13KB
create-plugin (index.js)10.08KB3.26KB
data-objectstack (index.js)173.10KB47.96KB
fields (index.js)240.93KB60.76KB
i18n (LocalizationContext.js)1.76KB0.96KB
i18n (currency.js)1.22KB0.64KB
i18n (fallbackInterpolation.js)6.25KB2.77KB
i18n (i18n.js)4.28KB1.75KB
i18n (index.js)3.44KB1.39KB
i18n (pickLocalized.js)7.62KB3.26KB
i18n (provider.js)26.89KB9.04KB
i18n (useDisplayLocale.js)2.85KB1.45KB
i18n (useObjectLabel.js)33.40KB8.71KB
i18n (useSafeTranslation.js)5.60KB2.33KB
layout (index.js)38.95KB10.97KB
mobile (MobileProvider.js)0.92KB0.49KB
mobile (ResponsiveContainer.js)0.94KB0.38KB
mobile (breakpoints.js)1.51KB0.70KB
mobile (createOfflineDataSource.js)5.61KB1.75KB
mobile (index.js)1.55KB0.62KB
mobile (offlineQueue.js)3.91KB1.35KB
mobile (pwa.js)0.97KB0.49KB
mobile (serviceWorker.js)1.48KB0.62KB
mobile (serviceWorkerSource.js)3.41KB1.48KB
mobile (useBreakpoint.js)1.54KB0.65KB
mobile (useGesture.js)6.96KB1.98KB
mobile (useOfflineSync.js)1.99KB0.72KB
mobile (usePullToRefresh.js)2.53KB0.85KB
mobile (useResponsive.js)0.72KB0.42KB
mobile (useResponsiveConfig.js)1.37KB0.63KB
mobile (useSpecGesture.js)4.32KB1.64KB
mobile (useTouchTarget.js)1.01KB0.54KB
permissions (MePermissionsProvider.js)9.53KB3.38KB
permissions (PermissionContext.js)0.31KB0.25KB
permissions (PermissionGuard.js)0.89KB0.45KB
permissions (PermissionProvider.js)4.64KB1.50KB
permissions (evaluator.js)5.12KB1.74KB
permissions (index.js)0.93KB0.41KB
permissions (store.js)0.91KB0.42KB
permissions (useFieldPermissions.js)1.28KB0.53KB
permissions (usePermissions.js)1.93KB0.88KB
plugin-ai (index.js)15.75KB3.80KB
plugin-calendar (index.js)46.92KB12.93KB
plugin-charts (index.js)64.68KB18.35KB
plugin-chatbot (index.js)190.33KB45.10KB
plugin-dashboard (index.js)133.48KB34.51KB
plugin-designer (index.js)212.87KB43.19KB
plugin-detail (index.js)245.46KB62.46KB
plugin-editor (index.js)2.46KB1.10KB
plugin-form (index.js)133.03KB32.64KB
plugin-gantt (index.js)165.23KB40.37KB
plugin-grid (index.js)201.57KB54.55KB
plugin-kanban (index.js)53.14KB14.64KB
plugin-list (index.js)113.15KB27.59KB
plugin-map (index.js)20.20KB6.66KB
plugin-markdown (index.js)13.72KB4.69KB
plugin-report (index.js)43.51KB11.94KB
plugin-timeline (index.js)28.95KB8.33KB
plugin-tree (index.js)9.00KB3.08KB
plugin-view (index.js)85.87KB21.12KB
providers (DataSourceProvider.js)0.75KB0.39KB
providers (MetadataProvider.js)1.37KB0.59KB
providers (ThemeProvider.js)1.90KB0.85KB
providers (UploadProvider.js)11.66KB3.50KB
providers (index.js)0.45KB0.23KB
providers (types.js)0.01KB0.04KB
react-runtime (index.js)5.62KB2.34KB
react (LazyPluginLoader.js)4.47KB1.63KB
react (SchemaRenderer.js)76.75KB25.49KB
react (data-invalidation.js)5.05KB2.08KB
react (index.js)3.11KB1.48KB
react (schema-input.js)2.32KB1.24KB
react (spec-input.js)0.20KB0.18KB
sdui-parser (codegen.js)5.41KB2.34KB
sdui-parser (dashboard-widget-options.js)3.08KB1.30KB
sdui-parser (index.js)4.93KB2.24KB
sdui-parser (input-type.js)2.84KB1.40KB
sdui-parser (parse.js)20.57KB5.88KB
sdui-parser (provenance.js)3.66KB1.82KB
sdui-parser (types.js)0.28KB0.23KB
sdui-parser (validate.js)10.35KB3.60KB
types (ai.js)0.20KB0.17KB
types (api-types.js)0.20KB0.18KB
types (app.js)2.87KB0.99KB
types (base.js)0.20KB0.18KB
types (blocks.js)0.20KB0.18KB
types (complex.js)2.74KB1.41KB
types (crud.js)0.20KB0.18KB
types (dashboard-filter-alias.js)6.23KB2.74KB
types (data-display.js)3.75KB1.85KB
types (data-protocol.js)0.20KB0.19KB
types (data.js)0.20KB0.18KB
types (designer.js)1.85KB0.85KB
types (disclosure.js)0.20KB0.18KB
types (error-code.js)1.54KB0.88KB
types (feedback.js)0.20KB0.18KB
types (field-types.js)0.20KB0.18KB
types (form.js)0.20KB0.18KB
types (http-inflight.js)8.87KB3.73KB
types (http-retry.js)4.32KB2.02KB
types (icon-key-migration.js)4.26KB1.63KB
types (index.js)4.72KB2.24KB
types (layout.js)0.20KB0.18KB
types (managed-by.js)0.19KB0.18KB
types (mobile.js)2.59KB1.31KB
types (navigation.js)0.20KB0.18KB
types (objectql.js)0.20KB0.18KB
types (overlay.js)0.20KB0.18KB
types (permissions.js)0.20KB0.18KB
types (plugin-scope.js)0.20KB0.18KB
types (record-components.js)0.20KB0.19KB
types (record-semantics.js)1.28KB0.67KB
types (registry.js)0.20KB0.18KB
types (reports.js)0.20KB0.18KB
types (spec-report.js)5.05KB1.93KB
types (spec-ui-namespace.js)0.20KB0.19KB
types (system-fields.js)3.33KB1.54KB
types (theme.js)6.28KB2.87KB
types (ui-action.js)3.40KB1.71KB
types (views.js)0.20KB0.18KB
types (widget.js)0.20KB0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@os-sales
os-sales marked this pull request as ready for review August 29, 2026 22:15
@os-sam
os-sam added this pull request to the merge queueAug 30, 2026
Merged via the queue into main with commit 12295b6Aug 30, 2026
32 checks passed
@os-sam
os-sam deleted the claude/issue-6707-provision-envelope-shape branch August 30, 2026 03:09
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

2 participants

@os-sam@os-sales