feat: civitai CLI scaffold — App Blocks authoring (Phase 1) - #1
Merged
Conversation
Greenfield Go 1.25 CLI (Cobra + Viper) — single static `civitai` binary in the gh/kubectl/stripe mold. First feature group: App Blocks authoring. Commands: - `civitai app init [name] [--template static|page-vite] [--from <slug>]` — scaffolds a ready-to-build block project from go:embed templates. `--from` stubbed with a clear "not yet wired" message (needs a server source endpoint). - `civitai app validate [dir]` — validates block.manifest.json against the vendored JSON Schema (schema/app-block.manifest.schema.json, derived from the server validator) + structural checks; rejects dev-set iframe.src/trustTier. - `civitai app submit [dir]` — validates + packages the canonical SOURCE tree (excludes .git/node_modules/dist), then uploads via a token-accepting endpoint when configured, else writes the .zip + prints exact next steps. - `civitai login` / `civitai whoami` — token storage/verification via Viper (~/.config/civitai/config.yaml, chmod 600; CIVITAI_* env overrides). Tooling: Makefile, goreleaser config (brew tap), GitHub Actions CI (vet + gofmt + test + build). go build/test/vet all clean. Submit/auth: the live upload route POST /api/blocks/submit-version is session-cookie + moderator only (no token path), so a clean programmatic submit needs a companion token-authenticated server endpoint — documented in the README and the api package. Network/auth behind interfaces for testability. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…delity gap The vendored JSON Schema only covered syntactic rules, so `validate` green-lit manifests the server's approve-time BlockManifestValidator rejects. Port the missing semantic checks into internal/validate, mirroring how the CLI already rejects server-owned fields. - Sandbox trust-tier allowlist (validator ~L175-206): validate against the unverified tier (server-forced at submit) — only allow-scripts/allow-forms; explicitly reject allow-same-origin+allow-scripts (sandbox escape) and any out-of-allowlist token (allow-popups, allow-top-navigation, ...). - page ⇒ iframe required (~L504); renderMode=iframe ⇒ iframe required (~L377). - iframe.minHeight + iframe.resizable required when an iframe block is present (~L387-415). - renderMode inline/hybrid rejected for unverified (INLINE_REQUIRES_VERIFIED_TIER, ~L289). - targets[].slotId registry membership + page-slot rejection (~L426-460), using a vendored copy of the 4 slot ids (documented sync TODO; durable fix = server validate endpoint). False-INVALID fixes: drop the CLI's 128-char name cap (server only requires non-empty); add a clear Go-side outputDir safe-relative-path check (leading-/ + .. traversal), aligning the message with the server companion. Config 0600 race (internal/config): replace viper WriteConfigAs+chmod (creates 0644 then chmods — brief world-readable window for the token) with an atomic 0600 temp-file + rename. Existing perms test still asserts 0600. Honesty: add the two real example manifests (buzz-generator, notepad) under examples/ + a test asserting they validate clean; reframe README + validate help as a best-effort LOCAL pre-check mirroring the server validator, with the server remaining source of truth and a server validate endpoint as the durable direction. go build/test/vet/gofmt -s all clean. 51 tests pass, 0 fail. Mutation-checked the sandbox rule (removing sandboxChecks fails 4 reject tests). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Jun 25, 2026
Audit 🟡#1 (https/host enforcement). Asset download URLs come straight out of the GitHub release JSON and were fetched with http.DefaultClient, which follows redirects including https->http downgrades with no scheme/host check. An http:// checksums.txt + http:// tarball pair would make the SHA-256 gate self-referential (both halves attacker-controlled). - validateAssetURL: require scheme==https AND host in an allowlist {github.com, objects.githubusercontent.com, release-assets.githubusercontent.com}; reject (abort, no download) otherwise. Applied to BOTH the tarball and checksums.txt URLs before any bytes are read. - assetDownloadClient: dedicated *http.Client with CheckRedirect that re-validates every hop, so an https->http downgrade redirect is rejected rather than followed. Context timeouts + body-size caps kept. - Checksum-verify-before-replace gate unchanged (defense-in-depth on transport). Tests: validateAssetURL allow/reject table; an http:// asset URL and an off-host https asset URL each abort before download with the binary untouched; the asset client rejects an https->http redirect. httptest fixtures now serve TLS and inject the loopback host via a test seam so the production allowlist is never weakened to pass tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Jun 25, 2026
…-update (#39) * feat(cli): cached non-blocking update notice + `civitai upgrade` self-update Adds two related features in one PR: PART 1 — daily, cached, non-blocking "new version available" notice - A root PersistentPostRun hook prints at most ONE dim stderr line after any successful command. It never does a synchronous network call: it reads a cache (~/.config/civitai/update-check.json) and, when stale, spawns a DETACHED `civitai __update-check` (hidden subcommand) that fetches the latest release and rewrites the cache. The current run uses the cached value; the refresh lands for next time. First run (no cache) only kicks off the refresh. - Refresh at most once / 24h (last_check); notice shown at most once / 24h (last_notified) even while behind. Corrupt/missing cache => empty, fail-silent. - Suppressed when: stderr is not a TTY, CI env is set, --no-update-check / CIVITAI_NO_UPDATE_CHECK, or the command is version/upgrade/completion/help/ __update-check/__complete*. Only fires when current parses and latest > current. - stderr only — never pollutes stdout, so pipes/scripts are unaffected. - Reuses the existing fetchLatestRelease / semver helpers from update_check.go; `version` keeps its own explicit synchronous check (no double-notify). PART 2 — `civitai upgrade` self-update - Resolves the latest release (unauthenticated GitHub, no token ever sent). Already >= latest and not --force => "already up to date" no-op. - Homebrew detection: if the resolved executable lives under a brew path (/Cellar/, /Caskroom/, /opt/homebrew, /usr/local/Homebrew|Cellar, /home/linuxbrew/.linuxbrew), prints the brew upgrade command instead of self-replacing (--force overrides). - Otherwise downloads the platform tarball + checksums.txt, VERIFIES the tarball SHA-256 against checksums.txt and ABORTS on mismatch (binary left untouched), extracts the binary, and atomically replaces the running executable via github.com/minio/selfupdate. Permission-denied => clear sudo/brew/go-install guidance, non-zero exit, no half-written binary. Detaching uses a small build-tagged platform split (unix Setpgid / windows CREATE_NEW_PROCESS_GROUP); the parent never Waits. Spawn + apply + executable-path + TTY are behind injectable seams so tests assert behavior without forking or replacing the test binary. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(upgrade): enforce https + GitHub host on asset downloads Audit 🟡#1 (https/host enforcement). Asset download URLs come straight out of the GitHub release JSON and were fetched with http.DefaultClient, which follows redirects including https->http downgrades with no scheme/host check. An http:// checksums.txt + http:// tarball pair would make the SHA-256 gate self-referential (both halves attacker-controlled). - validateAssetURL: require scheme==https AND host in an allowlist {github.com, objects.githubusercontent.com, release-assets.githubusercontent.com}; reject (abort, no download) otherwise. Applied to BOTH the tarball and checksums.txt URLs before any bytes are read. - assetDownloadClient: dedicated *http.Client with CheckRedirect that re-validates every hop, so an https->http downgrade redirect is rejected rather than followed. Context timeouts + body-size caps kept. - Checksum-verify-before-replace gate unchanged (defense-in-depth on transport). Tests: validateAssetURL allow/reject table; an http:// asset URL and an off-host https asset URL each abort before download with the binary untouched; the asset client rejects an https->http redirect. httptest fixtures now serve TLS and inject the loopback host via a test seam so the production allowlist is never weakened to pass tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ZacxDev
added a commit
that referenced
this pull request
Aug 3, 2026
…s a to-do list The app-analytics handoff has been sitting on this branch, unmerged, describing three open PRs and a re-gate in flight. All of that shipped — 8 PRs across both repos, plus follow-ups #1, #2 and #4 — so as written the doc's most prominent content is a set of live-sounding directives for work that is done. A stranded doc that is also stale is worse than no doc. Changes: - A RESOLVED banner up top with the full merged-PR table, replacing "nothing merged". - Neutralised the "Do not merge until that re-gate reports" imperative, and recorded that the rebase it anticipated WAS needed, for a different reason: #3566 landed later and edited the same `detail: {}` object, turning #3561 CONFLICTING after its gate had passed. - Struck follow-ups #1, #2 and #4 with what actually happened, including the two places this doc was WRONG: * #1's entry missed a second proc with the identical live defect — `getMyForgejoCloneInfo`, which `civitai app pull` drives. It was found by an audit, not by the list, which is worth knowing about ranked follow-up lists in general: the list is not a survey. * #2's suggested fix (reuse `humaniseScopeEndpoint`) would have shipped a bug. Measured against the real function it returns '(no workflow id)' for `workflow:submit` and '' for `user-settings:write`, because it is the per-ROW labeller and an aggregate bucket has no `detail`. - Recorded #1's scope decision with the prod evidence that later confirmed it: 331 live tokens unblocked, 30 of which lack bit 26 — so copying the nearest precedent (AppBlocksDevTunnel) would have left those 30 still 403ing. Plus the measurement trap: `(mask & Full) = Full` is also true when `mask == Full` and reports 145 false hits; the strict-superset form needs `AND mask <> Full`. - New "Still open — start here" section: the CI `component`-tier gap (three PRs shipped browser tests that have never run on a canonical browser), the stale-node_modules trap that silently removes ~1,126 tests, the unverified `addCollaborator` downgrade lead, and a note that `installs: 0` should be assumed broken until a positive control exists. - "What actually caught the bugs": across 12 adversarial audit rounds every fix round found a defect in the previous fix, and the mechanical gate caught none of them — the suite and typecheck were green at every tip. Doc-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Greenfield Go 1.25 CLI for Civitai — a single static
civitaibinary in thegh/kubectl/stripemold. Phase 1 ships the App Blocks authoring feature group undercivitai app, replacing the confusing hand-format-a-ZIP flow: the CLI generates the correct project shape, validates the manifest against the platform contract, and packages/submits it.Command surface
app init— scaffolds a ready-to-build project fromgo:embedtemplates (staticno-build page block;page-viteVite+React with config-as-codebuildCommand/outputDir). Manifests omit the server-ownediframe.src/trustTier.--from <slug>is stubbed with a clear "not yet wired" message — not faked.app validate— a best-effort LOCAL pre-check mirroring the server's approve-time validator (the server remains source of truth). Validates against the vendored JSON Schema (syntactic) plus the ported semantic rules + structural checks (see below).app submit— validates, packages the canonical source tree (manifest + src + build config, NOT a prebuilt dist; excludes.git/node_modules/dist; enforces the server caps 50 MiB / 2000 files / 10 MiB-per-file), then either uploads via a token endpoint (when configured) or writes the.zip+ prints exact next steps.login/whoami— token storage/verification via Viper (~/.config/civitai/config.yaml,0600;CIVITAI_*env overrides).Audit fixes in this revision
🔴 Sandbox trust-tier allowlist (ported from
validateSandbox, validator ~L175-206)A submitted block is always the unverified tier (
trustTieris server-owned and forced at submit), whose sandbox allowlist is{allow-scripts, allow-forms}only.validatenow:allow-popups,allow-top-navigation,allow-same-origin, …) with a field-pathed message that notes the tier is server-forced to unverified;allow-same-origin+allow-scriptssandbox-escape combo (defense in depth, matching validator ~L201).🟡 Other ported semantic checks (same false-VALID class)
minHeight∈ [40,4000] +resizableboolean (~L387-415) — the schema validated ranges when present but not required-ness.inline/hybrid→INLINE_REQUIRES_VERIFIED_TIERfor unverified (~L289), rejected.targets[].slotIdregistry membership + page-slot rejection (~L426-460), against a vendored copy of the 4 slot ids (3 model +app.page). Cheap to vendor; documented sync TODO. We're launching page-only (page apps have notargets[]), so this is rarely exercised today.🟡 False-INVALID fixes (CLI rejected what the server accepts)
namecap — the server only requires non-empty.outputDirleading-//..-traversal rejection kept (the server companion validates outputDir as a safe relative path), now a clear Go-side message instead of the cryptic JSON-Schema regex error.🟡 Config
0600raceinternal/config.save()previously used viperWriteConfigAs(creates the file at umask0644, then chmods0600) — a brief world-readable window for the token. Now writes to a0600temp file and atomically renames it into place (also crash-safe). The perms test still asserts0600.🟡 Honesty
examples/(buzz-generator, notepad — copied from the shippingcivitai-block-*apps) + a test assertingvalidatepasses them, so the "validates clean" claim is true. Both validate clean against the now-stricter checks; no divergence found.validatehelp:validateis a best-effort local pre-check mirroring the server's approve-time validator; the server remains source of truth. Durable direction documented: a future servercivitai app validateendpoint that runs the realBlockManifestValidator(the faithful contract), with the published schema as the syntactic half.Validate fidelity
validateis a local mirror, not the contract. Syntactic rules live in the vendored JSON Schema; the semantic rules (sandbox tier allowlist, page⇒iframe, required iframe sub-fields, renderMode tier gate, slot-registry membership) are ported into Go fromblock-manifest-validator.service.ts. Necessarily approximate locally:targets[].slotIduses a vendored slot list — a brand-new server slot would false-INVALID locally until the list is updated (it still validates server-side). We launch page-only, so the blast radius is minimal.iframe.src/assetBundleUrl∈OauthClient.allowedOrigins) and scope ⊆ client allowedScopes depend on per-app server state the CLI can't see — not reproduced.Durable fix: a server
civitai app validateendpoint that calls the realBlockManifestValidator. The published manifest schema is a step toward that.Vendored manifest schema
schema/app-block.manifest.schema.jsonis derived from the server validator: required fields, scopes enum, page config (incl. positive-integerbuzzBudgetPerGen), sandbox tokens, contentRating enum, buildCommand/outputDir. Embedded viago:embed. It covers the syntactic half; the semantic half is the ported Go checks above.Submit path + the cross-repo dependency
The one cross-repo dependency (unchanged from the prior revision). The live route
POST /api/blocks/submit-versionis session-cookie + moderator (ModEndpoint) — no bearer token — so fully-programmatic submit is blocked on a server change. Implemented today: build + validate the canonical ZIP; if a token andCIVITAI_SUBMIT_PATHare configured, upload withAuthorization: Bearerand the existing{ "bundleBase64": ... }body; otherwise write the.zip+ print exact manual next steps. Network/auth sits behindapi.Submitter/api.Verifierinterfaces (fully unit-tested without a live server).Server-side follow-up for a clean
submit: a token-authenticated sibling ofPOST /api/blocks/submit-versionthat resolves the API key→user, applies the same App-Blocks + (currently) moderator gates, reusessubmitVersionunchanged, and returns{ publishRequestId, slug, version, status }.Build / test / quality
go build ./...,go test ./...,go vet ./...,gofmt -s -l .— all clean.sandboxCheckstoreturn nilfails 4 reject tests (the accept test correctly still passes), then restored..github/workflows/ci.yml.Remaining known fidelity gap
targets[].slotIdagainst a vendored slot list (low priority — page-only launch) and the per-app origin/scope binding the CLI can't see. Durable fix = a servercivitai app validateendpoint.Do not merge — Phase 1 review.
🤖 Generated with Claude Code