Skip to content

Private servers: probe with the anonymous discovery endpoints and confirm the server's identity before connecting #154

Closed

Description

@hotlong

Blocked-by: #150

Context

Design screens 5 (enter address) and 7 (confirm). Today the server picker probes GET /api/v1/health, which says nothing about whether the host is an ObjectOS server or which one it is.

The platform has anonymous endpoints made for this:

  • GET /.well-known/objectstack: discovery — name, version, capabilities; also at /api/v1/discovery;
  • GET /api/v1/runtime/config: branding — productName, logoUrl, brandColor;
  • GET /api/v1/auth/config: sign-in methods — emailPassword, socialProviders, features.sso, ssoEnforced.

Ask

  1. Probe. After normalizing the typed address, fetch the three endpoints.
    • Discovery decides "is this an ObjectOS server". A 404 or unparseable body means "这不是 ObjectOS 服务器", never a generic error.
    • Branding fills screen 7.
    • Auth config fills the sign-in-methods row.
    • Keep the redirect adoption (serverUrlAfterRedirect) and the https rule.
  2. Errors, each with its own copy:
    • unreachable → network/VPN hint;
    • TLS failure → "证书无效";
    • not ObjectOS → as above;
    • insecure http on a public host → as today.
  3. Confirm screen (7).
    • Show the logo (or the initial on the brand colour), the product name, the full URL, a lock row only for https, the sign-in methods, and the platform version.
    • Actions: 连接并登录 / 不是这台服务器.
    • Nothing is saved before the user confirms.
  4. Screen 5 copy. Enter address, a scan button, "在哪里找到地址?" steps pointing at Console › 个人设置 › 移动应用, and the VPN hint.

Make the probe a pure module in lib/ with injected fetch. It replaces validateServerUrl/resolveServerUrl; delete the old one, which no longer has a reader.

Acceptance

  • Unit tests cover each probe outcome: ObjectOS server, non-ObjectOS 200 page, 404, timeout, TLS error, redirect adoption, partial failures (discovery OK but branding missing → initial and default colour).
  • Component test for the confirm screen.

Activity

  1. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    First touch (single-lane repo, the seat does its own triage).

  2. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Claim: PM loop round 1
    Session: session_01QSeKYnW1Xf6HDSWz1eGAqv
    Account: os-justin
    Branch: claude/issue-154-server-probe-confirm
    Worktree: mobile-issue-154
    Domain: n/a — single-lane repo (repo:mobile)
    Seat: repo:mobile#1
    File surface: lib/server-url.ts (replaced), a new probe module under lib/, app/server-config.tsx, a new confirm screen under app/ or components/, stores/server-store.ts only where the save moves behind the confirm step, locales/{en,zh,ar}.json, __tests__/**, docs/HYBRID-SHELL.md, .changeset/154-server-probe-confirm.md (stop on breach; explain in the report)
    Container & model: M, mode:subagent, model: default tier (opus) — dispatch-gates --tier --repo objectstack-ai/mobile: no path-derived mandate, effort: high
    Clause-②: no
    Metadata-protocol: none
    Responsibility: n/a — not a defect card
    Thread-read: none
    Premise: holds
    Serial constraints cleared: #151 is dispatched in the same round and shares only locales/*.json and docs/*.md. That is ordinary concurrency. #152 and #157 are held behind this card on app/server-config.tsx.

  3. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
    os-dev-report

    {
      "issue": 154,
      "status": "done",
      "branch": "claude/issue-154-server-probe-confirm",
      "pr": "https://github.com/objectstack-ai/mobile/pull/165",
      "session": "session_01QSeKYnW1Xf6HDSWz1eGAqv",
      "premise_still_valid": true,
      "summary": "Replaced the /api/v1/health probe with lib/server-probe.ts, a pure probeServer() with injected fetch. Its result is a discriminated union (ok, empty, invalid, insecure-http, unreachable, tls, timeout, not-objectos, server-error). Discovery (/.well-known/objectstack, enveloped) decides ObjectOS; runtime/config branding and auth/config sign-in methods are best-effort. Redirect adoption via serverUrlAfterRedirect and the https rule are kept, and a redirect onto public http is now refused. The new confirm step (components/shell/ServerConfirm.tsx) shows logo or initial on brand colour, name, URL, an https lock, sign-in methods and version; the server is saved only on 'Connect and sign in'. resolveServerUrl/validateServerUrl are deleted. TLS cannot be told apart through RN fetch (whatwg-fetch 3.6.20 rejects everything as 'Network request failed'), so the screen injects xhrFetch, which keeps RN XHR's native error text. Premise measured on mobile main 8c6f13a and objectstack main d9885f5d: holds. Every field the card names exists, but branding is nested at branding.* in a bare body and ssoEnforced is data.features.ssoEnforced.",
      "tests": "head 49de79b: npx jest --ci --coverage -> Test Suites 29/29, Tests 329/329, exit 0; baseline 8c6f13a was 26 suites, 283 tests. lib/ statements 84.73% (threshold 80), server-probe.ts 95.2%. New: __tests__/lib/server-probe.test.ts (35 tests, every outcome the card lists plus xhrFetch), __tests__/components/ServerConfirm.test.tsx, __tests__/components/ServerConfigScreen.test.tsx (save only on confirm, per-kind copy, stale probe ignored). Ablation via objectstack scripts/ablation-replace.mjs on committed code; jest runs src through babel, so there is no dist leg. A1 (connect() right after probe) -> ServerConfigScreen 2 failed/10 passed, restored blob == HEAD 491eb9d71b85. A2 (TLS classification removed): the first attempt was refused by the tool because the replacement was not distinguishable on disk, so it did not run. A2b with a distinct replacement -> server-probe 5 failed/30 passed, restored blob == HEAD 789cd23b53ab.",
      "gates": "local on 49de79b under os-verify-lock (VERDICT command-exit 0), exit codes captured per command before any pipe into scratchpad issue-154/gates.txt: npx tsc --noEmit = 0; npx eslint . --ext .ts,.tsx = 0; npx jest --ci --coverage = 0. One earlier full run on 47ba7d8 was red: tsc = 2, 16 x TS2339 missing extend-expect matcher types in the two new component tests. Fixed by a type-only import in 49de79b. dispatch-gates.mjs is not applicable: mobile has no such script. Families were derived by hand from mobile .github/workflows: lint-typecheck.yml (tsc, eslint) and ci.yml (jest --ci --coverage); all three were run locally. PR check-runs at report time: none listed for 49de79b -> NOT MEASURED: CI, reason: no check-runs reported yet.",
      "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm",
      "mcp_calls": "0",
      "api_writes": "2 — relay pr_create: POST /repos/objectstack-ai/mobile/pulls (draft; no labels, no assignees; read-back 10575/10575 bytes identical); post-stamped report comment: POST /repos/objectstack-ai/mobile/issues/154/comments. git push is not counted.",
      "files_changed": [
        ".changeset/154-server-probe-confirm.md",
        "__tests__/components/ServerConfigScreen.test.tsx",
        "__tests__/components/ServerConfirm.test.tsx",
        "__tests__/lib/server-probe.test.ts",
        "__tests__/lib/server-url.test.ts",
        "app/server-config.tsx",
        "components/shell/ServerConfirm.tsx",
        "docs/HYBRID-SHELL.md",
        "lib/server-probe.ts",
        "lib/server-url.ts",
        "locales/ar.json",
        "locales/en.json",
        "locales/zh.json"
      ],
      "line_budget": "n/a — no skills/** or ratcheted ledger touched; diff 8c6f13a..49de79b = 13 files, +1475/-210",
      "deviations": [
        "Screen 5 'where to find the address' steps do not point at Console > 个人设置 > 移动应用. That page does not exist on objectstack main d9885f5d: no mobile-app settings page under ui/apps/console/src, and card #156 says an objectstack card will add it. The steps point at the browser address bar instead, which is true today. See open_questions.",
        "Added a 'server-error' (5xx on discovery) outcome beyond the PM-suggested union.",
        "The xhrFetch transport lives in lib/server-probe.ts, inside the file surface; probeServer itself stays pure with injected fetch.",
        "stores/server-store.ts untouched: the save was already only in connect(), which is now called only from the confirm step.",
        "Ran jest as CI does (npx jest --ci --coverage), a superset of the dispatch's npx jest --ci.",
        "Commit trailers use AGENTS.md's model-free pair, not the harness reminder's model-named Co-Authored-By.",
        "First A2 ablation was a refused no-op; redone as A2b with a distinct replacement."
      ],
      "open_questions": [
        {
          "question": "Screen 5 copy: the card asks the 'where to find the address' steps to point at Console > 个人设置 > 移动应用, but that Console page does not exist yet (objectstack main d9885f5d). Shipped: steps that are true today (open ObjectOS in a browser, copy the address bar, ask the admin). Keep that, or point at the not-yet-shipped page now?",
          "options": [
            "A — keep the true-today steps; swap serverConfig.where.step1/step2 in en/zh/ar once the objectstack Console page ships (one string per locale)",
            "B — follow the card text now, so the app tells users to open a page their server does not have until that objectstack card lands and the server upgrades"
          ],
          "recommendation": "A, because copy that sends users to a page that does not exist is a user-visible misleading-copy defect, and mobile releases independently of servers. Even after the Console page ships, older servers will lack it."
        }
      ],
      "security_findings": "none",
      "out_of_scope_findings": [
        "carrier: none · noted in PR #165 Acceptance notes, not filed: plugin-auth serves features.sso on /api/v1/auth/config, and it is classified in packages/spec/src/kernel/public-auth-features.ts:188, but GetAuthConfigResponseSchema (packages/spec/src/api/auth-endpoints.zod.ts) does not declare it; read-only observation, no measured consumer parses through that schema",
        "carrier: none · noted, not filed: whether deployed servers predate the /.well-known/objectstack { success, data } envelope is unmeasured (objectstack checkout is shallow); such a server would read as 'not ObjectOS'"
      ]
    }
  4. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    ACCEPT → PR #165 comment 6577280126 (head a93d7cb, after two patch rounds: a merge of main, then a CI cold-load timeout root-caused and fixed). Ready for review; the maintainer is requested. The maintainer merges. When it lands, #152 (serial behind this card) and #157 / #156 (blocked on it) are next.

  5. objectstack-fleet commented on Nov 2, 2026

    @objectstack-fleet
    Contributor

    repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv

    Landed. #165 merged as 83d4716, merged by os-zhuang.

    Reading Value
    PR #165 merged=true, merge commit 83d4716, last head a93d7cb (CI green)
    Card closed completed via Fixes #154

    State: pm:dispatched removed. Unblocked by this card: #156, whose Blocked-by cards #151 and #154 are now both merged; it is dispatched now. #152 stays queued. It now waits for #166 (#162) to land, because its WebView remount on switch goes through the load state machine that #166 introduces in ConsoleWebView.tsx. #157 still waits on #152.

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

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions