feat(cron): allow assigning a Schedule-page folder when creating a cron - #7249
Conversation
3b36928 to
1996e94
Compare
GPT 5.6 Review (fork) — ✅ no blocking findingsReviewed Review detailsFINDING -- src/kiro_crew/apps/bridges.py:1603 -- |
Design Review (Fable 5, fork) — 🟡 CONCERNSDesign-level review of Design-Verdict: CONCERNS Sound single-writer-respecting design; but the app-manifest leg quietly under-delivers the PR's own "reproducible on a fresh install" motivation. Watch
Suggestions
[DESIGN-REVIEWED] af875bd |
First Principles Review (Fable 5, fork) — 🟡 CONCERNSPremise-level review of All claims verified against the base. Findings are settled; here is the review. First-Principles-Verdict: CONCERNS Every surface earns its place except What this change shipsIntent: let a user's cron-folder organization be set at creation time and survive reinstalls — an ADDITION.
Watch
Subtractions
[FIRST-PRINCIPLES-REVIEWED] af875bd |
Opus 4.8 Review (fork) — ✅ no blocking findingsReviewed Review detailsBoth candidates are self-labeled low confidence, and both fail the falsification bar: Candidate 1 (non-idempotent folder auto-create) is a concurrency race — its (a) is "two concurrent creates of the same not-yet-existing name," a "could," not a condition that occurs in practice on a single-writer path. It also depends on a pre-existing endpoint property ( Candidate 2 (prefix entry exposes PATCH/DELETE to internal-secret callers) has no grounded (c): the internal secret is held only by trusted components, and the candidate itself concedes "there is no MCP tool that lets a steered agent hit the raw DELETE/PATCH route." No untrusted call path reaches those verbs, and it mirrors the deliberate Step 2: the new resolver correctly separates unreadable/ambiguous/missing outcomes, the MCP create leg is the only one that creates and it routes through the dashboard endpoint under the shared lock, id-shaped misses are refused, and every non-MCP surface (CLI, app manifest) is read-only and degrades safely. Nothing new rises to the 80+ bar. No findings. [OPUS-REVIEWED] af875bd |
1996e94 to
de56712
Compare
The finding holds. Fixed in
The other two call sites were checked and neither runs on an event loop, so both stay synchronous: Verified: |
iamwhatever
left a comment
There was a problem hiding this comment.
Same issue about duplicate code was raised in two reviews, so please fix them.
- Fold the match core into one function: have _resolve_cron_folder call lookup_cron_folder_id (returning a structured miss) and keep only the id-shape refusal and create leg local.
- Drop folder_id from CronSDK.add_job and add_job_async — grep of src/kiro_crew/apps/builtins shows 0 callers, and bridges consumes only add_job_if_absent_async (bridges.py:1595); keep it on the consumed method and _common_add_kwargs.
de56712 to
791c42a
Compare
|
@iamwhatever both done in 1. One match core.
The two existing callers moved to the named fields: 2. Gates on the rebased head: 234 passed / 2 skipped on the cron, SDK and bridges suites, |
cron_add/cron_update (MCP) accept a 'folder' argument (name or id; a missing name is created through POST /api/cron-folders, the dashboard's own endpoint, so the create shares the Schedule page's lock and in-memory list). 'kirocrew cron add' gains --folder (existing folders only). App manifests can set 'folder' on a cron entry (a NAME, resolved at registration; unresolved degrades to ungrouped with a warning and is re-applied on the next enable, so the assignment survives the disable/enable delete-and-recreate cycle). Shared read-only resolver in cron.py (load_cron_folders / lookup_cron_folder_id): cron_folders.json stays dashboard-owned; no non-dashboard surface writes it directly.
791c42a to
af875bd
Compare
|
Disposition:
The finding holds and the reachable path was exactly as described. Fixed at the read, not at the branch, so the distinction cannot be lost again by a later caller. New
Four tests added, one of which closes the hole that let this through: the pre-existing |
Open PR relationship auditThis is a consolidated, point-in-time code-level audit note. It compares complete merge-base diffs and current/merged code; it does not treat a shared topic as duplication or partial coverage as completion. Relationship findings
No PR, Issue, label, branch, or review state was changed by the relationship-note portion of this audit. |
Problem / Motivation
Cron folders exist in the Schedule page, but no creation path can set one: the
cron_addMCP tool has no folder parameter, the CLI has no flag, and app-manifest crons cannot carry a folder at all — worse,app disable/app enabledeletes and recreates the jobs, so a folder set by hand on an app cron is lost at every reload. Folder organization is not reproducible on a fresh install.Why it matters
Anyone who organizes more than a handful of crons has to re-file them by hand in the UI after every app reload or reinstall. Same class of gap as the app-cron timezone before #6020: the field exists on the live
Job(CronService.add_jobalready acceptsfolder_id), but no creation surface transports it.What changed (motivation → approach → change)
Goal: let every creation path name a folder, without breaking
cron_folders.json's single-writer model — the dashboard state holds the canonical in-memory folder list and rewrites the file wholesale, so an out-of-band append can be silently clobbered by the next Schedule-page folder operation.Approach: a shared read-only resolver (
load_cron_folders/lookup_cron_folder_idincron.py) for every surface; folder creation only from the MCP path, throughPOST /api/cron-folders— the dashboard's own endpoint, so the create runs under the same lock and lands in the same in-memory list as a Schedule-page create. This is the same loopback-with-internal-secret pattern the chat-folder MCP tools already use (_post("/api/chat/folders", ...)inmcp_dashboard.py).What was built:
cron_add/cron_update: new optionalfolderargument (name or id). Resolution order: exact id, then case-insensitive name (refused when several folders share it); a missing name is created via the endpoint; an id-shaped miss is refused rather than created (ids are minted server-side — same contract as the chat-folder resolver). Resolved beforeadd_job, in the same position as the model check, so a bad reference never strands an orphaned job. Oncron_update, an explicit""moves the job out of its folder.kirocrew cron add --folder <name|id>: resolves existing folders only and exits with a clear message pointing to the Schedule page otherwise — the CLI may run with no gateway up, and creating by writing the file directly is exactly the clobber case above.CronEntry.folder(a NAME, not an id — manifests are portable across installs and ids are minted per-machine). Propagated through_cron_defs_from_manifestandCronSDK(folder_idthreaded into the single locked first save, liketimezonein fix(apps): let an app set a cron job timezone and skip_dates at create #6020). Resolved at registration; an unresolved folder registers the job ungrouped with a logged warning rather than failing the app's enable (the job matters more than its grouping), and since enable re-registers from the manifest, the assignment is re-applied every cycle — which is what makes it survive the disable/enable delete-and-recreate.Alternative considered and rejected: having non-dashboard surfaces create folders by appending to
cron_folders.jsonunder a file lock — the dashboard's next wholesale save from its in-memory list would silently erase the entry until a gateway restart./api/cron-foldersadded to_MIXED_INTERNAL_API_PATHSindashboard/server.py.token_auth's prefix match ispath == p or path.startswith(p + "/"), so the existing/api/cronsentry does not cover/api/cron-folders— without this the MCP create leg falls through to cookie auth and 403s with "Token required". Same trap the neighbouring/api/artifact-folderscomment records;test_mcp_call_site_auth_coverage.pyis what caught it.Tests
test/test_cron_folder_on_create.py(24 tests):/api/cron-foldersexactly once, a create failure is surfaced (not swallowed), id-shaped misses are refused.folderround-tripsto_dict/from_dict, is omitted when empty, degrades non-string values to empty, and is carried by_cron_defs_from_manifest.CronServiceon an isolated store: an existing folder name lands as the job'sfolder_id; an unknown name registers the job ungrouped.--folderfiles the job into an existing folder; an unknown folder exits non-zero without creating the job.cron_add/cron_update, and the MCP tool declarations exposefolder.Updated:
test_app_bridges.py(two exact-call assertions gainfolder_id=""),test_cron_sdk.py(mock job gains the field),test_cli.py(sixTestCronCliexact-call assertions gainfolder_id=""),test_mcp_call_site_auth_coverage.py(mcp_cron.pyadded to_SOURCES— it now imports_post, so the guard must scan its call sites; that scan is what surfaced the missing/api/cron-foldersallowlist entry).Manual verification
Not performed against a live gateway: the POST-create path is exercised with the endpoint mocked, and the registration/CLI paths run against a real
CronServiceon an isolated store. The one inference a live pass would replace with observation — that/api/cron-foldersaccepts the internal-secret handshake from the MCP process — is now enforced statically instead:test_mcp_call_site_auth_coverage.pyresolves that call site's path and asserts it is in an internal allowlist, which is exactly the check that caught it missing.Related Issues
Fixes #7248
Checklist
feat|fix|docs|refactor|perf|test|chore|ci|build|revert: ...)Contribution License Agreement