Repository navigation
fix(cli): linear app-template prints the consent page as Linear's Redirect URI — print the AgentCore callback instead (#914) - #918
Conversation
ayushtr-aws
left a comment
There was a problem hiding this comment.
Verdict: Request changes (posted as a comment). The vault-path fix is right, and printing the callback on every consent is a real improvement. Two problems block it, though. The PR rests on "Linear never redirects to the consent page", which is false for one setup path that is still supported. And the new comments contradict each other.
Governance: #914 is approved and P1. The branch name is compliant and CI is green.
Blocking
-
On the Secrets-Manager path, the consent page is Linear's
redirect_uri.cli/src/commands/linear.ts:1110-1111:setupHostedUrl = vaultAccessToken ? undefined : consentPageUrl; setupRedirectUri = setupHostedUrl ?? CALLBACK_URL. The authorize URL and the token exchange both send this value.- A vault stack reaches this path in two ways:
- (a) The first-onboarding fallback when
upsertLinearCredentialProviderthrows andisVaultUnavailableErrorreturns true (:1009-1013). That covers anything other than Validation/Conflict/AccessDenied/Unauthorized: throttling, network errors, ResourceNotFound. - (b) The
--coderesume, which replayspending.redirectUri.
- (a) The first-onboarding fallback when
setupitself says so at:1155-1161: "That exact URL must be registered as a Redirect URI…".- The template now labels the consent page "nothing to paste" (
:359) and the field "Leave empty" (:334). An operator who follows it exactly then hitsInvalid redirect_urion the fallback. - The comments at
:297-303and:353-356("never Linear'sredirect_uri", "no flow uses") are wrong for this path. They will mislead whoever touches it next. - Fix: keep the consent page out of the main field. Change the label to something like
Identity vault consent page — add as a Redirect URI only if setup falls back to Secrets Manager (setup will say so), and narrow the comments to "on the vault path".
-
Contradictory comments in
renderLinearAppTemplate.:310-315("Loopback stays listed even when the vault is deployed… Linear requires at least one entry… dropping it would leave the field empty") describes the opposite of the code below it (:316-340). It looks left over from an earlier revision. Delete it.- Also fix
cli/test/commands/linear.test.ts:359-363. It says the callback and the loopback "both belong", but the test asserts the loopback is absent.
-
Is "Leave empty" actually possible? The only statement in the repo on this is the stale comment above, which claims Linear requires at least one Redirect URI to create the app. The description says a bare run "defers to setup". It doesn't say an app was actually created with this field empty. Please confirm on Linear's create-app form. If an entry is required, a first vault run can't finish the form, and the template needs a placeholder strategy (for example, the loopback marked "temporary, replace after setup").
Non-blocking
linear.ts:1092-1094: the stale user-facing text "confirm the Linear app lists both redirect URIs printed above". Only one is printed now.linear.ts:185-189: thehostedConsentUrlJSDoc still says it "is listed as a callback URL".- The
add-workspacetrap was removed (old:344-350). Vault-stack operators usingadd-workspacenow get no hint that they need the loopback until it fails at:1725. A one-line trap would cover it. linear.ts:245:getStackOutput(...).catch(() => null)now chooses between the three branches. With expired credentials or a wrong region, it silently renders the no-vault branch (register loopback, deploy the vault) on a stack that already has the vault. Consider rendering "could not read stack outputs" separately. The same applies tolookupLinearVaultCallbackUrlswallowing errors, which renders "Leave empty" for an already-onboarded--slug.cli/test/commands/linear.test.ts:164-169:redirectUriBlockpasses vacuously if'Redirect URIs'isn't found, becauseslice(-1, end)returns'', and several new tests make onlynot.toContainassertions. Addexpect(start).toBeGreaterThanOrEqual(0); expect(end).toBeGreaterThan(start).- Tests don't cover the
setup-side change at all: the callback printed on everyconsent-required(:1063-1068), the error at:1092, and the Secrets-Manager fallback'sredirect_urichoice. The last one would have caught blocker 1. - The commit body still says the field "renders the same two-URI list". That's stale, so squash with an updated message.
docs/guides/LINEAR_SETUP_GUIDE.md:73("paste exactly what the template prints") reads oddly now that the template says "Leave empty". One sentence on the empty-field first run would help.
Documentation
- No generated-mirror impact: no
docs/sources changed. - The guide is not contradicted, but it says nothing about the first-run empty field (see above).
Tests & CI
- All checks pass.
- Template-side coverage is good.
setup-side coverage is missing. - Bootstrap synth-coverage: not applicable (CLI only).
Review agents run
- Run: code-reviewer, comment-analyzer, pr-test-analyzer, silent-failure-hunter.
- Omitted:
- type-design-analyzer: no new types; the return type was only reformatted.
/security-review: no IAM, Cedar, network or secrets changes.
Human heuristics
- Proportionality: concern. The comment-to-code ratio in
renderLinearAppTemplate(:291-340) is very high, and it is internally inconsistent. Trim it to the one true explanation. - Coherence: concern. Inside one file, the template says the consent page is never a redirect URI, while
setup(:1155) says it must be registered. - Clarity: concern. "Nothing to paste" hides a real requirement on the fallback path, and swallowed lookup errors render the wrong branch with no signal (
:245). - Appropriateness: pass, with a caveat. The vault path was verified live, but the "Leave empty" claim about Linear's form needs checking against the real form.
9ac11dd to
968c3c9
Compare
`bgagent linear app-template` printed the hosted consent page as the only Linear
Redirect URI. On the vault path Linear redirects to the AgentCore provider callback
instead, so registering exactly what the template printed failed consent with
"Invalid redirect_uri parameter for the application".
Which URI Linear checks depends on the setup path:
- vault: the AgentCore provider callback, minted when setup first registers
the app, so it cannot exist on a first run;
- Secrets Manager on a vault stack (setup's fallback when AgentCore is
unavailable, and `setup --code` resuming one): the hosted consent page;
- no vault, or `add-workspace`: the CLI's loopback listener.
The Redirect URIs field now holds the vault-path URI, one line per state:
- provider exists -> the AgentCore callback
- vault, first run -> "Leave empty — setup prints the URI to add, then stops"
(Linear's create-app form accepts an empty field)
- no vault -> the loopback listener
The consent page is printed after the form as a conditional entry ("add it only
if setup falls back to Secrets Manager — setup says so"), and add-workspace's
loopback gets its own trap line when the field does not show it.
`setup` now prints the vault callback on every consent, not only on the run that
created the provider. `provider.created` was standing in for "this URI is
registered on the Linear app", which is true once per provider lifetime while the
real condition stays false until someone pastes the URI in.
Lookup failures are no longer swallowed. A credentials or region error reading the
stack used to render a vault stack as a no-vault one, and an unreadable provider
rendered "leave it empty" for an onboarded workspace. The template now warns, via
`probeLinearVaultCallbackUrl`, which tells "absent" from "unreadable".
`chooseSetupRedirect` is extracted from setup so a test can check that every
redirect_uri setup can send on a vault stack is one the template tells the
operator to register. That invariant is what drifted.
Refs #914
968c3c9 to
a30ffab
Compare
|
Thanks @ayushtr-aws — thorough and right on all three blockers. Addressed in Blocking
Non-blocking — all taken
Verification
|
ayushtr-aws
left a comment
There was a problem hiding this comment.
Verdict: Approve with nits
This fixes #914, an approved P1 bug, at the root cause. The template now prints the redirect_uri that each setup path actually sends. Setup reprints the vault callback on every consent, so it no longer relies on provider.created as a proxy. Lookup failures now show up as warnings instead of rendering as a no-vault stack. The change is scoped to the CLI and the docs mirror is in sync. The vision point (reviewable, unattended onboarding) is improved, and no tenets are traded away.
I traced each path against the template's claims, and they hold:
- First vault run: setup prints the callback and returns before consent.
- Later vault runs: setup prints the callback before the authorize URL.
- Secrets Manager fallback on a vault stack: setup warns that the consent page must be registered (about
linear.ts:1173, not:1156). --coderesume: setup reusespending.redirectUri.- No vault: setup uses the loopback.
ResourceNotFoundException is the documented not-found error for GetOauth2CredentialProvider. Nothing still calls lookupLinearVaultCallbackUrl.
Non-blocking (worth fixing here or in a follow-up)
- The
add-workspacetrap prints the wrong URI when--aws-callback-urlis set.cli/src/commands/linear.ts:391interpolatescallbackUrl(opts.awsCallbackUrl ?? loopback,:304), butadd-workspacealways sendsCALLBACK_URL(:1692). The old code hard-coded the loopback here. Fix: useCALLBACK_URL. - A vault callback without a consent page drops the loopback. This is
linear.ts:340-351and:390. Example: a stack redeployed without the vault but with a leftover AgentCore provider, run withapp-template --slug acme. A manual--vault-callback-urlwith no stack, or a mistyped--stack-name, does the same. In each case the field lists only the vault callback, and the loopback note and trap disappear.setupthen sends the loopback, which is not registered. Before this PR the loopback was printed wheneverhostedConsentUrlwas absent. Fix: put the callback in the field only whenhostedConsentUrlis also set, and otherwise fall through to the loopback branch. lookupWarningcontradicts URLs the user passed. With--vault-callback-url/--hosted-consent-urland expired credentials, the template says "may be wrong — check credentials" right above the URL the user typed (:331-336,:655-657). Fix: drop the failures for lookups the user overrode.- The headline behaviour has no test. Printing the callback on every consent (
:1068-1081) is untested, so deleting the block passes CI. Fix: extract a small pure helper or add one action test withcreated: falseand consent required. Also untested:--coderesume exchanging with the saved hosted URI. - The
chooseSetupRedirectinvariant test is partly tautological (linear.test.ts:244). It feeds the same literal to both functions by hand, covers only the vault-callback-present case, and depends onlines[at + 1]layout. Fix: loop over all(vaultAccessToken, consentPageUrl, vaultCallbackUrl)combinations and assert thatredirectUriappears in the field or in the conditional section. - A provider with no
callbackUrlis reported asabsent(linear-vault.ts:179). The template then says "Leave empty" for a provider that exists. Fix: map it tounreadable(errorName: 'MissingCallbackUrl'). Relatedly,if (provider.callbackUrl)at:1076skips the notice silently when upsert returns''.
Nits
- No region still renders no-vault without a warning.
linear.ts:249was deferred in #914, but the newfailureslist is the natural place to add "no region — stack/vault not checked" whenstackNameorslugis set. Also consider falling back toAWS_REGION. - The warning carries only
err.name. A DNS or endpoint failure from a wrong--regionreads as(Error). Append a truncated message. - "…prints the URI to add, then stops" (
:347) is inaccurate without--slugwhen a provider already exists. In that case setup prints the callback and then continues to consent. Suggest "prints the URI to add before consent". chooseSetupRedirectreturns a loopbackredirectUrion the vault leg, where it is never sent. A discriminated union (vault | hosted | loopback) would remove the duplicated fact, and it would also make the test atlinear.test.ts:258meaningful.- Comment hygiene:
- The JSDoc on
VaultCallbackProbedescribes the function, soprobeLinearVaultCallbackUrlhas no JSDoc of its own. - Drop the dated "verified on the live form, 2026-10" (
:343). - Trim the history narration (
:1066-1073, the second paragraph inlinear-vault.ts:152-164, "#914 review" / "blocker 1" in tests). - Write the #917 reference at
:261asTODO(#917).
- The JSDoc on
Docs
LINEAR_SETUP_GUIDE.md is updated to match the template output. The Starlight mirror is regenerated and in sync.
Tests & CI
The 3 touched suites (114 tests) pass locally. Semgrep silent-success-masking is clean on both touched files, so removing the nosemgrep is safe. CodeQL, secrets/deps, dead-code and the title check pass. build (agentcore) was still pending when I reviewed, so this approval assumes it goes green. Bootstrap synth-coverage does not apply (no CDK changes).
Review agents run
code-reviewersilent-failure-hunterpr-test-analyzertype-design-analyzercomment-analyzer
Omitted: /security-review. There are no IAM, Cedar, network, secrets or input-gateway changes; the redirect-URI handling was covered by code-reviewer.
Human heuristics
- Proportionality: pass. The fix is targeted, and the probe union is justified.
linear.tsis 2,924 lines, but that predates this PR. - Coherence: pass. Terms match the setup guide.
- Clarity: concern, minor. The history-narrating comments and
chooseSetupRedirect's duplicated return fields (above). - Appropriateness: pass. It was verified live against a vault stack. The test gaps in non-blocking items 4 and 5 are the main residual risk.
Closes #914.
The bug
bgagent linear app-templateprinted the hosted consent page as the only Linear Redirect URI. On the vault path Linear redirects to the AgentCore provider callback, so an operator who registered exactly what the template printed still failed consent withInvalid redirect_uri parameter for the application. Hit live on a three-workspace dev stack.Which URI Linear checks, per setup path
redirect_uriLinear validatessetupwith the Identity vaultsetupon a vault stack that falls back to Secrets Manager (AgentCore unavailable on first onboarding; alsosetup --coderesuming one)add-workspaceThe original template only handled the second row. An earlier revision of this PR overcorrected and claimed the consent page is never a redirect URI, which was wrong for that fallback. Thanks @ayushtr-aws for catching it.
What the template prints now
Leave empty — bgagent linear setup <slug> prints the URI to add, then stops. Linear's create-app form accepts an empty Redirect URIs field (confirmed on the live form).Identity vault consent page: <url>— "Add it as a Redirect URI only if setup falls back to Secrets Manager — setup says so when it does." (setupdoes say so,linear.tsaround:1156.)add-workspace's loopback when the field doesn't show it.setupprints the callback on every consentprovider.createdwas standing in for "this URI is registered on the Linear app". It is true once per provider lifetime, while the real condition stays false until someone pastes the URI. So anyone who interrupted the first run got no remedy afterwards.Lookup failures are surfaced, not swallowed
getStackOutput(...).catch(() => null)rendered a vault stack as no-vault on any credentials or region error. Separately, an unreadable provider rendered "leave it empty" for an onboarded workspace. Both now produce a⚠ Could not read …line.probeLinearVaultCallbackUrlreturnsfound | absent | unreadableand replaces the null-returning lookup.Tests
chooseSetupRedirectis extracted fromsetup. A test derives the fallback'sredirect_urifrom it and asserts the template tells the operator to register that URI — the invariant that drifted. Also covered:add-workspacetrapabsentvsunreadableredirectUriBlockno longer passes vacuously when the label is missingVerified
mise run buildpasses — 1,002 CLI / 1,800 agent / 4,781 CDK (222 suites) — pluscdk:synth:quietand eslint.--slug <onboarded>prints that workspace's real callback.Notes
listOnboardedWorkspaceSlugs. That's a natural follow-up once it merges.LINEAR_SETUP_GUIDE.mdon the empty first-run field and the fallback. Mirror regenerated viamise //docs:sync.