Skip to content

feat(data-dir): per-instance data directory (--data-dir / SPE_DATA_DIR) + secure-fs hardening - #39

Merged
Gregory Joseph (gnjoseph) merged 3 commits into
feat/spe-mcp-serverfrom
agents/pr3-data-dir
Jul 9, 2026
Merged

feat(data-dir): per-instance data directory (--data-dir / SPE_DATA_DIR) + secure-fs hardening#39
Gregory Joseph (gnjoseph) merged 3 commits into
feat/spe-mcp-serverfrom
agents/pr3-data-dir

Conversation

@gnjoseph

Copy link
Copy Markdown
Collaborator

Summary

Adds a per-instance data directory so multiple SPE MCP server instances (published VS Code build + local build; two tenants/projects; VS Code + the eval loop) stop clobbering the single, unpartitioned ~/.spe-mcp/state.json and token cache.

  • --data-dir <path> flag + SPE_DATA_DIR env on start / auth / logout. Default ~/.spe-mcp unchanged. Precedence flag > env > default.
  • New src/paths.ts — a lazy/memoized resolve-once seam (resolveDataDir, getDataDir, getStateFile, getCacheDir, getCacheFile, getLegacyCacheFile, setDataDirOverride). Removes the module-level STATE_DIR (state.ts) and CACHE_DIR (auth.ts) consts that were evaluated at import time.
  • state.json is not auto-partitioned — the directory is the isolation boundary (bootstrap has no tenant/client key; state.json is a resumable provisioning workspace; the r-appgate confirmedSessionId stamp is per-process).
  • Version bump 0.1.0-alpha.1 -> 0.2.0-alpha.1 (additive / back-compat; default byte-identical; no migration; zero new dependencies).

Security hardening (shipped in this PR — --data-dir activates latent secure-fs weaknesses)

Making the data dir caller-supplied moves it across a trust boundary (.vscode/mcp.json is workspace-scoped, potentially-untrusted). secure-fs was fail-open + symlink-following; now:

  1. Fail closed — refuse a dir that is a symlink, owned by another user, or group/other-accessible (POSIX 0o700); Windows off-profile override gets an owner-only DACL via icacls or is refused.
  2. Defeat symlink/TOCTOU — open cache read + secure write with O_NOFOLLOW + verify the fd via fstat; chmod the fd, never the path.
  3. Untrusted-input validationresolveDataDir requires absolute + normalized, rejects CWD-relative (never resolves against process.cwd()), expands a leading ~.
  4. Enforce, don't warn — an insecure/unverifiable target blocks refresh-token persistence (the MSAL cache writer swallows the refusal and forces a fresh interactive sign-in).

Compliance

  • Resolved path logged to stderr only, path-only (stdout is the JSON-RPC channel); cache contents never logged.
  • Secure-by-default: the flag/env cannot make an insecure path the easy default.
  • SDL threat-model refresh tracked as AB#3141790 — a warning + ETA at the current dev/testing stage (not a release blocker; becomes a hard gate at Preview/Production).

Tests / evidence

npm run ci (typecheck + build + vitest) green; npm run lint clean.

  • Test Files 50 passed (50)Tests 688 passed | 7 skipped (695) (the 7 skipped are POSIX-only symlink/perm cases skipped on the Windows dev box; they run on CI Linux).
  • New: src/paths.test.ts (precedence / ~ expansion / reject-relative / lazy re-resolution / cache partitioning), src/state.test.ts (isolation: writing dir B leaves dir A byte-identical; golden default; per-instance clearState), extended src/secure-fs.test.ts (O_NOFOLLOW symlink refusal on read+write, symlinked-dir refusal, 0o700 repair).
  • Smoke (built dist): SPE_DATA_DIR override -> state.json lands under the target and not under ~/.spe-mcp; reads back; relative path rejected (INVALID_DATA_DIR).

Work items

AB#3141787 (data-dir + seam) · AB#3141788 (secure-fs hardening) · AB#3141789 (docs — follow-up).
Follow-ups (not blocking): AB#3141790 (SDL threat-model), AB#3141791 (OS-keychain token storage), AB#3141792 (foreign-writer warning), AB#3141793 (eval-loop dogfood to SPE_DATA_DIR).

Reviewer notes

  • Risk This repo is missing a LICENSE file #1 (import-order): the seam is lazy/memoized and the CLI sets the override BEFORE dynamically importing index.js/auth.js — all four entry points (start/auth/logout/cleanup) resolve the same dir per process. cleanup is an MCP tool inside a start session, so it inherits the process env.
  • Not marked ready — awaiting SPE MCP Code Review + security-review, then Compliance sign-off.

Greg Joseph and others added 3 commits July 9, 2026 09:06
…R) + secure-fs hardening

Adds a configurable data directory so multiple SPE MCP server instances stop
clobbering the single ~/.spe-mcp/state.json + token cache.

- New src/paths.ts: lazy/memoized resolve-once seam (resolveDataDir, getDataDir,
  getStateFile, getCacheDir, getCacheFile, getLegacyCacheFile, setDataDirOverride).
  Precedence flag > env > default ~/.spe-mcp; default is byte-identical.
- Remove module-level STATE_DIR (state.ts) / CACHE_DIR (auth.ts) consts and route
  state + token cache through the seam. state.json is NOT auto-partitioned (the
  directory is the isolation boundary).
- CLI: --data-dir on start/auth/logout; resolve + set the override, propagate
  SPE_DATA_DIR, and log the resolved path to stderr (path-only) BEFORE importing
  state/auth so all entry points resolve the same dir.
- Harden secure-fs (fail-closed): refuse symlink / foreign-owned / group-or-other
  dirs; O_NOFOLLOW + fstat on read/write; chmod the fd, never the path; Windows
  off-profile owner-only DACL via icacls or refuse; insecure target blocks
  refresh-token persistence (forces fresh interactive sign-in).
- Untrusted-path validation in resolveDataDir: absolute + normalized, reject
  CWD-relative, expand leading ~.
- Tests: paths.test.ts, state.test.ts (isolation + golden default), secure-fs
  symlink/TOCTOU/perms. Suite: 688 passed / 7 skipped.
- Version bump 0.1.0-alpha.1 -> 0.2.0-alpha.1 (additive / back-compat).

AB#3141787 AB#3141788 AB#3141789

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- readState: route through readSecureFile (O_NOFOLLOW + owner check) so a
  symlinked/foreign-owned state.json fails closed, matching writeState. (sec S1)
- secure-fs: invoke icacls by absolute %SystemRoot%\System32 path, not bare
  name, to remove any binary-planting angle. (sec S2)
- writeSecureFile: drop O_TRUNC; ftruncate only AFTER the fstat owner/type
  checks pass (no truncate-before-verify), and use writeFileSync(fd,...) to
  handle short writes / EINTR. (sec S3 + review C3)
- ensureSecureDir: scope the strict group/other-accessible rejection to
  off-home overrides; keep best-effort for the default ~/.spe-mcp so a
  mode-ignoring FS (WSL DrvFs, some NFS/CIFS) doesn't hard-fail existing
  users. Ownership + symlink checks remain universal. (review C1)
- secureWindowsDirAclOrThrow: document the known off-profile explicit-ACE /
  owner-verify limitation (follow-up under AB#3116729). (review C2)
- README: Configuration row + Token Storage + "Running multiple instances"
  with .vscode/mcp.json snippets and the absolute-path rule. (review C4)
- tests: add sanitizeForFilename coverage.

Suite 689 passed / 7 skipped; npm run ci + lint green.

AB#3141787 AB#3141788 AB#3141789

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ardening

Compliance C1 (Keep-a-Changelog repo convention). Non-code; no ci impact.

AB#3141787

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@gnjoseph
Gregory Joseph (gnjoseph) marked this pull request as ready for review July 9, 2026 16:30
@gnjoseph
Gregory Joseph (gnjoseph) merged commit aec8ac4 into feat/spe-mcp-server Jul 9, 2026
5 checks passed
@gnjoseph
Gregory Joseph (gnjoseph) deleted the agents/pr3-data-dir branch July 9, 2026 23:11
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