11 KiB
AGENTS.md
Operational instructions for coding agents working in this repository.
Humans: see README.md for product overview and run configs.
Agents: read this file (and the nearest nested AGENTS.md) before editing.
Nested guides take precedence when editing under those trees:
crates/vnidrop/AGENTS.md— Rust coreshared/AGENTS.md— Compose Multiplatform UI / KMP
Project overview
VniDrop is a cross-platform local P2P file transfer app (Android, iOS, Desktop).
| Layer | Path | Responsibility |
|---|---|---|
| Rust core | crates/vnidrop/ |
Iroh endpoint, blobs, SQLite, tickets, approval, streaming |
| Shared KMP | shared/ |
Compose UI, ViewModels, expect/actual platform bridges |
| Hosts | androidApp/, iosApp/, desktopApp/ |
Thin app shells |
Invariant: UI/platform opens files and handles pickers; Rust streams bytes. Do not design features that move transfer payloads through Kotlin heap by default.
Domain docs (reference, do not paste into PRs):
Absolute rules
- Prefer PRs into
master. Do not merge tomasterlocally unless the user asks. - Do not
git push, force-push, or open a PR unless the user asks. - If
commit.gpgsignis enabled, create signed commits only. If signing fails (emptyssh-add -l), stop and tell the user to unlock the key. Never switch to unsigned commits to “unblock” yourself. - Change only files required for the task. No drive-by refactors, dependency bumps, or repo-wide formatting.
- Do not force architecture migrations (MVI, Hilt, Nav3, etc.) unless requested.
- Never commit secrets, key material, or passphrases.
- Destructive git (
reset --hard,push --force, dropping DBs) only with explicit user approval. - Every bug fix includes a regression test at the lowest layer that catches it.
- After code changes, run the relevant checks in Build and test and fix failures before finishing.
Build and test
Install prerequisites when missing: Rust stable + rustfmt + clippy, JDK 17, Android NDK/SDK only if building Android, Xcode only for iOS.
Rust core (crates/vnidrop or workspace root)
Run from the repo root (Cargo workspace):
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace --all-targets
Focused:
cargo test -p vnidrop
cargo test -p vnidrop --test output_sink
cargo test -p vnidrop --test transfer
cargo test -p vnidrop --test approval
cargo test -p vnidrop --test lifecycle
After finishing Rust edits, format:
cargo fmt --all
CI also runs cargo doc --workspace --no-deps with RUSTDOCFLAGS=-D warnings
(see .github/workflows/rust-core.yml). Run it before large Rust public-API changes.
Shared KMP / Compose (shared/)
./gradlew :shared:jvmTest
./gradlew :shared:compileKotlinJvm
Other targets (slower / machine-dependent):
./gradlew :shared:testAndroidHostTest
./gradlew :shared:iosSimulatorArm64Test # macOS + Xcode
./gradlew :androidApp:assembleDebug
./gradlew :desktopApp:run
Note: jvmTest CI currently runs on macOS. Gobley host cargo is enabled
for the current host and architecture, so local Linux and Windows builds embed
their matching desktop Rust library. Prefer macOS only when exact CI parity is
required.
What to run before finishing
| You changed… | Minimum verification |
|---|---|
crates/vnidrop/** only |
cargo fmt, cargo clippy … -D warnings, cargo test -p vnidrop |
| Cancel / export / sinks | Above + cargo test -p vnidrop --test output_sink |
shared/** only |
./gradlew :shared:jvmTest |
| Both | Rust suite + :shared:jvmTest |
| Docs only | No suite required; verify links/paths |
Do not kill long cargo / Gradle runs mid-flight unless they hang past several
minutes with no output; first builds are slow.
Repository map (edit here)
Rust runtime (keep split; do not re-merge into one file)
crates/vnidrop/src/runtime/
mod.rs # CoreInner, startup recovery, emit helpers
facade.rs # UniFFI VnidropCore + block_on
share.rs # import / share
receive.rs # receive, download, export, output sinks
lifecycle.rs # cancel, delete, status, access mode, shutdown
provider.rs # provider events, per-connection send progress
Other core modules: filesystem.rs, repository.rs, approval.rs,
handshake.rs, ticket.rs, access_policy.rs, event_hub.rs, api.rs.
Shared app
shared/src/commonMain/kotlin/com/vnidrop/app/
core/ # CoreGateway, models, pickers interfaces
feature/send|receive|approvals|settings|app/
ui/theme|components|navigation|feedback|state/
androidMain|iosMain|jvmMain/ # expect/actual implementations
Platform file rules (do not violate)
- Desktop / path-based iOS: paths; directory walk in Rust when
is_directory. - Android share: ParcelFileDescriptor file FDs only — never a directory FD. Folder share expands SAF trees in Kotlin to per-file FDs + relative names.
- Android receive default: MediaStore Downloads sink; custom trees via SAF write.
- Receive publish: no-overwrite temp + hard link / exclusive rename
(see
CORE_FLOW.md).
Code style
General
- Match surrounding code (naming, imports, error handling).
- Prefer small, reviewable diffs. Avoid files growing past ~800 LoC without splitting when adding substantial logic.
- Do not add one-off helpers used only once if an inline block is clearer.
- Prefer exhaustive
when/match; avoid wildcards that hide new cases.
Comments (strict)
Comment why, invariants, and platform/concurrency traps only.
- Do comment: cancel-before-await ordering, SAF/FD limits, security-scoped leases, “exactly one finish/abort after start_file”, durability rules.
- Do not comment: restating the next line, tutorial narration, section banners that repeat the function name, pasted docs from this file.
Rust
- Follow Clippy with
-D warnings(CI fails otherwise). - Do not hold
std::sync::MutexGuardor other guards across.await. - Prefer
Handle::block_onvia existingVnidropCore::block_onfor concurrent API entry; cancel signals active transfers synchronously before async work. - Prefer private modules; export only what UniFFI / other crates need.
- New public traits/types: short docs when the role is non-obvious.
Kotlin / Compose
For UI and presentation work, load and follow the in-repo skill:
.codex/skills/compose-skill/SKILL.md
- Open at most one
references/*.mdfile when the skill’s Quick Routing requires it. - Do not invent a second Compose style guide.
- VniDrop uses MVVM-style ViewModels (
*State+StateFlow+ named methods), not a forced MVIonEventbase — adapt, do not rewrite. - Theme via
LocalVniDropColors/VniDropThemeTokensonly. Brand primary (light): HSL271, 91%, 65%≈#A855F7. - Strings: CMP
Res.string.*/ composeResources — not AndroidRincommonMain. - Verify multiplatform target support before adding AndroidX/Jetpack deps to
commonMain.
Details: shared/AGENTS.md.
Testing instructions
- Prefer deterministic tests (gates, fixed sizes, public API fixtures).
- Avoid long sleeps; if polling is required: short interval + hard timeout + clear assertion message.
- Rust integration tests use public UniFFI API +
tests/support/only. - Failure paths: assert durable status and/or events when applicable, not only the error string.
- Do not add tests for pure static constants.
- Do not add negative tests for code you deleted.
- Prefer comparing whole objects when equality is meaningful.
Layout:
| Layer | Location |
|---|---|
| Rust unit / private | crates/vnidrop/src/tests/ |
| Rust integration | crates/vnidrop/tests/ |
| Shared logic | shared/src/commonTest/ |
| Shared Compose/JVM | shared/src/jvmTest/ |
Git and PR instructions
Branches
Name the change, not a roadmap step:
- Good:
feat/folder-share,fix/cancel-export-hang,docs/agents-md - Bad:
feat/step3-remaining,wip,temp
After a PR merges: delete the feature branch locally and on origin, then
branch from updated master.
Commits
- Style in history:
feat(scope):,fix(scope):,refactor(scope):,docs:,ci:. - Subject = outcome; body only when needed.
- Signed when repo requires it.
Pull requests
- Title matches the main change.
- Summary: short bullets of what/why.
- Test plan must be executable for this PR:
- exact commands, and/or
- 1–2 concrete scenarios that would catch a regression.
- No filler plans (“everything works”, “CI green”) without commands or scenarios.
Security considerations
- Treat tickets and endpoint IDs as sensitive enough not to log full blobs in production paths.
- Do not weaken approval/access checks for convenience.
- Do not store secrets in the repo; app data dirs and key files stay out of git.
- Be careful with file publish races (no-clobber rename/link policy exists for a reason).
Common tasks → start files
| Task | Start here |
|---|---|
| Share / multi-file / folders | runtime/share.rs, filesystem.rs, platform FileSystemService.* |
| Receive / export / sinks | runtime/receive.rs |
| Cancel / delete / stop share | runtime/lifecycle.rs, facade.rs |
| Per-receiver send progress | runtime/provider.rs, ui/state/AppUiModels.kt |
| Approvals | feature/approvals/, approval.rs |
| QR / NFC invitations | TransferShareActions.*, ReceiveInvitationActions.* |
| Theme / brand | ui/theme/VniDropTheme.kt |
| Compose skill | .codex/skills/compose-skill/SKILL.md |
Anti-patterns (never)
- Streaming multi-MB transfer data through Kotlin as the primary design
- Passing Android directory FDs into Rust
- Nested exclusive
Runtime::block_onthat deadlocks cancel during receive - Holding locks across
.await - Rebuilding a monolithic
runtime.rs - Flaky multi-minute sleeps in tests
- Unsigned commits when signing is required
- Force-push or secret commits without explicit user direction
Implementation checklist
- Read this file + nearest nested
AGENTS.md. - For Compose/UI: load
compose-skill. - Smallest correct change; tests for bugs/behavior changes.
- Run relevant build/test commands; fix failures.
- Sparse comments only where non-obvious.
- Commit (signed) / push / PR only as the user requests.
- Summarize what changed and what you ran.