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
Managed deployment: read the server URL (and an optional lock) from MDM managed app configuration #157
Design, managed path. Enterprises distribute the app through MDM and expect the server to be preconfigured, so employees type nothing. Both platforms have a standard channel:
iOS: Managed App Configuration (the com.apple.configuration.managed dictionary in user defaults);
White-label builds already have the build-time EXPO_PUBLIC_DEFAULT_SERVER_URL.
Ask
Read managed keys on launch and on change:
serverUrl (string);
lockServer (boolean);
optionally trustedHosts (comma-separated, same meaning as the build-time variable).
This needs a native module or Expo config plugin. Pick a maintained one or write a small module, and name it in the PR. Document the Android restrictions schema (app_restrictions.xml).
Behaviour.
With serverUrl present, skip the welcome, address and scan screens: run the probe and confirm, then go to sign-in.
With lockServer, hide 添加组织或公司服务器 and the remove action for that connection.
Managed values win over stored ones.
A managed URL that fails the probe shows the error with "请联系 IT 管理员", and no way to edit when locked.
Precedence: managed config > build-time default > user entry.
Acceptance
Unit tests cover precedence and lock behaviour with the native read mocked.
docs/DEPLOYMENT.md has an MDM section with the key table and a sample for Intune / Jamf (generic, no vendor-specific claims beyond the standard channels).
Claim: PM loop round 1
Session: session_01QSeKYnW1Xf6HDSWz1eGAqv
Account: os-justin
Branch: claude/issue-157-managed-config
Worktree: mobile-issue-157
Domain: n/a — single-lane repo (repo:mobile)
Seat: repo:mobile#1
File surface: lib/connections/**, stores/server-store.ts, app/server-config.tsx, components/shell/ConnectionSwitcher.tsx (lock hides add and remove), a new managed-config module under lib/, app.config.ts, package.json and pnpm-lock.yaml (native module or config plugin), Android app_restrictions.xml through a config plugin, locales/*.json (all ten), __tests__/**, docs/DEPLOYMENT.md, docs/HYBRID-SHELL.md, .changeset/157-managed-config.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: 6576867957
Premise: holds. main at 678fbe9 has no managed-config reader (no hits for managed/RestrictionsManager); connections are in lib/connections/.
Serial constraints cleared: #174 and #155 are dispatched in the same round. Overlap with #174 is possible in lib/connections/** (ordinary concurrency; the PR that lands second reconciles). #155 does not touch this surface.
{
"issue": 157,
"status": "done",
"branch": "claude/issue-157-managed-config",
"pr": "https://github.com/objectstack-ai/mobile/pull/175",
"session": "session_01QSeKYnW1Xf6HDSWz1eGAqv",
"premise_still_valid": true,
"summary": "Managed deployment: a local Expo module (modules/managed-config, Swift + Kotlin, no npm dependency) reads serverUrl and lockServer from iOS com.apple.configuration.managed and Android RestrictionsManager. Its config plugin writes the Android app_restrictions.xml. The server store makes the saved managed connection current at launch and on a change. The picker skips the address step and scanner, but always probes and shows the confirm step. On failure it shows the error with 请联系 IT 管理员 and Try again, and offers no edit when locked. When locked, the switcher hides add and the managed row's Remove, removal and resetServer() are refused, and connect links cannot bring another address in. trustedHosts was not built (open question). Native code needs a dev-client or store rebuild, which was not compiled here (no Android SDK or Xcode in the container). The device check is owed to the maintainer and was not verified.",
"tests": "head 6f7f583. npx jest --ci --coverage: exit 0, Test Suites 42/42, Tests 742/742; lib/managed-config 100% statements/branches/functions/lines, server-store.ts 98.59% statements, ConnectionSwitcher.tsx 100% statements / 95% branches. Cold run npx jest --ci --no-cache --maxWorkers=2 on ServerConfigScreen, ConnectionSwitcher, ConsoleScreen and lib/managed-config: exit 0, 6/6 suites, 102/102 tests. The same subset with --coverage exits 1 only on the global ./lib/ and ./stores/ thresholds, because it is a 6-file subset (all tests passed); this is declared. Ablation 1: switcher removable={connection.host !== lockedHost} set to removable={true} (anchor count 1 before, 0 after) makes ConnectionSwitcher.test exit 1, 1 of 11 failing. Ablation 2: removing the reset() lock guard (anchor 1 to 0) makes server-store + ConsoleScreen exit 1, 2 of 31 failing. Both were restored with git checkout HEAD -- PATH; git diff HEAD is empty and the HEAD blob hash matches. expo-modules-autolinking resolve finds ManagedConfig for android and apple (exit 0/0). expo prebuild --platform android in a throwaway copy exits 0 and writes app_restrictions.xml, the APP_RESTRICTIONS meta-data and 4 strings.",
"gates": "local, head 6f7f583, exit codes captured before any pipe: pnpm install --frozen-lockfile exit=0; npx tsc --noEmit exit=0; npx eslint . --ext .ts,.tsx exit=0; npx jest --ci --coverage exit=0; npx jest --ci --no-cache --maxWorkers=2 (touched component suites + lib/managed-config) exit=0. Baseline on 678fbe9: tsc exit=0, eslint exit=0. PR #175 check-runs on 6f7f583 when read: 'Lint & Type Check' completed success; 'Code Quality' in_progress. Other CI runs (ci.yml Jest) had not appeared when read: NOT MEASURED, reason: not reported yet. dispatch-gates.mjs --commands is not applicable: mobile has no scripts/pm/dispatch-gates.mjs, and the gate list came from the dispatch order.",
"dogfood_battery": "n/a — no ! in the PR title and Clause-②: no without a (narrowing) arm",
"mcp_calls": "0",
"api_writes": "2 — relay writes: (1) fleet-write pr_create to objectstack-ai/mobile (POST /repos/objectstack-ai/mobile/pulls, draft), run 41548884182 success, read-back identical 12657/12657 bytes; (2) post-stamped report comment, POST /repos/objectstack-ai/mobile/issues/157/comments. Plus git push of the branch (not REST).",
"line_budget": "35 files, +1851 / -51 (git diff --numstat origin/main...HEAD at 6f7f583); under the 3,000 changed-line class (c) limit",
"files_changed": [
".changeset/157-managed-config.md",
"__tests__/components/ConnectionSwitcher.test.tsx",
"__tests__/components/ConsoleScreen.test.tsx",
"__tests__/components/ServerConfigScreen.test.tsx",
"__tests__/lib/connections/model.test.ts",
"__tests__/lib/managed-config/model.test.ts",
"__tests__/lib/managed-config/native.test.ts",
"__tests__/lib/managed-config/plugin.test.ts",
"__tests__/lib/server-store.test.ts",
"app.config.ts",
"app/console.tsx",
"app/server-config.tsx",
"components/shell/ConnectionSwitcher.tsx",
"docs/DEPLOYMENT.md",
"docs/HYBRID-SHELL.md",
"lib/connections/model.ts",
"lib/managed-config/model.ts",
"lib/managed-config/native.ts",
"locales/ar.json",
"locales/de.json",
"locales/en.json",
"locales/es.json",
"locales/fr.json",
"locales/ja.json",
"locales/ko.json",
"locales/pt.json",
"locales/ru.json",
"locales/zh.json",
"modules/managed-config/android/build.gradle",
"modules/managed-config/android/src/main/java/ai/objectos/managedconfig/ManagedConfigModule.kt",
"modules/managed-config/app.plugin.js",
"modules/managed-config/expo-module.config.json",
"modules/managed-config/ios/ManagedConfig.podspec",
"modules/managed-config/ios/ManagedConfigModule.swift",
"stores/server-store.ts"
],
"deviations": [
"landing point outside the claim surface: modules/managed-config/** (native module + config plugin). Reason: Expo autolinking's default nativeModulesDir is ./modules (expo-modules-autolinking normalizeAutolinkingOptions); the JS reader is under lib/managed-config/ as claimed",
"landing point outside the claim surface: app/console.tsx. Reason: the Console screen renders the switcher (lockedHost prop), handles removal and resetServer(), and owns the 'no server, go to the picker' redirect, which now also covers a pending managed address",
"package.json and pnpm-lock.yaml, though claimed, are unchanged: no npm dependency was added",
"commit trailers use the model-free pair from os-dev.md (Co-authored-by: Claude) instead of the harness reminder's model-named Co-Authored-By; the PR footer uses the os-dev.md session-URL form instead of the harness's Generated-with line",
"cold subset with --coverage exits 1 on global thresholds only; the subset's verdict is the cold run without --coverage (exit 0)"
],
"open_questions": [
{
"question": "Should trustedHosts be read from MDM? It would widen decideNavigation's 'host in trusted hosts, load in the WebView' row (lib/shell/navigation-policy.ts, fed through ConsoleWebView's trustedHosts prop from app/console.tsx). MDM-named hosts would then run in the main frame with the bridge bootstrap injected (non-identifying fields; installId and method answers stay origin-checked to the server) and keep their cookies in the shared WebView store.",
"options": [
"A: do not read it; the build's EXPO_PUBLIC_TRUSTED_HOSTS stays the only source (today's behaviour, zero work)",
"B: read it as an addition to the build list (small: about 20 lines plus a test; widens the navigation policy at runtime, from MDM)",
"C: read it only while lockServer is set, so an unlocked install cannot be widened (small, adds a coupling to explain)"
],
"recommendation": "A for now. 导航策略放行新来源 is the lane's human floor; B only on the maintainer's ruling, with the key added to app_restrictions.xml and the DEPLOYMENT key table in the same PR."
},
{
"question": "Under lockServer, should the user still be able to switch to other connections saved before the lock? The card's text hides only add and the managed connection's Remove, and that is what was built.",
"options": [
"A: keep as built: others stay switchable and removable (zero work)",
"B: while locked, show only the managed connection in the switcher (small: a filter in app/console.tsx plus a test)"
],
"recommendation": "A, because it matches the card text; B if IT expects the lock to pin the app to one server."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted, not filed — Apple's current UserDefaults.didChangeNotification doc says another process's change posts no notification, and the dotted managed key cannot be a KVO key path; the guaranteed path is the foreground re-read. Recorded in PR Acceptance notes, not a defect",
"carrier: none · noted, not filed — three existing test files (ConnectionSwitcher, ConsoleScreen, connections/model tests) are not prettier-clean on main 678fbe9; left untouched"
]
}
ACCEPT → PR #175 (head 6f7f583). The seat landed it through the merge queue under the ruling on #160. Native code was not compiled in the container: the next dev-client build is its first compile (maintainer device check). Open questions Q1 (trustedHosts, not built, human floor) and Q2 (lock scope, built as the card reads) are on the PR.
Landed (seat landing).#175 merged through the merge queue. The merge_group run passed CI and Lint & Type Check. The card closed completed via Fixes #157; pm:dispatched removed.
Native rebuild owed: the new modules/managed-config has never been compiled, so the next dev-client build is its first compile. Open questions Q1 and Q2 are on PR #175. #158 (docs) now waits only on #153 and #155.
Activity
objectstack-fleet commentedon Nov 2, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Gate #150 released. This card moves to
pm:blockedon #152 (the connection store) and #154 (the probe), typeenhancement.It also queues behind #151 on
app.config.ts.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-157-managed-configWorktree:
mobile-issue-157Domain:
n/a — single-lane repo (repo:mobile)Seat:
repo:mobile#1File surface:
lib/connections/**,stores/server-store.ts,app/server-config.tsx,components/shell/ConnectionSwitcher.tsx(lock hides add and remove), a new managed-config module underlib/,app.config.ts,package.jsonandpnpm-lock.yaml(native module or config plugin), Androidapp_restrictions.xmlthrough a config plugin,locales/*.json(all ten),__tests__/**,docs/DEPLOYMENT.md,docs/HYBRID-SHELL.md,.changeset/157-managed-config.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: highClause-②: no
Metadata-protocol: none
Responsibility:
n/a — not a defect cardThread-read: 6576867957
Premise: holds.
mainat678fbe9has no managed-config reader (no hits formanaged/RestrictionsManager); connections are inlib/connections/.Serial constraints cleared: #174 and #155 are dispatched in the same round. Overlap with #174 is possible in
lib/connections/**(ordinary concurrency; the PR that lands second reconciles). #155 does not touch this surface.objectstack-fleet commentedon Nov 2, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
os-dev-report
{ "issue": 157, "status": "done", "branch": "claude/issue-157-managed-config", "pr": "https://github.com/objectstack-ai/mobile/pull/175", "session": "session_01QSeKYnW1Xf6HDSWz1eGAqv", "premise_still_valid": true, "summary": "Managed deployment: a local Expo module (modules/managed-config, Swift + Kotlin, no npm dependency) reads serverUrl and lockServer from iOS com.apple.configuration.managed and Android RestrictionsManager. Its config plugin writes the Android app_restrictions.xml. The server store makes the saved managed connection current at launch and on a change. The picker skips the address step and scanner, but always probes and shows the confirm step. On failure it shows the error with 请联系 IT 管理员 and Try again, and offers no edit when locked. When locked, the switcher hides add and the managed row's Remove, removal and resetServer() are refused, and connect links cannot bring another address in. trustedHosts was not built (open question). Native code needs a dev-client or store rebuild, which was not compiled here (no Android SDK or Xcode in the container). The device check is owed to the maintainer and was not verified.", "tests": "head 6f7f583. npx jest --ci --coverage: exit 0, Test Suites 42/42, Tests 742/742; lib/managed-config 100% statements/branches/functions/lines, server-store.ts 98.59% statements, ConnectionSwitcher.tsx 100% statements / 95% branches. Cold run npx jest --ci --no-cache --maxWorkers=2 on ServerConfigScreen, ConnectionSwitcher, ConsoleScreen and lib/managed-config: exit 0, 6/6 suites, 102/102 tests. The same subset with --coverage exits 1 only on the global ./lib/ and ./stores/ thresholds, because it is a 6-file subset (all tests passed); this is declared. Ablation 1: switcher removable={connection.host !== lockedHost} set to removable={true} (anchor count 1 before, 0 after) makes ConnectionSwitcher.test exit 1, 1 of 11 failing. Ablation 2: removing the reset() lock guard (anchor 1 to 0) makes server-store + ConsoleScreen exit 1, 2 of 31 failing. Both were restored with git checkout HEAD -- PATH; git diff HEAD is empty and the HEAD blob hash matches. expo-modules-autolinking resolve finds ManagedConfig for android and apple (exit 0/0). expo prebuild --platform android in a throwaway copy exits 0 and writes app_restrictions.xml, the APP_RESTRICTIONS meta-data and 4 strings.", "gates": "local, head 6f7f583, exit codes captured before any pipe: pnpm install --frozen-lockfile exit=0; npx tsc --noEmit exit=0; npx eslint . --ext .ts,.tsx exit=0; npx jest --ci --coverage exit=0; npx jest --ci --no-cache --maxWorkers=2 (touched component suites + lib/managed-config) exit=0. Baseline on 678fbe9: tsc exit=0, eslint exit=0. PR #175 check-runs on 6f7f583 when read: 'Lint & Type Check' completed success; 'Code Quality' in_progress. Other CI runs (ci.yml Jest) had not appeared when read: NOT MEASURED, reason: not reported yet. dispatch-gates.mjs --commands is not applicable: mobile has no scripts/pm/dispatch-gates.mjs, and the gate list came from the dispatch order.", "dogfood_battery": "n/a — no ! in the PR title and Clause-②: no without a (narrowing) arm", "mcp_calls": "0", "api_writes": "2 — relay writes: (1) fleet-write pr_create to objectstack-ai/mobile (POST /repos/objectstack-ai/mobile/pulls, draft), run 41548884182 success, read-back identical 12657/12657 bytes; (2) post-stamped report comment, POST /repos/objectstack-ai/mobile/issues/157/comments. Plus git push of the branch (not REST).", "line_budget": "35 files, +1851 / -51 (git diff --numstat origin/main...HEAD at 6f7f583); under the 3,000 changed-line class (c) limit", "files_changed": [ ".changeset/157-managed-config.md", "__tests__/components/ConnectionSwitcher.test.tsx", "__tests__/components/ConsoleScreen.test.tsx", "__tests__/components/ServerConfigScreen.test.tsx", "__tests__/lib/connections/model.test.ts", "__tests__/lib/managed-config/model.test.ts", "__tests__/lib/managed-config/native.test.ts", "__tests__/lib/managed-config/plugin.test.ts", "__tests__/lib/server-store.test.ts", "app.config.ts", "app/console.tsx", "app/server-config.tsx", "components/shell/ConnectionSwitcher.tsx", "docs/DEPLOYMENT.md", "docs/HYBRID-SHELL.md", "lib/connections/model.ts", "lib/managed-config/model.ts", "lib/managed-config/native.ts", "locales/ar.json", "locales/de.json", "locales/en.json", "locales/es.json", "locales/fr.json", "locales/ja.json", "locales/ko.json", "locales/pt.json", "locales/ru.json", "locales/zh.json", "modules/managed-config/android/build.gradle", "modules/managed-config/android/src/main/java/ai/objectos/managedconfig/ManagedConfigModule.kt", "modules/managed-config/app.plugin.js", "modules/managed-config/expo-module.config.json", "modules/managed-config/ios/ManagedConfig.podspec", "modules/managed-config/ios/ManagedConfigModule.swift", "stores/server-store.ts" ], "deviations": [ "landing point outside the claim surface: modules/managed-config/** (native module + config plugin). Reason: Expo autolinking's default nativeModulesDir is ./modules (expo-modules-autolinking normalizeAutolinkingOptions); the JS reader is under lib/managed-config/ as claimed", "landing point outside the claim surface: app/console.tsx. Reason: the Console screen renders the switcher (lockedHost prop), handles removal and resetServer(), and owns the 'no server, go to the picker' redirect, which now also covers a pending managed address", "package.json and pnpm-lock.yaml, though claimed, are unchanged: no npm dependency was added", "commit trailers use the model-free pair from os-dev.md (Co-authored-by: Claude) instead of the harness reminder's model-named Co-Authored-By; the PR footer uses the os-dev.md session-URL form instead of the harness's Generated-with line", "cold subset with --coverage exits 1 on global thresholds only; the subset's verdict is the cold run without --coverage (exit 0)" ], "open_questions": [ { "question": "Should trustedHosts be read from MDM? It would widen decideNavigation's 'host in trusted hosts, load in the WebView' row (lib/shell/navigation-policy.ts, fed through ConsoleWebView's trustedHosts prop from app/console.tsx). MDM-named hosts would then run in the main frame with the bridge bootstrap injected (non-identifying fields; installId and method answers stay origin-checked to the server) and keep their cookies in the shared WebView store.", "options": [ "A: do not read it; the build's EXPO_PUBLIC_TRUSTED_HOSTS stays the only source (today's behaviour, zero work)", "B: read it as an addition to the build list (small: about 20 lines plus a test; widens the navigation policy at runtime, from MDM)", "C: read it only while lockServer is set, so an unlocked install cannot be widened (small, adds a coupling to explain)" ], "recommendation": "A for now. 导航策略放行新来源 is the lane's human floor; B only on the maintainer's ruling, with the key added to app_restrictions.xml and the DEPLOYMENT key table in the same PR." }, { "question": "Under lockServer, should the user still be able to switch to other connections saved before the lock? The card's text hides only add and the managed connection's Remove, and that is what was built.", "options": [ "A: keep as built: others stay switchable and removable (zero work)", "B: while locked, show only the managed connection in the switcher (small: a filter in app/console.tsx plus a test)" ], "recommendation": "A, because it matches the card text; B if IT expects the lock to pin the app to one server." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted, not filed — Apple's current UserDefaults.didChangeNotification doc says another process's change posts no notification, and the dotted managed key cannot be a KVO key path; the guaranteed path is the foreground re-read. Recorded in PR Acceptance notes, not a defect", "carrier: none · noted, not filed — three existing test files (ConnectionSwitcher, ConsoleScreen, connections/model tests) are not prettier-clean on main 678fbe9; left untouched" ] }objectstack-fleet commentedon Nov 2, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
ACCEPT → PR #175 (head
6f7f583). The seat landed it through the merge queue under the ruling on #160. Native code was not compiled in the container: the next dev-client build is its first compile (maintainer device check). Open questions Q1 (trustedHosts, not built, human floor) and Q2 (lock scope, built as the card reads) are on the PR.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Landed (seat landing). #175 merged through the merge queue. The
merge_grouprun passedCIandLint & Type Check. The card closedcompletedviaFixes #157;pm:dispatchedremoved.Native rebuild owed: the new
modules/managed-confighas never been compiled, so the next dev-client build is its first compile. Open questions Q1 and Q2 are on PR #175. #158 (docs) now waits only on #153 and #155.objectstack-fleet commentedon Nov 3, 2026
repo:mobile#1 · os-justin · session_01QSeKYnW1Xf6HDSWz1eGAqv
Ruling: batch #2 item 2 · letter A · maintainer 「批 #2 同意」 2026-11-03T08:21Z
Ruling: batch #2 item 3 · letter B · maintainer 「批 #2 同意」 2026-11-03T08:21Z
The maintainer accepted batch #2's recommendations in the session
session_01QSeKYnW1Xf6HDSWz1eGAqv.trustedHosts. Trusted hosts stay build-time only (EXPO_PUBLIC_TRUSTED_HOSTS).docs/DEPLOYMENT.md's MDM key table will say it is not supported. The doc line rides the item-3 PR. Effort: none.lockServeris set, the connection switcher shows only the managed connection. The other saved connections are hidden, not deleted, and come back when the lock is lifted. Effort: mobile 1 small PR. Precedent: feat: read the server from MDM managed app configuration (serverUrl, lockServer) #175. Filed as its own card and dispatched now.