Skip to content

feat(templates): the credential declaration standard — credential_setup: + two HARD-gate fixes (ent#128 PR-B) - #1899

Merged
vybe merged 15 commits into
devfrom
feature/ent128b-credential-declaration-standard
Aug 3, 2026
Merged

feat(templates): the credential declaration standard — credential_setup: + two HARD-gate fixes (ent#128 PR-B)#1899
vybe merged 15 commits into
devfrom
feature/ent128b-credential-declaration-standard

Conversation

@AndriiPasternak31

@AndriiPasternak31 AndriiPasternak31 commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

ent#128 PR-B — the credential declaration standard

Closes AC #1–4 of abilityai/trinity-enterprise#128. AC #5 already shipped in #1835 (PR-A).

Cross-repo Fixes does not fire — the issue is referenced, not closed. Close it manually at the release cut, and set status-in-dev by hand (the same-repo automation won't promote it).

Baseline: rebased onto origin/dev @ c4c83f4a (2026-08-02), which contains PR-A (8d59b6c8, #1835) — so this diff isolates PR-B cleanly.

Rebase resolved two doc conflicts by conceding dev: ent#263 renamed feature-flows/templates-page.mdlibrary-page.md and rewrote it (477 → 168 lines), dropping the required_credentials blocks my annotations targeted — the three-way shape table survives in template-processing.md:183-185, so nothing was lost. Index + learnings.md entries merged in date order.

2026-08-02 rebase. Re-based onto c4c83f4a after dev moved 5 commits ahead. Two conflicts, both resolved by keeping both sides: requirements/core-agent.md §4.3 (two independent bullets) and template_service.py (dev's new contained_template_dir block vs this PR's _build_local_template(…, *, is_bundled) signature). The commit SHAs cited further down were rewritten by that rebase and have been updated here. The substantive consequence — dev landing #1900 — is written up in the CodeQL section below.

Size: 25 files, +3402/−63 — but that splits as 944 lines of production code, 1995 of tests, 406 of docs, 57 of template fixtures. The single largest file in the diff is a test file.


✅ Decisions — all resolved by @vybe

Ruling on ent#128. No vetoes; nothing outstanding.

# Call Landed as
Schema sign-off Approved, incl. tri-state required (unknown for legacy bare names) — that's what ent#137 propagates Ships as-is
config_files Keep as shipped — hardened + deprecated in the schema. Don't delete here Removal follow-up → #1930
YAML hardening File in trinity-enterprise — documents a working amplification attack against live installs; nothing public until the platform-wide caps ship trinity-enterprise#314
${VAR:-default} Public type-bug, P1 — breaks the seeded first-run agent #1929
OSS-core gating Confirmed This PR's placement
Split the fastapi cap? "One PR is fine, don't split" Rides here

The path-traversal CodeQL surfaced (pre-existing, not introduced here) is #1900since fixed on dev by #1935, and inherited by this branch through the 2026-08-02 rebase. Details in the CodeQL section below.


Behaviour changes to look for (rather than trip over)

Two HARD gates change verdicts — these were live false-positives

  • K-002 (HARD) — c_t015 compared ${VAR}s against the credentials block's section names, never the variable names. A template whose ${VAR} happened to match a section name passed vacuously; a correctly-declared variable could fail.
  • K-001 (HARD) — _env_example_vars' uppercase-only filter could not see a documented lowercase variable, so it reported it as undocumented.

Neither needed a spec change: docs/agent-validation-spec.md already describes the intended behaviour ("is listed in template.yaml credentials schema"). The code was wrong, not the spec.

⚠️ The verdict diff is NOT monotone fail→pass

I want to be explicit because an earlier version of this write-up claimed "only fail→pass", and that is measured false. Across 54 fixtures × 66 static checks (origin/dev @ 39f29c64 vs this branch) there are 60 transitions in three classes:

Count Transition What it is
27 fail → pass The fix — T-015/K-002 section-name bug, K-001 lowercase reader
27 skipped(check_error) → fail run_static failing closed. All 27 on one fixture (60k-deep-nesting), whose hard_count goes 7 → 15, i.e. UP
6 pass → fail Vacuous passes becoming true positives

The claim I am defending is not "only fail→pass". It is narrower and checkable:

  1. Zero escapes from run_static on either tree.
  2. Zero dark checks on the branch (nothing left skipped/check_error).
  3. Every one of the 15 fixtures whose hard_count dropped traces line-by-line to an intended fail→passnone to a fail→skipped. That is the property that matters: no check went quiet to buy a green number.

Correction to my own brief: the 6 pass→fail split is 2 HARD + 4 SOFT, not "4 HARD, 2 SOFT". Verified against spec.py (K-002: hard, K-001: hard, T-015: soft, K-003: soft) and cross-checked against the per-fixture hard/soft deltas:

  • var-is-section-env_file and var-is-section-mcp_serversK-002 (HARD) + T-015 (SOFT) each, hard 8→9, soft 12→13. These are the section-name bug in the opposite direction: a ${VAR} that collides with a section name used to pass vacuously and now correctly fails. Same root cause as the 27 fail→pass.
  • env_example-generic-lowercase, env_example-lowercase-only-no-commentsK-003 (SOFT), soft →+1. K-001 now sees lowercase vars, so K-003 ("comments explain each variable") finally has them to evaluate and correctly reports the undocumented ones.

Evidence + rerunnable harness in .plan/ent128b-transition-evidence/ (git-excluded — it does not travel with the branch, which is why the numbers are pasted here).

Smaller behaviour changes

  • The "N credentials" badge starts showing non-zero. It was always wrong at 0 — it read a top-level required_credentials: key that no template defines. Release-note it.
  • Templates declaring both mcp_servers: and credentials.mcp_servers now report the former. This matches what info.py already did.
  • Lowercase .env.example vars are now extracted. No production callerextract_agent_credentials is dead code today (V4).
  • No DB change → Rule Fix git pushing bug #9 (dual-track migration) does not apply.
  • docker/base-image/** changed (info.py), so this needs a base-image rebuild to reach agents.

Two commits landed after the plan was written

11251b39 — the tolerant credential reader must not blow up or raise

Two holes in the "never raises, never amplifies" property:

(a) credential_shape_errors was uncapped while its sibling normalize_credential_requirements got the cap — and both feed the same two surfaces (the catalog's credential_errors, and the "; ".join(errors) that becomes the agent-creation 400 body).

Measured on a 6,738-byte template with one 200-element anchor aliased across 200 servers (re-verified on this rebased branch, not copied forward):

Tree Errors Joined bytes Amplification
origin/dev @ 39f29c64 0 0
this branch, walk-bound removed 40,000 3,635,998 (3.64 MB) 539.6×
this branch, as shipped 101 8,973 1.3×

origin/dev returns 0, so the amplification is this branch's own — introduced and closed within the PR. The cap bounds the walk, not just the output; a cap applied after the loops would still have built and joined every string.

(b) source_trust not in _SOURCE_TRUST_LEVELS is a frozenset membership test, so an unhashable value raised TypeError on the guard line itself. Verified as shipped: list, dict, set, str, None all pass through and fall back to github with a warning.

Both regression tests were confirmed to fail with the fix reverted.

93daa1e5 — cap fastapi to prod's 0.115.x line

Out of scope for a credential PR, flagged rather than let ride in silently. @vybe ruled: "One PR is fine, don't split" — so it stays.

The case for keeping it: prod pins fastapi==0.115.6 exactly, while the test env floated to 0.140.13 — ~25 minors ahead, i.e. the unit suite was validating against a different FastAPI than the one that runs. CI is green only on a warm pip cache keyed on tests/requirements-test.txt, so the next edit to that file for any reason busts the key, re-resolves, and breaks CI for everyone. Capping is the safe way to bust that cache — the change that invalidates the key is the one that makes re-resolution correct.

Note <0.141 does not work (0.140.13 < 0.141 is true). The symbol died in 0.140.7 — a PATCH release — so no minor-level bound is trustworthy.


CodeQL — resolved, and what it turned up

The first push tripped 1 new HIGH py/path-injection (alert 260). Disposition, since it is worth understanding rather than skimming:

The flagged line was mine, but it performed no I/O — it was a pure == comparison used to pick a trust label (source_trust selects logger.warning vs logger.info and nothing else). It still called .resolve() on a path built from a user-supplied template id, which is a genuine tainted-path sink, added to decide a log level.

Fixed in 5e02fe53 by deleting the sink, not by suppressing the alert. is_bundled is now a required kwarg from whoever knows the provenance: the catalog scan iterates the curated root (bundled by construction), and the by-id lookup decides from the id string. Measured against the old predicate across 9 ids: 7 identical, 2 divergent ('../agent-templates/sage', 'a\b'), both True → False — strictly more conservative, never over-granting bundled.

What it turned up is the part worth your attention. Tracing the taint to its source found a pre-existing traversal in get_local_template (byte-identical on dev at the time, not introduced by ent#128): _local_templates_dir() / name with no containment check, reachable by any authenticated user via GET /api/templates/{id:path}. Verified that local:.. escaped the templates root.

That was not fixed here — filed as #1900 rather than folded into a credential-declaration PR or dismissed. Nothing was dismissed.

Update (2026-08-02): #1900 is now fixed on dev, by #1935, which this branch is rebased onto. get_local_template routes name through contained_template_dir() — a name allowlist plus resolve() + is_relative_tobefore this PR's label check runs. Two consequences worth knowing at review time:

  • is_plain_segment is now redundant — provably True wherever it is reached, since the barrier above rejects every non-plain name first. It is deliberately kept as defence in depth: it decides a trust label, and contained_template_dir is a shared primitive the remote-template-registry work (trinity-enterprise#14) is expected to edit. A label that silently became bundled if that barrier were ever widened is the exact failure the is_bundled kwarg exists to prevent. It re-adds no tainted-path sink — it reads the id string, never the filesystem.
  • The in-code comment and the commit message were both rewritten. The merged versions still claimed "deliberately NOT a traversal guard … name still reaches the filesystem exactly as it did before ent#128" — true when written, false after the rebase. Left alone, that is the kind of stale comment a later reader takes as a statement about current behaviour.

Verification

Run on the 2026-08-02 rebased branch (5e02fe53), at the capped FastAPI:

Not run: /verify-local. docker/base-image/** is touched, so a base-image rebuild + live agent exercise is the right gate before release.


Honest framing

  • AC Fix: Add missing Docker labels to system agent container #1's literal wording is superseded. Enrichment lands in a sibling credential_setup: key rather than by extending credentials: — because an old Trinity iterating an enriched env_file raises TypeError: unhashable type: 'dict'. Same outcome, better compatibility. (Posted as an issue comment so the AC isn't read as unmet at close.)
  • credentials: is frozen, names-only, forever. credential_setup: decorates it and can never declare a variable credentials: does not — base-set-plus-overlay, joined by name, so the pair cannot drift.
  • The new tests fail on origin/dev and pass here — the proof they encode real defects rather than being written to fit the implementation. Measured, not asserted: copying test_ent128b1_compat_gates.py onto a pristine origin/dev worktree gives 21 failed / 18 passed; on this branch all 39 pass. The failures are exactly the named defects (test_k002_no_longer_passes_a_reference_to_a_section_name[env_file|mcp_servers|config_files], test_a_raising_check_is_a_failure_not_a_skip, test_check_error_cannot_erase_a_hard_finding, …).
  • AC fix: add missing logging_config.py to backend Dockerfile #4 is a ratchet, not coverage. The bundle has exactly one non-empty declaration (test-codex); the other three bundled templates declare credentials: {}. So the parity test guards ent#137's curated fleet going forward — it does not prove breadth today. Not overselling it.
  • Cross-reference enforcement is asymmetric, and that's fine. Drift is impossible for bundled templates (the parity test) and visible for external ones (credential_errors + the compatibility check). Trinity does not gate a third-party repo's CI.
  • normalize_credential_requirements never raises and never mutates_build_template runs unfenced inside get_all_templates(), so a raise there empties the whole catalog. That is the property (a) above exists to keep literally true rather than true-by-call-site-audit.

🤖 Generated with Claude Code

Comment thread src/backend/services/template_service.py Fixed
@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

Filed the pre-existing traversal this PR's CodeQL alert surfaced (but did not introduce): #1900.

CodeQL alert 260 is resolved in 5c0f712f by removing the sink, not suppressing it — the trust label now comes from the caller instead of a .resolve() on a user-supplied template id. Nothing was dismissed. Detail in the PR body.

@github-actions

Copy link
Copy Markdown

⚠️ Nightly unit-suite check skipped — merge conflict against dev.

Resolve by running git merge dev locally and pushing the result. The next nightly run will re-test once the conflict is gone.

@AndriiPasternak31

Copy link
Copy Markdown
Contributor Author

Un-drafted per @vybe's call on ent#128. Rebased onto dev; all 20 checks green.

/verify-local: full PASS (8/8 stages, agent stage included — docker/base-image/agent_server/routers/info.py is touched, so a backend-only run would have been blind to it):

stage
unit 6199 passed / 0 failed
build + import main in the built backend image pass
agent-build + import agent_server in the built base image pass
boot + health pass
agent-exercise real agent created → started → /health healthy in 3s, clone_status: ok, correct network
integration 70 passed (2 known-false-fails deselected from the registry)

Two timings looked too good, so I checked rather than trusting the banner:

On the CI blip: pytest (head, seed 67890) first came back red. It was cancelled by timeout-minutes: 25, not a test failure — conclusion: cancelled, zero failed steps, log progressing and passing to 79% with all dots and bursty runner timestamps. regression diff went red only because the cancelled job skipped its JUnit upload. Re-ran it: 8m54s, pass — same code, same seed, 3× faster, so runner variance. This is the failure mode the workflow's own comment documents at the previous 15-min cap.

Worth flagging separately: the suite is now 6199 tests, a normal shard is ~10m, and the cap is 25m — a 2.5× slow runner lands exactly on the ceiling. Not this PR's doing, but that margin is thinning.

Release-note requirement: agents only pick up the info.py fix after a base-image rebuild.

AndriiPasternak31 and others added 14 commits August 2, 2026 16:10
…hared_folders

`_resolve_local_template` read `creds.get("mcp_servers", {}).keys()` straight
through the block. A null / list / string `credentials:` raises AttributeError
there, and that read sits FIRST in a run of `config` mutations wrapped in one
broad `except Exception` — so the failure skipped every mutation after it. A
single malformed key therefore silently cost the agent its `runtime:` (wrong
harness) and its `shared_folders:` config too, with only a WARNING to show for
it.

Reads through PR-A's tolerant `credential_mcp_server_names()` instead, so the
credential parse degrades on its own and the unrelated settings survive.

The five malformed shapes are pinned as parametrized regressions; all five fail
on the pre-fix code.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…info

Same uncaught reach-through PR-A fixed on the backend, still live on the agent
image: `.get("credentials", {}).get("mcp_servers", {}).keys()` raises
AttributeError on a null / list / string block at EITHER level, and the
endpoint's own `try/except` wraps only the YAML load — so the crash escaped as a
500 on the Info tab and the brain-orb route guard. `template.yaml` here is read
from the agent's own workspace, which the agent itself can rewrite, so this is
reachable without an operator touching anything.

The agent server ships in its own image and structurally cannot import
`src/backend`, so the reader is DUPLICATED, not imported. The two in-repo
precedents for that (`credential_paths.py`, `model_context.py`) are vendored
byte-identically WITH a parity test; a 6-line reader does not earn a whole
vendored module, but it does earn the same guard — before this commit NO parity
test covered `agent_server/routers/info.py`, so the copies could diverge freely.
Added in the `test_1713_scheduler_utils_parity.py` shape: one shared 17-row table
of malformed shapes driven through BOTH implementations, asserting agreement on
OUTPUT (the copies are textually divergent by design, so a source diff cannot
verify them).

Also routes the endpoint through the existing `get_template_path()` helper —
`/api/metrics` already does — instead of a second copy of the path literal, so
the regression is testable without patching `Path`.

This is the change that makes `/verify-local` mandatory WITHOUT `--skip-agent`.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… it audits

K-001 (HARD) compared `.mcp.json.template`'s `${VAR}` references against an
UPPERCASE-ONLY view of `.env.example`. Trinity's substitution engines impose no
charset at all — the agent-side writer is a `str.replace` and the `.env` writer
slices `env_val[2:-1]` — so `${my_var}` IS substituted at runtime, and a template
that correctly documents `my_var=` was HARD-failed for a gap that does not exist.

`services/credential_charset.py` is the one place that decision now lives, named
for its ROLE (`CREDENTIAL_DETECTOR_CHARSET` — "the widest charset a detector must
accept so it is never narrower than the engine it audits"), not for a reach it
does not have. Four detectors adopt it; the docstring carries an explicit
NON-MEMBERS list with a reason per entry, because the previous framing ("the
charset every Trinity surface agrees on") is false and reads as an instruction to
the next engineer who greps `[A-Z][A-Z0-9_]*`:

  * `mcp_validator._ENV_VAR_REF_RE` is a FAIL-CLOSED gate (`.mcp.json` inject →
    400, `.credentials.enc` import, deploy-local), deliberately paired with the
    WIDEST finder (`[^}]*`). Widening it admits input that is currently rejected.
  * `skill_packaging.ENV_KEY_RE` is an adjacent domain with its own length cap.
  * `static_checks._ASSIGN_RE` carries the quantifier shape behind an
    already-FIXED py/polynomial-redos alert, on an agent-supplied-text path.
  * `c_d006` is a different vocabulary that merely looks similar.

The constant lives in a pure-stdlib leaf module, NOT in `services/compatibility/`:
that package's `__init__` imports `database`, and `static_checks` imports
`template_service`, so a `template_service` → compatibility edge is a hard cycle
(reproduced: "cannot import name '_is_platform_injected' from partially
initialized module").

Behaviour changes, both named:
  * K-001 (HARD) `fail → pass` for a documented lowercase variable — the fix.
  * K-003 (SOFT) `pass → fail` for a lowercase-only, comment-free `.env.example`.
    `_env_example_vars` is K-003's precondition for DEMANDING comments, so growing
    it makes the verdict worse. The verdict is correct — that file genuinely has
    no comments — but it is a `pass → fail` and is release-noted, not smuggled.
  * S-010 (SOFT) does NOT flip: its `generic` blocklist is uppercase-exact, so no
    newly-visible lowercase name can join it. Asserted, not assumed — it is safe
    by coincidence of casing.

The two `template_service` extractors are included because both feed the LIVE
`collect_mcp_credential_warnings` → `deploy.py` path; leaving them out would
half-fix the very inconsistency this closes while a four-way agreement test
passed. Direction there is FEWER spurious warnings. `test_deploy_local_validation.py`
(the 8-assertion suite on that path) stays green.

Also hardens two latent crashes in the same file: a null / non-mapping
`template.yaml` document reaching `extract_credentials_from_template_yaml`, and
`extract_agent_credentials` reaching through `credentials:` at three levels. New
`credential_mcp_env_vars()` reader returns non-empty strings only, so an
`env_vars` element smuggled in as a mapping can never reach a consumer — that
element is exactly what turns a set comprehension into
`TypeError: unhashable type: 'dict'`.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… could go dark

Two defects in the same HARD gate, one of them a way for the gate to stop
protecting entirely.

**1. It read the structure, not the declaration.** `listed` was
`set(creds.keys())` — `{"mcp_servers", "env_file"}` — so the documented structured
form `credentials.mcp_servers.stripe.env_vars: [STRIPE_API_KEY]` satisfied nothing
and HARD-failed a correctly declared template, while `${env_file}` and
`${mcp_servers}` PASSED. The admitted set was "whichever section names this
template happens to use", so the blind spot was template-dependent — the worst
kind, because it cannot be found by reading the check.

`declared_credential_names()` (the union of `mcp_servers.*.env_vars` and
`env_file`, over PR-A's tolerant readers) is now unioned in, and the three known
STRUCTURE keys are subtracted. A flat `credentials: {STRIPE_API_KEY: '...'}`
mapping is still admitted — that legacy shape is legitimate and keeps passing.

The section subtraction is a deliberate `pass → fail` for a genuinely broken
template. Shipped named, tested and release-noted, NOT smuggled under a
monotonicity claim: the blanket "strictly monotone, fail→pass only" claim is false
and a reviewer would find the counter-examples.

**2. It could go dark.** `run_static` caught `Exception` → `skipped`, and `_counts`
counted only `status == "fail"`, so a raise inside a HARD check DROPPED
`hard_count` and could flip `overall_status` from `issues` to `compatible` on an
agent with a genuinely undeclared credential. `c_k002` delegates to `c_t015`, so
ONE raise took both HARD gates dark together, and the result is indistinguishable
from a clean pass in the counts. The trigger is four lines of untrusted YAML:

    credentials:
      mcp_servers:
        s:
          env_vars:
            - {STRIPE_SECRET_KEY: "please"}

`template.yaml` here is read from a live agent workspace, whose git repo the agent
itself owns — a self-attestation bypass on the surface whose job is to police it.
The same `TypeError: unhashable type: 'dict'` is the failure mode that argued
against enriching `credentials.env_file` in the first place, so reintroducing it
at the new call site would have been the plan diagnosing a bug and then shipping
it.

Three layers, deliberately:
  * `c_t015` wraps ONLY the new term and degrades to the narrower set — which
    makes `missing` LARGER, i.e. errs toward failing — never to `skipped`.
  * `run_static` returns FAIL for a check that raises. A check that could not
    evaluate is not a check that passed; one bad check still never breaks the
    report.
  * `_counts` also counts `skipped` + `skip_reason == "check_error"` as a finding,
    at the sink (#1525), so the property survives a future path reintroducing the
    skip. A benign precondition skip (`no_template`, `ai_not_run`) still counts as
    nothing — that distinction is why the skip path exists.

`declared_credential_names` guarantees `str` elements structurally, and the call
site filters `isinstance(name, str)` anyway: the gate must not depend on the
reader's contract holding.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ounts

Three catalog defects PR-A deferred, all in the two builders.

**Defect D — precedence was backwards.** `_build_local_template` read
`credential_mcp_server_names(credentials_block) or data.get("mcp_servers", [])`, so
a `credentials:` block silently OUTRANKED the template's own `mcp_servers:`
declaration. `agent_server/routers/info.py` has always read them in the other
order, so the catalog and the agent's own Info tab disagreed for any template
declaring both. Operands flipped; the `credentials:` path stays as the fallback.

**W14 — the GitHub builder had no fallback at all**, so a GitHub template declaring
only `credentials.mcp_servers` showed an empty list in the catalog while its Info
tab listed them. That was the third of three surfaces; all three now agree.

**Defect C / W6 — the badge.** Both builders read a flat top-level
`required_credentials:` key that ZERO templates declare — 25 bundled and all 7
configured GitHub repos — so `Templates.vue` rendered 0 for everything. Now derived
from the declared base set, with `platform_injected` vars EXCLUDED.

That exclusion is the badge's semantic, and it is not cosmetic: measured on the
real shipped catalog, a naive derivation is correct on 1 of 7 repos and wrong in
both directions — the ent#124 first-run agent would read 5 where the operator
supplies 2, while three shipped repos stay at 0. The chip is read as "how much work
is this to set up", so counting `GEMINI_API_KEY` / `GITHUB_PAT` / `TRINITY_*`
inflates it with rows nobody can fill. A consumer that wants every declared
variable wants `declared_credential_names`, not this.

Derived unconditionally rather than "explicit key wins, else derive": that override
branch is unreachable (no template declares the key), so keeping it would be one
dead code path guarding a live one.

No frontend change: `Templates.vue:103,107,171,175` read only `.length`, so the
shape it already expects is preserved and a variable name never reaches the DOM.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…metadata

Closes ent#128 AC #1-2. A template can now describe each credential an operator
must supply — title, description, required, secret, format, setup_url, default —
and `template_service` surfaces the normalized result as `credential_requirements`
on every catalog entry.

**Enrichment lives in a NEW sibling top-level key; `credentials:` is FROZEN as
names-only, forever.** An already-deployed older Trinity reads `env_file` through
`credential_env_file_names` and then does `agent_credentials.get(var_name, "")` —
hand it a list of mappings and that is `TypeError: unhashable type: 'dict'` at the
moment it writes the agent's `.env`. A sibling key is structurally invisible to
that binary, so there is no floor version and enrichment distributes immediately.

**Base-set-plus-overlay, so the two keys cannot drift.** One record per variable
`credentials:` declares, decorated by `credential_setup:` entries joined BY NAME.
An entry naming nothing is a named three-line error (problem, cause, FIX) and is
dropped; valid siblings survive. `credential_setup:` can only ever decorate — the
sibling-key shape's usual failure mode is closed by construction, not by
discipline. Stated honestly: for an EXTERNAL template that error is neither
impossible nor visible in the UI — `credential_errors` has zero frontend and zero
MCP consumers, so the only human channel is the backend log. It is LOGGED.

`required` is a tri-state. Enriched-and-omitted means `True` (an author who
described a variable meant it); a legacy bare `- FOO` is `"unknown"`, never `True`
— it carries no authorial intent, and reading it as required makes a guided
checklist cry wolf. `"unknown"` doubles as the enriched/un-enriched discriminator,
which is why no `enriched: false` flag is needed. `secret` defaults `True`
(fail-safe). Path-free by construction, so trinity#570's `template.yaml` →
`trinity.yaml` rename cannot reach it.

**The normalizer never raises, and that is load-bearing.** `_build_template` runs
in bare list comprehensions in `get_all_templates()`, OUTSIDE PR-A's per-template
fence (which covers `_build_local_template` only) — a raise there is HTTP 500 with
an EMPTY CATALOG, i.e. PR-A's exact bug reopened by the change that surfaces the
new metadata. And no bomb is needed: `title: 123` or a bare `title:` was enough.
So the builders ALSO wrap the call and degrade to `[]` plus a named error, rather
than fencing the comprehension — that keeps the named error the resilience
contract promises. The property does not rest on one function's discipline.
(Which earned its keep immediately: the wrapper caught a real NameError during
development instead of emptying the catalog.)

Trust boundary — `title`/`description`/`setup_url`/`name`/`source` are
author-controlled strings from arbitrary GitHub repos flowing into an
operator-facing "paste your API key" checklist:

  * **Type-guard before touching.** Never `str()` a container from untrusted YAML:
    `str()` EXPANDS a shared alias during the walk (443 B → 52 MB in 1.5 s, x10 per
    level), and both the sanitizer and the record cap act after that cost is paid.
  * **Cap the INPUT**, entries AND errors AND the base set. Capping records while
    leaving `errors` uncapped built a 35 MB response out of the cap meant to
    prevent it; and `default` had no type row, so the 100-record cap acted as a
    x100 multiplier on it.
  * **`source` is sanitized** — it carries the raw MCP server name, the exact
    string `_sanitize_for_warning`'s own docstring names as the threat, and it was
    not on the list.
  * **Per-field length caps.** Reusing the 80-char terminal-warning default
    truncated a realistic 159-char description and made a real 90-char vendor
    console URL unusable.
  * **`setup_url` above scheme-only**: https (case-insensitive — `HTTPS://` is a
    legitimate author), a parseable host, NO userinfo (`https://google.com@evil.tld`
    renders as one host and resolves to another — the display/resolve split IS the
    attack), ≤2048, printable. Validate THEN sanitize, and never through a
    truncator. Residual documented, not claimed closed: `isprintable()` rejects
    RTL/ANSI but an IDN homograph survives, so a consumer must render the parsed
    hostname beside the link.
  * **Never mutates its input** — `_metadata_cache` holds the parsed dict for 600 s
    and YAML aliases genuinely share nodes, so one in-place normalize would rewrite
    both aliased fields and persist for ten minutes. Asserted against a deep-copy
    snapshot, including a real `&anchor`/`*alias` document.

`credential_shape_errors` also gains the per-server and per-ELEMENT rows for
`mcp_servers`, mirroring what `env_file` already had. The element row is the one
that matters — an `env_vars` entry smuggled in as a mapping was the single most
dangerous shape in the block and was unnamed. Note this makes the write path
(`generate_credential_files` → 400) reject a template that previously created an
agent with a garbage declaration: correct per PR-A's fail-loud write contract, and
release-noted.

`generate_credential_files` is deliberately UNTOUCHED — it still reads `env_file`
names-only, which is what makes the forward-compatibility argument true rather
than asserted.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…n contract

Closes ent#128 AC #3's machine-readable half. Follows the established
`docs/schemas/` convention (`agent-pipeline.schema.json`): Draft 2020-12,
date-stamped `$id` so a future revision keeps answering for templates written
against this one, and self-described as the authoritative documentation contract
while the backend reader stays deliberately tolerant.

**Rooted at `template.yaml`, not at `credentials:`.** The two keys are ONE contract
joined by a mandatory cross-reference, and validating either alone cannot check it.

**`additionalProperties: true` at the root and on `credentials`** — template.yaml
carries many keys this schema deliberately says nothing about, and a template
predating the schema must stay VALID. Accepted asymmetry, and it is asserted as a
test rather than left as a surprise: a made-up top-level key IS valid here.

**`config_files` is enumerated and `deprecated: true`, not omitted.** The earlier
posture was "don't delete, don't advertise", which made the authoritative contract
answer VALID to `path: "/etc/cron.d/pwn"`. Undocumented is not a control against an
author who knows the key — only against the reviewer who doesn't. So it is
documented as deprecated, with a containment `pattern` that rejects absolute and
`..` paths and a description saying plainly that it writes files into the agent's
credential directory. Still reversible, still invalidates nobody. (Whether to
DELETE the key is a public behaviour change and stays @vybe's call.)

Carries the A2 consumer requirements in `$comment`, because the schema is the
artifact a downstream implementer reads:
  * a record with `required: "unknown"` carries no authorial intent and MUST NOT be
    presented as a required field — without this a naive UI renders a seeded agent
    as five mandatory rows, three of them platform variables nobody can fill;
  * `platform_injected: true` MUST NOT be asked of an operator;
  * `secret: true` (the default) MUST be masked;
  * `setup_url` MUST be rendered with its parsed hostname shown, because the IDN
    homograph residual is real and documented rather than claimed closed;
  * there is intentionally NO reverse cross-reference requirement — a declared
    variable with no `credential_setup:` entry is normal.

Also states the author cost honestly in the authoring note: declaring in
`credentials:` is a separate edit from referencing `${VAR}` in
`.mcp.json.template`, K-002 checks the two agree, and that is deliberate because
`.mcp.json.template` must not become a second declaration authority. Plus the two
brace forms Trinity's readers cannot see (`${my-key}`, `${VAR:-default}`).

Tests pin the schema against the implementation — field caps, the format
vocabulary, the allowed-key set, the record cap — so the reviewed text and the
enforced text cannot drift. The 13 document cases run under `importorskip`
(`jsonschema` is not a declared Trinity dependency); the security-relevant pattern
assertions are unconditional.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nce examples

Closes ent#128 AC #3-4.

**Reference examples (AC #4).** The substrate the original plan targeted is gone —
`3317247e` deleted `config/agent-templates/cornelius/` in favour of seeding from
the public upstream repo — so AC #4 lands on what the bundle actually has:

  * `scout` / `sage` / `scribe` (the ent#124 seeded trio) declare an explicit
    `credentials: {}` with the zero-credential contract written out. Absent and
    empty mean the same thing to Trinity, but *absent* is ambiguous to a HUMAN —
    it could equally mean the author forgot. `{}` says "considered, and there are
    none", so the catalog's 0-credential badge is trustworthy.
  * `test-codex` carries the enriched reference: its one real variable gets a
    title, description, `required`, `secret`, `format` and `setup_url`.

Deliberately NO `GEMINI_API_KEY` in any example: it is platform-injected, so an
example asking for it would violate the very rule the guide documents — and it
makes a K-002 fixture pass VACUOUSLY, which is how a test proves nothing while
looking green. A test asserts no bundled example asks for a platform-injected var.

Framed honestly rather than oversold: with one enriched declaration and one
names-only one in the bundle, the parity test ("every bundled template normalizes
with zero errors") is thin today. Its value is as a RATCHET for ent#137's curated
fleet.

**The guide (AC #3).** New `## Declaring Credentials` section, TOC renumbered
5→21. Covers the field table, the decorate-don't-declare rule with the actual
error text, why `credentials:` stays names-only, the zero-credential contract,
degrade-don't-demand, the platform-injected list, fork-to-own composition
(ent#109), and the two brace forms Trinity's readers cannot see (`${my-key}`
silently dropped, `${VAR:-default}` mis-substituted to an empty string).

It also states the AUTHOR COST plainly instead of claiming the design is free:
declaring a variable is a separate edit from referencing it in
`.mcp.json.template`, and three of Trinity's own six default GitHub templates
declare zero credentials while referencing 2-6 and documenting 7-12. Those are
K-002-red today and stay red until someone does the edit. Kept that way on purpose
— if `.mcp.json.template` counted as a declaration it would become a second
authority on what an agent needs, which is the drift this design exists to
prevent. The practical order is stated: seed `credentials:` first, enrich second.

**Memory docs.** `requirements/credentials.md` §3.5's ✅ was false in both halves
and is corrected in place with the correction recorded: the extractor it credited
has no production caller, and nothing showed configured-vs-missing status because
the badge read a key no template defines. `template-processing.md` and
`templates-page.md` get the "two shapes, two owners" table that reconciles the
objects-vs-strings contradiction (catalog `required_credentials` = names,
`credential_requirements` = objects, extractor `required_credentials` = a
different function with the same key name), plus the corrected regex. Compatibility
checklist gains six credential rows and a starts-with-nothing-configured row.

**No DB change → Rule #9 (dual-track migration) does not apply.**

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A transition diff over a corpus with zero coverage of the diff is not evidence, so
the changed statements were measured against PR-B's real base (`c07afab7` =
origin/dev + PR-A) rather than assumed. The gate found the new paths that no test
reached and this closes them:

  * §4's new `mcp_servers` shape-error rows — per-server AND per-element, six
    parametrized cases plus the sanitized-server-name case. The element row is the
    dangerous one and it had no test.
  * The write-path consequence, asserted explicitly: `generate_credential_files`
    now raises on `env_vars: [{K: v}]`, where before it created the agent silently.
  * `_setup_url_error`'s `urlsplit` ValueError branch (malformed IPv6 literal).
  * The dedup early-return in the base-record builder — a variable declared under
    two servers AND `env_file` yields one record with a stable `source`.
  * A non-string mapping key in a descriptor (`{1: "x"}`), which must not reach the
    "did you mean" helper.
  * The caller-less `extract_agent_credentials` across eight malformed shapes. It
    has no production caller, which makes hardening cheap rather than unnecessary —
    the next caller would have inherited the crashes. Now exercised instead of
    merely present.

Result: 227 changed statements, 225 executed. The two remaining are a defensive
`except OSError` around a `Path.resolve()`, and the three gate files
(`static_checks.py`, `compatibility/__init__.py`, `credential_charset.py`) are at
100% of changed statements.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`/sync-feature-flows`. `template-processing.md` and `templates-page.md` were already
updated with the declaration standard; this adds the flow the code change actually
lands hardest on and which nothing had touched: `agent-compatibility-validation.md`.

Both credential HARD gates changed, and the flow doc described neither the defect
nor the new semantics:

  * "a detector must never read narrower than the mechanism it audits" — the shared
    root cause of K-001 and K-002/T-015, with the NON-MEMBERS list spelled out so
    the next reader does not "align all the regexes" and widen
    `mcp_validator._ENV_VAR_REF_RE`, which is a fail-closed GATE and not a detector;
  * "a HARD gate must not be able to go dark" — the `run_static` →`skipped` +
    `_counts`-counts-only-`fail` interaction that let 4 lines of untrusted YAML drop
    `hard_count` 1→0, and the three fail-closed layers that replace it;
  * the complete verdict-transition set, because the blanket "strictly monotone"
    claim is false and a reader will find K-003's `pass→fail`. The claim that
    survives is "no agent gains a HARD failure".

Testing section records why the bundled templates cannot prove any of this — 0
`.mcp.json.template` and 0 `.env.example` files, so every changed check
short-circuits before reaching changed code and a green diff there is
green-because-vacuous — and points at the 49-fixture synthetic corpus instead.

Plus the Recent Updates row in `feature-flows.md` (the step this skill's own docs
warn gets skipped).

Observation, deliberately NOT fixed here: the Recent Updates table carries 57 rows
against its own documented "newest ~20" cap (#1360), so the index is 434 lines vs
the 400-line guideline. Trimming it means deleting 37 other engineers' entries,
which is not this PR's call.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`sk-live-xxx` is stripe-shaped and gitleaks' default ruleset covers `sk-`. The value
is arbitrary in this test — it only has to round-trip byte-identically through the
`.env` writer — so there is no reason to hand CI a secret-shaped string to reason
about.

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Real regression I introduced in `886aab5b` and initially mis-attributed as
pre-existing. Recording both the fix and how the mis-attribution happened, because
the second part is the reusable lesson.

**The bug.** `test_1484_create_agent_characterization.py` and
`test_1759_local_template_not_found.py` MagicMock the whole
`services.template_service` module and stub each function crud actually calls with a
faithful return value (`generate_credential_files` → `{}`, `get_github_template` →
`None`). `_resolve_local_template` now calls a THIRD one —
`credential_mcp_server_names` — and it was unstubbed, so it returned a truthy Mock
that passed `if mcp_servers:`, landed in `config.mcp_servers`, and blew up later
inside a `yaml.dump` as `ValueError: dictionary update sequence element #0 has
length 1; 2 is required`. 18 tests, entirely a harness gap: in production the real
function returns a list.

Stubbed with a faithful 3-line mirror rather than a fixed `[]`, so a fixture that
DOES declare `credentials:` cannot be silently masked by the stub.

**One test needed a real update, not a stub.**
`test_malformed_field_still_creates_and_names_the_template` used
`credentials: "a string"` as its trigger for the broad-except degrade path. That is
exactly what `886aab5b` fixes — `credentials:` is no longer a trigger BY DESIGN,
because it raised FIRST in that run of mutations and so cost the agent its
`runtime:` and `shared_folders:` config as collateral. Swapped the trigger to
`shared_folders: not-a-mapping`, which still raises, so the degrade path and the
two identifiers in its warning stay under test. The docstring records why and points
at the new coverage.

**How I mis-attributed it.** I compared with `git stash push -- src/backend`, which
reverts only the WORKING TREE — commits 1 and 2 were already committed, so my
"baseline" still contained the cause and the failures looked identical on both
sides. The `-k`-filtered selection also happened to include only 1 of the 13
`test_1484` failures, which made the set look small and stable. Only a worktree at
`c07afab7` (PR-A's tip, PR-B absent) showed the truth: 2 failures there vs 20 on the
branch. **A baseline has to be a worktree at the base commit, not a stash.**

Now identical to `origin/dev` and to `c07afab7`: 2 failures, both genuinely
pre-existing (`test_agent_analytics::test_day_stacks_present_in_by_type`,
`test_1069_voip_call_path_param` — the documented `get_flat_dependant` venv drift).

Refs Abilityai/trinity-enterprise#128

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two holes in the "never raises, never amplifies" property PR-B rests on, both
found by asking which OTHER producers reach the surface the new cap protects.

1. `credential_shape_errors` was uncapped. The cap shipped on the NEW function
   (`normalize_credential_requirements`), but the same PR added a per-ELEMENT
   loop to this PRE-EXISTING one, and it feeds the same two surfaces: the
   catalog's `credential_errors`, and the `"; ".join(errors)` that becomes
   `CredentialDeclarationError`'s agent-creation 400 body. A cap is a property
   of the producer, not of the PR that invented the concept.

   YAML anchors make input size a useless proxy for output size, so the bound
   has to stop the WALK, not slice the result. Measured on a 6,738-byte
   `template.yaml` (one 200-element anchor aliased across 200 servers):
   40,000 errors / 3.64 MB joined (540x) before, 101 errors / 8,973 bytes after.
   `origin/dev` returns 0 on the same input, so the amplification is this
   branch's own — reachable since ent#123 by any creator-role user pointing at
   an arbitrary public repo.

2. `source_trust not in _SOURCE_TRUST_LEVELS` is frozenset membership, so an
   UNHASHABLE value raised `TypeError` *on the guard line* — before the
   degrade-to-`github` branch that guard exists to reach. Unreachable from
   parsed YAML today (every call site passes a literal), but this is the one
   function whose docstring makes "NEVER RAISES" load-bearing: a raise here is
   an empty catalog and a dark HARD gate. The property should be literally
   true, not true-by-call-site-audit.

Both regression tests were confirmed to FAIL with their fix reverted and pass
with it restored.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The unit suite was validating against a FastAPI ~25 minor versions ahead of
the one production ships. `docker/backend/Dockerfile` pins `fastapi==0.115.6`
exactly; `tests/requirements-test.txt` carried an unbounded floor that resolved
0.140.13. The comment at :43 already claimed these "match the floors set in
docker/backend/Dockerfile" — that file uses exact pins, so the claim was untrue.

Surfaced as `test_1069_voip_call_path_param` failing with `ImportError: cannot
import name 'get_flat_dependant'`. That test is only the messenger: it is the
one test coupled to a private FastAPI symbol (`src/backend` imports none, and
the other test touching `fastapi.routing` uses the public `APIRoute`).

The obvious ceiling does not work: `0.140.13 < 0.141` is true, so `<0.141`
still admits the breaking version. Bisected against the real wheels — present
in 0.140.6, gone in 0.140.7 — a private API dropped in a PATCH release, so no
minor-level bound is trustworthy. Tracking prod's line is the durable fix.

Why now rather than "separate follow-up": CI is green only on a warm pip cache.
backend-unit-test.yml keys `cache-dependency-path` on this file, and 0.140.13
allows py3.11, so the next edit to this file for ANY reason busts the key,
re-resolves, and breaks CI for everyone. Capping is the safe way to bust that
cache — the change that invalidates the key is the one that makes re-resolution
correct.

Follows this file's own precedent (`bcrypt>=4.2.0,<5`, added when bcrypt 5.0.0
removed the `__about__` shim passlib reads): floor + ceiling + a comment saying
why, rather than an exact pin that would break the file's `>=` convention.

Verified by execution, not argument:
  - full tests/unit at 0.115.14: 5861 passed, 16 skipped, 2 xfailed, 0 failed
    (at 0.140.13 the same command is 1 failed, 5860 passed)
  - the edited file installs clean in a fresh venv and resolves 0.115.14
  - an existing verify venv self-heals: pip downgrades 0.140.13 -> 0.115.14

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndriiPasternak31
AndriiPasternak31 force-pushed the feature/ent128b-credential-declaration-standard branch from e46db13 to 3aa3dc2 Compare August 2, 2026 15:20
…ted path

`_build_local_template` derived `is_bundled` itself:

    is_bundled = template_dir.resolve().parent == _local_templates_dir().resolve()

`template_dir` on the by-id path is `_local_templates_dir() / name` where `name`
comes from a user-supplied `local:<name>` template id, so this called `.resolve()`
on attacker-influenced input. CodeQL flagged it as `py/path-injection` (alert 260,
high) — a new tainted-path sink introduced by ent#128 purely to pick a log level
(`source_trust` selects `logger.warning` vs `logger.info` and nothing else).

`is_bundled` is now a required keyword arg supplied by whoever knows the
provenance:

  - `get_local_templates()` iterates the curated root, so its children are
    bundled by construction -> `is_bundled=True`.
  - `get_local_template()` decides from the id STRING (plain single segment, no
    separator, not a dot-segment) rather than a path operation on it.

Behaviour, measured against the old predicate across 9 ids: 7 identical, 2
divergent — `'../agent-templates/sage'` and `'a\b'` go True -> False. Both moves
are old=True -> new=False, i.e. strictly more conservative: the new check never
grants the `bundled` label where the old one withheld it, only the reverse. An id
that traverses to arrive inside the curated root is not curated, so the new
answer is also the more correct one; the blast radius either way is one log
level.

This is deliberately NOT a traversal guard — and as of the 2026-08-02 rebase it
no longer needs to be. An earlier version of this message said the traversal was
"being routed as its own issue rather than fixed"; that issue, #1900, has since
been fixed on `dev` by #1935, which this branch is now rebased onto.
`get_local_template` therefore routes `name` through `contained_template_dir()`
— a name allowlist plus resolve + `is_relative_to` — BEFORE the label check runs.
(The traversal was real while it lasted: `local:..` escaped the templates dir,
reachable by any authenticated user via `GET /api/templates/{id:path}`.)

That makes the `is_plain_segment` check redundant today — provably True wherever
it is reached, since the barrier above rejects every non-plain name first. It is
kept as defence in depth: it decides a trust LABEL, and `contained_template_dir`
is a shared primitive the remote-template-registry work (trinity-enterprise#14)
is expected to edit. A label that silently became `bundled` if that barrier were
ever widened is the exact failure this keyword argument exists to prevent. It
re-adds no tainted-path sink — it reads the id string, never the filesystem.

Two test call sites updated for the new signature.

Verified on the rebased branch: full tests/unit 6410 passed / 16 skipped /
0 failed; no `template_dir.resolve()` remains in the module.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AndriiPasternak31
AndriiPasternak31 force-pushed the feature/ent128b-credential-declaration-standard branch from 3aa3dc2 to 5e02fe5 Compare August 2, 2026 15:28

@vybe vybe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

/validate-pr: PASS.

  • Base dev ✅ · 25 files ✅ · security scan clean (STRIPE_API_KEY: 'from the vault' and google.com@evil.tld are fixtures/spoof vectors, not credentials) ✅
  • No new top-level backend module (credential_charset.py sits under services/, copied wholesale by the Dockerfile) ✅ · no new os.getenv()
  • Docs: 2 requirements files + architecture + 3 feature flows + a JSON schema artifact ✅ · 6 test files ✅

Three things stood out as genuinely good:

  1. _did_not_pass is a real correctness fix, not cleanup. _counts only counted status == "fail", so a raise inside a HARD check dropped hard_count and flipped overall_status to compatible — a broken agent reporting green. Counting skipped+check_error as a finding at the sink, in addition to run_static now returning FAIL, is the right two-layer shape (#1525): if any future path reintroduces the skip, the count still holds.

  2. The input caps are measured, not guessed. The record cap is applied to the INPUT before the walk because str() on an aliased YAML node expands it (443 B → 52 MB measured) — a cap applied after the loop would bound nothing. The separate error cap exists because 400k malformed entries produced 1 capped record and 400,000 uncapped errors, i.e. a 35 MB response built out of the cap meant to prevent it.

  3. The fastapi pin is a CI-integrity fix worth more than its diff. An unbounded fastapi>=0.115.0 resolved 0.140.13 while docker/backend/Dockerfile pins fastapi==0.115.6 exactly — the unit suite was validating ~25 minor versions ahead of production. It was green only on a warm pip cache keyed on this very file, so the next unrelated edit would have busted the key and broken CI for everyone. The note that <0.141 would NOT have worked (the private symbol vanished in a PATCH, 0.140.6 → 0.140.7) is the part that makes tracking prod's line the durable fix rather than a bigger number. Verified 5861 passed / 0 failed at 0.115.14 vs 1 failed at 0.140.13.

This pairs with #1910 (CI on Python 3.11 while every image ships 3.13) — same class, and both worth landing.

⚠️ Cross-tracker: ent#128 needs status-in-dev by hand (AC #1–4 here; AC #5 shipped in #1835).

@vybe
vybe merged commit 4c96a80 into dev Aug 3, 2026
23 checks passed
vybe pushed a commit that referenced this pull request Aug 3, 2026
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 pushed a commit that referenced this pull request Aug 3, 2026
#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.
vybe pushed a commit that referenced this pull request Aug 3, 2026
#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).
vybe pushed a commit that referenced this pull request Aug 3, 2026
…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>
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.

3 participants