mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
iOS port M2: iOS app skeleton — onboarding, Citadel wrapper, Keychain, Dashboard
First iOS phase. Delivers all the code needed to build + TestFlight a
functional v1 iOS app (onboarding with SSH-key generate / import +
real Citadel-backed connection test; persistent Keychain key +
UserDefaults server config; placeholder Dashboard) — but NOT the
scarf-ios.xcodeproj. Creating that from scratch by hand is too risky
without an iOS SDK to build against, so Alan creates it in Xcode's UI
following scarf/scarf-ios/SETUP.md (~5 minutes, one-time).
## ScarfCore additions (all Linux-testable)
Packages/ScarfCore/Sources/ScarfCore/Security/:
- SSHKey.swift — SSHKeyBundle + SSHKeyStore protocol
+ InMemorySSHKeyStore test actor
- IOSServerConfig.swift — IOSServerConfig + store protocol + mock;
toServerContext(id:) bridges to the
existing ServerContext so all ScarfCore
services work against an iOS config
- OnboardingState.swift — OnboardingStep enum + pure validators
(host, port, PEM shape, public-key parse)
- SSHConnectionTester.swift — protocol + error enum + mock
- OnboardingViewModel.swift — @Observable @MainActor state machine,
fully dependency-injected (key store /
config store / tester / generator closure)
## New Packages/ScarfIOS local SPM package
Depends on ScarfCore + Citadel (from: "0.7.0").
- KeychainSSHKeyStore.swift — real iOS Keychain storage
(kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, no iCloud
sync). Gated on canImport(Security) for Linux skip.
- UserDefaultsIOSServerConfigStore.swift — JSON-encoded single-key
persistence of IOSServerConfig.
- Ed25519KeyGenerator.swift — CryptoKit-backed Ed25519 minting.
Emits standard OpenSSH public-key lines (authorized_keys-ready).
Stores the private half in a compact SCARF ED25519 PRIVATE KEY
PEM shape that CitadelSSHService decodes back into a
Curve25519.Signing.PrivateKey. Non-interop with OpenSSH's
`BEGIN OPENSSH PRIVATE KEY` envelope — export flow for sharing
keys is deferred to a later phase.
- CitadelSSHService.swift — SSHConnectionTester conformance +
key-generation wrapper. Runs `echo scarf-ok` over a one-shot
Citadel exec for the onboarding connection test. One FIXME on
buildClientSettings because Citadel 0.7→0.9 shifted the
`.ed25519(...)` authentication-method variant name; every other
line is Citadel-version-independent. Gated on
canImport(Citadel) && canImport(CryptoKit).
## scarf/scarf-ios/ app source tree
- App/ScarfIOSApp.swift — @main, RootModel routes to
onboarding or dashboard based on
stored state.
- Onboarding/OnboardingRootView.swift — 8 sub-views, one per
OnboardingStep. Validated
server-details form, key-source
picker, generate / show-public
/ import / test / retry /
connected.
- Dashboard/DashboardView.swift — M2 placeholder: connected host
details + Disconnect button.
M3 replaces with real data.
## scarf/scarf-ios/SETUP.md
Step-by-step Xcode project creation:
- iOS 18 / iPhone-only / team 3Q6X2L86C4 / Bundle ID
com.scarf.scarf-ios / Swift 5 language mode.
- Wire Packages/ScarfCore + Packages/ScarfIOS (Citadel resolves
transitively).
- Replace Xcode's default scaffolded files with this source tree.
- Smoke-test procedure (simulator → physical iPhone).
- TestFlight upload steps.
- Troubleshooting for the known Citadel-variant-name drift.
## Test coverage (Linux, `swift test`)
M2OnboardingTests, 26 new tests (ScarfCore):
- SSHKeyBundle memberwise + display fingerprint
- InMemorySSHKeyStore + InMemoryIOSServerConfigStore round-trips
- IOSServerConfig.toServerContext bridging (with + without
remoteHome override)
- All OnboardingLogic validators (empty / whitespace / port range /
legacy-RSA rejection / public-key line parser)
- MockSSHConnectionTester scripting (success + failure)
- 10 OnboardingViewModel end-to-end paths: happy-path
save-and-test, invalid-host blocks advance, connection-failure
routes to .testFailed (and crucially does NOT save config),
retry-after-failure-works, import-happy, import-rejects-bad-PEM,
reset clears all state
ScarfIOSSmokeTests, 3 tests (Apple-only, won't run on Linux):
- Ed25519KeyGenerator bundle shape + base64 wire format
- OpenSSH public-key line byte-length pinned at 51 bytes
- Corrupted PEM rejection on round-trip decode
Running
docker run --rm -v $PWD/scarf/Packages/ScarfCore:/work -w /work swift:6.0 swift test
reports **88 / 88 passing** (62 pre-M2 + 26 new).
## Real bug caught in development
First pass of OnboardingViewModel had `confirmPublicKeyAdded()` set
`isWorking=true`, then call `runConnectionTest()` which bailed on
`!isWorking` — meaning the connection probe never ran and the config
was never saved. Caught by the end-to-end test. Fixed by extracting
the shared probe body into `performConnectionTest()` and letting
both entry points own their own `isWorking` transition.
## Manual validation still needed on Mac
1. Xcode project creation per SETUP.md — confirm the resulting
project builds cleanly.
2. Citadel 0.9.x authentication-method variant — verify the one
FIXME line in buildClientSettings.
3. End-to-end onboarding: simulator against `localhost:22` (or a
test host), then TestFlight → physical iPhone → real SSH host
with the shown public key in authorized_keys.
Updated scarf/docs/IOS_PORT_PLAN.md with M2's shipped scope, the
scope decision about NOT generating the xcodeproj, and the list of
rules M3+ can rely on (Citadel transport dispatch, ChannelFactory
hook, single-server invariant).
https://claude.ai/code/session_019yMRP6mwZWfzVrPTqevx2y
This commit is contained in:
+100
-2
@@ -454,8 +454,106 @@ Two real regressions caught by a pre-M1 audit, both silent:
|
||||
- **The `ChannelFactory` closure is `@Sendable` and async.** Any per-context setup (env enrichment, SSH handshake) happens inside the factory — not inside `ACPClient.start()`. That keeps `start()` boring and portable.
|
||||
- **`ACPClient` does not handle subprocess spontaneous exits via `terminationHandler`** anymore — it notices via channel-stream EOF. Pipe-EOF fires reliably when a Mac subprocess exits (OS closes the pipe). If a future phase sees "session hangs after crash" symptoms, add a `terminationHandler` inside `ProcessACPChannel` that explicitly finishes the `incoming` continuation.
|
||||
|
||||
### M2 — pending
|
||||
### M2 — pending
|
||||
### M2 — shipped (on `claude/ios-m2-skeleton` branch, separate PR from M0+M1)
|
||||
|
||||
**Scope note:** M2 delivers all the **code** needed for TestFlight
|
||||
(onboarding, Keychain, Citadel, Dashboard placeholder, unit tests) but
|
||||
**not** the `scarf-ios.xcodeproj`. Hand-editing ~600 lines of pbxproj
|
||||
from scratch is too high-risk without an iOS SDK to build against, so
|
||||
the Xcode target is created once in Xcode's UI following the written
|
||||
instructions in `scarf/scarf-ios/SETUP.md`. Total setup time: ~5
|
||||
minutes.
|
||||
|
||||
**Shipped — ScarfCore additions (testable on Linux):**
|
||||
|
||||
- `Security/SSHKey.swift` — `SSHKeyBundle` struct, `SSHKeyStore`
|
||||
protocol, `InMemorySSHKeyStore` test actor.
|
||||
- `Security/IOSServerConfig.swift` — `IOSServerConfig` struct
|
||||
(single-server v1), `IOSServerConfigStore` protocol,
|
||||
`InMemoryIOSServerConfigStore`. `toServerContext(id:)` bridges to
|
||||
the existing `ServerContext` type so the rest of ScarfCore's
|
||||
services work against an iOS-configured server unchanged.
|
||||
- `Security/OnboardingState.swift` — `OnboardingStep` enum,
|
||||
`OnboardingKeyChoice`, `OnboardingServerDetailsValidation`, pure
|
||||
functions `OnboardingLogic.validateServerDetails` /
|
||||
`authorizedKeysLine(for:)` / `isLikelyValidOpenSSHPrivateKey` /
|
||||
`parseOpenSSHPublicKeyLine`.
|
||||
- `Security/SSHConnectionTester.swift` — protocol +
|
||||
`SSHConnectionTestError` enum + `MockSSHConnectionTester`.
|
||||
- `Security/OnboardingViewModel.swift` — `@Observable @MainActor`
|
||||
state machine. Dependency-injects `SSHKeyStore`,
|
||||
`IOSServerConfigStore`, `SSHConnectionTester`, and a `KeyGenerator`
|
||||
closure so every transition is testable with mocks.
|
||||
|
||||
**Shipped — new `Packages/ScarfIOS` local SPM package:**
|
||||
|
||||
- Depends on local ScarfCore + remote Citadel (`from: "0.7.0"`).
|
||||
- `KeychainSSHKeyStore.swift` — real iOS Keychain impl
|
||||
(`kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly`, no iCloud sync).
|
||||
- `UserDefaultsIOSServerConfigStore.swift` — JSON in UserDefaults.
|
||||
- `Ed25519KeyGenerator.swift` — mints fresh Ed25519 keypairs via
|
||||
CryptoKit, emits standard OpenSSH public-key lines, stores the
|
||||
private half in a compact custom PEM that
|
||||
`CitadelSSHService` decodes back into
|
||||
`Curve25519.Signing.PrivateKey`.
|
||||
- `CitadelSSHService.swift` — `SSHConnectionTester` conformance +
|
||||
key-generation wrapper. Runs a one-shot SSH exec (`echo scarf-ok`)
|
||||
for the onboarding probe. Clearly-marked FIXME on the Citadel
|
||||
authentication-method call site because 0.7→0.9 has shifted the
|
||||
variant name; other than that one line, everything is
|
||||
Citadel-version-independent.
|
||||
|
||||
**Shipped — `scarf/scarf-ios/` iOS app source tree:**
|
||||
|
||||
- `App/ScarfIOSApp.swift` — `@main` + `RootModel` routing to
|
||||
onboarding / dashboard based on stored state.
|
||||
- `Onboarding/OnboardingRootView.swift` — 8 sub-views, one per
|
||||
`OnboardingStep`. Validated server-details form, key-source
|
||||
picker, generate / show / import / test / retry / connected.
|
||||
- `Dashboard/DashboardView.swift` — M2 placeholder: connected
|
||||
server details + Disconnect. M3 replaces with real data.
|
||||
|
||||
**Shipped — `scarf/scarf-ios/SETUP.md`:**
|
||||
|
||||
Step-by-step Xcode project creation + troubleshooting. Alan runs
|
||||
this once on a Mac (~5 minutes).
|
||||
|
||||
**Test coverage:**
|
||||
|
||||
- **ScarfCore (Linux):** 26 new tests covering key-bundle memberwise,
|
||||
both in-memory stores, config-to-ServerContext bridging, all
|
||||
`OnboardingLogic` validators (empty / whitespace / port range /
|
||||
legacy-RSA rejection), mock tester, and 10 end-to-end
|
||||
`OnboardingViewModel` paths (happy, bad import,
|
||||
connection-failure → retry-success, reset).
|
||||
- **ScarfIOS (Apple-only):** 3 smoke tests for the Ed25519 generator,
|
||||
OpenSSH public-key wire format (byte-length pinned at 51), and
|
||||
corrupted-PEM rejection on round-trip decode.
|
||||
|
||||
Total: **88 passing on Linux** (62 pre-M2 + 26 new). Apple CI adds
|
||||
the 3 ScarfIOS tests.
|
||||
|
||||
**Manual validation needed on Mac:**
|
||||
|
||||
1. Xcode project creation per SETUP.md.
|
||||
2. Citadel 0.9.x `SSHAuthenticationMethod.ed25519(...)` variant name
|
||||
— verify and fix if it's been renamed.
|
||||
3. Onboarding end-to-end: simulator → physical iPhone via TestFlight
|
||||
→ real SSH host with the public key added to `authorized_keys`.
|
||||
|
||||
**Rules next phases can rely on:**
|
||||
|
||||
- **M3** adds a Citadel-backed `ServerTransport` in ScarfIOS; iOS
|
||||
`IOSServerConfig.toServerContext(...).makeTransport()` dispatches to
|
||||
it automatically.
|
||||
- **M4** adds `SSHExecACPChannel` in ScarfIOS; iOS wires the
|
||||
`ACPClient.ChannelFactory` hook (from M1) to produce it — sibling
|
||||
to Mac's `ACPClient+Mac.swift`.
|
||||
- iOS is single-server in v1 — don't prematurely generalize the
|
||||
onboarding flow.
|
||||
- Source tree stays **pure SwiftUI + Foundation + ScarfCore + ScarfIOS**;
|
||||
`#if canImport(UIKit)` fine for pasteboard but keep it minimal.
|
||||
|
||||
### M3 — pending
|
||||
### M4 — pending
|
||||
### M5 — pending
|
||||
|
||||
Reference in New Issue
Block a user