GET /repos/{owner}/{repo}/installation returns 404 both when the App is
not installed for the owner and when the installation's repository
selection excludes the repository. The single-repository mint path
reported only the first cause, which misleads users whose App is
installed but not scoped to the repository. Name both causes and the
repository in the error.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Consolidate the copies that review found across the feature:
- One GITHUB_CREDENTIAL_HELPER / GITHUB_CREDENTIAL_HELPER_KEY pair in
fabro-github, with apply_probe_git_env() for probe commands; the runtime
git bridge, server preflight probe, and live contract test all consume it
so the probes exercise exactly what the bridge configures.
- GitHubRepositoryAccess::resolve_verified_token() owns the
resolve-installations-then-mint choreography shared by server preflight,
workflow initialization, and the live test.
- A shared lookup_installation() helper backs both the shared-installation
resolution and the mint's installation lookup.
- The contents = read|write rule lives once as
RunIntegrationsGithubSettings::contents_permission_allows_repository_access.
- The preflight probe paces retries with fabro-sandbox's exported
replication_backoff() (3s/9s) instead of a contradicting 1s/2s loop, and
shares one run_ls_remote() runner with the existing remote-ref check.
Also: collapse the dead Ok(None) arm and repeated error blocks in the
preflight token check, drop the derivable bridge_entry_count(), privatize
resolve_permissions() behind resolve_integration(), make
GitHubRepositorySlug ordering/hashing allocation-free, and use EnvVars
constants for env names.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Add `additional_repositories` to the RunIntegrationsGithubSettings
OpenAPI schema and reuse the canonical Rust settings types through
`with_replacement`, with type-identity witnesses and JSON parity
tests for populated and empty repository sets.
- Regenerate the TypeScript API client.
- Document the feature in the GitHub integration and run-configuration
guides: exact layer replacement rules, single-token scope, gh/API
support, App-versus-PAT scope, the same-owner/same-installation
requirement, validation errors, supported Git URL forms, hard-failure
semantics for declared repositories, GH_TOKEN precedence, and the
security boundary (no second server-side repository intersection;
contents = "write" lets any stage push to any declared repository).
Correct the earlier claim that injecting GITHUB_TOKEN alone makes
arbitrary additional private clones work.
- Add a dated changelog entry and an opt-in live GitHub App e2e test
that verifies a scoped multi-repository token reads every declared
repository (and that a primary-only token cannot), with repositories
supplied through the test environment.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Carry the resolved GitHub integration (permissions plus declared
additional repositories) as one value from run materialization into
workflow startup, and make the sandbox environment reach every declared
repository through the single managed GITHUB_TOKEN.
- `StartServices.github_permissions` becomes
`github_integration: ResolvedGithubIntegration`; CLI and server
workers build it with `resolve_integration()` after interpolation and
pass it through `SandboxEnvSpec` as one unit.
- `build_sandbox_env` constructs the validated
`GitHubRepositoryAccess` and scopes the App token source to the whole
effective set. Missing credentials or a missing origin are hard
initialization errors when additional repositories are declared;
legacy permissions-only configuration keeps its best-effort behavior.
- When additional repositories are declared, initialization eagerly
resolves each repository's App installation (naming any repository
the App cannot see) and the token itself, so an inaccessible declared
repository fails before the first workflow stage.
- A new `git_bridge` module injects secret-free `GIT_CONFIG_*` entries
into the stage environment: a github.com credential helper that reads
`$GITHUB_TOKEN` at invocation time, per-repository SSH-to-HTTPS
`insteadOf` rewrites, and `GIT_TERMINAL_PROMPT=0`. Entries append
after a valid user-provided Git config overlay and fail clearly on a
malformed one. Contract tests drive the installed git binary against
local fixtures for the rewrite, credential, prefix-collision, and
overlay-preservation behaviors.
- The long-running ACP notice now says all declared repository access
expires together.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add `GitHubRepositoryAccess`, the secret-free validated value describing a
run's effective GitHub repository set: the primary origin repository plus
the declared additional repositories with the shared permission map.
- The constructor normalizes HTTPS and both SSH origin spellings to one
primary slug, rejects a missing or non-GitHub origin when additional
repositories are declared, rejects primary duplication and cross-owner
additional repositories, and re-checks that interpolated permissions
carry `contents = "read"|"write"` — exposing targets in deterministic
primary-first order.
- `resolve_shared_installation` resolves every target's App installation
with the App JWT and requires one shared installation ID, naming the
repository the App cannot see before any mint.
- The installation-token mint now accepts a repository-name list; the
single-repository entry points delegate to it, and the request body
lists every projected name with the shared permissions.
- `InstallationTokenSource::for_access` builds a source over the access
value; caching, refresh margin, and single-flight are unchanged.
- The scripted `MockHttpClient` and test RSA key move to a shared
crate-internal `tests_mock` module.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every push previously re-minted a fresh GitHub App installation token and
embedded it in the origin URL, so pushes routinely landed inside GitHub's
token-replication lag window (run 01M0DH033P2XSTHAGVBHG6922F failed
terminally on four consecutive fresh-token 404s). Reusing mature tokens
removes the failure trigger and saves two GitHub API calls plus one sandbox
exec per push.
- New fabro_github::token_source::InstallationTokenSource: one cached,
single-flight source per origin repo. Static credentials pass through
(generation 0); App credentials mint through the cache and reuse tokens
until REFRESH_MARGIN (10 min) before expiry. Every resolve returns a
non-secret TokenSnapshot (generation + Minted/Reused/Static provenance),
and the source logs mints at INFO and reuses at DEBUG.
- Docker and Daytona share the source through PushCredentialState: an embed
mutex serializes compare -> set-url -> record, a matching generation skips
the set-url exec, and the generation is recorded only after a successful
exec. The clone still mints its own token, but now seeds the source cache
(generation 1) and the last-embedded state, so a refresh mint failure
falls back to the known embedded token instead of believing nothing was
ever embedded.
- RefreshOutcome now reports the remote action (embedded/unchanged/none)
separately from the token snapshot; git_push_via_exec logs token age and
provenance with each push, and refresh failures log the last embedded
generation.
- The run-metadata writer resolves through the sandbox's shared source
instead of minting per snapshot (with its own cached source on resume).
- The ACP refresh-ahead loop reschedules from the embedded token's
expires_at minus the margin instead of a fixed 45-minute interval, which
a cached source would have broken for long turns; static credentials stop
the loop.
Plan: .ai/plans/git-push-token-resilience.md (PR 1: items 3 and 6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Structural cleanup of the durable pull request creation feature, from a
three-agent review (reuse, quality, efficiency) of the branch:
- Move the supervisor out of handler/ into server/pull_request_supervisor.rs,
collapse its double bookkeeping into one task-id map, and fold the five
copy-pasted failure arms into attempt_pull_request_creation.
- Tag pull_request.failed events with the creation id they resolve, so a
publish-stage failure can never fail an unrelated explicit creation. The
reducer gains PullRequestCreation::succeed/fail transition methods.
- Scan pending creations through a narrow projection-cache accessor instead
of materializing every run summary, raise the scan interval to 30s (notify
covers the live path), and cap retries for runs whose worker cannot even
record a failure.
- Answer "creation already pending" POSTs before taking the per-run create
lock, which a worker can hold for the whole creation.
- Replace the hand-rolled per-run lock map with fabro_store::KeyedMutex.
- Reuse cheap Arc'd projections (cached_run_projection) on the poll endpoint
and in the worker instead of deep-cloning run summaries and diffs.
- Merge ExistingPullRequest into fabro_github::CreatedPullRequest and
extract one reconcile_existing_pull_request helper for both call sites.
- Give the client poll loop a 15-minute deadline; document that Retry-After
and the poll interval are the same constant.
- Resolve a wedged pending creation (run already has a pull request) as a
durable failure instead of skipping it forever.
- Tests: shared wait_for_pull_request_creation helper, a pinned generation-
failure assertion, and a new pipeline test proving reconciliation adopts
an existing PR without an LLM call or create request.
Verified: cargo build --workspace, cargo nextest run --workspace (7,767
passed), nightly clippy -D warnings, fmt --check, insta (no pending), bun
typecheck in fabro-api-client.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Follow-up cleanup on the publish-failures change.
Error model:
- Collapse `Error::{Engine, Publish, Handler}` into one `Error::Stage` with an
`ErrorStage` discriminator. The three shared a field shape and had to be
edited together in four match groups; nine near-identical constructors
become two private helpers.
- Add `Error::failure_reason()`, replacing the same error -> FailureReason
mapping written out in four places.
- Publish errors are now terminal. Publish runs once, after execution, so no
caller could ever act on the retryable classification.
Publish phase:
- Fix: a branch that was pushed is now still reported when pull request
creation fails afterwards. `PublishOutcome` records what happened and
carries the error separately, instead of hiding both behind a `Result`.
- Drop `PublishOutcome::NoChanges`, which no consumer distinguished from
`Published { pr_url: None }`.
- Move publish onto `Concluded` as methods and replace three near-identical
precondition guards with one `publish_target()`.
Pull requests:
- `maybe_open_pull_request` -> `open_pull_request` returning the record
directly. Both callers already reject empty diffs, so the `Ok(None)` path
was unreachable.
- Drop `CreatedPullRequest.head_sha`, which echoed back its own input.
GitHub client:
- Delete `branch_exists`, which had no callers and duplicated
`branch_head_sha`. Give `branch_head_sha` the `_with_client` split every
sibling has and port the tests to `MockHttpClient`.
- Collapse the copy-pasted credential match in `resolve_clone_credentials`.
Events:
- `PullRequestCreated.head_sha` is `Option<String>` instead of using an empty
string to mean absent.
- Centralize the run-branch refspec in `lifecycle::push_run_branch`, so
`git.push` reports a branch name from both emitters as documented.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>