Skip to content

feat(api): recover dropped services (scpm, orchestrator, asset-graph, infrastructure) and fix command-name collisions#150

Merged
tim-thacker-nullify merged 4 commits into
mainfrom
feat/recover-dropped-api-surface
May 26, 2026
Merged

feat(api): recover dropped services (scpm, orchestrator, asset-graph, infrastructure) and fix command-name collisions#150
tim-thacker-nullify merged 4 commits into
mainfrom
feat/recover-dropped-api-surface

Conversation

@tim-thacker-nullify
Copy link
Copy Markdown
Member

Claude

Why

The generated nullify api … command surface is built from the published OpenAPI bundle by scripts/generate/main.go. An audit found two silent filters hiding large parts of the platform from the CLI:

  1. Stale spec path. The generator read ../public-docs/specs/merged-openapi.yml — an untracked, gitignored, stale (Apr 28) copy. The canonical, pipeline-maintained bundle is ../public-docs/.gitbook/assets/api/nullify-openapi-bundle.yaml.
  2. Prefix allowlist drop. serviceMapping dropped any path whose prefix wasn't listed, silently discarding entire services that are in the bundle.

What

  • Repoint the generator at the canonical bundle. This alone recovers */scan-runs (sast/sca/secrets), /context/repo-scans, and /context/sboms/{generate,resolve}.
  • Add the dropped service prefixes (/scpm/, /orchestrator/, /asset-graph/, /infrastructure/) to serviceMapping + serviceDescriptions, and wire RegisterScpmCommands / RegisterOrchestratorCommands / RegisterAssetGraphCommands / RegisterInfrastructureCommands into root.go.
  • Fix command-name collisions. generateCobraUse produced colliding names within a service parent (e.g. /dast/pentest/scans and /dast/bughunt/scans both → list-scans), which silently shadow each other in cobra. New assignCobraUses guarantees a unique Use per service, expanding only the colliding names with distinguishing path segments (list-pentest-scans / list-bughunt-scans).
  • Enforce required path args. Path params now generate cobra.ExactArgs(n) instead of MaximumNArgs(n), so a missing arg fails clearly instead of building a malformed URL.

Result: 458 endpoints across 12 services (was 366 across 8). SCPM (findings CRUD/triage/allowlist/autofix/retriage/upload), orchestrator (batch autofix, code reviews, retriage, onboarding), asset-graph, and infrastructure graphs are now reachable.

Compatibility

Some auto-generated nullify api <svc> command names change where they previously collided (and were therefore already broken/shadowed). The renames make every endpoint reachable. Labeled minor.

Test plan

  • make build ✅, make unit ✅, go vet ./...
  • Smoke: nullify api --help lists the 4 new services; nullify api scpm findings --help; nullify api sast shows list-scan-runs; missing required arg → Error: accepts 1 arg(s), received 0.

🤖 Generated with Claude Code

… infrastructure) and fix command-name collisions

The generated command surface is built from the published OpenAPI bundle by
scripts/generate/main.go. Two issues hid large parts of the platform:

1. The generator read a stale, untracked, gitignored copy of the spec at
   ../public-docs/specs/merged-openapi.yml (dated Apr 28). Repoint it at the
   canonical, pipeline-maintained bundle
   ../public-docs/.gitbook/assets/api/nullify-openapi-bundle.yaml. This alone
   recovers */scan-runs (sast/sca/secrets), /context/repo-scans, and
   /context/sboms/{generate,resolve}.

2. serviceMapping's prefix allowlist dropped any path whose prefix wasn't
   listed, silently discarding /scpm/ (24 ops), /orchestrator/ (9),
   /asset-graph/ (4), and /infrastructure/ (7) even though they are in the
   bundle. Add those prefixes and wire the new Register*Commands into root.go.

Also fixes generated-command correctness:
- generateCobraUse produced colliding names within a service parent (e.g.
  /dast/pentest/scans and /dast/bughunt/scans both -> "list-scans"), silently
  shadowing each other in cobra. assignCobraUses now guarantees a unique Use per
  service, expanding collisions to include distinguishing path segments
  (list-pentest-scans / list-bughunt-scans).
- Path parameters now generate cobra.ExactArgs(n) instead of MaximumNArgs(n), so
  a missing required arg fails clearly instead of producing a malformed URL.

Endpoints generated: 458 across 12 services (was 366 across 8).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tim-thacker-nullify tim-thacker-nullify added the minor Minor version updates (features) label May 26, 2026
Make CLI codegen self-contained and reproducible instead of depending on a
sibling ../public-docs checkout.

- Add `make fetch-spec`: downloads the OpenAPI bundle from the (private) nullify
  monorepo at a pinned commit (SPEC_REF) via `gh api` (raw media type, so the
  2 MB file isn't subject to the contents API's 1 MB base64 limit). Requires
  GitHub auth.
- Vendor the bundle at spec/nullify-openapi-bundle.yaml and generate from it
  offline. To update: bump SPEC_REF, `make fetch-spec`, `make generate-api`,
  commit both.
- Make generation deterministic: extractEndpoints sorted only by (service, path),
  but the spec's paths/methods decode from YAML maps with random iteration
  order, so two operations on one path could swap and produce spurious diffs.
  Tie-break on method as well. Verified identical across consecutive regens.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tim-thacker-nullify
Copy link
Copy Markdown
Member Author

Claude

Followup: made codegen self-contained and reproducible. The generator no longer depends on a sibling ../public-docs checkout — make fetch-spec downloads the OpenAPI bundle from the private monorepo at a pinned commit (SPEC_REF) via gh api, vendors it at spec/nullify-openapi-bundle.yaml, and generation runs offline from there. Also made the generator deterministic (tie-break the endpoint sort on method) so regeneration no longer produces spurious diffs / flaky drift checks.

…ng it

/auth/* (access/refresh/github tokens, logout) was absent from both
serviceMapping and excludePrefixes, so it fell through the silent-drop
path this PR otherwise documents and recovers from. The CLI drives those
endpoints through internal/auth, not the generated `nullify api` surface,
so make the exclusion explicit and auditable. Generated output unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tim-thacker-nullify
Copy link
Copy Markdown
Member Author

Claude

Review — solid, ship-worthy

Verified the 86k-line diff is honest and the generator logic is sound.

Independently verified

Check Result
go build / go vet / go test -skip TestIntegration ./... ✅ all pass
Regeneration drift — re-ran generator against vendored spec zero diff; committed generated code is faithful
Spec provenancespec/…yaml vs monorepo @7b34970b ✅ byte-identical (e94b88aa…); make fetch-spec is reproducible
Collision dedup (list-pentest-scans / list-bughunt-scans) ✅ present, zero numeric-suffix fallbacks anywhere
ExactArgs migration ✅ 220 ExactArgs, 0 MaximumNArgs left
Totals ✅ 458 endpoints / 12 generated services

assignCobraUses is correct (unique short name → full-segment expansion on collision → numeric suffix as last resort, which never fires on the current spec). The method-level sort tie-break genuinely de-flakes regeneration, and per-service CobraUse assignment is independent of grouped map-iteration order.

Addressed (pushed to this branch)

The PR documents the silent-drop footgun but /auth/ (access_token, refresh_token, github_token, logout) was still falling through it — absent from both serviceMapping and excludePrefixes. Since the CLI drives those through internal/auth rather than the nullify api surface, I made the exclusion explicit in excludePrefixes so a future spec audit doesn't rediscover it as a "missing service." Generated output unchanged. (/core/* was already correctly excluded.)

Minor, non-blocking

  • -by-id disambiguation is POST-only; a PUT/PATCH/DELETE batch-vs-{id} collision would fall to a numeric suffix. Latent only — doesn't occur in today's spec.
  • /ticket/ remains in serviceMapping but the bundle has no /ticket/ paths — harmless dead entry.

The published bundle is the merge of each service's openapi-public.yml, so
genuinely internal endpoints are excluded at the source and never reach the
bundle (verified: zero /internal/ paths). The /internal/ excludePrefix matched
nothing and implied internal paths could leak into the public surface. Remove
it; keep /auth/ and /core/{bitbucket,jira}/ which the bundle does carry.
Generated output unchanged.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tim-thacker-nullify tim-thacker-nullify marked this pull request as ready for review May 26, 2026 07:54
@tim-thacker-nullify tim-thacker-nullify added this pull request to the merge queue May 26, 2026
Merged via the queue into main with commit 751b20b May 26, 2026
2 checks passed
@tim-thacker-nullify tim-thacker-nullify deleted the feat/recover-dropped-api-surface branch May 26, 2026 08:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

minor Minor version updates (features)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants