Skip to content

fix(configuration): provision the GitHub origin source binding - #2221

Merged
ScriptedAlchemy merged 5 commits into
masterfrom
fleet/github-source-binding
Sep 26, 2026
Merged

ScriptedAlchemy merged 5 commits into
masterfrom
fleet/github-source-binding

Conversation

@ScriptedAlchemy

Copy link
Copy Markdown
Owner

Fixes #2159

Root cause

Pull-request discovery never ran for a project unless an operator did two things by hand. Four gaps sat behind that, and a fifth bug stopped review ingest once discovery worked.

  1. Nothing provisioned the GitHub source binding. Project open wrote only the Cursor binding.tracedecay-daemon.project-open. ConfiguredGitHubSourceAccessAuthorityV1 found no GitHub binding, so discovery logged outcome="not_attempted" source_access=Denied.
  2. A repository missing from the profile was gated off. A repository without a [[github_review_sources]] entry answered ProfileGitHubReadOnlyCredentialMountOutcomeV1::NotConfigured. That became the GitHubCredentialNotConfigured Delivery gate, before any read.
  3. Anonymous discovery could not work. Discovery uses one GraphQL query, and GitHub's GraphQL API refuses anonymous clients. The cached refusal is 403 with x-ratelimit-limit: 0 and no usable checkpoint. The code mapped that to Denied, so a public repository was hard-denied without a token. The review-threads read is GraphQL as well, so review ingest had the same problem.
  4. Preview and apply ran different validators (the filed bug). BindSource appended the new binding (and UpsertAccessRule appended the new rule), leaving the list out of canonical order. ConfigurationValueV1::validate requires canonical order, and only apply built the resulting snapshot. So the dry run returned a plan that apply then refused with source binding order is not canonical.
  5. Found during the journey: one reply dropped the whole review read. The canonical code anchor is keyed by (repository, commit, path, lines) alone, so every comment on the same lines shares it. resolve_stored_seed still compared the stored seed's comment_id. The second comment on a line (any reply, here 4069777906 answering the Nit:) resolved to None. resolve_many then failed the batch, and the whole PR's review read became Unavailable.

What changed

  • Provisioning. Project open (both tracedecay init and every later open) binds the origin remote's GitHub repository as binding.tracedecay-daemon.github-origin. It goes through a compare-and-swap daemon write, publish_daemon_source_binding, which also replaced rebind_daemon_project_source_binding.

    • The binding follows origin when the remote changes.
    • An operator-bound GitHub binding for another repository is left alone.
    • The remote parser moved into application as github_repository_from_remote_v1, and the daemon's copy was deleted.
  • Credential. The local-login token source tries GH_TOKEN, then gh auth token, then git credential fill for https://github.com. The git probe runs with terminal prompts disabled and sends only the protocol/host request. A repository the profile does not name is read through that credential, anonymously when none exists. The GitHubCredentialNotConfigured gate lost its only producer and was deleted.

  • Anonymous discovery. Without a token, discovery uses the REST issue search repo:{owner}/{repo} is:pr head:{branch}. It then reads each candidate GET /repos/{owner}/{repo}/pulls/{n} to pin the exact head commit and head repository, which also finds fork heads. REST 401/404/422 map to Denied. The GraphQL route is unchanged for a credential. Review ingest for an anonymous source uses RestListPullRequestReviewComments, and the pull-request identity read follows either review read.

  • Typed state. GitHubSourceStatusV1 carries state (bound | unauthenticated_public | denied_no_credential), remedy, the discovery outcome, the PR number, and the head repository. It is recorded at advisory mount, served as github_source by tracedecay_status, and printed by tracedecay status. It is not in doctor; status is the only surface.

  • One validator for preview and apply. protected_change_snapshot_v1 (global-db) derives the candidate snapshot for the dry run, for protected apply, and for daemon binding writes. BindSource and UpsertAccessRule now insert in canonical order.

  • Anchors. The stored code anchor matches on location only. Per-comment author, body and URL anchors come from each comment's own seed.

  • Fixtures. Everything captured was cached; no test reaches GitHub. In fork_head_pull_request.json (Add Error::new_with_backtrace dtolnay/anyhow#463), all captured without a credential:

    • the REST head-ref search;
    • the pull-request read;
    • the GraphQL refusal;
    • a 422 search refusal for an unseen repository.

    Also added: rust_lang_log_741_review_comments.rest.json, which is anonymous REST.

  • Hermetic harness tests. The two harness journeys that used a github.com remote now use a non-GitHub host, so no test reaches GitHub. Docs: USER-GUIDE.md (GitHub source and pull-request discovery) and SECURITY.md (outbound access and credentials).

Fail before / pass after

Each "before" run is this branch with only the named fix reverted.

  • discovery::tests::anonymous_discovery_finds_a_fork_headed_pull_request_by_rest_head_ref_search, which runs anonymously against the cached anyhow#463 answers. Before, with the scan routed to GraphQL as on master:
    assertion `left == right` failed
      left: Denied
     right: Found(GitHubExactCommitPullRequestV1 { target: GitHubRepositoryTargetV1 { owner: "dtolnay", repository: "anyhow", pull_request_number: 463, ... }, head_repository_owner: "sb123sb123", head_repository_name: "anyhow", ... })
    
    After: ok, with GitHubSourceStatusV1 { state: UnauthenticatedPublic, pull_request_discovery: Found, pull_request: Some(463), head_repository: Some("sb123sb123/anyhow"), .. }. No GraphQL request is sent.
  • config::tests::runtime_configuration_cutover::fresh_open_binds_the_github_origin_as_the_project_github_source, a fresh open of a checkout whose origin is https://github.com/dtolnay/anyhow.git. Before:
    left: []
    right: [("binding.tracedecay-daemon.github-origin", LocatorDigest("sha256:358b3600…"))]
    
    After: ok. A reopen keeps the revision, and a remote moved to git@github.com:rust-lang/log.git rebinds.
  • configuration::operations::tests::protected_dry_run_refuses_what_apply_refuses_with_the_same_reason: unbinding an absent binding. Before: left: Ok(ProtectedChangePlan { … }), right: Err(PlanStale). After: ok, and the preview equals protected_change_snapshot_v1's refusal.
  • domain_suite::configuration_contract::bind_source_inserts_in_canonical_binding_order, which binds binding.github.rust-lang-log before binding.tracedecay-daemon.project-open. Before:
    an out-of-order binding id still binds: Domain(NonCanonical { field: "source binding order" })
    
    After: ok.
  • runtime_acceptance_suite::advisory_runtime_acceptance::retained_review_body_expansion_rechecks_exact_scope_and_source_access, using the real ProjectGitHubAnchorAuthorityV1 with a reply seed on the same lines. Before: panicked at …advisory_runtime_acceptance.rs:661:10: canonical body anchors. After: ok.
  • Also new (after: ok):
    • credentialed_discovery_reads_the_head_ref_graphql_query;
    • anonymous_discovery_of_a_repository_github_will_not_show_is_denied_no_credential;
    • delivery::tests::anonymous_rest_review_comments_publish_the_pull_request_lane;
    • the harness journey protected_bind_source_preview_and_apply_share_one_outcome: preview, then apply → effect.
  • Updated: in protected_preview_redacts_the_change_and_refuses_stale_or_invalid_input, previewing an absent-binding unbind is now refused with configuration.stale, which is what apply answers.

Runtime journey

Debug tracedecay-cli --no-default-features --features production. One daemon under systemd-run --user --scope -p MemoryMax=6G -p MemorySwapMax=1G. Isolated HOME/XDG_*, with GH_TOKEN unset, gh auth status reporting You are not logged into any GitHub hosts, and no profile config.toml.

$ git clone https://github.com/rust-lang/log && git fetch origin pull/741/head:ci/msrv-build-vs-test && git checkout ci/msrv-build-vs-test
$ git log --oneline -1
1a4b67c remove win32 build target
$ tracedecay init
initialized …/log; daemon code-index reconciliation requested
$ tracedecay tool status --args '{"wait_for":{"state":"fresh","timeout_ms":90000}}'
readiness_wait: {"outcome": "reached"}   code_index_freshness.status: current
daemon: event="github_source" state=UnauthenticatedPublic repository=rust-lang/log
daemon: event="github_pull_request_discovery" outcome="found" pull_request=741 head_repository=rust-lang/log
daemon: event="feedback_advisory_mount" outcome="mounted" deferred=true
$ tracedecay status
GitHub rust-lang/log: unauthenticated_public · PR #741 found (head rust-lang/log)
  remedy: reads are anonymous (60 requests/hour); run `gh auth login` or set GH_TOKEN to read with a credential
$ tracedecay tool feedback_advisory_cycle --document-uri file://…/log/src/lib.rs
advisory_provider_states: git_hub_review failed (#2218), ci_localization supported_completed_complete, proximity unavailable
$ curl /api/delivery/overview
pull_requests ready   "Split MSRV used to build from MSRV used to test" open +17 −8 1 files
  operations: pull_request complete/complete, review_comments complete/complete
review_comments ready total_retained 3 [4069686687, 4069691901, 4069777906]

In the dashboard, Delivery → Journey for the admitted PR serves 56 episodes over 3 lanes. The CI / review lane holds the three review comments on .github/workflows/main.yml:135/113/113, the review comments read · complete observation, and Next action REVIEW · Unresolved review / NEW REV.

Before this branch, the same journey logged github_pull_request_discovery outcome="not_attempted" source_access=Denied. The #2173 journey needed a hand-added binding and a profile [[github_review_sources]] entry, and /api/delivery/overview read pull_requests: not_published.

Checks

  • cargo test --lib, after rebase:
    • tracedecay-application: 476 passed.
    • -configuration: 42.
    • -dashboard-api: 172.
    • -domain: 222.
    • -global-db: 374.
    • -mcp: 392.
    • -project: 37.
  • Suites (application_suite and domain_suite ran before the rebase; the other two after):
    • application_suite: 64 passed.
    • domain_suite: 171.
    • runtime_acceptance_suite -- advisory_runtime_acceptance: 5.
    • tracedecay --lib -- production_harness::{configuration_protected_preview_journey_test, delivery_read_gate_journey_test, advisory_cycle_language_journey_test}: 5.
  • cargo clippy -p tracedecay-domain -p tracedecay-global-db -p tracedecay-configuration -p tracedecay-application -p tracedecay-project -p tracedecay-mcp -p tracedecay-cli -p tracedecay-dashboard-api -p tracedecay --all-targets --features tracedecay/test-helpers -- -D warnings: clean.
  • cargo fmt --all -- --check: clean.
  • No dashboard wire shape changed.

Left open

@changeset-bot

changeset-bot Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7e84af6

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 26, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-26T13:57:58.387352Z 46d69b5 PR opened
ℹ️ 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" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 46d69b55d2

ℹ️ 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".

Comment on lines +1645 to +1647
let operation = if github.credential.is_anonymous() {
GitHubReviewReadOperationV1::RestListPullRequestReviewComments
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve review-thread resolution on anonymous reads

When no credential is available, this routes production ingestion through the REST review-comments endpoint, whose payload has no thread-resolution field; rest_lifecycle therefore never emits Resolved. Consequently, comments from resolved threads remain non-resolved, and delivery.rs filters only Resolved at line 1124, presenting those comments as unresolved review attention. Anonymous reads need a truthful lifecycle/coverage state rather than treating unavailable resolution data as unresolved.

AGENTS.md reference: AGENTS.md:L7-L12

Useful? React with 👍 / 👎.

Comment on lines +190 to +193
if !credential.is_anonymous() {
Self::Bound
} else if discovery == Some(&GitHubExactCommitDiscoveryOutcomeV1::Denied) {
Self::DeniedNoCredential

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Report credential rejection instead of bound

If GH_TOKEN, gh auth token, or the credential helper returns an expired token or one without access to this repository, discovery returns Denied, but this branch reports Bound solely because the credential is non-anonymous and supplies no remedy. Thus tracedecay status claims that a credential authorizes reads precisely when GitHub has rejected it; the denied credential needs its own truthful state/remediation.

AGENTS.md reference: AGENTS.md:L7-L12

Useful? React with 👍 / 👎.

Comment on lines +480 to +485
let Some((owner, repository)) =
tracedecay_runtime_core::git::git_remote_url(&target.project_root)
.as_deref()
.and_then(github_repository_from_remote_v1)
else {
return Ok(current);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the daemon binding when origin stops being GitHub

When a checkout previously bound to GitHub has origin removed or changed to a non-GitHub host, this early return leaves binding.tracedecay-daemon.github-origin durably authorizing the old repository. That contradicts the documented promise in docs/USER-GUIDE.md:885-890 that the binding follows origin and leaves a stale source authority indefinitely; this path should unbind the daemon-owned binding while preserving operator-owned bindings.

AGENTS.md reference: AGENTS.md:L169-L171

Useful? React with 👍 / 👎.

Comment on lines +295 to +299
pub fn record_github_source_status_v1(project_root: &Path, status: GitHubSourceStatusV1) {
github_source_status_registry_v1()
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.insert(project_root.to_path_buf(), status);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear cached GitHub status when the source disappears

This process-global registry only inserts or replaces entries and exposes no removal path. If a checkout records a GitHub source and is later reopened after removing or repointing origin to another host, GitHub discovery no longer records anything, so tracedecay_status continues returning the old repository, credential state, and PR indefinitely instead of not_observed. Reconcile or remove this entry on every project reopen/owner teardown.

AGENTS.md reference: AGENTS.md:L7-L12

Useful? React with 👍 / 👎.

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.

fix(configuration): bind_source preview accepts a binding its apply refuses

1 participant