Skip to content
Closed
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 }}
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
36 changes: 36 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -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.

22 changes: 22 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.

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