From d17daf1484aba1176993611b6cc4a0d6c426de24 Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2025 22:49:18 +0100 Subject: [PATCH 1/3] 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/3] 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 797aa032203b017406bec04f974bc40abf9b88bd Mon Sep 17 00:00:00 2001 From: Michael Date: Wed, 3 Sep 2025 23:17:40 +0100 Subject: [PATCH 3/3] chore(ci): trigger checks for PR #4