Skip to content

ci: clone meta_core, which loop_lib now depends on - #14

Merged
gold-silver-copper merged 3 commits into
mainfrom
fix/meta-config-checks
Oct 26, 2026
Merged

gold-silver-copper merged 3 commits into
mainfrom
fix/meta-config-checks

Conversation

@gold-silver-copper

@gold-silver-copper gold-silver-copper commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

CI only: loop_lib now depends on meta_core, so CI clones both, from the same branch when it exists.

One change set across these PRs, all on branch fix/meta-config-checks (meta's CI builds each child from that branch):

Fixes three findings from the 2026-10-24 fuzz run (all three reproduced on main 4dc5fa3 before this change). KB: tasks/harmony-2749, incidents fuzz-2026-10-24-12, -15 and -18.

What changed and why

12: .meta paths and names are validated once, when the config is parsed (meta_core::config::parse_meta_config). Before, an absolute path, a ../ path, or a name like ../../../../x made meta git clone write outside the workspace, made meta exec run in unrelated repos, and put worktrees outside .worktrees/. Every caller joins path onto the config's directory, so checking at the single parse point covers them all. Worktree creation also checks each child destination right before adding it, because the root repo's checkout inside the worktree can hold a symlink where a project's worktree goes. The rules:

  • a name is a relative path in plain form, usually one word; a nested alias such as vendor/tree-sitter-markdown (as gitkb's config has) is fine, because worktrees put the project at <worktree>/<name>
  • names and path components use only ASCII letters, digits, space and ._-+@, so that one .meta names the same directory on every platform: no Unicode that macOS or Windows would normalize or fold into another name, no C:, <>:"|?* or ~1 short names. Also no trailing dot or space (Windows drops it, so a. is a), no device name (NUL, COM1), no .git component (a project inside a git directory, e.g. its hooks), and no path under .worktrees, as written or through a symlink
  • path is relative and stays inside the config's directory: no .., no absolute paths, no existing symlink that leads outside. It is normalized (./a//b/ becomes a/b).
  • no two projects share a path, and no project's path lies inside another's (compared where they resolve); names may not nest or collide by case either, since they are worktree destinations. Otherwise a clone of the outer repo could check out a symlink that the inner path then follows out of the workspace. A repo inside another goes in a nested .meta (meta: true).
  • no value (name, path, repo, tags, provides, depends_on) is empty or holds a control or bidi-control character
  • repo cannot start with -, because git clone <repo> <path> would read it as an option
  • tags hold no whitespace and no , (the --tag filter splits on ,)

A bad entry fails the command with the file and project named:

Error: Invalid project 'abs' in /…/hostile/.meta: path '/…/dropped-abs' must be relative

A nested .meta that fails to parse or validate is now an error, both from walk_meta_tree and from meta git clone --recursive/update. Before, its repos were silently dropped and the command exited 0.

15: the store lock is an OS file lock (std::fs::File::try_lock: flock on Unix, LockFileEx on Windows). The OS releases it when the holder exits or crashes, so there is no PID and no staleness check. A leftover lock file (empty, garbage, or naming a reused PID) is just a file that gets locked again. The file is never deleted, so two racing processes cannot lock two different files. If the lock is held for more than 5 s, the command fails with a clear message. The lock file is 0600 (an older one is tightened), because anyone who can open it could hold the lock and block every store write; on Windows a lock directory other accounts can read is refused for the same reason. Store, cache and registry-config files are written 0600 and read only when they are regular files owned by you that no one else can write (including through a macOS ACL or a Windows access list, inherited or not); the lock file gets the same check for opening. A plugin is run only if no directory or link on the way to it, and not the file itself, can be changed by another user. A lock file that is a symlink is refused, and store writes use a fresh temp file per writer. The PID scheme, is_stale and the Windows OpenProcess FFI are deleted.

meta git worktree create/add/remove/prune, and the cleanup after exec --ephemeral, now exit non-zero when the store update fails. Before, they printed a warning and exited 0, and the worktree never got TTL or ephemeral cleanup. create rolls back what it added when it cannot record it, using meta-git-safety's rollback (which keeps a branch it cannot prove it made); remove reads its store record before deleting, so the record is found even through a symlinked path. Each child worktree destination is checked to stay inside the set before git adds it. When an ephemeral exec command fails and its cleanup fails too, the cleanup error is still printed. The worktree plugin also prints the error's full cause chain ({e:#}), so the reason is visible.

Multi-user machines: ~/.meta is created 0700. A data or lock directory that another user owns or can write to is refused before anything in it is read or used. The same applies when it sits behind a directory another user could rename it in: the check walks the path as written, through each symlink it follows, and every directory holding an entry must belong to you or root and be writable by no one else unless sticky (like /tmp). On macOS a directory whose ACL has an allow entry is refused too (ACLs can grant write the mode bits do not show). On Windows the same rule reads each directory's owner and access list: an account other than you, SYSTEM, Administrators or TrustedInstaller that owns the data directory or may change it (or rename it through a directory above) makes meta refuse it, and a link or junction on the way is refused. This covers: the worktree store (so prune cannot act on planted entries), the context cache, the registry config, and global plugins. Plugin discovery prints a warning and skips a refused ~/.meta/plugins, and any .meta/plugins found walking up from cwd that another user owns or can write to (such as a planted /tmp/.meta/plugins). Plugins in your own checkouts are unaffected unless a directory on the way is group- or world-writable; under umask 002 (the Fedora/RHEL default) checkouts are, so a workspace's .meta/plugins is skipped with a warning and a chmod hint. Programs on PATH are the user's own choice, like the git meta runs, and get no such check: only one that leads into a plugin directory refused in the same run (through any link or folder on the way) is skipped. The refusal comes with a chmod go-w hint. meta_core::meta_dir() now returns an error when there is no home directory, instead of falling back to the shared /tmp/meta-fallback.

18: untrusted text is escaped in human-readable output through one helper, meta_core::display::escape. It writes control characters (C0, DEL, C1), bidi controls and invisible format characters (zero-width, tag characters) as visible \u{1b}, \n and so on. It is used in:

  • meta project list: tree, root remote, dependents
  • meta context: also escapes | so a value stays in its table cell, because agents read this markdown
  • worktree list, status, create (and its dry run), add, diff and prune: names, aliases, branches, paths, store entries
  • the ssh remote-mismatch report and the update stray-directory warning
  • meta_cli worktree-context and orphan warnings, and verbose paths
  • loop_lib (the meta exec engine): directory names in progress and result lines, and dry-run and verbose paths. loop_lib now depends on meta_core for this, so its CI and loop_cli's clone meta_core.

Errors are escaped where they leave each program: the meta-git and meta-project plugins' execute, and meta's main. They use display::escape_lines, which keeps newlines, so multi-line errors read as before.

--json output is unchanged; it was already escaped.

worktrees_dir in .meta, where worktrees are created and removed, must lie inside the workspace (a relative path, or an absolute one inside it); a directory elsewhere is set with META_WORKTREES. A .worktrees symlink leading outside is refused too, and so is a worktree set directory that is a symlink; prune acts on an expired worktree only while its stored (canonical) path still resolves to itself, so a path that has since become a symlink, or sits under one, is skipped. No project may sit inside the worktree directory, as written or where it resolves, and a worktrees_dir that breaks these rules is an error when the config is read.

How it was verified

  • Every .meta file of the real harmony workspace (16 files, copied to a scratch dir) parses with the new rules: meta project list --recursive lists the same 97 projects as main.
  • The fuzz repros (05-meta-paths-escape-workspace, 07-pidless-lock-silent-store-loss, R2-04-terminal-escape-injection) were re-run with throwaway binaries in a scratch HOME. On main all three reproduce. With this change:
    • 05: clone and worktree create refuse the config, and nothing is written outside the workspace.
    • 07: create takes 0.02 s instead of 5 s, the store records w1 and w2, and prune sees the TTL.
    • R2-04: every command refuses the config, and the error shows the escaped name.
  • New tests, each failing on main and passing here:
    • bats: project_list.bats (path outside the workspace, symlink escape, terminal escapes) and worktree_edge_cases.bats (leftover lock file, store not writable on create and on remove, data dir writable by others)
    • unit tests in meta_core: config rules, path normalization, symlink escape, lock, and display; plus meta_cli context and meta_project_cli tree tests
  • Locally: cargo fmt --check, cargo clippy --workspace --all-targets -D warnings, and cargo test --workspace pass, except two registry::tests::*_fails_outside_workspace tests. Those fail on unmodified main too on this machine, because they depend on the environment. The project_list, worktree_edge_cases, worktree and context bats suites (132 tests) pass.

Please look closely at

  • Rebased onto meta-git-safety (meta a640b54, meta_git_lib 9f8e270, meta_git_cli 79af652). Both changed the same worktree code, so meta_git_lib and meta_git_cli are now one squashed commit each on that main. Where both did the same job, meta-git-safety's code was kept (create rollback, git_worktree_add, remove/prune checks) and this PR's own rollback and its tests were dropped. This PR adds on top: store failures in remove/prune fail instead of warning, every child destination is checked before git worktree add, and paths, file names and git output in the new safety messages are escaped. Two of its tests used worktrees_dir values the config rules refuse (. and ../link); they now assert the refusal or use a link inside the workspace.
  • Rebased onto meta-shell-args (gitkb/meta 47eeca3). That change lets --include/--exclude select repos whose path has .. (a repo outside the root, or a second spelling such as libs/../api). Finding 12 forbids such paths, so these configs are now refused when read, before any repo is selected or any command runs. Its two new tests in tests/exec.bats were adapted to that (the recursive --config test drops its .. back-edge; the --exclude test now checks a duplicate spelling is refused). repo_filter's handling of .. stays as written but no longer sees such paths.
  • Windows checkouts outside the profile. Folders created under a drive root (C:\src) inherit "Authenticated Users: Modify", so their .meta\plugins is skipped and a META_DATA_DIR there is refused, with a hint to remove other accounts' write access (icacls) or use the profile. That is the same rule as on Unix.
  • Non-ASCII project names and paths are refused. It is the simplest rule that keeps one name from meaning two directories across platforms (normalization, case folding, short names).
  • Group-writable directories are refused, even with a user private group. On systems with umask 002 (Fedora, RHEL), directories are 0775. A ~/.meta or a checkout's .meta/plugins under such a tree is refused with a chmod go-w hint (the store fails, plugins are skipped with a warning). An exception for "your own private group" was tried and dropped: POSIX ACLs and other accounts sharing that primary group can widen group access in ways the mode bits do not show.
  • Symlink escape is checked when the config is parsed, against the symlinks that exist then. Within one run, only meta's own clones create directories, and the ban on overlapping project paths means no clone can create a directory that another project's path goes through. A nested .meta is parsed, and checked, after its repo is cloned.
  • Overlapping project paths are now an error. A config that lists both a and a/b must move a/b into a nested .meta. The meta_mcp atomic-batch test used such a config; meta_mcp#12 switches it to a plain directory inside the workspace root's repository, which keeps it covering the same refusal (a project that is not the root of its own repository).
  • Sandboxes with a user namespace that maps only you (some bubblewrap or rootless setups) show every other account's files, root's included, as uid 65534. meta cannot tell those apart from another user, so it refuses the data dir there, with an error that says so. This is the strict reading of "refuse what another user owns"; loosening it is a decision for a human.
  • meta git commit --edit no longer writes its message template to a fixed /tmp/META_COMMIT_EDITMSG; it uses an exclusively created file in ~/.meta/tmp. So it now needs a private data directory, like the worktree store (reviewers disagreed on whether a random file in the system temp dir would do; the owner's rule is no shared /tmp default).
  • Filesystems without Unix ownership (FAT/exFAT, some network mounts) show modes from mount options; when those make files group- or world-writable, plugins and data dirs there are refused with the usual warning.
  • NFS homes. An OS file lock needs lock support from the filesystem. NFSv4 and NFSv3 with lockd have it. Where there is none, a store write fails with "Failed to lock …" instead of silently doing nothing.
  • Mixed versions. An old meta binary (PID lock) and a new one (OS lock) running at the same moment do not exclude each other. This is pre-release, so there is no compat shim.
  • crates.io: loop_lib (published) now depends on meta_core (published) with the range <1. Child-repo CI builds meta_core as 0.0.0 or 0.1.0, and releases use the workspace version, so both satisfy it. The release workflow publishes meta_core before loop_lib.
  • An absolute worktrees_dir outside the workspace is now refused, with a pointer to META_WORKTREES.
  • Not escaped here: meta_plugin_protocol prints what plugins hand it; the plugins escape their errors before that. (meta git snapshot no longer exists: meta-git-safety deleted it.)
  • Error text keeps its newlines. At the error boundary, escape_lines escapes every control character except \n, because several errors are multi-line by design. Paths in the errors this PR writes are escaped where the message is built.

Overlap with other open meta PRs

meta_git_cli (worktree mod.rs, create.rs, remove.rs, prune.rs, add.rs, ssh.rs, update.rs) and meta_cli (main.rs, context.rs) are also touched by the meta-git-safety and meta-shell-args work. The edits here are small and local (store error handling and escaping at print sites), so whichever merges second should rebase easily.

@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 09360e73-87fa-4ead-8766-840440b8351b
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

Clones loop_lib and meta_core from the same branch when it exists, falling back to main.

Implements [[tasks/harmony-2749]]
@gold-silver-copper
gold-silver-copper merged commit 2b51829 into main Oct 26, 2026
27 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