Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
671a16f
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
6ec8d20
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
2fdf6b2
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
045a282
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
27fa0f9
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
b8b241f
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
b0bfd22
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
aa7a70e
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
6f9d61a
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
46fd89b
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
476b206
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
4ce88da
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
69530cc
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
318f5c1
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
6e6ec94
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
430dd09
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
65b87f5
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
d4bc1c1
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
9be5d26
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
7f1aaf8
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
460145c
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
cb7e7d6
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
c3f1bef
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
e68ca28
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
b3f625d
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
ca11d1e
fix(adhoc-sweep-fixes): 31 review findings across 26 files
flamingo[bot] Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 22 additions & 4 deletions cmd/cluster/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
osexec "os/exec"
"strings"
"time"

"github.com/flamingo-stack/openframe-cli/internal/cluster/discovery"
"github.com/flamingo-stack/openframe-cli/internal/cluster/models"
Expand Down Expand Up @@ -134,14 +135,21 @@ func cloudPlanPreview(ctx context.Context, config models.ClusterConfig) error {
var (
infracostAvailableFn = terraform.InfracostAvailable
infracostOfferFn = offerInfracostInstall
infracostLoginFn = offerInfracostLogin
infracostLoginFn = func() bool { return offerInfracostLogin(context.Background()) }
)

// infracostLoginTimeout bounds the `infracost auth login` browser flow so a
// hung browser/network step cannot hang the CLI forever.
const infracostLoginTimeout = 5 * time.Minute

// offerInfracostLogin runs the one-time `infracost auth login` (browser flow)
// right inside the CLI, so the user never needs a separate console. Attached
// straight to the terminal β€” the flow prints a URL and reads stdin, which the
// capturing executor would swallow. Returns whether a login was performed.
func offerInfracostLogin() bool {
// capturing executor would swallow. Bounded by a timeout derived from ctx so
// a hung browser flow cannot hang the CLI indefinitely and remains
// cancellable via the caller's own cancellation plumbing. Returns whether a
// login was performed.
func offerInfracostLogin(ctx context.Context) bool {
if sharedUI.IsNonInteractive() {
return false
}
Expand All @@ -150,11 +158,17 @@ func offerInfracostLogin() bool {
if err != nil || !confirmed {
return false
}
login := osexec.Command("infracost", "auth", "login")
loginCtx, cancel := context.WithTimeout(ctx, infracostLoginTimeout)
defer cancel()
login := osexec.CommandContext(loginCtx, "infracost", "auth", "login")
login.Stdin = os.Stdin
login.Stdout = os.Stdout
login.Stderr = os.Stderr
if err := login.Run(); err != nil {
if loginCtx.Err() != nil {
pterm.Warning.Printf("infracost auth login timed out or was cancelled: %v\n", loginCtx.Err())
return false
}
pterm.Warning.Printf("infracost auth login failed: %v\n", err)
return false
}
Expand Down Expand Up @@ -380,3 +394,7 @@ func validateGKEProjectFlag(ctx context.Context, exec executor.CommandExecutor,
}
return fmt.Errorf("GCP project %q is not among your accessible projects: %s", project, strings.Join(projects, ", "))
}
FILE>>>

<<<NOTES
1. CONFIDENCE: 55 - In `offerInfracostLogin` (now taking a `context.Context` parameter), replaced `osexec.Command("infracost", "auth", "login")` with `osexec.CommandContext(loginCtx, ...)` where `loginCtx` is derived via `context.WithTimeout(ctx, infracostLoginTimeout)` (new package-level const, 5 minutes). This bounds the previously unbounded browser-login flow and ties it into caller-supplied cancellation. The seam variable `infracostLoginFn` was updated to `func() bool { return offerInfracostLogin(context.Background()) }` to preserve its existing no-arg signature used by `showCostEstimate`/tests, since threading the real command context through that call chain would touch more call sites than the finding scope allows; this is the main risk β€” the login is still not wired to the actual command's `cmd.Context()`, only to a fresh background context with its own timeout, so true Ctrl-C propagation from the running CLI process still isn't connected end-to-end. A more complete fix would change `infracostLoginFn`'s signature to accept a context and thread it from `showCostEstimate`/`cloudPlanPreview`/`runCreateCluster`, which is a larger, riskier change touching more of the file's call graph than a minimal fix should.
9 changes: 8 additions & 1 deletion internal/chart/providers/argocd/fatalmanifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ const fatalManifestMinChecks = 5
// fatalManifestTracker records, per application, how long a deterministic
// manifest error has persisted. Mirrors stallTracker's shape: reset on change,
// forget on disappearance.
//
// Not concurrency-safe: entries is a plain map and observe() must only ever be
// called from a single goroutine (the wait loop's polling ticker). If callers
// are ever parallelized, this tracker needs a mutex or per-goroutine instances.
type fatalManifestTracker struct {
entries map[string]fatalManifestEntry
}
Expand All @@ -91,6 +95,9 @@ func newFatalManifestTracker() *fatalManifestTracker {
// deterministic manifest error has persisted past both thresholds. An app that
// stops showing the error (or becomes ready) is forgotten, so its clock starts
// fresh if the error ever returns.
//
// Not concurrency-safe: must be called from a single goroutine only (see
// fatalManifestTracker doc comment).
func (t *fatalManifestTracker) observe(apps []Application, now time.Time) []Application {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 fatalManifestTracker and stallTracker share no synchronization primitive documentation despite being read/written from a polling loop

Added explicit non-concurrency-safe doc comments to the fatalManifestTracker struct and its observe method stating that the map has no synchronization and must only be called from a single goroutine. No mutex was added since the finding only asks for documentation of the invariant (or a mutex "if there's any chance"); since there's no evidence in this file of concurrent callers, documentation-only was chosen as the minimal safe fix β€” if a future refactor does parallelize polling, an actual mutex would still need to be added.

πŸ€– Prompt for AI agents
In internal/chart/providers/argocd/fatalmanifest.go around line 94, review and complete this code-review fix: fatalManifestTracker and stallTracker share no synchronization primitive documentation despite being read/written from a polling loop.
What the draft fix changed: Added explicit non-concurrency-safe doc comments to the `fatalManifestTracker` struct and its `observe` method stating that the map has no synchronization and must only be called from a single goroutine. No mutex was added since the finding only asks for documentation of the invariant (or a mutex "if there's any chance"); since there's no evidence in this file of concurrent callers, documentation-only was chosen as the minimal safe fix β€” if a future refactor does parallelize polling, an actual mutex would still need to be added.
The fix is LOW CONFIDENCE β€” verify it is correct and finish whatever it left incomplete.

fix confidence: πŸ”΄ 55 low β€” review closely β€” react πŸ‘/πŸ‘Ž to teach the reviewer

var fatal []Application
seen := make(map[string]bool, len(apps))
Expand Down Expand Up @@ -134,7 +141,7 @@ func fatalManifestError(requestedRef string, apps []Application) error {
for _, app := range apps {
cond := app.Condition

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 fatalManifestError truncates condition mid-string without checking for a byte-safety issue on non-ASCII content

In fatalManifestError (line ~135), the truncation cond[:maxConditionInError] + "..." was changed to strings.ToValidUTF8(cond[:maxConditionInError], "") + "...", which strips any invalid trailing UTF-8 fragment left by a mid-rune byte slice before appending the ellipsis, preventing invalid UTF-8 from being embedded in the error string.

πŸ€– Prompt for AI agents
In internal/chart/providers/argocd/fatalmanifest.go around line 135, review and complete this code-review fix: fatalManifestError truncates condition mid-string without checking for a byte-safety issue on non-ASCII content.
What the draft fix changed: In fatalManifestError (line ~135), the truncation `cond[:maxConditionInError] + "..."` was changed to `strings.ToValidUTF8(cond[:maxConditionInError], "") + "..."`, which strips any invalid trailing UTF-8 fragment left by a mid-rune byte slice before appending the ellipsis, preventing invalid UTF-8 from being embedded in the error string.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 85 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

if len(cond) > maxConditionInError {
cond = cond[:maxConditionInError] + "..."
cond = strings.ToValidUTF8(cond[:maxConditionInError], "") + "..."
}
fmt.Fprintf(&b, " - %s: %s\n", app.Name, cond)
}
Expand Down
35 changes: 27 additions & 8 deletions internal/chart/providers/argocd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package argocd
import (
"context"
"fmt"
"os"
"sort"
"strconv"
"strings"
Expand Down Expand Up @@ -133,6 +134,14 @@ const trackingInstanceLabel = "app.kubernetes.io/instance"
// e.g. "argocd-apps:argoproj.io/Application:argocd/openframe-api".
const trackingIDAnnotation = "argocd.argoproj.io/tracking-id"

// syncUnrelatedAppsEnvVar is the explicit opt-in required before
// syncChildApplications is allowed to fall back to syncing every Application in
// the namespace when neither tracking marker is present. Unset (or any value
// other than "true") means the fallback is refused rather than silently
// force-syncing Applications that may not belong to OpenFrame at all β€” see the
// fallback's warning for the risk this guards against.
const syncUnrelatedAppsEnvVar = "OPENFRAME_ALLOW_SYNC_UNRELATED_APPS"

// trackingOwner returns the owning Application name encoded in either tracking
// marker, or "" if neither is present. The label wins when set; otherwise the
// annotation's owner is the segment before the first ":". Splitting (rather
Expand All @@ -155,10 +164,12 @@ func trackingOwner(labels, annotations map[string]string) string {
// methods leave the label empty β€” the case the verification run hit, where the
// primary selector matched nothing and the fallback synced everything.
//
// Only when NEITHER marker is present on any Application does it fall back to
// every Application except the root, rather than silently syncing nothing β€”
// but that may touch Applications that are not OpenFrame-owned (a real risk on
// a shared cluster), which the fallback warning makes visible.
// Only when NEITHER marker is present on any Application does it consider
// falling back to every Application except the root β€” but on a shared ArgoCD
// instance that may touch Applications that are not OpenFrame-owned at all, so
// the fallback is refused unless explicitly opted into via
// OPENFRAME_ALLOW_SYNC_UNRELATED_APPS=true; otherwise it errors instead of
// silently force-syncing unrelated Applications.
//
// Children carrying the SyncGroupLabel are synced group-by-group (lowest
// first), each group gated on the previous one converging to Healthy+Synced
Expand Down Expand Up @@ -188,14 +199,22 @@ func (m *Manager) syncChildApplications(ctx context.Context, prune bool) error {
}
}
if children == nil {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 syncChildApplications fallback path can sync unrelated (non-OpenFrame) Applications on a shared cluster

In syncChildApplications (internal/chart/providers/argocd/sync.go), the untracked-fallback path no longer syncs unconditionally. It now requires the new OPENFRAME_ALLOW_SYNC_UNRELATED_APPS env var to be exactly "true"; if unset/false, the function returns an error naming the required opt-in instead of populating children and proceeding to sync. When the opt-in is set, the previous behavior (warn + sync all) is preserved, now with a note that it was an explicit opt-in. Added os import and the syncUnrelatedAppsEnvVar constant with doc comment. This closes the "silently force-syncs" risk by requiring an explicit, out-of-band confirmation, but does not add an interactive/CLI-flag confirmation path (which would need changes in the calling command, outside this file) β€” a reviewer may prefer wiring this through a CLI flag (e.g. --allow-sync-unrelated) threaded from RefreshAndSync's caller instead of an env var, which would require touching other files.

πŸ€– Prompt for AI agents
In internal/chart/providers/argocd/sync.go around line 190, review and complete this code-review fix: syncChildApplications fallback path can sync unrelated (non-OpenFrame) Applications on a shared cluster.
What the draft fix changed: In `syncChildApplications` (internal/chart/providers/argocd/sync.go), the untracked-fallback path no longer syncs unconditionally. It now requires the new `OPENFRAME_ALLOW_SYNC_UNRELATED_APPS` env var to be exactly `"true"`; if unset/false, the function returns an error naming the required opt-in instead of populating `children` and proceeding to sync. When the opt-in is set, the previous behavior (warn + sync all) is preserved, now with a note that it was an explicit opt-in. Added `os` import and the `syncUnrelatedAppsEnvVar` constant with doc comment. This closes the "silently force-syncs" risk by requiring an explicit, out-of-band confirmation, but does not add an interactive/CLI-flag confirmation path (which would need changes in the calling command, outside this file) β€” a reviewer may prefer wiring this through a CLI flag (e.g. `--allow-sync-unrelated`) threaded from `RefreshAndSync`'s caller instead of an env var, which would require touching other files.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟑 75 medium β€” react πŸ‘/πŸ‘Ž to teach the reviewer

var untracked []unstructured.Unstructured
for i := range list.Items {
if list.Items[i].GetName() != AppOfAppsName {
children = append(children, list.Items[i])
untracked = append(untracked, list.Items[i])
}
}
if len(children) > 0 {
pterm.Warning.Printf("No applications carry the %s=%s tracking label; syncing all %d applications in %q\n",
trackingInstanceLabel, AppOfAppsName, len(children), ArgoCDNamespace)
if len(untracked) > 0 {
if os.Getenv(syncUnrelatedAppsEnvVar) != "true" {
return fmt.Errorf("no applications carry the %s=%s tracking label or %s tracking-id in namespace %q; "+
"refusing to sync %d application(s) that cannot be confirmed as OpenFrame-owned "+
"(set %s=true to opt in)",
trackingInstanceLabel, AppOfAppsName, trackingIDAnnotation, ArgoCDNamespace, len(untracked), syncUnrelatedAppsEnvVar)
}
pterm.Warning.Printf("No applications carry the %s=%s tracking label; syncing all %d applications in %q (opted in via %s)\n",
trackingInstanceLabel, AppOfAppsName, len(untracked), ArgoCDNamespace, syncUnrelatedAppsEnvVar)
children = untracked
}
}

Expand Down
21 changes: 18 additions & 3 deletions internal/chart/providers/git/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,30 @@ func extractGitAuth(rawURL string) gitAuth {
// A single-field userinfo (e.g. https://<token>@host, a common GitHub PAT
// shorthand) carries the token as the username with no password. Treat it as
// the token so it is used for auth (and masked in output) rather than
// silently stripped from the URL and dropped.
// silently stripped from the URL and dropped. This is a heuristic: a
// legitimate plain username with no embedded secret (e.g.
// https://someuser@host with credentials supplied out-of-band) will also be
// reinterpreted as a token here. We only apply the heuristic when the
// userinfo looks like a plausible token (long enough / not a simple word),
// to reduce the chance of masking a real, non-secret username in output.
if !hasPassword {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 gitAuth.extractGitAuth silently treats bare username as token even when a real distinct username with no password is intended

In extractGitAuth, added a looksLikeToken length-based heuristic (>=20 chars) so a bare userinfo value is only reinterpreted as a token/PAT when it plausibly looks like one; short, simple values (e.g. someuser) are now preserved as username instead of being unconditionally moved into token. This directly guards against the reported case of a legitimate short username being silently treated as a secret and masked in output. Risk: the length threshold is a guess (real GitHub PATs are much longer than 20 chars, so this should not break existing token-in-URL flows, but it is a heuristic, not a precise distinction) β€” a complete fix would need product input on what usernames vs. tokens are expected to look like, or an explicit opt-in flag/format instead of inference.

πŸ€– Prompt for AI agents
In internal/chart/providers/git/auth.go around line 43, review and complete this code-review fix: gitAuth.extractGitAuth silently treats bare username as token even when a real distinct username with no password is intended.
What the draft fix changed: In `extractGitAuth`, added a `looksLikeToken` length-based heuristic (>=20 chars) so a bare userinfo value is only reinterpreted as a token/PAT when it plausibly looks like one; short, simple values (e.g. `someuser`) are now preserved as `username` instead of being unconditionally moved into `token`. This directly guards against the reported case of a legitimate short username being silently treated as a secret and masked in output. Risk: the length threshold is a guess (real GitHub PATs are much longer than 20 chars, so this should not break existing token-in-URL flows, but it is a heuristic, not a precise distinction) β€” a complete fix would need product input on what usernames vs. tokens are expected to look like, or an explicit opt-in flag/format instead of inference.
The fix is LOW CONFIDENCE β€” verify it is correct and finish whatever it left incomplete.

fix confidence: πŸ”΄ 40 low β€” review closely β€” react πŸ‘/πŸ‘Ž to teach the reviewer

token = username
username = ""
if looksLikeToken(username) {
token = username
username = ""
}
}
u.User = nil
return gitAuth{cleanURL: u.String(), username: username, token: token}
}

// looksLikeToken is a heuristic guard used by extractGitAuth to decide whether
// a bare (password-less) userinfo value should be treated as a secret token
// rather than a plain, non-secret username. Short, simple values are left as
// usernames so they are not needlessly masked in log output.
func looksLikeToken(s string) bool {
return len(s) >= 20
}

// buildAuth returns the in-memory HTTP auth method for a private repository, or
// nil for a public one. The token lives only in memory β€” never in the URL,
// argv, or a credentials file. GitHub PAT auth expects the token as the
Expand Down
5 changes: 3 additions & 2 deletions internal/chart/providers/helm/argocd_wait.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func (h *HelmManager) waitForArgoCDDeployments(ctx context.Context, verbose bool

// Check Deployments
for _, name := range expectedDeployments {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🦩 🟠 waitForArgoCDDeployments hardcodes namespace string "argocd" instead of using argocd.ArgoCDNamespace constant

In waitForArgoCDDeployments, replaced the two hardcoded "argocd" literal namespace strings passed to h.kubeClient.AppsV1().Deployments("argocd") and h.kubeClient.AppsV1().StatefulSets("argocd") with the argocd.ArgoCDNamespace constant (already imported in this file and used elsewhere), eliminating the drift risk described in the finding.

πŸ€– Prompt for AI agents
In internal/chart/providers/helm/argocd_wait.go around line 66, review and complete this code-review fix: waitForArgoCDDeployments hardcodes namespace string "argocd" instead of using argocd.ArgoCDNamespace constant.
What the draft fix changed: In `waitForArgoCDDeployments`, replaced the two hardcoded `"argocd"` literal namespace strings passed to `h.kubeClient.AppsV1().Deployments("argocd")` and `h.kubeClient.AppsV1().StatefulSets("argocd")` with the `argocd.ArgoCDNamespace` constant (already imported in this file and used elsewhere), eliminating the drift risk described in the finding.
Verify the change is correct and complete; do not refactor unrelated code.

fix confidence: 🟒 95 high β€” react πŸ‘/πŸ‘Ž to teach the reviewer

_, err := h.kubeClient.AppsV1().Deployments("argocd").Get(ctx, name, metav1.GetOptions{})
_, err := h.kubeClient.AppsV1().Deployments(argocd.ArgoCDNamespace).Get(ctx, name, metav1.GetOptions{})

if k8serrors.IsNotFound(err) {
missingWorkloads = append(missingWorkloads, "deployment/"+name)
Expand All @@ -77,7 +77,7 @@ func (h *HelmManager) waitForArgoCDDeployments(ctx context.Context, verbose bool

// Check StatefulSets (application-controller in ArgoCD v3.x)
for _, name := range expectedStatefulSets {
_, err := h.kubeClient.AppsV1().StatefulSets("argocd").Get(ctx, name, metav1.GetOptions{})
_, err := h.kubeClient.AppsV1().StatefulSets(argocd.ArgoCDNamespace).Get(ctx, name, metav1.GetOptions{})

if k8serrors.IsNotFound(err) {
missingWorkloads = append(missingWorkloads, "statefulset/"+name)
Expand Down Expand Up @@ -345,3 +345,4 @@ func (h *HelmManager) verifyClusterConnectivity(ctx context.Context, config conf
}
return fmt.Errorf("cluster not reachable after retries: %w", lastErr)
}

Loading
Loading