PR 18.5a: type-seal trusted trigger ingress - #4406
Conversation
There was a problem hiding this comment.
Code Review
This pull request refactors the trusted trigger ingress boundary by retiring the ironclaw_trusted_ingress crate and moving the trusted trigger submission logic directly into ironclaw_conversations. It introduces the ConversationTrustedTriggerSubmitter implementing TrustedTriggerFireSubmitter, seals TrustedTriggerSubmitRequest fields, and updates architecture boundary tests to prevent untrusted ingress paths from submitting host-trusted inbound requests. Feedback on the changes suggests optimizing performance by avoiding unnecessary heap allocations when constructing TrustedInboundTurnRequest and filtering non-blocking warnings, as well as improving maintainability by using an exhaustive match statement instead of a wildcard arm when mapping inbound turn errors.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
henrypark133
left a comment
There was a problem hiding this comment.
Code Review (8 reviewers, sonnet) — head 766d314
Intent: PR 18.5a: type-seal host-trusted trigger ingress path. Seal TrustedTriggerSubmitRequest so only trigger worker can mint trusted submissions from durable state. Move trigger→conversation trusted inbound mapping into ironclaw_conversations. Expose narrow trusted_trigger_fire_submitter(...) -> Arc<dyn TrustedTriggerSubmitter>. Remove old public authority-token facade.
Stats: 9 findings (from ~15 raw, 9 after dedup) across 2 files (mainly inbound.rs). Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, pattern-refactor. Reviewers failed: none. Body-only: 0.
Event: would-be REQUEST_CHANGES (1 High ≥75). PR is draft + author's own PR → posting as COMMENT. Treat the High finding as a blocker before promoting from draft.
Findings
Bugs (High — must fix)
- High
submit_trusted_trigger_outcomereturnsReplayedfor freshly-submitted turn whenidempotency==Duplicatebut no prior turn_submission existed (crates/ironclaw_conversations/src/inbound.rs:391-411, confidence 82) — anchor::402submit_or_replayonly returns early with existing turn_submission whenDuplicate AND inbound_message_turn_submission == Some. WhenDuplicatebut no prior record, falls through and submits fresh turn — response carriesidempotency=Duplicate+ brand-new turn_submission.submit_trusted_trigger_outcomesees Duplicate → returnsReplayed{original_run_id: NEW_RUN_ID}. Caller callsmark_fire_replayedinstead ofmark_fire_accepted→ replayed semantics on fresh submission → can stall subsequent poller decisions gating onactive_run_ref.- Fix: return Replayed only when response came from stored prior submission, not purely on idempotency flag. Thread an explicit "returned-existing-record" flag from
submit_or_replay. - Also flagged by: tests/Medium —
None turn_submissionbranch untested
Conventions / Architecture (Medium)
-
Medium
validate_trigger_prompt+classify_inbound_errorduplicated byte-for-byte acrossironclaw_conversations+ironclaw_reborn_composition(crates/ironclaw_conversations/src/inbound.rs:452-484, confidence 92) — anchor:crates/ironclaw_reborn_composition/src/trigger_poller_trusted_submit.rs:170- 4 reviewers flagged (conventions/maintainability/local-patterns x2). Same Sanitizer, same Severity::High threshold, same log/error messages. Safety/policy change must land in both files or silently diverge. Per
.claude/rules/architecture.md§4 ('duplicate dispatch pipelines'). PR's stated goal was to consolidate trusted trigger logic inironclaw_conversations— but most safety-critical helper still lives in two places. - Fix: extract
validate_trigger_prompttoironclaw_safety(orironclaw_triggers); same forclassify_inbound_error.
- 4 reviewers flagged (conventions/maintainability/local-patterns x2). Same Sanitizer, same Severity::High threshold, same log/error messages. Safety/policy change must land in both files or silently diverge. Per
-
Medium Public
trusted_trigger_fire_submitterre-export at crate root removes compile-time authority enforcement (crates/ironclaw_conversations/src/lib.rs:30-34, confidence 78) — anchor::32- Old design:
&HostTrustedTriggerIngresszero-sized authority token (feature-gated constructor) → compile-time boundary. New design:pub fnre-exported from crate root, relying solely on architecture tests (string-matchinguntrusted_src_rootsallowlist). New workspace crate importingironclaw_conversationscan call factory if missing from allowlist. - Fix: cargo feature
trusted-submitteronly composition-root crates enable; OR enumerate Cargo.toml deps against allowlist (rather than separate src-root list); OR keep factorypub(crate). - Also flagged by: security/Medium — trust-boundary-weakening
- Old design:
-
Medium
classify_trusted_trigger_inbound_errornever producesRetryableFailed/PermanentFailedOk variants expected by caller (crates/ironclaw_conversations/src/inbound.rs:414-450, confidence 75) — anchor::414- Always returns
Err(TriggerError::Backend/InvalidMaterialization). Callerdue_fire.rs:176-193has match arms forRetryableFailed/PermanentFailed— UNREACHABLE. End behavior likely correct (Err arm at due_fire.rs:194 also classifies), but trait contract with 4 Ok variants partially unfulfilled. - Fix: either return
Ok(RetryableFailed/PermanentFailed)to align trait contract, or documentTrustedTriggerSubmittertrait that Err is preferred signal and Ok-failure variants are deprecated. - Also flagged by: tests/Medium — classifier has no unit tests
- Always returns
-
Medium
ironclaw_conversationsnow owns trigger-protocol normalization that crate CLAUDE.md says belongs in adapters (crates/ironclaw_conversations/src/inbound.rs:221-328, confidence 72) — anchor:crates/ironclaw_conversations/CLAUDE.md:1ConversationTrustedTriggerSubmitter+trusted_inbound_request_from_triggerembed hard-codedAdapterKind::new("trigger"),AdapterInstallationId::new("reborn-trigger-poller"), TriggerFire field extraction. Per crate CLAUDE.md: "Own adapter-safe conversation binding... only. Do not parse concrete Slack/Telegram/Web/CLI payloads. Product adapters normalize protocol payloads before calling these services." TriggerFire is a protocol payload.- Fix: keep
ironclaw_conversationsprotocol-neutral. Accept pre-builtInboundTurnRequest/TrustedInboundTurnRequestfrom caller. Move trigger-to-conversation mapping back toironclaw_triggersorironclaw_reborn_composition. - Also flagged by: maintainability/Low — field mapping duplicated
Security (Medium → Low)
-
Medium
validate_trigger_promptscansfire().promptbutcontent_reftarget (actual conversation content) is unchecked at submission (crates/ironclaw_conversations/src/inbound.rs:276-291, confidence 65) — anchor::281- Type system doesn't enforce 'content_ref was minted from this exact scanned prompt'. Test shim or alternate materializer with mismatched prompt/content_ref bypasses detection at submission.
- Fix: document materializer contract clearly OR carry sealed token from materializer→submitter proving consistency.
-
Low
tracing::warn-equivalent internal identifiers (actor_id/thread_id) may leak viaTriggerErrorreason strings to operational logs (crates/ironclaw_conversations/src/inbound.rs:414-450, confidence 55) — anchor::446- Catch-all arm
format!("trusted trigger inbound request invalid: {error}").InboundTurnError::AccessDenied{actor_id, thread_id}Display includes identifiers. Reason propagated to logs. - Fix: opaque message +
tracing::debug!for internal detail.
- Catch-all arm
Performance / Tests
-
Low Trigger prompt safety scan runs twice per fire — materializer + submitter both call
validate_trigger_prompt(crates/ironclaw_conversations/src/inbound.rs:281, confidence 85) — anchor::281- Pure function of input → second scan cannot produce different result. Hot path waste.
- Fix: remove submitter scan (materializer contract guarantees pre-scan); OR document intentional defense-in-depth.
-
Low
validate_trigger_prompt+trusted_inbound_request_from_triggerhave no direct tests in inbound.rs (crates/ironclaw_conversations/src/inbound.rs:452-484, confidence 75) — anchor::452- Only integration tests in
trigger_poller_trusted_submitdrive indirectly. New falliblenew()error paths (invalid creator_user_id, etc.) uncovered.
- Only integration tests in
Other categories
- pattern-refactor ✅ empty — Factory pattern correctly applied; 2-arm BindingResolutionDispatch is correct inline shape
Mergeability
Other priorities before merge:
henrypark133
left a comment
There was a problem hiding this comment.
Code Review Round 2 (8 reviewers, sonnet) — head e026031
Intent: R2 follow-up — address R1 review feedback on PR 18.5a type-seal trusted trigger ingress.
R1 status — substantial progress ✅:
- ✅ R1 High (82) bugs Replayed bug — RESOLVED.
replayed_turn_submission: booladded to InboundTurnResponse; both submit_or_replay branches set it correctly; full test coverage at submit_trusted_trigger_outcome_preserves_received_at_for_accepted_and_replayed_fires. ⚠️ R1 Med (92) conv duplicate validate+classify — PARTIAL.validate_trigger_promptshared. Two classify_*_inbound_error functions remain (see #2 below + new bug #1).- ✅ R1 Med (78) conv public re-export — RESOLVED.
ConversationTrustedTriggerSubmitterpub(crate); only factorytrusted_trigger_fire_submitterexported. - ✅ R1 Med (75) bugs dead Ok variants — RESOLVED via restructure.
- ✅ R1 Med (72) conv layer-misplacement — RESOLVED per commit 11ec73a.
- ✅ R1 Med (65) sec content_ref scan gap — RESOLVED via documentation.
- ✅ R1 Low (85) perf double-scan — DOCUMENTED as intentional defense-in-depth.
- ✅ R1 Low (75) tests untested helpers — RESOLVED.
⚠️ R1 Low (55) sec actor_id/thread_id leak — PARTIAL. opaque_trusted_trigger_inbound_rejection added for permanent path. Retryable branches still leak (see #10).
Stats: 11 findings (from ~16 raw, 11 after dedup). Reviewers: security, bugs, performance, tests, conventions, local-patterns, maintainability, pattern-refactor. Failed: none. Body-only: 2. performance/conventions/pattern-refactor clean.
Event: would-be REQUEST_CHANGES (1 High ≥75). PR is draft + author's own PR → posting as COMMENT. Treat the High finding as blocker before promoting from draft.
Findings
Bugs (High — direct consequence of partial R1 #2 fix)
- High
DurableStateerror classified as permanent — discards trigger fires during transient DB outages (crates/ironclaw_conversations/src/inbound.rs:465-476, confidence 80) — anchor::472- catch-all routes
InboundTurnError::DurableStateto opaque_trusted_trigger_inbound_rejection →TriggerError::InvalidMaterialization(permanent). Siblingclassify_inbound_errorat trigger_poller_trusted_submit.rs:269-272 correctly maps DurableState toBackend(retryable). DB outage during submission phase → silent permanent fire loss instead of retry. Direct consequence of unresolved R1 #2 (two divergent classifiers). - Fix: add explicit arm before catch-all returning
Backendwithdurable state unavailablereason.
- catch-all routes
Maintainability (Medium — root cause of #1)
- Medium R1 #2 PARTIAL — two diverging
classify_*_inbound_errorfunctions remain (crates/ironclaw_conversations/src/inbound.rs:428-477, confidence 75) — anchor::428- inbound.rs:428 (exhaustive) + trigger_poller_trusted_submit.rs:240 (catch-all
_arm). Same InboundTurnError → TriggerError mapping in two places. #1 DurableState bug is direct consequence. Future TurnError variants silently diverge. - Fix: consolidate to
impl From<InboundTurnError> for TriggerErrorinironclaw_triggersorironclaw_conversations. Delete composition copy. - Also flagged by: local-patterns/Nit naming divergence
- inbound.rs:428 (exhaustive) + trigger_poller_trusted_submit.rs:240 (catch-all
Tests (Medium — would have caught #1)
-
Medium
classify_trusted_trigger_inbound_errorhas no direct unit tests in conversations crate (crates/ironclaw_conversations/src/inbound.rs:428-477, confidence 75) — anchor::428- 10 InboundTurnError variants; only exercised indirectly via composition crate's parallel classify_inbound_error. Direct unit test for DurableState would have caught #1.
- Fix: add
classify_trusted_trigger_inbound_error_durable_state_is_retryable_backend+ arm-by-arm tests.
-
Medium
trusted_inbound_request_from_triggererror paths untested at caller level (crates/ironclaw_conversations/src/inbound.rs:305-338, confidence 75) — anchor::305- Helper calls 6
*::new()Result-returning constructors mapped via?to InboundTurnError. Caller-level test required by testing.md (wrapper between helper + side effect).
- Helper calls 6
Security (Medium)
- Medium
validate_trigger_promptkeeps only LAST High-severity warning + missing audit counts (crates/ironclaw_conversations/src/inbound.rs:493-523, confidence 72) — anchor::497- Loop overwrites
blocked_warningeach High hit. High warnings don't incrementwarning_countor updatemax_severity→ audit log shows 0 warnings + no max_severity if ALL findings are High. Prompt still blocked, defense-in-depth audit trail lost on multi-pattern injection. - Fix: count+max_severity-update ALL warnings;
breakon first High (early-exit safer).
- Loop overwrites
Maintainability (Low)
-
Low Reborn-specific tracing-target check hardcoded into shared boundary scanner (
crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs:2648-2651, confidence 72) — anchor::2649collect_forbidden_uses+collect_forbidden_reborn_auth_file_usescontainif rule.pattern == "ironclaw::" && is_reborn_tracing_target_line(line)— couples Reborn-specific exemption into general-purpose scanner.- Fix: add
exempt: Option<fn(&str) -> bool>field to ForbiddenUse struct.
-
Low
replayed_turn_submission: boolleaks trigger-delivery concern into general InboundTurnResponse (crates/ironclaw_conversations/src/types.rs:193-200, confidence 65) — anchor::199pub+serialized field used only by trigger submission path. Part of cross-crate contract for trigger-specific concern.- Fix: return distinction from submit_or_replay via local
SubmitOrReplayOutcomestruct; don't expose on shared InboundTurnResponse.
Local patterns
- Low
validate_trigger_promptreturns TriggerError but lives in ironclaw_conversations (crates/ironclaw_conversations/src/inbound.rs:489-524, confidence 65) — anchor:CLAUDE.md:1- Function returns TriggerError (from ironclaw_triggers). conversations CLAUDE.md restricts crate to "adapter-safe conversation binding". Reader would not look here for trigger prompt validation.
- Fix: move to ironclaw_triggers (e.g.
src/validation.rsadjacent to TriggerError).
Tests (Low)
- Low dir-scanner detection self-test missing (
crates/ironclaw_architecture/tests/reborn_dependency_boundaries.rs:2626-2664, confidence 65) — anchor::2626- File-level scanner has detection self-test ✅. Dir-level scanner only has allowance test (skip tracing targets). No test that dir-scanner REPORTS violations.
Security (Low — R1 #9 partial)
-
Low R1 #9 PARTIAL — retryable Backend reasons still embed raw InboundTurnError display (
crates/ironclaw_conversations/src/inbound.rs:428-456, confidence 58) — anchor::430- Opaque rejection ✅ for permanent. Retryable arms still
format!("...{error}")interpolating Display incl.active_run_idfrom ThreadBusy. Cross-tenant identifier exposure via logs. - Fix: apply same opaque wrapping to retryable branches.
- Opaque rejection ✅ for permanent. Retryable arms still
-
Low Composition catch-all
classify_inbound_erroralso embeds InboundTurnError display (crates/ironclaw_reborn_composition/src/trigger_poller_trusted_submit.rs:240-276, confidence 55) — anchor::272(body only)- Same leak in catch-all
{error}. Includes raw actor_id/thread_id from AccessDenied/BindingConflict. Folds into #10 fix.
- Same leak in catch-all
Other categories
- performance ✅ clean — bounded by MAX_TRIGGER_PROMPT_BYTES=32KiB + 1 fire/tick
- conventions ✅ clean (rolled into local-patterns + maintainability above)
- pattern-refactor ✅ empty
Mergeability
Other priorities before merge:
henrypark133
left a comment
There was a problem hiding this comment.
Code Review Round 3 (8 reviewers, sonnet) — head c1056bd
Intent: R3 follow-up — address R2 review feedback. Extract validate_trigger_prompt to ironclaw_safety. Move trusted_submit helpers to ironclaw_triggers. Retire old ironclaw_trusted_ingress crate. Redact classifier logs.
R2 status — substantial progress ✅:
- ✅ R2 High (80) DurableState — RESOLVED. Now retryable_trusted_trigger_backend_error in both classifiers.
⚠️ R2 Med (75) duplicate classifiers — PARTIAL. validate_trigger_prompt extracted ✅, BUT classifiers STILL duplicated (see #1).- ✅ R2 Med (72) validate keeps LAST warning — RESOLVED. Now FIRST via
blocked_warning.is_none()guard. - ✅ R2 Low (72) Reborn tracing-target hardcoded — RESOLVED.
exempt: Option<fn(&str) -> bool>field added to ForbiddenUse. - ✅ R2 Low (65) validate_trigger_prompt location — RESOLVED. Extracted to ironclaw_safety/src/prompt_validation.rs.
- ✅ R2 Low (65) dir-scanner detection test — RESOLVED (
collect_forbidden_uses_detects_violationadded). - ✅ R2 Low (58) retryable Backend reason leak — RESOLVED.
_error: &InboundTurnErrorunused, static strings only. - ✅ R2 Low (55) composition catch-all leak — RESOLVED. Same pattern.
⚠️ R2 Low (65) replayed_turn_submission field leak — STILL OPEN (see #7).⚠️ R2 Med (75) tests classify untested — PARTIAL coverage (see #2/#3).⚠️ R2 Med (75) tests trusted_inbound_request_from_trigger untested — STILL OPEN (see #4).
✅ Old ironclaw_trusted_ingress crate retired — clean removal of AGENTS.md / Cargo.toml / lib.rs.
Stats: 10 findings (from ~13 raw, 10 after dedup) across 5 files. Reviewers run: security, bugs, performance, tests, conventions, local-patterns, maintainability, pattern-refactor. Reviewers failed: none. Body-only: 5. security/conventions/pattern-refactor clean.
Event: COMMENT — no Critical/High ≥75. Largest = Med 85.
Findings
Maintainability (Medium — R2 #2 PARTIAL)
- Medium R2 #2 PARTIAL — InboundTurnError→TriggerError classifier still written twice (
crates/ironclaw_conversations/src/inbound.rs:441-484, confidence 85) — anchor::441- validate_trigger_prompt extracted ✅, but classify_trusted_trigger_inbound_error (inbound.rs:441) + classify_materializer_inbound_error (trigger_poller_trusted_submit.rs:251) STILL structurally identical: same exhaustive match arms, same Backend/InvalidMaterialization mapping. Any new InboundTurnError variant requires changes in both. Architecture test enforces privacy but not single source of truth.
- Fix: extract
pub(crate) fn classify_inbound_turn_error_for_triggerinto ironclaw_triggers::trusted_submit. Both callers invoke shared fn. - Also flagged by: local-patterns/Low (helper functions take unused
_errorparam breaking sibling consistency with composition variants)
Tests (Medium — class of bugs prevented by coverage)
-
Medium classify_trusted_trigger_inbound_error missing AdmissionRejected branches + remaining TurnError + non-submission arms (
crates/ironclaw_conversations/src/inbound.rs:441-484, confidence 85) — anchor::441- Current tests cover only ThreadBusy/Conflict/Unauthorized/DurableState/InvalidExternalRef. UNTESTED: AdmissionRejected(TenantLimit/Unavailable/ProfileRejected/Policy), Unavailable, CapacityExceeded, ScopeNotFound, InvalidRequest, InvalidTransition, LeaseMismatch, BindingRequired, AccessDenied, BindingConflict, ThreadNotFound, StatePoisoned, InvalidCanonicalRef. Misclassification silently stalls or discards fires.
-
Medium classify_materializer_inbound_error missing non-submission + remaining TurnError arms (
crates/ironclaw_reborn_composition/src/trigger_poller_trusted_submit.rs:251-293, confidence 80) — anchor::251(body only)- Materializer-side classifier coverage mirrors #2 gap. UNTESTED: BindingRequired, BindingConflict, ThreadNotFound, StatePoisoned, InvalidCanonicalRef, ScopeNotFound, InvalidRequest, InvalidTransition, LeaseMismatch. Same misclassification risk.
-
Medium R2 #4 STILL OPEN — trusted_inbound_request_from_trigger error paths untested (
crates/ironclaw_conversations/src/inbound.rs:308-341, confidence 75) — anchor::308- Helper calls 6
*::new()Result-returning constructors. None of constructor-failure paths tested. Malformed TriggerTrustedInboundBinding → silently classified InvalidMaterialization without path exercised.
- Helper calls 6
Bugs (Medium — variant mismatch)
- Medium submit_trusted_trigger_outcome conflates None+non-Accepted variants into misleading Backend error (
crates/ironclaw_conversations/src/inbound.rs:411-417, confidence 72) — anchor::412let Some(SubmitTurnResponse::Accepted{...}) = &response.turn_submission elsecollapses two distinct cases into one error: (1) None, (2) Some(non-Accepted variant). Future non-Accepted variant → factually-wrong "no turn submission" reason + retryable classification. Correct behavior for non-Accepted may be permanent.- Fix: match None vs Some(_) catch-all separately.
Tests (Medium — defense-in-depth gate untested at submitter boundary)
- Medium submit_trusted_trigger_fire safety-rejection path untested at submitter boundary (
crates/ironclaw_conversations/src/inbound.rs:262-305, confidence 70) — anchor::286- ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire defense-in-depth scan returns InvalidMaterialization on rejection. No test calls submit_trusted_trigger_fire directly with malicious prompt — only materializer-level scan tested. Per testing.md Test-Through-the-Caller, submitter's own gate needs coverage.
Performance
- Medium Default TriggerActiveRunLookup::active_run_states impl is N sequential awaits, not batch (
crates/ironclaw_triggers/src/worker/ports.rs:108-117, confidence 62) — anchor:crates/ironclaw_triggers/src/worker/active_cleanup.rs:70(body only)- Default loops + awaits one at a time. active_cleanup batches requests specifically to amortize backend reads. Production composition wiring default (not override) → active-cleanup degrades from O(1 batch) to O(N sequential) per tick.
- Fix: seal trait OR make active_run_states the required method with active_run_state convenience default.
Maintainability (Low)
-
Low R2 #7 STILL OPEN — replayed_turn_submission bool leaks trigger replay semantics into shared InboundTurnResponse (
crates/ironclaw_conversations/src/types.rs:193-200, confidence 72) — anchor::199(body only)- InboundTurnResponse used by both untrusted + trusted paths. Untrusted callers never read field. CLAUDE.md scopes crate to 'adapter-safe conversation binding'.
- Fix: return enum from handle_inbound_turn_with_trusted_scope encoding trusted outcome directly.
-
Low TriggerTrustedInboundBinding stores typed values as raw Strings, forcing re-validation downstream (
crates/ironclaw_triggers/src/trusted_submit.rs:10-60, confidence 65) — anchor::10(body only)- 7 String fields. Downstream consumers call AdapterKind::new etc. re-validating values already typed in for_fire(). Compiler can't see re-validation can never fail → 4
.map_errcalls + nested Ok(...) per call site. - Fix: store typed values OR expose
into_inbound_turn_request()method on binding.
- 7 String fields. Downstream consumers call AdapterKind::new etc. re-validating values already typed in for_fire(). Compiler can't see re-validation can never fail → 4
Tests (Low)
- Low validate_trusted_trigger_prompt missing empty-string + multi-medium coverage (
crates/ironclaw_safety/src/prompt_validation.rs:23-57, confidence 65) — anchor::31(body only)- Empty-prompt + multi-medium audit-log path uncovered. Audit branch conditioning not exercised.
Other categories
- security ✅ clean — R2 #10/#11 leaks resolved via _error-unused + static strings
- conventions ✅ clean — trusted_ingress crate retirement properly removes all artifacts
- pattern-refactor ✅ empty — Factory correctly applied; classifier duplication caught by maintainability
Mergeability
✅ Substantial R3 progress — 8 of 11 R2 findings RESOLVED. PR still draft.
Recommend before promoting from draft:
- Close #1 (consolidate classifiers — R2 #2 PARTIAL, prevents class of bugs from divergence)
- Close #2 + #3 (test the existing classifiers — would have caught misclassification regressions)
- Close #4 (R2 #4 carryover — caller-level error path test)
- Close #5 (one-line match restructure — guards future SubmitTurnResponse variants)
Optional follow-ups: #6 (submitter-level safety test), #7-#10 (architectural cleanups).
henrypark133
left a comment
There was a problem hiding this comment.
Code Review Round 4 (8 reviewers, sonnet) — head 8824613
R3 status — 4/10 RESOLVED ✅:
- ✅ R3 #1 Med (85) duplicate classifier → RESOLVED. New
crates/ironclaw_conversations/src/trusted_trigger.rscentralizes; composition imports from there. - ✅ R3 #2 Med (85) AdmissionRejected + remaining InboundTurnError arms → RESOLVED (b2f8ab8).
- ✅ R3 #3 Med (80) materializer non-submission arms → RESOLVED (same commit).
- ✅ R3 #5 Med (72) submit_outcome conflates None/non-Accepted → RESOLVED (8824613).
⚠️ R3 #4 Med (75) trusted_inbound_request_from_trigger error paths → STILL OPEN (see #2).⚠️ R3 #6 Med (70) submitter-level safety-rejection → STILL OPEN (see #1).- ⚪ R3 #7 Med (62) default active_run_states N+1 → NOTE-ONLY (R3 deferred).
⚠️ R3 #8 Low (72) replayed_turn_submission field leak → STILL OPEN. NOTE-ONLY per directive.⚠️ R3 #9 Low (65) raw Strings → STILL OPEN (see #7).⚠️ R3 #10 Low (65) prompt empty/multi-medium → STILL OPEN. NOTE-ONLY.
Stats: 7 findings (from ~9 raw, 7 after dedup). Reviewers: 8. Reviewers failed: none. Body-only: 3. security/performance/pattern-refactor clean.
Event: COMMENT — no Critical/High ≥75.
Findings
Tests (Medium)
- Medium R3 #6 STILL OPEN — submit_trusted_trigger_fire safety-rejection untested at submitter boundary (
crates/ironclaw_conversations/src/inbound.rs:287-308, conf 85) — anchor::295- validate_trusted_trigger_prompt + trigger_prompt_safety_rejection map_err NOT tested through ConversationTrustedTriggerSubmitter. Per Test-Through-the-Caller.
- Medium R3 #4 STILL OPEN — trusted_inbound_request_from_trigger field-validation error paths untested (
crates/ironclaw_conversations/src/inbound.rs:309-357, conf 80) — anchor::309- 5 *::new() constructors → InvalidExternalRef. None of rejection arms exercised.
Conventions (Medium)
- Medium
trusted_triggermodule ispub— all sibling modules in same crate private (crates/ironclaw_conversations/src/lib.rs:22, conf 72) — anchor::22(body only)pub mod trusted_triggermakesclassify_inbound_error+TrustedTriggerInboundFailureKindaccessible to every downstream crate. Architecture testconversation_trusted_trigger_classifier_stays_out_of_root_exportsonly guards root re-exports — NOT pub module.untrusted_ingress_paths_cannot_submit_host_trusted_inboundonly checks 4 patterns — classify_inbound_error not listed. All other modules in crate are private + selective pub use.- Fix:
mod trusted_trigger; pub use trusted_trigger::{TrustedTriggerInboundFailureKind, classify_inbound_error};OR add architecture guard forbidding classify_inbound_error in untrusted ingress paths. - Also flagged by: local-patterns/Low (sibling-pattern)
Bugs (Low)
- Low opaque_trusted_trigger_inbound_rejection logs same hardcoded message for both call sites (
crates/ironclaw_conversations/src/inbound.rs:467-475, conf 85) — anchor::471- SubmitRejected (reason='trusted trigger submit rejected') + InboundRequestRejected (reason='trusted trigger inbound request rejected') both emit identical debug log. SubmitRejected case silently misattributed in traces.
- Fix:
tracing::debug!(reason, "trusted trigger inbound rejection");
Maintainability (Low)
- Low TriggerConversationFields single-use pass-through struct (
crates/ironclaw_reborn_composition/src/trigger_poller_trusted_submit.rs:130-183, conf 75) — anchor::130(body only)- Struct constructed only by trigger_conversation_fields, fields immediately destructured. No methods, no reuse.
- Low BindingResolutionPolicy + BindingResolutionDispatch structurally identical enums (
crates/ironclaw_conversations/src/inbound.rs:344-390, conf 72) — anchor::353- Same variants. Only semantic difference: into_resolution_parts zeros requested_* on Trusted arm. Single enum + inline match sufficient.
Local patterns (Low)
- Low R3 #9 STILL — TriggerTrustedInboundBinding 7 raw String fields (
crates/ironclaw_triggers/src/trusted_submit.rs:10-18, conf 65) — anchor::10(body only)- types.md rule: domain identifiers should use newtypes. Re-validated downstream → callers can pass raw strings without compile enforcement.
Other categories
- security ✅ clean
- performance ✅ clean (double scan documented defense-in-depth)
- pattern-refactor ✅ empty
Mergeability
Recommend before promoting from draft:
|
R3 #7 active-run batch lookup: keeping this deferred. The general trait note is valid: the default |
henrypark133
left a comment
There was a problem hiding this comment.
Code Review Round 5 (8 reviewers, sonnet) — head 20931c1
Intent: R5 follow-up — hide trusted trigger classifier module + R4 follow-ups.
R4 status:
⚠️ R4 #1 Med (85) tests submit_trusted_trigger_fire safety-rejection at submitter → STILL OPEN (see #2).⚠️ R4 #2 Med (80) tests trusted_inbound_request_from_trigger field-validation → STILL OPEN (see #3).⚠️ R4 #3 Med (72) conv trusted_trigger pub module → PARTIAL — module is private ✅ but symbols re-exported (see #1).- ✅ R4 #4 Low (85) bugs opaque log message → RESOLVED. Now uses structured
reasonfield. - ⚪ R4 #5 Low (75) maint TriggerConversationFields → NOTE-ONLY (deferred).
- ✅ R4 #6 Low (72) maint BindingResolutionDispatch removed → RESOLVED.
BindingResolutionPolicyis single enum. - ⚪ R4 #7 Low (65) LP raw String fields → NOTE-ONLY (deferred).
Stats: 3 findings. Reviewers: 8. Reviewers failed: none. Body-only: 0. security/bugs/performance/local-patterns/maintainability/pattern-refactor clean.
Event: COMMENT — no Critical/High ≥75.
Findings
Conventions (Medium — high-confidence regression)
- Medium R4 #3 PARTIAL — lib.rs re-exports classify_inbound_error despite "Hide trusted trigger classifier module" commit (
crates/ironclaw_conversations/src/lib.rs:36, confidence 95) — anchor::36- 5 reviewers converged on this finding.
mod trusted_triggermade private ✅. But line 36 stillpub use trusted_trigger::{TrustedTriggerInboundFailureKind, classify_inbound_error}. Architecture testconversation_trusted_trigger_classifier_stays_out_of_root_exportschecks for OLD nameclassify_trusted_trigger_inbound_error+pub mod trusted_trigger— both absent now, test passes SILENTLY while new nameclassify_inbound_erroris still exported. ironclaw_reborn_composition imports both via crate root (trigger_poller_trusted_submit.rs:8). Self-contradicting commit. - Fix: remove
pub use trusted_trigger::{...}from lib.rs. Update architecture guard to also check new names. Composition absorbs local classification OR accesses via crate-private path.
- 5 reviewers converged on this finding.
Tests (Medium — R4 carryovers)
- Medium R4 #1 STILL OPEN — submit_trusted_trigger_fire safety-rejection at submitter boundary (
crates/ironclaw_conversations/src/inbound.rs:289-308, confidence 85) — anchor::297- validate_trusted_trigger_prompt + trigger_prompt_safety_rejection map_err uncovered through direct ConversationTrustedTriggerSubmitter call. Worker-path tests insufficient per Test-Through-the-Caller — wrapper + mapping layer between helper + side effect.
- Medium R4 #2 STILL OPEN — trusted_inbound_request_from_trigger field-validation untested at caller (
crates/ironclaw_conversations/src/inbound.rs:311-357, confidence 80) — anchor::311- 6 *::new() constructors → InvalidExternalRef. None of rejection arms exercised. Malformed binding → silent classification.
Other categories all clean
Mergeability
Must close before promoting from draft:
- #1 (boundary leak — high conf 95, the commit title was "Hide" but symbols still exported. Architecture guard doesn't catch the new names.)
- #2 + #3 (R4 carryovers — Test-Through-the-Caller for safety + binding validation)
Other R4 findings either RESOLVED or NOTE-ONLY (raw Strings, TriggerConversationFields).
| S: SessionThreadService, | ||
| C: TurnCoordinator + ?Sized, | ||
| { | ||
| async fn submit_trusted_trigger_fire( |
There was a problem hiding this comment.
Medium — R4 #1 STILL OPEN — submit_trusted_trigger_fire safety-rejection untested at submitter boundary.
Reviewer disagreement: some say resolved via worker-path test, others say submitter-level safety gate (validate_trusted_trigger_prompt + trigger_prompt_safety_rejection map_err) still uncovered when driven directly through ConversationTrustedTriggerSubmitter. Per testing.md Test-Through-the-Caller — wrapper + mapping layer between helper + side effect needs caller-level test at the submitter itself, not just the worker.
Fix: Add submit_trusted_trigger_fire_rejects_high_severity_injection_prompt — construct ConversationTrustedTriggerSubmitter via trusted_trigger_fire_submitter, submit TrustedTriggerSubmitRequest with injection prompt, assert InvalidMaterialization + coordinator.submissions().is_empty().
| } | ||
| } | ||
|
|
||
| fn trusted_inbound_request_from_trigger( |
There was a problem hiding this comment.
Medium — R4 #2 STILL OPEN — trusted_inbound_request_from_trigger field-validation untested at caller.
5 *::new() constructors (AdapterKind, AdapterInstallationId, ExternalActorRef, ExternalConversationRef, ExternalEventId, InboundMessageContentRef) → InvalidExternalRef. None of rejection arms exercised through full ConversationTrustedTriggerSubmitter::submit_trusted_trigger_fire path. Malformed TriggerTrustedInboundBinding (empty adapter_kind) → propagates through classify → InvalidMaterialization without dedicated test.
Fix: Add submit_trusted_trigger_fire_maps_invalid_binding_field_to_invalid_materialization covering empty/malformed adapter_kind, external_actor_id, content_ref propagated through trusted_inbound_request_from_trigger.
|
R5 classifier export finding addressed in efe42cb. Removed the root export of trusted_trigger classifier symbols, made TrustedTriggerInboundFailureKind and classify_inbound_error pub(crate), updated composition to stop importing them from ironclaw_conversations, and tightened the architecture guardrail to check the current symbol names instead of only the old private helper name. This preserves the boundary: conversations keeps submitter classification private; composition owns its local materializer classification. |
efe42cb to
9bc7207
Compare
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9bc720737d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| @@ -48,9 +48,7 @@ impl TriggerPollerWorker { | |||
| active_run_ref: None, | |||
| }, | |||
| }); | |||
There was a problem hiding this comment.
Advance past claim-only active rows
When the first active record in a cleanup page has active_fire_slot but no active_run_ref, this now records first_unadvanced_cursor, which prevents next_cursor from advancing for every later record in the page. In the inspected trigger poller cleanup flow, a single claim-only row at the front of the active scan causes every subsequent tick to re-read and skip that same row, so terminal active runs after it are never looked up or cleared and their triggers remain stuck as already-active indefinitely.
Useful? React with 👍 / 👎.
* Seal trusted trigger ingress * Address trusted trigger review feedback * Tighten trusted trigger materialization boundary * Cover trusted trigger replay flags * Document trusted trigger prompt rescan * Allow Reborn tracing targets in auth guard * Address trusted trigger review comments * Refine trusted trigger boundary helpers * Redact trusted trigger classifier logs * Tighten trusted trigger helper boundaries * Remove retired trusted ingress crate * Cover trusted trigger classifier branches * Centralize trusted trigger inbound classification * Make trusted trigger submit outcome exhaustive * Address trusted trigger review follow-ups * Hide trusted trigger classifier module * Keep trusted trigger classifier private * Refresh trusted trigger plan status * Advance cleanup cursor past claim-only rows
Summary
Implements PR 18.5a for the trigger loop delivery resolution plan: type-seal the host-trusted trigger ingress path and remove the old public authority-token facade.
TrustedTriggerSubmitRequestso only the trigger worker can mint trusted trigger submissions from durable trigger stateironclaw_conversations, keepingTrustedInboundTurnRequestprivatetrusted_trigger_fire_submitter(...) -> Arc<dyn TrustedTriggerFireSubmitter>factory instead of a public concrete trusted submitterValidation
cargo fmtcargo test -p ironclaw_triggers worker --libcargo test -p ironclaw_triggers worker --lib -- --nocapturecargo test -p ironclaw_conversations inbound::tests --libcargo test -p ironclaw_reborn_composition trigger_poller_trusted_submit::tests -- --nocapturecargo test -p ironclaw_reborn_composition materializer_rejects_foreign_tenant_fire_before_binding_or_prompt_write -- --nocapturecargo test -p ironclaw_architecture --test reborn_dependency_boundaries -- --nocapturegit diff --check$code-reviewwithgpt-5.4-minireviewers; straightforward test comments fixed, design/hygiene comment addressed by factory + guardrails