Skip to content

feat: add workspace base config#2108

Merged
yottahmd merged 2 commits intomainfrom
codex/workspace-base-config
May 6, 2026
Merged

feat: add workspace base config#2108
yottahmd merged 2 commits intomainfrom
codex/workspace-base-config

Conversation

@yottahmd
Copy link
Copy Markdown
Collaborator

@yottahmd yottahmd commented May 6, 2026

Summary

  • add workspace-scoped base config layered after the global base config
  • expose workspace base config editing in the Web UI when a named workspace is selected
  • include global and workspace base config files in git sync

Details

  • global base config remains the shared default for all DAGs
  • named workspace DAGs can inherit an additional workspace config from workspaces/<workspace>/base.yaml
  • default/all workspace selections do not get workspace config editing

Validation

  • go test ./internal/core/spec ./internal/persis/filedag ./internal/gitsync ./internal/service/frontend/api/v1 ./internal/cmd ./internal/engine ./internal/service/frontend ./internal/test ./internal/workspace -count=1
  • pnpm test
  • pnpm typecheck
  • pnpm build
  • git diff --check

Summary by CodeRabbit

Release Notes

  • New Features

    • Added workspace-scoped base configuration management via new API endpoints for retrieving and updating workspace-specific DAG configurations
    • Introduced workspace base config as a distinct sync item kind alongside DAGs and other item types
  • User Interface

    • Base config editor now supports switching between Global and Workspace configuration scopes with dedicated management per scope
    • Git sync interface updated to recognize and handle configuration items separately from DAGs

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da1e8640-4283-4485-b5a9-4a3effca6900

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduces workspace-scoped base configuration API endpoints and loading infrastructure. Adds GET/PUT endpoints for managing workspace-specific DAG base configurations that merge with global base configs. Extends DAG loading, git sync, and UI to support this new scoped configuration level.

Changes

Workspace-Scoped Base Configuration Feature

Layer / File(s) Summary
API Specification
api/v1/api.yaml, api/v1/api.gen.go
New workspace-scoped base-config endpoints: GET /settings/workspaces/{workspaceName}/base-config and PUT to update. Adds GetWorkspaceBaseConfigParams, UpdateWorkspaceBaseConfigJSONBody types, response objects for 200/400/401/403/404 statuses. Expands SyncItemKind enum with config value. Generates middleware wrappers and route registrations.
Workspace Path Utilities
internal/workspace/workspace.go
New helpers BaseConfigDir(dagsDir) and BaseConfigPath(dagsDir, name) compute workspace-specific base config directory and file paths. Adds constants BaseConfigDirName and BaseConfigFileName.
Core DAG Loading with Workspace Merge
internal/core/spec/loader.go, internal/core/spec/builder.go
Introduces LoadOption WithWorkspaceBaseConfigDir(dir) to thread workspace base config through DAG loading. Adds loadEffectiveBaseDefinition, readWorkspaceBaseDefinitionData, mergeBaseDefinitionData, and supporting helpers to merge global and workspace base configs. BuildOpts gains WorkspaceBaseConfigDir field. Propagates baseRaw and merged base definitions through loading pipeline.
DAG Store Persistence
internal/persis/filedag/store.go
Adds exported Option WithWorkspaceBaseConfigDir(dir) and WorkspaceBaseConfigDir field in Options. Extends state tracking with composite baseConfigState combining global and workspace configs via describeBaseConfigStateSet and describeWorkspaceBaseConfigState helpers. Integrates workspace base config dir into defaultLoadOptions and refreshBaseConfigState logic.
Git Sync Config File Support
internal/gitsync/state.go, internal/gitsync/service.go
Adds DAGKindConfig constant and isConfigFile/workspaceBaseConfigNameFromID helpers to identify config-type DAGs. Extends KindForDAGID to classify config files. Updates NewService signature to accept optional baseConfigPath. Adds scanConfigFiles/scanConfigFile to discover and track base/workspace config files as untracked DAGs. Updates dagIDToFilePath and safeDAGIDToFilePath to route config IDs to appropriate base config paths. Prevents moving config items via Move guard.
Command & Engine Wiring
internal/cmd/context.go, internal/cmd/dry.go, internal/cmd/start.go, internal/cmd/stop.go, internal/cmd/validate.go, internal/engine/engine.go, internal/engine/run.go, internal/agentsnapshot/dispatch.go
All DAG loading paths updated to include spec.WithWorkspaceBaseConfigDir(workspace.BaseConfigDir(...)) option. Command initialization, dry-run, start, stop, validate, and engine now thread workspace base config directory into DAG spec loading. NewService call in sync.go passes baseConfig parameter.
Frontend API Handler Implementation
internal/service/frontend/api/v1/base_config.go
Implements GetWorkspaceBaseConfig and UpdateWorkspaceBaseConfig handlers. Adds ErrFailedToLoadWorkspaceBaseConfig and ErrFailedToSaveWorkspaceBaseConfig error types. Includes helpers resolveWorkspaceBaseConfigTarget, requireWorkspaceConfigWrite, readWorkspaceBaseConfigSpec, workspaceBaseConfigStore to load, validate, and persist workspace-specific base configs with access control and auditing.
Frontend API Wiring
internal/service/frontend/api/v1/sync.go, internal/service/frontend/api/v1/dagruns.go, internal/service/frontend/server.go
Maps DAGKindConfig to SyncItemKindConfig in toAPISyncItemKind. Updates sync file-path handling for config items. Extends dagruns DAG loading to include workspace base config dir. Updates gitsync.NewService call to pass baseConfig argument.
Frontend UI Schema & Components
ui/src/api/v1/schema.ts
Adds workspace-scoped base-config endpoints to TypeScript schema with new path parameter workspaceName, request/response structures, and error handling. Adds SyncItemKind.config enum member.
Frontend Base-Config UI
ui/src/pages/base-config/index.tsx
Refactors BaseConfigPage to support dual scopes (global and workspace). Adds per-scope state (activeScope, globalValue, workspaceValue, globalDirty, workspaceDirty). Separates global and workspace data fetches. Introduces Tabs UI for scope switching when workspace configured. Per-scope editing, save, and revert logic targets appropriate API endpoints. Editor value/model binding reflects active scope.
Frontend Git-Sync Config Support
ui/src/pages/git-sync/MoveDialog.tsx, ui/src/pages/git-sync/RowActionMenu.tsx, ui/src/pages/git-sync/index.tsx
Extends ItemKind type to include 'config'. MoveDialog prevents moving config items. RowActionMenuProps gains kind property; Move action disabled for config kind. Git-Sync index adds config to type filters, UI labels, and item counts. Table header changed from DAG-specific to generic Item label. Per-row rendering now passes kind to RowActionMenu.
Tests & Validation
internal/core/spec/loader_test.go, internal/persis/filedag/store_test.go, internal/gitsync/service_test.go, internal/service/frontend/api/v1/sync_test.go
Adds tests validating workspace base config merging (WithWorkspaceBaseConfigDir_MergesNamedWorkspaceConfig, WithWorkspaceBaseConfigDir_IgnoresDefaultWorkspace). Tests workspace base config cache/index refresh. Tests config DAG kind handling in git sync (TestKindForDAGID_ConfigFiles, TestDagIDPathHelpers_ConfigFiles, TestScanLocalDAGs_ConfigFiles). Updates sync test to include config item.
Test Infrastructure
internal/test/helper.go, internal/test/scheduler.go
Updates test DAG store and scheduler initialization to include WithWorkspaceBaseConfigDir. Test DAG loading extended with workspace base config dir option.

Sequence Diagram

sequenceDiagram
    participant UI as Frontend UI
    participant API as API Handler
    participant Loader as DAG Loader
    participant Store as Workspace<br/>Config Store
    participant FS as Filesystem

    rect rgba(100, 149, 237, 0.5)
    note over UI,FS: Load DAG with Workspace Base Config Merge
    UI->>API: Load DAG (workspace context)
    API->>Loader: LoadDAG with<br/>WorkspaceBaseConfigDir
    Loader->>FS: Read global base.yaml
    FS-->>Loader: Global base definition
    Loader->>Store: Get workspace base<br/>config directory
    Store->>FS: Read workspace/base.yaml
    FS-->>Store: Workspace base definition
    Store-->>Loader: Workspace base definition
    Loader->>Loader: Merge global base<br/>+ workspace base
    Loader->>FS: Read DAG spec
    FS-->>Loader: DAG spec
    Loader->>Loader: Apply merged base<br/>to DAG
    Loader-->>API: Resolved DAG
    API-->>UI: DAG with merged config
    end

    rect rgba(144, 238, 144, 0.5)
    note over UI,FS: Update Workspace Base Config
    UI->>API: PUT new base config
    API->>API: Validate spec
    API->>Store: Save to workspace<br/>config store
    Store->>FS: Write workspace/base.yaml
    FS-->>Store: Write complete
    Store-->>API: Saved
    API-->>UI: Success response
    UI->>UI: Refresh DAG view
    end
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly Related PRs

  • dagucloud/dagu#2034: Modifies internal/core/spec/loader.go and affects DAG loading refactoring that overlaps with workspace base-config loading changes.
  • dagucloud/dagu#2019: Introduces workspace selection and workspace utilities that form the foundation for this PR's workspace-scoped configuration feature.
  • dagucloud/dagu#1986: Updates internal/agentsnapshot/dispatch.go BuildFromPaths which is modified here to include workspace base-config load options.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 41.89% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add workspace base config' directly and clearly summarizes the main change: adding a new workspace-scoped base configuration feature. It accurately reflects the primary objective of the PR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/workspace-base-config

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
ui/src/pages/git-sync/MoveDialog.tsx (1)

1-1: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add the required GPL license header to this TSX file.

This changed source file is missing the repository-required GPL v3 header block.

🔧 Suggested fix
+// Copyright (C) 2026 Yota Hamada
+// SPDX-License-Identifier: GPL-3.0-or-later
+
 import { SyncStatus } from '@/api/v1/schema';
As per coding guidelines `**/*.{go,ts,tsx,js}: Apply GPL v3 license headers on source files, managed via make addlicense`.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/git-sync/MoveDialog.tsx` at line 1, This file is missing the
repository-required GPL v3 header; add the standard GPL v3 license header block
to the top of the MoveDialog TSX module (above the import of SyncStatus) so the
MoveDialog React component file includes the required header; best approach is
to run the repo tool to apply headers (e.g., run "make addlicense" or follow the
project's addlicense convention) so the correct GPL v3 block is inserted
consistently.
🧹 Nitpick comments (2)
internal/gitsync/service_test.go (1)

79-86: ⚡ Quick win

Consider adding a comment explaining why "workspaces/default/base" maps to DAGKindDAG.

The assertion at Line 84 correctly captures that "default" is not a valid workspace name per workspace.ValidateName, so it falls through to DAGKindDAG. Without a brief comment, a future reader may wonder why an ID that structurally looks like a workspace config path (3-segment, ending in "base") does not receive DAGKindConfig.

💬 Suggested comment
-	assert.Equal(t, DAGKindDAG, KindForDAGID("workspaces/default/base"))
+	// "default" is rejected by workspace.ValidateName, so this is treated as a regular DAG ID.
+	assert.Equal(t, DAGKindDAG, KindForDAGID("workspaces/default/base"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/gitsync/service_test.go` around lines 79 - 86, Add a brief inline
comment in the TestKindForDAGID_ConfigFiles test (near the assertion for
"workspaces/default/base") explaining that KindForDAGID maps that ID to
DAGKindDAG because "default" is rejected by workspace.ValidateName (i.e., it's
not a valid workspace name), so the code falls through to treat it as a DAG
rather than a config; reference the test name TestKindForDAGID_ConfigFiles and
the KindForDAGID function in the comment for clarity.
internal/gitsync/service.go (1)

511-527: 💤 Low value

Hardcoded .yaml trim couples producer and parser to a specific filename convention.

Both the scanner (scanConfigFiles at line 524) and the parser (workspaceBaseConfigNameFromID in state.go:106) use strings.TrimSuffix(workspace.BaseConfigFileName, ".yaml"). If BaseConfigFileName ever changes extension (e.g., from "base.yaml" to "base.yml"), both locations must be updated simultaneously. Failure to do so creates a silent mismatch where scanner and parser diverge, orphaning or unmapping workspace configs.

Consider deriving the suffix from path.Ext(workspace.BaseConfigFileName) or exposing a shared helper in the workspace package, so both producer and consumer reference one source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/gitsync/service.go` around lines 511 - 527, The scanner in
scanConfigFiles uses strings.TrimSuffix(workspace.BaseConfigFileName, ".yaml")
which hardcodes the extension and can diverge from
workspaceBaseConfigNameFromID; change both sides to use a single source of truth
by either deriving the suffix via path.Ext(workspace.BaseConfigFileName) or
adding a helper in the workspace package (e.g., WorkspaceBaseConfigStem or
TrimBaseConfigExt) and update scanConfigFiles and workspaceBaseConfigNameFromID
to call that helper so producer and parser always agree on the base config name.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@api/v1/api.yaml`:
- Around line 5149-5153: Replace the plain string schema for the path parameter
named workspaceName with a $ref to the existing components schema WorkspaceName;
locate the parameter definition for workspaceName (currently "schema: type:
string") and change it to "schema: $ref: '#/components/schemas/WorkspaceName'"
for both occurrences (the one at the top of the diff and the other occurrence
around the second mention), ensuring the parameter name remains workspaceName
and the ref exactly matches the existing WorkspaceName schema symbol.

In `@internal/core/spec/loader.go`:
- Around line 414-423: loadDAGsFromData currently resolves workspace/base
per-document so later documents in a multi-doc file can miss the workspace and
get a different BaseConfigData; change loadDAGsFromData to resolve the
workspace/base once for the file (using the primary/first document after
decodeDocuments) and then pass that resolved baseDef/baseRaw into
processDAGDocument for subsequent docs when they don't declare workspace labels;
update the loop in loadDAGsFromData (and the analogous logic around
processDAGDocument at the other mentioned site) to check each doc for an
explicit workspace and otherwise reuse the file-level baseDef/baseRaw so all
documents in the file inherit the same workspace base.
- Around line 640-687: mergeDefinitionMaps currently only deep-merges nested
map[string]any and overwrites when a field is a list (e.g., env written as YAML
list), causing workspace base lists to replace rather than layer on global
lists; fix by normalizing union fields before merging: in
mergeBaseDefinitionData, decode/normalize baseRaw and overrideRaw into their
canonical dag/structured form (using decode or decodeDefinitionData) or add
logic in mergeDefinitionMaps to special-case known union keys (e.g., "env") —
detect when baseValue or overrideValue is a slice vs map and convert list syntax
into the canonical map/list representation (or merge by name/append deduplicated
entries) before assigning merged[key]; ensure mergedRaw is produced from the
normalized merged structure so YAML syntax differences no longer affect
semantics (refer to mergeBaseDefinitionData, mergeDefinitionMaps, decode,
decodeDefinitionData).

In `@internal/service/frontend/api/v1/dagruns.go`:
- Around line 4258-4261: The default reschedule path rebuilds the DAG using
loadInlineDAG(snapshotDAG.YamlData) without reapplying workspace or preserved
base-config layers, so when rescheduling a named-workspace DAG workspace-scoped
settings are lost; update the rebuild to pass the same spec.LoadOption values as
the useCurrentDAGFile branch (include
spec.WithWorkspaceBaseConfigDir(workspace.BaseConfigDir(...)) and
spec.WithBaseConfig populated from snapshotDAG.BaseConfigData or the persisted
base config) or change loadInlineDAG to accept and apply those load options and
honor snapshotDAG.BaseConfigData so the workspace layer and preserved
base-config are reapplied during the snapshot-based load.

In `@internal/workspace/workspace.go`:
- Around line 33-38: The BaseConfigPath function currently builds a path with
the raw name which allows names like "../other" to escape the workspaces
directory; call ValidateName(name) (or equivalent workspace name validator) at
the start of BaseConfigPath and return "" if validation fails so only validated
names are joined (i.e., add a guard: if !ValidateName(name) return "") before
calling BaseConfigDir and filepath.Join.

In `@ui/src/pages/base-config/index.tsx`:
- Around line 87-90: The global scope isn't reset when remoteNode (or
selectedWorkspace) changes, leaving stale globalValue/globalDirty and risking
cross-node saves; add a useEffect that listens to [selectedWorkspace,
remoteNode] and calls setGlobalValue(null) and setGlobalDirty(false) (mirroring
the workspace reset) so globalValue will be re-seeded from globalData for the
new node; reference the existing state setters setGlobalValue and setGlobalDirty
and the globalData/init effect that seeds globalValue when null.

---

Outside diff comments:
In `@ui/src/pages/git-sync/MoveDialog.tsx`:
- Line 1: This file is missing the repository-required GPL v3 header; add the
standard GPL v3 license header block to the top of the MoveDialog TSX module
(above the import of SyncStatus) so the MoveDialog React component file includes
the required header; best approach is to run the repo tool to apply headers
(e.g., run "make addlicense" or follow the project's addlicense convention) so
the correct GPL v3 block is inserted consistently.

---

Nitpick comments:
In `@internal/gitsync/service_test.go`:
- Around line 79-86: Add a brief inline comment in the
TestKindForDAGID_ConfigFiles test (near the assertion for
"workspaces/default/base") explaining that KindForDAGID maps that ID to
DAGKindDAG because "default" is rejected by workspace.ValidateName (i.e., it's
not a valid workspace name), so the code falls through to treat it as a DAG
rather than a config; reference the test name TestKindForDAGID_ConfigFiles and
the KindForDAGID function in the comment for clarity.

In `@internal/gitsync/service.go`:
- Around line 511-527: The scanner in scanConfigFiles uses
strings.TrimSuffix(workspace.BaseConfigFileName, ".yaml") which hardcodes the
extension and can diverge from workspaceBaseConfigNameFromID; change both sides
to use a single source of truth by either deriving the suffix via
path.Ext(workspace.BaseConfigFileName) or adding a helper in the workspace
package (e.g., WorkspaceBaseConfigStem or TrimBaseConfigExt) and update
scanConfigFiles and workspaceBaseConfigNameFromID to call that helper so
producer and parser always agree on the base config name.
🪄 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: 9a0a3401-03ba-4cec-8040-f9062b45f88d

📥 Commits

Reviewing files that changed from the base of the PR and between 129d2f0 and 46f72bf.

📒 Files selected for processing (32)
  • api/v1/api.gen.go
  • api/v1/api.yaml
  • internal/agentsnapshot/dispatch.go
  • internal/cmd/context.go
  • internal/cmd/dry.go
  • internal/cmd/start.go
  • internal/cmd/stop.go
  • internal/cmd/sync.go
  • internal/cmd/validate.go
  • internal/core/spec/builder.go
  • internal/core/spec/loader.go
  • internal/core/spec/loader_test.go
  • internal/engine/engine.go
  • internal/engine/run.go
  • internal/gitsync/service.go
  • internal/gitsync/service_test.go
  • internal/gitsync/state.go
  • internal/persis/filedag/store.go
  • internal/persis/filedag/store_test.go
  • internal/service/frontend/api/v1/base_config.go
  • internal/service/frontend/api/v1/dagruns.go
  • internal/service/frontend/api/v1/sync.go
  • internal/service/frontend/api/v1/sync_test.go
  • internal/service/frontend/server.go
  • internal/test/helper.go
  • internal/test/scheduler.go
  • internal/workspace/workspace.go
  • ui/src/api/v1/schema.ts
  • ui/src/pages/base-config/index.tsx
  • ui/src/pages/git-sync/MoveDialog.tsx
  • ui/src/pages/git-sync/RowActionMenu.tsx
  • ui/src/pages/git-sync/index.tsx

Comment thread api/v1/api.yaml Outdated
Comment on lines +5149 to +5153
- name: workspaceName
in: path
required: true
schema:
type: string
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Constrain workspaceName with the existing WorkspaceName schema.

On Line 5149 and Line 5212, workspaceName is a plain string, but this endpoint is for named workspaces only. Reusing #/components/schemas/WorkspaceName keeps reserved-name validation (all, default) consistent with the rest of the API contract.

Suggested diff
       parameters:
         - name: workspaceName
           in: path
           required: true
           schema:
-            type: string
+            $ref: "#/components/schemas/WorkspaceName"
         - $ref: "#/components/parameters/RemoteNode"
...
       parameters:
         - name: workspaceName
           in: path
           required: true
           schema:
-            type: string
+            $ref: "#/components/schemas/WorkspaceName"
         - $ref: "#/components/parameters/RemoteNode"

Also applies to: 5212-5216

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@api/v1/api.yaml` around lines 5149 - 5153, Replace the plain string schema
for the path parameter named workspaceName with a $ref to the existing
components schema WorkspaceName; locate the parameter definition for
workspaceName (currently "schema: type: string") and change it to "schema: $ref:
'#/components/schemas/WorkspaceName'" for both occurrences (the one at the top
of the diff and the other occurrence around the second mention), ensuring the
parameter name remains workspaceName and the ref exactly matches the existing
WorkspaceName schema symbol.

Comment on lines +414 to 423
func loadDAGsFromData(ctx BuildContext, data []byte, filePath string, baseDef *dag, baseRaw []byte) ([]*core.DAG, error) {
docs, err := decodeDocuments(data)
if err != nil {
return nil, err
}

dags := make([]*core.DAG, 0, len(docs))
for _, doc := range docs {
dag, err := processDAGDocument(buildDocumentContext(ctx, doc.index), doc.data, baseDef, filePath, data)
dag, err := processDAGDocument(buildDocumentContext(ctx, doc.index), doc.data, baseDef, baseRaw, filePath, data)
if err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Propagate the workspace base config to every document in a multi-document DAG file.

The workspace layer is resolved from each document independently, so only documents that repeat labels/tags: workspace=... inherit it. Inline local DAG documents usually omit those labels, which means a parent DAG can build with the workspace base while its local sub-DAGs fall back to the global base and carry different BaseConfigData. Please resolve the workspace once per file, or at least fall back to the primary document's workspace when later documents omit it.

Also applies to: 515-516

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/spec/loader.go` around lines 414 - 423, loadDAGsFromData
currently resolves workspace/base per-document so later documents in a multi-doc
file can miss the workspace and get a different BaseConfigData; change
loadDAGsFromData to resolve the workspace/base once for the file (using the
primary/first document after decodeDocuments) and then pass that resolved
baseDef/baseRaw into processDAGDocument for subsequent docs when they don't
declare workspace labels; update the loop in loadDAGsFromData (and the analogous
logic around processDAGDocument at the other mentioned site) to check each doc
for an explicit workspace and otherwise reuse the file-level baseDef/baseRaw so
all documents in the file inherit the same workspace base.

Comment thread internal/core/spec/loader.go Outdated
Comment on lines +640 to +687
func mergeBaseDefinitionData(baseRaw, overrideRaw []byte) (*dag, []byte, error) {
if len(baseRaw) == 0 {
def, err := decodeDefinitionData(overrideRaw, "workspace base config")
return def, overrideRaw, err
}
if len(overrideRaw) == 0 {
def, err := decodeDefinitionData(baseRaw, "base config")
return def, baseRaw, err
}

baseMap, err := unmarshalData(baseRaw)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal base config: %w", err)
}
overrideMap, err := unmarshalData(overrideRaw)
if err != nil {
return nil, nil, fmt.Errorf("failed to unmarshal workspace base config: %w", err)
}

mergedMap := mergeDefinitionMaps(baseMap, overrideMap)
def, err := decode(mergedMap)
if err != nil {
return nil, nil, fmt.Errorf("failed to decode merged base config: %w", err)
}

mergedRaw, err := yaml.Marshal(mergedMap)
if err != nil {
return nil, nil, fmt.Errorf("failed to marshal merged base config: %w", err)
}
return def, mergedRaw, nil
}

func mergeDefinitionMaps(base, override map[string]any) map[string]any {
merged := cloneMap(base)
if merged == nil {
merged = make(map[string]any, len(override))
}
for key, overrideValue := range override {
baseValue, ok := merged[key]
baseMap, baseIsMap := baseValue.(map[string]any)
overrideMap, overrideIsMap := overrideValue.(map[string]any)
if ok && baseIsMap && overrideIsMap {
merged[key] = mergeDefinitionMaps(baseMap, overrideMap)
continue
}
merged[key] = cloneAny(overrideValue)
}
return merged
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Raw map merging drops inherited values when base configs use list syntax.

mergeDefinitionMaps only deep-merges nested map[string]any. Supported manifest unions like env can also be written as lists, and those hit the overwrite path at Line 685, so a workspace base can silently replace global entries instead of layering on top of them. That makes the merged result depend on YAML syntax rather than config meaning. Please merge after decoding/normalizing these union fields, or special-case them before re-encoding the merged raw YAML.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/core/spec/loader.go` around lines 640 - 687, mergeDefinitionMaps
currently only deep-merges nested map[string]any and overwrites when a field is
a list (e.g., env written as YAML list), causing workspace base lists to replace
rather than layer on global lists; fix by normalizing union fields before
merging: in mergeBaseDefinitionData, decode/normalize baseRaw and overrideRaw
into their canonical dag/structured form (using decode or decodeDefinitionData)
or add logic in mergeDefinitionMaps to special-case known union keys (e.g.,
"env") — detect when baseValue or overrideValue is a slice vs map and convert
list syntax into the canonical map/list representation (or merge by name/append
deduplicated entries) before assigning merged[key]; ensure mergedRaw is produced
from the normalized merged structure so YAML syntax differences no longer affect
semantics (refer to mergeBaseDefinitionData, mergeDefinitionMaps, decode,
decodeDefinitionData).

Comment on lines 4258 to 4261
loadOpts := []spec.LoadOption{
spec.WithBaseConfig(a.config.Paths.BaseConfig),
spec.WithWorkspaceBaseConfigDir(workspace.BaseConfigDir(a.config.Paths.DAGsDir)),
spec.WithDAGsDir(a.config.Paths.DAGsDir),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Workspace base config is still skipped on the default reschedule path.

This only fixes the useCurrentDAGFile branch. The default branch still rebuilds from snapshotDAG.YamlData via loadInlineDAG on Line 3031, and that loader never reapplies the workspace layer or preserved snapshotDAG.BaseConfigData. As a result, rescheduling a named-workspace DAG still drops workspace-scoped settings unless callers explicitly opt into the current-file path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/service/frontend/api/v1/dagruns.go` around lines 4258 - 4261, The
default reschedule path rebuilds the DAG using
loadInlineDAG(snapshotDAG.YamlData) without reapplying workspace or preserved
base-config layers, so when rescheduling a named-workspace DAG workspace-scoped
settings are lost; update the rebuild to pass the same spec.LoadOption values as
the useCurrentDAGFile branch (include
spec.WithWorkspaceBaseConfigDir(workspace.BaseConfigDir(...)) and
spec.WithBaseConfig populated from snapshotDAG.BaseConfigData or the persisted
base config) or change loadInlineDAG to accept and apply those load options and
honor snapshotDAG.BaseConfigData so the workspace layer and preserved
base-config are reapplied during the snapshot-based load.

Comment on lines +33 to +38
// BaseConfigPath returns the workspace-scoped base config path for name.
func BaseConfigPath(dagsDir, name string) string {
if strings.TrimSpace(dagsDir) == "" || strings.TrimSpace(name) == "" {
return ""
}
return filepath.Join(BaseConfigDir(dagsDir), name, BaseConfigFileName)
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validate name before building the workspace config path.

filepath.Join(BaseConfigDir(dagsDir), name, BaseConfigFileName) will happily turn ../other into a path outside workspaces/, so any caller that forgets to validate the workspace name can read or write the wrong file. Since this helper already returns "" for invalid input, it should also reject names that fail ValidateName.

Suggested guard
 func BaseConfigPath(dagsDir, name string) string {
 	if strings.TrimSpace(dagsDir) == "" || strings.TrimSpace(name) == "" {
 		return ""
 	}
+	if err := ValidateName(name); err != nil {
+		return ""
+	}
 	return filepath.Join(BaseConfigDir(dagsDir), name, BaseConfigFileName)
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/workspace/workspace.go` around lines 33 - 38, The BaseConfigPath
function currently builds a path with the raw name which allows names like
"../other" to escape the workspaces directory; call ValidateName(name) (or
equivalent workspace name validator) at the start of BaseConfigPath and return
"" if validation fails so only validated names are joined (i.e., add a guard: if
!ValidateName(name) return "") before calling BaseConfigDir and filepath.Join.

Comment on lines +87 to +90
useEffect(() => {
setWorkspaceValue(null);
setWorkspaceDirty(false);
}, [selectedWorkspace, remoteNode]);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset global scope state on remoteNode change to avoid stale/cross-node writes.

The workspace scope correctly resets its local state when remoteNode (or selectedWorkspace) changes, but there is no equivalent reset for the global scope. Because the init effect at Lines 69-73 only seeds globalValue when it is null, switching remote nodes keeps the previous node's globalValue (and any globalDirty edits) in the editor even though globalData refetches against the new node. If the user had unsaved edits before switching, clicking Save will PUT the old node's content to the new node's /settings/base-config endpoint.

🛠️ Proposed fix
   useEffect(() => {
     setWorkspaceValue(null);
     setWorkspaceDirty(false);
   }, [selectedWorkspace, remoteNode]);
+
+  useEffect(() => {
+    setGlobalValue(null);
+    setGlobalDirty(false);
+  }, [remoteNode]);
📝 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.

Suggested change
useEffect(() => {
setWorkspaceValue(null);
setWorkspaceDirty(false);
}, [selectedWorkspace, remoteNode]);
useEffect(() => {
setWorkspaceValue(null);
setWorkspaceDirty(false);
}, [selectedWorkspace, remoteNode]);
useEffect(() => {
setGlobalValue(null);
setGlobalDirty(false);
}, [remoteNode]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ui/src/pages/base-config/index.tsx` around lines 87 - 90, The global scope
isn't reset when remoteNode (or selectedWorkspace) changes, leaving stale
globalValue/globalDirty and risking cross-node saves; add a useEffect that
listens to [selectedWorkspace, remoteNode] and calls setGlobalValue(null) and
setGlobalDirty(false) (mirroring the workspace reset) so globalValue will be
re-seeded from globalData for the new node; reference the existing state setters
setGlobalValue and setGlobalDirty and the globalData/init effect that seeds
globalValue when null.

@yottahmd yottahmd merged commit 59d92fc into main May 6, 2026
11 checks passed
@yottahmd yottahmd deleted the codex/workspace-base-config branch May 6, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant