diff --git a/.github/workflows/aimock-drift.yml b/.github/workflows/aimock-drift.yml
index adee182b..e94fab56 100644
--- a/.github/workflows/aimock-drift.yml
+++ b/.github/workflows/aimock-drift.yml
@@ -22,8 +22,8 @@ jobs:
drift:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -34,7 +34,7 @@ jobs:
run: npx nx run examples-chat-angular:drift --skip-nx-cache
- name: Open issue on drift
if: failure()
- uses: actions/github-script@v8
+ uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const { owner, repo } = context.repo;
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index abef8b76..d7a428b7 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -33,10 +33,10 @@ jobs:
website_e2e: ${{ steps.scope.outputs.website_e2e }}
posthog: ${{ steps.scope.outputs.posthog }}
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -85,8 +85,8 @@ jobs:
env:
LIBS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -104,10 +104,10 @@ jobs:
permissions:
contents: write
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && github.head_ref || github.sha }}
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -137,8 +137,8 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -152,10 +152,10 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_examples == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -183,8 +183,8 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_smoke == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -208,9 +208,9 @@ jobs:
fi
env:
COCKPIT_SECRET_TOKEN: ${{ secrets.COCKPIT_SECRET_TOKEN }}
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
if: steps.integration_secret.outputs.enabled == 'true'
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
if: steps.integration_secret.outputs.enabled == 'true'
with:
node-version: 22
@@ -228,8 +228,8 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.cockpit_deploy_smoke == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -242,18 +242,18 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.examples_chat == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache examples-chat python venv
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: examples/chat/python/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/chat/python/uv.lock') }}
@@ -272,25 +272,25 @@ jobs:
matrix:
shard: [1, 2, 3, 4]
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache examples-chat python venv
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: examples/chat/python/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/chat/python/uv.lock') }}
- working-directory: examples/chat/python
run: uv sync
- name: Cache Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -300,7 +300,7 @@ jobs:
- run: npx nx e2e examples-chat-angular --skip-nx-cache -- --shard=${{ matrix.shard }}/4
- name: Upload Playwright trace on failure
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: examples-chat-e2e-trace-shard-${{ matrix.shard }}
path: |
@@ -332,25 +332,25 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 35
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache examples-ag-ui python venv
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: examples/ag-ui/python/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ hashFiles('examples/ag-ui/python/uv.lock') }}
- working-directory: examples/ag-ui/python
run: uv sync
- name: Cache Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -360,7 +360,7 @@ jobs:
- run: npx nx e2e examples-ag-ui-angular --skip-nx-cache
- name: Upload Playwright trace on failure
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: examples-ag-ui-e2e-trace
path: |
@@ -376,10 +376,10 @@ jobs:
outputs:
caps: ${{ steps.matrix.outputs.caps }}
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -415,18 +415,18 @@ jobs:
matrix:
cap: ${{ fromJson(needs.cockpit-e2e-dispatcher.outputs.caps) }}
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- name: Install uv
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- run: npm ci
- name: Cache cap python venv
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ${{ matrix.cap.python }}/.venv
key: uv-venv-${{ runner.os }}-py3.12-${{ matrix.cap.python }}-${{ hashFiles(format('{0}/uv.lock', matrix.cap.python)) }}
@@ -434,7 +434,7 @@ jobs:
working-directory: ${{ matrix.cap.python }}
run: uv sync
- name: Cache Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -445,7 +445,7 @@ jobs:
run: npx nx e2e "${{ matrix.cap.angular }}" --skip-nx-cache
- name: Upload Playwright trace on failure
if: failure()
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: cockpit-e2e-trace-${{ matrix.cap.angular }}
path: |
@@ -472,14 +472,14 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.website_e2e == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- name: Cache Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -608,7 +608,7 @@ jobs:
# Only deploy on pushes to main, not on pull requests
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Detect deploy-relevant changes
@@ -658,7 +658,7 @@ jobs:
examples_changed=true
fi
echo "changed=$examples_changed" >> "$GITHUB_OUTPUT"
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
if: steps.deploy_preflight.outputs.relevant == 'true' || steps.examples_changed.outputs.changed == 'true'
with:
node-version: 22
@@ -711,7 +711,7 @@ jobs:
echo "cockpit=$cockpit_changed" >> "$GITHUB_OUTPUT"
- name: Cache Playwright browsers
if: steps.affected.outputs.website == 'true'
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -792,8 +792,8 @@ jobs:
echo "::error::examples/chat — e2e finished with ${{ needs.examples-chat-e2e.result }}; refusing to deploy the canonical demo."
exit 1
fi
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -858,7 +858,7 @@ jobs:
runs-on: ubuntu-latest
if: ${{ always() && !cancelled() && github.ref == 'refs/heads/main' && github.event_name == 'push' }}
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
- name: Require ag-ui demo prerequisite jobs
@@ -887,14 +887,14 @@ jobs:
if [ "$ag_ui_changed" != "true" ]; then
echo "::notice::No ag-ui demo files changed; skipping ag-ui demo deploy."
fi
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
if: steps.ag_ui_changed.outputs.changed == 'true'
with:
node-version: 22
cache: npm
- name: Install uv
if: steps.ag_ui_changed.outputs.changed == 'true'
- uses: astral-sh/setup-uv@v8.0.0
+ uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
with:
python-version: '3.12'
- if: steps.ag_ui_changed.outputs.changed == 'true'
@@ -932,8 +932,8 @@ jobs:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
@@ -943,7 +943,7 @@ jobs:
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
- name: Cache Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -964,7 +964,7 @@ jobs:
if: github.event_name == 'push' || needs.ci-scope.outputs.posthog == 'true'
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- name: Detect PostHog-relevant changes
@@ -990,7 +990,7 @@ jobs:
if [ "$posthog_relevant" != "true" ]; then
echo "::notice::No PostHog tooling files changed — skipping dependency setup and drift check."
fi
- - uses: actions/setup-node@v4
+ - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
if: steps.posthog_preflight.outputs.relevant == 'true'
with:
node-version: '20'
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 00000000..6974f258
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,38 @@
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '27 4 * * 1'
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ actions: read
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['javascript-typescript', 'python']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
+ with:
+ languages: ${{ matrix.language }}
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
+ with:
+ category: '/language:${{ matrix.language }}'
diff --git a/.github/workflows/deploy-ag-ui.yml b/.github/workflows/deploy-ag-ui.yml
index 5b5ea20e..bf2347cb 100644
--- a/.github/workflows/deploy-ag-ui.yml
+++ b/.github/workflows/deploy-ag-ui.yml
@@ -25,9 +25,9 @@ jobs:
name: Deploy ag-ui-dev to Railway
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
diff --git a/.github/workflows/deploy-langgraph.yml b/.github/workflows/deploy-langgraph.yml
index 1b0d8d94..5ead692e 100644
--- a/.github/workflows/deploy-langgraph.yml
+++ b/.github/workflows/deploy-langgraph.yml
@@ -32,16 +32,16 @@ jobs:
name: Deploy shared cockpit-dev to LangGraph Cloud
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
- run: npm ci
- - uses: actions/setup-python@v5
+ - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
with:
python-version: '3.12'
@@ -49,7 +49,7 @@ jobs:
run: pip install uv
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v3
+ uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
- name: Generate shared deployment manifest
run: npx tsx scripts/generate-shared-deployment-config.ts
diff --git a/.github/workflows/posthog-quality.yml b/.github/workflows/posthog-quality.yml
index 3effa5c7..ae6b7459 100644
--- a/.github/workflows/posthog-quality.yml
+++ b/.github/workflows/posthog-quality.yml
@@ -29,8 +29,8 @@ jobs:
name: Live telemetry contract and coverage
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v6.0.2
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 22
cache: npm
diff --git a/.github/workflows/publish-middleware-npm.yml b/.github/workflows/publish-middleware-npm.yml
index 57b491c4..bbdc1a06 100644
--- a/.github/workflows/publish-middleware-npm.yml
+++ b/.github/workflows/publish-middleware-npm.yml
@@ -48,6 +48,9 @@ concurrency:
env:
DO_NOT_TRACK: '1'
+permissions:
+ contents: read
+
jobs:
build-and-publish:
name: Build and publish @threadplane/middleware
@@ -59,12 +62,12 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v6.0.2
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Node 24 ships npm 11+, which fully implements npm trusted publishing
# over OIDC. (The rest of CI runs on Node 22; this job needs Node 24.)
- name: Setup Node
- uses: actions/setup-node@v6.3.0
+ uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
cache: npm
diff --git a/.github/workflows/publish-middleware-python.yml b/.github/workflows/publish-middleware-python.yml
index 624235e7..01731870 100644
--- a/.github/workflows/publish-middleware-python.yml
+++ b/.github/workflows/publish-middleware-python.yml
@@ -39,6 +39,9 @@ concurrency:
group: publish-middleware-python
cancel-in-progress: false
+permissions:
+ contents: read
+
jobs:
build-and-publish:
name: Build and publish threadplane-middleware
@@ -50,10 +53,10 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v4
+ uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Install uv
- uses: astral-sh/setup-uv@v5
+ uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
with:
version: "latest"
@@ -75,5 +78,7 @@ jobs:
- name: Publish to PyPI (real release — OIDC trusted publishing)
if: ${{ inputs.dry_run == false }}
- working-directory: packages/threadplane-middleware
- run: uv publish dist/*
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # release/v1
+ with:
+ packages-dir: packages/threadplane-middleware/dist
+ attestations: true
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cd50db8a..ffbeac5e 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -18,6 +18,9 @@ concurrency:
env:
DO_NOT_TRACK: '1'
+permissions:
+ contents: read
+
jobs:
publish:
runs-on: ubuntu-latest
@@ -27,13 +30,13 @@ jobs:
env:
NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
steps:
- - uses: actions/checkout@v6.0.2
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
# Node 24 ships npm 11+ which fully implements npm trusted publishing
# over OIDC. Node 22 is LTS but locked at npm 10.x, which has only
# partial trusted-publishing support and fails OIDC on this registry.
# The rest of CI (lint/test/build) runs on Node 22; this workflow
# uses Node 24 specifically for the publish step.
- - uses: actions/setup-node@v6.3.0
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: 24
cache: npm
diff --git a/.github/workflows/release-provenance.yml b/.github/workflows/release-provenance.yml
new file mode 100644
index 00000000..07189a7e
--- /dev/null
+++ b/.github/workflows/release-provenance.yml
@@ -0,0 +1,55 @@
+name: Release provenance (SLSA)
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: read
+
+jobs:
+ build-artifacts:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write # gh release upload attaches tarballs to the release
+ outputs:
+ hashes: ${{ steps.hash.outputs.hashes }}
+ env:
+ NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
+ steps:
+ - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ with:
+ node-version: 24
+ cache: npm
+ - run: npm ci
+ - name: Build publishable projects
+ env:
+ CACHEPLANE_LICENSE_PUBLIC_KEY: ${{ secrets.CACHEPLANE_LICENSE_PUBLIC_KEY }}
+ run: npx nx run-many -t build --projects="$NPM_PUBLISHABLE_PROJECTS" --skip-nx-cache
+ - name: Pack tarballs
+ run: |
+ mkdir -p release-artifacts
+ for p in chat langgraph ag-ui render a2ui licensing telemetry; do
+ npm pack "dist/libs/$p" --pack-destination release-artifacts
+ done
+ - name: Generate subject hashes
+ id: hash
+ run: |
+ cd release-artifacts
+ echo "hashes=$(sha256sum ./*.tgz | base64 -w0)" >> "$GITHUB_OUTPUT"
+ - name: Upload tarballs to the release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: gh release upload "${{ github.event.release.tag_name }}" -- release-artifacts/*.tgz --clobber
+
+ provenance:
+ needs: [build-artifacts]
+ permissions:
+ actions: read
+ id-token: write
+ contents: write
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
+ with:
+ base64-subjects: ${{ needs.build-artifacts.outputs.hashes }}
+ upload-assets: true
diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml
new file mode 100644
index 00000000..78daa0fa
--- /dev/null
+++ b/.github/workflows/scorecard.yml
@@ -0,0 +1,41 @@
+name: Scorecard supply-chain security
+
+on:
+ branch_protection_rule:
+ schedule:
+ - cron: '18 5 * * 2'
+ push:
+ branches: [main]
+
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ id-token: write
+ contents: read
+ actions: read
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ with:
+ persist-credentials: false
+ - name: Run analysis
+ uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ publish_results: true
+ - name: Upload artifact
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
+ with:
+ name: SARIF file
+ path: results.sarif
+ retention-days: 5
+ - name: Upload to code-scanning
+ uses: github/codeql-action/upload-sarif@dd903d2e4f5405488e5ef1422510ee31c8b32357 # v3.36.2
+ with:
+ sarif_file: results.sarif
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
new file mode 100644
index 00000000..17e0f61a
--- /dev/null
+++ b/CONTRIBUTING.md
@@ -0,0 +1,16 @@
+# Contributing
+
+## Signed commits
+
+`main` requires signed commits. Configure SSH commit signing once:
+
+```bash
+git config --global gpg.format ssh
+git config --global user.signingkey ~/.ssh/id_ed25519.pub
+git config --global commit.gpgsign true
+git config --global tag.gpgsign true
+```
+
+Then add the same public key as a **Signing Key** at
+. Commits merged through the GitHub UI and
+bot commits (Renovate, Dependabot) are signed automatically.
diff --git a/README.md b/README.md
index ef3f12f1..7122b059 100644
--- a/README.md
+++ b/README.md
@@ -20,6 +20,9 @@
+
+
+
---
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 00000000..3ebda5e3
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Supported Versions
+
+threadplane publishes patch-level releases of the `@threadplane/*` packages.
+Only the latest published version of each package is supported with security
+fixes.
+
+| Package scope | Supported |
+| ------------- | --------- |
+| `@threadplane/*` (latest) | ✅ |
+| older versions | ❌ |
+
+## Reporting a Vulnerability
+
+Please report security issues privately via GitHub's
+[private vulnerability reporting](https://github.com/cacheplane/angular-agent-framework/security/advisories/new).
+
+We aim to acknowledge reports within 5 business days and to provide a
+remediation timeline after triage. Please do not open public issues for
+security vulnerabilities.
diff --git a/docs/superpowers/plans/2026-06-18-hvtrust-supply-chain-hardening.md b/docs/superpowers/plans/2026-06-18-hvtrust-supply-chain-hardening.md
new file mode 100644
index 00000000..2ce71de5
--- /dev/null
+++ b/docs/superpowers/plans/2026-06-18-hvtrust-supply-chain-hardening.md
@@ -0,0 +1,727 @@
+# HVTrust Supply-Chain Hardening Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Raise threadplane's HVTracker "HVTrust" score by closing supply-chain evidence gaps with genuine hardening — OSSF Scorecard publishing, security/SAST/dependency config, pinned Actions, enforced commit signing, and SLSA-attested releases.
+
+**Architecture:** Pure repository + CI configuration. No application code changes. Work is sequenced into four PR-sized groups; group 1 alone makes the OSSF Scorecard measurable. "Tests" are verification commands: YAML validity (`actionlint`), `gh api` assertions on repo/branch settings, and `scorecard` score deltas.
+
+**Tech Stack:** GitHub Actions, OSSF Scorecard, CodeQL, Renovate, `pinact` (Action SHA pinning), SSH commit/tag signing, `slsa-github-generator`, npm OIDC trusted publishing (already in place), PyPI trusted publishing.
+
+**Spec:** [docs/superpowers/specs/2026-06-18-hvtrust-supply-chain-hardening-design.md](../specs/2026-06-18-hvtrust-supply-chain-hardening-design.md)
+
+**Manual owner steps (not automatable, called out where relevant):** install the Renovate GitHub App (Task 7); configure local SSH signing key on the maintainer machine (Task 9); optionally create a `SCORECARD_TOKEN` secret (noted in Task 5).
+
+---
+
+## File Structure
+
+| File | Responsibility | Task |
+|---|---|---|
+| `.github/workflows/scorecard.yml` | Run + publish OSSF Scorecard | 5 |
+| `.github/workflows/codeql.yml` | CodeQL SAST for JS/TS + Python | 4 |
+| `SECURITY.md` | Vulnerability disclosure policy | 2 |
+| `.github/workflows/publish.yml` | + top-level least-privilege perms | 3 |
+| `.github/workflows/publish-middleware-npm.yml` | + top-level least-privilege perms | 3 |
+| `.github/workflows/publish-middleware-python.yml` | + top-level perms; + PyPI attestations | 3, 13 |
+| `renovate.json` | Dependency updates + Action SHA pinning | 7 |
+| all `.github/workflows/*.yml` | Actions pinned by commit SHA | 8 |
+| `CONTRIBUTING.md` | Document SSH commit/tag signing setup | 9 |
+| `README.md` | OSSF Scorecard badge | 5 |
+| `.github/workflows/release-provenance.yml` | SLSA provenance for Release artifacts | 12 |
+
+---
+
+## Preconditions
+
+- [ ] **Confirm tooling is available** (install on demand in later tasks; just verify access now):
+
+Run:
+```bash
+gh auth status
+command -v docker || echo "docker missing (needed for scorecard local run)"
+npx --yes actionlint --version 2>/dev/null || echo "actionlint via npx ok"
+```
+Expected: `gh` authenticated; note whether docker exists (Task 1 needs it or falls back to reading the first published run).
+
+---
+
+## Group 1 — Make Scorecard measurable (hardening + publishing)
+
+### Task 1: Capture OSSF Scorecard baseline
+
+**Files:**
+- Create: `docs/superpowers/audits/2026-06-18-scorecard-baseline.md`
+
+- [ ] **Step 1: Run Scorecard against the repo**
+
+Run (requires a token with public-repo read; classic PAT or `gh auth token`):
+```bash
+export GITHUB_AUTH_TOKEN=$(gh auth token)
+docker run -e GITHUB_AUTH_TOKEN \
+ gcr.io/openssf/scorecard:stable \
+ --repo=github.com/cacheplane/angular-agent-framework --format=json > /tmp/scorecard-baseline.json
+```
+If docker is unavailable, skip the run and record "baseline deferred to first published run" instead.
+
+- [ ] **Step 2: Record per-check scores**
+
+Run:
+```bash
+python3 -c "import json;d=json.load(open('/tmp/scorecard-baseline.json'));print('AGGREGATE',d['score']);[print(c['name'],c['score']) for c in d['checks']]"
+```
+
+- [ ] **Step 3: Write the baseline audit file**
+
+Create `docs/superpowers/audits/2026-06-18-scorecard-baseline.md` containing the aggregate score and the per-check table from Step 2 (paste the actual numbers). This is the before-state we measure deltas against in Task 14.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add docs/superpowers/audits/2026-06-18-scorecard-baseline.md
+git commit -m "docs(audit): OSSF Scorecard baseline before hardening"
+```
+
+### Task 2: SECURITY.md + private vulnerability reporting
+
+**Files:**
+- Create: `SECURITY.md`
+
+- [ ] **Step 1: Create `SECURITY.md`**
+
+```markdown
+# Security Policy
+
+## Supported Versions
+
+threadplane publishes patch-level releases of the `@threadplane/*` packages.
+Only the latest published version of each package is supported with security
+fixes.
+
+| Package scope | Supported |
+| ------------- | --------- |
+| `@threadplane/*` (latest) | ✅ |
+| older versions | ❌ |
+
+## Reporting a Vulnerability
+
+Please report security issues privately via GitHub's
+[private vulnerability reporting](https://github.com/cacheplane/angular-agent-framework/security/advisories/new).
+
+We aim to acknowledge reports within 5 business days and to provide a
+remediation timeline after triage. Please do not open public issues for
+security vulnerabilities.
+```
+
+- [ ] **Step 2: Enable private vulnerability reporting (repo setting)**
+
+Run:
+```bash
+gh api -X PUT repos/cacheplane/angular-agent-framework/private-vulnerability-reporting
+```
+Expected: HTTP 204 (no output) or a JSON confirming enabled.
+
+- [ ] **Step 3: Verify the file is valid markdown and the setting took**
+
+Run:
+```bash
+test -f SECURITY.md && echo "SECURITY.md present"
+gh api repos/cacheplane/angular-agent-framework/private-vulnerability-reporting --jq '.enabled'
+```
+Expected: `SECURITY.md present` and `true`.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add SECURITY.md
+git commit -m "docs(security): add SECURITY.md vulnerability disclosure policy"
+```
+
+### Task 3: Top-level least-privilege permissions on publish workflows
+
+**Files:**
+- Modify: `.github/workflows/publish.yml` (add top-level `permissions:` after the `env:` block, before `jobs:`)
+- Modify: `.github/workflows/publish-middleware-npm.yml`
+- Modify: `.github/workflows/publish-middleware-python.yml`
+
+- [ ] **Step 1: Add top-level `permissions` to `publish.yml`**
+
+Insert immediately before the `jobs:` line:
+```yaml
+permissions:
+ contents: read
+
+jobs:
+```
+The existing job-level block (`contents: read` + `id-token: write`) stays as-is — job-level elevation is correct and is what Token-Permissions rewards.
+
+- [ ] **Step 2: Add the same top-level block to `publish-middleware-npm.yml` and `publish-middleware-python.yml`**
+
+Insert before each file's `jobs:` line:
+```yaml
+permissions:
+ contents: read
+
+jobs:
+```
+
+- [ ] **Step 3: Validate all three workflows parse**
+
+Run:
+```bash
+npx --yes actionlint .github/workflows/publish.yml .github/workflows/publish-middleware-npm.yml .github/workflows/publish-middleware-python.yml
+```
+Expected: no output (exit 0). Note: `actionlint` may warn on shellcheck items unrelated to this change; only permission/syntax errors are blockers.
+
+- [ ] **Step 4: Confirm each file now has a top-level `permissions:`**
+
+Run:
+```bash
+for f in publish publish-middleware-npm publish-middleware-python; do
+ awk '/^permissions:/{found=1} /^jobs:/{print FILENAME, (found?"OK":"MISSING"); exit}' .github/workflows/$f.yml
+done
+```
+Expected: three `OK` lines.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add .github/workflows/publish.yml .github/workflows/publish-middleware-npm.yml .github/workflows/publish-middleware-python.yml
+git commit -m "ci: set least-privilege top-level token permissions on publish workflows"
+```
+
+### Task 4: CodeQL SAST workflow
+
+**Files:**
+- Create: `.github/workflows/codeql.yml`
+
+- [ ] **Step 1: Create `.github/workflows/codeql.yml`**
+
+```yaml
+name: CodeQL
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+ schedule:
+ - cron: '27 4 * * 1'
+
+permissions:
+ contents: read
+
+jobs:
+ analyze:
+ name: Analyze (${{ matrix.language }})
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ actions: read
+ contents: read
+ strategy:
+ fail-fast: false
+ matrix:
+ language: ['javascript-typescript', 'python']
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v6.0.2
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ - name: Autobuild
+ uses: github/codeql-action/autobuild@v3
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: '/language:${{ matrix.language }}'
+```
+(Actions left at tags here; Task 8 pins every Action by SHA across all workflows.)
+
+- [ ] **Step 2: Validate the workflow parses**
+
+Run:
+```bash
+npx --yes actionlint .github/workflows/codeql.yml
+```
+Expected: no output (exit 0).
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add .github/workflows/codeql.yml
+git commit -m "ci: add CodeQL SAST for javascript-typescript and python"
+```
+
+### Task 5: OSSF Scorecard workflow + README badge
+
+**Files:**
+- Create: `.github/workflows/scorecard.yml`
+- Modify: `README.md` (add badge near the top badge row)
+
+- [ ] **Step 1: Create `.github/workflows/scorecard.yml`**
+
+```yaml
+name: Scorecard supply-chain security
+
+on:
+ branch_protection_rule:
+ schedule:
+ - cron: '18 5 * * 2'
+ push:
+ branches: [main]
+
+permissions: read-all
+
+jobs:
+ analysis:
+ name: Scorecard analysis
+ runs-on: ubuntu-latest
+ permissions:
+ security-events: write
+ id-token: write
+ contents: read
+ actions: read
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6.0.2
+ with:
+ persist-credentials: false
+ - name: Run analysis
+ uses: ossf/scorecard-action@v2.4.0
+ with:
+ results_file: results.sarif
+ results_format: sarif
+ publish_results: true
+ - name: Upload artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: SARIF file
+ path: results.sarif
+ retention-days: 5
+ - name: Upload to code-scanning
+ uses: github/codeql-action/upload-sarif@v3
+ with:
+ sarif_file: results.sarif
+```
+**Optional follow-up (note in PR description, do not block):** to unlock the Branch-Protection check, create a fine-grained read PAT secret `SCORECARD_TOKEN` and add `repo_token: ${{ secrets.SCORECARD_TOKEN }}` to the "Run analysis" step. The default `GITHUB_TOKEN` cannot read branch protection.
+
+- [ ] **Step 2: Add the badge to `README.md`**
+
+Find the existing badge row near the top of `README.md` (the line(s) containing other shield badges). Add, on its own line in that row:
+```markdown
+[](https://scorecard.dev/viewer/?uri=github.com/cacheplane/angular-agent-framework)
+```
+If no badge row exists, add this line immediately under the top-level `# ` heading.
+
+- [ ] **Step 3: Validate the workflow parses**
+
+Run:
+```bash
+npx --yes actionlint .github/workflows/scorecard.yml
+```
+Expected: no output (exit 0).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add .github/workflows/scorecard.yml README.md
+git commit -m "ci: add OSSF Scorecard workflow with public results + README badge"
+```
+
+### Task 6: Repo posture settings
+
+**Files:** none (GitHub repo settings via API)
+
+- [ ] **Step 1: Enable secret scanning + push protection + Dependabot security updates**
+
+Run:
+```bash
+gh api -X PATCH repos/cacheplane/angular-agent-framework \
+ -f 'security_and_analysis[secret_scanning][status]=enabled' \
+ -f 'security_and_analysis[secret_scanning_push_protection][status]=enabled' \
+ -f 'security_and_analysis[dependabot_security_updates][status]=enabled'
+```
+Expected: JSON response; no error.
+
+- [ ] **Step 2: Verify all three are enabled**
+
+Run:
+```bash
+gh api repos/cacheplane/angular-agent-framework --jq '.security_and_analysis | {secret_scanning:.secret_scanning.status, push_protection:.secret_scanning_push_protection.status, dependabot:.dependabot_security_updates.status}'
+```
+Expected: all three `enabled`.
+
+- [ ] **Step 3: Record the change**
+
+No file to commit. Note in the PR description that repo posture settings were enabled (these are account-side, not in git).
+
+---
+
+## Group 2 — Renovate + Action SHA pinning
+
+### Task 7: Renovate configuration
+
+**Files:**
+- Create: `renovate.json`
+
+- [ ] **Step 1: Create `renovate.json`**
+
+```json
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended",
+ "helpers:pinGitHubActionDigests",
+ ":dependencyDashboard",
+ ":semanticCommits"
+ ],
+ "schedule": ["before 9am on monday"],
+ "prConcurrentLimit": 5,
+ "packageRules": [
+ {
+ "matchManagers": ["github-actions"],
+ "groupName": "github-actions",
+ "pinDigests": true
+ },
+ {
+ "matchManagers": ["npm"],
+ "matchUpdateTypes": ["minor", "patch"],
+ "groupName": "npm minor+patch"
+ }
+ ]
+}
+```
+
+- [ ] **Step 2: Validate the config**
+
+Run:
+```bash
+npx --yes --package renovate -- renovate-config-validator renovate.json
+```
+Expected: `Validating renovate.json` ... `Config validated successfully`.
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add renovate.json
+git commit -m "ci: add Renovate config with GitHub Action digest pinning"
+```
+
+- [ ] **Step 4: MANUAL OWNER STEP — install the Renovate GitHub App**
+
+Install the Renovate app on `cacheplane/angular-agent-framework` at https://github.com/apps/renovate (or confirm the org already has it). This activates the config. Document completion in the PR; nothing to commit.
+
+### Task 8: Pin all GitHub Actions by commit SHA
+
+**Files:**
+- Modify: every `.github/workflows/*.yml`
+
+- [ ] **Step 1: Pin all Actions by SHA with `pinact`**
+
+Run:
+```bash
+go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest 2>/dev/null || \
+ brew install pinact
+pinact run
+```
+If neither `go` nor `brew` is available, use the Docker image:
+```bash
+docker run --rm -v "$PWD":/work -w /work -e GITHUB_TOKEN=$(gh auth token) \
+ ghcr.io/suzuki-shunsuke/pinact:latest run
+```
+This rewrites every `uses: org/action@vX` to `uses: org/action@<40-char-sha> # vX` in place.
+
+- [ ] **Step 2: Verify no Action remains unpinned**
+
+Run:
+```bash
+grep -rhE "uses: " .github/workflows/ | grep -vE "@[0-9a-f]{40}" | grep -E "@v?[0-9]" || echo "ALL PINNED"
+```
+Expected: `ALL PINNED`.
+
+- [ ] **Step 3: Validate all workflows still parse**
+
+Run:
+```bash
+npx --yes actionlint .github/workflows/*.yml
+```
+Expected: exit 0 (pre-existing shellcheck warnings, if any, are not blockers).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add .github/workflows
+git commit -m "ci: pin all GitHub Actions to commit SHAs"
+```
+
+---
+
+## Group 3 — Commit signing + enforcement
+
+### Task 9: SSH commit/tag signing setup + CONTRIBUTING docs
+
+**Files:**
+- Create or Modify: `CONTRIBUTING.md` (add a "Signed commits" section; create the file if absent)
+
+- [ ] **Step 1: MANUAL OWNER STEP — configure local SSH signing**
+
+On the maintainer machine:
+```bash
+git config --global gpg.format ssh
+git config --global user.signingkey ~/.ssh/id_ed25519.pub # existing key
+git config --global commit.gpgsign true
+git config --global tag.gpgsign true
+```
+Then add the SAME public key as a **Signing Key** (not just Authentication Key) at
+https://github.com/settings/ssh/new with key type "Signing Key".
+
+- [ ] **Step 2: Verify a signed commit is produced**
+
+Run (in this repo, on a throwaway file):
+```bash
+git commit --allow-empty -m "chore: verify signing" -S
+git log -1 --show-signature 2>&1 | grep -i "good \"git\" signature\|Good signature" && echo "SIGNED OK"
+git reset --soft HEAD~1 # undo the throwaway commit
+```
+Expected: `SIGNED OK`.
+
+- [ ] **Step 3: Document in `CONTRIBUTING.md`**
+
+Add (create the file with a `# Contributing` heading if it doesn't exist) a section:
+```markdown
+## Signed commits
+
+`main` requires signed commits. Configure SSH commit signing once:
+
+\`\`\`bash
+git config --global gpg.format ssh
+git config --global user.signingkey ~/.ssh/id_ed25519.pub
+git config --global commit.gpgsign true
+git config --global tag.gpgsign true
+\`\`\`
+
+Then add the same public key as a **Signing Key** at
+. Commits merged through the GitHub UI and
+bot commits (Renovate, Dependabot) are signed automatically.
+```
+
+- [ ] **Step 4: Commit (signed)**
+
+```bash
+git add CONTRIBUTING.md
+git commit -m "docs(contributing): document required SSH commit signing"
+```
+
+### Task 10: Enforce required signatures on `main`
+
+**Files:** none (branch protection via API)
+
+- [ ] **Step 1: Enable required signatures on `main`**
+
+Run:
+```bash
+gh api -X POST repos/cacheplane/angular-agent-framework/branches/main/protection/required_signatures \
+ -H "Accept: application/vnd.github+json"
+```
+Expected: JSON with `"enabled": true`.
+
+- [ ] **Step 2: Verify enforcement is active**
+
+Run:
+```bash
+gh api repos/cacheplane/angular-agent-framework/branches/main/protection/required_signatures --jq '.enabled'
+```
+Expected: `true`.
+
+- [ ] **Step 3: Record**
+
+No file to commit. Note in PR description that `main` now requires signed commits.
+
+---
+
+## Group 4 — SLSA release artifacts
+
+### Task 11: Confirm signed tags flow through `nx release`
+
+**Files:** none (relies on Task 9 `tag.gpgsign=true`)
+
+- [ ] **Step 1: Verify a dry-run tag would be signed**
+
+Run:
+```bash
+git config --get tag.gpgsign
+git tag -s _sigcheck -m "sig check" && git tag -v _sigcheck 2>&1 | grep -i "good signature" && echo "TAG SIGNING OK"
+git tag -d _sigcheck
+```
+Expected: `true`, then `TAG SIGNING OK`.
+
+- [ ] **Step 2: No commit** (configuration is local/global from Task 9). Note completion in PR description.
+
+### Task 12: SLSA provenance for GitHub Release artifacts
+
+**Files:**
+- Create: `.github/workflows/release-provenance.yml`
+
+- [ ] **Step 1: Create `.github/workflows/release-provenance.yml`**
+
+```yaml
+name: Release provenance (SLSA)
+
+on:
+ release:
+ types: [published]
+
+permissions:
+ contents: read
+
+jobs:
+ build-artifacts:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ outputs:
+ hashes: ${{ steps.hash.outputs.hashes }}
+ env:
+ NPM_PUBLISHABLE_PROJECTS: chat,langgraph,ag-ui,render,a2ui,licensing,telemetry
+ steps:
+ - uses: actions/checkout@v6.0.2
+ - uses: actions/setup-node@v6.3.0
+ with:
+ node-version: 24
+ cache: npm
+ - run: npm ci
+ - name: Build publishable projects
+ env:
+ CACHEPLANE_LICENSE_PUBLIC_KEY: ${{ secrets.CACHEPLANE_LICENSE_PUBLIC_KEY }}
+ run: npx nx run-many -t build --projects=$NPM_PUBLISHABLE_PROJECTS --skip-nx-cache
+ - name: Pack tarballs
+ run: |
+ mkdir -p release-artifacts
+ for p in chat langgraph ag-ui render a2ui licensing telemetry; do
+ npm pack "dist/libs/$p" --pack-destination release-artifacts
+ done
+ - name: Generate subject hashes
+ id: hash
+ run: |
+ cd release-artifacts
+ echo "hashes=$(sha256sum *.tgz | base64 -w0)" >> "$GITHUB_OUTPUT"
+ - name: Upload tarballs to the release
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: gh release upload "${{ github.event.release.tag_name }}" release-artifacts/*.tgz --clobber
+
+ provenance:
+ needs: [build-artifacts]
+ permissions:
+ actions: read
+ id-token: write
+ contents: write
+ uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.0.0
+ with:
+ base64-subjects: ${{ needs.build-artifacts.outputs.hashes }}
+ upload-assets: true
+```
+This produces a signed `*.intoto.jsonl` provenance attached to the GitHub Release — what
+Scorecard's Signed-Releases check inspects. (Actions pinned by SHA in the next Renovate
+run / Task 8 re-run; the reusable SLSA workflow is referenced by its release tag as required
+by the generator.)
+
+- [ ] **Step 2: Validate the workflow parses**
+
+Run:
+```bash
+npx --yes actionlint .github/workflows/release-provenance.yml
+```
+Expected: exit 0. (`actionlint` may not resolve the reusable-workflow outputs of the SLSA call — a `workflow_call` warning there is acceptable.)
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add .github/workflows/release-provenance.yml
+git commit -m "ci: generate SLSA provenance for GitHub Release artifacts"
+```
+
+### Task 13: Enable PyPI attestations
+
+**Files:**
+- Modify: `.github/workflows/publish-middleware-python.yml`
+
+- [ ] **Step 1: Inspect the current publish step**
+
+Run:
+```bash
+grep -nE "pypa/gh-action-pypi-publish|twine|python -m build|attestations" .github/workflows/publish-middleware-python.yml
+```
+Determine whether it uses `pypa/gh-action-pypi-publish` (supports `attestations: true`) or raw `twine`.
+
+- [ ] **Step 2a: If it uses `pypa/gh-action-pypi-publish`** — add `attestations: true` under its `with:`:
+
+```yaml
+ - name: Publish to PyPI (real release — OIDC trusted publishing)
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ attestations: true
+```
+
+- [ ] **Step 2b: If it uses raw `twine`** — switch the publish step to `pypa/gh-action-pypi-publish@release/v1` with `attestations: true` (it defaults to OIDC trusted publishing, matching the existing `id-token: write`). Keep any existing `packages-dir` pointing at the built `dist/`.
+
+- [ ] **Step 3: Validate**
+
+Run:
+```bash
+npx --yes actionlint .github/workflows/publish-middleware-python.yml
+```
+Expected: exit 0.
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add .github/workflows/publish-middleware-python.yml
+git commit -m "ci: enable PyPI build provenance attestations on middleware publish"
+```
+
+### Task 14: Final verification — Scorecard delta
+
+**Files:**
+- Modify: `docs/superpowers/audits/2026-06-18-scorecard-baseline.md` (append an "after" section)
+
+- [ ] **Step 1: Re-run Scorecard** (after Group 1–2 are merged to `main`; the published run is authoritative)
+
+Run:
+```bash
+export GITHUB_AUTH_TOKEN=$(gh auth token)
+docker run -e GITHUB_AUTH_TOKEN gcr.io/openssf/scorecard:stable \
+ --repo=github.com/cacheplane/angular-agent-framework --format=json > /tmp/scorecard-after.json
+python3 -c "import json;d=json.load(open('/tmp/scorecard-after.json'));print('AGGREGATE',d['score']);[print(c['name'],c['score']) for c in d['checks']]"
+```
+
+- [ ] **Step 2: Append the delta to the audit file**
+
+Add an "After hardening" table next to the baseline, and confirm these checks improved:
+`Security-Policy`, `SAST`, `Token-Permissions`, `Pinned-Dependencies`, `Dependency-Update-Tool`,
+`Signed-Releases`. Note any check still at 0 with a one-line reason.
+
+- [ ] **Step 3: Confirm the badge renders**
+
+Run:
+```bash
+curl -sI "https://api.scorecard.dev/projects/github.com/cacheplane/angular-agent-framework/badge" | head -1
+```
+Expected: `HTTP/2 200` (may take up to one scheduled run after first publish to populate).
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add docs/superpowers/audits/2026-06-18-scorecard-baseline.md
+git commit -m "docs(audit): record OSSF Scorecard delta after hardening"
+```
+
+---
+
+## Self-Review notes
+
+- **Spec coverage:** Scorecard workflow (T5), SECURITY.md + private reporting (T2), CodeQL (T4), Renovate + SHA pinning (T7, T8), token permissions (T3), repo settings (T6), commit signing + enforcement (T9, T10), signed tags + SLSA artifacts (T11, T12), PyPI attestations (T13), verification (T1, T14). All spec sections mapped.
+- **Out of scope (per spec), intentionally absent:** HVTracker registration, Adoption optimization, Fuzzing, CII-Best-Practices.
+- **Sequencing guard:** Task 8 pins Actions in workflows created by Tasks 4, 5, 12. Task 14 must run only after Groups 1–2 land on `main`.
diff --git a/docs/superpowers/specs/2026-06-18-hvtrust-supply-chain-hardening-design.md b/docs/superpowers/specs/2026-06-18-hvtrust-supply-chain-hardening-design.md
new file mode 100644
index 00000000..e81e4daf
--- /dev/null
+++ b/docs/superpowers/specs/2026-06-18-hvtrust-supply-chain-hardening-design.md
@@ -0,0 +1,145 @@
+# HVTrust Supply-Chain Hardening — Design
+
+**Date:** 2026-06-18
+**Status:** Approved (brainstorm), pending implementation plan
+**Owner:** Brian Love
+
+## Background
+
+[HVTracker](https://github.com/YugantM/hvtracker) (live at hvtracker.net) is a public
+trust registry that ranks open-source AI agent projects — threadplane's exact category —
+by an **HVTrust** score (0–100). It deliberately rewards *verifiable supply-chain evidence*
+over GitHub stars.
+
+### How HVTrust is computed
+
+From `compute_trust_score()` in HVTracker's `fetch_and_build.py`:
+
+```
+HVTrust = clamp( gate( confidence × Σ(weightᵢ · dimᵢ) − penalties ) )
+```
+
+Five weighted dimensions (each a 0–1 sub-score × weight):
+
+| Dimension | Weight | Inputs |
+|---|---|---|
+| Safety / Integrity | 25 | `0.5·OSSF-Scorecard + 0.3·has_provenance + 0.2·signed_commits_ratio` |
+| Identity / Provenance | 18 | `0.6·listed_in_registry + 0.4·has_provenance` |
+| Transparency | 17 | `0.5·license_present + 0.5·OSSF-Scorecard` |
+| Maintenance | 20 | `0.6·freshness(180d) + 0.4·log(weekly_commits)` |
+| Adoption | 20 | `0.6·log(stars/100k) + 0.4·log(downloads/1M)`, capped |
+
+Modifiers: **confidence** = present÷applicable signal types (floored 0.4);
+**penalties** = −10 if no push in >365 days; **gate** caps delisted/rejected at 25,
+legacy at 70. **Evidence grade** is a band of the final score: A ≥80, B ≥65, C ≥50, D <50.
+
+### threadplane's measured baseline (2026-06-18)
+
+| Signal | Value |
+|---|---|
+| License | MIT ✓ |
+| npm provenance (SLSA attestations) | **Present ✓** on all published `@threadplane/*` |
+| Last push / freshness | today (max) |
+| Weekly commits | ~29 (843 / 90d) |
+| Stars | 99 |
+| Weekly downloads (scope) | ~450 across 8 published packages |
+| Commit verification on `main` | All recent commits `verification.reason: valid` (GitHub squash-merge signing) |
+| OSSF Scorecard | **Not run** |
+| Listed on HVTracker | No (handled separately by owner) |
+| Repo hardening | No SECURITY.md, no Dependabot/Renovate, no CodeQL, Actions pinned by tag not SHA |
+
+**Computed today (confidence ≈ 1.0):** ~49 (auto-crawl, not listed, no Scorecard) → Grade D;
+~70 once listed + Scorecard ~5.0 → Grade B. The dominant lever is the **OSSF Scorecard**,
+whose score feeds both Safety (×0.5) and Transparency (×0.5) — ~21 points of weight.
+
+## Goal
+
+Raise threadplane's HVTrust by closing *evidence-publishing* gaps with genuine
+supply-chain improvements. No score gaming. Registering with HVTracker and chasing
+Adoption are explicitly **out of scope**.
+
+## Traceability
+
+| Change | OSSF Scorecard check | HVTrust effect |
+|---|---|---|
+| Scorecard workflow + `publish_results` | (enables measurement + badge) | Makes Safety/Transparency computable; HVTracker ingests it |
+| `SECURITY.md` + private vuln reporting | Security-Policy (0→10) | ↑ Scorecard → Safety/Transparency |
+| CodeQL workflow | SAST (0→10) | ↑ Scorecard |
+| Renovate + pin Actions by SHA | Dependency-Update-Tool + Pinned-Dependencies | ↑ Scorecard |
+| Top-level `permissions:` on 3 publish workflows | Token-Permissions | ↑ Scorecard |
+| Secret scanning + push protection + Dependabot security updates | (repo posture) | ↑ Scorecard |
+| Require signed commits on `main` + SSH-sign local | (locks signed ratio) | Safety ×0.2 held at max |
+| Signed git tags + SLSA provenance on Release artifacts | Signed-Releases (0→high) | ↑ Scorecard; Safety/Identity provenance |
+
+## Components
+
+### 1. OSSF Scorecard workflow (`.github/workflows/scorecard.yml`)
+- `ossf/scorecard-action`, **pinned by SHA**.
+- Triggers: weekly `schedule`, `push` to `main`, `branch_protection_rule`.
+- Least-privilege perms: `security-events: write`, `id-token: write`, `contents: read`, `actions: read`.
+- `publish_results: true` (publishes to the public OpenSSF API — required for the badge and
+ for HVTracker to read). SARIF uploaded to GitHub code scanning.
+- Add the Scorecard badge to `README.md`.
+- **Optional follow-up:** a fine-grained read `SCORECARD_TOKEN` secret to unlock the
+ Branch-Protection check (the default `GITHUB_TOKEN` can't read branch protection).
+ Flagged, not required for the first pass.
+
+### 2. Hardening files
+- `SECURITY.md` — supported versions + private reporting via GitHub Security Advisories.
+ Enable **Private vulnerability reporting** in repo settings.
+- `.github/workflows/codeql.yml` — CodeQL for `javascript-typescript` **and** `python`
+ (the repo ships the Python middleware). Pinned by SHA, minimal perms.
+- Add top-level `permissions: contents: read` to `publish.yml`, `publish-middleware-npm.yml`,
+ `publish-middleware-python.yml` (the only 3 of 8 workflows lacking it; job-level
+ `id-token: write` already elevates where needed).
+
+### 3. Renovate (`renovate.json`)
+- Extends `config:recommended` + `helpers:pinGitHubActionDigests` (pins every Action by SHA
+ and keeps the SHAs current — satisfies Dependency-Update-Tool **and** Pinned-Dependencies).
+- Grouped, scheduled updates to limit PR noise.
+- Initial bulk SHA-pinning pass across all 8 existing workflows (via `pinact` locally) so we
+ don't wait on Renovate's first run.
+- **Manual step (owner):** install the Renovate GitHub App on the repo. The config lands in
+ the PR.
+
+### 4. Commit signing — enforce + sign local
+- Local SSH commit signing: `gpg.format=ssh`, `commit.gpgsign=true`, existing SSH key as
+ `user.signingkey`; add that key as a **signing key** on GitHub. Capture exact setup in
+ `CONTRIBUTING.md`.
+- Enforce `required_signatures` on `main` branch protection (`gh api`). Safe: GitHub
+ squash-merges, Renovate-app commits, and Dependabot commits are all auto-signed; only stray
+ unsigned *direct* pushes to `main` are rejected (intended).
+
+### 5. Release signing — full SLSA artifacts
+- Signed tags: `tag.gpgsign=true` so `nx release` tags are SSH-signed (piggybacks on #4).
+ Existing npm provenance untouched.
+- GitHub Release provenance: add `slsa-framework/slsa-github-generator` (generic generator)
+ to the tag-triggered release flow — hash the built artifacts (npm pack tarballs of the
+ publishable libs), generate a signed `*.intoto.jsonl`, attach it to the GitHub Release.
+ This is what Scorecard's Signed-Releases check inspects (Release assets), complementing the
+ registry-side npm provenance.
+- PyPI: enable attestations in the Python publish (`gh-action-pypi-publish` with
+ `attestations: true`).
+- Heaviest component; sequenced last.
+
+## Sequencing
+
+1. Hardening files + token perms + Scorecard workflow + repo settings *(makes Scorecard measurable)*
+2. Renovate + SHA pinning
+3. Commit signing + enforcement
+4. SLSA release artifacts
+
+Each step is an independent PR-sized chunk.
+
+## Verification
+
+- Run `scorecard` locally (or read the first published run) before/after to confirm check deltas.
+- Confirm the badge renders in `README.md`.
+- Confirm a dry-run release produces and attaches the SLSA provenance `*.intoto.jsonl`.
+- Confirm `required_signatures` is active via `gh api .../branches/main/protection`.
+
+## Out of scope
+
+- Registering with HVTracker (owner handling separately).
+- Adoption optimization (log-capped; not worth the effort).
+- Fuzzing and CII-Best-Practices Scorecard checks (high effort, low marginal HVTrust return).
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 00000000..7c24ad44
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,23 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended",
+ "helpers:pinGitHubActionDigests",
+ ":dependencyDashboard",
+ ":semanticCommits"
+ ],
+ "schedule": ["before 9am on monday"],
+ "prConcurrentLimit": 5,
+ "packageRules": [
+ {
+ "matchManagers": ["github-actions"],
+ "groupName": "github-actions",
+ "pinDigests": true
+ },
+ {
+ "matchManagers": ["npm"],
+ "matchUpdateTypes": ["minor", "patch"],
+ "groupName": "npm minor+patch"
+ }
+ ]
+}