- .gitignore matching sibling repos (target/, node_modules/, .worktrees/, Cargo.lock) - AGENTS.md on the alkcall/alksocks template: git workflow, 17 project conventions adapted for a virtual-filesystem crate (no forced host-FS access, metadata/content split, durability ordering, storage-trait one-way doors), verification commands (docs-only posture for Phase 0), and architecture context pointing at the ancestor research and family ADRs - docs/research/phase-0.md: initial Phase 0 draft — vision/scope sketch, guiding principles, what's already settled, prior art (alknet-filesystem POCs, iroh-blobs + external-store probe, git/git-lfs, alkgit backend seam, russh-sftp, SQLite/honker, rudolfs), 15 open questions (OQ-FS-01..15) grouped by theme, a 7-entry candidate POC register (proposals, none run), survey list, and an unconverged checklist Verification: docs-only repo — every referenced path, ADR id, and OQ id checked to exist (workspace paths, alkcall ADRs 034-051, alkgit research files); OQ numbering OQ-FS-01..15 complete with no gaps
16 KiB
AGENTS.md
Operating instructions for opencode agents working in this repo. opencode
auto-loads this file as instructions, overriding the built-in defaults for
this project. Custom agents in .opencode/agents/ inherit these rules
unless their own prompts say otherwise.
Git Workflow
Commit and push when reasonable. When a change is complete and
verified, commit and push to origin/main without asking. This overrides
the built-in default of "only commit when explicitly asked."
The workflow:
- Make the change
- Verify (once
src/exists):cargo test,cargo clippy --all-targets -- -D warnings,cargo fmt --check,cargo doc --no-depsif docs changed. While the repo is docs-only (Phase 0), verification is a careful re-read of the changed docs and a check that every referenced path/ADR/OQ id exists - Inspect
git statusandgit diffbefore staging — stage only the intended files, never secrets - Write a concise commit message matching the repo style (see
git log --oneline -10). For multi-point changes, use a summary line plus a body with bullet points and a verification block. git push origin main- Report the commit hash and the verification summary
Exceptions — do not commit or push without asking:
- The change is exploratory / speculative (you're not sure the user wants it kept)
- The user is actively reviewing the diff and may ask for changes
- The change touches a wire format or a trait shape that backends or consumers implement (one-way doors — see "Wire formats are stable" and "Backend/storage trait shapes are one-way doors" below; once consumers exist, those signatures are wire-stable contracts)
- You'd be force-pushing, amending a published commit, creating an empty commit, or skipping hooks
Never commit secrets, keys, or credentials. If a commit fails or hooks reject it, fix the issue and create a new commit — do not amend the failed one.
Git identity is preconfigured (glm-5.3-flash <glm-5.3-flash@alk.dev>). Do
not change git config, skip hooks, or use git commit -i.
Project Conventions (Rust / virtual filesystem crate)
This is the alkfs crate — a content-addressed, branch-aware virtual
filesystem for the alk family. The intended shape (pending Phase 0
convergence): a local storage engine (path-tree metadata + content-addressed
blob store) with a producer/consumer serving protocol on alkcall channels,
so that alkgit's object storage, the coming alksftp, and the alknet
filesystem vision all compose on the same substrate. It sits in the alk*
family: alkcall (call + channels — the substrate), alktty, alktunnels,
alksocks (the protocol-crate siblings), alkgit (the first in-family
storage consumer). The conventions below apply to all work in src/ and
tests/ once they exist. They mirror
.opencode/agents/implementation-specialist.md §Project Conventions and
are repeated here so they apply to every session, not just spawned
implementation agents.
-
No comments in code unless the user explicitly asks. This is a project-wide convention. Doc comments (
///,//!) are fine and expected on public API. Inline//comments only when the user asks or when a non-obvious safety/correctness constraint would otherwise be missed (e.g., durability ordering constraints — "the blob bytes must be durable before the path-tree row that names them commits, or a crash orphans the reference"). -
Error handling —
thiserrorfor library error types. No panics in library code. Nounwrap()orexpect()outside tests. If you reach forunwrap, the error path wasn't specified — stop and decide what should actually happen. For poisonedRwLock/Mutex, useunwrap_or_else(|e| e.into_inner())so a panic in one operation does not cascade to other operations. Filesystem and storage errors get faithful, typed mapping — never collapse a durability/corruption error into a generic one (a silent-corruption bug in a filesystem crate is the worst-case failure mode). -
tokiois the async runtime — all I/O is async. Use the wasm-clean tokio subset (rt,sync,io-util,macros,time) — do NOT usefeatures = ["full"]. CPU-heavy storage work (hashing, pack/pao assembly, fsync-heavy batches) goes throughtokio::task::spawn_blocking, the alkgit POC-2 pattern. Usetokio::syncprimitives (oneshot,mpsc) for lifecycle correlation;parking_lotfor short-held internal locks. -
WASM target is load-bearing by default, with an honest escape hatch — the family default (alktty/alktunnels/alksocks) is a wasm-clean protocol layer. A filesystem crate has heavier native gravity (real FS I/O, SQLite); the expected shape is a wasm-clean protocol/ops layer with the storage engine native-only behind features — mirroring how alksocks keeps socket I/O behind
local. Whether even the protocol layer stays wasm-clean is a Phase 0 question, not a settled convention; decide deliberately and record it. Runcargo check --target wasm32-unknown-unknownonce the split exists. -
Wire formats are stable — no wire format exists yet; when the first one is specced (the serving ALPN, its channel open-op params, any chunk/framing on the data path), it is a one-way door: decide via ADR before the first consumer exists, then additive-only. The ALPN string (provisional
alk/fs; final naming per alkcall ADR-004/006) and the params-object shape follow the alkcall ADR-039 precedent (paramsis ALPN-specific, interpreted by the open handler). -
Producer/consumer, not server/client — both sides of a channels connection can initiate. A producer exposes filesystem resources (registers openable channels via
ChannelCore::register_openable); a consumer opens them and speaks the file protocol inside. Both sides can be both simultaneously — connection direction is independent of service direction. Avoid "server" and "client" framing in docs and API names; use "producer" and "consumer," or "accept side" / "connect side" for the connection-establishment half specifically. See alkcall ADR-022, ADR-037. -
Substrate-agnostic by construction — the protocol layer must not know whether the far side is a channels
BiStream, an in-process handle, a local loopback, or a door adapter (alksftp, FUSE, a sync client). The file-protocol state machine is generic overT: AsyncRead + AsyncWrite + Unpin(the fast-socks5/alksocks genericity precedent); substrate-specific types are confined to feature-gated modules injected at the assembly layer (alkttyTtyBackendinversion-point pattern). -
No forced host-FS access — the defining requirement of this crate, the analogue of alksocks' "never binds a port." The virtual filesystem manages its own content store and never reads, writes, or mounts the host filesystem unless explicitly configured to. Bridges to the real world are explicit, optional, feature-gated assembly shapes: a local blob-file store, an SFTP door (alksftp), a mount adapter, a directory-sync tool. The engine itself is storage-agnostic about what backs its blob layer (filesystem, SQLite, remote).
-
Metadata/content split is the load-bearing architecture — small structured state (path edges, branches, snapshots, refs, cached sizes) lives in a transactional store; content bytes live in a content-addressed blob layer (dedup by hash, branch sharing for free). This is the alknet-filesystem POC's three-layer conclusion and the same "shape" as git + gitlfs and iroh-blobs' inline/outboard split. Path-tree operations are O(path edges), never O(bytes) — rename is O(1) on edges regardless of file size. Do not let byte-scale concerns leak into the metadata layer or vice versa. See
docs/research/phase-0.md. -
Content addressing and durability ordering — content is identified by its hash, not its path; identical content is shared across branches/paths by construction. The durability contract is the crash-safety story: content must be durable before any metadata naming it commits; a crash mid-write leaves the old version intact and visible (the POC's branch-on-write/merge-on-close property). Exact hashing/chunking choices are Phase 0 OQs; the invariant (no torn versions visible, no orphaned-name commits) is not.
-
Vendored core types come from alkcall —
Connection,ProtocolHandler,BiStream,BidiStreamSource,AuthContext,Identity,IdentityProvider,AccessControl,OwnershipProvider,HandlerError,StreamErrorcome fromalkcall::core. Do not vendor copies into this crate. alkcall is ours and co-developed — breaking changes are expected at this major-zero stage; find and fix issues upstream rather than working around them. Pin deliberately and bump deliberately. The establishment surface (alkcall ADR-049 + amendments) and the identity seam (CF-005/CF-006) are load-bearing for the serving protocol, same as the alktty/alktunnels/alksocks pattern. -
Access control — a filesystem is arbitrary read/write by nature: the open gate and path-scope policy are the security boundary, the same posture as alksocks' arbitrary egress. Scope-gate file opens (an
alkfs-shaped scope following alktty'sTTY_OPEN_SCOPE/ alksocks'SOCKS5_OPEN_SCOPEprecedent), wire producer openable channels throughAccessControlfor free viaChannelCore::register_openable, and treat per-path/per-bucket policy as a Phase 1 design question (OQ). Multi-tenancy isolation (the POC'sbucketconcept) is a where-clause, not an afterthought. -
Backend/storage trait shapes are one-way doors — the seam the protocol crate exposes to storage implementers (the alkgit
GitRefs/GitPackGen/GitPackIngesttrait-family precedent,docs/architecture/backend.mdthere) is a contract once consumers exist: keep traits small, orthogonal, and substrate-blind; never leak gix/iroh/SQLite types across them. The alknet-filesystem probe (alknet-blobs-external-store-probe.md) is the cautionary example — the sealed/pub(crate)surface is exactly what turns "external store" into "fork". -
Feature flags — substrate backends and heavy dependencies are feature-gated if the need arises. The base crate should compile lean (no SQLite, no gix, no kernel-FS access unless the feature is on). Verify both
cargo test(default) andcargo test --all-featurespass if features are added. -
Naming — Rust standard:
snake_casefor functions/variables/ modules,PascalCasefor types/traits,SCREAMING_SNAKE_CASEfor constants. -
Module structure — one module per file under
src/, re-exported fromsrc/lib.rs. Public API surface islib.rsre-exports. The expected shape (pending Phase 0/1 pinning): storage engine modules (path tree, blob store, write sessions, GC), backend traits + feature-gated implementations, and the producer/consumer protocol modules mirroring the alktty/alktunnels/alksocks structure. Backend modules are feature-gated and never imported from the protocol/adapter/client modules. -
Upstream posture — alkcall, alktunnels, and the alk* crates are ours to shape: file asks early and land them there rather than working around them locally (the alktunnels E-01/E-02 precedent — filed from Phase 0, landed within a day). Third-party crates (iroh-blobs, sqlite/rusqlite, russh-sftp, gix, automerge, honker) are NOT ours and never will be: wrap, extract, or fork deliberately per the alksocks precedent (OQ-SK-04 → ADR-013: extraction as owned code with provenance notices, differential tests against the reference checkout, no silent absorption) — and only when the carried changes pay for themselves.
/workspace/iroh-blobs,/workspace/russh-sftp, etc. are read-only reference checkouts.
Verification Commands
Run these before committing (once src/ exists). All must pass.
cargo test # full suite
cargo clippy --all-targets -- -D warnings
cargo fmt --check
cargo doc --no-deps # if docs changed
cargo test --all-features # if features are added
cargo check --target wasm32-unknown-unknown # if the wasm posture applies (see convention 4)
cargo publish --dry-run --allow-dirty # before a release
While the repo is docs-only (Phase 0), there is no build to verify — verification means re-reading changed docs and checking that every referenced path, ADR, and OQ id exists.
Architecture Context
docs/research/phase-0.md— the Phase 0 (Exploration) document and the current state of this repo: vision, prior art, open questions (OQ-FS-NN), and the POC register. Read it before non-trivial work. The SDD process lives indocs/sdd_process.md(Phase 0 in progress;docs/architecture/does not exist yet — do not create it; that is Phase 1).- The ancestor research:
- alknet-filesystem POCs —
/workspace/@alkdev/alknet/docs/research/alknet-filesystem/(poc-summary.md— the three-iteration POC: SQLite path tree + iroh-blobs + honker, branch-on-write/merge-on-close, automerge sync;alknet-blobs-external-store-probe.md— the iroh-blobs Command-actor probe). This crate is the decomposition-era continuation of that research on the published alk* substrate. - alkgit —
/workspace/@alkdev/alkgit: the first in-family storage consumer (currently paused on this problem). Its backend trait family (docs/architecture/backend.md—GitRefs,GitPackGen,GitPackIngestover gix-odb) is the shape alkfs must serve efficiently: streaming pack generation/ingestion, CAS ref transactions, budgeted resources. Itsdocs/research/holds the gitoxide and git-protocol surveys.
- alknet-filesystem POCs —
- The substrate and sibling crates:
- alkcall —
/workspace/@alkdev/alkcall(v0.8.0, crates.io). The substrate: call protocol + channels multiplexing. This crate will consumealkcall::coretypes and the channelsChannelCore/ChannelClient/register_openable_with_establishersurface, same as alktty/alktunnels/alksocks. - alktty —
/workspace/@alkdev/alktty: the first producer/consumer protocol crate; the backend inversion point, wasm-clean default, scope-gating, and feature-gated backend precedents. - alktunnels —
/workspace/@alkdev/alktunnels:-L/-R/-Dtunnels on channels; the Phase 0 findings format and POC placement conventions (docs/research/phase-0-findings.md) this repo inherits. - alksocks —
/workspace/@alkdev/alksocks: the SOCKS5 sibling; the closest Phase 0 template (docs/research/phase-0.mdshape, OQ numbering, upstream-posture convention). - alkvault —
/workspace/@alkdev/alkvault: secure secret handling; the family rule is metadata stores hold vault references, never plaintext secrets (alkgit vision principle 3).
- alkcall —
- Key upstream ADRs that inform this crate's design (alkcall numbers
unless noted):
- ADR-035 — channels pure channel multiplexing (8-byte header); the
file-session rides inside a channel's
BiStream - ADR-050 — two-pump shutdown-on-completion (
channels::pump_bidi); use it, do not hand-roll - ADR-049 (amendments) — the establishment phase;
register_openable_with_establisher; refused sessions are typedchannel:open_failedcall errors, never phantom channels - ADR-039 —
paramsis ALPN-specific; ADR-037 — channel lifecycle ops on channel 0; ADR-034 — the channels wire format (one-way door); ADR-036 — channel 0 is pre-negotiatedalk/call - ADR-042 — hub relay (terminate-and-re-produce); ADR-051 —
ChannelRelay/HubLegTemplate - Ledger CF-005/CF-006 — the per-call opener identity seam on the open-op hooks
- ADR-035 — channels pure channel multiplexing (8-byte header); the
file-session rides inside a channel's
- If a TODO references a design direction that a later ADR has decided against, the TODO is stale — remove it and align with the ADR. Do not implement the rejected design.