Skip to content

feat(connect): add volcano connect for one-step onboarding - #317

Open
tkkhq wants to merge 16 commits into
mainfrom
feat/connect-onboarding
Open

tkkhq wants to merge 16 commits into
mainfrom
feat/connect-onboarding

Conversation

@tkkhq

@tkkhq tkkhq commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Tracking

Why

  • onboarding sends users in several directions that never meet: they can start in the CLI, but sign-in comes later, at the first cloud deploy, and the steps to a linked project need a browser trip and several commands
  • a signed-in dashboard user should be able to paste one prompt into their coding agent and end up with a cloud project, a local directory set up for it, and a link back to the project's dashboard

Summary

  • add volcano connect [directory]: signs in (reuses a valid login, --token, browser device login, or --signup), picks the project, links the directory, runs the base scaffold if volcano/ is missing (or --template), writes the API URL and default anon key to volcano/cloud.env, gitignores the link and volcano/*.env, adds a marked Volcano section to AGENTS.md, installs skills, and prints Project:, Directory: and Dashboard: lines; --json supported
  • with no directory argument, connect creates the project first (--name, or a generated {volcano}-{color}-{NN} name using the dashboard's word lists) and sets it up in a new ./<name> directory; with a directory argument the project is named after the directory; --project <project-id> uses an existing project; run inside a linked directory it keeps the linked project
  • when the name is taken by a project (409) or, without a directory argument, by an existing directory, connect tries name-2, name-3, and so on (up to -20)
  • at the plan's project limit, connect fails and lists each existing project with its volcano connect --project <id> command
  • exchange one-time vbt_ bootstrap tokens: --token vbt_… on connect or login calls POST /auth/cli-bootstrap-tokens/exchange (volcano-hosting#1924) and saves only the returned CLI login, with the same lifetime as a volcano login token; the vbt_ token is never written to disk
  • when the bootstrap token is expired, already used, invalid, rate limited, or the API has no exchange endpoint, connect says which and falls back to browser login, not the saved login, which may be another account
  • when a command needs a login and none is saved, offer sign-in on a terminal (asks whether the user has an account, then runs browser login or signup, then continues the command); without a terminal or in CI it exits 2 as before, and the error now names volcano signup too
  • the no-project error now points at volcano connect . to create a project and link the current directory
  • connect installs skills for every detected coding agent, as volcano setup --yes does; volcano setup itself is unchanged from main
  • add per-directory project links in .volcano/project.json; project precedence is now VOLCANO_PROJECT_ID > directory link > the project volcano use saved; volcano use inside a linked directory updates the link, and projects rename keeps the link's name in step
  • the link is gitignored because each developer usually has their own project; the AGENTS.md section names no project for the same reason
  • save the API URL with the login (api_url in ~/.volcano/config.json) and use it when VOLCANO_API_URL is unset, so a staging login no longer sends its token to production; it is ignored when VOLCANO_TOKEN supplies the token
  • on a 401 from a non-default API, name that API in the error; login/signup print the API they signed in to when it is not the default
  • add stable exit codes for every command: 2 not logged in or HTTP 401, 3 no project, 4 HTTP 404 or unknown project, 5 HTTP 403 or account token required; sandboxes exec/shell still pass the guest's exit code through. This changes exit codes that were 1 before
  • add --json to projects list and projects get
  • parse the API's code field from raw error bodies in internal/api/errors.go, so hand-written calls can branch on it
  • add docs/connect.md and docs/scripting.md, and update docs/authentication.md and docs/README.md; vbt_ tokens stay out of docs/ until volcano-hosting#1924 ships
  • merge order: volcano-hosting#1924 first, then this PR and a CLI release, then volcano-web#904, whose prompt runs volcano connect --token
  • a missing project only gets a better error, not an inline create: volcano connect . also writes the env file, AGENTS.md and the scaffold, which a bare create would skip
  • saved logins are not renewed; like a volcano login token, the exchanged token lasts the API's platform_token_ttl (30 days by default), then the user signs in again
  • defer global --json on every command; this PR covers only what connect and the projects commands need
  • defer the skills repo fix: volcano-platform still says volcano cloud migrations deploy, which doesn't exist (it is cloud databases migration up, or cloud deploy)
  • defer the cloud databases create --pg-version papercut

Verification

  • make check (generated-code check, release workflow check, lint, installer tests, go test ./...)
  • node scripts/lint-docs.mjs <volcano-cli>/docs from a local Kong/volcano-docs checkout that contains the pinned commit
  • go run ./cmd/volcano connect --help
  • connect is covered by unit tests against a fake API: first run, rerun, project limit, name suffixes, generated names and directories, an existing directory's name skipped, a rerun from inside a linked directory, --project with and without a directory argument, --project rejecting a name, a bootstrap token exchanged, and a refused bootstrap token going to browser login instead of the saved login; git check-ignore confirms volcano/cloud.env and .volcano/project.json are ignored
  • bootstrap exchange tests cover each refusal code, the 429, a missing endpoint, no bearer header on the exchange, and a malformed success body never echoing the token
  • the terminal sign-in offer is tested only for its no-terminal path and the session hook; the interactive prompt needs a real terminal
  • not run end to end against staging or production: connect creates a real project, and the exchange endpoint isn't deployed yet

@tkkhq
tkkhq requested a review from a team as a code owner October 15, 2026 02:23
Copilot AI balanced review requested due to automatic review settings October 15, 2026 02:23

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions
github-actions Bot requested a review from swkeever October 15, 2026 02:39
@github-actions

github-actions Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

AI review: 8 P0-P2 finding(s)

Reviewed 0d8f9bb. Run log

Adds volcano connect, which signs in, creates or picks a project, links the directory through .volcano/project.json, scaffolds it and writes env, gitignore and AGENTS.md. The PR also adds vbt_ bootstrap-token exchange, saves the API URL with each login, offers an interactive sign-in when no login is saved, introduces stable exit codes 2–5 and adds --json to projects list and projects get. The code is careful and well tested. The main risks are contract and trust-boundary changes rather than defects: exit codes that used to be 1 change, a new credential exchange is added, and saved tokens are now pinned to the API they logged in to. I found one wrong user-facing doc line and a few low-severity edge cases.

🟠 P1

  • internal/cmd/connect/connect.go:159 Prevent VOLCANO_TOKEN from overriding connect's explicit login
  • internal/auth/auth.go:332 Apply login --project to an existing directory link

🟡 P2

  • docs/README.md:110 README says volcano connect links this directory, but with no argument it creates a new project in a new subdirectory
  • internal/cmd/connect/files.go:121 Use connect . to restore a checkout's missing link
  • cmd/volcano/main.go:96 Preserve command error categories for the advertised exit codes
  • internal/cmd/connect/connect.go:589 Search all anon-key pages before choosing a fallback
  • internal/cmd/connect/connect.go:449 Fail when directory-name candidates are exhausted
  • internal/cmd/connect/files.go:36 Replace every existing definition of an env key

⚪ P3

  • internal/auth/auth.go:73 vbt_ tokens are sent to the saved session API, which may not be the API that minted them
  • internal/cmd/connect/connect.go:337 Directory argument is created before the project exists, so a failed create leaves an empty directory
  • internal/config/config.go:136 A malformed or unreadable .volcano/project.json anywhere up the tree breaks every command, including login and connect
  • internal/auth/auth.go:169 Project-token login inside a linked directory uses the link's project but names it as the volcano use selection

Not checked: The AI review read the diff, the code, and the PR's text; it ran no tests or builds and read nothing outside this repository.

  • Whether volcano-hosting#1924's exchange endpoint returns the response and refusal shapes the CLI parses (status codes, an error/message field alongside code, and the token/user_id fields).
  • The interactive sign-in prompt and the browser device-login fallback after a refused bootstrap token were not exercised on a real terminal.
  • Whether external consumers (e.g. Kong/volcano-actions, the volcano-web prompt, skills) depend on exit code 1 or on the old ErrNotAuthenticated/ErrNoProjectSelected message text.
  • Verify the bootstrap exchange schema, refusal codes, and rollout compatibility against Kong/volcano-hosting#1924.
  • Verify the interactive sign-in prompt and command continuation in a real terminal.

Push a fix, or have a collaborator with write access (including the PR author) add the ai-review-override label to accept 0d8f9bb as is. An override also needs a human review.

Human review: needed

  • Can scripts, CI and integrations tolerate auth, no-project, 404 and 403 failures now exiting 2–5 instead of 1 for every command (cmd/volcano/main.go exitCode)?
  • Is the new vbt_ bootstrap-token exchange acceptable as a credential path, with the returned login saved like a normal CLI token (internal/api/bootstrap.go, internal/auth/auth.go)?
  • Should saved tokens now go to the api_url recorded at login, including for later login/signup/connect, instead of the compiled default (internal/config/config.go APIURL)?
  • Is it OK that any command run on a terminal without a login now prompts for browser login or signup instead of failing (internal/session/session.go, internal/auth/signin.go)?
  • Should dashboard bootstrap tokens grant persistent account-wide CLI credentials through the exchange and browser-fallback flows in internal/auth/auth.go and internal/cmd/connect/connect.go?

Decide the points above: approve to accept them, or request changes.

Suggested reviewer: swkeever (most commits to internal/api client/errors, auth tests and cmd/volcano/main.go). Any collaborator with write access other than the author can approve.

if err := os.MkdirAll(dir, dirMode); err != nil {
return fmt.Errorf("failed to create %s: %w", dir, err)
}
starter, err := initcmd.ResolveStarter(opts.template, "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: An unknown --template is caught only after sign-in and project creation, and then only as a warning

initcmd.ResolveStarter doesn't check that the starter exists (in volcano init, the 'unknown starter' error comes later from projectinit). So volcano connect --template react (or any typo) signs in, creates a real cloud project, links it, and writes the env file. Only then does scaffold() fail with unknown starter, which line 473 turns into Warning: Skipped the scaffold. The command exits 0 and --json reports success, so an agent won't notice. The user ends up with an extra project, which counts against the plan's limit, and no starter. Fix: check the starter up front, before ensureSession (for example, export a projectinit check that runs validStarterName and starterExists), and fail on an unknown template.

Found by claude-opus-5-5

Comment thread internal/config/config.go
// A working directory that no longer exists can't be linked.
return cfg, nil //nolint:nilerr // the link is optional; the config itself loaded
}
if cfg.Link, err = FindLink(wd); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: A malformed .volcano/project.json in the directory or any parent breaks every command that loads config, including login and connect

Load now returns an error when FindLink can't read or parse a link file. A link with merge-conflict markers (the docs suggest committing the link to share it), an empty {}, or a directory at that path makes volcano login, volcano connect, and projects list fail with failed to load config: failed to parse project link .... Even connect, which would overwrite the link anyway, can't repair it. In local mode, withLocalConfig also quietly loses the user's function aliases, because it ignores the Load error. The message names the file, so users can recover. Still, it would be better for commands that don't need a project (login, connect, account-scoped commands) to warn and ignore a bad link instead of failing.

Found by claude-opus-5-5

Comment thread internal/auth/auth.go
return config.Update(func(cfg *config.Config) (bool, error) {
cfg.UserToken = credentials.Token
cfg.UserID = credentials.UserID
cfg.SessionAPIURL = credentials.APIURL

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: api_url is saved even when it equals the compiled default, which pins every new login to today's default

SaveCredentials always writes cfg.SessionAPIURL = credentials.APIURL, including https://api.volcano.dev. Config.APIURL() ranks the saved URL above compiledDefaultAPIURL. So if a later release ships a different compiled default, or someone runs a make local build with its own DEFAULT_API_URL, the new default is silently ignored until the user logs out. The stated goal is only to remember a non-default API. Saving api_url only when it differs from DefaultAPIURL() (or clearing it otherwise) keeps that goal without the pinning.

Found by claude-opus-5-5

Comment thread internal/cmd/connect/connect.go Outdated
}

func run(ctx context.Context, opts options) error {
dir, err := filepath.Abs(opts.dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Running connect from the home directory links and scaffolds the whole home directory

Onboarding means pasting npx -y @​volcano.dev/cli connect into a terminal, and a fresh terminal often starts in ~. Run there, connect creates a project named after the user's account directory and writes ~/.volcano/project.json (next to the global config), ~/volcano/, ~/.gitignore, and ~/AGENTS.md. FindLink reads the home directory's link before it stops walking, so that link then applies to every non-git directory under home. Consider refusing to connect $HOME (or asking first on a terminal), and telling the user to cd into a project directory.

Found by claude-opus-5-5

Comment thread internal/cmd/auth/auth.go
output.Success(opts.out, "Logged in successfully")
printNonDefaultAPI(opts.out, credentials)
if credentials.Project != nil {
output.Success(opts.out, "Now using project: %s (%s)", credentials.Project.Name, credentials.Project.ID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: login --project says 'Now using project' even inside a linked directory, where the link overrides it

SaveCredentials saves the login project as the global current_project, but in a linked directory Config.Project() now prefers the link. So volcano login --token pt-... --project B, run where the link names project A, prints Now using project: B, while later commands there still target A. With a project token, those commands then fail with 403. volcano use was updated to write the link in this case; login wasn't. Either update the link the same way, or mention that the directory's link takes precedence.

Found by claude-opus-5-5

Comment thread docs/scripting.md
- `volcano connect`
- `volcano cloud access-tokens …`
- `volcano cloud domains list` and `volcano cloud domains verify`
- `volcano durable approvals …`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: The scripting doc claims --json for whole command groups that only partly support it

The list includes volcano projects keys service ... and volcano cloud access-tokens .... But only projects keys service create has --json (service_keys.go), and cloud access-tokens revoke has none. A script that copies these and runs, say, volcano cloud access-tokens revoke <id> --json fails with unknown flag: --json (exit 1). Name the subcommands that actually support it.

Found by claude-opus-5-5

Comment thread internal/auth/auth.go
cfg.UserID = credentials.UserID
cfg.SessionAPIURL = credentials.APIURL
if credentials.Project != nil {
cfg.CurrentProject = credentials.Project

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Honor explicit login project selection inside linked directories

In a directory linked to project A, volcano login --token pt-B --project B validates B and reports that B is selected, but this saves only CurrentProject. Config.Project() still prioritizes the link to A, so subsequent cloud commands address A and fail with 403. An account token can instead operate on A despite the explicit selection of B. Make explicit login selection respect the directory link, as volcano use now does.

Found by gpt-6.1-sol

if err := ensureSession(ctx, opts, sessions); err != nil {
return err
}
authenticated, err := sessions.Authenticated()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Use the supplied token throughout the connect workflow

When VOLCANO_TOKEN contains account A's token and --token supplies account B's token, ensureSession validates and saves B, but this reload uses Config.Token(), which selects A's environment token. Connect then creates its project under A or fails accessing --project from B after reporting successful sign-in. Use the explicit token for the entire operation, or reject conflicting credential inputs before proceeding.

Found by gpt-6.1-sol

Comment thread cmd/volcano/main.go
func exitCode(err error) int {
status := api.Status(err)
switch {
case errors.Is(err, config.ErrNotAuthenticated), status == http.StatusUnauthorized:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Preserve error categories from real commands for stable exit codes

auth.LoginWithToken and projectTokenValidationError replace HTTP 401 errors with plain errors.New("invalid token"), so rejected login --token and connect --token calls still exit 1 instead of 2. Similarly, project.ListAnonKeys and selectedProject return plain missing-project errors, making key commands exit 1 instead of 3. Preserve the API errors or wrap the corresponding sentinels so these commands satisfy the new documented contract.

Found by gpt-6.1-sol

output.Warning(log, "Skipped the scaffold: %s. Run `volcano init` to see the conflicts.", strings.Join(conflicts, "; "))
return
}
output.Warning(log, "Skipped the scaffold: %v", err)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Reject invalid templates before creating a cloud project

ResolveStarter normalizes names without checking that a starter exists. Consequently, connect --template nextjss creates and links a cloud project, then turns the unknown-starter error into a warning and exits successfully without the requested scaffold. Validate the template before remote mutations and propagate invalid-template errors instead of reporting a successful connection.

Found by gpt-6.1-sol

// projectAnonKey returns the project's default anon key, the publishable key
// the SDK and frontends use. It creates one only when the project has none.
func projectAnonKey(ctx context.Context, client *api.Client, projectID uuid.UUID) (string, error) {
keys, err := client.ListAnonKeys(ctx, projectID)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Search subsequent anon-key pages before choosing a fallback

api.ListAnonKeys requests only the default page and discards pagination metadata; the vendored API contract defaults to 10 keys. When an existing project's default key is on a later page, this function writes an arbitrary first-page key into cloud.env, potentially giving the application different permissions. Fetch subsequent pages until the default key is found before using a fallback.

Found by gpt-6.1-sol


// projectDashboardURL is the project's overview page in the dashboard.
func projectDashboardURL(webURL string, projectID uuid.UUID) string {
return strings.TrimRight(webURL, "/") + "/dashboard/overview?" + url.Values{"project": []string{projectID.String()}}.Encode()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Coordinate project dashboard links with the dashboard consumer

The PR description states that volcano-web's /dashboard/overview does not yet select projects from ?project=. If the browser last selected A, connecting B prints a supposedly project-specific link that opens A instead. Deploy the matching dashboard support before relying on this URL, or use a route that already honors project selection.

Found by gpt-6.1-sol

defer server.Close()
chdirTemp(t)

out, err := runConnectHere(t, server, "--token", "vbt_one-time")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Exposed secret in internal/cmd/connect/connect_test.go - low severity
Detected a secret as command line option

Reply @AikidoSec ignore: [REASON] to ignore this issue.
More Info

Comment thread internal/auth/signin.go

// canOfferSignIn reports whether a person is there to answer: both ends are
// terminals and nothing marks the run as non-interactive.
func canOfferSignIn(in io.Reader, out io.Writer) bool {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Sign-in prompt can hang agents that run commands under a pseudo-terminal, and can't be declined

canOfferSignIn only checks that stdin and stderr are TTYs, plus CI and the new, undocumented VOLCANO_NONINTERACTIVE. Agents that run shell commands in a real PTY, such as Copilot agent mode or Cursor in the VS Code integrated terminal, pass that check. Because session.Factory.Authenticated() now calls deps.SignIn, a volcano cloud … command run there without a login stops at Do you already have one? [y/N]: and waits for input. Before this change it failed right away with the not-authenticated error.

The prompt also has no decline option: an empty answer starts browser signup. docs/authentication.md says that a coding agent gets exit code 2 instead, which isn't true for these agents.

Suggested fixes:

  • Skip the prompt when known agent markers are set. internal/setup already detects some of them.
  • Make the default answer fail with ErrNotAuthenticated instead of starting signup.
  • Document VOLCANO_NONINTERACTIVE.

Found by claude-opus-5-5

output.Note(opts.log, "%s is taken, so the new project is %s.", name, candidate)
}
return project, nil
case api.Status(err) == http.StatusConflict && suffix <= maxNameSuffix:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Name-suffix retry depends on a 409 that the API spec doesn't list for project create

createProject only tries name-2, name-3, … when POST /projects returns 409. The generated client in this repo (CreateProjectClientResponse) lists only 201, 400, 403 and 503, and the spec says names "must be unique". If the platform answers a duplicate name with 400, connect in a directory whose name matches an existing project fails instead of adding a suffix. The fake API in the tests returns 409, so the tests can't catch this. Confirm the status with volcano-hosting, or also match the 400 duplicate-name refusal.

Found by claude-opus-5-5

Comment thread internal/auth/auth.go
func (e *BootstrapRefusedError) Unwrap() error { return e.Err }

// exchangeBootstrapToken trades a vbt_ token for a normal CLI login.
func (s Service) exchangeBootstrapToken(ctx context.Context, apiURL, token string) (*api.BootstrapExchange, error) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: A vbt_ token is spent before --project is validated, so a failed login wastes it and leaves an unused token

LoginWithToken exchanges the one-time token first, then validates --project. If validation fails (wrong ID, 403 or 404), the exchanged CLI login is never saved, and the server keeps a live login named after this host that nobody holds. The vbt_ token is also used up, so rerunning volcano connect --token vbt_… --project <id> reports "already used" and falls back to browser login. Consider saving the exchanged credential before project validation, or validating the project in a separate step.

Found by claude-opus-5-5

Comment thread docs/README.md
volcano projects create my-app # create a project
volcano projects list # list your projects
volcano use my-app # set the active project
volcano connect # link this directory to a project

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: README example says volcano connect links this directory, but with no argument it creates a new one

With no directory argument, connect creates a new project and a new subdirectory named after it (see resolveTarget and docs/connect.md). The comment # link this directory to a project describes volcano connect .. Change the example to volcano connect ., or change the comment.

Found by claude-opus-5-5

Comment thread docs/scripting.md
- `volcano cloud domains list` and `volcano cloud domains verify`
- `volcano durable approvals …`
- `volcano functions invoke`
- `volcano projects keys service …`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Lists volcano projects keys service … as supporting --json, but only create does

In internal/cmd/project/service_keys.go, only the create subcommand registers --json. list and get don't, so a script following this page gets an unknown-flag error. Narrow the entry to volcano projects keys service create.

Found by claude-opus-5-5

Comment thread internal/auth/auth.go
cfg.UserID = credentials.UserID
cfg.SessionAPIURL = credentials.APIURL
if credentials.Project != nil {
cfg.CurrentProject = credentials.Project

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 P1: Update the directory link when login selects a project

Inside a directory linked to A, volcano login --token pk-... --project B saves B globally and prints “Now using project: B”, but Config.Project() still selects the link to A. Subsequent commands such as volcano cloud deploy therefore target A despite the successful explicit selection; a token scoped to B instead produces 403 errors. Update the active directory link when persisting the selected login project, as volcano use now does.

Found by gpt-6.1-sol

output.Note(opts.log, "%s is taken, so the new project is %s.", name, candidate)
}
return project, nil
case api.Status(err) == http.StatusConflict && suffix <= maxNameSuffix:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Handle duplicate project names returned as HTTP 400

The existing internal/project/project_test.go::TestCreateWrapsAPIError represents a duplicate-name refusal as HTTP 400 with name already exists, and the create endpoint's OpenAPI contract documents 400 rather than 409. With that response, connecting another directory named after an existing project fails immediately instead of trying name-2. Recognize the duplicate-name refusal specifically rather than requiring HTTP 409.

Found by gpt-6.1-sol

Comment thread cmd/volcano/main.go
switch {
case errors.Is(err, config.ErrNotAuthenticated), status == http.StatusUnauthorized:
return exitNotAuthenticated
case errors.Is(err, config.ErrNoProjectSelected), errors.Is(err, config.ErrNoProjectSelectedForProjectToken):

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Classify missing-project errors from project key commands

For an authenticated user without a selected project, volcano projects keys anon list and volcano projects keys service list return plain errors from project.Service.ListAnonKeys and selectedProject. Neither wraps the sentinels checked here, so these commands exit 1 instead of the documented 3. Have those paths preserve the appropriate missing-project sentinel.

Found by gpt-6.1-sol

}

func run(ctx context.Context, opts options) error {
starter, err := initcmd.ResolveStarter(opts.template, "")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Validate templates before creating a cloud project

ResolveStarter only normalizes names, so a typo such as --template nextjss passes this check. Connect then creates and links a real cloud project before RunStarterIn rejects the template; scaffold reduces that error to a warning and connect exits successfully without the requested scaffold. Validate starter availability before authentication or project creation and return an error for an unsupported template.

Found by gpt-6.1-sol


This directory is linked to a Volcano cloud project in .volcano/project.json
(gitignored). Volcano CLI commands run here use that project. If the file is
missing, run `+"`volcano connect`"+`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Use connect dot to recover a checkout's missing link

The link is intentionally gitignored, so a teammate cloning the repository normally reaches this instruction. Running bare volcano connect in that unlinked checkout creates a new child directory and scaffold, leaving the cloned application unlinked. Generate volcano connect . here; the newly added “link this directory” example in docs/README.md needs the same correction.

Found by gpt-6.1-sol

func createProject(ctx context.Context, opts options, client *api.Client, name string, taken func(string) bool) (*apiclient.Project, error) {
candidate := name
for suffix := 2; ; suffix++ {
if taken != nil && taken(candidate) && suffix <= maxNameSuffix {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Stop when directory-name suffixes are exhausted

If directories shop through shop-20 already exist, the iteration with candidate shop-20 has suffix == 21, so this condition stops honoring taken(candidate). Connect can then create that cloud project, reuse the occupied directory, and overwrite its project link and cloud values. Return an exhaustion error instead of proceeding with a known directory collision.

Found by gpt-6.1-sol

Comment thread docs/README.md
volcano projects create my-app # create a project
volcano projects list # list your projects
volcano use my-app # set the active project
volcano connect # link this directory to a project

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: README says bare volcano connect links this directory, but it creates a new project in a new subdirectory

The new Projects example reads volcano connect # link this directory to a project. With no directory argument, resolveTarget creates a new project with a generated name and a new ./<name> subdirectory, then links that subdirectory. Only volcano connect . links the current directory, as docs/connect.md and ErrNoProjectSelected both say. A reader who follows the README gets an extra project (or a project-limit error on the free plan) and an unexpected nested directory, while the current directory stays unlinked. Change the example to volcano connect ..

Found by claude-opus-5-5

} else {
output.Success(opts.log, "Using project %s (%s)", project.Name, project.Id)
}
if err := waitUntilActive(ctx, authenticated.API, project); err != nil {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: A failed or repeated connect without a directory argument leaves an unlinked project and creates another on rerun

The project is created in resolveTarget, but cliproject.Link only runs after waitUntilActive, which can fail after its 2-minute timeout or when the user interrupts. That leaves a created project and an empty ./<generated-name> directory with no link. A rerun from the same working directory finds no link, generates a new name, and creates another project. On paid plans that duplicates projects; on the free plan it hits the project limit. Agents that rerun a pasted command do exactly this, yet docs/connect.md says the command "is safe to run again". Consider writing the link right after the project is chosen, before waiting, and qualify the "safe to run again" wording (it only holds inside the linked directory).

Found by claude-opus-5-5

Comment thread internal/auth/signin.go
// canOfferSignIn reports whether a person is there to answer: both ends are
// terminals and nothing marks the run as non-interactive.
func canOfferSignIn(in io.Reader, out io.Writer) bool {
if os.Getenv("CI") != "" || os.Getenv("VOLCANO_NONINTERACTIVE") != "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: canOfferSignIn doesn't treat TERM=dumb as non-interactive, unlike volcano setup

setupcmd.interactive treats CI, VOLCANO_NONINTERACTIVE, and TERM=dumb as non-interactive. canOfferSignIn checks only the first two plus TTYs. Some coding agents run commands inside a pseudo-terminal, where both TTY checks pass. When such an agent is not logged in, any cloud command blocks on Do you already have one? [y/N]:. docs/authentication.md promises that an agent gets exit code 2 instead. If the agent sends EOF or anything other than y/yes, the empty or non-yes answer starts browser signup. Add the TERM=dumb check (or share the setup helper) so both commands detect non-interactive runs the same way.

Found by claude-opus-5-5

Comment thread internal/config/link.go
if link != nil || err != nil {
return link, err
}
if dir == home || exists(filepath.Join(dir, ".git")) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: FindLink walks to / outside $HOME and git repos, so any ancestor's .volcano/project.json can break or retarget every command

For a working directory that is neither inside a git repository nor under $HOME (e.g. /tmp/app, or /workspace in a container), the walk continues to the filesystem root. config.Load now returns an error for any unreadable or malformed link it meets. So a stray or planted /tmp/.volcano/project.json makes every command run below it fail, including login and connect, which could repair it. A valid one silently overrides volcano use for those commands. /tmp is world-writable on shared hosts. Consider stopping the walk at the filesystem boundary of the first directory the user owns, or treating link errors from ancestors other than the working directory as "no link" with a warning.

Found by claude-opus-5-5

Comment thread docs/scripting.md
|---|---|---|
| `0` | Success | |
| `1` | Any other failure | Read the error on stderr |
| `2` | No credential is set, or the API answered HTTP 401 | Run `volcano login`, or `volcano signup` for a new account |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: An HTTP 401 on login --token / connect --token exits 1, not the documented 2

scripting.md documents exit code 2 for "the API answered HTTP 401". LoginWithToken turns a 401 from ValidateToken into errors.New("invalid token"), which wraps neither *api.Error nor config.ErrNotAuthenticated, so exitCode returns 1. A script or agent that branches on 2 to fetch a new credential won't recognize a rejected --token. Wrap config.ErrNotAuthenticated (or the API error) in that return, or narrow the doc's wording.

Found by claude-opus-5-5

if err := ensureSession(ctx, opts, sessions); err != nil {
return err
}
authenticated, err := sessions.Authenticated()

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 P1: Use the explicitly validated token for the connect workflow

With VOLCANO_TOKEN=pk-A exported, connect --token pk-B validates and saves B, then reloads authentication through sessions.Authenticated(). Config.Token() still returns A, so project creation and key retrieval run under account A despite reporting successful authentication with B. Pin the validated credentials for the remaining workflow, or reject the conflicting environment token before proceeding.

Found by gpt-6.1-sol

if err != nil {
return "", nil, false, err
}
dir, err := makeDir(filepath.Join(wd, dirName(project.Name)))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Check existing links before reusing a project-named directory

connect --project B silently reuses the automatically chosen directory even when it belongs to A. For example, valid project names my_app and my-app normalize to the same directory. The subsequent writes replace A's link and cloud credentials with B's, changing the target of later volcano cloud deploy calls. Reuse an existing directory only when its link matches B; otherwise choose an unused directory or fail.

Found by gpt-6.1-sol

Comment thread internal/auth/auth.go
token = strings.TrimSpace(token)
project = strings.TrimSpace(project)
client, err := s.sessions.APIClient(s.apiURL(cfg), token)
apiURL := s.apiURL(cfg)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: vbt_ tokens are sent to the saved session API, which may not be the API that minted them

LoginWithToken resolves apiURL through cfg.APIURL(), which now returns SessionAPIURL from an earlier login. Take a user whose last login was against staging or a local API. They paste the dashboard prompt (connect --token vbt_…), and the production-minted one-time token goes to that other host. The exchange is refused as bootstrap_token_invalid, and ensureSession falls back to browser login on the same non-default API, so the project is created there instead of in the account the prompt came from. This only affects users with a saved non-default session, mostly maintainers. The same stickiness means a plain volcano login or volcano signup can't return to the default API without logout or VOLCANO_API_URL. Consider ignoring SessionAPIURL for login, signup and the bootstrap exchange, or only persisting it when it differs from the default.

Found by claude-opus-5-5

// a new directory named after it.
func resolveTarget(ctx context.Context, opts options, session *clisession.Session, wd string) (string, *apiclient.Project, bool, error) {
if opts.dir != "" {
dir, err := makeDir(opts.dir)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚪ P3: Directory argument is created before the project exists, so a failed create leaves an empty directory

resolveTarget calls makeDir(opts.dir) before existingProject or linkedOrNewProject run. If project creation fails, for example at the plan's project limit (common on the free plan, where signup already created one project) or with an invalid --project value, volcano connect my-app exits with an error and leaves an empty ./my-app. The error then suggests volcano connect --project <id>, which with no directory argument sets up a different ./<project-name> directory, so the empty one stays behind. Creating the directory after the project resolves would avoid this.

Found by claude-opus-5-5

fallback = key.KeyValue
}
}
if fallback != "" {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Search all anon-key pages before choosing a fallback

api.Client.ListAnonKeys fetches only the first default-sized page and discards pagination metadata. If an existing project's default key is on a later page, connect selects an arbitrary first-page key instead. Anon keys can have different permissions, so the generated frontend configuration can fail operations allowed by the project's default key. Paginate until the default is found before considering a fallback.

Found by gpt-6.1-sol

if envKey(line) == v.key {
lines[i] = v.key + "=" + v.value
found = true
break

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 P2: Replace every existing definition of an env key

upsertEnv stops after replacing the first matching assignment. If cloud.env already contains a duplicate key, a later stale definition remains authoritative when envfile.Load parses it. Connect can report updating the project values while the recommended cloud deploy still reads the previous project's anon key. Remove duplicate assignments or update every occurrence of each managed key.

Found by gpt-6.1-sol

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.

2 participants