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
20 changes: 20 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# SPDX-FileCopyrightText: 2026 Sudo Apt Holdings LLC
# SPDX-License-Identifier: Apache-2.0

version: 2
updates:
- package-ecosystem: mix
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: "deps"

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: "ci"
20 changes: 18 additions & 2 deletions .github/workflows/gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,28 @@ jobs:
- name: every commit in the range carries a Signed-off-by
run: |
set -uo pipefail
before="${{ github.event.before }}"
if [ "${{ github.event_name }}" = "pull_request" ]; then
range="origin/${{ github.base_ref }}..HEAD"
git rev-list --no-merges "$range" > /tmp/commits \
|| { echo "cannot resolve $range"; exit 1; }
elif [ -z "$before" ] \
|| [ "$before" = "0000000000000000000000000000000000000000" ] \
|| ! git rev-parse --verify --quiet "$before^{commit}" >/dev/null; then
# Two cases, one fallback. A branch's first push has no previous state, so
# github.event.before is the all-zeros SHA. A force-push names a real-looking SHA
# that is no longer reachable -- dropping a commit leaves `before` pointing at
# something the fetched history does not contain. Testing the SHA's SHAPE catches
# only the first; asking git whether it RESOLVES catches both.
echo "before is unusable ('$before'): checking every commit reachable from HEAD"
git rev-list --no-merges HEAD > /tmp/commits \
|| { echo "cannot list commits from HEAD"; exit 1; }
else
range="${{ github.event.before }}..HEAD"
range="$before..HEAD"
git rev-list --no-merges "$range" > /tmp/commits \
|| { echo "cannot resolve $range"; exit 1; }
fi
git rev-list --no-merges "$range" > /tmp/commits || { echo "cannot resolve $range"; exit 1; }
echo "commits in scope: $(wc -l < /tmp/commits)"
fail=0
while read -r sha; do
[ -z "$sha" ] && continue
Expand Down
44 changes: 44 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<!--
SPDX-FileCopyrightText: 2026 Sudo Apt Holdings LLC
SPDX-License-Identifier: Apache-2.0
-->

# Contributing

## Four rules, all enforced

**1. Sign off every commit.** `git commit -s` adds the `Signed-off-by` line, which certifies
the [Developer Certificate of Origin](https://developercertificate.org/): you wrote the patch,
or have the right to submit it under this project's licence. **CI checks every commit in the
range and fails the whole push if one is missing.** A missing sign-off cannot be waived — the
history has to be rewritten to add it, which is easier before review than after.

**2. No tool-attribution trailers.** No `Co-Authored-By` naming a tool, no session links, no
generator banners. A commit message says what changed and why it is believed correct. Use of a
tool is not a fact about the change.

**3. The gate must be green.** `./tools/gate.sh` runs format, compile with
`--warnings-as-errors`, tests, `credo --strict`, an SPDX header check, and a licence-file
check. **There is no baseline and none will be added:** this tree started clean, so a non-zero
count is a failure rather than a number to hold. Run it before you push; CI runs the same
script.

**4. Rebase, never merge.** Keep history linear. Rebase onto `main` and force-push your branch
rather than merging `main` into it.

## What a change is expected to carry

- **A red before a fix.** Show the failure first, in the commit message, with its output. A
test that has never been seen failing is not evidence that it works. Where a red is not
available — the code already exists and passes — demonstrate coverage by mutation instead:
break the property in a throwaway copy and show the test catches it.
- **Counts quoted from command output, never typed.** Test counts, exit codes, file counts.
- **SPDX headers** on every `.ex`, `.exs`, `.sh` and `.yml`. The gate checks it.
- **Scope discipline.** One commit does one thing and says so. If a fix uncovers a second
defect, file it rather than folding it in.

`CONVENTIONS.md` records why these exist, with the specific failures that produced them.

## Reporting a vulnerability

Not here. See `SECURITY.md`.
19 changes: 19 additions & 0 deletions CONVENTIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ The same rule covers the toolchain a workflow pins: `setup-beam` resolving a giv
pair on a given runner image is a claim about someone else's infrastructure, and it is
unverified until observed.

**Observed, and accepted rather than pinned.** The workflow asks for `otp-version: 28` and
`elixir-version: 1.18`. What CI installs is:

Installing Erlang/OTP OTP-28.5.0.6 - built on amd64/ubuntu-24.04
Using Elixir 1.18.5 (built for Erlang/OTP 27)
Erlang/OTP 28 [erts-16.4.0.6]
Elixir 1.18.5 (compiled with Erlang/OTP 27)

**An Elixir 1.18.5 build compiled for OTP 27, running on OTP 28.** The gate passes on it.

This is **recorded, not pinned**, and the reason is that pinning is the change which looks
more rigorous and is worse: Elixir 1.18 publishes no `otp-28` build, so pinning
`1.18.5-otp-28` would fail to resolve, and moving to an Elixir line that has one is a
toolchain decision rather than a CI tidy-up. Revisit when the Elixir pin next moves.

The point of writing it down is that the next person to read the workflow sees `otp-version: 28`
and `elixir-version: 1.18` and would reasonably assume a matched pair. It is not one, it is
known not to be one, and nothing here depends on it being one.

## Every "verified" names its command and its exit code

Counts are quoted from command output, never typed fresh. A red is demonstrated before a fix,
Expand Down
72 changes: 72 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!--
SPDX-FileCopyrightText: 2026 Sudo Apt Holdings LLC
SPDX-License-Identifier: Apache-2.0
-->

# Security Policy

## Reporting a vulnerability

Report privately through **[GitHub Security Advisories](https://github.com/ScriptKittyOS/beam_mcp/security/advisories/new)**.
Please do not open a public issue for a suspected vulnerability.

Useful in a report: the protocol revision and transport, a request that triggers it, what you
expected and what happened. A failing test is welcome and never required.

## What this project can commit to

This package is maintained by **one person**, and the commitment is written to be one that can
be kept rather than one that sounds reassuring:

- **Acknowledgement within 7 days.** If you have heard nothing after 7 days, assume the report
did not arrive and open a public issue saying only that you are waiting on a security
response — no detail.
- **An assessment within 30 days** of acknowledgement: whether it is in scope, and if so a
rough severity and intended fix window. If it will take longer, you will be told that
instead of being left waiting.
- **Credit in the advisory and the changelog**, unless you ask otherwise.

There is no paid bounty, and no guaranteed fix deadline. A single maintainer cannot honestly
promise a 24-hour turnaround, so this policy does not.

## In scope

The package's own code, `lib/`:

- **Protocol handling** — malformed, hostile or ambiguous JSON-RPC that crashes the server,
bypasses validation, or is answered under the wrong protocol revision.
- **Framing and bounds** — input that escapes the line or body limits, or desynchronises the
reader so one message is interpreted as another.
- **Schema validation** — arguments that reach dispatch despite violating the schema the
catalog advertised, including key- or type-confusion between the validated form and the
dispatched form.
- **Era confusion** — a request served under a protocol revision other than the one it
declared.
- **Information disclosure across the wire boundary** — server internals reaching a client
that should not see them.

## Out of scope

- **What a host's tools do.** This package validates and routes; it does not execute. A tool
that deletes files when asked is the host's design, not a defect here.
- **Anything the host injects** — the catalog, the dispatch function, and whatever they reach.
- **Transport security.** stdio is a local pipe; confidentiality and authentication of that
channel belong to whatever spawns the process.
- **Denial of service through legitimate volume.** Bounds exist to stop unbounded buffering,
not to ration throughput.
- **Dependencies**, unless the defect is in how this package uses one. Report those upstream.

## Known, already public

Recorded so they are not reported as discoveries, and so their status is not mistaken for
ignorance of them:

- **Error payloads carry `inspect/1` output**, so Elixir term syntax reaches the wire. It is a
boundary defect, it is tracked, and it is fixed before the first release.
- **The package is pre-1.0 and unpublished.** No version has shipped, so there is no supported
release to backport a fix to yet.

## Supported versions

None yet — nothing is published. When a release exists, this section will name which versions
receive fixes.
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ defmodule BeamMCP.MixProject do
use Mix.Project

@version "0.1.0"
@source_url "https://github.com/HackTuah/beam_mcp"
@source_url "https://github.com/ScriptKittyOS/beam_mcp"

def project do
[
Expand Down
Loading