Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
178 lines
8.5 KiB
Markdown
178 lines
8.5 KiB
Markdown
# ADR 0014: long-lived macOS host-association observer
|
|
|
|
Status: planned production boundary; software contract may be developed behind
|
|
a disabled feature flag.
|
|
|
|
Related acceptance item: `CONN-66` in
|
|
[`../20_K1_CONNECTION_SUPERVISION_CANON.md`](../20_K1_CONNECTION_SUPERVISION_CANON.md).
|
|
|
|
## Context
|
|
|
|
Mission Core must distinguish a K1 that is configured for a network from a Mac
|
|
that is currently attached to the same network. Route, TCP, DeviceInfo,
|
|
control and data evidence are bound to a host-path epoch; a Wi-Fi switch,
|
|
sleep/wake cycle or observer restart must invalidate that epoch before any late
|
|
TCP/MQTT result can restore command authority.
|
|
|
|
The current laboratory implementation is fail-closed but not a production
|
|
observer. One normal connection-monitor poll samples the host path before and
|
|
after its TCP probe. Each sample synchronously invokes:
|
|
|
|
```text
|
|
/usr/bin/xcrun swift plugins/xgrids-k1/macos/associate_wifi.swift
|
|
```
|
|
|
|
under one process-local lock with a 30-second timeout. At the one-second
|
|
monitor interval this can launch two Swift processes per second. A failed
|
|
cycle can occupy the lock for roughly sixty seconds, and cancellation of the
|
|
Python `asyncio.to_thread()` waiter does not terminate the native process or
|
|
thread. Physical-command validation shares this observation path. Shorter
|
|
timeouts, cached shell output or automatic fallback would hide rather than
|
|
remove the lifecycle defect.
|
|
|
|
## Decision
|
|
|
|
Production host-association evidence will come from one signed, long-lived,
|
|
read-only agent in the user's macOS login session.
|
|
|
|
- The agent owns one `CWWiFiClient` for its process lifetime.
|
|
- It observes CoreWLAN link/association/power events and macOS sleep/wake.
|
|
- It never scans BLE, changes Wi-Fi, reads K1 credentials, reconnects MQTT or
|
|
sends START/STOP.
|
|
- It is packaged in a minimal container app and registered with `SMAppService`;
|
|
it is not a `LaunchDaemon` and is not launched through `xcrun` at runtime.
|
|
- The required Wi-Fi event entitlement and Location authorization are checked
|
|
before K1 network mutation is offered. Missing authorization produces
|
|
explicit unavailable evidence, not a crash loop or guessed association.
|
|
- The existing Wi-Fi mutator remains a separate component under the exclusive
|
|
network process lease. Observer authority and mutation authority are never
|
|
combined.
|
|
|
|
The backend communicates with the observer through bounded local IPC. Each
|
|
backend session supplies a random HMAC key. SSID and BSSID remain inside the
|
|
agent; only an opaque continuity token is returned and it cannot be correlated
|
|
between backend processes. The token material is interface plus BSSID; SSID is
|
|
used only to report evidence quality. This keeps one AP identity stable when
|
|
macOS alternates between `ssid+bssid` and `bssid-only` disclosure.
|
|
|
|
## Observer contract
|
|
|
|
```text
|
|
schema_version: missioncore.macos-host-association/v2
|
|
agent_instance_id: random 128-bit process instance
|
|
sequence: uint64
|
|
association_epoch: uint64
|
|
interface_name: string | null
|
|
wifi_interface: true | false | null
|
|
state: associated | not-associated | inactive | not-wifi | unavailable
|
|
evidence_quality: ssid+bssid | bssid-only | not-wifi | unavailable
|
|
continuity_token: 64 lowercase hex | null
|
|
reason_code: string | null
|
|
observed_monotonic_ns: uint64
|
|
sample_age_ms: uint32
|
|
cause: initial | link-change | association-change | power-change |
|
|
permission-change | will-sleep | did-wake | poll-correction |
|
|
observer-restart
|
|
```
|
|
|
|
`sequence` changes for every event or heartbeat. `association_epoch` changes
|
|
when interface, power, state, SSID or BSSID changes. Sleep and wake each create
|
|
a barrier even if the visible network looks unchanged afterward. A new agent
|
|
instance, IPC reconnect, sequence rollback/gap, malformed frame or timeout is
|
|
also a discontinuity.
|
|
|
|
The backend adds its own `observer_session_epoch`; the effective host-route
|
|
fingerprint includes the agent instance, observer session, association epoch
|
|
and opaque token. A response from an old session or sequence is discarded.
|
|
Unknown schema/state or incomplete evidence is `unavailable` and immediately
|
|
revokes host authority.
|
|
|
|
## Timing and failure semantics
|
|
|
|
- Heartbeat: 1 second.
|
|
- Maximum cached-snapshot age: 750 ms.
|
|
- Snapshot RPC deadline: 250 ms.
|
|
- Initial handshake deadline: 2 seconds.
|
|
- Two missed heartbeats or one invalid IPC frame revoke authority immediately.
|
|
- Reconnect backoff: 250 ms, 500 ms, 1 s, 2 s, then at most 5 s.
|
|
- There is no automatic fallback to the Swift source runner.
|
|
- Agent loss affects only read-only host evidence. It never triggers a K1
|
|
network write, MQTT reconnect or physical command.
|
|
- A discontinuity first marks the supervisor host path unavailable and rotates
|
|
its epoch. Recovery then requires fresh route, TCP and DeviceInfo/control
|
|
evidence in that order.
|
|
|
|
## Delivery phases
|
|
|
|
Phase A is safe without signing or a physical K1:
|
|
|
|
1. Define the Python observer protocol and validate the v2 schema.
|
|
2. Add a fake/in-memory transport and backend session/sequence validator.
|
|
3. Implement immediate epoch invalidation and bounded cached lookup.
|
|
4. Inject the observer into the monitor behind a disabled feature flag.
|
|
5. Implement the Swift reducer and local transport as a testable Swift package.
|
|
6. Test sleep/wake, timeout, event gaps, delayed replies, crash/restart and
|
|
manual network changes.
|
|
7. Expose secret-free observer health and next action to the UI.
|
|
8. Prove 10,000 samples launch no child process and cause no lock starvation.
|
|
|
|
Phase B requires the actual Mac signing and permission environment:
|
|
|
|
1. Package and register the user-session agent.
|
|
2. Obtain the Wi-Fi events entitlement and complete Location onboarding.
|
|
3. Run the observer in shadow mode beside the current fail-closed probe.
|
|
4. Cut over only after the physical fault matrix and an eight-hour soak show no
|
|
unexplained divergence.
|
|
|
|
## Acceptance gate
|
|
|
|
- No `xcrun`, `swift` or `swiftc` occurs on the observer path.
|
|
- One agent and one CoreWLAN client serve one login session.
|
|
- Snapshot p99 is below 50 ms, hard deadline 250 ms, monitor-cycle p99 below
|
|
1.5 seconds.
|
|
- No mutex is held across native or IPC calls.
|
|
- Sleep, wake, agent restart, sequence gap and timeout always invalidate the
|
|
effective host epoch.
|
|
- Late TCP/DeviceInfo evidence from an old epoch is rejected.
|
|
- SSID, BSSID and credentials never enter IPC logs, API state or artifacts.
|
|
- Quick-to-Bridge, Bridge-to-Quick, manual Wi-Fi switch, Wi-Fi off/on,
|
|
router loss/return with the same SSID/IP, backend restart and Location denial
|
|
all revoke control authority within two seconds and recover only through
|
|
fresh route, TCP and DeviceInfo evidence.
|
|
|
|
Until this gate passes, the current association probe remains explicitly a
|
|
laboratory implementation and `CONN-66` remains open.
|
|
|
|
## Laboratory containment while Location evidence is hidden
|
|
|
|
The source-runner helper can return `association-identity-unavailable` on a
|
|
connected Mac when macOS privacy rules hide SSID and BSSID from the CLI child
|
|
process. Rotating a random fallback token on every one-second poll made a
|
|
stable route and a successful TCP probe mutually impossible: every following
|
|
sample revoked the preceding endpoint result as a fictitious network switch.
|
|
|
|
Until the signed observer above replaces the source runner, the laboratory
|
|
probe uses one random, process-scoped token for the same interface and
|
|
unavailable-evidence scope. This is not promoted to association evidence:
|
|
|
|
- the public evidence quality remains `unavailable`;
|
|
- interface, source address, kernel route, availability, a proven different
|
|
BSSID and process restart remain epoch barriers;
|
|
- endpoint reachability alone remains `configured-unverified`;
|
|
- only fresh exact DeviceInfo/control evidence can grant control authority;
|
|
- `CONN-66`, sleep/wake and same-subnet network-switch acceptance remain open.
|
|
|
|
For an already reachable lease whose exact DeviceInfo identity and control
|
|
session remain healthy, a temporary helper timeout or privacy-limited
|
|
association sample may retain the preceding proven association fingerprint
|
|
only while the kernel route fingerprint, interface, source, intent and target
|
|
are unchanged. That retained sample still performs TCP contact and a second
|
|
kernel-route check, refreshing only route/TCP observation TTLs. Endpoint loss,
|
|
control loss, control-proof expiry, target/intent change, a proven association
|
|
identity change or any raw route change revokes immediately. A
|
|
`configured-unverified` path does not receive this bridge and remains bounded
|
|
by the existing technical-failure debounce and transport TTL.
|
|
|
|
This containment removes the false per-poll epoch churn observed on the field
|
|
Mac without claiming that the planned production observer has been delivered.
|