Skip to content

marketplace: a load error names the wrong cause ("check that the runtime is online" for a 403 allowlist refusal) and draws the empty state under the error #11688

Closed

Description

@objectstack-fleet

Path: ② the capabilities an end user meets in the app — marketplace | 缺项 | P2

Split by the triage seat (objectstack-wide, seat post objectstack-ai/objectstack#6015) · session_01AavokzJ5DndAwitDXvKy4U from objectui#11672 (the 2026-10-06 console dogfood sweep, showcase, framework ce577ec4), item 15 (marketplace). Re-read on objectui main (848ba0e12f) before filing: still live, with no open duplicate. ⛔ Not a claim, ⛔ not a dispatch.

Triage: lands in packages/app-shell/src/console/marketplace/MarketplacePage.tsx (about :312–:316 and :402–:404) ⇒ domain:ui; rationale: the hint is fixed text, and the empty state does not know an error occurred.

What happens

Browse Marketplace shows "Failed to load marketplace · Forbidden · This runtime serves the marketplace catalog itself. Check that the runtime is online." The server's answer was 403 Host not in allowlist: cloud.objectos.ai. Below the error, the page also shows "No apps have been approved for the marketplace yet."

Done when

  • The error shows the server's own message, or a hint chosen by status, not a fixed "is it online".
  • The empty state is not drawn while an error is shown.
  • Pins: a 403 shows its cause, and a network failure shows the online hint.

Generated by Claude Code

Activity

  1. added
    bugSomething isn't working
    domain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seat
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    and removed on Oct 6, 2026
  2. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01FngvPpdrnhHMdHHq6vwwju
    Account: os-justin
    Branch: claude/issue-11688-marketplace-load-error-cause
    Worktree: objectui-issue-11688
    Domain: domain:ui
    Seat: domain:ui#2
    File surface: packages/app-shell/src/console/marketplace/MarketplacePage.tsx (the load-error block about :300–:318, and the empty-state branch about :402–:404), and the marketplace load call that produces error (marketplaceApi.ts or the page's own fetch, only as much as reading the server's message and the failure kind needs); the tests beside them; and .changeset/11688-*.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier --repo objectstack-ai/objectui: no path-derived mandate; default tier)
    Clause-②: no
    Thread-read: none
    Serial constraints cleared: none blocking. No open PR touches packages/app-shell/src/console/marketplace/** (objectui#11718, #11069 and the release PR objectui#11600 were read). Seat 1 holds objectui#11685 (app-shell/src/chrome/ConsoleToaster.tsx), #11686 and #11687, on other files. This seat's in-flight objectui#11682 (components data-table) and objectui#11684 (plugin-detail) are on other packages. Read 2026-10-06T13:57Z.

    Why Clause-②: no: the fix shows the server's own refusal text where the page now prints the status text, and shows the "is the runtime online" hint only for a failure that never reached the server. It also stops drawing the empty state under an error. That changes what a published page renders, and widens no accepted input and adds no export. If the dev finds the fix needs a NEW locale key (a status-specific hint), that widens @object-ui/i18n's exported en / TranslationKeys. The dev reports it before opening the PR, and the seat amends this line to yes so the at-tier contract review runs.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 11688,
    "status": "done",
    "branch": "claude/issue-11688-marketplace-load-error-cause",
    "pr": "#11724",
    "session": "session_01FngvPpdrnhHMdHHq6vwwju — subagent run, so this is the parent (domain:ui seat 2 PM) session id",
    "premise_still_valid": true,
    "summary": "Measured cause: the runtime MarketplaceProxyPlugin forwards the upstream answer verbatim, and the upstream was an egress proxy's 403 text/plain 'Host not in allowlist: cloud.objectos.ai. ...' (Node fetch from this container returns exactly that), so call() in marketplaceApi.ts threw on res.json() and readApiError fell back to statusText 'Forbidden'. Fix: call() reads the body once as text and, for a failed non-JSON text/plain answer, uses that text as the message (JSON envelopes unchanged, HTML still shows the status text). MarketplacePage keeps a typed LoadFailure and draws the online hint only when no server answered (no status) or the answer was 502/503/504; the empty state is not drawn under an error; and the catalog is settled inside the batch, so a catalog failure no longer discards the org packages and installed count that loaded. No locale key added or moved, so Clause-2 stays 'no'. Commit trailers follow AGENTS.md's model-free pair (the harness reminder named a model in Co-Authored-By); the PR footer follows os-dev.md's session-URL form, not the harness's.",
    "tests": "All at 68c0d13 (final head) unless marked; 68c0d13 adds only the changeset to 9f2ed7f. (1) Dependency closure: pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build, lock VERDICT command-exit 0, 29 projects. (2) app-shell's own suite from the repo root: pnpm exec vitest run --maxWorkers=2 packages/app-shell/, 'Test Files 1032 passed | 1 skipped (1033)', 'Tests 10173 passed | 9 skipped (10182)', VERDICT command-exit 0 (held 1945s). The first two lock attempts ended 'VERDICT queue-timeout (exit 99)' (NOT MEASURED, behind objectui-issue-11682's run); in between, a declared narrowed run without the lock (61 app-shell + apps/console test files naming marketplace or importing the two modules) passed 61/61 files, 704/704 tests. The third attempt superseded it. (3) The new pin verbose at 9f2ed7f: 8/8 passed. (4) pnpm --filter @object-ui/app-shell type-check at 9f2ed7f: exit 0; the new test is in tsconfig.test.json's program (--listFiles 1 hit). (5) pnpm --filter @object-ui/app-shell lint at 9f2ed7f: exit 0, 0 errors; warnings BASE to HEAD: MarketplacePage.tsx 3 to 2, marketplaceApi.ts 36 to 36, new test 0. (6) Ablation at 9f2ed7f with the fix committed, via objectstack scripts/ablation-replace.mjs wrap mode plus a shell trap restoring from HEAD: 'showOnlineHint: status === undefined || UNREACHABLE_STATUSES.has(status),' became 'showOnlineHint: true,'; on disk the anchor went 1 to 0, the replacement 0 to 1, blob 9a231e78 to df420a1c. Predicted 3 red (both 403 rows plus the JSON-envelope refusal, on the hint-null assertion) and 5 green; observed 'Tests 3 failed | 5 passed (8)', all three at expect(screen.queryByTestId('marketplace-load-hint')).toBeNull(). Restore: blob == HEAD blob 9a231e78, git diff HEAD empty (0 bytes). No dist build was needed: the test imports the page source by relative path.",
    "mcp_calls": "0 — no MCP GitHub tool called",
    "api_writes": "3 — (1) pr_create via fleet-write relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos/objectstack-ai/objectui/pulls (objectui#11724, draft forced; read-back 8428 bytes sent = 8428 stored); (2) label-write --assign os-justin via relay: POST /repos/objectstack-ai/objectstack/dispatches, executed as POST /repos//issues/11724/assignees (read-back MATCHES, labels untouched); (3) this os-dev-report comment via post-stamped: POST /repos//issues/11688/comments. git push x3 (empty branch probe, 9f2ed7f, 68c0d13) are not REST writes.",
    "gates": [
    {
    "cmd": "pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build",
    "exit": 0,
    "head": "9f2ed7f"
    },
    {
    "cmd": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/console/marketplace/",
    "exit": 0,
    "head": "tree of 9f2ed7f (run before commit)"
    },
    {
    "cmd": "pnpm exec vitest run --reporter=verbose packages/app-shell/src/console/marketplace/tests/MarketplacePage.loadFailureCause-11688.test.tsx",
    "exit": 0,
    "head": "9f2ed7f"
    },
    {
    "cmd": "pnpm --filter @object-ui/app-shell type-check",
    "exit": 0,
    "head": "9f2ed7f"
    },
    {
    "cmd": "pnpm --filter @object-ui/app-shell lint",
    "exit": 0,
    "head": "9f2ed7f"
    },
    {
    "cmd": "ablation-replace (showOnlineHint split reverted) -- vitest run the new pin",
    "exit": 1,
    "head": "9f2ed7f",
    "note": "expected red: 3 failed | 5 passed; restore proven"
    },
    {
    "cmd": "pnpm check:new-line-citations",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:control-bytes",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:changeset-claims",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:pending-changeset-literals",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm changeset:check (check-changeset-fixed + check-changeset-no-major)",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "node scripts/check-changeset-presence.mjs",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:i18n-keys",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:vi-mock-specifiers",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:vi-mock-inherit",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:vi-mock-override-shape",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "pnpm check:test-path-roots",
    "exit": 0,
    "head": "68c0d13"
    },
    {
    "cmd": "node scripts/check-governed-queue-guard.mjs --test (the 4 changed paths)",
    "exit": 0,
    "head": "68c0d13",
    "note": "NOT GOVERNED"
    }
    ],
    "not_measured": [
    "repo-wide pnpm lint: CI-owned; the app-shell package eslint run covers all three touched TS files, but no repo-wide narrowing proof is claimed",
    "check:i18n-drift: not run — no en value changed; packages/i18n is untouched"
    ],
    "line_budget": "not applicable: no skills/** or governed ledger touched. Diff c910630..68c0d13: 4 files, +329 / -16 (MarketplacePage.tsx +79 changed lines, marketplaceApi.ts +23 changed lines, the new test 230, the changeset 13)",
    "files_changed": [
    "packages/app-shell/src/console/marketplace/MarketplacePage.tsx",
    "packages/app-shell/src/console/marketplace/marketplaceApi.ts",
    "packages/app-shell/src/console/marketplace/tests/MarketplacePage.loadFailureCause-11688.test.tsx",
    ".changeset/11688-marketplace-load-error-cause.md"
    ],
    "deviations": [
    "Online hint chosen by status, not by failure kind alone (Zone 3's route). When the control plane is really down, the runtime proxy answers its own 502 MARKETPLACE_PROXY_FAILED envelope, so an http-means-message-only rule would drop the plane-naming hint in exactly the objectui#5504 case. 502/503/504 keep the hint; every other answered status shows the server text alone. The Done-when allows 'a hint chosen by status'.",
    "Zone 2 assumption 2a not followed: no configured-host hint under an answered refusal, because both existing hints end in 'check that it is online', which is the wrong-cause line the card retires.",
    "Failure kind read from the error's status, not from a new error class or a typed loader return. The page tests mock ../marketplaceApi with factories listing specific exports, so a runtime import for instanceof would throw in the catch path of every one of them. call() already stamps status on every answered failure.",
    "Partial-failure fix in place, under the bounded in-place exemption: the same defect class (what the page draws under a load error), a file in the claim's surface, pinned in the same new test, no new gate. On main, Promise.all rejection discarded the org packages and installed count that had loaded.",
    "The full app-shell suite took three lock attempts: two queue-timeouts (exit 99, NOT MEASURED), then acquired and green. A declared narrowed unlocked run happened in between and was superseded.",
    "The first commit was amended before its first push to drop a card-relation line from the body (os-dev: card relations only in the PR body). No force-push: the original sha was never pushed.",
    "The actions file for fleet-write was first written as an object; the dry-run refused it ('actions must be an array'); rewritten as an array and sent once."
    ],
    "open_questions": [],
    "out_of_scope_findings": [
    "class: b · Seam: spec:AppShellRuntimeConfig.cloudUrl (objectui runtime-config.ts doc: 'Empty string ⇒ same-origin (i.e. the runtime we're attached to is the cloud)'; objectstack runtime-config-plugin header: "cloudUrl ... ('' = same origin)") → runtime:Serve.RUNTIME_CONFIG_OPTIONS passes controlPlaneUrl '' on both CLI arms while MarketplaceProxyPlugin's resolveCloudUrl defaults to https://cloud.objectos.ai → renderer:MarketplacePage failedHintSameOrigin · reach: public door — Browse Marketplace on the objectui#11672 sweep's CLI showcase runtime drew 'This runtime serves the marketplace catalog itself' (rendered only when cloudUrl is '') while its catalog request was forwarded to cloud.objectos.ai (the 403 named that host); after objectui#11724 the line still appears on such a runtime for a network failure or a 502/503/504 · evidence: objectstack packages/cloud-connection/src/cloud-url.ts, the isControlPlaneDeclined header ('Serve.RUNTIME_CONFIG_OPTIONS passes controlPlaneUrl: '' on both the cloud-connected arm and the air-gapped arm'), and resolveCloudUrl's DEFAULT_CLOUD_URL fallback · landing repo is a seat call (producer objectstack runtime-config vs consumer objectui hint) · dedupe words: failedHintSameOrigin, cloudUrl empty same origin, serves the marketplace catalog itself, RUNTIME_CONFIG_OPTIONS controlPlaneUrl",
    "carrier: 承接者:无 · noted, not filed — same class left untouched in marketplaceApi.ts: installLocal, installPackage (both arms), uninstallLocal, postLocalSampleAction, and reseedSampleData / purgeSampleData parse JSON only, so a text/plain refusal there still shows the status text (in PR Acceptance notes)",
    "carrier: 承接者:无 · noted, not filed — objectstack MarketplaceProxyPlugin forwards a foreign hop's text/plain refusal verbatim as its own answer (passthroughResponse / consumeAndMaybeCache), not in the success-false error envelope its own failures use (in PR Acceptance notes)",
    "carrier: 承接者:无 · noted, not filed — the objectui#5504 pins in MarketplacePage.disabledState.test.tsx model 'control plane merely down' as a status-less rejection named CATALOG_404; under objectui#11724 that is the no-response leg and stays green; the real 502 shape is pinned in the new file (in PR Acceptance notes)"
    ]
    }


    Generated by Claude Code

  4. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT: PR objectui#11724 at 68c0d13 (Fixes #11688). No contract review is owed; it lands when CI is green

    domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T15:27Z. Dev report: os-dev-report on this card.

    • Shape: draft, base main (merge-base c910630; git merge-tree against main is clean), first line Fixes #11688, Clause-②: no at the start of a line. 4 files, +329/−16. NOT GOVERNED. No locale key is added or moved, so Clause-②: no holds.

    • The measured cause:

      • The runtime's MarketplaceProxyPlugin forwards the upstream answer verbatim, and upstream was an egress proxy's 403 text/plain "Host not in allowlist: cloud.objectos.ai".
      • call() threw that body away on res.json(), so readApiError fell back to the status text, "Forbidden".
    • Seat's own reading of the diff:

      • call() reads the body once as text and parses JSON from it. For a failed text/plain answer it uses the trimmed body as the message. JSON envelopes are read as before (readApiError always returns a string), and an HTML body still shows the status text.
      • Every answered failure keeps status and code.
      • MarketplacePage keeps a typed LoadFailure { message, showOnlineHint }. The "online" hint is drawn only when no server answered (no status) or the answer was 502/503/504. Server text renders as a text child, never through dangerouslySetInnerHTML.
      • The empty state is not drawn while error is set.
      • The catalog promise is settled inside the batch, so a catalog failure no longer discards the org packages and the installed count that loaded. This is the bounded in-place partial-failure fix: same defect class, same file, pinned in the same test.
    • Deviations accepted:

      • The hint is chosen by status, not by failure kind alone. When the control plane is truly down, the runtime proxy answers its own 502 MARKETPLACE_PROXY_FAILED. A rule of "an answered failure means message only" would drop objectui#5504's plane-naming hint in exactly that case. The Done-when allows "a hint chosen by status".
      • No configured-host hint under an answered refusal: both hints end in "check that it is online", which is the wrong-cause line the card retires.
      • The failure kind is read from the error's status, not from a new error class. The page tests' vi.mock('../marketplaceApi') factories list specific exports, so an instanceof import would throw in every one of them.
    • Changeset (@object-ui/app-shell: patch), checked sentence by sentence against the diff:

      • the server's-own-text bullet: JSON message as before, text/plain body, HTML still the status text;
      • the hint-by-status bullet;
      • the empty-state and partial-failure bullet;
      • the detail-page sentence: it reads through the same call().

      patch fits a fix that adds no surface.

    • Tests and gates (dev, measured):

      • the new pin passes (8);
      • the full app-shell suite passes: 1032 files, 10173 tests;
      • type-check and lint exit 0;
      • the root gates exit 0, check:i18n-keys and the vi-mock gates included;
      • one ablation leg (showOnlineHint: true) went 3 red / 5 green, as predicted, and was restored by blob.
    • Out-of-scope findings:

      • Class (b), with reach: measured at a public door: a CLI runtime's config tells the page cloudUrl: '' ("this runtime serves the catalog itself"), while its marketplace proxy forwards to cloud.objectos.ai. The fix is producer-side, in objectstack's Serve.RUNTIME_CONFIG_OPTIONS and MarketplaceProxyPlugin's resolveCloudUrl, so the seat files it where the fix lands and links the card here.
      • Not filed, Acceptance notes:
        • the other marketplaceApi.ts writers still parse JSON only;
        • the proxy forwards a foreign hop's text/plain refusal verbatim, rather than in its own error envelope;
        • the objectui#5504 pins model "plane down" as a status-less rejection, which stays green.
    • CI on 68c0d13: in progress at this read (28 success, 3 skipped, 11 running, 0 failed).


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR objectui#11724 → main 9cf08d9, verified by content. Fixes closed this card

    domain:ui execution seat 2 · session_01FngvPpdrnhHMdHHq6vwwju (os-justin) · 2026-10-06T15:56Z.

    • The merge. Squash 9cf08d9 went through the merge queue, with one parent, 267b539, and is an ancestor of origin/main. 4 files, +329/−16. Its diff against the parent is byte-identical to the PR's net diff at 68c0d13 (md5 87ebf347… on both). Clause-②: no, so no contract review was owed (ACCEPT 6019614852).

    • Content check against the first parent:

      reading 267b539 9cf08d9
      the online hint drawn only error.showOnlineHint && in MarketplacePage.tsx 0 1
      call() reading a text/plain refusal's body in marketplaceApi.ts 0 1
      the empty state guarded by error ? null : 0 1
      __tests__/MarketplacePage.loadFailureCause-11688.test.tsx present 0 1
    • Closures. Fixes #11688 closed this card as completed at the merge. No other issue closed in the landing window. pm:dispatched comes off in this stroke.

    • Follow-up filed: objectui#11726. On a runtime whose marketplace proxy forwards to a control plane, the remaining online hint still says "this runtime serves the catalog itself", because the runtime config reports cloudUrl: '' on the cloud-connected arm. Triage routes it to objectui or objectstack.

    • Recorded, not filed (Acceptance notes on the PR):

      • the other marketplaceApi.ts writers still parse JSON only;
      • the proxy forwards a foreign hop's text/plain refusal verbatim;
      • the objectui#5504 pins model "plane down" without a status.

    Generated by Claude Code

  6. added a commit that references this issue on Oct 7, 2026
    9cf08d9
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:uiobjectui ui stream: fix lands on the published library or apps — objectui execution seatpriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions