Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
19bc0cd
Add neutral host preparation seam to desktop service startup
JasonVMo Nov 12, 2026
50c1f86
Add isolated AppKit fixture for native application ownership contracts
JasonVMo Nov 12, 2026
c276f21
Implement neutral macOS application ownership and exact workload iden…
JasonVMo Nov 12, 2026
f835830
Harden causal application capture and native failure evidence
JasonVMo Nov 12, 2026
56e73b6
Pin the native ownership fixture to a compatible macOS deployment target
JasonVMo Nov 12, 2026
58b2cd5
Record bounded macOS application ownership qualification and remainin…
JasonVMo Nov 12, 2026
5df4b9d
Keep package evidence references within guidance-link boundaries
JasonVMo Nov 12, 2026
ddeb328
Record final locked desktop ownership aggregate validation
JasonVMo Nov 12, 2026
ff61264
Settle rejected launches without leaking workload claims
JasonVMo Nov 12, 2026
12ec40a
Record launch settlement review qualification
JasonVMo Nov 12, 2026
9113909
Record final clean-cache ownership review validation
JasonVMo Nov 12, 2026
6690c6d
Separate macOS application identity inspection from control
JasonVMo Nov 12, 2026
49f58b4
Record macOS identity observation regression qualification
JasonVMo Nov 12, 2026
3ff7c5c
Record final identity observation regression validation
JasonVMo Nov 12, 2026
d58d473
Fix live macOS discovery and record ownership qualification
JasonVMo Nov 12, 2026
d5a7530
Qualify interrupted macOS ownership publication and survivors
JasonVMo Nov 12, 2026
ca88c28
Record rebased macOS ownership failure qualification
JasonVMo Nov 12, 2026
1655861
Record rebased Fabric and Paper ownership qualification
JasonVMo Nov 12, 2026
0fb6cd4
Bound abandoned macOS ownership qualification fixtures
JasonVMo Nov 12, 2026
507effe
Record bounded fixture review-fix qualification and remaining gates
JasonVMo Nov 12, 2026
45705fd
Merge remote-tracking branch 'refs/remotes/origin/main' into user/jas…
Copilot Nov 13, 2026
cad22c8
Merge remote-tracking branch 'refs/remotes/origin/main' into user/jas…
Copilot Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .changeset/neutral-macos-application-ownership.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
'@fluentui-react-native/desktop-driver': patch
'@fluentui-react-native/desktop-test': minor
'@fluentui-react-native/storybook-desktop': patch
---

Add opt-in neutral macOS supervisor-owned application launch, exact kernel
incarnation attach and cleanup, and cross-helper application exclusion. Record
pending and acquired applications as workload claim resources, including
uncontained launches and uncertain failures.

Workload claim schema 3 uses the driver's OS process identity instead of an
uptime-derived wall-clock estimate. Legacy or unverified identities remain
unknown during inspection. `claimDesktopWorkload` now returns a promise and
must be awaited; the Storybook integration awaits it before starting.

Pre-spawn and rejected LaunchServices acquisitions release their claims.
Dispatched or surviving acquisitions remain fenced, and non-contention lock
failures clean up only the exact application that the helper launched.

Read-only macOS application identity inspection shares descriptor verification
without acquiring a controlling lease or disturbing session window records.
Storybook bridge-epoch recovery uses it on the embedded helper while real
launch/attach operations retain cross-service exact-incarnation exclusion.

Discover live macOS application candidates from kernel PIDs rather than the
main-run-loop-dependent workspace cache, so read-only inspection sees newly
launched apps and still refuses duplicate identities.

Native ownership contracts additionally qualify failed and interrupted
acquisition publication, abrupt owner death and known uncontained descendants:
uncertainty retains the workload claim until exact operator cleanup. This adds
qualification only, not automatic survivor adoption or descendant containment.
Abandoned qualification owners now retire nonzero after two minutes, and the
AppKit fixture requests termination after five minutes; workload fences remain.
69 changes: 64 additions & 5 deletions packages/agentic/desktop-driver/native/PROTOCOL.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,60 @@ and `external` for an attached application it never ends. A provider that omits
`containment` makes no containment claim. The field is additive and does not
change the wire version.

The macOS provider emits its kernel `proc_pidinfo` start timestamp with exactly
six fractional digits and `Z`, never a LaunchServices launch-date estimate. Its
launched lease additionally reports `launchKind: "launch-services" |
"direct-child"` and `containment: "uncontained"`; an attached lease reports
`containment: "external"`. Causal LaunchServices ownership is not parent/child
containment. Cleanup rechecks the captured exact incarnation before each signal;
it never adopts whichever process currently occupies a launch PID.
Initial capture also checks the causal child or LaunchServices application
identity before and after the kernel read. LaunchServices substitution and
interactive authorization prompts are disabled.

macOS attach descriptors may supply `processIdentity: { processId,
processStartedAt }` with the exact native timestamp and an exact `windowTitle`.
Optional bundle/executable constraints must also match. This external identity
cannot be combined with `leasePath`/`leaseNonce`, cannot authorize launch, and
never grants termination authority. Existing Storybook lease files remain a
separate, legacy identity path. Independent helpers hold a stable per-user
exclusive lock for each exact application incarnation until its lease is
released. Lock contention never permits signalling another helper's application.
The macOS-only `inspectApplication` command accepts the same configured target
parameters as `attach` and returns only `{ processId, processStartedAt }`. It
performs the same descriptor, legacy-lease/nonce, bundle/executable, exact-window
and uniqueness checks, with the exact kernel incarnation captured before window
discovery and rechecked afterwards. It does not create an application lease,
acquire an application or physical-input lock, register windows/elements, or
grant control/termination authority. Identity observation can therefore run on
the session's helper while its controlling lease remains held, without stealing
window records or permitting another service to attach. Cancellation and
unverifiable identity/Accessibility fail closed. Other providers do not implement
this command.
Other lock failures after a proven launch use exact-incarnation cleanup and
report `exited` only when that primary process is confirmed gone.
Explicit app paths are authoritative; bundle-only launch rejects
multiple distinct registered existing bundle paths rather than selecting one.

A failed or cancelled macOS launch reports
`applicationSettlement: "not-started" | "exited" | "unknown"` in
`response.error.data` or `cancelled.data`. `not-started` proves rejection before
process creation or a completed LaunchServices failure with no application.
`exited` requires exact cleanup proof; an unreadable or surviving incarnation is
`unknown`. A process that was created is named by its captured `processId`,
`processStartedAt` when readable, and `launchKind`. A pending launch whose
LaunchServices callback has not arrived may have no process identity; callers
must retain their pending acquisition rather than infer no effect. Node also
marks a dispatched launch `unknown` if its helper transport fails before a
response. A legacy provider's ordinary no-data rejection is not evidence of a
created process. These failures may additionally include bounded
`launchErrorDomain`, `launchErrorCode`,
`underlyingErrorDomain`, and `underlyingErrorCode` diagnostics, without arbitrary
NSError user information. A callback returning both an error and a new process
uses the same exact-incarnation cleanup path. These
additive fields are described in [protocol.json](protocol.json) and retain
wire version 1.0.

Element snapshots can additionally include `required` as a supported Boolean
or an unsupported value with its reason. Older or non-supporting providers may
omit it; the Node boundary treats absence as unsupported. This is additive
Expand Down Expand Up @@ -526,6 +580,9 @@ The helper stops side effects, unwinds command-local work, and emits:

`effect` is the mutation effect described above and is present for mutating
commands. A queued request that is cancelled before it starts reports `none`.
An optional `data` object preserves lifecycle settlement evidence, including
the exact unleased application from a cancelled macOS launch. It is not a
mutation-effect verdict and never authorizes a retry.

Node retains the session queue and physical-input ownership until the request
settles. Failure to settle before the cancellation deadline is fatal to the
Expand Down Expand Up @@ -597,11 +654,13 @@ privacy access. `--service-identity` follows the one-shot contract below; see
[the macOS service verifier](../references/security.md#macos-service-verifier).
`--application-incarnation` also follows it: the request is exactly
`{"pid":<positive 31-bit integer>,"startedAt":"<ISO 8601 time>"}`, and the
answer is `{"ok":true,"state":"running"|"exited"|"unknown"}`. `running` means
the process id names a process whose start time is within 10 ms of `startedAt`;
`exited` means the process id is free or a zombie, or names a process that
started more than one second later; anything else, including a process the
kernel will not show this user, is `unknown`. See
answer is `{"ok":true,"state":"running"|"exited"|"unknown"}`. For canonical
macOS six-fractional-digit UTC identities, `running` requires exact equality;
`exited` means the PID is gone/a zombie or names a strictly later kernel
incarnation, including a one-microsecond difference. An earlier timestamp or
unreadable identity is `unknown`. Legacy timestamps keep the old compatibility
policy: `running` within 10 ms, `exited` only when gone or more than one second
later. Legacy precision must not be used to authorize exact owned cleanup. See
[settlement journal and recovery](../references/security.md#settlement-journal-and-recovery).

The Windows provider additionally implements:
Expand Down
58 changes: 50 additions & 8 deletions packages/agentic/desktop-driver/native/macos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Accessibility.
| `Application.swift` | Launch/attach leases, exact process/window identity, and ownership |
| `ApplicationTermination.swift` | `terminateOwned`: quit, wait, kill, and verify the exit of an owned process |
| `ApplicationIncarnation.swift` | One-shot `--application-incarnation`: whether a recorded process exited |
| `ApplicationLock.swift` | Stable per-user exclusion for an exact application PID/kernel start time |
| `Accessibility.swift` | AX tree walking, lookup, snapshots, focus, hit testing, actions, and source |
| `Observation.swift` | Provider-neutral bounded sampler: request rules, budgets, and BFS result |
| `AccessibilityObservation.swift` | AX source for `observe`: multi-attribute reads, shared ids, `observe` run |
Expand Down Expand Up @@ -84,6 +85,38 @@ The coordinator:
SwiftPM `.build`, DerivedData, editor files, and other generated output never
participate in source identity or staging.

### Application ownership fixture

The native ownership contracts use a small AppKit application, independent of
Storybook and React Native. From this package, build it into an ignored
artifact directory:

```sh
node native/macos/fixtures/build.cjs artifacts/desktop/application-fixture
```

The command prints the verified bundle/executable identity and window title.
The arm64 fixture pins its compiler and bundle minimum macOS version to 11.0,
matching the helper instead of inheriting a newer machine deployment target.
The executable accepts `--title <exact-window-title>` and an optional
`--ready-file <path>` containing its PID after window creation; its Increment button
and counter have accessibility identifiers `ownership-fixture-increment` and
`ownership-fixture-count`. Use the normal native host launch API, or launch the
exact executable externally for attach-preservation tests. The test owner must
stop only that process incarnation. The fixture is supporting native evidence,
not Fabric/Paper renderer qualification. Its main run loop requests termination
after five minutes if the test owner never cleans it up.

For crash-recovery contracts, `--descendant-ready-file <path>` starts a bounded
`/bin/sleep 300` child and writes its PID and parent PID as JSON. The fixture does
not terminate that child when its primary process exits. The contract owner
verifies both native start identities and the parent relationship, proves the
descendant survived, and separately stops that exact descendant. This tests the
limits of `uncontained` ownership; it does not add descendant containment.
The crash-owner Node fixture waits at most two minutes before exiting nonzero;
the helper then observes EOF. These fixture-only retirement bounds do not
settle or remove an abandoned workload claim.

## Signing

### Stable development signing
Expand Down Expand Up @@ -170,15 +203,24 @@ transport: the helper writes a fixed diagnostic and exits with status 1 instead

The one-shot `--application-incarnation` mode reads exactly `{ "pid", "startedAt" }` from stdin and answers whether
that exact process, a process id and its start time, has `exited`, is `running`, or cannot be told (`unknown`).
`exited` is reported only with proof: the process id is gone (a zombie included) or now names a process that
started more than a second later. Settlement recovery uses it to decide whether a launched application that the helper
New leases use kernel start timestamps with exactly six fractional digits.
For these identities, `running` requires equality and `exited` requires a gone/zombie PID
or a strictly later kernel start time; unreadable/earlier identities are unknown.
Legacy lower-precision records retain their historical one-second exit tolerance.
Settlement recovery uses it to decide whether a launched application that the helper
could not prove closed is really gone; [PROTOCOL.md](../PROTOCOL.md) defines its request and answer.

The input lock lives beneath
`~/Library/Caches/com.microsoft.fluentui-react-native.desktop-driver`, so
helpers started through different Aqua bootstrap namespaces still serialize
physical input. Restricted `--release-input` accepts only the Node-owned ledger.
`--sweep` is an operator recovery diagnostic, not normal cleanup.
Exact application leases hold a separate owner-only `flock` beneath that root's
`applications` directory. It excludes other helpers for the same PID and
microsecond start identity, independent of checkouts or temporary directories.
Attached leases only release the lock; they never terminate the app. Launched
leases distinguish `launch-services` from `direct-child` causal ownership and
report `uncontained`: neither promises descendant containment.
The one-shot `--service-identity` mode describes a service process or verifies a
service claim for `describeMacOSService` and `createMacOSServiceVerifier`; it
uses only public process and socket information APIs and links `libbsm` for the
Expand All @@ -192,12 +234,12 @@ resident memory for the life of the session (about 25 KB for a `probe`, 80 KB
for a `tree`, and 250 KB for a 500-element `observe`). A new thread that serves
requests must drain a pool per request too.

The main run loop is also what refreshes `NSWorkspace.runningApplications` and `NSRunningApplication.isTerminated`.
Measured without it, a launched application registers within about 0.2 s, but after the process was killed
`isTerminated` stayed false and the workspace list kept the dead process for the 15 s the probe watched; with the
run loop pumped both updated within 0.25 s. Registration therefore needs no run loop, while exit never trusts that
state: termination verification and lease checks read the kernel through `ApplicationIncarnation`, and attaching
without a lease drops listed processes the kernel no longer knows.
The main run loop refreshes `NSWorkspace.runningApplications` and `NSRunningApplication.isTerminated`.
Without it, the workspace's cached array can omit a newly launched application and retain an exited one.
Descriptor-based discovery therefore enumerates the current user's kernel PIDs
and constructs `NSRunningApplication` instances directly instead of trusting that cached array.
Enumeration failure refuses discovery; exact window and uniqueness checks still apply.
Termination verification and lease checks independently read the kernel through `ApplicationIncarnation`.

## Application and window identity

Expand Down
Loading
Loading