feat(credentials): runtime secret-scrub seam + license-blind vault MCP/UI (ent#279 PR-2) - #2264
Conversation
|
Resolve by running |
|
Resolve by merging |
d00570f to
bbdf0b0
Compare
A generic OSS mechanism so a producer that delivers an arbitrary secret to an agent as a plaintext tool result can keep that value from persisting verbatim into a durable backend sink. The pattern-based `utils/credential_sanitizer` (prefix regexes + `KEY=value`) cannot catch a prefix-less value; this seam is identity-based instead. - `stage_secret(agent_name, value)` fails CLOSED (raises `StagingUnavailable` on any failure — Redis down or hard cap — so a producer that cannot stage refuses delivery). Values < 8 chars are skipped, never staged. - `get_staged_values()` / `scrub_text` / `scrub_obj` fail OPEN and redact every staged value (marker `***REDACTED***`; raw / JSON-escaped / base64 renditions; longest-first; literal replace; falsy passthrough). - Store = one global Redis HASH `secret_scrub:staged` (sha256(value) -> AES-256-GCM envelope) + a `staged_at` ZSET for 24h per-member expiry. Mechanism-only by design — names no consumer. Behaviour-neutral for OSS (no staged values => every scrub is a no-op). Chokepoint wiring + the parity guard land next. 26 seam-direct unit tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Public half only — zero enterprise_* tokens (enterprise-docs-guard). - requirements/credentials.md §3.7 "Credential Vault — Public Surfaces": the OSS-visible half — license-blind MCP proxy tools (degrade shapes), the entitlement-gate shape, the runtime secret-scrub mechanism, Vue gating. The paid module's schema/internals stay in the private repo. - architecture.md: services-catalog entry for `runtime_secret_scrub.py` (generic wording, names no consumer). - feature-flows/runtime-secret-scrub.md: the mechanism end-to-end (store, stage-closed/scrub-open asymmetry, chokepoints, residuals, drift guard) + Recent-Updates and Authentication & Security index rows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add `src/backend/services/runtime_secret_scrub.py` to the guard in all three places — the PR `paths:` filter, the push `paths:` filter, and the `SEAM_FILES` grep set — plus a comment noting it is a mechanism-only seam whose docstring must never name the private producer. A seam file's comments are unguarded prose that can name the paid catalog just as a doc can (#1461), so the guard greps it alongside the docs. No PATTERN change. Verified locally: the guard's grep over docs/ + CLAUDE.md + the updated SEAM_FILES yields zero hits. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…erminal chokepoint (ent#279) Wire the runtime secret-scrub seam into every persistence chokepoint that can carry a runtime-fetched vault secret into a durable sink: apply_result (both branches + raw_response fields), _write_terminal_and_gate, chat_execution_service (_finalize_chat_success incl. the idempotency snapshot, _parse_agent_http_error before its ERROR log, _finalize_budget_exhausted), pull_coordination_service .apply_task_result, proactive_message_service.send_message (before delivery AND persist), channel_history, and routers/voice _save_transcript. Every site scrubs RAW fields before sanitize_*/json.dumps/truncation/log lines (R10), one get_staged_values() call per applier invocation. Add test_ent279_scrub_parity.py — the discovered-not-derived drift guard (test_1804 idiom, D20): anchored on the persistence sinks, so a new terminal applier that bypasses scrubbing fails CI instead of silently leaking. Extend test_ent279_secret_scrub.py with the failure-terminal paths, both idempotency snapshot writes, the transitive sessions check, and the cross-worker stage/scrub test via two clients against a sibling Redis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ense-blind (ent#279)
Add createCredentialVaultTools: list_available_credentials and
fetch_credential({name, execution_id?}) — the agent-facing half of the vault.
Both proxy the entitlement-gated backend routes and are license-blind: they
report whatever the routes say instead of assuming the feature exists.
list_available_credentials degrades to the enabled:false shape
(list_runnable_skills contract) and branches on the 403 body code — a 404 is an
OSS build, agent_key_required is a call-from-an-agent hint (never 'feature
missing', so a human operator on a user key is not misled, D25), an
entitlement-string 403 is 'not licensed'.
fetch_credential returns the outbound-A2A {success:false, error, ...flags} shape
and NEVER throws. It branches on the detail SHAPE, not the status (D31):
dict-with-code (vault_not_granted / agent_key_required / vault_approval_required
/ the three 503 codes) vs the entitlement plain string (not_entitled) — telling
an ungranted NAME apart from an unlicensed INSTANCE, both 403.
client.ts proxies (GET /available, POST /fetch); server.ts registers the group
in the operatorOnly allow-list; tool-visibility.test.ts pins them
operator-scope-only (hidden from connector/anonymous, fails closed for unknown
scopes). credential_vault.test.ts asserts the codes + both 403 detail shapes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a Settings 'Vault' tab (ALL_TABS requires:'credential_vault', so it renders
only on an entitled build) backed by CredentialVaultPanel.vue — self-contained
API calls over the enterprise control plane (SlackAgentBotPanel precedent).
The backend require_vault_admin (admin AND interactive human) is the real
boundary; an entitled non-admin sees the tab (the SSO-precedent quirk) and gets
a named admin-required state on 403 instead of raw error toasts. The panel:
entries table with a real empty state, Add credential (value write-only, named
floor/cap errors surfaced from the backend), Update value, Delete with a
grants-aware confirm, per-entry grants with an escalated-consent confirm ('Agent
X will be able to read the VALUE of <name> at any time while granted'), and a
Rewrap action — tucked in a collapsed key-rotation maintenance disclosure, with
a loud decrypt-failure banner surfaced only when a rewrap reports failures
(honest status, not a dead button). Semantic tokens only (raw_nongray: 0).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ion (ent#279) _parse_agent_http_error's non-JSON fallback set error_msg = e.response.text[:500] and only scrubbed AFTER the slice, violating the plan's R10 "scrub before truncation" rule (learnings 2026-07-24) at one of the four positively-pinned direct sinks. A staged secret straddling char 500 in a non-JSON agent error body would leave an unmatchable leading fragment in the platform ERROR log and the FAILED row. Read the staged set once up front and scrub the raw body before the slice; the structured-detail paths (untruncated) keep the trailing scrub. Adds a regression test asserting a secret placed across the 500-char boundary leaves neither the whole value nor its truncated prefix in error_msg. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LuDx5TdR2s9TXAp5kRdgkx
… (ent#279) The FAILED-path applier scrubbed envelope.error by identity but serialized the salvaged transcript (and its derived #1741 tool_calls summary) with only the pattern sanitizer — a fetched secret with no known prefix persisted verbatim on the FAILED row. Mirror the SUCCESS branch: scrub_obj the envelope's execution_log before the salvage serialization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…rub parity guard (ent#279) _cancel_inflight_if_parked arrived on dev after this branch forked; its single update_execution_status(error=...) write is a static literal — the execution never dispatched, so no agent-authored text exists to leak. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Proves by interception that the enterprise vault's lazy stage_secret import and the OSS persistence chokepoints bind the SAME runtime_secret_scrub module object — the assumption the whole scrub mitigation rests on. Skips on OSS-only clones (submodule absent). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
bbdf0b0 to
aa15aa9
Compare
dolho
left a comment
There was a problem hiding this comment.
/review Report
Scope: CLEAN on substance — the diff is the ent#279 PR-2 surface exactly as described (seam + six chokepoint wirings + license-blind MCP tools + gated panel + docs/CI/tests). One scope note below on formatter churn.
I read the full seam, every chokepoint wiring, the MCP tool/visibility/audit surface, and the panel; test files and docs skimmed.
Critical findings
None. The specific traps I went hunting for all check out:
- Failure asymmetry is real, not just claimed:
stage_secretraisesStagingUnavailableon any store/encrypt error (fail-closed),get_staged_valuesreturns[]+ one throttled ERROR (fail-open) — and the docstring's argument for why open-scrub never widens exposure (no NEW secret delivered while staging is down) holds. - Scrub placement is right at all six chokepoints: before
sanitize_*, beforejson.dumps, before truncation (scrub_text(_staged, e.response.text)[:500]— the straddling-fragment case is explicitly handled), before the CAS write, and before the idempotencyresponse_snapshotthat replays to duplicate-key callers for 24h. The FAILED branch scrubsenvelope.errorat the top, so the #1578 completion event, the activity close, and the ent#265 channel report all inherit it. fetch_credentialplaintext cannot leak via the MCP audit wrapper:withAuditlogs duration/error/target_id and returns the result without capturing it (audit.ts:172-176); the tools never throw (both wrap indegradeList/failFetch), so no error path carries a value either.- Tool visibility is an allowlist that fails closed: operator scopes only, connector/anonymous/
portal_delegate/future-scope all hidden, pinned by the new tests — the exact #848/#2323 deny-check lesson, applied correctly to a tool whose whole job is handing out secrets. REDACTION_PLACEHOLDERexists atutils/credential_sanitizer.py:112; the seam reuses it rather than minting a second marker.- Panel: values are write-only (
type="password", never echoed, "shown once"), nov-html, raw-axios-in-component matches the existing panel precedent, and the loading gates satisfy the #1927 ratchet as claimed.
Informational
[I1] Formatter churn dominates the two hottest dispatch files (Confidence: 9/10)
A large fraction of the task_execution_service.py (+242/−82) and chat_execution_service.py (+537/−211) hunks are pure black-style re-wraps of untouched code (import splitting, comment realignment, 2**attempt, ternary re-flow). RoE #2 says no cosmetic formatting of unrelated code — and practically, these are the files every in-flight execution-path branch touches (the #2314 decomposition included), so each pure-format hunk is a future conflict for zero semantic content. If a re-push happens anyway, consider dropping the format-only hunks; if not, the merger should just know the real diff is ~⅓ the stated size.
[I2] The proactive-message wiring deliberately breaks the seam's own contract — say so in the seam (Confidence: 8/10)
The seam docstring promises "the live tool result stays whatever the producer delivered; only the persisted copy is scrubbed", but proactive_message_service.send_message scrubs before channel delivery (D19, argued in-code and I think correctly — a proactive message is an exfil channel, not a tool result). The two texts currently contradict each other; one sentence in the seam docstring naming the delivery-scrub exception keeps the next reader from "fixing" either side.
[I3] The scrub tax is fleet-global once anything is staged (Confidence: 7/10)
The store is deliberately global, so one vault-active agent makes every terminal fleet-wide pay HGETALL + N AES decrypts + N×3 str.replace passes over multi-MB transcripts, on the event loop. Bounded (dedup by value-hash, 24h member TTL, warn 200 / cap 1000) and str.replace is C-fast, so this is fine at realistic set sizes — but worth one sentence in the feature-flow's operational notes so a future "why did terminals get slower" investigation starts in the right place.
[I4] The <8-char floor is a fail-open edge on the fail-closed side (Confidence: 6/10)
stage_secret silently skips values under _MIN_VALUE_LEN (WARNING only) and the caller proceeds to deliver — a 6-char vault value is delivered and never identity-scrubbed. The floor itself is right (scrubbing short strings shreds transcripts, and it matches the sanitizer's own ≥8 rule). The clean closure is on the private side: have the vault write path refuse to store values under the floor, so the unstaged-delivery state is unreachable. Worth a cross-check against trinity-enterprise#410 — if it already does, ignore.
Summary
- Critical: none found.
- Informational: 4 — I2 is a one-sentence docstring fix worth doing; I1 is for the merger's awareness; I3/I4 are notes.
backend-unit-testis still pending as I post this — note thatdevhas a time-bomb test failing after 10:00 UTC today (test_2467_turn_integrityfleet-list arm, hardcoded timestamp vs thehours=24window; fix in #2488). If this PR's run reds on exactly that test, it is not this PR.
Approving — the seam's design is careful, the chokepoint discipline (scrub-before-sanitize/truncate/CAS/snapshot, both applier branches) is exactly right, and the guards (visibility allowlist, D26 combined-tree, parity suite) pin the properties that matter.
🤖 Generated with Claude Code
obasilakis
left a comment
There was a problem hiding this comment.
/validate-pr: no critical findings — leaving this as a comment rather than a second approval, since @dolho has already reviewed head aa15aa93. One item below should gate the merge, though.
Blocking-ish: the green CI predates the base this will merge into
The tested merge commit is dd910564 + aa15aa93 (run created 12:12Z). dev has since gained 12 commits, including bf6fcf11 — the #2398 sanitizer ReDoS hotfix, +92/−6 to src/backend/utils/credential_sanitizer.py. That is the exact module this seam imports (REDACTION_PLACEHOLDER) and composes with at every chokepoint (identity scrub, then pattern sanitize).
The imported constant is unchanged (credential_sanitizer.py:112), so the coupling looks stable — but the ent#279 suites have never executed against the new sanitizer. Please re-run CI on current dev before merging.
(For the record: the PR is not stale in effect. Opened 2026-08-17 but rebased today; 12 commits behind dev with zero file overlap against dev-only changes; mergeable: MERGEABLE, mergeStateStatus: CLEAN.)
Security battery — all clean
- Steps 4.1–4.9: no key/token hits, only test-fixture emails (
u@x.io), no public IPs, no.envor credential files, no infra change, no new top-levelsrc/backend/*.py(the seam lives underservices/, copied wholesale), no newos.getenv. The sole 4.5 hit is a test literal intest_ent279_secret_scrub.py.gitleakspasses. - Fail-open scrub is deliberate design, not a defect (
runtime_secret_scrub.py:26-34,176-189), and the asymmetry holds:stage_secretfails closed, and enterprise_stage_or_fail_closed(service.py:398) turns any staging failure into a 503 refuse-delivery — so no new secret ships during an outage. The residual is honestly documented (flow doc, lines 97-98): a turn already in flight when Redis drops persists its transcript un-identity-scrubbed, with the pattern pass still running, for a window bounded by the turn timeout. A fail-closed scrub would destroy terminal CAS writes; this is the right trade. - No secret value reaches any log, response or error body — verified end to end.
StagingUnavailablecarriestype(e).__name__only, neverstr(e)(:168-170); short-value WARNING logs the floor and agent only (:130-135); the fail-open ERROR logs the redis exception only (:188); the decrypt WARNING (:200) logs aValueErrorfromcredential_encryption.decrypt, whose messages carry only JSON-parse/version/algorithm/InvalidTagtext — no plaintext, no ciphertext;_parse_agent_http_errorscrubs before the 500-char slice and before its ERROR log (f47f73c); MCPwithAuditnever captures the result; the Vue value fields are write-only. - Invariant #13 verified against the pinned submodule
90f2f2c(equal to dev's gitlink, so no bump is owed): backendrouter.py:51prefix/api/enterprise/credential-vaultplusGET /available(:200) andPOST /fetch(:205) matchclient.ts:2828/2836exactly. The agent-server surface is N/A — agents reach the vault via MCP. - @dolho's open I4 is closed: enterprise
VALUE_MIN_CHARS = 8(service.py:40) is enforced at the write path with a 422, exactly matching the seam's_MIN_VALUE_LEN = 8, so the silent-skip branch is unreachable for vault values by construction.
Warnings
- Formatter churn in the two hottest dispatch files (RoE #2).
chat_execution_service.py: 741 changed lines, 19 of them (2.6%) mentioning scrub/staged.task_execution_service.py: 297 / 21 (7%). Roughly 95% is cosmetic re-wrap, which is real conflict surface for in-flight branches — #2314 and #2489 both touchtask_execution_service.py. devis now atv0.9.5-rc1, so merging post-RC lands this in the next cut, not 0.9.5-rc1 — worth noting against the claim of clearing release-floor bar 6 of ent#441.- The body uses "Refs", not a closing keyword — and it's cross-tracker anyway, so automation can't fire. trinity-enterprise#279 is already
status-in-devbut will need a manual close at the release cut.
Suggestions
- If a re-push happens anyway, drop the format-only hunks from the two dispatch files — it also shrinks the rebase surface for #2314.
- Add one seam-docstring sentence naming the
proactive_message_servicedelivery-scrub exception (@dolho's I2); the docstring's "only the persisted copy is scrubbed" currently contradicts that wiring. - Note the fleet-global scrub cost in the flow doc's operational notes (@dolho's I3), so a future "terminals got slower" hunt starts in the right place.
Findings produced by /validate-pr (Claude Code).
Resolves the conflict in services/task_execution_service.py between this branch and #2314 (PR #2489), which decomposed execute_task into named phases. The two changes are semantically disjoint. Every ent#279 scrub site lives in _write_terminal_and_gate and apply_result, both of which #2314 left byte-identical; every other difference this branch had in that file was black reformatting with no semantic content. Resolution therefore takes dev's file whole -- keeping the decomposed execute_task, the nine extracted helpers and the services/execution_envelope re-export -- and re-applies only the scrub import and the four scrub blocks. No reformatting is carried over. One real cross-PR gap surfaced, caught by test_every_free_text_writer_scrubs: #2314 moved three static admission-refusal writes (capacity-full, dispatch-breaker, ephemeral-exhausted) out of execute_task and into the new _admission_gate, which the ent#279 allowlist did not yet name. Those writes are unchanged and still run before any agent call, so no agent text or staged secret can exist on that path; _admission_gate is allowlisted on the same grounds the execute_task entry already recorded, and that entry is narrowed to the single backend-shutdown write it still holds. Verified: 1864 passed, 9 skipped across all 90 unit-test files touching task_execution_service / execute_task / apply_result. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YTHX5aMsCJf71QhtysewL4
Rebased onto current
|
Brings the branch onto dev at 36e6e1a: #2509 (main reconcile), #2479 (zod 4), #2513 (pull: scheduled work reaches the durable queue), plus #2500/#2480. The only shared file with #2513 is task_execution_service.py; git merged the import block cleanly and the four scrub blocks in _write_terminal_and_gate / apply_result are untouched (#2513 adds build_pull_queue_payload above them and widens _admission_gate, which the ent#279 allowlist already names).
Merged current
|
vybe
left a comment
There was a problem hiding this comment.
/validate-pr — APPROVE
Building on @dolho's /review (approved, no critical findings) and @obasilakis's /validate-pr (security battery clean, one gating item: re-run CI on current dev). That item is now closed twice over — the branch has been brought onto dev at 36e6e1a3 (post-#2513, the only change since that touches task_execution_service.py), and the full matrix on c596d15e is green: 6/6 pytest seeds, regression diff, e2e (path-triggered, ran and passed without the ui label), frontend build, CodeQL, gitleaks, schema-parity, prod-image-smoke, non-root.
Local verification on the merged tree: 3634 passed / 16 skipped across the 175 unit files adjacent to the seam and the dispatch path; the one failure (test_1310_auth_consolidation…[sqlite]) is order-dependent and passes 46/46 in isolation on this tree and on dev.
Remaining notes, none blocking:
- Cross-tracker
Refs trinity-enterprise#279— no closing keyword, so the automation cannot promote it; ent#279 is alreadystatus-in-devand needs the manual close at the release cut. - The new feature flow (
runtime-secret-scrub.md) uses the Problem/Goal/seam/chokepoints structure rather than the template's Overview/User Story/Entry Points/Frontend/Backend sections. Reasonable for a backend-only seam with no UI entry point; Error Handling / Testing / Related Flows are present. - @dolho's I2 (seam docstring vs. the proactive-message delivery-scrub exception) and I3 (fleet-global scrub cost in the flow's operational notes) are still open as doc follow-ups; I4 was closed by the enterprise write-path floor.
Squash-merging.
Refs trinity-enterprise#279
What this is — PR-2, the public OSS half
The public-stream half of trinity-enterprise#279: a generic OSS runtime secret-scrub seam plus its execution-terminal chokepoint wiring, the license-blind MCP proxy tools, a gated Settings panel, and docs. The private producer that stages a fetched secret before delivering it merged separately in the enterprise submodule (trinity-enterprise#410, on enterprise
mainas90f2f2c).The seam (
src/backend/services/runtime_secret_scrub.py) is mechanism-only and behaviour-neutral for OSS: a producer callsstage_secret(value)(fail-CLOSED — an unstageable value raisesStagingUnavailableso delivery is refused), and the persistence chokepoints callget_staged_values()+scrub_text/scrub_objto strip every staged value out of the durable output (fail-OPEN) before the write. This catches arbitrary prefix-less values (a customer DB password) that the pattern-basedutils/credential_sanitizercannot. With nothing staged, every scrub is a no-op → no schema change, no migration, no flag.Scope (rebased onto
dev@dd910564, 2026-09-02)services/runtime_secret_scrub.pytask_execution_service(apply_resultboth branches — SUCCESSresponse/execution_log/raw_responseand FAILEDerror+ the bug: error_during_execution runs store zero telemetry (no execution_log/session_id/cost) and the JSONL is reaped in 1-7h — failure class is undiagnosable #1853 salvaged transcript & tool_calls summary — plus_write_terminal_and_gate),chat_execution_service(_finalize_chat_successincl. the idempotency snapshot ·_parse_agent_http_errorscrubbed before its ERROR log and before the 500-char truncation ·_finalize_budget_exhausted),pull_coordination_service,proactive_message_service(TOP — before delivery and persist),channel_history,routers/voicetools/credential_vault.ts(list/fetch, license-blind, operator-scope) +client.ts/server.ts/tools/index.ts+tool-visibility.test.tsCredentialVaultPanel.vue+ Settings tab entry (adminOnly,requires: 'credential_vault'); loading states gated onloading && no-data-yetper the bug: background polls re-flash loaded content and reset UI state (design-system p13/p14) #1927 ratchet (design-system p13/p14)credentials.md§3.7 · architecture services-catalog entry · newfeature-flows/runtime-secret-scrub.md+ both index rowsenterprise-docs-guard.yml(SEAM_FILES + bothpaths:)test_ent279_secret_scrub.py(seam + chokepoints + escaping-evasion + cross-worker) ·test_ent279_scrub_parity.py(drift guard, test_1804 idiom; now allowlists dev's bug: watchdog fails admitted-but-undispatched executions as "completed on agent but status not reported", releasing their slots and masking the real terminal #2433_cancel_inflight_if_parked, whose only error write is a static literal) ·test_ent279_d26_combined_tree.py(proves by interception that the enterprise vault's lazystage_secretimport and the OSS chokepoints bind the same module object; skips on OSS-only clones)Status — all former blockers closed; READY
Gitlink bumpDONE, then superseded:devitself now carries the gitlink at90f2f2c, so the branch's own bump commit was dropped as previously-applied during the rebase.git ls-files -s src/backend/enterpriseon this branch →90f2f2c. (Reminder for anyone touching this: the submodule isupdate = none/ignore = all, so gitlink moves are invisible to a plaingit status.)RebaseDONE: ontodd910564(2026-09-02). Two conflicts, both inchat_execution_service.py, both additive (dev's bug: watchdog fails admitted-but-undispatched executions as "completed on agent but status not reported", releasing their slots and masking the real terminal #2433cancelledflag and quote-style change vs. this branch's scrub insertions) — resolved keeping both sides.bug: error_during_execution runs store zero telemetry (no execution_log/session_id/cost) and the JSONL is reaped in 1-7h — failure class is undiagnosable #1853 FAILED-path scrub gapDONE (82b83340): the FAILED branch now identity-scrubsenvelope.execution_logbefore the salvage serialization, mirroring SUCCESS — the salvaged transcript and its derived bug: schedule_executions.tool_calls stores a verbatim copy of execution_log — breaks the tool-call metric and survives log retention #1741tool_callssummary no longer rely on the pattern pass alone.D26 combined-tree testDONE (74531123): the hand-run composition check is now a committed guard.CIDONE (buildred (bug: background polls re-flash loaded content and reset UI state (design-system p13/p14) #1927 loading-gate ratchet)aa15aa93): both bare gates inCredentialVaultPanel.vuenow gate onloading && !items.length.Validation (re-run on this exact tree, 2026-09-02)
pytest unit/ -k 'ent279 or ent435 or 2052'→ 161 passed, 2 skipped (skips environmental: sibling redis:6390)pytest unit/ -k '2433 or 1853'→ 152 passed, 1 skippedvitest tests/unit/loadingGateRatchet.spec.js→ 6 passed/validate-prsweep and/verify-local. CI on this push is the current signal.From the original pre-rebase
/validate-pr— all green at that time: security scans clean;enterprise-docs-guardgrep = 0 hits; packaging fine (services/is COPY'd wholesale, no newos.getenv); commits conventional; docs + both feature-flow index rows present. (The Vue panel has no unit test — repo has no component-mount harness.)Notes for the reviewer / merger
Refs trinity-enterprise#279: same-repo status auto-promotion will not fire, and GitHub will not auto-close the private issue. Setstatus-in-devand close trinity-enterprise#279 by hand at the release cut.🤖 Generated with Claude Code