diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..6196deea --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,60 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + packages: write + +jobs: + docker: + name: Build and publish image + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.repository_owner }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository_owner }}/soulfield + tags: | + type=raw,value=latest + type=ref,event=tag + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + + github_release: + name: Create GitHub Release + needs: docker + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Create release with autogenerated notes + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..80f3928d --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,36 @@ +Contributing to Soulfield + +Thanks for helping improve Soulfield! This guide keeps contributions smooth and predictable. + +Basics +- Use feature branches from `main`; open PRs early as draft when helpful. +- Keep PRs focused and small; include a clear title and summary. +- Prefer tests or a quick manual validation plan in the PR. + +Development +- Node: `npm ci && DEV_NO_API=1 npm start` then hit `GET /health`. +- MCP (read‑only FS): `npm run start:mcp` then `GET /mcp/tools`. +- Python (optional): run `pytest` for FastAPI sanity tests. + +Coding Standards +- JS/Node: follow existing patterns; keep changes minimal and explicit. +- Python: prefer pytest for tests; descriptive names; no one‑letter vars. +- Avoid unrelated refactors in the same PR; call them out if necessary. + +CI & Checks +- CI runs Node and Python jobs; both must be green. +- New workflows should be minimal and secure by default. + +Commit & PR Hygiene +- Conventional style (suggested): + - feat:, fix:, chore:, docs:, ci:, test:, refactor: +- PR template: fill Summary, Checklist, and Validation steps. + +Security +- Never commit secrets. `.env` is ignored; use `.env.example` for docs. +- MCP is read‑only and hardened, but treat it as a potential disclosure surface. +- See SECURITY.md for reporting vulnerabilities. + +Release +- Tag `vX.Y.Z` on main to trigger Docker image publish and a GitHub Release. + diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..ceaf2d60 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,22 @@ +Security Policy + +Supported Versions +- Active development on `main`. Releases are tagged `vX.Y.Z`. + +Reporting a Vulnerability +- Please report security issues privately via GitHub Security Advisories: + - Go to the repository’s “Security” tab → “Advisories” → “Report a vulnerability”. +- If that’s unavailable, email the maintainer or open a minimal issue requesting a private channel. + +Do not disclose publicly until a fix is available and coordinated. + +Handling & Disclosure +- We will acknowledge reports within 72 hours. +- We aim to provide a fix or mitigation and publish a patch release. +- After a fix is released, we’ll coordinate a responsible disclosure timeline. + +Secrets & Hardening +- `.env` and `.env.*` are ignored by git; do not commit secrets. +- The MCP server is read‑only and blocks `.env*`, `.git`, `.ssh`, and `receipts/` paths. +- Avoid logging sensitive values; use environment variables for API keys. + diff --git a/backend/mcp-server.cjs b/backend/mcp-server.cjs index c3e1c54f..48ddea1d 100644 --- a/backend/mcp-server.cjs +++ b/backend/mcp-server.cjs @@ -9,6 +9,19 @@ app.use(express.json()); const ROOT = path.resolve(__dirname); // ~/soulfield const SAFE = ROOT; // restrict to project root +// Basic denylist to avoid accidental secret exfiltration when MCP is exposed. +const FORBIDDEN_DIRS = new Set([".git", ".ssh", "receipts"]); +function isForbidden(relPath){ + const rel = relPath.replace(/\\/g, "/"); + if (!rel || rel === ".") return false; + // Block .env files anywhere + if (/(^|\/)\.env(\.|$)/.test(rel)) return true; + // Block specific directories anywhere in the path + const parts = rel.split("/"); + if (parts.some(p => FORBIDDEN_DIRS.has(p))) return true; + return false; +} + function safePath(p){ const abs = path.resolve(SAFE, p || "."); if (!abs.startsWith(SAFE)) throw new Error("path escapes SAFE root"); @@ -16,15 +29,24 @@ function safePath(p){ } function listDir(p){ const abs = safePath(p); + const baseRel = path.relative(SAFE, abs) || "."; const ents = fs.readdirSync(abs, { withFileTypes: true }); - return ents.map(e => ({ - name: e.name, - type: e.isDirectory() ? "dir" : e.isFile() ? "file" : "other", - size: e.isFile() ? (fs.statSync(path.join(abs, e.name)).size) : null, - })); + return ents + .filter(e => { + const rel = baseRel === "." ? e.name : path.posix.join(baseRel.replace(/\\/g,"/"), e.name); + // Hide forbidden entries from listing + return !isForbidden(rel); + }) + .map(e => ({ + name: e.name, + type: e.isDirectory() ? "dir" : e.isFile() ? "file" : "other", + size: e.isFile() ? (fs.statSync(path.join(abs, e.name)).size) : null, + })); } function readFile(p, maxBytes=131072){ // 128 KiB cap const abs = safePath(p); + const rel = path.relative(SAFE, abs).replace(/\\/g, "/"); + if (isForbidden(rel)) throw new Error("access denied"); const st = fs.statSync(abs); if (!st.isFile()) throw new Error("not a file"); const cap = Math.min(st.size, maxBytes|0 || 0);