fix: CLI update resilience + setup wizard UX overhaul#642
Conversation
CLI update command: - Add dirty state detection (missing compose, images, config, secrets) with interactive recovery prompt after partial uninstall - Add old image cleanup after successful upgrade, identifying non-current images by Docker image ID (handles both tagged and digest-pinned refs) Setup wizard: - Always show all 5 steps (admin never disappears on refresh) - Store-driven step completion tracking synced from backend status - Re-fetch /setup/status after each step for accurate state - Completed steps show read-only summaries (admin locked, others editable) - Free navigation to any completed step via step indicators - Provider step: "Change Provider" button, proper internal back navigation - Password fields: show/hide toggles, fixed autocomplete attributes Provider discovery SSRF fix: - Extract _fetch_json_trusted() for preset-originated URLs that skip SSRF validation (localhost/private IPs are valid for Ollama, LM Studio, etc.) - Thread trust_url through discover_models -> _fetch_json when preset_hint is provided in management service Closes #641 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Extends trust_url logic in discover_models_for_provider to also skip SSRF validation when the provider uses auth_type=none (local providers like Ollama/LM Studio configured outside the preset flow). The admin explicitly configured the URL, and localhost/private IPs are valid for local providers. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Security (CRITICAL): - Validate preset_hint against preset registry before trusting (SSRF) - Only trust preset default URL, not user-supplied base_url override - Add ImageTag validation to config.State.validate() (glob injection) - Add httpx.HTTPStatusError handler to _fetch_json_trusted - Upgrade SSRF bypass log level from INFO to WARNING Go CLI (MAJOR): - Refactor checkInstallationHealth into helpers (< 50 lines each) - Refactor cleanupOldImages into findOldImages + collectCurrentImageIDs + promptAndRemoveImages (< 50 lines each) - Docker detection failure now reported in health check issues - Separate Docker errors from missing-image detection - Abort cleanup if current image IDs can't be resolved - Log Docker listing failures before early return - Validate Docker image IDs before passing to docker rmi Vue (CRITICAL/MAJOR): - Make SetupAdmin isComplete reactive via computed() - Add markStepComplete store action, remove direct mutation - Add fetchStatus to handleAgentComplete - Clear savedAgent on startEditing in SetupAgent - Replace bare catch with error logging in SetupProvider probe Docs (MEDIUM): - Update operations.md SSRF description for trust_url bypass - Update CLAUDE.md package structure for discovery.py - Update user_guide.md setup wizard step count Pre-reviewed by 14 agents, 18 findings addressed. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI (base), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (1)
WalkthroughThis PR adds CLI installation-health checks and a recovery prompt, optional post-upgrade cleanup of non-current Docker images, and image-tag validation. It refactors the update flow (restart/rollback behavior) and adds new health/cleanup helpers. The setup wizard was reworked to always show five steps, track per-step completion, present completed-step summaries (editable), and add password show/hide controls. Provider discovery was refactored: probing moved to a new probing module, 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cli/internal/config/state.go`:
- Around line 148-164: IsValidImageTag currently validates characters but not
length; add a check enforcing Docker's 128-character tag limit by returning
false if len(tag) > 128 (place this near the existing len(tag) == 0 check at the
top of IsValidImageTag) so tags exceeding 128 chars fail validation before
character checks run.
In `@web/src/components/setup/SetupAdmin.vue`:
- Around line 127-135: Add an aria-label to the password visibility toggle
buttons in SetupAdmin.vue so screen readers receive a clear accessible name: for
the button that toggles showPassword (the button with `@click`="showPassword =
!showPassword" and :title="showPassword ? 'Hide password' : 'Show password'")
add a bound aria-label that mirrors the title (e.g., :aria-label="showPassword ?
'Hide password' : 'Show password'"); do the same for the confirm-password toggle
(the corresponding button around lines 152-159) so both use aria-label rather
than relying solely on title.
In `@web/src/components/setup/SetupAgent.vue`:
- Around line 125-128: The startEditing() function currently clears savedAgent
(savedAgent.value = null) which prevents form fields from being pre-populated;
change startEditing() to preserve savedAgent and instead set editing.value =
true while leaving savedAgent.value intact (or copy savedAgent into a separate
mutable form model such as agentForm or initialize reactive form fields from
savedAgent when editing starts), and update the form initialization logic to
read from savedAgent (or agentForm) so fields are populated when entering edit
mode (refer to startEditing(), savedAgent.value, editing.value and the
component's form model where SetupCompany implements similar behavior).
In `@web/src/components/setup/SetupCompany.vue`:
- Around line 88-127: The summary can show stale/fallback values because
createdResult is a component-local ref and lost on refresh; update
SetupCompany.vue so that on mounted (or when showSummary is true) if
setup.isStepComplete('company') you fetch the company details from the backend
(or read them from a Pinia store) and populate createdResult before
rendering/allowing the Next action; ensure the emit call in the Next button uses
the fetched createdResult.companyName (not the fallback) and consider persisting
createdResult into the Pinia store when initially created so navigation/refresh
retains the real values.
- Around line 42-44: startEditing() currently only sets editing.value = true so
form fields stay empty; update startEditing() to populate companyName,
companyDescription, and selectedTemplate from createdResult (guarding for
null/undefined) before flipping editing.value to true so the form is pre-filled.
In other words, inside startEditing() read createdResult (or
createdResult.value) and assign its name/description/template into the reactive
variables companyName, companyDescription, selectedTemplate (use default
fallbacks), then set editing.value = true.
In `@web/src/components/setup/SetupProvider.vue`:
- Around line 165-183: The handler handleChangeProvider sets
changingProvider.value = true when starting a provider change but never resets
it, so showSummary (which checks !changingProvider.value) never re-enables;
update the provider-completion flow to set changingProvider.value = false when
the new provider setup completes successfully (e.g., after store.deleteProvider
resolves and after any subsequent successful provider creation/configuration
path), ensuring you clear changingProvider in the success branch alongside
resetting createdProviderName, selectedPreset, testPassed, testResult, and error
so the summary can show again.
In `@web/src/views/SetupPage.vue`:
- Around line 69-74: The click handler currently permits navigating to any index
less than setup.currentStep which can let users jump to earlier but incomplete
steps; update handleStepClick to only allow navigation when the target step is
marked complete or is the current step itself by replacing the condition with a
check that calls setup.setStep(index, steps.value.length) only if
isStepDone(index) || index === setup.currentStep; keep references to
handleStepClick, isStepDone, setup.currentStep, setup.setStep and
steps.value.length so the change is easy to locate.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 86b12df4-493f-4950-b3a2-f7e186c541b0
📒 Files selected for processing (15)
CLAUDE.mdcli/cmd/update.gocli/internal/config/state.godocs/design/operations.mddocs/user_guide.mdsrc/synthorg/observability/events/provider.pysrc/synthorg/providers/discovery.pysrc/synthorg/providers/management/service.pytests/unit/providers/management/test_service.pyweb/src/components/setup/SetupAdmin.vueweb/src/components/setup/SetupAgent.vueweb/src/components/setup/SetupCompany.vueweb/src/components/setup/SetupProvider.vueweb/src/stores/setup.tsweb/src/views/SetupPage.vue
| function startEditing() { | ||
| editing.value = true | ||
| } |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Form fields not pre-populated when entering edit mode.
When startEditing() is called, the form fields (companyName, companyDescription, selectedTemplate) retain their initial empty values rather than being populated from createdResult. The user would need to re-enter all information.
♻️ Proposed fix to pre-populate form fields
function startEditing() {
+ if (createdResult.value) {
+ companyName.value = createdResult.value.companyName
+ // Note: description and template aren't in createdResult;
+ // consider storing them or fetching from backend
+ }
editing.value = true
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function startEditing() { | |
| editing.value = true | |
| } | |
| function startEditing() { | |
| if (createdResult.value) { | |
| companyName.value = createdResult.value.companyName | |
| // Note: description and template aren't in createdResult; | |
| // consider storing them or fetching from backend | |
| } | |
| editing.value = true | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/setup/SetupCompany.vue` around lines 42 - 44,
startEditing() currently only sets editing.value = true so form fields stay
empty; update startEditing() to populate companyName, companyDescription, and
selectedTemplate from createdResult (guarding for null/undefined) before
flipping editing.value to true so the form is pre-filled. In other words, inside
startEditing() read createdResult (or createdResult.value) and assign its
name/description/template into the reactive variables companyName,
companyDescription, selectedTemplate (use default fallbacks), then set
editing.value = true.
| <!-- Completed summary --> | ||
| <template v-if="showSummary"> | ||
| <div class="rounded-lg border border-green-500/20 bg-green-500/10 p-4"> | ||
| <div class="mb-3 flex items-center gap-2"> | ||
| <i class="pi pi-check-circle text-xl text-green-400" /> | ||
| <span class="text-sm font-medium text-green-300">Company created</span> | ||
| </div> | ||
| <div class="space-y-1 text-sm text-slate-300"> | ||
| <p>Name: <strong>{{ createdResult?.companyName ?? 'Your Company' }}</strong></p> | ||
| <p>Template: {{ createdResult?.templateApplied ?? 'Start Blank' }}</p> | ||
| <p>Departments: {{ createdResult?.departmentCount ?? 0 }}</p> | ||
| </div> | ||
| <Button | ||
| label="Edit" | ||
| icon="pi pi-pencil" | ||
| severity="secondary" | ||
| size="small" | ||
| outlined | ||
| class="mt-3" | ||
| @click="startEditing" | ||
| /> | ||
| </div> | ||
|
|
||
| <div | ||
| v-if="error" | ||
| role="alert" | ||
| class="rounded bg-red-500/10 p-3 text-sm text-red-400" | ||
| > | ||
| {{ error }} | ||
| </div> | ||
|
|
||
| <div class="flex items-center gap-3"> | ||
| <div class="mt-8 flex items-center gap-3"> | ||
| <Button | ||
| type="button" | ||
| label="Back" | ||
| icon="pi pi-arrow-left" | ||
| severity="secondary" | ||
| outlined | ||
| :disabled="creating" | ||
| @click="emit('previous')" | ||
| /> | ||
| <Button | ||
| type="submit" | ||
| label="Create Company" | ||
| icon="pi pi-building" | ||
| label="Next" | ||
| icon="pi pi-arrow-right" | ||
| icon-pos="right" | ||
| class="flex-1" | ||
| :loading="creating" | ||
| :disabled="!isValid" | ||
| @click="emit('next', createdResult?.companyName ?? '')" | ||
| /> | ||
| </div> | ||
| </form> | ||
| </template> |
There was a problem hiding this comment.
Summary view may show stale/empty data after page refresh.
When the user refreshes the page, createdResult is lost (component-local ref) but setup.isStepComplete('company') may still return true (from backend status). This causes:
- The summary to display fallback values ("Your Company", 0 departments)
- The "Next" button at line 124 to emit an empty string for
companyName
Consider fetching the company details from the backend on mount when the step is already complete, or store the created result in the Pinia store for persistence across navigation.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@web/src/components/setup/SetupCompany.vue` around lines 88 - 127, The summary
can show stale/fallback values because createdResult is a component-local ref
and lost on refresh; update SetupCompany.vue so that on mounted (or when
showSummary is true) if setup.isStepComplete('company') you fetch the company
details from the backend (or read them from a Pinia store) and populate
createdResult before rendering/allowing the Next action; ensure the emit call in
the Next button uses the fetched createdResult.companyName (not the fallback)
and consider persisting createdResult into the Pinia store when initially
created so navigation/refresh retains the real values.
|
Warning Gemini is experiencing higher than usual traffic and was unable to create the summary. Please try again in a few hours by commenting |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #642 +/- ##
==========================================
- Coverage 92.45% 92.44% -0.02%
==========================================
Files 557 559 +2
Lines 28168 28229 +61
Branches 2705 2715 +10
==========================================
+ Hits 26043 26095 +52
- Misses 1675 1680 +5
- Partials 450 454 +4 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
SSRF: remove auth_type=none trust bypass -- only validated preset hints grant SSRF bypass now (security-reviewer, code-reviewer). Python: extract probing.py from discovery.py (888->625 lines), refactor _fetch_json/_discover_* to shared helpers (<50 lines each), extract trust resolution and preset discovery helpers in service.py, rename validated_preset to hint_is_known_preset. Go: split update.go (942->702 lines) into update_cleanup.go and update_health.go, fix rollback error logging, Windows \r\n line splitting, recover->doRecover rename, safeStateDir error surfacing, remove dead sha256: branch, accept uppercase hex, remove --force from docker rmi, add 128-char tag limit, add IsValidImageTag tests, add PRIVATE_KEY/CERT to secret redaction, add Docker health timeout. Vue: reset changingProvider after success, fix stale company summary, pre-populate edit forms (company + agent), guard step advance on statusLoaded, remove console.warn, import RouterLink, tighten step click nav, add aria-label to password toggles. Tests: 7 new tests for trust_url logic, SSRF bypass verification, and IsValidImageTag edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
web/src/components/setup/SetupCompany.vue (1)
73-79: 🧹 Nitpick | 🔵 TrivialConsider fetching company details on mount for refresh resilience.
After a page refresh,
createdResultis lost (component-local ref) butisStepComplete('company')may still be true from backend status. Currently, this causes the form to display (sinceshowSummaryrequirescreatedResult !== null), which is acceptable fallback behavior.For a more polished UX, consider fetching the company details from the backend on mount when the step is already complete, so the summary displays correctly after refresh.
Also applies to: 91-130
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/setup/SetupCompany.vue` around lines 73 - 79, When the component mounts, if the backend reports isStepComplete('company') true but createdResult is null, call the setup method that fetches the persisted company (e.g., add a call like setup.fetchCompanyDetails or setup.getCompany / setup.fetchCreatedResult) inside the existing onMounted block after setup.fetchTemplates; handle errors the same way you do for setup.fetchTemplates (surface via setup.error -> error.value). Also apply the same logic in the other mount/initialization code path around the showSummary logic (the region referenced by lines 91-130) so the summary is populated after a page refresh when createdResult is lost.
♻️ Duplicate comments (2)
web/src/components/setup/SetupCompany.vue (1)
42-47:⚠️ Potential issue | 🟡 MinorIncomplete form pre-population when entering edit mode.
The
startEditing()function only restorescompanyNamefromcreatedResult, butcompanyDescriptionandselectedTemplateare not restored. Users editing a company will see their name but must re-enter the description and re-select the template.♻️ Proposed enhancement to store and restore all fields
const createdResult = ref<{ companyName: string templateApplied: string | null departmentCount: number + description: string | null } | null>(null) // In handleCreate, store description: createdResult.value = { companyName: result.company_name, templateApplied: result.template_applied, departmentCount: result.department_count, + description: companyDescription.value.trim() || null, } // In startEditing: function startEditing() { if (createdResult.value) { companyName.value = createdResult.value.companyName + companyDescription.value = createdResult.value.description ?? '' + selectedTemplate.value = createdResult.value.templateApplied } editing.value = true }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/setup/SetupCompany.vue` around lines 42 - 47, startEditing currently only restores companyName from createdResult, causing companyDescription and selectedTemplate to be lost when entering edit mode; update startEditing to also read and set companyDescription.value and selectedTemplate.value from createdResult.value (e.g., createdResult.value.companyDescription and the template identifier/property used in the component) before setting editing.value = true so all form fields are pre-populated when editing.web/src/components/setup/SetupAgent.vue (1)
125-132:⚠️ Potential issue | 🟡 Minor
selectedPersonalitynot pre-populated when entering edit mode.The
startEditing()function restoresagentName,selectedRole, andselectedModelfromsavedAgent, but omitsselectedPersonality. SinceisValidrequiresselectedPersonality !== null(line 104), the user must re-select personality after clicking Edit, even though it was already configured.Additionally,
selectedProvideris not restored, which may cause the model dropdown to show all providers instead of filtering to the agent's original provider.🐛 Proposed fix to pre-populate all fields
function startEditing() { if (savedAgent.value) { agentName.value = savedAgent.value.name selectedRole.value = savedAgent.value.role + selectedProvider.value = savedAgent.value.model_provider selectedModel.value = `${savedAgent.value.model_provider}::${savedAgent.value.model_id}` + // Restore personality if available (may need backend to return it) + // selectedPersonality.value = savedAgent.value.personality_preset ?? null } editing.value = true }Note: The backend response (
SetupAgentResponse) needs to includepersonality_presetfor full restoration. If it's not currently returned, consider adding it to the API response.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@web/src/components/setup/SetupAgent.vue` around lines 125 - 132, startEditing currently restores agentName, selectedRole and selectedModel from savedAgent but omits selectedPersonality and selectedProvider, which causes isValid to fail and the model list to be unfiltered; update startEditing to also set selectedPersonality.value = savedAgent.value.personality_preset (or personality field returned by the API) and selectedProvider.value = savedAgent.value.model_provider when savedAgent exists, and ensure the API/SetupAgentResponse returns personality_preset if not present so the personality can be fully restored; keep setting editing.value = true at the end.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cli/cmd/update_cleanup.go`:
- Around line 86-99: collectCurrentImageIDs currently treats an empty idOut as
success and returns a partial currentIDs map, causing findOldImages to
misclassify current images as old; update collectCurrentImageIDs to treat an
empty or whitespace idOut as an error for each svc: after running docker.RunCmd
(in collectCurrentImageIDs) check if strings.TrimSpace(idOut) == "" and if so
return an error (e.g., fmt.Errorf("no current image ID found for %s", svc)) so
callers never receive a partial currentIDs set and findOldImages won't mark
current images for removal.
In `@cli/cmd/update_health.go`:
- Around line 57-69: The Docker probe failure from docker.Detect should not be
treated as a definitive "dirty install" issue; remove adding docker.Detect
errors to the issues slice and only report missing images when
detectMissingImages actually returns missing artifacts. In practice, keep the
short healthCtx and call docker.Detect(healthCtx), but if docker.Detect returns
an error do not append fmt.Sprintf("Docker not available: %v", dockerErr) to
issues — instead treat docker as unavailable (allowing updateContainerImages to
handle it) and only append the formatted missing-image message when
detectMissingImages(healthCtx, info, state) returns a non-empty slice; ensure
the logic around state.ImageTag, detectMissingImages, and promptHealthRecover
remains intact so non-interactive runs are only aborted for confirmed missing
artifacts.
In `@cli/cmd/update.go`:
- Around line 54-59: The recovery path still short-circuits recreate/pull logic
when the stored CLI tag equals the current tag; modify the post-recovery flow so
that when checkInstallationHealth indicates a recover-and-continue scenario you
set a "force recovery" flag and pass it into refreshCompose and
updateContainerImages; change refreshCompose to treat a missing compose.yml as
requiring recreation when force is true (instead of "nothing to refresh") and
change updateContainerImages to skip the early-return on matching stored tag
when force is true (force a re-pull/recreate of images); update calling sites
and signatures for refreshCompose and updateContainerImages accordingly so
recovery triggers recreation even if the version did not change.
- Around line 61-63: Call migrateSettingsKey before running the installation
health check so the migration that populates SettingsKey runs prior to
detectInstallationIssues; specifically, move the migrateSettingsKey(cmd, &state)
invocation to execute before detectInstallationIssues(...) (the health-check
call that currently flags an empty SettingsKey) so older installs with no
SettingsKey are repaired before prompts/aborts.
In `@src/synthorg/providers/discovery.py`:
- Around line 328-339: The code currently always emits the
PROVIDER_MODELS_DISCOVERED event even when all raw entries were malformed;
change the final logger.info call in the discovery function to be conditional:
after computing raw_key, raw_entries, skipped and calling _log_skip_counts, only
call logger.info(PROVIDER_MODELS_DISCOVERED, preset=preset_name,
model_count=len(models)) when not (skipped > 0 and len(models) == 0) (i.e.,
suppress the success event if entries were skipped and no models were parsed) so
that an all_entries_malformed outcome does not also emit a success metric.
In `@src/synthorg/providers/management/service.py`:
- Around line 449-455: The current logic sets trust=True whenever preset_hint
resolves, which re-enables SSRF for arbitrary stored config.base_url; change
discover_models_for_provider to only set trust=True when the stored
config.base_url exactly matches the preset's known safe URLs (the preset's
default_base_url or any entry in candidate_urls) — retrieve the preset object
(the one used by _resolve_discovery_trust or similar lookup), compare
config.base_url against preset.default_base_url and preset.candidate_urls, and
only pass trust=True to discover_models when there is a match; otherwise keep
trust=False and let _validate_discovery_url run normally. Apply the same guarded
check for the other occurrence noted (around the 468-486 block).
In `@src/synthorg/providers/probing.py`:
- Around line 195-209: Change probe_preset_urls so it no longer accepts
arbitrary candidate_urls: have it take only preset_name and internally call
get_preset(preset_name) (from the presets module) to resolve the trusted
candidate URLs (and raise/return an error if the preset is missing), or
alternatively make the function controller-private by renaming it to
_probe_preset_urls and keep callers that already resolve presets; update all
callers of probe_preset_urls to pass only preset_name (or use the new private
name) so the SSRF-trust boundary is enforced in code rather than by docstring.
---
Outside diff comments:
In `@web/src/components/setup/SetupCompany.vue`:
- Around line 73-79: When the component mounts, if the backend reports
isStepComplete('company') true but createdResult is null, call the setup method
that fetches the persisted company (e.g., add a call like
setup.fetchCompanyDetails or setup.getCompany / setup.fetchCreatedResult) inside
the existing onMounted block after setup.fetchTemplates; handle errors the same
way you do for setup.fetchTemplates (surface via setup.error -> error.value).
Also apply the same logic in the other mount/initialization code path around the
showSummary logic (the region referenced by lines 91-130) so the summary is
populated after a page refresh when createdResult is lost.
---
Duplicate comments:
In `@web/src/components/setup/SetupAgent.vue`:
- Around line 125-132: startEditing currently restores agentName, selectedRole
and selectedModel from savedAgent but omits selectedPersonality and
selectedProvider, which causes isValid to fail and the model list to be
unfiltered; update startEditing to also set selectedPersonality.value =
savedAgent.value.personality_preset (or personality field returned by the API)
and selectedProvider.value = savedAgent.value.model_provider when savedAgent
exists, and ensure the API/SetupAgentResponse returns personality_preset if not
present so the personality can be fully restored; keep setting editing.value =
true at the end.
In `@web/src/components/setup/SetupCompany.vue`:
- Around line 42-47: startEditing currently only restores companyName from
createdResult, causing companyDescription and selectedTemplate to be lost when
entering edit mode; update startEditing to also read and set
companyDescription.value and selectedTemplate.value from createdResult.value
(e.g., createdResult.value.companyDescription and the template
identifier/property used in the component) before setting editing.value = true
so all form fields are pre-populated when editing.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 41a3cf68-6b63-47c6-97d8-85d0e89d5ff9
📒 Files selected for processing (17)
cli/cmd/update.gocli/cmd/update_cleanup.gocli/cmd/update_health.gocli/internal/config/state.gocli/internal/config/state_imagetag_test.gosrc/synthorg/api/controllers/providers.pysrc/synthorg/providers/discovery.pysrc/synthorg/providers/management/service.pysrc/synthorg/providers/probing.pytests/unit/api/controllers/test_providers.pytests/unit/providers/management/test_service.pytests/unit/providers/test_discovery.pyweb/src/components/setup/SetupAdmin.vueweb/src/components/setup/SetupAgent.vueweb/src/components/setup/SetupCompany.vueweb/src/components/setup/SetupProvider.vueweb/src/views/SetupPage.vue
SSRF: preset_hint trust now also verifies base_url matches the preset's candidate_urls or default_base_url -- prevents storing an arbitrary URL and using a valid preset_hint to bypass SSRF. Go: empty image ID guard in collectCurrentImageIDs, Docker unavailability no longer triggers recovery, migration runs before health check, recovery clears stored tag to force re-pull. Python: suppress PROVIDER_MODELS_DISCOVERED after all_entries_malformed, probe_preset_urls resolves own candidates from preset registry. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
cli/cmd/update.go (1)
60-74:⚠️ Potential issue | 🟠 MajorRecovery still doesn't rebuild a missing
compose.yml.Line 73 only forces the later image-update path.
refreshCompose()still treats a missing compose file as success, so a “recover” run can still exit withcompose.ymlmissing whenever Docker is unavailable or the user declines the later image prompt. Please thread an explicit recovery/force flag intorefreshCompose()instead of relying onstate.ImageTag = "".Also applies to: 78-103, 239-254, 425-429
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cli/cmd/update.go` around lines 60 - 74, checkInstallationHealth's recovered boolean is currently used only to clear state.ImageTag, which doesn't force refreshCompose to rebuild a missing compose.yml; instead add a recovery/force parameter to refreshCompose (e.g., refreshCompose(ctx, force bool)) and pass the recovered flag into it wherever refreshCompose is called (including the update flow around updateContainerImages and the other call sites referenced), so refreshCompose treats force=true as a hard rebuild of compose.yml rather than a no-op; remove the implicit reliance on state.ImageTag="" for this behavior and ensure callers that receive the recovered value from checkInstallationHealth forward it to refreshCompose.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cli/cmd/update_health.go`:
- Around line 51-56: The code currently treats a missing state.JWTSecret as
recoverable but only offers the existing recovery path which regenerates
images/compose and calls migrateSettingsKey() (which only repairs
state.SettingsKey), leaving the installation unusable; fix by either (preferred)
synthesizing and persisting a new JWT secret before offering recovery (e.g.,
generate a secure secret, assign to state.JWTSecret and persist the state/save
function used elsewhere) so subsequent recovery works, or skip the in-place
"recover" option for the JWT secret and route this case to the interactive
synthorg init flow instead; also ensure similar handling is applied where
JWTSecret is checked in the later block (lines ~86-107) so you don't present an
inappropriate recover option.
In `@cli/cmd/update.go`:
- Around line 445-450: Change restartIfRunning to signal whether the new
revision is actually running: update its signature to return (bool, error) (true
= restarted and passed health checks, false = skipped or failed). Inside
restartIfRunning ensure you return false when the user skips restart or when the
post-restart health check fails; return true only on a successful
restart+healthy result. Update callers (the call near restartIfRunning(...) then
cleanupOldImages(...) and the other similar block around lines 652-674) to check
the bool and only call cleanupOldImages(cmd, info, updatedState) when the bool
is true; propagate errors unchanged.
In `@src/synthorg/providers/probing.py`:
- Around line 47-54: Extract the _redact_url function into a single shared
utility function in synthorg.providers.utils (keep the same signature and
behavior: parsing, hostname/port handling, and replacing query with
"<redacted>"), then replace the local definitions in probing.py and discovery.py
with an import from synthorg.providers.utils; remove the duplicated
implementations, update imports (use from synthorg.providers.utils import
_redact_url or a public name), and run tests/linters to ensure no references
break.
---
Duplicate comments:
In `@cli/cmd/update.go`:
- Around line 60-74: checkInstallationHealth's recovered boolean is currently
used only to clear state.ImageTag, which doesn't force refreshCompose to rebuild
a missing compose.yml; instead add a recovery/force parameter to refreshCompose
(e.g., refreshCompose(ctx, force bool)) and pass the recovered flag into it
wherever refreshCompose is called (including the update flow around
updateContainerImages and the other call sites referenced), so refreshCompose
treats force=true as a hard rebuild of compose.yml rather than a no-op; remove
the implicit reliance on state.ImageTag="" for this behavior and ensure callers
that receive the recovered value from checkInstallationHealth forward it to
refreshCompose.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 14c2c7e5-13b7-40fb-b931-3bf8c6e099a7
📒 Files selected for processing (8)
cli/cmd/update.gocli/cmd/update_cleanup.gocli/cmd/update_health.gosrc/synthorg/api/controllers/providers.pysrc/synthorg/providers/discovery.pysrc/synthorg/providers/management/service.pysrc/synthorg/providers/probing.pytests/unit/providers/test_discovery.py
| if state.JWTSecret == "" { | ||
| issues = append(issues, "JWT secret is not configured") | ||
| } | ||
| if state.SettingsKey == "" { | ||
| issues = append(issues, "settings encryption key is not configured") | ||
| } |
There was a problem hiding this comment.
Don't offer “recover” for a missing JWTSecret.
Lines 51-53 flag this as an installation issue, but the recovery path only regenerates compose/images and migrateSettingsKey() only repairs SettingsKey. If the user accepts recovery here, the command still leaves an unusable config. Either synthesize and persist the JWT secret before prompting, or route this case straight to synthorg init.
Also applies to: 86-107
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cli/cmd/update_health.go` around lines 51 - 56, The code currently treats a
missing state.JWTSecret as recoverable but only offers the existing recovery
path which regenerates images/compose and calls migrateSettingsKey() (which only
repairs state.SettingsKey), leaving the installation unusable; fix by either
(preferred) synthesizing and persisting a new JWT secret before offering
recovery (e.g., generate a secure secret, assign to state.JWTSecret and persist
the state/save function used elsewhere) so subsequent recovery works, or skip
the in-place "recover" option for the JWT secret and route this case to the
interactive synthorg init flow instead; also ensure similar handling is applied
where JWTSecret is checked in the later block (lines ~86-107) so you don't
present an inappropriate recover option.
Go: restartIfRunning returns (bool, error) -- cleanup only runs when restart succeeded and health check passed. refreshCompose takes force parameter for recovery mode (generates missing compose.yml from template). Remove migrateSettingsKey (no migration code). Python: extract _redact_url into shared url_utils.py, remove duplicate implementations from discovery.py and probing.py. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/synthorg/providers/probing.py (1)
164-182:⚠️ Potential issue | 🟠 MajorTreat all-malformed model lists as a probe miss.
This has the same failure mode that was already fixed in discovery:
_parse_ollama_models()/_parse_standard_models()return(), notNone, for payloads like{"models": [42]}or{"data": [42]}._build_probe_hit()therefore accepts a garbage response as a successful probe withmodel_count=0and stops before trying the next candidate.🛠️ Suggested fix
def _build_probe_hit( @@ Returns: Probe result on success, ``None`` if the payload is not a recognizable model-listing response. """ if preset_name == "ollama": + raw_entries = data.get("models", []) models = _parse_ollama_models(data) else: + raw_entries = data.get("data", []) models = _parse_standard_models(data) if models is None: _log_probe_miss(preset_name, "unrecognized_schema", url) return None + if isinstance(raw_entries, list) and raw_entries and not models: + _log_probe_miss(preset_name, "all_entries_malformed", url) + return None logger.info( PROVIDER_PROBE_HIT, preset=preset_name, url=_redact_url(url),🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/synthorg/providers/probing.py` around lines 164 - 182, The code only treats None as a probe miss but _parse_ollama_models/_parse_standard_models can return an empty tuple or empty list for malformed payloads, so change the check from "if models is None" to a falsy/emptiness check (e.g., "if not models") so that empty/invalid model lists are treated as probe misses; keep using _log_probe_miss(preset_name, "unrecognized_schema", url) and return None, and ensure the subsequent ProbeResult(...) only runs when models is a non-empty sequence (referencing preset_name, _parse_ollama_models, _parse_standard_models, models, _log_probe_miss, url, idx, and ProbeResult).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cli/cmd/update.go`:
- Around line 250-263: When existing == nil and force recovery is requested, the
code assumes fresh contains generated content but loadAndGenerate currently
returns (nil,nil,nil) for a missing compose.yml; update the recovery branch in
the function containing the existing == nil check to explicitly generate the
compose content (call the same generator used by loadAndGenerate or invoke
loadAndGenerate in a mode that forces generation) to populate fresh before
calling atomicWriteFile(composePath, fresh, safeDir), handle and wrap any
generation errors, and then write and log the new compose.yml; ensure you
reference loadAndGenerate (or the underlying generator), composePath, fresh,
atomicWriteFile, and the force flag when implementing the fix so recovery
succeeds when compose.yml is absent.
In `@src/synthorg/providers/url_utils.py`:
- Around line 16-21: The redact_url() implementation accesses parsed.port
directly (can raise ValueError for malformed ports) and rebuilds IPv6 addresses
without brackets, causing exceptions during logging; fix it by guarding
parsed.port in a try/except (treat invalid/ValueError ports as absent), build
safe_netloc from parsed.hostname (or empty) and if hostname contains ':' wrap it
in brackets before appending a valid port, set query to "<redacted>" when
present, and return the rebuilt URL; update the logic inside redact_url() (refer
to parsed, parsed.port, parsed.hostname, safe_netloc and
urlunparse(parsed._replace(...))) so malformed ports are ignored and IPv6
literals are bracketed to avoid the unhandled exception during
_fetch_json()/discover_models() logging.
---
Duplicate comments:
In `@src/synthorg/providers/probing.py`:
- Around line 164-182: The code only treats None as a probe miss but
_parse_ollama_models/_parse_standard_models can return an empty tuple or empty
list for malformed payloads, so change the check from "if models is None" to a
falsy/emptiness check (e.g., "if not models") so that empty/invalid model lists
are treated as probe misses; keep using _log_probe_miss(preset_name,
"unrecognized_schema", url) and return None, and ensure the subsequent
ProbeResult(...) only runs when models is a non-empty sequence (referencing
preset_name, _parse_ollama_models, _parse_standard_models, models,
_log_probe_miss, url, idx, and ProbeResult).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 0a9c2ec9-440f-43d3-9af2-f61efcc4015d
📒 Files selected for processing (4)
cli/cmd/update.gosrc/synthorg/providers/discovery.pysrc/synthorg/providers/probing.pysrc/synthorg/providers/url_utils.py
Go: refreshCompose recovery explicitly generates compose from template when compose.yml is missing (loadAndGenerate returns nil for absent files). Python: redact_url handles IPv6 literals (brackets) and malformed ports (try/except ValueError). _build_probe_hit uses falsy check instead of None check to catch empty model tuples from valid-schema-but-all-malformed payloads. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
♻️ Duplicate comments (1)
cli/cmd/update.go (1)
54-75:⚠️ Potential issue | 🟠 MajorDon’t clear
state.ImageTagbeforerefreshCompose().Line 68 repurposes
state.ImageTagas a force-update flag, but Line 74 passes that samestateintorefreshCompose(), which regeneratescompose.ymlfromcompose.ParamsFromState(state). On installs without persisted digest pins, recovery can now render empty image refs or fail compose generation entirely, so the same-version dirty-state path still can’t self-heal. Keep the original tag for compose generation and pass an explicitforceImageRefreshflag intoupdateContainerImages()instead.🐛 Suggested direction
- if recovered { - // Clear stored tag so updateContainerImages does not short-circuit - // when the CLI version matches the stored tag but images are gone. - state.ImageTag = "" - } + forceImageRefresh := recovered ... - return updateContainerImages(cmd, state, true) + return updateContainerImages(cmd, state, true, forceImageRefresh) ... - return updateContainerImages(cmd, state, false) + return updateContainerImages(cmd, state, false, forceImageRefresh)Then make
updateContainerImages()skip thestate.ImageTag == tagearly return only whenforceImageRefreshis true.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@cli/cmd/update.go` around lines 54 - 75, Do not clear state.ImageTag before calling refreshCompose(cmd, state, recovered); restore the original tag for compose generation so compose.ParamsFromState(state) can render valid image refs. Instead, add a boolean parameter (e.g., forceImageRefresh) to updateContainerImages(...) and, when recovered is true, call updateContainerImages(..., forceImageRefresh=true). Modify updateContainerImages to skip its early-return that compares state.ImageTag == tag only when forceImageRefresh is true; you can still clear or reset state.ImageTag after refreshCompose if you must preserve the “force” semantics for later steps.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In `@cli/cmd/update.go`:
- Around line 54-75: Do not clear state.ImageTag before calling
refreshCompose(cmd, state, recovered); restore the original tag for compose
generation so compose.ParamsFromState(state) can render valid image refs.
Instead, add a boolean parameter (e.g., forceImageRefresh) to
updateContainerImages(...) and, when recovered is true, call
updateContainerImages(..., forceImageRefresh=true). Modify updateContainerImages
to skip its early-return that compares state.ImageTag == tag only when
forceImageRefresh is true; you can still clear or reset state.ImageTag after
refreshCompose if you must preserve the “force” semantics for later steps.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI (base), Organization UI (inherited)
Review profile: ASSERTIVE
Plan: Pro
Run ID: 37b08046-ff67-403d-85d3-b6b3a86005c9
📒 Files selected for processing (3)
cli/cmd/update.gosrc/synthorg/providers/probing.pysrc/synthorg/providers/url_utils.py
…mageTag Clearing state.ImageTag before refreshCompose broke compose generation (empty image refs). Instead, pass a forceRefresh flag to updateContainerImages to skip the version-match early return during recovery, preserving valid state for compose. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
🤖 I have created a release *beep* *boop* --- ## [0.4.1](v0.4.0...v0.4.1) (2026-03-20) ### Bug Fixes * CLI update resilience + setup wizard UX overhaul ([#642](#642)) ([774e4e4](774e4e4)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
Summary
updatecommand: Add dirty state detection (missing compose, images, config, secrets) with interactive recovery prompt after partial uninstall. Add old image cleanup after successful upgrade -- identifies non-current images by Docker image ID (handles tagged + digest-pinned refs), validates IDs beforedocker rmi./setup/status. Completed steps show read-only summaries (admin locked, provider/company/agent editable). Free navigation to any completed step. Provider: "Change Provider" button + proper internal back navigation. Password: show/hide toggles + autocomplete fixes.trust_urlbypass for preset-originated and no-auth provider URLs. Validatedpreset_hintagainst preset registry to prevent arbitrary SSRF bypass. SSRF bypass logged at WARNING level for security monitoring.config.State.validate()prevents glob injection via crafted config. Docker image ID format validation beforedocker rmi. User-suppliedbase_urloverrides go through normal SSRF validation (only preset defaults are trusted).Test plan
go build ./...+go vet ./...+go test ./...-- all passruff check+ruff format+mypy+pytest(9713 tests) -- all passtype-check+lint+test(723 tests) -- all passReview coverage
Pre-reviewed by 14 specialized agents: Go reviewer, Security reviewer, Frontend reviewer, Python reviewer, Docs consistency, Issue resolution verifier, Silent failure hunter, Conventions enforcer, Go conventions, Logging audit, Test quality analyzer, Async concurrency reviewer, Resilience audit, Go security reviewer.
18 findings identified and addressed (6 Critical, 7 Major, 5 Medium). Key fixes:
preset_hintagainst registry; only trust preset default URLsisComplete, store action for step mutation, stale cache fixCloses #641