diff --git a/.claude/skills/running-tend/SKILL.md b/.claude/skills/running-tend/SKILL.md new file mode 100644 index 0000000..3577ec0 --- /dev/null +++ b/.claude/skills/running-tend/SKILL.md @@ -0,0 +1,2 @@ +No project-specific tend preferences yet. Add guidance here as +needed — this file is loaded by tend workflows alongside AGENTS.md. diff --git a/.config/tend.yaml b/.config/tend.yaml new file mode 100644 index 0000000..9d09e08 --- /dev/null +++ b/.config/tend.yaml @@ -0,0 +1,11 @@ +bot_name: dormouse-bot + +secrets: + allowed: + - CHROMATIC_PROJECT_TOKEN + +workflows: + ci-fix: + watched_workflows: + - CI + - Chromatic diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8a82418..17ee8e6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -218,11 +218,17 @@ jobs: vscode-ext/*.vsix vscode-ext/artifact-manifest.sha256 + security-audit: + name: Security audit + uses: ./.github/workflows/security-audit.yaml + secrets: inherit + publish-vscode: name: Publish VSCode Extension needs: - build-standalone - build-vscode + - security-audit runs-on: ubuntu-latest environment: name: vscode-extension-publish diff --git a/.github/workflows/security-audit.yaml b/.github/workflows/security-audit.yaml new file mode 100644 index 0000000..4e683b2 --- /dev/null +++ b/.github/workflows/security-audit.yaml @@ -0,0 +1,133 @@ +name: security-audit + +# Audits this repo against SECURITY.md. Reusable: runs nightly via the +# schedule trigger, on-demand via workflow_dispatch, and is called from +# release.yml as a precondition to publishing. + +on: + schedule: + - cron: "21 4 * * *" + workflow_dispatch: + workflow_call: + +permissions: + contents: read + actions: read + issues: write + +jobs: + audit: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: 22 + + - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6.0.8 + with: + version: 11.0.6 + + - name: Install workspace dependencies + run: pnpm install --frozen-lockfile + + - name: Audit against SECURITY.md + uses: anthropics/claude-code-action@4481e6d3c7bbb88db2a928ca3444c536f589c7c1 # v1 + env: + GH_TOKEN: ${{ github.token }} + with: + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + prompt: | + You are auditing this repository against SECURITY.md. The + specifications are concrete `FAIL IF` lines plus the + explicit clause that the list is not exhaustive — any code + change that creates a security hole or reveals an existing + one should fail this job. + + Process: + 1. Read SECURITY.md. + 2. For each `FAIL IF` line, identify the mechanical check + (gh api, grep, file presence, running a script) and + execute it. Record PASS or FAIL with concrete evidence + — file path and line number, API response excerpt, or + command output. + 3. After the FAIL IF list is exhausted, do a qualitative + pass. Inspect `.github/workflows/`, `.config/tend.yaml`, + `.github/dependabot.yml`, `scripts/`, and any code that + references secrets, for security holes the specs don't + cover. + + Produce a Markdown report with three sections: + - `## FAIL IF results` — one line per check with PASS/FAIL + and concrete evidence + - `## Qualitative findings` — free-form findings with + severity (BLOCKER / WARNING / INFO) + - `## Summary` — overall PASS or FAIL with a one-paragraph + rationale + + Write the report to `audit-report.md` in the workspace. + Write `PASS` or `FAIL` (no other text, no newline required) + to `audit-status.txt`. Status is FAIL if any `FAIL IF` is + violated or any qualitative finding is BLOCKER severity. + Do not call `exit`; the next workflow step inspects the + status file and surfaces the result. + + Available environment: `$GH_TOKEN` is the workflow's + GitHub token (read repo, write issues), `$GITHUB_REPOSITORY` + is `owner/name`. Use `gh api` for GitHub configuration + queries (rulesets, secrets, environments, collaborators). + + - name: Surface result, file or close issue + if: always() + env: + GH_TOKEN: ${{ github.token }} + run: | + set -eo pipefail + + STATUS=$(tr -d '[:space:]' < audit-status.txt 2>/dev/null || echo "FAIL") + DATE=$(date -u +%Y-%m-%dT%H:%MZ) + RUN_URL="https://github.com/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + + # Idempotent label creation; ignore "already exists" errors. + gh label create security-audit-failure \ + --color B60205 --description "Security audit failure" 2>/dev/null || true + + if [ "$STATUS" = "PASS" ]; then + # Auto-close any open audit-failure issues so the issue + # tracker reflects the live state. + for n in $(gh issue list --label security-audit-failure \ + --state open --json number --jq '.[].number'); do + gh issue close "$n" --comment "Audit passed at $DATE. [Run]($RUN_URL)" + done + echo "Audit passed." + exit 0 + fi + + if [ ! -s audit-report.md ]; then + printf '%s\n' \ + "Audit step produced no \`audit-report.md\`. See workflow run logs." \ + > audit-report.md + fi + + { + echo "Audit failed at $DATE. [Run]($RUN_URL)" + echo + cat audit-report.md + } > audit-comment.md + + EXISTING=$(gh issue list --label security-audit-failure \ + --state open --json number --jq '.[0].number' || true) + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --body-file audit-comment.md + echo "Appended re-audit failure to issue #$EXISTING" + else + gh issue create \ + --title "[security-audit] FAIL on $(date -u +%Y-%m-%d)" \ + --label security-audit-failure \ + --body-file audit-comment.md + fi + exit 1 diff --git a/.github/workflows/tend-ci-fix.yaml b/.github/workflows/tend-ci-fix.yaml new file mode 100644 index 0000000..18ec44b --- /dev/null +++ b/.github/workflows/tend-ci-fix.yaml @@ -0,0 +1,44 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-ci-fix +on: + workflow_run: + workflows: ["CI", "Chromatic"] + types: [completed] + branches: ["main"] + +jobs: + fix-ci: + if: github.repository_owner == 'diffplug' && github.event.workflow_run.conclusion == 'failure' + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: | + /tend-ci-runner:ci-fix ${{ github.event.workflow_run.id }} + - Run URL: ${{ github.event.workflow_run.html_url }} + - Commit: ${{ github.event.workflow_run.head_sha }} + - Commit message: ${{ github.event.workflow_run.head_commit.message }} diff --git a/.github/workflows/tend-install-test.yaml b/.github/workflows/tend-install-test.yaml new file mode 100644 index 0000000..c700b96 --- /dev/null +++ b/.github/workflows/tend-install-test.yaml @@ -0,0 +1,71 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-install-test +on: + pull_request: + paths: + - .github/workflows/tend-*.yaml + - .config/tend.yaml + +jobs: + install-test: + # Same-repo PRs only. Fork PRs don't carry secrets and the workflow + # is short-lived (removed on the next nightly regen), so cross-fork + # validation isn't worth special-casing. + if: github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v6 + - name: Verify required secrets are set + env: + BOT_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + CLAUDE_OAUTH: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + ANTHROPIC_KEY: ${{ secrets.ANTHROPIC_API_KEY }} + run: | + missing="" + [ -n "$BOT_TOKEN" ] || missing="$missing TEND_BOT_TOKEN" + if [ -z "$CLAUDE_OAUTH$ANTHROPIC_KEY" ]; then + missing="$missing harness-auth(set CLAUDE_CODE_OAUTH_TOKEN or ANTHROPIC_API_KEY)" + fi + if [ -n "$missing" ]; then + echo "::error::Missing repo secrets:$missing" + exit 1 + fi + - name: Verify generator output matches committed files + env: + GH_TOKEN: ${{ github.token }} + run: | + # actions/checkout's default shallow clone leaves + # refs/remotes/origin/ unfetched, so + # `git remote set-head origin --auto` errors with + # "Not a valid ref: refs/remotes/origin/main" before any + # default-branch detection runs (tend issue #582). Query the + # API and set the symbolic-ref directly. + DEFAULT_BRANCH=$(gh api "repos/$GITHUB_REPOSITORY" --jq .default_branch) + git fetch origin --depth 1 "+refs/heads/$DEFAULT_BRANCH:refs/remotes/origin/$DEFAULT_BRANCH" + git symbolic-ref refs/remotes/origin/HEAD "refs/remotes/origin/$DEFAULT_BRANCH" + # Pin the regen to the version in the committed header so a tend + # release between local `init` and this CI run doesn't fail the + # drift check for an irrelevant reason. + HEADER=$(head -1 .github/workflows/tend-install-test.yaml) + HEADER="${HEADER#'# Generated by tend '}" + TEND_VERSION="${HEADER%%'. Regenerate'*}" + uvx "tend@$TEND_VERSION" init --with-install-test + # Exclude this file from the drift comparison: it's locally + # patched to work around tend issue #582, so it diverges from + # generator output by design until upstream lands a fix. The + # file is removed on the next nightly regen anyway. + if ! git diff --quiet -- ':!.github/workflows/tend-install-test.yaml' .github/workflows/; then + echo "::error::Committed workflows differ from 'uvx tend@$TEND_VERSION init --with-install-test' output. Run it locally and commit the result." + git --no-pager diff -- ':!.github/workflows/tend-install-test.yaml' .github/workflows/ + exit 1 + fi diff --git a/.github/workflows/tend-mention.yaml b/.github/workflows/tend-mention.yaml new file mode 100644 index 0000000..47ea741 --- /dev/null +++ b/.github/workflows/tend-mention.yaml @@ -0,0 +1,242 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-mention +on: + issues: + types: [edited] + issue_comment: + types: [created, edited] + # Works for same-repo PRs only; secrets unavailable on fork PRs (no _target variant exists) + pull_request_review: + types: [submitted] + # `created` is intentionally absent. Modern GitHub fires *both* + # pull_request_review and pull_request_review_comment for every newly-created + # inline comment (the standalone POST /pulls/PR/comments endpoint, the + # /replies endpoint, the "Add single comment" UI button, and reviews + # submitted with inline comments — all empirically verified). Subscribing to + # `created` would produce a duplicate workflow run that collides on the + # tend-mention-handle-PR concurrency group, with the loser cancelled and + # posted as a CANCELLED check_run on the PR head SHA — which renders the + # PR's statusCheckRollup as FAILURE even though the bot did its job from the + # sibling run. Edits have no sibling event (review submissions don't fire on + # edits), so we still need to listen for `edited` to catch edit-to-summon + # ("@bot" added to an existing comment after the fact). + pull_request_review_comment: + types: [edited] + +jobs: + verify: + # Filter out fork PRs for review events — secrets are unavailable there + # (no _target variant exists). The notifications workflow polls for these. + # Skip comments on `tend-outage` issues: the action's Report-failure step + # auto-comments on those when Claude invocation fails, and without this + # guard those comments re-trigger tend-mention during a persistent outage + # (e.g. Anthropic 401), producing a self-sustaining ~1 run/minute loop + # until the outage clears. The prompt's self-loop guard can't help here + # because the model never executes — the action fails before Claude starts. + if: | + (github.event_name == 'issues' && + contains(github.event.issue.body, '@dormouse-bot')) || + (github.event_name == 'issue_comment' && + !contains(github.event.issue.labels.*.name, 'tend-outage')) || + (github.event_name == 'pull_request_review_comment' && + github.event.pull_request.head.repo.full_name == github.repository) || + (github.event_name == 'pull_request_review' && + github.event.pull_request.head.repo.full_name == github.repository) + runs-on: ubuntu-24.04 + outputs: + should_run: ${{ steps.check.outputs.should_run }} + steps: + - name: Verify bot engagement + id: check + run: | + # Mentions always run + if [ "$EVENT_NAME" = "issues" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + if [ -n "$COMMENT_BODY" ] && printf '%s\n' "$COMMENT_BODY" | grep -qF '@dormouse-bot'; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + + # pull_request_review payloads include review.body (checked above) + # but NOT the bodies of inline comments attached to the review. + # Fetch them so a first-contact @-mention inside an inline comment + # is detected on PRs where the bot has no prior engagement. + if [ "$EVENT_NAME" = "pull_request_review" ] && [ -n "$REVIEW_ID" ]; then + if gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$EVENT_PR_NUMBER/reviews/$REVIEW_ID/comments" \ + --jq '.[].body' | grep -qF '@dormouse-bot'; then + echo "should_run=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + fi + + # Non-mention: check bot engagement + if [ "$EVENT_NAME" = "issue_comment" ]; then + ISSUE_NUMBER="$ISSUE_OR_PR_NUMBER" + + if [ -z "$PR_URL" ]; then + if [ "$ISSUE_AUTHOR" = "dormouse-bot" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + if printf '%s\n' "$ISSUE_BODY" | grep -qF '@dormouse-bot'; then + echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + BOT_COMMENTS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$ISSUE_NUMBER/comments" \ + --jq '[.[] | select(.user.login == "dormouse-bot")] | length') + if [ "$BOT_COMMENTS" -gt "0" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + echo "should_run=false" >> "$GITHUB_OUTPUT"; exit 0 + fi + + PR_NUMBER="$ISSUE_NUMBER" + else + PR_NUMBER="$EVENT_PR_NUMBER" + fi + + PR_AUTHOR=$(gh pr view "$PR_NUMBER" --repo "$GITHUB_REPOSITORY" --json author --jq '.author.login') + if [ "$PR_AUTHOR" = "dormouse-bot" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + + BOT_REVIEWS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/pulls/$PR_NUMBER/reviews" \ + --jq '[.[] | select(.user.login == "dormouse-bot")] | length') + if [ "$BOT_REVIEWS" -gt "0" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + + BOT_COMMENTS=$(gh api --paginate "repos/$GITHUB_REPOSITORY/issues/$PR_NUMBER/comments" \ + --jq '[.[] | select(.user.login == "dormouse-bot")] | length') + if [ "$BOT_COMMENTS" -gt "0" ]; then + echo "should_run=true" >> "$GITHUB_OUTPUT"; exit 0 + fi + + echo "should_run=false" >> "$GITHUB_OUTPUT" + env: + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + EVENT_NAME: ${{ github.event_name }} + COMMENT_BODY: ${{ github.event.comment.body || github.event.review.body }} + ISSUE_BODY: ${{ github.event.issue.body }} + ISSUE_OR_PR_NUMBER: ${{ github.event.issue.number }} + ISSUE_AUTHOR: ${{ github.event.issue.user.login }} + PR_URL: ${{ github.event.issue.pull_request.url }} + EVENT_PR_NUMBER: ${{ github.event.pull_request.number }} + REVIEW_ID: ${{ github.event.review.id }} + + - name: React to mention + if: | + steps.check.outputs.should_run == 'true' + && github.event.comment + && contains(github.event.comment.body, '@dormouse-bot') + run: | + gh api "repos/$REPO/issues/comments/$COMMENT_ID/reactions" -f content=eyes 2>/dev/null || \ + gh api "repos/$REPO/pulls/comments/$COMMENT_ID/reactions" -f content=eyes 2>/dev/null || true + env: + REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + + handle: + needs: verify + if: needs.verify.outputs.should_run == 'true' + concurrency: + group: ${{ github.workflow }}-handle-${{ github.event.issue.number || github.event.pull_request.number }} + cancel-in-progress: false + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - name: Check out PR branch + if: | + (github.event_name == 'issue_comment' && github.event.issue.pull_request.url != '') || + github.event_name == 'pull_request_review_comment' || + github.event_name == 'pull_request_review' + run: | + PR_STATE=$(gh pr view "$PR_NUMBER" --json state --jq '.state') + if [ "$PR_STATE" = "OPEN" ]; then + gh pr checkout "$PR_NUMBER" + else + echo "::warning::PR is $PR_STATE — staying on default branch" + fi + env: + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + PR_NUMBER: ${{ github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number }} + + - name: Compute queue delay + id: delay + run: | + if [ -z "$EVENT_TS" ]; then + echo "seconds=" >> "$GITHUB_OUTPUT" + exit 0 + fi + event_epoch=$(date -d "$EVENT_TS" +%s) + echo "seconds=$(( $(date +%s) - event_epoch ))" >> "$GITHUB_OUTPUT" + env: + EVENT_TS: ${{ github.event.comment.created_at || github.event.review.submitted_at || github.event.issue.updated_at }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: >- + ${{ steps.delay.outputs.seconds + && format('This job started {0}s after the triggering event (over ~40s means it was queued). ', + steps.delay.outputs.seconds) || '' }}Before acting, + check recent comments: exit silently if the bot already responded + to the trigger; handle any other unaddressed comments too. + + ${{ github.event_name == 'issues' + && format('An issue was updated with a mention of you ({0}). Read it and respond.', github.event.issue.html_url) + || (github.event_name == 'pull_request_review_comment' && contains(github.event.comment.body, '@dormouse-bot') + && format('You were mentioned in an inline review comment on PR #{0} ({1}, comment ID {2}). Read the full context, then respond. If changes are requested, make them, commit, and push.', (github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number), github.event.comment.html_url, github.event.comment.id)) + || (github.event_name == 'pull_request_review_comment' + && format('An inline review comment was posted on a PR where you previously participated (PR #{0}, {1}, comment ID {2}). Read the full context. Only respond if the comment is directed at you or requests changes.', (github.event_name == 'issue_comment' && github.event.issue.number || github.event.pull_request.number), github.event.comment.html_url, github.event.comment.id)) + || (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@dormouse-bot') + && format('A review was submitted on PR #{0} that mentions you ({1}, review ID {2}). Read the review and full context, then respond. If changes were requested, make them, commit, and push.', github.event.pull_request.number, github.event.review.html_url, github.event.review.id)) + || (github.event_name == 'pull_request_review' + && format('A review was submitted on a PR where you previously participated (PR #{0}, {1}, review ID {2}). Read the review and full context. If the review requests changes or asks questions, respond appropriately. If the review approves or is between humans, exit silently.', github.event.pull_request.number, github.event.review.html_url, github.event.review.id)) + || (contains(github.event.comment.body, '@dormouse-bot') + && format('You were mentioned in a comment ({0}). Read the full context and respond. If changes are requested, make them, commit, and push.', github.event.comment.html_url)) + || format('A user commented on an issue/PR where you previously participated ({0}). Read the full context. Only respond if the comment is directed at you, asks a question you can help with, or requests changes you can make. If the conversation is between other participants, exit silently.', github.event.comment.html_url) + }} + + - name: Remove eyes reaction + if: | + always() + && github.event.comment + && contains(github.event.comment.body, '@dormouse-bot') + run: | + for KIND in issues pulls; do + REACTION_ID=$(gh api "repos/$REPO/$KIND/comments/$COMMENT_ID/reactions?content=eyes" \ + --jq ".[] | select(.user.login == \"$BOT_NAME\") | .id" 2>/dev/null | head -n1) + if [ -n "$REACTION_ID" ]; then + gh api -X DELETE "repos/$REPO/$KIND/comments/$COMMENT_ID/reactions/$REACTION_ID" 2>/dev/null && break + fi + done + env: + REPO: ${{ github.repository }} + COMMENT_ID: ${{ github.event.comment.id }} + BOT_NAME: dormouse-bot + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} diff --git a/.github/workflows/tend-nightly.yaml b/.github/workflows/tend-nightly.yaml new file mode 100644 index 0000000..e2c755c --- /dev/null +++ b/.github/workflows/tend-nightly.yaml @@ -0,0 +1,41 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-nightly +on: + schedule: + - cron: "17 6 * * *" + workflow_dispatch: + +jobs: + nightly: + if: github.repository_owner == 'diffplug' + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: | + /tend-ci-runner:nightly diff --git a/.github/workflows/tend-notifications.yaml b/.github/workflows/tend-notifications.yaml new file mode 100644 index 0000000..00c1be6 --- /dev/null +++ b/.github/workflows/tend-notifications.yaml @@ -0,0 +1,108 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-notifications +on: + schedule: + - cron: "*/15 * * * *" + workflow_dispatch: + +jobs: + notifications: + if: github.repository_owner == 'diffplug' + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + - name: Check for unread notifications + id: check + run: | + COUNT=$(gh api notifications --jq 'length') + if [ "$COUNT" = "0" ]; then + echo "count=0" >> "$GITHUB_OUTPUT" + echo "No unread notifications — skipping" + exit 0 + fi + + # --- Layer B: drop notifications shadowed by recent dedicated runs --- + # Event workflows mark their own notifications read via action.yaml's + # post-step on success; this sweeps the case where Claude failed + # (post-step is gated by `if: success()`) so the notification still + # gets cleared without burning Claude turns to rediscover it. + SINCE=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%SZ) + RECENT_PRS=$(gh api "repos/$GITHUB_REPOSITORY/actions/runs?created=>=$SINCE&per_page=50" --jq '[.workflow_runs[] | select(.name | test("^(tend-review|tend-mention|tend-triage|tend-ci-fix)$")) | .pull_requests[]?.number] | unique | .[]' || true) + + if [ -n "$RECENT_PRS" ]; then + NOTIFS=$(gh api notifications) + for pr in $RECENT_PRS; do + echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" --arg pr "$pr" '.[] | select(.subject.url == "https://api.github.com/repos/" + $repo + "/pulls/" + $pr or .subject.url == "https://api.github.com/repos/" + $repo + "/issues/" + $pr) | .id' | while read -r tid; do + [ -n "$tid" ] || continue + gh api "notifications/threads/$tid" -X PATCH || true + done + done + fi + + # --- Layer C: drop notifications on bot-authored closed PRs --- + # The bot auto-subscribes to its own PRs. After merge/close, leftover + # subscription notifications are pure noise — no action needed. + NOTIFS=$(gh api notifications) + echo "$NOTIFS" | jq -r --arg repo "$GITHUB_REPOSITORY" '.[] | select(.repository.full_name == $repo and .subject.type == "PullRequest") | .id' | while read -r tid; do + [ -n "$tid" ] || continue + PR_NUM=$(echo "$NOTIFS" | jq -r --arg tid "$tid" '.[] | select(.id == $tid) | .subject.url | split("/") | last') + PR_INFO=$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR_NUM" --jq '"\(.user.login) \(.state)"' 2>/dev/null) || continue + PR_AUTHOR=${PR_INFO%% *} + PR_STATE=${PR_INFO##* } + if [ "$PR_AUTHOR" = "dormouse-bot" ] && [ "$PR_STATE" = "closed" ]; then + gh api "notifications/threads/$tid" -X PATCH || true + fi + done + + # --- Layer D: count processable notifications --- + # Same-repo notifications younger than 10 minutes are deferred: a + # dedicated workflow (tend-review/mention/triage/ci-fix) is likely + # still starting up or mid-flight and hasn't posted its response yet. + # Processing them now risks duplicating work. Cross-repo notifications + # are exempt — no dedicated workflow handles them. + CUTOFF=$(date -u -d '10 minutes ago' +%Y-%m-%dT%H:%M:%SZ) + REMAINING=$(gh api notifications) + COUNT=$(echo "$REMAINING" | jq --arg repo "$GITHUB_REPOSITORY" --arg cutoff "$CUTOFF" '[.[] | select(.repository.full_name != $repo or .updated_at <= $cutoff)] | length') + echo "count=$COUNT" >> "$GITHUB_OUTPUT" + if [ "$COUNT" = "0" ]; then + TOTAL=$(echo "$REMAINING" | jq 'length') + if [ "$TOTAL" = "0" ]; then + echo "All notifications handled by pre-checks — skipping" + else + echo "$TOTAL notification(s) remain but all are fresh same-repo (deferred) — skipping" + fi + else + echo "$COUNT processable notification(s) — proceeding" + fi + env: + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: actions/checkout@v6 + if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + - uses: max-sixty/tend@0.0.25 + if: steps.check.outputs.count != '0' || github.event_name == 'workflow_dispatch' + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: | + /tend-ci-runner:notifications diff --git a/.github/workflows/tend-review-runs.yaml b/.github/workflows/tend-review-runs.yaml new file mode 100644 index 0000000..440e0b6 --- /dev/null +++ b/.github/workflows/tend-review-runs.yaml @@ -0,0 +1,41 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-review-runs +on: + schedule: + - cron: "47 7 * * *" + workflow_dispatch: + +jobs: + review-runs: + if: github.repository_owner == 'diffplug' + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: | + /tend-ci-runner:review-runs diff --git a/.github/workflows/tend-review.yaml b/.github/workflows/tend-review.yaml new file mode 100644 index 0000000..090e618 --- /dev/null +++ b/.github/workflows/tend-review.yaml @@ -0,0 +1,60 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-review +on: + pull_request_target: + types: [opened, synchronize, ready_for_review, reopened] + +jobs: + review: + concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + # GitHub only materializes refs/pull/N/merge for mergeable PRs — on + # conflicting PRs it 404s and every downstream step cascades as skipped. + # Probe first and fall back to /head so review always runs; on fallback + # the review sees the PR branch in isolation rather than the post-merge + # tree. + - name: Resolve PR checkout ref + id: pr_ref + env: + GITHUB_TOKEN: ${{ secrets.TEND_BOT_TOKEN }} + PR: ${{ github.event.pull_request.number }} + run: | + if gh api "repos/${{ github.repository }}/git/ref/pull/$PR/merge" --silent 2>/dev/null; then + echo "ref=refs/pull/$PR/merge" >> "$GITHUB_OUTPUT" + else + echo "ref=refs/pull/$PR/head" >> "$GITHUB_OUTPUT" + echo "::notice::refs/pull/$PR/merge unavailable (likely merge conflict); falling back to /head" + fi + - uses: actions/checkout@v6 + with: + ref: ${{ steps.pr_ref.outputs.ref }} + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + use_sticky_comment: true + prompt: >- + ${{ format('/tend-ci-runner:review {0}', github.event.pull_request.number) }} diff --git a/.github/workflows/tend-triage.yaml b/.github/workflows/tend-triage.yaml new file mode 100644 index 0000000..941d09f --- /dev/null +++ b/.github/workflows/tend-triage.yaml @@ -0,0 +1,44 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-triage +on: + issues: + types: [opened] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.issue.number }} + cancel-in-progress: true + +jobs: + triage: + if: github.repository_owner == 'diffplug' + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: | + /tend-ci-runner:triage ${{ github.event.issue.number }} diff --git a/.github/workflows/tend-weekly.yaml b/.github/workflows/tend-weekly.yaml new file mode 100644 index 0000000..b6d818f --- /dev/null +++ b/.github/workflows/tend-weekly.yaml @@ -0,0 +1,41 @@ +# Generated by tend 0.0.25. Regenerate with: uvx tend@latest init +# +# Do not edit this file directly — it will be overwritten on regeneration. +# To customize behavior, edit the relevant skill (for example, +# `running-tend`) in this repo's .claude/skills/ directory, or open an issue at +# https://github.com/max-sixty/tend/issues for changes that need to +# happen upstream in the tend-ci-runner plugin. + +name: tend-weekly +on: + schedule: + - cron: "17 9 * * 0" + workflow_dispatch: + +jobs: + weekly: + if: github.repository_owner == 'diffplug' + runs-on: ubuntu-24.04 + permissions: + contents: write + pull-requests: write + id-token: write + actions: read + issues: write + steps: + - uses: actions/checkout@v6 + with: + ref: main + fetch-depth: 0 + fetch-tags: true + token: ${{ secrets.TEND_BOT_TOKEN }} + + - uses: max-sixty/tend@0.0.25 + with: + github_token: ${{ secrets.TEND_BOT_TOKEN }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} + anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }} + bot_name: dormouse-bot + model: opus + prompt: | + /tend-ci-runner:weekly diff --git a/.github/workflows/workflow-audit.yaml b/.github/workflows/workflow-audit.yaml new file mode 100644 index 0000000..031e177 --- /dev/null +++ b/.github/workflows/workflow-audit.yaml @@ -0,0 +1,82 @@ +name: workflow-audit + +# Nightly audit of every commit touching .github/workflows/. Surfaces +# changes from feature branches and direct pushes, not just the main +# branch — so a bot push that adds a new workflow file gets a visible +# issue even if it never opens a PR. +# +# Gap-resistant: the "since" lower bound comes from the previous +# successful run's API timestamp, so a failed run pushes the window +# forward rather than skipping commits. + +on: + schedule: + - cron: "13 7 * * *" + workflow_dispatch: + +permissions: + contents: read + issues: write + actions: read + +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Fetch all branches + run: git fetch origin '+refs/heads/*:refs/remotes/origin/*' + + - name: Audit workflow file changes + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + + SINCE=$(gh api \ + "/repos/$GITHUB_REPOSITORY/actions/workflows/workflow-audit.yaml/runs?status=success&per_page=1" \ + --jq '.workflow_runs[0].created_at // ""') + if [ -z "$SINCE" ]; then + SINCE=$(date -u -d '25 hours ago' --iso-8601=seconds) + fi + echo "Auditing commits since: $SINCE" + + COMMITS=$(git log --all --since="$SINCE" --pretty=format:'%H' \ + -- .github/workflows/ | sort -u) + + if [ -z "$COMMITS" ]; then + echo "No workflow file changes since $SINCE." + exit 0 + fi + + COUNT=$(echo "$COMMITS" | wc -l | tr -d ' ') + REPORT=$(mktemp) + { + echo "$COUNT commit(s) touching \`.github/workflows/\` since \`$SINCE\`:" + echo "" + for sha in $COMMITS; do + AUTHOR=$(git show -s --format='%an <%ae>' "$sha") + DATE=$(git show -s --format='%ci' "$sha") + SUBJECT=$(git show -s --format='%s' "$sha") + REFS=$(git branch -a --contains "$sha" 2>/dev/null \ + | grep -v 'HEAD ->' | head -10 \ + | sed 's/^[[:space:]]*//' | paste -sd ', ' -) + FILES=$(git show --name-only --pretty='' "$sha" -- .github/workflows/) + echo "### \`${sha:0:7}\` — $SUBJECT" + echo "" + echo "- **Author:** $AUTHOR" + echo "- **Date:** $DATE" + echo "- **Refs:** $REFS" + echo "- **Files:**" + echo "$FILES" | sed 's|^| - `|; s|$|`|' + echo "- [View diff](https://github.com/$GITHUB_REPOSITORY/commit/$sha)" + echo "" + done + } > "$REPORT" + + TITLE="[workflow-audit] $COUNT change(s) on $(date -u +%Y-%m-%d)" + gh issue create --repo "$GITHUB_REPOSITORY" \ + --title "$TITLE" --body-file "$REPORT" diff --git a/AGENTS.md b/AGENTS.md index 919eb5b..123e56d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,7 +45,7 @@ When updating code covered by a spec, update the spec to match. When the two spe ## Design -See [.impeccable.md](.impeccable.md) for full design context. Key principles: +See [DESIGN.md](DESIGN.md) for full design context. Key principles: 1. **Native first** — Inside VSCode, feel indistinguishable from a built-in feature. Use the host's theme tokens. 2. **Information density without intimidation** — Dense for power users, approachable for beginners. Progressive disclosure. diff --git a/README.md b/README.md index c4f5ccc..718b2ce 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ ![Dormouse — Multitasking Terminal for Mice](website/public/og-image.jpg) +[![maintained with tend](https://img.shields.io/badge/maintained_with-tend-bba580?logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxNiAxNiI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMCwxNikgc2NhbGUoMC4wMTI1LC0wLjAxMjUpIiBmaWxsPSIjZmZmIiBzdHJva2U9Im5vbmUiPjxwYXRoIGQ9Ik02ODAgMTEyOCBjNjIgLTk2IDY5IC0xNzggMjAgLTI0MSAtMTcgLTIyIC0yMCAtNDAgLTIwIC0xMzQgbDEgLTEwOCAyMSAyOCBjMTEgMTYgMzAgNDcgNDIgNzAgMTIgMjIgMzIgNDkgNDYgNTkgMzcgMjcgMTE0IDM4IDE4NCAyNyA5MyAtMTUgOTQgLTE4IDQ0IC03OSAtNzIgLTg4IC0xMDkgLTExMyAtMTc2IC0xMTcgLTMxIC0yIC02NCAxIC03MiA2IC0yMyAxNSAyMSA1NiAxMDcgOTggNDAgMjAgNzEgMzggNjkgNDAgLTYgNyAtODggLTE3IC0xMjYgLTM3IC00OSAtMjUgLTEwMCAtNzggLTEyMSAtMTI1IC0xNSAtMzMgLTE5IC02NiAtMTkgLTE4OCAwIC0xNTcgOCAtMTk1IDUwIC0yMzIgMTcgLTE2IDM2IC0yMCA4NSAtMTkgNjIgMSA2MyAxIDczIC0zMiA5IC0zMiA5IC0zMyAtMjIgLTQwIC01MCAtMTIgLTEzMiAtNyAtMTY0IDEwIC00MCAyMSAtNzkgNjkgLTkyIDExNCAtNSAyMCAtMTAgMTAyIC0xMCAxODIgMCA4MCAtNSAxNjIgLTExIDE4NCAtMjIgNzkgLTEzNSAxNjYgLTIzNCAxODEgLTM3IDYgLTM1IDMgMzAgLTI4IDc4IC0zOSAxNDQgLTkxIDEzMiAtMTA0IC01IC00IC0zNyAtOCAtNzEgLTggLTc3IDAgLTExNyAyNCAtMTgyIDEwOSAtNTIgNjggLTUxIDcwIDQyIDg1IDcxIDExIDE0MyAwIDE4MyAtMjkgMTYgLTExIDQwIC00MyA1NCAtNzMgMTMgLTI5IDMyIC01OSA0MSAtNjYgMTQgLTEyIDE2IC03IDE2IDU4IDAgNTkgNCA3NyAyMyAxMDIgMTkgMjYgMjMgNDYgMjUgMTMwIDMgNjcgMCA5OSAtNyA5OSAtNyAwIC0xMSAtMjMgLTEyIC01NyAwIC0zMiAtNiAtNzYgLTEyIC05NyBsLTEyIC00MCAtMjcgMzIgYy0zNCA0MSAtNDMgOTYgLTI0IDE1MSAxNCA0MSA3NSAxNDEgODYgMTQxIDMgMCAyMSAtMjQgNDAgLTUyeiIvPjwvZz48L3N2Zz4K)](https://github.com/max-sixty/tend) + ## Try it - **[Playground](https://dormouse.sh/playground)** - try in your browser, no install @@ -11,7 +13,7 @@ - **Automatic completion detection.** Detect when an agent needs your attention with standard terminal .,mn,.mn.,mnWhen a pane goes quiet for two seconds, it's marked done. Works with builds, AI agents, scripts, anything. - **tmux-compatible keybindings.** Same prefix, same splits, same pane navigation. Muscle memory transfers. - **Full mouse support.** Click to split, drag to resize, scroll to navigate. Or stay on the keyboard. -- **Copy-paste that works.** Click and drag selects text the way you'd expect, even in mouse-aware TUIs that normally swallow it as escape codes. Ctrl+C copies; killing the program is a separate gesture. +- **Copy-paste that works.** Click and drag selects text the way you'd expect, even in mouse-aware TUIs that normally swallow it as escape codes. - **Sleep/wake panes.** Minimize a terminal to a compact status indicator. It keeps running and you can still see whether its task finished. - **Dual distribution.** Standalone desktop app (Mac/Windows/Linux) or VS Code extension. - **Pocket (coming soon).** Tether your sessions to your phone over WebRTC — walk away, keep working. @@ -48,7 +50,7 @@ pnpm test # runs all tests ### Agent strategy -This project was built with a combination of Claude, Codex, and Devin. Recommend running `npx skills experimental_install` to install the skills we are using (namely [impeccable.style](https://impeccable.style/)). See [AGENTS.md](AGENTS.md) for more detail. +This project was built with a combination of Claude, Codex, and Devin. We make heavy use of the [impeccable.style](https://impeccable.style/) agent skill, we recommend having it installed. See [AGENTS.md](AGENTS.md) for more detail. ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..da1c20b --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,77 @@ +# Security + +Dormouse is a terminal, so users trust it with shells, source trees, credentials, and local files. The dependency graph and release pipeline is part of the product's security boundary. + +The policies described in this document are enforced on every PR. + +## Dependency Supply Chain + +Dormouse keeps its runtime dependency surface intentionally small. We add dependencies only when they are necessary, and we expect dependency changes to justify their value against their supply-chain risk. + +Every production transitive dependency shipped in the end-user application is listed at + +That page is generated from the lockfile and reviewed as part of release work. If a production dependency is added, removed, or upgraded, the dependency list must be regenerated and committed. + +New npm package versions are not adopted immediately. The workspace uses pnpm's package maturity gate so newly published npm versions have time to be reported, yanked, or investigated before Dormouse resolves them into the lockfile. Dependabot also applies cooldown windows for npm and Cargo updates so routine dependency PRs do not chase brand-new releases unnecessarily. + +- FAIL IF `node website/scripts/generate-deps.js` changes `website/src/data/dependencies.json` when run from a clean checkout. +- FAIL IF `pnpm-workspace.yaml` is missing `minimumReleaseAge: 1440`. +- FAIL IF `.github/dependabot.yml` is missing npm coverage for `/` or Cargo coverage for `/standalone/src-tauri`. +- FAIL IF `.github/dependabot.yml` is missing dependency cooldown windows. + +## GitHub Actions Policies + +GitHub Actions are always pinned by commit hash, not version tag. Dependabot will update the hashes as necessary. + +The agent-managed workflows (`tend-*.yaml` and `workflow-audit.yaml`) are exempt from the two rules below because the maintainer agent's job requires modifying issues, PRs, and code. Their scope is bounded separately in the "Automated Maintainer" section. + +- FAIL IF `pull_request_target` appears in any `.github/workflows/**` file other than `tend-*.yaml`. +- FAIL IF a non-agent-managed workflow grants write permissions other than the explicitly scoped release provenance permissions `id-token: write` and `attestations: write`. + +## Automated Maintainer (tend) + +This repository runs the [tend](https://github.com/max-sixty/tend) agent harness as the GitHub user `dormouse-bot`. tend reviews PRs, triages issues, fixes CI failures, regenerates its own workflow files on a nightly schedule, and responds to mentions. The agent expands the project's attack surface. The boundaries we accept are codified below. + +**Prompt-injection through user-supplied content.** tend's harness reads PR descriptions, code diffs, issue text, comments, and CI logs — all attacker-influenceable surfaces. A malicious prompt could direct the harness to push a workflow that references a repo-level secret to an external URL. The bot cannot merge to `main` or push tags, so admin-gated release paths stay sealed, but a workflow on a bot-pushed feature branch will still execute with repo-level secrets in scope. + +**Bot collaborator authority.** `dormouse-bot` is a direct repo collaborator with `push` permission and 2FA enforced by org policy. Its PAT (`TEND_BOT_TOKEN`) carries the scopes `repo`, `workflow`, `notifications`, `write:discussion`, `gist`, and `user`. The `workflow` scope is required for the nightly regeneration of `tend-*.yaml` files; the same scope lets the harness add arbitrary new workflow files. Ref-protection rulesets restrict where bot-controlled commits can land but do not gate workflow execution on feature branches. + +**Reachable repo-level secrets.** `CHROMATIC_PROJECT_TOKEN` is reachable by any workflow the bot can author, because `chromatic.yml` is `pull_request`-triggered and GitHub environment policies cannot distinguish a bot from a human contributor at the ref level. Chromatic project tokens are scoped to a single project, easy to rotate, and any abuse is detectable in Chromatic's own dashboard — this risk is accepted with rotation as the mitigation. `OVSX_PAT` and `VSCE_PAT` are protected: they live only in the `vscode-extension-publish` environment, whose deployment-branch-policy admits only `v*` tags, and tag creation is admin-only. + +**Upstream compromise.** Tend's action is pinned by commit SHA (`max-sixty/tend@`) in every generated workflow, so silent updates to the running setup are not possible. `uvx tend@latest` runs only at install and during nightly regen; a compromise of that path would affect the next re-run, not the in-flight workflows. + +**Audit visibility.** `workflow-audit.yaml` is a nightly job that walks every commit touching `.github/workflows/` since its previous successful run (using the GitHub API's timestamp as the lower bound, so a failed run pushes the window forward rather than dropping commits). It opens an issue summarizing each commit's author, refs, and changed files. A bot push that adds a new workflow file is visible in the next successful audit even if the bot tries to silently modify the audit workflow — the modification itself appears in the audit. + +- FAIL IF the repository ruleset named `Merge access` is missing, doesn't target `~DEFAULT_BRANCH`, blocks anything other than `update`, or doesn't have admin (`RepositoryRole` actor `5`) as its sole bypass actor. +- FAIL IF the repository ruleset named `Tag operations` is missing, doesn't target `~ALL` tags, doesn't block both `creation` and `update`, or doesn't have admin-only bypass. +- FAIL IF `dormouse-bot` holds a permission higher than `push` on this repository. +- FAIL IF `OVSX_PAT` or `VSCE_PAT` appears as a repo-level secret. They must live only in the `vscode-extension-publish` environment. +- FAIL IF the `vscode-extension-publish` environment's deployment-branch-policies allow any ref pattern that is not admin-gated by the `Tag operations` or `Merge access` rulesets. +- FAIL IF `CHROMATIC_PROJECT_TOKEN` is missing from `secrets.allowed` in `.config/tend.yaml`. The allowlist entry is an explicit acknowledgment that the bot can read this token. +- FAIL IF `.github/workflows/workflow-audit.yaml` is missing, disabled, or has not produced a successful run in the last 48 hours. +- FAIL IF any `tend-*.yaml` workflow references `max-sixty/tend` with anything other than a pinned version tag matching a published release (e.g. `@0.0.25`). The other actions inside tend's workflows must still be SHA-pinned per the rule above. The tag-pin exception for `max-sixty/tend` itself is accepted because that reference is owned by the upstream generator. +- FAIL IF any agent-managed workflow (`tend-*.yaml`, `workflow-audit.yaml`) grants a permission beyond `contents: write`, `pull-requests: write`, `issues: write`, `id-token: write`, `actions: read`, or any `read` permission. + +## VS Code Extension Releases + +The VS Code extension is published by GitHub Actions. The secrets which allow this publish are `VSCE_PAT` and `OVSX_PAT`. These secrets are contained only within a protected GitHub environment. The environment requires a human to manually approve, and it can't be the same account which triggered the publish. This prevents a single compromised tag or maintainer account from immediately publishing a new extension version without an explicit release approval. + +- FAIL IF `.github/workflows/release.yml` is missing the `vscode-extension-publish` environment on the VS Code publish job. +- FAIL IF `VSCE_PAT` or `OVSX_PAT` are used anywhere except within the `vscode-extension-publish` environment. +- FAIL IF `.github/workflows/release.yml` uses production desktop signing secrets in CI. +- FAIL IF `.github/workflows/release.yml` stops generating an ephemeral Tauri updater key for unsigned CI artifacts. + +## Desktop Releases + +Desktop releases are not fully automated. GitHub Actions builds unsigned artifacts, publishes attestations and hash manifests, and uploads those unsigned artifacts for local release signing. Final desktop deployment is manual through `scripts/sign-and-deploy.sh`. Before signing, the script verifies the CI artifact attestations and the recorded SHA-256 hashes. The local machine then performs platform signing and uploads the final release assets. Windows Authenticode signing requires a physical YubiKey and the signing PIN. macOS signing and notarization also happen locally, outside GitHub Actions. CI must not have the production Tauri updater private key; CI uses only an ephemeral updater key so Tauri emits updater-shaped unsigned artifacts. Tauri updater signing is applied locally after OS signing so the updater signs the final release bundles that users will download. + +- FAIL IF `scripts/sign-and-deploy.sh` stops verifying GitHub artifact attestations. +- FAIL IF `scripts/sign-and-deploy.sh` stops verifying artifact SHA-256 manifests. +- FAIL IF `scripts/sign-and-deploy.sh` stops using PIV-backed Windows signing. + +## CI Validation Contract + +The `security-audit` workflow at `.github/workflows/security-audit.yaml` enforces this document. It runs nightly and is a required dependency of the VS Code publish job in `release.yml`, so no release ships without a passing audit. The audit reads SECURITY.md, executes each `FAIL IF` as a mechanical check, and also does a qualitative pass for security holes the specs don't cover. On any `FAIL IF` violation or BLOCKER-severity finding, the workflow opens (or updates) an issue labeled `security-audit-failure` with the full audit report, and exits non-zero. When a subsequent audit passes, the open failure issue is auto-closed so the tracker matches the live state. + +- FAIL IF `.github/workflows/security-audit.yaml` is missing, disabled, or no longer invoked from `release.yml`'s publish path. +- FAIL IF the audit has been weakened — e.g. the prompt no longer requires the qualitative pass, a `FAIL IF` can be ignored, or the failure-reporting step that opens a `security-audit-failure` issue and exits non-zero has been removed. \ No newline at end of file diff --git a/website/src/data/dependencies.json b/website/src/data/dependencies.json index c93a72d..25fb60d 100644 --- a/website/src/data/dependencies.json +++ b/website/src/data/dependencies.json @@ -39,7 +39,7 @@ "version": "0.5.0-beta.219", "license": "MIT", "author": "The xterm.js authors", - "homepage": "https://github.com/xtermjs/xterm.js/tree/master#readme" + "homepage": "https://github.com/xtermjs/xterm.js/tree/master/addons/addon-unicode-graphemes" }, { "name": "@xterm/xterm",