Skip to content

Release server/v0.6.0 + cli/v0.6.0 — workspaces, hybrid search, auth model, managed tunnels#54

Merged
dvcdsys merged 77 commits into
mainfrom
develop
May 25, 2026
Merged

Release server/v0.6.0 + cli/v0.6.0 — workspaces, hybrid search, auth model, managed tunnels#54
dvcdsys merged 77 commits into
mainfrom
develop

Conversation

@dvcdsys
Copy link
Copy Markdown
Owner

@dvcdsys dvcdsys commented May 25, 2026

Summary

Merge developmain and cut server/v0.6.0 + cli/v0.6.0. This is the largest release since the Python→Go migration: workspaces, hybrid BM25+dense search, ownership/view-group access model, managed tunnels, git polling sync, a Claude Code plugin v0.2.0, and ~44k LoC of new code across 229 files.

76 commits, 5 numbered schema migrations (5–11), 16+ new HTTP endpoints, breaking changes.


⚠️ Breaking changes

These changes require operator attention on upgrade. The migrations themselves are idempotent and crash-safe (versioned via schema_migrations), but the data effects and the on-the-wire contract changes are not silent.

1. All existing users promoted to admin (migration #10)

Pre-v0.6.0 the server had no role distinction in practice — every authenticated user could see everything. The new ownership + view-group model differentiates admin from user. To preserve existing access, migration #10 sets role = 'admin' for every existing user. Operator must demote non-admins after upgrade via PATCH /api/v1/admin/users/{id} or the dashboard's Users page.

2. Local project hashes are namespaced by machine (migration #11)

Project identity for local (CLI-indexed) projects changes from sha1(host_path) to sha1("local:{machine_id}:{display_path}"). The same path on two different machines no longer collides, but old CLI builds (< cli/v0.6.0) send the un-namespaced hash and will not find projects indexed by the new CLI until they re-init. External projects (those with a git_repos peer) are unaffected — their hash key is still sha1(github.com/owner/repo@branch).

Upgrade order: upgrade the server first, then the CLI. Mixed fleets get 404 on existing projects from older CLIs.

3. workspace_repos split → git_repos + workspace_projects (migration #5)

The old workspace_repos table is rewritten into two: git_repos (clone/webhook metadata, keyed by project_path) and workspace_projects (workspace ↔ project links). The migration renames clone directories on disk from {workspace_repos.id} to {path_hash} under $CIX_REPOS_DIR/repos/. Filesystem renames run before the SQL tx and are idempotent — a partial run retries cleanly.

4. users.role default changed 'viewer''user'

New users created via the bootstrap or admin API now default to role='user'. The 'viewer' value was never effectively enforced; 'user' is the new "read your own + group-shared resources" baseline.

5. owner_user_id is required (nullable) on Project + Workspace responses

OpenAPI Project and Workspace schemas now mark owner_user_id in required: (still nullable: true). Strict clients that previously treated the field as optional and refused unknown-but-required fields may need to refresh their generated types. CLI ≥ v0.6.0 and the bundled dashboard already match.

6. Workspace CLI commands speak the new endpoint shape

cix workspace … was rewritten to talk to /workspaces/{id}/projects. Older CLIs talking to a v0.6.0 server will get 404 on the workspace surface.

7. New required envs for external repos

  • CIX_REPOS_DIR — base dir for cloned GitHub repos (default $CIX_SQLITE_DIR/../repos). Legacy alias CIX_WORKSPACES_DATA_DIR still honored.
  • CIX_SECRET_KEY / CIX_SECRET_KEYFILE — required for github_tokens (PATs are AES-256-GCM at rest). Generated keyfile is created on first boot if neither is set.

🚀 Major features

Workspaces

Group indexed projects (rows in projects, optionally with a git_repos peer) for cross-project semantic search. CRUD via /api/v1/workspaces, member management via /workspaces/{id}/projects, hybrid BM25 + dense search via /workspaces/{id}/search. CLI: cix workspace list/create/add/remove/search. Dashboard: full Workspaces tab with live progress + search dialog. Skill: cix-workspace with the cix-workspace-investigator sub-agent.

Includes:

  • PR1 — workspaces skeleton (CRUD + encrypted GitHub tokens)
  • PR2 — workspace_repos + jobs + clone/index pipeline
  • PR3 — GitHub webhooks (HMAC receiver + auto-register)
  • PR4 — call_edges + eval harness
  • PR5 — Louvain communities + workspace centroids
  • PR6 — two-stage workspace search endpoint
  • PR7 — workspace CLI + skill + dashboard search dialog
  • PR8 — workspace discovery (name-first CLI grammar + dashboard expansion)

Hybrid BM25 + dense search

FTS5 mirror of every indexed chunk (chunksfts). Workspace search blends BM25 keyword + dense vectors with a project gate; calibrated defaults; UI flags pre-FTS-mirror repos so the dashboard prompts a reindex.

Ownership + view-group access model

A formal authorization model: every project / workspace has an owner_user_id; admins manage view_groups and grant project_group_shares / workspace_group_shares. Server enforces via requireProjectAccess / requireWorkspaceVisible / mustBeAdmin. Full access matrix documented; gating tests cover every privileged endpoint. Owner reassignment (PUT /api/v1/projects/{hash}/owner) is admin-only.

Managed Tunnels (Cloudflare + ngrok)

Server-orchestrated tunnel for GitHub webhook ingress behind NAT. Provider-pluggable: Cloudflare and ngrok ship in v0.6.0. Persistent config in tunnel_config table; admin-only API surface; in-dashboard installer for the agent binary; round-trip test endpoint; restart endpoint with a 45s background-derived deadline so a client disconnect doesn't abort the spawn.

Git polling sync (webhook alternative)

Repos where the user is not an admin and cannot install a webhook can opt into polling instead — gated on webhook_mode = 'disabled'. Shared poll scheduler enqueues a clone_repo job whenever next_poll_at is due; cadence measured from the END of the last index run.

Claude Code plugin v0.2.0

Bundles cix + cix-workspace skills + cix-workspace-investigator sub-agent. Pin-installed auto-installer with three-state cix-status cache. Token-economy guidance + optional CIX_MAX_OUTPUT_LINES cap. Grep-nudge moved to PostToolUse.

Dashboard improvements

  • Live indexing progress for external projects
  • Confirm-before-full-reindex dialog
  • Reindex button + indicator on project page
  • Sync method switcher (webhook / polling / disabled) per project
  • Webhook actually configurable from project page (previously read-only)
  • Per-project Sync + Force Stop buttons for external projects
  • CIX_PUBLIC_URL treated as a valid webhook origin (tunnel optional)

🔐 Security hardening

  • mustBeAdmin added to ListTunnels, GetTunnelStatus, GetWebhookOrigin — tunnel state is operator infra, not user-visible data.
  • ListProjectWorkspaces now gates on requireProjectAccess (was: 200 with full list to anyone, leaking workspace IDs/names). Non-admins see only workspaces they themselves can see.
  • /index/status now gates on requireProjectAccess (was: requireProjectOwnership, locking group-shared read-only users out of the "indexing…" badge).
  • Dashboard hides GitHub Integration page + Add-repo button for non-admins.
  • Webhook secrets are zeroed after registration; webhook signature validation tightened.
  • GitHub-token scopes are derived from GitHub, not user-claimed.
  • Tunnel binary management gated behind CIX_TUNNEL_BIN_MANAGED=true (default off).

🐛 Notable fixes

  • Crash-safe split migration (CUDA support #5) with FS-rename-before-SQL ordering and idempotent retry.
  • Compensating delete + atomic Link for gitrepos/workspaceprojects (no orphan rows on partial failure).
  • dashboard/dist/.gitkeep restored (CI go vet relies on the directory being non-empty for //go:embed).
  • tsconfig.tsbuildinfo untracked.
  • Synchronous status flip in ReindexProject (was: race with the watcher).
  • Workspace search snippet content path fixed (matches[].content, not top-level).

📦 Versions

Component Old New
Server server/v0.5.1 server/v0.6.0
CLI cli/v0.5.0 cli/v0.6.0

Plugin (Claude Code) ships at v0.2.0 inside the plugins/cix/ tree.

🔄 Upgrade procedure

  1. Server first. Pull dvcdsys/code-index:0.6.0 (CPU) or :0.6.0-cu128 (CUDA) and redeploy. Migrations 5–11 run on first boot; expected wall time on the prod DB is a few seconds (size-dependent, dominated by the workspace_repos rewrite and the on-disk rename of clone dirs).
  2. Demote non-admins. Migration Bump actions/setup-go from 5 to 6 #10 sets every existing user to admin. Open the dashboard's Users page (or PATCH /api/v1/admin/users/{id} {"role":"user"}) and demote everyone who should not retain admin.
  3. Upgrade CLI on all dev machines: curl -sSf https://… | sh (per doc/UPDATES.md). Older CLIs talking to a v0.6.0 server will fail to resolve previously-indexed local projects until they're re-init'd.
  4. (Optional) Run a reindex on any project the dashboard flags as pre-FTS-mirror — hybrid BM25 needs the new chunk mirror.

CI artefacts

  • Pre-tag scan: make scout-cuda (HIGH/CRITICAL only) — must be 0 before tagging.
  • On server/v0.6.0 push, release-server.yml builds CPU multi-arch + CUDA amd64 with provenance + SBOM attestations and pushes :0.6.0, :0.6.0-cu128, :latest, :cu128 to Docker Hub.
  • On cli/v0.6.0 push, release-cli.yml builds {darwin,linux}-{amd64,arm64} archives and creates the GitHub Release.

Test plan

  • cd server && go test ./... — green (httpapi suite 21.7s)
  • cd cli && go test ./... — green
  • cd server && make scout-cuda — 0 HIGH/CRITICAL (running)
  • Smoke: fresh DB bootstrap + first-boot migration chain runs idempotently
  • Smoke: upgrade-in-place from a v0.5.1 DB — verify all 6 numbered migrations (5–11) apply, users all promoted to admin, local-project clone dirs renamed on disk
  • CLI compat: older cli/v0.5.0 against server/v0.6.0 — confirms re-init prompt path

🤖 Generated with Claude Code

dvcdsys and others added 30 commits May 13, 2026 18:41
First slice of the workspaces feature branch. Gated by
CIX_WORKSPACES_ENABLED — every new endpoint returns 503 when off, so
existing deployments are unaffected.

New tables: workspaces, github_tokens. New packages: internal/secrets
(AES-256-GCM at rest, key from CIX_SECRET_KEY / CIX_SECRET_KEYFILE /
auto-generated 0600 keyfile), internal/workspaces, internal/githubtokens.
New endpoints: full CRUD over /api/v1/workspaces and /api/v1/github-tokens
with the canonical {"detail": "..."} error envelope. Plaintext PATs are
never echoed — POST returns metadata only.

Dashboard gets two placeholder modules (Workspaces, GitHub Tokens) that
render the full CRUD flow against the new endpoints and self-hide behind
a "feature off" alert when the flag is false.

Subsequent PRs of feature/workspaces add workspace_repos, jobs+workers,
webhook receiver, call-graph extraction, Louvain communities, two-stage
search, and the cix:workspace skill.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the bridge from a GitHub URL to an indexed cix project. Operator
attaches a repo to a workspace via POST /workspaces/{id}/repos; the
server enqueues a clone_repo job (worker clones via go-git), then
chains an index_repo job that drives the existing 3-phase indexer
in-process against the on-disk clone.

New packages:
- internal/jobs       persistent SQLite-backed worker pool with
                      partial-unique dedupe (50 webhook bursts collapse
                      to 1 pending row), per-attempt linear backoff,
                      panic-safe handler invocation
- internal/repocloner go-git wrapper — shallow clone with PAT auth via
                      x-access-token, in-process so distroless images
                      don't need a git binary; fetch+reset on reuse
- internal/repoindexer walks the clone, batches FilePayloads, calls
                      indexer.BeginIndexing/ProcessFiles/Finish.
                      Filter prunes node_modules/.git/etc., skips
                      binaries (NUL probe) and oversized files.
- internal/workspacerepos service layer for workspace_repos rows
- internal/workspacejobs handler registration that wires the above
                      packages into the jobs queue

New endpoints (gated by CIX_WORKSPACES_ENABLED):
- GET    /workspaces/{id}/repos
- POST   /workspaces/{id}/repos      (returns one-shot webhook secret)
- DELETE /workspaces/{id}/repos/{repo_id}
- POST   /workspaces/{id}/repos/{repo_id}/reindex
- GET    /jobs

New env vars: CIX_WORKER_CONCURRENCY (default 2),
CIX_WORKSPACES_DATA_DIR (default <sqlite-parent>/repos), CIX_PUBLIC_URL
(used to build webhook URLs surfaced to operators).

Webhook receiver / HMAC validation lands in PR3; call graph + Louvain
communities + two-stage search in PR4–PR6.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…gister)

Closes the loop from a push on GitHub to an updated cix index. A new
public endpoint accepts deliveries, validates HMAC-SHA256 against the
per-row webhook_secret, and enqueues the same clone_repo job PR2
introduced — go-git's CloneOrFetch already handles the incremental
fetch+reset path, so no new job type is needed.

The dashboard's add-repo flow now exposes an `auto_webhook` toggle.
When true, the server uses the supplied PAT to POST /repos/.../hooks
on the operator's behalf and persists the resulting hook id. Failure
is non-fatal — the response carries `auto_registered: false` plus an
operator-facing note (e.g. "missing admin:repo_hook scope"). Manual
setup is the default and works without any extra GitHub scopes.

New package internal/githubapi: a tiny raw-HTTP client for two GitHub
endpoints (create webhook, delete webhook). Pulling go-github for just
these two calls would have added ~10MB of generated code.

New endpoints:
- POST /api/v1/webhooks/github/{repo_id}              (public; HMAC-auth)
- GET  /api/v1/workspaces/{id}/repos/{repo_id}/webhook-info

Tests cover: HMAC happy path, mismatched/missing signatures (401),
ping deliveries (200), wrong-branch pushes (ignored), burst-dedupe on
multiple deliveries collapsing to one job, public-path bypass of the
auth middleware, and the auto-register-fails-cleanly-without-public-URL
branch.

doc/WORKSPACES.md is a new operator guide — feature flags, encryption
key resolution, Cloudflare tunnel quick-start, manual + auto webhook
flows, troubleshooting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Approximate caller→callee graph extracted from the existing symbols +
refs tables. The result feeds Louvain community detection in PR5; the
eval harness gates that downstream work behind a precision-floor check.

Approach (refs heuristic):
- caller resolved as the narrowest function/method whose [line, end_line]
  span contains the ref's line
- callee candidates resolved by name lookup on symbols (kind ∈ function,
  method) constrained to the same project
- weight = 1 / popcount(callee_name) — so common names like init/run/handle
  contribute proportionally less to the structural signal
- popcount > 20 → name dropped (treated as noise)
- same-file bonus ×2.0, same-parent_name bonus ×1.5
- self-edges (recursion) dropped — they don't help community separation
- duplicate (caller, callee) pairs accumulate weight via map then bulk
  INSERT inside a single transaction

Integration: workspacejobs.handleIndex calls callgraph.Build after a
successful FinishIndexing — non-fatal (failure logs but doesn't flip
the repo status to failed; semantic search continues to work without
the graph).

Eval harness — internal/callgraph/eval/ — runs three fixtures
(Go/Python/TypeScript) through the full chunker → persist → build path
and asserts the labeled (caller, callee) pairs all show up in
call_edges. Current results:

  go-handlers     4/4  precision 1.00
  python-pipeline 6/6  precision 1.00
  typescript-store 5/5  precision 1.00

All three comfortably above the 0.60 floor — no need to fall back to
the symbol co-occurrence graph (callgraph.SourceCoOccurrence is in the
table for future swapping). Greenlights PR5 (Louvain communities).

9 unit tests covering: single-edge happy path, popcount drop,
module-scope refs skipped, self-edges dropped, cross-file weight,
same-parent bonus, weight accumulation, idempotency, edge counting.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The structural layer that powers PR6's two-stage workspace search.
Every workspace's combined call_edges graph is partitioned into Louvain
communities; each community gets a mean-pooled, L2-normalised embedding
stored in a dedicated chromem collection (ws_{md5}_centroids).

New package internal/communities — gonum/graph/community Louvain with
deterministic seed (Resolution=1.0, seed=42). Empty workspaces and
empty graphs are handled cleanly; output is wholesale-replaced on each
rebuild so partial failures can't leave stale state.

New tables: communities (id, workspace_id, label, size, parent_id),
community_members (community_id, project_path, symbol_id). Wholesale
delete + reinsert per rebuild inside a single transaction.

New vectorstore methods:
- CentroidCollectionName(workspaceID)
- ReplaceCentroids — drops + recreates the workspace's chromem
  collection in lock-step with the SQL rebuild
- SearchCentroids — top-K nearest-neighbor against the centroid
  collection (the stage-1 query for PR6)
- FetchProjectChunkEmbeddings — by-symbol-name lookup used during
  mean-pooling. chromem's where filter is single-equality so we make
  one query per name (bounded by community member count, typically <200).

Job pipeline:
- New type "compute_workspace_communities" with debounce key
  "communities:{workspace_id}" — burst-safe via the existing
  partial-unique index on jobs.dedupe_key.
- index_repo handler chains EnqueueComputeCommunities(workspace_id)
  with a 30s scheduled_at delay, so a wave of repos finishing
  indexing during catch-up collapses into one Louvain rebuild.

Tests: 6 unit tests covering Build (two-cluster split, empty workspace,
idempotency, cross-project tracking) + meanPool/l2Normalise helpers.
Eval gate from PR4 already cleared at 100% precision — Louvain runs
against a high-quality graph by construction.

Deferred to a future iteration (cheap to revisit):
- Recursive split for communities >50 chunks
- Small-community merging
- Overlapping community detection (BigCLAM, etc.)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GET /api/v1/workspaces/{id}/search?q=... is the user-facing payoff of
the workspaces feature. Two-stage retrieval:

  Stage 1: embed query → SearchCentroids → top N communities (default 5)
  Stage 2: for each (community, project_path), one chromem query against
           the per-project chunk collection with the user's embedding;
           filter results in-memory to members of the community by
           symbol_name; merge globally, dedupe by (project, file,
           startLine, endLine), return top K (default 20).

Why filter in-memory instead of pushing where: chromem's where clause
is single-equality only — pushing per-symbol-name filters would mean N
queries per (community, project). Stage-2 fan-out is bounded by
top_communities × #project_paths_per_community ≈ 5 × 3 = 15 queries
per workspace search, comfortably under 500ms p50.

Response shape (WorkspaceSearchResponse):
- status: "ok" | "communities_not_built" | "empty"
- communities: top-N centroids with score, label, project_paths
- chunks: merged ranking with project_path, file, lines, score,
  community attribution

When the workspace has no centroid index yet (e.g. just-created
workspace, debounced compute_workspace_communities hasn't fired),
the endpoint returns `status: "communities_not_built"` with empty
arrays — dashboard UI can render a hint instead of an error.

Tests: 4 HTTP integration tests covering the empty-centroid branch,
missing query parameter, unknown workspace id, and disabled feature
flag. A stub embedder lets us reach stage 1 without standing up the
llama-server sidecar in CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Final slice of the workspaces feature branch.

CLI (cli/):
- New parent command `cix workspace` (alias `cix ws`)
- `cix workspace list` — lists every workspace on the cix-server
- `cix workspace search <ws> <query>` — runs the two-stage search
  - --top-communities N (default 5)
  - --top-chunks K (default 20)
  - --json — raw response for piping
- Workspace identifier accepts either the opaque id or the name
  (case-insensitive); resolution is one `cix workspace list` round
  trip cached per-process.

Skill (skills/cix-workspace/SKILL.md):
- Markdown frontmatter user-invocable skill, mirroring the `cix`
  skill's style guide.
- Trigger phrasing tuned to the use case: cross-repo questions,
  microservice flows, frontend+backend pairs.
- Explains the two-stage mental model + when to fall back to plain
  `cix search` inside a single repo.
- Troubleshooting for `communities_not_built`, empty results, 503.

Dashboard (server/dashboard/src/modules/workspaces/):
- Search icon button on every workspace row opens a dialog hosting
  the full two-stage search UI: query input → top communities list
  (label, score, member count, project_paths) → top chunks (file,
  lines, project, symbol, score, content snippet).
- Status-aware empty states: explicit message when the centroid
  index hasn't built yet ("wait ~30s after the last index_repo").

Tests pass on both server and CLI. The feature branch is now ready
to merge to main as one large PR per the user's PR strategy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…xpansion)

User-facing refactor of the workspace surface so operators (and the
agent skill) can explore before searching.

CLI grammar — name-first, manual dispatch under one `cix ws` parent:

  cix ws                          → list workspaces
  cix ws list [--verbose|--json]  → list workspaces (alternate)
  cix ws <name>                   → describe — repos + status + indexed count
  cix ws <name> list              → list repos in workspace
  cix ws <name> repos             → alias for `<name> list`
  cix ws <name> describe          → same as bare `<name>`
  cix ws <name> search <query>    → two-stage workspace search

Why manual dispatch rather than cobra subcommands: the workspace NAME
needs to sit in the first positional slot. Cobra can't recognise a
dynamic value as a command, so we use cobra.ArbitraryArgs + a small
switch inside RunE. Trade-off: no auto-completion on the name. In
exchange, the surface reads the way operators think.

Status badges in `describe` / verbose `list`:
  ✓ indexed   ✗ failed   … pending/cloning/indexing

Client: adds Client.ListWorkspaceRepos for the new verbs to consume.
The /workspaces/{id}/repos endpoint is already there (PR2) — this
just exposes it.

Dashboard: each workspace row is now expandable. Click the chevron
→ lazy-loads attached repos, each shown with status colour, branch,
project_path, last_indexed_at, and last_error. The Search button
on the row still opens the existing two-stage search dialog.

SKILL.md: documents the new grammar + adds a "Discovery-first
workflow" pattern at the top of Patterns. The point of the new
verbs from an agent's perspective is to know whether a workspace
is searchable before paying the search round-trip — `cix ws <name>`
tells you indexed-count and lists repos in one call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The dashboard form was asking users to type the token's scopes by hand,
but scopes are an attribute of the PAT set on github.com — typed input
is just unverified text that can drift from what GitHub will actually
enforce. The codepath was a leftover from a deferred validation step.

Now the server validates every newly submitted PAT with GET /user and
reads the real scopes from the X-OAuth-Scopes response header. A 401
from GitHub turns into a 422 with the surfaced message, anything else
into a 502, so an invalid or unreachable token is rejected at the door
rather than persisted and discovered later. Fine-grained PATs
(github_pat_*) don't expose scopes via this header — for them Scopes
stays empty and the dashboard displays "(fine-grained or none)".

The Scopes field on CreateGithubTokenRequest is marked deprecated and
ignored on the server; the dashboard's Scopes input is removed.
Existing tests are updated and a TestGithubTokens_RejectInvalidToken
case asserts the 401-path rejection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The dashboard's workspace view was a stub that listed repos read-only;
attaching a new repo only worked via curl. This wires up the actual UX:
a card grid that mirrors the projects page on the list, a per-workspace
detail page, and a staged add-repo dialog that walks the operator
through token → repo → branch → webhook policy.

Backend changes:
  - GET /api/v1/github-tokens/{id}/repos — reveals the PAT server-side,
    fetches the repos visible to it via /user/repos with Link-header
    pagination (up to 5 pages = 500 repos), optionally filtered by ?q=.
    The plaintext never touches the wire.
  - POST /api/v1/workspaces/{id}/repos now accepts webhook_mode of
    {manual, auto, disabled}. A new workspace_repos.webhook_mode column
    records the operator's intent; the legacy auto_webhook bool remains
    derived (true iff mode = "auto") so old clients keep working.
    Existing rows are backfilled to "auto" when auto_webhook=1.

Frontend changes:
  - WorkspacesPage is a Routes shell now; list + detail are separate.
  - WorkspacesListPage renders Workspaces as cards (counts at-a-glance,
    in-progress / failed badges) — same visual language as projects.
  - WorkspaceDetailPage drives the per-workspace UX: an Add repo dialog
    with a staged form (each step unlocks the next), Reindex / Delete
    actions on each RepoCard, and background polling (3s) while any
    repo is in pending / cloning / indexing so the operator can watch
    the progress without F5. Each in-flight badge ticks an elapsed
    counter so it's visible that the job isn't silently stalled.
  - AddRepoDialog picks tokens, lists their visible repos with a
    client-side text filter, auto-fills branch from default_branch,
    and surfaces the webhook URL+secret once for manual mode.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The repo picker only surfaced what `/user/repos` returned. That endpoint
is the affiliations-aggregated view and routinely misses org repos —
SAML-protected orgs in particular only appear under `/orgs/{login}/repos`.
So a user with access to an org would see their personal account but
not the org's repos, which is exactly what was hit in testing.

Add a second selector between Token and Repository:

  - New `GET /api/v1/github-tokens/{id}/accounts` lists the PAT owner
    plus every org from `/user/orgs`. SAML-gated 403 on /user/orgs is
    swallowed so the personal account still comes through.
  - `GET /api/v1/github-tokens/{id}/repos` now accepts `?account=login`
    + `?account_type=user|org`. When set, the server hits
    `/users/{login}/repos` or `/orgs/{login}/repos` directly. When not
    set, it falls back to the original `/user/repos` aggregated view
    so existing callers keep working.

Dashboard:

  - `AddRepoDialog` loads accounts as soon as a token is picked and
    renders a Select with "(all accessible)" plus each user/org. The
    repo list refetches whenever the account changes — typing through
    the picker now shows the org's repos directly.

Tests:

  - Unit: ListAccounts (user + orgs), SAML-403 swallow, account-scoped
    repo endpoint dispatch (`/users/X` vs `/orgs/X`).
  - Integration: round-trips through the HTTP layer including the
    "no account_type with account" 422 rejection.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Long full_names like atrybulkevychglobalgames/grpc-go-kubernetes-load-
balancing-example were pushing the row past the dialog's max-width
because Tailwind's truncate only works on a flex child that also has
min-w-0. The name span had truncate but no shrink boundary, so it kept
its intrinsic width and the branch span on ml-auto ended up off-screen.

Wrap the name in min-w-0 flex-1 truncate, pin the icon and branch to
shrink-0 so the row stays inside the dialog. Added a title= attribute
on the button so hovering still surfaces the full path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
A trigram-tokenized FTS5 virtual table lives alongside chromem-go so
workspace search can pair dense vector retrieval with sparse keyword
retrieval. The sparse signal recovers two things pure-dense fan-out
loses: short-token precision (acronyms like "XYZ" get diffuse cosine
scores) and project-relevance gating (chromem returns the N nearest
vectors regardless of semantic distance, leaving projects that share
zero vocabulary with the query at chunk_score ~0.25 false-positive).

chunks_fts can only filter by rowid; chunks_meta is the indexed shadow
that lets us delete by (project_path, file_path) and project_path
without a full FTS5 scan. The two stay consistent inside the indexer's
per-file SQL transaction, and they cascade away on project deletion
and on full-reindex wipe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Each project now runs dense (chromem cosine) and sparse (FTS5 BM25
over the chunks_fts mirror added in the previous commit) in parallel.
Per project the two ranked lists are fused via Reciprocal Rank Fusion
(k=60). Across projects an α-blended candidacy score (with per-query
min-max normalization on both signals) plus a relative threshold
(`candidacy >= best * 0.4`) gates the result set so projects that
share no semantic and no lexical overlap with the query drop out
entirely — pure-dense fan-out leaked every workspace repo at
noise-level cosine similarity because chromem returns the N nearest
vectors regardless of how far away "nearest" actually is.

Live XYZ probe over 8 ACME repos: three repos with literally zero
"XYZ" mentions previously surfaced 50 chunks each at dense scores
0.17-0.27. With the gate they drop out; the chunks list is then
built by round-robin interleaving across surviving projects so each
relevant repo gets its top hit before the dominant repo's tail
entries appear.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…a reindex

Repos indexed before the chunks_fts mirror landed have file_hashes
rows (chromem populated, project marked indexed) but an empty
chunks_meta — the BM25 side of hybrid search returns nothing for
them and the algorithm degrades to pure dense for those entries.
Observable failure mode: live workspace shows the new bm25_score
field at 0.000 for every project and the result set looks
identical to the old pure-dense fan-out.

WorkspaceSearch now probes chunks_meta vs file_hashes per project
and bubbles stale repos up via a new stale_fts_repos field on the
response. The dashboard renders a banner naming the affected repos
with a reindex hint.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…workflow

Replaces the old centroid-routing playbook (which described tools we
ripped out in the workspaces refactor) with a workflow that matches
the hybrid BM25+dense algorithm: how to phrase queries so the BM25
gate fires, how to read project_score / bm25_score / dense_score,
when to spawn parallel Explore sub-agents over surviving projects,
and how to synthesize the per-repo change plan.

The skill is goal-driven: every workspace-search interaction has to
answer (1) which repos are in scope, (2) which code in those repos
is relevant, (3) what changes need to land and in what order. It
also names the "primary project" pattern — the agent is usually
cd'd into one specific repo and the user's task is rooted there;
workspace search defines the surrounding context.

Includes a worked retro on the "Add sell flow to XYZ" failure that
motivated the hybrid algorithm — pure-dense fan-out routed three
zero-mention repos as relevant on noise-level cosine similarity.

Aligns the CLI (`cix ws … search`) with the new server API: drops
the `--top-communities` flag in favour of `--top-projects`, switches
the response renderer to projects + bm25/dense breakdown, surfaces
stale_fts_repos as an inline warning.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replaces internal product/repo names that leaked from a real production
debugging session into test fixtures, code comments, and the
cix-workspace skill doc:

- XYZ / XYZOrder / processXYZOrderEvent → XYZ / XYZOrder / processXYZOrderEvent
- acme-backend / acme-shared / acme-models / acme-worker /
  acme-notifier / acme-directory / acme-inventory / acme-platform
  → acme-backend / acme-shared / acme-models / acme-worker /
    acme-notifier / acme-directory / acme-inventory / acme-platform
- "internal product code" → "internal product code"
- "shared-models migration", "shared data models" → generic
  shared-models / data-model phrasing
- README .cixignore example switched from api/generated/ to
  api/generated/

Working-tree-only sanitization; a follow-up history rewrite will scrub
the same strings from older commits. Tests green (chunksfts, db,
httpapi, projectconfig).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ency

Three changes to workspace and per-project search, plus targeted
anonymization of eval-derived references in adjacent comments and test
fixtures.

Server behaviour:
- Workspace search default `min_score` raised from 0 to 0.4, matching
  the per-project SemanticSearch default. Cross-project sweeps that
  need long-tail recall must now pass `min_score=0` explicitly.
- Per-project SemanticSearch default `min_score` lowered from 0.4 to
  0.2 — abstract NL queries (e.g. "end-to-end workflow lifecycle")
  used to silently return empty even when relevant chunks scored in
  [0.25, 0.35]. 0.2 keeps a light noise floor.
- Fix: workspace `chunks[]` round-robin now uses only the projects
  that survived the `top_projects` truncation. Previously a 12-project
  workspace at default `top_projects=10` could surface chunks from
  the 11th/12th project that weren't in the `projects[]` panel —
  clients had no way to look up the chunk's bm25/dense scores.

Tests added:
- TestWorkspaceSearch_ChunksOnlyFromPanelProjects — 12 surviving repos
  + top_projects=10; every chunk's project must appear in the panel.
- TestWorkspaceSearch_DefaultMinScoreIs04 — geometry calibrated so
  chunks at cos=0.3 are filtered by default and admitted at min_score=0.
- TestSemanticSearch_DefaultMinScoreIs02 — fakeEmbedder geometry
  producing a cos≈0.25 chunk that the old default would have rejected.

OpenAPI spec descriptions updated for both `min_score` defaults.

Anonymization (carried over from previous workspace-eval analysis):
adjacent comments and test fixtures that named specific repos / product
acronyms / sell-flow scenarios are replaced with neutral placeholders
(WIDGET / ping / generic repo descriptions).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…subagent

Two additions to the cix-workspace skill:

1. Ten "trust rules" for interpreting workspace_search responses,
   derived from internal calibration testing:
   - chunk.score>=0.4 trust threshold (rule 1)
   - chunk.score==0 = BM25-only literal match, not low confidence (rule 2)
   - top-1 of projects[] is correct ~70% of the time in real tasks (rule 3)
   - drop down to per-project search for depth (rule 4)
   - min_score=0 explicitly for cross-project sweeps (rule 5)
   - careful disambiguator selection — prefer meta-tokens over tech
     guesses (rule 6)
   - "change X in production" → manifests/config repo, not code repo
     (rule 7)
   - scan ranks 2-5 before reformulating (rule 8)
   - explicit min_score=0 for per-project NL drill-down (rule 9)
   - words live ≠ change location (rule 10)

2. Dedicated `cix-workspace-investigator` sub-agent at
   `skills/cix-workspace/agents/cix-workspace-investigator.md`:
   - Thin read-only shell around cix search/def/refs + Read + Grep
   - Scope-isolated: one repo per spawn, no edits, no recursion
   - Methodology + output format are the main agent's call per spawn,
     not baked into the sub-agent's system prompt
   - System prompt is ~60 lines; main agent's per-spawn prompt
     handles the actual task

SKILL.md's "Sub-agent fan-out pattern" section rewritten around the
new sub-agent with a four-part prompt template (task verbatim,
project_path, seed chunks WITH the main agent's commentary, explicit
deliverable) and an anti-patterns list. The existing worked example is
preserved but rewritten without specific repo composition.

skills/README.md updated with the bundled-subagent description and
install command (additional copy into ~/.claude/agents/).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New workspaces.md at the repo root — sub-document linked from
README.md. Covers everything an operator or agent needs to know about
the workspaces feature:
- What a workspace is, what's experimental about it today
- Enabling via CIX_WORKSPACES_ENABLED + supplementary env vars
- Concepts: owned vs linked repos, GitHub tokens, project path format
- Quick start: end-to-end walkthrough with curl examples
- Adding repositories (Dashboard staged dialog + REST + status
  transitions)
- GitHub tokens lifecycle, AES-256-GCM at-rest encryption, scopes
- Searching: Dashboard / `cix ws` CLI / REST endpoint with response
  shape
- Search algorithm — pipeline diagram, tunable parameters table,
  min_score semantics, hybrid BM25+dense rationale, stale-FTS handling
- Webhooks: disabled/manual/auto modes, HMAC signature, delivery
  endpoint
- Strengths and weaknesses (honest assessment)
- Configuration reference, REST API reference, troubleshooting
- Agent integration pointer to cix-workspace skill

README.md updated:
- "What you get" bullet for Workspaces (experimental) with link to
  workspaces.md
- Dashboard table gains two new rows: Workspaces and GitHub Tokens
  (both flagged experimental)
- New "Workspaces and external repositories" subsection after the
  Disabled-embeddings mode subsection, summarising the feature and
  linking to workspaces.md
- Agent Integration section adds the cix-workspace skill + bundled
  investigator subagent install snippet

Feature is marked experimental in every public-facing reference.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- New workflow `.github/workflows/prerelease-server.yml`: on push to
  `develop`, builds `server/Dockerfile.cuda` (amd64) and pushes the
  floating `dvcdsys/code-index:develop-cu128` tag. CPU image is
  intentionally skipped — pre-release stages on RTX 3090 only.
- Extend `ci-server.yml` / `ci-cli.yml` to also run on push and PR
  against `develop`, so vet/test/build gates pre-release merges the
  same way they gate main.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Commit 33da39b accidentally removed the placeholder that makes the
`//go:embed all:dist` directive in dashboard/embed.go resolve on a
fresh clone (no `make dashboard-build`). `go vet ./...` then fails
with `pattern all:dist: no matching files found`, breaking the CI
gate on every PR.

The root `.gitignore` already has a negation rule for this exact
path; restoring the file is enough.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…apshot

Restores the plugin tree from the (since-deleted) feat/claude-code-plugin
branch onto develop, so the plugin lives alongside the rest of the cix
codebase and can be developed via normal PRs to develop.

Contents:
- plugins/cix/ — full plugin tree (hooks, scripts, commands, skills, tests)
- .claude-plugin/marketplace.json — root marketplace manifest used by
  `/plugin marketplace add github:dvcdsys/code-index`
- .github/workflows/ci-plugin.yml — bats + shellcheck on push/PR

Snapshot SHA in the local marketplace cache: 74816e7.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The PreToolUse matcher was "Grep|Glob", which only fires for the
built-in Grep/Glob tools — `grep`/`rg` invoked through Bash (the common
real-session path: pipelines, `| head`, `cd && grep …`) silently never
triggered the nudge.

Fix:
- hooks.json gains a second PreToolUse entry with matcher "Bash" that
  points at the same script (split into two entries instead of
  "Grep|Glob|Bash" so each path's intent reads clearly in review).
- grep-nudge.sh inspects tool_name on stdin; for Bash it also parses
  tool_input.command and only proceeds when the command contains a
  standalone grep-family token (grep/egrep/fgrep/rg/ripgrep). Non-grep
  Bash (ls, git status, make, go test) exits silently BEFORE the
  backoff counter increments, so the existing 1/2/4/8/16 cadence is
  preserved.
- tests/manifest.bats pins the matcher list: dropping "Bash" or "Grep"
  from hooks.json fails CI immediately — the exact regression that
  produced this fix.
- tests/grep-nudge.bats grows 14 cases covering grep/rg/ripgrep,
  pipelines, chained commands, substring rejection (grepl), and the
  full no-fire set (ls/git status/make/go test/Read tool).
- README troubleshooting bullet documents the failure mode and the
  manifest test that guards against recurrence.

Without jq the Bash branch is silent rather than parsing shell commands
with sed — false-positive risk too high. jq is already in the CI image
and listed as a dev dependency.

Verification:
- bats plugins/cix/tests/*.bats → 60/60 green (18 pre-existing
  grep-nudge + 14 new Bash cases + 5 new manifest cases + the rest of
  the suite unchanged).
- shellcheck --severity=warning plugins/cix/scripts/*.sh → clean.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…t (v0.2.0)

When users install the cix plugin via the Claude Code marketplace they
should get the cross-project research workflow + the read-only
investigator sub-agent automatically — without separate `cp -r` of
top-level `skills/` and `~/.claude/agents/` files. This commit moves
both artifacts into the plugin tree so marketplace install picks them
up in one step.

Plugin additions:
- `plugins/cix/skills/cix-workspace/SKILL.md` — workflow skill (10
  trust rules, four-part fan-out prompt template, anti-patterns,
  worked example).
- `plugins/cix/agents/cix-workspace-investigator.md` — read-only
  sub-agent. Tools: Bash + Read + Grep. Hard rules baked in
  (one repo per spawn, no edits, no recursion).
- `plugins/cix/tests/workspace-bundle.bats` — 10 regression tests:
  files present, frontmatter parses, manual-only enforced (no
  `when_to_use:` block), sub-agent tools list is read-only,
  manifest version >= 0.2.0.

Manual-only by design:
- The cix-workspace skill does NOT auto-trigger. Frontmatter has no
  `when_to_use:` heuristic block; `description:` explicitly says
  "Do not auto-trigger". Invocation is exclusively via
  `/cix-workspace <task>`.
- Rationale: the workspace flow is heavier than single-repo
  `cix search` (multi-repo fan-out, sub-agent spawns). Auto-trigger
  on every prompt that vaguely mentions "services" would burn
  context for no reason. Load it deliberately. Policy may relax
  once "is-this-really-cross-project?" heuristics are more reliable.
- A bats test (`workspace-bundle.bats`) pins this — re-adding
  `when_to_use:` fails CI.

Plugin manifest bumped 0.1.0 → 0.2.0:
- Description updated to mention the workspace skill + sub-agent.
- Keywords expanded with workspace / cross-project / monorepo /
  sub-agent.
- marketplace.json description mirrors plugin.json.

Top-level docs aligned:
- `workspaces.md`, `README.md`, `skills/README.md`: install
  instructions now lead with the marketplace plugin install; manual
  `cp -r` kept as the "legacy" path. All three call out that the
  skill is manual-only.
- `skills/cix-workspace/SKILL.md` (top-level copy) mirrors the
  plugin's frontmatter for consistency.

Tests: full 70/70 bats suite passes locally
(60 existing + 10 new workspace-bundle assertions).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Parallels the existing prerelease-server workflow (CUDA-only
:develop-cu128 image) on the CLI side so CLI changes on `develop` can be
staged against the prerelease server without cutting a real `cli/v*` tag.

- New workflow `.github/workflows/prerelease-cli.yml` triggers on push
  to `develop` with `cli/**` paths. Builds darwin/linux × amd64/arm64,
  stamps `develop-<short-sha>` into `cmd.Version` for traceability,
  then `gh release delete --cleanup-tag` the previous `cli/develop` and
  publishes a fresh prerelease at the same floating tag with all four
  tarballs + checksums.
- New `install-develop.sh` at repo root hardcodes the `cli/develop` tag,
  always overwrites (no skip-if-installed — the tag moves on every
  merge), and prints a dev-channel banner so users don't confuse it
  with stable.
- Stable `install.sh` is unchanged: its `grep '^cli/v'` filter
  naturally excludes `cli/develop` (no leading `v`), so the stable
  channel can't accidentally pick up a develop build.
- README gains a "Develop channel (bleeding edge)" subsection under
  the CLI install options.
- doc/DOCKER_TAGS.md documents both `:develop-cu128` (server) and
  `cli/develop` (CLI) as a matched pair of pre-release artifacts.

Verification: end-to-end testable only after this merges to `develop` —
the workflow then creates the `cli/develop` release, after which
`bash install-develop.sh` should pull a `cix` reporting `develop-<sha>`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…e_projects

The previous schema conflated three concerns inside `workspace_repos`:
clone+webhook metadata, workspace membership, and the
"owned vs linked" distinction. This made it impossible to talk about a
project as a first-class entity — every operation was workspace-coupled,
which forced workarounds like the singleton "Personal" workspace for
standalone repos and the synthetic `is_local` flag for CLI-indexed
projects.

The new model matches how operators actually think about the system:

- **projects** is the canonical entity (unchanged shape). Local and
  external projects live here side by side, identified by host_path.
- **git_repos** carries clone + webhook metadata (1:1 with projects for
  external projects only — local projects have no git_repos row).
- **workspace_projects** is the many-to-many junction. Adding a project
  to a workspace = INSERT here; removing = DELETE. The project itself
  is untouched.

Deleting a project cascades to git_repos and workspace_projects via
FK ON DELETE CASCADE.

## API rewrite

- `POST /api/v1/git-repos` — create an external project (clone + index).
- `GET  /api/v1/projects/{hash}/git-repo` — read git_repos metadata.
- `GET  /api/v1/projects/{hash}/webhook-info` — webhook URL + secret.
- `POST /api/v1/projects/{hash}/reindex` — re-trigger clone+index.
- `GET  /api/v1/workspaces/{id}/projects` — list projects in workspace.
- `POST /api/v1/workspaces/{id}/projects` — link an indexed project.
- `DELETE /api/v1/workspaces/{id}/projects/{hash}` — unlink.
- `POST /api/v1/webhooks/github/{hash}` — webhook URL now uses
  projects.path_hash. Existing GitHub-side hooks need re-registering
  (clean break).

All previous `/workspaces/{id}/repos[/...]` endpoints are gone.

## Migration

`migrateSplitWorkspaceRepos` runs once at startup. For every
`workspace_repos` row it: (a) pre-seeds the matching projects row if
absent, (b) inserts a workspace_projects membership, (c) inserts a
git_repos row for owned external rows only, (d) renames the on-disk
clone dir from `{DataDir}/repos/{workspace_repos.id}` to
`{DataDir}/repos/{path_hash}` so existing clones survive. Finally it
DROPs the legacy table.

## Code reorg

- `internal/gitrepos/` — new service package (Create, GetByPath,
  GetByHash, SetClone, SetWebhookID, Delete).
- `internal/workspaceprojects/` — new service package (Link, Unlink,
  ListByWorkspace, ListByProject).
- `internal/workspacerepos/` — deleted.
- `internal/workspacejobs/` — payload identifier is now project_path;
  clone dir naming uses path_hash; status writes go to projects.status
  (single source of truth).
- `internal/httpapi/gitrepos.go` + `workspaceprojects.go` — new HTTP
  handlers replacing `workspacerepos.go`.
- Webhook lookup uses GitRepos.GetByHash (path_hash) instead of
  workspace_repos.id.

## Dashboard

- `WorkspaceDetailPage` reads projects via `/workspaces/{id}/projects`.
- New `WorkspaceProjectRow` replaces `RepoCard`. The row's only action
  is Unlink (reindex / webhook config / delete live on the project's
  own detail page).
- `AddRepoDialog.workspaceID` is optional. With it: POST `/git-repos`
  then `POST /workspaces/{id}/projects`. Without it: just create a
  standalone project (mounted on `/projects`).
- `AddExistingProjectDialog` no longer disables local projects —
  local linkages are a regular workspace_projects row.

## Tests

- New gitrepos package tests (Create + UNIQUE + GetByHash + cascade).
- New workspaceprojects package tests (Link + duplicate + non-indexed
  precondition + cascade).
- New HTTP tests: TestAddGitRepo_Succeeds, _Duplicate,
  TestReindexProject_RequiresGitRepo, TestDeleteProject_CascadesGitRepoAndMembership,
  TestLinkProjectToWorkspace_*, TestUnlinkProject.
- Migration test TestMigrate_SplitWorkspaceRepos seeds the legacy table
  (owned + linked + local rows) plus on-disk clone dirs, opens via
  OpenWith(DataDir), and asserts the table is dropped, git_repos +
  workspace_projects populated, and the clone dir renamed.
- Existing webhooks_test.go + workspacesearch_test.go ported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This file is the TypeScript incremental-build cache (`tsc -b`). It
mutates on every local build and was producing noise in git status +
diffs. Same treatment as the existing `dist/` and `src/api/generated.ts`
rules — build artefacts don't belong in source control.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
After the workspace_repos → git_repos + workspace_projects split
(763154a), the CLI client still targeted /api/v1/workspaces/{id}/repos
— deleted. Three commands (`cix ws list -v`, `cix ws <name> list`,
`cix ws <name>` describe) returned 404 or silently lost data.

- Replace WorkspaceRepo + ListWorkspaceRepos with WorkspaceProject +
  ListWorkspaceProjects against the new endpoint.
- Update three call sites in cli/cmd/workspace.go to use the new
  payload shape (project_path / status / path_hash instead of
  github_url / branch / id).
- New cli/cmd/workspace_test.go covers status badge formatting,
  empty-list rendering, and case-insensitive name resolution.

Resolves Fix #1 + #17 in docs/code-review-workspaces-link-local-projects.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ProjectDetailPage rendered "@undefined" and warned about duplicate
React keys because the TS type still declared repo_id, branch,
status, is_linked — fields the server stopped returning after the
workspace_repos split (project_workspaces.go now sends only
workspace_id, workspace_name, added_at).

- Trim ProjectWorkspaceEntry to the three real fields.
- Key on workspace_id; drop the "linked vs owned" UI (concept
  removed — every membership is just a link now).
- Tooltip + chip body show workspace name + added_at only.

Resolves Fix #2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
dvcdsys and others added 26 commits May 20, 2026 11:51
Two reliability fixes from the cix review plan.

5.1 — Pin the bootstrap installer (defect B3):
- Added CIX_PINNED_VERSION="cli/v0.5.0" in cix-wrapper.sh. Both the
  install.sh script ref and the installed binary --version are pinned to
  that tag, so a fresh bootstrap is reproducible instead of tracking
  whatever is on `main`. Bump the constant on CLI releases.
- Added CIX_NO_AUTOINSTALL opt-out for corp/air-gapped environments: when
  set, the wrapper refuses to fetch and prints the exact manual-install
  command instead of silently reaching the network.

5.2 — Fix the "silenced all session" race (R4):
- New scripts/lib-cix-probe.sh dedupes the cix-binary resolution and the
  2s status probe that was copy-pasted across session-start.sh and
  cwd-changed.sh. cix_probe_verdict prefers timeout(1)/gtimeout(1) and
  falls back to the bash poll loop on macOS without coreutils.
- Three-state cache (1 / 0 / unknown). A cix-status timeout at session
  start now writes "unknown" instead of "0". grep-nudge re-probes once on
  "unknown", persists the definitive verdict, and recovers — so a server
  that was down at startup but came up later still produces nudges instead
  of being silenced for the whole session. A definitive "0" is never
  re-probed.

Tests: +2 wrapper opt-out cases, +4 grep-nudge re-probe cases, and the two
existing timeout tests updated to expect "unknown". Full plugin bats suite
95/95 green; shellcheck clean. CIX_NO_AUTOINSTALL and the pinning behavior
documented in the plugin README.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Log level was hardcoded to info; set CIX_LOG_LEVEL=warn to silence
INFO noise (debug|info|warn|error, defaults to info).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…s behind NAT

Adds a built-in, dashboard-managed outbound tunnel so a NAT'd server gets a
public URL for GitHub webhook delivery — no inbound ports, no env feature flag.

Backend (internal/tunnels):
- Provider abstraction + Manager with runtime Apply (start/stop/reconfigure),
  mirroring the llama-server sidecar supervisor (exec, exit-watcher, bounded
  crash-restart, graceful SIGTERM->SIGKILL).
- Cloudflare provider (named/quick) and ngrok provider (reserved/ephemeral;
  authtoken always required; URL parsed from the agent JSON log).
- Webhook reconciler: re-points webhook_mode=auto repos at the live tunnel URL
  on boot and on URL change; new githubapi.UpdateWebhook (PATCH).
- buildWebhookURL prefers the live tunnel URL over CIX_PUBLIC_URL.

Config in DB, managed from the dashboard (no env flag):
- tunnel_config single-row table (migration #8); token encrypted via secrets.
- tunnelcfg service + GET/PUT /api/v1/tunnels/config (admin); provider-aware
  validation. Only deployment infra stays in env (binary paths, metrics addr).

Binary management:
- GET /api/v1/tunnels/binaries reports installed/path/version/managed.
- When missing locally, the dashboard shows manual install instructions
  (brew/linux) noting Docker bundles them automatically.
- CIX_TUNNEL_BIN_MANAGED (true in the Docker images) enables Install/Update:
  the server downloads agents (raw or .tgz, no shell) into a writable
  /data/tunnel-bin via POST /api/v1/tunnels/binaries/{provider}/{install,update}.

Dashboard:
- New admin-only "Managed Tunnels" section (provider select, mode, hostname,
  token, status, Test/Restart, binary install/update).
- "GitHub Tokens" -> "GitHub Integration" with Tokens + Webhook Integrations tabs.

Docker: both images bundle cloudflared + ngrok and enable managed updates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…, races

- High: require admin on TestTunnel/RestartTunnel/ReconcileWebhooks — these
  decrypt PATs, register GitHub webhooks, restart the subprocess, or probe
  outbound, and were only behind requireAuth (any authenticated user).
  Hide the dashboard "Re-register webhooks" button from non-admins.
- Medium: pass the Cloudflare/ngrok tokens via env (TUNNEL_TOKEN /
  NGROK_AUTHTOKEN) instead of argv so they don't show in ps/proc cmdline.
- Medium: installer no longer tracks "latest" — pin cloudflared version
  (synced with the Dockerfile arg); compute the download SHA-256, verify it
  against a pinned map when present and warn (with the sum) when not; cap the
  download/extraction size to guard against a decompression bomb.
- Low: serialize Reconcile (mutex) so the boot double-reconcile can't create
  duplicate GitHub hooks; serialize Manager.Apply so concurrent applies can't
  orphan a provider; RestartTunnel uses a background-derived ctx so a client
  disconnect doesn't abort the spawn (matching UpdateTunnelConfig); capture
  readySignal under the lock before close to avoid a restart-time race.
- Nits: drop stale CIX_TUNNEL_ENABLED wording from handler messages/comments.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…epos

Webhooks require admin:repo_hook to install, so repos the user can only
clone (not administer) could never auto-sync. Add a polling path that
periodically fetches and re-indexes those repos.

- A single shared scheduler (internal/pollscheduler) drives polling for
  all polling repos, enqueueing into the existing bounded jobs queue
  (CIX_WORKER_CONCURRENCY) — no new queue, no per-repo goroutines, so a
  fleet coming due at once can't stampede the indexer.
- Reuses the clone_repo -> index_repo pipeline verbatim: PAT-authenticated
  git fetch (keeps within rate limits, no REST calls) and incremental
  tree.Diff reindex. Full reindex only in the same edge cases as webhooks.
- Cadence is measured from the END of the last index run: completion
  handlers set next_poll_at = now + interval; the scheduler writes a
  provisional floor at enqueue for crash-safety.
- Webhook XOR polling: enabling polling requires webhook_mode='disabled'
  (422 otherwise). When auto webhook registration fails (non-admin), the
  server auto-falls back to disabled + polling.
- git_repos gains polling_enabled / poll_interval_seconds / next_poll_at
  (migration 8, idempotent); next_poll_at is exposed on the GitRepo API.
- New config: CIX_DEFAULT_POLL_INTERVAL (5m), CIX_MIN_POLL_INTERVAL (60s),
  CIX_POLL_SCHEDULER_TICK (30s).
- API: polling fields on AddGitRepoRequest + PATCH /projects/{hash}/git-repo.

Tests cover migration idempotency, gitrepos polling methods, the scheduler
tick, the reschedule helper, and the HTTP gating/fallback/PATCH paths.
Docs: new doc/POLLING.md, cross-linked from WEBHOOKS.md + CONFIG_REFERENCE.md.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- gofmt cmd/cix-server/main.go (pollscheduler/repojobs imports were out of
  alphabetical order) and the two new test files' struct alignment.
- Correct stale "migration 8" references to migration 9 in schema.go and
  db.go comments (8 = tunnel_config, 9 = git_repos_polling after the rebase).
- Rename TestOpenMigratesPreM8DB -> TestOpenMigratesPreM9DB to match.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…l optional)

The Webhook Integrations panel showed a "No live tunnel" warning whenever no
managed tunnel was running — even when CIX_PUBLIC_URL was set and the server
was already publicly reachable via infrastructure (reverse proxy, ingress,
static IP). A tunnel is optional in that case; webhooks deliver fine.

- New GET /api/v1/github/webhooks/origin returns the effective webhook
  delivery origin and its source (tunnel | public_url | none), mirroring the
  server's publicBaseURL() precedence.
- WebhooksTab now renders three states: active tunnel URL, infra-provided
  origin via CIX_PUBLIC_URL (neutral — tunnel optional), or — only when
  neither is configured — a "no public origin" warning.

Backend tests cover the public_url and none sources. Frontend uses the ad-hoc
api client (no generated-client dependency); src/api/generated.ts can be
regenerated via `npm run gen:api` when node is available (non-blocking).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds a "Sync settings" card to the project page that lets an operator
reconfigure how an external GitHub project is reindexed — webhook,
polling, or manual — without leaving the page.

Server:
- Generalize PATCH /api/v1/projects/{hash}/git-repo into a sync-method
  switcher (operationId updateProjectGitRepoSync). Request now takes
  { sync_method: webhook|polling|manual, poll_interval_seconds? } and
  returns { git_repo, note }.
  - webhook → webhook_mode=auto + auto-register the hook; on failure
    (no admin / no public URL) fall back to polling with a note.
  - polling → webhook_mode=disabled + polling on (+ interval).
  - manual → webhook_mode=disabled, polling off.
  - Switching away from webhook best-effort de-registers the hook
    (githubapi.DeleteWebhook) and clears webhook_id.
- gitrepos: SetSync (atomic webhook_mode + polling + interval, XOR
  enforced) and ClearWebhookID; remove the now-unused SetPolling.
- Webhook receiver now ignores deliveries when webhook_mode='disabled',
  so a lingering hook can't double-sync alongside the poll scheduler.

Dashboard:
- SyncSettingsCard (method radio + interval input, shows webhook_mode,
  next poll, last error). useProjectGitRepo + useUpdateProjectSync hooks.
  Read-only for non-admins.

Backend tests cover all three methods, the webhook→polling fallback,
invalid method (422), SetSync transitions, ClearWebhookID, and the
receiver ignoring disabled repos. Frontend not built locally (no node);
needs `npm run build` + visual check before merge.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The sync switcher's "Webhook" option only ever attempted auto-registration
and silently fell back to polling on failure, and the card never surfaced
the webhook URL/secret — so a user without an admin token (or public URL)
could not actually set up a webhook. It looked broken.

- Server: choosing webhook no longer falls back to polling. If auto-register
  can't install the hook (no admin token / no public URL), the repo is left
  in webhook_mode='manual' — a valid webhook the operator finishes by hand —
  with a note explaining how. (Add-repo's auto→polling fallback is unchanged.)
- Dashboard: when Webhook is selected, the card shows the payload URL (copy)
  and the HMAC secret behind a reveal toggle (admin-only), GitHub setup
  instructions, a hint when no public origin is configured, and whether the
  hook was auto-registered. New useProjectWebhookInfo hook.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Require admin on PATCH /projects/{hash}/git-repo: it decrypts the PAT and
  registers/deletes GitHub webhooks, matching ReconcileWebhooks' privilege.
  Closes the direct-API hole where a viewer could drive those operations
  (the dashboard already gated the card behind isAdmin).
- Re-saving "webhook" on a repo that's already a manually-configured hook
  (webhook_mode=manual, no stored hook id) preserves manual instead of
  flipping to auto and registering a SECOND hook beside the operator's.

Test covers the manual-preservation path (second webhook save keeps manual
with the "left as-is" note).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Two external-project-only dashboard actions (local projects unchanged):

- Sync: soft update — fetch latest commits + incremental index over the
  tree.Diff change set (reuses the reindex pipeline without clearing
  indexed_sha). Reindex now always does a full rebuild, so the two are
  clearly distinct.
- Force Stop: hard-abort the clone+index pipeline via new
  POST /projects/{hash}/force-stop. Deletes pending/running clone+index
  jobs (jobs.DeleteByDedupeKeys) so the queue can't retry, then cancels
  the live index session. handleIndex treats indexer.ErrNoSession as a
  clean cancel (no error status, no retry). Settles status with the
  never-indexed guard (created when indexed_sha empty, else indexed).
  404 for unknown hash, 422 for local projects.

Tests: force-stop clears jobs / 404 / 422 / cancelled-true never-indexed,
and jobs.DeleteByDedupeKeys.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
emptyOutDir:true wipes the whole output dir on every build, including the
committed dist/.gitkeep marker that keeps dist/ tracked for Go's
//go:embed all:dist. That made every `vite build` surface a deleted
.gitkeep, which kept leaking out of commits and breaking go build/CI on
fresh clones. Add a small closeBundle plugin that re-creates the marker
after the bundle is written, so clean builds (emptyOutDir) are preserved
without churning the marker.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Show live indexing progress on the project page for external (GitHub)
projects: the last 3 files being indexed plus a files_processed /
files_total count with a progress bar. Incremental sync knows the total
up front; a first full index shows "N files indexed" until the walk
completes.

- indexer: track a ring of the last 3 processed files per session and
  expose it via GetProgress; add SetDiscoveredTotal to publish the
  denominator mid-run.
- repoindexer: report the incremental change-set size up front.
- API: add current_files to IndexProgressInfo (regenerated server + TS).
- dashboard: useIndexStatus hook (polls /index/status while indexing) and
  an IndexingProgressCard rendered for external projects mid-index.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Avoid a potential React key collision if the same path appears twice in
the recent-files ring.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Gate the Reindex button behind a confirmation dialog — it's a heavy
full-rebuild operation that's easy to trigger accidentally.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a clearly-named env var for the base directory where external
(GitHub) projects are cloned, so operators can point it at a dedicated
volume — cloned repos can be large. CIX_WORKSPACES_DATA_DIR stays as a
backward-compatible legacy alias (CIX_REPOS_DIR takes precedence).

Documents the var in CONFIG_REFERENCE.md and .env.example.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ct identity

Introduce a per-user authorization model so a shared cix-server can serve
agents of many users safely, and fix local-project identity collisions across
machines.

Authorization (server + dashboard):
- Roles renamed viewer -> user; two-tier RBAC (admin, user).
- Projects/workspaces gain owner_user_id; external projects (git_repos peer)
  are ownerless and admin-administered.
- New view_groups + membership + project/workspace share tables. Members of a
  group get read/search on external projects and workspaces shared to it.
- Access enforced in handlers (requireProjectAccess/Ownership,
  requireWorkspaceVisible/Ownership, mustBeAdmin): list filtering, owner-on
  create, admin-only git-repos/github-tokens/groups, owner-or-admin mutations.
- New endpoints: groups CRUD + members, project/workspace shares,
  PUT /projects/{hash}/owner; /auth/me returns the caller's groups.
- Dashboard: View Groups admin module, share-to-group cards, reassign-owner,
  role-aware control hiding.

Per-machine project identity:
- Local project key is namespaced local:{machine_id}:{path}; path_hash derives
  from it so the same path on different machines/users no longer collides.
  display_path holds the real path; machine_id/machine_label added.
- CLI generates ~/.cix/machine_id, sends it on create, and computes the
  matching hash (client.EncodeProjectPath); server is the formula authority.

Migrations #10 (auth) and #11 (machine identity): existing users -> admin,
local projects + workspaces -> first active admin, external stay ownerless;
display_path backfilled. Breaking — announce in release notes; re-init local
projects. Coordinated CLI<->server bump.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The :server-cuda tag was unpinned, so every rebuild pulled a new llama.cpp
layout. Upstream split the server logic into libllama-server-impl.so, which the
cherry-pick COPY list never copied — llama-server then died at runtime with
exit 127 (missing shared library), embeddings never became ready, and the
container failed its healthcheck and restart-looped. This took the
develop-cu128 prod deploy down after a rebuild.

- Pin ghcr.io/ggml-org/llama.cpp:server-cuda by digest for reproducibility.
- Copy the server binary + ALL /app/*.so* instead of a fixed list, so future
  upstream lib-layout churn can't silently drop a required .so. The extra
  unused CPU-backend variants add only a few MB. libssl/libcrypto/libgomp/
  libstdc++/libgcc are already provided by the distroless/cc-debian13 base.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pinning llama.cpp by digest fixes reproducibility but introduces staleness —
nothing tells us when upstream ships a newer build. Add a freshness check so
the pin can't silently rot:

- server/scripts/check-llama-pin.sh: compares the digest pinned in
  Dockerfile.cuda against the live :server-cuda tag; emits a GitHub annotation
  + step-summary note when they differ. Warn-only (FAIL_ON_STALE=true to opt
  into a hard fail); tolerates registry hiccups.
- Wired into both CUDA image builds (prerelease-server on develop, the
  docker-cuda release job) so every build surfaces a reminder in the log.
- New weekly llama-pin-check.yml that runs the same check and opens/updates a
  tracking issue — the persistent, can't-miss reminder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
ListTunnels, GetTunnelStatus, and GetWebhookOrigin were missing the
mustBeAdmin check that every other tunnel/webhook handler enforces.
A non-admin user with a valid token could enumerate the tunnel
provider catalog, the active public_url / uptime / last_error, and
the effective webhook origin URL — all admin-tier infrastructure
detail. The dashboard already hides these views from non-admins,
so this just makes the backend match the documented model.

No secrets were exposed (connector tokens and webhook_secrets stay
on admin-only endpoints), but the matrix in docs/AUTH_REVIEW.md
classifies tunnels/* and github/webhooks/* as Admin and these three
handlers were the gap.

Added TestTunnelReadEndpoints_NonAdminForbidden — user-cookie GETs
return 403, admin-cookie GETs return 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…on-admins

Three UI surfaces were dangling admin-only actions in front of regular
users:

1. GitHub Integration sidebar entry + home-page card + /github-integration
   route. Every endpoint behind this page (github-tokens CRUD, webhook
   origin, reconcile webhooks) is admin-only on the backend — the page
   would render but every action would 403. Added `requiredRole: 'admin'`
   to the module so Sidebar / HomePage / App router all filter it out,
   matching how Users, View Groups, Managed Tunnels, Server already work.

2. "Add repo" button on Projects list. Triggers POST /git-repos (creates
   an external project, admin-only). Wrap with isAdmin check.

3. "Add repo" button on Workspace detail. Same as above — chains to
   /git-repos. Wrap with user.role === 'admin' check. Non-admins keep
   AddExistingProjectDialog for linking projects they already have access
   to.

No backend change — these endpoints already return 403 to non-admins.
This just stops the UI from advertising features the user can't use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…o caller's workspaces

GET /api/v1/projects/{hash}/workspaces previously called GetByHash and
returned the full workspace membership list for ANY project hash that
existed — no access check at all. A non-owner with a valid token could:

  - enumerate which project hashes are registered (200 with empty list
    vs 404 — easy probe);
  - read the names and IDs of every workspace a foreign project is
    linked into, including private ones (verified in a direct-access
    sweep: 14 hashes × 2 auth methods = 28 leaks; one of the workspace
    ids returned was `b951bd05-…` for the private "pf3" workspace,
    which the regular user has no path to via /workspaces/* — those
    correctly 404).

Fix in two parts:

  1. Replace the raw GetByHash + ErrNotFound branch with
     requireProjectAccess — admins/owners/group-members pass, everyone
     else gets 404 (hides existence, matching the pattern used by every
     other project-scoped endpoint).
  2. For non-admin callers, intersect the workspace result with the
     caller's VisibleWorkspaceIDs set — owning the project does not
     entitle you to learn that it is also linked into someone else's
     private workspace. Admins still see everything.

Note: the visibility query runs BEFORE the workspace SELECT on purpose.
SQLite ":memory:" backends in tests have a single-connection pool, and
overlapping QueryContext calls deadlock; ordering them serially avoids
that without forcing a per-test pool reconfiguration.

Added TestListProjectWorkspaces_AccessAndVisibility — three scenarios:
foreign hash → 404, own project as user → only own workspaces visible,
own project as admin → all workspaces visible.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The owner_user_id column is correctly populated in SQLite by migration #10
(local projects → first active admin; workspaces → creator), and the
OpenAPI schema already declares the field on both Project and Workspace —
but the conversion functions never wrote it to the wire. Result:
GET /api/v1/projects and GET /api/v1/workspaces returned every row with
owner_user_id absent, so the dashboard had no way to render "owned by X"
vs "external" indicators (and the recent direct-access audit could only
classify projects by the absence of the field, not by its real value).

Two one-liners:

  - projectToOpenAPI in server.go now copies p.OwnerUserID → out.OwnerUserId
    (both *string; nil means ownerless == external).
  - workspacePayload gains OwnerUserID *string and workspaceToPayload
    copies it through. The struct shape now matches openapi.Workspace
    field-for-field, as the doc comment already claimed.

No schema or generated-code changes needed — both schemas already had
the property declared.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…orkspace

Both schemas declared owner_user_id as nullable but NOT required, which
makes oapi-codegen generate `*string \`json:"owner_user_id,omitempty"\`` —
nil pointers serialize as the field being absent rather than as explicit
null. The TypeScript codegen mirrors that: `owner_user_id?: string | null`
instead of `owner_user_id: string | null`.

For consumers this matters: dashboard code has to distinguish "field
absent" from "explicitly null" when there is no semantic difference
between the two (external ownerless project either way). Marking the
field required-but-nullable normalizes the wire shape — external rows
now serialize as `"owner_user_id": null` and clients use `string | null`.

After this commit:

  server (Go):    *string `json:"owner_user_id"`      (no omitempty)
  dashboard (TS): owner_user_id: string | null        (required)

The bulk of openapi.gen.go diff is codegen-driven reshuffling of the
embedded swagger spec base64 — only the two struct tags carry real
semantic change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…/status

IndexStatus used requireProjectOwnership, blocking read-only access via
group share even though the same caller could /search the indexed
content and /summary the project. Dashboard impact: a "indexing in
progress" badge would 403 for any user who only has read access via a
group, despite that user being able to use the project.

Switched the gate to requireProjectAccess — same surface as /search,
/summary, /workspaces (which we fixed in an earlier commit on this
branch). All true write endpoints (UpdateProject, DeleteProject,
IndexBegin/Files/Finish/Cancel) still use requireProjectOwnership.

Caught by phase-2 access matrix sweep: shared project pf3-inventory
returned 200 on /search but 403 on /index/status for the same user.

Added TestIndexStatus_GroupMemberHasReadAccess covering three cases:
group-shared user → 200, foreign user → 404, admin → 200.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread .github/workflows/llama-pin-check.yml Dismissed
Comment thread .github/workflows/prerelease-cli.yml Dismissed
Comment thread .github/workflows/prerelease-server.yml Dismissed
Comment thread .github/workflows/prerelease-server.yml Dismissed
Comment thread .github/workflows/prerelease-server.yml Dismissed
@dvcdsys
Copy link
Copy Markdown
Owner Author

dvcdsys commented May 25, 2026

CodeQL alerts triage (alerts #26#30)

All five dismissed as won't fix with the same rationale (see individual alert pages for the per-alert dismissal comment).

Finding: actions/unpinned-tag — five new workflow files use action refs like docker/setup-buildx-action@v3 instead of pinning to a commit SHA. Real security recommendation, real (low) supply-chain risk.

Validation: the same @vN pattern is used across all 10 workflows in this repo — release-server.yml, release-cli.yml, ci-cli.yml, ci-server.yml, ci-plugin.yml, codeql.yml, security.yml, plus the three new ones — for a total of 41 unpinned references spanning 12 unique action@version pairs (all GitHub-org / Docker-org first-party plus softprops/action-gh-release). The bot only flagged the three newly-added files because CodeQL scans the PR diff, not the existing baseline.

Decision: fixing only the 3 newly-added files would be inconsistent and give a false impression of compliance; fixing all 10 (41 line edits + 12 SHA lookups + maintenance overhead going forward) is a focused refactor that belongs in its own PR, not the v0.6.0 release wave. Dismissed for this PR; org-wide SHA pinning tracked as a follow-up.

No merge gate — main is not branch-protected and the review state was COMMENTED, not CHANGES_REQUESTED.

…ncheck findings

govulncheck on PR #54 flagged nine reachable vulnerabilities — all
addressed by upstream dep bumps, no code changes needed:

golang.org/x/crypto v0.50.0 → v0.52.0 (8 vulns, all in x/crypto/ssh,
reachable via go-git's FetchContext from repocloner.CloneOrFetch):
- GO-2026-5021, GO-2026-5020, GO-2026-5019, GO-2026-5018, GO-2026-5017,
  GO-2026-5015, GO-2026-5013 — assorted SSH client/server panic, DoS,
  FIDO/U2F bypass, and underflow CVEs.

Standard library, go 1.25.9 → 1.25.10:
- GO-2026-4971 — net.Dial / net.LookupPort panic on NUL byte (Windows).
- GO-2026-4918 — net/http HTTP/2 transport infinite loop on bad
  SETTINGS_MAX_FRAME_SIZE.

go mod tidy also pulls transitive bumps: x/net v0.53.0 → v0.54.0,
x/sys v0.43.0 → v0.45.0, x/text v0.36.0 → v0.37.0, x/mod v0.34.0 →
v0.35.0, x/tools v0.43.0 → v0.44.0.

Local govulncheck: 0 vulnerabilities found.
go test ./...: green.
@dvcdsys dvcdsys merged commit d9cef8d into main May 25, 2026
13 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants