Quickstart
This walkthrough validates ordinary Rust development first. It does not require Android Studio, Xcode, an emulator, a simulator, or a phone.
1. Install cargo-ferry
cargo install cargo-ferry --locked
The package requires Rust 1.92 or newer. Registry installations select the matching rustferry runtime automatically and need no path override. See Installation for source-development commands and mobile toolchains.
2. Generate the starter
cargo ferry new weather \
--id com.example.weather
cd weather
Generation is staged in a temporary sibling directory and refuses to overwrite an existing destination. By default it initializes Git and runs cargo check; use --no-git or --no-check to opt out.
Open src/app.rs first. The starter keeps its Slint UI, click handlers, lifecycle binding, async notification request, and error presentation together. State and the network service are in small neighboring modules.
3. Check normal Rust code
cargo ferry check
cargo test
cargo ferry check validates strict ferry.toml configuration before invoking cargo check.
4. Inspect prerequisites
cargo ferry doctor --all
Doctor is read-only. cargo ferry doctor --fix --dry-run prints suggestions; automatic mutation is not currently enabled.
5. Build an artifact
Android build is build-only: it must not require or contact a device.
cargo ferry build android
On macOS with full Xcode:
cargo ferry build ios --simulator
A zero exit from a finished platform build means the produced artifact passed the pipeline’s inspections. Dry-run output, generated manifests, or skipped CI jobs are not artifact evidence. Check Support matrix and Implementation status for the exact validated level in this revision.
Deployment is a separate, explicit level:
cargo ferry devices
cargo ferry install android --device SERIAL
cargo ferry run ios --simulator SIMULATOR_UDID
The current evidence does not include a running emulator, Simulator, or physical device.
6. Add capabilities deliberately
cargo ferry add notifications
cargo ferry add widget
cargo ferry capabilities
Permission prompts are never automatic. Application code chooses the user-initiated moment.
Slint license choice
Starter UI uses Slint and retains an accessible AboutSlint component. Before distributing binaries, choose and satisfy GPL-3.0, Slint’s royalty-free application license and attribution condition, or a commercial license. See ADR-001. This is not legal advice.
Next: Project structure, Configuration, and State and events.
Installation
Host-only development
Required:
- Rust and Cargo 1.92 or newer;
- Git when
cargo ferry newshould initialize a repository.
Install the public pre-release from crates.io:
cargo install cargo-ferry --locked
cargo ferry new weather --id com.example.weather
cargo ferry doctor --all
cargo ferry check
Development from source
Install from a checkout for contributor development and select its runtime explicitly:
cargo install --locked --path crates/cargo-ferry
cargo ferry --version
cargo ferry new weather --runtime-source path --runtime-path "$PWD/crates/rustferry"
Normal generation writes an exact registry dependency and does not contain a developer checkout path. Contributors can select --runtime-source workspace or --runtime-source path --runtime-path ABSOLUTE_PATH. CARGO_FERRY_RUNTIME_PATH remains an optional development override; it must be UTF-8, absolute, canonicalizable, and contain Cargo.toml. Invalid inputs fail before files are written.
Checking a generated Slint/Skia application on Linux also requires pkg-config and the system Fontconfig development package (libfontconfig1-dev on Debian and Ubuntu). RustFerry reports the underlying Cargo diagnostic when either prerequisite is missing.
PowerShell equivalent:
cargo install --locked --path crates/cargo-ferry
cargo ferry --version
cargo ferry new weather --runtime-source path --runtime-path (Resolve-Path crates/rustferry).Path
Android artifacts
Android builds require an Android SDK platform, Build Tools (aapt2, d8, zipalign, apksigner), an NDK with LLVM, Java/Javac, and each configured Rust Android target. A connected device and ADB are not build prerequisites.
Use:
cargo ferry doctor --all
Then follow Android setup. cargo-ferry does not invoke sudo, accept licenses, or silently download executables.
iOS Simulator artifacts
iOS builds require macOS, full Xcode with an iPhone Simulator SDK, and aarch64-apple-ios-sim for Apple Silicon builds. A booted Simulator, Apple account, physical iPhone, and development signing are not build-only prerequisites.
Follow iOS setup. Physical-device builds have separate signing/provisioning requirements and a separate validation level.
Remote physical-iPhone artifacts
Linux and Windows clients do not install Xcode or an Apple SDK. They can use the configured GitHub
provider for an exact-revision build, or explicitly select a named SSH Mac for deterministic
unsigned snapshot session v1. The macOS worker still needs full Xcode, iPhoneOS, Rust/Cargo/rustup,
and aarch64-apple-ios. The accepted GitHub evidence is an unsigned XCArchive; the SSH path has no
live artifact evidence, and neither path has validated a real development-signed IPA or device
runtime. See physical iPhone development.
Shell completions
Generate definitions to a file appropriate for your shell:
cargo ferry completions zsh > cargo-ferry.zsh
Supported shell names come from Clap; run cargo ferry completions --help for the active list.
Project structure
The default starter is a normal Rust package. Platform scaffolding is generated later and never becomes application source.
weather/
├── .gitignore
├── Cargo.toml
├── Cargo.lock
├── ferry.toml
├── README.md
├── assets/
│ ├── icon.png
│ └── splash.png
├── src/
│ ├── main.rs
│ ├── lib.rs
│ ├── app.rs
│ ├── state.rs
│ ├── capabilities/
│ │ └── mod.rs
│ └── services/
│ ├── mod.rs
│ └── network.rs
└── tests/
└── basic.rs
src/app.rs: Slint component, window setup, click handlers, lifecycle/network subscriptions, async notification flow, and error display.src/state.rs: serializable application state throughrustferry::storage::Store.src/services/network.rs: OS path status and explicit endpoint probe kept separate.src/lib.rs: shared entry point and guarded Androidandroid_mainsymbol.src/main.rs: short host/Apple executable entry point.src/capabilities/mod.rs: generated capability module index;cargo ferry addadds focused Rust files beside it.ferry.toml: versioned identity, target, capability, permission-derived, and extension configuration..gitignore: excludes Rust and generated RustFerry build output.assets/: validated 1024×1024 opaque RustFerry-branded default PNGs. Replace them for product identity;cargo ferry assets checkvalidates source constraints before platform generation. The canonical editable vector source isdocs/assets/rustferry-icon.svg.tests/basic.rs:TestRuntimeexample with no mobile SDK.
Capability scaffolds are placed below src/capabilities/ only when missing; existing application files are not force-rewritten. Widget and Live Activity templates add restricted Rust snapshot modules rather than user-authored Kotlin or Swift.
Build output belongs below:
target/ferry/
├── assets/
│ └── <source-fingerprint>/
├── android/
└── ios/
The fingerprint cache contains a SHA-256 manifest, Android density resources, and the iOS catalog inputs consumed by runtime-present builds. Treat generated hosts and caches as disposable. Never put signing files there as their only copy. See Generated files.
Configuration
ferry.toml uses schema version 1, rejects unknown fields, and is semantically validated before expensive work. A representative configuration is:
schema_version = 1
platforms = ["android", "ios"]
[app]
name = "Weather"
identifier = "com.example.weather"
version = "0.1.0"
display_version = "0.1.0"
[app.window]
orientation = "automatic"
theme = "system"
[android]
min_sdk = 26
target_sdk = "installed"
abis = ["arm64-v8a"]
[ios]
min_version = "16.0"
[capabilities.network]
mode = "status"
probe_timeout_ms = 3000
[capabilities.notifications]
local = true
push = false
[capabilities.storage]
enabled = true
[capabilities.haptics]
enabled = true
[capabilities.clipboard]
enabled = false
[capabilities.share]
enabled = false
[capabilities.deep_links]
schemes = ["weather"]
allowed_hosts = []
allowed_actions = []
[extensions.widget]
enabled = false
[extensions.live_activity]
enabled = false
android_fallback = "ongoing-notification"
Inspect and validate
cargo ferry config validate
cargo ferry config show --resolved
cargo ferry config schema > ferry.schema.json
cargo ferry --dry-run config migrate
The active parser validates application identifiers, nonempty platforms and ABIs, Android SDK bounds, iOS major/minor syntax, network mode/probe conflicts, remote-push rejection, deep-link schemes, widget app groups, and the iOS 16.1 Live Activity floor.
Schema mismatches are rejected. cargo ferry config migrate upgrades a supported older schema atomically after parsing and validating the result; dry-run reports the proposed version change without writing. A newer, unknown schema is never downgraded—use a cargo-ferry version that understands it.
Network modes
none: no status/probe configuration; omit related platform permissions where possible.status: observe the OS path only.optional: application may operate offline and may probe explicitly.required: application code uses an explicitnetwork::require_online()gate; build never tests the developer machine’s internet connection.
An OS path does not prove a backend is reachable. probe_url, when used, belongs to the application and must be paired with a nonzero timeout.
Extensions
A widget needs an app_group, normally group.<app.identifier>. Enabling Live Activity raises the configured iOS minimum to at least 16.1 and keeps Android’s honest ongoing-notification fallback.
Secrets
Never store keystore passwords, private keys, tokens, certificates, or provisioning profiles in ferry.toml. Machine-specific and signing data belong in platform credential stores, environment input, protected files, or cargo-ferry’s system configuration directory. See Security policy.
CLI reference
Commands work as a Cargo subcommand (cargo ferry ...) or direct binary (cargo-ferry ...). Run --help on the installed revision for exact parsing.
Global flags
--verbose: external-command/discovery detail in human mode; conflicts with--quiet,--json, and--json-stream; secrets must be redacted.--quiet: suppress successful human output; conflicts with--verbose,--json, and--json-stream.--json: schema-versioned JSON without terminal styling; conflicts with human verbosity flags and--json-stream. Current output schema is version 1.--json-stream: protocol-v1 NDJSON forideoperations,devices, live applicationlogs, andjobs logs; conflicts with human verbosity flags and--json. Other commands reject it and use--jsoninstead.--dry-run: validate and show intended mutations where the command supports planning.
Commands
| Command | Current contract |
|---|---|
new <name> | Atomic generation; --display-name, --id, --template, --platform, runtime source controls, --no-git, --no-check, --parent |
add <capability> | Preserve TOML formatting, enable config/Cargo feature, create a missing example module, support dry-run |
remove <capability> | Disable config/Cargo feature; preserve example source |
check | Validate config, then run ordinary cargo check |
doctor [--all] | Read-only host/toolchain inventory |
doctor --fix --dry-run | Print fixes; automatic mutation is not implemented |
build android | Build-only Android request; platform readiness and validation level are in STATUS |
build ios --simulator | Build-only Simulator request; no automatic boot/install/launch |
build ios --device --team <id> | Implemented official arm64/Xcode development-signing build; provisioning updates remain explicit; no identity, Team, profile, or signed artifact was available for artifact validation, and no device was available for device validation |
build ios --device | Uses GitHub automatically on Linux/Windows; remains local by default on macOS; an explicit --remote always wins |
build iphone --unsigned | Remote-only alias; defaults to GitHub when --remote is omitted, submits an exact source revision, then rehashes, inspects, and atomically publishes the downloaded unsigned physical-device archive |
build iphone --remote github --snapshot --unsigned | Explicit public GitSnapshot of the canonical current project; dry-run is zero-write, interactive execution asks [y/N], and JSON/non-interactive execution requires --yes |
build iphone --team <id> | Defaults to protected GitHub Apple Development signing when --remote is omitted; implemented and synthetically tested, but no real signed IPA acceptance has run |
remote setup github | Validate source/execution Git remote identities, generate the trusted workflow, and persist ignored provider metadata; signing requires a distinct private execution repository |
remote doctor github | Read-only provider, repository, and workflow health; use signing doctor --remote github for protected-signing readiness |
remote add ssh-mac <name> | Persist a create-only named endpoint after validating an exact dedicated known_hosts entry, pinned host-key fingerprint, and optional private-key path reference |
remote doctor <name> | Run the fixed-command SSH worker handshake and host doctor; readiness requires snapshot/unsigned/XCArchive/events/cancellation/download/cleanup and retention zero, but is not live-build evidence |
build iphone --remote <name> --unsigned | Create a deterministic snapshot, use fixed SSH session v1, stream ordered events, independently verify and create-only publish the unsigned XCArchive, acknowledge it, then require non-retaining cleanup |
remote bundle inspect | Print the deterministic snapshot manifest, path dependencies, rejected-symlink set, excluded sensitive roots, sizes, executable bits, and SHA-256 digests |
remote bundle create | Create a no-clobber deterministic source ZIP and separate versioned descriptor; global --dry-run writes neither |
remote bundle verify | Treat ZIP and descriptor as untrusted, perform bounded extraction, and require exact manifest/archive integrity |
jobs list [--limit <1..1000>] | List bounded newest private project-bound durable jobs |
jobs show <job-id> / jobs artifacts <job-id> | Show one secret-free durable job or its recorded artifact metadata |
jobs logs <job-id> | Refresh and read bounded sanitized durable lifecycle/worker events; --follow, --since, --phase, and create-new --output are supported |
jobs cancel <job-id> | Persist exact owned cancellation intent before at most one provider request, then reconcile terminal state and cleanup across restarts |
jobs retry <job-id> | Create/resume one exact-source child; retrying a fully evidenced successful parent requires --force; current-source recapture requires --use-current-source --yes |
jobs prune --before <unix-ms> | Plan or, with --yes, remove complete terminal retry lineages only after local artifact removal and retained-source release authorization |
artifact list|show|inspect|verify|reveal|remove | Manage durable local artifact evidence; bare IDs must resolve uniquely, exact removal is Windows-only and requires --yes, and local removal never means remote Actions deletion |
devices [--platform all|android|ios] | Typed ADB/simctl/devicectl inventory; --watch --json-stream emits the initial snapshot followed by polling deltas until cancelled |
install android|ios | Build, independently validate, select an exact compatible device, then install |
run android|ios | Build → validate → install → launch; --logs adds one bounded filtered snapshot where standalone logging is supported |
logs android|ios | Finite application-filtered history by default; --json-stream runs the live protocol stream until cancellation or platform-tool exit |
signing teams | Read-only Apple Development identity/Team inventory |
signing doctor --remote github | Metadata-only signing readiness; non-ready returns github_signing_not_ready without reading secret values |
signing setup manual | Validate one PKCS#12 plus one profile per application/extension target outside Git, check protected GitHub Environment policy, and upload secrets only after dry-run review and confirmation |
assets check|generate | Validate release sources; generate fingerprinted Android densities and an iOS asset catalog |
clean [android|ios|generated] | Remove only selected generated output below target/ferry/ |
clean --all | Remove cargo-ferry output below target/ferry/, not application source/signing inputs |
config validate | Strict parse and semantic validation |
config show --resolved | Print resolved defaults |
config schema | Print JSON Schema |
config migrate | Atomically upgrade a supported older schema; use global --dry-run to inspect first |
capabilities | List known runtime/platform state and current enablement when inside a project |
examples | List bundled template choices and generation commands |
docs [topic] | Show the source-tree page when available; otherwise print packaged embedded content |
completions <shell> | Generate shell completion definitions |
ide <operation> | Direct protocol-v1 JSON/NDJSON for editor integrations, including durable jobs/log pages/cancel/retry/artifacts, snapshot preview/submit, and signing readiness |
Goal 3 IDE-v1 operations are jobs-list, jobs-show, jobs-artifacts, jobs-logs,
jobs-logs-page, jobs-cancel, jobs-retry, jobs-artifact-verify,
jobs-artifact-reveal, jobs-artifact-remove, remote-build-preview,
remote-build-submit, and signing-readiness. Legacy jobs-logs remains a finite timestamp
snapshot; the UI uses decimal-cursor jobs-logs-page. Preview and submit are capability-co-gated,
and submit accepts one bounded consent object on standard input.
Capabilities accepted by add/remove: network, notifications, storage, haptics, clipboard, deep-links, share, widget, and live-activity.
Manual GitHub signing accepts at most three profiles. An extension-free project may retain the
legacy --profile PATH form. A project with Widget or Live Activity targets must pass repeatable,
exact --profile TARGET=PATH arguments for the application and every extension; keyed and unkeyed
forms cannot be mixed. The profiles must share the selected registered device and match their target
bundle identifiers, Team, certificate, validity, and required entitlements. Multi-profile secret
input uses RFSIGNV2; the legacy worker frame remains single-application-only.
Templates accepted by new: starter, minimal, counter, network, notifications, widget, live-activity, and kitchen-sink. They share a template engine and feature fragments rather than copied project trees.
Runtime controls are source-specific. --runtime-source registry accepts an optional semantic --runtime-version; --runtime-source workspace accepts neither version nor path; --runtime-source path requires --runtime-path naming an absolute, existing directory containing Cargo.toml. Version/path flags are rejected without an explicit source. With no runtime flag, generation uses the CLI’s registry version unless the contributor-only CARGO_FERRY_RUNTIME_PATH override is set.
Device watch mode requires --json-stream. It emits the current devices and warnings first, then polls at --interval-ms (2,000 ms by default, clamped to 500–60,000 ms) and emits only added, changed, removed, or warning changes. Ctrl+C ends watch mode with cancellation status.
logs without --json-stream collects a finite snapshot using --since-seconds, --max-entries, --max-bytes, and --level. --json-stream selects continuous, application-filtered Android or iOS Simulator logging and emits protocol events incrementally. Standalone physical-iOS logs is currently unsupported; CoreDevice console attachment is not exposed as this command.
build never discovers, boots, installs on, or launches a device. Those side effects exist only behind the explicit deployment commands. Android reinstall/downgrade/permission grant/data clear, Simulator boot, process termination, and Xcode provisioning updates are opt-in.
SSH snapshot v1 is unsigned-only. A signed request or --team fails explicitly; it is never
downgraded. Named SSH endpoints are always selected explicitly; omission never falls back from
GitHub to a configured SSH endpoint. The returned XCArchive ZIP is not an IPA and is not installable
on a stock iPhone. Unsigned remote archives are published at
target/ferry/ios/device/<debug|release>/<Product>-unsigned.xcarchive.zip.
GitHub GitSnapshot is also unsigned-only and explicit. Preview binds the invocation, exact source manifest, public repository/ref, retention, and side effects; archive construction occurs only after consent. The caller branch, worktree, index, remotes, and hooks are unchanged. Public Git objects may remain recoverable after RustFerry deletes its temporary ref.
Durable job logs use log_scope=durable_sanitized_job_events. Raw provider payloads and raw worker
bytes are never stored. provider_full_logs=true requires an exact completion proof for the current
run attempt. Local artifact removal and job pruning do not delete GitHub Actions artifacts.
JSON failures
Failures include schema_version, status, and a stable error object with code, message, optional help, and safe details. Nonzero exit classes distinguish usage/configuration, missing/unsupported prerequisites, external command failure, and filesystem/safety failure.
Architecture
RustFerry separates application intent from disposable platform packaging.
Rust application + ferry.toml
|
cargo-ferry CLI
/ | \
config codegen build routing
| / \
target/ferry/ local remote
hosts | / \
/ \ Xcode GitHub named SSH
Android Apple | |
| | macOS worker snapshot worker
inspected APK/.app verified returned artifact
Workspace responsibilities
cargo-ferry: command parsing, project discovery, human/JSON reporting, orchestration.rustferry-core: strict configuration, naming, validation, schema, shared platform types.rustferry-codegen: atomic user-project generation and deterministic template fragments.rustferry(crates/rustferry): public capability API, event bus, storage, backend contract, andTestRuntime.rustferry-android: SDK/NDK discovery, direct packaging plans/builds, signing, and independent APK checks.rustferry-apple: Xcode/SDK discovery, generated-host plans/builds, and Apple bundle checks.rustferry-remote: protocol v1, deterministic source manifests, build/signing contracts, events, cancellation, sealed artifact models, and cross-platform Apple artifact inspection.rustferry-github: exact-revision GitHub transport, workflow/provider policy, and Actions artifact ingestion.rustferry-ssh: fixed-argument pinned OpenSSH transport and snapshot-session v1 client.rustferry-worker-macos: non-publishable trusted macOS worker for GitHub and stdio snapshot sessions.
Physical-iPhone routing
cargo ferry build iphone is always remote and defaults to GitHub when --remote is omitted. On
Linux and Windows, cargo ferry build ios --device also defaults to GitHub; on macOS it remains the
local Xcode path. A named SSH endpoint is never inferred and must be selected explicitly. SSH
snapshot session v1 accepts only deterministic snapshot source, unsigned compile-only signing mode,
and one XCArchive result. GitHub remains the only provider with live no-Mac artifact evidence and
the only implemented protected signing path; that signed path has synthetic validation only.
Rust-only application boundary
Application authors edit Rust and assets. Android Java/DEX or Apple Swift/Objective-C/Xcode metadata may exist when a system API requires them, but cargo-ferry generates it below target/ferry/. Generated glue contains adaptation, not application business logic.
Runtime boundary
Public capability functions look up an installed Runtime. Each backend advertises granular Operation support; absent operations return typed Unsupported errors instead of fake success. TestRuntime installs a thread-scoped deterministic backend for host tests.
Event subscriptions are owned values. Dropping a subscription prevents later callback starts; one source preserves its serial order, while concurrent sources may interleave. Mobile operating systems may omit termination/background events.
Platform packaging
Android’s default design invokes Cargo, NDK LLVM, aapt2, optional javac/d8, zipalign, and apksigner directly. It does not create a Gradle project in user source. See Android without Gradle.
Apple builds generate a hidden host project and metadata, then invoke official Xcode tooling. This is not a claim of pure Rust internals; it is a Rust-only application-source contract. See Apple generated host.
Trust and evidence
Paths, config, assets, metadata, external tools, callbacks, and archives cross trust boundaries. The threat model defines controls. The support matrix and status log separate code existence from compile, artifact, simulator/emulator, and device evidence.
Support matrix
This matrix deliberately separates validation levels. “Model tested” means host-side API/model behavior passed deterministic tests; it is not mobile runtime evidence. STATUS records general platform evidence; Goal 3 status is the dated authority for the remote-iPhone continuation and overrides stale Goal 3 prose.
Validation vocabulary
| Level | Required evidence |
|---|---|
| Implemented | Concrete code path exists; unsupported defaults do not count |
| Locally tested | Focused deterministic tests passed on a development host |
| Windows-native tested | Exact behavior passed on a real Windows host |
| GitHub live validated | Real GitHub mutation/run completed with retained exact identifiers |
| Apple signed validated | Real Apple credentials produced independently verified signed output |
| Physical-device validated | The exact signed artifact was installed, launched, and observed on registered hardware |
Build-path tables additionally use compile, artifact, and simulator/emulator evidence. Those narrower results never imply a higher Goal 3 level.
Goal 3 physical-iPhone scenarios
These are the 18 scenarios named by the Goal 3 acceptance specification. A status applies only to the evidence in the last column; for example, the validated GitHub result is an unsigned XCArchive, not a signed IPA or a physical-device run.
Status legend: ✅ artifact validated; 📱 physical-device validated; 🧪 implemented with hardware or signing validation pending; 🟡 partial; 🚫 unsupported; 📋 planned. No current Goal 3 scenario has physical-device validation.
| Scenario | Status | Exact evidence / limitation |
|---|---|---|
| iOS Simulator local macOS | ✅ artifact validated | arm64 .app and .appex bundles built and independently inspected; no Simulator runtime observation |
| Physical iPhone local macOS | 🧪 implemented, hardware/signing validation pending | Official local Xcode development-signing/install/launch path exists; no real Team, profile, signed device artifact, or attached iPhone |
| Physical iPhone from Windows via GitHub | 🟡 partial | Durable control plane, GitSnapshot, artifacts, and native Windows suites pass; no live Windows-to-GitHub physical-iPhone build/download run |
| Physical iPhone from Linux via GitHub | ✅ artifact validated | Linux acceptance 31261962599 triggered macOS worker 31262066567, downloaded, hashed, and inspected an unsigned physical-device XCArchive |
| Physical iPhone via SSH Mac | 🧪 implemented, hardware/signing validation pending | Named endpoint and unsigned snapshot session are locally tested; no live SSH Mac compile, SSH artifact, or signing support |
| Unsigned device compile | ✅ artifact validated | Real aarch64-apple-ios/iphoneos XCArchive built by the GitHub macOS worker and independently revalidated on Linux |
| Development signing | 🧪 implemented, hardware/signing validation pending | Signing engine and protected GitHub phase use synthetic fixtures only; no real Apple identity/profile run |
| Manual development signing | 🧪 implemented, hardware/signing validation pending | Bounded app/Widget/Live Activity profile mapping, exact target-graph binding, and protected secret transport pass local integration tests; real asset upload remains pending |
| Personal Team | 🚫 unsupported | GitHub, SSH, and worker capability reports disable Personal Team; no headless Personal Team flow exists |
| Widget device signing | 🧪 implemented, hardware/signing validation pending | Remote manual setup accepts an exact Widget profile and static protected secret; no real development-signed Widget artifact or device run exists |
| Live Activity device signing | 🧪 implemented, hardware/signing validation pending | Remote manual setup accepts an exact Live Activity profile and static protected secret; no real development-signed Activity artifact or device run exists |
| GitHub Actions provider | ✅ artifact validated | Historical Linux Push exact-Git unsigned build/download accepted; GitSnapshot, Windows, cancellation/retry, and WorkflowDispatch paths have no live result |
| SSH provider | 🧪 implemented, hardware/signing validation pending | Handshake, doctor, source upload, events, cancel, XCArchive receipt, and cleanup pass deterministic tests; v1 is unsigned-only |
| Windows client artifact download | 🧪 implemented, live validation pending | Download/verification/publication and managed artifact commands pass native suites; no live Windows client acceptance run |
| Linux client artifact download | ✅ artifact validated | Acceptance 31261962599 automatically downloaded and independently verified artifact 9023136948 |
| Physical install | 🧪 implemented, hardware/signing validation pending | Typed devicectl install service exists; no signed downloaded IPA or attached-device install was exercised |
| Physical launch | 🧪 implemented, hardware/signing validation pending | Typed devicectl launch service exists; no physical launch was exercised |
| Physical logs | 🚫 unsupported | Standalone physical-iOS log streaming is not exposed; no device runtime logs were collected |
Platform build paths
| Path | Implemented | Compile | Artifact | Simulator/emulator | Device |
|---|---|---|---|---|---|
| Host config/runtime/tests | Yes | Host workspace checks; see STATUS | N/A | N/A | N/A |
| Android direct APK | Build plus typed devices/install/run/logs implemented; user project remains Rust-only without Gradle or Android Studio | aarch64-linux-android generated Rust app plus Java/DEX bridge | GitHub-hosted Ubuntu run 31590994094 artifact-validated the retained v2/v3-signed, basic/16 KiB-aligned arm64 APK from exact production source ed45328d6fc375e81b20ab10c1014c4b8d224a85; the Windows-built Calculator artifact was independently inspected | Emulator runtime not validated | Calculator startup, JNI dispatch, and interaction confirmed on one physical Android device; generic install/run/log flow and broader runtime remain unvalidated |
iOS Simulator .app | Build plus typed devices/install/run/logs implemented | arm64 Slint executables | Public-CLI starter and Kitchen Sink .app/.appex bundles independently inspected | Deployment runtime not validated | N/A |
| iOS physical-device app, local Mac | Official development-signing build/install/run implemented; explicit Team and provisioning controls | Deterministic arm64/Xcode plan tested; no local signing identity available | Signed app not validated | N/A | Not validated |
| iOS physical-device archive, GitHub remote macOS | Exact-Git and explicit public GitSnapshot submission, trusted macOS compile, automatic download, independent client validation, and durable jobs implemented | Real aarch64-apple-ios archive compiled only from a historical Linux exact-Git request | Unsigned .xcarchive validated for historical exact-Git; GitSnapshot and development signing not live-validated | N/A | Not validated |
| Deterministic snapshot transport | Inspect/create/verify plus explicit GitHub GitSnapshot consent/staging/recovery implemented | Host and Windows-native tests only | Source ZIP/descriptor round-trip validated in tests; no GitSnapshot-built GitHub artifact | N/A | N/A |
| SSH Mac provider | Pinned endpoint, handshake/doctor, snapshot-v1 unsigned build, and private Unix/Windows config/operation staging implemented; deterministic local tests only | No live SSH Mac compile | No SSH-produced artifact; protocol returns unsigned XCArchive only | N/A | Not validated |
Goal 3 Windows control plane
| Area | Implemented | Locally tested | Windows-native tested | GitHub live validated | Apple signed validated | Physical-device validated |
|---|---|---|---|---|---|---|
| Durable jobs and sanitized logs | Yes | Yes | Yes | No Windows result | N/A | N/A |
| Fresh-process cancel/retry/prune | Yes | Yes | Yes | No | No | No |
| Managed local artifacts | Yes | Yes | Yes | No Windows management result; historical Linux download only | No | No |
| Explicit GitHub GitSnapshot | Yes | Yes | Yes | No | No | No |
| VS Code Remote Jobs | Yes | Yes | Yes | No | No | No |
| Signing readiness | Yes | Yes | CLI path tested; no configured ready result | No signed run | No | No |
Capability evidence
| Capability | Host model/tests | Android backend/artifact | iOS backend/artifact | Runtime observation |
|---|---|---|---|---|
| Lifecycle/event bus | Implemented | Backend and DEX/native callback bridge compiled into inspected APK | Backend and dynamic framework artifact-inspected | None |
| Network status/probe | Implemented model and mock | Connectivity/HTTP backend enabled and bridge artifact-inspected | NWPath/URLSession backend and framework artifact-inspected | None |
| JSON storage | In-memory/file backend tests | Application-private file backend installed by Android host; target-compiled | Application Support file backend and framework artifact-inspected | None |
| Haptics | API and mock | Backend enabled and bridge artifact-inspected | Backend and framework artifact-inspected | None |
| Clipboard/share/system | API and mock | Backends compiled; enabled share provider artifact-inspected | Backends and framework artifact-inspected | None |
| Deep links | Parser/policy/event tests | Intent filter/allowlist backend and bridge artifact-inspected | URL scheme/delegate allowlist and framework artifact-inspected | None |
| Local notifications | API/model/mock | Backend/receiver enabled and artifact-inspected | UserNotifications backend and framework artifact-inspected | None |
| Permissions | API/model/mock | Enabled purpose strings, exact permissions, and bridge artifact-inspected | Supported permission backends and framework artifact-inspected | None |
| Widget | Snapshot model/tests + standalone example | Provider/backend enabled and artifact-inspected | Publisher, timeline renderer, and WidgetKit .appex artifact-inspected | None |
| Live Activity | State model/tests + standalone example | Ongoing-notification fallback enabled in an inspected Kitchen Sink APK | ActivityKit lifecycle bridge and .appex artifact-inspected | None |
Application notification remote push is unavailable; schema version 1 rejects push = true. This is unrelated to the GitHub provider’s Push workflow trigger. Device discovery and deployment use exact stable IDs, validated artifacts, bounded application-filtered logs, and official ADB/simctl/devicectl commands. No device behavior is inferred from those implemented paths.
CI interpretation
Linux, macOS, and Windows host jobs run independently. Android and Apple artifact jobs are not repository-variable gates: they install/select their build prerequisites, invoke real pipelines, repeat independent checks, and upload only non-empty expected artifacts. The remote physical-iPhone acceptance additionally proves that a Linux client has no local Apple toolchain, binds an exact source revision, and revalidates the macOS-produced archive after automatic download. A cancelled or skipped job means “no evidence,” never “passed”; missing prerequisites, a failed build, or a missing/invalid artifact fails the job.
Distribution is a public pre-release. Registry installations generate starters with the matching exact rustferry version and require no checkout path override. Release preparation built and independently validated a signed/aligned arm64 APK from a fresh Windows source-checkout project, while a separate Calculator artifact has one physical-device launch and interaction acceptance. Generic Android device install/run/log coverage and Windows-originated GitHub/macOS iPhone acceptance remain unvalidated. iPhone builds require local or remote macOS with full Xcode and the official Apple toolchain; the live-proven output is unsigned XCArchive evidence, not a signed IPA, install, launch, logs, or physical-device runtime result.
RustFerry IDE protocol
The RustFerry IDE protocol is the stable machine boundary between cargo-ferry and editor clients. Rust owns project parsing, validation, builds, deployment, signing, artifact metadata, and diagnostics. Clients must not parse human CLI output as a fallback.
Protocol version 1 uses UTF-8 JSON. Unary commands write one JSON object. Long-running commands write newline-delimited JSON (NDJSON): one compact, complete object followed by \n per event. Protocol stdout never contains ANSI styling, progress bars, raw child-process output, or binary data.
The canonical generated schema is ../schemas/ide-protocol-v1.schema.json. Rust structs under crates/cargo-ferry/src/ide/ are the source of truth. cargo ferry ide schema --json prints the schema represented by the running executable.
Negotiation
Run a handshake before any other operation:
cargo ferry ide handshake --json
The direct response contains:
protocol_version: selected version; currently1;tool: executable name and package version;host: extension-host operating system and architecture;supported_protocol_versions;supported_platforms;supported_commands;supported_event_types;features: explicit booleans for build, deployment, logs, physical iOS, and cancellation;build: profile, host target, development-build state, and optional injected Git commit;runtime_dependency: whether project-generation runtime resolution is usable and whether it usesregistryor an explicit developmentpath;templates: generator-owned IDs and descriptions.
Clients must reject a protocol_version outside their supported range. Feature availability comes from the response, not OS guessing.
Unary commands
cargo ferry ide project --workspace /absolute/project --json
cargo ferry ide validate --workspace /absolute/project --json
cargo ferry ide doctor --workspace /absolute/project --all --json
cargo ferry ide devices --platform all --json
cargo ferry ide signing-teams --workspace /absolute/project --json
cargo ferry ide schema --json
project returns canonical absolute paths, application identity, crate name, versions, platforms, enabled capabilities, resolved Android/iOS configuration, the generated-output boundary, and template metadata.
validate returns every available diagnostic. A configuration problem is a successful protocol exchange with valid: false; it is not a bootstrap failure. Diagnostics include an absolute file path and a zero-based, half-open range. character counts UTF-16 code units so it maps directly to VS Code positions. A safe fix is present only when Rust supplies an exact edit or registered command.
Editor clients validate unsaved text without writing it to disk or placing it in arguments:
cargo ferry ide validate --workspace /absolute/project --manifest-stdin --json
With --manifest-stdin, the command reads at most 1 MiB of UTF-8 ferry.toml source from standard input. Rust still resolves the real project and reports the canonical manifest path, while ranges refer to the supplied source. Omitting the flag validates the saved file. Clients must discard results when the editor URI, document version, content digest, or dirty state changes during the request. Disk-backed quick fixes must not be offered for dirty-source results.
devices invokes installed ADB/CoreSimulator/CoreDevice tools through argument arrays. Platform failures are independent warnings, so one missing tool does not hide other device families. Each record reports build, install, launch, and application-log capabilities independently. devices --watch --json-stream polls every 2,000 ms by default; --interval-ms is clamped to 500–60,000 ms. Watch mode emits one initial snapshot, suppresses identical later snapshots, emits added/changed device records and device_removed, and stops through the normal cancellation path.
signing-teams returns installed usable Apple Development identities as non-secret team_id, identity-label, and public certificate-fingerprint fields. It never returns a private key, credential, or provisioning profile.
Unary bootstrap failures have this shape:
{
"protocol_version": 1,
"error": {
"code": "project_not_found",
"message": "No RustFerry application was found",
"help": "Open a directory containing ferry.toml and Cargo.toml"
}
}
Unknown optional fields may be ignored. Missing required fields are incompatible input.
Streaming commands
cargo ferry ide check \
--workspace /absolute/project \
--operation-id vscode:check-8 \
--json-stream
cargo ferry ide build \
--workspace /absolute/project \
--platform android \
--profile debug \
--operation-id vscode:build-42 \
--json-stream
cargo ferry ide install \
--workspace /absolute/project \
--platform android \
--device emulator-5554 \
--operation-id vscode:install-7 \
--json-stream
cargo ferry ide run \
--workspace /absolute/project \
--platform ios-simulator \
--device 00000000-0000-0000-0000-000000000000 \
--json-stream
cargo ferry ide build \
--workspace /absolute/project \
--platform ios-device \
--team ABCDE12345 \
--json-stream
cargo ferry ide install \
--workspace /absolute/project \
--platform ios-device \
--device 00008110-000000000000001E \
--team ABCDE12345 \
--json-stream
cargo ferry ide logs \
--workspace /absolute/project \
--platform android \
--device emulator-5554 \
--json-stream
--operation-id is optional. When omitted, the CLI creates an opaque UUID. A caller may supply 1–128 ASCII letters, digits, ., _, :, or -. --parent-operation-id links nested operations.
check runs Cargo’s JSON message stream through Rust-owned decoding. Compiler diagnostics use canonical absolute paths, zero-based half-open UTF-16 ranges, severity, code, help, and documentation URLs. Diagnostics are emitted before the terminal event even when compilation fails, so editor Problems remain useful. The ordinary human cargo ferry check command renders the same diagnostics into a readable bounded log under target/ferry/logs/.
Every event has these fields:
| Field | Type | Meaning |
|---|---|---|
protocol_version | integer | Always 1 for this protocol |
event | string | Event discriminator |
operation_id | string | Same value for the full operation |
parent_operation_id | string, optional | Enclosing operation |
timestamp_ms | integer | UTC Unix epoch milliseconds |
Event order is deterministic for a given operation path. Object field order is stable but clients must not depend on JSON key order.
install and run require one exact stable device ID. They first produce a fresh debug artifact through the ordinary builder, require completed independent validation, structurally recheck that output, then call only official platform tools with argument arrays. Conservative defaults do not clear Android application data, request a downgrade, boot a shutdown Simulator, or terminate an existing process. run installs before launch and emits application_started only after the platform confirms startup.
ios-device build, install, and run require --team. --provisioning-profile NAME_OR_UUID selects manual signing and --allow-provisioning-updates explicitly permits Xcode provisioning mutation; both require a Team and neither is enabled implicitly. The physical builder cross-compiles aarch64-apple-ios, stages it in the generated Xcode host, signs through Xcode, then independently checks the expected executable and Mach-O identity, architectures, recursive signatures, leaf certificate/profile authorization, Team, entitlements, and exact profile expiration before emitting an artifact. Install and Run then use the exact CoreDevice identifier.
An explicit --artifact is rejected until RustFerry can load persisted validator-owned metadata for it; a path or extension alone is never treated as deployment proof. Physical iOS deployment uses the implemented development-signed builder and rejects missing or inconsistent Team, executable, signature, certificate, profile, entitlement, and artifact evidence as typed protocol errors.
logs remains active until cancellation or platform-tool exit and emits each application record as a complete NDJSON event as soon as it is decoded. Android uses adb logcat --pid for the exact running package process; Simulator unified logging uses log stream --style ndjson with the exact project process/bundle predicate. It never clears or substitutes the global system log. One source line is capped at 256 KiB, the reader-to-emitter queue is capped at 1,024 records, and stderr retention remains bounded. Backpressure reaches the official platform tool instead of accumulating unbounded memory. A platform-tool exit ends the operation; version 1 does not reconnect automatically. Standalone physical-iOS log streaming is reported as unsupported when CoreDevice cannot provide the same application boundary.
The human command cargo ferry logs remains a finite snapshot. Adding the global --json-stream flag routes it through the same live, bounded stream and terminal-event lifecycle as cargo ferry ide logs.
Version 1 defines:
| Event | Purpose |
|---|---|
operation_started | Opens an operation and names the command/workspace |
phase_started | Opens a stable phase |
progress | Bounded or indeterminate progress |
command_started | Sanitized executable plus argument array, never a shell command |
diagnostic | File-bound structured diagnostic |
device | Added or changed typed device |
device_removed | Device removed from a watched snapshot |
artifact | Validated artifact path and metadata |
application_started | Confirmed package/bundle launch |
log | One application-specific log record |
warning | Non-fatal actionable warning |
fix | Standalone Rust-supplied safe action |
phase_finished | Closes a phase with status and duration |
operation_finished | Closes success or a typed failure |
operation_cancelled | Closes cancellation |
Clients must ignore an unknown event after validating the common fields and protocol version. A partial final line is not an event and must be discarded as a truncated stream.
Cancellation and process failures
The client cancels by terminating the cargo-ferry process (SIGINT is preferred where available). RustFerry’s process-control layer terminates the tracked child process tree. A cooperative streaming command emits exactly one operation_cancelled event and exits with status 130. Editor shutdown must still terminate the process tree even if stdout is no longer readable.
A normal tool or build failure emits structured diagnostics, closes any open phase, emits operation_finished with success: false and a typed error, then returns a non-zero status. Raw child stdout/stderr never becomes protocol framing.
Paths, text, and secrets
- Project, diagnostic, and artifact paths are canonical absolute UTF-8 paths. A field that is not a file path is explicitly named otherwise.
- Spaces, Windows separators, and Unicode are ordinary JSON string content.
- Invalid child-process UTF-8 is converted safely before it reaches a text field.
- Argument arrays preserve process boundaries; shell command concatenation is forbidden.
- Passwords, tokens, signing secrets, private keys, provisioning contents, and broad environment dumps are forbidden. Known credential values are redacted as
<redacted>. - Logs must be bounded by the producer/consumer and must not contain binary data.
Compatibility policy
Protocol v1 may add optional object fields and new event types. Existing required fields do not change meaning. Removing a required field, changing its type/meaning, or changing framing requires a new protocol version. Clients reject incompatible versions with an actionable update message and never fall back to human-output parsing.
Compatibility tests cover Rust serialization, external fixtures, optional fields, unknown events, missing fields, incompatible versions, Unicode, Windows paths, spaces, cancellation, invalid UTF-8, and truncated streams.
Protocol and editor-host tests do not establish mobile runtime evidence. The current environment had no Android emulator/device, iOS Simulator runtime/device, Apple Development identity, Team, provisioning profile, or attached iPhone; physical signing and device operations therefore remain unvalidated outside deterministic host tests.
Visual Studio Code
Build the extension package from a source checkout, then install the resulting VSIX:
cd editors/vscode
npm ci
npm run package
code --install-extension dist/rustferry-vscode.vsix
The final command can instead be completed from the VS Code Extensions view with Install from VSIX… and editors/vscode/dist/rustferry-vscode.vsix.
The extension activates for a trusted workspace containing ferry.toml. It discovers cargo-ferry, negotiates IDE protocol v1, and provides:
- Project, Devices, and Artifacts trees;
- status and build progress;
- saved and unsaved
ferry.tomldiagnostics in Problems through the bounded IDE protocol; - Create Project, Check, Doctor, Build, Install, Run, Logs, capability, documentation, reveal, and copy-path commands;
- native Quick Pick/Input Box project creation;
- generated cancellable Terminal tasks that run the documented human CLI commands.
Multi-root workspaces retain independent project/device/artifact state. Virtual and untrusted workspaces cannot execute or mutate projects. Trusted file-backed Remote SSH, WSL, Dev Container, and Codespaces workspaces execute the extension and CLI in the remote extension host; platform operations are available only when its SDK tools and device connections are available. Editor commands consume the structured IDE protocol, while generated Terminal tasks intentionally display the human CLI output. The extension does not implement platform builds, store Apple credentials, or edit generated native projects.
Open Logs starts the application-filtered IDE stream and keeps it active. Stop Logs sends cancellation to the CLI process tree; the CLI closes the protocol with operation_cancelled, and no global Android or Apple log stream is used.
For troubleshooting and exact settings, see the extension’s README and support guide. Protocol details are in IDE protocol.
Install the VS Code extension
RustFerry requires Visual Studio Code 1.100 or newer and a compatible cargo-ferry executable. Install RustFerry for VS Code from the Visual Studio Marketplace, or install the inspected VSIX from a source checkout.
code --install-extension ShiroKSH.rustferry-vscode
cargo build --locked -p cargo-ferry
cd editors/vscode
npm ci
npm run package
code --install-extension dist/rustferry-vscode.vsix
The final command may be replaced with Extensions: Install from VSIX…. Reinstall with --force when testing a rebuilt package.
The extension resolves the CLI in this order:
- absolute
rustferry.cliPath; cargo-ferryonPATH;cargo ferrythroughcargoonPATH;- the standard Cargo bin directory;
- an ancestor checkout’s
target/debug/cargo-ferry, only withrustferry.developmentModeenabled.
Open a trusted, file-backed folder containing ferry.toml. Virtual workspaces remain non-executable. In Remote SSH, WSL, Dev Containers, and Codespaces, the extension and CLI run in the trusted remote extension host; commands work only when that host can see the required SDK tools and device connections.
See VSIX packaging for package verification and settings for explicit CLI selection.
VS Code project wizard
Run RustFerry: Create New Project from a trusted workspace. The wizard collects, in order:
- a local parent directory;
- project/crate name, display name, and reverse-DNS application identifier;
- a template reported by the selected
cargo-ferryCLI; - Android, iOS, or both platforms;
- zero or more CLI-reported capabilities;
- whether to initialize Git;
- whether to open in the current window, a new window, or the current multi-root workspace.
RustFerry validates names and identifiers before generation and refuses an existing destination. A final modal shows the exact destination and choices. No build starts automatically.
The extension creates the base project first, then adds selected capabilities one at a time. If capability setup fails, the created project is preserved, the remaining capability names are reported, and the project can be opened to finish setup. Cancellation stops the active CLI process tree.
Opening or adding the project starts discovery and the Getting Started walkthrough. Generated user code stays Rust-only; native glue is generated later below target/ferry/.
The equivalent CLI surface is documented under cargo ferry new.
VS Code commands
Commands are available from the Command Palette under RustFerry and context menus where applicable.
| Workflow | Commands |
|---|---|
| Project | Create New Project, Refresh, Select Project, Open ferry.toml, Open src/app.rs |
| Validation | Check, Doctor |
| Target | Select Target, Build Android, Build iOS Simulator, Build for Physical iPhone, Build Selected Target |
| Capabilities | Add Capability, Remove Capability |
| Devices and signing | Refresh Devices, Select Device, Select Development Team, Run iOS Doctor, Open iOS Signing Guide |
| Deployment | Install, Run, Open Logs, Stop Logs |
| Artifacts | Reveal Artifact, Copy Artifact Path, Inspect Artifact Metadata, Delete Generated Artifact… |
| Maintenance | Clean Generated Files, Manage Workspace Trust, Select cargo-ferry Executable, Open Documentation |
Check and Build publish structured diagnostics to Problems. Build records only artifacts reported as validated by the CLI. Install and Run require a compatible selected device; Open Logs starts an application-filtered stream, while Stop Logs cancels its CLI process tree. There is no automatic log reconnection.
Delete and Clean require confirmation and are limited to generated RustFerry output. Untrusted or virtual workspaces cannot execute or mutate projects.
Physical-iPhone Build, Install, and Run use an exact CoreDevice ID and a Team selected from installed Apple Development identities. The extension stores only the non-secret Team ID; explicit provisioning-profile selection and permission for Xcode to update provisioning assets remain CLI controls. Standalone physical-iPhone logs are unavailable because the current CoreDevice path cannot guarantee an application-only stream. See the physical iPhone workflow. RustFerry also intentionally exposes no fake VS Code debugger; see ADR-004.
VS Code settings
Project settings have resource scope, so multi-root folders may use independent values. The selected Apple Development Team is machine-scoped because it describes identities installed on that extension host.
| Setting | Default | Meaning |
|---|---|---|
rustferry.cliPath | empty | Absolute path to cargo-ferry or cargo; empty enables safe discovery. |
rustferry.developmentMode | false | Permit an ancestor RustFerry checkout’s target/debug/cargo-ferry; never permits arbitrary workspace executables. |
rustferry.defaultPlatform | android | Initial target: android, ios-simulator, or ios-device. |
rustferry.defaultProfile | debug | Initial build profile: debug or release. |
rustferry.ios.developmentTeam | empty | Machine-scoped, non-secret Team ID selected from installed Apple Development identities. |
rustferry.validation.debounceMs | 350 | Manifest validation delay, constrained to 100–5000 ms. |
rustferry.maxProtocolLineBytes | 1048576 | Maximum UTF-8 bytes in one protocol event, constrained to 65536–4194304. |
Selected project, platform, profile, device ID, and recent artifact metadata live in VS Code workspace state rather than ferry.toml. Changing target clears an incompatible selected device.
Prefer RustFerry: Select cargo-ferry Executable over editing JSON; it writes the path at workspace or workspace-folder scope. The path must be absolute and executable.
Remote settings resolve in the remote extension host. A local macOS SDK or USB connection does not become visible to an extension running remotely.
VS Code tasks
RustFerry contributes a rustferry task provider. In a trusted, executable project it generates Check, Doctor, Android Build, iOS Simulator Build, selected-target Build, and Clean tasks. Install, Run, and Logs appear only when the negotiated CLI protocol advertises those features.
Generated tasks use the human CLI, not the editor JSON protocol, and run in a dedicated terminal with the project directory as cwd. Build Selected Target is assigned to VS Code’s standard Build task group; it is not marked as the default task.
A checked-in task may use the same definition:
{
"version": "2.0.0",
"tasks": [
{
"type": "rustferry",
"action": "build",
"platform": "android",
"profile": "release",
"project": "${workspaceFolder}"
}
]
}
Supported actions are check, doctor, build, install, run, logs, and clean. Platforms are android, ios-simulator, and ios-device; profiles are debug and release. The physical-iPhone build task appears only when the protocol advertises physical iOS support and passes the configured non-secret Team ID when one is selected. Deployment tasks may include an exact stable device ID. Physical-iPhone install and run require both that ID and a Team; standalone physical-iPhone logs remain unavailable.
Task argument construction uses process executable/argument arrays. It does not build a shell command string. Tasks are absent in untrusted workspaces and unresolved when the project or CLI cannot execute.
VS Code diagnostics
RustFerry publishes ferry.toml and Rust compiler diagnostics to VS Code Problems with protocol-provided severity, code, help, documentation target, and source range.
Manifest validation is debounced on open, edit, save, and relevant workspace changes. An unsaved buffer is sent to cargo ferry ide validate --manifest-stdin; the bounded UTF-8 request is validated without writing it to disk. A newer document version cancels or invalidates an older result, and results are discarded if the manifest path or content changes during validation.
Check and Build use Cargo’s structured compiler messages. Rust source diagnostics retain real file paths and ranges, including UTF-16 column conversion for VS Code. A failed build still publishes diagnostics gathered before the failure.
Protocol text edits become Quick Fixes only for a clean, file-backed manifest whose version, digest, canonical path, and real file contents still match validation. Fixes are rejected across symbolic-link boundaries, outside the project root, or after any intervening edit. Dirty-buffer diagnostics remain visible but receive no disk-backed mutation.
Use Run RustFerry Doctor for environment failures and Open RustFerry documentation when a diagnostic provides a documentation URL. Human terminal output is never scraped into Problems; see IDE protocol.
VS Code devices
The Devices view normalizes Android physical devices/emulators, iOS Simulators, and paired physical Apple devices returned by cargo-ferry. Refresh is explicit; state such as offline, unauthorized, shutdown, unavailable, unpaired, or disconnected remains visible.
Select a target first, then choose a compatible device by its stable ADB serial, Simulator UDID, or CoreDevice identifier. The selection is stored per project. Switching platform clears an incompatible selection, and refreshed inventory restores a selection only when the exact ID remains compatible.
Install, Run, and Logs prompt for a device if none is selected. Each device record carries separate build, install, launch, and application-log capabilities; unsupported or stale device state fails closed in the CLI even when the extension-level feature exists. Multi-root workspaces retain separate project, target, device, and artifact state.
For a physical iPhone, select iOS Device, choose the paired device by its exact CoreDevice identifier, then run RustFerry: Select Development Team. The extension queries cargo ferry ide signing-teams, shows the Team ID, identity label, and public certificate fingerprint, and stores only the selected Team ID in the global rustferry.ios.developmentTeam setting. It does not collect credentials, private keys, or profile contents.
Build for Physical iPhone, Install, and Run pass that Team to the same official Xcode/devicectl pipeline used by the CLI. Install and Run create and independently validate a fresh development-signed artifact before using the selected device. The editor uses automatic signing without provisioning mutation; manual profile selection and opt-in -allowProvisioningUpdates remain explicit CLI/protocol controls. Standalone physical-iPhone Logs is unavailable because the current CoreDevice path does not provide the same application-only boundary as Android and Simulator logging.
In a trusted file-backed remote workspace, discovery runs remotely. Only SDK tools and connections visible to that remote extension host can appear. The current validation environment had no Android emulator/device, iOS Simulator runtime/device, Apple Development identity, Team, provisioning profile, or attached iPhone. The editor flow is implemented and host-tested, but physical signing and all mobile runtime operations remain unobserved. See the general device discovery contract.
VS Code troubleshooting
No project appears
Open a file-backed workspace containing ferry.toml, then run RustFerry: Refresh. Discovery excludes target, node_modules, and .git and is bounded to the workspace. Virtual workspaces cannot execute RustFerry.
CLI not found or incompatible
Run RustFerry: Select cargo-ferry Executable and choose an absolute executable, or install cargo-ferry on the extension host’s PATH. Update the CLI when protocol v1 negotiation fails. In a remote window, installing only on the local machine is insufficient.
Commands are disabled
Trust the workspace. Remote execution also requires a file-backed folder and the needed Rust/platform tools in the remote extension host.
No device or operation
Run Doctor and Refresh Devices. Use exact stable IDs and resolve offline, unauthorized, shutdown, unpaired, or disabled Developer Mode state with platform tooling. For a physical iPhone, select the iOS Device target, choose the exact CoreDevice ID, then run RustFerry: Select Development Team. No team is shown until Keychain contains a usable Apple Development identity. Install and Run are available in the editor; manual profile selection and provisioning updates remain explicit CLI controls.
Diagnostics or Quick Fixes disappear
RustFerry discards stale validation results. Save or stop editing, wait for validation, and retry. Quick Fixes intentionally require a clean unchanged regular file.
Logs stop
Open Logs attaches to the currently running selected application. Stop Logs cancels it. A platform-tool exit ends the stream; automatic reconnect is not implemented, so relaunch the application if needed and run Open Logs again.
Standalone physical-iPhone logs are intentionally unavailable: the current CoreDevice path cannot enforce the same application-only log boundary. Build, Install, and Run support does not imply that Logs is supported for the same device.
For a reproducible report, include extension and CLI versions, extension-host environment, target, operation ID, and sanitized RustFerry output. Never attach keys, profiles, tokens, passwords, or complete environment dumps. See the extension support guide.
VS Code extension development
The extension source is editors/vscode/. It targets Node 20 and VS Code 1.100, bundles production code with esbuild, and keeps mobile build logic in cargo-ferry.
cargo build --locked -p cargo-ferry
cd editors/vscode
npm ci
npm run typecheck
npm run lint
npm test
npm run test:host
npm run perf
npm run package
npm run vsix:smoke
Set RUSTFERRY_TEST_CLI to an alternate real CLI; otherwise host and performance tests expect ../../target/debug/cargo-ferry. Extension Host smoke uses isolated user/extension directories and checks both an ordinary Rust workspace that must remain inactive and a Ferry workspace that must activate, discover, validate, refresh views, and open its manifest.
Unit tests cover protocol framing, process bounds/cancellation, discovery, tasks, validation freshness, fix safety, and project input validation. The host smoke does not build, install, launch, or observe a mobile application.
For contributor use, rustferry.developmentMode may resolve the checkout’s debug CLI. Keep it disabled for normal projects. See VSIX packaging for the exact bundle boundary.
VS Code extension release
An extension candidate is the exact VSIX produced by npm run package, followed by npm run vsix:smoke. Before approval, run the complete npm run check, real-CLI Extension Host smoke, performance measurement, npm audit, and repository license policy.
The package allowlist contains user-facing docs, license, production bundle, icon, walkthrough media, and snippets. Source maps, TypeScript sources, tests, package locks, node_modules, workflows, and nested VSIX files must not ship. Record size and SHA-256 only from the final bytes; do not copy a stale value into documentation.
The repository’s extension workflow checks Linux, macOS, and Windows. Linux additionally runs the real CLI integration, Extension Host smoke, measurements, license policy, and uploads the verified VSIX as a workflow artifact.
Marketplace publication remains a separate manual operation. RustFerry for VS Code 0.1.0 is available in the Visual Studio Marketplace; subsequent releases use the exact draft-release assembly through the protected approval and verification described in VS Code Marketplace. The draft GitHub Release assembly includes a versioned copy of the same inspected VSIX without changing its bytes.
Device discovery
cargo ferry devices normalizes Android physical devices/emulators, iOS Simulators, and paired CoreDevice hardware without making one missing tool hide the others.
cargo ferry devices
cargo ferry devices --platform android --json
cargo ferry devices --platform ios
cargo ferry devices --watch --json-stream
Use the exact ADB serial, Simulator UDID, or CoreDevice identifier shown by the command. Display names are not stable selectors. Automatic selection succeeds only when exactly one compatible device exists; ambiguity fails with the candidate IDs.
States such as offline, unauthorized, shutdown, unavailable, unpaired, and disconnected remain visible and never count as install/launch success. Watch mode emits protocol-v1 NDJSON until Ctrl+C; cancellation stops the complete child process tree.
No emulator, Simulator runtime, or physical device was available for the current validation pass. Parser, selection, capability, watch, cancellation, and real empty-inventory discovery paths were tested.
Install, run, and logs
Deployment consumes only freshly built, independently validated artifacts. A file suffix alone never makes an APK or app deployable.
Android:
cargo ferry install android --device SERIAL
cargo ferry run android --device SERIAL
cargo ferry run android --device SERIAL --logs
cargo ferry logs android --device SERIAL --since-seconds 300
iOS Simulator:
cargo ferry install ios --simulator
cargo ferry run ios --simulator SIMULATOR_UDID
cargo ferry logs ios --simulator SIMULATOR_UDID
install composes build → validation → install. run adds launch. Logs are never attached implicitly unless run --logs is present, and that option collects one finite snapshot rather than leaving a process running.
Safe defaults do not reinstall/downgrade, grant permissions, clear Android data, boot a Simulator, terminate an existing process, or mutate provisioning. Each behavior has a named opt-in flag. RustFerry never uninstalls an application or clears the global log buffer.
Android logs are PID-filtered; Simulator logs use an application predicate. Entry count, bytes, history, command runtime, and retained output are bounded. Standalone physical-iOS historical logs remain unsupported when CoreDevice does not expose an application-filtered operation; RustFerry will not substitute the full system log.
Install on Android
Connect or start an Android target, confirm it with cargo ferry devices --platform android, then install using its exact ADB serial:
cargo ferry install android --device SERIAL
Install composes a fresh Android build, independent APK validation, a final integrity recheck, then adb -s SERIAL install. Automatic selection is allowed only when exactly one compatible target exists.
Conservative defaults do not replace an installed package, permit a downgrade, grant runtime permissions, or clear application data. Each behavior is explicit:
cargo ferry install android --device SERIAL --reinstall
cargo ferry install android --device SERIAL --reinstall --allow-downgrade
cargo ferry install android --device SERIAL --grant-permissions
cargo ferry install android --device SERIAL --clear-data
--clear-data affects only this application and runs after a successful install. RustFerry never uninstalls an app or clears global device logs. --release builds the release profile before installation.
No Android device or emulator was available in the current validation environment, so the command path and tool parsing are tested but live installation was not observed. See Android build and the shared deployment contract.
Run on Android
Run builds, validates, installs, and launches the configured application on an Android physical device or emulator:
cargo ferry run android --device SERIAL
Use cargo ferry devices --platform android for the exact serial. Omitting it is safe only when discovery returns exactly one compatible target. Install flags remain opt-in:
cargo ferry run android --device SERIAL --reinstall --grant-permissions
RustFerry does not terminate an existing process unless --terminate-existing is present. --logs collects one finite, application-filtered snapshot after launch; it does not leave a background stream running:
cargo ferry run android --device SERIAL --terminate-existing --logs
Use the separate Android logs command for history or a continuous JSON stream. A successful Run report requires the official platform tools to confirm install and launch; a built APK alone is not launch evidence.
No Android device or emulator was available in the current validation environment, so launch behavior was not observed on hardware or an emulator.
Android logs
The human command returns a finite snapshot for the running application selected by package PID:
cargo ferry logs android --device SERIAL
cargo ferry logs android --device SERIAL --since-seconds 60 --level warning
Bounds default to a five-minute window, 2,000 retained entries, and 2 MiB. Override them with --since-seconds, --max-entries, and --max-bytes. RustFerry uses adb logcat --pid and never clears the global log buffer. The application must already be running.
For continuous protocol output, add --json-stream:
cargo ferry logs android --device SERIAL --json-stream
Each application-filtered entry is emitted as a bounded protocol event until Ctrl+C or platform-tool exit. Cancellation terminates the process tree. There is no automatic reconnect after process restart, disconnect, or adb logcat exit.
The VS Code Open Logs command consumes this structured live stream; Stop Logs cancels it. No Android target was available in the current validation environment, so live device output was not observed.
Install on iOS Simulator
List Simulators and use the exact UDID:
cargo ferry devices --platform ios
cargo ferry install ios --simulator SIMULATOR_UDID
--simulator without a value means automatic selection and succeeds only when exactly one compatible Simulator exists. Install composes a fresh Simulator build, independent .app validation, a final integrity recheck, then xcrun simctl install.
A shutdown Simulator is rejected by default. Boot only the selected Simulator explicitly:
cargo ferry install ios --simulator SIMULATOR_UDID --boot-on-demand
RustFerry waits for boot completion before the final integrity check and install. It does not select, boot, or mutate another Simulator. Simulator applications use the validated ad-hoc signing path and require no Apple Team.
The current host had no installed Simulator runtime or device. SDK-only .app build validation was completed, but Simulator installation was not observed. See Simulator build.
Run on iOS Simulator
Run builds, validates, installs, and launches the configured bundle on one exact Simulator:
cargo ferry run ios --simulator SIMULATOR_UDID
Use --boot-on-demand to boot a selected shutdown Simulator. Existing application processes are left alone unless termination is explicit:
cargo ferry run ios --simulator SIMULATOR_UDID \
--boot-on-demand --terminate-existing
--logs collects one finite application-filtered snapshot after launch. It does not open a persistent stream; use iOS logs for that.
A successful Run report requires simctl to confirm both installation and launch. A validated .app alone is not runtime evidence. Automatic selection is permitted only with exactly one compatible Simulator.
No Simulator runtime or device was present in the current validation environment, so installation, launch, UI, and runtime callbacks were not observed.
iOS logs
For an iOS Simulator, the human command returns a finite unified-log snapshot filtered to the configured application process/bundle:
cargo ferry logs ios --simulator SIMULATOR_UDID
cargo ferry logs ios --simulator SIMULATOR_UDID --since-seconds 60 --level debug
The command uses simctl spawn … log show --style ndjson with an application predicate. Entry count, UTF-8 bytes, history, and command runtime are bounded. It never returns the entire Simulator system log.
Continuous protocol output uses log stream:
cargo ferry logs ios --simulator SIMULATOR_UDID --json-stream
The stream ends on Ctrl+C or platform-tool exit and has no automatic reconnect. VS Code Open Logs consumes this stream; Stop Logs cancels the process tree.
Standalone physical-iPhone historical or live logs are unsupported when the installed CoreDevice API lacks a safe application-filtered operation. RustFerry will not substitute a global device log. No Simulator runtime or physical device was available for current live-log validation.
Physical iPhone deployment
RustFerry uses official Xcode development signing and CoreDevice operations. It has no signing bypass and never stores Apple credentials, private keys, profiles, or passwords in ferry.toml.
Inspect available development identities and connected devices:
cargo ferry signing teams
cargo ferry devices --platform ios
Build, install, and run with an explicit Team and exact CoreDevice identifier:
cargo ferry build ios --device --team ABCDE12345
cargo ferry install ios --device DEVICE_ID --team ABCDE12345
cargo ferry run ios --device DEVICE_ID --team ABCDE12345
Provisioning mutation is disabled by default. Enable Xcode account/profile updates only deliberately with --allow-provisioning-updates, or choose an explicit manual profile with --provisioning-profile NAME_OR_UUID. RustFerry independently checks arm64 architecture, bundle structure, nested extensions, signatures, profiles, entitlements, Team ID, and bundle IDs before deployment.
The VS Code extension can select an installed Apple Development Team and an exact paired device, then build, install, and run through the same pipeline. Manual profile selection and opt-in provisioning updates remain explicit CLI controls. Standalone physical-device logs are unsupported unless CoreDevice exposes a safe application-filtered operation; RustFerry does not stream the full device log.
No Apple Development identity, Team, profile, or attached device was available in the current environment. The pipeline and deterministic tests exist, but a physical artifact was not built, installed, launched, or observed here. See physical iOS development for implementation details.
Ferry Remote Build Protocol v1
Ferry Remote Build Protocol v1 is the runtime-neutral boundary between a cargo-ferry client,
a build provider, and a trusted macOS worker. Rust structs in rustferry-remote are the source of
truth. The checked-in JSON Schema is schemas/ferry-remote-protocol-v1.schema.json.
Generate it with:
cargo run -p rustferry-remote --example generate-protocol-schema -- \
schemas/ferry-remote-protocol-v1.schema.json
Compatibility
The current version is 1.0. Peers negotiate the lower minor version when their nonzero major
versions match. Different major versions fail before source transfer. A protocol document carries
UTF-8 text only; every identifier, path, source manifest, signing plan, and event is validated again
by the receiving boundary.
SSH snapshot session v1
Handshake and doctor use strict JSON stdio envelopes. An unsigned SSH build then invokes only
ferry-worker-macos serve --stdio-session-v1 and switches to a full-duplex framed stream. Each
24-byte big-endian header contains RFDP, schema 1, a typed frame kind, a per-direction sequence,
and the exact following payload length. Sequences start at zero and reject gaps, duplicates,
replay, and exhaustion.
The client sends BuildRequest, SourceDescriptor, and streamed SourceArchive frames. The worker
returns JobAccepted, zero or more ordered Event frames, one ArtifactDescriptor, and the
streamed Artifact. Only after the client has rehashed, safely extracted, independently inspected,
durably published, and rebound the returned unsigned XCArchive does it send ArtifactReceipt.
The worker then removes the exact capability-bound job root and returns Complete with
non-retention cleanup proof. Error is terminal; Cancel is valid only at a clean client frame
boundary. Disconnect, cancellation, timeout, malformed order, identity mismatch, or missing receipt
cannot become success.
JSON requests are limited to 1 MiB, snapshot descriptors to 8 MiB, control frames to 64 KiB, events to the protocol event-line bound, source ZIPs to 640 MiB, and sealed XCArchive ZIPs to 2 GiB. Large source and artifact payloads are copied with fixed memory. Bootstrap input has both total and inactivity deadlines; the complete OpenSSH build session also has a finite deadline and bounded process cleanup.
Snapshot session v1 accepts only snapshot source mode, unsigned-compile-only signing, and exactly
one XCArchive artifact. It carries no signing key, password, provisioning profile, IPA, device
operation, or arbitrary command.
These dedicated session capabilities are negotiated only by SSH handshake/doctor. The generic
BuildProvider view does not advertise snapshot submit/events/cancel/download operations that its
generic methods do not implement; those methods return typed unsupported errors.
Build request
An iPhone request fixes:
- operation ID, bundle ID, product name, build profile, and minimum iOS version;
- a client-derived product expectation: exact
.appdirectory, executable, app version, build number, and sorted extension/framework path, bundle-ID, executable, and kind graph; - source mode and deterministic source manifest;
- exact credential-free GitHub HTTPS repository plus lowercase 40-hex commit in
gitmode; - no repository or revision fields in explicit
snapshotmode; - a complete signing plan containing expected public certificate metadata and opaque secret references, never secret values;
- requested artifact kinds.
Unsigned compile-only mode cannot request an installable IPA. Signed plans identify the expected team, device, application and extension targets, profile references, and entitlement expectations. Providers reject unsupported signing/source/artifact capabilities with a typed error; they do not substitute a weaker build or return fake success.
Device plans carry only a strict lowercase SHA-256 digest in udid_sha256. A raw UDID is validated
and hashed at the local constructor boundary; protected workers likewise hash decoded profile UDIDs
before comparison or creation of public metadata. Raw UDIDs are excluded from requests, reports,
debug output, and serialization.
The product expectation is computed before submission. The worker must compare its regenerated plan and unsigned archive with it; a client derives final IPA expectations only from this submitted request, never from a worker report. Product paths are portable, version/build strings use canonical numeric components, nested paths are unique after Unicode normalization and case folding, and the nested bundle graph must equal the corresponding framework and extension signing targets.
Canonical compact request bytes and their lowercase SHA-256 are produced by the shared
canonical_request_bytes and canonical_request_sha256 functions. Providers and workers must not
implement their own request encoding.
Compile handoff
The public CompileHandoff envelope contains the exact submitted request and credential-free
CompilePhaseEvidence. Its SealedUnsignedArchive descriptor binds the deterministic unsigned
.xcarchive ZIP size and SHA-256, its complete source-style content manifest, and the worker’s
toolchain-specific unsigned archive expectation. These wire structs live in rustferry-remote so a
Windows or Linux client can decode them without depending on the macOS worker implementation.
Receiving boundaries still hash the sealed ZIP bytes, safely extract and inspect the archive, bind the embedded request to the independently retained submitted request, and compare client-owned product fields before trusting the handoff. A digest copied only from a signing report is not independent evidence.
Events
Each progress record is one compact JSON object followed by \n. It carries protocol version,
operation ID, job ID, millisecond UTC timestamp, provider, phase, monotonically increasing sequence,
and a typed payload. ANSI terminal escapes, oversized records, invalid UTF-8, malformed JSON, and
truncated JSON are rejected.
Required v1 payload names:
operation_started job_created job_queued
worker_assigned source_prepared source_upload_started
source_upload_progress source_verified phase_started
progress command_started diagnostic
signing_started artifact_created artifact_validated
artifact_upload_started artifact_download_started artifact_download_progress
artifact_downloaded warning cleanup_started
cleanup_finished operation_finished operation_cancelled
Unknown optional fields are ignored. An unknown event with the same major version is retained as an
unknown event so an older client can keep consuming the stream. Unknown or incompatible major
versions are not accepted.
Paths and source
Every wire path declares one semantic root: project-relative, worker-relative, client-absolute, or provider URI. Relative paths reject absolute forms, traversal, empty components, and mixed separator ambiguity. Provider URIs reject embedded credentials.
Snapshot manifests bind sorted portable paths, byte sizes, executable bits, per-file SHA-256, total
size, and a domain-separated manifest SHA-256. Selection rejects symlinks, hardlinks, special files,
case/Unicode-normalization aliases, sensitive signing and credential paths, oversized inputs, and
source changes during hashing. .ferryignore intentionally supports a small literal exclusion
subset; it cannot re-include built-in sensitive paths.
Signing and secrets
Secret and SecretBytes are non-cloneable, non-debuggable, and non-serializable. Their memory
overwrite on drop is defense in depth, not guaranteed erasure. SecretReference serializes only a
validated environment, credential-store, GitHub Actions, or worker-owned handle.
Protected GitHub signing supports at most three application/extension provisioning profiles. A
multi-profile worker invocation receives only the bounded RFSIGNV2 stdin frame: eight-byte magic,
big-endian record count, then records containing a 16-bit reference-name length, 32-bit value length,
and the exact reference/value bytes. The immutable signing plan defines the expected two
certificate/password references plus one profile reference per target. Missing, duplicate, unknown,
oversized, non-canonical, truncated, or trailing records fail before signing; values are resolved
once and input storage is wiped on every exit. The legacy three-field NUL-delimited input remains
available only for a single application profile. Secret values never enter the remote JSON protocol,
arguments, events, reports, or workflow source.
The modern GitHub signing workflow also binds the complete public signing-target graph, including application, extension, framework, and dynamic-library names, bundle identifiers, and target kinds. Shared canonical encoding produces a domain-separated lowercase SHA-256. The provider checks exact graph equality without depending on order, and the worker recomputes the digest before checkout of the requested project revision or compilation. The digest is public policy metadata; it contains no secret values.
Each signed request binds the expected certificate common name, Team ID, SHA-256 fingerprint, and expiry to an opaque private-key reference. The protected worker derives the imported identity again and rejects any mismatch before profiles or application code are signed.
All process and provider output passes through the same redaction policy before logs, diagnostics, or events are emitted. Redaction holds possible secret prefixes across stdout/stderr chunks and also handles nested JSON, command arguments, environment values, authorization fields, private-key fields, passwords, tokens, signed URLs, and temporary-keychain credentials.
Signing status is staged: unsigned, certificate_validated, profile_validated,
nested_code_signed, application_signed, ipa_exported, and artifact_validated. It is never one
boolean. Dynamic libraries and frameworks precede extensions; the main application is signed last.
Artifacts and cleanup
Artifact manifests bind source, worker/toolchain, signing evidence, timestamps, cleanup state, and
each downloadable file’s byte size and SHA-256. Client downloads must verify the expected size and
SHA-256 before placement. IPA inspection additionally validates ZIP safety, Payload/<App>.app,
plist identity, arm64 Mach-O slices, and LC_BUILD_VERSION platform metadata. An arm64 Simulator
binary is rejected explicitly; arm64 alone is not device proof.
The default protected GitHub result is an exact five-file transport set: development IPA, artifact
manifest, signing report, validation report, and sanitized-build-log.txt. The fixed sanitized log
is created only after protected signing, IPA export, validation, and signing-material cleanup are
confirmed; its size and SHA-256 are part of the immutable worker manifest. The client publishes the
IPA, manifest, validation report, and sanitized log by default after verifying all five transport
files.
Signed optional products extend that exact set only when declared by the request.
--artifact app adds application.app.zip, --artifact archive adds
application.xcarchive.zip, and --artifact all adds both. --include-dsym separately adds
application.dSYM.zip; all does not imply dSYM. The application and reconstructed XCArchive must
contain the exact signed app tree independently validated from the IPA. A requested dSYM is limited
to the main application executable, must contain real DWARF debug information, and must have the
same nonzero arm64 Mach-O UUID as the signed executable. Every selected file is size- and
SHA-256-bound in the manifest; absence, extras, or substitution fail the operation.
A successful build and successful cleanup are distinct states. Cleanup proof records isolated workspace removal, signing-material/keychain removal, and intentional artifact retention. Cleanup failure remains visible even when compilation or export succeeded.
Source bundles
RustFerry snapshot transport selects project source and local Cargo packages without copying the repository, credentials, generated output, or signing material. It creates a deterministic ZIP and a separate versioned descriptor that binds the ZIP size and SHA-256 to the canonical source manifest.
Inspect the exact selection before creating anything:
cargo ferry remote bundle inspect --project-dir ./weather
The report lists every portable path, byte size, executable bit, file SHA-256, resolved local path
dependency, excluded sensitive path, and the empty selected-symlink set (any encountered symlink is
rejected). Sensitive directories are listed at the skipped root and are not traversed. Cargo
metadata is read with --locked; only the selected package’s resolved local dependency closure is
included, including when the selected package is the workspace root, and unrelated workspace
members are excluded. Local path dependencies must remain inside the selected workspace. Built-in
sensitive exclusions cannot be negated. A root or project .ferryignore may add literal relative
path exclusions using the restricted syntax described by command errors.
On Windows and other hosts without Unix mode bits, tracked 100755 modes are imported from the Git
index when available. Use repeatable --executable <workspace-relative-path> arguments for selected
untracked files or an index-less workspace; invalid or unselected paths are rejected.
Create new files without overwriting an existing path:
cargo ferry remote bundle create \
--project-dir ./weather \
--output ../exports/weather-source.zip \
--descriptor ../exports/weather-source.json
The example assumes ../exports already exists outside the Cargo workspace. Omitting
--descriptor uses <output>.manifest.json. Global --dry-run performs source planning
and destination checks but does not create either file. Both destinations must be outside the
selected Cargo workspace so temporary or previously generated bundles cannot alter their own source
manifest.
Archive and descriptor publication is independently no-clobber, not a two-file transaction. If the archive is published but descriptor publication fails, the error reports both paths and leaves the verified archive in place. RustFerry does not delete a path after it can no longer prove that the path still names the operation-owned file.
Verify a received bundle independently:
cargo ferry remote bundle verify \
--archive ./weather-source.zip \
--descriptor ./weather-source.json
Verification treats both files as untrusted. Descriptor reads are bounded and identity-stable. ZIP size, SHA-256, entry order, path portability, file count, per-file and total sizes, compression ratio, executable bits, and every content digest must match before success. Extraction uses a fresh temporary directory and rejects traversal, symlinks, hard links, case/Unicode collisions, extra or missing entries, archive expansion abuse, and destination replacement.
GitHub GitSnapshot use
The GitHub provider reuses the same canonical selection only after explicit snapshot selection:
cargo ferry --dry-run build iphone --remote github --snapshot --unsigned
cargo ferry build iphone --remote github --snapshot --unsigned
Dry-run is zero-write and invocation-bound. It reports the exact public repository/ref, source
manifest, local path dependencies, included paths, exclusions, raw-byte totals, retention, and
effects; the archive SHA-256 is computed only after consent. Interactive execution asks [y/N];
JSON/non-interactive execution requires --yes. Execution repeats source/config/filesystem checks,
and any drift fails before staging, store mutation, or network access.
The client builds a create-only operation-scoped GitSnapshot without switching the caller branch, staging the Git index, changing remotes, or running Git hooks. Source bytes enter a public Git object database; temporary-ref deletion is cleanup, not erasure. The remote ref is retained until terminal cleanup, and the local keepalive remains available for exact retry until explicit complete-lineage prune authorizes release.
SSH Mac provider
The SSH Mac provider implements trusted endpoint storage, a versioned worker handshake and doctor, and snapshot-session v1 for unsigned physical-iPhone XCArchive builds. Deterministic source upload, ordered events, cancellation, digest-bound artifact transfer, client receipt, and zero-retention worker cleanup are covered by deterministic local tests. There is no live SSH/OpenSSH Mac build or SSH-produced artifact evidence yet. Signing, IPA export, installation, launch, and device runtime are not supported by this session.
Trust material
Obtain the Mac’s host public key and SHA256: fingerprint from its operator through an independent
trusted channel. Create a dedicated known_hosts file containing exactly one entry:
build.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...
For a non-default port, the host token must be [build.example.com]:2222. Hashed host tokens,
multiple keys, unsupported key types, padded fingerprints, symlinks, and empty files are rejected.
RustFerry does not run ssh-keyscan or accept a host key on first use.
Add the endpoint:
cargo ferry remote add ssh-mac production-mac \
--host build.example.com \
--user ferry \
--known-hosts /absolute/path/rustferry-production.known_hosts \
--host-key-sha256 SHA256:BASE64_WITHOUT_PADDING \
--identity-file /absolute/path/id_ed25519
The stored record contains endpoint fields, the pinned fingerprint, and only the canonical identity
file path. Private-key bytes are never read into or copied by provider configuration. Existing named
records are not overwritten. Records use the operating system’s user config directory under
rustferry/remotes/ssh; --config-dir <absolute-path> selects an isolated root for automation.
On Unix, managed directories must remain mode 0700 and endpoint records mode 0600. On Windows,
RustFerry creates each managed config directory and endpoint record with a protected DACL owned by
the current user and allowing only that user, LocalSystem, and built-in Administrators. It retains
the base and child handles, rejects reparse points or unexpected ACLs/link counts, and binds file
identity across create-only publication and reads. The Windows implementation has native ACL/runtime
tests. Core all-target/strict-Clippy and rustferry-ssh library Windows
cross-checks pass; the tests were not executed on this macOS host, and a full cargo-ferry cross
check is blocked in external vendored openssl-sys because Darwin Perl cannot configure
VC-WIN64A.
OpenSSH expands % and $ tokens in path-valued options, so RustFerry rejects either character in
trust and identity paths. For each connection, the validated canonical host-key entry is copied to
a retained operation-owned directory; Unix uses 0700 directories and 0600 files. On Windows,
the operation directory is atomically created with a protected DACL owned by the current user and
granting full inheritable access only to that user, LocalSystem, and built-in Administrators.
RustFerry verifies the owner, DACL, filesystem ACL support, and non-reparse retained handle before
writing source or trust bytes; the project-local parent session root does not need to be private.
The original known_hosts path is not passed to OpenSSH. An identity file is opened without
following the final link and its handle is retained without reading key bytes. Unix parent
directories must be owned by the key owner or root and must not be writable by another principal
unless protected by sticky-directory semantics; Windows opens deny replacement while the retained
handle exists. SSH-agent authentication remains available when --identity-file is omitted.
Doctor
Install the matching ferry-worker-macos binary on the Mac’s command path. The worker needs macOS,
full Xcode with iPhoneOS, Cargo/Rust/rustup, and the aarch64-apple-ios target. Configure exactly one
of RUNNER_TEMP or RUSTFERRY_WORKER_ROOT as a canonical, non-root, private directory owned by the
worker account. Handshake/doctor, control stdio, and snapshot stdio resolve this same exact one-of
root; doctor does not use a fallback workspace that a build would later reject. Then run:
cargo ferry remote doctor production-mac
Handshake and doctor invoke only ferry-worker-macos serve --stdio; a build invokes only
ferry-worker-macos serve --stdio-session-v1. OpenSSH uses a dedicated known-hosts file, strict
host-key checking, batch mode, no agent forwarding, no forwarding rules, no TTY, fixed
connection/keepalive limits, bounded stdio, and finite deadlines. User-supplied SSH options and
arbitrary remote commands are not accepted. Timeout and cancellation paths close the session and
use bounded process reaping without waiting indefinitely on pipe readers.
Cancellation and timeout prove local staging cleanup only. The client sends a best-effort cancel frame and then terminates the transport without draining a terminal worker cleanup proof; inspect the worker retention root before retrying. Exact non-retention proof remains mandatory for success.
Handshake and doctor responses use strict schema version 1 envelopes. Readiness requires snapshot
source mode, physical-iPhone compile, unsigned-compile-only signing mode, XCArchive output, ordered
events, cancellation, artifact download, cleanup, and retention 0. It is host/capability evidence,
not proof that this client completed a live build.
Build
cargo ferry build iphone --remote production-mac --unsigned
Named SSH selection is explicit. On Linux and Windows, omitting --remote for a physical-iPhone
build currently selects the GitHub provider; there is no configured-provider fallback.
The client plans a bounded deterministic snapshot, uploads its descriptor and ZIP, streams validated events, downloads the sealed XCArchive into a private create-only spool, rehashes and safely extracts it, independently verifies physical-iPhone Mach-O and bundle identity, and publishes the final ZIP without overwrite. Only then does it acknowledge the artifact. Success additionally requires the worker’s exact non-retaining cleanup proof. A sanitized JSON-lines event log is published beside the archive.
This produces an unsigned XCArchive transport ZIP, not an installable IPA. --team and signed SSH
requests fail explicitly; the client never downgrades a signing request to unsigned.
Worker isolation
Pinned host identity authenticates the endpoint; it does not make source or returned bytes trusted. Cargo manifests, procedural macros, and build scripts execute arbitrary code as the worker account. The reference worker is for a dedicated single-tenant account with no signing secrets or unrelated host credentials. Hostile co-tenancy requires stronger VM-equivalent isolation, ephemeral storage, network policy, resource controls, process-tree containment, and no cross-job cache.
GitHub macOS provider security
Trust boundary
The client trusts one public source repository, one separate private execution repository for signing, one trusted source branch or tag, one generated workflow digest, one verified worker binary, and one protected GitHub Environment. A build request binds both repository identities, an exact source commit and manifest, the execution-repository temporary ref, workflow path and digest, signing references, hashed device identity, and requested artifacts. Branch names and GitHub run status are routing evidence, not artifact proof.
Application source and Cargo build scripts are untrusted. They run only in phase A without signing secrets. Phase B receives the sealed unsigned archive, verifies its SHA-256 and structural evidence, and does not check out application source or invoke Cargo. Signing secrets exist only in the phase-B step environment and an ephemeral keychain/private workspace.
Temporary ref
Push-mode submission reads the trusted ref and workflow only from the public source remote, then creates one
operation-scoped branch below the configured RustFerry namespace in the execution remote. Git
plumbing does not switch the caller’s branch or modify the index/worktree. The dispatch commit is an
orphan root containing exactly the approved generated workflow and request envelope; it has no
source files, inherited workflows, parent, or imported source history. Publication is create-only.
Run lookup and artifact APIs target only the execution repository and bind workflow ID, dispatch
commit, branch, and exact push event.
Cleanup may delete only the exact operation ref and only while its remote tip still equals the recorded dispatch commit. An absent, moved, ambiguous, or unowned ref fails closed.
Workflow
The generated workflow uses immutable first-party action SHAs, fixed permissions, timeouts, retention, and concurrency. Push remains the compatible/default provider trigger and carries the immutable request envelope on the operation ref. The worker rejects a raw request, duplicate or unknown JSON fields, repository/ref/workflow mismatches, and a workflow digest that differs from the trusted checkout.
For Push, the trusted workflow must already exist with identical bytes on the configured trusted ref. Run discovery binds the exact workflow path, branch, event, dispatch commit, run ID, and attempt; it does not depend on default-branch numeric workflow registration.
An additive WorkflowDispatch foundation accepts exactly four required string inputs:
operation_id, request_sha256, source_revision, and dispatch_revision. It uses a fixed-origin
direct HTTPS POST with no redirects, exact headers and body, an exact HTTP 200 JSON receipt with a
positive run ID, then run-by-ID repository/workflow/path/ref/SHA/event validation. The worker binds
the whole canonical request and rejects Push/WorkflowDispatch input crossover.
No provider/controller consumer or live WorkflowDispatch run is claimed. Live use requires an active workflow whose default-branch and dispatched-ref definitions both declare the exact input contract; active registration alone is insufficient. The current default-branch definition is push-only, so WorkflowDispatch is not currently runnable evidence.
GitHub does not enforce the client’s workflow digest when a workflow is loaded from a temporary push branch. Until phase B moves into a fully-qualified, full-SHA reusable workflow, signed readiness therefore also requires a server-side Environment reviewer. Branch policy alone is insufficient: another repository writer could otherwise place different workflow bytes below the permitted branch namespace. The long-term reusable signer must own the Environment, never execute caller-controlled code, and revalidate the exact sealed handoff.
Setup and doctor prove through GitHub API metadata that the source repository is public. Signed setup, doctor, and submission separately query the exact execution repository and fail closed unless it is private. Submission repeats this check immediately before publication, and the signing job also requires the push event’s execution-repository metadata to be private. Same-repository mode is retained only for unsigned compilation. These checks cannot make an already-uploaded artifact private if repository visibility changes later; signed execution therefore requires a dedicated private repository with restricted membership and visibility-change governance.
Repository setup
The project checkout needs two credential-free Git remotes. Both fetch and push URLs for each remote must resolve to the same GitHub identity. Example:
cargo ferry remote setup github \
--source-remote-name public \
--execution-remote-name signing \
--execution-repository OWNER/private-signing \
--worker-revision <exact-lowercase-commit>
The generated workflow is installed in the public source checkout. Temporary dispatch refs are published only to the execution remote. The private execution repository slug is stored only in the project-local ignored provider config and dispatch envelope; it is never rendered into the public workflow.
Signing material
The repository stores only configurable secret names. Certificate private-key material, password, and provisioning profile values belong in the protected Environment. The request contains the expected public certificate, Team ID, profile, entitlement, and SHA-256 device evidence plus opaque secret references. Values are never request arguments, repository files, public reports, or diagnostics.
Manual setup accepts at most three signable targets: the application, Widget extension, and Live
Activity extension. Extension-bearing projects require one repeatable, exact
--profile TARGET=PATH argument for every generated application and extension target. An unkeyed
--profile PATH remains compatible only with a single application target; keyed and unkeyed forms
cannot be mixed. All profiles must authorize the same selected device and match the certificate,
Team, target bundle identifier, required entitlements, and validity window. The PKCS#12 and profile
paths must resolve to stable regular files outside every Git repository; on Unix, each file must
also have one hard link. The client verifies the PKCS#12 private-key match, Apple Development
certificate chain, Team ID and validity, then verifies every profile’s CMS signature and bindings.
Use a dry run first:
cargo ferry signing setup manual \
--certificate /private/signing/development.p12 \
--profile weather=/private/signing/application.mobileprovision \
--profile FerryWidgetExtension=/private/signing/widget.mobileprovision \
--profile FerryLiveActivityExtension=/private/signing/live-activity.mobileprovision \
--remote github \
--device-sha256 <lowercase-sha256> \
--dry-run
Target names are exact and case-sensitive; use the generated signing preview rather than guessing
them. --device-sha256 is optional only when all supplied profiles contain the same single
registered device. The example uses the interactive no-echo prompt. Password input is mutually
exclusive: omit all selectors for that prompt, or use
--password-stdin, --password-env <NAME>, or --password-credential <ENTRY>. Credential entries
use operating-system secure storage under service org.rustferry.cargo-ferry.signing.
GH_TOKEN and GITHUB_TOKEN cannot be used as password-variable names. Passwords are bounded to
4 KiB of UTF-8 without NUL or line-break bytes and are never accepted as command arguments.
The dry run performs asset validation and read-only GitHub policy checks, prints only public
metadata, and uploads nothing. A mutating interactive run prints the same preview and asks for
confirmation. JSON, non-interactive, and --password-stdin mutation require --yes; repeat the
reviewed command with --yes only after the dry run.
Initial setup requires a public source repository, a distinct active private execution repository,
and an empty protected Environment. The Environment must require a deployment reviewer, enable
custom branch policies, and contain exactly rustferry/goal3/builds/*. After rechecking local and
remote state, the client cryptographically revalidates the retained bytes and sends canonical padded
base64 PKCS#12 and profile values plus the raw password. The certificate, password, and application
profile retain these fixed names:
RUSTFERRY_GOAL3_IOS_CERTIFICATE_P12RUSTFERRY_GOAL3_IOS_CERTIFICATE_PASSWORDRUSTFERRY_GOAL3_IOS_PROVISIONING_PROFILE
Each extension profile uses a canonical static name
RUSTFERRY_GOAL3_IOS_PROFILE_<32_HEX>, where the uppercase suffix is derived from the SHA-256 of the
exact target name, a NUL separator, and its bundle identifier. The preview shows the complete mapping
before mutation. The protected Environment and generated workflow must contain exactly two
certificate/password secrets plus one profile secret per signable target: three to five names.
Each final value is limited to 48 KiB. The upload process sends secret bytes to gh only through
standard input, not its arguments, environment, output, or repository files. Existing secrets are
never replaced implicitly. A project-local exclusive lock and stable no-follow file snapshots
serialize config writers. The client requires the exact planned name set after upload, rechecks the
workflow and private provider config, and persists the signing plan last. Partial or indeterminate
remote writes leave the config unsigned and list both uploaded and possibly-uploaded cleanup roles.
A failure after atomic config replacement reports the config as possibly signed and requires
inspection instead of claiming rollback.
The generated protected signing job binds every reviewed secret name statically. Multi-profile jobs
send the exact reference/value set through the bounded RFSIGNV2 stdin frame; the worker rejects
missing, duplicate, unknown, oversized, or trailing records and resolves each secret once. The
legacy three-field one-profile frame remains accepted only for a single application profile. Neither
format places secret values in arguments, workflow files, or logs.
Modern setup also stores the exact public target graph: target name, bundle identifier, and kind for the application, extensions, frameworks, and dynamic libraries. The workflow embeds a domain-separated canonical SHA-256 of that complete graph. The provider requires an exact order-independent match, and the worker rederives the digest before checkout of the requested project revision or compilation. Schema-v2 configuration remains readable only for the legacy target-free workflow; adding targets requires recreating the provider workflow instead of silently weakening the binding.
The worker uses a per-job private keychain and provisioning home. Changes to the user-global keychain search list are serialized by one worker-user-wide lock. Cleanup restores the prior search list and proves removal of decoded material, the keychain, isolated home, export options, validation workspace, and private workspace. A missing cleanup proof prevents success.
Artifact acceptance
GitHub’s successful conclusion is insufficient. The client selects artifact names by exact run ID
and attempt, verifies GitHub metadata size/digest before writing, validates the sealed phase-A
handoff, and binds the final report to the submitted request and sealed archive digests. Final ZIP
ingestion accepts the exact request-derived set: the IPA, artifact manifest, signing report,
validation report, and sanitized protected-signing log, plus only the explicitly selected
application.app.zip, application.xcarchive.zip, and application.dSYM.zip products. The log is
manifest-bound and plain-text validated; compile-phase output is not substituted for it. Ingestion
rejects links, traversal, collisions, aliased ZIP headers or payload ranges, expansion bombs,
implicit wrapper roots, source or signing-material paths, unexpected files, identity drift, and
existing output.
Verification uses a newly created operation directory beneath a private cache root. Any error removes that exact directory. A successful in-process cache retains only the verified downloadable files; transport ZIPs and extraction staging are removed, and dropping the store removes the operation directory. Fresh CLI processes therefore do not accumulate duplicate multi-gigabyte run caches.
Cross-platform IPA inspection verifies Payload/<App>.app, plist identity, arm64 Mach-O physical-iOS
platform metadata, nested code inventory, and embedded provisioning presence. Remote evidence must
also prove strict code-signature validation, certificate/profile/team/device/entitlement bindings,
and complete signing cleanup. Optional application and XCArchive transports are accepted only when
their path, size, SHA-256, and executable-bit trees exactly match the inspected IPA application; the
archive application also requires a fresh deep strict signature check. The dSYM transport requires
an explicit single wrapper, real DWARF content, an arm64 MH_DSYM, and an exact nonzero LC_UUID
match with the signed main executable. Publication and rollback retain original file identities and
fail closed when a replacement path is observed.
The protected Phase B job has no source checkout, compile step, or untrusted same-UID process. Capability-relative file cleanup is fail-closed for observed identity replacement, but portable POSIX APIs do not provide atomic unlink-if-inode against an actively racing same-UID peer. A shared or multi-tenant runner therefore requires separate OS identities or stronger isolation and is not a validated deployment mode.
Required external controls
- Protected Environment limited to the signing job, with custom policies enabled and the single
rustferry/goal3/builds/*deployment branch policy. - Empty protected Environment before initial setup; exact planned three-to-five signing-secret set afterward.
- Private signing repository with restricted membership and controlled visibility changes.
- Required deployment reviewer while signing workflow bytes come from the temporary push branch.
- Minimal repository/Actions permissions; no fork or
pull_request_targetsigning path. - Protected Environment certificate/profile secrets with expiry monitoring and rotation.
- Immutable worker distribution or an equivalently isolated trusted worker-build provenance path.
- Limited artifact retention and exact-operation cleanup.
These controls must be observed through GitHub API evidence before a signed acceptance run. Their absence is a setup failure, not a warning.
Rust package readiness
RustFerry has 10 workspace members: nine publishable crates and one non-publishable trusted worker.
All publishable versions come from workspace.package; a release must keep them identical.
| Order | Package | Role | Internal prerequisites |
|---|---|---|---|
| 1 | rustferry-core | Configuration, validation, assets, process control | None |
| 2 | rustferry | Application runtime API | None |
| 3 | rustferry-codegen | Project, capability, and asset generation | rustferry-core |
| 4 | rustferry-remote | Remote-build protocol, source, signing, and artifact contracts | rustferry-core on Windows |
| 5 | rustferry-android | Direct Android packaging backend | rustferry-core, rustferry-codegen |
| 6 | rustferry-apple | Apple generation and artifact backend | rustferry-core, rustferry-codegen, rustferry-remote |
| 7 | rustferry-github | GitHub transport and workflow provider | rustferry-core, rustferry-remote |
| 8 | rustferry-ssh | Pinned OpenSSH transport for macOS workers | rustferry-core, rustferry-remote |
| 9 | cargo-ferry | Public CLI | Runtime, backend, core, codegen, remote, GitHub, and SSH crates |
Wait for each prerequisite version to appear in the registry index before publishing the next group. The automated release workflow never publishes to crates.io.
Manifest contract
Every crate must declare its name, version, Rust version, description, repository, homepage, documentation URL, dual-license expression, README, keywords, categories, and an explicit include set. Internal path dependencies must also carry the exact release version so Cargo removes the path when it normalizes the package.
Each crate root links LICENSE-MIT and LICENSE-APACHE to the canonical
workspace files and includes both names explicitly. Cargo dereferences those
links into regular files in the portable archive. The archive scanner requires
both members at the package root and verifies their exact SHA-256 digests.
The committed workspace Cargo.lock is the release lock. Use --locked for
every gate. Generated package archives contain only their declared source,
templates, tests, and embedded docs; they must not contain target/, signing
material, absolute developer paths, or missing include_bytes!/include_str!
inputs.
rustferry-worker-macos is a workspace-only trusted worker and declares
publish = false. Workspace checks compile and test it, but package and publish
commands must explicitly exclude it.
Package gates
Run from the repository root on a clean release revision:
cargo metadata --locked --no-deps --format-version 1
python3 scripts/check-release-contract.py
cargo package --workspace --exclude rustferry-worker-macos --locked --list
cargo package --workspace --exclude rustferry-worker-macos --locked
cargo publish --workspace --exclude rustferry-worker-macos --dry-run --locked
python3 scripts/check-release-archives.py \
--check-sources \
--target-dir target/package-source-check \
target/package/*.crate
cargo package --workspace --exclude rustferry-worker-macos verifies the
normalized archives together, so unpublished internal dependencies resolve
from the package set. The following publish --dry-run repeats registry upload
checks and package verification without uploading. Do not weaken the release
gate with --no-verify.
Inspect the produced archives before approving a draft:
find target/package -maxdepth 1 -name '*.crate' -print | sort
for archive in target/package/*.crate; do tar -tzf "$archive"; done
The manual draft-release workflow copies all nine .crate files into one
release assembly, adds the schema, VSIX, license bundle, release notes, and
SHA-256 checksums, then uploads that assembly as a workflow artifact.
Historical package results do not validate a later release revision. Re-run the package, source, license, and publish dry-run gates for all nine archives and record the resulting file counts and compressed sizes.
Publish procedure
Publishing is a separate, protected manual operation. Run a complete dry-run,
then publish one crate at a time in the documented dependency order. Confirm
each version with cargo info <CRATE>@<VERSION>, cargo owner --list <CRATE>,
and the crates.io API before continuing. Do not use --no-verify, pass a token
on the command line, bump versions, or publish from a dirty checkout.
After publication, install the CLI from the registry into an isolated Cargo root and generate/check a new project without a runtime-path environment override. This is the acceptance test for registry-based template resolution; workspace-path tests alone are insufficient.
If a publish command times out, query the registry before retrying. If a later crate fails after prerequisites are public, preserve the published boundary and do not yank merely to make the release atomic. Fix only unpublished crates when their contracts can remain compatible; otherwise prepare the next patch. Yank only for a specific security, legal, or unusable-package defect and document the reason.
Publish Rust crates
RustFerry contains 10 workspace members: nine publishable crates with one workspace version plus the non-publishable macOS worker. Publishable crates must be published in this topological order:
rustferry-core;rustferry;rustferry-codegen;rustferry-remote;rustferry-android;rustferry-apple;rustferry-github;rustferry-ssh;cargo-ferry.
rustferry-worker-macos is a non-publishable workspace tool. Keep it in normal
workspace checks and exclude it from package/publish selection.
Run package and upload dry-runs from a clean release revision:
cargo package --workspace --exclude rustferry-worker-macos --locked --list
cargo package --workspace --exclude rustferry-worker-macos --locked
cargo publish --workspace --exclude rustferry-worker-macos --dry-run --locked
python3 scripts/check-release-contract.py
python3 scripts/check-release-archives.py \
--check-sources \
--target-dir target/package-source-check \
target/package/*.crate
Inspect every normalized archive, its manifest, canonical root license files, embedded templates/docs, and absence of generated output, signing material, or developer paths. The draft-release workflow assembles .crate files but never publishes them.
Real publication is manual. Run cargo publish -p <CRATE> --locked once per crate in the order above. After every upload, wait for cargo info <CRATE>@0.1.0 and the crates.io API to expose the version, then verify owner, repository, documentation, and license metadata before continuing. Do not use --no-verify for any release upload. Afterward, install cargo-ferry into an isolated Cargo root and generate/check a project using registry dependencies.
If an upload times out, query the registry before retrying. Never overwrite an existing version. Do not yank a successfully published prerequisite merely because a later crate failed; record the exact publication boundary and either fix only unpublished crates without changing published contracts or prepare a coordinated patch release. Yank only for a concrete security, legal, or unusable-package defect. See package readiness for the complete manifest and archive contract.
VSIX packaging
The extension source is under editors/vscode. It uses a pinned local
@vscode/vsce dependency and never requires a globally installed packager.
Build and inspect
cargo build -p cargo-ferry
cd editors/vscode
npm ci
npm run typecheck
npm run lint
npm test
npm run perf
npm run test:host
npm run package
npm run vsix:smoke
test:host requires the real debug CLI at ../../target/debug/cargo-ferry.
Set RUSTFERRY_TEST_CLI to use another executable. perf has the same rule.
npm run package produces dist/rustferry-vscode.vsix. Release assembly
renames it to rustferry-vscode-<version>.vsix without changing its bytes.
The 2026-08-01 acceptance candidate contains 18 entries, is 44,435 bytes, and
has SHA-256 ba8cac7e8d5ec10d3c7a96082f405c3d4d5cdd64afef82bc1f50a5a3d183ce6d.
These values identify a historical candidate. Marketplace version 0.1.0 is
publicly listed. The final release assembly contains a later 73,636-byte VSIX
with SHA-256 0ae442e9c5b5fb2bc27a9af5093f227a7b42a0334238ce4faf0fc6ce09135641;
the protected workflow did not republish it during release closeout because
the VSCE_PAT environment secret is not configured.
The base extension run passes 42 tests and skips 4 live-CLI tests when no CLI is supplied. With the final CLI supplied, all 46 tests pass across 12 files. The real Extension Host smoke also passed.
The production bundle targets Node 20. Source maps are generated locally for
development with sourcesContent disabled, then excluded from the VSIX.
node_modules, TypeScript sources, tests, package locks, repository workflows,
and nested VSIX files are also excluded. The smoke script verifies required
entries, rejects development files, scans text entries for common secrets and
absolute developer paths, and prints size plus SHA-256.
Extension Host smoke
npm run test:host uses pinned @vscode/test-electron and VS Code 1.100.0.
It launches two isolated Extension Hosts with disposable user and extension
directories:
- an ordinary Rust workspace must leave RustFerry inactive;
- a workspace containing
ferry.tomlmust auto-activate, register the core commands, discover exactly one project, refresh its trees, open the discovered manifest, and validate a dirty manifest buffer without changing the file on disk.
The ferry fixture uses the real cargo-ferry protocol for discovery and
validation. The smoke does not build, install, or run a mobile application and
does not require an Android SDK, emulator, simulator, or device. Host profiles
are removed after every run. The downloaded VS Code runtime is cached outside
the repository; set RUSTFERRY_VSCODE_TEST_CACHE to choose that cache.
Linux CI runs the command through xvfb-run --auto-servernum. Local Linux
reproduction uses the same wrapper:
RUSTFERRY_TEST_CLI="$PWD/../../target/debug/cargo-ferry" \
xvfb-run --auto-servernum npm run test:host
The test prints a RUSTFERRY_HOST_PERF JSON line with host activation,
discovery, tree refresh, and manifest-open observations. These are diagnostic
measurements, not performance promises.
CI and publication
The VS Code workflow runs npm ci and npm run check on Linux, macOS, and
Windows. Linux additionally builds the real cargo-ferry binary, runs the
headless Extension Host smoke and performance measurements, and enables the
protocol integration test. Only the Linux VSIX is uploaded, avoiding three
identical artifacts.
Marketplace publication remains absent from push and pull-request workflows.
The separate manual workflow accepts the successful draft-release assembly run
ID, requires an exact source-revision match, verifies the retained VSIX before
and after protected approval, and exposes VSCE_PAT only to the final
vsce publish --pre-release step.
Publish to the VS Code Marketplace
Marketplace publication is a separate protected manual operation. Push and pull-request workflows only verify the extension and upload a VSIX workflow artifact; they never receive a Marketplace credential or publish.
Protected credential
The GitHub Environment is vscode-marketplace. It requires a reviewer and must hold one Environment secret, VSCE_PAT. For the temporary PAT path, create a short-lived Azure DevOps token for All accessible organizations with only Marketplace: Manage. Enter it directly in GitHub; never pass it as a vsce argument or expose it in logs.
The pinned local vsce reads VSCE_PAT from its environment. The workflow maps that secret only to the final publication step and runs vsce publish --pre-release against the already-inspected VSIX.
Global Azure DevOps PATs stop working on December 1, 2026. Replace this temporary credential with Microsoft Entra ID/workload identity or Marketplace trusted publishing before that date; do not silently broaden or extend the PAT. See the official VS Code publishing guide and Azure DevOps retirement notice.
Publish the assembly candidate
Before publication:
- run all checks in VSIX packaging;
- run Draft release on the exact intended
masterrevision with draft creation disabled; - inspect the final VSIX allowlist and secret/path scan;
- verify the assembly checksums, release notes, VSIX SHA-256, size, publisher, and version;
- run Publish VS Code Marketplace on the same revision with the version and successful assembly run ID;
- approve the
vscode-marketplacedeployment only after the unprivileged assembly verification job passes.
The publication workflow rejects an assembly from another workflow or revision, downloads the exact retained VSIX instead of rebuilding it, verifies SHA256SUMS before and after approval, and publishes it as a prerelease. Then verify the public publisher, version, prerelease state, listing, and install the Marketplace version into an isolated VS Code profile.
Do not publish automatically from ordinary CI, expose the token to pull requests, or claim publication from a successful VSIX build. RustFerry for VS Code 0.1.0 is published at ShiroKSH.rustferry-vscode.
Create a GitHub Release
Run the manual Draft release workflow with create_draft_release disabled to verify and assemble the nine .crate archives, versioned VSIX, IDE protocol schema, license bundle, changelog-derived notes, and SHA256SUMS. Download that workflow artifact, verify every checksum and expected member, and inspect the notes. This assembly run does not publish crates or create a tag.
Create the public release only after:
- the release commit is on
masterand all required exact-SHA checks are green; - all nine crates are visible and verified on crates.io;
cargo-ferrypasses the isolated registry-only installation and generated-project smoke test;- any intended release assets have been reproduced, inspected, and scanned for private signing material.
Create and push an annotated tag from the verified master commit:
git tag -a v0.1.0 -m "RustFerry 0.1.0"
git push origin v0.1.0
Use the assembly’s RELEASE_NOTES.md, which is generated from the complete 0.1.0 changelog section and records the exact source revision. Confirm it includes the crates.io package list, cargo install cargo-ferry --locked, Rust 1.92 minimum, artifact-validated platform scenarios, still-unvalidated runtime/device scenarios, and the Slint licensing and attribution section with the official #MadeWithSlint badge. Do not redraw, recolor, or replace the badge. Then create the GitHub release as a pre-release:
gh release create v0.1.0 \
--verify-tag \
--prerelease \
--title "RustFerry 0.1.0" \
--notes-file RELEASE_NOTES.md
Do not attach incidental local artifacts. Attach an Android artifact only when it is an intentional reproducible release asset, its package/signature/alignment/ABI have been verified, and it contains no private signing material.
After creation, verify that the tag and release target the intended master commit, the release is marked pre-release, and the published notes contain the complete changelog content and required installation/licensing context.
Release checklist
No step below implies that a release or registry publication has occurred.
Source and version
- Clean checkout on the intended protected revision; CI green.
- Git author and repository destination verified.
- One explicit version across all workspace crates and the extension.
- Changelog contains complete notes for that version.
- Documentation no longer describes the selected version as unpublished.
- No signing files, environment files, generated platform artifacts, or developer-specific paths tracked.
Licensing
- Run
python3 scripts/check-licenses.py --generate; inspect the inventory diff instead of accepting it mechanically. - Run
python3 scripts/check-licenses.pywith no stale inventory. - Confirm RustFerry root licenses and the release license bundle are present.
- Record the Slint license path for every generated mobile binary considered for attachment. Do not attach such binaries when the choice is unresolved.
- Confirm release notes retain the official
#MadeWithSlintbadge unchanged and explain the downstream publisher’s attribution or alternate-license duty. - Recheck third-party notices if the VSIX gains any production npm import.
Rust and packages
-
cargo fmt --all -- --check -
cargo clippy --locked --workspace --all-targets --all-features -- -D warnings -
cargo test --locked --workspace --all-targets --all-features -
RUSTDOCFLAGS="-D warnings" cargo doc --locked --workspace --all-features --no-deps -
python3 scripts/check-release-contract.pyreports every internal edge exact. -
cargo package --workspace --exclude rustferry-worker-macos --locked --list -
cargo package --workspace --exclude rustferry-worker-macos --locked -
cargo publish --workspace --exclude rustferry-worker-macos --dry-run --locked -
python3 scripts/check-release-archives.py --check-sources --target-dir target/package-source-check target/package/*.crate - Inspect all nine
.cratearchives and their normalized manifests; confirm both canonical license files are regular root members.
VS Code extension
-
npm cifromeditors/vscode. -
npm run checkpasses, including VSIX structural smoke. -
npm run test:hostpasses with the intended realcargo-ferrybinary. - Ordinary Rust stays inactive; a
ferry.tomlworkspace auto-activates, registers commands, discovers the project, and opens its manifest. -
npm run perfresults reviewed and recorded with host and revision context. - Views and trust gating checked manually when their behavior changed.
- VSIX size and SHA-256 recorded from the final bytes.
Platform evidence
- Android and iOS Simulator artifact workflows green for the release commit.
- Exact artifact validation recorded; no simulator/device/signing claim beyond observed evidence.
- Physical-device binaries excluded unless signing, installation, launch, and required license notices were actually validated.
Release assembly
- Run the
Draft releaseworkflow with the exact workspace version andcreate_draft_releasedisabled. - Download the release assembly. Verify package count, schema, versioned VSIX,
license bundle, notes, and every entry in
SHA256SUMS. - Confirm the release commit has successful exact-SHA
pushjobs for CI, VS Code, both platform artifacts, and mdBook/Pages. - Keep the assembly private until registry publication and registry-only smoke testing succeed.
Registry and Marketplace publication
- Publish crates manually in the order documented in Rust package readiness, waiting for registry visibility between dependency groups.
- Verify local crates.io authentication without printing or passing the token; stop before the first upload when
cargo loginis still required. - Verify each crate version, tarball, checksum, dependency metadata, docs.rs build, and registry timestamp.
- Install
cargo-ferryfrom crates.io into an isolated Cargo root; generate and check a project without a runtime-path override. - Publish the already-inspected VSIX to Marketplace only through a protected
manual
vscode-marketplaceenvironment with a required reviewer and theVSCE_PATEnvironment secret. - Verify the Marketplace workflow consumed the successful assembly run from
the exact release revision and used
vsce publish --pre-releasewithout a token argument. - Verify Marketplace version and install the public extension into an isolated VS Code profile.
- Create and push annotated tag
v0.1.0from the verifiedmastercommit. - Create GitHub Release
RustFerry 0.1.0as a pre-release with manually reviewed notes covering packages, installation, Rust 1.92, validation limits, and Slint licensing. - Verify the tag target, pre-release flag, notes, links, and any intentional assets.
- Create the next patch
Unreleasedchangelog section and commit release closeout. - Track migration from the temporary global Marketplace PAT to Microsoft Entra ID or trusted publishing before December 1, 2026.
Failure and rollback policy
- Query crates.io before retrying any timed-out upload.
- Record the last verified package when publication stops partway through.
- Never overwrite a published version or change its dependency contract.
- Do not yank a healthy prerequisite because a later package failed. Fix only unpublished packages when compatible; otherwise prepare the next patch release.
- Yank only for a concrete security, legal, or unusable-package defect, and record the reason and replacement version.
Third-party licenses
This inventory separates what RustFerry distributes from what its development tooling or generated applications resolve. It is release-engineering context, not legal advice.
Distribution surfaces
| Surface | Third-party material | Release treatment |
|---|---|---|
| Rust source crates | Dependencies resolved by the root Cargo.lock | Exact names, versions, sources, and SPDX expressions are in LICENSES/cargo-dependencies.json. Root source remains MIT OR Apache-2.0. |
cargo-ferry binary | Linked Rust dependencies from the same lock | Ship the root licenses and the Cargo inventory with binary releases. Preserve any upstream notices required by the selected license branch. |
| VS Code extension | VS Code API plus Node built-ins; no third-party runtime import in the current production bundle | VS Code is external. Packaging excludes node_modules and source maps; the VSIX smoke test enforces that boundary. |
| VS Code build toolchain | Packages in editors/vscode/package-lock.json | Development-only inventory in LICENSES/vscode-development-dependencies.json. It includes @vscode/vsce-sign, whose Microsoft terms restrict it to use with Visual Studio products and services. It is not bundled in the VSIX. |
| Generated Java and Swift glue | RustFerry-authored templates | Covered by RustFerry’s MIT OR Apache-2.0 license; no copied platform SDK source is stored in the repository. |
| Icons and splash assets | Original RustFerry development artwork | No external image or font input. Generated derivatives retain the project license. No font files are bundled. |
| Generated application UI | Slint 1.17.1 and its resolved application dependency graph | The application distributor must choose and satisfy a Slint license and audit the generated application’s own lockfile. RustFerry release packages do not redistribute Slint itself. |
The current root lock includes option-ext under MPL-2.0 and ICU4X-related
packages under Unicode-3.0. r-efi declares an OR expression that offers MIT
or Apache-2.0 in addition to LGPL-2.1-or-later; accepting the expression in CI
does not itself select a branch. Any future linked CLI binary distribution must
record its selected branches and carry the corresponding notices. The current
draft workflow ships source .crate packages, not a linked CLI binary.
Slint 1.17.1
Slint 1.17.1 offers three framework license paths:
- The Slint Royalty-free License 2.0 covers proprietary or open desktop,
mobile, and web applications at no cost when its attribution condition is
met. The license permits either an accessible
AboutSlintwidget or the Slint attribution badge on a readily discoverable public application page. It excludes embedded systems, standalone Slint distribution, and applications exposing Slint APIs. - GPL-3.0-only covers open-source applications under GPL-compatible terms and carries GPL source and redistribution obligations for the distributed work.
- A commercial Slint license covers use cases outside the two paths above, including proprietary embedded applications or applications without the royalty-free attribution.
Generated RustFerry starters keep AboutSlint visible. That is useful for the
royalty-free path, but it does not make a license selection for the application
author. The pinned royalty-free text is copied in
LICENSES/SLINT-ROYALTY-FREE-2.0.md; the canonical Slint 1.17.1 overview and
license texts remain at:
- https://github.com/slint-ui/slint/blob/v1.17.1/LICENSE.md
- https://github.com/slint-ui/slint/tree/v1.17.1/LICENSES
Do not attach a generated APK, .app, or .appex containing Slint to a public
release until that artifact’s license path and notices are recorded.
Dependency policy
scripts/check-licenses.py enforces reviewed Cargo and npm license-expression
sets and deterministic machine inventories. The npm lock currently describes
build/test/package tools only. If extension production code gains a third-party
runtime import, add that package’s notice to the VSIX and update this document
before release.
After any lockfile change:
python3 scripts/check-licenses.py --generate
git diff -- LICENSES/
python3 scripts/check-licenses.py
Review changed licenses and notices; never accept a newly observed expression only to make CI green.
Android setup
RustFerry’s Android backend calls the SDK, NDK, Rust, and JDK tools directly. It does not require Android Studio, Gradle, an emulator, adb, or a connected device for build.
Required tools
- Rust, Cargo, and rustup.
- One Rust Android target for every configured ABI. The starter uses
aarch64-linux-androidforarm64-v8a. - Android SDK platform matching
android.target_sdk, or any installed platform when it isinstalled. - A complete SDK Build Tools revision containing
aapt2,d8,zipalign, andapksigner. - Android NDK with an LLVM prebuilt for the host.
- A JDK containing
java,javac, andkeytool.
Typical setup for the starter:
rustup target add aarch64-linux-android
sdkmanager "platforms;android-35" "build-tools;35.0.0" "ndk;29.0.14206865"
cargo ferry doctor
Accept SDK licenses deliberately with the Android SDK tooling. cargo ferry doctor is read-only and never accepts licenses or installs components.
Discovery order
The SDK root is resolved from an explicit CLI/build request, then ANDROID_SDK_ROOT or ANDROID_HOME, then the conventional host location:
- macOS:
~/Library/Android/sdk - Linux:
~/Android/Sdk - Windows:
%USERPROFILE%\AppData\Local\Android\Sdk
The NDK is resolved from an explicit path, ANDROID_NDK_HOME or ANDROID_NDK_ROOT, versioned directories below <sdk>/ndk/, then <sdk>/ndk-bundle. Installed platforms, Build Tools, and NDKs are sorted numerically; the newest complete compatible installation is selected. JAVA_HOME/bin takes precedence over PATH for JDK tools.
Doctor scope
The Android report separates build requirements from optional deployment tools. Missing adb or emulator tooling is a warning because APK creation does not use either. Missing SDK platform, Build Tools, NDK/linker, Cargo, Rust target, Java runtime, or keytool blocks a build and includes the searched paths and a concrete install command.
References: Android command-line build tools, Rust platform support, and Slint Android setup.
Direct Android build
cargo ferry build android
The command builds only. It does not discover devices, start an emulator, install the APK, launch the application, or stream logs.
Pipeline
- Validate
ferry.tomland discover a compatible SDK platform, complete Build Tools, NDK LLVM prebuilt, and JDK. - Generate a deterministic
AndroidManifest.xml,res/tree, and private Java runtime bridge undertarget/ferry/. - Run Cargo once per configured ABI with a target-specific NDK Clang linker and
--message-format=json-render-diagnostics. - Parse Cargo
compiler-artifactmessages for thecdylib. Parsebuild-script-executedmessages and recursively collect dependency.dexfiles from thoseOUT_DIRs. - Compile the generated
FerryActivity, capability bridge, notification receiver, widget provider, and read-only share provider directly withjavacagainst the selectedandroid.jar. - Compile and link resources with
aapt2. - Merge compiled bridge classes and dependency bytecode with
d8. - Add stored
lib/<abi>/lib<name>.soandclasses*.dexentries to the resource APK. - Run
zipalign -P 16 -f 4before signing. - Sign with
apksigner, then verify the signature and runzipalign -c -P 16 4. - Independently inspect the ZIP: reject unsafe/duplicate names; require manifest, resources, icon, sequential DEX files, exact ABI libraries, and matching ELF class/machine headers. Enabled manifest components must have class definitions in merged DEX.
aapt2 dump badgingmust report the configured package andorg.rustferry.bridge.FerryActivitylauncher.
All external processes receive an executable plus an argument array. Paths with spaces or Unicode are not joined into a shell command. Each stage has a bounded runtime and a log below target/ferry/android/<profile>/logs/.
Output
The debug artifact is:
target/ferry/android/debug/<native-library-name>.apk
Intermediates and generated platform glue remain under target/ferry/; no Gradle project, Java/Kotlin source, or Android Studio project is written into user source.
Runtime bridge
FerryActivity is a generated subclass of Android’s NativeActivity, so the Rust entry point remains android_main(AndroidApp). The generated starter installs rustferry::android before Slint. Typed RustFerry calls cross one private JSON/JNI method; Java performs Android framework and main-thread work, while Rust owns typed results and application events.
Capability flags are baked into the bridge. rustferry::supports is true only when the corresponding configuration enables a concrete Android implementation. Persistent ordinary storage uses FileStorage below the application’s internal data directory. rustferry::android::with_context is an advanced synchronous escape hatch; raw JNI references must not outlive its callback.
Android emits foreground, background, resume, pause, low-memory, theme, window-size, network, deep-link, and notification-open events when the platform provides them. It deliberately does not translate Activity.onDestroy into Terminating: configuration changes also destroy activities, while process termination may deliver no callback. Persist important state eagerly.
Current Android widget rendering supports title/value/caption text plus one text, link, or button content node. Other widget content, image, or progress shapes return a backend error instead of reporting false success. File sharing accepts files inside application-owned files/cache directories through the generated read-only content provider. Android Live Activities use the configured ongoing-notification fallback.
ABIs
ferry.toml ABI | Rust target | APK directory |
|---|---|---|
arm64-v8a | aarch64-linux-android | lib/arm64-v8a/ |
x86_64 | x86_64-linux-android | lib/x86_64/ |
armeabi-v7a | armv7-linux-androideabi | lib/armeabi-v7a/ |
Each configured Rust target must already be installed. Cargo’s own target directory provides incremental Rust compilation. Generated resource and D8 intermediates use content fingerprints plus output-digest completion markers. Interrupted or modified intermediates are rebuilt instead of counted as cache hits, and builds sharing one profile output are serialized with a lock.
Dry run
cargo ferry build android --dry-run performs discovery and request validation, then returns an ordered plan without writing generated files, creating a keystore, or launching commands. Commands are represented as redacted argument arrays. The D8 step explicitly records that its inputs are deferred until Cargo JSON is parsed.
The custom-tool order follows Android’s documentation for AAPT2, zipalign, and apksigner.
Recorded artifact evidence
Current RustFerry evidence comes from Platform artifacts run 30719811812 at commit 8ed0192. The public CLI generated and built default Starter and Kitchen Sink projects for arm64. Both APKs passed ZIP integrity, v2/v3 signature, 16 KiB-aware alignment, package/launcher/API, classes.dex, compiled resources and icon, AArch64 ELF, android_main, and JNI callback checks. The Kitchen Sink APK additionally passed exact permission, deep-link, notification receiver, file provider, widget provider, and Live Activity fallback inspection.
Historical pre-rename evidence remains reproducible by its exact path: on 2026-08-01, generated_minimal_project_produces_verified_apk built target/android-e2e/pocket/android/debug/android_probe.apk with the installed SDK 35, Build Tools, NDK 29, Java 21, and aarch64-linux-android target. APK signature verification, 16 KiB-aware alignment, package com.example.androidprobe, singleTop launcher, ZIP integrity, DEX classes, the arm64-v8a ELF header, android_main, and the JNI callback passed inspection. Signed-binary XML inspection also proved the exact configured permission set, Activity/FileProvider/NotificationReceiver/WidgetProvider components, and scheme=probe;host=open.example;pathPrefix=/details deep-link filter. A repeated build reported cache hits for AAPT2 compile/link and D8. The pre-rename public CLI also completed new then build for fresh Starter and Kitchen Sink projects; exact legacy paths and scope are recorded in the historical status record.
No emulator or device behavior was observed in either the current or historical validation.
Android signing
Every successful APK is signed and then verified. Alignment happens before apksigner; no archive mutation occurs after signing.
Persistent debug identity
The first debug build creates a machine-local PKCS#12 key and a random password file under cargo-ferry’s operating-system configuration directory:
- macOS:
~/Library/Application Support/cargo-ferry/android/ - Linux:
~/.config/cargo-ferry/android/ - Windows: the user’s roaming application-data config directory under
cargo-ferry\android\
The keystore is reused for later builds, allowing debug upgrades instead of producing a new signing identity every time. Creation is protected by a file lock for concurrent first builds. On Unix, the directory is mode 0700 and the keystore/password files are mode 0600.
RustFerry validates an existing key with keytool -list, checks the certificate expiry date, and refuses to overwrite it. An expired certificate fails before APK signing. Move the keystore and password file aside together to let the next build create a fresh identity. If the keystore exists but its password file is missing, restore the password file or move the old pair aside deliberately.
Password handling
Passwords never belong in ferry.toml, the project repository, normal output, a rendered dry-run plan, or an inline pass:<value> process argument. Signing accepts only:
file:/absolute/pathpassword sources; orenv:VARIABLE_NAMEreferences for an already exported exact variable.
Debug key creation passes the password file through keytool’s -storepass:file form. apksigner receives a password-file or environment reference, not the value. Full tool output is saved to the generated build log directory; command lines there are redacted.
Release keys
Release signing uses an explicit keystore, key alias, store-password source, and optional distinct key-password source. RustFerry checks that the keystore/password files exist before executing the build. It never copies release keys into target/ferry/ or user source.
Back up a release key and its credentials separately. Losing the signing identity prevents upgrades to an application distributed under that identity.
Android manifest permissions
Manifest declarations come only from enabled capabilities and permission settings. Purpose text is validated in ferry.toml for cross-platform UX but is not embedded in Android’s manifest; Android rationale UI remains application code.
| Configuration | Android manifest output |
|---|---|
network none | no network permission |
network status | ACCESS_NETWORK_STATE |
network optional or required | ACCESS_NETWORK_STATE, INTERNET |
| configured network probe | INTERNET |
| local notifications | POST_NOTIFICATIONS |
| Live Activity ongoing-notification fallback | POST_NOTIFICATIONS |
| haptics | VIBRATE |
| camera | CAMERA |
| microphone | RECORD_AUDIO |
| location when in use | ACCESS_COARSE_LOCATION, ACCESS_FINE_LOCATION |
| photos, target API 33+ | READ_MEDIA_IMAGES |
| photos, minimum API below 33 | READ_EXTERNAL_STORAGE capped with android:maxSdkVersion="32" |
| local network | no Android declaration; Apple-only privacy permission |
| widget extension | widget receiver and provider metadata |
| sharing | non-exported, read-only application file provider |
| custom deep-link schemes | VIEW/BROWSABLE intent filter on the launcher activity |
Deep-link filters are the Cartesian product of configured schemes, allowed_hosts, and
allowed_actions; actions become path prefixes. The generated bridge repeats the same checks for
cold-start and running-app intents, and rejects undeclared scheme/host/action values before Rust
receives an event. An empty host or action list leaves that dimension unrestricted.
The notification permission is declared only for local notifications or the enabled ongoing-notification fallback. The runtime requests it only in response to an explicit application call on Android versions where a runtime request exists. On older versions it also checks whether the user disabled this application’s notifications in system settings.
Notification, widget, and share components are omitted when their capabilities are disabled. Every generated activity, receiver, or provider is checked against the merged DEX before packaging completes.
Review generated output at:
target/ferry/android/<profile>/generated/<fingerprint>/AndroidManifest.xml
The final binary manifest and package metadata are checked again from the signed APK.
Android troubleshooting
Android SDK or platform not found
Run cargo ferry doctor and inspect every searched SDK root. Set ANDROID_SDK_ROOT to the SDK directory or install the requested platform with sdkmanager "platforms;android-<api>".
Complete Build Tools not found
One revision must contain all four tools: aapt2, d8, zipalign, and apksigner. An incomplete newer directory is skipped in favor of the newest complete revision. Reinstall with sdkmanager "build-tools;<version>".
NDK linker not found
The error names the Rust target and exact expected Clang driver below toolchains/llvm/prebuilt/<host>/bin/. Install a complete NDK, check ANDROID_NDK_HOME, and rerun cargo ferry doctor.
Rust target missing
Install the target corresponding to each configured ABI, for example:
rustup target add aarch64-linux-android
Generated bridge does not compile
The runtime bridge is mandatory and DexPolicy::None is rejected. A javac failure points to the full generated source and log below target/ferry/android/<profile>/. Confirm that JAVA_HOME selects a complete JDK and that the selected SDK platform contains android.jar.
APK validation reports a missing component class
RustFerry checks FerryActivity and every enabled receiver/provider in the merged DEX. Do not disable hasCode or remove the component. Inspect javac.log and d8.log; stale or incomplete content-addressed outputs are rebuilt automatically.
D8 reports duplicate classes
Identical DEX files emitted once per ABI are content-deduplicated automatically. A remaining duplicate usually means two different bridge dependencies define the same class. Remove one bridge input or align dependency versions; do not delete arbitrary classes from a DEX archive.
Debug keystore validation failed
The persistent key or password file may be damaged or mismatched. The error includes the machine-local path and keytool log. Restore the pair from backup. RustFerry will not overwrite an existing signing identity automatically.
Signature succeeds but alignment fails
The final verification is zipalign -c -P 16 4. Native libraries are injected uncompressed before alignment and signing. Inspect the logged zipalign and apksigner versions; no tool may modify the APK after signing.
APK validation rejects an ABI
RustFerry checks both the lib/<abi>/ path and ELF header. This catches a library copied into the wrong ABI directory. Verify android.abis, installed Rust targets, and the target-specific NDK linker environment in the Cargo log.
Device or adb missing
This does not affect cargo ferry build android, which stops after artifact validation and never discovers or mutates a device. Device inventory, install, and launch are separate explicit commands:
cargo ferry devices --platform android
cargo ferry install android --device SERIAL
cargo ferry run android --device SERIAL
The device selector may be omitted only when exactly one compatible target exists. These ADB deployment paths are implemented and covered by host-side tests, but this repository has no emulator or physical-device runtime validation; successful APK inspection alone does not prove install or launch behavior.
iOS setup
Local iOS builds require macOS, full Xcode, and the relevant Apple Rust target. A Linux or Windows client can request a remote physical-iPhone build without installing Xcode or an Apple SDK locally; the trusted macOS worker still requires Apple tooling. An Apple Account, provisioning profile, connected iPhone, booted Simulator, and installed Simulator runtime are not required for local Simulator build-only use.
Install Xcode, open it once so its components finish installing, then select it if necessary:
xcode-select -p
xcodebuild -version
xcrun --sdk iphonesimulator --show-sdk-path
rustup target add aarch64-apple-ios-sim
cargo ferry doctor
cargo ferry doctor is read-only. It reports:
- selected
DEVELOPER_DIR/Xcode; xcodebuild,xcrun, andplutil;- iPhone Simulator SDK path and version;
- Cargo, rustup, and
aarch64-apple-ios-sim; - installed CoreSimulator runtimes as an optional run-time check.
A missing runtime is a warning, not a build failure. Install a runtime from Xcode Settings > Platforms only when install or run support needs one.
For remote device archives and their separate signing/validation limits, see physical iPhone development.
Xcode selection
Discovery honors an explicit DEVELOPER_DIR, then xcode-select -p. It rejects Command Line Tools-only developer directories because they do not contain iPhoneSimulator.platform.
The tool never invokes sudo, changes the selected Xcode, accepts licenses, installs SDKs, or downloads executables. Apply any system-level fix yourself, then rerun cargo ferry doctor.
Security boundary
All generated Apple files remain under the application’s target/ferry/ios/ directory. Generated writes reject absolute paths, parent traversal, and symlinked output components. External programs receive argument arrays; project/config values are not interpolated into a shell command.
Build an iOS Simulator application
From a generated Rust project on macOS:
cargo ferry build ios --simulator
The build produces:
target/ferry/ios/debug/<binary>.app
Use --release for target/ferry/ios/release/<binary>.app. Building neither boots nor installs a Simulator.
Pipeline
- Validate
ferry.toml, Cargo selectors, versions, identifiers, paths, permissions, and extension settings. - Discover full Xcode and the
iphonesimulatorSDK throughxcrun. - Confirm
aarch64-apple-ios-simis installed. - Select
CompiledCatalogwhen discovery finds an available iOS runtime; otherwise select the explicitSdkOnlyResourcesfallback. Render the corresponding deterministicFerryHost.xcodeproj, scheme,Info.plist, resources, entitlements, and enabled extension sources belowtarget/ferry/ios/generated/. - Run Cargo for
aarch64-apple-ios-simwith an isolatedtarget/ferry/ios/cargo/target directory. - Stage Cargo’s executable under its validated binary name for Xcode’s argument-free Copy Files phase.
- Run
xcodebuild -target FerryApp -sdk iphonesimulatorwith deterministic ad-hoc signing. Target-based SDK selection avoids booting or selecting a Simulator device. RustFerry requires an available runtime before selecting catalog compilation; the SDK-only fallback avoids that tool step. - Xcode creates the bundle, processes
Info.plist, compiles or copies the selected asset representation, builds and signs nested dependencies, installs the prebuilt Rust executable at$TARGET_BUILD_DIR/$EXECUTABLE_PATH, and signs the application last. - When WidgetKit is enabled, ad-hoc re-sign the widget with
Widget.entitlements, then the application withApp.entitlements. Non-widget builds skip this step; signing never uses--deep. - Independently inspect the app and fail unless every invariant matches.
This follows Slint’s iOS architecture: the application is a Rust executable built for the simulator, using Slint’s Winit backend and Skia renderer, placed at Xcode’s expected executable path. The generated Xcode project contains packaging metadata and minimal platform extension code, not user business logic.
Asset packaging is explicit in the build plan and validation report:
CompiledCataloggeneratesAssets.xcassets, configuresAppIconandFerryLaunch, and requiresAssets.carplus the matching processed plist values. This mode is selected only when an available iOS Simulator runtime is reported.SdkOnlyResourcespreserves SDK-only builds on hosts with no runtime. It copiesFerryIcon.pngandFerrySplash.png, records their plist references, byte-compares them with the validated project inputs, and rejects a strayAssets.car.
The two modes use separate Xcode intermediate directories, so switching runtime availability cannot reuse stale catalog output.
Validation evidence
A successful result records and checks:
.appis a real directory, not a symlink;Info.plistpassesplutil -lint;- exact
CFBundleIdentifier,CFBundleExecutable, andCFBundlePackageType=APPL; - executable exists, is non-empty, and has executable permission bits;
xcrun lipo -archsreturns exactlyarm64;- the pre-sign embedded executable matches Cargo’s output byte-for-byte and the signed executable retains its Mach-O UUID;
FerryResources.jsonexists;- asset evidence matches the selected mode: either
Assets.carwithAppIcon/FerryLaunch, or exactFerryIcon.png/FerrySplash.pngbytes with the SDK-only plist keys; - each top-level framework/library is inspected;
- expected
.appexcount, identifiers, executable names, extension point, and architectures match. - app, runtime framework, and each
.appexhave sealed plists/resources, exact signature identifiers, and strict-valid ad-hoc signatures; - the application passes
codesign --verify --deep --strict; - application and widget signatures contain exactly the configured application group when enabled.
Command logs live below target/ferry/ios/logs/<profile>/. Logged argv/environment values use the plan’s redaction metadata.
Dry run
The Apple build API exposes a stable schema-versioned plan containing generated paths, Cargo/Xcode argument arrays, environment overrides, the selected asset packaging mode, the executable staging copy, and expected artifact path. A dry run returns this plan with no artifact or validation claim and performs no generation/build after read-only discovery.
Developer Experience 0.2 asset evidence
On the current Xcode 26.6 host, simctl reports no installed Simulator runtimes. The real smoke build therefore selected SdkOnlyResources and produced an arm64 .app. Independent inspection confirmed exact icon/splash bytes, bundle metadata, the Cargo Mach-O UUID, the generated runtime framework, and strict/deep ad-hoc signatures. The CompiledCatalog project/catalog/cache path is implemented and tested, but a complete Assets.car application artifact was not produced in this environment.
Verified environment
At commit 8ed0192, Platform artifacts run 30719811812 built and validated RustFerry-named arm64 Starter and Kitchen Sink .app bundles. FerryRuntimeBridge.framework, its required exports and application hook, WidgetKit and ActivityKit .appex products, Activity framework linkage, signatures, sealed resources, and application-group entitlements passed inspection. Before the rename, the equivalent pipeline also built and validated arm64 base, Slint 1.17.1, and extension-bearing .app artifacts with Xcode 26.6 and the iPhoneSimulator 26.5 SDK on 2026-08-01. Neither validation used a Simulator runtime or device, so install, launch, callbacks, UI, and other runtime interaction remain unvalidated.
iOS signing
iOS Simulator builds use Xcode’s local ad-hoc identity (-). Xcode signs the framework and
extensions before sealing the application. When WidgetKit is enabled, the pipeline then re-signs
only the widget and containing application, inside-out and without --deep, so their generated
application-group entitlements are embedded in the effective signatures. Artifact validation
requires exact signature identifiers, sealed plists/resources, strict verification for every
bundle, and recursive strict verification for the application. No Apple Account, team, certificate,
or provisioning profile is needed.
Local physical-device signing
The local path uses the official Xcode development pipeline with an explicit user-selected Team. Provisioning updates are disabled unless requested, and manual signing can name a profile. After Xcode builds, RustFerry checks the expected executable and Cargo provenance, arm64 architecture, signatures, signing certificate, embedded profiles, expiration, Team and bundle identifiers, entitlement authorization, and embedded extensions before returning a validated artifact.
The implementation and deterministic tests do not establish a real signing or device result. The local environment had no Apple Development identity, Team, provisioning profile, signed physical artifact, or attached device. Widget application groups and other entitlements can require additional profile capabilities.
Remote manual-development signing
Manual-development signing is implemented for the GitHub remote provider. The source repository
must be public. Signing runs in a distinct private execution repository through protected Environment
rustferry-goal3-signing, with a required reviewer and exactly the
rustferry/goal3/builds/* deployment policy.
Configure the unsigned remote provider first, then validate the signing assets without mutation:
cargo ferry signing setup manual \
--certificate /private/signing/development.p12 \
--profile weather=/private/signing/application.mobileprovision \
--profile FerryWidgetExtension=/private/signing/widget.mobileprovision \
--profile FerryLiveActivityExtension=/private/signing/live-activity.mobileprovision \
--remote github \
--device-sha256 <lowercase-sha256> \
--dry-run
The files must remain outside every Git repository. Manual setup accepts at most three application
and extension profiles. Projects with extensions require one exact, case-sensitive
--profile TARGET=PATH for every generated target; use the preview’s target names. The legacy
unkeyed --profile PATH form remains valid only for an extension-free single application, and the
two forms cannot be mixed. All profiles must contain one common selected device. Omit
--device-sha256 only when every profile contains the same single device.
The example uses the interactive no-echo prompt. Other password sources are --password-stdin,
--password-env <NAME>, or --password-credential <ENTRY>. Select one. No password value is accepted
on the command line. JSON, non-interactive, and stdin-password mutation require --yes; otherwise
the command asks for confirmation after printing public certificate, profile, team, device-hash, and
target metadata.
The protected Environment must contain no secrets before initial setup. RustFerry revalidates the
retained asset bytes immediately before upload, then sends the PKCS#12 and profiles as canonical
padded base64 and the password as raw UTF-8, with a 48 KiB limit per final value. The application
keeps the legacy profile secret name; each extension receives a deterministic static secret derived
from its target name and bundle identifier. RustFerry verifies the exact planned three-to-five-name
set remotely before persisting the private local signing config. Multi-profile jobs use the bounded
RFSIGNV2 stdin frame; the legacy frame remains single-application-only. See
GitHub macOS provider security for the full preflight, failure, and
cleanup contract.
The multi-profile setup and transport pass the affected-package integration suite locally. A real
Apple Development certificate/profile upload and signed IPA acceptance run remain pending. No
signing identity, private key, password, profile contents, or account token is stored in
ferry.toml, public workflow files, or generated logs. See Physical iPhone development
and STATUS for the current evidence level.
Physical iPhone development
RustFerry has three physical-device build paths. A local Mac can use the official Xcode development pipeline. A machine without Xcode can submit an exact source revision to a trusted GitHub-hosted macOS worker. A named SSH endpoint can instead receive a deterministic source snapshot and return an unsigned XCArchive. Downloaded bytes are trusted only after independent client validation.
Local Mac development build
The local path cross-compiles the Rust executable for aarch64-apple-ios, generates the hidden
Xcode host below target/ferry/ios-device/, asks Xcode to development-sign it for an explicit Team,
then checks the app, embedded extensions, signatures, profiles, entitlements, Team ID, bundle IDs,
and arm64 architecture.
List usable identities:
cargo ferry signing teams
Build without changing provisioning assets:
cargo ferry build ios --device --team ABCDE12345
Permit Xcode account/profile updates only when intended:
cargo ferry build ios --device --team ABCDE12345 --allow-provisioning-updates
Manual signing accepts --provisioning-profile NAME_OR_UUID. No password, private key, profile contents, or account token belongs in ferry.toml or CLI output.
Install and run use an exact CoreDevice identifier from cargo ferry devices --platform ios:
cargo ferry install ios --device DEVICE_ID --team ABCDE12345
cargo ferry run ios --device DEVICE_ID --team ABCDE12345
An unsigned or ad-hoc Simulator bundle is never accepted for a physical device. Provisioning mutation is off by default, no device is needed for build, and no signing bypass exists.
Implementation and deterministic signing-plan tests are complete, but the local environment has not produced, installed, or launched a physical-device artifact: no Apple Development identity, Team, profile, or attached device was available.
GitHub build without local Xcode
After GitHub remote setup, request an unsigned diagnostic build:
cargo ferry build iphone --remote github --unsigned
The client publishes only an operation-scoped request, waits for the trusted macOS worker, downloads
the result, rehashes and independently inspects it, then atomically writes
target/ferry/ios/device/<profile>/<product>-unsigned.xcarchive.zip. A Linux acceptance run produced
a real unsigned physical-iPhone archive and validated the automatic download end to end. This is
compile and unsigned-artifact evidence, not installability or device-runtime evidence.
Manual Apple Development signing setup accepts one exact profile for the application and each
enabled Widget or Live Activity extension, up to three profiles. Extension-bearing projects use
repeatable --profile TARGET=PATH; the legacy unkeyed path remains available only for a
single-application project. Configure the assets as described in iOS signing, then
request a signed build:
cargo ferry build iphone --remote github --team <TEAMID>
The signed path is designed to return a development IPA, artifact manifest, validation report, and
sanitized log below target/ferry/ios/device/<profile>/. App/Widget/Live Activity profile mapping
and protected secret transport pass local integration tests. Real certificate/profile upload,
signed IPA export, and independent signed-artifact acceptance have not run because the required
Apple assets and distinct private execution repository are not configured.
Named SSH Mac
Add a dedicated Mac with an independently obtained host key and explicit endpoint name, then run:
cargo ferry remote add ssh-mac production-mac \
--host build.example.com \
--user ferry \
--known-hosts /absolute/path/rustferry.known_hosts \
--host-key-sha256 SHA256:BASE64_WITHOUT_PADDING
cargo ferry remote doctor production-mac
cargo ferry build iphone --remote production-mac --unsigned
SSH snapshot session v1 is explicit and unsigned-only. It returns
target/ferry/ios/device/<profile>/<product>-unsigned.xcarchive.zip; it does not sign, export an
IPA, install, launch, or collect device logs. Its protocol/process/worker coverage is deterministic
and local: no live SSH Mac compile or SSH-produced artifact has been validated. See
SSH Mac provider.
Local devicectl install/launch services are implemented. Installing or launching a downloaded remote artifact has not been accepted, and no physical-device runtime behavior has been observed. See STATUS for the exact evidence level.
WidgetKit and ActivityKit extensions
Enabled Apple extensions are generated as separate Swift/Xcode application-extension targets, built as target dependencies, embedded under <app>.app/PlugIns/, and independently validated. Users do not create or edit Swift or Xcode files.
WidgetKit
Configuration requires an application group:
[extensions.widget]
enabled = true
app_group = "group.com.example.weather"
Generation adds:
FerryWidgetExtensionXcode target;FerryWidgetExtension.appexproduct;- WidgetKit/SwiftUI timeline provider and a small-system-family view;
- main-app and extension app-group entitlements;
- extension identifier
<app identifier>.widget; - embed-target dependency and
Embed App Extensionsphase.
The Rust widgets::update path validates and writes the serialized snapshot plus title, value, caption, progress, deep link, and constrained action data to the configured app-group UserDefaults suite, then requests a WidgetKit timeline reload. The generated provider reads that snapshot and renders the supported fields. The current Ferry* publisher, framework, provider, and embedded extension compiled and passed artifact inspection in Platform artifacts run 30719811812, including exact application-group entitlements. Their behavior has not been observed in a running Simulator.
ActivityKit and Dynamic Island
[ios]
min_version = "16.1"
[extensions.live_activity]
enabled = true
android_fallback = "ongoing-notification"
Generation adds:
FerryLiveActivityExtensionXcode target;FerryLiveActivityExtension.appexproduct;ActivityAttributescontent state;- Lock Screen and expanded/compact/minimal Dynamic Island presentations;
- main-app
NSSupportsLiveActivitiesmetadata; - extension identifier
<app identifier>.liveactivity; - embed-target dependency and
Embed App Extensionsphase.
The Rust start, update, end, and list_active paths call the generated ActivityKit application bridge. The current Ferry* main-app framework and presentation extension compiled, linked, embedded, and passed artifact inspection in Platform artifacts run 30719811812. No ActivityKit session has been started in a running Simulator or device, so this is not runtime validation. Push-based updates remain unavailable.
Artifact validation
For each enabled extension, the build requires:
- expected
.appexbeneathPlugIns/; - valid plist and exact bundle identifier;
CFBundleExecutablematching a non-empty executable;NSExtensionPointIdentifier=com.apple.widgetkit-extension;- exact arm64 Simulator Mach-O architecture;
- sealed plist/resources, exact signature identifier, and strict-valid ad-hoc signature;
- exact configured application-group entitlement on the widget signature;
- no unexpected extra
.appexbundles.
At commit 8ed0192, Platform artifacts run 30719811812 built a RustFerry-named Kitchen Sink app embedding FerryWidgetExtension.appex and FerryLiveActivityExtension.appex. Both arm64 products passed identifier, plist, extension-point, resource-sealing, and strict ad-hoc signature checks; the ActivityKit product also passed exact runtime-framework linkage inspection, and the widget and containing app carried the exact configured application-group entitlement. Before the rename, the equivalent legacy-named targets, standalone .appex products, and combined app were also built and validated with Xcode 26.6/iPhoneSimulator 26.5 without a Simulator runtime.
Physical-device signing status
Simulator builds use local ad-hoc signing and require no team. Widget builds re-sign the widget and then the containing app with their generated application-group entitlements; non-widget builds retain Xcode’s signatures unchanged.
The physical-development flow is implemented with explicit Team selection, Apple Development identity/profile resolution, generated entitlements, recursive signature/profile/entitlement inspection, and devicectl install/launch services. Remote manual setup accepts one exact profile for the application and every enabled extension, up to three profiles, and requires a common registered device. Extension-bearing device builds must preserve the configured application-group capability in the app and widget profiles. The multi-profile continuation passes local integration tests. This environment had no identity, Team, provisioning profile, signed device artifact, or attached iPhone, so physical signing, installation, launch, and extension behavior remain unvalidated.
Apple implementation status
Last validated: 2026-08-01
Current RustFerry-named Apple artifact evidence comes from Platform artifacts run 30719811812 at commit 8ed0192. Exact legacy paths, identifiers, symbols, and bundle names remain below as a separate historical record.
| Area | Status | Evidence |
|---|---|---|
| Xcode/xcrun/SDK discovery | Implemented and host-tested | Xcode 26.6, iPhoneSimulator 26.5 |
| Rust target discovery | Implemented and host-tested | aarch64-apple-ios-sim |
| Doctor | Implemented | Build prerequisites separate from optional runtime availability |
| Deterministic Xcode/plist/assets | Implemented; both asset modes host-tested | Compiled-catalog project tests plus a real SDK-only Xcode build |
Starter Simulator .app | Current artifact validation | Public-CLI arm64 .app in Platform run 30719811812, plus the Developer Experience 0.2 SDK-only smoke app |
Kitchen Sink Simulator .app | Current artifact validation | Public-CLI arm64 .app with two embedded extensions in the same run |
| Runtime bridge | Implemented; current target compile/artifact inspection | arm64 FerryRuntimeBridge.framework; required exports/application hook and strict ad-hoc signature validated |
| WidgetKit | Publisher and renderer implemented; current compile/embed/artifact inspection | Runtime app-group writer plus signed PlugIns/FerryWidgetExtension.appex |
| ActivityKit | Start/update/end/list and presentation implemented; current compile/embed/artifact inspection | Runtime framework plus signed PlugIns/FerryLiveActivityExtension.appex |
| Simulator install/launch/UI | CLI implemented; runtime unvalidated | Typed simctl install/launch services exist; no CoreSimulator runtime/device is installed |
| Physical-device signing/install | Implemented; signing and device unvalidated | Side-effect-free official-tool planning passes without Xcode; no identity, Team, profile, signed artifact, or device was available |
Platform artifacts run 30719811812 generated default Starter and Kitchen Sink projects with the public CLI. Both RustFerry-named application bundles have arm64 executables and passed plist/resource inspection plus deep/strict ad-hoc signature verification. The run also checked FerryRuntimeBridge.framework, its required exports and application hook, both embedded .appex products, exact identifiers and framework linkage, and the exact group.org.rustferry.ciextensions application-group entitlement on the Kitchen Sink app and widget.
No application was installed or launched; these checks do not establish Simulator or device runtime behavior.
Developer Experience 0.2 selects one of two explicit asset modes. With an available iOS Simulator runtime, CompiledCatalog emits Assets.xcassets, selects AppIcon and FerryLaunch, and requires a compiled Assets.car during artifact validation. That path is implemented and covered by deterministic project, catalog, cache, and plist tests, but it was not artifact-validated on this host because simctl reports no installed runtimes. Without a runtime, SdkOnlyResources emits FerryIcon.png and FerrySplash.png; a real Xcode 26.6 build produced a signed arm64 .app, and inspection verified the exact source bytes, plist references, Mach-O identity, resources, and strict/deep ad-hoc signature. The SDK-only report does not claim an Assets.car.
Historical pre-rename evidence
The historical combined extension artifact is:
target/final-acceptance-kitchen/target/pocket/ios/debug/final-acceptance-kitchen.app
Its app executable and both embedded extension executables are arm64. Validated identifiers:
org.cargopocket.kitchensink
org.cargopocket.kitchensink.widget
org.cargopocket.kitchensink.liveactivity
org.cargo-pocket.runtime-bridge
The pre-rename public CLI’s schema-5 artifact report accepted both application bundles only after exact identifier, arm64 Mach-O, sealed Info.plist and resources, ad-hoc signature, and strict/deep signature checks passed. The Kitchen Sink app and widget signatures contain the exact group.org.cargopocket.kitchensink application-group entitlement. Both extensions report com.apple.widgetkit-extension, have strict ad-hoc signatures with their exact identifiers, and are present beneath the app’s PlugIns/ directory.
The Activity extension links @rpath/PocketRuntimeBridge.framework/PocketRuntimeBridge; the strictly verified, ad-hoc-signed framework exports _pocket_bridge_call, _pocket_bridge_free, _pocket_bridge_init, _pocket_bridge_install, and _pocket_bridge_with_application. Artifact validation also required the PocketApplicationDelegate and exact application-initializer hook markers.
Runtime limitations
- Widget publication/reload and ActivityKit start/update/end/list are implemented and compiled, but none was invoked in a running Simulator application.
- Simulator install, run, and log commands are implemented, but this host has no installed runtime/device. No lifecycle callback, deep-link open, notification UI/action, permission prompt, widget timeline, or Live Activity session was observed.
- Asset-catalog compilation is selected only when discovery finds an available iOS runtime. The runtime-free mode keeps SDK-only builds working with exact, sealed PNG resources and reports that fallback explicitly.
- Physical development signing, install, and launch paths are implemented but were not exercised without an Apple Development identity, Team, provisioning profile, signed device artifact, or attached iPhone.
These limitations do not weaken the current or historical compile/link/artifact evidence, but they prevent any simulator-runtime or device-validation claim.
iOS troubleshooting
Full Xcode was not found
/Library/Developer/CommandLineTools is not enough. Install Xcode, then verify:
xcode-select -p
xcodebuild -version
xcrun --sdk iphonesimulator --show-sdk-path
If several Xcode versions are installed, set DEVELOPER_DIR for the command or select the intended version yourself. cargo-ferry never runs sudo xcode-select.
Rust target is missing
rustup target add aarch64-apple-ios-sim
cargo ferry doctor
No Simulator runtime
This does not block cargo ferry build ios --simulator. The build uses the installed SDK without a destination. A runtime/device is required only to install or launch the result.
Cargo failed before Xcode
Inspect:
target/ferry/ios/logs/debug/01-cargo-build.log
Confirm the requested binary exists in Cargo.toml, features are spelled correctly, Slint enables backend-winit and renderer-skia, and the project compiles for aarch64-apple-ios-sim.
Xcode failed
Inspect:
target/ferry/ios/logs/debug/02-xcodebuild.log
The generated project is disposable. Do not repair it manually; fix ferry.toml or the generator and rebuild. All generated paths remain below target/ferry/ios/.
Bundle validation failed
Validation errors name the exact failed invariant: plist key/value, missing executable/resource, architecture, Cargo-binary mismatch, framework entry, extension count, extension point, or bundle identifier. A non-zero result means the .app must not be distributed as a successful artifact.
Widget shows fallback values
Call widgets::update after the iOS runtime is installed, then verify the configured app-group identifier is identical for the app and extension. The bridge writes rustferry.widget.snapshot and requests a timeline reload, but WidgetKit controls refresh timing. Fallback values mean no readable snapshot reached that app-group suite; inspect the typed Rust error before treating it as a presentation bug.
Live Activity cannot start
The ActivityKit bridge is compiled and linked. Check that Live Activity is enabled, the deployment target is iOS 16.1 or newer, the runtime was installed before the call, and live_activity::is_supported() is true. Surface the typed start error: OS availability or user settings can reject a request even when the .appex is structurally valid. Runtime behavior has not yet been observed in this project’s Simulator environment.
State and events
What it does
Store<T> persists serializable state. app_events delivers typed lifecycle, deep-link, notification, network, theme, and window events until the returned subscription is dropped.
Support matrix
| Host test runtime | Android | iOS |
|---|---|---|
| State/event model implemented and tested | File host/event bridge implemented and compiled; runtime unobserved | File host/delegate bridge and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::app_events::{self, AppEvent};
use rustferry::storage::Store;
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let count = Store::<u32>::open("count")?;
count.save(&41)?;
count.save(&(count.load()?.unwrap_or_default() + 1))?;
let latest = Arc::new(Mutex::new(None));
let observed = Arc::clone(&latest);
let _subscription = app_events::subscribe(move |event| {
*observed.lock().unwrap() = Some(event);
});
runtime.send_event(AppEvent::Foregrounded);
assert_eq!(count.load()?, Some(42));
assert_eq!(*latest.lock().unwrap(), Some(AppEvent::Foregrounded));
Ok(())
}
Keep the subscription in application state. Dropping it ends delivery.
Configuration
[capabilities.storage]
enabled = true
Events themselves need no blanket capability; payload-producing features are generated only when enabled.
Permissions and entitlements
Storage and basic lifecycle events need no runtime prompt. Specific event sources can require notification, network, deep-link, or extension configuration.
Expected result
The count reloads as 42; the injected foreground event reaches the callback exactly while its subscription is alive.
Common errors
Unsupported(Storage): no storage backend is installed or the capability is disabled.- Lost callbacks: the
Subscriptionwas assigned to_and immediately dropped. - Missing final event: mobile operating systems do not guarantee termination delivery.
Platform differences
One source preserves serial delivery; concurrent sources may interleave. Persist important state eagerly on both platforms.
Test example
Use TestRuntime::send_event, then drop the subscription and inject another event to assert no later callback starts.
Example project
See the persistent state and event subscription in the Counter example, plus Project structure.
Lifecycle
What it does
AppEvent represents startup, foreground/background, resume/pause, low-memory, optional termination, and related platform events. use_app_events is an alias suited to UI ownership.
Support matrix
| Host model | Android delivery | iOS delivery |
|---|---|---|
| Implemented/tested | Lifecycle callbacks compiled into inspected bridge artifact; runtime unobserved | Delegate callbacks and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::app_events::{self, AppEvent};
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let events = Arc::new(Mutex::new(Vec::new()));
let observed = Arc::clone(&events);
let _subscription = app_events::use_app_events(move |event| {
observed.lock().unwrap().push(event);
});
runtime.send_event(AppEvent::Started);
runtime.send_event(AppEvent::Backgrounded);
assert_eq!(events.lock().unwrap().len(), 2);
}
Configuration
No ferry.toml section is required for core lifecycle events.
Permissions and entitlements
None for lifecycle delivery. Payload-specific events may need their own capability.
Expected result
The test observes Started then Backgrounded in source order.
Common errors
- Callback never runs: the subscription was dropped.
- Assuming
Terminating: the OS may kill an application without announcing it. - Updating UI directly from an arbitrary callback thread: dispatch through the UI backend; the starter uses
slint::invoke_from_event_loop.
Platform differences
Exact native callbacks mapped to foreground/resume and background/pause differ. Treat cross-platform events as semantic states, not one-to-one native callback names.
Test example
TestRuntime::send_event(AppEvent::LowMemory) exercises cleanup logic deterministically; no sleeps are needed.
Example project
The Counter example owns a lifecycle subscription until its window closes.
Async tasks
What it does
rustferry::spawn runs an independent Send + 'static future on a worker thread and propagates the active RustFerry runtime. Slint-local futures can instead use slint::spawn_local and return UI changes through its event loop.
Support matrix
| Host | Android | iOS |
|---|---|---|
| Worker helper tested | Rust behavior available when host is linked | Rust behavior available when host is linked |
Minimal complete example
use rustferry::network::{self, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Wifi));
let task = rustferry::spawn(async { network::is_online() });
assert!(task.join().expect("worker did not panic")?);
Ok(())
}
Configuration
Configure only the capability used by the task; the spawn helper itself has no section.
Permissions and entitlements
Determined by the called capability. Never use a background task to bypass a platform permission or UI-thread requirement.
Expected result
The worker sees the same scoped test runtime and returns true.
Common errors
- Borrowed data does not live long enough: move owned values into the future.
- UI handle is not
Send: useslint::spawn_localor send a result back withslint::invoke_from_event_loop. - Worker panic: inspect the
JoinHandle; do not convert it to success.
Platform differences
rustferry::spawn is a small thread-based helper, not a mobile background-execution service. OS background execution limits still apply.
Test example
Inject network/permission/probe behavior into TestRuntime, spawn the async operation, join it, and inspect recorded calls.
Example project
The Notifications example uses a Slint-local future for its permission request.
Network status
What it does
network::current, subscribe, and use_network_status expose the operating system’s path state. This does not prove internet or backend reachability; use probe separately.
Support matrix
| Host model/mock | Android path backend | iOS path backend |
|---|---|---|
| Implemented/tested, including debouncing | Enabled Connectivity backend and bridge artifact-inspected; runtime unobserved | NWPath backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::network::{self, NetworkState, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let monitor = network::use_network_status()?;
assert_eq!(monitor.current().state, NetworkState::Offline);
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Wifi));
assert_eq!(monitor.current().state, NetworkState::Online);
Ok(())
}
Configuration
[capabilities.network]
mode = "status"
probe_timeout_ms = 3000
Permissions and entitlements
Generated Android permissions depend on mode; none should omit network permissions where possible. iOS path monitoring does not authorize local-network discovery. Check platform docs before enabling LocalNetwork.
Expected result
The monitor updates from Offline to Online; duplicate equal statuses are debounced.
Common errors
- Treating
Onlineas backend health: perform an explicit probe. - Dropping a subscription: retain it or use
NetworkMonitor. Unsupported(NetworkStatus): capability/backend is absent.
Platform differences
Transport, expensive, and constrained fields can be unknown. VPN classification and path timing differ by OS.
Test example
Use set_network_status; assert a duplicate call returns false and produces no second event.
Example project
See the live path subscription in the Network Guard example.
Require internet
What it does
network::require_online gates one operation on reported path state. network::probe independently checks an application-supplied HTTP(S) endpoint with a timeout on a worker.
Support matrix
| Host mock | Android probe | iOS probe |
|---|---|---|
| Gate/probe semantics tested | Enabled HTTP probe backend and bridge artifact-inspected; runtime unobserved | URLSession probe backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::network::{self, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
use std::time::Duration;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Wifi));
runtime.set_probe_result(false, Some(503), Duration::from_millis(12));
network::require_online()?;
let result = rustferry::spawn(async {
network::probe("https://example.test/health", Duration::from_secs(1)).await
})
.join()
.expect("probe worker did not panic")?;
assert!(!result.reachable);
assert_eq!(result.status_code, Some(503));
Ok(())
}
Configuration
[capabilities.network]
mode = "required"
probe_url = "https://api.example.com/health"
probe_timeout_ms = 3000
The build does not probe this URL. Application code decides when to probe.
Permissions and entitlements
Android needs INTERNET for HTTP(S); status inspection can also need network-state access. Apple transport-security policy applies to insecure endpoints. Prefer HTTPS.
Expected result
Path gating succeeds while the endpoint probe reports a distinct 503 failure.
Common errors
file:or another scheme: only HTTP(S) probes are accepted.- Zero timeout: rejected before backend dispatch.
- Blocking the UI thread with
wait_until_online: use it only on a worker/test thread.
Platform differences
OS reachability and HTTP behavior remain separate everywhere. Captive portals, VPNs, DNS, and proxy policy can produce different results.
Test example
Configure set_probe_result, call probe, and inspect probe_requests() to assert URL and timeout without network traffic.
Example project
See the guarded action and independent retry in the Network Guard example, plus Network status.
Local notifications
What it does
The notification API queries/requests authorization, shows immediately, schedules, cancels, and lists pending/delivered local notifications. Permission is requested only when application code calls it.
Support matrix
| Host model/mock | Android bridge/artifact | iOS bridge/artifact |
|---|---|---|
| Full request lifecycle tested | Enabled backend/receiver artifact-inspected; runtime unobserved | UserNotifications backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::notifications::{self, Notification, PermissionStatus, UnixTimestamp};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_time(1_000);
runtime.set_notification_permission(
PermissionStatus::NotDetermined,
PermissionStatus::Granted,
);
let status = rustferry::spawn(async { notifications::request_permission().await })
.join()
.expect("permission worker did not panic")?;
assert_eq!(status, PermissionStatus::Granted);
let request = Notification::new("tea", "Tea", "Your timer finished")
.scheduled_at(UnixTimestamp(2_000));
notifications::schedule(request)?;
assert_eq!(notifications::pending()?.len(), 1);
Ok(())
}
Configuration
[capabilities.notifications]
local = true
push = false
Or run cargo ferry add notifications.
Permissions and entitlements
Request authorization from a user-initiated UI action. Android 13+ may require POST_NOTIFICATIONS; iOS uses UserNotifications authorization. Remote push credentials/entitlements are not part of local notification support.
Expected result
The test grants authorization and records one future request. On a validated platform backend, the OS owns actual delivery timing.
Common errors
- Scheduling without
scheduled_at: rejected. - Empty ID or empty title and body: rejected before the backend.
- Assuming exact delivery time: both operating systems may defer delivery.
push = true: schema version 1 rejects remote push.
Platform differences
Android channels are explicit and newer Android versions have a runtime permission. iOS authorization states and delivered-list semantics follow UserNotifications. Repeating minimums differ.
Test example
Use set_notification_permission, set_time, scheduled_notifications, and delivered_notifications; no OS prompt is shown.
Example project
See the complete local flow in the Notifications example.
Notification actions
What it does
Actions add stable buttons to a local notification. When the user opens a notification or chooses an action, RustFerry delivers AppEvent::NotificationOpened and the filtered on_notification_opened callback.
Support matrix
| Host event/model | Android action bridge | iOS action bridge |
|---|---|---|
| Implemented/tested | Enabled action/open receiver artifact-inspected; runtime unobserved | Action/open delegate and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::app_events;
use rustferry::notifications::{Notification, NotificationAction, NotificationId};
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let opened = Arc::new(Mutex::new(None));
let observed = Arc::clone(&opened);
let _subscription = app_events::on_notification_opened(
move |id, action, _payload, _link| {
*observed.lock().unwrap() = Some((id, action));
},
);
let _request = Notification::new("message", "Message", "Reply?").action(
NotificationAction {
id: "reply".into(),
title: "Reply".into(),
foreground: true,
authentication_required: false,
},
);
runtime.open_notification(
NotificationId::parse("message")?,
Some("reply".into()),
None,
None,
);
assert_eq!(opened.lock().unwrap().as_ref().unwrap().1.as_deref(), Some("reply"));
Ok(())
}
Configuration
[capabilities.notifications]
local = true
push = false
Permissions and entitlements
Same authorization as local notifications. An authentication-required action asks the OS to enforce device authentication; it is not application authorization.
Expected result
The filtered callback receives notification ID message and action ID reply.
Common errors
- Empty action ID/title: rejected when dispatching the notification.
- Subscription dropped before open.
- Treating payload/action input as trusted authorization: validate routes and ownership in Rust.
Platform differences
Presentation, action count, foreground behavior, and authentication UI differ. The stable action ID is the cross-platform contract.
Test example
Call TestRuntime::open_notification with each action and assert routing without showing a notification.
Example project
See the Open action and typed callback in the Notifications example, plus Local notifications.
Remote push
What it does
Remote push transport is not implemented or stable. cargo-ferry has no public API for device-token registration, APNs or FCM receipt, background delivery, or provider-side sending. The supported notification API is limited to local notifications.
Application-owned payload types can still be designed and tested in Rust. That keeps message routing separate from a future platform transport without implying that a device can receive the message today.
Support matrix
| Boundary | Status |
|---|---|
| Application payload model and routing | Available as ordinary Rust code |
| Local notification scheduling and display | Implemented; see Local notifications |
| Apple Push Notification service (APNs) registration and receipt | Not implemented |
| Firebase Cloud Messaging (FCM) registration and receipt | Not implemented |
| Provider/server delivery | Not implemented; no built-in sender or server component |
| Live Activity remote updates | Not implemented |
Minimal complete example
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
struct RemotePayload {
route: String,
item_id: u64,
}
fn destination(payload: &RemotePayload) -> String {
format!("{}/{}", payload.route.trim_end_matches('/'), payload.item_id)
}
fn main() {
let payload = RemotePayload {
route: "/orders".to_owned(),
item_id: 42,
};
assert_eq!(destination(&payload), "/orders/42");
}
This example handles an in-memory application value. It does not register a device, receive a remote notification, verify provider input, or contact a server.
Configuration
Schema version 1 accepts only local notification support:
[capabilities.notifications]
local = true
push = false
push = true is rejected. No current ferry.toml setting enables remote push.
Permissions and entitlements
A future Apple transport would need the appropriate aps-environment entitlement, provisioning, device-token handling, and APNs authentication on a server. A future Android transport would need FCM client configuration and generated service components; displaying notifications may also require POST_NOTIFICATIONS on applicable Android versions.
Provider credentials, APNs signing keys, and FCM server credentials belong in a protected server environment, never in ferry.toml, generated client source, or an application repository.
Expected result
The example constructs and routes one payload in memory. It produces no device token, operating-system registration, network request, notification, or background wake-up.
Common errors
- Enabling
push = true; schema version 1 rejects it. - Treating
notifications::show_nowornotifications::scheduleas remote delivery. - Shipping APNs or FCM provider credentials in the client application.
- Calling host-side payload tests proof of APNs, FCM, background, or device behavior.
- Treating simulator behavior as physical-device delivery evidence.
Future extension boundaries
A future implementation should keep these surfaces separate:
- Device registration: platform token acquisition, refresh, revocation, and typed lifecycle events.
- Event ingress: OS-delivered data converted into a validated Rust payload across cold, warm, foreground, and background states.
- Provider delivery: an application-owned server component using APNs or FCM credentials; not a mobile runtime responsibility.
- Presentation: local display policy after receipt, distinct from transport success.
- Live Activity push: ActivityKit push-token acquisition and remote update/end delivery, distinct from the existing local start/update/end API.
None of these boundaries is a stable API commitment yet. A configuration or public API should be added only with implemented platform backends, generated artifacts, lifecycle tests, and real-device evidence.
Platform differences
APNs and FCM use different token formats, credential models, payload limits, delivery semantics, and background restrictions. Apple Live Activity push tokens and update payloads form a separate ActivityKit path; the current Live Activities API performs local lifecycle operations only.
Test example
Test payload validation and routing as pure Rust functions. A future transport also needs deterministic token/event adapter tests, generated-entitlement and service-component inspection, and physical-device delivery tests for cold, warm, foreground, and background states. Record each evidence level separately.
Example project
No remote-push example exists because no transport exists. The Notifications example covers local notifications, and the Live Score example covers locally initiated Live Activity updates; neither demonstrates remote delivery.
Storage
What it does
storage::{set,get,remove,contains,clear} stores serde values. Store<T> adds a named schema version and optional migration. It is ordinary local storage, not a database or secret vault.
Support matrix
| In-memory/file backend | Android host install | iOS host install |
|---|---|---|
| Atomic/corruption/migration tests | Application-private backend installed by Android host; target-compiled; runtime unobserved | Application Support backend and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::storage::Store;
use rustferry::testing::TestRuntime;
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
struct Settings {
count: u32,
}
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let settings = Store::<Settings>::open("settings")?;
settings.save(&Settings { count: 42 })?;
assert_eq!(settings.load()?, Some(Settings { count: 42 }));
Ok(())
}
Configuration
[capabilities.storage]
enabled = true
Or run cargo ferry add storage.
Permissions and entitlements
Application-private ordinary storage normally needs no prompt. Do not store passwords, tokens, signing keys, or private keys here; a secure-storage capability is separate future work.
Expected result
The typed value round-trips. File writes use a same-directory temporary record, sync, rename, and record checksum; corruption returns a typed error.
Common errors
MigrationRequired: stored/current versions differ without a migration hook.CorruptStorage: record/checksum/serde decoding failed; do not silently replace data.- Empty or overlong key: rejected.
Platform differences
Platform hosts choose the application-private directory. Backup/eviction behavior is platform policy and is not currently promised by the cross-platform API.
Test example
TestRuntime::storage() exposes the in-memory backend. FileStorage tests cover truncated records and migration persistence.
Example project
See eager typed persistence in the Counter example.
Haptics
What it does
haptics::impact, notification, and selection request semantic feedback and return a typed error if the active backend cannot perform it.
Support matrix
| Host mock | Android bridge | iOS bridge |
|---|---|---|
| Calls recorded/tested | Enabled backend and bridge artifact-inspected; runtime unobserved | Backend implemented; framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::haptics::{self, ImpactStyle};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
haptics::impact(ImpactStyle::Light)?;
haptics::selection()?;
assert_eq!(runtime.haptic_calls().len(), 2);
Ok(())
}
Configuration
[capabilities.haptics]
enabled = true
Or run cargo ferry add haptics.
Permissions and entitlements
No runtime permission is normally required. Device settings and hardware still control whether feedback is perceptible.
Expected result
The host test records Impact(Light) then Selection.
Common errors
Unsupported(Haptics): checkhaptics::is_supported()or handle the result.- Using haptics as the only feedback channel: always keep visible/accessibility feedback.
Platform differences
Intensity and waveform are semantic approximations; simulators may not produce physical feedback. Device observation is the meaningful validation level.
Test example
Assert the ordered runtime.haptic_calls() vector after application actions.
Example project
See the explicit haptic button in the Kitchen Sink example.
Clipboard
What it does
clipboard::read_text and write_text access text only. Read and write support are queried separately.
Support matrix
| Host mock | Android bridge | iOS bridge |
|---|---|---|
| Implemented/tested | Backend implemented; bridge compiled; runtime unobserved | Backend implemented; framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::{clipboard, testing::TestRuntime};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
clipboard::write_text("copied from Rust")?;
assert_eq!(clipboard::read_text()?.as_deref(), Some("copied from Rust"));
Ok(())
}
Configuration
[capabilities.clipboard]
enabled = true
Or run cargo ferry add clipboard; cargo ferry remove clipboard reverses the generated config and feature changes.
Permissions and entitlements
No declared runtime permission is modeled, but operating systems can show privacy UI or restrict background reads. Read only after clear user intent.
Expected result
The test backend returns the text just written.
Common errors
- Read and write support differ: check
can_read_textandcan_write_textseparately. - Assuming clipboard contents remain: another app or the OS may replace/expire them.
Platform differences
Privacy notifications, focus requirements, and paste consent evolve independently. Runtime/device validation is required before UX claims.
Test example
Write text, then inspect runtime.clipboard_text() without touching the host clipboard.
Example project
See the copy action in the Kitchen Sink example.
Sharing
What it does
share::text, share::url, and share::files request the native share sheet. They report unsupported/failed operations rather than pretending content was shared.
Support matrix
| Host mock | Android bridge | iOS bridge |
|---|---|---|
| Requests recorded/tested | Enabled file provider and bridge artifact-inspected; runtime UI unobserved | Backend and framework artifact-inspected; runtime UI unobserved |
Minimal complete example
use rustferry::{share, testing::TestRuntime};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
share::text("Forecast: clear")?;
share::url("https://example.com/forecast")?;
assert_eq!(runtime.share_requests().len(), 2);
Ok(())
}
Configuration
[capabilities.share]
enabled = true
Or run cargo ferry add share.
Permissions and entitlements
Text/URL sharing normally needs no prompt. File URLs still need to be readable by the app and safely exposed through platform sharing mechanisms.
Expected result
The host test records two share requests. A platform backend should present its system chooser; cancellation is not failure unless the platform reports it as such.
Common errors
- Invalid/nonabsolute URL: rejected.
- Empty file list: rejected.
- Assuming a recipient completed the share: opening the sheet is the API boundary.
Platform differences
Available recipients, previews, file grants, and cancellation callbacks differ.
Test example
Inspect TestRuntime::share_requests() and match the ShareRequest variants.
Example project
See the user-initiated share action in the Kitchen Sink example.
Deep links
What it does
DeepLink parses absolute URLs, DeepLinkPolicy applies explicit scheme/host/action allowlists, initial reads a cold-start link, and subscribe receives links while the runtime is alive.
Support matrix
| Host parser/event mock | Android intents/artifact | iOS schemes/artifact |
|---|---|---|
| Implemented/tested | Intent filter/allowlist bridge artifact-inspected; runtime unobserved | URL scheme/delegate allowlist and framework artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::deep_links::{self, DeepLink, DeepLinkPolicy};
use rustferry::testing::TestRuntime;
use std::sync::{Arc, Mutex};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let policy = DeepLinkPolicy::new()
.allow_scheme("weather")
.allow_host("forecast")
.allow_action("today");
let link = DeepLink::parse("weather://forecast/today")?;
policy.validate(&link)?;
let received = Arc::new(Mutex::new(None));
let observed = Arc::clone(&received);
let _subscription = deep_links::subscribe(move |link| {
*observed.lock().unwrap() = Some(link);
});
runtime.send_deep_link(link.clone());
assert_eq!(*received.lock().unwrap(), Some(link));
Ok(())
}
Configuration
[capabilities.deep_links]
schemes = ["weather"]
allowed_hosts = ["forecast"]
allowed_actions = ["today"]
Or run cargo ferry add deep-links for a generated scheme.
Permissions and entitlements
Custom schemes normally need generated manifest/plist declarations, not a runtime prompt. Universal/app links require domain association and are advanced configuration not claimed complete here.
Expected result
The allowlisted link reaches the running-app callback. set_initial_deep_link separately tests cold start.
Common errors
- Relative URL: an absolute scheme is required.
- Allowlist mismatch: reject before routing.
- Treating a deep link as authorization: re-check identity/ownership for every sensitive action.
Platform differences
Android uses intent filters; Apple uses URL types. Cold-start and already-running delivery enter through different native callbacks but converge on the typed Rust event.
Test example
Test denied hosts/actions and both set_initial_deep_link plus send_deep_link.
Example project
The Widget Counter example routes a widget action back into Rust.
Permissions
What it does
The unified API queries and explicitly requests notifications, network state, local network, photos, camera, microphone, and foreground location. Unsupported permission/platform pairs return Unsupported status or typed operation failure.
Support matrix
| Host mock | Android permission bridge | iOS permission bridge |
|---|---|---|
| Status/request/rationale tested | Exact enabled permissions/purpose strings and bridge artifact-inspected; runtime prompts unobserved | Supported permission backends and framework artifact-inspected; runtime prompts unobserved |
Minimal complete example
use rustferry::permissions::{self, Permission, PermissionStatus};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_permission(
Permission::Camera,
PermissionStatus::NotDetermined,
PermissionStatus::Granted,
);
assert_eq!(permissions::status(Permission::Camera)?, PermissionStatus::NotDetermined);
let status = rustferry::spawn(async {
permissions::request_with_rationale(
Permission::Camera,
Some("Scan a receipt after you tap Continue"),
)
.await
})
.join()
.expect("permission worker did not panic")?;
assert_eq!(status, PermissionStatus::Granted);
Ok(())
}
Configuration
Permission declarations and purpose strings are capability-specific. Do not add every permission preemptively. The current schema models implemented capability fields; camera/location feature APIs are not otherwise claimed complete.
Permissions and entitlements
The user chooses the request moment. Platform purpose strings must explain the real use before build. No bulk startup prompt, permission bypass, or synthetic granted result is acceptable.
Expected result
The test records a user-initiated request with rationale and returns the configured result.
Common errors
PermanentlyDenied: direct the user topermissions::open_settings(); do not loop prompts.Unsupported: hide/disable the feature or provide a real fallback.- Missing purpose string: platform build should fail before packaging.
Platform differences
Android/iOS distinguish denial/restriction and repeat requests differently. PermanentlyDenied is used only when meaningful; platform restrictions can remain Restricted.
Test example
Use set_permission_supported(false) for fallback UI and inspect permission_requests() for rationale and ordering.
Example project
The Notifications example requests authorization only after a button press.
Widgets
What it does
RustFerry exposes a restricted, serializable WidgetSnapshot with title, value, caption, progress, deep-link, and one optional text/link/button content node. Both generated adapters render this common model as an Android App Widget and an Apple WidgetKit extension; this is not a general layout engine.
Support matrix
| Host snapshot/model | Android provider APK | iOS WidgetKit .appex |
|---|---|---|
| Implemented/tested | Enabled provider/backend artifact-inspected; runtime unobserved | App-group publisher, framework, and .appex artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::deep_links::DeepLink;
use rustferry::testing::TestRuntime;
use rustferry::widgets::{self, WidgetId, WidgetSnapshot};
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let id = WidgetId::parse("counter")?;
let snapshot = WidgetSnapshot::new()
.title("Counter")
.value("42")
.caption("Tap to open")
.progress(0.42)
.deep_link(DeepLink::parse("counter://open/current")?);
widgets::update(&id, snapshot.clone())?;
assert_eq!(runtime.widget_snapshot(&id), Some(snapshot));
Ok(())
}
Configuration
[extensions.widget]
enabled = true
app_group = "group.com.example.counter"
Run cargo ferry add widget to add the config/feature and a Rust snapshot fragment.
Permissions and entitlements
iOS shared state requires an application group and compatible signing/provisioning on devices. Android needs generated provider/receiver metadata, not an overlay permission.
Expected result
The host test records the latest snapshot. Apple publisher/extension artifacts and an Android APK with the enabled provider pass inspection. Neither platform has runtime-observation evidence.
Common errors
- Missing
app_group: strict config rejects it. - Progress outside
0.0..=1.0: rejected before backend dispatch. - Expecting arbitrary Rust UI: platform widgets use the constrained schema.
Platform differences
Android uses RemoteViews/provider scheduling; Apple uses WidgetKit timelines and families. Update timing is controlled by each OS.
Test example
Publish several snapshots and assert widget_snapshot(&id) contains the last valid one; assert invalid progress fails.
Example project
See shared state, snapshots, and an action route in the Widget Counter example.
Live Activities
What it does
The API starts, updates, lists, and ends a serializable activity with an optional constrained presentation snapshot. The iOS adapter uses ActivityKit; Android’s honest fallback is an ongoing notification.
Support matrix
| Host activity model | Android fallback artifact | iOS ActivityKit .appex |
|---|---|---|
| Lifecycle implemented/tested | Fallback enabled in an inspected APK; runtime unobserved | Lifecycle bridge, framework, and .appex artifact-inspected; runtime unobserved |
Minimal complete example
use rustferry::live_activity::{self, LiveActivitySnapshot};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
let id = live_activity::start_with_snapshot(
&"final-match",
&0_u32,
LiveActivitySnapshot::new().title("Score").status("0–0").progress(0.0),
)?;
live_activity::update(&id, &1_u32)?;
assert_eq!(live_activity::list_active()?[0].state, 1);
live_activity::end(&id, &2_u32)?;
assert!(live_activity::list_active()?.is_empty());
Ok(())
}
Configuration
[ios]
min_version = "16.1"
[extensions.live_activity]
enabled = true
android_fallback = "ongoing-notification"
Or run cargo ferry add live-activity.
Permissions and entitlements
iOS availability, user settings, ActivityKit/WidgetKit extension configuration, and signing constraints apply. Remote push updates require APNs/server credentials and are not implemented. Android fallback uses normal notification requirements.
Expected result
The host test tracks one activity through start/update/end. Apple compile/link/artifact checks cover the bridge and extension; an inspected Android Kitchen Sink APK contains the enabled fallback bridge and notification prerequisites. Neither platform has runtime-observation evidence.
Common errors
- iOS minimum below 16.1: strict config rejects it.
- Invalid progress: rejected.
- Assuming
is_supported()from OS version alone: backend and user settings also matter.
Platform differences
ActivityKit is Apple-specific. The Android backend maps the operation to an ongoing notification, never an overlay or imitation Dynamic Island; runtime delivery remains unobserved.
Test example
Use runtime.active_activities() on TestRuntime or live_activity::list_active() to assert attributes/state/snapshot after every transition.
Example project
See the complete start/update/end flow in the Live Score example.
Dynamic Island
What it does
LiveActivitySnapshot supplies title/status/progress plus compact leading/trailing text for a generated ActivityKit presentation. RustFerry does not expose arbitrary SwiftUI from application source.
Support matrix
| Host snapshot | iOS extension artifact | Simulator | Physical device |
|---|---|---|---|
| Implemented/tested | Presentation and lifecycle framework artifact-inspected | Not validated | Not validated |
Minimal complete example
use rustferry::deep_links::DeepLink;
use rustferry::live_activity::LiveActivitySnapshot;
fn score_snapshot() -> rustferry::Result<LiveActivitySnapshot> {
Ok(LiveActivitySnapshot::new()
.title("Final")
.status("3–2")
.progress(0.75)
.leading_text("HOME")
.trailing_text("75′")
.deep_link(DeepLink::parse("score://match/current")?))
}
fn main() -> rustferry::Result<()> {
let snapshot = score_snapshot()?;
assert_eq!(snapshot.trailing_text.as_deref(), Some("75′"));
Ok(())
}
Configuration
Use the same iOS 16.1+ and [extensions.live_activity] configuration as Live Activities.
Permissions and entitlements
ActivityKit availability and user settings apply. Device extension signing/provisioning can impose additional constraints. No overlay permission is used on Android.
Expected result
The snapshot serializes the compact labels. Visual presentation is not validated until the generated extension runs on supporting Simulator/device hardware.
Common errors
- Expecting every iPhone/iPad to show Dynamic Island: hardware and current OS context decide presentation.
- Packing long text into compact fields: keep it glanceable.
- Claiming visual parity from a serialization test.
Platform differences
Lock Screen presentation can exist without Dynamic Island. Android uses the configured ongoing-notification fallback.
Test example
Test snapshot serialization and 0.0..=1.0 progress validation; use device/simulator screenshots only as separate runtime evidence.
Example project
See the compact leading/trailing fields in the Live Score example and Live Activities.
Testing
What it does
TestRuntime installs a deterministic thread-scoped backend with in-memory storage and inspection/injection helpers for network, lifecycle, notifications, permissions, haptics, clipboard, sharing, URLs, widgets, and Live Activities.
Support matrix
| Linux | macOS | Windows | Mobile SDK required |
|---|---|---|---|
| Host tests | Host tests | Host tests/path coverage | No |
Minimal complete example
use rustferry::haptics;
use rustferry::network::{self, NetworkStatus, NetworkTransport};
use rustferry::testing::TestRuntime;
fn main() -> rustferry::Result<()> {
let runtime = TestRuntime::new();
let _guard = runtime.enter();
runtime.set_network_status(NetworkStatus::online(NetworkTransport::Ethernet));
assert!(network::is_online()?);
haptics::selection()?;
assert_eq!(runtime.haptic_calls().len(), 1);
Ok(())
}
Configuration
No ferry.toml configuration is needed for unit tests. Generated projects still include their real config so cargo ferry check covers it.
Permissions and entitlements
None. The mock does not grant real OS access and must never be counted as device validation.
Expected result
Application logic runs with no SDK, emulator, simulator, phone, prompt, or host clipboard/network effect.
Common errors
- Guard dropped too early: convenience APIs fall back to an unsupported runtime.
- Sharing a guard across threads:
RuntimeGuardintentionally is notSend; userustferry::spawnor enter the runtime on that thread. - Treating mock success as platform success.
Platform differences
The same deterministic model runs on all hosts. OS callback order, timing, UI, permissions, and hardware need separate artifact/runtime tests.
Test example
#![allow(unused)]
fn main() {
#[test]
fn unsupported_ui_path_is_visible() {
let runtime = rustferry::testing::TestRuntime::new();
let _guard = runtime.enter();
runtime.set_supported(rustferry::Operation::Haptics, false);
assert!(rustferry::haptics::selection().is_err());
}
}
Example project
Every standalone project under examples/ includes focused host-side tests. Capability examples use TestRuntime; start with Counter.
Custom platform code
What it does
The safe PlatformBackend trait lets platform-host code implement granular operations. For an operation that cannot fit the shared API, target-gated application code can synchronously borrow the active Android context through rustferry::android::with_context or the captured iOS application through rustferry::ios::with_application. Neither API grants cross-thread ownership or permits retaining raw handles after the callback.
Support matrix
| Backend contract | Android context | iOS application | External plugin registry |
|---|---|---|---|
| Implemented/tested | Implemented and target-compiled | Implemented and framework artifact-inspected | Not implemented |
Minimal complete example
use rustferry::{PlatformBackend, Runtime};
use std::sync::Arc;
struct EmptyBackend;
impl PlatformBackend for EmptyBackend {}
fn main() {
let runtime = Runtime::new(Arc::new(EmptyBackend));
let _guard = runtime.enter();
assert!(!rustferry::haptics::is_supported());
assert!(rustferry::haptics::selection().is_err());
}
Default trait methods are intentionally unsupported. A real adapter must advertise only operations it performs and override the matching method.
The target-specific escape hatches are available only behind #[cfg(target_os = "android")] or #[cfg(target_os = "ios")]. Keep every borrow inside its callback and dispatch UI work according to the platform API’s thread rules.
Configuration
An adapter for an existing operation can use the public PlatformBackend contract without changing the CLI or schema; see Custom adapters. Add a capability to ferry.toml only after its core schema, generated components, and validation are implemented.
Permissions and entitlements
A custom adapter must derive minimal manifest/plist/entitlement changes from validated configuration. It must not request permissions automatically or bypass platform policy.
Expected result
The empty backend reports an explicit unsupported error, never false success.
Common errors
- Returning
truefromsupportswhile keeping the default unsupported method. - Passing raw handles across threads/lifetimes without a platform guarantee.
- Letting Rust panic or a native exception cross FFI.
- Editing generated files directly; regeneration replaces them.
Platform differences
Android adapters may require generated Java/JNI/manifest components. Apple adapters may require Swift/Objective-C, frameworks, plist entries, entitlements, or an extension target. Keep those beneath target/ferry/.
Test example
Start with deterministic backend tests, golden generated-source tests, then target compilation and final artifact inspection. Simulator/device observation is a separate level.
Example project
The Kitchen Sink example demonstrates the portable application boundary. Use an escape hatch only for a narrowly scoped target-specific operation; study Architecture and FFI safety before changing a platform crate.
Custom adapters
What it does
PlatformBackend lets a host or test harness implement an existing runtime operation without adding a CLI command or configuration key. The adapter advertises only the operations it implements; every other trait method keeps its explicit unsupported default.
This is an extension point for the operations already listed by Operation, not a dynamic plugin system. A new capability, operation, manifest rule, entitlement, generated component, or bridge entry point still requires a core change.
Support matrix
| Extension | Status |
|---|---|
Construct a Runtime with a custom backend | Implemented and tested |
Override an existing Operation method | Implemented and tested |
| Add a new CLI capability through an adapter | Not supported |
| Register an adapter dynamically in generated hosts | No generic registry |
| Add platform declarations or generated native code | Requires platform/code-generation changes |
Minimal complete example
This adapter implements the existing haptics operation and records calls. It changes neither the CLI nor the configuration schema.
use rustferry::haptics::{self, HapticCall, ImpactStyle};
use rustferry::{Operation, PlatformBackend, Runtime};
use std::sync::{Arc, Mutex};
#[derive(Default)]
struct RecordingHaptics {
calls: Mutex<Vec<HapticCall>>,
}
impl PlatformBackend for RecordingHaptics {
fn supports(&self, operation: Operation) -> bool {
operation == Operation::Haptics
}
fn haptic(&self, call: HapticCall) -> rustferry::Result<()> {
self.calls.lock().expect("haptic call lock poisoned").push(call);
Ok(())
}
}
fn main() -> rustferry::Result<()> {
let backend = Arc::new(RecordingHaptics::default());
let runtime = Runtime::new(backend.clone());
let _guard = runtime.enter();
haptics::impact(ImpactStyle::Light)?;
assert_eq!(
backend.calls.lock().expect("haptic call lock poisoned").as_slice(),
&[HapticCall::Impact(ImpactStyle::Light)]
);
assert!(!rustferry::clipboard::can_write_text());
Ok(())
}
supports and the matching method must agree. Advertising an operation while leaving its default method in place produces an unsupported error.
Configuration
The recording adapter needs no ferry.toml entry. A generated mobile application must still enable the existing capability when that capability controls manifest, entitlement, framework, or native-component generation. For haptics:
[capabilities.haptics]
enabled = true
Adapters cannot add unknown configuration fields. Extend the schema and validation only when introducing a real core capability.
Permissions and entitlements
An adapter cannot bypass platform permissions or signing policy. Use the existing validated capability configuration when it already models the required declaration. New manifest entries, plist keys, entitlements, frameworks, or extension targets require corresponding platform generation and artifact checks.
Expected result
The scoped runtime reports haptics support, routes one light-impact call to RecordingHaptics, and records it. Other operations remain unsupported unless the adapter implements them.
Common errors
- Returning
truefromsupportswithout overriding the corresponding trait method. - Replacing a full platform backend and accidentally dropping operations the application still needs.
- Assuming a
PlatformBackendimplementation is automatically discovered by generated Android or Apple hosts. - Adding a new
Operationlocally without updating runtime APIs, platform bridges, configuration, code generation, and tests together. - Returning fabricated success from an adapter that performed no platform work.
Platform differences
The example is portable because it records calls in memory. A production Android adapter may need Java/JNI code, manifest components, and Android thread handling. An Apple adapter may need Swift or Objective-C code, frameworks, plist entries, entitlements, or an extension target. Generated mobile hosts install their platform backend explicitly; there is no runtime plugin loader.
Use the target-specific borrowing APIs in Custom platform code for a narrow operation that does not belong in the shared backend contract. Keep generated glue beneath target/ferry/.
Test example
Exercise an adapter through the public capability function, as the minimal example does, rather than calling its trait method directly. Also assert unsupported behavior for operations the adapter does not advertise. Platform adapters then need target compilation, generated-source checks, artifact inspection, and separate simulator or device observation.
Example project
The Kitchen Sink example exercises the built-in capability boundary. Use the recording adapter above as the starting point for host-side tests; no example currently demonstrates third-party adapter discovery in a generated mobile host because that registry does not exist.
Android without Gradle
The default Android design is a direct build-system integration. The user project contains no build.gradle, Gradle wrapper, Android Studio project, Java source, or Kotlin source.
Pipeline
- Load Cargo metadata and strict
ferry.toml. - Discover one coherent SDK platform, Build Tools revision, NDK LLVM toolchain, Java toolchain, and configured Rust targets.
- Cross-compile the Rust
cdylibfor each ABI. Cargo JSON identifies the exact native artifact and constrained build-script output directories. - Generate a deterministic manifest, resources, and NativeActivity metadata below
target/ferry/android/. - Compile and link resources with
aapt2against the selectedandroid.jar. - Collect required dependency DEX, and compile/merge generated JVM bridge code with
javacandd8only when needed. - Insert uncompressed native libraries under
lib/<abi>/and sequentialclasses*.dexentries. - Run
zipalignbefore signing, then sign withapksigner. - Verify signature, 16 KiB native-library alignment, package/launcher metadata, safe/unique ZIP entries, resources, DEX headers, and ELF ABI headers.
AAPT2’s linked APK is incomplete by itself: it lacks the Rust library, dependency DEX, and signature. Generated strings or an unsigned intermediate must never be reported as the final artifact.
Slint consequence
Slint’s Android integration may produce an internal Java helper/DEX from a Cargo build script. The packager therefore consumes Cargo JSON output and merges constrained DEX inputs. If DEX exists, the manifest must not declare android:hasCode="false".
Signing
Debug signing material belongs in cargo-ferry’s machine configuration directory, not in an application repository. Release signing is explicit. Password sources must be redacted and should avoid process arguments when the selected tool permits safer input.
Build versus deployment
build needs no ADB, emulator, USB connection, or phone and never installs or launches. The explicit install android and run android commands now compose a fresh, independently validated APK with typed ADB operations for one exact device ID, or for the sole compatible device when selection is unambiguous. logs android collects a bounded application-filtered snapshot by default; --json-stream selects the live protocol stream.
Those deployment paths are implemented and host-tested, but no emulator or physical Android device was available for runtime validation. Artifact status and device status remain independent in the Support matrix.
See ADR-002, Android build, and the official AAPT2, zipalign, apksigner, and NDK custom build-system documentation.
Apple generated host
iOS uses the official Apple build system through a generated host. The application author does not create or maintain an Xcode project, plist, Swift target, or extension target.
Simulator pipeline
- Require macOS and discover full Xcode, the selected developer directory, iPhone Simulator SDK,
xcodebuild,xcrun,plutil, Cargo, and the Rust simulator target. - Load Cargo metadata and strict
ferry.toml. - Generate deterministic host metadata below
target/ferry/ios/. - Cross-compile Rust for
aarch64-apple-ios-simon Apple Silicon. - Stage the Rust executable into the generated host and let Xcode assemble metadata, resources, and enabled extension targets into an
.app. - Inspect bundle identifier, executable, architecture, metadata, resources, linked content, and every required
.appexbefore success.
A Simulator runtime/device is optional for build-only work. It is required for install/launch observation, which has a separate status.
Extensions
Widgets and Live Activities require generated SwiftUI/WidgetKit/ActivityKit adapters because Apple owns their presentation and lifecycle. cargo-ferry exposes a restricted serializable Rust snapshot rather than attempting to reproduce SwiftUI. An extension is not complete until its .appex, executable, plist, identifier, and embedding relationship are inspected.
Physical devices
Device builds use only official Xcode signing/provisioning. Team, profile, entitlement, and application-group constraints can differ from the Simulator. No unsigned install, jailbreak, or signing bypass is supported. Device compile, artifact, install, and launch evidence are recorded separately.
See ADR-003, iOS Simulator, and Apple’s Xcode build system.
FFI safety
Platform adapters cross Rust/native/JVM/Swift boundaries. These are security and correctness boundaries, not ordinary internal calls.
Required invariants
- Catch Rust panics before every exported callback returns across FFI.
- Convert platform exceptions/errors into typed Rust errors; never report fabricated success.
- Document raw pointer ownership, nullability, lifetime, thread affinity, and release responsibility.
- Do not mark raw handles
SendorSyncwithout a platform guarantee. - Dispatch UI work on the required platform/UI thread.
- Stop new callbacks after runtime shutdown; dropping a subscription must prevent later callback starts.
- Copy or retain callback payloads according to the platform contract before their source lifetime ends.
- Keep generated bridge surface minimal and free of application business logic.
The workspace currently denies unsafe Rust in its crates. Generated platform glue still needs language-specific exception and lifetime handling; the Rust lint alone is not FFI validation.
Event ordering
Events sent serially by one source retain source order. Independent concurrent sources may interleave. Operating systems can terminate a process without a final lifecycle callback, so important state must be persisted eagerly.
Verification
Host tests cover event teardown, panic containment where expressible, typed model conversion, and mock behavior. Platform artifact tests must also prove bridge symbols/components were linked. Simulator/device observation is required before claiming callback behavior on that environment.
See the Threat model and Support matrix.
Generated files
All platform scaffolding and cargo-ferry artifacts belong below the project’s target/ferry/ directory. User source, assets, Cargo manifests, ferry.toml, and signing inputs remain outside that disposable boundary.
Properties
- Deterministic from validated config, Cargo metadata, capability set, assets, bridge version, and selected toolchain.
- Written without traversing parent directories or following output paths outside the validated root.
- Safe to regenerate after application changes.
- Never the only location of a user signing key or provisioning asset.
- Independently inspected after packaging; cached existence is not sufficient.
Cleaning
cargo ferry clean
cargo ferry clean android
cargo ferry clean ios
cargo ferry clean generated
cargo ferry clean --all
Each target is normalized and constrained below target/ferry/ before recursive removal. A missing target is a no-op. --dry-run prints the intended scope.
Do not manually edit a generated host: changes will be replaced. Extend a capability adapter in the cargo-ferry workspace instead; see Custom platform code.
Measurements
These are reproducible developer checks, not product performance promises. Wall-clock results depend on the host, filesystem cache, dependency state, and Rust/toolchain revision. The first four sections were recorded before the RustFerry rename on the Apple Silicon Mac described in STATUS on 2026-08-01. Their commands use current package names for reproduction; their timings require a fresh run before they describe the renamed revision. Later sections state their own environment.
Strict configuration parsing
cargo bench -p rustferry-core --bench config_parsing
The benchmark parses the generated starter configuration 10,000 times through FerryConfig::parse. Recorded result: 10,000 parses in 85.56775 ms.
Atomic starter generation
cargo bench -p rustferry-codegen --bench template_generation
The benchmark creates 100 complete starter projects in a temporary directory through the same atomic ProjectGenerator::generate path used by the CLI. Recorded result: 100 projects in 6.405533875 s.
Android incremental-cache assertion
cargo test -p rustferry-android --test real_android \
generated_minimal_project_produces_verified_apk -- \
--ignored --nocapture
This SDK/NDK integration test performs two real builds. The second build must report cache hits for aapt2-compile, aapt2-link, and d8; a missing hit fails the test. The check records cache correctness only—no elapsed-time or speedup guarantee is attached to it.
Android cache calculation and no-change planning
cargo test -p rustferry-android \
measures_cache_calculation_and_incremental_no_change_planning \
-- --ignored --nocapture
The ignored developer measurement hashes 16 one-KiB inputs and recreates the same side-effect-free Android plan 1,000 times. Recorded result: 1,000 cache calculations in 819.703292 ms; 1,000 incremental no-change plans in 3.112419209 s. Equality assertions verify stable cache and generated-content identities; these observations are not performance guarantees.
VS Code extension protocol and activation
cargo build -p cargo-ferry
cd editors/vscode
npm run perf
npm run test:host
Recorded on 2026-08-01 on Apple Silicon macOS with the debug cargo-ferry,
Node v22.23.1 for the isolated protocol benchmark, and pinned VS Code 1.100.0:
| Measurement | Observation |
|---|---|
| Extension Host startup to auto-activation | 6,191 ms |
| Extension activation | 1,327.817 ms |
| Project discovery | 44.227 ms |
| Initial discovery and tree refresh | 1,323.933 ms |
| Repeated tree refresh, median of 5 | 39.047 ms |
| Open discovered manifest | 75.862 ms |
| CLI handshake, median / p95 of 7 | 9.644 / 10.973 ms |
| Configuration validation, median / p95 of 7 | 11.605 / 14.342 ms |
| Parse 10,000 protocol events, median / p95 of 7 | 9.877 / 11.797 ms |
| Parse 100,000-event long stream | 135.939 ms |
| Long-stream peak / retained heap delta | 2,060,328 / 14,368 bytes |
The CLI samples use two warmups and include process startup. The host values are one smoke-run observation, except for repeated tree refresh. The memory values come from an isolated Node decoder process with exposed garbage collection; complete Extension Host peak memory is unavailable from this harness. No Android SDK, simulator, emulator, device, installation, or mobile build runs in either command.
Goal 3 status
Final master checkpoint (2026-08-08): PR #8 landed the integrated Goal 3 implementation at
088dedfd1462875f69584db738f5626680b02c91; PR #10 then pinned that trusted worker and enabled the
one-shot master acceptance at 607fe78cf1ae22f8c569fb48d067d8478f407883. This accepted head
preserves the Developer Experience line while adding the remote protocol/provider/worker path and
the portability, file-identity, no-clobber, bounded-process, and cleanup hardening recorded below.
Windows continuation pre-docs source commit: 0ff643f3ce2baf9a28cf0519ad7a825ecd09cbad. This continuation is
implemented and Windows-native tested; it has no Windows-originated GitHub/macOS run.
Current milestone
Milestone -2 — isolation: complete. Milestone -1 — inherited baseline: complete. Milestone 0 — remote/signing/source/artifact contracts: complete. Milestone 1 — physical-device compile: Linux-originated GitHub live-validated. Milestone 2 — signing engine and bounded app/extension manual setup: implemented and locally integration-tested. Milestone 3 — integrated GitHub unsigned provider acceptance: historical Linux path complete. Milestone 4 — private execution repository and real development signing: pending external Apple signing assets and a device. Milestone 5 — SSH unsigned snapshot v1: implementation and deterministic local validation complete; live SSH macOS acceptance pending. Milestone 6 — Windows durable jobs/cancel/retry/artifacts/GitSnapshot/IDE: implemented and Windows-native tested; live Windows acceptance pending.
The Goal 3 definition of done is not complete. The mandatory chain currently ends at a real unsigned XCArchive returned to a Linux client. No Windows-originated archive, real development-signed IPA, registered-device profile acceptance, physical install/launch/logs, Personal Team flow, extension device signing, live SSH build, performance matrix, or reference managed-cloud provider has been validated. The exact 18-scenario ledger is in the support matrix.
Final Windows Cargo gates at the pre-documentation source head pass: cargo-ferry library 145/145 with 0 failed and 0 ignored (3.79 seconds), the bounded prune-publication regression 1/1 (0.24 seconds; 563 ms wall), related prune tests 8/8 (1.68 seconds), cli 42/42 (50.70 seconds), artifact_cli 9/9 (0.42 seconds), jobs_cli 11/11 (0.44 seconds), and the all-target/all-feature cargo-ferry check. These results separately cover changes after the exact Android production source accepted by run 31590994094; that APK evidence is not attributed to the later branch head.
Windows-native jobs, artifacts, snapshot, and IDE paths have focused test coverage. A current-revision local Windows APK artifact is not claimed because the upstream skia-bindings full-source Windows build blocked local packaging. Windows-originated GitHub/macOS iPhone acceptance remains pending. iPhone builds require a local or remote macOS host with full Xcode and the official Apple toolchain. The live-proven result remains an unsigned physical-iPhone XCArchive; no development-signed IPA, installation, launch, logs, or physical-device runtime is claimed.
Final integrated acceptance
- Linux client acceptance run
31261962599completed successfully at exact source head607fe78cf1ae22f8c569fb48d067d8478f407883. The Linux client proved that no local Apple toolchain was available, used the real GitHub provider, dispatched the worker, and automatically downloaded and verified the returned artifact. - macOS worker run
31262066567completed Phase A successfully: trusted-worker verification at088dedfd1462875f69584db738f5626680b02c91, exact toolchain and target setup, immutable request/source checks, real unsigned physical-iPhone compilation, archive sealing and upload, digest recording, and cleanup all passed. Phase B development signing was skipped because no real PKCS#12 archive, password, provisioning profile, distinct private execution repository, or physical device was supplied. This is not signed-IPA evidence. - Acceptance artifact
9023136948has API digestsha256:6d98251ad82f98324b4df36799b71bb8f9f6d8523f8346757e4f7d9bcf1188c3; its independently validated inner archive has SHA-256ff532b50839eca54bb498393ac75929b951204f4b13a772c5e2bee96c36b2dc3. - Final CI run
31261962607completed successfully on attempt 2 at the same exact head. All five jobs are green: Linux quality/docs, Rust 1.92, Ubuntu tests/templates, macOS tests/templates, and Windows tests/templates. The run passed license policy, release contract, archive guards, formatting, Clippy, packaged CLI sources, all workspace package archives, examples, Rustdoc/doctests, cookbook, links, mdBook, Windows workspace tests, and Windows starter generation/check. - The merged Apple path preserves local device/deployment, IDE, extension, and asset behavior while adding remote compilation and signing contracts. Physical builds bind canonical Apple developer tools, use fixed
/usr/bin/xcrunand/usr/bin/security, set the validatedDEVELOPER_DIRfor every Apple-tool invocation, and reject relative, directory, or symlink tool substitutions.
Windows control-plane continuation
The frozen continuation adds a private immutable job store, bounded sanitized provider-log refresh, fresh-process cancellation, exact and current-source retry, complete-lineage prune, managed artifact operations, explicit public GitHub GitSnapshot submission, metadata-only signing readiness, and the VS Code Remote Jobs view. Push remains the compatible/default GitHub trigger. WorkflowDispatch is an additive foundation only: live use requires the exact four-input definition on both the default branch and dispatched ref. No Windows-originated GitHub run, snapshot run, cancellation, retry, signed artifact, or device result is claimed.
| Area | Implemented | Locally tested | Windows-native tested | GitHub live validated | Apple signed validated | Physical-device validated |
|---|---|---|---|---|---|---|
| Durable jobs/logs/cancel/retry/prune | Yes | Yes | Yes | No | No | No |
| Managed artifacts | Yes | Yes | Yes | No new live management result | No | No |
| GitHub GitSnapshot | Yes | Yes | Yes | No | No | No |
| VS Code Remote Jobs | Yes | Yes | Yes | No | No | No |
| Signing readiness | Yes | Yes | CLI path tested; no ready configuration | No signed run | No | No |
Frozen local gates: cargo-ferry library 139/139, binary 235/235, jobs CLI 11/11,
artifact module 12/12, artifact CLI 9/9, GitHub provider 316 passed/1 explicit live ignored, IDE
black-box 4/4, and live TypeScript 74 passed/8 skipped. All-target check, strict Clippy, formatting,
diff hygiene, and independent P0–P2 reviews pass. The two checked-in IDE schemas share SHA-256
DAC218CBCE888ACF6079E4DF304452D22990CE0DB81EDB5382549198255DB270.
Current continuation
Earlier SSH snapshot continuation
The earlier continuation after the accepted master head added physical-iOS auto-routing on
non-macOS hosts, deterministic source-bundle inspect/create/verify commands, and SSH snapshot
session v1. Source bundles include only the selected package dependency closure, exclude audited
sensitive roots, reject links and collisions, publish each output create-only, and bind descriptor,
manifest, archive, and extracted bytes. SSH configuration and trust snapshots are private and
create-only; Unix config and operation data use restrictive modes. Windows managed config objects
and per-operation source/trust directories are created with protected owner-bound DACLs and
verified through retained handles before sensitive bytes are trusted. The Windows implementation
has native runtime tests; core all-target/strict-Clippy and rustferry-ssh library cross-checks
pass, but those runtime tests were not executed on this macOS host. A full cargo-ferry Windows
cross-check is blocked in external vendored openssl-sys because Darwin Perl cannot configure
VC-WIN64A.
OpenSSH receives fixed arguments and retained path identities. The session streams a bounded
snapshot, ordered progress and one unsigned XCArchive, independently verifies and publishes the
artifact before receipt, supports cancellation, and
requires capability-bound non-retaining worker cleanup before success. Cancellation and timeout
prove local cleanup but do not drain a terminal remote cleanup proof.
This SSH path has deterministic local protocol, process, client, worker, and adversarial tests only. No live Windows/OpenSSH interoperability, live SSH macOS build, or SSH-produced artifact is claimed. The GitHub runs below remain the only live no-Mac physical-iPhone archive evidence; they do not prove SSH, signing, IPA, installation, launch, device runtime, or multi-tenant worker isolation.
Validation levels
- Source baseline: isolated at
d6887eba95b8116799801118c5026210628397f9; metadata and workspace check passed; workspace tests reported 158 passed, 0 failed, 7 intentionally ignored. - Isolation audit: source checkout remains read-only. A bounded set of independent review checks bypassed the Goal 3 command wrapper; exact commands/outcomes and recovery are recorded in
GOAL3_COMMAND_AUDIT_EXCEPTIONS.md. The resulting Goal 3-only root build cache was removed with an auditedcargo clean; subsequent checks use the wrapper. - Remote protocol: v1.0 Rust contracts, provider boundary, cancellation, 24 typed events, and checked-in JSON Schema implemented.
- Windows job control: immutable project-bound revisions, exact provider-session restoration, durable cancellation intent, exact/recaptured retry lineage, bounded sanitized log ingestion, complete-lineage pruning, and managed artifact verification/removal pass the frozen Windows suites. No GitHub-live Windows mutation is inferred.
- SSH snapshot v1: strict full-duplex framing, deterministic source upload, unsigned compile-only
request binding, ordered events, cancellation, digest-bound artifact/receipt, durable no-clobber
client publication, and capability-bound zero-retention cleanup are locally tested. No live SSH
Mac artifact exists. The current
rustferry-sshrun reports 54 passed and 2 ignored live-process tests. - Source snapshots: deterministic manifest selection, deterministic ZIP transport, per-output create-only publication, strict worker extraction, and exact post-extraction verification implemented. Explicit public unsigned GitHub GitSnapshot adds invocation-bound zero-write preview, consent/replan, private staging, create-only temporary-ref ownership, restart recovery, retained exact retry source, and cleanup authorization. It has no live GitHub result.
- Signing/provisioning: public certificate metadata plus opaque private-key/password/profile references, temporary-Keychain worker isolation, staged validation state, central chunk-safe redaction, and profile/entitlement/nested-code validation implemented. Manual setup accepts at most three exact application/extension profiles, requires a common selected device, assigns canonical static per-target GitHub secrets, and uses bounded
RFSIGNV2input for a multi-profile worker job while retaining the legacy single-application frame. Modern setup stores the exact public application/extension/framework/dynamic-library graph; the generated workflow embeds its domain-separated canonical SHA-256, and the worker rederives it before checkout of the requested project revision or compilation. It validates PKCS#12 and CMS profile bytes locally, pins three Apple roots, checks certificate/private-key/team/profile/device/target bindings, and retains only a lowercase SHA-256 of the UDID. The affected-package integration suite passes. No real certificate/profile bytes or Apple signing operation have been validated. - Artifact inspection: cross-platform strict unsigned
.xcarchive/.appand IPA ZIP/plist/Mach-O parsers implemented. Client-owned product identity now binds the exact app path, versions, deployment target, nested bundle graph, source manifest, and canonical request digest across compile and signing. They reject Simulator code, signing residue in unsigned input, hidden code, bundle-set drift, resource drift, links, collisions, and archive-limit violations. The current affected run passes 42rustferry-remotelibrary tests plus its integration suites; strict clippy passes. The parent-swap regression synchronizes its attacker before publication and passes. The real physical-device archive from run30724621750passed worker-side inspection and independent Linux-client revalidation. - Signed artifact transport: the protected worker keeps the exact five-file default, then materializes only request-selected
application.app.zip, reconstructedapplication.xcarchive.zip, and main-applicationapplication.dSYM.zipproducts.--artifact allselects the app and archive while--include-dsymremains separate. Worker and cross-platform client independently bind the complete app trees, archive deep-signature evidence, manifest records, real DWARF content, and the dSYM-to-signed-executable arm64 UUID. The workflow allowlists all eight possible paths, but ingestion requires the exact request-derived subset and preserves replacement paths during rollback. This path is locally tested only; no development-signed IPA, signed archive, dSYM, Organizer/export round trip, or device evidence exists. - Physical-device compile: deterministic unsigned
aarch64-apple-ios/iphoneos/generic-device archive plan and executor implemented. The Apple crate now exposes one pure request-product derivation shared by local and remote planners. The final affected run passes 47rustferry-applelibrary tests with 1 Xcode-dependent ignore plus its integration suites; strict clippy passes. Local Xcode 26.6 accepted the generated device project and a real signed Simulator extension smoke passed. GitHub-hostedmacos-15run30724621750produced a real physical-iPhone archive from an exact Rust 1.92 toolchain and confirmed cleanup. Local physical-device compilation remains unavailable because the isolated host lacks the Rust target and iPhoneOS platform component; it is no longer required for the validated remote path. - GitHub provider: public source and private execution identities are separate throughout config, workflow generation, temporary-ref publication, worker validation, Actions APIs, and artifact ingestion. Both fetch and push URLs are identity-checked. Push remains compatible/default. WorkflowDispatch has exact input/request/run binding but no wired provider consumer or live result; default-branch and dispatched-ref definitions must both carry its exact contract. Metadata-only signing doctor proves policy/identity facts without reading secret values. Historical schema-v2 same-repository Push compatibility is live-validated; distinct-private-execution, GitSnapshot, and WorkflowDispatch acceptance remain pending. Linux acceptance run
30726401991exercised the unsigned exact-Git artifact path successfully. - Workspace regression: the preceding continuation’s full workspace test run passed, including the
generated-project template check (94.56 seconds). The earlier
ENOSPCinterruption was environmental; after the specification-authorized Goal 3 target-cache cleanup, the same test completed successfully. At2e0773a, the affected package slice passes; the full workspace was not rerun at that revision because the volume remained 99% used with about 3.1 GiB free. - Manual GitHub signing setup: exact CLI grammar, dry-run preview, interactive or explicit confirmation, secure password sources, stable asset-file reads outside Git repositories, and bounded per-target profile mapping are implemented. An extension-free project may use legacy
--profile PATH; an app/Widget/Live Activity graph requires one exact repeatable--profile TARGET=PATHper generated target, with at most three profiles and one common device. Preflight requires public source, distinct active private execution, required reviewer, the singlerustferry/goal3/builds/*policy, and an empty Environment. Immediately before upload the retained bytes are cryptographically revalidated and converted to typed canonical-base64 PKCS#12/profile values plus a raw bounded password. The application retains the legacy profile secret; extensions use canonical static target-derived names. Values are limited to 48 KiB each and sent toghonly through standard input. A project-local exclusive lock and stable no-follow snapshots serialize config writers. The client post-checks the exact planned three-to-five secret names and persists the local signing plan last; partial or indeterminate remote writes leave local configuration unsigned and identify both uploaded and possibly-uploaded cleanup roles. Multi-profile jobs useRFSIGNV2; the legacy input frame remains single-application-only. Local integration tests cover all target roles, secret-set drift, project-state drift, malformed frames, cleanup names, and legacy compatibility. - IPA export: not validated.
- Client download: validated on final
masterhead607fe78cf1ae22f8c569fb48d067d8478f407883by Linux acceptance run31261962599; the client automatically downloaded and verified the real unsigned physical-device archive returned by worker run31262066567. - Install, launch, runtime: local physical install/launch services exist from the integrated Developer Experience path, but downloaded-remote-artifact install, launch, and runtime remain unvalidated.
GitHub readiness observation
- Repository: public
ShiroKSH/rustferry; current user has admin access; Actions enabled. - Current default-branch worker definition is active but Push-only. This satisfies registration discovery, not WorkflowDispatch readiness; no dispatch request was sent.
- Windows Push-mode preflight was read-only and made no mutation because the source/workflow revision and provider configuration were not yet frozen. No Windows run/job/artifact ID exists for this continuation.
- Integrated workflows: worker pinning and exact-source dispatch were accepted at
607fe78cf1ae22f8c569fb48d067d8478f407883by Linux-client run31261962599and worker run31262066567. - Goal 3 protected environment: absent.
- Repository signing secret names: none returned. No secret values were requested or accessed.
- Signed setup, doctor, and submission intentionally reject this public repository; a private execution repository is required before development signing.
- Live Linux acceptance runs
30722271351and30723002515failed before submission, first on unsupported Actions-token identity lookup and then because executable canonicalization changed the Rustupcargoproxy basename. Both failures were fixed with focused regression coverage; neither run created a temporary ref or macOS job. - Acceptance run
30723358152proved an authenticated temporary-ref push. Its first attempt exposed a first-registration discovery race; after an exact manual trigger registered the branch-only worker workflow, the failed-job rerun launched macOS worker run30723639422. Run discovery now uses the repository-wide Actions endpoint, retaining exact local workflow/SHA/branch/event matching while avoiding the unregistered-workflow 404. The worker verified and built the pinned trusted worker revision, then rejected the valid project manifest becausetoml::Value::from_strparses one TOML value rather than a TOML document intoml0.9. The worker now parsestoml::Table; all 15 worker binary tests and strict clippy pass. - Acceptance run
30723921887launched macOS worker run30723955811without manual registration. The exact worker and request checks passed; the physical-iPhone compile ran for about three and a half minutes before post-build source verification reportedsource_changed. The compile command now requires Cargo’s--lockedmode so dependency resolution cannot rewrite the request-bound lockfile. The failure cleanup also exposed a compile-output inventory denial of service; compile roots, which never receive signing secrets, now bypass the signing-material inventory and are removed after the same marker, ownership, and handle-binding checks. Sixteen worker binary tests, the focused physical-device plan test, and strict changed-target clippy pass; live confirmation remains pending. - Acceptance run
30724347922launched macOS worker run30724376092. The worker built and independently inspected a real unsigned physical-iPhone archive, sealed and uploaded it, and confirmed compile-root cleanup. The Linux client automatically downloaded it, revalidated it, published it at the expected local path, and reported SHA-25615f5e4feba2cd5bb9385e73e729fe70980968c316b5a65ec0717758cb3680ffcwithvalidated: trueandcleanup_confirmed: true. The acceptance job itself failed only because its extra ZIP-entry assertion expectedCounter.appinstead of the request-boundcounter.app; the assertion is corrected and a fully green evidence rerun is pending. - Acceptance run
30724588475and macOS worker run30724621750completed successfully. This is the independently recorded no-Mac unsigned acceptance: Linux had no Apple toolchain, submitted exact source revisionf7f43261967964dc50b0a70c03431c7d204a7ef0, observed a real physical-iPhone macOS build, automatically downloaded the archive, matched archive SHA-256446d21ef42721ae83de356e5bda3e80e93af4f63b8573d64f09918433d54a3f1, validated all 12 required archive entries, and uploaded the archive plus sanitized evidence. Downloaded evidence artifact8825940702has API digest97b31e74db951031b133980d2912b068d6066a6cc767c29c8e55dd661b6d0fd9; localunzip -treports no errors. The provider implementation in this run also included repository-wide first-run discovery. - Schema-v2 regression run
30726401991and macOS worker run30726432908completed successfully from exact source revisiond4fe76ff0d81afab140d1713b39431f310c3fbc1. The worker revision9a62db39eacda0daf8f6f3951452bad7c3aad582enforced explicit public-source identity while keeping dispatch execution implicit, compiled and sealed the physical-iPhone archive, and confirmed cleanup. The Linux client automatically downloaded and independently validated archive SHA-256d8a36f5cb6582493cdeef5699d0db6c6e02a5fbc8490d7d4530e8d751407c747. Evidence artifact8826548085has API digestbda767d1ac176f3bfb701576fcfbc55cfa3767d1c4ecf24bdf031541c9d5ad57; a fresh localshasum -a 256matched andunzip -tpassed all 12 entries. - Final pre-integration acceptance run
30731551293and macOS worker run30731629789completed successfully. The worker compiled, sealed, and cleaned up a real physical-iPhone archive; the Linux client automatically downloaded and independently validated archive SHA-2562b0a91a5b6e83d6655a137c3c95104466113963ce91332819f65b963c70112ed. The sanitized evidence reportedvalidated: true,cleanup_confirmed: true, anddry_run: false. This is the latest unsigned evidence, not evidence for the integrated revision or development signing. - Initial integrated acceptance run
31254848862and worker run31254940813completed successfully at source headc004b78931d3d1b530ad9afd0df24b37631a2589. Parallel CI run31254848783then exposed two Windows-only unsigned-archive fixture failures caused by native joining of a protocol-style relative path; commita39477cfixes the portable path construction. - Replacement acceptance run
31255552196and worker run31255650767completed successfully at source heada39477ca0180e211ee464acf7baf60de194ce591. Parallel CI run31255552203exposed seven additional Windows-only worker path/handle cleanup failures; commitc08e793serializes native components portably and closes identity handles before removal. - Intermediate acceptance run
31256498914was superseded before worker dispatch when the branch advanced fromc08e793to its final workflow-pin commit, so its trusted-source readiness failure is not worker evidence. - CI run
31256564859showed that Camino had normalized a traversal fixture before the boundary received it. Commits1c06dc9and2849b50retain production rejection of raw./..components and build the regression from an unnormalized platform-native string. CI run31258018324then exposed Windows’ unsupported directoryFile::open/sync_all; commit6658643follows the existing repository policy of Unix directory fsync and a non-Unix no-op, while the actual macOS worker remains fail-closed. - Final acceptance run
31258657179and worker run31258758075completed successfully at source headf55f5a94cc8cdfb050fb0fc17f6777ae625a19cc. Phase A produced, sealed, uploaded, downloaded, and verified the real unsigned physical-iPhone archive and completed cleanup. Artifact9022219517has API digestsha256:071a321305f361a3107128bf992322de77fa12114f6ef4b36a1972b2f3e7442c; the inner archive SHA-256 isebe4c99b0bab31f63b41fa043cf74a0ae3b2663faf0cef9bc0feeb2d5bc4aa28. Protected Phase B signing was skipped because the required real Apple assets and device were absent. - Final
masteracceptance run31261962599and worker run31262066567completed successfully at source head607fe78cf1ae22f8c569fb48d067d8478f407883, with worker revision088dedfd1462875f69584db738f5626680b02c91. The Linux client had no Apple toolchain; Phase A compiled, sealed, uploaded, downloaded, independently verified, and cleaned up a real unsigned physical-iPhone archive. Artifact9023136948has API digestsha256:6d98251ad82f98324b4df36799b71bb8f9f6d8523f8346757e4f7d9bcf1188c3; the inner archive SHA-256 isff532b50839eca54bb498393ac75929b951204f4b13a772c5e2bee96c36b2dc3. Protected Phase B remained skipped because the required private execution setup, real Apple assets, and device were absent.
Remaining validation
Goal 3 unsigned no-Mac acceptance is live-validated end to end on final master revision
607fe78cf1ae22f8c569fb48d067d8478f407883.
The Windows continuation completes the local durable control plane and explicit GitSnapshot route at
the frozen source revision recorded above. These paths are implemented, locally tested, and
Windows-native tested; no Windows-originated Push build, GitSnapshot build, cancellation, retry, or
artifact-management operation has GitHub-live evidence.
Manual-development setup now includes bounded app/Widget/Live Activity profile transport and its
affected-package integration tests pass, but no real signing secrets have been uploaded. Development
signing, IPA export, downloaded-artifact install/launch, extension behavior, and physical-device
runtime remain unvalidated. Live SSH/OpenSSH end-to-end acceptance also remains
pending. Final CI run 31261962607 is green across Linux,
macOS, and Windows, including the full workspace and platform starter checks.
The next product boundaries are a real Windows Push-mode archive acceptance, a live GitSnapshot and
cancel/retry sequence, then a distinct private execution repository plus real Apple Development
PKCS#12/password/profile/registered-device assets and a protected Environment. Existing Simulator
support, readiness metadata, and synthetic signing fixtures are not evidence for signed/device claims.
The specification’s requested documentation inventory is also incomplete as a path-by-path
deliverable: the implemented material is currently consolidated in docs/remote/, existing iOS,
deployment, threat-model, release, and status pages rather than every requested docs/iphone/,
docs/security/, and ADR file. This consolidation is documented, not counted as completion of the
missing files.
Goal 3 Completion Audit
Audit date: 2026-08-09; Windows continuation update: 2026-08-10
Audited code revision: frozen pre-docs Windows source revision recorded in docs/GOAL3_STATUS.md
This ledger preserves the original Goal 3 audit against e850deb, bounded multi-profile signing at a339fff, protected signed-log transport at 43b4476, and selectable signed products at 2e0773a, then updates the Windows durable-control-plane rows. The continuation remains on the dedicated Goal 3 branch. Local/native gates pass; Windows GitHub-live, signed, and device acceptance remain pending.
Status meanings:
- Proven — directly established by inspected current-tree artifacts or a cited live run.
- Implemented-unproven — implementation and usually tests exist, but the required real provider, Apple account, signing identity, host, or device path has not run successfully.
- Missing — required product behavior or evidence is absent.
- Contradicted — current state directly conflicts with the stated requirement.
Unit, integration, synthetic signing, and mocked-provider tests prove deterministic code behavior only. They are not counted as live GitHub, Apple, SSH-host, signing, or physical-device proof. The app/Widget/Live Activity profile continuation is therefore recorded only as implemented-unproven until its live signing gates pass.
Windows continuation verification
| Gate | Result |
|---|---|
| cargo-ferry | 139 library, 235 binary, 11 jobs CLI; all passed |
| Managed artifacts | 12 module and 9 CLI; all passed |
| GitHub provider | 316 passed, 1 explicit live test ignored; strict Clippy passed |
| IDE/VS Code | Rust black-box 4 passed; live TypeScript 74 passed/8 skipped; identical schema SHA-256 DAC218CBCE888ACF6079E4DF304452D22990CE0DB81EDB5382549198255DB270 |
| Strict gates | all-target check, Clippy with -D warnings, formatting, diff hygiene, and independent P0–P2 reviews passed |
| Live evidence | No Windows-originated GitHub run, GitSnapshot, cancellation, retry, signed artifact, or device result |
Historical local verification at 2e0773a
| Check | Result |
|---|---|
| Affected all-target tests | Pass: cargo-ferry 42 library, 112 binary, 52 CLI, 1 log-stream integration, and 1 doctest; Apple 47 library passed/1 ignored, 9 device passed/3 ignored, 2 golden passed, and 4 Xcode smoke ignored; GitHub 140; remote 42 library plus all integration suites and 4 doctests; worker 81 library and 27 binary |
| Strict Clippy | Pass with -D warnings for cargo-ferry, Apple, GitHub, remote, and worker, all targets |
| Formatting and patch hygiene | cargo fmt --all -- --check and git diff --check pass |
| Generated workflows | Modern generated YAML passes actionlint; the schema-v2 legacy snapshot is byte-identical |
| Independent review | Clean after signed-request base-contract, ZIP alias/tree/UUID, request-derived output, dSYM capability cleanup, optional-product ownership, and replacement-preservation fixes; active same-UID racing remains outside the isolated Phase B trust boundary |
The current-revision full workspace/CI matrix was not rerun: the workspace volume was 99% used with about 3.1 GiB free, while the affected dependency slice was fully tested from existing caches. The historical full-workspace and cross-platform CI results below remain separate evidence.
Main 27-point definition of done
| # | Requirement | Status | Evidence |
|---|---|---|---|
| 1 | Linux client builds for iPhone without local macOS or Xcode | Proven | Live client run 31261962599 and worker run 31262066567 at 607fe78; .github/workflows/rustferry-goal3-linux-client-acceptance.yml; docs/GOAL3_STATUS.md. |
| 2 | Windows client builds for iPhone without local macOS or Xcode | Implemented-unproven | Cross-platform routing in crates/cargo-ferry/src/commands/platform_build.rs; Windows remains partial in docs/support-matrix.md; no live Windows run. |
| 3 | cargo ferry build iphone and ios --device select a remote macOS path when needed | Proven | Routing and non-mac default in crates/cargo-ferry/src/commands/platform_build.rs; CLI definitions in crates/cargo-ferry/src/cli.rs. |
| 4 | Provider abstraction supports GitHub, SSH Mac, and local Mac | Missing | Trait exists in crates/rustferry-remote/src/provider.rs; concrete implementations only in crates/rustferry-github/src/provider.rs and crates/rustferry-ssh/src/provider.rs; no local-mac provider. |
| 5 | Remote macOS worker validates host readiness | Implemented-unproven | Worker commands in crates/rustferry-worker-macos/src/main.rs; macOS/Xcode/SDK/Rust/disk/signing/profile checks in crates/rustferry-worker-macos/src/host.rs; no current signed host proof. |
| 6 | Compile Rust for aarch64-apple-ios | Proven | Live unsigned worker run 31262066567; toolchain and target checks in crates/rustferry-worker-macos/src/host.rs; build pipeline in crates/rustferry-worker-macos/src/pipeline.rs. |
| 7 | Build a real iphoneos application bundle | Proven | Live unsigned .xcarchive path from runs 31261962599 / 31262066567; validation recorded in docs/GOAL3_STATUS.md; pipeline in crates/rustferry-worker-macos/src/pipeline.rs. |
| 8 | Produce a correctly signed .app | Implemented-unproven | Signing/keychain/provisioning pipeline exists in crates/rustferry-worker-macos/src/keychain.rs, provisioning.rs, and pipeline.rs; only synthetic evidence is recorded in docs/GOAL3_STATUS.md. |
| 9 | Produce a correctly signed .xcarchive | Implemented-unproven | The worker reconstructs the archive from the exact independently validated signed IPA app tree, requires a fresh deep strict signature check, and transports it for --artifact archive|all; no real signed archive or Organizer/export proof exists. |
| 10 | Export a signed .ipa | Implemented-unproven | Export implementation in crates/rustferry-worker-macos/src/export.rs; docs/GOAL3_STATUS.md states real IPA export remains unvalidated. |
| 11 | Validate certificate identity and requested Apple Team ID | Implemented-unproven | Typed plans in crates/rustferry-remote/src/signing.rs; validation in worker signing/provisioning modules; no real certificate/account run. |
| 12 | Validate provisioning profile against bundle, team, certificate, and device | Implemented-unproven | crates/rustferry-worker-macos/src/provisioning.rs; synthetic fixtures only. |
| 13 | Preserve and validate requested entitlements | Implemented-unproven | crates/rustferry-remote/src/signing.rs and worker provisioning/signing validation; no signed artifact evidence. |
| 14 | Sign frameworks, extensions, and app inside-out | Implemented-unproven | Multi-target plan and ordering exist in crates/rustferry-remote/src/signing.rs; bounded per-target CLI/provider/worker transport passes local integration tests, with real signing pending. |
| 15 | Support distinct profiles for the app and every extension | Implemented-unproven | Repeatable exact TARGET=PATH, at most three profiles, common-device validation, static per-target secrets, and RFSIGNV2 input pass local cargo-ferry, rustferry-github, and worker tests; no live signed proof yet. |
| 16 | Submit an exact clean Git revision without copying secrets | Proven | Clean-revision source flow in crates/cargo-ferry/src/commands/remote.rs; GitHub provider in crates/rustferry-github/src/provider.rs; live unsigned run 31261962599. |
| 17 | Submit an explicit deterministic source snapshot | Implemented-unproven | build iphone --remote github --snapshot --unsigned provides zero-write preview, explicit consent, deterministic staging, durable ownership, recovery, exact worker binding, retry retention, and cleanup. Local/Windows suites pass; no GitHub-live snapshot. |
| 18 | Emit structured job IDs, phases, progress, warnings, and terminal events | Implemented-unproven | Private immutable jobs plus list/show/logs/artifacts/prune persist sanitized lifecycle and bounded worker events across processes. Local/Windows suites pass; no Windows live job. |
| 19 | Cancel and retry remote work safely | Implemented-unproven | Fresh-process cancellation, exact Git/retained GitSnapshot retry, current-source recapture, atomic lineage, and crash recovery are implemented and Windows-native tested; no live cancel/retry result. |
| 20 | List and download all declared artifacts | Implemented-unproven | GitHub artifact flow in crates/cargo-ferry/src/commands/remote.rs and crates/rustferry-github/src/provider.rs; run 31261962599 proves unsigned archive download, while selected signed app/archive/dSYM sets have deterministic local coverage only. |
| 21 | Verify downloaded SHA-256 values before success | Proven | Strict artifact verification in crates/rustferry-remote/src/artifact.rs; live acceptance hash checks in .github/workflows/rustferry-goal3-linux-client-acceptance.yml. |
| 22 | Reject unsafe artifact paths and archive contents | Proven | Path/archive validation and limits in crates/rustferry-remote/src/artifact.rs and remote security tests. |
| 23 | Emit a complete, machine-readable artifact manifest and validation report | Implemented-unproven | Manifest fields and validation levels in crates/rustferry-remote/src/artifact.rs; the worker and client enforce exact request-derived records and signed-product evidence locally, but no real signed manifest exists. |
| 24 | Keep credentials and signing material out of source, argv, logs, and artifacts | Implemented-unproven | Central redaction tests in crates/rustferry-remote/tests/security.rs; protected sign phase and stdin frame in .github/workflows/rustferry-goal3-iphone.yml; no live signed-secret audit. |
| 25 | Install the downloaded application on a physical iPhone | Implemented-unproven | Device install service exists under crates/cargo-ferry/src/deployment/; docs/support-matrix.md records no end-to-end downloaded signed artifact proof. |
| 26 | Launch the installed application and report identity/result | Implemented-unproven | Run/device support exists under crates/rustferry-apple/src and cargo-ferry commands; no physical-device run. |
| 27 | Stream physical-device logs and complete the full remote-to-device path | Missing | docs/support-matrix.md marks physical logs unsupported; no signed IPA, install, launch, or runtime-log acceptance run. |
GitHub provider: 17 criteria
| # | Criterion | Status | Evidence |
|---|---|---|---|
| 1 | Concrete provider implements the shared remote-provider contract | Proven | crates/rustferry-github/src/provider.rs; contract in crates/rustferry-remote/src/provider.rs. |
| 2 | GitHub is the default remote path on non-macOS | Proven | crates/cargo-ferry/src/commands/platform_build.rs. |
| 3 | remote setup github installs provider config and workflow | Proven | crates/cargo-ferry/src/commands/remote.rs; generated workflow path .github/workflows/rustferry-goal3-iphone.yml. |
| 4 | Setup has deterministic preview/dry-run behavior | Proven | Remote setup preview/config logic in crates/cargo-ferry/src/commands/remote.rs; checked-in command tests. |
| 5 | Setup completes an unsigned smoke build, download, and inspection | Missing | Setup stops after installation/instructions in crates/cargo-ferry/src/commands/remote.rs; acceptance workflow is separate. |
| 6 | Doctor checks authentication, repository, workflow, permissions, environment, and secrets | Implemented-unproven | Doctor implementation in crates/cargo-ferry/src/commands/remote.rs; no private signed-environment run. |
| 7 | Exact Git revision mode works without Apple credentials | Proven | Live runs 31261962599 / 31262066567; .github/workflows/rustferry-goal3-linux-client-acceptance.yml. |
| 8 | Explicit GitHub source-snapshot mode works | Implemented-unproven | Explicit public unsigned GitSnapshot route, consent, recovery, retry retention, and cleanup ownership pass local/Windows tests; no live GitHub build. |
| 9 | Submission uses isolated, collision-resistant temporary refs/jobs | Proven | GitHub provider implementation and workflow concurrency in crates/rustferry-github/src/provider.rs and .github/workflows/rustferry-goal3-iphone.yml. |
| 10 | Workflow/action/toolchain inputs are pinned | Proven | .github/workflows/rustferry-goal3-iphone.yml; .github/workflows/rustferry-goal3-linux-client-acceptance.yml. |
| 11 | Compile phase has no signing-secret access | Proven | Workflow permissions/job separation in .github/workflows/rustferry-goal3-iphone.yml; compile job precedes protected sign job. |
| 12 | Signed phase is isolated behind a protected environment | Implemented-unproven | Sign job environment and secret bindings in .github/workflows/rustferry-goal3-iphone.yml; private environment protection not live-proven. |
| 13 | Secret set is explicit, exact, and passed through stdin | Implemented-unproven | crates/rustferry-github/src/workflow.rs derives a static application/extension profile set; the worker accepts bounded RFSIGNV2 for multiple profiles and the legacy frame only for one application. Local integration tests pass; live protected-Environment proof remains pending. |
| 14 | Provider reports queue/job/phase progress and terminal errors | Implemented-unproven | Durable sanitized job events include bounded provider refresh and exact completion proof. Persistent CLI/IDE UX passes local/Windows suites; no malformed/provider-failure live run. |
| 15 | Provider supports cancellation and cleanup | Implemented-unproven | Durable intent, exact owned-run cancellation, GET-only restart reconciliation, and cleanup are implemented/tested; no live cancellation/cleanup-failure evidence. |
| 16 | Provider downloads declared artifacts and verifies integrity | Proven | Live unsigned artifact path in run 31261962599; verification in crates/rustferry-remote/src/artifact.rs and acceptance workflow lines 141-158. |
| 17 | Protected manual signed workflow produces a real IPA | Missing | Current workflow is temporary-ref push-triggered, not the requested manual signed acceptance; no successful signed run or IPA artifact ID. |
Signed IPA: 18 criteria
| # | Criterion | Status | Evidence |
|---|---|---|---|
| 1 | Manual mode accepts a PKCS#12 signing certificate | Implemented-unproven | crates/cargo-ferry/src/commands/signing.rs; worker sign input in crates/rustferry-worker-macos/src/main.rs. |
| 2 | Certificate password is transferred without argv/log exposure | Implemented-unproven | Stdin-only workflow frame in .github/workflows/rustferry-goal3-iphone.yml; parser in crates/rustferry-worker-macos/src/main.rs. |
| 3 | Manual mode accepts one profile for every signable target | Implemented-unproven | Cargo-ferry accepts at most three exact TARGET=PATH profiles, preserves legacy PATH for a single app, requires a common device, and passes local integration tests; real profile proof remains pending. |
| 4 | Secret names are deterministic, static, and target-specific | Implemented-unproven | crates/rustferry-github/src/workflow.rs retains the legacy app secret and derives canonical static extension names from target identity; provider and worker integration tests pass. |
| 5 | Signing plan identifies application and extension bundle IDs | Proven | ProvisioningPlan / SigningPlan in crates/rustferry-remote/src/signing.rs. |
| 6 | Team ID consistency is checked before signing | Implemented-unproven | Signing/provisioning validation in crates/rustferry-remote/src/signing.rs and worker modules; synthetic evidence only. |
| 7 | Profile application identifier matches each target bundle ID | Implemented-unproven | crates/rustferry-worker-macos/src/provisioning.rs; no real profile run. |
| 8 | Registered device and profile device coverage are validated | Implemented-unproven | Device/profile checks in worker provisioning and remote signing models; no real device/profile evidence. |
| 9 | Requested entitlements are a permitted subset of profile entitlements | Implemented-unproven | Signing-plan validation and worker provisioning logic; synthetic fixtures only. |
| 10 | App Groups/keychain/shared capabilities remain consistent across targets | Implemented-unproven | Multi-target entitlement model in crates/rustferry-remote/src/signing.rs; no signed extension artifact. |
| 11 | Signing uses an isolated temporary keychain and restores host state | Implemented-unproven | Worker signing/keychain implementation and cleanup paths; no real host run. |
| 12 | Every target embeds its matching provisioning profile | Implemented-unproven | Per-target loop in crates/rustferry-worker-macos/src/pipeline.rs and provisioning.rs; bounded named transport supplies the exact profile set in synthetic tests, but no real signed artifact exists. |
| 13 | Frameworks and extensions are signed before the containing app | Implemented-unproven | Ordering in remote signing model and worker pipeline; synthetic proof only. |
| 14 | Final signatures and entitlements are independently verified | Implemented-unproven | Worker signing/export validation code and artifact validation report; no real signed output. |
| 15 | .xcarchive structure and metadata are valid | Implemented-unproven | The selected signed archive is reconstructed from the exact validated IPA app tree and rechecked with deep strict codesign; client tree verification passes synthetic coverage, but Organizer/export and real signed-artifact proof are absent. |
| 16 | Export options are derived from validated manual-signing inputs | Implemented-unproven | crates/rustferry-worker-macos/src/export.rs; no real account/profile export. |
| 17 | IPA, archive, manifest, validation report, and sanitized log are returned | Implemented-unproven | The exact five-file default and request-selected signed app/archive/main-app-dSYM transports are implemented, manifest-bound, and locally tested; no protected live signing run has returned them. |
| 18 | IPA installs, launches, and runs on the registered physical device | Missing | No real signed IPA artifact, device-install run, launch run, or runtime-log run; docs/GOAL3_STATUS.md and docs/support-matrix.md. |
SSH Mac provider: 13 criteria
| # | Criterion | Status | Evidence |
|---|---|---|---|
| 1 | CLI can add and name an SSH Mac remote | Proven | Remote CLI and configuration in crates/cargo-ferry/src/cli.rs and cargo-ferry remote commands. |
| 2 | Host, port, user, identity, and workspace settings are validated | Implemented-unproven | SSH config/transport in crates/rustferry-ssh; local deterministic tests only. |
| 3 | Host identity is verified without insecure shell interpolation | Implemented-unproven | Argument-array transport and SSH validation in crates/rustferry-ssh; no live host proof. |
| 4 | Client and worker perform protocol/version/capability handshake | Implemented-unproven | crates/rustferry-ssh/src/provider.rs; no live host handshake. |
| 5 | Doctor reports remote macOS/Xcode/SDK/Rust readiness | Implemented-unproven | SSH doctor plus worker host checks in crates/rustferry-worker-macos/src/host.rs; local test transport only. |
| 6 | Deterministic source bundle is uploaded safely | Implemented-unproven | Snapshot bundle implementation/tests and docs/remote/source-bundles.md; no live transfer. |
| 7 | Remote workspace is isolated per job | Implemented-unproven | Dedicated snapshot session and cleanup logic in crates/rustferry-ssh; no real concurrent host jobs. |
| 8 | Remote worker performs an unsigned device archive build | Implemented-unproven | Dedicated snapshot session advertises unsigned/archive capability in crates/rustferry-ssh/src/provider.rs; no live Mac. |
| 9 | Shared BuildProvider.submit/events path works for SSH | Missing | Generic methods return UnsupportedCapability in crates/rustferry-ssh/src/provider.rs. |
| 10 | Live progress/log events are streamed | Missing | Dedicated capability set has events but not live logs; generic events are unsupported in crates/rustferry-ssh/src/provider.rs. |
| 11 | Cancellation is propagated and remote work stops | Implemented-unproven | Dedicated session cancellation exists; local tests only, generic provider path incomplete. |
| 12 | Signed IPA build and download work over SSH | Missing | Snapshot capability set is unsigned; generic artifact listing/download is unsupported in crates/rustferry-ssh/src/provider.rs; no live signed host. |
| 13 | Artifact SHA verification and remote cleanup are proven | Implemented-unproven | Dedicated session includes download/cleanup and local deterministic tests; no live artifact transfer/cleanup evidence. |
Parallel safety and integration
| Requirement | Status | Evidence |
|---|---|---|
| Baseline captured before Goal 3 work | Proven | docs/GOAL3_BASELINE.md records base d6887eb, baseline checks, and test counts. |
| Source checkout treated as read-only during isolated development | Implemented-unproven | Historical record in docs/GOAL3_ISOLATION.md; the wrapper is a guard, not an OS sandbox. |
| Goal 3 commands reject source-checkout paths | Proven | scripts/goal3-run:24-49. |
| Goal 3 uses separate target, cache, config, artifact, and temp roots | Proven | scripts/goal3-run:51-59. |
| Commands are recorded with operation IDs | Proven | scripts/goal3-run:61-95; docs/GOAL3_COMMAND_AUDIT.jsonl. |
| Every continuation shell command passed through the wrapper | Contradicted | One intermediate package test followed a wrapped command through an outer && and therefore escaped wrapper recording. It stayed in the mutable checkout, touched no source checkout, and all authoritative final tests/checks were rerun through goal3-run. |
Work remains on a dedicated Goal 3 branch and never lands on main/master during Windows development | Proven | Windows continuation uses goal3/windows-live-acceptance; no automatic master merge. Historical pre-handoff local merges remain recorded separately. |
Goal 3 commits consistently use the mandated goal3: prefix | Contradicted | Integrated commit sequence includes b32be13, 36ea042, and e850deb with conventional non-goal3: subjects. |
| Generated integration packages are absent from landing | Proven | dist/goal3-integration/, dist/goal3-ssh-snapshot-v1/, and dist/goal3-multi-target-signing-v1/ were removed; /dist/goal3-*/ is ignored. Generated handoff artifacts are not product source. |
| SSH continuation history is retained | Proven | Git history retains integration commits b32be13, 36ea042, and e850deb; no removed dist/ path is cited as current evidence. |
| Multi-target signing history is retained | Proven | Git history retains bounded multi-target signing at a339fff and selectable signed products at 2e0773a; generated replay packages are intentionally not committed. |
| Historical requested local merge is recorded | Proven | The handoff ancestry contains the SSH integration through e850deb, multi-target signing at a339fff, and selectable signed products at 2e0773a; Windows work continues on a dedicated branch. |
| Windows continuation branch is pushed to origin | Missing | The frozen source/workflow commits and named branch have not yet been published. master is not the development target. |
| Current Windows revision has CI/live acceptance | Missing | Frozen local/native gates pass; no Windows-originated GitHub/macOS run or current remote CI result is recorded. |
External and live blockers
These are validation blockers, not substitutes for missing implementation.
| Blocker / required evidence | Status | Evidence / minimum proof needed |
|---|---|---|
| Private GitHub repository with protected signing environment | Missing | Configure reviewed environment and run the protected sign job; current workflow evidence is static only. |
| Real Apple Developer team and accepted agreements | Missing | Required for bundle/device/profile operations and signed export. No account evidence is stored. |
| Real certificate, password, and matching provisioning profiles | Missing | Required for a live manual-signing run; synthetic fixtures do not count. |
| Registered physical iPhone and device UDID/profile coverage | Missing | Required for install/launch/runtime acceptance. |
| Successful protected GitHub signed run | Missing | Must cite run, job, commit, environment, and retained artifact IDs. |
Real signed .app, .xcarchive, .ipa, and main-app dSYM inspection | Missing | Must validate signatures, embedded profiles, entitlements, nested code, manifest/SHA values, real DWARF content, and executable-to-dSYM UUID equality. |
| Physical install and launch evidence | Missing | Must cite device, artifact SHA, install result, bundle launch result, and sanitized logs. |
| Live Windows client acceptance | Missing | Run from Windows with no Xcode/macOS tools and cite job/artifact IDs. |
| Live SSH Mac acceptance | Missing | Run handshake, doctor, upload, build, event stream, artifact verification, cancellation, and cleanup on a real remote Mac. |
| Personal Team path | Missing | docs/support-matrix.md marks it unsupported; requires separate capability and live validation. |
| Live app + Widget + Live Activity signed acceptance | Missing | Bounded per-target profile transport passes local integration tests; protected secret upload, signed artifacts, and device evidence remain absent. |
| Physical-device log streaming | Missing | Support matrix marks it unsupported; implementation and live proof both required. |
Other explicit completion gaps
| Requirement | Status | Evidence |
|---|---|---|
Full CLI families: jobs, apple, device, artifact | Missing | jobs and artifact are now implemented; the specifically requested top-level apple and device families remain absent, with current Apple/device behavior exposed through signing/devices/install/run/logs commands. |
Apple resource plan/apply, bundle-ID registration, and device registration | Missing | No corresponding command/client implementation; current signing models only consume existing metadata. |
| Reusable deterministic fake provider covering all required failures | Missing | Only protocol unsupported doubles and transport-specific fakes exist; see crates/rustferry-remote/tests/protocol.rs. |
| Required documentation package | Missing | GitHub provider/source/security, CLI, support, status, completion audit, and the complete docs/goal3-windows/ package now exist. The broader original docs/iphone/*, docs/security/*, and requested Goal 3 ADR path inventory remains incomplete. |
| Required README headline and complete from-any-computer quickstart | Missing | README.md retains the existing RustFerry headline and an unsigned/source-install path. |
| Phase-by-phase cold/warm/cache performance ledger | Missing | No source-manifest, bundle, upload, queue, build, sign, export, download, or client-verification measurements; docs/GOAL3_STATUS.md records the gap. |
| Honest support/status reporting | Proven | docs/GOAL3_STATUS.md says DoD incomplete; docs/support-matrix.md distinguishes live, synthetic, partial, and unsupported paths. |
Completion conclusion
Goal 3 is not complete. The historical Linux-to-GitHub-to-macOS unsigned archive path is live-proven. The Windows continuation completes durable jobs/history/sanitized logs/cancel/retry/prune, managed artifacts, explicit public GitSnapshot, metadata-only signing readiness, and VS Code control-plane integration with local and Windows-native evidence. It does not have a Windows-originated GitHub run, live GitSnapshot/cancel/retry result, real Apple Development signing, or physical-device evidence. Remaining product gaps include the Apple registration/resource CLI surfaces, physical-device logs, broader documentation/ADR inventory, live performance ledger, private protected signing setup, real signed artifacts, and install/launch/runtime validation. The branch must be pushed and opened as a Draft PR; no automatic master merge closes these blockers.
Developer Experience 0.2 status
Last updated: 2026-08-08
This is the live execution ledger for Developer Experience 0.2. “Implemented” requires a concrete path and tests. “Artifact-validated”, “runtime-validated”, and “device-validated” require progressively stronger evidence and are never inferred from source code.
| Milestone | State | Evidence / next gate |
|---|---|---|
| 0. Baseline and safety | Complete | Exact commit/toolchain/artifact digests recorded in NEXT_PHASE_BASELINE; that baseline revision, not the current final suite, recorded 158 passed, 0 failed, 7 ignored |
| 1. IDE protocol | Complete | Direct protocol v1 handshake/project/validate/doctor/check/devices/build/install/run/logs/schema; dirty-manifest stdin, rustc Problems, bounded lifecycle tests, checked-in schema equality, strict Clippy |
| 2–7. VS Code MVP | Complete | Native multi-root/trust-aware extension, diagnostics, wizard, trees, tasks, artifacts, devices and deploy UI; 42 base tests pass and 4 live-CLI tests skip without a supplied CLI, while all 46 pass with the final CLI; package/VSIX smoke, isolated install, and a real Extension Host smoke also pass |
| 8–14. Deployment | Implemented; runtime unobserved | Human and IDE devices/install/run/logs use the same typed ADB/simctl/devicectl services; physical iOS official signing/build/install/launch code exists; no device/runtime claim |
| 16. Runtime/package flow | Integrated; current release rerun in progress; publication pending | The workspace now has 10 members: nine publishable crates plus the non-publishable trusted worker. The full workspace regression and all 28 release-contract edges pass; fresh package/archive and publish dry-run evidence is still required before release. |
| 17. Assets/release hardening | Complete; runtime gate explicit | SHA-256/tamper-safe concurrent cache; five Android densities and splash artifact-inspected; iOS compiled catalog implemented/tested; zero-runtime SDK-only .app built and inspected |
| 18–21. VSIX/tests/CI/release | Integrated baseline accepted; SSH continuation rerun pending | The 44,435-byte, 18-entry VSIX was smoke-tested (ba8cac7e…3d183ce6d). Final integrated CI 31261962607 passed five Linux/Rust/macOS/Windows jobs at 607fe78; the SSH/source-bundle continuation still needs its own final exact-commit CI and package matrix. |
Current validation levels
| Surface | Implemented | Artifact | Simulator/emulator | Physical device |
|---|---|---|---|---|
| Existing Android build | Yes | Baseline CI pass | Not validated: no emulator | Not validated: no device |
| Existing iOS Simulator build | Yes | Baseline CI pass | Not validated: no installed runtime/device | N/A |
| IDE protocol | Yes | Schema/fixture parity | N/A | N/A |
| VS Code extension | Yes | VSIX installed and enumerated by VS Code CLI | N/A | N/A |
| Devices/install/run/logs | Yes | Validated build metadata required before deployment | Not validated: no emulator/Simulator runtime | Not validated: no attached device |
| Physical iPhone local signing/build | Yes, official development flow | Not validated: no Team/identity | N/A | Not validated: no attached device |
| Physical iPhone remote build | Yes, exact-revision GitHub macOS compile and automatic client validation; named SSH snapshot path is unsigned-only | Real unsigned .xcarchive validated through GitHub at final integrated head; no live SSH artifact | N/A | Signed IPA and device runtime not validated |
| Platform assets | Yes | Android densities/splash and iOS SdkOnlyResources inspected; Assets.car pending a runtime-equipped host | Not required for build-only evidence | N/A |
Decisions and constraints
- Actual product naming is RustFerry:
cargo-ferry,cargo ferry, andferry.toml. - Existing build pipelines remain the source of truth; IDE and deployment paths call shared Rust services.
- Protocol stdout is versioned JSON/NDJSON only; raw tool output stays out of the protocol stream.
- The unpublished bundled registry version is reported as unusable by the handshake until the compile-time published-runtime version matches it.
- VS Code uses native QuickPick, TreeView, Tasks, Problems, Output, and progress APIs; no webview.
- Untrusted workspaces cannot execute project or tool commands.
- Trusted file-backed Remote SSH, WSL, Dev Container, and Codespaces workspaces execute the extension and CLI remotely and see only remote SDKs and devices; virtual/untrusted workspaces are browse-only.
- No Apple credentials in settings/input boxes; no silent provisioning changes.
- No device command implies install/run success when hardware is absent.
Remaining release gates
- Build and inspect the
CompiledCatalogiOS path on a host with an available Simulator runtime; keep the current SDK-only evidence distinct. - Exercise emulator/Simulator install, launch, logs, and UI when runtimes are available; keep absent-hardware claims explicit.
- Validate physical signing, profile selection, recursive entitlements, install, and launch with an authorized Team and attached iPhone.
- Run live SSH/OpenSSH-to-macOS unsigned acceptance from the exact continuation revision; keep it separate from the accepted GitHub unsigned path and the still-pending real signed-IPA gate.
- Require exact-SHA GitHub CI for every release revision.
ADR-001: Slint as the first UI backend
- Status: accepted for the initial vertical slice
- Date: 2026-07-31
- Version evaluated: Slint 1.17.1
Context
The application author must write Rust only, while generated hosts may contain minimal native glue. The first backend needs maintained Android and iOS support, touch/text/buttons/state/async behavior, and a path to a direct no-Gradle Android artifact. Building a renderer, text stack, or widget toolkit is out of scope.
Decision
Use Slint 1.17.1 with inline slint::slint! UI in src/app.rs. This keeps the starter’s interface and callbacks visible in the first file an application author opens. The runtime/service layer remains backend-neutral so another UI integration can be added later without changing platform capabilities.
The project MSRV is Rust 1.92, matching Slint 1.17.1. Android targets API 26 or later, matching Slint’s supported minimum. Platform glue remains generated and outside user source.
Evidence
| Candidate | Android | iOS | Main issue |
|---|---|---|---|
| Slint 1.17.1 | First-party android-activity backend and mobile lifecycle integration | First-party Winit/Skia instructions | License choice and attribution must be explicit; mobile Skia clean builds are heavy |
| Dioxus 0.7.9 | First-party mobile workflow | First-party mobile workflow | Stable Android workflow generates Gradle and Kotlin, conflicting with the default no-Gradle requirement |
| egui/eframe 0.35 | Official Android example | eframe does not list iOS as supported | iOS path and mobile IME/safe-area UX are weaker |
| raw Winit/android-activity | Platform event loop | Platform event loop | Not a UI toolkit; choosing it would require the prohibited custom renderer/widgets/text stack |
Primary references:
- Slint Android
- Slint iOS
- Slint mobile integration
- Slint backends and renderer tradeoffs
- Dioxus mobile guide
- egui Android example
- Winit platform support
Licensing consequence
Slint 1.17.1 is triple-licensed under GPL-3.0-only, the Slint Royalty-free License 2.0, or a commercial license; it is not an MIT/Apache runtime dependency. The royalty-free path covers desktop, mobile, and web applications when the application either exposes AboutSlint from its top-level UI or displays the Slint attribution badge on a readily discoverable public page. It excludes embedded systems, standalone Slint distribution, and applications that expose Slint APIs. The GPL path carries GPL source and redistribution obligations; the commercial path covers proprietary embedded applications and applications that cannot meet the royalty-free conditions.
Generated starters retain AboutSlint, but RustFerry does not select a license for downstream applications. Before distributing a generated binary, its author must select a Slint path and audit the application’s resolved dependency tree. The pinned royalty-free text and full dependency policy live in LICENSES/ and Third-party licenses. Canonical versioned terms remain in Slint 1.17.1 LICENSE.md and its LICENSES directory.
This ADR is technical context, not legal advice. Applications with incompatible distribution requirements can use a commercial Slint license or a future backend once implemented.
Android packaging consequence
Slint’s Android backend compiles an internal Java helper and DEX during its Cargo build script. The direct packager must consume Cargo JSON build-script output, merge dependency DEX with any enabled RustFerry bridge through d8, and package classes.dex. A NativeActivity manifest must not claim android:hasCode="false" when DEX is present.
Follow-up measurements
Record clean and incremental build duration plus stripped APK/.app sizes after the first artifacts. No unmeasured binary-size or startup promises are accepted.
ADR-002: Direct Android SDK/NDK packaging
- Status: accepted
- Date: 2026-07-31
Context
The primary user flow must produce an APK without a user-maintained Android Studio project, Gradle files, Java, or Kotlin. Build must not depend on a connected device. Slint and system capabilities can still require dependency DEX or generated JVM adapters.
Decision
Use the Android SDK/NDK tools directly: Cargo plus NDK LLVM for Rust native libraries, AAPT2 for resources/manifest, optional Javac/D8 for generated bridge and dependency bytecode, ZIP assembly, zipalign, and apksigner. Generated inputs and intermediates live below target/ferry/android/.
Gradle is not the default backend. Any future hidden-Gradle fallback must be capability-specific, explicit, generated outside user source, and explain why the direct path is insufficient.
Consequences
- cargo-ferry owns tool discovery, version coherence, argument construction, diagnostics, and packaging order.
- Cargo JSON output is the authority for exact native and build-script artifacts; recursive broad target scans are unsafe and ambiguous.
- AAPT2 output alone is not deployable. Native libraries/DEX must be inserted before alignment; alignment must precede signing.
- Success requires independent package, launcher, resource, DEX, ELF ABI, alignment, and signature checks.
- CI needs a real SDK, NDK, Rust Android target, Java, and Build Tools. A cancelled or skipped artifact job creates no platform evidence.
Security
Every external process uses an executable plus argument array, timeouts, checked exit status, constrained working directory, captured diagnostics, and redacted secrets. Archive entry names and output paths are validated before use.
See Android without Gradle and Threat model.
ADR-003: Generated minimal platform bridges
- Status: accepted
- Date: 2026-07-31
Context
Android and Apple expose several required APIs only through platform frameworks, lifecycle components, or extension targets. Requiring application authors to maintain Java, Kotlin, Swift, Objective-C, manifests, plists, or Xcode targets would break the Rust-only application-source contract.
Decision
Allow small deterministic adapters generated below target/ferry/. Public application APIs remain typed Rust and backend-neutral. Android adapters may use framework Java/JNI/DEX; Apple adapters may use Swift/Objective-C/C, Xcode targets, WidgetKit, ActivityKit, and UserNotifications.
The bridge contains conversion and lifecycle plumbing only. Application state and business rules stay in Rust. A platform backend advertises support per operation; absent adapters return Unsupported rather than no-op success.
Consequences
- The project says “application code is Rust-only,” not “the complete binary is pure Rust.”
- Generated bridge sources need golden/determinism tests; final bridge presence needs artifact inspection.
- Panics/exceptions, thread affinity, ownership, and shutdown form an explicit FFI contract.
- Restricted widget/Live Activity snapshot models are preferable to a custom renderer or arbitrary native plugin surface.
- An advanced platform escape hatch may be added only with narrow lifetimes and explicit portability warnings; it is not the default architecture.
See FFI safety, Custom platform code, and Threat model.
ADR-004: VS Code debugging
Status: accepted for Developer Experience 0.2
Decision
The accepted Developer Experience 0.2 surface includes Build, Install, Run, and application-filtered Logs. It does not contribute a RustFerry: Debug command, a debug type, or a generated launch.json.
A debugger integration will use an existing LLDB extension rather than a RustFerry Debug Adapter Protocol implementation. It becomes eligible for the stable editor surface only after an artifact produced by RustFerry passes all of these checks on the target platform:
- a source breakpoint in application Rust code is hit;
- stack frames and local variables are readable;
- generated-host frames do not break source mapping;
- cancellation and process cleanup leave no debug server or forwarded port;
- Android and Apple signing/deployment behavior remains identical to the normal Run path.
Rationale
CodeLLDB is the appropriate first integration candidate, but RustFerry still needs platform-specific launch and attach proof. Android requires an NDK-compatible lldb-server, package/process selection, symbol preservation, and path mapping. iOS Simulator requires Xcode/LLDB attach behavior and verified Rust source mapping. Physical iOS adds signing, Developer Mode, device transport, and Xcode restrictions.
This repository has artifact evidence for Android and iOS Simulator builds, but no emulator, Simulator runtime, or physical device was available for breakpoint validation. Calling Run through a debug-shaped command would therefore create a false capability.
Follow-up gate
Start with an opt-in iOS Simulator or Android prototype against the existing validated-artifact and explicit-device model. Record the exact CodeLLDB version, generated dynamic configuration, breakpoint result, stack trace, source mapping, symbol policy, and cleanup result before exposing F5 or documenting debugger support.
Execution plan
This plan is ordered by executable product risk. A milestone closes only when its stated command and artifact checks pass; generated files alone are not completion.
1. Foundation
- Strict, versioned
ferry.tomlmodel and JSON Schema. - Typed CLI output in human and stable JSON forms.
- Atomic
cargo ferry newwith shared templates and capability fragments. - Starter with real UI, state, lifecycle, async, network, storage, error, permission, and notification examples.
- Host-only
cargo ferry checkandrustferry::testing::TestRuntime.
Acceptance: a newly generated starter completes cargo check without Android SDK, Xcode, emulator, or device.
2. Direct Android artifact
- SDK/NDK discovery and actionable doctor results.
- Rust
cdylibcross-compilation forarm64-v8a. - Deterministic manifest, resources, native host, and optional JVM bridge generation.
aapt2compile/link, optionaljavac/d8, APK assembly, zip alignment, debug signing, and independent artifact verification.
Acceptance: a signed, aligned APK containing the correct application ID, launcher, resources, ABI library, and required DEX entries. No Gradle or user-authored Java/Kotlin in the generated project.
3. iOS Simulator artifact
- Xcode/SDK discovery and Rust simulator cross-compilation.
- Deterministic hidden Xcode host, plist, assets, and extension targets.
.appbuild and independent bundle/executable/architecture validation.
Acceptance: a simulator .app with the expected identifier, executable, linked Rust code, and resources. No Xcode project in the user project.
4. Runtime and bridges
- Lifecycle/events, network path versus probe, atomic storage, haptics, clipboard, share, system information, deep links, and permissions.
- Platform bridge callbacks with typed errors, main-thread delivery, teardown safety, and no panic crossing FFI.
- Deterministic mock backends for application tests.
5. Notifications and extensions
- Local-notification permission, schedule/show/cancel/query/open flows on Android and iOS.
- Restricted widget snapshot model, Android provider, WidgetKit extension, and shared state.
- Restricted Live Activity snapshot model, ActivityKit extension, and Android ongoing-notification fallback.
Acceptance requires platform components in inspected artifacts plus compiling examples and documentation. Runtime/device status remains separate.
6. Deployment and hardening
- Optional install/run/log/device commands, never coupled to
build. - Content-addressed generated/build cache and narrowly scoped clean operations.
- Linux/macOS/Windows CI, platform artifact jobs, docs, security review, and release packaging.
- Final acceptance run and factual support matrix.
Current environment constraints
- Host: Apple Silicon macOS 26.5.2, Rust 1.96.0, Xcode 26.6 with iOS 26.5 SDK.
- Android SDK platforms/build-tools and NDK 29.0.14206865 are installed.
- Rust targets
aarch64-linux-androidandaarch64-apple-ios-simare installed. - No iOS Simulator runtime/device is installed. Build-only artifact validation can proceed; install/launch validation cannot.
- Physical-device signing, provisioning, installation, and launch are not assumed.
Implementation status
Last updated: 2026-08-13
Source and documentation now use RustFerry. Platform artifacts run 30719811812 at commit 8ed0192 produced and inspected current RustFerry-named Android and iOS artifacts. Exact pre-rename cargo-pocket/Pocket* paths, identifiers, symbols, and hashes remain below as historical evidence.
Goal 3 landed on master through PR #8 at 088dedfd1462875f69584db738f5626680b02c91, followed by the trusted-master acceptance wiring in PR #10 at 607fe78cf1ae22f8c569fb48d067d8478f407883. That exact head passed Linux-to-macOS unsigned iPhone acceptance; worker run 31262066567 completed unsigned Phase A and cleanup. Protected signing Phase B was skipped because no real PKCS#12 archive, password, provisioning profile, distinct private execution repository, or device was available. Final CI run 31261962607 passed all five jobs on attempt 2 after a transient runner failure, including Windows workspace tests and starter generation/check.
Android source beta acceptance
The Android APK pipeline is artifact-validated on a GitHub-hosted Ubuntu runner. The user-owned application project remains Rust-only. It does not require Gradle or an Android Studio project. The accepted build used local-workspace path runtime mode. Installation, launch and runtime behavior were not validated.
Acceptance run 31590994094, job 94095650729, retained artifact 9139312833, and production source ed45328d6fc375e81b20ab10c1014c4b8d224a85 produced rustferry_android_ubuntu_acceptance.apk. The retained artifact was downloaded again and all 18 entries in checksums.txt matched. Exact APK evidence:
- size: 25,031,645 bytes;
- SHA-256:
4dfe658492d724ed3320a3221de9c4c407df0a6d4077431d7b72ab85d99f3088; - package and launcher:
org.rustferry.ubuntuacceptance,org.rustferry.bridge.FerryActivity; - Rust target and ABI:
aarch64-linux-android,arm64-v8a; - NDK and SDKs:
29.0.14206865; compile 35, minimum 26, target 36; - APK Signature Schemes v2 and v3: passed; certificate SHA-256
353336761cef79fa6df1e04d6782492bf8c9e3e18f62f2fbc05c52eb069104da; - basic ZIP alignment and 16 KiB ZIP alignment: passed;
- user-owned Gradle project: absent.
This artifact validates only exact Android production source ed45328d6fc375e81b20ab10c1014c4b8d224a85, not the later PR head. Later artifact/store/test commits have separate Windows Cargo evidence at pre-documentation head 0ff643f3ce2baf9a28cf0519ad7a825ecd09cbad: the cargo-ferry library suite passed 145 tests with 0 failed and 0 ignored in 3.79 seconds; the exact prune-publication test passed in 0.24 seconds (563 ms wall); eight related prune tests passed in 1.68 seconds; cli passed 42 tests in 50.70 seconds; artifact_cli passed 9 tests in 0.42 seconds; jobs_cli passed 11 tests in 0.44 seconds; and cargo check -p cargo-ferry --all-targets --all-features passed.
Distribution remains source beta / developer preview. Registry-based starter generation remains unavailable until the internal RustFerry crates are published.
Windows-native jobs, artifacts, snapshot, and IDE paths have focused test coverage. A current-revision local Windows APK artifact is not claimed because the upstream skia-bindings full-source Windows build blocked local packaging. Windows-originated GitHub/macOS iPhone acceptance remains pending.
iPhone builds require a local or remote macOS host with full Xcode and the official Apple toolchain. The live-proven result remains an unsigned physical-iPhone XCArchive. No development-signed IPA, installation, launch, logs, or physical-device runtime is claimed.
Current continuation
The current continuation after 607fe78cf1ae22f8c569fb48d067d8478f407883 adds deterministic
source-bundle inspect/create/verify commands, automatic remote routing for physical-iOS builds on
non-macOS hosts, and SSH snapshot session v1. Named SSH remotes use create-only private config, an
exact pinned host key, an operation-owned known_hosts snapshot, a retained identity-file handle,
fixed OpenSSH argument arrays, strict protocol-v1 envelopes, and bounded timeout/cancellation
cleanup. The data plane binds a deterministic source snapshot to a real unsigned physical-iPhone
compile request, streams ordered events and the sealed XCArchive, independently verifies and
create-only publishes it before receipt, and requires capability-bound non-retaining worker cleanup.
Unix config and operation data use restrictive modes. Windows managed endpoint config objects and
per-operation source/trust directories use protected owner-bound DACLs and retained-handle
verification. Core all-target/strict-Clippy and rustferry-ssh library Windows cross-checks pass;
native ACL tests were not run on this macOS host, and the full cargo-ferry cross-check stops in
external vendored openssl-sys because Darwin Perl cannot configure VC-WIN64A. Cancellation and
timeout prove local cleanup but do not drain a terminal remote cleanup proof.
The SSH implementation has deterministic local protocol, process, client, worker, and adversarial
coverage only. No live Windows/OpenSSH interoperability, live SSH macOS compile, or SSH-produced
artifact has been validated. GitHub runs 31261962599 and 31262066567 remain the live no-Mac
unsigned XCArchive evidence; they do not validate SSH, signing, IPA, install, launch, device runtime,
or multi-tenant worker isolation.
Status terms:
- Implemented: backend code exists and deterministic tests pass.
- Artifact-validated: a produced APK,
.app, or.appexpassed structural/toolchain inspection. - Runtime-validated: behavior was observed on a simulator, emulator, or device.
- Blocked: a named environmental prerequisite is absent.
| Area | Android | iOS Simulator | Host tests | Evidence |
|---|---|---|---|---|
| Workspace/config | Shared model implemented | Shared model implemented | Targeted tests pass | Strict schema, semantic validation, CLI JSON/human tests |
| Starter generation | Implemented; all eight templates host-check | Implemented; all eight templates host-check | Actual CLI generation/check passed | Atomic generator plus six standalone projects |
| UI backend | Slint 1.17.1 target-compiled into a public-CLI APK | Slint 1.17.1 public-CLI .app artifact-validated | Seven Slint examples compile | ADR-001 |
| Build pipeline | Direct arm64 public-CLI APKs artifact-validated | Public-CLI starter, widget, and Live Activity artifacts validated | Build-plan/golden tests | Direct packager and generated Xcode host |
| Lifecycle/network/storage | Backends implemented; bridge compiled into inspected APK; runtime unobserved | Backends implemented; framework artifact-inspected; runtime unobserved | Models, mocks, and examples pass | TestRuntime plus platform bridge tests |
| Local notifications | Backend/receiver implemented and artifact-inspected; runtime unobserved | Backend implemented and framework artifact-inspected; runtime unobserved | Model and complete mock flow pass | Notifications example plus generated-bridge tests |
| Widget | Provider/backend implemented and artifact-inspected; runtime unobserved | State publisher, WidgetKit .appex, and framework artifact-inspected | Snapshot model/example pass | Android probe plus combined iOS extension app |
| Live Activity | Ongoing-notification fallback enabled in an inspected APK; runtime unobserved | ActivityKit lifecycle bridge and .appex artifact-inspected | State model/example pass | Public-CLI Kitchen Sink plus Live Score |
| Devices/install/run/logs | Typed ADB services and IDE protocol implemented; one Calculator APK launched and exercised on a physical device | Typed simctl services and IDE protocol implemented; no Simulator runtime/device | Service, parser, protocol, schema, fixture, and CLI tests pass | Calculator startup, JNI dispatch, and interaction observed; generic install/run/log flow and broader runtime remain unvalidated |
| Physical iOS development — GitHub | N/A | Local official signing/build/install/launch plus exact-revision GitHub macOS compilation, bounded app/extension manual profiles, automatic client download, and selectable signed app/archive/main-app-dSYM transport implemented | Signing, deployment, protocol, provider, worker, and cross-platform artifact tests pass, including multi-profile signing input, the exact five-file default, request-derived optional sets, tree equality, and dSYM UUID binding | Real GitHub remote unsigned archive validated; development-signed IPA, signed XCArchive, dSYM, Organizer/export round trip, extension behavior, install, launch, and device runtime not validated |
| Physical iOS development — SSH | N/A | Pinned endpoint, handshake/doctor, snapshot upload, unsigned compile session, events/cancel, verified XCArchive return, receipt, and cleanup implemented | Deterministic local protocol/process/worker tests pass | No live SSH compile or SSH-produced artifact; signing, IPA, install, launch, and device runtime not validated |
| IDE and VS Code | Same CLI build/deploy service | Same CLI build/deploy service | Protocol v1 tests; extension TypeScript/lint; 42 base tests pass and 4 real-CLI tests skip when no CLI is supplied; all 46 pass with the final CLI | Installable VSIX and real Extension Host smoke-tested; no mobile runtime claim |
| Assets | Five-density launcher icons and splash integrated into an inspected signed/aligned arm64 APK | CompiledCatalog implemented/tested; runtime-free SdkOnlyResources integrated into an inspected signed arm64 .app | Source validation, SHA-256 cache integrity, concurrent publication, tamper rejection, packaging, and artifact tests pass | Full Assets.car artifact validation still needs an installed iOS Simulator runtime |
Real arm64 Android APK, iOS Simulator .app, and .appex artifacts have been produced and inspected from projects generated and built through the public CLI. The Calculator replacement has also been launched and exercised successfully on a physical Android device; broader runtime behavior remains unvalidated.
Local Windows Calculator acceptance
On 2026-08-13, the Rust-only examples/calculator project passed six arithmetic state-machine tests, cargo ferry check, host compilation, visual inspection, and a direct Windows Android build with SDK/Build Tools 36, NDK 29.0.14206865, JDK 17, and Rust 1.96.0. The build exposed and regression-tested Windows process-boundary fixes for canonical paths passed to Cargo, javac, D8, keytool, and apksigner, plus the ANDROID_NDK alias required by Skia.
The first debug artifact reached a physical device but crashed when Java dispatched its initial native event: NativeActivity had loaded the app entry point without registering the library for Java native-method lookup. The generated activity now calls System.loadLibrary with the validated app library name, and a regression test covers the generated source. The final rebased replacement artifact is examples/calculator/target/ferry/android/debug/calculator.apk (188,052,420 bytes; SHA-256 5ae21a074797f00ce480d3b375a2ba148cf91742e4037fad064b1c25f24267e8). Independent inspection verified the compiled System.loadLibrary("calculator") initializer, package com.example.rustferry.calculator, API 26 minimum/API 36 target, org.rustferry.bridge.FerryActivity launcher, zero requested permissions, v2/v3 signatures, 16 KiB-aware alignment, one classes.dex, five PNG icon densities, and lib/arm64-v8a/libcalculator.so as ELF64 AArch64 with the expected JNI callback export. A user-confirmed physical-device launch then verified startup, JNI event dispatch, and calculator interaction without the prior crash.
Developer Experience 0.2 evidence
- IDE protocol v1 implements direct JSON handshake/project/validate/doctor/schema and bounded NDJSON check/devices/watch/build/install/run/logs. Targeted protocol and black-box CLI tests, checked-in schema equality, and strict cargo-ferry Clippy pass. Dirty
ferry.tomlvalidation uses bounded UTF-8 stdin and never writes the editor buffer to disk. - Human CLI exposes
devices,install,run,logs,signing teams, andassets check/generate.cargo ferry logs --json-streamshares the live application-filtered protocol implementation; the default human command remains a finite snapshot. Install/run always rebuild and independently validate before selecting a device; explicit arbitrary artifact paths remain rejected until persisted validator metadata exists. - Physical iOS uses
aarch64-apple-ios, hidden Xcode generation, Apple Development signing, explicit Team selection, opt-in provisioning updates, and post-build recursive verification.cargo ferry build ios --device --team ABCDE12345 --dry-runproduces the same side-effect-free official-tool plan without requiring Xcode, including in Ubuntu integration tests; no signed artifact was produced. - Generated projects default to the exact registry version with no checkout path. Explicit registry, workspace, and canonical local-path modes plus independent
--display-nameare covered by generator and black-box CLI tests. RustFerry 0.1.0 is public across all nine publishable crates. A clean isolatedcargo install cargo-ferry --version 0.1.0 --lockeddownloaded the complete registry graph, the protocol handshake reportedsource=registryandusable=true, and a newly generated exact-registry project passedcargo ferry check. - The VS Code extension passed TypeScript, ESLint, all 46 tests across 12 files with the final CLI supplied,
npm auditwith zero findings, VSIX packaging/content checks, an isolated VS Code CLI install/list smoke, and a real Extension Host smoke. Without a supplied CLI, the same suite passes 42 base tests and skips 4 live-CLI tests. The host proved ordinary Rust workspaces stay inactive and Ferry workspaces auto-activate, discover, validate, and diagnose an unsaved manifest without changing the saved file. The integrated VSIX has 18 entries, is 44,435 bytes, and has SHA-256d7dc5fc4abc60ac8b1068ec89439274d2067585cc76b3c967c9224ccccfafada. - Marketplace publication has a manual protected workflow that binds a successful draft-release assembly to the exact
masterrevision, verifies the retained VSIX and checksums before approval, and exposes the temporary PAT only tovsce publish --pre-release. Marketplace version 0.1.0 is publicly listed. The protected workflow was not rerun for the final assembly because itsVSCE_PATenvironment secret is not configured. - Draft-release run
31693149961atb1726fa33cb8419b1ef90bb142880c1e4e8b2acbremains private historical evidence only. Publication used assembly run31701148625from final release revision3218e78aad377bd15a8eb4b8b263a156b62a618f; all 13SHA256SUMSentries passed, including nine crate archives, the versioned VSIX, protocol schema, license bundle, and Slint-aware release notes. - Release revision
3218e78aad377bd15a8eb4b8b263a156b62a618fpassed exact-SHA CI, VS Code, platform-artifact, docs, CodeQL, package, archive-source, license, Rust 1.92, and publish dry-run gates. All nine crates were published with matching API and CDN checksums, correct ownership and metadata, non-yanked 0.1.0 versions, and successful docs.rs pages. Annotated tagv0.1.0targets that revision, and the public GitHub pre-release contains installation, package, validation-limit, and Slint attribution context.
Asset integration has two separately reported Apple modes. An available iOS runtime selects CompiledCatalog, which emits Assets.xcassets and requires Assets.car; generation, project wiring, cache consumption, plist selection, and rejection tests pass, but this host could not produce that artifact because no runtime is installed. With zero runtimes, SdkOnlyResources produced a real Xcode-built arm64 .app; inspection verified exact source PNG bytes, plist references, Cargo Mach-O identity, resources, and strict/deep ad-hoc signing without claiming a compiled catalog. The Android integration test produced and inspected all five launcher densities plus the splash in a v2/v3-signed, 16 KiB-aligned arm64 APK.
Current RustFerry artifact evidence
At commit 8ed0192, Platform artifacts run 30719811812 generated default Starter and Kitchen Sink projects with the public CLI, then built and independently checked both platforms:
- Android: both arm64 APKs passed ZIP integrity, v2/v3 signature, 16 KiB-aware alignment, package/launcher/API, DEX, resources, and AArch64 ELF checks. The Kitchen Sink APK also passed permission, deep-link, notification, provider, widget, and Live Activity fallback inspection.
- iOS Simulator: both arm64
.appbundles passed plist, resource, architecture, and deep/strict ad-hoc signature checks. Inspection coveredFerryRuntimeBridge.framework, its required exports and application hook, embedded WidgetKit and ActivityKit.appexproducts, exact identifiers, framework linkage, and application-group entitlements.
This is artifact validation only. The workflow did not boot an emulator or Simulator, install or launch either application, or exercise behavior on a physical device.
Integrated physical-iPhone work
The merged Goal 3 implementation adds a deterministic unsigned aarch64-apple-ios archive
planner/executor, strict cross-platform .xcarchive and IPA validators, and a split GitHub provider
with public source and private signing-execution repositories. Final master Linux acceptance run
31261962599 completed successfully at exact source head
607fe78cf1ae22f8c569fb48d067d8478f407883 and dispatched macOS worker run 31262066567. The
Linux client had no Apple toolchain; worker Phase A verified the trusted worker and immutable
request/source, compiled and sealed the real unsigned physical-iPhone archive, uploaded the handoff,
recorded its digest, and cleaned up. The client automatically downloaded and verified the result.
Acceptance artifact 9023136948 has API digest
sha256:6d98251ad82f98324b4df36799b71bb8f9f6d8523f8346757e4f7d9bcf1188c3; the inner archive
SHA-256 is ff532b50839eca54bb498393ac75929b951204f4b13a772c5e2bee96c36b2dc3. This is live no-Mac
compile and unsigned-artifact evidence, not a signed or runtime result.
The integrated local physical-build path no longer trusts a bare cargo, xcrun, security, an
ambient PATH, or an unvalidated DEVELOPER_DIR. It binds canonical executables, pins the system
Apple-tool entry points, propagates the validated Developer directory to each Apple invocation, and
has regression coverage for relative paths, directories, and symlink substitution. Cross-platform
dry-run planning remains available without an Apple toolchain.
Manual GitHub signing setup now accepts at most three application/extension profiles. An
extension-free app retains legacy --profile PATH; app/Widget/Live Activity projects require an
exact repeatable --profile TARGET=PATH for every generated target, with one common selected device.
The client locally validates the Apple Development PKCS#12 archive and each development profile,
accepts bounded secure password sources, verifies the exact protected-Environment policy and empty
initial secret set, uploads only after confirmation, and persists local signing configuration last.
The application keeps the legacy profile secret name, extensions use canonical static target-derived
names, and multi-profile jobs use bounded RFSIGNV2 input while legacy input remains single-app-only.
The modern workflow and worker also bind the complete public target graph through a canonical
SHA-256. The affected-package integration suite for this continuation passes locally. A real
development-signed IPA acceptance still requires external Apple certificate/profile/device assets
and a distinct private execution repository. Local physical install/launch services exist, but
signed extension artifacts, acceptance of a downloaded remote artifact, and physical-device runtime
remain unvalidated.
Local physical-device compilation is unavailable because this host lacks the aarch64-apple-ios
Rust target and installed iPhoneOS platform component. The validated remote path does not depend on
that local toolchain.
Recorded checks
The local results in this subsection predate the rename and remain historical host/test evidence. The Platform run above supplies current rename-integration artifact evidence; commands below use RustFerry names for reproduction.
cargo test -p rustferryand its doctests passed.- Every Rust fence across the 20 cookbook pages compiled and ran through
rustdoc --test. cargo check --all-targetsand the focusedTestRuntimeintegration test passed for Counter, Network Guard, Notifications, Widget Counter, Live Score, and Kitchen Sink.- Actual CLI generation followed by
cargo ferry checkpassed forstarter,minimal,counter,network,notifications,widget,live-activity, andkitchen-sinkwith the source runtime override. - All six example
ferry.tomlfiles passedcargo ferry config validate. - The packaged CLI source list contains all 16 embedded documentation files, and an isolated checkout without the repository-level
docs/directory compiled successfully. - The three command runners and shared process-control crate pass
x86_64-pc-windows-msvcall-target checks; the Job Object runtime regression is compiled but was not executed on this macOS host. - GitHub Actions YAML parses and passes
actionlint; finalmasterCI run31261962607passed Linux quality/docs, Rust 1.92, Ubuntu, macOS, and Windows on attempt 2, repeating workspace, package, template, example, Rustdoc, cookbook, Markdown-link, and mdBook checks. Windows workspace tests and starter generation/check both passed.
Template/configuration benchmarks, cache calculation and no-change planning observations, and the Android second-build cache assertion are recorded in Measurements.
Historical Android artifact evidence
Before the RustFerry rename, the public CLI produced target/final-acceptance-starter/target/pocket/android/debug/final_acceptance_starter.apk and target/final-acceptance-kitchen/target/pocket/android/debug/final_acceptance_kitchen.apk from freshly generated projects. Independent inspection verified:
- APK ZIP integrity, v2/v3 APK signatures, and 16 KiB-aware ZIP alignment;
- packages
org.cargopocket.acceptanceandorg.cargopocket.kitchensink, API 26 minimum, API 35 target, generated icon/resources, andorg.cargopocket.bridge.PocketActivityas asingleToplauncher; - the compiled icon and splash resources byte-match both then-current project inputs (SHA-256
751ec3d49aff1e091c1fe0037060cd71701e3b03fd55031b078201afd10b7464); classes.dexwith the generated activity, file provider, notification receiver, and widget provider classes, each matching an exact manifest component;- the configured
acceptanceandkitchensinkdeep-link schemes; - the exact configured permission/component sets, including the enabled notification receiver, widget provider, and private file provider in Kitchen Sink;
- one
arm64-v8aELF64 AArch64 Rust library withandroid_mainand the JNI callback.
This is artifact evidence, not emulator/device behavior. The Kitchen Sink DEX contains the enabled start/update/end/list Live Activity fallback bridge and the inspected manifest contains its notification prerequisites.
Historical Apple artifact evidence
Before the RustFerry rename, the Xcode 26.6/iPhoneSimulator 26.5 build-only pipeline produced and independently validated through the public CLI:
- starter app:
target/final-acceptance-starter/target/pocket/ios/debug/final-acceptance-starter.app; - Kitchen Sink app:
target/final-acceptance-kitchen/target/pocket/ios/debug/final-acceptance-kitchen.app; PocketRuntimeBridge.framework, with the expected install name, arm64 executable, exported call/free/init/install/application functions, and application-delegate hook markers;- embedded
PocketWidgetExtension.appexandPocketLiveActivityExtension.appex, each with an arm64 executable, exact plist metadata, andcom.apple.widgetkit-extensionextension point; the Activity extension links the runtime framework by its exact@rpathinstall name.
Both rebuilt application bundles contain PocketIcon.png and PocketSplash.png that byte-match the then-current project inputs (SHA-256 751ec3d49aff1e091c1fe0037060cd71701e3b03fd55031b078201afd10b7464) and remain valid under codesign --verify --deep --strict after archival/restoration.
See Apple implementation status for identifiers, checks, and runtime limitations.
Toolchain inventory
- Rust/Cargo 1.96.0; host target
aarch64-apple-darwininstalled. - Xcode 26.6; iPhoneSimulator 26.5 available. An iPhoneOS 26.5 SDK directory is discoverable, but
xcodebuildreports its platform component is not installed. - Android SDK roots resolve to
~/Library/Android/sdk; platforms 35 and 37.0, build-tools 34.0.0 and 37.0.0, and NDK 29.0.14206865 are available. aapt2,d8,zipalign,apksigner,adb, Java 21,javac, andkeytoolavailable.- Rust targets
aarch64-linux-androidandaarch64-apple-ios-simare installed;aarch64-apple-iosis absent locally. No Android emulator/device, iOS Simulator runtime/device, Apple signing identity, Team, provisioning profile, or attached iPhone was available. Remote unsigned physical-device artifact validation is recorded above.
Threat model
Assets
- User source, assets, configuration, and existing build output.
- Signing keys, passwords, provisioning profiles, and platform credentials.
- Integrity and provenance of generated APK and Apple bundles.
- Developer workstation SDKs and executable search paths.
- Editor workspace trust, diagnostics, quick fixes, tasks, and extension settings.
- Device identifiers, application logs, pairing state, and development-team metadata.
- Remote request identity, trusted worker provenance, sealed unsigned handoff, and downloaded artifact integrity.
- SSH endpoint and trust snapshots, identity-file path/handle, operation/job IDs, snapshot descriptor and archive, local artifact spool/publication, receipt, and worker-root cleanup state.
Trust boundaries and entry points
- CLI arguments,
ferry.toml, Cargo metadata, filenames, assets, URLs, environment overrides, and external tool output are untrusted input. - Building a project executes its Cargo build scripts and procedural macros with the developer account’s privileges; do not build an untrusted project outside an isolated environment.
- Rust/native/JVM/Swift callbacks cross memory-management, exception, panic, and thread-affinity boundaries.
- SDK, NDK, Xcode, signing tools, devices, and emulators are external processes or systems.
- Generated archives and bundles are inspected independently before success is reported.
- The VS Code extension and
cargo-ferrycommunicate across a child-process boundary. Every stdout line, protocol version, operation ID, path, diagnostic range, and external-tool field is untrusted even when the extension started the process. - A workspace can contain malicious build scripts, symlinks, configuration, tasks, and very large output. Opening a folder is not consent to execute it.
- A selected device is a separate trust domain. Its reported name is display-only; stable IDs and capabilities come from ADB, CoreSimulator, or CoreDevice and can become stale between discovery and deployment.
- Application logs may contain user data. Log collection is explicit, application-filtered, bounded, and never clears the platform log buffer.
- Remote iPhone builds cross public source-repository, private execution-repository, GitHub API, Actions runner, artifact-store, and local publication boundaries. The source project and its build scripts are untrusted; the worker revision, workflow bytes, request envelope, temporary ref, run identity, and every downloaded byte require independent binding.
- An SSH build crosses the local OpenSSH process, authenticated peer, full-duplex frame stream, source/archive transport, worker filesystem, event stream, artifact/receipt, and cleanup proof. Every peer byte remains untrusted after host authentication.
Required controls
- Canonicalize and constrain every generated or cleaned path below the expected project
target/ferryroot. - Invoke executables directly with argument arrays; check every exit status; preserve diagnostic logs; redact signing values.
- Never place private keys or passwords in
ferry.toml, generated source, process arguments when avoidable, or normal output. - Reject path traversal, malformed identifiers, unknown configuration fields, URL schemes outside
http,https,mailto,tel, andsms, missing purpose strings, and incompatible capabilities before expensive builds. - Catch panics at FFI entry points, translate platform failures to typed errors, document pointer ownership, and stop callbacks after runtime shutdown.
- Add only permissions, manifest components, plist keys, and entitlements required by enabled capabilities.
- Treat cache entries as untrusted until their key and expected outputs validate.
- Require VS Code Workspace Trust before project mutation, build, install, launch, logs, or custom executable settings. Virtual and remote workspaces must be rejected when local platform tools cannot safely operate on them.
- Parse only the negotiated IDE protocol version. Bound line length, retained output, diagnostic counts, and log bytes; reject malformed or trailing JSON. Give finite external operations explicit deadlines, keep intentional watch streams cancellable, and terminate the complete child process tree on cancellation or timeout.
- Keep CLI discovery deterministic. An explicit executable path must be a regular executable file; never invoke a workspace-controlled shell command or concatenate arguments into a command line.
- Apply structured quick fixes only to the file and version that produced the diagnostic, after checking the edit range. Capability changes remain CLI-owned and idempotent.
- Install and launch only artifacts carrying independent build validation metadata, then recheck
their path, type, identity, executable, archive structure, and physical-device signature before
deployment. Never infer trust from
.apkor.appsuffixes alone. - Select devices by exact ADB serial, Simulator UDID, or CoreDevice identifier. Ambiguous automatic selection fails closed; offline, unauthorized, unpaired, or capability-incompatible devices are typed errors.
- Physical iOS builds use the official Cargo/Xcode/codesign/provisioning path. Require an explicit Development Team; keep provisioning updates opt-in; reject ad-hoc signatures, team/profile/ entitlement mismatches, expired profiles, missing extensions, and non-arm64 device binaries.
- Split remote physical-iOS compilation from signing. Phase A may execute untrusted Cargo code but receives no signing secrets. Phase B receives only a digest-bound sealed archive, runs behind a reviewed protected Environment in a distinct private repository, and must prove temporary keychain, decoded-secret, workspace, and operation-ref cleanup before success.
- Bind remote submission to exact repository identities, source and worker commits, trusted ref, generated workflow digest, request digest, operation ref, run/attempt, artifact metadata, and final file hashes. Reject replacement, traversal, links, collisions, expansion bombs, stale runs, moved refs, visibility drift, or ambiguous cleanup. See GitHub macOS provider security.
- Snapshot source transport must select only workspace-contained Cargo inputs, apply non-overridable sensitive-path exclusions, and bind a deterministic ZIP to a separately transported versioned descriptor. Verification uses bounded fresh extraction and rejects path traversal, links, normalization collisions, archive expansion abuse, input replacement, and partial extraction. Archive and descriptor destinations remain outside the selected workspace so publication cannot mutate its own source plan. Each output is no-clobber; if descriptor publication fails after the archive is published, the client reports and retains that archive instead of deleting a path whose filesystem identity it can no longer prove. See source bundles.
- SSH endpoint fields, trust files, worker stdout/stderr, frames, and connection timing are untrusted
at the client boundary. Require one exact pinned host key in a dedicated regular
known_hostsfile, copy its canonical entry into a private operation-owned trust snapshot, and retain an identity path plus no-follow handle without reading key bytes. Reject OpenSSH expansion tokens; use one fixed argument array with forwarding and TTY disabled. Enforce versioned per-direction frame and event sequences, fixed payload/deadline bounds, and exact operation/job/provider/request/ source/artifact bindings. Stream large payloads with fixed memory. Publish only an independently inspected unsigned XCArchive through a create-only durable local inode before receipt; cancel on local failure, terminate/reap the SSH process on cancellation or timeout, and require exact non-retaining worker cleanup before success. Unix config/operation directories and files use0700/0600. On Windows, managed endpoint directories, endpoint files, and each operation directory use an owner-bound protected DACL for the current user, LocalSystem, and built-in Administrators. Retained base/child/file handles, reparse and link-count checks, create-only staging plus publication, and post-publication verification bind the object before its contents are trusted. See SSH Mac provider. - Validate PNG type, dimensions, opacity, byte bounds, canonical containment, and cache manifests
before generating platform assets. Generate below
target/ferry, reject symlink boundaries, and commit a complete fingerprint directory atomically. - Package the extension from an allowlisted manifest and inspect the VSIX contents. Exclude source
maps, tests, development paths, workspace data, secrets, logs, and
node_modulesfrom release artifacts.
Residual risks
- On cancellation or timeout, the SSH client sends a best-effort cancel frame, closes input, and terminates/reaps OpenSSH within a bound. Local staging cleanup is checked, but the client does not drain a terminal worker cleanup proof; inspect the worker retention root before retrying. Source archive preparation and local post-receipt commit/cleanup also have bounded operations rather than cancellation checkpoints at every filesystem step.
- On Windows clients, every random operation directory has its own atomic protected DACL; the
project-local parent session root need not be private.
CreateDirectoryWdoes not return a handle, leaving a narrow create-to-open window. A replacement must still have the exact current owner, protected allowlist DACL, ACL-capable filesystem, and non-reparse type to pass verification; the retained handle then denies delete sharing through all pathname-based writes, and cleanup marks that exact handle for deletion. Same-user or administrator interference remains inside the trusted local boundary. Output-parent replacement is likewise outside the cross-user boundary. - Windows endpoint-config and operation ACL code has native runtime tests; core all-target and
strict Clippy plus
rustferry-sshlibrary Windows cross-checks pass. Those tests were not executed on this macOS host, and a fullcargo-ferrycross-check stops in external vendoredopenssl-sysbecause Darwin Perl cannot configureVC-WIN64A. Native Windows/OpenSSH interoperability remains a validation gap, not an inferred security result. - The artifact receipt is accepted only after a validated artifact flush and client verification, but session v1 has no server challenge that proves receipt freshness cryptographically. A pinned worker that deliberately pre-buffers a valid receipt remains inside the trusted endpoint boundary.
- Deployment rechecks the validator-owned artifact digest immediately before invoking the native installer. A separate process running as the same user can still replace a pathname after that check and before ADB, simctl, or devicectl opens it. Those tools do not expose one portable descriptor-based install API, so this final cross-process TOCTOU window cannot be eliminated by the current design. Do not build or deploy alongside untrusted same-user processes.
- SSH snapshot builds execute Cargo build scripts and procedural macros as the worker account. The reference mode is single-tenant and carries no signing secrets. Hostile multi-tenant operation needs VM-equivalent isolation, an ephemeral filesystem/keychain, network and resource policy, complete process-tree termination, no host credentials, and no cross-job cache; the current local deterministic tests are not evidence for that deployment model.
Out of scope
- Jailbreaks, unsigned iPhone installation, signing bypass, arbitrary executable downloads, store upload, remote push infrastructure, native debugging, remote device farms, and attacks against third-party systems.
Security status is evidence-based in docs/STATUS.md; this document is not a claim that unfinished code is secure.
References
Primary sources used for platform and UI decisions:
Rust and Cargo
Slint
License terms can change; review the current source before distributing an application. ADR-001 records this repository’s technical decision and attribution default.
Android
- AAPT2
- D8
- zipalign
- apksigner
- Use the NDK with other build systems
- App manifest overview
- Notification runtime permission
- App widgets
Apple
- Xcode build system
- Building and running an app
- UserNotifications
- WidgetKit
- ActivityKit
- Bundle resources