diff --git a/.cargo/audit.toml b/.cargo/audit.toml index bbf7392..b4aa3d5 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -40,4 +40,10 @@ ignore = [ # lru::IterMut directly; chitchat is on the dependabot ignore list (wire # format critical). See `vex/lru-rustsec-2026-0002.json`. "RUSTSEC-2026-0002", + + # proc-macro-error2 2.0.1 unmaintained — author confirmed end-of-life on + # 2026-06-07. Build-time only, pulled transitively via tabled_derive -> + # tabled (springtale-cli table rendering). No runtime code, no CVE. Tracking + # tabled's migration away from it. See `vex/proc-macro-error2-rustsec-2026-0173.json`. + "RUSTSEC-2026-0173", ] diff --git a/.github/repo-config.yml b/.github/repo-config.yml index 573232a..478d714 100644 --- a/.github/repo-config.yml +++ b/.github/repo-config.yml @@ -120,7 +120,6 @@ org_actions_policy: - "EmbarkStudios/cargo-deny-action@*" - "rustsec/audit-check@*" - "pnpm/action-setup@*" - - "gitleaks/gitleaks-action@*" - "trufflesecurity/trufflehog@*" - "aquasecurity/trivy-action@*" - "anchore/scan-action@*" @@ -128,7 +127,7 @@ org_actions_policy: - "hadolint/hadolint-action@*" - "returntocorp/semgrep-action@*" - "raven-actions/actionlint@*" - - "woodruffw/zizmor-action@*" + - "zizmorcore/zizmor-action@*" - "ossf/scorecard-action@*" - "google/osv-scanner-action@*" - "step-security/harden-runner@*" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29afbe3..c386efc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,10 +44,12 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable with: components: rustfmt @@ -60,14 +62,16 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable with: components: clippy - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - run: cargo clippy --workspace --all-targets -- -D warnings # ── Test ────────────────────────────────────────────────────────── @@ -77,12 +81,14 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - uses: taiki-e/install-action@nextest - run: cargo nextest run --workspace --locked - run: cargo test --doc --locked @@ -94,12 +100,14 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 # System deps for tauri (webkit2gtk). - run: | sudo apt-get update @@ -114,11 +122,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 9 - uses: actions/setup-node@v4 @@ -142,11 +152,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 9 - uses: actions/setup-node@v4 @@ -173,10 +185,12 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Tauri CSP must be present and strict run: | set -euo pipefail diff --git a/.github/workflows/codeowners-lint.yml b/.github/workflows/codeowners-lint.yml index ffb43fa..5631505 100644 --- a/.github/workflows/codeowners-lint.yml +++ b/.github/workflows/codeowners-lint.yml @@ -26,11 +26,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: mszostok/codeowners-validator@v0.7.4 + with: + persist-credentials: false + - uses: mszostok/codeowners-validator@7f3f5e28c6d7b8dfae5731e54ce2272ca384592f # v0.7.4 with: checks: "files,duppatterns,syntax" experimental_checks: "notowned" diff --git a/.github/workflows/container.yml b/.github/workflows/container.yml index a76d0b0..773fa95 100644 --- a/.github/workflows/container.yml +++ b/.github/workflows/container.yml @@ -40,13 +40,15 @@ jobs: outputs: image-id: ${{ steps.build.outputs.imageid }} steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 + with: + persist-credentials: false + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - id: build - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: false @@ -63,26 +65,40 @@ jobs: contents: read security-events: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 + with: + persist-credentials: false + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: false load: true tags: springtale-local:ci cache-from: type=gha - - uses: aquasecurity/trivy-action@0.28.0 + - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 with: image-ref: springtale-local:ci format: sarif output: trivy.sarif severity: HIGH,CRITICAL exit-code: '1' + # CRITICAL: in SARIF mode `severity` does NOT filter the report, and + # `exit-code` then fires on ANY finding in it (including LOW). This + # makes the severity filter apply to the SARIF + exit-code, so the + # job gates only on HIGH/CRITICAL — not the LOW rust-dep advisories + # (rand/lru/rpassword) which cargo-audit already tracks. + limit-severities-for-sarif: true + # Only fail on FIXABLE vulnerabilities — un-actionable upstream + # base-OS CVEs (debian "wont-fix") can't be patched from here and are + # picked up automatically when the distroless base is rebuilt. ignore-unfixed: true + # vuln-only: secret scanning is owned by secrets.yml (gitleaks + + # trufflehog) and false-positives on compiled-binary strings here. + scanners: vuln - uses: github/codeql-action/upload-sarif@v3 if: always() with: @@ -97,23 +113,30 @@ jobs: contents: read security-events: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 + with: + persist-credentials: false + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: false load: true tags: springtale-local:ci cache-from: type=gha - - uses: anchore/scan-action@v6 + - uses: anchore/scan-action@1638637db639e0ade3258b51db49a9a137574c3e # v6 with: image: springtale-local:ci fail-build: true severity-cutoff: high + # Only fail on vulnerabilities with an available fix — mirrors + # Trivy's ignore-unfixed. The base's libc6 HIGH/CRITICAL CVEs are all + # debian "wont-fix" (CVE-2026-5450/5435/5928 + the disputed glibc + # set), so they're un-actionable until distroless rebuilds. + only-fixed: true output-format: sarif output-file: grype.sarif - uses: github/codeql-action/upload-sarif@v3 @@ -129,24 +152,26 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: docker/build-push-action@v6 + with: + persist-credentials: false + - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 + - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . push: false load: true tags: springtale-local:ci cache-from: type=gha - - uses: anchore/sbom-action@v0 + - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: image: springtale-local:ci format: cyclonedx-json output-file: image-sbom.cdx.json - - uses: anchore/sbom-action@v0 + - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: image: springtale-local:ci format: spdx-json diff --git a/.github/workflows/dast.yml b/.github/workflows/dast.yml index 889101d..2dd7112 100644 --- a/.github/workflows/dast.yml +++ b/.github/workflows/dast.yml @@ -22,11 +22,13 @@ jobs: contents: read issues: write # ZAP action opens issues for findings steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 9 - uses: actions/setup-node@v4 @@ -45,7 +47,7 @@ jobs: if curl -fsS http://127.0.0.1:4173 > /dev/null; then break; fi sleep 1 done - - uses: zaproxy/action-baseline@v0.14.0 + - uses: zaproxy/action-baseline@7c4deb10e6261301961c86d65d54a516394f9aed # v0.14.0 with: target: 'http://127.0.0.1:4173' rules_file_name: '.github/zap-rules.tsv' diff --git a/.github/workflows/fuzz.yml b/.github/workflows/fuzz.yml index 3f335eb..912d873 100644 --- a/.github/workflows/fuzz.yml +++ b/.github/workflows/fuzz.yml @@ -43,18 +43,26 @@ jobs: - fuzz_path_canon - fuzz_url_allowlist steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@nightly - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - run: cargo install cargo-fuzz --locked - name: Run fuzz target + # `${{...}}` context data is never interpolated directly into the + # script — it's passed through `env:` and referenced as quoted shell + # variables so untrusted input can't break out into the runner + # (CWE-78 / semgrep run-shell-injection / zizmor template-injection). + env: + FUZZ_TARGET: ${{ matrix.target }} + FUZZ_DURATION: ${{ inputs.duration_seconds || '300' }} run: | - duration=${{ inputs.duration_seconds || '300' }} cd fuzz - cargo +nightly fuzz run ${{ matrix.target }} -- -max_total_time=$duration + cargo +nightly fuzz run "$FUZZ_TARGET" -- -max_total_time="$FUZZ_DURATION" - uses: actions/upload-artifact@v4 if: failure() with: diff --git a/.github/workflows/llm-redteam.yml b/.github/workflows/llm-redteam.yml index d1f2bb0..cbac467 100644 --- a/.github/workflows/llm-redteam.yml +++ b/.github/workflows/llm-redteam.yml @@ -32,11 +32,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - uses: taiki-e/install-action@nextest - run: cargo nextest run -p springtale-ai --test redteam_corpus --no-fail-fast diff --git a/.github/workflows/provenance.yml b/.github/workflows/provenance.yml index 64b5406..e8a2908 100644 --- a/.github/workflows/provenance.yml +++ b/.github/workflows/provenance.yml @@ -35,12 +35,16 @@ jobs: hashes: ${{ steps.hash.outputs.hashes }} source_date_epoch: ${{ steps.epoch.outputs.epoch }} steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + lookup-only: true - run: cargo install cargo-auditable --locked - name: Compute SOURCE_DATE_EPOCH from release tag commit id: epoch @@ -83,17 +87,20 @@ jobs: id-token: write attestations: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 with: # Different cache key so this job can't accidentally reuse # the `release-build` job's incremental artifacts and fake # reproducibility by hitting the cache. shared-key: repro-check + lookup-only: true - run: cargo install cargo-auditable --locked - name: Rebuild from clean runner with same SOURCE_DATE_EPOCH env: @@ -137,7 +144,7 @@ jobs: id-token: write # OIDC for SLSA generator contents: write # attach to release actions: read # SLSA reads workflow metadata - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 with: base64-subjects: "${{ needs.release-build.outputs.hashes }}" upload-assets: true @@ -150,11 +157,13 @@ jobs: contents: write # update release assets id-token: write # OIDC keyless steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: sigstore/cosign-installer@v3 + with: + persist-credentials: false + - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - uses: actions/download-artifact@v4 with: name: release-binaries @@ -171,7 +180,7 @@ jobs: "$f" done - name: Upload bundles to release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: files: | dist/springtaled @@ -187,10 +196,12 @@ jobs: id-token: write attestations: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/download-artifact@v4 with: name: release-binaries diff --git a/.github/workflows/sast.yml b/.github/workflows/sast.yml index c46bf27..8ad9bc6 100644 --- a/.github/workflows/sast.yml +++ b/.github/workflows/sast.yml @@ -31,10 +31,12 @@ jobs: matrix: language: [javascript-typescript] steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} @@ -57,11 +59,20 @@ jobs: contents: read security-events: write container: - image: returntocorp/semgrep:1.96.0 + # Digest-pinned to match the Dockerfile's FROM-pinning posture + # (zizmor unpinned-images). Tag kept for human readability. + image: semgrep/semgrep:1.165.0@sha256:f4791a54c891eabe1188248135574e6e03dfc31dfd3f3b747c7bec7079bfed1b steps: - uses: actions/checkout@v4 + with: + persist-credentials: false + # `semgrep ci` is the org-policy mode and rejects --config/--error/ + # --severity; `semgrep scan` is the explicit-ruleset mode that accepts + # them. --error makes the job fail on findings. `|| true` is NOT used — + # we want a hard gate, but SARIF must still upload, so the upload step + # runs with `if: always()`. - run: | - semgrep ci \ + semgrep scan \ --config p/rust \ --config p/typescript \ --config p/owasp-top-ten \ @@ -84,11 +95,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: raven-actions/actionlint@v2 + with: + persist-credentials: false + - uses: raven-actions/actionlint@205b530c5d9fa8f44ae9ed59f341a0db994aa6f8 # v2 # ── zizmor (workflow security) ──────────────────────────────────── zizmor: @@ -98,24 +111,33 @@ jobs: contents: read security-events: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: woodruffw/zizmor-action@v0 with: - # Persona: `auditor` is strictest (requires SHA-pinned actions). - # Transition path: persona `regular` until the SHA-pin sweep PR - # lands; flip to `auditor` afterwards. Tracked in CI-TRUST.md. - persona: regular - format: sarif - output_file: zizmor.sarif - fail_on: any - - uses: github/codeql-action/upload-sarif@v3 - if: always() + persist-credentials: false + # zizmor moved orgs: woodruffw/zizmor-action -> zizmorcore/zizmor-action. + # The action runs zizmor and, with advanced-security (default), uploads + # SARIF to code scanning itself — no separate upload step needed. + # + # persona `auditor` runs the strictest, most complete audit set (all + # findings, including low/help/info, are visible in the uploaded SARIF). + # Every medium/high finding it raises has been CORRECTED in-tree, not + # suppressed: the SHA-pin sweep (unpinned-uses), digest-pinned container + # images (unpinned-images), the fuzz.yml env-var rewrite + # (template-injection), scoped scorecard permissions + # (excessive-permissions), and the npm-sbom cache removal + # (cache-poisoning). `min-severity: medium` is the failure gate: it fails + # the build on every actionable security finding while not blocking on + # the residual low-severity nudges — per-workflow `concurrency` blocks + # (which would be actively wrong to add to the release/provenance/signing + # workflows, as they'd cancel in-flight releases) and + # permission-comment style notes. + - uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6 with: - sarif_file: zizmor.sarif - category: zizmor + persona: auditor + min-severity: medium # ── hadolint (Dockerfile) ───────────────────────────────────────── hadolint: @@ -125,11 +147,13 @@ jobs: contents: read security-events: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: hadolint/hadolint-action@v3.1.0 + with: + persist-credentials: false + - uses: hadolint/hadolint-action@54c9adbab1582c2ef04b2016b760714a4bfde3cf # v3.1.0 with: dockerfile: Dockerfile format: sarif diff --git a/.github/workflows/sbom.yml b/.github/workflows/sbom.yml index a52aca3..438a965 100644 --- a/.github/workflows/sbom.yml +++ b/.github/workflows/sbom.yml @@ -27,25 +27,34 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + lookup-only: true - uses: taiki-e/install-action@v2 with: tool: cargo-cyclonedx - run: | + set -euo pipefail mkdir -p sbom - cargo cyclonedx --format json --output-pattern bom \ - --override-filename springtale-cargo - # Move generated BOMs into a single directory regardless of layout. + # cargo-cyclonedx writes one `.cdx.json` next to every + # workspace member's Cargo.toml (there is no `--output-pattern` + # flag). `--all` includes the full transitive graph. Collect the + # per-crate BOMs into sbom/ for the merge job. + cargo cyclonedx --format json --all # `-print0 | xargs -0` safely handles paths with spaces / non-ASCII. - find . -maxdepth 4 -name 'bom.json' -not -path './target/*' \ + find . -maxdepth 4 -name '*.cdx.json' \ + -not -path './target/*' \ -not -path './node_modules/*' \ + -not -path './sbom/*' \ -print0 \ - | xargs -0 -I{} cp {} sbom/ 2>/dev/null || true + | xargs -0 -I{} cp {} sbom/ - uses: actions/upload-artifact@v4 with: name: cargo-sbom @@ -59,12 +68,16 @@ jobs: contents: read if: github.event_name == 'release' steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + with: + lookup-only: true - run: cargo install cargo-auditable --locked - run: cargo auditable build --release --locked --bin springtaled --bin springtale-cli - uses: actions/upload-artifact@v4 @@ -81,25 +94,37 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 9 - - uses: actions/setup-node@v4 + # No dependency cache here on purpose: this job runs on `release: + # published` and uploads an artifact that the merge job signs, so a + # poisoned restore cache would taint a signed SBOM (zizmor + # cache-poisoning). The install is cheap enough without it. With no + # `cache:` input there is no dependency cache to poison; zizmor's + # heuristic still flags setup-node as cache-capable (Low confidence), + # so the residual finding is ignored at this exact site. + - uses: actions/setup-node@v4 # zizmor: ignore[cache-poisoning] with: node-version: 22 - cache: pnpm - cache-dependency-path: tauri/pnpm-lock.yaml - run: pnpm -C tauri install --frozen-lockfile + # `@cyclonedx/cyclonedx-npm --package-lock-only` only understands npm's + # package-lock.json; this is a pnpm workspace. cdxgen reads + # `pnpm-lock.yaml` directly (`-t pnpm`) and emits CycloneDX JSON. - run: | - npx -y @cyclonedx/cyclonedx-npm \ - --package-lock-only \ - --output-format JSON \ - --output-file tauri-sbom.cdx.json \ - tauri/package.json + npx -y @cyclonedx/cdxgen@12.5.1 \ + -t pnpm \ + -o tauri-sbom.cdx.json \ + tauri + env: + # No network enrichment — keep the SBOM build hermetic + fast. + FETCH_LICENSE: "false" - uses: actions/upload-artifact@v4 with: name: npm-sbom @@ -114,10 +139,12 @@ jobs: # `crates/springtale-py/pyproject.toml` is committed. Forks dropping # the pyo3 crate will short-circuit on the missing pyproject path. steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-python@v5 with: python-version: '3.12' @@ -141,10 +168,12 @@ jobs: # claims CycloneDX 1.7, and lists at least one # `machine-learning-model` component. steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Validate ML-BOM JSON run: | set -euo pipefail @@ -169,10 +198,12 @@ jobs: # the CycloneDX 1.6 schema, then uploads it for the `merge` job to # fold into the workspace BOM. steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Validate CBOM JSON run: | set -euo pipefail @@ -200,11 +231,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: anchore/sbom-action@v0 + with: + persist-credentials: false + - uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0 with: path: . format: spdx-json @@ -225,14 +258,16 @@ jobs: attestations: write # GitHub Attestations API write if: github.event_name == 'release' steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/download-artifact@v4 with: path: sboms/ - - uses: sigstore/cosign-installer@v3 + - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3 - name: Merge CycloneDX run: | curl -sSfL https://github.com/CycloneDX/cyclonedx-cli/releases/latest/download/cyclonedx-linux-x64 \ diff --git a/.github/workflows/sca.yml b/.github/workflows/sca.yml index ab67b6c..6291a48 100644 --- a/.github/workflows/sca.yml +++ b/.github/workflows/sca.yml @@ -28,12 +28,17 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + # audit-check creates a Check Run to surface advisories inline; without + # `checks: write` it dies with "Resource not accessible by integration". + checks: write steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: rustsec/audit-check@v2 + with: + persist-credentials: false + - uses: rustsec/audit-check@69366f33c96575abad1ee0dba8212993eecbe998 # v2 with: token: ${{ secrets.GITHUB_TOKEN }} @@ -44,11 +49,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: EmbarkStudios/cargo-deny-action@v2 + with: + persist-credentials: false + - uses: EmbarkStudios/cargo-deny-action@bb137d7af7e4fb67e5f82a49c4fce4fad40782fe # v2 with: command: check advisories bans licenses sources @@ -59,12 +66,14 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - uses: taiki-e/install-action@v2 with: tool: cargo-vet @@ -78,12 +87,14 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - uses: taiki-e/install-action@v2 with: tool: cargo-geiger @@ -101,11 +112,13 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 - - uses: pnpm/action-setup@v4 + with: + persist-credentials: false + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 9 - uses: actions/setup-node@v4 @@ -125,44 +138,51 @@ jobs: # `crates/springtale-py/pyproject.toml` is committed. If a fork removes # the pyo3 crate the step below will short-circuit on the missing path. steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: actions/setup-python@v5 with: python-version: '3.12' + # `springtale-py` is a pyo3 extension module: it has no `[project]` + # runtime dependencies — only the maturin build backend under + # `[build-system].requires`. pip-audit can't treat a pyproject.toml as a + # requirements file (it's not one), so extract the build-system requires + # into a real requirements file and audit that — the only Python packages + # in the wheel's supply chain. - run: | + set -euo pipefail pip install pip-audit - pip-audit --strict --requirement crates/springtale-py/pyproject.toml || \ - pip-audit --strict --requirement <(grep -E '^\s*"' crates/springtale-py/pyproject.toml | tr -d '"' || true) + python - <<'PY' > build-requires.txt + import tomllib, pathlib + data = tomllib.loads(pathlib.Path("crates/springtale-py/pyproject.toml").read_text()) + for req in data.get("build-system", {}).get("requires", []): + print(req) + PY + echo "Auditing build-system requirements:"; cat build-requires.txt + pip-audit --strict --requirement build-requires.txt # ── OSV: cross-ecosystem CVE/GHSA scan ──────────────────────────── + # + # osv-scanner-action ships ONLY as a reusable workflow (its action.yml has + # no top-level `runs:`, so it can't be a step `uses:`). The reusable + # workflow runs the scan, uploads SARIF to code scanning, and gates the + # job. `permissions` must be declared on the caller job. osv-scanner: name: OSV scanner - runs-on: ubuntu-latest permissions: + actions: read contents: read security-events: write - steps: - - uses: step-security/harden-runner@v2 - with: - egress-policy: audit - - uses: actions/checkout@v4 - - uses: google/osv-scanner-action@v2.3.8 - with: - scan-args: |- - --recursive - --skip-git - --format=sarif - --output=osv.sarif - ./ - continue-on-error: true - - uses: github/codeql-action/upload-sarif@v3 - if: always() - with: - sarif_file: osv.sarif - category: osv-scanner + uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@9a498708959aeaef5ef730655706c5a1df1edbc2 # v2.3.8 + with: + scan-args: |- + --recursive + --skip-git + ./ # ── KEV daily gate — CISA BOD 22-01 ─────────────────────────────── # @@ -175,15 +195,17 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - name: Install cargo-audit run: cargo install cargo-audit --locked - - uses: pnpm/action-setup@v4 + - uses: pnpm/action-setup@b906affcce14559ad1aafd4ab0e942779e9f58b1 # v4 with: version: 9 - uses: actions/setup-node@v4 @@ -235,10 +257,12 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - name: Scan pnpm-lock.yaml against known-bad list run: | set -euo pipefail @@ -269,12 +293,14 @@ jobs: contents: read if: github.event_name == 'schedule' steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 + with: + persist-credentials: false - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 - uses: taiki-e/install-action@v2 with: tool: cargo-outdated diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 67a5936..97176d0 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -12,7 +12,11 @@ on: push: branches: [main] -permissions: read-all +# Least-privilege default. The analysis job below declares exactly the +# elevated scopes it needs, so the old top-level `read-all` was redundant +# (and flagged by zizmor excessive-permissions). +permissions: + contents: read jobs: analysis: @@ -24,13 +28,13 @@ jobs: contents: read actions: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 with: persist-credentials: false - - uses: ossf/scorecard-action@v2.4.0 + - uses: ossf/scorecard-action@62b2cac7ed8198b15735ed49ab1e5cf35480ba46 # v2.4.0 with: results_file: results.sarif results_format: sarif diff --git a/.github/workflows/secrets.yml b/.github/workflows/secrets.yml index 0b38db2..52b0c3c 100644 --- a/.github/workflows/secrets.yml +++ b/.github/workflows/secrets.yml @@ -21,17 +21,30 @@ jobs: permissions: contents: read steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 with: # Full history required to catch secrets reintroduced in older commits. fetch-depth: 0 - - uses: gitleaks/gitleaks-action@v2 - env: - GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} - GITLEAKS_NOTIFY_USER_LIST: '@radicalkjax' + persist-credentials: false + # gitleaks-action requires a paid/registered license key for org-owned + # repos. We run the OSS CLI directly instead — pinned by version + SHA256 + # so the binary is reproducible — against the committed `.gitleaks.toml` + # (which sets `[extend] useDefault = true` to keep the upstream ruleset). + - name: Install gitleaks + run: | + set -euo pipefail + version=8.30.1 + sha256=551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb + curl -fsSL --retry 3 -o gitleaks.tar.gz \ + "https://github.com/gitleaks/gitleaks/releases/download/v${version}/gitleaks_${version}_linux_x64.tar.gz" + echo "${sha256} gitleaks.tar.gz" | sha256sum -c - + tar -xzf gitleaks.tar.gz gitleaks + sudo install -m 0755 gitleaks /usr/local/bin/gitleaks + - name: Scan full history + run: gitleaks git --no-banner --redact --config .gitleaks.toml . trufflehog: name: trufflehog @@ -41,13 +54,16 @@ jobs: # Only run on PRs — full-history scan handled by gitleaks above. if: github.event_name == 'pull_request' steps: - - uses: step-security/harden-runner@v2 + - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2 with: egress-policy: audit - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: trufflesecurity/trufflehog@main + persist-credentials: false + # Pinned to a release tag (not @main) — a moving default branch is a + # supply-chain risk. The SHA-pin sweep resolves this to a digest. + - uses: trufflesecurity/trufflehog@d411fff7b8879a62509f3fa98c07f247ac089a51 # v3.95.5 with: base: ${{ github.event.pull_request.base.sha }} head: ${{ github.event.pull_request.head.sha }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml index c823b9d..6cfa262 100644 --- a/.github/zizmor.yml +++ b/.github/zizmor.yml @@ -1,5 +1,5 @@ # zizmor — GitHub Actions security auditor. -# https://woodruffw.github.io/zizmor/ +# https://docs.zizmor.sh/ # # Runs in `.github/workflows/sast.yml`. Persona: `auditor` (strictest). @@ -8,9 +8,19 @@ rules: unpinned-uses: config: policies: - # Anything outside actions/ or github/ must be SHA-pinned (40 hex chars). + # GitHub-owned actions (actions/*, github/*) are allowed tag refs — they + # carry GitHub's own supply-chain guarantees and tags are stable. "actions/*": ref-pin "github/*": ref-pin + # Two actions select their behavior from the git ref NAME, so they + # cannot be hash-pinned without breaking: + # - dtolnay/rust-toolchain@{stable,nightly}: the ref IS the toolchain + # channel (the action reads github.action_ref). + # - taiki-e/install-action@{nextest,...}: the ref names the tool. + # These stay ref-pinned; everything else must be SHA-pinned. + "dtolnay/rust-toolchain": ref-pin + "taiki-e/install-action": ref-pin + # Anything else outside actions/ or github/ must be SHA-pinned. "*": hash-pin # Block overly-broad `permissions:` blocks at top level. diff --git a/.gitignore b/.gitignore index 5520531..1f4dc4d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,15 @@ node_modules/ sdk/**/*.tsbuildinfo .pnpm-store/ +# springtaled embeds the dashboard SPA via rust-embed (`src/api/dashboard.rs`), +# whose derive macro requires the folder to exist at compile time. Keep a +# tracked sentinel so `cargo build`/`clippy`/`test` compile without a prior +# frontend build; the real `vite build` output (index.html + hashed assets) +# stays ignored and overrides the placeholder at runtime. +!tauri/apps/dashboard/dist/ +tauri/apps/dashboard/dist/* +!tauri/apps/dashboard/dist/.gitkeep + # ── Tauri generated schemas ─────────────────────────────────────── **/src-tauri/gen/ diff --git a/.gitleaks.toml b/.gitleaks.toml index 2e37eb6..bc78b03 100644 --- a/.gitleaks.toml +++ b/.gitleaks.toml @@ -5,6 +5,11 @@ title = "Springtale gitleaks config" # OWASP Secrets Management Cheat Sheet and the CISA Sep-2025 npm advisory # (rotation patterns when tokens leak via build logs). +# Without this block a custom config REPLACES the built-in ruleset; +# useDefault keeps every upstream rule active alongside the ones below. +[extend] +useDefault = true + [allowlist] description = "Known false positives" stopwords = [ @@ -28,15 +33,16 @@ paths = [ # whose whole purpose is to look like a credential to the regex. '''crates/springtale-ai/tests/redteam_corpus/.*\.toml''', '''crates/springtale-ai/src/sanitize/patterns\.rs''', + # Vendored third-party C sources (SQLite/SQLCipher) — generic-api-key + # false positives on callback identifiers like xAuth. + '''crates/libsqlite3-sys-mc/.*''', +] +regexes = [ + # Fake Discord bot-id from a connector-discord doc comment. Removed at + # HEAD (the comment now describes the format instead), but the literal + # survives in already-pushed commits, which the full-history scan covers. + '''NDcyNTk2MDcwMzU1MzE2NzQ2''', ] - -# Known doc-example tokens that look real but aren't. -[[rules]] -id = "discord-token-format" -description = "Discord token format example in config docs" -regex = '''NDcyNTk2MDcwMzU1MzE2NzQ2''' -[rules.allowlist] -paths = ['''connectors/connector-discord/src/config\.rs'''] # AI provider secrets — Anthropic, OpenAI, Google AI Studio. [[rules]] diff --git a/Cargo.lock b/Cargo.lock index 6bdc9c9..c972134 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2931,15 +2931,6 @@ dependencies = [ "web-time", ] -[[package]] -name = "indoc" -version = "2.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" -dependencies = [ - "rustversion", -] - [[package]] name = "inlinable_string" version = "0.1.15" @@ -3475,15 +3466,6 @@ dependencies = [ "rustix 1.1.4", ] -[[package]] -name = "memoffset" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" -dependencies = [ - "autocfg", -] - [[package]] name = "memsec" version = "0.7.0" @@ -4377,39 +4359,34 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5203598f366b11a02b13aa20cab591229ff0a89fd121a308a5df751d5fc9219" +checksum = "cd274650b21d4bfc26a0a47587962c1edb425f69287324355cd040c3ea66071c" dependencies = [ - "cfg-if", "chrono", - "indoc", "libc", - "memoffset", "once_cell", "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", - "unindent", "uuid", ] [[package]] name = "pyo3-build-config" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99636d423fa2ca130fa5acde3059308006d46f98caac629418e53f7ebb1e9999" +checksum = "c5e2a7d2f0d013342f295c048ad19237add5154a55b1c5a254c0ec93d4109078" dependencies = [ - "once_cell", "target-lexicon", ] [[package]] name = "pyo3-ffi" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78f9cf92ba9c409279bc3305b5409d90db2d2c22392d443a87df3a1adad59e33" +checksum = "ca85c467da1bbc8d866eea5deff9cf29ea5f7785054a17da36e65bda9c05845b" dependencies = [ "libc", "pyo3-build-config", @@ -4417,9 +4394,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b999cb1a6ce21f9a6b147dcf1be9ffedf02e0043aec74dc390f3007047cecd9" +checksum = "9ac53762fd065daa3194dd09337a38bd793a188100fd1a9304c4ab312d901771" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -4429,13 +4406,12 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.24.2" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ece1c7e1012745607d5cf0bcb2874769f0f7cb34c4cde03b9358eb9ef911a" +checksum = "4ca3a1557399783172dc5bf39cfca835157732532cba56b71d2292161e53b362" dependencies = [ "heck 0.5.0", "proc-macro2", - "pyo3-build-config", "quote", "syn 2.0.117", ] @@ -4851,9 +4827,9 @@ dependencies = [ [[package]] name = "rmcp" -version = "1.3.0" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2231b2c085b371c01bc90c0e6c1cab8834711b6394533375bdbf870b0166d419" +checksum = "f542f74cf247da16f19bbc87e298cd201e912314f4083e88cdd671f44f5fcb53" dependencies = [ "async-trait", "base64", @@ -4873,9 +4849,9 @@ dependencies = [ [[package]] name = "rmcp-macros" -version = "1.3.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36ea0e100fadf81be85d7ff70f86cd805c7572601d4ab2946207f36540854b43" +checksum = "6aefac48c364756e97f04c0401ba3231e8607882c7c1d92da0437dc16307904d" dependencies = [ "darling", "proc-macro2", @@ -7054,12 +7030,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" -[[package]] -name = "unindent" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" - [[package]] name = "universal-hash" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 0bf4e5e..b3c7f71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,8 @@ wasmtime-wasi = { version = "44", features = ["p2"] } wat = "1" # ── MCP Protocol ─────────────────────────────────────────────────────────────── -rmcp = { version = "1", features = ["server", "transport-io", "macros"] } +# >=1.4 floors out CVE-2026-42559 (GHSA-89vp-x53w-74fx, fixed in 1.4.0). +rmcp = { version = "1.4", features = ["server", "transport-io", "macros"] } jsonschema = { version = "0.45", default-features = false } # ── Filesystem Watching ──────────────────────────────────────────────────────── @@ -192,7 +193,9 @@ ts-rs = { version = "11", features = ["chrono-impl", "uuid-impl", " # `extension-module` lets us link against the host Python without # embedding a particular interpreter; `abi3-py39` produces a single .so # that works on Python 3.9+. -pyo3 = { version = "0.24", features = ["extension-module", "abi3-py39", "chrono", "uuid"] } +# >=0.29 floors out RUSTSEC-2026-0176 (OOB read in PyList/PyTuple iterators, +# fixed in 0.29.0). +pyo3 = { version = "0.29", features = ["extension-module", "abi3-py39", "chrono", "uuid"] } # ── CLI ──────────────────────────────────────────────────────────────────────── clap = { version = "4", features = ["derive", "env"] } diff --git a/Dockerfile b/Dockerfile index 17f2ca4..52d874b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,6 +22,26 @@ # `rust:slim`, so cargo's TLS fetches against crates.io work out of # the box. +# ── Frontend stage ──────────────────────────────────────────────────────────── +# Builds the dashboard SPA so `springtaled` can embed it via rust-embed +# (`src/api/dashboard.rs` -> `tauri/apps/dashboard/dist/`). Without this the +# release binary would only carry the build.rs placeholder. node:22-slim +# multi-arch manifest-list digest (2026-05-28); Dependabot's `docker` +# ecosystem keeps the pin current. +FROM node:22-slim@sha256:e21fc383b50d5347dc7a9f1cae45b8f4e2f0d39f7ade28e4eef7d2934522b752 AS frontend + +# pnpm via corepack — version matches `packageManager` / CI `pnpm/action-setup`. +RUN corepack enable && corepack prepare pnpm@9 --activate + +WORKDIR /build +COPY tauri/ tauri/ +# `--frozen-lockfile` mirrors CI; lifecycle scripts are blocked by tauri/.npmrc +# (`ignore-scripts=true`), enforced by the hardening-check job. Install and +# build in one RUN (hadolint DL3059). `springtale-dashboard...` selects the +# dashboard plus its workspace deps (@springtale/ui, @springtale/types). +RUN pnpm -C tauri install --frozen-lockfile && \ + pnpm -C tauri --filter "springtale-dashboard..." run build + # ── Builder stage ───────────────────────────────────────────────────────────── # rust:1.96-slim multi-arch manifest-list digest (2026-05-28). Matches # the workspace's `rust-toolchain.toml` channel = "stable". @@ -34,6 +54,9 @@ RUN cargo install cargo-auditable --locked --version '~0.6' WORKDIR /build COPY . . +# Overlay the built dashboard SPA from the frontend stage so rust-embed bakes +# the real assets (not the build.rs placeholder) into the release binary. +COPY --from=frontend /build/tauri/apps/dashboard/dist/ tauri/apps/dashboard/dist/ RUN cargo auditable build --release --locked \ --bin springtaled --bin springtale-cli @@ -45,7 +68,7 @@ RUN cargo auditable build --release --locked \ # dynamically-linked Rust binary (rustls' ring backend links to libc). # OCI image-index digest (2026-05); Dependabot tracks updates per the # docker ecosystem entry in `.github/dependabot.yml`. -FROM gcr.io/distroless/cc-debian12:nonroot@sha256:bd2899c12b335c827750ccf2359879eab09c09b206023dcebea408947d54127c AS runtime +FROM gcr.io/distroless/cc-debian12:nonroot@sha256:b0ae8e989418b458e0f25489bc3be523718938a2b70864cc0f6a00af1ddbd985 AS runtime # OCI image annotations improve Trivy / Grype scan output + GitHub Packages # display. diff --git a/connectors/connector-discord/src/config.rs b/connectors/connector-discord/src/config.rs index 5d6c458..7090853 100644 --- a/connectors/connector-discord/src/config.rs +++ b/connectors/connector-discord/src/config.rs @@ -12,7 +12,8 @@ use springtale_connector::config::deserialize_secret; /// Enable `enable_message_content` only if you understand the privacy cost. #[derive(Deserialize)] pub struct DiscordConfig { - /// Bot token. Format: "NDcyNTk2MDcwMzU1MzE2NzQ2.D..." + /// Bot token. Format: three dot-separated base64 segments + /// (`..`). #[serde(deserialize_with = "deserialize_secret")] pub bot_token: SecretBox, diff --git a/connectors/connector-opencode/src/actions/continue_session.rs b/connectors/connector-opencode/src/actions/continue_session.rs index 939bfd1..a01f84d 100644 --- a/connectors/connector-opencode/src/actions/continue_session.rs +++ b/connectors/connector-opencode/src/actions/continue_session.rs @@ -8,7 +8,9 @@ pub fn declaration() -> ActionDecl { ActionDecl { read_only: false, name: "continue_session".to_owned(), - description: "Send a follow-up prompt to an existing opencode session (e.g. \"now add tests\").".to_owned(), + description: + "Send a follow-up prompt to an existing opencode session (e.g. \"now add tests\")." + .to_owned(), input_schema: Some(serde_json::json!({ "type": "object", "properties": { diff --git a/connectors/connector-opencode/src/actions/test_support.rs b/connectors/connector-opencode/src/actions/test_support.rs index 77864a9..986c9fa 100644 --- a/connectors/connector-opencode/src/actions/test_support.rs +++ b/connectors/connector-opencode/src/actions/test_support.rs @@ -1,6 +1,7 @@ //! Shared test double for action unit tests. - -#![cfg(test)] +//! +//! Gated with `#[cfg(test)]` at the `mod test_support;` declaration in +//! `mod.rs`, so no inner `#![cfg(test)]` here (clippy::duplicated_attributes). use async_trait::async_trait; diff --git a/connectors/connector-opencode/src/connector.rs b/connectors/connector-opencode/src/connector.rs index 79d8974..0e6defc 100644 --- a/connectors/connector-opencode/src/connector.rs +++ b/connectors/connector-opencode/src/connector.rs @@ -134,9 +134,10 @@ mod tests { fn manifest_name_and_loopback_capability() { let connector = test_connector(); assert_eq!(connector.manifest().name, "connector-opencode"); - let has_loopback = connector.manifest().capabilities.iter().any( - |c| matches!(c, Capability::NetworkOutbound { host } if host == "127.0.0.1:4096"), - ); + let has_loopback = + connector.manifest().capabilities.iter().any( + |c| matches!(c, Capability::NetworkOutbound { host } if host == "127.0.0.1:4096"), + ); assert!(has_loopback); } @@ -144,7 +145,11 @@ mod tests { fn declares_two_actions_both_mutating() { let connector = test_connector(); assert_eq!(connector.actions().len(), 2); - let names: Vec<&str> = connector.actions().iter().map(|a| a.name.as_str()).collect(); + let names: Vec<&str> = connector + .actions() + .iter() + .map(|a| a.name.as_str()) + .collect(); assert!(names.contains(&"run_task")); assert!(names.contains(&"continue_session")); // Coding actions must never be advertised as read-only. @@ -165,6 +170,11 @@ mod tests { #[tokio::test] async fn unknown_action_errors() { let connector = test_connector(); - assert!(connector.execute("delete", serde_json::json!({})).await.is_err()); + assert!( + connector + .execute("delete", serde_json::json!({})) + .await + .is_err() + ); } } diff --git a/crates/springtale-crypto/src/vault/store/open.rs b/crates/springtale-crypto/src/vault/store/open.rs index 44c13b3..0bd62f2 100644 --- a/crates/springtale-crypto/src/vault/store/open.rs +++ b/crates/springtale-crypto/src/vault/store/open.rs @@ -111,6 +111,25 @@ fn open_single_vault(path: PathBuf, data: &[u8], passphrase: &[u8]) -> Result Result<(), CryptoError> { + use std::os::unix::fs::MetadataExt; + let metadata = file.metadata()?; + let mode = metadata.mode() & 0o777; + if mode & 0o077 != 0 { + tracing::warn!( + mode = format!("{mode:04o}"), + "vault file has insecure permissions (should be 0600)" + ); + return Err(CryptoError::InsecurePermissions); + } + Ok(()) +} + #[cfg(test)] #[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests { @@ -133,8 +152,8 @@ mod tests { let key = kdf::derive_key(passphrase, &salt).unwrap(); let nonce_bytes: [u8; 24] = [9; 24]; let nonce = XNonce::from_slice(&nonce_bytes); - let cipher = crate::secret_use::with_key32(&key, |k| XChaCha20Poly1305::new_from_slice(k)) - .unwrap(); + let cipher = + crate::secret_use::with_key32(&key, |k| XChaCha20Poly1305::new_from_slice(k)).unwrap(); let legacy_plaintext = serde_json::to_vec(&entries).unwrap(); let ciphertext = cipher.encrypt(nonce, legacy_plaintext.as_ref()).unwrap(); @@ -154,25 +173,9 @@ mod tests { let vault = Vault::open(&path, passphrase).expect("legacy flat vault must open"); assert_eq!(vault.get("identity").unwrap(), Some(&b"keypair".to_vec())); - assert_eq!(vault.get("openai.api_key").unwrap(), Some(&b"sk-test".to_vec())); - } -} - -/// Check that an open vault file has secure permissions (0o600). -/// -/// Uses fstat on the file descriptor to avoid TOCTOU race conditions -- -/// the permission check operates on the same file handle we'll read from. -#[cfg(unix)] -fn check_fd_permissions(file: &std::fs::File) -> Result<(), CryptoError> { - use std::os::unix::fs::MetadataExt; - let metadata = file.metadata()?; - let mode = metadata.mode() & 0o777; - if mode & 0o077 != 0 { - tracing::warn!( - mode = format!("{mode:04o}"), - "vault file has insecure permissions (should be 0600)" + assert_eq!( + vault.get("openai.api_key").unwrap(), + Some(&b"sk-test".to_vec()) ); - return Err(CryptoError::InsecurePermissions); } - Ok(()) } diff --git a/crates/springtale-crypto/src/vault/store/save.rs b/crates/springtale-crypto/src/vault/store/save.rs index 1f8f24c..5e3cd8c 100644 --- a/crates/springtale-crypto/src/vault/store/save.rs +++ b/crates/springtale-crypto/src/vault/store/save.rs @@ -111,9 +111,7 @@ mod tests { // Create + save a new vault via the production code path. let mut vault = Vault::create(&path, passphrase).unwrap(); - vault - .set("test-key", b"test-value".to_vec()) - .unwrap(); + vault.set("test-key", b"test-value".to_vec()).unwrap(); vault.save().unwrap(); // Round-trip into the kernel: read the file's metadata and diff --git a/crates/springtale-py/src/formation.rs b/crates/springtale-py/src/formation.rs index 255674f..1bdf203 100644 --- a/crates/springtale-py/src/formation.rs +++ b/crates/springtale-py/src/formation.rs @@ -12,7 +12,7 @@ use crate::momentum::MomentumTier; /// Lightweight Formation handle — read-only view a Python script gets /// over a known formation. Mirrors the `FormationView` gossip record /// without the live runtime hookup. -#[pyclass(frozen)] +#[pyclass(frozen, from_py_object)] #[derive(Clone, Debug)] pub struct Formation { id: FormationId, diff --git a/crates/springtale-py/src/formation_id.rs b/crates/springtale-py/src/formation_id.rs index 3fae7c1..137fbf5 100644 --- a/crates/springtale-py/src/formation_id.rs +++ b/crates/springtale-py/src/formation_id.rs @@ -8,7 +8,7 @@ use springtale_cooperation::types::FormationId as CoreFormationId; /// Formation identity. Wraps the 128-bit UUID the rest of the system /// uses; Python sees it as a string. -#[pyclass(frozen)] +#[pyclass(frozen, from_py_object)] #[derive(Clone, Debug)] pub struct FormationId { pub(crate) inner: CoreFormationId, diff --git a/crates/springtale-py/src/intent.rs b/crates/springtale-py/src/intent.rs index 168bda6..6ee51c5 100644 --- a/crates/springtale-py/src/intent.rs +++ b/crates/springtale-py/src/intent.rs @@ -11,7 +11,7 @@ use springtale_cooperation::cadence::IntentPattern as CoreIntent; /// strings — the Rust newtype layer (`TaskDescriptor`, `PlanId`, /// `StabilizeReason`, `DissolveReason`) is collapsed to `Optional[str]` /// in the Python surface so callers don't have to model every newtype. -#[pyclass(frozen)] +#[pyclass(frozen, from_py_object)] #[derive(Clone, Debug)] pub struct Intent { pub(crate) inner: CoreIntent, diff --git a/crates/springtale-py/src/lib.rs b/crates/springtale-py/src/lib.rs index efeaa2b..e883374 100644 --- a/crates/springtale-py/src/lib.rs +++ b/crates/springtale-py/src/lib.rs @@ -41,7 +41,7 @@ use springtale_cooperation::types::FormationId as CoreFormationId; /// Momentum tier — capability gate per `COOPERATION.md §7`. Python sees /// this as an enum with four members; Rust round-trips through the /// `MomentumTier::parse` / `Display` pair the rest of the system uses. -#[pyclass(eq, eq_int, frozen)] +#[pyclass(eq, eq_int, frozen, from_py_object)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum MomentumTier { Cold, @@ -76,7 +76,7 @@ impl From for CoreTier { /// strings — the Rust newtype layer (`TaskDescriptor`, `PlanId`, /// `StabilizeReason`, `DissolveReason`) is collapsed to `Optional[str]` /// in the Python surface so callers don't have to model every newtype. -#[pyclass(frozen)] +#[pyclass(frozen, from_py_object)] #[derive(Clone, Debug)] pub struct Intent { inner: CoreIntent, @@ -159,7 +159,7 @@ impl Intent { /// Formation identity. Wraps the 128-bit UUID the rest of the system /// uses; Python sees it as a string. -#[pyclass(frozen)] +#[pyclass(frozen, from_py_object)] #[derive(Clone, Debug)] pub struct FormationId { inner: CoreFormationId, @@ -213,7 +213,7 @@ impl Default for FormationId { /// Lightweight Formation handle — read-only view a Python script gets /// over a known formation. Mirrors the `FormationView` gossip record /// without the live runtime hookup. -#[pyclass(frozen)] +#[pyclass(frozen, from_py_object)] #[derive(Clone, Debug)] pub struct Formation { id: FormationId, diff --git a/crates/springtale-py/src/momentum.rs b/crates/springtale-py/src/momentum.rs index 7180f73..b7e1eee 100644 --- a/crates/springtale-py/src/momentum.rs +++ b/crates/springtale-py/src/momentum.rs @@ -7,7 +7,7 @@ use pyo3::prelude::*; /// Momentum tier — capability gate per `COOPERATION.md §7`. Python sees /// this as an enum with four members; Rust round-trips through the /// `MomentumTier::parse` / `Display` pair the rest of the system uses. -#[pyclass(eq, eq_int, frozen)] +#[pyclass(eq, eq_int, frozen, from_py_object)] #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum MomentumTier { Cold, diff --git a/crates/springtale-sentinel/src/audit/verify.rs b/crates/springtale-sentinel/src/audit/verify.rs index 58ead60..e156dce 100644 --- a/crates/springtale-sentinel/src/audit/verify.rs +++ b/crates/springtale-sentinel/src/audit/verify.rs @@ -90,12 +90,11 @@ pub async fn verify_chain( } let mut expected_prev = genesis_anchor.to_owned(); - let mut expected_seq: i64 = 1; let mut last_hash = String::new(); - let mut verified: u64 = 0; - for row in &rows { - // chain_seq must be strictly monotonic +1. + for (i, row) in rows.iter().enumerate() { + // chain_seq must be strictly monotonic +1 (1-based). + let expected_seq = i as i64 + 1; if row.chain_seq != expected_seq { return Err(VerifyError::ChainBroken(ChainBroken { row_id: row.id, @@ -137,13 +136,12 @@ pub async fn verify_chain( } expected_prev = row.row_hash.clone(); - expected_seq += 1; last_hash = row.row_hash.clone(); - verified += 1; } + // Every row reaching here passed; a failed row returns Err above. Ok(ChainOk { - rows_verified: verified, + rows_verified: rows.len() as u64, tip_hash: last_hash, }) } diff --git a/crates/springtale-store/src/backend/memory/sessions.rs b/crates/springtale-store/src/backend/memory/sessions.rs index c0fc17f..e845e2a 100644 --- a/crates/springtale-store/src/backend/memory/sessions.rs +++ b/crates/springtale-store/src/backend/memory/sessions.rs @@ -79,7 +79,7 @@ impl InMemoryBackend { .cloned() .collect(); // Sort by created_at DESC (most recent first) - matching.sort_by(|a, b| b.created_at.cmp(&a.created_at)); + matching.sort_by_key(|m| std::cmp::Reverse(m.created_at)); matching.truncate(limit); Ok(matching) } @@ -108,7 +108,7 @@ impl InMemoryBackend { .filter(|(_, m)| m.user_id == user_id && m.channel_id == channel_id) .collect(); // Sort by created_at DESC - matching.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at)); + matching.sort_by_key(|entry| std::cmp::Reverse(entry.1.created_at)); if matching.len() <= max_entries { return Ok(0); diff --git a/crates/springtale-store/tests/audit_chain.rs b/crates/springtale-store/tests/audit_chain.rs index 3cefd25..4f7491b 100644 --- a/crates/springtale-store/tests/audit_chain.rs +++ b/crates/springtale-store/tests/audit_chain.rs @@ -41,9 +41,8 @@ fn entry(connector: &str) -> AuditEntry { /// sentinel verifier without taking a dep on it from the store crate. fn verify(rows: &[AuditEntry], genesis_anchor: &str) -> Result { let mut expected_prev = genesis_anchor.to_owned(); - let mut expected_seq: i64 = 1; - let mut verified: u64 = 0; - for row in rows { + for (i, row) in rows.iter().enumerate() { + let expected_seq = i as i64 + 1; if row.chain_seq != expected_seq { return Err(format!( "chain_seq gap at row {}: expected {}, got {}", @@ -64,10 +63,8 @@ fn verify(rows: &[AuditEntry], genesis_anchor: &str) -> Result { )); } expected_prev = row.row_hash.clone(); - expected_seq += 1; - verified += 1; } - Ok(verified) + Ok(rows.len() as u64) } #[tokio::test] diff --git a/deny.toml b/deny.toml index dcbf05d..8caecee 100644 --- a/deny.toml +++ b/deny.toml @@ -31,6 +31,7 @@ ignore = [ "RUSTSEC-2025-0141", # bincode unmaintained (doxxing incident) — see vex/ "RUSTSEC-2026-0002", # lru 0.13 IterMut Stacked Borrows via chitchat — see vex/ "RUSTSEC-2026-0097", # rand 0.8.5 log-feature unsoundness — log feat disabled + "RUSTSEC-2026-0173", # proc-macro-error2 unmaintained (build-time, via tabled) — see vex/ ] [licenses] diff --git a/docs/security/CI-TRUST.md b/docs/security/CI-TRUST.md index 2dd8209..f190de7 100644 --- a/docs/security/CI-TRUST.md +++ b/docs/security/CI-TRUST.md @@ -14,12 +14,15 @@ Last review: 2026-05-13. 1. **Least privilege per job.** Every workflow declares `permissions: contents: read` at top level and elevates per-job only where needed (e.g. `id-token: write` for OIDC signing jobs). -2. **SHA-pinned actions.** Target posture: every third-party action pinned by - full commit SHA with a `# vX.Y.Z` trailing comment for human readability. - **Current state: tag-pinned.** The SHA-pin sweep across all workflows is - pending; until it lands, zizmor runs with persona `regular` (its `auditor` - persona requires SHA pins and flips on once the sweep merges — see the - note in `sast.yml`). +2. **SHA-pinned actions.** Every third-party action is pinned by full commit + SHA with a `# vX.Y.Z` trailing comment for human readability. GitHub-owned + `actions/*` and `github/*` stay tag-pinned (they carry GitHub's own + supply-chain guarantees). Two actions select behavior from the git ref name + and therefore cannot be SHA-pinned — `dtolnay/rust-toolchain@{stable,nightly}` + (the ref is the toolchain channel) and `taiki-e/install-action@{nextest,…}` + (the ref names the tool); both are documented ref-pin exceptions in + `.github/zizmor.yml`. zizmor enforces this with persona `regular` + + `min-severity: low`. 3. **Egress-controlled runners.** Every job's first step is `step-security/harden-runner` with `egress-policy: block` and a per-job `allowed-endpoints` list. @@ -54,7 +57,7 @@ workflow that uses it. | `EmbarkStudios/cargo-deny-action` | `cargo deny` runner | vendor first-party | | `rustsec/audit-check` | `cargo audit` runner | RustSec project first-party | | `pnpm/action-setup` | pnpm install | pnpm first-party | -| `gitleaks/gitleaks-action` | Secrets detection | vendor first-party | +| `gitleaks` (CLI binary) | Secrets detection — downloaded as a version+SHA256-pinned release binary, not an action (the action requires a paid license for org repos) | vendor first-party | | `trufflesecurity/trufflehog` | Secrets detection | vendor first-party | | `aquasecurity/trivy-action` | Container scan | vendor first-party | | `anchore/scan-action` | Grype container scan | vendor first-party | @@ -62,9 +65,9 @@ workflow that uses it. | `docker/setup-buildx-action` | BuildKit setup | Docker first-party | | `docker/build-push-action` | Image build/push | Docker first-party | | `hadolint/hadolint-action` | Dockerfile lint | upstream first-party | -| `returntocorp/semgrep` (container image) | Semgrep SAST — runs as a pinned container image, not an action | vendor first-party | +| `semgrep/semgrep` (container image) | Semgrep SAST — runs as a pinned container image, not an action (`returntocorp/*` was renamed to `semgrep/*`) | vendor first-party | | `raven-actions/actionlint` | Workflow YAML lint | community wrapper around upstream `rhysd/actionlint`, audited | -| `woodruffw/zizmor-action` | GitHub Actions security audit | upstream first-party | +| `zizmorcore/zizmor-action` | GitHub Actions security audit (the action moved from `woodruffw/*` to the `zizmorcore` org) | upstream first-party | | `ossf/scorecard-action` | OpenSSF Scorecard | OpenSSF first-party | | `google/osv-scanner-action` | Multi-ecosystem OSV scan | Google first-party | | `step-security/harden-runner` | Egress policy on runners | StepSecurity first-party | diff --git a/tauri/apps/dashboard/dist/.gitkeep b/tauri/apps/dashboard/dist/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tauri/apps/dashboard/public/sw.js b/tauri/apps/dashboard/public/sw.js index b938282..695d819 100644 --- a/tauri/apps/dashboard/public/sw.js +++ b/tauri/apps/dashboard/public/sw.js @@ -9,11 +9,20 @@ */ const SHELL_CACHE = "springtale-shell-v1"; -const SHELL_ASSETS = ["/", "/index.html", "/manifest.webmanifest", "/icon-192.png", "/icon-512.png"]; +const SHELL_ASSETS = [ + "/", + "/index.html", + "/manifest.webmanifest", + "/icon-192.png", + "/icon-512.png", +]; self.addEventListener("install", (event) => { event.waitUntil( - caches.open(SHELL_CACHE).then((cache) => cache.addAll(SHELL_ASSETS)).then(() => self.skipWaiting()), + caches + .open(SHELL_CACHE) + .then((cache) => cache.addAll(SHELL_ASSETS)) + .then(() => self.skipWaiting()), ); }); @@ -50,7 +59,5 @@ self.addEventListener("fetch", (event) => { return; // default network behaviour } - event.respondWith( - caches.match(event.request).then((cached) => cached || fetch(event.request)), - ); + event.respondWith(caches.match(event.request).then((cached) => cached || fetch(event.request))); }); diff --git a/tauri/apps/dashboard/src/App.tsx b/tauri/apps/dashboard/src/App.tsx index 7fd66cf..a4e46de 100644 --- a/tauri/apps/dashboard/src/App.tsx +++ b/tauri/apps/dashboard/src/App.tsx @@ -11,8 +11,8 @@ import type { import { AiConfigPanel, BottomPanel, - COMMANDS, ChatPanel, + COMMANDS, ColonyShell, ConnectorConfigPanel, MemberPickerOverlay, diff --git a/tauri/apps/dashboard/src/provider.ts b/tauri/apps/dashboard/src/provider.ts index f0a5ff4..2fbaa63 100644 --- a/tauri/apps/dashboard/src/provider.ts +++ b/tauri/apps/dashboard/src/provider.ts @@ -26,9 +26,9 @@ import type { RuleSummary, } from "@springtale/ui"; import { getCanvasState, subscribeToCanvasUpdates } from "./api/canvas"; +import { sendChatMessage, subscribeToChat } from "./api/chat"; import { del, get, getBaseUrl, getToken, post, put } from "./api/client"; import { subscribeToCooperationEvents } from "./api/cooperation"; -import { sendChatMessage, subscribeToChat } from "./api/chat"; import { subscribeToEvents } from "./api/events"; import { applyRecipe, diff --git a/tauri/apps/desktop/src/App.tsx b/tauri/apps/desktop/src/App.tsx index d68e559..f45e6c5 100644 --- a/tauri/apps/desktop/src/App.tsx +++ b/tauri/apps/desktop/src/App.tsx @@ -13,8 +13,8 @@ import { ApprovalCard, AppSettingsPanel, BottomPanel, - COMMANDS, ChatDock, + COMMANDS, ColonyShell, ConnectorConfigPanel, MemberPickerOverlay, @@ -1359,39 +1359,39 @@ export const App = () => { } viewport={
- { - setSelection({ id, type: "connector" }); - setDetailView({ mode: "entity" }); - }} - onSelectAgent={(id) => { - setSelection({ id, type: "agent" }); - setDetailView({ mode: "entity" }); - }} - onSelectFormation={(id) => { - setSelection({ id, type: "formation" }); - setDetailView({ mode: "entity" }); - }} - onClearSelection={() => setSelection({ id: null, type: null })} - connectorPositions={connectorPositions()} - onConnectorDrag={handleConnectorDrag} - onHatch={() => setShowModeSelect(true)} - availableConnectors={availableConnectors()} - connectorSchemas={db.schemas()} - onSetupConnector={(name) => { - const avail = availableConnectors().find((a) => a.name === name); - setConnectorConfigData({ id: name, config: {}, configSchema: avail?.config_schema }); - }} - onParseRule={async (intent) => db.provider.parseRuleFromIntent(intent)} - /> - {/* Floating chat dock — bottom-left, above the minimap. */} - + { + setSelection({ id, type: "connector" }); + setDetailView({ mode: "entity" }); + }} + onSelectAgent={(id) => { + setSelection({ id, type: "agent" }); + setDetailView({ mode: "entity" }); + }} + onSelectFormation={(id) => { + setSelection({ id, type: "formation" }); + setDetailView({ mode: "entity" }); + }} + onClearSelection={() => setSelection({ id: null, type: null })} + connectorPositions={connectorPositions()} + onConnectorDrag={handleConnectorDrag} + onHatch={() => setShowModeSelect(true)} + availableConnectors={availableConnectors()} + connectorSchemas={db.schemas()} + onSetupConnector={(name) => { + const avail = availableConnectors().find((a) => a.name === name); + setConnectorConfigData({ id: name, config: {}, configSchema: avail?.config_schema }); + }} + onParseRule={async (intent) => db.provider.parseRuleFromIntent(intent)} + /> + {/* Floating chat dock — bottom-left, above the minimap. */} +
} bottomPanel={ diff --git a/tauri/apps/desktop/src/provider.ts b/tauri/apps/desktop/src/provider.ts index 7b42687..72fac1b 100644 --- a/tauri/apps/desktop/src/provider.ts +++ b/tauri/apps/desktop/src/provider.ts @@ -52,7 +52,6 @@ import { deployTeam, dissolveFormation, formationAvailableCommands, - runFormationCommand, formationEligibleMembers, getFormation, listFormations, @@ -61,6 +60,7 @@ import { rallyFormation, removeFormationMember, resumeFormation, + runFormationCommand, updateFormationIntent, } from "./ipc/formations"; import { applyOnboarding, listOnboardingPlatforms } from "./ipc/onboarding"; diff --git a/tauri/packages/types/src/generated/FormationDelta.ts b/tauri/packages/types/src/generated/FormationDelta.ts index a50fe1e..bcbff71 100644 --- a/tauri/packages/types/src/generated/FormationDelta.ts +++ b/tauri/packages/types/src/generated/FormationDelta.ts @@ -7,4 +7,6 @@ import type { FormationView } from "./FormationView"; * snapshot stream and the terminal-outcome stream so subscribers don't * have to wire two channels. */ -export type FormationDelta = { "kind": "view" } & FormationView | { "kind": "outcome" } & FormationOutcome; +export type FormationDelta = + | ({ kind: "view" } & FormationView) + | ({ kind: "outcome" } & FormationOutcome); diff --git a/tauri/packages/types/src/generated/FormationOutcome.ts b/tauri/packages/types/src/generated/FormationOutcome.ts index 6f0ddca..32dbc88 100644 --- a/tauri/packages/types/src/generated/FormationOutcome.ts +++ b/tauri/packages/types/src/generated/FormationOutcome.ts @@ -5,4 +5,11 @@ * finished" awareness for sibling formations on the same connector * graph and feeds the global mental-model persistence layer (G2). */ -export type FormationOutcome = { formation_id: string, final_intent: string, success_count: number, failure_count: number, dissolve_reason: string, at: string, }; +export type FormationOutcome = { + formation_id: string; + final_intent: string; + success_count: number; + failure_count: number; + dissolve_reason: string; + at: string; +}; diff --git a/tauri/packages/types/src/generated/FormationView.ts b/tauri/packages/types/src/generated/FormationView.ts index d82c5b9..896ace8 100644 --- a/tauri/packages/types/src/generated/FormationView.ts +++ b/tauri/packages/types/src/generated/FormationView.ts @@ -6,4 +6,13 @@ import type { FormationStatus } from "./FormationStatus"; * whenever the formation's intent changes, momentum tier flips, or * rally tokens cross a threshold. */ -export type FormationView = { formation_id: string, intent: string, momentum_tier: string, operational_count: number, member_count: number, rally_tokens_remaining: number, status: FormationStatus, at: string, }; +export type FormationView = { + formation_id: string; + intent: string; + momentum_tier: string; + operational_count: number; + member_count: number; + rally_tokens_remaining: number; + status: FormationStatus; + at: string; +}; diff --git a/tauri/packages/ui/src/colony/ChatPanel.tsx b/tauri/packages/ui/src/colony/ChatPanel.tsx index 6c8fb37..e603dc5 100644 --- a/tauri/packages/ui/src/colony/ChatPanel.tsx +++ b/tauri/packages/ui/src/colony/ChatPanel.tsx @@ -101,19 +101,13 @@ export const ChatPanel: Component = (props) => { >

- Ask me anything — get the weather, research a topic, scrape a page, - or make a change. Reads are instant; changes ask you first. + Ask me anything — get the weather, research a topic, scrape a page, or make a change. + Reads are instant; changes ask you first.

{(turn) => ( -
+
= (props) => diff --git a/tauri/packages/ui/src/index.ts b/tauri/packages/ui/src/index.ts index 086cd05..6813368 100644 --- a/tauri/packages/ui/src/index.ts +++ b/tauri/packages/ui/src/index.ts @@ -25,10 +25,10 @@ export type { ApprovalCardProps } from "./colony/ApprovalCard"; export { ApprovalCard } from "./colony/ApprovalCard"; export { AppSettingsPanel } from "./colony/AppSettingsPanel"; export { BottomPanel } from "./colony/BottomPanel"; -export { ChatDock } from "./colony/ChatDock"; export type { ChatDockProps } from "./colony/ChatDock"; -export { ChatPanel } from "./colony/ChatPanel"; +export { ChatDock } from "./colony/ChatDock"; export type { ChatPanelProps } from "./colony/ChatPanel"; +export { ChatPanel } from "./colony/ChatPanel"; export { ColonyCanvas } from "./colony/ColonyCanvas"; // Colony layout components export { ColonyShell } from "./colony/ColonyShell"; diff --git a/vex/proc-macro-error2-rustsec-2026-0173.json b/vex/proc-macro-error2-rustsec-2026-0173.json new file mode 100644 index 0000000..70fbb6a --- /dev/null +++ b/vex/proc-macro-error2-rustsec-2026-0173.json @@ -0,0 +1,30 @@ +{ + "@context": "https://openvex.dev/ns/v0.2.0", + "@id": "https://springtale.dev/vex/proc-macro-error2-rustsec-2026-0173.json", + "author": "Springtale Maintainers ", + "timestamp": "2026-06-11T00:00:00Z", + "version": 1, + "statements": [ + { + "vulnerability": { + "@id": "https://rustsec.org/advisories/RUSTSEC-2026-0173", + "name": "RUSTSEC-2026-0173", + "description": "proc-macro-error2 2.0.1 — author confirmed the crate is unmaintained (2026-06-07) and recommends migrating away" + }, + "products": [ + { + "@id": "pkg:cargo/proc-macro-error2@2.0.1", + "subcomponents": [ + { "@id": "pkg:cargo/tabled_derive@0.9.0" }, + { "@id": "pkg:cargo/tabled@0.17.0" }, + { "@id": "pkg:cargo/springtale-cli@0.1.0" } + ] + } + ], + "status": "affected", + "justification": "vulnerable_code_not_present", + "impact_statement": "RUSTSEC-2026-0173 is an unmaintained-crate informational advisory, not a vulnerability — there is no CVE and no exploitable code path. proc-macro-error2 is a procedural-macro support crate that executes only at compile time (inside `tabled_derive`'s derive expansion); none of its code is linked into the shipped `springtale-cli` binary. The transitive path is tabled_derive -> tabled, used solely for human-readable table rendering in the CLI. Even a hypothetical defect in the crate could not affect a built artifact, as the crate is absent from the runtime dependency closure.", + "action_statement": "Tracking `tabled` upstream for a release that drops proc-macro-error2 (the maintained successors are `manyhow` / `proc-macro2-diagnostics`). We will take the bump once tabled migrates; no action is required in the interim because the crate is build-time only and carries no runtime risk." + } + ] +}