Skip to content

Tools dev data#101

Open
hughdbrown wants to merge 12 commits intowesm:mainfrom
hughdbrown:tools-dev-data
Open

Tools dev data#101
hughdbrown wants to merge 12 commits intowesm:mainfrom
hughdbrown:tools-dev-data

Conversation

@hughdbrown
Copy link
Contributor

This is to address issue #100.

- `func Execute() error` returns `rootCmd.Execute()` — errors propagate to `main()` rather than calling `os.Exit` directly, ensuring deferred cleanup runs.
- In `PersistentPreRunE`: if `MSGVAULT_HOME` is set and `--home` was not explicitly provided, print a warning to stderr about the env var being set.

**Dependencies:**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ MSGVAULT_HOME check bypasses symlink isolation (medium severity)

The PersistentPreRunE warning about MSGVAULT_HOME is insufficient. If MSGVAULT_HOME points to a sensitive directory and the user runs devdata commands, symlink operations on ~/.msgvault could create confusion or accidental data modification. The tool should error (not just warn) if MSGVAULT_HOME is set unless --home is explicitly provided, forcing the user to acknowledge the override.


Automated security review by Claude 4.5 Sonnet - Human review still required

@hughdbrown
Copy link
Contributor Author

A session using a data excerpt for development:

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ ./devdata
devdata manages multiple msgvault data directories via symlinks, allowing developers to switch between datasets and create expendable subsets for development.

Usage:
  devdata [command]

Available Commands:
  completion    Generate the autocompletion script for the specified shell
  exit-dev-data Exit dev mode and restore ~/.msgvault from ~/.msgvault-gold
  help          Help about any command
  init-dev-data Initialize dev mode by moving ~/.msgvault to ~/.msgvault-gold
  list          List available datasets
  mount-data    Point ~/.msgvault at a named dataset
  new-data      Create a new dataset by copying N messages from a source

Flags:
  -h, --help          help for devdata
      --home string   override home directory (default: user home)

Use "devdata [command] --help" for more information about a command.

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ ./devdata init-dev-data
devdata: initialized dev mode: /Users/hughbrown/.msgvault -> /Users/hughbrown/.msgvault-gold

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ ./devdata new-data --dst tiny --rows 500
devdata: copying 500 messages from /Users/hughbrown/.msgvault-gold to /Users/hughbrown/.msgvault-tiny...
devdata: created dataset "tiny" in 367ms
Messages:      500
Conversations: 466
Participants:  163
Labels:        43gg
Database size: 57.3 MB

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ ./devdata mount-data --dataset tiny
devdata: mounted dataset "tiny": /Users/hughbrown/.msgvault -> /Users/hughbrown/.msgvault-tiny

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ ls ~/.msgvault/*
/Users/hughbrown/.msgvault/config.toml /Users/hughbrown/.msgvault/msgvault.db

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ msgvault tui
Building analytics cache (no cache exists)...
Full rebuild: clearing existing cache...
Building cache...
  messages...               done (9ms)
  message_recipients...     done (2ms)
  message_labels...         done (1ms)
  attachments...            done (1ms)
  participants...           done (1ms)
  labels...                 done (1ms)
  sources...                done (1ms)
  Total:                    17ms
Cached 500 messages for fast queries.

~/workspace/hughdbrown/msgvault tools-dev-data !1 ?4 ❯ ./devdata exit-dev-data
devdata: exited dev mode: /Users/hughbrown/.msgvault restored

absHome, err := filepath.Abs(home)
if err != nil {
return fmt.Errorf("resolve home directory: %w", err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Path traversal check uses filepath.Separator incorrectly (medium severity)

The path traversal check strings.HasPrefix(srcDir, absHome+string(filepath.Separator)) will fail for the home directory itself (when srcDir == absHome). Line 72 handles this with || srcDir != absHome, but the logic is inverted: it should be && srcDir != absHome. This could allow paths equal to home directory to bypass validation. The check should be: !strings.HasPrefix(srcDir, absHome+string(filepath.Separator)) && srcDir != absHome.


Automated security review by Claude 4.5 Sonnet - Human review still required

}

// Remove symlink
if err := os.Remove(path); err != nil {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 Symlink removal without re-verification before os.Remove (high severity)

The code re-verifies the symlink status at line 46 but then proceeds to os.Remove at line 56 without a second verification immediately before removal. A race condition between the check and removal could allow deletion of a real directory. The PRD explicitly requires re-verification immediately before os.Remove (see SECURITY.md section 'Symlink removal safety'). Move the Lstat check to line 55, immediately before os.Remove(path).


Automated security review by Claude 4.5 Sonnet - Human review still required


// Sanitize source path for ATTACH: reject null bytes, escape single quotes
if strings.ContainsRune(srcDBPath, 0) {
os.RemoveAll(dstDir)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Null byte check alone insufficient for SQL injection (medium severity)

The code checks for null bytes at line 64 and escapes single quotes at line 66, but does not validate that srcDBPath is an absolute canonical path within the expected directory. An attacker-controlled path could contain '..' sequences or symlinks that resolve outside the home directory, bypassing path traversal protection. The PRD requires canonicalization with filepath.Clean() and filepath.Abs() before this point, but newdata.go performs this. Add a defense-in-depth check here to verify srcDBPath is absolute and within expected bounds.


Automated security review by Claude 4.5 Sonnet - Human review still required

absHome, err := filepath.Abs(home)
if err != nil {
return fmt.Errorf("resolve home directory: %w", err)
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Path traversal check uses string prefix instead of proper path validation (medium severity)

The path traversal check on lines 80-85 uses strings.HasPrefix with separator appending, but this can be bypassed with symlinks or redundant separators (e.g., ///home vs /home). Use filepath.Rel(absHome, srcDir) and check that the result does not start with ".." to properly validate paths are within bounds. Apply the same fix to dstDir validation on line 83.


Automated security review by Claude 4.5 Sonnet - Human review still required

// Resolve destination path
dstDir := datasetPath(newDataDstFlag)

// Canonicalize and validate paths
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚨 Path traversal via filepath.Abs race condition (high severity)

Lines 66-73 call filepath.Abs on user-controlled --src/--dst flags, but these can still resolve symlinks outside the home directory. An attacker creating ~/.msgvault-evil as a symlink to /etc would pass validation but srcDir would resolve to /etc. Use filepath.EvalSymlinks explicitly and verify the final resolved path prefix-matches the canonical home directory before any filesystem operations.


Automated security review by Claude 4.5 Sonnet - Human review still required

defer db.Close()

// Canonicalize source path for ATTACH (defense in depth — caller should
// also validate, but CopySubset is public and must not trust its inputs).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Canonicalization does not prevent symlink attacks (medium severity)

Lines 63-75 use filepath.Abs and filepath.Clean to canonicalize srcDBPath, but do not resolve symlinks. An attacker could place ~/.msgvault-evil/msgvault.db as a symlink to a victim's database outside the home directory. Use filepath.EvalSymlinks to resolve the final target and verify it is within the expected home directory before ATTACH.


Automated security review by Claude 4.5 Sonnet - Human review still required

@hughdbrown hughdbrown force-pushed the tools-dev-data branch 2 times, most recently from 764979c to 8cd7b38 Compare February 8, 2026 15:00
@hughdbrown
Copy link
Contributor Author

Claude on why the test is failing on Windows:


Based on my analysis of the code, TestReplaceSymlink is failing on Windows due to symlink permission requirements. Here's why:

The Problem

On Windows, creating symbolic links requires either:

  1. Administrator privileges (elevated process), OR
  2. Developer Mode enabled (Windows 10 version 1703+), OR
  3. SeCreateSymbolicLinkPrivilege granted to the user

GitHub Actions' windows-latest runners do not run with elevated privileges by default, so the test fails at line 158 when trying to create the first symlink:

  if err := os.Symlink(targetA, linkPath); err != nil {
      t.Fatal(err)  // ← Fails here on Windows with permission error
  }

The Solutions

You have several options:

Option 1: Skip the test on Windows (Recommended)

Add a build constraint or skip condition:

  func TestReplaceSymlink(t *testing.T) {
      if runtime.GOOS == "windows" {
          t.Skip("symlink creation requires elevated privileges on Windows")
      }
      // ... rest of test
  }

Option 2: Make symlink tests Windows-specific with proper error handling

Accept that symlinks may not be available and handle the permission error gracefully:

  func TestReplaceSymlink(t *testing.T) {
      dir := t.TempDir()
      targetA := filepath.Join(dir, "target-a")
      targetB := filepath.Join(dir, "target-b")
      // ... create directories ...

      linkPath := filepath.Join(dir, "link")
      if err := os.Symlink(targetA, linkPath); err != nil {
          if runtime.GOOS == "windows" {
              t.Skipf("symlink creation failed (requires admin/developer mode on Windows): %v", err)
          }
          t.Fatal(err)
      }
      // ... rest of test
  }

Option 3: Request Developer Mode in CI (Not reliable)

You could try enabling Developer Mode in the GitHub Actions Windows runner, but this is fragile and not guaranteed to work across all Windows CI environments.

Recommended Action

Skip the symlink tests on Windows since:

  1. The devdata tool is primarily a developer convenience feature
  2. Symlink functionality is unlikely to be used on Windows development environments
  3. The core functionality can still be tested on macOS/Linux where symlinks work without privileges

I don't like turning off tests for a platform, but it seems the best choice. The alternative seems to be to fight Github Actions configuration.

@github-actions
Copy link

github-actions bot commented Feb 8, 2026

Security Review: 3 High/Medium Issues Found

Claude's automated security review identified potential security concerns. Please review each finding below.

Note: 1 low severity issue(s) were omitted to reduce noise.


⚠️ SQL injection via dataset path in ATTACH DATABASE (medium)

Location: tools/devdata/dataset/copy.go:94

The ATTACH DATABASE statement uses string interpolation with only single-quote escaping. While single quotes are escaped, the control character check (lines 87-91) does not reject semicolons, allowing multiple SQL statements if the path contains ';'. An attacker controlling the dataset name could inject statements like '--src=foo; DROP TABLE messages; --'. Validate that escapedSrcPath contains no semicolons before interpolation, or use a prepared statement wrapper.


🚨 Symlink TOCTOU in CopyFileIfExists allows arbitrary read (high)

Location: tools/devdata/dataset/copy.go:365

CopyFileIfExists resolves symlinks via EvalSymlinks (line 365) then opens the resolved path (line 376), creating a TOCTOU window. An attacker with filesystem access could replace the symlink between resolution and open, causing the tool to read an arbitrary file outside containDir and copy it to the destination dataset. Use os.OpenFile with O_NOFOLLOW (if available) or open the file descriptor before resolving, then fstat to verify it matches expectations.


⚠️ CopyFileIfExists containment validated at wrong granularity (medium)

Location: tools/devdata/cmd/newdata.go:138

CopyFileIfExists is called with srcDir (the dataset root) as containDir, but srcConfig (line 135) is constructed by joining srcDir + 'config.toml'. If srcDir itself is a symlink to a directory outside the home, the containment check passes but the file read escapes. Resolve srcDir symlinks and verify containment before constructing srcConfig, or pass the canonical home directory as containDir instead of srcDir.


Powered by Claude 4.5 Sonnet — this is an automated review, false positives are possible.

@roborev-ci
Copy link

roborev-ci bot commented Feb 9, 2026

roborev: Combined Review

Summary verdict: 3 low-severity findings; no critical/high/medium issues.

Low

  • Path traversal via symlinked dataset directories
    Location: tools/devdata/cmd/newdata.go
    The “within home” validation uses filepath.Rel on an un-resolved srcDir, so a symlinked dataset can escape home. Resolve symlinks (filepath.EvalSymlinks) for srcDir (and home) before validation, and consider dstDir post-create.
  • Copied config.toml permissions may become overly permissive
    Location: tools/devdata/dataset/copy.go
    os.Create uses 0666 masked by umask, potentially widening permissions vs source. Preserve source mode (os.Stat + Chmod or OpenFile) or default to 0600 then optionally match source.
  • Missing Windows symlink permission hint
    Locations: tools/devdata/cmd/initdevdata.go, tools/devdata/dataset/dataset.go
    Spec says to print a user-friendly hint on Windows symlink failures; current code returns raw error. Wrap os.Symlink errors and, on Windows, suggest enabling Developer Mode or running as admin.

Synthesized from 4 reviews (agents: codex, gemini | types: security, review)

hughdbrown and others added 2 commits February 8, 2026 20:43
PRD changes:
- Acknowledge tools/ as new convention vs scripts/ for shell tools
- Document exit-dev-data restores gold regardless of current mount
- Fix FTS5 rebuild description to match actual backfillFTSBatch query
- Note attachment storage_path file-not-found behavior in dev datasets
- Note analytics/ Parquet cache auto-build on TUI launch
- Add MSGVAULT_HOME env var interaction section with warning behavior
- Add schema version compatibility section acknowledging version coupling

Task list changes:
- Replace os.Exit(1) with error-returning Execute() pattern
- Add MSGVAULT_HOME warning in PersistentPreRunE
- Clarify two-phase open sequence for schema init + FK-off bulk copy
- Add SELECT * fragility acknowledgment with rationale
- Fix FTS5 populate query to match store.backfillFTSBatch exactly
- Note conversation participant_count reflects subset, not original
- Add IsDefault field to DatasetInfo for non-symlink directory listing
- Handle real ~/.msgvault directory in list command output
- Add automated test tasks for dataset package (Stage 1) and copy
  logic (Stage 2) using t.TempDir() and in-memory SQLite

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
hughdbrown and others added 5 commits February 8, 2026 20:43
1. Add dataset name allowlist to prevent SQL injection in ATTACH

Restrict dataset names to [a-zA-Z0-9_-] via ValidateDatasetName(),
called at the CLI boundary for --dataset, --src, and --dst flags.
This eliminates SQL injection vectors in the ATTACH DATABASE statement
while keeping the existing quote-escaping as defense in depth.

2. Move Lstat guard immediately before os.Remove in exit-dev-data

Collapse the Lstat re-verification and os.Remove into a single tight
block with no intervening code, minimizing the race window between
symlink verification and removal.

3. Canonicalize srcDBPath in CopySubset before ATTACH

Add filepath.Abs + filepath.Clean as defense in depth inside
CopySubset, since it is a public function that must not trust
its inputs. The caller already canonicalizes, but this ensures
the path used in the ATTACH DATABASE statement is always an
absolute canonical path regardless of call site.

4. Eliminate TOCTOU race in ReplaceSymlink with atomic rename

Replace the Lstat-then-Remove-then-Symlink pattern with a
temp-symlink + os.Rename pattern. On POSIX, rename atomically
replaces an existing symlink and fails with an error if the
target has become a real directory, eliminating the race window
entirely.

5. Reject control characters in source path before ATTACH

Replace the null-byte-only check with a full control character
scan (0x00-0x1F and 0x7F DEL) on the canonicalized source path.
This hardens the ATTACH DATABASE statement against characters
that could interfere with SQL parsing beyond just null bytes.

6. Use filepath.Rel for path traversal check instead of string prefix

Replace strings.HasPrefix-based path containment check with
filepath.Rel, which correctly handles symlinks and redundant
separators. A relative path starting with ".." means the target
escapes the home directory.

7. Harden devdata tool: error handling, path safety, race prevention

- Return errors from homeDir()/msgvaultPath()/datasetPath() instead of
  calling os.Exit(1), so Cobra error handling and deferred cleanup run
- Add symlink-resolution + containment check in CopyFileIfExists to
  prevent symlink escape from dataset directories
- Use random suffix for temp symlink in ReplaceSymlink to avoid
  collisions between concurrent calls
- Fix misleading comment about PRAGMA foreign_keys (FK check is
  standalone, not dependent on the pragma)
- Clarify temp table cleanup comment re: rollback behavior
- Use realistic string format for source_message_id in test data
- Replace hardcoded /tmp/ paths in tests with t.TempDir()
- Strengthen TestCopySubset_DestinationExists with actual assertions

8. Check all error return values in copy.go

- Check st.Close() on success path (can indicate unflushed writes)
- Check rows.Close() and rows.Err() after FK check iteration
- Check rows.Scan() errors (report as violations instead of silently skipping)
- Check res.RowsAffected() for conversations, participants, labels
- Check tx.Exec for temp table DROP
- Check db.Exec("DETACH DATABASE src") on success path
- Mark all best-effort cleanup calls with _ = (os.RemoveAll,
  tx.Rollback, db.Exec on error paths) to document intentional ignore

9. Harden devdata: file sync, safe cleanup, input validation, test errors

- CopyFileIfExists: explicitly Sync and Close destination file instead of
  relying on deferred Close, ensuring write errors are not silently lost
- CopySubset: track whether destination directory was newly created and
  only RemoveAll on error if we created it, preventing deletion of
  pre-existing directories passed by other callers
- new-data command: validate --rows is positive (negative values would
  bypass SQLite LIMIT, zero would create empty databases)
- Tests: check all QueryRow().Scan() errors in TestCopySubset_Basic

10. Address security review findings for devdata tool

- Widen CopyFileIfExists containment check to verify against dataset root
  directory (containDir param) instead of just the immediate parent, preventing
  symlink escapes to sibling directories within the dataset
- Reorder rows.Err() before rows.Close() for idiomatic Go error checking
- Add _ = to intentionally-ignored os.Remove in ReplaceSymlink for consistency
- Document deferred db.Close() vs cleanupDir() ordering interaction
- Remove resolved SECURITY-REVIEW-SKIP comments
- Add TestCopyFileIfExists_SymlinkEscape test for symlink containment

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 9, 2026

roborev: Combined Review

Verdict: 5 issues found (2 Medium, 3 Low); no Critical/High.

Critical

  • None.

High

  • None.

Medium

  1. tools/devdata/dataset/dataset.go (function ReplaceSymlink)
    Windows dataset switching can fail because os.Rename doesn’t overwrite existing paths on Windows. This can break mount-data even when symlinks are allowed.
    Suggested fix: On Windows, attempt os.Rename and if it fails with “exists,” fall back to os.Remove of the existing symlink then os.Symlink, or use a guarded remove+create path for Windows.

  2. tools/devdata/dataset/dataset_test.go (functions TestReadTarget, TestListDatasets)
    Symlink tests still run on Windows and can fail when symlink creation requires elevated privileges. Only TestReplaceSymlink is skipped.
    Suggested fix: Skip these tests on Windows too, or detect os.Symlink permission errors and t.Skip.

Low

  1. tools/devdata/dataset/dataset.go:68-104
    ReplaceSymlink has a TOCTOU window: it checks linkPath is a symlink, then renames over it. If linkPath is replaced with a regular file between check and rename, the file can be clobbered.
    Suggested fix: Use an unlink that refuses to follow non‑symlinks (e.g., unlinkat with AT_SYMLINK_NOFOLLOW) or re‑Lstat immediately before os.Remove + os.Symlink.

  2. tools/devdata/cmd/list.go:55-72 and tools/devdata/dataset/dataset.go:118-160
    list prints dataset names/paths verbatim, allowing ANSI/control‑sequence spoofing via malicious directory names.
    Suggested fix: Sanitize or escape control characters before printing, or validate against ValidateDatasetName and replace invalid names.

  3. tools/devdata/dataset/copy.go:309-379
    CopyFileIfExists uses os.Create(dst) and can follow a malicious symlink, overwriting arbitrary files.
    Suggested fix: Lstat and reject symlinks, and/or use os.OpenFile with O_EXCL (and O_NOFOLLOW on Unix) to prevent following or overwriting.


Synthesized from 4 reviews (agents: codex, gemini | types: security, review)

hughdbrown and others added 5 commits February 8, 2026 20:59
Windows doesn't allow os.Rename to overwrite existing files, which broke
dataset switching and token file updates. Fixed by:

1. ReplaceSymlink: On Windows, remove old symlink before creating new one
   (POSIX continues to use atomic temp+rename)

2. saveToken: On Windows, remove existing token file before rename
   (POSIX continues to use atomic temp+rename)

Both fixes maintain safety checks (verify symlink before removal) and
preserve atomicity guarantees where possible.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Addresses code review findings for Windows file system compatibility:

1. **Symlink replacement race condition fix** (dataset.go:92-116)
   - Changed Windows symlink replacement to try temp+rename first for atomicity
   - Falls back to remove+create only if rename fails
   - Eliminates race window where another process could interfere between remove and create
   - Maintains safety check that verifies path is a symlink before any modifications

2. **Token file removal error handling** (oauth.go:324-332)
   - Changed Windows file removal to only ignore os.ErrNotExist errors
   - Now properly reports permission errors, I/O errors, and directory conflicts
   - Prevents confusing failures on subsequent rename operation
   - Cleans up temp file on error

3. **Windows-specific test coverage** (new files)
   - Added dataset_windows_test.go with build tag for Windows-only tests
   - Added oauth_windows_test.go with build tag for Windows-only tests
   - Tests verify correct behavior for file overwrites, error cases, and initial saves
   - Ensures Windows-specific code paths are validated on Windows platform

All tests pass. Changes maintain backward compatibility with POSIX systems
while improving robustness and error reporting on Windows.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Replace custom contains()/findSubstring() helpers with strings.Contains()
  in both Windows test files
- Remove unused encoding/json and path/filepath imports from oauth test
- Make temp symlink cleanup in ReplaceSymlink report real errors instead
  of silently ignoring all failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add destination path containment check to CopyFileIfExists (defense in depth)
- Reject CopySubset when destination database already exists (prevent corruption)
- Document CopySubset's caller responsibility for path-traversal validation
- Move formatSize to root.go to eliminate hidden cross-file dependency
- Save original symlink target in exit-dev-data recovery path
- Warn on unexpected FTS population errors instead of silently swallowing
- Validate dataset name inside datasetPath to prevent future callers from skipping

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Clean up partially-created DB file when directory pre-existed (copy.go)
- Report symlink recovery success/failure in exit-dev-data error messages
- Remove misleading PRAGMA foreign_keys = ON from FK check test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@roborev-ci
Copy link

roborev-ci bot commented Feb 9, 2026

roborev: Combined Review

Summary verdict: 3 findings (2 Medium, 1 Low); otherwise clean.

Medium

  • internal/oauth/oauth.go (around saveToken, Windows branch): os.Remove(path) can delete an empty directory because it doesn’t verify the target is a file/symlink. Add os.Lstat(path) and error on ModeDir before removing.
  • tools/devdata/dataset/dataset.go:110-118 (Windows fallback in ReplaceSymlink): TOCTOU risk; after os.Rename fails, os.Remove(linkPath) doesn’t re-validate the path is still a symlink. Re-os.Lstat immediately before removal and abort if not a symlink.

Low

  • tools/devdata/dataset/copy.go:43-52: CopySubset doesn’t re-check that dstDir isn’t a symlink after MkdirAll; a local race could redirect writes. Lstat before and after MkdirAll, or create in a temp dir then os.Rename.

Synthesized from 4 reviews (agents: codex, gemini | types: security, review)

@wesm
Copy link
Owner

wesm commented Feb 9, 2026

I have squelched the incompetent Sonnet review bot so it will just be the roborev reviews going forward. I will work on this PR today!

@wesm
Copy link
Owner

wesm commented Feb 13, 2026

I haven't forgotten about this! I've been in triage mode and with a lot of meetings and other work obligations this week. I will work on this soon

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.

2 participants