diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3932966..0c89414 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -150,8 +150,6 @@ setups. ## License -This repo does not yet have a finalized license (see the root -[README](./README.md#license)). Until one is added, treat any contribution -discussion as provisional - the license that ultimately covers this project will -also cover contributions made before it's added, and will be called out clearly -when it lands. +This repo is MIT licensed (see [LICENSE](./LICENSE)). By submitting a +contribution, you agree it is provided under the same MIT license as the rest +of the project. diff --git a/DISTILLING.md b/DISTILLING.md index 798f166..bb20115 100644 --- a/DISTILLING.md +++ b/DISTILLING.md @@ -24,17 +24,18 @@ Work through every file that's new or changed since the last distillation. For each one: **Strip entirely - these should never appear in this repo:** -- Internal Rails/app file paths and method names (`app/services/...`, +- Internal Rails/app file paths and method names (`app//...`, `client.rb#some_method`, specific class/service names from the private repo's codebase). -- Internal hostnames and URLs (`demo.butterstack.com`, `staging.butterstack.com`, - any `*.butterstack.com` subdomain that isn't the public marketing site, - internal admin panels). +- Internal hostnames and URLs (`.example.com`-shaped internal subdomains, + any subdomain of the private repo's production domain that isn't the public + marketing site, internal admin panels). - Docker container/service names tied to the private repo's compose setup - (`butter_stack-jenkins-1` and similar). -- Personal names (Kevin, Ryan, or any other individual) - reword to "the team", - "we", or drop the attribution entirely. First-person plural ("we found", - "we recommend") reads fine and is honest without naming anyone. + (`-jenkins-1`-shaped names and similar). +- Personal names (individual names used internally, or any other individual) - + reword to "the team", "we", or drop the attribution entirely. First-person + plural ("we found", "we recommend") reads fine and is honest without naming + anyone. - Internal tracking references: GitHub issue numbers, branch names (`claude/issue-36-...`), PR numbers, internal doc filenames. - Private repo URLs, including private forks (e.g. an org-internal fork of a @@ -58,7 +59,7 @@ each one: what make a playbook copy-pasteable and are never sensitive. **Genericize - keep the substance, drop the specific internal-ish identifier:** -- Personal or team machine/rig names (`beast`, `wanda`, `beast-wsl`) → "the +- Personal or team machine/rig names (`rig-01`, `rig-02`, `workstation-b`) → "the primary test machine," "a second machine on hand." - An internal environment name used as a stand-in for "a real server" (e.g. "the demo Perforce server") → "our own internal Perforce server" or similar - the @@ -72,9 +73,13 @@ each one: to disclose, genericize it; if it's just a label for "our second test game," it's fine. -Run a final grep sweep before considering a file done: +Run a final grep sweep before considering a file done. The pattern below is a +shape, not the real values - swap in the actual internal names, project +prefix, and production subdomains from the private repo before running it +(deliberately not spelled out here so this document doesn't itself become a +list of live internal identifiers): ``` -grep -rniE 'Kevin|Ryan|butter_stack-|app/(services|clients|controllers)|demo\.butterstack|staging\.butterstack|issue #[0-9]|branch `claude/' +grep -rniE '|-|app/(services|clients|controllers)|\.|issue #[0-9]|branch `claude/' ``` Zero hits, or every hit explained, is the bar. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e6146ae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 ButterStack + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 06423b8..67f661c 100644 --- a/README.md +++ b/README.md @@ -54,11 +54,20 @@ a standing basis - see [DISTILLING.md](./DISTILLING.md) for the process. ## License -**Not yet licensed for reuse.** This repo does not currently include a LICENSE file, -so all rights are reserved by default - treat it as read-only reference, not -licensed open-source code. A license decision (most likely MIT, matching -ButterStack's stated intent for this series) is still pending and will be made -before this repo is ever made public. +MIT. See [LICENSE](./LICENSE). + +## Trademarks + +Perforce, Helix Core, and P4 are trademarks or registered trademarks of Perforce +Software, Inc. Unreal Engine, Epic Games, and Lore are trademarks or registered +trademarks of Epic Games, Inc. Unity is a trademark or registered trademark of +Unity Technologies. Jenkins is a trademark of the Jenkins project (a +Continuous Delivery Foundation project). + +ButterStack is not affiliated with, endorsed by, or sponsored by Perforce +Software, Epic Games, Unity Technologies, or the Jenkins project. These marks +are used only to identify the third-party tools these agents observe and +operate; naming a tool is not a claim of partnership or endorsement. --- diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..0b92065 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,39 @@ +# Security Policy + +## Reporting a vulnerability + +If you find a security issue in one of these agents, skills, or guard hooks - +including a way to bypass a guard hook's "hard block," a prompt-injection +path, or anything else with a real security impact - please report it +privately rather than opening a public issue. + +Email **hello@butterstack.com** with a description of the issue, the affected +plugin, and reproduction steps if you have them. We will acknowledge reports +within a few business days and keep you updated as we work through a fix. + +Please do not open a public GitHub issue for a security report until we've +had a chance to address it. + +## Scope + +This repo ships Claude Code plugins: agent definitions, skills, slash +commands, and PreToolUse guard hooks (`*/scripts/guard-*.sh`) for Perforce, +Unreal Engine, Unity, Epic's Lore VCS, and Jenkins integrations. In scope: + +- A guard hook that fails to block a command it documents as blocked. +- A prompt-injection or command-injection path in any agent, skill, or hook. +- Anything that could exfiltrate credentials or tokens from the environment + the agent runs in. + +Out of scope: vulnerabilities in Perforce, Unreal Engine, Unity, Lore, or +Jenkins themselves - report those to the respective vendor. + +## A note on the guard hooks + +The guard hooks in this repo are shell-string matchers, not sandboxes. They +are a backstop against common invocation forms of catastrophic operations, +not a guarantee against every possible way to construct an equivalent +command. Each plugin's README says plainly what the actual safety boundary +is (the underlying account's own permissions, source control, or both) - the +hooks reduce the blast radius of an obvious mistake, they do not replace +least-privileged credentials. diff --git a/jenkins/.claude-plugin/plugin.json b/jenkins/.claude-plugin/plugin.json index 8857589..3f35e53 100644 --- a/jenkins/.claude-plugin/plugin.json +++ b/jenkins/.claude-plugin/plugin.json @@ -3,6 +3,7 @@ "displayName": "Jenkins Agent", "description": "Observe, operate, and safely diagnose a Jenkins CI/CD server. A read-first, gamedev-focused Claude Code agent that pairs with the Perforce agent's change-commit build loop.", "version": "0.1.0", + "license": "MIT", "author": { "name": "ButterStack" }, diff --git a/jenkins/README.md b/jenkins/README.md index aeeb230..ea4ad91 100644 --- a/jenkins/README.md +++ b/jenkins/README.md @@ -83,7 +83,9 @@ Jenkins admin/`ADMINISTER`-holding account. (`/script`, `/scriptText`, CLI `groovy`/`groovysh`) regardless of how your Claude Code permissions are configured - but a least-privileged Jenkins account that simply *can't* reach the Script Console (no `ADMINISTER`/`RUN_SCRIPTS` permission) -is the guardrail that actually matters. +is the guardrail that actually matters. It is a shell-string matcher, not a full +shell-grammar parser, so treat it as a backstop against the common mistake, not +a guarantee against every way to construct the same request. ## Quickstart diff --git a/lore/.claude-plugin/plugin.json b/lore/.claude-plugin/plugin.json index f6cb433..6226ddf 100644 --- a/lore/.claude-plugin/plugin.json +++ b/lore/.claude-plugin/plugin.json @@ -3,6 +3,7 @@ "displayName": "Lore Agent (Epic Games VCS)", "description": "Observe, analyze, and safely operate an Epic Games Lore version control system (the `lore` CLI - formerly Unreal Revision Control). A read-first, gamedev-focused Claude Code agent with observability + operations skills and slash commands.", "version": "0.1.0", + "license": "MIT", "author": { "name": "ButterStack" }, diff --git a/lore/README.md b/lore/README.md index 0eb3703..18ec495 100644 --- a/lore/README.md +++ b/lore/README.md @@ -79,7 +79,9 @@ commit identity (`lore ... --identity you@example.com`, or `identity` in **The real safety boundary is your Lore account and server permissions**, not Claude Code's. As a backstop, this plugin ships a `guard-lore` hook that hard-blocks `lore file obliterate` (irreversible fragment destruction) and `lore repository -delete` regardless of how your permissions are set. +delete` regardless of how your permissions are set. It is a shell-string matcher, +not a full shell-grammar parser - a backstop against the common mistake, not a +guarantee against every way to construct the same request. Optionally reduce prompts for read-only inspection by allowlisting the safe commands in your **own** `~/.claude/settings.json` or project `.claude/settings.json` (plugins diff --git a/perforce/.claude-plugin/plugin.json b/perforce/.claude-plugin/plugin.json index da40930..be3241f 100644 --- a/perforce/.claude-plugin/plugin.json +++ b/perforce/.claude-plugin/plugin.json @@ -3,6 +3,7 @@ "displayName": "Perforce Agent (Helix Core)", "description": "Observe, analyze, and safely operate a Perforce / Helix Core (p4/p4d) system. A read-first, gamedev-focused Claude Code agent with observability + operations skills and slash commands.", "version": "0.2.0", + "license": "MIT", "author": { "name": "ButterStack" }, diff --git a/perforce/README.md b/perforce/README.md index 4ef0211..0e3a9ca 100644 --- a/perforce/README.md +++ b/perforce/README.md @@ -60,7 +60,10 @@ read-only Perforce user - never as `super`/admin.** Perforce protections, not Cl Code permissions, are what actually stop a destructive command. As a backstop, this plugin also ships a `guard-p4` hook that hard-blocks `p4 obliterate`, `p4 admin`, `archive`/`restore`, `dbverify`, and other server-lifecycle commands regardless of -how your permissions are set. +how your permissions are set, including when a benign `p4` call precedes the +destructive one in the same command (`p4 info && p4 obliterate ...`). It is a +shell-string matcher, not a full shell-grammar parser - the p4 account's own +permissions remain the control that cannot be talked around. Two settings are worth adding to your **own** `~/.claude/settings.json` or project `.claude/settings.json` (plugins can't ship `env`/`permissions` for you): diff --git a/perforce/scripts/guard-p4.sh b/perforce/scripts/guard-p4.sh index a4803b5..523b147 100755 --- a/perforce/scripts/guard-p4.sh +++ b/perforce/scripts/guard-p4.sh @@ -19,26 +19,34 @@ else fi [ -n "$cmd" ] || exit 0 -# Extract the p4 SUBCOMMAND (verb): the first non-flag token after a `p4`/`.../p4` -# token, skipping global flags and the values of value-taking flags. Matching the -# verb position (not "anywhere in the string") avoids false positives like a depot -# path //depot/archive/... being read as the `archive` verb. -verb="$(printf '%s' "$cmd" | awk '{ - s=0 - for (i=1;i<=NF;i++) { if ($i ~ /(^|\/)p4$/) { s=i; break } } - if (!s) exit - for (i=s+1;i<=NF;i++) { - t=$i - if (t ~ /^-/) { if (t ~ /^-(p|u|c|C|d|H|P|Q|r|v|x|z|F|L|G|I)$/) i++; continue } - print t; exit +# Extract the p4 SUBCOMMAND (verb): for EVERY `p4`/`.../p4` token in the command, +# the first non-flag token after it, skipping global flags and the values of +# value-taking flags. Emitting a verb per occurrence (not just the first) catches +# forms like `p4 info && p4 obliterate ...` or `p4 -ztag info; p4 obliterate ...`, +# where a benign p4 call precedes the destructive one. Matching the verb POSITION +# (not "anywhere in the string") avoids false positives like a depot path +# //depot/archive/... being read as the `archive` verb. +verbs="$(printf '%s' "$cmd" | awk '{ + for (s=1;s<=NF;s++) { + if ($s !~ /(^|\/)p4$/) continue + for (i=s+1;i<=NF;i++) { + t=$i + if (t ~ /^-/) { if (t ~ /^-(p|u|c|C|d|H|P|Q|r|v|x|z|F|L|G|I)$/) i++; continue } + print t; break + } } }')" +[ -n "$verbs" ] || exit 0 -case "$verb" in - obliterate|admin|dbverify|dbpack|journalcopy|journaldbchecksums|archive|restore|unload|ldapsync|storage|unsubmit|duplicate|retype) - echo "guard-p4: refusing '$verb' - server-lifecycle / archive / history-rewriting operations are out of scope for this agent. Refer to the Perforce administrator." >&2 - exit 2 - ;; -esac +while IFS= read -r verb; do + case "$verb" in + obliterate|admin|dbverify|dbpack|journalcopy|journaldbchecksums|archive|restore|unload|ldapsync|storage|unsubmit|duplicate|retype) + echo "guard-p4: refusing '$verb' - server-lifecycle / archive / history-rewriting operations are out of scope for this agent. Refer to the Perforce administrator." >&2 + exit 2 + ;; + esac +done <