Account-only identity + monitoring fixes (account↔computer pairing)#91
Open
xmqywx wants to merge 29 commits into
Open
Account-only identity + monitoring fixes (account↔computer pairing)#91xmqywx wants to merge 29 commits into
xmqywx wants to merge 29 commits into
Conversation
- New AgentSettingsTab: status card (binary/process/health endpoint), install/start/stop controls via launchctl, 4 unavailable capability rows - SystemSettingsView: add .agent tab; expose SectionLabel/SettingsListCard/ SettingRow as internal (was private) so AgentSettingsTab can reuse them - Build verified: BUILD SUCCEEDED, no new compiler errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per @运维 review finding #1: unconditional KeepAlive + kill SIGTERM means launchd immediately restarts the process — Stop never actually stops. - stopAgent(): `launchctl bootout user/<uid>/<label>` (unloads job) - startAgent(): `launchctl bootstrap user/<uid> <plistPath>` (reloads job) after a bootout the job is unloaded; kickstart only works when loaded - launchAgentPlist(): add ExitTimeOut=15 (§2/§3 drain-deadline alignment) - Phase 2 NOTE added in stopAgent(): full IPC drain-safe Stop is Phase 2 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- isJobLoaded(): domain-aware `launchctl print user/<uid>/<label>` (exit 0 =
loaded), consistent with bootstrap/bootout/kickstart domain specifiers
- startAgent(): loaded → kickstart -k (restart without re-registering plist);
not loaded → bootstrap (registers plist). Handles the crash-recovery edge
case where the job is loaded but the process exited.
- stopAgent(): requestDrain(deadlineMs:) stub called BEFORE bootout, matching
spec §5 drain-then-stop order so Phase 2 only needs to fill in the body
- requestDrain(): explicit no-op with Phase 2 wire-up comment; does NOT claim
to be drain-safe; UI feedback ("Stopping…"/"Stopped") unchanged
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iable) @运維 empirically tested: `launchctl print user/<uid>/<label>` returns exit 113 even for a loaded job in non-GUI shell context. `launchctl list <label>` correctly returns exit 0 for loaded / non-zero for absent on the same machine. Reverts isJobLoaded() to the tested `launchctl list <label>` form and documents the print-vs-list finding in the comment so the next engineer doesn't repeat the same mistake. GUI app context caveat noted for future end-to-end smoke. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the loaded-check + branch pattern with the idiomatic bootstrap-first approach, eliminating the isJobLoaded() helper and the TOCTOU race it carried. Motivation: @运维 empirically confirmed that launchctl exit codes are unreliable from a shell context (shell cannot enter the user/$uid domain), so neither `list` nor `print` exit-code tests can be trusted without in-app smoke testing. The bootstrap-first pattern sidesteps the problem entirely: - bootstrap succeeds (exit 0) → job registered and started, done. - bootstrap returns non-zero (already loaded) → kickstart -k to restart. Removes isJobLoaded() (no longer needed), cleans up the retracted "Empirically verified" comment, and makes startAgent() properly idiomatic. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… rows + Logs card Implements docs/productization/agent-tab-phase2-state-design.md (#90 spec). - AgentLifecyclePhase enum: 11 states (notInstalled / installedUnloaded / loadedStopped / starting / runningHealthy / runningUnhealthy / stopping / draining / error / checking / unknown) - AgentLifecycleSnapshot view model: binaryExists, launchAgentLoaded, processRunning, healthReachable, socketExists, logFileExists, phase, lastCheckedAt, lastDiagnostic - LaunchAgent loaded probe: `launchctl list io.miomioos.mio-agent` (exits 0 if loaded in GUI app domain; shell context is expected to return non-zero) - Status card: dot/spinner + primaryLabel + descriptionText + lastChecked - Lifecycle detail rows: Binary / LaunchAgent / Process / Health / Socket with state pill labels (Installed/Missing, Loaded/Unloaded, Running/Stopped, OK/Failed, Pending Phase 2) - Controls: accent fill for Install/Start, outline for Stop, hierarchy matches spec — disabled uses controlFill + 0.55 opacity (not <0.5) - Logs card: Open log file (NSWorkspace) + Copy last 100 lines (pasteboard), empty state + logFileExists probe - Phase 1 stop copy: honest "Drain-safe stop is coming in Phase 2" — no false drain-safe promise - Error states: controlled copy per spec §10 (no raw shell output) - IPC drain stub preserved with TODO(Phase2) comment Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Per @运维 #93 review follow-up: install uses `bootstrap user/$uid` (user domain) while probe uses `launchctl list` (which may resolve to gui/<uid> domain in GUI app context). Probe may show false-negative if domains differ. Added TODO for #79 smoke verification. Control flow (bootstrap-first/kickstart) is unaffected — this is display-only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…omain + real launchctl errors Root causes (both confirmed by @运维 live on-machine): 1. launchctl domain bug: all bootstrap/kickstart/bootout calls used user/$uid but LaunchAgents in ~/Library/LaunchAgents/ must be managed in gui/$uid (the GUI session launchd domain). user/$uid bootstrap fails with "Bootstrap failed: 5" from a GUI app context. 2. Config missing bug: ~/.mio/agent.json not present → agent Fatals immediately on start, launchd KeepAlive restart-loops but never succeeds. Fixes: - Add AgentLifecyclePhase.notConfigured: detected when binary exists but agent.json missing. Start button hidden when phase == .notConfigured; controls card shows "mio-agent login" command with copy button so user has actionable next step. - Add configPath / mioDir constants; configExists field in AgentLifecycleSnapshot. - Update refreshStatus() to probe ~/.mio/agent.json; phase determination checks config after binary. - Fix all launchctl calls: user/$uid → gui/$uid (installAgent, startAgent, stopAgent). - Capture launchctl stderr in shellRun() (now returns (Int32, String)); use real error text in snap.lastDiagnostic instead of generic "Could not start the loaded job. Check Logs." - Log buttons always enabled: openLogFile() falls back to opening ~/.mio/ dir when log missing; copyLastLinesOfLog() copies last diagnostic when log file doesn't exist. - Improve "No agent log yet" copy to explain WHY (agent hasn't started successfully yet). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ctions - Lifecycle detail rows: add Config row between Binary and LaunchAgent showing ~/.mio/agent.json Present/Missing state (gearshape.fill icon, warning color when missing) - notConfigured hint copy: mention server URL prompt explicitly "Run this command in Terminal, then enter your server URL (e.g. https://mio.wdao.chat) when prompted" — addresses @nova review finding that command alone leaves user stuck at the first login prompt without knowing what to type Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Localization.swift: add full // MARK: - Mio Agent Settings block — 12 phase labels, 11 phase descriptions, tab description, 4 section labels, 6 lifecycle row names, 13 state pills, 5 button labels, setup-hint strings, logs-card strings, 4 capability rows + Soon pill, and 8 action-diagnostic strings (4 static + 4 dynamic functions). Also adds agentDiagErrorFallback, agentDiagNoLog, agentLastChecked(). AgentSettingsTab.swift: replace every hardcoded English string with the matching L10n.* call — controls card buttons (Install/Start/Stop), setup hint text + copy button tooltip, logs card hint + button labels + empty-log text, all 4 capabilities rows + Soon pill, and all diagnostic messages in installAgent/startAgent/stopAgent/copyLastLines. lastCheckedLabel and overallStatusCard error fallback also localized. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Root path returns 404 on the mio-agent health server. Only GET /health returns 200. pingHealth() now returns a tuple (reachable, error?) so the actual HTTP status or connection error is surfaced in snap.lastDiagnostic when probe fails (runningUnhealthy). Also raised probe timeout from 0.5s to 2.0s. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Provides a correct replacement procedure for /Applications/Mio Island.app: 1. Validates source bundle ID (com.codeisland.app) before touching anything 2. Quits any running "Mio Island" process (graceful SIGTERM → SIGKILL fallback) 3. rm -rf old target first — critical: cp -R into an existing .app nests source inside the destination instead of replacing it (2026-05-22 incident) 4. cp -R source into /Applications/ (parent dir, not target path) 5. Clears com.apple.quarantine so macOS doesn't block launch 6. Verifies installed bundle ID + prints version info Usage: ./scripts/safe-install-app.sh /path/to/built/"Mio Island.app" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Step 7: assert $TARGET/<source-basename> does not exist post-install. If nesting occurred (cp -R source INTO existing target), this exits 1 with a clear error message pointing to the nested path to remove. - Print lsappinfo command after successful install so user can verify the running process comes from /Applications/ not a DerivedData path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace Bundle.main.path(forResource:"mio-agent") stub with a real download flow via MioAgentDistribution: MioAgentDistribution.swift (new): - Fetch SHASUMS256.txt from the pinned GitHub Release tag - Download darwin-arm64 tarball (mio-agent-<ver>-darwin-arm64.tar.gz) - Verify SHA-256 against the manifest entry - Extract with /usr/bin/tar into a per-install temp directory - Atomically copy binary to destination (no-overwrite-on-fail contract: destination is untouched until all verification passes) - Uses CryptoKit.SHA256; parses coreutils SHASUMS256.txt format AgentSettingsTab.installAgent(): - Removed Bundle.main.path lookup (Resources phase stays empty intentionally) - Calls MioAgentDistribution.downloadAndInstall(to:onProgress:) which surfaces real-time progress strings into snap.lastDiagnostic - Rest of the flow (codesign, plist write, launchctl bootstrap) unchanged Pin: v0.1.0 (git_commit fcaeea0, contains #115 machine-api-v1-prefix and socketio-control-path required fixes per #120 manifest) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After a successful install, --launch-verify opens the target .app and polls lsappinfo (up to 10 s) to assert the running instance bundle path is /Applications/Mio Island.app — not a stale DerivedData or quarantined copy. Exits non-zero if the wrong binary is running. Also adds proper positional + flag argument parsing so unknown flags fail loudly instead of being silently ignored. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
🔴 required_fixes anti-rollback (task #118/#120):
- Download manifest JSON before fetching SHASUMS/tarball.
- Assert required_fixes ⊇ {machine-api-v1-prefix, socketio-control-path}.
- Reject install with requiredFixesMissing error if any fix is absent —
even if the tarball SHA-256 matches — preventing a pinned-but-stale
release from silently installing a buggy daemon.
🟡 Staging + atomic replace (true no-overwrite-on-fail):
- Copy verified binary to dest+".staging" first; old binary untouched.
- FileManager.replaceItemAt maps to renameat(2) on APFS — atomic swap.
- First install (no existing binary): moveItem staging → dest.
- Old binary survives any failure before the atomic swap completes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…es doc
Bug: findFile(named: "mio-agent") would never match because build-release.mjs
packs the SEA binary as "mio-agent-<ver>-darwin-arm64" (not plain "mio-agent").
Found during #122 Swift/TS consistency review: TS daemonDownload.ts correctly
uses the versioned name; Swift was wrong.
Fix: introduce binaryFilename computed var ("mio-agent-<ver>-darwin-arm64")
and use it in Step 6 instead of the hardcoded "mio-agent" string.
Also unifies the caller-responsibilities docstring (line 119 inconsistency
flagged by @运维 in #121 review): ad-hoc vs Developer-ID codesigning caveat
now matches the step-7 comment exactly; launchctl domain note added (gui/<uid>).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… config UI + dual QR R2.1: MioAgentEnroller shells out to the installed mio-agent binary (login + enroll), letting it write its own Keychain/agent.json (no reimplementation); AgentSettingsTab config UI for the autonomous_agent block; retires the .notConfigured/coming-soon stub so a workspace daemon is configurable in-app instead of via Terminal. R2.7: PairPhoneView emits a dual-purpose QR (monitoring shortCode + enrollment intent, kind:both). xcodebuild macOS: BUILD SUCCEEDED (verified by me). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…l error line isn't lost Prior review found a fast-exit race: the per-chunk ingest Tasks and the termination Task hop onto the main actor with no ordering guarantee, so a fast-failing 'mio-agent login' could show a generic 'exited N' instead of mio-agent's real reason (the diagnostic the failed-UI exists to show). handleTermination now detaches the handlers + synchronously drains both pipes into lastLine BEFORE building the error detail. (Review verdict otherwise: interop sound — agent.json + Keychain + argv all match login.ts/keychain.ts; no crash/security/corruption bug.) xcodebuild macOS: BUILD SUCCEEDED. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…settings tab
- MioAgentDistribution.pinnedVersion 0.2.1 → 0.2.3 (0.2.1 was unpublished; 0.2.3 is
the published protected build with the isEntryPoint npx-launch fix).
- Remove non-functional placeholders from AgentSettingsTab (this is a product, no fakes):
- Lifecycle "socket" row: hardcoded `pending` status that ignored the real probe.
- Entire "Upcoming capabilities" section: 4 static "Soon" placeholder rows + the
unavailableRow helper.
- requestDrain() IPC no-op (the "drain in-flight before stop" was a no-op stub);
bootout stop path is real and unchanged.
- Orphaned socket detection (socketExists field, socketPath, the probe + assignment).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…eLight)
PairQRPayload built a bespoke JSON {server,code,enroll_intent_id,enroll_code,kind}
that CodeLight never parsed for enrollment (it only reads the mio://enroll URL that
'mio-agent login' emits) — so phone scans never bound a workspace. Emit the SAME
mio://enroll/<id>?code&server&name&platform&arch deeplink (already held verbatim in
MioEnrollIntent.deeplink), carrying the monitoring shortCode as optional &monitor_code
for the #118 one-scan-does-both. Monitor-only falls back to the legacy {server,code} JSON.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ssociates) ensureWorkspaceIntentForQR bailed on `guard !configExists` — so an already-enrolled Mac (even one bound to a stale/archived workroom) refused to generate a workspace enrollment QR, leaving the Pair-iPhone code empty. 'Already enrolled' is not a reason to refuse a fresh QR: re-scanning must be able to re-associate to a live workspace/account. Drop the configExists gate; keep the idle/in-flight + launchable guards. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…k enroll ServerConnection now reads the mio-agent machine_token from Keychain (via security, no ACL prompt) and exchanges it at POST /v1/auth/machine for a monitoring device JWT + shortCode, so the dual QR carries monitor_code and one scan pairs both workspace and monitoring. Keypair /v1/auth kept as fallback. MioAgentLauncher resolves npx-cached or installed binary; MioAgentEnroller retries on the installed binary when npx exits non-zero with no intent. Removed the temporary /tmp/mio_enroll_debug.log dbg instrumentation. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…factor) Drops the legacy device-keypair /v1/auth fallback; the Mac authenticates solely via its enrollment machine_token at the de-ownered /v1/auth/machine (ownership now lives in server-side AccountComputerLink set by a phone scan). The device JWT is a non-identity push/transport handle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
findNpx() returned the first npx on disk, letting a stale /usr/local/bin/npx
from an old global npm 6 win over the user's modern nvm/Homebrew npx. npm 6's
npx mis-resolves `npx -y <pkg> <subcmd>`: it runs <subcmd> as a standalone
command instead of passing it to the package's bin — so `mio-agent login`
fell through to the system /usr/bin/login ("usage: login -f …") and
`mio-agent run` installed + ran the unrelated `runjs` package (crash-looping
the LaunchAgent and flooding ~/.mio/agent.log). Net effect: mio-agent never
ran → no machine_token → Pair iPhone stuck on "生成配对码中…".
Reject any npx whose `--version` major is < 7 (npm 6.x) so resolve() falls
through to a modern npx, or to the SEA binary if none. Verified on the repro
machine: skips 6.14.11, picks nvm 10.8.2. Fixes both enrollment (login) and
the daemon (run) since both go through resolve().
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Agent tab decided "daemon running" via `pgrep -x mio-agent`, which only matches a process literally named "mio-agent" (the SEA-binary launch path). Under the npx path the daemon runs as `node …/mio-agent run`, so the check never matched: the tab showed the daemon "stopped/offline" and skipped the /health probe entirely, even though the daemon was running and healthy (127.0.0.1:7878/health → 200). Switch to `pgrep -f "mio-agent run"`, which matches both the SEA binary and the npx `node`/`npm exec` processes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Account-only identity refactor for the Mac monitoring/enrollment side, plus enrollment robustness.
What's here
machine_tokenat the de-owneredPOST /v1/auth/machine; ownership lives in the server-sideAccountComputerLink(set by a phone scan), not on the device.security, no prompt) so one QR pairs workspace + monitoring.MioAgentLauncherresolves npx-cached or installed binary; retries on the installed binary when npx exits non-zero with no intent. Removed temporary dbg instrumentation.main(v3.0.4) — it does NOT contain the team's token-usage-meter / PR feat(updater): add toggle to disable automatic update checks (#87) #88–feat(usage-bar): cross-model token-consumption meter + Catppuccin theme #90 work. Merging requires reconciling with v3.0.4 (expect conflicts).docs/specs/2026-06-03-account-only-identity-design.md./v1/pairing/linksclient calls 404 gracefully.🤖 Generated with Claude Code