English
Native device identity and private host channel
Status (2026-10-23): the candidate now wires secure storage and the shared controller through RuntimeSidecar, the sidecar entry point, native settings, callback routing and command approval UI. Cloud remains disabled without configured service origins. No real account has been authorized, no Electron identity has been transferred, and profiles containing encrypted device identities still require the separate migration gate below. Real browser authorization, Keychain prompts and native UI acceptance remain pending.
Ownership and storage
CraftHubMacCore/KeychainDeviceVault.swift owns a generic-password Keychain item scoped to the canonical native profile, cloud API origin and cloud web origin. The scope is serialized unambiguously and hashed into the service name; the item account is device-v1. A different profile or cloud uses a different identity. HTTPS origins are accepted, with HTTP limited to loopback fixtures; credentials, paths, query strings, fragments and invalid ports are rejected.
The stored value uses the existing shared controller's Ed25519 PKCS#8 private-key and SPKI public-key PEM formats. CryptoKit generates the key pair; both Swift and Node validate the key type and derived public key. Registration and deletion compare the complete expected identity, including the device ID, so an old request cannot delete a later registration that reused the same key. No identity, private key or encrypted placeholder is written to the profile.
Automatic load() retains kSecUseAuthenticationUIFail, but that flag alone does not suppress file-based Keychain interaction. FileKeychainAccess serializes the vault's Security calls and temporarily suppresses process interaction, restoring the prior policy even when an operation throws. It first resolves an item reference and checks its actual keychain's lock status, refusing a locked item before asking for secret bytes. Explicit operations respect an already disabled host policy; they do not enable it themselves. The file-keychain compatibility issue is also documented in Chromium's scoped interaction implementation.
On the unlocked macOS 26.6.2 test host, the previous query-only implementation waited inside SecItemCopyMatching; the isolated locked-keychain regression exposed this instead of passing under a locked desktop. The revised check returns without waiting for unlock, preserves the stored identity after an explicit fixture unlock and restores interaction policy after errors. The regression bounds both identity and transfer-metadata reads to two seconds. This uses disposable password-protected keychains; real-account and signed-upgrade prompt acceptance remains separate. There is no plaintext fallback, replacement identity or automatic retry. Explicit create/register/clear operations are invoked only from a connection/disconnection flow. Security calls run away from the UI actor, and errors and identity descriptions are redacted.
This candidate uses the file-based macOS Keychain supported by local/ad hoc builds. It does not request synchronizable items, a data-protection access group or invented signing entitlements. Developer ID upgrades, Keychain access prompts and the eventual update chain still require real signed-bundle acceptance. Apple's TN3137: On Mac keychains describes the distinction; SecItemCopyMatching is a blocking operation.
Private pipe contract
apps/desktop/src/native-host-channel.ts implements the existing DeviceIdentityVault interface using private streams supplied by its caller. CraftHubMacCore/NativeHostChannel.swift implements the corresponding parent service and output decoder. Neither exposes a URL, HTTP endpoint, browser callback or command-execution method.
The existing version-1 runtime handshake is retained as the first stdout line. The decoder accepts subsequent JSON lines with hostBridge: 1, a UUID request ID, a known method and its exact payload. Replies carry the same ID and either a value or a fixed error code. Implemented methods are vault.load, vault.create, vault.register and vault.clear; methods or payload fields outside this contract are rejected.
Each frame is limited to 16 KiB and each side to eight outstanding requests. The parent rejects its most recent 256 request IDs, including pending duplicates; the output decoder also bounds messages returned from one chunk. The child correlates fragmented/coalesced replies, ignores duplicate acknowledgements and retires the connection after invalid frames, substituted keys, stream closure or a timeout. A timed-out mutation has an unknown outcome: no automatic retry or transfer to a replacement process is allowed. The caller must re-read Keychain state before deciding on another operation. Closing a parent service suppresses pending replies; an already submitted Security operation may have completed.
NativeRuntimeChannel now connects the app's stdout reader to the scoped vault. NativePipeWriter owns a close-on-exec duplicated descriptor, uses nonblocking writes with a two-second deadline and descriptor-local F_SETNOSIGPIPE, and serializes bounded replies off the UI actor. Retirement closes the writer, rejects old work and waits for already-started secure-storage operations before restart or profile release. A failure is never retried against the replacement process.
Native account and approval flow
Configure CRAFT_HUB_CLOUD_ENDPOINT and CRAFT_HUB_CLOUD_ORIGIN in the native app's launch environment. Both must be bare HTTPS origins; loopback HTTP is supported for isolated development. Only validated, normalized origins are forwarded to the child. The bundled cloud connection page now accepts the exact craft-hub-native://cloud/connect callback in addition to the existing Electron schemes; a deployed cloud page needs this change before real native login can complete. The public default-handler transition is not performed by this change.
Native Settings shows account/device state, synchronization, explicit connect/disconnect and pending remote requests. The main window points to pending reviews. Each review displays the executable, individual arguments, working directory, prerequisites, required environment names, package script and deadline; rejection is the keyboard cancel action and approval has no default Return shortcut. The shared runtime still performs the final trust/declaration/expiry checks.
The /api/native-host/cloud endpoint requires the independent native bearer and rejects browser cookies. Approval queues are scoped to one process, capped at eight entries and 64 KiB per review, removed on abort/expiry/shutdown, and cannot replay a decision. No HTTP route exposes vault methods. The four compatibility bridge methods can read status or connect/disconnect/synchronize through Swift; they never expose approvals or identity material. Cloud callbacks are intercepted before the web inbox, and a callback arriving while the connection response is pending waits for that operation without persisting its URL.
Shared personal-cloud lifecycle
The shared controller now validates API/web origins before reading an identity. All HTTP requests have a 15-second deadline, reject redirects and cap response bodies at 8 MiB. Provider errors and callback contents are not returned as diagnostics. Callbacks require the configured scheme and exact host/path/parameter set; duplicate fields, credentials, fragments and raw controls are rejected. A pending connection expires after ten minutes; an accepted callback is consumed before adoption begins, including when its response is lost.
Account mutations are serialized. Disconnect cancels the active session immediately, waits for an already-started key creation/registration and synchronization, then revokes and clears the identity that actually completed. An unread or failed vault is not treated as empty: disconnect must first read it successfully, and otherwise keeps an error state. A failed revocation is reported separately after local deletion; a storage failure preserves the identity for explicit recovery. Old synchronization responses and timer callbacks cannot reconnect the device. close() returns a promise that drains account/sync writes before the host releases its profile; Electron now awaits it during shutdown. Cancellation can let an already-started document apply finish, but prevents subsequent applies and revision commits.
Remote command approval carries the reviewed invocation, project path, script and a bounded deadline. The runtime re-discovers the capability and rechecks the current project, selected inputs, cancellation and deadline before spawning; each later prerequisite step also checks cancellation/deadline. Changing a package script or revoking trust while the prompt is open therefore requires a new review. New requests for an untrusted project are rejected before prompting. A cancelled prompt's late acceptance cannot execute. The runner records its in-process execution attempt before entering the runtime so losing completion/status replies cannot repeat it; the cloud repository already refuses to reclaim running requests across client restarts. This does not claim an atomic filesystem snapshot of arbitrary concurrent repository edits.
Isolated verification
From the repository root:
bash
pnpm exec vitest run apps/desktop/test/native-host-channel.test.ts
CRAFT_HUB_TEST_NODE="$(command -v node)" swift test --package-path apps/macos --filter 'KeychainDeviceVaultTests|NativeHostChannelTests'The Swift tests create and delete only their own password-protected temporary keychains. They never use the user's login keychain as a fixture or change the default keychain. Cases cover lock/unlock, scope isolation, repeated creation, stale register/delete, invalid key material and Node signature compatibility. The cross-process test starts the real Node adapter, carries generated identities only through its private stdin/stdout, then exercises load/create/register/load/clear/load against the temporary Keychain. That vault-only phase leaves the profile absent.
The same real Node child then loads the built shared controller and runtime, with a test transport mapped to its own loopback cloud fixture. It verifies adoption and request signatures, connection/sync/disconnect, Keychain deletion, and rejection of an untrusted remote command before prompting or creating a run. This phase creates ordinary disposable runtime/configuration files and scans them to confirm no device identity file or private key is stored. It does not open a browser or use an actual cloud account. Child output is parsed as protocol data, not printed; failures use fixed diagnostics. Owned processes, servers, pipes and fixture keychains are cleaned up. Build the workspace before this cross-module test so its package imports use current output.
TypeScript tests additionally cover bounded concurrency, timeout retirement, malformed versions/frames, fragmented replies, duplicate acknowledgements, pipe errors, unknown-identity deletion refusal and key substitution. The broader native tests continue to cover plugin/diagnostic discovery, trust and reviewed operations; this foundation adds no automatic project discovery, trust or execution.
After pnpm build:macos, enable the packaged-sidecar fixture with an absolute CRAFT_HUB_TEST_CLOUD_SIDECAR path pointing to Craft Hub Prototype.app/Contents/Resources/app/apps/desktop/dist/sidecar.mjs, together with CRAFT_HUB_TEST_NODE, when running Swift tests. PackagedCloudTests.swift uses its own Keychain and loopback cloud, pre-registers a fixture device, then exercises the actual packaged controller and production pipe with signed synchronization, revocation, key deletion and normal process exit. It does not open a browser. The separate controller fixture covers adoption signatures; neither substitutes for an authenticated real-account or signed-update acceptance run.
bash
CRAFT_HUB_TEST_NODE="$(command -v node)" \
CRAFT_HUB_TEST_CLOUD_SIDECAR="$PWD/apps/macos/build/Craft Hub Prototype.app/Contents/Resources/app/apps/desktop/dist/sidecar.mjs" \
swift test --package-path apps/macosShared-controller tests cover cancellation during load/create/register/sync, shutdown draining, expired and reused callbacks, revocation failure and error redaction. Real loopback HTTP tests verify that redirect targets receive no adoption body or signed headers, and a stalled response body cancels. Runtime and runner tests execute approved fixture commands and reject late trust/script/input/expiry changes; final-status delivery failure does not repeat an execution.
Device identity transfer core
The transfer core, runtime profile journal coordinator, native Keychain participant and receiving maintenance entry are implemented. The authenticated source-owned recipient launcher, bounded TypeScript client and private maintenance coordinator are connected to native consent/recovery dialogs. The packaged macOS source now also provides an ordinary App menu and guarded-startup recovery entry described below. Positive signed-pair acceptance remains pending; coordinated-transfer release markers keep production transfer closed. The offline migration command still blocks encrypted identities. Do not use these low-level APIs to bypass preflight or transfer a real account independently of its profile transaction.
DeviceVault.previewTransfer() reads only the registered device ID, public key, canonical source directory and ciphertext-file SHA-256. It does not call safeStorage, create a key or ask for authentication. Source files are bounded to 16 KiB and must be single-link regular files owned by the current user; symlinks, malformed records and unknown fields are refused. Ordinary Electron identity reads now also validate matching Ed25519 keys and redact provider errors. New ciphertext files use mode 0600.
NativeIdentityTransfer reads the same source metadata and creates a two-minute request with an in-memory X25519 recipient key. The request binds the source directory/revision, device ID/public key, native target, both canonical cloud origins, recipient key, challenge and expiry. The source host's sealTransfer() verifies the request against its own configured origins and checks the ciphertext before and after decryption. It never creates, changes or removes the source identity.
The envelope uses X25519, HKDF-SHA256 and AES-256-GCM with a fresh sender key and nonce. Every review field is authenticated through UTF-8 length-prefixed associated data. Only the native recipient can decrypt; the recovered private key must match the public key read from the source. The receiver rechecks the source revision and canonical roots, consumes its key before parsing any response, and rejects expiration, replay, altered context or substituted identity. No plaintext transfer file, environment variable, URL or HTTP endpoint is introduced. Node and CryptoKit use the same HKDF parameters and shared-secret derivation. JavaScript/Swift strings can retain in-memory copies until runtime reclamation; this is not a claim of guaranteed memory erasure.
KeychainDeviceVault has a separate pending-transfer item in the same profile/cloud scope. Staging accepts only an empty destination and persists the reviewed request and identity in Keychain. It does not activate the account. An existing destination, a different transfer, wrong scope or later unrelated identity is preserved and rejected. Ordinary load/create/register/clear operations fail while a reservation exists, including after process restart.
The runtime coordinator holds exclusive source/target profile leases throughout these steps. Before touching Keychain, it awaits durable pending interlocks for every protected root. profile-lease.ts keeps the authoritative marker outside the profiles and temporary directory, under the OS account's Library/Application Support/Craft Hub Migration/guards on macOS. Node resolves the account with userInfo() and Swift with getpwuid(); overriding HOME, XDG_* or TMPDIR does not select another guard namespace. New directories and marker writes are synced, and the directory and files are private to the account.
The existing /tmp SQLite lease files retain their inode identity. A legacy marker is mirrored beside them for clients that understand only that location: write the durable copy first, then the mirror; remove the mirror first and the durable copy last. Both current clients inspect both locations, reject corrupt metadata, and block ordinary startup even after the coordinator exits or the mirror is lost. Only an actual exclusive Node lease for the matching canonical transaction can change a guard. Conflicting transaction owners fail closed; interrupted pending/source-retained transitions with the same owner remain blocked until recovery. Do not remove markers or lock databases to force startup. A legacy-only marker is still recognized, but does not gain persistence until explicitly rewritten by its matching coordinator.
applyIdentityProfileMigration accepts the exact data preflight revision, a public identity binding and an authenticated host participant. The participant is a trusted in-process adapter, not an approval token or a remote API. It owns recipient authentication, encrypted transport and native vault access; the runtime never receives a private key or transfer ciphertext. The runtime permits only the known encrypted-identity preflight blocker, verifies the retained ciphertext hash, and refuses other blockers or an occupied native identity destination. Its sequence is:
- Create the private transaction journal and install every persistent guard, then stage the received identity before preparing the profile copy.
- Install and verify the reviewed data using the same copy/backup engine as offline migration. Prepare native activation; its reservation continues to block cloud startup.
- Sync the journal's
commitdecision before finishing the native reservation. Re-read participant state to distinguish a lost reply from an unfinished operation. Mark the journal settled only after the profile and identity agree. - Retain the source data/configuration guards as
source-retained, then release the native guards. The source files remain unchanged, but current clients cannot reopen the old device identity concurrently.
recoverIdentityProfileMigration rolls back an undecided transaction; a persisted commit decision finishes committing. It does not retransmit the private identity. rollbackIdentityProfileMigration requires a current candidate revision, persists the rollback decision, archives later candidate edits, restores the old profile and removes only the matching native identity. It refuses to delete that identity if the retained source ciphertext changed or disappeared. Lost rollback replies are resolved by inspecting Keychain again. A settled operation can be repeated without overwriting later profile changes; pending or ambiguous outcomes keep the journal and guards.
The public journal records paths, hashes, transaction ID, device public key/ID, cloud origins, decision and settlement state, never key material. Journals must be private regular metadata, have a valid transaction location and consistent states. Data-only CLI apply/recover/rollback refuses identity journals even if their guards were manually removed. KeychainDeviceVault checks the complete source/target/cloud/device binding while a reservation exists and compares the scoped active device after completion; unrelated later devices are preserved. Its low-level abortTransfer still does not remove completed transfers. Only the coordinator's reviewed rollback path uses rollbackMigration for a finished transfer.
IdentityTransferTests runs a real Node child using the production source adapter and encryption code, sends only a public request and encrypted envelope through private pipes, then decrypts, stages and commits into a temporary Keychain. Its fake OS adapter keeps the source secret only in child memory and writes opaque ciphertext. Tests cover UTF-8 paths, stale/expired/replayed input, key substitution, restart recovery, account blocking, abort, mismatched destination preservation and locked storage. This does not verify Electron's actual safeStorage authorization prompt, signed-bundle access or a complete profile-and-key migration.
The durable-guard interoperability test exits the real Node writer, removes only its fixture's legacy marker, and checks Swift startup refusal before a profile exists. A second Node process clears the guard through the matching recovery lease, then native startup succeeds. This simulates temporary marker loss; it is not evidence of a physical reboot or power-loss test.
Runtime coordinator tests kill real Node processes at six boundaries: guarded, staged, installed, commit decided, identity finished and journal settled. Their participant persists public fixture state only. A separate Swift/Node test uses the production coordinator, source encryption, recipient protocol engine and a real isolated Keychain: it stages the encrypted identity, interrupts after the commit decision, retires the old recipient session, resumes the same public binding, recovers the commit, then rolls back after a later candidate edit. This engine test injects peer authentication only through its internal compiled test initializer; it does not prove a signed parent/child handoff. The source-host coordinator and native consent/recovery integration are covered below; no real identity migration has been performed.
The production handoff requires a source build that honors durable migration guards on ordinary startup. The legacy temporary mirror does not make older hosts safe after temporary-directory cleanup. The source also launches and verifies its native recipient itself; accepting an arbitrary caller-selected recipient key would expose a general private-key export channel. Electron maintenance supports read-only preview/recipient-preflight and source-coordinated migrate/recover/rollback; an independent seal request remains rejected before secure-storage access.
Follow the profile migration procedure; encrypted device identities remain blockers for offline CLI copies and for application pairs that have not passed the authenticated handoff release gate.
Authenticated host preflight
identity-transfer-recipient.ts checks the running source executable's application and the selected native bundle with fixed system codesign/plutil tools, bounded output and deadlines. It requires Developer ID Application certificates from the same team, the fixed native bundle identity, sealed reciprocal source/recipient identifiers, hardened runtime, durable-guard version 1 and the coordinated-transfer release contract. Injection/debugging entitlement exceptions, inconsistent bundle executables, ad hoc signatures and older contracts are rejected. Requirement literals are validated before interpolation. The selected application is never executed by this preflight, and no profile, lease, Keychain or safeStorage identity is accessed.
The maintenance input is exactly { "version": 1, "action": "recipient-preflight", "application": "/absolute/Native.app" }. An unavailable peer returns { "available": false, "reason": "peer-verification-failed" }; valid metadata includes the bundle paths, team and code hashes. This read-only result is neither approval nor reusable authentication: the source-owned launcher repeats verification immediately before starting the recipient and authenticating its message channel; the pipe is only an address and lifetime bootstrap.
IdentityTransferPeer.swift supplies receiving-side authentication. It gets its own code identity and actual parent process from Security, validates both running code objects plus their full sealed bundles, pins the source identifier from its own sealed signing information, and retains the original parent code object for revalidation. It takes no caller PID/team/requirement or environment exception. The maintenance receiver constructs it before generating a transfer key or accessing Keychain and revalidates before and after each operation, including after OS secure-storage interaction. The source launcher still needs to authenticate its actual running recipient and bind that identity to its private transport; a filesystem preflight or caller-supplied readiness JSON alone is insufficient.
The CraftHubIdentityHostSecurityVersion = 1 source marker is a release promise, not a substitute for executable hardening. Current packaging disables Electron RunAsNode, Node options and inspector entry points, enables ASAR integrity/ASAR-only loading before signing, and rechecks the actual fuse bits after packaging. Electron's fuses and ASAR integrity document these protections. The bundled Workstation backend now uses an independent, architecture-matched official Node 24.22.1 executable, with archive hashes pinned in scripts/desktop-node.lock.json; it no longer needs Electron Node mode, including for detached workers and backup subprocesses. Only the Electron package includes this executable and its license; native candidates retain their existing Node bootstrap. Bundled backend children clear inherited Node preload/module-path hooks. The additional Electron resource size is a migration-host tradeoff, not a change to the native candidate's size model.
test:desktop-backend <Electron.app> verifies the packaged Node, backend capabilities/configuration/catalog and a real detached worker for a detected item whose plan explicitly skips installation. It also corrupts a temporary copy's ASAR header, re-signs that copy to avoid relying on OS resource-signature failure, and requires an ASAR integrity failure before startup. test:macos-identity-source <Electron.app> verifies actual packaged preview/preflight and ordinary startup with attempted Node-mode, preload and inspector injection. Both keep data disposable; release CI runs them before handing off archives. Current packaging deliberately still emits no security or recipient-operation marker: the authenticated source client is not connected to coordinator consent and recovery, so transport authentication alone does not open real identity transfer.
Tests cover malformed contracts, wrong teams/identifiers, unsafe entitlements and requirement injection. A real ad hoc fixture carrying the expected bundle identifier and claimed protocol metadata fails the compiled Developer ID requirement; the actual test process cannot authenticate as a recipient. The real Electron maintenance verifier exercises unavailable-peer reporting without changing its isolated identity file. Positive metadata tests use explicit signing-tool fixtures, not an available Developer ID certificate. Signed parent/child success, actual source-owned launch, Keychain consent and recovery UI remain pending. Apple's code requirements and dynamic validation API describe the signing boundary used here.
Native maintenance receiver
The fixed --craft-hub-native-identity-recipient mode dispatches in CraftHubMain before SwiftUI or AppDelegate is constructed. It does not open NativeStorage, acquire a normal instance/profile lease, start a sidecar, register links or create windows. Additional or malformed identity-mode arguments exit with status 64 instead of falling through to ordinary launch. Authentication/protocol failures exit with status 1 without printing request contents or storage errors.
Stdin and stdout must be pipes or sockets. After authenticating its actual parent, the receiver reads exactly one bootstrap line: { "version": 1, "service": "dev.craft-hub.identity.<uuid>" }. This is only a rendezvous address; stdout returns no transaction bytes. The authenticated Mach exchange below supplies the protocol-ready acknowledgement and carries the existing JSON requests. Requests use version 1 and consecutive numeric id values starting at 1. Frames are limited to 64 KiB and the session to 64 requests; bootstrap and idle message waits are limited to 120 seconds, sends to two seconds. The receiver loop does not time out an in-flight Keychain operation; the source client has a separate bounded response deadline described below. Any additional stdin byte or EOF retires the session. Unknown fields, malformed versions, replayed/out-of-order IDs, noncanonical roots/origins and changed transaction bindings also retire it. The executable exposes no test authentication or test-Keychain switch.
| Operation | Purpose |
|---|---|
prepare | Bind canonical source-data/source-config/native-target paths and cloud origins; return the public expiring recipient request. Read source ciphertext metadata only. |
resume | Bind one journal context for recovery without generating another transfer key. |
inspect | Return only empty, staged, activated or active for the matching vault and transaction. |
stage | Consume the bound encrypted envelope and stage the validated identity in Keychain. A resumed session cannot stage. |
activate, finish, rollback | Call the existing Keychain participant for the same complete public binding. |
close | Retire ephemeral state and exit; do not mutate a durable reservation. |
Every response is { "version": 1, "id": <request id>, "value": ... }; it never returns the device secret or received encrypted envelope. EOF, a failed write, cancellation and uncertain acknowledgements discard only the ephemeral recipient key. Recovery inspects the existing reservation in a newly authenticated session. The source coordinator remains responsible for explicit consent, all exclusive profile leases, persistent guards and journal decisions; Swift does not reproduce its copy/commit/rollback policy.
pnpm test:macos-identity-recipient runs the actual built app from an untrusted Node parent while holding the fixture's exclusive profile lease. It requires immediate refusal, no handshake/key, no profile creation and no instance/runtime trace, and also verifies malformed maintenance arguments. The normal launch regression is pnpm test:macos-instance. Swift tests cover bounded fragmented input, stalled writers, oversized frames, failure retirement, binding/replay rejection, pre/post-operation authentication and the real Node coordinator with isolated Keychain recovery. The actual-app refusal check remains a local native acceptance requirement; routine CI no longer runs native checks. Positive Developer ID launch and source consent are separate remaining acceptance work.
Source-owned process foundation
The macOS Electron package includes a small Node-API module at app.asar.unpacked/apps/desktop/identity-launcher/launcher.node. Packaging compiles the current source for the selected architecture with the local SDK and the pinned Node-API headers, then signs it with the app. Sources and test programs are excluded from the deployed app. The native candidate does not include this Electron-only module. Packaged maintenance previews load it to check the real Electron ABI but never launch a recipient.
The module independently validates its running source, the selected native bundle, reciprocal release contracts, same Developer ID team, hardened-runtime flags and injection entitlements. It creates the exact child using posix_spawn with POSIX_SPAWN_START_SUSPENDED, validates the actual kernel guest with Security, compares its code hash and executable against the selected signed bundle, then resumes it. A fixed argument and minimal OS-account environment are used; no caller PID, team, arbitrary argument list or environment is accepted. POSIX_SPAWN_CLOEXEC_DEFAULT limits inherited descriptors to the explicit standard pipes. Nonblocking IO, child status, termination and reaping operate only on the child owned by this module, never a caller-supplied or already-reaped PID. Apple documents spawn attributes before execution and kernel-hosted code lookup.
Process ownership is not message authentication. Another process under the same account can send signals, including SIGCONT; a raw pipe reply must not be used as proof of the code that produced it. The native receiver now uses the authenticated Mach transport below. The source addon now creates its own Mach rendezvous, authenticates every received frame, and exposes only complete verified frames to its TypeScript client. Pipes carry only the initial address and parent lifetime; no JavaScript method can send transaction bytes through them. Source sealing is now wired through the reviewed coordinator below; coordinated-transfer release markers remain disabled.
pnpm test:identity-launcher [Electron.app] builds the production addon and requires rejection from a non-source Node host. A separate native harness uses an explicitly local fixture validator to observe a stopped child, query its real Security code object before the owner resumes it, exercise refusal cleanup, verify only standard descriptors and the fixed environment arrive, then exchange fixture bytes and reap the owned child. That test callback is compiled only into the harness; it is not a release addon option. With an app path, the verifier additionally loads the actual packaged module using its bundled Node. test:macos-identity-source <Electron.app> exercises actual Electron loading. These creation and ABI checks alone do not prove signed parent/child success or a real identity transfer. Additional message-channel coverage follows below.
Authenticated recipient message transport
An anonymous NSXPCListenerEndpoint must be passed through an existing XPC connection; the local secure-archive probe refuses a plain pipe round trip. Creating a new launchd service with bootstrap_create_service is also unsupported on the test host. The owned-process design therefore uses a bounded Mach message channel without installing a LaunchAgent or service. The shared C module uses the public, deprecated bootstrap_register only for a random transient rendezvous name. Receive rights use mach_port_construct/mach_port_destruct; destroying the receive right removes the name, as the real-process verifier confirms. This compatibility dependency still needs the supported minimum-system acceptance; it is not a persistent service registration or an authentication credential. See Apple's XPC endpoint contract and Mach port model.
The native receiver looks up the source address and sends a harmless identity-recipient-channel-ready marker with a send right to its own private receive port. It generates no transfer key for that announcement. The source verifies the announcement's kernel audit trailer against the exact owned child, then retains that reply right; it never takes a destination port, audit token or PID from JSON. Its first authenticated empty message requests the existing identity-recipient-ready response. All subsequent transaction JSON and responses use Mach messages. The source Node-API addon now implements these steps; a positive Developer ID pair is still required for transfer acceptance.
On every incoming message, IdentityTransferPeer checks the kernel audit token's PID against the actual retained parent, resolves the sender through kSecGuestAttributeAudit, validates the fixed Developer ID requirement and full signed resources, and compares the original source code hash. The first accepted audit token is pinned for the session. Parent/recipient validation still runs before and after protocol operations. The kernel token includes a process incarnation, so an untrusted sender cannot send bytes and then exec a trusted executable to launder the old message. Apple's audit guest attribute supplies the Security lookup; the verifier checks actual exec invalidation rather than relying on PID equality.
The C boundary rejects complex messages, unknown wire versions, inconsistent lengths and oversized frames; the Swift receiver also rejects reply rights on transaction frames. It requests the audit trailer from the kernel and never reads an identity from the payload. Port ownership is explicit: failed/short/malformed receives dispose of transferred rights, and send timeouts clean up the rights returned by the kernel's pseudo-receive. The timeout case follows the ownership semantics used in Apple's Mach message implementation; repeated queue-full and dead-destination tests verify reference counts. EOF/cancellation closes ephemeral state only; durable migration reservations remain available to the coordinator's recovery policy.
pnpm test:identity-launcher now compiles the same C transport into a standalone fixture. It verifies a full 64 KiB cross-process exchange, kernel sender lookup, a forwarding child's fake PID rejection, stale audit-token rejection after same-PID exec, malformed/complex/oversized messages, queue saturation, dead destinations, right cleanup and disappearance of the transient name. Swift tests exercise the real receiving loop with explicit fixture authentication: no protocol-ready response before authentication, authentication before every operation, no vault construction on refusal, and prompt closure on parent EOF or even a partial unexpected pipe frame. Those test callbacks are unavailable from the application entry. Developer ID positive handoff and actual consent/recovery UI acceptance remain separate work; the source coordinator integration is described below.
Authenticated source channel and client
The source addon links the same C transport as the Swift recipient. Its private SourceChannel owns the published inbox and adopts a reply send right only after verifying the incoming kernel token. Every reply must come from the still-owned child PID, resolve through kSecGuestAttributeAudit, satisfy the fixed recipient Developer ID requirement and complete sealed-resource validation, and match the originally selected CDHash. Source and recipient code are revalidated before and after that lookup. The first accepted audit token is pinned, including the exec generation. Authentication and protocol failures close the channel and retire the owned child; the discarded receive right also removes its transient name.
The two-stage handshake first accepts the exact channel-ready marker with one reply right, sends an empty authenticated request, then requires the exact protocol-ready marker without a reply right. Transaction replies cannot carry rights. The addon exposes complete frames only, rejects writes before ready and limits frames to 64 KiB. end() closes the channel and its parent-lifetime pipe; close() additionally retires the owned process. No raw pipe response reaches the client.
openIdentityRecipient obtains this transport only from the fixed native launcher; it has no injectable public transport, PID, key or signing policy. It validates the ready marker and exact response envelopes, binds prepare replies to the requested source, target and cloud origins, validates the expiring recipient request, and enforces one pending operation, consecutive IDs and a 64-request session limit. Startup waits at most ten seconds; each response waits at most 120 seconds, including time spent in an OS prompt. Abort, timeout, lost acknowledgements and explicit close retire the session without guessing a commit or rollback. The caller must inspect the existing journal/reservation through a newly authenticated resume session. Temporary frame buffers are cleared after use and provider errors are replaced with a fixed diagnostic.
The native verifier compiles the actual SourceChannel into a standalone harness. A separate ad hoc fixture covers both handshake stages, maximum-size echo, forged stdout, relay before/after readiness, same-PID exec, malformed markers, unexpected reply rights, empty/oversized frames, authentication loss and transient-name cleanup. The fixture authentication callback is not compiled into the addon. Twenty-five TypeScript protocol cases cover scope binding, recovery, response correlation, limits, cancellation and uncertain stage acknowledgements. These engineering tests do not substitute for a real Developer ID signed pair or user consent.
Source-owned migration review and recovery
The private Electron maintenance entry now recognizes declarative migrate, recover and rollback requests alongside read-only preview/preflight. It derives source directories and cloud origins from its own host configuration; callers cannot supply approval flags, source paths, keys, process IDs or launch arguments. Unsupported and ad hoc hosts fail signing/release preflight before any consent dialog or vault access. Coordinated-transfer release markers remain disabled; the ordinary App menu/bootstrap entry below uses the same coordinator.
The coordinator shows source-owned native system dialogs without starting a browser or the ordinary runtime. Review includes both applications, team, exact source/configuration/target paths, cloud origins, device public-key fingerprint, copied/excluded entries and the durable recovery decision. Cancel is the default. Proceeding requires the action button and an explicit acknowledgement that both ordinary hosts and older CLI clients are closed. A public-metadata report can be saved as a new private file; existing files are never overwritten and saving returns to a fresh confirmation. The parentless macOS system dialog may remain visible after cancellation; the signal is rechecked afterward and no recipient is launched while consent is pending.
Only after consent does the coordinator start the authenticated recipient and request its short-lived transfer key. The launcher revalidates the complete source/recipient signing policies and additionally compares both CDHashes against the reviewed applications before spawning. Those caller-supplied hashes constrain the review; they never replace native authentication. The runtime then acquires exclusive profile leases and durable guards before the source vault seals an envelope. Profile/identity changes after review fail closed, and each transfer can stage only once.
Native quiescence checks use running application identifiers and the known legacy Craft Hub executable layout, excluding only the actual maintenance source and its owned recipient. They do not terminate other clients and cannot identify every older CLI or renamed downstream app; explicit confirmation and kernel leases remain necessary. The standalone process fixture verifies detection and exact PID exclusion without stopping that process.
Recovery reuses the existing journal and recipient reservation; it does not generate a replacement transfer key or decrypt the source again. The approved recovery revision covers both journal and target snapshot, and the runtime checks it after acquiring exclusive leases, before participant calls or mutations. Lost stage/finish acknowledgements retain a recovery-required receipt. Native result dialogs report the durable outcome and transaction location only after releasing the client and leases. Isolated tests cover consent cancellation, changed reviews, lease-held sealing, interruption recovery and rollback preserving later candidate edits. Real signed-pair acceptance remains outstanding; offline CLI migration still rejects encrypted identities.
App menu and guarded-startup recovery
The packaged macOS Electron source exposes Craft Hub → 原生版资料迁移… (Migrate to Native App…). It checks for active commands and agent tasks, asks before closing the workbench, rechecks after the dialog, then relaunches with the fixed --craft-hub-native-migration mode after ordinary shutdown. Repeated menu clicks share one dialog. This is navigation into maintenance, not approval to copy data or transfer an identity.
The dialog-only host uses a private temporary Chromium profile. Its identity actions offer migrate, recover and rollback, then use system selectors for the native .app and candidate/transaction directory. Application signature and release-contract preflight happens before destination selection or vault access. The resulting command enters the same source-owned coordinator and separate full consent described above. Cancelled selections do not call the coordinator. Unsupported/ad hoc pairs display a bounded error; selecting a bundle does not execute it. A separate ordinary data wizard handles profiles without device identity through the existing offline runtime coordinator. It does not load the identity launcher or access a vault; identity files and identity transactions remain rejected on that path.
Ordinary macOS startup reads migration guards for both source roots before importing normal desktop startup. A pending or retained source opens the maintenance host with its existing transaction pinned; the user cannot replace that transaction through the selectors or start a new migration. Unreadable or conflicting guards show an error and leave the workbench closed. If a guard appears after the initial inspection, the runtime lease still rejects startup and the source drains its partial initialization before reopening maintenance. Neither path deletes guards, starts account sync, changes project trust, or weakens the signed-peer requirement. Windows/Linux startup is unchanged.
The macOS 26.6.2 Release UI rehearsal on 2026-10-23 used an independently identified ad hoc copy and empty temporary data/configuration roots. The menu's Escape cancellation kept the workbench open; explicit navigation closed the ordinary host and relaunched the dialog-only wizard. Pending guards on both fixture roots caused an ordinary launch to display the existing transaction with recovery/rollback actions, without adding another runtime-ready event. Selecting the local ad hoc native candidate reached the bounded signature refusal before any destination selector. Closing the UI left both guards intact and the transaction directory empty; the verifier then removed only its own guards under an exclusive recovery lease. No real identity, account or profile was migrated. This is refusal and navigation evidence, not positive signed-pair acceptance.
When making an isolated Electron UI copy, retain CFBundleName because Electron derives its Helper lookup from it; change the bundle identifier/display name instead, remove URL/document registrations, redirect data/configuration/browser state, and re-sign that disposable copy. The App selector accepts files and directories; the subsequent preflight owns bundle/type/signature validation. After navigating to an .app, accessibility state can briefly report the Open button disabled while the system finishes selection; confirm the final screenshot and actual action before concluding selection failed.