feat(miner): add tenant create/list/destroy control-plane admin CLI - #7284
Conversation
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Add a `loopover-miner tenant` command group that provisions, lists, and tears down hosted tenant instances against the ORB+AMS hosting control-plane's provisioning API. tenant-client.js is a Bearer-authed HTTP client that mirrors discovery-index-client.js's env-gated, never-auto-enabled posture: it is inert unless LOOPOVER_MINER_CONTROL_PLANE is set and a URL configured. Unlike that client's fail-open opportunistic supplement, tenant admin actions FAIL LOUD -- a disabled/unconfigured/unreachable/non-2xx/malformed condition throws a clear error surfaced by the CLI as a non-zero exit, never a silent degrade. Each call is a single bounded request (no retry), so a non-idempotent create is never silently re-sent. Lifecycle states are passed through exactly as the API reports them. tenant-cli.js is a thin argv-parse + dispatch layer over the client; tenant is wired into the bin dispatch alongside the other network command. Regenerated the env reference for the three new LOOPOVER_MINER_CONTROL_PLANE* vars. Closes JSONbored#7275
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #7284 +/- ##
==========================================
+ Coverage 93.81% 93.82% +0.01%
==========================================
Files 704 706 +2
Lines 69462 69576 +114
Branches 18896 18896
==========================================
+ Hits 65167 65281 +114
Misses 3302 3302
Partials 993 993
Flags with carried forward coverage won't be shown. Click here to find out more.
|
fdf7136 to
e9d05e9
Compare
|
Tip ✅ LoopOver review result - approve/merge recommendedReview updated: 2026-07-19 12:46:08 UTC
Review summary Nits — 5 non-blocking
Decision drivers
Context & advisory signals — never blocks the verdict
Linked issue satisfactionAddressed Review context
Contributor next steps
Signal definitions
🧪 Chat with LoopOverAsk LoopOver a question about this PR directly in a comment — grounded only in the same cached, public-safe facts shown above, never a new claim.
Full command reference: https://loopover.ai/docs/loopover-commands 🧪 Experimental — new and may change. 🟩 Safe / merged · 🟦 Advisory · 🟨 Held for review · 🟥 Blocked / closed 💰 Earn for open-source contributions like this. Gittensor lets GitHub contributors earn for the work they already do — register to start earning →. Checked by LoopOver, a quiet PR intelligence layer for OSS maintainers.
|
…ck namespace (#8805) (#8813) maybeCloseForContributorCapOnOpen executed its close under only the per-(repo, author) cap lock — outside the "one shared lock namespace covering every mutating PR pass" contract (transient-locks.ts) — so a sweep-fanned agent-regate-pr job holding the PR-actuation lock could plan/execute a different action for the SAME PR concurrently with the cap close. The execution section now also try-claims the per-PR actuation lock, nested author-outer → PR-inner. That is the OPPOSITE nesting of the executor's pre-merge cap re-check (PR-outer → author-inner, #7284) — safe regardless: both locks are non-blocking try-claims, so cross-order contention degrades to both passes deferring cleanly (the end-of-pipeline cap check and the next tick are the backstops), never a blocking-wait deadlock. Documented at the claim site. Contention test mirrors the existing author-lock sibling: the close defers (no PATCH), the normal pipeline falls through.
…ibutor-cap-lock TTL (#9109) * fix(orb): flush orphaned locks at boot, restore sweep candidacy on resume, and widen the contributor-cap-lock TTL Three related lock/liveness fixes: #9021 -- every Redis-backed lock (pr-actuation-lock, ai-review-lock, contributor-cap-wake/-lock) survives a container restart with its TTL intact. On this single-instance deployment, any lock present at boot is provably orphaned -- the process that claimed it is gone. Left alone, each class strands real work for its own TTL (30 min for ai-review-lock, #8998). Adds flushOrphanedLocksAtBoot (selfhost/redis-cache.ts), a best-effort SCAN-delete over the four exclusivity-lock prefixes, wired in at server boot before the queue starts. Deliberately leaves delivery:*, pr-panel-retrigger-pending:*, ci-pending-first-seen:*, and fresh-rebase-forced:* untouched -- none are exclusivity locks, and each has its own restart-survival contract. #9018 -- a paused repo's PRs can go green DURING the pause window (CI- completion passes plan-and-suppress the whole time), and resuming performed no catch-up: if a PR was ALSO regated once before the pause, agent-sweep.ts's #never-endless-reregate rule permanently excludes it from future sweep candidacy, stranding it silently. Adds clearPullRequestsRegatedAtForOpenPrs and calls it on the paused->live transition, both from the single-repo MCP pause/resume tool and the installation-wide bulk-settings route -- restoring one-shot sweep candidacy for the repo's open PRs exactly once. #9024 -- claimContributorCapLock's 30s TTL was sized only for the executor's brief pre-merge recheck, but maybeCloseForContributorCapOnOpen holds the SAME lock across a much longer body (token mint, live GitHub calls, label writes, the nested pr-actuation-lock, a full executeAgentMaintenanceActions pass) that can exceed 30s under GitHub rate-limit backoff -- reopening the exact #7284 TOCTOU this lock exists to close. Widened to 600s, matching claimPrActuationLock's own TTL for a comparably long mutating body. Closes #9018, #9021, #9024 Tests: 3 new flushOrphanedLocksAtBoot tests (deletes matching keys, returns 0 with nothing to do, fails open per-pattern on a scan error), a paused->live MCP-tool test (clears open PRs' markers, never closed ones, never on pause or a repeat resume), two bulk-settings-route tests (clears across all repos in an installation; a non-agentPaused bulk change never touches the marker), and a TTL-parity regression pinning claimContributorCapLock's TTL to claimPrActuationLock's. 100% line+branch coverage on every changed line (src/server.ts is Codecov's own documented ignore-listed entrypoint file). * fix(ci): add the missing engine-build edge to @loopover/ui-miner#typecheck The miner-UI typecheck reaches into packages/loopover-miner/lib/** (declared as an input), and those files import @loopover/engine -- whose types resolve to packages/loopover-engine/dist/index.d.ts. But unlike @loopover/ui#typecheck (which declares the explicit @loopover/engine#build dependsOn edge for exactly this reason), the miner-UI task only had ^build, and miner-ui has no package.json dependency on engine to create that edge implicitly. Turbo therefore ran engine build CONCURRENTLY with this typecheck; whenever the engine cache missed AND the scheduler interleaved the two the wrong way, the typecheck raced ahead of the dist emit and failed with dozens of phantom "Cannot find module '@loopover/engine'" errors -- an intermittent, whole-job validate-code failure with no real defect behind it (observed live on #9107's first run and on main run 30214733191, while sibling runs of the identical commit passed).
…t just the re-check The per-author contributor-cap mutex was released before the merge it guards, reopening the #7284 TOCTOU: a concurrent cap-close could claim the just-released lock, live-verify the merging PR as still open, and wrong-close it for a cap the merge was about to relieve milliseconds later. The lock now spans the actual merge mutation via a finally that wraps performMutation, matching the sibling cap-close path. The approval-queue accept path also builds its own contributorCapMergeRecheck now, using the same resolvePerRepoContributorCapMatch the live webhook path uses -- previously an accepted staged merge skipped the pre-merge re-check entirely. Closes #9159
…t just the re-check The per-author contributor-cap mutex was released before the merge it guards, reopening the #7284 TOCTOU: a concurrent cap-close could claim the just-released lock, live-verify the merging PR as still open, and wrong-close it for a cap the merge was about to relieve milliseconds later. The lock now spans the actual merge mutation via a finally that wraps performMutation, matching the sibling cap-close path. The approval-queue accept path also builds its own contributorCapMergeRecheck now, using the same resolvePerRepoContributorCapMatch the live webhook path uses -- previously an accepted staged merge skipped the pre-merge re-check entirely. Closes #9159
…t just the re-check The per-author contributor-cap mutex was released before the merge it guards, reopening the #7284 TOCTOU: a concurrent cap-close could claim the just-released lock, live-verify the merging PR as still open, and wrong-close it for a cap the merge was about to relieve milliseconds later. The lock now spans the actual merge mutation via a finally that wraps performMutation, matching the sibling cap-close path. The approval-queue accept path also builds its own contributorCapMergeRecheck now, using the same resolvePerRepoContributorCapMatch the live webhook path uses -- previously an accepted staged merge skipped the pre-merge re-check entirely. Closes #9159
…-label integrity (#9235) * fix(gate): store linked-issue claim time per (PR, issue) instead of per PR The blended pull_requests.linked_issue_claimed_at column preserves an old timestamp whenever the new linked-issue set overlaps the old one, so a PR that claimed a throwaway issue on day one and later adds a valuable issue inherits the day-one timestamp for the new issue too -- letting an old placeholder PR backdate a claim and steal the duplicate-cluster winner slot. Add a durable per-(repo, PR, issue) claim ledger (linked_issue_claims, migration 0191) written once, immutably, the first time each issue is observed linked to a PR. The duplicate-winner election now reads this ledger, scoped to only the issue(s) actually contested with an open sibling, instead of the PR-level blended value. Closes #9160 * fix(gate): scope the duplicate-winner election to per-issue claim times Wire the per-(PR, issue) claim ledger (previous commit) into every duplicate-winner call site: resolveScopedLinkedIssueClaimedAt reads the ledger's earliest claim among only the issue(s) actually contested with an open sibling, and buildPullRequestAdvisory's election now compares against that scoped value instead of the PR's blended linkedIssueClaimedAt column. Falls back to the blended column when the caller doesn't resolve a scoped value, so every unwired caller keeps prior behavior exactly. Closes #9160 * fix(review): require reward-label opt-in and maintainer check for issue authors A contributor who authored (or was assigned) the linked issue previously inherited every label on it unconditionally, including reward-semantic labels from an additive mapping -- bypassing the trustMaintainerAuthoredIssueForReward opt-in and the maintainer-authorship check every other path already requires. Split the unlock: author/assignee identity still unlocks a plain type label unconditionally, but a reward-semantic label (named by any mapping with removeOtherTypeLabels !== true) now always falls through to the same opt-in + maintainer-check gate regardless of the author match. Closes #9161 * fix(orb): make the label-vs-close correlation guard durable across entry points The #label-close-split-brain guard only inspected the same-batch planned array, so a rejected or breaker-downgraded close could leave its paired enforcement label applied to a PR that stayed open. Two paths never carried the pair through to the executor's own correlation check: - downgradeCloseToHold now also drops any label whose closeKind matches a close it just downgraded (the breaker-downgrade half). - The approval-queue accept path now re-derives pairing from the durable audit trail (the same agent.action.close record every execution entry point writes through) before applying a close-coupled label, since the queue stages one action per row and an independently rejected close has nothing correlating it to the label in the same batch. Closes #9158 * fix(orb): hold the contributor-cap lock across the merge mutation, not just the re-check The per-author contributor-cap mutex was released before the merge it guards, reopening the #7284 TOCTOU: a concurrent cap-close could claim the just-released lock, live-verify the merging PR as still open, and wrong-close it for a cap the merge was about to relieve milliseconds later. The lock now spans the actual merge mutation via a finally that wraps performMutation, matching the sibling cap-close path. The approval-queue accept path also builds its own contributorCapMergeRecheck now, using the same resolvePerRepoContributorCapMatch the live webhook path uses -- previously an accepted staged merge skipped the pre-merge re-check entirely. Closes #9159 * chore(cf): regenerate worker-configuration.d.ts after rebasing onto main
Summary
Adds a
loopover-miner tenantcommand group —create <name> [--product <p>],list, anddestroy <name>— that provisions, lists, and tears down hosted tenant instances against the ORB+AMS hosting control-plane's provisioning API.lib/tenant-client.js— a Bearer-authed HTTP client that mirrorsdiscovery-index-client.js's env-gated, never-auto-enabled posture: completely inert unlessLOOPOVER_MINER_CONTROL_PLANEis set and a URL is configured. Unlike that client's deliberately fail-open opportunistic supplement, tenant admin actions fail loud — a disabled / unconfigured / unreachable / non-2xx / malformed-body condition throws a clearErrorthat the CLI surfaces as a non-zero exit and message, never a silent degrade. Each call is a single bounded request (AbortSignal.timeout, no retry) so a non-idempotentcreateis never silently re-sent. Lifecyclestatevalues are passed through exactly as the API reports them.lib/tenant-cli.js— a thin argv-parse + dispatch layer (runTenantCreate/runTenantList/runTenantDestroybehind arunTenantClidispatcher) over the client, with--jsonand human-readable output modes.tenantinto the bin dispatch alongsideorb export(the other network command), added help lines, and regenerated the env reference for the three newLOOPOVER_MINER_CONTROL_PLANE*vars.The client is injectable (
fetchImpl,env) and the CLI accepts injected client functions, so the whole surface is tested hermetically with no real control plane.Scope
wantedPaths(packages/**,apps/loopover-ui/**,test/**); no blocked pathsLOOPOVER_MINER_CONTROL_PLANE)Validation
typecheck(roottsc --noEmit) — cleantest:coveragefor the new suite — 38 tests pass; 100% line + branch coverage on both new source files (verified againstcoverage-final.json)npm run miner:env-reference);--checkcleanupstream/main;git diff --name-only upstream/mainshows only this change's filesSafety
Authorizationheader — never logged, printed, or echoedCloses #7275