Repository navigation
Require authenticated Qdrant, least-privilege workflow tokens, scoped dependency triage - #109
Conversation
…lient key Server-mode Qdrant (http/https) clients now resolve QDRANT_API_KEY through resolve_qdrant_server_api_key() and raise QdrantAuthConfigError before any request when it is missing. QDRANT_REQUIRE_API_KEY=false is an explicit opt-out for disposable test servers only. Covered: SemanticIndexer (and all its users), semantic preflight probes and collection bootstrap, index management status, secure export, the debug utility, and the v13 smoke and pilot harnesses, which now start disposable servers with per-run keys passed to docker by name only. Repository Compose files require QDRANT__SERVICE__API_KEY (render fails without QDRANT_API_KEY; an entrypoint guard exits 64 on an empty key), pin qdrant/qdrant:v1.17.0 by digest, bind to loopback, disable telemetry and use the auth-exempt /readyz health check. Tests: unit fail-closed/propagation coverage per client, and an integration test that runs the repository docker-compose.qdrant.yml in a disposable project to prove anonymous denial and authorized access with synthetic collections. Migration and rollback (never restoring anonymous access) are in docs/operations/qdrant-authentication.md. Refs #106 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seven workflows had no top-level permissions block and inherited the
repository default (currently read/write). Add top-level
`contents: read` to workflows that check out code, and `permissions: {}`
to echo-only or external-notify workflows. Job-level write scopes remain
only where a job publishes, signs, attests, uploads SARIF or mutates PRs
and releases; the two SARIF upload jobs now also get the `contents: read`
their checkout needs.
tests/test_workflow_permissions.py fails if a workflow lacks top-level
permissions, grants a top-level write, or adds an unreviewed job write.
The repository default stays unchanged here; flipping it to read-only is
a separate owner action.
Refs #106
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… advisories Scoped pip-audit triage of uv.lock: 397 rows in 36 packages, 280 distinct advisories after alias deduplication. Only two base-install matches meet their advisory prerequisites: - starlette 1.0.0 -> 1.0.1 (GHSA-86qp-5c8j-p5mr): AuthenticationMiddleware decides auth exclusion from request.url.path, which Starlette reconstructs from an unvalidated Host header. - certifi 2026.2.25 -> 2026.10.18 (GHSA-6q48-6c7w-9gw2): every runtime HTTPS client trusts the bundle, which still includes a distrusted root. Smallest fixed versions; the lock diff touches only these two packages (revision header kept at 3; uv lock --locked passes). Every deferred and inapplicable match is recorded with its reason in docs/security/dependency-triage-2026-10-31.md, including mcp GHSA-hvrp-rf83-w775 (STDIO single session; fix changes TaskStore listing) and cryptography GHSA-537c-gmf6-5ccf (major bump, runtime unknown). Refs #106 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 717a97dbdd
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| server_url = os.getenv("QDRANT_URL", "http://localhost:6333") | ||
| if os.environ.get("QDRANT_USE_SERVER", "true").lower() == "true": | ||
| # Resolved outside the broad except so a missing required key is reported. | ||
| api_key = resolve_qdrant_server_api_key() |
There was a problem hiding this comment.
Handle missing Qdrant credentials in the status command
For a lexical-only installation with no file-backed vector index and no Qdrant configuration, QDRANT_USE_SERVER defaults to true, so this now raises QdrantAuthConfigError even though semantic search is optional. The index status command calls _get_vector_backend_status() outside its error handler, causing an uncaught exception and preventing the remaining metadata report from being displayed. Preserve the fail-closed connection behavior, but handle the configuration error in the status command so users can inspect their lexical indexes without configuring an unused Qdrant server.
Useful? React with 👍 / 👎.
Refs #106
This PR prepares the source side of #106 as three separate commits. It changes no repository settings and touches no live deployment, credentials or restarts. Merging it does not repair the running deployment. That needs the owner actions listed at the end.
1. Authenticated Qdrant and explicit client migration (
a745397)Before
docker-compose.qdrant.yml,docker-compose.yml,docker-compose.mcp.yml,docker/compose/development/docker-compose.development.yml) hadQDRANT__SERVICE__API_KEYcommented out, so Qdrant accepted anonymous calls.qdrant/qdrant:latest.docker-compose.qdrant.ymland the development file published 6333 and 6334 on all interfaces.QDRANT_API_KEYonly when it was set. Otherwise they connected anonymously.After
QDRANT__SERVICE__API_KEY=${QDRANT_API_KEY:?...}, so Compose refuses to render without a key.docker-compose.mcp.ymlrelies on this guard alone, because its Qdrant service is an optional profile.qdrant/qdrant:v1.17.0@sha256:f1c7272c…, the release that wrote the existing data and the digest the smoke harness already uses.127.0.0.1, telemetry is off, and the health check is/readyz(exempt from auth).http/https) client resolves its key throughresolve_qdrant_server_api_key(). Without a key it raisesQdrantAuthConfigErrorbefore sending any request; preflight reportsfailure_class: qdrant_api_key_missing.QDRANT_REQUIRE_API_KEY=falseis an explicit opt-out for disposable test servers only. File-mode and:memory:clients are unchanged.docs/operations/qdrant-authentication.md):SemanticIndexer._init_qdrant_client: gateway, plugin semantic path (now logs a warning instead of a silent debug message), indexer registry, index CLI.check_qdrant,check_qdrant_collectionand collection bootstrap.index_management._get_vector_backend_status: the key is resolved outside its broadexcept, so a missing key is reported.secure_export.scripts/utilities/debug_semantic_search.py.v13_qdrant_smoke.pyandv13_pmcp_pilot.pyharnesses: disposable servers now get a random key per run, passed to docker by name only. The pilot writes it into its private fixture.mcp.jsonnext to its existing per-run guard token.k8s/deployment.yamlalready maps theqdrant-api-keysecret.tests/root_tests/andarchive/now get 401 from an authenticated server; they do not bypass it.docs/operations/qdrant-authentication.mdhas behavior, inventory, migration, readback and rollback. Env templates, README, TROUBLESHOOTING, onboarding and the docker README are updated. CHANGELOG is unchanged because repository policy keeps[Unreleased]empty.2. Actions least privilege (
a65219c)permissions:and inherited the repository default, which is currently read/write.contents: readtoci-cd-pipeline,container-registryandindex-artifact-management.permissions: {}toindex-management,lockfile-check,mcp-indexandmaintenance(echo-only or external webhook jobs).contents: readnext tosecurity-events: write. Their checkout needs it once job-level permissions replace the defaults.tests/test_workflow_permissions.pyfails if a workflow lacks top-level permissions, grants a top-level write, or adds a job write scope that is not on the reviewed allowlist.3.
uv.lockadvisory triage (717a97d)uvx pip-audit2.10.1 againstuv export --locked --all-extrasfound 397 rows in 36 packages, or 280 distinct advisories after alias deduplication. Each match is classified by dependency set and import reachability. Only two base-install matches meet their advisory prerequisites:AuthenticationMiddlewaredecides auth exclusions fromrequest.url.path, which Starlette rebuilds from an unvalidatedHostheader.The lockfile diff is 12 lines and touches only these two packages. The
revisionheader stays at 3, anduv lock --lockedanduv sync --lockedpass. Re-scan: certifi 0 matches; starlette has 4 remaining matches, none applicable.Deferred and not-applicable items, each with its reason, are in
docs/security/dependency-triage-2026-10-31.md. Notable ones:enable_tasks()is used, but the server runs over STDIO with one session per process. The fix also changes how a customTaskStorelists tasks.chunker.repo.processorimports it, and that module is never imported (example ID: GHSA-284h-m62q-gf8w).semanticextras: not in the production image.Test evidence
tests/test_qdrant_api_key.py: 43 passed. Covers fail-closed behavior and key propagation for every client construction, and checks that the key never appears in error messages.tests/integration/test_qdrant_auth_container.py -m integration: 5 passed, run against a real disposable container. It starts the repositorydocker-compose.qdrant.ymlin a uniquely named Compose project on loopback host ports with throwaway storage and a random key.GET /collections,/telemetry,/metricsand an anonymousPUTof a collection all return 401./readyzreturns 200.SemanticIndexerclient creates, upserts, queries and deletes a synthetic collection.mcp_qdrantcontainer was untouched.test_workflow_permissions,test_localci_workflow_posture,test_workflow_release_policy,test_workflow_action_pins,test_coverage_workflow_posture): 51 passed.tests/ --ignore=tests/integration --ignore=tests/test_git_integration.py -n auto), local Python 3.14: 4130 passed, 137 skipped, 3 failed.test_release_notes.py::test_changelog_unreleased_emptyfailed on a CHANGELOG entry I had added. The entry is removed and the test now passes (4 passed).test_v13_pmcp_pilot.py::test_prepare_delivered_wheel_never_builds[True|False]raisepython_312_required. They fail the same way onmainbecause the local venv runs Python 3.14 and the test needs 3.12.make alpha-format-lint: rc=0 (pylint 8.94/10).make alpha-mypy-ratchet: rc=0 (1351 current errors, baseline 1562).make agent-gate: rc=0 ate643513. That commit has the same tree as the head except for the CHANGELOG entry removed afterwards.agent-gateruns failed only intest_baml_contract.py::test_baml_regeneration_and_formatting_are_reproducible, for environment reasons. First, no BAML 0.20.1 toolchain was installed. I then used the release tarball CI downloads, with the same sha256 check6067729f…. Second, that toolchain refused to run in an agent session without its "agent skill". It passed withBAML_AGENT_SKILL_CHECK=off, which skips only that tooling check.Residual risk
QDRANT_URLwithoutQDRANT_API_KEYstops semantic search (fail closed). Lexical search keeps working and indexed tools returnindex_unavailablewithnative_search.docker compose -f docker-compose.qdrant.yml(includingmake semantic-upand autostart) now needsQDRANT_API_KEYin the environment.mcp_qdrantruns fromdocker-compose.qdrant.ymlplus out-of-repo overrides. Its environment contains aQDRANT__SERVICE__API_KEYvariable (names inspected only, values not). The override supplies the key throughenv_file. After this change the base file'senvironmententry takes precedence, so any recompose must exportQDRANT_API_KEYwith the same value. Otherwise Compose refuses to render, which fails closed. Live anonymous denial has not been read back.GITHUB_TOKENhardening protects only workflows on this branch onward. The repository default stays read/write until the owner changes it.Rollout
ci-cd-pipeline,container-registryand manual release runs for permission errors.docs/operations/qdrant-authentication.mdsteps 1 to 7 in a maintenance window: inventory, back up storage, provision the key in the secret manager, update every client environment, restart through the repository Compose file, read back, then restart the clients.Rollback
QDRANT_API_KEYwhenever it is set.qdrant_api_key_missing): stay failed closed and fix the key injection.QDRANT_REQUIRE_API_KEY=falseon a production client, or by restoring anonymous access.a65219c, or add a specific job-level scope if a job turns out to need one.717a97d.Owner approval required (not done here)
writepergh api repos/Consiliency/Code-Index-MCP/actions/permissions/workflow.QDRANT_API_KEYinjected, update client environments, and approve the production readback. The readback checks: anonymousGET /collectionsreturns 401, the authorized client works, collections and point counts are preserved.🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.