From d17daf1484aba1176993611b6cc4a0d6c426de24 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2025 22:49:18 +0100 Subject: [PATCH 1/4] ci: add tag-based Docker release to GHCR --- .github/workflows/release.yml | 47 +++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..17221d1c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,47 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: read + 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 }} + From 58cdec91fbe8757dbac83d2b91898635e8056977 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2025 22:50:59 +0100 Subject: [PATCH 2/4] ci: extend release workflow to create GitHub Release with notes --- .github/workflows/release.yml | 15 ++++++++++++++- backend/mcp-server.cjs | 32 +++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 17221d1c..6196deea 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - 'v*' permissions: - contents: read + contents: write packages: write jobs: @@ -45,3 +45,16 @@ jobs: 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/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); From 3b4455b370b70623acf88c603f4ab9be8577cd1a Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2025 22:51:31 +0100 Subject: [PATCH 3/4] docs: add CONTRIBUTING and SECURITY policies --- CONTRIBUTING.md | 36 ++++++++++++++++++++++++++++++++++++ SECURITY.md | 22 ++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 CONTRIBUTING.md create mode 100644 SECURITY.md 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. + From 0304ef5497f31955c1c6df833fc0b3406ab58829 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2025 23:17:43 +0100 Subject: [PATCH 4/4] chore(ci): trigger checks for PR #5