Skip to content

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

Open
mvanhorn wants to merge 1 commit into
Ephemeral-AI-Lab:mainfrom
mvanhorn:feat/readonly-store-open
Open

mvanhorn wants to merge 1 commit into
Ephemeral-AI-Lab:mainfrom
mvanhorn:feat/readonly-store-open

Conversation

@mvanhorn

@mvanhorn mvanhorn commented Oct 30, 2026 •

Copy link
Copy Markdown

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
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

None yet

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

2 participants