Skip to content

feat(reborn): project Slack ingress routes from the manifest, delete the Rust policy literals - #5626

Merged
ilblackdragon merged 5 commits into
mainfrom
feat/slack-manifest-ingress-projection
Jul 5, 2026
Merged

ilblackdragon merged 5 commits into
mainfrom
feat/slack-manifest-ingress-projection

Conversation

@ilblackdragon

Copy link
Copy Markdown
Member

Summary

Makes the manifest-driven ingress contract from #5625 load-bearing: Slack's two inbound routes (slack.events, slack.commands) are now declared as data in the bundled extension manifest and projected into descriptors at serve time, instead of being hand-written Rust policy literals. This is the real example of usage the mechanism needed — and it answers the "unused API" question on #5625 by giving host_ingress() a production consumer that deletes per-channel Rust.

Stacked on #5625 (needs ProductAdapterHostApiSection::host_ingress()). Review/merge that first; this PR's base is the #5625 branch so the diff here is only the migration.

What moved (and what didn't)

  • assets/slack/manifest.toml now declares [[product_adapter.inbound.host_ingress]] for both routes, each naming slack_bot_token as its verifying credential (fail-closed credential coherence, enforced by the registry).
  • slack_serve.rs: slack_events_route_descriptors / slack_commands_route_descriptors now project their descriptor from the bundled manifest via a new generic helper (composition::host_ingress::bundled_host_ingress_descriptor); the two hardcoded slack_events_policy() / slack_commands_policy() literals (~40 lines each) are deleted.
  • Only the declarative descriptor moved. The axum handler and the HMAC verifier (behavior) stay in Rust — a manifest cannot carry behavior, and that's correct.

The descriptor is validated by ironclaw_host_api on deserialize (dotted route id, absolute path, and the fail-closed floor that a public_webhook listener MUST require webhook_signature) and by ironclaw_product_adapter_registry for ingress credential coherence — so a manifest cannot declare a weaker route than the Rust literal did.

Why this is safe

  • Behavior-preserving equivalence guards: slack_{events,commands}_route_descriptor_matches_manifest_projection assert the manifest-projected descriptor is byte-for-byte identical to the pre-migration Rust literal (1 MiB / 12k·60s for events, 16 KiB / 6k·60s for commands).
  • Existing coverage still green: the 377 Slack serve/e2e/handler lib tests — which drive real signed and forged webhooks through the mounts — all pass against the projected descriptors.
  • cargo fmt + cargo clippy (slack-v2-host-beta,webui-v2-beta,libsql, --tests) clean. No public API signature changed; the manifest digest recomputes consistently (no test pins it).

Net effect

Deletes the per-surface Rust policy literals for both Slack routes; adding or adjusting a channel's inbound route becomes editing a manifest, not writing serve-layer Rust. Next step (separate PR): a generic serve loop that mounts every enabled extension's declared ingress routes, so new channels need no bespoke mount code at all.

🤖 Generated with Claude Code

…dential coherence

Re-derives the one still-valuable idea from the superseded earliest-reborn
ingress stack (#5072/#5093/#5100/#5107, closed) fresh atop main's current
`ironclaw_host_api::ingress` contract — as a single small, self-contained
registry-only change, instead of rebasing a ~250-commit-stale 4-deep stack
built on a `host_ingress_registry` crate main never adopted.

A ProductAdapter manifest section may now declare `[[...host_ingress]]`
routes, each carrying a full host-owned `IngressRouteDescriptor` (validated
by host_api's own Deserialize — dotted route id, absolute path, and every
policy invariant including the fail-closed floor that a `public_webhook`
listener MUST require `webhook_signature`) plus the `credential_handles`
that verify it.

The registry does NOT re-own ingress route/policy vocabulary; it projects
the descriptor and adds the binding host_api deliberately lacks — ingress
credential coherence, enforced fail-closed:
- every credential handle must be declared in `required_credentials`
  (mirrors the egress rule; ingress handles flow into the same declared set
  installation bindings validate against),
- an auth-required route must name at least one verifying credential handle
  (no route nothing can authenticate),
- route ids stay distinct within a section.

This is exactly the seam PR #5107's review flagged as fail-open/TODO'd. The
serve-layer generic mount (descriptor -> axum route + reused
SharedSecretHeader/Hmac verifier) is the deliberate follow-up; main's
PublicRouteMount + descriptor-fold middleware already generalize, so it is a
small wiring change with no new crate.

Tests: ingress credential-coherence matrix (undeclared handle, auth-required
route missing credential, duplicate route id, happy projection) as focused
unit tests in src/lib.rs; wire-path projection, the inherited fail-closed
floor, and coherence-over-the-wire as integration tests in
tests/manifest_ingestion.rs. fmt + clippy (all-features, --tests) clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Slack webhook routes are now defined from the bundled extension manifest, enabling hosted ingress for Slack events and slash commands.
    • Added support for an additional required Slack credential for inbound requests.
  • Bug Fixes

    • Tightened validation so webhook routes fail closed when credentials are missing, mismatched, or duplicated.
    • Ensured public webhook routes require the proper signature checks and route settings.
  • Tests

    • Added coverage for manifest projection and ingress validation edge cases.

Walkthrough

Adds host_ingress route declarations to the Slack manifest with webhook signature auth, extends the product adapter registry to project and validate these routes with fail-closed credential coherence rules, and updates the composition layer to project Slack ingress descriptors from the manifest instead of hardcoded literals.

Changes

Manifest-driven Slack host-ingress route projection

Layer / File(s) Summary
Slack manifest route declarations
crates/ironclaw_first_party_extensions/assets/slack/manifest.toml
Adds slack_signing_secret credential and host_ingress entries for slack.events/slack.commands with webhook_signature auth, body/rate limits, and effect_path.
Registry projection and validation
crates/ironclaw_product_adapter_registry/src/lib.rs, crates/ironclaw_product_adapter_registry/CLAUDE.md, crates/ironclaw_product_adapter_registry/tests/manifest_ingestion.rs
Adds HostIngressRoute, host_ingress field/accessor, raw manifest parsing, fail-closed coherence validation (unique IDs, credential declaration, auth-required checks), new RegistryError variants, and matching tests/docs.
Composition-layer projection module
crates/ironclaw_reborn_composition/src/available_extensions.rs, crates/ironclaw_reborn_composition/src/host_ingress.rs, crates/ironclaw_reborn_composition/src/lib.rs
Adds slack_manifest_toml() accessor, host_ingress module projecting IngressRouteDescriptors from the manifest with route lookup, and module registration.
Slack serve wiring migration
crates/ironclaw_reborn_composition/src/slack_serve.rs
Replaces hardcoded descriptor/policy construction with manifest-projected SLACK_INGRESS_DESCRIPTORS, removes local constants/helpers, updates tests for equality against reconstructed pre-migration values.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • nearai/ironclaw#5625: Overlaps directly with the registry-only HostIngressRoute projection, fail-closed coherence validation, and manifest ingestion tests.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description covers the summary, but most required template sections are missing or unfilled, including change type, linked issue, and review track. Add the missing template sections and complete the required fields for Change Type, Linked Issue, Validation, Security Impact, trust-boundary, Database Impact, Blast Radius, Rollback Plan, and Review Follow-Through.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title uses Conventional Commits and accurately summarizes the Slack ingress migration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5626 July 4, 2026 00:21 Destroyed
@github-actions github-actions Bot added size: L 200-499 changed lines risk: low Changes to docs, tests, or low-risk modules contributor: core 20+ merged PRs labels Jul 4, 2026

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request migrates the Slack ingress route definitions (events and commands) from hardcoded Rust literals in slack_serve.rs to declarative data in the bundled Slack extension manifest (manifest.toml). It introduces a helper module host_ingress to project these route descriptors from the manifest at runtime, and updates the tests to verify that the projected descriptors match the expected pre-migration configurations. The reviewer suggests caching the projected descriptors for both the Slack events and commands routes using std::sync::LazyLock to avoid parsing the manifest TOML repeatedly on every call.

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.

Comment on lines 241 to 243
pub fn slack_events_route_descriptors() -> Vec<IngressRouteDescriptor> {
let descriptor = IngressRouteDescriptor::new(
SLACK_EVENTS_ROUTE_ID,
NetworkMethod::Post,
SLACK_EVENTS_PATH,
slack_events_policy(),
)
.expect("Slack events route descriptor must validate at startup"); // safety: route id/path are crate-local literals and policy is built by sibling helper.
vec![descriptor]
vec![bundled_slack_ingress_descriptor(SLACK_EVENTS_ROUTE_ID)]
}

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.

medium

To avoid redundant parsing of the Slack manifest TOML on every call to slack_events_route_descriptors, we should cache the projected descriptor using std::sync::LazyLock. This aligns with the general rule to avoid redundant parsing of configuration files by projecting them exactly once.

Suggested change
pub fn slack_events_route_descriptors() -> Vec<IngressRouteDescriptor> {
let descriptor = IngressRouteDescriptor::new(
SLACK_EVENTS_ROUTE_ID,
NetworkMethod::Post,
SLACK_EVENTS_PATH,
slack_events_policy(),
)
.expect("Slack events route descriptor must validate at startup"); // safety: route id/path are crate-local literals and policy is built by sibling helper.
vec![descriptor]
vec![bundled_slack_ingress_descriptor(SLACK_EVENTS_ROUTE_ID)]
}
pub fn slack_events_route_descriptors() -> Vec<IngressRouteDescriptor> {
static DESCRIPTOR: std::sync::LazyLock<IngressRouteDescriptor> = std::sync::LazyLock::new(|| {
bundled_slack_ingress_descriptor(SLACK_EVENTS_ROUTE_ID)
});
vec![DESCRIPTOR.clone()]
}
References
  1. Avoid redundant parsing and iteration of configuration files (e.g., TOML manifests) by projecting them into typed entries exactly once, validating the set, and replaying the pre-parsed entries.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in fd152f9 (prior commit): the projection is cached in a LazyLock — now consolidated further in 27731cb, where both Slack descriptors are projected from a single manifest parse (SLACK_INGRESS_DESCRIPTORS).

Comment on lines 510 to 512
pub fn slack_commands_route_descriptors() -> Vec<IngressRouteDescriptor> {
let descriptor = IngressRouteDescriptor::new(
SLACK_COMMANDS_ROUTE_ID,
NetworkMethod::Post,
SLACK_COMMANDS_PATH,
slack_commands_policy(),
)
.expect("Slack commands route descriptor must validate at startup"); // safety: route id/path are crate-local literals and policy is built by sibling helper.
vec![descriptor]
}

fn slack_commands_policy() -> IngressPolicy {
IngressPolicy::new(IngressPolicyParts {
listener_class: ListenerClass::PublicWebhook,
auth: IngressAuthPolicy::Required {
schemes: vec![IngressAuthScheme::WebhookSignature],
},
scope_source: IngressScopeSource::HostResolved,
body_limit: BodyLimitPolicy::Limited {
max_bytes: SLACK_COMMANDS_BODY_LIMIT_BYTES,
},
rate_limit: RateLimitPolicy::Limited {
// Coarse pre-auth abuse guard, mirroring the events route. A second
// post-verification bucket keyed by the resolved installation is
// applied inside `SlackIngressService::resolve_command`.
scope: RateLimitScope::Global,
max_requests: SLACK_COMMANDS_MAX_REQUESTS,
window_seconds: SLACK_COMMANDS_RATE_WINDOW_SECONDS,
},
cors: CorsPolicy::NotApplicable,
websocket_origin: WebSocketOriginPolicy::NotApplicable,
streaming: StreamingMode::None,
audit: AuditTraceClass::PublicCallback,
effect_path: AllowedEffectPath::ProductWorkflow,
})
.expect("Slack commands ingress policy must validate") // safety: policy combines validated constants and host-resolved webhook-signature scope.
vec![bundled_slack_ingress_descriptor(SLACK_COMMANDS_ROUTE_ID)]
}

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.

medium

To avoid redundant parsing of the Slack manifest TOML on every call to slack_commands_route_descriptors, we should cache the projected descriptor using std::sync::LazyLock. This aligns with the general rule to avoid redundant parsing of configuration files by projecting them exactly once.

pub fn slack_commands_route_descriptors() -> Vec<IngressRouteDescriptor> {
    static DESCRIPTOR: std::sync::LazyLock<IngressRouteDescriptor> = std::sync::LazyLock::new(|| {
        bundled_slack_ingress_descriptor(SLACK_COMMANDS_ROUTE_ID)
    });
    vec![DESCRIPTOR.clone()]
}
References
  1. Avoid redundant parsing and iteration of configuration files (e.g., TOML manifests) by projecting them into typed entries exactly once, validating the set, and replaying the pre-parsed entries.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed in fd152f9 (prior commit) and consolidated in 27731cb: both the events and commands descriptors are now projected once, from a single manifest parse, in one LazyLock.

@railway-app

railway-app Bot commented Jul 4, 2026 •

Copy link
Copy Markdown

🚅 Deployed to the ironclaw-pr-5626 environment in ironclaw-ci-preview

Service Status Web Updated (UTC)
ironclaw ✅ Success (View Logs) Web Jul 5, 2026 at 1:06 am

ilblackdragon and others added 3 commits July 4, 2026 00:45
…obustness + type nits

Addresses bot review feedback on #5625:
- gemini (security): a public (no-auth) host-ingress route declaring a
  credential handle is incoherent and misleading (a reader would assume it is
  authenticated). Add PublicIngressRouteHasCredential and reject it, making the
  coherence rule symmetric with the auth-required-needs-credential rule. Adds
  two unit tests (rejection + the valid no-credential complement).
- gemini (types): store &IngressRouteId in the route-id dedup set instead of
  downgrading to &str.
- copilot/coderabbit (tests): the fail-closed-floor wire test now accepts both
  RegistryError::Manifest(_) and ManifestSectionParse, so it pins the
  fail-closed behavior rather than the error-routing path.
- copilot (naming): documented on HostIngressRoute::credential_handles why the
  shared EgressCredentialHandle newtype is reused for ingress (type-placement
  rule) and that its Display leaks no "egress" wording.

cargo fmt + clippy (all-features, --tests) clean; registry tests green
(6 unit incl. the 2 new public-route cases + 9 manifest + 13 contract).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…the Rust policy literals

Makes the manifest-driven ingress contract from #5625 load-bearing: Slack's
two inbound routes are now declared as data in the bundled extension manifest
and projected into descriptors at serve time, instead of being hand-written
Rust policy literals. This is the real example of usage the mechanism needed.

- assets/slack/manifest.toml declares `[[product_adapter.inbound.host_ingress]]`
  for `slack.events` and `slack.commands`, each naming `slack_bot_token` as
  its verifying credential (fail-closed credential coherence, enforced by the
  registry).
- slack_serve::slack_events_route_descriptors / slack_commands_route_descriptors
  now project their descriptor from the bundled manifest via a new generic
  helper (composition::host_ingress::bundled_host_ingress_descriptor), and the
  two hardcoded slack_events_policy() / slack_commands_policy() literals are
  deleted.
- Only the declarative descriptor moved to the manifest. The axum handler and
  the HMAC verifier (behavior) stay in Rust — a manifest cannot carry behavior.

The descriptor is validated by ironclaw_host_api on deserialize (dotted route
id, absolute path, and the fail-closed floor that a public_webhook listener
MUST require webhook_signature) and by ironclaw_product_adapter_registry for
ingress credential coherence — so a manifest cannot declare a weaker route
than the Rust literal did.

Stacked on #5625 (needs ProductAdapterHostApiSection::host_ingress()). This
also answers the "unused API" question on #5625: the accessor now has a
production consumer that deletes per-channel Rust.

Tests: two behavior-preserving equivalence guards assert the manifest-projected
descriptor is byte-for-byte identical to the pre-migration literal
(slack_{events,commands}_route_descriptor_matches_manifest_projection); the
existing 377 Slack serve/e2e/handler tests still drive real signed and forged
webhooks through the projected mounts (all pass). fmt + clippy
(slack-v2-host-beta,webui-v2-beta,libsql; --tests) clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Addresses gemini review feedback on #5626: project each Slack route descriptor
from the bundled manifest exactly once (LazyLock) instead of re-parsing the
manifest TOML on every slack_{events,commands}_route_descriptors() call. The
manifest is a compile-time constant, so the projection is deterministic and
safe to memoize for the process lifetime.

Behavior-preserving: the equivalence guards and the full 377-test Slack serve
suite still pass; clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ilblackdragon
ilblackdragon force-pushed the feat/slack-manifest-ingress-projection branch from 82f5a58 to fd152f9 Compare July 4, 2026 00:49
@ironloopai

ironloopai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

IronLoop Review Status

Head: fd152f940babd8a4a82f969a967c86ff8af28eda
Updated: 2026-07-04T00:49:47.611Z
Admission: webhook accepted the request and IronLoop persisted review state before this projection.

Current reviewers:

Reviewer State What it means Last update
none Queued No reviewer jobs scheduled yet. n/a

Recent activity:

Time Reviewer State Detail
n/a n/a Waiting No progress events recorded yet.

Commands:

  • @ironloop review
  • @ironloop review <agent-alias>
  • @ironloop status

@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5626 July 4, 2026 00:49 Destroyed
@ilblackdragon

Copy link
Copy Markdown
Member Author

Thanks for the review — addressed (pushed in fd152f9; branch also rebased onto the updated #5625 tip):

  • gemini (perf): each Slack route descriptor is now projected from the bundled manifest exactly once via LazyLock, instead of re-parsing the manifest TOML on every slack_{events,commands}_route_descriptors() call. The manifest is a compile-time constant, so the projection is deterministic and safe to memoize for the process lifetime.

Behavior-preserving: the two equivalence guards and the full 377-test Slack serve/e2e/handler suite still pass; clippy clean.

Base automatically changed from feat/manifest-projected-ingress-descriptor to main July 4, 2026 00:54

@serrrfirat serrrfirat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Review summary for fd152f940babd8a4a82f969a967c86ff8af28eda:

  • Security: clean
  • Bugs: reviewer timed out; not included
  • Performance/concurrency: clean
  • Tests: 1 low-confidence gap worth adding
  • Conventions/API contract: 1 medium finding
  • Thermo-nuclear maintainability: 3 findings, with the two higher-signal ones inlined

The main issue is that the new manifest contract says credential_handles are the credentials that verify the ingress route, but the Slack runtime verifier is built from the Slack signing secret, not the bot token declared in the manifest.

# handler + HMAC verifier stay in Rust. Credential coherence: each route is
# verified by `slack_bot_token`, which is declared in required_credentials above.
[[product_adapter.inbound.host_ingress]]
credential_handles = ["slack_bot_token"]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The new contract says these credential_handles are the credentials that verify the ingress route, but Slack verification is not done with slack_bot_token. The runtime builds WebhookAuth::Hmac from config.signing_secret (crates/ironclaw_reborn_composition/src/slack_host_beta.rs:956-961), and setup stores that under a separate signing_secret_handle (slack_setup.rs:37-38, 304-309). With this manifest, future registry/installation checks will treat the outbound bot token as the inbound verifier even though the actual HMAC path uses the signing secret. Either declare the signing-secret credential handle here, or rename/soften this manifest field/docs so it represents required installation credentials rather than verifier credentials.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Good catch — fixed in 27731cb by declaring the real verifier. The manifest now declares slack_signing_secret in required_credentials and both routes' credential_handles name it instead of slack_bot_token (which stays as the egress credential only). This matches the runtime: WebhookAuth::Hmac is built from config.signing_secret. Safe for existing installs because registry binding validation is "bindings ⊆ declared" (validate_installation), so declaring an extra handle never invalidates an installation.

)
.expect("Slack events route descriptor must validate at startup"); // safety: route id/path are crate-local literals and policy is built by sibling helper.
vec![descriptor]
vec![SLACK_EVENTS_DESCRIPTOR.clone()]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This still leaves the mount path/method duplicated outside the manifest descriptor. The descriptor now owns route_pattern and method, but slack_events_route_mount still independently mounts SLACK_EVENTS_PATH with post(...) above, and the commands route repeats the same split. That means the manifest can drift from what Axum actually mounts, with equality tests catching drift after the fact instead of the construction making drift impossible. Consider projecting a typed Slack ingress route spec once and building both the router and descriptor list from that same descriptor, failing closed if the descriptor method is not POST.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 27731cb. Both mount functions now build their axum route from the projected descriptor (descriptor.route_pattern().as_str()), and the projection fails closed at startup if a route ever declares a non-POST method (bundled_slack_post_descriptor), since the handlers are wired with post(...). The SLACK_*_PATH consts remain only as the pub API other modules use for URL display, with the existing equality tests pinning const ↔ manifest — but what axum mounts now comes from the manifest descriptor, so mount/manifest drift is structurally impossible.

manifest_toml: &str,
route_id: &str,
) -> Result<IngressRouteDescriptor, HostIngressProjectionError> {
let record = parse_product_adapter_manifest_record(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This helper introduces a second manifest-ingestion path for serve descriptors: it reparses the bundled manifest with HostPortCatalog::empty() and only the product-adapter parser, then calls product_adapter_sections even though parse_product_adapter_manifest_record already validated those sections. That makes future manifest changes harder to reason about because bundled extension installation and serve-time descriptor projection can stop using the same parsing context. A cleaner shape would reuse the same parsed bundled package/manifest path as available_extensions, or add a registry API that returns the already-projected ProductAdapter sections and project all Slack ingress routes once.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 27731cb. host_ingress no longer parses with HostPortCatalog::empty() + a product-adapter-only contract registry — it now uses the exact same parsing context as bundled extension installation (default_host_port_catalog() + default_host_api_contract_registry() + from_toml_with_contracts, the same calls bundled_extension_package makes), so a bundled manifest cannot be installable yet fail serve-time projection or vice versa. It also projects all routes in one pass (bundled_host_ingress_descriptors) with a separate id selector, and slack_serve projects both Slack routes from one parse in a single LazyLock.

.flat_map(|section| section.host_ingress())
.find(|route| route.descriptor().route_id().as_str() == route_id)
.map(|route| route.descriptor().clone())
.ok_or_else(|| HostIngressProjectionError::RouteNotDeclared {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This new RouteNotDeclared branch is not directly tested. The Slack tests only request the two routes present in the manifest, and the registry tests cover manifest projection rather than this selector's missing-route behavior. Please add a focused test like host_ingress::tests::bundled_host_ingress_descriptor_rejects_missing_route_id that passes a valid bundled manifest but asks for an absent route id.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added in 27731cb: host_ingress::tests::bundled_host_ingress_descriptor_rejects_missing_route_id projects a valid bundled manifest and asserts an absent route id yields RouteNotDeclared (plus a happy-path projection test on the same fixture).

# verified by `slack_bot_token`, which is declared in required_credentials above.
[[product_adapter.inbound.host_ingress]]
credential_handles = ["slack_bot_token"]
descriptor = { route_id = "slack.events", method = "post", route_pattern = "/webhooks/slack/events", policy = { listener_class = "public_webhook", auth = { type = "required", schemes = ["webhook_signature"] }, scope_source = "host_resolved", body_limit = { type = "limited", max_bytes = 1048576 }, rate_limit = { type = "limited", scope = "global", max_requests = 12000, window_seconds = 60 }, cors = "not_applicable", websocket_origin = "not_applicable", streaming = "none", audit = "public_callback", effect_path = { type = "product_workflow" } } }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Now that the manifest is the canonical home for this route policy, the deeply nested one-line inline table is harder to review than the Rust literal it replaces. Future auth/body-limit/rate-limit/audit/effect-path changes will be buried in a long single-line diff. Please expand the descriptor into multiline TOML tables, or introduce a narrower manifest route shape that keeps each policy field reviewable.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed in 27731cb. Each descriptor and policy is now a multiline TOML table ([product_adapter.inbound.host_ingress.descriptor] / .policy), one field per line, so auth/body-limit/rate-limit/audit/effect-path changes diff line-by-line. Only the short leaf values (e.g. auth, body_limit) remain as one-line inline tables.

…gle manifest parse path

Address PR #5626 review feedback:

- manifest.toml: the ingress routes' credential_handles now name
  slack_signing_secret (declared in required_credentials) — the secret
  the runtime's HMAC webhook verifier is actually built from —
  instead of the outbound slack_bot_token. Descriptors expanded from
  one-line inline tables into multiline TOML tables so policy changes
  diff field-by-field.
- slack_serve: both descriptors project from one manifest parse via a
  single LazyLock, and the axum mounts now build their route path from
  the projected descriptor (failing closed at projection time if a
  route ever declares a non-POST method), so what axum mounts cannot
  drift from what the manifest declares.
- host_ingress: parsing reuses the same context as bundled extension
  installation (default host-port catalog + default host-API contract
  registry) instead of a second ingestion path with an empty catalog;
  projects all routes in one pass with a separate route-id selector.
- host_ingress tests: cover the happy projection and the previously
  untested RouteNotDeclared branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 5, 2026 01:00
@railway-app
railway-app Bot temporarily deployed to ironclaw-ci-preview / ironclaw-pr-5626 July 5, 2026 01:00 Destroyed
@github-actions github-actions Bot added scope: docs Documentation size: XL 500+ changed lines and removed size: L 200-499 changed lines labels Jul 5, 2026

Copilot AI 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.

Pull request overview

This PR makes Slack’s Reborn host-ingress routes manifest-driven: the serve layer now projects IngressRouteDescriptors from the bundled Slack extension manifest instead of maintaining hand-written Rust policy literals, while keeping the actual axum handlers + HMAC verification logic in Rust.

Changes:

  • Declare Slack’s slack.events / slack.commands host-ingress routes in the bundled Slack extension manifest and add the inbound verifying credential (slack_signing_secret).
  • Update slack_serve to mount routes using manifest-projected descriptors (via a cached, single-parse projection) and delete the old Rust policy literal builders.
  • Add a composition helper to project host-ingress descriptors from bundled manifests; extend registry parsing/validation + tests to support host_ingress routes with fail-closed credential coherence.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
crates/ironclaw_reborn_composition/src/slack_serve.rs Switch Slack route mounting/descriptors to use manifest-projected IngressRouteDescriptors; remove Rust policy literals; add migration-guard equality tests.
crates/ironclaw_reborn_composition/src/lib.rs Add the new host_ingress module behind slack-v2-host-beta.
crates/ironclaw_reborn_composition/src/host_ingress.rs New helper to parse bundled manifest TOML and project host-ingress descriptors; lookup helper by route id.
crates/ironclaw_reborn_composition/src/available_extensions.rs Expose Slack manifest TOML string for serve-time projection.
crates/ironclaw_product_adapter_registry/tests/manifest_ingestion.rs Add wire-path tests for parsing host_ingress, host_api’s fail-closed floor, and ingress credential coherence.
crates/ironclaw_product_adapter_registry/src/lib.rs Add HostIngressRoute, manifest parsing for host_ingress, and validation for ingress credential coherence + duplicate route ids (within a section).
crates/ironclaw_product_adapter_registry/CLAUDE.md Document the new manifest host_ingress contract and its validation responsibilities.
crates/ironclaw_first_party_extensions/assets/slack/manifest.toml Declare Slack host-ingress routes and add slack_signing_secret as a required credential used to verify inbound routes.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +48 to +50
[[product_adapter.inbound.host_ingress]]
credential_handles = ["slack_signing_secret"]

Comment on lines +65 to +76
pub(crate) fn descriptor_for_route(
descriptors: &[IngressRouteDescriptor],
route_id: &str,
) -> Result<IngressRouteDescriptor, HostIngressProjectionError> {
descriptors
.iter()
.find(|descriptor| descriptor.route_id().as_str() == route_id)
.cloned()
.ok_or_else(|| HostIngressProjectionError::RouteNotDeclared {
route_id: route_id.to_string(),
})
}
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

⚠️ 14 Reborn crate(s) have 0 int-tier coverage (target: 0) — ironclaw_embeddings, ironclaw_event_projections, ironclaw_event_streams, ironclaw_gateway, ironclaw_hooks, ironclaw_oauth, ironclaw_process_sandbox, ironclaw_prompt_envelope, ironclaw_reborn_identity, ironclaw_reborn_traces, ironclaw_scripts, ironclaw_skill_learning, ironclaw_tui, ironclaw_webui_v2

Reborn integration-tier coverage

Line coverage (Reborn crates): 26.09% — 45017 / 172537 lines

Per-crate breakdown (62 crates, lowest-covered first)
Crate Line % Covered / Total
ironclaw_embeddings 0% 0 / 337
ironclaw_event_projections 0% 0 / 1489
ironclaw_event_streams 0% 0 / 1034
ironclaw_gateway 0% 0 / 283
ironclaw_hooks 0% 0 / 4468
ironclaw_oauth 0% 0 / 155
ironclaw_process_sandbox 0% 0 / 795
ironclaw_prompt_envelope 0% 0 / 88
ironclaw_reborn_identity 0% 0 / 220
ironclaw_reborn_traces 0% 0 / 6420
ironclaw_scripts 0% 0 / 347
ironclaw_skill_learning 0% 0 / 61
ironclaw_tui 0% 0 / 4776
ironclaw_webui_v2 0% 0 / 2683
ironclaw_outbound 0.22% 3 / 1339
ironclaw_reborn_event_store 0.66% 6 / 906
ironclaw_reborn_config 1.36% 15 / 1101
ironclaw_llm 3.1% 374 / 12075
ironclaw_product_adapter_registry 5.38% 25 / 465
ironclaw_extractors 6.18% 26 / 421
ironclaw_product_workflow 7.23% 697 / 9635
ironclaw_wasm_sandbox_core 7.37% 7 / 95
ironclaw_common 8.43% 61 / 724
ironclaw_processes 8.61% 98 / 1138
ironclaw_events 12.18% 140 / 1149
ironclaw_product_adapters 12.53% 280 / 2234
ironclaw_network 13.25% 66 / 498
ironclaw_skills 14.58% 377 / 2585
ironclaw_first_party_extensions 22.46% 1125 / 5010
ironclaw_triggers 22.82% 655 / 2870
ironclaw_reborn 22.98% 2009 / 8742
ironclaw_reborn_composition 25.53% 7776 / 30461
ironclaw_secrets 26.22% 450 / 1716
ironclaw_capabilities 32.97% 580 / 1759
ironclaw_auth 33.09% 667 / 2016
ironclaw_runtime_policy 33.2% 80 / 241
ironclaw_memory_native 37.02% 857 / 2315
ironclaw_host_api 39.77% 939 / 2361
ironclaw_filesystem 40.08% 1400 / 3493
ironclaw_host_runtime 40.48% 6068 / 14989
ironclaw_threads 41.98% 1326 / 3159
ironclaw_trust 42.56% 326 / 766
ironclaw_loop_support 42.61% 3141 / 7372
ironclaw_memory 47.47% 357 / 752
ironclaw_first_party_extension_ports 47.97% 627 / 1307
ironclaw_wasm 48.79% 363 / 744
ironclaw_projects 50% 147 / 294
ironclaw_extensions 51.19% 1206 / 2356
ironclaw_agent_loop 51.48% 2399 / 4660
ironclaw_resources 51.49% 1106 / 2148
ironclaw_run_state 52.73% 222 / 421
ironclaw_authorization 53.54% 461 / 861
ironclaw_turns 57.71% 5199 / 9009
ironclaw_safety 58.98% 1028 / 1743
ironclaw_observability 61.54% 16 / 26
ironclaw_conversations 66.13% 937 / 1417
ironclaw_approvals 66.63% 549 / 824
ironclaw_dispatcher 67.15% 92 / 137
ironclaw_mcp 67.42% 569 / 844
ironclaw_wasm_limiter 74.6% 47 / 63
ironclaw_product_context 78.57% 11 / 14
ironclaw_attachments 84.92% 107 / 126

This signal is informational: coverage never gates the PR — not the percentage, not the per-crate holes, not the 0-coverage callout.

Exemptions (0 file(s) excluded from the accounting above)

No exemptions configured.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
crates/ironclaw_reborn_composition/src/host_ingress.rs (1)

42-62: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Second manifest-ingestion path for serve descriptors persists.

This still reparses the bundled manifest independently of available_extensions::bundled_extension_package, rather than reusing the already-projected ProductAdapterHostApiSections from bundled installation. The doc comment (lines 10-14) explains the parsing context now matches (default catalog/contracts), but the manifest is still parsed twice at different times/call sites, so a future context divergence between the two paths could silently reintroduce drift.

A prior reviewer suggested reusing the same parsed bundled package path, or adding a registry API that returns already-projected sections once.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/host_ingress.rs` around lines 42 - 62,
The bundled host ingress path still reparses the manifest instead of reusing the
already-projected bundled package data. Update bundled_host_ingress_descriptors
to consume the projected ProductAdapterHostApiSection values from the same
bundled-installation flow used by
available_extensions::bundled_extension_package, or add a registry helper that
returns those projected sections directly. Keep the existing ingress flattening
logic, but avoid calling ExtensionManifestRecord::from_toml_with_contracts and
product_adapter_sections a second time.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@crates/ironclaw_reborn_composition/src/host_ingress.rs`:
- Around line 42-62: The bundled host ingress path still reparses the manifest
instead of reusing the already-projected bundled package data. Update
bundled_host_ingress_descriptors to consume the projected
ProductAdapterHostApiSection values from the same bundled-installation flow used
by available_extensions::bundled_extension_package, or add a registry helper
that returns those projected sections directly. Keep the existing ingress
flattening logic, but avoid calling
ExtensionManifestRecord::from_toml_with_contracts and product_adapter_sections a
second time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: d67af8af-d0ae-4109-a252-410069b5fd89

📥 Commits

Reviewing files that changed from the base of the PR and between 920d9de and 27731cb.

📒 Files selected for processing (8)
  • crates/ironclaw_first_party_extensions/assets/slack/manifest.toml
  • crates/ironclaw_product_adapter_registry/CLAUDE.md
  • crates/ironclaw_product_adapter_registry/src/lib.rs
  • crates/ironclaw_product_adapter_registry/tests/manifest_ingestion.rs
  • crates/ironclaw_reborn_composition/src/available_extensions.rs
  • crates/ironclaw_reborn_composition/src/host_ingress.rs
  • crates/ironclaw_reborn_composition/src/lib.rs
  • crates/ironclaw_reborn_composition/src/slack_serve.rs

@ilblackdragon
ilblackdragon merged commit 771c1fe into main Jul 5, 2026
61 checks passed
@ilblackdragon
ilblackdragon deleted the feat/slack-manifest-ingress-projection branch July 5, 2026 21:22

This branch was successfully deployed

No deployments
ironclaw-ci-preview / ironclaw-pr-5626 — 27731cb6 Deployed Jul 5, 2026 by railway-app[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor: core 20+ merged PRs risk: low Changes to docs, tests, or low-risk modules scope: docs Documentation size: XL 500+ changed lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants