Skip to content

feat(lmux): promote managed Linux workspace service - #602

Merged
zhnt merged 45 commits into
mainfrom
lane/harness
Sep 22, 2026
Merged

zhnt merged 45 commits into
mainfrom
lane/harness

Conversation

@zhnt

@zhnt zhnt commented Sep 22, 2026

Copy link
Copy Markdown
Owner

Summary

  • introduce the managed Linux workspace service and lmux lifecycle
  • enforce rooted storage, writer authority, and managed service ownership
  • wire the Coding CLI and HarnessTUI surfaces to the managed runtime
  • add cross-platform contracts, acceptance evidence, and CI coverage

Verification

  • PR feat(lmux): deliver managed Linux workspace service #601 Change-aware Quality matrix: all 54 jobs passed
  • make check-harness: 5186 passed, 66 skipped
  • make check-harnesstui: 1572 passed, 67 deselected
  • focused concurrent managed-process regression: 4 passed
  • M4 acceptance suite: 474 passed; PTY suite: 21 passed
  • measurement suites: 409 passed, 4 skipped; 738 passed

Promotes the reviewed and merged lane/harness result to main.

zhnt added 30 commits September 22, 2026 01:27
Preserve the in-flight lmux M0 design and implementation record that so far
existed only in the working tree. No product code changes.

Contents:
- lmux-contract-m0.md: slices 19-96 (4689 lines), including the shared
  data-root admission delta that is still candidate, not accepted
- lmux-managed-output-capture.md: managed command output capture contract
- lmux-capability-projection-plan.md, lmux-live-probe-plan.md,
  lmux-performance-acceptance-plan.md: remaining M3/M4 acceptance plans
- ARD-004: AA stability gate vs interactive metric scale
- user-guide lmux pages (en, zh-CN) and their README links

Status remains partial: managed CLI preview, Linux launch/lifetime, owned
transcripts, discovery, exact-instance connections, close and shared
Markdown/action/input binding are implemented; full M3/M4 acceptance and the
final goal-wide review are still pending. This commit records evidence only;
it does not claim the lmux goal is complete.
Path.mkdir(mode=0o700, parents=True) and os.makedirs(mode=0o700) apply mode to
the leaf only: each intermediate level is created through an internal
self.parent.mkdir(parents=True, exist_ok=True) call that drops it, so ancestors
inherit the process default (0o777 & ~umask). Under a permissive umask such as
0o002 -- the Debian/Ubuntu system-user default in use here -- those ancestors
become group-writable 0o775.

Private-directory admission rejects a group-writable ancestor as unsafe, so a
caller can build a tree and then be refused by its own admission layer. Any
commit that introduces such an admission layer must not inherit 0o775 ancestors
from this creation path.

Add create_private_directory_chain() as the single implementation of the
creation rule, shared by the journal owner and the machine-resource control
plane instead of two copies of security-relevant directory logic. The journal
lock path uses it directly; prepare_private_directory_chain() remains a thin
Product-facing wrapper over it that only fixes the entry contract.

The primitive also closes two ways the naive form failed:

- A directory symlink satisfies is_dir(), so the previous is_dir()/is_symlink()
  pair accepted it and continued into the link target. Levels are now classified
  with lstat(), and the FileExistsError race branch reclassifies the same way
  rather than trusting a following is_dir(). Symlinks, dangling symlinks and
  non-directory entries are refused and never followed.
- mkdir copies an inherited setgid bit, so under a setgid parent mkdir(mode=0o700)
  yields 0o2700, which admission rejects for not being exactly 0o700. A level this
  call created is now tightened to 0o700. Only levels created by this call are
  touched; an existing directory is never rewritten, since it may belong to
  another owner or hold data this call did not create.

Scope is deliberately narrow: creating the chain is not an admission or
ownership guarantee, and the module documents that. Callers that own a specific
leaf still tighten and validate that leaf through their own authority.

Covered by tests/harness/test_private_directory.py for the rule itself (creation
mode, setgid, directory/dangling/leaf symlinks, regular file, both
concurrent-creator branches, relative target, home expansion), wrapper-only tests
in tests/harness/machine_resources/test_control_plane.py, and the existing
integration coverage that journal_file_lock routes through the primitive.

Verified on a worktree containing only this commit: the mode regression fails
before and passes after, and the new suite passes. Not verified here, and not
claimed: this commit does not by itself fix any startup failure, because the
writer-admission layer that rejects these ancestors is not yet committed.

Not addressed: a user whose own HOME is 0o775 is still refused, because the
Product deliberately does not rewrite directories it did not create. Relaxing
that is a security-contract decision and remains open.
The M0 contract named 1286348e as its starting point. That commit was the tip of
harness/linux-interactive-startup-v2, a G18 phase-two line whose own document
records that its two 20% primary targets were not met and that it is local
delivery only. It is not part of the lmux goal and has been split off with
git rebase --onto main 1286348e so the v2 line decides its own destination.

Record the split, the range-diff evidence that the eleven retained commits are
mechanically equivalent (11/11 "="), and the resulting main baseline, so the
contract no longer states a stale starting point.
The M0 contract, its output-capture supplement, ARD-004 and the lmux user guides
were committed as a snapshot while the implementation they describe was still
uncommitted. Several "implemented / wired / passed" records therefore cannot be
reproduced from the commit that carries them: apphost.managed.*,
transcript/writer_lease.py, journal/_rooted_io.py, coding/cli/lmux*.py,
appserver/managed_mux.py and scripts/dev/reevaluate_g18_comparison.py are all
absent from this tree, and the guides document a `lmux` entrypoint that does not
exist here.

Keep the records, but state the scope so a reader cannot mistake authoring
working-tree observations for committed evidence:

- lmux-contract-m0.md: add an Evidence scope status field naming the uncommitted
  implementation and requiring per-slice claims to be read as working-tree
  observations until the matching code slice lands
- ARD-004: change "implemented" to "accepted decision; implementation observed
  only in the authoring working tree" and name the uncommitted reevaluation tool
- user guides (en, zh-CN): state that a build from this commit has no `lmux`
  entrypoint yet and that the commands are the intended interface, not a
  shipped one

The partial-goal framing was already correct and is unchanged; only the
evidence scope was over-claimed.
Add a Linux rooted-file adapter that borrows an already-owned directory descriptor, keeps cleanup and publication receipts explicit, and never falls back to pathname authority after admission. Wire JSONL reads, appends, rewrites, repair and unlink through the borrowed adapter while preserving the existing pathname mode for callers without a retained root.

Cover replacement-root attacks, native boundary races, cancellation and cleanup debt, directory bindings, durable deletion, and single-publication inode witnesses.
Introduce the shared retained directory lease, transcript writer lease and store-admission witness used to bind one logical writer to a validated Linux root. Keep creation, binding, process inheritance and cleanup outcomes explicit and fail closed on replacement, unsafe modes, duplicate authority or unknown close results.

Cover private root creation, cross-process exclusion, store witnesses, identity replacement, cancellation boundaries and retryable cleanup without importing transcript lifecycle consumers.
Add descriptor-relative blob transaction IO and an explicit blob writer lease on top of the shared directory authority. SessionBlobStore can now borrow a retained data root or operate read-only without reopening mutable pathname authority.

Cover root replacement, manifest/object publication, rollback and deletion, read-only rejection, writer exclusion, cleanup debt and identity-preserving recovery.
Route file-backed conversation records and JSON indexes through borrowed rooted handles, and add a shared native-settlement helper so cancellation never turns an accepted filesystem effect into an implicit retry. Preserve the existing pathname mode for unowned callers.

Cover replacement roots, index publication and recovery, subprocess exclusion, cancellation, cleanup failure and unknown native outcomes.
Compose retained transcript and blob writers with rooted conversation storage, explicit store-family admission, Product runtime binding and one owner for construction, delivery, import, fork, delete and cleanup. Linux maintenance deletion now requires the caller's retained authority instead of constructing an implicit writer.

Carry the authority through catalogs, model-input indexes, images, bundles and session factories. The complete transcript suite covers read-only discovery, process exclusion, replacement races, rollback, cancellation, unknown outcomes and cleanup retry.
@zhnt
zhnt merged commit e71f086 into main Sep 22, 2026
66 checks passed
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.

1 participant