feat(templates): materialize a template's declared schedules: at agent creation (trinity-enterprise#89) - #1946
Conversation
…ent#89) Requirements-first per Rule of Engagement #1 — written and committed before any implementation. Covers the declared `schedules:` contract, the total-function reader and its tolerance matrix, the normalized carrier that feeds BOTH resolver branches (the `github:` half needs the creation-resolved PAT + parsed ref, not the catalog's global-PAT cached fetch), the honored-`enabled` decision and its `set_autonomy_status` caveat, idempotency in all three places creation / intra-block / manifest deploy, and the T-018 + A-002 + c_p006 compatibility half. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
New leaf `services/template_schedules.py`: `schedule_shape_errors` + `normalize_declared_schedules` over one private `_parse`, mirroring the sibling `credential_shape_errors` / `credential_mcp_server_names` convention (ent#128). The contract is TOTALITY — template.yaml is untrusted and `yaml.safe_load()` can yield a scalar, list or mapping at any level, so a raise here would empty the template catalog (#1835 class), enter the creation rollback fence, or fail-open the T-018 check. Every shape degrades to a safe value plus a named error. Cron and timezone are validated with the SAME parser the dedicated scheduler registers with (#1472) because `_calculate_next_run_at` swallows a bad cron and `set_schedule_enabled` never re-validates — an unvalidated entry would become a zombie schedule that exists, shows no next run, and can never fire. Errors name the index, the key and a YAML type — never the `name`/`message` VALUE, which is unbounded and lands in a persisted, UI-rendered blob. The cron/timezone strings are the one echo, bounded and printable-filtered by a local twin of `_sanitize_for_warning` (importing it would close a cycle). Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tHub list paths (ent#89) Three changes in one file: 1. `_build_template` (GitHub) and `_build_local_template` (local) both surface `schedules` (normalized) + `schedule_errors`. BOTH, explicitly — the pre-existing asymmetry (`persistent_state` is surfaced only by the GitHub builder) means parity cannot be assumed, and AC #2 covers both sources. 2. R3 — both GitHub catalog list paths were BARE list comprehensions. ent#128 PR-A fenced `_build_local_template` only, so adding an untrusted-input reader to `_build_template` would have put a new raise-capable call on an unfenced path: one malformed repo would 500 the whole GitHub half of GET /api/templates. That is the #1835 bug this feature is modelled on, re-opened by the feature itself. Both are now fenced per-template via `_safe_build_github_template`. 3. `fetch_template_metadata_for_create` — the creation path must NOT read the catalog cache. That cache uses the GLOBAL platform PAT (creation resolves per-agent -> per-user -> global, ent#162), sends no `?ref=` (so an `@branch` create would read the default branch), and is a 10-minute per-process TTL. The new fetch takes the resolved PAT + parsed ref, and is loud on every failure — a silently empty declaration on the `github:` path is the exact class this feature exists to close. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (ent#89) `_TemplateResolution.declared_schedules` is a normalized carrier populated by BOTH resolver branches, so AC #2's "GitHub and local" is real rather than nominal. It is deliberately not folded into `template_data`: that field is raw template YAML on the `local:` path and `{}` on the `github:` path — which has never populated it, so #383's `persistent_state` and #1169's `data_paths` are effectively `local:`-only — and `_stage_config_files` gates credential-file generation on `if template_data:`, so merging the two shapes would change credential generation for every GitHub agent. `reconcile_declared_schedules` is shaped as a reconcile primitive (takes `agent_name`, not `AgentConfig`) so a future "re-apply template" can reuse it. No recreate hook is added — an eager re-materialize would resurrect schedules an operator deliberately deleted. Two failure modes handled explicitly: - `db.create_schedule` RETURNS None on three paths (unknown user, no access, the #1445 is_agent_live gate) and never raises, so a try/except alone would catch nothing and a length-derived counter would report schedules that were never written. The return value is checked and counted as failed. - The whole step is non-fatal and the try/except wraps the entire call including `list_agent_schedules` — this function sits inside the destructive rollback fence, so an escaping raise would roll back a successful creation over a schedule. `enabled` is passed explicitly (ScheduleCreate defaults it to True, which would invert AC #3), and ghosts are skipped at the caller per ent#69 fleet hygiene. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lized (ent#89) `deploy_manifest` creates each agent and THEN calls `create_schedules`, so post-ent#89 it is the second schedule producer for the same agent: the first call materializes the template's declared block, the second adds the manifest's. With no UNIQUE(agent_name, name) index, a manifest declaring `daily-briefing` on a template that also declares it produced two rows. This is a regression ent#89 itself creates, so the guard ships with it rather than as a follow-up. Fails open on a read error — dropping a manifest's schedules would be worse than the duplicate this prevents. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ctions (ent#89) T-018 (SOFT, STATIC) reports the `schedules:` block's STRUCTURE only, sharing one reader with the materializer and the catalog surface so the report cannot drift from what creation actually does. It deliberately does not report cron syntax — see correction 2. It fails CLOSED, against the grain of every other check: `run_static` turns a raise into `skipped` and the report counts only `fail`, so a raising SOFT check drops soft_count 1->0 and flips overall_status issues->compatible exactly when its finding was the only failure — the entire population T-018 exists to serve — and `_report_from_persisted` then replays that clean bill of health from checks_json on every stopped-agent read. Detail carries the exception TYPE only; `str(e)` can embed untrusted template content into a persisted, UI-rendered blob. Three corrections, all in blast radius: 1. `c_p006` was missing the `isinstance(..., list)` guard its four sibling readers of this field all carry. `schedules: 5` raised TypeError -> swallowed -> a HARD check silently vanished from hard_count. A live instance of the exact class T-018 guards against, which is what makes the fail-closed design evidence-backed rather than theoretical. 2. `_valid_cron` (A-002) was a per-field `^[\d*/,\-]+$` regex, wrong in BOTH directions: it rejected `0 9 * * MON` and accepted `99 99 * * *`. It now delegates to `validate_cron_expression` — the same parser the scheduler registers with (#1472) and the same one the ent#89 reader gates on. One cron authority, agreeing with the executor. 3. `run_static`'s swallow now logs. It previously left no trace anywhere, for all ~100 checks; this is the instrument for deciding later whether to flip it to `fail` platform-wide. Catalog 100 -> 101. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (ent#89) The error list is persisted into agent_compatibility_results.checks_json, rendered in the UI, and returned in the catalog response, so the discipline is index + key + type name only. The entry index already identifies the offender; the name added disclosure without adding actionability. Cron and timezone stay the only echoed values — bounded, printable-filtered, and what makes those particular errors fixable. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three new files plus compatibility extensions, 143 tests. test_ent89_template_schedules.py — the reader's tolerance matrix (40 rows), error-string discipline (no name/message/description ever echoed; cron sanitized and bounded), the catalog surface in BOTH builders, the GitHub list-path fence, and the create-path fetch. Plus a Hypothesis property over recursive JSON-ish values asserting totality — a 40-row matrix cannot be a totality proof for `yaml.safe_load` output, and totality is this module's entire contract. test_ent89_schedule_materialization.py — REAL DB rows via db_harness, not `mock.assert_called`. Both recorded lessons apply here: a mock-`db` suite is blind to a facade gap, and a parameter only one branch consumes is a severed wire a mock will happily confirm — which is precisely the failure history of the `github:` half of AC #2. So both resolver branches are driven for real, and the github fetch's PAT and ref are asserted; without that the §0 regression test would be self-attestation. test_ent89_manifest_no_duplicate.py — R5, in creation order: template first, manifest second, one row, and the template's row is not overwritten. test_compatibility_checks.py — T-018 pass/fail/absent, its fail-closed branch (and that `detail` never carries `str(e)`), a `build_report`-level test pinning the DIRECTION (a raising reader must still yield `overall_status == "issues"`), the `_report_from_persisted` recompute, A-002 in both directions and agreeing with the materializer, c_p006 on `schedules: 5`, the run_static log, and the whole static catalog run against 7 hostile templates asserting no check lands at `check_error` — a T-018-only assertion would never have caught c_p006. The two fence tests pin `get_github_templates` by sys.modules KEY rather than by module object: `get_all_templates` imports it lazily inside the function, so an attribute patch on a separately-imported reference passed in isolation and failed under the full suite, where an earlier file swaps that object. Full unit suite: 6405 passed. The one red, test_1069_voip_call_path_param, is a pre-existing venv FastAPI drift (`get_flat_dependant` no longer exported) in a file this branch does not touch. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n class (ent#89) - template_service bullet: the `schedules:` reader beside the `credentials:` ones, the newly fenced GitHub catalog list paths, and why the creation path needs its own metadata fetch rather than the global-PAT default-branch cache. - New `template_schedules.py` leaf bullet with its totality contract. - crud bullet: the `declared_schedules` carrier, why it is not `template_data`, and the non-fatal reconcile step. - Compatibility block: T-018, why it is the one check that fails closed, and the two live instances of that class it fixes (c_p006, _valid_cron/A-002). - template.yaml file-tree note now names its four declarative blocks. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… (ent#89) - template-processing.md: the `schedules:` reader beside the `credentials:` ones, the three deliberate differences (normalized surface, both GitHub list paths fenced, creation does not read the catalog's copy), and the error-string discipline. - scheduling.md: new flow 1c — the fourth schedule producer, why the carrier is not `template_data`, why non-fatality is the invariant, the falsy-return check, and idempotency in all three places. - agent-compatibility-validation.md: T-018, why it is the one check that fails closed, the A-002 cron-authority consolidation, and the c_p006 live instance. - feature-flows.md: Recent Updates row (required even when the flow docs already existed). - learnings.md: the durable half — a per-item swallow plus a count that ignores skips makes a validator's own bug invisible, and persistence replays it. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two gaps the /update-tests coverage checklist surfaced: - Every other create-path-fetch test stubs `_fetch_template_yaml_result`, i.e. BELOW the HTTP layer — so a wrong param name (`?ref=` is what pins the revision) or a dropped Authorization header would leave them all green while the feature silently read the default branch, or read nothing for a private repo. That is precisely the R2 failure this fetch exists to prevent, so it is now asserted at the wire, including the ent#123 tokenless case sending no Authorization header at all. - The unit suite proves both BUILDERS emit `schedules`/`schedule_errors`; only a live call proves the router serves them, that entries are the normalized shape, and that a template reporting errors is still LISTED — the ent#128/#1835 property, which now also covers the two newly-fenced GitHub list paths. Unit run: 208 passed across the four ent#89-touched unit files. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/sync-feature-flows caught what the plan had deliberately trimmed: the `create_schedules()` section documents a function whose behaviour this change alters. Its docstring block gave no hint that the function is now the SECOND schedule producer for the same agent. Adds the name-match skip, why it exists (no UNIQUE(agent_name, name) index and adding one is a dual-track schema change that would fail on installs already holding duplicates), that the skip does not overwrite the template's row including its `enabled` value, and that an unreadable existing set fails open. Index row now lists the flow too. Noted, not acted on: `feature-flows.md` Recent Updates carries 65 dated rows against the ~20 cap #1360 set — pre-existing drift, out of scope here. Refs trinity-enterprise#89 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`fetch_template_metadata_for_create`'s WARNING interpolated `reason` raw while its two neighbours on the same call are `_sanitize_for_warning`-wrapped. The value embeds `str(e)`, and an httpx error message carries the request URL — i.e. the caller-supplied `owner/repo`. A repo with no `/` skips `_GITHUB_REPO_PATH_RE` upstream, so control bytes do reach that line. Bounded at 200 rather than the 80 default: this WARNING exists to be diagnosable, and an 80-char truncation defeats its purpose. Raised as a code-consistency item by the /review pass; the CSO diff audit discarded it as a finding under hard exclusion #9 (log spoofing), so this is hygiene inside one call, not a security fix. Tests pin both halves (control-char stripping and the length bound) and both fail without the change. Also renames a stale `_template_schedule_errors()` reference in the requirements doc and cites #1945 for the autonomy-clobber follow-up the doc previously promised without an issue number. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PASS with 1 MEDIUM (LLM cost amplification: declared schedules meeting the pre-existing `set_autonomy_status` clobber). No CRITICAL, no HIGH. The finding is an amplification of a pre-existing mechanism — there has never been a per-agent schedule cap — so it is filed as #1945 rather than changing this PR. Matches the convention of the 98 reports already tracked here; the file's Trend section is only meaningful alongside its siblings. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Resolve by running |
dev has since taken #1913/#1937/#1947/#1949/#1899. Seven conflicts, resolved so that no side's change is lost: SOURCE - static_checks.py the one real semantic conflict. ent#128 (#1899) flipped the per-check swallow from _skip to _fail so a crashed check is counted by _counts; ent#89 kept _skip and added logging. Taking this branch's side verbatim would have silently reverted the HARD-count fix. Merged: _fail from #1899 + logger.error(exc_info=True) from ent#89, which is strictly more diagnostic than the logger.warning it replaces. The docstring directly above already asserts "a check that could not evaluate is not a check that passed". - template_service three hunks, all adjacent additions: both import blocks kept (template_schedules got its own statement — the two sides shared a closing paren), both new functions kept, and both pre-literal computations kept at each of the two call sites. - crud.py import list, both symbols kept. DOCS - architecture.md two hunks where BOTH sides had edited the same three bullets (template_service / fork_to_own / crud). Not a pick — each line was 3-way merged at word granularity against the merge base; no edit pairs overlapped, so both sides' text survives verbatim. dev's bullet order preserved, ent#89's new template_schedules.py bullet appended. - feature-flows.md all rows kept, table stays reverse-chronological. - learnings.md both sets of entries kept. - registry.json both entry lists kept. The conflict opened after a bare '{' and closed before a bare '}', so each side was an object BODY -- a naive concatenation produced invalid JSON. Re-added the '},{' separator. 109 -> 112 entries, none dropped. Verified: zero markers tree-wide, registry.json parses (112 entries), all three touched modules compile, and every deletion vs origin/dev is one of ent#89's own intended replacements (crud docstring three->four, the cron helper replaced by the shared validator, T-018 wired into the dispatch map).
vybe
left a comment
There was a problem hiding this comment.
/validate-pr: PASS.
- Base
dev✅ · 23 files ✅ · security scan clean ✅ · no new top-level backend module, no newos.getenv()✅ - Docs: requirements §10.16 + architecture + 5 feature flows + a CSO diff audit ✅ · 6 test files ✅
The safety design is the right one, and it composes correctly with what just landed:
enableddefaults to False and only a realboolarms a schedule —enabled: "no"is truthy in Python, so a loose read would arm a schedule its author meant to leave off.MAX_DECLARED_SCHEDULES = 20sits in the reader, not the materializer, so the catalog surface and creation inherit one bound; name/description/message caps close the unbounded-TEXT hole (ScheduleCreatecarries noFieldconstraints)._safe_echois applied to cron/timezone only. Never echoingname/message/descriptionis correct — those are unbounded, prompt-injection-shaped, and this list is persisted intoagent_compatibility_results.checks_jsonand rendered in the UI.- One private
_parsebehind both public functions means the reported errors and the accepted entries are structurally unable to disagree. - The materialize call is wrapped whole — including the helper's own
list_agent_schedulesread — because it sits inside the destructive rollback fence; a raise there would roll back a successful agent creation over a schedule.
Worth noting the interaction: this makes a template able to mint up to 20 recurring autonomous turns, which is exactly the risk #1949 (just merged) removed by making the autonomy toggle a gate rather than a bulk edit. Landing them together is what keeps declared schedules default-off in practice — before #1949, one unrelated autonomy toggle would have armed all 20.
Conflict resolution (mine), pushed as 4e36d284. Seven conflicts. One was a real semantic conflict rather than an adjacency:
static_checks.py— #1899 flipped the per-check swallow from_skipto_failso a crashed check is counted by_counts; this branch kept_skipand added logging. Taking this branch's side verbatim would have silently reverted #1899's HARD-count fix, restoring "broken validator reports healthy". Merged instead:_failfrom #1899 plus this branch'slogger.error(..., exc_info=True), which is strictly more diagnostic than thelogger.warningit replaced. The docstring immediately above already asserts "a check that could not evaluate is not a check that passed", so the merged form is what that text describes.
The rest: template_service.py ×3 and crud.py ×1 were adjacent additions (both kept; template_schedules needed its own import statement since the two sides shared a closing paren, and both pre-literal computations were kept at each call site). architecture.md had both sides editing the same three bullets — 3-way merged at word granularity against the merge base rather than picked, no edit pairs overlapped, so both texts survive verbatim. registry.json's conflict opened after a bare { and closed before a bare }, so each side was an object body and a naive concatenation produced invalid JSON; re-added the },{ separator (109 → 112 entries, none dropped).
Verified: zero markers tree-wide, registry parses, all three touched modules compile, and every deletion vs origin/dev is one of this branch's own intended replacements.
status-in-dev by hand.
#1899 (this branch's parent) was squash-merged, and #1946 landed after it, so the stack surfaced as 7 conflicts. Every one is additive: SOURCE (all three are content this branch simply does not have — ent#89's) - crud.py fetch_template_metadata_for_create import - template_service the template_schedules import, _template_schedules(), and its two call sites - static_checks.py the run_static swallow. This branch carried #1899's logger.warning; dev now has the merged form from #1946 — logger.error(exc_info=True) + _fail. Took dev's: the _fail is #1899's own HARD-count fix and must not regress. DOCS - architecture.md dev's line is a strict superset (same two bullets plus ent#89's template_schedules.py); ent#128's own bullet is byte-identical on both sides. - feature-flows.md all four rows kept, table stays reverse-chronological. - learnings.md both sets of entries kept. - credentials.md §3.6 is this branch's alone; dev added nothing. Verified: zero markers tree-wide, all five touched modules compile, and `git diff origin/dev` reports ZERO deletions in all seven conflicted files — this merge is purely additive.
#1899 (ent#128) and #1946 (ent#89) were both green in isolation and both merged to dev, but four ent#89 tests assert against pre-#1899 API and behaviour, so the INTEGRATION is red on dev. Nothing in the production code is wrong; the assertions describe a contract that #1899 deliberately changed. Caught by the regression-diff job, which runs after the four required checks and therefore after auto-merge had already landed #1946 (dev's required set is Analyze x2 + schema-parity + verify-non-root; it runs no unit tests). tests/unit/test_compatibility_checks.py test_a_raising_check_is_logged run_static's swallow now returns _fail(..., {"check_error": ...}), not _skip(..., "check_error"), so there is no skip_reason to read. Asserts the FAIL shape instead -- which is the point of the change, since only a counted status reaches hard_count/soft_count. A sibling in the same file already asserted "fail" and was passing, so the file was internally inconsistent. test_persisted_check_error_does_not_replay_as_clean Asserted soft_count == 0 for a persisted skipped+check_error row -- i.e. it pinned the BUG as expected behaviour. #1899's _did_not_pass counts that row as a finding on purpose, so the correct expectation is 1. Kept as a real regression test: a row persisted by an OLDER build, before the swallow started returning fail, still must not replay as clean. tests/unit/test_ent89_template_schedules.py (x2) _build_local_template gained a required keyword-only is_bundled parameter in #1899 (it selects the credential-metadata trust label). Both call sites updated; orthogonal to the schedules: block under test. Verified: test_compatibility_checks.py + test_ent89_template_schedules.py + test_ent128b1_compat_gates.py -> 225 passed, 1 skipped. Full tests/unit on this branch -> 6698 passed (the four hypothesis property files need a dependency absent from this host and were excluded).
…1957) #1899 (ent#128) and #1946 (ent#89) were both green in isolation and both merged to dev, but four ent#89 tests assert against pre-#1899 API and behaviour, so the INTEGRATION is red on dev. Nothing in the production code is wrong; the assertions describe a contract that #1899 deliberately changed. Caught by the regression-diff job, which runs after the four required checks and therefore after auto-merge had already landed #1946 (dev's required set is Analyze x2 + schema-parity + verify-non-root; it runs no unit tests). tests/unit/test_compatibility_checks.py test_a_raising_check_is_logged run_static's swallow now returns _fail(..., {"check_error": ...}), not _skip(..., "check_error"), so there is no skip_reason to read. Asserts the FAIL shape instead -- which is the point of the change, since only a counted status reaches hard_count/soft_count. A sibling in the same file already asserted "fail" and was passing, so the file was internally inconsistent. test_persisted_check_error_does_not_replay_as_clean Asserted soft_count == 0 for a persisted skipped+check_error row -- i.e. it pinned the BUG as expected behaviour. #1899's _did_not_pass counts that row as a finding on purpose, so the correct expectation is 1. Kept as a real regression test: a row persisted by an OLDER build, before the swallow started returning fail, still must not replay as clean. tests/unit/test_ent89_template_schedules.py (x2) _build_local_template gained a required keyword-only is_bundled parameter in #1899 (it selects the credential-metadata trust label). Both call sites updated; orthogonal to the schedules: block under test. Verified: test_compatibility_checks.py + test_ent89_template_schedules.py + test_ent128b1_compat_gates.py -> 225 passed, 1 skipped. Full tests/unit on this branch -> 6698 passed (the four hypothesis property files need a dependency absent from this host and were excluded). Co-authored-by: trinity-ability <309458136+trinity-ability@users.noreply.github.com>
Refs trinity-enterprise#89
A template's
template.yamlcan declare aschedules:block. The abilitiesplugin ecosystem treats that block as an agent's design source of truth — but
only
/trinity:syncever reconciled it. Creation via the UI, the API, or MCPmaterialized none of it: you created an agent from a template that declared
its own cadence, and it sat there with an empty Schedules tab. This closes that,
for both
github:andlocal:sources, with every declared schedule landingdisabled by default (issue AC — the operator opts in).
The shape
A new tolerant leaf,
services/template_schedules.py, is the single reader —shared by both catalog builders, the creation-time materializer, and the new
compat check, so the report can never drift from what creation actually does.
It is total by contract: a raise would empty the catalog, enter creation's
rollback fence, or fail-open the compat check. Bounds (
MAX_DECLARED_SCHEDULES=20,name/description/message limits), intra-block name dedupe, and strict cron via
schedule_validation.validate_cron_expression— the scheduler's own parser,not a second regex.
The declared list rides a normalized
_TemplateResolution.declared_schedulescarrier, deliberately not folded into
template_data. That field is raw YAMLon the
local:path and{}on thegithub:path (which has never populatedit — which is why #383's
persistent_stateand #1169'sdata_pathsareeffectively
local:-only), and its truthiness gates credential-file generation.Reusing it would have coupled schedules to a field that means two different
things and load-bears a third.
The bit worth reviewing — the
github:half needs its own fetchThe catalog's cached metadata is read with the global platform PAT off the
default branch. Materializing from that copy means a creator using a
per-user token against a private repo, or pinning
@branch, would havegot zero schedules with no signal — the exact silent-ignore class this
feature exists to close, reintroduced one layer up. So the
github:branch doesits own creation-path read:
fetch_template_metadata_for_create(repo, pat, ref)— resolved PAT, parsed
@branchref, cache-bypassed, and loud on anyfailure.
Three regressions this change would otherwise have created
list comprehensions, so one raise in the untrusted GitHub builder 500'd the
whole GitHub half of
GET /api/templates. ent#128 PR-A fenced only the localpath; adding a second declarative reader to the unfenced one is what made this
reachable.
system_service.create_schedulesgained a name-match guard. It runsafter
create_agent_internal, so post-ent#89 it is the second scheduleproducer for the same agent — a manifest declaring a name the template already
materialized would have double-created.
db.create_schedule's falsy return is now checked. It returnsNoneonthree paths and never raises, so an unchecked call reports success on failure.
Compat: T-018, and the fail-open class it exposed
T-018(soft, static) reports theschedules:block's structure only —cron stays A-002's, because two checks disagreeing on one field is worse than
either. It is the one check that fails closed, and the reason is the bug
class it sits on:
run_staticconverts a raise intoskippedand_countscounts only
fail, so a raising soft check dropssoft_count1→0 and — sinceoverallis a bare> 0test — flipsissues → compatibleexactly when itsfinding was the only failure.
_report_from_persistedthen replays that fromchecks_jsonon every stopped-agent read.Two live instances found and fixed in radius:
c_p006(a HARD check) iteratedscheduleswith noisinstance(..., list)guard, unlike its four siblings — so
schedules: 5silently vanished fromhard_count._valid_cron(A-002) was a per-field regex wrong in both directions:it rejected
0 9 * * MONand accepted99 99 * * *. Now delegates to thescheduler's own validator.
run_static's swallow now logs (it was silent for all ~100 checks). Convertingit to
failplatform-wide is a measured follow-up, not this change.Validation
tests/unitsuite, order-independent underpytest-randomly). +391 new across three ent#89 files.test_1069_voip_call_path_param) reproducesidentically on pristine
origin/dev(c4c83f4) — a FastAPI-version driftin the local venv, not a regression from this branch.
pre-existing on
dev(line numbers shifted only). The newtemplate_schedules.pyis ruff-clean.enterprise-docs-guardpattern run locally againstdocs/+ the seam files:clean.
os.getenv, no compose/Dockerfile change.Scope
OSS-core — no entitlement gate, consistent with the ent#123 / ent#128 / ent#46→#118
precedent for enterprise-tracker features built in core code.
Docs: requirements §10.16,
architecture.md, four feature flows(template-processing, scheduling, agent-compatibility-validation, system-manifest)
docs/agent-validation-spec.md(T-018),learnings.md, and theCSO diff audit under
docs/security-reports/.🤖 Generated with Claude Code