feat(config): persist per-project agent config and resolve it at spawn#154
feat(config): persist per-project agent config and resolve it at spawn#154neversettle17-101 wants to merge 8 commits into
Conversation
Each project can now carry its own agent config (model, permissions,
adapter-specific keys) that survives daemon restart and is resolved into
the launch command when a session spawns.
- storage: add nullable projects.agent_config JSON column (migration 0008);
marshal/unmarshal in the store so the domain carries map[string]any
- resolution: session manager loads the project row and populates
LaunchConfig.Config before GetLaunchCommand
- validation: claude-code declares a ConfigSpec (model, permissions) and
rejects unknown keys / bad types / bad enums at spawn; it applies the
model override and config-driven permission mode (explicit Permissions
still wins)
- surface: PUT /projects/{id}/agent-config + `ao project set-config`
(--set/--config-json/--clear), config shown in `ao project get`
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Greptile SummaryThis PR wires the previously-dormant per-project agent config half of the project registry. A typed
Confidence Score: 5/5Safe to merge; the core spawn/restore config-resolution path is correct and well-tested, including the previously-flagged restore fallback, which now properly carries the project config. The typed-config approach replaces the old free-form map cleanly, validation is enforced at write time (service) and again at launch (adapter), and both Spawn and Restore thread the resolved config through all sub-paths. The two flagged items are confined to authenticated-user-controlled config fields and do not affect spawn correctness or data integrity. backend/internal/session_manager/manager.go (applySymlinks and projectRules path handling) and backend/internal/storage/sqlite/store/project_store.go (silent unmarshal failure) Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI as ao project set-config
participant HTTP as PUT /projects/{id}/config
participant SVC as project.Service
participant DB as SQLite store
participant SM as session_manager.Spawn
participant WS as gitworktree.Workspace
participant Agent as claudecode adapter
CLI->>HTTP: "PUT {config: {model, permissions, ...}}"
HTTP->>SVC: SetConfig(id, SetConfigInput)
SVC->>SVC: ProjectConfig.Validate()
SVC->>DB: GetProject then UpsertProject (JSON column)
DB-->>SVC: updated row
SVC-->>CLI: Project read-model
Note over SM,Agent: At spawn time
SM->>DB: GetProject(projectID)
DB-->>SM: "ProjectRecord{Config}"
SM->>SM: effectiveHarness(kind, config)
SM->>SM: effectiveAgentConfig(kind, config)
SM->>WS: "Create(WorkspaceConfig{BaseBranch})"
SM->>SM: applySymlinks + runPostCreate
SM->>SM: projectRules (AgentRules + AgentRulesFile)
SM->>Agent: "GetLaunchCommand(LaunchConfig{Config, Permissions})"
Agent->>Agent: AgentConfig.Validate()
Agent-->>SM: argv [claude --model X ...]
Reviews (6): Last reviewed commit: "fix(config): fail-safe paths for missing..." | Re-trigger Greptile |
…led types Address review on per-project agent config validation: - handle ConfigFieldStringList (list of strings) explicitly - reject unhandled ConfigFieldType via a default case rather than silently passing - enforce Required fields are present Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the free-form map[string]any agent config with a typed
domain.AgentConfig{Model, Permissions} so values are validated when set
(CLI/API) instead of silently dropped at spawn, and the OpenAPI/TS schema
and UI get real typed fields.
- domain: AgentConfig struct + Validate(); PermissionMode moves to domain
and ports re-exports it as a type alias (zero adapter churn)
- storage: marshal/unmarshal the typed struct (IsZero → SQL NULL)
- service: validate on Add and SetAgentConfig; read-model exposes a typed
*AgentConfig
- claudecode: read typed cfg.Config.Model/.Permissions; drop the
map/spec-based validateConfig in favor of the typed Validate()
- cli: typed `ao project set-config --model/--permission/--clear`
- docs: add docs/design/per-project-config.md blueprint sequencing the
remaining # Projects fields toward fully typed per-project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…urface)
Expand per-project config from agentConfig-only to the full legacy
`projects.<id>` surface, modeled as one typed domain.ProjectConfig
persisted in a single projects.config JSON column.
Wired end-to-end at spawn:
- defaultBranch → base branch for the session worktree (ports.WorkspaceConfig.BaseBranch)
- env → merged into the runtime env (AO-internal vars still win)
- symlinks → repo files linked into the workspace
- postCreate → commands run in the workspace (OS-agnostic shell)
- agentRules / agentRulesFile / orchestratorRules → merged into the prompt
- worker/orchestrator role overrides → harness + agent-config resolution
Stored + validated + surfaced now, consumption deferred (no consumer yet):
tracker, scm(+webhook), opencodeIssueSessionStrategy; sessionPrefix feeds
the display prefix only (session-id generation unchanged).
Validation lives on domain.ProjectConfig.Validate() and runs when config is
set (CLI/API). PermissionMode/AgentConfig stay typed; harness names validated
via domain.AgentHarness.IsKnown().
Surface: PUT /projects/{id}/config (replaces /agent-config) + typed
`ao project set-config` flags (--default-branch/--env/--symlink/--post-create/
--agent-rules/--worker-agent/… or --config-json). OpenAPI + TS regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add domain.DefaultProjectConfig / ProjectConfig.WithDefaults with a single
DefaultBranchName ("main") source of truth, replacing the literal "main"
scattered in the read-model and the gitworktree adapter. Unconfigured
projects now resolve the default branch through one path; every other field
defaults to its zero value.
Tests: defaults present for all fields (DefaultProjectConfig/WithDefaults),
and an unconfigured project reports the default branch + derived session
prefix while omitting the empty config object.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Review: default-config / fail-safe pathsReviewing specifically from the lens of: if a user defines no config (or partial config), spawning should never fail. The overall direction is correct — 1.
|
Address review on default-config / fail-safe spawning: - projectRules: a missing AgentRulesFile is optional context, skipped rather than aborting every spawn (only a real read error surfaces) - store: a corrupt config JSON column degrades to a zero config instead of failing GetProject/ListProjects/FindProjectByPath for that row - restore: re-apply the project's resolved AgentConfig so a configured model/permissions carry across a restore (matches fresh spawn) Tests: missing rules file skips, corrupt config degrades to zero, restore applies the project agent config. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Thanks — great fail-safe review. Addressed in 1f86e8c: 1. AgentRulesFile typo → spawn failure (high) — Fixed. 2. Corrupt config JSON blocks the project (medium) — Fixed. 3. Restore doesn't apply AgentConfig (low) — Wired it now rather than deferring: Restore loads the project and passes 4. IsZero / reflect.DeepEqual note — Keeping |
Closes #107
Summary
Wires the previously-dormant per-project agent config half of the project registry. A project can now store agent config (model, permissions, adapter-specific keys) that survives daemon restart and is resolved into the launch command at spawn.
The DB row is authoritative, written through the existing
ao projectCLI → daemon HTTP → SQLite path (no YAML loader exists in the Go rewrite — see note below).Changes
projects.agent_configJSON column (migration0008). The store marshals/unmarshals sodomain.ProjectRecord.AgentConfigis a plainmap[string]any(matchesports.AgentConfig, no translation layer). Unset round-trips tonil, not{}.session_managerloads the project row and populatesLaunchConfig.ConfigbeforeGetLaunchCommand. A missing project yields a nil config (adapter defaults) rather than a spawn failure.claude-codenow declares a realConfigSpec(model,permissions) and validatescfg.Configagainst it at spawn: unknown key, wrong primitive type, or out-of-set enum → clear error. It applies themodeloverride (--model) and a config-driven permission mode (an explicitLaunchConfig.Permissionsstill wins).PUT /api/v1/projects/{id}/agent-config+ao project set-config <id>(--set key=value,--config-json,--clear); config shown inao project get. OpenAPI + frontend TS schema regenerated.Note on the issue comments
The "solution design" comment framed this as YAML-authoritative with a write-through cache and a config sync loop. That's the legacy TS model — the Go rewrite has no YAML project loader (global config is env vars; projects come via
ao project add). This PR follows the issue body + the first comment: the DB row is the source of truth, written via the CLI/API. The file checklist from that comment is otherwise followed (migration number bumped0004→0008).Acceptance criteria
AgentConfiginLaunchConfig.Config.Tests
SetAgentConfigpersists + not-found path🤖 Generated with Claude Code