Provider-aware repository rollout
Owner: coordinating project-manager and DevOps. Contract: #3373 Revision 2; implementation #3377/#3378. This runbook does not authorize deployment or enablement. Registration records declared GitHub/GitLab metadata, not upstream existence, credentials, repository access, checkout, redirect following, or a verified head.
Application default and explicit opt-out
Starting with #3429, provider metadata registration defaults on in every
environment running the upgraded application and deployment configuration.
cmd/context-engine/main.go passes the tested repoProviderRegistrationEnabled
helper into reporegistry.WithProviderRegistration. The helper uses os.LookupEnv:
| Environment value | New explicit-provider registration |
|---|---|
| Absent | Enabled |
Exactly true | Enabled |
Exactly false | Disabled |
| Present but empty | Disabled |
Any other value (TRUE, 1, whitespace, invalid text) | Disabled |
This is not a change to old running binaries. Existing explicit false values
continue to disable registration after upgrade until the operator removes or
changes them. Compose uses ${REPO_PROVIDER_REGISTRATION_ENABLED-true} (without
:), so an absent input becomes true while an explicitly empty input stays
empty. Do not use a permissive boolean parser or :-true interpolation.
Every registration still goes through ConnectAdapter.RegisterRepo →
Service.RegisterRepo → Store.RegisterRepo. The store interprets identity and
applies the existing capability, clearance and tenant/team checks. A bare
reporegistry.NewStore() retains its library default off;
cmd/upsquad-knowledge-mcp/main.go uses this constructor for reads and exposes no
registration writer.
Disabling the flag blocks new explicit-provider registrations, with
FailedPrecondition REPO_PROVIDER_REGISTRATION_DISABLED. It does not disable
list/read/map/removal, remove registry history, delete upstream content, revoke
credentials or relax server authority. Omitted-provider, unknown-host legacy
registration keeps its previous nonblank path/ID bytes. A host or URL without a
provider is InvalidArgument. Historical 007, group//project, and URI-looking
paths remain legacy data; unknown host does not mean gitlab.com.
Compatibility before upgrading or enabling
Default-on makes readiness a pre-upgrade requirement, including automatic
reconcilers watching main. If an installation is not ready, persist an explicit
REPO_PROVIDER_REGISTRATION_ENABLED=false in its supported operator configuration
before upgrading code/manifests. Keep it false throughout a mixed-version
rollout. Do not add false defaults to the shipped manifests: the hold is a
specific installation's explicit opt-out.
- Land reviewed schema/backend changes and verify the actual merged revisions, migration blobs, independent exact-head approvals and executed CI. Apply migration 251 or a higher clean version with the authorized migration role; record version/dirty state and role posture with migration-role-rls-posture.md.
- Inventory and upgrade every reader of the installation's registry dataset to compatible pinned code before allowing new provider-aware writes. Include RepoService replicas, gateway processes running the context-engine binary, standalone knowledge MCP, and scale-out templates. An independently isolated tenant dataset has its own readiness record; one healthy beta endpoint proves nothing about another tenant's inventory.
- Record each actual instance/container/pod UID, role, digest/revision, desired template version, selected flag value, health and probe results. Inspect only these fields, never entire environments or secret contents. Resolve missing revision labels against the release digest manifest. Unknown or older readers block activation; a mutable restart must not resurrect an older reader.
- Use the deployment's established identity flow and authorized tenant/team
scope for per-reader probes. Read retained odd legacy paths by path and their
printed
{"repo_id":"..."}selectors; preserve unknown GitLab host and exact legacy bytes, and verify no-map/UNKNOWN freshness where applicable. Exercise qualified selectors and provider-host presence using existing legacy rows; absent retained rows are an explicit evidence limitation, not permission to fabricate history. Use scoped staging fixtures for mixed-provider reads. During an explicit hold, confirm the named gate-off denial and no new row. - After the scoped readiness evidence passes and deployment is authorized,
remove the explicit false override or set literal
true; recreate/restart the affected writers using compatible images. Then perform authorized disposable GitHub/GitLab registration/list/map/removal acceptance, retaining receipts and a cleanup manifest. These are metadata operations, not evidence of upstream access, checkout or a verified head.
For beta's existing dev-bootstrap identity flow, record the founder-authorized Vaisakh or Ashik persona and actual Engineering-team mapping. This is live beta identity acceptance, not Clerk-authentication evidence. Do not request a nonexistent Clerk session for that flow, invent caller IDs, or bypass server authority. Production identity requirements remain unchanged. This runbook does not itself authorize a fleet deployment or changes to auth/network/secrets.
Supported operator configuration and restart
| Deployment | Writers / configuration |
|---|---|
docker-compose.dev.yml | context-engine; retained box-local .env |
docker-compose.tenant.yml | context-engine; tenant operator interpolation |
docker-compose.ics.yml | Both context-engine and gateway; ICS operator interpolation |
| Dev registry and local-embedding overlays | Inherit the dev flag; render with their dev base |
docker-compose.embedding-egress-smoke.yml | Isolated CI context-engine writer; same opt-out, no deployment claim |
test/load/docker/docker-compose.drills.yml | Source-built fault-drill context-engine; same operator opt-out |
| Kubernetes base and dev/staging/prod/mnet overlays | context-engine-config ConfigMap; all three context-engine containers explicitly bind its key |
The inventory guard discovers every docker-compose*.yml outside generated/cache
directories and requires actual render coverage for each writer. Root-Dockerfile
non-writers are explicitly excluded only while their declared SERVICE remains:
mg-baseline-report, preview-router, upsquad-memory-mcp, agent-orchestrator,
approval-scheduler, model-gateway, and the smoke bench seed/reembed/score
commands (tools/recall-bench, tools/rag-reembed). They build other binaries and
expose no RepoService writer. The registry overlay inherits the guarded dev flag
and is tested together with that base; it is the sole mapping-inheritance
exclusion. Missing or stale exclusions fail the sweep.
The sandbox worker overlay has no context-engine writer. Standalone knowledge MCP
is read-only and keeps NewStore()'s unchanged library default. Kubernetes's
compaction/embedding containers use the same binary; they receive the same flag
rather than relying on a service-name assumption. The base ConfigMap ships
REPO_PROVIDER_REGISTRATION_ENABLED: "true". Every explicit env binding uses
configMapKeyRef, so an envFrom Secret cannot shadow this flag. No secret is read
by the render tests.
Compose
Set the following in the installation's retained operator .env to disable new
registrations, or set literal true to re-enable after readiness checks:
REPO_PROVIDER_REGISTRATION_ENABLED=false
The calling shell takes precedence over .env; unset a stale exported flag
before running Compose so the durable operator value wins. Explicitly empty is
also off. A one-off command-line override is not durable across reconciliation.
Select the installation's actual Compose file(s) and recreate all its writers:
# Tenant example; images must already be present and verified compatible.
unset GH_TOKEN REPO_PROVIDER_REGISTRATION_ENABLED
docker compose -f docker-compose.tenant.yml up -d --no-deps --pull never context-engine
# ICS has TWO writers.
docker compose -f docker-compose.ics.yml up -d --no-deps --pull never context-engine gateway
For beta, the existing reconciler reads docker-compose.dev.yml and the retained,
gitignored /opt/upsquad/upsquad-core/.env. Preserve all unrelated entries and
retain a verified compatible digest pin across flag changes:
CONTEXT_ENGINE_IMAGE_VERSION=latest@sha256:<verified-compatible-digest>
REPO_PROVIDER_REGISTRATION_ENABLED=false
The dev image retains the literal ghcr.io/upsquad-ai/upsquad-context-engine
namespace required by publication coverage. Its default version remains latest;
readiness-sensitive operations pin the actual compatible digest to prevent a
mutable-tag restart surprise. Registry overlays have their own image override;
verify/pin that effective image through their established release mechanism too.
Inspect only selected rendered values, not unrelated environment or credentials:
unset GH_TOKEN REPO_PROVIDER_REGISTRATION_ENABLED
cd /opt/upsquad/upsquad-core
docker compose -f docker-compose.dev.yml config --format json | python3 -c '
import json, sys
s = json.load(sys.stdin)["services"]["context-engine"]
print(json.dumps({"image": s["image"], "provider_registration":
s["environment"]["REPO_PROVIDER_REGISTRATION_ENABLED"]}))'
flock -n /tmp/dev-reconcile.lock docker compose -f docker-compose.dev.yml \
up -d --no-deps --pull never context-engine
An occupied reconciler lock is a wait condition. Record before/after selected
flag, container ID, image digest/revision and health; require the verified image
to remain in use. Beta health is http://127.0.0.1:9095/healthz (use /readyz
if you want dependencies checked rather than liveness). It was 8084 until
#3585 — that was the deleted WS listener's /healthz, and the host port is no
longer published, so the old URL is connection-refused. /healthz and /readyz
are served on the metrics mux (container :9091, host 9095). Then run scoped
acceptance through the deployed UI. Removing/changing the explicit false is a
controlled activation; merging a default-on config can activate an unheld beta
reconciler, so establish readiness or the explicit hold before landing.
Kubernetes
Persist REPO_PROVIDER_REGISTRATION_ENABLED: "false" in the installation's
ConfigMap overlay/managed configuration, render it with the existing Kustomize
workflow, and apply through that installation's approved deployment process. The
same value can be set to "true" after compatibility checks. Set this key; never
delete it: the non-optional binding deliberately makes a missing key a
CreateContainerConfigError, not the bare binary's absent-environment default.
Managed overlay "false" holds are supported by the render guard, including an
overlay explicitly set to "true"; only overlays without an override inherit
base changes. EnvFrom and
configMapKeyRef values are captured at container start: changing the ConfigMap
alone does not change running processes. Restart and observe all three
Deployments (or use their established GitOps rollout mechanism):
kubectl -n platform rollout restart deployment/context-engine \
deployment/context-engine-compaction-worker deployment/context-engine-embedding-worker
kubectl -n platform rollout status deployment/context-engine
kubectl -n platform rollout status deployment/context-engine-compaction-worker
kubectl -n platform rollout status deployment/context-engine-embedding-worker
Do not rely on an ad-hoc ConfigMap patch that GitOps will overwrite. Keep the explicit hold in the managed overlay through mixed-version upgrades, and retain compatible image pins. The checked-in dev Argo Application watches main with automated sync; production is an explicit promotion path, not authorization to deploy a production fleet from this change.
Verification
bash scripts/test-repo-provider-compose.sh and its Bash-CI discovery wrapper
render real profiles with clean synthetic interpolation and no operator secrets.
They test absent/default, false, empty, true and invalid values, every writer,
complete unrelated-field isolation, image pin/publication coverage and exact
negative-control predicates. bash scripts/test-repo-provider-kubernetes.sh and
its Bash-CI wrapper use existing Kustomize/PyYAML to render every maintained overlay
and prove all three explicit ConfigMap bindings and opt-outs. Malformed renders
or failures at a different predicate cannot count as semantic controls. Neither
harness starts containers, pulls images, contacts a cluster or changes data.
The focused TestRepoProviderRegistrationEnabled Go table covers absent, empty,
literal true/false, uppercase, numeric, invalid and whitespace inputs; the Compose
harness guards the production constructor's actual helper linkage. The registry
package's existing tests retain library defaults and server-owned behavior.
Identity and operator guidance
GitHub owner/repo defaults to github.com; an HTTPS URL supplies its actual host.
GitLab explicit registration requires a visible known host and canonical decimal
project ID [1-9][0-9]*, retained as text without an integer-width limit. Never
normalize legacy IDs or infer their hosts. DNS case and HTTPS port 443 normalize;
nondefault ports remain visible. DNS/strict IPv4 are supported; IPv6, trailing-dot
DNS, Unicode host spellings and leading-zero ports are rejected. URLs and supplied
structured fields must agree. .git is never stripped and self-hosted provider
is an explicit declaration, not an inference from DNS.
Same paths can legitimately coexist across provider/host namespaces. Plain path
reads return REPO_LOCATION_AMBIGUOUS when more than one visible active row
matches. Use the exact registry UUID selector printed by list_repos or a qualified
path; explicit empty provider_host selects the unknown-host legacy namespace,
while omission does not filter host. Team comes from validated agent identity on
MCP and is not a tool argument.
GitHub rename/transfer is remove plus add. Re-add receives a new registry UUID and inherits no map, head, section curator or divergence provenance. Removal does not delete upstream content or revoke credentials. No redirect/upstream-ID continuity is claimed.
Rollback after provider-aware data
The committed old_first_match_reader_is_unsafe DB control demonstrates why an
old binary is unsafe: four visible same-path registrations exist and its
unqualified QueryRow returns an arbitrary row, whereas the compatible resolver
refuses ambiguity. Turning new writes off cannot repair old readers of existing
data.
Once any GitHub or known-host identity exists, including removed history, retain migration 251 and compatible readers on rollback. Disable new writes if needed, then fix forward or deploy a previously verified compatible build. Do not roll back to an old reader merely because the flag is off. DOWN must refuse unrepresentable all-history metadata and old-schema active collisions, with a write-exclusive lock covering preflight through schema mutation. A refusal can leave golang-migrate metadata dirty while transactional schema/data remains unchanged; investigate using the schema runbook instead of forcing the version. Do not delete, rename, normalize, or fabricate real registration data to force a rollback. Old-binary/DOWN eligibility needs a separately verified representable, unambiguous entire population and coordinator authorization.