feat(bin): read-only Bitbucket forge credential with session-start check#953
Open
mattwwatson wants to merge 3 commits into
Open
feat(bin): read-only Bitbucket forge credential with session-start check#953mattwwatson wants to merge 3 commits into
mattwwatson wants to merge 3 commits into
Conversation
mattwwatson
force-pushed
the
up/bb-credential
branch
from
July 24, 2026 03:37
cb51359 to
4803b58
Compare
4 tasks
firstmate could read GitHub through gh but had no way to read a Bitbucket repository, so Bitbucket-hosted projects had no credential path at all. Add bin/fm-forge-credential.sh: a login-keychain-backed read-only credential resolver that never places the secret on argv or stdout and passes it to curl through a config on stdin, plus api-get, forge-of, and repo-of helpers for read-only forge API calls. Session start gains a FORGE_CREDENTIAL: diagnostic that probes one repository per forge and reports a missing, stalled, or unusable credential instead of failing later at the point of use.
…rror classification
mattwwatson
force-pushed
the
up/bb-credential
branch
from
July 24, 2026 19:29
4803b58 to
2093ed9
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Upstream contribution from the mattwwatson/firstmate fork, first of three sequenced Bitbucket increments: give firstmate a read-only credential path for Bitbucket-hosted repositories, which it previously had no way to read at all (GitHub went through gh). Adds bin/fm-forge-credential.sh, a login-keychain-backed read-only resolver that never puts the secret on argv or stdout and hands it to curl through a config on stdin, with api-get/forge-of/repo-of helpers, plus a FORGE_CREDENTIAL: session-start diagnostic that probes one repository per forge so a missing, stalled, or unusable credential is reported up front rather than at the point of use. Rebased onto current upstream/main with one docs/scripts.md table conflict hand-merged (upstream's newer fm-pr-lib/fm-pr-poll wording kept, only the new credential row added). Test fixtures use neutral example-team/example-repo identities and the tests are fully stubbed and offline. Suite passes 26/26 locally on the upstream base. Note for reviewers: tests/fm-forge-credential.test.sh is a large new suite that will land in the portable serial lane unless you would prefer shard placement or an isolation proof - happy to follow your preference.
Part of a three-PR series adding Bitbucket support
This is the first of three sequenced PRs that together add Bitbucket support to firstmate. They are deliberately kept separate rather than combined, because together they are ~4,800 lines and each stage is independently reviewable and green on its own:
bin/fm-forge-credential.sh) and theFORGE_CREDENTIAL:session-start diagnostic.Each later PR will link back to the earlier ones. Nothing in this PR depends on the later two, so it can be reviewed and merged on its own.
What Changed
bin/fm-forge-credential.sh, a login-keychain-backed read-only credential resolver for Bitbucket-hosted repositories (a forge firstmate previously had no way to read at all, since GitHub goes throughgh). It keeps the secret off argv and stdout by handing it tocurlthrough a config on stdin, fixes the API host per forge so no caller can redirect the credential elsewhere, and exposescheck,api-get,forge-of,repo-of, andprobe-targetbehind a documented exit-code contract (absent, incomplete, rejected, no store, inconclusive, not-visible, unexpected, store timeout, store refused).FORGE_CREDENTIAL: <forge>: <reason>session-start diagnostic inbin/fm-bootstrap.sh. It scans tracked clones' origin remotes, asks the resolver once for the single repository worth probing, and spends at most one bounded request per forge per session start, so a missing, stalled, or unusable credential surfaces up front instead of when a PR step fails. "No credential store" and "credential cannot see repository" are reported once per home understate/forge-credential-<outcome>.<forge>, and a lock-refused session reports without recording. Missing-tool reporting now routes through areport_missing_toolhelper socurlis named at most once across checks.tests/fm-forge-credential.test.sh(fully stubbed and offline, neutralexample-team/example-repofixtures) covering the resolver and the bootstrap detection path, plus docs: a forge-credentials section indocs/configuration.md, the new script row indocs/scripts.md, theFORGE_CREDENTIALhandling playbook in thebootstrap-diagnosticsskill, and matchingAGENTS.md/README.mdpointers.Follow-up review pass corrected credential-store error classification (distinguishing a store that refused the read from a genuinely absent entry) and forge-error classification (mapping 5xx and rate-limit refusals to inconclusive rather than emitting a session-start line for an unreachable forge). Tests: the new suite passes 31/31,
--changed --base 587f591runs 42 suites with only the pre-existing timing-flakytests/fm-test-run.test.shfailing (passes 3/3 in isolation; this branch touches no test-runner code), and the lane coverage guard is green with the new suite in the portable serial lane. One open info-level review note remains:probe-targetdrops a final non-newline-terminated stdin line — latent only, as the in-tree caller always terminates.Risk Assessment
✅ Low: All four prior findings are fixed at the root cause with dedicated behavioral tests rather than patched at the symptom, the forge table now has a single owner, and the one delicate addition beyond the requested fixes — the single-read rewrite of the FIFO store read — checks out against bash 3.2 semantics and every value shape the suite exercises, leaving only a latent unterminated-final-line nit in a subcommand whose sole caller always terminates its input.
Testing
Ran the new
fm-forge-credentialsuite (31/31), the coverage-lane guard, and the full changed-file selection (42 suites), then demonstrated the feature end-to-end with an offline CLI transcript covering every exit-code branch of the resolver, proof that the credential reaches curl via stdin config and never argv, and the actualFORGE_CREDENTIAL:lines a captain sees frombin/fm-bootstrap.shincluding once-per-home suppression, detect-only handover, incident silence, and the GitHub-only home staying quiet. No screenshot is included because the change has no rendered UI surface — the end-user experience is a shell command and a session-start digest line, which the transcript captures verbatim. The only failure wastests/fm-test-run.test.sh, a wall-clock-race scheduler test unrelated to this change that passed 3/3 on re-run.Evidence: End-to-end CLI + session-start transcript (full)
Evidence: Reproducible offline demo script (stubbed keychain + curl)
Evidence: Resolver exit-code contract, exercised
Evidence: Secret never on argv — it travels on curl's stdin config
Evidence: Session-start digest (bin/fm-bootstrap.sh stdout, one home over successive sessions)
Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-forge-credential.sh:295- read_keychain_half treats every non-zero exit fromsecurityas "keychain entry <service> is absent from the login keychain". macOSfind-generic-passwordreturns 44 for not-found but 51 (errSecInteractionNotAllowed) when the stored item's access control requires interaction and the session cannot provide it, and 36/25293 for other auth failures. Failure scenario: firstmate runs over SSH or from launchd against an item whose ACL demands confirmation;securityexits 51 immediately (it does not block, so the STORE_TIMEOUT watchdog never fires and exit 10 is never reached), and session start prints a FORGE_CREDENTIAL line telling the captain the entry is absent from the keychain when it is present and merely unreadable unattended. The captain then goes looking for a missing entry instead of re-caching an existing one. Consider distinguishing the store's exit status: 44 keeps EX_ABSENT, any other non-zero status gets its own reason naming that the store refused the read rather than asserting the entry does not exist.bin/fm-forge-credential.sh:429- forge_get classifies every non-2xx/401/403/404/000 status as EX_UNEXPECTED (9), and forge_credential_report has no case for 9, so it prints the reason on every session start with no report-once record. Failure scenario: Bitbucket returns 503 during an incident or 429 under API rate limiting; the credential is fine, but every session start emitsFORGE_CREDENTIAL: bitbucket: unexpected response from bitbucket (HTTP 503)and loads the bootstrap-diagnostics skill for a problem that is neither a credential fault nor captain-actionable. This contradicts the contract the change itself documents at docs/configuration.md:321 ("It stays silent when the forge cannot be reached: being offline is not a credential fault") and at bin/fm-bootstrap.sh:78 — a 503/429 is the forge being unreachable in every sense that matters here. Consider mapping 5xx and 429 to EX_INCONCLUSIVE (7) alongside the transport-error path, leaving exit 9 for genuinely unclassifiable statuses.bin/fm-bootstrap.sh:590- forge_credential_check hardcodes[ "$forge" = bitbucket ] || continue, duplicating knowledge that bin/fm-forge-credential.sh's forge_supported/forge_keychain_service are documented as solely owning (docs/configuration.md:297: "its header owns the entry names, subcommands, and exit-code contract, and every caller goes through it rather than reading a store directly"). Failure scenario: a later increment adds a second firstmate-held forge to the resolver's forge table; the resolver handles it correctly but bootstrap's scan skips every clone on it, so the session-start diagnostic silently never fires for that forge and the regression is invisible until a PR step fails — exactly the failure this check exists to pre-empt. Consider a resolver query (e.g.holds-credential <forge>returning 0/1) so the forge table stays single-owned.bin/fm-bootstrap.sh:586- The probe-target scan spawns a freshfm-forge-credential.sh forge-ofbash process for every tracked clone (plusrepo-offor each Bitbucket hit), on top of the existinggit remote get-urlper clone, and only breaks once a usable Bitbucket remote is found. Failure scenario: a GitHub-only home tracking 30 clones runs 30 extra bash interpreter startups at every single session start and can never produce a FORGE_CREDENTIAL line, adding several hundred milliseconds of latency to a check that cannot apply to it. The change is otherwise careful about session-start cost ("the scan must never turn a home's clone count into a session-start request count" at line 583), but the process cost per clone is unbounded in the same way. A cheap pre-filter on the remote URL before spawning the resolver, or one resolver invocation fed all remotes, would remove it.🔧 Fix: fix(forge): correct credential-store and forge-error classification
1 info still open:
bin/fm-forge-credential.sh:608- cmd_probe_target'swhile IFS= read -r url; do ... donesilently discards a final line that is not newline-terminated:readreturns non-zero for the trailing partial record, so the loop body never runs for it. The header documents the contract as "Read git remote URLs on stdin, one per line" (line 33), with no requirement that the last line be terminated. Failure scenario: a caller pipes remotes without a trailing newline (e.g.git config --get-all remote.origin.url | tr '\n' ' '-style plumbing, orprintf '%s' "$url"for a single remote) and the only Bitbucket remote is the last one; probe-target exits 1, forge_credential_check returns silently at bin/fm-bootstrap.sh:598, and the session-start credential check never runs for a home that does track a Bitbucket repository. The current in-tree caller always appends a newline (bin/fm-bootstrap.sh:586-587), so this is latent rather than live, but probe-target is now a documented subcommand with its own usage line.while IFS= read -r url || [ -n "$url" ]; docloses it.tests/fm-test-run.test.sh:540-tests/fm-test-run.test.sh("jobs=2 must refill the first completed slot") is timing-flaky and failed once during the full changed-file run. Its fixture races asleep 0.5script against asleep 0.05one (tests/fm-test-run.test.sh:514-532) and asserts the third script starts before the slow one finishes, so it fails under machine load. It passed 3/3 on re-run in isolation, and the change touches none ofbin/fm-test-run.sh, so it is unrelated to this commit — flagged only so it is not mistaken for a regression.bash bin/fm-test-run.sh tests/fm-forge-credential.test.sh— 31/31 assertions pass (resolver + bootstrap detection)bash bin/fm-test-run.sh --changed --base 587f591— 42 suites, only the unrelated timing-flakytests/fm-test-run.test.shfailedbash bin/fm-test-run.sh --check-coverage— lane coverage guard green with the new suite in the portable serial lane (total=97 parallel=30 serial=58 herdr=9)bash bin/fm-test-run.sh tests/fm-test-run.test.shx3 — flake reproduction check, passed every time in isolationManual end-to-end demo:ROOT=$PWD bash demo-forge-credential.shdrivingbin/fm-forge-credential.shcheck/api-get/forge-of/repo-of andbin/fm-bootstrap.shagainst a stubbed keychain and stubbed curlManual leak check:grep -c '<dummy secret>' forge-credential-demo.txt→ 0, plus explicit inspection of the recorded curl argv vs curl stdin configdocs/fm-test-portable-shards.md:68- docs/fm-test-portable-shards.md's serial-lane timing rationale ("Measured serial remainder wall ... about 13 minutes", and the 20-minute portable-serial timeout justified by it) predates this change's 1031-line tests/fm-forge-credential.test.sh, which lands in that lane by the rule at line 66. The prose is not falsified — it is explicitly a Phase 1 measurement snapshot — and re-measuring requires CI timing artifacts I cannot produce here, so I left it alone rather than hand-editing a measured number. Worth refreshing from the next fm-test-timing artifact on main if the serial lane's wall clock has moved materially.✅ **Lint** - passed
✅ No issues found.
✅ **Push** - passed
✅ No issues found.