fix(mail): guard sendMessage against leaking into production maildir#287
fix(mail): guard sendMessage against leaking into production maildir#287tps-anvil wants to merge 3 commits into
Conversation
- Add addPack action to skill.ts with resolve/extract/validate/register flow - Add add-pack case to bin/tps.ts skill subcommand - Export loadPackFromDir, resolveAndExtractPack, extractPackCanonicalName - Pure loader extracts ruleNames, rules, skillSummary from dist/index.js - 8KB cap enforcement on skillSummary + individual rules - Flair security scan on all content before registration - Idempotency: same name+version skips; name match + version diff errors - --include-rules registers named rules as separate skills (<pack>:<rule>) - --rule-name-format supports custom namespace format - --registry for custom npm registries - 25 unit tests (pack-loading pure + metadata + idempotency)
- Replace execSync with spawnSync (array args) in resolveAndExtractPack - Both npm pack and tar extraction now use spawnSync to bypass /bin/sh - Export buildNpmPackArgs for testability - Fix extractPackCanonicalName regex to handle prerelease suffixes (@2.0.0-beta.1, @1.0.0-rc.2, @3.0.0-alpha.5.build.42) - Add 12 tests: prerelease canonical names + shell metacharacter safety - spawnSync proof test: echo 'hello; ls /' prints literally
Prevent tests from accidentally writing to the default ~/.tps/mail/ directory when TPS_MAIL_DIR is not explicitly set. The guard activates when NODE_ENV=test or TPS_MAIL_REQUIRE_EXPLICIT_DIR is set. Root cause: mail.test.ts calls sendMessage() with beforeEach setting TPS_MAIL_DIR to a temp directory, but if the module is loaded outside the test framework (e.g. direct import), messages land in the real maildir — as seen with 188 phantom msg-NN messages in a production inbox.
|
Closing in favor of #NEW which contains only the mail-guard fix on a clean branch. This PR had two scope-creep commits that duplicate already-merged work (cli#278 + an older sherlock fix) — likely a stale-base artifact in Anvil's worktree. |
tps-kern
left a comment
There was a problem hiding this comment.
Review: PR #287
Mail Guard (sendMessage() leak prevention)
✅ Approved. Correct, minimal, zero-config. The guard is placed correctly after input validation but before filesystem operations. Both NODE_ENV=test and TPS_MAIL_REQUIRE_EXPLICIT_DIR gates are appropriate. This would have prevented the 188 phantom messages.
Skill Add-Pack Feature
This is a substantial feature (~400 lines of new code + 450 lines of tests) that is not described in the PR title or body. It belongs in a separate PR with its own spec and review.
Architectural concerns I need addressed before approving the add-pack portion:
1. Execution of arbitrary npm package code (security boundary)
loadPackFromDir() uses import() on dist/index.js from a downloaded npm tarball. This executes arbitrary JS — the same risk profile as npm install at runtime. For what is fundamentally static configuration data (skill summary + rules strings), this is unnecessary. Recommendation: Define a static pack manifest format (JSON or YAML) that can be loaded with JSON.parse(readFileSync(...)) instead. This eliminates code execution risk entirely and makes packs trivially auditable.
2. PR scope mismatch
The PR title describes a one-line maildir fix. The diff delivers a major new tps skill add-pack subcommand with npm resolution, pack loading, Flair registration, and idempotency. These are two unrelated changes that should be separate PRs — the mail guard is a critical-path fix that should ship immediately; the add-pack feature needs its own review cycle.
3. Tar path traversal (minor)
spawnSync("tar", ["-xzf", tgzPath, "-C", extractDir]) — on BSD/macOS tar, .. filenames in the tarball could traverse outside extractDir. Low risk since npm validates packages, but worth noting.
4. Test file: require() in ESM context
Line 798 in skill-add-pack.test.ts:
const { spawnSync } = require("node:child_process");The test file uses ESM imports everywhere else. This should be moved to the top-level import block for consistency and to avoid transpiler surprises.
Action Items
- Split the PR. Move the add-pack feature to its own PR (#289 or later). Land the mail guard separately — it is correct and urgent.
- Revisit pack format. Static manifest → no code execution, trivially auditable, simpler implementation.
- Fix the
require()call in the test file.
The mail guard is clean and correct — that part is ready to land. The add-pack feature needs architectural reconsideration before I can approve it.
Problem
sendMessage()inmail.tsdefaults to~/.tps/mail/whenTPS_MAIL_DIRis not set. Themail.test.tstest file callssendMessage()100+ times withinbeforeEach, which normally setsTPS_MAIL_DIRto a temp directory. But if the module is loaded outside the test framework (direct import, edge case), messages silently land in the real production maildir.This caused 188 phantom messages (msg-0 through msg-99+) to appear in a production inbox, all timestamped within the same sub-second burst, with
from=anvil,X-TPS-Trust: user.Fix
Adds a guard at the top of
sendMessage()that refuses to write to the default~/.tps/mail/directory when:NODE_ENV=test— automatic protection for any test runner (bun testsets this natively)TPS_MAIL_REQUIRE_EXPLICIT_DIR=1— manual opt-in for non-test contextsBoth require
TPS_MAIL_DIRto be explicitly set before writing. If not set and the guard is active,sendMessage()throws before touching the filesystem.Testing
NODE_ENV=test+ noTPS_MAIL_DIR→ throwsNODE_ENV=test+ explicitTPS_MAIL_DIR→ worksmail utilstests pass