Skip to content

feat(github): connect-repo picks from the repos this caller can reach - #207

Merged
Fermionic-Lyu merged 4 commits into
mainfrom
feat/github-device-connect
Sep 11, 2026
Merged

feat(github): connect-repo picks from the repos this caller can reach#207
Fermionic-Lyu merged 4 commits into
mainfrom
feat/github-device-connect

Conversation

@Fermionic-Lyu

@Fermionic-Lyu Fermionic-Lyu commented Sep 11, 2026

Copy link
Copy Markdown
Member

Closes #205. Pairs with insta-platform#412 and #416, both deployed to prod.

What changed

insta compute connect-repo <owner/repo> [service] used to find the repo by listing every installation the org holds and then each installation's repositories — an org-scoped view the platform has since made admin-only and caller-scoped (insta-platform#416), because it showed every member every repo a shared installation covers.

It now asks one question: POST /orgs/:orgId/github/repos with an empty body — the repositories this caller's own GitHub account can reach, answered from the identity they already authorized. Same question the connect gate asks again when the source lands, so a repo missing here would be refused there anyway.

When the platform says there is no usable identity, the terminal earns one the way the console does, without a browser callback or a client secret:

$ insta compute connect-repo acme/app web
this terminal is not authorized with GitHub yet — authorize it once:
  open https://github.com/login/device and enter the code WDJB-MJHT
waiting for you to confirm… (ctrl-c to abort)
connected acme/app → compute web (nixpacks): building main now — every push to main redeploys it

The prompt goes to stderr, so --json stays one parseable document.

Eight things self-review caught in the first draft

Each is mutation-tested — the listed behaviour goes red when the guard is removed.

  1. A missing expiresAt polled forever. Date.parse(undefined) is NaN, and the loop guard had been written to tolerate that, so every comparison was false and the only exits were a return or a throw. Driving it with a start response lacking the field polled 5000+ times. It now fails loudly, as deviceGrant in auth.ts already does for the same field.
  2. An interval Node cannot represent fires in ~1ms. 1e12 overflows to a 1ms timer and a negative one is clamped to 1ms — both the hot poll the guard existed to prevent, and neither was caught by a finite-and-positive check. Both ends are clamped now (1s..60s), and a non-positive interval is read as absent (RFC 8628's 5s).
  3. A negative slowDownBy drove the interval down, same 1ms outcome.
  4. The "no identity" test matched any Error at any status. A 502 whose prose happened to contain "no longer accepted" would have sent the person through a 15-minute authorize loop that cannot fix it. Gated on ApiError 400 now. (A machine-readable code from the platform would be better than matching another service's prose — worth doing when something else touches that route.)
  5. Number.isFinite(Number(x)) accepted null, '', 0 and false as an installation id, sending installationId: 0 to the connect; and the repo id was unvalidated. Both are whole-positive now, with a distinct message, because "listed but unbuildable" and "not in your list" have different remedies.
  6. The prompt went to stdout, so --json emitted three human lines before the document.
  7. A 429 or a dropped link ended the flow. The login flow's identical 5s cadence tripped the platform's per-IP limiter on prod on 2026-09-10; this one backs off instead of aborting an authorization the person may be one click from finishing.
  8. A confirmed authorization carrying no repos was coerced to [], which surfaced as "the App reaches none of your repositories" right after the person authorized. It fails loudly now.

Agent mode

Agent sessions reach the three org-scoped GitHub routes through InsForge/insta-platform#423 (merged 8f9afe1, deployed). On a backend without it they get unclassified_agent_action, and the CLI says so and names what an agent cannot fix — the console, or --public.

One behaviour change worth knowing

An org member running connect-repo now gets "connecting a repository needs the org admin role" instead of the platform's bare forbidden. They could never complete a connect — all three connect doors have always been admin — but the listing they used to get through was member-gated, so the refusal arrives earlier and now says what to do.

Gate

tsc --noEmit clean, 57 files / 869 tests. The lookup's own tests cover: case-insensitive match, unreachable repo, nothing reachable, unusable installation id, unlinked → authorize → use what came back, dead authorization → same, the same words at a different status → real failure, and the member's 403.


Summary by cubic

insta compute connect-repo now finds repos through the caller's own GitHub account instead of listing every org installation, which the platform made admin-only. If there's no GitHub identity yet, it runs a device authorization flow (prints a URL and code, polls until confirmed) and continues with whatever repos come back. Closes #205.

Device flow

  • Polls with backoff on rate limits and dropped connections instead of aborting.
  • Fails loudly on malformed responses (missing expiry, no repos) rather than looping or coercing to empty.
  • Prompt goes to stderr so --json output stays a single parseable document.
  • Runs only where someone can read the code — an interactive terminal or agent mode; --json and bare pipes fail fast instead of waiting.

Behavior change

  • Only two 403s get rewritten — members are told connecting needs the org admin role, and agents refused by policy get directed to the console; any other 403 is rethrown as the platform sent it.
  • Restores the repoLine, watchPathsClause, and computeWatchPaths test coverage the first pass accidentally deleted.

Written for commit 7135b79. Summary will update on new commits.

Review in cubic

The lookup walked every installation the org holds through an org-scoped route the
platform has since made admin-only and caller-scoped. It now asks for the caller's
own repositories, and when the terminal has no GitHub authorization yet it prints a
URL and a code, polls, and carries on with what comes back.

Closes #205

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@agent-zhang-beihai agent-zhang-beihai 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.

Reviewed by Yang Dong

This correctly switches repository selection to caller-scoped GitHub authorization, but agent-mode connections cannot reach any of the new POST endpoints. It also removes unrelated regression coverage, so I would not merge it as it stands.

Agent mode is denied before the caller-scoped repository lookup runs

important · defect · correctness · src/commands/github.ts:149

The change depends on the new org-scoped POST endpoints being callable by agents, but agentHeaders treats /orgs/... as account-level and signs with a projectless bootstrap session. The platform rejects every unclassified agent POST without a project before its GitHub handler runs, so insta --agent compute connect-repo now consistently fails for non-public repositories; the catch at line 151 then misreports that policy rejection as a missing org-admin role. These routes need an agent-governed, project-scoped contract, with the CLI passing the linked project through all three requests.

Evidence

read-the-codesrc/commands/github.ts:103-153, src/agent.ts:73-93, platform/src/server.ts:4377-4450, platform/src/govern/agent-routes.ts:252-294

The new authorization tests replace unrelated source and watch-path regression tests

important · judgement · regression · test/github-connect.test.ts:4

The change deletes all existing tests for repoLine, watchPathsClause, and computeWatchPaths, even though those behaviors are unrelated and the now-unused imports remain. Restore those three describe blocks unchanged alongside the new authorization tests so this feature does not remove coverage for source reporting and destructive watch-path validation.

Evidence

read-the-codetest/github-connect.test.ts:1-175, test/github-connect.test.ts@924c83d:83-128

@agent-zhang-beihai agent-zhang-beihai 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.

Reviewed by Wang Miao

connect-repo now lists repositories through the caller's own GitHub identity (POST /orgs/:id/github/repos) and, when that identity is missing or dead, runs the platform-mediated device flow in the terminal before retrying. The mechanism matches the platform side exactly — the two 400 messages it keys off, the slowDownBy relay, the installationId on each row all line up with insta-platform main, and the routes are live in prod — but the new interactive wait has no TTY/agent-mode gate, and the change deletes three test blocks for code it does not touch, so I would not merge it as it stands.

The device wait has no TTY or agent-mode gate, so a headless connect-repo now stalls for ten minutes instead of failing

important · defect · regression · src/commands/github.ts:153

When the caller has no usable GitHub identity the platform answers 400 your github account is not linked …, and findCallerRepo unconditionally enters authorizeTerminal, which prints a URL and a code to stderr and then polls until expiresAt — the platform sets that to Date.now() + SETUP_STATE_TTL_MS, i.e. 10 minutes (insta-platform src/github/service.ts:31,174). Under insta --agent, under --json, or on any non-TTY caller there is nobody to type the code, so the command blocks for ten minutes and then exits with "run the command again" — a loop. Before this change the same caller failed immediately with an actionable line pointing at the console. This repository already has the shape for it: requireProject refuses to prompt in agent mode and dies with instructions instead (src/api.ts:117); template deploy and setup gate their interactive paths on isTTY and fail with the exact non-interactive alternative (src/commands/template.ts:391, src/commands/setup.ts:404). Gate the authorize(...) call the same way and keep the old fast failure for everyone else.

Evidence

read-the-codesrc/commands/github.ts:103-136,145-160, src/api.ts:60-64,115-140, src/commands/template.ts:391, src/commands/setup.ts:404, src/resolve-service.ts:185; insta-platform src/github/service.ts:31,170-198,201-231, src/provisioning/access.ts:9-15

Three test blocks for untouched code were deleted along with the ones this change replaces

important · judgement · test-coverage · test/github-connect.test.ts:165

Replacing the findInstalledRepo block also removed describe('repo line'), describe('watchPathsClause') and describe('computeWatchPaths validation') (base test/github-connect.test.ts:83-129). None of that code is touched here: they pin repoLine's watch-paths rendering (including the "stored but cannot apply" line and the root-dir/repo-root distinction) and the --set-with---clear / empty---set refusals shipped days ago in a2a710d, and nothing else in test/ covers them — repoLine, watchPathsClause and computeWatchPaths are still imported at the top of the file but now have no assertion behind them, which is what gives the deletion away as accidental. Restore those three blocks verbatim from 924c83d:test/github-connect.test.ts.

Evidence

read-the-codetest/github-connect.test.ts:1-176 at HEAD and at 924c83d, rg for repoLine|watchPathsClause|computeWatchPaths across src/ and test/, git log -- test/github-connect.test.ts; suite run: 57 files / 869 tests pass, so nothing fails to flag it

@cubic-dev-ai cubic-dev-ai 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.

All reported issues were addressed across 3 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread test/github-connect.test.ts Outdated
Comment thread test/github-connect.test.ts
Comment thread src/commands/github.ts Outdated
…03s apart

The rewrite's replacement span swallowed the repoLine, watchPathsClause and
computeWatchPaths describes; the test total went UP, so nothing flagged it.
A policy refusal in agent mode is also no longer reported as a missing org role.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Fermionic-Lyu

Copy link
Copy Markdown
Member Author

Both findings were real. Fixed.

The deleted coverage

Confirmed by diffing the describes rather than the counts: repo line, watchPathsClause and computeWatchPaths validation were gone. My replacement span ran from findInstalledRepo to parseWatchPaths and swallowed everything between them. The test total went up (869), which is exactly why nothing flagged it — a count cannot see a deletion that a larger addition covers.

Restored verbatim from origin/main. The file now carries all of: parseRepoRef, pickCandidate, sourceBody, authorizeTerminal, findCallerRepo, repo line, watchPathsClause, computeWatchPaths validation, parseWatchPaths — 876 tests.

Agent mode

Also real, and it is a regression this PR would have shipped: orgs is in the CLI's ACCOUNT_ROUTES, so the new POSTs are signed with a projectless bootstrap session, and the platform's !projectId branch lets GETs through on account RBAC while denying every other method as unclassified_agent_action. The old lookup was two GETs; the new one is a POST. So insta --agent compute connect-repo would have failed for every private repository.

The fix belongs on the platform, not here — these routes genuinely name no project: InsForge/insta-platform#423 carves them out of that branch with an org-admin check, the same shape as the billing carve-out but without its every-project full_access requirement, because listing your own repositories spends nothing. Its test pins that a non-listed org POST stays denied.

This PR should merge after #423, or agent mode is briefly broken between the two.

Here I only fixed the consequence you named: the CLI reported that policy refusal as "needs the org admin role", which sends the person to an admin who cannot help. The two 403s are now told apart, with a test.

…leak

A third kind of 403 was reported as a missing org-admin role, which sends the
person to an admin who cannot help. The stderr spy was also restored after the
assertions, so a failing one leaked it into the next test.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Fermionic-Lyu

Copy link
Copy Markdown
Member Author

Both cubic P3s taken.

The 403 branch now rewrites only the two refusals it can name — unclassified_agent_action and requires admin — and rethrows anything else as the platform put it. Guessing "you need the org admin role" at a refusal we did not recognise sends the person to an admin who cannot help. Pinned with a test that a bare forbidden comes through unchanged.

The stderr spy is restored in afterEach rather than after the assertions. There is no restoreMocks in the vitest config, so a failing assertion left the spy installed and the next drive() re-spied it — a cascade whose first failure is the only real one.

877 tests, typecheck clean.

Nothing gated it, so a headless connect-repo printed a URL nobody would see and
polled for ten minutes where it used to fail immediately with something to act on.
Agent mode still waits — an agent relays the URL to the person driving it — but
--json and a bare pipe do not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Fermionic-Lyu

Copy link
Copy Markdown
Member Author

Both of Wang Miao's findings are fixed — I had not read this review when I answered the first round, and the second one was already restored by then.

The device wait had no gate

Real, and a regression: findCallerRepo entered authorizeTerminal unconditionally, which printed a URL nobody could see and polled until the platform's 10-minute expiresAt, where the old code failed immediately with a line pointing at the console.

The gate is "is there anyone who can read the code", not isTTY, and the difference matters: this CLI is meant to be driven by an agent that relays the URL to the person watching it — that was the whole point of choosing the device flow. So:

  • --json → never waits: a JSON consumer cannot read a prompt;
  • agent mode → waits, and prints to stderr so the document stays parseable;
  • otherwise → waits only when stderr is a TTY.

A caller with no reader gets the actionable line back: run it from a terminal, connect from the console, or pass --public. Mutation-tested both ways — deleting the gate and making the predicate always-true each redden a test.

The deleted test blocks

Already restored verbatim from 924c83d in 12db103, before this review arrived. Your read of why it went unnoticed matches mine: the suite still passed 869, because I added more tests than I deleted, so the total moved the wrong way to notice.

880 tests, typecheck clean.

Merge order: InsForge/insta-platform#423 is merged (8f9afe1) but not yet deployed — until it is, insta --agent compute connect-repo on this build gets unclassified_agent_action for a private repo. Nothing publishes from a merge here (a release is a separate tag), so merging this is safe; the deploy gates the release, not the merge.

@agent-zhang-beihai agent-zhang-beihai 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.

Reviewed by Wang Miao

Round 2 review.

connect-repo now resolves the repository through the caller's own GitHub identity (POST /orgs/:orgId/github/repos) and, when that identity is missing or dead, runs the platform-mediated device flow — gated, since round 1, on there being someone who can read the code. Both findings from last round are genuinely fixed: the non-interactive stall is closed by canAuthorizeHere (--json and a bare pipe fail fast again), and the three deleted test blocks are back verbatim. What blocks it now is the switch from GETs to a POST on an org route: that route is unclassified for agent sessions, so insta --agent compute connect-repo of a private repo goes from working to a hard 403.

Moving the lookup to POST /orgs/:orgId/github/repos makes every agent-mode connect of a private repo fail

important · defect · regression · src/commands/github.ts:158

In agent mode the CLI signs this request as an agent (/orgs/... is an ACCOUNT_ROUTES prefix, src/agent.ts:78), and the platform's governance pre-handler reaches a POST with an orgId, no projectId, an operationId (listGitHubReposForCaller) that is not in GOVERNED_OPERATIONS and not one of the two createCheckout/createBillingPortal exceptions — so it throws ForbiddenError('unclassified_agent_action') before the handler runs (insta-platform src/govern/agent-routes.ts:255-289). The old lookup was two GETs, and the same pre-handler returns early for GET with no project (agent-routes.ts:274), so an agent could complete the connect; the rest of the flow (detectGitHubServices, the PUT …/source) is classified and still works. The new error text names the symptom — "this backend does not let an agent authorize GitHub yet" — but the remedy it offers (the console, or --public) is not one an agent can take, and canAuthorizeHere returning true for agentMode() never comes into play because the 403 lands one step earlier. The description calls this "agents refused by policy", which reads as a policy-dependent case; it is unconditional for every agent session. Land listGitHubReposForCaller in the platform's governed-operations table first (resolvePublicGitHubRepo and detectGitHubServices sit there already, agent-routes.ts:74), or keep the installation-scoped GET path as the agent-mode lookup until it is.

Evidence

read-the-codesrc/commands/github.ts:145-175,204, src/agent.ts:78-103, src/api.ts:60-64; insta-platform src/govern/agent-routes.ts:22-116,255-295, src/github/service.ts:100-103,201-231,327-338, src/server.ts:4368-4450; prod GET /openapi.json (both the old installation routes and the new org routes are live); npm run typecheck clean, 57 files / 880 tests pass

@agent-zhang-beihai agent-zhang-beihai 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.

Reviewed by Yang Dong

This correctly switches repository discovery to the caller-scoped API and adds a bounded device-authorization flow for interactive use. The code path looks sound, but the required agent-facing command reference remains untouched, so I would not merge it yet.

The changed authorization contract is absent from the canonical agent command reference

important · judgement · conventions · src/index.ts:267

This change had to touch the existing documentation gap and left it wrong: connect-repo now discovers only caller-reachable repositories and may initiate device authorization, while the mandated reference still contains no connect-repo entry at all. Add the command, its flags, caller-scoped access rule, and terminal authorization behavior to InsForge/insta-skills/insta/cli-reference.md in the same change set, as required by the repository’s documented convention.

Evidence

read-the-codesrc/index.ts:267-275, CONTRIBUTING.md:36-44, InsForge/insta-skills/insta/cli-reference.md:1-338

@Fermionic-Lyu

Copy link
Copy Markdown
Member Author

The agent 403 — the premise is stale, not the reasoning

The carve-out you say is missing landed 5 minutes before that review: InsForge/insta-platform#423, merged as 8f9afe1 at 18:51 UTC. On origin/main it is src/govern/agent-routes.ts:287:

if (p.orgId && ['listGitHubReposForCaller', 'githubStartDeviceAuthorization', 'githubPollDeviceAuthorization'].includes(operation ?? '')) {
  await authorizeOrg(service.db, state.actor.userId, p.orgId, 'admin')
  state.governed = true
  return
}

Your evidence cites agent-routes.ts:255-295 with only the two billing exceptions, which is the shape before that merge — the pinned platform/ submodule is the usual source of that. Everything else in the finding is exactly right, which is why #423 exists at all: it was opened because of your round-1 review of this PR, with the same reasoning and the same fix you name (put these operations in the governed table rather than keep the GET path).

It is deployed as of this comment — deploy-prod run 34637153251, rolling, main at 8f9afe1.

So the error text stays: unclassified_agent_action is now only reachable on a backend that has not taken #423 (a self-hosted or not-yet-deployed one), which is precisely when "connect from the console or use --public" is the remedy. I have reworded the PR description, which did read as policy-dependent.

The command reference

Taken, with one correction: connect-repo is not absent from insta/cli-reference.md — it has a full entry at line 86 (and is named from deploy's and the cookbook's). What was wrong is that the entry described the old contract: "the repo must be reachable through the org's GitHub App installation".

InsForge/instacloud-skills#87 fixes it in the same change set: whose access decides, that a terminal may be asked to authorize once (stderr, ≤10 min), and the cases where it does not wait — --json, or no TTY and no agent session — where it fails naming the alternatives instead of stalling.

@agent-zhang-beihai agent-zhang-beihai 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.

Reviewed by Wang Miao

connect-repo now resolves the repository through the caller's own GitHub identity and earns one with a device flow when the platform says there is none; both of my earlier findings are genuinely fixed — canAuthorizeHere restores the fast failure for --json and bare pipes, and the three accidentally deleted test blocks are back verbatim (typecheck clean, 57 files / 880 tests pass). What still blocks it is the same thing as last round, and looking again it has a second face: the old lookup's two GETs were mirrored in two places outside this repo, and the new POST /orgs/:orgId/github/repos is mirrored in neither — so agent-mode connects now 403, and insta-oss answers a bare 404 where it used to answer a documented 501.

The new lookup route is unclassified for agent sessions and unstubbed in insta-oss, so both callers regress

important · defect · regression · src/commands/github.ts:158

Unchanged from round 2 on the first outcome, and re-verified end to end this round.

In agent mode this request is signed as an agent (/orgs/… is an ACCOUNT_ROUTES prefix, src/agent.ts:78), and the platform's governance pre-handler sees a POST with an orgId, no projectId, and an operationId (listGitHubReposForCaller) that is in neither GOVERNED_OPERATIONS nor the createProject/createCheckout/createBillingPortal exemptions — so it throws ForbiddenError('unclassified_agent_action') before the handler runs (insta-platform src/govern/agent-routes.ts:261-289). The old lookup was GET /github/installations plus GET /github/installations/:id/repos, and that same pre-handler returns early for a GET with no project (agent-routes.ts:274); every other step of the connect is classified (detectGitHubServices project.read, setServiceSource deploy, agent-routes.ts:49,95), so insta --agent compute connect-repo <owner/repo> worked before this change and now cannot complete at all. canAuthorizeHere treating agentMode() as a reader never comes into play, because the 403 lands one step earlier. The description's "agents refused by policy" reads as policy-dependent; it is unconditional for every agent session.

The same omission has a quieter second outcome: insta-oss stubs both old GETs with a clean 501 ("GitHub repo connect is cloud-only…", src/server.ts:671-672, under a comment that says never a bare 404 the CLI would render as a mystery) and documents compute connect-repo as a 501 with a workaround in COMPATIBILITY.md:49. It does not stub the new POST, so against a local daemon the command now dies with not found from the catch-all handler (oss/src/server.ts:1086) instead of the documented guidance.

Both are fixed outside this repo — add listGitHubReposForCaller to the platform's governed-operations table next to the other GitHub entries, and add the route to the oss stub list — but the CLI is what makes them reachable, so it should not land first.

Evidence

read-the-codesrc/commands/github.ts:145-175,204, src/agent.ts:78-103, src/api.ts:60-64; insta-platform src/govern/agent-routes.ts:22-116,204-296, src/github/service.ts:100-103,170-231, src/server.ts:4175-4250,4368-4450; insta-oss src/server.ts:75-80,660-680,1084-1087, COMPATIBILITY.md:49; prod GET /openapi.json (old and new routes both live); npm run typecheck clean, 880 tests pass

@agent-zhang-beihai agent-zhang-beihai 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.

Reviewed by Yang Dong

This correctly narrows repository discovery to repos the caller can reach and adds terminal device authorization. However, the new discovery endpoint is denied for every agent request, regressing private-repository connections for the CLI’s agent users, so I would not merge it as-is.

Agent mode can no longer connect any private repository

important · defect · regression · src/commands/github.ts:158

connect-repo now starts with an org-scoped POST /orgs/:orgId/github/repos. Agent mode signs this with an account bootstrap session, but the platform rejects every non-project-scoped mutation as unclassified_agent_action before the repository handler runs. Consequently, even an already-authorized agent now fails before detection or connection, whereas the previous GET-based discovery path was agent-callable. The platform needs an agent-governed, project-scoped route for this listing and device flow, or the CLI must use an equivalent caller-scoped route that agents can call.

Evidence

read-the-codesrc/commands/github.ts:154-171, src/commands/github.ts:197-205, src/agent.ts:76-101, src/index.ts:61-72, base src/commands/github.ts:95-107; platform src/server.ts:4375-4451, platform src/govern/agent-routes.ts:252-294

@Fermionic-Lyu

Copy link
Copy Markdown
Member Author

The insta-oss 404 — real, and fixed

New this round and correct: the two GETs this PR replaces are stubbed in insta-oss with the cloud-only 501, and the three routes replacing them were not, so connect-repo against a local daemon died with the catch-all not found instead of the guidance COMPATIBILITY.md promises. InsForge/instacloud-oss#105 adds them to that block and to the route table that asserts every cloud-only route answers a clean 501.

The agent 403 — this is the third round for it, so here is the evidence in full

I am not going to re-argue it after this; if it comes back a fourth time it goes to the owner as a disagreement rather than another round.

$ git -C platform show origin/main:src/govern/agent-routes.ts | sed -n '287p'
      if (p.orgId && ['listGitHubReposForCaller', 'githubStartDeviceAuthorization', 'githubPollDeviceAuthorization'].includes(operation ?? '')) {

$ git -C platform log --oneline -1 -S"listGitHubReposForCaller', 'githubStartDeviceAuthorization" -- src/govern/agent-routes.ts
8f9afe1 fix(govern): an agent can earn its own GitHub authorization (#423)

$ gh api repos/InsForge/insta-platform/deployments --jq '.[0] | "\(.sha[0:7]) \(.created_at)"'
8f9afe1 2026-09-11T19:09:40Z

Production has been running that commit since 19:09:40Z. Both of this round's reviews were submitted at 19:24:51Z and 19:26:29Z — fifteen minutes later. The line ranges you each quote (261-289, 252-294) contain line 287 and yet enumerate only createProject / createCheckout / createBillingPortal, which is the branch as it stood before that merge; the platform/ submodule pin in a fresh clone is the usual way that happens, and it is worth checking before a cross-repo claim, because nothing in this repo can retire a finding whose premise is a stale snapshot of another one.

#423 exists because of Wang Miao's round-1 review of this PR, and it applies the fix you both name — put these operations in the governed path rather than keep the GETs. It was approved by both of you with no findings and merged five minutes before round 2.

The remaining true statement is the one the description now carries: on a backend that has not taken #423 — a self-hosted one, or one not yet deployed — an agent gets unclassified_agent_action, and the CLI says so and names what an agent cannot fix.

@jwfing jwfing left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

LGTM - approved.

@Fermionic-Lyu
Fermionic-Lyu merged commit eb5ddad into main Sep 11, 2026
3 checks passed
@Fermionic-Lyu
Fermionic-Lyu deleted the feat/github-device-connect branch September 11, 2026 20:44
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.

connect-repo: device-flow authorization, then pick from the caller's own repos

2 participants