Skip to content

Open a Store read-only from StoreService and the SDK - #519

Merged
Mola-maker merged 1 commit into
Ephemeral-AI-Lab:mainfrom
mvanhorn:feat/readonly-store-open
Nov 8, 2026
Merged

Mola-maker merged 1 commit into
Ephemeral-AI-Lab:mainfrom
mvanhorn:feat/readonly-store-open

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Oct 30, 2026 •

Copy link
Copy Markdown
Contributor

Closes #485.

Store-only reads (fs.read, fs.list, fs.stat, branch.list_commits and
the others) went through a StoreService that opened writable catalog handles
and prepared an overlay template, even when the caller never wrote. Persistence
already has Handles::open_read_only, which refuses mutation before BEGIN.
This adds a read-only Store constructor and an SDK factory so a reader never
holds writable handles.

What changes

  • StoreService::open_read_only(path, binding, cursor) opens the installed
    Store after the kernel guard, selected Disposable explicitly, with the
    catalog handle opened read-only. It creates no overlay template.
  • StoreService::from_read_only(opened) wraps sessions from
    open_store_read_only. File-changing and history-mutating calls are refused
    ReadOnly ("this Store is open read-only") before they bind or write.
  • open_store_read_only is the same session setup as a writable open,
    without merge scratch and with Handles::open_read_only for the catalog.
  • LayerFS.openReadOnly(name) is a third factory beside open and init.
    It takes no options argument. Store calls send readOnly: true. Writable and
    read-only clients of the same Store path are separate process slots.
  • CLI --read-only opens with LayerFS.openReadOnly. It cannot be used
    with init.
  • Worker Store slots are keyed by path and open mode, so a read-only open
    does not share a writable handle.
  • Workspace and Sandbox calls still go to the daemon. This constructor is the
    Store-call route.

No Store format change.

Commits

  • feature, tests and docs. Production LOC: 102831 -> 102994 (delta +163).

Tests

No test deleted or weakened.

  • New engine/crates/layerfs-api/tests/store_read_only.rs: a writable client
    forks and writes, the Store is reopened read-only, reads return the published
    bytes, and fs.write, fs.mkdir, branch.fork, branch.merge and
    branch.publish are refused ReadOnly with the file and Commit count
    unchanged.
  • kernel_guard: a read-only open on a filesystem outside the allowlist is
    Unsupported and creates no extra files; a missing Store is Missing (or
    Unsupported when the guard refuses first).
  • rustfmt, and Clippy -D warnings on layerfs-store-service, layerfs-api
    and layerfs-node: clean.
  • write_refusals: pass (writable path unchanged).
  • JS against the fake binding: api/agent/test/open_read_only.mjs,
    api/agent/test/calls.mjs and api/cli/test/cli.test.mjs: pass.

Not run: the macOS/Windows worker against a real Linux worker. The worker keys
and storeOpen arguments were checked with the stand-in worker.

Author

  • I ran the checks listed above
  • Docs match the implemented constructors

Demo

demo

…b#485)

StoreService::open_read_only opens the installed Store with a read-only
catalog handle and no overlay template. File-changing and history-mutating
Store calls are refused ReadOnly before they bind or write. LayerFS.openReadOnly
is the SDK factory; the CLI --read-only flag uses it. Writable and read-only
clients of the same path are separate process and worker slots.

New store_read_only test covers published reads and refused writes. Kernel
guard tests cover Unsupported and a missing Store. Clippy -D warnings on
layerfs-store-service, layerfs-api and layerfs-node is clean. Fake-binding
SDK, CLI and open_read_only tests pass. Deleted tests: none.

Production LOC: 102831 -> 102994 (delta +163)
First-parent comparison: 35d3657.
Scopes: core 38395 -> 38395 (0), engine 61777 -> 61920 (+143),
api 2659 -> 2679 (+20). Engine layerfs-store-service +101, layerfs-node +24,
layerfs-api +18; api agent +12, runtime +5, cli +3.
Method: tools/production_loc.py blob 3341e4d,
--json on each snapshot. First-party production Rust, runtime SQL and
JavaScript; tests, docs, tooling, generated and third-party code excluded.
Reference absent (retired R9c; last counted 65,417 at tree 498dd19).
@Mola-maker

Mola-maker commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

感谢贡献,也感谢 AI 协作工具在 #485 讨论中厘清只读边界 🙏

真实评估(读 PR 描述与文件范围):21 文件 +610/−102,把只读的 Store 调用面(fs.read/list/stat、branch.list_commits 等)从"打开可写 StoreService"中解耦。方向与 AGENTS.md 的 direct-Store 语义一致:只读 opener 不进入写者准入/发布通道,单写者模型不变。有一个建议在合入前明确:只读 opener 是否仍受 kernel guard 的 Linux-only 约束——按"每个 Store opener 共享一个 Linux 内核"的规则应该是,文档里写清楚可以避免 #456 那类平台疑问重演。diff 细节我未逐行读,此条评估基于描述与范围。

对 AI agent 的使用价值:agent 的分析类任务(读代码、diff、统计)可以安全并发,不必排队等写者身份。

建议:合入前在真实环境实测"只读客户端 + 写者客户端并发"场景,确认只读侧不会被写者的事务短暂阻塞到失败。功能性调整等 LayerFS 2.0(#446)发布后再做。

@mvanhorn

mvanhorn commented Nov 1, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for the review. On the kernel guard question: yes, the read-only opener is still behind it. StoreService::open_read_only calls kernel_guard::check(store) first and maps a refusal to OpenRefusal::Unsupported, the same as the writable open. The new test in tests/kernel_guard.rs checks that a read-only open on a filesystem outside the allowlist is Unsupported and creates no extra files. The module header comment in the diff also says the guard runs before any read-only open.

On concurrency: the added store_read_only.rs test reopens the Store read-only after a writable client has written, but it does not run a reader and a writer at the same time. I have not tested that case, so I can't say yet that a reader is never failed by a short write transaction. I'll leave it for a follow-up unless you want it before merge.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new feature A new user-facing capability

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A read-only Store open (StoreService and SDK) for readers that must never write

4 participants