You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
External sign-in bar: let enterprise IdP pages load in the WebView during sign-in, behind a native bar showing the IdP's domain #155
Design screen 3. A private server's Console may sign in through an enterprise IdP (OIDC or SAML) whose host is not known to the shell in advance. Today the navigation policy sends any off-server top-frame page to the system browser, so the IdP session never reaches the WebView. EXPO_PUBLIC_TRUSTED_HOSTS only helps white-label builds that know the host at build time.
Ask
Sign-in excursion mode.
A top-frame navigation away from the current server is admitted only when the excursion started from the server's sign-in surface: the Console /login route, or /api/v1/auth/*.
The excursion lasts until the WebView returns to the server's origin, or the user cancels.
Outside an excursion, the existing policy holds unchanged.
Native bar. While on an off-server host, a native top bar shows a lock and the exact host, plus "正在外部页面登录 · " and a 取消 action. Cancel returns to the server's /login.
Limits.
Only https: hosts are admitted.
Never admit hosts on the bridge's trust list; the bridge never answers an excursion page.
Cap the excursion: after N redirects, or after a time limit, end it and show an error. Choose N and the limit in the PR.
Security boundary: this admits new top-frame origins. Per the charter it stays on the human floor; the maintainer merges and reviews the threat model in the PR (phishing via a crafted login redirect, an excursion that never returns).
Acceptance
Unit tests cover the excursion state machine: start only from the sign-in surface, end on return, cancel, http refused, cap reached, and the bridge never trusting an excursion origin.
Manual run against a test OIDC provider on a development build.
Gate #150 released. This card moves to pm:blocked on #152, type enhancement.
When #152 lands, this card goes to the maintainer before any dispatch. Letting IdP pages load in the WebView admits a new origin to the navigation policy, which the lane charter puts on the human floor.
Claim: PM loop round 1
Session: session_01QSeKYnW1Xf6HDSWz1eGAqv
Account: os-justin
Branch: claude/issue-155-idp-excursion
Worktree: mobile-issue-155
Domain: n/a — single-lane repo (repo:mobile)
Seat: repo:mobile#1
File surface: lib/shell/navigation-policy.ts, a new excursion state machine under lib/shell/, components/shell/ConsoleWebView.tsx (excursion wiring, through the load state machine), a native bar component under components/shell/, lib/shell/bridge/handler.ts (never trust an excursion origin), app/console.tsx, locales/*.json (all ten), __tests__/**, docs/HYBRID-SHELL.md, .changeset/155-idp-excursion.md (stop on breach; explain in the report)
Container & model: L, mode:subagent, model: default tier (opus) — dispatch-gates --tier --repo objectstack-ai/mobile: no path-derived mandate, effort: xhigh
Clause-②: no
Metadata-protocol: none
Responsibility: n/a — not a defect card
Thread-read: 6576856193
Premise: holds. main at 678fbe9 sends every off-server top-frame page to the system browser (lib/shell/navigation-policy.ts), and there is no excursion mode. Security boundary: the maintainer merges this PR and reviews its threat model; the seat does not land it.
Serial constraints cleared: #174 may touch app/console.tsx (ordinary concurrency). #157 does not touch this surface.
{
"issue": 155,
"status": "done",
"branch": "claude/issue-155-idp-excursion",
"pr": "https://github.com/objectstack-ai/mobile/pull/176",
"session": "session_01QSeKYnW1Xf6HDSWz1eGAqv",
"premise_still_valid": true,
"summary": "Sign-in excursion mode: when the current server's sign-in surface (Console /login, or /api/v1/auth/*) navigates the top frame off the server, the provider's https pages load in the WebView under a native bar (lock, exact host, 「正在外部页面登录 · SERVER_LABEL」, 取消); it ends on return to the server origin, on 取消 (new WebView at /login), or with a notice after 30 off-server navigations, 10 minutes (better-auth 1.7.12's OAuth state lifetime), an http page, or a provider that refuses embedded WebViews (accounts.google.com or a disallowed_useragent URL: 'not supported in the app yet'). The start is judged on the shell's own native record of the top-frame URL (admitted top-frame requests plus load start/end URLs), never on routeChanged; the bridge is closed during an excursion (handler inExcursion) and the excursion origin is never on its trust list; routes and push tokens are held through the load state machine's reload event and answers in flight are dropped; decideNavigation and console-load.ts are unchanged. PR body carries the Threat model (T1 phishing via crafted login redirect, T2 never-returning excursion, T3 stale reports, T4 same-origin script, T5 Android sub-frames) with mitigation and pinning test per row, plus two documented residuals the maintainer reviews at merge (OAuth-provider hand-off to a consent-skipped first-party client from /login loads under the bar; iOS back-swipe after a return opens the in-app browser). Deviation: the first WIP commit carried the harness's model-named trailer; I amended it to the AGENTS.md model-free pair and pushed with --force-with-lease=claude/issue-155-idp-excursion:381aef1 (all five section 3 conditions held, no PR existed yet).",
"tests": "Head 0c0706a (objectstack-ai/mobile). npx jest --ci --coverage: exit 0, Test Suites 41 passed / 41, Tests 729 passed / 729 (baseline 678fbe9: 39 / 669). Cold variant npx jest --ci --no-cache __tests__/components/SignInExcursionBar.test.tsx __tests__/components/ConsoleWebView.test.tsx: exit 0, 2 suites, 82 tests. New: __tests__/lib/shell/sign-in-excursion.test.ts, __tests__/components/SignInExcursionBar.test.tsx; extended: navigation-policy.test.ts (isSignInSurface), bridge-handler.test.ts (during a sign-in excursion), ConsoleWebView.test.tsx (sign-in excursion, 21 tests). Ablations at f015c25 (same code, 728 tests), each via objectstack scripts/ablation-replace.mjs WRAP mode (anchor 1 -> 0, blob changed) plus a shell EXIT/INT/TERM trap, restored with git checkout HEAD and proven by blob == HEAD, git diff HEAD empty, porcelain empty. Leg A sign-in-surface start check (drop the isSignInSurface clause): jest exit 1, 7 failed / 728: the-start ×3 (any other server page, another origin, record only from WebView reports), the-end ×2 (no restart after return, current connection only), ConsoleWebView 'sends the same page to the system browser from any other Console page', pre-existing 'applies the navigation policy'. Leg B1 bridge closure (drop if inExcursion return null): 2 failed: bridge-handler 'answers no call and takes no event, even one naming the server's origin', ConsoleWebView 'answers no call while an excursion page is on screen, whatever origin it names'. Leg B2 in-flight answer drop (epoch check): 1 failed: 'drops the answer to a call the Console made before it left for the provider'. Leg B3 trust list widened to any origin: 12 failed incl. new 'never trusts the excursion page's origin, in an excursion or after it' + 11 pre-existing origin tests. Device check on a development build against a test OIDC provider: NOT RUN, NOT MEASURED (no device here), handed to the maintainer in 需要你做的.",
"gates": "Local, head 0c0706a, exit codes captured before any pipe (recorded in scratchpad issue-155/gate-exit-codes.txt): npx tsc --noEmit exit 0; npx eslint . --ext .ts,.tsx exit 0; npx jest --ci --coverage exit 0 (lock VERDICT command-exit 0); cold component run exit 0. CI on PR #176 head 0c0706a, check-runs read once: 'Code Quality' (ci.yml, runs jest --ci --coverage) completed · success; 'Lint & Type Check' (lint-typecheck.yml, tsc + eslint) completed · success. dispatch-gates.mjs not run: it answers for the objectstack tree only; the mobile gate list is hand-derived from its .github/workflows, as os-dev.md directs for a sister repo. Control-byte self-scan of the 23 changed files: no hits.",
"dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm (Clause-②: no)",
"mcp_calls": "0 — no MCP GitHub calls of any kind",
"api_writes": "2 relay/REST writes: (1) pr_create through scripts/pm/fleet-write/dispatch.mjs (relay run 41550124706, POST /repos/objectstack-ai/mobile/pulls, draft, no labels, no assignees; read-back 22014 bytes sent = stored); (2) this os-dev-report comment through scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/mobile/issues/155/comments). Not REST: git pushes of claude/issue-155-idp-excursion: empty probe, 5 commit pushes, 1 --force-with-lease (trailer amend, see summary).",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: seat · two pre-existing navigation-policy observations, reported to the maintainer privately"
]
}
ACCEPT → PR #176 (head 0c0706a). This is a security boundary, so the maintainer reviews the Threat model and merges; the seat does not land it. Next after merge: #158 (docs) waits only on #153.
Landed.#176 merged through the merge queue. os-zhuang enqueued it after the Threat-model review. The merge_group run passed CI and Lint & Type Check. The card closed completed via Fixes #155, and pm:dispatched is removed.
Not covered here: IdPs that refuse embedded WebViews still wait for the ruling on #148. #158 (docs) now waits only on #153.
Activity
objectstack-fleet commentedon Nov 2, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Gate #150 released. This card moves to
pm:blockedon #152, typeenhancement.When #152 lands, this card goes to the maintainer before any dispatch. Letting IdP pages load in the WebView admits a new origin to the navigation policy, which the lane charter puts on the human floor.
objectstack-fleet commentedon Nov 2, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Unblocked:
Blocked-by#152 merged (678fbe9).Claim: PM loop round 1
Session:
session_01QSeKYnW1Xf6HDSWz1eGAqvAccount:
os-justinBranch:
claude/issue-155-idp-excursionWorktree:
mobile-issue-155Domain:
n/a — single-lane repo (repo:mobile)Seat:
repo:mobile#1File surface:
lib/shell/navigation-policy.ts, a new excursion state machine underlib/shell/,components/shell/ConsoleWebView.tsx(excursion wiring, through the load state machine), a native bar component undercomponents/shell/,lib/shell/bridge/handler.ts(never trust an excursion origin),app/console.tsx,locales/*.json(all ten),__tests__/**,docs/HYBRID-SHELL.md,.changeset/155-idp-excursion.md(stop on breach; explain in the report)Container & model:
L,mode:subagent,model: default tier (opus) — dispatch-gates --tier --repo objectstack-ai/mobile: no path-derived mandate,effort: xhighClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6576856193
Premise: holds.
mainat678fbe9sends every off-server top-frame page to the system browser (lib/shell/navigation-policy.ts), and there is no excursion mode. Security boundary: the maintainer merges this PR and reviews its threat model; the seat does not land it.Serial constraints cleared: #174 may touch
app/console.tsx(ordinary concurrency). #157 does not touch this surface.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
os-dev-report
{ "issue": 155, "status": "done", "branch": "claude/issue-155-idp-excursion", "pr": "https://github.com/objectstack-ai/mobile/pull/176", "session": "session_01QSeKYnW1Xf6HDSWz1eGAqv", "premise_still_valid": true, "summary": "Sign-in excursion mode: when the current server's sign-in surface (Console /login, or /api/v1/auth/*) navigates the top frame off the server, the provider's https pages load in the WebView under a native bar (lock, exact host, 「正在外部页面登录 · SERVER_LABEL」, 取消); it ends on return to the server origin, on 取消 (new WebView at /login), or with a notice after 30 off-server navigations, 10 minutes (better-auth 1.7.12's OAuth state lifetime), an http page, or a provider that refuses embedded WebViews (accounts.google.com or a disallowed_useragent URL: 'not supported in the app yet'). The start is judged on the shell's own native record of the top-frame URL (admitted top-frame requests plus load start/end URLs), never on routeChanged; the bridge is closed during an excursion (handler inExcursion) and the excursion origin is never on its trust list; routes and push tokens are held through the load state machine's reload event and answers in flight are dropped; decideNavigation and console-load.ts are unchanged. PR body carries the Threat model (T1 phishing via crafted login redirect, T2 never-returning excursion, T3 stale reports, T4 same-origin script, T5 Android sub-frames) with mitigation and pinning test per row, plus two documented residuals the maintainer reviews at merge (OAuth-provider hand-off to a consent-skipped first-party client from /login loads under the bar; iOS back-swipe after a return opens the in-app browser). Deviation: the first WIP commit carried the harness's model-named trailer; I amended it to the AGENTS.md model-free pair and pushed with --force-with-lease=claude/issue-155-idp-excursion:381aef1 (all five section 3 conditions held, no PR existed yet).", "tests": "Head 0c0706a (objectstack-ai/mobile). npx jest --ci --coverage: exit 0, Test Suites 41 passed / 41, Tests 729 passed / 729 (baseline 678fbe9: 39 / 669). Cold variant npx jest --ci --no-cache __tests__/components/SignInExcursionBar.test.tsx __tests__/components/ConsoleWebView.test.tsx: exit 0, 2 suites, 82 tests. New: __tests__/lib/shell/sign-in-excursion.test.ts, __tests__/components/SignInExcursionBar.test.tsx; extended: navigation-policy.test.ts (isSignInSurface), bridge-handler.test.ts (during a sign-in excursion), ConsoleWebView.test.tsx (sign-in excursion, 21 tests). Ablations at f015c25 (same code, 728 tests), each via objectstack scripts/ablation-replace.mjs WRAP mode (anchor 1 -> 0, blob changed) plus a shell EXIT/INT/TERM trap, restored with git checkout HEAD and proven by blob == HEAD, git diff HEAD empty, porcelain empty. Leg A sign-in-surface start check (drop the isSignInSurface clause): jest exit 1, 7 failed / 728: the-start ×3 (any other server page, another origin, record only from WebView reports), the-end ×2 (no restart after return, current connection only), ConsoleWebView 'sends the same page to the system browser from any other Console page', pre-existing 'applies the navigation policy'. Leg B1 bridge closure (drop if inExcursion return null): 2 failed: bridge-handler 'answers no call and takes no event, even one naming the server's origin', ConsoleWebView 'answers no call while an excursion page is on screen, whatever origin it names'. Leg B2 in-flight answer drop (epoch check): 1 failed: 'drops the answer to a call the Console made before it left for the provider'. Leg B3 trust list widened to any origin: 12 failed incl. new 'never trusts the excursion page's origin, in an excursion or after it' + 11 pre-existing origin tests. Device check on a development build against a test OIDC provider: NOT RUN, NOT MEASURED (no device here), handed to the maintainer in 需要你做的.", "gates": "Local, head 0c0706a, exit codes captured before any pipe (recorded in scratchpad issue-155/gate-exit-codes.txt): npx tsc --noEmit exit 0; npx eslint . --ext .ts,.tsx exit 0; npx jest --ci --coverage exit 0 (lock VERDICT command-exit 0); cold component run exit 0. CI on PR #176 head 0c0706a, check-runs read once: 'Code Quality' (ci.yml, runs jest --ci --coverage) completed · success; 'Lint & Type Check' (lint-typecheck.yml, tsc + eslint) completed · success. dispatch-gates.mjs not run: it answers for the objectstack tree only; the mobile gate list is hand-derived from its .github/workflows, as os-dev.md directs for a sister repo. Control-byte self-scan of the 23 changed files: no hits.", "dogfood_battery": "n/a — no ! in the PR title and no (narrowing) arm (Clause-②: no)", "mcp_calls": "0 — no MCP GitHub calls of any kind", "api_writes": "2 relay/REST writes: (1) pr_create through scripts/pm/fleet-write/dispatch.mjs (relay run 41550124706, POST /repos/objectstack-ai/mobile/pulls, draft, no labels, no assignees; read-back 22014 bytes sent = stored); (2) this os-dev-report comment through scripts/pm/post-stamped.mjs (POST /repos/objectstack-ai/mobile/issues/155/comments). Not REST: git pushes of claude/issue-155-idp-excursion: empty probe, 5 commit pushes, 1 --force-with-lease (trailer amend, see summary).", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [ "carrier: seat · two pre-existing navigation-policy observations, reported to the maintainer privately" ] }objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
ACCEPT → PR #176 (head
0c0706a). This is a security boundary, so the maintainer reviews the Threat model and merges; the seat does not land it. Next after merge: #158 (docs) waits only on #153.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Landed. #176 merged through the merge queue.
os-zhuangenqueued it after the Threat-model review. Themerge_grouprun passedCIandLint & Type Check. The card closedcompletedviaFixes #155, andpm:dispatchedis removed.Not covered here: IdPs that refuse embedded WebViews still wait for the ruling on #148. #158 (docs) now waits only on #153.