3293cb5ff40c3eb18a599ce9113ed74d3d8d1607
20 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3293cb5ff4 |
feat(py): mirage mcp over stdio, and invert the mypy allowlist
Two cleanup-plan items, both Python-side.
Item 29 -- `mirage mcp`. TypeScript shipped a six-tool stdio MCP server;
Python had none, so a pip-install user could not point Cursor or Claude
Desktop at a workspace and `mirage --help` differed by distribution.
Adding the entry point alone would have duplicated the tools, because
this side kept them private inside the Claude Agent SDK integration, so
the shared layer comes first:
- agents/tool_descriptions.py -- the six strings, one copy.
- agents/tool_operations.py -- MirageToolOperations, lifted out of the
SDK server's private _MirageTools.
- agents/file_version.py -- stale-write protection, which this side
lacked entirely. TS stamps stored bytes; here the stamp covers the
rendered bytes, because this read tool has always rendered and an
edit must search what the agent was actually shown.
- agents/mcp/server.py + cli/mcp.py -- the server and `mirage mcp`.
- server/workspace_config.py -- config discovery (candidates, env
names, walk up from cwd), which Python had nowhere, so every entry
point had to be handed an explicit path.
The server is the low-level MCP Server rather than FastMCP: FastMCP does
not forward a version, and TS advertises one. Handlers are bound methods,
not decorated closures, so nothing nests.
Item 28 -- the mypy allowlist. 54 modules opted *in* to annotation
checking against 1826, so the default was unchecked and every new file
joined the unchecked side. The default is now strict, with a list of
what is not yet annotated that only shrinks. 166 annotations cleared
along the way; the remainder is named module by module.
Two real defects surfaced by the annotations, neither of them typing:
- Workspace._original_open / _original_os were invented by assignment
in lifecycle.patch_process, so unpatch without a patch raised
AttributeError. Declared, and the restore is guarded.
- sed_generic declared a non-optional writer while its own docstring
and its `write_bytes is None` branch said otherwise; the builder
passes None whenever the backend cannot write.
Tests keep the PathSpec rule instead of full strict: measured, full
strict on python/tests is 2374 errors, of which 634 are `str` where a
pydantic field declares SecretStr -- which pydantic coerces at runtime --
and most of the rest is the monkeypatched-fake pattern CLAUDE.md
sanctions. The rule that is violated for real is PathSpec, 19 times, and
scripts/check_test_pathspec.py now holds that line. One of the 19 was a
latent AttributeError: tests/e2e passes a str to s3 write_bytes, which
reads .mount_path, and the test skips without a live versioned bucket.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6748e66c55 |
fix(stat): render the symlink arrow for %N (#779)
* fix(stat): render the symlink arrow for %N stat -c '%N' on a symlink printed only the quoted name. GNU prints 'name' -> 'target'. The target was already on FileStat.extra under LINK_TARGET_KEY, which is what ls -l and file read, so %N now reads the same fact. Pinned against GNU coreutils 9.7 on debian:stable-slim, which showed two more rules: any format modifier drops the quoting (only a bare %N quotes), and a width or precision applies to the name and the target separately rather than to the joined line. That second rule had to be implemented either way to render %20N on a link, and it also fixes %30N on a plain file, which used to keep the quotes. Tests in both languages plus six integ cases across all 22 targets. * fix(stat): quote %N the way gnulib shell-escape does The helper picked double quotes for any name holding an apostrophe, so a'b$c rendered as "a'b$c" and replaying the advertised shell-safe output expanded $c. It also passed control characters through raw. Probed GNU 9.7 across ASCII 32-126 and the C0 range: single quotes are the default with '\'' escaping, double quotes are an optimization for a name whose only awkward character is an apostrophe, and a control character becomes a $'..' segment spliced between single-quoted runs. Now applies to a symlink target too, since %N quotes each field on its own. |
||
|
|
0d5915457f |
feat(workspace): state doors and per-plane views (#771)
* feat(workspace): state doors and per-plane views * fix(workspace): one general session door for every variable write Codex round: shell readlink dispatches through the op door; array-shaped assignments and subscripted unsets can no longer bypass pre_session. SessionView.set is now general over variable shapes (str | ShellArray), pre_session_gate is private to the door and takes a SessionContext that carries session_id, staged declaration arrays store through the builtins, for/select loop variables write through the view, and readonly pre-checks are separated from policy-denial catches at every shell site. Also fixes export ARR=(x) printing the export listing and CI formatter drift. * test(integ): pin the state-door fixes; readonly declaration arrays abandon the line * ci(integ): key the WinFsp install on the DLL, not choco's exit code |
||
|
|
ef99849b94 |
refactor(seam): dispatcher-built CommandOpts, typed adapter ops, named ParsedCommand, one DispatchFn home (#609 items 26+23) (#772)
* wip(seam): py dispatcher builds CommandOpts; handlers take (accessor, paths, texts, opts); adapter op protocols; DispatchFn typing Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * refactor(seam): dispatcher-built CommandOpts, typed adapter ops, named ParsedCommand, one DispatchFn home (#609 items 26+23) Item 26 (T2-8 seam typing), all four pieces, plus task 23's static FlagView query-name gate: - The python dispatcher (Mount.execute_cmd) now constructs one CommandOpts per invocation and calls every handler as fn(accessor, paths, texts, opts) — the TS convention. Flags stop sharing a bag with injected context, accepts_kwarg opt-in dies, and all 72 builders, 75 bespoke wrappers and ~30 provision functions become pure wiring; the provision path builds the same bag with command/spec set. Generics that took trailing fact params (ls, find, tree, zip, tar, du, file, stat) now read opts.links/opts.mounts/... like their TS twins. - adapter.py gains per-slot op protocols (ReaddirOp/StatOp/WriteOp/...) mirroring adapter.ts, so wiring readdir where stat belongs no longer type-checks; Builder.fn/provision and CommandIO fields are typed. Surfaced and fixed real drift: hf/databricks mkdir had index before parents (a positional parents call would land in index), wget -O dropped its PathSpec value under as_str. - TS parseFlags returns a named ParsedCommand (twin of the py NamedTuple) instead of a 15-slot positional tuple; optionError takes (cmdName, parsed) like python's option_error. - DispatchFn moves to runtime/types.ts (python's home for the same protocol); the crossmount re-export inversion and the CommandDispatch duplicate are gone; 38 python files stop spelling it Callable[..., Any]. The dead CommandOpts.resource field (zero readers) is deleted along with the CrossResourceStub that existed to fill it. - Task 23: tests/commands/test_flag_query_names.py and commands/flag_query_names.test.ts fail on a FlagView query naming a dest no spec bound in the module declares, without needing the code path to run (both verified to fire on a planted typo). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(examples): redis example calls provisions with the 4-positional shape file_read_provision/head_tail_provision/metadata_provision are (accessor, paths, texts, opts) now; the direct demo calls still passed the old command= kwarg, crashing the example before the persistence section (CI examples gate). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * test(integ): pin the #772 fixes where the harness can reach them - find -empty on chroma + github (the wrappers used to drop the flag, which would flood every path; -empty now rides find_generic), plus a -not -empty composition case on each so the pin has positive output - mkdir -p gains databricks/databricks-prefix (zero prior mkdir coverage on that backend; the param-order bug itself is only reachable positionally, so MkdirOp+mypy is its real gate) - wget -O / curl -o were already pinned by integ/resources/http Both hosts green: py chroma+github 83 ok, ts all five targets 4070 ok. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: bytecii <bytecii@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ae36ac41ee |
test(integ): report-and-continue pins + builder wiring gate
postgres/mongodb mixed good+missing operand facets, history cross-mount continue, test_builders_declare_only_dispatcher_params ratchet; history ls/tree/stat/find and dify find/cat wrappers ride the generics. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
836047e286 |
refactor(commands): builders become wiring; generics own the flags (T2-2)
26 builders forward **flags wholesale; each generic parses once into a frozen struct via a spec-bound FlagView (ls/du/find/sed/tar/tree + 20 mechanical). rm/rmdir/ln/touch are builder-is-implementation and read the bag through FlagView inline, mirroring the TS builders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a16684823d |
fix(symlinks): report links across commands, and add -R to chmod/chown/chgrp (#688)
* fix(symlinks): report and follow links the way GNU does Symlinks live in the namespace, so no backend readdir or stat can see them and every command was blind to them unless wired by hand. Six GNU divergences, each pinned against debian:stable-slim first, mirrored in python and typescript. - ls -l on a dangling link failed the whole listing with exit 2; GNU prints the link row and exits 0. ls -l/-d on a live link silently followed it, and ls -R and -F omitted links entirely. - readlink -e printed a dangling target and exited 0; GNU requires the whole resolved path to exist. -f now requires only the parent. - file followed the link and relabelled the operand as its target; GNU prints "symbolic link to <target verbatim>", or "broken symbolic link to ..." when the target is absent. - du omitted links; GNU lists one line per link under -a and does not follow a link operand without -L. - find followed a symlink start point; GNU's default is -P, which reports the link and stops. -P/-H/-L are leading options, not predicates, so the expression splitter consumes them first. - -L has to apply below the operand too, not just to it: ls -L stats each link child and reports the target under the link's own name, and du -L stops counting links as entries of their own. Generalize the seam while it is being touched: - A command opts in by naming a `links: LinkView` parameter. execute_cmd offers the fact to every handler and accepts_kwarg reads the signature to decide delivery, so there is no allowlist to keep in step. This also removes the hardcoded stat_overlay command list. TypeScript needs no equivalent: a command that does not read `links` off its context ignores it. - LinkView bundles all five link facts, so a new need is a field read rather than a keyword threaded through four layers. - Link merging lives in the generic above the native-op/walk fork, so a mount's symlink behavior cannot depend on whether its backend ships a native find or du op. - ls merges links as stat rows instead of appending to its own stdout, which is what makes -R, -F, -l and sorting work. Deletes inject_links and its latent dedup bug, where splitting a GNU long row on tabs returned the whole line. Also moves data shapes and constant tables to the modules the layout calls for (ops/types, commands/builtin/constants, utils/constants) in both languages, and adds 33 integ cases that run in both runners. du sizes a link at its target length, which is not a divergence: mirage counts bytes, GNU's --apparent-size mode, and there GNU reports len(target) too. Two known gaps, both documented rather than papered over: du -L undercounts a link pointing outside the operand's own subtree, and ls -RL does not descend a directory link. A version of the latter that only worked on RAM was removed rather than shipped, because descending needs the child read re-keyed onto the resolved target and that only resolves on backends with real directory entries. Symlink behavior that depends on which backend is mounted is the exact failure this branch adds a rule against. A link operand is caught on both readdir shapes. Backends without real directories (s3, nextcloud) answer readdir on a link path with an empty list rather than raising, so the link row has to be resolved there too or the operand renders as an empty directory. Pinned by unit tests against the generic in both languages, so the case is covered without needing a remote backend to reproduce it. Fixes #414 * fix(integ): drop the empty-directory dependency from the symlink fixtures * fix(symlinks): give bespoke stat wrappers the link table, and make find's link options last-wins s3 and gridfs ship their own stat command, and neither declared the links parameter, so signature-based injection silently withheld the namespace link table and stat on a symlink reported ENOENT on exactly those two backends. TypeScript is unaffected: its wrappers forward the whole opts object, so the generic reads opts.links regardless. find states its link policy as a leading option and GNU takes the last one, so find -L -P x must not follow while find -P -L x must. The scanner returned true on seeing any -L or -H anywhere. * feat(metadata): walk the subtree for chmod, chown and chgrp -R All three refused -R with exit 2. GNU splits three ways here: chmod -R skips a traversed link but follows a command-line link to a directory, while chown -R and chgrp -R change the link node itself because POSIX gives -R an implicit -P. Two bugs surfaced while pinning it. chown -h wrote uid onto the node but link_stat dropped ownership when rendering, so nothing ever showed it; only mode is meaningless on a link. And TypeScript rebuilt stat rows field by field where Python uses model_copy, dropping uid/gid on the link row, so FileStat gains a with() method and the four rename sites use it. * fix(find): classify a link by its target under -L find -L must test a link as what it points at, not as a link: a link to a file is -type f, a link to a directory is -type d, and only a dangling link stays -type l. link_results classified every namespace link as l regardless, so find -L d -type f missed the links and find -L d -type l wrongly named the live ones. Both find paths reach link_results, so the classification lands above the native-op/walk fork rather than in one branch. * fix(symlinks): stop backends losing links and -L one wrapper at a time `find -L` classifies a link by statting its target, and that stat was reached through the callable wired for the -mtime post-filter, which is gated on ops.local. Post-filtering costs one stat per result, so gating it is right; classification costs one stat per link, so gating it was not. Off the local backends nothing was statted and every link stayed typed `l`: -type f and -type d missed them, -type l still listed them. The fact belongs on LinkView, not on a threaded argument, so it is now `target_stat` / `targetStat`. It resolves through the op dispatcher rather than a backend key, which also fixes a link pointing into another mount: the old code built the key from the search root's mount prefix and could never resolve one. Pinned in integ/crossmount/find/deref.json. The same omission then turned up in ten bespoke commands that shadow a link-aware generic without declaring `links`, and in the same ten for `-L`, which lands in the wrapper's opaque flag bag when unnamed. Both failures run fine and exit 0, so nothing notices until someone makes a link on that backend. tests/commands/test_links_optin.py asserts it instead, deriving from the builders which link parameters each family requires; it caught history/ls on the first run. TypeScript needs no guard because wrappers forward the whole opts object. Its one exception, email/find, walks its own tree and never reaches a generic, so it now calls linkResults directly. * fix(find): trim slashes without a regex, and derive the link guard from the router Exporting linkResults made its input library-reachable, which surfaced the polynomial-ReDoS trim inside it (CodeQL js/polynomial-redos, high). Both `/^\/+|\/+$/g` trims now use the loop-based stripSlash helper. The guard's list of required link options was hardcoded. It now reads DEREFERENCE_FLAGS and LAST_WINS_LINK_OPTIONS, so a new link option taught to the router starts being required of bespoke wrappers too. * fix(symlinks): let real backend failures surface while statting a link target link_target_stat caught OSError around _stat_or_none, which already maps the missing-target case to None. The extra catch could therefore only swallow a genuine failure: a permission, connection or I/O error came back as a dangling link, printed as -type l with exit 0. Only the two legitimate no-target cases stay None now, a loop from namespace.follow and a missing target from _stat_or_none. Everything else propagates. Same in TypeScript, where the bare catch did the same. |
||
|
|
6e1b800c2a |
Fix dependency and CodeQL security alerts (#639)
* Fix dependency and CodeQL security alerts * fix(stat): port linear-time format parser to Python (CodeQL #247) The TypeScript fix left the identical backtracking regex live in the Python mirror, so stat -c FORMAT could still hang on a crafted format. Replace _FORMAT_RE with the same cursor-based directive scanner. |
||
|
|
426090484a |
feat(stat): full GNU stat -c directive set + agent-aware ownership (#609 Tier 1) (#626)
Pre-commit / pre-commit (push) Has been cancelled
CLI exit codes / changes (push) Has been cancelled
Test (Install) / changes (push) Has been cancelled
Integ / changes (push) Has been cancelled
Test (Python) / changes (push) Has been cancelled
Test (TypeScript) / changes (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / python-install (push) Has been cancelled
Test (Install) / ts-install (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-shared-py (push) Has been cancelled
Integ / integ-shared-ts (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Integ / integ-fuse-windows (push) Has been cancelled
Integ / runtime-py (push) Has been cancelled
Integ / runtime-ts (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Python) / runtime (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
|
||
|
|
1c193bc4e8 |
Optional-typing sweep: path-only generic ops, NULL_INDEX, no bare generics (#541)
* refactor(types): stdin annotations use the ByteSource alias * refactor(generics): injected ops are path-only, accessor+index bind at the wrapper Generic commands no longer take accessor (or a dead index) just to thread them back into injected callables. Builders and bespoke wrappers bind both via bound_op (None-passthrough) or partial for write-side ops, mirroring the TS builders' closures. call_*/resolve_pattern/relay helpers and the cache read-through wrappers gain path-first forms; CommandIO-level ops keep the raw (accessor, path, index) shape. * refactor(index): NULL_INDEX everywhere, no index-is-None branches index is a required IndexCacheStore on every op and wrapper; callers with no real index pass NULL_INDEX (the null object built for exactly this). Dead is-None guards drop out of trello/linear/github_ci/google readdir+stat, CacheManager, and the github wrappers. * style(types): parameterize every bare generic, gate with disallow_any_generics dict -> dict[str, Any] (payloads) / dict[str, object] (flag bags), Callable -> Callable[..., Any], plus list/tuple/Awaitable/Pattern/ Task/Future/Token/AsyncMongoClient. The mypy gate now carries disallow_any_generics so bare generics cannot come back. * style: pre-commit formatting + parameterize main's new bare dicts * fix: post-merge gate fixes + make security barriers analyzer-recognizable - resolve_within_root: startswith(root + sep) guard instead of commonpath (same semantics; the shape CodeQL models as a path-injection barrier) - cli table output: trimEnd() instead of the polynomial /\s+$/ trim - example proxies: re-encode validated endpoint segments before URL construction (recognized SSRF barrier; no-op for the allowed charset) - parameterize bare generics arriving from #540 (the new mypy gate caught them) * fix(server): plain startswith barrier in resolve_within_root |
||
|
|
5244558e29 |
Fix latent type-safety and lifecycle bugs (#528)
* Fix latent type and lifecycle bugs * fix: close ownership for shared resources, gate find -empty, gdrive populate parity, drop mongo DriverInfo * style: yapf/isort formatting on merged test files * fix: fan-out grep keeps GNU any-match exit 0, mock github_ci stat populate readdir, current mock s3 mtime * fix(ts): mirror close ownership, shared stores and copy-shared resources stay open * refactor: shared op-fn aliases and copy/move strategies in mirage.types, tar package with types and constants * refactor: single mtime-window primitive, drop msgraph.time re-export, github declares supportsSnapshot * fix: find -mtime keeps unknown-mtime dirs (s3 integ), mktemp -p PATH-kind spec parity * fix(find): remote -mtime reverts to TS parity, onedrive root stat carries mtime - generic_bind find builder: stat filter only for local backends (matches main + TS; remote backend find ops ignore mtime like the TS s3 core) - s3/onedrive core find: drop in-core mtime filtering, document the deliberate no-op in the s3 docstring - onedrive stat: mount root fetches the real Graph root item so size/modified are populated (du/find no longer see modified=None) - integ: fake Graph stamps items at run time like moto; bespoke onedrive suite freezes the clock for deterministic ls -l; stat_dir case prints stable fields only * test: drop remote find -mtime tests reverted to TS-parity semantics |
||
|
|
ea8096f155 |
Drive Python mypy to zero and enable it as a hard gate (#522)
* chore(py): shrink mypy baseline 117->84 (resource accessor typing) Narrow each resource's accessor attribute to its concrete type so the backend resolve_glob/stat calls type-check; wrap fingerprint stat() paths with PathSpec.from_str_path (fixes a latent str-to-stat bug); retype the shared hf_buckets core to _HfAccessor (common base of the hf siblings); make DevStore a RAMStore subclass (_DevFiles a dict subclass); narrow the sync redis get_state() reads (redis-py ResponseT is an async/sync union). Full Python test suite green (9139 passed; fuse excluded, macOS limit). * chore(py): shrink mypy baseline 84->74 (workspace/executor/shell/resource typing) * chore(py): clear non-generic mypy errors, shrink baseline 74->56 * chore(py): clear all generic command-layer mypy errors (127->0) * chore(py): enable mypy as a hard gate (pre-commit + CI), drop ratchet baseline * fix(py): sed -i guards write availability; dedupe mypy pre-commit hook post-merge * fix(py): pyproject trailing newline; run mypy on hosted pre-commit too |
||
|
|
e1df638356 |
Read family keeps partial output past missing operands (#464)
* fix(commands): read family keeps partial output past missing operands cat/head/tail/wc abort on the first missing operand: the error reaches the executor chokepoint before any output is emitted (or the lazy drain discards it), so the good operands' output is lost. GNU keeps going: partial stdout, one stderr line per failed operand, exit 1. Builders now partition operands eagerly (split_readable / splitReadable) and stream only the readable ones; wc's format_multi reports failed operands and still prints the total row (0 total when none resolve). Cross-mount run_operands catches filesystem errors at materialize so a lazy drain failure no longer escapes to the route catch-all, which dropped every operand's output and printed a line with no strerror. Single-mount and cross-mount are byte-identical in both languages; integ covers cat/wc/head/tail good+missing on ram, redis, and s3. * refactor(errors): FS_ERRORS single catch set; document grep exit rule The recoverable filesystem exception tuple was spelled out at five catch sites; derive FS_ERRORS from the strerror table in utils/errors so the catch set and the formatter cannot drift (mirrors TS isFsError). Hoist runOperands' TextEncoder to module scope. Document at combinedExit that grep's exit 0 on match-despite-read-error is a deliberate GNU divergence pinned by integ. * fix(commands): read family sweep processes every operand, keeps partial output cut, tac, zcat, and strings processed only paths[0], silently dropping every other operand (cut even exited 0 with a missing operand). They now handle each operand independently and concatenate in operand order, and nl's counter continues across operands instead of resetting per file, all per GNU. The remaining multi-file read builders (nl, md5, sha256sum, strings, tac, rev, cut, expand, unexpand, fold, fmt, zcat) and the stat generic now continue past a missing operand: partial output, one stderr line per failed operand, exit 1 (resolve_readable/withReadable wrap the shared split). sort keeps its GNU-style abort. Cross-mount STREAM errors respell the internal cat sub-run's prefix to the real command name, so cross-mount bytes match single-mount. Integ pins nl (STREAM respell), md5 (FANOUT), and the previously uncovered cross-mount stat on ram, redis, and s3. * test(integ): partial-read battery on every backend; sed keeps going, sort aborts Add PARTIAL_READ_CASES to the shared battery (both langs, truth.txt): multi-operand correctness for cut/tac/nl/strings/zcat plus good+missing for the whole read family, so all 13 shared-truth backends pin partial output, per-operand stderr, and exit codes. cross_commands adds cut/tac/sed/sort and the missing cross-mount stat coverage. Pinning exposed two more divergences, fixed both langs: sed aborted single-mount but GNU keeps going past a missing operand (generic_sed now skips and reports per operand, including -i, exit 1 where GNU exits 2); sort kept partial output cross-mount but GNU aborts (run_stream now drops the body for sort when any operand failed, matching single-mount). * fix(ts): missing-operand handling moves into the read-family generics The withReadable builder wrapper only protected factory-built backends; OPFS's bespoke wrappers call the generics directly, so integ-ts lost the partial output on every swept command. The per-operand read handling now lives inside the generics themselves (readOperands/operandsIo in utils/operands), like cat/head/tail/wc/sed already do, and the factory builders go back to thin wiring. nl, cut, rev, and sha256sum read their operands eagerly in paths mode so the IOResult is sealed before the output stream is handed back (tail/wc already buffer this way). * refactor(crossmount): respell helper named for the fetch, prefix from Cmd.CAT The rewrite is an artifact of the STREAM strategy fetching operands via a native Cmd.CAT sub-run, not anything about the cat command itself, so the name says what it means and the stripped prefix derives from the enum instead of a hardcoded literal. |
||
|
|
ce39e351ce | chore(generic): type accessor as Accessor, document probe except-pass sites | ||
|
|
31340ad9ca |
refactor(shell): raw_path owns word spelling; respell lazy drain errors
- relative_spec: one constructor for typed word + cwd -> PathSpec - raw_path is total (defaults to virtual); PathSpec.display deleted - word_text/wordText: single str-or-spelling coercion helper - rebase_display -> rebase_raw, nullable param dropped - lazy-stream drain errors respell operands as typed via ExecutionNode.paths (TS drain now also formats GNU lines) - integ: relspell battery (walker labels, error spelling, ./ glob, du trailing slash) |
||
|
|
e80af8fe0b | feat: GNU-aligned cwd/env, subshell isolation, and relative-path display | ||
|
|
98e9d8bb22 | fix: stat -c %n prints the operand path like GNU stat | ||
|
|
2671ad7517 |
fix(commands): terminate generated record output with newlines (#147)
* fix(commands): terminate generated record output with newlines
Add a shared record-output helper for commands that build logical text rows, and use it across affected find/grep/rg/ls/tree/stat/du/md5/cmp/file/wc call sites.
This keeps byte-preserving commands untouched while making generated command records end with POSIX-style trailing newlines. Update focused generic, RAM/Redis, and mocked Discord tests for the new output contract.
* fix(commands): terminate du and rm verbose record outputs
* test(commands): update assertions for terminated command output
The newline-termination fix makes find/grep/rg/ls/stat/tree/wc/du
emit a trailing newline on the last record. These assertions still encoded
the old separator-style output (no final newline) and failed in CI:
- dify/discord/github/workspace: expect a trailing "\n"
- notion: switch to splitlines() so the terminator doesn't add an empty
member to the compared set
- github wc: rstrip the newline before splitting on tab
- s3 integration: expect the blank separator line that `echo >>` always
intended — the missing grep terminator had previously swallowed it
Test-only; no production code changed.
* fix(commands): terminate grep/rg -c count output
* refactor: use format_records at remaining grep/rg/wc sites; add header to output.py
DRY-only: every changed site is guaranteed non-empty (or guards the empty
case separately), so output is byte-identical. discord stderr helper uses
format_optional_records to preserve None-on-empty.
---------
Co-authored-by: Zecheng Zhang <zechengzhang97@gmail.com>
|
||
|
|
0ac9cecca9 |
refactor(commands): consolidate cloud backend wrappers (s3/gdrive/google workspaces) (#70)
* refactor(commands): consolidate s3/gdrive/gdocs/gsheets/gslides/gmail wrappers Apply the same thin-wrapper + generic-dispatch pattern that landed for ram/disk/redis in #68. Each cloud backend wrapper now does just three things: glob-resolve paths, inject backend-specific callables (read_bytes / read_stream / readdir_fn / stat_fn / mkdir_fn), and pass through stdin/options to the generic implementation. Backends touched (84 wrappers, net -2487 lines): - s3: 34 wrappers - gdrive: 33 wrappers - gdocs / gsheets / gslides / gmail: 4 each (basename, dirname, realpath, tree) Kept resource-specific (per "use resource-specific when generic doesn't fit" guidance): - s3/du, s3/find: custom S3 listing semantics - gdrive/du, gdrive/find, gdrive/sed: no core/gdrive/du, find, or write - gdocs/gsheets/gslides/gmail jq, nl, find: no core/stream or core/find - Folder-based commands (cat/, head/, tail/, ls/, stat/, file/, cut/, grep/, wc/): dispatch over format variants (parquet/feather/etc.) - gws_* domain-specific Google commands: no generic equivalent For Google backends that only expose `read` (no `read_bytes`), the wrappers alias `from mirage.core.<b>.read import read as read_bytes`. This makes gdrive's special-format handling (.gdoc.json / .gsheet.json / .gslide.json -> JSON bytes) transparent to the generic commands: they see "just bytes" regardless of source format. gdrive/__init__.py renamed `sort_cmd` import to `sort` for consistency with other backends. Streaming semantics preserved verbatim: read_stream callables continue to chunk-iterate S3 GetObject responses, backpressure propagates through multi-stage pipes (verified end-to-end on s3 and gcs -- `cat | tr | grep | head -n 1` downloads only 8 KB of a 10 MB object). Two pre-existing example bugs fixed while verifying: - examples/python/s3/s3.py: Workspace.load was passing only /s3/ override when the workspace had both /s3/ and /deep/ mounts. Added the missing /deep/ override. - examples/python/gcs/gcs.py: called ws.reset() which never existed on Workspace. Replaced with ws.cache.clear() (which is what the surrounding code clearly intended given the cache.get() assertions). Also extended the example with streaming + multi-stage pipe backpressure demonstrations. .gitignore: added docs/learnings/ alongside docs/plans/ for local-only notes. Tests: 6203 passed, 266 skipped (no agent tests run). Pre-commit: all hooks pass. * fix(gdrive): thread index into delegated command closures (gdrive/gdocs/gsheets/gslides/gmail) * test(integ): add moto-based S3/GCS cross-backend integ harness * refactor(commands): promote index to an explicit param in backend command wrappers * refactor(filetype): redirect ls/file for parquet/orc/feather to shared core handlers * refactor(commands): delegate cloud/s3 content commands to generic layer - thread index through generic ls/stat so delegated cloud stat works in walk - migrate gdocs/gdrive/gmail/gsheets/gslides/s3 cat/head/wc/ls/stat/jq/nl off inlined logic - remove dead package-shadowed cat/head modules * test(integ): add streaming byte-accounting cases to s3/gcs harness Clear cache, run, and report bytes pulled from the backend for early-exit commands (head -c/-n, grep -m) vs a full read, per mount. Output is deterministic (no timing) and identical across s3/gcs, proving parity. * refactor(integ): iterate s3/gcs cases over a single MOUNTS loop * fix(commands): use index fast-path for stat in tree/grep/rg - generic tree threads index to stat (uniform with ls); cloud tree wrappers stop binding index in the stat partial - s3 grep/rg bind index into call_stat so per-path stat hits the index - integ/s3.py asserts API-call counts: ls -l and tree do 0 HeadObject, proving stats resolve from the index readdir populated * fix(ls): use index fast-path for filetype rendering in directory ls -l render_long_entry passed a prefix-less str to the filetype handler, so cloud handlers hit the wrong key and silently fell back to standard format while wasting HeadObject/GetObject calls. Build a prefixed PathSpec and thread index through; s3 ls filetype handlers now stat via the index. Adds parquet/orc/feather cases to the s3/gcs integ harness. * chore(commands): remove stray no-op expression statements Drop leftover bare statements: 'index' in email/rg.py and 'accessor.store' in ram/redis stat provision. No behavior change. |
||
|
|
870016c5b4 |
refactor(commands): generic dedup + cross-backend integ harness (#68)
* feat(utils): add stream/bytes conversion helpers
* feat(commands): add generic cat as async iterator
* refactor(commands): ram cat wrapper uses generic
* test(commands): expand generic cat coverage; fix $ on trailing partial line
Old _number_lines_stream had loose tests that missed three POSIX edge cases:
multi-digit line number alignment (%6d vs literal 5-space prefix), trailing
newline preservation, and $-marker placement on partial last lines. Added
unit coverage for all three, plus combined-flag scenarios and a
one-byte-at-a-time chunking test.
The trailing-$ test caught a real bug: my generic was emitting "hello$" for
`cat -E` on input without a final \n, but BSD/GNU cat only emit $ before \n.
Fixed by dropping the suffix from the trailing-partial-line branch.
* test(ram): add 1:1 test file for ram cat wrapper
Establishes src↔test correspondence (every src file has a matching test_*.py)
for the ram cat wrapper. Covers the wrapper's behavior end-to-end via
Workspace.execute, including regression tests for the two POSIX bug fixes
that landed via the generic refactor:
- cat -n multi-digit line number alignment (lines 1-12)
- no-trailing-newline preservation for both cat and cat -n
Plus multi-file concatenation, empty input, and the all-blank-lines edge case.
* refactor(commands): disk cat wrapper uses generic + 1:1 tests
The active disk cat is at disk/cat/cat.py (the disk/cat package shadows the
top-level disk/cat.py file, which is dead code). Wires through generic_cat,
preserves CachableAsyncIterator/IOResult shape, drops _number_lines_stream.
Tests cover the same POSIX edge cases as ram cat: multi-digit alignment,
no-trailing-newline preservation, empty input, all-blank-lines.
* refactor(commands): redis cat wrapper uses generic + 1:1 tests
Structurally identical to ram cat migration. Tests use the same REDIS_URL
skip pattern as tests/core/redis/conftest.py — they run when REDIS_URL is
set in the environment, skip otherwise.
* fix(commands): preserve cachable identity in cat wrappers for cache to populate
Bug: wrapping cachable in generic_cat unconditionally caused stdout and
io.reads[path] to reference different objects. mount.py:_wrap then wrapped
them separately, and wrap_cachable_streams only updated stdout when
identity matched (stdout is stream). The cache cachable was never iterated
by stdout materialization, so the background drain saw an exhausted source
and cached empty bytes.
Fix: only wrap with generic_cat when a flag (currently just -n) actually
requires line processing. For the plain 'cat path' case, return cachable
directly — matching the pre-refactor behavior and keeping stdout identical
to the value in io.reads[path].
Tests caught: test_cache_hit_serves_from_ram, test_grep_uses_cache,
test_fuse_read_uses_cache_when_populated (workspace cache layer).
* feat(commands): add generic head with -c fast path and -n line-buffered path
* refactor(commands): ram head wrapper uses generic + 1:1 tests
* refactor(commands): disk head wrapper uses generic + 1:1 tests
* refactor(commands): redis head wrapper uses generic + 1:1 tests
* test(commands): backfill generic cat/head coverage missing from old helpers
Audited tests against legacy head_helper/cat_helper test files. Added:
cat:
- empty input passthrough (no flags)
- binary passthrough across full 0..255 byte range
- show_ends only marks newlines, not other binary bytes
head:
- n=1 single-line output
- empty input (with and without -c)
- single line no newline with default n
- c=negative emits nothing
* refactor(commands): consolidate ram/disk/redis logic into generic modules
Phases K through Z extracted shared per-command logic into
mirage/commands/builtin/generic/<name>.py modules that accept VFS
callables (read_bytes, read_stream, write_bytes, stat, readdir, mkdir)
via dependency injection. Per-backend wrappers (ram, disk, redis) are
now thin shims that resolve globs, supply backend-specific callables,
and delegate to the generic.
Scope:
- 57 generic modules
- 170 backend wrappers reduced to shims
- 67 new generic-level tests under tests/commands/builtin/generic/
- Net -7,772 lines (178 files changed: +1,891 / -9,663)
Bug fixes converged during refactor (no backward compat needed):
- disk/md5 emitted only the digest; now matches coreutils `{digest} {path}`
- disk/sort and disk/nl only operated on paths[0]; now process all paths
- disk/awk lacked BEGIN/END/accumulator support; converged on ram/redis
- disk/unzip flattened archive directory structure; now preserves it
- disk/patch used offset-splice algorithm; converged on lockstep walk
(closer to real patch(1) semantics)
- rg failed to auto-recurse on bare directories; now matches ripgrep
- file command silently swallowed read errors; now logs to logger.debug
Cleanup:
- Deleted 3 dead top-level head.py files shadowed by head/ packages in
ram/disk/redis (Python package always wins over .py at same level)
- Removed 4 stray __init__.py files under tests/ per CLAUDE.md rule
- Replaced inline string literals with enums (FindType, LsSortBy)
- Removed bare `try/except: pass` for mkdir using idempotent
mkdir(parents=True) instead
* test(integ): add cross-backend equivalence harness with 240 cases
New /integ folder runs identical command suite across ram/disk/redis
and diffs each backend's output against committed integ/truth.txt
canonical fixture. CI workflow (.github/workflows/test_integ.yml)
boots a redis service and runs all three backends.
Also fixes bugs surfaced by running the suite:
- disk/cat: chain multiple files (was processing only paths[0])
- sha256sum: use original path string; -c mode resolves mount prefix
- md5: support stdin when no paths given
- jq: iterate per-record on .jsonl files (real jq semantics)
- xxd -r: strip offset prefix and ASCII suffix before unhexlify
- find -mindepth/-maxdepth: off-by-one across ram/disk/redis/s3
- find -o vs -or: handle both spellings
- awk: \$NF/\$NR resolve via field indirection; bare condition
programs (e.g. '\$2 > 28') treated as condition with default
print \$0; && and || compound conditions; BEGIN block executes;
END can read NR
- diff: default to GNU normal format (1,5c1,3 with < / > / ---);
-u flag required for unified output
- cmp: use 'char N' instead of 'byte N' (matches GNU/BSD)
Extracts:
- DiffOpTag StrEnum (equal/delete/insert/replace) in diff_types.py
- AwkCmpOp / AwkBoolOp / AwkBlock / AwkBuiltin StrEnums plus
FIELD_PREFIX / PRINT_STMT / CMP_OP_PATTERN constants in
generic/awk_types.py
* fix(integ): preserve trailing whitespace in truth.txt + redis find test
The trailing-whitespace pre-commit hook stripped tabs at line-end in
integ/truth.txt, which broke the diff against backends (paste pads
shorter columns with trailing tabs). Exclude integ/truth.txt from
end-of-file-fixer, mixed-line-ending, and trailing-whitespace hooks.
Also fix tests/core/redis/test_find.py {maxdepth, mindepth} that
asserted the previously-buggy depth math (matches the existing
ram/disk test updates).
|