diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..f5c91ceed6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,6 @@ +# The dashboard SPA bundle under internal/api/dashboardspa/dist is emitted by the +# Vite build and embedded into the gc binary. Treat it as generated output: skip +# textual diffing and the whitespace lint (`git diff --check`) on the minified +# assets, and collapse it in review UIs. Drift is still caught by the content-based +# `git diff --quiet -- internal/api/dashboardspa/dist` gate in the Makefile. +internal/api/dashboardspa/dist/** -diff linguist-generated diff --git a/.github/requirements/mcp-agent-mail.in b/.github/requirements/mcp-agent-mail.in index bef25528c9..67fa1e2512 100644 --- a/.github/requirements/mcp-agent-mail.in +++ b/.github/requirements/mcp-agent-mail.in @@ -25,6 +25,12 @@ idna>=3.15 # transitive constraint carries the patched version itself. litellm>=1.84.0 +# Security floor: CVE-2026-49852 (HIGH, HS256/HS384/HS512 verify accepts an +# empty/nil HMAC key — cross-language sibling of CVE-2026-45363) in joserfc +# < 1.6.8. joserfc arrives transitively via authlib; pin the floor until +# authlib's transitive constraint carries the patched version itself. +joserfc>=1.6.8 + # Authlib and FastMCP security floors live in # .github/requirements/mcp-agent-mail.overrides.txt because mcp-agent-mail # v0.3.2 still caps Authlib below the fixed release. diff --git a/.github/requirements/mcp-agent-mail.txt b/.github/requirements/mcp-agent-mail.txt index 2f3ed6bcbd..716f55ce3d 100644 --- a/.github/requirements/mcp-agent-mail.txt +++ b/.github/requirements/mcp-agent-mail.txt @@ -1211,10 +1211,12 @@ jiter==0.14.0 \ --hash=sha256:ff3a6465b3a0f54b1a430f45c3c0ba7d61ceb45cbc3e33f9e1a7f638d690baf3 \ --hash=sha256:ffb2a08a406465bb076b7cc1df41d833106d3cf7905076cc73f0cb90078c7d10 # via openai -joserfc==1.6.5 \ - --hash=sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48 \ - --hash=sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e - # via authlib +joserfc==1.7.2 \ + --hash=sha256:537ffb8888b2df039cb5b6d017d7cff6f09d521ce65d89cc9b8ab752b1cff947 \ + --hash=sha256:ddd818c0ca9b4f17bbc2d72cb3966e6ded7502be089316c62c3cc64ae86132b5 + # via + # -r .github/requirements/mcp-agent-mail.in + # authlib jsonref==1.1.0 \ --hash=sha256:32fe8e1d85af0fdefbebce950af85590b22b60f9e95443176adbde4e1ecea552 \ --hash=sha256:590dc7773df6c21cbf948b5dac07a72a251db28b0238ceecce0a2abfa8ec30a9 diff --git a/.github/scripts/install-bd-archive.sh b/.github/scripts/install-bd-archive.sh index 99c7bd6353..e5aeafdffd 100755 --- a/.github/scripts/install-bd-archive.sh +++ b/.github/scripts/install-bd-archive.sh @@ -57,6 +57,10 @@ version_no_v="${version#v}" platform_tuple="${os}_${arch}" expected_sha="" case "${version}:${platform_tuple}" in + v1.1.0:linux_amd64) expected_sha="b0f3dd607c3fb989ee08d0a6854fba80d0402971eb108f9af6170bc14d491a34" ;; + v1.1.0:linux_arm64) expected_sha="e64eb6f5f998c9eae3ef9ec786f5f1c907ab3ed04fe220ebf265ca9952e21b2f" ;; + v1.1.0:darwin_amd64) expected_sha="5d7d30fdadcf012b7e0c1933a62cdfaef106e2561509b904e50a6733621cf8da" ;; + v1.1.0:darwin_arm64) expected_sha="c42e24d83b258f7ba9f52a6d2d5f6b055869dfe7807165055988b12e7ea8c564" ;; v1.0.5:linux_amd64) expected_sha="24706f65c7131c7b3261388709ae8781c8db53f0795398f67aa40538750aacf3" ;; v1.0.5:linux_arm64) expected_sha="ccae5eb4478876ae224687ba98baef46848e603470b241966b63ccd3e01129a4" ;; v1.0.5:darwin_amd64) expected_sha="0b0b017a3f2b23a1a9b53056ff160de318ebbca6a991c3db5924f5f48390e490" ;; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b320f88f8a..991ae4dff3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -175,7 +175,7 @@ jobs: runs-on: ${{ needs.runner-policy.outputs.runner_16vcpu }} env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: ./.github/actions/setup-gascity-ubuntu @@ -191,20 +191,28 @@ jobs: run: make check-native-dependency-surface - name: Event-export isolation guard run: make check-eventexport-isolation + - name: Open-core boundary guard + run: make check-core-boundary - name: Native DoltLite beads tests run: make test-native-doltlite-beads - - name: Get golangci-lint version for cache key + - name: Get golangci-lint + Go toolchain versions for cache key id: glint-version run: | version=$(grep -m1 '^GOLANGCI_LINT_VERSION := ' Makefile | awk '{print $3}') echo "version=${version}" >> "$GITHUB_OUTPUT" + # staticcheck (bundled in golangci-lint) caches type/CFG facts whose + # validity depends on the Go toolchain it was built with. Keying the + # cache on GOVERSION prevents a cache populated under one Go toolchain + # from being replayed under another, which produced a stale-fact + # SA5011 false-positive flood on main (ga-v7x9vk). + echo "goversion=$(go env GOVERSION)" >> "$GITHUB_OUTPUT" - name: Restore golangci-lint cache uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/golangci-lint - key: ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}-${{ hashFiles('go.sum', '.golangci.yml') }} + key: ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}-${{ steps.glint-version.outputs.goversion }}-${{ hashFiles('go.sum', '.golangci.yml') }} restore-keys: | - ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}- + ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}-${{ steps.glint-version.outputs.goversion }}- - name: Lint env: GOLANGCI_LINT_CACHE: ${{ github.workspace }}/.cache/golangci-lint @@ -229,7 +237,7 @@ jobs: runs-on: ${{ needs.runner-policy.outputs.runner_32vcpu }} env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: ./.github/actions/setup-gascity-ubuntu @@ -258,7 +266,7 @@ jobs: runs-on: ${{ needs.runner-policy.outputs.runner_32vcpu }} env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" strategy: fail-fast: false matrix: @@ -290,7 +298,7 @@ jobs: runs-on: ${{ needs.runner-policy.outputs.runner_32vcpu }} env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: ./.github/actions/setup-gascity-ubuntu @@ -490,7 +498,7 @@ jobs: shard: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: ./.github/actions/setup-gascity-ubuntu @@ -622,7 +630,7 @@ jobs: command: ./scripts/test-integration-shard rest-full-16-of-16 env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: ./.github/actions/setup-gascity-ubuntu @@ -1123,7 +1131,7 @@ jobs: GC_TEST_GASCITY_PACKS_REGISTRY: main env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" # Dashboard SPA typecheck + tests + build. Runs on every push/PR # so TS drift against the spec (e.g. a query param tightening from @@ -1150,6 +1158,9 @@ jobs: - name: Typecheck (tsc --noEmit) run: npm run typecheck working-directory: internal/api/dashboardspa/web + - name: Typecheck test files (tsc -p tsconfig.test.json) + run: npm run --workspace gas-city-dashboard-frontend typecheck:test + working-directory: internal/api/dashboardspa/web - name: Vitest run: npm run --workspace gas-city-dashboard-frontend test working-directory: internal/api/dashboardspa/web diff --git a/.github/workflows/fork-verify.yml b/.github/workflows/fork-verify.yml index 21a17d4801..4c5d0b46df 100644 --- a/.github/workflows/fork-verify.yml +++ b/.github/workflows/fork-verify.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - uses: ./.github/actions/setup-gascity-ubuntu diff --git a/.github/workflows/mac-regression.yml b/.github/workflows/mac-regression.yml index 31eca67764..2c209a3ed1 100644 --- a/.github/workflows/mac-regression.yml +++ b/.github/workflows/mac-regression.yml @@ -75,7 +75,7 @@ concurrency: env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" # Trigger gate re-used by every job below via `if:`. # We want each job to run when EITHER: @@ -137,18 +137,24 @@ jobs: install-claude-cli: "false" - name: Install tools run: make install-tools - - name: Get golangci-lint version for cache key + - name: Get golangci-lint + Go toolchain versions for cache key id: glint-version run: | version=$(grep -m1 '^GOLANGCI_LINT_VERSION := ' Makefile | awk '{print $3}') echo "version=${version}" >> "$GITHUB_OUTPUT" + # staticcheck (bundled in golangci-lint) caches type/CFG facts whose + # validity depends on the Go toolchain it was built with. Keying the + # cache on GOVERSION prevents a cache populated under one Go toolchain + # from being replayed under another, which produced a stale-fact + # SA5011 false-positive flood on main (ga-v7x9vk). + echo "goversion=$(go env GOVERSION)" >> "$GITHUB_OUTPUT" - name: Restore golangci-lint cache uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4 with: path: .cache/golangci-lint - key: ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}-${{ hashFiles('go.sum', '.golangci.yml') }} + key: ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}-${{ steps.glint-version.outputs.goversion }}-${{ hashFiles('go.sum', '.golangci.yml') }} restore-keys: | - ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}- + ${{ runner.os }}-golangci-lint-${{ steps.glint-version.outputs.version }}-${{ steps.glint-version.outputs.goversion }}- - name: Lint env: GOLANGCI_LINT_CACHE: ${{ github.workspace }}/.cache/golangci-lint diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index f8ecf14600..4fa3dc0ffd 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -10,7 +10,7 @@ permissions: env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" jobs: tier-b: @@ -89,7 +89,7 @@ jobs: timeout-minutes: 30 env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" GC_BEADS: sqlite GC_ACCEPTANCE_BEADS_PROVIDER: sqlite steps: diff --git a/.github/workflows/notify-image-build.yaml b/.github/workflows/notify-image-build.yaml index 5b6360a32c..08ac3de743 100644 --- a/.github/workflows/notify-image-build.yaml +++ b/.github/workflows/notify-image-build.yaml @@ -1,10 +1,12 @@ -# Notify gasworks-internal to rebuild the gc-runtime image when Go source changes. +# Notify gasworks-control-plane to rebuild the gc-runtime image when Go source +# changes. # # gascity provides the gc binary embedded in runtime images. # When source code changes, images need rebuilding. # -# Required secret: GASCITY_HOSTED_TOKEN — PAT with repo scope for -# gascity/gasworks-internal (needed for repository_dispatch). +# Required secret: GASCITY_HOSTED_TOKEN — a token with contents:write on +# gascity/gasworks-control-plane (needed for repository_dispatch). A +# fine-grained PAT scoped to that repo, or a classic PAT with repo scope. name: Notify Image Rebuilds @@ -33,5 +35,5 @@ jobs: run: | gh api \ --method POST \ - repos/gascity/gasworks-internal/dispatches \ + repos/gascity/gasworks-control-plane/dispatches \ -f event_type=runtime-dep-updated diff --git a/.github/workflows/ollama-acceptance-c.yml b/.github/workflows/ollama-acceptance-c.yml index 5fbbcb51d2..4a744506e0 100644 --- a/.github/workflows/ollama-acceptance-c.yml +++ b/.github/workflows/ollama-acceptance-c.yml @@ -8,7 +8,7 @@ permissions: env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" ANTHROPIC_BASE_URL: https://ollama.com ANTHROPIC_API_KEY: "" ANTHROPIC_AUTH_TOKEN: ${{ secrets.OLLAMA_API_KEY }} diff --git a/.github/workflows/rc-gate.yml b/.github/workflows/rc-gate.yml index 33eebcf95a..d4537c5260 100644 --- a/.github/workflows/rc-gate.yml +++ b/.github/workflows/rc-gate.yml @@ -8,7 +8,7 @@ permissions: env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" # Bypass the systemd gascity-test.slice re-exec on CI runners. The Blacksmith # runners' user systemd cannot reliably create a transient --scope for the # heavy acceptance workloads; gc_test_slice_reexec then exec's diff --git a/.github/workflows/review-formulas.yml b/.github/workflows/review-formulas.yml index 425dcb04f6..ddc851c8a8 100644 --- a/.github/workflows/review-formulas.yml +++ b/.github/workflows/review-formulas.yml @@ -37,7 +37,7 @@ concurrency: env: DOLT_VERSION: "2.1.7" - BD_VERSION: "v1.0.4" + BD_VERSION: "v1.1.0" jobs: runner-policy: diff --git a/.trivyignore.yaml b/.trivyignore.yaml index 10de688f66..1f5372e1ad 100644 --- a/.trivyignore.yaml +++ b/.trivyignore.yaml @@ -1,4 +1,14 @@ vulnerabilities: + # Expiry horizon bulk-extended 2026-07-06: 2026-07-07 -> 2026-08-07. Re-audit + # confirmed every waived upstream is still pinned at its vulnerable version, so + # nothing is droppable yet: dolt v2.1.7 (Go 1.26.2 stdlib + old + # x/net/x/crypto/thrift), bundled bd v1.1.0 (beads repin: x-net/x-crypto; + # thrift+go-jose cleared by the v1.1.0 rebuild), kubectl base binary (external + # x/net), and gc (go.mod still + # golang.org/x/net v0.52.0 / golang.org/x/crypto v0.49.0). gh already cleared + # by cli/cli v2.94.0. Drop each entry per its own `statement:` once that + # upstream actually rebuilds. + # # Go stdlib CVEs disclosed 2026-05-12. Fixed in Go 1.25.10 / 1.26.3 for # the first batch (33811–42499); Go 1.26.4 / 1.25.11 for CVE-2026-42504. # As of 2026-06-15: cli/cli v2.94.0 ships Go 1.26.4 (clears all entries); @@ -13,7 +23,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-33814 paths: @@ -21,7 +31,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-39820 paths: @@ -29,7 +39,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-39823 paths: @@ -37,7 +47,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-39825 paths: @@ -45,7 +55,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-39826 paths: @@ -53,7 +63,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-39836 paths: @@ -61,7 +71,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-42499 paths: @@ -69,7 +79,7 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Upstream bd, br, dolt, and kubectl embed Go 1.26.2 stdlib; remove once each rebuilds against 1.26.3+ (or 1.25.10+). gh cleared by v2.94.0 (Go 1.26.4) on base-image rebuild. - id: CVE-2026-42504 paths: @@ -77,122 +87,111 @@ vulnerabilities: - "usr/local/bin/br" - "usr/local/bin/dolt" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Go stdlib MIME-header DoS (CVE-2026-42504), fixed in Go 1.26.4 / 1.25.11. bd, br, dolt, and kubectl still embed an older Go stdlib; remove once each rebuilds against 1.26.4+. gh cleared by v2.94.0 on rebuild; gc cleared by go.mod bump to 1.26.4 (PR #3297). - id: CVE-2026-27145 paths: - "usr/local/bin/bd" - expired_at: 2026-07-07 - statement: Go stdlib x509 hostname verification issue (CVE-2026-27145), fixed in Go 1.26.4 / 1.25.11. bd v1.0.4 is the deliberate beads repin; remove once the bundled CLI rebuilds against 1.26.4+. + expired_at: 2026-08-07 + statement: Go stdlib x509 hostname verification issue (CVE-2026-27145), fixed in Go 1.26.4 / 1.25.11. bd v1.1.0 is the deliberate beads repin (still built with Go 1.26.2); remove once the bundled CLI rebuilds against 1.26.4+. - id: CVE-2026-27145 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Go stdlib x509 hostname verification issue (CVE-2026-27145), fixed in Go 1.26.4 / 1.25.11. Dolt v2.1.7 still embeds Go 1.26.2; remove once upstream rebuilds against 1.26.4+. - id: CVE-2026-27145 paths: - "usr/local/bin/kubectl" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Go stdlib x509 hostname verification issue (CVE-2026-27145), fixed in Go 1.26.4 / 1.25.11. kubectl in the base image still embeds Go 1.26.2; remove once the bundled CLI rebuilds against 1.26.4+. - id: CVE-2026-41602 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Latest Dolt 1.88.0 still embeds github.com/apache/thrift v0.13.1; remove after a Dolt release includes thrift 0.23.0 or later. - id: CVE-2026-25680 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles a Go module set with this x/net HTML parsing issue; remove once upstream rebuilds against the fixed x/net release. - id: CVE-2026-25681 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles a Go module set with this x/net HTML rendering issue; remove once upstream rebuilds against the fixed x/net release. - id: CVE-2026-27136 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles a Go module set with this x/net HTML rendering issue; remove once upstream rebuilds against the fixed x/net release. - id: CVE-2026-39821 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles a Go module set with this x/net idna issue; remove once upstream rebuilds against the fixed x/net release. - id: CVE-2026-42502 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles a Go module set with this x/net HTML rendering issue; remove once upstream rebuilds against the fixed x/net release. - id: CVE-2026-42506 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles a Go module set with this x/net HTML rendering issue; remove once upstream rebuilds against the fixed x/net release. - id: CVE-2026-39827 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-39828 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-39829 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-39830 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 + statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. + - id: CVE-2026-39831 + paths: + - "usr/local/bin/dolt" + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-39832 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-39835 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-42508 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-46595 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - id: CVE-2026-46597 paths: - "usr/local/bin/dolt" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: Dolt v2.1.7 still bundles golang.org/x/crypto v0.48.0; remove once upstream rebuilds against the fixed release. - # HIGH severity, fixed upstream. Both CVEs below are present ONLY via the - # deliberate steveyegge/beads v1.0.4 repin (v1.0.5 has data corruption) in the - # bundled bd CLI binary. bd is a local CLI: the affected apache/thrift and - # go-jose/go-jose/v4 code paths are non-network in this usage (no thrift RPC - # server, no JWE handling). Waiver expires when gascity moves off bd 1.0.4 - # (tracking: move gascity off bd v1.0.4). Do not bump bd here. - - id: CVE-2026-41602 - paths: - - "usr/local/bin/bd" - expired_at: 2026-07-07 - statement: "HIGH, fixed upstream (apache/thrift). Present only via the deliberate steveyegge/beads v1.0.4 repin (v1.0.5 has data corruption), in the bundled bd CLI binary on non-network thrift paths. Remove when gascity moves off bd 1.0.4 (tracking: move gascity off bd v1.0.4)." - - id: CVE-2026-34986 - paths: - - "usr/local/bin/bd" - expired_at: 2026-07-07 - statement: "HIGH, fixed upstream (go-jose/go-jose/v4). Present only via the deliberate steveyegge/beads v1.0.4 repin (v1.0.5 has data corruption), in the bundled bd CLI binary on non-network JWE paths. Remove when gascity moves off bd 1.0.4 (tracking: move gascity off bd v1.0.4)." # The golang.org/x/net (HTML/idna/http2) and golang.org/x/crypto/ssh CVEs in # the same series the dolt entries above waive are also reported against the - # bd CLI binary (steveyegge/beads v1.0.4 repin, external), the gc binary + # bd CLI binary (steveyegge/beads v1.1.0, external), the gc binary # (indirect golang.org/x/net v0.52.0 / golang.org/x/crypto v0.49.0), and the # external kubectl binary (golang.org/x/net v0.49.0; the gc-controller image # adds kubectl on top of the agent image). With set -e the Container Scan @@ -208,99 +207,105 @@ vulnerabilities: - "usr/local/bin/bd" - "usr/local/bin/gc" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 - statement: golang.org/x/net HTML parsing DoS; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. + expired_at: 2026-08-07 + statement: golang.org/x/net HTML parsing DoS; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. - id: CVE-2026-25681 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 - statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. + expired_at: 2026-08-07 + statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. - id: CVE-2026-27136 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 - statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. + expired_at: 2026-08-07 + statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. - id: CVE-2026-39821 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 - statement: golang.org/x/net/idna issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. + expired_at: 2026-08-07 + statement: golang.org/x/net/idna issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. - id: CVE-2026-42502 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 - statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. + expired_at: 2026-08-07 + statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. - id: CVE-2026-42506 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - "usr/local/bin/kubectl" - expired_at: 2026-07-07 - statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. + expired_at: 2026-08-07 + statement: golang.org/x/net HTML rendering issue; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0), gc (indirect x/net v0.52.0), and kubectl (external, x/net v0.49.0). Remove once bd/kubectl rebuild upstream and gc bumps golang.org/x/net >= 0.55.0. - id: CVE-2026-33814 paths: - "usr/local/bin/gc" - expired_at: 2026-07-07 + expired_at: 2026-08-07 statement: golang.org/x/net http2 issue; base-pre-existing (also red on main 2026-06-24). gc only (indirect x/net v0.52.0); bd/br/dolt/kubectl already covered by the stdlib waiver above. Remove once gc bumps golang.org/x/net >= 0.53.0. - id: CVE-2026-39827 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-39828 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-39829 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-39830 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + - id: CVE-2026-39831 + paths: + - "usr/local/bin/bd" + - "usr/local/bin/gc" + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-39832 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh/agent CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh/agent CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-39835 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-42508 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh/knownhosts CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh/knownhosts CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-46595 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. - id: CVE-2026-46597 paths: - "usr/local/bin/bd" - "usr/local/bin/gc" - expired_at: 2026-07-07 - statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.0.4 repin) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. + expired_at: 2026-08-07 + statement: golang.org/x/crypto/ssh CVE; base-pre-existing (also red on main 2026-06-24). Present in bd (beads v1.1.0) and gc (indirect x/crypto v0.49.0). Remove once bd rebuilds and gc bumps golang.org/x/crypto >= 0.52.0. diff --git a/Makefile b/Makefile index 5ae90c2dfb..ff0820ec14 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ endif endif endif -.PHONY: build check check-all check-bd check-docker check-docs check-dolt check-eventexport-isolation check-gomod-replace check-native-dependency-surface check-routed-test-rows check-version-tag lint lint-full lint-new lint-changed fmt-check fmt vet test test-mac test-fast-parallel test-fsys-darwin-compile test-pack-registry-live test-native-doltlite-beads test-cmd-gc-process test-cmd-gc-process-shard test-cmd-gc-process-parallel test-worker-core test-worker-core-phase2 test-worker-core-phase2-real-transport setup-worker-inference test-worker-inference test-worker-inference-phase3 test-acceptance test-acceptance-b test-acceptance-c test-acceptance-all test-tutorial-goldens test-tutorial-regression test-tutorial test-integration test-integration-shards test-integration-shards-parallel test-integration-shards-cover test-integration-packages test-integration-packages-cover test-integration-review-formulas test-integration-review-formulas-cover test-integration-review-formulas-basic test-integration-review-formulas-basic-cover test-integration-review-formulas-retries test-integration-review-formulas-retries-cover test-integration-review-formulas-recovery test-integration-review-formulas-recovery-cover test-integration-bdstore test-integration-bdstore-cover test-integration-rest test-integration-rest-cover test-integration-rest-smoke test-integration-rest-smoke-cover test-integration-rest-full test-integration-rest-full-cover test-local-full-parallel test-mail-wisp-insert test-mcp-mail test-openclaw-bridge test-docker test-k8s test-cover test-cover-mac test-cover-noncmdgc test-cover-cmdgc-shard cover install install-tools install-buildx setup clean generate check-schema docker-base docker-agent docker-controller docs-dev diagrams-excalidraw dashboard-smoke +.PHONY: build check check-all check-bd check-docker check-docs check-dolt check-eventexport-isolation check-gomod-replace check-core-boundary check-native-dependency-surface check-routed-test-rows check-version-tag lint lint-full lint-new lint-changed fmt-check fmt vet test test-mac test-fast-parallel test-fsys-darwin-compile test-pack-registry-live test-native-doltlite-beads test-cmd-gc-process test-cmd-gc-process-shard test-cmd-gc-process-parallel test-worker-core test-worker-core-phase2 test-worker-core-phase2-real-transport setup-worker-inference test-worker-inference test-worker-inference-phase3 test-acceptance test-acceptance-b test-acceptance-c test-acceptance-all test-tutorial-goldens test-tutorial-regression test-tutorial test-integration test-integration-shards test-integration-shards-parallel test-integration-shards-cover test-integration-packages test-integration-packages-cover test-integration-review-formulas test-integration-review-formulas-cover test-integration-review-formulas-basic test-integration-review-formulas-basic-cover test-integration-review-formulas-retries test-integration-review-formulas-retries-cover test-integration-review-formulas-recovery test-integration-review-formulas-recovery-cover test-integration-bdstore test-integration-bdstore-cover test-integration-rest test-integration-rest-cover test-integration-rest-smoke test-integration-rest-smoke-cover test-integration-rest-full test-integration-rest-full-cover test-local-full-parallel test-mail-wisp-insert test-mcp-mail test-openclaw-bridge test-docker test-k8s test-cover test-cover-mac test-cover-noncmdgc test-cover-cmdgc-shard cover install install-tools install-buildx setup clean generate check-schema docker-base docker-agent docker-controller docs-dev diagrams-excalidraw dashboard-smoke ## build: compile gc binary with version metadata build: @@ -127,6 +127,10 @@ check-routed-test-rows: check-gomod-replace: bash scripts/check-gomod-replace.sh go.mod +## check-core-boundary: guard the open-core boundary (no commercial coupling in the OSS module) +check-core-boundary: + bash scripts/check-core-boundary.sh + ## check-native-dependency-surface: guard native beads dependency and binary growth check-native-dependency-surface: bash scripts/check-native-dependency-surface.sh @@ -711,9 +715,9 @@ dashboard-build: dashboard-dev: cd internal/api/dashboardspa/web && npm run --workspace gas-city-dashboard-frontend dev -## dashboard-check: typecheck + build the SPA, then go test the embedded handler + BFF +## dashboard-check: typecheck (src + test files) + build the SPA, then go test the embedded handler + BFF dashboard-check: dashboard-build - cd internal/api/dashboardspa/web && npm run typecheck + cd internal/api/dashboardspa/web && npm run typecheck && npm run --workspace gas-city-dashboard-frontend typecheck:test $(TEST_ENV) go test ./internal/api/dashboardspa/... ./internal/api/dashboardbff/... ## dashboard-smoke: serve the built SPA bundle via Vite preview and verify it responds diff --git a/README.md b/README.md index dd279e3316..3628dbdff1 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ trying to port the entire Town architecture literally. ## What You Get - Declarative city configuration in `city.toml` -- Multiple runtime providers: tmux, subprocess, exec, ACP, and Kubernetes +- Multiple runtime providers: tmux, subprocess, exec, ACP, Kubernetes, and herdr - Beads-backed work tracking, formulas, molecules, waits, and mail - A controller/supervisor loop that reconciles desired state to running state - Packs, overrides, and rig-scoped orchestration for multi-project setups @@ -62,6 +62,12 @@ Gas City requires the following tools on your system. `gc init` and | gh | Optional GitHub gates | — | `brew install gh` | [cli.github.com](https://cli.github.com/) | | claude / codex / gemini | Per provider | — | See provider docs | See provider docs | +tmux is the default session backend **and** the fallback, so it stays required +even if you run agents on another backend. [herdr](https://herdr.dev) is an +optional alternative backend — see +[herdr Session Provider](docs/reference/herdr-provider.md) to enable it +per-agent, per-rig, or city-wide. + The `bd` (beads) provider is the default. To use a file-based store instead (no dolt/bd/flock needed), set `GC_BEADS=file` or add `[beads] provider = "file"` to your `city.toml`. @@ -158,7 +164,7 @@ make docs-dev | Path | What it contains | |---|---| | `cmd/gc/` | CLI entrypoints, controller wiring, runtime assembly, and command handlers | -| `internal/runtime/` | Runtime provider abstraction plus tmux, subprocess, exec, ACP, K8s, and hybrid implementations | +| `internal/runtime/` | Runtime provider abstraction plus tmux, subprocess, exec, ACP, K8s, hybrid, and herdr implementations | | `internal/config/` | `city.toml` schema, validation, composition, packs, patches, and override resolution | | `internal/beads/` | Store abstraction and provider implementations for beads (work, mail, convoys) and waits | | `internal/session/` | Session bead metadata, wait lifecycle helpers, and session identity utilities | diff --git a/SECURITY.md b/SECURITY.md index 919ee2b3ff..2dd1e07c40 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,6 +25,32 @@ security advisory. If a fix is needed, it will be released before public disclosure unless there is an active exploitation risk that requires faster notice. +## Malicious "fix" attachments in issues, PRs, and discussions + +Automated spam accounts have been posting on newly-opened issues across GitHub — +including in this project — with a friendly, issue-specific message and an +attached archive (for example a file named like `*_fix.zip`, `fix_win.zip`, or a +"patched build") that claims to solve your problem. **These files are malware. Do +not download or run them.** + +How to stay safe: + +- **Official builds and releases come only from this repository's + [Releases](https://github.com/gastownhall/gascity/releases) page** and the project's documented install + instructions. Maintainers will never ask you to download a zip or executable + posted in an issue, pull request, or discussion comment. +- A link that points to `github.com/user-attachments/files/...` is a file + someone attached to a comment — it is **not** a vetted release asset, even + though the URL is hosted on `github.com`. +- Be especially wary of a brand-new account offering a "fix" as a download + within minutes of your post, or telling you to run an install command for a + package or module that is not an official project source. + +If you see one of these comments, please **report it** (the comment's `...` menu +→ *Report content*) and do not click the attachment. Note that deleting the +comment does not remove the uploaded file from GitHub's servers, so also report +the attachment to GitHub Support so it can be taken down. + ## Supported Versions Security fixes target the current stable major release unless a separate support diff --git a/cmd/gc/adoption_barrier.go b/cmd/gc/adoption_barrier.go index bf46f050e8..85fe299da7 100644 --- a/cmd/gc/adoption_barrier.go +++ b/cmd/gc/adoption_barrier.go @@ -54,7 +54,7 @@ var poolSlotPattern = regexp.MustCompile(`-(\d+)$`) // sessions have beads). func runAdoptionBarrier( cityPath string, - sessFront *sessionpkg.InfoStore, + sessFront *sessionpkg.Store, sp runtime.Provider, cfg *config.City, cityName string, @@ -67,9 +67,9 @@ func runAdoptionBarrier( if sessFront == nil { return result, false } - // Session-bead list queries below go through the raw store the front door - // wraps (sessionpkg.ListAllSessionBeads takes a beads.Store); creates go - // through the front door. Same underlying store, so behavior is unchanged. + // Session-bead list queries below go through the raw session-class store the + // front door wraps (sessionpkg.ListAllSessionBeads takes a raw store); creates + // go through the front door. Same underlying store, so behavior is unchanged. store := sessFront.Store().Store // Step 1: List all running sessions. @@ -103,7 +103,7 @@ func runAdoptionBarrier( if b.Status == "closed" { continue // closed beads don't count for dedup } - if sn := b.Metadata["session_name"]; sn != "" { + if sn := sessionpkg.InfoFromPersistedBead(b).SessionNameMetadata; sn != "" { bySessionName[sn] = true } } @@ -168,14 +168,15 @@ func runAdoptionBarrier( } // Build bead metadata. Config/live hashes are left empty — - // syncSessionBeads populates them from built agent objects. - meta := map[string]string{ - "session_name": sessionName, - "state": "active", - "generation": strconv.Itoa(sessionpkg.DefaultGeneration), - "continuation_epoch": strconv.Itoa(sessionpkg.DefaultContinuationEpoch), - "instance_token": sessionpkg.NewInstanceToken(), - } + // syncSessionBeads populates them from built agent objects. agent_name + // and pool_slot are stamped below once pool-base resolution completes. + meta := desiredSessionIdentity(sessionIdentityInputs{ + SessionName: sessionName, + State: "active", + Generation: sessionpkg.DefaultGeneration, + ContinuationEpoch: sessionpkg.DefaultContinuationEpoch, + InstanceToken: sessionpkg.NewInstanceToken(), + }) detail := adoptionDetail{SessionName: sessionName} @@ -274,7 +275,7 @@ func runAdoptionBarrier( return result, passed } -func openSessionBeadExists(sessFront *sessionpkg.InfoStore, sessionName string) (bool, error) { +func openSessionBeadExists(sessFront *sessionpkg.Store, sessionName string) (bool, error) { existing, err := sessionpkg.ListAllSessionBeads(sessFront.Store().Store, beads.ListQuery{ Metadata: map[string]string{"session_name": sessionName}, Live: true, diff --git a/cmd/gc/api_state.go b/cmd/gc/api_state.go index 9724d2af71..39439557f8 100644 --- a/cmd/gc/api_state.go +++ b/cmd/gc/api_state.go @@ -27,6 +27,7 @@ import ( "github.com/gastownhall/gascity/internal/git" "github.com/gastownhall/gascity/internal/mail" "github.com/gastownhall/gascity/internal/orderdiscovery" + "github.com/gastownhall/gascity/internal/orderdispatch" "github.com/gastownhall/gascity/internal/orders" "github.com/gastownhall/gascity/internal/runtime" "github.com/gastownhall/gascity/internal/session" @@ -36,7 +37,8 @@ import ( "github.com/gastownhall/gascity/internal/workspacesvc" ) -// controllerState implements api.State and api.StateMutator. +// controllerState implements api.State, api.StateMutator, and +// api.ConfigWriteSerializer. // Protected by an RWMutex for hot-reload: readers take RLock, // the controller loop takes Lock when updating cfg/sp/stores. type controllerState struct { @@ -1189,6 +1191,18 @@ func (cs *controllerState) CityBeadStore() beads.Store { return cs.cityBeadStore } +// ScopedStoreLike implements api.State. See the interface doc comment for +// the contract; scopedStoreLike (cmd/gc/scoped_store.go) does the actual +// unwrap-and-rebuild work, reusing the same credential/env resolution as +// every other bd-CLI store this package constructs. +func (cs *controllerState) ScopedStoreLike(ctx context.Context, existing beads.Store) (beads.Store, error) { + cs.mu.RLock() + cityPath := cs.cityPath + cfg := cs.cfg + cs.mu.RUnlock() + return scopedStoreLike(ctx, cityPath, cfg, existing) +} + // NudgesBeadStore returns the store backing the nudge-queue shadow beads. At the // default backend resolveNudgesStore returns cityBeadStore, so this is byte-identical // to CityBeadStore; when [beads.classes.nudges] is relocated it returns the per-class @@ -1319,6 +1333,18 @@ func (cs *controllerState) DisableOrder(name, rig string) error { }) } +// SerializeConfigWrite runs fn under the same per-city mutation lock the +// configedit.Editor uses for agent/rig/provider/formula edits. The HTTP pack +// import add/remove handlers write pack.toml, packs.lock, and sometimes +// city.toml outside the Editor callback shape, so routing them through this +// shared lock keeps concurrent config writers from interleaving and losing an +// update or desyncing the manifest and lockfile. +func (cs *controllerState) SerializeConfigWrite(fn func() error) error { + return cs.editor.Do(fn) +} + +var _ api.ConfigWriteSerializer = (*controllerState)(nil) + // SuspendAgent writes suspended=true to durable agent config. // Uses configedit.Editor for provenance-aware edit (inline vs discovered vs patch). func (cs *controllerState) SuspendAgent(name string) error { @@ -1580,6 +1606,7 @@ func (cs *controllerState) UpdateProvider(name string, patch api.ProviderUpdate) Env: patch.Env, OptionsSchemaMerge: patch.OptionsSchemaMerge, OptionsSchema: patch.OptionsSchema, + OptionDefaults: patch.OptionDefaults, }) }) } @@ -1863,6 +1890,42 @@ func (cs *controllerState) ServiceRegistry() workspacesvc.Registry { return cs.services } +// WebhookDispatcher implements api.WebhookDispatchProvider — the H1/E0.5 dispatch +// seam the supervisor webhook receiver (E3/E6) fires verified+matched deliveries +// through. It returns an adapter that dispatches a pre-resolved order through the +// same launchResolvedDispatch → dispatchOne core the controller tick loop uses. +// +// The adapter builds a fresh, detached memoryOrderDispatcher per delivery from the +// CURRENT cfg (read under the hot-reload lock) so a webhook dispatch reflects a +// config reload without a rebuild hook and never races the reconciler's live tick +// dispatcher (cr.od, which is single-goroutine-owned by the reconcile loop and may +// be nil for a webhook-only city). The seam's Dispatch path consults no per-tick +// dispatcher state (cooldown cache, open-work gate) — it validates required params, +// writes the tracking bead, and launches dispatchOne — so a per-delivery instance +// is byte-equivalent to a long-lived one, and the order's own timeout bounds the +// async work. +func (cs *controllerState) WebhookDispatcher() orderdispatch.Dispatcher { + return controllerWebhookDispatcher{cs: cs} +} + +// controllerWebhookDispatcher adapts controllerState into orderdispatch.Dispatcher. +type controllerWebhookDispatcher struct{ cs *controllerState } + +func (d controllerWebhookDispatcher) Dispatch(ctx context.Context, req orderdispatch.DispatchRequest) (orderdispatch.DispatchResult, error) { + cs := d.cs + cs.mu.RLock() + cfg := cs.cfg + var rec events.Recorder = cs.eventProv + cs.mu.RUnlock() + if rec == nil { + // dispatchOne records OrderFired/Completed/Failed unconditionally; a + // discard recorder keeps it panic-free when the city has events disabled. + rec = events.Discard + } + md := newMemoryOrderDispatcher(nil, cs.cityPath, cfg, rec, os.Stderr) + return md.Dispatch(ctx, req) +} + // ExtMsgServices returns the external messaging services. func (cs *controllerState) ExtMsgServices() *extmsg.Services { cs.mu.RLock() diff --git a/cmd/gc/assigned_work_scope.go b/cmd/gc/assigned_work_scope.go index c0e4a08af3..7805f37aa9 100644 --- a/cmd/gc/assigned_work_scope.go +++ b/cmd/gc/assigned_work_scope.go @@ -6,6 +6,7 @@ import ( "github.com/gastownhall/gascity/internal/agentutil" "github.com/gastownhall/gascity/internal/beads" "github.com/gastownhall/gascity/internal/config" + sessionpkg "github.com/gastownhall/gascity/internal/session" ) func assignedWorkStoreRefForAgent(cityPath string, cfg *config.City, agentCfg *config.Agent) string { @@ -87,7 +88,7 @@ func assignedWorkIndexReachableFromAgent(cityPath string, cfg *config.City, agen func filterAssignedWorkBeadsForPoolDemand( cfg *config.City, cityPath string, - sessionBeads []beads.Bead, + sessionInfos []sessionpkg.Info, assignedWorkBeads []beads.Bead, assignedWorkStoreRefs []string, ) []beads.Bead { @@ -99,18 +100,18 @@ func filterAssignedWorkBeadsForPoolDemand( } assigneeToSessionBeadID := make(map[string]string) sessionBeadTemplate := make(map[string]string) - for _, sb := range sessionBeads { - if sb.Status == "closed" { + for _, sb := range sessionInfos { + if sb.Closed { continue } - template := normalizedSessionTemplate(sb, cfg) + template := normalizedSessionTemplateInfo(sb, cfg) if template == "" { - template = strings.TrimSpace(sb.Metadata["template"]) + template = strings.TrimSpace(sb.Template) } if template != "" { sessionBeadTemplate[sb.ID] = template } - for _, id := range sessionBeadAssigneeIdentities(sb) { + for _, id := range sessionBeadAssigneeIdentitiesInfo(sb) { assigneeToSessionBeadID[id] = sb.ID } } @@ -147,7 +148,7 @@ func filterAssignedWorkBeadsForPoolDemand( func filterAssignedWorkBeadsForSessionWake( cfg *config.City, cityPath string, - sessionBeads []beads.Bead, + sessionInfos []sessionpkg.Info, assignedWorkBeads []beads.Bead, assignedWorkStoreRefs []string, ) ([]beads.Bead, []string) { @@ -186,27 +187,27 @@ func filterAssignedWorkBeadsForSessionWake( } add(identity, assignedWorkStoreRefForAgent(cityPath, cfg, spec.Agent)) } - for _, sb := range sessionBeads { - if sb.Status == "closed" { + for _, sb := range sessionInfos { + if sb.Closed { continue } - template := normalizedSessionTemplate(sb, cfg) + template := normalizedSessionTemplateInfo(sb, cfg) if template == "" { - template = strings.TrimSpace(sb.Metadata["template"]) + template = strings.TrimSpace(sb.Template) } agentCfg := findAgentByTemplate(cfg, template) if agentCfg == nil { continue } if agentIsCrossStoreEligible(agentCfg) { - for _, id := range sessionBeadAssigneeIdentities(sb) { + for _, id := range sessionBeadAssigneeIdentitiesInfo(sb) { crossStore[strings.TrimSpace(id)] = struct{}{} } crossStore[strings.TrimSpace(template)] = struct{}{} continue } storeRef := assignedWorkStoreRefForAgent(cityPath, cfg, agentCfg) - for _, id := range sessionBeadAssigneeIdentities(sb) { + for _, id := range sessionBeadAssigneeIdentitiesInfo(sb) { add(id, storeRef) } add(template, storeRef) diff --git a/cmd/gc/assigned_work_scope_test.go b/cmd/gc/assigned_work_scope_test.go index 3b36c80920..b2aea1515d 100644 --- a/cmd/gc/assigned_work_scope_test.go +++ b/cmd/gc/assigned_work_scope_test.go @@ -7,8 +7,23 @@ import ( "github.com/gastownhall/gascity/internal/agentutil" "github.com/gastownhall/gascity/internal/beads" "github.com/gastownhall/gascity/internal/config" + sessionpkg "github.com/gastownhall/gascity/internal/session" ) +// sessionInfosFromBeads projects raw session beads through the production codec +// (session.InfoFromPersistedBead), matching how the reconciler feeds +// snapshot.OpenInfos() into the pool-demand/session-wake filters. +func sessionInfosFromBeads(bs []beads.Bead) []sessionpkg.Info { + if bs == nil { + return nil + } + infos := make([]sessionpkg.Info, len(bs)) + for i, b := range bs { + infos[i] = sessionpkg.InfoFromPersistedBead(b) + } + return infos +} + func TestFilterAssignedWorkBeadsForSessionWakeKeepsOnlyReachableAssigneeSources(t *testing.T) { cityPath := t.TempDir() rigPath := filepath.Join(cityPath, "riga") @@ -42,7 +57,7 @@ func TestFilterAssignedWorkBeadsForSessionWakeKeepsOnlyReachableAssigneeSources( } storeRefs := []string{"", "riga", "", "riga"} - got, gotRefs := filterAssignedWorkBeadsForSessionWake(cfg, cityPath, sessions, work, storeRefs) + got, gotRefs := filterAssignedWorkBeadsForSessionWake(cfg, cityPath, sessionInfosFromBeads(sessions), work, storeRefs) if len(got) != 2 { t.Fatalf("filtered work length = %d, want 2: %#v", len(got), got) @@ -113,7 +128,7 @@ func TestFilterAssignedWorkBeadsForPoolDemandKeepsDirectAssigneeAfterTemplateFal Metadata: map[string]string{}, }} - got := filterAssignedWorkBeadsForPoolDemand(cfg, "", sessions, work, []string{""}) + got := filterAssignedWorkBeadsForPoolDemand(cfg, "", sessionInfosFromBeads(sessions), work, []string{""}) if len(got) != 1 || got[0].ID != "direct-assigned" { t.Fatalf("filtered work = %#v, want direct-assigned work preserved through template fallback", got) @@ -172,7 +187,7 @@ func TestFilterAssignedWorkBeadsForPoolDemandKeepsPersistedBoundRoute(t *testing }, }} - got := filterAssignedWorkBeadsForPoolDemand(cfg, cityPath, sessions, work, []string{"gascity-packs"}) + got := filterAssignedWorkBeadsForPoolDemand(cfg, cityPath, sessionInfosFromBeads(sessions), work, []string{"gascity-packs"}) if len(got) != 1 || got[0].ID != "gp-qx0o" { t.Fatalf("filtered work = %#v, want persisted bound route preserved", got) @@ -204,7 +219,7 @@ func TestFilterAssignedWorkBeadsForPoolDemandDropsDirectAssigneeFromUnreachableS Metadata: map[string]string{}, }} - got := filterAssignedWorkBeadsForPoolDemand(cfg, cityPath, sessions, work, []string{"riga"}) + got := filterAssignedWorkBeadsForPoolDemand(cfg, cityPath, sessionInfosFromBeads(sessions), work, []string{"riga"}) if len(got) != 0 { t.Fatalf("filtered work = %#v, want unreachable rig-store direct assignment dropped", got) @@ -616,7 +631,7 @@ func TestResolveTaskWorkDirIncludesAssignedWisp(t *testing.T) { t.Fatalf("mark wisp in progress: %v", err) } - if got := resolveTaskWorkDir(store, "worker-session"); got != workDir { + if got := resolveTaskWorkDir("", store, "worker-session"); got != workDir { t.Fatalf("resolveTaskWorkDir = %q, want assigned wisp work_dir %q", got, workDir) } } diff --git a/cmd/gc/bd_env.go b/cmd/gc/bd_env.go index f114601957..93287c0832 100644 --- a/cmd/gc/bd_env.go +++ b/cmd/gc/bd_env.go @@ -1309,7 +1309,18 @@ func cityPostgresProjectionErrorCanBeBypassed(cityPath string, err error) bool { } func bdRuntimeEnvForRigWithError(cityPath string, cfg *config.City, rigPath string) (map[string]string, error) { - env, cityErr := bdRuntimeEnvWithError(cityPath) + return bdRuntimeEnvForRigWithErrorRecovery(cityPath, cfg, rigPath, true) +} + +// bdRuntimeEnvForRigWithErrorNoRecovery is bdRuntimeEnvForRigWithError +// without the managed-dolt recovery side effects; see +// bdRuntimeEnvWithErrorNoRecovery for why (gascity ga-cdmx6x). +func bdRuntimeEnvForRigWithErrorNoRecovery(cityPath string, cfg *config.City, rigPath string) (map[string]string, error) { + return bdRuntimeEnvForRigWithErrorRecovery(cityPath, cfg, rigPath, false) +} + +func bdRuntimeEnvForRigWithErrorRecovery(cityPath string, cfg *config.City, rigPath string, allowRecovery bool) (map[string]string, error) { + env, cityErr := bdRuntimeEnvWithErrorRecovery(cityPath, allowRecovery) rigPath = filepath.Clean(rigPath) // Pin the rig store explicitly. The gc-beads-bd provider derives its Dolt // data root from GC_CITY_PATH unless BEADS_DIR is set, so cwd-based @@ -1333,7 +1344,7 @@ func bdRuntimeEnvForRigWithError(cityPath string, cfg *config.City, rigPath stri mirrorBeadsDoltEnv(env) return env, nil } - if err := applyResolvedRigDoltEnv(env, cityPath, rigPath, explicitRig, true); err != nil { + if err := applyResolvedRigDoltEnv(env, cityPath, rigPath, explicitRig, allowRecovery); err != nil { clearProjectedDoltEnv(env) clearProjectedPostgresEnv(env) mirrorBeadsDoltEnv(env) @@ -1418,6 +1429,23 @@ func nativeStoreCanaryScopeName(cityPath string, cfg *config.City, scopeRoot str } func bdRuntimeEnvWithError(cityPath string) (map[string]string, error) { + return bdRuntimeEnvWithErrorRecovery(cityPath, true) +} + +// bdRuntimeEnvWithErrorNoRecovery is bdRuntimeEnvWithError without the +// managed-dolt recovery/health-check/autostart side effects: it reads +// existing published or configured connection state only, and fails fast +// (env still gets the non-Dolt opt-out vars set) when no managed server is +// currently reachable. Recovering a managed dolt server is legitimate +// work, but doing it from every concurrent, short-budget scoped-store +// construction would multiply exactly the load a read-storm mitigation +// exists to bound (gascity ga-cdmx6x) — those callers use this instead of +// bdRuntimeEnvWithError. +func bdRuntimeEnvWithErrorNoRecovery(cityPath string) (map[string]string, error) { + return bdRuntimeEnvWithErrorRecovery(cityPath, false) +} + +func bdRuntimeEnvWithErrorRecovery(cityPath string, allowRecovery bool) (map[string]string, error) { env := cityRuntimeEnvMapForCity(cityPath) env["BEADS_DIR"] = filepath.Join(cityPath, ".beads") env["GC_RIG"] = "" @@ -1470,7 +1498,7 @@ func bdRuntimeEnvWithError(cityPath string) (map[string]string, error) { } else if usedPostgres { return env, nil } - if err := applyResolvedCityDoltEnv(env, cityPath, true); err != nil { + if err := applyResolvedCityDoltEnv(env, cityPath, allowRecovery); err != nil { clearProjectedDoltEnv(env) mirrorBeadsDoltEnv(env) if isRecoverableManagedDoltEnvError(err) { @@ -1520,7 +1548,7 @@ func cityRuntimeProcessEnvWithError(cityPath string) ([]string, error) { clearProjectedDoltEnv(source) } } - keys := execProjectedBackendEnvKeys() + keys := execProjectedBackendCopyKeys() keys = append(keys, "BEADS_DOLT_AUTO_START") for _, key := range keys { if value, ok := source[key]; ok { @@ -1596,6 +1624,42 @@ func mirrorBeadsDoltEnv(env map[string]string) { } else { delete(env, "BEADS_DOLT_PASSWORD") } + // Carry the hosted beads-gateway credential command into the projected env. + // bd authenticates to the gateway by running the helper named in + // BEADS_DOLT_CREDENTIAL_COMMAND; without it bd falls back to the static/root + // user and the gateway rejects the connection (MySQL Error 1045). That key + // contains "CREDENTIAL", so execenv.FilterInherited strips it from every + // gc-spawned bd subprocess and agent session. preserveHostedBeadsCredentialEnv + // re-adds it on the slice-merge paths (overlayEnvEntries / mergeRuntimeEnv), + // but only when it is already present in the pre-filter environ and only on + // those paths — the agent session env is built from this projected map, which + // does not carry the ambient value, and a controller that exports the helper + // under only the non-sensitive GC_DOLT_CRED_CMD (which survives filtering) has + // nothing for that pass to preserve. Mirror GC_DOLT_CRED_CMD into + // BEADS_DOLT_CREDENTIAL_COMMAND here (map value wins, else the ambient value + // of either key) so bd authenticates the same way the in-process native store + // does. + // + // Two intentional asymmetries with the sibling BEADS_DOLT_* branches above, + // kept deliberately (do not "normalize" them into the map->map convention): + // 1. Ambient fallback: this is the only branch that reads process env + // (os.Getenv), because a controller commonly exports only the helper and + // never seeds it into the projected map. + // 2. Preserve-not-clear: when no source exists this branch leaves any + // existing target value untouched instead of deleting/emptying it. The + // siblings clear their target to defeat stale tmux inheritance; the + // credential key is instead preserved from ambient by + // preserveHostedBeadsCredentialEnv on the slice-merge paths, so clearing + // it here would fight that pass. + if cred := strings.TrimSpace(env["GC_DOLT_CRED_CMD"]); cred != "" { + env["BEADS_DOLT_CREDENTIAL_COMMAND"] = cred + } else if cred := strings.TrimSpace(env["BEADS_DOLT_CREDENTIAL_COMMAND"]); cred != "" { + env["BEADS_DOLT_CREDENTIAL_COMMAND"] = cred + } else if ambient := strings.TrimSpace(os.Getenv("GC_DOLT_CRED_CMD")); ambient != "" { + env["BEADS_DOLT_CREDENTIAL_COMMAND"] = ambient + } else if ambient := strings.TrimSpace(os.Getenv("BEADS_DOLT_CREDENTIAL_COMMAND")); ambient != "" { + env["BEADS_DOLT_CREDENTIAL_COMMAND"] = ambient + } } // cityForStoreDir resolves ambient store contexts. GC_CITY intentionally wins diff --git a/cmd/gc/bd_env_test.go b/cmd/gc/bd_env_test.go index 8d11c57784..eec9f83d5f 100644 --- a/cmd/gc/bd_env_test.go +++ b/cmd/gc/bd_env_test.go @@ -318,6 +318,67 @@ func TestBdRuntimeEnvIncludesDoltHost(t *testing.T) { } } +// TestBdRuntimeEnvNoRecoveryMatchesRecoveryForExternalTarget proves the +// NoRecovery variant (used by ga-cdmx6x's scoped throwaway stores) still +// resolves an explicitly configured external Dolt target identically to +// the recovery-allowing default — only the managed-dolt +// recovery/health-check/autostart side effect is skipped, not the +// ordinary external-config path. +func TestBdRuntimeEnvNoRecoveryMatchesRecoveryForExternalTarget(t *testing.T) { + t.Setenv("GC_BEADS", "bd") + t.Setenv("GC_DOLT_HOST", "mini2.hippo-tilapia.ts.net") + t.Setenv("GC_DOLT_PORT", "3307") + t.Setenv("GC_DOLT_USER", "agent") + t.Setenv("GC_DOLT_PASSWORD", "s3cret") + t.Setenv("GC_DOLT", "skip") + + cityPath := t.TempDir() + env, err := bdRuntimeEnvWithErrorNoRecovery(cityPath) + if err != nil { + t.Fatalf("bdRuntimeEnvWithErrorNoRecovery() error = %v", err) + } + + if got := env["GC_DOLT_HOST"]; got != "mini2.hippo-tilapia.ts.net" { + t.Errorf("GC_DOLT_HOST = %q, want %q", got, "mini2.hippo-tilapia.ts.net") + } + if got := env["GC_DOLT_PORT"]; got != "3307" { + t.Errorf("GC_DOLT_PORT = %q, want %q", got, "3307") + } + if got := env["BEADS_DOLT_AUTO_START"]; got != "0" { + t.Errorf("BEADS_DOLT_AUTO_START = %q, want %q", got, "0") + } +} + +// TestBdRuntimeEnvForRigNoRecoveryMatchesRecoveryForExternalTarget is +// TestBdRuntimeEnvNoRecoveryMatchesRecoveryForExternalTarget for the +// rig-scoped resolver. +func TestBdRuntimeEnvForRigNoRecoveryMatchesRecoveryForExternalTarget(t *testing.T) { + t.Setenv("GC_BEADS", "bd") + t.Setenv("GC_DOLT_HOST", "mini2.hippo-tilapia.ts.net") + t.Setenv("GC_DOLT_PORT", "3307") + t.Setenv("GC_DOLT_USER", "agent") + t.Setenv("GC_DOLT_PASSWORD", "s3cret") + t.Setenv("GC_DOLT", "skip") + + cityPath := t.TempDir() + rigPath := filepath.Join(cityPath, "rigs", "repo") + if err := os.MkdirAll(rigPath, 0o755); err != nil { + t.Fatal(err) + } + cfg := &config.City{Rigs: []config.Rig{{Name: "repo", Path: "rigs/repo"}}} + + env, err := bdRuntimeEnvForRigWithErrorNoRecovery(cityPath, cfg, rigPath) + if err != nil { + t.Fatalf("bdRuntimeEnvForRigWithErrorNoRecovery() error = %v", err) + } + if got := env["GC_DOLT_HOST"]; got != "mini2.hippo-tilapia.ts.net" { + t.Errorf("GC_DOLT_HOST = %q, want %q", got, "mini2.hippo-tilapia.ts.net") + } + if got := env["GC_RIG"]; got != "repo" { + t.Errorf("GC_RIG = %q, want repo", got) + } +} + func TestBdRuntimeEnvDisablesCLIRemoteSync(t *testing.T) { t.Setenv("GC_BEADS", "bd") t.Setenv("BD_DOLT_SYNC_CLI_REMOTES", "true") @@ -5442,3 +5503,72 @@ func TestReapStaleBdExportJSONLLeavesFileOnUnmanagedScope(t *testing.T) { t.Fatalf("jsonl removed on unmanaged scope; stat err = %v, want nil", err) } } + +// TestMirrorBeadsDoltEnvPropagatesCredentialCommand covers the hosted +// beads-gateway credential helper. bd authenticates by running the command in +// BEADS_DOLT_CREDENTIAL_COMMAND; that key contains "CREDENTIAL" so +// execenv.FilterInherited strips it from every gc-spawned bd subprocess and +// agent session, and the gateway then rejects the root fallback with Error +// 1045. The controller entrypoint also exports the same helper under the +// non-sensitive GC_DOLT_CRED_CMD (which survives filtering), so +// mirrorBeadsDoltEnv must re-derive BEADS_DOLT_CREDENTIAL_COMMAND from it. +func TestMirrorBeadsDoltEnvPropagatesCredentialCommand(t *testing.T) { + t.Run("from GC_DOLT_CRED_CMD in the map", func(t *testing.T) { + t.Setenv("GC_DOLT_CRED_CMD", "") + t.Setenv("BEADS_DOLT_CREDENTIAL_COMMAND", "") + env := map[string]string{"GC_DOLT_HOST": "gw.beads.example", "GC_DOLT_CRED_CMD": "/usr/local/bin/eia-helper"} + mirrorBeadsDoltEnv(env) + if got := env["BEADS_DOLT_CREDENTIAL_COMMAND"]; got != "/usr/local/bin/eia-helper" { + t.Fatalf("BEADS_DOLT_CREDENTIAL_COMMAND = %q, want %q (from GC_DOLT_CRED_CMD)", got, "/usr/local/bin/eia-helper") + } + }) + t.Run("from ambient GC_DOLT_CRED_CMD when map is unset", func(t *testing.T) { + t.Setenv("GC_DOLT_CRED_CMD", "/usr/local/bin/eia-helper") + t.Setenv("BEADS_DOLT_CREDENTIAL_COMMAND", "") + env := map[string]string{"GC_DOLT_HOST": "gw.beads.example"} + mirrorBeadsDoltEnv(env) + if got := env["BEADS_DOLT_CREDENTIAL_COMMAND"]; got != "/usr/local/bin/eia-helper" { + t.Fatalf("BEADS_DOLT_CREDENTIAL_COMMAND = %q, want %q (from ambient GC_DOLT_CRED_CMD)", got, "/usr/local/bin/eia-helper") + } + }) + t.Run("from ambient BEADS_DOLT_CREDENTIAL_COMMAND fallback", func(t *testing.T) { + t.Setenv("GC_DOLT_CRED_CMD", "") + t.Setenv("BEADS_DOLT_CREDENTIAL_COMMAND", "/usr/local/bin/eia-helper") + env := map[string]string{"GC_DOLT_HOST": "gw.beads.example"} + mirrorBeadsDoltEnv(env) + if got := env["BEADS_DOLT_CREDENTIAL_COMMAND"]; got != "/usr/local/bin/eia-helper" { + t.Fatalf("BEADS_DOLT_CREDENTIAL_COMMAND = %q, want %q (from ambient fallback)", got, "/usr/local/bin/eia-helper") + } + }) + t.Run("GC_DOLT_CRED_CMD in map wins over ambient BEADS_DOLT_CREDENTIAL_COMMAND", func(t *testing.T) { + t.Setenv("GC_DOLT_CRED_CMD", "") + t.Setenv("BEADS_DOLT_CREDENTIAL_COMMAND", "/ambient/helper") + env := map[string]string{"GC_DOLT_HOST": "gw.beads.example", "GC_DOLT_CRED_CMD": "/map/helper"} + mirrorBeadsDoltEnv(env) + if got := env["BEADS_DOLT_CREDENTIAL_COMMAND"]; got != "/map/helper" { + t.Fatalf("BEADS_DOLT_CREDENTIAL_COMMAND = %q, want %q (map GC_DOLT_CRED_CMD wins)", got, "/map/helper") + } + }) + t.Run("map BEADS_DOLT_CREDENTIAL_COMMAND wins over ambient GC_DOLT_CRED_CMD", func(t *testing.T) { + // Locks the branch-2-over-branch-3 precedence: an explicit map + // BEADS_DOLT_CREDENTIAL_COMMAND must beat an ambient GC_DOLT_CRED_CMD. + // Without this a branches-2/3 reorder would silently flip precedence and + // still pass every other subtest. + t.Setenv("GC_DOLT_CRED_CMD", "/ambient/helper") + t.Setenv("BEADS_DOLT_CREDENTIAL_COMMAND", "") + env := map[string]string{"GC_DOLT_HOST": "gw.beads.example", "BEADS_DOLT_CREDENTIAL_COMMAND": "/map/helper"} + mirrorBeadsDoltEnv(env) + if got := env["BEADS_DOLT_CREDENTIAL_COMMAND"]; got != "/map/helper" { + t.Fatalf("BEADS_DOLT_CREDENTIAL_COMMAND = %q, want %q (explicit map value wins over ambient GC_DOLT_CRED_CMD)", got, "/map/helper") + } + }) + t.Run("absent when no credential command anywhere", func(t *testing.T) { + t.Setenv("GC_DOLT_CRED_CMD", "") + t.Setenv("BEADS_DOLT_CREDENTIAL_COMMAND", "") + env := map[string]string{"GC_DOLT_HOST": "gw.beads.example"} + mirrorBeadsDoltEnv(env) + if got, ok := env["BEADS_DOLT_CREDENTIAL_COMMAND"]; ok && got != "" { + t.Fatalf("BEADS_DOLT_CREDENTIAL_COMMAND = %q, want unset/empty", got) + } + }) +} diff --git a/cmd/gc/build_desired_state.go b/cmd/gc/build_desired_state.go index f39efa0157..1978348f2f 100644 --- a/cmd/gc/build_desired_state.go +++ b/cmd/gc/build_desired_state.go @@ -375,7 +375,7 @@ func evaluatePendingPools( if err != nil { outcome = "failed" } - trace.recordOperation("trace.scale_check_exec", template, "", "", "scale_check", outcome, traceRecordPayload{ + trace.RecordOperation(TraceSiteScaleCheckExec, TraceReasonScaleCheck, TraceOutcomeCode(outcome), "", template, "", time.Since(started), traceRecordPayload{ "pool_dir": dir, "command": sp.Check, "desired": d, @@ -383,7 +383,7 @@ func evaluatePendingPools( "duration_ms": time.Since(started).Milliseconds(), "agent_template": template, "agent_index": agentIndex, - }, "") + }) } }(j, template, agentName, agentIndex, sp, pw.poolDir, newDemand) } @@ -468,6 +468,21 @@ func buildDesiredState( return result } +// recordDemandSubPhase emits one operation record under the demand-snapshot +// trace site for a sub-phase of buildDesiredStateWithSessionBeads. The parent +// `load_demand_snapshot` phase regularly dominates the controller tick +// (measured avg 6.4s / max 40.8s across 190 storm-window cycles on a small +// idle city, gastownhall/gascity#2463) but was previously opaque: a single +// aggregate duration with no split between the cross-store collection reads, +// the per-demand-group Ready probes, the scale_check subprocess execs, and +// pure computation. These records make that split first-class trace data so +// store-contention regressions can be attributed without ad-hoc rebuilds. +// RecordControllerOperation is nil-receiver-safe, so callers without an +// active trace (e.g. buildDesiredState outside the tick) cost one branch. +func recordDemandSubPhase(trace *sessionReconcilerTraceCycle, name string, start time.Time, fields map[string]any) { + trace.RecordControllerOperation(TraceSiteDemandSnapshot, TraceReasonRetained, TraceOutcomeComplete, name, time.Since(start), fields) +} + func buildDesiredStateWithSessionBeads( cityName, cityPath string, beaconTime time.Time, @@ -494,7 +509,12 @@ func buildDesiredStateWithSessionBeads( // running sessions for each pool. A partial/failed collection is logged, // not swallowed: undercounting running sessions can misclassify a pool as // cold and trigger a spurious scale-from-zero probe. + subPhaseStart := time.Now() allOpenSessionBeads, openSessionBeadsErr := collectAllOpenSessionBeads(cfg, store, rigStores, suspendedRigPaths) + recordDemandSubPhase(trace, "demand_snapshot.collect_open_session_beads", subPhaseStart, map[string]any{ + "beads": len(allOpenSessionBeads), + "partial": openSessionBeadsErr != nil, + }) if openSessionBeadsErr != nil { fmt.Fprintf(stderr, "collectAllOpenSessionBeads: PARTIAL — %v (cold-pool detection may undercount running sessions)\n", openSessionBeadsErr) //nolint:errcheck } @@ -705,7 +725,12 @@ func buildDesiredStateWithSessionBeads( var scaleCheckPartialTemplates map[string]bool var namedDefaultDemand map[string]bool if store != nil { + subPhaseStart = time.Now() assignedWorkBeads, assignedWorkStores, assignedWorkStoreRefs, readyAssigned, storePartial = collectAssignedWorkBeadsWithStores(cfg, store, rigStores, suspendedRigPaths, sessionBeads) + recordDemandSubPhase(trace, "demand_snapshot.collect_assigned_work", subPhaseStart, map[string]any{ + "beads": len(assignedWorkBeads), + "partial": storePartial, + }) if storePartial { fmt.Fprintf(stderr, "assignedWorkBeads: PARTIAL — store query failed, drain decisions suppressed\n") //nolint:errcheck } @@ -737,12 +762,24 @@ func buildDesiredStateWithSessionBeads( // the worker work_query/claim path match gc.routed_to canonically by raw // string, so the route must be canonicalized before demand is counted or // the cold pool never wakes for it. + subPhaseStart = time.Now() unassignedRoutedBeads, unassignedRoutedStores := collectOpenUnassignedRoutedWork(cfg, store, rigStores, suspendedRigPaths, stderr) canonicalizeLegacyBoundUnassignedRoutedWork(cfg, unassignedRoutedBeads, unassignedRoutedStores, stderr) controlDispatcherOpenDemand := openControlDispatcherDemand(cfg, unassignedRoutedBeads) + recordDemandSubPhase(trace, "demand_snapshot.collect_unassigned_routed", subPhaseStart, map[string]any{ + "beads": len(unassignedRoutedBeads), + }) + subPhaseStart = time.Now() scaleCheckCounts, poolScaleCheckPartialTemplates = evaluatePendingPoolsMap(cfg, pendingPools, stderr, trace) + recordDemandSubPhase(trace, "demand_snapshot.evaluate_pending_pools", subPhaseStart, map[string]any{ + "pools": len(pendingPools), + }) if len(defaultScaleTargets) > 0 { + subPhaseStart = time.Now() defaultCounts, defaultDemand, partialTemplates, errs := defaultScaleCheckCountsAndDemand(defaultScaleTargets) + recordDemandSubPhase(trace, "demand_snapshot.default_scale_demand", subPhaseStart, map[string]any{ + "targets": len(defaultScaleTargets), + }) for _, err := range errs { // defaultScaleCheckCounts wraps Ready() failures with // enough context to keep this generic outer log honest @@ -789,7 +826,11 @@ func buildDesiredStateWithSessionBeads( if len(defaultNamedScaleTargets) > 0 { var namedErrs []error var partialTemplates map[string]bool + subPhaseStart = time.Now() namedDefaultDemand, partialTemplates, namedErrs = defaultNamedSessionDemand(defaultNamedScaleTargets, cfg, cityName) + recordDemandSubPhase(trace, "demand_snapshot.named_session_demand", subPhaseStart, map[string]any{ + "targets": len(defaultNamedScaleTargets), + }) for _, err := range namedErrs { fmt.Fprintf(stderr, "buildDesiredState: %v (using named demand=false)\n", err) //nolint:errcheck } @@ -800,11 +841,11 @@ func buildDesiredStateWithSessionBeads( if len(scaleCheckPartialTemplates) > 0 { fmt.Fprintf(stderr, "scaleCheck: PARTIAL — scale_check failed for %s, retaining affected sessions\n", strings.Join(sortedBoolMapKeys(scaleCheckPartialTemplates), ",")) //nolint:errcheck } - poolWorkBeads := filterAssignedWorkBeadsForPoolDemand(cfg, cityPath, sessionBeads.Open(), assignedWorkBeads, assignedWorkStoreRefs) + poolWorkBeads := filterAssignedWorkBeadsForPoolDemand(cfg, cityPath, sessionBeads.OpenInfos(), assignedWorkBeads, assignedWorkStoreRefs) bp.assignedWorkBeads = poolWorkBeads bp.poolScaleCheckPartialTemplates = poolScaleCheckPartialTemplates bp.providerHealthSnapshot = loadProviderHealthSnapshot(cityPath) - poolDesiredStates := ComputePoolDesiredStatesWithDemandTraced(cfg, poolWorkBeads, sessionBeads.Open(), scaleCheckCounts, scaleCheckDemandByTemplate, trace) + poolDesiredStates := ComputePoolDesiredStatesWithDemandTraced(cfg, poolWorkBeads, sessionBeads.OpenInfos(), scaleCheckCounts, scaleCheckDemandByTemplate, trace) bp.configurePoolSessionCreateFairShare(poolDesiredStates) for _, poolState := range poolDesiredStates { cfgAgent := findAgentByTemplate(cfg, poolState.Template) @@ -892,6 +933,20 @@ func buildDesiredStateWithSessionBeads( if assignee != identity { continue } + if spec.Agent.SupportsExpandedSessionIdentities() { + // Defense in depth (ga-i1d0tr Candidate B): a bare-template Assignee + // is only a legitimate "this IS my identity" match for a template + // with exactly one possible live identity. For a template that + // supports expanded per-instance identities (a multi-slot pool or + // namepool coexisting with this named session), a bare-template + // Assignee means some other path wrote the wrong value — a pool + // slot's claim, a human running `bd update --assignee=