Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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 }}
Comment on lines +23 to +27
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] Authenticate to GHCR with workflow actor, not repository owner

The login step uses username: ${{ github.repository_owner }} while authenticating with secrets.GITHUB_TOKEN. GitHub issues the workflow token for the triggering actor, not for the repository owner, so when the repo lives under an organization the credentials don’t match and docker/login-action returns 401 and the job never reaches the build stage. Using github.actor (the token’s principal) avoids failed releases whenever a maintainer or automation pushes a tag.

Useful? React with 👍 / 👎.

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
32 changes: 27 additions & 5 deletions backend/mcp-server.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,44 @@ 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");
return abs;
}
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);
Expand Down