Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ jobs:
- name: cargo test tcfs-sync (nats + crypto features)
run: cargo test -p tcfs-sync --features nats,crypto

- name: Feature-isolated consumer tests
run: |
cargo test -p tcfs-file-provider --no-default-features --features grpc --locked
cargo test -p tcfs-file-provider --no-default-features --features uniffi --locked
cargo test -p tcfs-dbus --features grpc --locked

- name: Wire-up integration tests (prove modules are connected)
run: |
cargo test -p tcfs-e2e --test blacklist_wireup -- --nocapture
Expand All @@ -88,6 +94,24 @@ jobs:
- name: Build tcfs CLI (no FUSE — macOS/Windows compat)
run: cargo build -p tcfs-cli --no-default-features

cloudfilter-windows:
name: CloudFilter compile (Windows)
runs-on: windows-latest
steps:
- uses: actions/checkout@v5

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}

- uses: Swatinem/rust-cache@v2
with:
cache-bin: false

- name: Compile Windows CloudFilter targets
run: cargo check -p tcfs-cloudfilter --all-targets --locked

nix:
name: Nix Build
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ task dev
| `tcfsd` | lib+bin | Daemon: gRPC over Unix socket, FUSE, metrics, systemd. Lib surface exposed for integration tests (see `tcfsd::daemon::test_support`). |
| `tcfs-cli` | lib+bin | CLI: push, pull, mount, device, status, unsync. Lib surface exposes ordering-sensitive command helpers for integration tests. |
| `tcfs-tui` | bin | Terminal UI: ratatui 5-tab dashboard |
| `tcfs-mcp` | bin | MCP server: 8 tools, rmcp 0.16, stdio transport |
| `tcfs-mcp` | bin | MCP server: 7 non-resolution tools, rmcp 0.16, stdio transport |
| `tests/e2e` | test | End-to-end integration test crate |

## Key Patterns
Expand Down
111 changes: 111 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,122 @@ intent rather than the current supported/proven surface.

## [Unreleased]

### Added

- Daemon-trusted stable-root routing for isolated conflict caches: clients can
inspect a named root and run the bounded Git keep-both dry-run/execute flow
without supplying a state path or storage prefix. Registered roots inherit
the global storage transport. An earlier mechanism build was enrolled and
exercised on Honey before the TIN-2856 incident freeze; this exact hardening
head remains source-only, and that earlier run is not evidence for it. The
entire named-root precursor, including read-only `conflicts --root`, is
Linux/macOS-only and fails closed on every other platform.

### Security

- Registered-root authorization now hides known-but-unauthorized IDs behind
the same not-found response as unknown IDs, before path-bearing validation.
- Registered-root RPCs now require real session enforcement and fail closed
when `auth.require_session = false`; the development-only synthetic admin
bypass is excluded from every named-root operation, while existing legacy
auth-disabled daemon behavior is unchanged.
- Registered-root Git keep-both dry-run requires pull permission; execute
requires both pull and push. Inspect-only policy permits a pull-authorized
dry-run but rejects execute even for a pull+push session.
- Legacy primary-cache Git resolution now checks capabilities by data flow:
dry-run requires pull and execute requires pull+push. The unrooted legacy
per-file mutation strategies (`keep-local`, `keep-remote`, and `keep-both`)
are retired fail-closed; `defer` remains a push-authorized no-op until
ordinary-file resolution has daemon-owned root and manifest identity.
- Registered-root conflict operations reject stale cache keys outside the
authorized storage prefix. This precursor owns only repository-group Git
keep-both mutation; ordinary per-file mutation is disabled on both primary
and named roots.
- The shipped CLI no longer advertises retired ordinary-file strategies, and
the unrooted `resolve_conflict` MCP mutation tool is removed. Older direct
gRPC clients receive one uniform fail-closed response before path or state
lookup.
- Git keep-both captures the authorized repository's filesystem identity,
binds Git mutation subprocesses and lock acquisition to captured repository
and `.git` directory handles, and revalidates the configured pathname across
storage, state-lock, mutation, and state-flush boundaries so a same-path
replacement is never mutated. This applies to every Git keep-both mutation,
including the legacy primary-cache route; all such mutation is
Linux/macOS-only and fails closed elsewhere. The seam accepts only the
ordinary files-ref backend and rejects reftable, enabled
`core.sharedRepository`, and shared-writable repositories.
- Named-root routing requires the local root, critical Git metadata, state
directory, and state cache to be owned by the exact tcfsd effective UID.
Roots, metadata, and state directories must not be group/world writable;
the cache must be mode 0600 or stricter. Canonical ancestors must be owned
by that UID or root and not writable by another principal, except for a
protected root-owned sticky boundary such as `/tmp`.
- Git keep-both cleanliness checks use isolated metadata, disable lazy fetch
and replacement refs, and reject executable filter/attribute and partial
clone routing. The cooperative Git lock uses one persistent advisory-locked
inode and serializes TCFS writers only. Native Git busy markers remain a
fail-closed preflight, but same-euid native Git races are outside the
resolver's concurrency guarantee.
- CLI `push`, `pull`, `rm`, and executing `reconcile` serialize explicit
`--state` cache mutations with daemon-side registered-root operations before
constructing storage clients. These remain legacy mutation routes, not
named-root authorization surfaces; only `conflicts --state` is diagnostic.
- Credential-bearing S3/SeaweedFS clients now require HTTPS by default across
daemon, CLI, direct mount, and FileProvider operator construction. Plaintext
compatibility requires an explicit development/test opt-in.
- On Linux/macOS, JSON sync-state reads now fail closed on symlinks, hardlinks,
permissive modes, unsafe parent chains, ACL grants, and non-content I/O
failures. A missing or content-corrupt primary may recover only from a
securely opened, valid backup; the first durable repair preserves that
known-good backup.
- tcfsd now keeps policy, auth, and pending-delete authority only in its
persistent owner-only data directory, holds a process-lifetime singleton
lock before opening caches or sockets, and refuses the former shared `/tmp`
fallback. Automatic remote deletes use a durable ledger plus atomic
rename-without-replacement so recovery never overwrites a recreated path.
- Indexed publication now verifies live create-if-absent, same-ETag update,
and stale-read semantics before remote mutation; configured client-side
concurrency limits cannot serialize the verification race.
- Portable remote paths now acquire monotonic per-component namespace
reservations, atomically excluding Unicode/case aliases and concurrent
file-versus-directory claims across publishers.
- Remote file, directory-marker, rename-source, and trash removal now uses
conditional version-4 tombstones instead of proof followed by unconditional
delete. Consumers distinguish physical absence from a durable deletion:
only a freshly rechecked v4 tombstone may authorize local byte removal.
Trash restore/purge uses one exclusive immutable lifecycle claim per exact
generation, makes interrupted restores retryable, validates current and
staged manifest bindings before either tombstoning or republication, and
retains its evidence. Trash-backed v4 tombstones bind the exact generation
key and evidence digest at the deletion linearization point; the later
completion marker is independently verifiable, so a lost response stays
recoverable while a failed compare-and-swap remains indeterminate. Historical
timestamp-only generations remain explicitly restorable. Retention begins at
the storage timestamp of the delete-completion marker (or its exact bound
tombstone), uses one cleaned fixed storage-clock guard, and retains legacy
generations without a provable deletion time unless the operator requests
`--all`. Trash scans validate legacy payload/manifest bindings, report and
retain malformed objects without blocking valid listing rows or
independently proved purge claims; purge returns a structured partial
report and the CLI exits unsuccessfully after reporting any retained issue.
Exact-key restore bypasses unrelated scan issues, and `trash purge --all`
conflicts with `--older-than`.
Executing prefix migration now requires an explicit quiesced-writer
assertion and manifest-binding validation; exact double-prefixed sources are
logically retired while orphan-prefix evidence remains for reachability-safe
physical GC.
- FileProvider reads now use a distinct exact-path RPC, so an older daemon
returns `UNIMPLEMENTED` before mutation instead of silently falling back to a
same-basename object. Manual hydrators bind index size and chunk count before
local mutation, and deleted index tombstones are treated as logical absence.
VFS, daemon listing, Apple providers, and Windows placeholder discovery now
exclude physical tombstone objects while propagating corrupt index state.
Apple write capabilities and callbacks plus the C, gRPC, and UniFFI mutation
entry points are explicitly read-only and reject before I/O until an opaque
exact-version conditional publication protocol is available. Incomplete or
skipped daemon push streams now terminate with an error rather than success,
and the gRPC-backed direct reader rejects malformed or missing required
encryption key material before constructing storage or daemon clients.

## [0.12.17] - 2026-07-09

Expand Down
12 changes: 12 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ serde = { version = "1", features = ["derive"] }
serde_json = { version = "1" }
toml = { version = "0.8" }
unicode-normalization = { version = "0.1" }
unicode-casefold = { version = "0.2" }

# CLI
clap = { version = "4", features = ["derive", "env"] }
Expand Down
19 changes: 17 additions & 2 deletions config/tcfs.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
[daemon]
# Unix domain socket for gRPC (CLI <-> daemon communication)
socket = "/run/tcfsd/tcfsd.sock"
# Optional: TCP listen for remote access (e.g. "0.0.0.0:7777")
# listen = "127.0.0.1:7777"
# Plaintext TCP is refused. Tunnel the owner-only Unix socket over SSH.
# listen = "127.0.0.1:7777" # legacy; tcfsd will fail closed
# Prometheus metrics endpoint
metrics_addr = "127.0.0.1:9100"
log_level = "info" # trace, debug, info, warn, error
Expand Down Expand Up @@ -42,11 +42,26 @@ credentials_file = "/etc/tcfs/credentials.yaml"
nats_url = "nats://localhost:4222"
# RocksDB local state cache (tracks file hashes + sync state)
state_db = "/var/lib/tcfsd/state.db"
# Trusted parent for daemon-enrolled, isolated root caches. Set this for system
# daemons because the socket parent (/run/tcfsd) is not persistent. The same
# applies to systemd user sockets under %t.
root_state_dir = "/var/lib/tcfsd/reconcile"
# Sync worker threads (0 = auto-detect CPU count)
workers = 0
# Retry limit for failed tasks before moving to DLQ
max_retries = 3

# Strategy-A stable-root routing (optional). Clients send only the table ID;
# tcfsd selects and fences every path/prefix below from this trusted config.
# `inspect-only` permits conflict listing and keep-both dry-run. Change to
# `resolve` only for a root approved for attended repo-group execution.
#
# [sync.roots.git-roam-tool-daemon]
# local_root = "/home/user/git/tinyland-tool-daemon"
# remote_prefix = "git-roam/tool-daemon"
# state_path = "/var/lib/tcfsd/reconcile/git-roam-tool-daemon.json"
# policy = "inspect-only"

[fuse]
# Negative dentry cache TTL in seconds (important for git performance)
# Set higher (120+) for large repos with many untracked files
Expand Down
1 change: 1 addition & 0 deletions crates/tcfs-auth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ blake3 = { workspace = true }
secrecy = { workspace = true }
base64 = { workspace = true }
uuid = { workspace = true }
libc = "0.2"

# TOTP (RFC 6238)
totp-rs = { version = "5", features = ["gen_secret", "serde_support", "otpauth"], optional = true }
Expand Down
Loading
Loading