diff --git a/.github/actions/checkout-eyrie/action.yml b/.github/actions/checkout-eyrie/action.yml index 198388be..0f4be0b5 100644 --- a/.github/actions/checkout-eyrie/action.yml +++ b/.github/actions/checkout-eyrie/action.yml @@ -1,68 +1,37 @@ name: Checkout ecosystem -description: Clone graycode-eco ecosystem repos into hawk/external for hawk go.work +description: Clone graycode-eco sibling repos into the workspace parent for hawk's go.work inputs: ref: - description: Git ref to checkout only when allow_branch_fallback is true + description: Git ref to checkout (falls back to main) required: false default: main - allow_branch_fallback: - description: > - When true, clone a branch head if the Gitlink is missing (dev-only escape hatch). - Default false — missing or unreachable pins fail the job so releases never - silently track main. - required: false - default: "false" runs: using: composite steps: - - name: Clone ecosystem repos + - name: Clone sibling repos shell: bash env: - # Passed via env (not ${{ }} interpolation into the script) so an - # attacker-controlled ref (e.g. a fork branch name) cannot inject shell. + # Passed via env (not ${{ }} interpolation) so an attacker-controlled + # ref (e.g. a fork branch name) cannot inject shell. INPUT_REF: ${{ inputs.ref }} - ALLOW_BRANCH_FALLBACK: ${{ inputs.allow_branch_fallback }} run: | set -euo pipefail - mkdir -p "${GITHUB_WORKSPACE}/external" - for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do - dest="${GITHUB_WORKSPACE}/external/${repo}" + # hawk's committed go.work references ../, so the siblings must + # be cloned into the parent of the checked-out workspace. + ws_parent="$(cd "${GITHUB_WORKSPACE}/.." && pwd)" + for repo in hawk-core-contracts eyrie inspect sight tok trace yaad hawk-mcpkit; do + dest="${ws_parent}/${repo}" if [ -d "$dest/.git" ]; then echo "$repo already present at $dest" continue fi - commit=$(git ls-tree HEAD "external/${repo}" | awk '{print $3}' || true) - if [ -n "$commit" ]; then - echo "Cloning $repo at submodule commit $commit" - # Full clone so the pinned commit is reachable even after - # the dependency repo's main has been rewritten past it - # (e.g. by a squash-merge). A depth-1 clone can't check - # out older commits and fails with "unable to read tree". - git clone "https://github.com/GrayCodeAI/${repo}.git" "$dest" - if ! (cd "$dest" && git checkout --quiet "$commit"); then - echo "::error::Pinned submodule commit $commit is not reachable in $repo" - echo "Refusing to test an unpinned branch head for a pinned Hawk commit." - exit 1 - fi + if git ls-remote --heads "https://github.com/GrayCodeAI/${repo}.git" "$INPUT_REF" | grep -q .; then + echo "Cloning $repo at branch $INPUT_REF" + git clone --depth=1 --branch "$INPUT_REF" "https://github.com/GrayCodeAI/${repo}.git" "$dest" else - if [ "${ALLOW_BRANCH_FALLBACK}" != "true" ]; then - echo "::error::Missing Gitlink for external/${repo} at HEAD" - echo "Hawk requires a pinned submodule commit for every engine." - echo "Record the pin with: git submodule update --init external/${repo}" - echo "and commit the Gitlink. Branch-head fallback is disabled by default" - echo "(set allow_branch_fallback=true only for local experiments)." - exit 1 - fi - ref="$INPUT_REF" - # Optional escape hatch: fall back to main if the branch doesn't exist. - if ! git ls-remote --heads "https://github.com/GrayCodeAI/${repo}.git" "$ref" | grep -q .; then - echo "Branch '$ref' not found on $repo, falling back to main" - ref="main" - fi - echo "::warning::Cloning $repo at branch head '$ref' (no Gitlink; allow_branch_fallback=true)" - git clone --depth=1 --branch "$ref" \ - "https://github.com/GrayCodeAI/${repo}.git" "$dest" + echo "Branch '$INPUT_REF' not found on $repo, falling back to main" + git clone --depth=1 --branch main "https://github.com/GrayCodeAI/${repo}.git" "$dest" fi done diff --git a/.github/actions/setup-deps/action.yml b/.github/actions/setup-deps/action.yml index 7b6522ee..b9e6ed83 100644 --- a/.github/actions/setup-deps/action.yml +++ b/.github/actions/setup-deps/action.yml @@ -1,5 +1,5 @@ name: Setup Dependencies -description: Clone ecosystem deps with retry, setup Go, create workspace +description: Clone ecosystem sibling repos with retry, setup Go, sync workspace inputs: go-version: @@ -12,7 +12,7 @@ inputs: runs: using: composite steps: - - name: Checkout dependencies + - name: Checkout sibling dependencies shell: bash env: GH_TOKEN: ${{ inputs.token }} @@ -25,21 +25,25 @@ runs: done echo "Failed to clone $repo after 3 attempts" && return 1 } - mkdir -p external - clone_with_retry hawk-core-contracts external/hawk-core-contracts main - clone_with_retry eyrie external/eyrie main - clone_with_retry tok external/tok main - clone_with_retry yaad external/yaad main - clone_with_retry inspect external/inspect main - clone_with_retry sight external/sight main - clone_with_retry trace external/trace main + ws_parent="$(cd "${GITHUB_WORKSPACE}/.." && pwd)" + mkdir -p "$ws_parent" + clone_with_retry hawk-core-contracts "$ws_parent/hawk-core-contracts" main + clone_with_retry eyrie "$ws_parent/eyrie" main + clone_with_retry tok "$ws_parent/tok" main + clone_with_retry yaad "$ws_parent/yaad" main + clone_with_retry inspect "$ws_parent/inspect" main + clone_with_retry sight "$ws_parent/sight" main + clone_with_retry trace "$ws_parent/trace" main + clone_with_retry hawk-mcpkit "$ws_parent/hawk-mcpkit" main - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ inputs.go-version }} - - name: Create workspace + - name: Sync workspace shell: bash run: | - printf 'go 1.26.6\n\nuse .\n\nreplace (\n\tgithub.com/GrayCodeAI/hawk-core-contracts => ./external/hawk-core-contracts\n\tgithub.com/GrayCodeAI/eyrie => ./external/eyrie\n\tgithub.com/GrayCodeAI/inspect => ./external/inspect\n\tgithub.com/GrayCodeAI/sight => ./external/sight\n\tgithub.com/GrayCodeAI/tok => ./external/tok\n\tgithub.com/GrayCodeAI/trace => ./external/trace\n\tgithub.com/GrayCodeAI/yaad => ./external/yaad\n\tgithub.com/GrayCodeAI/hawk-mcpkit => ./external/hawk-mcpkit\n)\n' > go.work + # hawk's committed go.work references the sibling repos; ensure it is + # consistent with the cloned siblings. + go work sync diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6176f914..fc9313f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,8 @@ concurrency: env: GO_VERSION: "1.26.6" - # GrayCodeAI sibling modules are resolved from the local external/ submodules via - # go.work; their go.mod require versions (v0.1.0) intentionally do not match the + # GrayCodeAI sibling modules are resolved from the workspace ../ checkouts via + # go.work; their go.mod require versions intentionally do not match the # frozen public proxy/sumdb snapshot, so bypass the proxy + checksum DB for them. GOPRIVATE: "github.com/GrayCodeAI/*" GONOSUMDB: "github.com/GrayCodeAI/*" @@ -84,9 +84,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: | - git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -104,11 +101,11 @@ jobs: fi - name: go mod verify run: go mod verify - - name: workspace points at external checkouts + - name: workspace references sibling checkouts run: | for module in hawk-core-contracts eyrie inspect sight tok trace yaad hawk-mcpkit; do - if ! grep -q "./external/${module}" go.work; then - echo "::error::go.work must include ./external/${module}." + if ! grep -q "../${module}" go.work; then + echo "::error::go.work must include ../${module}." cat go.work exit 1 fi @@ -133,9 +130,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: | - git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -149,13 +143,16 @@ jobs: go build -mod=readonly ./cmd/hawk go test ./... -count=1 -timeout=300s -skip='TestDefaultSkillDirsCrossAgent|TestCopySelectionE2E' - submodule-release-parity: - name: submodule and module parity + release-parity: + name: workspace and module parity runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 + - uses: ./.github/actions/checkout-eyrie + with: + ref: ${{ github.head_ref || github.ref_name }} - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -175,9 +172,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: | - git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -203,8 +197,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -234,8 +226,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -306,8 +296,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -416,9 +404,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: | - git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c with: go-version: "${{ env.GO_VERSION }}" @@ -477,8 +462,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -524,8 +507,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} @@ -553,8 +534,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - - name: Init hawk-mcpkit submodule - run: git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: ${{ env.GO_VERSION }} diff --git a/.github/workflows/compatibility-matrix.yml b/.github/workflows/compatibility-matrix.yml index b5bc0045..e52499ae 100644 --- a/.github/workflows/compatibility-matrix.yml +++ b/.github/workflows/compatibility-matrix.yml @@ -26,10 +26,6 @@ jobs: - uses: ./.github/actions/checkout-eyrie with: ref: ${{ github.head_ref || github.ref_name }} - allow_branch_fallback: "true" - - name: Init hawk-mcpkit submodule - run: | - git submodule update --init external/hawk-mcpkit - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: "1.26.6" @@ -38,6 +34,6 @@ jobs: run: make compat-check - name: Report 'next' matrix run: make compat-test - - name: Pin freshness vs external/ (advisory) + - name: Pin freshness vs sibling repos (advisory) run: make compat-drift continue-on-error: true diff --git a/.github/workflows/daemon-image.yml b/.github/workflows/daemon-image.yml index 6ac85820..71fdf0c6 100644 --- a/.github/workflows/daemon-image.yml +++ b/.github/workflows/daemon-image.yml @@ -11,7 +11,6 @@ on: - "packaging/systemd/hawk-daemon.service" - "internal/**" - "cmd/**" - - "external/**" - "go.mod" - "go.sum" @@ -31,8 +30,6 @@ jobs: steps: - name: Check out source uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -62,18 +59,27 @@ jobs: BUILD_DATE=${{ github.event.head_commit.timestamp }} - name: Scan daemon image with Trivy - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: - image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:scan - format: sarif - output: trivy-daemon-image.sarif - severity: CRITICAL,HIGH + version: v0.70.0 + cache: true + - name: Run Trivy daemon scan (sarif) + shell: bash + run: | # Go reachability is enforced separately by govulncheck in CI. The # binary also carries the full workspace module graph, including # non-reachable packages that Trivy reports as binary findings. - vuln-type: os - ignore-unfixed: true - exit-code: '1' + # CVE-2026-14456 (OpenSSL) is ignored via .trivyignore — the fixed + # libcrypto 3.5.8-r0 is not yet published in Alpine 3.23. + trivy image \ + --severity CRITICAL,HIGH \ + --ignore-unfixed \ + --ignorefile "${GITHUB_WORKSPACE}/.trivyignore" \ + --vuln-type os \ + --format sarif \ + --output trivy-daemon-image.sarif \ + --exit-code 1 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:scan - name: Generate image metadata id: meta diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index b9478173..b6630429 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -37,10 +37,6 @@ jobs: image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:amd64-${{ github.sha }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - # Pull the pinned ecosystem submodules under external/ so the Docker - # build compiles against the integrated revisions, not each repo's main. - submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -71,18 +67,27 @@ jobs: BUILD_DATE=${{ github.event.head_commit.timestamp }} - name: Scan image with Trivy - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: - image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:scan - format: sarif - output: trivy-image.sarif - severity: CRITICAL,HIGH + version: v0.70.0 + cache: true + - name: Run Trivy scan (sarif) + shell: bash + run: | # Go reachability is enforced separately by govulncheck in CI. The # binary also carries the full workspace module graph, including # non-reachable packages that Trivy reports as binary findings. - vuln-type: os - ignore-unfixed: true - exit-code: '1' # Block publishing images with actionable vulnerabilities + # CVE-2026-14456 (OpenSSL) is ignored via .trivyignore — the fixed + # libcrypto 3.5.8-r0 is not yet published in Alpine 3.23. + trivy image \ + --severity CRITICAL,HIGH \ + --ignore-unfixed \ + --ignorefile "${GITHUB_WORKSPACE}/.trivyignore" \ + --vuln-type os \ + --format sarif \ + --output trivy-image.sarif \ + --exit-code 1 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:scan # Second build is a cache hit (layers exported by the scan build), so it # only re-links and pushes the platform image. @@ -115,10 +120,6 @@ jobs: image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:arm64-${{ github.sha }} steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 - with: - # Pull the pinned ecosystem submodules under external/ so the Docker - # build compiles against the integrated revisions, not each repo's main. - submodules: recursive - name: Set up Docker Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 @@ -149,18 +150,27 @@ jobs: BUILD_DATE=${{ github.event.head_commit.timestamp }} - name: Scan image with Trivy - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: - image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:scan - format: sarif - output: trivy-image.sarif - severity: CRITICAL,HIGH + version: v0.70.0 + cache: true + - name: Run Trivy scan (sarif) + shell: bash + run: | # Go reachability is enforced separately by govulncheck in CI. The # binary also carries the full workspace module graph, including # non-reachable packages that Trivy reports as binary findings. - vuln-type: os - ignore-unfixed: true - exit-code: '1' # Block publishing images with actionable vulnerabilities + # CVE-2026-14456 (OpenSSL) is ignored via .trivyignore — the fixed + # libcrypto 3.5.8-r0 is not yet published in Alpine 3.23. + trivy image \ + --severity CRITICAL,HIGH \ + --ignore-unfixed \ + --ignorefile "${GITHUB_WORKSPACE}/.trivyignore" \ + --vuln-type os \ + --format sarif \ + --output trivy-image.sarif \ + --exit-code 1 \ + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:scan # Second build is a cache hit (layers exported by the scan build), so it # only re-links and pushes the platform image. @@ -262,18 +272,25 @@ jobs: cache-to: type=gha,mode=max,scope=hawk-sandbox - name: Scan sandbox image - uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + uses: aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514 with: - image-ref: ${{ env.REGISTRY }}/graycodeai/hawk-sandbox:scan - format: sarif - output: trivy-sandbox-image.sarif - severity: CRITICAL,HIGH + version: v0.70.0 + cache: true + - name: Run Trivy sandbox scan (sarif) + shell: bash + run: | # This image gate covers the Debian OS package surface. npm's # bundled CLI dependency tree is pinned by the Node base image and # reviewed separately from the runtime OS scan. - vuln-type: os - ignore-unfixed: true - exit-code: '1' + trivy image \ + --severity CRITICAL,HIGH \ + --ignore-unfixed \ + --ignorefile "${GITHUB_WORKSPACE}/.trivyignore" \ + --vuln-type os \ + --format sarif \ + --output trivy-sandbox-image.sarif \ + --exit-code 1 \ + ${{ env.REGISTRY }}/graycodeai/hawk-sandbox:scan - name: Build and publish public sandbox image uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7077661..c0c4a76d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,33 +22,10 @@ jobs: with: fetch-depth: 0 # goreleaser needs full history for changelog - # Releases must use Gitlink pins only — never a branch head fallback. + # Releases build against the sibling repos resolved via the workspace go.work. - uses: ./.github/actions/checkout-eyrie with: - allow_branch_fallback: "false" - - - name: Verify Gitlink pins present - run: | - set -euo pipefail - missing=0 - for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do - commit=$(git ls-tree HEAD "external/${repo}" | awk '{print $3}' || true) - if [ -z "$commit" ]; then - echo "::error::Release requires Gitlink for external/${repo}" - missing=1 - else - head=$(git -C "external/${repo}" rev-parse HEAD) - if [ "$head" != "$commit" ]; then - echo "::error::external/${repo} checked out $head, Gitlink wants $commit" - missing=1 - else - echo "OK external/${repo} @ ${commit:0:12}" - fi - fi - done - if [ "$missing" -ne 0 ]; then - exit 1 - fi + ref: ${{ github.ref_name }} - name: Set up Go uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 92f18c3d..00000000 --- a/.gitmodules +++ /dev/null @@ -1,24 +0,0 @@ -[submodule "external/eyrie"] - path = external/eyrie - url = https://github.com/GrayCodeAI/eyrie.git -[submodule "external/yaad"] - path = external/yaad - url = https://github.com/GrayCodeAI/yaad.git -[submodule "external/tok"] - path = external/tok - url = https://github.com/GrayCodeAI/tok.git -[submodule "external/sight"] - path = external/sight - url = https://github.com/GrayCodeAI/sight.git -[submodule "external/inspect"] - path = external/inspect - url = https://github.com/GrayCodeAI/inspect.git -[submodule "external/trace"] - path = external/trace - url = https://github.com/GrayCodeAI/trace.git -[submodule "external/hawk-core-contracts"] - path = external/hawk-core-contracts - url = https://github.com/GrayCodeAI/hawk-core-contracts.git -[submodule "hawk-mcpkit"] - path = external/hawk-mcpkit - url = https://github.com/GrayCodeAI/hawk-mcpkit.git diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 00000000..8dca84a9 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,10 @@ +# Trivy OS-package ignore list for Hawk Docker images. +# +# CVE-2026-14456 — OpenSSL DoS via unbounded memory (libcrypto3/libssl3). +# Fixed upstream in OpenSSL 3.5.8-r0, but that package is NOT yet published in +# the Alpine 3.23 repository (the latest alpine:3.23 still ships 3.5.7-r0, as +# of 2026-08-27). Hawk is a Go binary and does not link libcrypto; this affects +# only the base OS TLS stack and is not reachable from Hawk's runtime. Re-add a +# base-image bump to remove this entry once Alpine 3.23 publishes OpenSSL +# 3.5.8-r0. +CVE-2026-14456 diff --git a/AGENTS.md b/AGENTS.md index 1f88069d..1306f91a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -192,7 +192,7 @@ Legacy `hawk/shared/types` has been removed. Cross-repo severity and finding con ### Architecture note: provider ownership Implement provider protocols, adapters, catalog metadata, credential mappings, and -provider contract tests in `external/eyrie` first. Hawk consumes providers only +provider contract tests in `../eyrie` (the eyrie sibling repo) first. Hawk consumes providers only through Eyrie's stable engine facade; Hawk changes should be limited to host UX and facade integration. Concentrate AI is a pay-as-you-go gateway implemented with its native Responses API (`/v1/responses`) under the @@ -243,13 +243,12 @@ This project is indexed by GitNexus as **hawk** (97743 symbols, 322940 relations -### Submodule workflow (external/ repos) +### Workspace workflow (sibling repos) -hawk depends on ecosystem repos (`eyrie`, `hawk-core-contracts`, etc.) via git submodules under `external/`. Hawk's `go.work` points to `./external/`, so changes in the submodule are automatically picked up by hawk. +hawk depends on ecosystem repos (`eyrie`, `hawk-core-contracts`, etc.) as independent sibling repos in the `graycode-eco` workspace. Hawk's `go.work` lists them as `../`, so local changes in any sibling are automatically picked up by hawk. Each sibling is its own git repo, versioned and released independently. -1. Edit + test in `hawk/external/` — run its tests, run `make test` in hawk -2. Push from the submodule: `git push origin ` -3. Sync to the independent repo: `cd ../ && git fetch origin && git checkout ` -4. PR → merge from the independent repo -5. Pull main in the submodule: `cd ../hawk/external/ && git checkout main && git pull origin main` -6. Commit the pointer in hawk: `cd .. && git add external/ && git commit -m "chore: update "` +1. Edit + test in `../` — run its tests, run `make test` in hawk +2. Push from the sibling: `git push origin ` +3. Open a PR in the sibling repo → merge to `main` +4. Ensure hawk's `go.mod` pins a version that resolves to (or is an ancestor of) the sibling's `main` — run `make sync` to verify parity +5. No pointer commits: hawk resolves the sibling via `go.work` for local dev and via the pinned `go.mod` version for standalone/module-mode builds (Docker, released consumers) diff --git a/Dockerfile b/Dockerfile index 23a63ddd..a60275cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,9 +8,10 @@ RUN apk upgrade --no-cache && \ WORKDIR /build -# GrayCodeAI sibling modules are unpublished at their current code (the public proxy -# froze v0.1.0 at old commits). Resolve them locally via a generated go.work -# (use . + replace => ./external/), bypassing the proxy/sumdb entirely. +# GrayCodeAI engine modules are published and pinned in go.mod at tagged or +# commit-pseudo versions, resolved from the module proxy. The committed +# go.work (which references sibling checkouts ../) is excluded from the +# build context, so build in module mode (no go.work) against those pins. ENV GOPRIVATE=github.com/GrayCodeAI/* \ GONOSUMDB=github.com/GrayCodeAI/* \ GONOSUMCHECK=1 @@ -25,12 +26,9 @@ ARG BUILD_DATE=unknown COPY . . -# external/ are committed submodules pinned to the integrated revisions -# (populated by `submodules: recursive` in .github/workflows/docker.yml, or -# `git submodule update --init --recursive` for a local `docker build`). Build -# against those pinned checkouts via a generated go.work — the committed -# go.work/go.work.sum are excluded by .dockerignore, and the public proxy froze -# v0.1.0 at older commits. Do NOT run 'go mod download' first. +# Build against the engine versions pinned in go.mod via the module proxy. The +# committed go.work/go.work.sum (sibling-checkout based) are excluded by +# .dockerignore and must not be present for a module-mode build. # # main.Version / main.Commit / main.BuildDate are baked in from the ARGs above; # this is the only correct source — `git describe` would always return empty @@ -43,10 +41,6 @@ COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ rm -f go.work go.work.sum && \ - { echo "go 1.26.6"; echo; echo "use ."; echo; echo "replace ("; \ - for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do \ - echo " github.com/GrayCodeAI/${repo} => ./external/${repo}"; \ - done; echo ")"; } > go.work && \ CGO_ENABLED=0 GOOS=linux go build -trimpath \ -ldflags="-s -w \ -X main.Version=${VERSION} \ diff --git a/Dockerfile.daemon b/Dockerfile.daemon index b920ad11..5ed3b77e 100644 --- a/Dockerfile.daemon +++ b/Dockerfile.daemon @@ -24,10 +24,6 @@ COPY . . RUN --mount=type=cache,target=/go/pkg/mod \ --mount=type=cache,target=/root/.cache/go-build \ rm -f go.work go.work.sum && \ - { echo "go 1.26.6"; echo; echo "use ."; echo; echo "replace ("; \ - for repo in hawk-core-contracts eyrie inspect sight tok trace yaad; do \ - echo " github.com/GrayCodeAI/${repo} => ./external/${repo}"; \ - done; echo ")"; } > go.work && \ CGO_ENABLED=0 GOOS=linux go build -trimpath \ -ldflags="-s -w \ -X main.Version=${VERSION} \ diff --git a/Makefile b/Makefile index 7484abb1..834076a7 100644 --- a/Makefile +++ b/Makefile @@ -36,8 +36,8 @@ GORELEASER := $(GOBIN_DIR)/goreleaser # --------------------------------------------------------------------------- # Phony declarations (alphabetical). # --------------------------------------------------------------------------- -.PHONY: all bench boundaries build check-replace ci clean contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard package-boundaries-guard submodule-release-parity cover cover-new fmt help install lint lint-fix \ - release security setup smoke path sync-external test test-10x test-live test-new test-race tidy version vet api-docs api-validate +.PHONY: all bench boundaries build check-replace ci clean contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard package-boundaries-guard release-parity cover cover-new fmt help install lint lint-fix \ + release security setup smoke path sync test test-10x test-live test-new test-race tidy version vet api-docs api-validate check-replace: ## Fail if go.mod has local replace directives (run before tagging) @bash scripts/check-no-replace-directives.sh @@ -132,7 +132,7 @@ package-boundaries-guard: ## Enforce AST/package-graph boundaries with file/line boundaries: contracts-guard ecosystem-guard eyrie-client-guard eyrie-engine-guard peer-guard internal-layers-guard package-boundaries-guard ## Alias for all boundary guards (matches `make boundaries` in engine repos). -submodule-release-parity: ## Verify every go.mod ecosystem version resolves to its pinned Gitlink. +release-parity: ## Verify every go.mod ecosystem version is reachable from its sibling repo HEAD. bash ./scripts/check-submodule-release-parity.sh lint: ## Run golangci-lint. @@ -182,40 +182,18 @@ FOUNDATION_REPOS := hawk-core-contracts ECO_REPOS := eyrie inspect sight tok trace yaad hawk-mcpkit WORKSPACE_REPOS := $(FOUNDATION_REPOS) $(ECO_REPOS) -setup: ## Set up local development environment (go.work + external repos). +setup: ## Set up local development environment (workspace go.work + sibling repos). @echo "=== Setting up hawk development environment ===" - @mkdir -p external - @for repo in $(WORKSPACE_REPOS); do \ - dest="external/$$repo"; \ - if git -C "$$dest" rev-parse --git-dir >/dev/null 2>&1; then \ - echo "✓ $$dest already exists"; \ - else \ - commit=$$(git ls-tree HEAD "$$dest" 2>/dev/null | awk '{print $$3}' || true); \ - if [ -n "$$commit" ]; then \ - echo "Cloning $$repo at pinned commit $$commit..."; \ - git clone "https://github.com/GrayCodeAI/$$repo.git" "$$dest" 2>/dev/null && \ - (cd "$$dest" && git checkout --quiet "$$commit") || \ - echo " ⚠ Could not clone $$repo at pinned commit $$commit"; \ - else \ - ref=$$(git branch --show-current 2>/dev/null || echo main); \ - if ! git ls-remote --heads "https://github.com/GrayCodeAI/$$repo.git" "$$ref" | grep -q .; then \ - ref=main; \ - fi; \ - echo "Cloning $$repo at branch $$ref..."; \ - git clone --depth=1 --branch "$$ref" "https://github.com/GrayCodeAI/$$repo.git" "$$dest" 2>/dev/null || \ - echo " ⚠ Could not clone $$repo (may not exist yet or no access)"; \ - fi; \ - fi; \ - done - @echo "Generating go.work..." + @echo "Generating go.work (workspace model)..." @echo "go 1.26.6" > go.work @echo "" >> go.work - @echo "use ." >> go.work - @echo "" >> go.work - @echo "replace (" >> go.work + @echo "use (" >> go.work + @echo " ." >> go.work @for repo in $(WORKSPACE_REPOS); do \ - if [ -d "external/$$repo" ]; then \ - echo " github.com/GrayCodeAI/$$repo => ./external/$$repo" >> go.work; \ + if [ -d "../$$repo" ]; then \ + echo " ../$$repo" >> go.work; \ + else \ + echo " ⚠ sibling repo ../$$repo not found — clone it into the graycode-eco workspace"; \ fi; \ done @echo ")" >> go.work @@ -260,26 +238,14 @@ compat-check: ## Strict validation — non-zero exit if any component lacks a ve compat-drift: ## Advisory: report pin drift between hawk's go.mod and external/ submodules. Never fails. @go run ./cmd/compat-test -check-external -file=testdata/compatibility-matrix.json -.PHONY: hooks sync-submodules sync-submodule-versions sync-clone +.PHONY: hooks sync hooks: ## Install git hooks via lefthook (formatting, linting, conventional commits). @command -v lefthook >/dev/null 2>&1 || (echo "install: go install github.com/evilmartians/lefthook@latest" && exit 1) lefthook install -sync-submodules: ## Fetch and checkout latest origin/main for all external/ submodules. - git submodule foreach 'git fetch origin && git checkout origin/main 2>/dev/null || git checkout origin/HEAD' - @echo "Submodule heads:" - @git submodule status - -sync-submodule-versions: ## After advancing submodules, bump go.mod requires to match each gitlink. - @chmod +x scripts/sync-submodule-versions.sh - @./scripts/sync-submodule-versions.sh - -sync-external: ## Read-only drift report: external/ pin vs sibling ../ HEAD. - @bash ./scripts/sync-external.sh - -sync-clone: ## Hard-reset hawk and submodules to origin/main (post history rewrite). - @chmod +x scripts/sync-clone.sh scripts/commit-clean.sh - @./scripts/sync-clone.sh +sync: ## Sync the workspace go.work and verify sibling release parity. + @go work sync + @bash ./scripts/check-submodule-release-parity.sh # === Cross-platform binary targets (add after existing 'build' target) === .PHONY: build-all build-static size-check diff --git a/README.md b/README.md index 99b91080..97664f43 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ hawk path # verify readiness ```bash git clone https://github.com/GrayCodeAI/hawk && cd hawk -make setup # clones required support repos into external/ and syncs go.work +make setup # generates go.work referencing sibling support repos in the graycode-eco workspace go build -o hawk ./cmd/hawk ./hawk @@ -434,15 +434,14 @@ hawk/ ├── docs/ # Architecture, security, integration docs └── testdata/ # Test fixtures -External ecosystem modules (git submodules): -├── external/ -│ ├── eyrie/ # LLM provider runtime -│ ├── hawk-core-contracts/ # Shared cross-repo types -│ ├── inspect/ # Security audit library -│ ├── sight/ # Diff-based code review -│ ├── tok/ # Tokenizer, compression, secrets scanning -│ ├── trace/ # Session capture and replay -│ └── yaad/ # Graph-based persistent memory +Ecosystem sibling repos (in the graycode-eco workspace): +├── eyrie/ # LLM provider runtime +├── hawk-core-contracts/ # Shared cross-repo types +├── inspect/ # Security audit library +├── sight/ # Diff-based code review +├── tok/ # Tokenizer, compression, secrets scanning +├── trace/ # Session capture and replay +└── yaad/ # Graph-based persistent memory ``` ### Ecosystem @@ -456,8 +455,8 @@ hawk integrates these GrayCodeAI repos in three layers: Local development uses: - **`go.mod` modules:** pinned requirements for the support engines and `hawk-core-contracts` -- **External checkout + `go.work`:** clone support repos under `external/`; `go.work` maps the module paths to those local checkouts -- **Submodules in this repo:** the same external layout is pinned under `external/` for reproducible CI and multi-repo work +- **Workspace + `go.work`:** sibling support repos are cloned in the `graycode-eco` workspace (as `../`); `go.work` resolves the module paths to those local checkouts +- **Module-mode builds:** standalone / Docker builds resolve the pinned `go.mod` versions from the module proxy (no workspace) Cross-repo contracts now live in **`github.com/GrayCodeAI/hawk-core-contracts`** so support repos do not depend on Hawk internals. The old `hawk/shared/types` path has been removed; use `hawk-core-contracts/types` for shared severity and finding contracts. diff --git a/cmd/compat-test/drift.go b/cmd/compat-test/drift.go index c480ded9..f66e3cff 100644 --- a/cmd/compat-test/drift.go +++ b/cmd/compat-test/drift.go @@ -19,19 +19,19 @@ var trackedPins = []string{ } // checkDrift compares hawk's own go.mod requirements for trackedPins against -// what each external/ submodule (a locally pinned clone of a hawk dependency) -// declares for the same modules in its own go.mod. It never fails — this is -// advisory, printed for humans/CI logs to notice, not a build gate. +// what each sibling repo (a peer checkout of a hawk dependency in the shared +// workspace) declares for the same modules in its own go.mod. It never fails — +// this is advisory, printed for humans/CI logs to notice, not a build gate. func checkDrift(repoRoot string) error { hawkRequires, err := readRequires(filepath.Join(repoRoot, "go.mod")) if err != nil { return fmt.Errorf("read hawk go.mod: %w", err) } - externalDir := filepath.Join(repoRoot, "external") - entries, err := os.ReadDir(externalDir) + workspaceDir := filepath.Join(repoRoot, "..") + entries, err := os.ReadDir(workspaceDir) if err != nil { - return fmt.Errorf("read external/: %w", err) + return fmt.Errorf("read workspace (%s): %w", workspaceDir, err) } fmt.Println("Pin freshness (advisory — see docs/compatibility.md):") @@ -40,10 +40,10 @@ func checkDrift(repoRoot string) error { if !e.IsDir() { continue } - modPath := filepath.Join(externalDir, e.Name(), "go.mod") + modPath := filepath.Join(workspaceDir, e.Name(), "go.mod") consumerRequires, err := readRequires(modPath) if err != nil { - continue // submodule not checked out / no go.mod — skip silently + continue // sibling not a Go module / no go.mod — skip silently } for _, pin := range trackedPins { hawkVer, hawkHas := hawkRequires[pin] @@ -57,7 +57,7 @@ func checkDrift(repoRoot string) error { } } if drifted == 0 { - fmt.Println(" OK — no drift between hawk's pins and external/ consumers") + fmt.Println(" OK — no drift between hawk's pins and sibling consumers") } return nil } diff --git a/cmd/compat-test/drift_test.go b/cmd/compat-test/drift_test.go index d71e02a0..1d7114fc 100644 --- a/cmd/compat-test/drift_test.go +++ b/cmd/compat-test/drift_test.go @@ -70,15 +70,15 @@ func TestReadRequires_InvalidMod(t *testing.T) { // TestCheckDrift reports drift when a consumer pins an older version than hawk. func TestCheckDrift_DetectsDrift(t *testing.T) { - root := t.TempDir() - writeMod(t, filepath.Join(root, "go.mod"), `module github.com/GrayCodeAI/hawk + ws := t.TempDir() + writeMod(t, filepath.Join(ws, "hawk", "go.mod"), `module github.com/GrayCodeAI/hawk go 1.26 require github.com/GrayCodeAI/hawk-core-contracts v1.5.0 `) - // Consumer pins an older version of the shared contract. - writeMod(t, filepath.Join(root, "external", "inspect", "go.mod"), `module github.com/GrayCodeAI/inspect + // Consumer sibling pins an older version of the shared contract. + writeMod(t, filepath.Join(ws, "inspect", "go.mod"), `module github.com/GrayCodeAI/inspect go 1.26 @@ -89,7 +89,7 @@ require github.com/GrayCodeAI/hawk-core-contracts v1.2.0 old := os.Stdout r, w, _ := os.Pipe() os.Stdout = w - err := checkDrift(root) + err := checkDrift(filepath.Join(ws, "hawk")) _ = w.Close() os.Stdout = old _, _ = buf.ReadFrom(r) @@ -105,14 +105,14 @@ require github.com/GrayCodeAI/hawk-core-contracts v1.2.0 // TestCheckDrift_NoDriftWhenVersionsMatch verifies the happy path: matching // pins produce the "OK" line and no per-consumer drift lines. func TestCheckDrift_NoDriftWhenVersionsMatch(t *testing.T) { - root := t.TempDir() - writeMod(t, filepath.Join(root, "go.mod"), `module github.com/GrayCodeAI/hawk + ws := t.TempDir() + writeMod(t, filepath.Join(ws, "hawk", "go.mod"), `module github.com/GrayCodeAI/hawk go 1.26 require github.com/GrayCodeAI/hawk-core-contracts v1.5.0 `) - writeMod(t, filepath.Join(root, "external", "sight", "go.mod"), `module github.com/GrayCodeAI/sight + writeMod(t, filepath.Join(ws, "sight", "go.mod"), `module github.com/GrayCodeAI/sight go 1.26 @@ -123,7 +123,7 @@ require github.com/GrayCodeAI/hawk-core-contracts v1.5.0 r, w, _ := os.Pipe() old := os.Stdout os.Stdout = w - err := checkDrift(root) + err := checkDrift(filepath.Join(ws, "hawk")) _ = w.Close() os.Stdout = old _, _ = buf.ReadFrom(r) @@ -136,30 +136,30 @@ require github.com/GrayCodeAI/hawk-core-contracts v1.5.0 } } -// TestCheckDrift_SkipsMissingSubmodules verifies that a consumer directory -// without a go.mod (e.g. not checked out) is skipped without failing. -func TestCheckDrift_SkipsMissingSubmodules(t *testing.T) { - root := t.TempDir() - writeMod(t, filepath.Join(root, "go.mod"), `module github.com/GrayCodeAI/hawk +// TestCheckDrift_SkipsMissingSiblings verifies that a sibling directory +// without a go.mod (e.g. not a Go module) is skipped without failing. +func TestCheckDrift_SkipsMissingSiblings(t *testing.T) { + ws := t.TempDir() + writeMod(t, filepath.Join(ws, "hawk", "go.mod"), `module github.com/GrayCodeAI/hawk go 1.26 require github.com/GrayCodeAI/hawk-core-contracts v1.5.0 `) // Directory present but no go.mod — must be skipped silently. - if err := os.MkdirAll(filepath.Join(root, "external", "not-checked-out"), 0o755); err != nil { + if err := os.MkdirAll(filepath.Join(ws, "not-checked-out"), 0o755); err != nil { t.Fatal(err) } r, w, _ := os.Pipe() old := os.Stdout os.Stdout = w - err := checkDrift(root) + err := checkDrift(filepath.Join(ws, "hawk")) _ = w.Close() os.Stdout = old _, _ = io.Copy(io.Discard, r) if err != nil { - t.Fatalf("checkDrift with missing submodule go.mod: %v", err) + t.Fatalf("checkDrift with missing sibling go.mod: %v", err) } } diff --git a/cmd/compat-test/main.go b/cmd/compat-test/main.go index f352e26a..8f7ae7d8 100644 --- a/cmd/compat-test/main.go +++ b/cmd/compat-test/main.go @@ -20,7 +20,7 @@ // go run ./cmd/compat-test -check-external # advisory: compare hawk's own // # go.mod pins for shared leaf // # deps against what each -// # external/ submodule declares. +// # sibling repo declares. // # Always exits 0; see drift.go. package main @@ -52,7 +52,7 @@ func main() { matrixName := flag.String("matrix", "next", "matrix entry to inspect (next, stable, ...)") strict := flag.Bool("strict", false, "exit non-zero if any component lacks a pinned version") path := flag.String("file", findMatrixFile(), "path to compatibility-matrix.json") - checkExternal := flag.Bool("check-external", false, "advisory: report pin drift against external/ submodules and exit (see drift.go)") + checkExternal := flag.Bool("check-external", false, "advisory: report pin drift against sibling repos and exit (see drift.go)") flag.Parse() if *path == "" { diff --git a/docs/DYNAMIC-MODELS.md b/docs/DYNAMIC-MODELS.md index ffa7b223..574c8c51 100644 --- a/docs/DYNAMIC-MODELS.md +++ b/docs/DYNAMIC-MODELS.md @@ -100,9 +100,9 @@ hosts must be isolated from one another. 2. Add Eyrie tests for cache and live discovery, credential status, selection, and generation/streaming. 3. Commit and verify standalone Eyrie. -4. Advance Hawk's `external/eyrie` submodule to that exact commit, then update +4. Advance Hawk's `../eyrie` sibling checkout to that exact commit, then update Hawk's module version when the Eyrie revision is published. -5. Verify both the clean submodule (`go.work`) and published-module +5. Verify both the workspace (`go.work`) and published-module (`GOWORK=off`) build modes. Hawk changes are needed only for a new product behavior or an additive diff --git a/docs/architecture.md b/docs/architecture.md index a08631b8..fa17a859 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -52,7 +52,7 @@ hawk/ │ ├── bridge/ link Bridges to ecosystem services │ └── resilience/ refresh-cw Circuit breaker, retry, rate limit ├── docs/ book-open Architecture docs -└── external/ link Pinned ecosystem submodules for go.work integration +└── (ecosystem siblings live at ../ in the graycode-eco workspace; see docs/architecture/ecosystem-design.md) ``` Legacy note: `hawk/shared/types` has been removed. Shared cross-repo severity @@ -129,5 +129,5 @@ Tool Call → `: + +```go +go 1.26.6 +use ( + . + ../eyrie + ../yaad + ../tok + ../trace + ../sight + ../inspect + ../hawk-core-contracts + ../hawk-mcpkit +) +``` + +- No `git submodule`s, no committed `replace` directives. +- Clone all repos into `graycode-eco/`, run `make setup` in `hawk` to regenerate + `go.work`, then `make sync` / `go work sync`. +- Local edits in any sibling are picked up immediately by hawk's build. + +### 3. Published connectivity: Go module versions + +Standalone and module-mode builds (Docker, released consumers) resolve the engine +versions pinned in `hawk/go.mod` from the module proxy. Each engine is released +independently with its own semver tag. `hawk-core-contracts` is released first, +then engines bump to it, then hawk bumps to the engines. + +Release order: **contracts → engines → hawk.** + +### 4. HTTP connectivity (non-Go components) + +Components outside the Go module graph connect over public APIs: + +- **`hawk-cloud`** (Cloudflare Worker) — hosted control plane (tenancy, usage, + audit, optional sync). Hawk syncs to it via fail-open, user-approved HTTP + (`POST /v1/graph/sync`, usage events). Needed only when a user opts into cloud. +- **`graycode-core`** — the GrayCode web/backend monorepo. Not a Hawk runtime + dependency. It consumes `hawk-cloud`'s public API; hawk sends only opt-in, + fail-open telemetry to it over HTTP (ADR-0001). Never a Go import. +- **`hawk-graph`** (Node dashboard) — dev tooling that reads architecture data; + no Go module link. + +## Enforcement guards (scripts + CI) + +| Guard | Script / target | +|---|---| +| No engine→engine imports | `check-support-repo-coupling.sh` | +| No engine→hawk imports | `check-ecosystem-boundaries.sh` | +| No legacy `shared/types` | `check-shared-types-imports.sh` | +| Eyrie only via `engine` facade | `check-eyrie-client-imports.sh`, `check-eyrie-engine-boundary.sh` | +| No committed `replace` | `check-no-replace-directives.sh` | +| go.mod version reachable from sibling HEAD | `check-submodule-release-parity.sh` (`make release-parity`) | + +## Best practices + +- Bump `hawk-core-contracts` first; keep all engines on the same latest tag at + each ecosystem release to avoid version skew. +- Never commit `replace` directives into `go.mod`. +- Add a CI integration matrix so each engine builds against the latest contracts + and one job compiles hawk + all engines together. diff --git a/docs/architecture/plan.md b/docs/architecture/plan.md index 6c283f41..7cb3799c 100644 --- a/docs/architecture/plan.md +++ b/docs/architecture/plan.md @@ -151,10 +151,10 @@ services; it is not a claim that those fields have already moved. ## Dependencies -- **eyrie:** LLM provider engine behind `eyrie/engine` (external submodule) -- **yaad:** Graph-based persistent memory (external submodule) -- **tok:** Tokenizer, compression (external submodule) -- **hawk-core-contracts:** Shared types (external submodule) +- **eyrie:** LLM provider engine behind `eyrie/engine` (workspace sibling) +- **yaad:** Graph-based persistent memory (workspace sibling) +- **tok:** Tokenizer, compression (workspace sibling) +- **hawk-core-contracts:** Shared types (workspace sibling) ## Success Metrics diff --git a/docs/architecture/spec.md b/docs/architecture/spec.md index f2473399..c1419ca5 100644 --- a/docs/architecture/spec.md +++ b/docs/architecture/spec.md @@ -28,7 +28,7 @@ layout: |-----------|---------| | `cmd/` | CLI entry point (Cobra) and TUI (Bubble Tea) | | `internal/` | Private Go packages (not importable by external repos) | -| `external/` | Pinned ecosystem submodules (eyrie, yaad, tok, inspect, sight, trace, hawk-core-contracts) | +| `go.work` | Resolves ecosystem siblings (eyrie, yaad, tok, inspect, sight, trace, hawk-core-contracts) | | `spec/` | OpenSpec schema consumed by `internal/spec` | | `docs/` | Architecture docs, design docs, plans | | `rules/` | User-defined rules | diff --git a/docs/architecture/tasks.md b/docs/architecture/tasks.md index 62989705..8ac600ff 100644 --- a/docs/architecture/tasks.md +++ b/docs/architecture/tasks.md @@ -18,7 +18,7 @@ ### Repository Structure (REQ-1, REQ-2) - [ ] Verify all internal/ packages exist and are documented -- [ ] Verify external/ submodules are pinned correctly +- [ ] Verify go.work sibling repos are checked out correctly - [ ] Verify spec/ reference repos are up to date - [ ] Check for orphaned or unused packages diff --git a/docs/troubleshooting-guide.md b/docs/troubleshooting-guide.md index ccddc737..e1b80d44 100644 --- a/docs/troubleshooting-guide.md +++ b/docs/troubleshooting-guide.md @@ -85,9 +85,11 @@ curl -v http://localhost:4590/v1/ready Check the response body for the specific failed check. Common causes: - No model configured — set `HAWK_MODEL` or provider credentials. -- Eyrie catalog not initialized — ensure submodules are checked out: +- Eyrie catalog not initialized — ensure the ecosystem siblings are present in + the graycode-eco workspace and run `make setup` in hawk (regenerates `go.work`): ```bash - git submodule update --init --recursive + make setup + go work sync ``` ### `GET /v1/health` returns 503 diff --git a/external/eyrie b/external/eyrie deleted file mode 160000 index 6057740d..00000000 --- a/external/eyrie +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 6057740d18c72f38aaec90515427af4efdc7deae diff --git a/external/hawk-core-contracts b/external/hawk-core-contracts deleted file mode 160000 index 16ebcfd5..00000000 --- a/external/hawk-core-contracts +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 16ebcfd5ad6e298c9cace718af8e958d2e3e299f diff --git a/external/hawk-mcpkit b/external/hawk-mcpkit deleted file mode 160000 index 82c1c610..00000000 --- a/external/hawk-mcpkit +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 82c1c610efe3bf4390da964035708b364cbcb216 diff --git a/external/inspect b/external/inspect deleted file mode 160000 index 8556ee05..00000000 --- a/external/inspect +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8556ee05ff07459cd546057b40aae2d97843b829 diff --git a/external/sight b/external/sight deleted file mode 160000 index 39553454..00000000 --- a/external/sight +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 39553454cd601d5f13d3c983520a9a10f1bb016f diff --git a/external/tok b/external/tok deleted file mode 160000 index a7c4b99d..00000000 --- a/external/tok +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a7c4b99d37b8241d43e838f9d3c648a70fdc22f1 diff --git a/external/trace b/external/trace deleted file mode 160000 index 59b437bb..00000000 --- a/external/trace +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 59b437bbe8dc62f1d601cd9f124ad9aad58b4b97 diff --git a/external/yaad b/external/yaad deleted file mode 160000 index c968f179..00000000 --- a/external/yaad +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c968f1798a5b7c839ba13db1f313c38032fd9543 diff --git a/go.work b/go.work index e741f6c7..fa4d62fe 100644 --- a/go.work +++ b/go.work @@ -1,14 +1,13 @@ go 1.26.6 -use . - -replace ( - github.com/GrayCodeAI/eyrie => ./external/eyrie - github.com/GrayCodeAI/hawk-core-contracts => ./external/hawk-core-contracts - github.com/GrayCodeAI/hawk-mcpkit => ./external/hawk-mcpkit - github.com/GrayCodeAI/inspect => ./external/inspect - github.com/GrayCodeAI/sight => ./external/sight - github.com/GrayCodeAI/tok => ./external/tok - github.com/GrayCodeAI/trace => ./external/trace - github.com/GrayCodeAI/yaad => ./external/yaad +use ( + . + ../eyrie + ../hawk-core-contracts + ../hawk-mcpkit + ../inspect + ../sight + ../tok + ../trace + ../yaad ) diff --git a/lefthook.yml b/lefthook.yml index 5ca6637c..ee17b025 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -124,11 +124,11 @@ pre-push: boundary-ecosystem: run: bash scripts/check-ecosystem-boundaries.sh - external-drift: + release-parity: run: | - # Informational only — warns if external/ submodule pins have fallen - # behind the sibling dev clones, but never blocks a push. - bash scripts/sync-external.sh || true + # Informational only — warns if go.mod versions have fallen behind the + # sibling dev clones, but never blocks a push. + bash scripts/check-submodule-release-parity.sh || true # --------------------------------------------------------------------------- # commit-msg — validate Conventional Commits and strip AI co-author trailers. diff --git a/scripts/check-ecosystem-boundaries.sh b/scripts/check-ecosystem-boundaries.sh index e97e3ae2..235189c6 100755 --- a/scripts/check-ecosystem-boundaries.sh +++ b/scripts/check-ecosystem-boundaries.sh @@ -7,13 +7,6 @@ cd "$ROOT_DIR" pattern='github\.com/GrayCodeAI/hawk/(internal/|shared/types)' violations="" -external_hits="$( - git grep -n -E "${pattern}" -- 'external/**/*.go' || true -)" -if [[ -n "${external_hits}" ]]; then - violations+="${external_hits}"$'\n' -fi - for repo in ../sight ../inspect ../tok ../trace ../yaad ../eyrie; do if [[ -d "${repo}" ]]; then repo_hits="$( diff --git a/scripts/check-submodule-release-parity.sh b/scripts/check-submodule-release-parity.sh index 8cdedc13..f1566f7d 100755 --- a/scripts/check-submodule-release-parity.sh +++ b/scripts/check-submodule-release-parity.sh @@ -1,39 +1,48 @@ #!/usr/bin/env bash set -euo pipefail -# Compare each engine Gitlink (from the superproject index) to the commit -# resolved by the go.mod module version. Uses `git ls-tree` so the check -# works without `git submodule update` / checkout-eyrie having populated -# external/* working trees. +# Workspace release parity: for each Go module hawk requires, verify that the +# version pinned in hawk's go.mod resolves to a commit reachable from the +# sibling repo's HEAD. This catches hawk pinning a version that the sibling +# repo has not published / is behind on. Run from the hawk repo root with the +# ecosystem cloned as siblings (../) in the graycode-eco workspace. repos=(hawk-core-contracts eyrie inspect sight tok trace yaad hawk-mcpkit) failed=0 -printf '%-24s %-14s %-14s %s\n' MODULE GITLINK MODULE_COMMIT STATUS +printf '%-24s %-14s %-14s %s\n' MODULE MODULE_COMMIT SIBLING_HEAD STATUS for repo in "${repos[@]}"; do module="github.com/GrayCodeAI/${repo}" - gitlink=$(git ls-tree HEAD "external/${repo}" | awk '{print $3}') - if [[ -z "$gitlink" ]]; then - printf '%-24s %-14s %-14s %s\n' "$repo" missing - MISSING_GITLINK - failed=1 + sibling="../${repo}" + + version=$(GOWORK=off go list -m -f '{{.Version}}' "$module" 2>/dev/null || true) + if [[ -z "$version" ]]; then + printf '%-24s %-14s %-14s %s\n' "$repo" unknown - NOT_REQUIRED continue fi - version=$(GOWORK=off go list -m -f '{{.Version}}' "$module") metadata=$(GOWORK=off go mod download -json "${module}@${version}" 2>/dev/null || true) module_commit=$(printf '%s\n' "$metadata" | sed -n 's/.*"Hash": "\([0-9a-f]*\)".*/\1/p' | head -1) + + if [[ ! -d "$sibling/.git" ]]; then + printf '%-24s %-14s %-14s %s\n' "$repo" "${module_commit:0:12}" - NO_SIBLING + failed=1 + continue + fi + sibling_head=$(git -C "$sibling" rev-parse HEAD 2>/dev/null || echo "") + if [[ -z "$module_commit" ]]; then - printf '%-24s %-14s %-14s %s\n' "$repo" "${gitlink:0:12}" unknown UNRESOLVED + printf '%-24s %-14s %-14s %s\n' "$repo" unknown "${sibling_head:0:12}" UNRESOLVED failed=1 - elif [[ "$module_commit" == "$gitlink" ]]; then - printf '%-24s %-14s %-14s %s\n' "$repo" "${gitlink:0:12}" "${module_commit:0:12}" OK + elif git -C "$sibling" merge-base --is-ancestor "$module_commit" "$sibling_head" 2>/dev/null; then + printf '%-24s %-14s %-14s %s\n' "$repo" "${module_commit:0:12}" "${sibling_head:0:12}" OK else - printf '%-24s %-14s %-14s %s\n' "$repo" "${gitlink:0:12}" "${module_commit:0:12}" MISMATCH + printf '%-24s %-14s %-14s %s\n' "$repo" "${module_commit:0:12}" "${sibling_head:0:12}" AHEAD_OF_SIBLING failed=1 fi done if ((failed)); then - echo "submodule/module release parity failed" >&2 + echo "workspace/module release parity failed" >&2 exit 1 fi diff --git a/scripts/check-support-repo-coupling.sh b/scripts/check-support-repo-coupling.sh index b37c16d0..3d504c58 100644 --- a/scripts/check-support-repo-coupling.sh +++ b/scripts/check-support-repo-coupling.sh @@ -34,7 +34,6 @@ scan_dir() { } for repo in "${support_repos[@]}"; do - scan_dir "${repo}" "external/${repo}" scan_dir "${repo}" "../${repo}" done diff --git a/scripts/sync-clone.sh b/scripts/sync-clone.sh deleted file mode 100755 index fb32fb4b..00000000 --- a/scripts/sync-clone.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash -# Hard-reset hawk and all external/ submodules to origin/main. -# Use after a history rewrite or when your clone has stale SHAs. -set -euo pipefail - -cd "$(git rev-parse --show-toplevel)" - -# Guard: this script hard-resets hawk AND all submodules, destroying any -# uncommitted work. Refuse to run on a dirty tree unless explicitly forced. -if [ -n "$(git status --porcelain)" ] && [ "${SYNC_CLONE_FORCE:-}" != "1" ]; then - echo "sync-clone: working tree has uncommitted changes." >&2 - echo "This will HARD-RESET hawk and all external/ submodules to origin/main." >&2 - echo "Commit/stash your work, or re-run with SYNC_CLONE_FORCE=1 to proceed." >&2 - exit 1 -fi - -echo "==> Fetching origin" -git fetch origin - -echo "==> Resetting hawk to origin/main" -git checkout main -git reset --hard origin/main - -echo "==> Updating submodules" -git submodule update --init --recursive -git submodule foreach 'git fetch origin && git checkout origin/main 2>/dev/null || git checkout origin/HEAD' - -echo "==> Done. hawk: $(git rev-parse --short HEAD)" -git submodule status diff --git a/scripts/sync-external.sh b/scripts/sync-external.sh deleted file mode 100755 index 876274de..00000000 --- a/scripts/sync-external.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env bash -# Read-only drift report: compares each external/ submodule's pinned -# commit against the HEAD of the sibling dev clone at ../ (relative to -# the graycode-eco workspace root). Unlike `make sync-submodules` (which mutates -# the submodule checkout), this makes no changes — it only reports. -# -# Typical drift: you commit changes in ../tok, but forget `make -# sync-submodules` + a commit in hawk to bump the external/tok pin. This -# script catches that before it becomes a stale-dependency surprise in CI. -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -if [[ ! -f .gitmodules ]]; then - echo "no .gitmodules file found — nothing to check" - exit 0 -fi - -exit_code=0 -printf '%-28s %-10s %-10s %s\n' "SUBMODULE" "PINNED" "SIBLING" "STATUS" - -while IFS= read -r path; do - name="$(basename "$path")" - sibling="../$name" - - pinned="$(git ls-tree HEAD "$path" 2>/dev/null | awk '{print $3}')" - if [[ -z "$pinned" ]]; then - printf '%-28s %-10s %-10s %s\n' "$path" "none" "-" "NOT-PINNED (submodule never committed)" - exit_code=1 - continue - fi - pinned_short="${pinned:0:10}" - - if [[ ! -d "$sibling/.git" ]]; then - printf '%-28s %-10s %-10s %s\n' "$path" "$pinned_short" "-" "NO-SIBLING (expected clone at $sibling)" - exit_code=1 - continue - fi - - sibling_head="$(git -C "$sibling" rev-parse HEAD 2>/dev/null || echo "")" - if [[ -z "$sibling_head" ]]; then - printf '%-28s %-10s %-10s %s\n' "$path" "$pinned_short" "-" "SIBLING-UNREADABLE" - exit_code=1 - continue - fi - sibling_short="${sibling_head:0:10}" - - if [[ "$pinned" == "$sibling_head" ]]; then - printf '%-28s %-10s %-10s %s\n' "$path" "$pinned_short" "$sibling_short" "OK" - elif git -C "$sibling" merge-base --is-ancestor "$pinned" "$sibling_head" 2>/dev/null; then - printf '%-28s %-10s %-10s %s\n' "$path" "$pinned_short" "$sibling_short" "BEHIND (sibling has newer commits)" - exit_code=1 - elif git -C "$sibling" merge-base --is-ancestor "$sibling_head" "$pinned" 2>/dev/null; then - printf '%-28s %-10s %-10s %s\n' "$path" "$pinned_short" "$sibling_short" "AHEAD (pin is newer than local sibling checkout)" - exit_code=1 - else - printf '%-28s %-10s %-10s %s\n' "$path" "$pinned_short" "$sibling_short" "DIVERGED (different history)" - exit_code=1 - fi -done < <(git config -f .gitmodules --get-regexp path | awk '{print $2}') - -if [[ $exit_code -ne 0 ]]; then - echo - echo "drift detected — run 'make sync-submodules' then 'make sync-submodule-versions'" - echo "(advances the submodule working trees, then bumps hawk's go.mod requires to match the gitlinks), then commit the updated external/ pins" -fi - -exit $exit_code diff --git a/scripts/sync-submodule-versions.sh b/scripts/sync-submodule-versions.sh deleted file mode 100755 index 0d071e53..00000000 --- a/scripts/sync-submodule-versions.sh +++ /dev/null @@ -1,86 +0,0 @@ -#!/usr/bin/env bash -# Bump every external/ submodule's requirement in hawk's go.mod to the exact -# commit the submodule pointer (gitlink) is pinned to. -# -# Hawk tracks its engine dependencies two ways: -# 1. as git submodules external/ -# 2. as Go module requires github.com/GrayCodeAI/ -# in go.mod. These must agree, otherwise `make submodule-release-parity` fails. -# -# `make sync-submodules` advances the submodule working trees, but does NOT -# update go.mod. This script is the companion "sync-versions" step: for every -# submodule that maps to a go.mod require, it runs `go get @` -# so the pseudo-version in go.mod resolves to the same commit as the gitlink. -# -# Run from the repo root: -# make sync-submodules # advance + checkout external/ trees -# make sync-submodule-versions # THEN bump go.mod to match -# -# The script is read-only until it writes — it prints each update and exits -# non-zero on the first `go get` that can't resolve a published commit. -set -euo pipefail - -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT_DIR" - -if [[ ! -f .gitmodules ]]; then - echo "sync-submodule-versions: no .gitmodules — nothing to do" - exit 0 -fi - -# Submodule paths that are also Go modules hawk requires. -# (Derived from WORKSPACE_REPOS in Makefile plus hawk-mcpkit.) Parallel arrays -# keep this portable across bash 3.2 (macOS /bin/bash) and bash 5.x (CI). -repos=( - eyrie - inspect - sight - tok - trace - yaad - hawk-core-contracts - hawk-mcpkit -) -mods=( - github.com/GrayCodeAI/eyrie - github.com/GrayCodeAI/inspect - github.com/GrayCodeAI/sight - github.com/GrayCodeAI/tok - github.com/GrayCodeAI/trace - github.com/GrayCodeAI/yaad - github.com/GrayCodeAI/hawk-core-contracts - github.com/GrayCodeAI/hawk-mcpkit -) - -echo "Syncing go.mod require versions to external/ submodule gitlinks:" -exit_code=0 -for i in "${!repos[@]}"; do - repo="external/${repos[$i]}" - mod="${mods[$i]}" - gitlink="$(git ls-tree HEAD "$repo" 2>/dev/null | awk '{print $3}')" - if [[ -z "$gitlink" ]]; then - echo " $repo: MISSING_GITLINK (submodule never committed) — skipping" - continue - fi - echo " $repo ($mod): $gitlink -> go get $mod@$gitlink" - if ! go get "$mod@$gitlink"; then - echo " ERROR: could not resolve $mod@$gitlink (commit must be pushed)" >&2 - exit_code=1 - fi -done - -if [[ $exit_code -ne 0 ]]; then - echo "sync-submodule-versions: one or more go get calls failed" >&2 - exit $exit_code -fi - -echo "Tidying go.mod / go.sum..." -go mod tidy - -echo "Verifying submodule/module release parity..." -bash ./scripts/check-submodule-release-parity.sh - -echo -echo "Sync complete. go.mod now tracks each external/ submodule's gitlink." -echo "Review with: git diff go.mod go.sum" -echo "Then commit: git add go.mod go.sum external/ && git commit -m 'chore: sync external/ module versions'" diff --git a/scripts/verify-milestone.sh b/scripts/verify-milestone.sh index 08b6d399..def651bd 100755 --- a/scripts/verify-milestone.sh +++ b/scripts/verify-milestone.sh @@ -4,17 +4,17 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" -echo "== eyrie (external) ==" -EYRIE="./external/eyrie" +echo "== eyrie (sibling) ==" +EYRIE="../eyrie" if [[ -d "$EYRIE" ]]; then (cd "$EYRIE" && go test ./... -count=1 -short) else - echo "skip: ./external/eyrie not found" + echo "skip: ../eyrie not found" fi -echo "== external ecosystem modules ==" +echo "== sibling ecosystem modules ==" for module in yaad tok sight inspect trace; do - dir="./external/$module" + dir="../$module" if [[ -d "$dir" ]]; then (cd "$dir" && go test ./... -count=1 -short) else