feat(workspace-server): seed schedules from workspace-template config.yaml #1929
Reference in New Issue
Block a user
Delete Branch "feat/seed-schedules-from-ws-template"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Adds a new
template_schedules.gohelper that parses a workspace template'sconfig.yamlfor itsschedules:block and INSERTs each entry intoworkspace_scheduleswithsource='template'. Mirrors the org/import path (org_import.go) so a workspace created directly from a workspace template lands with the same schedule grid the org/import path would have produced.Closes the gap the SEO Agent template hit: the template documented a "comprehensive schedule" (in
source/.../recommended-schedule.mdand the matchingconfig.yamlschedules block landing in molecule-ai-workspace-template-seo-agent#12), but the workspace-server provisioner never consumedschedules:from a workspace template — only the org/import path seededworkspace_schedules.Wiring
handlers/template_schedules.goparseTemplateSchedules(templatePath)— minimal YAML parse ofschedules:only; returns(nil, nil)whenconfig.yamlis absent or the block is empty. Errors only on read/parse failure of a present file.seedTemplateSchedules(ctx, workspaceID, templatePath, schedules)— per-entry resolve+insert via the canonicalorgImportScheduleSQLconstant fromorg.go. Reuses the existingresolvePromptRef+scheduler.ComputeNextRunhelpers. Per-row failures are logged and skipped; never fatal.handlers/workspace.goWorkspaceHandler.Createcalls the parser + seeder aftertemplatePathresolves and beforeprovisionWorkspaceAutoruns. Non-fatal — a brokenschedules:block can never block workspace provisioning.handlers/template_schedules_test.goparseTemplateSchedules: absent file, empty path, noschedulesblock, happy path (3 entries incl. explicitenabled: false), malformed YAML.Issue #24 contract preserved
Both the new path and the existing org/import path execute the same
orgImportScheduleSQLconstant, so the four guarantees stay enforced:WHERE source='template')TestImport_OrgScheduleSQLShapealready gates that SQL's shape against regression.Test plan
go vet ./...— cleango build ./...— cleangofmt -don changed files — cleango test ./workspace-server/internal/handlers/— PASS (incl. 5 new unit tests)Limitations / non-goals
org_import.goinsert loop is not refactored to shareseedTemplateSchedules. The shared piece (the SQL) is already centralized viaorgImportScheduleSQL; org_import usescontext.Background()(long-running async tx) which differs from Create's request ctx. Keeping the call sites separate keeps this PR focused.${TENANT_TIMEZONE}) is not added in this PR. Mirrors current org/import behavior; template authors pick a literal IANA zone (or rely on UTC + per-tenant operator overrides). Tracked as a follow-up.Companion PR
molecule-ai/molecule-ai-workspace-template-seo-agent#12 — adds the SEO Agent
schedules:block this PR teaches the provisioner to consume.🤖 Generated with Claude Code
Adds a new template_schedules.go helper that parses a workspace template's config.yaml for its `schedules:` block and INSERTs each entry into workspace_schedules with source='template'. Mirrors the org/import path (org_import.go) so a workspace created directly from a workspace template lands with the same schedule grid the org/import path would have produced. Closes the gap the SEO Agent template hit: the template documented a "comprehensive schedule" (in source/.../recommended-schedule.md and the matching config.yaml schedules: block from molecule-ai-workspace-template-seo-agent#12), but the workspace- server provisioner never consumed `schedules:` from a workspace template — only the org/import path seeded workspace_schedules. Wiring: - New: handlers/template_schedules.go * parseTemplateSchedules(templatePath) — minimal YAML parse of `schedules:` only; returns (nil, nil) when config.yaml is absent or the block is empty. Errors only on read/parse failure of a present file. * seedTemplateSchedules(ctx, workspaceID, templatePath, schedules) — per-entry resolve+insert via the canonical orgImportScheduleSQL constant from org.go. Reuses the existing resolvePromptRef + scheduler.ComputeNextRun helpers. Per-row failures are logged and skipped; never fatal. - Modified: handlers/workspace.go * WorkspaceHandler.Create calls parseTemplateSchedules + seedTemplateSchedules after the templatePath resolves and before provisionWorkspaceAuto runs. Non-fatal — a broken schedules: block can never block workspace provisioning. * Schedules are seeded once at workspace creation; Restart does not re-seed (so user-deleted template rows stay deleted). - New: handlers/template_schedules_test.go * Table-driven coverage for parseTemplateSchedules: absent file, empty path, no schedules block, happy path (3 entries inc. explicit enabled: false), malformed YAML. Issue #24 contract preserved (additive, idempotent, runtime-row preservation, never-DELETE) because both the new path and the existing org/import path execute the same orgImportScheduleSQL constant — and TestImport_OrgScheduleSQLShape already gates that SQL's shape against regression. Verified locally: go vet ./... → clean go build ./... → clean gofmt -d <changed files> → clean go test ./internal/handlers/ → PASS (incl. 5 new tests) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Self-review summary
I ran a two-axis review on this PR via Claude Code subagents (code-correctness + security) before requesting platform review. Both initial verdicts: REQUEST_CHANGES. Fixes landed in
b9a3ef4; current SHA is what the watcher is tracking.Code-correctness findings (addressed)
provisionWorkspaceAuto; failed-backend workspaces could end up with orphanworkspace_schedulesrows. Fix: seed only whenprovisionWorkspaceAutoreturnstrue.seedTemplateSchedulesreturned onlyint seeded; per-row DB errors were silent. Fix: returns(seeded, skipped int); Create logs partial-seed warnings.Security findings (addressed)
io.LimitReaderon the YAML file (anchor-bomb defense),maxTemplateSchedules=100,maxScheduleCronExprLen=128,maxSchedulePromptBytes=16<<10. Oversized rows are skipped, not committed; ctx.Err() honoured inside the seed loop.%q, neutralizing CRLF in attacker-controlled names.Findings not addressed in this PR (rationale)
schedules:block, so benign in practice; documented in the new comment block.Defenses preserved
prompt_file: routed throughresolvePromptRef→resolveInsideRoot; existingTestResolvePromptRef_RejectsTraversalstill gates.$1..$7args into canonicalorgImportScheduleSQL; no string concat.TestImport_OrgScheduleSQLShapestill gates the shape.(workspace_id, name)+WHERE source='template'— hostile names cannot mutate other workspaces.Local CI
go vet ./...— cleango build ./...— cleangofmt -d <changed>— cleango test ./workspace-server/internal/handlers/— PASS (7 parser unit tests incl. 2 new bounds tests; full handlers suite green 17.4s)QA review — APPROVE
Automated review on behalf of the QA axis. Findings from a code-correctness subagent on the initial commit
d8b409f:provisionWorkspaceAuto; orphan-schedule risk on failed-backend workspaces.Both addressed in
b9a3ef4:provisionWorkspaceAuto == trueseedTemplateSchedulesreturns(seeded, skipped int); Create logspartial-seed: seeded=N skipped=M total=Kwhenskipped > 0TestImport_OrgScheduleSQLShapestill passes — the Issue #24 SQL contract (additive / idempotent / preserve-runtime-rows / never-DELETE) is preserved because both paths reuse the sameorgImportScheduleSQLconstantNote: this PR review approval is a soft signal from the QA persona — the
qa-review / approvedCI status check is set by a separate agent path and is not flipped by this review.—
core-qa(automated, dispatched by the workspace template-schedule wiring author)Security review — APPROVE
Automated review on behalf of the Security axis. Threat model: workspace-template
config.yamlis partially attacker-influenced (templates can be webhook-synced or uploaded viaPOST /templates/import).Findings on initial commit
d8b409f(REQUEST_CHANGES)len(schedules), prompt body, cron length, orconfig.yamlfile size.LimitReaderaroundos.ReadFile→yaml.Unmarshal.sched.Nameflowing intolog.Printfun-escaped.Fixes landed in
b9a3ef4parseTemplateSchedulesnow opens the file and reads throughio.LimitReader(f, 1<<20)(1 MiB hard cap); rejects oversize beforeUnmarshalruns.maxTemplateSchedules=100,maxScheduleCronExprLen=128,maxSchedulePromptBytes=16<<10.seedTemplateScheduleshonoursctx.Err()inside the loop (aborted Create stops further inserts).%qforsched.Name— CRLF neutralised.TestParseTemplateSchedules_RejectsOversizeFile,TestParseTemplateSchedules_RejectsTooManySchedules.Defenses verified intact
prompt_file: routed throughresolvePromptRef→resolveInsideRoot(CWE-22).orgImportScheduleSQL(CWE-89).ON CONFLICT (workspace_id, name) DO UPDATE … WHERE source='template'— hostile names cannot mutate other workspaces.POST /workspacesgated bymiddleware.AdminAuth— seeder unreachable without admin token.Out-of-scope, file follow-up issue
Note: this PR review approval is a soft signal — the
security-review / approvedCI status check is set by a separate agent path and is not flipped by this review.—
core-security(automated, dispatched by the workspace template-schedule wiring author)