← back to Cli Printing Press
fix(cli): derive doctor auth.verify_path from me-shaped endpoint (#1431)
95f0350152285207c99616b3d80869471af9957e · 2026-05-14 23:59:47 -0700 · Trevin Chow
When a spec omits auth.verify_path, the generated doctor renders the
"present (not verified)" placeholder for credentials, which leaves
operators no way to tell a valid key from an invalid one without editing
the spec. PR #1406 introduced a me-shaped path derivation for the
unauthenticated reachability probe; the credentials probe is the
symmetric case and benefits from the same heuristic.
Mirror the HealthCheckPath derivation for Auth.VerifyPath at the top of
Generator.Generate():
1. Auth.VerifyPath (explicit operator override, never clobbered)
2. Heuristic me-shaped GET endpoint (users/me, viewer, account, ...)
3. Empty -> template falls back to the existing "present (not verified)"
branch so we never fabricate a verdict from a public path.
Run the new derivation before deriveHealthCheckPath so HealthCheckPath's
fallback chain still picks up the (now-populated) Auth.VerifyPath, which
keeps the two paths in lockstep when both fall through to the heuristic.
Closes #1161
Files touched
M internal/generator/doctor_health_path.goM internal/generator/doctor_health_path_test.goM internal/generator/generator.go
Diff
commit 95f0350152285207c99616b3d80869471af9957e
Author: Trevin Chow <trevin@trevinchow.com>
Date: Thu May 14 23:59:47 2026 -0700
fix(cli): derive doctor auth.verify_path from me-shaped endpoint (#1431)
When a spec omits auth.verify_path, the generated doctor renders the
"present (not verified)" placeholder for credentials, which leaves
operators no way to tell a valid key from an invalid one without editing
the spec. PR #1406 introduced a me-shaped path derivation for the
unauthenticated reachability probe; the credentials probe is the
symmetric case and benefits from the same heuristic.
Mirror the HealthCheckPath derivation for Auth.VerifyPath at the top of
Generator.Generate():
1. Auth.VerifyPath (explicit operator override, never clobbered)
2. Heuristic me-shaped GET endpoint (users/me, viewer, account, ...)
3. Empty -> template falls back to the existing "present (not verified)"
branch so we never fabricate a verdict from a public path.
Run the new derivation before deriveHealthCheckPath so HealthCheckPath's
fallback chain still picks up the (now-populated) Auth.VerifyPath, which
keeps the two paths in lockstep when both fall through to the heuristic.
Closes #1161
---
internal/generator/doctor_health_path.go | 37 +++++++
internal/generator/doctor_health_path_test.go | 134 ++++++++++++++++++++++++++
internal/generator/generator.go | 9 ++
3 files changed, 180 insertions(+)
diff --git a/internal/generator/doctor_health_path.go b/internal/generator/doctor_health_path.go
index ed04f774..aa1fbd82 100644
--- a/internal/generator/doctor_health_path.go
+++ b/internal/generator/doctor_health_path.go
@@ -51,6 +51,43 @@ func deriveHealthCheckPath(s *spec.APISpec) string {
if s.Auth.VerifyPath != "" {
return s.Auth.VerifyPath
}
+ return findMeShapedEndpointPath(s)
+}
+
+// deriveAuthVerifyPath chooses the path the generated `doctor` command should
+// hit for its authenticated credentials probe. Mirrors deriveHealthCheckPath
+// but for the auth-required side of the doctor report: a verify path that
+// returns 401 on bad credentials and 2xx on good ones lets doctor distinguish
+// "credentials accepted" from "credentials silently present but never tested."
+//
+// Priority:
+// 1. Auth.VerifyPath when set (explicit user override, never clobbered).
+// 2. A heuristic me-shaped GET endpoint discovered in the spec. Me-shaped
+// endpoints (`/users/me`, `/account`, `/viewer`, etc.) describe the
+// calling identity and almost universally require auth, which is exactly
+// the contract the credentials probe needs.
+// 3. Empty string. The doctor template falls back to reporting credentials
+// as "present (not verified — set auth.verify_path in spec for an API
+// acceptance check)" rather than fabricating a verdict from `/`.
+//
+// Returning empty in case 3 keeps the template's existing "not verified"
+// branch authoritative; the goal is to skip that branch whenever the spec
+// gives us a defensible me-shaped path to probe, not to invent one.
+func deriveAuthVerifyPath(s *spec.APISpec) string {
+ if s == nil {
+ return ""
+ }
+ if s.Auth.VerifyPath != "" {
+ return s.Auth.VerifyPath
+ }
+ return findMeShapedEndpointPath(s)
+}
+
+// findMeShapedEndpointPath walks the spec for the first GET endpoint whose
+// path matches one of meShapedPathTails (most-specific tail first). Returns
+// "" when no candidate qualifies — used as the shared heuristic step inside
+// the two derivation helpers above.
+func findMeShapedEndpointPath(s *spec.APISpec) string {
for _, tail := range meShapedPathTails {
if e, ok := findEndpointMatch(s, func(e spec.Endpoint) bool {
return isMeShapedEndpoint(e, tail)
diff --git a/internal/generator/doctor_health_path_test.go b/internal/generator/doctor_health_path_test.go
index 2e652698..507211bb 100644
--- a/internal/generator/doctor_health_path_test.go
+++ b/internal/generator/doctor_health_path_test.go
@@ -327,6 +327,140 @@ func TestGeneratedDoctor_KeepsExplicitHealthCheckPath(t *testing.T) {
assert.Contains(t, string(doctorGo), `healthPath := "api/marketStatus"`)
}
+func TestDeriveAuthVerifyPath_PrioritizesExplicitOverride(t *testing.T) {
+ t.Parallel()
+
+ s := minimalSpec("explicit-verify")
+ s.Auth.VerifyPath = "/operator-vetted"
+ // Add a me-shaped endpoint that would otherwise win.
+ s.Resources["users"] = spec.Resource{
+ Endpoints: map[string]spec.Endpoint{
+ "me": {Method: "GET", Path: "/users/me"},
+ },
+ }
+
+ assert.Equal(t, "/operator-vetted", deriveAuthVerifyPath(s),
+ "explicit Auth.VerifyPath must not be overwritten by the me-shaped heuristic")
+}
+
+func TestDeriveAuthVerifyPath_PicksMeShapedEndpoint(t *testing.T) {
+ t.Parallel()
+
+ s := minimalSpec("me-fallback")
+ s.Resources["users"] = spec.Resource{
+ Endpoints: map[string]spec.Endpoint{
+ "me": {Method: "GET", Path: "/users/me"},
+ },
+ }
+
+ assert.Equal(t, "/users/me", deriveAuthVerifyPath(s),
+ "a me-shaped GET should be discovered when Auth.VerifyPath is unset")
+}
+
+func TestDeriveAuthVerifyPath_FallsBackToEmpty(t *testing.T) {
+ t.Parallel()
+
+ // minimalSpec's only endpoint is /items — not me-shaped. The function
+ // must return "" so the doctor template keeps emitting the existing
+ // "present (not verified)" branch rather than fabricating a probe.
+ s := minimalSpec("no-candidate")
+ assert.Equal(t, "", deriveAuthVerifyPath(s),
+ "no me-shaped tail match must surface as empty so the template fallback wins")
+}
+
+func TestDeriveAuthVerifyPath_NilSpec(t *testing.T) {
+ t.Parallel()
+ assert.Equal(t, "", deriveAuthVerifyPath(nil))
+}
+
+// TestGeneratedDoctor_DerivesAuthVerifyPathFromMeEndpoint wires the helper
+// through Generate(): a spec with a me-shaped GET and no explicit
+// Auth.VerifyPath must emit a doctor.go that actually probes that endpoint
+// for credential validity, not the "present (not verified)" placeholder.
+func TestGeneratedDoctor_DerivesAuthVerifyPathFromMeEndpoint(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("derive-verify-me")
+ apiSpec.Resources["users"] = spec.Resource{
+ Description: "Users",
+ Endpoints: map[string]spec.Endpoint{
+ "me": {Method: "GET", Path: "/users/me", Description: "Get current user"},
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "derive-verify-me-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ doctorGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+ content := string(doctorGo)
+
+ assert.Contains(t, content, `verifyPath := "/users/me"`,
+ "doctor should probe the derived me-shaped path for credential validity")
+ assert.Contains(t, content, `c.GetWithHeaders(verifyPath`,
+ "doctor should issue the authenticated probe through the configured client")
+ assert.NotContains(t, content, `"present (not verified — set auth.verify_path in spec for an API acceptance check)"`,
+ "the no-verify-path placeholder branch must not be rendered once a path is derived")
+ assert.Equal(t, "/users/me", apiSpec.Auth.VerifyPath,
+ "derived value should be visible on spec after Generate()")
+}
+
+// TestGeneratedDoctor_KeepsExplicitAuthVerifyPath guards the override path:
+// an operator-supplied auth.verify_path must survive Generate() even when the
+// spec also ships a me-shaped endpoint that the heuristic would otherwise pick.
+func TestGeneratedDoctor_KeepsExplicitAuthVerifyPath(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("explicit-verify-override")
+ apiSpec.Auth.VerifyPath = "/operator-vetted"
+ apiSpec.Resources["users"] = spec.Resource{
+ Description: "Users",
+ Endpoints: map[string]spec.Endpoint{
+ "me": {Method: "GET", Path: "/users/me", Description: "Get current user"},
+ },
+ }
+
+ outputDir := filepath.Join(t.TempDir(), "explicit-verify-override-pp-cli")
+ require.NoError(t, New(apiSpec, outputDir).Generate())
+
+ doctorGo, err := os.ReadFile(filepath.Join(outputDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+ content := string(doctorGo)
+
+ assert.Contains(t, content, `verifyPath := "/operator-vetted"`)
+ assert.NotContains(t, content, `verifyPath := "/users/me"`)
+}
+
+// TestGenerate_AuthVerifyPathDerivationIsIdempotent pins re-entry behavior:
+// regen-merge and mcp-sync re-invoke Generate() on a spec that already saw
+// the derivation pass; the second call must observe the populated value and
+// emit byte-identical output.
+func TestGenerate_AuthVerifyPathDerivationIsIdempotent(t *testing.T) {
+ t.Parallel()
+
+ apiSpec := minimalSpec("verify-idempotent")
+ apiSpec.Resources["users"] = spec.Resource{
+ Endpoints: map[string]spec.Endpoint{
+ "me": {Method: "GET", Path: "/users/me"},
+ },
+ }
+
+ firstDir := filepath.Join(t.TempDir(), "first")
+ require.NoError(t, New(apiSpec, firstDir).Generate())
+ first, err := os.ReadFile(filepath.Join(firstDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+
+ secondDir := filepath.Join(t.TempDir(), "second")
+ require.NoError(t, New(apiSpec, secondDir).Generate())
+ second, err := os.ReadFile(filepath.Join(secondDir, "internal", "cli", "doctor.go"))
+ require.NoError(t, err)
+
+ assert.Equal(t, string(first), string(second),
+ "second Generate() must emit byte-identical doctor.go")
+ assert.Equal(t, "/users/me", apiSpec.Auth.VerifyPath,
+ "derived value should persist on spec across Generate() calls")
+}
+
// TestGeneratedDoctor_NoCandidateFallsBackToRoot keeps the negative case
// stable: a spec with nothing me-shaped renders the `/`-probe branch the
// pre-derivation template has always emitted.
diff --git a/internal/generator/generator.go b/internal/generator/generator.go
index 0450e8e8..c6e7c2c2 100644
--- a/internal/generator/generator.go
+++ b/internal/generator/generator.go
@@ -1637,6 +1637,15 @@ func (g *Generator) Generate() error {
"Set `git config github.user` (your GitHub @handle) to populate this correctly before publishing.\n",
)
}
+ // Auth.VerifyPath drives the doctor's credentials probe. Derive it before
+ // HealthCheckPath so HealthCheckPath's fallback chain can pick up the
+ // derived value (mirroring how an operator-set auth.verify_path already
+ // flows through). Without this, specs that ship a me-shaped endpoint but
+ // no explicit auth.verify_path generate a doctor that reports credentials
+ // as merely "present (not verified)" instead of probing the API.
+ if g.Spec.Auth.VerifyPath == "" {
+ g.Spec.Auth.VerifyPath = deriveAuthVerifyPath(g.Spec)
+ }
if g.Spec.HealthCheckPath == "" {
g.Spec.HealthCheckPath = deriveHealthCheckPath(g.Spec)
}
← 2988814e fix(skills): canonicalize auth env var when slug-derived dif
·
back to Cli Printing Press
·
fix(cli): bypass response cache when polling async jobs (#14 acbb3dfe →