feat: replace CLI remote nodes with contexts#1949
Conversation
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughReplaces Dagu's remote-node resolution system with a CLI-context-based remote execution model. Introduces a new persistent Changes
Sequence Diagram(s)sequenceDiagram
participant CLI as CLI User
participant CmdHandler as Command Handler<br/>(start/stop/status/etc)
participant ContextMgr as Context Manager<br/>(NewContext)
participant ContextStore as Context Store
participant RemoteClient as Remote Client
participant RemoteServer as Remote Dagu Server
CLI->>CmdHandler: Run command with --context flag
CmdHandler->>ContextMgr: NewContext(cmd, args, flags)
ContextMgr->>ContextStore: Initialize store from config
ContextStore-->>ContextMgr: Store ready (or nil if unavailable)
ContextMgr->>ContextStore: Resolve requested/current context
ContextStore-->>ContextMgr: CLI context details
alt Non-local context selected
ContextMgr->>RemoteClient: newRemoteClient(clicontext)
RemoteClient->>RemoteClient: Set base URL, API key, timeout
RemoteClient-->>ContextMgr: Remote client initialized
ContextMgr->>CmdHandler: Return Context{IsRemote: true, Remote: client}
CmdHandler->>CmdHandler: Detect ctx.IsRemote()
CmdHandler->>RemoteClient: remoteRunStart/Stop/Status/etc(ctx, args)
RemoteClient->>RemoteServer: HTTP request with Bearer auth
RemoteServer-->>RemoteClient: API response (JSON)
RemoteClient->>RemoteClient: Decode/map to internal format
RemoteClient-->>CmdHandler: Result
CmdHandler-->>CLI: Output/status
else Local context or fallback
ContextMgr-->>CmdHandler: Return Context{IsRemote: false}
CmdHandler->>CmdHandler: Execute local logic
CmdHandler-->>CLI: Output/status
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🧹 Nitpick comments (2)
internal/clicontext/store_test.go (1)
69-109: Add regressions for the validation edge-cases.The table doesn't cover
Name: "current"or whitespace-padded values, so the reserved-marker and normalization cases inStorecan regress without failing this file.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/clicontext/store_test.go` around lines 69 - 109, Add test cases to the tests slice in store_test.go to cover the reserved name "current" and inputs with leading/trailing whitespace so Store's reserved-marker logic and normalization can't regress unnoticed; specifically add a case where Context{Name: "current", ServerURL: "https://example.com", APIKey: "dagu_test"} expects the same reserved-name error as LocalContextName, and cases with whitespace-padded fields (e.g., Name: " prod ", ServerURL: " https://example.com ", APIKey: " dagu_test ") expecting validation to trim/normalize and either pass or produce the same validation errors (e.g., invalid URL or api key prefix) so the behavior in Store/Context validation is fully covered.internal/agent/api.go (1)
573-596: CentralizeSessionManagerConfigassembly.These three literals now need the same session wiring kept in sync, and this PR already had to touch all of them for
RemoteContextResolver. A small builder/helper would make the next session option much harder to miss in one path.Also applies to: 935-962, 1146-1167
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@internal/agent/api.go` around lines 573 - 596, The repeated inline construction of SessionManagerConfig in newManagedSession (and the other SessionManagerConfig call sites) risks divergence when new fields like RemoteContextResolver are added; create a single helper function (e.g., buildSessionManagerConfig or NewSessionManagerConfigFromRuntime) that accepts the common inputs (ctx, id, user, cfg, now, and receiver *API) and returns a fully populated SessionManagerConfig, then replace direct SessionManagerConfig literals in newManagedSession and the other SessionManagerConfig call sites with calls to this helper and pass its result into NewSessionManager to ensure all fields stay centralized and in sync.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@internal/agent/remote_agent.go`:
- Around line 166-177: The current handler collapses all resolver.GetByName
errors into "Unknown context" whenever resolver.ListRemoteContexts succeeds,
losing real configuration errors; change the logic in the GetByName error branch
so that you only return the "Unknown context %q. Available contexts: ..."
toolError when the original error actually means "not found" (e.g., compare with
a sentinel like resolver.ErrNotFound or use errors.Is/As on the error returned
by resolver.GetByName); for any other error from resolver.GetByName, return the
original failure via toolError("Failed to resolve context %q: %v", args.Context,
err) so configuration/permission errors are preserved (update the branch that
calls resolver.ListRemoteContexts and returns toolError accordingly, keeping
symbols resolver.GetByName, resolver.ListRemoteContexts, args.Context,
ctx.Context, and toolError).
In `@internal/clicontext/store.go`:
- Around line 211-214: In the List method loop, skipping entries on
read/decrypt/unmarshal failure (using s.readContext(filepath.Join(s.baseDir,
entry.Name())) and simply continue) causes List() to return a silently partial
result; change the behavior to record and return an error when any entry fails:
capture the filename (entry.Name()) and the underlying error from readContext,
aggregate failures (or return the first encountered error) and ensure List()
returns a non-nil error alongside any partial results so callers (CLI) can
detect corrupt store or wrong key rather than seeing a silently missing context.
- Around line 107-120: ValidateContext currently trims Name and ServerURL only
for validation, but Create/Update still persist the original struct; update the
code so the normalized values are saved. Either have ValidateContext mutate the
passed-in context (trimmed Name, trimmed ServerURL, and similarly trimmed APIKey
if you want) or add a small helper normalizeContext(ctx *Context) called from
Create, Update, and ValidateContext to set ctx.Name =
strings.TrimSpace(ctx.Name), ctx.ServerURL = strings.TrimSpace(ctx.ServerURL),
and ctx.APIKey = strings.TrimSpace(ctx.APIKey) before deriving filenames or
JSON; ensure Create and Update use the normalized ctx when computing filenames
and writing the file so trailing whitespace cannot be persisted (refer to
functions ValidateContext, Create, Update and the Context struct).
- Around line 111-115: ValidateContext currently only rejects LocalContextName
("local") and only checks os.PathSeparator; update it to also reserve "current"
(so names "local" and "current" are rejected) and to reject both forward and
backward slashes by checking for '/' and '\\' (e.g. via strings.ContainsAny) so
names like "prod/east" fail on all platforms; mention the reserved names in the
error message and keep List()'s skip of "current.json" and contextPath
concatenation behavior consistent with this validation (refer to
LocalContextName, the ValidateContext function, List, and contextPath in
store.go).
In `@internal/cmd/context.go`:
- Around line 160-163: NewContext currently inspects the leaf cobra command name
(via cmd.Name()) so it sees "list"/"add"/"use" instead of the "context" family;
change the detection to walk the parent chain (using cmd.Parent() repeatedly)
and check ancestor.Name() == "context" (or otherwise derive the command family
from the ancestor chain) before calling newCLIContextStore or scopeForCommand;
update the same parent-chain check in the other similar blocks mentioned (the
other NewContext usages and any checks that dereference ContextStore in
context_command.go) so newCLIContextStore is only invoked when the command is
actually in the "context" family and ContextStore cannot be nil.
In `@internal/cmd/flags.go`:
- Around line 52-55: The package-level variable contextFlag (type
commandLineFlag) is unused and triggering golangci-lint; either remove this dead
variable or wire it up by registering a persistent --context flag using the
shared contextFlag descriptor. If you choose to wire it, hook into your
global/root flag setup (e.g., in the root command init or the function that
registers global flags) and use contextFlag.name and contextFlag.usage to create
the persistent flag (matching how other commandLineFlag descriptors are
registered), ensuring the flag value is stored where the CLI reads it; otherwise
delete the contextFlag definition to satisfy the linter.
In `@internal/cmd/history.go`:
- Around line 81-82: The remote path returns a truncated exec.DAGRunStatus to
the shared renderer so fields emitted by renderHistoryJSON (Tags, WorkerID,
Error) are missing for remote contexts; update the remote adapter
(remoteRunHistory and the code that prepares the DAGRunStatus in
remote_commands.go) to populate Tags, WorkerID and Error on the
exec.DAGRunStatus objects it constructs before calling the shared renderer (the
same structure renderHistoryJSON expects) so that remote `history --format json`
output matches local output.
In `@internal/cmd/remote_client.go`:
- Around line 69-74: The http.Client creation currently builds a new
http.Transport which drops defaults like ProxyFromEnvironment; instead clone the
default transport by type-asserting http.DefaultTransport to *http.Transport,
make a shallow copy, set the TLSClientConfig.InsecureSkipVerify to
ctx.SkipTLSVerify, and use that copy as the Transport for the client (refer to
the client instantiation, http.Client and TLSClientConfig usage in
remote_client.go). Ensure you preserve other default fields (Proxy, DialContext,
IdleConnTimeout, etc.) by copying the default transport rather than creating a
fresh one.
In `@internal/cmd/remote_commands_test.go`:
- Around line 27-29: The test currently uses invalid new() calls for string
pointers; update the Params and WorkerId fields (which are *string) to provide
proper *string values — e.g., create a small helper (like stringPtr) or temp
variables and assign pointers to them—so replace new("P1=foo") and
new("worker-a") with stringPtr("P1=foo") and stringPtr("worker-a") (or &v-style
temps) where Params and WorkerId are set in the test in
internal/cmd/remote_commands_test.go.
In `@internal/cmd/remote_commands.go`:
- Around line 483-489: The code around reading limitStr via ctx.StringParam and
parsing into limit currently ignores fmt.Sscanf errors and accepts partial
parses (e.g., "10foo"); change parsing to use strconv.Atoi (or strconv.ParseInt)
and check the error and that the value is >0, and if parsing fails or value <=0
return a flag parsing error to the caller instead of silently defaulting to 100;
update the import to include strconv and modify the handling where limit is used
so malformed --limit values produce an explicit error rather than falling back.
- Around line 514-526: The waitForRemoteStop function currently uses a fixed 30s
deadline and time.Sleep, so update it to honor the selected CLI context timeout
and respond to cancellation: derive the overall deadline from ctx.Context’s
deadline (fallback to 30s if none) and replace time.Sleep with a select that
listens on ctx.Context.Done() and a time.After(250*time.Millisecond) timer; keep
using ctx.Remote.getDAGRunDetails and core.Status(detail.Status) != core.Running
as the stop condition, and return ctx.Context.Err() if the context is cancelled
before the run stops.
In `@internal/cmd/retry.go`:
- Around line 57-59: Before delegating to remoteRunRetry in the ctx.IsRemote()
branch inside retry.go, add a validation that rejects any local-only retry flags
(--root, --default-working-dir, --worker-id, --attempt-id) when running with
--context/remote; detect these flags from the parsed args or command flag state
and return an error if any are present so they don't silently become no-ops.
Implement this guard immediately before the existing if ctx.IsRemote() return
remoteRunRetry(ctx, args) line (e.g., validateNoLocalOnlyRetryFlags(args) or
check FlagChanged for the named flags) so only permitted flags (--run-id,
--step) are forwarded to remoteRunRetry.
In `@internal/cmn/config/loader.go`:
- Line 392: envBindings is missing a binding for paths.contexts_dir so the
DAGU_CONTEXTS_DIR env var is ignored; add an entry that binds
cfg.Paths.ContextsDir to the environment layer (the same way other path entries
are bound) by adding a binding for "paths.contexts_dir" that references
&cfg.Paths.ContextsDir (and uses the same default reference
def.Paths.ContextsDir pattern) alongside the other envBindings items so
DAGU_CONTEXTS_DIR takes effect.
---
Nitpick comments:
In `@internal/agent/api.go`:
- Around line 573-596: The repeated inline construction of SessionManagerConfig
in newManagedSession (and the other SessionManagerConfig call sites) risks
divergence when new fields like RemoteContextResolver are added; create a single
helper function (e.g., buildSessionManagerConfig or
NewSessionManagerConfigFromRuntime) that accepts the common inputs (ctx, id,
user, cfg, now, and receiver *API) and returns a fully populated
SessionManagerConfig, then replace direct SessionManagerConfig literals in
newManagedSession and the other SessionManagerConfig call sites with calls to
this helper and pass its result into NewSessionManager to ensure all fields stay
centralized and in sync.
In `@internal/clicontext/store_test.go`:
- Around line 69-109: Add test cases to the tests slice in store_test.go to
cover the reserved name "current" and inputs with leading/trailing whitespace so
Store's reserved-marker logic and normalization can't regress unnoticed;
specifically add a case where Context{Name: "current", ServerURL:
"https://example.com", APIKey: "dagu_test"} expects the same reserved-name error
as LocalContextName, and cases with whitespace-padded fields (e.g., Name: " prod
", ServerURL: " https://example.com ", APIKey: " dagu_test ") expecting
validation to trim/normalize and either pass or produce the same validation
errors (e.g., invalid URL or api key prefix) so the behavior in Store/Context
validation is fully covered.
🪄 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: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 9db9c1cd-85a6-4c9e-ab64-7c1ad46b2d47
📒 Files selected for processing (39)
cmd/main.gointernal/agent/api.gointernal/agent/contextkeys.gointernal/agent/delegate.gointernal/agent/remote_adapter.gointernal/agent/remote_agent.gointernal/agent/remote_agent_test.gointernal/agent/remote_list.gointernal/agent/remote_types.gointernal/agent/session.gointernal/agent/tool_registry.gointernal/agent/tool_registry_test.gointernal/clicontext/store.gointernal/clicontext/store_test.gointernal/cmd/command_scope.gointernal/cmd/context.gointernal/cmd/context_command.gointernal/cmd/context_command_test.gointernal/cmd/context_hardening_test.gointernal/cmd/dequeue.gointernal/cmd/dry.gointernal/cmd/enqueue.gointernal/cmd/flags.gointernal/cmd/history.gointernal/cmd/remote_client.gointernal/cmd/remote_commands.gointernal/cmd/remote_commands_test.gointernal/cmd/restart.gointernal/cmd/retry.gointernal/cmd/start.gointernal/cmd/status.gointernal/cmd/stop.gointernal/cmn/config/config.gointernal/cmn/config/definition.gointernal/cmn/config/loader.gointernal/runtime/agent/agent.gointernal/runtime/builtin/agentstep/executor.gointernal/service/frontend/remote_adapter.gointernal/service/frontend/server.go
| // Resolve the target context. | ||
| node, err := resolver.GetByName(ctx.Context, args.Context) | ||
| if err != nil { | ||
| // On unknown node, list available nodes. | ||
| available, listErr := resolver.ListTokenAuthNodes(ctx.Context) | ||
| available, listErr := resolver.ListRemoteContexts(ctx.Context) | ||
| if listErr == nil && len(available) > 0 { | ||
| names := make([]string, 0, len(available)) | ||
| for _, n := range available { | ||
| names = append(names, n.Name) | ||
| } | ||
| return toolError("Unknown node %q. Available nodes: %s", args.Node, strings.Join(names, ", ")) | ||
| return toolError("Unknown context %q. Available contexts: %s", args.Context, strings.Join(names, ", ")) | ||
| } | ||
| return toolError("Failed to resolve node %q: %v", args.Node, err) | ||
| return toolError("Failed to resolve context %q: %v", args.Context, err) |
There was a problem hiding this comment.
Don't collapse resolver failures into "Unknown context".
GetByName can fail for configured-but-unsupported contexts as well as true misses. This branch rewrites all of those to "Unknown context" whenever ListRemoteContexts succeeds, so the real configuration error is lost.
Proposed fix
- // Resolve the target context.
- node, err := resolver.GetByName(ctx.Context, args.Context)
- if err != nil {
- available, listErr := resolver.ListRemoteContexts(ctx.Context)
- if listErr == nil && len(available) > 0 {
- names := make([]string, 0, len(available))
- for _, n := range available {
- names = append(names, n.Name)
- }
- return toolError("Unknown context %q. Available contexts: %s", args.Context, strings.Join(names, ", "))
- }
- return toolError("Failed to resolve context %q: %v", args.Context, err)
- }
+ // Resolve the target context.
+ node, err := resolver.GetByName(ctx.Context, args.Context)
+ if err != nil {
+ available, listErr := resolver.ListRemoteContexts(ctx.Context)
+ if listErr == nil && len(available) > 0 {
+ names := make([]string, 0, len(available))
+ for _, n := range available {
+ names = append(names, n.Name)
+ }
+ return toolError(
+ "Failed to resolve context %q: %v. Available contexts: %s",
+ args.Context,
+ err,
+ strings.Join(names, ", "),
+ )
+ }
+ return toolError("Failed to resolve context %q: %v", args.Context, err)
+ }📝 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.
| // Resolve the target context. | |
| node, err := resolver.GetByName(ctx.Context, args.Context) | |
| if err != nil { | |
| // On unknown node, list available nodes. | |
| available, listErr := resolver.ListTokenAuthNodes(ctx.Context) | |
| available, listErr := resolver.ListRemoteContexts(ctx.Context) | |
| if listErr == nil && len(available) > 0 { | |
| names := make([]string, 0, len(available)) | |
| for _, n := range available { | |
| names = append(names, n.Name) | |
| } | |
| return toolError("Unknown node %q. Available nodes: %s", args.Node, strings.Join(names, ", ")) | |
| return toolError("Unknown context %q. Available contexts: %s", args.Context, strings.Join(names, ", ")) | |
| } | |
| return toolError("Failed to resolve node %q: %v", args.Node, err) | |
| return toolError("Failed to resolve context %q: %v", args.Context, err) | |
| // Resolve the target context. | |
| node, err := resolver.GetByName(ctx.Context, args.Context) | |
| if err != nil { | |
| available, listErr := resolver.ListRemoteContexts(ctx.Context) | |
| if listErr == nil && len(available) > 0 { | |
| names := make([]string, 0, len(available)) | |
| for _, n := range available { | |
| names = append(names, n.Name) | |
| } | |
| return toolError( | |
| "Failed to resolve context %q: %v. Available contexts: %s", | |
| args.Context, | |
| err, | |
| strings.Join(names, ", "), | |
| ) | |
| } | |
| return toolError("Failed to resolve context %q: %v", args.Context, err) | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/agent/remote_agent.go` around lines 166 - 177, The current handler
collapses all resolver.GetByName errors into "Unknown context" whenever
resolver.ListRemoteContexts succeeds, losing real configuration errors; change
the logic in the GetByName error branch so that you only return the "Unknown
context %q. Available contexts: ..." toolError when the original error actually
means "not found" (e.g., compare with a sentinel like resolver.ErrNotFound or
use errors.Is/As on the error returned by resolver.GetByName); for any other
error from resolver.GetByName, return the original failure via toolError("Failed
to resolve context %q: %v", args.Context, err) so configuration/permission
errors are preserved (update the branch that calls resolver.ListRemoteContexts
and returns toolError accordingly, keeping symbols resolver.GetByName,
resolver.ListRemoteContexts, args.Context, ctx.Context, and toolError).
| name := strings.TrimSpace(ctx.Name) | ||
| if name == "" { | ||
| return errors.New("context name is required") | ||
| } | ||
| if name == LocalContextName { | ||
| return fmt.Errorf("%q is reserved", LocalContextName) | ||
| } | ||
| if strings.ContainsRune(name, os.PathSeparator) { | ||
| return errors.New("context name cannot contain path separators") | ||
| } | ||
| if !strings.HasPrefix(ctx.APIKey, "dagu_") { | ||
| return errors.New("api key must use the dagu_ prefix") | ||
| } | ||
| u, err := url.Parse(strings.TrimSpace(ctx.ServerURL)) |
There was a problem hiding this comment.
Persist the normalized values you validated.
ValidateContext trims Name and ServerURL only for checking, but Create/Update still derive the filename and stored JSON from the original struct. "prod " therefore becomes a different on-disk context than "prod", and trailing whitespace can survive into ServerURL/APIKey and break remote calls.
♻️ Proposed fix
+func normalizeContext(in *Context) *Context {
+ if in == nil {
+ return nil
+ }
+ out := *in
+ out.Name = strings.TrimSpace(out.Name)
+ out.ServerURL = strings.TrimSpace(out.ServerURL)
+ out.APIKey = strings.TrimSpace(out.APIKey)
+ return &out
+}
+
func (s *Store) Create(_ context.Context, ctx *Context) error {
+ ctx = normalizeContext(ctx)
if err := s.ValidateContext(ctx); err != nil {
return err
}
@@
func (s *Store) Update(_ context.Context, ctx *Context) error {
+ ctx = normalizeContext(ctx)
if err := s.ValidateContext(ctx); err != nil {
return err
}Also applies to: 136-163, 251-256
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/clicontext/store.go` around lines 107 - 120, ValidateContext
currently trims Name and ServerURL only for validation, but Create/Update still
persist the original struct; update the code so the normalized values are saved.
Either have ValidateContext mutate the passed-in context (trimmed Name, trimmed
ServerURL, and similarly trimmed APIKey if you want) or add a small helper
normalizeContext(ctx *Context) called from Create, Update, and ValidateContext
to set ctx.Name = strings.TrimSpace(ctx.Name), ctx.ServerURL =
strings.TrimSpace(ctx.ServerURL), and ctx.APIKey = strings.TrimSpace(ctx.APIKey)
before deriving filenames or JSON; ensure Create and Update use the normalized
ctx when computing filenames and writing the file so trailing whitespace cannot
be persisted (refer to functions ValidateContext, Create, Update and the Context
struct).
| if name == LocalContextName { | ||
| return fmt.Errorf("%q is reserved", LocalContextName) | ||
| } | ||
| if strings.ContainsRune(name, os.PathSeparator) { | ||
| return errors.New("context name cannot contain path separators") |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Go on Windows, do filepath.Join/filepath.Clean treat both "/" and "\" as path separators, and would a name like "../prod" with forward slashes escape a base directory?
💡 Result:
In Go on Windows, filepath.Join and filepath.Clean treat both "/" and "" as path separators. They use os.PathSeparator ('\') for output but recognize both during parsing and cleaning via lexical processing that collapses multiple separators and handles both. A relative path like "../prod" (with forward slashes) does not escape a base directory. filepath.Clean("../prod") returns "../prod" unchanged (lexical cleaning preserves it as relative). filepath.Join("C:/base", "../prod") becomes "C:/base/../prod" then cleaned to "C:/prod" (stays within the C: volume, navigating up from "base" to "prod" sibling/parent level). The ".." is handled relative to the base path and cannot escape the volume root. Use filepath.IsLocal("../prod") which returns false (contains "..", indicating potential escape), confirming Join(base, "../prod") may navigate above base's parent but stays OS-appropriate (volume-bounded on Windows).
Citations:
- 1: https://pkg.go.dev/path/filepath
- 2: https://go.dev/src/path/filepath/path.go?s=6447%3A6493
- 3: https://go.dev/src/path/filepath/path.go
- 4: https://pkg.go.dev/path/filepath/
- 5: https://go.dev/src/path/filepath/path_windows.go
- 6: https://go.dev/src/path/filepath/path_test.go
- 7: path/filepath: unexpected behavior of filepath.Join on Windows golang/go#26953
- 8: https://go.dev/issue/11551
🏁 Script executed:
cat -n internal/clicontext/store.go | head -250 | tail -150Repository: dagu-org/dagu
Length of output: 4816
🏁 Script executed:
# Get context constants and the relevant sections
rg -n "LocalContextName|currentFileName" internal/clicontext/store.go -A 2 -B 2Repository: dagu-org/dagu
Length of output: 2007
🏁 Script executed:
# Verify List() implementation skips current.json
rg -n "func.*List|currentFileName" internal/clicontext/store.go -A 10Repository: dagu-org/dagu
Length of output: 1423
🏁 Script executed:
# Check the exact validation code at lines 111-115
sed -n '105,120p' internal/clicontext/store.goRepository: dagu-org/dagu
Length of output: 579
🏁 Script executed:
# Check lines 208-210 and 247-248 mentioned in "Also applies to"
sed -n '205,250p' internal/clicontext/store.goRepository: dagu-org/dagu
Length of output: 1138
Add "current" to reserved context names and validate both forward and backslashes.
ValidateContext only checks for LocalContextName ("local") as reserved, but List() explicitly skips current.json (lines 208-210). A context named "current" would pass validation and be persisted as current.json, but remain invisible to callers of List(). Additionally, validation only rejects os.PathSeparator with strings.ContainsRune, which on Windows matches only backslashes. Forward slashes in names like "prod/east" pass validation but can still serve as separators in filepath operations.
🛡️ Proposed fix
- if name == LocalContextName {
- return fmt.Errorf("%q is reserved", LocalContextName)
- }
- if strings.ContainsRune(name, os.PathSeparator) {
+ switch name {
+ case LocalContextName, currentFileName:
+ return fmt.Errorf("%q is reserved", name)
+ }
+ if strings.ContainsAny(name, `/\`) {
return errors.New("context name cannot contain path separators")
}Applies to lines 111–115; see also lines 208–210 (List skip) and 247–248 (contextPath concatenation).
📝 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.
| if name == LocalContextName { | |
| return fmt.Errorf("%q is reserved", LocalContextName) | |
| } | |
| if strings.ContainsRune(name, os.PathSeparator) { | |
| return errors.New("context name cannot contain path separators") | |
| switch name { | |
| case LocalContextName, currentFileName: | |
| return fmt.Errorf("%q is reserved", name) | |
| } | |
| if strings.ContainsAny(name, `/\`) { | |
| return errors.New("context name cannot contain path separators") | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/clicontext/store.go` around lines 111 - 115, ValidateContext
currently only rejects LocalContextName ("local") and only checks
os.PathSeparator; update it to also reserve "current" (so names "local" and
"current" are rejected) and to reject both forward and backward slashes by
checking for '/' and '\\' (e.g. via strings.ContainsAny) so names like
"prod/east" fail on all platforms; mention the reserved names in the error
message and keep List()'s skip of "current.json" and contextPath concatenation
behavior consistent with this validation (refer to LocalContextName, the
ValidateContext function, List, and contextPath in store.go).
| ctx, err := s.readContext(filepath.Join(s.baseDir, entry.Name())) | ||
| if err != nil { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Don't return a clean List() result after skipping broken entries.
If decrypt/unmarshal fails for one file, this loop just continues and returns a partial list with nil error. From the CLI that looks exactly like the context disappeared, which makes wrong-key or corrupt-store recovery much harder.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/clicontext/store.go` around lines 211 - 214, In the List method
loop, skipping entries on read/decrypt/unmarshal failure (using
s.readContext(filepath.Join(s.baseDir, entry.Name())) and simply continue)
causes List() to return a silently partial result; change the behavior to record
and return an error when any entry fails: capture the filename (entry.Name())
and the underlying error from readContext, aggregate failures (or return the
first encountered error) and ensure List() returns a non-nil error alongside any
partial results so callers (CLI) can detect corrupt store or wrong key rather
than seeing a silently missing context.
| func NewContext(cmd *cobra.Command, flags []commandLineFlag) (*Context, error) { | ||
| ctx := cmd.Context() | ||
| scope := scopeForCommand(cmd.Name()) | ||
|
|
There was a problem hiding this comment.
Detect the context command family from the parent chain, not the leaf name.
NewContext runs on the leaf Cobra command, so these checks see "list", "add", "use", etc. for the real context-management flows, not "context". That means context list/add/update/... can skip newCLIContextStore(...) entirely and then dereference a nil ContextStore inside internal/cmd/context_command.go.
🛠️ Proposed fix
+func isContextCommandTree(cmd *cobra.Command) bool {
+ for c := cmd; c != nil; c = c.Parent() {
+ if c.Name() == "context" {
+ return true
+ }
+ }
+ return false
+}
+
func NewContext(cmd *cobra.Command, flags []commandLineFlag) (*Context, error) {
ctx := cmd.Context()
+ isContextCommand := isContextCommandTree(cmd)
scope := scopeForCommand(cmd.Name())
@@
- if cmd.Name() == "context" || scope != commandScopeStatic {
+ if isContextCommand || scope != commandScopeStatic {
contextStore, err = newCLIContextStore(cfg.Paths.DataDir, cfg.Paths.ContextsDir)
if err != nil {
- if shouldFailForContextStoreError(cmd, scope, requestedContextName) {
+ if shouldFailForContextStoreError(isContextCommand, scope, requestedContextName) {
return nil, fmt.Errorf("failed to initialize context store: %w", err)
}
contextStoreWarning = fmt.Errorf("failed to initialize context store, using local context: %w", err)
- } else if cmd.Name() != "context" {
+ } else if !isContextCommand {
selectedContextName, selectedContext, err = resolveCLIContext(cmd, contextStore, requestedContextName)
@@
-func shouldFailForContextStoreError(cmd *cobra.Command, scope commandScope, requested string) bool {
- if cmd.Name() == "context" {
+func shouldFailForContextStoreError(isContextCommand bool, scope commandScope, requested string) bool {
+ if isContextCommand {
return true
}Also applies to: 213-230, 469-477
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmd/context.go` around lines 160 - 163, NewContext currently
inspects the leaf cobra command name (via cmd.Name()) so it sees
"list"/"add"/"use" instead of the "context" family; change the detection to walk
the parent chain (using cmd.Parent() repeatedly) and check ancestor.Name() ==
"context" (or otherwise derive the command family from the ancestor chain)
before calling newCLIContextStore or scopeForCommand; update the same
parent-chain check in the other similar blocks mentioned (the other NewContext
usages and any checks that dereference ContextStore in context_command.go) so
newCLIContextStore is only invoked when the command is actually in the "context"
family and ContextStore cannot be nil.
| Params: new("P1=foo"), | ||
| WorkerId: new("worker-a"), | ||
| Tags: &[]string{"env=prod"}, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'new\("' internal/cmd/remote_commands_test.go
rg -n 'return new\(' internal/cmd/remote_commands_test.goRepository: dagu-org/dagu
Length of output: 130
🏁 Script executed:
sed -n '1,100p' internal/cmd/remote_commands_test.go | cat -nRepository: dagu-org/dagu
Length of output: 2300
🏁 Script executed:
sed -n '60,80p' internal/cmd/remote_commands_test.go | cat -nRepository: dagu-org/dagu
Length of output: 225
🏁 Script executed:
wc -l internal/cmd/remote_commands_test.go
rg -n 'stringPtr' internal/cmd/remote_commands_test.goRepository: dagu-org/dagu
Length of output: 96
🏁 Script executed:
find . -name '*.go' -path '*/api/*' | head -20
rg -n 'type DAGRunDetails' --type goRepository: dagu-org/dagu
Length of output: 1119
🏁 Script executed:
sed -n '1266,1310p' api/v1/api.gen.go | cat -nRepository: dagu-org/dagu
Length of output: 2096
🏁 Script executed:
sed -n '1266,1350p' api/v1/api.gen.go | grep -A2 -B2 'WorkerId'Repository: dagu-org/dagu
Length of output: 250
Fix the new() calls at lines 27–28; this test file will not compile.
Go's new builtin requires a type argument, not a value. new("P1=foo") and new("worker-a") are invalid syntax. Both Params and WorkerId are *string fields, so use a helper function or assign through a variable.
🛠️ Minimal fix
+func stringPtr(v string) *string {
+ return &v
+}
+
func TestToExecStatus_MapsRemoteFieldsExplicitly(t *testing.T) {
t.Parallel()
detail := &api.DAGRunDetails{
...
- Params: new("P1=foo"),
- WorkerId: new("worker-a"),
+ Params: stringPtr("P1=foo"),
+ WorkerId: stringPtr("worker-a"),📝 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.
| Params: new("P1=foo"), | |
| WorkerId: new("worker-a"), | |
| Tags: &[]string{"env=prod"}, | |
| func stringPtr(v string) *string { | |
| return &v | |
| } | |
| func TestToExecStatus_MapsRemoteFieldsExplicitly(t *testing.T) { | |
| t.Parallel() | |
| detail := &api.DAGRunDetails{ | |
| Params: stringPtr("P1=foo"), | |
| WorkerId: stringPtr("worker-a"), | |
| Tags: &[]string{"env=prod"}, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmd/remote_commands_test.go` around lines 27 - 29, The test
currently uses invalid new() calls for string pointers; update the Params and
WorkerId fields (which are *string) to provide proper *string values — e.g.,
create a small helper (like stringPtr) or temp variables and assign pointers to
them—so replace new("P1=foo") and new("worker-a") with stringPtr("P1=foo") and
stringPtr("worker-a") (or &v-style temps) where Params and WorkerId are set in
the test in internal/cmd/remote_commands_test.go.
| limitStr, _ := ctx.StringParam("limit") | ||
| if limitStr != "" { | ||
| fmt.Sscanf(limitStr, "%d", &limit) | ||
| if limit <= 0 { | ||
| limit = 100 | ||
| } | ||
| } |
There was a problem hiding this comment.
Reject malformed --limit values instead of silently falling back.
Ignoring fmt.Sscanf here makes bad input fall back to 100, and partial parses like 10foo can still be accepted. Return a flag error instead so remote history does not hide malformed user input.
🛠️ Proposed fix
- fmt.Sscanf(limitStr, "%d", &limit)
- if limit <= 0 {
- limit = 100
- }
+ parsed, err := strconv.Atoi(limitStr)
+ if err != nil {
+ return query, 0, fmt.Errorf("invalid --limit value %q: must be an integer", limitStr)
+ }
+ if parsed <= 0 {
+ return query, 0, fmt.Errorf("--limit must be greater than 0")
+ }
+ limit = parsedAlso add strconv to the imports.
📝 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.
| limitStr, _ := ctx.StringParam("limit") | |
| if limitStr != "" { | |
| fmt.Sscanf(limitStr, "%d", &limit) | |
| if limit <= 0 { | |
| limit = 100 | |
| } | |
| } | |
| limitStr, _ := ctx.StringParam("limit") | |
| if limitStr != "" { | |
| parsed, err := strconv.Atoi(limitStr) | |
| if err != nil { | |
| return query, 0, fmt.Errorf("invalid --limit value %q: must be an integer", limitStr) | |
| } | |
| if parsed <= 0 { | |
| return query, 0, fmt.Errorf("--limit must be greater than 0") | |
| } | |
| limit = parsed | |
| } |
🧰 Tools
🪛 GitHub Check: Go Linter
[failure] 485-485:
Error return value of fmt.Sscanf is not checked (errcheck)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmd/remote_commands.go` around lines 483 - 489, The code around
reading limitStr via ctx.StringParam and parsing into limit currently ignores
fmt.Sscanf errors and accepts partial parses (e.g., "10foo"); change parsing to
use strconv.Atoi (or strconv.ParseInt) and check the error and that the value is
>0, and if parsing fails or value <=0 return a flag parsing error to the caller
instead of silently defaulting to 100; update the import to include strconv and
modify the handling where limit is used so malformed --limit values produce an
explicit error rather than falling back.
| func waitForRemoteStop(ctx *Context, name, dagRunID string) error { | ||
| deadline := time.Now().Add(30 * time.Second) | ||
| for time.Now().Before(deadline) { | ||
| detail, err := ctx.Remote.getDAGRunDetails(ctx, name, dagRunID) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if core.Status(detail.Status) != core.Running { | ||
| return nil | ||
| } | ||
| time.Sleep(250 * time.Millisecond) | ||
| } | ||
| return fmt.Errorf("timed out waiting for remote DAG run %s to stop", dagRunID) |
There was a problem hiding this comment.
Honor the selected context timeout while waiting for a remote stop.
remoteRunRestart always gives up after 30 seconds here, even when the chosen CLI context was configured with a longer timeout. That makes remote restarts fail spuriously on DAGs that take longer to shut down, and time.Sleep also delays cancellation handling.
🛠️ Proposed fix
func waitForRemoteStop(ctx *Context, name, dagRunID string) error {
- deadline := time.Now().Add(30 * time.Second)
- for time.Now().Before(deadline) {
- detail, err := ctx.Remote.getDAGRunDetails(ctx, name, dagRunID)
- if err != nil {
- return err
- }
- if core.Status(detail.Status) != core.Running {
- return nil
- }
- time.Sleep(250 * time.Millisecond)
- }
- return fmt.Errorf("timed out waiting for remote DAG run %s to stop", dagRunID)
+ timeout := defaultRemoteTimeout
+ if ctx != nil && ctx.Remote != nil && ctx.Remote.client != nil && ctx.Remote.client.Timeout > 0 {
+ timeout = ctx.Remote.client.Timeout
+ }
+
+ timer := time.NewTimer(timeout)
+ ticker := time.NewTicker(250 * time.Millisecond)
+ defer timer.Stop()
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-timer.C:
+ return fmt.Errorf("timed out waiting for remote DAG run %s to stop", dagRunID)
+ case <-ticker.C:
+ detail, err := ctx.Remote.getDAGRunDetails(ctx, name, dagRunID)
+ if err != nil {
+ return err
+ }
+ if core.Status(detail.Status) != core.Running {
+ return nil
+ }
+ }
+ }
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmd/remote_commands.go` around lines 514 - 526, The
waitForRemoteStop function currently uses a fixed 30s deadline and time.Sleep,
so update it to honor the selected CLI context timeout and respond to
cancellation: derive the overall deadline from ctx.Context’s deadline (fallback
to 30s if none) and replace time.Sleep with a select that listens on
ctx.Context.Done() and a time.After(250*time.Millisecond) timer; keep using
ctx.Remote.getDAGRunDetails and core.Status(detail.Status) != core.Running as
the stop condition, and return ctx.Context.Err() if the context is cancelled
before the run stops.
| if ctx.IsRemote() { | ||
| return remoteRunRetry(ctx, args) | ||
| } |
There was a problem hiding this comment.
Reject local-only retry flags before delegating remotely.
Line 57 now exits before --root, --default-working-dir, --worker-id, and --attempt-id are inspected, but the remote implementation only forwards --run-id and --step. With --context, those flags become silent no-ops instead of an explicit error.
💡 Suggested guard for the remote branch
func runRetry(ctx *Context, args []string) error {
if ctx.IsRemote() {
+ for _, flag := range []commandLineFlag{
+ rootDAGRunFlag,
+ defaultWorkingDirFlag,
+ retryWorkerIDFlag,
+ attemptIDFlag,
+ } {
+ if ctx.Command.Flags().Changed(flag.name) {
+ return fmt.Errorf("--%s is not supported with --context", flag.name)
+ }
+ }
return remoteRunRetry(ctx, args)
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmd/retry.go` around lines 57 - 59, Before delegating to
remoteRunRetry in the ctx.IsRemote() branch inside retry.go, add a validation
that rejects any local-only retry flags (--root, --default-working-dir,
--worker-id, --attempt-id) when running with --context/remote; detect these
flags from the parsed args or command flag state and return an error if any are
present so they don't silently become no-ops. Implement this guard immediately
before the existing if ctx.IsRemote() return remoteRunRetry(ctx, args) line
(e.g., validateNoLocalOnlyRetryFlags(args) or check FlagChanged for the named
flags) so only permitted flags (--run-id, --step) are forwarded to
remoteRunRetry.
| {"APIKeysDir", &cfg.Paths.APIKeysDir, def.Paths.APIKeysDir}, | ||
| {"WebhooksDir", &cfg.Paths.WebhooksDir, def.Paths.WebhooksDir}, | ||
| {"SessionsDir", &cfg.Paths.SessionsDir, def.Paths.SessionsDir}, | ||
| {"ContextsDir", &cfg.Paths.ContextsDir, def.Paths.ContextsDir}, |
There was a problem hiding this comment.
Also bind paths.contexts_dir in the environment layer.
The new path is resolved and defaulted here, but envBindings still has no paths.contexts_dir entry, so DAGU_CONTEXTS_DIR never takes effect. That makes the new context store the only path in this group that cannot be relocated from environment-only configs.
🔧 Minimal follow-up
@@
{key: "paths.service_registry_dir", env: "SERVICE_REGISTRY_DIR", isPath: true},
{key: "paths.users_dir", env: "USERS_DIR", isPath: true},
+ {key: "paths.contexts_dir", env: "CONTEXTS_DIR", isPath: true},
{key: "paths.workspaces_dir", env: "WORKSPACES_DIR", isPath: true},Also applies to: 1268-1268
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@internal/cmn/config/loader.go` at line 392, envBindings is missing a binding
for paths.contexts_dir so the DAGU_CONTEXTS_DIR env var is ignored; add an entry
that binds cfg.Paths.ContextsDir to the environment layer (the same way other
path entries are bound) by adding a binding for "paths.contexts_dir" that
references &cfg.Paths.ContextsDir (and uses the same default reference
def.Paths.ContextsDir pattern) alongside the other envBindings items so
DAGU_CONTEXTS_DIR takes effect.
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1949 +/- ##
==========================================
- Coverage 68.62% 67.98% -0.65%
==========================================
Files 468 475 +7
Lines 59992 61304 +1312
==========================================
+ Hits 41171 41679 +508
- Misses 14959 15652 +693
- Partials 3862 3973 +111
... and 16 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
Summary
Testing
Summary by CodeRabbit
Release Notes
contextCLI command for managing remote server connections (list, add, update, remove, use, test)--contextflag for workflow commands