Skip to content

fix(generate): --print-input without --image needs no credential (#257) - #262

Open
ZacxDev wants to merge 1 commit into
mainfrom
fix/257-print-input-auth
Open

fix(generate): --print-input without --image needs no credential (#257)#262
ZacxDev wants to merge 1 commit into
mainfrom
fix/257-print-input-auth

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Fixes #257.

The defect

internal/cmd/generate.go's RunE checked cfg.Token() == "" and returned a civitai.ErrUnauthorized-tagged error unconditionally, before runGenerate was ever entered. The --print-input short-circuit sits inside runGenerate and returns printAssembledGraph before the estimator, the submit and the balance read.

The strongest evidence is not the README — it is the code's own comment at that short-circuit, which already asserts:

With no --checkpoint/--lora there is no request of any kind.

So the command contradicted its own documented invariant. The reporter measured the same thing from the other side: with a garbage token (CIVITAI_TOKEN=not-a-real-token-000) --print-input succeeds fully offline, which is what proves the credential is never used on that path.

The fix

One line, at the existing gate — narrowed, not pushed down into the call sites (moving it would invite a "gate deleted for every path" mutation the tests would have to re-cover from scratch):

needsCredential := !o.printInput || len(o.images) > 0
if needsCredential && cfg.Token() == "" { … }

o.images is bound by StringArrayVar, so it is already populated at this point in RunE — no Flags().Changed dance needed.

Why --image and not the issue's --image/--checkpoint/--lora

The issue proposes gating on all three. That is too wide:

  • --image does need a credential even here. --print-input really uploads local files (AGENTS.md item 19(f)), and hop 1 of that upload — getConsumerBlobUploadUrl — is authed (item 19(e)).
  • --checkpoint/--lora resolve through the public GET /api/v1/model-versions/{id}, which generate.go's own comment calls "free, unauthenticated-capable" (item 13). Gating on them would keep refusing a case that demonstrably works with no credential.

An https --image value only reaches the credential-free fetch, so it does not strictly need a token. The gate still covers the whole flag: "some --image values need a credential" is a worse contract than "--image does", and the narrower rule buys nothing. Stated as a residual in the code comment rather than hidden.

Tests — internal/cmd/generate_print_input_auth_test.go

Every row drives the real command through NewRootCmd() + SetArgs, with an isolated XDG_CONFIG_HOME (t.TempDir()), CIVITAI_TOKEN explicitly cleared, and CIVITAI_BASE_URL pointed at a live httptest server that records every request. "No network" is therefore a measured zero, and TestGeneratePrintInput_NetworkRecorderPositiveControl drives that same recorder above zero through the same wiring so the zeros are not a recorder wired to nothing.

Classification is asserted with errors.Is, never message text (item 7). The JSON in the permitted case is decoded to map[string]any and checked key-by-key — a strings.Contains cannot tell cfg from cfgScale (item 14) — including that quantity is present and 2 and that unset steps is absent.

🔴 "Still ErrUnauthorized" was not enough to pin the gate, and that was measured

The first version of this table asserted exit code plus offline-ness on the three refusal rows. The whole "delete the gate" mutation survived it: internal/auth's token source refuses a credential-less request too, with its own ErrUnauthorized tag and without dialing. A control satisfied by a bystander — the exact near-miss shape AGENTS item 23 records.

The discriminator is the gate's place in the order, asserted structurally rather than by message: it runs ahead of validateGenerateOpts, so an invocation that is both credential-less and usage-invalid answers ErrUnauthorized today and flips to ErrUsage the moment the gate is gone. Those rows carry assertRefusedByTheGate.

The --checkpoint row exists for the same reason in the other direction: the issue's wider gate survived everything else, because nothing exercised --print-input --checkpoint offline. That row asserts the run reaches /api/v1/model-versions/ carrying no Authorization header — "a request went out" alone cannot distinguish a public read from an authed one, and the whole argument for excluding --checkpoint rests on which it is.

Mutation matrix

Each mutant is checksum-gated (md5 must move, gate line must occur exactly once) so an edit that silently failed to apply cannot read as a survivor. M0 is the unmutated null control.

mutant production change reddens failure message
M1 needsCredential := true (revert the fix — base 8ed4d69) 3 tests --print-input with no credential must succeed, got no token configured … (unauthorized=true); the credential gate still fired on a --print-input run that needs no credential; --print-input --checkpoint needs no credential …, but it was refused
M2 needsCredential := false (delete the gate) 3 leaf subtests + 3 parents refused as a USAGE error, so the credential gate no longer runs ahead of validateGenerateOpts: --image requires --ecosystem … / … --quantity must be at least 1, got 0 (×2)
M3 needsCredential := !o.printInput (drop the --image term) 1 leaf + 1 parent refused as a USAGE error, so the credential gate no longer runs ahead of validateGenerateOpts: --image requires --ecosystem …
M4 … || o.checkpointSet || len(o.loras) > 0 (the issue's wider gate) 1 test --print-input --checkpoint needs no credential (the version read is public), but it was refused: …
M0 none 0 all green

Note recorded honestly: under M2 and M3 the plain --image refusal subtest stays green — the downstream token source carries it. That is why the discriminating subtest sits beside it rather than replacing it.

Gate

make circ=0, --- FAIL count = 0, build failed count = 0, 18 packages ok (positive control on the count: 18 ≥ 1). gofmt -s -l . prints nothing over 297 Go files found (positive control: the tool found files).

Not done, deliberately

  • No new AGENTS.md item (six agents are working in parallel and would collide on the number). Nothing in the existing text became inaccurate: item 19(f) still describes the --image upload correctly, and item 13's "free, unauthenticated-capable" is now load-bearing for the gate rather than merely descriptive. If a maintainer wants an item, the substance is the "still ErrUnauthorized was not enough" measurement above.

  • README untouched (outside this PR's file ownership). It is not made inaccurate by this change — it says --print-input "reaches no money seam", which stays true — but it also never says the command works without a credential. Suggested addition to the Raw graphs section, for whoever owns README next:

    Without --image, --print-input needs no credential at all: it reaches no authenticated route, so it works offline and before civitai login.

  • No standalone-binary reproduction. The symptom is reproduced in-process through the real NewRootCmd() (which is exactly what main executes; main only maps the returned error to an exit code, and that mapping is pinned elsewhere). The sandbox refuses HOME/XDG_CONFIG_HOME overrides in a shell, so running ./bin/civitai against an isolated config was not possible here — flagged rather than claimed.

🤖 Generated with Claude Code

`generate`'s RunE refused whenever no token was configured, before
runGenerate was ever entered. That contradicted the invariant stated in
the command's own comment at the --print-input short-circuit — "With no
--checkpoint/--lora there is no request of any kind" — and the reporter
measured the same thing from the other side: with a garbage token the
invocation completes fully offline, so the credential was never used.

The gate stays in ONE place and is narrowed rather than pushed down into
the call sites: it is skipped only when --print-input is set AND no
--image is present.

The condition is --image and not the issue's wider --image/--checkpoint/
--lora proposal. --print-input really does upload local --image files
(AGENTS.md item 19(f)) and hop 1 of that upload is authed (item 19(e)),
so --image genuinely needs a token even here. --checkpoint/--lora resolve
through the PUBLIC GET /api/v1/model-versions/{id} (item 13, "free,
unauthenticated-capable"), so gating on those would keep refusing a case
that demonstrably works with no credential.

Tests drive the real command through NewRootCmd with an isolated
XDG_CONFIG_HOME and CIVITAI_BASE_URL pointed at a recording httptest
server, so "no network" is a measured zero with its own positive control.
Classification is asserted with errors.Is, never message text (item 7).

Measured: "delete the gate entirely" SURVIVED a table built on exit code
plus offline-ness alone, because internal/auth's token source refuses a
credential-less request too, with its own ErrUnauthorized and without
dialing. The discriminator is the gate's place in the order — it runs
ahead of validateGenerateOpts, so an invocation that is both
credential-less and usage-invalid answers ErrUnauthorized today and flips
to ErrUsage the moment the gate is gone.

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

generate --print-input requires a credential for an operation documented as making no request

1 participant