cargo clippy --fix applied style nits (redundant return, ?-operator,
map_or simplify) plus doc-comment list indentation. The full ./check gate
(fmt+test+clippy -D warnings) now passes; R5 read-gate + grant tests
remain green (39 cubesys lib tests).
Closes the read-privacy gap flagged in plan R1/R5. Rather than fork the
FUSE uid/gid Acl model (whose identity is POSIX uid, incompatible with the
daemon's name-based HELLO identity), read-gating is done owner/grant-native:
- commands.rs: new admit_read(coord) mirroring admit_mutate (owner ->
read-grant -> deny; unowned records world-readable). Wired into
(read+execute) and (metadata read), gated by enforce_owner exactly
like writes.
- Tests: read_gate_blocks_non_owner_and_allows_read_grant (bob denied,
allowed after alice grants read), read_gate_requires_identity_under_enforce
(anonymous stat rejected under --require-identity).
- Full ./check quick green: 39 cubesys lib tests, clippy -D warnings clean.
Note in plan: Acl lift (R1) is NOT a literal fork; the daemon reuses the
owner/grant enforcement concept, not the POSIX Acl struct.
Adopted recommendation (B): make owner authority a hard guarantee instead
of the non-breaking opt-in. A mutating op now requires a stamped HELLO
identity; anonymous writes are rejected. seal/open (destructive writes)
are gated the same way, and seal stamps the owner onto the encrypted record.
Design / non-breaking bridge:
- Session gains enforce_owner: bool (default false) so library/REPL/unit
tests stay permissive — the 26 prior tests + 3 Task-6 tests are unchanged.
- owner_violation() gains require_identity: the (false) path keeps legacy
behaviour; the (true) path rejects no-identity mutating ops.
- The daemon flips enforce_owner=true on every connection (both HELLO and
no-HELLO branches), implementing the default --require-identity policy.
- Added --allow-anonymous escape hatch so legacy cubec/stress.sh (which
send no HELLO) keep working; stress.sh now passes --allow-anonymous.
- Session::set_enforce_owner() accessor so the daemon (separate bin) can
set the private field.
Verification:
- ./check quick: EXIT=0, fmt+clippy clean, 28 cubesys lib tests (added
enforce_owner_requires_identity, seal_open_respect_owner).
- Ad-hoc daemon verifier (LE framing) against the rebuilt cube-server:
anonymous prog/del rejected, HELLO'd owner first-claim + self-overwrite
allowed, cross-owner overwrite rejected. ALL PASS.
Note: seal's demo key-cell crypto path (KeyCellMissing on the synthetic key
material) is a pre-existing quirk unrelated to this change; the gate fires
before crypto, so the test verifies the gate, not the crypto.
Stamp owner_local_user on records written via prog/write and gate the
mutating paths (prog, write, del — both live and buffered txn) so a
session may only create or overwrite a record whose owner_local_user
matches its HELLO-declared identity.
Design (logical + expedient for the whole project):
- Owner is the durable record-level CubeHeader.owner_local_user field,
so enforcement is replay-safe and works across daemon restart.
- Enforcement is opt-in/non-breaking: gated only when the session has a
stamped identity AND the record has an owner. First write by an owner
claims an unowned coord; a session with no identity (tests, legacy)
writes freely.
- COMMIT re-checks owner on each buffered op before applying, so a
concurrent cross-owner commit between BEGIN and COMMIT is rejected
(txn is restored for retry, not silently dropped).
- seal/open (encrypted raw put/del) left ungated for now: their headers
are not owner-stamped yet — tracked as follow-up.
Verification:
- ./check quick: EXIT=0, fmt+clippy clean, 26 cubesys tests (added
owner_enforcement_blocks_cross_owner_overwrite,
owner_enforcement_allows_first_claim_and_same_owner,
for_tenant_carries_identity).
- Ad-hoc daemon verifier over real cube-server socket (LE framing):
cross-owner overwrite + delete rejected, same-owner + first-claim
allowed, no-HELLO legacy writes allowed. ALL PASS.
Two correctness bugs found via ad-hoc daemon verification (T5 was
compile-verified only before):
1. decode_wal dropped WalOp::Txn entries on replay: the txn encoder emits
{"seq","op":"txn","batch"} with NO c/z/y/x fields, but decode_wal
read c/z/y/x unconditionally -> field_u8("c") returned Err -> the
whole entry was skipped. Committed transactions silently vanished on
restart. Fix: branch on op=='txn' before the c/z/y/x extraction.
2. commit was not synchronously durable: append_txn only buffered to the
WAL pending buffer; fsync happened on the 25ms group thread. A
clean stop within that window lost the commit. Fix: commit_txn now
calls wal.flush_pending() (fsync) before returning, so COMMIT is
durable on return -- a real transaction boundary.
Adds unit test commit_replays_from_wal_without_checkpoint (would have
failed before fix 1). Ad-hoc verifier exercises all 3 changed paths on
the live cube-server socket.
ConcurrentStore.inner is now Arc<RwLock<CubeStore>>: all read paths take the
read side, all mutations + checkpoint take the write side. Readers no longer
exclude each other and overlap an active writer (verified by
concurrent_reads_dont_block_on_writer + cube-bench Task 4 section). WAL,
checkpoint, and coordinate encoding are untouched, so durability/replay is
unchanged (.check green).
Honest finding recorded in docs/task4-reader-writer-sharding.md: on this 8-core
host std RwLock removes reader-vs-reader exclusion (correct) but shows no
wall-clock speedup for short reads (cache-line bounce on one shared lock). Real
read-throughput scaling would need sharded/lock-free storage, left as a
follow-up decision rather than invented.
The wire protocol required an explicit HELLO acknowledgement frame; a
HELLO-only client would otherwise block on the next recv. Add the ACK.
Ad-hoc E2E (real built cube-server, real 4-byte LE framed Unix socket):
- SHARED mode: a HELLO tenant and a no-HELLO client share ONE store
(write visible across both) — backward-compatible with cubec/stress.sh.
- PER-TENANT mode (--tenant-dir): two HELLO tenants are HARD-isolated
(T2 and default tenant cannot see T1's record).
Verifier: /tmp/hermes-verify-driver.sh + /tmp/hermes-verify-e2e.py
- TenantRegistry gains a true single-store 'shared' mode so the legacy
--store PATH invocation (and stress.sh / old cubec) keeps serving one
global store, while still parsing HELLO frames.
- cube-server now holds an Arc<TenantRegistry>, resolves each connection to
its tenant's TenantSession, and stamps the client identity from
HELLO <tenant> <owner_local> [<owner_remote>].
- Add --tenant-dir DIR for real per-tenant disk isolation (opt-in).
- Add TenantIdentity + TenantRegistry::parse_hello and a shared-session
integration test.
TenantConfig::Disk opens each tenant's ConcurrentStore under its own
sanitized subdir of store_dir (path separators -> '_', no traversal escape),
reusing the durable WAL+checkpoint machinery (incl. 49698af delta-path fix).
Registry takes a config; get_or_provision returns io::Result so a disk
open failure surfaces instead of silently falling back to memory.
Tests: tenant_store_is_isolated_on_disk (separate dirs + survives reopen,
B still isolated) + tenant_id_cannot_traverse_store_dir. ./check green.
Add TenantId (from-str, normalized, non-empty) and TenantRegistry that
provisions one isolated TenantSession per tenant behind an RwLock<HashMap>.
In-memory store only for now; disk-backed per-tenant stores + daemon wiring
land in Tasks 2-3. 4 unit tests pass, ./check green.
open() derived the delta path as "${db_path}.delta" (e.g. "db3.json.delta")
while checkpoint_store() writes via db_path.with_extension("delta")
(e.g. "db3.delta"). On reopen, load_base_plus_delta therefore read a
never-written path and silently skipped the delta, so post-checkpoint
changes were lost. Use db_p.with_extension("delta") in both places.
Also drop a no-op cp_seq.max(0) (u64 >= 0 always) to clear the clippy
-W clippy::unnecessary_min_or_max lint.
Verified: ./check all green; incremental_checkpoint_delta_model,
durable_checkpoint_and_replay, wal_recovery_after_crash pass in isolation.
- Add persist.rs: std-only NDJSON snapshot of the HashBackend store
(no serde) for the durable checkpoint + load_into_store replay.
- Add store.rs: ConcurrentStore = Mutex<HashBackend> live store + WAL
(newline-delimited JSON, group-commit fsync, idempotent seq-numbered
replay) + durable JSON checkpoint + bg flusher + startup replay.
- Recovery events (checkpoint failure, WAL fsync failure, WAL replay)
are written to a recovery.ndjson you asked to keep as the written backup
log, so any fall-back to JSON is recorded 'in writing'.
- Refactor cube-server to thread-per-connection over ConcurrentStore.
- query_doc_type / scan_prefix / linked_to / delete_raw added.
Verified: ./check (fmt, 7 unit tests, clippy -D warnings) all green;
./check stress drove 22,080 prog+run pairs (~368/s) over 60s, daemon
survived, latency prog~9us/run~13us mean.
- ./check stress spins a fresh cube-server+cubec built from this tree on a
throwaway socket/store and drives ~150s of real prog/run traffic while
sampling the per-command latency + per-C telemetry.
- tools/stress.sh: never touches the production daemon; STRESS_SECONDS override.
- Fixed latent control-flow bug: the old '|| exit 0' guards made 'bench' and
'mount' subcommands unreachable after the gate (they exited early). Now uses
explicit run_* flags and only exits at the true end.
- cube-bench crate: real-code-path throughput/latency over cubestore,
cubecrypt (aes/gcm/chacha/xts), cubecode VM, and cubesys Session.
Every section asserts correctness before timing. Wired into ./check
as an opt-in 'bench' stage.
- cubesys Session: per-command latency histogram + per-C-namespace record
counts, exposed via a new 'stats' command over the live socket.
- Deployed rebuilt cube-server to /home/luulu/.cubelinux/bin and
restarted the system cube.service; verified stats live.
Implements the requested cube service: a long-lived daemon that holds ONE
CubeStore for its whole lifetime and serves the cube command language over a
Unix-domain socket, plus cubec to talk to it.
- cubesys::commands: factored the single command interpreter (Session::exec)
so cube REPL, cubec client, and the daemon run identical logic
- cubesys::net: dependency-free length-framed AF_UNIX transport
- cubesys::persist: dependency-free JSON snapshot (atomic tmp+rename) so the
store -- including sealed/encrypted records -- survives daemon restarts
- cube-server: listens on $XDG_RUNTIME_DIR/cube/cube.sock, snapshots to
$XDG_STATE_HOME/cube/cube-store.json, replays on startup
- cubec: one-shot + REPL client over the socket
- cube.rs trimmed to a thin REPL/script/demo driver (help text updated)
- /etc/systemd/system/cube.service: runs as luulu, ProtectSystem=strict,
RestrictAddressFamilies=AF_UNIX, Restart=on-failure; enabled + active
- integration.md documents the daemon + caveat (open rewrites plaintext)
Verified: ./check (fmt+tests+clippy -D warnings) green; ./check mount (27
FUSE e2e) green; socket CLI round-trips; sealed record survived a full
service restart and reopened+r with original value.
Integrates Packages 3-5 over a single shared CubeStore, the literal
CUBELinux premise (data addressed by coordinate, not path). Adds the
cubesys crate (lib + cube CLI + cube-demo) proving two end-to-end
properties: a cubefs path IS a runnable code cell at the same coordinate,
and a sealed record reopens and runs on the same store.
Two latent cross-crate bugs surfaced and fixed while integrating:
- cubecoords: refresh_flags() now preserves out-of-band flag bits
(8..=15), so cubecrypt's HEADER_FLAG_ENCRYPTED survives refresh.
- cubestore: record codec now serializes raw flag bits (TLV tag 12) so
the encrypted bit survives the store round-trip.
All gates green (./check, incl. cubefs --features mount).
XTS was previously covered only by a deleted throwaway example. Fold the
verification into the committed suite so it runs under ./check: 16-multiple
roundtrip, short-plaintext pad/strip roundtrip, multi-block sector, and the
correct non-AEAD wrong-key property (wrong key opens to different garbage).
Ad-hoc verification (factorial via recursive CALL_LINK) surfaced a real defect:
each exec_cell had its own private stack, but the design (and the doc
contract) is that callees run on the SHARED data stack so a caller passes
args by leaving them on the stack and reads the callee result there after
RET. With per-frame stacks, a callee that popped its argument faulted with
PcOutOfRange.
- exec_cell now takes &mut Vec<u8> (the one shared stack) instead of owning
a fresh one; run() owns it and threads it through recursion.
- Added regression test shared_stack_passes_args_across_cube_edges (caller
leaves 21, callee doubles it via Store/Load, caller sees 42) so the
convention is locked by ./check, not just ad-hoc.
The prior commit's unit tests didn't exercise cross-frame stack args, which
is why the bug slipped through; suite is now 50 tests green (incl. 14
cubecode).
Implements the PDF's Package 4 (cubevm/cubecode): the cube as a substrate for
storing and introspecting code + AI artifacts.
- opcode.rs: decode/encode codec for a deterministic, safe bytecode (28 ops:
stack arithmetic/logic/shift, comparisons, jumps, CALL_LINK, RET, SYSCALL,
DUP/DROP). Body is always decoded through this codec before execution, so
the cube never runs code that didn't survive decode.
- cell.rs: CodeCell + Kind (Fn/Kernel/Layer/Checkpoint/Variant/Other). Kind
rides in the record doc_type; the call graph is the cube's linked_records,
so CALL_LINK n executes linked_records[n].
- vm.rs: stack-based interpreter over CubeStore. CALL_LINK follows cube edges;
call depth is bounded (no infinite cube loops); deterministic + no unsafe +
no float, so runs are reproducible (prereq for 'navigate and reconstruct
experiments'). Host syscalls introspect the cube (degree/link-exists/trace).
Design decision (the PDF's interesting tension): it says body may be
'bytecode, machine code, or serialized model weights' but the load-bearing
clause is a DSL/runtime that 'walks cube links to load and dispatch functions'
= addressable code-as-data. On this hardware a foreign-machine-code JIT is
neither safe nor needed, so we built the safe bytecode VM. cubetrace/
cubedbt/cubeai (execution capture + ML over traces) are the next layer and are
excluded here.
Verified: ./check (fmt+tests+clippy -D warnings) green; cubecode: 13 tests
(arith, div-by-zero fault, CALL_LINK edge-follow, bad-link fault, recursion
depth bound, syscall trace/degree). Gate: cde1e62 -> +Package4.
The workspace had no single verification entrypoint, so every claim of
'green' rested on an ad-hoc command chosen after the fact. ./check makes it
one command, mirroring the convention already used by the original build.
./check fmt + tests + clippy -D warnings
./check quick tests only
./check mount the above + live FUSE end-to-end (root, 27 assertions)
The FUSE adapter compiles in every stage (--features cubefs/mount) so the
kernel-facing code can never silently rot behind a feature flag. The live
mount stage is opt-in because it needs root, /dev/fuse and attr — but it is
the only stage that exercises the real kernel VFS path, and it encodes the
two Package 3 regressions (mkdir -p to depth 4, cross-user ACLs) that every
unit test missed.
Adopting clippy -D warnings immediately paid for itself: it failed on two
pre-existing Package 1 defects that per-crate greps had let through —
a needless_range_loop in TriEnc::unpack_6 and a doc comment orphaned by a
blank line in HeaderFlags. Both fixed.
Verified: ./check green; ./check mount green (27/27, no leftover mounts).
Implements the PDF's Package 3 with new code:
- path: bijective POSIX path <-> Czyx mapping (/c001/z002/y003/x004).
Axis-letter + 3-digit zero-padded canonical names so lexical order equals
numeric order and each coordinate has exactly one spelling. Inode IS the
packed u32 coordinate — no inode side table.
- nullspace: the PDF's 'use Null cubes for ACLs, xattrs, journaling, volume
metadata', with the Z-plane allocation fixed and documented (Z=1 volume,
Z=2 ACL, Z=3 xattr, Z=4 journal ring). ACL/xattr tables are FNV
hash-bucketed with exact-match resolution inside the bucket, because 4
axes of subject cannot injectively mirror into 2 axes of Null space.
Journal is a bounded ring; wraps are detectable via a monotonic counter.
- vfs: the whole filesystem, kernel-free and unit-testable — lookup,
readdir, create/read/write/truncate/unlink, mkdir/rmdir, ACL enforcement,
xattrs, journaling, POSIX errno mapping.
- fuse (feature 'mount'): thin kernel adapter, zero TTL (the store is
writable out-of-band, so cached metadata would go stale).
- cubestore: added the PDF's 'optional scanning primitives' (keys,
scan_prefix) and the Package 2 association API (associate, linked_to)
that cubefs needs for directory listings.
Two defects were found by LIVE MOUNT testing and fixed, not by unit tests:
1. mkdir succeeded then the kernel's revalidating lookup returned ENOENT,
so 'mkdir -p' could never reach depth 4. Directories were purely
inferred from records, making an empty directory unrepresentable. Fixed
with an explicit Null-space directory marker; rmdir removes it; readdir
merges markers in. 5 regression tests added.
2. Multi-user ACL behaviour was untestable because the mount lacked
AllowOther — the kernel returned EACCES at the mountpoint before any
request reached us. Added --allow-other.
Verified: 58 unit tests pass; clippy clean; live mount exercised with cat,
echo, dd, truncate, cp, chmod, chown, getfattr/setfattr, mkdir -p, rmdir,
find, a 200-record write loop, and cross-user reads/writes as luulu.
User clarified: the earlier prune was a ONE-TIME cleanup of an over-backfill
bug, NOT a recurring retention cap. Saved history must persist. Removed
cmd_prune / --prune-older-than-hours entirely; the script now only captures
forward (default) or seeds a bounded window once. No code path deletes cube
entries. Verified: help has no prune arg, forward/seed dry-runs change no
state, live timer runs forward-only.
Captures user/assistant messages from /root/.hermes/state.db into the
'hermes' cube namespace as TYPE: HIST-QNA entries. Forward-only by default
(no backfill); --seed-hours N for bounded one-time population; idempotent;
--prune-older-than-hours to undo over-backfill. systemd timer runs every
5 min. Tested: 24h seed = 1171 msgs; prune cleared a 3164-entry backfill
to 44, then re-seeded correctly.